fix(description): render leaked markdown headers instead of showing raw
AI-generated descriptions ("Letra" / "Chorus" / "Translation") landed
in the DB with markdown prefixes that the sanitiser treated as plain
text — so viewers saw literal "## Letra" and "### Chorus" instead of
headings.
HtmlSanitizer::render() now promotes `##` / `###` at the start of a
line into <h2>/<h3> before sanitising, so the existing allowlist
accepts them and the reader sees real headings. Only touches lines
whose entire content is a heading — markdown mid-sentence is left
alone to avoid false positives.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
9eb9535c83
commit
36295fcd6a
1 file changed
+25
@@ -77,6 +77,11 @@ class HtmlSanitizer
|
||||
/**
|
||||
* Produce safe display HTML. Legacy plain-text values (no tags) are
|
||||
* escaped and converted to <br>; 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 <h2>/<h3> 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] = '<h3>' . e($m[1]) . '</h3>';
|
||||
} elseif (preg_match('/^\s*##\s+(.+?)\s*$/u', $line, $m)) {
|
||||
$lines[$i] = '<h2>' . e($m[1]) . '</h2>';
|
||||
}
|
||||
}
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
private static function sanitizeChildren(DOMNode $node): void
|
||||
{
|
||||
foreach (iterator_to_array($node->childNodes) as $child) {
|
||||
|
||||
Reference in new issue
Block a user