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 13namespace ComboStrap; 14 15use dokuwiki\Extension\SyntaxPlugin; 16 17 18/** 19 * Class Call 20 * @package ComboStrap 21 * 22 * A wrapper around what's called a call 23 * which is an array of information such 24 * the mode, the data 25 * 26 * The {@link CallStack} is the only syntax representation that 27 * is available in DokuWiki 28 */ 29class Call 30{ 31 32 const INLINE_DISPLAY = "inline"; 33 const BlOCK_DISPLAY = "block"; 34 /** 35 * List of inline components 36 * Used to manage white space before an unmatched string. 37 * The syntax tree of Dokuwiki (ie {@link \Doku_Handler::$calls}) 38 * has only data and no class, for now, we create this 39 * lists manually because this is a hassle to retrieve this information from {@link \DokuWiki_Syntax_Plugin::getType()} 40 */ 41 const INLINE_DOKUWIKI_COMPONENTS = array( 42 /** 43 * Formatting https://www.dokuwiki.org/devel:syntax_plugins#syntax_types 44 * Comes from the {@link \dokuwiki\Parsing\ParserMode\Formatting} class 45 */ 46 "cdata", 47 "unformatted", // ie %% or nowiki 48 "doublequoteclosing", // https://www.dokuwiki.org/config:typography / https://www.dokuwiki.org/wiki:syntax#text_to_html_conversions 49 "doublequoteopening", 50 "singlequoteopening", 51 "singlequoteclosing", 52 "multiplyentity", 53 "apostrophe", 54 "strong", 55 "emphasis", 56 "emphasis_open", 57 "emphasis_close", 58 "underline", 59 "monospace", 60 "subscript", 61 "superscript", 62 "deleted", 63 "footnote", 64 /** 65 * Others 66 */ 67 "acronym", 68 "strong_close", 69 "strong_open", 70 "monospace_open", 71 "monospace_close", 72 "doublequoteopening", // ie the character " in "The" 73 "entity", // for instance `...` are transformed in character 74 "linebreak", 75 "externallink", 76 "internallink", 77 MediaLink::INTERNAL_MEDIA_CALL_NAME, 78 MediaLink::EXTERNAL_MEDIA_CALL_NAME, 79 /** 80 * The inline of combo 81 * TODO: Should be deleted when {@link PluginUtility::renderUnmatched()} is not using the array anymore 82 * but is using {@link Call::getDisplay()} instead or any other rewrite 83 */ 84 \syntax_plugin_combo_link::TAG, 85 \syntax_plugin_combo_icon::TAG, 86 \syntax_plugin_combo_inote::TAG, 87 \syntax_plugin_combo_button::TAG, 88 \syntax_plugin_combo_tooltip::TAG, 89 \syntax_plugin_combo_pipeline::TAG, 90 ); 91 92 93 const BLOCK_MARKUP_DOKUWIKI_COMPONENTS = array( 94 "listu_open", // ul 95 "listu_close", 96 "listitem_open", //li 97 "listitem_close", 98 "listcontent_open", // after li ??? 99 "listcontent_close", 100 "table_open", 101 "table_close", 102 ); 103 104 private $call; 105 106 /** 107 * The key identifier in the {@link CallStack} 108 * @var mixed|string 109 */ 110 private $key; 111 112 /** 113 * Call constructor. 114 * @param $call - the instruction array (ie called a call) 115 */ 116 public function __construct(&$call, $key = "") 117 { 118 $this->call = &$call; 119 $this->key = $key; 120 } 121 122 /** 123 * Insert a tag above 124 * @param $tagName 125 * @param $state 126 * @param $attribute 127 * @param $context 128 * @param string $content 129 * @return Call - a call 130 */ 131 public static function createComboCall($tagName, $state, $attribute = array(), $context = null, $content = '', $payload = null) 132 { 133 $data = array( 134 PluginUtility::ATTRIBUTES => $attribute, 135 PluginUtility::CONTEXT => $context, 136 PluginUtility::STATE => $state 137 ); 138 if ($payload != null) { 139 $data[PluginUtility::PAYLOAD] = $payload; 140 } 141 $positionInText = null; 142 143 $call = [ 144 "plugin", 145 array( 146 PluginUtility::getComponentName($tagName), 147 $data, 148 $state, 149 $content 150 ), 151 $positionInText 152 ]; 153 return new Call($call); 154 } 155 156 /** 157 * Insert a dokuwiki call 158 * @param $callName 159 * @param $array 160 * @param $positionInText 161 * @return Call 162 */ 163 public static function createNativeCall($callName, $array = [], $positionInText = null) 164 { 165 $call = [ 166 $callName, 167 $array, 168 $positionInText 169 ]; 170 return new Call($call); 171 } 172 173 public static function createFromInstruction($instruction) 174 { 175 return new Call($instruction); 176 } 177 178 179 /** 180 * 181 * Return the tag name from a call array 182 * 183 * This is not the logical tag. 184 * This is much more what's called: 185 * * the component name for a plugin 186 * * or the handler name for dokuwiki 187 * 188 * For a plugin, this is equivalent 189 * to the {@link SyntaxPlugin::getPluginComponent()} 190 * 191 * This is not the fully qualified component name: 192 * * with the plugin as prefix such as in {@link Call::getComponentName()} 193 * * or with the `open` and `close` prefix such as `p_close` ... 194 * 195 * @return mixed|string 196 */ 197 public function getTagName() 198 { 199 $mode = $this->call[0]; 200 if ($mode != "plugin") { 201 202 /** 203 * This is a standard dokuwiki node 204 */ 205 $dokuWikiNodeName = $this->call[0]; 206 207 /** 208 * The dokwuiki node name has also the open and close notion 209 * We delete this is not in the doc and therefore not logical 210 */ 211 $tagName = str_replace("_close", "", $dokuWikiNodeName); 212 $tagName = str_replace("_open", "", $tagName); 213 214 } else { 215 216 /** 217 * This is a plugin node 218 */ 219 $pluginDokuData = $this->call[1]; 220 $component = $pluginDokuData[0]; 221 if (!is_array($component)) { 222 /** 223 * Tag name from class 224 */ 225 $componentNames = explode("_", $component); 226 /** 227 * To take care of 228 * PHP Warning: sizeof(): Parameter must be an array or an object that implements Countable 229 * in lib/plugins/combo/class/Tag.php on line 314 230 */ 231 if (is_array($componentNames)) { 232 $tagName = $componentNames[sizeof($componentNames) - 1]; 233 } else { 234 $tagName = $component; 235 } 236 } else { 237 // To resolve: explode() expects parameter 2 to be string, array given 238 LogUtility::msg("The call (" . print_r($this->call, true) . ") has an array and not a string as component (" . print_r($component, true) . "). Page: " . PluginUtility::getPageId(), LogUtility::LVL_MSG_ERROR); 239 $tagName = ""; 240 } 241 242 243 } 244 return $tagName; 245 246 } 247 248 249 /** 250 * The parser state 251 * @return mixed 252 * May be null (example eol, internallink, ...) 253 */ 254 public function getState() 255 { 256 $mode = $this->call[0]; 257 if ($mode != "plugin") { 258 259 /** 260 * There is no state because this is a standard 261 * dokuwiki syntax found in {@link \Doku_Renderer_xhtml} 262 * check if this is not a `...._close` or `...._open` 263 * to derive the state 264 */ 265 $mode = $this->call[0]; 266 $lastPositionSepName = strrpos($mode, "_"); 267 $closeOrOpen = substr($mode, $lastPositionSepName + 1); 268 switch ($closeOrOpen) { 269 case "open": 270 return DOKU_LEXER_ENTER; 271 case "close": 272 return DOKU_LEXER_EXIT; 273 default: 274 return null; 275 } 276 277 } else { 278 // Plugin 279 $returnedArray = $this->call[1]; 280 if (array_key_exists(2, $returnedArray)) { 281 return $returnedArray[2]; 282 } else { 283 return null; 284 } 285 } 286 } 287 288 /** 289 * @return mixed the data returned from the {@link DokuWiki_Syntax_Plugin::handle} (ie attributes, payload, ...) 290 */ 291 public function &getPluginData() 292 { 293 return $this->call[1][1]; 294 } 295 296 /** 297 * @return mixed the matched content from the {@link DokuWiki_Syntax_Plugin::handle} 298 */ 299 public function getCapturedContent() 300 { 301 $caller = $this->call[0]; 302 switch ($caller) { 303 case "plugin": 304 return $this->call[1][3]; 305 case "internallink": 306 return '[[' . $this->call[1][0] . '|' . $this->call[1][1] . ']]'; 307 case "eol": 308 return DOKU_LF; 309 case "header": 310 case "cdata": 311 return $this->call[1][0]; 312 default: 313 if (isset($this->call[1][0]) && is_string($this->call[1][0])) { 314 return $this->call[1][0]; 315 } else { 316 return ""; 317 } 318 } 319 } 320 321 322 public function getAttributes() 323 { 324 325 $tagName = $this->getTagName(); 326 switch ($tagName) { 327 case MediaLink::INTERNAL_MEDIA_CALL_NAME: 328 return $this->call[1]; 329 default: 330 $data = $this->getPluginData(); 331 if (isset($data[PluginUtility::ATTRIBUTES])) { 332 return $data[PluginUtility::ATTRIBUTES]; 333 } else { 334 return null; 335 } 336 } 337 } 338 339 public function removeAttributes() 340 { 341 342 $data = &$this->getPluginData(); 343 if (isset($data[PluginUtility::ATTRIBUTES])) { 344 unset($data[PluginUtility::ATTRIBUTES]); 345 } 346 347 } 348 349 public function updateToPluginComponent($component, $state, $attributes = array()) 350 { 351 if ($this->call[0] == "plugin") { 352 $match = $this->call[1][3]; 353 } else { 354 $this->call[0] = "plugin"; 355 $match = ""; 356 } 357 $this->call[1] = array( 358 0 => $component, 359 1 => array( 360 PluginUtility::ATTRIBUTES => $attributes, 361 PluginUtility::STATE => $state, 362 ), 363 2 => $state, 364 3 => $match 365 ); 366 367 } 368 369 public function getDisplay() 370 { 371 if ($this->getState() == DOKU_LEXER_UNMATCHED) { 372 /** 373 * Unmatched are content (ie text node in XML/HTML) and have 374 * no display 375 */ 376 return Call::INLINE_DISPLAY; 377 } else { 378 $mode = $this->call[0]; 379 if ($mode == "plugin") { 380 global $DOKU_PLUGINS; 381 $component = $this->getComponentName(); 382 /** 383 * @var SyntaxPlugin $syntaxPlugin 384 */ 385 $syntaxPlugin = $DOKU_PLUGINS['syntax'][$component]; 386 $pType = $syntaxPlugin->getPType(); 387 switch ($pType) { 388 case "normal": 389 return Call::INLINE_DISPLAY; 390 case "block": 391 case "stack": 392 return Call::BlOCK_DISPLAY; 393 default: 394 LogUtility::msg("The ptype (" . $pType . ") is unknown."); 395 return null; 396 } 397 } else { 398 if ($mode == "eol") { 399 /** 400 * Control character 401 * We return it as it's used in the 402 * {@link \syntax_plugin_combo_para::fromEolToParagraphUntilEndOfStack()} 403 * to create the paragraph 404 * This is not a block, nor an inline 405 */ 406 return $mode; 407 } 408 409 if (in_array($mode, self::INLINE_DOKUWIKI_COMPONENTS)) { 410 return Call::INLINE_DISPLAY; 411 } 412 413 if (in_array($mode, self::BLOCK_MARKUP_DOKUWIKI_COMPONENTS)) { 414 return Call::BlOCK_DISPLAY; 415 } 416 417 LogUtility::msg("The display of the call with the mode " . $mode . " is unknown"); 418 return null; 419 420 421 } 422 } 423 424 } 425 426 /** 427 * Same as {@link Call::getTagName()} 428 * but fully qualified 429 * @return string 430 */ 431 public function getComponentName() 432 { 433 $mode = $this->call[0]; 434 if ($mode == "plugin") { 435 $pluginDokuData = $this->call[1]; 436 return $pluginDokuData[0]; 437 } else { 438 return $mode; 439 } 440 } 441 442 public function updateEolToSpace() 443 { 444 $mode = $this->call[0]; 445 if ($mode != "eol") { 446 LogUtility::msg("You can't update a " . $mode . " to a space. It should be a eol", LogUtility::LVL_MSG_WARNING, "support"); 447 } else { 448 $this->call[0] = "cdata"; 449 $this->call[1] = array( 450 0 => " " 451 ); 452 } 453 454 } 455 456 public function addAttribute($key, $value) 457 { 458 $mode = $this->call[0]; 459 if ($mode == "plugin") { 460 $this->call[1][1][PluginUtility::ATTRIBUTES][$key] = $value; 461 } else { 462 LogUtility::msg("You can't add an attribute to the non plugin call mode (" . $mode . ")", LogUtility::LVL_MSG_WARNING, "support"); 463 } 464 } 465 466 public function getContext() 467 { 468 $mode = $this->call[0]; 469 if ($mode == "plugin") { 470 return $this->call[1][1][PluginUtility::CONTEXT]; 471 } else { 472 LogUtility::msg("You can't ask for a context from a non plugin call mode (" . $mode . ")", LogUtility::LVL_MSG_WARNING, "support"); 473 return null; 474 } 475 } 476 477 /** 478 * 479 * @return array 480 */ 481 public function toCallArray() 482 { 483 return $this->call; 484 } 485 486 public function __toString() 487 { 488 $name = $this->key; 489 if (!empty($name)) { 490 $name .= " - "; 491 } 492 $name .= $this->getTagName(); 493 return $name; 494 } 495 496 public function getType() 497 { 498 if ($this->getState() == DOKU_LEXER_UNMATCHED) { 499 LogUtility::msg("The unmatched tag ($this) does not have any attributes. Get its parent if you want the type", LogUtility::LVL_MSG_ERROR); 500 return null; 501 } else { 502 /** 503 * don't use {@link Call::getAttribute()} to get the type 504 * as this function stack also depends on 505 * this function {@link Call::getType()} 506 * to return the value 507 * Ie: if this is a boolean attribute without specified type 508 * if the boolean value is in the type, we return it 509 */ 510 return $this->call[1][1][PluginUtility::ATTRIBUTES][TagAttributes::TYPE_KEY]; 511 } 512 } 513 514 /** 515 * @param $key 516 * @param null $default 517 * @return string|null 518 */ 519 public function getAttribute($key, $default = null) 520 { 521 $attributes = $this->getAttributes(); 522 if (isset($attributes[$key])) { 523 return $attributes[$key]; 524 } else { 525 // boolean attribute 526 if ($this->getType() == $key) { 527 return true; 528 } else { 529 return $default; 530 } 531 } 532 } 533 534 public function getPayload() 535 { 536 $mode = $this->call[0]; 537 if ($mode == "plugin") { 538 return $this->call[1][1][PluginUtility::PAYLOAD]; 539 } else { 540 LogUtility::msg("You can't ask for a payload from a non plugin call mode (" . $mode . ").", LogUtility::LVL_MSG_WARNING, "support"); 541 return null; 542 } 543 } 544 545 public function setContext($value) 546 { 547 $this->call[1][1][PluginUtility::CONTEXT] = $value; 548 return $this; 549 } 550 551 public function hasAttribute($attributeName) 552 { 553 $attributes = $this->getAttributes(); 554 if (isset($attributes[$attributeName])) { 555 return true; 556 } else { 557 if ($this->getType() == $attributeName) { 558 return true; 559 } else { 560 return false; 561 } 562 } 563 } 564 565 public function isPluginCall() 566 { 567 return $this->call[0] === "plugin"; 568 } 569 570 /** 571 * @return mixed|string the position (ie key) in the array 572 */ 573 public function getKey() 574 { 575 return $this->key; 576 } 577 578 public function &getCall() 579 { 580 return $this->call; 581 } 582 583 public function setState($state) 584 { 585 if ($this->call[0] == "plugin") { 586 // for dokuwiki 587 $this->call[1][2] = $state; 588 // for the combo plugin if any 589 if (isset($this->call[1][1][PluginUtility::STATE])) { 590 $this->call[1][1][PluginUtility::STATE] = $state; 591 } 592 } else { 593 LogUtility::msg("This modification of state is not yet supported for a native call"); 594 } 595 } 596 597 598 /** 599 * Return the position of the first matched character in the text file 600 * @return mixed 601 */ 602 public function getFirstMatchedCharacterPosition() 603 { 604 605 return $this->call[2]; 606 607 } 608 609 /** 610 * Return the position of the last matched character in the text file 611 * 612 * This is the {@link Call::getFirstMatchedCharacterPosition()} 613 * plus the length of the {@link Call::getCapturedContent()} 614 * matched content 615 * @return int|mixed 616 */ 617 public function getLastMatchedCharacterPosition() 618 { 619 return $this->getFirstMatchedCharacterPosition() + strlen($this->getCapturedContent()); 620 } 621 622 /** 623 * @param $value string the class string to add 624 * @return Call 625 */ 626 public function addClassName($value) 627 { 628 $class = $this->getAttribute("class"); 629 if ($class != null) { 630 $value = "$class $value"; 631 } 632 $this->addAttribute("class", $value); 633 return $this; 634 635 } 636 637 /** 638 * @param $key 639 * @return mixed|null - the delete value of null if not found 640 */ 641 public function removeAttribute($key) 642 { 643 644 $data = &$this->getPluginData(); 645 if (isset($data[PluginUtility::ATTRIBUTES][$key])) { 646 $value = $data[PluginUtility::ATTRIBUTES][$key]; 647 unset($data[PluginUtility::ATTRIBUTES][$key]); 648 return $value; 649 } else { 650 // boolean attribute as first attribute 651 if ($this->getType() == $key) { 652 unset($data[PluginUtility::ATTRIBUTES][TagAttributes::TYPE_KEY]); 653 return true; 654 } 655 return null; 656 } 657 658 } 659 660 public function setPayload($text) 661 { 662 if ($this->isPluginCall()) { 663 $this->call[1][1][PluginUtility::PAYLOAD] = $text; 664 } else { 665 LogUtility::msg("Setting the payload for a non-native call ($this) is not yet implemented"); 666 } 667 } 668 669 /** 670 * @return bool true if the call is a text call (same as dom text node) 671 */ 672 public function isTextCall() 673 { 674 return ( 675 $this->getState() == DOKU_LEXER_UNMATCHED || 676 $this->getTagName() == "cdata" || 677 $this->getTagName() == "acronym" 678 ); 679 } 680 681 public function setType($type) 682 { 683 if ($this->isPluginCall()) { 684 $this->call[1][1][PluginUtility::ATTRIBUTES][TagAttributes::TYPE_KEY] = $type; 685 } else { 686 LogUtility::msg("This is not a plugin call ($this), you can't set the type"); 687 } 688 } 689 690 public function addCssStyle($key, $value) 691 { 692 $style = $this->getAttribute("style"); 693 $cssValue = "$key:$value"; 694 if ($style != null) { 695 $cssValue = "$style; $cssValue"; 696 } 697 $this->addAttribute("style", $cssValue); 698 } 699 700 public function setSyntaxComponentFromTag($tag) 701 { 702 703 if ($this->isPluginCall()) { 704 $this->call[1][0] = PluginUtility::getComponentName($tag); 705 } else { 706 LogUtility::msg("The call ($this) is a native call and we don't support yet the modification of the component to ($tag)"); 707 } 708 } 709 710 /** 711 * @param Page $page 712 * @return Call 713 */ 714 public function render(Page $page) 715 { 716 return $this->renderFromData(TemplateUtility::getMetadataDataFromPage($page)); 717 } 718 719 public function renderFromData(array $array) 720 { 721 $state = $this->getState(); 722 if ($state == DOKU_LEXER_UNMATCHED) { 723 if ($this->isPluginCall()) { 724 $payload = $this->getPayload(); 725 if (!empty($payload)) { 726 $this->setPayload(TemplateUtility::renderStringTemplateFromDataArray($payload, $array)); 727 } 728 } 729 } else { 730 $tagName = $this->getTagName(); 731 switch ($tagName) { 732 case "eol": 733 break; 734 case "cdata": 735 $payload = $this->getCapturedContent(); 736 $this->setCapturedContent(TemplateUtility::renderStringTemplateFromDataArray($payload, $array)); 737 break; 738 case \syntax_plugin_combo_pipeline::TAG: 739 $pageTemplate = PluginUtility::getTagContent($this->getCapturedContent()); 740 $script = TemplateUtility::renderStringTemplateFromDataArray($pageTemplate, $array); 741 $string = PipelineUtility::execute($script); 742 $this->setPayload($string); 743 break; 744 case \syntax_plugin_combo_link::TAG: 745 switch ($this->getState()) { 746 case DOKU_LEXER_ENTER: 747 $ref = $this->getAttribute("ref"); 748 $this->addAttribute("ref", TemplateUtility::renderStringTemplateFromDataArray($ref, $array)); 749 break; 750 } 751 break; 752 } 753 } 754 return $this; 755 } 756 757 public function setCapturedContent($content) 758 { 759 $tagName = $this->getTagName(); 760 switch ($tagName) { 761 case "cdata": 762 $this->call[1][0] = $content; 763 break; 764 default: 765 LogUtility::msg("Setting the captured content on a call for the tag ($tagName) is not yet implemented", LogUtility::LVL_MSG_ERROR); 766 } 767 } 768 769 770} 771