From 5119ca5233a469db296518199a83b3d1c2beeb03 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sun, 18 Oct 2026 12:18:46 +0200 Subject: [PATCH 1/2] Fix bracket scanning across link destinations --- src/Parser/InlineParser.php | 8 ++++++++ tests/TestCase/DjotConverterTest.php | 15 +++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/Parser/InlineParser.php b/src/Parser/InlineParser.php index a85f043..14d97d5 100644 --- a/src/Parser/InlineParser.php +++ b/src/Parser/InlineParser.php @@ -1159,6 +1159,14 @@ protected function findBalancedBracketEnd(string $text, int $open): ?int if ($starts === []) { return $at; } + if ($at + 1 < $length && $text[$at + 1] === '(') { + $destinationEnd = $this->cachedLinkDestinationEnd($text, $at + 1); + if ($destinationEnd !== null) { + $at = $destinationEnd; + + continue; + } + } } $at++; } diff --git a/tests/TestCase/DjotConverterTest.php b/tests/TestCase/DjotConverterTest.php index 0f9b962..d644f67 100644 --- a/tests/TestCase/DjotConverterTest.php +++ b/tests/TestCase/DjotConverterTest.php @@ -2345,6 +2345,21 @@ public function testTripleBackticksInline(): void // Edge cases: Links and URLs + #[DataProvider('bracketInDestinationCases')] + public function testBracketInDestinationDoesNotCloseOuterText(string $djot, string $expected): void + { + $this->assertSame($expected, $this->converter->convert($djot)); + } + + public static function bracketInDestinationCases(): array + { + return [ + ['a [b [x](http://u/]) c', "

a [b x c

\n"], + ['a [b [x](http://u/[a]) c', "

a [b x c

\n"], + ['a [b ![x](http://u/]) c', "

a [b \"x\" c

\n"], + ]; + } + public function testLinkWithParenthesesInUrl(): void { $djot = '[Wikipedia](https://en.wikipedia.org/wiki/Example_(disambiguation))'; From c97c8e9138b854c60291335131fb3c0719daa420 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sun, 18 Oct 2026 12:19:59 +0200 Subject: [PATCH 2/2] Keep bracketed spans and code opaque to outer links --- src/Parser/InlineParser.php | 17 ++++++++++++++++- tests/TestCase/DjotConverterTest.php | 11 ++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Parser/InlineParser.php b/src/Parser/InlineParser.php index 14d97d5..c15e281 100644 --- a/src/Parser/InlineParser.php +++ b/src/Parser/InlineParser.php @@ -1141,7 +1141,7 @@ protected function findBalancedBracketEnd(string $text, int $open): ?int $length = strlen($text); $at = $open + 1; while ($at < $length) { - $at += strcspn($text, '[]\\', $at); + $at += strcspn($text, '[]\\`', $at); if ($at >= $length) { break; } @@ -1151,6 +1151,14 @@ protected function findBalancedBracketEnd(string $text, int $open): ?int continue; } + if ($char === '`') { + $codeEnd = $this->findCodeSpanEnd($text, $at); + if ($codeEnd !== null) { + $at = $codeEnd; + + continue; + } + } if ($char === '[') { $starts[] = $at; } elseif ($char === ']') { @@ -1164,6 +1172,13 @@ protected function findBalancedBracketEnd(string $text, int $open): ?int if ($destinationEnd !== null) { $at = $destinationEnd; + continue; + } + } elseif ($at + 1 < $length && $text[$at + 1] === '{') { + $attributeEnd = $this->findAttributeEnd($text, $at + 1); + if ($attributeEnd !== null && $this->isValidAttrPayload(substr($text, $at + 2, $attributeEnd - $at - 2))) { + $at = $attributeEnd + 1; + continue; } } diff --git a/tests/TestCase/DjotConverterTest.php b/tests/TestCase/DjotConverterTest.php index d644f67..96ad084 100644 --- a/tests/TestCase/DjotConverterTest.php +++ b/tests/TestCase/DjotConverterTest.php @@ -2345,18 +2345,23 @@ public function testTripleBackticksInline(): void // Edge cases: Links and URLs - #[DataProvider('bracketInDestinationCases')] - public function testBracketInDestinationDoesNotCloseOuterText(string $djot, string $expected): void + #[DataProvider('opaqueBracketContentCases')] + public function testOpaqueContentDoesNotCloseOuterBrackets(string $djot, string $expected): void { $this->assertSame($expected, $this->converter->convert($djot)); } - public static function bracketInDestinationCases(): array + public static function opaqueBracketContentCases(): array { return [ ['a [b [x](http://u/]) c', "

a [b x c

\n"], ['a [b [x](http://u/[a]) c', "

a [b x c

\n"], ['a [b ![x](http://u/]) c', "

a [b \"x\" c

\n"], + ['a [b [x](http://u/]) c](v)', "

a b x c

\n"], + ['[a [x](u\\]) c](v)', "

a x c

\n"], + ['[a [x]{title="]"} c](v)', "

a x c

\n"], + ['[a `]` b](v)', "

a ] b

\n"], + ['[a `[x]` b](v)', "

a [x] b

\n"], ]; }