xref: /plugin/combo/ComboStrap/Prism.php (revision 37748cd8654635afbeca80942126742f0f4cc346)
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.23.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.23.0/themes/prism-{theme}.min.css
38     *
39     * or default
40     *
41     * https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.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 => "sha512-tN7Ec6zAFaVSG3TpNAKtk4DOHNpSwKHxxrsiw4GHKESGPs5njn/0sMCUMl2svV4wo4BK/rCP7juYz+zx+l6oeQ==",
51        "coy" => "sha512-CKzEMG9cS0+lcH4wtn/UnxnmxkaTFrviChikDEk1MAWICCSN59sDWIF0Q5oDgdG9lxVrvbENSV1FtjLiBnMx7Q==",
52        "dark" => "sha512-Njdz7T/p6Ud1FiTMqH87bzDxaZBsVNebOWmacBjMdgWyeIhUSFU4V52oGwo3sT+ud+lyIE98sS291/zxBfozKw==",
53        "funky" => "sha512-q59Usnbm/Dz3MeqoMEATHqIwozatJmXr/bFurDR7hpB5e2KxU+j2mp89Am9wq9jwZRaikpnKGHw4LP/Kr9soZQ==",
54        "okaidia" => "sha512-mIs9kKbaw6JZFfSuo+MovjU+Ntggfoj8RwAmJbVXQ5mkAX5LlgETQEweFPI18humSPHymTb5iikEOKWF7I8ncQ==",
55        "solarizedlight" => "sha512-fibfhB71IpdEKqLKXP/96WuX1cTMmvZioYp7T6I+lTbvJrrjEGeyYdAf09GHpFptF8toQ32woGZ8bw9+HjZc0A==",
56        "tomorrow" => "sha512-vswe+cgvic/XBoF1OcM/TeJ2FW0OofqAVdCZiEYkd6dwGXthvkSFWOoGGJgS2CW70VK5dQM5Oh+7ne47s74VTg==",
57        "twilight" => "sha512-akb4nfKzpmhujLUyollw5waBPeohuVf0Z5+cL+4Ngc4Db+V8szzx6ZTujguFjpmD076W8LImVIbOblmQ+vZMKA=="
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("src" => "$BASE_PRISM_CDN/components/prism-core.min.js");
96        $tags['script'][] = array("src" => "$BASE_PRISM_CDN/plugins/toolbar/prism-toolbar.min.js");
97        // https://prismjs.com/plugins/normalize-whitespace/
98        $tags['script'][] = array("src" => "$BASE_PRISM_CDN/plugins/normalize-whitespace/prism-normalize-whitespace.min.js");
99        // https://prismjs.com/plugins/show-language/
100        $tags['script'][] = array("src" => "$BASE_PRISM_CDN/plugins/show-language/prism-show-language.min.js");
101        // https://prismjs.com/plugins/command-line/
102        $tags['script'][] = array("src" => "$BASE_PRISM_CDN/plugins/command-line/prism-command-line.min.js");
103        //https://prismjs.com/plugins/line-numbers/
104        $tags['script'][] = array("src" => "$BASE_PRISM_CDN/plugins/line-numbers/prism-line-numbers.min.js");
105        // https://prismjs.com/plugins/download-button/-->
106        $tags['script'][] = array(
107            "src" => "$BASE_PRISM_CDN/plugins/download-button/prism-download-button.min.js",
108            "integrity" => "sha512-rGJwSZEEYPBQjqYxrdg6Ug/6i763XQogKx+N/GF1rCGvfmhIlIUFxCjc4FmEdCu5dvovqxHsoe3IPMKP+KlgNQ==",
109            "crossorigin" => "anonymous"
110        );
111
112        PluginUtility::getSnippetManager()->upsertTagsForBar(self::SNIPPET_NAME, $tags);
113
114        $javascriptCode = <<<EOD
115document.addEventListener('DOMContentLoaded', (event) => {
116
117    if (typeof self === 'undefined' || !self.Prism || !self.document) {
118        return;
119    }
120
121    // Loading the css from https://cdnjs.com/libraries/prism
122    const head = document.querySelector('head');
123    const baseCdn = "$BASE_PRISM_CDN";
124    const stylesheets = [
125        ["themes/$themeStyleSheet", "$themeIntegrity"],
126        ["plugins/toolbar/prism-toolbar.min.css","sha512-DSAA0ziYwggOJ3QyWFZhIaU8bSwQLyfnyIrmShRLBdJMtiYKT7Ju35ujBCZ6ApK3HURt34p2xNo+KX9ebQNEPQ=="],
127        /*https://prismjs.com/plugins/command-line/*/
128        ["plugins/command-line/prism-command-line.min.css","sha512-4Y1uID1tEWeqDdbb7452znwjRVwseCy9kK9BNA7Sv4PlMroQzYRznkoWTfRURSADM/SbfZSbv/iW5sNpzSbsYg=="],
129        /*https://prismjs.com/plugins/line-numbers/*/
130        ["plugins/line-numbers/prism-line-numbers.min.css","sha512-cbQXwDFK7lj2Fqfkuxbo5iD1dSbLlJGXGpfTDqbggqjHJeyzx88I3rfwjS38WJag/ihH7lzuGlGHpDBymLirZQ=="]
131    ];
132
133    stylesheets.forEach(stylesheet => {
134            let link = document.createElement('link');
135            link.rel="stylesheet"
136            link.href=baseCdn+"/"+stylesheet[0];
137            link.integrity=stylesheet[1];
138            link.crossOrigin="anonymous";
139            head.append(link);
140        }
141    )
142
143
144    Prism.plugins.NormalizeWhitespace.setDefaults({
145        'remove-trailing': true,
146        'remove-indent': true,
147        'left-trim': true,
148        'right-trim': true,
149    });
150
151    if (!Prism.plugins.toolbar) {
152        console.warn('Copy to Clipboard plugin loaded before Toolbar plugin.');
153
154        return;
155    }
156
157    let ClipboardJS = window.ClipboardJS || undefined;
158
159    if (!ClipboardJS && typeof require === 'function') {
160        ClipboardJS = require('clipboard');
161    }
162
163    const callbacks = [];
164
165    if (!ClipboardJS) {
166        const script = document.createElement('script');
167        const head = document.querySelector('head');
168
169        script.onload = function() {
170            ClipboardJS = window.ClipboardJS;
171
172            if (ClipboardJS) {
173                while (callbacks.length) {
174                    callbacks.pop()();
175                }
176            }
177        };
178
179        script.src = 'https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.0/clipboard.min.js';
180        head.appendChild(script);
181    }
182
183    Prism.plugins.toolbar.registerButton('copy-to-clipboard', function (env) {
184        var linkCopy = document.createElement('button');
185        linkCopy.textContent = 'Copy';
186        linkCopy.setAttribute('type', 'button');
187
188        var element = env.element;
189
190        if (!ClipboardJS) {
191            callbacks.push(registerClipboard);
192        } else {
193            registerClipboard();
194        }
195
196        return linkCopy;
197
198        function registerClipboard() {
199            var clip = new ClipboardJS(linkCopy, {
200                'text': function () {
201                    return element.textContent;
202                }
203            });
204
205            clip.on('success', function() {
206                linkCopy.textContent = 'Copied!';
207
208                resetText();
209            });
210            clip.on('error', function () {
211                linkCopy.textContent = 'Press Ctrl+C to copy';
212
213                resetText();
214            });
215        }
216
217        function resetText() {
218            setTimeout(function () {
219                linkCopy.textContent = 'Copy';
220            }, 5000);
221        }
222    });
223
224});
225EOD;
226        PluginUtility::getSnippetManager()->upsertJavascriptForBar(self::SNIPPET_NAME, $javascriptCode);
227
228    }
229
230    /**
231     * Add the first block of prism
232     * @param \Doku_Renderer_xhtml $renderer
233     * @param TagAttributes $attributes
234     * @param \DokuWiki_Syntax_Plugin $plugin
235     */
236    public static function htmlEnter(\Doku_Renderer_xhtml $renderer, \DokuWiki_Syntax_Plugin $plugin, $attributes = null)
237    {
238
239        if ($attributes == null) {
240            $attributes = TagAttributes::createEmpty();
241        }
242
243        /**
244         * Display none, no rendering
245         */
246        $display = $attributes->getValueAndRemove("display");
247        if ($display != null) {
248            if ($display == "none") {
249                return;
250            }
251        }
252
253
254
255        /**
256         * Add prism theme
257         */
258        $theme = $plugin->getConf(Prism::CONF_PRISM_THEME);
259        Prism::addSnippet($theme);
260
261        /**
262         * Logical tag
263         */
264        $logicalTag = $plugin->getPluginComponent();
265        if ($attributes->getLogicalTag() != null) {
266            $logicalTag = $attributes->getLogicalTag();
267        }
268        // for the https://combostrap.com/styling/userstyle
269        $attributes->setLogicalTag($logicalTag."-container");
270
271        /**
272         * The child element (code) of the `pre` element
273         * The container is the passed `attributes`
274         * We can then constrained in height ...
275         * It contains the language
276         */
277        $codeAttributes = TagAttributes::createEmpty($logicalTag);
278        $codeAttributes->setType($attributes->getType());
279        $language = $attributes->getValue(TagAttributes::TYPE_KEY);
280        if ($language == null) {
281            // Prism does not have any default language
282            // There is a bug has it tried to download the txt javascript
283            // but without language, there is no styling
284            $language = "txt";
285        } else {
286            $language = strtolower($language);
287            Prism::addAutoloaderSnippet();
288        }
289
290        if (in_array($language, \syntax_plugin_combo_webcode::MARKIS)) {
291            // Marki is not fully markdown
292            // because it accepts space in super set html container and
293            // prism will highlight them as indented code
294            $language = "html";
295        }
296        /**
297         * Language name mapping between the dokuwiki default
298         * and prism
299         */
300        if ($language == "rsplus") {
301            $language = "r";
302        }
303        if ($language == "dos") {
304            $language = "batch";
305        }
306        if ($language == "apache") {
307            $language = "apacheconf";
308        }
309        if ($language == "babel") {
310            $language = "javascript";
311        }
312
313        StringUtility::addEolCharacterIfNotPresent($renderer->doc);
314        $codeAttributes->addClassName('language-' . $language);
315        /**
316         * Code element
317         * Don't put a fucking EOL after it
318         * Otherwise it fucked up the output as the text below a code tag is printed
319         */
320        $codeHtml = $codeAttributes->toHtmlEnterTag('code');
321        $attributes->addHtmlAfterEnterTag($codeHtml);
322
323
324        /**
325         * Pre Element
326         * Line numbers
327         */
328        if ($attributes->hasComponentAttribute("line-numbers")) {
329            $attributes->removeComponentAttribute("line-numbers");
330            $attributes->addClassName('line-numbers');
331        }
332
333
334        // Command line
335        if ($attributes->hasComponentAttribute("prompt")) {
336            $attributes->addClassName("command-line");
337            $attributes->addHtmlAttributeValue("data-prompt", $attributes->getValueAndRemove("prompt"));
338        } else {
339            switch ($language) {
340                case "bash":
341                    $attributes->addClassName("command-line");
342                    $attributes->addHtmlAttributeValue("data-prompt", $plugin->getConf(self::CONF_BASH_PROMPT));
343                    break;
344                case "batch":
345                    $attributes->addClassName("command-line");
346                    $batch = trim($plugin->getConf(self::CONF_BATCH_PROMPT));
347                    if (!empty($batch)) {
348                        if (!strpos($batch, -1) == ">") {
349                            $batch .= ">";
350                        }
351                    }
352                    $attributes->addHtmlAttributeValue("data-prompt", $batch);
353                    break;
354                case "powershell":
355                    $attributes->addClassName("command-line");
356                    $powerShell = trim($plugin->getConf(self::CONF_POWERSHELL_PROMPT));
357                    if (!empty($powerShell)) {
358                        if (!strpos($powerShell, -1) == ">") {
359                            $powerShell .= ">";
360                        }
361                    }
362                    $attributes->addHtmlAttributeValue("data-prompt", $powerShell);
363                    break;
364            }
365        }
366
367        // Download
368        $attributes->addHtmlAttributeValue('data-download-link', true);
369        if ($attributes->hasComponentAttribute(syntax_plugin_combo_code::FILE_PATH_KEY)) {
370            $fileSrc = $attributes->getValueAndRemove(syntax_plugin_combo_code::FILE_PATH_KEY);
371            $attributes->addHtmlAttributeValue('data-src', $fileSrc);
372            $attributes->addHtmlAttributeValue('data-download-link-label', "Download " . $fileSrc);
373        } else {
374            $fileName = "file." . $language;
375            $attributes->addHtmlAttributeValue('data-src', $fileName);
376        }
377        /**
378         * No end of line after the pre, please, otherwise we get a new line
379         * in the code output
380         */
381        $htmlCode = $attributes->toHtmlEnterTag("pre");
382
383
384        /**
385         * Return
386         */
387        $renderer->doc .= $htmlCode;
388
389    }
390
391    /**
392     * @param Doku_Renderer_xhtml $renderer
393     * @param TagAttributes $attributes
394     */
395    public static function htmlExit(\Doku_Renderer_xhtml $renderer, $attributes = null)
396    {
397
398        if ($attributes != null) {
399            /**
400             * Display none, no rendering
401             */
402            $display = $attributes->getValueAndRemove("display");
403            if ($display != null) {
404                if ($display == "none") {
405                    return;
406                }
407            }
408        }
409        $renderer->doc .= '</code>' . DOKU_LF . '</pre>' . DOKU_LF;
410    }
411
412    /**
413     * The autoloader try to download all language
414     * Even the one such as txt that does not exist
415     * This function was created to add it conditionally
416     */
417    private static function addAutoloaderSnippet()
418    {
419        $tags = [];
420        $tags['script'][] = array("src" => self::BASE_PRISM_CDN . "/plugins/autoloader/prism-autoloader.min.js");
421        PluginUtility::getSnippetManager()->upsertTagsForBar(self::SNIPPET_ID_AUTOLOADER, $tags);
422    }
423
424
425}
426