Completed
Push — master ( 52e853...8255d7 )
by Colin
01:03
created

TableOfContentsBuilder   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 139
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 15

Test Coverage

Coverage 93.55%

Importance

Changes 0
Metric Value
wmc 23
lcom 1
cbo 15
dl 0
loc 139
c 0
b 0
f 0
ccs 58
cts 62
cp 0.9355
rs 10

6 Methods

Rating   Name   Duplication   Size   Complexity  
B onDocumentParsed() 0 52 9
A createToc() 0 22 4
A getMinAndMaxHeadingLevels() 0 7 1
A getHeadingLinks() 0 9 4
A getNormalizer() 0 13 4
A setConfiguration() 0 4 1
1
<?php
2
3
/*
4
 * This file is part of the league/commonmark package.
5
 *
6
 * (c) Colin O'Dell <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace League\CommonMark\Extension\TableOfContents;
13
14
use League\CommonMark\Block\Element\Document;
15
use League\CommonMark\Block\Element\Heading;
16
use League\CommonMark\Block\Element\ListBlock;
17
use League\CommonMark\Block\Element\ListData;
18
use League\CommonMark\Block\Element\ListItem;
19
use League\CommonMark\Block\Element\Paragraph;
20
use League\CommonMark\Event\DocumentParsedEvent;
21
use League\CommonMark\Exception\InvalidOptionException;
22
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalink;
23
use League\CommonMark\Extension\TableOfContents\Normalizer\AsIsNormalizerStrategy;
24
use League\CommonMark\Extension\TableOfContents\Normalizer\FlatNormalizerStrategy;
25
use League\CommonMark\Extension\TableOfContents\Normalizer\RelativeNormalizerStrategy;
26
use League\CommonMark\Inline\Element\Link;
27
use League\CommonMark\Util\ConfigurationAwareInterface;
28
use League\CommonMark\Util\ConfigurationInterface;
29
30
final class TableOfContentsBuilder implements ConfigurationAwareInterface
31
{
32
    public const STYLE_BULLET = ListBlock::TYPE_BULLET;
33
    public const STYLE_ORDERED = ListBlock::TYPE_ORDERED;
34
35
    public const NORMALIZE_DISABLED = 'as-is';
36
    public const NORMALIZE_RELATIVE = 'relative';
37
    public const NORMALIZE_FLAT = 'flat';
38
39
    public const POSITION_TOP = 'top';
40
    public const POSITION_BEFORE_HEADINGS = 'before-headings';
41
42
    /** @var ConfigurationInterface */
43
    private $config;
44
45 33
    public function onDocumentParsed(DocumentParsedEvent $event)
46
    {
47 33
        $document = $event->getDocument();
48 33
        $toc = $this->createToc();
49
50 33
        $normalizer = $this->getNormalizer($toc);
51 33
        [$min, $max] = $this->getMinAndMaxHeadingLevels();
0 ignored issues
show
Bug introduced by
The variable $min does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $max does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
52
53 33
        $firstHeading = null;
54
55 33
        foreach ($this->getHeadingLinks($document) as $headingLink) {
56 30
            $heading = $headingLink->parent();
57
            // Make sure this is actually tied to a heading
58 30
            if (!$heading instanceof Heading) {
59
                continue;
60
            }
61
62
            // Skip any headings outside the configured min/max levels
63 30
            if ($heading->getLevel() < $min || $heading->getLevel() > $max) {
64 3
                continue;
65
            }
66
67
            // Keep track of the first heading we see - we might need this later
68 30
            $firstHeading = $firstHeading ?? $heading;
69
70
            // Create the new link
71 30
            $link = new Link('#' . $headingLink->getSlug(), $heading->getStringContent());
72 30
            $paragraph = new Paragraph();
73 30
            $paragraph->appendChild($link);
74
75 30
            $listItem = new ListItem($toc->getListData());
76 30
            $listItem->appendChild($paragraph);
77
78
            // Add it to the correct place
79 30
            $normalizer->addItem($heading->getLevel(), $listItem);
80
        }
81
82
        // Don't add the TOC if no headings were present
83 33
        if (!$toc->hasChildren() || $firstHeading === null) {
84 3
            return;
85
        }
86
87
        // Add the TOC to the Document
88 30
        $position = $this->config->get('table_of_contents/position', self::POSITION_TOP);
89 30
        if ($position === self::POSITION_TOP) {
90 27
            $document->prependChild($toc);
91 3
        } elseif ($position === self::POSITION_BEFORE_HEADINGS) {
92 3
            $firstHeading->insertBefore($toc);
93
        } else {
94
            throw new InvalidOptionException(\sprintf('Invalid config option "%s" for "table_of_contents/position"', $position));
95
        }
96 30
    }
97
98 33
    private function createToc(): TableOfContents
99
    {
100 33
        $listData = new ListData();
101
102 33
        $style = $this->config->get('table_of_contents/style', self::STYLE_BULLET);
103 33
        if ($style === self::STYLE_BULLET) {
104 30
            $listData->type = ListBlock::TYPE_BULLET;
105 3
        } elseif ($style === self::STYLE_ORDERED) {
106 3
            $listData->type = ListBlock::TYPE_ORDERED;
107
        } else {
108
            throw new InvalidOptionException(\sprintf('Invalid config option "%s" for "table_of_contents/style"', $style));
109
        }
110
111 33
        $toc = new TableOfContents($listData);
112
113 33
        $class = $this->config->get('table_of_contents/html_class', 'table-of-contents');
114 33
        if (!empty($class)) {
115 33
            $toc->data['attributes']['class'] = $class;
116
        }
117
118 33
        return $toc;
119
    }
120
121
    /**
122
     * @return array<int>
123
     */
124 33
    private function getMinAndMaxHeadingLevels(): array
125
    {
126
        return [
127 33
            (int) $this->config->get('table_of_contents/min_heading_level', 1),
128 33
            (int) $this->config->get('table_of_contents/max_heading_level', 6),
129
        ];
130
    }
131
132
    /**
133
     * @param Document $document
134
     *
135
     * @return HeadingPermalink[]
136
     */
137 33
    private function getHeadingLinks(Document $document)
138
    {
139 33
        $walker = $document->walker();
140 33
        while ($event = $walker->next()) {
141 33
            if ($event->isEntering() && ($node = $event->getNode()) instanceof HeadingPermalink) {
142 30
                yield $node;
143
            }
144
        }
145 33
    }
146
147 33
    private function getNormalizer(TableOfContents $toc)
148
    {
149 33
        $strategy = $this->config->get('table_of_contents/normalize', self::NORMALIZE_RELATIVE);
150 33
        if ($strategy === self::NORMALIZE_DISABLED) {
151 3
            return new AsIsNormalizerStrategy($toc);
152 30
        } elseif ($strategy === self::NORMALIZE_RELATIVE) {
153 27
            return new RelativeNormalizerStrategy($toc);
154 3
        } elseif ($strategy === self::NORMALIZE_FLAT) {
155 3
            return new FlatNormalizerStrategy($toc);
156
        }
157
158
        throw new InvalidOptionException(\sprintf('Invalid config option "%s" for "table_of_contents/normalize"', $strategy));
159
    }
160
161
    /**
162
     * {@inheritdoc}
163
     */
164 33
    public function setConfiguration(ConfigurationInterface $config)
165
    {
166 33
        $this->config = $config;
167 33
    }
168
}
169