Completed
Push — 1.6 ( b4f008...1351ea )
by Colin
129:05 queued 94:09
created

TableOfContentsGenerator::getHeadingText()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 0
cts 0
cp 0
rs 9.8333
c 0
b 0
f 0
cc 4
nc 3
nop 1
crap 20
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\AbstractStringContainer;
28
use League\CommonMark\Inline\Element\Link;
29
30
final class TableOfContentsGenerator implements TableOfContentsGeneratorInterface
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
    /** @var string */
40
    private $style;
41
    /** @var string */
42
    private $normalizationStrategy;
43
    /** @var int */
44
    private $minHeadingLevel;
45
    /** @var int */
46
    private $maxHeadingLevel;
47 36
48
    public function __construct(string $style, string $normalizationStrategy, int $minHeadingLevel, int $maxHeadingLevel)
49 36
    {
50 36
        $this->style = $style;
51 36
        $this->normalizationStrategy = $normalizationStrategy;
52 36
        $this->minHeadingLevel = $minHeadingLevel;
53 36
        $this->maxHeadingLevel = $maxHeadingLevel;
54
    }
55 36
56
    public function generate(Document $document): ?TableOfContents
57 36
    {
58
        $toc = $this->createToc($document);
59 36
60
        $normalizer = $this->getNormalizer($toc);
61 36
62
        $firstHeading = null;
63 36
64 33
        foreach ($this->getHeadingLinks($document) as $headingLink) {
65
            $heading = $headingLink->parent();
66 33
            // Make sure this is actually tied to a heading
67
            if (!$heading instanceof Heading) {
68
                continue;
69
            }
70
71 33
            // Skip any headings outside the configured min/max levels
72 3
            if ($heading->getLevel() < $this->minHeadingLevel || $heading->getLevel() > $this->maxHeadingLevel) {
73
                continue;
74
            }
75
76 33
            // Keep track of the first heading we see - we might need this later
77
            $firstHeading = $firstHeading ?? $heading;
78
79 33
            // Keep track of the start and end lines
80 33
            $toc->setStartLine($firstHeading->getStartLine());
81
            $toc->setEndLine($heading->getEndLine());
82
83 33
            // Create the new link
84 33
            $link = new Link('#' . $headingLink->getSlug(), self::getHeadingText($heading));
85 33
            $paragraph = new Paragraph();
86 33
            $paragraph->setStartLine($heading->getStartLine());
87 33
            $paragraph->setEndLine($heading->getEndLine());
88
            $paragraph->appendChild($link);
89 33
90 33
            $listItem = new ListItem($toc->getListData());
91 33
            $listItem->setStartLine($heading->getStartLine());
92 33
            $listItem->setEndLine($heading->getEndLine());
93
            $listItem->appendChild($paragraph);
94
95 33
            // Add it to the correct place
96
            $normalizer->addItem($heading->getLevel(), $listItem);
97
        }
98
99 36
        // Don't add the TOC if no headings were present
100 6
        if (!$toc->hasChildren() || $firstHeading === null) {
101
            return null;
102
        }
103 33
104
        return $toc;
105
    }
106 36
107
    private function createToc(Document $document): TableOfContents
108 36
    {
109
        $listData = new ListData();
110 36
111 33
        if ($this->style === self::STYLE_BULLET) {
112 3
            $listData->type = ListBlock::TYPE_BULLET;
113 3
        } elseif ($this->style === self::STYLE_ORDERED) {
114
            $listData->type = ListBlock::TYPE_ORDERED;
115
        } else {
116
            throw new InvalidOptionException(\sprintf('Invalid table of contents list style "%s"', $this->style));
117
        }
118 36
119
        $toc = new TableOfContents($listData);
120 36
121 36
        $toc->setStartLine($document->getStartLine());
122
        $toc->setEndLine($document->getEndLine());
123 36
124
        return $toc;
125
    }
126
127
    /**
128
     * @param Document $document
129
     *
130
     * @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...
131 36
     */
132
    private function getHeadingLinks(Document $document)
133 36
    {
134 36
        $walker = $document->walker();
135 36
        while ($event = $walker->next()) {
136 33
            if ($event->isEntering() && ($node = $event->getNode()) instanceof HeadingPermalink) {
137
                yield $node;
138
            }
139 36
        }
140
    }
141 36
142
    private function getNormalizer(TableOfContents $toc): NormalizerStrategyInterface
143 36
    {
144 36
        switch ($this->normalizationStrategy) {
145 3
            case self::NORMALIZE_DISABLED:
146 33
                return new AsIsNormalizerStrategy($toc);
147 30
            case self::NORMALIZE_RELATIVE:
148 3
                return new RelativeNormalizerStrategy($toc);
149 3
            case self::NORMALIZE_FLAT:
150
                return new FlatNormalizerStrategy($toc);
151
            default:
152
                throw new InvalidOptionException(\sprintf('Invalid table of contents normalization strategy "%s"', $this->normalizationStrategy));
153
        }
154
    }
155
156
    /**
157
     * @return string
158
     */
159
    private static function getHeadingText(Heading $heading)
160
    {
161
        $text = '';
162
163
        $walker = $heading->walker();
164
        while ($event = $walker->next()) {
165
            if ($event->isEntering() && ($child = $event->getNode()) instanceof AbstractStringContainer) {
166
                $text .= $child->getContent();
167
            }
168
        }
169
170
        return $text;
171
    }
172
}
173