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