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