1<?php
2
3/*
4 * This file is part of the league/commonmark package.
5 *
6 * (c) Colin O'Dell <colinodell@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace League\CommonMark\Extension\TableOfContents;
13
14use League\CommonMark\Block\Element\Document;
15use League\CommonMark\Block\Element\Heading;
16use League\CommonMark\Event\DocumentParsedEvent;
17use League\CommonMark\Exception\InvalidOptionException;
18use League\CommonMark\Extension\HeadingPermalink\HeadingPermalink;
19use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
20use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsPlaceholder;
21use League\CommonMark\Util\ConfigurationAwareInterface;
22use League\CommonMark\Util\ConfigurationInterface;
23
24final class TableOfContentsBuilder implements ConfigurationAwareInterface
25{
26    /**
27     * @deprecated Use TableOfContentsGenerator::STYLE_BULLET instead
28     */
29    public const STYLE_BULLET = TableOfContentsGenerator::STYLE_BULLET;
30
31    /**
32     * @deprecated Use TableOfContentsGenerator::STYLE_ORDERED instead
33     */
34    public const STYLE_ORDERED = TableOfContentsGenerator::STYLE_ORDERED;
35
36    /**
37     * @deprecated Use TableOfContentsGenerator::NORMALIZE_DISABLED instead
38     */
39    public const NORMALIZE_DISABLED = TableOfContentsGenerator::NORMALIZE_DISABLED;
40
41    /**
42     * @deprecated Use TableOfContentsGenerator::NORMALIZE_RELATIVE instead
43     */
44    public const NORMALIZE_RELATIVE = TableOfContentsGenerator::NORMALIZE_RELATIVE;
45
46    /**
47     * @deprecated Use TableOfContentsGenerator::NORMALIZE_FLAT instead
48     */
49    public const NORMALIZE_FLAT = TableOfContentsGenerator::NORMALIZE_FLAT;
50
51    public const POSITION_TOP = 'top';
52    public const POSITION_BEFORE_HEADINGS = 'before-headings';
53    public const POSITION_PLACEHOLDER = 'placeholder';
54
55    /** @var ConfigurationInterface */
56    private $config;
57
58    public function onDocumentParsed(DocumentParsedEvent $event): void
59    {
60        $document = $event->getDocument();
61
62        $generator = new TableOfContentsGenerator(
63            $this->config->get('table_of_contents/style', TableOfContentsGenerator::STYLE_BULLET),
64            $this->config->get('table_of_contents/normalize', TableOfContentsGenerator::NORMALIZE_RELATIVE),
65            (int) $this->config->get('table_of_contents/min_heading_level', 1),
66            (int) $this->config->get('table_of_contents/max_heading_level', 6)
67        );
68
69        $toc = $generator->generate($document);
70        if ($toc === null) {
71            // No linkable headers exist, so no TOC could be generated
72            return;
73        }
74
75        // Add custom CSS class(es), if defined
76        $class = $this->config->get('table_of_contents/html_class', 'table-of-contents');
77        if (!empty($class)) {
78            $toc->data['attributes']['class'] = $class;
79        }
80
81        // Add the TOC to the Document
82        $position = $this->config->get('table_of_contents/position', self::POSITION_TOP);
83        if ($position === self::POSITION_TOP) {
84            $document->prependChild($toc);
85        } elseif ($position === self::POSITION_BEFORE_HEADINGS) {
86            $this->insertBeforeFirstLinkedHeading($document, $toc);
87        } elseif ($position === self::POSITION_PLACEHOLDER) {
88            $this->replacePlaceholders($document, $toc);
89        } else {
90            throw new InvalidOptionException(\sprintf('Invalid config option "%s" for "table_of_contents/position"', $position));
91        }
92    }
93
94    private function insertBeforeFirstLinkedHeading(Document $document, TableOfContents $toc): void
95    {
96        $walker = $document->walker();
97        while ($event = $walker->next()) {
98            if ($event->isEntering() && ($node = $event->getNode()) instanceof HeadingPermalink && ($parent = $node->parent()) instanceof Heading) {
99                $parent->insertBefore($toc);
100
101                return;
102            }
103        }
104    }
105
106    private function replacePlaceholders(Document $document, TableOfContents $toc): void
107    {
108        $walker = $document->walker();
109        while ($event = $walker->next()) {
110            // Add the block once we find a placeholder (and we're about to leave it)
111            if (!$event->getNode() instanceof TableOfContentsPlaceholder) {
112                continue;
113            }
114
115            if ($event->isEntering()) {
116                continue;
117            }
118
119            $event->getNode()->replaceWith(clone $toc);
120        }
121    }
122
123    public function setConfiguration(ConfigurationInterface $config)
124    {
125        $this->config = $config;
126    }
127}
128