Fix markdown_to_html mangling content that starts with a blank line

This commit is contained in:
Fabien Potencier
2026-06-05 20:43:14 +02:00
parent afad02326d
commit 113aec62e7
3 changed files with 86 additions and 4 deletions
+1
View File
@@ -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
+48 -4
View File
@@ -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);
}
}
@@ -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, "<h1>Hello</h1>\n+<p>Great!</p>"],
{% endapply %}
EOF, "<h1>Hello</h1>\n+<p>Great!</p>"],
["{{ include('html')|markdown_to_html }}", "<h1>Hello</h1>\n+<p>Great!</p>"],
[<<<EOF
{% apply markdown_to_html %}
Paragraph 1
Paragraph 2
{% endapply %}
EOF, "<p>Paragraph 1</p>\n+<p>Paragraph 2</p>"],
];
}
/**
* @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"],
];
}