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