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 // Cache the values 214 return array( 215 PluginUtility::STATE => $state, 216 PluginUtility::PAYLOAD => $match 217 ); 218 219 case DOKU_LEXER_EXIT: 220 221 /** 222 * Capture all codes 223 */ 224 $codes = array(); 225 /** 226 * Does the javascript contains a console statement 227 */ 228 $useConsole = false; 229 $exitTag = new Tag(self::TAG, array(), $state, $handler); 230 $openingTag = $exitTag->getOpeningTag(); 231 if ($openingTag->hasDescendants()) { 232 $tags = $openingTag->getDescendants(); 233 /** 234 * Mime and code content are in two differents 235 * tag. To be able to set the content to the good type 236 * we keep a trace of it 237 */ 238 $actualCodeType = ""; 239 foreach ($tags as $tag) { 240 if (in_array($tag->getName(), self::CODE_TAGS)) { 241 242 if ($tag->getState() == DOKU_LEXER_ENTER) { 243 // Get the code (The content between the code nodes) 244 // We ltrim because the match gives us the \n at the beginning and at the end 245 $actualCodeType = strtolower(trim($tag->getType())); 246 247 // Xml is html 248 if ($actualCodeType == 'xml') { 249 $actualCodeType = 'html'; 250 } 251 // The code for a language may be scattered in mutliple block 252 if (!isset($codes[$actualCodeType])) { 253 $codes[$actualCodeType] = ""; 254 } 255 continue; 256 } 257 258 if ($tag->getState() == DOKU_LEXER_UNMATCHED) { 259 260 $codeContent = $tag->getData()[PluginUtility::PAYLOAD]; 261 262 if (empty($actualCodeType)) { 263 LogUtility::msg("The type of the code should not be null for the code content " . $codeContent, LogUtility::LVL_MSG_WARNING, self::TAG); 264 continue; 265 } 266 267 // Append it 268 $codes[$actualCodeType] = $codes[$actualCodeType] . $codeContent; 269 270 // Check if a javascript console function is used, only if the flag is not set to true 271 if (!$useConsole == true) { 272 if (in_array($actualCodeType, array('babel', 'javascript', 'html', 'xml'))) { 273 // if the code contains 'console.' 274 $result = preg_match('/' . 'console\.' . '/is', $codeContent); 275 if ($result) { 276 $useConsole = true; 277 } 278 } 279 } 280 // Reset 281 $actualCodeType = ""; 282 } 283 } 284 } 285 } 286 return array( 287 PluginUtility::STATE => $state, 288 self::CODES_ATTRIBUTE => $codes, 289 self::USE_CONSOLE_ATTRIBUTE => $useConsole 290 ); 291 292 } 293 return false; 294 295 } 296 297 /** 298 * Render the output 299 * @param string $mode 300 * @param Doku_Renderer $renderer 301 * @param array $data - what the function handle() return'ed 302 * @return bool - rendered correctly (not used) 303 * 304 * The rendering process 305 * @see DokuWiki_Syntax_Plugin::render() 306 * 307 */ 308 public function render($mode, Doku_Renderer $renderer, $data) 309 { 310 // The $data variable comes from the handle() function 311 // 312 // $mode = 'xhtml' means that we output html 313 // There is other mode such as metadata where you can output data for the headers (Not 100% sure) 314 if ($mode == 'xhtml') { 315 316 317 /** @var Doku_Renderer_xhtml $renderer */ 318 319 $state = $data[PluginUtility::STATE]; 320 switch ($state) { 321 322 case DOKU_LEXER_ENTER : 323 324 PluginUtility::getSnippetManager()->upsertJavascriptForBar(self::TAG); 325 326 // The extracted data are the attribute of the webcode tag 327 // We put in a class variable so that we can use in the last step (DOKU_LEXER_EXIT) 328 $this->attributes = $data[PluginUtility::ATTRIBUTES]; 329 330 break; 331 332 case DOKU_LEXER_UNMATCHED : 333 334 // Render and escape 335 $renderer->doc .= $renderer->_xmlEntities($data[PluginUtility::PAYLOAD]); 336 break; 337 338 case DOKU_LEXER_EXIT : 339 $codes = $data[self::CODES_ATTRIBUTE]; 340 // Create the real output of webcode 341 if (sizeof($codes) == 0) { 342 return false; 343 } 344 345 PluginUtility::getSnippetManager()->upsertCssSnippetForBar(self::TAG); 346 347 // Dokuwiki Code ? 348 if (array_key_exists('dw', $codes)) { 349 350 $renderer->doc .= PluginUtility::render($codes['dw']); 351 352 } else { 353 354 355 // Js, Html, Css 356 $iframeHtml = '<html><head>'; 357 $iframeHtml .= '<meta http-equiv="content-type" content="text/html; charset=UTF-8">'; 358 $iframeHtml .= '<title>Made by Webcode</title>'; 359 $iframeHtml .= '<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.min.css">'; 360 361 362 // External Resources such as css stylesheet or js 363 $externalResources = array(); 364 if (array_key_exists(self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY, $this->attributes)) { 365 $externalResources = explode(",", $this->attributes[self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY]); 366 } 367 368 // Babel Preprocessor, if babel is used, add it to the external resources 369 if (array_key_exists('babel', $codes)) { 370 $babelMin = "https://unpkg.com/babel-standalone@6/babel.min.js"; 371 // a load of babel invoke it (be sure to not have it twice 372 if (!(array_key_exists($babelMin, $externalResources))) { 373 $externalResources[] = $babelMin; 374 } 375 } 376 377 // Add the external resources 378 foreach ($externalResources as $externalResource) { 379 $pathInfo = pathinfo($externalResource); 380 $fileExtension = $pathInfo['extension']; 381 switch ($fileExtension) { 382 case 'css': 383 $iframeHtml .= '<link rel="stylesheet" type="text/css" href="' . $externalResource . '">'; 384 break; 385 case 'js': 386 $iframeHtml .= '<script type="text/javascript" src="' . $externalResource . '"></script>'; 387 break; 388 } 389 } 390 391 392 // WebConsole style sheet 393 $iframeHtml .= '<link rel="stylesheet" type="text/css" href="' . PluginUtility::getResourceBaseUrl() . '/webcode/webcode-iframe.css?ver=' . self::WEB_CSS_VERSION . '"/>'; 394 395 if (array_key_exists('css', $codes)) { 396 $iframeHtml .= '<!-- The CSS code -->'; 397 $iframeHtml .= '<style>' . $codes['css'] . '</style>'; 398 }; 399 $iframeHtml .= '</head><body style="margin:10px">'; 400 if (array_key_exists('html', $codes)) { 401 $iframeHtml .= '<!-- The HTML code -->'; 402 $iframeHtml .= $codes['html']; 403 } 404 // The javascript console area is based at the end of the HTML document 405 $useConsole = $data[self::USE_CONSOLE_ATTRIBUTE]; 406 if ($useConsole) { 407 $iframeHtml .= '<!-- WebCode Console -->'; 408 $iframeHtml .= '<div><p class=\'webConsoleTitle\'>Console Output:</p>'; 409 $iframeHtml .= '<div id=\'webCodeConsole\'></div>'; 410 $iframeHtml .= '<script type=\'text/javascript\' src=\'' . PluginUtility::getResourceBaseUrl() . '/webcode/webcode-console.js?ver=' . self::WEB_CONSOLE_JS_VERSION . '\'></script>'; 411 $iframeHtml .= '</div>'; 412 } 413 // The javascript comes at the end because it may want to be applied on previous HTML element 414 // as the page load in the IO order, javascript must be placed at the end 415 if (array_key_exists('javascript', $codes)) { 416 $iframeHtml .= '<!-- The Javascript code -->'; 417 $iframeHtml .= '<script type="text/javascript">' . $codes['javascript'] . '</script>'; 418 } 419 if (array_key_exists('babel', $codes)) { 420 $iframeHtml .= '<!-- The Babel code -->'; 421 $iframeHtml .= '<script type="text/babel">' . $codes['babel'] . '</script>'; 422 } 423 $iframeHtml .= '</body></html>'; 424 425 // Here the magic from the plugin happens 426 // We add the Iframe and the JsFiddleButton 427 $iFrameHtml = '<iframe '; 428 429 // We add the name HTML attribute 430 $name = "WebCode iFrame"; 431 if (array_key_exists('name', $this->attributes)) { 432 $name .= ' ' . $this->attributes['name']; 433 } 434 $iFrameHtml .= ' name="' . $name . '" '; 435 436 // The class to be able to select them 437 $iFrameHtml .= ' class="webCode" '; 438 439 // We add the others HTML attributes 440 $iFrameHtmlAttributes = array('width', 'height', 'frameborder', 'scrolling'); 441 foreach ($this->attributes as $attribute => $value) { 442 if (in_array($attribute, $iFrameHtmlAttributes)) { 443 $iFrameHtml .= ' ' . $attribute . '=' . $value; 444 } 445 } 446 $iFrameHtml .= ' srcdoc="' . htmlentities($iframeHtml) . '" ></iframe>';// 447 448 // Credits bar 449 $bar = '<div class="webcode-bar">'; 450 $bar .= '<div class="webcode-bar-item">' . PluginUtility::getUrl(self::TAG, "Rendered by Webcode",false) . '</div>'; 451 $bar .= '<div class="webcode-bar-item">' . $this->addJsFiddleButton($codes, $this->attributes) . '</div>'; 452 $bar .= '</div>'; 453 $renderer->doc .= '<div class="webcode">' . $iFrameHtml . $bar . '</div>'; 454 } 455 456 break; 457 } 458 459 return true; 460 } 461 return false; 462 } 463 464 /** 465 * @param array $codes the array containing the codes 466 * @param array $attributes the attributes of a call (for now the externalResources) 467 * @return string the HTML form code 468 * 469 * Specification, see http://doc.jsfiddle.net/api/post.html 470 */ 471 public function addJsFiddleButton($codes, $attributes) 472 { 473 474 $postURL = "https://jsfiddle.net/api/post/library/pure/"; //No Framework 475 476 $externalResources = array(); 477 if (array_key_exists(self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY, $attributes)) { 478 $externalResources = explode(",", $attributes[self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY]); 479 } 480 481 482 if ($this->useConsole) { 483 // If their is a console.log function, add the Firebug Lite support of JsFiddle 484 // Seems to work only with the Edge version of jQuery 485 // $postURL .= "edge/dependencies/Lite/"; 486 // The firebug logging is not working anymore because of 404 487 // Adding them here 488 $externalResources[] = 'The firebug resources for the console.log features'; 489 $externalResources[] = PluginUtility::getResourceBaseUrl() . '/firebug/firebug-lite.css'; 490 $externalResources[] = PluginUtility::getResourceBaseUrl() . '/firebug/firebug-lite-1.2.js'; 491 } 492 493 // The below code is to prevent this JsFiddle bug: https://github.com/jsfiddle/jsfiddle-issues/issues/726 494 // The order of the resources is not guaranteed 495 // We pass then the resources only if their is one resources 496 // Otherwise we pass them as a script element in the HTML. 497 if (count($externalResources) <= 1) { 498 $externalResourcesInput = '<input type="hidden" name="resources" value="' . implode(",", $externalResources) . '">'; 499 } else { 500 $codes['html'] .= "\n\n\n\n\n<!-- The resources -->\n"; 501 $codes['html'] .= "<!-- They have been added here because their order is not guarantee through the API. -->\n"; 502 $codes['html'] .= "<!-- See: https://github.com/jsfiddle/jsfiddle-issues/issues/726 -->\n"; 503 foreach ($externalResources as $externalResource) { 504 if ($externalResource != "") { 505 $extension = pathinfo($externalResource)['extension']; 506 switch ($extension) { 507 case "css": 508 $codes['html'] .= "<link href=\"" . $externalResource . "\" rel=\"stylesheet\">\n"; 509 break; 510 case "js": 511 $codes['html'] .= "<script src=\"" . $externalResource . "\"></script>\n"; 512 break; 513 default: 514 $codes['html'] .= "<!-- " . $externalResource . " -->\n"; 515 } 516 } 517 } 518 } 519 520 $jsCode = $codes['javascript']; 521 $jsPanel = 0; // language for the js specific panel (0 = JavaScript) 522 if (array_key_exists('babel', $codes)) { 523 $jsCode = $codes['babel']; 524 $jsPanel = 3; // 3 = Babel 525 } 526 527 // Title and description 528 global $ID; 529 $title = $attributes['name']; 530 $pageTitle = tpl_pagetitle($ID, true); 531 if (!$title) { 532 533 $title = "Code from " . $pageTitle; 534 } 535 $description = "Code from the page '" . $pageTitle . "' \n" . wl($ID, $absolute = true); 536 return '<form method="post" action="' . $postURL . '" target="_blank">' . 537 '<input type="hidden" name="title" value="' . htmlentities($title) . '">' . 538 '<input type="hidden" name="description" value="' . htmlentities($description) . '">' . 539 '<input type="hidden" name="css" value="' . htmlentities($codes['css']) . '">' . 540 '<input type="hidden" name="html" value="' . htmlentities("<!-- The HTML -->" . $codes['html']) . '">' . 541 '<input type="hidden" name="js" value="' . htmlentities($jsCode) . '">' . 542 '<input type="hidden" name="panel_js" value="' . htmlentities($jsPanel) . '">' . 543 '<input type="hidden" name="wrap" value="b">' . //javascript no wrap in body 544 $externalResourcesInput . 545 '<button>Try the code</button>' . 546 '</form>'; 547 548 } 549 550 /** 551 * @param $codes the array containing the codes 552 * @param $attributes the attributes of a call (for now the externalResources) 553 * @return string the HTML form code 554 */ 555 public function addCodePenButton($codes, $attributes) 556 { 557 // TODO 558 // http://blog.codepen.io/documentation/api/prefill/ 559 } 560 561 562} 563