Skip to content

Commit

Permalink
feat: allow attributes string as constructor argument
Browse files Browse the repository at this point in the history
  • Loading branch information
fabianmichael committed Jul 3, 2024
1 parent 3cd7737 commit 84c2fc2
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 3 deletions.
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,24 @@ You can also use named arguments if you prefer a leaner syntax. Be aware, that t
) ?>>
```

⚠️ 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.
Or if all you have is an attributes string, you can also feed the that to the `attributes()` helper:

```php
<?php

// get image dimensions as height="yyy" width="xxx"
$src = 'img.png';
$size = getimagesize($src)[3];

?>

<img <?= attributes($size)->merge([
'src' => $src,
'alt' => '',
]) ?>>
```

⚠️ 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.

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

Expand Down
68 changes: 67 additions & 1 deletion src/Attributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace FabianMichael\TemplateAttributes;

use ArrayAccess;
use DOMDocument;
use Exception;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Html;
Expand All @@ -19,6 +20,38 @@ class Attributes implements ArrayAccess, Stringable
protected ?string $before = null;
protected ?string $after = null;

// see https://html.spec.whatwg.org/multipage/indices.html#attributes-3
public static array $booleanAttributes = [
'allowfullscreen',
'async',
'autofocus',
'autoplay',
'checked',
'controls',
'default',
'defer',
'disabled',
'formnovalidate',
'hidden', // not a boolean attribute, but can be used like one
'inert',
'ismap',
'itemscope',
'loop',
'multiple',
'muted',
'nomodule',
'novalidate',
'open',
'playsinline',
'readonly',
'required',
'reversed',
'selected',
'shadowrootclonable',
'shadowrootdelegatesfocus',
'shadowrootserializable',
];

public function __construct(...$data)
{
$this->merge(...$data);
Expand Down Expand Up @@ -81,14 +114,19 @@ public function get(?string $name = null): AttributeValue|array|null
public function merge(...$data): static
{
if (count($data) === 1 && array_key_first($data) === 0) {
// Single array/object input
// single array/object input
$data = $data[0];
}

if (is_a($data, self::class)) {
// argument was another instance of Attributes
$data = $data->data;
}

if (is_string($data)) {
$data = static::parseAttributesString($data);
}

foreach ($data as $name => $value) {
$this->set($name, $value);
}
Expand Down Expand Up @@ -236,4 +274,32 @@ public function __toString(): string
{
return $this->toHtml() ?? '';
}

protected static function parseAttributesString(string $str): array
{
$tagName = 'yolo';
$errorsBefore = libxml_use_internal_errors();
$attr = [];

libxml_use_internal_errors(true);
$dom = new DOMDocument();
$dom->loadHTML("<{$tagName} {$str}>");
$node = $dom->getElementsByTagName($tagName)->item(0);
if ($node->hasAttributes()) {
foreach ($node->attributes as $attribute) {
$name = $attribute->nodeName;
$value = $attribute->nodeValue;

if (in_array($value, ['', $name]) && in_array($name, static::$booleanAttributes)) {
// convert known boolean attributes to bool
$value = true;
}

$attr[$name] = $value;
}
}

libxml_use_internal_errors($errorsBefore);
return $attr;
}
}
23 changes: 23 additions & 0 deletions tests/AttributesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ public function testConstructorArray(): void
$this->assertSame((string) $attr, 'bar="foo" foo="bar"');
}

public function testConstructorSingleNamedArgument(): void
{
$attr = new Attributes(
bar: 'foo',
);

$this->assertSame((string) $attr, 'bar="foo"');
}

public function testConstrucorNamedArguments(): void
{
$attr = new Attributes(
Expand All @@ -29,6 +38,20 @@ public function testConstrucorNamedArguments(): void
$this->assertSame((string) $attr, 'bar="foo" foo="bar"');
}

public function testConstructFromAttributesString(): void
{
$attr = new Attributes('foo="bar" bar="foo"');

$this->assertSame((string) $attr, 'bar="foo" foo="bar"');
}

public function testConstructFromAttributesStringWithBooleanAttributes(): void
{
$attr = new Attributes('novalidate inert unknown-boolean');

$this->assertSame((string) $attr, 'inert novalidate unknown-boolean=""');
}

public function testConversionToLowerCase(): void
{
$attr = new Attributes(
Expand Down

0 comments on commit 84c2fc2

Please sign in to comment.