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 15 16use Doku_Handler; 17use dokuwiki\Extension\SyntaxPlugin; 18use dokuwiki\Parsing\Parser; 19use syntax_plugin_combo_media; 20 21/** 22 * Class CallStack 23 * @package ComboStrap 24 * 25 * This is a class that manipulate the call stack. 26 * 27 * A call stack is composed of call (ie array) 28 * A tag is a call that has a state {@link DOKU_LEXER_ENTER} or {@link DOKU_LEXER_SPECIAL} 29 * An opening call is a call with the {@link DOKU_LEXER_ENTER} 30 * An closing call is a call with the {@link DOKU_LEXER_EXIT} 31 * 32 * You can move on the stack with the function: 33 * * {@link CallStack::next()} 34 * * {@link CallStack::previous()} 35 * * `MoveTo`. example: {@link CallStack::moveToPreviousCorrespondingOpeningCall()} 36 * 37 * 38 */ 39class CallStack 40{ 41 42 const TAG_STATE = [DOKU_LEXER_SPECIAL, DOKU_LEXER_ENTER]; 43 44 const CANONICAL = "support"; 45 46 /** 47 * The type of callstack 48 * * main is the normal 49 * * writer is when there is a temporary call stack from the writer 50 */ 51 const CALLSTACK_WRITER = "writer"; 52 const CALLSTACK_MAIN = "main"; 53 public const MESSAGE_PREFIX_CALLSTACK_NOT_CONFORM = "Your DokuWiki installation is too old or a writer plugin does not conform"; 54 55 private $handler; 56 57 /** 58 * The max key of the calls 59 * @var int|null 60 */ 61 private $maxIndex = 0; 62 63 /** 64 * @var array the call stack 65 */ 66 private $callStack = []; 67 68 /** 69 * A pointer to keep the information 70 * if we have gone to far in the stack 71 * (because you lost the fact that you are outside 72 * the boundary, ie you can do a {@link \prev}` after that a {@link \next} return false 73 * @var bool 74 * If true, we are at the offset: end of th array + 1 75 */ 76 private $endWasReached = false; 77 /** 78 * If true, we are at the offset: start of th array - 1 79 * You can use {@link CallStack::next()} 80 * @var bool 81 */ 82 private $startWasReached = false; 83 84 /** 85 * @var string the type of callstack 86 */ 87 private $callStackType = "unknown"; 88 89 /** 90 * A callstack is a pointer implementation to manipulate 91 * the {@link Doku_Handler::$calls call stack of the handler} 92 * 93 * When you create a callstack object, the pointer 94 * is located at the end. 95 * 96 * If you want to reset the pointer, you need 97 * to call the {@link CallStack::closeAndResetPointer()} function 98 * 99 * @param \Doku_Handler 100 */ 101 public function __construct(&$handler) 102 { 103 $this->handler = $handler; 104 105 /** 106 * A temporary Call stack is created in the writer 107 * for list, table, blockquote 108 * 109 * But third party plugin can overwrite the writer 110 * to capture the call 111 * 112 * See the 113 * https://www.dokuwiki.org/devel:parser#handler_token_methods 114 * for an example with a list component 115 * 116 */ 117 $headErrorMessage = self::MESSAGE_PREFIX_CALLSTACK_NOT_CONFORM; 118 if (!method_exists($handler, 'getCallWriter')) { 119 $class = get_class($handler); 120 LogUtility::msg("$headErrorMessage. The handler ($class) provided cannot manipulate the callstack (ie the function getCallWriter does not exist).", LogUtility::LVL_MSG_ERROR); 121 return; 122 } 123 $callWriter = $handler->getCallWriter(); 124 125 /** 126 * Check the calls property 127 */ 128 $callWriterClass = get_class($callWriter); 129 $callsPropertyFromCallWriterExists = true; 130 try { 131 $rp = new \ReflectionProperty($callWriterClass, "calls"); 132 if ($rp->isPrivate()) { 133 LogUtility::msg("$headErrorMessage. The call writer ($callWriterClass) provided cannot manipulate the callstack (ie the calls of the call writer are private).", LogUtility::LVL_MSG_ERROR); 134 return; 135 } 136 } catch (\ReflectionException $e) { 137 $callsPropertyFromCallWriterExists = false; 138 } 139 140 /** 141 * The calls 142 */ 143 if ($callsPropertyFromCallWriterExists) { 144 145 $writerCalls = &$callWriter->calls; 146 $this->callStack = &$writerCalls; 147 $this->callStackType = self::CALLSTACK_WRITER; 148 149 } else { 150 151 /** 152 * Check the calls property of the handler 153 */ 154 $handlerClass = get_class($handler); 155 try { 156 $rp = new \ReflectionProperty($handlerClass, "calls"); 157 if ($rp->isPrivate()) { 158 LogUtility::msg("$headErrorMessage. The handler ($handlerClass) provided cannot manipulate the callstack (ie the calls of the handler are private).", LogUtility::LVL_MSG_ERROR); 159 return; 160 } 161 } catch (\ReflectionException $e) { 162 LogUtility::msg("$headErrorMessage. The handler ($handlerClass) provided cannot manipulate the callstack (ie the handler does not have any calls property).", LogUtility::LVL_MSG_ERROR); 163 return; 164 } 165 166 /** 167 * Initiate the callstack 168 */ 169 $this->callStack = &$handler->calls; 170 $this->callStackType = self::CALLSTACK_MAIN; 171 172 } 173 174 $this->maxIndex = ArrayUtility::array_key_last($this->callStack); 175 $this->moveToEnd(); 176 177 178 } 179 180 public 181 static function createFromMarkup($marki): CallStack 182 { 183 184 $modes = p_get_parsermodes(); 185 $handler = new Doku_Handler(); 186 $parser = new Parser($handler); 187 188 //add modes to parser 189 foreach ($modes as $mode) { 190 $parser->addMode($mode['mode'], $mode['obj']); 191 } 192 $parser->parse($marki); 193 return self::createFromHandler($handler); 194 195 } 196 197 public static function createEmpty(): CallStack 198 { 199 $emptyHandler = new class extends \Doku_Handler { 200 public $calls = []; 201 202 public function getCallWriter(): object 203 { 204 return new class { 205 public $calls = array(); 206 }; 207 } 208 }; 209 return new CallStack($emptyHandler); 210 } 211 212 public static function createFromInstructions(?array $callStackArray): CallStack 213 { 214 return CallStack::createEmpty() 215 ->appendInstructionsFromNativeArray($callStackArray); 216 217 } 218 219 220 /** 221 * Reset the pointer 222 */ 223 public 224 function closeAndResetPointer() 225 { 226 reset($this->callStack); 227 } 228 229 /** 230 * Delete from the call stack 231 * @param $calls 232 * @param $start 233 * @param $end 234 */ 235 public 236 static function deleteCalls(&$calls, $start, $end) 237 { 238 for ($i = $start; $i <= $end; $i++) { 239 unset($calls[$i]); 240 } 241 } 242 243 /** 244 * @param array $calls 245 * @param integer $position 246 * @param array $callStackToInsert 247 */ 248 public 249 static function insertCallStackUpWards(&$calls, $position, $callStackToInsert) 250 { 251 252 array_splice($calls, $position, 0, $callStackToInsert); 253 254 } 255 256 /** 257 * A callstack pointer based implementation 258 * that starts at the end 259 * @param mixed|Doku_Handler $handler - mixed because we test if the handler passed is not the good one (It can happen with third plugin) 260 * @return CallStack 261 */ 262 public 263 static function createFromHandler(&$handler): CallStack 264 { 265 return new CallStack($handler); 266 } 267 268 269 /** 270 * Process the EOL call to the end of stack 271 * replacing them with paragraph call 272 * 273 * A sort of {@link Block::process()} but only from a tag 274 * to the end of the current stack 275 * 276 * This function is used basically in the {@link DOKU_LEXER_EXIT} 277 * state of {@link SyntaxPlugin::handle()} to create paragraph 278 * with the class given as parameter 279 * 280 * @param array $attributes - the attributes in an callstack array form passed to the paragraph 281 */ 282 public 283 function processEolToEndStack(array $attributes = []) 284 { 285 286 \syntax_plugin_combo_para::fromEolToParagraphUntilEndOfStack($this, $attributes); 287 288 } 289 290 /** 291 * Delete the call where the pointer is 292 * And go to the previous position 293 * 294 * This function can be used in a next loop 295 * 296 * @return Call the deleted call 297 */ 298 public 299 function deleteActualCallAndPrevious(): ?Call 300 { 301 302 $actualCall = $this->getActualCall(); 303 304 $offset = $this->getActualOffset(); 305 array_splice($this->callStack, $offset, 1, []); 306 307 /** 308 * Move to the next element (array splice reset the pointer) 309 * if there is a eol as, we delete it 310 * otherwise we may end up with two eol 311 * and this is an empty paragraph 312 */ 313 $this->moveToOffset($offset); 314 if (!$this->isPointerAtEnd()) { 315 if ($this->getActualCall()->getTagName() == 'eol') { 316 array_splice($this->callStack, $offset, 1, []); 317 } 318 } 319 320 /** 321 * Move to the previous element 322 */ 323 $this->moveToOffset($offset - 1); 324 325 return $actualCall; 326 327 } 328 329 /** 330 * @return Call - get a reference to the actual call 331 * This function returns a {@link Call call} object 332 * by reference, meaning that every update will also modify the element 333 * in the stack 334 */ 335 public 336 function getActualCall(): ?Call 337 { 338 if ($this->endWasReached) { 339 LogUtility::msg("The actual call cannot be ask because the end of the stack was reached", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 340 return null; 341 } 342 if ($this->startWasReached) { 343 LogUtility::msg("The actual call cannot be ask because the start of the stack was reached", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 344 return null; 345 } 346 $actualCallKey = key($this->callStack); 347 $actualCallArray = &$this->callStack[$actualCallKey]; 348 return new Call($actualCallArray, $actualCallKey); 349 350 } 351 352 /** 353 * put the pointer one position further 354 * false if at the end 355 * @return false|Call 356 */ 357 public 358 function next() 359 { 360 if ($this->startWasReached) { 361 $this->startWasReached = false; 362 $result = reset($this->callStack); 363 if ($result === false) { 364 return false; 365 } else { 366 return $this->getActualCall(); 367 } 368 } else { 369 $next = next($this->callStack); 370 if ($next === false) { 371 $this->endWasReached = true; 372 return $next; 373 } else { 374 return $this->getActualCall(); 375 } 376 } 377 378 } 379 380 /** 381 * 382 * From an exit call, move the corresponding Opening call 383 * 384 * This is used mostly in {@link SyntaxPlugin::handle()} from a {@link DOKU_LEXER_EXIT} 385 * to retrieve the {@link DOKU_LEXER_ENTER} call 386 * 387 * @return bool|Call 388 */ 389 public 390 function moveToPreviousCorrespondingOpeningCall() 391 { 392 393 /** 394 * Edge case 395 */ 396 if (empty($this->callStack)) { 397 return false; 398 } 399 400 if (!$this->endWasReached) { 401 $actualCall = $this->getActualCall(); 402 $actualState = $actualCall->getState(); 403 if ($actualState != DOKU_LEXER_EXIT) { 404 /** 405 * Check if we are at the end of the stack 406 */ 407 LogUtility::msg("You are not at the end of stack and you are not on a opening tag, you can't ask for the opening tag." . $actualState, LogUtility::LVL_MSG_ERROR, "support"); 408 return false; 409 } 410 } 411 $level = 0; 412 while ($actualCall = $this->previous()) { 413 414 $state = $actualCall->getState(); 415 switch ($state) { 416 case DOKU_LEXER_ENTER: 417 $level++; 418 break; 419 case DOKU_LEXER_EXIT: 420 $level--; 421 break; 422 } 423 if ($level > 0) { 424 break; 425 } 426 427 } 428 if ($level > 0) { 429 return $actualCall; 430 } else { 431 return false; 432 } 433 } 434 435 436 /** 437 * @return Call|false the previous call or false if there is no more previous call 438 */ 439 public 440 function previous() 441 { 442 if ($this->endWasReached) { 443 $this->endWasReached = false; 444 $return = end($this->callStack); 445 if ($return == false) { 446 // empty array (first call on the stack) 447 return false; 448 } else { 449 return $this->getActualCall(); 450 } 451 } else { 452 $prev = prev($this->callStack); 453 if ($prev === false) { 454 $this->startWasReached = true; 455 return $prev; 456 } else { 457 return $this->getActualCall(); 458 } 459 } 460 461 } 462 463 /** 464 * Return the first enter or special child call (ie a tag) 465 * @return Call|false 466 */ 467 public 468 function moveToFirstChildTag() 469 { 470 $found = false; 471 while ($this->next()) { 472 473 $actualCall = $this->getActualCall(); 474 $state = $actualCall->getState(); 475 switch ($state) { 476 case DOKU_LEXER_ENTER: 477 case DOKU_LEXER_SPECIAL: 478 $found = true; 479 break 2; 480 case DOKU_LEXER_EXIT: 481 break 2; 482 } 483 484 } 485 if ($found) { 486 return $this->getActualCall(); 487 } else { 488 return false; 489 } 490 491 492 } 493 494 /** 495 * The end is the one after the last element 496 */ 497 public 498 function moveToEnd() 499 { 500 if ($this->startWasReached) { 501 $this->startWasReached = false; 502 } 503 end($this->callStack); 504 return $this->next(); 505 } 506 507 /** 508 * On the same level 509 */ 510 public 511 function moveToNextSiblingTag() 512 { 513 514 /** 515 * Edgde case 516 */ 517 if (empty($this->callStack)) { 518 return false; 519 } 520 521 $actualCall = $this->getActualCall(); 522 $enterState = $actualCall->getState(); 523 if (!in_array($enterState, CallStack::TAG_STATE)) { 524 LogUtility::msg("A next sibling can be asked only from a tag call. The state is " . $actualState, LogUtility::LVL_MSG_ERROR, "support"); 525 return false; 526 } 527 $level = 0; 528 while ($this->next()) { 529 530 $actualCall = $this->getActualCall(); 531 $state = $actualCall->getState(); 532 switch ($state) { 533 case DOKU_LEXER_ENTER: 534 $level++; 535 break; 536 case DOKU_LEXER_SPECIAL: 537 if ($enterState === DOKU_LEXER_SPECIAL) { 538 break; 539 } else { 540 // ENTER TAG 541 continue 2; 542 } 543 case DOKU_LEXER_EXIT: 544 $level--; 545 break; 546 } 547 548 if ($level == 0 && in_array($state, self::TAG_STATE)) { 549 break; 550 } 551 } 552 if ($level == 0 && !$this->endWasReached) { 553 return $this->getActualCall(); 554 } else { 555 return false; 556 } 557 } 558 559 /** 560 * @param Call $call 561 * @return Call the inserted call 562 */ 563 public 564 function insertBefore(Call $call): Call 565 { 566 if ($this->endWasReached) { 567 568 $this->callStack[] = $call->toCallArray(); 569 570 } else { 571 572 $offset = $this->getActualOffset(); 573 array_splice($this->callStack, $offset, 0, [$call->toCallArray()]); 574 // array splice reset the pointer 575 // we move it to the actual element (ie the key is offset +1) 576 $this->moveToOffset($offset + 1); 577 578 } 579 return $call; 580 } 581 582 /** 583 * Move pointer by offset 584 * @param $offset 585 */ 586 private 587 function moveToOffset($offset) 588 { 589 $this->resetPointer(); 590 for ($i = 0; $i < $offset; $i++) { 591 $result = $this->next(); 592 if ($result === false) { 593 break; 594 } 595 } 596 } 597 598 /** 599 * Move pointer by key 600 * @param $targetKey 601 */ 602 private 603 function moveToKey($targetKey) 604 { 605 $this->resetPointer(); 606 for ($i = 0; $i < $targetKey; $i++) { 607 next($this->callStack); 608 } 609 $actualKey = key($this->callStack); 610 if ($actualKey != $targetKey) { 611 LogUtility::msg("The target key ($targetKey) is not equal to the actual key ($actualKey). The moveToKey was not successful"); 612 } 613 } 614 615 /** 616 * Insert After. The pointer stays at the current state. 617 * If you don't need to process the call that you just 618 * inserted, you may want to call {@link CallStack::next()} 619 * @param Call $call 620 */ 621 public 622 function insertAfter($call) 623 { 624 $actualKey = key($this->callStack); 625 if ($actualKey == null) { 626 if ($this->endWasReached == true) { 627 $this->callStack[] = $call->toCallArray(); 628 } else { 629 LogUtility::msg("Callstack: Actual key is null, we can't insert after null"); 630 } 631 } else { 632 $offset = array_search($actualKey, array_keys($this->callStack), true); 633 array_splice($this->callStack, $offset + 1, 0, [$call->toCallArray()]); 634 // array splice reset the pointer 635 // we move it to the actual element 636 $this->moveToKey($actualKey); 637 } 638 } 639 640 public 641 function getActualKey() 642 { 643 return key($this->callStack); 644 } 645 646 /** 647 * Insert an EOL call if the next call is not an EOL 648 * This is to enforce an new paragraph 649 */ 650 public 651 function insertEolIfNextCallIsNotEolOrBlock() 652 { 653 if (!$this->isPointerAtEnd()) { 654 $nextCall = $this->next(); 655 if ($nextCall != false) { 656 if ($nextCall->getTagName() != "eol" && $nextCall->getDisplay() != "block") { 657 $this->insertBefore( 658 Call::createNativeCall("eol") 659 ); 660 // move on the eol 661 $this->previous(); 662 } 663 // move back 664 $this->previous(); 665 } 666 } 667 } 668 669 private 670 function isPointerAtEnd() 671 { 672 return $this->endWasReached; 673 } 674 675 public 676 function &getHandler() 677 { 678 return $this->handler; 679 } 680 681 /** 682 * Return The offset (not the key): 683 * * starting at 0 for the first element 684 * * 1 for the second ... 685 * 686 * @return false|int|string 687 */ 688 private 689 function getActualOffset() 690 { 691 $actualKey = key($this->callStack); 692 return array_search($actualKey, array_keys($this->callStack), true); 693 } 694 695 private 696 function resetPointer() 697 { 698 reset($this->callStack); 699 $this->endWasReached = false; 700 } 701 702 public 703 function moveToStart() 704 { 705 $this->resetPointer(); 706 return $this->previous(); 707 } 708 709 /** 710 * @return Call|false the parent call or false if there is no parent 711 * If you are on an {@link DOKU_LEXER_EXIT} state, you should 712 * call first the {@link CallStack::moveToPreviousCorrespondingOpeningCall()} 713 */ 714 public function moveToParent() 715 { 716 717 /** 718 * Case when we start from the exit state element 719 * We go first to the opening tag 720 * because the algorithm is level based. 721 * 722 * When the end is reached, there is no call 723 * (this not the {@link end php end} but one further 724 */ 725 if (!$this->endWasReached && !$this->startWasReached && $this->getActualCall()->getState() == DOKU_LEXER_EXIT) { 726 727 $this->moveToPreviousCorrespondingOpeningCall(); 728 729 } 730 731 732 /** 733 * We are in a parent when the tree level is negative 734 */ 735 $treeLevel = 0; 736 while ($actualCall = $this->previous()) { 737 738 /** 739 * Add 740 * would become a parent on its enter state 741 */ 742 $actualCallState = $actualCall->getState(); 743 switch ($actualCallState) { 744 case DOKU_LEXER_ENTER: 745 $treeLevel = $treeLevel - 1; 746 break; 747 case DOKU_LEXER_EXIT: 748 /** 749 * When the tag has a sibling with an exit tag 750 */ 751 $treeLevel = $treeLevel + 1; 752 break; 753 } 754 755 /** 756 * The breaking statement 757 */ 758 if ($treeLevel < 0) { 759 break; 760 } 761 762 } 763 return $actualCall; 764 765 766 } 767 768 /** 769 * Delete the anchor link to the image (ie the lightbox) 770 * 771 * This is used in navigation and for instance 772 * in heading 773 */ 774 public function processNoLinkOnImageToEndStack() 775 { 776 while ($this->next()) { 777 $actualCall = $this->getActualCall(); 778 if ($actualCall->getTagName() == syntax_plugin_combo_media::TAG) { 779 $actualCall->addAttribute(MediaLink::LINKING_KEY, MediaLink::LINKING_NOLINK_VALUE); 780 } 781 } 782 } 783 784 /** 785 * Append instructions to the callstack (ie at the end) 786 * @param array $instructions 787 * @return CallStack 788 */ 789 public function appendInstructionsFromNativeArray(array $instructions): CallStack 790 { 791 array_splice($this->callStack, count($this->callStack), 0, $instructions); 792 return $this; 793 } 794 795 /** 796 * @param Call $call 797 */ 798 public function appendCallAtTheEnd($call) 799 { 800 $this->callStack[] = $call->toCallArray(); 801 } 802 803 public function moveToPreviousSiblingTag() 804 { 805 /** 806 * Edge case 807 */ 808 if (empty($this->callStack)) { 809 return false; 810 } 811 812 $enterState = null; 813 if (!$this->endWasReached) { 814 $actualCall = $this->getActualCall(); 815 $enterState = $actualCall->getState(); 816 if (!in_array($enterState, CallStack::TAG_STATE)) { 817 LogUtility::msg("A previous sibling can be asked only from a tag call. The state is " . $actualState, LogUtility::LVL_MSG_ERROR, "support"); 818 return false; 819 } 820 } 821 $level = 0; 822 while ($actualCall = $this->previous()) { 823 824 $state = $actualCall->getState(); 825 switch ($state) { 826 case DOKU_LEXER_ENTER: 827 $level++; 828 break; 829 case DOKU_LEXER_SPECIAL: 830 if ($enterState === DOKU_LEXER_SPECIAL) { 831 break; 832 } else { 833 continue 2; 834 } 835 case DOKU_LEXER_EXIT: 836 $level--; 837 break; 838 default: 839 // cdata 840 continue 2; 841 } 842 843 if ($level == 0 && in_array($state, self::TAG_STATE)) { 844 break; 845 } 846 } 847 if ($level == 0 && !$this->startWasReached) { 848 return $this->getActualCall(); 849 } else { 850 return false; 851 } 852 } 853 854 /** 855 * Delete all calls after the passed call 856 * 857 * It's used in syntax generator that: 858 * * capture the children callstack at the end, 859 * * delete it 860 * * and use it to generate more calls. 861 * 862 * @param Call $call 863 */ 864 public function deleteAllCallsAfter(Call $call) 865 { 866 $key = $call->getKey(); 867 $offset = array_search($key, array_keys($this->callStack), true); 868 if ($offset !== false) { 869 /** 870 * We delete from the next 871 * {@link array_splice()} delete also the given offset 872 */ 873 array_splice($this->callStack, $offset + 1); 874 } else { 875 LogUtility::msg("The call ($call) could not be found in the callStack. We couldn't therefore delete the calls after"); 876 } 877 878 } 879 880 /** 881 * @param Call[] $calls 882 */ 883 public function appendInstructionsFromCallObjects($calls) 884 { 885 foreach ($calls as $call) { 886 $this->appendCallAtTheEnd($call); 887 } 888 889 } 890 891 /** 892 * 893 * @return int|mixed - the last position on the callstack 894 * If you are at the end of the callstack after a full parsing, 895 * this should be the length of the string of the page 896 */ 897 public function getLastCharacterPosition() 898 { 899 $offset = $this->getActualOffset(); 900 901 $lastEndPosition = 0; 902 $this->moveToEnd(); 903 while ($actualCall = $this->previous()) { 904 // p_open and p_close have always a position value of 0 905 $lastEndPosition = $actualCall->getLastMatchedCharacterPosition(); 906 if ($lastEndPosition !== 0) { 907 break; 908 } 909 } 910 if ($offset == null) { 911 $this->moveToEnd(); 912 } else { 913 $this->moveToOffset($offset); 914 } 915 return $lastEndPosition; 916 917 } 918 919 public function getStack(): array 920 { 921 return $this->callStack; 922 } 923 924 public function moveToFirstEnterTag() 925 { 926 927 while ($actualCall = $this->next()) { 928 929 if ($actualCall->getState() === DOKU_LEXER_ENTER) { 930 return $this->getActualCall(); 931 } 932 } 933 return false; 934 935 } 936 937 /** 938 * Move the pointer to the corresponding exit call 939 * and return it or false if not found 940 * @return Call|false 941 */ 942 public function moveToNextCorrespondingExitTag() 943 { 944 /** 945 * Edge case 946 */ 947 if (empty($this->callStack)) { 948 return false; 949 } 950 951 /** 952 * Check if we are on an enter tag 953 */ 954 $actualCall = $this->getActualCall(); 955 if ($actualCall === null) { 956 LogUtility::msg("You are not on the stack (start or end), you can't ask for the corresponding exit call", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 957 return false; 958 } 959 $actualState = $actualCall->getState(); 960 if ($actualState != DOKU_LEXER_ENTER) { 961 LogUtility::msg("You are not on an enter tag ($actualState). You can't ask for the corresponding exit call .", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 962 return false; 963 } 964 965 $level = 0; 966 while ($actualCall = $this->next()) { 967 968 $state = $actualCall->getState(); 969 switch ($state) { 970 case DOKU_LEXER_ENTER: 971 $level++; 972 break; 973 case DOKU_LEXER_EXIT: 974 $level--; 975 break; 976 } 977 if ($level < 0) { 978 break; 979 } 980 981 } 982 if ($level < 0) { 983 return $actualCall; 984 } else { 985 return false; 986 } 987 988 } 989 990 public function moveToCall(Call $call): ?Call 991 { 992 $targetKey = $call->getKey(); 993 $actualKey = $this->getActualKey(); 994 $diff = $targetKey - $actualKey ; 995 for ($i = 0; $i < abs($diff); $i++) { 996 if ($diff > 0) { 997 $this->next(); 998 } else { 999 $this->previous(); 1000 } 1001 } 1002 return $this->getActualCall(); 1003 } 1004 1005 1006} 1007