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