Skip to content

Commit

Permalink
Merge pull request #1040 from xavierlacot/fix/curly-braces-in-text
Browse files Browse the repository at this point in the history
Fix the parser behavior for texts containing curly-braces when the Attributes extension is enabled
  • Loading branch information
colinodell authored Aug 14, 2024
2 parents ac81592 + f8e47fc commit 393a451
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 14 deletions.
15 changes: 15 additions & 0 deletions docs/2.5/extensions/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,21 @@ Output:
<p>This is <em style="color: red">red</em>.</p>
```

### 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
<h2 itemscope>Header</h2>
```

## Installation

This extension is bundled with `league/commonmark`. This library can be installed via Composer:
Expand Down
15 changes: 7 additions & 8 deletions src/Extension/Attributes/Util/AttributesHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, mixed>
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,19 @@ <h2 class="main shine" id="the-site">The Site</h2>
<p>Attributes without quote and non-whitespace char <a target="_blank" href="http://url.com" rel="noopener noreferrer">link</a></p>
<p>Attributes without quote and non-whitespace char and a dot <a target="_blank" href="http://url.com" rel="noopener noreferrer">link</a>.</p>
<p>Multiple attributes without quote and non-whitespace char and a dot <a class="class" id="id" target="_blank" href="http://url.com" rel="noopener noreferrer">link</a>.</p>
<p><img valueless-attribute src="/assets/image.jpg" alt="image" /></p>
<p><img src="/assets/image.jpg" alt="image" />{some-text}</p>
<p><img boolean-attribute="boolean-attribute" src="/assets/image.jpg" alt="image" /></p>
<p>A paragraph containing {{ mustache }} templating</p>
<p>A paragraph ending with {{ mustache }} templating</p>
<p>{{ mustache }} A paragraph starting with mustache templating</p>
<p>a. <a href="https://example.com">Some{text}</a>.</p>
<p>b. <a href="https://example.com">Some</a>.</p>
<p>c. <a class="text" href="https://example.com">Some</a>.</p>
<p>d. <a href="https://example.com">Some{text}</a>.</p>
<p>e. <a text="text" href="https://example.com">Some</a>.</p>
<p>f. <a text href="https://example.com">Some</a>.</p>
<p>g. <a href="https://example.com">Some{{text}}</a>.</p>
<p hello="hello">some</p>
<p class="test" hello="hello">some</p>
<p class="test" hello="hello">some</p>
<p hello="hello" goodbye="goodbye">some</p>
29 changes: 28 additions & 1 deletion tests/functional/Extension/Attributes/data/special_attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,38 @@ 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

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
10 changes: 6 additions & 4 deletions tests/unit/Extension/Attributes/Util/AttributesHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']];
Expand All @@ -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']];
Expand Down

0 comments on commit 393a451

Please sign in to comment.