diff --git a/src/CommonMark/Extensions/Highlighter/CodeRenderer.php b/src/CommonMark/Extensions/Highlighter/CodeRenderer.php
new file mode 100644
index 000000000..9d65ff036
--- /dev/null
+++ b/src/CommonMark/Extensions/Highlighter/CodeRenderer.php
@@ -0,0 +1,49 @@
+parseEncodedHtml($node);
+
+ $attrs = $node->data->get('attributes');
+
+ return new HtmlElement('code', $attrs, Xml::escape($node->getLiteral()));
+ }
+
+ public function getXmlTagName(Node $node): string
+ {
+ return 'code';
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getXmlAttributes(Node $node): array
+ {
+ return [];
+ }
+}
diff --git a/src/CommonMark/Extensions/Highlighter/Concerns/DecodesHtmlEntities.php b/src/CommonMark/Extensions/Highlighter/Concerns/DecodesHtmlEntities.php
new file mode 100644
index 000000000..71de37c72
--- /dev/null
+++ b/src/CommonMark/Extensions/Highlighter/Concerns/DecodesHtmlEntities.php
@@ -0,0 +1,31 @@
+getLiteral();
+ $hasEncodedHtml = preg_match_all('/&(\w+|\d+);/', $content, $matches);
+ if ($hasEncodedHtml === false || $hasEncodedHtml === 0) {
+ return $node;
+ }
+
+ $entitiesToUpdate = [];
+ foreach (array_unique($matches[0]) as $element) {
+ $entitiesToUpdate[$element] = Html5EntityDecoder::decode($element);
+ }
+
+ $content = str_replace(array_keys($entitiesToUpdate), $entitiesToUpdate, $content);
+
+ $node->setLiteral($content);
+
+ return $node;
+ }
+}
diff --git a/src/CommonMark/Extensions/Highlighter/FencedCodeRenderer.php b/src/CommonMark/Extensions/Highlighter/FencedCodeRenderer.php
index 65c96e2c6..ce6c10f49 100644
--- a/src/CommonMark/Extensions/Highlighter/FencedCodeRenderer.php
+++ b/src/CommonMark/Extensions/Highlighter/FencedCodeRenderer.php
@@ -4,6 +4,7 @@
namespace ARKEcosystem\Foundation\CommonMark\Extensions\Highlighter;
+use ARKEcosystem\Foundation\CommonMark\Extensions\Highlighter\Concerns\DecodesHtmlEntities;
use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
use League\CommonMark\Extension\CommonMark\Renderer\Block\FencedCodeRenderer as BaseFencedCodeRenderer;
use League\CommonMark\Node\Node;
@@ -15,6 +16,8 @@
final class FencedCodeRenderer implements NodeRendererInterface, XmlNodeRendererInterface
{
+ use DecodesHtmlEntities;
+
/** @var \ARKEcosystem\Foundation\CommonMark\Extensions\Highlighter\CodeBlockHighlighter */
private $highlighter;
@@ -29,7 +32,10 @@ public function __construct()
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
- $element = $this->baseRenderer->render($node, $childRenderer);
+ $element = $this->baseRenderer->render(
+ $this->parseEncodedHtml($node),
+ $childRenderer
+ );
$this->configureLineNumbers($element);
@@ -81,13 +87,13 @@ private function configureLineNumbers(HtmlElement $element): void
}
}
- private function getSpecifiedLanguage(FencedCode $block): ?string
+ private function getSpecifiedLanguage(FencedCode $block): string
{
$infoWords = $block->getInfoWords();
/* @phpstan-ignore-next-line */
if (empty($infoWords) || empty($infoWords[0])) {
- return null;
+ return 'plaintext';
}
return Xml::escape($infoWords[0]);
diff --git a/src/Providers/CommonMarkServiceProvider.php b/src/Providers/CommonMarkServiceProvider.php
index c7a9055f6..347d5853c 100644
--- a/src/Providers/CommonMarkServiceProvider.php
+++ b/src/Providers/CommonMarkServiceProvider.php
@@ -5,6 +5,7 @@
namespace ARKEcosystem\Foundation\Providers;
use ARKEcosystem\Foundation\CommonMark\Extensions\HeadingPermalink\HeadingPermalinkRenderer;
+use ARKEcosystem\Foundation\CommonMark\Extensions\Highlighter\CodeRenderer;
use ARKEcosystem\Foundation\CommonMark\Extensions\Highlighter\FencedCodeRenderer;
use ARKEcosystem\Foundation\CommonMark\Extensions\Highlighter\IndentedCodeRenderer;
use ARKEcosystem\Foundation\CommonMark\Extensions\Image\ImageRenderer;
@@ -22,6 +23,7 @@
use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock;
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
use League\CommonMark\Extension\CommonMark\Node\Block\ThematicBreak;
+use League\CommonMark\Extension\CommonMark\Node\Inline\Code;
use League\CommonMark\Extension\CommonMark\Node\Inline\Emphasis;
use League\CommonMark\Extension\CommonMark\Node\Inline\HtmlInline;
use League\CommonMark\Extension\CommonMark\Node\Inline\Image;
@@ -175,7 +177,7 @@ private function registerCommonMarkEnvironment(): void
$environment->addRenderer(Paragraph::class, new ParagraphRenderer(), 0);
$environment->addRenderer(ThematicBreak::class, new ThematicBreakRenderer(), 0);
- // $environment->addRenderer(Code::class, new CodeRenderer(), 0);
+ $environment->addRenderer(Code::class, new CodeRenderer(), 0);
$environment->addRenderer(Emphasis::class, new EmphasisRenderer(), 0);
$environment->addRenderer(HtmlInline::class, new HtmlInlineRenderer(), 0);
$environment->addRenderer(Image::class, new ImageRenderer(), 0);
@@ -184,6 +186,7 @@ private function registerCommonMarkEnvironment(): void
$environment->addRenderer(Text::class, new TextRenderer(), 0);
$inlineRenderers = array_merge([
+ Code::class => CodeRenderer::class,
Emphasis::class => EmphasisRenderer::class,
HtmlInline::class => HtmlInlineRenderer::class,
Image::class => ImageRenderer::class,
diff --git a/tests/CommonMark/Extensions/Highlighter/CodeRendererTest.php b/tests/CommonMark/Extensions/Highlighter/CodeRendererTest.php
new file mode 100644
index 000000000..2b0cd3587
--- /dev/null
+++ b/tests/CommonMark/Extensions/Highlighter/CodeRendererTest.php
@@ -0,0 +1,63 @@
+inlineCode = new Code();
+ $this->renderer = new CodeRenderer();
+
+ $document = new Document();
+
+ $documentRenderer = $this->createMock(NodeRendererInterface::class);
+ $documentRenderer->method('render')->willReturn('::document::');
+
+ $environment = new Environment();
+ $environment->addRenderer(Document::class, $documentRenderer);
+ $this->htmlRenderer = new HtmlRenderer($environment);
+});
+
+it('should render', function () {
+ $this->inlineCode->setLiteral('test');
+
+ $result = $this->renderer->render($this->inlineCode, $this->htmlRenderer);
+
+ expect($result)->toBeInstanceOf(HtmlElement::class);
+ expect($result->getTagName())->toBe('code');
+ expect($result->getContents())->toBe('<span>test</span>');
+});
+
+it('should parse encoded html characters', function () {
+ $this->inlineCode->setLiteral('<span>test</span>');
+
+ $result = $this->renderer->render($this->inlineCode, $this->htmlRenderer);
+
+ expect($result)->toBeInstanceOf(HtmlElement::class);
+ expect($result->getTagName())->toBe('code');
+ expect($result->getContents())->toBe('<span>test</span>');
+});
+
+it('should do nothing if no encoded html characters', function () {
+ $this->inlineCode->setLiteral('this is a test');
+
+ $result = $this->renderer->render($this->inlineCode, $this->htmlRenderer);
+
+ expect($result)->toBeInstanceOf(HtmlElement::class);
+ expect($result->getTagName())->toBe('code');
+ expect($result->getContents())->toBe('this is a test');
+});
+
+it('should get an xml tag name', function () {
+ expect($this->renderer->getXmlTagName($this->inlineCode))->toBe('code');
+});
+
+it('should return no xml attributes', function () {
+ expect($this->renderer->getXmlAttributes($this->inlineCode))->toBe([]);
+});
diff --git a/tests/CommonMark/Extensions/Highlighter/FencedCodeRendererTest.php b/tests/CommonMark/Extensions/Highlighter/FencedCodeRendererTest.php
new file mode 100644
index 000000000..87860c767
--- /dev/null
+++ b/tests/CommonMark/Extensions/Highlighter/FencedCodeRendererTest.php
@@ -0,0 +1,132 @@
+block = new FencedCode(3, '`', 0);
+ $this->renderer = new FencedCodeRenderer();
+
+ $document = new Document();
+
+ $documentRenderer = $this->createMock(NodeRendererInterface::class);
+ $documentRenderer->method('render')->willReturn('::document::');
+
+ $environment = new Environment();
+ $environment->addRenderer(Document::class, $documentRenderer);
+ $this->htmlRenderer = new HtmlRenderer($environment);
+});
+
+it('should render', function () {
+ $this->block->setInfo('blade');
+ $this->block->setLiteral('test');
+
+ $result = $this->renderer->render($this->block, $this->htmlRenderer);
+
+ expect($result)->toBeInstanceOf(HtmlElement::class);
+ expect($result->getTagName())->toBe('div');
+ expect($result->getContents(false)->getTagName())->toBe('pre');
+ expect($result->getContents())->toContain('');
+ expect($result->getContents())->toContain('<span>test</span>');
+ expect($result->getAllAttributes())->toBe([
+ 'class' => 'p-4 mb-6 rounded-xl bg-theme-secondary-800 overflow-x-auto',
+ ]);
+ expect($result->getContents(false)->getAllAttributes())->toBe(['class' => 'hljs']);
+});
+
+it('should parse encoded html characters', function () {
+ $this->block->setInfo('html');
+ $this->block->setLiteral('<span>test</span>');
+
+ $result = $this->renderer->render($this->block, $this->htmlRenderer);
+
+ expect($result)->toBeInstanceOf(HtmlElement::class);
+ expect($result->getTagName())->toBe('div');
+ expect($result->getContents(false)->getTagName())->toBe('pre');
+ expect($result->getContents())->toContain('');
+ expect($result->getContents())->toContain('<span>test</span>');
+ expect($result->getAllAttributes())->toBe([
+ 'class' => 'p-4 mb-6 rounded-xl bg-theme-secondary-800 overflow-x-auto',
+ ]);
+ expect($result->getContents(false)->getAllAttributes())->toBe(['class' => 'hljs']);
+});
+
+it('should do nothing if no encoded html characters', function () {
+ $this->block->setInfo('html');
+ $this->block->setLiteral('this is a test');
+
+ $result = $this->renderer->render($this->block, $this->htmlRenderer);
+
+ expect($result)->toBeInstanceOf(HtmlElement::class);
+ expect($result->getTagName())->toBe('div');
+ expect($result->getContents(false)->getTagName())->toBe('pre');
+ expect($result->getContents())->toContain('');
+ expect($result->getContents())->toContain('this is a test');
+ expect($result->getAllAttributes())->toBe([
+ 'class' => 'p-4 mb-6 rounded-xl bg-theme-secondary-800 overflow-x-auto',
+ ]);
+ expect($result->getContents(false)->getAllAttributes())->toBe(['class' => 'hljs']);
+});
+
+it('should handle line numbers', function () {
+ $this->block->setInfo('plaintext');
+ $this->block->setLiteral('test
+ test
+ test');
+
+ $result = $this->renderer->render($this->block, $this->htmlRenderer);
+
+ expect($result)->toBeInstanceOf(HtmlElement::class);
+ expect($result->getTagName())->toBe('div');
+ expect($result->getContents(false)->getTagName())->toBe('pre');
+ expect($result->getContents())->toContain('');
+ expect($result->getContents())->toContain('test
+ test
+ test');
+ expect($result->getAllAttributes())->toBe([
+ 'class' => 'p-4 mb-6 rounded-xl bg-theme-secondary-800 overflow-x-auto',
+ ]);
+ expect($result->getContents(false)->getAllAttributes())->toBe(['class' => 'hljs line-numbers']);
+});
+
+it('should handle no codeblock type', function () {
+ $this->block->setInfo('');
+ $this->block->setLiteral('test
+ test
+ test');
+
+ $result = $this->renderer->render($this->block, $this->htmlRenderer);
+
+ expect($result)->toBeInstanceOf(HtmlElement::class);
+ expect($result->getTagName())->toBe('div');
+ expect($result->getContents(false)->getTagName())->toBe('pre');
+ expect($result->getContents())->toContain('');
+ expect($result->getContents())->toContain('test
+ test
+ test');
+ expect($result->getAllAttributes())->toBe([
+ 'class' => 'p-4 mb-6 rounded-xl bg-theme-secondary-800 overflow-x-auto',
+ ]);
+ expect($result->getContents(false)->getAllAttributes())->toBe(['class' => 'hljs line-numbers']);
+});
+
+it('should get an xml tag name', function () {
+ expect($this->renderer->getXmlTagName($this->block))->toBe('code_block');
+});
+
+it('should get xml attributes', function () {
+ $this->block->setInfo('blade');
+
+ expect($this->renderer->getXmlAttributes($this->block))->toBe(['info' => 'blade']);
+});
+
+it('should get no xml attributes if no codeblock type', function () {
+ expect($this->renderer->getXmlAttributes($this->block))->toBe([]);
+});