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