xref: /template/strap/ComboStrap/Outline.php (revision dff3a8c8e9d5229502eb360aee88a665b7d1c2dc)
104fd306cSNickeau<?php
204fd306cSNickeau
304fd306cSNickeaunamespace ComboStrap;
404fd306cSNickeau
504fd306cSNickeau
604fd306cSNickeauuse ComboStrap\Meta\Field\PageH1;
704fd306cSNickeauuse ComboStrap\Tag\TableTag;
804fd306cSNickeauuse ComboStrap\TagAttribute\StyleAttribute;
904fd306cSNickeauuse dokuwiki\Extension\SyntaxPlugin;
1004fd306cSNickeauuse syntax_plugin_combo_analytics;
1104fd306cSNickeauuse syntax_plugin_combo_header;
1204fd306cSNickeauuse syntax_plugin_combo_headingatx;
1304fd306cSNickeauuse syntax_plugin_combo_headingwiki;
1404fd306cSNickeauuse syntax_plugin_combo_media;
1504fd306cSNickeau
1604fd306cSNickeau/**
1704fd306cSNickeau * The outline is creating a XML like document
1804fd306cSNickeau * with section
1904fd306cSNickeau *
2004fd306cSNickeau * It's also the post-processing of the instructions
2104fd306cSNickeau */
2204fd306cSNickeauclass Outline
2304fd306cSNickeau{
2404fd306cSNickeau
2504fd306cSNickeau
2604fd306cSNickeau    const CANONICAL = "outline";
2704fd306cSNickeau    private const OUTLINE_HEADING_PREFIX = "outline-heading";
2804fd306cSNickeau    const CONTEXT = self::CANONICAL;
2904fd306cSNickeau    public const OUTLINE_HEADING_NUMBERING = "outline-heading-numbering";
3004fd306cSNickeau    public const TOC_NUMBERING = "toc-numbering";
3104fd306cSNickeau    /**
3204fd306cSNickeau     * As seen on
3304fd306cSNickeau     * https://drafts.csswg.org/css-counter-styles-3/#predefined-counters
3404fd306cSNickeau     */
3504fd306cSNickeau    public const CONF_COUNTER_STYLES_CHOICES = [
3604fd306cSNickeau        'arabic-indic',
3704fd306cSNickeau        'bengali',
3804fd306cSNickeau        'cambodian/khmer',
3904fd306cSNickeau        'cjk-decimal',
4004fd306cSNickeau        'decimal',
4104fd306cSNickeau        'decimal-leading-zero',
4204fd306cSNickeau        'devanagari',
4304fd306cSNickeau        'georgian',
4404fd306cSNickeau        'gujarati',
4504fd306cSNickeau        'gurmukhi',
4604fd306cSNickeau        'hebrew',
4704fd306cSNickeau        'hiragana',
4804fd306cSNickeau        'hiragana-iroha',
4904fd306cSNickeau        'kannada',
5004fd306cSNickeau        'katakana',
5104fd306cSNickeau        'katakana-iroha',
5204fd306cSNickeau        'lao',
5304fd306cSNickeau        'lower-alpha',
5404fd306cSNickeau        'lower-armenian',
5504fd306cSNickeau        'lower-greek',
5604fd306cSNickeau        'lower-roman',
5704fd306cSNickeau        'malayalam',
5804fd306cSNickeau        'mongolian',
5904fd306cSNickeau        'myanmar',
6004fd306cSNickeau        'oriya',
6104fd306cSNickeau        'persian',
6204fd306cSNickeau        'tamil',
6304fd306cSNickeau        'telugu',
6404fd306cSNickeau        'thai',
6504fd306cSNickeau        'tibetan',
6604fd306cSNickeau        'upper-alpha',
6704fd306cSNickeau        'upper-armenian',
6804fd306cSNickeau        'upper-roman'
6904fd306cSNickeau    ];
7004fd306cSNickeau    public const CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL4 = "outlineNumberingCounterStyleLevel4";
7104fd306cSNickeau    public const CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL3 = "outlineNumberingCounterStyleLevel3";
7204fd306cSNickeau    public const CONF_OUTLINE_NUMBERING_SUFFIX = "outlineNumberingSuffix";
7304fd306cSNickeau    public const CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL2 = "outlineNumberingCounterStyleLevel2";
7404fd306cSNickeau    public const CONF_OUTLINE_NUMBERING_COUNTER_SEPARATOR = "outlineNumberingCounterSeparator";
7504fd306cSNickeau    public const CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL6 = "outlineNumberingCounterStyleLevel6";
7604fd306cSNickeau    public const CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL5 = "outlineNumberingCounterStyleLevel5";
7704fd306cSNickeau    public const CONF_OUTLINE_NUMBERING_PREFIX = "outlineNumberingPrefix";
7804fd306cSNickeau    public const CONF_OUTLINE_NUMBERING_ENABLE = "outlineNumberingEnable";
7904fd306cSNickeau    /**
8004fd306cSNickeau     * To add hash tag to heading
8104fd306cSNickeau     */
8204fd306cSNickeau    public const OUTLINE_ANCHOR = "outline-anchor";
8304fd306cSNickeau    const CONF_OUTLINE_NUMBERING_ENABLE_DEFAULT = 1;
84912a1845Sgerardnico
85912a1845Sgerardnico    /**
86912a1845Sgerardnico     * The dokuwiki heading call is called the header...
87912a1845Sgerardnico     */
88912a1845Sgerardnico    const DOKUWIKI_HEADING_CALL_NAME = "header";
8904fd306cSNickeau    private OutlineSection $rootSection;
9004fd306cSNickeau
9104fd306cSNickeau    private OutlineSection $actualSection; // the actual section that is created
9204fd306cSNickeau    private Call $actualHeadingCall; // the heading that is parsed
9304fd306cSNickeau    private int $actualHeadingParsingState = DOKU_LEXER_EXIT;  // the state of the heading parsed (enter, closed), enter if we have entered an heading, exit if not;
9404fd306cSNickeau    private ?MarkupPath $markupPath = null;
9504fd306cSNickeau    private bool $isFragment;
9604fd306cSNickeau    private bool $metaHeaderCapture;
9704fd306cSNickeau
9804fd306cSNickeau    /**
9904fd306cSNickeau     * @param CallStack $callStack
10004fd306cSNickeau     * @param MarkupPath|null $markup - needed to store the parsed toc, h1, ... (null if the markup is dynamic)
10104fd306cSNickeau     * @param bool $isFragment - needed to control the structure of the outline (if this is a preview, the first heading may be not h1)
10204fd306cSNickeau     * @return void
10304fd306cSNickeau     */
10404fd306cSNickeau    public function __construct(CallStack $callStack, MarkupPath $markup = null, bool $isFragment = false)
10504fd306cSNickeau    {
10604fd306cSNickeau        if ($markup !== null) {
10704fd306cSNickeau            $this->markupPath = $markup;
10804fd306cSNickeau        }
10904fd306cSNickeau        $this->isFragment = $isFragment;
11004fd306cSNickeau        $this->buildOutline($callStack);
11104fd306cSNickeau        $this->storeH1();
11204fd306cSNickeau        $this->storeTocForMarkupIfAny();
11304fd306cSNickeau    }
11404fd306cSNickeau
11504fd306cSNickeau    /**
11604fd306cSNickeau     * @param CallStack $callStack
11704fd306cSNickeau     * @param MarkupPath|null $markupPath - needed to store the parsed toc, h1, ... (null if the markup is dynamic)
11804fd306cSNickeau     * @param bool $isFragment - needed to control the structure of the outline (if this is a preview, the first heading may be not h1)
11904fd306cSNickeau     * @return Outline
12004fd306cSNickeau     */
12104fd306cSNickeau    public static function createFromCallStack(CallStack $callStack, MarkupPath $markupPath = null, bool $isFragment = false): Outline
12204fd306cSNickeau    {
12304fd306cSNickeau        return new Outline($callStack, $markupPath, $isFragment);
12404fd306cSNickeau    }
12504fd306cSNickeau
12604fd306cSNickeau    private function buildOutline(CallStack $callStack)
12704fd306cSNickeau    {
12804fd306cSNickeau
12904fd306cSNickeau        /**
13004fd306cSNickeau         * {@link syntax_plugin_combo_analytics tag analytics}
13104fd306cSNickeau         * By default, in a test environment
13204fd306cSNickeau         * this is set to 0
13304fd306cSNickeau         * to speed test and to not pollute
13404fd306cSNickeau         */
13504fd306cSNickeau        $analtyicsEnabled = SiteConfig::getConfValue(syntax_plugin_combo_analytics::CONF_SYNTAX_ANALYTICS_ENABLE, true);
13604fd306cSNickeau        $analyticsTagUsed = [];
13704fd306cSNickeau
13804fd306cSNickeau        /**
13904fd306cSNickeau         * Processing variable about the context
14004fd306cSNickeau         */
14104fd306cSNickeau        $this->rootSection = OutlineSection::createOutlineRoot()
142*dff3a8c8SNico            ->setStartPosition(0)
143*dff3a8c8SNico            ->setOutlineContext($this);
14404fd306cSNickeau        $this->actualSection = $this->rootSection;
14504fd306cSNickeau        $actualLastPosition = 0;
14604fd306cSNickeau        $callStack->moveToStart();
14704fd306cSNickeau        while ($actualCall = $callStack->next()) {
14804fd306cSNickeau
14904fd306cSNickeau
15004fd306cSNickeau            $state = $actualCall->getState();
15104fd306cSNickeau
15204fd306cSNickeau            /**
15304fd306cSNickeau             * Block Post Processing
15404fd306cSNickeau             * to not get any unwanted p
15504fd306cSNickeau             * to counter {@link Block::process()}
15604fd306cSNickeau             * setting dynamically the {@link SyntaxPlugin::getPType()}
15704fd306cSNickeau             *
15804fd306cSNickeau             * Unfortunately, it can't work because this is called after
15904fd306cSNickeau             * {@link Block::process()}
16004fd306cSNickeau             */
16104fd306cSNickeau            if ($analtyicsEnabled) {
16204fd306cSNickeau
16304fd306cSNickeau                if (in_array($state, CallStack::TAG_STATE)) {
164aea52b49Sgerardnico                    $tagName = $actualCall->getTagName();
16504fd306cSNickeau                    // The dokuwiki component name have open in their name
16604fd306cSNickeau                    $tagName = str_replace("_open", "", $tagName);
167aea52b49Sgerardnico                    $actual = $analyticsTagUsed[$tagName] ?? 0;
168aea52b49Sgerardnico                    $analyticsTagUsed[$tagName] = $actual + 1;
16904fd306cSNickeau                }
17004fd306cSNickeau
17104fd306cSNickeau            }
17204fd306cSNickeau
17304fd306cSNickeau            /**
17404fd306cSNickeau             * We don't take the outline and document call if any
17504fd306cSNickeau             * This is the case when we build from an actual stored instructions
17604fd306cSNickeau             * (to bundle multiple page for instance)
17704fd306cSNickeau             */
17804fd306cSNickeau            $tagName = $actualCall->getTagName();
17904fd306cSNickeau            switch ($tagName) {
18004fd306cSNickeau                case "document_start":
18104fd306cSNickeau                case "document_end":
18204fd306cSNickeau                case SectionTag::TAG:
18304fd306cSNickeau                    continue 2;
18404fd306cSNickeau                case syntax_plugin_combo_header::TAG:
18504fd306cSNickeau                    if ($actualCall->isPluginCall() && $actualCall->getContext() === self::CONTEXT) {
18604fd306cSNickeau                        continue 2;
18704fd306cSNickeau                    }
18804fd306cSNickeau            }
18904fd306cSNickeau
19004fd306cSNickeau            /**
19104fd306cSNickeau             * Wrap the table
19204fd306cSNickeau             */
19304fd306cSNickeau            $componentName = $actualCall->getComponentName();
19404fd306cSNickeau            if ($componentName === "table_open") {
19504fd306cSNickeau                $position = $actualCall->getFirstMatchedCharacterPosition();
19604fd306cSNickeau                $originalInstructionCall = &$actualCall->getInstructionCall();
19704fd306cSNickeau                $originalInstructionCall = Call::createComboCall(
19804fd306cSNickeau                    TableTag::TAG,
19904fd306cSNickeau                    DOKU_LEXER_ENTER,
20004fd306cSNickeau                    [PluginUtility::POSITION => $position],
20104fd306cSNickeau                    null,
20204fd306cSNickeau                    null,
20304fd306cSNickeau                    null,
20404fd306cSNickeau                    $position,
20504fd306cSNickeau                    \syntax_plugin_combo_xmlblocktag::TAG
20604fd306cSNickeau                )->toCallArray();
20704fd306cSNickeau            }
20804fd306cSNickeau
20904fd306cSNickeau            /**
21004fd306cSNickeau             * Enter new section ?
21104fd306cSNickeau             */
21204fd306cSNickeau            $shouldWeCreateASection = false;
21304fd306cSNickeau            switch ($tagName) {
21404fd306cSNickeau                case syntax_plugin_combo_headingatx::TAG:
21504fd306cSNickeau                    $actualCall->setState(DOKU_LEXER_ENTER);
21604fd306cSNickeau                    if ($actualCall->getContext() === HeadingTag::TYPE_OUTLINE) {
21704fd306cSNickeau                        $shouldWeCreateASection = true;
21804fd306cSNickeau                    }
21904fd306cSNickeau                    $this->enterHeading($actualCall);
22004fd306cSNickeau                    break;
22104fd306cSNickeau                case HeadingTag::HEADING_TAG:
22204fd306cSNickeau                case syntax_plugin_combo_headingwiki::TAG:
22304fd306cSNickeau                    if ($state == DOKU_LEXER_ENTER
22404fd306cSNickeau                        && $actualCall->getContext() === HeadingTag::TYPE_OUTLINE) {
22504fd306cSNickeau                        $shouldWeCreateASection = true;
22604fd306cSNickeau                        $this->enterHeading($actualCall);
22704fd306cSNickeau                    }
22804fd306cSNickeau                    break;
229912a1845Sgerardnico                case self::DOKUWIKI_HEADING_CALL_NAME:
23004fd306cSNickeau                    // Should happen only on outline section
23104fd306cSNickeau                    // we take over inside a component
23204fd306cSNickeau                    if (!$actualCall->isPluginCall()) {
23304fd306cSNickeau                        /**
23404fd306cSNickeau                         * ie not {@link syntax_plugin_combo_header}
23504fd306cSNickeau                         * but the dokuwiki header (ie heading)
23604fd306cSNickeau                         */
23704fd306cSNickeau                        $shouldWeCreateASection = true;
23804fd306cSNickeau                        $this->enterHeading($actualCall);
239912a1845Sgerardnico                        // The dokuiki heading call (header) is a one call for the whole heading,
240912a1845Sgerardnico                        // It enters and exits at the same time
241912a1845Sgerardnico                        $this->exitHeading();
24204fd306cSNickeau                    }
24304fd306cSNickeau                    break;
24404fd306cSNickeau            }
24504fd306cSNickeau            if ($shouldWeCreateASection) {
24604fd306cSNickeau                if ($this->actualSection->hasParent()) {
24704fd306cSNickeau                    // -1 because the actual position is the start of the next section
24804fd306cSNickeau                    $this->actualSection->setEndPosition($actualCall->getFirstMatchedCharacterPosition() - 1);
24904fd306cSNickeau                }
25004fd306cSNickeau                $actualSectionLevel = $this->actualSection->getLevel();
25104fd306cSNickeau
25204fd306cSNickeau                if ($actualCall->isPluginCall()) {
25304fd306cSNickeau                    try {
25404fd306cSNickeau                        $newSectionLevel = DataType::toInteger($actualCall->getAttribute(HeadingTag::LEVEL));
25504fd306cSNickeau                    } catch (ExceptionBadArgument $e) {
25604fd306cSNickeau                        LogUtility::internalError("The level was not present on the heading call", self::CANONICAL);
25704fd306cSNickeau                        $newSectionLevel = $actualSectionLevel;
25804fd306cSNickeau                    }
25904fd306cSNickeau                } else {
26004fd306cSNickeau                    $headerTagName = $tagName;
261912a1845Sgerardnico                    if ($headerTagName !== self::DOKUWIKI_HEADING_CALL_NAME) {
26204fd306cSNickeau                        throw new ExceptionRuntimeInternal("This is not a dokuwiki header call", self::CANONICAL);
26304fd306cSNickeau                    }
26404fd306cSNickeau                    $newSectionLevel = $actualCall->getInstructionCall()[1][1];
26504fd306cSNickeau                }
26604fd306cSNickeau
26704fd306cSNickeau
26804fd306cSNickeau                $newOutlineSection = OutlineSection::createFromEnterHeadingCall($actualCall);
26904fd306cSNickeau                $sectionDiff = $newSectionLevel - $actualSectionLevel;
27004fd306cSNickeau                if ($sectionDiff > 0) {
27104fd306cSNickeau
27204fd306cSNickeau                    /**
27304fd306cSNickeau                     * A child of the actual section
27404fd306cSNickeau                     * We append it first before the message check to
27504fd306cSNickeau                     * build the {@link TreeNode::getTreeIdentifier()}
27604fd306cSNickeau                     */
27704fd306cSNickeau                    try {
27804fd306cSNickeau                        $this->actualSection->appendChild($newOutlineSection);
27904fd306cSNickeau                    } catch (ExceptionBadState $e) {
28004fd306cSNickeau                        throw new ExceptionRuntimeInternal("The node is not added multiple time, this error should not fired. Error:{$e->getMessage()}", self::CANONICAL, 1, $e);
28104fd306cSNickeau                    }
28204fd306cSNickeau
28304fd306cSNickeau                    if ($sectionDiff > 1 & !($actualSectionLevel === 0 && $newSectionLevel === 2)) {
28404fd306cSNickeau                        $expectedLevel = $actualSectionLevel + 1;
28504fd306cSNickeau                        if ($actualSectionLevel === 0) {
28604fd306cSNickeau                            /**
28704fd306cSNickeau                             * In a fragment run (preview),
28804fd306cSNickeau                             * the first heading may not be the first one
28904fd306cSNickeau                             */
29004fd306cSNickeau                            if (!$this->isFragment) {
29104fd306cSNickeau                                $message = "The first section heading should have the level 1 or 2 (not $newSectionLevel).";
29204fd306cSNickeau                            }
29304fd306cSNickeau                        } else {
29404fd306cSNickeau                            $message = "The child section heading ($actualSectionLevel) has the level ($newSectionLevel) but is parent ({$this->actualSection->getLabel()}) has the level ($actualSectionLevel). The expected level is ($expectedLevel).";
29504fd306cSNickeau                        }
29604fd306cSNickeau                        if (isset($message)) {
29704fd306cSNickeau                            LogUtility::warning($message, self::CANONICAL);
29804fd306cSNickeau                        }
29904fd306cSNickeau                        $actualCall->setAttribute(HeadingTag::LEVEL, $newSectionLevel);
30004fd306cSNickeau                    }
30104fd306cSNickeau
30204fd306cSNickeau                } else {
30304fd306cSNickeau
30404fd306cSNickeau                    /**
30504fd306cSNickeau                     * A child of the parent section, A sibling of the actual session
30604fd306cSNickeau                     */
30704fd306cSNickeau                    try {
30804fd306cSNickeau                        $parent = $this->actualSection->getParent();
30904fd306cSNickeau                        for ($i = 0; $i < abs($sectionDiff); $i++) {
31004fd306cSNickeau                            $parent = $parent->getParent();
31104fd306cSNickeau                        }
31204fd306cSNickeau                        try {
31304fd306cSNickeau                            $parent->appendChild($newOutlineSection);
31404fd306cSNickeau                        } catch (ExceptionBadState $e) {
31504fd306cSNickeau                            throw new ExceptionRuntimeInternal("The node is not added multiple time, this error should not fired. Error:{$e->getMessage()}", self::CANONICAL, 1, $e);
31604fd306cSNickeau                        }
31704fd306cSNickeau                    } catch (ExceptionNotFound $e) {
31804fd306cSNickeau                        /**
31904fd306cSNickeau                         * no parent
32004fd306cSNickeau                         * May happen in preview (ie fragment)
32104fd306cSNickeau                         */
32204fd306cSNickeau                        if (!$this->isFragment) {
32304fd306cSNickeau                            LogUtility::internalError("Due to the level logic, the actual section should have a parent");
32404fd306cSNickeau                        }
32504fd306cSNickeau                        try {
32604fd306cSNickeau                            $this->actualSection->appendChild($newOutlineSection);
32704fd306cSNickeau                        } catch (ExceptionBadState $e) {
32804fd306cSNickeau                            throw new ExceptionRuntimeInternal("The node is not added multiple time, this error should not fired. Error:{$e->getMessage()}", self::CANONICAL, 1, $e);
32904fd306cSNickeau                        }
33004fd306cSNickeau                    }
33104fd306cSNickeau
33204fd306cSNickeau                }
33304fd306cSNickeau
33404fd306cSNickeau                $this->actualSection = $newOutlineSection;
33504fd306cSNickeau                continue;
33604fd306cSNickeau            }
33704fd306cSNickeau
33804fd306cSNickeau            /**
33904fd306cSNickeau             * Track the number of lines
34004fd306cSNickeau             * to inject ads
34104fd306cSNickeau             */
34204fd306cSNickeau            switch ($tagName) {
34304fd306cSNickeau                case "linebreak":
34404fd306cSNickeau                case "tablerow":
34504fd306cSNickeau                    // linebreak is an inline component
34604fd306cSNickeau                    $this->actualSection->incrementLineNumber();
34704fd306cSNickeau                    break;
34804fd306cSNickeau                default:
34904fd306cSNickeau                    $display = $actualCall->getDisplay();
35004fd306cSNickeau                    if ($display === Call::BlOCK_DISPLAY) {
35104fd306cSNickeau                        $this->actualSection->incrementLineNumber();
35204fd306cSNickeau                    }
35304fd306cSNickeau                    break;
35404fd306cSNickeau            }
35504fd306cSNickeau
35604fd306cSNickeau            /**
35704fd306cSNickeau             * Track the position in the file
35804fd306cSNickeau             */
35904fd306cSNickeau            $currentLastPosition = $actualCall->getLastMatchedCharacterPosition();
36004fd306cSNickeau            if ($currentLastPosition > $actualLastPosition) {
36104fd306cSNickeau                // the position in the stack is not always good
36204fd306cSNickeau                $actualLastPosition = $currentLastPosition;
36304fd306cSNickeau            }
36404fd306cSNickeau
36504fd306cSNickeau
36604fd306cSNickeau            switch ($actualCall->getComponentName()) {
36704fd306cSNickeau                case \action_plugin_combo_instructionspostprocessing::EDIT_SECTION_OPEN:
36804fd306cSNickeau                case \action_plugin_combo_instructionspostprocessing::EDIT_SECTION_CLOSE:
36904fd306cSNickeau                    // we don't store them
37004fd306cSNickeau                    continue 2;
37104fd306cSNickeau            }
37204fd306cSNickeau
37304fd306cSNickeau            /**
37404fd306cSNickeau             * Close/Process the heading description
37504fd306cSNickeau             */
37604fd306cSNickeau            if ($this->actualHeadingParsingState === DOKU_LEXER_ENTER) {
37704fd306cSNickeau                switch ($tagName) {
37804fd306cSNickeau
37904fd306cSNickeau                    case HeadingTag::HEADING_TAG:
38004fd306cSNickeau                    case syntax_plugin_combo_headingwiki::TAG:
38104fd306cSNickeau                        if ($state == DOKU_LEXER_EXIT) {
38204fd306cSNickeau                            $this->addCallToSection($actualCall);
38304fd306cSNickeau                            $this->exitHeading();
38404fd306cSNickeau                            continue 2;
38504fd306cSNickeau                        }
38604fd306cSNickeau                        break;
38704fd306cSNickeau
38804fd306cSNickeau                    case "internalmedia":
38904fd306cSNickeau                        // no link for media in heading
39004fd306cSNickeau                        $actualCall->getInstructionCall()[1][6] = MediaMarkup::LINKING_NOLINK_VALUE;
39104fd306cSNickeau                        break;
39204fd306cSNickeau                    case syntax_plugin_combo_media::TAG:
39304fd306cSNickeau                        // no link for media in heading
39404fd306cSNickeau                        $actualCall->addAttribute(MediaMarkup::LINKING_KEY, MediaMarkup::LINKING_NOLINK_VALUE);
39504fd306cSNickeau                        break;
39604fd306cSNickeau
397912a1845Sgerardnico                    case self::DOKUWIKI_HEADING_CALL_NAME:
39804fd306cSNickeau                        if (SiteConfig::getConfValue(syntax_plugin_combo_headingwiki::CONF_WIKI_HEADING_ENABLE, syntax_plugin_combo_headingwiki::CONF_DEFAULT_WIKI_ENABLE_VALUE) == 1) {
39904fd306cSNickeau                            LogUtility::msg("The combo heading wiki is enabled, we should not see `header` calls in the call stack");
40004fd306cSNickeau                        }
40104fd306cSNickeau                        break;
40204fd306cSNickeau
40304fd306cSNickeau                    case "p":
40404fd306cSNickeau
40504fd306cSNickeau                        if ($this->actualHeadingCall->getTagName() === syntax_plugin_combo_headingatx::TAG) {
40604fd306cSNickeau                            // A new p is the end of an atx call
40704fd306cSNickeau                            switch ($actualCall->getComponentName()) {
40804fd306cSNickeau                                case "p_open":
40904fd306cSNickeau                                    // We don't take the p tag inside atx heading
41004fd306cSNickeau                                    // therefore we continue
41104fd306cSNickeau                                    continue 3;
41204fd306cSNickeau                                case "p_close":
41304fd306cSNickeau                                    $endAtxCall = Call::createComboCall(
41404fd306cSNickeau                                        syntax_plugin_combo_headingatx::TAG,
41504fd306cSNickeau                                        DOKU_LEXER_EXIT,
41604fd306cSNickeau                                        $this->actualHeadingCall->getAttributes(),
41704fd306cSNickeau                                        $this->actualHeadingCall->getContext(),
41804fd306cSNickeau                                    );
41904fd306cSNickeau                                    $this->addCallToSection($endAtxCall);
42004fd306cSNickeau                                    $this->exitHeading();
42104fd306cSNickeau                                    // We don't take the p tag inside atx heading
42204fd306cSNickeau                                    // therefore we continue
42304fd306cSNickeau                                    continue 3;
42404fd306cSNickeau                            }
42504fd306cSNickeau                        }
42604fd306cSNickeau                        break;
42704fd306cSNickeau
42804fd306cSNickeau                }
42904fd306cSNickeau            }
43004fd306cSNickeau            $this->addCallToSection($actualCall);
43104fd306cSNickeau        }
43204fd306cSNickeau
43304fd306cSNickeau        // empty text
43404fd306cSNickeau        if (sizeof($analyticsTagUsed) > 0) {
43504fd306cSNickeau            $pluginAnalyticsCall = Call::createComboCall(
43604fd306cSNickeau                syntax_plugin_combo_analytics::TAG,
43704fd306cSNickeau                DOKU_LEXER_SPECIAL,
43804fd306cSNickeau                $analyticsTagUsed
43904fd306cSNickeau            );
44004fd306cSNickeau            $this->addCallToSection($pluginAnalyticsCall);
44104fd306cSNickeau        }
44204fd306cSNickeau
44304fd306cSNickeau        // Add label the heading text to the metadata
44404fd306cSNickeau        $this->saveOutlineToMetadata();
44504fd306cSNickeau
44604fd306cSNickeau
44704fd306cSNickeau    }
44804fd306cSNickeau
44904fd306cSNickeau    public static function getOutlineHeadingClass(): string
45004fd306cSNickeau    {
45104fd306cSNickeau        return StyleAttribute::addComboStrapSuffix(self::OUTLINE_HEADING_PREFIX);
45204fd306cSNickeau    }
45304fd306cSNickeau
45404fd306cSNickeau    public function getRootOutlineSection(): OutlineSection
45504fd306cSNickeau    {
45604fd306cSNickeau        return $this->rootSection;
45704fd306cSNickeau
45804fd306cSNickeau    }
45904fd306cSNickeau
46004fd306cSNickeau    /**
461*dff3a8c8SNico     * Merge into a flat outline
46204fd306cSNickeau     */
4637dbcdecdSNico    public static function merge(Outline $inner, Outline $outer, int $actualLevel)
46404fd306cSNickeau    {
46504fd306cSNickeau        /**
46604fd306cSNickeau         * Get the inner section where the outer section will be added
46704fd306cSNickeau         */
46804fd306cSNickeau        $innerRootOutlineSection = $inner->getRootOutlineSection();
46904fd306cSNickeau        $innerTopSections = $innerRootOutlineSection->getChildren();
47004fd306cSNickeau        if (count($innerTopSections) === 0) {
47104fd306cSNickeau            $firstInnerSection = $innerRootOutlineSection;
47204fd306cSNickeau        } else {
47304fd306cSNickeau            $firstInnerSection = $innerTopSections[count($innerTopSections)];
47404fd306cSNickeau        }
47504fd306cSNickeau        $firstInnerSectionLevel = $firstInnerSection->getLevel();
47604fd306cSNickeau
47704fd306cSNickeau        /**
47804fd306cSNickeau         * Add the outer sections
47904fd306cSNickeau         */
48004fd306cSNickeau        $outerRootOutlineSection = $outer->getRootOutlineSection();
48104fd306cSNickeau        foreach ($outerRootOutlineSection->getChildren() as $childOuterSection) {
48204fd306cSNickeau            /**
48304fd306cSNickeau             * One level less than where the section is included
48404fd306cSNickeau             */
485*dff3a8c8SNico            $level = $firstInnerSectionLevel + $actualLevel + 1;
486*dff3a8c8SNico            $childOuterSection->setLevel($level);
487*dff3a8c8SNico            $childOuterSection->updatePageLinkToInternal($inner->markupPath);
48804fd306cSNickeau            $childOuterSection->detachBeforeAppend();
489*dff3a8c8SNico
49004fd306cSNickeau            try {
49104fd306cSNickeau                $firstInnerSection->appendChild($childOuterSection);
49204fd306cSNickeau            } catch (ExceptionBadState $e) {
49304fd306cSNickeau                // We add the node only once. This error should not happen
49404fd306cSNickeau                throw new ExceptionRuntimeInternal("Error while adding a section during the outline merge. Error: {$e->getMessage()}", self::CANONICAL, 1, $e);
49504fd306cSNickeau            }
496*dff3a8c8SNico
49704fd306cSNickeau        }
49804fd306cSNickeau
49904fd306cSNickeau    }
50004fd306cSNickeau
50104fd306cSNickeau    public static function mergeRecurse(Outline $inner, Outline $outer)
50204fd306cSNickeau    {
50304fd306cSNickeau        $innerRootOutlineSection = $inner->getRootOutlineSection();
50404fd306cSNickeau        $outerRootOutlineSection = $outer->getRootOutlineSection();
50504fd306cSNickeau
50604fd306cSNickeau    }
50704fd306cSNickeau
50804fd306cSNickeau    /**
50904fd306cSNickeau     * Utility class to create a outline from a markup string
51004fd306cSNickeau     * @param string $content
511*dff3a8c8SNico     * @param MarkupPath $contentPath
512*dff3a8c8SNico     * @param WikiPath $contextPath
51304fd306cSNickeau     * @return Outline
51404fd306cSNickeau     */
515*dff3a8c8SNico    public static function createFromMarkup(string $content, MarkupPath $contentPath, WikiPath $contextPath): Outline
51604fd306cSNickeau    {
51704fd306cSNickeau        $instructions = MarkupRenderer::createFromMarkup($content, $contentPath, $contextPath)
51804fd306cSNickeau            ->setRequestedMimeToInstruction()
51904fd306cSNickeau            ->getOutput();
52004fd306cSNickeau        $callStack = CallStack::createFromInstructions($instructions);
521*dff3a8c8SNico        return Outline::createFromCallStack($callStack, $contentPath);
52204fd306cSNickeau    }
52304fd306cSNickeau
52404fd306cSNickeau    /**
52504fd306cSNickeau     * Get the heading numbering snippet
52604fd306cSNickeau     * @param string $type heading or toc - for {@link Outline::TOC_NUMBERING} or {@link Outline::OUTLINE_HEADING_NUMBERING}
52704fd306cSNickeau     * @return string - the css internal stylesheet
52804fd306cSNickeau     * @throws ExceptionNotEnabled
52904fd306cSNickeau     * @throws ExceptionBadSyntax
53004fd306cSNickeau     * Page on DokuWiki
53104fd306cSNickeau     * https://www.dokuwiki.org/tips:numbered_headings
53204fd306cSNickeau     */
53304fd306cSNickeau    public static function getCssNumberingRulesFor(string $type): string
53404fd306cSNickeau    {
53504fd306cSNickeau
53604fd306cSNickeau        $enable = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_ENABLE, Outline::CONF_OUTLINE_NUMBERING_ENABLE_DEFAULT);
53704fd306cSNickeau        if (!$enable) {
53804fd306cSNickeau            throw new ExceptionNotEnabled();
53904fd306cSNickeau        }
54004fd306cSNickeau
54104fd306cSNickeau        $level2CounterStyle = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL2, "decimal");
54204fd306cSNickeau        $level3CounterStyle = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL3, "decimal");
54304fd306cSNickeau        $level4CounterStyle = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL4, "decimal");
54404fd306cSNickeau        $level5CounterStyle = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL5, "decimal");
54504fd306cSNickeau        $level6CounterStyle = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL6, "decimal");
54604fd306cSNickeau        $counterSeparator = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_SEPARATOR, ".");
54704fd306cSNickeau        $prefix = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_PREFIX, "");
54804fd306cSNickeau        $suffix = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_SUFFIX, " - ");
54904fd306cSNickeau
55004fd306cSNickeau        switch ($type) {
55104fd306cSNickeau
55204fd306cSNickeau            case self::OUTLINE_HEADING_NUMBERING:
55304fd306cSNickeau                global $ACT;
55404fd306cSNickeau                /**
55504fd306cSNickeau                 * Because the HTML file structure is not really fixed
55604fd306cSNickeau                 * (we may have section HTML element with a bar, the sectioning heading
55704fd306cSNickeau                 * may be not enabled)
55804fd306cSNickeau                 * We can't select via html structure
55904fd306cSNickeau                 * the outline heading consistently
56004fd306cSNickeau                 * We do it then with the class value
56104fd306cSNickeau                 */
56204fd306cSNickeau                $outlineClass = Outline::getOutlineHeadingClass();
563ef115533Sgerardnico                if ($ACT === "preview") {
564ef115533Sgerardnico                    $mainContainerSelector = ".pad";
565ef115533Sgerardnico                } else {
566ef115533Sgerardnico                    $mainContainerSelector = "#" . TemplateSlot::MAIN_CONTENT_ID;
567484d95f4Sgerardnico                }
568484d95f4Sgerardnico                /**
569484d95f4Sgerardnico                 * Counter inheritance works by sibling and if not found on parents
570484d95f4Sgerardnico                 * we therefore needs to take into account the 2 HTML structure
571484d95f4Sgerardnico                 * * one counter on h1 if this is the flat structure
572484d95f4Sgerardnico                 * one counter on the section if this is the section structure
573484d95f4Sgerardnico                 */
574ef115533Sgerardnico                $reset = <<<EOF
575ef115533Sgerardnico$mainContainerSelector { counter-reset: h2; }
576484d95f4Sgerardnico$mainContainerSelector > h2.$outlineClass { counter-increment: h2 1; counter-reset: h3 h4 h5 h6;}
577484d95f4Sgerardnico$mainContainerSelector > h3.$outlineClass { counter-increment: h3 1; counter-reset: h4 h5 h6;}
578484d95f4Sgerardnico$mainContainerSelector > h4.$outlineClass { counter-increment: h4 1; counter-reset: h5 h6;}
579484d95f4Sgerardnico$mainContainerSelector > h5.$outlineClass { counter-increment: h5 1; counter-reset: h6;}
580484d95f4Sgerardnico$mainContainerSelector > h6.$outlineClass { counter-increment: h6 1; }
5819557b9a7Sgerardnico$mainContainerSelector section.outline-level-2-cs { counter-increment: h2; counter-reset: h3 h4 h5 h6;}
5829557b9a7Sgerardnico$mainContainerSelector section.outline-level-3-cs { counter-increment: h3; counter-reset: h4 h5 h6;}
5839557b9a7Sgerardnico$mainContainerSelector section.outline-level-4-cs { counter-increment: h4; counter-reset: h5 h6;}
5849557b9a7Sgerardnico$mainContainerSelector section.outline-level-5-cs { counter-increment: h5; counter-reset: h6;}
585ef115533SgerardnicoEOF;
58604fd306cSNickeau                return <<<EOF
587ef115533Sgerardnico$reset
588effabefcSgerardnico$mainContainerSelector h2.$outlineClass::before { content: "$prefix" counter(h2, $level2CounterStyle) "$suffix\A"; }
589effabefcSgerardnico$mainContainerSelector h3.$outlineClass::before { content: "$prefix" counter(h2, $level2CounterStyle) "$counterSeparator" counter(h3,$level3CounterStyle) "$suffix\A"; }
590effabefcSgerardnico$mainContainerSelector h4.$outlineClass::before { content: "$prefix" counter(h2, $level2CounterStyle) "$counterSeparator" counter(h3,$level3CounterStyle) "$counterSeparator" counter(h4,$level4CounterStyle) "$suffix\A"; }
591effabefcSgerardnico$mainContainerSelector h5.$outlineClass::before { content: "$prefix" counter(h2, $level2CounterStyle) "$counterSeparator" counter(h3,$level3CounterStyle) "$counterSeparator" counter(h4,$level4CounterStyle) "$counterSeparator" counter(h5,$level5CounterStyle) "$suffix\A"; }
592effabefcSgerardnico$mainContainerSelector h6.$outlineClass::before { content: "$prefix" counter(h2, $level2CounterStyle) "$counterSeparator" counter(h3,$level3CounterStyle) "$counterSeparator" counter(h4,$level4CounterStyle) "$counterSeparator" counter(h5,$level5CounterStyle) "$counterSeparator" counter(h6,$level6CounterStyle) "$suffix\A"; }
59304fd306cSNickeauEOF;
594ef115533Sgerardnico
595ef115533Sgerardnico
59604fd306cSNickeau            case self::TOC_NUMBERING:
59704fd306cSNickeau                /**
59804fd306cSNickeau                 * The level counter on the toc are based
59904fd306cSNickeau                 * on the https://www.dokuwiki.org/config:toptoclevel
60004fd306cSNickeau                 * configuration
60104fd306cSNickeau                 * if toptoclevel = 2, then level1 = h2 and not h1
60204fd306cSNickeau                 * @deprecated
60304fd306cSNickeau                 */
60404fd306cSNickeau                // global $conf;
60504fd306cSNickeau                // $topTocLevel = $conf['toptoclevel'];
60604fd306cSNickeau
60704fd306cSNickeau                $tocSelector = "." . Toc::getClass() . " ul";
60804fd306cSNickeau                return <<<EOF
60904fd306cSNickeau$tocSelector li { counter-increment: toc2; }
61004fd306cSNickeau$tocSelector li li { counter-increment: toc3; }
61104fd306cSNickeau$tocSelector li li li { counter-increment: toc4; }
61204fd306cSNickeau$tocSelector li li li li { counter-increment: toc5; }
61304fd306cSNickeau$tocSelector li li li li li { counter-increment: toc6; }
61404fd306cSNickeau$tocSelector li a::before { content: "$prefix" counter(toc2, $level2CounterStyle) "$suffix\A"; }
61504fd306cSNickeau$tocSelector li li a::before { content: "$prefix" counter(toc2, $level2CounterStyle) "$counterSeparator" counter(toc3,$level3CounterStyle) "$suffix\A"; }
61604fd306cSNickeau$tocSelector li li li a::before { content: "$prefix" counter(toc2, $level2CounterStyle) "$counterSeparator" counter(toc3,$level3CounterStyle) "$counterSeparator" counter(toc4,$level4CounterStyle) "$suffix\A"; }
61704fd306cSNickeau$tocSelector li li li li a::before { content: "$prefix" counter(toc2, $level2CounterStyle) "$counterSeparator" counter(toc3,$level3CounterStyle) "$counterSeparator" counter(toc4,$level4CounterStyle) "$counterSeparator" counter(toc5,$level5CounterStyle) "$suffix\A"; }
61804fd306cSNickeau$tocSelector li li li li li a::before { content: "$prefix" counter(toc2, $level2CounterStyle) "$counterSeparator" counter(toc3,$level3CounterStyle) "$counterSeparator" counter(toc4,$level4CounterStyle) "$counterSeparator" counter(toc5,$level5CounterStyle) "$counterSeparator" counter(toc6,$level6CounterStyle) "$suffix\A"; }
61904fd306cSNickeauEOF;
62004fd306cSNickeau
62104fd306cSNickeau            default:
62204fd306cSNickeau                throw new ExceptionBadSyntax("The type ($type) is unknown");
62304fd306cSNickeau        }
62404fd306cSNickeau
62504fd306cSNickeau
62604fd306cSNickeau    }
62704fd306cSNickeau
62804fd306cSNickeau    /**
62904fd306cSNickeau     * @throws ExceptionNotFound
63004fd306cSNickeau     */
63104fd306cSNickeau    public static function createFromMarkupPath(MarkupPath $markupPath): Outline
63204fd306cSNickeau    {
63304fd306cSNickeau        $path = $markupPath->getPathObject();
63404fd306cSNickeau        if (!($path instanceof WikiPath)) {
63504fd306cSNickeau            throw new ExceptionRuntimeInternal("The path is not a wiki path");
63604fd306cSNickeau        }
63704fd306cSNickeau        $markup = FileSystems::getContent($path);
63804fd306cSNickeau        $instructions = MarkupRenderer::createFromMarkup($markup, $path, $path)
63904fd306cSNickeau            ->setRequestedMimeToInstruction()
64004fd306cSNickeau            ->getOutput();
64104fd306cSNickeau        $callStack = CallStack::createFromInstructions($instructions);
64204fd306cSNickeau        return new Outline($callStack, $markupPath);
64304fd306cSNickeau    }
64404fd306cSNickeau
64504fd306cSNickeau    public function getInstructionCalls(): array
64604fd306cSNickeau    {
64704fd306cSNickeau        $totalInstructionCalls = [];
64804fd306cSNickeau        $collectCalls = function (OutlineSection $outlineSection) use (&$totalInstructionCalls) {
64904fd306cSNickeau            $instructionCalls = array_map(function (Call $element) {
65004fd306cSNickeau                return $element->getInstructionCall();
65104fd306cSNickeau            }, $outlineSection->getCalls());
65204fd306cSNickeau            $totalInstructionCalls = array_merge($totalInstructionCalls, $instructionCalls);
65304fd306cSNickeau        };
65404fd306cSNickeau        TreeVisit::visit($this->rootSection, $collectCalls);
65504fd306cSNickeau        return $totalInstructionCalls;
65604fd306cSNickeau    }
65704fd306cSNickeau
65804fd306cSNickeau    public function toDokuWikiTemplateInstructionCalls(): array
65904fd306cSNickeau    {
66004fd306cSNickeau        $totalInstructionCalls = [];
66104fd306cSNickeau        $sectionSequenceId = 0;
66204fd306cSNickeau        $collectCalls = function (OutlineSection $outlineSection) use (&$totalInstructionCalls, &$sectionSequenceId) {
66304fd306cSNickeau
66404fd306cSNickeau            $wikiSectionOpen = Call::createNativeCall(
66504fd306cSNickeau                \action_plugin_combo_instructionspostprocessing::EDIT_SECTION_OPEN,
66604fd306cSNickeau                array($outlineSection->getLevel()),
66704fd306cSNickeau                $outlineSection->getStartPosition()
66804fd306cSNickeau            );
66904fd306cSNickeau            $wikiSectionClose = Call::createNativeCall(
67004fd306cSNickeau                \action_plugin_combo_instructionspostprocessing::EDIT_SECTION_CLOSE,
67104fd306cSNickeau                array(),
67204fd306cSNickeau                $outlineSection->getEndPosition()
67304fd306cSNickeau            );
67404fd306cSNickeau
67504fd306cSNickeau
67604fd306cSNickeau            if ($outlineSection->hasParent()) {
67704fd306cSNickeau
67804fd306cSNickeau
67904fd306cSNickeau                $sectionCalls = array_merge(
68004fd306cSNickeau                    $outlineSection->getHeadingCalls(),
68104fd306cSNickeau                    [$wikiSectionOpen],
68204fd306cSNickeau                    $outlineSection->getContentCalls(),
68304fd306cSNickeau                    [$wikiSectionClose],
68404fd306cSNickeau                );
68504fd306cSNickeau
68604fd306cSNickeau                if ($this->isSectionEditingEnabled()) {
68704fd306cSNickeau
68804fd306cSNickeau                    /**
68904fd306cSNickeau                     * Adding sectionedit class to be conform
69004fd306cSNickeau                     * with the Dokuwiki {@link \Doku_Renderer_xhtml::header()} function
69104fd306cSNickeau                     */
69204fd306cSNickeau                    $sectionSequenceId++;
69304fd306cSNickeau                    $headingCall = $outlineSection->getEnterHeadingCall();
69404fd306cSNickeau                    if ($headingCall->isPluginCall()) {
69504fd306cSNickeau                        $level = DataType::toIntegerOrDefaultIfNull($headingCall->getAttribute(HeadingTag::LEVEL), 0);
69604fd306cSNickeau                        if ($level <= $this->getTocMaxLevel()) {
69704fd306cSNickeau                            $headingCall->addClassName("sectionedit$sectionSequenceId");
69804fd306cSNickeau                        }
69904fd306cSNickeau                    }
70004fd306cSNickeau
70104fd306cSNickeau                    $editButton = EditButton::create($outlineSection->getLabel())
70204fd306cSNickeau                        ->setStartPosition($outlineSection->getStartPosition())
70304fd306cSNickeau                        ->setEndPosition($outlineSection->getEndPosition())
70404fd306cSNickeau                        ->setOutlineHeadingId($outlineSection->getHeadingId())
70504fd306cSNickeau                        ->setOutlineSectionId($sectionSequenceId)
70604fd306cSNickeau                        ->toComboCallDokuWikiForm();
70704fd306cSNickeau                    $sectionCalls[] = $editButton;
70804fd306cSNickeau                }
70904fd306cSNickeau
71004fd306cSNickeau            } else {
71104fd306cSNickeau                // dokuwiki seems to have no section for the content before the first heading
71204fd306cSNickeau                $sectionCalls = $outlineSection->getContentCalls();
71304fd306cSNickeau            }
71404fd306cSNickeau
71504fd306cSNickeau            $instructionCalls = array_map(function (Call $element) {
71604fd306cSNickeau                return $element->getInstructionCall();
71704fd306cSNickeau            }, $sectionCalls);
71804fd306cSNickeau            $totalInstructionCalls = array_merge($totalInstructionCalls, $instructionCalls);
71904fd306cSNickeau        };
72004fd306cSNickeau        TreeVisit::visit($this->rootSection, $collectCalls);
72104fd306cSNickeau        return $totalInstructionCalls;
72204fd306cSNickeau    }
72304fd306cSNickeau
72404fd306cSNickeau    private function addCallToSection(Call $actualCall)
72504fd306cSNickeau    {
72604fd306cSNickeau        if ($this->actualHeadingParsingState === DOKU_LEXER_ENTER && !$this->actualSection->hasContentCall()) {
72704fd306cSNickeau            $this->actualSection->addHeaderCall($actualCall);
72804fd306cSNickeau        } else {
72904fd306cSNickeau            // an content heading (not outline) or another call
73004fd306cSNickeau            $this->actualSection->addContentCall($actualCall);
73104fd306cSNickeau        }
73204fd306cSNickeau    }
73304fd306cSNickeau
73404fd306cSNickeau    private function enterHeading(Call $actualCall)
73504fd306cSNickeau    {
73604fd306cSNickeau        $this->actualHeadingParsingState = DOKU_LEXER_ENTER;
73704fd306cSNickeau        $this->actualHeadingCall = $actualCall;
73804fd306cSNickeau    }
73904fd306cSNickeau
74004fd306cSNickeau    private function exitHeading()
74104fd306cSNickeau    {
74204fd306cSNickeau        $this->actualHeadingParsingState = DOKU_LEXER_EXIT;
74304fd306cSNickeau    }
74404fd306cSNickeau
74504fd306cSNickeau    /**
74604fd306cSNickeau     * @return array - Dokuwiki TOC array format
74704fd306cSNickeau     */
74804fd306cSNickeau    public function toTocDokuwikiFormat(): array
74904fd306cSNickeau    {
75004fd306cSNickeau
75104fd306cSNickeau        $tableOfContent = [];
75204fd306cSNickeau        $collectTableOfContent = function (OutlineSection $outlineSection) use (&$tableOfContent) {
75304fd306cSNickeau
75404fd306cSNickeau            if (!$outlineSection->hasParent()) {
75504fd306cSNickeau                // Root Section, no heading
75604fd306cSNickeau                return;
75704fd306cSNickeau            }
75804fd306cSNickeau            $tableOfContent[] = [
75904fd306cSNickeau                'link' => '#' . $outlineSection->getHeadingId(),
76004fd306cSNickeau                'title' => $outlineSection->getLabel(),
76104fd306cSNickeau                'type' => 'ul',
76204fd306cSNickeau                'level' => $outlineSection->getLevel()
76304fd306cSNickeau            ];
76404fd306cSNickeau
76504fd306cSNickeau        };
76604fd306cSNickeau        TreeVisit::visit($this->rootSection, $collectTableOfContent);
76704fd306cSNickeau        return $tableOfContent;
76804fd306cSNickeau
76904fd306cSNickeau    }
77004fd306cSNickeau
77104fd306cSNickeau
77204fd306cSNickeau    public
77304fd306cSNickeau    function toHtmlSectionOutlineCalls(): array
77404fd306cSNickeau    {
77504fd306cSNickeau        return OutlineVisitor::create($this)->getCalls();
77604fd306cSNickeau    }
77704fd306cSNickeau
77804fd306cSNickeau
77904fd306cSNickeau    /**
78004fd306cSNickeau     * Fragment Rendering
78104fd306cSNickeau     * * does not have any section/edit button
78204fd306cSNickeau     * * no outline or edit button for dynamic rendering but closing of atx heading
78304fd306cSNickeau     *
78404fd306cSNickeau     * The outline processing ({@link Outline::buildOutline()} just close the atx heading
78504fd306cSNickeau     *
78604fd306cSNickeau     * @return array
78704fd306cSNickeau     */
78804fd306cSNickeau    public
78904fd306cSNickeau    function toFragmentInstructionCalls(): array
79004fd306cSNickeau    {
79104fd306cSNickeau        $totalInstructionCalls = [];
79204fd306cSNickeau        $collectCalls = function (OutlineSection $outlineSection) use (&$totalInstructionCalls) {
79304fd306cSNickeau
79404fd306cSNickeau            $sectionCalls = array_merge(
79504fd306cSNickeau                $outlineSection->getHeadingCalls(),
79604fd306cSNickeau                $outlineSection->getContentCalls()
79704fd306cSNickeau            );
79804fd306cSNickeau
79904fd306cSNickeau            $instructionCalls = array_map(function (Call $element) {
80004fd306cSNickeau                return $element->getInstructionCall();
80104fd306cSNickeau            }, $sectionCalls);
80204fd306cSNickeau            $totalInstructionCalls = array_merge($totalInstructionCalls, $instructionCalls);
80304fd306cSNickeau        };
80404fd306cSNickeau        TreeVisit::visit($this->rootSection, $collectCalls);
80504fd306cSNickeau        return $totalInstructionCalls;
80604fd306cSNickeau
80704fd306cSNickeau    }
80804fd306cSNickeau
80904fd306cSNickeau    /**
81004fd306cSNickeau     * Add the label (ie heading text to the cal attribute)
81104fd306cSNickeau     *
81204fd306cSNickeau     * @return void
81304fd306cSNickeau     */
81404fd306cSNickeau    private
81504fd306cSNickeau    function saveOutlineToMetadata()
81604fd306cSNickeau    {
81704fd306cSNickeau        try {
81804fd306cSNickeau            $firstChild = $this->rootSection->getFirstChild();
81904fd306cSNickeau        } catch (ExceptionNotFound $e) {
82004fd306cSNickeau            // no child
82104fd306cSNickeau            return;
82204fd306cSNickeau        }
82304fd306cSNickeau        if ($firstChild->getLevel() === 1) {
82404fd306cSNickeau            $headingCall = $firstChild->getEnterHeadingCall();
82504fd306cSNickeau            // not dokuwiki header ?
82604fd306cSNickeau            if ($headingCall->isPluginCall()) {
82704fd306cSNickeau                $headingCall->setAttribute(HeadingTag::HEADING_TEXT_ATTRIBUTE, $firstChild->getLabel());
82804fd306cSNickeau            }
82904fd306cSNickeau        }
83004fd306cSNickeau
83104fd306cSNickeau    }
83204fd306cSNickeau
83304fd306cSNickeau
83404fd306cSNickeau    private
83504fd306cSNickeau    function storeH1()
83604fd306cSNickeau    {
83704fd306cSNickeau        try {
83804fd306cSNickeau            $outlineSection = $this->getRootOutlineSection()->getFirstChild();
83904fd306cSNickeau        } catch (ExceptionNotFound $e) {
84004fd306cSNickeau            //
84104fd306cSNickeau            return;
84204fd306cSNickeau        }
84304fd306cSNickeau        if ($this->markupPath != null && $outlineSection->getLevel() === 1) {
84404fd306cSNickeau            $label = $outlineSection->getLabel();
84504fd306cSNickeau            $call = $outlineSection->getEnterHeadingCall();
84604fd306cSNickeau            if ($call->isPluginCall()) {
84704fd306cSNickeau                // we support also the dokwuiki header call that does not need the label
84804fd306cSNickeau                $call->addAttribute(HeadingTag::PARSED_LABEL, $label);
84904fd306cSNickeau            }
85004fd306cSNickeau            PageH1::createForPage($this->markupPath)->setDefaultValue($label);
85104fd306cSNickeau        }
85204fd306cSNickeau    }
85304fd306cSNickeau
85404fd306cSNickeau    private
85504fd306cSNickeau    function storeTocForMarkupIfAny()
85604fd306cSNickeau    {
85704fd306cSNickeau
85804fd306cSNickeau        $toc = $this->toTocDokuwikiFormat();
85904fd306cSNickeau
86004fd306cSNickeau        try {
86104fd306cSNickeau            $fetcherMarkup = ExecutionContext::getActualOrCreateFromEnv()->getExecutingMarkupHandler();
86204fd306cSNickeau            $fetcherMarkup->toc = $toc;
86304fd306cSNickeau            if ($fetcherMarkup->isDocument()) {
86404fd306cSNickeau                /**
86504fd306cSNickeau                 * We still update the global TOC Dokuwiki variables
86604fd306cSNickeau                 */
86704fd306cSNickeau                global $TOC;
86804fd306cSNickeau                $TOC = $toc;
86904fd306cSNickeau            }
87004fd306cSNickeau        } catch (ExceptionNotFound $e) {
87104fd306cSNickeau            // outline is not runnned from a markup handler
87204fd306cSNickeau        }
87304fd306cSNickeau
87404fd306cSNickeau        if (!isset($this->markupPath)) {
87504fd306cSNickeau            return;
87604fd306cSNickeau        }
87704fd306cSNickeau
87804fd306cSNickeau        try {
87904fd306cSNickeau            Toc::createForPage($this->markupPath)
88004fd306cSNickeau                ->setValue($toc)
88104fd306cSNickeau                ->persist();
88204fd306cSNickeau        } catch (ExceptionBadArgument $e) {
88304fd306cSNickeau            LogUtility::error("The Toc could not be persisted. Error:{$e->getMessage()}");
88404fd306cSNickeau        }
88504fd306cSNickeau    }
88604fd306cSNickeau
88704fd306cSNickeau    public
88804fd306cSNickeau    function getMarkupPath(): ?MarkupPath
88904fd306cSNickeau    {
89004fd306cSNickeau        return $this->markupPath;
89104fd306cSNickeau    }
89204fd306cSNickeau
89304fd306cSNickeau
89404fd306cSNickeau    private
89504fd306cSNickeau    function getTocMaxLevel(): int
89604fd306cSNickeau    {
89704fd306cSNickeau        return ExecutionContext::getActualOrCreateFromEnv()
89804fd306cSNickeau            ->getConfig()->getTocMaxLevel();
89904fd306cSNickeau    }
90004fd306cSNickeau
90104fd306cSNickeau    public function setMetaHeaderCapture(bool $metaHeaderCapture): Outline
90204fd306cSNickeau    {
90304fd306cSNickeau        $this->metaHeaderCapture = $metaHeaderCapture;
90404fd306cSNickeau        return $this;
90504fd306cSNickeau    }
90604fd306cSNickeau
90704fd306cSNickeau    public function getMetaHeaderCapture(): bool
90804fd306cSNickeau    {
90904fd306cSNickeau        if (isset($this->metaHeaderCapture)) {
91004fd306cSNickeau            return $this->metaHeaderCapture;
91104fd306cSNickeau        }
91204fd306cSNickeau        try {
91304fd306cSNickeau            if ($this->markupPath !== null) {
91404fd306cSNickeau                $contextPath = $this->markupPath->getPathObject()->toWikiPath();
91504fd306cSNickeau                $hasMainHeaderElement = TemplateForWebPage::create()
91604fd306cSNickeau                    ->setRequestedContextPath($contextPath)
91704fd306cSNickeau                    ->hasElement(TemplateSlot::MAIN_HEADER_ID);
91804fd306cSNickeau                $isThemeSystemEnabled = ExecutionContext::getActualOrCreateFromEnv()
91904fd306cSNickeau                    ->getConfig()
92004fd306cSNickeau                    ->isThemeSystemEnabled();
92104fd306cSNickeau                if ($isThemeSystemEnabled && $hasMainHeaderElement) {
92204fd306cSNickeau                    return true;
92304fd306cSNickeau                }
92404fd306cSNickeau            }
92504fd306cSNickeau        } catch (ExceptionCast $e) {
92604fd306cSNickeau            // to Wiki Path should be good
92704fd306cSNickeau        }
92804fd306cSNickeau        return false;
92904fd306cSNickeau    }
93004fd306cSNickeau
93104fd306cSNickeau    public function isSectionEditingEnabled(): bool
93204fd306cSNickeau    {
93304fd306cSNickeau
93404fd306cSNickeau        return ExecutionContext::getActualOrCreateFromEnv()
93504fd306cSNickeau            ->getConfig()->isSectionEditingEnabled();
93604fd306cSNickeau
93704fd306cSNickeau    }
93804fd306cSNickeau
93904fd306cSNickeau
94004fd306cSNickeau}
941