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                    $reset = <<<EOF
550$mainContainerSelector { counter-reset: h2; }
551$mainContainerSelector h2.$outlineClass { counter-increment: h2 1; counter-reset: h3 h4 h5 h6;}
552$mainContainerSelector h3.$outlineClass { counter-increment: h3 1; counter-reset: h4 h5 h6;}
553$mainContainerSelector h4.$outlineClass { counter-increment: h4 1; counter-reset: h5 h6;}
554$mainContainerSelector h5.$outlineClass { counter-increment: h5 1; counter-reset: h6;}
555$mainContainerSelector h6.$outlineClass { counter-increment: h6 1; }
556EOF;
557                } else {
558                    $mainContainerSelector = "#" . TemplateSlot::MAIN_CONTENT_ID;
559                    $reset = <<<EOF
560$mainContainerSelector { counter-reset: h2; }
561$mainContainerSelector section.outline-level-2-cs { counter-increment: h2; counter-reset: h3 h4 h5 h6;}
562$mainContainerSelector section.outline-level-3-cs { counter-increment: h3; counter-reset: h4 h5 h6;}
563$mainContainerSelector section.outline-level-4-cs { counter-increment: h4; counter-reset: h5 h6;}
564$mainContainerSelector section.outline-level-5-cs { counter-increment: h5; counter-reset: h6;}
565EOF;
566
567                }
568                return <<<EOF
569$reset
570$mainContainerSelector h2.$outlineClass::before { content: "$prefix" counter(h2, $level2CounterStyle) "$suffix\A"; }
571$mainContainerSelector h3.$outlineClass::before { content: "$prefix" counter(h2, $level2CounterStyle) "$counterSeparator" counter(h3,$level3CounterStyle) "$suffix\A"; }
572$mainContainerSelector h4.$outlineClass::before { content: "$prefix" counter(h2, $level2CounterStyle) "$counterSeparator" counter(h3,$level3CounterStyle) "$counterSeparator" counter(h4,$level4CounterStyle) "$suffix\A"; }
573$mainContainerSelector h5.$outlineClass::before { content: "$prefix" counter(h2, $level2CounterStyle) "$counterSeparator" counter(h3,$level3CounterStyle) "$counterSeparator" counter(h4,$level4CounterStyle) "$counterSeparator" counter(h5,$level5CounterStyle) "$suffix\A"; }
574$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"; }
575EOF;
576
577
578            case self::TOC_NUMBERING:
579                /**
580                 * The level counter on the toc are based
581                 * on the https://www.dokuwiki.org/config:toptoclevel
582                 * configuration
583                 * if toptoclevel = 2, then level1 = h2 and not h1
584                 * @deprecated
585                 */
586                // global $conf;
587                // $topTocLevel = $conf['toptoclevel'];
588
589                $tocSelector = "." . Toc::getClass() . " ul";
590                return <<<EOF
591$tocSelector li { counter-increment: toc2; }
592$tocSelector li li { counter-increment: toc3; }
593$tocSelector li li li { counter-increment: toc4; }
594$tocSelector li li li li { counter-increment: toc5; }
595$tocSelector li li li li li { counter-increment: toc6; }
596$tocSelector li a::before { content: "$prefix" counter(toc2, $level2CounterStyle) "$suffix\A"; }
597$tocSelector li li a::before { content: "$prefix" counter(toc2, $level2CounterStyle) "$counterSeparator" counter(toc3,$level3CounterStyle) "$suffix\A"; }
598$tocSelector li li li a::before { content: "$prefix" counter(toc2, $level2CounterStyle) "$counterSeparator" counter(toc3,$level3CounterStyle) "$counterSeparator" counter(toc4,$level4CounterStyle) "$suffix\A"; }
599$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"; }
600$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"; }
601EOF;
602
603            default:
604                throw new ExceptionBadSyntax("The type ($type) is unknown");
605        }
606
607
608    }
609
610    /**
611     * @throws ExceptionNotFound
612     */
613    public static function createFromMarkupPath(MarkupPath $markupPath): Outline
614    {
615        $path = $markupPath->getPathObject();
616        if (!($path instanceof WikiPath)) {
617            throw new ExceptionRuntimeInternal("The path is not a wiki path");
618        }
619        $markup = FileSystems::getContent($path);
620        $instructions = MarkupRenderer::createFromMarkup($markup, $path, $path)
621            ->setRequestedMimeToInstruction()
622            ->getOutput();
623        $callStack = CallStack::createFromInstructions($instructions);
624        return new Outline($callStack, $markupPath);
625    }
626
627    public function getInstructionCalls(): array
628    {
629        $totalInstructionCalls = [];
630        $collectCalls = function (OutlineSection $outlineSection) use (&$totalInstructionCalls) {
631            $instructionCalls = array_map(function (Call $element) {
632                return $element->getInstructionCall();
633            }, $outlineSection->getCalls());
634            $totalInstructionCalls = array_merge($totalInstructionCalls, $instructionCalls);
635        };
636        TreeVisit::visit($this->rootSection, $collectCalls);
637        return $totalInstructionCalls;
638    }
639
640    public function toDokuWikiTemplateInstructionCalls(): array
641    {
642        $totalInstructionCalls = [];
643        $sectionSequenceId = 0;
644        $collectCalls = function (OutlineSection $outlineSection) use (&$totalInstructionCalls, &$sectionSequenceId) {
645
646            $wikiSectionOpen = Call::createNativeCall(
647                \action_plugin_combo_instructionspostprocessing::EDIT_SECTION_OPEN,
648                array($outlineSection->getLevel()),
649                $outlineSection->getStartPosition()
650            );
651            $wikiSectionClose = Call::createNativeCall(
652                \action_plugin_combo_instructionspostprocessing::EDIT_SECTION_CLOSE,
653                array(),
654                $outlineSection->getEndPosition()
655            );
656
657
658            if ($outlineSection->hasParent()) {
659
660
661                $sectionCalls = array_merge(
662                    $outlineSection->getHeadingCalls(),
663                    [$wikiSectionOpen],
664                    $outlineSection->getContentCalls(),
665                    [$wikiSectionClose],
666                );
667
668                if ($this->isSectionEditingEnabled()) {
669
670                    /**
671                     * Adding sectionedit class to be conform
672                     * with the Dokuwiki {@link \Doku_Renderer_xhtml::header()} function
673                     */
674                    $sectionSequenceId++;
675                    $headingCall = $outlineSection->getEnterHeadingCall();
676                    if ($headingCall->isPluginCall()) {
677                        $level = DataType::toIntegerOrDefaultIfNull($headingCall->getAttribute(HeadingTag::LEVEL), 0);
678                        if ($level <= $this->getTocMaxLevel()) {
679                            $headingCall->addClassName("sectionedit$sectionSequenceId");
680                        }
681                    }
682
683                    $editButton = EditButton::create($outlineSection->getLabel())
684                        ->setStartPosition($outlineSection->getStartPosition())
685                        ->setEndPosition($outlineSection->getEndPosition())
686                        ->setOutlineHeadingId($outlineSection->getHeadingId())
687                        ->setOutlineSectionId($sectionSequenceId)
688                        ->toComboCallDokuWikiForm();
689                    $sectionCalls[] = $editButton;
690                }
691
692            } else {
693                // dokuwiki seems to have no section for the content before the first heading
694                $sectionCalls = $outlineSection->getContentCalls();
695            }
696
697            $instructionCalls = array_map(function (Call $element) {
698                return $element->getInstructionCall();
699            }, $sectionCalls);
700            $totalInstructionCalls = array_merge($totalInstructionCalls, $instructionCalls);
701        };
702        TreeVisit::visit($this->rootSection, $collectCalls);
703        return $totalInstructionCalls;
704    }
705
706    private function addCallToSection(Call $actualCall)
707    {
708        if ($this->actualHeadingParsingState === DOKU_LEXER_ENTER && !$this->actualSection->hasContentCall()) {
709            $this->actualSection->addHeaderCall($actualCall);
710        } else {
711            // an content heading (not outline) or another call
712            $this->actualSection->addContentCall($actualCall);
713        }
714    }
715
716    private function enterHeading(Call $actualCall)
717    {
718        $this->actualHeadingParsingState = DOKU_LEXER_ENTER;
719        $this->actualHeadingCall = $actualCall;
720    }
721
722    private function exitHeading()
723    {
724        $this->actualHeadingParsingState = DOKU_LEXER_EXIT;
725    }
726
727    /**
728     * @return array - Dokuwiki TOC array format
729     */
730    public function toTocDokuwikiFormat(): array
731    {
732
733        $tableOfContent = [];
734        $collectTableOfContent = function (OutlineSection $outlineSection) use (&$tableOfContent) {
735
736            if (!$outlineSection->hasParent()) {
737                // Root Section, no heading
738                return;
739            }
740            $tableOfContent[] = [
741                'link' => '#' . $outlineSection->getHeadingId(),
742                'title' => $outlineSection->getLabel(),
743                'type' => 'ul',
744                'level' => $outlineSection->getLevel()
745            ];
746
747        };
748        TreeVisit::visit($this->rootSection, $collectTableOfContent);
749        return $tableOfContent;
750
751    }
752
753
754    public
755    function toHtmlSectionOutlineCalls(): array
756    {
757        return OutlineVisitor::create($this)->getCalls();
758    }
759
760
761    /**
762     * Fragment Rendering
763     * * does not have any section/edit button
764     * * no outline or edit button for dynamic rendering but closing of atx heading
765     *
766     * The outline processing ({@link Outline::buildOutline()} just close the atx heading
767     *
768     * @return array
769     */
770    public
771    function toFragmentInstructionCalls(): array
772    {
773        $totalInstructionCalls = [];
774        $collectCalls = function (OutlineSection $outlineSection) use (&$totalInstructionCalls) {
775
776            $sectionCalls = array_merge(
777                $outlineSection->getHeadingCalls(),
778                $outlineSection->getContentCalls()
779            );
780
781            $instructionCalls = array_map(function (Call $element) {
782                return $element->getInstructionCall();
783            }, $sectionCalls);
784            $totalInstructionCalls = array_merge($totalInstructionCalls, $instructionCalls);
785        };
786        TreeVisit::visit($this->rootSection, $collectCalls);
787        return $totalInstructionCalls;
788
789    }
790
791    /**
792     * Add the label (ie heading text to the cal attribute)
793     *
794     * @return void
795     */
796    private
797    function saveOutlineToMetadata()
798    {
799        try {
800            $firstChild = $this->rootSection->getFirstChild();
801        } catch (ExceptionNotFound $e) {
802            // no child
803            return;
804        }
805        if ($firstChild->getLevel() === 1) {
806            $headingCall = $firstChild->getEnterHeadingCall();
807            // not dokuwiki header ?
808            if ($headingCall->isPluginCall()) {
809                $headingCall->setAttribute(HeadingTag::HEADING_TEXT_ATTRIBUTE, $firstChild->getLabel());
810            }
811        }
812
813    }
814
815
816    private
817    function storeH1()
818    {
819        try {
820            $outlineSection = $this->getRootOutlineSection()->getFirstChild();
821        } catch (ExceptionNotFound $e) {
822            //
823            return;
824        }
825        if ($this->markupPath != null && $outlineSection->getLevel() === 1) {
826            $label = $outlineSection->getLabel();
827            $call = $outlineSection->getEnterHeadingCall();
828            if ($call->isPluginCall()) {
829                // we support also the dokwuiki header call that does not need the label
830                $call->addAttribute(HeadingTag::PARSED_LABEL, $label);
831            }
832            PageH1::createForPage($this->markupPath)->setDefaultValue($label);
833        }
834    }
835
836    private
837    function storeTocForMarkupIfAny()
838    {
839
840        $toc = $this->toTocDokuwikiFormat();
841
842        try {
843            $fetcherMarkup = ExecutionContext::getActualOrCreateFromEnv()->getExecutingMarkupHandler();
844            $fetcherMarkup->toc = $toc;
845            if ($fetcherMarkup->isDocument()) {
846                /**
847                 * We still update the global TOC Dokuwiki variables
848                 */
849                global $TOC;
850                $TOC = $toc;
851            }
852        } catch (ExceptionNotFound $e) {
853            // outline is not runnned from a markup handler
854        }
855
856        if (!isset($this->markupPath)) {
857            return;
858        }
859
860        try {
861            Toc::createForPage($this->markupPath)
862                ->setValue($toc)
863                ->persist();
864        } catch (ExceptionBadArgument $e) {
865            LogUtility::error("The Toc could not be persisted. Error:{$e->getMessage()}");
866        }
867    }
868
869    public
870    function getMarkupPath(): ?MarkupPath
871    {
872        return $this->markupPath;
873    }
874
875
876    private
877    function getTocMaxLevel(): int
878    {
879        return ExecutionContext::getActualOrCreateFromEnv()
880            ->getConfig()->getTocMaxLevel();
881    }
882
883    public function setMetaHeaderCapture(bool $metaHeaderCapture): Outline
884    {
885        $this->metaHeaderCapture = $metaHeaderCapture;
886        return $this;
887    }
888
889    public function getMetaHeaderCapture(): bool
890    {
891        if (isset($this->metaHeaderCapture)) {
892            return $this->metaHeaderCapture;
893        }
894        try {
895            if ($this->markupPath !== null) {
896                $contextPath = $this->markupPath->getPathObject()->toWikiPath();
897                $hasMainHeaderElement = TemplateForWebPage::create()
898                    ->setRequestedContextPath($contextPath)
899                    ->hasElement(TemplateSlot::MAIN_HEADER_ID);
900                $isThemeSystemEnabled = ExecutionContext::getActualOrCreateFromEnv()
901                    ->getConfig()
902                    ->isThemeSystemEnabled();
903                if ($isThemeSystemEnabled && $hasMainHeaderElement) {
904                    return true;
905                }
906            }
907        } catch (ExceptionCast $e) {
908            // to Wiki Path should be good
909        }
910        return false;
911    }
912
913    public function isSectionEditingEnabled(): bool
914    {
915
916        return ExecutionContext::getActualOrCreateFromEnv()
917            ->getConfig()->isSectionEditingEnabled();
918
919    }
920
921
922}
923