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