Passed
Push — latest ( 6cb8cd...7d037c )
by Colin
02:17
created

TableOfContentsGenerator   A

Complexity

Total Complexity 19

Size/Duplication

Total Lines 140
Duplicated Lines 0 %

Test Coverage

Coverage 94.83%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 19
eloc 63
c 4
b 0
f 0
dl 0
loc 140
ccs 55
cts 58
cp 0.9483
rs 10

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
B generate() 0 49 7
A getHeadingLinks() 0 6 4
A createToc() 0 18 3
A getNormalizer() 0 11 4
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the league/commonmark package.
7
 *
8
 * (c) Colin O'Dell <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace League\CommonMark\Extension\TableOfContents;
15
16
use League\CommonMark\Exception\InvalidOptionException;
17
use League\CommonMark\Extension\CommonMark\Node\Block\Heading;
18
use League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock;
19
use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock;
20
use League\CommonMark\Extension\CommonMark\Node\Block\ListData;
21
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
22
use League\CommonMark\Extension\CommonMark\Node\Inline\HtmlInline;
23
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
24
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalink;
25
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
26
use League\CommonMark\Extension\TableOfContents\Normalizer\AsIsNormalizerStrategy;
27
use League\CommonMark\Extension\TableOfContents\Normalizer\FlatNormalizerStrategy;
28
use League\CommonMark\Extension\TableOfContents\Normalizer\NormalizerStrategyInterface;
29
use League\CommonMark\Extension\TableOfContents\Normalizer\RelativeNormalizerStrategy;
30
use League\CommonMark\Node\Block\Document;
31
use League\CommonMark\Node\Block\Paragraph;
32
use League\CommonMark\Node\StringContainerHelper;
33
34
final class TableOfContentsGenerator implements TableOfContentsGeneratorInterface
35
{
36
    public const STYLE_BULLET  = ListBlock::TYPE_BULLET;
37
    public const STYLE_ORDERED = ListBlock::TYPE_ORDERED;
38
39
    public const NORMALIZE_DISABLED = 'as-is';
40
    public const NORMALIZE_RELATIVE = 'relative';
41
    public const NORMALIZE_FLAT     = 'flat';
42
43
    /**
44
     * @var string
45
     *
46
     * @psalm-readonly
47
     */
48
    private $style;
49
50
    /**
51
     * @var string
52
     *
53
     * @psalm-readonly
54
     */
55
    private $normalizationStrategy;
56
57
    /**
58
     * @var int
59
     *
60
     * @psalm-readonly
61
     */
62
    private $minHeadingLevel;
63
64
    /**
65
     * @var int
66
     *
67
     * @psalm-readonly
68
     */
69
    private $maxHeadingLevel;
70
71 39
    public function __construct(string $style, string $normalizationStrategy, int $minHeadingLevel, int $maxHeadingLevel)
72
    {
73 39
        $this->style                 = $style;
74 39
        $this->normalizationStrategy = $normalizationStrategy;
75 39
        $this->minHeadingLevel       = $minHeadingLevel;
76 39
        $this->maxHeadingLevel       = $maxHeadingLevel;
77 39
    }
78
79 39
    public function generate(Document $document): ?TableOfContents
80
    {
81 39
        $toc = $this->createToc($document);
82
83 39
        $normalizer = $this->getNormalizer($toc);
84
85 39
        $firstHeading = null;
86
87 39
        foreach ($this->getHeadingLinks($document) as $headingLink) {
88 36
            $heading = $headingLink->parent();
89
            // Make sure this is actually tied to a heading
90 36
            if (! $heading instanceof Heading) {
91
                continue;
92
            }
93
94
            // Skip any headings outside the configured min/max levels
95 36
            if ($heading->getLevel() < $this->minHeadingLevel || $heading->getLevel() > $this->maxHeadingLevel) {
96 3
                continue;
97
            }
98
99
            // Keep track of the first heading we see - we might need this later
100 36
            $firstHeading = $firstHeading ?? $heading;
101
102
            // Keep track of the start and end lines
103 36
            $toc->setStartLine($firstHeading->getStartLine());
104 36
            $toc->setEndLine($heading->getEndLine());
105
106
            // Create the new link
107 36
            $link      = new Link('#' . $headingLink->getSlug(), StringContainerHelper::getChildText($heading, [HtmlBlock::class, HtmlInline::class]));
108 36
            $paragraph = new Paragraph();
109 36
            $paragraph->setStartLine($heading->getStartLine());
110 36
            $paragraph->setEndLine($heading->getEndLine());
111 36
            $paragraph->appendChild($link);
112
113 36
            $listItem = new ListItem($toc->getListData());
114 36
            $listItem->setStartLine($heading->getStartLine());
115 36
            $listItem->setEndLine($heading->getEndLine());
116 36
            $listItem->appendChild($paragraph);
117
118
            // Add it to the correct place
119 36
            $normalizer->addItem($heading->getLevel(), $listItem);
120
        }
121
122
        // Don't add the TOC if no headings were present
123 39
        if (! $toc->hasChildren() || $firstHeading === null) {
124 6
            return null;
125
        }
126
127 36
        return $toc;
128
    }
129
130 39
    private function createToc(Document $document): TableOfContents
131
    {
132 39
        $listData = new ListData();
133
134 39
        if ($this->style === self::STYLE_BULLET) {
135 36
            $listData->type = ListBlock::TYPE_BULLET;
136 3
        } elseif ($this->style === self::STYLE_ORDERED) {
137 3
            $listData->type = ListBlock::TYPE_ORDERED;
138
        } else {
139
            throw InvalidOptionException::forParameter('table of contents list style', $this->style);
140
        }
141
142 39
        $toc = new TableOfContents($listData);
143
144 39
        $toc->setStartLine($document->getStartLine());
145 39
        $toc->setEndLine($document->getEndLine());
146
147 39
        return $toc;
148
    }
149
150
    /**
151
     * @return iterable<HeadingPermalink>
152
     */
153 39
    private function getHeadingLinks(Document $document): iterable
154
    {
155 39
        $walker = $document->walker();
156 39
        while ($event = $walker->next()) {
157 39
            if ($event->isEntering() && ($node = $event->getNode()) instanceof HeadingPermalink) {
158 36
                yield $node;
159
            }
160
        }
161 39
    }
162
163 39
    private function getNormalizer(TableOfContents $toc): NormalizerStrategyInterface
164
    {
165 39
        switch ($this->normalizationStrategy) {
166 39
            case self::NORMALIZE_DISABLED:
167 3
                return new AsIsNormalizerStrategy($toc);
168 36
            case self::NORMALIZE_RELATIVE:
169 33
                return new RelativeNormalizerStrategy($toc);
170 3
            case self::NORMALIZE_FLAT:
171 3
                return new FlatNormalizerStrategy($toc);
172
            default:
173
                throw InvalidOptionException::forParameter('table of contents normalization strategy', $this->normalizationStrategy);
174
        }
175
    }
176
}
177