From 113aec62e7b3fa8462dfcda73f8e2504d08dece9 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 5 Jun 2026 20:43:14 +0200 Subject: [PATCH] Fix markdown_to_html mangling content that starts with a blank line --- CHANGELOG | 1 + extra/markdown-extra/MarkdownRuntime.php | 52 +++++++++++++++++-- extra/markdown-extra/Tests/FunctionalTest.php | 37 +++++++++++++ 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 443f4f010..054da6566 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ * Fix nested `block()` calls to resolve against the overriding template when a block rendered through `block(name, template)` calls `parent()` * Stop reporting a skipped test in `IntegrationTestCase` when there is no legacy test to run + * Fix `markdown_to_html` to strip the indentation shared by all lines instead of mangling content that starts with a blank line * Make the `IntegrationTestCase` and `NodeTestCase` test helpers compatible with PHPUnit 11 * Cast printed expressions to string so values that cannot be converted to a string (arrays, non-`Stringable` objects, ...) report a usable stack trace at the print location * Make the `include()` function return a `Markup` object so an assigned result is not re-escaped when printed diff --git a/extra/markdown-extra/MarkdownRuntime.php b/extra/markdown-extra/MarkdownRuntime.php index 6a96a5275..0572d7f42 100644 --- a/extra/markdown-extra/MarkdownRuntime.php +++ b/extra/markdown-extra/MarkdownRuntime.php @@ -22,11 +22,55 @@ class MarkdownRuntime public function convert(string $body): string { - // remove indentation - if ($white = substr($body, 0, strspn($body, " \t\r\n\0\x0B"))) { - $body = preg_replace("{^$white}m", '', $body); + return $this->converter->convert(self::stripCommonIndentation($body)); + } + + /** + * Removes the indentation shared by all non-blank lines. + * + * This lets authors indent a `{% apply markdown_to_html %}` block to match + * the surrounding template without that indentation leaking into Markdown + * (where leading whitespace is significant, e.g. code blocks). + */ + private static function stripCommonIndentation(string $body): string + { + $lines = explode("\n", $body); + + $indent = null; + foreach ($lines as $line) { + if ('' === trim($line)) { + continue; + } + + $lineIndent = substr($line, 0, strspn($line, " \t")); + if (null === $indent) { + $indent = $lineIndent; + continue; + } + + $max = min(\strlen($indent), \strlen($lineIndent)); + $common = 0; + while ($common < $max && $indent[$common] === $lineIndent[$common]) { + ++$common; + } + $indent = substr($indent, 0, $common); + + if ('' === $indent) { + return $body; + } } - return $this->converter->convert($body); + if (null === $indent || '' === $indent) { + return $body; + } + + $length = \strlen($indent); + foreach ($lines as $i => $line) { + if (str_starts_with($line, $indent)) { + $lines[$i] = substr($line, $length); + } + } + + return implode("\n", $lines); } } diff --git a/extra/markdown-extra/Tests/FunctionalTest.php b/extra/markdown-extra/Tests/FunctionalTest.php index 58fd7298e..9d5608253 100644 --- a/extra/markdown-extra/Tests/FunctionalTest.php +++ b/extra/markdown-extra/Tests/FunctionalTest.php @@ -17,6 +17,7 @@ use Twig\Extra\Markdown\DefaultMarkdown; use Twig\Extra\Markdown\ErusevMarkdown; use Twig\Extra\Markdown\LeagueMarkdown; use Twig\Extra\Markdown\MarkdownExtension; +use Twig\Extra\Markdown\MarkdownInterface; use Twig\Extra\Markdown\MarkdownRuntime; use Twig\Extra\Markdown\MichelfMarkdown; use Twig\Loader\ArrayLoader; @@ -77,6 +78,42 @@ EOF, "

Hello

\n+

Great!

"], {% endapply %} EOF, "

Hello

\n+

Great!

"], ["{{ include('html')|markdown_to_html }}", "

Hello

\n+

Great!

"], + [<<Paragraph 1

\n+

Paragraph 2

"], + ]; + } + + /** + * @dataProvider getIndentationTests + */ + public function testStripsCommonIndentation(string $body, string $expected) + { + $runtime = new MarkdownRuntime(new class implements MarkdownInterface { + public function convert(string $body): string + { + return $body; + } + }); + + $this->assertSame($expected, $runtime->convert($body)); + } + + public static function getIndentationTests() + { + return [ + 'leading blank line keeps blank lines' => ["\nParagraph 1\n\nParagraph 2", "\nParagraph 1\n\nParagraph 2"], + 'common indentation is removed' => ["\n Hello\n =====\n\n Great!\n", "\nHello\n=====\n\nGreat!\n"], + 'minimal common indentation is removed' => [" a\n b\n", "a\n b\n"], + 'indented code block before non-indented prose is preserved' => [" Code\n\nParagraph\n", " Code\n\nParagraph\n"], + 'tab indentation is removed' => ["\tHello\n\tGreat!\n", "Hello\nGreat!\n"], + 'mixed tabs and spaces are left untouched' => ["\ta\n b\n", "\ta\n b\n"], + 'blank lines are ignored when computing indentation' => [" a\n\n b\n", "a\n\nb\n"], ]; }