Completed
Push — 1.5 ( 93aa3f...fe2226 )
by Colin
01:05
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\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\Exception\InvalidOptionException;
21
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalink;
22
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, League\CommonMark\Extens...ontents\TableOfContents.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
23
use League\CommonMark\Extension\TableOfContents\Normalizer\AsIsNormalizerStrategy;
24
use League\CommonMark\Extension\TableOfContents\Normalizer\FlatNormalizerStrategy;
25
use League\CommonMark\Extension\TableOfContents\Normalizer\NormalizerStrategyInterface;
26
use League\CommonMark\Extension\TableOfContents\Normalizer\RelativeNormalizerStrategy;
27
use League\CommonMark\Inline\Element\Link;
28
29
final class TableOfContentsGenerator implements TableOfContentsGeneratorInterface
30
{
31
    public const STYLE_BULLET = ListBlock::TYPE_BULLET;
32
    public const STYLE_ORDERED = ListBlock::TYPE_ORDERED;
33
34
    public const NORMALIZE_DISABLED = 'as-is';
35
    public const NORMALIZE_RELATIVE = 'relative';
36
    public const NORMALIZE_FLAT = 'flat';
37
38
    /** @var string */
39
    private $style;
40
    /** @var string */
41
    private $normalizationStrategy;
42
    /** @var int */
43
    private $minHeadingLevel;
44
    /** @var int */
45
    private $maxHeadingLevel;
46
47 36
    public function __construct(string $style, string $normalizationStrategy, int $minHeadingLevel, int $maxHeadingLevel)
48
    {
49 36
        $this->style = $style;
50 36
        $this->normalizationStrategy = $normalizationStrategy;
51 36
        $this->minHeadingLevel = $minHeadingLevel;
52 36
        $this->maxHeadingLevel = $maxHeadingLevel;
53 36
    }
54
55 36
    public function generate(Document $document): ?TableOfContents
56
    {
57 36
        $toc = $this->createToc();
58
59 36
        $normalizer = $this->getNormalizer($toc);
60
61 36
        $firstHeading = null;
62
63 36
        foreach ($this->getHeadingLinks($document) as $headingLink) {
64 33
            $heading = $headingLink->parent();
65
            // Make sure this is actually tied to a heading
66 33
            if (!$heading instanceof Heading) {
67
                continue;
68
            }
69
70
            // Skip any headings outside the configured min/max levels
71 33
            if ($heading->getLevel() < $this->minHeadingLevel || $heading->getLevel() > $this->maxHeadingLevel) {
72 3
                continue;
73
            }
74
75
            // Keep track of the first heading we see - we might need this later
76 33
            $firstHeading = $firstHeading ?? $heading;
77
78
            // Keep track of the start and end lines
79 33
            $toc->setStartLine($firstHeading->getStartLine());
80 33
            $toc->setEndLine($heading->getEndLine());
81
82
            // Create the new link
83 33
            $link = new Link('#' . $headingLink->getSlug(), $heading->getStringContent());
84 33
            $paragraph = new Paragraph();
85 33
            $paragraph->setStartLine($heading->getStartLine());
86 33
            $paragraph->setEndLine($heading->getEndLine());
87 33
            $paragraph->appendChild($link);
88
89 33
            $listItem = new ListItem($toc->getListData());
90 33
            $listItem->setStartLine($heading->getStartLine());
91 33
            $listItem->setEndLine($heading->getEndLine());
92 33
            $listItem->appendChild($paragraph);
93
94
            // Add it to the correct place
95 33
            $normalizer->addItem($heading->getLevel(), $listItem);
96
        }
97
98
        // Don't add the TOC if no headings were present
99 36
        if (!$toc->hasChildren() || $firstHeading === null) {
100 6
            return null;
101
        }
102
103 33
        return $toc;
104
    }
105
106 36
    private function createToc(): TableOfContents
107
    {
108 36
        $listData = new ListData();
109
110 36
        if ($this->style === self::STYLE_BULLET) {
111 33
            $listData->type = ListBlock::TYPE_BULLET;
112 3
        } elseif ($this->style === self::STYLE_ORDERED) {
113 3
            $listData->type = ListBlock::TYPE_ORDERED;
114
        } else {
115
            throw new InvalidOptionException(\sprintf('Invalid table of contents list style "%s"', $this->style));
116
        }
117
118 36
        return new TableOfContents($listData);
119
    }
120
121
    /**
122
     * @param Document $document
123
     *
124
     * @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...
125
     */
126 36
    private function getHeadingLinks(Document $document)
127
    {
128 36
        $walker = $document->walker();
129 36
        while ($event = $walker->next()) {
130 36
            if ($event->isEntering() && ($node = $event->getNode()) instanceof HeadingPermalink) {
131 33
                yield $node;
132
            }
133
        }
134 36
    }
135
136 36
    private function getNormalizer(TableOfContents $toc): NormalizerStrategyInterface
137
    {
138 36
        switch ($this->normalizationStrategy) {
139 36
            case self::NORMALIZE_DISABLED:
140 3
                return new AsIsNormalizerStrategy($toc);
141 33
            case self::NORMALIZE_RELATIVE:
142 30
                return new RelativeNormalizerStrategy($toc);
143 3
            case self::NORMALIZE_FLAT:
144 3
                return new FlatNormalizerStrategy($toc);
145
            default:
146
                throw new InvalidOptionException(\sprintf('Invalid table of contents normalization strategy "%s"', $this->normalizationStrategy));
147
        }
148
    }
149
}
150