@stll/docx-core 0.0.1-placeholder.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1938 @@
1
+ import JSZip from "jszip";
2
+ import { panic } from "better-result";
3
+ //#region src/serialize/xml.ts
4
+ const ILLEGAL_XML_CHARS_RE = /[^\u0009\u000A\u000D\u0020-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]/gu;
5
+ const stripIllegalXmlChars = (value) => value.replace(ILLEGAL_XML_CHARS_RE, "");
6
+ const escapeXml = (value) => stripIllegalXmlChars(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
7
+ const attr = (name, value) => value === void 0 ? "" : ` ${name}="${escapeXml(String(value))}"`;
8
+ //#endregion
9
+ //#region src/serialize/docx.ts
10
+ const DEFAULT_FOOTER_REL_ID = "rId10";
11
+ const DEFAULT_FOOTER_PART_NAME = "footer1.xml";
12
+ const FOOTER_LABEL_FALLBACK = {
13
+ page: "Page",
14
+ of: "of"
15
+ };
16
+ const FOOTER_LABELS = {
17
+ en: FOOTER_LABEL_FALLBACK,
18
+ cs: {
19
+ page: "Strana",
20
+ of: "z"
21
+ },
22
+ sk: {
23
+ page: "Strana",
24
+ of: "z"
25
+ },
26
+ de: {
27
+ page: "Seite",
28
+ of: "von"
29
+ },
30
+ fr: {
31
+ page: "Page",
32
+ of: "sur"
33
+ },
34
+ es: {
35
+ page: "Página",
36
+ of: "de"
37
+ },
38
+ it: {
39
+ page: "Pagina",
40
+ of: "di"
41
+ },
42
+ pl: {
43
+ page: "Strona",
44
+ of: "z"
45
+ },
46
+ pt: {
47
+ page: "Página",
48
+ of: "de"
49
+ },
50
+ nl: {
51
+ page: "Pagina",
52
+ of: "van"
53
+ },
54
+ hu: {
55
+ page: "Oldal",
56
+ of: "/"
57
+ }
58
+ };
59
+ const resolveFooterLabels = (language) => {
60
+ if (!language) return FOOTER_LABEL_FALLBACK;
61
+ const primary = language.toLowerCase().split(/[-_]/u)[0] ?? "";
62
+ return FOOTER_LABELS[primary] ?? FOOTER_LABEL_FALLBACK;
63
+ };
64
+ const buildFooterXml = (language) => {
65
+ const { page, of } = resolveFooterLabels(language);
66
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:p><w:pPr><w:pStyle w:val="Footer"/><w:jc w:val="center"/></w:pPr><w:r><w:rPr><w:sz w:val="18"/></w:rPr><w:t xml:space="preserve">${escapeXml(page)} </w:t></w:r><w:fldSimple w:instr="PAGE \\* MERGEFORMAT"><w:r><w:rPr><w:sz w:val="18"/></w:rPr><w:t>1</w:t></w:r></w:fldSimple><w:r><w:rPr><w:sz w:val="18"/></w:rPr><w:t xml:space="preserve"> ${escapeXml(of)} </w:t></w:r><w:fldSimple w:instr="NUMPAGES \\* MERGEFORMAT"><w:r><w:rPr><w:sz w:val="18"/></w:rPr><w:t>1</w:t></w:r></w:fldSimple></w:p></w:ftr>`;
67
+ };
68
+ const contentTypesXml = (hasNumbering) => [
69
+ "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>",
70
+ "<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">",
71
+ "<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>",
72
+ "<Default Extension=\"xml\" ContentType=\"application/xml\"/>",
73
+ "<Override PartName=\"/word/document.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml\"/>",
74
+ "<Override PartName=\"/word/styles.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml\"/>",
75
+ hasNumbering ? "<Override PartName=\"/word/numbering.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml\"/>" : "",
76
+ `<Override PartName="/word/${DEFAULT_FOOTER_PART_NAME}" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"/>`,
77
+ "<Override PartName=\"/docProps/core.xml\" ContentType=\"application/vnd.openxmlformats-package.core-properties+xml\"/>",
78
+ "<Override PartName=\"/docProps/app.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.extended-properties+xml\"/>",
79
+ "</Types>"
80
+ ].join("");
81
+ const ROOT_RELS_XML = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\"><Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\" Target=\"word/document.xml\"/><Relationship Id=\"rId2\" Type=\"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties\" Target=\"docProps/core.xml\"/><Relationship Id=\"rId3\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties\" Target=\"docProps/app.xml\"/></Relationships>";
82
+ const documentRelsXml = (hasNumbering) => [
83
+ "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>",
84
+ "<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">",
85
+ "<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\" Target=\"styles.xml\"/>",
86
+ hasNumbering ? "<Relationship Id=\"rId2\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering\" Target=\"numbering.xml\"/>" : "",
87
+ `<Relationship Id="${DEFAULT_FOOTER_REL_ID}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" Target="${DEFAULT_FOOTER_PART_NAME}"/>`,
88
+ "</Relationships>"
89
+ ].join("");
90
+ const APP_XML = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><Properties xmlns=\"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties\" xmlns:vt=\"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes\"><Application>stella</Application></Properties>";
91
+ const serializeDocumentToDocx = async (document, options = {}) => {
92
+ const zip = new JSZip();
93
+ const hasNumbering = hasNumberingDefinitions(document.package.numbering);
94
+ zip.file("[Content_Types].xml", contentTypesXml(hasNumbering));
95
+ zip.folder("_rels")?.file(".rels", ROOT_RELS_XML);
96
+ zip.folder("docProps")?.file("app.xml", APP_XML);
97
+ zip.folder("docProps")?.file("core.xml", serializeCoreProperties(document));
98
+ const word = zip.folder("word");
99
+ word?.file("document.xml", serializeDocumentXml(document));
100
+ word?.file("styles.xml", serializeStyles(document.package.styles));
101
+ if (hasNumbering) word?.file("numbering.xml", serializeNumbering(document.package.numbering));
102
+ word?.file(DEFAULT_FOOTER_PART_NAME, buildFooterXml(options.language));
103
+ word?.folder("_rels")?.file("document.xml.rels", documentRelsXml(hasNumbering));
104
+ return await zip.generateAsync({
105
+ type: "arraybuffer",
106
+ compression: "DEFLATE",
107
+ compressionOptions: { level: 6 }
108
+ });
109
+ };
110
+ const hasNumberingDefinitions = (numbering) => (numbering?.abstractNums.length ?? 0) > 0 || (numbering?.nums.length ?? 0) > 0;
111
+ const serializeDocumentXml = (document) => {
112
+ const body = document.package.document;
113
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><w:body>${body.content.map(serializeBlock).join("")}${serializeSectionProperties(body.finalSectionProperties ?? {})}</w:body></w:document>`;
114
+ };
115
+ const serializeBlock = (block) => {
116
+ switch (block.type) {
117
+ case "paragraph": return serializeParagraph(block);
118
+ case "table": return serializeTable(block);
119
+ case "blockSdt": return "";
120
+ default: return "";
121
+ }
122
+ };
123
+ const serializeParagraph = (paragraph) => {
124
+ return `<w:p>${serializeParagraphProperties(paragraph)}${paragraph.content.map(serializeParagraphContent).join("")}</w:p>`;
125
+ };
126
+ const serializeParagraphProperties = (paragraph) => {
127
+ const formatting = paragraph.formatting;
128
+ if (!formatting && !paragraph.sectionProperties) return "";
129
+ const parts = [];
130
+ if (formatting?.styleId) parts.push(`<w:pStyle w:val="${escapeXml(formatting.styleId)}"/>`);
131
+ if (formatting?.keepNext) parts.push("<w:keepNext/>");
132
+ if (formatting?.keepLines) parts.push("<w:keepLines/>");
133
+ if (formatting?.pageBreakBefore) parts.push("<w:pageBreakBefore/>");
134
+ if (formatting?.numPr?.numId !== void 0) parts.push(`<w:numPr><w:ilvl w:val="${formatting.numPr.ilvl}"/><w:numId w:val="${formatting.numPr.numId}"/></w:numPr>`);
135
+ if (formatting?.spaceBefore !== void 0 || formatting?.spaceAfter !== void 0 || formatting?.lineSpacing !== void 0) parts.push(`<w:spacing${attr("w:before", formatting.spaceBefore)}${attr("w:after", formatting.spaceAfter)}${attr("w:line", formatting.lineSpacing)}${attr("w:lineRule", formatting.lineSpacingRule)}/>`);
136
+ if (formatting?.indentLeft !== void 0 || formatting?.indentRight !== void 0 || formatting?.indentFirstLine !== void 0) parts.push(`<w:ind${attr("w:left", formatting.indentLeft)}${attr("w:right", formatting.indentRight)}${attr(formatting.hangingIndent ? "w:hanging" : "w:firstLine", formatting.indentFirstLine)}/>`);
137
+ if (formatting?.alignment) parts.push(`<w:jc w:val="${formatting.alignment}"/>`);
138
+ if (paragraph.sectionProperties) parts.push(serializeSectionProperties(paragraph.sectionProperties));
139
+ return `<w:pPr>${parts.join("")}</w:pPr>`;
140
+ };
141
+ const serializeParagraphContent = (content) => {
142
+ switch (content.type) {
143
+ case "run": return serializeRun(content);
144
+ case "hyperlink":
145
+ case "bookmarkStart":
146
+ case "bookmarkEnd":
147
+ case "simpleField":
148
+ case "complexField":
149
+ case "inlineSdt":
150
+ case "commentRangeStart":
151
+ case "commentRangeEnd":
152
+ case "commentReference":
153
+ case "insertion":
154
+ case "deletion":
155
+ case "moveFrom":
156
+ case "moveTo":
157
+ case "moveFromRangeStart":
158
+ case "moveFromRangeEnd":
159
+ case "moveToRangeStart":
160
+ case "moveToRangeEnd":
161
+ case "mathEquation": return "";
162
+ default: return "";
163
+ }
164
+ };
165
+ const serializeRun = (run) => {
166
+ return `<w:r>${serializeRunProperties(run.formatting)}${run.content.map(serializeRunContent).join("")}</w:r>`;
167
+ };
168
+ const serializeRunProperties = (formatting) => {
169
+ if (!formatting) return "";
170
+ const parts = [];
171
+ if (formatting.styleId) parts.push(`<w:rStyle w:val="${escapeXml(formatting.styleId)}"/>`);
172
+ if (formatting.bold) parts.push("<w:b/>");
173
+ if (formatting.italic) parts.push("<w:i/>");
174
+ if (formatting.allCaps) parts.push("<w:caps/>");
175
+ if (formatting.smallCaps) parts.push("<w:smallCaps/>");
176
+ if (formatting.highlight) parts.push(`<w:highlight w:val="${formatting.highlight}"/>`);
177
+ if (formatting.fontSize !== void 0) parts.push(`<w:sz w:val="${formatting.fontSize}"/>`);
178
+ if (formatting.fontFamily) parts.push(`<w:rFonts${attr("w:ascii", formatting.fontFamily.ascii)}${attr("w:hAnsi", formatting.fontFamily.hAnsi)}${attr("w:cs", formatting.fontFamily.cs)}/>`);
179
+ return parts.length > 0 ? `<w:rPr>${parts.join("")}</w:rPr>` : "";
180
+ };
181
+ const serializeRunContent = (content) => {
182
+ switch (content.type) {
183
+ case "text": return `<w:t${content.preserveSpace ? " xml:space=\"preserve\"" : ""}>${escapeXml(content.text)}</w:t>`;
184
+ case "tab": return "<w:tab/>";
185
+ case "break": return `<w:br${attr("w:type", content.breakType)}/>`;
186
+ case "symbol": return `<w:sym w:font="${escapeXml(content.font)}" w:char="${escapeXml(content.char)}"/>`;
187
+ case "footnoteRef": return `<w:footnoteReference w:id="${content.id}"/>`;
188
+ case "endnoteRef": return `<w:endnoteReference w:id="${content.id}"/>`;
189
+ case "fieldChar":
190
+ case "instrText":
191
+ case "softHyphen":
192
+ case "noBreakHyphen":
193
+ case "drawing":
194
+ case "shape": return "";
195
+ default: return "";
196
+ }
197
+ };
198
+ const serializeTable = (table) => {
199
+ const columnCount = Math.max(0, ...table.rows.map((row) => row.cells.length));
200
+ const gridWidth = columnCount > 0 ? Math.max(1, Math.floor(9e3 / columnCount)) : 9e3;
201
+ const grid = columnCount > 0 ? `<w:tblGrid>${Array.from({ length: columnCount }, () => `<w:gridCol w:w="${gridWidth}"/>`).join("")}</w:tblGrid>` : "<w:tblGrid/>";
202
+ const tableBorderXml = renderTableBorders(table);
203
+ const cellBorderXml = renderCellBorders(table);
204
+ return [
205
+ "<w:tbl>",
206
+ "<w:tblPr><w:tblW w:w=\"0\" w:type=\"auto\"/><w:tblBorders>",
207
+ tableBorderXml,
208
+ "</w:tblBorders></w:tblPr>",
209
+ grid,
210
+ table.rows.map((row) => `<w:tr>${row.cells.map((cell) => `<w:tc><w:tcPr><w:tcW w:w="0" w:type="auto"/><w:tcBorders>${cellBorderXml}</w:tcBorders></w:tcPr>${cell.content.map(serializeBlock).join("")}</w:tc>`).join("")}</w:tr>`).join(""),
211
+ "</w:tbl>"
212
+ ].join("");
213
+ };
214
+ const renderTableBorders = (table) => {
215
+ const borders = table.formatting?.borders;
216
+ if (!borders) return defaultBorderXml();
217
+ return [
218
+ borderEdgeXml("w:top", borders.top),
219
+ borderEdgeXml("w:left", borders.left),
220
+ borderEdgeXml("w:bottom", borders.bottom),
221
+ borderEdgeXml("w:right", borders.right),
222
+ borderEdgeXml("w:insideH", borders.insideH),
223
+ borderEdgeXml("w:insideV", borders.insideV)
224
+ ].join("");
225
+ };
226
+ const renderCellBorders = (table) => {
227
+ const borders = table.formatting?.borders;
228
+ if (!borders) return defaultBorderXml();
229
+ return [
230
+ borderEdgeXml("w:top", borders.top),
231
+ borderEdgeXml("w:left", borders.left),
232
+ borderEdgeXml("w:bottom", borders.bottom),
233
+ borderEdgeXml("w:right", borders.right)
234
+ ].join("");
235
+ };
236
+ const borderEdgeXml = (tag, spec) => {
237
+ if (!spec) return `<${tag} w:val="nil"/>`;
238
+ if (spec.style === "none" || spec.style === "nil") return `<${tag} w:val="nil"/>`;
239
+ const sz = spec.size ?? 4;
240
+ const color = spec.color?.rgb ?? "CCCCCC";
241
+ return `<${tag} w:val="${escapeXml(spec.style)}" w:sz="${sz}" w:space="0" w:color="${escapeXml(color)}"/>`;
242
+ };
243
+ const defaultBorderXml = () => "<w:top w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"CCCCCC\"/><w:left w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"CCCCCC\"/><w:bottom w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"CCCCCC\"/><w:right w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"CCCCCC\"/><w:insideH w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"CCCCCC\"/><w:insideV w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"CCCCCC\"/>";
244
+ const serializeSectionProperties = (properties) => `<w:sectPr><w:footerReference w:type="default" r:id="${DEFAULT_FOOTER_REL_ID}"/><w:pgSz${attr("w:w", properties.pageWidth)}${attr("w:h", properties.pageHeight)}${attr("w:orient", properties.orientation === "landscape" ? "landscape" : void 0)}/><w:pgMar${attr("w:top", properties.marginTop)}${attr("w:right", properties.marginRight)}${attr("w:bottom", properties.marginBottom)}${attr("w:left", properties.marginLeft)}${attr("w:header", properties.headerDistance)}${attr("w:footer", properties.footerDistance)}${attr("w:gutter", properties.gutter ?? 0)}/></w:sectPr>`;
245
+ const serializeStyles = (styles) => {
246
+ const styleDefinitions = styles ?? { styles: [] };
247
+ return [
248
+ "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>",
249
+ "<w:styles xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\">",
250
+ serializeDocDefaults(styleDefinitions),
251
+ styleDefinitions.styles.map(serializeStyle).join(""),
252
+ "</w:styles>"
253
+ ].join("");
254
+ };
255
+ const serializeDocDefaults = (styles) => {
256
+ const rPr = serializeRunProperties(styles.docDefaults?.rPr);
257
+ const pPrParts = [];
258
+ const pPr = styles.docDefaults?.pPr;
259
+ if (pPr?.spaceAfter !== void 0 || pPr?.lineSpacing !== void 0) pPrParts.push(`<w:spacing${attr("w:after", pPr.spaceAfter)}${attr("w:line", pPr.lineSpacing)}/>`);
260
+ return `<w:docDefaults><w:rPrDefault>${rPr}</w:rPrDefault><w:pPrDefault><w:pPr>${pPrParts.join("")}</w:pPr></w:pPrDefault></w:docDefaults>`;
261
+ };
262
+ const serializeStyle = (style) => {
263
+ const pPr = style.pPr ? serializeParagraphProperties({
264
+ type: "paragraph",
265
+ formatting: style.pPr,
266
+ content: []
267
+ }).replace(/^<w:pPr>|<\/w:pPr>$/gu, "") : "";
268
+ const rPr = serializeRunProperties(style.rPr);
269
+ return [
270
+ `<w:style w:type="${style.type}" w:styleId="${escapeXml(style.styleId)}"${style.default ? " w:default=\"1\"" : ""}>`,
271
+ `<w:name w:val="${escapeXml(style.name ?? style.styleId)}"/>`,
272
+ style.basedOn ? `<w:basedOn w:val="${escapeXml(style.basedOn)}"/>` : "",
273
+ style.next ? `<w:next w:val="${escapeXml(style.next)}"/>` : "",
274
+ style.qFormat ? "<w:qFormat/>" : "",
275
+ pPr ? `<w:pPr>${pPr}</w:pPr>` : "",
276
+ rPr,
277
+ "</w:style>"
278
+ ].join("");
279
+ };
280
+ const serializeNumbering = (numbering) => [
281
+ "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>",
282
+ "<w:numbering xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\">",
283
+ (numbering?.abstractNums ?? []).map((abstractNum) => [
284
+ `<w:abstractNum w:abstractNumId="${abstractNum.abstractNumId}">`,
285
+ `<w:nsid w:val="${abstractNumberingNsid(abstractNum.abstractNumId)}"/>`,
286
+ abstractNum.multiLevelType ? `<w:multiLevelType w:val="${abstractNum.multiLevelType}"/>` : "",
287
+ abstractNum.levels.map(serializeNumberingLevel).join(""),
288
+ "</w:abstractNum>"
289
+ ].join("")).join(""),
290
+ (numbering?.nums ?? []).map((num) => `<w:num w:numId="${num.numId}"><w:abstractNumId w:val="${num.abstractNumId}"/></w:num>`).join(""),
291
+ "</w:numbering>"
292
+ ].join("");
293
+ const serializeNumberingLevel = (level) => [
294
+ `<w:lvl w:ilvl="${level.ilvl}">`,
295
+ `<w:start w:val="${level.start ?? 1}"/>`,
296
+ `<w:numFmt w:val="${level.numFmt}"/>`,
297
+ level.isLgl ? "<w:isLgl/>" : "",
298
+ level.suffix ? `<w:suff w:val="${level.suffix}"/>` : "",
299
+ `<w:lvlText w:val="${escapeXml(level.lvlText)}"/>`,
300
+ `<w:lvlJc w:val="${level.lvlJc ?? "left"}"/>`,
301
+ level.pPr ? serializeParagraphProperties({
302
+ type: "paragraph",
303
+ formatting: level.pPr,
304
+ content: []
305
+ }) : "",
306
+ serializeRunProperties(level.rPr),
307
+ "</w:lvl>"
308
+ ].join("");
309
+ const abstractNumberingNsid = (abstractNumId) => abstractNumId.toString(16).toUpperCase().padStart(8, "0").slice(-8);
310
+ const serializeCoreProperties = (document) => {
311
+ const properties = document.package.properties;
312
+ const now = (/* @__PURE__ */ new Date()).toISOString();
313
+ const title = properties?.title ?? "";
314
+ const creator = properties?.creator ?? "Stella";
315
+ const created = properties?.created?.toISOString() ?? now;
316
+ const modified = properties?.modified?.toISOString() ?? now;
317
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dc:title>${escapeXml(title)}</dc:title><dc:creator>${escapeXml(creator)}</dc:creator><cp:lastModifiedBy>${escapeXml(creator)}</cp:lastModifiedBy><dcterms:created xsi:type="dcterms:W3CDTF">${created}</dcterms:created><dcterms:modified xsi:type="dcterms:W3CDTF">${modified}</dcterms:modified></cp:coreProperties>`;
318
+ };
319
+ //#endregion
320
+ //#region src/validate/docx.ts
321
+ const validateDocxPackage = async (buffer) => {
322
+ try {
323
+ const zip = await JSZip.loadAsync(buffer);
324
+ for (const requiredPath of [
325
+ "[Content_Types].xml",
326
+ "_rels/.rels",
327
+ "word/document.xml",
328
+ "word/styles.xml",
329
+ "word/_rels/document.xml.rels"
330
+ ]) if (!zip.file(requiredPath)) return {
331
+ valid: false,
332
+ error: `Generated DOCX is missing required package part: ${requiredPath}`
333
+ };
334
+ if (!(await zip.file("word/document.xml")?.async("string"))?.includes("<w:document")) return {
335
+ valid: false,
336
+ error: "Generated DOCX has no word/document.xml root document."
337
+ };
338
+ if ((await zip.file("word/_rels/document.xml.rels")?.async("string"))?.includes("/relationships/numbering") && !zip.file("word/numbering.xml")) return {
339
+ valid: false,
340
+ error: "Generated DOCX references numbering.xml but does not include it."
341
+ };
342
+ return { valid: true };
343
+ } catch (error) {
344
+ return {
345
+ valid: false,
346
+ error: error instanceof Error ? error.message : "Invalid DOCX package."
347
+ };
348
+ }
349
+ };
350
+ const validateDocumentModel = (document) => {
351
+ const ctx = createValidationContext(document);
352
+ validateBlocks(document.package.document.content, "package.document.content", ctx);
353
+ validateHeaderFooterMap(document.package.headers, "package.headers", ctx);
354
+ validateHeaderFooterMap(document.package.footers, "package.footers", ctx);
355
+ validateComments(document.package.document.comments ?? [], ctx);
356
+ validateNotes(document, ctx);
357
+ validateMedia(document, ctx);
358
+ validateCounterPairs(ctx.commentRangeStarts, ctx.commentRangeEnds, {
359
+ label: "comment range",
360
+ startName: "commentRangeStart",
361
+ endName: "commentRangeEnd",
362
+ ctx
363
+ });
364
+ validateCounterPairs(ctx.bookmarkStarts, ctx.bookmarkEnds, {
365
+ label: "bookmark",
366
+ startName: "bookmarkStart",
367
+ endName: "bookmarkEnd",
368
+ ctx,
369
+ severity: "warning"
370
+ });
371
+ validateCounterPairs(ctx.moveFromStarts, ctx.moveFromEnds, {
372
+ label: "move-from range",
373
+ startName: "moveFromRangeStart",
374
+ endName: "moveFromRangeEnd",
375
+ ctx
376
+ });
377
+ validateCounterPairs(ctx.moveToStarts, ctx.moveToEnds, {
378
+ label: "move-to range",
379
+ startName: "moveToRangeStart",
380
+ endName: "moveToRangeEnd",
381
+ ctx
382
+ });
383
+ return {
384
+ valid: !ctx.issues.some((issue) => issue.severity === "error"),
385
+ issues: ctx.issues
386
+ };
387
+ };
388
+ const assertValidDocumentModel = (document) => {
389
+ const result = validateDocumentModel(document);
390
+ if (result.valid) return;
391
+ panic(`Invalid DOCX document model:\n${result.issues.flatMap((issue) => issue.severity === "error" ? [`${issue.path}: ${issue.message}`] : []).join("\n")}`);
392
+ };
393
+ const createValidationContext = (document) => ({
394
+ issues: [],
395
+ commentIds: new Set((document.package.document.comments ?? []).map((comment) => comment.id)),
396
+ referencedCommentIds: /* @__PURE__ */ new Map(),
397
+ commentRangeStarts: /* @__PURE__ */ new Map(),
398
+ commentRangeEnds: /* @__PURE__ */ new Map(),
399
+ bookmarkStarts: /* @__PURE__ */ new Map(),
400
+ bookmarkEnds: /* @__PURE__ */ new Map(),
401
+ moveFromStarts: /* @__PURE__ */ new Map(),
402
+ moveFromEnds: /* @__PURE__ */ new Map(),
403
+ moveToStarts: /* @__PURE__ */ new Map(),
404
+ moveToEnds: /* @__PURE__ */ new Map(),
405
+ noteRefs: {
406
+ footnote: /* @__PURE__ */ new Map(),
407
+ endnote: /* @__PURE__ */ new Map()
408
+ },
409
+ paraIds: /* @__PURE__ */ new Map(),
410
+ numberingNums: new Set((document.package.numbering?.nums ?? []).map((num) => num.numId)),
411
+ headers: document.package.headers,
412
+ footers: document.package.footers
413
+ });
414
+ const addIssue = (ctx, issue) => {
415
+ ctx.issues.push(issue);
416
+ };
417
+ const addError = (ctx, path, message) => {
418
+ addIssue(ctx, {
419
+ path,
420
+ message,
421
+ severity: "error"
422
+ });
423
+ };
424
+ const addWarning = (ctx, path, message) => {
425
+ addIssue(ctx, {
426
+ path,
427
+ message,
428
+ severity: "warning"
429
+ });
430
+ };
431
+ const increment = (map, id) => {
432
+ map.set(id, (map.get(id) ?? 0) + 1);
433
+ };
434
+ const recordPath = (map, id, path) => {
435
+ const existing = map.get(id);
436
+ if (existing) {
437
+ existing.push(path);
438
+ return;
439
+ }
440
+ map.set(id, [path]);
441
+ };
442
+ const validateBlocks = (blocks, path, ctx) => {
443
+ for (const [index, block] of blocks.entries()) validateBlock(block, `${path}[${index}]`, ctx);
444
+ };
445
+ const validateBlock = (block, path, ctx) => {
446
+ if (block.type === "paragraph") {
447
+ validateParagraph(block, path, ctx);
448
+ return;
449
+ }
450
+ if (block.type === "table") {
451
+ validateTable(block, path, ctx);
452
+ return;
453
+ }
454
+ if (block.content.length === 0) {
455
+ addWarning(ctx, `${path}.content`, "Block content control is empty.");
456
+ return;
457
+ }
458
+ validateBlocks(block.content, `${path}.content`, ctx);
459
+ };
460
+ const validateParagraph = (paragraph, path, ctx) => {
461
+ if (paragraph.paraId) {
462
+ const existingPath = ctx.paraIds.get(paragraph.paraId);
463
+ if (existingPath) addWarning(ctx, `${path}.paraId`, `Duplicate paragraph id also appears at ${existingPath}.`);
464
+ else ctx.paraIds.set(paragraph.paraId, path);
465
+ }
466
+ validateNumbering(paragraph, path, ctx);
467
+ if (paragraph.sectionProperties) validateSectionProperties(paragraph.sectionProperties, `${path}.sectionProperties`, ctx);
468
+ for (const [index, content] of paragraph.content.entries()) validateParagraphContent(content, `${path}.content[${index}]`, ctx);
469
+ };
470
+ const validateParagraphContent = (content, path, ctx) => {
471
+ if (content.type === "run") {
472
+ validateRun(content, path, ctx);
473
+ return;
474
+ }
475
+ if (content.type === "hyperlink") {
476
+ validateHyperlink(content, path, ctx);
477
+ return;
478
+ }
479
+ if (content.type === "simpleField") {
480
+ for (const [index, child] of content.content.entries()) validateFieldChild(child, `${path}.content[${index}]`, ctx);
481
+ return;
482
+ }
483
+ if (content.type === "complexField") {
484
+ validateRuns(content.fieldCode, `${path}.fieldCode`, ctx);
485
+ validateRuns(content.fieldResult, `${path}.fieldResult`, ctx);
486
+ return;
487
+ }
488
+ if (content.type === "inlineSdt") {
489
+ for (const [index, child] of content.content.entries()) validateParagraphContent(child, `${path}.content[${index}]`, ctx);
490
+ return;
491
+ }
492
+ if (content.type === "commentRangeStart") {
493
+ increment(ctx.commentRangeStarts, content.id);
494
+ recordPath(ctx.referencedCommentIds, content.id, path);
495
+ return;
496
+ }
497
+ if (content.type === "commentRangeEnd") {
498
+ increment(ctx.commentRangeEnds, content.id);
499
+ recordPath(ctx.referencedCommentIds, content.id, path);
500
+ return;
501
+ }
502
+ if (content.type === "commentReference") {
503
+ recordPath(ctx.referencedCommentIds, content.id, path);
504
+ return;
505
+ }
506
+ if (content.type === "insertion" || content.type === "deletion" || content.type === "moveFrom" || content.type === "moveTo") {
507
+ validateTrackedRunChange(content, path, ctx);
508
+ return;
509
+ }
510
+ if (content.type === "bookmarkStart") {
511
+ increment(ctx.bookmarkStarts, content.id);
512
+ return;
513
+ }
514
+ if (content.type === "bookmarkEnd") {
515
+ increment(ctx.bookmarkEnds, content.id);
516
+ return;
517
+ }
518
+ if (content.type === "moveFromRangeStart") {
519
+ increment(ctx.moveFromStarts, content.id);
520
+ return;
521
+ }
522
+ if (content.type === "moveFromRangeEnd") {
523
+ increment(ctx.moveFromEnds, content.id);
524
+ return;
525
+ }
526
+ if (content.type === "moveToRangeStart") {
527
+ increment(ctx.moveToStarts, content.id);
528
+ return;
529
+ }
530
+ if (content.type === "moveToRangeEnd") {
531
+ increment(ctx.moveToEnds, content.id);
532
+ return;
533
+ }
534
+ if (content.ommlXml.trim() === "") addError(ctx, `${path}.ommlXml`, "Math equation must preserve OMML XML.");
535
+ };
536
+ const validateFieldChild = (child, path, ctx) => {
537
+ if (child.type === "run") {
538
+ validateRun(child, path, ctx);
539
+ return;
540
+ }
541
+ validateHyperlink(child, path, ctx);
542
+ };
543
+ const validateRuns = (runs, path, ctx) => {
544
+ for (const [index, run] of runs.entries()) validateRun(run, `${path}[${index}]`, ctx);
545
+ };
546
+ const validateRun = (run, path, ctx) => {
547
+ for (const [index, content] of run.content.entries()) validateRunContent(content, `${path}.content[${index}]`, ctx);
548
+ };
549
+ const validateRunContent = (content, path, ctx) => {
550
+ if (content.type === "drawing") {
551
+ validateImage(content.image, `${path}.image`, ctx, { hasPreservedRawDrawing: content.rawXml?.trim() !== "" });
552
+ return;
553
+ }
554
+ if (content.type === "shape") {
555
+ validateShape(content.shape, `${path}.shape`, ctx);
556
+ return;
557
+ }
558
+ if (content.type === "footnoteRef" || content.type === "endnoteRef") {
559
+ const noteType = content.type === "footnoteRef" ? "footnote" : "endnote";
560
+ recordPath(ctx.noteRefs[noteType], content.id, path);
561
+ return;
562
+ }
563
+ if (content.type === "symbol" && content.char.trim() === "") addError(ctx, `${path}.char`, "Symbol content must include a character.");
564
+ };
565
+ const validateHyperlink = (hyperlink, path, ctx) => {
566
+ if (!hyperlink.rId && !hyperlink.href && !hyperlink.anchor) addWarning(ctx, path, "Hyperlink has no relationship, href, or internal anchor.");
567
+ for (const [index, child] of hyperlink.children.entries()) validateHyperlinkChild(child, `${path}.children[${index}]`, ctx);
568
+ };
569
+ const validateHyperlinkChild = (child, path, ctx) => {
570
+ if (child.type === "run") {
571
+ validateRun(child, path, ctx);
572
+ return;
573
+ }
574
+ if (child.type === "bookmarkStart") {
575
+ increment(ctx.bookmarkStarts, child.id);
576
+ return;
577
+ }
578
+ increment(ctx.bookmarkEnds, child.id);
579
+ };
580
+ const validateTrackedRunChange = (change, path, ctx) => {
581
+ if (change.info.author.trim() === "") addError(ctx, `${path}.info.author`, "Tracked change author is empty.");
582
+ for (const [index, child] of change.content.entries()) validateFieldChild(child, `${path}.content[${index}]`, ctx);
583
+ };
584
+ const validateImage = (image, path, ctx, options = {}) => {
585
+ if (!image.rId && !image.src?.startsWith("data:") && !options.hasPreservedRawDrawing) addError(ctx, `${path}.rId`, "Image must have a relationship id.");
586
+ validateNonNegativeSize(image.size.width, `${path}.size.width`, ctx);
587
+ validateNonNegativeSize(image.size.height, `${path}.size.height`, ctx);
588
+ };
589
+ const validateShape = (shape, path, ctx) => {
590
+ validateNonNegativeSize(shape.size.width, `${path}.size.width`, ctx);
591
+ validateNonNegativeSize(shape.size.height, `${path}.size.height`, ctx);
592
+ if (shape.textBody) validateParagraphs(shape.textBody.content, `${path}.textBody.content`, ctx);
593
+ };
594
+ const validateNonNegativeSize = (value, path, ctx) => {
595
+ if (value < 0) {
596
+ addError(ctx, path, "Size must be zero or greater.");
597
+ return;
598
+ }
599
+ if (value === 0) addWarning(ctx, path, "Size is zero; the drawing may be invisible.");
600
+ };
601
+ const validateTable = (table, path, ctx) => {
602
+ if (table.rows.length === 0) {
603
+ addError(ctx, `${path}.rows`, "Table must contain at least one row.");
604
+ return;
605
+ }
606
+ for (const [index, row] of table.rows.entries()) validateTableRow(row, `${path}.rows[${index}]`, ctx);
607
+ };
608
+ const validateTableRow = (row, path, ctx) => {
609
+ if (row.cells.length === 0) {
610
+ addError(ctx, `${path}.cells`, "Table row must contain at least one cell.");
611
+ return;
612
+ }
613
+ for (const [index, cell] of row.cells.entries()) validateTableCell(cell, `${path}.cells[${index}]`, ctx);
614
+ };
615
+ const validateTableCell = (cell, path, ctx) => {
616
+ if (cell.content.length === 0) {
617
+ addError(ctx, `${path}.content`, "Table cell must contain block content.");
618
+ return;
619
+ }
620
+ validateBlocks(cell.content, `${path}.content`, ctx);
621
+ };
622
+ const validateParagraphs = (paragraphs, path, ctx) => {
623
+ for (const [index, paragraph] of paragraphs.entries()) validateParagraph(paragraph, `${path}[${index}]`, ctx);
624
+ };
625
+ const validateNumbering = (paragraph, path, ctx) => {
626
+ const numPr = paragraph.formatting?.numPr;
627
+ if (!numPr) return;
628
+ const ilvl = numPr.ilvl ?? 0;
629
+ if (ilvl < 0) addError(ctx, `${path}.formatting.numPr.ilvl`, "List level must be zero or greater.");
630
+ else if (ilvl > 8) addWarning(ctx, `${path}.formatting.numPr.ilvl`, "List level is outside Word's standard 0-8 range.");
631
+ if (numPr.numId === void 0 || numPr.numId === 0) return;
632
+ if (!ctx.numberingNums.has(numPr.numId)) addError(ctx, `${path}.formatting.numPr.numId`, `Numbering definition ${numPr.numId} is missing.`);
633
+ };
634
+ const validateSectionProperties = (section, path, ctx) => {
635
+ validateHeaderFooterReferences(section.headerReferences ?? [], ctx.headers, `${path}.headerReferences`, "header", ctx);
636
+ validateHeaderFooterReferences(section.footerReferences ?? [], ctx.footers, `${path}.footerReferences`, "footer", ctx);
637
+ };
638
+ const validateHeaderFooterReferences = (refs, map, path, label, ctx) => {
639
+ for (const [index, ref] of refs.entries()) if (!map?.has(ref.rId)) addError(ctx, `${path}[${index}].rId`, `Section references missing ${label} ${ref.rId}.`);
640
+ };
641
+ const validateHeaderFooterMap = (map, path, ctx) => {
642
+ if (!map) return;
643
+ for (const [rId, headerFooter] of map.entries()) validateBlocks(headerFooter.content, `${path}.${rId}.content`, ctx);
644
+ };
645
+ const validateComments = (comments, ctx) => {
646
+ const seen = /* @__PURE__ */ new Set();
647
+ for (const [index, comment] of comments.entries()) {
648
+ const path = `package.document.comments[${index}]`;
649
+ if (seen.has(comment.id)) addError(ctx, `${path}.id`, `Duplicate comment id ${comment.id}.`);
650
+ seen.add(comment.id);
651
+ if (comment.author.trim() === "") addError(ctx, `${path}.author`, "Comment author is empty.");
652
+ if (comment.parentId !== void 0 && !ctx.commentIds.has(comment.parentId)) addError(ctx, `${path}.parentId`, `Parent comment ${comment.parentId} is missing.`);
653
+ validateParagraphs(comment.content, `${path}.content`, ctx);
654
+ }
655
+ for (const [id, paths] of ctx.referencedCommentIds.entries()) if (!ctx.commentIds.has(id)) addError(ctx, paths.at(0) ?? "package.document", `Comment ${id} is referenced but not present in comments.xml.`);
656
+ };
657
+ const validateNotes = (document, ctx) => {
658
+ validateNoteCollection({
659
+ refs: ctx.noteRefs.footnote,
660
+ notes: document.package.footnotes ?? [],
661
+ path: "package.footnotes",
662
+ label: "Footnote",
663
+ ctx
664
+ });
665
+ validateNoteCollection({
666
+ refs: ctx.noteRefs.endnote,
667
+ notes: document.package.endnotes ?? [],
668
+ path: "package.endnotes",
669
+ label: "Endnote",
670
+ ctx
671
+ });
672
+ };
673
+ const validateNoteCollection = ({ refs, notes, path, label, ctx }) => {
674
+ const ids = /* @__PURE__ */ new Set();
675
+ for (const [index, note] of notes.entries()) {
676
+ if (ids.has(note.id)) addError(ctx, `${path}[${index}].id`, `Duplicate ${label} id ${note.id}.`);
677
+ ids.add(note.id);
678
+ validateBlocks(note.content, `${path}[${index}].content`, ctx);
679
+ }
680
+ for (const [id, paths] of refs.entries()) if (!ids.has(id)) addError(ctx, paths.at(0) ?? path, `${label} ${id} is referenced but not present in the package.`);
681
+ };
682
+ const validateMedia = (document, ctx) => {
683
+ const media = document.package.media;
684
+ if (!media) return;
685
+ for (const [path, file] of media.entries()) {
686
+ const issuePath = `package.media.${path}`;
687
+ if (file.path.trim() === "") addError(ctx, `${issuePath}.path`, "Media file path is empty.");
688
+ if (file.mimeType.trim() === "") addError(ctx, `${issuePath}.mimeType`, "Media MIME type is empty.");
689
+ if (file.data.byteLength === 0) addWarning(ctx, `${issuePath}.data`, "Media file has no binary data.");
690
+ }
691
+ };
692
+ const validateCounterPairs = (starts, ends, { label, startName, endName, ctx, severity = "error" }) => {
693
+ const ids = /* @__PURE__ */ new Set([...starts.keys(), ...ends.keys()]);
694
+ for (const id of ids) {
695
+ const startCount = starts.get(id) ?? 0;
696
+ const endCount = ends.get(id) ?? 0;
697
+ if (startCount === endCount) continue;
698
+ const message = `Unbalanced ${label} ${id}: ${startName}=${startCount}, ${endName}=${endCount}.`;
699
+ if (severity === "warning") {
700
+ addWarning(ctx, "package.document", message);
701
+ continue;
702
+ }
703
+ addError(ctx, "package.document", message);
704
+ }
705
+ };
706
+ //#endregion
707
+ //#region src/legal-source/parser.ts
708
+ const DEFAULT_KIND = "agreement";
709
+ const DEFAULT_LOCALE = "en-GB";
710
+ const DEFAULT_NUMBERING = "legal";
711
+ const DEFAULT_PAGE_SIZE = "A4";
712
+ const DEFAULT_ORIENTATION = "portrait";
713
+ const DIRECTIVE_ALIASES = {
714
+ "@annex": "@schedule",
715
+ "@appendix": "@schedule",
716
+ "@body": "@paragraph",
717
+ "@para": "@paragraph",
718
+ "@preamble": "@paragraph",
719
+ "@section": "@clause",
720
+ "@signature": "@signatures",
721
+ "@subsection": "@subclause"
722
+ };
723
+ const DIRECTIVES = /* @__PURE__ */ new Set([
724
+ "@doc",
725
+ "@title",
726
+ "@recital",
727
+ "@clause",
728
+ "@subclause",
729
+ "@paragraph",
730
+ "@list",
731
+ "@table",
732
+ "@schedule",
733
+ "@signatures",
734
+ "@pagebreak"
735
+ ]);
736
+ const parseLegalSource = (source, options = {}) => {
737
+ const fixes = [];
738
+ const diagnostics = [];
739
+ const blocks = [];
740
+ const meta = {
741
+ kind: DEFAULT_KIND,
742
+ locale: DEFAULT_LOCALE,
743
+ numbering: DEFAULT_NUMBERING,
744
+ page: {
745
+ size: DEFAULT_PAGE_SIZE,
746
+ orientation: DEFAULT_ORIENTATION
747
+ },
748
+ title: null
749
+ };
750
+ let pending = null;
751
+ const pushPending = () => {
752
+ if (!pending) return;
753
+ const block = pendingToBlock(pending, diagnostics, fixes);
754
+ if (block) {
755
+ blocks.push(block);
756
+ if (block.type === "title") meta.title = block.text;
757
+ }
758
+ pending = null;
759
+ };
760
+ const lines = source.replace(/\r\n?/gu, "\n").split("\n");
761
+ for (const [index, rawLine] of lines.entries()) {
762
+ const lineNumber = index + 1;
763
+ const line = rawLine.trimEnd();
764
+ const trimmed = line.trim();
765
+ if (!trimmed) {
766
+ pending?.lines.push("");
767
+ continue;
768
+ }
769
+ const markdownHeading = parseMarkdownHeading(trimmed);
770
+ if (markdownHeading) {
771
+ pushPending();
772
+ const { depth, heading } = markdownHeading;
773
+ if (depth === 1) {
774
+ pending = {
775
+ type: "title",
776
+ line: lineNumber,
777
+ heading,
778
+ lines: []
779
+ };
780
+ pushPending();
781
+ } else pending = {
782
+ type: "clause",
783
+ line: lineNumber,
784
+ level: Math.min(depth - 1, 6),
785
+ heading,
786
+ lines: []
787
+ };
788
+ fixes.push({
789
+ code: "markdown-heading-normalized",
790
+ message: "Converted a Markdown heading into a legal directive.",
791
+ line: lineNumber
792
+ });
793
+ continue;
794
+ }
795
+ if (trimmed.startsWith("@")) {
796
+ const [rawDirective = "", ...rest] = trimmed.split(/\s+/u);
797
+ const canonicalDirective = DIRECTIVE_ALIASES[rawDirective.toLowerCase()] ?? rawDirective.toLowerCase();
798
+ const argument = rest.join(" ").trim();
799
+ if (!DIRECTIVES.has(canonicalDirective)) {
800
+ diagnostics.push({
801
+ code: "unknown-directive",
802
+ message: `Unknown legal directive "${rawDirective}".`,
803
+ severity: "error",
804
+ line: lineNumber
805
+ });
806
+ pending?.lines.push(line);
807
+ continue;
808
+ }
809
+ if (canonicalDirective !== rawDirective.toLowerCase()) fixes.push({
810
+ code: "directive-alias-normalized",
811
+ message: `Normalized ${rawDirective} to ${canonicalDirective}.`,
812
+ line: lineNumber
813
+ });
814
+ pushPending();
815
+ switch (canonicalDirective) {
816
+ case "@doc":
817
+ parseDocDirective(argument, meta, diagnostics, lineNumber);
818
+ break;
819
+ case "@title":
820
+ pending = {
821
+ type: "title",
822
+ line: lineNumber,
823
+ heading: argument,
824
+ lines: []
825
+ };
826
+ pushPending();
827
+ break;
828
+ case "@recital":
829
+ pending = {
830
+ type: "recital",
831
+ line: lineNumber,
832
+ heading: argument,
833
+ lines: []
834
+ };
835
+ break;
836
+ case "@clause":
837
+ pending = {
838
+ type: "clause",
839
+ line: lineNumber,
840
+ level: 1,
841
+ heading: argument,
842
+ lines: []
843
+ };
844
+ break;
845
+ case "@subclause":
846
+ pending = {
847
+ type: "clause",
848
+ line: lineNumber,
849
+ level: 2,
850
+ heading: argument,
851
+ lines: []
852
+ };
853
+ break;
854
+ case "@paragraph":
855
+ pending = {
856
+ type: "paragraph",
857
+ line: lineNumber,
858
+ heading: argument,
859
+ lines: []
860
+ };
861
+ break;
862
+ case "@list":
863
+ pending = {
864
+ type: "list",
865
+ line: lineNumber,
866
+ ordered: /\bordered\b/iu.test(argument),
867
+ heading: argument,
868
+ lines: []
869
+ };
870
+ break;
871
+ case "@table":
872
+ pending = {
873
+ type: "table",
874
+ line: lineNumber,
875
+ heading: argument,
876
+ lines: []
877
+ };
878
+ break;
879
+ case "@schedule":
880
+ pending = {
881
+ type: "schedule",
882
+ line: lineNumber,
883
+ heading: argument,
884
+ lines: []
885
+ };
886
+ break;
887
+ case "@signatures":
888
+ pending = {
889
+ type: "signatures",
890
+ line: lineNumber,
891
+ heading: argument,
892
+ lines: []
893
+ };
894
+ break;
895
+ case "@pagebreak":
896
+ blocks.push({ type: "pageBreak" });
897
+ break;
898
+ default: break;
899
+ }
900
+ continue;
901
+ }
902
+ pending ??= {
903
+ type: "paragraph",
904
+ line: lineNumber,
905
+ heading: "",
906
+ lines: []
907
+ };
908
+ pending.lines.push(line);
909
+ }
910
+ pushPending();
911
+ if (!meta.title) {
912
+ const firstTitle = blocks.find((block) => block.type === "title");
913
+ meta.title = firstTitle?.type === "title" ? firstTitle.text : options.titleFallback ?? "Untitled document";
914
+ }
915
+ return applyDocumentAutofixes({
916
+ diagnostics,
917
+ draft: {
918
+ meta,
919
+ blocks
920
+ },
921
+ fixes
922
+ });
923
+ };
924
+ const parseDocDirective = (argument, meta, diagnostics, line) => {
925
+ const attrs = parseAttributes(argument);
926
+ const kind = attrs.get("kind");
927
+ if (kind !== void 0 && isLegalKind(kind)) meta.kind = kind;
928
+ else if (kind !== void 0) diagnostics.push({
929
+ code: "invalid-doc-attribute",
930
+ message: `Invalid @doc kind "${kind}".`,
931
+ severity: "warning",
932
+ line
933
+ });
934
+ const locale = attrs.get("locale");
935
+ if (locale) meta.locale = locale;
936
+ const numbering = attrs.get("numbering");
937
+ if (numbering !== void 0 && isNumberingProfile(numbering)) meta.numbering = numbering;
938
+ else if (numbering !== void 0) diagnostics.push({
939
+ code: "invalid-doc-attribute",
940
+ message: `Invalid @doc numbering "${numbering}".`,
941
+ severity: "warning",
942
+ line
943
+ });
944
+ const page = attrs.get("page");
945
+ if (page !== void 0 && isPageSize(page)) meta.page.size = page;
946
+ else if (page !== void 0) diagnostics.push({
947
+ code: "invalid-doc-attribute",
948
+ message: `Invalid @doc page "${page}".`,
949
+ severity: "warning",
950
+ line
951
+ });
952
+ const orientation = attrs.get("orientation");
953
+ if (orientation !== void 0 && isPageOrientation(orientation)) meta.page.orientation = orientation;
954
+ else if (orientation !== void 0) diagnostics.push({
955
+ code: "invalid-doc-attribute",
956
+ message: `Invalid @doc orientation "${orientation}".`,
957
+ severity: "warning",
958
+ line
959
+ });
960
+ const title = attrs.get("title");
961
+ if (title) meta.title = title;
962
+ for (const key of attrs.keys()) if (![
963
+ "kind",
964
+ "locale",
965
+ "numbering",
966
+ "page",
967
+ "orientation",
968
+ "title"
969
+ ].includes(key)) diagnostics.push({
970
+ code: "unknown-doc-attribute",
971
+ message: `Unknown @doc attribute "${key}".`,
972
+ severity: "warning",
973
+ line
974
+ });
975
+ };
976
+ const parseAttributes = (value) => {
977
+ const attrs = /* @__PURE__ */ new Map();
978
+ let index = 0;
979
+ while (index < value.length) {
980
+ const attr = readAttribute(value, index);
981
+ if (!attr) {
982
+ index = skipMalformedAttribute(value, index);
983
+ continue;
984
+ }
985
+ attrs.set(attr.key, attr.value);
986
+ index = attr.nextIndex;
987
+ }
988
+ return attrs;
989
+ };
990
+ const readAttribute = (value, startIndex) => {
991
+ const keyStart = skipWhitespace(value, startIndex);
992
+ const keyEnd = readAttributeKeyEnd(value, keyStart);
993
+ const key = value.slice(keyStart, keyEnd).toLowerCase();
994
+ const equalsIndex = skipWhitespace(value, keyEnd);
995
+ if (!key || value.charAt(equalsIndex) !== "=") return null;
996
+ const attributeValue = readAttributeValue(value, skipWhitespace(value, equalsIndex + 1));
997
+ return {
998
+ key,
999
+ value: attributeValue.value,
1000
+ nextIndex: attributeValue.nextIndex
1001
+ };
1002
+ };
1003
+ const skipWhitespace = (value, startIndex) => {
1004
+ let index = startIndex;
1005
+ while (index < value.length && isWhitespace(value.charAt(index))) index++;
1006
+ return index;
1007
+ };
1008
+ const readAttributeKeyEnd = (value, startIndex) => {
1009
+ let index = startIndex;
1010
+ while (index < value.length && isAttributeKeyChar(value.charAt(index))) index++;
1011
+ return index;
1012
+ };
1013
+ const readAttributeValue = (value, startIndex) => {
1014
+ const quote = value.charAt(startIndex);
1015
+ if (quote === "\"" || quote === "'") return readQuotedAttributeValue(value, startIndex + 1, quote);
1016
+ let index = startIndex;
1017
+ while (index < value.length && !isWhitespace(value.charAt(index))) index++;
1018
+ return {
1019
+ value: value.slice(startIndex, index),
1020
+ nextIndex: index
1021
+ };
1022
+ };
1023
+ const readQuotedAttributeValue = (value, startIndex, quote) => {
1024
+ let index = startIndex;
1025
+ while (index < value.length && value.charAt(index) !== quote) index++;
1026
+ return {
1027
+ value: value.slice(startIndex, index),
1028
+ nextIndex: index < value.length ? index + 1 : index
1029
+ };
1030
+ };
1031
+ const skipMalformedAttribute = (value, startIndex) => {
1032
+ let index = skipWhitespace(value, startIndex);
1033
+ while (index < value.length && !isWhitespace(value.charAt(index))) index++;
1034
+ return index === startIndex ? index + 1 : index;
1035
+ };
1036
+ const isWhitespace = (char) => char === " " || char === " " || char === "\r" || char === "\n";
1037
+ const isAttributeKeyChar = (char) => isAsciiAlphaNumeric(char) || char === "_" || char === "." || char === "-";
1038
+ const isAsciiAlphaNumeric = (char) => {
1039
+ const code = char.codePointAt(0);
1040
+ if (code === void 0) return false;
1041
+ return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122;
1042
+ };
1043
+ const parseMarkdownHeading = (line) => {
1044
+ let depth = 0;
1045
+ for (const char of line) {
1046
+ if (char !== "#") break;
1047
+ depth++;
1048
+ }
1049
+ if (depth < 1 || depth > 6 || line.at(depth) !== " ") return null;
1050
+ const heading = line.slice(depth + 1).trim();
1051
+ return heading ? {
1052
+ depth,
1053
+ heading
1054
+ } : null;
1055
+ };
1056
+ const pendingToBlock = (pending, diagnostics, fixes) => {
1057
+ switch (pending.type) {
1058
+ case "title": {
1059
+ const text = pending.heading || paragraphText(pending.lines);
1060
+ if (!text) return null;
1061
+ return {
1062
+ type: "title",
1063
+ text
1064
+ };
1065
+ }
1066
+ case "recital": return {
1067
+ type: "recital",
1068
+ paragraphs: compactParagraphs(pending.lines)
1069
+ };
1070
+ case "clause": {
1071
+ const heading = stripManualNumbering(pending.heading, pending.line, fixes);
1072
+ if (!heading) {
1073
+ fixes.push({
1074
+ code: "headingless-clause-downgraded",
1075
+ message: "Converted a headingless @clause into a paragraph block.",
1076
+ line: pending.line
1077
+ });
1078
+ return {
1079
+ type: "paragraph",
1080
+ paragraphs: compactParagraphs(pending.lines)
1081
+ };
1082
+ }
1083
+ return {
1084
+ type: "clause",
1085
+ level: pending.level,
1086
+ heading,
1087
+ paragraphs: compactParagraphs(pending.lines)
1088
+ };
1089
+ }
1090
+ case "paragraph": return {
1091
+ type: "paragraph",
1092
+ paragraphs: compactParagraphs(pending.lines)
1093
+ };
1094
+ case "list": return {
1095
+ type: "list",
1096
+ ordered: pending.ordered,
1097
+ items: pending.lines.flatMap((line) => {
1098
+ const stripped = stripListMarker(line, pending.ordered);
1099
+ return stripped ? [stripped] : [];
1100
+ })
1101
+ };
1102
+ case "table": return parseTableBlock(pending, diagnostics, fixes);
1103
+ case "schedule": return {
1104
+ type: "schedule",
1105
+ heading: stripManualNumbering(pending.heading, pending.line, fixes),
1106
+ paragraphs: compactParagraphs(pending.lines)
1107
+ };
1108
+ case "signatures": return {
1109
+ type: "signatures",
1110
+ parties: parseSignatureParties(pending.lines, pending.heading)
1111
+ };
1112
+ default: return null;
1113
+ }
1114
+ };
1115
+ const parseTableBlock = (pending, diagnostics, fixes) => {
1116
+ const rows = pending.lines.flatMap((rawLine) => {
1117
+ const line = rawLine.trim();
1118
+ return line.startsWith("|") ? [line] : [];
1119
+ }).flatMap((line) => {
1120
+ const row = parsePipeRow(line);
1121
+ return row.length > 0 ? [row] : [];
1122
+ });
1123
+ const header = rows.at(0) ?? [];
1124
+ const normalizedRows = rows.slice(1).filter((row) => !isMarkdownDividerRow(row)).map((row) => {
1125
+ if (row.length === header.length) return row;
1126
+ fixes.push({
1127
+ code: "table-row-width-normalized",
1128
+ message: "Normalized a table row to match the header width.",
1129
+ line: pending.line
1130
+ });
1131
+ return header.map((_, index) => row.at(index) ?? "");
1132
+ });
1133
+ if (header.length === 0) diagnostics.push({
1134
+ code: "missing-table-header",
1135
+ message: "Table directives must include a pipe-table header row.",
1136
+ severity: "error",
1137
+ line: pending.line
1138
+ });
1139
+ return {
1140
+ type: "table",
1141
+ table: {
1142
+ headers: header,
1143
+ rows: normalizedRows
1144
+ }
1145
+ };
1146
+ };
1147
+ const applyDocumentAutofixes = ({ diagnostics, draft, fixes }) => {
1148
+ const blocks = [];
1149
+ const normalizedTitle = normalizeTitle(draft.meta.title ?? "");
1150
+ let hasSeenSubstantiveBlock = false;
1151
+ for (const block of draft.blocks) {
1152
+ if (!hasSeenSubstantiveBlock && block.type === "clause" && block.level === 1 && normalizedTitle.length > 0 && normalizeTitle(block.heading) === normalizedTitle) {
1153
+ fixes.push({
1154
+ code: "duplicate-title-clause-removed",
1155
+ message: "Removed a first clause that duplicated the document title."
1156
+ });
1157
+ continue;
1158
+ }
1159
+ blocks.push(block);
1160
+ hasSeenSubstantiveBlock ||= isSubstantiveDraftBlock(block);
1161
+ }
1162
+ const signatureIndex = blocks.findIndex((block) => block.type === "signatures");
1163
+ if (signatureIndex !== -1 && signatureIndex !== blocks.length - 1) {
1164
+ const [signatureBlock] = blocks.splice(signatureIndex, 1);
1165
+ if (signatureBlock) {
1166
+ blocks.push(signatureBlock);
1167
+ fixes.push({
1168
+ code: "signatures-moved-to-end",
1169
+ message: "Moved the signatures block to the end of the document."
1170
+ });
1171
+ }
1172
+ }
1173
+ return {
1174
+ draft: {
1175
+ ...draft,
1176
+ blocks
1177
+ },
1178
+ diagnostics,
1179
+ fixes
1180
+ };
1181
+ };
1182
+ const isSubstantiveDraftBlock = (block) => block.type !== "title" && block.type !== "pageBreak";
1183
+ const SIGNATURE_KEY_ALIASES = {
1184
+ party: "party",
1185
+ by: "by",
1186
+ name: "name",
1187
+ title: "title",
1188
+ date: "date",
1189
+ strana: "party",
1190
+ podepisuje: "by",
1191
+ podpisuje: "by",
1192
+ jméno: "name",
1193
+ jmeno: "name",
1194
+ meno: "name",
1195
+ funkce: "title",
1196
+ funkcia: "title",
1197
+ datum: "date",
1198
+ partei: "party",
1199
+ unterzeichnet: "by",
1200
+ unterschreibt: "by",
1201
+ funktion: "title",
1202
+ partie: "party",
1203
+ signataire: "by",
1204
+ nom: "name",
1205
+ fonction: "title",
1206
+ parte: "party",
1207
+ firmante: "by",
1208
+ nombre: "name",
1209
+ cargo: "title",
1210
+ fecha: "date",
1211
+ firmatario: "by",
1212
+ nome: "name",
1213
+ carica: "title",
1214
+ data: "date",
1215
+ imie: "name",
1216
+ imię: "name",
1217
+ stanowisko: "title",
1218
+ assinante: "by",
1219
+ ondertekent: "by",
1220
+ naam: "name",
1221
+ functie: "title",
1222
+ fél: "party",
1223
+ fel: "party",
1224
+ aláírja: "by",
1225
+ alairja: "by",
1226
+ név: "name",
1227
+ nev: "name",
1228
+ beosztás: "title",
1229
+ beosztas: "title",
1230
+ dátum: "date"
1231
+ };
1232
+ const SIGNATURE_FIELD_KEYS = /* @__PURE__ */ new Set([
1233
+ "party",
1234
+ "by",
1235
+ "name",
1236
+ "title",
1237
+ "date"
1238
+ ]);
1239
+ const parseSignatureParties = (lines, heading) => {
1240
+ const parties = [];
1241
+ let current = null;
1242
+ const pushCurrent = () => {
1243
+ if (!current?.name.trim()) {
1244
+ current = null;
1245
+ return;
1246
+ }
1247
+ parties.push({
1248
+ name: current.name.trim(),
1249
+ ...current.signatory ? { signatory: current.signatory.trim() } : {},
1250
+ ...current.title ? { title: current.title.trim() } : {}
1251
+ });
1252
+ current = null;
1253
+ };
1254
+ const startParty = (name) => {
1255
+ pushCurrent();
1256
+ current = { name };
1257
+ };
1258
+ if (heading.trim()) startParty(heading.trim().replace(/^party:\s*/iu, ""));
1259
+ for (const line of lines) {
1260
+ const trimmed = line.trim();
1261
+ if (!trimmed) continue;
1262
+ const separatorIndex = trimmed.indexOf(":");
1263
+ if (separatorIndex === -1) {
1264
+ startParty(trimmed);
1265
+ continue;
1266
+ }
1267
+ const rawKey = trimmed.slice(0, separatorIndex).toLowerCase();
1268
+ const canonicalKey = SIGNATURE_KEY_ALIASES[rawKey];
1269
+ if (!canonicalKey || !SIGNATURE_FIELD_KEYS.has(canonicalKey)) {
1270
+ startParty(trimmed);
1271
+ continue;
1272
+ }
1273
+ const value = trimmed.slice(separatorIndex + 1).trim();
1274
+ if (canonicalKey === "party") {
1275
+ startParty(value);
1276
+ continue;
1277
+ }
1278
+ current ??= { name: "" };
1279
+ if ((canonicalKey === "by" || canonicalKey === "name") && value) current.signatory = value;
1280
+ if (canonicalKey === "title" && value) current.title = value;
1281
+ }
1282
+ pushCurrent();
1283
+ const seen = /* @__PURE__ */ new Set();
1284
+ return parties.filter((party) => {
1285
+ const key = `${party.name}\u0000${party.signatory ?? ""}\u0000${party.title ?? ""}`;
1286
+ if (seen.has(key)) return false;
1287
+ seen.add(key);
1288
+ return true;
1289
+ });
1290
+ };
1291
+ const ORDERED_LIST_MARKER_RE = /^(?:\d+(?:\.\d+)+|\d+[.)])\s+/u;
1292
+ const MANUAL_NUMBERING_PREFIX_RE = /^(?:\d+(?:\.\d+)+|\d+[.)]|[A-Za-z][.)]|\([a-zivx]+\))\s+/u;
1293
+ const stripListMarker = (line, ordered) => {
1294
+ const trimmed = line.trim();
1295
+ if (ordered) return trimmed.replace(ORDERED_LIST_MARKER_RE, "");
1296
+ return trimmed.replace(/^[-*•]\s+/u, "");
1297
+ };
1298
+ const parsePipeRow = (line) => line.replace(/^\|/u, "").replace(/\|$/u, "").split("|").map((cell) => cell.trim());
1299
+ const isMarkdownDividerRow = (row) => row.every((cell) => /^:?-{3,}:?$/u.test(cell));
1300
+ const compactParagraphs = (lines) => {
1301
+ const paragraphs = [];
1302
+ let current = [];
1303
+ for (const line of lines) {
1304
+ const trimmed = line.trim();
1305
+ if (!trimmed) {
1306
+ if (current.length > 0) {
1307
+ paragraphs.push(current.join(" "));
1308
+ current = [];
1309
+ }
1310
+ continue;
1311
+ }
1312
+ current.push(trimmed);
1313
+ }
1314
+ if (current.length > 0) paragraphs.push(current.join(" "));
1315
+ return paragraphs;
1316
+ };
1317
+ const paragraphText = (lines) => compactParagraphs(lines).join(" ");
1318
+ const stripManualNumbering = (value, line, fixes) => {
1319
+ const stripped = value.trim().replace(MANUAL_NUMBERING_PREFIX_RE, "");
1320
+ if (stripped !== value.trim()) fixes.push({
1321
+ code: "manual-numbering-stripped",
1322
+ message: "Removed manual numbering from a directive heading.",
1323
+ line
1324
+ });
1325
+ return stripped;
1326
+ };
1327
+ const normalizeTitle = (value) => value.normalize("NFKC").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim();
1328
+ const isLegalKind = (value) => value === "agreement" || value === "letter" || value === "memo" || value === "checklist" || value === "pleading" || value === "other";
1329
+ const isNumberingProfile = (value) => value === "legal" || value === "none" || value === "checklist";
1330
+ const isPageSize = (value) => value === "A4" || value === "Letter";
1331
+ const isPageOrientation = (value) => value === "portrait" || value === "landscape";
1332
+ //#endregion
1333
+ //#region src/legal-source/validate.ts
1334
+ const validateLegalDraft = (draft) => {
1335
+ const diagnostics = [];
1336
+ if (!draft.meta.title?.trim()) diagnostics.push({
1337
+ code: "missing-title",
1338
+ message: "The draft must have a title.",
1339
+ severity: "error"
1340
+ });
1341
+ let hasTopLevelClause = false;
1342
+ for (const block of draft.blocks) {
1343
+ if (block.type === "clause") {
1344
+ if (block.level === 1) hasTopLevelClause = true;
1345
+ if (block.level > 1 && !hasTopLevelClause) diagnostics.push({
1346
+ code: "subclause-before-clause",
1347
+ message: "A subclause cannot appear before the first top-level clause.",
1348
+ severity: "error"
1349
+ });
1350
+ if (!block.heading.trim()) diagnostics.push({
1351
+ code: "empty-clause-heading",
1352
+ message: "Clause headings cannot be empty.",
1353
+ severity: "error"
1354
+ });
1355
+ }
1356
+ if (block.type === "table") {
1357
+ if (block.table.headers.length === 0) diagnostics.push({
1358
+ code: "empty-table",
1359
+ message: "Tables must include at least one header.",
1360
+ severity: "error"
1361
+ });
1362
+ for (const row of block.table.rows) if (row.length !== block.table.headers.length) diagnostics.push({
1363
+ code: "ragged-table",
1364
+ message: "Every table row must match the header width.",
1365
+ severity: "error"
1366
+ });
1367
+ }
1368
+ if (block.type === "signatures" && block.parties.length === 0) diagnostics.push({
1369
+ code: "empty-signatures",
1370
+ message: "The signatures block must include at least one party.",
1371
+ severity: "warning"
1372
+ });
1373
+ }
1374
+ return diagnostics;
1375
+ };
1376
+ //#endregion
1377
+ //#region src/legal-source/compile.ts
1378
+ const LEGAL_NUMBERING_ID = 1;
1379
+ const LEGAL_ABSTRACT_NUMBERING_ID = 1;
1380
+ const BULLET_NUMBERING_ID = 2;
1381
+ const BULLET_ABSTRACT_NUMBERING_ID = 2;
1382
+ const CHECKLIST_NUMBERING_ID = 3;
1383
+ const CHECKLIST_ABSTRACT_NUMBERING_ID = 3;
1384
+ const SIGNATURE_LINE = "_".repeat(28);
1385
+ const compileLegalSourceToDocument = (source, options = {}) => {
1386
+ const parsed = parseLegalSource(source, options.titleFallback ? { titleFallback: options.titleFallback } : {});
1387
+ const validationDiagnostics = validateLegalDraft(parsed.draft);
1388
+ const diagnostics = [...parsed.diagnostics, ...validationDiagnostics];
1389
+ const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
1390
+ if (errors.length > 0) return {
1391
+ status: "needs_llm_repair",
1392
+ draft: parsed.draft,
1393
+ fixes: parsed.fixes,
1394
+ errors
1395
+ };
1396
+ return {
1397
+ status: "ok",
1398
+ document: draftToDocument(parsed.draft),
1399
+ draft: parsed.draft,
1400
+ fixes: parsed.fixes,
1401
+ warnings: diagnostics.filter((diagnostic) => diagnostic.severity === "warning")
1402
+ };
1403
+ };
1404
+ const draftToDocument = (draft) => {
1405
+ const content = [];
1406
+ const title = draft.meta.title ?? "Untitled document";
1407
+ const numbering = createNumberingDefinitions(draft.meta.numbering);
1408
+ content.push(paragraph(title, "Title", { bold: true }));
1409
+ for (const block of draft.blocks) appendBlock(content, block, { numberingProfile: draft.meta.numbering });
1410
+ const docxPackage = {
1411
+ document: {
1412
+ content,
1413
+ finalSectionProperties: pageProperties(draft.meta.page.size, draft.meta.page.orientation)
1414
+ },
1415
+ styles: createStyleDefinitions(),
1416
+ properties: {
1417
+ title,
1418
+ creator: "Stella",
1419
+ created: /* @__PURE__ */ new Date(),
1420
+ modified: /* @__PURE__ */ new Date()
1421
+ }
1422
+ };
1423
+ if (numbering) docxPackage.numbering = numbering;
1424
+ return { package: docxPackage };
1425
+ };
1426
+ const appendBlock = (content, block, options) => {
1427
+ switch (block.type) {
1428
+ case "title": return;
1429
+ case "recital":
1430
+ appendParagraphs(content, block.paragraphs, "Recital");
1431
+ return;
1432
+ case "clause":
1433
+ content.push(paragraph(block.heading, clauseStyle(block.level), { bold: block.level === 1 }, clauseNumbering(block, options.numberingProfile)));
1434
+ appendParagraphs(content, block.paragraphs, "BodyText");
1435
+ return;
1436
+ case "paragraph":
1437
+ appendParagraphs(content, block.paragraphs, "BodyText");
1438
+ return;
1439
+ case "list":
1440
+ for (const item of block.items) content.push(paragraph(item, "ListParagraph", {}, listNumbering(block, options.numberingProfile)));
1441
+ return;
1442
+ case "table":
1443
+ content.push(table(block.table.headers, block.table.rows));
1444
+ return;
1445
+ case "schedule":
1446
+ content.push(paragraph(block.heading, "ScheduleHeading", { bold: true }, void 0, true));
1447
+ appendParagraphs(content, block.paragraphs, "BodyText");
1448
+ return;
1449
+ case "signatures":
1450
+ content.push(signatureTable(block.parties));
1451
+ return;
1452
+ case "pageBreak":
1453
+ content.push({
1454
+ type: "paragraph",
1455
+ content: [{
1456
+ type: "run",
1457
+ content: [{
1458
+ type: "break",
1459
+ breakType: "page"
1460
+ }]
1461
+ }]
1462
+ });
1463
+ return;
1464
+ default:
1465
+ }
1466
+ };
1467
+ const clauseNumbering = (block, profile) => {
1468
+ if (profile !== "legal") return;
1469
+ return {
1470
+ numId: LEGAL_NUMBERING_ID,
1471
+ ilvl: Math.max(0, block.level - 1)
1472
+ };
1473
+ };
1474
+ const listNumbering = (block, profile) => {
1475
+ if (profile === "none") return;
1476
+ if (profile === "checklist") return {
1477
+ numId: CHECKLIST_NUMBERING_ID,
1478
+ ilvl: 0
1479
+ };
1480
+ return {
1481
+ numId: block.ordered ? LEGAL_NUMBERING_ID : BULLET_NUMBERING_ID,
1482
+ ilvl: block.ordered ? 2 : 0
1483
+ };
1484
+ };
1485
+ const appendParagraphs = (content, paragraphs, styleId) => {
1486
+ for (const text of paragraphs) content.push(paragraph(text, styleId));
1487
+ };
1488
+ const paragraph = (text, styleId, runOptions = {}, numPr, pageBreakBefore = false) => ({
1489
+ type: "paragraph",
1490
+ formatting: {
1491
+ styleId,
1492
+ ...numPr ? { numPr } : {},
1493
+ ...pageBreakBefore ? { pageBreakBefore: true } : {}
1494
+ },
1495
+ content: textRunsWithPlaceholders(text, runOptions)
1496
+ });
1497
+ const PLACEHOLDER_PATTERN = /\[\[(?<inner>[^\][]+?)\]\]/gu;
1498
+ const textRunsWithPlaceholders = (text, options = {}) => {
1499
+ if (!text.includes("[[")) return [textRun(text, options)];
1500
+ const runs = [];
1501
+ let cursor = 0;
1502
+ for (const match of text.matchAll(PLACEHOLDER_PATTERN)) {
1503
+ const start = match.index;
1504
+ if (start > cursor) runs.push(textRun(text.slice(cursor, start), options));
1505
+ const inner = match.groups?.["inner"] ?? "";
1506
+ runs.push(textRun(inner, options, { highlight: "yellow" }));
1507
+ cursor = start + match[0].length;
1508
+ }
1509
+ if (cursor < text.length) runs.push(textRun(text.slice(cursor), options));
1510
+ return runs.length > 0 ? runs : [textRun(text, options)];
1511
+ };
1512
+ const textRun = (text, options = {}, extra = {}) => ({
1513
+ type: "run",
1514
+ formatting: {
1515
+ ...options.bold ? { bold: true } : {},
1516
+ ...options.italic ? { italic: true } : {},
1517
+ ...extra.highlight ? { highlight: extra.highlight } : {}
1518
+ },
1519
+ content: [{
1520
+ type: "text",
1521
+ text,
1522
+ preserveSpace: true
1523
+ }]
1524
+ });
1525
+ const table = (headers, rows) => ({
1526
+ type: "table",
1527
+ rows: [tableRow(headers, true), ...rows.map((row) => tableRow(row, false))]
1528
+ });
1529
+ const tableRow = (cells, header) => ({
1530
+ type: "tableRow",
1531
+ cells: cells.map((cell) => tableCell(cell, header))
1532
+ });
1533
+ const tableCell = (text, header) => ({
1534
+ type: "tableCell",
1535
+ content: [paragraph(text, "TableText", { bold: header })]
1536
+ });
1537
+ const signatureTable = (parties) => {
1538
+ const partyList = parties.length > 0 ? parties : [{
1539
+ name: "",
1540
+ signatory: "",
1541
+ title: ""
1542
+ }];
1543
+ const empty = () => paragraph("", "SignatureSpacer");
1544
+ const buildCell = (party) => {
1545
+ const cellContent = [
1546
+ paragraph(party.name, "SignatureParty", { bold: true }),
1547
+ empty(),
1548
+ empty(),
1549
+ paragraph(SIGNATURE_LINE, "SignatureRule")
1550
+ ];
1551
+ if (party.signatory) cellContent.push(paragraph(party.signatory, "SignatureField"));
1552
+ if (party.title) cellContent.push(paragraph(party.title, "SignatureField", { italic: true }));
1553
+ return {
1554
+ type: "tableCell",
1555
+ content: cellContent
1556
+ };
1557
+ };
1558
+ return {
1559
+ type: "table",
1560
+ formatting: { borders: {
1561
+ top: { style: "none" },
1562
+ left: { style: "none" },
1563
+ bottom: { style: "none" },
1564
+ right: { style: "none" },
1565
+ insideH: { style: "none" },
1566
+ insideV: { style: "none" }
1567
+ } },
1568
+ rows: [{
1569
+ type: "tableRow",
1570
+ cells: partyList.map(buildCell)
1571
+ }]
1572
+ };
1573
+ };
1574
+ const clauseStyle = (level) => {
1575
+ if (level === 1) return "ClauseHeading1";
1576
+ if (level === 2) return "ClauseHeading2";
1577
+ return "ClauseHeading3";
1578
+ };
1579
+ const pageProperties = (size, orientation) => {
1580
+ const portrait = size === "A4" ? {
1581
+ pageWidth: 11906,
1582
+ pageHeight: 16838
1583
+ } : {
1584
+ pageWidth: 12240,
1585
+ pageHeight: 15840
1586
+ };
1587
+ return {
1588
+ ...orientation === "landscape" ? {
1589
+ pageWidth: portrait.pageHeight,
1590
+ pageHeight: portrait.pageWidth
1591
+ } : portrait,
1592
+ orientation,
1593
+ marginTop: 1440,
1594
+ marginBottom: 1440,
1595
+ marginLeft: 1440,
1596
+ marginRight: 1440,
1597
+ headerDistance: 720,
1598
+ footerDistance: 720
1599
+ };
1600
+ };
1601
+ const createStyleDefinitions = () => ({
1602
+ docDefaults: {
1603
+ rPr: {
1604
+ fontFamily: {
1605
+ ascii: "Calibri",
1606
+ hAnsi: "Calibri",
1607
+ cs: "Calibri"
1608
+ },
1609
+ fontSize: 22
1610
+ },
1611
+ pPr: {
1612
+ lineSpacing: 276,
1613
+ spaceAfter: 160
1614
+ }
1615
+ },
1616
+ styles: [
1617
+ {
1618
+ styleId: "Normal",
1619
+ type: "paragraph",
1620
+ name: "Normal",
1621
+ default: true,
1622
+ rPr: {
1623
+ fontFamily: {
1624
+ ascii: "Calibri",
1625
+ hAnsi: "Calibri",
1626
+ cs: "Calibri"
1627
+ },
1628
+ fontSize: 22
1629
+ }
1630
+ },
1631
+ {
1632
+ styleId: "Title",
1633
+ type: "paragraph",
1634
+ name: "Title",
1635
+ basedOn: "Normal",
1636
+ next: "BodyText",
1637
+ qFormat: true,
1638
+ rPr: {
1639
+ bold: true,
1640
+ fontSize: 36,
1641
+ allCaps: true
1642
+ },
1643
+ pPr: {
1644
+ alignment: "center",
1645
+ spaceBefore: 240,
1646
+ spaceAfter: 480
1647
+ }
1648
+ },
1649
+ {
1650
+ styleId: "BodyText",
1651
+ type: "paragraph",
1652
+ name: "Body Text",
1653
+ basedOn: "Normal",
1654
+ rPr: { fontSize: 22 },
1655
+ pPr: {
1656
+ alignment: "both",
1657
+ spaceAfter: 160
1658
+ }
1659
+ },
1660
+ {
1661
+ styleId: "Recital",
1662
+ type: "paragraph",
1663
+ name: "Recital",
1664
+ basedOn: "BodyText",
1665
+ rPr: { italic: true }
1666
+ },
1667
+ {
1668
+ styleId: "ClauseHeading1",
1669
+ type: "paragraph",
1670
+ name: "Clause Heading 1",
1671
+ basedOn: "BodyText",
1672
+ next: "BodyText",
1673
+ qFormat: true,
1674
+ rPr: {
1675
+ bold: true,
1676
+ fontSize: 24,
1677
+ allCaps: true
1678
+ },
1679
+ pPr: {
1680
+ keepNext: true,
1681
+ spaceBefore: 360,
1682
+ spaceAfter: 160
1683
+ }
1684
+ },
1685
+ {
1686
+ styleId: "ClauseHeading2",
1687
+ type: "paragraph",
1688
+ name: "Clause Heading 2",
1689
+ basedOn: "BodyText",
1690
+ next: "BodyText",
1691
+ qFormat: true,
1692
+ rPr: {
1693
+ bold: true,
1694
+ fontSize: 22
1695
+ },
1696
+ pPr: {
1697
+ keepNext: true,
1698
+ spaceBefore: 240,
1699
+ spaceAfter: 120
1700
+ }
1701
+ },
1702
+ {
1703
+ styleId: "ClauseHeading3",
1704
+ type: "paragraph",
1705
+ name: "Clause Heading 3",
1706
+ basedOn: "BodyText",
1707
+ next: "BodyText",
1708
+ qFormat: true,
1709
+ rPr: { italic: true },
1710
+ pPr: {
1711
+ keepNext: true,
1712
+ spaceBefore: 160,
1713
+ spaceAfter: 120
1714
+ }
1715
+ },
1716
+ {
1717
+ styleId: "ListParagraph",
1718
+ type: "paragraph",
1719
+ name: "List Paragraph",
1720
+ basedOn: "BodyText"
1721
+ },
1722
+ {
1723
+ styleId: "TableText",
1724
+ type: "paragraph",
1725
+ name: "Table Text",
1726
+ basedOn: "Normal",
1727
+ pPr: { spaceAfter: 0 }
1728
+ },
1729
+ {
1730
+ styleId: "ScheduleHeading",
1731
+ type: "paragraph",
1732
+ name: "Schedule Heading",
1733
+ basedOn: "ClauseHeading1",
1734
+ pPr: {
1735
+ pageBreakBefore: true,
1736
+ keepNext: true,
1737
+ spaceAfter: 240
1738
+ }
1739
+ },
1740
+ {
1741
+ styleId: "SignatureParty",
1742
+ type: "paragraph",
1743
+ name: "Signature Party",
1744
+ basedOn: "Normal",
1745
+ rPr: {
1746
+ bold: true,
1747
+ fontSize: 22
1748
+ },
1749
+ pPr: {
1750
+ spaceBefore: 120,
1751
+ spaceAfter: 80
1752
+ }
1753
+ },
1754
+ {
1755
+ styleId: "SignatureRule",
1756
+ type: "paragraph",
1757
+ name: "Signature Rule",
1758
+ basedOn: "Normal",
1759
+ pPr: {
1760
+ spaceBefore: 0,
1761
+ spaceAfter: 0
1762
+ }
1763
+ },
1764
+ {
1765
+ styleId: "SignatureField",
1766
+ type: "paragraph",
1767
+ name: "Signature Field",
1768
+ basedOn: "Normal",
1769
+ rPr: { fontSize: 20 },
1770
+ pPr: { spaceAfter: 60 }
1771
+ },
1772
+ {
1773
+ styleId: "SignatureSpacer",
1774
+ type: "paragraph",
1775
+ name: "Signature Spacer",
1776
+ basedOn: "Normal",
1777
+ pPr: {
1778
+ spaceBefore: 0,
1779
+ spaceAfter: 80
1780
+ }
1781
+ },
1782
+ {
1783
+ styleId: "Footer",
1784
+ type: "paragraph",
1785
+ name: "Footer",
1786
+ basedOn: "Normal",
1787
+ rPr: { fontSize: 18 },
1788
+ pPr: {
1789
+ alignment: "center",
1790
+ spaceAfter: 0
1791
+ }
1792
+ }
1793
+ ]
1794
+ });
1795
+ const createNumberingDefinitions = (profile) => {
1796
+ if (profile === "none") return;
1797
+ if (profile === "checklist") return createChecklistNumberingDefinitions();
1798
+ return createLegalNumberingDefinitions();
1799
+ };
1800
+ const createLegalNumberingDefinitions = () => ({
1801
+ abstractNums: [{
1802
+ abstractNumId: LEGAL_ABSTRACT_NUMBERING_ID,
1803
+ multiLevelType: "hybridMultilevel",
1804
+ levels: [
1805
+ {
1806
+ ilvl: 0,
1807
+ start: 1,
1808
+ numFmt: "decimal",
1809
+ lvlText: "%1.",
1810
+ suffix: "tab",
1811
+ isLgl: true,
1812
+ pPr: {
1813
+ indentLeft: 720,
1814
+ indentFirstLine: 720,
1815
+ hangingIndent: true
1816
+ },
1817
+ rPr: { bold: true }
1818
+ },
1819
+ {
1820
+ ilvl: 1,
1821
+ start: 1,
1822
+ numFmt: "decimal",
1823
+ lvlText: "%1.%2",
1824
+ suffix: "tab",
1825
+ isLgl: true,
1826
+ pPr: {
1827
+ indentLeft: 720,
1828
+ indentFirstLine: 720,
1829
+ hangingIndent: true
1830
+ }
1831
+ },
1832
+ {
1833
+ ilvl: 2,
1834
+ start: 1,
1835
+ numFmt: "lowerLetter",
1836
+ lvlText: "(%3)",
1837
+ suffix: "tab",
1838
+ pPr: {
1839
+ indentLeft: 1440,
1840
+ indentFirstLine: 720,
1841
+ hangingIndent: true
1842
+ }
1843
+ },
1844
+ {
1845
+ ilvl: 3,
1846
+ start: 1,
1847
+ numFmt: "lowerRoman",
1848
+ lvlText: "(%4)",
1849
+ suffix: "tab",
1850
+ pPr: {
1851
+ indentLeft: 1440,
1852
+ indentFirstLine: 720,
1853
+ hangingIndent: true
1854
+ }
1855
+ },
1856
+ {
1857
+ ilvl: 4,
1858
+ start: 1,
1859
+ numFmt: "upperLetter",
1860
+ lvlText: "(%5)",
1861
+ suffix: "tab",
1862
+ pPr: {
1863
+ indentLeft: 2160,
1864
+ indentFirstLine: 720,
1865
+ hangingIndent: true
1866
+ }
1867
+ }
1868
+ ]
1869
+ }, {
1870
+ abstractNumId: BULLET_ABSTRACT_NUMBERING_ID,
1871
+ multiLevelType: "singleLevel",
1872
+ levels: [{
1873
+ ilvl: 0,
1874
+ start: 1,
1875
+ numFmt: "bullet",
1876
+ lvlText: "•",
1877
+ suffix: "tab",
1878
+ pPr: {
1879
+ indentLeft: 720,
1880
+ indentFirstLine: 360,
1881
+ hangingIndent: true
1882
+ }
1883
+ }]
1884
+ }],
1885
+ nums: [{
1886
+ numId: LEGAL_NUMBERING_ID,
1887
+ abstractNumId: LEGAL_ABSTRACT_NUMBERING_ID
1888
+ }, {
1889
+ numId: BULLET_NUMBERING_ID,
1890
+ abstractNumId: BULLET_ABSTRACT_NUMBERING_ID
1891
+ }]
1892
+ });
1893
+ const createChecklistNumberingDefinitions = () => ({
1894
+ abstractNums: [{
1895
+ abstractNumId: CHECKLIST_ABSTRACT_NUMBERING_ID,
1896
+ multiLevelType: "singleLevel",
1897
+ levels: [{
1898
+ ilvl: 0,
1899
+ start: 1,
1900
+ numFmt: "bullet",
1901
+ lvlText: "☐",
1902
+ suffix: "tab",
1903
+ pPr: {
1904
+ indentLeft: 720,
1905
+ indentFirstLine: 360,
1906
+ hangingIndent: true
1907
+ }
1908
+ }]
1909
+ }],
1910
+ nums: [{
1911
+ numId: CHECKLIST_NUMBERING_ID,
1912
+ abstractNumId: CHECKLIST_ABSTRACT_NUMBERING_ID
1913
+ }]
1914
+ });
1915
+ //#endregion
1916
+ //#region src/legal-source/index.ts
1917
+ const compileLegalSourceToDocx = async (source, options = {}) => {
1918
+ const result = compileLegalSourceToDocument(source, options);
1919
+ if (result.status !== "ok") return result;
1920
+ const buffer = Buffer.from(await serializeDocumentToDocx(result.document, { language: result.draft.meta.locale }));
1921
+ const validation = await validateDocxPackage(buffer);
1922
+ if (!validation.valid) return {
1923
+ status: "needs_llm_repair",
1924
+ draft: result.draft,
1925
+ fixes: result.fixes,
1926
+ errors: [{
1927
+ code: "generated-docx-invalid",
1928
+ message: validation.error,
1929
+ severity: "error"
1930
+ }]
1931
+ };
1932
+ return {
1933
+ ...result,
1934
+ buffer
1935
+ };
1936
+ };
1937
+ //#endregion
1938
+ export { assertValidDocumentModel, compileLegalSourceToDocument, compileLegalSourceToDocx, parseLegalSource, serializeDocumentToDocx, validateDocumentModel, validateDocxPackage, validateLegalDraft };