1<?php
2/**
3 * Copyright (c) 2020. 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
16require_once(__DIR__ . '/Call.php');
17
18use Doku_Handler;
19use dokuwiki\Extension\SyntaxPlugin;
20use dokuwiki\Parsing\Handler\CallWriter;
21use Exception;
22use RuntimeException;
23
24
25/**
26 * Class Tag
27 * @package ComboStrap
28 * This is class that have tree like function on tag level
29 * to match what's called a {@link Doku_Handler::$calls call}
30 *
31 * It's just a wrapper that adds tree functionality above the {@link CallStack}
32 *
33 * @deprecated Move to the {@link CallStack::createFromHandler()}
34 */
35class Tag
36{
37
38    const CANONICAL = "support";
39
40    /**
41     * Invisible Content tag (ie p (p_open/p_close))
42     * They are not seen in the content
43     * and are automatically generated
44     */
45    const INVISIBLE_CONTENT_TAG = ["p"];
46
47    /**
48     * The {@link Doku_Handler::$calls} or {@link CallWriter::$calls}
49     * @var
50     */
51    private $calls;
52
53    /**
54     * The parsed attributes for the tag
55     * @var
56     */
57    private $attributes;
58    /**
59     * The name of the tag
60     * @var
61     */
62    private $name;
63    /**
64     * The lexer state
65     * @var
66     */
67    private $state;
68    /**
69     * The position is the call stack
70     * @var int
71     */
72    private $actualPosition;
73    /**
74     * @var Doku_Handler
75     */
76    private $handler;
77    /**
78     *
79     * @var Call
80     * The tag call may be null if this is the actual tag
81     * in the handler process (ie not yet created in the stack)
82     */
83    private $tagCall;
84    /**
85     * The maximum key number position of the stack
86     * @var int|string|null
87     */
88    private $maxPosition;
89    /**
90     * @var string
91     */
92    private $callStackType;
93
94
95    /**
96     * Token constructor
97     * A token represent a call of {@link \Doku_Handler}
98     * It can be seen as a the counter part of the HTML tag.
99     *
100     * It has a state of:
101     *   * {@link DOKU_LEXER_ENTER} (open),
102     *   * {@link DOKU_LEXER_UNMATCHED} (unmatched content),
103     *   * {@link DOKU_LEXER_EXIT} (closed)
104     *
105     * @param $name
106     * @param $attributes
107     * @param $state
108     * @param Doku_Handler $handler - A reference to the dokuwiki handler
109     * @param null $position - The key (position) in the call stack of null if it's the HEAD tag (The tag that is created from the data of the {@link SyntaxPlugin::render()}
110     */
111    public function __construct($name, $attributes, $state, &$handler, $position = null, $callStackType = null)
112    {
113        $this->name = $name;
114        $this->state = $state;
115
116        /**
117         * Callstack
118         */
119        $writerCalls = &$handler->getCallWriter()->calls;
120        if ($position == null) {
121            /**
122             * A temporary Call stack is created in the writer
123             * for list, table, blockquote
124             */
125            if (!empty($writerCalls)) {
126                $this->calls = &$writerCalls;
127                $this->callStackType = CallStack::CALLSTACK_WRITER;
128            } else {
129                $this->calls = &$handler->calls;
130                $this->callStackType = CallStack::CALLSTACK_MAIN;
131            }
132        } else {
133            if ($callStackType == null) {
134                LogUtility::msg("When the position is set, the callstack type should be given", LogUtility::LVL_MSG_ERROR);
135            }
136            $this->callStackType = $callStackType;
137            if ($callStackType == CallStack::CALLSTACK_MAIN) {
138                $this->calls = &$handler->calls;
139            } else {
140                $this->calls = &$writerCalls;
141            }
142        }
143
144        $this->handler = &$handler;
145        $this->maxPosition = ArrayUtility::array_key_last($this->calls);
146        if ($position !== null) {
147            $this->actualPosition = $position;
148            /**
149             * A shortcut access variable to the call of the tag
150             */
151            if (isset($this->calls[$this->actualPosition])) {
152                $this->tagCall = $this->createCallObjectFromIndex($this->actualPosition);
153                $this->attributes = $this->tagCall->getAttributes();
154            }
155
156        } else {
157            // The tag is not yet in the stack
158
159            // Get the last position
160            // We use get_last and not sizeof
161            // because some plugin may delete element of the stack
162            $this->actualPosition = ArrayUtility::array_key_last($this->calls) + 1;
163            if ($attributes == null) {
164                $this->attributes = array();
165            } else {
166                $this->attributes = $attributes;
167            }
168        }
169
170
171    }
172
173    public static function createDocumentStartFromHandler(Doku_Handler &$handler)
174    {
175        return new Tag("document_start", [], DOKU_LEXER_ENTER, $handler, 0);
176    }
177
178    private function createCallObjectFromIndex($i)
179    {
180        if (isset($this->calls[$i])) {
181            return new Call($this->calls[$i]);
182        } else {
183            LogUtility::msg("There is no call at the index ($i)", LogUtility::LVL_MSG_ERROR);
184            return null;
185        }
186
187    }
188
189
190    /**
191     * The lexer state
192     *   DOKU_LEXER_ENTER = 1
193     *   DOKU_LEXER_MATCHED = 2
194     *   DOKU_LEXER_UNMATCHED = 3
195     *   DOKU_LEXER_EXIT = 4
196     *   DOKU_LEXER_SPECIAL = 5
197     * @return mixed
198     */
199    public function getState()
200    {
201        return $this->state;
202    }
203
204    /**
205     * From a call position to a tag
206     * @param $position - the position in the call stack (ie in the array)
207     * @return Tag
208     */
209    public function createFromCall($position)
210    {
211
212        if (!isset($this->calls[$position])) {
213            LogUtility::msg("The index ($position) does not exist in the call stack, cannot create a call", LogUtility::LVL_MSG_ERROR);
214            return null;
215        }
216
217        $callArray = &$this->calls[$position];
218        $call = new Call($callArray);
219
220        $attributes = $call->getAttributes();
221        $name = $call->getTagName();
222        $state = $call->getState();
223
224        /**
225         * Getting attributes
226         * If we don't have already the attributes
227         * in the returned array of the handler,
228         * (ie the full HTML was given for instance)
229         * we parse the match again
230         */
231        if ($attributes == null
232            && $call->getState() == DOKU_LEXER_ENTER
233            && $name != 'preformatted' // preformatted does not have any attributes
234        ) {
235            $match = $call->getCapturedContent();
236            /**
237             * If this is not a combo element, we got no match
238             */
239            if ($match != null) {
240                $attributes = PluginUtility::getTagAttributes($match);
241            }
242        }
243
244        return new Tag($name, $attributes, $state, $this->handler, $position, $this->callStackType);
245    }
246
247    public function isChildOf($tag)
248    {
249        $componentNode = $this->getParent();
250        return $componentNode !== false ? $componentNode->getName() === $tag : false;
251    }
252
253    /**
254     * To determine if there is no content
255     * between the child and the parent
256     * @return bool
257     */
258    public function hasSiblings()
259    {
260        if ($this->getPreviousSibling() === null) {
261            return false;
262        } else {
263            return true;
264        }
265
266    }
267
268    /**
269     * Return the parent node or false if root
270     *
271     * The node should not be in an exit node
272     * (If this is the case, use the function {@link Tag::getOpeningTag()} first
273     *
274     * @return bool|Tag
275     */
276    public function getParent()
277    {
278        /**
279         * Case when we start from the exit state element
280         * We go first to the opening tag
281         * because the algorithm is level based.
282         */
283        if ($this->state == DOKU_LEXER_EXIT) {
284
285            return $this->getOpeningTag()->getParent();
286
287        } else {
288
289            /**
290             * Start to the actual call minus one
291             */
292            $callStackPosition = $this->actualPosition - 1;
293            $treeLevel = 0;
294
295            /**
296             * We are in a parent when the tree level is negative
297             */
298            while ($callStackPosition >= 0) {
299
300                $callStackPosition = $this->getPreviousPositionNonEmpty($callStackPosition);
301                if ($callStackPosition < 0) {
302                    break;
303                }
304
305                /**
306                 * Get the previous call
307                 */
308                $previousCallArray = $this->calls[$callStackPosition];
309                $previousCall = new Call($previousCallArray);
310                $parentCallState = $previousCall->getState();
311
312                /**
313                 * Add
314                 * would become a parent on its enter state
315                 */
316                switch ($parentCallState) {
317                    case DOKU_LEXER_ENTER:
318                        $treeLevel = $treeLevel - 1;
319                        break;
320                    case DOKU_LEXER_EXIT:
321                        /**
322                         * When the tag has a sibling with an exit tag
323                         */
324                        $treeLevel = $treeLevel + 1;
325                        break;
326                }
327
328                /**
329                 * The breaking statement
330                 */
331                if ($treeLevel >= 0) {
332                    $callStackPosition = $callStackPosition - 1;
333                    unset($previousCallArray);
334                } else {
335                    break;
336                }
337
338
339            }
340            if (isset($previousCallArray)) {
341                return $this->createFromCall($callStackPosition);
342            } else {
343                return false;
344            }
345        }
346    }
347
348    /**
349     * Return an attribute of the node or null if it does not exist
350     * @param string $name
351     * @return string the attribute value
352     */
353    public function getAttribute($name)
354    {
355        if (isset($this->attributes)) {
356            return $this->attributes[$name];
357        } else {
358            return null;
359        }
360
361    }
362
363    /**
364     * Return all attributes
365     * @return string[] the attributes
366     */
367    public function getAttributes()
368    {
369
370        return $this->attributes;
371
372    }
373
374    /**
375     * @return mixed - the name of the element (ie the opening tag)
376     */
377    public function getName()
378    {
379        if ($this->tagCall != null) {
380            return $this->tagCall->getTagName();
381        } else {
382            return $this->name;
383        }
384    }
385
386
387    /**
388     * @return string - the type attribute of the opening tag
389     */
390    public function getType()
391    {
392
393        if ($this->getState() == DOKU_LEXER_UNMATCHED) {
394            LogUtility::msg("The unmatched tag (" . $this->name . ") does not have any attributes. Get its parent if you want the type", LogUtility::LVL_MSG_ERROR);
395            return null;
396        } else {
397            return $this->getAttribute("type");
398        }
399    }
400
401    /**
402     * @param $tag
403     * @return int
404     */
405    public function isDescendantOf($tag)
406    {
407
408        for ($i = sizeof($this->calls) - 1; $i >= 0; $i--) {
409            if (isset($this->calls[$i])) {
410                $call = $this->createCallObjectFromIndex($i);
411                if ($call->getTagName() == "$tag") {
412                    return true;
413                }
414            }
415        }
416        return false;
417
418    }
419
420    /**
421     *
422     * @return null|Tag - the sibling tag (in ascendant order) or null
423     */
424    public function getPreviousSibling()
425    {
426
427        if ($this->actualPosition == 1) {
428            return null;
429        }
430
431        $counter = $this->actualPosition - 1;
432        $treeLevel = 0;
433        while ($counter > 0) {
434
435            $callArray = $this->calls[$counter];
436            $call = new Call($callArray);
437            $state = $call->getState();
438
439            /**
440             * Edge case
441             */
442            if ($state == null) {
443                if ($call->getTagName() == "acronym") {
444                    // Acronym does not have an enter/exit state
445                    //  this is a sibling
446                    break;
447                }
448            }
449
450            /**
451             * Before the breaking condition
452             * to take the case when the first call is an exit
453             */
454            switch ($state) {
455                case DOKU_LEXER_ENTER:
456                    $treeLevel = $treeLevel - 1;
457                    break;
458                case DOKU_LEXER_EXIT:
459                    /**
460                     * When the tag has a sibling with an exit tag
461                     */
462                    $treeLevel = $treeLevel + 1;
463                    break;
464                case DOKU_LEXER_UNMATCHED:
465                    if (empty(trim($call->getCapturedContent()))) {
466                        // An empty unmatched is not considered a sibling
467                        // state = null will continue the loop
468                        // we can't use a continue statement in a switch
469                        $state = null;
470                    }
471                    break;
472            }
473
474            /*
475             * Breaking conditions
476             * If we get above or on the same level
477             */
478            if ($treeLevel <= 0
479                && $state != null // eol state, strong close or empty tag
480            ) {
481                break;
482            } else {
483                $counter = $counter - 1;
484                unset($callArray);
485            }
486
487        }
488        /**
489         * Because we don't count tag without an state such as eol,
490         * the tree level may be negative which means
491         * that there is no sibling
492         */
493        if ($treeLevel == 0) {
494            return $this->createFromCall($counter);
495        }
496        return null;
497
498
499    }
500
501    public function hasParent()
502    {
503        return $this->getParent() !== false;
504    }
505
506
507    /**
508     * @return bool|Tag
509     * @deprecated use {@link CallStack::moveToPreviousCorrespondingOpeningCall()} instead
510     * @date 2021-05-13 deprecation date
511     */
512    public function getOpeningTag()
513    {
514        $descendantCounter = sizeof($this->calls) - 1;
515        while ($descendantCounter > 0) {
516
517            $previousCallArray = $this->calls[$descendantCounter];
518            $previousCall = new Call($previousCallArray);
519            $parentTagName = $previousCall->getTagName();
520            $state = $previousCall->getState();
521            if ($state === DOKU_LEXER_ENTER && $parentTagName === $this->getName()) {
522                break;
523            } else {
524                $descendantCounter = $descendantCounter - 1;
525                unset($previousCallArray);
526            }
527
528        }
529        if (isset($previousCallArray)) {
530            return $this->createFromCall($descendantCounter);
531        } else {
532            return false;
533        }
534    }
535
536    /**
537     * @return bool
538     * @throws Exception - if the tag is not an exit tag
539     */
540    public function hasDescendants()
541    {
542
543        if (sizeof($this->getDescendants()) > 0) {
544            return true;
545        } else {
546            return false;
547        }
548    }
549
550
551    /**
552     *
553     * @return Tag the first descendant that is not whitespace
554     * @deprecated use {@link CallStack::moveToFirstChildTag()}
555     * @date 2021-05-13
556     */
557    public function getFirstMeaningFullDescendant()
558    {
559        $descendants = $this->getDescendants();
560        $firstDescendant = $descendants[0];
561        if ($firstDescendant->getState() == DOKU_LEXER_UNMATCHED && trim($firstDescendant->getContentRecursively()) == "") {
562            return $descendants[1];
563        } else {
564            return $firstDescendant;
565        }
566    }
567
568    /**
569     * Descendant can only be run on enter tag
570     * @return Tag[]
571     */
572    public function getDescendants()
573    {
574
575        if ($this->state != DOKU_LEXER_ENTER) {
576            throw new RuntimeException("Descendant should be called on enter tag. Get the opening tag first if you are in a exit tag");
577        }
578        if (isset($this->actualPosition)) {
579            $index = $this->actualPosition + 1;
580        } else {
581            throw new RuntimeException("It seems that we are at the end of the stack because the position is not set");
582        }
583        $descendants = array();
584        while ($index <= sizeof($this->calls) - 1) {
585
586            $childCallArray = $this->calls[$index];
587            $childCall = new Call($childCallArray);
588            $childTagName = $childCall->getTagName();
589            $state = $childCall->getState();
590
591            /**
592             * We break when got to the exit tag
593             */
594            if ($state === DOKU_LEXER_EXIT && $childTagName === $this->getName()) {
595                break;
596            } else {
597                /**
598                 * We don't take the end of line
599                 */
600                if ($childCallArray[0] != "eol") {
601                    /**
602                     * We don't take text
603                     */
604                    //if ($state!=DOKU_LEXER_UNMATCHED) {
605                    $descendants[] = $this->createFromCall($index);
606                    //}
607                }
608                /**
609                 * Close
610                 */
611                $index = $index + 1;
612                unset($childCallArray);
613            }
614
615        }
616        return $descendants;
617    }
618
619    /**
620     * @param string $requiredTagName
621     * @return Tag|null
622     */
623    public function getDescendant($requiredTagName)
624    {
625        $tags = $this->getDescendants();
626        foreach ($tags as $tag) {
627            $currentTagName = $tag->getName();
628            if ($currentTagName === $requiredTagName) {
629                $currentTagState = $tag->getState();
630                /**
631                 * No unmatched tag
632                 */
633                if (
634                    $currentTagState === DOKU_LEXER_ENTER
635                    || $currentTagState === DOKU_LEXER_MATCHED
636                    || $currentTagState === DOKU_LEXER_SPECIAL
637                ) {
638                    return $tag;
639                } else {
640                    /**
641                     * Dokuwiki special match does not have any
642                     * state
643                     */
644                    if ($currentTagName == "internalmedia") {
645                        return $tag;
646                    }
647                }
648            }
649        }
650        return null;
651    }
652
653    /**
654     * @deprecated use {@link CallStack::deleteCall} instead
655     */
656    public function deleteCall()
657    {
658        /**
659         * The current call in an handle method cannot be deleted
660         * because it does not exist yet
661         */
662        if (!$this->isCurrent()) {
663            unset($this->calls[$this->actualPosition]);
664        } else {
665            LogUtility::msg("Internal error: The current call cannot be deleted", LogUtility::LVL_MSG_WARNING, self::CANONICAL);
666        }
667    }
668
669    /**
670     * Returned the matched content for this tag
671     */
672    public function getMatchedContent()
673    {
674        if ($this->tagCall != null) {
675
676            return $this->tagCall->getCapturedContent();
677        } else {
678            return null;
679        }
680    }
681
682    /**
683     *
684     * @return array|mixed - the data
685     */
686    public function getData()
687    {
688        if ($this->tagCall != null) {
689            return $this->tagCall->getPluginData();
690        } else {
691            return array();
692        }
693    }
694
695    /**
696     *
697     * @return array|mixed - the context (generally a tag name)
698     */
699    public function getContext()
700    {
701        if ($this->tagCall != null) {
702            $data = $this->tagCall->getPluginData();
703            return $data[PluginUtility::CONTEXT];
704        } else {
705            return array();
706        }
707    }
708
709
710    /**
711     * Return the content of a tag (the string between this tag)
712     * This function is generally called after a function that goes up on the stack
713     * such as {@link getDescendant}
714     * @return string
715     */
716    public function getContentRecursively()
717    {
718        $content = "";
719        $state = $this->getState();
720        switch ($state) {
721            case DOKU_LEXER_ENTER:
722                $index = $this->actualPosition + 1;
723                while ($index <= sizeof($this->calls) - 1) {
724
725                    $currentCallArray = $this->calls[$index];
726                    $currentCall = new Call($currentCallArray);
727                    if (
728                        $currentCall->getTagName() == $this->getName()
729                        &&
730                        $currentCall->getState() == DOKU_LEXER_EXIT
731                    ) {
732                        break;
733                    } else {
734                        $content .= $currentCall->getCapturedContent();
735                        $index++;
736                    }
737                }
738                break;
739            case DOKU_LEXER_UNMATCHED:
740            default:
741                $content = $this->getContent();
742                break;
743        }
744
745
746        return $content;
747
748    }
749
750    /**
751     * Return true if the tag is the first sibling
752     *
753     *
754     * @return boolean - true if this is the first sibling
755     */
756    public function isFirstMeaningFullSibling()
757    {
758        $sibling = $this->getPreviousSibling();
759        if ($sibling == null) {
760            return true;
761        } else {
762            /** Whitespace string */
763            if ($sibling->getState() == DOKU_LEXER_UNMATCHED && trim($sibling->getContentRecursively()) == "") {
764                $sibling = $sibling->getPreviousSibling();
765            }
766            if ($sibling == null) {
767                return true;
768            } else {
769                return false;
770            }
771        }
772
773    }
774
775    /**
776     * @param $key
777     * @param $value
778     * @return $this
779     */
780    public function addAttribute($key, $value)
781    {
782        $call = $this->createCallObjectFromIndex($this->actualPosition);
783        $call->addAttribute($key, $value);
784        return $this;
785    }
786
787    /**
788     * @param $value
789     * @return $this
790     */
791    public function setContext($value)
792    {
793        $call = $this->createCallObjectFromIndex($this->actualPosition);
794        $call->setContext($value);
795        return $this;
796    }
797
798    /**
799     * @param $key
800     * @param $value
801     * @return $this
802     */
803    public function setAttribute($key, $value)
804    {
805        $call = $this->createCallObjectFromIndex($this->actualPosition);
806        $call->addAttribute($key,$value);
807        return $this;
808    }
809
810    /**
811     * @param $key
812     * @return $this
813     */
814    public function unsetAttribute($key)
815    {
816        unset($this->calls[$this->actualPosition][1][1][PluginUtility::ATTRIBUTES][$key]);
817        return $this;
818    }
819
820    /**
821     * Add a class to an element
822     * @param $value
823     * @return $this
824     */
825    public function addClass($value)
826    {
827        $call = $this->createCallObjectFromIndex($this->actualPosition);
828        $classValue = $call->getAttribute("class");
829
830        if (empty($classValue)) {
831            $call->addAttribute("class", $value);
832        } else {
833            $call->addAttribute("class", $classValue . " " . $value);
834        }
835        return $this;
836
837    }
838
839    /**
840     * @param $value
841     * @return $this
842     */
843    public function setType($value)
844    {
845        $call = $this->createCallObjectFromIndex($this->actualPosition);
846        $call->addAttribute(TagAttributes::TYPE_KEY, $value);
847        return $this;
848    }
849
850    public function getActualPosition()
851    {
852        return $this->actualPosition;
853    }
854
855    public function getContent()
856    {
857        if ($this->tagCall != null) {
858            return $this->tagCall->getCapturedContent();
859        } else {
860            return null;
861        }
862    }
863
864    public function getDocumentStartTag()
865    {
866        if (sizeof($this->calls) > 0) {
867            return $this->createFromCall(0);
868        } else {
869            throw new RuntimeException("The stack is empty, there is no root tag");
870        }
871    }
872
873    /**
874     * The current call is the call being created
875     * in a {@link SyntaxPlugin::handle()}
876     * It does not exist yet in the call stack
877     * and cannot be deleted
878     * @return bool
879     */
880    private function isCurrent()
881    {
882        return $this->actualPosition == sizeof($this->calls);
883    }
884
885    public function removeAttributes()
886    {
887        if (!$this->isCurrent()) {
888            $call = $this->createCallObjectFromIndex($this->actualPosition);
889            $call->removeAttributes();
890        } else {
891            LogUtility::msg("Internal error: This is not logic to remove the attributes of the current node because it's not yet created, not yet in the stack. Don't add them to the constructor signature.", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
892        }
893    }
894
895    public function getNextOpeningTag($tagName)
896    {
897        $position = $this->actualPosition;
898        while ($this->toNextPositionNonEmpty($position)) {
899
900            $call = new Call($this->handler->calls[$position]);
901            if ($call->getTagName() == $tagName && $call->getState() == DOKU_LEXER_ENTER) {
902                return $this->createFromCall($position);
903            }
904
905        }
906        return null;
907    }
908
909    /**
910     *
911     * If element were deleted,
912     * the previous calls may be empty
913     * make sure that we got something
914     *
915     * @param $callStackPosition
916     * @return int|mixed
917     */
918    public function getPreviousPositionNonEmpty($callStackPosition)
919    {
920
921        while (!isset($this->calls[$callStackPosition]) && $callStackPosition > 0) {
922            $callStackPosition = $callStackPosition - 1;
923        }
924        return $callStackPosition;
925    }
926
927    /**
928     *
929     * Increment the callStackPosition to a non-empty call stack position
930     *
931     * Array index are sequence number that may be deleted. We get then empty gap.
932     * This function increments the pointer and return false if the end of the stack was reached
933     *
934     * @param $callStackPointer
935     * @return true|false - If this is the end of the stack, return false, otherwise return true
936     */
937    public function toNextPositionNonEmpty(&$callStackPointer)
938    {
939        $callStackPointer = $callStackPointer + 1;
940        while (!isset($this->calls[$callStackPointer]) && $callStackPointer < $this->maxPosition) {
941            $callStackPointer = $callStackPointer + 1;
942        }
943        if (!isset($this->calls[$callStackPointer])) {
944            return false;
945        } else {
946            return true;
947        }
948    }
949
950    public function getNextClosingTag($tagName)
951    {
952        $position = $this->actualPosition;
953        while ($this->toNextPositionNonEmpty($position)) {
954
955            $call = new Call($this->handler->calls[$position]);
956            if ($call->getTagName() == $tagName && $call->getState() == DOKU_LEXER_EXIT) {
957                return $this->createFromCall($position);
958            }
959
960        }
961        return null;
962    }
963
964    /**
965     * @return Tag|null the next children or null - the tag should be an enter tag
966     */
967    public function getNextChild()
968    {
969        if ($this->getState() !== DOKU_LEXER_ENTER) {
970            // The tag ($this) is not an enter tag and has therefore no children."
971            return null;
972        }
973        $position = $this->actualPosition;
974        $result = $this->toNextPositionNonEmpty($position);
975        if ($result === false) {
976            return null;
977        } else {
978            return $this->createFromCall($position);
979        }
980
981    }
982
983    /**
984     * Children are tag
985     * @return array|null
986     */
987    public function getChildren()
988    {
989        if ($this->getState() !== DOKU_LEXER_ENTER) {
990            // The tag ($this) is not an enter tag and has therefore no children."
991            return null;
992        }
993        $children = [];
994        $level = 0;
995        $position = $this->actualPosition;
996        while ($this->toNextPositionNonEmpty($position)) {
997
998            $call = new Call($this->handler->calls[$position]);
999            $state = $call->getState();
1000            switch ($state) {
1001                case DOKU_LEXER_ENTER:
1002                    $level += 1;
1003                    break;
1004                case DOKU_LEXER_EXIT:
1005                    $level -= 1;
1006                    break;
1007            }
1008            if ($level < 0) {
1009                break;
1010            } else {
1011                if ($state == DOKU_LEXER_ENTER && !in_array($call->getTagName(), Tag::INVISIBLE_CONTENT_TAG)) {
1012                    $children[] = $this->createFromCall($position);
1013                }
1014            }
1015        }
1016        return $children;
1017    }
1018
1019    public function setAttributeIfNotPresent($key, $value)
1020    {
1021        if (!isset($this->calls[$this->actualPosition][1][1][PluginUtility::ATTRIBUTES][$key])) {
1022            $this->setAttribute($key, $value);
1023        }
1024
1025    }
1026
1027    public function getNextTag()
1028    {
1029        $position = $this->actualPosition;
1030        $this->toNextPositionNonEmpty($position);
1031        return $this->createFromCall($position);
1032    }
1033
1034
1035}
1036