Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/Rules/EscapeRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace Tempest\Markdown\Rules;

use Tempest\Markdown\Parser;
use Tempest\Markdown\ProvidesFirstChar;
use Tempest\Markdown\ProvidesStopChar;
use Tempest\Markdown\Rule;
use Tempest\Markdown\Token;
use Tempest\Markdown\Tokens\TextToken;

/**
* A backslash before ASCII punctuation makes that character literal.
*
* @see https://spec.commonmark.org/0.31.2/#backslash-escapes
*/
final class EscapeRule implements Rule, ProvidesFirstChar, ProvidesStopChar
{
private const string PUNCTUATION = '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~';

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 ?? ' ')
? '&#92;'
: htmlspecialchars($char, ENT_COMPAT | ENT_HTML5);

if ($parser->lastToken instanceof TextToken) {
$parser->lastToken->append($text);

return null;
}

return new TextToken($text);
}
}
2 changes: 2 additions & 0 deletions src/Tokens/BoldAndItalicToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,6 +27,7 @@ public function parse(Parser $parser): string
new LinkRule(),
new SocialHandleRule(),
new ImageRule(),
new EscapeRule(),
new TextRule(),
])
->parse($this->content);
Expand Down
2 changes: 2 additions & 0 deletions src/Tokens/BoldToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,6 +29,7 @@ public function parse(Parser $parser): string
new LinkRule(),
new SocialHandleRule(),
new ImageRule(),
new EscapeRule(),
new TextRule(),
])
->parse($this->content);
Expand Down
2 changes: 2 additions & 0 deletions src/Tokens/DivToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -39,6 +40,7 @@ public function parse(Parser $parser): string
new ImageRule(),
new PreRule(),
new CodeRule(),
new EscapeRule(),
new TextRule(),
])
->parse($this->content);
Expand Down
2 changes: 2 additions & 0 deletions src/Tokens/HeadingToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -42,6 +43,7 @@ public function parse(Parser $parser): string
new SocialHandleRule(),
new ImageRule(),
new CodeRule(),
new EscapeRule(),
new TextRule(),
])
->parse($this->content);
Expand Down
2 changes: 2 additions & 0 deletions src/Tokens/HtmlToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -39,6 +40,7 @@ public function parse(Parser $parser): string
new SocialHandleRule(),
new ImageRule(),
new CodeRule(),
new EscapeRule(),
new TextRule(),
])
->parse($this->html)
Expand Down
2 changes: 2 additions & 0 deletions src/Tokens/ItalicToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,6 +29,7 @@ public function parse(Parser $parser): string
new LinkRule(),
new SocialHandleRule(),
new ImageRule(),
new EscapeRule(),
new TextRule(),
])
->parse($this->content);
Expand Down
2 changes: 2 additions & 0 deletions src/Tokens/LinkToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -35,6 +36,7 @@ public function parse(Parser $parser): string
new ItalicRule(),
new StrikethroughRule(),
new ImageRule(),
new EscapeRule(),
new TextRule(),
])
->parse($this->content);
Expand Down
2 changes: 2 additions & 0 deletions src/Tokens/ListToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,6 +33,7 @@ public function parse(Parser $parser): string
new SocialHandleRule(),
new ImageRule(),
new CodeRule(),
new EscapeRule(),
new TextRule(),
]);

Expand Down
2 changes: 2 additions & 0 deletions src/Tokens/OrderedListToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -35,6 +36,7 @@ public function parse(Parser $parser): string
new SocialHandleRule(),
new ImageRule(),
new CodeRule(),
new EscapeRule(),
new TextRule(),
]);

Expand Down
2 changes: 2 additions & 0 deletions src/Tokens/ParagraphToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -36,6 +37,7 @@ public function parse(Parser $parser): string
new PreRule(),
new CodeRule(),
new DivRule(),
new EscapeRule(),
new TextRule(),
]);

Expand Down
2 changes: 2 additions & 0 deletions src/Tokens/QuoteToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -34,6 +35,7 @@ public function parse(Parser $parser): string
new LinkRule(),
new SocialHandleRule(),
new ImageRule(),
new EscapeRule(),
new TextRule(),
])
->parse($this->content);
Expand Down
2 changes: 2 additions & 0 deletions src/Tokens/StrikethroughToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,6 +31,7 @@ public function parse(Parser $parser): string
new LinkRule(),
new SocialHandleRule(),
new ImageRule(),
new EscapeRule(),
new TextRule(),
])
->parse($this->content);
Expand Down
2 changes: 2 additions & 0 deletions src/Tokens/TableToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,6 +33,7 @@ public function parse(Parser $parser): string
new SocialHandleRule(),
new CodeRule(),
new ImageRule(),
new EscapeRule(),
new TextRule(),
]);

Expand Down
60 changes: 60 additions & 0 deletions tests/Rules/EscapeRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace Tempest\Markdown\Tests\Rules;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Tempest\Markdown\Markdown;
use Tempest\Markdown\Tests\ParserTestCase;

final class EscapeRuleTest extends ParserTestCase
{
#[Test]
#[DataProvider('provideEscapes')]
public function escaped_punctuation_is_literal(
string $markdown,
string $expected,
): void {
$this->assertSame(
$expected,
new Markdown(highlighter: null)->parse($markdown)->html,
);
}

public static function provideEscapes(): iterable
{
yield 'asterisks' => [
'Hôtels 3\*\*\* et 4\*\*\*\*',
'<p>Hôtels 3*** et 4****</p>',
];
yield 'emphasis delimiters' => [
'\*not emphasis\*',
'<p>*not emphasis*</p>',
];
yield 'underscores' => ['\_x\_', '<p>_x_</p>'];
yield 'link brackets' => ['\[x\](/y)', '<p>[x](/y)</p>'];
yield 'ordered list marker' => ['1\. Step', '<p>1. Step</p>'];
yield 'heading marker' => ['\# Title', '<p># Title</p>'];
yield 'backslash' => ['a\\\\b', '<p>a\b</p>'];
yield 'markup' => ['a\<b', '<p>a&lt;b</p>'];
yield 'in a list item' => ['- \* star', '<ul><li>* star</li></ul>'];
yield 'in a table cell' => [
"| a |\n| - |\n| 3\*\* |",
'<table><thead><tr><th>a</th></tr></thead>'
. '<tbody><tr><td>3**</td></tr></tbody></table>',
];

// Only ASCII punctuation can be escaped, and code spans keep their
// backslashes.
yield 'before a letter' => ['a\qb', '<p>a\qb</p>'];
yield 'inside code' => ['`a\*b`', '<p><code>a\*b</code></p>'];

// A backslash before a line ending is a hard line break, an escaped
// one is not.
yield 'hard line break' => ["a\\\nb", "<p>a<br />\nb</p>"];
yield 'escaped backslash before a line ending' => [
"a\\\\\nb",
"<p>a&#92;\nb</p>",
];
}
}
Loading