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"],
];
}