1<?php 2/** 3 * Plugin Webcode: Show webcode (Css, HTML) in a iframe 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Nicolas GERARD 7 */ 8 9// must be run within Dokuwiki 10use dokuwiki\Extension\Event; 11 12if (!defined('DOKU_INC')) die(); 13 14/** 15 * All DokuWiki plugins to extend the parser/rendering mechanism 16 * need to inherit from this class 17 */ 18class syntax_plugin_webcode_basis extends DokuWiki_Syntax_Plugin 19{ 20 21 const EXTERNAL_RESOURCES_ATTRIBUTE_DISPLAY = 'externalResources'; // In the action bar 22 const EXTERNAL_RESOURCES_ATTRIBUTE_KEY = 'externalresources'; // In the code 23 24 // Simple cache bursting implementation for the webCodeConsole.(js|css) file 25 // They must be incremented manually when they changed 26 const WEB_CONSOLE_CSS_VERSION = 1.1; 27 const WEB_CONSOLE_JS_VERSION = 2.1; 28 29 /** 30 * @var array that holds the iframe attributes 31 */ 32 private $attributes = array(); 33 /** 34 * @var array That holds the code parts 35 */ 36 37 private $codes = array(); 38 39 /** 40 * Print the output of the console javascript function ? 41 */ 42 private $useConsole = false; 43 44 /** 45 * @param $mode 46 * @param array $instructions 47 * @param array $info - the $info of the renderer to pass context information 48 * @return string|null 49 * Wrapper around {@link p_render()} that pass the $info through tho the created Renderer 50 */ 51 private static function p_render($mode, array $instructions, array &$info) 52 { 53 if (is_null($instructions)) return ''; 54 if ($instructions === false) return ''; 55 56 $Renderer = p_get_renderer($mode); 57 if (is_null($Renderer)) return null; 58 59 $Renderer->reset(); 60 if (!empty($info)) { 61 $Renderer->info = $info; 62 } 63 64 $Renderer->smileys = getSmileys(); 65 $Renderer->entities = getEntities(); 66 $Renderer->acronyms = getAcronyms(); 67 $Renderer->interwiki = getInterwiki(); 68 69 // Loop through the instructions 70 foreach ($instructions as $instruction) { 71 // Execute the callback against the Renderer 72 if (method_exists($Renderer, $instruction[0])) { 73 call_user_func_array(array(&$Renderer, $instruction[0]), $instruction[1] ? $instruction[1] : array()); 74 } 75 } 76 77 //set info array 78 $info = $Renderer->info; 79 80 // Post process and return the output 81 $data = array($mode, & $Renderer->doc); 82 Event::createAndTrigger('RENDERER_CONTENT_POSTPROCESS', $data); 83 return $Renderer->doc; 84 85 } 86 87 88 /** 89 * Syntax Type. 90 * 91 * Needs to return one of the mode types defined in $PARSER_MODES in parser.php 92 * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types 93 * 94 * container because it may contain header in case of how to 95 */ 96 public function getType() 97 { 98 // formatting ? 99 // container 100 return 'container'; 101 } 102 103 /** 104 * @return array 105 * Allow which kind of plugin inside 106 * 107 * array('container', 'baseonly','formatting', 'substition', 'protected', 'disabled', 'paragraphs') 108 * 109 */ 110 public function getAllowedTypes() 111 { 112 return array('container', 'baseonly', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs'); 113 } 114 115 /* 116 * Don't accept the code mode 117 * in order to get the code block 118 * in the DOKU_LEXER_MATCHED state through addPattern 119 */ 120 function accepts($mode) 121 { 122 if ($mode == "code" || $mode == "plugin_combo_code") { 123 return false; 124 } 125 return parent::accepts($mode); 126 } 127 128 129 /** 130 * @see Doku_Parser_Mode::getSort() 131 * The mode (plugin) with the lowest sort number will win out 132 * 133 * See {@link Doku_Parser_Mode_code} 134 */ 135 public function getSort() 136 { 137 return 100; 138 } 139 140 /** 141 * Called before any calls to ConnectTo 142 * @return void 143 */ 144 function preConnect() 145 { 146 } 147 148 /** 149 * Create a pattern that will called this plugin 150 * 151 * @param string $mode 152 * 153 * All dokuwiki mode can be seen in the parser.php file 154 * @see Doku_Parser_Mode::connectTo() 155 */ 156 public function connectTo($mode) 157 { 158 159 $this->Lexer->addEntryPattern('<webcode.*?>(?=.*?</webcode>)', $mode, $this->getPluginMode()); 160 161 } 162 163 164 // This where the addPattern and addExitPattern are defined 165 public function postConnect() 166 { 167 168 /** 169 * Capture all code block 170 * See {@link Doku_Parser_Mode_code} 171 */ 172 $this->Lexer->addPattern('<code.*?</code>', $this->getPluginMode()); 173 174 /** 175 * End 176 */ 177 $this->Lexer->addExitPattern('</webcode>', $this->getPluginMode()); 178 179 } 180 181 182 /** 183 * Handle the match 184 * You get the match for each pattern in the $match variable 185 * $state says if it's an entry, exit or match pattern 186 * 187 * This is an instruction block and is cached apart from the rendering output 188 * There is two caches levels 189 * This cache may be suppressed with the url parameters ?purge=true 190 * 191 * The returned values are cached in an array that will be passed to the render method 192 * The handle function goal is to parse the matched syntax through the pattern function 193 * and to return the result for use in the renderer 194 * This result is always cached until the page is modified. 195 * @param string $match 196 * @param int $state 197 * @param int $pos 198 * @param Doku_Handler $handler 199 * @return array|bool 200 * @throws Exception 201 * @see DokuWiki_Syntax_Plugin::handle() 202 * 203 */ 204 public function handle($match, $state, $pos, Doku_Handler $handler) 205 { 206 switch ($state) { 207 208 case DOKU_LEXER_ENTER : 209 210 // We got the first webcode tag and its attributes 211 212 $match = substr($match, 8, -1); //9 = strlen("<webcode") 213 214 // Reset of the attributes 215 // With some framework the php object may be still persisted in memory 216 // And you may get some attributes from other page 217 $attributes = array(); 218 $attributes['frameborder'] = 1; 219 $attributes['width'] = '100%'; 220 221 $renderingModeKey = 'renderingmode'; 222 $attributes[$renderingModeKey] = 'story'; 223 224 // config Parameters will get their value in lowercase 225 $configAttributes = [$renderingModeKey]; 226 227 // /i not case sensitive 228 $attributePattern = "\s*(\w+)\s*=\s*\"?([^\"\s]+)\"?\\s*"; 229 $result = preg_match_all('/' . $attributePattern . '/i', $match, $matches); 230 231 232 if ($result != 0) { 233 foreach ($matches[1] as $key => $lang) { 234 $attributeKey = strtolower($lang); 235 $attributeValue = $matches[2][$key]; 236 if (in_array($attributeKey, $configAttributes)) { 237 $attributeValue = strtolower($attributeValue); 238 } 239 $attributes[$attributeKey] = $attributeValue; 240 } 241 } 242 243 // We set the attributes on a class scope 244 // to be used in the DOKU_LEXER_UNMATCHED step 245 $this->attributes = $attributes; 246 247 // Cache the values to be used by the render method 248 return array($state, $attributes); 249 250 251 /** 252 * The code block as asked 253 * by addPattern() into {@link postConnect} 254 */ 255 case DOKU_LEXER_MATCHED: 256 257 $xhtmlWebCode = ""; 258 259 // We got the content between the webcode tag and its attributes 260 // We parse it in order to extract the code in the codes array 261 $codes = array(); 262 /** 263 * Does the javascript contains a console statement 264 */ 265 $useConsole = false; 266 267 // Regexp Pattern to parse the codes block 268 $codePattern = "<code\s*([^>\s]*)\s*([^>\s]*)>(.+?)<\/code>"; 269 // The first group is the lang 270 // The second group is the file name and options 271 // The third group is the code 272 $result = preg_match_all('/' . $codePattern . '/msi', $match, $matches, PREG_PATTERN_ORDER); 273 if ($result) { 274 275 // Loop through the block codes 276 foreach ($matches[1] as $key => $lang) { 277 278 // Get the code (The content between the code nodes) 279 // We ltrim because the match gives us the \n at the beginning and at the end 280 $code = ltrim($matches[3][$key]); 281 282 // String are in lowercase 283 $lowerCodeName = strtolower($lang); 284 285 // Xml is html 286 if ($lowerCodeName == 'xml') { 287 $lowerCodeName = 'html'; 288 } 289 290 // If the code doesn't exist in the array, index it otherwise append it 291 if (!array_key_exists($lowerCodeName, $codes)) { 292 $codes[$lowerCodeName] = $code; 293 } else { 294 $codes[$lowerCodeName] = $codes[$lowerCodeName] . $code; 295 } 296 297 // Check if a javascript console function is used, only if the flag is not set to true 298 if (!$useConsole == true) { 299 if (in_array($lowerCodeName, array('babel', 'javascript', 'html', 'xml'))) { 300 // if the code contains 'console.' 301 $result = preg_match('/' . 'console\.' . '/is', $code); 302 if ($result) { 303 $useConsole = true; 304 } 305 } 306 } 307 } 308 $matchedTextToRender = ""; 309 // Render the whole 310 if ($this->attributes["renderingmode"] != "onlyresult") { 311 312 // Replace babel by javascript because babel highlight does not exist in the dokuwiki and babel is only javascript ES2015 313 $matchedTextToRender = preg_replace('/<code[\s]+babel/', '<code javascript', $match); 314 315 // Delete a display="none" block 316 $matchedTextToRender = preg_replace('/<code([a-z\s]*)\[([a-z\s]*)display="none"([a-z\s]*)\]>(.*)<\/code>/msiU', '', $matchedTextToRender); 317 318 } 319 return array($state, $matchedTextToRender, $codes, $useConsole); 320 321 } else { 322 throw new Exception("There was a match of the pattern but not when parsing"); 323 } 324 325 326 case DOKU_LEXER_UNMATCHED : 327 328 // Cache the values 329 return array($state, $match); 330 331 case DOKU_LEXER_EXIT: 332 333 // Cache the values 334 return array($state); 335 336 } 337 338 } 339 340 /** 341 * Render the output 342 * @param string $mode 343 * @param Doku_Renderer $renderer 344 * @param array $data - what the function handle() return'ed 345 * @return bool - rendered correctly (not used) 346 * 347 * The rendering process 348 * @see DokuWiki_Syntax_Plugin::render() 349 * 350 */ 351 public function render($mode, Doku_Renderer $renderer, $data) 352 { 353 // The $data variable comes from the handle() function 354 // 355 // $mode = 'xhtml' means that we output html 356 // There is other mode such as metadata where you can output data for the headers (Not 100% sure) 357 if ($mode == 'xhtml') { 358 359 /** @var Doku_Renderer_xhtml $renderer */ 360 361 $state = $data[0]; 362 switch ($state) { 363 364 case DOKU_LEXER_ENTER : 365 366 // The extracted data are the attribute of the webcode tag 367 // We put in a class variable so that we can use in the last step (DOKU_LEXER_EXIT) 368 $this->attributes = $data[1]; 369 370 // Reinit the codes to make sure that the code does not leak into another webcode 371 $this->useConsole = false; 372 $this->codes = array(); 373 break; 374 375 case DOKU_LEXER_MATCHED : 376 377 // The extracted data are the codes for this step 378 // We put them in a class variable so that we can use them in the last step (DOKU_LEXER_EXIT) 379 $code = $data[2]; 380 $codeType = key($code); 381 $this->codes[$codeType] = $this->codes[$codeType] . $code[$codeType]; 382 383 // if not true, see if it's true 384 if (!$this->useConsole) { 385 $this->useConsole = $data[3]; 386 } 387 388 // Render 389 $textToRender = $data[1]; 390 if (!empty($textToRender)) { 391 $instructions = p_get_instructions($textToRender); 392 $xhtmlWebCode = self::p_render('xhtml', $instructions, $renderer->info); 393 $renderer->doc .= $xhtmlWebCode; 394 } 395 break; 396 397 case DOKU_LEXER_UNMATCHED : 398 399 // Render and escape 400 $renderer->doc .= $renderer->_xmlEntities($data[1]); 401 break; 402 403 case DOKU_LEXER_EXIT : 404 // Create the real output of webcode 405 if (sizeof($this->codes) == 0) { 406 return false; 407 } 408 // Dokuwiki Code ? 409 if (array_key_exists('dw', $this->codes)) { 410 $instructions = p_get_instructions($this->codes['dw']); 411 $renderer->doc .= self::p_render('xhtml', $instructions, $renderer->info); 412 } else { 413 414 // Js, Html, Css 415 $htmlContent = '<html><head>'; 416 $htmlContent .= '<meta http-equiv="content-type" content="text/html; charset=UTF-8">'; 417 $htmlContent .= '<title>Made by Webcode</title>'; 418 $htmlContent .= '<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.min.css">'; 419 420 421 // External Resources such as css stylesheet or js 422 $externalResources = array(); 423 if (array_key_exists(self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY, $this->attributes)) { 424 $externalResources = explode(",", $this->attributes[self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY]); 425 } 426 427 // Babel Preprocessor, if babel is used, add it to the external resources 428 if (array_key_exists('babel', $this->codes)) { 429 $babelMin = "https://unpkg.com/babel-standalone@6/babel.min.js"; 430 // a load of babel invoke it (be sure to not have it twice 431 if (!(array_key_exists($babelMin, $externalResources))) { 432 $externalResources[] = $babelMin; 433 } 434 } 435 436 // Add the external resources 437 foreach ($externalResources as $externalResource) { 438 $pathInfo = pathinfo($externalResource); 439 $fileExtension = $pathInfo['extension']; 440 switch ($fileExtension) { 441 case 'css': 442 $htmlContent .= '<link rel="stylesheet" type="text/css" href="' . $externalResource . '">'; 443 break; 444 case 'js': 445 $htmlContent .= '<script type="text/javascript" src="' . $externalResource . '"></script>'; 446 break; 447 } 448 } 449 450 451 // WebConsole style sheet 452 if ($this->useConsole) { 453 $htmlContent .= '<link rel="stylesheet" type="text/css" href="' . DOKU_URL . 'lib/plugins/webcode/webCodeConsole.css?ver=' . self::WEB_CONSOLE_CSS_VERSION . '"/>'; 454 } 455 456 if (array_key_exists('css', $this->codes)) { 457 $htmlContent .= '<!-- The CSS code -->'; 458 $htmlContent .= '<style>' . $this->codes['css'] . '</style>'; 459 }; 460 $htmlContent .= '</head><body style="margin:10px">'; 461 if (array_key_exists('html', $this->codes)) { 462 $htmlContent .= '<!-- The HTML code -->'; 463 $htmlContent .= $this->codes['html']; 464 } 465 // The javascript console area is based at the end of the HTML document 466 if ($this->useConsole) { 467 $htmlContent .= '<!-- WebCode Console -->'; 468 $htmlContent .= '<div><p class=\'webConsoleTitle\'>Console Output:</p>'; 469 $htmlContent .= '<div id=\'webCodeConsole\'/>'; 470 $htmlContent .= '<script type=\'text/javascript\' src=\'' . DOKU_URL . 'lib/plugins/webcode/webCodeConsole.js?ver=' . self::WEB_CONSOLE_JS_VERSION . '\'></script>'; 471 $htmlContent .= '</div>'; 472 } 473 // The javascript comes at the end because it may want to be applied on previous HTML element 474 // as the page load in the IO order, javascript must be placed at the end 475 if (array_key_exists('javascript', $this->codes)) { 476 $htmlContent .= '<!-- The Javascript code -->'; 477 $htmlContent .= '<script type="text/javascript">' . $this->codes['javascript'] . '</script>'; 478 } 479 if (array_key_exists('babel', $this->codes)) { 480 $htmlContent .= '<!-- The Babel code -->'; 481 $htmlContent .= '<script type="text/babel">' . $this->codes['babel'] . '</script>'; 482 } 483 $htmlContent .= '</body></html>'; 484 485 // Here the magic from the plugin happens 486 // We add the Iframe and the JsFiddleButton 487 $iFrameHtml = '<iframe '; 488 489 // We add the name HTML attribute 490 $name = "WebCode iFrame"; 491 if (array_key_exists('name', $this->attributes)) { 492 $name .= ' ' . $this->attributes['name']; 493 } 494 $iFrameHtml .= ' name="' . $name . '" '; 495 496 // The class to be able to select them 497 $iFrameHtml .= ' class="webCode" '; 498 499 // We add the others HTML attributes 500 $iFrameHtmlAttributes = array('width', 'height', 'frameborder', 'scrolling'); 501 foreach ($this->attributes as $attribute => $value) { 502 if (in_array($attribute, $iFrameHtmlAttributes)) { 503 $iFrameHtml .= ' ' . $attribute . '=' . $value; 504 } 505 } 506 $iFrameHtml .= ' srcdoc="' . htmlentities($htmlContent) . '" ></iframe>';// 507 508 // Credits bar 509 $bar = '<div class="webcode-bar">'; 510 $bar .= '<div class="webcode-bar-item"><a href="https://combostrap.com/webcode">' . $this->getLang('RenderedBy') . '</a></div>'; 511 $bar .= '<div class="webcode-bar-item">'.$this->addJsFiddleButton($this->codes, $this->attributes).'</div>'; 512 $bar .= '</div>'; 513 $renderer->doc .= '<div class="webcode">' . $iFrameHtml . $bar . '</div>'; 514 } 515 516 break; 517 } 518 519 return true; 520 } 521 return false; 522 } 523 524 /** 525 * @param array $codes the array containing the codes 526 * @param array $attributes the attributes of a call (for now the externalResources) 527 * @return string the HTML form code 528 * 529 * Specification, see http://doc.jsfiddle.net/api/post.html 530 */ 531 public function addJsFiddleButton($codes, $attributes) 532 { 533 534 $postURL = "https://jsfiddle.net/api/post/library/pure/"; //No Framework 535 536 $externalResources = array(); 537 if (array_key_exists(self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY, $attributes)) { 538 $externalResources = explode(",", $attributes[self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY]); 539 } 540 541 542 if ($this->useConsole) { 543 // If their is a console.log function, add the Firebug Lite support of JsFiddle 544 // Seems to work only with the Edge version of jQuery 545 // $postURL .= "edge/dependencies/Lite/"; 546 // The firebug logging is not working anymore because of 404 547 // Adding them here 548 $externalResources[] = 'The firebug resources for the console.log features'; 549 $externalResources[] = DOKU_URL . 'lib/plugins/webcode/vendor/firebug-lite.css'; 550 $externalResources[] = DOKU_URL . 'lib/plugins/webcode/vendor/firebug-lite-1.2.js'; 551 } 552 553 // The below code is to prevent this JsFiddle bug: https://github.com/jsfiddle/jsfiddle-issues/issues/726 554 // The order of the resources is not guaranteed 555 // We pass then the resources only if their is one resources 556 // Otherwise we pass them as a script element in the HTML. 557 if (count($externalResources) <= 1) { 558 $externalResourcesInput = '<input type="hidden" name="resources" value="' . implode(",", $externalResources) . '">'; 559 } else { 560 $codes['html'] .= "\n\n\n\n\n<!-- The resources -->\n"; 561 $codes['html'] .= "<!-- They have been added here because their order is not guarantee through the API. -->\n"; 562 $codes['html'] .= "<!-- See: https://github.com/jsfiddle/jsfiddle-issues/issues/726 -->\n"; 563 foreach ($externalResources as $externalResource) { 564 if ($externalResource != "") { 565 $extension = pathinfo($externalResource)['extension']; 566 switch ($extension) { 567 case "css": 568 $codes['html'] .= "<link href=\"" . $externalResource . "\" rel=\"stylesheet\">\n"; 569 break; 570 case "js": 571 $codes['html'] .= "<script src=\"" . $externalResource . "\"></script>\n"; 572 break; 573 default: 574 $codes['html'] .= "<!-- " . $externalResource . " -->\n"; 575 } 576 } 577 } 578 } 579 580 $jsCode = $codes['javascript']; 581 $jsPanel = 0; // language for the js specific panel (0 = JavaScript) 582 if (array_key_exists('babel', $this->codes)) { 583 $jsCode = $codes['babel']; 584 $jsPanel = 3; // 3 = Babel 585 } 586 587 // Title and description 588 global $ID; 589 $title = $attributes['name']; 590 $pageTitle = tpl_pagetitle($ID, true); 591 if (!$title) { 592 593 $title = "Code from " . $pageTitle; 594 } 595 $description = "Code from the page '" . $pageTitle . "' \n" . wl($ID, $absolute = true); 596 return '<form method="post" action="' . $postURL . '" target="_blank">' . 597 '<input type="hidden" name="title" value="' . htmlentities($title) . '">' . 598 '<input type="hidden" name="description" value="' . htmlentities($description) . '">' . 599 '<input type="hidden" name="css" value="' . htmlentities($codes['css']) . '">' . 600 '<input type="hidden" name="html" value="' . htmlentities("<!-- The HTML -->" . $codes['html']) . '">' . 601 '<input type="hidden" name="js" value="' . htmlentities($jsCode) . '">' . 602 '<input type="hidden" name="panel_js" value="' . htmlentities($jsPanel) . '">' . 603 '<input type="hidden" name="wrap" value="b">' . //javascript no wrap in body 604 $externalResourcesInput . 605 '<button>' . $this->getLang('JsFiddleButtonContent') . '</button>' . 606 '</form>'; 607 608 } 609 610 /** 611 * @param $codes the array containing the codes 612 * @param $attributes the attributes of a call (for now the externalResources) 613 * @return string the HTML form code 614 */ 615 public function addCodePenButton($codes, $attributes) 616 { 617 // TODO 618 // http://blog.codepen.io/documentation/api/prefill/ 619 } 620 621 622 /** 623 * @return string the mode (the name of this plugin for the lexer) 624 */ 625 public function getPluginMode() 626 { 627 $pluginName = $this->getPluginName(); 628 $pluginComponent = $this->getPluginComponent(); 629 return 'plugin_' . $pluginName . '_' . $pluginComponent; 630 } 631 632 633} 634