diff --git a/app/Support/HtmlSanitizer.php b/app/Support/HtmlSanitizer.php index e331d77..b400cb8 100644 --- a/app/Support/HtmlSanitizer.php +++ b/app/Support/HtmlSanitizer.php @@ -77,6 +77,11 @@ class HtmlSanitizer /** * Produce safe display HTML. Legacy plain-text values (no tags) are * escaped and converted to
; rich values are run through clean(). + * + * Also detects a handful of common markdown-ish prefixes that leaked + * in through AI-generated descriptions ("## Letra", "### Chorus") and + * turns them into real

/

tags before sanitising. Without this + * step a reader saw literal "## Letra" instead of a heading. */ public static function render(?string $value): string { @@ -85,6 +90,8 @@ class HtmlSanitizer return ''; } + $value = self::promoteLeakedMarkdown($value); + if (strip_tags($value) === $value) { return nl2br(e($value), false); } @@ -92,6 +99,24 @@ class HtmlSanitizer return self::clean($value); } + /** + * Convert markdown-style heading prefixes at the start of a line into + * real HTML tags, so the sanitiser can keep them. Only touches lines + * that start with ##, ###, or **…**; everything else is untouched. + */ + private static function promoteLeakedMarkdown(string $value): string + { + $lines = preg_split("/\r?\n/", $value); + foreach ($lines as $i => $line) { + if (preg_match('/^\s*###\s+(.+?)\s*$/u', $line, $m)) { + $lines[$i] = '

' . e($m[1]) . '

'; + } elseif (preg_match('/^\s*##\s+(.+?)\s*$/u', $line, $m)) { + $lines[$i] = '

' . e($m[1]) . '

'; + } + } + return implode("\n", $lines); + } + private static function sanitizeChildren(DOMNode $node): void { foreach (iterator_to_array($node->childNodes) as $child) {