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