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