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