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            case "ansi":
282                $language = "txt";
283                $snippetManager = PluginUtility::getSnippetManager();
284                $snippetManager->attachJavascriptFromComponentId("prism-ansi-to-html");
285                break;
286        }
287
288        StringUtility::addEolCharacterIfNotPresent($renderer->doc);
289        $codeAttributes->addClassName('language-' . $language);
290        /**
291         * Code element
292         * Don't put a fucking EOL after it
293         * Otherwise it fucked up the output as the text below a code tag is printed
294         */
295        $codeHtml = $codeAttributes->toHtmlEnterTag('code');
296        $attributes->addHtmlAfterEnterTag($codeHtml);
297
298
299        /**
300         * Pre Element
301         * Line numbers
302         */
303        $lineNumberEnabled = false;
304        if ($attributes->hasComponentAttribute(self::LINE_NUMBERS_ATTR)) {
305            $attributes->removeComponentAttribute(self::LINE_NUMBERS_ATTR);
306            $attributes->addClassName("line-numbers");
307            $lineNumberEnabled = true;
308        }
309
310
311        /**
312         * Command line prompt
313         * (Line element and prompt cannot be chosen together
314         * otherwise they end up on top of each other)
315         */
316        if (!$lineNumberEnabled) {
317            if ($attributes->hasComponentAttribute("prompt")) {
318                $promptValue = $attributes->getValueAndRemove("prompt");
319                // prompt may be the empty string
320                if (!empty($promptValue)) {
321                    $attributes->addClassName("command-line");
322                    $attributes->addOutputAttributeValue("data-prompt", $promptValue);
323                }
324            } else {
325                /**
326                 * Default prompt
327                 */
328                switch ($language) {
329                    case "bash":
330                        $prompt = $plugin->getConf(self::CONF_BASH_PROMPT);
331                        break;
332                    case "batch":
333                        $prompt = trim($plugin->getConf(self::CONF_BATCH_PROMPT));
334                        if (!empty($prompt)) {
335                            if (!strpos($prompt, -1) == ">") {
336                                $prompt .= ">";
337                            }
338                        }
339                        break;
340                    case "powershell":
341                        $prompt = trim($plugin->getConf(self::CONF_POWERSHELL_PROMPT));
342                        if (!empty($prompt)) {
343                            if (!strpos($prompt, -1) == ">") {
344                                $prompt .= ">";
345                            }
346                        }
347                        break;
348                }
349                if(!empty($prompt)) {
350                    $attributes->addClassName("command-line");
351                    $attributes->addOutputAttributeValue("data-prompt", $prompt);
352                }
353            }
354        }
355
356        /**
357         * Line highlight
358         */
359        if ($attributes->hasComponentAttribute("line-highlight")) {
360            $lineHighlight = $attributes->getValueAndRemove("line-highlight");
361            if(!empty($lineHighlight)) {
362                $attributes->addOutputAttributeValue('data-line', $lineHighlight);
363            }
364        }
365
366        // Download
367        $attributes->addOutputAttributeValue('data-download-link', true);
368        if ($attributes->hasComponentAttribute(syntax_plugin_combo_code::FILE_PATH_KEY)) {
369            $fileSrc = $attributes->getValueAndRemove(syntax_plugin_combo_code::FILE_PATH_KEY);
370            $attributes->addOutputAttributeValue('data-src', $fileSrc);
371            $attributes->addOutputAttributeValue('data-download-link-label', "Download " . $fileSrc);
372        } else {
373            $fileName = "file." . $language;
374            $attributes->addOutputAttributeValue('data-src', $fileName);
375        }
376        /**
377         * No end of line after the pre, please, otherwise we get a new line
378         * in the code output
379         */
380        $htmlCode = $attributes->toHtmlEnterTag("pre");
381
382
383        /**
384         * Return
385         */
386        $renderer->doc .= $htmlCode;
387
388    }
389
390    /**
391     * @param Doku_Renderer_xhtml $renderer
392     * @param TagAttributes $attributes
393     */
394    public static function htmlExit(\Doku_Renderer_xhtml $renderer, $attributes = null)
395    {
396
397        if ($attributes != null) {
398            /**
399             * Display none, no rendering
400             */
401            $display = $attributes->getValueAndRemove("display");
402            if ($display != null) {
403                if ($display == "none") {
404                    return;
405                }
406            }
407        }
408        $renderer->doc .= '</code>' . DOKU_LF . '</pre>' . DOKU_LF;
409    }
410
411    /**
412     * The autoloader try to download all language
413     * Even the one such as txt that does not exist
414     * This function was created to add it conditionally
415     */
416    private static function addAutoloaderSnippet()
417    {
418        PluginUtility::getSnippetManager()
419            ->attachRemoteJavascriptLibrary(
420                self::SNIPPET_ID_AUTOLOADER,
421                self::BASE_PRISM_CDN . "/plugins/autoloader/prism-autoloader.min.js"
422            );
423    }
424
425
426}
427