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