-
Notifications
You must be signed in to change notification settings - Fork 48
Expand file tree
/
Copy pathBaseExtension.php
More file actions
executable file
·230 lines (190 loc) · 5.82 KB
/
BaseExtension.php
File metadata and controls
executable file
·230 lines (190 loc) · 5.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
<?php
namespace Torchlight\Commonmark;
use Illuminate\Support\Str;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Node\Block\AbstractBlock;
use League\CommonMark\Util\HtmlElement;
use League\CommonMark\Util\Xml;
use Torchlight\Block;
use Torchlight\Torchlight;
abstract class BaseExtension
{
/**
* @var array
*/
public static $torchlightBlocks = [];
/**
* @var callable
*/
protected $customBlockRenderer;
/**
* @param DocumentParsedEvent $event
*/
public function onDocumentParsed(DocumentParsedEvent $event)
{
$walker = $event->getDocument()->walker();
while ($event = $walker->next()) {
$node = $event->getNode();
// Only look for code nodes, and only process them upon entering.
if (!$this->isCodeNode($node) || !$event->isEntering()) {
continue;
}
$block = $this->makeTorchlightBlock($node);
// Set by hash instead of ID, because we'll be remaking all the
// blocks in the `render` function so the ID will be different,
// but the hash will always remain the same.
static::$torchlightBlocks[$block->hash()] = $block;
}
// All we need to do is fire the request, which will store
// the results in the cache. In the render function we
// use that cached value.
Torchlight::highlight(static::$torchlightBlocks);
}
/**
* @param callable $callback
* @return $this
*/
public function useCustomBlockRenderer($callback)
{
$this->customBlockRenderer = $callback;
return $this;
}
/**
* @return \Closure
*/
public function defaultBlockRenderer()
{
return function (Block $block, AbstractBlock $node) {
$inner = '';
// Clones come from multiple themes.
$blocks = $block->clones();
array_unshift($blocks, $block);
foreach ($blocks as $block) {
$inner .= "<code {$block->attrsAsString()}class='{$block->classes}' style='{$block->styles}'>{$block->highlighted}</code>";
}
return new HtmlElement(
'pre',
$node->data->getData('attributes')->export(),
$inner
);
};
}
/**
* @return array
*/
abstract protected function codeNodes();
/**
* @param $node
* @return string
*/
abstract protected function getLiteralContent($node);
/**
* Bind into a Commonmark V1 or V2 environment.
*
* @param $environment
* @param string $renderMethod
*/
protected function bind($environment, $renderMethod)
{
// We start by walking the document immediately after it's parsed
// to gather all the code blocks and send off our requests.
$environment->addEventListener(DocumentParsedEvent::class, [$this, 'onDocumentParsed']);
foreach ($this->codeNodes() as $blockType) {
// After the document is parsed, it's rendered. We register our
// renderers with a higher priority than the default ones,
// and we'll fetch the blocks straight from the cache.
$environment->{$renderMethod}($blockType, $this, 10);
}
}
/**
* @param $node
* @return bool
*/
protected function isCodeNode($node)
{
return in_array(get_class($node), $this->codeNodes());
}
/**
* @param $node
* @return Block
*/
protected function makeTorchlightBlock($node)
{
return Block::make()
->language($this->getLanguage($node))
->theme($this->getTheme($node))
->code($this->getContent($node));
}
/**
* @param $node
* @return string
*/
protected function renderNode($node)
{
$hash = $this->makeTorchlightBlock($node)->hash();
if (array_key_exists($hash, static::$torchlightBlocks)) {
$renderer = $this->customBlockRenderer ?? $this->defaultBlockRenderer();
return call_user_func($renderer, static::$torchlightBlocks[$hash], $node);
}
}
/**
* @param $node
* @return string
*/
protected function getContent($node)
{
$content = $this->getLiteralContent($node);
// Check for our file loading convention.
if (!Str::contains($content, '<<<')) {
return $content;
}
$file = trim(Str::after($content, '<<<'));
// It must be only one line, because otherwise it might be a heredoc.
if (count(explode("\n", $file)) > 1) {
return $content;
}
// Blow off the end of comments that require closing tags, e.g. <!-- -->
$file = head(explode(' ', $file));
return Torchlight::processFileContents($file) ?: $content;
}
/**
* @param $node
* @return array|mixed|null
*/
protected function getInfo($node)
{
if (!$this->isCodeNode($node)) {
return [];
}
if (!is_callable([$node, 'getInfoWords'])) {
return [];
}
$infoWords = $node->getInfoWords();
return empty($infoWords) ? [] : $infoWords;
}
/**
* @param $node
* @return string|null
*/
protected function getLanguage($node)
{
$info = $this->getInfo($node);
if (empty($info)) {
return null;
}
$language = $info[0];
return $language ? Xml::escape($language, true) : null;
}
/**
* @param $node
* @return string
*/
protected function getTheme($node)
{
foreach ($this->getInfo($node) as $item) {
if (Str::startsWith($item, 'theme:')) {
return Str::after($item, 'theme:');
}
}
}
}