Passed
Push — latest ( ba39d8...ca9086 )
by Colin
08:23
created

HeadingPermalinkProcessor::ensureUnique()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 10
dl 0
loc 18
ccs 10
cts 10
cp 1
c 1
b 0
f 0
rs 9.9332
cc 3
nc 2
nop 2
crap 3
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\HeadingPermalink;
15
16
use League\CommonMark\Configuration\ConfigurationAwareInterface;
17
use League\CommonMark\Configuration\ConfigurationInterface;
18
use League\CommonMark\Event\DocumentParsedEvent;
19
use League\CommonMark\Exception\InvalidOptionException;
20
use League\CommonMark\Extension\CommonMark\Node\Block\Heading;
21
use League\CommonMark\Node\Block\Document;
22
use League\CommonMark\Node\StringContainerHelper;
23
use League\CommonMark\Normalizer\SlugNormalizer;
24
use League\CommonMark\Normalizer\TextNormalizerInterface;
25
26
/**
27
 * Searches the Document for Heading elements and adds HeadingPermalinks to each one
28
 */
29
final class HeadingPermalinkProcessor implements ConfigurationAwareInterface
30
{
31
    public const INSERT_BEFORE = 'before';
32
    public const INSERT_AFTER  = 'after';
33
34
    /**
35
     * @var TextNormalizerInterface
36
     *
37
     * @psalm-readonly-allow-private-mutation
38
     */
39
    private $slugNormalizer;
40
41
    /**
42
     * @var ConfigurationInterface
43
     *
44
     * @psalm-readonly-allow-private-mutation
45
     */
46
    private $config;
47
48 87
    public function __construct(?TextNormalizerInterface $slugNormalizer = null)
49
    {
50 87
        $this->slugNormalizer = $slugNormalizer ?? new SlugNormalizer();
51 87
    }
52
53 87
    public function setConfiguration(ConfigurationInterface $configuration): void
54
    {
55 87
        $this->config = $configuration;
56 87
    }
57
58 87
    public function __invoke(DocumentParsedEvent $e): void
59
    {
60 87
        $this->useNormalizerFromConfigurationIfProvided();
61
62 84
        $min = (int) $this->config->get('heading_permalink/min_heading_level', 1);
63 84
        $max = (int) $this->config->get('heading_permalink/max_heading_level', 6);
64
65 84
        $walker = $e->getDocument()->walker();
66
67 84
        while ($event = $walker->next()) {
68 84
            $node = $event->getNode();
69 84
            if ($node instanceof Heading && $event->isEntering() && $node->getLevel() >= $min && $node->getLevel() <= $max) {
70 81
                $this->addHeadingLink($node, $e->getDocument());
71
            }
72
        }
73 81
    }
74
75 87
    private function useNormalizerFromConfigurationIfProvided(): void
76
    {
77 87
        $normalizer = $this->config->get('heading_permalink/slug_normalizer');
78 87
        if ($normalizer === null) {
79 81
            return;
80
        }
81
82 6
        if (! $normalizer instanceof TextNormalizerInterface) {
83 3
            throw new InvalidOptionException('The heading_permalink/slug_normalizer option must be an instance of ' . TextNormalizerInterface::class);
84
        }
85
86 3
        $this->slugNormalizer = $normalizer;
87 3
    }
88
89 81
    private function addHeadingLink(Heading $heading, Document $document): void
90
    {
91 81
        $text = StringContainerHelper::getChildText($heading);
92 81
        $slug = $this->slugNormalizer->normalize($text, $heading);
93
94 81
        $slug = $this->ensureUnique($slug, $document);
95
96 81
        $headingLinkAnchor = new HeadingPermalink($slug);
97
98 81
        switch ($this->config->get('heading_permalink/insert', 'before')) {
99 81
            case self::INSERT_BEFORE:
100 69
                $heading->prependChild($headingLinkAnchor);
101
102 69
                return;
103 12
            case self::INSERT_AFTER:
104 9
                $heading->appendChild($headingLinkAnchor);
105
106 9
                return;
107
            default:
108 3
                throw new \RuntimeException("Invalid configuration value for heading_permalink/insert; expected 'before' or 'after'");
109
        }
110
    }
111
112 81
    private function ensureUnique(string $proposed, Document $document): string
113
    {
114
        // Quick path, it's a unique ID
115 81
        if (! isset($document->data['heading_ids'][$proposed])) {
116 81
            $document->data['heading_ids'][$proposed] = true;
117
118 81
            return $proposed;
119
        }
120
121 9
        $extension = 0;
122
        do {
123 9
            ++$extension;
124 9
            $id = \sprintf('%s-%s', $proposed, $extension);
125 9
        } while (isset($document->data['heading_ids'][$id]));
126
127 9
        $document->data['heading_ids'][$id] = true;
128
129 9
        return $id;
130
    }
131
}
132