From 99bb03c9dcb91d62260e32b8f84789af0b47389e Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 02:32:22 +0000 Subject: [PATCH] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/forms/custom-form-builder.tsx | 81 ++++++++++++++++++-- 1 file changed, 74 insertions(+), 7 deletions(-) diff --git a/src/components/forms/custom-form-builder.tsx b/src/components/forms/custom-form-builder.tsx index e8575d3..0f8e38f 100644 --- a/src/components/forms/custom-form-builder.tsx +++ b/src/components/forms/custom-form-builder.tsx @@ -359,12 +359,14 @@ export function CustomFormBuilder() { editor.chain().focus().updateAttributes("paragraph", { caption: !cur }).run(); }; - // Linearized document segments for export. Tables now carry per-cell border data. - type TableCellSeg = { text: string; borders: CellBorders; isHeader: boolean }; + // Linearized document segments for export. Inline runs preserve bold/italic/underline. + // Newlines inside `text` (from
or block splits inside cells) are honored on render. + type InlineRun = { text: string; bold?: boolean; italic?: boolean; underline?: boolean }; + type TableCellSeg = { runs: InlineRun[]; borders: CellBorders; isHeader: boolean }; type Segment = | { kind: "text"; - text: string; + runs: InlineRun[]; align: "left" | "center" | "right" | "justify"; indent: number; caption?: boolean; @@ -372,6 +374,73 @@ export function CustomFormBuilder() { } | { kind: "table"; rows: TableCellSeg[][] }; + // Walk a DOM subtree, emitting inline runs with bold/italic/underline marks. + //
becomes a newline character within the current run stream. + const collectRuns = ( + node: Node, + marks: { bold?: boolean; italic?: boolean; underline?: boolean } = {}, + ): InlineRun[] => { + if (node.nodeType === Node.TEXT_NODE) { + const t = node.textContent ?? ""; + if (!t) return []; + return [{ text: t, ...marks }]; + } + if (node.nodeType !== Node.ELEMENT_NODE) return []; + const el = node as HTMLElement; + const tag = el.tagName.toLowerCase(); + if (tag === "br") return [{ text: "\n", ...marks }]; + const next = { ...marks }; + if (tag === "strong" || tag === "b") next.bold = true; + if (tag === "em" || tag === "i") next.italic = true; + if (tag === "u") next.underline = true; + // Honor inline style overrides too + const fw = el.style.fontWeight; + if (fw && (fw === "bold" || parseInt(fw, 10) >= 600)) next.bold = true; + if (el.style.fontStyle === "italic") next.italic = true; + const td = el.style.textDecoration || el.style.textDecorationLine; + if (td && td.includes("underline")) next.underline = true; + const out: InlineRun[] = []; + el.childNodes.forEach((child) => out.push(...collectRuns(child, next))); + return out; + }; + + // Merge adjacent runs with identical marks for cleaner output. + const mergeRuns = (runs: InlineRun[]): InlineRun[] => { + const out: InlineRun[] = []; + for (const r of runs) { + if (!r.text) continue; + const last = out[out.length - 1]; + if ( + last && + !!last.bold === !!r.bold && + !!last.italic === !!r.italic && + !!last.underline === !!r.underline + ) { + last.text += r.text; + } else { + out.push({ ...r }); + } + } + return out; + }; + + const collectCellRuns = (cell: HTMLElement): InlineRun[] => { + // Cells may contain multiple

blocks — join them with a newline. + const blocks = Array.from(cell.children).filter((c) => + ["p", "h1", "h2", "h3", "div", "li"].includes((c as HTMLElement).tagName.toLowerCase()), + ) as HTMLElement[]; + let runs: InlineRun[] = []; + if (blocks.length === 0) { + runs = collectRuns(cell); + } else { + blocks.forEach((b, i) => { + if (i > 0) runs.push({ text: "\n" }); + runs.push(...collectRuns(b)); + }); + } + return mergeRuns(runs); + }; + const getRenderedSegments = (): Segment[] => { if (!editor) return []; const html = editor.getHTML(); @@ -379,8 +448,6 @@ export function CustomFormBuilder() { tmp.innerHTML = html; const out: Segment[] = []; - const cellText = (cell: HTMLElement) => (cell.textContent ?? "").replace(/\s+/g, " ").trim(); - const pushBlock = (el: HTMLElement) => { const tag = el.tagName.toLowerCase(); if (tag === "table") { @@ -389,7 +456,7 @@ export function CustomFormBuilder() { const cells = Array.from(tr.children) as HTMLElement[]; rows.push( cells.map((c) => ({ - text: cellText(c), + runs: collectCellRuns(c), borders: parseBordersFromAttr(c.getAttribute("data-borders") ?? "trbl"), isHeader: c.tagName.toLowerCase() === "th", })), @@ -405,7 +472,7 @@ export function CustomFormBuilder() { const heading = tag === "h1" ? 1 : tag === "h2" ? 2 : tag === "h3" ? 3 : 0; out.push({ kind: "text", - text: el.textContent || "", + runs: mergeRuns(collectRuns(el)), align, indent: indentPx, caption,