Skip to content

Commit a8df15c

Browse files
committed
Use a separate stack for brackets
See commonmark/commonmark.js#101
1 parent af0d839 commit a8df15c

File tree

8 files changed

+271
-29
lines changed

8 files changed

+271
-29
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi
1111
- Added `RegexHelper::isWhitespace()` method to check if a given character is an ASCII whitespace character
1212
- Added `CacheableDelimiterProcessorInterface` to ensure linear complexity for dynamic delimiter processing
1313
- Added `table/max_autocompleted_cells` config option to prevent denial of service attacks when parsing large tables
14+
- Added `Bracket` delimiter type to optimize bracket parsing
1415

1516
### Changed
1617

18+
- `[` and `]` are no longer added as `Delimiter` objects on the stack; a new `Bracket` type with its own stack is used instead
1719
- `UrlAutolinkParser` no longer parses URLs with more than 127 subdomains
1820
- Expanded reference links can no longer exceed 100kb, or the size of the input document (whichever is greater)
1921
- Several small performance optimizations
@@ -93,6 +95,8 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi
9395

9496
- Returning dynamic values from `DelimiterProcessorInterface::getDelimiterUse()` is deprecated
9597
- You should instead implement `CacheableDelimiterProcessorInterface` to help the engine perform caching to avoid performance issues.
98+
- Deprecated `DelimiterInterface::isActive()` and `DelimiterInterface::setActive()`, as these are no longer used by the engine
99+
- Deprecated `DelimiterStack::removeEarlierMatches()` and `DelimiterStack::searchByCharacter()`, as these are no longer used by the engine
96100

97101
### Fixed
98102

src/Delimiter/Bracket.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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 <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace League\CommonMark\Delimiter;
15+
16+
use League\CommonMark\Node\Node;
17+
18+
final class Bracket
19+
{
20+
private Node $node;
21+
private ?Bracket $previous;
22+
private ?DelimiterInterface $previousDelimiter;
23+
private bool $hasNext = false;
24+
private int $index;
25+
private bool $image;
26+
private bool $active = true;
27+
28+
public function __construct(Node $node, ?Bracket $previous, ?DelimiterInterface $previousDelimiter, int $index, bool $image)
29+
{
30+
$this->node = $node;
31+
$this->previous = $previous;
32+
$this->previousDelimiter = $previousDelimiter;
33+
$this->index = $index;
34+
$this->image = $image;
35+
}
36+
37+
public function getNode(): Node
38+
{
39+
return $this->node;
40+
}
41+
42+
public function getPrevious(): ?Bracket
43+
{
44+
return $this->previous;
45+
}
46+
47+
public function getPreviousDelimiter(): ?DelimiterInterface
48+
{
49+
return $this->previousDelimiter;
50+
}
51+
52+
public function hasNext(): bool
53+
{
54+
return $this->hasNext;
55+
}
56+
57+
public function getIndex(): int
58+
{
59+
return $this->index;
60+
}
61+
62+
public function isImage(): bool
63+
{
64+
return $this->image;
65+
}
66+
67+
public function isActive(): bool
68+
{
69+
return $this->active;
70+
}
71+
72+
/**
73+
* @internal
74+
*/
75+
public function setHasNext(bool $hasNext): void
76+
{
77+
$this->hasNext = $hasNext;
78+
}
79+
80+
/**
81+
* @internal
82+
*/
83+
public function setActive(bool $active): void
84+
{
85+
$this->active = $active;
86+
}
87+
}

src/Delimiter/DelimiterInterface.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,14 @@ public function canClose(): bool;
2424

2525
public function canOpen(): bool;
2626

27+
/**
28+
* @deprecated This method is no longer used internally and will be removed in 3.0
29+
*/
2730
public function isActive(): bool;
2831

32+
/**
33+
* @deprecated This method is no longer used internally and will be removed in 3.0
34+
*/
2935
public function setActive(bool $active): void;
3036

3137
public function getChar(): string;

src/Delimiter/DelimiterStack.php

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,16 @@
2222
use League\CommonMark\Delimiter\Processor\CacheableDelimiterProcessorInterface;
2323
use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
2424
use League\CommonMark\Node\Inline\AdjacentTextMerger;
25+
use League\CommonMark\Node\Node;
2526

2627
final class DelimiterStack
2728
{
2829
/** @psalm-readonly-allow-private-mutation */
2930
private ?DelimiterInterface $top = null;
3031

32+
/** @psalm-readonly-allow-private-mutation */
33+
private ?Bracket $brackets = null;
34+
3135
public function push(DelimiterInterface $newDelimiter): void
3236
{
3337
$newDelimiter->setPrevious($this->top);
@@ -39,6 +43,26 @@ public function push(DelimiterInterface $newDelimiter): void
3943
$this->top = $newDelimiter;
4044
}
4145

46+
/**
47+
* @internal
48+
*/
49+
public function addBracket(Node $node, int $index, bool $image): void
50+
{
51+
if ($this->brackets !== null) {
52+
$this->brackets->setHasNext(true);
53+
}
54+
55+
$this->brackets = new Bracket($node, $this->brackets, $this->top, $index, $image);
56+
}
57+
58+
/**
59+
* @psalm-immutable
60+
*/
61+
public function getLastBracket(): ?Bracket
62+
{
63+
return $this->brackets;
64+
}
65+
4266
private function findEarliest(?DelimiterInterface $stackBottom = null): ?DelimiterInterface
4367
{
4468
$delimiter = $this->top;
@@ -49,6 +73,22 @@ private function findEarliest(?DelimiterInterface $stackBottom = null): ?Delimit
4973
return $delimiter;
5074
}
5175

76+
/**
77+
* @internal
78+
*/
79+
public function removeBracket(): void
80+
{
81+
if ($this->brackets === null) {
82+
return;
83+
}
84+
85+
$this->brackets = $this->brackets->getPrevious();
86+
87+
if ($this->brackets !== null) {
88+
$this->brackets->setHasNext(false);
89+
}
90+
}
91+
5292
public function removeDelimiter(DelimiterInterface $delimiter): void
5393
{
5494
if ($delimiter->getPrevious() !== null) {
@@ -98,6 +138,9 @@ public function removeAll(?DelimiterInterface $stackBottom = null): void
98138
}
99139
}
100140

141+
/**
142+
* @deprecated This method is no longer used internally and will be removed in 3.0
143+
*/
101144
public function removeEarlierMatches(string $character): void
102145
{
103146
$opener = $this->top;
@@ -111,6 +154,22 @@ public function removeEarlierMatches(string $character): void
111154
}
112155

113156
/**
157+
* @internal
158+
*/
159+
public function deactivateLinkOpeners(): void
160+
{
161+
$opener = $this->brackets;
162+
while ($opener !== null) {
163+
if (! $opener->isImage()) {
164+
$opener->setActive(false);
165+
}
166+
$opener = $opener->getPrevious();
167+
}
168+
}
169+
170+
/**
171+
* @deprecated This method is no longer used internally and will be removed in 3.0
172+
*
114173
* @param string|string[] $characters
115174
*/
116175
public function searchByCharacter($characters): ?DelimiterInterface
@@ -233,6 +292,12 @@ public function processDelimiters(?DelimiterInterface $stackBottom, DelimiterPro
233292
*/
234293
public function __destruct()
235294
{
236-
$this->removeAll();
295+
while ($this->top) {
296+
$this->removeDelimiter($this->top);
297+
}
298+
299+
while ($this->brackets) {
300+
$this->removeBracket();
301+
}
237302
}
238303
}

src/Extension/CommonMark/Parser/Inline/BangParser.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
namespace League\CommonMark\Extension\CommonMark\Parser\Inline;
1818

19-
use League\CommonMark\Delimiter\Delimiter;
2019
use League\CommonMark\Node\Inline\Text;
2120
use League\CommonMark\Parser\Inline\InlineParserInterface;
2221
use League\CommonMark\Parser\Inline\InlineParserMatch;
@@ -38,8 +37,7 @@ public function parse(InlineParserContext $inlineContext): bool
3837
$inlineContext->getContainer()->appendChild($node);
3938

4039
// Add entry to stack for this opener
41-
$delimiter = new Delimiter('!', 1, $node, true, false, $cursor->getPosition());
42-
$inlineContext->getDelimiterStack()->push($delimiter);
40+
$inlineContext->getDelimiterStack()->addBracket($node, $cursor->getPosition(), true);
4341

4442
return true;
4543
}

src/Extension/CommonMark/Parser/Inline/CloseBracketParser.php

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
namespace League\CommonMark\Extension\CommonMark\Parser\Inline;
1818

19+
use League\CommonMark\Delimiter\Bracket;
1920
use League\CommonMark\Environment\EnvironmentAwareInterface;
2021
use League\CommonMark\Environment\EnvironmentInterface;
2122
use League\CommonMark\Extension\CommonMark\Node\Inline\AbstractWebResource;
@@ -46,14 +47,14 @@ public function getMatchDefinition(): InlineParserMatch
4647
public function parse(InlineParserContext $inlineContext): bool
4748
{
4849
// Look through stack of delimiters for a [ or !
49-
$opener = $inlineContext->getDelimiterStack()->searchByCharacter(['[', '!']);
50+
$opener = $inlineContext->getDelimiterStack()->getLastBracket();
5051
if ($opener === null) {
5152
return false;
5253
}
5354

5455
if (! $opener->isActive()) {
55-
// no matched opener; remove from emphasis stack
56-
$inlineContext->getDelimiterStack()->removeDelimiter($opener);
56+
// no matched opener; remove from stack
57+
$inlineContext->getDelimiterStack()->removeBracket();
5758

5859
return false;
5960
}
@@ -70,21 +71,19 @@ public function parse(InlineParserContext $inlineContext): bool
7071
// Inline link?
7172
if ($result = $this->tryParseInlineLinkAndTitle($cursor)) {
7273
$link = $result;
73-
} elseif ($link = $this->tryParseReference($cursor, $inlineContext->getReferenceMap(), $opener->getIndex(), $startPos)) {
74+
} elseif ($link = $this->tryParseReference($cursor, $inlineContext->getReferenceMap(), $opener, $startPos)) {
7475
$reference = $link;
7576
$link = ['url' => $link->getDestination(), 'title' => $link->getTitle()];
7677
} else {
77-
// No match
78-
$inlineContext->getDelimiterStack()->removeDelimiter($opener); // Remove this opener from stack
78+
// No match; remove this opener from stack
79+
$inlineContext->getDelimiterStack()->removeBracket();
7980
$cursor->restoreState($previousState);
8081

8182
return false;
8283
}
8384

84-
$isImage = $opener->getChar() === '!';
85-
86-
$inline = $this->createInline($link['url'], $link['title'], $isImage, $reference ?? null);
87-
$opener->getInlineNode()->replaceWith($inline);
85+
$inline = $this->createInline($link['url'], $link['title'], $opener->isImage(), $reference ?? null);
86+
$opener->getNode()->replaceWith($inline);
8887
while (($label = $inline->next()) !== null) {
8988
// Is there a Mention or Link contained within this link?
9089
// CommonMark does not allow nested links, so we'll restore the original text.
@@ -104,17 +103,18 @@ public function parse(InlineParserContext $inlineContext): bool
104103

105104
// Process delimiters such as emphasis inside link/image
106105
$delimiterStack = $inlineContext->getDelimiterStack();
107-
$stackBottom = $opener->getPrevious();
106+
$stackBottom = $opener->getPreviousDelimiter();
108107
$delimiterStack->processDelimiters($stackBottom, $this->environment->getDelimiterProcessors());
108+
$delimiterStack->removeBracket();
109109
$delimiterStack->removeAll($stackBottom);
110110

111111
// Merge any adjacent Text nodes together
112112
AdjacentTextMerger::mergeChildNodes($inline);
113113

114114
// processEmphasis will remove this and later delimiters.
115115
// Now, for a link, we also remove earlier link openers (no links in links)
116-
if (! $isImage) {
117-
$inlineContext->getDelimiterStack()->removeEarlierMatches('[');
116+
if (! $opener->isImage()) {
117+
$inlineContext->getDelimiterStack()->deactivateLinkOpeners();
118118
}
119119

120120
return true;
@@ -168,21 +168,23 @@ private function tryParseInlineLinkAndTitle(Cursor $cursor): ?array
168168
return ['url' => $dest, 'title' => $title];
169169
}
170170

171-
private function tryParseReference(Cursor $cursor, ReferenceMapInterface $referenceMap, ?int $openerIndex, int $startPos): ?ReferenceInterface
171+
private function tryParseReference(Cursor $cursor, ReferenceMapInterface $referenceMap, Bracket $opener, int $startPos): ?ReferenceInterface
172172
{
173-
if ($openerIndex === null) {
174-
return null;
175-
}
176-
177173
$savePos = $cursor->saveState();
178174
$beforeLabel = $cursor->getPosition();
179175
$n = LinkParserHelper::parseLinkLabel($cursor);
180-
if ($n === 0 || $n === 2) {
181-
$start = $openerIndex;
182-
$length = $startPos - $openerIndex;
183-
} else {
176+
if ($n > 2) {
184177
$start = $beforeLabel + 1;
185178
$length = $n - 2;
179+
} elseif (! $opener->hasNext()) {
180+
// Empty or missing second label means to use the first label as the reference.
181+
// The reference must not contain a bracket. If we know there's a bracket, we don't even bother checking it.
182+
$start = $opener->getIndex();
183+
$length = $startPos - $start;
184+
} else {
185+
$cursor->restoreState($savePos);
186+
187+
return null;
186188
}
187189

188190
$referenceLabel = $cursor->getSubstring($start, $length);

src/Extension/CommonMark/Parser/Inline/OpenBracketParser.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
namespace League\CommonMark\Extension\CommonMark\Parser\Inline;
1818

19-
use League\CommonMark\Delimiter\Delimiter;
2019
use League\CommonMark\Node\Inline\Text;
2120
use League\CommonMark\Parser\Inline\InlineParserInterface;
2221
use League\CommonMark\Parser\Inline\InlineParserMatch;
@@ -36,8 +35,7 @@ public function parse(InlineParserContext $inlineContext): bool
3635
$inlineContext->getContainer()->appendChild($node);
3736

3837
// Add entry to stack for this opener
39-
$delimiter = new Delimiter('[', 1, $node, true, false, $inlineContext->getCursor()->getPosition());
40-
$inlineContext->getDelimiterStack()->push($delimiter);
38+
$inlineContext->getDelimiterStack()->addBracket($node, $inlineContext->getCursor()->getPosition(), false);
4139

4240
return true;
4341
}

0 commit comments

Comments
 (0)