DelimiterStack::processDelimiters()   C
last analyzed

Complexity

Conditions 15
Paths 30

Size

Total Lines 90
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 45
CRAP Score 15

Importance

Changes 0
Metric Value
eloc 45
dl 0
loc 90
ccs 45
cts 45
cp 1
rs 5.9166
c 0
b 0
f 0
cc 15
nc 30
nop 2
crap 15

How to fix   Long Method    Complexity   

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
 * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
11
 *  - (c) John MacFarlane
12
 *
13
 * Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java)
14
 *  - (c) Atlassian Pty Ltd
15
 *
16
 * For the full copyright and license information, please view the LICENSE
17
 * file that was distributed with this source code.
18
 */
19
20
namespace League\CommonMark\Delimiter;
21
22
use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
23
use League\CommonMark\Node\Inline\AdjacentTextMerger;
24
25
final class DelimiterStack
26
{
27
    /**
28
     * @var DelimiterInterface|null
29
     *
30
     * @psalm-readonly-allow-private-mutation
31
     */
32
    private $top;
33
34 1116
    public function push(DelimiterInterface $newDelimiter): void
35
    {
36 1116
        $newDelimiter->setPrevious($this->top);
37
38 1116
        if ($this->top !== null) {
39 636
            $this->top->setNext($newDelimiter);
40
        }
41
42 1116
        $this->top = $newDelimiter;
43 1116
    }
44
45 2694
    private function findEarliest(?DelimiterInterface $stackBottom = null): ?DelimiterInterface
46
    {
47 2694
        $delimiter = $this->top;
48 2694
        while ($delimiter !== null && $delimiter->getPrevious() !== $stackBottom) {
49 594
            $delimiter = $delimiter->getPrevious();
50
        }
51
52 2694
        return $delimiter;
53
    }
54
55 1116
    public function removeDelimiter(DelimiterInterface $delimiter): void
56
    {
57 1116
        if ($delimiter->getPrevious() !== null) {
58
            /** @psalm-suppress PossiblyNullReference */
59 330
            $delimiter->getPrevious()->setNext($delimiter->getNext());
60
        }
61
62 1116
        if ($delimiter->getNext() === null) {
63
            // top of stack
64 1116
            $this->top = $delimiter->getPrevious();
65
        } else {
66
            /** @psalm-suppress PossiblyNullReference */
67 516
            $delimiter->getNext()->setPrevious($delimiter->getPrevious());
68
        }
69 1116
    }
70
71 525
    private function removeDelimiterAndNode(DelimiterInterface $delimiter): void
72
    {
73 525
        $delimiter->getInlineNode()->detach();
74 525
        $this->removeDelimiter($delimiter);
75 525
    }
76
77 525
    private function removeDelimitersBetween(DelimiterInterface $opener, DelimiterInterface $closer): void
78
    {
79 525
        $delimiter = $closer->getPrevious();
80 525
        while ($delimiter !== null && $delimiter !== $opener) {
81 24
            $previous = $delimiter->getPrevious();
82 24
            $this->removeDelimiter($delimiter);
83 24
            $delimiter = $previous;
84
        }
85 525
    }
86
87 2694
    public function removeAll(?DelimiterInterface $stackBottom = null): void
88
    {
89 2694
        while ($this->top && $this->top !== $stackBottom) {
90 570
            $this->removeDelimiter($this->top);
91
        }
92 2694
    }
93
94 351
    public function removeEarlierMatches(string $character): void
95
    {
96 351
        $opener = $this->top;
97 351
        while ($opener !== null) {
98 69
            if ($opener->getChar() === $character) {
99 24
                $opener->setActive(false);
100
            }
101
102 69
            $opener = $opener->getPrevious();
103
        }
104 351
    }
105
106
    /**
107
     * @param string|string[] $characters
108
     */
109 510
    public function searchByCharacter($characters): ?DelimiterInterface
110
    {
111 510
        if (! \is_array($characters)) {
112
            $characters = [$characters];
113
        }
114
115 510
        $opener = $this->top;
116 510
        while ($opener !== null) {
117 504
            if (\in_array($opener->getChar(), $characters, true)) {
118 501
                break;
119
            }
120
121 72
            $opener = $opener->getPrevious();
122
        }
123
124 510
        return $opener;
125
    }
126
127 2694
    public function processDelimiters(?DelimiterInterface $stackBottom, DelimiterProcessorCollection $processors): void
128
    {
129 2694
        $openersBottom = [];
130
131
        // Find first closer above stackBottom
132 2694
        $closer = $this->findEarliest($stackBottom);
133
134
        // Move forward, looking for closers, and handling each
135 2694
        while ($closer !== null) {
136 1032
            $delimiterChar = $closer->getChar();
137
138 1032
            $delimiterProcessor = $processors->getDelimiterProcessor($delimiterChar);
139 1032
            if (! $closer->canClose() || $delimiterProcessor === null) {
140 945
                $closer = $closer->getNext();
141 945
                continue;
142
            }
143
144 630
            $openingDelimiterChar = $delimiterProcessor->getOpeningCharacter();
145
146 630
            $useDelims            = 0;
147 630
            $openerFound          = false;
148 630
            $potentialOpenerFound = false;
149 630
            $opener               = $closer->getPrevious();
150 630
            while ($opener !== null && $opener !== $stackBottom && $opener !== ($openersBottom[$delimiterChar] ?? null)) {
151 555
                if ($opener->canOpen() && $opener->getChar() === $openingDelimiterChar) {
152 534
                    $potentialOpenerFound = true;
153 534
                    $useDelims            = $delimiterProcessor->getDelimiterUse($opener, $closer);
154 534
                    if ($useDelims > 0) {
155 525
                        $openerFound = true;
156 525
                        break;
157
                    }
158
                }
159
160 63
                $opener = $opener->getPrevious();
161
            }
162
163 630
            if (! $openerFound) {
164 186
                if (! $potentialOpenerFound) {
165
                    // Only do this when we didn't even have a potential
166
                    // opener (one that matches the character and can open).
167
                    // If an opener was rejected because of the number of
168
                    // delimiters (e.g. because of the "multiple of 3"
169
                    // Set lower bound for future searches for openersrule),
170
                    // we want to consider it next time because the number
171
                    // of delimiters can change as we continue processing.
172 165
                    $openersBottom[$delimiterChar] = $closer->getPrevious();
173 165
                    if (! $closer->canOpen()) {
174
                        // We can remove a closer that can't be an opener,
175
                        // once we've seen there's no matching opener.
176 123
                        $this->removeDelimiter($closer);
177
                    }
178
                }
179
180 186
                $closer = $closer->getNext();
181 186
                continue;
182
            }
183
184
            \assert($opener !== null);
185
186 525
            $openerNode = $opener->getInlineNode();
187 525
            $closerNode = $closer->getInlineNode();
188
189
            // Remove number of used delimiters from stack and inline nodes.
190 525
            $opener->setLength($opener->getLength() - $useDelims);
191 525
            $closer->setLength($closer->getLength() - $useDelims);
192
193 525
            $openerNode->setLiteral(\substr($openerNode->getLiteral(), 0, -$useDelims));
194 525
            $closerNode->setLiteral(\substr($closerNode->getLiteral(), 0, -$useDelims));
195
196 525
            $this->removeDelimitersBetween($opener, $closer);
197
            // The delimiter processor can re-parent the nodes between opener and closer,
198
            // so make sure they're contiguous already. Exclusive because we want to keep opener/closer themselves.
199 525
            AdjacentTextMerger::mergeTextNodesBetweenExclusive($openerNode, $closerNode);
200 525
            $delimiterProcessor->process($openerNode, $closerNode, $useDelims);
201
202
            // No delimiter characters left to process, so we can remove delimiter and the now empty node.
203 525
            if ($opener->getLength() === 0) {
204 498
                $this->removeDelimiterAndNode($opener);
205
            }
206
207
            // phpcs:disable SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed
208 525
            if ($closer->getLength() === 0) {
209 498
                $next = $closer->getNext();
210 498
                $this->removeDelimiterAndNode($closer);
211 498
                $closer = $next;
212
            }
213
        }
214
215
        // Remove all delimiters
216 2694
        $this->removeAll($stackBottom);
217 2694
    }
218
}
219