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