Completed
Push — master ( d4d1b7...0de0ea )
by Colin
01:02
created

DelimiterStack::searchByCharacter()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.0218

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 8
cts 9
cp 0.8889
rs 9.7
c 0
b 0
f 0
cc 4
nc 6
nop 1
crap 4.0218
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
    /** @var DelimiterInterface|null */
28
    private $top;
29
30 1023
    public function push(DelimiterInterface $newDelimiter): void
31
    {
32 1023
        $newDelimiter->setPrevious($this->top);
33
34 1023
        if ($this->top !== null) {
35 573
            $this->top->setNext($newDelimiter);
36
        }
37
38 1023
        $this->top = $newDelimiter;
39 1023
    }
40
41 2208
    private function findEarliest(?DelimiterInterface $stackBottom = null): ?DelimiterInterface
42
    {
43 2208
        $delimiter = $this->top;
44 2208
        while ($delimiter !== null && $delimiter->getPrevious() !== $stackBottom) {
45 531
            $delimiter = $delimiter->getPrevious();
46
        }
47
48 2208
        return $delimiter;
49
    }
50
51 1023
    public function removeDelimiter(DelimiterInterface $delimiter): void
52
    {
53 1023
        if ($delimiter->getPrevious() !== null) {
54 306
            $delimiter->getPrevious()->setNext($delimiter->getNext());
55
        }
56
57 1023
        if ($delimiter->getNext() === null) {
58
            // top of stack
59 1023
            $this->top = $delimiter->getPrevious();
60
        } else {
61 459
            $delimiter->getNext()->setPrevious($delimiter->getPrevious());
62
        }
63 1023
    }
64
65 468
    private function removeDelimiterAndNode(DelimiterInterface $delimiter): void
66
    {
67 468
        $delimiter->getInlineNode()->detach();
68 468
        $this->removeDelimiter($delimiter);
69 468
    }
70
71 468
    private function removeDelimitersBetween(DelimiterInterface $opener, DelimiterInterface $closer): void
72
    {
73 468
        $delimiter = $closer->getPrevious();
74 468
        while ($delimiter !== null && $delimiter !== $opener) {
75 24
            $previous = $delimiter->getPrevious();
76 24
            $this->removeDelimiter($delimiter);
77 24
            $delimiter = $previous;
78
        }
79 468
    }
80
81 2208
    public function removeAll(?DelimiterInterface $stackBottom = null): void
82
    {
83 2208
        while ($this->top && $this->top !== $stackBottom) {
84 516
            $this->removeDelimiter($this->top);
85
        }
86 2208
    }
87
88 306
    public function removeEarlierMatches(string $character): void
89
    {
90 306
        $opener = $this->top;
91 306
        while ($opener !== null) {
92 57
            if ($opener->getChar() === $character) {
93 24
                $opener->setActive(false);
94
            }
95
96 57
            $opener = $opener->getPrevious();
97
        }
98 306
    }
99
100
    /**
101
     * @param string|string[] $characters
102
     */
103 453
    public function searchByCharacter($characters): ?DelimiterInterface
104
    {
105 453
        if (! \is_array($characters)) {
106
            $characters = [$characters];
107
        }
108
109 453
        $opener = $this->top;
110 453
        while ($opener !== null) {
111 447
            if (\in_array($opener->getChar(), $characters, true)) {
112 444
                break;
113
            }
114
115 72
            $opener = $opener->getPrevious();
116
        }
117
118 453
        return $opener;
119
    }
120
121 2208
    public function processDelimiters(?DelimiterInterface $stackBottom, DelimiterProcessorCollection $processors): void
122
    {
123 2208
        $openersBottom = [];
124
125
        // Find first closer above stackBottom
126 2208
        $closer = $this->findEarliest($stackBottom);
127
128
        // Move forward, looking for closers, and handling each
129 2208
        while ($closer !== null) {
130 948
            $delimiterChar = $closer->getChar();
131
132 948
            $delimiterProcessor = $processors->getDelimiterProcessor($delimiterChar);
133 948
            if (! $closer->canClose() || $delimiterProcessor === null) {
134 861
                $closer = $closer->getNext();
135 861
                continue;
136
            }
137
138 564
            $openingDelimiterChar = $delimiterProcessor->getOpeningCharacter();
139
140 564
            $useDelims            = 0;
141 564
            $openerFound          = false;
142 564
            $potentialOpenerFound = false;
143 564
            $opener               = $closer->getPrevious();
144 564
            while ($opener !== null && $opener !== $stackBottom && $opener !== ($openersBottom[$delimiterChar] ?? null)) {
145 492
                if ($opener->canOpen() && $opener->getChar() === $openingDelimiterChar) {
146 471
                    $potentialOpenerFound = true;
147 471
                    $useDelims            = $delimiterProcessor->getDelimiterUse($opener, $closer);
148 471
                    if ($useDelims > 0) {
149 468
                        $openerFound = true;
150 468
                        break;
151
                    }
152
                }
153
154 57
                $opener = $opener->getPrevious();
155
            }
156
157 564
            if (! $openerFound) {
158 177
                if (! $potentialOpenerFound) {
159
                    // Only do this when we didn't even have a potential
160
                    // opener (one that matches the character and can open).
161
                    // If an opener was rejected because of the number of
162
                    // delimiters (e.g. because of the "multiple of 3"
163
                    // Set lower bound for future searches for openersrule),
164
                    // we want to consider it next time because the number
165
                    // of delimiters can change as we continue processing.
166 162
                    $openersBottom[$delimiterChar] = $closer->getPrevious();
167 162
                    if (! $closer->canOpen()) {
168
                        // We can remove a closer that can't be an opener,
169
                        // once we've seen there's no matching opener.
170 120
                        $this->removeDelimiter($closer);
171
                    }
172
                }
173
174 177
                $closer = $closer->getNext();
175 177
                continue;
176
            }
177
178 468
            $openerNode = $opener->getInlineNode();
179 468
            $closerNode = $closer->getInlineNode();
180
181
            // Remove number of used delimiters from stack and inline nodes.
182 468
            $opener->setLength($opener->getLength() - $useDelims);
183 468
            $closer->setLength($closer->getLength() - $useDelims);
184
185 468
            $openerNode->setLiteral(\substr($openerNode->getLiteral(), 0, -$useDelims));
186 468
            $closerNode->setLiteral(\substr($closerNode->getLiteral(), 0, -$useDelims));
187
188 468
            $this->removeDelimitersBetween($opener, $closer);
189
            // The delimiter processor can re-parent the nodes between opener and closer,
190
            // so make sure they're contiguous already. Exclusive because we want to keep opener/closer themselves.
191 468
            AdjacentTextMerger::mergeTextNodesBetweenExclusive($openerNode, $closerNode);
192 468
            $delimiterProcessor->process($openerNode, $closerNode, $useDelims);
193
194
            // No delimiter characters left to process, so we can remove delimiter and the now empty node.
195 468
            if ($opener->getLength() === 0) {
196 441
                $this->removeDelimiterAndNode($opener);
197
            }
198
199
            // phpcs:disable SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed
200 468
            if ($closer->getLength() === 0) {
201 441
                $next = $closer->getNext();
202 441
                $this->removeDelimiterAndNode($closer);
203 441
                $closer = $next;
204
            }
205
        }
206
207
        // Remove all delimiters
208 2208
        $this->removeAll($stackBottom);
209 2208
    }
210
}
211