Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-18 16:52:43 +00:00
co-authored by renee-png
parent bd9fe62f38
commit 914e717d42
2 changed files with 115 additions and 15 deletions
+80 -14
View File
@@ -520,6 +520,65 @@ export function CustomFormBuilder() {
const LIST_INDENT = 24; // px per nesting level
const toRoman = (num: number) => {
const map: Array<[number, string]> = [
[1000, "M"],
[900, "CM"],
[500, "D"],
[400, "CD"],
[100, "C"],
[90, "XC"],
[50, "L"],
[40, "XL"],
[10, "X"],
[9, "IX"],
[5, "V"],
[4, "IV"],
[1, "I"],
];
let out = "";
let n = Math.max(1, Math.floor(num));
for (const [value, glyph] of map) {
while (n >= value) {
out += glyph;
n -= value;
}
}
return out;
};
const formatOrderedMarker = (index: number, listEl: HTMLElement) => {
const explicitType = (listEl.getAttribute("type") || "").toLowerCase();
const styleType = (listEl.style.listStyleType || "").toLowerCase();
const kind = explicitType || styleType;
if (kind === "a" || kind === "lower-alpha") return `${String.fromCharCode(96 + Math.max(1, index))}.`;
if (kind === "A" || kind === "upper-alpha") return `${String.fromCharCode(64 + Math.max(1, index))}.`;
if (kind === "i" || kind === "lower-roman") return `${toRoman(index).toLowerCase()}.`;
if (kind === "I" || kind === "upper-roman") return `${toRoman(index)}.`;
return `${index}.`;
};
const extractLeadingManualMarker = (runs: InlineRun[]) => {
const preview = runs.map((r) => r.text).join("").slice(0, 32);
const match = preview.match(/^\s*((?:\d+|[A-Za-z]+|[ivxlcdmIVXLCDM]+)[.)])\s+/);
if (!match) return { marker: null as string | null, runs };
let remaining = match[0].length;
const trimmed: InlineRun[] = [];
for (const run of runs) {
if (remaining <= 0) {
trimmed.push(run);
continue;
}
if (run.text.length <= remaining) {
remaining -= run.text.length;
continue;
}
trimmed.push({ ...run, text: run.text.slice(remaining) });
remaining = 0;
}
return { marker: match[1], runs: mergeRuns(trimmed) };
};
const pushListItems = (
listEl: HTMLElement,
ordered: boolean,
@@ -528,26 +587,26 @@ export function CustomFormBuilder() {
const items = Array.from(listEl.children).filter(
(c) => (c as HTMLElement).tagName.toLowerCase() === "li",
) as HTMLElement[];
let n = 1;
const startAttr = parseInt(listEl.getAttribute("start") || "1", 10);
let n = Number.isFinite(startAttr) && startAttr > 0 ? startAttr : 1;
for (const li of items) {
// Collect inline runs from this li, excluding nested lists.
const inlineNodes = Array.from(li.childNodes).filter((c) => {
if (c.nodeType !== Node.ELEMENT_NODE) return true;
const t = (c as HTMLElement).tagName.toLowerCase();
return t !== "ol" && t !== "ul";
});
const runs: InlineRun[] = [];
inlineNodes.forEach((n) => runs.push(...collectRuns(n)));
inlineNodes.forEach((node) => runs.push(...collectRuns(node)));
const merged = mergeRuns(runs);
const manual = ordered ? extractLeadingManualMarker(merged) : { marker: null as string | null, runs: merged };
out.push({
kind: "text",
runs: merged,
runs: manual.runs,
align: "left",
indent: depth * LIST_INDENT,
marker: ordered ? `${n}.` : "•",
marker: ordered ? manual.marker ?? formatOrderedMarker(n, listEl) : "•",
});
n += 1;
// Recurse into any nested lists.
Array.from(li.children).forEach((child) => {
const tag = (child as HTMLElement).tagName.toLowerCase();
if (tag === "ol") pushListItems(child as HTMLElement, true, depth + 1);
@@ -801,21 +860,28 @@ export function CustomFormBuilder() {
}
const indentPx = seg.indent || 0;
const indentPt = indentPx * 0.75; // px → pt
const segMaxW = maxW - indentPt;
const startX = margin + indentPt;
const runsForLayout: InlineRun[] = seg.marker
? [{ text: `${seg.marker} ` }, ...seg.runs]
: seg.runs;
const visualLines = runsToLines(runsForLayout);
const markerGapPt = seg.marker ? Math.max(14, doc.getTextWidth(seg.marker) + 8) : 0;
const markerX = margin + indentPt;
const textX = markerX + markerGapPt;
const segMaxW = Math.max(1, maxW - indentPt - markerGapPt);
const visualLines = runsToLines(seg.runs);
if (visualLines.length === 0 || (visualLines.length === 1 && visualLines[0].length === 0)) {
y += lineH;
} else {
if (seg.marker) {
if (y > pageH - margin) {
doc.addPage();
y = margin;
}
doc.setFont(pdfFont, "normal");
doc.text(seg.marker, markerX, y);
}
for (const lineRuns of visualLines) {
if (lineRuns.length === 0) {
y += lineH;
continue;
}
y = drawRunsLine(lineRuns, startX, y, segMaxW, seg.align);
y = drawRunsLine(lineRuns, textX, y, segMaxW, seg.align);
}
}
y += seg.caption ? 2 : 4;
@@ -958,7 +1024,7 @@ export function CustomFormBuilder() {
new DocxParagraph({
alignment: align,
spacing: docxLineSpacing,
indent: indentTwips ? { left: indentTwips, hanging: seg.marker ? 360 : undefined } : seg.marker ? { left: 360, hanging: 360 } : undefined,
indent: seg.marker ? { left: indentTwips + 360, hanging: 360 } : indentTwips ? { left: indentTwips } : undefined,
children: runsWithMarker,
}),
);
+35 -1
View File
@@ -144,8 +144,42 @@
h1, h2, h3, h4 { font-family: var(--font-serif); letter-spacing: -0.01em; }
}
/* Custom form editor: table + caption styling */
/* Custom form editor: table + list + caption styling */
.ProseMirror table { border-collapse: collapse; margin: 8px 0; width: 100%; table-layout: fixed; }
.ProseMirror th, .ProseMirror td { border: 1px solid #999; padding: 6px 8px; vertical-align: top; min-width: 40px; }
.ProseMirror th { background: #f3f4f6; font-weight: 600; text-align: left; }
.ProseMirror .selectedCell { background: rgba(59,130,246,0.15); }
.ProseMirror ul,
.ProseMirror ol {
margin: 0.5rem 0;
list-style-position: outside;
}
.ProseMirror ul {
list-style-type: disc !important;
padding-inline-start: 1.5rem;
}
.ProseMirror ul ul {
list-style-type: circle !important;
}
.ProseMirror ul ul ul {
list-style-type: square !important;
}
.ProseMirror ol {
list-style-type: decimal !important;
padding-inline-start: 1.75rem;
}
.ProseMirror ol ol {
list-style-type: lower-alpha !important;
}
.ProseMirror ol ol ol {
list-style-type: lower-roman !important;
}
.ProseMirror li {
display: list-item;
}
.ProseMirror li > p {
margin: 0;
}
.ProseMirror li::marker {
color: var(--color-foreground);
}