Passed
Push — master ( 301e3e...f73be1 )
by Caen
03:14 queued 11s
created

MarkdownService::configurePermalinksExtension()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 9
nc 1
nop 0
dl 0
loc 13
rs 9.9666
c 0
b 0
f 0
1
<?php
2
3
namespace Hyde\Framework\Services;
4
5
use Hyde\Framework\Actions\MarkdownConverter;
6
use Hyde\Framework\Contracts\MarkdownPostProcessorContract as PostProcessor;
7
use Hyde\Framework\Contracts\MarkdownPreProcessorContract as PreProcessor;
8
use Hyde\Framework\Helpers\Features;
9
use Hyde\Framework\Models\Pages\DocumentationPage;
10
use Hyde\Framework\Modules\Markdown\BladeDownProcessor;
11
use Hyde\Framework\Modules\Markdown\CodeblockFilepathProcessor;
12
use Hyde\Framework\Modules\Markdown\ShortcodeProcessor;
13
use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
14
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
15
use Torchlight\Commonmark\V2\TorchlightExtension;
16
17
/**
18
 * Dynamically creates a Markdown converter tailored for the target model and setup,
19
 * then converts the Markdown to HTML using both pre- and post-processors.
20
 *
21
 * @see \Hyde\Framework\Testing\Feature\MarkdownServiceTest
22
 */
23
class MarkdownService
24
{
25
    public string $markdown;
26
    public ?string $sourceModel = null;
27
28
    protected array $config = [];
29
    protected array $extensions = [];
30
    protected MarkdownConverter $converter;
31
32
    protected string $html;
33
    protected array $features = [];
34
35
    protected array $preprocessors = [];
36
    protected array $postprocessors = [];
37
38
    public function __construct(string $markdown, ?string $sourceModel = null)
39
    {
40
        $this->sourceModel = $sourceModel;
41
        $this->markdown = $markdown;
42
    }
43
44
    public function parse(): string
45
    {
46
        $this->setupConverter();
47
48
        $this->runPreProcessing();
49
50
        $this->html = $this->converter->convert($this->markdown);
51
52
        $this->runPostProcessing();
53
54
        return $this->html;
55
    }
56
57
    public function addExtension(string $extensionClassName): void
58
    {
59
        if (! in_array($extensionClassName, $this->extensions)) {
60
            $this->extensions[] = $extensionClassName;
61
        }
62
    }
63
64
    public function initializeExtension(string $extensionClassName): void
65
    {
66
        $this->converter->getEnvironment()->addExtension(new $extensionClassName());
67
    }
68
69
    protected function setupConverter(): void
70
    {
71
        $this->enableDynamicExtensions();
72
73
        $this->enableConfigDefinedExtensions();
74
75
        $this->mergeMarkdownConfiguration();
76
77
        $this->converter = new MarkdownConverter($this->config);
78
79
        foreach ($this->extensions as $extension) {
80
            $this->initializeExtension($extension);
81
        }
82
83
        $this->registerPreProcessors();
84
        $this->registerPostProcessors();
85
    }
86
87
    protected function enableDynamicExtensions(): void
88
    {
89
        if ($this->canEnablePermalinks()) {
90
            $this->configurePermalinksExtension();
91
        }
92
93
        if ($this->canEnableTorchlight()) {
94
            $this->addExtension(TorchlightExtension::class);
95
        }
96
97
        if (config('markdown.allow_html', false)) {
98
            $this->enableAllHtmlElements();
99
        }
100
    }
101
102
    protected function enableConfigDefinedExtensions(): void
103
    {
104
        foreach (config('markdown.extensions', []) as $extensionClassName) {
105
            $this->addExtension($extensionClassName);
106
        }
107
    }
108
109
    protected function mergeMarkdownConfiguration(): void
110
    {
111
        $this->config = array_merge(config('markdown.config', []), $this->config);
112
    }
113
114
    protected function registerPreProcessors(): void
115
    {
116
        $this->registerPreProcessor(BladeDownProcessor::class, config('markdown.enable_blade', false));
117
118
        $this->registerPreProcessor(ShortcodeProcessor::class);
119
        $this->registerPreProcessor(CodeblockFilepathProcessor::class);
120
    }
121
122
    protected function registerPostProcessors(): void
123
    {
124
        $this->registerPostProcessor(BladeDownProcessor::class,
125
            config('markdown.enable_blade', false));
126
127
        $this->registerPostProcessor(CodeblockFilepathProcessor::class,
128
            config('markdown.features.codeblock_filepaths', true));
129
    }
130
131
    protected function registerPreProcessor(string $class, bool $when = true): void
132
    {
133
        if (! in_array($class, $this->preprocessors) && $when) {
134
            $this->preprocessors[] = $class;
135
        }
136
    }
137
138
    protected function registerPostProcessor(string $class, bool $when = true): void
139
    {
140
        if (! in_array($class, $this->postprocessors) && $when) {
141
            $this->postprocessors[] = $class;
142
        }
143
    }
144
145
    protected function runPreProcessing(): void
146
    {
147
        /** @var PreProcessor $processor */
148
        foreach ($this->preprocessors as $processor) {
149
            $this->markdown = $processor::preprocess($this->markdown);
150
        }
151
    }
152
153
    protected function runPostProcessing(): void
154
    {
155
        if ($this->determineIfTorchlightAttributionShouldBeInjected()) {
156
            $this->html .= $this->injectTorchlightAttribution();
157
        }
158
159
        /** @var PostProcessor $processor */
160
        foreach ($this->postprocessors as $processor) {
161
            $this->html = $processor::postprocess($this->html);
162
        }
163
164
        // Remove any Hyde annotations (everything between `// HYDE!` and `HYDE! //`) (must be done last)
165
        $this->html = preg_replace('/ \/\/ HYDE!.*HYDE! \/\//s', '', $this->html);
166
    }
167
168
    public function getExtensions(): array
169
    {
170
        return $this->extensions;
171
    }
172
173
    public function removeFeature(string $feature): static
174
    {
175
        if (in_array($feature, $this->features)) {
176
            $this->features = array_diff($this->features, [$feature]);
177
        }
178
179
        return $this;
180
    }
181
182
    public function addFeature(string $feature): static
183
    {
184
        if (! in_array($feature, $this->features)) {
185
            $this->features[] = $feature;
186
        }
187
188
        return $this;
189
    }
190
191
    public function withPermalinks(): static
192
    {
193
        $this->addFeature('permalinks');
194
195
        return $this;
196
    }
197
198
    public function isDocumentationPage(): bool
199
    {
200
        return isset($this->sourceModel) && $this->sourceModel === DocumentationPage::class;
201
    }
202
203
    public function withTableOfContents(): static
204
    {
205
        $this->addFeature('table-of-contents');
206
207
        return $this;
208
    }
209
210
    public function canEnableTorchlight(): bool
211
    {
212
        return $this->hasFeature('torchlight') ||
213
            Features::hasTorchlight();
214
    }
215
216
    public function canEnablePermalinks(): bool
217
    {
218
        if ($this->hasFeature('permalinks')) {
219
            return true;
220
        }
221
222
        if ($this->isDocumentationPage() && DocumentationPage::hasTableOfContents()) {
223
            return true;
224
        }
225
226
        return false;
227
    }
228
229
    public function hasFeature(string $feature): bool
230
    {
231
        return in_array($feature, $this->features);
232
    }
233
234
    protected function determineIfTorchlightAttributionShouldBeInjected(): bool
235
    {
236
        return ! $this->isDocumentationPage()
237
            && config('torchlight.attribution.enabled', true)
238
            && str_contains($this->html, 'Syntax highlighted by torchlight.dev');
239
    }
240
241
    protected function injectTorchlightAttribution(): string
242
    {
243
        return '<br>'.$this->converter->convert(config(
244
            'torchlight.attribution.markdown',
245
            'Syntax highlighted by torchlight.dev'
246
        ));
247
    }
248
249
    protected function configurePermalinksExtension(): void
250
    {
251
        $this->addExtension(HeadingPermalinkExtension::class);
252
253
        $this->config = array_merge([
254
            'heading_permalink' => [
255
                'id_prefix' => '',
256
                'fragment_prefix' => '',
257
                'symbol' => '#',
258
                'insert' => 'after',
259
                'min_heading_level' => 2,
260
            ],
261
        ], $this->config);
262
    }
263
264
    protected function enableAllHtmlElements(): void
265
    {
266
        $this->addExtension(DisallowedRawHtmlExtension::class);
267
268
        $this->config = array_merge([
269
            'disallowed_raw_html' => [
270
                'disallowed_tags' => [],
271
            ],
272
        ], $this->config);
273
    }
274
}
275