1<?php
2
3namespace ComboStrap;
4
5
6use Doku_Renderer_xhtml;
7use syntax_plugin_combo_code;
8
9/**
10 * Concurrent: https://highlightjs.org/ used by remark powerpoint
11 */
12class Prism
13{
14
15    const SNIPPET_NAME = 'prism';
16    /**
17     * The class used to mark the added prism code
18     * See: https://cdnjs.com/libraries/prism/
19     */
20    const BASE_PRISM_CDN = "https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0";
21    /**
22     * The default prompt for bash
23     */
24    const CONF_BASH_PROMPT = "bashPrompt";
25    /**
26     * The default prompt for batch (dos)
27     */
28    const CONF_BATCH_PROMPT = "batchPrompt";
29    /**
30     * The default prompt for powershell
31     */
32    const CONF_POWERSHELL_PROMPT = "powershellPrompt";
33
34    /**
35     * The default name of prism
36     * It does not follow the naming of the theming
37     */
38    const PRISM_THEME = "prism";
39
40    /**
41     * @var string[] https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/themes/prism-{theme}.min.css
42     *
43     * or default
44     *
45     * https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/themes/prism.min.css
46     *
47     * or
48     *
49     * https://github.com/PrismJS/prism-themes
50     *
51     * from https://cdnjs.com/libraries/prism
52     */
53    const THEMES_INTEGRITY = [
54        Prism::PRISM_THEME => "sha256-ko4j5rn874LF8dHwW29/xabhh8YBleWfvxb8nQce4Fc=",
55        "coy" => "sha256-gkHLZLptZZHaBY+jqrRkAVzOGfMa4HBhSCJteem8wy8=",
56        "dark" => "sha256-l+VX6V333ll/PXrjqG1W6DyZvDEw+50M7aAP6dcD7Qc=",
57        "funky" => "sha256-l9GTgvTMmAvPQ6IlNCd/I2FQwXVlJCLbGId7z6QlOpo=",
58        "okaidia" => "sha256-zzHVEO0xOoVm0I6bT9v5SgpRs1cYNyvEvHXW/1yCgqU=",
59        "solarizedlight" => "sha256-Lr49DyE+/KstnLdBxqZBoDYgNi6ONfZyAZw3LDhxB9I=",
60        "tomorrow" => "sha256-GxX+KXGZigSK67YPJvbu12EiBx257zuZWr0AMiT1Kpg=",
61        "twilight" => "sha256-R7PF7y9XAuz19FB93NgH/WQUVGk30iytl7EwtETrypo="
62    ];
63
64    /**
65     * The theme
66     */
67    const CONF_PRISM_THEME = "prismTheme";
68    const PRISM_THEME_DEFAULT = "tomorrow";
69    const SNIPPET_ID_AUTOLOADER = self::SNIPPET_NAME . "-autoloader";
70    const LINE_NUMBERS_ATTR = "line-numbers";
71
72
73    /**
74     *
75     * @param $theme
76     *
77     * Ter info: The theme of the default wiki is in the print.css file (search for code blocks)
78     */
79    public static function addSnippet($theme)
80    {
81        $BASE_PRISM_CDN = self::BASE_PRISM_CDN;
82
83        if ($theme == self::PRISM_THEME) {
84            $themeStyleSheet = "prism.min.css";
85        } else {
86            $themeStyleSheet = "prism-$theme.min.css";
87        }
88        $themeIntegrity = self::THEMES_INTEGRITY[$theme];
89
90        /**
91         * We miss a bottom margin
92         * as a paragraph
93         */
94        $snippetManager = PluginUtility::getSnippetManager();
95        $snippetManager->attachCssInternalStyleSheet(self::SNIPPET_NAME);
96
97        /**
98         * Javascript
99         */
100        $snippetManager->attachRemoteJavascriptLibraryFromLiteral(
101            self::SNIPPET_NAME,
102            "$BASE_PRISM_CDN/components/prism-core.min.js",
103            "sha256-vlRYHThwdq55dA+n1BKQRzzLwFtH9VINdSI68+5JhpU=");
104        $snippetManager->attachRemoteJavascriptLibraryFromLiteral(
105            self::SNIPPET_NAME,
106            "$BASE_PRISM_CDN/plugins/toolbar/prism-toolbar.min.js",
107            "sha256-FyIVdIHL0+ppj4Q4Ft05K3wyCsYikpHIDGI7dcaBalU="
108        );
109        $snippetManager->attachRemoteCssStyleSheetFromLiteral(
110            self::SNIPPET_NAME,
111            "$BASE_PRISM_CDN/plugins/toolbar/prism-toolbar.css",
112            "sha256-kK4/JIYJUKI4Zdg9ZQ7FYyRIqeWPfYKi5QZHO2n/lJI="
113        );
114        // https://prismjs.com/plugins/normalize-whitespace/
115        $snippetManager->attachRemoteJavascriptLibraryFromLiteral(
116            self::SNIPPET_NAME,
117            "$BASE_PRISM_CDN/plugins/normalize-whitespace/prism-normalize-whitespace.min.js",
118            "sha256-gBzABGbXfQYYnyr8xmDFjx6KGO9dBYuypG1QBjO76pY=");
119        // https://prismjs.com/plugins/copy-to-clipboard/
120        $snippetManager->attachRemoteJavascriptLibraryFromLiteral(
121            self::SNIPPET_NAME,
122            "$BASE_PRISM_CDN/plugins/copy-to-clipboard/prism-copy-to-clipboard.min.js",
123            "sha512-pUNGXbOrc+Y3dm5z2ZN7JYQ/2Tq0jppMDOUsN4sQHVJ9AUQpaeERCUfYYBAnaRB9r8d4gtPKMWICNhm3tRr4Fg==");
124        // https://prismjs.com/plugins/show-language/
125        $snippetManager->attachRemoteJavascriptLibraryFromLiteral(
126            self::SNIPPET_NAME,
127            "$BASE_PRISM_CDN/plugins/show-language/prism-show-language.min.js",
128            "sha256-Z3GTw2RIadLG7KyP/OYB+aAxVYzvg2PByKzYrJlA1EM=");
129        // https://prismjs.com/plugins/command-line/
130        $snippetManager->attachRemoteJavascriptLibraryFromLiteral(
131            self::SNIPPET_NAME,
132            "$BASE_PRISM_CDN/plugins/command-line/prism-command-line.min.js",
133            "sha256-9WlakH0Upf3N8DDteHlbeKCHxSsljby+G9ucUCQNiU0=");
134        $snippetManager->attachRemoteCssStyleSheetFromLiteral(
135            self::SNIPPET_NAME,
136            "$BASE_PRISM_CDN/plugins/command-line/prism-command-line.css",
137            "sha256-UvoA9bIYCYQkCMTYG5p2LM8ZpJmnC4G8k0oIc89nuQA="
138        );
139        // https://prismjs.com/plugins/line-highlight/
140        $snippetManager->attachRemoteJavascriptLibraryFromLiteral(
141            self::SNIPPET_NAME,
142            "$BASE_PRISM_CDN/plugins/line-highlight/prism-line-highlight.min.js",
143            "sha512-O5GVPBZIURR9MuNiCjSa1wNTL3w91tojKlgCXmOjWDT5a3+9Ms+wGsTkBO93PI3anfdajkJD0sJiS6qdQq7jRA==");
144        $snippetManager->attachRemoteCssStyleSheetFromLiteral(
145            self::SNIPPET_NAME,
146            "$BASE_PRISM_CDN/plugins/line-highlight/prism-line-highlight.min.css",
147            "sha512-nXlJLUeqPMp1Q3+Bd8Qds8tXeRVQscMscwysJm821C++9w6WtsFbJjPenZ8cQVMXyqSAismveQJc0C1splFDCA=="
148        );
149        //https://prismjs.com/plugins/line-numbers/
150        $snippetManager->attachRemoteJavascriptLibraryFromLiteral(
151            self::SNIPPET_NAME,
152            "$BASE_PRISM_CDN/plugins/line-numbers/prism-line-numbers.min.js",
153            "sha256-K837BwIyiXo5k/9fCYgqUyA14bN4/Ve9P2SIT0KmZD0=");
154        $snippetManager->attachRemoteJavascriptLibraryFromLiteral(
155            self::SNIPPET_NAME,
156            "$BASE_PRISM_CDN/plugins/line-numbers/prism-line-numbers.css",
157            "sha256-ye8BkHf2lHXUtqZ18U0KI3xjJ1Yv7P8lvdKBt9xmVJM="
158        );
159
160        // https://prismjs.com/plugins/download-button/-->
161        $snippetManager->attachRemoteJavascriptLibraryFromLiteral(
162            self::SNIPPET_NAME,
163            "$BASE_PRISM_CDN/plugins/download-button/prism-download-button.min.js",
164            "sha256-CQyVQ5ejeTshlzOS/eCiry40br9f4fQ9jb5e4qPl7ZA=");
165
166        // Loading the theme
167        $snippetManager->attachRemoteCssStyleSheetFromLiteral(
168            self::SNIPPET_NAME,
169            "$BASE_PRISM_CDN/themes/$themeStyleSheet",
170            $themeIntegrity
171        );
172
173        $javascriptCode = <<<EOD
174window.addEventListener('load', (event) => {
175
176    Prism.plugins.NormalizeWhitespace.setDefaults({
177        'remove-trailing': true,
178        'remove-indent': true,
179        'left-trim': true,
180        'right-trim': true,
181    });
182
183});
184EOD;
185        $snippetManager->attachJavascriptFromComponentId(self::SNIPPET_NAME, $javascriptCode);
186
187    }
188
189    /**
190     * Add the first block of prism
191     * @param \Doku_Renderer_xhtml $renderer
192     * @param TagAttributes $attributes
193     * @param \DokuWiki_Syntax_Plugin $plugin
194     */
195    public static function htmlEnter(\Doku_Renderer_xhtml $renderer, \DokuWiki_Syntax_Plugin $plugin, $attributes = null)
196    {
197
198        if ($attributes == null) {
199            $attributes = TagAttributes::createEmpty();
200        }
201
202        /**
203         * Display none, no rendering
204         */
205        $display = $attributes->getValueAndRemove("display");
206        if ($display != null) {
207            if ($display == "none") {
208                return;
209            }
210        }
211
212
213        /**
214         * Add prism theme
215         */
216        $theme = $plugin->getConf(Prism::CONF_PRISM_THEME, Prism::PRISM_THEME_DEFAULT);
217        Prism::addSnippet($theme);
218
219        /**
220         * Logical tag
221         */
222        $logicalTag = $plugin->getPluginComponent();
223        if ($attributes->getLogicalTag() != null) {
224            $logicalTag = $attributes->getLogicalTag();
225        }
226        // for the https://combostrap.com/styling/userstyle
227        $attributes->setLogicalTag($logicalTag . "-container");
228
229        /**
230         * The child element (code) of the `pre` element
231         * The container is the passed `attributes`
232         * We can then constrained in height ...
233         * It contains the language
234         */
235        $codeAttributes = TagAttributes::createEmpty($logicalTag);
236        $codeAttributes->setType($attributes->getType());
237        $language = $attributes->getValue(TagAttributes::TYPE_KEY);
238        if ($language == null) {
239            // Prism does not have any default language
240            // There is a bug has it tried to download the txt javascript
241            // but without language, there is no styling
242            $language = "txt";
243        } else {
244            $language = strtolower($language);
245            Prism::addAutoloaderSnippet();
246        }
247
248        if (in_array($language, Tag\WebCodeTag::MARKIS)) {
249            // Marki is not fully markdown
250            // because it accepts space in super set html container and
251            // prism will highlight them as indented code
252            $language = "html";
253        }
254        /**
255         * Language name mapping between the syntax name and prism
256         */
257        switch ($language) {
258            case "rsplus":
259                $language = "r";
260                break;
261            case "dos":
262            case "bat":
263                $language = "batch";
264                break;
265            case "grok":
266                $language = "regex";
267                break;
268            case "jinja":
269                // https://github.com/PrismJS/prism/issues/759
270                $language = "twig";
271                break;
272            case "apache":
273                $language = "apacheconf";
274                break;
275            case "babel":
276                $language = "jsx";
277                break;
278            case "antlr":
279                $language = "g4";
280                break;
281
282        }
283
284        StringUtility::addEolCharacterIfNotPresent($renderer->doc);
285        $codeAttributes->addClassName('language-' . $language);
286        /**
287         * Code element
288         * Don't put a fucking EOL after it
289         * Otherwise it fucked up the output as the text below a code tag is printed
290         */
291        $codeHtml = $codeAttributes->toHtmlEnterTag('code');
292        $attributes->addHtmlAfterEnterTag($codeHtml);
293
294
295        /**
296         * Pre Element
297         * Line numbers
298         */
299        $lineNumberEnabled = false;
300        if ($attributes->hasComponentAttribute(self::LINE_NUMBERS_ATTR)) {
301            $attributes->removeComponentAttribute(self::LINE_NUMBERS_ATTR);
302            $attributes->addClassName("line-numbers");
303            $lineNumberEnabled = true;
304        }
305
306
307        /**
308         * Command line prompt
309         * (Line element and prompt cannot be chosen together
310         * otherwise they endup on top of each other)
311         */
312        if (!$lineNumberEnabled) {
313            if ($attributes->hasComponentAttribute("prompt")) {
314                $promptValue = $attributes->getValueAndRemove("prompt");
315                // prompt may be the empty string
316                if (!empty($promptValue)) {
317                    $attributes->addClassName("command-line");
318                    $attributes->addOutputAttributeValue("data-prompt", $promptValue);
319                }
320            } else {
321                /**
322                 * Default prompt
323                 */
324                switch ($language) {
325                    case "bash":
326                        $prompt = $plugin->getConf(self::CONF_BASH_PROMPT);
327                        break;
328                    case "batch":
329                        $prompt = trim($plugin->getConf(self::CONF_BATCH_PROMPT));
330                        if (!empty($prompt)) {
331                            if (!strpos($prompt, -1) == ">") {
332                                $prompt .= ">";
333                            }
334                        }
335                        break;
336                    case "powershell":
337                        $prompt = trim($plugin->getConf(self::CONF_POWERSHELL_PROMPT));
338                        if (!empty($prompt)) {
339                            if (!strpos($prompt, -1) == ">") {
340                                $prompt .= ">";
341                            }
342                        }
343                        break;
344                }
345                if(!empty($prompt)) {
346                    $attributes->addClassName("command-line");
347                    $attributes->addOutputAttributeValue("data-prompt", $prompt);
348                }
349            }
350        }
351
352        /**
353         * Line highlight
354         */
355        if ($attributes->hasComponentAttribute("line-highlight")) {
356            $lineHiglight = $attributes->getValueAndRemove("line-highlight");
357            if(!empty($lineHiglight)) {
358                $attributes->addOutputAttributeValue('data-line', $lineHiglight);
359            }
360        }
361
362        // Download
363        $attributes->addOutputAttributeValue('data-download-link', true);
364        if ($attributes->hasComponentAttribute(syntax_plugin_combo_code::FILE_PATH_KEY)) {
365            $fileSrc = $attributes->getValueAndRemove(syntax_plugin_combo_code::FILE_PATH_KEY);
366            $attributes->addOutputAttributeValue('data-src', $fileSrc);
367            $attributes->addOutputAttributeValue('data-download-link-label', "Download " . $fileSrc);
368        } else {
369            $fileName = "file." . $language;
370            $attributes->addOutputAttributeValue('data-src', $fileName);
371        }
372        /**
373         * No end of line after the pre, please, otherwise we get a new line
374         * in the code output
375         */
376        $htmlCode = $attributes->toHtmlEnterTag("pre");
377
378
379        /**
380         * Return
381         */
382        $renderer->doc .= $htmlCode;
383
384    }
385
386    /**
387     * @param Doku_Renderer_xhtml $renderer
388     * @param TagAttributes $attributes
389     */
390    public static function htmlExit(\Doku_Renderer_xhtml $renderer, $attributes = null)
391    {
392
393        if ($attributes != null) {
394            /**
395             * Display none, no rendering
396             */
397            $display = $attributes->getValueAndRemove("display");
398            if ($display != null) {
399                if ($display == "none") {
400                    return;
401                }
402            }
403        }
404        $renderer->doc .= '</code>' . DOKU_LF . '</pre>' . DOKU_LF;
405    }
406
407    /**
408     * The autoloader try to download all language
409     * Even the one such as txt that does not exist
410     * This function was created to add it conditionally
411     */
412    private static function addAutoloaderSnippet()
413    {
414        PluginUtility::getSnippetManager()
415            ->attachRemoteJavascriptLibrary(
416                self::SNIPPET_ID_AUTOLOADER,
417                self::BASE_PRISM_CDN . "/plugins/autoloader/prism-autoloader.min.js"
418            );
419    }
420
421
422}
423