diff --git a/src/Rules/EscapeRule.php b/src/Rules/EscapeRule.php new file mode 100644 index 0000000..cdbb764 --- /dev/null +++ b/src/Rules/EscapeRule.php @@ -0,0 +1,51 @@ +?@[\\]^_`{|}~'; + + private(set) string $firstChar = '\\'; + private(set) string $stopChar = '\\'; + + public function shouldParse(Parser $parser): bool + { + $next = $parser->content[$parser->position + 1] ?? ''; + + return $next !== '' && str_contains(self::PUNCTUATION, $next); + } + + public function parse(Parser $parser): ?Token + { + $parser->consume(); + $char = $parser->consume(); + + // Written as an entity before a line ending, an escaped backslash + // cannot be read back as a hard line break by TextToken. + $text = $char === '\\' + && str_contains(Parser::NEW_LINE, $parser->current ?? ' ') + ? '\' + : htmlspecialchars($char, ENT_COMPAT | ENT_HTML5); + + if ($parser->lastToken instanceof TextToken) { + $parser->lastToken->append($text); + + return null; + } + + return new TextToken($text); + } +} diff --git a/src/Tokens/BoldAndItalicToken.php b/src/Tokens/BoldAndItalicToken.php index 271ee18..47e89c2 100644 --- a/src/Tokens/BoldAndItalicToken.php +++ b/src/Tokens/BoldAndItalicToken.php @@ -4,6 +4,7 @@ use Tempest\Markdown\Parser; use Tempest\Markdown\Rules\CodeRule; +use Tempest\Markdown\Rules\EscapeRule; use Tempest\Markdown\Rules\ImageRule; use Tempest\Markdown\Rules\LinkRule; use Tempest\Markdown\Rules\SocialHandleRule; @@ -26,6 +27,7 @@ public function parse(Parser $parser): string new LinkRule(), new SocialHandleRule(), new ImageRule(), + new EscapeRule(), new TextRule(), ]) ->parse($this->content); diff --git a/src/Tokens/BoldToken.php b/src/Tokens/BoldToken.php index cd2a8a0..42cf845 100644 --- a/src/Tokens/BoldToken.php +++ b/src/Tokens/BoldToken.php @@ -4,6 +4,7 @@ use Tempest\Markdown\Parser; use Tempest\Markdown\Rules\CodeRule; +use Tempest\Markdown\Rules\EscapeRule; use Tempest\Markdown\Rules\ImageRule; use Tempest\Markdown\Rules\ItalicRule; use Tempest\Markdown\Rules\LinkRule; @@ -28,6 +29,7 @@ public function parse(Parser $parser): string new LinkRule(), new SocialHandleRule(), new ImageRule(), + new EscapeRule(), new TextRule(), ]) ->parse($this->content); diff --git a/src/Tokens/DivToken.php b/src/Tokens/DivToken.php index 0ea7470..6dcb2de 100644 --- a/src/Tokens/DivToken.php +++ b/src/Tokens/DivToken.php @@ -6,6 +6,7 @@ use Tempest\Markdown\Rules\BoldAndItalicRule; use Tempest\Markdown\Rules\BoldRule; use Tempest\Markdown\Rules\CodeRule; +use Tempest\Markdown\Rules\EscapeRule; use Tempest\Markdown\Rules\HeadingRule; use Tempest\Markdown\Rules\ImageRule; use Tempest\Markdown\Rules\ItalicRule; @@ -39,6 +40,7 @@ public function parse(Parser $parser): string new ImageRule(), new PreRule(), new CodeRule(), + new EscapeRule(), new TextRule(), ]) ->parse($this->content); diff --git a/src/Tokens/HeadingToken.php b/src/Tokens/HeadingToken.php index 385f3b2..6ff38ac 100644 --- a/src/Tokens/HeadingToken.php +++ b/src/Tokens/HeadingToken.php @@ -6,6 +6,7 @@ use Tempest\Markdown\Rules\BoldAndItalicRule; use Tempest\Markdown\Rules\BoldRule; use Tempest\Markdown\Rules\CodeRule; +use Tempest\Markdown\Rules\EscapeRule; use Tempest\Markdown\Rules\ImageRule; use Tempest\Markdown\Rules\ItalicRule; use Tempest\Markdown\Rules\LinkRule; @@ -42,6 +43,7 @@ public function parse(Parser $parser): string new SocialHandleRule(), new ImageRule(), new CodeRule(), + new EscapeRule(), new TextRule(), ]) ->parse($this->content); diff --git a/src/Tokens/HtmlToken.php b/src/Tokens/HtmlToken.php index 5663e04..b7a892f 100644 --- a/src/Tokens/HtmlToken.php +++ b/src/Tokens/HtmlToken.php @@ -6,6 +6,7 @@ use Tempest\Markdown\Rules\BoldAndItalicRule; use Tempest\Markdown\Rules\BoldRule; use Tempest\Markdown\Rules\CodeRule; +use Tempest\Markdown\Rules\EscapeRule; use Tempest\Markdown\Rules\ImageRule; use Tempest\Markdown\Rules\ItalicRule; use Tempest\Markdown\Rules\LinkRule; @@ -39,6 +40,7 @@ public function parse(Parser $parser): string new SocialHandleRule(), new ImageRule(), new CodeRule(), + new EscapeRule(), new TextRule(), ]) ->parse($this->html) diff --git a/src/Tokens/ItalicToken.php b/src/Tokens/ItalicToken.php index dceeb9e..31ade66 100644 --- a/src/Tokens/ItalicToken.php +++ b/src/Tokens/ItalicToken.php @@ -5,6 +5,7 @@ use Tempest\Markdown\Parser; use Tempest\Markdown\Rules\BoldRule; use Tempest\Markdown\Rules\CodeRule; +use Tempest\Markdown\Rules\EscapeRule; use Tempest\Markdown\Rules\ImageRule; use Tempest\Markdown\Rules\LinkRule; use Tempest\Markdown\Rules\SocialHandleRule; @@ -28,6 +29,7 @@ public function parse(Parser $parser): string new LinkRule(), new SocialHandleRule(), new ImageRule(), + new EscapeRule(), new TextRule(), ]) ->parse($this->content); diff --git a/src/Tokens/LinkToken.php b/src/Tokens/LinkToken.php index 7e8c856..d7b2e87 100644 --- a/src/Tokens/LinkToken.php +++ b/src/Tokens/LinkToken.php @@ -6,6 +6,7 @@ use Tempest\Markdown\Rules\BoldAndItalicRule; use Tempest\Markdown\Rules\BoldRule; use Tempest\Markdown\Rules\CodeRule; +use Tempest\Markdown\Rules\EscapeRule; use Tempest\Markdown\Rules\ImageRule; use Tempest\Markdown\Rules\ItalicRule; use Tempest\Markdown\Rules\StrikethroughRule; @@ -35,6 +36,7 @@ public function parse(Parser $parser): string new ItalicRule(), new StrikethroughRule(), new ImageRule(), + new EscapeRule(), new TextRule(), ]) ->parse($this->content); diff --git a/src/Tokens/ListToken.php b/src/Tokens/ListToken.php index e228ed3..0fabb41 100644 --- a/src/Tokens/ListToken.php +++ b/src/Tokens/ListToken.php @@ -6,6 +6,7 @@ use Tempest\Markdown\Rules\BoldAndItalicRule; use Tempest\Markdown\Rules\BoldRule; use Tempest\Markdown\Rules\CodeRule; +use Tempest\Markdown\Rules\EscapeRule; use Tempest\Markdown\Rules\ImageRule; use Tempest\Markdown\Rules\ItalicRule; use Tempest\Markdown\Rules\LinkRule; @@ -32,6 +33,7 @@ public function parse(Parser $parser): string new SocialHandleRule(), new ImageRule(), new CodeRule(), + new EscapeRule(), new TextRule(), ]); diff --git a/src/Tokens/OrderedListToken.php b/src/Tokens/OrderedListToken.php index d588b46..361a901 100644 --- a/src/Tokens/OrderedListToken.php +++ b/src/Tokens/OrderedListToken.php @@ -6,6 +6,7 @@ use Tempest\Markdown\Rules\BoldAndItalicRule; use Tempest\Markdown\Rules\BoldRule; use Tempest\Markdown\Rules\CodeRule; +use Tempest\Markdown\Rules\EscapeRule; use Tempest\Markdown\Rules\ImageRule; use Tempest\Markdown\Rules\ItalicRule; use Tempest\Markdown\Rules\LinkRule; @@ -35,6 +36,7 @@ public function parse(Parser $parser): string new SocialHandleRule(), new ImageRule(), new CodeRule(), + new EscapeRule(), new TextRule(), ]); diff --git a/src/Tokens/ParagraphToken.php b/src/Tokens/ParagraphToken.php index e7af088..a344d76 100644 --- a/src/Tokens/ParagraphToken.php +++ b/src/Tokens/ParagraphToken.php @@ -7,6 +7,7 @@ use Tempest\Markdown\Rules\BoldRule; use Tempest\Markdown\Rules\CodeRule; use Tempest\Markdown\Rules\DivRule; +use Tempest\Markdown\Rules\EscapeRule; use Tempest\Markdown\Rules\ImageRule; use Tempest\Markdown\Rules\ItalicRule; use Tempest\Markdown\Rules\LinkRule; @@ -36,6 +37,7 @@ public function parse(Parser $parser): string new PreRule(), new CodeRule(), new DivRule(), + new EscapeRule(), new TextRule(), ]); diff --git a/src/Tokens/QuoteToken.php b/src/Tokens/QuoteToken.php index f6f11d5..737d9fe 100644 --- a/src/Tokens/QuoteToken.php +++ b/src/Tokens/QuoteToken.php @@ -6,6 +6,7 @@ use Tempest\Markdown\Rules\BoldAndItalicRule; use Tempest\Markdown\Rules\BoldRule; use Tempest\Markdown\Rules\CodeRule; +use Tempest\Markdown\Rules\EscapeRule; use Tempest\Markdown\Rules\ImageRule; use Tempest\Markdown\Rules\ItalicRule; use Tempest\Markdown\Rules\LinkRule; @@ -34,6 +35,7 @@ public function parse(Parser $parser): string new LinkRule(), new SocialHandleRule(), new ImageRule(), + new EscapeRule(), new TextRule(), ]) ->parse($this->content); diff --git a/src/Tokens/StrikethroughToken.php b/src/Tokens/StrikethroughToken.php index 8b68180..a6d1cde 100644 --- a/src/Tokens/StrikethroughToken.php +++ b/src/Tokens/StrikethroughToken.php @@ -6,6 +6,7 @@ use Tempest\Markdown\Rules\BoldAndItalicRule; use Tempest\Markdown\Rules\BoldRule; use Tempest\Markdown\Rules\CodeRule; +use Tempest\Markdown\Rules\EscapeRule; use Tempest\Markdown\Rules\ImageRule; use Tempest\Markdown\Rules\ItalicRule; use Tempest\Markdown\Rules\LinkRule; @@ -30,6 +31,7 @@ public function parse(Parser $parser): string new LinkRule(), new SocialHandleRule(), new ImageRule(), + new EscapeRule(), new TextRule(), ]) ->parse($this->content); diff --git a/src/Tokens/TableToken.php b/src/Tokens/TableToken.php index ea69f2d..3abb897 100644 --- a/src/Tokens/TableToken.php +++ b/src/Tokens/TableToken.php @@ -6,6 +6,7 @@ use Tempest\Markdown\Rules\BoldAndItalicRule; use Tempest\Markdown\Rules\BoldRule; use Tempest\Markdown\Rules\CodeRule; +use Tempest\Markdown\Rules\EscapeRule; use Tempest\Markdown\Rules\ImageRule; use Tempest\Markdown\Rules\ItalicRule; use Tempest\Markdown\Rules\LinkRule; @@ -32,6 +33,7 @@ public function parse(Parser $parser): string new SocialHandleRule(), new CodeRule(), new ImageRule(), + new EscapeRule(), new TextRule(), ]); diff --git a/tests/Rules/EscapeRuleTest.php b/tests/Rules/EscapeRuleTest.php new file mode 100644 index 0000000..a619d58 --- /dev/null +++ b/tests/Rules/EscapeRuleTest.php @@ -0,0 +1,60 @@ +assertSame( + $expected, + new Markdown(highlighter: null)->parse($markdown)->html, + ); + } + + public static function provideEscapes(): iterable + { + yield 'asterisks' => [ + 'Hôtels 3\*\*\* et 4\*\*\*\*', + '
Hôtels 3*** et 4****
', + ]; + yield 'emphasis delimiters' => [ + '\*not emphasis\*', + '*not emphasis*
', + ]; + yield 'underscores' => ['\_x\_', '_x_
']; + yield 'link brackets' => ['\[x\](/y)', '[x](/y)
']; + yield 'ordered list marker' => ['1\. Step', '1. Step
']; + yield 'heading marker' => ['\# Title', '# Title
']; + yield 'backslash' => ['a\\\\b', 'a\b
']; + yield 'markup' => ['a\a<b']; + yield 'in a list item' => ['- \* star', '| a |
|---|
| 3** |
a\qb
']; + yield 'inside code' => ['`a\*b`', 'a\*b
a
\nb
a\\nb
", + ]; + } +}