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,