Passed
Branch refactor-markdown-helper (ffdce7)
by Caen
06:11
created

MarkdownConverterService   A

Complexity

Total Complexity 37

Size/Duplication

Total Lines 200
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 81
dl 0
loc 200
rs 9.44
c 1
b 0
f 0
wmc 37

18 Methods

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