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 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 17 * @author Nicolas GERARD 18 */ 19 20// must be run within Dokuwiki 21use ComboStrap\SnippetManager; 22use ComboStrap\LogUtility; 23use ComboStrap\PluginUtility; 24use ComboStrap\Tag; 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 = array("code", "plugin_combo_code"); 48 49 /** 50 * The attribute names in the array 51 */ 52 const CODES_ATTRIBUTE = "codes"; 53 const USE_CONSOLE_ATTRIBUTE = "useConsole"; 54 55 /** 56 * @var array that holds the iframe attributes 57 */ 58 private $attributes = array(); 59 60 61 /** 62 * Syntax Type. 63 * 64 * Needs to return one of the mode types defined in $PARSER_MODES in parser.php 65 * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types 66 * 67 * container because it may contain header in case of how to 68 */ 69 public function getType() 70 { 71 return 'container'; 72 } 73 74 /** 75 * @return array 76 * Allow which kind of plugin inside 77 * 78 * array('container', 'baseonly','formatting', 'substition', 'protected', 'disabled', 'paragraphs') 79 * 80 */ 81 public function getAllowedTypes() 82 { 83 return array('container', 'baseonly', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs'); 84 } 85 86 87 public function accepts($mode) 88 { 89 if (!$this->getConf(syntax_plugin_combo_preformatted::CONF_PREFORMATTED_ENABLE)) { 90 return PluginUtility::disablePreformatted($mode); 91 } else { 92 return true; 93 } 94 } 95 96 /** 97 * @see Doku_Parser_Mode::getSort() 98 * The mode (plugin) with the lowest sort number will win out 99 * 100 * See {@link Doku_Parser_Mode_code} 101 */ 102 public function getSort() 103 { 104 return 99; 105 } 106 107 /** 108 * Called before any calls to ConnectTo 109 * @return void 110 */ 111 function preConnect() 112 { 113 } 114 115 /** 116 * Create a pattern that will called this plugin 117 * 118 * @param string $mode 119 * 120 * All dokuwiki mode can be seen in the parser.php file 121 * @see Doku_Parser_Mode::connectTo() 122 */ 123 public function connectTo($mode) 124 { 125 126 $pattern = PluginUtility::getContainerTagPattern(self::TAG); 127 $this->Lexer->addEntryPattern($pattern, $mode, PluginUtility::getModeForComponent($this->getPluginComponent())); 128 129 } 130 131 132 // This where the addPattern and addExitPattern are defined 133 public function postConnect() 134 { 135 $this->Lexer->addExitPattern('</' . self::TAG . '>', PluginUtility::getModeForComponent($this->getPluginComponent())); 136 } 137 138 139 /** 140 * Handle the match 141 * You get the match for each pattern in the $match variable 142 * $state says if it's an entry, exit or match pattern 143 * 144 * This is an instruction block and is cached apart from the rendering output 145 * There is two caches levels 146 * This cache may be suppressed with the url parameters ?purge=true 147 * 148 * The returned values are cached in an array that will be passed to the render method 149 * The handle function goal is to parse the matched syntax through the pattern function 150 * and to return the result for use in the renderer 151 * This result is always cached until the page is modified. 152 * @param string $match 153 * @param int $state 154 * @param int $pos 155 * @param Doku_Handler $handler 156 * @return array|bool 157 * @throws Exception 158 * @see DokuWiki_Syntax_Plugin::handle() 159 * 160 */ 161 public function handle($match, $state, $pos, Doku_Handler $handler) 162 { 163 switch ($state) { 164 165 case DOKU_LEXER_ENTER : 166 167 // We got the first webcode tag and its attributes 168 169 $match = substr($match, 8, -1); //9 = strlen("<webcode") 170 171 // Reset of the attributes 172 // With some framework the php object may be still persisted in memory 173 // And you may get some attributes from other page 174 $attributes = array(); 175 $attributes['frameborder'] = 1; 176 $attributes['width'] = '100%'; 177 178 $renderingModeKey = 'renderingmode'; 179 $attributes[$renderingModeKey] = 'story'; 180 181 // config Parameters will get their value in lowercase 182 $configAttributes = [$renderingModeKey]; 183 184 // /i not case sensitive 185 $attributePattern = "\s*(\w+)\s*=\s*\"?([^\"\s]+)\"?\\s*"; 186 $result = preg_match_all('/' . $attributePattern . '/i', $match, $matches); 187 188 189 if ($result != 0) { 190 foreach ($matches[1] as $key => $lang) { 191 $attributeKey = strtolower($lang); 192 $attributeValue = $matches[2][$key]; 193 if (in_array($attributeKey, $configAttributes)) { 194 $attributeValue = strtolower($attributeValue); 195 } 196 $attributes[$attributeKey] = $attributeValue; 197 } 198 } 199 200 // We set the attributes on a class scope 201 // to be used in the DOKU_LEXER_UNMATCHED step 202 $this->attributes = $attributes; 203 204 // Cache the values to be used by the render method 205 return array( 206 PluginUtility::STATE => $state, 207 PluginUtility::ATTRIBUTES => $attributes 208 ); 209 210 211 case DOKU_LEXER_UNMATCHED : 212 213 return PluginUtility::handleAndReturnUnmatchedData(self::TAG,$match,$handler); 214 215 216 case DOKU_LEXER_EXIT: 217 218 /** 219 * Capture all codes 220 */ 221 $codes = array(); 222 /** 223 * Does the javascript contains a console statement 224 */ 225 $useConsole = false; 226 $exitTag = new Tag(self::TAG, array(), $state, $handler); 227 $openingTag = $exitTag->getOpeningTag(); 228 if ($openingTag->hasDescendants()) { 229 $tags = $openingTag->getDescendants(); 230 /** 231 * Mime and code content are in two differents 232 * tag. To be able to set the content to the good type 233 * we keep a trace of it 234 */ 235 $actualCodeType = ""; 236 foreach ($tags as $tag) { 237 if (in_array($tag->getName(), self::CODE_TAGS)) { 238 239 if ($tag->getState() == DOKU_LEXER_ENTER) { 240 // Get the code (The content between the code nodes) 241 // We ltrim because the match gives us the \n at the beginning and at the end 242 $actualCodeType = strtolower(trim($tag->getType())); 243 244 // Xml is html 245 if ($actualCodeType == 'xml') { 246 $actualCodeType = 'html'; 247 } 248 // The code for a language may be scattered in mutliple block 249 if (!isset($codes[$actualCodeType])) { 250 $codes[$actualCodeType] = ""; 251 } 252 continue; 253 } 254 255 if ($tag->getState() == DOKU_LEXER_UNMATCHED) { 256 257 $codeContent = $tag->getData()[PluginUtility::PAYLOAD]; 258 259 if (empty($actualCodeType)) { 260 LogUtility::msg("The type of the code should not be null for the code content " . $codeContent, LogUtility::LVL_MSG_WARNING, self::TAG); 261 continue; 262 } 263 264 // Append it 265 $codes[$actualCodeType] = $codes[$actualCodeType] . $codeContent; 266 267 // Check if a javascript console function is used, only if the flag is not set to true 268 if (!$useConsole == true) { 269 if (in_array($actualCodeType, array('babel', 'javascript', 'html', 'xml'))) { 270 // if the code contains 'console.' 271 $result = preg_match('/' . 'console\.' . '/is', $codeContent); 272 if ($result) { 273 $useConsole = true; 274 } 275 } 276 } 277 // Reset 278 $actualCodeType = ""; 279 } 280 } 281 } 282 } 283 return array( 284 PluginUtility::STATE => $state, 285 self::CODES_ATTRIBUTE => $codes, 286 self::USE_CONSOLE_ATTRIBUTE => $useConsole 287 ); 288 289 } 290 return false; 291 292 } 293 294 /** 295 * Render the output 296 * @param string $mode 297 * @param Doku_Renderer $renderer 298 * @param array $data - what the function handle() return'ed 299 * @return bool - rendered correctly (not used) 300 * 301 * The rendering process 302 * @see DokuWiki_Syntax_Plugin::render() 303 * 304 */ 305 public function render($mode, Doku_Renderer $renderer, $data) 306 { 307 // The $data variable comes from the handle() function 308 // 309 // $mode = 'xhtml' means that we output html 310 // There is other mode such as metadata where you can output data for the headers (Not 100% sure) 311 if ($mode == 'xhtml') { 312 313 314 /** @var Doku_Renderer_xhtml $renderer */ 315 316 $state = $data[PluginUtility::STATE]; 317 switch ($state) { 318 319 case DOKU_LEXER_ENTER : 320 321 PluginUtility::getSnippetManager()->upsertJavascriptForBar(self::TAG); 322 323 // The extracted data are the attribute of the webcode tag 324 // We put in a class variable so that we can use in the last step (DOKU_LEXER_EXIT) 325 $this->attributes = $data[PluginUtility::ATTRIBUTES]; 326 327 break; 328 329 case DOKU_LEXER_UNMATCHED : 330 331 $renderer->doc .= PluginUtility::renderUnmatched($data); 332 break; 333 334 case DOKU_LEXER_EXIT : 335 $codes = $data[self::CODES_ATTRIBUTE]; 336 // Create the real output of webcode 337 if (sizeof($codes) == 0) { 338 return false; 339 } 340 341 PluginUtility::getSnippetManager()->upsertCssSnippetForBar(self::TAG); 342 343 // Dokuwiki Code ? 344 if (array_key_exists('dw', $codes)) { 345 346 $renderer->doc .= PluginUtility::render($codes['dw']); 347 348 } else { 349 350 351 // Js, Html, Css 352 $iframeHtml = '<html><head>'; 353 $iframeHtml .= '<meta http-equiv="content-type" content="text/html; charset=UTF-8">'; 354 $iframeHtml .= '<title>Made by Webcode</title>'; 355 $iframeHtml .= '<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.min.css">'; 356 357 358 // External Resources such as css stylesheet or js 359 $externalResources = array(); 360 if (array_key_exists(self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY, $this->attributes)) { 361 $externalResources = explode(",", $this->attributes[self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY]); 362 } 363 364 // Babel Preprocessor, if babel is used, add it to the external resources 365 if (array_key_exists('babel', $codes)) { 366 $babelMin = "https://unpkg.com/babel-standalone@6/babel.min.js"; 367 // a load of babel invoke it (be sure to not have it twice 368 if (!(array_key_exists($babelMin, $externalResources))) { 369 $externalResources[] = $babelMin; 370 } 371 } 372 373 // Add the external resources 374 foreach ($externalResources as $externalResource) { 375 $pathInfo = pathinfo($externalResource); 376 $fileExtension = $pathInfo['extension']; 377 switch ($fileExtension) { 378 case 'css': 379 $iframeHtml .= '<link rel="stylesheet" type="text/css" href="' . $externalResource . '">'; 380 break; 381 case 'js': 382 $iframeHtml .= '<script type="text/javascript" src="' . $externalResource . '"></script>'; 383 break; 384 } 385 } 386 387 388 // WebConsole style sheet 389 $iframeHtml .= '<link rel="stylesheet" type="text/css" href="' . PluginUtility::getResourceBaseUrl() . '/webcode/webcode-iframe.css?ver=' . self::WEB_CSS_VERSION . '"/>'; 390 391 if (array_key_exists('css', $codes)) { 392 $iframeHtml .= '<!-- The CSS code -->'; 393 $iframeHtml .= '<style>' . $codes['css'] . '</style>'; 394 }; 395 $iframeHtml .= '</head><body style="margin:10px">'; 396 if (array_key_exists('html', $codes)) { 397 $iframeHtml .= '<!-- The HTML code -->'; 398 $iframeHtml .= $codes['html']; 399 } 400 // The javascript console area is based at the end of the HTML document 401 $useConsole = $data[self::USE_CONSOLE_ATTRIBUTE]; 402 if ($useConsole) { 403 $iframeHtml .= '<!-- WebCode Console -->'; 404 $iframeHtml .= '<div><p class=\'webConsoleTitle\'>Console Output:</p>'; 405 $iframeHtml .= '<div id=\'webCodeConsole\'></div>'; 406 $iframeHtml .= '<script type=\'text/javascript\' src=\'' . PluginUtility::getResourceBaseUrl() . '/webcode/webcode-console.js?ver=' . self::WEB_CONSOLE_JS_VERSION . '\'></script>'; 407 $iframeHtml .= '</div>'; 408 } 409 // The javascript comes at the end because it may want to be applied on previous HTML element 410 // as the page load in the IO order, javascript must be placed at the end 411 if (array_key_exists('javascript', $codes)) { 412 $iframeHtml .= '<!-- The Javascript code -->'; 413 $iframeHtml .= '<script type="text/javascript">' . $codes['javascript'] . '</script>'; 414 } 415 if (array_key_exists('babel', $codes)) { 416 $iframeHtml .= '<!-- The Babel code -->'; 417 $iframeHtml .= '<script type="text/babel">' . $codes['babel'] . '</script>'; 418 } 419 $iframeHtml .= '</body></html>'; 420 421 // Here the magic from the plugin happens 422 // We add the Iframe and the JsFiddleButton 423 $iFrameHtml = '<iframe '; 424 425 // We add the name HTML attribute 426 $name = "WebCode iFrame"; 427 if (array_key_exists('name', $this->attributes)) { 428 $name .= ' ' . $this->attributes['name']; 429 } 430 $iFrameHtml .= ' name="' . $name . '" '; 431 432 // The class to be able to select them 433 $iFrameHtml .= ' class="webCode" '; 434 435 // We add the others HTML attributes 436 $iFrameHtmlAttributes = array('width', 'height', 'frameborder', 'scrolling'); 437 foreach ($this->attributes as $attribute => $value) { 438 if (in_array($attribute, $iFrameHtmlAttributes)) { 439 $iFrameHtml .= ' ' . $attribute . '=' . $value; 440 } 441 } 442 $iFrameHtml .= ' srcdoc="' . htmlentities($iframeHtml) . '" ></iframe>';// 443 444 // Credits bar 445 $bar = '<div class="webcode-bar">'; 446 $bar .= '<div class="webcode-bar-item">' . PluginUtility::getUrl(self::TAG, "Rendered by Webcode",false) . '</div>'; 447 $bar .= '<div class="webcode-bar-item">' . $this->addJsFiddleButton($codes, $this->attributes) . '</div>'; 448 $bar .= '</div>'; 449 $renderer->doc .= '<div class="webcode">' . $iFrameHtml . $bar . '</div>'; 450 } 451 452 break; 453 } 454 455 return true; 456 } 457 return false; 458 } 459 460 /** 461 * @param array $codes the array containing the codes 462 * @param array $attributes the attributes of a call (for now the externalResources) 463 * @return string the HTML form code 464 * 465 * Specification, see http://doc.jsfiddle.net/api/post.html 466 */ 467 public function addJsFiddleButton($codes, $attributes) 468 { 469 470 $postURL = "https://jsfiddle.net/api/post/library/pure/"; //No Framework 471 472 $externalResources = array(); 473 if (array_key_exists(self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY, $attributes)) { 474 $externalResources = explode(",", $attributes[self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY]); 475 } 476 477 478 if ($this->useConsole) { 479 // If their is a console.log function, add the Firebug Lite support of JsFiddle 480 // Seems to work only with the Edge version of jQuery 481 // $postURL .= "edge/dependencies/Lite/"; 482 // The firebug logging is not working anymore because of 404 483 // Adding them here 484 $externalResources[] = 'The firebug resources for the console.log features'; 485 $externalResources[] = PluginUtility::getResourceBaseUrl() . '/firebug/firebug-lite.css'; 486 $externalResources[] = PluginUtility::getResourceBaseUrl() . '/firebug/firebug-lite-1.2.js'; 487 } 488 489 // The below code is to prevent this JsFiddle bug: https://github.com/jsfiddle/jsfiddle-issues/issues/726 490 // The order of the resources is not guaranteed 491 // We pass then the resources only if their is one resources 492 // Otherwise we pass them as a script element in the HTML. 493 if (count($externalResources) <= 1) { 494 $externalResourcesInput = '<input type="hidden" name="resources" value="' . implode(",", $externalResources) . '">'; 495 } else { 496 $codes['html'] .= "\n\n\n\n\n<!-- The resources -->\n"; 497 $codes['html'] .= "<!-- They have been added here because their order is not guarantee through the API. -->\n"; 498 $codes['html'] .= "<!-- See: https://github.com/jsfiddle/jsfiddle-issues/issues/726 -->\n"; 499 foreach ($externalResources as $externalResource) { 500 if ($externalResource != "") { 501 $extension = pathinfo($externalResource)['extension']; 502 switch ($extension) { 503 case "css": 504 $codes['html'] .= "<link href=\"" . $externalResource . "\" rel=\"stylesheet\">\n"; 505 break; 506 case "js": 507 $codes['html'] .= "<script src=\"" . $externalResource . "\"></script>\n"; 508 break; 509 default: 510 $codes['html'] .= "<!-- " . $externalResource . " -->\n"; 511 } 512 } 513 } 514 } 515 516 $jsCode = $codes['javascript']; 517 $jsPanel = 0; // language for the js specific panel (0 = JavaScript) 518 if (array_key_exists('babel', $codes)) { 519 $jsCode = $codes['babel']; 520 $jsPanel = 3; // 3 = Babel 521 } 522 523 // Title and description 524 global $ID; 525 $title = $attributes['name']; 526 $pageTitle = tpl_pagetitle($ID, true); 527 if (!$title) { 528 529 $title = "Code from " . $pageTitle; 530 } 531 $description = "Code from the page '" . $pageTitle . "' \n" . wl($ID, $absolute = true); 532 return '<form method="post" action="' . $postURL . '" target="_blank">' . 533 '<input type="hidden" name="title" value="' . htmlentities($title) . '">' . 534 '<input type="hidden" name="description" value="' . htmlentities($description) . '">' . 535 '<input type="hidden" name="css" value="' . htmlentities($codes['css']) . '">' . 536 '<input type="hidden" name="html" value="' . htmlentities("<!-- The HTML -->" . $codes['html']) . '">' . 537 '<input type="hidden" name="js" value="' . htmlentities($jsCode) . '">' . 538 '<input type="hidden" name="panel_js" value="' . htmlentities($jsPanel) . '">' . 539 '<input type="hidden" name="wrap" value="b">' . //javascript no wrap in body 540 $externalResourcesInput . 541 '<button>Try the code</button>' . 542 '</form>'; 543 544 } 545 546 /** 547 * @param $codes the array containing the codes 548 * @param $attributes the attributes of a call (for now the externalResources) 549 * @return string the HTML form code 550 */ 551 public function addCodePenButton($codes, $attributes) 552 { 553 // TODO 554 // http://blog.codepen.io/documentation/api/prefill/ 555 } 556 557 558} 559