diff --git a/docs/2.5/extensions/attributes.md b/docs/2.5/extensions/attributes.md index 7ee11b9a4a..9c8a7b01d5 100644 --- a/docs/2.5/extensions/attributes.md +++ b/docs/2.5/extensions/attributes.md @@ -55,6 +55,21 @@ Output:

This is red.

``` +### Empty-Value Attributes + +Attributes can be rendered in HTML without a value by using `true` value in the markdown document: + +```markdown +{itemscope=true} +## Header +``` + +Output: + +```html +

Header

+``` + ## Installation This extension is bundled with `league/commonmark`. This library can be installed via Composer: diff --git a/src/Extension/Attributes/Util/AttributesHelper.php b/src/Extension/Attributes/Util/AttributesHelper.php index 5fbbdea1c3..d13a565ef6 100644 --- a/src/Extension/Attributes/Util/AttributesHelper.php +++ b/src/Extension/Attributes/Util/AttributesHelper.php @@ -23,8 +23,8 @@ */ final class AttributesHelper { - private const SINGLE_ATTRIBUTE = '\s*([.]-?[_a-z][^\s}]*|[#][^\s}]+|' . RegexHelper::PARTIAL_ATTRIBUTENAME . RegexHelper::PARTIAL_ATTRIBUTEVALUESPEC . '?)\s*'; - private const ATTRIBUTE_LIST = '/^{:?(' . self::SINGLE_ATTRIBUTE . ')+}(?!})/i'; + private const SINGLE_ATTRIBUTE = '\s*([.]-?[_a-z][^\s}]*|[#][^\s}]+|' . RegexHelper::PARTIAL_ATTRIBUTENAME . RegexHelper::PARTIAL_ATTRIBUTEVALUESPEC . ')\s*'; + private const ATTRIBUTE_LIST = '/^{:?(' . self::SINGLE_ATTRIBUTE . ')+}/i'; /** * @return array @@ -72,15 +72,14 @@ public static function parseAttributes(Cursor $cursor): array continue; } - $parts = \explode('=', $attribute, 2); - if (\count($parts) === 1) { - $attributes[$attribute] = true; + /** @psalm-suppress PossiblyUndefinedArrayOffset */ + [$name, $value] = \explode('=', $attribute, 2); + + if ($value === 'true') { + $attributes[$name] = true; continue; } - /** @psalm-suppress PossiblyUndefinedArrayOffset */ - [$name, $value] = $parts; - $first = $value[0]; $last = \substr($value, -1); if (($first === '"' && $last === '"') || ($first === "'" && $last === "'") && \strlen($value) > 1) { diff --git a/tests/functional/Extension/Attributes/data/special_attributes.html b/tests/functional/Extension/Attributes/data/special_attributes.html index c37856c51f..be4efd48fb 100644 --- a/tests/functional/Extension/Attributes/data/special_attributes.html +++ b/tests/functional/Extension/Attributes/data/special_attributes.html @@ -12,7 +12,19 @@

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

+

image{some-text}

+

image

A paragraph containing {{ mustache }} templating

A paragraph ending with {{ mustache }} templating

{{ mustache }} A paragraph starting with mustache templating

+

a. Some{text}.

+

b. Some.

+

c. Some.

+

d. Some{text}.

+

e. Some.

+

f. Some.

+

g. Some{{text}}.

+

some

+

some

+

some

+

some

diff --git a/tests/functional/Extension/Attributes/data/special_attributes.md b/tests/functional/Extension/Attributes/data/special_attributes.md index c94db9739e..83688fcddc 100644 --- a/tests/functional/Extension/Attributes/data/special_attributes.md +++ b/tests/functional/Extension/Attributes/data/special_attributes.md @@ -29,7 +29,9 @@ Attributes without quote and non-whitespace char and a dot [link](http://url.com 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} +![image](/assets/image.jpg){some-text} + +![image](/assets/image.jpg){boolean-attribute="boolean-attribute"} A paragraph containing {{ mustache }} templating @@ -37,3 +39,28 @@ A paragraph ending with {{ mustache }} templating {{ mustache }} A paragraph starting with mustache templating +a. [Some{text}](https://example.com). + +b. [Some{.text}](https://example.com). + +c. [Some](https://example.com){.text}. + +d. [Some{text}](https://example.com). + +e. [Some](https://example.com){text="text"}. + +f. [Some](https://example.com){text=true}. + +g. [Some{{text}}](https://example.com). + +{hello="hello"} +some + +{.test hello="hello"} +some + +{hello="hello" .test} +some + +{hello="hello" goodbye="goodbye"} +some diff --git a/tests/unit/Extension/Attributes/Util/AttributesHelperTest.php b/tests/unit/Extension/Attributes/Util/AttributesHelperTest.php index 500b270a4b..257621f910 100644 --- a/tests/unit/Extension/Attributes/Util/AttributesHelperTest.php +++ b/tests/unit/Extension/Attributes/Util/AttributesHelperTest.php @@ -53,12 +53,13 @@ public static 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" disabled }'), ['id' => 'custom-id', 'class' => 'class1 class2', 'title' => 'My Title', 'disabled' => true]]; + yield [new Cursor('{: #custom-id .class1 .class2 title="My Title" disabled=true }'), ['id' => 'custom-id', 'class' => 'class1 class2', 'title' => 'My Title', 'disabled' => true]]; + yield [new Cursor('{: #custom-id .class1 .class2 title="My Title" disabled="disabled" }'), ['id' => 'custom-id', 'class' => 'class1 class2', 'title' => 'My Title', 'disabled' => 'disabled']]; 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]]; + yield [new Cursor('{: disabled=disabled}'), ['disabled' => 'disabled']]; // Examples without colons yield [new Cursor('{title="My Title"}'), ['title' => 'My Title']]; @@ -69,12 +70,13 @@ public static 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" disabled }'), ['id' => 'custom-id', 'class' => 'class1 class2', 'title' => 'My Title', 'disabled' => true]]; + yield [new Cursor('{ #custom-id .class1 .class2 title="My Title" disabled=true }'), ['id' => 'custom-id', 'class' => 'class1 class2', 'title' => 'My Title', 'disabled' => true]]; + yield [new Cursor('{ #custom-id .class1 .class2 title="My Title" disabled="disabled" }'), ['id' => 'custom-id', 'class' => 'class1 class2', 'title' => 'My Title', 'disabled' => 'disabled']]; 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]]; + yield [new Cursor('{disabled=disabled}'), ['disabled' => 'disabled']]; // Stuff at the beginning yield [new Cursor(' {: #custom-id }'), ['id' => 'custom-id']];