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:33:23 +00:00
co-authored by renee-png
parent 99bb03c9dc
commit 00f16ca400
+106 -25
View File
@@ -529,6 +529,84 @@ export function CustomFormBuilder() {
doc.setFontSize(fontSize);
const lineH = fontSize * 1.4;
// Group runs into visual lines (split where applied text contains "\n").
const runsToLines = (runs: InlineRun[]): InlineRun[][] => {
const lines: InlineRun[][] = [[]];
for (const r of runs) {
const parts = applyVariables(r.text, ctx).split("\n");
parts.forEach((part, i) => {
if (i > 0) lines.push([]);
if (part) lines[lines.length - 1].push({ ...r, text: part });
});
}
return lines;
};
// Draw inline runs on one visual line, honoring word-wrap and per-run font style.
const drawRunsLine = (
runs: InlineRun[],
startX: number,
startY: number,
widthLimit: number,
align: "left" | "center" | "right" | "justify",
): number => {
type Token = { text: string; bold: boolean; italic: boolean; underline: boolean; isSpace: boolean };
const tokens: Token[] = [];
for (const r of runs) {
const bold = !!r.bold;
const italic = !!r.italic;
const underline = !!r.underline;
const parts = r.text.split(/(\s+)/);
for (const p of parts) {
if (!p) continue;
tokens.push({ text: p, bold, italic, underline, isSpace: /^\s+$/.test(p) });
}
}
const styleFor = (t: Token) =>
t.bold && t.italic ? "bolditalic" : t.bold ? "bold" : t.italic ? "italic" : "normal";
const widthOf = (t: Token) => {
doc.setFont(pdfFont, styleFor(t));
return doc.getTextWidth(t.text);
};
// Wrap into lines
const lines: Token[][] = [[]];
let cur = 0;
for (const t of tokens) {
const w = widthOf(t);
if (cur + w > widthLimit && lines[lines.length - 1].length > 0) {
lines.push([]);
cur = 0;
if (t.isSpace) continue;
}
lines[lines.length - 1].push(t);
cur += w;
}
let yy = startY;
for (const ln of lines) {
while (ln.length && ln[ln.length - 1].isSpace) ln.pop();
if (yy > pageH - margin) {
doc.addPage();
yy = margin;
}
const lineW = ln.reduce((a, t) => a + widthOf(t), 0);
let x = startX;
if (align === "center") x = startX + (widthLimit - lineW) / 2;
else if (align === "right") x = startX + (widthLimit - lineW);
for (const t of ln) {
doc.setFont(pdfFont, styleFor(t));
doc.text(t.text, x, yy);
if (t.underline && !t.isSpace) {
const w = widthOf(t);
doc.setLineWidth(0.5);
doc.line(x, yy + 1.5, x + w, yy + 1.5);
}
x += widthOf(t);
}
yy += lineH;
}
return yy;
};
const drawTablePdf = (rows: TableCellSeg[][]) => {
if (!rows.length) return;
const cols = Math.max(...rows.map((r) => r.length));
@@ -536,11 +614,19 @@ export function CustomFormBuilder() {
const cellPad = 4;
for (let ri = 0; ri < rows.length; ri++) {
const row = rows[ri];
// Compute row height based on tallest cell
const cellLines = row.map((c) =>
doc.splitTextToSize(applyVariables(c.text, ctx) || " ", colW - cellPad * 2),
);
const rowH = Math.max(...cellLines.map((l) => l.length)) * lineH + cellPad * 2;
// Estimate row height by wrapping each cell's visual lines as plain text.
const cellLineCounts = row.map((c) => {
const ls = runsToLines(c?.runs ?? []);
let total = 0;
for (const lineRuns of ls) {
doc.setFont(pdfFont, "normal");
const flat = lineRuns.map((r) => r.text).join("");
const wrapped = doc.splitTextToSize(flat || " ", colW - cellPad * 2);
total += Math.max(1, wrapped.length);
}
return Math.max(1, total);
});
const rowH = Math.max(...cellLineCounts) * lineH + cellPad * 2;
if (y + rowH > pageH - margin) {
doc.addPage();
y = margin;
@@ -555,13 +641,11 @@ export function CustomFormBuilder() {
if (borders.b) doc.line(x, y + rowH, x + colW, y + rowH);
if (borders.l) doc.line(x, y, x, y + rowH);
if (borders.r) doc.line(x + colW, y, x + colW, y + rowH);
const isHeader = cell?.isHeader ?? false;
doc.setFont(pdfFont, isHeader ? "bold" : "normal");
const lines = cellLines[ci] ?? [""];
// Render runs — no auto-bold for headers; user controls formatting.
const visualLines = runsToLines(cell?.runs ?? []);
let ty = y + cellPad + lineH * 0.8;
for (const ln of lines) {
doc.text(ln, x + cellPad, ty);
ty += lineH;
for (const lineRuns of visualLines) {
ty = drawRunsLine(lineRuns, x + cellPad, ty, colW - cellPad * 2, "left");
}
}
y += rowH;
@@ -574,24 +658,21 @@ export function CustomFormBuilder() {
drawTablePdf(seg.rows);
continue;
}
const text = applyVariables(seg.text, ctx) || " ";
doc.setFont(pdfFont, seg.heading && seg.heading > 0 ? "bold" : "normal");
const indentPx = seg.indent || 0;
const indentPt = indentPx * 0.75; // px → pt
const segMaxW = maxW - indentPt;
const lines = doc.splitTextToSize(text, segMaxW);
for (const line of lines) {
if (y > pageH - margin) {
doc.addPage();
y = margin;
}
let x = margin + indentPt;
if (seg.align === "center") x = pageW / 2;
else if (seg.align === "right") x = pageW - margin;
doc.text(line, x, y, {
align: seg.align === "center" ? "center" : seg.align === "right" ? "right" : "left",
});
const startX = margin + indentPt;
const visualLines = runsToLines(seg.runs);
if (visualLines.length === 0 || (visualLines.length === 1 && visualLines[0].length === 0)) {
y += lineH;
} else {
for (const lineRuns of visualLines) {
if (lineRuns.length === 0) {
y += lineH;
continue;
}
y = drawRunsLine(lineRuns, startX, y, segMaxW, seg.align);
}
}
y += seg.caption ? 2 : 4;
}