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 if ($language == "rsplus") { 324 $language = "r"; 325 } 326 if ($language == "dos") { 327 $language = "batch"; 328 } 329 if ($language == "apache") { 330 $language = "apacheconf"; 331 } 332 if ($language == "babel") { 333 $language = "javascript"; 334 } 335 336 StringUtility::addEolCharacterIfNotPresent($renderer->doc); 337 $codeAttributes->addClassName('language-' . $language); 338 /** 339 * Code element 340 * Don't put a fucking EOL after it 341 * Otherwise it fucked up the output as the text below a code tag is printed 342 */ 343 $codeHtml = $codeAttributes->toHtmlEnterTag('code'); 344 $attributes->addHtmlAfterEnterTag($codeHtml); 345 346 347 /** 348 * Pre Element 349 * Line numbers 350 */ 351 if ($attributes->hasComponentAttribute("line-numbers")) { 352 $attributes->removeComponentAttribute("line-numbers"); 353 $attributes->addClassName('line-numbers'); 354 } 355 356 357 // Command line 358 if ($attributes->hasComponentAttribute("prompt")) { 359 $attributes->addClassName("command-line"); 360 $attributes->addHtmlAttributeValue("data-prompt", $attributes->getValueAndRemove("prompt")); 361 } else { 362 switch ($language) { 363 case "bash": 364 $attributes->addClassName("command-line"); 365 $attributes->addHtmlAttributeValue("data-prompt", $plugin->getConf(self::CONF_BASH_PROMPT)); 366 break; 367 case "batch": 368 $attributes->addClassName("command-line"); 369 $batch = trim($plugin->getConf(self::CONF_BATCH_PROMPT)); 370 if (!empty($batch)) { 371 if (!strpos($batch, -1) == ">") { 372 $batch .= ">"; 373 } 374 } 375 $attributes->addHtmlAttributeValue("data-prompt", $batch); 376 break; 377 case "powershell": 378 $attributes->addClassName("command-line"); 379 $powerShell = trim($plugin->getConf(self::CONF_POWERSHELL_PROMPT)); 380 if (!empty($powerShell)) { 381 if (!strpos($powerShell, -1) == ">") { 382 $powerShell .= ">"; 383 } 384 } 385 $attributes->addHtmlAttributeValue("data-prompt", $powerShell); 386 break; 387 } 388 } 389 390 // Download 391 $attributes->addHtmlAttributeValue('data-download-link', true); 392 if ($attributes->hasComponentAttribute(syntax_plugin_combo_code::FILE_PATH_KEY)) { 393 $fileSrc = $attributes->getValueAndRemove(syntax_plugin_combo_code::FILE_PATH_KEY); 394 $attributes->addHtmlAttributeValue('data-src', $fileSrc); 395 $attributes->addHtmlAttributeValue('data-download-link-label', "Download " . $fileSrc); 396 } else { 397 $fileName = "file." . $language; 398 $attributes->addHtmlAttributeValue('data-src', $fileName); 399 } 400 /** 401 * No end of line after the pre, please, otherwise we get a new line 402 * in the code output 403 */ 404 $htmlCode = $attributes->toHtmlEnterTag("pre"); 405 406 407 /** 408 * Return 409 */ 410 $renderer->doc .= $htmlCode; 411 412 } 413 414 /** 415 * @param Doku_Renderer_xhtml $renderer 416 * @param TagAttributes $attributes 417 */ 418 public static function htmlExit(\Doku_Renderer_xhtml $renderer, $attributes = null) 419 { 420 421 if ($attributes != null) { 422 /** 423 * Display none, no rendering 424 */ 425 $display = $attributes->getValueAndRemove("display"); 426 if ($display != null) { 427 if ($display == "none") { 428 return; 429 } 430 } 431 } 432 $renderer->doc .= '</code>' . DOKU_LF . '</pre>' . DOKU_LF; 433 } 434 435 /** 436 * The autoloader try to download all language 437 * Even the one such as txt that does not exist 438 * This function was created to add it conditionally 439 */ 440 private static function addAutoloaderSnippet() 441 { 442 $tags = []; 443 $tags['script'][] = array("src" => self::BASE_PRISM_CDN . "/plugins/autoloader/prism-autoloader.min.js"); 444 PluginUtility::getSnippetManager()->upsertTagsForBar(self::SNIPPET_ID_AUTOLOADER, $tags); 445 } 446 447 448} 449