├── CHANGELOG.md
├── psalm.xml.dist
├── src
├── Helpers
│ └── Arr.php
├── TagRenderer.php
├── AbbreviationParser.php
├── Attributes.php
└── HtmlElement.php
├── composer.json
├── LICENSE.md
├── .php_cs.dist.php
└── README.md
/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 |
--------------------------------------------------------------------------------
/psalm.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/Helpers/Arr.php:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 "{$this->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 |
--------------------------------------------------------------------------------
/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/HtmlElement.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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | [
](https://supportukrainenow.org)
3 |
4 | # HtmlElement
5 |
6 | [](https://packagist.org/packages/spatie/html-element)
7 | [](https://github.com/spatie/html-element/actions/workflows/run-tests.yml)
8 | [](https://github.com/spatie/html-element/actions?query=workflow%3A"Check+%26+fix+styling"+branch%3Amaster)
9 | [](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 |
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 |
--------------------------------------------------------------------------------