@opengeni/artifact-tool 0.2.2
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/LICENSE +190 -0
- package/README.md +104 -0
- package/dist/bounded-zip.d.ts +28 -0
- package/dist/chunk-4BSENMAE.js +39 -0
- package/dist/chunk-4BSENMAE.js.map +1 -0
- package/dist/chunk-4FRFNNBM.js +276 -0
- package/dist/chunk-4FRFNNBM.js.map +1 -0
- package/dist/chunk-5XIA6KHZ.js +49 -0
- package/dist/chunk-5XIA6KHZ.js.map +1 -0
- package/dist/chunk-66CT2ZRP.js +2229 -0
- package/dist/chunk-66CT2ZRP.js.map +1 -0
- package/dist/chunk-72SYDU4O.js +319 -0
- package/dist/chunk-72SYDU4O.js.map +1 -0
- package/dist/chunk-B2ZJRZDA.js +279 -0
- package/dist/chunk-B2ZJRZDA.js.map +1 -0
- package/dist/chunk-DCTLTNPW.js +2458 -0
- package/dist/chunk-DCTLTNPW.js.map +1 -0
- package/dist/chunk-FGZM4FQ6.js +473 -0
- package/dist/chunk-FGZM4FQ6.js.map +1 -0
- package/dist/chunk-HTJBLBDE.js +591 -0
- package/dist/chunk-HTJBLBDE.js.map +1 -0
- package/dist/chunk-IYFXZQ34.js +5306 -0
- package/dist/chunk-IYFXZQ34.js.map +1 -0
- package/dist/chunk-J6N7IR44.js +172 -0
- package/dist/chunk-J6N7IR44.js.map +1 -0
- package/dist/chunk-N2IHNWBS.js +1002 -0
- package/dist/chunk-N2IHNWBS.js.map +1 -0
- package/dist/chunk-NAG44K67.js +549 -0
- package/dist/chunk-NAG44K67.js.map +1 -0
- package/dist/chunk-O24KTEGP.js +33 -0
- package/dist/chunk-O24KTEGP.js.map +1 -0
- package/dist/chunk-PI2AZVGK.js +2074 -0
- package/dist/chunk-PI2AZVGK.js.map +1 -0
- package/dist/chunk-Q2FFHEY7.js +40 -0
- package/dist/chunk-Q2FFHEY7.js.map +1 -0
- package/dist/chunk-RBGQADXA.js +57 -0
- package/dist/chunk-RBGQADXA.js.map +1 -0
- package/dist/chunk-U5G2BKM2.js +613 -0
- package/dist/chunk-U5G2BKM2.js.map +1 -0
- package/dist/chunk-VOJHMSM3.js +39 -0
- package/dist/chunk-VOJHMSM3.js.map +1 -0
- package/dist/chunk-VUNV25KB.js +16 -0
- package/dist/chunk-VUNV25KB.js.map +1 -0
- package/dist/chunk-XIFUNVBO.js +101 -0
- package/dist/chunk-XIFUNVBO.js.map +1 -0
- package/dist/chunk-ZCJYRJGB.js +489 -0
- package/dist/chunk-ZCJYRJGB.js.map +1 -0
- package/dist/chunk-ZZXPEE4S.js +2260 -0
- package/dist/chunk-ZZXPEE4S.js.map +1 -0
- package/dist/document-docx-api.d.ts +49 -0
- package/dist/document-docx-codec.d.ts +9 -0
- package/dist/document-docx-codec.js +4765 -0
- package/dist/document-docx-codec.js.map +1 -0
- package/dist/document-docx-import.d.ts +261 -0
- package/dist/document-docx-packer.d.ts +18 -0
- package/dist/document-docx-state.d.ts +12 -0
- package/dist/document-render.d.ts +3 -0
- package/dist/document-render.js +492 -0
- package/dist/document-render.js.map +1 -0
- package/dist/document.d.ts +432 -0
- package/dist/document.js +37 -0
- package/dist/document.js.map +1 -0
- package/dist/errors.d.ts +23 -0
- package/dist/file-blob.d.ts +11 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +186 -0
- package/dist/index.js.map +1 -0
- package/dist/inspection.d.ts +12 -0
- package/dist/kernel.d.ts +101 -0
- package/dist/lib-GQLPACFK.js +9746 -0
- package/dist/lib-GQLPACFK.js.map +1 -0
- package/dist/materializer-cli-entry.d.ts +2 -0
- package/dist/materializer-cli-entry.js +29 -0
- package/dist/materializer-cli-entry.js.map +1 -0
- package/dist/materializer-cli.d.ts +24 -0
- package/dist/materializer-cli.js +24 -0
- package/dist/materializer-cli.js.map +1 -0
- package/dist/native-raster.d.ts +10 -0
- package/dist/native.d.ts +129 -0
- package/dist/native.js +31 -0
- package/dist/native.js.map +1 -0
- package/dist/office-import.d.ts +48 -0
- package/dist/office-import.js +154 -0
- package/dist/office-import.js.map +1 -0
- package/dist/presentation-pptx-api.d.ts +60 -0
- package/dist/presentation-pptx-import.d.ts +5 -0
- package/dist/presentation-pptx-state-digest-765CZZED.js +10 -0
- package/dist/presentation-pptx-state-digest-765CZZED.js.map +1 -0
- package/dist/presentation-pptx-state-digest.d.ts +3 -0
- package/dist/presentation-pptx-state.d.ts +4 -0
- package/dist/presentation-pptx-xml.d.ts +29 -0
- package/dist/presentation-pptx.d.ts +5 -0
- package/dist/presentation-pptx.js +2365 -0
- package/dist/presentation-pptx.js.map +1 -0
- package/dist/presentation-render.d.ts +29 -0
- package/dist/presentation-render.js +375 -0
- package/dist/presentation-render.js.map +1 -0
- package/dist/presentation.d.ts +565 -0
- package/dist/presentation.js +71 -0
- package/dist/presentation.js.map +1 -0
- package/dist/production-composite.d.ts +68 -0
- package/dist/production-document.d.ts +2 -0
- package/dist/production-document.js +176 -0
- package/dist/production-document.js.map +1 -0
- package/dist/production-native-codecs.d.ts +94 -0
- package/dist/production-presentation.d.ts +2 -0
- package/dist/production-presentation.js +176 -0
- package/dist/production-presentation.js.map +1 -0
- package/dist/production-sha256.d.ts +2 -0
- package/dist/production-spreadsheet-native.d.ts +12 -0
- package/dist/production-spreadsheet.d.ts +2 -0
- package/dist/production-spreadsheet.js +176 -0
- package/dist/production-spreadsheet.js.map +1 -0
- package/dist/production.d.ts +96 -0
- package/dist/raster-image.d.ts +34 -0
- package/dist/reference.d.ts +17 -0
- package/dist/reference.js +574 -0
- package/dist/reference.js.map +1 -0
- package/dist/runtime-cli-entry.d.ts +2 -0
- package/dist/runtime-cli-entry.js +21 -0
- package/dist/runtime-cli-entry.js.map +1 -0
- package/dist/runtime-cli.d.ts +76 -0
- package/dist/runtime-cli.js +26 -0
- package/dist/runtime-cli.js.map +1 -0
- package/dist/runtime-development.d.ts +73 -0
- package/dist/runtime-development.js +30 -0
- package/dist/runtime-development.js.map +1 -0
- package/dist/runtime-receipt.d.ts +19 -0
- package/dist/runtime.d.ts +355 -0
- package/dist/runtime.js +42 -0
- package/dist/runtime.js.map +1 -0
- package/dist/snapshot.d.ts +26 -0
- package/dist/snapshot.js +61 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/spreadsheet-address.d.ts +18 -0
- package/dist/spreadsheet-file.d.ts +17 -0
- package/dist/spreadsheet-formula.d.ts +67 -0
- package/dist/spreadsheet-image.d.ts +17 -0
- package/dist/spreadsheet-render.d.ts +5 -0
- package/dist/spreadsheet-render.js +752 -0
- package/dist/spreadsheet-render.js.map +1 -0
- package/dist/spreadsheet-types.d.ts +155 -0
- package/dist/spreadsheet-xlsx-api.d.ts +88 -0
- package/dist/spreadsheet-xlsx-codec.d.ts +16 -0
- package/dist/spreadsheet-xlsx-codec.js +16 -0
- package/dist/spreadsheet-xlsx-codec.js.map +1 -0
- package/dist/spreadsheet-xlsx-state.d.ts +16 -0
- package/dist/spreadsheet.d.ts +724 -0
- package/dist/spreadsheet.js +67 -0
- package/dist/spreadsheet.js.map +1 -0
- package/package.json +129 -0
- package/src/bounded-zip.ts +450 -0
- package/src/document-docx-api.ts +64 -0
- package/src/document-docx-codec.ts +2253 -0
- package/src/document-docx-import.ts +3814 -0
- package/src/document-docx-packer.ts +72 -0
- package/src/document-docx-state.ts +59 -0
- package/src/document-render.ts +615 -0
- package/src/document.ts +2800 -0
- package/src/errors.ts +45 -0
- package/src/file-blob.ts +43 -0
- package/src/index.ts +4 -0
- package/src/inspection.ts +50 -0
- package/src/kernel.ts +525 -0
- package/src/materializer-cli-entry.ts +11 -0
- package/src/materializer-cli.ts +743 -0
- package/src/native-raster.ts +141 -0
- package/src/native.ts +390 -0
- package/src/office-import.ts +243 -0
- package/src/presentation-pptx-api.ts +107 -0
- package/src/presentation-pptx-import.ts +2065 -0
- package/src/presentation-pptx-state-digest.ts +46 -0
- package/src/presentation-pptx-state.ts +18 -0
- package/src/presentation-pptx-xml.ts +473 -0
- package/src/presentation-pptx.ts +447 -0
- package/src/presentation-render.ts +532 -0
- package/src/presentation.ts +2599 -0
- package/src/production-composite.ts +805 -0
- package/src/production-document.ts +2 -0
- package/src/production-native-codecs.ts +1141 -0
- package/src/production-presentation.ts +2 -0
- package/src/production-sha256.ts +150 -0
- package/src/production-spreadsheet-native.ts +686 -0
- package/src/production-spreadsheet.ts +2 -0
- package/src/production.ts +1022 -0
- package/src/raster-image.ts +586 -0
- package/src/reference.ts +17 -0
- package/src/runtime-cli-entry.ts +13 -0
- package/src/runtime-cli.ts +670 -0
- package/src/runtime-development.ts +689 -0
- package/src/runtime-receipt.ts +169 -0
- package/src/runtime.ts +1463 -0
- package/src/snapshot.ts +88 -0
- package/src/spreadsheet-address.ts +90 -0
- package/src/spreadsheet-file.ts +71 -0
- package/src/spreadsheet-formula.ts +1212 -0
- package/src/spreadsheet-image.ts +242 -0
- package/src/spreadsheet-render.ts +985 -0
- package/src/spreadsheet-types.ts +171 -0
- package/src/spreadsheet-xlsx-api.ts +123 -0
- package/src/spreadsheet-xlsx-codec.ts +3063 -0
- package/src/spreadsheet-xlsx-state.ts +191 -0
- package/src/spreadsheet.ts +5195 -0
|
@@ -0,0 +1,4765 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DOCUMENT_LOSS_PRESERVATION,
|
|
3
|
+
DOCX_MEDIA_TYPE,
|
|
4
|
+
DocumentFidelityError,
|
|
5
|
+
documentFidelityReport,
|
|
6
|
+
documentLossPreservationEnvelope
|
|
7
|
+
} from "./chunk-RBGQADXA.js";
|
|
8
|
+
import {
|
|
9
|
+
Document,
|
|
10
|
+
DocumentPageBreak,
|
|
11
|
+
DocumentParagraph,
|
|
12
|
+
DocumentTable,
|
|
13
|
+
DocumentTextRun
|
|
14
|
+
} from "./chunk-66CT2ZRP.js";
|
|
15
|
+
import {
|
|
16
|
+
UnsupportedArtifactFeatureError
|
|
17
|
+
} from "./chunk-O24KTEGP.js";
|
|
18
|
+
import {
|
|
19
|
+
inflateBoundedZipEntry,
|
|
20
|
+
parseBoundedZip
|
|
21
|
+
} from "./chunk-72SYDU4O.js";
|
|
22
|
+
import {
|
|
23
|
+
FileBlob
|
|
24
|
+
} from "./chunk-4BSENMAE.js";
|
|
25
|
+
import "./chunk-Q2FFHEY7.js";
|
|
26
|
+
import "./chunk-VUNV25KB.js";
|
|
27
|
+
|
|
28
|
+
// src/document-docx-packer.ts
|
|
29
|
+
var DOCX_MIME_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
30
|
+
var DocxExportCompatibilityError = class extends Error {
|
|
31
|
+
name = "DocxExportCompatibilityError";
|
|
32
|
+
code = "incompatible_docx_runtime";
|
|
33
|
+
};
|
|
34
|
+
async function packDocxWithBoundedCompression(file, packer, overrides = []) {
|
|
35
|
+
if (typeof packer !== "object" && typeof packer !== "function" || packer === null) {
|
|
36
|
+
throw incompatibleRuntime();
|
|
37
|
+
}
|
|
38
|
+
const compiler = Reflect.get(packer, "compiler");
|
|
39
|
+
if (!compiler || typeof compiler.compile !== "function") throw incompatibleRuntime();
|
|
40
|
+
const archive = compiler.compile(file, void 0, overrides);
|
|
41
|
+
if (!archive || typeof archive.generateAsync !== "function" || typeof archive.forEach !== "function")
|
|
42
|
+
throw incompatibleRuntime();
|
|
43
|
+
archive.forEach((_path, entry) => {
|
|
44
|
+
entry.date = new Date(1980, 0, 1, 0, 0, 0, 0);
|
|
45
|
+
});
|
|
46
|
+
const bytes = await archive.generateAsync({
|
|
47
|
+
type: "uint8array",
|
|
48
|
+
mimeType: DOCX_MIME_TYPE,
|
|
49
|
+
compression: "STORE"
|
|
50
|
+
});
|
|
51
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength === 0) throw incompatibleRuntime();
|
|
52
|
+
return bytes;
|
|
53
|
+
}
|
|
54
|
+
function incompatibleRuntime() {
|
|
55
|
+
return new DocxExportCompatibilityError(
|
|
56
|
+
"The pinned DOCX runtime no longer exposes the verified bounded-compression compiler adapter"
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/document-docx-import.ts
|
|
61
|
+
var DocxImportError = class extends Error {
|
|
62
|
+
constructor(code, message, partName) {
|
|
63
|
+
super(partName ? `${message} (${partName})` : message);
|
|
64
|
+
this.code = code;
|
|
65
|
+
this.partName = partName;
|
|
66
|
+
}
|
|
67
|
+
name = "DocxImportError";
|
|
68
|
+
};
|
|
69
|
+
var DEFAULT_LIMITS = {
|
|
70
|
+
maxCompressedBytes: 32 * 1024 * 1024,
|
|
71
|
+
maxEntries: 1024,
|
|
72
|
+
maxEntryCompressedBytes: 16 * 1024 * 1024,
|
|
73
|
+
maxEntryUncompressedBytes: 24 * 1024 * 1024,
|
|
74
|
+
maxTotalUncompressedBytes: 64 * 1024 * 1024,
|
|
75
|
+
maxCompressionRatio: 100,
|
|
76
|
+
maxXmlBytes: 16 * 1024 * 1024,
|
|
77
|
+
maxTotalXmlBytes: 32 * 1024 * 1024,
|
|
78
|
+
maxRetainedXmlCharacters: 16e6,
|
|
79
|
+
maxXmlNodes: 1e5,
|
|
80
|
+
maxXmlDepth: 128,
|
|
81
|
+
maxXmlAttributesPerElement: 256,
|
|
82
|
+
maxRelationshipsPerPart: 1e4,
|
|
83
|
+
maxTotalRelationships: 1e5,
|
|
84
|
+
maxBlocks: 1e5,
|
|
85
|
+
maxTextCharacters: 1e7,
|
|
86
|
+
maxSections: 4096,
|
|
87
|
+
maxProjectedStoryBlocks: 2e5,
|
|
88
|
+
maxProjectedStoryCharacters: 2e7,
|
|
89
|
+
maxStyles: 1e4,
|
|
90
|
+
maxStyleInheritanceDepth: 64,
|
|
91
|
+
maxNumberingDefinitions: 2e4,
|
|
92
|
+
maxComments: 1e4,
|
|
93
|
+
maxProjectedCommentWork: 1e6
|
|
94
|
+
};
|
|
95
|
+
var WORD_RELATIONSHIP_BASE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/";
|
|
96
|
+
var PACKAGE_RELATIONSHIP_BASE = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/";
|
|
97
|
+
var WORDPROCESSINGML_NAMESPACE = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
98
|
+
var CONTENT_TYPES_NAMESPACE = "http://schemas.openxmlformats.org/package/2006/content-types";
|
|
99
|
+
var RELATIONSHIPS_NAMESPACE = "http://schemas.openxmlformats.org/package/2006/relationships";
|
|
100
|
+
var COMMENTS_EXTENDED_NAMESPACE = "http://schemas.microsoft.com/office/word/2012/wordml";
|
|
101
|
+
async function importDocx(input, options = {}) {
|
|
102
|
+
const limits = resolveLimits(options);
|
|
103
|
+
const bytes = await readInput(input, limits.maxCompressedBytes);
|
|
104
|
+
const entries = parseZipDirectory(bytes, limits);
|
|
105
|
+
const opaqueParts = validatePartNames(entries);
|
|
106
|
+
const entriesByName = new Map(
|
|
107
|
+
entries.filter((entry) => !entry.directory).map((entry) => [entry.name, entry])
|
|
108
|
+
);
|
|
109
|
+
const partNames = new Set(entriesByName.keys());
|
|
110
|
+
validatePackageParts(partNames);
|
|
111
|
+
const budget = {
|
|
112
|
+
limits,
|
|
113
|
+
xmlBytes: 0,
|
|
114
|
+
xmlRetainedCharacters: 0,
|
|
115
|
+
xmlNodes: 0,
|
|
116
|
+
blocks: 0,
|
|
117
|
+
textCharacters: 0,
|
|
118
|
+
nextBlockId: 1
|
|
119
|
+
};
|
|
120
|
+
const parsedParts = /* @__PURE__ */ new Set();
|
|
121
|
+
const readXml = async (partName) => {
|
|
122
|
+
const entry = entriesByName.get(partName);
|
|
123
|
+
if (!entry)
|
|
124
|
+
throw new DocxImportError("invalid_package", "Required DOCX part is missing", partName);
|
|
125
|
+
if (parsedParts.has(partName)) {
|
|
126
|
+
throw new DocxImportError(
|
|
127
|
+
"invalid_package",
|
|
128
|
+
"DOCX part was consumed more than once",
|
|
129
|
+
partName
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
parsedParts.add(partName);
|
|
133
|
+
const value = await inflateBoundedZipEntry(
|
|
134
|
+
bytes,
|
|
135
|
+
entry,
|
|
136
|
+
limits.maxEntryUncompressedBytes,
|
|
137
|
+
docxZipFailure
|
|
138
|
+
);
|
|
139
|
+
return parseXmlPart(value, partName, budget);
|
|
140
|
+
};
|
|
141
|
+
for (const partName of partNames) {
|
|
142
|
+
if (isSyntaxOnlyXmlPart(partName) || opaqueParts.has(partName) && partName.endsWith(".xml"))
|
|
143
|
+
await readXml(partName);
|
|
144
|
+
}
|
|
145
|
+
const contentTypes = await readXml("[Content_Types].xml");
|
|
146
|
+
const contentTypeByPart = validateContentTypes(contentTypes, partNames);
|
|
147
|
+
const opaqueContentTypes = [];
|
|
148
|
+
for (const partName of opaqueParts) {
|
|
149
|
+
const contentType = contentTypeByPart.get(partName);
|
|
150
|
+
if (!contentType || !isSafeOpaqueContentType(partName, contentType)) {
|
|
151
|
+
throw new DocxImportError(
|
|
152
|
+
"unsupported_feature",
|
|
153
|
+
`Opaque DOCX part has an unsupported content type: ${contentType ?? "missing"}`,
|
|
154
|
+
partName
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
opaqueContentTypes.push({ partName, contentType });
|
|
158
|
+
}
|
|
159
|
+
if (partNames.has("word/footnotes.xml"))
|
|
160
|
+
validateEmptyNotePart(await readXml("word/footnotes.xml"), "footnote", "word/footnotes.xml");
|
|
161
|
+
if (partNames.has("word/endnotes.xml"))
|
|
162
|
+
validateEmptyNotePart(await readXml("word/endnotes.xml"), "endnote", "word/endnotes.xml");
|
|
163
|
+
const settings = partNames.has("word/settings.xml") ? validateSettings(await readXml("word/settings.xml")) : { evenAndOddHeaders: false, trackRevisions: false };
|
|
164
|
+
let totalRelationships = 0;
|
|
165
|
+
const readRelationships = async (partName) => {
|
|
166
|
+
const parsed = parseRelationships(
|
|
167
|
+
await readXml(partName),
|
|
168
|
+
partName,
|
|
169
|
+
limits.maxRelationshipsPerPart
|
|
170
|
+
);
|
|
171
|
+
if (totalRelationships + parsed.length > limits.maxTotalRelationships) {
|
|
172
|
+
throw limitError("DOCX exceeds maxTotalRelationships", partName);
|
|
173
|
+
}
|
|
174
|
+
totalRelationships += parsed.length;
|
|
175
|
+
return parsed;
|
|
176
|
+
};
|
|
177
|
+
const rootRelationships = await readRelationships("_rels/.rels");
|
|
178
|
+
const officeDocuments = rootRelationships.filter(
|
|
179
|
+
(relationship) => relationship.type === `${WORD_RELATIONSHIP_BASE}officeDocument`
|
|
180
|
+
);
|
|
181
|
+
const officeDocument = officeDocuments[0];
|
|
182
|
+
if (officeDocuments.length !== 1 || !officeDocument || officeDocument.partName !== "word/document.xml") {
|
|
183
|
+
throw new DocxImportError(
|
|
184
|
+
"invalid_package",
|
|
185
|
+
"DOCX officeDocument must target word/document.xml",
|
|
186
|
+
"_rels/.rels"
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const documentRelationships = partNames.has("word/_rels/document.xml.rels") ? await readRelationships("word/_rels/document.xml.rels") : [];
|
|
190
|
+
const auxiliaryRelationships = [];
|
|
191
|
+
for (const partName of partNames) {
|
|
192
|
+
if (!partName.endsWith(".rels") || partName === "_rels/.rels" || partName === "word/_rels/document.xml.rels")
|
|
193
|
+
continue;
|
|
194
|
+
for (const relationship of await readRelationships(partName))
|
|
195
|
+
auxiliaryRelationships.push(relationship);
|
|
196
|
+
}
|
|
197
|
+
const allRelationships = [
|
|
198
|
+
...rootRelationships,
|
|
199
|
+
...documentRelationships,
|
|
200
|
+
...auxiliaryRelationships
|
|
201
|
+
];
|
|
202
|
+
const opaqueRelationships = validateRelationships(allRelationships, opaqueParts);
|
|
203
|
+
for (const relationship of allRelationships) {
|
|
204
|
+
if (!partNames.has(relationship.partName)) {
|
|
205
|
+
throw new DocxImportError(
|
|
206
|
+
"invalid_package",
|
|
207
|
+
"Relationship targets a missing package part",
|
|
208
|
+
relationship.partName
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
validateRelationshipCoverage(partNames, allRelationships);
|
|
213
|
+
const relationshipById = new Map(
|
|
214
|
+
documentRelationships.map((relationship) => [relationship.id, relationship])
|
|
215
|
+
);
|
|
216
|
+
const styles = partNames.has("word/styles.xml") ? parseStyles(await readXml("word/styles.xml"), limits) : [];
|
|
217
|
+
const lists = partNames.has("word/numbering.xml") ? parseNumbering(await readXml("word/numbering.xml"), limits) : [];
|
|
218
|
+
const trackedChanges = [];
|
|
219
|
+
const mainContext = { budget, trackedChanges, idPrefix: "body" };
|
|
220
|
+
const main = parseMainDocument(await readXml("word/document.xml"), mainContext, relationshipById);
|
|
221
|
+
resolveSectionStoryInheritance(main.sections);
|
|
222
|
+
const headers = [];
|
|
223
|
+
const footers = [];
|
|
224
|
+
const parsedStoryParts = /* @__PURE__ */ new Set();
|
|
225
|
+
for (const relationship of documentRelationships) {
|
|
226
|
+
const kind = relationship.type === `${WORD_RELATIONSHIP_BASE}header` ? "header" : relationship.type === `${WORD_RELATIONSHIP_BASE}footer` ? "footer" : void 0;
|
|
227
|
+
if (!kind) continue;
|
|
228
|
+
if (parsedStoryParts.has(relationship.partName)) continue;
|
|
229
|
+
parsedStoryParts.add(relationship.partName);
|
|
230
|
+
const root = await readXml(relationship.partName);
|
|
231
|
+
requireLocalName(root, kind === "header" ? "hdr" : "ftr", relationship.partName);
|
|
232
|
+
requireWordNamespace(root, relationship.partName);
|
|
233
|
+
const context = {
|
|
234
|
+
budget,
|
|
235
|
+
trackedChanges,
|
|
236
|
+
idPrefix: `${kind}:${relationship.partName}`
|
|
237
|
+
};
|
|
238
|
+
const blocks = parseBlockContainer(root, context);
|
|
239
|
+
(kind === "header" ? headers : footers).push({ kind, partName: relationship.partName, blocks });
|
|
240
|
+
}
|
|
241
|
+
validateProjectedStories(
|
|
242
|
+
main.sections,
|
|
243
|
+
headers,
|
|
244
|
+
footers,
|
|
245
|
+
limits.maxProjectedStoryBlocks,
|
|
246
|
+
limits.maxProjectedStoryCharacters
|
|
247
|
+
);
|
|
248
|
+
const commentsExtended = partNames.has("word/commentsExtended.xml") ? parseCommentsExtended(await readXml("word/commentsExtended.xml"), limits.maxComments) : /* @__PURE__ */ new Map();
|
|
249
|
+
const comments = partNames.has("word/comments.xml") ? parseComments(
|
|
250
|
+
await readXml("word/comments.xml"),
|
|
251
|
+
{ budget, trackedChanges, idPrefix: "comment" },
|
|
252
|
+
commentsExtended,
|
|
253
|
+
limits.maxComments
|
|
254
|
+
) : [];
|
|
255
|
+
if (commentsExtended.size > 0 && comments.length === 0) {
|
|
256
|
+
throw new DocxImportError(
|
|
257
|
+
"invalid_package",
|
|
258
|
+
"commentsExtended.xml exists without comments.xml",
|
|
259
|
+
"word/commentsExtended.xml"
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
validateCommentReferences(main.blocks, headers, footers, comments);
|
|
263
|
+
const rootCommentCount = comments.reduce(
|
|
264
|
+
(count, comment) => count + Number(comment.parentId === void 0),
|
|
265
|
+
0
|
|
266
|
+
);
|
|
267
|
+
if (rootCommentCount > 0 && rootCommentCount * (budget.blocks + comments.length) > limits.maxProjectedCommentWork) {
|
|
268
|
+
throw limitError("DOCX exceeds maxProjectedCommentWork", "word/comments.xml");
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
format: "docx",
|
|
272
|
+
schemaVersion: 1,
|
|
273
|
+
evenAndOddHeaders: settings.evenAndOddHeaders,
|
|
274
|
+
trackRevisions: settings.trackRevisions,
|
|
275
|
+
blocks: main.blocks,
|
|
276
|
+
sections: main.sections,
|
|
277
|
+
styles,
|
|
278
|
+
lists,
|
|
279
|
+
headers,
|
|
280
|
+
footers,
|
|
281
|
+
comments,
|
|
282
|
+
trackedChanges,
|
|
283
|
+
opaqueContent: {
|
|
284
|
+
parts: [...opaqueParts].sort(compareStrings),
|
|
285
|
+
relationships: opaqueRelationships.map(({ sourcePart, type, partName }) => ({ sourcePart, type, targetPart: partName })).sort(compareOpaqueRelationships),
|
|
286
|
+
contentTypes: opaqueContentTypes.sort(compareOpaqueContentTypes)
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function resolveLimits(options) {
|
|
291
|
+
if (options === null || typeof options !== "object" || Array.isArray(options) || Object.getPrototypeOf(options) !== Object.prototype || Object.getOwnPropertySymbols(options).length > 0)
|
|
292
|
+
throw new DocxImportError("invalid_input", "DOCX import limits must be a plain data object");
|
|
293
|
+
for (const [name, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(options))) {
|
|
294
|
+
if (!("value" in descriptor) || !descriptor.enumerable) {
|
|
295
|
+
throw new DocxImportError(
|
|
296
|
+
"invalid_input",
|
|
297
|
+
"DOCX import limits must contain only enumerable data properties"
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
if (!Object.prototype.hasOwnProperty.call(DEFAULT_LIMITS, name)) {
|
|
301
|
+
throw new DocxImportError("invalid_input", `Unknown DOCX import limit: ${name}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const limits = { ...DEFAULT_LIMITS, ...options };
|
|
305
|
+
for (const [name, value] of Object.entries(limits)) {
|
|
306
|
+
if (!Number.isFinite(value) || value <= 0 || name !== "maxCompressionRatio" && !Number.isInteger(value)) {
|
|
307
|
+
throw new DocxImportError("invalid_input", `Invalid DOCX import limit: ${name}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
for (const [name, cap] of Object.entries(DEFAULT_LIMITS)) {
|
|
311
|
+
if (limits[name] > cap)
|
|
312
|
+
throw new DocxImportError(
|
|
313
|
+
"invalid_input",
|
|
314
|
+
`${name} cannot exceed the hard safety cap of ${cap}`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
return limits;
|
|
318
|
+
}
|
|
319
|
+
async function readInput(input, maxBytes) {
|
|
320
|
+
if (input instanceof ArrayBuffer) {
|
|
321
|
+
if (input.byteLength > maxBytes) throw limitError("Compressed DOCX exceeds maxCompressedBytes");
|
|
322
|
+
return new Uint8Array(input);
|
|
323
|
+
}
|
|
324
|
+
if (typeof Blob !== "undefined" && input instanceof Blob) {
|
|
325
|
+
if (input.size > maxBytes) throw limitError("Compressed DOCX exceeds maxCompressedBytes");
|
|
326
|
+
return new Uint8Array(await input.arrayBuffer());
|
|
327
|
+
}
|
|
328
|
+
throw new DocxImportError("invalid_input", "DOCX input must be a Blob or ArrayBuffer");
|
|
329
|
+
}
|
|
330
|
+
function parseZipDirectory(bytes, limits) {
|
|
331
|
+
return parseBoundedZip(
|
|
332
|
+
bytes,
|
|
333
|
+
{
|
|
334
|
+
entries: limits.maxEntries,
|
|
335
|
+
compressedEntryBytes: limits.maxEntryCompressedBytes,
|
|
336
|
+
expandedEntryBytes: limits.maxEntryUncompressedBytes,
|
|
337
|
+
expandedBytes: limits.maxTotalUncompressedBytes,
|
|
338
|
+
compressionRatio: limits.maxCompressionRatio
|
|
339
|
+
},
|
|
340
|
+
docxZipFailure
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
function docxZipFailure(kind, message, partName) {
|
|
344
|
+
const code = kind === "limit" ? "limit_exceeded" : kind === "invalid" ? "invalid_zip" : kind === "platform" ? "unsupported_platform" : "unsupported_feature";
|
|
345
|
+
throw new DocxImportError(code, message, partName);
|
|
346
|
+
}
|
|
347
|
+
function validatePartNames(parts) {
|
|
348
|
+
const opaqueParts = /* @__PURE__ */ new Set();
|
|
349
|
+
for (const entry of parts) {
|
|
350
|
+
if (entry.directory) continue;
|
|
351
|
+
const name = entry.name;
|
|
352
|
+
const supported = name === "[Content_Types].xml" || name === "_rels/.rels" || /^docProps\/(?:core|app|custom)\.xml$/.test(name) || name === "word/document.xml" || name === "word/styles.xml" || name === "word/numbering.xml" || name === "word/comments.xml" || name === "word/commentsExtended.xml" || name === "word/footnotes.xml" || name === "word/endnotes.xml" || name === "word/settings.xml" || name === "word/webSettings.xml" || name === "word/fontTable.xml" || /^word\/theme\/theme\d+\.xml$/.test(name) || /^word\/(?:header|footer)\d+\.xml$/.test(name) || /^word\/_rels\/(?:document|header\d+|footer\d+|footnotes|endnotes|comments|fontTable)\.xml\.rels$/.test(
|
|
353
|
+
name
|
|
354
|
+
);
|
|
355
|
+
if (supported) {
|
|
356
|
+
if (isSyntaxOnlyXmlPart(name)) opaqueParts.add(name);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (isSafeOpaquePartName(name)) {
|
|
360
|
+
opaqueParts.add(name);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const active = /(?:^|\/)(?:vbaProject|macros?|activeX|embeddings|oleObject|customUI|webExtensions?|signatures?|drm|encryptedPackage)(?:\/|\.|$)/i.test(
|
|
364
|
+
name
|
|
365
|
+
);
|
|
366
|
+
const media = /(?:^|\/)media(?:\/|$)|\.(?:png|jpe?g|gif|webp|svg|emf|wmf|tiff?|bmp|bin)$/i.test(
|
|
367
|
+
name
|
|
368
|
+
);
|
|
369
|
+
throw new DocxImportError(
|
|
370
|
+
"unsupported_feature",
|
|
371
|
+
active ? "Active, executable, embedded, encrypted, or signed DOCX content is unsupported" : media ? "DOCX media is not represented by the editable document model" : "DOCX part is neither represented nor in the inert opaque-part allowlist",
|
|
372
|
+
name
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return opaqueParts;
|
|
376
|
+
}
|
|
377
|
+
function isSafeOpaquePartName(partName) {
|
|
378
|
+
return /^customXml\/item\d+\.xml$/.test(partName) || /^customXml\/itemProps\d+\.xml$/.test(partName) || /^customXml\/_rels\/item\d+\.xml\.rels$/.test(partName) || partName === "word/glossary/document.xml" || partName === "word/glossary/_rels/document.xml.rels";
|
|
379
|
+
}
|
|
380
|
+
function validatePackageParts(parts) {
|
|
381
|
+
for (const required of ["[Content_Types].xml", "_rels/.rels", "word/document.xml"]) {
|
|
382
|
+
if (!parts.has(required))
|
|
383
|
+
throw new DocxImportError("invalid_package", "Required DOCX part is missing", required);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function isSyntaxOnlyXmlPart(partName) {
|
|
387
|
+
return /^docProps\/(?:core|app|custom)\.xml$/.test(partName) || partName === "word/webSettings.xml" || partName === "word/fontTable.xml" || /^word\/theme\/theme\d+\.xml$/.test(partName);
|
|
388
|
+
}
|
|
389
|
+
function parseXmlPart(bytes, partName, budget) {
|
|
390
|
+
if (bytes.byteLength > budget.limits.maxXmlBytes)
|
|
391
|
+
throw limitError("XML part exceeds maxXmlBytes", partName);
|
|
392
|
+
budget.xmlBytes += bytes.byteLength;
|
|
393
|
+
if (budget.xmlBytes > budget.limits.maxTotalXmlBytes)
|
|
394
|
+
throw limitError("DOCX exceeds maxTotalXmlBytes", partName);
|
|
395
|
+
const xml = decodeUtf8(bytes, "XML", partName);
|
|
396
|
+
if (/<!\s*(?:DOCTYPE|ENTITY)\b/i.test(xml)) {
|
|
397
|
+
throw new DocxImportError(
|
|
398
|
+
"invalid_xml",
|
|
399
|
+
"DOCTYPE and entity declarations are forbidden",
|
|
400
|
+
partName
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
const parsed = parseXml(
|
|
404
|
+
xml,
|
|
405
|
+
partName,
|
|
406
|
+
budget.limits.maxXmlDepth,
|
|
407
|
+
budget.limits.maxXmlNodes - budget.xmlNodes,
|
|
408
|
+
budget.limits.maxRetainedXmlCharacters - budget.xmlRetainedCharacters,
|
|
409
|
+
budget.limits.maxXmlAttributesPerElement,
|
|
410
|
+
partName.endsWith(".rels") ? budget.limits.maxRelationshipsPerPart : void 0
|
|
411
|
+
);
|
|
412
|
+
validateNamespaceWellFormedness(parsed.root, partName);
|
|
413
|
+
budget.xmlNodes += parsed.nodeCount;
|
|
414
|
+
if (budget.xmlNodes > budget.limits.maxXmlNodes)
|
|
415
|
+
throw limitError("DOCX exceeds maxXmlNodes", partName);
|
|
416
|
+
budget.xmlRetainedCharacters += parsed.retainedCharacters;
|
|
417
|
+
if (budget.xmlRetainedCharacters > budget.limits.maxRetainedXmlCharacters) {
|
|
418
|
+
throw limitError("DOCX exceeds maxRetainedXmlCharacters", partName);
|
|
419
|
+
}
|
|
420
|
+
return parsed.root;
|
|
421
|
+
}
|
|
422
|
+
function parseXml(xml, partName, maxDepth, remainingNodes, remainingRetainedCharacters, maxAttributesPerElement, maxRootElementChildren) {
|
|
423
|
+
const stack = [];
|
|
424
|
+
let root;
|
|
425
|
+
let cursor = 0;
|
|
426
|
+
let nodeCount = 0;
|
|
427
|
+
let retainedCharacters = 0;
|
|
428
|
+
let sawXmlDeclaration = false;
|
|
429
|
+
let rootElementChildren = 0;
|
|
430
|
+
const addNode = (node) => {
|
|
431
|
+
nodeCount += 1;
|
|
432
|
+
if (nodeCount > remainingNodes) throw limitError("DOCX exceeds maxXmlNodes", partName);
|
|
433
|
+
retainedCharacters += retainedXmlCharacters(node);
|
|
434
|
+
if (retainedCharacters > remainingRetainedCharacters) {
|
|
435
|
+
throw limitError("DOCX exceeds maxRetainedXmlCharacters", partName);
|
|
436
|
+
}
|
|
437
|
+
const parent = stack.at(-1);
|
|
438
|
+
if (parent) {
|
|
439
|
+
if (parent === root && node.type === "element" && maxRootElementChildren !== void 0) {
|
|
440
|
+
if (rootElementChildren >= maxRootElementChildren) {
|
|
441
|
+
throw limitError("Relationships part exceeds maxRelationshipsPerPart", partName);
|
|
442
|
+
}
|
|
443
|
+
rootElementChildren += 1;
|
|
444
|
+
}
|
|
445
|
+
parent.children.push(node);
|
|
446
|
+
} else if (node.type === "text" && isOnlyXmlWhitespace(node.value)) return;
|
|
447
|
+
else if (node.type === "element" && !root) root = node;
|
|
448
|
+
else
|
|
449
|
+
throw new DocxImportError(
|
|
450
|
+
"invalid_xml",
|
|
451
|
+
"XML has multiple roots or text outside the root",
|
|
452
|
+
partName
|
|
453
|
+
);
|
|
454
|
+
};
|
|
455
|
+
while (cursor < xml.length) {
|
|
456
|
+
const opening = xml.indexOf("<", cursor);
|
|
457
|
+
if (opening < 0) {
|
|
458
|
+
const tail = xml.slice(cursor);
|
|
459
|
+
if (tail.length > 0) addNode({ type: "text", value: decodeXmlEntities(tail, partName) });
|
|
460
|
+
cursor = xml.length;
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
if (opening > cursor)
|
|
464
|
+
addNode({ type: "text", value: decodeXmlEntities(xml.slice(cursor, opening), partName) });
|
|
465
|
+
if (xml.startsWith("<!--", opening)) {
|
|
466
|
+
const end2 = xml.indexOf("-->", opening + 4);
|
|
467
|
+
const content = end2 < 0 ? "" : xml.slice(opening + 4, end2);
|
|
468
|
+
if (end2 < 0 || content.includes("--") || content.endsWith("-"))
|
|
469
|
+
throw new DocxImportError("invalid_xml", "Malformed XML comment", partName);
|
|
470
|
+
validateXmlCharacters(content, partName);
|
|
471
|
+
cursor = end2 + 3;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
if (xml.startsWith("<?", opening)) {
|
|
475
|
+
const end2 = xml.indexOf("?>", opening + 2);
|
|
476
|
+
if (end2 < 0)
|
|
477
|
+
throw new DocxImportError("invalid_xml", "Malformed XML processing instruction", partName);
|
|
478
|
+
const instruction = xml.slice(opening + 2, end2);
|
|
479
|
+
const validDeclaration = /^xml[\x20\t\r\n]+version=(?:"1\.[01]"|'1\.[01]')(?:[\x20\t\r\n]+encoding=(?:"[Uu][Tt][Ff]-8"|'[Uu][Tt][Ff]-8'))?(?:[\x20\t\r\n]+standalone=(?:"(?:yes|no)"|'(?:yes|no)'))?[\x20\t\r\n]*$/.test(
|
|
480
|
+
instruction
|
|
481
|
+
);
|
|
482
|
+
if (!validDeclaration || opening !== 0 || root || stack.length > 0 || sawXmlDeclaration) {
|
|
483
|
+
throw new DocxImportError(
|
|
484
|
+
"invalid_xml",
|
|
485
|
+
"XML processing instructions are unsupported",
|
|
486
|
+
partName
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
sawXmlDeclaration = true;
|
|
490
|
+
cursor = end2 + 2;
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (xml.startsWith("<!", opening))
|
|
494
|
+
throw new DocxImportError(
|
|
495
|
+
"invalid_xml",
|
|
496
|
+
"XML declarations and CDATA are unsupported",
|
|
497
|
+
partName
|
|
498
|
+
);
|
|
499
|
+
const end = findXmlTagEnd(xml, opening + 1, partName);
|
|
500
|
+
const source = xml.slice(opening + 1, end);
|
|
501
|
+
if (source.startsWith("/")) {
|
|
502
|
+
const closing = /^\/([^\x20\t\r\n]+)[\x20\t\r\n]*$/.exec(source);
|
|
503
|
+
const name = closing?.[1];
|
|
504
|
+
if (!name || !XML_QNAME.test(name))
|
|
505
|
+
throw new DocxImportError("invalid_xml", "Malformed XML closing tag", partName);
|
|
506
|
+
const current = stack.pop();
|
|
507
|
+
if (!current || current.name !== name)
|
|
508
|
+
throw new DocxImportError("invalid_xml", "Mismatched XML closing tag", partName);
|
|
509
|
+
} else {
|
|
510
|
+
if (isXmlSpace(source[0]))
|
|
511
|
+
throw new DocxImportError("invalid_xml", "XML start tag has leading whitespace", partName);
|
|
512
|
+
const selfClosing = /\/[\x20\t\r\n]*$/.test(source);
|
|
513
|
+
const element = parseXmlStartTag(
|
|
514
|
+
selfClosing ? source.replace(/\/[\x20\t\r\n]*$/, "") : source,
|
|
515
|
+
partName,
|
|
516
|
+
maxAttributesPerElement
|
|
517
|
+
);
|
|
518
|
+
addNode(element);
|
|
519
|
+
if (!selfClosing) {
|
|
520
|
+
stack.push(element);
|
|
521
|
+
if (stack.length > maxDepth) throw limitError("XML exceeds maxXmlDepth", partName);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
cursor = end + 1;
|
|
525
|
+
}
|
|
526
|
+
if (stack.length > 0)
|
|
527
|
+
throw new DocxImportError("invalid_xml", "XML document ended with unclosed elements", partName);
|
|
528
|
+
if (!root) throw new DocxImportError("invalid_xml", "XML root element is missing", partName);
|
|
529
|
+
return { root, nodeCount, retainedCharacters };
|
|
530
|
+
}
|
|
531
|
+
function retainedXmlCharacters(node) {
|
|
532
|
+
if (node.type === "text") return node.value.length;
|
|
533
|
+
let characters = node.name.length + node.localName.length;
|
|
534
|
+
for (const [name, value] of node.attributes) characters += name.length + value.length;
|
|
535
|
+
return characters;
|
|
536
|
+
}
|
|
537
|
+
function findXmlTagEnd(xml, start, partName) {
|
|
538
|
+
let quote;
|
|
539
|
+
for (let index = start; index < xml.length; index += 1) {
|
|
540
|
+
const character = xml[index];
|
|
541
|
+
if (quote) {
|
|
542
|
+
if (character === quote) quote = void 0;
|
|
543
|
+
} else if (character === '"' || character === "'") quote = character;
|
|
544
|
+
else if (character === ">") return index;
|
|
545
|
+
}
|
|
546
|
+
throw new DocxImportError("invalid_xml", "Unterminated XML tag", partName);
|
|
547
|
+
}
|
|
548
|
+
function parseXmlStartTag(source, partName, maxAttributes) {
|
|
549
|
+
let cursor = 0;
|
|
550
|
+
const skipSpace = () => {
|
|
551
|
+
while (cursor < source.length && isXmlSpace(source[cursor])) cursor += 1;
|
|
552
|
+
};
|
|
553
|
+
skipSpace();
|
|
554
|
+
const nameStart = cursor;
|
|
555
|
+
while (cursor < source.length && !isXmlSpace(source[cursor])) cursor += 1;
|
|
556
|
+
const name = source.slice(nameStart, cursor);
|
|
557
|
+
if (!XML_QNAME.test(name))
|
|
558
|
+
throw new DocxImportError("invalid_xml", "Invalid XML element name", partName);
|
|
559
|
+
const attributes = /* @__PURE__ */ new Map();
|
|
560
|
+
const localAttributeNames = /* @__PURE__ */ new Set();
|
|
561
|
+
while (cursor < source.length) {
|
|
562
|
+
if (!isXmlSpace(source[cursor]))
|
|
563
|
+
throw new DocxImportError(
|
|
564
|
+
"invalid_xml",
|
|
565
|
+
"XML attributes must be separated by whitespace",
|
|
566
|
+
partName
|
|
567
|
+
);
|
|
568
|
+
skipSpace();
|
|
569
|
+
if (cursor >= source.length) break;
|
|
570
|
+
const attributeStart = cursor;
|
|
571
|
+
while (cursor < source.length && !isXmlSpace(source[cursor]) && source[cursor] !== "=")
|
|
572
|
+
cursor += 1;
|
|
573
|
+
const attributeName = source.slice(attributeStart, cursor);
|
|
574
|
+
if (!XML_QNAME.test(attributeName))
|
|
575
|
+
throw new DocxImportError("invalid_xml", "Invalid XML attribute name", partName);
|
|
576
|
+
skipSpace();
|
|
577
|
+
if (source[cursor] !== "=")
|
|
578
|
+
throw new DocxImportError("invalid_xml", "XML attribute lacks a value", partName);
|
|
579
|
+
cursor += 1;
|
|
580
|
+
skipSpace();
|
|
581
|
+
const quote = source[cursor];
|
|
582
|
+
if (quote !== '"' && quote !== "'")
|
|
583
|
+
throw new DocxImportError("invalid_xml", "XML attribute value must be quoted", partName);
|
|
584
|
+
cursor += 1;
|
|
585
|
+
const valueStart = cursor;
|
|
586
|
+
const valueEnd = source.indexOf(quote, valueStart);
|
|
587
|
+
if (valueEnd < 0)
|
|
588
|
+
throw new DocxImportError("invalid_xml", "Unterminated XML attribute value", partName);
|
|
589
|
+
if (attributes.has(attributeName))
|
|
590
|
+
throw new DocxImportError("invalid_xml", "Duplicate XML attribute", partName);
|
|
591
|
+
if (attributes.size >= maxAttributes)
|
|
592
|
+
throw limitError("XML element exceeds maxXmlAttributesPerElement", partName);
|
|
593
|
+
const attributeLocalName = localName(attributeName);
|
|
594
|
+
const namespaceDeclaration = attributeName === "xmlns" || attributeName.startsWith("xmlns:");
|
|
595
|
+
if (!namespaceDeclaration && localAttributeNames.has(attributeLocalName)) {
|
|
596
|
+
throw new DocxImportError(
|
|
597
|
+
"invalid_xml",
|
|
598
|
+
"Ambiguous namespace-local XML attributes",
|
|
599
|
+
partName
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
localAttributeNames.add(attributeLocalName);
|
|
603
|
+
const rawValue = source.slice(valueStart, valueEnd);
|
|
604
|
+
if (rawValue.includes("<"))
|
|
605
|
+
throw new DocxImportError(
|
|
606
|
+
"invalid_xml",
|
|
607
|
+
"XML attribute contains an unescaped less-than sign",
|
|
608
|
+
partName
|
|
609
|
+
);
|
|
610
|
+
attributes.set(attributeName, decodeXmlEntities(rawValue, partName));
|
|
611
|
+
cursor = valueEnd + 1;
|
|
612
|
+
}
|
|
613
|
+
return { type: "element", name, localName: localName(name), attributes, children: [] };
|
|
614
|
+
}
|
|
615
|
+
var XML_QNAME = /^(?:[A-Za-z_][A-Za-z0-9_.-]*:)?[A-Za-z_][A-Za-z0-9_.-]*$/;
|
|
616
|
+
function isXmlSpace(value) {
|
|
617
|
+
return value === " " || value === " " || value === "\r" || value === "\n";
|
|
618
|
+
}
|
|
619
|
+
function isOnlyXmlWhitespace(value) {
|
|
620
|
+
for (let index = 0; index < value.length; index += 1) if (!isXmlSpace(value[index])) return false;
|
|
621
|
+
return true;
|
|
622
|
+
}
|
|
623
|
+
function decodeXmlEntities(value, partName) {
|
|
624
|
+
validateXmlCharacters(value, partName);
|
|
625
|
+
if (value.includes("]]>"))
|
|
626
|
+
throw new DocxImportError("invalid_xml", "XML contains malformed character data", partName);
|
|
627
|
+
let ampersand = value.indexOf("&");
|
|
628
|
+
if (ampersand < 0) return value;
|
|
629
|
+
const output = [];
|
|
630
|
+
let cursor = 0;
|
|
631
|
+
let referenceCount = 0;
|
|
632
|
+
while (ampersand >= 0) {
|
|
633
|
+
referenceCount += 1;
|
|
634
|
+
if (referenceCount > 65536)
|
|
635
|
+
throw limitError("XML text node exceeds the entity-reference safety cap", partName);
|
|
636
|
+
output.push(value.slice(cursor, ampersand));
|
|
637
|
+
const semicolon = value.indexOf(";", ampersand + 1);
|
|
638
|
+
const nestedAmpersand = value.indexOf("&", ampersand + 1);
|
|
639
|
+
if (semicolon < 0 || nestedAmpersand >= 0 && nestedAmpersand < semicolon) {
|
|
640
|
+
throw new DocxImportError(
|
|
641
|
+
"invalid_xml",
|
|
642
|
+
"XML contains an unterminated entity reference",
|
|
643
|
+
partName
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
const entity = value.slice(ampersand + 1, semicolon);
|
|
647
|
+
if (entity.length === 0 || entity.length > 16) {
|
|
648
|
+
throw new DocxImportError(
|
|
649
|
+
"invalid_xml",
|
|
650
|
+
"XML contains a malformed entity reference",
|
|
651
|
+
partName
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
output.push(decodeXmlEntity(entity, partName));
|
|
655
|
+
cursor = semicolon + 1;
|
|
656
|
+
ampersand = value.indexOf("&", cursor);
|
|
657
|
+
}
|
|
658
|
+
output.push(value.slice(cursor));
|
|
659
|
+
return output.join("");
|
|
660
|
+
}
|
|
661
|
+
function decodeXmlEntity(entity, partName) {
|
|
662
|
+
if (entity === "amp") return "&";
|
|
663
|
+
if (entity === "lt") return "<";
|
|
664
|
+
if (entity === "gt") return ">";
|
|
665
|
+
if (entity === "quot") return '"';
|
|
666
|
+
if (entity === "apos") return "'";
|
|
667
|
+
const decimal = /^#([0-9]+)$/.exec(entity);
|
|
668
|
+
const hex = /^#x([0-9a-f]+)$/i.exec(entity);
|
|
669
|
+
const codePoint = decimal ? Number(decimal[1]) : hex ? Number.parseInt(hex[1] ?? "", 16) : NaN;
|
|
670
|
+
if (!Number.isInteger(codePoint) || !isValidXmlCodePoint(codePoint)) {
|
|
671
|
+
throw new DocxImportError(
|
|
672
|
+
"invalid_xml",
|
|
673
|
+
`Unsupported or invalid XML entity: &${entity};`,
|
|
674
|
+
partName
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
return String.fromCodePoint(codePoint);
|
|
678
|
+
}
|
|
679
|
+
function validateXmlCharacters(value, partName) {
|
|
680
|
+
for (const character of value) {
|
|
681
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
682
|
+
if (!isValidXmlCodePoint(codePoint)) {
|
|
683
|
+
throw new DocxImportError("invalid_xml", "XML contains a forbidden character", partName);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
function isValidXmlCodePoint(codePoint) {
|
|
688
|
+
return codePoint === 9 || codePoint === 10 || codePoint === 13 || codePoint >= 32 && codePoint <= 55295 || codePoint >= 57344 && codePoint <= 65533 || codePoint >= 65536 && codePoint <= 1114111;
|
|
689
|
+
}
|
|
690
|
+
function validateContentTypes(root, parts) {
|
|
691
|
+
requireLocalName(root, "Types", "[Content_Types].xml");
|
|
692
|
+
requireDefaultNamespaceTree(root, CONTENT_TYPES_NAMESPACE, "[Content_Types].xml");
|
|
693
|
+
const defaults = /* @__PURE__ */ new Map();
|
|
694
|
+
const overrides = /* @__PURE__ */ new Map();
|
|
695
|
+
for (const entry of elementChildren(root)) {
|
|
696
|
+
if (entry.name !== "Default" && entry.name !== "Override" || entry.localName !== entry.name) {
|
|
697
|
+
throw new DocxImportError(
|
|
698
|
+
"invalid_package",
|
|
699
|
+
"Unknown content-type declaration",
|
|
700
|
+
"[Content_Types].xml"
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
assertExactAttributes(
|
|
704
|
+
entry,
|
|
705
|
+
entry.name === "Default" ? /* @__PURE__ */ new Set(["Extension", "ContentType"]) : /* @__PURE__ */ new Set(["PartName", "ContentType"]),
|
|
706
|
+
"[Content_Types].xml"
|
|
707
|
+
);
|
|
708
|
+
requireLeafElement(entry, "[Content_Types].xml");
|
|
709
|
+
const contentType = exactAttribute(entry, "ContentType") ?? "";
|
|
710
|
+
if (contentType.length === 0)
|
|
711
|
+
throw new DocxImportError(
|
|
712
|
+
"invalid_package",
|
|
713
|
+
"Content-type declaration lacks ContentType",
|
|
714
|
+
"[Content_Types].xml"
|
|
715
|
+
);
|
|
716
|
+
if (/(?:macroEnabled|vbaProject|activeX|oleObject|encryptedPackage|digital-signature|customUI|webExtension)/i.test(
|
|
717
|
+
contentType
|
|
718
|
+
)) {
|
|
719
|
+
throw new DocxImportError(
|
|
720
|
+
"unsupported_feature",
|
|
721
|
+
`Unsupported DOCX content type: ${contentType}`,
|
|
722
|
+
"[Content_Types].xml"
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
if (entry.localName === "Default") {
|
|
726
|
+
const extension = requiredExactAttribute(
|
|
727
|
+
entry,
|
|
728
|
+
"Extension",
|
|
729
|
+
"[Content_Types].xml"
|
|
730
|
+
).toLowerCase();
|
|
731
|
+
if (!/^[a-z0-9]+$/.test(extension) || defaults.has(extension)) {
|
|
732
|
+
throw new DocxImportError(
|
|
733
|
+
"invalid_package",
|
|
734
|
+
"Invalid or duplicate default content type",
|
|
735
|
+
"[Content_Types].xml"
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
defaults.set(extension, contentType);
|
|
739
|
+
} else {
|
|
740
|
+
const declared = requiredExactAttribute(entry, "PartName", "[Content_Types].xml");
|
|
741
|
+
if (declared.length > 1025)
|
|
742
|
+
throw limitError("Override PartName exceeds its safety cap", "[Content_Types].xml");
|
|
743
|
+
if (!declared.startsWith("/") || declared.includes("\\") || /%(?:2e|2f|5c)/i.test(declared)) {
|
|
744
|
+
throw new DocxImportError(
|
|
745
|
+
"invalid_package",
|
|
746
|
+
"Unsafe override PartName",
|
|
747
|
+
"[Content_Types].xml"
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
const partName = declared.slice(1);
|
|
751
|
+
let segmentCount = 1;
|
|
752
|
+
for (let index = 0; index < partName.length; index += 1) {
|
|
753
|
+
if (partName.charCodeAt(index) === 47 && ++segmentCount > 256) {
|
|
754
|
+
throw limitError(
|
|
755
|
+
"Override PartName exceeds its path-segment safety cap",
|
|
756
|
+
"[Content_Types].xml"
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
const segments = partName.split("/");
|
|
761
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
762
|
+
throw new DocxImportError(
|
|
763
|
+
"invalid_package",
|
|
764
|
+
"Unsafe override PartName",
|
|
765
|
+
"[Content_Types].xml"
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
const key = partName.toLowerCase();
|
|
769
|
+
if (overrides.has(key))
|
|
770
|
+
throw new DocxImportError(
|
|
771
|
+
"invalid_package",
|
|
772
|
+
"Duplicate override content type",
|
|
773
|
+
"[Content_Types].xml"
|
|
774
|
+
);
|
|
775
|
+
if (!parts.has(partName))
|
|
776
|
+
throw new DocxImportError(
|
|
777
|
+
"invalid_package",
|
|
778
|
+
"Content-type override targets a missing part",
|
|
779
|
+
partName
|
|
780
|
+
);
|
|
781
|
+
overrides.set(key, contentType);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
const contentTypeByPart = /* @__PURE__ */ new Map();
|
|
785
|
+
for (const partName of parts.keys()) {
|
|
786
|
+
if (partName === "[Content_Types].xml") continue;
|
|
787
|
+
const extensionIndex = partName.lastIndexOf(".");
|
|
788
|
+
const extension = extensionIndex < 0 ? "" : partName.slice(extensionIndex + 1).toLowerCase();
|
|
789
|
+
const contentType = overrides.get(partName.toLowerCase()) ?? defaults.get(extension);
|
|
790
|
+
if (!contentType) {
|
|
791
|
+
throw new DocxImportError(
|
|
792
|
+
"invalid_package",
|
|
793
|
+
"Package part has no declared content type",
|
|
794
|
+
partName
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
contentTypeByPart.set(partName, contentType);
|
|
798
|
+
}
|
|
799
|
+
const documentType = overrides.get("word/document.xml");
|
|
800
|
+
if (documentType !== "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml") {
|
|
801
|
+
throw new DocxImportError(
|
|
802
|
+
"invalid_package",
|
|
803
|
+
"word/document.xml has the wrong content type",
|
|
804
|
+
"[Content_Types].xml"
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
return contentTypeByPart;
|
|
808
|
+
}
|
|
809
|
+
function isSafeOpaqueContentType(partName, contentType) {
|
|
810
|
+
if (/(?:macroEnabled|vbaProject|activeX|oleObject|encryptedPackage|digital-signature|customUI|webExtension)/i.test(
|
|
811
|
+
contentType
|
|
812
|
+
)) {
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
if (partName.endsWith(".rels")) {
|
|
816
|
+
return contentType === "application/vnd.openxmlformats-package.relationships+xml";
|
|
817
|
+
}
|
|
818
|
+
return /^(?:application|text)\/xml$/i.test(contentType) || /^(?:application|text)\/[A-Za-z0-9!#$&^_.+-]+\+xml$/i.test(contentType);
|
|
819
|
+
}
|
|
820
|
+
function parseRelationships(root, relationshipsPart, maxRelationships) {
|
|
821
|
+
requireLocalName(root, "Relationships", relationshipsPart);
|
|
822
|
+
requireDefaultNamespaceTree(root, RELATIONSHIPS_NAMESPACE, relationshipsPart);
|
|
823
|
+
const sourcePart = relationshipSourcePart(relationshipsPart);
|
|
824
|
+
const relationships = [];
|
|
825
|
+
const ids = /* @__PURE__ */ new Set();
|
|
826
|
+
for (const child of elementChildren(root)) {
|
|
827
|
+
if (relationships.length >= maxRelationships)
|
|
828
|
+
throw limitError("Relationships part exceeds maxRelationshipsPerPart", relationshipsPart);
|
|
829
|
+
if (child.name !== "Relationship")
|
|
830
|
+
throw new DocxImportError(
|
|
831
|
+
"invalid_package",
|
|
832
|
+
"Unknown relationship element",
|
|
833
|
+
relationshipsPart
|
|
834
|
+
);
|
|
835
|
+
requireLeafElement(child, relationshipsPart);
|
|
836
|
+
assertExactAttributes(
|
|
837
|
+
child,
|
|
838
|
+
/* @__PURE__ */ new Set(["Id", "Type", "Target", "TargetMode"]),
|
|
839
|
+
relationshipsPart
|
|
840
|
+
);
|
|
841
|
+
const id = requiredExactAttribute(child, "Id", relationshipsPart);
|
|
842
|
+
const type = requiredExactAttribute(child, "Type", relationshipsPart);
|
|
843
|
+
const target = requiredExactAttribute(child, "Target", relationshipsPart);
|
|
844
|
+
if (id.length > 512 || type.length > 2048 || target.length > 4096) {
|
|
845
|
+
throw limitError(
|
|
846
|
+
"Relationship Id, Type, or Target exceeds its safety cap",
|
|
847
|
+
relationshipsPart
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
if (ids.has(id))
|
|
851
|
+
throw new DocxImportError("invalid_package", "Duplicate relationship id", relationshipsPart);
|
|
852
|
+
ids.add(id);
|
|
853
|
+
if ((exactAttribute(child, "TargetMode") ?? "Internal") !== "Internal") {
|
|
854
|
+
throw new DocxImportError(
|
|
855
|
+
"unsupported_feature",
|
|
856
|
+
"External DOCX relationships are unsupported",
|
|
857
|
+
relationshipsPart
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
relationships.push({
|
|
861
|
+
id,
|
|
862
|
+
type,
|
|
863
|
+
target,
|
|
864
|
+
sourcePart,
|
|
865
|
+
partName: resolvePartTarget(sourcePart, target, relationshipsPart)
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
return relationships;
|
|
869
|
+
}
|
|
870
|
+
function validateRelationships(relationships, opaqueParts) {
|
|
871
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
872
|
+
`${WORD_RELATIONSHIP_BASE}officeDocument`,
|
|
873
|
+
`${WORD_RELATIONSHIP_BASE}styles`,
|
|
874
|
+
`${WORD_RELATIONSHIP_BASE}settings`,
|
|
875
|
+
`${WORD_RELATIONSHIP_BASE}webSettings`,
|
|
876
|
+
`${WORD_RELATIONSHIP_BASE}theme`,
|
|
877
|
+
`${WORD_RELATIONSHIP_BASE}fontTable`,
|
|
878
|
+
`${WORD_RELATIONSHIP_BASE}numbering`,
|
|
879
|
+
`${WORD_RELATIONSHIP_BASE}header`,
|
|
880
|
+
`${WORD_RELATIONSHIP_BASE}footer`,
|
|
881
|
+
`${WORD_RELATIONSHIP_BASE}comments`,
|
|
882
|
+
"http://schemas.microsoft.com/office/2011/relationships/commentsExtended",
|
|
883
|
+
`${WORD_RELATIONSHIP_BASE}footnotes`,
|
|
884
|
+
`${WORD_RELATIONSHIP_BASE}endnotes`,
|
|
885
|
+
`${PACKAGE_RELATIONSHIP_BASE}core-properties`,
|
|
886
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties",
|
|
887
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties"
|
|
888
|
+
]);
|
|
889
|
+
const opaqueRelationships = [];
|
|
890
|
+
for (const relationship of relationships) {
|
|
891
|
+
if (allowed.has(relationship.type)) {
|
|
892
|
+
validateRelationshipRoute(relationship);
|
|
893
|
+
if (opaqueParts.has(relationship.partName)) opaqueRelationships.push(relationship);
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
if (!isSafeOpaqueRelationship(relationship) || !opaqueParts.has(relationship.partName)) {
|
|
897
|
+
throw new DocxImportError(
|
|
898
|
+
"unsupported_feature",
|
|
899
|
+
`Unsupported DOCX relationship: ${relationship.type}`,
|
|
900
|
+
relationship.partName
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
opaqueRelationships.push(relationship);
|
|
904
|
+
}
|
|
905
|
+
const singletonTypes = new Set(
|
|
906
|
+
[
|
|
907
|
+
"styles",
|
|
908
|
+
"settings",
|
|
909
|
+
"webSettings",
|
|
910
|
+
"theme",
|
|
911
|
+
"fontTable",
|
|
912
|
+
"numbering",
|
|
913
|
+
"comments",
|
|
914
|
+
"footnotes",
|
|
915
|
+
"endnotes"
|
|
916
|
+
].map((suffix) => `${WORD_RELATIONSHIP_BASE}${suffix}`).concat(["http://schemas.microsoft.com/office/2011/relationships/commentsExtended"])
|
|
917
|
+
);
|
|
918
|
+
const seenSingletons = /* @__PURE__ */ new Set();
|
|
919
|
+
for (const relationship of relationships) {
|
|
920
|
+
if (relationship.sourcePart !== "word/document.xml" || !singletonTypes.has(relationship.type))
|
|
921
|
+
continue;
|
|
922
|
+
if (seenSingletons.has(relationship.type)) {
|
|
923
|
+
throw new DocxImportError(
|
|
924
|
+
"invalid_package",
|
|
925
|
+
"Document contains duplicate singleton relationships",
|
|
926
|
+
relationship.partName
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
seenSingletons.add(relationship.type);
|
|
930
|
+
}
|
|
931
|
+
return opaqueRelationships;
|
|
932
|
+
}
|
|
933
|
+
function isSafeOpaqueRelationship(relationship) {
|
|
934
|
+
const { sourcePart, type, partName } = relationship;
|
|
935
|
+
if (type === `${WORD_RELATIONSHIP_BASE}customXml`) {
|
|
936
|
+
return sourcePart === "word/document.xml" && /^customXml\/item\d+\.xml$/.test(partName);
|
|
937
|
+
}
|
|
938
|
+
if (type === `${WORD_RELATIONSHIP_BASE}customXmlProps`) {
|
|
939
|
+
return /^customXml\/item\d+\.xml$/.test(sourcePart) && /^customXml\/itemProps\d+\.xml$/.test(partName);
|
|
940
|
+
}
|
|
941
|
+
if (type === `${WORD_RELATIONSHIP_BASE}glossaryDocument`) {
|
|
942
|
+
return sourcePart === "word/document.xml" && partName === "word/glossary/document.xml";
|
|
943
|
+
}
|
|
944
|
+
return false;
|
|
945
|
+
}
|
|
946
|
+
function validateRelationshipRoute(relationship) {
|
|
947
|
+
const { sourcePart, type, partName } = relationship;
|
|
948
|
+
if (sourcePart === "") {
|
|
949
|
+
const expected = (/* @__PURE__ */ new Map([
|
|
950
|
+
[`${WORD_RELATIONSHIP_BASE}officeDocument`, "word/document.xml"],
|
|
951
|
+
[`${PACKAGE_RELATIONSHIP_BASE}core-properties`, "docProps/core.xml"],
|
|
952
|
+
[
|
|
953
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties",
|
|
954
|
+
"docProps/app.xml"
|
|
955
|
+
],
|
|
956
|
+
[
|
|
957
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties",
|
|
958
|
+
"docProps/custom.xml"
|
|
959
|
+
]
|
|
960
|
+
])).get(type);
|
|
961
|
+
if (!expected || partName !== expected) {
|
|
962
|
+
throw new DocxImportError(
|
|
963
|
+
"invalid_package",
|
|
964
|
+
"Package-root relationship has an invalid type or target",
|
|
965
|
+
partName
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (sourcePart !== "word/document.xml") {
|
|
971
|
+
throw new DocxImportError(
|
|
972
|
+
"unsupported_feature",
|
|
973
|
+
"Relationships from auxiliary Word parts are not yet supported",
|
|
974
|
+
sourcePart
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
const fixedTargets = /* @__PURE__ */ new Map([
|
|
978
|
+
[`${WORD_RELATIONSHIP_BASE}styles`, "word/styles.xml"],
|
|
979
|
+
[`${WORD_RELATIONSHIP_BASE}settings`, "word/settings.xml"],
|
|
980
|
+
[`${WORD_RELATIONSHIP_BASE}webSettings`, "word/webSettings.xml"],
|
|
981
|
+
[`${WORD_RELATIONSHIP_BASE}fontTable`, "word/fontTable.xml"],
|
|
982
|
+
[`${WORD_RELATIONSHIP_BASE}numbering`, "word/numbering.xml"],
|
|
983
|
+
[`${WORD_RELATIONSHIP_BASE}comments`, "word/comments.xml"],
|
|
984
|
+
[`${WORD_RELATIONSHIP_BASE}footnotes`, "word/footnotes.xml"],
|
|
985
|
+
[`${WORD_RELATIONSHIP_BASE}endnotes`, "word/endnotes.xml"],
|
|
986
|
+
[
|
|
987
|
+
"http://schemas.microsoft.com/office/2011/relationships/commentsExtended",
|
|
988
|
+
"word/commentsExtended.xml"
|
|
989
|
+
]
|
|
990
|
+
]);
|
|
991
|
+
const fixed = fixedTargets.get(type);
|
|
992
|
+
const valid = fixed !== void 0 ? partName === fixed : type === `${WORD_RELATIONSHIP_BASE}theme` ? /^word\/theme\/theme\d+\.xml$/.test(partName) : type === `${WORD_RELATIONSHIP_BASE}header` ? /^word\/header\d+\.xml$/.test(partName) : type === `${WORD_RELATIONSHIP_BASE}footer` && /^word\/footer\d+\.xml$/.test(partName);
|
|
993
|
+
if (!valid)
|
|
994
|
+
throw new DocxImportError(
|
|
995
|
+
"invalid_package",
|
|
996
|
+
"Document relationship has an invalid type or target",
|
|
997
|
+
partName
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
function validateRelationshipCoverage(parts, relationships) {
|
|
1001
|
+
const targets = new Set(relationships.map((relationship) => relationship.partName));
|
|
1002
|
+
for (const partName of parts.keys()) {
|
|
1003
|
+
if (partName === "[Content_Types].xml" || partName.endsWith(".rels")) continue;
|
|
1004
|
+
if (!targets.has(partName))
|
|
1005
|
+
throw new DocxImportError(
|
|
1006
|
+
"invalid_package",
|
|
1007
|
+
"Package contains an orphaned content part",
|
|
1008
|
+
partName
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
for (const partName of parts.keys()) {
|
|
1012
|
+
if (!partName.endsWith(".rels") || partName === "_rels/.rels") continue;
|
|
1013
|
+
const sourcePart = relationshipSourcePart(partName);
|
|
1014
|
+
if (!parts.has(sourcePart))
|
|
1015
|
+
throw new DocxImportError(
|
|
1016
|
+
"invalid_package",
|
|
1017
|
+
"Relationships part has no source part",
|
|
1018
|
+
partName
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
function validateEmptyNotePart(root, itemName, partName) {
|
|
1023
|
+
requireLocalName(root, `${itemName}s`, partName);
|
|
1024
|
+
requireWordNamespace(root, partName);
|
|
1025
|
+
for (const item of elementChildren(root)) {
|
|
1026
|
+
if (item.localName !== itemName)
|
|
1027
|
+
throw new DocxImportError("invalid_package", `Unknown ${itemName} element`, partName);
|
|
1028
|
+
const id = Number(requiredAttribute(item, "id", partName));
|
|
1029
|
+
const type = attribute(item, "type");
|
|
1030
|
+
if (!Number.isInteger(id) || type !== "separator" && type !== "continuationSeparator") {
|
|
1031
|
+
throw new DocxImportError(
|
|
1032
|
+
"unsupported_feature",
|
|
1033
|
+
`DOCX ${itemName}s are not yet supported`,
|
|
1034
|
+
partName
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
function validateSettings(root) {
|
|
1040
|
+
requireLocalName(root, "settings", "word/settings.xml");
|
|
1041
|
+
requireWordNamespace(root, "word/settings.xml");
|
|
1042
|
+
assertAllowedContentChildren(
|
|
1043
|
+
root,
|
|
1044
|
+
/* @__PURE__ */ new Set([
|
|
1045
|
+
"displayBackgroundShape",
|
|
1046
|
+
"evenAndOddHeaders",
|
|
1047
|
+
"trackRevisions",
|
|
1048
|
+
"compat",
|
|
1049
|
+
"documentProtection",
|
|
1050
|
+
"writeProtection"
|
|
1051
|
+
]),
|
|
1052
|
+
"document settings"
|
|
1053
|
+
);
|
|
1054
|
+
for (const protectedFeature of ["documentProtection", "writeProtection"]) {
|
|
1055
|
+
if (firstChild(root, protectedFeature)) {
|
|
1056
|
+
throw new DocxImportError(
|
|
1057
|
+
"unsupported_feature",
|
|
1058
|
+
`DOCX ${protectedFeature} is not preserved by import`,
|
|
1059
|
+
"word/settings.xml"
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
const background = singletonChild(root, "displayBackgroundShape", "document settings");
|
|
1064
|
+
const evenAndOdd = singletonChild(root, "evenAndOddHeaders", "document settings");
|
|
1065
|
+
const trackRevisions = singletonChild(root, "trackRevisions", "document settings");
|
|
1066
|
+
const compat = singletonChild(root, "compat", "document settings");
|
|
1067
|
+
assertAllowedAttributes(background, /* @__PURE__ */ new Set(["val"]), "display-background-shape setting");
|
|
1068
|
+
assertAllowedAttributes(evenAndOdd, /* @__PURE__ */ new Set(["val"]), "even-and-odd-headers setting");
|
|
1069
|
+
assertAllowedAttributes(trackRevisions, /* @__PURE__ */ new Set(["val"]), "track-revisions setting");
|
|
1070
|
+
assertAllowedAttributes(compat, /* @__PURE__ */ new Set(), "compatibility settings");
|
|
1071
|
+
assertAllowedContentChildren(background, /* @__PURE__ */ new Set(), "display-background-shape setting");
|
|
1072
|
+
assertAllowedContentChildren(evenAndOdd, /* @__PURE__ */ new Set(), "even-and-odd-headers setting");
|
|
1073
|
+
assertAllowedContentChildren(trackRevisions, /* @__PURE__ */ new Set(), "track-revisions setting");
|
|
1074
|
+
const tracksRevisions = booleanValue(trackRevisions, false);
|
|
1075
|
+
assertAllowedContentChildren(compat, /* @__PURE__ */ new Set(["compatSetting"]), "compatibility settings");
|
|
1076
|
+
const seenCompat = /* @__PURE__ */ new Set();
|
|
1077
|
+
for (const setting of children(compat, "compatSetting")) {
|
|
1078
|
+
assertAllowedAttributes(setting, /* @__PURE__ */ new Set(["name", "uri", "val"]), "compatibility setting");
|
|
1079
|
+
assertAllowedContentChildren(setting, /* @__PURE__ */ new Set(), "compatibility setting");
|
|
1080
|
+
const name = attribute(setting, "name");
|
|
1081
|
+
const uri = attribute(setting, "uri");
|
|
1082
|
+
const value = attribute(setting, "val");
|
|
1083
|
+
const key = `${uri ?? ""}\0${name ?? ""}`;
|
|
1084
|
+
if (seenCompat.has(key))
|
|
1085
|
+
throw new DocxImportError(
|
|
1086
|
+
"invalid_package",
|
|
1087
|
+
"Duplicate compatibility setting",
|
|
1088
|
+
"word/settings.xml"
|
|
1089
|
+
);
|
|
1090
|
+
seenCompat.add(key);
|
|
1091
|
+
if (name !== "compatibilityMode" || uri !== "http://schemas.microsoft.com/office/word" || value !== "15") {
|
|
1092
|
+
throw new DocxImportError(
|
|
1093
|
+
"unsupported_feature",
|
|
1094
|
+
"Document compatibility setting is not represented",
|
|
1095
|
+
"word/settings.xml"
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return { evenAndOddHeaders: booleanValue(evenAndOdd, false), trackRevisions: tracksRevisions };
|
|
1100
|
+
}
|
|
1101
|
+
function parseMainDocument(root, context, relationships) {
|
|
1102
|
+
requireLocalName(root, "document", "word/document.xml");
|
|
1103
|
+
rejectUnsupportedContent(root, "word/document.xml");
|
|
1104
|
+
requireWordNamespace(root, "word/document.xml");
|
|
1105
|
+
const body = onlyElement(root, "body", "word/document.xml");
|
|
1106
|
+
const blocks = [];
|
|
1107
|
+
const sections = [];
|
|
1108
|
+
let sectionStart = 0;
|
|
1109
|
+
const addSection = (section) => {
|
|
1110
|
+
if (sections.length >= context.budget.limits.maxSections) {
|
|
1111
|
+
throw limitError("DOCX exceeds maxSections", "word/document.xml");
|
|
1112
|
+
}
|
|
1113
|
+
sections.push(section);
|
|
1114
|
+
};
|
|
1115
|
+
for (const child of elementChildren(body)) {
|
|
1116
|
+
if (child.localName === "p") {
|
|
1117
|
+
const sectionProperties = firstChild(firstChild(child, "pPr"), "sectPr");
|
|
1118
|
+
if (!sectionProperties || !isPureSectionBoundaryParagraph(child)) {
|
|
1119
|
+
blocks.push(parseParagraph(child, context));
|
|
1120
|
+
}
|
|
1121
|
+
if (sectionProperties) {
|
|
1122
|
+
addSection(parseSection(sectionProperties, sectionStart, blocks.length, relationships));
|
|
1123
|
+
sectionStart = blocks.length;
|
|
1124
|
+
}
|
|
1125
|
+
} else if (child.localName === "tbl") blocks.push(parseTable(child, context));
|
|
1126
|
+
else if (child.localName === "sectPr") {
|
|
1127
|
+
addSection(parseSection(child, sectionStart, blocks.length, relationships));
|
|
1128
|
+
sectionStart = blocks.length;
|
|
1129
|
+
} else if (!NON_CONTENT_MARKERS.has(child.localName)) {
|
|
1130
|
+
throw unsupportedElement(child, "word/document.xml");
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
if (sections.length === 0 || sectionStart < blocks.length) {
|
|
1134
|
+
addSection(defaultSection(sectionStart, blocks.length));
|
|
1135
|
+
}
|
|
1136
|
+
return { blocks, sections };
|
|
1137
|
+
}
|
|
1138
|
+
function parseBlockContainer(root, context) {
|
|
1139
|
+
rejectUnsupportedContent(root, context.idPrefix);
|
|
1140
|
+
const blocks = [];
|
|
1141
|
+
for (const child of elementChildren(root)) {
|
|
1142
|
+
if (child.localName === "p") blocks.push(parseParagraph(child, context));
|
|
1143
|
+
else if (child.localName === "tbl") blocks.push(parseTable(child, context));
|
|
1144
|
+
else if (!NON_CONTENT_MARKERS.has(child.localName))
|
|
1145
|
+
throw unsupportedElement(child, context.idPrefix);
|
|
1146
|
+
}
|
|
1147
|
+
return blocks;
|
|
1148
|
+
}
|
|
1149
|
+
function parseParagraph(element, context) {
|
|
1150
|
+
consumeBlock(context.budget);
|
|
1151
|
+
const id = `${context.idPrefix}:p${context.budget.nextBlockId++}`;
|
|
1152
|
+
const properties = singletonChild(element, "pPr", "paragraph");
|
|
1153
|
+
const styleId = valueOf(firstChild(properties, "pStyle"));
|
|
1154
|
+
const paragraph = {
|
|
1155
|
+
kind: "paragraph",
|
|
1156
|
+
id,
|
|
1157
|
+
...styleId !== void 0 ? { styleId } : {},
|
|
1158
|
+
style: parseParagraphProperties(properties),
|
|
1159
|
+
inlines: [],
|
|
1160
|
+
commentAnchors: []
|
|
1161
|
+
};
|
|
1162
|
+
const firstTrackedChange = context.trackedChanges.length;
|
|
1163
|
+
let textOffset = 0;
|
|
1164
|
+
const addRun = (run) => {
|
|
1165
|
+
paragraph.inlines.push(run);
|
|
1166
|
+
if (run.kind === "run") {
|
|
1167
|
+
textOffset += run.text.length;
|
|
1168
|
+
consumeText(context.budget, run.text.length);
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
const parseChildren = (parent, change) => {
|
|
1172
|
+
for (const child of elementChildren(parent)) {
|
|
1173
|
+
if (child.localName === "pPr") continue;
|
|
1174
|
+
if (child.localName === "r") {
|
|
1175
|
+
const paragraphOffset = textOffset;
|
|
1176
|
+
const parsed = parseRun(child, change, (commentId, runOffset) => {
|
|
1177
|
+
paragraph.commentAnchors.push({
|
|
1178
|
+
commentId,
|
|
1179
|
+
kind: "reference",
|
|
1180
|
+
textOffset: paragraphOffset + runOffset
|
|
1181
|
+
});
|
|
1182
|
+
});
|
|
1183
|
+
for (const inline of parsed) addRun(inline);
|
|
1184
|
+
} else if (child.localName === "ins" || child.localName === "del") {
|
|
1185
|
+
if (change)
|
|
1186
|
+
throw new DocxImportError(
|
|
1187
|
+
"invalid_package",
|
|
1188
|
+
"Tracked changes cannot be nested",
|
|
1189
|
+
context.idPrefix
|
|
1190
|
+
);
|
|
1191
|
+
const changeStart = paragraph.inlines.length;
|
|
1192
|
+
const changeTextStart = textOffset;
|
|
1193
|
+
const author = attribute(child, "author");
|
|
1194
|
+
const createdAt = attribute(child, "date");
|
|
1195
|
+
const metadata = {
|
|
1196
|
+
id: requiredAttribute(child, "id", context.idPrefix),
|
|
1197
|
+
kind: child.localName === "ins" ? "insert" : "delete",
|
|
1198
|
+
...author !== void 0 ? { author } : {},
|
|
1199
|
+
...createdAt !== void 0 ? { createdAt } : {}
|
|
1200
|
+
};
|
|
1201
|
+
parseChildren(child, metadata);
|
|
1202
|
+
context.trackedChanges.push({
|
|
1203
|
+
id: metadata.id,
|
|
1204
|
+
kind: metadata.kind,
|
|
1205
|
+
...metadata.author !== void 0 ? { author: metadata.author } : {},
|
|
1206
|
+
...metadata.createdAt !== void 0 ? { createdAt: metadata.createdAt } : {},
|
|
1207
|
+
blockId: id,
|
|
1208
|
+
startInlineIndex: changeStart,
|
|
1209
|
+
endInlineIndex: paragraph.inlines.length,
|
|
1210
|
+
startTextOffset: changeTextStart,
|
|
1211
|
+
endTextOffset: textOffset
|
|
1212
|
+
});
|
|
1213
|
+
} else if (child.localName === "commentRangeStart" || child.localName === "commentRangeEnd") {
|
|
1214
|
+
paragraph.commentAnchors.push({
|
|
1215
|
+
commentId: requiredAttribute(child, "id", context.idPrefix),
|
|
1216
|
+
kind: child.localName === "commentRangeStart" ? "start" : "end",
|
|
1217
|
+
textOffset
|
|
1218
|
+
});
|
|
1219
|
+
} else if (child.localName === "commentReference") {
|
|
1220
|
+
paragraph.commentAnchors.push({
|
|
1221
|
+
commentId: requiredAttribute(child, "id", context.idPrefix),
|
|
1222
|
+
kind: "reference",
|
|
1223
|
+
textOffset
|
|
1224
|
+
});
|
|
1225
|
+
} else if (!NON_CONTENT_MARKERS.has(child.localName))
|
|
1226
|
+
throw unsupportedElement(child, context.idPrefix);
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
parseChildren(element);
|
|
1230
|
+
validateScalarBoundaries(paragraph, context.trackedChanges, firstTrackedChange, context.idPrefix);
|
|
1231
|
+
return stripUndefined(paragraph);
|
|
1232
|
+
}
|
|
1233
|
+
function isPureSectionBoundaryParagraph(element) {
|
|
1234
|
+
const paragraphChildren = elementChildren(element);
|
|
1235
|
+
if (paragraphChildren.length !== 1 || paragraphChildren[0]?.localName !== "pPr") return false;
|
|
1236
|
+
const propertyChildren = elementChildren(paragraphChildren[0]);
|
|
1237
|
+
return propertyChildren.length === 1 && propertyChildren[0]?.localName === "sectPr";
|
|
1238
|
+
}
|
|
1239
|
+
function validateScalarBoundaries(paragraph, trackedChanges, firstTrackedChange, partName) {
|
|
1240
|
+
const chunks = [];
|
|
1241
|
+
for (const inline of paragraph.inlines) if (inline.kind === "run") chunks.push(inline.text);
|
|
1242
|
+
const text = chunks.join("");
|
|
1243
|
+
const validate = (offset, label) => {
|
|
1244
|
+
if (!Number.isInteger(offset) || offset < 0 || offset > text.length) {
|
|
1245
|
+
throw new DocxImportError("invalid_package", `${label} is outside its paragraph`, partName);
|
|
1246
|
+
}
|
|
1247
|
+
const before = offset > 0 ? text.charCodeAt(offset - 1) : 0;
|
|
1248
|
+
const after = offset < text.length ? text.charCodeAt(offset) : 0;
|
|
1249
|
+
if (before >= 55296 && before <= 56319 && after >= 56320 && after <= 57343) {
|
|
1250
|
+
throw new DocxImportError(
|
|
1251
|
+
"invalid_package",
|
|
1252
|
+
`${label} splits a Unicode scalar value`,
|
|
1253
|
+
partName
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
};
|
|
1257
|
+
for (const anchor of paragraph.commentAnchors) validate(anchor.textOffset, "Comment anchor");
|
|
1258
|
+
for (let index = firstTrackedChange; index < trackedChanges.length; index += 1) {
|
|
1259
|
+
const change = trackedChanges[index];
|
|
1260
|
+
if (!change || change.blockId !== paragraph.id) continue;
|
|
1261
|
+
validate(change.startTextOffset, "Tracked-change start");
|
|
1262
|
+
validate(change.endTextOffset, "Tracked-change end");
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
function parseRun(element, change, onCommentReference) {
|
|
1266
|
+
const properties = singletonChild(element, "rPr", "run");
|
|
1267
|
+
const styleId = valueOf(firstChild(properties, "rStyle"));
|
|
1268
|
+
const style = parseRunProperties(properties);
|
|
1269
|
+
const styled = styleId !== void 0 || Object.keys(style).length > 0;
|
|
1270
|
+
const inlines = [];
|
|
1271
|
+
let text = "";
|
|
1272
|
+
let emittedTextLength = 0;
|
|
1273
|
+
let hasCommentReference = false;
|
|
1274
|
+
const flush = () => {
|
|
1275
|
+
if (text.length === 0) return;
|
|
1276
|
+
inlines.push(
|
|
1277
|
+
stripUndefined({
|
|
1278
|
+
kind: "run",
|
|
1279
|
+
text,
|
|
1280
|
+
styleId,
|
|
1281
|
+
style,
|
|
1282
|
+
changeId: change?.id
|
|
1283
|
+
})
|
|
1284
|
+
);
|
|
1285
|
+
emittedTextLength += text.length;
|
|
1286
|
+
text = "";
|
|
1287
|
+
};
|
|
1288
|
+
for (const child of elementChildren(element)) {
|
|
1289
|
+
if (child.localName === "rPr") continue;
|
|
1290
|
+
if (child.localName === "t" || child.localName === "delText") text += textContent(child);
|
|
1291
|
+
else if (child.localName === "tab") text += " ";
|
|
1292
|
+
else if (child.localName === "cr") text += "\n";
|
|
1293
|
+
else if (child.localName === "noBreakHyphen") text += "\u2011";
|
|
1294
|
+
else if (child.localName === "softHyphen") text += "\xAD";
|
|
1295
|
+
else if (child.localName === "br") {
|
|
1296
|
+
assertAllowedAttributes(child, /* @__PURE__ */ new Set(["type"]), "run break");
|
|
1297
|
+
assertAllowedContentChildren(child, /* @__PURE__ */ new Set(), "run break");
|
|
1298
|
+
const type = attribute(child, "type") ?? "textWrapping";
|
|
1299
|
+
if (type === "page") {
|
|
1300
|
+
flush();
|
|
1301
|
+
inlines.push({ kind: "pageBreak" });
|
|
1302
|
+
} else if (type === "textWrapping") text += "\n";
|
|
1303
|
+
else throw new DocxImportError("unsupported_feature", `Unsupported break type: ${type}`);
|
|
1304
|
+
} else if (child.localName === "commentReference") {
|
|
1305
|
+
hasCommentReference = true;
|
|
1306
|
+
const commentId = requiredAttribute(child, "id", "word/document.xml");
|
|
1307
|
+
onCommentReference?.(commentId, emittedTextLength + text.length);
|
|
1308
|
+
} else if (child.localName !== "lastRenderedPageBreak" && !NON_CONTENT_MARKERS.has(child.localName)) {
|
|
1309
|
+
throw unsupportedElement(child, "word/document.xml");
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
flush();
|
|
1313
|
+
if (emittedTextLength === 0 && styled && (hasCommentReference || inlines.length > 0)) {
|
|
1314
|
+
throw new DocxImportError(
|
|
1315
|
+
"unsupported_feature",
|
|
1316
|
+
"Formatting on a non-text run cannot be represented without changing marker behavior",
|
|
1317
|
+
"word/document.xml"
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
if (emittedTextLength === 0 && inlines.length === 0 && !hasCommentReference) {
|
|
1321
|
+
inlines.push(
|
|
1322
|
+
stripUndefined({
|
|
1323
|
+
kind: "run",
|
|
1324
|
+
text: "",
|
|
1325
|
+
styleId,
|
|
1326
|
+
style,
|
|
1327
|
+
changeId: change?.id
|
|
1328
|
+
})
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
return inlines;
|
|
1332
|
+
}
|
|
1333
|
+
function parseTable(element, context) {
|
|
1334
|
+
consumeBlock(context.budget);
|
|
1335
|
+
assertAllowedContentChildren(element, /* @__PURE__ */ new Set(["tblPr", "tblGrid", "tr"]), "table");
|
|
1336
|
+
const id = `${context.idPrefix}:tbl${context.budget.nextBlockId++}`;
|
|
1337
|
+
const properties = singletonChild(element, "tblPr", "table");
|
|
1338
|
+
assertAllowedPropertyChildren(
|
|
1339
|
+
properties,
|
|
1340
|
+
/* @__PURE__ */ new Set(["tblStyle", "tblW", "tblInd", "jc", "tblLayout", "tblCellMar", "tblBorders"]),
|
|
1341
|
+
"table properties"
|
|
1342
|
+
);
|
|
1343
|
+
assertUniqueChildNames(properties, "table properties");
|
|
1344
|
+
assertAllowedAttributes(firstChild(properties, "tblStyle"), /* @__PURE__ */ new Set(["val"]), "table style");
|
|
1345
|
+
assertAllowedAttributes(firstChild(properties, "jc"), /* @__PURE__ */ new Set(["val"]), "table alignment");
|
|
1346
|
+
const grid = singletonChild(element, "tblGrid", "table");
|
|
1347
|
+
assertAllowedContentChildren(grid, /* @__PURE__ */ new Set(["gridCol"]), "table grid");
|
|
1348
|
+
assertAllowedAttributes(grid, /* @__PURE__ */ new Set(), "table grid");
|
|
1349
|
+
for (const column of children(grid, "gridCol")) {
|
|
1350
|
+
assertAllowedAttributes(column, /* @__PURE__ */ new Set(["w"]), "table grid column");
|
|
1351
|
+
assertAllowedContentChildren(column, /* @__PURE__ */ new Set(), "table grid column");
|
|
1352
|
+
}
|
|
1353
|
+
const rows = [];
|
|
1354
|
+
for (const rowElement of children(element, "tr")) {
|
|
1355
|
+
assertAllowedContentChildren(rowElement, /* @__PURE__ */ new Set(["trPr", "tc"]), "table row");
|
|
1356
|
+
const rowProperties = singletonChild(rowElement, "trPr", "table row");
|
|
1357
|
+
assertAllowedPropertyChildren(
|
|
1358
|
+
rowProperties,
|
|
1359
|
+
/* @__PURE__ */ new Set(["tblHeader", "cantSplit", "trHeight"]),
|
|
1360
|
+
"table row properties"
|
|
1361
|
+
);
|
|
1362
|
+
assertUniqueChildNames(rowProperties, "table row properties");
|
|
1363
|
+
assertAllowedAttributes(rowProperties, /* @__PURE__ */ new Set(), "table row properties");
|
|
1364
|
+
assertAllowedAttributes(
|
|
1365
|
+
firstChild(rowProperties, "tblHeader"),
|
|
1366
|
+
/* @__PURE__ */ new Set(["val"]),
|
|
1367
|
+
"table header-row setting"
|
|
1368
|
+
);
|
|
1369
|
+
assertAllowedAttributes(
|
|
1370
|
+
firstChild(rowProperties, "cantSplit"),
|
|
1371
|
+
/* @__PURE__ */ new Set(["val"]),
|
|
1372
|
+
"table row-splitting setting"
|
|
1373
|
+
);
|
|
1374
|
+
assertAllowedAttributes(
|
|
1375
|
+
firstChild(rowProperties, "trHeight"),
|
|
1376
|
+
/* @__PURE__ */ new Set(["val", "hRule"]),
|
|
1377
|
+
"table row height"
|
|
1378
|
+
);
|
|
1379
|
+
const cells = [];
|
|
1380
|
+
for (const cellElement of children(rowElement, "tc")) {
|
|
1381
|
+
assertAllowedContentChildren(cellElement, /* @__PURE__ */ new Set(["tcPr", "p", "tbl"]), "table cell");
|
|
1382
|
+
const cellProperties = singletonChild(cellElement, "tcPr", "table cell");
|
|
1383
|
+
assertAllowedPropertyChildren(
|
|
1384
|
+
cellProperties,
|
|
1385
|
+
/* @__PURE__ */ new Set(["tcW", "tcMar", "gridSpan", "vMerge", "vAlign", "shd"]),
|
|
1386
|
+
"table cell properties"
|
|
1387
|
+
);
|
|
1388
|
+
assertUniqueChildNames(cellProperties, "table cell properties");
|
|
1389
|
+
assertAllowedAttributes(cellProperties, /* @__PURE__ */ new Set(), "table cell properties");
|
|
1390
|
+
for (const name of ["gridSpan", "vMerge", "vAlign"]) {
|
|
1391
|
+
assertAllowedAttributes(
|
|
1392
|
+
firstChild(cellProperties, name),
|
|
1393
|
+
/* @__PURE__ */ new Set(["val"]),
|
|
1394
|
+
`table cell ${name}`
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
const verticalMergeElement = firstChild(cellProperties, "vMerge");
|
|
1398
|
+
const verticalMergeValue = verticalMergeElement ? valueOf(verticalMergeElement) ?? "continue" : void 0;
|
|
1399
|
+
if (verticalMergeValue !== void 0 && verticalMergeValue !== "restart" && verticalMergeValue !== "continue") {
|
|
1400
|
+
throw new DocxImportError(
|
|
1401
|
+
"invalid_package",
|
|
1402
|
+
`Invalid vertical merge value: ${verticalMergeValue}`,
|
|
1403
|
+
context.idPrefix
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
const columnSpan = integerValue(firstChild(cellProperties, "gridSpan"), 1);
|
|
1407
|
+
if (columnSpan < 1)
|
|
1408
|
+
throw new DocxImportError(
|
|
1409
|
+
"invalid_package",
|
|
1410
|
+
"Table gridSpan must be positive",
|
|
1411
|
+
context.idPrefix
|
|
1412
|
+
);
|
|
1413
|
+
const blocks = [];
|
|
1414
|
+
for (const block of elementChildren(cellElement)) {
|
|
1415
|
+
if (block.localName === "tcPr") continue;
|
|
1416
|
+
if (block.localName === "p") blocks.push(parseParagraph(block, context));
|
|
1417
|
+
else if (block.localName === "tbl") blocks.push(parseTable(block, context));
|
|
1418
|
+
else if (!NON_CONTENT_MARKERS.has(block.localName))
|
|
1419
|
+
throw unsupportedElement(block, context.idPrefix);
|
|
1420
|
+
}
|
|
1421
|
+
const shading = firstChild(cellProperties, "shd");
|
|
1422
|
+
validateShading(shading, context.idPrefix);
|
|
1423
|
+
cells.push(
|
|
1424
|
+
stripUndefined({
|
|
1425
|
+
width: parseMeasure(firstChild(cellProperties, "tcW")),
|
|
1426
|
+
margins: parseBoxMeasures(firstChild(cellProperties, "tcMar"), "table cell margins"),
|
|
1427
|
+
columnSpan,
|
|
1428
|
+
verticalMerge: verticalMergeValue === void 0 ? void 0 : verticalMergeValue === "restart" ? "restart" : "continue",
|
|
1429
|
+
verticalAlignment: valueOf(firstChild(cellProperties, "vAlign")),
|
|
1430
|
+
fill: attribute(shading, "fill"),
|
|
1431
|
+
blocks
|
|
1432
|
+
})
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
const height = firstChild(rowProperties, "trHeight");
|
|
1436
|
+
const heightRule = attribute(height, "hRule");
|
|
1437
|
+
if (heightRule !== void 0 && heightRule !== "auto" && heightRule !== "atLeast" && heightRule !== "exact") {
|
|
1438
|
+
throw new DocxImportError(
|
|
1439
|
+
"invalid_package",
|
|
1440
|
+
`Invalid table row height rule: ${heightRule}`,
|
|
1441
|
+
context.idPrefix
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
rows.push(
|
|
1445
|
+
stripUndefined({
|
|
1446
|
+
header: booleanValue(firstChild(rowProperties, "tblHeader"), false),
|
|
1447
|
+
cannotSplit: booleanValue(firstChild(rowProperties, "cantSplit"), false),
|
|
1448
|
+
heightPt: numberAttribute(height, "val", twipsToPt),
|
|
1449
|
+
heightRule,
|
|
1450
|
+
cells
|
|
1451
|
+
})
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
return stripUndefined({
|
|
1455
|
+
kind: "table",
|
|
1456
|
+
id,
|
|
1457
|
+
styleId: valueOf(firstChild(properties, "tblStyle")),
|
|
1458
|
+
width: parseMeasure(firstChild(properties, "tblW")),
|
|
1459
|
+
indent: parseMeasure(firstChild(properties, "tblInd")),
|
|
1460
|
+
alignment: valueOf(firstChild(properties, "jc")),
|
|
1461
|
+
layout: parseTableLayout(firstChild(properties, "tblLayout")),
|
|
1462
|
+
cellMargins: parseBoxMeasures(firstChild(properties, "tblCellMar"), "table cell margins"),
|
|
1463
|
+
borders: parseTableBorders(firstChild(properties, "tblBorders"), context.idPrefix),
|
|
1464
|
+
gridColumnWidthsPt: children(grid, "gridCol").map(
|
|
1465
|
+
(column) => numberAttribute(column, "w", twipsToPt) ?? 0
|
|
1466
|
+
),
|
|
1467
|
+
rows
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
function parseBoxMeasures(element, label) {
|
|
1471
|
+
if (!element) return void 0;
|
|
1472
|
+
assertAllowedAttributes(element, /* @__PURE__ */ new Set(), label);
|
|
1473
|
+
const allowed = /* @__PURE__ */ new Set(["top", "right", "bottom", "left", "start", "end"]);
|
|
1474
|
+
assertAllowedPropertyChildren(element, allowed, label);
|
|
1475
|
+
assertUniqueChildNames(element, label);
|
|
1476
|
+
if (firstChild(element, "left") && firstChild(element, "start") || firstChild(element, "right") && firstChild(element, "end")) {
|
|
1477
|
+
throw new DocxImportError(
|
|
1478
|
+
"unsupported_feature",
|
|
1479
|
+
`${label} mixes physical and logical side aliases`,
|
|
1480
|
+
label
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
const result = stripUndefined({
|
|
1484
|
+
top: parseMeasure(firstChild(element, "top")),
|
|
1485
|
+
right: parseMeasure(firstChild(element, "right")),
|
|
1486
|
+
bottom: parseMeasure(firstChild(element, "bottom")),
|
|
1487
|
+
left: parseMeasure(firstChild(element, "left")),
|
|
1488
|
+
start: parseMeasure(firstChild(element, "start")),
|
|
1489
|
+
end: parseMeasure(firstChild(element, "end"))
|
|
1490
|
+
});
|
|
1491
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
1492
|
+
}
|
|
1493
|
+
function parseTableBorders(element, partName) {
|
|
1494
|
+
if (!element) return void 0;
|
|
1495
|
+
assertAllowedAttributes(element, /* @__PURE__ */ new Set(), "table borders");
|
|
1496
|
+
assertAllowedPropertyChildren(
|
|
1497
|
+
element,
|
|
1498
|
+
/* @__PURE__ */ new Set(["top", "right", "bottom", "left", "insideH", "insideV"]),
|
|
1499
|
+
"table borders"
|
|
1500
|
+
);
|
|
1501
|
+
assertUniqueChildNames(element, "table borders");
|
|
1502
|
+
const parse = (name) => {
|
|
1503
|
+
const border = firstChild(element, name);
|
|
1504
|
+
if (!border) return void 0;
|
|
1505
|
+
assertAllowedAttributes(
|
|
1506
|
+
border,
|
|
1507
|
+
/* @__PURE__ */ new Set(["val", "color", "sz", "space", "shadow", "frame"]),
|
|
1508
|
+
"table border"
|
|
1509
|
+
);
|
|
1510
|
+
const style = valueOf(border);
|
|
1511
|
+
if (!style)
|
|
1512
|
+
throw new DocxImportError("invalid_package", "Table border lacks a style", partName);
|
|
1513
|
+
return stripUndefined({
|
|
1514
|
+
style,
|
|
1515
|
+
color: attribute(border, "color"),
|
|
1516
|
+
sizePt: numberAttribute(border, "sz", (value) => value / 8),
|
|
1517
|
+
spacePt: numberAttribute(border, "space"),
|
|
1518
|
+
shadow: optionalBooleanAttribute(border, "shadow"),
|
|
1519
|
+
frame: optionalBooleanAttribute(border, "frame")
|
|
1520
|
+
});
|
|
1521
|
+
};
|
|
1522
|
+
const result = stripUndefined({
|
|
1523
|
+
top: parse("top"),
|
|
1524
|
+
right: parse("right"),
|
|
1525
|
+
bottom: parse("bottom"),
|
|
1526
|
+
left: parse("left"),
|
|
1527
|
+
insideHorizontal: parse("insideH"),
|
|
1528
|
+
insideVertical: parse("insideV")
|
|
1529
|
+
});
|
|
1530
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
1531
|
+
}
|
|
1532
|
+
function validateShading(element, partName) {
|
|
1533
|
+
if (!element) return;
|
|
1534
|
+
assertAllowedAttributes(element, /* @__PURE__ */ new Set(["val", "color", "fill"]), "table cell shading");
|
|
1535
|
+
const pattern = valueOf(element);
|
|
1536
|
+
const foreground = attribute(element, "color");
|
|
1537
|
+
if (pattern !== void 0 && pattern !== "clear" && pattern !== "solid") {
|
|
1538
|
+
throw new DocxImportError(
|
|
1539
|
+
"unsupported_feature",
|
|
1540
|
+
`Table shading pattern ${pattern} is not represented`,
|
|
1541
|
+
partName
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
if (foreground !== void 0 && foreground.toLowerCase() !== "auto") {
|
|
1545
|
+
throw new DocxImportError(
|
|
1546
|
+
"unsupported_feature",
|
|
1547
|
+
"Patterned table foreground colors are not represented",
|
|
1548
|
+
partName
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
function parseTableLayout(element) {
|
|
1553
|
+
if (!element) return void 0;
|
|
1554
|
+
assertAllowedAttributes(element, /* @__PURE__ */ new Set(["type"]), "table layout");
|
|
1555
|
+
const value = attribute(element, "type");
|
|
1556
|
+
if (value === "fixed") return "fixed";
|
|
1557
|
+
if (value === "autofit") return "autofit";
|
|
1558
|
+
throw new DocxImportError(
|
|
1559
|
+
"invalid_package",
|
|
1560
|
+
`Invalid table layout type: ${String(value)}`,
|
|
1561
|
+
"table layout"
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
function parseMeasure(element) {
|
|
1565
|
+
if (!element) return void 0;
|
|
1566
|
+
assertAllowedAttributes(element, /* @__PURE__ */ new Set(["w", "type"]), "OOXML measure");
|
|
1567
|
+
const width = attribute(element, "w");
|
|
1568
|
+
if (width === void 0)
|
|
1569
|
+
throw new DocxImportError("invalid_package", "OOXML measure lacks a width");
|
|
1570
|
+
const raw = Number(width);
|
|
1571
|
+
if (!Number.isFinite(raw))
|
|
1572
|
+
throw new DocxImportError("invalid_package", "OOXML measure width is invalid");
|
|
1573
|
+
const type = attribute(element, "type") ?? "dxa";
|
|
1574
|
+
if (type === "dxa") return { value: twipsToPt(raw), unit: "pt" };
|
|
1575
|
+
if (type === "pct") return { value: raw / 50, unit: "percent" };
|
|
1576
|
+
if (type === "auto" || type === "nil") return { value: raw, unit: "auto" };
|
|
1577
|
+
throw new DocxImportError("unsupported_feature", `Unsupported OOXML measure type: ${type}`);
|
|
1578
|
+
}
|
|
1579
|
+
function parseStyles(root, limits) {
|
|
1580
|
+
requireLocalName(root, "styles", "word/styles.xml");
|
|
1581
|
+
requireWordNamespace(root, "word/styles.xml");
|
|
1582
|
+
rejectUnsupportedContent(root, "word/styles.xml");
|
|
1583
|
+
assertAllowedContentChildren(
|
|
1584
|
+
root,
|
|
1585
|
+
/* @__PURE__ */ new Set(["docDefaults", "latentStyles", "style"]),
|
|
1586
|
+
"styles root"
|
|
1587
|
+
);
|
|
1588
|
+
const defaults = children(root, "docDefaults");
|
|
1589
|
+
if (defaults.length > 1)
|
|
1590
|
+
throw new DocxImportError("invalid_package", "Styles repeats docDefaults", "word/styles.xml");
|
|
1591
|
+
if (defaults[0]) validateEmptyStyleDefaults(defaults[0]);
|
|
1592
|
+
const styles = [];
|
|
1593
|
+
const styleIds = /* @__PURE__ */ new Set();
|
|
1594
|
+
for (const element of children(root, "style")) {
|
|
1595
|
+
if (styles.length >= limits.maxStyles)
|
|
1596
|
+
throw limitError("DOCX exceeds maxStyles", "word/styles.xml");
|
|
1597
|
+
const styleId = requiredAttribute(element, "styleId", "word/styles.xml");
|
|
1598
|
+
if (styleIds.has(styleId))
|
|
1599
|
+
throw new DocxImportError("invalid_package", "Duplicate style id", "word/styles.xml");
|
|
1600
|
+
styleIds.add(styleId);
|
|
1601
|
+
const kind = attribute(element, "type") ?? "paragraph";
|
|
1602
|
+
if (kind !== "paragraph" && kind !== "character") {
|
|
1603
|
+
throw new DocxImportError(
|
|
1604
|
+
"unsupported_feature",
|
|
1605
|
+
`Style type ${kind} is not represented`,
|
|
1606
|
+
"word/styles.xml"
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
assertAllowedAttributes(
|
|
1610
|
+
element,
|
|
1611
|
+
/* @__PURE__ */ new Set(["styleId", "type", "default", "customStyle"]),
|
|
1612
|
+
"style"
|
|
1613
|
+
);
|
|
1614
|
+
assertAllowedPropertyChildren(
|
|
1615
|
+
element,
|
|
1616
|
+
/* @__PURE__ */ new Set([
|
|
1617
|
+
"name",
|
|
1618
|
+
"aliases",
|
|
1619
|
+
"basedOn",
|
|
1620
|
+
"next",
|
|
1621
|
+
"link",
|
|
1622
|
+
"uiPriority",
|
|
1623
|
+
"semiHidden",
|
|
1624
|
+
"unhideWhenUsed",
|
|
1625
|
+
"qFormat",
|
|
1626
|
+
"locked",
|
|
1627
|
+
"personal",
|
|
1628
|
+
"personalCompose",
|
|
1629
|
+
"personalReply",
|
|
1630
|
+
"rsid",
|
|
1631
|
+
"pPr",
|
|
1632
|
+
"rPr"
|
|
1633
|
+
]),
|
|
1634
|
+
"style definition"
|
|
1635
|
+
);
|
|
1636
|
+
styles.push(
|
|
1637
|
+
stripUndefined({
|
|
1638
|
+
styleId,
|
|
1639
|
+
kind,
|
|
1640
|
+
name: valueOf(firstChild(element, "name")),
|
|
1641
|
+
basedOn: valueOf(firstChild(element, "basedOn")),
|
|
1642
|
+
next: valueOf(firstChild(element, "next")),
|
|
1643
|
+
isDefault: booleanAttribute(element, "default", false),
|
|
1644
|
+
paragraph: parseParagraphProperties(singletonChild(element, "pPr", "style definition")),
|
|
1645
|
+
run: parseRunProperties(singletonChild(element, "rPr", "style definition"))
|
|
1646
|
+
})
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
const styleById = new Map(styles.map((style) => [style.styleId, style]));
|
|
1650
|
+
for (const style of styles) {
|
|
1651
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1652
|
+
let current = style;
|
|
1653
|
+
let depth = 0;
|
|
1654
|
+
while (current?.basedOn !== void 0) {
|
|
1655
|
+
if (!seen.add(current.styleId)) {
|
|
1656
|
+
throw new DocxImportError(
|
|
1657
|
+
"invalid_package",
|
|
1658
|
+
"Style inheritance contains a cycle",
|
|
1659
|
+
"word/styles.xml"
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
depth += 1;
|
|
1663
|
+
if (depth > limits.maxStyleInheritanceDepth) {
|
|
1664
|
+
throw limitError("DOCX exceeds maxStyleInheritanceDepth", "word/styles.xml");
|
|
1665
|
+
}
|
|
1666
|
+
current = styleById.get(current.basedOn);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
return styles;
|
|
1670
|
+
}
|
|
1671
|
+
function validateEmptyStyleDefaults(defaults) {
|
|
1672
|
+
assertAllowedPropertyChildren(defaults, /* @__PURE__ */ new Set(["pPrDefault", "rPrDefault"]), "style defaults");
|
|
1673
|
+
for (const [wrapperName, propertyName, parse] of [
|
|
1674
|
+
["pPrDefault", "pPr", parseParagraphProperties],
|
|
1675
|
+
["rPrDefault", "rPr", parseRunProperties]
|
|
1676
|
+
]) {
|
|
1677
|
+
const wrappers = children(defaults, wrapperName);
|
|
1678
|
+
if (wrappers.length > 1)
|
|
1679
|
+
throw new DocxImportError(
|
|
1680
|
+
"invalid_package",
|
|
1681
|
+
`Styles repeats ${wrapperName}`,
|
|
1682
|
+
"word/styles.xml"
|
|
1683
|
+
);
|
|
1684
|
+
const wrapper = wrappers[0];
|
|
1685
|
+
assertAllowedPropertyChildren(wrapper, /* @__PURE__ */ new Set([propertyName]), "style defaults");
|
|
1686
|
+
const properties = singletonChild(wrapper, propertyName, "style defaults");
|
|
1687
|
+
if (properties && Object.keys(parse(properties)).length > 0) {
|
|
1688
|
+
throw new DocxImportError(
|
|
1689
|
+
"unsupported_feature",
|
|
1690
|
+
"Non-empty document style defaults are not yet represented",
|
|
1691
|
+
"word/styles.xml"
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
function parseNumbering(root, limits) {
|
|
1697
|
+
requireLocalName(root, "numbering", "word/numbering.xml");
|
|
1698
|
+
requireWordNamespace(root, "word/numbering.xml");
|
|
1699
|
+
rejectUnsupportedContent(root, "word/numbering.xml");
|
|
1700
|
+
assertAllowedContentChildren(
|
|
1701
|
+
root,
|
|
1702
|
+
/* @__PURE__ */ new Set(["numPicBullet", "abstractNum", "num"]),
|
|
1703
|
+
"numbering root"
|
|
1704
|
+
);
|
|
1705
|
+
if (children(root, "numPicBullet").length > 0) {
|
|
1706
|
+
throw new DocxImportError(
|
|
1707
|
+
"unsupported_feature",
|
|
1708
|
+
"Picture bullets are not yet supported",
|
|
1709
|
+
"word/numbering.xml"
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
const abstract = /* @__PURE__ */ new Map();
|
|
1713
|
+
for (const definition of children(root, "abstractNum")) {
|
|
1714
|
+
if (abstract.size >= limits.maxNumberingDefinitions) {
|
|
1715
|
+
throw limitError("DOCX exceeds maxNumberingDefinitions", "word/numbering.xml");
|
|
1716
|
+
}
|
|
1717
|
+
const id = requiredAttribute(definition, "abstractNumId", "word/numbering.xml");
|
|
1718
|
+
assertAllowedAttributes(
|
|
1719
|
+
definition,
|
|
1720
|
+
/* @__PURE__ */ new Set(["abstractNumId", "w15:restartNumberingAfterBreak"]),
|
|
1721
|
+
"abstract numbering definition"
|
|
1722
|
+
);
|
|
1723
|
+
const restartAfterBreak = prefixedAttribute(definition, "w15", "restartNumberingAfterBreak");
|
|
1724
|
+
if (restartAfterBreak !== void 0 && parseBooleanLexical(restartAfterBreak)) {
|
|
1725
|
+
throw new DocxImportError(
|
|
1726
|
+
"unsupported_feature",
|
|
1727
|
+
"Restarting numbering after section breaks is not represented",
|
|
1728
|
+
"word/numbering.xml"
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
assertAllowedPropertyChildren(
|
|
1732
|
+
definition,
|
|
1733
|
+
/* @__PURE__ */ new Set(["nsid", "multiLevelType", "tmpl", "name", "lvl"]),
|
|
1734
|
+
"abstract numbering definition"
|
|
1735
|
+
);
|
|
1736
|
+
if (abstract.has(id))
|
|
1737
|
+
throw new DocxImportError(
|
|
1738
|
+
"invalid_package",
|
|
1739
|
+
"Duplicate abstract numbering id",
|
|
1740
|
+
"word/numbering.xml"
|
|
1741
|
+
);
|
|
1742
|
+
const levels = children(definition, "lvl").map(parseListLevel);
|
|
1743
|
+
validateListLevels(levels, "abstract numbering definition");
|
|
1744
|
+
abstract.set(id, levels);
|
|
1745
|
+
}
|
|
1746
|
+
const numIds = /* @__PURE__ */ new Set();
|
|
1747
|
+
const nums = children(root, "num");
|
|
1748
|
+
if (nums.length > limits.maxNumberingDefinitions) {
|
|
1749
|
+
throw limitError("DOCX exceeds maxNumberingDefinitions", "word/numbering.xml");
|
|
1750
|
+
}
|
|
1751
|
+
return nums.map((numbering) => {
|
|
1752
|
+
assertAllowedAttributes(numbering, /* @__PURE__ */ new Set(["numId"]), "numbering definition");
|
|
1753
|
+
assertAllowedPropertyChildren(
|
|
1754
|
+
numbering,
|
|
1755
|
+
/* @__PURE__ */ new Set(["abstractNumId", "lvlOverride"]),
|
|
1756
|
+
"numbering definition"
|
|
1757
|
+
);
|
|
1758
|
+
const numId = requiredAttribute(numbering, "numId", "word/numbering.xml");
|
|
1759
|
+
if (numIds.has(numId))
|
|
1760
|
+
throw new DocxImportError("invalid_package", "Duplicate numbering id", "word/numbering.xml");
|
|
1761
|
+
numIds.add(numId);
|
|
1762
|
+
const abstractNumId = valueOf(firstChild(numbering, "abstractNumId"));
|
|
1763
|
+
if (!abstractNumId || !abstract.has(abstractNumId))
|
|
1764
|
+
throw new DocxImportError(
|
|
1765
|
+
"invalid_package",
|
|
1766
|
+
"Numbering references a missing abstract definition",
|
|
1767
|
+
"word/numbering.xml"
|
|
1768
|
+
);
|
|
1769
|
+
const overrides = children(numbering, "lvlOverride").map((override) => {
|
|
1770
|
+
assertAllowedAttributes(override, /* @__PURE__ */ new Set(["ilvl"]), "numbering override");
|
|
1771
|
+
assertAllowedPropertyChildren(
|
|
1772
|
+
override,
|
|
1773
|
+
/* @__PURE__ */ new Set(["startOverride", "lvl"]),
|
|
1774
|
+
"numbering override"
|
|
1775
|
+
);
|
|
1776
|
+
return stripUndefined({
|
|
1777
|
+
level: parseLevelIndex(override),
|
|
1778
|
+
start: numberValue(firstChild(override, "startOverride")),
|
|
1779
|
+
definition: firstChild(override, "lvl") ? parseListLevel(firstChild(override, "lvl")) : void 0
|
|
1780
|
+
});
|
|
1781
|
+
});
|
|
1782
|
+
validateOverrideLevels(overrides);
|
|
1783
|
+
return {
|
|
1784
|
+
numId,
|
|
1785
|
+
abstractNumId,
|
|
1786
|
+
levels: (abstract.get(abstractNumId) ?? []).map((level) => ({ ...level })),
|
|
1787
|
+
overrides
|
|
1788
|
+
};
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
function parseListLevel(level) {
|
|
1792
|
+
assertAllowedAttributes(level, /* @__PURE__ */ new Set(["ilvl", "tplc", "w15:tentative"]), "numbering level");
|
|
1793
|
+
assertAllowedPropertyChildren(
|
|
1794
|
+
level,
|
|
1795
|
+
/* @__PURE__ */ new Set([
|
|
1796
|
+
"start",
|
|
1797
|
+
"numFmt",
|
|
1798
|
+
"lvlRestart",
|
|
1799
|
+
"pStyle",
|
|
1800
|
+
"isLgl",
|
|
1801
|
+
"suff",
|
|
1802
|
+
"lvlText",
|
|
1803
|
+
"lvlJc",
|
|
1804
|
+
"pPr",
|
|
1805
|
+
"rPr"
|
|
1806
|
+
]),
|
|
1807
|
+
"numbering level"
|
|
1808
|
+
);
|
|
1809
|
+
assertUniqueChildNames(level, "numbering level");
|
|
1810
|
+
for (const name of [
|
|
1811
|
+
"start",
|
|
1812
|
+
"numFmt",
|
|
1813
|
+
"lvlRestart",
|
|
1814
|
+
"pStyle",
|
|
1815
|
+
"isLgl",
|
|
1816
|
+
"suff",
|
|
1817
|
+
"lvlText",
|
|
1818
|
+
"lvlJc"
|
|
1819
|
+
]) {
|
|
1820
|
+
assertAllowedAttributes(firstChild(level, name), /* @__PURE__ */ new Set(["val"]), `numbering level ${name}`);
|
|
1821
|
+
}
|
|
1822
|
+
const restartElement = firstChild(level, "lvlRestart");
|
|
1823
|
+
const restart = restartElement ? integerValue(restartElement, 0) : void 0;
|
|
1824
|
+
if (restart !== void 0 && (restart < 0 || restart > 9)) {
|
|
1825
|
+
throw new DocxImportError(
|
|
1826
|
+
"invalid_package",
|
|
1827
|
+
"Numbering restart level must be from 0 through 9",
|
|
1828
|
+
"word/numbering.xml"
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
return stripUndefined({
|
|
1832
|
+
level: parseLevelIndex(level),
|
|
1833
|
+
start: integerValue(firstChild(level, "start"), 1),
|
|
1834
|
+
restart,
|
|
1835
|
+
legal: optionalBoolean(firstChild(level, "isLgl")),
|
|
1836
|
+
alignment: valueOf(firstChild(level, "lvlJc")),
|
|
1837
|
+
format: valueOf(firstChild(level, "numFmt")),
|
|
1838
|
+
text: valueOf(firstChild(level, "lvlText")),
|
|
1839
|
+
suffix: valueOf(firstChild(level, "suff")),
|
|
1840
|
+
paragraphStyleId: valueOf(firstChild(level, "pStyle")),
|
|
1841
|
+
paragraph: parseParagraphProperties(singletonChild(level, "pPr", "numbering level")),
|
|
1842
|
+
run: parseRunProperties(singletonChild(level, "rPr", "numbering level"))
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
function parseLevelIndex(element) {
|
|
1846
|
+
const raw = requiredAttribute(element, "ilvl", "word/numbering.xml");
|
|
1847
|
+
const level = Number(raw);
|
|
1848
|
+
if (!Number.isInteger(level) || level < 0 || level > 8) {
|
|
1849
|
+
throw new DocxImportError(
|
|
1850
|
+
"invalid_package",
|
|
1851
|
+
"Numbering level must be an integer from 0 through 8",
|
|
1852
|
+
"word/numbering.xml"
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
return level;
|
|
1856
|
+
}
|
|
1857
|
+
function validateListLevels(levels, label) {
|
|
1858
|
+
if (levels.length > 9)
|
|
1859
|
+
throw new DocxImportError(
|
|
1860
|
+
"invalid_package",
|
|
1861
|
+
`${label} has more than nine levels`,
|
|
1862
|
+
"word/numbering.xml"
|
|
1863
|
+
);
|
|
1864
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1865
|
+
for (const level of levels) {
|
|
1866
|
+
if (seen.has(level.level))
|
|
1867
|
+
throw new DocxImportError(
|
|
1868
|
+
"invalid_package",
|
|
1869
|
+
`${label} repeats a level`,
|
|
1870
|
+
"word/numbering.xml"
|
|
1871
|
+
);
|
|
1872
|
+
seen.add(level.level);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
function validateOverrideLevels(overrides) {
|
|
1876
|
+
if (overrides.length > 9)
|
|
1877
|
+
throw new DocxImportError(
|
|
1878
|
+
"invalid_package",
|
|
1879
|
+
"Numbering definition has more than nine overrides",
|
|
1880
|
+
"word/numbering.xml"
|
|
1881
|
+
);
|
|
1882
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1883
|
+
for (const override of overrides) {
|
|
1884
|
+
if (seen.has(override.level))
|
|
1885
|
+
throw new DocxImportError(
|
|
1886
|
+
"invalid_package",
|
|
1887
|
+
"Numbering definition repeats an override",
|
|
1888
|
+
"word/numbering.xml"
|
|
1889
|
+
);
|
|
1890
|
+
seen.add(override.level);
|
|
1891
|
+
if (override.definition && override.definition.level !== override.level) {
|
|
1892
|
+
throw new DocxImportError(
|
|
1893
|
+
"invalid_package",
|
|
1894
|
+
"Numbering override level disagrees with its definition",
|
|
1895
|
+
"word/numbering.xml"
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
function parseParagraphProperties(element) {
|
|
1901
|
+
assertAllowedPropertyChildren(
|
|
1902
|
+
element,
|
|
1903
|
+
/* @__PURE__ */ new Set([
|
|
1904
|
+
"pStyle",
|
|
1905
|
+
"jc",
|
|
1906
|
+
"spacing",
|
|
1907
|
+
"ind",
|
|
1908
|
+
"keepNext",
|
|
1909
|
+
"keepLines",
|
|
1910
|
+
"pageBreakBefore",
|
|
1911
|
+
"outlineLvl",
|
|
1912
|
+
"bidi",
|
|
1913
|
+
"numPr",
|
|
1914
|
+
"sectPr"
|
|
1915
|
+
]),
|
|
1916
|
+
"paragraph properties"
|
|
1917
|
+
);
|
|
1918
|
+
assertUniqueChildNames(element, "paragraph properties");
|
|
1919
|
+
const spacing = firstChild(element, "spacing");
|
|
1920
|
+
const indent = firstChild(element, "ind");
|
|
1921
|
+
const numProperties = firstChild(element, "numPr");
|
|
1922
|
+
assertAllowedPropertyChildren(
|
|
1923
|
+
numProperties,
|
|
1924
|
+
/* @__PURE__ */ new Set(["numId", "ilvl"]),
|
|
1925
|
+
"paragraph numbering properties"
|
|
1926
|
+
);
|
|
1927
|
+
assertUniqueChildNames(numProperties, "paragraph numbering properties");
|
|
1928
|
+
for (const name of [
|
|
1929
|
+
"pStyle",
|
|
1930
|
+
"jc",
|
|
1931
|
+
"keepNext",
|
|
1932
|
+
"keepLines",
|
|
1933
|
+
"pageBreakBefore",
|
|
1934
|
+
"outlineLvl",
|
|
1935
|
+
"bidi"
|
|
1936
|
+
]) {
|
|
1937
|
+
assertAllowedAttributes(firstChild(element, name), /* @__PURE__ */ new Set(["val"]), `paragraph ${name}`);
|
|
1938
|
+
}
|
|
1939
|
+
assertAllowedAttributes(
|
|
1940
|
+
spacing,
|
|
1941
|
+
/* @__PURE__ */ new Set(["before", "after", "line", "lineRule"]),
|
|
1942
|
+
"paragraph spacing"
|
|
1943
|
+
);
|
|
1944
|
+
assertAllowedAttributes(
|
|
1945
|
+
indent,
|
|
1946
|
+
/* @__PURE__ */ new Set(["left", "right", "firstLine", "hanging"]),
|
|
1947
|
+
"paragraph indentation"
|
|
1948
|
+
);
|
|
1949
|
+
assertAllowedAttributes(numProperties, /* @__PURE__ */ new Set(), "paragraph numbering properties");
|
|
1950
|
+
assertAllowedAttributes(
|
|
1951
|
+
firstChild(numProperties, "numId"),
|
|
1952
|
+
/* @__PURE__ */ new Set(["val"]),
|
|
1953
|
+
"paragraph numbering id"
|
|
1954
|
+
);
|
|
1955
|
+
assertAllowedAttributes(
|
|
1956
|
+
firstChild(numProperties, "ilvl"),
|
|
1957
|
+
/* @__PURE__ */ new Set(["val"]),
|
|
1958
|
+
"paragraph numbering level"
|
|
1959
|
+
);
|
|
1960
|
+
if (attribute(indent, "firstLine") !== void 0 && attribute(indent, "hanging") !== void 0) {
|
|
1961
|
+
throw new DocxImportError(
|
|
1962
|
+
"invalid_package",
|
|
1963
|
+
"Paragraph indentation cannot specify both firstLine and hanging",
|
|
1964
|
+
"paragraph indentation"
|
|
1965
|
+
);
|
|
1966
|
+
}
|
|
1967
|
+
const lineRule = attribute(spacing, "lineRule");
|
|
1968
|
+
if (lineRule !== void 0 && lineRule !== "auto" && lineRule !== "exact" && lineRule !== "atLeast") {
|
|
1969
|
+
throw new DocxImportError(
|
|
1970
|
+
"invalid_package",
|
|
1971
|
+
"Paragraph line rule is invalid",
|
|
1972
|
+
"paragraph spacing"
|
|
1973
|
+
);
|
|
1974
|
+
}
|
|
1975
|
+
const numId = valueOf(firstChild(numProperties, "numId"));
|
|
1976
|
+
return stripUndefined({
|
|
1977
|
+
alignment: valueOf(firstChild(element, "jc")),
|
|
1978
|
+
spaceBeforePt: numberAttribute(spacing, "before", twipsToPt),
|
|
1979
|
+
spaceAfterPt: numberAttribute(spacing, "after", twipsToPt),
|
|
1980
|
+
line: numberAttribute(spacing, "line"),
|
|
1981
|
+
lineRule,
|
|
1982
|
+
indentLeftPt: numberAttribute(indent, "left", twipsToPt),
|
|
1983
|
+
indentRightPt: numberAttribute(indent, "right", twipsToPt),
|
|
1984
|
+
firstLinePt: numberAttribute(indent, "firstLine", twipsToPt),
|
|
1985
|
+
hangingPt: numberAttribute(indent, "hanging", twipsToPt),
|
|
1986
|
+
keepNext: optionalBoolean(firstChild(element, "keepNext")),
|
|
1987
|
+
keepLines: optionalBoolean(firstChild(element, "keepLines")),
|
|
1988
|
+
pageBreakBefore: optionalBoolean(firstChild(element, "pageBreakBefore")),
|
|
1989
|
+
outlineLevel: numberValue(firstChild(element, "outlineLvl")),
|
|
1990
|
+
bidirectional: optionalBoolean(firstChild(element, "bidi")),
|
|
1991
|
+
list: numId === void 0 ? void 0 : { numId, level: integerValue(firstChild(numProperties, "ilvl"), 0) }
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
function parseRunProperties(element) {
|
|
1995
|
+
assertAllowedPropertyChildren(
|
|
1996
|
+
element,
|
|
1997
|
+
/* @__PURE__ */ new Set([
|
|
1998
|
+
"rStyle",
|
|
1999
|
+
"rFonts",
|
|
2000
|
+
"sz",
|
|
2001
|
+
"szCs",
|
|
2002
|
+
"color",
|
|
2003
|
+
"b",
|
|
2004
|
+
"bCs",
|
|
2005
|
+
"i",
|
|
2006
|
+
"iCs",
|
|
2007
|
+
"u",
|
|
2008
|
+
"strike",
|
|
2009
|
+
"highlight",
|
|
2010
|
+
"vertAlign",
|
|
2011
|
+
"lang"
|
|
2012
|
+
]),
|
|
2013
|
+
"run properties"
|
|
2014
|
+
);
|
|
2015
|
+
assertUniqueChildNames(element, "run properties");
|
|
2016
|
+
const fonts = firstChild(element, "rFonts");
|
|
2017
|
+
const color = firstChild(element, "color");
|
|
2018
|
+
if (["asciiTheme", "hAnsiTheme", "eastAsiaTheme", "cstheme"].some(
|
|
2019
|
+
(name) => attribute(fonts, name) !== void 0
|
|
2020
|
+
)) {
|
|
2021
|
+
throw new DocxImportError(
|
|
2022
|
+
"unsupported_feature",
|
|
2023
|
+
"Theme font references are not represented by the document model",
|
|
2024
|
+
"run properties"
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
if (["themeColor", "themeTint", "themeShade"].some((name) => attribute(color, name) !== void 0)) {
|
|
2028
|
+
throw new DocxImportError(
|
|
2029
|
+
"unsupported_feature",
|
|
2030
|
+
"Theme colors are not represented by the document model",
|
|
2031
|
+
"run properties"
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
assertAllowedAttributes(fonts, /* @__PURE__ */ new Set(["ascii", "hAnsi", "eastAsia", "cs"]), "run fonts");
|
|
2035
|
+
assertAllowedAttributes(color, /* @__PURE__ */ new Set(["val"]), "run color");
|
|
2036
|
+
const underlineElement = firstChild(element, "u");
|
|
2037
|
+
assertAllowedAttributes(underlineElement, /* @__PURE__ */ new Set(["val"]), "run underline");
|
|
2038
|
+
for (const name of [
|
|
2039
|
+
"rStyle",
|
|
2040
|
+
"sz",
|
|
2041
|
+
"szCs",
|
|
2042
|
+
"b",
|
|
2043
|
+
"bCs",
|
|
2044
|
+
"i",
|
|
2045
|
+
"iCs",
|
|
2046
|
+
"strike",
|
|
2047
|
+
"highlight",
|
|
2048
|
+
"vertAlign",
|
|
2049
|
+
"lang"
|
|
2050
|
+
]) {
|
|
2051
|
+
assertAllowedAttributes(firstChild(element, name), /* @__PURE__ */ new Set(["val"]), `run ${name}`);
|
|
2052
|
+
}
|
|
2053
|
+
const underline = valueOf(underlineElement);
|
|
2054
|
+
if (underline !== void 0 && underline !== "single" && underline !== "none") {
|
|
2055
|
+
throw new DocxImportError(
|
|
2056
|
+
"unsupported_feature",
|
|
2057
|
+
`Underline style ${underline} is not represented by the document model`,
|
|
2058
|
+
"run properties"
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
const directFonts = ["ascii", "hAnsi", "eastAsia", "cs"].map((name) => attribute(fonts, name)).filter((value) => value !== void 0);
|
|
2062
|
+
if (new Set(directFonts).size > 1) {
|
|
2063
|
+
throw new DocxImportError(
|
|
2064
|
+
"unsupported_feature",
|
|
2065
|
+
"Script-specific font families are not represented by the document model",
|
|
2066
|
+
"run properties"
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
const colorValue = valueOf(color);
|
|
2070
|
+
if (colorValue?.toLowerCase() === "auto") {
|
|
2071
|
+
throw new DocxImportError(
|
|
2072
|
+
"unsupported_feature",
|
|
2073
|
+
"Automatic run colors are not represented by the document model",
|
|
2074
|
+
"run properties"
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
return stripUndefined({
|
|
2078
|
+
fontFamily: directFonts[0],
|
|
2079
|
+
fontSizePt: numberValue(firstChild(element, "sz"), (value) => value / 2),
|
|
2080
|
+
fontSizeComplexScriptPt: numberValue(firstChild(element, "szCs"), (value) => value / 2),
|
|
2081
|
+
color: colorValue,
|
|
2082
|
+
bold: optionalBoolean(firstChild(element, "b")),
|
|
2083
|
+
boldComplexScript: optionalBoolean(firstChild(element, "bCs")),
|
|
2084
|
+
italic: optionalBoolean(firstChild(element, "i")),
|
|
2085
|
+
italicComplexScript: optionalBoolean(firstChild(element, "iCs")),
|
|
2086
|
+
underline,
|
|
2087
|
+
strike: optionalBoolean(firstChild(element, "strike")),
|
|
2088
|
+
highlight: valueOf(firstChild(element, "highlight")),
|
|
2089
|
+
verticalAlign: valueOf(firstChild(element, "vertAlign")),
|
|
2090
|
+
language: attribute(firstChild(element, "lang"), "val")
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
function assertAllowedPropertyChildren(element, allowed, label) {
|
|
2094
|
+
for (const child of elementChildren(element)) {
|
|
2095
|
+
if (!allowed.has(child.localName)) {
|
|
2096
|
+
throw new DocxImportError(
|
|
2097
|
+
"unsupported_feature",
|
|
2098
|
+
`Unsupported ${label}: ${child.name}`,
|
|
2099
|
+
label
|
|
2100
|
+
);
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
function assertAllowedContentChildren(element, allowed, label) {
|
|
2105
|
+
for (const child of elementChildren(element)) {
|
|
2106
|
+
if (!allowed.has(child.localName) && !NON_CONTENT_MARKERS.has(child.localName)) {
|
|
2107
|
+
throw new DocxImportError(
|
|
2108
|
+
"unsupported_feature",
|
|
2109
|
+
`Unsupported ${label} content: ${child.name}`,
|
|
2110
|
+
label
|
|
2111
|
+
);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
function assertAllowedAttributes(element, allowed, label) {
|
|
2116
|
+
if (!element) return;
|
|
2117
|
+
for (const name of element.attributes.keys()) {
|
|
2118
|
+
if (name === "xmlns" || name.startsWith("xmlns:")) continue;
|
|
2119
|
+
const permitted = allowed.has(name) || name.startsWith("w:") && allowed.has(name.slice(2));
|
|
2120
|
+
if (!permitted) {
|
|
2121
|
+
throw new DocxImportError(
|
|
2122
|
+
"unsupported_feature",
|
|
2123
|
+
`Unsupported ${label} attribute: ${name}`,
|
|
2124
|
+
label
|
|
2125
|
+
);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
function parseSection(element, startBlockIndex, endBlockIndex, relationships) {
|
|
2130
|
+
assertAllowedAttributes(element, /* @__PURE__ */ new Set(["rsidR", "rsidRPr", "rsidSect"]), "section properties");
|
|
2131
|
+
assertAllowedPropertyChildren(
|
|
2132
|
+
element,
|
|
2133
|
+
/* @__PURE__ */ new Set([
|
|
2134
|
+
"headerReference",
|
|
2135
|
+
"footerReference",
|
|
2136
|
+
"type",
|
|
2137
|
+
"pgSz",
|
|
2138
|
+
"pgMar",
|
|
2139
|
+
"pgNumType",
|
|
2140
|
+
"titlePg",
|
|
2141
|
+
"docGrid"
|
|
2142
|
+
]),
|
|
2143
|
+
"section properties"
|
|
2144
|
+
);
|
|
2145
|
+
for (const singleton of ["type", "pgSz", "pgMar", "pgNumType", "titlePg", "docGrid"]) {
|
|
2146
|
+
if (children(element, singleton).length > 1) {
|
|
2147
|
+
throw new DocxImportError(
|
|
2148
|
+
"invalid_package",
|
|
2149
|
+
`Section repeats ${singleton}`,
|
|
2150
|
+
"word/document.xml"
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
const pageSize = firstChild(element, "pgSz");
|
|
2155
|
+
const margins = firstChild(element, "pgMar");
|
|
2156
|
+
assertAllowedAttributes(pageSize, /* @__PURE__ */ new Set(["w", "h", "orient", "code"]), "section page size");
|
|
2157
|
+
assertAllowedAttributes(
|
|
2158
|
+
margins,
|
|
2159
|
+
/* @__PURE__ */ new Set(["top", "right", "bottom", "left", "header", "footer", "gutter"]),
|
|
2160
|
+
"section page margins"
|
|
2161
|
+
);
|
|
2162
|
+
assertAllowedContentChildren(pageSize, /* @__PURE__ */ new Set(), "section page size");
|
|
2163
|
+
assertAllowedContentChildren(margins, /* @__PURE__ */ new Set(), "section page margins");
|
|
2164
|
+
if (pageSize && (attribute(pageSize, "w") === void 0 || attribute(pageSize, "h") === void 0)) {
|
|
2165
|
+
throw new DocxImportError(
|
|
2166
|
+
"invalid_package",
|
|
2167
|
+
"Section page size is incomplete",
|
|
2168
|
+
"word/document.xml"
|
|
2169
|
+
);
|
|
2170
|
+
}
|
|
2171
|
+
if (margins && ["top", "right", "bottom", "left", "header", "footer", "gutter"].some(
|
|
2172
|
+
(name) => attribute(margins, name) === void 0
|
|
2173
|
+
)) {
|
|
2174
|
+
throw new DocxImportError(
|
|
2175
|
+
"invalid_package",
|
|
2176
|
+
"Section page margins are incomplete",
|
|
2177
|
+
"word/document.xml"
|
|
2178
|
+
);
|
|
2179
|
+
}
|
|
2180
|
+
const pageNumbering = firstChild(element, "pgNumType");
|
|
2181
|
+
assertAllowedAttributes(pageNumbering, /* @__PURE__ */ new Set(), "section page numbering");
|
|
2182
|
+
assertAllowedContentChildren(pageNumbering, /* @__PURE__ */ new Set(), "section page numbering");
|
|
2183
|
+
const documentGrid = firstChild(element, "docGrid");
|
|
2184
|
+
assertAllowedAttributes(
|
|
2185
|
+
documentGrid,
|
|
2186
|
+
/* @__PURE__ */ new Set(["type", "linePitch", "charSpace"]),
|
|
2187
|
+
"section document grid"
|
|
2188
|
+
);
|
|
2189
|
+
assertAllowedContentChildren(documentGrid, /* @__PURE__ */ new Set(), "section document grid");
|
|
2190
|
+
const gridType = attribute(documentGrid, "type");
|
|
2191
|
+
if (gridType !== void 0 && gridType !== "default") {
|
|
2192
|
+
throw new DocxImportError(
|
|
2193
|
+
"unsupported_feature",
|
|
2194
|
+
`Document grid type ${gridType} is not represented`,
|
|
2195
|
+
"word/document.xml"
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2198
|
+
numberAttribute(documentGrid, "linePitch");
|
|
2199
|
+
numberAttribute(documentGrid, "charSpace");
|
|
2200
|
+
const sectionTypeElement = firstChild(element, "type");
|
|
2201
|
+
const titlePageElement = firstChild(element, "titlePg");
|
|
2202
|
+
assertAllowedAttributes(sectionTypeElement, /* @__PURE__ */ new Set(["val"]), "section break type");
|
|
2203
|
+
assertAllowedAttributes(titlePageElement, /* @__PURE__ */ new Set(["val"]), "section title-page setting");
|
|
2204
|
+
assertAllowedContentChildren(sectionTypeElement, /* @__PURE__ */ new Set(), "section break type");
|
|
2205
|
+
assertAllowedContentChildren(titlePageElement, /* @__PURE__ */ new Set(), "section title-page setting");
|
|
2206
|
+
const orientation = attribute(pageSize, "orient");
|
|
2207
|
+
if (orientation !== void 0 && orientation !== "portrait" && orientation !== "landscape") {
|
|
2208
|
+
throw new DocxImportError(
|
|
2209
|
+
"invalid_package",
|
|
2210
|
+
"Section page orientation is invalid",
|
|
2211
|
+
"word/document.xml"
|
|
2212
|
+
);
|
|
2213
|
+
}
|
|
2214
|
+
const sectionType = valueOf(sectionTypeElement);
|
|
2215
|
+
if (sectionType !== void 0 && !(/* @__PURE__ */ new Set(["nextPage", "nextColumn", "continuous", "evenPage", "oddPage"])).has(sectionType)) {
|
|
2216
|
+
throw new DocxImportError(
|
|
2217
|
+
"invalid_package",
|
|
2218
|
+
"Section break type is invalid",
|
|
2219
|
+
"word/document.xml"
|
|
2220
|
+
);
|
|
2221
|
+
}
|
|
2222
|
+
const references = (kind) => children(element, `${kind}Reference`).map((reference) => {
|
|
2223
|
+
assertAllowedAttributes(reference, /* @__PURE__ */ new Set(["r:id", "type"]), `${kind} reference`);
|
|
2224
|
+
assertAllowedContentChildren(reference, /* @__PURE__ */ new Set(), `${kind} reference`);
|
|
2225
|
+
const relationshipId = requiredPrefixedAttribute(reference, "r", "id", "word/document.xml");
|
|
2226
|
+
const relationship = relationships.get(relationshipId);
|
|
2227
|
+
if (!relationship || relationship.type !== `${WORD_RELATIONSHIP_BASE}${kind}`) {
|
|
2228
|
+
throw new DocxImportError(
|
|
2229
|
+
"invalid_package",
|
|
2230
|
+
`Section references a missing ${kind}`,
|
|
2231
|
+
"word/document.xml"
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2234
|
+
const referenceKind = attribute(reference, "type") ?? "default";
|
|
2235
|
+
if (referenceKind !== "default" && referenceKind !== "first" && referenceKind !== "even") {
|
|
2236
|
+
throw new DocxImportError(
|
|
2237
|
+
"invalid_package",
|
|
2238
|
+
`Invalid ${kind} reference kind`,
|
|
2239
|
+
"word/document.xml"
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
2242
|
+
return {
|
|
2243
|
+
kind: referenceKind,
|
|
2244
|
+
relationshipId,
|
|
2245
|
+
partName: relationship.partName
|
|
2246
|
+
};
|
|
2247
|
+
});
|
|
2248
|
+
const headers = references("header");
|
|
2249
|
+
const footers = references("footer");
|
|
2250
|
+
for (const storyReferences of [headers, footers]) {
|
|
2251
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
2252
|
+
for (const reference of storyReferences) {
|
|
2253
|
+
if (kinds.has(reference.kind))
|
|
2254
|
+
throw new DocxImportError(
|
|
2255
|
+
"invalid_package",
|
|
2256
|
+
"Section repeats a header/footer reference kind",
|
|
2257
|
+
"word/document.xml"
|
|
2258
|
+
);
|
|
2259
|
+
kinds.add(reference.kind);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
return stripUndefined({
|
|
2263
|
+
startBlockIndex,
|
|
2264
|
+
endBlockIndex,
|
|
2265
|
+
type: sectionType,
|
|
2266
|
+
titlePage: booleanValue(titlePageElement, false),
|
|
2267
|
+
page: {
|
|
2268
|
+
widthPt: numberAttribute(pageSize, "w", twipsToPt) ?? 612,
|
|
2269
|
+
heightPt: numberAttribute(pageSize, "h", twipsToPt) ?? 792,
|
|
2270
|
+
...orientation !== void 0 ? { orientation } : {},
|
|
2271
|
+
marginTopPt: numberAttribute(margins, "top", twipsToPt) ?? 72,
|
|
2272
|
+
marginRightPt: numberAttribute(margins, "right", twipsToPt) ?? 72,
|
|
2273
|
+
marginBottomPt: numberAttribute(margins, "bottom", twipsToPt) ?? 72,
|
|
2274
|
+
marginLeftPt: numberAttribute(margins, "left", twipsToPt) ?? 72,
|
|
2275
|
+
headerPt: numberAttribute(margins, "header", twipsToPt) ?? 36,
|
|
2276
|
+
footerPt: numberAttribute(margins, "footer", twipsToPt) ?? 36,
|
|
2277
|
+
gutterPt: numberAttribute(margins, "gutter", twipsToPt) ?? 0
|
|
2278
|
+
},
|
|
2279
|
+
headers,
|
|
2280
|
+
footers
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
function defaultSection(startBlockIndex, endBlockIndex) {
|
|
2284
|
+
return {
|
|
2285
|
+
startBlockIndex,
|
|
2286
|
+
endBlockIndex,
|
|
2287
|
+
titlePage: false,
|
|
2288
|
+
page: {
|
|
2289
|
+
widthPt: 612,
|
|
2290
|
+
heightPt: 792,
|
|
2291
|
+
marginTopPt: 72,
|
|
2292
|
+
marginRightPt: 72,
|
|
2293
|
+
marginBottomPt: 72,
|
|
2294
|
+
marginLeftPt: 72,
|
|
2295
|
+
headerPt: 36,
|
|
2296
|
+
footerPt: 36,
|
|
2297
|
+
gutterPt: 0
|
|
2298
|
+
},
|
|
2299
|
+
headers: [],
|
|
2300
|
+
footers: []
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
function resolveSectionStoryInheritance(sections) {
|
|
2304
|
+
for (let index = 1; index < sections.length; index += 1) {
|
|
2305
|
+
const previous = sections[index - 1];
|
|
2306
|
+
const current = sections[index];
|
|
2307
|
+
if (!previous || !current) continue;
|
|
2308
|
+
for (const key of ["headers", "footers"]) {
|
|
2309
|
+
const present = new Set(current[key].map((reference) => reference.kind));
|
|
2310
|
+
for (const inherited of previous[key]) {
|
|
2311
|
+
if (!present.has(inherited.kind)) current[key].push({ ...inherited });
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
function validateProjectedStories(sections, headers, footers, maximumBlocks, maximumCharacters) {
|
|
2317
|
+
const costByPart = /* @__PURE__ */ new Map();
|
|
2318
|
+
for (const story of [...headers, ...footers]) {
|
|
2319
|
+
costByPart.set(story.partName, measureBlockTree(story.blocks));
|
|
2320
|
+
}
|
|
2321
|
+
let projectedBlocks = 0;
|
|
2322
|
+
let projectedCharacters = 0;
|
|
2323
|
+
for (const section of sections) {
|
|
2324
|
+
for (const reference of [...section.headers, ...section.footers]) {
|
|
2325
|
+
const storyCost = costByPart.get(reference.partName);
|
|
2326
|
+
if (storyCost === void 0) {
|
|
2327
|
+
throw new DocxImportError(
|
|
2328
|
+
"invalid_package",
|
|
2329
|
+
"Section references an unparsed header or footer",
|
|
2330
|
+
reference.partName
|
|
2331
|
+
);
|
|
2332
|
+
}
|
|
2333
|
+
projectedBlocks += storyCost.blocks;
|
|
2334
|
+
projectedCharacters += storyCost.characters;
|
|
2335
|
+
if (projectedBlocks > maximumBlocks)
|
|
2336
|
+
throw limitError("DOCX exceeds maxProjectedStoryBlocks", reference.partName);
|
|
2337
|
+
if (projectedCharacters > maximumCharacters) {
|
|
2338
|
+
throw limitError("DOCX exceeds maxProjectedStoryCharacters", reference.partName);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
function measureBlockTree(blocks) {
|
|
2344
|
+
let count = 0;
|
|
2345
|
+
let characters = 0;
|
|
2346
|
+
const stack = [...blocks];
|
|
2347
|
+
while (stack.length > 0) {
|
|
2348
|
+
const block = stack.pop();
|
|
2349
|
+
if (!block) continue;
|
|
2350
|
+
count += 1;
|
|
2351
|
+
if (block.kind === "paragraph") {
|
|
2352
|
+
for (const inline of block.inlines)
|
|
2353
|
+
if (inline.kind === "run") characters += inline.text.length;
|
|
2354
|
+
} else if (block.kind === "table") {
|
|
2355
|
+
for (const row of block.rows)
|
|
2356
|
+
for (const cell of row.cells) {
|
|
2357
|
+
for (const child of cell.blocks) stack.push(child);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
return { blocks: count, characters };
|
|
2362
|
+
}
|
|
2363
|
+
function parseCommentsExtended(root, maximum) {
|
|
2364
|
+
requireLocalName(root, "commentsEx", "word/commentsExtended.xml");
|
|
2365
|
+
requirePrefixedNamespaceTree(
|
|
2366
|
+
root,
|
|
2367
|
+
"w15",
|
|
2368
|
+
COMMENTS_EXTENDED_NAMESPACE,
|
|
2369
|
+
"word/commentsExtended.xml"
|
|
2370
|
+
);
|
|
2371
|
+
const result = /* @__PURE__ */ new Map();
|
|
2372
|
+
for (const comment of elementChildren(root)) {
|
|
2373
|
+
if (comment.localName !== "commentEx")
|
|
2374
|
+
throw unsupportedElement(comment, "word/commentsExtended.xml");
|
|
2375
|
+
if (result.size >= maximum)
|
|
2376
|
+
throw limitError("DOCX exceeds maxComments", "word/commentsExtended.xml");
|
|
2377
|
+
const paragraphId = requiredPrefixedAttribute(
|
|
2378
|
+
comment,
|
|
2379
|
+
"w15",
|
|
2380
|
+
"paraId",
|
|
2381
|
+
"word/commentsExtended.xml"
|
|
2382
|
+
);
|
|
2383
|
+
if (result.has(paragraphId))
|
|
2384
|
+
throw new DocxImportError(
|
|
2385
|
+
"invalid_package",
|
|
2386
|
+
"Duplicate extended comment paragraph id",
|
|
2387
|
+
"word/commentsExtended.xml"
|
|
2388
|
+
);
|
|
2389
|
+
const parentParagraphId = prefixedAttribute(comment, "w15", "paraIdParent");
|
|
2390
|
+
const done = prefixedAttribute(comment, "w15", "done");
|
|
2391
|
+
result.set(paragraphId, {
|
|
2392
|
+
...parentParagraphId !== void 0 ? { parentParagraphId } : {},
|
|
2393
|
+
...done !== void 0 ? { resolved: !FALSE_VALUES.has(done.toLowerCase()) } : {}
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2396
|
+
return result;
|
|
2397
|
+
}
|
|
2398
|
+
function parseComments(root, context, extended, maximum) {
|
|
2399
|
+
requireLocalName(root, "comments", "word/comments.xml");
|
|
2400
|
+
rejectUnsupportedContent(root, "word/comments.xml");
|
|
2401
|
+
requireWordNamespace(root, "word/comments.xml");
|
|
2402
|
+
const drafts = [];
|
|
2403
|
+
for (const comment of elementChildren(root)) {
|
|
2404
|
+
if (comment.localName !== "comment") throw unsupportedElement(comment, "word/comments.xml");
|
|
2405
|
+
if (drafts.length >= maximum) throw limitError("DOCX exceeds maxComments", "word/comments.xml");
|
|
2406
|
+
const lastParagraph = lastDescendant(comment, "p");
|
|
2407
|
+
const paragraphId = prefixedAttribute(lastParagraph, "w14", "paraId");
|
|
2408
|
+
drafts.push({
|
|
2409
|
+
// commentsExtended keys a comment by the paraId of its final paragraph.
|
|
2410
|
+
...paragraphId !== void 0 ? { paragraphId } : {},
|
|
2411
|
+
comment: stripUndefined({
|
|
2412
|
+
id: requiredAttribute(comment, "id", "word/comments.xml"),
|
|
2413
|
+
author: attribute(comment, "author"),
|
|
2414
|
+
initials: attribute(comment, "initials"),
|
|
2415
|
+
createdAt: attribute(comment, "date"),
|
|
2416
|
+
blocks: parseBlockContainer(comment, context)
|
|
2417
|
+
})
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
const commentIdByParagraphId = /* @__PURE__ */ new Map();
|
|
2421
|
+
const commentIds = /* @__PURE__ */ new Set();
|
|
2422
|
+
for (const draft of drafts) {
|
|
2423
|
+
if (commentIds.has(draft.comment.id))
|
|
2424
|
+
throw new DocxImportError("invalid_package", "Duplicate comment id", "word/comments.xml");
|
|
2425
|
+
commentIds.add(draft.comment.id);
|
|
2426
|
+
if (!draft.paragraphId) continue;
|
|
2427
|
+
if (commentIdByParagraphId.has(draft.paragraphId)) {
|
|
2428
|
+
throw new DocxImportError(
|
|
2429
|
+
"invalid_package",
|
|
2430
|
+
"Duplicate comment paragraph id",
|
|
2431
|
+
"word/comments.xml"
|
|
2432
|
+
);
|
|
2433
|
+
}
|
|
2434
|
+
commentIdByParagraphId.set(draft.paragraphId, draft.comment.id);
|
|
2435
|
+
}
|
|
2436
|
+
for (const draft of drafts) {
|
|
2437
|
+
if (!draft.paragraphId) continue;
|
|
2438
|
+
const metadata = extended.get(draft.paragraphId);
|
|
2439
|
+
if (!metadata) continue;
|
|
2440
|
+
if (metadata.resolved !== void 0) draft.comment.resolved = metadata.resolved;
|
|
2441
|
+
if (metadata.parentParagraphId !== void 0) {
|
|
2442
|
+
const parentId = commentIdByParagraphId.get(metadata.parentParagraphId);
|
|
2443
|
+
if (!parentId)
|
|
2444
|
+
throw new DocxImportError(
|
|
2445
|
+
"invalid_package",
|
|
2446
|
+
"Comment reply references a missing parent",
|
|
2447
|
+
"word/commentsExtended.xml"
|
|
2448
|
+
);
|
|
2449
|
+
draft.comment.parentId = parentId;
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
for (const paragraphId of extended.keys()) {
|
|
2453
|
+
if (!commentIdByParagraphId.has(paragraphId)) {
|
|
2454
|
+
throw new DocxImportError(
|
|
2455
|
+
"invalid_package",
|
|
2456
|
+
"Extended comment references a missing comment paragraph",
|
|
2457
|
+
"word/commentsExtended.xml"
|
|
2458
|
+
);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
return drafts.map((draft) => draft.comment);
|
|
2462
|
+
}
|
|
2463
|
+
function validateCommentReferences(blocks, headers, footers, comments) {
|
|
2464
|
+
const ids = new Set(comments.map((comment) => comment.id));
|
|
2465
|
+
const commentById = new Map(comments.map((comment) => [comment.id, comment]));
|
|
2466
|
+
const roots = new Set(
|
|
2467
|
+
comments.filter((comment) => comment.parentId === void 0).map((comment) => comment.id)
|
|
2468
|
+
);
|
|
2469
|
+
for (const comment of comments) {
|
|
2470
|
+
if (comment.parentId !== void 0 && !ids.has(comment.parentId)) {
|
|
2471
|
+
throw new DocxImportError(
|
|
2472
|
+
"invalid_package",
|
|
2473
|
+
"Comment reply references a missing parent",
|
|
2474
|
+
"word/commentsExtended.xml"
|
|
2475
|
+
);
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
const visitState = /* @__PURE__ */ new Map();
|
|
2479
|
+
for (const comment of comments) {
|
|
2480
|
+
if (visitState.get(comment.id) === 2) continue;
|
|
2481
|
+
const path = [];
|
|
2482
|
+
let current = comment;
|
|
2483
|
+
while (current && visitState.get(current.id) === void 0) {
|
|
2484
|
+
visitState.set(current.id, 1);
|
|
2485
|
+
path.push(current.id);
|
|
2486
|
+
current = current.parentId === void 0 ? void 0 : commentById.get(current.parentId);
|
|
2487
|
+
}
|
|
2488
|
+
if (current && visitState.get(current.id) === 1) {
|
|
2489
|
+
throw new DocxImportError(
|
|
2490
|
+
"invalid_package",
|
|
2491
|
+
"Comment reply graph contains a cycle",
|
|
2492
|
+
"word/commentsExtended.xml"
|
|
2493
|
+
);
|
|
2494
|
+
}
|
|
2495
|
+
for (const id of path) visitState.set(id, 2);
|
|
2496
|
+
}
|
|
2497
|
+
const storyCountsByComment = /* @__PURE__ */ new Map();
|
|
2498
|
+
const countStory = (items) => {
|
|
2499
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2500
|
+
const walk = (nested) => {
|
|
2501
|
+
for (const block of nested) {
|
|
2502
|
+
if (block.kind === "paragraph") {
|
|
2503
|
+
for (const anchor of block.commentAnchors) {
|
|
2504
|
+
if (!ids.has(anchor.commentId))
|
|
2505
|
+
throw new DocxImportError(
|
|
2506
|
+
"invalid_package",
|
|
2507
|
+
"Document references a missing comment",
|
|
2508
|
+
"word/comments.xml"
|
|
2509
|
+
);
|
|
2510
|
+
if (!roots.has(anchor.commentId))
|
|
2511
|
+
throw new DocxImportError(
|
|
2512
|
+
"invalid_package",
|
|
2513
|
+
"Document anchors a comment reply instead of its root",
|
|
2514
|
+
"word/comments.xml"
|
|
2515
|
+
);
|
|
2516
|
+
const count = counts.get(anchor.commentId) ?? { start: 0, end: 0, reference: 0 };
|
|
2517
|
+
count[anchor.kind] += 1;
|
|
2518
|
+
counts.set(anchor.commentId, count);
|
|
2519
|
+
}
|
|
2520
|
+
} else if (block.kind === "table") {
|
|
2521
|
+
for (const row of block.rows) for (const cell of row.cells) walk(cell.blocks);
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
};
|
|
2525
|
+
walk(items);
|
|
2526
|
+
for (const [commentId, count] of counts) {
|
|
2527
|
+
const stories = storyCountsByComment.get(commentId);
|
|
2528
|
+
if (stories) stories.push(count);
|
|
2529
|
+
else storyCountsByComment.set(commentId, [count]);
|
|
2530
|
+
}
|
|
2531
|
+
};
|
|
2532
|
+
countStory(blocks);
|
|
2533
|
+
for (const item of [...headers, ...footers]) countStory(item.blocks);
|
|
2534
|
+
for (const comment of comments) {
|
|
2535
|
+
const anchoredStories = storyCountsByComment.get(comment.id) ?? [];
|
|
2536
|
+
if (comment.parentId === void 0) {
|
|
2537
|
+
if (anchoredStories.length !== 1) {
|
|
2538
|
+
throw new DocxImportError(
|
|
2539
|
+
"invalid_package",
|
|
2540
|
+
"Root comment must be anchored in exactly one document story",
|
|
2541
|
+
"word/document.xml"
|
|
2542
|
+
);
|
|
2543
|
+
}
|
|
2544
|
+
const count = anchoredStories[0];
|
|
2545
|
+
if (count.reference !== 1 || count.start > 1 || count.end > 1) {
|
|
2546
|
+
throw new DocxImportError(
|
|
2547
|
+
"invalid_package",
|
|
2548
|
+
"Comment story has duplicate or missing reference anchors",
|
|
2549
|
+
"word/document.xml"
|
|
2550
|
+
);
|
|
2551
|
+
}
|
|
2552
|
+
} else if (anchoredStories.length > 0) {
|
|
2553
|
+
throw new DocxImportError(
|
|
2554
|
+
"invalid_package",
|
|
2555
|
+
"Comment replies must not have document anchors",
|
|
2556
|
+
"word/document.xml"
|
|
2557
|
+
);
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
var UNSUPPORTED_CONTENT = /* @__PURE__ */ new Map([
|
|
2562
|
+
["drawing", "drawings and images"],
|
|
2563
|
+
["pict", "VML drawings and images"],
|
|
2564
|
+
["object", "embedded objects"],
|
|
2565
|
+
["oleObject", "OLE objects"],
|
|
2566
|
+
["altChunk", "altChunk content"],
|
|
2567
|
+
["hyperlink", "hyperlinks"],
|
|
2568
|
+
["fldSimple", "fields"],
|
|
2569
|
+
["fldChar", "fields"],
|
|
2570
|
+
["instrText", "fields"],
|
|
2571
|
+
["footnoteReference", "footnotes"],
|
|
2572
|
+
["endnoteReference", "endnotes"],
|
|
2573
|
+
["sdt", "content controls"],
|
|
2574
|
+
["customXml", "custom XML content"],
|
|
2575
|
+
["smartTag", "smart tags"],
|
|
2576
|
+
["oMath", "equations"],
|
|
2577
|
+
["oMathPara", "equations"],
|
|
2578
|
+
["sym", "symbol-font characters"],
|
|
2579
|
+
["ruby", "ruby annotations"],
|
|
2580
|
+
["subDoc", "subdocuments"],
|
|
2581
|
+
["txbxContent", "text boxes"],
|
|
2582
|
+
["AlternateContent", "alternate content"],
|
|
2583
|
+
["moveFrom", "tracked moves"],
|
|
2584
|
+
["moveTo", "tracked moves"],
|
|
2585
|
+
["pPrChange", "paragraph-property revisions"],
|
|
2586
|
+
["rPrChange", "run-property revisions"],
|
|
2587
|
+
["tblPrChange", "table-property revisions"],
|
|
2588
|
+
["tblGridChange", "table-grid revisions"],
|
|
2589
|
+
["trPrChange", "table-row-property revisions"],
|
|
2590
|
+
["tcPrChange", "table-cell-property revisions"],
|
|
2591
|
+
["sectPrChange", "section-property revisions"],
|
|
2592
|
+
["numberingChange", "numbering-property revisions"],
|
|
2593
|
+
["cellIns", "table-cell insertion revisions"],
|
|
2594
|
+
["cellDel", "table-cell deletion revisions"],
|
|
2595
|
+
["cellMerge", "table-cell merge revisions"],
|
|
2596
|
+
["background", "document backgrounds"]
|
|
2597
|
+
]);
|
|
2598
|
+
function rejectUnsupportedContent(root, partName) {
|
|
2599
|
+
const stack = [root];
|
|
2600
|
+
while (stack.length > 0) {
|
|
2601
|
+
const element = stack.pop();
|
|
2602
|
+
if (!element) break;
|
|
2603
|
+
const feature = UNSUPPORTED_CONTENT.get(element.localName);
|
|
2604
|
+
if (feature)
|
|
2605
|
+
throw new DocxImportError(
|
|
2606
|
+
"unsupported_feature",
|
|
2607
|
+
`DOCX ${feature} are not yet supported`,
|
|
2608
|
+
partName
|
|
2609
|
+
);
|
|
2610
|
+
for (const child of elementChildren(element)) stack.push(child);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
var NON_CONTENT_MARKERS = /* @__PURE__ */ new Set([
|
|
2614
|
+
"bookmarkStart",
|
|
2615
|
+
"bookmarkEnd",
|
|
2616
|
+
"proofErr",
|
|
2617
|
+
"permStart",
|
|
2618
|
+
"permEnd",
|
|
2619
|
+
"commentReference",
|
|
2620
|
+
"lastRenderedPageBreak"
|
|
2621
|
+
]);
|
|
2622
|
+
function unsupportedElement(element, partName) {
|
|
2623
|
+
return new DocxImportError(
|
|
2624
|
+
"unsupported_feature",
|
|
2625
|
+
`Unsupported content element: ${element.name}`,
|
|
2626
|
+
partName
|
|
2627
|
+
);
|
|
2628
|
+
}
|
|
2629
|
+
function relationshipSourcePart(relationshipPart) {
|
|
2630
|
+
if (relationshipPart === "_rels/.rels") return "";
|
|
2631
|
+
const match = /^(.*\/)?_rels\/([^/]+)\.rels$/.exec(relationshipPart);
|
|
2632
|
+
if (!match)
|
|
2633
|
+
throw new DocxImportError(
|
|
2634
|
+
"invalid_package",
|
|
2635
|
+
"Invalid relationships part name",
|
|
2636
|
+
relationshipPart
|
|
2637
|
+
);
|
|
2638
|
+
return `${match[1] ?? ""}${match[2] ?? ""}`;
|
|
2639
|
+
}
|
|
2640
|
+
function resolvePartTarget(sourcePart, target, relationshipsPart) {
|
|
2641
|
+
if (/[\u0000-\u001f\u007f]/.test(target) || target.includes("\\") || target.startsWith("//") || /%(?:2e|2f|5c)/i.test(target)) {
|
|
2642
|
+
throw new DocxImportError("invalid_package", "Unsafe relationship target", relationshipsPart);
|
|
2643
|
+
}
|
|
2644
|
+
const packageAbsolute = target.startsWith("/");
|
|
2645
|
+
const relativeTarget = packageAbsolute ? target.slice(1) : target;
|
|
2646
|
+
const base = !packageAbsolute && sourcePart.includes("/") ? sourcePart.slice(0, sourcePart.lastIndexOf("/") + 1) : "";
|
|
2647
|
+
const combined = `${base}${relativeTarget}`;
|
|
2648
|
+
if (combined.includes("//"))
|
|
2649
|
+
throw new DocxImportError(
|
|
2650
|
+
"invalid_package",
|
|
2651
|
+
"Relationship target contains an empty path segment",
|
|
2652
|
+
relationshipsPart
|
|
2653
|
+
);
|
|
2654
|
+
let segmentCount = 1;
|
|
2655
|
+
for (let index = 0; index < combined.length; index += 1) {
|
|
2656
|
+
if (combined.charCodeAt(index) === 47) {
|
|
2657
|
+
segmentCount += 1;
|
|
2658
|
+
if (segmentCount > 256)
|
|
2659
|
+
throw limitError(
|
|
2660
|
+
"Relationship target exceeds its path-segment safety cap",
|
|
2661
|
+
relationshipsPart
|
|
2662
|
+
);
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
const output = [];
|
|
2666
|
+
for (const segment of combined.split("/")) {
|
|
2667
|
+
if (segment === "")
|
|
2668
|
+
throw new DocxImportError(
|
|
2669
|
+
"invalid_package",
|
|
2670
|
+
"Relationship target contains an empty path segment",
|
|
2671
|
+
relationshipsPart
|
|
2672
|
+
);
|
|
2673
|
+
if (segment === ".") continue;
|
|
2674
|
+
if (segment === "..") {
|
|
2675
|
+
if (output.length === 0)
|
|
2676
|
+
throw new DocxImportError(
|
|
2677
|
+
"invalid_package",
|
|
2678
|
+
"Relationship target escapes the package root",
|
|
2679
|
+
relationshipsPart
|
|
2680
|
+
);
|
|
2681
|
+
output.pop();
|
|
2682
|
+
} else output.push(segment);
|
|
2683
|
+
}
|
|
2684
|
+
return output.join("/");
|
|
2685
|
+
}
|
|
2686
|
+
function consumeBlock(budget) {
|
|
2687
|
+
budget.blocks += 1;
|
|
2688
|
+
if (budget.blocks > budget.limits.maxBlocks) throw limitError("DOCX exceeds maxBlocks");
|
|
2689
|
+
}
|
|
2690
|
+
function consumeText(budget, characters) {
|
|
2691
|
+
budget.textCharacters += characters;
|
|
2692
|
+
if (budget.textCharacters > budget.limits.maxTextCharacters)
|
|
2693
|
+
throw limitError("DOCX exceeds maxTextCharacters");
|
|
2694
|
+
}
|
|
2695
|
+
function elementChildren(element) {
|
|
2696
|
+
return element?.children.filter((child) => child.type === "element") ?? [];
|
|
2697
|
+
}
|
|
2698
|
+
function children(element, name) {
|
|
2699
|
+
return elementChildren(element).filter((child) => child.localName === name);
|
|
2700
|
+
}
|
|
2701
|
+
function firstChild(element, name) {
|
|
2702
|
+
return elementChildren(element).find((child) => child.localName === name);
|
|
2703
|
+
}
|
|
2704
|
+
function singletonChild(element, name, label) {
|
|
2705
|
+
let match;
|
|
2706
|
+
for (const child of elementChildren(element)) {
|
|
2707
|
+
if (child.localName !== name) continue;
|
|
2708
|
+
if (match) throw new DocxImportError("invalid_package", `${label} repeats ${name}`);
|
|
2709
|
+
match = child;
|
|
2710
|
+
}
|
|
2711
|
+
return match;
|
|
2712
|
+
}
|
|
2713
|
+
function assertUniqueChildNames(element, label) {
|
|
2714
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2715
|
+
for (const child of elementChildren(element)) {
|
|
2716
|
+
if (seen.has(child.localName)) {
|
|
2717
|
+
throw new DocxImportError("invalid_package", `${label} repeats ${child.localName}`);
|
|
2718
|
+
}
|
|
2719
|
+
seen.add(child.localName);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
function lastDescendant(element, name) {
|
|
2723
|
+
let match;
|
|
2724
|
+
const stack = [element];
|
|
2725
|
+
while (stack.length > 0) {
|
|
2726
|
+
const current = stack.pop();
|
|
2727
|
+
if (!current) continue;
|
|
2728
|
+
if (current.localName === name) match = current;
|
|
2729
|
+
const nested = elementChildren(current);
|
|
2730
|
+
for (let index = nested.length - 1; index >= 0; index -= 1) {
|
|
2731
|
+
const child = nested[index];
|
|
2732
|
+
if (child) stack.push(child);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
return match;
|
|
2736
|
+
}
|
|
2737
|
+
function onlyElement(element, name, partName) {
|
|
2738
|
+
const matches = children(element, name);
|
|
2739
|
+
if (matches.length !== 1)
|
|
2740
|
+
throw new DocxImportError("invalid_package", `Expected exactly one ${name} element`, partName);
|
|
2741
|
+
return matches[0];
|
|
2742
|
+
}
|
|
2743
|
+
function attribute(element, name) {
|
|
2744
|
+
return prefixedAttribute(element, "w", name);
|
|
2745
|
+
}
|
|
2746
|
+
function requiredAttribute(element, name, partName) {
|
|
2747
|
+
const value = attribute(element, name);
|
|
2748
|
+
if (value === void 0 || value === "")
|
|
2749
|
+
throw new DocxImportError("invalid_package", `Missing required ${name} attribute`, partName);
|
|
2750
|
+
return value;
|
|
2751
|
+
}
|
|
2752
|
+
function prefixedAttribute(element, prefix, name) {
|
|
2753
|
+
return element?.attributes.get(`${prefix}:${name}`);
|
|
2754
|
+
}
|
|
2755
|
+
function requiredPrefixedAttribute(element, prefix, name, partName) {
|
|
2756
|
+
const value = prefixedAttribute(element, prefix, name);
|
|
2757
|
+
if (value === void 0 || value === "") {
|
|
2758
|
+
throw new DocxImportError(
|
|
2759
|
+
"invalid_package",
|
|
2760
|
+
`Missing required ${prefix}:${name} attribute`,
|
|
2761
|
+
partName
|
|
2762
|
+
);
|
|
2763
|
+
}
|
|
2764
|
+
return value;
|
|
2765
|
+
}
|
|
2766
|
+
function exactAttribute(element, name) {
|
|
2767
|
+
return element?.attributes.get(name);
|
|
2768
|
+
}
|
|
2769
|
+
function requiredExactAttribute(element, name, partName) {
|
|
2770
|
+
const value = exactAttribute(element, name);
|
|
2771
|
+
if (value === void 0 || value === "")
|
|
2772
|
+
throw new DocxImportError("invalid_package", `Missing required ${name} attribute`, partName);
|
|
2773
|
+
return value;
|
|
2774
|
+
}
|
|
2775
|
+
function assertExactAttributes(element, allowed, partName) {
|
|
2776
|
+
for (const name of element.attributes.keys()) {
|
|
2777
|
+
if (name === "xmlns" || name.startsWith("xmlns:")) continue;
|
|
2778
|
+
if (!allowed.has(name))
|
|
2779
|
+
throw new DocxImportError("invalid_package", `Unexpected attribute: ${name}`, partName);
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
function requireLeafElement(element, partName) {
|
|
2783
|
+
for (const child of element.children) {
|
|
2784
|
+
if (child.type === "element" || !isOnlyXmlWhitespace(child.value)) {
|
|
2785
|
+
throw new DocxImportError(
|
|
2786
|
+
"invalid_package",
|
|
2787
|
+
"Package declaration element must be empty",
|
|
2788
|
+
partName
|
|
2789
|
+
);
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
function textContent(element) {
|
|
2794
|
+
let output = "";
|
|
2795
|
+
const stack = [...element.children].reverse();
|
|
2796
|
+
while (stack.length > 0) {
|
|
2797
|
+
const node = stack.pop();
|
|
2798
|
+
if (!node) break;
|
|
2799
|
+
if (node.type === "text") output += node.value;
|
|
2800
|
+
else {
|
|
2801
|
+
for (let index = node.children.length - 1; index >= 0; index -= 1) {
|
|
2802
|
+
const child = node.children[index];
|
|
2803
|
+
if (child) stack.push(child);
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
return output;
|
|
2808
|
+
}
|
|
2809
|
+
function requireLocalName(element, expected, partName) {
|
|
2810
|
+
if (element.localName !== expected)
|
|
2811
|
+
throw new DocxImportError("invalid_package", `Expected ${expected} root element`, partName);
|
|
2812
|
+
}
|
|
2813
|
+
function requireDefaultNamespace(element, expected, partName) {
|
|
2814
|
+
if (element.attributes.get("xmlns") !== expected) {
|
|
2815
|
+
throw new DocxImportError(
|
|
2816
|
+
"invalid_package",
|
|
2817
|
+
"Unexpected or missing XML default namespace",
|
|
2818
|
+
partName
|
|
2819
|
+
);
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
function requireDefaultNamespaceTree(root, expected, partName) {
|
|
2823
|
+
requireDefaultNamespace(root, expected, partName);
|
|
2824
|
+
const stack = [root];
|
|
2825
|
+
while (stack.length > 0) {
|
|
2826
|
+
const element = stack.pop();
|
|
2827
|
+
if (!element) break;
|
|
2828
|
+
if (element.name.includes(":"))
|
|
2829
|
+
throw new DocxImportError(
|
|
2830
|
+
"invalid_package",
|
|
2831
|
+
`Prefixed element is forbidden in this package part: ${element.name}`,
|
|
2832
|
+
partName
|
|
2833
|
+
);
|
|
2834
|
+
const rebound = element.attributes.get("xmlns");
|
|
2835
|
+
if (rebound !== void 0 && rebound !== expected)
|
|
2836
|
+
throw new DocxImportError("invalid_package", "Default XML namespace is rebound", partName);
|
|
2837
|
+
for (const child of elementChildren(element)) stack.push(child);
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
function requirePrefixedNamespace(element, prefix, expected, partName) {
|
|
2841
|
+
if (element.attributes.get(`xmlns:${prefix}`) !== expected) {
|
|
2842
|
+
throw new DocxImportError(
|
|
2843
|
+
"unsupported_feature",
|
|
2844
|
+
`Unsupported ${prefix} XML namespace`,
|
|
2845
|
+
partName
|
|
2846
|
+
);
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
function requireWordNamespace(root, partName) {
|
|
2850
|
+
requirePrefixedNamespaceTree(root, "w", WORDPROCESSINGML_NAMESPACE, partName);
|
|
2851
|
+
}
|
|
2852
|
+
function requirePrefixedNamespaceTree(root, prefix, expected, partName) {
|
|
2853
|
+
requirePrefixedNamespace(root, prefix, expected, partName);
|
|
2854
|
+
validateKnownNamespaceBindings(root, partName);
|
|
2855
|
+
const stack = [root];
|
|
2856
|
+
while (stack.length > 0) {
|
|
2857
|
+
const element = stack.pop();
|
|
2858
|
+
if (!element) break;
|
|
2859
|
+
if (!element.name.startsWith(`${prefix}:`)) {
|
|
2860
|
+
throw new DocxImportError(
|
|
2861
|
+
"unsupported_feature",
|
|
2862
|
+
`Unsupported namespaced Word content: ${element.name}`,
|
|
2863
|
+
partName
|
|
2864
|
+
);
|
|
2865
|
+
}
|
|
2866
|
+
const rebound = element.attributes.get(`xmlns:${prefix}`);
|
|
2867
|
+
if (rebound !== void 0 && rebound !== expected) {
|
|
2868
|
+
throw new DocxImportError("invalid_package", `${prefix} XML namespace is rebound`, partName);
|
|
2869
|
+
}
|
|
2870
|
+
for (const child of elementChildren(element)) stack.push(child);
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
var KNOWN_XML_NAMESPACES = /* @__PURE__ */ new Map([
|
|
2874
|
+
["xml", "http://www.w3.org/XML/1998/namespace"],
|
|
2875
|
+
["w", WORDPROCESSINGML_NAMESPACE],
|
|
2876
|
+
["r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships"],
|
|
2877
|
+
["w14", "http://schemas.microsoft.com/office/word/2010/wordml"],
|
|
2878
|
+
["w15", COMMENTS_EXTENDED_NAMESPACE]
|
|
2879
|
+
]);
|
|
2880
|
+
function validateNamespaceWellFormedness(root, partName) {
|
|
2881
|
+
const xmlNamespace = KNOWN_XML_NAMESPACES.get("xml");
|
|
2882
|
+
const bindings = /* @__PURE__ */ new Map([["xml", xmlNamespace]]);
|
|
2883
|
+
const visit = (element) => {
|
|
2884
|
+
const changes = [];
|
|
2885
|
+
const defaultNamespace = element.attributes.get("xmlns");
|
|
2886
|
+
if (defaultNamespace === xmlNamespace || defaultNamespace === "http://www.w3.org/2000/xmlns/") {
|
|
2887
|
+
throw new DocxImportError(
|
|
2888
|
+
"invalid_xml",
|
|
2889
|
+
"Default namespace uses a reserved XML namespace",
|
|
2890
|
+
partName
|
|
2891
|
+
);
|
|
2892
|
+
}
|
|
2893
|
+
for (const [name, value] of element.attributes) {
|
|
2894
|
+
if (!name.startsWith("xmlns:")) continue;
|
|
2895
|
+
const prefix = name.slice(6);
|
|
2896
|
+
if (prefix === "xmlns" || value.length === 0 || value === "http://www.w3.org/2000/xmlns/") {
|
|
2897
|
+
throw new DocxImportError("invalid_xml", "Invalid XML namespace declaration", partName);
|
|
2898
|
+
}
|
|
2899
|
+
if (prefix === "xml" !== (value === xmlNamespace)) {
|
|
2900
|
+
throw new DocxImportError("invalid_xml", "The xml namespace is reserved", partName);
|
|
2901
|
+
}
|
|
2902
|
+
const previous = bindings.get(prefix);
|
|
2903
|
+
changes.push(stripUndefined({ prefix, previous }));
|
|
2904
|
+
bindings.set(prefix, value);
|
|
2905
|
+
}
|
|
2906
|
+
const elementSeparator = element.name.indexOf(":");
|
|
2907
|
+
if (elementSeparator >= 0 && !bindings.has(element.name.slice(0, elementSeparator))) {
|
|
2908
|
+
throw new DocxImportError(
|
|
2909
|
+
"invalid_xml",
|
|
2910
|
+
"Element uses an undeclared namespace prefix",
|
|
2911
|
+
partName
|
|
2912
|
+
);
|
|
2913
|
+
}
|
|
2914
|
+
for (const name of element.attributes.keys()) {
|
|
2915
|
+
if (name === "xmlns" || name.startsWith("xmlns:")) continue;
|
|
2916
|
+
const separator = name.indexOf(":");
|
|
2917
|
+
if (separator >= 0 && !bindings.has(name.slice(0, separator))) {
|
|
2918
|
+
throw new DocxImportError(
|
|
2919
|
+
"invalid_xml",
|
|
2920
|
+
"Attribute uses an undeclared namespace prefix",
|
|
2921
|
+
partName
|
|
2922
|
+
);
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
for (const child of elementChildren(element)) visit(child);
|
|
2926
|
+
for (let index = changes.length - 1; index >= 0; index -= 1) {
|
|
2927
|
+
const change = changes[index];
|
|
2928
|
+
if (!change) continue;
|
|
2929
|
+
if (change.previous === void 0) bindings.delete(change.prefix);
|
|
2930
|
+
else bindings.set(change.prefix, change.previous);
|
|
2931
|
+
}
|
|
2932
|
+
};
|
|
2933
|
+
visit(root);
|
|
2934
|
+
}
|
|
2935
|
+
function validateKnownNamespaceBindings(root, partName) {
|
|
2936
|
+
const bindings = /* @__PURE__ */ new Map([["xml", KNOWN_XML_NAMESPACES.get("xml")]]);
|
|
2937
|
+
const visit = (element) => {
|
|
2938
|
+
const changes = [];
|
|
2939
|
+
for (const [name, value] of element.attributes) {
|
|
2940
|
+
if (!name.startsWith("xmlns:")) continue;
|
|
2941
|
+
const declaredPrefix = name.slice(6);
|
|
2942
|
+
const previous = bindings.get(declaredPrefix);
|
|
2943
|
+
changes.push(stripUndefined({ prefix: declaredPrefix, previous }));
|
|
2944
|
+
const expected = KNOWN_XML_NAMESPACES.get(declaredPrefix);
|
|
2945
|
+
if (expected !== void 0 && value !== expected) {
|
|
2946
|
+
throw new DocxImportError(
|
|
2947
|
+
"invalid_package",
|
|
2948
|
+
`Known namespace ${declaredPrefix} is rebound`,
|
|
2949
|
+
partName
|
|
2950
|
+
);
|
|
2951
|
+
}
|
|
2952
|
+
bindings.set(declaredPrefix, value);
|
|
2953
|
+
}
|
|
2954
|
+
for (const name of element.attributes.keys()) {
|
|
2955
|
+
if (name === "xmlns" || name.startsWith("xmlns:")) continue;
|
|
2956
|
+
const separator = name.indexOf(":");
|
|
2957
|
+
if (separator < 0) continue;
|
|
2958
|
+
const attributePrefix = name.slice(0, separator);
|
|
2959
|
+
const expected = KNOWN_XML_NAMESPACES.get(attributePrefix);
|
|
2960
|
+
if (!bindings.has(attributePrefix)) {
|
|
2961
|
+
throw new DocxImportError(
|
|
2962
|
+
"invalid_package",
|
|
2963
|
+
`Attribute uses an unbound ${attributePrefix} namespace`,
|
|
2964
|
+
partName
|
|
2965
|
+
);
|
|
2966
|
+
}
|
|
2967
|
+
if (expected !== void 0 && bindings.get(attributePrefix) !== expected) {
|
|
2968
|
+
throw new DocxImportError(
|
|
2969
|
+
"invalid_package",
|
|
2970
|
+
`Attribute uses an unbound ${attributePrefix} namespace`,
|
|
2971
|
+
partName
|
|
2972
|
+
);
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
for (const child of elementChildren(element)) visit(child);
|
|
2976
|
+
for (let index = changes.length - 1; index >= 0; index -= 1) {
|
|
2977
|
+
const change = changes[index];
|
|
2978
|
+
if (!change) continue;
|
|
2979
|
+
if (change.previous === void 0) bindings.delete(change.prefix);
|
|
2980
|
+
else bindings.set(change.prefix, change.previous);
|
|
2981
|
+
}
|
|
2982
|
+
};
|
|
2983
|
+
visit(root);
|
|
2984
|
+
}
|
|
2985
|
+
function valueOf(element) {
|
|
2986
|
+
return attribute(element, "val");
|
|
2987
|
+
}
|
|
2988
|
+
function numberValue(element, transform = (value) => value) {
|
|
2989
|
+
const value = valueOf(element);
|
|
2990
|
+
if (value === void 0) return void 0;
|
|
2991
|
+
const number = Number(value);
|
|
2992
|
+
if (!Number.isFinite(number))
|
|
2993
|
+
throw new DocxImportError("invalid_package", "OOXML numeric value is invalid");
|
|
2994
|
+
const transformed = transform(number);
|
|
2995
|
+
if (!Number.isFinite(transformed))
|
|
2996
|
+
throw new DocxImportError("invalid_package", "OOXML numeric value is out of range");
|
|
2997
|
+
return transformed;
|
|
2998
|
+
}
|
|
2999
|
+
function integerValue(element, fallback) {
|
|
3000
|
+
if (valueOf(element) === void 0) return fallback;
|
|
3001
|
+
const value = numberValue(element);
|
|
3002
|
+
if (value === void 0 || !Number.isInteger(value)) {
|
|
3003
|
+
throw new DocxImportError("invalid_package", "OOXML integer value is invalid");
|
|
3004
|
+
}
|
|
3005
|
+
return value;
|
|
3006
|
+
}
|
|
3007
|
+
function numberAttribute(element, name, transform = (value) => value) {
|
|
3008
|
+
const raw = attribute(element, name);
|
|
3009
|
+
if (raw === void 0) return void 0;
|
|
3010
|
+
const value = Number(raw);
|
|
3011
|
+
if (!Number.isFinite(value))
|
|
3012
|
+
throw new DocxImportError("invalid_package", "OOXML numeric attribute is invalid");
|
|
3013
|
+
const transformed = transform(value);
|
|
3014
|
+
if (!Number.isFinite(transformed))
|
|
3015
|
+
throw new DocxImportError("invalid_package", "OOXML numeric attribute is out of range");
|
|
3016
|
+
return transformed;
|
|
3017
|
+
}
|
|
3018
|
+
function booleanAttribute(element, name, fallback) {
|
|
3019
|
+
const value = attribute(element, name);
|
|
3020
|
+
return value === void 0 ? fallback : parseBooleanLexical(value);
|
|
3021
|
+
}
|
|
3022
|
+
function optionalBooleanAttribute(element, name) {
|
|
3023
|
+
const value = attribute(element, name);
|
|
3024
|
+
return value === void 0 ? void 0 : parseBooleanLexical(value);
|
|
3025
|
+
}
|
|
3026
|
+
function optionalBoolean(element) {
|
|
3027
|
+
if (!element) return void 0;
|
|
3028
|
+
return booleanValue(element, true);
|
|
3029
|
+
}
|
|
3030
|
+
function booleanValue(element, fallback) {
|
|
3031
|
+
if (!element) return fallback;
|
|
3032
|
+
const value = valueOf(element);
|
|
3033
|
+
return value === void 0 ? true : parseBooleanLexical(value);
|
|
3034
|
+
}
|
|
3035
|
+
var FALSE_VALUES = /* @__PURE__ */ new Set(["0", "false", "off", "no"]);
|
|
3036
|
+
var TRUE_VALUES = /* @__PURE__ */ new Set(["1", "true", "on", "yes"]);
|
|
3037
|
+
function parseBooleanLexical(value) {
|
|
3038
|
+
const normalized = value.toLowerCase();
|
|
3039
|
+
if (FALSE_VALUES.has(normalized)) return false;
|
|
3040
|
+
if (TRUE_VALUES.has(normalized)) return true;
|
|
3041
|
+
throw new DocxImportError("invalid_package", "OOXML boolean value is invalid");
|
|
3042
|
+
}
|
|
3043
|
+
function localName(name) {
|
|
3044
|
+
return name.slice(name.lastIndexOf(":") + 1);
|
|
3045
|
+
}
|
|
3046
|
+
function twipsToPt(value) {
|
|
3047
|
+
return value / 20;
|
|
3048
|
+
}
|
|
3049
|
+
function stripUndefined(value) {
|
|
3050
|
+
const record = value;
|
|
3051
|
+
for (const key of Object.keys(record)) if (record[key] === void 0) delete record[key];
|
|
3052
|
+
return value;
|
|
3053
|
+
}
|
|
3054
|
+
function decodeUtf8(bytes, description, partName) {
|
|
3055
|
+
try {
|
|
3056
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
3057
|
+
} catch {
|
|
3058
|
+
throw new DocxImportError(
|
|
3059
|
+
partName ? "invalid_xml" : "invalid_zip",
|
|
3060
|
+
`${description} is not valid UTF-8`,
|
|
3061
|
+
partName
|
|
3062
|
+
);
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
function limitError(message, partName) {
|
|
3066
|
+
return new DocxImportError("limit_exceeded", message, partName);
|
|
3067
|
+
}
|
|
3068
|
+
function compareStrings(left, right) {
|
|
3069
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
3070
|
+
}
|
|
3071
|
+
function compareOpaqueRelationships(left, right) {
|
|
3072
|
+
return compareStrings(left.sourcePart, right.sourcePart) || compareStrings(left.type, right.type) || compareStrings(left.targetPart, right.targetPart);
|
|
3073
|
+
}
|
|
3074
|
+
function compareOpaqueContentTypes(left, right) {
|
|
3075
|
+
return compareStrings(left.partName, right.partName) || compareStrings(left.contentType, right.contentType);
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
// src/document-docx-codec.ts
|
|
3079
|
+
var importedStyleCache = /* @__PURE__ */ new WeakMap();
|
|
3080
|
+
async function exportDocx(document, options = {}) {
|
|
3081
|
+
validateExportOptions(options);
|
|
3082
|
+
document.toJSON();
|
|
3083
|
+
const envelope = DOCUMENT_LOSS_PRESERVATION.get(document);
|
|
3084
|
+
if (envelope && await documentModelDigest(document) === envelope.modelDigest) {
|
|
3085
|
+
return FileBlob.fromBytes(envelope.sourceBytes.slice(), {
|
|
3086
|
+
type: DOCX_MEDIA_TYPE,
|
|
3087
|
+
...options.fileName ? { name: options.fileName } : {}
|
|
3088
|
+
});
|
|
3089
|
+
}
|
|
3090
|
+
const blockingIssues = documentFidelityReport(document).filter(
|
|
3091
|
+
(issue) => issue.severity === "error"
|
|
3092
|
+
);
|
|
3093
|
+
if (blockingIssues.length > 0 && options.unsupportedContent !== "discard") {
|
|
3094
|
+
throw new DocumentFidelityError(
|
|
3095
|
+
'DOCX export would discard opaque OOXML; export again with unsupportedContent: "discard" only after explicit user acknowledgement',
|
|
3096
|
+
blockingIssues
|
|
3097
|
+
);
|
|
3098
|
+
}
|
|
3099
|
+
const docx = await import("docx");
|
|
3100
|
+
const context = buildDocxContext(document, docx);
|
|
3101
|
+
const evenAndOddHeaderAndFooters = document.evenAndOddHeaders;
|
|
3102
|
+
const sections = document.sections.items.map((section, index) => {
|
|
3103
|
+
const end = document.sections.items[index + 1]?.startBlockIndex ?? document.blocks.items.length;
|
|
3104
|
+
const blocks = document.blocks.items.slice(section.startBlockIndex, end);
|
|
3105
|
+
const landscape = section.page.widthPt > section.page.heightPt;
|
|
3106
|
+
return {
|
|
3107
|
+
properties: {
|
|
3108
|
+
page: {
|
|
3109
|
+
size: {
|
|
3110
|
+
width: ptToTwip(landscape ? section.page.heightPt : section.page.widthPt),
|
|
3111
|
+
height: ptToTwip(landscape ? section.page.widthPt : section.page.heightPt),
|
|
3112
|
+
orientation: landscape ? docx.PageOrientation.LANDSCAPE : docx.PageOrientation.PORTRAIT
|
|
3113
|
+
},
|
|
3114
|
+
margin: {
|
|
3115
|
+
top: ptToTwip(section.page.marginTopPt),
|
|
3116
|
+
right: ptToTwip(section.page.marginRightPt),
|
|
3117
|
+
bottom: ptToTwip(section.page.marginBottomPt),
|
|
3118
|
+
left: ptToTwip(section.page.marginLeftPt),
|
|
3119
|
+
header: ptToTwip(section.page.headerPt ?? 36),
|
|
3120
|
+
footer: ptToTwip(section.page.footerPt ?? 36),
|
|
3121
|
+
gutter: ptToTwip(section.page.gutterPt ?? 0)
|
|
3122
|
+
}
|
|
3123
|
+
},
|
|
3124
|
+
titlePage: section.titlePage
|
|
3125
|
+
},
|
|
3126
|
+
headers: buildDocxStories(section.headers, section.page, docx, context),
|
|
3127
|
+
footers: buildDocxStories(section.footers, section.page, docx, context),
|
|
3128
|
+
children: blocks.map((block) => toDocxBlock(block, section.page, docx, context))
|
|
3129
|
+
};
|
|
3130
|
+
});
|
|
3131
|
+
const file = new docx.Document({
|
|
3132
|
+
sections,
|
|
3133
|
+
numbering: { config: numberingConfig(docx) },
|
|
3134
|
+
comments: { children: context.commentDefinitions },
|
|
3135
|
+
features: { trackRevisions: document.trackRevisions },
|
|
3136
|
+
evenAndOddHeaderAndFooters,
|
|
3137
|
+
creator: "OpenGeni",
|
|
3138
|
+
lastModifiedBy: "OpenGeni",
|
|
3139
|
+
revision: document.revision
|
|
3140
|
+
});
|
|
3141
|
+
const bytes = await packDocxWithBoundedCompression(file, docx.Packer, [
|
|
3142
|
+
{
|
|
3143
|
+
path: "docProps/core.xml",
|
|
3144
|
+
data: deterministicCoreProperties(document.revision)
|
|
3145
|
+
}
|
|
3146
|
+
]);
|
|
3147
|
+
return FileBlob.fromBytes(bytes, {
|
|
3148
|
+
type: DOCX_MEDIA_TYPE,
|
|
3149
|
+
...options.fileName ? { name: options.fileName } : {}
|
|
3150
|
+
});
|
|
3151
|
+
}
|
|
3152
|
+
async function importDocx2(input, options = {}) {
|
|
3153
|
+
validateImportOptions(options);
|
|
3154
|
+
const sourceBytes = await ownedDocxBytes(input);
|
|
3155
|
+
const imported = await importDocx(ownedArrayBuffer(sourceBytes), options.limits);
|
|
3156
|
+
if (hasOpaqueContent(imported.opaqueContent) && options.unsupportedContent === "error") {
|
|
3157
|
+
throw new DocumentFidelityError(
|
|
3158
|
+
"The DOCX contains bounded inert OOXML outside the editable document model",
|
|
3159
|
+
[opaqueContentIssue(imported.opaqueContent, "error")]
|
|
3160
|
+
);
|
|
3161
|
+
}
|
|
3162
|
+
const document = documentFromImportedDocx(imported, await semanticNamespace(imported));
|
|
3163
|
+
if (hasOpaqueContent(imported.opaqueContent)) {
|
|
3164
|
+
DOCUMENT_LOSS_PRESERVATION.set(document, {
|
|
3165
|
+
sourceBytes,
|
|
3166
|
+
sourceDigest: await sha256Hex(sourceBytes),
|
|
3167
|
+
importedRevision: document.revision,
|
|
3168
|
+
opaqueContent: cloneOpaqueContent(imported.opaqueContent),
|
|
3169
|
+
modelDigest: await documentModelDigest(document)
|
|
3170
|
+
});
|
|
3171
|
+
}
|
|
3172
|
+
return document;
|
|
3173
|
+
}
|
|
3174
|
+
function fidelityReport(document) {
|
|
3175
|
+
document.toJSON();
|
|
3176
|
+
return documentFidelityReport(document);
|
|
3177
|
+
}
|
|
3178
|
+
function lossPreservationEnvelope(document) {
|
|
3179
|
+
document.toJSON();
|
|
3180
|
+
return documentLossPreservationEnvelope(document);
|
|
3181
|
+
}
|
|
3182
|
+
async function attachLossPreservationEnvelope(document, envelope, options = {}) {
|
|
3183
|
+
document.toJSON();
|
|
3184
|
+
validateAttachOptions(options);
|
|
3185
|
+
const normalized = normalizeLossPreservationEnvelope(envelope);
|
|
3186
|
+
if (normalized.sourceDigest !== await sha256Hex(normalized.sourceBytes)) {
|
|
3187
|
+
throw new DocxImportError(
|
|
3188
|
+
"invalid_package",
|
|
3189
|
+
"DOCX loss-preservation source digest does not match its bytes"
|
|
3190
|
+
);
|
|
3191
|
+
}
|
|
3192
|
+
const imported = await importDocx(ownedArrayBuffer(normalized.sourceBytes), options.limits);
|
|
3193
|
+
if (!sameOpaqueContent(imported.opaqueContent, normalized.opaqueContent)) {
|
|
3194
|
+
throw new DocxImportError(
|
|
3195
|
+
"invalid_package",
|
|
3196
|
+
"DOCX loss-preservation metadata does not match its source package"
|
|
3197
|
+
);
|
|
3198
|
+
}
|
|
3199
|
+
const projected = documentFromImportedDocx(imported, await semanticNamespace(imported));
|
|
3200
|
+
const projectedDigest = await documentModelDigest(projected);
|
|
3201
|
+
const documentDigest = await documentModelDigest(document);
|
|
3202
|
+
if (projectedDigest !== normalized.modelDigest || documentDigest !== normalized.modelDigest) {
|
|
3203
|
+
throw new DocxImportError(
|
|
3204
|
+
"invalid_package",
|
|
3205
|
+
"DOCX loss-preservation envelope belongs to a different document snapshot"
|
|
3206
|
+
);
|
|
3207
|
+
}
|
|
3208
|
+
DOCUMENT_LOSS_PRESERVATION.set(document, {
|
|
3209
|
+
sourceBytes: normalized.sourceBytes,
|
|
3210
|
+
sourceDigest: normalized.sourceDigest,
|
|
3211
|
+
importedRevision: document.revision,
|
|
3212
|
+
opaqueContent: normalized.opaqueContent,
|
|
3213
|
+
modelDigest: normalized.modelDigest
|
|
3214
|
+
});
|
|
3215
|
+
}
|
|
3216
|
+
function documentFromImportedDocx(imported, idNamespace) {
|
|
3217
|
+
if (imported.schemaVersion !== 1 || imported.format !== "docx") {
|
|
3218
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3219
|
+
"document",
|
|
3220
|
+
"unknown imported DOCX schema",
|
|
3221
|
+
"DOCX import"
|
|
3222
|
+
);
|
|
3223
|
+
}
|
|
3224
|
+
const importedSections = [...imported.sections].sort(
|
|
3225
|
+
(left, right) => left.startBlockIndex - right.startBlockIndex
|
|
3226
|
+
);
|
|
3227
|
+
validateImportedSections(imported, importedSections);
|
|
3228
|
+
const firstSection = importedSections[0];
|
|
3229
|
+
if (!firstSection || firstSection.startBlockIndex !== 0)
|
|
3230
|
+
throw new Error("Imported DOCX has no section starting at body block 0");
|
|
3231
|
+
const document = Document.create({
|
|
3232
|
+
page: importedPage(firstSection),
|
|
3233
|
+
idNamespace,
|
|
3234
|
+
titlePage: firstSection.titlePage,
|
|
3235
|
+
evenAndOddHeaders: imported.evenAndOddHeaders,
|
|
3236
|
+
trackRevisions: imported.trackRevisions
|
|
3237
|
+
});
|
|
3238
|
+
const paragraphMap = /* @__PURE__ */ new Map();
|
|
3239
|
+
const styleMap = new Map(imported.styles.map((style) => [style.styleId, style]));
|
|
3240
|
+
const listKinds = importedListKinds(imported.lists);
|
|
3241
|
+
let sectionIndex = 1;
|
|
3242
|
+
imported.blocks.forEach((block, blockIndex) => {
|
|
3243
|
+
while (importedSections[sectionIndex]?.startBlockIndex === blockIndex) {
|
|
3244
|
+
document.sections.add({
|
|
3245
|
+
page: importedPage(importedSections[sectionIndex]),
|
|
3246
|
+
titlePage: importedSections[sectionIndex].titlePage
|
|
3247
|
+
});
|
|
3248
|
+
sectionIndex += 1;
|
|
3249
|
+
}
|
|
3250
|
+
appendImportedBodyBlock(document, block, styleMap, listKinds, paragraphMap);
|
|
3251
|
+
});
|
|
3252
|
+
while (sectionIndex < importedSections.length) {
|
|
3253
|
+
const section = importedSections[sectionIndex];
|
|
3254
|
+
if (section.startBlockIndex !== imported.blocks.length)
|
|
3255
|
+
throw new Error("Imported DOCX section boundary does not align with body blocks");
|
|
3256
|
+
if (document.sections.items.at(-1)?.startBlockIndex === imported.blocks.length) {
|
|
3257
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3258
|
+
"document",
|
|
3259
|
+
"multiple empty sections at the same boundary",
|
|
3260
|
+
"DOCX import"
|
|
3261
|
+
);
|
|
3262
|
+
}
|
|
3263
|
+
document.sections.add({ page: importedPage(section), titlePage: section.titlePage });
|
|
3264
|
+
sectionIndex += 1;
|
|
3265
|
+
}
|
|
3266
|
+
const headers = new Map(imported.headers.map((story) => [story.partName, story]));
|
|
3267
|
+
const footers = new Map(imported.footers.map((story) => [story.partName, story]));
|
|
3268
|
+
importedSections.forEach((source, index) => {
|
|
3269
|
+
const target = document.sections.items[index];
|
|
3270
|
+
if (!target) throw new Error("Imported DOCX section count changed during conversion");
|
|
3271
|
+
importSectionStories(
|
|
3272
|
+
document,
|
|
3273
|
+
source,
|
|
3274
|
+
target,
|
|
3275
|
+
headers,
|
|
3276
|
+
footers,
|
|
3277
|
+
styleMap,
|
|
3278
|
+
listKinds,
|
|
3279
|
+
paragraphMap
|
|
3280
|
+
);
|
|
3281
|
+
});
|
|
3282
|
+
importComments(document, imported.comments, paragraphMap, styleMap);
|
|
3283
|
+
importTrackedChanges(document, imported, paragraphMap);
|
|
3284
|
+
validateImportedReviewProjection(document);
|
|
3285
|
+
document.toJSON();
|
|
3286
|
+
return document;
|
|
3287
|
+
}
|
|
3288
|
+
function appendImportedBodyBlock(document, block, styles, listKinds, paragraphs) {
|
|
3289
|
+
if (block.kind === "pageBreak") {
|
|
3290
|
+
document.blocks.addPageBreak();
|
|
3291
|
+
return;
|
|
3292
|
+
}
|
|
3293
|
+
if (block.kind === "paragraph") {
|
|
3294
|
+
if (isOnlyPageBreak(block, styles)) {
|
|
3295
|
+
document.blocks.addPageBreak();
|
|
3296
|
+
return;
|
|
3297
|
+
}
|
|
3298
|
+
const projected2 = importedParagraph(block, styles, listKinds);
|
|
3299
|
+
const paragraph = document.blocks.addParagraph(projected2.runs, projected2.style);
|
|
3300
|
+
appendMap(paragraphs, block.id, { source: block, target: paragraph });
|
|
3301
|
+
return;
|
|
3302
|
+
}
|
|
3303
|
+
const projected = importedTable(
|
|
3304
|
+
block,
|
|
3305
|
+
styles,
|
|
3306
|
+
listKinds,
|
|
3307
|
+
document.sections.items.at(-1)?.page ?? document.page
|
|
3308
|
+
);
|
|
3309
|
+
document.blocks.addTable(projected.rows, projected.style);
|
|
3310
|
+
}
|
|
3311
|
+
function appendImportedStoryBlock(story, block, document, styles, listKinds, paragraphs, page) {
|
|
3312
|
+
if (block.kind === "pageBreak" || block.kind === "paragraph" && block.inlines.some((inline) => inline.kind === "pageBreak")) {
|
|
3313
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3314
|
+
"document",
|
|
3315
|
+
"page breaks in headers or footers",
|
|
3316
|
+
"DOCX import"
|
|
3317
|
+
);
|
|
3318
|
+
}
|
|
3319
|
+
if (block.kind === "paragraph") {
|
|
3320
|
+
const projected = importedParagraph(block, styles, listKinds);
|
|
3321
|
+
const paragraph = story.addParagraph(projected.runs, projected.style);
|
|
3322
|
+
appendMap(paragraphs, block.id, { source: block, target: paragraph });
|
|
3323
|
+
} else {
|
|
3324
|
+
const projected = importedTable(block, styles, listKinds, page);
|
|
3325
|
+
story.addTable(projected.rows, projected.style);
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
function importedParagraph(source, styles, listKinds) {
|
|
3329
|
+
if (source.inlines.some((inline) => inline.kind === "pageBreak")) {
|
|
3330
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3331
|
+
"document",
|
|
3332
|
+
"inline page breaks mixed with paragraph text",
|
|
3333
|
+
"DOCX import"
|
|
3334
|
+
);
|
|
3335
|
+
}
|
|
3336
|
+
const inherited = resolveImportedStyle(source.styleId, styles, "paragraph");
|
|
3337
|
+
const paragraphStyle = mapImportedParagraphStyle(
|
|
3338
|
+
{ ...inherited.paragraph, ...source.style },
|
|
3339
|
+
source.styleId,
|
|
3340
|
+
listKinds
|
|
3341
|
+
);
|
|
3342
|
+
const runs = source.inlines.map((inline) => {
|
|
3343
|
+
if (inline.kind !== "run") throw new Error("Unexpected non-run in imported paragraph");
|
|
3344
|
+
const inheritedRun = resolveImportedStyle(inline.styleId, styles, "character").run;
|
|
3345
|
+
return new DocumentTextRun(
|
|
3346
|
+
inline.text,
|
|
3347
|
+
mapImportedRunStyle({ ...inherited.run, ...inheritedRun, ...inline.style })
|
|
3348
|
+
);
|
|
3349
|
+
});
|
|
3350
|
+
return { runs: runs.length > 0 ? runs : "", style: paragraphStyle };
|
|
3351
|
+
}
|
|
3352
|
+
function importedTable(source, styles, listKinds, page) {
|
|
3353
|
+
if (source.rows.length === 0 || source.rows.some((row) => row.cells.length === 0))
|
|
3354
|
+
throw new Error("Imported DOCX table is empty");
|
|
3355
|
+
if (source.styleId) {
|
|
3356
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3357
|
+
"document",
|
|
3358
|
+
`table style ${source.styleId}`,
|
|
3359
|
+
"DOCX import"
|
|
3360
|
+
);
|
|
3361
|
+
}
|
|
3362
|
+
const columnCount = source.rows[0].cells.length;
|
|
3363
|
+
if (source.rows.some((row) => row.cells.length !== columnCount)) {
|
|
3364
|
+
throw new UnsupportedArtifactFeatureError("document", "ragged DOCX tables", "DOCX import");
|
|
3365
|
+
}
|
|
3366
|
+
const headerRows = source.rows.findIndex((row) => !row.header);
|
|
3367
|
+
const normalizedHeaderRows = headerRows < 0 ? source.rows.length : headerRows;
|
|
3368
|
+
if (source.rows.slice(normalizedHeaderRows).some((row) => row.header)) {
|
|
3369
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3370
|
+
"document",
|
|
3371
|
+
"non-contiguous repeating table headers",
|
|
3372
|
+
"DOCX import"
|
|
3373
|
+
);
|
|
3374
|
+
}
|
|
3375
|
+
const cannotSplit = source.rows[0].cannotSplit;
|
|
3376
|
+
if (source.rows.some((row) => row.cannotSplit !== cannotSplit)) {
|
|
3377
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3378
|
+
"document",
|
|
3379
|
+
"mixed table row-splitting policies",
|
|
3380
|
+
"DOCX import"
|
|
3381
|
+
);
|
|
3382
|
+
}
|
|
3383
|
+
if (source.rows.some((row) => row.heightPt !== void 0 || row.heightRule !== void 0)) {
|
|
3384
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3385
|
+
"document",
|
|
3386
|
+
"explicit table row heights",
|
|
3387
|
+
"DOCX import"
|
|
3388
|
+
);
|
|
3389
|
+
}
|
|
3390
|
+
let headerFill;
|
|
3391
|
+
let filledHeaderCells = 0;
|
|
3392
|
+
let unfilledHeaderCells = 0;
|
|
3393
|
+
const rows = source.rows.map(
|
|
3394
|
+
(row, rowIndex) => row.cells.map((cell, columnIndex) => {
|
|
3395
|
+
if (cell.columnSpan !== 1 || cell.verticalMerge !== void 0) {
|
|
3396
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3397
|
+
"document",
|
|
3398
|
+
"merged DOCX table cells",
|
|
3399
|
+
"DOCX import"
|
|
3400
|
+
);
|
|
3401
|
+
}
|
|
3402
|
+
if (cell.verticalAlignment !== "center") {
|
|
3403
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3404
|
+
"document",
|
|
3405
|
+
`table cell vertical alignment ${cell.verticalAlignment ?? "default"}`,
|
|
3406
|
+
"DOCX import"
|
|
3407
|
+
);
|
|
3408
|
+
}
|
|
3409
|
+
if (cell.blocks.length !== 1 || cell.blocks[0]?.kind !== "paragraph") {
|
|
3410
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3411
|
+
"document",
|
|
3412
|
+
"multi-paragraph or nested DOCX table cells",
|
|
3413
|
+
"DOCX import"
|
|
3414
|
+
);
|
|
3415
|
+
}
|
|
3416
|
+
const explicitFill = cell.fill && cell.fill.toLowerCase() !== "auto" ? normalizeHex(cell.fill) : void 0;
|
|
3417
|
+
if (explicitFill) {
|
|
3418
|
+
if (rowIndex >= normalizedHeaderRows) {
|
|
3419
|
+
throw new UnsupportedArtifactFeatureError("document", "body cell shading", "DOCX import");
|
|
3420
|
+
}
|
|
3421
|
+
if (headerFill && headerFill !== explicitFill) {
|
|
3422
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3423
|
+
"document",
|
|
3424
|
+
"mixed table-header fills",
|
|
3425
|
+
"DOCX import"
|
|
3426
|
+
);
|
|
3427
|
+
}
|
|
3428
|
+
headerFill = explicitFill;
|
|
3429
|
+
filledHeaderCells += 1;
|
|
3430
|
+
} else if (rowIndex < normalizedHeaderRows) {
|
|
3431
|
+
unfilledHeaderCells += 1;
|
|
3432
|
+
}
|
|
3433
|
+
const paragraph = cell.blocks[0];
|
|
3434
|
+
if (paragraph.inlines.some((inline) => inline.kind === "pageBreak") || paragraph.commentAnchors.length > 0) {
|
|
3435
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3436
|
+
"document",
|
|
3437
|
+
"page breaks or comments in table cells",
|
|
3438
|
+
"DOCX import"
|
|
3439
|
+
);
|
|
3440
|
+
}
|
|
3441
|
+
const inherited = resolveImportedStyle(paragraph.styleId, styles, "paragraph");
|
|
3442
|
+
assertRepresentableTableCellParagraph(
|
|
3443
|
+
{ ...inherited.paragraph, ...paragraph.style },
|
|
3444
|
+
paragraph.styleId,
|
|
3445
|
+
listKinds
|
|
3446
|
+
);
|
|
3447
|
+
if (cell.width) {
|
|
3448
|
+
const expected = source.gridColumnWidthsPt[columnIndex];
|
|
3449
|
+
if (cell.width.unit !== "pt" || expected === void 0 || Math.abs(cell.width.value - expected) > 0.05) {
|
|
3450
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3451
|
+
"document",
|
|
3452
|
+
"table cell widths inconsistent with the fixed column grid",
|
|
3453
|
+
"DOCX import"
|
|
3454
|
+
);
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
return paragraph.inlines.map((inline) => {
|
|
3458
|
+
if (inline.kind !== "run") throw new Error("Unexpected non-run in imported table cell");
|
|
3459
|
+
if (inline.changeId)
|
|
3460
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3461
|
+
"document",
|
|
3462
|
+
"tracked changes in table cells",
|
|
3463
|
+
"DOCX import"
|
|
3464
|
+
);
|
|
3465
|
+
const runStyle = resolveImportedStyle(inline.styleId, styles, "character").run;
|
|
3466
|
+
return new DocumentTextRun(
|
|
3467
|
+
inline.text,
|
|
3468
|
+
mapImportedRunStyle({ ...inherited.run, ...runStyle, ...inline.style })
|
|
3469
|
+
);
|
|
3470
|
+
});
|
|
3471
|
+
})
|
|
3472
|
+
);
|
|
3473
|
+
if (filledHeaderCells > 0 && unfilledHeaderCells > 0) {
|
|
3474
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3475
|
+
"document",
|
|
3476
|
+
"partially shaded table-header rows",
|
|
3477
|
+
"DOCX import"
|
|
3478
|
+
);
|
|
3479
|
+
}
|
|
3480
|
+
const usable = page.widthPt - page.marginLeftPt - page.marginRightPt;
|
|
3481
|
+
if (source.gridColumnWidthsPt.length > 0 && (source.gridColumnWidthsPt.length !== columnCount || source.gridColumnWidthsPt.some((width) => !Number.isFinite(width) || width <= 0))) {
|
|
3482
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3483
|
+
"document",
|
|
3484
|
+
"invalid or incomplete fixed table column grid",
|
|
3485
|
+
"DOCX import"
|
|
3486
|
+
);
|
|
3487
|
+
}
|
|
3488
|
+
const widths = source.gridColumnWidthsPt.length === columnCount ? source.gridColumnWidthsPt : void 0;
|
|
3489
|
+
const widthPt = source.width?.unit === "pt" ? source.width.value : widths?.reduce((total, width) => total + width, 0) ?? usable;
|
|
3490
|
+
if (source.width && source.width.unit !== "pt" && source.width.unit !== "auto") {
|
|
3491
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3492
|
+
"document",
|
|
3493
|
+
"percentage-width DOCX tables",
|
|
3494
|
+
"DOCX import"
|
|
3495
|
+
);
|
|
3496
|
+
}
|
|
3497
|
+
if (source.width?.unit === "pt" && widths && Math.abs(source.width.value - widths.reduce((total, width) => total + width, 0)) > 0.05) {
|
|
3498
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3499
|
+
"document",
|
|
3500
|
+
"table width inconsistent with its fixed column grid",
|
|
3501
|
+
"DOCX import"
|
|
3502
|
+
);
|
|
3503
|
+
}
|
|
3504
|
+
if (source.alignment && source.alignment !== "left" && source.alignment !== "start") {
|
|
3505
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3506
|
+
"document",
|
|
3507
|
+
`table alignment ${source.alignment}`,
|
|
3508
|
+
"DOCX import"
|
|
3509
|
+
);
|
|
3510
|
+
}
|
|
3511
|
+
if (source.layout !== "fixed") {
|
|
3512
|
+
throw new UnsupportedArtifactFeatureError("document", "autofit DOCX tables", "DOCX import");
|
|
3513
|
+
}
|
|
3514
|
+
const cellPaddingPt = importedUniformCellPadding(source);
|
|
3515
|
+
const borderColor = importedUniformTableBorder(source.borders);
|
|
3516
|
+
if (!source.indent || source.indent.unit !== "pt" || Math.abs(source.indent.value - cellPaddingPt) > 0.01) {
|
|
3517
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3518
|
+
"document",
|
|
3519
|
+
"table indent not equal to its leading cell padding",
|
|
3520
|
+
"DOCX import"
|
|
3521
|
+
);
|
|
3522
|
+
}
|
|
3523
|
+
return {
|
|
3524
|
+
rows,
|
|
3525
|
+
style: {
|
|
3526
|
+
widthPt,
|
|
3527
|
+
...widths ? { columnWidthsPt: [...widths] } : {},
|
|
3528
|
+
headerRows: normalizedHeaderRows,
|
|
3529
|
+
cellPaddingPt,
|
|
3530
|
+
borderColor,
|
|
3531
|
+
...headerFill ? { headerFill } : {},
|
|
3532
|
+
...!cannotSplit ? { allowRowSplit: true } : {}
|
|
3533
|
+
}
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3536
|
+
function importSectionStories(document, source, target, headers, footers, styles, listKinds, paragraphs) {
|
|
3537
|
+
const apply = (references, available, stories) => {
|
|
3538
|
+
for (const reference of references) {
|
|
3539
|
+
const key = reference.kind === "default" ? "default" : reference.kind === "first" ? "first" : reference.kind === "even" ? "even" : void 0;
|
|
3540
|
+
if (!key)
|
|
3541
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3542
|
+
"document",
|
|
3543
|
+
`header/footer variant ${reference.kind}`,
|
|
3544
|
+
"DOCX import"
|
|
3545
|
+
);
|
|
3546
|
+
const importedStory = available.get(reference.partName);
|
|
3547
|
+
if (!importedStory)
|
|
3548
|
+
throw new Error(`Imported DOCX references missing story ${reference.partName}`);
|
|
3549
|
+
const story = stories[key];
|
|
3550
|
+
if (story.items.length > 0)
|
|
3551
|
+
throw new Error(`Imported DOCX repeats ${key} header/footer for one section`);
|
|
3552
|
+
if (isEmptyStoryPlaceholder(importedStory)) continue;
|
|
3553
|
+
for (const block of importedStory.blocks)
|
|
3554
|
+
appendImportedStoryBlock(
|
|
3555
|
+
story,
|
|
3556
|
+
block,
|
|
3557
|
+
document,
|
|
3558
|
+
styles,
|
|
3559
|
+
listKinds,
|
|
3560
|
+
paragraphs,
|
|
3561
|
+
target.page
|
|
3562
|
+
);
|
|
3563
|
+
}
|
|
3564
|
+
};
|
|
3565
|
+
apply(source.headers, headers, target.headers);
|
|
3566
|
+
apply(source.footers, footers, target.footers);
|
|
3567
|
+
}
|
|
3568
|
+
function isEmptyStoryPlaceholder(story) {
|
|
3569
|
+
const block = story.blocks[0];
|
|
3570
|
+
return story.blocks.length === 1 && block?.kind === "paragraph" && block.styleId === void 0 && Object.keys(block.style).length === 0 && block.commentAnchors.length === 0 && block.inlines.every(
|
|
3571
|
+
(inline) => inline.kind === "run" && inline.text.length === 0 && inline.styleId === void 0 && Object.keys(inline.style).length === 0
|
|
3572
|
+
);
|
|
3573
|
+
}
|
|
3574
|
+
function importComments(document, comments, paragraphs, styles) {
|
|
3575
|
+
const byId = new Map(comments.map((comment) => [comment.id, comment]));
|
|
3576
|
+
const roots = [];
|
|
3577
|
+
const repliesByParent = /* @__PURE__ */ new Map();
|
|
3578
|
+
for (const raw of comments) {
|
|
3579
|
+
const comment = raw;
|
|
3580
|
+
if (comment.parentId === void 0) roots.push(comment);
|
|
3581
|
+
else appendMap(repliesByParent, comment.parentId, comment);
|
|
3582
|
+
}
|
|
3583
|
+
const anchorsByComment = /* @__PURE__ */ new Map();
|
|
3584
|
+
for (const projections of paragraphs.values()) {
|
|
3585
|
+
for (const { source, target } of projections) {
|
|
3586
|
+
const anchors = /* @__PURE__ */ new Map();
|
|
3587
|
+
for (const anchor of source.commentAnchors) appendMap(anchors, anchor.commentId, anchor);
|
|
3588
|
+
for (const [commentId, values] of anchors) {
|
|
3589
|
+
const start = values.find((anchor) => anchor.kind === "start")?.textOffset ?? values.find((anchor) => anchor.kind === "reference")?.textOffset;
|
|
3590
|
+
const end = values.find((anchor) => anchor.kind === "end")?.textOffset ?? start;
|
|
3591
|
+
if (start === void 0 || end === void 0) {
|
|
3592
|
+
throw new Error(`Imported comment ${commentId} has an incomplete anchor`);
|
|
3593
|
+
}
|
|
3594
|
+
appendMap(anchorsByComment, commentId, { source, target, start, end });
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
for (const root of roots) {
|
|
3599
|
+
const matches = anchorsByComment.get(root.id) ?? [];
|
|
3600
|
+
const sourceIds = new Set(matches.map((match) => match.source.id));
|
|
3601
|
+
if (matches.length === 0 || sourceIds.size !== 1)
|
|
3602
|
+
throw new Error(`Imported comment ${root.id} must have exactly one source text anchor`);
|
|
3603
|
+
if (matches.length > 1) {
|
|
3604
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3605
|
+
"document",
|
|
3606
|
+
"comments in shared or inherited header/footer stories",
|
|
3607
|
+
"DOCX import"
|
|
3608
|
+
);
|
|
3609
|
+
}
|
|
3610
|
+
const rootText = importedCommentText(root, styles);
|
|
3611
|
+
assertRepresentableCommentInitials(root.author ?? "Unknown", root.initials);
|
|
3612
|
+
const replies = repliesByParent.get(root.id) ?? [];
|
|
3613
|
+
for (const match of matches) {
|
|
3614
|
+
const thread = document.comments.addThread(
|
|
3615
|
+
{ block: match.target, start: match.start, end: match.end },
|
|
3616
|
+
rootText,
|
|
3617
|
+
{
|
|
3618
|
+
author: root.author ?? "Unknown",
|
|
3619
|
+
createdAt: normalizedImportedDate(root.createdAt),
|
|
3620
|
+
resolved: root.resolved ?? false
|
|
3621
|
+
}
|
|
3622
|
+
);
|
|
3623
|
+
for (const reply of replies) {
|
|
3624
|
+
assertRepresentableCommentInitials(reply.author ?? "Unknown", reply.initials);
|
|
3625
|
+
thread.addReply(
|
|
3626
|
+
importedCommentText(reply, styles),
|
|
3627
|
+
reply.author ?? "Unknown",
|
|
3628
|
+
normalizedImportedDate(reply.createdAt)
|
|
3629
|
+
);
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
byId.delete(root.id);
|
|
3633
|
+
for (const reply of replies) byId.delete(reply.id);
|
|
3634
|
+
}
|
|
3635
|
+
if (byId.size > 0)
|
|
3636
|
+
throw new Error(`Imported DOCX has orphaned comment replies: ${[...byId.keys()].join(", ")}`);
|
|
3637
|
+
}
|
|
3638
|
+
function importTrackedChanges(document, imported, paragraphs) {
|
|
3639
|
+
for (const source of imported.trackedChanges) {
|
|
3640
|
+
const projections = paragraphs.get(source.blockId);
|
|
3641
|
+
if (!projections || projections.length === 0)
|
|
3642
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3643
|
+
"document",
|
|
3644
|
+
"tracked changes outside body/header/footer paragraphs",
|
|
3645
|
+
"DOCX import"
|
|
3646
|
+
);
|
|
3647
|
+
if (projections.length > 1) {
|
|
3648
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3649
|
+
"document",
|
|
3650
|
+
"tracked changes in shared or inherited header/footer stories",
|
|
3651
|
+
"DOCX import"
|
|
3652
|
+
);
|
|
3653
|
+
}
|
|
3654
|
+
const extended = source;
|
|
3655
|
+
for (const paragraph of projections) {
|
|
3656
|
+
const start = extended.startTextOffset ?? inlineIndexToTextOffset(paragraph.source, source.startInlineIndex);
|
|
3657
|
+
const end = extended.endTextOffset ?? inlineIndexToTextOffset(paragraph.source, source.endInlineIndex);
|
|
3658
|
+
document.changes.add(
|
|
3659
|
+
{ block: paragraph.target, start, end },
|
|
3660
|
+
source.kind,
|
|
3661
|
+
source.author ?? "Unknown",
|
|
3662
|
+
normalizedImportedDate(source.createdAt)
|
|
3663
|
+
);
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
function validateImportedReviewProjection(document) {
|
|
3668
|
+
const commentsByBlock = /* @__PURE__ */ new Map();
|
|
3669
|
+
for (const comment of document.comments.items) {
|
|
3670
|
+
appendMap(commentsByBlock, comment.blockId, comment);
|
|
3671
|
+
}
|
|
3672
|
+
const changesByBlock = /* @__PURE__ */ new Map();
|
|
3673
|
+
for (const change of document.changes.items) {
|
|
3674
|
+
appendMap(changesByBlock, change.blockId, change);
|
|
3675
|
+
}
|
|
3676
|
+
for (const block of document.allStoryBlocks()) {
|
|
3677
|
+
if (!(block instanceof DocumentParagraph)) continue;
|
|
3678
|
+
const comments = commentsByBlock.get(block.id) ?? [];
|
|
3679
|
+
const changes = changesByBlock.get(block.id) ?? [];
|
|
3680
|
+
validateNonCrossingComments(comments);
|
|
3681
|
+
validateNonOverlappingChanges(changes);
|
|
3682
|
+
validateReviewMarkupForDocx(block, comments, changes);
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
function resolveImportedStyle(styleId, styles, expectedKind, seen = /* @__PURE__ */ new Set(), applyDefault = true) {
|
|
3686
|
+
const cacheKey = `${expectedKind}:${applyDefault ? "default" : "direct"}:${styleId ?? ""}`;
|
|
3687
|
+
const cache = importedStyleCache.get(styles) ?? /* @__PURE__ */ new Map();
|
|
3688
|
+
if (!importedStyleCache.has(styles)) importedStyleCache.set(styles, cache);
|
|
3689
|
+
if (seen.size === 0) {
|
|
3690
|
+
const cached = cache.get(cacheKey);
|
|
3691
|
+
if (cached) return cached;
|
|
3692
|
+
}
|
|
3693
|
+
if (!styleId) {
|
|
3694
|
+
if (!applyDefault) {
|
|
3695
|
+
const empty = { paragraph: {}, run: {} };
|
|
3696
|
+
if (seen.size === 0) cache.set(cacheKey, empty);
|
|
3697
|
+
return empty;
|
|
3698
|
+
}
|
|
3699
|
+
const defaults = [...styles.values()].filter(
|
|
3700
|
+
(style2) => style2.isDefault && style2.kind === expectedKind
|
|
3701
|
+
);
|
|
3702
|
+
if (defaults.length > 1) {
|
|
3703
|
+
throw new Error(`Imported DOCX defines multiple default ${expectedKind} styles`);
|
|
3704
|
+
}
|
|
3705
|
+
const defaultStyle = defaults[0];
|
|
3706
|
+
const resolved2 = defaultStyle ? resolveImportedStyle(defaultStyle.styleId, styles, expectedKind, seen, false) : { paragraph: {}, run: {} };
|
|
3707
|
+
if (seen.size === 0) cache.set(cacheKey, resolved2);
|
|
3708
|
+
return resolved2;
|
|
3709
|
+
}
|
|
3710
|
+
if (seen.has(styleId)) throw new Error(`Imported DOCX style cycle at ${styleId}`);
|
|
3711
|
+
const style = styles.get(styleId);
|
|
3712
|
+
if (!style) {
|
|
3713
|
+
if (["Normal", "DefaultParagraphFont", "ListParagraph"].includes(styleId)) {
|
|
3714
|
+
return { paragraph: {}, run: {} };
|
|
3715
|
+
}
|
|
3716
|
+
throw new Error(`Imported DOCX references missing style ${styleId}`);
|
|
3717
|
+
}
|
|
3718
|
+
if (style.kind !== expectedKind) {
|
|
3719
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3720
|
+
"document",
|
|
3721
|
+
`${expectedKind} content using ${style.kind} style ${styleId}`,
|
|
3722
|
+
"DOCX import"
|
|
3723
|
+
);
|
|
3724
|
+
}
|
|
3725
|
+
const canonical = expectedKind === "paragraph" ? styleId === "Normal" || styleId === "ListParagraph" || /^(?:Heading\s*)[1-6]$/iu.test(styleId) : styleId === "DefaultParagraphFont";
|
|
3726
|
+
if (!canonical) {
|
|
3727
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3728
|
+
"document",
|
|
3729
|
+
`custom named ${expectedKind} style ${styleId}`,
|
|
3730
|
+
"DOCX import"
|
|
3731
|
+
);
|
|
3732
|
+
}
|
|
3733
|
+
seen.add(styleId);
|
|
3734
|
+
const parent = resolveImportedStyle(style.basedOn, styles, expectedKind, seen, false);
|
|
3735
|
+
seen.delete(styleId);
|
|
3736
|
+
const resolved = {
|
|
3737
|
+
paragraph: { ...parent.paragraph, ...style.paragraph },
|
|
3738
|
+
run: { ...parent.run, ...style.run }
|
|
3739
|
+
};
|
|
3740
|
+
if (seen.size === 0) cache.set(cacheKey, resolved);
|
|
3741
|
+
return resolved;
|
|
3742
|
+
}
|
|
3743
|
+
function mapImportedParagraphStyle(style, styleId, listKinds) {
|
|
3744
|
+
if (style.bidirectional)
|
|
3745
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3746
|
+
"document",
|
|
3747
|
+
"bidirectional paragraphs",
|
|
3748
|
+
"DOCX import"
|
|
3749
|
+
);
|
|
3750
|
+
if ((style.indentLeftPt ?? 0) !== 0 || (style.indentRightPt ?? 0) !== 0 || (style.firstLinePt ?? 0) !== 0 || (style.hangingPt ?? 0) !== 0) {
|
|
3751
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3752
|
+
"document",
|
|
3753
|
+
"direct paragraph indentation",
|
|
3754
|
+
"DOCX import"
|
|
3755
|
+
);
|
|
3756
|
+
}
|
|
3757
|
+
if (style.keepLines)
|
|
3758
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3759
|
+
"document",
|
|
3760
|
+
"keep-lines paragraph pagination",
|
|
3761
|
+
"DOCX import"
|
|
3762
|
+
);
|
|
3763
|
+
if (style.lineRule && style.lineRule !== "auto")
|
|
3764
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3765
|
+
"document",
|
|
3766
|
+
`paragraph line rule ${style.lineRule}`,
|
|
3767
|
+
"DOCX import"
|
|
3768
|
+
);
|
|
3769
|
+
const headingMatch = /(?:^|\s)heading\s*([1-6])$/iu.exec(styleId ?? "");
|
|
3770
|
+
const importedHeadingLevel = headingMatch ? Number(headingMatch[1]) : void 0;
|
|
3771
|
+
if (style.outlineLevel !== void 0 && (style.outlineLevel < 0 || style.outlineLevel > 5 || importedHeadingLevel === void 0 || style.outlineLevel + 1 !== importedHeadingLevel)) {
|
|
3772
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3773
|
+
"document",
|
|
3774
|
+
`outline level ${style.outlineLevel} without the matching supported heading style`,
|
|
3775
|
+
"DOCX import"
|
|
3776
|
+
);
|
|
3777
|
+
}
|
|
3778
|
+
const alignmentValue = style.alignment === "both" ? "justify" : style.alignment === "center" ? "center" : style.alignment === "right" || style.alignment === "end" ? "right" : style.alignment === "left" || style.alignment === "start" ? "left" : void 0;
|
|
3779
|
+
if (style.alignment && !alignmentValue)
|
|
3780
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3781
|
+
"document",
|
|
3782
|
+
`paragraph alignment ${style.alignment}`,
|
|
3783
|
+
"DOCX import"
|
|
3784
|
+
);
|
|
3785
|
+
const list = style.list;
|
|
3786
|
+
const listKind = list ? listKinds.get(list.numId)?.get(list.level) : void 0;
|
|
3787
|
+
if (list && !listKind)
|
|
3788
|
+
throw new Error(`Imported paragraph references missing numbering ${list.numId}/${list.level}`);
|
|
3789
|
+
return {
|
|
3790
|
+
...importedHeadingLevel ? { headingLevel: importedHeadingLevel } : {},
|
|
3791
|
+
...alignmentValue ? { alignment: alignmentValue } : {},
|
|
3792
|
+
...style.spaceBeforePt !== void 0 ? { spaceBeforePt: style.spaceBeforePt } : {},
|
|
3793
|
+
...style.spaceAfterPt !== void 0 ? { spaceAfterPt: style.spaceAfterPt } : {},
|
|
3794
|
+
...style.line !== void 0 ? { lineHeight: style.line / 240 } : {},
|
|
3795
|
+
...style.keepNext !== void 0 ? { keepNext: style.keepNext } : {},
|
|
3796
|
+
...style.pageBreakBefore !== void 0 ? { pageBreakBefore: style.pageBreakBefore } : {},
|
|
3797
|
+
...list && listKind ? { list: { kind: listKind, level: list.level, instanceId: list.numId } } : {}
|
|
3798
|
+
};
|
|
3799
|
+
}
|
|
3800
|
+
function mapImportedRunStyle(style) {
|
|
3801
|
+
if (style.highlight && style.highlight !== "none")
|
|
3802
|
+
throw new UnsupportedArtifactFeatureError("document", "text highlighting", "DOCX import");
|
|
3803
|
+
if (style.verticalAlign && style.verticalAlign !== "baseline")
|
|
3804
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3805
|
+
"document",
|
|
3806
|
+
`text vertical alignment ${style.verticalAlign}`,
|
|
3807
|
+
"DOCX import"
|
|
3808
|
+
);
|
|
3809
|
+
if (style.fontSizeComplexScriptPt !== void 0 && style.fontSizeComplexScriptPt !== style.fontSizePt) {
|
|
3810
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3811
|
+
"document",
|
|
3812
|
+
"script-specific font sizing",
|
|
3813
|
+
"DOCX import"
|
|
3814
|
+
);
|
|
3815
|
+
}
|
|
3816
|
+
if ((style.boldComplexScript ?? false) !== (style.bold ?? false)) {
|
|
3817
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3818
|
+
"document",
|
|
3819
|
+
"script-specific bold formatting",
|
|
3820
|
+
"DOCX import"
|
|
3821
|
+
);
|
|
3822
|
+
}
|
|
3823
|
+
if ((style.italicComplexScript ?? false) !== (style.italic ?? false)) {
|
|
3824
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3825
|
+
"document",
|
|
3826
|
+
"script-specific italic formatting",
|
|
3827
|
+
"DOCX import"
|
|
3828
|
+
);
|
|
3829
|
+
}
|
|
3830
|
+
if (style.language) {
|
|
3831
|
+
throw new UnsupportedArtifactFeatureError("document", "run language metadata", "DOCX import");
|
|
3832
|
+
}
|
|
3833
|
+
return {
|
|
3834
|
+
...style.fontFamily ? { fontFamily: style.fontFamily } : {},
|
|
3835
|
+
...style.fontSizePt !== void 0 ? { fontSizePt: style.fontSizePt } : {},
|
|
3836
|
+
...style.color && style.color !== "auto" ? { color: normalizeHex(style.color) } : {},
|
|
3837
|
+
...style.bold !== void 0 ? { bold: style.bold } : {},
|
|
3838
|
+
...style.italic !== void 0 ? { italic: style.italic } : {},
|
|
3839
|
+
...style.underline !== void 0 ? { underline: style.underline !== "none" } : {},
|
|
3840
|
+
...style.strike !== void 0 ? { strike: style.strike } : {}
|
|
3841
|
+
};
|
|
3842
|
+
}
|
|
3843
|
+
function importedPage(section) {
|
|
3844
|
+
return {
|
|
3845
|
+
widthPt: section.page.widthPt,
|
|
3846
|
+
heightPt: section.page.heightPt,
|
|
3847
|
+
marginTopPt: section.page.marginTopPt,
|
|
3848
|
+
marginRightPt: section.page.marginRightPt,
|
|
3849
|
+
marginBottomPt: section.page.marginBottomPt,
|
|
3850
|
+
marginLeftPt: section.page.marginLeftPt,
|
|
3851
|
+
headerPt: section.page.headerPt,
|
|
3852
|
+
footerPt: section.page.footerPt,
|
|
3853
|
+
gutterPt: section.page.gutterPt
|
|
3854
|
+
};
|
|
3855
|
+
}
|
|
3856
|
+
function validateImportedSections(imported, sections) {
|
|
3857
|
+
if (sections.length === 0) throw new Error("Imported DOCX has no sections");
|
|
3858
|
+
const storyReferences = /* @__PURE__ */ new Set();
|
|
3859
|
+
let expectedStart = 0;
|
|
3860
|
+
for (const section of sections) {
|
|
3861
|
+
for (const reference of [...section.headers, ...section.footers]) {
|
|
3862
|
+
if (storyReferences.has(reference.partName)) {
|
|
3863
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3864
|
+
"document",
|
|
3865
|
+
"linked or inherited header/footer stories",
|
|
3866
|
+
"DOCX import"
|
|
3867
|
+
);
|
|
3868
|
+
}
|
|
3869
|
+
storyReferences.add(reference.partName);
|
|
3870
|
+
}
|
|
3871
|
+
if (section.startBlockIndex !== expectedStart || !Number.isSafeInteger(section.endBlockIndex) || section.endBlockIndex < section.startBlockIndex || section.endBlockIndex > imported.blocks.length) {
|
|
3872
|
+
throw new Error("Imported DOCX sections do not form a contiguous body partition");
|
|
3873
|
+
}
|
|
3874
|
+
expectedStart = section.endBlockIndex;
|
|
3875
|
+
if (section.type !== void 0 && section.type !== "nextPage") {
|
|
3876
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3877
|
+
"document",
|
|
3878
|
+
`section break type ${section.type}`,
|
|
3879
|
+
"DOCX import"
|
|
3880
|
+
);
|
|
3881
|
+
}
|
|
3882
|
+
const orientation = section.page.orientation;
|
|
3883
|
+
if (orientation !== void 0 && orientation !== "portrait" && orientation !== "landscape") {
|
|
3884
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3885
|
+
"document",
|
|
3886
|
+
`page orientation ${orientation}`,
|
|
3887
|
+
"DOCX import"
|
|
3888
|
+
);
|
|
3889
|
+
}
|
|
3890
|
+
if (orientation === "landscape" && section.page.widthPt < section.page.heightPt || orientation === "portrait" && section.page.widthPt > section.page.heightPt) {
|
|
3891
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3892
|
+
"document",
|
|
3893
|
+
"page dimensions inconsistent with page orientation",
|
|
3894
|
+
"DOCX import"
|
|
3895
|
+
);
|
|
3896
|
+
}
|
|
3897
|
+
const hasFirstStory = [...section.headers, ...section.footers].some(
|
|
3898
|
+
(reference) => reference.kind === "first"
|
|
3899
|
+
);
|
|
3900
|
+
if (section.titlePage && !hasFirstStory) {
|
|
3901
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3902
|
+
"document",
|
|
3903
|
+
"first-page header/footer mode without a representable first-page story",
|
|
3904
|
+
"DOCX import"
|
|
3905
|
+
);
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
if (expectedStart !== imported.blocks.length) {
|
|
3909
|
+
throw new Error("Imported DOCX sections do not cover the full body");
|
|
3910
|
+
}
|
|
3911
|
+
const hasEvenStory = sections.some(
|
|
3912
|
+
(section) => [...section.headers, ...section.footers].some((reference) => reference.kind === "even")
|
|
3913
|
+
);
|
|
3914
|
+
if (imported.evenAndOddHeaders && !hasEvenStory) {
|
|
3915
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3916
|
+
"document",
|
|
3917
|
+
"even-page header/footer mode without a representable even-page story",
|
|
3918
|
+
"DOCX import"
|
|
3919
|
+
);
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
function importedListKinds(lists) {
|
|
3923
|
+
const result = /* @__PURE__ */ new Map();
|
|
3924
|
+
for (const list of lists) {
|
|
3925
|
+
let instanceSupported = true;
|
|
3926
|
+
for (const override of list.overrides) {
|
|
3927
|
+
if (override.definition || override.start !== void 0 && override.start !== 1) {
|
|
3928
|
+
instanceSupported = false;
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
const levels = /* @__PURE__ */ new Map();
|
|
3932
|
+
let definitionSupported = true;
|
|
3933
|
+
for (const level of list.levels) {
|
|
3934
|
+
if (level.start !== 1 || level.restart !== void 0 || level.legal !== void 0 || level.format !== "bullet" && level.format !== "decimal") {
|
|
3935
|
+
definitionSupported = false;
|
|
3936
|
+
continue;
|
|
3937
|
+
}
|
|
3938
|
+
const runtimeLevel = level;
|
|
3939
|
+
const expectedText = level.format === "decimal" ? `%${level.level + 1}.` : void 0;
|
|
3940
|
+
const supportedBullet = level.format === "bullet" && level.text === ["\u2022", "\u25E6", "\u25AA"][level.level % 3];
|
|
3941
|
+
const paragraphEntries = Object.entries(level.paragraph);
|
|
3942
|
+
const hasExpectedIndent = level.paragraph.indentLeftPt !== void 0 && Math.abs(level.paragraph.indentLeftPt - 36 * (level.level + 1)) <= 0.01 && level.paragraph.hangingPt !== void 0 && Math.abs(level.paragraph.hangingPt - 18) <= 0.01;
|
|
3943
|
+
const onlyExpectedIndent = paragraphEntries.every(
|
|
3944
|
+
([key]) => key === "indentLeftPt" || key === "hangingPt"
|
|
3945
|
+
);
|
|
3946
|
+
if (level.format === "decimal" && level.text !== expectedText || level.format === "bullet" && !supportedBullet || level.suffix !== void 0 && level.suffix !== "tab" || level.paragraphStyleId !== void 0 || runtimeLevel.alignment !== void 0 && runtimeLevel.alignment !== "left" || !hasExpectedIndent || !onlyExpectedIndent || Object.keys(level.run).length > 0) {
|
|
3947
|
+
definitionSupported = false;
|
|
3948
|
+
continue;
|
|
3949
|
+
}
|
|
3950
|
+
levels.set(level.level, level.format === "bullet" ? "bullet" : "number");
|
|
3951
|
+
}
|
|
3952
|
+
result.set(list.numId, instanceSupported && definitionSupported ? levels : /* @__PURE__ */ new Map());
|
|
3953
|
+
}
|
|
3954
|
+
return result;
|
|
3955
|
+
}
|
|
3956
|
+
function assertRepresentableTableCellParagraph(style, styleId, listKinds) {
|
|
3957
|
+
const projected = mapImportedParagraphStyle(style, styleId, listKinds);
|
|
3958
|
+
const remaining = Object.entries(projected).filter(
|
|
3959
|
+
([key, value]) => !(key === "alignment" && value === "left" || key === "spaceBeforePt" && value === 0 || key === "spaceAfterPt" && value === 0)
|
|
3960
|
+
);
|
|
3961
|
+
if (remaining.length > 0) {
|
|
3962
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3963
|
+
"document",
|
|
3964
|
+
"paragraph formatting inside table cells",
|
|
3965
|
+
"DOCX import"
|
|
3966
|
+
);
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
function importedUniformCellPadding(source) {
|
|
3970
|
+
const hasExplicitMargins = source.cellMargins !== void 0 || source.rows.some((row) => row.cells.some((cell) => cell.margins !== void 0));
|
|
3971
|
+
if (!hasExplicitMargins) {
|
|
3972
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3973
|
+
"document",
|
|
3974
|
+
"table cell padding inherited from an unrepresented style",
|
|
3975
|
+
"DOCX import"
|
|
3976
|
+
);
|
|
3977
|
+
}
|
|
3978
|
+
const values = [];
|
|
3979
|
+
for (const row of source.rows) {
|
|
3980
|
+
for (const cell of row.cells) {
|
|
3981
|
+
for (const side of ["top", "right", "bottom", "left"]) {
|
|
3982
|
+
const measure = boxMeasure(cell.margins, side) ?? boxMeasure(source.cellMargins, side);
|
|
3983
|
+
if (!measure || measure.unit !== "pt" || !Number.isFinite(measure.value)) {
|
|
3984
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3985
|
+
"document",
|
|
3986
|
+
"asymmetric or non-point table cell margins",
|
|
3987
|
+
"DOCX import"
|
|
3988
|
+
);
|
|
3989
|
+
}
|
|
3990
|
+
values.push(measure.value);
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3994
|
+
const first = values[0];
|
|
3995
|
+
if (first === void 0 || values.some((value) => Math.abs(value - first) > 0.01)) {
|
|
3996
|
+
throw new UnsupportedArtifactFeatureError(
|
|
3997
|
+
"document",
|
|
3998
|
+
"non-uniform table cell padding",
|
|
3999
|
+
"DOCX import"
|
|
4000
|
+
);
|
|
4001
|
+
}
|
|
4002
|
+
return first;
|
|
4003
|
+
}
|
|
4004
|
+
function boxMeasure(box, side) {
|
|
4005
|
+
if (!box) return void 0;
|
|
4006
|
+
if (side === "left") return box.left ?? box.start;
|
|
4007
|
+
if (side === "right") return box.right ?? box.end;
|
|
4008
|
+
return box[side];
|
|
4009
|
+
}
|
|
4010
|
+
function importedUniformTableBorder(borders) {
|
|
4011
|
+
if (!borders) {
|
|
4012
|
+
throw new UnsupportedArtifactFeatureError(
|
|
4013
|
+
"document",
|
|
4014
|
+
"table borders inherited from an unrepresented style",
|
|
4015
|
+
"DOCX import"
|
|
4016
|
+
);
|
|
4017
|
+
}
|
|
4018
|
+
const edges = [
|
|
4019
|
+
borders.top,
|
|
4020
|
+
borders.right,
|
|
4021
|
+
borders.bottom,
|
|
4022
|
+
borders.left,
|
|
4023
|
+
borders.insideHorizontal,
|
|
4024
|
+
borders.insideVertical
|
|
4025
|
+
];
|
|
4026
|
+
if (edges.some((edge) => edge === void 0)) {
|
|
4027
|
+
throw new UnsupportedArtifactFeatureError("document", "partial table borders", "DOCX import");
|
|
4028
|
+
}
|
|
4029
|
+
const colors = edges.map((edge) => {
|
|
4030
|
+
if (edge?.style !== "single" || edge.sizePt === void 0 || Math.abs(edge.sizePt - 0.5) > 0.01 || (edge.spacePt ?? 0) !== 0 || edge.shadow === true || edge.frame === true || !edge.color || edge.color.toLowerCase() === "auto") {
|
|
4031
|
+
throw new UnsupportedArtifactFeatureError(
|
|
4032
|
+
"document",
|
|
4033
|
+
"non-uniform or decorated table borders",
|
|
4034
|
+
"DOCX import"
|
|
4035
|
+
);
|
|
4036
|
+
}
|
|
4037
|
+
return normalizeHex(edge.color);
|
|
4038
|
+
});
|
|
4039
|
+
const first = colors[0];
|
|
4040
|
+
if (colors.some((color) => color !== first)) {
|
|
4041
|
+
throw new UnsupportedArtifactFeatureError(
|
|
4042
|
+
"document",
|
|
4043
|
+
"mixed table border colors",
|
|
4044
|
+
"DOCX import"
|
|
4045
|
+
);
|
|
4046
|
+
}
|
|
4047
|
+
return first;
|
|
4048
|
+
}
|
|
4049
|
+
function isOnlyPageBreak(paragraph, styles) {
|
|
4050
|
+
const inherited = resolveImportedStyle(paragraph.styleId, styles, "paragraph");
|
|
4051
|
+
return paragraph.inlines.length === 1 && paragraph.inlines[0]?.kind === "pageBreak" && paragraph.commentAnchors.length === 0 && Object.keys({ ...inherited.paragraph, ...paragraph.style }).length === 0 && Object.keys(inherited.run).length === 0;
|
|
4052
|
+
}
|
|
4053
|
+
function importedCommentText(comment, styles) {
|
|
4054
|
+
const paragraph = comment.blocks[0];
|
|
4055
|
+
if (comment.blocks.length !== 1 || paragraph?.kind !== "paragraph" || paragraph.styleId !== void 0 || Object.keys(paragraph.style).length > 0 || paragraph.inlines.some(
|
|
4056
|
+
(inline) => inline.kind !== "run" || inline.styleId !== void 0 || Object.keys(inline.style).length > 0
|
|
4057
|
+
) || paragraph.commentAnchors.length > 0) {
|
|
4058
|
+
throw new UnsupportedArtifactFeatureError(
|
|
4059
|
+
"document",
|
|
4060
|
+
"multi-paragraph or formatted content inside comments",
|
|
4061
|
+
"DOCX import"
|
|
4062
|
+
);
|
|
4063
|
+
}
|
|
4064
|
+
const defaults = resolveImportedStyle(void 0, styles, "paragraph");
|
|
4065
|
+
if (Object.keys(defaults.paragraph).length > 0 || Object.keys(defaults.run).length > 0) {
|
|
4066
|
+
throw new UnsupportedArtifactFeatureError(
|
|
4067
|
+
"document",
|
|
4068
|
+
"formatted default style inside comments",
|
|
4069
|
+
"DOCX import"
|
|
4070
|
+
);
|
|
4071
|
+
}
|
|
4072
|
+
return paragraph.inlines.map((inline) => inline.kind === "run" ? inline.text : "").join("");
|
|
4073
|
+
}
|
|
4074
|
+
function inlineIndexToTextOffset(paragraph, index) {
|
|
4075
|
+
if (!Number.isInteger(index) || index < 0 || index > paragraph.inlines.length)
|
|
4076
|
+
throw new Error("Imported tracked-change inline index is invalid");
|
|
4077
|
+
return paragraph.inlines.slice(0, index).reduce((total, inline) => total + (inline.kind === "run" ? inline.text.length : 0), 0);
|
|
4078
|
+
}
|
|
4079
|
+
function normalizedImportedDate(value) {
|
|
4080
|
+
if (!value) {
|
|
4081
|
+
throw new UnsupportedArtifactFeatureError(
|
|
4082
|
+
"document",
|
|
4083
|
+
"comment or tracked-change timestamp omitted",
|
|
4084
|
+
"DOCX import"
|
|
4085
|
+
);
|
|
4086
|
+
}
|
|
4087
|
+
const date = new Date(value);
|
|
4088
|
+
if (!Number.isFinite(date.getTime()))
|
|
4089
|
+
throw new Error(`Imported DOCX contains an invalid timestamp: ${value}`);
|
|
4090
|
+
return date.toISOString();
|
|
4091
|
+
}
|
|
4092
|
+
function buildDocxContext(document, docx) {
|
|
4093
|
+
const commentsByBlock = /* @__PURE__ */ new Map();
|
|
4094
|
+
const commentDefinitions = [];
|
|
4095
|
+
let commentId = 0;
|
|
4096
|
+
for (const thread of document.comments.items) {
|
|
4097
|
+
const rootId = commentId++;
|
|
4098
|
+
appendMap(commentsByBlock, thread.blockId, { thread, numericId: rootId });
|
|
4099
|
+
thread.replies.forEach((reply, index) => {
|
|
4100
|
+
const id = index === 0 ? rootId : commentId++;
|
|
4101
|
+
commentDefinitions.push({
|
|
4102
|
+
id,
|
|
4103
|
+
...index > 0 ? { parentId: rootId } : {},
|
|
4104
|
+
author: reply.author,
|
|
4105
|
+
initials: initials(reply.author),
|
|
4106
|
+
date: new Date(reply.createdAt),
|
|
4107
|
+
...index === 0 ? { resolved: thread.resolved } : {},
|
|
4108
|
+
children: [
|
|
4109
|
+
new docx.Paragraph({
|
|
4110
|
+
children: [new docx.TextRun(docxTextRunOptions(reply.text, {}, docx))]
|
|
4111
|
+
})
|
|
4112
|
+
]
|
|
4113
|
+
});
|
|
4114
|
+
});
|
|
4115
|
+
}
|
|
4116
|
+
const changesByBlock = /* @__PURE__ */ new Map();
|
|
4117
|
+
document.changes.items.forEach(
|
|
4118
|
+
(change, numericId) => appendMap(changesByBlock, change.blockId, { change, numericId })
|
|
4119
|
+
);
|
|
4120
|
+
return {
|
|
4121
|
+
commentsByBlock,
|
|
4122
|
+
changesByBlock,
|
|
4123
|
+
commentDefinitions,
|
|
4124
|
+
listInstances: buildListInstances(document)
|
|
4125
|
+
};
|
|
4126
|
+
}
|
|
4127
|
+
function buildDocxStories(stories, page, docx, context) {
|
|
4128
|
+
const make = (story, emitEmpty) => {
|
|
4129
|
+
if (story.items.length === 0 && !emitEmpty) return void 0;
|
|
4130
|
+
const children2 = story.items.length > 0 ? story.items.map((block) => toDocxBlock(block, page, docx, context)) : [new docx.Paragraph({ children: [] })];
|
|
4131
|
+
return story.kind === "header" ? new docx.Header({ children: children2 }) : new docx.Footer({ children: children2 });
|
|
4132
|
+
};
|
|
4133
|
+
const result = {};
|
|
4134
|
+
const defaultStory = make(stories.default, true);
|
|
4135
|
+
const firstStory = make(stories.first, true);
|
|
4136
|
+
const evenStory = make(stories.even, true);
|
|
4137
|
+
if (defaultStory) result.default = defaultStory;
|
|
4138
|
+
if (firstStory) result.first = firstStory;
|
|
4139
|
+
if (evenStory) result.even = evenStory;
|
|
4140
|
+
return result;
|
|
4141
|
+
}
|
|
4142
|
+
function toDocxBlock(block, page, docx, context) {
|
|
4143
|
+
if (block instanceof DocumentPageBreak) {
|
|
4144
|
+
return new docx.Paragraph({ children: [new docx.PageBreak()] });
|
|
4145
|
+
}
|
|
4146
|
+
if (block instanceof DocumentTable) return toDocxTable(block, page, docx);
|
|
4147
|
+
const heading = headingLevel(block.style.headingLevel, docx);
|
|
4148
|
+
const paragraphAlignment = alignment(block.style.alignment, docx);
|
|
4149
|
+
const spacing = {
|
|
4150
|
+
...block.style.spaceBeforePt !== void 0 ? { before: ptToTwip(block.style.spaceBeforePt) } : {},
|
|
4151
|
+
...block.style.spaceAfterPt !== void 0 ? { after: ptToTwip(block.style.spaceAfterPt) } : {},
|
|
4152
|
+
...block.style.lineHeight !== void 0 ? { line: Math.round(block.style.lineHeight * 240) } : {}
|
|
4153
|
+
};
|
|
4154
|
+
const list = block.style.list;
|
|
4155
|
+
return new docx.Paragraph({
|
|
4156
|
+
...heading ? { heading } : {},
|
|
4157
|
+
...paragraphAlignment ? { alignment: paragraphAlignment } : {},
|
|
4158
|
+
...Object.keys(spacing).length > 0 ? { spacing } : {},
|
|
4159
|
+
...block.style.keepNext !== void 0 ? { keepNext: block.style.keepNext } : {},
|
|
4160
|
+
...block.style.pageBreakBefore !== void 0 ? { pageBreakBefore: block.style.pageBreakBefore } : {},
|
|
4161
|
+
...list ? {
|
|
4162
|
+
numbering: {
|
|
4163
|
+
reference: list.kind === "bullet" ? "opengeni-bullet" : "opengeni-number",
|
|
4164
|
+
level: list.level ?? 0,
|
|
4165
|
+
...context.listInstances.has(block.id) ? { instance: context.listInstances.get(block.id) } : {}
|
|
4166
|
+
}
|
|
4167
|
+
} : {},
|
|
4168
|
+
children: toDocxParagraphChildren(block, docx, context)
|
|
4169
|
+
});
|
|
4170
|
+
}
|
|
4171
|
+
function toDocxTable(block, page, docx) {
|
|
4172
|
+
const columnCount = block.rows[0]?.length ?? 0;
|
|
4173
|
+
const usableWidth = page.widthPt - page.marginLeftPt - page.marginRightPt;
|
|
4174
|
+
const totalWidth = block.style.widthPt ?? block.style.columnWidthsPt?.reduce((total, width) => total + width, 0) ?? usableWidth;
|
|
4175
|
+
const columnWidths = block.style.columnWidthsPt ?? Array.from({ length: columnCount }, () => totalWidth / columnCount);
|
|
4176
|
+
const columnWidthsTwip = columnWidths.map(ptToTwip);
|
|
4177
|
+
const tableWidthTwip = columnWidthsTwip.reduce((sum, width) => sum + width, 0);
|
|
4178
|
+
const padding = ptToTwip(block.style.cellPaddingPt ?? 6);
|
|
4179
|
+
const borderColor = normalizeHex(block.style.borderColor ?? "#D1D5DB").slice(1);
|
|
4180
|
+
const border = { style: docx.BorderStyle.SINGLE, size: 4, color: borderColor };
|
|
4181
|
+
return new docx.Table({
|
|
4182
|
+
width: { size: tableWidthTwip, type: docx.WidthType.DXA },
|
|
4183
|
+
columnWidths: columnWidthsTwip,
|
|
4184
|
+
indent: { size: padding, type: docx.WidthType.DXA },
|
|
4185
|
+
layout: docx.TableLayoutType.FIXED,
|
|
4186
|
+
margins: { top: padding, bottom: padding, left: padding, right: padding },
|
|
4187
|
+
borders: {
|
|
4188
|
+
top: border,
|
|
4189
|
+
bottom: border,
|
|
4190
|
+
left: border,
|
|
4191
|
+
right: border,
|
|
4192
|
+
insideHorizontal: border,
|
|
4193
|
+
insideVertical: border
|
|
4194
|
+
},
|
|
4195
|
+
rows: block.rows.map(
|
|
4196
|
+
(row, rowIndex) => new docx.TableRow({
|
|
4197
|
+
tableHeader: rowIndex < (block.style.headerRows ?? 0),
|
|
4198
|
+
cantSplit: !(block.style.allowRowSplit ?? false),
|
|
4199
|
+
children: row.map(
|
|
4200
|
+
(cell, columnIndex) => new docx.TableCell({
|
|
4201
|
+
width: {
|
|
4202
|
+
size: columnWidthsTwip[columnIndex] ?? ptToTwip(72),
|
|
4203
|
+
type: docx.WidthType.DXA
|
|
4204
|
+
},
|
|
4205
|
+
verticalAlign: docx.VerticalAlign.CENTER,
|
|
4206
|
+
margins: { top: padding, bottom: padding, left: padding, right: padding },
|
|
4207
|
+
...rowIndex < (block.style.headerRows ?? 0) && block.style.headerFill ? { shading: { fill: normalizeHex(block.style.headerFill).slice(1) } } : {},
|
|
4208
|
+
children: [
|
|
4209
|
+
new docx.Paragraph({ children: cell.map((run) => toDocxTextRun(run, docx)) })
|
|
4210
|
+
]
|
|
4211
|
+
})
|
|
4212
|
+
)
|
|
4213
|
+
})
|
|
4214
|
+
)
|
|
4215
|
+
});
|
|
4216
|
+
}
|
|
4217
|
+
function toDocxParagraphChildren(block, docx, context) {
|
|
4218
|
+
const comments = context.commentsByBlock.get(block.id) ?? [];
|
|
4219
|
+
const changes = context.changesByBlock.get(block.id) ?? [];
|
|
4220
|
+
const paragraphText = block.text;
|
|
4221
|
+
validateNonCrossingComments(comments.map(({ thread }) => thread));
|
|
4222
|
+
validateNonOverlappingChanges(changes.map(({ change }) => change));
|
|
4223
|
+
validateReviewMarkupForDocx(
|
|
4224
|
+
block,
|
|
4225
|
+
comments.map(({ thread }) => thread),
|
|
4226
|
+
changes.map(({ change }) => change)
|
|
4227
|
+
);
|
|
4228
|
+
const boundaries = /* @__PURE__ */ new Set([0, paragraphText.length]);
|
|
4229
|
+
const runSpans = [];
|
|
4230
|
+
const pointComments = /* @__PURE__ */ new Map();
|
|
4231
|
+
const openingComments = /* @__PURE__ */ new Map();
|
|
4232
|
+
const closingComments = /* @__PURE__ */ new Map();
|
|
4233
|
+
let offset = 0;
|
|
4234
|
+
for (const run of block.runs) {
|
|
4235
|
+
const start = offset;
|
|
4236
|
+
offset += run.text.length;
|
|
4237
|
+
runSpans.push({ start, end: offset, run });
|
|
4238
|
+
boundaries.add(start);
|
|
4239
|
+
boundaries.add(offset);
|
|
4240
|
+
}
|
|
4241
|
+
for (const { thread } of comments) {
|
|
4242
|
+
boundaries.add(thread.start);
|
|
4243
|
+
boundaries.add(thread.end);
|
|
4244
|
+
}
|
|
4245
|
+
for (const comment of comments) {
|
|
4246
|
+
if (comment.thread.start === comment.thread.end) {
|
|
4247
|
+
appendMap(pointComments, comment.thread.start, comment);
|
|
4248
|
+
} else {
|
|
4249
|
+
appendMap(openingComments, comment.thread.start, comment);
|
|
4250
|
+
appendMap(closingComments, comment.thread.end, comment);
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
for (const values of openingComments.values()) {
|
|
4254
|
+
values.sort((left, right) => right.thread.end - left.thread.end);
|
|
4255
|
+
}
|
|
4256
|
+
for (const values of closingComments.values()) {
|
|
4257
|
+
values.sort((left, right) => right.thread.start - left.thread.start);
|
|
4258
|
+
}
|
|
4259
|
+
for (const { change } of changes) {
|
|
4260
|
+
boundaries.add(change.start);
|
|
4261
|
+
boundaries.add(change.end);
|
|
4262
|
+
}
|
|
4263
|
+
const points = [...boundaries].sort((a, b) => a - b);
|
|
4264
|
+
const sortedChanges = [...changes].sort(
|
|
4265
|
+
(left, right) => left.change.start - right.change.start || left.change.end - right.change.end
|
|
4266
|
+
);
|
|
4267
|
+
const children2 = [];
|
|
4268
|
+
if (paragraphText.length === 0) {
|
|
4269
|
+
for (const { numericId } of pointComments.get(0) ?? []) {
|
|
4270
|
+
children2.push(
|
|
4271
|
+
new docx.CommentRangeStart(numericId),
|
|
4272
|
+
new docx.CommentRangeEnd(numericId),
|
|
4273
|
+
commentReferenceRun(numericId, docx)
|
|
4274
|
+
);
|
|
4275
|
+
}
|
|
4276
|
+
children2.push(...block.runs.map((run) => toDocxTextRun(run, docx)));
|
|
4277
|
+
return children2;
|
|
4278
|
+
}
|
|
4279
|
+
let runIndex = 0;
|
|
4280
|
+
let changeIndex = 0;
|
|
4281
|
+
for (let index = 0; index < points.length; index++) {
|
|
4282
|
+
const point = points[index];
|
|
4283
|
+
for (const { numericId } of pointComments.get(point) ?? []) {
|
|
4284
|
+
children2.push(
|
|
4285
|
+
new docx.CommentRangeStart(numericId),
|
|
4286
|
+
new docx.CommentRangeEnd(numericId),
|
|
4287
|
+
commentReferenceRun(numericId, docx)
|
|
4288
|
+
);
|
|
4289
|
+
}
|
|
4290
|
+
for (const { numericId } of openingComments.get(point) ?? []) {
|
|
4291
|
+
children2.push(new docx.CommentRangeStart(numericId));
|
|
4292
|
+
}
|
|
4293
|
+
const next = points[index + 1];
|
|
4294
|
+
if (next !== void 0 && next > point) {
|
|
4295
|
+
while (runSpans[runIndex] && runSpans[runIndex].end <= point) runIndex += 1;
|
|
4296
|
+
const span = runSpans[runIndex];
|
|
4297
|
+
if (!span) throw new Error(`Document paragraph ${block.id} has a discontinuous run map`);
|
|
4298
|
+
if (span.start > point || span.end < next)
|
|
4299
|
+
throw new Error(`Document paragraph ${block.id} has a discontinuous run map`);
|
|
4300
|
+
const text = span.run.text.slice(point - span.start, next - span.start);
|
|
4301
|
+
while (sortedChanges[changeIndex]?.change.end !== void 0 && sortedChanges[changeIndex].change.end <= point) {
|
|
4302
|
+
changeIndex += 1;
|
|
4303
|
+
}
|
|
4304
|
+
const candidate = sortedChanges[changeIndex];
|
|
4305
|
+
const active = candidate && candidate.change.start <= point && candidate.change.end >= next ? candidate : void 0;
|
|
4306
|
+
if (!active || point === active.change.start) {
|
|
4307
|
+
children2.push(
|
|
4308
|
+
active ? toDocxChangedTextRun(
|
|
4309
|
+
paragraphText.slice(active.change.start, active.change.end),
|
|
4310
|
+
span.run.style,
|
|
4311
|
+
active,
|
|
4312
|
+
docx
|
|
4313
|
+
) : toDocxChangedTextRun(text, span.run.style, void 0, docx)
|
|
4314
|
+
);
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
if (next !== void 0) {
|
|
4318
|
+
for (const { numericId } of closingComments.get(next) ?? []) {
|
|
4319
|
+
children2.push(new docx.CommentRangeEnd(numericId));
|
|
4320
|
+
children2.push(commentReferenceRun(numericId, docx));
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
4324
|
+
return children2;
|
|
4325
|
+
}
|
|
4326
|
+
function commentReferenceRun(numericId, docx) {
|
|
4327
|
+
return new docx.TextRun({ children: [new docx.CommentReference(numericId)] });
|
|
4328
|
+
}
|
|
4329
|
+
function toDocxChangedTextRun(text, style, active, docx) {
|
|
4330
|
+
const options = docxTextRunOptions(text, style, docx);
|
|
4331
|
+
if (!active) return materializedTextRun(text, options, docx);
|
|
4332
|
+
const changeOptions = {
|
|
4333
|
+
...options,
|
|
4334
|
+
id: active.numericId,
|
|
4335
|
+
author: active.change.author,
|
|
4336
|
+
date: active.change.createdAt
|
|
4337
|
+
};
|
|
4338
|
+
return active.change.kind === "insert" ? new docx.InsertedTextRun(changeOptions) : new docx.DeletedTextRun(changeOptions);
|
|
4339
|
+
}
|
|
4340
|
+
function toDocxTextRun(run, docx) {
|
|
4341
|
+
return materializedTextRun(run.text, docxTextRunOptions(run.text, run.style, docx), docx);
|
|
4342
|
+
}
|
|
4343
|
+
function materializedTextRun(text, options, docx) {
|
|
4344
|
+
if (text.length > 0) return new docx.TextRun(options);
|
|
4345
|
+
const { text: _text, children: _children, ...properties } = options;
|
|
4346
|
+
return new docx.Run({
|
|
4347
|
+
...properties,
|
|
4348
|
+
children: [new docx.EmptyElement("w:t")]
|
|
4349
|
+
});
|
|
4350
|
+
}
|
|
4351
|
+
function docxTextRunOptions(text, style, docx) {
|
|
4352
|
+
return {
|
|
4353
|
+
...text.includes(" ") || text.includes("\n") ? { children: docxTextChildren(text, docx) } : { text },
|
|
4354
|
+
...style.fontFamily ? { font: style.fontFamily } : {},
|
|
4355
|
+
...style.fontSizePt !== void 0 ? { size: Math.round(style.fontSizePt * 2) } : {},
|
|
4356
|
+
...style.color ? { color: normalizeHex(style.color).slice(1) } : {},
|
|
4357
|
+
...style.bold !== void 0 ? { bold: style.bold } : {},
|
|
4358
|
+
...style.italic !== void 0 ? { italics: style.italic } : {},
|
|
4359
|
+
...style.underline !== void 0 ? {
|
|
4360
|
+
underline: {
|
|
4361
|
+
type: style.underline ? docx.UnderlineType.SINGLE : docx.UnderlineType.NONE
|
|
4362
|
+
}
|
|
4363
|
+
} : {},
|
|
4364
|
+
...style.strike !== void 0 ? { strike: style.strike } : {}
|
|
4365
|
+
};
|
|
4366
|
+
}
|
|
4367
|
+
function docxTextChildren(text, docx) {
|
|
4368
|
+
const children2 = [];
|
|
4369
|
+
let start = 0;
|
|
4370
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
4371
|
+
const character = text[index];
|
|
4372
|
+
if (character !== " " && character !== "\n") continue;
|
|
4373
|
+
if (index > start) children2.push(text.slice(start, index));
|
|
4374
|
+
children2.push(character === " " ? new docx.Tab() : new docx.CarriageReturn());
|
|
4375
|
+
start = index + 1;
|
|
4376
|
+
}
|
|
4377
|
+
if (start < text.length) children2.push(text.slice(start));
|
|
4378
|
+
return children2;
|
|
4379
|
+
}
|
|
4380
|
+
function numberingConfig(docx) {
|
|
4381
|
+
const makeLevels = (kind) => Array.from({ length: 9 }, (_, level) => ({
|
|
4382
|
+
level,
|
|
4383
|
+
format: kind === "bullet" ? docx.LevelFormat.BULLET : docx.LevelFormat.DECIMAL,
|
|
4384
|
+
text: kind === "bullet" ? ["\u2022", "\u25E6", "\u25AA"][level % 3] : `%${level + 1}.`,
|
|
4385
|
+
alignment: docx.AlignmentType.LEFT,
|
|
4386
|
+
style: { paragraph: { indent: { left: 720 * (level + 1), hanging: 360 } } }
|
|
4387
|
+
}));
|
|
4388
|
+
return [
|
|
4389
|
+
{ reference: "opengeni-bullet", levels: makeLevels("bullet") },
|
|
4390
|
+
{ reference: "opengeni-number", levels: makeLevels("number") }
|
|
4391
|
+
];
|
|
4392
|
+
}
|
|
4393
|
+
function buildListInstances(document) {
|
|
4394
|
+
const instances = /* @__PURE__ */ new Map();
|
|
4395
|
+
const explicitInstances = /* @__PURE__ */ new Map();
|
|
4396
|
+
let instance = 0;
|
|
4397
|
+
const visit = (blocks) => {
|
|
4398
|
+
let activeKind;
|
|
4399
|
+
for (const block of blocks) {
|
|
4400
|
+
if (!(block instanceof DocumentParagraph) || !block.style.list) {
|
|
4401
|
+
activeKind = void 0;
|
|
4402
|
+
continue;
|
|
4403
|
+
}
|
|
4404
|
+
const explicitId = block.style.list.instanceId;
|
|
4405
|
+
if (explicitId) {
|
|
4406
|
+
const key = `${block.style.list.kind}:${explicitId}`;
|
|
4407
|
+
let explicitInstance = explicitInstances.get(key);
|
|
4408
|
+
if (explicitInstance === void 0) {
|
|
4409
|
+
explicitInstance = ++instance;
|
|
4410
|
+
explicitInstances.set(key, explicitInstance);
|
|
4411
|
+
}
|
|
4412
|
+
instances.set(block.id, explicitInstance);
|
|
4413
|
+
activeKind = void 0;
|
|
4414
|
+
continue;
|
|
4415
|
+
}
|
|
4416
|
+
if (activeKind !== block.style.list.kind) instance += 1;
|
|
4417
|
+
activeKind = block.style.list.kind;
|
|
4418
|
+
instances.set(block.id, instance);
|
|
4419
|
+
}
|
|
4420
|
+
};
|
|
4421
|
+
visit(document.blocks.items);
|
|
4422
|
+
for (const section of document.sections.items)
|
|
4423
|
+
for (const story of section.allStories()) visit(story.items);
|
|
4424
|
+
return instances;
|
|
4425
|
+
}
|
|
4426
|
+
function headingLevel(level, docx) {
|
|
4427
|
+
if (!level) return void 0;
|
|
4428
|
+
return [
|
|
4429
|
+
docx.HeadingLevel.HEADING_1,
|
|
4430
|
+
docx.HeadingLevel.HEADING_2,
|
|
4431
|
+
docx.HeadingLevel.HEADING_3,
|
|
4432
|
+
docx.HeadingLevel.HEADING_4,
|
|
4433
|
+
docx.HeadingLevel.HEADING_5,
|
|
4434
|
+
docx.HeadingLevel.HEADING_6
|
|
4435
|
+
][level - 1];
|
|
4436
|
+
}
|
|
4437
|
+
function alignment(value, docx) {
|
|
4438
|
+
if (!value) return void 0;
|
|
4439
|
+
return {
|
|
4440
|
+
left: docx.AlignmentType.LEFT,
|
|
4441
|
+
center: docx.AlignmentType.CENTER,
|
|
4442
|
+
right: docx.AlignmentType.RIGHT,
|
|
4443
|
+
justify: docx.AlignmentType.JUSTIFIED
|
|
4444
|
+
}[value];
|
|
4445
|
+
}
|
|
4446
|
+
function validateNonOverlappingChanges(changes) {
|
|
4447
|
+
const sorted = [...changes].sort((a, b) => a.start - b.start || a.end - b.end);
|
|
4448
|
+
for (let index = 1; index < sorted.length; index++) {
|
|
4449
|
+
if (sorted[index].start < sorted[index - 1].end) {
|
|
4450
|
+
throw new UnsupportedArtifactFeatureError(
|
|
4451
|
+
"document",
|
|
4452
|
+
"overlapping tracked changes",
|
|
4453
|
+
"DOCX export"
|
|
4454
|
+
);
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
function validateNonCrossingComments(comments) {
|
|
4459
|
+
const sorted = [...comments].sort(
|
|
4460
|
+
(left, right) => left.start - right.start || right.end - left.end
|
|
4461
|
+
);
|
|
4462
|
+
const activeEnds = [];
|
|
4463
|
+
for (const comment of sorted) {
|
|
4464
|
+
while (activeEnds.length > 0 && comment.start >= activeEnds[activeEnds.length - 1]) {
|
|
4465
|
+
activeEnds.pop();
|
|
4466
|
+
}
|
|
4467
|
+
const enclosingEnd = activeEnds[activeEnds.length - 1];
|
|
4468
|
+
if (enclosingEnd !== void 0 && comment.end > enclosingEnd) {
|
|
4469
|
+
throw new UnsupportedArtifactFeatureError(
|
|
4470
|
+
"document",
|
|
4471
|
+
"crossing comment ranges",
|
|
4472
|
+
"DOCX export"
|
|
4473
|
+
);
|
|
4474
|
+
}
|
|
4475
|
+
if (comment.end > comment.start) activeEnds.push(comment.end);
|
|
4476
|
+
}
|
|
4477
|
+
}
|
|
4478
|
+
function validateReviewMarkupForDocx(paragraph, comments, changes) {
|
|
4479
|
+
const runSlices = paragraphRunSlices(paragraph);
|
|
4480
|
+
const sortedChanges = [...changes].sort(
|
|
4481
|
+
(left, right) => left.start - right.start || left.end - right.end
|
|
4482
|
+
);
|
|
4483
|
+
let runIndex = 0;
|
|
4484
|
+
for (const change of sortedChanges) {
|
|
4485
|
+
while (runSlices[runIndex] && runSlices[runIndex].end <= change.start) runIndex += 1;
|
|
4486
|
+
let scanIndex = runIndex;
|
|
4487
|
+
let firstStyle;
|
|
4488
|
+
while (runSlices[scanIndex] && runSlices[scanIndex].start < change.end) {
|
|
4489
|
+
const style = runSlices[scanIndex].run.style;
|
|
4490
|
+
firstStyle ??= style;
|
|
4491
|
+
if (!sameTextStyle(style, firstStyle)) {
|
|
4492
|
+
throw new UnsupportedArtifactFeatureError(
|
|
4493
|
+
"document",
|
|
4494
|
+
"one tracked change spanning multiple text styles",
|
|
4495
|
+
"DOCX export"
|
|
4496
|
+
);
|
|
4497
|
+
}
|
|
4498
|
+
scanIndex += 1;
|
|
4499
|
+
}
|
|
4500
|
+
}
|
|
4501
|
+
const sortedComments = [...comments].sort(
|
|
4502
|
+
(left, right) => left.start - right.start || left.end - right.end
|
|
4503
|
+
);
|
|
4504
|
+
let changeIndex = 0;
|
|
4505
|
+
for (const comment of sortedComments) {
|
|
4506
|
+
while (sortedChanges[changeIndex] && sortedChanges[changeIndex].end <= comment.start) {
|
|
4507
|
+
changeIndex += 1;
|
|
4508
|
+
}
|
|
4509
|
+
const change = sortedChanges[changeIndex];
|
|
4510
|
+
if (!change) continue;
|
|
4511
|
+
const pointInside = comment.start === comment.end && comment.start > change.start && comment.start < change.end;
|
|
4512
|
+
const rangesOverlap = comment.start < change.end && comment.end > change.start;
|
|
4513
|
+
const aligned = comment.start === change.start && comment.end === change.end;
|
|
4514
|
+
if (pointInside || rangesOverlap && !aligned) {
|
|
4515
|
+
throw new UnsupportedArtifactFeatureError(
|
|
4516
|
+
"document",
|
|
4517
|
+
"interleaved comments and tracked changes",
|
|
4518
|
+
"DOCX export"
|
|
4519
|
+
);
|
|
4520
|
+
}
|
|
4521
|
+
}
|
|
4522
|
+
}
|
|
4523
|
+
function paragraphRunSlices(paragraph) {
|
|
4524
|
+
const result = [];
|
|
4525
|
+
let offset = 0;
|
|
4526
|
+
for (const run of paragraph.runs) {
|
|
4527
|
+
const start = offset;
|
|
4528
|
+
offset += run.text.length;
|
|
4529
|
+
result.push({ start, end: offset, run });
|
|
4530
|
+
}
|
|
4531
|
+
return result;
|
|
4532
|
+
}
|
|
4533
|
+
function sameTextStyle(left, right) {
|
|
4534
|
+
return left.fontFamily === right.fontFamily && left.fontSizePt === right.fontSizePt && left.color === right.color && left.bold === right.bold && left.italic === right.italic && left.underline === right.underline && left.strike === right.strike;
|
|
4535
|
+
}
|
|
4536
|
+
var MAX_DOCX_SOURCE_BYTES = 32 * 1024 * 1024;
|
|
4537
|
+
function validateImportOptions(options) {
|
|
4538
|
+
assertPlainDataRecord(options, /* @__PURE__ */ new Set(["unsupportedContent", "limits"]), "DOCX import options");
|
|
4539
|
+
if (options.unsupportedContent !== void 0 && options.unsupportedContent !== "preserve" && options.unsupportedContent !== "error")
|
|
4540
|
+
throw new TypeError("DOCX unsupportedContent import policy is invalid");
|
|
4541
|
+
if (options.limits !== void 0) {
|
|
4542
|
+
assertPlainDataRecord(options.limits, void 0, "DOCX import limits");
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4545
|
+
function validateExportOptions(options) {
|
|
4546
|
+
assertPlainDataRecord(
|
|
4547
|
+
options,
|
|
4548
|
+
/* @__PURE__ */ new Set(["fileName", "unsupportedContent"]),
|
|
4549
|
+
"DOCX export options"
|
|
4550
|
+
);
|
|
4551
|
+
if (options.unsupportedContent !== void 0 && options.unsupportedContent !== "error" && options.unsupportedContent !== "discard")
|
|
4552
|
+
throw new TypeError("DOCX unsupportedContent export policy is invalid");
|
|
4553
|
+
if (options.fileName !== void 0 && (typeof options.fileName !== "string" || options.fileName.length === 0 || options.fileName.length > 255))
|
|
4554
|
+
throw new TypeError("DOCX export fileName must contain 1 through 255 characters");
|
|
4555
|
+
}
|
|
4556
|
+
function validateAttachOptions(options) {
|
|
4557
|
+
assertPlainDataRecord(options, /* @__PURE__ */ new Set(["limits"]), "DOCX envelope attach options");
|
|
4558
|
+
if (options.limits !== void 0) {
|
|
4559
|
+
assertPlainDataRecord(options.limits, void 0, "DOCX import limits");
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
function assertPlainDataRecord(value, allowedKeys, label) {
|
|
4563
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype)
|
|
4564
|
+
throw new TypeError(`${label} must be a plain data object`);
|
|
4565
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
4566
|
+
throw new TypeError(`${label} must not contain symbol properties`);
|
|
4567
|
+
}
|
|
4568
|
+
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) {
|
|
4569
|
+
if (!("value" in descriptor) || !descriptor.enumerable) {
|
|
4570
|
+
throw new TypeError(`${label} must contain only enumerable data properties`);
|
|
4571
|
+
}
|
|
4572
|
+
if (allowedKeys && !allowedKeys.has(key))
|
|
4573
|
+
throw new TypeError(`${label} contains unknown property ${key}`);
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4576
|
+
async function ownedDocxBytes(input) {
|
|
4577
|
+
if (input instanceof ArrayBuffer) {
|
|
4578
|
+
if (input.byteLength > MAX_DOCX_SOURCE_BYTES) {
|
|
4579
|
+
throw new DocxImportError("limit_exceeded", "Compressed DOCX exceeds maxCompressedBytes");
|
|
4580
|
+
}
|
|
4581
|
+
return new Uint8Array(input.slice(0));
|
|
4582
|
+
}
|
|
4583
|
+
if (typeof Blob !== "undefined" && input instanceof Blob) {
|
|
4584
|
+
const size = Object.getOwnPropertyDescriptor(Blob.prototype, "size")?.get?.call(input);
|
|
4585
|
+
if (typeof size !== "number" || size > MAX_DOCX_SOURCE_BYTES) {
|
|
4586
|
+
throw new DocxImportError("limit_exceeded", "Compressed DOCX exceeds maxCompressedBytes");
|
|
4587
|
+
}
|
|
4588
|
+
return new Uint8Array(await Blob.prototype.arrayBuffer.call(input));
|
|
4589
|
+
}
|
|
4590
|
+
throw new DocxImportError("invalid_input", "DOCX input must be a Blob or ArrayBuffer");
|
|
4591
|
+
}
|
|
4592
|
+
function ownedArrayBuffer(bytes) {
|
|
4593
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
4594
|
+
copy.set(bytes);
|
|
4595
|
+
return copy.buffer;
|
|
4596
|
+
}
|
|
4597
|
+
function deterministicCoreProperties(revision) {
|
|
4598
|
+
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/"><dc:creator>OpenGeni</dc:creator><cp:lastModifiedBy>OpenGeni</cp:lastModifiedBy><cp:revision>${revision}</cp:revision></cp:coreProperties>`;
|
|
4599
|
+
}
|
|
4600
|
+
function hasOpaqueContent(content) {
|
|
4601
|
+
return content.parts.length > 0 || content.relationships.length > 0 || content.contentTypes.length > 0;
|
|
4602
|
+
}
|
|
4603
|
+
function opaqueContentIssue(content, severity) {
|
|
4604
|
+
return {
|
|
4605
|
+
code: severity === "warning" ? "content-preserved-in-source" : "content-will-be-discarded",
|
|
4606
|
+
severity,
|
|
4607
|
+
feature: "opaque-ooxml",
|
|
4608
|
+
message: severity === "warning" ? "The DOCX contains bounded inert OOXML outside the editable model; unchanged export preserves the package byte-for-byte" : "The DOCX contains bounded inert OOXML outside the editable model and cannot be imported under the requested strict fidelity policy",
|
|
4609
|
+
...cloneOpaqueContent(content)
|
|
4610
|
+
};
|
|
4611
|
+
}
|
|
4612
|
+
function cloneOpaqueContent(content) {
|
|
4613
|
+
return {
|
|
4614
|
+
parts: [...content.parts],
|
|
4615
|
+
relationships: content.relationships.map((relationship) => ({ ...relationship })),
|
|
4616
|
+
contentTypes: content.contentTypes.map((contentType) => ({ ...contentType }))
|
|
4617
|
+
};
|
|
4618
|
+
}
|
|
4619
|
+
function sameOpaqueContent(left, right) {
|
|
4620
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
4621
|
+
}
|
|
4622
|
+
function normalizeLossPreservationEnvelope(value) {
|
|
4623
|
+
assertPlainDataRecord(
|
|
4624
|
+
value,
|
|
4625
|
+
/* @__PURE__ */ new Set([
|
|
4626
|
+
"version",
|
|
4627
|
+
"mediaType",
|
|
4628
|
+
"sourceBytes",
|
|
4629
|
+
"sourceDigest",
|
|
4630
|
+
"opaqueContent",
|
|
4631
|
+
"modelDigest"
|
|
4632
|
+
]),
|
|
4633
|
+
"DOCX loss-preservation envelope"
|
|
4634
|
+
);
|
|
4635
|
+
if (value.version !== 1 || value.mediaType !== DOCX_MEDIA_TYPE) {
|
|
4636
|
+
throw new TypeError("DOCX loss-preservation envelope version or media type is invalid");
|
|
4637
|
+
}
|
|
4638
|
+
if (!(value.sourceBytes instanceof Uint8Array) || Object.getPrototypeOf(value.sourceBytes) !== Uint8Array.prototype || value.sourceBytes.byteLength > MAX_DOCX_SOURCE_BYTES)
|
|
4639
|
+
throw new TypeError("DOCX loss-preservation sourceBytes must be a bounded Uint8Array");
|
|
4640
|
+
if (typeof value.sourceDigest !== "string" || !/^[0-9a-f]{64}$/.test(value.sourceDigest)) {
|
|
4641
|
+
throw new TypeError("DOCX loss-preservation sourceDigest must be canonical SHA-256");
|
|
4642
|
+
}
|
|
4643
|
+
if (typeof value.modelDigest !== "string" || !/^[0-9a-f]{64}$/.test(value.modelDigest)) {
|
|
4644
|
+
throw new TypeError("DOCX loss-preservation modelDigest must be canonical SHA-256");
|
|
4645
|
+
}
|
|
4646
|
+
const opaqueContent = normalizeOpaqueContent(value.opaqueContent);
|
|
4647
|
+
if (!hasOpaqueContent(opaqueContent)) {
|
|
4648
|
+
throw new TypeError("DOCX loss-preservation envelope contains no opaque content");
|
|
4649
|
+
}
|
|
4650
|
+
return {
|
|
4651
|
+
version: 1,
|
|
4652
|
+
mediaType: DOCX_MEDIA_TYPE,
|
|
4653
|
+
sourceBytes: value.sourceBytes.slice(),
|
|
4654
|
+
sourceDigest: value.sourceDigest,
|
|
4655
|
+
opaqueContent,
|
|
4656
|
+
modelDigest: value.modelDigest
|
|
4657
|
+
};
|
|
4658
|
+
}
|
|
4659
|
+
function normalizeOpaqueContent(value) {
|
|
4660
|
+
assertPlainDataRecord(
|
|
4661
|
+
value,
|
|
4662
|
+
/* @__PURE__ */ new Set(["parts", "relationships", "contentTypes"]),
|
|
4663
|
+
"DOCX opaque-content metadata"
|
|
4664
|
+
);
|
|
4665
|
+
const parts = denseDataArray(value.parts, 1024, "DOCX opaque part list").map((part) => {
|
|
4666
|
+
if (typeof part !== "string" || part.length === 0 || part.length > 1025) {
|
|
4667
|
+
throw new TypeError("DOCX opaque part name is invalid");
|
|
4668
|
+
}
|
|
4669
|
+
return part;
|
|
4670
|
+
});
|
|
4671
|
+
const relationships = denseDataArray(
|
|
4672
|
+
value.relationships,
|
|
4673
|
+
1e5,
|
|
4674
|
+
"DOCX opaque relationship list"
|
|
4675
|
+
).map((relationship) => {
|
|
4676
|
+
assertPlainDataRecord(
|
|
4677
|
+
relationship,
|
|
4678
|
+
/* @__PURE__ */ new Set(["sourcePart", "type", "targetPart"]),
|
|
4679
|
+
"DOCX opaque relationship"
|
|
4680
|
+
);
|
|
4681
|
+
const { sourcePart, type, targetPart } = relationship;
|
|
4682
|
+
if (typeof sourcePart !== "string" || sourcePart.length > 1025 || typeof type !== "string" || type.length === 0 || type.length > 2048 || typeof targetPart !== "string" || targetPart.length === 0 || targetPart.length > 1025)
|
|
4683
|
+
throw new TypeError("DOCX opaque relationship metadata is invalid");
|
|
4684
|
+
return { sourcePart, type, targetPart };
|
|
4685
|
+
});
|
|
4686
|
+
const contentTypes = denseDataArray(
|
|
4687
|
+
value.contentTypes,
|
|
4688
|
+
1024,
|
|
4689
|
+
"DOCX opaque content-type list"
|
|
4690
|
+
).map((contentType) => {
|
|
4691
|
+
assertPlainDataRecord(
|
|
4692
|
+
contentType,
|
|
4693
|
+
/* @__PURE__ */ new Set(["partName", "contentType"]),
|
|
4694
|
+
"DOCX opaque content type"
|
|
4695
|
+
);
|
|
4696
|
+
if (typeof contentType.partName !== "string" || contentType.partName.length === 0 || contentType.partName.length > 1025 || typeof contentType.contentType !== "string" || contentType.contentType.length === 0 || contentType.contentType.length > 2048)
|
|
4697
|
+
throw new TypeError("DOCX opaque content-type metadata is invalid");
|
|
4698
|
+
return { partName: contentType.partName, contentType: contentType.contentType };
|
|
4699
|
+
});
|
|
4700
|
+
return { parts, relationships, contentTypes };
|
|
4701
|
+
}
|
|
4702
|
+
function denseDataArray(value, maximum, label) {
|
|
4703
|
+
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype || value.length > maximum) {
|
|
4704
|
+
throw new TypeError(`${label} must be a bounded ordinary array`);
|
|
4705
|
+
}
|
|
4706
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
4707
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
4708
|
+
const descriptor = descriptors[String(index)];
|
|
4709
|
+
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
|
|
4710
|
+
throw new TypeError(`${label} must contain only dense data elements`);
|
|
4711
|
+
}
|
|
4712
|
+
}
|
|
4713
|
+
for (const key of Object.keys(descriptors)) {
|
|
4714
|
+
if (key !== "length" && !/^(?:0|[1-9][0-9]*)$/.test(key)) {
|
|
4715
|
+
throw new TypeError(`${label} contains an unknown property`);
|
|
4716
|
+
}
|
|
4717
|
+
}
|
|
4718
|
+
return value.slice();
|
|
4719
|
+
}
|
|
4720
|
+
async function documentModelDigest(document) {
|
|
4721
|
+
return sha256Hex(new TextEncoder().encode(JSON.stringify(document.toJSON())));
|
|
4722
|
+
}
|
|
4723
|
+
async function sha256Hex(bytes) {
|
|
4724
|
+
const ownedBytes = new Uint8Array(bytes.byteLength);
|
|
4725
|
+
ownedBytes.set(bytes);
|
|
4726
|
+
const digest = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", ownedBytes));
|
|
4727
|
+
return [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4728
|
+
}
|
|
4729
|
+
async function semanticNamespace(value) {
|
|
4730
|
+
const { opaqueContent: _opaqueContent, ...semanticValue } = value;
|
|
4731
|
+
const canonicalBytes = new TextEncoder().encode(JSON.stringify(semanticValue));
|
|
4732
|
+
const digest = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", canonicalBytes));
|
|
4733
|
+
return [...digest.subarray(0, 8)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4734
|
+
}
|
|
4735
|
+
function appendMap(map, key, value) {
|
|
4736
|
+
const values = map.get(key);
|
|
4737
|
+
if (values) values.push(value);
|
|
4738
|
+
else map.set(key, [value]);
|
|
4739
|
+
}
|
|
4740
|
+
function initials(author) {
|
|
4741
|
+
const value = author.trim().split(/\s+/u).slice(0, 2).map((part) => Array.from(part)[0]?.toUpperCase() ?? "").join("");
|
|
4742
|
+
return Array.from(value).slice(0, 8).join("") || "U";
|
|
4743
|
+
}
|
|
4744
|
+
function assertRepresentableCommentInitials(author, value) {
|
|
4745
|
+
if (value !== void 0 && value !== initials(author)) {
|
|
4746
|
+
throw new UnsupportedArtifactFeatureError("document", "custom comment initials", "DOCX import");
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
function ptToTwip(value) {
|
|
4750
|
+
return Math.round(value * 20);
|
|
4751
|
+
}
|
|
4752
|
+
function normalizeHex(value) {
|
|
4753
|
+
if (!/^#?[0-9A-Fa-f]{6}$/.test(value)) throw new Error(`Invalid or unsafe color: ${value}`);
|
|
4754
|
+
return `#${value.replace(/^#/, "").toUpperCase()}`;
|
|
4755
|
+
}
|
|
4756
|
+
export {
|
|
4757
|
+
DOCX_MEDIA_TYPE,
|
|
4758
|
+
DocumentFidelityError,
|
|
4759
|
+
attachLossPreservationEnvelope,
|
|
4760
|
+
exportDocx,
|
|
4761
|
+
fidelityReport,
|
|
4762
|
+
importDocx2 as importDocx,
|
|
4763
|
+
lossPreservationEnvelope
|
|
4764
|
+
};
|
|
4765
|
+
//# sourceMappingURL=document-docx-codec.js.map
|