diff --git a/src/Rules/BoldAndItalicRule.php b/src/Rules/BoldAndItalicRule.php index f1733a0..9eb1680 100644 --- a/src/Rules/BoldAndItalicRule.php +++ b/src/Rules/BoldAndItalicRule.php @@ -14,15 +14,28 @@ final class BoldAndItalicRule implements ProvidesFirstChar, ProvidesStopChar { + use ChecksFlanking; + private(set) string $firstChar = '*_'; private(set) string $stopChar = '_*'; public function shouldParse(Parser $parser): bool { - return ( - $parser->comesNext('***', length: 3) - || $parser->comesNext('___', length: 3) - ); + if ( + ! $parser->comesNext('***', length: 3) + && ! $parser->comesNext('___', length: 3) + ) { + return false; + } + + $start = $parser->position + 3; + $end = strpos($parser->content, $parser->current ?? '', $start); + + return $this->isFlanked(substr( + $parser->content, + $start, + $end === false ? null : $end - $start, + )); } public function parse(Parser $parser): Token diff --git a/src/Rules/BoldRule.php b/src/Rules/BoldRule.php index de2a7ad..9af743b 100644 --- a/src/Rules/BoldRule.php +++ b/src/Rules/BoldRule.php @@ -11,6 +11,8 @@ final class BoldRule implements Rule, ProvidesFirstChar, ProvidesStopChar { + use ChecksFlanking; + private(set) string $firstChar = '*_'; private(set) string $stopChar = '*_'; @@ -49,7 +51,7 @@ public function shouldParse(Parser $parser): bool return false; } - return true; + return $this->isFlanked(substr($content, 0, -1)); } public function parse(Parser $parser): Token diff --git a/src/Rules/ChecksFlanking.php b/src/Rules/ChecksFlanking.php new file mode 100644 index 0000000..18edee7 --- /dev/null +++ b/src/Rules/ChecksFlanking.php @@ -0,0 +1,23 @@ +isFlanked(substr($end, 0, -1)); } public function parse(Parser $parser): Token diff --git a/tests/EmphasisFlankingTest.php b/tests/EmphasisFlankingTest.php new file mode 100644 index 0000000..daa1ea9 --- /dev/null +++ b/tests/EmphasisFlankingTest.php @@ -0,0 +1,66 @@ +assertSame( + $expected, + new Markdown(highlighter: null)->parse($markdown)->html, + ); + } + + public static function provideFlanking(): iterable + { + yield 'italic' => ['*a*', '
a
']; + yield 'bold' => ['**a**', 'a
']; + yield 'bold and italic' => [ + '***a***', + 'a
', + ]; + yield 'underscore italic' => ['_a_', 'a
']; + yield 'underscore bold' => ['__a__', 'a
']; + yield 'inside a word' => ['foo*bar*', 'foobar
']; + + // An opening delimiter may not be followed by whitespace, and a + // closing one may not be preceded by it. + yield 'spaced asterisks' => ['2 * 3 * 4', '2 * 3 * 4
']; + yield 'spaced double asterisks' => ['a ** b **', 'a ** b **
']; + yield 'spaced triple asterisks' => [ + 'a *** b ***', + 'a *** b ***
', + ]; + yield 'spaced underscores' => ['x _ y _', 'x _ y _
']; + yield 'space before the closing asterisk' => ['*a *', '*a *
']; + yield 'space before the closing double asterisk' => [ + '**a **', + '**a **
', + ]; + yield 'space after the opening double asterisk' => [ + '** a**', + '** a**
', + ]; + yield 'space before the closing double underscore' => [ + '__a __', + '__a __
', + ]; + yield 'hotel ratings' => [ + 'Hôtels 3*** et 4****', + 'Hôtels 3*** et 4****
', + ]; + yield 'literal run after emphasis' => [ + '*a* * b *', + 'a * b *
', + ]; + } +}