1<?php 2/** 3 * Copyright (c) 2021. ComboStrap, Inc. and its affiliates. All Rights Reserved. 4 * 5 * This source code is licensed under the GPL license found in the 6 * COPYING file in the root directory of this source tree. 7 * 8 * @license GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html) 9 * @author ComboStrap <support@combostrap.com> 10 * 11 */ 12 13/** 14 * Plugin Webcode: Show webcode (Css, HTML) in a iframe 15 * 16 */ 17 18// must be run within Dokuwiki 19use ComboStrap\CallStack; 20use ComboStrap\LogUtility; 21use ComboStrap\PluginUtility; 22use ComboStrap\Site; 23use ComboStrap\TagAttributes; 24 25if (!defined('DOKU_INC')) die(); 26 27/** 28 * Webcode 29 */ 30class syntax_plugin_combo_webcode extends DokuWiki_Syntax_Plugin 31{ 32 33 const EXTERNAL_RESOURCES_ATTRIBUTE_DISPLAY = 'externalResources'; // In the action bar 34 const EXTERNAL_RESOURCES_ATTRIBUTE_KEY = 'externalresources'; // In the code 35 36 // Simple cache bursting implementation for the webCodeConsole.(js|css) file 37 // They must be incremented manually when they changed 38 const WEB_CSS_VERSION = 1.1; 39 const WEB_CONSOLE_JS_VERSION = 2.1; 40 41 const TAG = 'webcode'; 42 43 /** 44 * The tag that have codes 45 */ 46 const CODE_TAGS = 47 array( 48 syntax_plugin_combo_code::CODE_TAG, 49 "plugin_combo_code", 50 syntax_plugin_combo_codemarkdown::TAG 51 ); 52 53 /** 54 * The attribute names in the array 55 */ 56 const CODES_ATTRIBUTE = "codes"; 57 const USE_CONSOLE_ATTRIBUTE = "useConsole"; 58 const RENDERING_MODE_ATTRIBUTE = 'renderingmode'; 59 const RENDERING_ONLY_RESULT = "onlyresult"; 60 61 /** 62 * Marki code 63 */ 64 const MARKI_LANG = 'marki'; 65 const DOKUWIKI_LANG = 'dw'; 66 const MARKIS = [self::MARKI_LANG, self::DOKUWIKI_LANG]; 67 68 /** 69 * Syntax Type. 70 * 71 * Needs to return one of the mode types defined in $PARSER_MODES in parser.php 72 * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types 73 * 74 * container because it may contain header in case of how to 75 */ 76 public function getType() 77 { 78 return 'container'; 79 } 80 81 public function getPType() 82 { 83 return "stack"; 84 } 85 86 87 /** 88 * @return array 89 * Allow which kind of plugin inside 90 * 91 * array('container', 'baseonly','formatting', 'substition', 'protected', 'disabled', 'paragraphs') 92 * 93 */ 94 public function getAllowedTypes() 95 { 96 return array('container', 'baseonly', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs'); 97 } 98 99 100 public function accepts($mode) 101 { 102 103 return syntax_plugin_combo_preformatted::disablePreformatted($mode); 104 105 } 106 107 /** 108 * @see Doku_Parser_Mode::getSort() 109 * The mode (plugin) with the lowest sort number will win out 110 * 111 * See {@link Doku_Parser_Mode_code} 112 */ 113 public function getSort() 114 { 115 return 99; 116 } 117 118 /** 119 * Called before any calls to ConnectTo 120 * @return void 121 */ 122 function preConnect() 123 { 124 } 125 126 /** 127 * Create a pattern that will called this plugin 128 * 129 * @param string $mode 130 * 131 * All dokuwiki mode can be seen in the parser.php file 132 * @see Doku_Parser_Mode::connectTo() 133 */ 134 public function connectTo($mode) 135 { 136 137 $pattern = PluginUtility::getContainerTagPattern(self::TAG); 138 $this->Lexer->addEntryPattern($pattern, $mode, PluginUtility::getModeForComponent($this->getPluginComponent())); 139 140 } 141 142 143 // This where the addPattern and addExitPattern are defined 144 public function postConnect() 145 { 146 $this->Lexer->addExitPattern('</' . self::TAG . '>', PluginUtility::getModeForComponent($this->getPluginComponent())); 147 } 148 149 150 /** 151 * Handle the match 152 * You get the match for each pattern in the $match variable 153 * $state says if it's an entry, exit or match pattern 154 * 155 * This is an instruction block and is cached apart from the rendering output 156 * There is two caches levels 157 * This cache may be suppressed with the url parameters ?purge=true 158 * 159 * The returned values are cached in an array that will be passed to the render method 160 * The handle function goal is to parse the matched syntax through the pattern function 161 * and to return the result for use in the renderer 162 * This result is always cached until the page is modified. 163 * @param string $match 164 * @param int $state 165 * @param int $pos 166 * @param Doku_Handler $handler 167 * @return array|bool 168 * @throws Exception 169 * @see DokuWiki_Syntax_Plugin::handle() 170 * 171 */ 172 public function handle($match, $state, $pos, Doku_Handler $handler) 173 { 174 switch ($state) { 175 176 case DOKU_LEXER_ENTER : 177 178 // Default 179 $defaultAttributes = array(); 180 $defaultAttributes['frameborder'] = 1; 181 $defaultAttributes['width'] = '100%'; 182 $defaultAttributes['name'] = "WebCode iFrame"; 183 $defaultAttributes[self::RENDERING_MODE_ATTRIBUTE] = 'story'; 184 // 'height' is set by the javascript if not set 185 // 'width' and 'scrolling' gets their natural value 186 187 // Parse and create the call stack array 188 $tagAttributes = TagAttributes::createFromTagMatch($match, $defaultAttributes); 189 $callStackArray = $tagAttributes->toCallStackArray(); 190 191 return array( 192 PluginUtility::STATE => $state, 193 PluginUtility::ATTRIBUTES => $callStackArray 194 ); 195 196 197 case DOKU_LEXER_UNMATCHED : 198 199 return PluginUtility::handleAndReturnUnmatchedData(self::TAG, $match, $handler); 200 201 202 case DOKU_LEXER_EXIT: 203 204 /** 205 * Capture all codes 206 */ 207 $codes = array(); 208 /** 209 * Does the javascript contains a console statement 210 */ 211 $useConsole = false; 212 213 /** 214 * Callstack 215 */ 216 $callStack = CallStack::createFromHandler($handler); 217 $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall(); 218 $renderingMode = strtolower($openingTag->getAttribute(self::RENDERING_MODE_ATTRIBUTE)); 219 220 /** 221 * The mime (ie xml,html, ...) and code content are in two differents 222 * call. To be able to set the content to the good type 223 * we keep a trace of it 224 */ 225 $actualCodeType = ""; 226 227 /** 228 * Loop 229 */ 230 while ($actualTag = $callStack->next()) { 231 232 233 $tagName = $actualTag->getTagName(); 234 if (in_array($tagName, self::CODE_TAGS)) { 235 236 /** 237 * Only rendering mode, we don't display the node 238 * on all node (enter, exit and unmatched) 239 */ 240 if ($renderingMode == self::RENDERING_ONLY_RESULT) { 241 $actualTag->addAttribute(TagAttributes::DISPLAY, "none"); 242 } 243 244 switch ($actualTag->getState()) { 245 246 case DOKU_LEXER_ENTER: 247 // Get the code (The content between the code nodes) 248 // We ltrim because the match gives us the \n at the beginning and at the end 249 $actualCodeType = strtolower(trim($actualTag->getType())); 250 251 // Xml is html 252 if ($actualCodeType == 'xml') { 253 $actualCodeType = 'html'; 254 } 255 256 // markdown, dokuwiki is marki 257 if (in_array($actualCodeType, ['md', 'markdown', 'dw'])) { 258 $actualCodeType = self::MARKI_LANG; 259 } 260 261 // The code for a language may be scattered in multiple block 262 if (!isset($codes[$actualCodeType])) { 263 $codes[$actualCodeType] = ""; 264 } 265 266 continue 2; 267 268 case DOKU_LEXER_UNMATCHED: 269 270 $codeContent = $actualTag->getPluginData()[PluginUtility::PAYLOAD]; 271 272 if (empty($actualCodeType)) { 273 LogUtility::msg("The type of the code should not be null for the code content " . $codeContent, LogUtility::LVL_MSG_WARNING, self::TAG); 274 continue 2; 275 } 276 277 // Append it 278 $codes[$actualCodeType] = $codes[$actualCodeType] . $codeContent; 279 280 // Check if a javascript console function is used, only if the flag is not set to true 281 if (!$useConsole == true) { 282 if (in_array($actualCodeType, array('babel', 'javascript', 'html', 'xml'))) { 283 // if the code contains 'console.' 284 $result = preg_match('/' . 'console\.' . '/is', $codeContent); 285 if ($result) { 286 $useConsole = true; 287 } 288 } 289 } 290 // Reset 291 $actualCodeType = ""; 292 break; 293 294 } 295 } 296 297 } 298 299 return array( 300 PluginUtility::STATE => $state, 301 self::CODES_ATTRIBUTE => $codes, 302 self::USE_CONSOLE_ATTRIBUTE => $useConsole, 303 PluginUtility::ATTRIBUTES => $openingTag->getAttributes() 304 ); 305 306 } 307 return false; 308 309 } 310 311 /** 312 * Render the output 313 * @param string $mode 314 * @param Doku_Renderer $renderer 315 * @param array $data - what the function handle() return'ed 316 * @return bool - rendered correctly (not used) 317 * 318 * The rendering process 319 * @see DokuWiki_Syntax_Plugin::render() 320 * 321 */ 322 public function render($mode, Doku_Renderer $renderer, $data) 323 { 324 // The $data variable comes from the handle() function 325 // 326 // $mode = 'xhtml' means that we output html 327 // There is other mode such as metadata where you can output data for the headers (Not 100% sure) 328 if ($mode == 'xhtml') { 329 330 331 /** @var Doku_Renderer_xhtml $renderer */ 332 333 $state = $data[PluginUtility::STATE]; 334 switch ($state) { 335 336 337 case DOKU_LEXER_UNMATCHED : 338 339 $renderer->doc .= PluginUtility::renderUnmatched($data); 340 break; 341 342 case DOKU_LEXER_EXIT : 343 $codes = $data[self::CODES_ATTRIBUTE]; 344 $callStackArray = $data[PluginUtility::ATTRIBUTES]; 345 $iFrameAttributes = TagAttributes::createFromCallStackArray($callStackArray, self::TAG); 346 347 // Create the real output of webcode 348 if (sizeof($codes) == 0) { 349 return false; 350 } 351 352 // Credits bar 353 $bar = '<div class="webcode-bar">'; 354 355 356 // Dokuwiki Code ? 357 if (array_key_exists(self::MARKI_LANG, $codes)) { 358 359 $markiCode = $codes[self::MARKI_LANG]; 360 /** 361 * By default, markup code 362 * is rendered inside the page 363 * We got less problem such as iframe overflow 364 * due to lazy loading, such as relative link, ... 365 * 366 */ 367 if (!$iFrameAttributes->hasComponentAttribute("iframe")) { 368 $renderer->doc .= PluginUtility::render($markiCode); 369 return true; 370 } 371 372 $queryParams = array( 373 'call' => action_plugin_combo_webcode::CALL_ID, 374 action_plugin_combo_webcode::MARKI_PARAM => $markiCode 375 ); 376 $queryString = http_build_query($queryParams); 377 $url = Site::getAjaxUrl() . "?$queryString"; 378 $iFrameAttributes->addHtmlAttributeValue("src", $url); 379 380 } else { 381 382 383 // Js, Html, Css 384 $iframeSrcValue = '<html><head>'; 385 $iframeSrcValue .= '<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>'; 386 $iframeSrcValue .= '<title>Made by WebCode</title>'; 387 $iframeSrcValue .= '<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.min.css"/>'; 388 389 390 // External Resources such as css stylesheet or js 391 $externalResources = []; 392 if ($iFrameAttributes->hasComponentAttribute(self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY)) { 393 $resources = $iFrameAttributes->getValueAndRemove(self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY); 394 $externalResources = explode(",", $resources); 395 } 396 397 // Babel Preprocessor, if babel is used, add it to the external resources 398 if (array_key_exists('babel', $codes)) { 399 $babelMin = "https://unpkg.com/babel-standalone@6/babel.min.js"; 400 // a load of babel invoke it (be sure to not have it twice 401 if (!(array_key_exists($babelMin, $externalResources))) { 402 $externalResources[] = $babelMin; 403 } 404 } 405 406 // Add the external resources 407 foreach ($externalResources as $externalResource) { 408 $pathInfo = pathinfo($externalResource); 409 $fileExtension = $pathInfo['extension']; 410 switch ($fileExtension) { 411 case 'css': 412 $iframeSrcValue .= '<link rel="stylesheet" type="text/css" href="' . $externalResource . '"/>'; 413 break; 414 case 'js': 415 $iframeSrcValue .= '<script type="text/javascript" src="' . $externalResource . '"></script>'; 416 break; 417 } 418 } 419 420 421 // WebConsole style sheet 422 $iframeSrcValue .= '<link rel="stylesheet" type="text/css" href="' . PluginUtility::getResourceBaseUrl() . '/webcode/webcode-iframe.css?ver=' . self::WEB_CSS_VERSION . '"/>'; 423 424 // A little margin to make it neater 425 // that can be overwritten via cascade 426 $iframeSrcValue .= '<style>body { margin:10px } /* default margin */</style>'; 427 428 // The css 429 if (array_key_exists('css', $codes)) { 430 $iframeSrcValue .= '<!-- The CSS code -->'; 431 $iframeSrcValue .= '<style>' . $codes['css'] . '</style>'; 432 }; 433 $iframeSrcValue .= '</head><body>'; 434 if (array_key_exists('html', $codes)) { 435 $iframeSrcValue .= '<!-- The HTML code -->'; 436 $iframeSrcValue .= $codes['html']; 437 } 438 // The javascript console area is based at the end of the HTML document 439 $useConsole = $data[self::USE_CONSOLE_ATTRIBUTE]; 440 if ($useConsole) { 441 $iframeSrcValue .= '<!-- WebCode Console -->'; 442 $iframeSrcValue .= '<div><p class="webConsoleTitle">Console Output:</p>'; 443 $iframeSrcValue .= '<div id="webCodeConsole"></div>'; 444 $iframeSrcValue .= '<script type="text/javascript" src="' . PluginUtility::getResourceBaseUrl() . '/webcode/webcode-console.js?ver=' . self::WEB_CONSOLE_JS_VERSION . '"></script>'; 445 $iframeSrcValue .= '</div>'; 446 } 447 // The javascript comes at the end because it may want to be applied on previous HTML element 448 // as the page load in the IO order, javascript must be placed at the end 449 if (array_key_exists('javascript', $codes)) { 450 $iframeSrcValue .= '<!-- The Javascript code -->'; 451 $iframeSrcValue .= '<script type="text/javascript">' . $codes['javascript'] . '</script>'; 452 } 453 if (array_key_exists('babel', $codes)) { 454 $iframeSrcValue .= '<!-- The Babel code -->'; 455 $iframeSrcValue .= '<script type="text/babel">' . $codes['babel'] . '</script>'; 456 } 457 $iframeSrcValue .= '</body></html>'; 458 $iFrameAttributes->addHtmlAttributeValue("srcdoc", $iframeSrcValue); 459 460 // Code bar with button 461 $bar .= '<div class="webcode-bar-item">' . PluginUtility::getUrl(self::TAG, "Rendered by WebCode", false) . '</div>'; 462 $bar .= '<div class="webcode-bar-item">' . $this->addJsFiddleButton($codes, $externalResources, $useConsole, $iFrameAttributes->getValue("name")) . '</div>'; 463 464 465 } 466 467 /** 468 * If there is no height 469 */ 470 if (!$iFrameAttributes->hasComponentAttribute(TagAttributes::HEIGHT_KEY)) { 471 472 /** 473 * Adjust the height attribute 474 * of the iframe element 475 * Any styling attribute would take over 476 */ 477 PluginUtility::getSnippetManager()->attachJavascriptSnippetForBar(self::TAG); 478 /** 479 * CSS Height auto works when an image is loaded asynchronously but not 480 * when there is only text in the iframe 481 */ 482 //$iFrameAttributes->addStyleDeclaration("height", "auto"); 483 /** 484 * Due to margin at the bottom with css height=auto, 485 * we may see a scroll bar 486 * This block of code is to avoid scrolling, 487 * then scrolling = no if not set 488 */ 489 if (!$iFrameAttributes->hasComponentAttribute("scrolling")) { 490 $iFrameAttributes->addHtmlAttributeValue("scrolling", "no"); 491 } 492 493 } 494 495 496 PluginUtility::getSnippetManager()->attachCssSnippetForBar(self::TAG); 497 498 /** 499 * The iframe does not have any width 500 * By default, we set it to 100% and it can be 501 * constraint with the `width` attributes that will 502 * set a a max-width 503 */ 504 $iFrameAttributes->addStyleDeclaration("width","100%"); 505 506 $iFrameHtml = $iFrameAttributes->toHtmlEnterTag("iframe") . '</iframe>'; 507 $bar .= '</div>'; // close the bar 508 $renderer->doc .= "<div class=\"webcode-wrapper\">" . $iFrameHtml . $bar . '</div>'; 509 510 511 break; 512 } 513 514 return true; 515 } 516 return false; 517 } 518 519 /** 520 * @param array $codes the array containing the codes 521 * @param array $externalResources the attributes of a call (for now the externalResources) 522 * @param bool $useConsole 523 * @param string $snippetTitle 524 * @return string the HTML form code 525 * 526 * Specification, see http://doc.jsfiddle.net/api/post.html 527 */ 528 public function addJsFiddleButton($codes, $externalResources, $useConsole = false, $snippetTitle = null) 529 { 530 531 $postURL = "https://jsfiddle.net/api/post/library/pure/"; //No Framework 532 533 534 if ($useConsole) { 535 // If their is a console.log function, add the Firebug Lite support of JsFiddle 536 // Seems to work only with the Edge version of jQuery 537 // $postURL .= "edge/dependencies/Lite/"; 538 // The firebug logging is not working anymore because of 404 539 // Adding them here 540 $externalResources[] = 'The firebug resources for the console.log features'; 541 $externalResources[] = PluginUtility::getResourceBaseUrl() . '/firebug/firebug-lite.css'; 542 $externalResources[] = PluginUtility::getResourceBaseUrl() . '/firebug/firebug-lite-1.2.js'; 543 } 544 545 // The below code is to prevent this JsFiddle bug: https://github.com/jsfiddle/jsfiddle-issues/issues/726 546 // The order of the resources is not guaranteed 547 // We pass then the resources only if their is one resources 548 // Otherwise we pass them as a script element in the HTML. 549 if (count($externalResources) <= 1) { 550 $externalResourcesInput = '<input type="hidden" name="resources" value="' . implode(",", $externalResources) . '">'; 551 } else { 552 $codes['html'] .= "\n\n\n\n\n<!-- The resources -->\n"; 553 $codes['html'] .= "<!-- They have been added here because their order is not guarantee through the API. -->\n"; 554 $codes['html'] .= "<!-- See: https://github.com/jsfiddle/jsfiddle-issues/issues/726 -->\n"; 555 foreach ($externalResources as $externalResource) { 556 if ($externalResource != "") { 557 $extension = pathinfo($externalResource)['extension']; 558 switch ($extension) { 559 case "css": 560 $codes['html'] .= "<link href=\"" . $externalResource . "\" rel=\"stylesheet\">\n"; 561 break; 562 case "js": 563 $codes['html'] .= "<script src=\"" . $externalResource . "\"></script>\n"; 564 break; 565 default: 566 $codes['html'] .= "<!-- " . $externalResource . " -->\n"; 567 } 568 } 569 } 570 } 571 572 $jsCode = $codes['javascript']; 573 $jsPanel = 0; // language for the js specific panel (0 = JavaScript) 574 if (array_key_exists('babel', $codes)) { 575 $jsCode = $codes['babel']; 576 $jsPanel = 3; // 3 = Babel 577 } 578 579 // Title and description 580 global $ID; 581 $pageTitle = tpl_pagetitle($ID, true); 582 if (!$snippetTitle) { 583 584 $snippetTitle = "Code from " . $pageTitle; 585 } 586 $description = "Code from the page '" . $pageTitle . "' \n" . wl($ID, $absolute = true); 587 return '<form method="post" action="' . $postURL . '" target="_blank">' . 588 '<input type="hidden" name="title" value="' . htmlentities($snippetTitle) . '">' . 589 '<input type="hidden" name="description" value="' . htmlentities($description) . '">' . 590 '<input type="hidden" name="css" value="' . htmlentities($codes['css']) . '">' . 591 '<input type="hidden" name="html" value="' . htmlentities("<!-- The HTML -->" . $codes['html']) . '">' . 592 '<input type="hidden" name="js" value="' . htmlentities($jsCode) . '">' . 593 '<input type="hidden" name="panel_js" value="' . htmlentities($jsPanel) . '">' . 594 '<input type="hidden" name="wrap" value="b">' . //javascript no wrap in body 595 $externalResourcesInput . 596 '<button>Try the code</button>' . 597 '</form>'; 598 599 } 600 601 /** 602 * @param $codes the array containing the codes 603 * @param $attributes the attributes of a call (for now the externalResources) 604 * @return string the HTML form code 605 */ 606 public function addCodePenButton($codes, $attributes) 607 { 608 // TODO 609 // http://blog.codepen.io/documentation/api/prefill/ 610 } 611 612 613} 614