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