Passed
Pull Request — main (#1074)
by
unknown
02:27
created

insertBeforeFirstLinkedHeading()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 7.7656

Importance

Changes 0
Metric Value
cc 7
eloc 10
c 0
b 0
f 0
nc 5
nop 2
dl 0
loc 19
ccs 9
cts 12
cp 0.75
crap 7.7656
rs 8.8333
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\Event\DocumentParsedEvent;
17
use League\CommonMark\Exception\InvalidArgumentException;
18
use League\CommonMark\Extension\CommonMark\Node\Block\Heading;
19
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalink;
20
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
21
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsPlaceholder;
22
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsWrapper;
23
use League\CommonMark\Node\Block\AbstractBlock;
24
use League\CommonMark\Node\Block\Document;
25
use League\CommonMark\Node\NodeIterator;
26
use League\Config\ConfigurationAwareInterface;
27
use League\Config\ConfigurationInterface;
28
use League\Config\Exception\InvalidConfigurationException;
29
30
final class TableOfContentsBuilder implements ConfigurationAwareInterface
31
{
32
    public const POSITION_TOP             = 'top';
33
    public const POSITION_BEFORE_HEADINGS = 'before-headings';
34
    public const POSITION_PLACEHOLDER     = 'placeholder';
35
36
    /** @psalm-readonly-allow-private-mutation */
37
    private ConfigurationInterface $config;
38
39 70
    public function onDocumentParsed(DocumentParsedEvent $event): void
40
    {
41 70
        $document = $event->getDocument();
42
43 70
        $generator = new TableOfContentsGenerator(
44 70
            (string) $this->config->get('table_of_contents/style'),
45 70
            (string) $this->config->get('table_of_contents/normalize'),
46 70
            (int) $this->config->get('table_of_contents/min_heading_level'),
47 70
            (int) $this->config->get('table_of_contents/max_heading_level'),
48 70
            (string) $this->config->get('heading_permalink/fragment_prefix'),
49 70
            (string) $this->config->get('table_of_contents/label'),
50 70
        );
51
52 70
        $toc = $generator->generate($document);
53 70
        if ($toc === null) {
54
            // No linkable headers exist, so no TOC could be generated
55 8
            return;
56
        }
57
58
        // Add custom CSS class(es), if defined
59 62
        $class = $this->config->get('table_of_contents/html_class');
60 62
        if ($class !== null) {
61 62
            if ($toc instanceof TableOfContentsWrapper) {
62 4
                $toc->getInnerToc()->data->append('attributes/class', $class);
63
            } else {
64 58
                $toc->data->append('attributes/class', $class);
65
            }
66
        }
67
68
        // Add the TOC to the Document
69 62
        $position = $this->config->get('table_of_contents/position');
70 62
        if ($position === self::POSITION_TOP) {
71 50
            $document->prependChild($toc);
72 12
        } elseif ($position === self::POSITION_BEFORE_HEADINGS) {
73 4
            $this->insertBeforeFirstLinkedHeading($document, $toc);
74 8
        } elseif ($position === self::POSITION_PLACEHOLDER) {
75 8
            $this->replacePlaceholders($document, $toc);
76
        } else {
77
            throw InvalidConfigurationException::forConfigOption('table_of_contents/position', $position);
78
        }
79
    }
80
81
    /**
82
     * @psalm-param TableOfContents|TableOfContentsWrapper $toc
83
     *
84
     * @phpstan-param TableOfContents|TableOfContentsWrapper $toc
85
     */
86 4
    private function insertBeforeFirstLinkedHeading(Document $document, AbstractBlock $toc): void
87
    {
88
        // @phpstan-ignore booleanAnd.alwaysFalse
89 4
        if (! $toc instanceof TableOfContents && ! $toc instanceof TableOfContentsWrapper) {
90
            throw new InvalidArgumentException(
91
                'Toc should be a TableOfContents or TableOfContentsWrapper, got ' . \get_class($toc)
92
            );
93
        }
94
95 4
        foreach ($document->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) {
96 4
            if (! $node instanceof Heading) {
97 4
                continue;
98
            }
99
100 4
            foreach ($node->children() as $child) {
101 4
                if ($child instanceof HeadingPermalink) {
102 4
                    $node->insertBefore($toc);
103
104 4
                    return;
105
                }
106
            }
107
        }
108
    }
109
110
    /**
111
     * @psalm-param TableOfContents|TableOfContentsWrapper $toc
112
     *
113
     * @phpstan-param TableOfContents|TableOfContentsWrapper $toc
114
     */
115 8
    private function replacePlaceholders(Document $document, AbstractBlock $toc): void
116
    {
117
        // @phpstan-ignore booleanAnd.alwaysFalse
118 8
        if (! $toc instanceof TableOfContents && ! $toc instanceof TableOfContentsWrapper) {
119
            throw new InvalidArgumentException(
120
                'Toc should be a TableOfContents or TableOfContentsWrapper, got ' . \get_class($toc)
121
            );
122
        }
123
124 8
        foreach ($document->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) {
125
            // Add the block once we find a placeholder
126 8
            if (! $node instanceof TableOfContentsPlaceholder) {
127 8
                continue;
128
            }
129
130 4
            $node->replaceWith(clone $toc);
131
        }
132
    }
133
134 70
    public function setConfiguration(ConfigurationInterface $configuration): void
135
    {
136 70
        $this->config = $configuration;
137
    }
138
}
139