Skip to content

Commit 5326e04

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 5326e04

24 files changed

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

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: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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\Document;
19+
use League\CommonMark\Block\Element\FencedCode;
20+
use League\CommonMark\Block\Element\ListBlock;
21+
use League\CommonMark\Block\Element\ListItem;
22+
use League\CommonMark\Event\DocumentParsedEvent;
23+
use League\CommonMark\Extension\Attributes\Node\Attributes;
24+
use League\CommonMark\Extension\Attributes\Node\AttributesInline;
25+
use League\CommonMark\Inline\Element\AbstractInline;
26+
use League\CommonMark\Node\Node;
27+
28+
final class AttributesListener
29+
{
30+
private const DIRECTION_PREFIX = 'prefix';
31+
private const DIRECTION_SUFFIX = 'suffix';
32+
33+
public function processDocument(DocumentParsedEvent $event): void
34+
{
35+
$walker = $event->getDocument()->walker();
36+
while ($event = $walker->next()) {
37+
$node = $event->getNode();
38+
if (!$node instanceof AttributesInline && ($event->isEntering() || !$node instanceof Attributes)) {
39+
continue;
40+
}
41+
42+
[$target, $direction] = self::findTargetAndDirection($node);
43+
44+
if ($target instanceof Node) {
45+
$parent = $target->parent();
46+
if ($parent instanceof ListItem && $parent->parent() instanceof ListBlock && $parent->parent()->isTight()) {
47+
$target = $parent;
48+
}
49+
50+
if ($direction === self::DIRECTION_SUFFIX) {
51+
$attributes = self::merge($target, $node->getAttributes());
52+
} else {
53+
$attributes = self::merge($node->getAttributes(), $target);
54+
}
55+
56+
if ($target instanceof AbstractBlock || $target instanceof AbstractInline) {
57+
$target->data['attributes'] = $attributes;
58+
}
59+
}
60+
61+
if ($node instanceof AbstractBlock && $node->endsWithBlankLine() && $node->next() && $node->previous()) {
62+
$previous = $node->previous();
63+
if ($previous instanceof AbstractBlock) {
64+
$previous->setLastLineBlank(true);
65+
}
66+
}
67+
68+
$node->detach();
69+
}
70+
}
71+
72+
/**
73+
* @param Node $node
74+
*
75+
* @return array<Node|string>
76+
*/
77+
private static function findTargetAndDirection(Node $node): array
78+
{
79+
$target = null;
80+
$direction = null;
81+
$previous = $next = $node;
82+
while (true) {
83+
$previous = self::getPrevious($previous);
84+
$next = self::getNext($next);
85+
86+
if ($previous === null && $next === null) {
87+
if (!$node->parent() instanceof FencedCode) {
88+
$target = $node->parent();
89+
$direction = self::DIRECTION_SUFFIX;
90+
}
91+
92+
break;
93+
}
94+
95+
if ($node instanceof AttributesInline && ($previous === null || ($previous instanceof AbstractInline && $node->isBlock()))) {
96+
continue;
97+
}
98+
99+
if ($previous !== null && !self::isAttributesNode($previous)) {
100+
$target = $previous;
101+
$direction = self::DIRECTION_SUFFIX;
102+
103+
break;
104+
}
105+
106+
if ($next !== null && !self::isAttributesNode($next)) {
107+
$target = $next;
108+
$direction = self::DIRECTION_PREFIX;
109+
110+
break;
111+
}
112+
}
113+
114+
return [$target, $direction];
115+
}
116+
117+
private static function getPrevious(?Node $node = null): ?Node
118+
{
119+
$previous = $node instanceof Node ? $node->previous() : null;
120+
121+
if ($previous instanceof AbstractBlock && $previous->endsWithBlankLine()) {
122+
$previous = null;
123+
}
124+
125+
return $previous;
126+
}
127+
128+
private static function getNext(?Node $node = null): ?Node
129+
{
130+
$next = $node instanceof Node ? $node->next() : null;
131+
132+
if ($node instanceof AbstractBlock && $node->endsWithBlankLine()) {
133+
$next = null;
134+
}
135+
136+
return $next;
137+
}
138+
139+
private static function isAttributesNode(Node $node): bool
140+
{
141+
return $node instanceof Attributes || $node instanceof AttributesInline;
142+
}
143+
144+
/**
145+
* @param AbstractBlock|AbstractInline|array<string, mixed> $attributes1
146+
* @param AbstractBlock|AbstractInline|array<string, mixed> $attributes2
147+
*
148+
* @return array<string, mixed>
149+
*/
150+
private static function merge($attributes1, $attributes2): array
151+
{
152+
$attributes = [];
153+
foreach ([$attributes1, $attributes2] as $arg) {
154+
if ($arg instanceof AbstractBlock || $arg instanceof AbstractInline) {
155+
$arg = $arg->data['attributes'] ?? [];
156+
}
157+
158+
$arg = (array) $arg;
159+
if (isset($arg['class'])) {
160+
foreach (\array_filter(\explode(' ', \trim($arg['class']))) as $class) {
161+
$attributes['class'][] = $class;
162+
}
163+
164+
unset($arg['class']);
165+
}
166+
167+
$attributes = \array_merge($attributes, $arg);
168+
}
169+
170+
if (isset($attributes['class'])) {
171+
$attributes['class'] = \implode(' ', $attributes['class']);
172+
}
173+
174+
return $attributes;
175+
}
176+
}
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)