xref: /plugin/combo/ComboStrap/Prism.php (revision 5cb7c87d4de446a77e815169392c9289114dce31)
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->attachRemoteJavascriptLibraryFromLiteral(
100            self::SNIPPET_NAME,
101            "$BASE_PRISM_CDN/components/prism-core.min.js",
102            "sha256-vlRYHThwdq55dA+n1BKQRzzLwFtH9VINdSI68+5JhpU=");
103        $snippetManager->attachRemoteJavascriptLibraryFromLiteral(
104            self::SNIPPET_NAME,
105            "$BASE_PRISM_CDN/plugins/toolbar/prism-toolbar.min.js",
106            "sha256-FyIVdIHL0+ppj4Q4Ft05K3wyCsYikpHIDGI7dcaBalU="
107        );
108        $snippetManager->attachRemoteCssStyleSheetFromLiteral(
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->attachRemoteJavascriptLibraryFromLiteral(
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->attachRemoteJavascriptLibraryFromLiteral(
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->attachRemoteJavascriptLibraryFromLiteral(
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->attachRemoteJavascriptLibraryFromLiteral(
130            self::SNIPPET_NAME,
131            "$BASE_PRISM_CDN/plugins/command-line/prism-command-line.min.js",
132            "sha256-9WlakH0Upf3N8DDteHlbeKCHxSsljby+G9ucUCQNiU0=");
133        $snippetManager->attachRemoteCssStyleSheetFromLiteral(
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->attachRemoteJavascriptLibraryFromLiteral(
140            self::SNIPPET_NAME,
141            "$BASE_PRISM_CDN/plugins/line-numbers/prism-line-numbers.min.js",
142            "sha256-K837BwIyiXo5k/9fCYgqUyA14bN4/Ve9P2SIT0KmZD0=");
143        $snippetManager->attachRemoteJavascriptLibraryFromLiteral(
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->attachRemoteJavascriptLibraryFromLiteral(
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->attachRemoteCssStyleSheetFromLiteral(
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        /**
295         * Command line prompt
296         * (Line element and prompt cannot be chosen together
297         * otherwise they endup on top of each other)
298         */
299        if (!$attributes->hasComponentAttribute("line-numbers")) {
300            if ($attributes->hasComponentAttribute("prompt")) {
301                $promptValue = $attributes->getValueAndRemove("prompt");
302                // prompt may be the empty string
303                if (!empty($promptValue)) {
304                    $attributes->addClassName("command-line");
305                    $attributes->addOutputAttributeValue("data-prompt", $promptValue);
306                }
307            } else {
308                /**
309                 * Default prompt
310                 */
311                switch ($language) {
312                    case "bash":
313                        $prompt = $plugin->getConf(self::CONF_BASH_PROMPT);
314                        break;
315                    case "batch":
316                        $prompt = trim($plugin->getConf(self::CONF_BATCH_PROMPT));
317                        if (!empty($prompt)) {
318                            if (!strpos($prompt, -1) == ">") {
319                                $prompt .= ">";
320                            }
321                        }
322                        break;
323                    case "powershell":
324                        $prompt = trim($plugin->getConf(self::CONF_POWERSHELL_PROMPT));
325                        if (!empty($prompt)) {
326                            if (!strpos($prompt, -1) == ">") {
327                                $prompt .= ">";
328                            }
329                        }
330                        break;
331                }
332                if(!empty($prompt)) {
333                    $attributes->addClassName("command-line");
334                    $attributes->addOutputAttributeValue("data-prompt", $prompt);
335                }
336            }
337        }
338
339        // Download
340        $attributes->addOutputAttributeValue('data-download-link', true);
341        if ($attributes->hasComponentAttribute(syntax_plugin_combo_code::FILE_PATH_KEY)) {
342            $fileSrc = $attributes->getValueAndRemove(syntax_plugin_combo_code::FILE_PATH_KEY);
343            $attributes->addOutputAttributeValue('data-src', $fileSrc);
344            $attributes->addOutputAttributeValue('data-download-link-label', "Download " . $fileSrc);
345        } else {
346            $fileName = "file." . $language;
347            $attributes->addOutputAttributeValue('data-src', $fileName);
348        }
349        /**
350         * No end of line after the pre, please, otherwise we get a new line
351         * in the code output
352         */
353        $htmlCode = $attributes->toHtmlEnterTag("pre");
354
355
356        /**
357         * Return
358         */
359        $renderer->doc .= $htmlCode;
360
361    }
362
363    /**
364     * @param Doku_Renderer_xhtml $renderer
365     * @param TagAttributes $attributes
366     */
367    public static function htmlExit(\Doku_Renderer_xhtml $renderer, $attributes = null)
368    {
369
370        if ($attributes != null) {
371            /**
372             * Display none, no rendering
373             */
374            $display = $attributes->getValueAndRemove("display");
375            if ($display != null) {
376                if ($display == "none") {
377                    return;
378                }
379            }
380        }
381        $renderer->doc .= '</code>' . DOKU_LF . '</pre>' . DOKU_LF;
382    }
383
384    /**
385     * The autoloader try to download all language
386     * Even the one such as txt that does not exist
387     * This function was created to add it conditionally
388     */
389    private static function addAutoloaderSnippet()
390    {
391        PluginUtility::getSnippetManager()
392            ->attachRemoteJavascriptLibrary(
393                self::SNIPPET_ID_AUTOLOADER,
394                self::BASE_PRISM_CDN . "/plugins/autoloader/prism-autoloader.min.js"
395            );
396    }
397
398
399}
400