Skip to content

Commit 4d38101

Browse files
committed
Import the Attributes extension (#484)
This extension is based https://github.com/webuni/commonmark-attributes-extension, imported and relicensed with permission from the maintainer: #474 (comment)
1 parent fb52dd0 commit 4d38101

24 files changed

+795
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi
66

77
### Added
88

9+
- Added new `AttributesExtension` based on <https://github.com/webuni/commonmark-attributes-extension> (#474)
910
- Added new `FootnoteExtension` based on <https://github.com/rezozero/commonmark-ext-footnotes> (#474)
1011
- Added a new `MentionParser` to replace `InlineMentionParser` with more flexibility and customization
1112
- Added the ability to render `TableOfContents` nodes anywhere in a document (given by a placeholder)

docs/1.5/extensions/attributes.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
layout: default
3+
title: Attributes Extension
4+
description: The AttributesExtension allows HTML attributes to be added from within the document.
5+
redirect_from: /extensions/attributes/
6+
---
7+
8+
# Attributes
9+
10+
The `AttributesExtension` allows HTML attributes to be added from within the document.
11+
12+
## Attribute Syntax
13+
14+
The basic syntax was inspired by [Kramdown](http://kramdown.gettalong.org/syntax.html#attribute-list-definitions)'s Attribute Lists feature.
15+
16+
You can assign any attribute to a block-level element. Just directly prepend or follow the block with a block inline attribute list.
17+
That consists of a left curly brace, optionally followed by a colon, the attribute definitions and a right curly brace:
18+
19+
```markdown
20+
> A nice blockquote
21+
{: title="Blockquote title"}
22+
23+
{#id .class}
24+
## Header
25+
```
26+
27+
As with a block-level element you can assign any attribute to a span-level elements using a span inline attribute list,
28+
that has the same syntax and must immediately follow the span-level element:
29+
30+
```markdown
31+
This is *red*{style="color: red"}.
32+
```
33+
34+
## Usage
35+
36+
Configure your `Environment` as usual and simply add the `AttributesExtension`:
37+
38+
```php
39+
<?php
40+
use League\CommonMark\CommonMarkConverter;
41+
use League\CommonMark\Environment;
42+
use League\CommonMark\Extension\Attributes\AttributesExtension;
43+
44+
// Obtain a pre-configured Environment with all the CommonMark parsers/renderers ready-to-go
45+
$environment = Environment::createCommonMarkEnvironment();
46+
47+
// Add the extension
48+
$environment->addExtension(new AttributesExtension());
49+
50+
// Set your configuration if needed
51+
$config = [
52+
// ...
53+
];
54+
55+
// Instantiate the converter engine and start converting some Markdown!
56+
$converter = new CommonMarkConverter($config, $environment);
57+
echo $converter->convertToHtml('# Hello World!');
58+
```

docs/1.5/extensions/overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ These extensions are not part of GFM, but can be useful in many cases:
7373

7474
| Extension | Purpose | Documentation |
7575
| --------- | ------- | ------------- |
76+
| `AttributesExtension` | Add HTML attributes (like `id` and `class`) from within the Markdown content | [Documentation](/1.5/extensions/attributes/) |
7677
| `ExternalLinkExtension` | Tags external links with additional markup | [Documentation](/1.5/extensions/external-links/) |
7778
| `FootnoteExtension` | Add footnote references throughout the document and show a listing of them at the bottom | [Documentation](/1.5/extensions/footnotes/) |
7879
| `HeadingPermalinkExtension` | Makes heading elements linkable | [Documentation](/1.5/extensions/heading-permalinks/) |

docs/_data/menu.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ version:
1414
'Overview': '/1.5/extensions/overview/'
1515
'CommonMark': '/1.5/extensions/commonmark/'
1616
'Github-Flavored Markdown': '/1.5/extensions/github-flavored-markdown/'
17+
'Attributes': '/1.5/extensions/attributes/'
1718
'Autolinks': '/1.5/extensions/autolinks/'
1819
'Disallowed Raw HTML': '/1.5/extensions/disallowed-raw-html/'
1920
'External Links': '/1.5/extensions/external-links/'
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the league/commonmark package.
5+
*
6+
* (c) Colin O'Dell <[email protected]>
7+
* (c) 2015 Martin Hasoň <[email protected]>
8+
*
9+
* For the full copyright and license information, please view the LICENSE
10+
* file that was distributed with this source code.
11+
*/
12+
13+
declare(strict_types=1);
14+
15+
namespace League\CommonMark\Extension\Attributes;
16+
17+
use League\CommonMark\ConfigurableEnvironmentInterface;
18+
use League\CommonMark\Event\DocumentParsedEvent;
19+
use League\CommonMark\Extension\Attributes\Event\AttributesListener;
20+
use League\CommonMark\Extension\Attributes\Parser\AttributesBlockParser;
21+
use League\CommonMark\Extension\Attributes\Parser\AttributesInlineParser;
22+
use League\CommonMark\Extension\ExtensionInterface;
23+
24+
final class AttributesExtension implements ExtensionInterface
25+
{
26+
public function register(ConfigurableEnvironmentInterface $environment)
27+
{
28+
$environment->addBlockParser(new AttributesBlockParser());
29+
$environment->addInlineParser(new AttributesInlineParser());
30+
$environment->addEventListener(DocumentParsedEvent::class, [new AttributesListener(), 'processDocument']);
31+
}
32+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the league/commonmark package.
5+
*
6+
* (c) Colin O'Dell <[email protected]>
7+
* (c) 2015 Martin Hasoň <[email protected]>
8+
*
9+
* For the full copyright and license information, please view the LICENSE
10+
* file that was distributed with this source code.
11+
*/
12+
13+
declare(strict_types=1);
14+
15+
namespace League\CommonMark\Extension\Attributes\Event;
16+
17+
use League\CommonMark\Block\Element\AbstractBlock;
18+
use League\CommonMark\Block\Element\FencedCode;
19+
use League\CommonMark\Block\Element\ListBlock;
20+
use League\CommonMark\Block\Element\ListItem;
21+
use League\CommonMark\Event\DocumentParsedEvent;
22+
use League\CommonMark\Extension\Attributes\Node\Attributes;
23+
use League\CommonMark\Extension\Attributes\Node\AttributesInline;
24+
use League\CommonMark\Inline\Element\AbstractInline;
25+
use League\CommonMark\Node\Node;
26+
27+
final class AttributesListener
28+
{
29+
private const DIRECTION_PREFIX = 'prefix';
30+
private const DIRECTION_SUFFIX = 'suffix';
31+
32+
public function processDocument(DocumentParsedEvent $event): void
33+
{
34+
$walker = $event->getDocument()->walker();
35+
while ($event = $walker->next()) {
36+
$node = $event->getNode();
37+
if (!$node instanceof AttributesInline && ($event->isEntering() || !$node instanceof Attributes)) {
38+
continue;
39+
}
40+
41+
[$target, $direction] = self::findTargetAndDirection($node);
42+
43+
if ($target instanceof AbstractBlock || $target instanceof AbstractInline) {
44+
$parent = $target->parent();
45+
if ($parent instanceof ListItem && $parent->parent() instanceof ListBlock && $parent->parent()->isTight()) {
46+
$target = $parent;
47+
}
48+
49+
if ($direction === self::DIRECTION_SUFFIX) {
50+
$attributes = self::merge($target, $node->getAttributes());
51+
} else {
52+
$attributes = self::merge($node->getAttributes(), $target);
53+
}
54+
55+
$target->data['attributes'] = $attributes;
56+
}
57+
58+
if ($node instanceof AbstractBlock && $node->endsWithBlankLine() && $node->next() && $node->previous()) {
59+
$previous = $node->previous();
60+
if ($previous instanceof AbstractBlock) {
61+
$previous->setLastLineBlank(true);
62+
}
63+
}
64+
65+
$node->detach();
66+
}
67+
}
68+
69+
/**
70+
* @param Node $node
71+
*
72+
* @return array<Node|string|null>
73+
*/
74+
private static function findTargetAndDirection(Node $node): array
75+
{
76+
$target = null;
77+
$direction = null;
78+
$previous = $next = $node;
79+
while (true) {
80+
$previous = self::getPrevious($previous);
81+
$next = self::getNext($next);
82+
83+
if ($previous === null && $next === null) {
84+
if (!$node->parent() instanceof FencedCode) {
85+
$target = $node->parent();
86+
$direction = self::DIRECTION_SUFFIX;
87+
}
88+
89+
break;
90+
}
91+
92+
if ($node instanceof AttributesInline && ($previous === null || ($previous instanceof AbstractInline && $node->isBlock()))) {
93+
continue;
94+
}
95+
96+
if ($previous !== null && !self::isAttributesNode($previous)) {
97+
$target = $previous;
98+
$direction = self::DIRECTION_SUFFIX;
99+
100+
break;
101+
}
102+
103+
if ($next !== null && !self::isAttributesNode($next)) {
104+
$target = $next;
105+
$direction = self::DIRECTION_PREFIX;
106+
107+
break;
108+
}
109+
}
110+
111+
return [$target, $direction];
112+
}
113+
114+
private static function getPrevious(?Node $node = null): ?Node
115+
{
116+
$previous = $node instanceof Node ? $node->previous() : null;
117+
118+
if ($previous instanceof AbstractBlock && $previous->endsWithBlankLine()) {
119+
$previous = null;
120+
}
121+
122+
return $previous;
123+
}
124+
125+
private static function getNext(?Node $node = null): ?Node
126+
{
127+
$next = $node instanceof Node ? $node->next() : null;
128+
129+
if ($node instanceof AbstractBlock && $node->endsWithBlankLine()) {
130+
$next = null;
131+
}
132+
133+
return $next;
134+
}
135+
136+
private static function isAttributesNode(Node $node): bool
137+
{
138+
return $node instanceof Attributes || $node instanceof AttributesInline;
139+
}
140+
141+
/**
142+
* @param AbstractBlock|AbstractInline|array<string, mixed> $attributes1
143+
* @param AbstractBlock|AbstractInline|array<string, mixed> $attributes2
144+
*
145+
* @return array<string, mixed>
146+
*/
147+
private static function merge($attributes1, $attributes2): array
148+
{
149+
$attributes = [];
150+
foreach ([$attributes1, $attributes2] as $arg) {
151+
if ($arg instanceof AbstractBlock || $arg instanceof AbstractInline) {
152+
$arg = $arg->data['attributes'] ?? [];
153+
}
154+
155+
/** @var array<string, mixed> $arg */
156+
$arg = (array) $arg;
157+
if (isset($arg['class'])) {
158+
foreach (\array_filter(\explode(' ', \trim($arg['class']))) as $class) {
159+
$attributes['class'][] = $class;
160+
}
161+
162+
unset($arg['class']);
163+
}
164+
165+
$attributes = \array_merge($attributes, $arg);
166+
}
167+
168+
if (isset($attributes['class'])) {
169+
$attributes['class'] = \implode(' ', $attributes['class']);
170+
}
171+
172+
return $attributes;
173+
}
174+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the league/commonmark package.
5+
*
6+
* (c) Colin O'Dell <[email protected]>
7+
* (c) 2015 Martin Hasoň <[email protected]>
8+
*
9+
* For the full copyright and license information, please view the LICENSE
10+
* file that was distributed with this source code.
11+
*/
12+
13+
declare(strict_types=1);
14+
15+
namespace League\CommonMark\Extension\Attributes\Node;
16+
17+
use League\CommonMark\Block\Element\AbstractBlock;
18+
use League\CommonMark\Cursor;
19+
20+
final class Attributes extends AbstractBlock
21+
{
22+
/** @var array<string, mixed> */
23+
private $attributes;
24+
25+
/**
26+
* @param array<string, mixed> $attributes
27+
*/
28+
public function __construct(array $attributes)
29+
{
30+
$this->attributes = $attributes;
31+
}
32+
33+
/**
34+
* @return array<string, mixed>
35+
*/
36+
public function getAttributes(): array
37+
{
38+
return $this->attributes;
39+
}
40+
41+
public function canContain(AbstractBlock $block): bool
42+
{
43+
return false;
44+
}
45+
46+
public function isCode(): bool
47+
{
48+
return false;
49+
}
50+
51+
public function matchesNextLine(Cursor $cursor): bool
52+
{
53+
$this->setLastLineBlank($cursor->isBlank());
54+
55+
return false;
56+
}
57+
58+
public function shouldLastLineBeBlank(Cursor $cursor, int $currentLineNumber): bool
59+
{
60+
return false;
61+
}
62+
}

0 commit comments

Comments
 (0)