Completed
Push — toc-changes ( e65cac...16e29a )
by Colin
02:29
created

TableOfContentsGenerator::getNormalizer()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.0218

Importance

Changes 0
Metric Value
dl 0
loc 13
c 0
b 0
f 0
ccs 8
cts 9
cp 0.8889
rs 9.8333
cc 4
nc 4
nop 1
crap 4.0218
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\Exception\InvalidOptionException;
15
use League\CommonMark\Extension\CommonMark\Node\Block\Heading;
16
use League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock;
17
use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock;
18
use League\CommonMark\Extension\CommonMark\Node\Block\ListData;
19
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
20
use League\CommonMark\Extension\CommonMark\Node\Inline\HtmlInline;
21
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
22
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalink;
23
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
24
use League\CommonMark\Extension\TableOfContents\Normalizer\AsIsNormalizerStrategy;
25
use League\CommonMark\Extension\TableOfContents\Normalizer\FlatNormalizerStrategy;
26
use League\CommonMark\Extension\TableOfContents\Normalizer\NormalizerStrategyInterface;
27
use League\CommonMark\Extension\TableOfContents\Normalizer\RelativeNormalizerStrategy;
28
use League\CommonMark\Node\Block\Document;
29
use League\CommonMark\Node\Block\Paragraph;
30
use League\CommonMark\Node\StringContainerHelper;
31
32
final class TableOfContentsGenerator implements TableOfContentsGeneratorInterface
33
{
34
    public const STYLE_BULLET = ListBlock::TYPE_BULLET;
35
    public const STYLE_ORDERED = ListBlock::TYPE_ORDERED;
36
37
    public const NORMALIZE_DISABLED = 'as-is';
38
    public const NORMALIZE_RELATIVE = 'relative';
39
    public const NORMALIZE_FLAT = 'flat';
40
41
    /** @var string */
42
    private $style;
43
    /** @var string */
44
    private $normalizationStrategy;
45
    /** @var int */
46
    private $minHeadingLevel;
47
    /** @var int */
48
    private $maxHeadingLevel;
49
50 36
    public function __construct(string $style, string $normalizationStrategy, int $minHeadingLevel, int $maxHeadingLevel)
51
    {
52 36
        $this->style = $style;
53 36
        $this->normalizationStrategy = $normalizationStrategy;
54 36
        $this->minHeadingLevel = $minHeadingLevel;
55 36
        $this->maxHeadingLevel = $maxHeadingLevel;
56 36
    }
57
58 36
    public function generate(Document $document): ?TableOfContents
59
    {
60 36
        $toc = $this->createToc();
61
62 36
        $normalizer = $this->getNormalizer($toc);
63
64 36
        $firstHeading = null;
65
66 36
        foreach ($this->getHeadingLinks($document) as $headingLink) {
67 33
            $heading = $headingLink->parent();
68
            // Make sure this is actually tied to a heading
69 33
            if (!$heading instanceof Heading) {
70
                continue;
71
            }
72
73
            // Skip any headings outside the configured min/max levels
74 33
            if ($heading->getLevel() < $this->minHeadingLevel || $heading->getLevel() > $this->maxHeadingLevel) {
75 3
                continue;
76
            }
77
78
            // Keep track of the first heading we see - we might need this later
79 33
            $firstHeading = $firstHeading ?? $heading;
80
81
            // Keep track of the start and end lines
82 33
            $toc->setStartLine($firstHeading->getStartLine());
83 33
            $toc->setEndLine($heading->getEndLine());
84
85
            // Create the new link
86 33
            $link = new Link('#' . $headingLink->getSlug(), StringContainerHelper::getChildText($heading, [HtmlBlock::class, HtmlInline::class]));
87 33
            $paragraph = new Paragraph();
88 33
            $paragraph->setStartLine($heading->getStartLine());
89 33
            $paragraph->setEndLine($heading->getEndLine());
90 33
            $paragraph->appendChild($link);
91
92 33
            $listItem = new ListItem($toc->getListData());
93 33
            $listItem->setStartLine($heading->getStartLine());
94 33
            $listItem->setEndLine($heading->getEndLine());
95 33
            $listItem->appendChild($paragraph);
96
97
            // Add it to the correct place
98 33
            $normalizer->addItem($heading->getLevel(), $listItem);
99
        }
100
101
        // Don't add the TOC if no headings were present
102 36
        if (!$toc->hasChildren() || $firstHeading === null) {
103 6
            return null;
104
        }
105
106 33
        return $toc;
107
    }
108
109 36
    private function createToc(): TableOfContents
110
    {
111 36
        $listData = new ListData();
112
113 36
        if ($this->style === self::STYLE_BULLET) {
114 33
            $listData->type = ListBlock::TYPE_BULLET;
115 3
        } elseif ($this->style === self::STYLE_ORDERED) {
116 3
            $listData->type = ListBlock::TYPE_ORDERED;
117
        } else {
118
            throw new InvalidOptionException(\sprintf('Invalid table of contents list style "%s"', $this->style));
119
        }
120
121 36
        return new TableOfContents($listData);
122
    }
123
124
    /**
125
     * @param Document $document
126
     *
127
     * @return iterable<HeadingPermalink>
0 ignored issues
show
Documentation introduced by
The doc-type iterable<HeadingPermalink> could not be parsed: Expected "|" or "end of type", but got "<" at position 8. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
128
     */
129 36
    private function getHeadingLinks(Document $document)
130
    {
131 36
        $walker = $document->walker();
132 36
        while ($event = $walker->next()) {
133 36
            if ($event->isEntering() && ($node = $event->getNode()) instanceof HeadingPermalink) {
134 33
                yield $node;
135
            }
136
        }
137 36
    }
138
139 36
    private function getNormalizer(TableOfContents $toc): NormalizerStrategyInterface
140
    {
141 36
        switch ($this->normalizationStrategy) {
142 36
            case self::NORMALIZE_DISABLED:
143 3
                return new AsIsNormalizerStrategy($toc);
144 33
            case self::NORMALIZE_RELATIVE:
145 30
                return new RelativeNormalizerStrategy($toc);
146 3
            case self::NORMALIZE_FLAT:
147 3
                return new FlatNormalizerStrategy($toc);
148
            default:
149
                throw new InvalidOptionException(\sprintf('Invalid table of contents normalization strategy "%s"', $this->normalizationStrategy));
150
        }
151
    }
152
}
153