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) 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 /** 198 * Reset the pointer 199 */ 200 public 201 function closeAndResetPointer() 202 { 203 reset($this->callStack); 204 } 205 206 /** 207 * Delete from the call stack 208 * @param $calls 209 * @param $start 210 * @param $end 211 */ 212 public 213 static function deleteCalls(&$calls, $start, $end) 214 { 215 for ($i = $start; $i <= $end; $i++) { 216 unset($calls[$i]); 217 } 218 } 219 220 /** 221 * @param array $calls 222 * @param integer $position 223 * @param array $callStackToInsert 224 */ 225 public 226 static function insertCallStackUpWards(&$calls, $position, $callStackToInsert) 227 { 228 229 array_splice($calls, $position, 0, $callStackToInsert); 230 231 } 232 233 /** 234 * A callstack pointer based implementation 235 * that starts at the end 236 * @param Doku_Handler $handler 237 * @return CallStack 238 */ 239 public 240 static function createFromHandler(&$handler) 241 { 242 return new CallStack($handler); 243 } 244 245 246 /** 247 * Process the EOL call to the end of stack 248 * replacing them with paragraph call 249 * 250 * A sort of {@link Block::process()} but only from a tag 251 * to the end of the current stack 252 * 253 * This function is used basically in the {@link DOKU_LEXER_EXIT} 254 * state of {@link SyntaxPlugin::handle()} to create paragraph 255 * with the class given as parameter 256 * 257 * @param $attributes - the attributes in an callstack array form passed to the paragraph 258 */ 259 public 260 function processEolToEndStack($attributes = []) 261 { 262 263 \syntax_plugin_combo_para::fromEolToParagraphUntilEndOfStack($this, $attributes); 264 265 } 266 267 /** 268 * Delete the call where the pointer is 269 * And go to the previous position 270 * 271 * This function can be used in a next loop 272 * 273 * @return Call the deleted call 274 */ 275 public 276 function deleteActualCallAndPrevious(): ?Call 277 { 278 279 $actualCall = $this->getActualCall(); 280 281 $offset = $this->getActualOffset(); 282 array_splice($this->callStack, $offset, 1, []); 283 284 /** 285 * Move to the next element (array splice reset the pointer) 286 * if there is a eol as, we delete it 287 * otherwise we may end up with two eol 288 * and this is an empty paragraph 289 */ 290 $this->moveToOffset($offset); 291 if (!$this->isPointerAtEnd()) { 292 if ($this->getActualCall()->getTagName() == 'eol') { 293 array_splice($this->callStack, $offset, 1, []); 294 } 295 } 296 297 /** 298 * Move to the previous element 299 */ 300 $this->moveToOffset($offset - 1); 301 302 return $actualCall; 303 304 } 305 306 /** 307 * @return Call - get a reference to the actual call 308 * This function returns a {@link Call call} object 309 * by reference, meaning that every update will also modify the element 310 * in the stack 311 */ 312 public 313 function getActualCall() 314 { 315 if ($this->endWasReached) { 316 LogUtility::msg("The actual call cannot be ask because the end of the stack was reached", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 317 return null; 318 } 319 if ($this->startWasReached) { 320 LogUtility::msg("The actual call cannot be ask because the start of the stack was reached", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 321 return null; 322 } 323 $actualCallKey = key($this->callStack); 324 $actualCallArray = &$this->callStack[$actualCallKey]; 325 return new Call($actualCallArray, $actualCallKey); 326 327 } 328 329 /** 330 * put the pointer one position further 331 * false if at the end 332 * @return false|Call 333 */ 334 public 335 function next() 336 { 337 if ($this->startWasReached) { 338 $this->startWasReached = false; 339 $result = reset($this->callStack); 340 if ($result === false) { 341 return false; 342 } else { 343 return $this->getActualCall(); 344 } 345 } else { 346 $next = next($this->callStack); 347 if ($next === false) { 348 $this->endWasReached = true; 349 return $next; 350 } else { 351 return $this->getActualCall(); 352 } 353 } 354 355 } 356 357 /** 358 * 359 * From an exit call, move the corresponding Opening call 360 * 361 * This is used mostly in {@link SyntaxPlugin::handle()} from a {@link DOKU_LEXER_EXIT} 362 * to retrieve the {@link DOKU_LEXER_ENTER} call 363 * 364 * @return bool|Call 365 */ 366 public 367 function moveToPreviousCorrespondingOpeningCall() 368 { 369 370 /** 371 * Edgde case 372 */ 373 if(empty($this->callStack)){ 374 return false; 375 } 376 377 if (!$this->endWasReached) { 378 $actualCall = $this->getActualCall(); 379 $actualState = $actualCall->getState(); 380 if ($actualState != DOKU_LEXER_EXIT) { 381 /** 382 * Check if we are at the end of the stack 383 */ 384 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"); 385 return false; 386 } 387 } 388 $level = 0; 389 while ($actualCall = $this->previous()) { 390 391 $state = $actualCall->getState(); 392 switch ($state) { 393 case DOKU_LEXER_ENTER: 394 $level++; 395 break; 396 case DOKU_LEXER_EXIT: 397 $level--; 398 break; 399 } 400 if ($level > 0) { 401 break; 402 } 403 404 } 405 if ($level > 0) { 406 return $actualCall; 407 } else { 408 return false; 409 } 410 } 411 412 413 /** 414 * @return Call|false the previous call or false if there is no more previous call 415 */ 416 public 417 function previous() 418 { 419 if ($this->endWasReached) { 420 $this->endWasReached = false; 421 $return = end($this->callStack); 422 if ($return == false) { 423 // empty array (first call on the stack) 424 return false; 425 } else { 426 return $this->getActualCall(); 427 } 428 } else { 429 $prev = prev($this->callStack); 430 if ($prev === false) { 431 $this->startWasReached = true; 432 return $prev; 433 } else { 434 return $this->getActualCall(); 435 } 436 } 437 438 } 439 440 /** 441 * Return the first enter or special child call (ie a tag) 442 * @return Call|false 443 */ 444 public 445 function moveToFirstChildTag() 446 { 447 $found = false; 448 while ($this->next()) { 449 450 $actualCall = $this->getActualCall(); 451 $state = $actualCall->getState(); 452 switch ($state) { 453 case DOKU_LEXER_ENTER: 454 case DOKU_LEXER_SPECIAL: 455 $found = true; 456 break 2; 457 case DOKU_LEXER_EXIT: 458 break 2; 459 } 460 461 } 462 if ($found) { 463 return $this->getActualCall(); 464 } else { 465 return false; 466 } 467 468 469 } 470 471 /** 472 * The end is the one after the last element 473 */ 474 public 475 function moveToEnd() 476 { 477 if ($this->startWasReached) { 478 $this->startWasReached = false; 479 } 480 end($this->callStack); 481 $this->next(); 482 } 483 484 /** 485 * On the same level 486 */ 487 public 488 function moveToNextSiblingTag() 489 { 490 491 /** 492 * Edgde case 493 */ 494 if(empty($this->callStack)){ 495 return false; 496 } 497 498 $actualCall = $this->getActualCall(); 499 $actualState = $actualCall->getState(); 500 if (!in_array($actualState, CallStack::TAG_STATE)) { 501 LogUtility::msg("A next sibling can be asked only from a tag call. The state is " . $actualState, LogUtility::LVL_MSG_ERROR, "support"); 502 return false; 503 } 504 $level = 0; 505 while ($this->next()) { 506 507 $actualCall = $this->getActualCall(); 508 $state = $actualCall->getState(); 509 switch ($state) { 510 case DOKU_LEXER_ENTER: 511 case DOKU_LEXER_SPECIAL: 512 $level++; 513 break; 514 case DOKU_LEXER_EXIT: 515 $level--; 516 break; 517 } 518 519 if ($level == 0 && in_array($state, self::TAG_STATE)) { 520 break; 521 } 522 } 523 if ($level == 0 && !$this->endWasReached) { 524 return $this->getActualCall(); 525 } else { 526 return false; 527 } 528 } 529 530 /** 531 * @param Call $call 532 * @return Call the inserted call 533 */ 534 public 535 function insertBefore(Call $call): Call 536 { 537 if ($this->endWasReached) { 538 539 $this->callStack[] = $call->toCallArray(); 540 541 } else { 542 543 $offset = $this->getActualOffset(); 544 array_splice($this->callStack, $offset, 0, [$call->toCallArray()]); 545 // array splice reset the pointer 546 // we move it to the actual element (ie the key is offset +1) 547 $this->moveToOffset($offset + 1); 548 549 } 550 return $call; 551 } 552 553 /** 554 * Move pointer by offset 555 * @param $offset 556 */ 557 private 558 function moveToOffset($offset) 559 { 560 $this->resetPointer(); 561 for ($i = 0; $i < $offset; $i++) { 562 $result = $this->next(); 563 if ($result === false) { 564 break; 565 } 566 } 567 } 568 569 /** 570 * Move pointer by key 571 * @param $targetKey 572 */ 573 private 574 function moveToKey($targetKey) 575 { 576 $this->resetPointer(); 577 for ($i = 0; $i < $targetKey; $i++) { 578 next($this->callStack); 579 } 580 $actualKey = key($this->callStack); 581 if ($actualKey != $targetKey) { 582 LogUtility::msg("The target key ($targetKey) is not equal to the actual key ($actualKey). The moveToKey was not successful"); 583 } 584 } 585 586 /** 587 * Insert After. The pointer stays at the current state. 588 * If you don't need to process the call that you just 589 * inserted, you may want to call {@link CallStack::next()} 590 * @param Call $call 591 */ 592 public 593 function insertAfter($call) 594 { 595 $actualKey = key($this->callStack); 596 if ($actualKey == null) { 597 if ($this->endWasReached == true) { 598 $this->callStack[] = $call->toCallArray(); 599 } else { 600 LogUtility::msg("Callstack: Actual key is null, we can't insert after null"); 601 } 602 } else { 603 $offset = array_search($actualKey, array_keys($this->callStack), true); 604 array_splice($this->callStack, $offset + 1, 0, [$call->toCallArray()]); 605 // array splice reset the pointer 606 // we move it to the actual element 607 $this->moveToKey($actualKey); 608 } 609 } 610 611 public 612 function getActualKey() 613 { 614 return key($this->callStack); 615 } 616 617 /** 618 * Insert an EOL call if the next call is not an EOL 619 * This is to enforce an new paragraph 620 */ 621 public 622 function insertEolIfNextCallIsNotEolOrBlock() 623 { 624 if (!$this->isPointerAtEnd()) { 625 $nextCall = $this->next(); 626 if ($nextCall != false) { 627 if ($nextCall->getTagName() != "eol" && $nextCall->getDisplay() != "block") { 628 $this->insertBefore( 629 Call::createNativeCall("eol") 630 ); 631 // move on the eol 632 $this->previous(); 633 } 634 // move back 635 $this->previous(); 636 } 637 } 638 } 639 640 private 641 function isPointerAtEnd() 642 { 643 return $this->endWasReached; 644 } 645 646 public 647 function &getHandler() 648 { 649 return $this->handler; 650 } 651 652 /** 653 * Return The offset (not the key): 654 * * starting at 0 for the first element 655 * * 1 for the second ... 656 * 657 * @return false|int|string 658 */ 659 private 660 function getActualOffset() 661 { 662 $actualKey = key($this->callStack); 663 return array_search($actualKey, array_keys($this->callStack), true); 664 } 665 666 private 667 function resetPointer() 668 { 669 reset($this->callStack); 670 $this->endWasReached = false; 671 } 672 673 public 674 function moveToStart() 675 { 676 $this->resetPointer(); 677 $this->previous(); 678 } 679 680 /** 681 * @return Call|false the parent call or false if there is no parent 682 * If you are on an {@link DOKU_LEXER_EXIT} state, you should 683 * call first the {@link CallStack::moveToPreviousCorrespondingOpeningCall()} 684 */ 685 public function moveToParent() 686 { 687 688 /** 689 * Case when we start from the exit state element 690 * We go first to the opening tag 691 * because the algorithm is level based. 692 * 693 * When the end is reached, there is no call 694 * (this not the {@link end php end} but one further 695 */ 696 if (!$this->endWasReached && !$this->startWasReached && $this->getActualCall()->getState() == DOKU_LEXER_EXIT) { 697 698 $this->moveToPreviousCorrespondingOpeningCall(); 699 700 } 701 702 703 /** 704 * We are in a parent when the tree level is negative 705 */ 706 $treeLevel = 0; 707 while ($actualCall = $this->previous()) { 708 709 /** 710 * Add 711 * would become a parent on its enter state 712 */ 713 $actualCallState = $actualCall->getState(); 714 switch ($actualCallState) { 715 case DOKU_LEXER_ENTER: 716 $treeLevel = $treeLevel - 1; 717 break; 718 case DOKU_LEXER_EXIT: 719 /** 720 * When the tag has a sibling with an exit tag 721 */ 722 $treeLevel = $treeLevel + 1; 723 break; 724 } 725 726 /** 727 * The breaking statement 728 */ 729 if ($treeLevel < 0) { 730 break; 731 } 732 733 } 734 return $actualCall; 735 736 737 } 738 739 /** 740 * Delete the anchor link to the image (ie the lightbox) 741 * 742 * This is used in navigation and for instance 743 * in heading 744 */ 745 public function processNoLinkOnImageToEndStack() 746 { 747 while ($this->next()) { 748 $actualCall = $this->getActualCall(); 749 if ($actualCall->getTagName() == syntax_plugin_combo_media::TAG) { 750 $actualCall->addAttribute(MediaLink::LINKING_KEY, MediaLink::LINKING_NOLINK_VALUE); 751 } 752 } 753 } 754 755 /** 756 * Append instructions to the callstack (ie at the end) 757 * @param array $instructions 758 */ 759 public function appendInstructionsFromNativeArray($instructions) 760 { 761 array_splice($this->callStack, count($this->callStack), 0, $instructions); 762 } 763 764 /** 765 * @param Call $call 766 */ 767 public function appendCallAtTheEnd($call) 768 { 769 $this->callStack[] = $call->toCallArray(); 770 } 771 772 public function moveToPreviousSiblingTag() 773 { 774 /** 775 * Edge case 776 */ 777 if(empty($this->callStack)){ 778 return false; 779 } 780 781 if (!$this->endWasReached) { 782 $actualCall = $this->getActualCall(); 783 $actualState = $actualCall->getState(); 784 if (!in_array($actualState, CallStack::TAG_STATE)) { 785 LogUtility::msg("A previous sibling can be asked only from a tag call. The state is " . $actualState, LogUtility::LVL_MSG_ERROR, "support"); 786 return false; 787 } 788 } 789 $level = 0; 790 while ($this->previous()) { 791 792 $actualCall = $this->getActualCall(); 793 $state = $actualCall->getState(); 794 switch ($state) { 795 case DOKU_LEXER_ENTER: 796 case DOKU_LEXER_SPECIAL: 797 $level++; 798 break; 799 case DOKU_LEXER_EXIT: 800 $level--; 801 break; 802 } 803 804 if ($level == 0 && in_array($state, self::TAG_STATE)) { 805 break; 806 } 807 } 808 if ($level == 0 && !$this->startWasReached) { 809 return $this->getActualCall(); 810 } else { 811 return false; 812 } 813 } 814 815 /** 816 * Delete all calls after the passed call 817 * 818 * It's used in syntax generator that: 819 * * capture the children callstack at the end, 820 * * delete it 821 * * and use it to generate more calls. 822 * 823 * @param Call $call 824 */ 825 public function deleteAllCallsAfter(Call $call) 826 { 827 $key = $call->getKey(); 828 $offset = array_search($key, array_keys($this->callStack), true); 829 if ($offset !== false) { 830 /** 831 * We delete from the next 832 * {@link array_splice()} delete also the given offset 833 */ 834 array_splice($this->callStack, $offset + 1); 835 } else { 836 LogUtility::msg("The call ($call) could not be found in the callStack. We couldn't therefore delete the calls after"); 837 } 838 839 } 840 841 /** 842 * @param Call[] $calls 843 */ 844 public function appendInstructionsFromCallObjects($calls) 845 { 846 foreach($calls as $call){ 847 $this->appendCallAtTheEnd($call); 848 } 849 850 } 851 852 853} 854