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