xref: /template/strap/ComboStrap/CallStack.php (revision c3437056399326d621a01da73b649707fbb0ae69)
137748cd8SNickeau<?php
237748cd8SNickeau/**
337748cd8SNickeau * Copyright (c) 2021. ComboStrap, Inc. and its affiliates. All Rights Reserved.
437748cd8SNickeau *
537748cd8SNickeau * This source code is licensed under the GPL license found in the
637748cd8SNickeau * COPYING  file in the root directory of this source tree.
737748cd8SNickeau *
837748cd8SNickeau * @license  GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html)
937748cd8SNickeau * @author   ComboStrap <support@combostrap.com>
1037748cd8SNickeau *
1137748cd8SNickeau */
1237748cd8SNickeau
1337748cd8SNickeaunamespace ComboStrap;
1437748cd8SNickeau
1537748cd8SNickeau
1637748cd8SNickeauuse Doku_Handler;
1737748cd8SNickeauuse dokuwiki\Extension\SyntaxPlugin;
1837748cd8SNickeauuse dokuwiki\Parsing\Parser;
1937748cd8SNickeauuse syntax_plugin_combo_media;
2037748cd8SNickeau
2137748cd8SNickeau/**
2237748cd8SNickeau * Class CallStack
2337748cd8SNickeau * @package ComboStrap
2437748cd8SNickeau *
2537748cd8SNickeau * This is a class that manipulate the call stack.
2637748cd8SNickeau *
2737748cd8SNickeau * A call stack is composed of call (ie array)
2837748cd8SNickeau * A tag is a call that has a state {@link DOKU_LEXER_ENTER} or {@link DOKU_LEXER_SPECIAL}
2937748cd8SNickeau * An opening call is a call with the {@link DOKU_LEXER_ENTER}
3037748cd8SNickeau * An closing call is a call with the {@link DOKU_LEXER_EXIT}
3137748cd8SNickeau *
3237748cd8SNickeau * You can move on the stack with the function:
3337748cd8SNickeau *   * {@link CallStack::next()}
3437748cd8SNickeau *   * {@link CallStack::previous()}
3537748cd8SNickeau *   * `MoveTo`. example: {@link CallStack::moveToPreviousCorrespondingOpeningCall()}
3637748cd8SNickeau *
3737748cd8SNickeau *
3837748cd8SNickeau */
3937748cd8SNickeauclass CallStack
4037748cd8SNickeau{
4137748cd8SNickeau
4237748cd8SNickeau    const TAG_STATE = [DOKU_LEXER_SPECIAL, DOKU_LEXER_ENTER];
4337748cd8SNickeau
4437748cd8SNickeau    const CANONICAL = "support";
4537748cd8SNickeau
4637748cd8SNickeau    /**
4737748cd8SNickeau     * The type of callstack
4837748cd8SNickeau     *   * main is the normal
4937748cd8SNickeau     *   * writer is when there is a temporary call stack from the writer
5037748cd8SNickeau     */
5137748cd8SNickeau    const CALLSTACK_WRITER = "writer";
5237748cd8SNickeau    const CALLSTACK_MAIN = "main";
5337748cd8SNickeau    public const MESSAGE_PREFIX_CALLSTACK_NOT_CONFORM = "Your DokuWiki installation is too old or a writer plugin does not conform";
5437748cd8SNickeau
5537748cd8SNickeau    private $handler;
5637748cd8SNickeau
5737748cd8SNickeau    /**
5837748cd8SNickeau     * The max key of the calls
5937748cd8SNickeau     * @var int|null
6037748cd8SNickeau     */
6137748cd8SNickeau    private $maxIndex = 0;
6237748cd8SNickeau
6337748cd8SNickeau    /**
6437748cd8SNickeau     * @var array the call stack
6537748cd8SNickeau     */
6637748cd8SNickeau    private $callStack = [];
6737748cd8SNickeau
6837748cd8SNickeau    /**
6937748cd8SNickeau     * A pointer to keep the information
7037748cd8SNickeau     * if we have gone to far in the stack
7137748cd8SNickeau     * (because you lost the fact that you are outside
7237748cd8SNickeau     * the boundary, ie you can do a {@link \prev}` after that a {@link \next} return false
7337748cd8SNickeau     * @var bool
7437748cd8SNickeau     * If true, we are at the offset: end of th array + 1
7537748cd8SNickeau     */
7637748cd8SNickeau    private $endWasReached = false;
7737748cd8SNickeau    /**
7837748cd8SNickeau     * If true, we are at the offset: start of th array - 1
7937748cd8SNickeau     * You can use {@link CallStack::next()}
8037748cd8SNickeau     * @var bool
8137748cd8SNickeau     */
8237748cd8SNickeau    private $startWasReached = false;
8337748cd8SNickeau
8437748cd8SNickeau    /**
8537748cd8SNickeau     * @var string the type of callstack
8637748cd8SNickeau     */
8737748cd8SNickeau    private $callStackType = "unknown";
8837748cd8SNickeau
8937748cd8SNickeau    /**
9037748cd8SNickeau     * A callstack is a pointer implementation to manipulate
9137748cd8SNickeau     * the {@link Doku_Handler::$calls call stack of the handler}
9237748cd8SNickeau     *
9337748cd8SNickeau     * When you create a callstack object, the pointer
9437748cd8SNickeau     * is located at the end.
9537748cd8SNickeau     *
9637748cd8SNickeau     * If you want to reset the pointer, you need
9737748cd8SNickeau     * to call the {@link CallStack::closeAndResetPointer()} function
9837748cd8SNickeau     *
9937748cd8SNickeau     * @param \Doku_Handler
10037748cd8SNickeau     */
10137748cd8SNickeau    public function __construct(&$handler)
10237748cd8SNickeau    {
10337748cd8SNickeau        $this->handler = $handler;
10437748cd8SNickeau
10537748cd8SNickeau        /**
10637748cd8SNickeau         * A temporary Call stack is created in the writer
10737748cd8SNickeau         * for list, table, blockquote
10837748cd8SNickeau         *
10937748cd8SNickeau         * But third party plugin can overwrite the writer
11037748cd8SNickeau         * to capture the call
11137748cd8SNickeau         *
11237748cd8SNickeau         * See the
11337748cd8SNickeau         * https://www.dokuwiki.org/devel:parser#handler_token_methods
11437748cd8SNickeau         * for an example with a list component
11537748cd8SNickeau         *
11637748cd8SNickeau         */
11737748cd8SNickeau        $headErrorMessage = self::MESSAGE_PREFIX_CALLSTACK_NOT_CONFORM;
11837748cd8SNickeau        if (!method_exists($handler, 'getCallWriter')) {
11937748cd8SNickeau            $class = get_class($handler);
12037748cd8SNickeau            LogUtility::msg("$headErrorMessage. The handler ($class) provided cannot manipulate the callstack (ie the function getCallWriter does not exist).", LogUtility::LVL_MSG_ERROR);
12137748cd8SNickeau            return;
12237748cd8SNickeau        }
12337748cd8SNickeau        $callWriter = $handler->getCallWriter();
12437748cd8SNickeau
12537748cd8SNickeau        /**
12637748cd8SNickeau         * Check the calls property
12737748cd8SNickeau         */
12837748cd8SNickeau        $callWriterClass = get_class($callWriter);
12937748cd8SNickeau        $callsPropertyFromCallWriterExists = true;
13037748cd8SNickeau        try {
13137748cd8SNickeau            $rp = new \ReflectionProperty($callWriterClass, "calls");
13237748cd8SNickeau            if ($rp->isPrivate()) {
13337748cd8SNickeau                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);
13437748cd8SNickeau                return;
13537748cd8SNickeau            }
13637748cd8SNickeau        } catch (\ReflectionException $e) {
13737748cd8SNickeau            $callsPropertyFromCallWriterExists = false;
13837748cd8SNickeau        }
13937748cd8SNickeau
14037748cd8SNickeau        /**
14137748cd8SNickeau         * The calls
14237748cd8SNickeau         */
14337748cd8SNickeau        if ($callsPropertyFromCallWriterExists) {
14437748cd8SNickeau
14537748cd8SNickeau            $writerCalls = &$callWriter->calls;
14637748cd8SNickeau            $this->callStack = &$writerCalls;
14737748cd8SNickeau            $this->callStackType = self::CALLSTACK_WRITER;
14837748cd8SNickeau
14937748cd8SNickeau        } else {
15037748cd8SNickeau
15137748cd8SNickeau            /**
15237748cd8SNickeau             * Check the calls property of the handler
15337748cd8SNickeau             */
15437748cd8SNickeau            $handlerClass = get_class($handler);
15537748cd8SNickeau            try {
15637748cd8SNickeau                $rp = new \ReflectionProperty($handlerClass, "calls");
15737748cd8SNickeau                if ($rp->isPrivate()) {
15837748cd8SNickeau                    LogUtility::msg("$headErrorMessage. The handler ($handlerClass) provided cannot manipulate the callstack (ie the calls of the handler are private).", LogUtility::LVL_MSG_ERROR);
15937748cd8SNickeau                    return;
16037748cd8SNickeau                }
16137748cd8SNickeau            } catch (\ReflectionException $e) {
16237748cd8SNickeau                LogUtility::msg("$headErrorMessage. The handler ($handlerClass) provided cannot manipulate the callstack (ie the handler does not have any calls property).", LogUtility::LVL_MSG_ERROR);
16337748cd8SNickeau                return;
16437748cd8SNickeau            }
16537748cd8SNickeau
16637748cd8SNickeau            /**
16737748cd8SNickeau             * Initiate the callstack
16837748cd8SNickeau             */
16937748cd8SNickeau            $this->callStack = &$handler->calls;
17037748cd8SNickeau            $this->callStackType = self::CALLSTACK_MAIN;
17137748cd8SNickeau
17237748cd8SNickeau        }
17337748cd8SNickeau
17437748cd8SNickeau        $this->maxIndex = ArrayUtility::array_key_last($this->callStack);
17537748cd8SNickeau        $this->moveToEnd();
17637748cd8SNickeau
17737748cd8SNickeau
17837748cd8SNickeau    }
17937748cd8SNickeau
18037748cd8SNickeau    public
18137748cd8SNickeau    static function createFromMarkup($marki)
18237748cd8SNickeau    {
18337748cd8SNickeau
18437748cd8SNickeau        $modes = p_get_parsermodes();
18537748cd8SNickeau        $handler = new Doku_Handler();
18637748cd8SNickeau        $parser = new Parser($handler);
18737748cd8SNickeau
18837748cd8SNickeau        //add modes to parser
18937748cd8SNickeau        foreach ($modes as $mode) {
19037748cd8SNickeau            $parser->addMode($mode['mode'], $mode['obj']);
19137748cd8SNickeau        }
19237748cd8SNickeau        $parser->parse($marki);
19337748cd8SNickeau        return self::createFromHandler($handler);
19437748cd8SNickeau
19537748cd8SNickeau    }
19637748cd8SNickeau
19737748cd8SNickeau    /**
19837748cd8SNickeau     * Reset the pointer
19937748cd8SNickeau     */
20037748cd8SNickeau    public
20137748cd8SNickeau    function closeAndResetPointer()
20237748cd8SNickeau    {
20337748cd8SNickeau        reset($this->callStack);
20437748cd8SNickeau    }
20537748cd8SNickeau
20637748cd8SNickeau    /**
20737748cd8SNickeau     * Delete from the call stack
20837748cd8SNickeau     * @param $calls
20937748cd8SNickeau     * @param $start
21037748cd8SNickeau     * @param $end
21137748cd8SNickeau     */
21237748cd8SNickeau    public
21337748cd8SNickeau    static function deleteCalls(&$calls, $start, $end)
21437748cd8SNickeau    {
21537748cd8SNickeau        for ($i = $start; $i <= $end; $i++) {
21637748cd8SNickeau            unset($calls[$i]);
21737748cd8SNickeau        }
21837748cd8SNickeau    }
21937748cd8SNickeau
22037748cd8SNickeau    /**
22137748cd8SNickeau     * @param array $calls
22237748cd8SNickeau     * @param integer $position
22337748cd8SNickeau     * @param array $callStackToInsert
22437748cd8SNickeau     */
22537748cd8SNickeau    public
22637748cd8SNickeau    static function insertCallStackUpWards(&$calls, $position, $callStackToInsert)
22737748cd8SNickeau    {
22837748cd8SNickeau
22937748cd8SNickeau        array_splice($calls, $position, 0, $callStackToInsert);
23037748cd8SNickeau
23137748cd8SNickeau    }
23237748cd8SNickeau
23337748cd8SNickeau    /**
23437748cd8SNickeau     * A callstack pointer based implementation
23537748cd8SNickeau     * that starts at the end
23637748cd8SNickeau     * @param Doku_Handler $handler
23737748cd8SNickeau     * @return CallStack
23837748cd8SNickeau     */
23937748cd8SNickeau    public
24037748cd8SNickeau    static function createFromHandler(&$handler)
24137748cd8SNickeau    {
24237748cd8SNickeau        return new CallStack($handler);
24337748cd8SNickeau    }
24437748cd8SNickeau
24537748cd8SNickeau
24637748cd8SNickeau    /**
24737748cd8SNickeau     * Process the EOL call to the end of stack
24837748cd8SNickeau     * replacing them with paragraph call
24937748cd8SNickeau     *
25037748cd8SNickeau     * A sort of {@link Block::process()} but only from a tag
25137748cd8SNickeau     * to the end of the current stack
25237748cd8SNickeau     *
25337748cd8SNickeau     * This function is used basically in the {@link DOKU_LEXER_EXIT}
25437748cd8SNickeau     * state of {@link SyntaxPlugin::handle()} to create paragraph
25537748cd8SNickeau     * with the class given as parameter
25637748cd8SNickeau     *
25737748cd8SNickeau     * @param $attributes - the attributes in an callstack array form passed to the paragraph
25837748cd8SNickeau     */
25937748cd8SNickeau    public
26037748cd8SNickeau    function processEolToEndStack($attributes = [])
26137748cd8SNickeau    {
26237748cd8SNickeau
26337748cd8SNickeau        \syntax_plugin_combo_para::fromEolToParagraphUntilEndOfStack($this, $attributes);
26437748cd8SNickeau
26537748cd8SNickeau    }
26637748cd8SNickeau
26737748cd8SNickeau    /**
26837748cd8SNickeau     * Delete the call where the pointer is
26937748cd8SNickeau     * And go to the previous position
27037748cd8SNickeau     *
27137748cd8SNickeau     * This function can be used in a next loop
27237748cd8SNickeau     *
27337748cd8SNickeau     * @return Call the deleted call
27437748cd8SNickeau     */
27537748cd8SNickeau    public
2761fa8c418SNickeau    function deleteActualCallAndPrevious(): ?Call
27737748cd8SNickeau    {
27837748cd8SNickeau
27937748cd8SNickeau        $actualCall = $this->getActualCall();
28037748cd8SNickeau
28137748cd8SNickeau        $offset = $this->getActualOffset();
28237748cd8SNickeau        array_splice($this->callStack, $offset, 1, []);
28337748cd8SNickeau
28437748cd8SNickeau        /**
28537748cd8SNickeau         * Move to the next element (array splice reset the pointer)
28637748cd8SNickeau         * if there is a eol as, we delete it
28737748cd8SNickeau         * otherwise we may end up with two eol
28837748cd8SNickeau         * and this is an empty paragraph
28937748cd8SNickeau         */
29037748cd8SNickeau        $this->moveToOffset($offset);
29137748cd8SNickeau        if (!$this->isPointerAtEnd()) {
29237748cd8SNickeau            if ($this->getActualCall()->getTagName() == 'eol') {
29337748cd8SNickeau                array_splice($this->callStack, $offset, 1, []);
29437748cd8SNickeau            }
29537748cd8SNickeau        }
29637748cd8SNickeau
29737748cd8SNickeau        /**
29837748cd8SNickeau         * Move to the previous element
29937748cd8SNickeau         */
30037748cd8SNickeau        $this->moveToOffset($offset - 1);
30137748cd8SNickeau
30237748cd8SNickeau        return $actualCall;
30337748cd8SNickeau
30437748cd8SNickeau    }
30537748cd8SNickeau
30637748cd8SNickeau    /**
30737748cd8SNickeau     * @return Call - get a reference to the actual call
30837748cd8SNickeau     * This function returns a {@link Call call} object
30937748cd8SNickeau     * by reference, meaning that every update will also modify the element
31037748cd8SNickeau     * in the stack
31137748cd8SNickeau     */
31237748cd8SNickeau    public
31337748cd8SNickeau    function getActualCall()
31437748cd8SNickeau    {
31537748cd8SNickeau        if ($this->endWasReached) {
31637748cd8SNickeau            LogUtility::msg("The actual call cannot be ask because the end of the stack was reached", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
31737748cd8SNickeau            return null;
31837748cd8SNickeau        }
31937748cd8SNickeau        if ($this->startWasReached) {
32037748cd8SNickeau            LogUtility::msg("The actual call cannot be ask because the start of the stack was reached", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
32137748cd8SNickeau            return null;
32237748cd8SNickeau        }
32337748cd8SNickeau        $actualCallKey = key($this->callStack);
32437748cd8SNickeau        $actualCallArray = &$this->callStack[$actualCallKey];
32537748cd8SNickeau        return new Call($actualCallArray, $actualCallKey);
32637748cd8SNickeau
32737748cd8SNickeau    }
32837748cd8SNickeau
32937748cd8SNickeau    /**
33037748cd8SNickeau     * put the pointer one position further
33137748cd8SNickeau     * false if at the end
33237748cd8SNickeau     * @return false|Call
33337748cd8SNickeau     */
33437748cd8SNickeau    public
33537748cd8SNickeau    function next()
33637748cd8SNickeau    {
33737748cd8SNickeau        if ($this->startWasReached) {
33837748cd8SNickeau            $this->startWasReached = false;
33937748cd8SNickeau            $result = reset($this->callStack);
34037748cd8SNickeau            if ($result === false) {
34137748cd8SNickeau                return false;
34237748cd8SNickeau            } else {
34337748cd8SNickeau                return $this->getActualCall();
34437748cd8SNickeau            }
34537748cd8SNickeau        } else {
34637748cd8SNickeau            $next = next($this->callStack);
34737748cd8SNickeau            if ($next === false) {
34837748cd8SNickeau                $this->endWasReached = true;
34937748cd8SNickeau                return $next;
35037748cd8SNickeau            } else {
35137748cd8SNickeau                return $this->getActualCall();
35237748cd8SNickeau            }
35337748cd8SNickeau        }
35437748cd8SNickeau
35537748cd8SNickeau    }
35637748cd8SNickeau
35737748cd8SNickeau    /**
35837748cd8SNickeau     *
35937748cd8SNickeau     * From an exit call, move the corresponding Opening call
36037748cd8SNickeau     *
36137748cd8SNickeau     * This is used mostly in {@link SyntaxPlugin::handle()} from a {@link DOKU_LEXER_EXIT}
36237748cd8SNickeau     * to retrieve the {@link DOKU_LEXER_ENTER} call
36337748cd8SNickeau     *
36437748cd8SNickeau     * @return bool|Call
36537748cd8SNickeau     */
36637748cd8SNickeau    public
36737748cd8SNickeau    function moveToPreviousCorrespondingOpeningCall()
36837748cd8SNickeau    {
36937748cd8SNickeau
37037748cd8SNickeau        /**
37137748cd8SNickeau         * Edgde case
37237748cd8SNickeau         */
37337748cd8SNickeau        if (empty($this->callStack)) {
37437748cd8SNickeau            return false;
37537748cd8SNickeau        }
37637748cd8SNickeau
37737748cd8SNickeau        if (!$this->endWasReached) {
37837748cd8SNickeau            $actualCall = $this->getActualCall();
37937748cd8SNickeau            $actualState = $actualCall->getState();
38037748cd8SNickeau            if ($actualState != DOKU_LEXER_EXIT) {
38137748cd8SNickeau                /**
38237748cd8SNickeau                 * Check if we are at the end of the stack
38337748cd8SNickeau                 */
38437748cd8SNickeau                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");
38537748cd8SNickeau                return false;
38637748cd8SNickeau            }
38737748cd8SNickeau        }
38837748cd8SNickeau        $level = 0;
38937748cd8SNickeau        while ($actualCall = $this->previous()) {
39037748cd8SNickeau
39137748cd8SNickeau            $state = $actualCall->getState();
39237748cd8SNickeau            switch ($state) {
39337748cd8SNickeau                case DOKU_LEXER_ENTER:
39437748cd8SNickeau                    $level++;
39537748cd8SNickeau                    break;
39637748cd8SNickeau                case DOKU_LEXER_EXIT:
39737748cd8SNickeau                    $level--;
39837748cd8SNickeau                    break;
39937748cd8SNickeau            }
40037748cd8SNickeau            if ($level > 0) {
40137748cd8SNickeau                break;
40237748cd8SNickeau            }
40337748cd8SNickeau
40437748cd8SNickeau        }
40537748cd8SNickeau        if ($level > 0) {
40637748cd8SNickeau            return $actualCall;
40737748cd8SNickeau        } else {
40837748cd8SNickeau            return false;
40937748cd8SNickeau        }
41037748cd8SNickeau    }
41137748cd8SNickeau
41237748cd8SNickeau
41337748cd8SNickeau    /**
41437748cd8SNickeau     * @return Call|false the previous call or false if there is no more previous call
41537748cd8SNickeau     */
41637748cd8SNickeau    public
41737748cd8SNickeau    function previous()
41837748cd8SNickeau    {
41937748cd8SNickeau        if ($this->endWasReached) {
42037748cd8SNickeau            $this->endWasReached = false;
42137748cd8SNickeau            $return = end($this->callStack);
42237748cd8SNickeau            if ($return == false) {
42337748cd8SNickeau                // empty array (first call on the stack)
42437748cd8SNickeau                return false;
42537748cd8SNickeau            } else {
42637748cd8SNickeau                return $this->getActualCall();
42737748cd8SNickeau            }
42837748cd8SNickeau        } else {
42937748cd8SNickeau            $prev = prev($this->callStack);
43037748cd8SNickeau            if ($prev === false) {
43137748cd8SNickeau                $this->startWasReached = true;
43237748cd8SNickeau                return $prev;
43337748cd8SNickeau            } else {
43437748cd8SNickeau                return $this->getActualCall();
43537748cd8SNickeau            }
43637748cd8SNickeau        }
43737748cd8SNickeau
43837748cd8SNickeau    }
43937748cd8SNickeau
44037748cd8SNickeau    /**
44137748cd8SNickeau     * Return the first enter or special child call (ie a tag)
44237748cd8SNickeau     * @return Call|false
44337748cd8SNickeau     */
44437748cd8SNickeau    public
44537748cd8SNickeau    function moveToFirstChildTag()
44637748cd8SNickeau    {
44737748cd8SNickeau        $found = false;
44837748cd8SNickeau        while ($this->next()) {
44937748cd8SNickeau
45037748cd8SNickeau            $actualCall = $this->getActualCall();
45137748cd8SNickeau            $state = $actualCall->getState();
45237748cd8SNickeau            switch ($state) {
45337748cd8SNickeau                case DOKU_LEXER_ENTER:
45437748cd8SNickeau                case DOKU_LEXER_SPECIAL:
45537748cd8SNickeau                    $found = true;
45637748cd8SNickeau                    break 2;
45737748cd8SNickeau                case DOKU_LEXER_EXIT:
45837748cd8SNickeau                    break 2;
45937748cd8SNickeau            }
46037748cd8SNickeau
46137748cd8SNickeau        }
46237748cd8SNickeau        if ($found) {
46337748cd8SNickeau            return $this->getActualCall();
46437748cd8SNickeau        } else {
46537748cd8SNickeau            return false;
46637748cd8SNickeau        }
46737748cd8SNickeau
46837748cd8SNickeau
46937748cd8SNickeau    }
47037748cd8SNickeau
47137748cd8SNickeau    /**
47237748cd8SNickeau     * The end is the one after the last element
47337748cd8SNickeau     */
47437748cd8SNickeau    public
47537748cd8SNickeau    function moveToEnd()
47637748cd8SNickeau    {
47737748cd8SNickeau        if ($this->startWasReached) {
47837748cd8SNickeau            $this->startWasReached = false;
47937748cd8SNickeau        }
48037748cd8SNickeau        end($this->callStack);
48137748cd8SNickeau        $this->next();
48237748cd8SNickeau    }
48337748cd8SNickeau
48437748cd8SNickeau    /**
48537748cd8SNickeau     * On the same level
48637748cd8SNickeau     */
48737748cd8SNickeau    public
48837748cd8SNickeau    function moveToNextSiblingTag()
48937748cd8SNickeau    {
49037748cd8SNickeau
49137748cd8SNickeau        /**
49237748cd8SNickeau         * Edgde case
49337748cd8SNickeau         */
49437748cd8SNickeau        if (empty($this->callStack)) {
49537748cd8SNickeau            return false;
49637748cd8SNickeau        }
49737748cd8SNickeau
49837748cd8SNickeau        $actualCall = $this->getActualCall();
49937748cd8SNickeau        $actualState = $actualCall->getState();
50037748cd8SNickeau        if (!in_array($actualState, CallStack::TAG_STATE)) {
50137748cd8SNickeau            LogUtility::msg("A next sibling can be asked only from a tag call. The state is " . $actualState, LogUtility::LVL_MSG_ERROR, "support");
50237748cd8SNickeau            return false;
50337748cd8SNickeau        }
50437748cd8SNickeau        $level = 0;
50537748cd8SNickeau        while ($this->next()) {
50637748cd8SNickeau
50737748cd8SNickeau            $actualCall = $this->getActualCall();
50837748cd8SNickeau            $state = $actualCall->getState();
50937748cd8SNickeau            switch ($state) {
51037748cd8SNickeau                case DOKU_LEXER_ENTER:
51137748cd8SNickeau                case DOKU_LEXER_SPECIAL:
51237748cd8SNickeau                    $level++;
51337748cd8SNickeau                    break;
51437748cd8SNickeau                case DOKU_LEXER_EXIT:
51537748cd8SNickeau                    $level--;
51637748cd8SNickeau                    break;
51737748cd8SNickeau            }
51837748cd8SNickeau
51937748cd8SNickeau            if ($level == 0 && in_array($state, self::TAG_STATE)) {
52037748cd8SNickeau                break;
52137748cd8SNickeau            }
52237748cd8SNickeau        }
52337748cd8SNickeau        if ($level == 0 && !$this->endWasReached) {
52437748cd8SNickeau            return $this->getActualCall();
52537748cd8SNickeau        } else {
52637748cd8SNickeau            return false;
52737748cd8SNickeau        }
52837748cd8SNickeau    }
52937748cd8SNickeau
53037748cd8SNickeau    /**
53137748cd8SNickeau     * @param Call $call
53237748cd8SNickeau     * @return Call the inserted call
53337748cd8SNickeau     */
53437748cd8SNickeau    public
5351fa8c418SNickeau    function insertBefore(Call $call): Call
53637748cd8SNickeau    {
53737748cd8SNickeau        if ($this->endWasReached) {
53837748cd8SNickeau
53937748cd8SNickeau            $this->callStack[] = $call->toCallArray();
54037748cd8SNickeau
54137748cd8SNickeau        } else {
54237748cd8SNickeau
54337748cd8SNickeau            $offset = $this->getActualOffset();
54437748cd8SNickeau            array_splice($this->callStack, $offset, 0, [$call->toCallArray()]);
54537748cd8SNickeau            // array splice reset the pointer
54637748cd8SNickeau            // we move it to the actual element (ie the key is offset +1)
54737748cd8SNickeau            $this->moveToOffset($offset + 1);
54837748cd8SNickeau
54937748cd8SNickeau        }
55037748cd8SNickeau        return $call;
55137748cd8SNickeau    }
55237748cd8SNickeau
55337748cd8SNickeau    /**
55437748cd8SNickeau     * Move pointer by offset
55537748cd8SNickeau     * @param $offset
55637748cd8SNickeau     */
55737748cd8SNickeau    private
55837748cd8SNickeau    function moveToOffset($offset)
55937748cd8SNickeau    {
56037748cd8SNickeau        $this->resetPointer();
56137748cd8SNickeau        for ($i = 0; $i < $offset; $i++) {
56237748cd8SNickeau            $result = $this->next();
56337748cd8SNickeau            if ($result === false) {
56437748cd8SNickeau                break;
56537748cd8SNickeau            }
56637748cd8SNickeau        }
56737748cd8SNickeau    }
56837748cd8SNickeau
56937748cd8SNickeau    /**
57037748cd8SNickeau     * Move pointer by key
57137748cd8SNickeau     * @param $targetKey
57237748cd8SNickeau     */
57337748cd8SNickeau    private
57437748cd8SNickeau    function moveToKey($targetKey)
57537748cd8SNickeau    {
57637748cd8SNickeau        $this->resetPointer();
57737748cd8SNickeau        for ($i = 0; $i < $targetKey; $i++) {
57837748cd8SNickeau            next($this->callStack);
57937748cd8SNickeau        }
58037748cd8SNickeau        $actualKey = key($this->callStack);
58137748cd8SNickeau        if ($actualKey != $targetKey) {
58237748cd8SNickeau            LogUtility::msg("The target key ($targetKey) is not equal to the actual key ($actualKey). The moveToKey was not successful");
58337748cd8SNickeau        }
58437748cd8SNickeau    }
58537748cd8SNickeau
58637748cd8SNickeau    /**
58737748cd8SNickeau     * Insert After. The pointer stays at the current state.
58837748cd8SNickeau     * If you don't need to process the call that you just
58937748cd8SNickeau     * inserted, you may want to call {@link CallStack::next()}
59037748cd8SNickeau     * @param Call $call
59137748cd8SNickeau     */
59237748cd8SNickeau    public
59337748cd8SNickeau    function insertAfter($call)
59437748cd8SNickeau    {
59537748cd8SNickeau        $actualKey = key($this->callStack);
59637748cd8SNickeau        if ($actualKey == null) {
59737748cd8SNickeau            if ($this->endWasReached == true) {
59837748cd8SNickeau                $this->callStack[] = $call->toCallArray();
59937748cd8SNickeau            } else {
60037748cd8SNickeau                LogUtility::msg("Callstack: Actual key is null, we can't insert after null");
60137748cd8SNickeau            }
60237748cd8SNickeau        } else {
60337748cd8SNickeau            $offset = array_search($actualKey, array_keys($this->callStack), true);
60437748cd8SNickeau            array_splice($this->callStack, $offset + 1, 0, [$call->toCallArray()]);
60537748cd8SNickeau            // array splice reset the pointer
60637748cd8SNickeau            // we move it to the actual element
60737748cd8SNickeau            $this->moveToKey($actualKey);
60837748cd8SNickeau        }
60937748cd8SNickeau    }
61037748cd8SNickeau
61137748cd8SNickeau    public
61237748cd8SNickeau    function getActualKey()
61337748cd8SNickeau    {
61437748cd8SNickeau        return key($this->callStack);
61537748cd8SNickeau    }
61637748cd8SNickeau
61737748cd8SNickeau    /**
61837748cd8SNickeau     * Insert an EOL call if the next call is not an EOL
61937748cd8SNickeau     * This is to enforce an new paragraph
62037748cd8SNickeau     */
62137748cd8SNickeau    public
62237748cd8SNickeau    function insertEolIfNextCallIsNotEolOrBlock()
62337748cd8SNickeau    {
62437748cd8SNickeau        if (!$this->isPointerAtEnd()) {
62537748cd8SNickeau            $nextCall = $this->next();
62637748cd8SNickeau            if ($nextCall != false) {
62737748cd8SNickeau                if ($nextCall->getTagName() != "eol" && $nextCall->getDisplay() != "block") {
62837748cd8SNickeau                    $this->insertBefore(
62937748cd8SNickeau                        Call::createNativeCall("eol")
63037748cd8SNickeau                    );
63137748cd8SNickeau                    // move on the eol
63237748cd8SNickeau                    $this->previous();
63337748cd8SNickeau                }
63437748cd8SNickeau                // move back
63537748cd8SNickeau                $this->previous();
63637748cd8SNickeau            }
63737748cd8SNickeau        }
63837748cd8SNickeau    }
63937748cd8SNickeau
64037748cd8SNickeau    private
64137748cd8SNickeau    function isPointerAtEnd()
64237748cd8SNickeau    {
64337748cd8SNickeau        return $this->endWasReached;
64437748cd8SNickeau    }
64537748cd8SNickeau
64637748cd8SNickeau    public
64737748cd8SNickeau    function &getHandler()
64837748cd8SNickeau    {
64937748cd8SNickeau        return $this->handler;
65037748cd8SNickeau    }
65137748cd8SNickeau
65237748cd8SNickeau    /**
65337748cd8SNickeau     * Return The offset (not the key):
65437748cd8SNickeau     *   * starting at 0 for the first element
65537748cd8SNickeau     *   * 1 for the second ...
65637748cd8SNickeau     *
65737748cd8SNickeau     * @return false|int|string
65837748cd8SNickeau     */
65937748cd8SNickeau    private
66037748cd8SNickeau    function getActualOffset()
66137748cd8SNickeau    {
66237748cd8SNickeau        $actualKey = key($this->callStack);
66337748cd8SNickeau        return array_search($actualKey, array_keys($this->callStack), true);
66437748cd8SNickeau    }
66537748cd8SNickeau
66637748cd8SNickeau    private
66737748cd8SNickeau    function resetPointer()
66837748cd8SNickeau    {
66937748cd8SNickeau        reset($this->callStack);
67037748cd8SNickeau        $this->endWasReached = false;
67137748cd8SNickeau    }
67237748cd8SNickeau
67337748cd8SNickeau    public
67437748cd8SNickeau    function moveToStart()
67537748cd8SNickeau    {
67637748cd8SNickeau        $this->resetPointer();
67737748cd8SNickeau        $this->previous();
67837748cd8SNickeau    }
67937748cd8SNickeau
68037748cd8SNickeau    /**
68137748cd8SNickeau     * @return Call|false the parent call or false if there is no parent
68237748cd8SNickeau     * If you are on an {@link DOKU_LEXER_EXIT} state, you should
68337748cd8SNickeau     * call first the {@link CallStack::moveToPreviousCorrespondingOpeningCall()}
68437748cd8SNickeau     */
68537748cd8SNickeau    public function moveToParent()
68637748cd8SNickeau    {
68737748cd8SNickeau
68837748cd8SNickeau        /**
68937748cd8SNickeau         * Case when we start from the exit state element
69037748cd8SNickeau         * We go first to the opening tag
69137748cd8SNickeau         * because the algorithm is level based.
69237748cd8SNickeau         *
69337748cd8SNickeau         * When the end is reached, there is no call
69437748cd8SNickeau         * (this not the {@link end php end} but one further
69537748cd8SNickeau         */
69637748cd8SNickeau        if (!$this->endWasReached && !$this->startWasReached && $this->getActualCall()->getState() == DOKU_LEXER_EXIT) {
69737748cd8SNickeau
69837748cd8SNickeau            $this->moveToPreviousCorrespondingOpeningCall();
69937748cd8SNickeau
70037748cd8SNickeau        }
70137748cd8SNickeau
70237748cd8SNickeau
70337748cd8SNickeau        /**
70437748cd8SNickeau         * We are in a parent when the tree level is negative
70537748cd8SNickeau         */
70637748cd8SNickeau        $treeLevel = 0;
70737748cd8SNickeau        while ($actualCall = $this->previous()) {
70837748cd8SNickeau
70937748cd8SNickeau            /**
71037748cd8SNickeau             * Add
71137748cd8SNickeau             * would become a parent on its enter state
71237748cd8SNickeau             */
71337748cd8SNickeau            $actualCallState = $actualCall->getState();
71437748cd8SNickeau            switch ($actualCallState) {
71537748cd8SNickeau                case DOKU_LEXER_ENTER:
71637748cd8SNickeau                    $treeLevel = $treeLevel - 1;
71737748cd8SNickeau                    break;
71837748cd8SNickeau                case DOKU_LEXER_EXIT:
71937748cd8SNickeau                    /**
72037748cd8SNickeau                     * When the tag has a sibling with an exit tag
72137748cd8SNickeau                     */
72237748cd8SNickeau                    $treeLevel = $treeLevel + 1;
72337748cd8SNickeau                    break;
72437748cd8SNickeau            }
72537748cd8SNickeau
72637748cd8SNickeau            /**
72737748cd8SNickeau             * The breaking statement
72837748cd8SNickeau             */
72937748cd8SNickeau            if ($treeLevel < 0) {
73037748cd8SNickeau                break;
73137748cd8SNickeau            }
73237748cd8SNickeau
73337748cd8SNickeau        }
73437748cd8SNickeau        return $actualCall;
73537748cd8SNickeau
73637748cd8SNickeau
73737748cd8SNickeau    }
73837748cd8SNickeau
73937748cd8SNickeau    /**
74037748cd8SNickeau     * Delete the anchor link to the image (ie the lightbox)
74137748cd8SNickeau     *
74237748cd8SNickeau     * This is used in navigation and for instance
74337748cd8SNickeau     * in heading
74437748cd8SNickeau     */
74537748cd8SNickeau    public function processNoLinkOnImageToEndStack()
74637748cd8SNickeau    {
74737748cd8SNickeau        while ($this->next()) {
74837748cd8SNickeau            $actualCall = $this->getActualCall();
74937748cd8SNickeau            if ($actualCall->getTagName() == syntax_plugin_combo_media::TAG) {
75037748cd8SNickeau                $actualCall->addAttribute(MediaLink::LINKING_KEY, MediaLink::LINKING_NOLINK_VALUE);
75137748cd8SNickeau            }
75237748cd8SNickeau        }
75337748cd8SNickeau    }
75437748cd8SNickeau
75537748cd8SNickeau    /**
75637748cd8SNickeau     * Append instructions to the callstack (ie at the end)
75737748cd8SNickeau     * @param array $instructions
75837748cd8SNickeau     */
75937748cd8SNickeau    public function appendInstructionsFromNativeArray($instructions)
76037748cd8SNickeau    {
76137748cd8SNickeau        array_splice($this->callStack, count($this->callStack), 0, $instructions);
76237748cd8SNickeau    }
76337748cd8SNickeau
76437748cd8SNickeau    /**
76537748cd8SNickeau     * @param Call $call
76637748cd8SNickeau     */
76737748cd8SNickeau    public function appendCallAtTheEnd($call)
76837748cd8SNickeau    {
76937748cd8SNickeau        $this->callStack[] = $call->toCallArray();
77037748cd8SNickeau    }
77137748cd8SNickeau
77237748cd8SNickeau    public function moveToPreviousSiblingTag()
77337748cd8SNickeau    {
77437748cd8SNickeau        /**
77537748cd8SNickeau         * Edge case
77637748cd8SNickeau         */
77737748cd8SNickeau        if (empty($this->callStack)) {
77837748cd8SNickeau            return false;
77937748cd8SNickeau        }
78037748cd8SNickeau
78137748cd8SNickeau        if (!$this->endWasReached) {
78237748cd8SNickeau            $actualCall = $this->getActualCall();
78337748cd8SNickeau            $actualState = $actualCall->getState();
78437748cd8SNickeau            if (!in_array($actualState, CallStack::TAG_STATE)) {
78537748cd8SNickeau                LogUtility::msg("A previous sibling can be asked only from a tag call. The state is " . $actualState, LogUtility::LVL_MSG_ERROR, "support");
78637748cd8SNickeau                return false;
78737748cd8SNickeau            }
78837748cd8SNickeau        }
78937748cd8SNickeau        $level = 0;
79037748cd8SNickeau        while ($this->previous()) {
79137748cd8SNickeau
79237748cd8SNickeau            $actualCall = $this->getActualCall();
79337748cd8SNickeau            $state = $actualCall->getState();
79437748cd8SNickeau            switch ($state) {
79537748cd8SNickeau                case DOKU_LEXER_ENTER:
79637748cd8SNickeau                case DOKU_LEXER_SPECIAL:
79737748cd8SNickeau                    $level++;
79837748cd8SNickeau                    break;
79937748cd8SNickeau                case DOKU_LEXER_EXIT:
80037748cd8SNickeau                    $level--;
80137748cd8SNickeau                    break;
80237748cd8SNickeau            }
80337748cd8SNickeau
80437748cd8SNickeau            if ($level == 0 && in_array($state, self::TAG_STATE)) {
80537748cd8SNickeau                break;
80637748cd8SNickeau            }
80737748cd8SNickeau        }
80837748cd8SNickeau        if ($level == 0 && !$this->startWasReached) {
80937748cd8SNickeau            return $this->getActualCall();
81037748cd8SNickeau        } else {
81137748cd8SNickeau            return false;
81237748cd8SNickeau        }
81337748cd8SNickeau    }
81437748cd8SNickeau
81537748cd8SNickeau    /**
81637748cd8SNickeau     * Delete all calls after the passed call
81737748cd8SNickeau     *
81837748cd8SNickeau     * It's used in syntax generator that:
81937748cd8SNickeau     *   * capture the children callstack at the end,
82037748cd8SNickeau     *   * delete it
82137748cd8SNickeau     *   * and use it to generate more calls.
82237748cd8SNickeau     *
82337748cd8SNickeau     * @param Call $call
82437748cd8SNickeau     */
82537748cd8SNickeau    public function deleteAllCallsAfter(Call $call)
82637748cd8SNickeau    {
82737748cd8SNickeau        $key = $call->getKey();
82837748cd8SNickeau        $offset = array_search($key, array_keys($this->callStack), true);
82937748cd8SNickeau        if ($offset !== false) {
83037748cd8SNickeau            /**
83137748cd8SNickeau             * We delete from the next
83237748cd8SNickeau             * {@link array_splice()} delete also the given offset
83337748cd8SNickeau             */
83437748cd8SNickeau            array_splice($this->callStack, $offset + 1);
83537748cd8SNickeau        } else {
83637748cd8SNickeau            LogUtility::msg("The call ($call) could not be found in the callStack. We couldn't therefore delete the calls after");
83737748cd8SNickeau        }
83837748cd8SNickeau
83937748cd8SNickeau    }
84037748cd8SNickeau
84137748cd8SNickeau    /**
84237748cd8SNickeau     * @param Call[] $calls
84337748cd8SNickeau     */
84437748cd8SNickeau    public function appendInstructionsFromCallObjects($calls)
84537748cd8SNickeau    {
84637748cd8SNickeau        foreach ($calls as $call) {
84737748cd8SNickeau            $this->appendCallAtTheEnd($call);
84837748cd8SNickeau        }
849*c3437056SNickeau
850*c3437056SNickeau    }
851*c3437056SNickeau
852*c3437056SNickeau    /**
853*c3437056SNickeau     *
854*c3437056SNickeau     * @return int|mixed - the last position on the callstack
855*c3437056SNickeau     * If you are at the end of the callstack after a full parsing,
856*c3437056SNickeau     * this should be the length of the string of the page
857*c3437056SNickeau     */
858*c3437056SNickeau    public function getLastCharacterPosition()
859*c3437056SNickeau    {
860*c3437056SNickeau        $offset = $this->getActualOffset();
861*c3437056SNickeau
862*c3437056SNickeau        $lastEndPosition = 0;
863*c3437056SNickeau        $this->moveToEnd();
864*c3437056SNickeau        while ($actualCall = $this->previous()) {
865*c3437056SNickeau            // p_open and p_close have always a position value of 0
866*c3437056SNickeau            $lastEndPosition = $actualCall->getLastMatchedCharacterPosition();
867*c3437056SNickeau            if ($lastEndPosition !== 0) {
868*c3437056SNickeau                break;
869*c3437056SNickeau            }
870*c3437056SNickeau        }
871*c3437056SNickeau        if ($offset == null) {
872*c3437056SNickeau            $this->moveToEnd();
873*c3437056SNickeau        } else {
874*c3437056SNickeau            $this->moveToOffset($offset);
875*c3437056SNickeau        }
876*c3437056SNickeau        return $lastEndPosition;
87737748cd8SNickeau
87837748cd8SNickeau    }
87937748cd8SNickeau
88037748cd8SNickeau
88137748cd8SNickeau}
882