Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
co-authored by
renee-png
parent
c7374382e1
commit
e1b20e7395
+173
-4
@@ -11,9 +11,16 @@ import {
|
||||
TextRun,
|
||||
WidthType,
|
||||
BorderStyle,
|
||||
Footer,
|
||||
LevelFormat,
|
||||
} from "docx";
|
||||
import { saveAs } from "file-saver";
|
||||
|
||||
export interface PleadingFootnote {
|
||||
id: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface PleadingInput {
|
||||
courtType: "CIRCUIT" | "COUNTY";
|
||||
circuit: string; // e.g. "ELEVENTH"
|
||||
@@ -22,7 +29,9 @@ export interface PleadingInput {
|
||||
defendants: string; // multi-line
|
||||
caseNumber: string;
|
||||
title?: string;
|
||||
body?: string;
|
||||
bodyHtml?: string; // rich HTML body
|
||||
body?: string; // legacy plain text fallback
|
||||
footnotes?: PleadingFootnote[];
|
||||
}
|
||||
|
||||
const FONT = "Bookman Old Style";
|
||||
@@ -44,8 +53,16 @@ const verticalLine = {
|
||||
right: { style: BorderStyle.SINGLE, size: 8, color: "000000" },
|
||||
};
|
||||
|
||||
function run(text: string, opts: { bold?: boolean } = {}) {
|
||||
return new TextRun({ text, bold: opts.bold, font: FONT, size: SIZE });
|
||||
function run(text: string, opts: { bold?: boolean; italic?: boolean; underline?: boolean; superscript?: boolean; size?: number } = {}) {
|
||||
return new TextRun({
|
||||
text,
|
||||
bold: opts.bold,
|
||||
italics: opts.italic,
|
||||
underline: opts.underline ? {} : undefined,
|
||||
superScript: opts.superscript,
|
||||
font: FONT,
|
||||
size: opts.size ?? SIZE,
|
||||
});
|
||||
}
|
||||
|
||||
function p(text: string, opts: { bold?: boolean; align?: (typeof AlignmentType)[keyof typeof AlignmentType] } = {}) {
|
||||
@@ -59,6 +76,108 @@ function emptyP() {
|
||||
return new Paragraph({ children: [run("")] });
|
||||
}
|
||||
|
||||
// ---------- HTML → docx Paragraphs ----------
|
||||
|
||||
type RunStyle = { bold?: boolean; italic?: boolean; underline?: boolean; superscript?: boolean };
|
||||
|
||||
function alignFromStyle(node: Element): (typeof AlignmentType)[keyof typeof AlignmentType] | undefined {
|
||||
const ta = (node.getAttribute("style") || "").match(/text-align:\s*(left|center|right|justify)/i)?.[1]?.toLowerCase();
|
||||
switch (ta) {
|
||||
case "center": return AlignmentType.CENTER;
|
||||
case "right": return AlignmentType.RIGHT;
|
||||
case "justify": return AlignmentType.JUSTIFIED;
|
||||
case "left": return AlignmentType.LEFT;
|
||||
default: return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function collectRuns(node: Node, style: RunStyle, out: TextRun[]) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const text = node.textContent || "";
|
||||
if (text) out.push(run(text, style));
|
||||
return;
|
||||
}
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||
const el = node as Element;
|
||||
const tag = el.tagName.toLowerCase();
|
||||
const next: RunStyle = { ...style };
|
||||
if (tag === "strong" || tag === "b") next.bold = true;
|
||||
if (tag === "em" || tag === "i") next.italic = true;
|
||||
if (tag === "u") next.underline = true;
|
||||
if (tag === "sup") next.superscript = true;
|
||||
if (tag === "br") {
|
||||
out.push(new TextRun({ text: "", break: 1, font: FONT, size: SIZE }));
|
||||
return;
|
||||
}
|
||||
el.childNodes.forEach((child) => collectRuns(child, next, out));
|
||||
}
|
||||
|
||||
function blockToParagraphs(el: Element, listCtx?: { ref: string; level: number }): Paragraph[] {
|
||||
const tag = el.tagName.toLowerCase();
|
||||
const align = alignFromStyle(el);
|
||||
|
||||
if (tag === "ul" || tag === "ol") {
|
||||
const ref = tag === "ul" ? "pl-bullets" : "pl-numbers";
|
||||
const level = (listCtx?.level ?? -1) + 1;
|
||||
const out: Paragraph[] = [];
|
||||
el.querySelectorAll(":scope > li").forEach((li) => {
|
||||
const runs: TextRun[] = [];
|
||||
li.childNodes.forEach((child) => {
|
||||
if (child.nodeType === Node.ELEMENT_NODE && /^(ul|ol)$/i.test((child as Element).tagName)) return;
|
||||
collectRuns(child, {}, runs);
|
||||
});
|
||||
out.push(new Paragraph({
|
||||
numbering: { reference: ref, level },
|
||||
children: runs.length ? runs : [run("")],
|
||||
}));
|
||||
li.querySelectorAll(":scope > ul, :scope > ol").forEach((nested) => {
|
||||
out.push(...blockToParagraphs(nested as Element, { ref, level }));
|
||||
});
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
if (tag === "blockquote") {
|
||||
const out: Paragraph[] = [];
|
||||
el.childNodes.forEach((child) => {
|
||||
if (child.nodeType === Node.ELEMENT_NODE) {
|
||||
out.push(...blockToParagraphs(child as Element).map((par) => par));
|
||||
} else if (child.nodeType === Node.TEXT_NODE && (child.textContent || "").trim()) {
|
||||
out.push(new Paragraph({
|
||||
alignment: align,
|
||||
indent: { left: 720 },
|
||||
children: [run(child.textContent || "")],
|
||||
}));
|
||||
}
|
||||
});
|
||||
if (out.length === 0) out.push(new Paragraph({ indent: { left: 720 }, children: [run("")] }));
|
||||
return out;
|
||||
}
|
||||
|
||||
if (tag === "h1" || tag === "h2" || tag === "h3") {
|
||||
const runs: TextRun[] = [];
|
||||
el.childNodes.forEach((c) => collectRuns(c, { bold: true }, runs));
|
||||
return [new Paragraph({ alignment: align, children: runs.length ? runs : [run("")] })];
|
||||
}
|
||||
|
||||
// Default: paragraph
|
||||
const runs: TextRun[] = [];
|
||||
el.childNodes.forEach((c) => collectRuns(c, {}, runs));
|
||||
return [new Paragraph({ alignment: align, children: runs.length ? runs : [run("")] })];
|
||||
}
|
||||
|
||||
function htmlToParagraphs(html: string): Paragraph[] {
|
||||
if (typeof window === "undefined" || !html) return [];
|
||||
const doc = new DOMParser().parseFromString(`<div>${html}</div>`, "text/html");
|
||||
const root = doc.body.firstElementChild;
|
||||
if (!root) return [];
|
||||
const out: Paragraph[] = [];
|
||||
Array.from(root.children).forEach((child) => {
|
||||
out.push(...blockToParagraphs(child));
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
export function buildPleadingDoc(input: PleadingInput): Document {
|
||||
const headerLine1 = `IN THE ${input.courtType} COURT OF THE ${input.circuit} JUDICIAL CIRCUIT,`;
|
||||
const headerLine2 = `IN AND FOR ${input.county.toUpperCase()} COUNTY, FLORIDA`;
|
||||
@@ -114,14 +233,59 @@ export function buildPleadingDoc(input: PleadingInput): Document {
|
||||
bodyParagraphs.push(p(input.title.toUpperCase(), { bold: true, align: AlignmentType.CENTER }));
|
||||
bodyParagraphs.push(emptyP());
|
||||
}
|
||||
if (input.body) {
|
||||
if (input.bodyHtml && input.bodyHtml.trim()) {
|
||||
bodyParagraphs.push(...htmlToParagraphs(input.bodyHtml));
|
||||
} else if (input.body) {
|
||||
input.body.split("\n").forEach((line) => bodyParagraphs.push(p(line)));
|
||||
}
|
||||
|
||||
// Footnotes rendered as endnote-style block at bottom of document
|
||||
const footnotes = (input.footnotes || []).filter((f) => f.text.trim().length > 0);
|
||||
if (footnotes.length > 0) {
|
||||
bodyParagraphs.push(emptyP());
|
||||
bodyParagraphs.push(new Paragraph({
|
||||
border: { top: { style: BorderStyle.SINGLE, size: 6, color: "000000", space: 4 } },
|
||||
children: [run("")],
|
||||
}));
|
||||
footnotes.forEach((f) => {
|
||||
bodyParagraphs.push(new Paragraph({
|
||||
children: [
|
||||
run(`${f.id}`, { superscript: true, size: 20 }),
|
||||
run(" "),
|
||||
run(f.text, { size: 20 }),
|
||||
],
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
return new Document({
|
||||
styles: {
|
||||
default: { document: { run: { font: FONT, size: SIZE } } },
|
||||
},
|
||||
numbering: {
|
||||
config: [
|
||||
{
|
||||
reference: "pl-bullets",
|
||||
levels: [0, 1, 2].map((lvl) => ({
|
||||
level: lvl,
|
||||
format: LevelFormat.BULLET,
|
||||
text: "\u2022",
|
||||
alignment: AlignmentType.LEFT,
|
||||
style: { paragraph: { indent: { left: 720 * (lvl + 1), hanging: 360 } } },
|
||||
})),
|
||||
},
|
||||
{
|
||||
reference: "pl-numbers",
|
||||
levels: [0, 1, 2].map((lvl) => ({
|
||||
level: lvl,
|
||||
format: LevelFormat.DECIMAL,
|
||||
text: `%${lvl + 1}.`,
|
||||
alignment: AlignmentType.LEFT,
|
||||
style: { paragraph: { indent: { left: 720 * (lvl + 1), hanging: 360 } } },
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
properties: {
|
||||
@@ -130,6 +294,11 @@ export function buildPleadingDoc(input: PleadingInput): Document {
|
||||
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 },
|
||||
},
|
||||
},
|
||||
footers: {
|
||||
default: new Footer({
|
||||
children: [new Paragraph({ alignment: AlignmentType.CENTER, children: [run("")] })],
|
||||
}),
|
||||
},
|
||||
children: [
|
||||
p(headerLine1, { bold: true, align: AlignmentType.CENTER }),
|
||||
p(headerLine2, { bold: true, align: AlignmentType.CENTER }),
|
||||
|
||||
Reference in New Issue
Block a user