diff --git a/app/Support/HtmlHelper.php b/app/Support/HtmlHelper.php index d57893a2..249b152e 100644 --- a/app/Support/HtmlHelper.php +++ b/app/Support/HtmlHelper.php @@ -80,6 +80,36 @@ public static function sanitizeHtml(?string $html): string return self::htmlSanitizer()->sanitize($html); } + /** + * Render `libri.descrizione`, which may be either editor HTML or legacy + * plain text with newlines (imported records). + * + * Plain text gets nl2br so its line breaks survive. Content that is already + * block-structured HTML is left as-is: the editor (TinyMCE) separates its + *

paragraphs with a literal "\n", and running nl2br over that injects a + * stray
between every block, double-spacing the output (issue #313). + * The result is always sanitized. This intentionally remains specific to + * the book-description field addressed by issue #313. + * + * @param string|null $value Raw stored value + * @return string Sanitized HTML ready to echo + */ + public static function bookDescription(?string $value): string + { + if ($value === null || $value === '') { + return ''; + } + + // If the value already carries block/line HTML, its line breaks are + // markup — nl2br would turn the newlines *between* blocks into
. + $hasBlockHtml = preg_match( + '/<(?:p|br|div|ul|ol|li|h[1-6]|blockquote|pre|table|hr)\b/i', + $value + ) === 1; + + return self::sanitizeHtml($hasBlockHtml ? $value : nl2br($value, false)); + } + /** * Return a public absolute HTTP(S) URL, or an empty string when unsafe. * diff --git a/app/Views/frontend/book-detail.php b/app/Views/frontend/book-detail.php index be4800fd..0e3e9227 100644 --- a/app/Views/frontend/book-detail.php +++ b/app/Views/frontend/book-detail.php @@ -2026,7 +2026,7 @@ class="book-cover-large img-fluid" : \App\Support\MediaLabels::formatTracklist($musicDescription) ?> -

+

diff --git a/app/Views/libri/scheda_libro.php b/app/Views/libri/scheda_libro.php index f9a11671..c06831bb 100644 --- a/app/Views/libri/scheda_libro.php +++ b/app/Views/libri/scheda_libro.php @@ -718,7 +718,7 @@ class="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium rounded-full ?> - + diff --git a/tests/bookdescription-render-313.unit.php b/tests/bookdescription-render-313.unit.php new file mode 100644 index 00000000..b8dcdf75 --- /dev/null +++ b/tests/bookdescription-render-313.unit.php @@ -0,0 +1,58 @@ + per line break (not two). +$crlf = "Prima riga\r\nSeconda riga"; +$out = HtmlHelper::bookDescription($crlf); +$check(substr_count($out, ' per newline"); + +// 2. HTML with attributes on the block tag is still detected as HTML (no
). +$attr = "

Uno.

\n

Due.

"; +$out = HtmlHelper::bookDescription($attr); +$check(!str_contains($out, ')"); + +// 3. A leading plain sentence before a block still counts as HTML overall +// (has a block marker), so its internal newline is not turned into
. +$mixed = "Intro\n

Blocco.

"; +$out = HtmlHelper::bookDescription($mixed); +$check(!str_contains($out, ' between the

+ * paragraphs of a book description. + * + * Root cause: the book description was rendered with + * sanitizeHtml(nl2br($value, false)). Editor HTML separates its

blocks + * with a literal "\n", so nl2br turned every inter-block newline into a stray + *
, double-spacing the output. HtmlHelper::bookDescription() applies nl2br + * only to plain-text values and leaves block HTML untouched. The helper is + * intentionally specific to `libri.descrizione`; other fields are outside #313. + * + * Run: php tests/richtext-nl2br-313.unit.php (exit 0 iff all pass) + */ + +$root = dirname(__DIR__); +require $root . '/vendor/autoload.php'; + +use App\Support\HtmlHelper; + +$pass = 0; +$fail = 0; +$check = static function (bool $ok, string $label) use (&$pass, &$fail): void { + if ($ok) { $pass++; echo " OK {$label}\n"; } + else { $fail++; echo " FAIL {$label}\n"; } +}; + +// ── 1. The bug: editor HTML must NOT gain
between paragraphs ──────────── +$html = "

Para uno.

\n

Para due.

\n

Para tre.

"; +$out = HtmlHelper::bookDescription($html); +$check(!str_contains($out, ' (issue #313)"); +$check(substr_count($out, '

') === 3, "02 all three paragraphs are preserved"); + +// ── 2. Plain text still gets its line breaks ──────────────────────────────── +$plain = "Prima riga\nSeconda riga\nTerza riga"; +$out = HtmlHelper::bookDescription($plain); +$check(substr_count($out, '"); + +// A single-block value that only uses
is already HTML → left as-is (no +// doubling): the two
stay two, they are not turned into four. +$brHtml = "Riga uno
Riga due"; +$out = HtmlHelper::bookDescription($brHtml); +$check(substr_count($out, '-only value is treated as HTML, not re-broken"); + +// Other block markers are detected too (div / ul / heading). +foreach (['

x
', '
  • x
', '

x

'] as $i => $block) { + $withNl = $block . "\ntail"; + $out = HtmlHelper::bookDescription($withNl); + // The trailing "\ntail" newline must not become a
when the value is HTML. + $check(!str_contains($out, '') . '>')); +} + +// ── 3. Sanitisation is still applied ──────────────────────────────────────── +$out = HtmlHelper::bookDescription('

ok

'); +$check(!str_contains($out, 'alert(1) text"); +$check(!str_contains($out, ' empty string"); +$check(HtmlHelper::bookDescription('') === '', "11 empty => empty string"); + +// ── 5. Cyrillic content (the reporter's data) survives ────────────────────── +$cyr = "

Америка…

\n

Однако…

"; +$out = HtmlHelper::bookDescription($cyr); +$check(str_contains($out, 'Америка') && !str_contains($out, '