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