Skip to content

Commit bb1ea92

Browse files
committed
Commonmark 2.0 WIP
1 parent 45741fe commit bb1ea92

6 files changed

Lines changed: 460 additions & 4 deletions

File tree

.github/workflows/tests.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ jobs:
1515
matrix:
1616
php: [ 7.3, 7.4, 8.0 ]
1717
laravel: [ 7.*, 8.* ]
18+
commonmark: [^1.0, ^2.0]
1819
dependency-version: [ prefer-lowest, prefer-stable ]
1920
include:
2021
- laravel: 7.*
@@ -23,7 +24,12 @@ jobs:
2324
- laravel: 8.*
2425
testbench: 6.*
2526

26-
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }}
27+
exclude:
28+
# Commonmark 2.0 requires PHP 7.4
29+
- commonmark: ^2.0
30+
php: 7.3
31+
32+
name: P${{ matrix.php }} - L${{ matrix.laravel }} - C${{ matrix.commonmark }} - ${{ matrix.dependency-version }}
2733

2834
steps:
2935
- name: Checkout code
@@ -45,7 +51,7 @@ jobs:
4551
- name: Install dependencies
4652
run: |
4753
composer self-update
48-
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
54+
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "league/commonmark:${{ matrix.commonmark }}" --no-interaction --no-update
4955
composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction
5056
5157
- name: Execute tests

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
],
2020
"require": {
2121
"php": "^7.2|^8.0",
22-
"league/commonmark": "^1.5",
23-
"torchlight/torchlight-laravel": "^0.5.5"
22+
"torchlight/torchlight-laravel": "^0.5.5",
23+
"league/commonmark": "^2.0"
2424
},
2525
"require-dev": {
2626
"orchestra/testbench": "^5.0|^6.0",

src/TorchlightExtensionV2.php

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<?php
2+
3+
namespace Torchlight\Commonmark;
4+
5+
6+
use Illuminate\Support\Str;
7+
use League\CommonMark\Environment\EnvironmentBuilderInterface;
8+
use League\CommonMark\Event\DocumentParsedEvent;
9+
use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
10+
use League\CommonMark\Extension\CommonMark\Node\Block\IndentedCode;
11+
use League\CommonMark\Extension\ExtensionInterface;
12+
use League\CommonMark\Node\Node;
13+
use League\CommonMark\Renderer\ChildNodeRendererInterface;
14+
use League\CommonMark\Renderer\NodeRendererInterface;
15+
use League\CommonMark\Util\Xml;
16+
use Torchlight\Block;
17+
use Torchlight\Torchlight;
18+
19+
class TorchlightExtensionV2 implements ExtensionInterface, NodeRendererInterface
20+
{
21+
public static $torchlightBlocks = [];
22+
23+
public function register(EnvironmentBuilderInterface $environment): void
24+
{
25+
// We start by walking the document immediately after it's parsed
26+
// to gather up all the code blocks and send off our requests.
27+
$environment->addEventListener(DocumentParsedEvent::class, [$this, 'onDocumentParsed']);
28+
29+
// After the document is parsed, it's rendered. We register our
30+
// renderers with a higher priority than the default ones,
31+
// and we'll fetch the blocks straight from the cache.
32+
$environment->addRenderer(FencedCode::class, $this, 10);
33+
$environment->addRenderer(IndentedCode::class, $this, 10);
34+
}
35+
36+
public function onDocumentParsed(DocumentParsedEvent $event)
37+
{
38+
$walker = $event->getDocument()->walker();
39+
40+
while ($event = $walker->next()) {
41+
/** @var FencedCode|IndentedCode $node */
42+
$node = $event->getNode();
43+
44+
// Only look for code nodes, and only process them upon entering.
45+
if (!$this->isCodeNode($node) || !$event->isEntering()) {
46+
continue;
47+
}
48+
49+
$block = $this->makeTorchlightBlock($node);
50+
51+
// Set by hash instead of ID, because we'll be remaking all the
52+
// blocks in the `render` function so the ID will be different,
53+
// but the hash will always remain the same.
54+
static::$torchlightBlocks[$block->hash()] = $block;
55+
}
56+
57+
// All we need to do is fire the request, which will store
58+
// the results in the cache. In the render function we
59+
// use that cached value.
60+
Torchlight::highlight(static::$torchlightBlocks);
61+
}
62+
63+
public function render(Node $node, ChildNodeRendererInterface $childRenderer)
64+
{
65+
$hash = $this->makeTorchlightBlock($node)->hash();
66+
67+
if (array_key_exists($hash, static::$torchlightBlocks)) {
68+
$renderer = $this->customBlockRenderer ?? $this->defaultBlockRenderer();
69+
70+
return call_user_func($renderer, static::$torchlightBlocks[$hash]);
71+
}
72+
}
73+
74+
public function useCustomBlockRenderer($callback)
75+
{
76+
$this->customBlockRenderer = $callback;
77+
78+
return $this;
79+
}
80+
81+
public function defaultBlockRenderer()
82+
{
83+
return function (Block $block) {
84+
return "<pre><code class='{$block->classes}' style='{$block->styles}'>{$block->highlighted}</code></pre>";
85+
};
86+
}
87+
88+
protected function makeTorchlightBlock($node)
89+
{
90+
return Block::make()
91+
->language($this->getLanguage($node))
92+
->theme($this->getTheme($node))
93+
->code($this->getContent($node));
94+
}
95+
96+
protected function isCodeNode($node)
97+
{
98+
return $node instanceof FencedCode || $node instanceof IndentedCode;
99+
}
100+
101+
protected function getContent($node)
102+
{
103+
$content = $node->getLiteral();
104+
105+
if (!Str::startsWith($content, '<<<')) {
106+
return $content;
107+
}
108+
109+
$file = trim(Str::after($content, '<<<'));
110+
111+
// It must be only one line, because otherwise it might be a heredoc.
112+
if (count(explode("\n", $file)) > 1) {
113+
return $content;
114+
}
115+
116+
return Torchlight::processFileContents($file) ?: $content;
117+
}
118+
119+
protected function getInfo($node)
120+
{
121+
if (!$this->isCodeNode($node) || $node instanceof IndentedCode) {
122+
return null;
123+
}
124+
125+
$infoWords = $node->getInfoWords();
126+
127+
return empty($infoWords) ? [] : $infoWords;
128+
}
129+
130+
protected function getLanguage($node)
131+
{
132+
$language = $this->getInfo($node)[0];
133+
134+
return $language ? Xml::escape($language, true) : null;
135+
}
136+
137+
protected function getTheme($node)
138+
{
139+
foreach ($this->getInfo($node) as $item) {
140+
if (Str::startsWith($item, 'theme:')) {
141+
return Str::after($item, 'theme:');
142+
}
143+
}
144+
}
145+
}

test

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/bash
2+
3+
rm composer.lock
4+
composer require "league/commonmark:^1.0" -q
5+
6+
./vendor/bin/phpunit
7+
8+
composer require "league/commonmark:^2.0" -q
9+
10+
./vendor/bin/phpunit
11+

tests/CodeRendererTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ class CodeRendererTest extends TestCase
1414
{
1515
protected function getEnvironmentSetUp($app)
1616
{
17+
if (!interface_exists('League\\CommonMark\\ConfigurableEnvironmentInterface')) {
18+
$this->markTestSkipped('CommonMark V1 not detected.');
19+
}
20+
1721
config()->set('torchlight.token', 'token');
1822

1923
$ids = [

0 commit comments

Comments
 (0)