Skip to content

Commit 26ac652

Browse files
authored
refactor: code block decode html entities (#30)
1 parent 65c26a3 commit 26ac652

File tree

6 files changed

+288
-4
lines changed

6 files changed

+288
-4
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ARKEcosystem\Foundation\CommonMark\Extensions\Highlighter;
6+
7+
use ARKEcosystem\Foundation\CommonMark\Extensions\Highlighter\Concerns\DecodesHtmlEntities;
8+
use League\CommonMark\Extension\CommonMark\Node\Inline\Code;
9+
use League\CommonMark\Node\Node;
10+
use League\CommonMark\Renderer\ChildNodeRendererInterface;
11+
use League\CommonMark\Renderer\NodeRendererInterface;
12+
use League\CommonMark\Util\HtmlElement;
13+
use League\CommonMark\Util\Xml;
14+
use League\CommonMark\Xml\XmlNodeRendererInterface;
15+
16+
final class CodeRenderer implements NodeRendererInterface, XmlNodeRendererInterface
17+
{
18+
use DecodesHtmlEntities;
19+
20+
/**
21+
* @param Code $node
22+
*
23+
* {@inheritDoc}
24+
*
25+
* @psalm-suppress MoreSpecificImplementedParamType
26+
*/
27+
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
28+
{
29+
Code::assertInstanceOf($node);
30+
$node = $this->parseEncodedHtml($node);
31+
32+
$attrs = $node->data->get('attributes');
33+
34+
return new HtmlElement('code', $attrs, Xml::escape($node->getLiteral()));
35+
}
36+
37+
public function getXmlTagName(Node $node): string
38+
{
39+
return 'code';
40+
}
41+
42+
/**
43+
* {@inheritDoc}
44+
*/
45+
public function getXmlAttributes(Node $node): array
46+
{
47+
return [];
48+
}
49+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ARKEcosystem\Foundation\CommonMark\Extensions\Highlighter\Concerns;
6+
7+
use League\CommonMark\Node\Node;
8+
use League\CommonMark\Util\Html5EntityDecoder;
9+
10+
trait DecodesHtmlEntities
11+
{
12+
private function parseEncodedHtml(Node $node): Node
13+
{
14+
$content = $node->getLiteral();
15+
$hasEncodedHtml = preg_match_all('/&(\w+|\d+);/', $content, $matches);
16+
if ($hasEncodedHtml === false || $hasEncodedHtml === 0) {
17+
return $node;
18+
}
19+
20+
$entitiesToUpdate = [];
21+
foreach (array_unique($matches[0]) as $element) {
22+
$entitiesToUpdate[$element] = Html5EntityDecoder::decode($element);
23+
}
24+
25+
$content = str_replace(array_keys($entitiesToUpdate), $entitiesToUpdate, $content);
26+
27+
$node->setLiteral($content);
28+
29+
return $node;
30+
}
31+
}

src/CommonMark/Extensions/Highlighter/FencedCodeRenderer.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace ARKEcosystem\Foundation\CommonMark\Extensions\Highlighter;
66

7+
use ARKEcosystem\Foundation\CommonMark\Extensions\Highlighter\Concerns\DecodesHtmlEntities;
78
use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
89
use League\CommonMark\Extension\CommonMark\Renderer\Block\FencedCodeRenderer as BaseFencedCodeRenderer;
910
use League\CommonMark\Node\Node;
@@ -15,6 +16,8 @@
1516

1617
final class FencedCodeRenderer implements NodeRendererInterface, XmlNodeRendererInterface
1718
{
19+
use DecodesHtmlEntities;
20+
1821
/** @var \ARKEcosystem\Foundation\CommonMark\Extensions\Highlighter\CodeBlockHighlighter */
1922
private $highlighter;
2023

@@ -29,7 +32,10 @@ public function __construct()
2932

3033
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
3134
{
32-
$element = $this->baseRenderer->render($node, $childRenderer);
35+
$element = $this->baseRenderer->render(
36+
$this->parseEncodedHtml($node),
37+
$childRenderer
38+
);
3339

3440
$this->configureLineNumbers($element);
3541

@@ -81,13 +87,13 @@ private function configureLineNumbers(HtmlElement $element): void
8187
}
8288
}
8389

84-
private function getSpecifiedLanguage(FencedCode $block): ?string
90+
private function getSpecifiedLanguage(FencedCode $block): string
8591
{
8692
$infoWords = $block->getInfoWords();
8793

8894
/* @phpstan-ignore-next-line */
8995
if (empty($infoWords) || empty($infoWords[0])) {
90-
return null;
96+
return 'plaintext';
9197
}
9298

9399
return Xml::escape($infoWords[0]);

src/Providers/CommonMarkServiceProvider.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace ARKEcosystem\Foundation\Providers;
66

77
use ARKEcosystem\Foundation\CommonMark\Extensions\HeadingPermalink\HeadingPermalinkRenderer;
8+
use ARKEcosystem\Foundation\CommonMark\Extensions\Highlighter\CodeRenderer;
89
use ARKEcosystem\Foundation\CommonMark\Extensions\Highlighter\FencedCodeRenderer;
910
use ARKEcosystem\Foundation\CommonMark\Extensions\Highlighter\IndentedCodeRenderer;
1011
use ARKEcosystem\Foundation\CommonMark\Extensions\Image\ImageRenderer;
@@ -22,6 +23,7 @@
2223
use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock;
2324
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
2425
use League\CommonMark\Extension\CommonMark\Node\Block\ThematicBreak;
26+
use League\CommonMark\Extension\CommonMark\Node\Inline\Code;
2527
use League\CommonMark\Extension\CommonMark\Node\Inline\Emphasis;
2628
use League\CommonMark\Extension\CommonMark\Node\Inline\HtmlInline;
2729
use League\CommonMark\Extension\CommonMark\Node\Inline\Image;
@@ -175,7 +177,7 @@ private function registerCommonMarkEnvironment(): void
175177
$environment->addRenderer(Paragraph::class, new ParagraphRenderer(), 0);
176178
$environment->addRenderer(ThematicBreak::class, new ThematicBreakRenderer(), 0);
177179

178-
// $environment->addRenderer(Code::class, new CodeRenderer(), 0);
180+
$environment->addRenderer(Code::class, new CodeRenderer(), 0);
179181
$environment->addRenderer(Emphasis::class, new EmphasisRenderer(), 0);
180182
$environment->addRenderer(HtmlInline::class, new HtmlInlineRenderer(), 0);
181183
$environment->addRenderer(Image::class, new ImageRenderer(), 0);
@@ -184,6 +186,7 @@ private function registerCommonMarkEnvironment(): void
184186
$environment->addRenderer(Text::class, new TextRenderer(), 0);
185187

186188
$inlineRenderers = array_merge([
189+
Code::class => CodeRenderer::class,
187190
Emphasis::class => EmphasisRenderer::class,
188191
HtmlInline::class => HtmlInlineRenderer::class,
189192
Image::class => ImageRenderer::class,
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use ARKEcosystem\Foundation\CommonMark\Extensions\Highlighter\CodeRenderer;
6+
use League\CommonMark\Environment\Environment;
7+
use League\CommonMark\Extension\CommonMark\Node\Inline\Code;
8+
use League\CommonMark\Node\Block\Document;
9+
use League\CommonMark\Renderer\HtmlRenderer;
10+
use League\CommonMark\Renderer\NodeRendererInterface;
11+
use League\CommonMark\Util\HtmlElement;
12+
13+
beforeEach(function () {
14+
$this->inlineCode = new Code();
15+
$this->renderer = new CodeRenderer();
16+
17+
$document = new Document();
18+
19+
$documentRenderer = $this->createMock(NodeRendererInterface::class);
20+
$documentRenderer->method('render')->willReturn('::document::');
21+
22+
$environment = new Environment();
23+
$environment->addRenderer(Document::class, $documentRenderer);
24+
$this->htmlRenderer = new HtmlRenderer($environment);
25+
});
26+
27+
it('should render', function () {
28+
$this->inlineCode->setLiteral('<span>test</span>');
29+
30+
$result = $this->renderer->render($this->inlineCode, $this->htmlRenderer);
31+
32+
expect($result)->toBeInstanceOf(HtmlElement::class);
33+
expect($result->getTagName())->toBe('code');
34+
expect($result->getContents())->toBe('&lt;span&gt;test&lt;/span&gt;');
35+
});
36+
37+
it('should parse encoded html characters', function () {
38+
$this->inlineCode->setLiteral('&lt;span&gt;test&lt;/span&gt;');
39+
40+
$result = $this->renderer->render($this->inlineCode, $this->htmlRenderer);
41+
42+
expect($result)->toBeInstanceOf(HtmlElement::class);
43+
expect($result->getTagName())->toBe('code');
44+
expect($result->getContents())->toBe('&lt;span&gt;test&lt;/span&gt;');
45+
});
46+
47+
it('should do nothing if no encoded html characters', function () {
48+
$this->inlineCode->setLiteral('this is a test');
49+
50+
$result = $this->renderer->render($this->inlineCode, $this->htmlRenderer);
51+
52+
expect($result)->toBeInstanceOf(HtmlElement::class);
53+
expect($result->getTagName())->toBe('code');
54+
expect($result->getContents())->toBe('this is a test');
55+
});
56+
57+
it('should get an xml tag name', function () {
58+
expect($this->renderer->getXmlTagName($this->inlineCode))->toBe('code');
59+
});
60+
61+
it('should return no xml attributes', function () {
62+
expect($this->renderer->getXmlAttributes($this->inlineCode))->toBe([]);
63+
});
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use ARKEcosystem\Foundation\CommonMark\Extensions\Highlighter\FencedCodeRenderer;
6+
use League\CommonMark\Environment\Environment;
7+
use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
8+
use League\CommonMark\Node\Block\Document;
9+
use League\CommonMark\Renderer\HtmlRenderer;
10+
use League\CommonMark\Renderer\NodeRendererInterface;
11+
use League\CommonMark\Util\HtmlElement;
12+
13+
beforeEach(function () {
14+
$this->block = new FencedCode(3, '`', 0);
15+
$this->renderer = new FencedCodeRenderer();
16+
17+
$document = new Document();
18+
19+
$documentRenderer = $this->createMock(NodeRendererInterface::class);
20+
$documentRenderer->method('render')->willReturn('::document::');
21+
22+
$environment = new Environment();
23+
$environment->addRenderer(Document::class, $documentRenderer);
24+
$this->htmlRenderer = new HtmlRenderer($environment);
25+
});
26+
27+
it('should render', function () {
28+
$this->block->setInfo('blade');
29+
$this->block->setLiteral('<span>test</span>');
30+
31+
$result = $this->renderer->render($this->block, $this->htmlRenderer);
32+
33+
expect($result)->toBeInstanceOf(HtmlElement::class);
34+
expect($result->getTagName())->toBe('div');
35+
expect($result->getContents(false)->getTagName())->toBe('pre');
36+
expect($result->getContents())->toContain('<code class="hljs-copy language-blade">');
37+
expect($result->getContents())->toContain('&lt;span&gt;test&lt;/span&gt;');
38+
expect($result->getAllAttributes())->toBe([
39+
'class' => 'p-4 mb-6 rounded-xl bg-theme-secondary-800 overflow-x-auto',
40+
]);
41+
expect($result->getContents(false)->getAllAttributes())->toBe(['class' => 'hljs']);
42+
});
43+
44+
it('should parse encoded html characters', function () {
45+
$this->block->setInfo('html');
46+
$this->block->setLiteral('&lt;span&gt;test&lt;/span&gt;');
47+
48+
$result = $this->renderer->render($this->block, $this->htmlRenderer);
49+
50+
expect($result)->toBeInstanceOf(HtmlElement::class);
51+
expect($result->getTagName())->toBe('div');
52+
expect($result->getContents(false)->getTagName())->toBe('pre');
53+
expect($result->getContents())->toContain('<code class="hljs-copy language-html">');
54+
expect($result->getContents())->toContain('&lt;span&gt;test&lt;/span&gt;');
55+
expect($result->getAllAttributes())->toBe([
56+
'class' => 'p-4 mb-6 rounded-xl bg-theme-secondary-800 overflow-x-auto',
57+
]);
58+
expect($result->getContents(false)->getAllAttributes())->toBe(['class' => 'hljs']);
59+
});
60+
61+
it('should do nothing if no encoded html characters', function () {
62+
$this->block->setInfo('html');
63+
$this->block->setLiteral('this is a test');
64+
65+
$result = $this->renderer->render($this->block, $this->htmlRenderer);
66+
67+
expect($result)->toBeInstanceOf(HtmlElement::class);
68+
expect($result->getTagName())->toBe('div');
69+
expect($result->getContents(false)->getTagName())->toBe('pre');
70+
expect($result->getContents())->toContain('<code class="hljs-copy language-html">');
71+
expect($result->getContents())->toContain('this is a test');
72+
expect($result->getAllAttributes())->toBe([
73+
'class' => 'p-4 mb-6 rounded-xl bg-theme-secondary-800 overflow-x-auto',
74+
]);
75+
expect($result->getContents(false)->getAllAttributes())->toBe(['class' => 'hljs']);
76+
});
77+
78+
it('should handle line numbers', function () {
79+
$this->block->setInfo('plaintext');
80+
$this->block->setLiteral('test
81+
test
82+
test');
83+
84+
$result = $this->renderer->render($this->block, $this->htmlRenderer);
85+
86+
expect($result)->toBeInstanceOf(HtmlElement::class);
87+
expect($result->getTagName())->toBe('div');
88+
expect($result->getContents(false)->getTagName())->toBe('pre');
89+
expect($result->getContents())->toContain('<code class="hljs-copy language-plaintext">');
90+
expect($result->getContents())->toContain('test
91+
test
92+
test');
93+
expect($result->getAllAttributes())->toBe([
94+
'class' => 'p-4 mb-6 rounded-xl bg-theme-secondary-800 overflow-x-auto',
95+
]);
96+
expect($result->getContents(false)->getAllAttributes())->toBe(['class' => 'hljs line-numbers']);
97+
});
98+
99+
it('should handle no codeblock type', function () {
100+
$this->block->setInfo('');
101+
$this->block->setLiteral('test
102+
test
103+
test');
104+
105+
$result = $this->renderer->render($this->block, $this->htmlRenderer);
106+
107+
expect($result)->toBeInstanceOf(HtmlElement::class);
108+
expect($result->getTagName())->toBe('div');
109+
expect($result->getContents(false)->getTagName())->toBe('pre');
110+
expect($result->getContents())->toContain('<code class="hljs-copy language-plaintext">');
111+
expect($result->getContents())->toContain('test
112+
test
113+
test');
114+
expect($result->getAllAttributes())->toBe([
115+
'class' => 'p-4 mb-6 rounded-xl bg-theme-secondary-800 overflow-x-auto',
116+
]);
117+
expect($result->getContents(false)->getAllAttributes())->toBe(['class' => 'hljs line-numbers']);
118+
});
119+
120+
it('should get an xml tag name', function () {
121+
expect($this->renderer->getXmlTagName($this->block))->toBe('code_block');
122+
});
123+
124+
it('should get xml attributes', function () {
125+
$this->block->setInfo('blade');
126+
127+
expect($this->renderer->getXmlAttributes($this->block))->toBe(['info' => 'blade']);
128+
});
129+
130+
it('should get no xml attributes if no codeblock type', function () {
131+
expect($this->renderer->getXmlAttributes($this->block))->toBe([]);
132+
});

0 commit comments

Comments
 (0)