1<?php 2 3namespace ComboStrap\Tag; 4 5use ComboStrap\CallStack; 6use ComboStrap\Dimension; 7use ComboStrap\Display; 8use ComboStrap\ExceptionBadState; 9use ComboStrap\ExceptionCompile; 10use ComboStrap\ExceptionNotFound; 11use ComboStrap\ExecutionContext; 12use ComboStrap\FetcherMarkup; 13use ComboStrap\FetcherMarkupWebcode; 14use ComboStrap\FetcherRawLocalPath; 15use ComboStrap\LogUtility; 16use ComboStrap\PluginUtility; 17use ComboStrap\TagAttribute\StyleAttribute; 18use ComboStrap\TagAttributes; 19use ComboStrap\WikiPath; 20use syntax_plugin_combo_code; 21use syntax_plugin_combo_codemarkdown; 22 23class WebCodeTag 24{ 25 26 public const TAG = 'webcode'; 27 /** 28 * The tag that have codes 29 */ 30 public const CODE_TAGS = array( 31 syntax_plugin_combo_code::CODE_TAG, 32 "plugin_combo_code", 33 syntax_plugin_combo_codemarkdown::TAG 34 ); 35 /** 36 * The attribute names in the array 37 */ 38 public const CODES_ATTRIBUTE = "codes"; 39 public const EXTERNAL_RESOURCES_ATTRIBUTE_DISPLAY = 'externalResources'; 40 public const USE_CONSOLE_ATTRIBUTE = "useConsole"; 41 public const RENDERING_ONLY_RESULT_DEPRECATED = "onlyresult"; 42 public const CANONICAL = WebCodeTag::TAG; 43 public const DOKUWIKI_LANG = 'dw'; 44 public const FRAMEBORDER_ATTRIBUTE = "frameborder"; 45 /** 46 * @deprecated for type 47 */ 48 public const RENDERING_MODE_ATTRIBUTE = 'renderingmode'; 49 public const MARKIS = [WebCodeTag::MARKI_LANG, WebCodeTag::DOKUWIKI_LANG]; 50 public const EXTERNAL_RESOURCES_ATTRIBUTE_KEY = 'externalresources'; 51 /** 52 * Marki code 53 */ 54 public const MARKI_LANG = 'marki'; 55 public const IFRAME_BOOLEAN_ATTRIBUTE = "iframe"; 56 const STORY_TYPE = "story"; 57 const RESULT_TYPE = "result"; 58 const INJECT_TYPE = "inject"; 59 60 public static function getClass(): string 61 { 62 return StyleAttribute::addComboStrapSuffix(WebCodeTag::TAG); 63 } 64 65 public static function getKnownTypes(): array 66 { 67 return [self::STORY_TYPE, self::RESULT_TYPE, self::INJECT_TYPE]; 68 } 69 70 public static function getDefaultAttributes(): array 71 { 72 $defaultAttributes = array(); 73 $defaultAttributes[Dimension::WIDTH_KEY] = '100%'; 74 // 'type': no default to see if it was set because the default now is dependent on the content 75 // 'height' is set by the javascript if not set 76 // 'width' and 'scrolling' gets their natural value 77 return $defaultAttributes; 78 } 79 80 public static function handleExit(\Doku_Handler $handler): array 81 { 82 /** 83 * Capture all codes 84 */ 85 $codes = array(); 86 /** 87 * Does the javascript contains a console statement 88 */ 89 $useConsole = false; 90 91 /** 92 * Callstack 93 */ 94 $callStack = CallStack::createFromHandler($handler); 95 $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall(); 96 $type = $openingTag->getType(); 97 $renderingMode = $openingTag->getAttribute(WebCodeTag::RENDERING_MODE_ATTRIBUTE); 98 if ($renderingMode !== null) { 99 LogUtility::warning("The `renderingmode` attribute has been deprecated for the webcode `type` attribute."); 100 if ($type === null) { 101 $type = strtolower($renderingMode); 102 } 103 } 104 if ($type === WebCodeTag::RENDERING_ONLY_RESULT_DEPRECATED) { 105 LogUtility::warning("The `type` value (" . self::RENDERING_ONLY_RESULT_DEPRECATED . ") should be replaced by (" . self::RESULT_TYPE . ")"); 106 $type = WebCodeTag::RESULT_TYPE; 107 } 108 109 /** 110 * The mime (ie xml,html, ...) and code content are in two differents 111 * call. To be able to set the content to the good type 112 * we keep a trace of it 113 */ 114 $actualCodeType = ""; 115 116 /** 117 * Loop 118 */ 119 while ($actualTag = $callStack->next()) { 120 121 122 $tagName = $actualTag->getTagName(); 123 if (in_array($tagName, WebCodeTag::CODE_TAGS)) { 124 125 /** 126 * Only result or inject mode, we don't display the code 127 * on all node (enter, exit and unmatched) 128 */ 129 if (in_array($type, [WebCodeTag::RESULT_TYPE, self::INJECT_TYPE])) { 130 $actualTag->addAttribute(Display::DISPLAY, Display::DISPLAY_NONE_VALUE); 131 } 132 133 switch ($actualTag->getState()) { 134 135 case DOKU_LEXER_ENTER: 136 // Get the code (The content between the code nodes) 137 // We ltrim because the match gives us the \n at the beginning and at the end 138 $actualCodeType = strtolower(trim($actualTag->getType())); 139 140 // Xml is html 141 if ($actualCodeType === 'xml') { 142 $actualCodeType = 'html'; 143 } 144 145 // markdown, dokuwiki is marki 146 if (in_array($actualCodeType, ['md', 'markdown', 'dw'])) { 147 $actualCodeType = WebCodeTag::MARKI_LANG; 148 } 149 150 // The code for a language may be scattered in multiple block 151 if (!isset($codes[$actualCodeType])) { 152 $codes[$actualCodeType] = ""; 153 } 154 155 continue 2; 156 157 case DOKU_LEXER_UNMATCHED: 158 159 $codeContent = $actualTag->getPluginData()[PluginUtility::PAYLOAD]; 160 161 if (empty($actualCodeType)) { 162 LogUtility::msg("The type of the code should not be null for the code content " . $codeContent, LogUtility::LVL_MSG_WARNING, WebCodeTag::TAG); 163 continue 2; 164 } 165 166 // Append it 167 $codes[$actualCodeType] = $codes[$actualCodeType] . $codeContent; 168 169 // Check if a javascript console function is used, only if the flag is not set to true 170 if (!$useConsole) { 171 if (in_array($actualCodeType, array('babel', 'javascript', 'html', 'xml'))) { 172 // if the code contains 'console.' 173 $result = preg_match('/' . 'console\.' . '/is', $codeContent); 174 if ($result) { 175 $useConsole = true; 176 } 177 } 178 } 179 // Reset 180 $actualCodeType = ""; 181 break; 182 183 } 184 } 185 186 } 187 188 /** 189 * By default, markup code 190 * is rendered inside the page 191 * We got less problem such as iframe overflow 192 * due to lazy loading, such as relative link, ... 193 */ 194 if ( 195 array_key_exists(WebCodeTag::MARKI_LANG, $codes) 196 && count($codes) === 1 197 && $openingTag->getAttribute(WebCodeTag::IFRAME_BOOLEAN_ATTRIBUTE) === null 198 && $openingTag->getType() === null 199 ) { 200 $openingTag->setType(self::INJECT_TYPE); 201 } 202 203 return [ 204 WebCodeTag::CODES_ATTRIBUTE => $codes, 205 WebCodeTag::USE_CONSOLE_ATTRIBUTE => $useConsole, 206 PluginUtility::ATTRIBUTES => $openingTag->getAttributes() 207 ]; 208 } 209 210 /** 211 * Tag is of an iframe (Web code) or a div (wiki markup) 212 */ 213 public static function renderExit(TagAttributes $tagAttributes, array $data) 214 { 215 216 $codes = $data[WebCodeTag::CODES_ATTRIBUTE]; 217 218 $type = $tagAttributes->getType(); 219 if ($type === null) { 220 $type = self::STORY_TYPE; 221 } 222 223 /** 224 * Rendering mode is used in handle exit, we delete it 225 * to not get it in the HTML output 226 */ 227 $tagAttributes->removeComponentAttributeIfPresent(WebCodeTag::RENDERING_MODE_ATTRIBUTE); 228 229 // Create the real output of webcode 230 if (sizeof($codes) == 0) { 231 return false; 232 } 233 234 235 // Css 236 $snippetSystem = PluginUtility::getSnippetManager(); 237 $snippetSystem->attachCssInternalStyleSheet(WebCodeTag::TAG); 238 $snippetSystem->attachJavascriptFromComponentId(WebCodeTag::TAG); 239 240 // Mermaid code ? 241 if (array_key_exists(MermaidTag::MERMAID_CODE, $codes)) { 242 $mermaidCode = ""; 243 foreach ($codes as $codeKey => $code) { 244 if ($codeKey !== MermaidTag::MERMAID_CODE) { 245 LogUtility::error("The code type ($codeKey) was mixed with mermaid code in a webcode and this is not yet supported. The code was skipped"); 246 continue; 247 } 248 $mermaidCode .= $code; 249 } 250 $tagAttributes->addComponentAttributeValue(MermaidTag::MARKUP_CONTENT_ATTRIBUTE, $mermaidCode); 251 return MermaidTag::renderEnter($tagAttributes); 252 } 253 254 /** 255 * Dokuwiki Code 256 * (Just HTML) 257 */ 258 if (array_key_exists(WebCodeTag::MARKI_LANG, $codes)) { 259 260 $markupCode = $codes[WebCodeTag::MARKI_LANG]; 261 262 if ($type === self::INJECT_TYPE) { 263 /** 264 * the div is to be able to apply some CSS 265 * such as don't show editbutton on webcode 266 */ 267 $html = $tagAttributes->toHtmlEnterTag("div"); 268 try { 269 $contextPath = ExecutionContext::getActualOrCreateFromEnv() 270 ->getContextPath(); 271 $html .= FetcherMarkup::confChild() 272 ->setRequestedMarkupString($markupCode) 273 ->setDeleteRootBlockElement(false) 274 ->setIsDocument(false) 275 ->setRequestedContextPath($contextPath) 276 ->setRequestedMimeToXhtml() 277 ->build() 278 ->getFetchString(); 279 } catch (ExceptionCompile $e) { 280 $html .= $e->getMessage(); 281 LogUtility::log2file("Error while rendering webcode", LogUtility::LVL_MSG_ERROR, WebCodeTag::CANONICAL, $e); 282 } 283 $html .= "</div>"; 284 return $html; 285 } 286 287 /** 288 * Iframe output 289 */ 290 $tagAttributes->removeComponentAttribute(WebCodeTag::IFRAME_BOOLEAN_ATTRIBUTE); 291 292 if (!$tagAttributes->hasAttribute(TagAttributes::NAME_ATTRIBUTE)) { 293 $tagAttributes->addOutputAttributeValueIfNotEmpty(TagAttributes::NAME_ATTRIBUTE, "WebCode iFrame"); 294 } 295 try { 296 $url = FetcherMarkupWebcode::createFetcherMarkup($markupCode) 297 ->getFetchUrl() 298 ->toString(); 299 $tagAttributes->addOutputAttributeValue("src", $url); 300 } catch (ExceptionBadState $e) { 301 // The markup is provided, we shouldn't have a bad state 302 LogUtility::internalError("We were unable to set the iframe URL. Error:{$e->getMessage()}", WebCodeTag::CANONICAL); 303 } 304 return self::finishIframe($tagAttributes); 305 306 307 } 308 309 310 /** 311 * Js Html Css language 312 */ 313 if ($type === self::INJECT_TYPE) { 314 $htmlToInject = self::getCss($codes); 315 return $htmlToInject . self::getBodyHtmlAndJavascript($codes, false); 316 } 317 318 /** @noinspection JSUnresolvedLibraryURL */ 319 320 $headIFrame = <<<EOF 321<meta http-equiv="content-type" content="text/html; charset=UTF-8"/> 322<link id="normalize" rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.min.css"/> 323EOF; 324 325 326 // External Resources such as css stylesheet or js 327 $externalResources = []; 328 if ($tagAttributes->hasComponentAttribute(WebCodeTag::EXTERNAL_RESOURCES_ATTRIBUTE_KEY)) { 329 LogUtility::warning("The (" . WebCodeTag::EXTERNAL_RESOURCES_ATTRIBUTE_KEY . ") has been deprecated. You should put your script/link in a code block with the `display` attribute set to `none`."); 330 $resources = $tagAttributes->getValueAndRemove(WebCodeTag::EXTERNAL_RESOURCES_ATTRIBUTE_KEY); 331 $externalResources = explode(",", $resources); 332 } 333 334 // Jsx / Babel Preprocessor, if babel is used, add it to the external resources 335 if (array_key_exists('babel', $codes)) { 336 $babelMin = "https://unpkg.com/babel-standalone@6/babel.min.js"; 337 // a load of babel invoke it (be sure to not have it twice 338 if (!(array_key_exists($babelMin, $externalResources))) { 339 $externalResources[] = $babelMin; 340 } 341 } 342 343 // Add the external resources 344 foreach ($externalResources as $externalResource) { 345 $pathInfo = pathinfo($externalResource); 346 $fileExtension = $pathInfo['extension']; 347 switch ($fileExtension) { 348 case 'css': 349 $headIFrame .= "<link rel=\"stylesheet\" type=\"text/css\" href=\"$externalResource\"/>"; 350 break; 351 case 'js': 352 $headIFrame .= "<script type=\"text/javascript\" src=\"$externalResource\"></script>"; 353 break; 354 } 355 } 356 357 // WebConsole style sheet 358 $webcodeClass = WebCodeTag::getClass(); 359 $cssUrl = FetcherRawLocalPath::createFromPath(WikiPath::createComboResource("webcode:webcode-iframe.css"))->getFetchUrl()->toHtmlString(); 360 $headIFrame .= "<link class='$webcodeClass' rel=\"stylesheet\" type=\"text/css\" href=\"$cssUrl\"/>"; 361 362 // A little margin to make it neater 363 // that can be overwritten via cascade 364 $headIFrame .= "<style class=\"$webcodeClass\">body { margin:10px } /* default margin */</style>"; 365 366 // The css 367 $headIFrame .= self::getCss($codes); 368 369 // The javascript console script should be first to handle console.log in the content 370 $useConsole = $data[WebCodeTag::USE_CONSOLE_ATTRIBUTE]; 371 if ($useConsole) { 372 $url = FetcherRawLocalPath::createFromPath(WikiPath::createComboResource("webcode:webcode-console.js"))->getFetchUrl()->toHtmlString(); 373 $headIFrame .= <<<EOF 374<script class="$webcodeClass" type="text/javascript" src="$url"></script> 375EOF; 376 } 377 $body = self::getBodyHtmlAndJavascript($codes, $useConsole); 378 $iframeSrcValue = <<<EOF 379<html lang="en"> 380<head> 381<title>Made by WebCode</title> 382$headIFrame 383</head> 384<body> 385$body 386</body> 387</html> 388EOF; 389 $tagAttributes->addOutputAttributeValue("srcdoc", $iframeSrcValue); 390 391 // Code bar with button 392 // Credits bar 393 $bar = '<div class="webcode-bar">'; 394 $bar .= '<div class="webcode-bar-item">' . PluginUtility::getDocumentationHyperLink(WebCodeTag::TAG, "Rendered by WebCode", false) . '</div>'; 395 $bar .= '<div class="webcode-bar-item">' . self::addJsFiddleButton($codes, $externalResources, $useConsole, $tagAttributes->getValue("name")) . '</div>'; 396 $bar .= '</div>'; 397 398 return self::finishIframe($tagAttributes, $bar); 399 400 401 } 402 403 /** 404 * @param array $codes the array containing the codes 405 * @param array $externalResources the attributes of a call (for now the externalResources) 406 * @param bool $useConsole 407 * @param null $snippetTitle 408 * @return string the HTML form code 409 * 410 * Specification, see http://doc.jsfiddle.net/api/post.html 411 */ 412 public static function addJsFiddleButton($codes, $externalResources, $useConsole = false, $snippetTitle = null): string 413 { 414 415 $postURL = "https://jsfiddle.net/api/post/library/pure/"; //No Framework 416 417 418 if ($useConsole) { 419 // If their is a console.log function, add the Firebug Lite support of JsFiddle 420 // Seems to work only with the Edge version of jQuery 421 // $postURL .= "edge/dependencies/Lite/"; 422 // The firebug logging is not working anymore because of 404 423 424 // Adding them here 425 // The firebug resources for the console.log features 426 try { 427 $externalResources[] = FetcherRawLocalPath::createFromPath(WikiPath::createComboResource(':firebug:firebug-lite.css'))->getFetchUrl()->toString(); 428 $externalResources[] = FetcherRawLocalPath::createFromPath(WikiPath::createComboResource(':firebug:firebug-lite-1.2.js'))->getFetchUrl()->toString(); 429 } catch (ExceptionNotFound $e) { 430 LogUtility::internalError("We were unable to add the firebug css and js. Error: {$e->getMessage()}", WebCodeTag::CANONICAL); 431 } 432 433 } 434 435 // The below code is to prevent this JsFiddle bug: https://github.com/jsfiddle/jsfiddle-issues/issues/726 436 // The order of the resources is not guaranteed 437 // We pass then the resources only if their is one resources 438 // Otherwise we pass them as a script element in the HTML. 439 if (count($externalResources) <= 1) { 440 $externalResourcesInput = '<input type="hidden" name="resources" value="' . implode(",", $externalResources) . '"/>'; 441 } else { 442 $externalResourcesInput = ''; 443 if (!array_key_exists('html', $codes)) { 444 $codes['html'] = ''; 445 } 446 $codes['html'] .= "\n\n\n\n\n<!-- The resources -->\n"; 447 $codes['html'] .= "<!-- They have been added here because their order is not guarantee through the API. -->\n"; 448 $codes['html'] .= "<!-- See: https://github.com/jsfiddle/jsfiddle-issues/issues/726 -->\n"; 449 foreach ($externalResources as $externalResource) { 450 if ($externalResource !== "") { 451 $extension = pathinfo($externalResource)['extension']; 452 switch ($extension) { 453 case "css": 454 $codes['html'] .= "<link href=\"$externalResource\" rel=\"stylesheet\"/>\n"; 455 break; 456 case "js": 457 $codes['html'] .= "<script src=\"$externalResource\"></script>\n"; 458 break; 459 default: 460 $codes['html'] .= "<!-- " . $externalResource . " -->\n"; 461 } 462 } 463 } 464 } 465 466 $jsCode = $codes['javascript'] ?? null; 467 $jsPanel = 0; // language for the js specific panel (0 = JavaScript) 468 if (array_key_exists('babel', $codes)) { 469 $jsCode = $codes['babel']; 470 $jsPanel = 3; // 3 = Babel 471 } 472 473 // Title and description 474 global $ID; 475 $pageTitle = tpl_pagetitle($ID, true); 476 if (!$snippetTitle) { 477 478 $snippetTitle = "Code from " . $pageTitle; 479 } 480 $description = "Code from the page '" . $pageTitle . "' \n" . wl($ID, $absolute = true); 481 return '<form method="post" action="' . $postURL . '" target="_blank">' . 482 '<input type="hidden" name="title" value="' . htmlentities($snippetTitle) . '"/>' . 483 '<input type="hidden" name="description" value="' . htmlentities($description) . '"/>' . 484 '<input type="hidden" name="css" value="' . htmlentities($codes['css'] ?? '') . '"/>' . 485 '<input type="hidden" name="html" value="' . htmlentities("<!-- The HTML -->" . $codes['html'] ?? '') . '"/>' . 486 '<input type="hidden" name="js" value="' . htmlentities($jsCode) . '"/>' . 487 '<input type="hidden" name="panel_js" value="' . htmlentities($jsPanel) . '"/>' . 488 '<input type="hidden" name="wrap" value="b"/>' . //javascript no wrap in body 489 $externalResourcesInput . 490 '<button>Try the code</button>' . 491 '</form>'; 492 493 } 494 495 private static function finishIframe(TagAttributes $tagAttributes, string $bar = ""): string 496 { 497 /** 498 * The iframe does not have any width 499 * By default, we set it to 100% and it can be 500 * constraint with the `width` attributes that will 501 * set a a max-width 502 */ 503 $tagAttributes->addStyleDeclarationIfNotSet("width", "100%"); 504 505 /** 506 * FrameBorder 507 */ 508 $frameBorder = $tagAttributes->getValueAndRemoveIfPresent(WebCodeTag::FRAMEBORDER_ATTRIBUTE); 509 if ($frameBorder !== null && $frameBorder == 0) { 510 $tagAttributes->addStyleDeclarationIfNotSet("border", "none"); 511 } 512 513 $iFrameHtml = $tagAttributes->toHtmlEnterTag("iframe") . '</iframe>'; 514 return "<div class=\"webcode-wrapper\">" . $iFrameHtml . $bar . '</div>'; 515 } 516 517 /** 518 * Return the body 519 * @param $codes - the code to apply 520 * @param $useConsole - if the console area should be printed 521 * @return string - the html and javascript 522 */ 523 private static function getBodyHtmlAndJavascript($codes, $useConsole): string 524 { 525 526 $body = ""; 527 if (array_key_exists('html', $codes)) { 528 // The HTML code 529 $body .= $codes['html']; 530 } 531 // The javascript console area is based at the end of the HTML document 532 if ($useConsole) { 533 534 $body .= <<<EOF 535<!-- WebCode Console --> 536<div class="webcode-console-wrapper"> 537 <p class="webConsoleTitle">Console Output:</p> 538 <div id="webCodeConsole"></div> 539</div> 540EOF; 541 } 542 // The javascript comes at the end because it may want to be applied on previous HTML element 543 // as the page load in the IO order, javascript must be placed at the end 544 if (array_key_exists('javascript', $codes)) { 545 /** 546 * The user should escapes the following character * <, >, ", ', \, and &. 547 * because they will interfere with the HTML parser 548 * 549 * The user should write `<\/script>` and note `</script>` 550 */ 551 // The Javascript code 552 $body .= '<script class="webcode-javascript" type="text/javascript">' . $codes['javascript'] . '</script>'; 553 } 554 if (array_key_exists('babel', $codes)) { 555 // The Babel code 556 $body .= '<script type="text/babel">' . $codes['babel'] . '</script>'; 557 } 558 return $body; 559 560 } 561 562 private static function getCss($codes): string 563 { 564 if (array_key_exists('css', $codes)) { 565 return '<style>' . $codes['css'] . '</style>'; 566 }; 567 return ""; 568 } 569} 570