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        $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 dokuiki 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($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     */
461    public static function merge(Outline $inner, Outline $outer)
462    {
463        /**
464         * Get the inner section where the outer section will be added
465         */
466        $innerRootOutlineSection = $inner->getRootOutlineSection();
467        $innerTopSections = $innerRootOutlineSection->getChildren();
468        if (count($innerTopSections) === 0) {
469            $firstInnerSection = $innerRootOutlineSection;
470        } else {
471            $firstInnerSection = $innerTopSections[count($innerTopSections)];
472        }
473        $firstInnerSectionLevel = $firstInnerSection->getLevel();
474
475        /**
476         * Add the outer sections
477         */
478        $outerRootOutlineSection = $outer->getRootOutlineSection();
479        foreach ($outerRootOutlineSection->getChildren() as $childOuterSection) {
480            /**
481             * One level less than where the section is included
482             */
483            $childOuterSection->setLevel($firstInnerSectionLevel + 1);
484            $childOuterSection->detachBeforeAppend();
485            try {
486                $firstInnerSection->appendChild($childOuterSection);
487            } catch (ExceptionBadState $e) {
488                // We add the node only once. This error should not happen
489                throw new ExceptionRuntimeInternal("Error while adding a section during the outline merge. Error: {$e->getMessage()}", self::CANONICAL, 1, $e);
490            }
491        }
492
493    }
494
495    public static function mergeRecurse(Outline $inner, Outline $outer)
496    {
497        $innerRootOutlineSection = $inner->getRootOutlineSection();
498        $outerRootOutlineSection = $outer->getRootOutlineSection();
499
500    }
501
502    /**
503     * Utility class to create a outline from a markup string
504     * @param string $content
505     * @return Outline
506     */
507    public static function createFromMarkup(string $content, Path $contentPath, WikiPath $contextPath): Outline
508    {
509        $instructions = MarkupRenderer::createFromMarkup($content, $contentPath, $contextPath)
510            ->setRequestedMimeToInstruction()
511            ->getOutput();
512        $callStack = CallStack::createFromInstructions($instructions);
513        return Outline::createFromCallStack($callStack);
514    }
515
516    /**
517     * Get the heading numbering snippet
518     * @param string $type heading or toc - for {@link Outline::TOC_NUMBERING} or {@link Outline::OUTLINE_HEADING_NUMBERING}
519     * @return string - the css internal stylesheet
520     * @throws ExceptionNotEnabled
521     * @throws ExceptionBadSyntax
522     * Page on DokuWiki
523     * https://www.dokuwiki.org/tips:numbered_headings
524     */
525    public static function getCssNumberingRulesFor(string $type): string
526    {
527
528        $enable = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_ENABLE, Outline::CONF_OUTLINE_NUMBERING_ENABLE_DEFAULT);
529        if (!$enable) {
530            throw new ExceptionNotEnabled();
531        }
532
533        $level2CounterStyle = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL2, "decimal");
534        $level3CounterStyle = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL3, "decimal");
535        $level4CounterStyle = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL4, "decimal");
536        $level5CounterStyle = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL5, "decimal");
537        $level6CounterStyle = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL6, "decimal");
538        $counterSeparator = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_SEPARATOR, ".");
539        $prefix = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_PREFIX, "");
540        $suffix = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_SUFFIX, " - ");
541
542        switch ($type) {
543
544            case self::OUTLINE_HEADING_NUMBERING:
545                global $ACT;
546                /**
547                 * Because the HTML file structure is not really fixed
548                 * (we may have section HTML element with a bar, the sectioning heading
549                 * may be not enabled)
550                 * We can't select via html structure
551                 * the outline heading consistently
552                 * We do it then with the class value
553                 */
554                $outlineClass = Outline::getOutlineHeadingClass();
555                if ($ACT === "preview") {
556                    $mainContainerSelector = ".pad";
557                } else {
558                    $mainContainerSelector = "#" . TemplateSlot::MAIN_CONTENT_ID;
559                }
560                /**
561                 * Counter inheritance works by sibling and if not found on parents
562                 * we therefore needs to take into account the 2 HTML structure
563                 * * one counter on h1 if this is the flat structure
564                 * one counter on the section if this is the section structure
565                 */
566                $reset = <<<EOF
567$mainContainerSelector { counter-reset: h2; }
568$mainContainerSelector > h2.$outlineClass { counter-increment: h2 1; counter-reset: h3 h4 h5 h6;}
569$mainContainerSelector > h3.$outlineClass { counter-increment: h3 1; counter-reset: h4 h5 h6;}
570$mainContainerSelector > h4.$outlineClass { counter-increment: h4 1; counter-reset: h5 h6;}
571$mainContainerSelector > h5.$outlineClass { counter-increment: h5 1; counter-reset: h6;}
572$mainContainerSelector > h6.$outlineClass { counter-increment: h6 1; }
573$mainContainerSelector section.outline-level-2-cs { counter-increment: h2; counter-reset: h3 h4 h5 h6;}
574$mainContainerSelector section.outline-level-3-cs { counter-increment: h3; counter-reset: h4 h5 h6;}
575$mainContainerSelector section.outline-level-4-cs { counter-increment: h4; counter-reset: h5 h6;}
576$mainContainerSelector section.outline-level-5-cs { counter-increment: h5; counter-reset: h6;}
577EOF;
578                return <<<EOF
579$reset
580$mainContainerSelector h2.$outlineClass::before { content: "$prefix" counter(h2, $level2CounterStyle) "$suffix\A"; }
581$mainContainerSelector h3.$outlineClass::before { content: "$prefix" counter(h2, $level2CounterStyle) "$counterSeparator" counter(h3,$level3CounterStyle) "$suffix\A"; }
582$mainContainerSelector h4.$outlineClass::before { content: "$prefix" counter(h2, $level2CounterStyle) "$counterSeparator" counter(h3,$level3CounterStyle) "$counterSeparator" counter(h4,$level4CounterStyle) "$suffix\A"; }
583$mainContainerSelector h5.$outlineClass::before { content: "$prefix" counter(h2, $level2CounterStyle) "$counterSeparator" counter(h3,$level3CounterStyle) "$counterSeparator" counter(h4,$level4CounterStyle) "$counterSeparator" counter(h5,$level5CounterStyle) "$suffix\A"; }
584$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"; }
585EOF;
586
587
588            case self::TOC_NUMBERING:
589                /**
590                 * The level counter on the toc are based
591                 * on the https://www.dokuwiki.org/config:toptoclevel
592                 * configuration
593                 * if toptoclevel = 2, then level1 = h2 and not h1
594                 * @deprecated
595                 */
596                // global $conf;
597                // $topTocLevel = $conf['toptoclevel'];
598
599                $tocSelector = "." . Toc::getClass() . " ul";
600                return <<<EOF
601$tocSelector li { counter-increment: toc2; }
602$tocSelector li li { counter-increment: toc3; }
603$tocSelector li li li { counter-increment: toc4; }
604$tocSelector li li li li { counter-increment: toc5; }
605$tocSelector li li li li li { counter-increment: toc6; }
606$tocSelector li a::before { content: "$prefix" counter(toc2, $level2CounterStyle) "$suffix\A"; }
607$tocSelector li li a::before { content: "$prefix" counter(toc2, $level2CounterStyle) "$counterSeparator" counter(toc3,$level3CounterStyle) "$suffix\A"; }
608$tocSelector li li li a::before { content: "$prefix" counter(toc2, $level2CounterStyle) "$counterSeparator" counter(toc3,$level3CounterStyle) "$counterSeparator" counter(toc4,$level4CounterStyle) "$suffix\A"; }
609$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"; }
610$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"; }
611EOF;
612
613            default:
614                throw new ExceptionBadSyntax("The type ($type) is unknown");
615        }
616
617
618    }
619
620    /**
621     * @throws ExceptionNotFound
622     */
623    public static function createFromMarkupPath(MarkupPath $markupPath): Outline
624    {
625        $path = $markupPath->getPathObject();
626        if (!($path instanceof WikiPath)) {
627            throw new ExceptionRuntimeInternal("The path is not a wiki path");
628        }
629        $markup = FileSystems::getContent($path);
630        $instructions = MarkupRenderer::createFromMarkup($markup, $path, $path)
631            ->setRequestedMimeToInstruction()
632            ->getOutput();
633        $callStack = CallStack::createFromInstructions($instructions);
634        return new Outline($callStack, $markupPath);
635    }
636
637    public function getInstructionCalls(): array
638    {
639        $totalInstructionCalls = [];
640        $collectCalls = function (OutlineSection $outlineSection) use (&$totalInstructionCalls) {
641            $instructionCalls = array_map(function (Call $element) {
642                return $element->getInstructionCall();
643            }, $outlineSection->getCalls());
644            $totalInstructionCalls = array_merge($totalInstructionCalls, $instructionCalls);
645        };
646        TreeVisit::visit($this->rootSection, $collectCalls);
647        return $totalInstructionCalls;
648    }
649
650    public function toDokuWikiTemplateInstructionCalls(): array
651    {
652        $totalInstructionCalls = [];
653        $sectionSequenceId = 0;
654        $collectCalls = function (OutlineSection $outlineSection) use (&$totalInstructionCalls, &$sectionSequenceId) {
655
656            $wikiSectionOpen = Call::createNativeCall(
657                \action_plugin_combo_instructionspostprocessing::EDIT_SECTION_OPEN,
658                array($outlineSection->getLevel()),
659                $outlineSection->getStartPosition()
660            );
661            $wikiSectionClose = Call::createNativeCall(
662                \action_plugin_combo_instructionspostprocessing::EDIT_SECTION_CLOSE,
663                array(),
664                $outlineSection->getEndPosition()
665            );
666
667
668            if ($outlineSection->hasParent()) {
669
670
671                $sectionCalls = array_merge(
672                    $outlineSection->getHeadingCalls(),
673                    [$wikiSectionOpen],
674                    $outlineSection->getContentCalls(),
675                    [$wikiSectionClose],
676                );
677
678                if ($this->isSectionEditingEnabled()) {
679
680                    /**
681                     * Adding sectionedit class to be conform
682                     * with the Dokuwiki {@link \Doku_Renderer_xhtml::header()} function
683                     */
684                    $sectionSequenceId++;
685                    $headingCall = $outlineSection->getEnterHeadingCall();
686                    if ($headingCall->isPluginCall()) {
687                        $level = DataType::toIntegerOrDefaultIfNull($headingCall->getAttribute(HeadingTag::LEVEL), 0);
688                        if ($level <= $this->getTocMaxLevel()) {
689                            $headingCall->addClassName("sectionedit$sectionSequenceId");
690                        }
691                    }
692
693                    $editButton = EditButton::create($outlineSection->getLabel())
694                        ->setStartPosition($outlineSection->getStartPosition())
695                        ->setEndPosition($outlineSection->getEndPosition())
696                        ->setOutlineHeadingId($outlineSection->getHeadingId())
697                        ->setOutlineSectionId($sectionSequenceId)
698                        ->toComboCallDokuWikiForm();
699                    $sectionCalls[] = $editButton;
700                }
701
702            } else {
703                // dokuwiki seems to have no section for the content before the first heading
704                $sectionCalls = $outlineSection->getContentCalls();
705            }
706
707            $instructionCalls = array_map(function (Call $element) {
708                return $element->getInstructionCall();
709            }, $sectionCalls);
710            $totalInstructionCalls = array_merge($totalInstructionCalls, $instructionCalls);
711        };
712        TreeVisit::visit($this->rootSection, $collectCalls);
713        return $totalInstructionCalls;
714    }
715
716    private function addCallToSection(Call $actualCall)
717    {
718        if ($this->actualHeadingParsingState === DOKU_LEXER_ENTER && !$this->actualSection->hasContentCall()) {
719            $this->actualSection->addHeaderCall($actualCall);
720        } else {
721            // an content heading (not outline) or another call
722            $this->actualSection->addContentCall($actualCall);
723        }
724    }
725
726    private function enterHeading(Call $actualCall)
727    {
728        $this->actualHeadingParsingState = DOKU_LEXER_ENTER;
729        $this->actualHeadingCall = $actualCall;
730    }
731
732    private function exitHeading()
733    {
734        $this->actualHeadingParsingState = DOKU_LEXER_EXIT;
735    }
736
737    /**
738     * @return array - Dokuwiki TOC array format
739     */
740    public function toTocDokuwikiFormat(): array
741    {
742
743        $tableOfContent = [];
744        $collectTableOfContent = function (OutlineSection $outlineSection) use (&$tableOfContent) {
745
746            if (!$outlineSection->hasParent()) {
747                // Root Section, no heading
748                return;
749            }
750            $tableOfContent[] = [
751                'link' => '#' . $outlineSection->getHeadingId(),
752                'title' => $outlineSection->getLabel(),
753                'type' => 'ul',
754                'level' => $outlineSection->getLevel()
755            ];
756
757        };
758        TreeVisit::visit($this->rootSection, $collectTableOfContent);
759        return $tableOfContent;
760
761    }
762
763
764    public
765    function toHtmlSectionOutlineCalls(): array
766    {
767        return OutlineVisitor::create($this)->getCalls();
768    }
769
770
771    /**
772     * Fragment Rendering
773     * * does not have any section/edit button
774     * * no outline or edit button for dynamic rendering but closing of atx heading
775     *
776     * The outline processing ({@link Outline::buildOutline()} just close the atx heading
777     *
778     * @return array
779     */
780    public
781    function toFragmentInstructionCalls(): array
782    {
783        $totalInstructionCalls = [];
784        $collectCalls = function (OutlineSection $outlineSection) use (&$totalInstructionCalls) {
785
786            $sectionCalls = array_merge(
787                $outlineSection->getHeadingCalls(),
788                $outlineSection->getContentCalls()
789            );
790
791            $instructionCalls = array_map(function (Call $element) {
792                return $element->getInstructionCall();
793            }, $sectionCalls);
794            $totalInstructionCalls = array_merge($totalInstructionCalls, $instructionCalls);
795        };
796        TreeVisit::visit($this->rootSection, $collectCalls);
797        return $totalInstructionCalls;
798
799    }
800
801    /**
802     * Add the label (ie heading text to the cal attribute)
803     *
804     * @return void
805     */
806    private
807    function saveOutlineToMetadata()
808    {
809        try {
810            $firstChild = $this->rootSection->getFirstChild();
811        } catch (ExceptionNotFound $e) {
812            // no child
813            return;
814        }
815        if ($firstChild->getLevel() === 1) {
816            $headingCall = $firstChild->getEnterHeadingCall();
817            // not dokuwiki header ?
818            if ($headingCall->isPluginCall()) {
819                $headingCall->setAttribute(HeadingTag::HEADING_TEXT_ATTRIBUTE, $firstChild->getLabel());
820            }
821        }
822
823    }
824
825
826    private
827    function storeH1()
828    {
829        try {
830            $outlineSection = $this->getRootOutlineSection()->getFirstChild();
831        } catch (ExceptionNotFound $e) {
832            //
833            return;
834        }
835        if ($this->markupPath != null && $outlineSection->getLevel() === 1) {
836            $label = $outlineSection->getLabel();
837            $call = $outlineSection->getEnterHeadingCall();
838            if ($call->isPluginCall()) {
839                // we support also the dokwuiki header call that does not need the label
840                $call->addAttribute(HeadingTag::PARSED_LABEL, $label);
841            }
842            PageH1::createForPage($this->markupPath)->setDefaultValue($label);
843        }
844    }
845
846    private
847    function storeTocForMarkupIfAny()
848    {
849
850        $toc = $this->toTocDokuwikiFormat();
851
852        try {
853            $fetcherMarkup = ExecutionContext::getActualOrCreateFromEnv()->getExecutingMarkupHandler();
854            $fetcherMarkup->toc = $toc;
855            if ($fetcherMarkup->isDocument()) {
856                /**
857                 * We still update the global TOC Dokuwiki variables
858                 */
859                global $TOC;
860                $TOC = $toc;
861            }
862        } catch (ExceptionNotFound $e) {
863            // outline is not runnned from a markup handler
864        }
865
866        if (!isset($this->markupPath)) {
867            return;
868        }
869
870        try {
871            Toc::createForPage($this->markupPath)
872                ->setValue($toc)
873                ->persist();
874        } catch (ExceptionBadArgument $e) {
875            LogUtility::error("The Toc could not be persisted. Error:{$e->getMessage()}");
876        }
877    }
878
879    public
880    function getMarkupPath(): ?MarkupPath
881    {
882        return $this->markupPath;
883    }
884
885
886    private
887    function getTocMaxLevel(): int
888    {
889        return ExecutionContext::getActualOrCreateFromEnv()
890            ->getConfig()->getTocMaxLevel();
891    }
892
893    public function setMetaHeaderCapture(bool $metaHeaderCapture): Outline
894    {
895        $this->metaHeaderCapture = $metaHeaderCapture;
896        return $this;
897    }
898
899    public function getMetaHeaderCapture(): bool
900    {
901        if (isset($this->metaHeaderCapture)) {
902            return $this->metaHeaderCapture;
903        }
904        try {
905            if ($this->markupPath !== null) {
906                $contextPath = $this->markupPath->getPathObject()->toWikiPath();
907                $hasMainHeaderElement = TemplateForWebPage::create()
908                    ->setRequestedContextPath($contextPath)
909                    ->hasElement(TemplateSlot::MAIN_HEADER_ID);
910                $isThemeSystemEnabled = ExecutionContext::getActualOrCreateFromEnv()
911                    ->getConfig()
912                    ->isThemeSystemEnabled();
913                if ($isThemeSystemEnabled && $hasMainHeaderElement) {
914                    return true;
915                }
916            }
917        } catch (ExceptionCast $e) {
918            // to Wiki Path should be good
919        }
920        return false;
921    }
922
923    public function isSectionEditingEnabled(): bool
924    {
925
926        return ExecutionContext::getActualOrCreateFromEnv()
927            ->getConfig()->isSectionEditingEnabled();
928
929    }
930
931
932}
933