Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
CodeblockFilepathProcessor
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
7 / 7
16
100.00% covered (success)
100.00%
1 / 1
 preprocess
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 postprocess
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 lineMatchesPattern
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 trimHydeDirective
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 resolveTemplate
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 injectLabelToTorchlightCodeLine
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 injectLabelToCodeLine
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Hyde\Markdown\Processing;
6
7use Hyde\Facades\Config;
8use Hyde\Markdown\Contracts\MarkdownPostProcessorContract;
9use Hyde\Markdown\Contracts\MarkdownPreProcessorContract;
10use Illuminate\Support\Facades\View;
11use Illuminate\Support\HtmlString;
12
13use function array_merge;
14use function preg_replace;
15use function str_contains;
16use function str_ireplace;
17use function str_starts_with;
18use function strtolower;
19use function str_replace;
20use function explode;
21use function implode;
22use function sprintf;
23use function trim;
24
25/**
26 * Resolves file path comments found in Markdown code blocks into a neat badge shown in the top right corner.
27 */
28class CodeblockFilepathProcessor implements MarkdownPreProcessorContract, MarkdownPostProcessorContract
29{
30    protected static string $torchlightKey = '<!-- Syntax highlighted by torchlight.dev -->';
31
32    /** @var array<string> */
33    protected static array $patterns = [
34        '// filepath: ',
35        '// filepath ',
36        '/* filepath: ',
37        '/* filepath ',
38        '# filepath: ',
39        '# filepath ',
40        '<!-- filepath: ',
41        '<!-- filepath ',
42    ];
43
44    /**
45     * Extract lines matching the shortcode pattern and replace them with meta-blocks that will be processed later.
46     */
47    public static function preprocess(string $markdown): string
48    {
49        $lines = explode("\n", $markdown);
50
51        foreach ($lines as $index => $line) {
52            if (static::lineMatchesPattern($line)) {
53                // Add the meta-block two lines before the pattern, placing it just above the code block.
54                // This prevents the meta-block from interfering with other processes during compile.
55                // We then replace these markers in the post-processor.
56                $lines[$index - 2] .= sprintf(
57                    "\n<!-- HYDE[Filepath]%s -->",
58                    trim(str_ireplace(array_merge(static::$patterns, ['-->']), '', $line))
59                );
60
61                // Remove the original comment lines
62                unset($lines[$index]);
63                // Only unset the next line if it's empty
64                if (trim($lines[$index + 1]) === '') {
65                    unset($lines[$index + 1]);
66                }
67            }
68        }
69
70        return implode("\n", $lines);
71    }
72
73    /**
74     * Process the meta-blocks added by the preprocessor, injecting the filepath badge template into the code block.
75     */
76    public static function postprocess(string $html): string
77    {
78        $lines = explode("\n", $html);
79
80        /** @var int $index */
81        foreach ($lines as $index => $line) {
82            if (str_starts_with($line, '<!-- HYDE[Filepath]')) {
83                $path = static::trimHydeDirective($line);
84                unset($lines[$index]);
85                $codeBlockLine = $index + 1;
86                $label = static::resolveTemplate($path);
87
88                $lines[$codeBlockLine] = str_contains($html, static::$torchlightKey)
89                    ? static::injectLabelToTorchlightCodeLine($label, $lines[$codeBlockLine])
90                    : static::injectLabelToCodeLine($label, $lines[$codeBlockLine]);
91            }
92        }
93
94        return implode("\n", $lines);
95    }
96
97    protected static function lineMatchesPattern(string $line): bool
98    {
99        foreach (static::$patterns as $pattern) {
100            if (str_starts_with(strtolower($line), (string) $pattern)) {
101                return true;
102            }
103        }
104
105        return false;
106    }
107
108    protected static function trimHydeDirective(string $line): string
109    {
110        return trim(str_replace('-->', '',
111            str_replace('<!-- HYDE[Filepath]', '', $line)
112        ));
113    }
114
115    protected static function resolveTemplate(string $path): string
116    {
117        return View::make('hyde::components.filepath-label', [
118            'path' => Config::getBool('markdown.allow_html', false) ? new HtmlString($path) : $path,
119        ])->render();
120    }
121
122    protected static function injectLabelToTorchlightCodeLine(string $label, string $lines): string
123    {
124        return str_replace(
125            static::$torchlightKey,
126            static::$torchlightKey.$label,
127            $lines
128        );
129    }
130
131    protected static function injectLabelToCodeLine(string $label, string $lines): string
132    {
133        return preg_replace(
134            '/<pre><code class="language-(.*?)">/',
135            '<pre><code class="language-$1">'.$label,
136            $lines
137        );
138    }
139}