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