From 80e531f6ae133d9020e58fdafa6d7eed634e0473 Mon Sep 17 00:00:00 2001 From: lacatoire Date: Wed, 1 Apr 2026 11:06:30 +0200 Subject: [PATCH 1/2] Deprecate legacy tilde-to-NBSP behavior The tilde character (~) was converted to a non-breaking space, a behavior inherited from doctrine/rst-parser that does not match the reStructuredText specification or Sphinx behavior. Add a `disableLegacyTilde` flag to InlineLexer, passed through InlineParser, and enabled by default in the DI configuration. When enabled, ~ is treated as a regular character. The :nbsp: text role remains available for explicit non-breaking spaces. --- .../config/guides-restructured-text.php | 1 + .../RestructuredText/Parser/InlineLexer.php | 6 +++++- .../RestructuredText/Parser/InlineParser.php | 7 +++++-- .../tests/unit/Parser/InlineLexerTest.php | 18 ++++++++++++++++++ .../Functional/tests/nbsp-role/nbsp-role.html | 2 +- tests/Functional/tests/nbsp/nbsp.html | 2 +- 6 files changed, 31 insertions(+), 5 deletions(-) diff --git a/packages/guides-restructured-text/resources/config/guides-restructured-text.php b/packages/guides-restructured-text/resources/config/guides-restructured-text.php index 163ff6b40..2c230b695 100644 --- a/packages/guides-restructured-text/resources/config/guides-restructured-text.php +++ b/packages/guides-restructured-text/resources/config/guides-restructured-text.php @@ -373,6 +373,7 @@ ->set(DocumentRule::class) ->set(InlineParser::class) ->arg('$inlineRules', tagged_iterator('phpdoc.guides.parser.rst.inline_rule')) + ->arg('$disableLegacyTilde', true) ->set(GlobSearcher::class) ->set(ToctreeBuilder::class) ->set(InlineMarkupRule::class) diff --git a/packages/guides-restructured-text/src/RestructuredText/Parser/InlineLexer.php b/packages/guides-restructured-text/src/RestructuredText/Parser/InlineLexer.php index 93613c651..f905f6fb2 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Parser/InlineLexer.php +++ b/packages/guides-restructured-text/src/RestructuredText/Parser/InlineLexer.php @@ -34,6 +34,10 @@ /** @extends AbstractLexer */ final class InlineLexer extends AbstractLexer { + public function __construct(private readonly bool $disableLegacyTilde = false) + { + } + public const WORD = 1; public const UNDERSCORE = 2; public const ANONYMOUS_END = 3; @@ -131,7 +135,7 @@ protected function getType(string &$value) '#' => self::OCTOTHORPE, '[' => self::ANNOTATION_START, ']' => self::ANNOTATION_END, - '~' => self::NBSP, + '~' => $this->disableLegacyTilde ? null : self::NBSP, '\\' => self::BACKSLASH, default => null, }; diff --git a/packages/guides-restructured-text/src/RestructuredText/Parser/InlineParser.php b/packages/guides-restructured-text/src/RestructuredText/Parser/InlineParser.php index fcf4d8416..5ca03d54e 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Parser/InlineParser.php +++ b/packages/guides-restructured-text/src/RestructuredText/Parser/InlineParser.php @@ -33,7 +33,10 @@ class InlineParser private array $cache = []; /** @param iterable $inlineRules */ - public function __construct(iterable $inlineRules) + public function __construct( + iterable $inlineRules, + private readonly bool $disableLegacyTilde = false, + ) { $this->rules = array_filter([...$inlineRules], static fn ($rule) => $rule instanceof CachableInlineRule === false); usort($this->rules, static fn (InlineRule $a, InlineRule $b): int => $a->getPriority() > $b->getPriority() ? -1 : 1); @@ -48,7 +51,7 @@ public function __construct(iterable $inlineRules) public function parse(string $content, BlockContext $blockContext): InlineCompoundNode { - $lexer = new InlineLexer(); + $lexer = new InlineLexer($this->disableLegacyTilde); $lexer->setInput($content); $lexer->moveNext(); $lexer->moveNext(); diff --git a/packages/guides-restructured-text/tests/unit/Parser/InlineLexerTest.php b/packages/guides-restructured-text/tests/unit/Parser/InlineLexerTest.php index 4a8f7426f..3b70faab1 100644 --- a/packages/guides-restructured-text/tests/unit/Parser/InlineLexerTest.php +++ b/packages/guides-restructured-text/tests/unit/Parser/InlineLexerTest.php @@ -34,6 +34,24 @@ public function testLexer(string $input, array $result): void } } + public function testTildeIsNbspByDefault(): void + { + $lexer = new InlineLexer(); + $lexer->setInput('~'); + $lexer->moveNext(); + $lexer->moveNext(); + assertEquals(InlineLexer::NBSP, $lexer->token?->type); + } + + public function testTildeIsWordWhenLegacyTildeDisabled(): void + { + $lexer = new InlineLexer(disableLegacyTilde: true); + $lexer->setInput('~'); + $lexer->moveNext(); + $lexer->moveNext(); + assertEquals(InlineLexer::WORD, $lexer->token?->type); + } + /** @return array> */ public static function inlineLexerProvider(): array { diff --git a/tests/Functional/tests/nbsp-role/nbsp-role.html b/tests/Functional/tests/nbsp-role/nbsp-role.html index d25d8b5b1..928e732de 100644 --- a/tests/Functional/tests/nbsp-role/nbsp-role.html +++ b/tests/Functional/tests/nbsp-role/nbsp-role.html @@ -1,2 +1,2 @@ -

This is a non breakable space: a b

+

This is a non breakable space: a~b

This is also a non breakable space: a   b

diff --git a/tests/Functional/tests/nbsp/nbsp.html b/tests/Functional/tests/nbsp/nbsp.html index ed2e611dc..532af81c2 100644 --- a/tests/Functional/tests/nbsp/nbsp.html +++ b/tests/Functional/tests/nbsp/nbsp.html @@ -1 +1 @@ -

This is a non breakable space: a b

+

This is a non breakable space: a~b

From 81ad5ac02a2cdf5c3a4bd7677a311f47b9bdab5d Mon Sep 17 00:00:00 2001 From: lacatoire Date: Thu, 2 Apr 2026 08:32:43 +0200 Subject: [PATCH 2/2] Default disableLegacyTilde to false to preserve backward compatibility The legacy tilde-to-NBSP behavior is now preserved by default. Users who want the new behavior can explicitly set disableLegacyTilde to true. --- .../resources/config/guides-restructured-text.php | 2 +- .../src/RestructuredText/Parser/InlineParser.php | 3 +-- tests/Functional/tests/nbsp-role/nbsp-role.html | 2 +- tests/Functional/tests/nbsp/nbsp.html | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/guides-restructured-text/resources/config/guides-restructured-text.php b/packages/guides-restructured-text/resources/config/guides-restructured-text.php index 2c230b695..7491b6144 100644 --- a/packages/guides-restructured-text/resources/config/guides-restructured-text.php +++ b/packages/guides-restructured-text/resources/config/guides-restructured-text.php @@ -373,7 +373,7 @@ ->set(DocumentRule::class) ->set(InlineParser::class) ->arg('$inlineRules', tagged_iterator('phpdoc.guides.parser.rst.inline_rule')) - ->arg('$disableLegacyTilde', true) + ->arg('$disableLegacyTilde', false) ->set(GlobSearcher::class) ->set(ToctreeBuilder::class) ->set(InlineMarkupRule::class) diff --git a/packages/guides-restructured-text/src/RestructuredText/Parser/InlineParser.php b/packages/guides-restructured-text/src/RestructuredText/Parser/InlineParser.php index 5ca03d54e..41fae968f 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Parser/InlineParser.php +++ b/packages/guides-restructured-text/src/RestructuredText/Parser/InlineParser.php @@ -36,8 +36,7 @@ class InlineParser public function __construct( iterable $inlineRules, private readonly bool $disableLegacyTilde = false, - ) - { + ) { $this->rules = array_filter([...$inlineRules], static fn ($rule) => $rule instanceof CachableInlineRule === false); usort($this->rules, static fn (InlineRule $a, InlineRule $b): int => $a->getPriority() > $b->getPriority() ? -1 : 1); foreach ($inlineRules as $rule) { diff --git a/tests/Functional/tests/nbsp-role/nbsp-role.html b/tests/Functional/tests/nbsp-role/nbsp-role.html index 928e732de..d25d8b5b1 100644 --- a/tests/Functional/tests/nbsp-role/nbsp-role.html +++ b/tests/Functional/tests/nbsp-role/nbsp-role.html @@ -1,2 +1,2 @@ -

This is a non breakable space: a~b

+

This is a non breakable space: a b

This is also a non breakable space: a   b

diff --git a/tests/Functional/tests/nbsp/nbsp.html b/tests/Functional/tests/nbsp/nbsp.html index 532af81c2..ed2e611dc 100644 --- a/tests/Functional/tests/nbsp/nbsp.html +++ b/tests/Functional/tests/nbsp/nbsp.html @@ -1 +1 @@ -

This is a non breakable space: a~b

+

This is a non breakable space: a b