Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-18 02:32:22 +00:00
co-authored by renee-png
parent de39281fba
commit 99bb03c9dc
+74 -7
View File
@@ -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 <br> 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.
// <br> 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 <p> 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,