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