1<?php 2/** 3 * Copyright (c) 2020. 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 15 16require_once(__DIR__ . '/Call.php'); 17 18use Doku_Handler; 19use dokuwiki\Extension\SyntaxPlugin; 20use dokuwiki\Parsing\Handler\CallWriter; 21use Exception; 22use RuntimeException; 23 24 25/** 26 * Class Tag 27 * @package ComboStrap 28 * This is class that have tree like function on tag level 29 * to match what's called a {@link Doku_Handler::$calls call} 30 * 31 * It's just a wrapper that adds tree functionality above the {@link CallStack} 32 * 33 * @deprecated Move to the {@link CallStack::createFromHandler()} 34 */ 35class Tag 36{ 37 38 const CANONICAL = "support"; 39 40 /** 41 * Invisible Content tag (ie p (p_open/p_close)) 42 * They are not seen in the content 43 * and are automatically generated 44 */ 45 const INVISIBLE_CONTENT_TAG = ["p"]; 46 47 /** 48 * The {@link Doku_Handler::$calls} or {@link CallWriter::$calls} 49 * @var 50 */ 51 private $calls; 52 53 /** 54 * The parsed attributes for the tag 55 * @var 56 */ 57 private $attributes; 58 /** 59 * The name of the tag 60 * @var 61 */ 62 private $name; 63 /** 64 * The lexer state 65 * @var 66 */ 67 private $state; 68 /** 69 * The position is the call stack 70 * @var int 71 */ 72 private $actualPosition; 73 /** 74 * @var Doku_Handler 75 */ 76 private $handler; 77 /** 78 * 79 * @var Call 80 * The tag call may be null if this is the actual tag 81 * in the handler process (ie not yet created in the stack) 82 */ 83 private $tagCall; 84 /** 85 * The maximum key number position of the stack 86 * @var int|string|null 87 */ 88 private $maxPosition; 89 /** 90 * @var string 91 */ 92 private $callStackType; 93 94 95 /** 96 * Token constructor 97 * A token represent a call of {@link \Doku_Handler} 98 * It can be seen as a the counter part of the HTML tag. 99 * 100 * It has a state of: 101 * * {@link DOKU_LEXER_ENTER} (open), 102 * * {@link DOKU_LEXER_UNMATCHED} (unmatched content), 103 * * {@link DOKU_LEXER_EXIT} (closed) 104 * 105 * @param $name 106 * @param $attributes 107 * @param $state 108 * @param Doku_Handler $handler - A reference to the dokuwiki handler 109 * @param null $position - The key (position) in the call stack of null if it's the HEAD tag (The tag that is created from the data of the {@link SyntaxPlugin::render()} 110 */ 111 public function __construct($name, $attributes, $state, &$handler, $position = null, $callStackType = null) 112 { 113 $this->name = $name; 114 $this->state = $state; 115 116 /** 117 * Callstack 118 */ 119 $writerCalls = &$handler->getCallWriter()->calls; 120 if ($position == null) { 121 /** 122 * A temporary Call stack is created in the writer 123 * for list, table, blockquote 124 */ 125 if (!empty($writerCalls)) { 126 $this->calls = &$writerCalls; 127 $this->callStackType = CallStack::CALLSTACK_WRITER; 128 } else { 129 $this->calls = &$handler->calls; 130 $this->callStackType = CallStack::CALLSTACK_MAIN; 131 } 132 } else { 133 if ($callStackType == null) { 134 LogUtility::msg("When the position is set, the callstack type should be given", LogUtility::LVL_MSG_ERROR); 135 } 136 $this->callStackType = $callStackType; 137 if ($callStackType == CallStack::CALLSTACK_MAIN) { 138 $this->calls = &$handler->calls; 139 } else { 140 $this->calls = &$writerCalls; 141 } 142 } 143 144 $this->handler = &$handler; 145 $this->maxPosition = ArrayUtility::array_key_last($this->calls); 146 if ($position !== null) { 147 $this->actualPosition = $position; 148 /** 149 * A shortcut access variable to the call of the tag 150 */ 151 if (isset($this->calls[$this->actualPosition])) { 152 $this->tagCall = $this->createCallObjectFromIndex($this->actualPosition); 153 $this->attributes = $this->tagCall->getAttributes(); 154 } 155 156 } else { 157 // The tag is not yet in the stack 158 159 // Get the last position 160 // We use get_last and not sizeof 161 // because some plugin may delete element of the stack 162 $this->actualPosition = ArrayUtility::array_key_last($this->calls) + 1; 163 if ($attributes == null) { 164 $this->attributes = array(); 165 } else { 166 $this->attributes = $attributes; 167 } 168 } 169 170 171 } 172 173 public static function createDocumentStartFromHandler(Doku_Handler &$handler) 174 { 175 return new Tag("document_start", [], DOKU_LEXER_ENTER, $handler, 0); 176 } 177 178 private function createCallObjectFromIndex($i) 179 { 180 if (isset($this->calls[$i])) { 181 return new Call($this->calls[$i]); 182 } else { 183 LogUtility::msg("There is no call at the index ($i)", LogUtility::LVL_MSG_ERROR); 184 return null; 185 } 186 187 } 188 189 190 /** 191 * The lexer state 192 * DOKU_LEXER_ENTER = 1 193 * DOKU_LEXER_MATCHED = 2 194 * DOKU_LEXER_UNMATCHED = 3 195 * DOKU_LEXER_EXIT = 4 196 * DOKU_LEXER_SPECIAL = 5 197 * @return mixed 198 */ 199 public function getState() 200 { 201 return $this->state; 202 } 203 204 /** 205 * From a call position to a tag 206 * @param $position - the position in the call stack (ie in the array) 207 * @return Tag 208 */ 209 public function createFromCall($position) 210 { 211 212 if (!isset($this->calls[$position])) { 213 LogUtility::msg("The index ($position) does not exist in the call stack, cannot create a call", LogUtility::LVL_MSG_ERROR); 214 return null; 215 } 216 217 $callArray = &$this->calls[$position]; 218 $call = new Call($callArray); 219 220 $attributes = $call->getAttributes(); 221 $name = $call->getTagName(); 222 $state = $call->getState(); 223 224 /** 225 * Getting attributes 226 * If we don't have already the attributes 227 * in the returned array of the handler, 228 * (ie the full HTML was given for instance) 229 * we parse the match again 230 */ 231 if ($attributes == null 232 && $call->getState() == DOKU_LEXER_ENTER 233 && $name != 'preformatted' // preformatted does not have any attributes 234 ) { 235 $match = $call->getCapturedContent(); 236 /** 237 * If this is not a combo element, we got no match 238 */ 239 if ($match != null) { 240 $attributes = PluginUtility::getTagAttributes($match); 241 } 242 } 243 244 return new Tag($name, $attributes, $state, $this->handler, $position, $this->callStackType); 245 } 246 247 public function isChildOf($tag) 248 { 249 $componentNode = $this->getParent(); 250 return $componentNode !== false ? $componentNode->getName() === $tag : false; 251 } 252 253 /** 254 * To determine if there is no content 255 * between the child and the parent 256 * @return bool 257 */ 258 public function hasSiblings() 259 { 260 if ($this->getPreviousSibling() === null) { 261 return false; 262 } else { 263 return true; 264 } 265 266 } 267 268 /** 269 * Return the parent node or false if root 270 * 271 * The node should not be in an exit node 272 * (If this is the case, use the function {@link Tag::getOpeningTag()} first 273 * 274 * @return bool|Tag 275 */ 276 public function getParent() 277 { 278 /** 279 * Case when we start from the exit state element 280 * We go first to the opening tag 281 * because the algorithm is level based. 282 */ 283 if ($this->state == DOKU_LEXER_EXIT) { 284 285 return $this->getOpeningTag()->getParent(); 286 287 } else { 288 289 /** 290 * Start to the actual call minus one 291 */ 292 $callStackPosition = $this->actualPosition - 1; 293 $treeLevel = 0; 294 295 /** 296 * We are in a parent when the tree level is negative 297 */ 298 while ($callStackPosition >= 0) { 299 300 $callStackPosition = $this->getPreviousPositionNonEmpty($callStackPosition); 301 if ($callStackPosition < 0) { 302 break; 303 } 304 305 /** 306 * Get the previous call 307 */ 308 $previousCallArray = $this->calls[$callStackPosition]; 309 $previousCall = new Call($previousCallArray); 310 $parentCallState = $previousCall->getState(); 311 312 /** 313 * Add 314 * would become a parent on its enter state 315 */ 316 switch ($parentCallState) { 317 case DOKU_LEXER_ENTER: 318 $treeLevel = $treeLevel - 1; 319 break; 320 case DOKU_LEXER_EXIT: 321 /** 322 * When the tag has a sibling with an exit tag 323 */ 324 $treeLevel = $treeLevel + 1; 325 break; 326 } 327 328 /** 329 * The breaking statement 330 */ 331 if ($treeLevel >= 0) { 332 $callStackPosition = $callStackPosition - 1; 333 unset($previousCallArray); 334 } else { 335 break; 336 } 337 338 339 } 340 if (isset($previousCallArray)) { 341 return $this->createFromCall($callStackPosition); 342 } else { 343 return false; 344 } 345 } 346 } 347 348 /** 349 * Return an attribute of the node or null if it does not exist 350 * @param string $name 351 * @return string the attribute value 352 */ 353 public function getAttribute($name) 354 { 355 if (isset($this->attributes)) { 356 return $this->attributes[$name]; 357 } else { 358 return null; 359 } 360 361 } 362 363 /** 364 * Return all attributes 365 * @return string[] the attributes 366 */ 367 public function getAttributes() 368 { 369 370 return $this->attributes; 371 372 } 373 374 /** 375 * @return mixed - the name of the element (ie the opening tag) 376 */ 377 public function getName() 378 { 379 if ($this->tagCall != null) { 380 return $this->tagCall->getTagName(); 381 } else { 382 return $this->name; 383 } 384 } 385 386 387 /** 388 * @return string - the type attribute of the opening tag 389 */ 390 public function getType() 391 { 392 393 if ($this->getState() == DOKU_LEXER_UNMATCHED) { 394 LogUtility::msg("The unmatched tag (" . $this->name . ") does not have any attributes. Get its parent if you want the type", LogUtility::LVL_MSG_ERROR); 395 return null; 396 } else { 397 return $this->getAttribute("type"); 398 } 399 } 400 401 /** 402 * @param $tag 403 * @return int 404 */ 405 public function isDescendantOf($tag) 406 { 407 408 for ($i = sizeof($this->calls) - 1; $i >= 0; $i--) { 409 if (isset($this->calls[$i])) { 410 $call = $this->createCallObjectFromIndex($i); 411 if ($call->getTagName() == "$tag") { 412 return true; 413 } 414 } 415 } 416 return false; 417 418 } 419 420 /** 421 * 422 * @return null|Tag - the sibling tag (in ascendant order) or null 423 */ 424 public function getPreviousSibling() 425 { 426 427 if ($this->actualPosition == 1) { 428 return null; 429 } 430 431 $counter = $this->actualPosition - 1; 432 $treeLevel = 0; 433 while ($counter > 0) { 434 435 $callArray = $this->calls[$counter]; 436 $call = new Call($callArray); 437 $state = $call->getState(); 438 439 /** 440 * Edge case 441 */ 442 if ($state == null) { 443 if ($call->getTagName() == "acronym") { 444 // Acronym does not have an enter/exit state 445 // this is a sibling 446 break; 447 } 448 } 449 450 /** 451 * Before the breaking condition 452 * to take the case when the first call is an exit 453 */ 454 switch ($state) { 455 case DOKU_LEXER_ENTER: 456 $treeLevel = $treeLevel - 1; 457 break; 458 case DOKU_LEXER_EXIT: 459 /** 460 * When the tag has a sibling with an exit tag 461 */ 462 $treeLevel = $treeLevel + 1; 463 break; 464 case DOKU_LEXER_UNMATCHED: 465 if (empty(trim($call->getCapturedContent()))) { 466 // An empty unmatched is not considered a sibling 467 // state = null will continue the loop 468 // we can't use a continue statement in a switch 469 $state = null; 470 } 471 break; 472 } 473 474 /* 475 * Breaking conditions 476 * If we get above or on the same level 477 */ 478 if ($treeLevel <= 0 479 && $state != null // eol state, strong close or empty tag 480 ) { 481 break; 482 } else { 483 $counter = $counter - 1; 484 unset($callArray); 485 } 486 487 } 488 /** 489 * Because we don't count tag without an state such as eol, 490 * the tree level may be negative which means 491 * that there is no sibling 492 */ 493 if ($treeLevel == 0) { 494 return $this->createFromCall($counter); 495 } 496 return null; 497 498 499 } 500 501 public function hasParent() 502 { 503 return $this->getParent() !== false; 504 } 505 506 507 /** 508 * @return bool|Tag 509 * @deprecated use {@link CallStack::moveToPreviousCorrespondingOpeningCall()} instead 510 * @date 2021-05-13 deprecation date 511 */ 512 public function getOpeningTag() 513 { 514 $descendantCounter = sizeof($this->calls) - 1; 515 while ($descendantCounter > 0) { 516 517 $previousCallArray = $this->calls[$descendantCounter]; 518 $previousCall = new Call($previousCallArray); 519 $parentTagName = $previousCall->getTagName(); 520 $state = $previousCall->getState(); 521 if ($state === DOKU_LEXER_ENTER && $parentTagName === $this->getName()) { 522 break; 523 } else { 524 $descendantCounter = $descendantCounter - 1; 525 unset($previousCallArray); 526 } 527 528 } 529 if (isset($previousCallArray)) { 530 return $this->createFromCall($descendantCounter); 531 } else { 532 return false; 533 } 534 } 535 536 /** 537 * @return bool 538 * @throws Exception - if the tag is not an exit tag 539 */ 540 public function hasDescendants() 541 { 542 543 if (sizeof($this->getDescendants()) > 0) { 544 return true; 545 } else { 546 return false; 547 } 548 } 549 550 551 /** 552 * 553 * @return Tag the first descendant that is not whitespace 554 * @deprecated use {@link CallStack::moveToFirstChildTag()} 555 * @date 2021-05-13 556 */ 557 public function getFirstMeaningFullDescendant() 558 { 559 $descendants = $this->getDescendants(); 560 $firstDescendant = $descendants[0]; 561 if ($firstDescendant->getState() == DOKU_LEXER_UNMATCHED && trim($firstDescendant->getContentRecursively()) == "") { 562 return $descendants[1]; 563 } else { 564 return $firstDescendant; 565 } 566 } 567 568 /** 569 * Descendant can only be run on enter tag 570 * @return Tag[] 571 */ 572 public function getDescendants() 573 { 574 575 if ($this->state != DOKU_LEXER_ENTER) { 576 throw new RuntimeException("Descendant should be called on enter tag. Get the opening tag first if you are in a exit tag"); 577 } 578 if (isset($this->actualPosition)) { 579 $index = $this->actualPosition + 1; 580 } else { 581 throw new RuntimeException("It seems that we are at the end of the stack because the position is not set"); 582 } 583 $descendants = array(); 584 while ($index <= sizeof($this->calls) - 1) { 585 586 $childCallArray = $this->calls[$index]; 587 $childCall = new Call($childCallArray); 588 $childTagName = $childCall->getTagName(); 589 $state = $childCall->getState(); 590 591 /** 592 * We break when got to the exit tag 593 */ 594 if ($state === DOKU_LEXER_EXIT && $childTagName === $this->getName()) { 595 break; 596 } else { 597 /** 598 * We don't take the end of line 599 */ 600 if ($childCallArray[0] != "eol") { 601 /** 602 * We don't take text 603 */ 604 //if ($state!=DOKU_LEXER_UNMATCHED) { 605 $descendants[] = $this->createFromCall($index); 606 //} 607 } 608 /** 609 * Close 610 */ 611 $index = $index + 1; 612 unset($childCallArray); 613 } 614 615 } 616 return $descendants; 617 } 618 619 /** 620 * @param string $requiredTagName 621 * @return Tag|null 622 */ 623 public function getDescendant($requiredTagName) 624 { 625 $tags = $this->getDescendants(); 626 foreach ($tags as $tag) { 627 $currentTagName = $tag->getName(); 628 if ($currentTagName === $requiredTagName) { 629 $currentTagState = $tag->getState(); 630 /** 631 * No unmatched tag 632 */ 633 if ( 634 $currentTagState === DOKU_LEXER_ENTER 635 || $currentTagState === DOKU_LEXER_MATCHED 636 || $currentTagState === DOKU_LEXER_SPECIAL 637 ) { 638 return $tag; 639 } else { 640 /** 641 * Dokuwiki special match does not have any 642 * state 643 */ 644 if ($currentTagName == "internalmedia") { 645 return $tag; 646 } 647 } 648 } 649 } 650 return null; 651 } 652 653 /** 654 * @deprecated use {@link CallStack::deleteCall} instead 655 */ 656 public function deleteCall() 657 { 658 /** 659 * The current call in an handle method cannot be deleted 660 * because it does not exist yet 661 */ 662 if (!$this->isCurrent()) { 663 unset($this->calls[$this->actualPosition]); 664 } else { 665 LogUtility::msg("Internal error: The current call cannot be deleted", LogUtility::LVL_MSG_WARNING, self::CANONICAL); 666 } 667 } 668 669 /** 670 * Returned the matched content for this tag 671 */ 672 public function getMatchedContent() 673 { 674 if ($this->tagCall != null) { 675 676 return $this->tagCall->getCapturedContent(); 677 } else { 678 return null; 679 } 680 } 681 682 /** 683 * 684 * @return array|mixed - the data 685 */ 686 public function getData() 687 { 688 if ($this->tagCall != null) { 689 return $this->tagCall->getPluginData(); 690 } else { 691 return array(); 692 } 693 } 694 695 /** 696 * 697 * @return array|mixed - the context (generally a tag name) 698 */ 699 public function getContext() 700 { 701 if ($this->tagCall != null) { 702 $data = $this->tagCall->getPluginData(); 703 return $data[PluginUtility::CONTEXT]; 704 } else { 705 return array(); 706 } 707 } 708 709 710 /** 711 * Return the content of a tag (the string between this tag) 712 * This function is generally called after a function that goes up on the stack 713 * such as {@link getDescendant} 714 * @return string 715 */ 716 public function getContentRecursively() 717 { 718 $content = ""; 719 $state = $this->getState(); 720 switch ($state) { 721 case DOKU_LEXER_ENTER: 722 $index = $this->actualPosition + 1; 723 while ($index <= sizeof($this->calls) - 1) { 724 725 $currentCallArray = $this->calls[$index]; 726 $currentCall = new Call($currentCallArray); 727 if ( 728 $currentCall->getTagName() == $this->getName() 729 && 730 $currentCall->getState() == DOKU_LEXER_EXIT 731 ) { 732 break; 733 } else { 734 $content .= $currentCall->getCapturedContent(); 735 $index++; 736 } 737 } 738 break; 739 case DOKU_LEXER_UNMATCHED: 740 default: 741 $content = $this->getContent(); 742 break; 743 } 744 745 746 return $content; 747 748 } 749 750 /** 751 * Return true if the tag is the first sibling 752 * 753 * 754 * @return boolean - true if this is the first sibling 755 */ 756 public function isFirstMeaningFullSibling() 757 { 758 $sibling = $this->getPreviousSibling(); 759 if ($sibling == null) { 760 return true; 761 } else { 762 /** Whitespace string */ 763 if ($sibling->getState() == DOKU_LEXER_UNMATCHED && trim($sibling->getContentRecursively()) == "") { 764 $sibling = $sibling->getPreviousSibling(); 765 } 766 if ($sibling == null) { 767 return true; 768 } else { 769 return false; 770 } 771 } 772 773 } 774 775 /** 776 * @param $key 777 * @param $value 778 * @return $this 779 */ 780 public function addAttribute($key, $value) 781 { 782 $call = $this->createCallObjectFromIndex($this->actualPosition); 783 $call->addAttribute($key, $value); 784 return $this; 785 } 786 787 /** 788 * @param $value 789 * @return $this 790 */ 791 public function setContext($value) 792 { 793 $call = $this->createCallObjectFromIndex($this->actualPosition); 794 $call->setContext($value); 795 return $this; 796 } 797 798 /** 799 * @param $key 800 * @param $value 801 * @return $this 802 */ 803 public function setAttribute($key, $value) 804 { 805 $call = $this->createCallObjectFromIndex($this->actualPosition); 806 $call->addAttribute($key,$value); 807 return $this; 808 } 809 810 /** 811 * @param $key 812 * @return $this 813 */ 814 public function unsetAttribute($key) 815 { 816 unset($this->calls[$this->actualPosition][1][1][PluginUtility::ATTRIBUTES][$key]); 817 return $this; 818 } 819 820 /** 821 * Add a class to an element 822 * @param $value 823 * @return $this 824 */ 825 public function addClass($value) 826 { 827 $call = $this->createCallObjectFromIndex($this->actualPosition); 828 $classValue = $call->getAttribute("class"); 829 830 if (empty($classValue)) { 831 $call->addAttribute("class", $value); 832 } else { 833 $call->addAttribute("class", $classValue . " " . $value); 834 } 835 return $this; 836 837 } 838 839 /** 840 * @param $value 841 * @return $this 842 */ 843 public function setType($value) 844 { 845 $call = $this->createCallObjectFromIndex($this->actualPosition); 846 $call->addAttribute(TagAttributes::TYPE_KEY, $value); 847 return $this; 848 } 849 850 public function getActualPosition() 851 { 852 return $this->actualPosition; 853 } 854 855 public function getContent() 856 { 857 if ($this->tagCall != null) { 858 return $this->tagCall->getCapturedContent(); 859 } else { 860 return null; 861 } 862 } 863 864 public function getDocumentStartTag() 865 { 866 if (sizeof($this->calls) > 0) { 867 return $this->createFromCall(0); 868 } else { 869 throw new RuntimeException("The stack is empty, there is no root tag"); 870 } 871 } 872 873 /** 874 * The current call is the call being created 875 * in a {@link SyntaxPlugin::handle()} 876 * It does not exist yet in the call stack 877 * and cannot be deleted 878 * @return bool 879 */ 880 private function isCurrent() 881 { 882 return $this->actualPosition == sizeof($this->calls); 883 } 884 885 public function removeAttributes() 886 { 887 if (!$this->isCurrent()) { 888 $call = $this->createCallObjectFromIndex($this->actualPosition); 889 $call->removeAttributes(); 890 } else { 891 LogUtility::msg("Internal error: This is not logic to remove the attributes of the current node because it's not yet created, not yet in the stack. Don't add them to the constructor signature.", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 892 } 893 } 894 895 public function getNextOpeningTag($tagName) 896 { 897 $position = $this->actualPosition; 898 while ($this->toNextPositionNonEmpty($position)) { 899 900 $call = new Call($this->handler->calls[$position]); 901 if ($call->getTagName() == $tagName && $call->getState() == DOKU_LEXER_ENTER) { 902 return $this->createFromCall($position); 903 } 904 905 } 906 return null; 907 } 908 909 /** 910 * 911 * If element were deleted, 912 * the previous calls may be empty 913 * make sure that we got something 914 * 915 * @param $callStackPosition 916 * @return int|mixed 917 */ 918 public function getPreviousPositionNonEmpty($callStackPosition) 919 { 920 921 while (!isset($this->calls[$callStackPosition]) && $callStackPosition > 0) { 922 $callStackPosition = $callStackPosition - 1; 923 } 924 return $callStackPosition; 925 } 926 927 /** 928 * 929 * Increment the callStackPosition to a non-empty call stack position 930 * 931 * Array index are sequence number that may be deleted. We get then empty gap. 932 * This function increments the pointer and return false if the end of the stack was reached 933 * 934 * @param $callStackPointer 935 * @return true|false - If this is the end of the stack, return false, otherwise return true 936 */ 937 public function toNextPositionNonEmpty(&$callStackPointer) 938 { 939 $callStackPointer = $callStackPointer + 1; 940 while (!isset($this->calls[$callStackPointer]) && $callStackPointer < $this->maxPosition) { 941 $callStackPointer = $callStackPointer + 1; 942 } 943 if (!isset($this->calls[$callStackPointer])) { 944 return false; 945 } else { 946 return true; 947 } 948 } 949 950 public function getNextClosingTag($tagName) 951 { 952 $position = $this->actualPosition; 953 while ($this->toNextPositionNonEmpty($position)) { 954 955 $call = new Call($this->handler->calls[$position]); 956 if ($call->getTagName() == $tagName && $call->getState() == DOKU_LEXER_EXIT) { 957 return $this->createFromCall($position); 958 } 959 960 } 961 return null; 962 } 963 964 /** 965 * @return Tag|null the next children or null - the tag should be an enter tag 966 */ 967 public function getNextChild() 968 { 969 if ($this->getState() !== DOKU_LEXER_ENTER) { 970 // The tag ($this) is not an enter tag and has therefore no children." 971 return null; 972 } 973 $position = $this->actualPosition; 974 $result = $this->toNextPositionNonEmpty($position); 975 if ($result === false) { 976 return null; 977 } else { 978 return $this->createFromCall($position); 979 } 980 981 } 982 983 /** 984 * Children are tag 985 * @return array|null 986 */ 987 public function getChildren() 988 { 989 if ($this->getState() !== DOKU_LEXER_ENTER) { 990 // The tag ($this) is not an enter tag and has therefore no children." 991 return null; 992 } 993 $children = []; 994 $level = 0; 995 $position = $this->actualPosition; 996 while ($this->toNextPositionNonEmpty($position)) { 997 998 $call = new Call($this->handler->calls[$position]); 999 $state = $call->getState(); 1000 switch ($state) { 1001 case DOKU_LEXER_ENTER: 1002 $level += 1; 1003 break; 1004 case DOKU_LEXER_EXIT: 1005 $level -= 1; 1006 break; 1007 } 1008 if ($level < 0) { 1009 break; 1010 } else { 1011 if ($state == DOKU_LEXER_ENTER && !in_array($call->getTagName(), Tag::INVISIBLE_CONTENT_TAG)) { 1012 $children[] = $this->createFromCall($position); 1013 } 1014 } 1015 } 1016 return $children; 1017 } 1018 1019 public function setAttributeIfNotPresent($key, $value) 1020 { 1021 if (!isset($this->calls[$this->actualPosition][1][1][PluginUtility::ATTRIBUTES][$key])) { 1022 $this->setAttribute($key, $value); 1023 } 1024 1025 } 1026 1027 public function getNextTag() 1028 { 1029 $position = $this->actualPosition; 1030 $this->toNextPositionNonEmpty($position); 1031 return $this->createFromCall($position); 1032 } 1033 1034 1035} 1036