Skip to content

Commit 84c2fc2

Browse files
committed
feat: allow attributes string as constructor argument
1 parent 3cd7737 commit 84c2fc2

File tree

3 files changed

+108
-3
lines changed

3 files changed

+108
-3
lines changed

README.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,24 @@ You can also use named arguments if you prefer a leaner syntax. Be aware, that t
4343
) ?>>
4444
```
4545

46-
⚠️ If you need XML-compatible attributes, always call `$attributes->toXml()` instead of just echoing the `Attributes` object,
47-
because otherwise all attributes will be converted to lower-case.
46+
Or if all you have is an attributes string, you can also feed the that to the `attributes()` helper:
47+
48+
```php
49+
<?php
50+
51+
// get image dimensions as height="yyy" width="xxx"
52+
$src = 'img.png';
53+
$size = getimagesize($src)[3];
54+
55+
?>
56+
57+
<img <?= attributes($size)->merge([
58+
'src' => $src,
59+
'alt' => '',
60+
]) ?>>
61+
```
62+
63+
⚠️ If you need XML-compatible attributes, always call `$attributes->toXml()` instead of just echoing the `Attributes` object, because otherwise all attributes will be converted to lower-case.
4864

4965
In many cases, you need to set different classes. The `classes()` helper is a nice shortcut for improved readability:
5066

src/Attributes.php

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace FabianMichael\TemplateAttributes;
44

55
use ArrayAccess;
6+
use DOMDocument;
67
use Exception;
78
use Kirby\Toolkit\A;
89
use Kirby\Toolkit\Html;
@@ -19,6 +20,38 @@ class Attributes implements ArrayAccess, Stringable
1920
protected ?string $before = null;
2021
protected ?string $after = null;
2122

23+
// see https://html.spec.whatwg.org/multipage/indices.html#attributes-3
24+
public static array $booleanAttributes = [
25+
'allowfullscreen',
26+
'async',
27+
'autofocus',
28+
'autoplay',
29+
'checked',
30+
'controls',
31+
'default',
32+
'defer',
33+
'disabled',
34+
'formnovalidate',
35+
'hidden', // not a boolean attribute, but can be used like one
36+
'inert',
37+
'ismap',
38+
'itemscope',
39+
'loop',
40+
'multiple',
41+
'muted',
42+
'nomodule',
43+
'novalidate',
44+
'open',
45+
'playsinline',
46+
'readonly',
47+
'required',
48+
'reversed',
49+
'selected',
50+
'shadowrootclonable',
51+
'shadowrootdelegatesfocus',
52+
'shadowrootserializable',
53+
];
54+
2255
public function __construct(...$data)
2356
{
2457
$this->merge(...$data);
@@ -81,14 +114,19 @@ public function get(?string $name = null): AttributeValue|array|null
81114
public function merge(...$data): static
82115
{
83116
if (count($data) === 1 && array_key_first($data) === 0) {
84-
// Single array/object input
117+
// single array/object input
85118
$data = $data[0];
86119
}
87120

88121
if (is_a($data, self::class)) {
122+
// argument was another instance of Attributes
89123
$data = $data->data;
90124
}
91125

126+
if (is_string($data)) {
127+
$data = static::parseAttributesString($data);
128+
}
129+
92130
foreach ($data as $name => $value) {
93131
$this->set($name, $value);
94132
}
@@ -236,4 +274,32 @@ public function __toString(): string
236274
{
237275
return $this->toHtml() ?? '';
238276
}
277+
278+
protected static function parseAttributesString(string $str): array
279+
{
280+
$tagName = 'yolo';
281+
$errorsBefore = libxml_use_internal_errors();
282+
$attr = [];
283+
284+
libxml_use_internal_errors(true);
285+
$dom = new DOMDocument();
286+
$dom->loadHTML("<{$tagName} {$str}>");
287+
$node = $dom->getElementsByTagName($tagName)->item(0);
288+
if ($node->hasAttributes()) {
289+
foreach ($node->attributes as $attribute) {
290+
$name = $attribute->nodeName;
291+
$value = $attribute->nodeValue;
292+
293+
if (in_array($value, ['', $name]) && in_array($name, static::$booleanAttributes)) {
294+
// convert known boolean attributes to bool
295+
$value = true;
296+
}
297+
298+
$attr[$name] = $value;
299+
}
300+
}
301+
302+
libxml_use_internal_errors($errorsBefore);
303+
return $attr;
304+
}
239305
}

tests/AttributesTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ public function testConstructorArray(): void
1919
$this->assertSame((string) $attr, 'bar="foo" foo="bar"');
2020
}
2121

22+
public function testConstructorSingleNamedArgument(): void
23+
{
24+
$attr = new Attributes(
25+
bar: 'foo',
26+
);
27+
28+
$this->assertSame((string) $attr, 'bar="foo"');
29+
}
30+
2231
public function testConstrucorNamedArguments(): void
2332
{
2433
$attr = new Attributes(
@@ -29,6 +38,20 @@ public function testConstrucorNamedArguments(): void
2938
$this->assertSame((string) $attr, 'bar="foo" foo="bar"');
3039
}
3140

41+
public function testConstructFromAttributesString(): void
42+
{
43+
$attr = new Attributes('foo="bar" bar="foo"');
44+
45+
$this->assertSame((string) $attr, 'bar="foo" foo="bar"');
46+
}
47+
48+
public function testConstructFromAttributesStringWithBooleanAttributes(): void
49+
{
50+
$attr = new Attributes('novalidate inert unknown-boolean');
51+
52+
$this->assertSame((string) $attr, 'inert novalidate unknown-boolean=""');
53+
}
54+
3255
public function testConversionToLowerCase(): void
3356
{
3457
$attr = new Attributes(

0 commit comments

Comments
 (0)