├── .php_cs.dist.php ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── psalm.xml.dist └── src ├── AbbreviationParser.php ├── Attributes.php ├── Helpers └── Arr.php ├── HtmlElement.php └── TagRenderer.php /.php_cs.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config()) 14 | ->setRules([ 15 | '@PSR12' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 18 | 'no_unused_imports' => true, 19 | 'not_operator_with_successor_space' => true, 20 | 'trailing_comma_in_multiline' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'class_attributes_separation' => [ 30 | 'elements' => [ 31 | 'method' => 'one', 32 | ], 33 | ], 34 | 'method_argument_space' => [ 35 | 'on_multiline' => 'ensure_fully_multiline', 36 | 'keep_multiple_spaces_after_comma' => true, 37 | ], 38 | 'single_trait_insert_per_statement' => true, 39 | ]) 40 | ->setFinder($finder); 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All Notable changes to `spatie/html-element` will be documented in this file 4 | 5 | ## 1.1.4 - 2022-01-09 6 | 7 | - fix release 8 | 9 | ## 1.1.3 - 2022-01-09 10 | 11 | - Allow PHP 8 12 | 13 | ## 1.1.1 - 2018-12-26 14 | 15 | - Fixed: Escape attribute values 16 | 17 | ## 1.1.0 - 2018-11-21 18 | 19 | - Added: Implicit `div` tags like Emmet (e.g. `.foo` -> `
`) 20 | 21 | ## 1.0.1 - 2017-03-13 22 | 23 | - Fixed: Allow `0` as an attribute value 24 | 25 | ## 1.0.0 - 2016-03-11 26 | 27 | - First release 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) Spatie bvba 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [](https://supportukrainenow.org) 3 | 4 | # HtmlElement 5 | 6 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/html-element.svg?style=flat-square)](https://packagist.org/packages/spatie/html-element) 7 | [![Tests](https://github.com/spatie/html-element/actions/workflows/run-tests.yml/badge.svg)](https://github.com/spatie/html-element/actions/workflows/run-tests.yml) 8 | [![GitHub Code Style Action Status](https://img.shields.io/github/workflow/status/spatie/html-element/Check%20&%20fix%20styling?label=code%20style)](https://github.com/spatie/html-element/actions?query=workflow%3A"Check+%26+fix+styling"+branch%3Amaster) 9 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/html-element.svg?style=flat-square)](https://packagist.org/packages/spatie/html-element) 10 | 11 | HtmlElement is a library to make dynamic HTML rendering more managable. The syntax is based on [Hyperscript](https://github.com/dominictarr/hyperscript), and adds some [Emmet](http://emmet.io/)-style syntactic sugar too. 12 | 13 | Elements are rendered using the static `HtmlElement::render` method (which I recommend wrapping in a plain function for readability). 14 | 15 | ```php 16 | el('div.container > div.row > div.col-md-6', 17 | el('a', ['href' => '#'], 'Hello world!') 18 | ); 19 | ``` 20 | ```html 21 |
22 |
23 |
24 | Hello world! 25 |
26 |
27 |
28 | ``` 29 | 30 | ## Support us 31 | 32 | [](https://spatie.be/github-ad-click/html-element) 33 | 34 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 35 | 36 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 37 | 38 | ## Postcardware 39 | 40 | You're free to use this package (it's [MIT-licensed](LICENSE.md)), but if it makes it to your production environment you are required to send us a postcard from your hometown, mentioning which of our package(s) you are using. 41 | 42 | Our address is: Spatie, Kruikstraat 22, 2018 Antwerp, Belgium. 43 | 44 | The best postcards will get published on the open source page on our website. 45 | 46 | ## Usage 47 | 48 | I recommend adding an `el` function to your application to improve readability over the static method. 49 | 50 | ```php 51 | function el(string $tag, $attributes = null, $content = null) : string 52 | { 53 | return \Spatie\HtmlElement\HtmlElement::render($tag, $attributes, $content); 54 | } 55 | ``` 56 | 57 | ## Examples 58 | 59 | An empty tag: 60 | 61 | ```php 62 | el('div'); 63 | ``` 64 | ```html 65 |
66 | ``` 67 | 68 | A plain tag with text contents: 69 | 70 | ```php 71 | el('p', 'Hello world!'); 72 | ``` 73 | ```html 74 |

Hello world!

75 | ``` 76 | 77 | A tag with an attribute: 78 | 79 | ```php 80 | el('p', ['style' => 'color: red;'], 'Hello world!'); 81 | ``` 82 | ```html 83 |

Hello world!

84 | ``` 85 | 86 | A tag with an ID set emmet-style: 87 | 88 | ```php 89 | el('p#introduction', 'Hello world!'); 90 | ``` 91 | ```html 92 |

Hello world!

93 | ``` 94 | 95 | A tag with an emmet-style ID and class: 96 | 97 | ```php 98 | el('p#introduction.red', 'Hello world!'); 99 | ``` 100 | ```html 101 |

Hello world!

102 | ``` 103 | 104 | A tag with emmet-style attributes: 105 | 106 | ```php 107 | el('a[href=#][title=Back to top]', 'Back to top'); 108 | ``` 109 | ```html 110 | Back to top 111 | ``` 112 | 113 | A more complex emmet-style abbreviation: 114 | 115 | ```php 116 | el('div.container > div.row > div.col-md-6', 'Hello world!'); 117 | ``` 118 | ```html 119 |
120 |
121 |
122 | Hello world! 123 |
124 |
125 |
126 | ``` 127 | 128 | Limited support of [implicit tag names](https://docs.emmet.io/abbreviations/implicit-names/) (`div`s only): 129 | 130 | ```php 131 | el('.container > .row > .col-md-6', 'Hello world!'); 132 | ``` 133 | ```html 134 |
135 |
136 |
137 | Hello world! 138 |
139 |
140 |
141 | ``` 142 | 143 | Manually nested tags: 144 | 145 | ```php 146 | el('div', ['class' => 'container'], 147 | el('nav', ['aria-role' => 'navigation'], '...') 148 | ); 149 | ``` 150 | ```html 151 |
152 | 153 |
154 | ``` 155 | 156 | Multiple children: 157 | 158 | ```php 159 | el('ul', [el('li'), el('li')]); 160 | ``` 161 | ```html 162 | 166 | ``` 167 | 168 | Self-closing tags: 169 | 170 | ```php 171 | el('img[src=/background.jpg]'); 172 | ``` 173 | ```html 174 | 175 | ``` 176 | 177 | ```php 178 | el('img', ['src' => '/background.jpg'], ''); 179 | ``` 180 | ```html 181 | 182 | ``` 183 | 184 | ## Arguments 185 | 186 | The `el` function behaves differently depending on how many arguments are passed in. 187 | 188 | #### `el(string $tag) : string` 189 | 190 | When one argument is passed, only a tag will be rendered. 191 | 192 | ```php 193 | el('p'); 194 | ``` 195 | ```html 196 |

197 | ``` 198 | 199 | --- 200 | 201 | #### `el(string $tag, string|array $contents) : string` 202 | 203 | When two arguments are passed, they represent a tag and it's contents. 204 | 205 | String example: 206 | 207 | ```php 208 | el('p', 'Hello world!'); 209 | ``` 210 | ```html 211 |

Hello world!

212 | ``` 213 | 214 | Array example: 215 | 216 | ```php 217 | el('ul', [el('li'), el('li')]); 218 | ``` 219 | ```html 220 | 224 | ``` 225 | 226 | --- 227 | 228 | #### `el(string $tag, array $attributes, string|array $contents) : string` 229 | 230 | When three arguments are passed, the first will be the tag name, the second an array of attributes, and the third a string or an array of contents. 231 | 232 | ```php 233 | el('div', ['class' => 'container'], 234 | el('nav', ['aria-role' => 'navigation'], '...') 235 | ); 236 | ``` 237 | ```html 238 |
239 | 240 |
241 | ``` 242 | 243 | ## Changelog 244 | 245 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 246 | 247 | ## Testing 248 | 249 | ``` bash 250 | $ composer test 251 | ``` 252 | 253 | ## Contributing 254 | 255 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 256 | 257 | ## Security 258 | 259 | If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. 260 | 261 | ## Credits 262 | 263 | - [Sebastian De Deyne](https://github.com/sebastiandedeyne) 264 | - [All Contributors](../../contributors) 265 | 266 | ## About Spatie 267 | Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). 268 | 269 | ## License 270 | 271 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 272 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/html-element", 3 | "description": "Html rendering in php inspired by hyperscript", 4 | "keywords": [ 5 | "spatie", 6 | "html-element" 7 | ], 8 | "homepage": "https://github.com/spatie/html-element", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Sebastian De Deyne", 13 | "email": "sebastian@spatie.be", 14 | "homepage": "https://spatie.be", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^7.3|^8.0" 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "^9.5", 23 | "dms/phpunit-arraysubset-asserts": "^0.3.1", 24 | "vimeo/psalm": "^4.13" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Spatie\\HtmlElement\\": "src" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "Spatie\\HtmlElement\\Test\\": "tests" 34 | } 35 | }, 36 | "scripts": { 37 | "test": "vendor/bin/phpunit" 38 | } 39 | } -------------------------------------------------------------------------------- /psalm.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/AbbreviationParser.php: -------------------------------------------------------------------------------- 1 | $parsed->element, 24 | 'classes' => $parsed->classes, 25 | 'attributes' => $parsed->attributes, 26 | ]; 27 | } 28 | 29 | protected function __construct(string $tag) 30 | { 31 | $this->parseTag($tag); 32 | } 33 | 34 | protected function parseTag(string $tag) 35 | { 36 | foreach ($this->explodeTag($tag) as $part) { 37 | switch ($part[0] ?? '') { 38 | case '.': 39 | $this->parseClass($part); 40 | 41 | break; 42 | case '#': 43 | $this->parseId($part); 44 | 45 | break; 46 | case '[': 47 | $this->parseAttribute($part); 48 | 49 | break; 50 | default: 51 | $this->parseElement($part); 52 | 53 | break; 54 | } 55 | } 56 | } 57 | 58 | protected function parseClass(string $class) 59 | { 60 | $this->classes[] = ltrim($class, '.'); 61 | } 62 | 63 | protected function parseId(string $id) 64 | { 65 | $this->attributes['id'] = ltrim($id, '#'); 66 | } 67 | 68 | protected function parseAttribute(string $attribute) 69 | { 70 | $keyValueSet = explode('=', trim($attribute, '[]'), 2); 71 | 72 | $key = $keyValueSet[0]; 73 | $value = $keyValueSet[1] ?? null; 74 | 75 | $this->attributes[$key] = trim($value, '\'"'); 76 | } 77 | 78 | protected function parseElement(string $element) 79 | { 80 | $this->element = $element; 81 | } 82 | 83 | protected function explodeTag(string $tag): array 84 | { 85 | // First split out the attributes set with `[...=...]` 86 | $parts = preg_split('/(?=( \[[^]]+] ))/x', $tag); 87 | 88 | // Afterwards we can extract the rest of the attributes 89 | return Arr::flatMap($parts, function ($part) { 90 | if (strpos($part, '[') === 0) { 91 | list($attributeValue, $rest) = explode(']', $part, 2); 92 | 93 | return [$attributeValue] + $this->explodeTag($rest); 94 | } 95 | 96 | return preg_split('/(?=( (\.) | (\#) ))/x', $part); 97 | }); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Attributes.php: -------------------------------------------------------------------------------- 1 | setAttributes($attributes); 16 | } 17 | 18 | /** 19 | * @param array $attributes 20 | * 21 | * @return $this 22 | */ 23 | public function setAttributes(array $attributes) 24 | { 25 | foreach ($attributes as $attribute => $value) { 26 | if ($attribute === 'class') { 27 | $this->addClass($value); 28 | 29 | continue; 30 | } 31 | 32 | if (is_int($attribute)) { 33 | $attribute = $value; 34 | $value = ''; 35 | } 36 | 37 | $this->setAttribute($attribute, $value); 38 | } 39 | 40 | return $this; 41 | } 42 | 43 | /** 44 | * @param string $attribute 45 | * @param string $value 46 | * 47 | * @return $this 48 | */ 49 | public function setAttribute(string $attribute, string $value = '') 50 | { 51 | if ($attribute === 'class') { 52 | $this->addClass($value); 53 | 54 | return $this; 55 | } 56 | 57 | $this->attributes[$attribute] = $value; 58 | 59 | return $this; 60 | } 61 | 62 | /** 63 | * @param string|array $class 64 | * 65 | * @return $this 66 | */ 67 | public function addClass($class) 68 | { 69 | if (! is_array($class)) { 70 | $class = [$class]; 71 | } 72 | 73 | $this->classes = array_unique( 74 | array_merge($this->classes, $class) 75 | ); 76 | 77 | return $this; 78 | } 79 | 80 | public function isEmpty(): bool 81 | { 82 | return empty($this->attributes) && empty($this->classes); 83 | } 84 | 85 | public function toArray(): array 86 | { 87 | if (empty($this->classes)) { 88 | return $this->attributes; 89 | } 90 | 91 | return array_merge($this->attributes, ['class' => implode(' ', $this->classes)]); 92 | } 93 | 94 | public function toString(): string 95 | { 96 | if ($this->isEmpty()) { 97 | return ''; 98 | } 99 | 100 | $attributeStrings = []; 101 | 102 | foreach ($this->toArray() as $attribute => $value) { 103 | if (is_null($value) || $value === '') { 104 | $attributeStrings[] = $attribute; 105 | 106 | continue; 107 | } 108 | $value = htmlspecialchars($value, ENT_COMPAT); 109 | 110 | $attributeStrings[] = "{$attribute}=\"{$value}\""; 111 | } 112 | 113 | return implode(' ', $attributeStrings); 114 | } 115 | 116 | public function __toString(): string 117 | { 118 | return $this->toString(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Helpers/Arr.php: -------------------------------------------------------------------------------- 1 |

20 | * el('p', 'Hello!')

Hello!

21 | * el('p#intro', 'Hello!')

Hello!

22 | * el('p', ['id' => 'intro'], 'Hello!')

Hello!

23 | * 24 | * 25 | * @param string $tag The html element tag. 26 | * @param array|string $attributes When only two arguments are passed, the second parameter 27 | * represents the content instead of the attribute. 28 | * @param array|string $contents Contents can be passed in as a string or an array which 29 | * will be concatenated as siblings. 30 | * 31 | * @return string 32 | */ 33 | public static function render(string $tag, $attributes = null, $contents = null): string 34 | { 35 | return (new static($tag, $attributes, $contents))->renderTag(); 36 | } 37 | 38 | protected function __construct(...$arguments) 39 | { 40 | list($abbreviation, $attributes, $contents) = $this->parseArguments($arguments); 41 | 42 | $this->attributes = new Attributes(); 43 | 44 | $this->parseContents($contents); 45 | $this->parseAbbreviation($abbreviation); 46 | $this->parseAttributes($attributes); 47 | } 48 | 49 | protected function parseArguments($arguments) 50 | { 51 | $attributes = isset($arguments[2]) ? $arguments[1] : []; 52 | $contents = $arguments[2] ?? $arguments[1] ?? ''; 53 | 54 | $tags = preg_split('/ \s* > \s* /x', $arguments[0], 2); 55 | 56 | if (isset($tags[1])) { 57 | $contents = static::render($tags[1], [], $contents); 58 | } 59 | 60 | return [$tags[0], $attributes, $contents]; 61 | } 62 | 63 | protected function parseContents($contents) 64 | { 65 | $this->contents = is_array($contents) ? implode('', $contents) : $contents; 66 | } 67 | 68 | protected function parseAbbreviation(string $abbreviation) 69 | { 70 | $parsed = AbbreviationParser::parse($abbreviation); 71 | 72 | $this->element = $parsed['element'] ?: 'div'; 73 | 74 | $this->attributes->addClass($parsed['classes']); 75 | 76 | foreach ($parsed['attributes'] as $attribute => $value) { 77 | $this->attributes->setAttribute($attribute, $value); 78 | } 79 | } 80 | 81 | protected function parseAttributes(array $attributes) 82 | { 83 | $this->attributes->setAttributes($attributes); 84 | } 85 | 86 | protected function renderTag(): string 87 | { 88 | return TagRenderer::render($this->element, $this->attributes, $this->contents); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/TagRenderer.php: -------------------------------------------------------------------------------- 1 | renderTag(); 19 | } 20 | 21 | protected function __construct(string $element, Attributes $attributes, string $contents) 22 | { 23 | $this->element = $element; 24 | $this->attributes = $attributes; 25 | $this->contents = $contents; 26 | } 27 | 28 | protected function renderTag(): string 29 | { 30 | if ($this->isSelfClosingTag()) { 31 | return $this->renderOpeningTag(); 32 | } 33 | 34 | return "{$this->renderOpeningTag()}{$this->contents}{$this->renderClosingTag()}"; 35 | } 36 | 37 | protected function renderOpeningTag(): string 38 | { 39 | return $this->attributes->isEmpty() ? 40 | "<{$this->element}>" : 41 | "<{$this->element} {$this->attributes}>"; 42 | } 43 | 44 | protected function renderClosingTag(): string 45 | { 46 | return "element}>"; 47 | } 48 | 49 | protected function isSelfClosingTag(): bool 50 | { 51 | return in_array(strtolower($this->element), [ 52 | 'area', 'base', 'br', 'col', 'embed', 'hr', 53 | 'img', 'input', 'keygen', 'link', 'menuitem', 54 | 'meta', 'param', 'source', 'track', 'wbr', 55 | ]); 56 | } 57 | } 58 | --------------------------------------------------------------------------------