From 7e4469b29a36c8ec44bf0dd3a90c043b2426a7fe Mon Sep 17 00:00:00 2001 From: BOHA Date: Fri, 12 Jun 2026 08:35:46 +0200 Subject: [PATCH] fix(richtext): blank lines survive into PDFs and read-only previews MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quill 2's semantic HTML saves a blank line as a truly empty

(no
placeholder) — verified in the DB (invoice_sections hold

, zero


) — which collapses to nothing outside the editor. cleanQuillHtml (PDF path) and the new restoreQuillBlankLines (order/ invoice read-only previews) restore the placeholder at render time, so existing stored content is covered retroactively. Co-Authored-By: Claude Fable 5 --- src/__tests__/pdf-shared.test.ts | 11 +++++++++++ src/admin/pages/InvoiceDetail.tsx | 9 +++++++-- src/admin/pages/OrderDetail.tsx | 10 ++++++++-- src/admin/utils/formatters.ts | 12 ++++++++++++ src/utils/pdf-shared.ts | 4 ++++ 5 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/__tests__/pdf-shared.test.ts b/src/__tests__/pdf-shared.test.ts index a283bd7..679c8f5 100644 --- a/src/__tests__/pdf-shared.test.ts +++ b/src/__tests__/pdf-shared.test.ts @@ -32,6 +32,17 @@ describe("cleanQuillHtml — inline-style whitelist", () => { ); }); + it("restores Quill 2's empty

blank lines to


", () => { + // Quill 2's semantic HTML drops the
placeholder from blank lines. + expect(cleanQuillHtml("

A

B

")).toBe( + "

A


B

", + ); + // Attribute-carrying and whitespace-only empties too. + expect(cleanQuillHtml('

')).toBe( + '


', + ); + }); + it("keeps stripping scripts, event handlers and js: URLs", () => { const out = cleanQuillHtml( '

' + diff --git a/src/admin/pages/InvoiceDetail.tsx b/src/admin/pages/InvoiceDetail.tsx index 7ef2c2a..a80b655 100644 --- a/src/admin/pages/InvoiceDetail.tsx +++ b/src/admin/pages/InvoiceDetail.tsx @@ -58,6 +58,7 @@ import { formatCurrency, formatDate, numberOr, + restoreQuillBlankLines, todayLocalStr, } from "../utils/formatters"; import { normalizeDateStr } from "../utils/attendanceHelpers"; @@ -1670,7 +1671,9 @@ export default function InvoiceDetail() { {(s.content || "").replace(/<[^>]*>/g, "").trim() && ( )} @@ -1690,7 +1693,9 @@ export default function InvoiceDetail() { invoice.internal_notes !== "


" ? ( ) : ( diff --git a/src/admin/pages/OrderDetail.tsx b/src/admin/pages/OrderDetail.tsx index e8c3f51..00f9911 100644 --- a/src/admin/pages/OrderDetail.tsx +++ b/src/admin/pages/OrderDetail.tsx @@ -18,7 +18,11 @@ import Typography from "@mui/material/Typography"; import OrderConfirmationModal from "../components/OrderConfirmationModal"; import Forbidden from "../components/Forbidden"; import apiFetch from "../utils/api"; -import { formatCurrency, formatDate } from "../utils/formatters"; +import { + formatCurrency, + formatDate, + restoreQuillBlankLines, +} from "../utils/formatters"; import { ORDER_STATUS, statusLabel, statusColor } from "../lib/documentStatus"; import { Button, @@ -713,7 +717,9 @@ export default function OrderDetail() { )} diff --git a/src/admin/utils/formatters.ts b/src/admin/utils/formatters.ts index 55b2723..b59e755 100644 --- a/src/admin/utils/formatters.ts +++ b/src/admin/utils/formatters.ts @@ -76,3 +76,15 @@ export function czechPlural( if (n >= 2 && n <= 4) return few; return many; } + +/** + * Quill 2's semantic HTML saves blank lines as truly empty

, which + * collapse to zero height outside the editor — restore the
placeholder + * before rendering stored rich text read-only. (The PDF path does the same + * in cleanQuillHtml.) + */ +export function restoreQuillBlankLines( + html: string | null | undefined, +): string { + return (html || "").replace(/]*)>\s*<\/p>/gi, "

"); +} diff --git a/src/utils/pdf-shared.ts b/src/utils/pdf-shared.ts index d325f76..6275ebc 100644 --- a/src/utils/pdf-shared.ts +++ b/src/utils/pdf-shared.ts @@ -141,6 +141,10 @@ export function cleanQuillHtml(html: string | null | undefined): string { ); // Replace   with regular space (outside of tags) s = s.replace(/( )/g, " "); + // Quill 2's semantic HTML saves blank lines as truly empty

, which + // collapse to zero height in the PDF — restore the
placeholder so an + // editor blank line stays a blank line. + s = s.replace(/]*)>\s*<\/p>/gi, "

"); // Inline styles: Quill writes text/background colors as style attributes. // Keep ONLY color/background-color with literal color values — everything // else (url(), expression(), positioning…) must never reach Puppeteer.