These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | /* |
||
4 | * This file is part of the league/commonmark package. |
||
5 | * |
||
6 | * (c) Colin O'Dell <[email protected]> |
||
7 | * |
||
8 | * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) |
||
9 | * - (c) John MacFarlane |
||
10 | * |
||
11 | * Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java) |
||
12 | * - (c) Atlassian Pty Ltd |
||
13 | * |
||
14 | * For the full copyright and license information, please view the LICENSE |
||
15 | * file that was distributed with this source code. |
||
16 | */ |
||
17 | |||
18 | namespace League\CommonMark\Delimiter; |
||
19 | |||
20 | use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection; |
||
21 | use League\CommonMark\Inline\AdjacentTextMerger; |
||
22 | |||
23 | final class DelimiterStack |
||
24 | { |
||
25 | /** |
||
26 | * @var DelimiterInterface|null |
||
27 | */ |
||
28 | private $top; |
||
29 | |||
30 | 990 | public function push(DelimiterInterface $newDelimiter) |
|
31 | { |
||
32 | 990 | $newDelimiter->setPrevious($this->top); |
|
33 | |||
34 | 990 | if ($this->top !== null) { |
|
35 | 597 | $this->top->setNext($newDelimiter); |
|
36 | } |
||
37 | |||
38 | 990 | $this->top = $newDelimiter; |
|
39 | 990 | } |
|
40 | |||
41 | /** |
||
42 | * @param DelimiterInterface|null $stackBottom |
||
43 | * |
||
44 | * @return DelimiterInterface|null |
||
45 | */ |
||
46 | 2019 | private function findEarliest(DelimiterInterface $stackBottom = null): ?DelimiterInterface |
|
47 | { |
||
48 | 2019 | $delimiter = $this->top; |
|
49 | 2019 | while ($delimiter !== null && $delimiter->getPrevious() !== $stackBottom) { |
|
50 | 555 | $delimiter = $delimiter->getPrevious(); |
|
51 | } |
||
52 | |||
53 | 2019 | return $delimiter; |
|
54 | } |
||
55 | |||
56 | /** |
||
57 | * @param DelimiterInterface $delimiter |
||
58 | */ |
||
59 | 990 | public function removeDelimiter(DelimiterInterface $delimiter) |
|
60 | { |
||
61 | 990 | if ($delimiter->getPrevious() !== null) { |
|
62 | 366 | $delimiter->getPrevious()->setNext($delimiter->getNext()); |
|
63 | } |
||
64 | |||
65 | 990 | if ($delimiter->getNext() === null) { |
|
66 | // top of stack |
||
67 | 990 | $this->top = $delimiter->getPrevious(); |
|
68 | } else { |
||
69 | 408 | $delimiter->getNext()->setPrevious($delimiter->getPrevious()); |
|
70 | } |
||
71 | 990 | } |
|
72 | |||
73 | 414 | private function removeDelimiterAndNode(DelimiterInterface $delimiter) |
|
74 | { |
||
75 | 414 | $delimiter->getInlineNode()->detach(); |
|
76 | 414 | $this->removeDelimiter($delimiter); |
|
77 | 414 | } |
|
78 | |||
79 | 414 | private function removeDelimitersBetween(DelimiterInterface $opener, DelimiterInterface $closer) |
|
80 | { |
||
81 | 414 | $delimiter = $closer->getPrevious(); |
|
82 | 414 | while ($delimiter !== null && $delimiter !== $opener) { |
|
83 | 27 | $previous = $delimiter->getPrevious(); |
|
84 | 27 | $this->removeDelimiter($delimiter); |
|
85 | 27 | $delimiter = $previous; |
|
86 | } |
||
87 | 414 | } |
|
88 | |||
89 | /** |
||
90 | * @param DelimiterInterface|null $stackBottom |
||
91 | */ |
||
92 | 2019 | public function removeAll(DelimiterInterface $stackBottom = null) |
|
93 | { |
||
94 | 2019 | while ($this->top && $this->top !== $stackBottom) { |
|
95 | 567 | $this->removeDelimiter($this->top); |
|
96 | } |
||
97 | 2019 | } |
|
98 | |||
99 | /** |
||
100 | * @param string $character |
||
101 | */ |
||
102 | 291 | public function removeEarlierMatches(string $character) |
|
103 | { |
||
104 | 291 | $opener = $this->top; |
|
105 | 291 | while ($opener !== null) { |
|
106 | 54 | if ($opener->getChar() === $character) { |
|
107 | 24 | $opener->setActive(false); |
|
108 | } |
||
109 | |||
110 | 54 | $opener = $opener->getPrevious(); |
|
111 | } |
||
112 | 291 | } |
|
113 | |||
114 | /** |
||
115 | * @param string|string[] $characters |
||
116 | * |
||
117 | * @return DelimiterInterface|null |
||
118 | */ |
||
119 | 432 | public function searchByCharacter($characters): ?DelimiterInterface |
|
120 | { |
||
121 | 432 | if (!\is_array($characters)) { |
|
122 | $characters = [$characters]; |
||
123 | } |
||
124 | |||
125 | 432 | $opener = $this->top; |
|
126 | 432 | while ($opener !== null) { |
|
127 | 426 | if (\in_array($opener->getChar(), $characters)) { |
|
128 | 423 | break; |
|
129 | } |
||
130 | 72 | $opener = $opener->getPrevious(); |
|
131 | } |
||
132 | |||
133 | 432 | return $opener; |
|
134 | } |
||
135 | |||
136 | 2019 | public function processDelimiters(?DelimiterInterface $stackBottom, DelimiterProcessorCollection $processors) |
|
137 | { |
||
138 | 2019 | $openersBottom = []; |
|
139 | |||
140 | // Find first closer above stackBottom |
||
141 | 2019 | $closer = $this->findEarliest($stackBottom); |
|
142 | |||
143 | // Move forward, looking for closers, and handling each |
||
144 | 2019 | while ($closer !== null) { |
|
145 | 918 | $delimiterChar = $closer->getChar(); |
|
146 | |||
147 | 918 | $delimiterProcessor = $processors->getDelimiterProcessor($delimiterChar); |
|
148 | 918 | if (!$closer->canClose() || $delimiterProcessor === null) { |
|
149 | 870 | $closer = $closer->getNext(); |
|
150 | 870 | continue; |
|
151 | } |
||
152 | |||
153 | 495 | $openingDelimiterChar = $delimiterProcessor->getOpeningCharacter(); |
|
154 | |||
155 | 495 | $useDelims = 0; |
|
156 | 495 | $openerFound = false; |
|
157 | 495 | $potentialOpenerFound = false; |
|
158 | 495 | $opener = $closer->getPrevious(); |
|
159 | 495 | while ($opener !== null && $opener !== $stackBottom && $opener !== ($openersBottom[$delimiterChar] ?? null)) { |
|
160 | 468 | if ($opener->canOpen() && $opener->getChar() === $openingDelimiterChar) { |
|
161 | 417 | $potentialOpenerFound = true; |
|
162 | 417 | $useDelims = $delimiterProcessor->getDelimiterUse($opener, $closer); |
|
163 | 417 | if ($useDelims > 0) { |
|
164 | 414 | $openerFound = true; |
|
165 | 414 | break; |
|
166 | } |
||
167 | } |
||
168 | 90 | $opener = $opener->getPrevious(); |
|
169 | } |
||
170 | |||
171 | 495 | if (!$openerFound) { |
|
172 | 153 | if (!$potentialOpenerFound) { |
|
173 | // Only do this when we didn't even have a potential |
||
174 | // opener (one that matches the character and can open). |
||
175 | // If an opener was rejected because of the number of |
||
176 | // delimiters (e.g. because of the "multiple of 3" |
||
177 | // Set lower bound for future searches for openersrule), |
||
178 | // we want to consider it next time because the number |
||
179 | // of delimiters can change as we continue processing. |
||
180 | 138 | $openersBottom[$delimiterChar] = $closer->getPrevious(); |
|
181 | 138 | if (!$closer->canOpen()) { |
|
182 | // We can remove a closer that can't be an opener, |
||
183 | // once we've seen there's no matching opener. |
||
184 | 96 | $this->removeDelimiter($closer); |
|
185 | } |
||
186 | } |
||
187 | 153 | $closer = $closer->getNext(); |
|
188 | 153 | continue; |
|
189 | } |
||
190 | |||
191 | 414 | $openerNode = $opener->getInlineNode(); |
|
192 | 414 | $closerNode = $closer->getInlineNode(); |
|
193 | |||
194 | // Remove number of used delimiters from stack and inline nodes. |
||
195 | 414 | $opener->setLength($opener->getLength() - $useDelims); |
|
196 | 414 | $closer->setLength($closer->getLength() - $useDelims); |
|
197 | |||
198 | 414 | $openerNode->setContent(\substr($openerNode->getContent(), 0, -$useDelims)); |
|
199 | 414 | $closerNode->setContent(\substr($closerNode->getContent(), 0, -$useDelims)); |
|
200 | |||
201 | 414 | $this->removeDelimitersBetween($opener, $closer); |
|
0 ignored issues
–
show
|
|||
202 | // The delimiter processor can re-parent the nodes between opener and closer, |
||
203 | // so make sure they're contiguous already. Exclusive because we want to keep opener/closer themselves. |
||
204 | 414 | AdjacentTextMerger::mergeTextNodesBetweenExclusive($openerNode, $closerNode); |
|
205 | 414 | $delimiterProcessor->process($openerNode, $closerNode, $useDelims); |
|
206 | |||
207 | // No delimiter characters left to process, so we can remove delimiter and the now empty node. |
||
208 | 414 | if ($opener->getLength() === 0) { |
|
209 | 390 | $this->removeDelimiterAndNode($opener); |
|
0 ignored issues
–
show
It seems like
$opener can be null ; however, removeDelimiterAndNode() does not accept null , maybe add an additional type check?
Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code: /** @return stdClass|null */
function mayReturnNull() { }
function doesNotAcceptNull(stdClass $x) { }
// With potential error.
function withoutCheck() {
$x = mayReturnNull();
doesNotAcceptNull($x); // Potential error here.
}
// Safe - Alternative 1
function withCheck1() {
$x = mayReturnNull();
if ( ! $x instanceof stdClass) {
throw new \LogicException('$x must be defined.');
}
doesNotAcceptNull($x);
}
// Safe - Alternative 2
function withCheck2() {
$x = mayReturnNull();
if ($x instanceof stdClass) {
doesNotAcceptNull($x);
}
}
Loading history...
|
|||
210 | } |
||
211 | |||
212 | 414 | if ($closer->getLength() === 0) { |
|
213 | 390 | $next = $closer->getNext(); |
|
214 | 390 | $this->removeDelimiterAndNode($closer); |
|
215 | 390 | $closer = $next; |
|
216 | } |
||
217 | } |
||
218 | |||
219 | // Remove all delimiters |
||
220 | 2019 | $this->removeAll($stackBottom); |
|
221 | 2019 | } |
|
222 | } |
||
223 |
Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code: