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