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
71
72    /**
73     *
74     * @param $theme
75     *
76     * Ter info: The theme of the default wiki is in the print.css file (search for code blocks)
77     */
78    public static function addSnippet($theme)
79    {
80        $BASE_PRISM_CDN = self::BASE_PRISM_CDN;
81
82        if ($theme == self::PRISM_THEME) {
83            $themeStyleSheet = "prism.min.css";
84        } else {
85            $themeStyleSheet = "prism-$theme.min.css";
86        }
87        $themeIntegrity = self::THEMES_INTEGRITY[$theme];
88
89        /**
90         * We miss a bottom margin
91         * as a paragraph
92         */
93        $snippetManager = PluginUtility::getSnippetManager();
94        $snippetManager->attachCssInternalStyleSheet(self::SNIPPET_NAME);
95
96        /**
97         * Javascript
98         */
99        $snippetManager->attachRemoteJavascriptLibrary(
100            self::SNIPPET_NAME,
101            "$BASE_PRISM_CDN/components/prism-core.min.js",
102            "sha256-vlRYHThwdq55dA+n1BKQRzzLwFtH9VINdSI68+5JhpU=");
103        $snippetManager->attachRemoteJavascriptLibrary(
104            self::SNIPPET_NAME,
105            "$BASE_PRISM_CDN/plugins/toolbar/prism-toolbar.min.js",
106            "sha256-FyIVdIHL0+ppj4Q4Ft05K3wyCsYikpHIDGI7dcaBalU="
107        );
108        $snippetManager->attachRemoteCssStyleSheet(
109            self::SNIPPET_NAME,
110            "$BASE_PRISM_CDN/plugins/toolbar/prism-toolbar.css",
111            "sha256-kK4/JIYJUKI4Zdg9ZQ7FYyRIqeWPfYKi5QZHO2n/lJI="
112        );
113        // https://prismjs.com/plugins/normalize-whitespace/
114        $snippetManager->attachRemoteJavascriptLibrary(
115            self::SNIPPET_NAME,
116            "$BASE_PRISM_CDN/plugins/normalize-whitespace/prism-normalize-whitespace.min.js",
117            "sha256-gBzABGbXfQYYnyr8xmDFjx6KGO9dBYuypG1QBjO76pY=");
118        // https://prismjs.com/plugins/copy-to-clipboard/
119        $snippetManager->attachRemoteJavascriptLibrary(
120            self::SNIPPET_NAME,
121            "$BASE_PRISM_CDN/plugins/copy-to-clipboard/prism-copy-to-clipboard.min.js",
122            "sha512-pUNGXbOrc+Y3dm5z2ZN7JYQ/2Tq0jppMDOUsN4sQHVJ9AUQpaeERCUfYYBAnaRB9r8d4gtPKMWICNhm3tRr4Fg==");
123        // https://prismjs.com/plugins/show-language/
124        $snippetManager->attachRemoteJavascriptLibrary(
125            self::SNIPPET_NAME,
126            "$BASE_PRISM_CDN/plugins/show-language/prism-show-language.min.js",
127            "sha256-Z3GTw2RIadLG7KyP/OYB+aAxVYzvg2PByKzYrJlA1EM=");
128        // https://prismjs.com/plugins/command-line/
129        $snippetManager->attachRemoteJavascriptLibrary(
130            self::SNIPPET_NAME,
131            "$BASE_PRISM_CDN/plugins/command-line/prism-command-line.min.js",
132            "sha256-9WlakH0Upf3N8DDteHlbeKCHxSsljby+G9ucUCQNiU0=");
133        $snippetManager->attachRemoteCssStyleSheet(
134            self::SNIPPET_NAME,
135            "$BASE_PRISM_CDN/plugins/command-line/prism-command-line.css",
136            "sha256-UvoA9bIYCYQkCMTYG5p2LM8ZpJmnC4G8k0oIc89nuQA="
137        );
138        //https://prismjs.com/plugins/line-numbers/
139        $snippetManager->attachRemoteJavascriptLibrary(
140            self::SNIPPET_NAME,
141            "$BASE_PRISM_CDN/plugins/line-numbers/prism-line-numbers.min.js",
142            "sha256-K837BwIyiXo5k/9fCYgqUyA14bN4/Ve9P2SIT0KmZD0=");
143        $snippetManager->attachRemoteCssStyleSheet(
144            self::SNIPPET_NAME,
145            "$BASE_PRISM_CDN/plugins/line-numbers/prism-line-numbers.css",
146            "sha256-ye8BkHf2lHXUtqZ18U0KI3xjJ1Yv7P8lvdKBt9xmVJM="
147        );
148
149        // https://prismjs.com/plugins/download-button/-->
150        $snippetManager->attachRemoteJavascriptLibrary(
151            self::SNIPPET_NAME,
152            "$BASE_PRISM_CDN/plugins/download-button/prism-download-button.min.js",
153            "sha256-CQyVQ5ejeTshlzOS/eCiry40br9f4fQ9jb5e4qPl7ZA=");
154
155        // Loading the theme
156        $snippetManager->attachRemoteCssStyleSheet(
157            self::SNIPPET_NAME,
158            "$BASE_PRISM_CDN/themes/$themeStyleSheet",
159            $themeIntegrity
160        );
161
162        $javascriptCode = <<<EOD
163window.addEventListener('load', (event) => {
164
165    Prism.plugins.NormalizeWhitespace.setDefaults({
166        'remove-trailing': true,
167        'remove-indent': true,
168        'left-trim': true,
169        'right-trim': true,
170    });
171
172});
173EOD;
174        $snippetManager->attachJavascriptFromComponentId(self::SNIPPET_NAME, $javascriptCode);
175
176    }
177
178    /**
179     * Add the first block of prism
180     * @param \Doku_Renderer_xhtml $renderer
181     * @param TagAttributes $attributes
182     * @param \DokuWiki_Syntax_Plugin $plugin
183     */
184    public static function htmlEnter(\Doku_Renderer_xhtml $renderer, \DokuWiki_Syntax_Plugin $plugin, $attributes = null)
185    {
186
187        if ($attributes == null) {
188            $attributes = TagAttributes::createEmpty();
189        }
190
191        /**
192         * Display none, no rendering
193         */
194        $display = $attributes->getValueAndRemove("display");
195        if ($display != null) {
196            if ($display == "none") {
197                return;
198            }
199        }
200
201
202        /**
203         * Add prism theme
204         */
205        $theme = $plugin->getConf(Prism::CONF_PRISM_THEME,Prism::PRISM_THEME_DEFAULT);
206        Prism::addSnippet($theme);
207
208        /**
209         * Logical tag
210         */
211        $logicalTag = $plugin->getPluginComponent();
212        if ($attributes->getLogicalTag() != null) {
213            $logicalTag = $attributes->getLogicalTag();
214        }
215        // for the https://combostrap.com/styling/userstyle
216        $attributes->setLogicalTag($logicalTag . "-container");
217
218        /**
219         * The child element (code) of the `pre` element
220         * The container is the passed `attributes`
221         * We can then constrained in height ...
222         * It contains the language
223         */
224        $codeAttributes = TagAttributes::createEmpty($logicalTag);
225        $codeAttributes->setType($attributes->getType());
226        $language = $attributes->getValue(TagAttributes::TYPE_KEY);
227        if ($language == null) {
228            // Prism does not have any default language
229            // There is a bug has it tried to download the txt javascript
230            // but without language, there is no styling
231            $language = "txt";
232        } else {
233            $language = strtolower($language);
234            Prism::addAutoloaderSnippet();
235        }
236
237        if (in_array($language, Tag\WebCodeTag::MARKIS)) {
238            // Marki is not fully markdown
239            // because it accepts space in super set html container and
240            // prism will highlight them as indented code
241            $language = "html";
242        }
243        /**
244         * Language name mapping between the syntax name and prism
245         */
246        switch ($language) {
247            case "rsplus":
248                $language = "r";
249                break;
250            case "dos":
251            case "bat":
252                $language = "batch";
253                break;
254            case "grok":
255                $language = "regex";
256                break;
257            case "jinja":
258                // https://github.com/PrismJS/prism/issues/759
259                $language = "twig";
260                break;
261            case "apache":
262                $language = "apacheconf";
263                break;
264            case "babel":
265                $language = "jsx";
266                break;
267            case "antlr":
268                $language = "g4";
269                break;
270
271        }
272
273        StringUtility::addEolCharacterIfNotPresent($renderer->doc);
274        $codeAttributes->addClassName('language-' . $language);
275        /**
276         * Code element
277         * Don't put a fucking EOL after it
278         * Otherwise it fucked up the output as the text below a code tag is printed
279         */
280        $codeHtml = $codeAttributes->toHtmlEnterTag('code');
281        $attributes->addHtmlAfterEnterTag($codeHtml);
282
283
284        /**
285         * Pre Element
286         * Line numbers
287         */
288        if ($attributes->hasComponentAttribute("line-numbers")) {
289            $attributes->removeComponentAttribute("line-numbers");
290            $attributes->addClassName('line-numbers');
291        }
292
293
294        // Command line
295        if ($attributes->hasComponentAttribute("prompt")) {
296            $attributes->addClassName("command-line");
297            $attributes->addOutputAttributeValue("data-prompt", $attributes->getValueAndRemove("prompt"));
298        } else {
299            switch ($language) {
300                case "bash":
301                    $attributes->addClassName("command-line");
302                    $attributes->addOutputAttributeValue("data-prompt", $plugin->getConf(self::CONF_BASH_PROMPT));
303                    break;
304                case "batch":
305                    $attributes->addClassName("command-line");
306                    $batch = trim($plugin->getConf(self::CONF_BATCH_PROMPT));
307                    if (!empty($batch)) {
308                        if (!strpos($batch, -1) == ">") {
309                            $batch .= ">";
310                        }
311                    }
312                    $attributes->addOutputAttributeValue("data-prompt", $batch);
313                    break;
314                case "powershell":
315                    $attributes->addClassName("command-line");
316                    $powerShell = trim($plugin->getConf(self::CONF_POWERSHELL_PROMPT));
317                    if (!empty($powerShell)) {
318                        if (!strpos($powerShell, -1) == ">") {
319                            $powerShell .= ">";
320                        }
321                    }
322                    $attributes->addOutputAttributeValue("data-prompt", $powerShell);
323                    break;
324            }
325        }
326
327        // Download
328        $attributes->addOutputAttributeValue('data-download-link', true);
329        if ($attributes->hasComponentAttribute(syntax_plugin_combo_code::FILE_PATH_KEY)) {
330            $fileSrc = $attributes->getValueAndRemove(syntax_plugin_combo_code::FILE_PATH_KEY);
331            $attributes->addOutputAttributeValue('data-src', $fileSrc);
332            $attributes->addOutputAttributeValue('data-download-link-label', "Download " . $fileSrc);
333        } else {
334            $fileName = "file." . $language;
335            $attributes->addOutputAttributeValue('data-src', $fileName);
336        }
337        /**
338         * No end of line after the pre, please, otherwise we get a new line
339         * in the code output
340         */
341        $htmlCode = $attributes->toHtmlEnterTag("pre");
342
343
344        /**
345         * Return
346         */
347        $renderer->doc .= $htmlCode;
348
349    }
350
351    /**
352     * @param Doku_Renderer_xhtml $renderer
353     * @param TagAttributes $attributes
354     */
355    public static function htmlExit(\Doku_Renderer_xhtml $renderer, $attributes = null)
356    {
357
358        if ($attributes != null) {
359            /**
360             * Display none, no rendering
361             */
362            $display = $attributes->getValueAndRemove("display");
363            if ($display != null) {
364                if ($display == "none") {
365                    return;
366                }
367            }
368        }
369        $renderer->doc .= '</code>' . DOKU_LF . '</pre>' . DOKU_LF;
370    }
371
372    /**
373     * The autoloader try to download all language
374     * Even the one such as txt that does not exist
375     * This function was created to add it conditionally
376     */
377    private static function addAutoloaderSnippet()
378    {
379        PluginUtility::getSnippetManager()
380            ->attachRemoteJavascriptLibrary(
381                self::SNIPPET_ID_AUTOLOADER,
382                self::BASE_PRISM_CDN . "/plugins/autoloader/prism-autoloader.min.js"
383            );
384    }
385
386
387}
388