Fixed caption border & parser

X-Lovable-Edit-ID: edt-18208ab2-8406-425f-9ccf-bf3f1a1df2e7
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-18 18:59:44 +00:00
co-authored by renee-png
3 changed files with 61 additions and 41 deletions
+1 -1
View File
@@ -85,7 +85,7 @@ const captionLeftBorders = {
const captionRightBorders = {
top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
bottom: { style: BorderStyle.SINGLE, size: 8, color: "000000" },
bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
};
+58 -38
View File
@@ -62,38 +62,53 @@ function htmlToBlocks(html: string): Block[] {
const walkList = (listEl: Element, ordered: boolean, level: number) => {
let idx = 1;
listEl.querySelectorAll(":scope > li").forEach((li) => {
const tokens: Token[] = [];
li.childNodes.forEach((child) => {
if (child.nodeType === Node.ELEMENT_NODE && /^(ul|ol)$/i.test((child as Element).tagName)) return;
tokenize(child, {}, tokens);
Array.from(listEl.children)
.filter((child): child is Element => child.tagName.toLowerCase() === "li")
.forEach((li) => {
const tokens: Token[] = [];
li.childNodes.forEach((child) => {
if (child.nodeType === Node.ELEMENT_NODE && /^(ul|ol)$/i.test((child as Element).tagName)) return;
tokenize(child, {}, tokens);
});
out.push({ kind: "list-item", ordered, level, index: idx++, tokens });
Array.from(li.children)
.filter((child): child is Element => /^(ul|ol)$/i.test(child.tagName))
.forEach((nested) => walkList(nested, nested.tagName.toLowerCase() === "ol", level + 1));
});
out.push({ kind: "list-item", ordered, level, index: idx++, tokens });
li.querySelectorAll(":scope > ul").forEach((n) => walkList(n as Element, false, level + 1));
li.querySelectorAll(":scope > ol").forEach((n) => walkList(n as Element, true, level + 1));
});
};
Array.from(root.children).forEach((el) => {
const tag = el.tagName.toLowerCase();
if (tag === "ul") return walkList(el, false, 0);
if (tag === "ol") return walkList(el, true, 0);
const walk = (node: Element) => {
const tag = node.tagName.toLowerCase();
if (tag === "ul") return walkList(node, false, 0);
if (tag === "ol") return walkList(node, true, 0);
if (tag === "blockquote") {
const tokens: Token[] = [];
el.childNodes.forEach((c) => tokenize(c, {}, tokens));
out.push({ kind: "para", align: alignOf(el), tokens, indent: 36 });
node.childNodes.forEach((c) => tokenize(c, {}, tokens));
out.push({ kind: "para", align: alignOf(node), tokens, indent: 36 });
return;
}
if (tag === "h1" || tag === "h2" || tag === "h3") {
const tokens: Token[] = [];
el.childNodes.forEach((c) => tokenize(c, { bold: true }, tokens));
out.push({ kind: "para", align: alignOf(el), tokens });
node.childNodes.forEach((c) => tokenize(c, { bold: true }, tokens));
out.push({ kind: "para", align: alignOf(node), tokens });
return;
}
if (tag === "p") {
const tokens: Token[] = [];
node.childNodes.forEach((c) => tokenize(c, {}, tokens));
out.push({ kind: "para", align: alignOf(node), tokens });
return;
}
if (tag === "div" || tag === "section" || tag === "article") {
Array.from(node.children).forEach((child) => walk(child));
return;
}
const tokens: Token[] = [];
el.childNodes.forEach((c) => tokenize(c, {}, tokens));
out.push({ kind: "para", align: alignOf(el), tokens });
});
node.childNodes.forEach((c) => tokenize(c, {}, tokens));
if (tokens.length > 0) out.push({ kind: "para", align: alignOf(node), tokens });
};
Array.from(root.children).forEach((el) => walk(el));
return out;
}
@@ -106,16 +121,24 @@ function layoutTokens(pdf: jsPDF, tokens: Token[], maxWidth: number): Piece[][]
};
const lineWidth = (line: Piece[]) => line.reduce((a, p) => a + p.w, 0);
const pushWord = (word: string, style: Style) => {
if (!word) return;
const w = measure(word, style);
const pushPiece = (text: string, style: Style) => {
if (!text) return;
const cur = lines[lines.length - 1];
if (lineWidth(cur) + w > maxWidth && cur.length > 0) {
while (cur.length && /\s+$/.test(cur[cur.length - 1].text)) cur.pop();
lines.push([{ text: word, style, w }]);
} else {
cur.push({ text: word, style, w });
const isWhitespace = /^\s+$/.test(text);
if (isWhitespace) {
if (cur.length === 0) return;
cur.push({ text, style, w: measure(text, style) });
return;
}
const w = measure(text, style);
if (lineWidth(cur) + w > maxWidth && cur.length > 0) {
while (cur.length && /^\s+$/.test(cur[cur.length - 1].text)) cur.pop();
lines.push([{ text, style, w }]);
return;
}
cur.push({ text, style, w });
};
for (const tok of tokens) {
@@ -123,8 +146,8 @@ function layoutTokens(pdf: jsPDF, tokens: Token[], maxWidth: number): Piece[][]
for (const part of parts) {
if (part === "\n") { lines.push([]); continue; }
if (!part) continue;
const words = part.match(/\S+\s*/g) || [part];
for (const w of words) pushWord(w, tok.style);
const chunks = part.match(/\s+|[^\s]+/g) || [part];
for (const chunk of chunks) pushPiece(chunk, tok.style);
}
}
return lines;
@@ -140,7 +163,7 @@ function drawLine(pdf: jsPDF, line: Piece[], x: number, y: number, align: "left"
if (align === "center") startX = x + (maxWidth - totalW) / 2;
else if (align === "right") startX = x + (maxWidth - totalW);
else if (align === "justify" && !isLast) {
const spaces = trimmed.filter((p) => /\s$/.test(p.text)).length;
const spaces = trimmed.filter((p) => /^\s+$/.test(p.text)).length;
if (spaces > 0) extraSpace = (maxWidth - totalW) / spaces;
}
@@ -149,13 +172,13 @@ function drawLine(pdf: jsPDF, line: Piece[], x: number, y: number, align: "left"
const sz = p.style.superscript ? FONT_SIZE * 0.75 : FONT_SIZE;
setFont(pdf, p.style, sz);
const drawY = p.style.superscript ? y - 4 : y;
pdf.text(p.text, cx, drawY);
if (!/^\s+$/.test(p.text)) pdf.text(p.text, cx, drawY);
if (p.style.underline) {
const tw = p.w;
pdf.setLineWidth(0.5);
pdf.line(cx, y + 1.5, cx + tw, y + 1.5);
}
cx += p.w + (/\s$/.test(p.text) ? extraSpace : 0);
cx += p.w + (/^\s+$/.test(p.text) ? extraSpace : 0);
}
}
@@ -197,7 +220,6 @@ export async function downloadPleadingPdf(input: PleadingInput, filename: string
const leftColW = CONTENT_W * 0.58;
const leftX = MARGIN;
const rightX = MARGIN + leftColW + 24;
const captionRightEdge = MARGIN + CONTENT_W;
const labelIndent = 24; // ~1/3" indent for "Plaintiff(s)," / "Defendant(s)."
const plaintiffLines = (input.plaintiffs || "").split("\n").filter((l) => l.trim());
@@ -230,10 +252,8 @@ export async function downloadPleadingPdf(input: PleadingInput, filename: string
const captionEndY = ly;
pdf.setLineWidth(0.6);
// Vertical divider between caption columns
pdf.line(MARGIN + leftColW + 12, captionStartY - LINE_H + 4, MARGIN + leftColW + 12, captionEndY);
// Bottom border spanning BOTH caption cells (full content width)
pdf.line(leftX, captionEndY + 2, captionRightEdge, captionEndY + 2);
// Bottom border only under the left caption cell
pdf.line(leftX, captionEndY + 2, MARGIN + leftColW + 12, captionEndY + 2);
y = captionEndY + LINE_H;
+2 -2
View File
@@ -304,8 +304,8 @@ function PleadingNewPage() {
<div>{headerLine2}</div>
</div>
<div className="mt-6 border-b border-black flex">
<div className="flex-1 pr-4 border-r border-black font-bold pb-2">
<div className="mt-6 flex">
<div className="flex-1 pr-4 border-b border-black font-bold pb-2">
<div className="whitespace-pre-line min-h-[1.5em]">{plaintiffs || " "}</div>
<div className="mt-3 pl-8">Plaintiff(s),</div>
<div className="mt-3">v.</div>