xref: /template/strap/ComboStrap/Outline.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
1*04fd306cSNickeau<?php
2*04fd306cSNickeau
3*04fd306cSNickeaunamespace ComboStrap;
4*04fd306cSNickeau
5*04fd306cSNickeau
6*04fd306cSNickeauuse ComboStrap\Meta\Field\FeaturedRasterImage;
7*04fd306cSNickeauuse ComboStrap\Meta\Field\FeaturedSvgImage;
8*04fd306cSNickeauuse ComboStrap\Meta\Field\PageH1;
9*04fd306cSNickeauuse ComboStrap\Tag\TableTag;
10*04fd306cSNickeauuse ComboStrap\TagAttribute\StyleAttribute;
11*04fd306cSNickeauuse dokuwiki\Extension\SyntaxPlugin;
12*04fd306cSNickeauuse syntax_plugin_combo_analytics;
13*04fd306cSNickeauuse syntax_plugin_combo_header;
14*04fd306cSNickeauuse syntax_plugin_combo_headingatx;
15*04fd306cSNickeauuse syntax_plugin_combo_headingwiki;
16*04fd306cSNickeauuse syntax_plugin_combo_media;
17*04fd306cSNickeau
18*04fd306cSNickeau/**
19*04fd306cSNickeau * The outline is creating a XML like document
20*04fd306cSNickeau * with section
21*04fd306cSNickeau *
22*04fd306cSNickeau * It's also the post-processing of the instructions
23*04fd306cSNickeau */
24*04fd306cSNickeauclass Outline
25*04fd306cSNickeau{
26*04fd306cSNickeau
27*04fd306cSNickeau
28*04fd306cSNickeau    const CANONICAL = "outline";
29*04fd306cSNickeau    private const OUTLINE_HEADING_PREFIX = "outline-heading";
30*04fd306cSNickeau    const CONTEXT = self::CANONICAL;
31*04fd306cSNickeau    public const OUTLINE_HEADING_NUMBERING = "outline-heading-numbering";
32*04fd306cSNickeau    public const TOC_NUMBERING = "toc-numbering";
33*04fd306cSNickeau    /**
34*04fd306cSNickeau     * As seen on
35*04fd306cSNickeau     * https://drafts.csswg.org/css-counter-styles-3/#predefined-counters
36*04fd306cSNickeau     */
37*04fd306cSNickeau    public const CONF_COUNTER_STYLES_CHOICES = [
38*04fd306cSNickeau        'arabic-indic',
39*04fd306cSNickeau        'bengali',
40*04fd306cSNickeau        'cambodian/khmer',
41*04fd306cSNickeau        'cjk-decimal',
42*04fd306cSNickeau        'decimal',
43*04fd306cSNickeau        'decimal-leading-zero',
44*04fd306cSNickeau        'devanagari',
45*04fd306cSNickeau        'georgian',
46*04fd306cSNickeau        'gujarati',
47*04fd306cSNickeau        'gurmukhi',
48*04fd306cSNickeau        'hebrew',
49*04fd306cSNickeau        'hiragana',
50*04fd306cSNickeau        'hiragana-iroha',
51*04fd306cSNickeau        'kannada',
52*04fd306cSNickeau        'katakana',
53*04fd306cSNickeau        'katakana-iroha',
54*04fd306cSNickeau        'lao',
55*04fd306cSNickeau        'lower-alpha',
56*04fd306cSNickeau        'lower-armenian',
57*04fd306cSNickeau        'lower-greek',
58*04fd306cSNickeau        'lower-roman',
59*04fd306cSNickeau        'malayalam',
60*04fd306cSNickeau        'mongolian',
61*04fd306cSNickeau        'myanmar',
62*04fd306cSNickeau        'oriya',
63*04fd306cSNickeau        'persian',
64*04fd306cSNickeau        'tamil',
65*04fd306cSNickeau        'telugu',
66*04fd306cSNickeau        'thai',
67*04fd306cSNickeau        'tibetan',
68*04fd306cSNickeau        'upper-alpha',
69*04fd306cSNickeau        'upper-armenian',
70*04fd306cSNickeau        'upper-roman'
71*04fd306cSNickeau    ];
72*04fd306cSNickeau    public const CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL4 = "outlineNumberingCounterStyleLevel4";
73*04fd306cSNickeau    public const CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL3 = "outlineNumberingCounterStyleLevel3";
74*04fd306cSNickeau    public const CONF_OUTLINE_NUMBERING_SUFFIX = "outlineNumberingSuffix";
75*04fd306cSNickeau    public const CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL2 = "outlineNumberingCounterStyleLevel2";
76*04fd306cSNickeau    public const CONF_OUTLINE_NUMBERING_COUNTER_SEPARATOR = "outlineNumberingCounterSeparator";
77*04fd306cSNickeau    public const CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL6 = "outlineNumberingCounterStyleLevel6";
78*04fd306cSNickeau    public const CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL5 = "outlineNumberingCounterStyleLevel5";
79*04fd306cSNickeau    public const CONF_OUTLINE_NUMBERING_PREFIX = "outlineNumberingPrefix";
80*04fd306cSNickeau    public const CONF_OUTLINE_NUMBERING_ENABLE = "outlineNumberingEnable";
81*04fd306cSNickeau    /**
82*04fd306cSNickeau     * To add hash tag to heading
83*04fd306cSNickeau     */
84*04fd306cSNickeau    public const OUTLINE_ANCHOR = "outline-anchor";
85*04fd306cSNickeau    const CONF_OUTLINE_NUMBERING_ENABLE_DEFAULT = 1;
86*04fd306cSNickeau    private OutlineSection $rootSection;
87*04fd306cSNickeau
88*04fd306cSNickeau    private OutlineSection $actualSection; // the actual section that is created
89*04fd306cSNickeau    private Call $actualHeadingCall; // the heading that is parsed
90*04fd306cSNickeau    private int $actualHeadingParsingState = DOKU_LEXER_EXIT;  // the state of the heading parsed (enter, closed), enter if we have entered an heading, exit if not;
91*04fd306cSNickeau    private ?MarkupPath $markupPath = null;
92*04fd306cSNickeau    private bool $isFragment;
93*04fd306cSNickeau    private bool $metaHeaderCapture;
94*04fd306cSNickeau
95*04fd306cSNickeau    /**
96*04fd306cSNickeau     * @param CallStack $callStack
97*04fd306cSNickeau     * @param MarkupPath|null $markup - needed to store the parsed toc, h1, ... (null if the markup is dynamic)
98*04fd306cSNickeau     * @param bool $isFragment - needed to control the structure of the outline (if this is a preview, the first heading may be not h1)
99*04fd306cSNickeau     * @return void
100*04fd306cSNickeau     */
101*04fd306cSNickeau    public function __construct(CallStack $callStack, MarkupPath $markup = null, bool $isFragment = false)
102*04fd306cSNickeau    {
103*04fd306cSNickeau        if ($markup !== null) {
104*04fd306cSNickeau            $this->markupPath = $markup;
105*04fd306cSNickeau        }
106*04fd306cSNickeau        $this->isFragment = $isFragment;
107*04fd306cSNickeau        $this->buildOutline($callStack);
108*04fd306cSNickeau        $this->storeH1();
109*04fd306cSNickeau        $this->storeTocForMarkupIfAny();
110*04fd306cSNickeau    }
111*04fd306cSNickeau
112*04fd306cSNickeau    /**
113*04fd306cSNickeau     * @param CallStack $callStack
114*04fd306cSNickeau     * @param MarkupPath|null $markupPath - needed to store the parsed toc, h1, ... (null if the markup is dynamic)
115*04fd306cSNickeau     * @param bool $isFragment - needed to control the structure of the outline (if this is a preview, the first heading may be not h1)
116*04fd306cSNickeau     * @return Outline
117*04fd306cSNickeau     */
118*04fd306cSNickeau    public static function createFromCallStack(CallStack $callStack, MarkupPath $markupPath = null, bool $isFragment = false): Outline
119*04fd306cSNickeau    {
120*04fd306cSNickeau        return new Outline($callStack, $markupPath, $isFragment);
121*04fd306cSNickeau    }
122*04fd306cSNickeau
123*04fd306cSNickeau    private function buildOutline(CallStack $callStack)
124*04fd306cSNickeau    {
125*04fd306cSNickeau
126*04fd306cSNickeau        /**
127*04fd306cSNickeau         * {@link syntax_plugin_combo_analytics tag analytics}
128*04fd306cSNickeau         * By default, in a test environment
129*04fd306cSNickeau         * this is set to 0
130*04fd306cSNickeau         * to speed test and to not pollute
131*04fd306cSNickeau         */
132*04fd306cSNickeau        $analtyicsEnabled = SiteConfig::getConfValue(syntax_plugin_combo_analytics::CONF_SYNTAX_ANALYTICS_ENABLE, true);
133*04fd306cSNickeau        $analyticsTagUsed = [];
134*04fd306cSNickeau
135*04fd306cSNickeau        /**
136*04fd306cSNickeau         * Processing variable about the context
137*04fd306cSNickeau         */
138*04fd306cSNickeau        $this->rootSection = OutlineSection::createOutlineRoot()
139*04fd306cSNickeau            ->setStartPosition(0);
140*04fd306cSNickeau        $this->actualSection = $this->rootSection;
141*04fd306cSNickeau        $actualLastPosition = 0;
142*04fd306cSNickeau        $callStack->moveToStart();
143*04fd306cSNickeau        while ($actualCall = $callStack->next()) {
144*04fd306cSNickeau
145*04fd306cSNickeau
146*04fd306cSNickeau            $state = $actualCall->getState();
147*04fd306cSNickeau
148*04fd306cSNickeau            /**
149*04fd306cSNickeau             * Block Post Processing
150*04fd306cSNickeau             * to not get any unwanted p
151*04fd306cSNickeau             * to counter {@link Block::process()}
152*04fd306cSNickeau             * setting dynamically the {@link SyntaxPlugin::getPType()}
153*04fd306cSNickeau             *
154*04fd306cSNickeau             * Unfortunately, it can't work because this is called after
155*04fd306cSNickeau             * {@link Block::process()}
156*04fd306cSNickeau             */
157*04fd306cSNickeau            if ($analtyicsEnabled) {
158*04fd306cSNickeau
159*04fd306cSNickeau                if (in_array($state, CallStack::TAG_STATE)) {
160*04fd306cSNickeau                    $tagName = $actualCall->getComponentName();
161*04fd306cSNickeau                    // The dokuwiki component name have open in their name
162*04fd306cSNickeau                    $tagName = str_replace("_open", "", $tagName);
163*04fd306cSNickeau                    if (isset($analyticsTagUsed[$tagName])) {
164*04fd306cSNickeau                        $analyticsTagUsed[$tagName] = $analyticsTagUsed[$tagName] + 1;
165*04fd306cSNickeau                    } else {
166*04fd306cSNickeau                        $analyticsTagUsed[$tagName] = 1;
167*04fd306cSNickeau                    }
168*04fd306cSNickeau                }
169*04fd306cSNickeau
170*04fd306cSNickeau            }
171*04fd306cSNickeau
172*04fd306cSNickeau            /**
173*04fd306cSNickeau             * We don't take the outline and document call if any
174*04fd306cSNickeau             * This is the case when we build from an actual stored instructions
175*04fd306cSNickeau             * (to bundle multiple page for instance)
176*04fd306cSNickeau             */
177*04fd306cSNickeau            $tagName = $actualCall->getTagName();
178*04fd306cSNickeau            switch ($tagName) {
179*04fd306cSNickeau                case "document_start":
180*04fd306cSNickeau                case "document_end":
181*04fd306cSNickeau                case SectionTag::TAG:
182*04fd306cSNickeau                    continue 2;
183*04fd306cSNickeau                case syntax_plugin_combo_header::TAG:
184*04fd306cSNickeau                    if ($actualCall->isPluginCall() && $actualCall->getContext() === self::CONTEXT) {
185*04fd306cSNickeau                        continue 2;
186*04fd306cSNickeau                    }
187*04fd306cSNickeau            }
188*04fd306cSNickeau
189*04fd306cSNickeau            /**
190*04fd306cSNickeau             * Wrap the table
191*04fd306cSNickeau             */
192*04fd306cSNickeau            $componentName = $actualCall->getComponentName();
193*04fd306cSNickeau            if ($componentName === "table_open") {
194*04fd306cSNickeau                $position = $actualCall->getFirstMatchedCharacterPosition();
195*04fd306cSNickeau                $originalInstructionCall = &$actualCall->getInstructionCall();
196*04fd306cSNickeau                $originalInstructionCall = Call::createComboCall(
197*04fd306cSNickeau                    TableTag::TAG,
198*04fd306cSNickeau                    DOKU_LEXER_ENTER,
199*04fd306cSNickeau                    [PluginUtility::POSITION => $position],
200*04fd306cSNickeau                    null,
201*04fd306cSNickeau                    null,
202*04fd306cSNickeau                    null,
203*04fd306cSNickeau                    $position,
204*04fd306cSNickeau                    \syntax_plugin_combo_xmlblocktag::TAG
205*04fd306cSNickeau                )->toCallArray();
206*04fd306cSNickeau            }
207*04fd306cSNickeau
208*04fd306cSNickeau            /**
209*04fd306cSNickeau             * Enter new section ?
210*04fd306cSNickeau             */
211*04fd306cSNickeau            $shouldWeCreateASection = false;
212*04fd306cSNickeau            switch ($tagName) {
213*04fd306cSNickeau                case syntax_plugin_combo_headingatx::TAG:
214*04fd306cSNickeau                    $actualCall->setState(DOKU_LEXER_ENTER);
215*04fd306cSNickeau                    if ($actualCall->getContext() === HeadingTag::TYPE_OUTLINE) {
216*04fd306cSNickeau                        $shouldWeCreateASection = true;
217*04fd306cSNickeau                    }
218*04fd306cSNickeau                    $this->enterHeading($actualCall);
219*04fd306cSNickeau                    break;
220*04fd306cSNickeau                case HeadingTag::HEADING_TAG:
221*04fd306cSNickeau                case syntax_plugin_combo_headingwiki::TAG:
222*04fd306cSNickeau                    if ($state == DOKU_LEXER_ENTER
223*04fd306cSNickeau                        && $actualCall->getContext() === HeadingTag::TYPE_OUTLINE) {
224*04fd306cSNickeau                        $shouldWeCreateASection = true;
225*04fd306cSNickeau                        $this->enterHeading($actualCall);
226*04fd306cSNickeau                    }
227*04fd306cSNickeau                    break;
228*04fd306cSNickeau                case "header":
229*04fd306cSNickeau                    // Should happen only on outline section
230*04fd306cSNickeau                    // we take over inside a component
231*04fd306cSNickeau                    if (!$actualCall->isPluginCall()) {
232*04fd306cSNickeau                        /**
233*04fd306cSNickeau                         * ie not {@link syntax_plugin_combo_header}
234*04fd306cSNickeau                         * but the dokuwiki header (ie heading)
235*04fd306cSNickeau                         */
236*04fd306cSNickeau                        $shouldWeCreateASection = true;
237*04fd306cSNickeau                        $this->enterHeading($actualCall);
238*04fd306cSNickeau                    }
239*04fd306cSNickeau                    break;
240*04fd306cSNickeau            }
241*04fd306cSNickeau            if ($shouldWeCreateASection) {
242*04fd306cSNickeau                if ($this->actualSection->hasParent()) {
243*04fd306cSNickeau                    // -1 because the actual position is the start of the next section
244*04fd306cSNickeau                    $this->actualSection->setEndPosition($actualCall->getFirstMatchedCharacterPosition() - 1);
245*04fd306cSNickeau                }
246*04fd306cSNickeau                $actualSectionLevel = $this->actualSection->getLevel();
247*04fd306cSNickeau
248*04fd306cSNickeau                if ($actualCall->isPluginCall()) {
249*04fd306cSNickeau                    try {
250*04fd306cSNickeau                        $newSectionLevel = DataType::toInteger($actualCall->getAttribute(HeadingTag::LEVEL));
251*04fd306cSNickeau                    } catch (ExceptionBadArgument $e) {
252*04fd306cSNickeau                        LogUtility::internalError("The level was not present on the heading call", self::CANONICAL);
253*04fd306cSNickeau                        $newSectionLevel = $actualSectionLevel;
254*04fd306cSNickeau                    }
255*04fd306cSNickeau                } else {
256*04fd306cSNickeau                    $headerTagName = $tagName;
257*04fd306cSNickeau                    if ($headerTagName !== "header") {
258*04fd306cSNickeau                        throw new ExceptionRuntimeInternal("This is not a dokuwiki header call", self::CANONICAL);
259*04fd306cSNickeau                    }
260*04fd306cSNickeau                    $newSectionLevel = $actualCall->getInstructionCall()[1][1];
261*04fd306cSNickeau                }
262*04fd306cSNickeau
263*04fd306cSNickeau
264*04fd306cSNickeau                $newOutlineSection = OutlineSection::createFromEnterHeadingCall($actualCall);
265*04fd306cSNickeau                $sectionDiff = $newSectionLevel - $actualSectionLevel;
266*04fd306cSNickeau                if ($sectionDiff > 0) {
267*04fd306cSNickeau
268*04fd306cSNickeau                    /**
269*04fd306cSNickeau                     * A child of the actual section
270*04fd306cSNickeau                     * We append it first before the message check to
271*04fd306cSNickeau                     * build the {@link TreeNode::getTreeIdentifier()}
272*04fd306cSNickeau                     */
273*04fd306cSNickeau                    try {
274*04fd306cSNickeau                        $this->actualSection->appendChild($newOutlineSection);
275*04fd306cSNickeau                    } catch (ExceptionBadState $e) {
276*04fd306cSNickeau                        throw new ExceptionRuntimeInternal("The node is not added multiple time, this error should not fired. Error:{$e->getMessage()}", self::CANONICAL, 1, $e);
277*04fd306cSNickeau                    }
278*04fd306cSNickeau
279*04fd306cSNickeau                    if ($sectionDiff > 1 & !($actualSectionLevel === 0 && $newSectionLevel === 2)) {
280*04fd306cSNickeau                        $expectedLevel = $actualSectionLevel + 1;
281*04fd306cSNickeau                        if ($actualSectionLevel === 0) {
282*04fd306cSNickeau                            /**
283*04fd306cSNickeau                             * In a fragment run (preview),
284*04fd306cSNickeau                             * the first heading may not be the first one
285*04fd306cSNickeau                             */
286*04fd306cSNickeau                            if (!$this->isFragment) {
287*04fd306cSNickeau                                $message = "The first section heading should have the level 1 or 2 (not $newSectionLevel).";
288*04fd306cSNickeau                            }
289*04fd306cSNickeau                        } else {
290*04fd306cSNickeau                            $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).";
291*04fd306cSNickeau                        }
292*04fd306cSNickeau                        if (isset($message)) {
293*04fd306cSNickeau                            LogUtility::warning($message, self::CANONICAL);
294*04fd306cSNickeau                        }
295*04fd306cSNickeau                        $actualCall->setAttribute(HeadingTag::LEVEL, $newSectionLevel);
296*04fd306cSNickeau                    }
297*04fd306cSNickeau
298*04fd306cSNickeau                } else {
299*04fd306cSNickeau
300*04fd306cSNickeau                    /**
301*04fd306cSNickeau                     * A child of the parent section, A sibling of the actual session
302*04fd306cSNickeau                     */
303*04fd306cSNickeau                    try {
304*04fd306cSNickeau                        $parent = $this->actualSection->getParent();
305*04fd306cSNickeau                        for ($i = 0; $i < abs($sectionDiff); $i++) {
306*04fd306cSNickeau                            $parent = $parent->getParent();
307*04fd306cSNickeau                        }
308*04fd306cSNickeau                        try {
309*04fd306cSNickeau                            $parent->appendChild($newOutlineSection);
310*04fd306cSNickeau                        } catch (ExceptionBadState $e) {
311*04fd306cSNickeau                            throw new ExceptionRuntimeInternal("The node is not added multiple time, this error should not fired. Error:{$e->getMessage()}", self::CANONICAL, 1, $e);
312*04fd306cSNickeau                        }
313*04fd306cSNickeau                    } catch (ExceptionNotFound $e) {
314*04fd306cSNickeau                        /**
315*04fd306cSNickeau                         * no parent
316*04fd306cSNickeau                         * May happen in preview (ie fragment)
317*04fd306cSNickeau                         */
318*04fd306cSNickeau                        if (!$this->isFragment) {
319*04fd306cSNickeau                            LogUtility::internalError("Due to the level logic, the actual section should have a parent");
320*04fd306cSNickeau                        }
321*04fd306cSNickeau                        try {
322*04fd306cSNickeau                            $this->actualSection->appendChild($newOutlineSection);
323*04fd306cSNickeau                        } catch (ExceptionBadState $e) {
324*04fd306cSNickeau                            throw new ExceptionRuntimeInternal("The node is not added multiple time, this error should not fired. Error:{$e->getMessage()}", self::CANONICAL, 1, $e);
325*04fd306cSNickeau                        }
326*04fd306cSNickeau                    }
327*04fd306cSNickeau
328*04fd306cSNickeau                }
329*04fd306cSNickeau
330*04fd306cSNickeau                $this->actualSection = $newOutlineSection;
331*04fd306cSNickeau                continue;
332*04fd306cSNickeau            }
333*04fd306cSNickeau
334*04fd306cSNickeau            /**
335*04fd306cSNickeau             * Track the number of lines
336*04fd306cSNickeau             * to inject ads
337*04fd306cSNickeau             */
338*04fd306cSNickeau            switch ($tagName) {
339*04fd306cSNickeau                case "linebreak":
340*04fd306cSNickeau                case "tablerow":
341*04fd306cSNickeau                    // linebreak is an inline component
342*04fd306cSNickeau                    $this->actualSection->incrementLineNumber();
343*04fd306cSNickeau                    break;
344*04fd306cSNickeau                default:
345*04fd306cSNickeau                    $display = $actualCall->getDisplay();
346*04fd306cSNickeau                    if ($display === Call::BlOCK_DISPLAY) {
347*04fd306cSNickeau                        $this->actualSection->incrementLineNumber();
348*04fd306cSNickeau                    }
349*04fd306cSNickeau                    break;
350*04fd306cSNickeau            }
351*04fd306cSNickeau
352*04fd306cSNickeau            /**
353*04fd306cSNickeau             * Track the position in the file
354*04fd306cSNickeau             */
355*04fd306cSNickeau            $currentLastPosition = $actualCall->getLastMatchedCharacterPosition();
356*04fd306cSNickeau            if ($currentLastPosition > $actualLastPosition) {
357*04fd306cSNickeau                // the position in the stack is not always good
358*04fd306cSNickeau                $actualLastPosition = $currentLastPosition;
359*04fd306cSNickeau            }
360*04fd306cSNickeau
361*04fd306cSNickeau
362*04fd306cSNickeau            switch ($actualCall->getComponentName()) {
363*04fd306cSNickeau                case \action_plugin_combo_instructionspostprocessing::EDIT_SECTION_OPEN:
364*04fd306cSNickeau                case \action_plugin_combo_instructionspostprocessing::EDIT_SECTION_CLOSE:
365*04fd306cSNickeau                    // we don't store them
366*04fd306cSNickeau                    continue 2;
367*04fd306cSNickeau            }
368*04fd306cSNickeau
369*04fd306cSNickeau            /**
370*04fd306cSNickeau             * Close/Process the heading description
371*04fd306cSNickeau             */
372*04fd306cSNickeau            if ($this->actualHeadingParsingState === DOKU_LEXER_ENTER) {
373*04fd306cSNickeau                switch ($tagName) {
374*04fd306cSNickeau
375*04fd306cSNickeau                    case HeadingTag::HEADING_TAG:
376*04fd306cSNickeau                    case syntax_plugin_combo_headingwiki::TAG:
377*04fd306cSNickeau                        if ($state == DOKU_LEXER_EXIT) {
378*04fd306cSNickeau                            $this->addCallToSection($actualCall);
379*04fd306cSNickeau                            $this->exitHeading();
380*04fd306cSNickeau                            continue 2;
381*04fd306cSNickeau                        }
382*04fd306cSNickeau                        break;
383*04fd306cSNickeau
384*04fd306cSNickeau                    case "internalmedia":
385*04fd306cSNickeau                        // no link for media in heading
386*04fd306cSNickeau                        $actualCall->getInstructionCall()[1][6] = MediaMarkup::LINKING_NOLINK_VALUE;
387*04fd306cSNickeau                        break;
388*04fd306cSNickeau                    case syntax_plugin_combo_media::TAG:
389*04fd306cSNickeau                        // no link for media in heading
390*04fd306cSNickeau                        $actualCall->addAttribute(MediaMarkup::LINKING_KEY, MediaMarkup::LINKING_NOLINK_VALUE);
391*04fd306cSNickeau                        break;
392*04fd306cSNickeau
393*04fd306cSNickeau                    case "header":
394*04fd306cSNickeau                        if (SiteConfig::getConfValue(syntax_plugin_combo_headingwiki::CONF_WIKI_HEADING_ENABLE, syntax_plugin_combo_headingwiki::CONF_DEFAULT_WIKI_ENABLE_VALUE) == 1) {
395*04fd306cSNickeau                            LogUtility::msg("The combo heading wiki is enabled, we should not see `header` calls in the call stack");
396*04fd306cSNickeau                        }
397*04fd306cSNickeau                        break;
398*04fd306cSNickeau
399*04fd306cSNickeau                    case "p":
400*04fd306cSNickeau
401*04fd306cSNickeau                        if ($this->actualHeadingCall->getTagName() === syntax_plugin_combo_headingatx::TAG) {
402*04fd306cSNickeau                            // A new p is the end of an atx call
403*04fd306cSNickeau                            switch ($actualCall->getComponentName()) {
404*04fd306cSNickeau                                case "p_open":
405*04fd306cSNickeau                                    // We don't take the p tag inside atx heading
406*04fd306cSNickeau                                    // therefore we continue
407*04fd306cSNickeau                                    continue 3;
408*04fd306cSNickeau                                case "p_close":
409*04fd306cSNickeau                                    $endAtxCall = Call::createComboCall(
410*04fd306cSNickeau                                        syntax_plugin_combo_headingatx::TAG,
411*04fd306cSNickeau                                        DOKU_LEXER_EXIT,
412*04fd306cSNickeau                                        $this->actualHeadingCall->getAttributes(),
413*04fd306cSNickeau                                        $this->actualHeadingCall->getContext(),
414*04fd306cSNickeau                                    );
415*04fd306cSNickeau                                    $this->addCallToSection($endAtxCall);
416*04fd306cSNickeau                                    $this->exitHeading();
417*04fd306cSNickeau                                    // We don't take the p tag inside atx heading
418*04fd306cSNickeau                                    // therefore we continue
419*04fd306cSNickeau                                    continue 3;
420*04fd306cSNickeau                            }
421*04fd306cSNickeau                        }
422*04fd306cSNickeau                        break;
423*04fd306cSNickeau
424*04fd306cSNickeau                }
425*04fd306cSNickeau            }
426*04fd306cSNickeau            $this->addCallToSection($actualCall);
427*04fd306cSNickeau        }
428*04fd306cSNickeau
429*04fd306cSNickeau        // empty text
430*04fd306cSNickeau        if (sizeof($analyticsTagUsed) > 0) {
431*04fd306cSNickeau            $pluginAnalyticsCall = Call::createComboCall(
432*04fd306cSNickeau                syntax_plugin_combo_analytics::TAG,
433*04fd306cSNickeau                DOKU_LEXER_SPECIAL,
434*04fd306cSNickeau                $analyticsTagUsed
435*04fd306cSNickeau            );
436*04fd306cSNickeau            $this->addCallToSection($pluginAnalyticsCall);
437*04fd306cSNickeau        }
438*04fd306cSNickeau
439*04fd306cSNickeau        // Add label the heading text to the metadata
440*04fd306cSNickeau        $this->saveOutlineToMetadata();
441*04fd306cSNickeau
442*04fd306cSNickeau
443*04fd306cSNickeau    }
444*04fd306cSNickeau
445*04fd306cSNickeau    public static function getOutlineHeadingClass(): string
446*04fd306cSNickeau    {
447*04fd306cSNickeau        return StyleAttribute::addComboStrapSuffix(self::OUTLINE_HEADING_PREFIX);
448*04fd306cSNickeau    }
449*04fd306cSNickeau
450*04fd306cSNickeau    public function getRootOutlineSection(): OutlineSection
451*04fd306cSNickeau    {
452*04fd306cSNickeau        return $this->rootSection;
453*04fd306cSNickeau
454*04fd306cSNickeau    }
455*04fd306cSNickeau
456*04fd306cSNickeau    /**
457*04fd306cSNickeau     */
458*04fd306cSNickeau    public static function merge(Outline $inner, Outline $outer)
459*04fd306cSNickeau    {
460*04fd306cSNickeau        /**
461*04fd306cSNickeau         * Get the inner section where the outer section will be added
462*04fd306cSNickeau         */
463*04fd306cSNickeau        $innerRootOutlineSection = $inner->getRootOutlineSection();
464*04fd306cSNickeau        $innerTopSections = $innerRootOutlineSection->getChildren();
465*04fd306cSNickeau        if (count($innerTopSections) === 0) {
466*04fd306cSNickeau            $firstInnerSection = $innerRootOutlineSection;
467*04fd306cSNickeau        } else {
468*04fd306cSNickeau            $firstInnerSection = $innerTopSections[count($innerTopSections)];
469*04fd306cSNickeau        }
470*04fd306cSNickeau        $firstInnerSectionLevel = $firstInnerSection->getLevel();
471*04fd306cSNickeau
472*04fd306cSNickeau        /**
473*04fd306cSNickeau         * Add the outer sections
474*04fd306cSNickeau         */
475*04fd306cSNickeau        $outerRootOutlineSection = $outer->getRootOutlineSection();
476*04fd306cSNickeau        foreach ($outerRootOutlineSection->getChildren() as $childOuterSection) {
477*04fd306cSNickeau            /**
478*04fd306cSNickeau             * One level less than where the section is included
479*04fd306cSNickeau             */
480*04fd306cSNickeau            $childOuterSection->setLevel($firstInnerSectionLevel + 1);
481*04fd306cSNickeau            $childOuterSection->detachBeforeAppend();
482*04fd306cSNickeau            try {
483*04fd306cSNickeau                $firstInnerSection->appendChild($childOuterSection);
484*04fd306cSNickeau            } catch (ExceptionBadState $e) {
485*04fd306cSNickeau                // We add the node only once. This error should not happen
486*04fd306cSNickeau                throw new ExceptionRuntimeInternal("Error while adding a section during the outline merge. Error: {$e->getMessage()}", self::CANONICAL, 1, $e);
487*04fd306cSNickeau            }
488*04fd306cSNickeau        }
489*04fd306cSNickeau
490*04fd306cSNickeau    }
491*04fd306cSNickeau
492*04fd306cSNickeau    public static function mergeRecurse(Outline $inner, Outline $outer)
493*04fd306cSNickeau    {
494*04fd306cSNickeau        $innerRootOutlineSection = $inner->getRootOutlineSection();
495*04fd306cSNickeau        $outerRootOutlineSection = $outer->getRootOutlineSection();
496*04fd306cSNickeau
497*04fd306cSNickeau    }
498*04fd306cSNickeau
499*04fd306cSNickeau    /**
500*04fd306cSNickeau     * Utility class to create a outline from a markup string
501*04fd306cSNickeau     * @param string $content
502*04fd306cSNickeau     * @return Outline
503*04fd306cSNickeau     */
504*04fd306cSNickeau    public static function createFromMarkup(string $content, Path $contentPath, WikiPath $contextPath): Outline
505*04fd306cSNickeau    {
506*04fd306cSNickeau        $instructions = MarkupRenderer::createFromMarkup($content, $contentPath, $contextPath)
507*04fd306cSNickeau            ->setRequestedMimeToInstruction()
508*04fd306cSNickeau            ->getOutput();
509*04fd306cSNickeau        $callStack = CallStack::createFromInstructions($instructions);
510*04fd306cSNickeau        return Outline::createFromCallStack($callStack);
511*04fd306cSNickeau    }
512*04fd306cSNickeau
513*04fd306cSNickeau    /**
514*04fd306cSNickeau     * Get the heading numbering snippet
515*04fd306cSNickeau     * @param string $type heading or toc - for {@link Outline::TOC_NUMBERING} or {@link Outline::OUTLINE_HEADING_NUMBERING}
516*04fd306cSNickeau     * @return string - the css internal stylesheet
517*04fd306cSNickeau     * @throws ExceptionNotEnabled
518*04fd306cSNickeau     * @throws ExceptionBadSyntax
519*04fd306cSNickeau     * Page on DokuWiki
520*04fd306cSNickeau     * https://www.dokuwiki.org/tips:numbered_headings
521*04fd306cSNickeau     */
522*04fd306cSNickeau    public static function getCssNumberingRulesFor(string $type): string
523*04fd306cSNickeau    {
524*04fd306cSNickeau
525*04fd306cSNickeau        $enable = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_ENABLE, Outline::CONF_OUTLINE_NUMBERING_ENABLE_DEFAULT);
526*04fd306cSNickeau        if (!$enable) {
527*04fd306cSNickeau            throw new ExceptionNotEnabled();
528*04fd306cSNickeau        }
529*04fd306cSNickeau
530*04fd306cSNickeau        $level2CounterStyle = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL2, "decimal");
531*04fd306cSNickeau        $level3CounterStyle = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL3, "decimal");
532*04fd306cSNickeau        $level4CounterStyle = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL4, "decimal");
533*04fd306cSNickeau        $level5CounterStyle = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL5, "decimal");
534*04fd306cSNickeau        $level6CounterStyle = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL6, "decimal");
535*04fd306cSNickeau        $counterSeparator = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_SEPARATOR, ".");
536*04fd306cSNickeau        $prefix = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_PREFIX, "");
537*04fd306cSNickeau        $suffix = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_SUFFIX, " - ");
538*04fd306cSNickeau
539*04fd306cSNickeau        switch ($type) {
540*04fd306cSNickeau
541*04fd306cSNickeau            case self::OUTLINE_HEADING_NUMBERING:
542*04fd306cSNickeau                global $ACT;
543*04fd306cSNickeau                if ($ACT === "preview") {
544*04fd306cSNickeau                    $mainContainerSelector = ".pad";
545*04fd306cSNickeau                } else {
546*04fd306cSNickeau                    $mainContainerSelector = "#" . TemplateSlot::MAIN_CONTENT_ID;
547*04fd306cSNickeau                }
548*04fd306cSNickeau                /**
549*04fd306cSNickeau                 * Because the HTML file structure is not really fixed
550*04fd306cSNickeau                 * (we may have section HTML element with a bar, the sectioning heading
551*04fd306cSNickeau                 * may be not enabled)
552*04fd306cSNickeau                 * We can't select via html structure
553*04fd306cSNickeau                 * the outline heading consistently
554*04fd306cSNickeau                 * We do it then with the class value
555*04fd306cSNickeau                 */
556*04fd306cSNickeau                $outlineClass = Outline::getOutlineHeadingClass();
557*04fd306cSNickeau                return <<<EOF
558*04fd306cSNickeau$mainContainerSelector { counter-set: h2 0 h3 0 h4 0 h5 0 h6 0; }
559*04fd306cSNickeau$mainContainerSelector h2.$outlineClass::before { counter-increment: h2; counter-set: h3 0 h4 0 h5 0 h6 0; content: "$prefix" counter(h2, $level2CounterStyle) "$suffix\A"; }
560*04fd306cSNickeau$mainContainerSelector h3.$outlineClass::before { counter-increment: h3; counter-set: h4 0 h5 0 h6 0; content: "$prefix" counter(h2, $level2CounterStyle) "$counterSeparator" counter(h3,$level3CounterStyle) "$suffix\A"; }
561*04fd306cSNickeau$mainContainerSelector h4.$outlineClass::before { counter-increment: h4; counter-set: h5 0 h6 0; content: "$prefix" counter(h2, $level2CounterStyle) "$counterSeparator" counter(h3,$level3CounterStyle) "$counterSeparator" counter(h4,$level4CounterStyle) "$suffix\A"; }
562*04fd306cSNickeau$mainContainerSelector h5.$outlineClass::before { counter-increment: h5; counter-set: h6 0; content: "$prefix" counter(h2, $level2CounterStyle) "$counterSeparator" counter(h3,$level3CounterStyle) "$counterSeparator" counter(h4,$level4CounterStyle) "$counterSeparator" counter(h5,$level5CounterStyle) "$suffix\A"; }
563*04fd306cSNickeau$mainContainerSelector h6.$outlineClass::before { counter-increment: h6; content: "$prefix" counter(h2, $level2CounterStyle) "$counterSeparator" counter(h3,$level3CounterStyle) "$counterSeparator" counter(h4,$level4CounterStyle) "$counterSeparator" counter(h5,$level5CounterStyle) "$counterSeparator" counter(h6,$level6CounterStyle) "$suffix\A"; }
564*04fd306cSNickeauEOF;
565*04fd306cSNickeau            case self::TOC_NUMBERING:
566*04fd306cSNickeau                /**
567*04fd306cSNickeau                 * The level counter on the toc are based
568*04fd306cSNickeau                 * on the https://www.dokuwiki.org/config:toptoclevel
569*04fd306cSNickeau                 * configuration
570*04fd306cSNickeau                 * if toptoclevel = 2, then level1 = h2 and not h1
571*04fd306cSNickeau                 * @deprecated
572*04fd306cSNickeau                 */
573*04fd306cSNickeau                // global $conf;
574*04fd306cSNickeau                // $topTocLevel = $conf['toptoclevel'];
575*04fd306cSNickeau
576*04fd306cSNickeau                $tocSelector = "." . Toc::getClass() . " ul";
577*04fd306cSNickeau                return <<<EOF
578*04fd306cSNickeau$tocSelector li { counter-increment: toc2; }
579*04fd306cSNickeau$tocSelector li li { counter-increment: toc3; }
580*04fd306cSNickeau$tocSelector li li li { counter-increment: toc4; }
581*04fd306cSNickeau$tocSelector li li li li { counter-increment: toc5; }
582*04fd306cSNickeau$tocSelector li li li li li { counter-increment: toc6; }
583*04fd306cSNickeau$tocSelector li a::before { content: "$prefix" counter(toc2, $level2CounterStyle) "$suffix\A"; }
584*04fd306cSNickeau$tocSelector li li a::before { content: "$prefix" counter(toc2, $level2CounterStyle) "$counterSeparator" counter(toc3,$level3CounterStyle) "$suffix\A"; }
585*04fd306cSNickeau$tocSelector li li li a::before { content: "$prefix" counter(toc2, $level2CounterStyle) "$counterSeparator" counter(toc3,$level3CounterStyle) "$counterSeparator" counter(toc4,$level4CounterStyle) "$suffix\A"; }
586*04fd306cSNickeau$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"; }
587*04fd306cSNickeau$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"; }
588*04fd306cSNickeauEOF;
589*04fd306cSNickeau
590*04fd306cSNickeau            default:
591*04fd306cSNickeau                throw new ExceptionBadSyntax("The type ($type) is unknown");
592*04fd306cSNickeau        }
593*04fd306cSNickeau
594*04fd306cSNickeau
595*04fd306cSNickeau    }
596*04fd306cSNickeau
597*04fd306cSNickeau    /**
598*04fd306cSNickeau     * @throws ExceptionNotFound
599*04fd306cSNickeau     */
600*04fd306cSNickeau    public static function createFromMarkupPath(MarkupPath $markupPath): Outline
601*04fd306cSNickeau    {
602*04fd306cSNickeau        $path = $markupPath->getPathObject();
603*04fd306cSNickeau        if (!($path instanceof WikiPath)) {
604*04fd306cSNickeau            throw new ExceptionRuntimeInternal("The path is not a wiki path");
605*04fd306cSNickeau        }
606*04fd306cSNickeau        $markup = FileSystems::getContent($path);
607*04fd306cSNickeau        $instructions = MarkupRenderer::createFromMarkup($markup, $path, $path)
608*04fd306cSNickeau            ->setRequestedMimeToInstruction()
609*04fd306cSNickeau            ->getOutput();
610*04fd306cSNickeau        $callStack = CallStack::createFromInstructions($instructions);
611*04fd306cSNickeau        return new Outline($callStack, $markupPath);
612*04fd306cSNickeau    }
613*04fd306cSNickeau
614*04fd306cSNickeau    public function getInstructionCalls(): array
615*04fd306cSNickeau    {
616*04fd306cSNickeau        $totalInstructionCalls = [];
617*04fd306cSNickeau        $collectCalls = function (OutlineSection $outlineSection) use (&$totalInstructionCalls) {
618*04fd306cSNickeau            $instructionCalls = array_map(function (Call $element) {
619*04fd306cSNickeau                return $element->getInstructionCall();
620*04fd306cSNickeau            }, $outlineSection->getCalls());
621*04fd306cSNickeau            $totalInstructionCalls = array_merge($totalInstructionCalls, $instructionCalls);
622*04fd306cSNickeau        };
623*04fd306cSNickeau        TreeVisit::visit($this->rootSection, $collectCalls);
624*04fd306cSNickeau        return $totalInstructionCalls;
625*04fd306cSNickeau    }
626*04fd306cSNickeau
627*04fd306cSNickeau    public function toDokuWikiTemplateInstructionCalls(): array
628*04fd306cSNickeau    {
629*04fd306cSNickeau        $totalInstructionCalls = [];
630*04fd306cSNickeau        $sectionSequenceId = 0;
631*04fd306cSNickeau        $collectCalls = function (OutlineSection $outlineSection) use (&$totalInstructionCalls, &$sectionSequenceId) {
632*04fd306cSNickeau
633*04fd306cSNickeau            $wikiSectionOpen = Call::createNativeCall(
634*04fd306cSNickeau                \action_plugin_combo_instructionspostprocessing::EDIT_SECTION_OPEN,
635*04fd306cSNickeau                array($outlineSection->getLevel()),
636*04fd306cSNickeau                $outlineSection->getStartPosition()
637*04fd306cSNickeau            );
638*04fd306cSNickeau            $wikiSectionClose = Call::createNativeCall(
639*04fd306cSNickeau                \action_plugin_combo_instructionspostprocessing::EDIT_SECTION_CLOSE,
640*04fd306cSNickeau                array(),
641*04fd306cSNickeau                $outlineSection->getEndPosition()
642*04fd306cSNickeau            );
643*04fd306cSNickeau
644*04fd306cSNickeau
645*04fd306cSNickeau            if ($outlineSection->hasParent()) {
646*04fd306cSNickeau
647*04fd306cSNickeau
648*04fd306cSNickeau                $sectionCalls = array_merge(
649*04fd306cSNickeau                    $outlineSection->getHeadingCalls(),
650*04fd306cSNickeau                    [$wikiSectionOpen],
651*04fd306cSNickeau                    $outlineSection->getContentCalls(),
652*04fd306cSNickeau                    [$wikiSectionClose],
653*04fd306cSNickeau                );
654*04fd306cSNickeau
655*04fd306cSNickeau                if ($this->isSectionEditingEnabled()) {
656*04fd306cSNickeau
657*04fd306cSNickeau                    /**
658*04fd306cSNickeau                     * Adding sectionedit class to be conform
659*04fd306cSNickeau                     * with the Dokuwiki {@link \Doku_Renderer_xhtml::header()} function
660*04fd306cSNickeau                     */
661*04fd306cSNickeau                    $sectionSequenceId++;
662*04fd306cSNickeau                    $headingCall = $outlineSection->getEnterHeadingCall();
663*04fd306cSNickeau                    if ($headingCall->isPluginCall()) {
664*04fd306cSNickeau                        $level = DataType::toIntegerOrDefaultIfNull($headingCall->getAttribute(HeadingTag::LEVEL), 0);
665*04fd306cSNickeau                        if ($level <= $this->getTocMaxLevel()) {
666*04fd306cSNickeau                            $headingCall->addClassName("sectionedit$sectionSequenceId");
667*04fd306cSNickeau                        }
668*04fd306cSNickeau                    }
669*04fd306cSNickeau
670*04fd306cSNickeau                    $editButton = EditButton::create($outlineSection->getLabel())
671*04fd306cSNickeau                        ->setStartPosition($outlineSection->getStartPosition())
672*04fd306cSNickeau                        ->setEndPosition($outlineSection->getEndPosition())
673*04fd306cSNickeau                        ->setOutlineHeadingId($outlineSection->getHeadingId())
674*04fd306cSNickeau                        ->setOutlineSectionId($sectionSequenceId)
675*04fd306cSNickeau                        ->toComboCallDokuWikiForm();
676*04fd306cSNickeau                    $sectionCalls[] = $editButton;
677*04fd306cSNickeau                }
678*04fd306cSNickeau
679*04fd306cSNickeau            } else {
680*04fd306cSNickeau                // dokuwiki seems to have no section for the content before the first heading
681*04fd306cSNickeau                $sectionCalls = $outlineSection->getContentCalls();
682*04fd306cSNickeau            }
683*04fd306cSNickeau
684*04fd306cSNickeau            $instructionCalls = array_map(function (Call $element) {
685*04fd306cSNickeau                return $element->getInstructionCall();
686*04fd306cSNickeau            }, $sectionCalls);
687*04fd306cSNickeau            $totalInstructionCalls = array_merge($totalInstructionCalls, $instructionCalls);
688*04fd306cSNickeau        };
689*04fd306cSNickeau        TreeVisit::visit($this->rootSection, $collectCalls);
690*04fd306cSNickeau        return $totalInstructionCalls;
691*04fd306cSNickeau    }
692*04fd306cSNickeau
693*04fd306cSNickeau    private function addCallToSection(Call $actualCall)
694*04fd306cSNickeau    {
695*04fd306cSNickeau        if ($this->actualHeadingParsingState === DOKU_LEXER_ENTER && !$this->actualSection->hasContentCall()) {
696*04fd306cSNickeau            $this->actualSection->addHeaderCall($actualCall);
697*04fd306cSNickeau        } else {
698*04fd306cSNickeau            // an content heading (not outline) or another call
699*04fd306cSNickeau            $this->actualSection->addContentCall($actualCall);
700*04fd306cSNickeau        }
701*04fd306cSNickeau    }
702*04fd306cSNickeau
703*04fd306cSNickeau    private function enterHeading(Call $actualCall)
704*04fd306cSNickeau    {
705*04fd306cSNickeau        $this->actualHeadingParsingState = DOKU_LEXER_ENTER;
706*04fd306cSNickeau        $this->actualHeadingCall = $actualCall;
707*04fd306cSNickeau    }
708*04fd306cSNickeau
709*04fd306cSNickeau    private function exitHeading()
710*04fd306cSNickeau    {
711*04fd306cSNickeau        $this->actualHeadingParsingState = DOKU_LEXER_EXIT;
712*04fd306cSNickeau    }
713*04fd306cSNickeau
714*04fd306cSNickeau    /**
715*04fd306cSNickeau     * @return array - Dokuwiki TOC array format
716*04fd306cSNickeau     */
717*04fd306cSNickeau    public function toTocDokuwikiFormat(): array
718*04fd306cSNickeau    {
719*04fd306cSNickeau
720*04fd306cSNickeau        $tableOfContent = [];
721*04fd306cSNickeau        $collectTableOfContent = function (OutlineSection $outlineSection) use (&$tableOfContent) {
722*04fd306cSNickeau
723*04fd306cSNickeau            if (!$outlineSection->hasParent()) {
724*04fd306cSNickeau                // Root Section, no heading
725*04fd306cSNickeau                return;
726*04fd306cSNickeau            }
727*04fd306cSNickeau            $tableOfContent[] = [
728*04fd306cSNickeau                'link' => '#' . $outlineSection->getHeadingId(),
729*04fd306cSNickeau                'title' => $outlineSection->getLabel(),
730*04fd306cSNickeau                'type' => 'ul',
731*04fd306cSNickeau                'level' => $outlineSection->getLevel()
732*04fd306cSNickeau            ];
733*04fd306cSNickeau
734*04fd306cSNickeau        };
735*04fd306cSNickeau        TreeVisit::visit($this->rootSection, $collectTableOfContent);
736*04fd306cSNickeau        return $tableOfContent;
737*04fd306cSNickeau
738*04fd306cSNickeau    }
739*04fd306cSNickeau
740*04fd306cSNickeau
741*04fd306cSNickeau    public
742*04fd306cSNickeau    function toHtmlSectionOutlineCalls(): array
743*04fd306cSNickeau    {
744*04fd306cSNickeau        return OutlineVisitor::create($this)->getCalls();
745*04fd306cSNickeau    }
746*04fd306cSNickeau
747*04fd306cSNickeau
748*04fd306cSNickeau    /**
749*04fd306cSNickeau     * Fragment Rendering
750*04fd306cSNickeau     * * does not have any section/edit button
751*04fd306cSNickeau     * * no outline or edit button for dynamic rendering but closing of atx heading
752*04fd306cSNickeau     *
753*04fd306cSNickeau     * The outline processing ({@link Outline::buildOutline()} just close the atx heading
754*04fd306cSNickeau     *
755*04fd306cSNickeau     * @return array
756*04fd306cSNickeau     */
757*04fd306cSNickeau    public
758*04fd306cSNickeau    function toFragmentInstructionCalls(): array
759*04fd306cSNickeau    {
760*04fd306cSNickeau        $totalInstructionCalls = [];
761*04fd306cSNickeau        $collectCalls = function (OutlineSection $outlineSection) use (&$totalInstructionCalls) {
762*04fd306cSNickeau
763*04fd306cSNickeau            $sectionCalls = array_merge(
764*04fd306cSNickeau                $outlineSection->getHeadingCalls(),
765*04fd306cSNickeau                $outlineSection->getContentCalls()
766*04fd306cSNickeau            );
767*04fd306cSNickeau
768*04fd306cSNickeau            $instructionCalls = array_map(function (Call $element) {
769*04fd306cSNickeau                return $element->getInstructionCall();
770*04fd306cSNickeau            }, $sectionCalls);
771*04fd306cSNickeau            $totalInstructionCalls = array_merge($totalInstructionCalls, $instructionCalls);
772*04fd306cSNickeau        };
773*04fd306cSNickeau        TreeVisit::visit($this->rootSection, $collectCalls);
774*04fd306cSNickeau        return $totalInstructionCalls;
775*04fd306cSNickeau
776*04fd306cSNickeau    }
777*04fd306cSNickeau
778*04fd306cSNickeau    /**
779*04fd306cSNickeau     * Add the label (ie heading text to the cal attribute)
780*04fd306cSNickeau     *
781*04fd306cSNickeau     * @return void
782*04fd306cSNickeau     */
783*04fd306cSNickeau    private
784*04fd306cSNickeau    function saveOutlineToMetadata()
785*04fd306cSNickeau    {
786*04fd306cSNickeau        try {
787*04fd306cSNickeau            $firstChild = $this->rootSection->getFirstChild();
788*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
789*04fd306cSNickeau            // no child
790*04fd306cSNickeau            return;
791*04fd306cSNickeau        }
792*04fd306cSNickeau        if ($firstChild->getLevel() === 1) {
793*04fd306cSNickeau            $headingCall = $firstChild->getEnterHeadingCall();
794*04fd306cSNickeau            // not dokuwiki header ?
795*04fd306cSNickeau            if ($headingCall->isPluginCall()) {
796*04fd306cSNickeau                $headingCall->setAttribute(HeadingTag::HEADING_TEXT_ATTRIBUTE, $firstChild->getLabel());
797*04fd306cSNickeau            }
798*04fd306cSNickeau        }
799*04fd306cSNickeau
800*04fd306cSNickeau    }
801*04fd306cSNickeau
802*04fd306cSNickeau
803*04fd306cSNickeau    private
804*04fd306cSNickeau    function storeH1()
805*04fd306cSNickeau    {
806*04fd306cSNickeau        try {
807*04fd306cSNickeau            $outlineSection = $this->getRootOutlineSection()->getFirstChild();
808*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
809*04fd306cSNickeau            //
810*04fd306cSNickeau            return;
811*04fd306cSNickeau        }
812*04fd306cSNickeau        if ($this->markupPath != null && $outlineSection->getLevel() === 1) {
813*04fd306cSNickeau            $label = $outlineSection->getLabel();
814*04fd306cSNickeau            $call = $outlineSection->getEnterHeadingCall();
815*04fd306cSNickeau            if ($call->isPluginCall()) {
816*04fd306cSNickeau                // we support also the dokwuiki header call that does not need the label
817*04fd306cSNickeau                $call->addAttribute(HeadingTag::PARSED_LABEL, $label);
818*04fd306cSNickeau            }
819*04fd306cSNickeau            PageH1::createForPage($this->markupPath)->setDefaultValue($label);
820*04fd306cSNickeau        }
821*04fd306cSNickeau    }
822*04fd306cSNickeau
823*04fd306cSNickeau    private
824*04fd306cSNickeau    function storeTocForMarkupIfAny()
825*04fd306cSNickeau    {
826*04fd306cSNickeau
827*04fd306cSNickeau        $toc = $this->toTocDokuwikiFormat();
828*04fd306cSNickeau
829*04fd306cSNickeau        try {
830*04fd306cSNickeau            $fetcherMarkup = ExecutionContext::getActualOrCreateFromEnv()->getExecutingMarkupHandler();
831*04fd306cSNickeau            $fetcherMarkup->toc = $toc;
832*04fd306cSNickeau            if ($fetcherMarkup->isDocument()) {
833*04fd306cSNickeau                /**
834*04fd306cSNickeau                 * We still update the global TOC Dokuwiki variables
835*04fd306cSNickeau                 */
836*04fd306cSNickeau                global $TOC;
837*04fd306cSNickeau                $TOC = $toc;
838*04fd306cSNickeau            }
839*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
840*04fd306cSNickeau            // outline is not runnned from a markup handler
841*04fd306cSNickeau        }
842*04fd306cSNickeau
843*04fd306cSNickeau        if (!isset($this->markupPath)) {
844*04fd306cSNickeau            return;
845*04fd306cSNickeau        }
846*04fd306cSNickeau
847*04fd306cSNickeau        try {
848*04fd306cSNickeau            Toc::createForPage($this->markupPath)
849*04fd306cSNickeau                ->setValue($toc)
850*04fd306cSNickeau                ->persist();
851*04fd306cSNickeau        } catch (ExceptionBadArgument $e) {
852*04fd306cSNickeau            LogUtility::error("The Toc could not be persisted. Error:{$e->getMessage()}");
853*04fd306cSNickeau        }
854*04fd306cSNickeau    }
855*04fd306cSNickeau
856*04fd306cSNickeau    public
857*04fd306cSNickeau    function getMarkupPath(): ?MarkupPath
858*04fd306cSNickeau    {
859*04fd306cSNickeau        return $this->markupPath;
860*04fd306cSNickeau    }
861*04fd306cSNickeau
862*04fd306cSNickeau
863*04fd306cSNickeau    private
864*04fd306cSNickeau    function getTocMaxLevel(): int
865*04fd306cSNickeau    {
866*04fd306cSNickeau        return ExecutionContext::getActualOrCreateFromEnv()
867*04fd306cSNickeau            ->getConfig()->getTocMaxLevel();
868*04fd306cSNickeau    }
869*04fd306cSNickeau
870*04fd306cSNickeau    public function setMetaHeaderCapture(bool $metaHeaderCapture): Outline
871*04fd306cSNickeau    {
872*04fd306cSNickeau        $this->metaHeaderCapture = $metaHeaderCapture;
873*04fd306cSNickeau        return $this;
874*04fd306cSNickeau    }
875*04fd306cSNickeau
876*04fd306cSNickeau    public function getMetaHeaderCapture(): bool
877*04fd306cSNickeau    {
878*04fd306cSNickeau        if (isset($this->metaHeaderCapture)) {
879*04fd306cSNickeau            return $this->metaHeaderCapture;
880*04fd306cSNickeau        }
881*04fd306cSNickeau        try {
882*04fd306cSNickeau            if ($this->markupPath !== null) {
883*04fd306cSNickeau                $contextPath = $this->markupPath->getPathObject()->toWikiPath();
884*04fd306cSNickeau                $hasMainHeaderElement = TemplateForWebPage::create()
885*04fd306cSNickeau                    ->setRequestedContextPath($contextPath)
886*04fd306cSNickeau                    ->hasElement(TemplateSlot::MAIN_HEADER_ID);
887*04fd306cSNickeau                $isThemeSystemEnabled = ExecutionContext::getActualOrCreateFromEnv()
888*04fd306cSNickeau                    ->getConfig()
889*04fd306cSNickeau                    ->isThemeSystemEnabled();
890*04fd306cSNickeau                if ($isThemeSystemEnabled && $hasMainHeaderElement) {
891*04fd306cSNickeau                    return true;
892*04fd306cSNickeau                }
893*04fd306cSNickeau            }
894*04fd306cSNickeau        } catch (ExceptionCast $e) {
895*04fd306cSNickeau            // to Wiki Path should be good
896*04fd306cSNickeau        }
897*04fd306cSNickeau        return false;
898*04fd306cSNickeau    }
899*04fd306cSNickeau
900*04fd306cSNickeau    public function isSectionEditingEnabled(): bool
901*04fd306cSNickeau    {
902*04fd306cSNickeau
903*04fd306cSNickeau        return ExecutionContext::getActualOrCreateFromEnv()
904*04fd306cSNickeau            ->getConfig()->isSectionEditingEnabled();
905*04fd306cSNickeau
906*04fd306cSNickeau    }
907*04fd306cSNickeau
908*04fd306cSNickeau
909*04fd306cSNickeau}
910