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