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