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