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