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) {