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