Passed
Push — master ( 2fb99f...a26130 )
by Caen
03:18 queued 12s
created

MarkdownService::configurePermalinksExtension()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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