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