diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c43dbf0a9..53920b982a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi ## [Unreleased][unreleased] +### Added + +- The `AttributesExtension` now supports attributes without values (#985, #986) + ## [2.4.0] - 2023-03-24 ### Added diff --git a/src/Extension/Attributes/Util/AttributesHelper.php b/src/Extension/Attributes/Util/AttributesHelper.php index de5c111e1d..8f094fddbd 100644 --- a/src/Extension/Attributes/Util/AttributesHelper.php +++ b/src/Extension/Attributes/Util/AttributesHelper.php @@ -23,7 +23,7 @@ */ final class AttributesHelper { - private const SINGLE_ATTRIBUTE = '\s*([.#][_a-z0-9-]+|' . RegexHelper::PARTIAL_ATTRIBUTENAME . RegexHelper::PARTIAL_ATTRIBUTEVALUESPEC . ')\s*'; + private const SINGLE_ATTRIBUTE = '\s*([.#][_a-z0-9-]+|' . RegexHelper::PARTIAL_ATTRIBUTENAME . RegexHelper::PARTIAL_ATTRIBUTEVALUESPEC . '?)\s*'; private const ATTRIBUTE_LIST = '/^{:?(' . self::SINGLE_ATTRIBUTE . ')+}/i'; /** @@ -72,8 +72,14 @@ public static function parseAttributes(Cursor $cursor): array continue; } + $parts = \explode('=', $attribute, 2); + if (\count($parts) === 1) { + $attributes[$attribute] = true; + continue; + } + /** @psalm-suppress PossiblyUndefinedArrayOffset */ - [$name, $value] = \explode('=', $attribute, 2); + [$name, $value] = $parts; $first = $value[0]; $last = \substr($value, -1); diff --git a/tests/functional/Extension/Attributes/data/special_attributes.html b/tests/functional/Extension/Attributes/data/special_attributes.html index 7826cb4553..670299c6fe 100644 --- a/tests/functional/Extension/Attributes/data/special_attributes.html +++ b/tests/functional/Extension/Attributes/data/special_attributes.html @@ -12,3 +12,4 @@

The Site

Attributes without quote and non-whitespace char link

Attributes without quote and non-whitespace char and a dot link.

Multiple attributes without quote and non-whitespace char and a dot link.

+

image

diff --git a/tests/functional/Extension/Attributes/data/special_attributes.md b/tests/functional/Extension/Attributes/data/special_attributes.md index f66089ced1..3e73c1c7f2 100644 --- a/tests/functional/Extension/Attributes/data/special_attributes.md +++ b/tests/functional/Extension/Attributes/data/special_attributes.md @@ -28,3 +28,5 @@ Attributes without quote and non-whitespace char [link](http://url.com){target=_ Attributes without quote and non-whitespace char and a dot [link](http://url.com){target=_blank}. Multiple attributes without quote and non-whitespace char and a dot [link](http://url.com){#id .class target=_blank}. + +![image](/assets/image.jpg){valueless-attribute} diff --git a/tests/unit/Extension/Attributes/Util/AttributesHelperTest.php b/tests/unit/Extension/Attributes/Util/AttributesHelperTest.php index 17a465348a..93da345906 100644 --- a/tests/unit/Extension/Attributes/Util/AttributesHelperTest.php +++ b/tests/unit/Extension/Attributes/Util/AttributesHelperTest.php @@ -53,11 +53,12 @@ public function dataForTestParseAttributes(): iterable yield [new Cursor('{: #custom-id }'), ['id' => 'custom-id']]; yield [new Cursor('{: #custom-id #another-id }'), ['id' => 'another-id']]; yield [new Cursor('{: .class1 .class2 }'), ['class' => 'class1 class2']]; - yield [new Cursor('{: #custom-id .class1 .class2 title="My Title" }'), ['id' => 'custom-id', 'class' => 'class1 class2', 'title' => 'My Title']]; + yield [new Cursor('{: #custom-id .class1 .class2 title="My Title" disabled }'), ['id' => 'custom-id', 'class' => 'class1 class2', 'title' => 'My Title', 'disabled' => true]]; yield [new Cursor('{:target=_blank}'), ['target' => '_blank']]; yield [new Cursor('{: target=_blank}'), ['target' => '_blank']]; yield [new Cursor('{: target=_blank }'), ['target' => '_blank']]; yield [new Cursor('{: target=_blank }'), ['target' => '_blank']]; + yield [new Cursor('{: disabled}'), ['disabled' => true]]; // Examples without colons yield [new Cursor('{title="My Title"}'), ['title' => 'My Title']]; @@ -68,11 +69,12 @@ public function dataForTestParseAttributes(): iterable yield [new Cursor('{ #custom-id }'), ['id' => 'custom-id']]; yield [new Cursor('{ #custom-id #another-id }'), ['id' => 'another-id']]; yield [new Cursor('{ .class1 .class2 }'), ['class' => 'class1 class2']]; - yield [new Cursor('{ #custom-id .class1 .class2 title="My Title" }'), ['id' => 'custom-id', 'class' => 'class1 class2', 'title' => 'My Title']]; + yield [new Cursor('{ #custom-id .class1 .class2 title="My Title" disabled }'), ['id' => 'custom-id', 'class' => 'class1 class2', 'title' => 'My Title', 'disabled' => true]]; yield [new Cursor('{target=_blank}'), ['target' => '_blank']]; yield [new Cursor('{ target=_blank}'), ['target' => '_blank']]; yield [new Cursor('{target=_blank }'), ['target' => '_blank']]; yield [new Cursor('{ target=_blank }'), ['target' => '_blank']]; + yield [new Cursor('{disabled}'), ['disabled' => true]]; // Stuff at the beginning yield [new Cursor(' {: #custom-id }'), ['id' => 'custom-id']];