Passed
Pull Request — main (#1074)
by
unknown
04:44 queued 02:25
created

TableOfContentsGenerator::generate()   B

Complexity

Conditions 8
Paths 12

Size

Total Lines 56
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 8.0021

Importance

Changes 0
Metric Value
cc 8
eloc 30
c 0
b 0
f 0
nc 12
nop 1
dl 0
loc 56
ccs 30
cts 31
cp 0.9677
crap 8.0021
rs 8.1954

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Extension\CommonMark\Node\Block\Heading;
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\Link;
21
use League\CommonMark\Extension\CommonMark\Node\Inline\Strong;
22
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalink;
23
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
24
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsWrapper;
25
use League\CommonMark\Extension\TableOfContents\Normalizer\AsIsNormalizerStrategy;
26
use League\CommonMark\Extension\TableOfContents\Normalizer\FlatNormalizerStrategy;
27
use League\CommonMark\Extension\TableOfContents\Normalizer\NormalizerStrategyInterface;
28
use League\CommonMark\Extension\TableOfContents\Normalizer\RelativeNormalizerStrategy;
29
use League\CommonMark\Node\Block\AbstractBlock;
30
use League\CommonMark\Node\Block\Document;
31
use League\CommonMark\Node\Inline\Text;
32
use League\CommonMark\Node\NodeIterator;
33
use League\CommonMark\Node\RawMarkupContainerInterface;
34
use League\CommonMark\Node\StringContainerHelper;
35
use League\Config\Exception\InvalidConfigurationException;
36
37
final class TableOfContentsGenerator implements TableOfContentsGeneratorInterface
38
{
39
    public const STYLE_BULLET  = ListBlock::TYPE_BULLET;
40
    public const STYLE_ORDERED = ListBlock::TYPE_ORDERED;
41
42
    public const NORMALIZE_DISABLED = 'as-is';
43
    public const NORMALIZE_RELATIVE = 'relative';
44
    public const NORMALIZE_FLAT     = 'flat';
45
46
    /** @psalm-readonly */
47
    private string $style;
48
49
    /** @psalm-readonly */
50
    private string $normalizationStrategy;
51
52
    /** @psalm-readonly */
53
    private int $minHeadingLevel;
54
55
    /** @psalm-readonly */
56
    private int $maxHeadingLevel;
57
58
    /** @psalm-readonly */
59
    private string $fragmentPrefix;
60
61
    /** @psalm-readonly */
62
    private string $label;
63
64 70
    public function __construct(string $style, string $normalizationStrategy, int $minHeadingLevel, int $maxHeadingLevel, string $fragmentPrefix, string $label = '')
65
    {
66 70
        $this->style                 = $style;
67 70
        $this->normalizationStrategy = $normalizationStrategy;
68 70
        $this->minHeadingLevel       = $minHeadingLevel;
69 70
        $this->maxHeadingLevel       = $maxHeadingLevel;
70 70
        $this->fragmentPrefix        = $fragmentPrefix;
71 70
        $this->label                 = $label;
72
73 70
        if ($fragmentPrefix !== '') {
74 70
            $this->fragmentPrefix .= '-';
75
        }
76
    }
77
78
    /**
79
     * If there is a table of contents, returns either a `TableOfContents` or
80
     * `TableOfContentsWrapper` node object
81
     */
82 70
    public function generate(Document $document): ?AbstractBlock
83
    {
84 70
        $toc = $this->createToc($document);
85
86 70
        $normalizer = $this->getNormalizer($toc);
87
88 70
        $firstHeading = null;
89
90 70
        foreach ($this->getHeadingLinks($document) as $headingLink) {
91 62
            $heading = $headingLink->parent();
92
            // Make sure this is actually tied to a heading
93 62
            if (! $heading instanceof Heading) {
94
                continue;
95
            }
96
97
            // Skip any headings outside the configured min/max levels
98 62
            if ($heading->getLevel() < $this->minHeadingLevel || $heading->getLevel() > $this->maxHeadingLevel) {
99 4
                continue;
100
            }
101
102
            // Keep track of the first heading we see - we might need this later
103 62
            $firstHeading ??= $heading;
104
105
            // Keep track of the start and end lines
106 62
            $toc->setStartLine($firstHeading->getStartLine());
107 62
            $toc->setEndLine($heading->getEndLine());
108
109
            // Create the new link
110 62
            $link = new Link('#' . $this->fragmentPrefix . $headingLink->getSlug(), StringContainerHelper::getChildText($heading, [RawMarkupContainerInterface::class]));
111
112 62
            $listItem = new ListItem($toc->getListData());
113 62
            $listItem->setStartLine($heading->getStartLine());
114 62
            $listItem->setEndLine($heading->getEndLine());
115 62
            $listItem->appendChild($link);
116
117
            // Add it to the correct place
118 62
            $normalizer->addItem($heading->getLevel(), $listItem);
119
        }
120
121
        // Don't add the TOC if no headings were present
122 70
        if (! $toc->hasChildren() || $firstHeading === null) {
123 8
            return null;
124
        }
125
126 62
        if ($this->label !== '') {
127 4
            $label = new Strong();
128 4
            $label->appendChild(new Text($this->label));
129 4
            $wrapper = new TableOfContentsWrapper();
130 4
            $wrapper->appendChild($label);
131 4
            $wrapper->appendChild($toc);
132 4
            $wrapper->setStartLine($toc->getStartLine());
133 4
            $wrapper->setEndLine($toc->getEndLine());
134 4
            return $wrapper;
135
        }
136
137 58
        return $toc;
138
    }
139
140 70
    private function createToc(Document $document): TableOfContents
141
    {
142 70
        $listData = new ListData();
143
144 70
        if ($this->style === self::STYLE_BULLET) {
145 66
            $listData->type = ListBlock::TYPE_BULLET;
146 4
        } elseif ($this->style === self::STYLE_ORDERED) {
147 4
            $listData->type = ListBlock::TYPE_ORDERED;
148
        } else {
149
            throw new InvalidConfigurationException(\sprintf('Invalid table of contents list style: "%s"', $this->style));
150
        }
151
152 70
        $toc = new TableOfContents($listData);
153
154 70
        $toc->setStartLine($document->getStartLine());
155 70
        $toc->setEndLine($document->getEndLine());
156
157 70
        return $toc;
158
    }
159
160
    /**
161
     * @return iterable<HeadingPermalink>
162
     */
163 70
    private function getHeadingLinks(Document $document): iterable
164
    {
165 70
        foreach ($document->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) {
166 70
            if (! $node instanceof Heading) {
167 70
                continue;
168
            }
169
170 62
            foreach ($node->children() as $child) {
171 62
                if ($child instanceof HeadingPermalink) {
172 62
                    yield $child;
173
                }
174
            }
175
        }
176
    }
177
178 70
    private function getNormalizer(TableOfContents $toc): NormalizerStrategyInterface
179
    {
180 70
        switch ($this->normalizationStrategy) {
181 35
            case self::NORMALIZE_DISABLED:
182 4
                return new AsIsNormalizerStrategy($toc);
183 33
            case self::NORMALIZE_RELATIVE:
184 62
                return new RelativeNormalizerStrategy($toc);
185 2
            case self::NORMALIZE_FLAT:
186 4
                return new FlatNormalizerStrategy($toc);
187
            default:
188
                throw new InvalidConfigurationException(\sprintf('Invalid table of contents normalization strategy: "%s"', $this->normalizationStrategy));
189
        }
190
    }
191
}
192