1<?php
2
3declare(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
20namespace League\CommonMark\Delimiter;
21
22use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
23use League\CommonMark\Node\Inline\AdjacentTextMerger;
24
25final 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}
215