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