@stll/anonymize-docx 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ATTRIBUTION.md +0 -2
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +149 -642
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
package/ATTRIBUTION.md
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -182,6 +182,7 @@ declare const DOCX_RESTORATION_ERROR_CODES: {
|
|
|
182
182
|
readonly invalidSession: "invalid-session";
|
|
183
183
|
readonly restorationLimitExceeded: "restoration-limit-exceeded";
|
|
184
184
|
readonly sessionMismatch: "session-mismatch";
|
|
185
|
+
readonly unsupportedDocument: "unsupported-document";
|
|
185
186
|
};
|
|
186
187
|
type DocxRestorationErrorCode = (typeof DOCX_RESTORATION_ERROR_CODES)[keyof typeof DOCX_RESTORATION_ERROR_CODES];
|
|
187
188
|
declare const DOCX_REWRITE_ERROR_CODES: {
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { SaxesParser } from "saxes";
|
|
1
|
+
import { loadNativeAnonymizeBinding } from "@stll/anonymize";
|
|
3
2
|
//#region src/types.ts
|
|
4
3
|
const DOCX_PART_TYPES = {
|
|
5
4
|
comments: "comments",
|
|
@@ -22,7 +21,8 @@ const DOCX_RESTORATION_ERROR_CODES = {
|
|
|
22
21
|
invalidPlaceholder: "invalid-placeholder",
|
|
23
22
|
invalidSession: "invalid-session",
|
|
24
23
|
restorationLimitExceeded: "restoration-limit-exceeded",
|
|
25
|
-
sessionMismatch: "session-mismatch"
|
|
24
|
+
sessionMismatch: "session-mismatch",
|
|
25
|
+
unsupportedDocument: "unsupported-document"
|
|
26
26
|
};
|
|
27
27
|
const DOCX_REWRITE_ERROR_CODES = {
|
|
28
28
|
invalidReplacement: "invalid-replacement",
|
|
@@ -45,26 +45,6 @@ const DOCX_ARCHIVE_MAX_BYTES = 64 * 1024 * 1024;
|
|
|
45
45
|
const DOCX_ENTRY_MAX_BYTES = 16 * 1024 * 1024;
|
|
46
46
|
const DOCX_UNCOMPRESSED_MAX_BYTES = 128 * 1024 * 1024;
|
|
47
47
|
const DOCX_XML_MAX_DEPTH = 256;
|
|
48
|
-
const DOCX_MAX_ENTRIES = 4096;
|
|
49
|
-
const DOCX_MAX_TEXT_BLOCKS = 1e5;
|
|
50
|
-
const DOCX_MAX_TEXT_SEGMENTS = 1e6;
|
|
51
|
-
const CONTENT_TYPES_PATH = "[Content_Types].xml";
|
|
52
|
-
const ROOT_RELATIONSHIPS_PATH = "_rels/.rels";
|
|
53
|
-
const CONTENT_TYPES_NAMESPACE = "http://schemas.openxmlformats.org/package/2006/content-types";
|
|
54
|
-
const PACKAGE_RELATIONSHIP_NAMESPACES = /* @__PURE__ */ new Set(["http://purl.oclc.org/ooxml/package/relationships", "http://schemas.openxmlformats.org/package/2006/relationships"]);
|
|
55
|
-
const WORDPROCESSING_CONTENT_TYPE_PREFIX = "application/vnd.openxmlformats-officedocument.wordprocessingml.";
|
|
56
|
-
const SUPPORTED_CONTENT_TYPE_SUFFIXES = {
|
|
57
|
-
"comments+xml": DOCX_PART_TYPES.comments,
|
|
58
|
-
"document.main+xml": DOCX_PART_TYPES.mainDocument,
|
|
59
|
-
"endnotes+xml": DOCX_PART_TYPES.endnotes,
|
|
60
|
-
"footer+xml": DOCX_PART_TYPES.footer,
|
|
61
|
-
"footnotes+xml": DOCX_PART_TYPES.footnotes,
|
|
62
|
-
"header+xml": DOCX_PART_TYPES.header
|
|
63
|
-
};
|
|
64
|
-
const WORDPROCESSING_NAMESPACES$1 = /* @__PURE__ */ new Set(["http://purl.oclc.org/ooxml/wordprocessingml/main", "http://schemas.openxmlformats.org/wordprocessingml/2006/main"]);
|
|
65
|
-
const RELATIONSHIP_NAMESPACES = /* @__PURE__ */ new Set(["http://purl.oclc.org/ooxml/officeDocument/relationships", "http://schemas.openxmlformats.org/officeDocument/2006/relationships"]);
|
|
66
|
-
const OFFICE_DOCUMENT_RELATIONSHIP_TYPES = new Set([...RELATIONSHIP_NAMESPACES].map((namespace) => `${namespace}/officeDocument`));
|
|
67
|
-
const MARKUP_COMPATIBILITY_NAMESPACES = /* @__PURE__ */ new Set(["http://purl.oclc.org/ooxml/markup-compatibility/main", "http://schemas.openxmlformats.org/markup-compatibility/2006"]);
|
|
68
48
|
var DocxExtractionError = class extends Error {
|
|
69
49
|
code;
|
|
70
50
|
constructor(code, message) {
|
|
@@ -73,388 +53,26 @@ var DocxExtractionError = class extends Error {
|
|
|
73
53
|
this.code = code;
|
|
74
54
|
}
|
|
75
55
|
};
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
budget.entryCount += 1;
|
|
84
|
-
if (budget.entryCount > DOCX_MAX_ENTRIES) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded, `DOCX archives must contain at most ${DOCX_MAX_ENTRIES} entries`);
|
|
85
|
-
if (!safeEntryPath(file.name)) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.unsafeEntryPath, "DOCX archive contains an unsafe entry path");
|
|
86
|
-
if (file.originalSize > 16777216) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded, `DOCX entries must not exceed ${DOCX_ENTRY_MAX_BYTES} bytes`);
|
|
87
|
-
budget.uncompressedBytes += file.originalSize;
|
|
88
|
-
if (budget.uncompressedBytes > 134217728) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded, `DOCX archives must not exceed ${DOCX_UNCOMPRESSED_MAX_BYTES} uncompressed bytes`);
|
|
89
|
-
return includeAllEntries || file.name === CONTENT_TYPES_PATH || file.name === ROOT_RELATIONSHIPS_PATH || file.name.startsWith("word/") && file.name.endsWith(".xml");
|
|
90
|
-
};
|
|
91
|
-
const unzipDocxArchive = (archive, includeAllEntries = false) => {
|
|
92
|
-
if (archive.byteLength > 67108864) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.archiveLimitExceeded, `DOCX archives must not exceed ${DOCX_ARCHIVE_MAX_BYTES} bytes`);
|
|
93
|
-
const budget = {
|
|
94
|
-
entryCount: 0,
|
|
95
|
-
uncompressedBytes: 0
|
|
96
|
-
};
|
|
97
|
-
try {
|
|
98
|
-
return unzipSync(archive, { filter: (file) => archiveFilter({
|
|
99
|
-
budget,
|
|
100
|
-
file,
|
|
101
|
-
includeAllEntries
|
|
102
|
-
}) });
|
|
103
|
-
} catch (error) {
|
|
104
|
-
if (error instanceof DocxExtractionError) throw error;
|
|
105
|
-
throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.invalidArchive, "Input is not a valid bounded DOCX ZIP archive");
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
const decodeXml = (bytes, path) => {
|
|
109
|
-
try {
|
|
110
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
111
|
-
} catch {
|
|
112
|
-
throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.invalidXml, `DOCX XML part is not valid UTF-8: ${path}`);
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
const attributeByLocalName = (tag, localName, namespaces) => {
|
|
116
|
-
for (const attribute of Object.values(tag.attributes)) if (attribute.local === localName && (namespaces === void 0 || namespaces.has(attribute.uri))) return attribute.value;
|
|
117
|
-
return null;
|
|
56
|
+
const nativeExtractionErrorCode = (message) => {
|
|
57
|
+
if (message.includes("unsafe entry path")) return DOCX_EXTRACTION_ERROR_CODES.unsafeEntryPath;
|
|
58
|
+
if (message.includes("valid bounded DOCX ZIP archive")) return DOCX_EXTRACTION_ERROR_CODES.invalidArchive;
|
|
59
|
+
if (message.includes("valid XML") || message.includes("valid UTF-8")) return DOCX_EXTRACTION_ERROR_CODES.invalidXml;
|
|
60
|
+
if (message.includes(`DOCX archives must not exceed 67108864 bytes`)) return DOCX_EXTRACTION_ERROR_CODES.archiveLimitExceeded;
|
|
61
|
+
if (message.includes("must not exceed") || message.includes("must not contain more than") || message.includes("at most")) return DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded;
|
|
62
|
+
return DOCX_EXTRACTION_ERROR_CODES.invalidPackage;
|
|
118
63
|
};
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
const parser = new SaxesParser({ xmlns: true });
|
|
123
|
-
let parseError = null;
|
|
124
|
-
let depth = 0;
|
|
125
|
-
parser.on("error", (error) => {
|
|
126
|
-
parseError = error;
|
|
127
|
-
});
|
|
128
|
-
parser.on("doctype", () => {
|
|
129
|
-
throw invalidPackage("DOCX XML must not contain a document type declaration");
|
|
130
|
-
});
|
|
131
|
-
parser.on("opentag", (tag) => {
|
|
132
|
-
assertXmlDepth(depth);
|
|
133
|
-
depth += 1;
|
|
134
|
-
if (tag.local !== "Override" || tag.uri !== CONTENT_TYPES_NAMESPACE) return;
|
|
135
|
-
const rawPath = attributeByLocalName(tag, "PartName");
|
|
136
|
-
const contentType = attributeByLocalName(tag, "ContentType");
|
|
137
|
-
if (rawPath === null || contentType === null) throw invalidPackage("DOCX content-type override is incomplete");
|
|
138
|
-
const path = rawPath.startsWith("/") ? rawPath.slice(1) : rawPath;
|
|
139
|
-
if (!safeEntryPath(path)) throw invalidPackage("DOCX content-type override has an unsafe path");
|
|
140
|
-
if (paths.has(path)) throw invalidPackage("DOCX content-type overrides must have unique paths");
|
|
141
|
-
paths.add(path);
|
|
142
|
-
parts.push({
|
|
143
|
-
path,
|
|
144
|
-
contentType
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
parser.on("closetag", () => {
|
|
148
|
-
depth -= 1;
|
|
149
|
-
});
|
|
150
|
-
try {
|
|
151
|
-
parser.write(xml).close();
|
|
152
|
-
} catch (error) {
|
|
153
|
-
if (error instanceof DocxExtractionError) throw error;
|
|
154
|
-
parseError = error instanceof Error ? error : /* @__PURE__ */ new Error("invalid XML");
|
|
155
|
-
}
|
|
156
|
-
if (parseError !== null) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.invalidXml, "DOCX content types are not valid XML");
|
|
157
|
-
return parts;
|
|
158
|
-
};
|
|
159
|
-
const parseMainDocumentTarget = (xml) => {
|
|
160
|
-
const targets = [];
|
|
161
|
-
const parser = new SaxesParser({ xmlns: true });
|
|
162
|
-
let parseError = null;
|
|
163
|
-
let depth = 0;
|
|
164
|
-
parser.on("error", (error) => {
|
|
165
|
-
parseError = error;
|
|
166
|
-
});
|
|
167
|
-
parser.on("doctype", () => {
|
|
168
|
-
throw invalidPackage("DOCX XML must not contain a document type declaration");
|
|
169
|
-
});
|
|
170
|
-
parser.on("opentag", (tag) => {
|
|
171
|
-
assertXmlDepth(depth);
|
|
172
|
-
depth += 1;
|
|
173
|
-
if (tag.local !== "Relationship" || !PACKAGE_RELATIONSHIP_NAMESPACES.has(tag.uri)) return;
|
|
174
|
-
const type = attributeByLocalName(tag, "Type");
|
|
175
|
-
if (type === null || !OFFICE_DOCUMENT_RELATIONSHIP_TYPES.has(type)) return;
|
|
176
|
-
const targetMode = attributeByLocalName(tag, "TargetMode");
|
|
177
|
-
const rawTarget = attributeByLocalName(tag, "Target");
|
|
178
|
-
if (targetMode === "External" || rawTarget === null) throw invalidPackage("DOCX main-document relationship must be internal");
|
|
179
|
-
const target = rawTarget.startsWith("/") ? rawTarget.slice(1) : rawTarget;
|
|
180
|
-
if (!safeEntryPath(target) || target.includes(":")) throw invalidPackage("DOCX main-document relationship has an unsafe target");
|
|
181
|
-
targets.push(target);
|
|
182
|
-
});
|
|
183
|
-
parser.on("closetag", () => {
|
|
184
|
-
depth -= 1;
|
|
185
|
-
});
|
|
186
|
-
try {
|
|
187
|
-
parser.write(xml).close();
|
|
188
|
-
} catch (error) {
|
|
189
|
-
if (error instanceof DocxExtractionError) throw error;
|
|
190
|
-
parseError = error instanceof Error ? error : /* @__PURE__ */ new Error("invalid XML");
|
|
191
|
-
}
|
|
192
|
-
if (parseError !== null) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.invalidXml, "DOCX root relationships are not valid XML");
|
|
193
|
-
if (targets.length !== 1) throw invalidPackage("DOCX archive must contain exactly one main-document relationship");
|
|
194
|
-
const target = targets.at(0);
|
|
195
|
-
if (target === void 0) throw invalidPackage("DOCX main-document relationship is unavailable");
|
|
196
|
-
return target;
|
|
197
|
-
};
|
|
198
|
-
const classifyPart = ({ contentType, path }) => {
|
|
199
|
-
if (!contentType.startsWith(WORDPROCESSING_CONTENT_TYPE_PREFIX)) return null;
|
|
200
|
-
const suffix = contentType.slice(63);
|
|
201
|
-
const type = SUPPORTED_CONTENT_TYPE_SUFFIXES[suffix];
|
|
202
|
-
return type === void 0 ? null : {
|
|
203
|
-
type,
|
|
204
|
-
path
|
|
205
|
-
};
|
|
206
|
-
};
|
|
207
|
-
const isWordTag = (tag, local) => tag.local === local && WORDPROCESSING_NAMESPACES$1.has(tag.uri);
|
|
208
|
-
const frameByLocalName = (stack, local) => {
|
|
209
|
-
for (let index = stack.length - 1; index >= 0; index -= 1) {
|
|
210
|
-
const frame = stack.at(index);
|
|
211
|
-
if (frame !== void 0 && isWordTag(frame.tag, local)) return frame;
|
|
212
|
-
}
|
|
213
|
-
return null;
|
|
214
|
-
};
|
|
215
|
-
const blockLocation = (part, blockIndex, paragraphPath, stack) => {
|
|
216
|
-
const textBox = frameByLocalName(stack, "txbxContent");
|
|
217
|
-
if (textBox !== null) return {
|
|
218
|
-
type: "text-box-paragraph",
|
|
219
|
-
part,
|
|
220
|
-
blockIndex,
|
|
221
|
-
xmlPath: paragraphPath,
|
|
222
|
-
textBoxPath: textBox.path
|
|
223
|
-
};
|
|
224
|
-
const cell = frameByLocalName(stack, "tc");
|
|
225
|
-
const row = frameByLocalName(stack, "tr");
|
|
226
|
-
const table = frameByLocalName(stack, "tbl");
|
|
227
|
-
if (cell !== null && row !== null && table !== null) return {
|
|
228
|
-
type: "table-cell-paragraph",
|
|
229
|
-
part,
|
|
230
|
-
blockIndex,
|
|
231
|
-
xmlPath: paragraphPath,
|
|
232
|
-
tablePath: table.path,
|
|
233
|
-
rowPath: row.path,
|
|
234
|
-
cellPath: cell.path
|
|
235
|
-
};
|
|
236
|
-
return {
|
|
237
|
-
type: "paragraph",
|
|
238
|
-
part,
|
|
239
|
-
blockIndex,
|
|
240
|
-
xmlPath: paragraphPath
|
|
241
|
-
};
|
|
242
|
-
};
|
|
243
|
-
const revisionForTag = (tag) => {
|
|
244
|
-
if (!WORDPROCESSING_NAMESPACES$1.has(tag.uri)) return null;
|
|
245
|
-
const revision = {
|
|
246
|
-
del: "deletion",
|
|
247
|
-
ins: "insertion",
|
|
248
|
-
moveFrom: "move-from",
|
|
249
|
-
moveTo: "move-to"
|
|
250
|
-
}[tag.local];
|
|
251
|
-
return revision === void 0 ? null : {
|
|
252
|
-
type: "revision",
|
|
253
|
-
revision
|
|
254
|
-
};
|
|
255
|
-
};
|
|
256
|
-
const inlineContexts = (stack) => {
|
|
257
|
-
const contexts = [];
|
|
258
|
-
for (const { tag } of stack) {
|
|
259
|
-
if (isWordTag(tag, "hyperlink")) contexts.push({
|
|
260
|
-
type: "hyperlink",
|
|
261
|
-
relationshipId: attributeByLocalName(tag, "id", RELATIONSHIP_NAMESPACES),
|
|
262
|
-
anchor: attributeByLocalName(tag, "anchor", WORDPROCESSING_NAMESPACES$1)
|
|
263
|
-
});
|
|
264
|
-
const revision = revisionForTag(tag);
|
|
265
|
-
if (revision !== null) contexts.push(revision);
|
|
266
|
-
}
|
|
267
|
-
return contexts;
|
|
268
|
-
};
|
|
269
|
-
const appendSegment = (block, budget, value, source, path, stack) => {
|
|
270
|
-
if (value.length === 0) return;
|
|
271
|
-
if (budget.segmentCount >= DOCX_MAX_TEXT_SEGMENTS) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded, `DOCX parts must not contain more than ${DOCX_MAX_TEXT_SEGMENTS} text segments`);
|
|
272
|
-
budget.segmentCount += 1;
|
|
273
|
-
const start = block.text.length;
|
|
274
|
-
block.text += value;
|
|
275
|
-
block.segments.push({
|
|
276
|
-
start,
|
|
277
|
-
end: block.text.length,
|
|
278
|
-
source,
|
|
279
|
-
contexts: inlineContexts(stack),
|
|
280
|
-
xmlPath: path
|
|
281
|
-
});
|
|
282
|
-
};
|
|
283
|
-
const extractPart = (part, xml) => {
|
|
284
|
-
const blocks = [];
|
|
285
|
-
const stack = [];
|
|
286
|
-
const blockStack = [];
|
|
287
|
-
let nextBlockIndex = 0;
|
|
288
|
-
let currentText = "";
|
|
289
|
-
let currentTextPath = null;
|
|
290
|
-
let parseError = null;
|
|
291
|
-
let unsupportedSymbolCount = 0;
|
|
292
|
-
let unsupportedFieldInstructionCount = 0;
|
|
293
|
-
let unsupportedAlternateContentCount = 0;
|
|
294
|
-
const textBudget = { segmentCount: 0 };
|
|
295
|
-
const parser = new SaxesParser({ xmlns: true });
|
|
296
|
-
parser.on("error", (error) => {
|
|
297
|
-
parseError = error;
|
|
298
|
-
});
|
|
299
|
-
parser.on("doctype", () => {
|
|
300
|
-
throw invalidPackage("DOCX XML must not contain a document type declaration");
|
|
301
|
-
});
|
|
302
|
-
parser.on("opentag", (tag) => {
|
|
303
|
-
assertXmlDepth(stack.length);
|
|
304
|
-
const parent = stack.at(-1);
|
|
305
|
-
const childIndex = parent?.nextChildIndex ?? 0;
|
|
306
|
-
if (parent !== void 0) parent.nextChildIndex += 1;
|
|
307
|
-
const path = [...parent?.path ?? [], childIndex];
|
|
308
|
-
if (isWordTag(tag, "p")) {
|
|
309
|
-
if (nextBlockIndex >= DOCX_MAX_TEXT_BLOCKS) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded, `DOCX parts must not contain more than ${DOCX_MAX_TEXT_BLOCKS} text blocks`);
|
|
310
|
-
blockStack.push({
|
|
311
|
-
text: "",
|
|
312
|
-
segments: [],
|
|
313
|
-
location: blockLocation(part, nextBlockIndex, path, stack)
|
|
314
|
-
});
|
|
315
|
-
nextBlockIndex += 1;
|
|
316
|
-
}
|
|
317
|
-
stack.push({
|
|
318
|
-
tag,
|
|
319
|
-
path,
|
|
320
|
-
nextChildIndex: 0
|
|
321
|
-
});
|
|
322
|
-
if (isWordTag(tag, "t") || isWordTag(tag, "delText")) {
|
|
323
|
-
currentText = "";
|
|
324
|
-
currentTextPath = path;
|
|
325
|
-
}
|
|
326
|
-
const currentBlock = blockStack.at(-1);
|
|
327
|
-
if (currentBlock !== void 0 && isWordTag(tag, "tab")) appendSegment(currentBlock, textBudget, " ", "tab", path, stack);
|
|
328
|
-
if (currentBlock !== void 0 && (isWordTag(tag, "br") || isWordTag(tag, "cr"))) appendSegment(currentBlock, textBudget, "\n", "break", path, stack);
|
|
329
|
-
if (isWordTag(tag, "sym")) unsupportedSymbolCount += 1;
|
|
330
|
-
if (isWordTag(tag, "instrText") || isWordTag(tag, "fldSimple")) unsupportedFieldInstructionCount += 1;
|
|
331
|
-
if (tag.local === "AlternateContent" && MARKUP_COMPATIBILITY_NAMESPACES.has(tag.uri)) unsupportedAlternateContentCount += 1;
|
|
332
|
-
});
|
|
333
|
-
parser.on("text", (text) => {
|
|
334
|
-
if (currentTextPath !== null) currentText += text;
|
|
335
|
-
});
|
|
336
|
-
parser.on("cdata", (text) => {
|
|
337
|
-
if (currentTextPath !== null) currentText += text;
|
|
338
|
-
});
|
|
339
|
-
parser.on("closetag", (tag) => {
|
|
340
|
-
const frame = stack.at(-1);
|
|
341
|
-
if (frame === void 0 || frame.tag !== tag) throw invalidPackage("DOCX XML element stack is inconsistent");
|
|
342
|
-
if (currentTextPath !== null && (isWordTag(tag, "t") || isWordTag(tag, "delText"))) {
|
|
343
|
-
const currentBlock = blockStack.at(-1);
|
|
344
|
-
if (currentBlock === void 0) {
|
|
345
|
-
if (currentText.length > 0) throw invalidPackage("DOCX text is outside a paragraph");
|
|
346
|
-
} else appendSegment(currentBlock, textBudget, currentText, "text", currentTextPath, stack);
|
|
347
|
-
currentText = "";
|
|
348
|
-
currentTextPath = null;
|
|
349
|
-
}
|
|
350
|
-
if (isWordTag(tag, "p")) {
|
|
351
|
-
const completedBlock = blockStack.pop();
|
|
352
|
-
if (completedBlock === void 0) throw invalidPackage("DOCX paragraph state is unavailable");
|
|
353
|
-
blocks.push(completedBlock);
|
|
354
|
-
}
|
|
355
|
-
stack.pop();
|
|
356
|
-
});
|
|
64
|
+
const extractDocxText = (archive) => {
|
|
65
|
+
const extract = loadNativeAnonymizeBinding().extractDocxTextJson;
|
|
66
|
+
if (extract === void 0) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.invalidPackage, "Native anonymize binding does not expose DOCX extraction");
|
|
357
67
|
try {
|
|
358
|
-
|
|
68
|
+
return JSON.parse(extract(archive));
|
|
359
69
|
} catch (error) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}
|
|
363
|
-
if (parseError !== null) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.invalidXml, `DOCX part is not valid XML: ${part.path}`);
|
|
364
|
-
blocks.sort((left, right) => left.location.blockIndex - right.location.blockIndex);
|
|
365
|
-
let hyperlinkTextSegmentCount = 0;
|
|
366
|
-
let revisionTextSegmentCount = 0;
|
|
367
|
-
for (const { segments } of blocks) for (const { contexts } of segments) {
|
|
368
|
-
if (contexts.some((context) => context.type === "hyperlink")) hyperlinkTextSegmentCount += 1;
|
|
369
|
-
if (contexts.some((context) => context.type === "revision")) revisionTextSegmentCount += 1;
|
|
70
|
+
const message = error instanceof Error ? error.message : "DOCX extraction failed";
|
|
71
|
+
throw new DocxExtractionError(nativeExtractionErrorCode(message), message);
|
|
370
72
|
}
|
|
371
|
-
return {
|
|
372
|
-
blocks,
|
|
373
|
-
hyperlinkTextSegmentCount,
|
|
374
|
-
revisionTextSegmentCount,
|
|
375
|
-
unsupportedAlternateContentCount,
|
|
376
|
-
unsupportedSymbolCount,
|
|
377
|
-
unsupportedFieldInstructionCount
|
|
378
|
-
};
|
|
379
|
-
};
|
|
380
|
-
const extractDocxText = (archive) => {
|
|
381
|
-
const entries = unzipDocxArchive(archive);
|
|
382
|
-
const contentTypesBytes = entries[CONTENT_TYPES_PATH];
|
|
383
|
-
if (contentTypesBytes === void 0) throw invalidPackage("DOCX archive is missing [Content_Types].xml");
|
|
384
|
-
const contentTypes = parseContentTypes(decodeXml(contentTypesBytes, CONTENT_TYPES_PATH));
|
|
385
|
-
const rootRelationshipsBytes = entries[ROOT_RELATIONSHIPS_PATH];
|
|
386
|
-
if (rootRelationshipsBytes === void 0) throw invalidPackage("DOCX archive is missing _rels/.rels");
|
|
387
|
-
const mainDocumentTarget = parseMainDocumentTarget(decodeXml(rootRelationshipsBytes, ROOT_RELATIONSHIPS_PATH));
|
|
388
|
-
const supportedParts = contentTypes.map(classifyPart).filter((part) => part !== null);
|
|
389
|
-
if (supportedParts.filter((part) => part.type === DOCX_PART_TYPES.mainDocument).length !== 1) throw invalidPackage("DOCX archive must contain exactly one main document");
|
|
390
|
-
if (supportedParts.find((part) => part.type === DOCX_PART_TYPES.mainDocument)?.path !== mainDocumentTarget) throw invalidPackage("DOCX main-document relationship and content type do not agree");
|
|
391
|
-
const blocks = [];
|
|
392
|
-
const coverageParts = [];
|
|
393
|
-
let hyperlinkTextSegmentCount = 0;
|
|
394
|
-
let revisionTextSegmentCount = 0;
|
|
395
|
-
let unsupportedSymbolCount = 0;
|
|
396
|
-
let unsupportedFieldInstructionCount = 0;
|
|
397
|
-
let unsupportedAlternateContentCount = 0;
|
|
398
|
-
let textSegmentCount = 0;
|
|
399
|
-
for (const part of supportedParts) {
|
|
400
|
-
const bytes = entries[part.path];
|
|
401
|
-
if (bytes === void 0) throw invalidPackage(`DOCX archive is missing declared part: ${part.path}`);
|
|
402
|
-
const extracted = extractPart(part, decodeXml(bytes, part.path));
|
|
403
|
-
if (blocks.length + extracted.blocks.length > DOCX_MAX_TEXT_BLOCKS) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded, `DOCX archives must not contain more than ${DOCX_MAX_TEXT_BLOCKS} text blocks`);
|
|
404
|
-
const extractedSegmentCount = extracted.blocks.reduce((count, block) => count + block.segments.length, 0);
|
|
405
|
-
if (textSegmentCount + extractedSegmentCount > DOCX_MAX_TEXT_SEGMENTS) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded, `DOCX archives must not contain more than ${DOCX_MAX_TEXT_SEGMENTS} text segments`);
|
|
406
|
-
textSegmentCount += extractedSegmentCount;
|
|
407
|
-
blocks.push(...extracted.blocks);
|
|
408
|
-
coverageParts.push({
|
|
409
|
-
status: "extracted",
|
|
410
|
-
part,
|
|
411
|
-
blockCount: extracted.blocks.length
|
|
412
|
-
});
|
|
413
|
-
hyperlinkTextSegmentCount += extracted.hyperlinkTextSegmentCount;
|
|
414
|
-
revisionTextSegmentCount += extracted.revisionTextSegmentCount;
|
|
415
|
-
unsupportedSymbolCount += extracted.unsupportedSymbolCount;
|
|
416
|
-
unsupportedFieldInstructionCount += extracted.unsupportedFieldInstructionCount;
|
|
417
|
-
unsupportedAlternateContentCount += extracted.unsupportedAlternateContentCount;
|
|
418
|
-
}
|
|
419
|
-
for (const { contentType, path } of contentTypes) if (contentType.startsWith(WORDPROCESSING_CONTENT_TYPE_PREFIX) && classifyPart({
|
|
420
|
-
contentType,
|
|
421
|
-
path
|
|
422
|
-
}) === null) coverageParts.push({
|
|
423
|
-
status: "unsupported",
|
|
424
|
-
path,
|
|
425
|
-
contentType,
|
|
426
|
-
reason: "WordprocessingML part type is not extracted"
|
|
427
|
-
});
|
|
428
|
-
return {
|
|
429
|
-
contractVersion: 1,
|
|
430
|
-
blocks,
|
|
431
|
-
coverage: {
|
|
432
|
-
parts: coverageParts,
|
|
433
|
-
hyperlinkTextSegmentCount,
|
|
434
|
-
revisionTextSegmentCount,
|
|
435
|
-
unsupportedAlternateContentCount,
|
|
436
|
-
unsupportedSymbolCount,
|
|
437
|
-
unsupportedFieldInstructionCount
|
|
438
|
-
}
|
|
439
|
-
};
|
|
440
73
|
};
|
|
441
74
|
//#endregion
|
|
442
|
-
//#region src/location.ts
|
|
443
|
-
const arraysEqual = (left, right) => left.length === right.length && left.every((value, index) => value === right.at(index));
|
|
444
|
-
const docxLocationsEqual = (left, right) => {
|
|
445
|
-
if (left.type !== right.type || left.part.type !== right.part.type || left.part.path !== right.part.path || left.blockIndex !== right.blockIndex || !arraysEqual(left.xmlPath, right.xmlPath)) return false;
|
|
446
|
-
if (left.type === "paragraph" && right.type === "paragraph") return true;
|
|
447
|
-
if (left.type === "table-cell-paragraph" && right.type === "table-cell-paragraph") return arraysEqual(left.tablePath, right.tablePath) && arraysEqual(left.rowPath, right.rowPath) && arraysEqual(left.cellPath, right.cellPath);
|
|
448
|
-
if (left.type === "text-box-paragraph" && right.type === "text-box-paragraph") return arraysEqual(left.textBoxPath, right.textBoxPath);
|
|
449
|
-
return false;
|
|
450
|
-
};
|
|
451
|
-
const docxLocationKey = ({ blockIndex, part }) => `${part.path}\0${blockIndex}`;
|
|
452
|
-
//#endregion
|
|
453
75
|
//#region src/rewrite.ts
|
|
454
|
-
const WORDPROCESSING_NAMESPACES = /* @__PURE__ */ new Set(["http://purl.oclc.org/ooxml/wordprocessingml/main", "http://schemas.openxmlformats.org/wordprocessingml/2006/main"]);
|
|
455
|
-
const XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace";
|
|
456
|
-
const DOCX_MAX_REPLACEMENTS = 1e6;
|
|
457
|
-
const SIGNATURE_PART_PREFIX = "_xmlsignatures/";
|
|
458
76
|
var DocxRewriteError = class extends Error {
|
|
459
77
|
code;
|
|
460
78
|
constructor(code, message) {
|
|
@@ -463,200 +81,110 @@ var DocxRewriteError = class extends Error {
|
|
|
463
81
|
this.code = code;
|
|
464
82
|
}
|
|
465
83
|
};
|
|
466
|
-
const
|
|
467
|
-
const
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
}
|
|
503
|
-
const values = /* @__PURE__ */ new Map();
|
|
504
|
-
const originalValues = /* @__PURE__ */ new Map();
|
|
505
|
-
for (const segment of block.segments) {
|
|
506
|
-
if (segment.source !== "text") continue;
|
|
507
|
-
values.set(pathKey(segment.xmlPath), {
|
|
508
|
-
path: segment.xmlPath,
|
|
509
|
-
value: block.text.slice(segment.start, segment.end)
|
|
510
|
-
});
|
|
511
|
-
originalValues.set(pathKey(segment.xmlPath), block.text.slice(segment.start, segment.end));
|
|
512
|
-
}
|
|
513
|
-
for (const replacement of replacements.toReversed()) {
|
|
514
|
-
const segments = coveredTextSegments(block, replacement);
|
|
515
|
-
const first = segments.at(0);
|
|
516
|
-
const last = segments.at(-1);
|
|
517
|
-
if (first === void 0 || last === void 0) throw rewriteError(DOCX_REWRITE_ERROR_CODES.unsupportedReplacement, "DOCX replacement text segments are unavailable");
|
|
518
|
-
const firstUpdate = values.get(pathKey(first.xmlPath));
|
|
519
|
-
const lastUpdate = values.get(pathKey(last.xmlPath));
|
|
520
|
-
if (firstUpdate === void 0 || lastUpdate === void 0) throw rewriteError(DOCX_REWRITE_ERROR_CODES.unsupportedReplacement, "DOCX replacement text nodes are unavailable");
|
|
521
|
-
const firstStart = replacement.start - first.start;
|
|
522
|
-
const lastEnd = replacement.end - last.start;
|
|
523
|
-
if (first === last) {
|
|
524
|
-
firstUpdate.value = firstUpdate.value.slice(0, firstStart) + replacement.replacement + firstUpdate.value.slice(lastEnd);
|
|
525
|
-
continue;
|
|
84
|
+
const REWRITE_ERROR_CODES = new Set(Object.values(DOCX_REWRITE_ERROR_CODES));
|
|
85
|
+
const EXTRACTION_ERROR_CODES = new Set(Object.values(DOCX_EXTRACTION_ERROR_CODES));
|
|
86
|
+
const DOCX_REWRITE_MAX_BLOCKS = 1e5;
|
|
87
|
+
const DOCX_REWRITE_MAX_REPLACEMENTS = 1e6;
|
|
88
|
+
const LOCATION_PATH_KEYS = [
|
|
89
|
+
"xmlPath",
|
|
90
|
+
"tablePath",
|
|
91
|
+
"rowPath",
|
|
92
|
+
"cellPath",
|
|
93
|
+
"textBoxPath"
|
|
94
|
+
];
|
|
95
|
+
const preflightRewritePlan = (rewrites) => {
|
|
96
|
+
const rewriteCount = rewrites.length;
|
|
97
|
+
if (rewriteCount > DOCX_REWRITE_MAX_BLOCKS) throw new DocxRewriteError(DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded, `DOCX rewrites must not contain more than ${DOCX_REWRITE_MAX_BLOCKS} blocks`);
|
|
98
|
+
let replacementCount = 0;
|
|
99
|
+
let estimatedBytes = rewriteCount * 256;
|
|
100
|
+
const serializableRewrites = [];
|
|
101
|
+
for (let rewriteIndex = 0; rewriteIndex < rewriteCount; rewriteIndex += 1) {
|
|
102
|
+
const rewrite = rewrites[rewriteIndex];
|
|
103
|
+
if (rewrite === void 0) throw new DocxRewriteError(DOCX_REWRITE_ERROR_CODES.invalidReplacement, "DOCX rewrite plans must not contain sparse blocks");
|
|
104
|
+
if (!Array.isArray(rewrite.replacements)) throw new DocxRewriteError(DOCX_REWRITE_ERROR_CODES.invalidReplacement, "DOCX block rewrite replacements must be an array");
|
|
105
|
+
const blockReplacementCount = rewrite.replacements.length;
|
|
106
|
+
replacementCount += blockReplacementCount;
|
|
107
|
+
if (replacementCount > DOCX_REWRITE_MAX_REPLACEMENTS) throw new DocxRewriteError(DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded, `DOCX rewrites must not contain more than ${DOCX_REWRITE_MAX_REPLACEMENTS} replacements`);
|
|
108
|
+
estimatedBytes += (typeof rewrite.expectedText === "string" ? rewrite.expectedText.length * 6 : 0) + blockReplacementCount * 96;
|
|
109
|
+
const serializableReplacements = [];
|
|
110
|
+
for (let replacementIndex = 0; replacementIndex < blockReplacementCount; replacementIndex += 1) {
|
|
111
|
+
const replacement = rewrite.replacements[replacementIndex];
|
|
112
|
+
if (replacement === void 0) throw new DocxRewriteError(DOCX_REWRITE_ERROR_CODES.invalidReplacement, "DOCX rewrite plans must not contain sparse replacements");
|
|
113
|
+
const value = replacement.replacement;
|
|
114
|
+
if (typeof value === "string") estimatedBytes += value.length * 6;
|
|
115
|
+
serializableReplacements.push({
|
|
116
|
+
start: typeof replacement.start === "number" ? replacement.start : null,
|
|
117
|
+
end: typeof replacement.end === "number" ? replacement.end : null,
|
|
118
|
+
replacement: typeof value === "string" ? value : null
|
|
119
|
+
});
|
|
526
120
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
121
|
+
const location = rewrite.location;
|
|
122
|
+
const part = location["part"];
|
|
123
|
+
for (const value of [
|
|
124
|
+
location["type"],
|
|
125
|
+
part?.["type"],
|
|
126
|
+
part?.["path"]
|
|
127
|
+
]) if (typeof value === "string") estimatedBytes += value.length * 6;
|
|
128
|
+
const serializableLocation = {
|
|
129
|
+
type: typeof location["type"] === "string" ? location["type"] : null,
|
|
130
|
+
part: {
|
|
131
|
+
type: typeof part?.["type"] === "string" ? part["type"] : null,
|
|
132
|
+
path: typeof part?.["path"] === "string" ? part["path"] : null
|
|
133
|
+
},
|
|
134
|
+
blockIndex: typeof location["blockIndex"] === "number" ? location["blockIndex"] : null
|
|
135
|
+
};
|
|
136
|
+
for (const key of LOCATION_PATH_KEYS) {
|
|
137
|
+
const path = location[key];
|
|
138
|
+
if (Array.isArray(path)) {
|
|
139
|
+
if (path.length > 256) throw new DocxRewriteError(DOCX_REWRITE_ERROR_CODES.invalidReplacement, `DOCX rewrite location paths must not exceed 256 entries`);
|
|
140
|
+
estimatedBytes += path.length * 24;
|
|
141
|
+
const serializablePath = [];
|
|
142
|
+
for (let pathIndex = 0; pathIndex < path.length; pathIndex += 1) {
|
|
143
|
+
const value = path[pathIndex];
|
|
144
|
+
serializablePath.push(typeof value === "number" ? value : null);
|
|
145
|
+
}
|
|
146
|
+
serializableLocation[key] = serializablePath;
|
|
147
|
+
}
|
|
531
148
|
}
|
|
532
|
-
|
|
149
|
+
serializableRewrites.push({
|
|
150
|
+
location: serializableLocation,
|
|
151
|
+
expectedText: typeof rewrite.expectedText === "string" ? rewrite.expectedText : null,
|
|
152
|
+
replacements: serializableReplacements
|
|
153
|
+
});
|
|
154
|
+
if (estimatedBytes > 134217728) throw new DocxRewriteError(DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded, `DOCX rewrite plans must not exceed ${DOCX_UNCOMPRESSED_MAX_BYTES} estimated serialized bytes`);
|
|
533
155
|
}
|
|
534
|
-
return
|
|
156
|
+
return serializableRewrites;
|
|
535
157
|
};
|
|
536
|
-
const
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
const findClosingTagStart = ({ xml, contentStart, parserPosition }) => {
|
|
541
|
-
for (let index = parserPosition - 1; index >= contentStart; index -= 1) if (xml[index] === "<" && xml[index + 1] === "/") return index;
|
|
542
|
-
throw rewriteError(DOCX_REWRITE_ERROR_CODES.staleExtraction, "DOCX text-node closing tag changed after extraction");
|
|
543
|
-
};
|
|
544
|
-
const rewritePartXml = (xml, updates) => {
|
|
545
|
-
const updatesByPath = new Map(updates.map((update) => [pathKey(update.path), update]));
|
|
546
|
-
const foundPaths = /* @__PURE__ */ new Set();
|
|
547
|
-
const patches = [];
|
|
548
|
-
const stack = [];
|
|
549
|
-
let activeText;
|
|
550
|
-
let parseError = null;
|
|
551
|
-
const parser = new SaxesParser({ xmlns: true });
|
|
552
|
-
parser.on("error", (error) => {
|
|
553
|
-
parseError = error;
|
|
554
|
-
});
|
|
555
|
-
parser.on("opentag", (tag) => {
|
|
556
|
-
if (stack.length >= 256) throw rewriteError(DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded, `DOCX XML must not exceed 256 nested elements`);
|
|
557
|
-
const parent = stack.at(-1);
|
|
558
|
-
const childIndex = parent?.nextChildIndex ?? 0;
|
|
559
|
-
if (parent !== void 0) parent.nextChildIndex += 1;
|
|
560
|
-
const path = [...parent?.path ?? [], childIndex];
|
|
561
|
-
stack.push({
|
|
562
|
-
path,
|
|
563
|
-
nextChildIndex: 0
|
|
564
|
-
});
|
|
565
|
-
const key = pathKey(path);
|
|
566
|
-
if (isWordTextTag(tag) && updatesByPath.has(key)) {
|
|
567
|
-
if (tag.isSelfClosing) throw rewriteError(DOCX_REWRITE_ERROR_CODES.unsupportedReplacement, "DOCX self-closing text nodes cannot receive replacements");
|
|
568
|
-
activeText = {
|
|
569
|
-
key,
|
|
570
|
-
contentStart: parser.position,
|
|
571
|
-
tag
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
});
|
|
575
|
-
parser.on("closetag", (tag) => {
|
|
576
|
-
if (activeText?.tag === tag) {
|
|
577
|
-
const update = updatesByPath.get(activeText.key);
|
|
578
|
-
if (update !== void 0) {
|
|
579
|
-
const contentEnd = findClosingTagStart({
|
|
580
|
-
xml,
|
|
581
|
-
contentStart: activeText.contentStart,
|
|
582
|
-
parserPosition: parser.position
|
|
583
|
-
});
|
|
584
|
-
patches.push({
|
|
585
|
-
start: activeText.contentStart,
|
|
586
|
-
end: contentEnd,
|
|
587
|
-
value: escapeXmlText(update.value)
|
|
588
|
-
});
|
|
589
|
-
if (requiresPreservedSpace(update.value) && !hasPreservedSpace(tag)) patches.push({
|
|
590
|
-
start: activeText.contentStart - 1,
|
|
591
|
-
end: activeText.contentStart - 1,
|
|
592
|
-
value: " xml:space=\"preserve\""
|
|
593
|
-
});
|
|
594
|
-
foundPaths.add(activeText.key);
|
|
595
|
-
}
|
|
596
|
-
activeText = void 0;
|
|
597
|
-
}
|
|
598
|
-
stack.pop();
|
|
599
|
-
});
|
|
158
|
+
const rewriteDocxText = (archive, rewrites) => {
|
|
159
|
+
const rewrite = loadNativeAnonymizeBinding().rewriteDocxTextNative;
|
|
160
|
+
if (rewrite === void 0) throw new Error("The native anonymize binding does not expose DOCX rewriting");
|
|
161
|
+
let serializableRewrites;
|
|
600
162
|
try {
|
|
601
|
-
|
|
163
|
+
serializableRewrites = preflightRewritePlan(rewrites);
|
|
602
164
|
} catch (error) {
|
|
603
165
|
if (error instanceof DocxRewriteError) throw error;
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
if (parseError !== null) throw rewriteError(DOCX_REWRITE_ERROR_CODES.unsupportedReplacement, "DOCX source XML changed after extraction");
|
|
607
|
-
if (foundPaths.size !== updatesByPath.size) throw rewriteError(DOCX_REWRITE_ERROR_CODES.staleExtraction, "DOCX text-node locations changed after extraction");
|
|
608
|
-
let rewritten = xml;
|
|
609
|
-
for (const patch of patches.toSorted((left, right) => right.start - left.start)) rewritten = rewritten.slice(0, patch.start) + patch.value + rewritten.slice(patch.end);
|
|
610
|
-
return rewritten;
|
|
611
|
-
};
|
|
612
|
-
const assertArchiveBudgets = (entries) => {
|
|
613
|
-
let totalBytes = 0;
|
|
614
|
-
for (const bytes of Object.values(entries)) {
|
|
615
|
-
if (bytes.byteLength > 16777216) throw rewriteError(DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded, `Rewritten DOCX entries must not exceed ${DOCX_ENTRY_MAX_BYTES} bytes`);
|
|
616
|
-
totalBytes += bytes.byteLength;
|
|
166
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
167
|
+
throw new DocxRewriteError(DOCX_REWRITE_ERROR_CODES.invalidReplacement, `DOCX rewrite plan is invalid: ${message}`);
|
|
617
168
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
rewrittenBlockCount: 0,
|
|
625
|
-
appliedReplacementCount: 0
|
|
626
|
-
};
|
|
627
|
-
const blocksByLocation = new Map(extraction.blocks.map((block) => [docxLocationKey(block.location), block]));
|
|
628
|
-
const updatesByPart = /* @__PURE__ */ new Map();
|
|
629
|
-
const rewrittenLocations = /* @__PURE__ */ new Set();
|
|
630
|
-
let appliedReplacementCount = 0;
|
|
631
|
-
for (const rewrite of rewrites) {
|
|
632
|
-
const key = docxLocationKey(rewrite.location);
|
|
633
|
-
if (rewrittenLocations.has(key)) throw rewriteError(DOCX_REWRITE_ERROR_CODES.invalidReplacement, "Each DOCX block may appear in a rewrite plan only once");
|
|
634
|
-
rewrittenLocations.add(key);
|
|
635
|
-
const block = blocksByLocation.get(key);
|
|
636
|
-
if (block === void 0 || !docxLocationsEqual(block.location, rewrite.location) || block.text !== rewrite.expectedText) throw rewriteError(DOCX_REWRITE_ERROR_CODES.staleExtraction, "DOCX block location or expected text no longer matches");
|
|
637
|
-
if (rewrite.replacements.length === 0) throw rewriteError(DOCX_REWRITE_ERROR_CODES.invalidReplacement, "DOCX block rewrite plans must contain at least one replacement");
|
|
638
|
-
if (appliedReplacementCount + rewrite.replacements.length > DOCX_MAX_REPLACEMENTS) throw rewriteError(DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded, `DOCX rewrites must not contain more than ${DOCX_MAX_REPLACEMENTS} replacements`);
|
|
639
|
-
const partUpdates = updatesByPart.get(block.location.part.path) ?? /* @__PURE__ */ new Map();
|
|
640
|
-
for (const update of planBlockUpdates(block, rewrite)) partUpdates.set(pathKey(update.path), update);
|
|
641
|
-
updatesByPart.set(block.location.part.path, partUpdates);
|
|
642
|
-
appliedReplacementCount += rewrite.replacements.length;
|
|
169
|
+
let rewritesJson;
|
|
170
|
+
try {
|
|
171
|
+
rewritesJson = JSON.stringify(serializableRewrites);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
174
|
+
throw new DocxRewriteError(DOCX_REWRITE_ERROR_CODES.invalidReplacement, `DOCX rewrite plan is not serializable: ${message}`);
|
|
643
175
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
const
|
|
650
|
-
|
|
176
|
+
try {
|
|
177
|
+
return rewrite(archive, rewritesJson);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
180
|
+
const separator = message.indexOf(": ");
|
|
181
|
+
const rawCode = message.slice(0, separator);
|
|
182
|
+
const extractionCode = rawCode;
|
|
183
|
+
if (separator > 0 && EXTRACTION_ERROR_CODES.has(extractionCode)) throw new DocxExtractionError(extractionCode, message.slice(separator + 2));
|
|
184
|
+
const code = rawCode;
|
|
185
|
+
if (separator > 0 && REWRITE_ERROR_CODES.has(code)) throw new DocxRewriteError(code, message.slice(separator + 2));
|
|
186
|
+
throw error;
|
|
651
187
|
}
|
|
652
|
-
assertArchiveBudgets(entries);
|
|
653
|
-
const document = zipSync(entries);
|
|
654
|
-
if (document.byteLength > 67108864) throw rewriteError(DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded, `Rewritten DOCX archives must not exceed ${DOCX_ARCHIVE_MAX_BYTES} bytes`);
|
|
655
|
-
return {
|
|
656
|
-
document,
|
|
657
|
-
rewrittenBlockCount: rewrites.length,
|
|
658
|
-
appliedReplacementCount
|
|
659
|
-
};
|
|
660
188
|
};
|
|
661
189
|
//#endregion
|
|
662
190
|
//#region src/coverage.ts
|
|
@@ -681,8 +209,6 @@ const docxWorkflowCoverage = (coverage) => {
|
|
|
681
209
|
};
|
|
682
210
|
//#endregion
|
|
683
211
|
//#region src/restore.ts
|
|
684
|
-
const DOCX_RESTORE_MAX_PLACEHOLDER_UTF16 = 512;
|
|
685
|
-
const DOCX_RESTORE_MAX_CANDIDATES = 1e6;
|
|
686
212
|
var DocxRestorationError = class extends Error {
|
|
687
213
|
code;
|
|
688
214
|
constructor(code, message) {
|
|
@@ -692,53 +218,6 @@ var DocxRestorationError = class extends Error {
|
|
|
692
218
|
}
|
|
693
219
|
};
|
|
694
220
|
const restorationError = (code, message) => new DocxRestorationError(code, message);
|
|
695
|
-
const encodedSessionNamespace = (sessionId) => sessionId.replaceAll("_", "%5F");
|
|
696
|
-
const isOwnedPlaceholderCandidate = (value, encodedSessionId) => {
|
|
697
|
-
const inner = value.endsWith("]") ? value.slice(0, -1) : value;
|
|
698
|
-
const countSeparator = inner.lastIndexOf("_");
|
|
699
|
-
if (countSeparator <= 0) return false;
|
|
700
|
-
const prefix = inner.slice(0, countSeparator);
|
|
701
|
-
const namespaceSeparator = prefix.lastIndexOf("_");
|
|
702
|
-
if (namespaceSeparator <= 0) return false;
|
|
703
|
-
return prefix.slice(namespaceSeparator + 1) === encodedSessionId;
|
|
704
|
-
};
|
|
705
|
-
const planBlockRestoration = ({ text, encodedSessionId, restoreCandidate, budget }) => {
|
|
706
|
-
const replacements = [];
|
|
707
|
-
let start;
|
|
708
|
-
for (let cursor = 0; cursor < text.length; cursor += 1) {
|
|
709
|
-
const character = text.at(cursor);
|
|
710
|
-
if (character === "[") {
|
|
711
|
-
if (start !== void 0 && isOwnedPlaceholderCandidate(text.slice(start + 1, cursor), encodedSessionId)) throw restorationError(DOCX_RESTORATION_ERROR_CODES.invalidPlaceholder, "DOCX text contains an incomplete placeholder for the expected session");
|
|
712
|
-
start = cursor;
|
|
713
|
-
continue;
|
|
714
|
-
}
|
|
715
|
-
if (character !== "]" || start === void 0) continue;
|
|
716
|
-
const candidateEnd = cursor + 1;
|
|
717
|
-
const candidate = text.slice(start, candidateEnd);
|
|
718
|
-
budget.candidateCount += 1;
|
|
719
|
-
if (budget.candidateCount > DOCX_RESTORE_MAX_CANDIDATES) throw restorationError(DOCX_RESTORATION_ERROR_CODES.restorationLimitExceeded, `DOCX restoration must not inspect more than ${DOCX_RESTORE_MAX_CANDIDATES} placeholder candidates`);
|
|
720
|
-
const isOwned = isOwnedPlaceholderCandidate(candidate.slice(1), encodedSessionId);
|
|
721
|
-
if (candidate.length > DOCX_RESTORE_MAX_PLACEHOLDER_UTF16) {
|
|
722
|
-
if (isOwned) throw restorationError(DOCX_RESTORATION_ERROR_CODES.invalidPlaceholder, "DOCX session placeholder exceeds the maximum length");
|
|
723
|
-
start = void 0;
|
|
724
|
-
continue;
|
|
725
|
-
}
|
|
726
|
-
if (!isOwned) {
|
|
727
|
-
start = void 0;
|
|
728
|
-
continue;
|
|
729
|
-
}
|
|
730
|
-
const replacement = restoreCandidate(candidate);
|
|
731
|
-
if (replacement !== candidate) replacements.push({
|
|
732
|
-
start,
|
|
733
|
-
end: candidateEnd,
|
|
734
|
-
replacement
|
|
735
|
-
});
|
|
736
|
-
else throw restorationError(DOCX_RESTORATION_ERROR_CODES.invalidPlaceholder, "DOCX text contains an unknown placeholder for the expected session");
|
|
737
|
-
start = void 0;
|
|
738
|
-
}
|
|
739
|
-
if (start !== void 0 && isOwnedPlaceholderCandidate(text.slice(start + 1), encodedSessionId)) throw restorationError(DOCX_RESTORATION_ERROR_CODES.invalidPlaceholder, "DOCX text contains an incomplete placeholder for the expected session");
|
|
740
|
-
return replacements;
|
|
741
|
-
};
|
|
742
221
|
const restoreDocxText = ({ document, session, expectedSessionId, observedAtEpochSeconds }) => {
|
|
743
222
|
const sessionId = session.sessionId();
|
|
744
223
|
if (sessionId !== expectedSessionId) throw restorationError(DOCX_RESTORATION_ERROR_CODES.sessionMismatch, "DOCX restoration session does not match the expected session id");
|
|
@@ -754,23 +233,40 @@ const restoreDocxText = ({ document, session, expectedSessionId, observedAtEpoch
|
|
|
754
233
|
restoredCandidates.set(candidate, restored);
|
|
755
234
|
return restored;
|
|
756
235
|
};
|
|
757
|
-
const
|
|
758
|
-
|
|
236
|
+
const planRestoration = loadNativeAnonymizeBinding().planDocxRestorationJson;
|
|
237
|
+
if (planRestoration === void 0) throw restorationError(DOCX_RESTORATION_ERROR_CODES.invalidSession, "Native anonymize binding does not expose DOCX restoration planning");
|
|
238
|
+
let plan;
|
|
239
|
+
try {
|
|
240
|
+
plan = JSON.parse(planRestoration(document, sessionId));
|
|
241
|
+
} catch (error) {
|
|
242
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
243
|
+
const separator = message.indexOf(": ");
|
|
244
|
+
const code = message.slice(0, separator);
|
|
245
|
+
const knownCodes = /* @__PURE__ */ new Set([
|
|
246
|
+
DOCX_RESTORATION_ERROR_CODES.invalidPlaceholder,
|
|
247
|
+
DOCX_RESTORATION_ERROR_CODES.restorationLimitExceeded,
|
|
248
|
+
DOCX_RESTORATION_ERROR_CODES.unsupportedDocument
|
|
249
|
+
]);
|
|
250
|
+
if (separator > 0 && knownCodes.has(code)) throw restorationError(code, message.slice(separator + 2));
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
759
253
|
const rewrites = [];
|
|
760
|
-
const budget = { candidateCount: 0 };
|
|
761
254
|
let restoredPlaceholderCount = 0;
|
|
762
|
-
for (const block of
|
|
763
|
-
const replacements =
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
255
|
+
for (const block of plan.blocks) {
|
|
256
|
+
const replacements = block.candidates.map(({ candidate, end, start }) => {
|
|
257
|
+
const replacement = restoreCandidate(candidate);
|
|
258
|
+
if (replacement === candidate) throw restorationError(DOCX_RESTORATION_ERROR_CODES.invalidPlaceholder, "DOCX text contains an unknown placeholder for the expected session");
|
|
259
|
+
return {
|
|
260
|
+
start,
|
|
261
|
+
end,
|
|
262
|
+
replacement
|
|
263
|
+
};
|
|
768
264
|
});
|
|
769
265
|
if (replacements.length === 0) continue;
|
|
770
266
|
restoredPlaceholderCount += replacements.length;
|
|
771
267
|
rewrites.push({
|
|
772
268
|
location: block.location,
|
|
773
|
-
expectedText: block.
|
|
269
|
+
expectedText: block.expectedText,
|
|
774
270
|
replacements
|
|
775
271
|
});
|
|
776
272
|
}
|
|
@@ -781,10 +277,21 @@ const restoreDocxText = ({ document, session, expectedSessionId, observedAtEpoch
|
|
|
781
277
|
sessionId,
|
|
782
278
|
restoredBlockCount: restored.rewrittenBlockCount,
|
|
783
279
|
restoredPlaceholderCount,
|
|
784
|
-
coverage: docxWorkflowCoverage(extraction.coverage)
|
|
280
|
+
coverage: docxWorkflowCoverage(plan.extraction.coverage)
|
|
785
281
|
};
|
|
786
282
|
};
|
|
787
283
|
//#endregion
|
|
284
|
+
//#region src/location.ts
|
|
285
|
+
const arraysEqual = (left, right) => left.length === right.length && left.every((value, index) => value === right.at(index));
|
|
286
|
+
const docxLocationsEqual = (left, right) => {
|
|
287
|
+
if (left.type !== right.type || left.part.type !== right.part.type || left.part.path !== right.part.path || left.blockIndex !== right.blockIndex || !arraysEqual(left.xmlPath, right.xmlPath)) return false;
|
|
288
|
+
if (left.type === "paragraph" && right.type === "paragraph") return true;
|
|
289
|
+
if (left.type === "table-cell-paragraph" && right.type === "table-cell-paragraph") return arraysEqual(left.tablePath, right.tablePath) && arraysEqual(left.rowPath, right.rowPath) && arraysEqual(left.cellPath, right.cellPath);
|
|
290
|
+
if (left.type === "text-box-paragraph" && right.type === "text-box-paragraph") return arraysEqual(left.textBoxPath, right.textBoxPath);
|
|
291
|
+
return false;
|
|
292
|
+
};
|
|
293
|
+
const docxLocationKey = ({ blockIndex, part }) => `${part.path}\0${blockIndex}`;
|
|
294
|
+
//#endregion
|
|
788
295
|
//#region src/anonymize.ts
|
|
789
296
|
const DOCX_ANONYMIZATION_MAX_CALLER_DETECTIONS = 1e6;
|
|
790
297
|
var DocxAnonymizationError = class extends Error {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["WORDPROCESSING_NAMESPACES"],"sources":["../src/types.ts","../src/extract.ts","../src/location.ts","../src/rewrite.ts","../src/coverage.ts","../src/restore.ts","../src/anonymize.ts"],"sourcesContent":["import type {\n NativeCallerDetection,\n NativeOperatorConfig,\n NativeSessionBlockRedactionPlan,\n NativeSessionCallerRedactionPlanOptions,\n} from \"@stll/anonymize\";\n\nexport const DOCX_PART_TYPES = {\n comments: \"comments\",\n endnotes: \"endnotes\",\n footer: \"footer\",\n footnotes: \"footnotes\",\n header: \"header\",\n mainDocument: \"main-document\",\n} as const;\n\nexport type DocxPartType =\n (typeof DOCX_PART_TYPES)[keyof typeof DOCX_PART_TYPES];\n\nexport type DocxPart = {\n type: DocxPartType;\n path: string;\n};\n\ntype DocxBaseBlockLocation = {\n part: DocxPart;\n blockIndex: number;\n xmlPath: readonly number[];\n};\n\nexport type DocxBlockLocation =\n | (DocxBaseBlockLocation & {\n type: \"paragraph\";\n })\n | (DocxBaseBlockLocation & {\n type: \"table-cell-paragraph\";\n tablePath: readonly number[];\n rowPath: readonly number[];\n cellPath: readonly number[];\n })\n | (DocxBaseBlockLocation & {\n type: \"text-box-paragraph\";\n textBoxPath: readonly number[];\n });\n\nexport type DocxInlineContext =\n | {\n type: \"hyperlink\";\n relationshipId: string | null;\n anchor: string | null;\n }\n | {\n type: \"revision\";\n revision: \"deletion\" | \"insertion\" | \"move-from\" | \"move-to\";\n };\n\nexport type DocxTextSegment = {\n start: number;\n end: number;\n source: \"break\" | \"tab\" | \"text\";\n contexts: readonly DocxInlineContext[];\n xmlPath: readonly number[];\n};\n\nexport type DocxTextBlock = {\n text: string;\n location: DocxBlockLocation;\n segments: readonly DocxTextSegment[];\n};\n\nexport type DocxCoverageItem =\n | {\n status: \"extracted\";\n part: DocxPart;\n blockCount: number;\n }\n | {\n status: \"unsupported\";\n path: string;\n contentType: string;\n reason: string;\n };\n\nexport type DocxCoverage = {\n parts: readonly DocxCoverageItem[];\n hyperlinkTextSegmentCount: number;\n revisionTextSegmentCount: number;\n unsupportedAlternateContentCount: number;\n unsupportedSymbolCount: number;\n unsupportedFieldInstructionCount: number;\n};\n\nexport type DocxExtraction = {\n contractVersion: 1;\n blocks: readonly DocxTextBlock[];\n coverage: DocxCoverage;\n};\n\nexport type DocxTextReplacement = {\n start: number;\n end: number;\n replacement: string;\n};\n\nexport type DocxBlockRewrite = {\n location: DocxBlockLocation;\n expectedText: string;\n replacements: readonly DocxTextReplacement[];\n};\n\nexport type DocxRewriteResult = {\n document: Uint8Array;\n rewrittenBlockCount: number;\n appliedReplacementCount: number;\n};\n\nexport const DOCX_COVERAGE_MODES = {\n allowPartial: \"allow-partial\",\n requireFull: \"require-full\",\n} as const;\n\nexport type DocxCoverageMode =\n (typeof DOCX_COVERAGE_MODES)[keyof typeof DOCX_COVERAGE_MODES];\n\nexport type DocxCoveragePolicy =\n | { mode: typeof DOCX_COVERAGE_MODES.requireFull }\n | { mode: typeof DOCX_COVERAGE_MODES.allowPartial };\n\nexport type DocxAnonymizationPolicy = {\n coverage: DocxCoveragePolicy;\n operators?: NativeOperatorConfig;\n};\n\nexport type DocxCallerDetection = NativeCallerDetection;\n\nexport type DocxBlockCallerDetections = {\n location: DocxBlockLocation;\n expectedText: string;\n detections: readonly DocxCallerDetection[];\n};\n\nexport type DocxSessionRedactionPlan = {\n blocks: readonly NativeSessionBlockRedactionPlan[];\n commit: () => void;\n};\n\nexport type DocxAnonymizationSession = {\n sessionId: () => string;\n planTextBatchWithCallerDetections: (\n options: NativeSessionCallerRedactionPlanOptions,\n ) => DocxSessionRedactionPlan;\n};\n\nexport type AnonymizeDocxOptions = {\n document: Uint8Array;\n session: DocxAnonymizationSession;\n expectedSessionId: string;\n policy: DocxAnonymizationPolicy;\n callerDetections?: readonly DocxBlockCallerDetections[];\n observedAtEpochSeconds?: number;\n};\n\nexport type DocxCoverageSummary = {\n extractedPartCount: number;\n unsupportedPartCount: number;\n hyperlinkTextSegmentCount: number;\n revisionTextSegmentCount: number;\n unsupportedAlternateContentCount: number;\n unsupportedSymbolCount: number;\n unsupportedFieldInstructionCount: number;\n};\n\nexport type DocxWorkflowCoverage =\n | { status: \"full\"; counts: DocxCoverageSummary }\n | { status: \"partial\"; counts: DocxCoverageSummary };\n\nexport type DocxAnonymizationSummary = {\n contractVersion: 1;\n sessionId: string;\n blockCount: number;\n rewrittenBlockCount: number;\n appliedReplacementCount: number;\n entityCount: number;\n callerDetectionCount: number;\n retainedCallerDetectionCount: number;\n coverage: DocxWorkflowCoverage;\n};\n\nexport type DocxAnonymizationResult = {\n document: Uint8Array;\n summary: DocxAnonymizationSummary;\n};\n\nexport const DOCX_ANONYMIZATION_ERROR_CODES = {\n incompleteCoverage: \"incomplete-coverage\",\n invalidCallerDetections: \"invalid-caller-detections\",\n sessionMismatch: \"session-mismatch\",\n} as const;\n\nexport type DocxAnonymizationErrorCode =\n (typeof DOCX_ANONYMIZATION_ERROR_CODES)[keyof typeof DOCX_ANONYMIZATION_ERROR_CODES];\n\nexport type DocxRestorationSession = {\n sessionId: () => string;\n restoreText: (text: string, observedAtEpochSeconds?: number) => string;\n};\n\nexport type RestoreDocxTextOptions = {\n document: Uint8Array;\n session: DocxRestorationSession;\n expectedSessionId: string;\n observedAtEpochSeconds?: number;\n};\n\nexport type DocxRestorationResult = {\n document: Uint8Array;\n sessionId: string;\n restoredBlockCount: number;\n restoredPlaceholderCount: number;\n coverage: DocxWorkflowCoverage;\n};\n\nexport const DOCX_RESTORATION_ERROR_CODES = {\n invalidPlaceholder: \"invalid-placeholder\",\n invalidSession: \"invalid-session\",\n restorationLimitExceeded: \"restoration-limit-exceeded\",\n sessionMismatch: \"session-mismatch\",\n} as const;\n\nexport type DocxRestorationErrorCode =\n (typeof DOCX_RESTORATION_ERROR_CODES)[keyof typeof DOCX_RESTORATION_ERROR_CODES];\n\nexport const DOCX_REWRITE_ERROR_CODES = {\n invalidReplacement: \"invalid-replacement\",\n rewriteLimitExceeded: \"rewrite-limit-exceeded\",\n staleExtraction: \"stale-extraction\",\n unsupportedReplacement: \"unsupported-replacement\",\n} as const;\n\nexport type DocxRewriteErrorCode =\n (typeof DOCX_REWRITE_ERROR_CODES)[keyof typeof DOCX_REWRITE_ERROR_CODES];\n\nexport const DOCX_EXTRACTION_ERROR_CODES = {\n archiveLimitExceeded: \"archive-limit-exceeded\",\n invalidArchive: \"invalid-archive\",\n invalidPackage: \"invalid-package\",\n invalidXml: \"invalid-xml\",\n unsafeEntryPath: \"unsafe-entry-path\",\n uncompressedLimitExceeded: \"uncompressed-limit-exceeded\",\n} as const;\n\nexport type DocxExtractionErrorCode =\n (typeof DOCX_EXTRACTION_ERROR_CODES)[keyof typeof DOCX_EXTRACTION_ERROR_CODES];\n","import { unzipSync, type UnzipFileInfo } from \"fflate\";\nimport { SaxesParser, type SaxesTagNS } from \"saxes\";\n\nimport {\n DOCX_EXTRACTION_ERROR_CODES,\n DOCX_PART_TYPES,\n type DocxCoverageItem,\n type DocxExtraction,\n type DocxExtractionErrorCode,\n type DocxPart,\n type DocxPartType,\n type DocxTextBlock,\n type DocxTextSegment,\n type DocxInlineContext,\n} from \"./types\";\n\nexport const DOCX_EXTRACTION_CONTRACT_VERSION = 1 as const;\nexport const DOCX_ARCHIVE_MAX_BYTES = 64 * 1024 * 1024;\nexport const DOCX_ENTRY_MAX_BYTES = 16 * 1024 * 1024;\nexport const DOCX_UNCOMPRESSED_MAX_BYTES = 128 * 1024 * 1024;\nexport const DOCX_XML_MAX_DEPTH = 256;\nconst DOCX_MAX_ENTRIES = 4096;\nconst DOCX_MAX_TEXT_BLOCKS = 100_000;\nconst DOCX_MAX_TEXT_SEGMENTS = 1_000_000;\n\nconst CONTENT_TYPES_PATH = \"[Content_Types].xml\";\nconst ROOT_RELATIONSHIPS_PATH = \"_rels/.rels\";\nconst CONTENT_TYPES_NAMESPACE =\n \"http://schemas.openxmlformats.org/package/2006/content-types\";\nconst PACKAGE_RELATIONSHIP_NAMESPACES = new Set([\n \"http://purl.oclc.org/ooxml/package/relationships\",\n \"http://schemas.openxmlformats.org/package/2006/relationships\",\n]);\nconst WORDPROCESSING_CONTENT_TYPE_PREFIX =\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.\";\nconst SUPPORTED_CONTENT_TYPE_SUFFIXES: Readonly<Record<string, DocxPartType>> =\n {\n \"comments+xml\": DOCX_PART_TYPES.comments,\n \"document.main+xml\": DOCX_PART_TYPES.mainDocument,\n \"endnotes+xml\": DOCX_PART_TYPES.endnotes,\n \"footer+xml\": DOCX_PART_TYPES.footer,\n \"footnotes+xml\": DOCX_PART_TYPES.footnotes,\n \"header+xml\": DOCX_PART_TYPES.header,\n };\nconst WORDPROCESSING_NAMESPACES = new Set([\n \"http://purl.oclc.org/ooxml/wordprocessingml/main\",\n \"http://schemas.openxmlformats.org/wordprocessingml/2006/main\",\n]);\nconst RELATIONSHIP_NAMESPACES = new Set([\n \"http://purl.oclc.org/ooxml/officeDocument/relationships\",\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships\",\n]);\nconst OFFICE_DOCUMENT_RELATIONSHIP_TYPES = new Set(\n [...RELATIONSHIP_NAMESPACES].map(\n (namespace) => `${namespace}/officeDocument`,\n ),\n);\nconst MARKUP_COMPATIBILITY_NAMESPACES = new Set([\n \"http://purl.oclc.org/ooxml/markup-compatibility/main\",\n \"http://schemas.openxmlformats.org/markup-compatibility/2006\",\n]);\n\nexport class DocxExtractionError extends Error {\n readonly code: DocxExtractionErrorCode;\n\n constructor(code: DocxExtractionErrorCode, message: string) {\n super(message);\n this.name = \"DocxExtractionError\";\n this.code = code;\n }\n}\n\ntype ContentTypePart = {\n path: string;\n contentType: string;\n};\n\ntype ElementFrame = {\n tag: SaxesTagNS;\n path: readonly number[];\n nextChildIndex: number;\n};\n\ntype MutableBlock = {\n text: string;\n segments: DocxTextSegment[];\n location: DocxTextBlock[\"location\"];\n};\n\ntype PartExtraction = {\n blocks: DocxTextBlock[];\n hyperlinkTextSegmentCount: number;\n revisionTextSegmentCount: number;\n unsupportedAlternateContentCount: number;\n unsupportedSymbolCount: number;\n unsupportedFieldInstructionCount: number;\n};\n\ntype PartTextBudget = {\n segmentCount: number;\n};\n\nconst invalidPackage = (message: string): DocxExtractionError =>\n new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.invalidPackage, message);\n\nconst assertXmlDepth = (depth: number): void => {\n if (depth < DOCX_XML_MAX_DEPTH) {\n return;\n }\n throw new DocxExtractionError(\n DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded,\n `DOCX XML must not exceed ${DOCX_XML_MAX_DEPTH} nested elements`,\n );\n};\n\nconst safeEntryPath = (name: string): boolean =>\n name.length > 0 &&\n !name.startsWith(\"/\") &&\n !name.includes(\"\\\\\") &&\n !name.split(\"/\").includes(\"..\") &&\n !name.includes(\"\\0\");\n\ntype ArchiveBudget = {\n entryCount: number;\n uncompressedBytes: number;\n};\n\ntype ArchiveFilterOptions = {\n budget: ArchiveBudget;\n file: UnzipFileInfo;\n includeAllEntries: boolean;\n};\n\nconst archiveFilter = ({\n budget,\n file,\n includeAllEntries,\n}: ArchiveFilterOptions): boolean => {\n budget.entryCount += 1;\n if (budget.entryCount > DOCX_MAX_ENTRIES) {\n throw new DocxExtractionError(\n DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded,\n `DOCX archives must contain at most ${DOCX_MAX_ENTRIES} entries`,\n );\n }\n if (!safeEntryPath(file.name)) {\n throw new DocxExtractionError(\n DOCX_EXTRACTION_ERROR_CODES.unsafeEntryPath,\n \"DOCX archive contains an unsafe entry path\",\n );\n }\n if (file.originalSize > DOCX_ENTRY_MAX_BYTES) {\n throw new DocxExtractionError(\n DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded,\n `DOCX entries must not exceed ${DOCX_ENTRY_MAX_BYTES} bytes`,\n );\n }\n budget.uncompressedBytes += file.originalSize;\n if (budget.uncompressedBytes > DOCX_UNCOMPRESSED_MAX_BYTES) {\n throw new DocxExtractionError(\n DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded,\n `DOCX archives must not exceed ${DOCX_UNCOMPRESSED_MAX_BYTES} uncompressed bytes`,\n );\n }\n return (\n includeAllEntries ||\n file.name === CONTENT_TYPES_PATH ||\n file.name === ROOT_RELATIONSHIPS_PATH ||\n (file.name.startsWith(\"word/\") && file.name.endsWith(\".xml\"))\n );\n};\n\nexport const unzipDocxArchive = (\n archive: Uint8Array,\n includeAllEntries = false,\n): Record<string, Uint8Array> => {\n if (archive.byteLength > DOCX_ARCHIVE_MAX_BYTES) {\n throw new DocxExtractionError(\n DOCX_EXTRACTION_ERROR_CODES.archiveLimitExceeded,\n `DOCX archives must not exceed ${DOCX_ARCHIVE_MAX_BYTES} bytes`,\n );\n }\n const budget: ArchiveBudget = { entryCount: 0, uncompressedBytes: 0 };\n try {\n return unzipSync(archive, {\n filter: (file) => archiveFilter({ budget, file, includeAllEntries }),\n });\n } catch (error) {\n if (error instanceof DocxExtractionError) {\n throw error;\n }\n throw new DocxExtractionError(\n DOCX_EXTRACTION_ERROR_CODES.invalidArchive,\n \"Input is not a valid bounded DOCX ZIP archive\",\n );\n }\n};\n\nconst decodeXml = (bytes: Uint8Array, path: string): string => {\n try {\n return new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes);\n } catch {\n throw new DocxExtractionError(\n DOCX_EXTRACTION_ERROR_CODES.invalidXml,\n `DOCX XML part is not valid UTF-8: ${path}`,\n );\n }\n};\n\nconst attributeByLocalName = (\n tag: SaxesTagNS,\n localName: string,\n namespaces?: ReadonlySet<string>,\n): string | null => {\n for (const attribute of Object.values(tag.attributes)) {\n if (\n attribute.local === localName &&\n (namespaces === undefined || namespaces.has(attribute.uri))\n ) {\n return attribute.value;\n }\n }\n return null;\n};\n\nconst parseContentTypes = (xml: string): ContentTypePart[] => {\n const parts: ContentTypePart[] = [];\n const paths = new Set<string>();\n const parser = new SaxesParser({ xmlns: true });\n let parseError: Error | null = null;\n let depth = 0;\n parser.on(\"error\", (error) => {\n parseError = error;\n });\n parser.on(\"doctype\", () => {\n throw invalidPackage(\n \"DOCX XML must not contain a document type declaration\",\n );\n });\n parser.on(\"opentag\", (tag) => {\n assertXmlDepth(depth);\n depth += 1;\n if (tag.local !== \"Override\" || tag.uri !== CONTENT_TYPES_NAMESPACE) {\n return;\n }\n const rawPath = attributeByLocalName(tag, \"PartName\");\n const contentType = attributeByLocalName(tag, \"ContentType\");\n if (rawPath === null || contentType === null) {\n throw invalidPackage(\"DOCX content-type override is incomplete\");\n }\n const path = rawPath.startsWith(\"/\") ? rawPath.slice(1) : rawPath;\n if (!safeEntryPath(path)) {\n throw invalidPackage(\"DOCX content-type override has an unsafe path\");\n }\n if (paths.has(path)) {\n throw invalidPackage(\n \"DOCX content-type overrides must have unique paths\",\n );\n }\n paths.add(path);\n parts.push({ path, contentType });\n });\n parser.on(\"closetag\", () => {\n depth -= 1;\n });\n try {\n parser.write(xml).close();\n } catch (error) {\n if (error instanceof DocxExtractionError) {\n throw error;\n }\n parseError = error instanceof Error ? error : new Error(\"invalid XML\");\n }\n if (parseError !== null) {\n throw new DocxExtractionError(\n DOCX_EXTRACTION_ERROR_CODES.invalidXml,\n \"DOCX content types are not valid XML\",\n );\n }\n return parts;\n};\n\nconst parseMainDocumentTarget = (xml: string): string => {\n const targets: string[] = [];\n const parser = new SaxesParser({ xmlns: true });\n let parseError: Error | null = null;\n let depth = 0;\n parser.on(\"error\", (error) => {\n parseError = error;\n });\n parser.on(\"doctype\", () => {\n throw invalidPackage(\n \"DOCX XML must not contain a document type declaration\",\n );\n });\n parser.on(\"opentag\", (tag) => {\n assertXmlDepth(depth);\n depth += 1;\n if (\n tag.local !== \"Relationship\" ||\n !PACKAGE_RELATIONSHIP_NAMESPACES.has(tag.uri)\n ) {\n return;\n }\n const type = attributeByLocalName(tag, \"Type\");\n if (type === null || !OFFICE_DOCUMENT_RELATIONSHIP_TYPES.has(type)) {\n return;\n }\n const targetMode = attributeByLocalName(tag, \"TargetMode\");\n const rawTarget = attributeByLocalName(tag, \"Target\");\n if (targetMode === \"External\" || rawTarget === null) {\n throw invalidPackage(\"DOCX main-document relationship must be internal\");\n }\n const target = rawTarget.startsWith(\"/\") ? rawTarget.slice(1) : rawTarget;\n if (!safeEntryPath(target) || target.includes(\":\")) {\n throw invalidPackage(\n \"DOCX main-document relationship has an unsafe target\",\n );\n }\n targets.push(target);\n });\n parser.on(\"closetag\", () => {\n depth -= 1;\n });\n try {\n parser.write(xml).close();\n } catch (error) {\n if (error instanceof DocxExtractionError) {\n throw error;\n }\n parseError = error instanceof Error ? error : new Error(\"invalid XML\");\n }\n if (parseError !== null) {\n throw new DocxExtractionError(\n DOCX_EXTRACTION_ERROR_CODES.invalidXml,\n \"DOCX root relationships are not valid XML\",\n );\n }\n if (targets.length !== 1) {\n throw invalidPackage(\n \"DOCX archive must contain exactly one main-document relationship\",\n );\n }\n const target = targets.at(0);\n if (target === undefined) {\n throw invalidPackage(\"DOCX main-document relationship is unavailable\");\n }\n return target;\n};\n\nconst classifyPart = ({\n contentType,\n path,\n}: ContentTypePart): DocxPart | null => {\n if (!contentType.startsWith(WORDPROCESSING_CONTENT_TYPE_PREFIX)) {\n return null;\n }\n const suffix = contentType.slice(WORDPROCESSING_CONTENT_TYPE_PREFIX.length);\n const type = SUPPORTED_CONTENT_TYPE_SUFFIXES[suffix];\n return type === undefined ? null : { type, path };\n};\n\nconst isWordTag = (tag: SaxesTagNS, local: string): boolean =>\n tag.local === local && WORDPROCESSING_NAMESPACES.has(tag.uri);\n\nconst frameByLocalName = (\n stack: readonly ElementFrame[],\n local: string,\n): ElementFrame | null => {\n for (let index = stack.length - 1; index >= 0; index -= 1) {\n const frame = stack.at(index);\n if (frame !== undefined && isWordTag(frame.tag, local)) {\n return frame;\n }\n }\n return null;\n};\n\nconst blockLocation = (\n part: DocxPart,\n blockIndex: number,\n paragraphPath: readonly number[],\n stack: readonly ElementFrame[],\n): DocxTextBlock[\"location\"] => {\n const textBox = frameByLocalName(stack, \"txbxContent\");\n if (textBox !== null) {\n return {\n type: \"text-box-paragraph\",\n part,\n blockIndex,\n xmlPath: paragraphPath,\n textBoxPath: textBox.path,\n };\n }\n const cell = frameByLocalName(stack, \"tc\");\n const row = frameByLocalName(stack, \"tr\");\n const table = frameByLocalName(stack, \"tbl\");\n if (cell !== null && row !== null && table !== null) {\n return {\n type: \"table-cell-paragraph\",\n part,\n blockIndex,\n xmlPath: paragraphPath,\n tablePath: table.path,\n rowPath: row.path,\n cellPath: cell.path,\n };\n }\n return {\n type: \"paragraph\",\n part,\n blockIndex,\n xmlPath: paragraphPath,\n };\n};\n\nconst revisionForTag = (\n tag: SaxesTagNS,\n): Extract<DocxInlineContext, { type: \"revision\" }> | null => {\n if (!WORDPROCESSING_NAMESPACES.has(tag.uri)) {\n return null;\n }\n const revisions: Readonly<\n Record<string, \"deletion\" | \"insertion\" | \"move-from\" | \"move-to\">\n > = {\n del: \"deletion\",\n ins: \"insertion\",\n moveFrom: \"move-from\",\n moveTo: \"move-to\",\n };\n const revision = revisions[tag.local];\n return revision === undefined ? null : { type: \"revision\", revision };\n};\n\nconst inlineContexts = (\n stack: readonly ElementFrame[],\n): DocxInlineContext[] => {\n const contexts: DocxInlineContext[] = [];\n for (const { tag } of stack) {\n if (isWordTag(tag, \"hyperlink\")) {\n contexts.push({\n type: \"hyperlink\",\n relationshipId: attributeByLocalName(\n tag,\n \"id\",\n RELATIONSHIP_NAMESPACES,\n ),\n anchor: attributeByLocalName(tag, \"anchor\", WORDPROCESSING_NAMESPACES),\n });\n }\n const revision = revisionForTag(tag);\n if (revision !== null) {\n contexts.push(revision);\n }\n }\n return contexts;\n};\n\nconst appendSegment = (\n block: MutableBlock,\n budget: PartTextBudget,\n value: string,\n source: DocxTextSegment[\"source\"],\n path: readonly number[],\n stack: readonly ElementFrame[],\n): void => {\n if (value.length === 0) {\n return;\n }\n if (budget.segmentCount >= DOCX_MAX_TEXT_SEGMENTS) {\n throw new DocxExtractionError(\n DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded,\n `DOCX parts must not contain more than ${DOCX_MAX_TEXT_SEGMENTS} text segments`,\n );\n }\n budget.segmentCount += 1;\n const start = block.text.length;\n block.text += value;\n block.segments.push({\n start,\n end: block.text.length,\n source,\n contexts: inlineContexts(stack),\n xmlPath: path,\n });\n};\n\nconst extractPart = (part: DocxPart, xml: string): PartExtraction => {\n const blocks: DocxTextBlock[] = [];\n const stack: ElementFrame[] = [];\n const blockStack: MutableBlock[] = [];\n let nextBlockIndex = 0;\n let currentText = \"\";\n let currentTextPath: readonly number[] | null = null;\n let parseError: Error | null = null;\n let unsupportedSymbolCount = 0;\n let unsupportedFieldInstructionCount = 0;\n let unsupportedAlternateContentCount = 0;\n const textBudget: PartTextBudget = { segmentCount: 0 };\n\n const parser = new SaxesParser({ xmlns: true });\n parser.on(\"error\", (error) => {\n parseError = error;\n });\n parser.on(\"doctype\", () => {\n throw invalidPackage(\n \"DOCX XML must not contain a document type declaration\",\n );\n });\n parser.on(\"opentag\", (tag) => {\n assertXmlDepth(stack.length);\n const parent = stack.at(-1);\n const childIndex = parent?.nextChildIndex ?? 0;\n if (parent !== undefined) {\n parent.nextChildIndex += 1;\n }\n const path = [...(parent?.path ?? []), childIndex];\n if (isWordTag(tag, \"p\")) {\n if (nextBlockIndex >= DOCX_MAX_TEXT_BLOCKS) {\n throw new DocxExtractionError(\n DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded,\n `DOCX parts must not contain more than ${DOCX_MAX_TEXT_BLOCKS} text blocks`,\n );\n }\n blockStack.push({\n text: \"\",\n segments: [],\n location: blockLocation(part, nextBlockIndex, path, stack),\n });\n nextBlockIndex += 1;\n }\n stack.push({ tag, path, nextChildIndex: 0 });\n if (isWordTag(tag, \"t\") || isWordTag(tag, \"delText\")) {\n currentText = \"\";\n currentTextPath = path;\n }\n const currentBlock = blockStack.at(-1);\n if (currentBlock !== undefined && isWordTag(tag, \"tab\")) {\n appendSegment(currentBlock, textBudget, \"\\t\", \"tab\", path, stack);\n }\n if (\n currentBlock !== undefined &&\n (isWordTag(tag, \"br\") || isWordTag(tag, \"cr\"))\n ) {\n appendSegment(currentBlock, textBudget, \"\\n\", \"break\", path, stack);\n }\n if (isWordTag(tag, \"sym\")) {\n unsupportedSymbolCount += 1;\n }\n if (isWordTag(tag, \"instrText\") || isWordTag(tag, \"fldSimple\")) {\n unsupportedFieldInstructionCount += 1;\n }\n if (\n tag.local === \"AlternateContent\" &&\n MARKUP_COMPATIBILITY_NAMESPACES.has(tag.uri)\n ) {\n unsupportedAlternateContentCount += 1;\n }\n });\n parser.on(\"text\", (text) => {\n if (currentTextPath !== null) {\n currentText += text;\n }\n });\n parser.on(\"cdata\", (text) => {\n if (currentTextPath !== null) {\n currentText += text;\n }\n });\n parser.on(\"closetag\", (tag) => {\n const frame = stack.at(-1);\n if (frame === undefined || frame.tag !== tag) {\n throw invalidPackage(\"DOCX XML element stack is inconsistent\");\n }\n if (\n currentTextPath !== null &&\n (isWordTag(tag, \"t\") || isWordTag(tag, \"delText\"))\n ) {\n const currentBlock = blockStack.at(-1);\n if (currentBlock === undefined) {\n if (currentText.length > 0) {\n throw invalidPackage(\"DOCX text is outside a paragraph\");\n }\n } else {\n appendSegment(\n currentBlock,\n textBudget,\n currentText,\n \"text\",\n currentTextPath,\n stack,\n );\n }\n currentText = \"\";\n currentTextPath = null;\n }\n if (isWordTag(tag, \"p\")) {\n const completedBlock = blockStack.pop();\n if (completedBlock === undefined) {\n throw invalidPackage(\"DOCX paragraph state is unavailable\");\n }\n blocks.push(completedBlock);\n }\n stack.pop();\n });\n try {\n parser.write(xml).close();\n } catch (error) {\n if (error instanceof DocxExtractionError) {\n throw error;\n }\n parseError = error instanceof Error ? error : new Error(\"invalid XML\");\n }\n if (parseError !== null) {\n throw new DocxExtractionError(\n DOCX_EXTRACTION_ERROR_CODES.invalidXml,\n `DOCX part is not valid XML: ${part.path}`,\n );\n }\n\n blocks.sort(\n (left, right) => left.location.blockIndex - right.location.blockIndex,\n );\n\n let hyperlinkTextSegmentCount = 0;\n let revisionTextSegmentCount = 0;\n for (const { segments } of blocks) {\n for (const { contexts } of segments) {\n if (contexts.some((context) => context.type === \"hyperlink\")) {\n hyperlinkTextSegmentCount += 1;\n }\n if (contexts.some((context) => context.type === \"revision\")) {\n revisionTextSegmentCount += 1;\n }\n }\n }\n return {\n blocks,\n hyperlinkTextSegmentCount,\n revisionTextSegmentCount,\n unsupportedAlternateContentCount,\n unsupportedSymbolCount,\n unsupportedFieldInstructionCount,\n };\n};\n\nexport const extractDocxText = (archive: Uint8Array): DocxExtraction => {\n const entries = unzipDocxArchive(archive);\n const contentTypesBytes = entries[CONTENT_TYPES_PATH];\n if (contentTypesBytes === undefined) {\n throw invalidPackage(\"DOCX archive is missing [Content_Types].xml\");\n }\n const contentTypes = parseContentTypes(\n decodeXml(contentTypesBytes, CONTENT_TYPES_PATH),\n );\n const rootRelationshipsBytes = entries[ROOT_RELATIONSHIPS_PATH];\n if (rootRelationshipsBytes === undefined) {\n throw invalidPackage(\"DOCX archive is missing _rels/.rels\");\n }\n const mainDocumentTarget = parseMainDocumentTarget(\n decodeXml(rootRelationshipsBytes, ROOT_RELATIONSHIPS_PATH),\n );\n const supportedParts = contentTypes\n .map(classifyPart)\n .filter((part): part is DocxPart => part !== null);\n if (\n supportedParts.filter((part) => part.type === DOCX_PART_TYPES.mainDocument)\n .length !== 1\n ) {\n throw invalidPackage(\"DOCX archive must contain exactly one main document\");\n }\n const mainDocument = supportedParts.find(\n (part) => part.type === DOCX_PART_TYPES.mainDocument,\n );\n if (mainDocument?.path !== mainDocumentTarget) {\n throw invalidPackage(\n \"DOCX main-document relationship and content type do not agree\",\n );\n }\n\n const blocks: DocxTextBlock[] = [];\n const coverageParts: DocxCoverageItem[] = [];\n let hyperlinkTextSegmentCount = 0;\n let revisionTextSegmentCount = 0;\n let unsupportedSymbolCount = 0;\n let unsupportedFieldInstructionCount = 0;\n let unsupportedAlternateContentCount = 0;\n let textSegmentCount = 0;\n for (const part of supportedParts) {\n const bytes = entries[part.path];\n if (bytes === undefined) {\n throw invalidPackage(\n `DOCX archive is missing declared part: ${part.path}`,\n );\n }\n const extracted = extractPart(part, decodeXml(bytes, part.path));\n if (blocks.length + extracted.blocks.length > DOCX_MAX_TEXT_BLOCKS) {\n throw new DocxExtractionError(\n DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded,\n `DOCX archives must not contain more than ${DOCX_MAX_TEXT_BLOCKS} text blocks`,\n );\n }\n const extractedSegmentCount = extracted.blocks.reduce(\n (count, block) => count + block.segments.length,\n 0,\n );\n if (textSegmentCount + extractedSegmentCount > DOCX_MAX_TEXT_SEGMENTS) {\n throw new DocxExtractionError(\n DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded,\n `DOCX archives must not contain more than ${DOCX_MAX_TEXT_SEGMENTS} text segments`,\n );\n }\n textSegmentCount += extractedSegmentCount;\n blocks.push(...extracted.blocks);\n coverageParts.push({\n status: \"extracted\",\n part,\n blockCount: extracted.blocks.length,\n });\n hyperlinkTextSegmentCount += extracted.hyperlinkTextSegmentCount;\n revisionTextSegmentCount += extracted.revisionTextSegmentCount;\n unsupportedSymbolCount += extracted.unsupportedSymbolCount;\n unsupportedFieldInstructionCount +=\n extracted.unsupportedFieldInstructionCount;\n unsupportedAlternateContentCount +=\n extracted.unsupportedAlternateContentCount;\n }\n\n for (const { contentType, path } of contentTypes) {\n if (\n contentType.startsWith(WORDPROCESSING_CONTENT_TYPE_PREFIX) &&\n classifyPart({ contentType, path }) === null\n ) {\n coverageParts.push({\n status: \"unsupported\",\n path,\n contentType,\n reason: \"WordprocessingML part type is not extracted\",\n });\n }\n }\n\n return {\n contractVersion: DOCX_EXTRACTION_CONTRACT_VERSION,\n blocks,\n coverage: {\n parts: coverageParts,\n hyperlinkTextSegmentCount,\n revisionTextSegmentCount,\n unsupportedAlternateContentCount,\n unsupportedSymbolCount,\n unsupportedFieldInstructionCount,\n },\n };\n};\n","import type { DocxBlockLocation } from \"./types\";\n\nconst arraysEqual = (\n left: readonly number[],\n right: readonly number[],\n): boolean =>\n left.length === right.length &&\n left.every((value, index) => value === right.at(index));\n\nexport const docxLocationsEqual = (\n left: DocxBlockLocation,\n right: DocxBlockLocation,\n): boolean => {\n if (\n left.type !== right.type ||\n left.part.type !== right.part.type ||\n left.part.path !== right.part.path ||\n left.blockIndex !== right.blockIndex ||\n !arraysEqual(left.xmlPath, right.xmlPath)\n ) {\n return false;\n }\n if (left.type === \"paragraph\" && right.type === \"paragraph\") {\n return true;\n }\n if (\n left.type === \"table-cell-paragraph\" &&\n right.type === \"table-cell-paragraph\"\n ) {\n return (\n arraysEqual(left.tablePath, right.tablePath) &&\n arraysEqual(left.rowPath, right.rowPath) &&\n arraysEqual(left.cellPath, right.cellPath)\n );\n }\n if (\n left.type === \"text-box-paragraph\" &&\n right.type === \"text-box-paragraph\"\n ) {\n return arraysEqual(left.textBoxPath, right.textBoxPath);\n }\n return false;\n};\n\nexport const docxLocationKey = ({\n blockIndex,\n part,\n}: DocxBlockLocation): string => `${part.path}\\0${blockIndex}`;\n","import { strToU8, zipSync } from \"fflate\";\nimport { SaxesParser, type SaxesTagNS } from \"saxes\";\n\nimport {\n DOCX_ARCHIVE_MAX_BYTES,\n DOCX_ENTRY_MAX_BYTES,\n DOCX_UNCOMPRESSED_MAX_BYTES,\n DOCX_XML_MAX_DEPTH,\n extractDocxText,\n unzipDocxArchive,\n} from \"./extract\";\nimport { docxLocationKey, docxLocationsEqual } from \"./location\";\nimport {\n DOCX_REWRITE_ERROR_CODES,\n type DocxBlockRewrite,\n type DocxRewriteErrorCode,\n type DocxRewriteResult,\n type DocxTextBlock,\n type DocxTextReplacement,\n type DocxTextSegment,\n} from \"./types\";\n\nconst WORDPROCESSING_NAMESPACES = new Set([\n \"http://purl.oclc.org/ooxml/wordprocessingml/main\",\n \"http://schemas.openxmlformats.org/wordprocessingml/2006/main\",\n]);\nconst XML_NAMESPACE = \"http://www.w3.org/XML/1998/namespace\";\nconst DOCX_MAX_REPLACEMENTS = 1_000_000;\nconst SIGNATURE_PART_PREFIX = \"_xmlsignatures/\";\n\nexport class DocxRewriteError extends Error {\n readonly code: DocxRewriteErrorCode;\n\n constructor(code: DocxRewriteErrorCode, message: string) {\n super(message);\n this.name = \"DocxRewriteError\";\n this.code = code;\n }\n}\n\ntype ElementFrame = {\n path: readonly number[];\n nextChildIndex: number;\n};\n\ntype TextNodeUpdate = {\n path: readonly number[];\n value: string;\n};\n\ntype XmlPatch = {\n start: number;\n end: number;\n value: string;\n};\n\nconst rewriteError = (\n code: DocxRewriteErrorCode,\n message: string,\n): DocxRewriteError => new DocxRewriteError(code, message);\n\nconst pathKey = (path: readonly number[]): string => path.join(\".\");\n\nconst isValidXmlText = (value: string): boolean => {\n for (const character of value) {\n const codePoint = character.codePointAt(0);\n if (\n codePoint === undefined ||\n (codePoint !== 0x09 &&\n codePoint !== 0x0a &&\n codePoint !== 0x0d &&\n (codePoint < 0x20 ||\n (codePoint > 0xd7ff && codePoint < 0xe000) ||\n (codePoint > 0xfffd && codePoint < 0x10_000) ||\n codePoint > 0x10_ffff))\n ) {\n return false;\n }\n }\n return true;\n};\n\nconst isUtf16Boundary = (value: string, offset: number): boolean => {\n if (offset === 0 || offset === value.length) {\n return true;\n }\n const previous = value.charCodeAt(offset - 1);\n const next = value.charCodeAt(offset);\n return !(\n previous >= 0xd800 &&\n previous <= 0xdbff &&\n next >= 0xdc00 &&\n next <= 0xdfff\n );\n};\n\nconst validateReplacement = (\n replacement: DocxTextReplacement,\n blockText: string,\n): void => {\n if (\n !Number.isSafeInteger(replacement.start) ||\n !Number.isSafeInteger(replacement.end) ||\n replacement.start < 0 ||\n replacement.start >= replacement.end ||\n replacement.end > blockText.length ||\n !isUtf16Boundary(blockText, replacement.start) ||\n !isUtf16Boundary(blockText, replacement.end)\n ) {\n throw rewriteError(\n DOCX_REWRITE_ERROR_CODES.invalidReplacement,\n \"DOCX replacement spans must be nonempty bounded integer ranges at UTF-16 boundaries\",\n );\n }\n if (!isValidXmlText(replacement.replacement)) {\n throw rewriteError(\n DOCX_REWRITE_ERROR_CODES.invalidReplacement,\n \"DOCX replacement text must contain only valid XML characters\",\n );\n }\n if (strToU8(replacement.replacement).byteLength > DOCX_ENTRY_MAX_BYTES) {\n throw rewriteError(\n DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded,\n `DOCX replacement text must not exceed ${DOCX_ENTRY_MAX_BYTES} UTF-8 bytes`,\n );\n }\n};\n\nconst coveredTextSegments = (\n block: DocxTextBlock,\n replacement: DocxTextReplacement,\n): readonly DocxTextSegment[] => {\n const segments = block.segments.filter(\n ({ end, start }) => start < replacement.end && end > replacement.start,\n );\n let cursor = replacement.start;\n for (const segment of segments) {\n if (\n segment.source !== \"text\" ||\n segment.start > cursor ||\n segment.contexts.some((context) => context.type === \"revision\")\n ) {\n throw rewriteError(\n DOCX_REWRITE_ERROR_CODES.unsupportedReplacement,\n \"DOCX replacements must stay within contiguous non-revision text segments\",\n );\n }\n cursor = Math.min(replacement.end, segment.end);\n }\n if (segments.length === 0 || cursor !== replacement.end) {\n throw rewriteError(\n DOCX_REWRITE_ERROR_CODES.unsupportedReplacement,\n \"DOCX replacements must stay within contiguous non-revision text segments\",\n );\n }\n return segments;\n};\n\nconst planBlockUpdates = (\n block: DocxTextBlock,\n rewrite: DocxBlockRewrite,\n): TextNodeUpdate[] => {\n const replacements = [...rewrite.replacements].sort(\n (left, right) => left.start - right.start,\n );\n for (const [index, replacement] of replacements.entries()) {\n validateReplacement(replacement, block.text);\n const previous = index === 0 ? undefined : replacements.at(index - 1);\n if (previous !== undefined && previous.end > replacement.start) {\n throw rewriteError(\n DOCX_REWRITE_ERROR_CODES.invalidReplacement,\n \"DOCX replacement spans must not overlap\",\n );\n }\n }\n\n const values = new Map<string, TextNodeUpdate>();\n const originalValues = new Map<string, string>();\n for (const segment of block.segments) {\n if (segment.source !== \"text\") {\n continue;\n }\n values.set(pathKey(segment.xmlPath), {\n path: segment.xmlPath,\n value: block.text.slice(segment.start, segment.end),\n });\n originalValues.set(\n pathKey(segment.xmlPath),\n block.text.slice(segment.start, segment.end),\n );\n }\n\n for (const replacement of replacements.toReversed()) {\n const segments = coveredTextSegments(block, replacement);\n const first = segments.at(0);\n const last = segments.at(-1);\n if (first === undefined || last === undefined) {\n throw rewriteError(\n DOCX_REWRITE_ERROR_CODES.unsupportedReplacement,\n \"DOCX replacement text segments are unavailable\",\n );\n }\n const firstUpdate = values.get(pathKey(first.xmlPath));\n const lastUpdate = values.get(pathKey(last.xmlPath));\n if (firstUpdate === undefined || lastUpdate === undefined) {\n throw rewriteError(\n DOCX_REWRITE_ERROR_CODES.unsupportedReplacement,\n \"DOCX replacement text nodes are unavailable\",\n );\n }\n const firstStart = replacement.start - first.start;\n const lastEnd = replacement.end - last.start;\n if (first === last) {\n firstUpdate.value =\n firstUpdate.value.slice(0, firstStart) +\n replacement.replacement +\n firstUpdate.value.slice(lastEnd);\n continue;\n }\n firstUpdate.value =\n firstUpdate.value.slice(0, firstStart) + replacement.replacement;\n for (const segment of segments.slice(1, -1)) {\n const update = values.get(pathKey(segment.xmlPath));\n if (update !== undefined) {\n update.value = \"\";\n }\n }\n lastUpdate.value = lastUpdate.value.slice(lastEnd);\n }\n return [...values.entries()]\n .filter(([key, update]) => update.value !== originalValues.get(key))\n .map(([, update]) => update);\n};\n\nconst escapeXmlText = (value: string): string =>\n value\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\");\n\nconst requiresPreservedSpace = (value: string): boolean =>\n /^\\s|\\s$/u.test(value);\n\nconst isWordTextTag = (tag: SaxesTagNS): boolean =>\n WORDPROCESSING_NAMESPACES.has(tag.uri) &&\n (tag.local === \"t\" || tag.local === \"delText\");\n\nconst hasPreservedSpace = (tag: SaxesTagNS): boolean =>\n Object.values(tag.attributes).some(\n (attribute) =>\n attribute.uri === XML_NAMESPACE &&\n attribute.local === \"space\" &&\n attribute.value === \"preserve\",\n );\n\ntype FindClosingTagStartOptions = {\n xml: string;\n contentStart: number;\n parserPosition: number;\n};\n\nconst findClosingTagStart = ({\n xml,\n contentStart,\n parserPosition,\n}: FindClosingTagStartOptions): number => {\n for (let index = parserPosition - 1; index >= contentStart; index -= 1) {\n if (xml[index] === \"<\" && xml[index + 1] === \"/\") {\n return index;\n }\n }\n throw rewriteError(\n DOCX_REWRITE_ERROR_CODES.staleExtraction,\n \"DOCX text-node closing tag changed after extraction\",\n );\n};\n\nconst rewritePartXml = (\n xml: string,\n updates: readonly TextNodeUpdate[],\n): string => {\n const updatesByPath = new Map(\n updates.map((update) => [pathKey(update.path), update]),\n );\n const foundPaths = new Set<string>();\n const patches: XmlPatch[] = [];\n const stack: ElementFrame[] = [];\n let activeText:\n | { key: string; contentStart: number; tag: SaxesTagNS }\n | undefined;\n let parseError: Error | null = null;\n const parser = new SaxesParser({ xmlns: true });\n parser.on(\"error\", (error) => {\n parseError = error;\n });\n parser.on(\"opentag\", (tag) => {\n if (stack.length >= DOCX_XML_MAX_DEPTH) {\n throw rewriteError(\n DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded,\n `DOCX XML must not exceed ${DOCX_XML_MAX_DEPTH} nested elements`,\n );\n }\n const parent = stack.at(-1);\n const childIndex = parent?.nextChildIndex ?? 0;\n if (parent !== undefined) {\n parent.nextChildIndex += 1;\n }\n const path = [...(parent?.path ?? []), childIndex];\n stack.push({ path, nextChildIndex: 0 });\n const key = pathKey(path);\n if (isWordTextTag(tag) && updatesByPath.has(key)) {\n if (tag.isSelfClosing) {\n throw rewriteError(\n DOCX_REWRITE_ERROR_CODES.unsupportedReplacement,\n \"DOCX self-closing text nodes cannot receive replacements\",\n );\n }\n activeText = { key, contentStart: parser.position, tag };\n }\n });\n parser.on(\"closetag\", (tag) => {\n if (activeText?.tag === tag) {\n const update = updatesByPath.get(activeText.key);\n if (update !== undefined) {\n const contentEnd = findClosingTagStart({\n xml,\n contentStart: activeText.contentStart,\n parserPosition: parser.position,\n });\n patches.push({\n start: activeText.contentStart,\n end: contentEnd,\n value: escapeXmlText(update.value),\n });\n if (requiresPreservedSpace(update.value) && !hasPreservedSpace(tag)) {\n patches.push({\n start: activeText.contentStart - 1,\n end: activeText.contentStart - 1,\n value: ' xml:space=\"preserve\"',\n });\n }\n foundPaths.add(activeText.key);\n }\n activeText = undefined;\n }\n stack.pop();\n });\n try {\n parser.write(xml).close();\n } catch (error) {\n if (error instanceof DocxRewriteError) {\n throw error;\n }\n parseError = error instanceof Error ? error : new Error(\"invalid XML\");\n }\n if (parseError !== null) {\n throw rewriteError(\n DOCX_REWRITE_ERROR_CODES.unsupportedReplacement,\n \"DOCX source XML changed after extraction\",\n );\n }\n if (foundPaths.size !== updatesByPath.size) {\n throw rewriteError(\n DOCX_REWRITE_ERROR_CODES.staleExtraction,\n \"DOCX text-node locations changed after extraction\",\n );\n }\n let rewritten = xml;\n for (const patch of patches.toSorted(\n (left, right) => right.start - left.start,\n )) {\n rewritten =\n rewritten.slice(0, patch.start) +\n patch.value +\n rewritten.slice(patch.end);\n }\n return rewritten;\n};\n\nconst assertArchiveBudgets = (entries: Record<string, Uint8Array>): void => {\n let totalBytes = 0;\n for (const bytes of Object.values(entries)) {\n if (bytes.byteLength > DOCX_ENTRY_MAX_BYTES) {\n throw rewriteError(\n DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded,\n `Rewritten DOCX entries must not exceed ${DOCX_ENTRY_MAX_BYTES} bytes`,\n );\n }\n totalBytes += bytes.byteLength;\n }\n if (totalBytes > DOCX_UNCOMPRESSED_MAX_BYTES) {\n throw rewriteError(\n DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded,\n `Rewritten DOCX archives must not exceed ${DOCX_UNCOMPRESSED_MAX_BYTES} uncompressed bytes`,\n );\n }\n};\n\nexport const rewriteDocxText = (\n archive: Uint8Array,\n rewrites: readonly DocxBlockRewrite[],\n): DocxRewriteResult => {\n const extraction = extractDocxText(archive);\n if (rewrites.length === 0) {\n return {\n document: archive.slice(),\n rewrittenBlockCount: 0,\n appliedReplacementCount: 0,\n };\n }\n const blocksByLocation = new Map(\n extraction.blocks.map((block) => [docxLocationKey(block.location), block]),\n );\n const updatesByPart = new Map<string, Map<string, TextNodeUpdate>>();\n const rewrittenLocations = new Set<string>();\n let appliedReplacementCount = 0;\n\n for (const rewrite of rewrites) {\n const key = docxLocationKey(rewrite.location);\n if (rewrittenLocations.has(key)) {\n throw rewriteError(\n DOCX_REWRITE_ERROR_CODES.invalidReplacement,\n \"Each DOCX block may appear in a rewrite plan only once\",\n );\n }\n rewrittenLocations.add(key);\n const block = blocksByLocation.get(key);\n if (\n block === undefined ||\n !docxLocationsEqual(block.location, rewrite.location) ||\n block.text !== rewrite.expectedText\n ) {\n throw rewriteError(\n DOCX_REWRITE_ERROR_CODES.staleExtraction,\n \"DOCX block location or expected text no longer matches\",\n );\n }\n if (rewrite.replacements.length === 0) {\n throw rewriteError(\n DOCX_REWRITE_ERROR_CODES.invalidReplacement,\n \"DOCX block rewrite plans must contain at least one replacement\",\n );\n }\n if (\n appliedReplacementCount + rewrite.replacements.length >\n DOCX_MAX_REPLACEMENTS\n ) {\n throw rewriteError(\n DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded,\n `DOCX rewrites must not contain more than ${DOCX_MAX_REPLACEMENTS} replacements`,\n );\n }\n const partUpdates =\n updatesByPart.get(block.location.part.path) ?? new Map();\n for (const update of planBlockUpdates(block, rewrite)) {\n partUpdates.set(pathKey(update.path), update);\n }\n updatesByPart.set(block.location.part.path, partUpdates);\n appliedReplacementCount += rewrite.replacements.length;\n }\n\n const entries = unzipDocxArchive(archive, true);\n if (\n Object.keys(entries).some((path) =>\n path.toLowerCase().startsWith(SIGNATURE_PART_PREFIX),\n )\n ) {\n throw rewriteError(\n DOCX_REWRITE_ERROR_CODES.unsupportedReplacement,\n \"Digitally signed DOCX packages must be re-signed before rewriting\",\n );\n }\n for (const [partPath, updates] of updatesByPart) {\n const partBytes = entries[partPath];\n if (partBytes === undefined) {\n throw rewriteError(\n DOCX_REWRITE_ERROR_CODES.staleExtraction,\n \"DOCX source part changed after extraction\",\n );\n }\n const xml = new TextDecoder(\"utf-8\", { fatal: true }).decode(partBytes);\n entries[partPath] = strToU8(rewritePartXml(xml, [...updates.values()]));\n }\n assertArchiveBudgets(entries);\n const document = zipSync(entries);\n if (document.byteLength > DOCX_ARCHIVE_MAX_BYTES) {\n throw rewriteError(\n DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded,\n `Rewritten DOCX archives must not exceed ${DOCX_ARCHIVE_MAX_BYTES} bytes`,\n );\n }\n return {\n document,\n rewrittenBlockCount: rewrites.length,\n appliedReplacementCount,\n };\n};\n","import type { DocxCoverage, DocxWorkflowCoverage } from \"./types\";\n\nconst hasPartialCoverage = (coverage: DocxCoverage): boolean =>\n coverage.parts.some(({ status }) => status === \"unsupported\") ||\n coverage.hyperlinkTextSegmentCount > 0 ||\n coverage.revisionTextSegmentCount > 0 ||\n coverage.unsupportedAlternateContentCount > 0 ||\n coverage.unsupportedSymbolCount > 0 ||\n coverage.unsupportedFieldInstructionCount > 0;\n\nexport const docxWorkflowCoverage = (\n coverage: DocxCoverage,\n): DocxWorkflowCoverage => {\n const counts = {\n extractedPartCount: coverage.parts.filter(\n ({ status }) => status === \"extracted\",\n ).length,\n unsupportedPartCount: coverage.parts.filter(\n ({ status }) => status === \"unsupported\",\n ).length,\n hyperlinkTextSegmentCount: coverage.hyperlinkTextSegmentCount,\n revisionTextSegmentCount: coverage.revisionTextSegmentCount,\n unsupportedAlternateContentCount: coverage.unsupportedAlternateContentCount,\n unsupportedSymbolCount: coverage.unsupportedSymbolCount,\n unsupportedFieldInstructionCount: coverage.unsupportedFieldInstructionCount,\n };\n return hasPartialCoverage(coverage)\n ? { status: \"partial\", counts }\n : { status: \"full\", counts };\n};\n","import { docxWorkflowCoverage } from \"./coverage\";\nimport { extractDocxText } from \"./extract\";\nimport { rewriteDocxText } from \"./rewrite\";\nimport {\n DOCX_RESTORATION_ERROR_CODES,\n type DocxBlockRewrite,\n type DocxRestorationErrorCode,\n type DocxRestorationResult,\n type DocxTextReplacement,\n type RestoreDocxTextOptions,\n} from \"./types\";\n\nconst DOCX_RESTORE_MAX_PLACEHOLDER_UTF16 = 512;\nconst DOCX_RESTORE_MAX_CANDIDATES = 1_000_000;\n\nexport class DocxRestorationError extends Error {\n readonly code: DocxRestorationErrorCode;\n\n constructor(code: DocxRestorationErrorCode, message: string) {\n super(message);\n this.name = \"DocxRestorationError\";\n this.code = code;\n }\n}\n\nconst restorationError = (\n code: DocxRestorationErrorCode,\n message: string,\n): DocxRestorationError => new DocxRestorationError(code, message);\n\nconst encodedSessionNamespace = (sessionId: string): string =>\n sessionId.replaceAll(\"_\", \"%5F\");\n\nconst isOwnedPlaceholderCandidate = (\n value: string,\n encodedSessionId: string,\n): boolean => {\n const inner = value.endsWith(\"]\") ? value.slice(0, -1) : value;\n const countSeparator = inner.lastIndexOf(\"_\");\n if (countSeparator <= 0) {\n return false;\n }\n const prefix = inner.slice(0, countSeparator);\n const namespaceSeparator = prefix.lastIndexOf(\"_\");\n if (namespaceSeparator <= 0) {\n return false;\n }\n return prefix.slice(namespaceSeparator + 1) === encodedSessionId;\n};\n\ntype PlanBlockRestorationOptions = {\n text: string;\n encodedSessionId: string;\n restoreCandidate: (candidate: string) => string;\n budget: { candidateCount: number };\n};\n\nconst planBlockRestoration = ({\n text,\n encodedSessionId,\n restoreCandidate,\n budget,\n}: PlanBlockRestorationOptions): DocxTextReplacement[] => {\n const replacements: DocxTextReplacement[] = [];\n let start: number | undefined;\n for (let cursor = 0; cursor < text.length; cursor += 1) {\n const character = text.at(cursor);\n if (character === \"[\") {\n if (\n start !== undefined &&\n isOwnedPlaceholderCandidate(\n text.slice(start + 1, cursor),\n encodedSessionId,\n )\n ) {\n throw restorationError(\n DOCX_RESTORATION_ERROR_CODES.invalidPlaceholder,\n \"DOCX text contains an incomplete placeholder for the expected session\",\n );\n }\n start = cursor;\n continue;\n }\n if (character !== \"]\" || start === undefined) {\n continue;\n }\n const candidateEnd = cursor + 1;\n const candidate = text.slice(start, candidateEnd);\n budget.candidateCount += 1;\n if (budget.candidateCount > DOCX_RESTORE_MAX_CANDIDATES) {\n throw restorationError(\n DOCX_RESTORATION_ERROR_CODES.restorationLimitExceeded,\n `DOCX restoration must not inspect more than ${DOCX_RESTORE_MAX_CANDIDATES} placeholder candidates`,\n );\n }\n const isOwned = isOwnedPlaceholderCandidate(\n candidate.slice(1),\n encodedSessionId,\n );\n if (candidate.length > DOCX_RESTORE_MAX_PLACEHOLDER_UTF16) {\n if (isOwned) {\n throw restorationError(\n DOCX_RESTORATION_ERROR_CODES.invalidPlaceholder,\n \"DOCX session placeholder exceeds the maximum length\",\n );\n }\n start = undefined;\n continue;\n }\n if (!isOwned) {\n start = undefined;\n continue;\n }\n const replacement = restoreCandidate(candidate);\n if (replacement !== candidate) {\n replacements.push({ start, end: candidateEnd, replacement });\n } else {\n throw restorationError(\n DOCX_RESTORATION_ERROR_CODES.invalidPlaceholder,\n \"DOCX text contains an unknown placeholder for the expected session\",\n );\n }\n start = undefined;\n }\n if (\n start !== undefined &&\n isOwnedPlaceholderCandidate(text.slice(start + 1), encodedSessionId)\n ) {\n throw restorationError(\n DOCX_RESTORATION_ERROR_CODES.invalidPlaceholder,\n \"DOCX text contains an incomplete placeholder for the expected session\",\n );\n }\n return replacements;\n};\n\nexport const restoreDocxText = ({\n document,\n session,\n expectedSessionId,\n observedAtEpochSeconds,\n}: RestoreDocxTextOptions): DocxRestorationResult => {\n const sessionId = session.sessionId();\n if (sessionId !== expectedSessionId) {\n throw restorationError(\n DOCX_RESTORATION_ERROR_CODES.sessionMismatch,\n \"DOCX restoration session does not match the expected session id\",\n );\n }\n\n const assertSessionAvailable = (): void => {\n if (session.restoreText(\"\", observedAtEpochSeconds) !== \"\") {\n throw restorationError(\n DOCX_RESTORATION_ERROR_CODES.invalidSession,\n \"DOCX restoration session must preserve text without placeholders\",\n );\n }\n };\n assertSessionAvailable();\n const restoredCandidates = new Map<string, string>();\n const restoreCandidate = (candidate: string): string => {\n const cached = restoredCandidates.get(candidate);\n if (cached !== undefined) {\n return cached;\n }\n const restored = session.restoreText(candidate, observedAtEpochSeconds);\n restoredCandidates.set(candidate, restored);\n return restored;\n };\n const encodedSessionId = encodedSessionNamespace(sessionId);\n const extraction = extractDocxText(document);\n const rewrites: DocxBlockRewrite[] = [];\n const budget = { candidateCount: 0 };\n let restoredPlaceholderCount = 0;\n for (const block of extraction.blocks) {\n const replacements = planBlockRestoration({\n text: block.text,\n encodedSessionId,\n restoreCandidate,\n budget,\n });\n if (replacements.length === 0) {\n continue;\n }\n restoredPlaceholderCount += replacements.length;\n rewrites.push({\n location: block.location,\n expectedText: block.text,\n replacements,\n });\n }\n assertSessionAvailable();\n const restored = rewriteDocxText(document, rewrites);\n return {\n document: restored.document,\n sessionId,\n restoredBlockCount: restored.rewrittenBlockCount,\n restoredPlaceholderCount,\n coverage: docxWorkflowCoverage(extraction.coverage),\n };\n};\n","import { docxWorkflowCoverage } from \"./coverage\";\nimport { extractDocxText } from \"./extract\";\nimport { docxLocationKey, docxLocationsEqual } from \"./location\";\nimport { rewriteDocxText } from \"./rewrite\";\nimport {\n DOCX_ANONYMIZATION_ERROR_CODES,\n DOCX_COVERAGE_MODES,\n type AnonymizeDocxOptions,\n type DocxAnonymizationErrorCode,\n type DocxAnonymizationResult,\n type DocxBlockCallerDetections,\n type DocxBlockRewrite,\n} from \"./types\";\n\nexport const DOCX_ANONYMIZATION_MAX_CALLER_DETECTIONS = 1_000_000;\n\nexport class DocxAnonymizationError extends Error {\n readonly code: DocxAnonymizationErrorCode;\n\n constructor(code: DocxAnonymizationErrorCode, message: string) {\n super(message);\n this.name = \"DocxAnonymizationError\";\n this.code = code;\n }\n}\n\nconst anonymizationError = (\n code: DocxAnonymizationErrorCode,\n message: string,\n): DocxAnonymizationError => new DocxAnonymizationError(code, message);\n\ntype DetectionPlan = {\n detectionsByLocation: ReadonlyMap<string, DocxBlockCallerDetections>;\n callerDetectionCount: number;\n};\n\nconst planCallerDetections = (\n extractionBlocks: ReturnType<typeof extractDocxText>[\"blocks\"],\n inputs: readonly DocxBlockCallerDetections[],\n): DetectionPlan => {\n const blocksByLocation = new Map(\n extractionBlocks.map((block) => [docxLocationKey(block.location), block]),\n );\n const detectionsByLocation = new Map<string, DocxBlockCallerDetections>();\n let callerDetectionCount = 0;\n for (const input of inputs) {\n const key = docxLocationKey(input.location);\n if (detectionsByLocation.has(key)) {\n throw anonymizationError(\n DOCX_ANONYMIZATION_ERROR_CODES.invalidCallerDetections,\n \"Each DOCX block may have only one caller-detection input\",\n );\n }\n const block = blocksByLocation.get(key);\n if (\n block === undefined ||\n !docxLocationsEqual(block.location, input.location) ||\n block.text !== input.expectedText\n ) {\n throw anonymizationError(\n DOCX_ANONYMIZATION_ERROR_CODES.invalidCallerDetections,\n \"DOCX caller-detection location or expected text no longer matches\",\n );\n }\n if (\n input.detections.length >\n DOCX_ANONYMIZATION_MAX_CALLER_DETECTIONS - callerDetectionCount\n ) {\n throw anonymizationError(\n DOCX_ANONYMIZATION_ERROR_CODES.invalidCallerDetections,\n `DOCX workflows must not contain more than ${DOCX_ANONYMIZATION_MAX_CALLER_DETECTIONS} caller detections`,\n );\n }\n detectionsByLocation.set(key, input);\n callerDetectionCount += input.detections.length;\n }\n return { detectionsByLocation, callerDetectionCount };\n};\n\nexport const anonymizeDocx = ({\n document,\n session,\n expectedSessionId,\n policy,\n callerDetections = [],\n observedAtEpochSeconds,\n}: AnonymizeDocxOptions): DocxAnonymizationResult => {\n const sessionId = session.sessionId();\n if (sessionId !== expectedSessionId) {\n throw anonymizationError(\n DOCX_ANONYMIZATION_ERROR_CODES.sessionMismatch,\n \"DOCX anonymization session does not match the expected session\",\n );\n }\n\n const extraction = extractDocxText(document);\n const coverage = docxWorkflowCoverage(extraction.coverage);\n if (\n coverage.status === \"partial\" &&\n policy.coverage.mode === DOCX_COVERAGE_MODES.requireFull\n ) {\n throw anonymizationError(\n DOCX_ANONYMIZATION_ERROR_CODES.incompleteCoverage,\n \"DOCX contains content outside the fully supported anonymization coverage\",\n );\n }\n\n const { detectionsByLocation, callerDetectionCount } = planCallerDetections(\n extraction.blocks,\n callerDetections,\n );\n const plan = session.planTextBatchWithCallerDetections({\n inputs: extraction.blocks.map((block) => ({\n fullText: block.text,\n detections:\n detectionsByLocation.get(docxLocationKey(block.location))?.detections ??\n [],\n })),\n ...(policy.operators === undefined ? {} : { operators: policy.operators }),\n ...(observedAtEpochSeconds === undefined ? {} : { observedAtEpochSeconds }),\n });\n if (plan.blocks.length !== extraction.blocks.length) {\n throw anonymizationError(\n DOCX_ANONYMIZATION_ERROR_CODES.invalidCallerDetections,\n \"DOCX session redaction plan does not match the extracted block count\",\n );\n }\n\n const rewrites: DocxBlockRewrite[] = [];\n let entityCount = 0;\n let retainedCallerDetectionCount = 0;\n for (const [index, block] of extraction.blocks.entries()) {\n const blockPlan = plan.blocks.at(index);\n if (blockPlan === undefined) {\n throw anonymizationError(\n DOCX_ANONYMIZATION_ERROR_CODES.invalidCallerDetections,\n \"DOCX session redaction plan is missing an extracted block\",\n );\n }\n entityCount += blockPlan.entityCount;\n retainedCallerDetectionCount += blockPlan.callerEntityCount;\n if (blockPlan.replacements.length === 0) {\n continue;\n }\n rewrites.push({\n location: block.location,\n expectedText: block.text,\n replacements: blockPlan.replacements,\n });\n }\n\n const rewritten = rewriteDocxText(document, rewrites);\n plan.commit();\n return {\n document: rewritten.document,\n summary: {\n contractVersion: 1,\n sessionId,\n blockCount: extraction.blocks.length,\n rewrittenBlockCount: rewritten.rewrittenBlockCount,\n appliedReplacementCount: rewritten.appliedReplacementCount,\n entityCount,\n callerDetectionCount,\n retainedCallerDetectionCount,\n coverage,\n },\n };\n};\n"],"mappings":";;;AAOA,MAAa,kBAAkB;CAC7B,UAAU;CACV,UAAU;CACV,QAAQ;CACR,WAAW;CACX,QAAQ;CACR,cAAc;AAChB;AAsGA,MAAa,sBAAsB;CACjC,cAAc;CACd,aAAa;AACf;AA0EA,MAAa,iCAAiC;CAC5C,oBAAoB;CACpB,yBAAyB;CACzB,iBAAiB;AACnB;AAyBA,MAAa,+BAA+B;CAC1C,oBAAoB;CACpB,gBAAgB;CAChB,0BAA0B;CAC1B,iBAAiB;AACnB;AAKA,MAAa,2BAA2B;CACtC,oBAAoB;CACpB,sBAAsB;CACtB,iBAAiB;CACjB,wBAAwB;AAC1B;AAKA,MAAa,8BAA8B;CACzC,sBAAsB;CACtB,gBAAgB;CAChB,gBAAgB;CAChB,YAAY;CACZ,iBAAiB;CACjB,2BAA2B;AAC7B;;;ACzOA,MAAa,mCAAmC;AAChD,MAAa,yBAAyB,KAAK,OAAO;AAClD,MAAa,uBAAuB,KAAK,OAAO;AAChD,MAAa,8BAA8B,MAAM,OAAO;AACxD,MAAa,qBAAqB;AAClC,MAAM,mBAAmB;AACzB,MAAM,uBAAuB;AAC7B,MAAM,yBAAyB;AAE/B,MAAM,qBAAqB;AAC3B,MAAM,0BAA0B;AAChC,MAAM,0BACJ;AACF,MAAM,kDAAkC,IAAI,IAAI,CAC9C,oDACA,8DACF,CAAC;AACD,MAAM,qCACJ;AACF,MAAM,kCACJ;CACE,gBAAgB,gBAAgB;CAChC,qBAAqB,gBAAgB;CACrC,gBAAgB,gBAAgB;CAChC,cAAc,gBAAgB;CAC9B,iBAAiB,gBAAgB;CACjC,cAAc,gBAAgB;AAChC;AACF,MAAMA,8CAA4B,IAAI,IAAI,CACxC,oDACA,8DACF,CAAC;AACD,MAAM,0CAA0B,IAAI,IAAI,CACtC,2DACA,qEACF,CAAC;AACD,MAAM,qCAAqC,IAAI,IAC7C,CAAC,GAAG,uBAAuB,CAAC,CAAC,KAC1B,cAAc,GAAG,UAAU,gBAC9B,CACF;AACA,MAAM,kDAAkC,IAAI,IAAI,CAC9C,wDACA,6DACF,CAAC;AAED,IAAa,sBAAb,cAAyC,MAAM;CAC7C;CAEA,YAAY,MAA+B,SAAiB;EAC1D,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,OAAO;CACd;AACF;AAgCA,MAAM,kBAAkB,YACtB,IAAI,oBAAoB,4BAA4B,gBAAgB,OAAO;AAE7E,MAAM,kBAAkB,UAAwB;CAC9C,IAAI,QAAA,KACF;CAEF,MAAM,IAAI,oBACR,4BAA4B,2BAC5B,8CACF;AACF;AAEA,MAAM,iBAAiB,SACrB,KAAK,SAAS,KACd,CAAC,KAAK,WAAW,GAAG,KACpB,CAAC,KAAK,SAAS,IAAI,KACnB,CAAC,KAAK,MAAM,GAAG,CAAC,CAAC,SAAS,IAAI,KAC9B,CAAC,KAAK,SAAS,IAAI;AAarB,MAAM,iBAAiB,EACrB,QACA,MACA,wBACmC;CACnC,OAAO,cAAc;CACrB,IAAI,OAAO,aAAa,kBACtB,MAAM,IAAI,oBACR,4BAA4B,2BAC5B,sCAAsC,iBAAiB,SACzD;CAEF,IAAI,CAAC,cAAc,KAAK,IAAI,GAC1B,MAAM,IAAI,oBACR,4BAA4B,iBAC5B,4CACF;CAEF,IAAI,KAAK,eAAA,UACP,MAAM,IAAI,oBACR,4BAA4B,2BAC5B,gCAAgC,qBAAqB,OACvD;CAEF,OAAO,qBAAqB,KAAK;CACjC,IAAI,OAAO,oBAAA,WACT,MAAM,IAAI,oBACR,4BAA4B,2BAC5B,iCAAiC,4BAA4B,oBAC/D;CAEF,OACE,qBACA,KAAK,SAAS,sBACd,KAAK,SAAS,2BACb,KAAK,KAAK,WAAW,OAAO,KAAK,KAAK,KAAK,SAAS,MAAM;AAE/D;AAEA,MAAa,oBACX,SACA,oBAAoB,UACW;CAC/B,IAAI,QAAQ,aAAA,UACV,MAAM,IAAI,oBACR,4BAA4B,sBAC5B,iCAAiC,uBAAuB,OAC1D;CAEF,MAAM,SAAwB;EAAE,YAAY;EAAG,mBAAmB;CAAE;CACpE,IAAI;EACF,OAAO,UAAU,SAAS,EACxB,SAAS,SAAS,cAAc;GAAE;GAAQ;GAAM;EAAkB,CAAC,EACrE,CAAC;CACH,SAAS,OAAO;EACd,IAAI,iBAAiB,qBACnB,MAAM;EAER,MAAM,IAAI,oBACR,4BAA4B,gBAC5B,+CACF;CACF;AACF;AAEA,MAAM,aAAa,OAAmB,SAAyB;CAC7D,IAAI;EACF,OAAO,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC,CAAC,CAAC,OAAO,KAAK;CAC/D,QAAQ;EACN,MAAM,IAAI,oBACR,4BAA4B,YAC5B,qCAAqC,MACvC;CACF;AACF;AAEA,MAAM,wBACJ,KACA,WACA,eACkB;CAClB,KAAK,MAAM,aAAa,OAAO,OAAO,IAAI,UAAU,GAClD,IACE,UAAU,UAAU,cACnB,eAAe,KAAA,KAAa,WAAW,IAAI,UAAU,GAAG,IAEzD,OAAO,UAAU;CAGrB,OAAO;AACT;AAEA,MAAM,qBAAqB,QAAmC;CAC5D,MAAM,QAA2B,CAAC;CAClC,MAAM,wBAAQ,IAAI,IAAY;CAC9B,MAAM,SAAS,IAAI,YAAY,EAAE,OAAO,KAAK,CAAC;CAC9C,IAAI,aAA2B;CAC/B,IAAI,QAAQ;CACZ,OAAO,GAAG,UAAU,UAAU;EAC5B,aAAa;CACf,CAAC;CACD,OAAO,GAAG,iBAAiB;EACzB,MAAM,eACJ,uDACF;CACF,CAAC;CACD,OAAO,GAAG,YAAY,QAAQ;EAC5B,eAAe,KAAK;EACpB,SAAS;EACT,IAAI,IAAI,UAAU,cAAc,IAAI,QAAQ,yBAC1C;EAEF,MAAM,UAAU,qBAAqB,KAAK,UAAU;EACpD,MAAM,cAAc,qBAAqB,KAAK,aAAa;EAC3D,IAAI,YAAY,QAAQ,gBAAgB,MACtC,MAAM,eAAe,0CAA0C;EAEjE,MAAM,OAAO,QAAQ,WAAW,GAAG,IAAI,QAAQ,MAAM,CAAC,IAAI;EAC1D,IAAI,CAAC,cAAc,IAAI,GACrB,MAAM,eAAe,+CAA+C;EAEtE,IAAI,MAAM,IAAI,IAAI,GAChB,MAAM,eACJ,oDACF;EAEF,MAAM,IAAI,IAAI;EACd,MAAM,KAAK;GAAE;GAAM;EAAY,CAAC;CAClC,CAAC;CACD,OAAO,GAAG,kBAAkB;EAC1B,SAAS;CACX,CAAC;CACD,IAAI;EACF,OAAO,MAAM,GAAG,CAAC,CAAC,MAAM;CAC1B,SAAS,OAAO;EACd,IAAI,iBAAiB,qBACnB,MAAM;EAER,aAAa,iBAAiB,QAAQ,wBAAQ,IAAI,MAAM,aAAa;CACvE;CACA,IAAI,eAAe,MACjB,MAAM,IAAI,oBACR,4BAA4B,YAC5B,sCACF;CAEF,OAAO;AACT;AAEA,MAAM,2BAA2B,QAAwB;CACvD,MAAM,UAAoB,CAAC;CAC3B,MAAM,SAAS,IAAI,YAAY,EAAE,OAAO,KAAK,CAAC;CAC9C,IAAI,aAA2B;CAC/B,IAAI,QAAQ;CACZ,OAAO,GAAG,UAAU,UAAU;EAC5B,aAAa;CACf,CAAC;CACD,OAAO,GAAG,iBAAiB;EACzB,MAAM,eACJ,uDACF;CACF,CAAC;CACD,OAAO,GAAG,YAAY,QAAQ;EAC5B,eAAe,KAAK;EACpB,SAAS;EACT,IACE,IAAI,UAAU,kBACd,CAAC,gCAAgC,IAAI,IAAI,GAAG,GAE5C;EAEF,MAAM,OAAO,qBAAqB,KAAK,MAAM;EAC7C,IAAI,SAAS,QAAQ,CAAC,mCAAmC,IAAI,IAAI,GAC/D;EAEF,MAAM,aAAa,qBAAqB,KAAK,YAAY;EACzD,MAAM,YAAY,qBAAqB,KAAK,QAAQ;EACpD,IAAI,eAAe,cAAc,cAAc,MAC7C,MAAM,eAAe,kDAAkD;EAEzE,MAAM,SAAS,UAAU,WAAW,GAAG,IAAI,UAAU,MAAM,CAAC,IAAI;EAChE,IAAI,CAAC,cAAc,MAAM,KAAK,OAAO,SAAS,GAAG,GAC/C,MAAM,eACJ,sDACF;EAEF,QAAQ,KAAK,MAAM;CACrB,CAAC;CACD,OAAO,GAAG,kBAAkB;EAC1B,SAAS;CACX,CAAC;CACD,IAAI;EACF,OAAO,MAAM,GAAG,CAAC,CAAC,MAAM;CAC1B,SAAS,OAAO;EACd,IAAI,iBAAiB,qBACnB,MAAM;EAER,aAAa,iBAAiB,QAAQ,wBAAQ,IAAI,MAAM,aAAa;CACvE;CACA,IAAI,eAAe,MACjB,MAAM,IAAI,oBACR,4BAA4B,YAC5B,2CACF;CAEF,IAAI,QAAQ,WAAW,GACrB,MAAM,eACJ,kEACF;CAEF,MAAM,SAAS,QAAQ,GAAG,CAAC;CAC3B,IAAI,WAAW,KAAA,GACb,MAAM,eAAe,gDAAgD;CAEvE,OAAO;AACT;AAEA,MAAM,gBAAgB,EACpB,aACA,WACsC;CACtC,IAAI,CAAC,YAAY,WAAW,kCAAkC,GAC5D,OAAO;CAET,MAAM,SAAS,YAAY,MAAM,EAAyC;CAC1E,MAAM,OAAO,gCAAgC;CAC7C,OAAO,SAAS,KAAA,IAAY,OAAO;EAAE;EAAM;CAAK;AAClD;AAEA,MAAM,aAAa,KAAiB,UAClC,IAAI,UAAU,SAASA,4BAA0B,IAAI,IAAI,GAAG;AAE9D,MAAM,oBACJ,OACA,UACwB;CACxB,KAAK,IAAI,QAAQ,MAAM,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG;EACzD,MAAM,QAAQ,MAAM,GAAG,KAAK;EAC5B,IAAI,UAAU,KAAA,KAAa,UAAU,MAAM,KAAK,KAAK,GACnD,OAAO;CAEX;CACA,OAAO;AACT;AAEA,MAAM,iBACJ,MACA,YACA,eACA,UAC8B;CAC9B,MAAM,UAAU,iBAAiB,OAAO,aAAa;CACrD,IAAI,YAAY,MACd,OAAO;EACL,MAAM;EACN;EACA;EACA,SAAS;EACT,aAAa,QAAQ;CACvB;CAEF,MAAM,OAAO,iBAAiB,OAAO,IAAI;CACzC,MAAM,MAAM,iBAAiB,OAAO,IAAI;CACxC,MAAM,QAAQ,iBAAiB,OAAO,KAAK;CAC3C,IAAI,SAAS,QAAQ,QAAQ,QAAQ,UAAU,MAC7C,OAAO;EACL,MAAM;EACN;EACA;EACA,SAAS;EACT,WAAW,MAAM;EACjB,SAAS,IAAI;EACb,UAAU,KAAK;CACjB;CAEF,OAAO;EACL,MAAM;EACN;EACA;EACA,SAAS;CACX;AACF;AAEA,MAAM,kBACJ,QAC4D;CAC5D,IAAI,CAACA,4BAA0B,IAAI,IAAI,GAAG,GACxC,OAAO;CAUT,MAAM,WAAW;EALf,KAAK;EACL,KAAK;EACL,UAAU;EACV,QAAQ;CAEe,EAAE,IAAI;CAC/B,OAAO,aAAa,KAAA,IAAY,OAAO;EAAE,MAAM;EAAY;CAAS;AACtE;AAEA,MAAM,kBACJ,UACwB;CACxB,MAAM,WAAgC,CAAC;CACvC,KAAK,MAAM,EAAE,SAAS,OAAO;EAC3B,IAAI,UAAU,KAAK,WAAW,GAC5B,SAAS,KAAK;GACZ,MAAM;GACN,gBAAgB,qBACd,KACA,MACA,uBACF;GACA,QAAQ,qBAAqB,KAAK,UAAUA,2BAAyB;EACvE,CAAC;EAEH,MAAM,WAAW,eAAe,GAAG;EACnC,IAAI,aAAa,MACf,SAAS,KAAK,QAAQ;CAE1B;CACA,OAAO;AACT;AAEA,MAAM,iBACJ,OACA,QACA,OACA,QACA,MACA,UACS;CACT,IAAI,MAAM,WAAW,GACnB;CAEF,IAAI,OAAO,gBAAgB,wBACzB,MAAM,IAAI,oBACR,4BAA4B,2BAC5B,yCAAyC,uBAAuB,eAClE;CAEF,OAAO,gBAAgB;CACvB,MAAM,QAAQ,MAAM,KAAK;CACzB,MAAM,QAAQ;CACd,MAAM,SAAS,KAAK;EAClB;EACA,KAAK,MAAM,KAAK;EAChB;EACA,UAAU,eAAe,KAAK;EAC9B,SAAS;CACX,CAAC;AACH;AAEA,MAAM,eAAe,MAAgB,QAAgC;CACnE,MAAM,SAA0B,CAAC;CACjC,MAAM,QAAwB,CAAC;CAC/B,MAAM,aAA6B,CAAC;CACpC,IAAI,iBAAiB;CACrB,IAAI,cAAc;CAClB,IAAI,kBAA4C;CAChD,IAAI,aAA2B;CAC/B,IAAI,yBAAyB;CAC7B,IAAI,mCAAmC;CACvC,IAAI,mCAAmC;CACvC,MAAM,aAA6B,EAAE,cAAc,EAAE;CAErD,MAAM,SAAS,IAAI,YAAY,EAAE,OAAO,KAAK,CAAC;CAC9C,OAAO,GAAG,UAAU,UAAU;EAC5B,aAAa;CACf,CAAC;CACD,OAAO,GAAG,iBAAiB;EACzB,MAAM,eACJ,uDACF;CACF,CAAC;CACD,OAAO,GAAG,YAAY,QAAQ;EAC5B,eAAe,MAAM,MAAM;EAC3B,MAAM,SAAS,MAAM,GAAG,EAAE;EAC1B,MAAM,aAAa,QAAQ,kBAAkB;EAC7C,IAAI,WAAW,KAAA,GACb,OAAO,kBAAkB;EAE3B,MAAM,OAAO,CAAC,GAAI,QAAQ,QAAQ,CAAC,GAAI,UAAU;EACjD,IAAI,UAAU,KAAK,GAAG,GAAG;GACvB,IAAI,kBAAkB,sBACpB,MAAM,IAAI,oBACR,4BAA4B,2BAC5B,yCAAyC,qBAAqB,aAChE;GAEF,WAAW,KAAK;IACd,MAAM;IACN,UAAU,CAAC;IACX,UAAU,cAAc,MAAM,gBAAgB,MAAM,KAAK;GAC3D,CAAC;GACD,kBAAkB;EACpB;EACA,MAAM,KAAK;GAAE;GAAK;GAAM,gBAAgB;EAAE,CAAC;EAC3C,IAAI,UAAU,KAAK,GAAG,KAAK,UAAU,KAAK,SAAS,GAAG;GACpD,cAAc;GACd,kBAAkB;EACpB;EACA,MAAM,eAAe,WAAW,GAAG,EAAE;EACrC,IAAI,iBAAiB,KAAA,KAAa,UAAU,KAAK,KAAK,GACpD,cAAc,cAAc,YAAY,KAAM,OAAO,MAAM,KAAK;EAElE,IACE,iBAAiB,KAAA,MAChB,UAAU,KAAK,IAAI,KAAK,UAAU,KAAK,IAAI,IAE5C,cAAc,cAAc,YAAY,MAAM,SAAS,MAAM,KAAK;EAEpE,IAAI,UAAU,KAAK,KAAK,GACtB,0BAA0B;EAE5B,IAAI,UAAU,KAAK,WAAW,KAAK,UAAU,KAAK,WAAW,GAC3D,oCAAoC;EAEtC,IACE,IAAI,UAAU,sBACd,gCAAgC,IAAI,IAAI,GAAG,GAE3C,oCAAoC;CAExC,CAAC;CACD,OAAO,GAAG,SAAS,SAAS;EAC1B,IAAI,oBAAoB,MACtB,eAAe;CAEnB,CAAC;CACD,OAAO,GAAG,UAAU,SAAS;EAC3B,IAAI,oBAAoB,MACtB,eAAe;CAEnB,CAAC;CACD,OAAO,GAAG,aAAa,QAAQ;EAC7B,MAAM,QAAQ,MAAM,GAAG,EAAE;EACzB,IAAI,UAAU,KAAA,KAAa,MAAM,QAAQ,KACvC,MAAM,eAAe,wCAAwC;EAE/D,IACE,oBAAoB,SACnB,UAAU,KAAK,GAAG,KAAK,UAAU,KAAK,SAAS,IAChD;GACA,MAAM,eAAe,WAAW,GAAG,EAAE;GACrC,IAAI,iBAAiB,KAAA;QACf,YAAY,SAAS,GACvB,MAAM,eAAe,kCAAkC;GAAA,OAGzD,cACE,cACA,YACA,aACA,QACA,iBACA,KACF;GAEF,cAAc;GACd,kBAAkB;EACpB;EACA,IAAI,UAAU,KAAK,GAAG,GAAG;GACvB,MAAM,iBAAiB,WAAW,IAAI;GACtC,IAAI,mBAAmB,KAAA,GACrB,MAAM,eAAe,qCAAqC;GAE5D,OAAO,KAAK,cAAc;EAC5B;EACA,MAAM,IAAI;CACZ,CAAC;CACD,IAAI;EACF,OAAO,MAAM,GAAG,CAAC,CAAC,MAAM;CAC1B,SAAS,OAAO;EACd,IAAI,iBAAiB,qBACnB,MAAM;EAER,aAAa,iBAAiB,QAAQ,wBAAQ,IAAI,MAAM,aAAa;CACvE;CACA,IAAI,eAAe,MACjB,MAAM,IAAI,oBACR,4BAA4B,YAC5B,+BAA+B,KAAK,MACtC;CAGF,OAAO,MACJ,MAAM,UAAU,KAAK,SAAS,aAAa,MAAM,SAAS,UAC7D;CAEA,IAAI,4BAA4B;CAChC,IAAI,2BAA2B;CAC/B,KAAK,MAAM,EAAE,cAAc,QACzB,KAAK,MAAM,EAAE,cAAc,UAAU;EACnC,IAAI,SAAS,MAAM,YAAY,QAAQ,SAAS,WAAW,GACzD,6BAA6B;EAE/B,IAAI,SAAS,MAAM,YAAY,QAAQ,SAAS,UAAU,GACxD,4BAA4B;CAEhC;CAEF,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;CACF;AACF;AAEA,MAAa,mBAAmB,YAAwC;CACtE,MAAM,UAAU,iBAAiB,OAAO;CACxC,MAAM,oBAAoB,QAAQ;CAClC,IAAI,sBAAsB,KAAA,GACxB,MAAM,eAAe,6CAA6C;CAEpE,MAAM,eAAe,kBACnB,UAAU,mBAAmB,kBAAkB,CACjD;CACA,MAAM,yBAAyB,QAAQ;CACvC,IAAI,2BAA2B,KAAA,GAC7B,MAAM,eAAe,qCAAqC;CAE5D,MAAM,qBAAqB,wBACzB,UAAU,wBAAwB,uBAAuB,CAC3D;CACA,MAAM,iBAAiB,aACpB,IAAI,YAAY,CAAC,CACjB,QAAQ,SAA2B,SAAS,IAAI;CACnD,IACE,eAAe,QAAQ,SAAS,KAAK,SAAS,gBAAgB,YAAY,CAAC,CACxE,WAAW,GAEd,MAAM,eAAe,qDAAqD;CAK5E,IAHqB,eAAe,MACjC,SAAS,KAAK,SAAS,gBAAgB,YAE3B,CAAC,EAAE,SAAS,oBACzB,MAAM,eACJ,+DACF;CAGF,MAAM,SAA0B,CAAC;CACjC,MAAM,gBAAoC,CAAC;CAC3C,IAAI,4BAA4B;CAChC,IAAI,2BAA2B;CAC/B,IAAI,yBAAyB;CAC7B,IAAI,mCAAmC;CACvC,IAAI,mCAAmC;CACvC,IAAI,mBAAmB;CACvB,KAAK,MAAM,QAAQ,gBAAgB;EACjC,MAAM,QAAQ,QAAQ,KAAK;EAC3B,IAAI,UAAU,KAAA,GACZ,MAAM,eACJ,0CAA0C,KAAK,MACjD;EAEF,MAAM,YAAY,YAAY,MAAM,UAAU,OAAO,KAAK,IAAI,CAAC;EAC/D,IAAI,OAAO,SAAS,UAAU,OAAO,SAAS,sBAC5C,MAAM,IAAI,oBACR,4BAA4B,2BAC5B,4CAA4C,qBAAqB,aACnE;EAEF,MAAM,wBAAwB,UAAU,OAAO,QAC5C,OAAO,UAAU,QAAQ,MAAM,SAAS,QACzC,CACF;EACA,IAAI,mBAAmB,wBAAwB,wBAC7C,MAAM,IAAI,oBACR,4BAA4B,2BAC5B,4CAA4C,uBAAuB,eACrE;EAEF,oBAAoB;EACpB,OAAO,KAAK,GAAG,UAAU,MAAM;EAC/B,cAAc,KAAK;GACjB,QAAQ;GACR;GACA,YAAY,UAAU,OAAO;EAC/B,CAAC;EACD,6BAA6B,UAAU;EACvC,4BAA4B,UAAU;EACtC,0BAA0B,UAAU;EACpC,oCACE,UAAU;EACZ,oCACE,UAAU;CACd;CAEA,KAAK,MAAM,EAAE,aAAa,UAAU,cAClC,IACE,YAAY,WAAW,kCAAkC,KACzD,aAAa;EAAE;EAAa;CAAK,CAAC,MAAM,MAExC,cAAc,KAAK;EACjB,QAAQ;EACR;EACA;EACA,QAAQ;CACV,CAAC;CAIL,OAAO;EACL,iBAAA;EACA;EACA,UAAU;GACR,OAAO;GACP;GACA;GACA;GACA;GACA;EACF;CACF;AACF;;;AChvBA,MAAM,eACJ,MACA,UAEA,KAAK,WAAW,MAAM,UACtB,KAAK,OAAO,OAAO,UAAU,UAAU,MAAM,GAAG,KAAK,CAAC;AAExD,MAAa,sBACX,MACA,UACY;CACZ,IACE,KAAK,SAAS,MAAM,QACpB,KAAK,KAAK,SAAS,MAAM,KAAK,QAC9B,KAAK,KAAK,SAAS,MAAM,KAAK,QAC9B,KAAK,eAAe,MAAM,cAC1B,CAAC,YAAY,KAAK,SAAS,MAAM,OAAO,GAExC,OAAO;CAET,IAAI,KAAK,SAAS,eAAe,MAAM,SAAS,aAC9C,OAAO;CAET,IACE,KAAK,SAAS,0BACd,MAAM,SAAS,wBAEf,OACE,YAAY,KAAK,WAAW,MAAM,SAAS,KAC3C,YAAY,KAAK,SAAS,MAAM,OAAO,KACvC,YAAY,KAAK,UAAU,MAAM,QAAQ;CAG7C,IACE,KAAK,SAAS,wBACd,MAAM,SAAS,sBAEf,OAAO,YAAY,KAAK,aAAa,MAAM,WAAW;CAExD,OAAO;AACT;AAEA,MAAa,mBAAmB,EAC9B,YACA,WAC+B,GAAG,KAAK,KAAK,IAAI;;;ACzBlD,MAAM,4CAA4B,IAAI,IAAI,CACxC,oDACA,8DACF,CAAC;AACD,MAAM,gBAAgB;AACtB,MAAM,wBAAwB;AAC9B,MAAM,wBAAwB;AAE9B,IAAa,mBAAb,cAAsC,MAAM;CAC1C;CAEA,YAAY,MAA4B,SAAiB;EACvD,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,OAAO;CACd;AACF;AAkBA,MAAM,gBACJ,MACA,YACqB,IAAI,iBAAiB,MAAM,OAAO;AAEzD,MAAM,WAAW,SAAoC,KAAK,KAAK,GAAG;AAElE,MAAM,kBAAkB,UAA2B;CACjD,KAAK,MAAM,aAAa,OAAO;EAC7B,MAAM,YAAY,UAAU,YAAY,CAAC;EACzC,IACE,cAAc,KAAA,KACb,cAAc,KACb,cAAc,MACd,cAAc,OACb,YAAY,MACV,YAAY,SAAU,YAAY,SAClC,YAAY,SAAU,YAAY,SACnC,YAAY,UAEhB,OAAO;CAEX;CACA,OAAO;AACT;AAEA,MAAM,mBAAmB,OAAe,WAA4B;CAClE,IAAI,WAAW,KAAK,WAAW,MAAM,QACnC,OAAO;CAET,MAAM,WAAW,MAAM,WAAW,SAAS,CAAC;CAC5C,MAAM,OAAO,MAAM,WAAW,MAAM;CACpC,OAAO,EACL,YAAY,SACZ,YAAY,SACZ,QAAQ,SACR,QAAQ;AAEZ;AAEA,MAAM,uBACJ,aACA,cACS;CACT,IACE,CAAC,OAAO,cAAc,YAAY,KAAK,KACvC,CAAC,OAAO,cAAc,YAAY,GAAG,KACrC,YAAY,QAAQ,KACpB,YAAY,SAAS,YAAY,OACjC,YAAY,MAAM,UAAU,UAC5B,CAAC,gBAAgB,WAAW,YAAY,KAAK,KAC7C,CAAC,gBAAgB,WAAW,YAAY,GAAG,GAE3C,MAAM,aACJ,yBAAyB,oBACzB,qFACF;CAEF,IAAI,CAAC,eAAe,YAAY,WAAW,GACzC,MAAM,aACJ,yBAAyB,oBACzB,8DACF;CAEF,IAAI,QAAQ,YAAY,WAAW,CAAC,CAAC,aAAA,UACnC,MAAM,aACJ,yBAAyB,sBACzB,yCAAyC,qBAAqB,aAChE;AAEJ;AAEA,MAAM,uBACJ,OACA,gBAC+B;CAC/B,MAAM,WAAW,MAAM,SAAS,QAC7B,EAAE,KAAK,YAAY,QAAQ,YAAY,OAAO,MAAM,YAAY,KACnE;CACA,IAAI,SAAS,YAAY;CACzB,KAAK,MAAM,WAAW,UAAU;EAC9B,IACE,QAAQ,WAAW,UACnB,QAAQ,QAAQ,UAChB,QAAQ,SAAS,MAAM,YAAY,QAAQ,SAAS,UAAU,GAE9D,MAAM,aACJ,yBAAyB,wBACzB,0EACF;EAEF,SAAS,KAAK,IAAI,YAAY,KAAK,QAAQ,GAAG;CAChD;CACA,IAAI,SAAS,WAAW,KAAK,WAAW,YAAY,KAClD,MAAM,aACJ,yBAAyB,wBACzB,0EACF;CAEF,OAAO;AACT;AAEA,MAAM,oBACJ,OACA,YACqB;CACrB,MAAM,eAAe,CAAC,GAAG,QAAQ,YAAY,CAAC,CAAC,MAC5C,MAAM,UAAU,KAAK,QAAQ,MAAM,KACtC;CACA,KAAK,MAAM,CAAC,OAAO,gBAAgB,aAAa,QAAQ,GAAG;EACzD,oBAAoB,aAAa,MAAM,IAAI;EAC3C,MAAM,WAAW,UAAU,IAAI,KAAA,IAAY,aAAa,GAAG,QAAQ,CAAC;EACpE,IAAI,aAAa,KAAA,KAAa,SAAS,MAAM,YAAY,OACvD,MAAM,aACJ,yBAAyB,oBACzB,yCACF;CAEJ;CAEA,MAAM,yBAAS,IAAI,IAA4B;CAC/C,MAAM,iCAAiB,IAAI,IAAoB;CAC/C,KAAK,MAAM,WAAW,MAAM,UAAU;EACpC,IAAI,QAAQ,WAAW,QACrB;EAEF,OAAO,IAAI,QAAQ,QAAQ,OAAO,GAAG;GACnC,MAAM,QAAQ;GACd,OAAO,MAAM,KAAK,MAAM,QAAQ,OAAO,QAAQ,GAAG;EACpD,CAAC;EACD,eAAe,IACb,QAAQ,QAAQ,OAAO,GACvB,MAAM,KAAK,MAAM,QAAQ,OAAO,QAAQ,GAAG,CAC7C;CACF;CAEA,KAAK,MAAM,eAAe,aAAa,WAAW,GAAG;EACnD,MAAM,WAAW,oBAAoB,OAAO,WAAW;EACvD,MAAM,QAAQ,SAAS,GAAG,CAAC;EAC3B,MAAM,OAAO,SAAS,GAAG,EAAE;EAC3B,IAAI,UAAU,KAAA,KAAa,SAAS,KAAA,GAClC,MAAM,aACJ,yBAAyB,wBACzB,gDACF;EAEF,MAAM,cAAc,OAAO,IAAI,QAAQ,MAAM,OAAO,CAAC;EACrD,MAAM,aAAa,OAAO,IAAI,QAAQ,KAAK,OAAO,CAAC;EACnD,IAAI,gBAAgB,KAAA,KAAa,eAAe,KAAA,GAC9C,MAAM,aACJ,yBAAyB,wBACzB,6CACF;EAEF,MAAM,aAAa,YAAY,QAAQ,MAAM;EAC7C,MAAM,UAAU,YAAY,MAAM,KAAK;EACvC,IAAI,UAAU,MAAM;GAClB,YAAY,QACV,YAAY,MAAM,MAAM,GAAG,UAAU,IACrC,YAAY,cACZ,YAAY,MAAM,MAAM,OAAO;GACjC;EACF;EACA,YAAY,QACV,YAAY,MAAM,MAAM,GAAG,UAAU,IAAI,YAAY;EACvD,KAAK,MAAM,WAAW,SAAS,MAAM,GAAG,EAAE,GAAG;GAC3C,MAAM,SAAS,OAAO,IAAI,QAAQ,QAAQ,OAAO,CAAC;GAClD,IAAI,WAAW,KAAA,GACb,OAAO,QAAQ;EAEnB;EACA,WAAW,QAAQ,WAAW,MAAM,MAAM,OAAO;CACnD;CACA,OAAO,CAAC,GAAG,OAAO,QAAQ,CAAC,CAAC,CACzB,QAAQ,CAAC,KAAK,YAAY,OAAO,UAAU,eAAe,IAAI,GAAG,CAAC,CAAC,CACnE,KAAK,GAAG,YAAY,MAAM;AAC/B;AAEA,MAAM,iBAAiB,UACrB,MACG,WAAW,KAAK,OAAO,CAAC,CACxB,WAAW,KAAK,MAAM,CAAC,CACvB,WAAW,KAAK,MAAM;AAE3B,MAAM,0BAA0B,UAC9B,WAAW,KAAK,KAAK;AAEvB,MAAM,iBAAiB,QACrB,0BAA0B,IAAI,IAAI,GAAG,MACpC,IAAI,UAAU,OAAO,IAAI,UAAU;AAEtC,MAAM,qBAAqB,QACzB,OAAO,OAAO,IAAI,UAAU,CAAC,CAAC,MAC3B,cACC,UAAU,QAAQ,iBAClB,UAAU,UAAU,WACpB,UAAU,UAAU,UACxB;AAQF,MAAM,uBAAuB,EAC3B,KACA,cACA,qBACwC;CACxC,KAAK,IAAI,QAAQ,iBAAiB,GAAG,SAAS,cAAc,SAAS,GACnE,IAAI,IAAI,WAAW,OAAO,IAAI,QAAQ,OAAO,KAC3C,OAAO;CAGX,MAAM,aACJ,yBAAyB,iBACzB,qDACF;AACF;AAEA,MAAM,kBACJ,KACA,YACW;CACX,MAAM,gBAAgB,IAAI,IACxB,QAAQ,KAAK,WAAW,CAAC,QAAQ,OAAO,IAAI,GAAG,MAAM,CAAC,CACxD;CACA,MAAM,6BAAa,IAAI,IAAY;CACnC,MAAM,UAAsB,CAAC;CAC7B,MAAM,QAAwB,CAAC;CAC/B,IAAI;CAGJ,IAAI,aAA2B;CAC/B,MAAM,SAAS,IAAI,YAAY,EAAE,OAAO,KAAK,CAAC;CAC9C,OAAO,GAAG,UAAU,UAAU;EAC5B,aAAa;CACf,CAAC;CACD,OAAO,GAAG,YAAY,QAAQ;EAC5B,IAAI,MAAM,UAAA,KACR,MAAM,aACJ,yBAAyB,sBACzB,8CACF;EAEF,MAAM,SAAS,MAAM,GAAG,EAAE;EAC1B,MAAM,aAAa,QAAQ,kBAAkB;EAC7C,IAAI,WAAW,KAAA,GACb,OAAO,kBAAkB;EAE3B,MAAM,OAAO,CAAC,GAAI,QAAQ,QAAQ,CAAC,GAAI,UAAU;EACjD,MAAM,KAAK;GAAE;GAAM,gBAAgB;EAAE,CAAC;EACtC,MAAM,MAAM,QAAQ,IAAI;EACxB,IAAI,cAAc,GAAG,KAAK,cAAc,IAAI,GAAG,GAAG;GAChD,IAAI,IAAI,eACN,MAAM,aACJ,yBAAyB,wBACzB,0DACF;GAEF,aAAa;IAAE;IAAK,cAAc,OAAO;IAAU;GAAI;EACzD;CACF,CAAC;CACD,OAAO,GAAG,aAAa,QAAQ;EAC7B,IAAI,YAAY,QAAQ,KAAK;GAC3B,MAAM,SAAS,cAAc,IAAI,WAAW,GAAG;GAC/C,IAAI,WAAW,KAAA,GAAW;IACxB,MAAM,aAAa,oBAAoB;KACrC;KACA,cAAc,WAAW;KACzB,gBAAgB,OAAO;IACzB,CAAC;IACD,QAAQ,KAAK;KACX,OAAO,WAAW;KAClB,KAAK;KACL,OAAO,cAAc,OAAO,KAAK;IACnC,CAAC;IACD,IAAI,uBAAuB,OAAO,KAAK,KAAK,CAAC,kBAAkB,GAAG,GAChE,QAAQ,KAAK;KACX,OAAO,WAAW,eAAe;KACjC,KAAK,WAAW,eAAe;KAC/B,OAAO;IACT,CAAC;IAEH,WAAW,IAAI,WAAW,GAAG;GAC/B;GACA,aAAa,KAAA;EACf;EACA,MAAM,IAAI;CACZ,CAAC;CACD,IAAI;EACF,OAAO,MAAM,GAAG,CAAC,CAAC,MAAM;CAC1B,SAAS,OAAO;EACd,IAAI,iBAAiB,kBACnB,MAAM;EAER,aAAa,iBAAiB,QAAQ,wBAAQ,IAAI,MAAM,aAAa;CACvE;CACA,IAAI,eAAe,MACjB,MAAM,aACJ,yBAAyB,wBACzB,0CACF;CAEF,IAAI,WAAW,SAAS,cAAc,MACpC,MAAM,aACJ,yBAAyB,iBACzB,mDACF;CAEF,IAAI,YAAY;CAChB,KAAK,MAAM,SAAS,QAAQ,UACzB,MAAM,UAAU,MAAM,QAAQ,KAAK,KACtC,GACE,YACE,UAAU,MAAM,GAAG,MAAM,KAAK,IAC9B,MAAM,QACN,UAAU,MAAM,MAAM,GAAG;CAE7B,OAAO;AACT;AAEA,MAAM,wBAAwB,YAA8C;CAC1E,IAAI,aAAa;CACjB,KAAK,MAAM,SAAS,OAAO,OAAO,OAAO,GAAG;EAC1C,IAAI,MAAM,aAAA,UACR,MAAM,aACJ,yBAAyB,sBACzB,0CAA0C,qBAAqB,OACjE;EAEF,cAAc,MAAM;CACtB;CACA,IAAI,aAAA,WACF,MAAM,aACJ,yBAAyB,sBACzB,2CAA2C,4BAA4B,oBACzE;AAEJ;AAEA,MAAa,mBACX,SACA,aACsB;CACtB,MAAM,aAAa,gBAAgB,OAAO;CAC1C,IAAI,SAAS,WAAW,GACtB,OAAO;EACL,UAAU,QAAQ,MAAM;EACxB,qBAAqB;EACrB,yBAAyB;CAC3B;CAEF,MAAM,mBAAmB,IAAI,IAC3B,WAAW,OAAO,KAAK,UAAU,CAAC,gBAAgB,MAAM,QAAQ,GAAG,KAAK,CAAC,CAC3E;CACA,MAAM,gCAAgB,IAAI,IAAyC;CACnE,MAAM,qCAAqB,IAAI,IAAY;CAC3C,IAAI,0BAA0B;CAE9B,KAAK,MAAM,WAAW,UAAU;EAC9B,MAAM,MAAM,gBAAgB,QAAQ,QAAQ;EAC5C,IAAI,mBAAmB,IAAI,GAAG,GAC5B,MAAM,aACJ,yBAAyB,oBACzB,wDACF;EAEF,mBAAmB,IAAI,GAAG;EAC1B,MAAM,QAAQ,iBAAiB,IAAI,GAAG;EACtC,IACE,UAAU,KAAA,KACV,CAAC,mBAAmB,MAAM,UAAU,QAAQ,QAAQ,KACpD,MAAM,SAAS,QAAQ,cAEvB,MAAM,aACJ,yBAAyB,iBACzB,wDACF;EAEF,IAAI,QAAQ,aAAa,WAAW,GAClC,MAAM,aACJ,yBAAyB,oBACzB,gEACF;EAEF,IACE,0BAA0B,QAAQ,aAAa,SAC/C,uBAEA,MAAM,aACJ,yBAAyB,sBACzB,4CAA4C,sBAAsB,cACpE;EAEF,MAAM,cACJ,cAAc,IAAI,MAAM,SAAS,KAAK,IAAI,qBAAK,IAAI,IAAI;EACzD,KAAK,MAAM,UAAU,iBAAiB,OAAO,OAAO,GAClD,YAAY,IAAI,QAAQ,OAAO,IAAI,GAAG,MAAM;EAE9C,cAAc,IAAI,MAAM,SAAS,KAAK,MAAM,WAAW;EACvD,2BAA2B,QAAQ,aAAa;CAClD;CAEA,MAAM,UAAU,iBAAiB,SAAS,IAAI;CAC9C,IACE,OAAO,KAAK,OAAO,CAAC,CAAC,MAAM,SACzB,KAAK,YAAY,CAAC,CAAC,WAAW,qBAAqB,CACrD,GAEA,MAAM,aACJ,yBAAyB,wBACzB,mEACF;CAEF,KAAK,MAAM,CAAC,UAAU,YAAY,eAAe;EAC/C,MAAM,YAAY,QAAQ;EAC1B,IAAI,cAAc,KAAA,GAChB,MAAM,aACJ,yBAAyB,iBACzB,2CACF;EAEF,MAAM,MAAM,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC,CAAC,CAAC,OAAO,SAAS;EACtE,QAAQ,YAAY,QAAQ,eAAe,KAAK,CAAC,GAAG,QAAQ,OAAO,CAAC,CAAC,CAAC;CACxE;CACA,qBAAqB,OAAO;CAC5B,MAAM,WAAW,QAAQ,OAAO;CAChC,IAAI,SAAS,aAAA,UACX,MAAM,aACJ,yBAAyB,sBACzB,2CAA2C,uBAAuB,OACpE;CAEF,OAAO;EACL;EACA,qBAAqB,SAAS;EAC9B;CACF;AACF;;;AC9eA,MAAM,sBAAsB,aAC1B,SAAS,MAAM,MAAM,EAAE,aAAa,WAAW,aAAa,KAC5D,SAAS,4BAA4B,KACrC,SAAS,2BAA2B,KACpC,SAAS,mCAAmC,KAC5C,SAAS,yBAAyB,KAClC,SAAS,mCAAmC;AAE9C,MAAa,wBACX,aACyB;CACzB,MAAM,SAAS;EACb,oBAAoB,SAAS,MAAM,QAChC,EAAE,aAAa,WAAW,WAC7B,CAAC,CAAC;EACF,sBAAsB,SAAS,MAAM,QAClC,EAAE,aAAa,WAAW,aAC7B,CAAC,CAAC;EACF,2BAA2B,SAAS;EACpC,0BAA0B,SAAS;EACnC,kCAAkC,SAAS;EAC3C,wBAAwB,SAAS;EACjC,kCAAkC,SAAS;CAC7C;CACA,OAAO,mBAAmB,QAAQ,IAC9B;EAAE,QAAQ;EAAW;CAAO,IAC5B;EAAE,QAAQ;EAAQ;CAAO;AAC/B;;;ACjBA,MAAM,qCAAqC;AAC3C,MAAM,8BAA8B;AAEpC,IAAa,uBAAb,cAA0C,MAAM;CAC9C;CAEA,YAAY,MAAgC,SAAiB;EAC3D,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,OAAO;CACd;AACF;AAEA,MAAM,oBACJ,MACA,YACyB,IAAI,qBAAqB,MAAM,OAAO;AAEjE,MAAM,2BAA2B,cAC/B,UAAU,WAAW,KAAK,KAAK;AAEjC,MAAM,+BACJ,OACA,qBACY;CACZ,MAAM,QAAQ,MAAM,SAAS,GAAG,IAAI,MAAM,MAAM,GAAG,EAAE,IAAI;CACzD,MAAM,iBAAiB,MAAM,YAAY,GAAG;CAC5C,IAAI,kBAAkB,GACpB,OAAO;CAET,MAAM,SAAS,MAAM,MAAM,GAAG,cAAc;CAC5C,MAAM,qBAAqB,OAAO,YAAY,GAAG;CACjD,IAAI,sBAAsB,GACxB,OAAO;CAET,OAAO,OAAO,MAAM,qBAAqB,CAAC,MAAM;AAClD;AASA,MAAM,wBAAwB,EAC5B,MACA,kBACA,kBACA,aACwD;CACxD,MAAM,eAAsC,CAAC;CAC7C,IAAI;CACJ,KAAK,IAAI,SAAS,GAAG,SAAS,KAAK,QAAQ,UAAU,GAAG;EACtD,MAAM,YAAY,KAAK,GAAG,MAAM;EAChC,IAAI,cAAc,KAAK;GACrB,IACE,UAAU,KAAA,KACV,4BACE,KAAK,MAAM,QAAQ,GAAG,MAAM,GAC5B,gBACF,GAEA,MAAM,iBACJ,6BAA6B,oBAC7B,uEACF;GAEF,QAAQ;GACR;EACF;EACA,IAAI,cAAc,OAAO,UAAU,KAAA,GACjC;EAEF,MAAM,eAAe,SAAS;EAC9B,MAAM,YAAY,KAAK,MAAM,OAAO,YAAY;EAChD,OAAO,kBAAkB;EACzB,IAAI,OAAO,iBAAiB,6BAC1B,MAAM,iBACJ,6BAA6B,0BAC7B,+CAA+C,4BAA4B,wBAC7E;EAEF,MAAM,UAAU,4BACd,UAAU,MAAM,CAAC,GACjB,gBACF;EACA,IAAI,UAAU,SAAS,oCAAoC;GACzD,IAAI,SACF,MAAM,iBACJ,6BAA6B,oBAC7B,qDACF;GAEF,QAAQ,KAAA;GACR;EACF;EACA,IAAI,CAAC,SAAS;GACZ,QAAQ,KAAA;GACR;EACF;EACA,MAAM,cAAc,iBAAiB,SAAS;EAC9C,IAAI,gBAAgB,WAClB,aAAa,KAAK;GAAE;GAAO,KAAK;GAAc;EAAY,CAAC;OAE3D,MAAM,iBACJ,6BAA6B,oBAC7B,oEACF;EAEF,QAAQ,KAAA;CACV;CACA,IACE,UAAU,KAAA,KACV,4BAA4B,KAAK,MAAM,QAAQ,CAAC,GAAG,gBAAgB,GAEnE,MAAM,iBACJ,6BAA6B,oBAC7B,uEACF;CAEF,OAAO;AACT;AAEA,MAAa,mBAAmB,EAC9B,UACA,SACA,mBACA,6BACmD;CACnD,MAAM,YAAY,QAAQ,UAAU;CACpC,IAAI,cAAc,mBAChB,MAAM,iBACJ,6BAA6B,iBAC7B,iEACF;CAGF,MAAM,+BAAqC;EACzC,IAAI,QAAQ,YAAY,IAAI,sBAAsB,MAAM,IACtD,MAAM,iBACJ,6BAA6B,gBAC7B,kEACF;CAEJ;CACA,uBAAuB;CACvB,MAAM,qCAAqB,IAAI,IAAoB;CACnD,MAAM,oBAAoB,cAA8B;EACtD,MAAM,SAAS,mBAAmB,IAAI,SAAS;EAC/C,IAAI,WAAW,KAAA,GACb,OAAO;EAET,MAAM,WAAW,QAAQ,YAAY,WAAW,sBAAsB;EACtE,mBAAmB,IAAI,WAAW,QAAQ;EAC1C,OAAO;CACT;CACA,MAAM,mBAAmB,wBAAwB,SAAS;CAC1D,MAAM,aAAa,gBAAgB,QAAQ;CAC3C,MAAM,WAA+B,CAAC;CACtC,MAAM,SAAS,EAAE,gBAAgB,EAAE;CACnC,IAAI,2BAA2B;CAC/B,KAAK,MAAM,SAAS,WAAW,QAAQ;EACrC,MAAM,eAAe,qBAAqB;GACxC,MAAM,MAAM;GACZ;GACA;GACA;EACF,CAAC;EACD,IAAI,aAAa,WAAW,GAC1B;EAEF,4BAA4B,aAAa;EACzC,SAAS,KAAK;GACZ,UAAU,MAAM;GAChB,cAAc,MAAM;GACpB;EACF,CAAC;CACH;CACA,uBAAuB;CACvB,MAAM,WAAW,gBAAgB,UAAU,QAAQ;CACnD,OAAO;EACL,UAAU,SAAS;EACnB;EACA,oBAAoB,SAAS;EAC7B;EACA,UAAU,qBAAqB,WAAW,QAAQ;CACpD;AACF;;;AC1LA,MAAa,2CAA2C;AAExD,IAAa,yBAAb,cAA4C,MAAM;CAChD;CAEA,YAAY,MAAkC,SAAiB;EAC7D,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,OAAO;CACd;AACF;AAEA,MAAM,sBACJ,MACA,YAC2B,IAAI,uBAAuB,MAAM,OAAO;AAOrE,MAAM,wBACJ,kBACA,WACkB;CAClB,MAAM,mBAAmB,IAAI,IAC3B,iBAAiB,KAAK,UAAU,CAAC,gBAAgB,MAAM,QAAQ,GAAG,KAAK,CAAC,CAC1E;CACA,MAAM,uCAAuB,IAAI,IAAuC;CACxE,IAAI,uBAAuB;CAC3B,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,MAAM,gBAAgB,MAAM,QAAQ;EAC1C,IAAI,qBAAqB,IAAI,GAAG,GAC9B,MAAM,mBACJ,+BAA+B,yBAC/B,0DACF;EAEF,MAAM,QAAQ,iBAAiB,IAAI,GAAG;EACtC,IACE,UAAU,KAAA,KACV,CAAC,mBAAmB,MAAM,UAAU,MAAM,QAAQ,KAClD,MAAM,SAAS,MAAM,cAErB,MAAM,mBACJ,+BAA+B,yBAC/B,mEACF;EAEF,IACE,MAAM,WAAW,SAAA,MAC0B,sBAE3C,MAAM,mBACJ,+BAA+B,yBAC/B,6CAA6C,yCAAyC,mBACxF;EAEF,qBAAqB,IAAI,KAAK,KAAK;EACnC,wBAAwB,MAAM,WAAW;CAC3C;CACA,OAAO;EAAE;EAAsB;CAAqB;AACtD;AAEA,MAAa,iBAAiB,EAC5B,UACA,SACA,mBACA,QACA,mBAAmB,CAAC,GACpB,6BACmD;CACnD,MAAM,YAAY,QAAQ,UAAU;CACpC,IAAI,cAAc,mBAChB,MAAM,mBACJ,+BAA+B,iBAC/B,gEACF;CAGF,MAAM,aAAa,gBAAgB,QAAQ;CAC3C,MAAM,WAAW,qBAAqB,WAAW,QAAQ;CACzD,IACE,SAAS,WAAW,aACpB,OAAO,SAAS,SAAS,oBAAoB,aAE7C,MAAM,mBACJ,+BAA+B,oBAC/B,0EACF;CAGF,MAAM,EAAE,sBAAsB,yBAAyB,qBACrD,WAAW,QACX,gBACF;CACA,MAAM,OAAO,QAAQ,kCAAkC;EACrD,QAAQ,WAAW,OAAO,KAAK,WAAW;GACxC,UAAU,MAAM;GAChB,YACE,qBAAqB,IAAI,gBAAgB,MAAM,QAAQ,CAAC,CAAC,EAAE,cAC3D,CAAC;EACL,EAAE;EACF,GAAI,OAAO,cAAc,KAAA,IAAY,CAAC,IAAI,EAAE,WAAW,OAAO,UAAU;EACxE,GAAI,2BAA2B,KAAA,IAAY,CAAC,IAAI,EAAE,uBAAuB;CAC3E,CAAC;CACD,IAAI,KAAK,OAAO,WAAW,WAAW,OAAO,QAC3C,MAAM,mBACJ,+BAA+B,yBAC/B,sEACF;CAGF,MAAM,WAA+B,CAAC;CACtC,IAAI,cAAc;CAClB,IAAI,+BAA+B;CACnC,KAAK,MAAM,CAAC,OAAO,UAAU,WAAW,OAAO,QAAQ,GAAG;EACxD,MAAM,YAAY,KAAK,OAAO,GAAG,KAAK;EACtC,IAAI,cAAc,KAAA,GAChB,MAAM,mBACJ,+BAA+B,yBAC/B,2DACF;EAEF,eAAe,UAAU;EACzB,gCAAgC,UAAU;EAC1C,IAAI,UAAU,aAAa,WAAW,GACpC;EAEF,SAAS,KAAK;GACZ,UAAU,MAAM;GAChB,cAAc,MAAM;GACpB,cAAc,UAAU;EAC1B,CAAC;CACH;CAEA,MAAM,YAAY,gBAAgB,UAAU,QAAQ;CACpD,KAAK,OAAO;CACZ,OAAO;EACL,UAAU,UAAU;EACpB,SAAS;GACP,iBAAiB;GACjB;GACA,YAAY,WAAW,OAAO;GAC9B,qBAAqB,UAAU;GAC/B,yBAAyB,UAAU;GACnC;GACA;GACA;GACA;EACF;CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/types.ts","../src/extract.ts","../src/rewrite.ts","../src/coverage.ts","../src/restore.ts","../src/location.ts","../src/anonymize.ts"],"sourcesContent":["import type {\n NativeCallerDetection,\n NativeOperatorConfig,\n NativeSessionBlockRedactionPlan,\n NativeSessionCallerRedactionPlanOptions,\n} from \"@stll/anonymize\";\n\nexport const DOCX_PART_TYPES = {\n comments: \"comments\",\n endnotes: \"endnotes\",\n footer: \"footer\",\n footnotes: \"footnotes\",\n header: \"header\",\n mainDocument: \"main-document\",\n} as const;\n\nexport type DocxPartType =\n (typeof DOCX_PART_TYPES)[keyof typeof DOCX_PART_TYPES];\n\nexport type DocxPart = {\n type: DocxPartType;\n path: string;\n};\n\ntype DocxBaseBlockLocation = {\n part: DocxPart;\n blockIndex: number;\n xmlPath: readonly number[];\n};\n\nexport type DocxBlockLocation =\n | (DocxBaseBlockLocation & {\n type: \"paragraph\";\n })\n | (DocxBaseBlockLocation & {\n type: \"table-cell-paragraph\";\n tablePath: readonly number[];\n rowPath: readonly number[];\n cellPath: readonly number[];\n })\n | (DocxBaseBlockLocation & {\n type: \"text-box-paragraph\";\n textBoxPath: readonly number[];\n });\n\nexport type DocxInlineContext =\n | {\n type: \"hyperlink\";\n relationshipId: string | null;\n anchor: string | null;\n }\n | {\n type: \"revision\";\n revision: \"deletion\" | \"insertion\" | \"move-from\" | \"move-to\";\n };\n\nexport type DocxTextSegment = {\n start: number;\n end: number;\n source: \"break\" | \"tab\" | \"text\";\n contexts: readonly DocxInlineContext[];\n xmlPath: readonly number[];\n};\n\nexport type DocxTextBlock = {\n text: string;\n location: DocxBlockLocation;\n segments: readonly DocxTextSegment[];\n};\n\nexport type DocxCoverageItem =\n | {\n status: \"extracted\";\n part: DocxPart;\n blockCount: number;\n }\n | {\n status: \"unsupported\";\n path: string;\n contentType: string;\n reason: string;\n };\n\nexport type DocxCoverage = {\n parts: readonly DocxCoverageItem[];\n hyperlinkTextSegmentCount: number;\n revisionTextSegmentCount: number;\n unsupportedAlternateContentCount: number;\n unsupportedSymbolCount: number;\n unsupportedFieldInstructionCount: number;\n};\n\nexport type DocxExtraction = {\n contractVersion: 1;\n blocks: readonly DocxTextBlock[];\n coverage: DocxCoverage;\n};\n\nexport type DocxTextReplacement = {\n start: number;\n end: number;\n replacement: string;\n};\n\nexport type DocxBlockRewrite = {\n location: DocxBlockLocation;\n expectedText: string;\n replacements: readonly DocxTextReplacement[];\n};\n\nexport type DocxRewriteResult = {\n document: Uint8Array;\n rewrittenBlockCount: number;\n appliedReplacementCount: number;\n};\n\nexport const DOCX_COVERAGE_MODES = {\n allowPartial: \"allow-partial\",\n requireFull: \"require-full\",\n} as const;\n\nexport type DocxCoverageMode =\n (typeof DOCX_COVERAGE_MODES)[keyof typeof DOCX_COVERAGE_MODES];\n\nexport type DocxCoveragePolicy =\n | { mode: typeof DOCX_COVERAGE_MODES.requireFull }\n | { mode: typeof DOCX_COVERAGE_MODES.allowPartial };\n\nexport type DocxAnonymizationPolicy = {\n coverage: DocxCoveragePolicy;\n operators?: NativeOperatorConfig;\n};\n\nexport type DocxCallerDetection = NativeCallerDetection;\n\nexport type DocxBlockCallerDetections = {\n location: DocxBlockLocation;\n expectedText: string;\n detections: readonly DocxCallerDetection[];\n};\n\nexport type DocxSessionRedactionPlan = {\n blocks: readonly NativeSessionBlockRedactionPlan[];\n commit: () => void;\n};\n\nexport type DocxAnonymizationSession = {\n sessionId: () => string;\n planTextBatchWithCallerDetections: (\n options: NativeSessionCallerRedactionPlanOptions,\n ) => DocxSessionRedactionPlan;\n};\n\nexport type AnonymizeDocxOptions = {\n document: Uint8Array;\n session: DocxAnonymizationSession;\n expectedSessionId: string;\n policy: DocxAnonymizationPolicy;\n callerDetections?: readonly DocxBlockCallerDetections[];\n observedAtEpochSeconds?: number;\n};\n\nexport type DocxCoverageSummary = {\n extractedPartCount: number;\n unsupportedPartCount: number;\n hyperlinkTextSegmentCount: number;\n revisionTextSegmentCount: number;\n unsupportedAlternateContentCount: number;\n unsupportedSymbolCount: number;\n unsupportedFieldInstructionCount: number;\n};\n\nexport type DocxWorkflowCoverage =\n | { status: \"full\"; counts: DocxCoverageSummary }\n | { status: \"partial\"; counts: DocxCoverageSummary };\n\nexport type DocxAnonymizationSummary = {\n contractVersion: 1;\n sessionId: string;\n blockCount: number;\n rewrittenBlockCount: number;\n appliedReplacementCount: number;\n entityCount: number;\n callerDetectionCount: number;\n retainedCallerDetectionCount: number;\n coverage: DocxWorkflowCoverage;\n};\n\nexport type DocxAnonymizationResult = {\n document: Uint8Array;\n summary: DocxAnonymizationSummary;\n};\n\nexport const DOCX_ANONYMIZATION_ERROR_CODES = {\n incompleteCoverage: \"incomplete-coverage\",\n invalidCallerDetections: \"invalid-caller-detections\",\n sessionMismatch: \"session-mismatch\",\n} as const;\n\nexport type DocxAnonymizationErrorCode =\n (typeof DOCX_ANONYMIZATION_ERROR_CODES)[keyof typeof DOCX_ANONYMIZATION_ERROR_CODES];\n\nexport type DocxRestorationSession = {\n sessionId: () => string;\n restoreText: (text: string, observedAtEpochSeconds?: number) => string;\n};\n\nexport type RestoreDocxTextOptions = {\n document: Uint8Array;\n session: DocxRestorationSession;\n expectedSessionId: string;\n observedAtEpochSeconds?: number;\n};\n\nexport type DocxRestorationResult = {\n document: Uint8Array;\n sessionId: string;\n restoredBlockCount: number;\n restoredPlaceholderCount: number;\n coverage: DocxWorkflowCoverage;\n};\n\nexport const DOCX_RESTORATION_ERROR_CODES = {\n invalidPlaceholder: \"invalid-placeholder\",\n invalidSession: \"invalid-session\",\n restorationLimitExceeded: \"restoration-limit-exceeded\",\n sessionMismatch: \"session-mismatch\",\n unsupportedDocument: \"unsupported-document\",\n} as const;\n\nexport type DocxRestorationErrorCode =\n (typeof DOCX_RESTORATION_ERROR_CODES)[keyof typeof DOCX_RESTORATION_ERROR_CODES];\n\nexport const DOCX_REWRITE_ERROR_CODES = {\n invalidReplacement: \"invalid-replacement\",\n rewriteLimitExceeded: \"rewrite-limit-exceeded\",\n staleExtraction: \"stale-extraction\",\n unsupportedReplacement: \"unsupported-replacement\",\n} as const;\n\nexport type DocxRewriteErrorCode =\n (typeof DOCX_REWRITE_ERROR_CODES)[keyof typeof DOCX_REWRITE_ERROR_CODES];\n\nexport const DOCX_EXTRACTION_ERROR_CODES = {\n archiveLimitExceeded: \"archive-limit-exceeded\",\n invalidArchive: \"invalid-archive\",\n invalidPackage: \"invalid-package\",\n invalidXml: \"invalid-xml\",\n unsafeEntryPath: \"unsafe-entry-path\",\n uncompressedLimitExceeded: \"uncompressed-limit-exceeded\",\n} as const;\n\nexport type DocxExtractionErrorCode =\n (typeof DOCX_EXTRACTION_ERROR_CODES)[keyof typeof DOCX_EXTRACTION_ERROR_CODES];\n","import { loadNativeAnonymizeBinding } from \"@stll/anonymize\";\n\nimport {\n DOCX_EXTRACTION_ERROR_CODES,\n type DocxExtraction,\n type DocxExtractionErrorCode,\n} from \"./types\";\n\nexport const DOCX_EXTRACTION_CONTRACT_VERSION = 1 as const;\nexport const DOCX_ARCHIVE_MAX_BYTES = 64 * 1024 * 1024;\nexport const DOCX_ENTRY_MAX_BYTES = 16 * 1024 * 1024;\nexport const DOCX_UNCOMPRESSED_MAX_BYTES = 128 * 1024 * 1024;\nexport const DOCX_XML_MAX_DEPTH = 256;\n\nexport class DocxExtractionError extends Error {\n readonly code: DocxExtractionErrorCode;\n\n constructor(code: DocxExtractionErrorCode, message: string) {\n super(message);\n this.name = \"DocxExtractionError\";\n this.code = code;\n }\n}\n\nconst nativeExtractionErrorCode = (\n message: string,\n): DocxExtractionErrorCode => {\n if (message.includes(\"unsafe entry path\")) {\n return DOCX_EXTRACTION_ERROR_CODES.unsafeEntryPath;\n }\n if (message.includes(\"valid bounded DOCX ZIP archive\")) {\n return DOCX_EXTRACTION_ERROR_CODES.invalidArchive;\n }\n if (message.includes(\"valid XML\") || message.includes(\"valid UTF-8\")) {\n return DOCX_EXTRACTION_ERROR_CODES.invalidXml;\n }\n if (\n message.includes(\n `DOCX archives must not exceed ${DOCX_ARCHIVE_MAX_BYTES} bytes`,\n )\n ) {\n return DOCX_EXTRACTION_ERROR_CODES.archiveLimitExceeded;\n }\n if (\n message.includes(\"must not exceed\") ||\n message.includes(\"must not contain more than\") ||\n message.includes(\"at most\")\n ) {\n return DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded;\n }\n return DOCX_EXTRACTION_ERROR_CODES.invalidPackage;\n};\n\nexport const extractDocxText = (archive: Uint8Array): DocxExtraction => {\n const extract = loadNativeAnonymizeBinding().extractDocxTextJson;\n if (extract === undefined) {\n throw new DocxExtractionError(\n DOCX_EXTRACTION_ERROR_CODES.invalidPackage,\n \"Native anonymize binding does not expose DOCX extraction\",\n );\n }\n try {\n return JSON.parse(extract(archive)) as DocxExtraction;\n } catch (error) {\n const message =\n error instanceof Error ? error.message : \"DOCX extraction failed\";\n throw new DocxExtractionError(nativeExtractionErrorCode(message), message);\n }\n};\n","import { loadNativeAnonymizeBinding } from \"@stll/anonymize\";\n\nimport {\n DOCX_EXTRACTION_ERROR_CODES,\n DOCX_REWRITE_ERROR_CODES,\n type DocxBlockRewrite,\n type DocxExtractionErrorCode,\n type DocxRewriteErrorCode,\n type DocxRewriteResult,\n} from \"./types\";\nimport {\n DOCX_UNCOMPRESSED_MAX_BYTES,\n DOCX_XML_MAX_DEPTH,\n DocxExtractionError,\n} from \"./extract\";\n\nexport class DocxRewriteError extends Error {\n readonly code: DocxRewriteErrorCode;\n\n constructor(code: DocxRewriteErrorCode, message: string) {\n super(message);\n this.name = \"DocxRewriteError\";\n this.code = code;\n }\n}\n\nconst REWRITE_ERROR_CODES = new Set<DocxRewriteErrorCode>(\n Object.values(DOCX_REWRITE_ERROR_CODES),\n);\nconst EXTRACTION_ERROR_CODES = new Set<DocxExtractionErrorCode>(\n Object.values(DOCX_EXTRACTION_ERROR_CODES),\n);\nconst DOCX_REWRITE_MAX_BLOCKS = 100_000;\nconst DOCX_REWRITE_MAX_REPLACEMENTS = 1_000_000;\nconst LOCATION_PATH_KEYS = [\n \"xmlPath\",\n \"tablePath\",\n \"rowPath\",\n \"cellPath\",\n \"textBoxPath\",\n] as const;\n\nconst preflightRewritePlan = (\n rewrites: readonly DocxBlockRewrite[],\n): readonly unknown[] => {\n const rewriteCount = rewrites.length;\n if (rewriteCount > DOCX_REWRITE_MAX_BLOCKS) {\n throw new DocxRewriteError(\n DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded,\n `DOCX rewrites must not contain more than ${DOCX_REWRITE_MAX_BLOCKS} blocks`,\n );\n }\n let replacementCount = 0;\n let estimatedBytes = rewriteCount * 256;\n const serializableRewrites: unknown[] = [];\n for (let rewriteIndex = 0; rewriteIndex < rewriteCount; rewriteIndex += 1) {\n const rewrite = rewrites[rewriteIndex];\n if (rewrite === undefined) {\n throw new DocxRewriteError(\n DOCX_REWRITE_ERROR_CODES.invalidReplacement,\n \"DOCX rewrite plans must not contain sparse blocks\",\n );\n }\n if (!Array.isArray(rewrite.replacements)) {\n throw new DocxRewriteError(\n DOCX_REWRITE_ERROR_CODES.invalidReplacement,\n \"DOCX block rewrite replacements must be an array\",\n );\n }\n const blockReplacementCount = rewrite.replacements.length;\n replacementCount += blockReplacementCount;\n if (replacementCount > DOCX_REWRITE_MAX_REPLACEMENTS) {\n throw new DocxRewriteError(\n DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded,\n `DOCX rewrites must not contain more than ${DOCX_REWRITE_MAX_REPLACEMENTS} replacements`,\n );\n }\n estimatedBytes +=\n (typeof rewrite.expectedText === \"string\"\n ? rewrite.expectedText.length * 6\n : 0) +\n blockReplacementCount * 96;\n const serializableReplacements: unknown[] = [];\n for (\n let replacementIndex = 0;\n replacementIndex < blockReplacementCount;\n replacementIndex += 1\n ) {\n const replacement = rewrite.replacements[replacementIndex];\n if (replacement === undefined) {\n throw new DocxRewriteError(\n DOCX_REWRITE_ERROR_CODES.invalidReplacement,\n \"DOCX rewrite plans must not contain sparse replacements\",\n );\n }\n const value = replacement.replacement;\n if (typeof value === \"string\") {\n estimatedBytes += value.length * 6;\n }\n serializableReplacements.push({\n start: typeof replacement.start === \"number\" ? replacement.start : null,\n end: typeof replacement.end === \"number\" ? replacement.end : null,\n replacement: typeof value === \"string\" ? value : null,\n });\n }\n const location = rewrite.location as unknown as Record<string, unknown>;\n const part = location[\"part\"] as Record<string, unknown> | undefined;\n for (const value of [location[\"type\"], part?.[\"type\"], part?.[\"path\"]]) {\n if (typeof value === \"string\") {\n estimatedBytes += value.length * 6;\n }\n }\n const serializableLocation: Record<string, unknown> = {\n type: typeof location[\"type\"] === \"string\" ? location[\"type\"] : null,\n part: {\n type: typeof part?.[\"type\"] === \"string\" ? part[\"type\"] : null,\n path: typeof part?.[\"path\"] === \"string\" ? part[\"path\"] : null,\n },\n blockIndex:\n typeof location[\"blockIndex\"] === \"number\"\n ? location[\"blockIndex\"]\n : null,\n };\n for (const key of LOCATION_PATH_KEYS) {\n const path = location[key];\n if (Array.isArray(path)) {\n if (path.length > DOCX_XML_MAX_DEPTH) {\n throw new DocxRewriteError(\n DOCX_REWRITE_ERROR_CODES.invalidReplacement,\n `DOCX rewrite location paths must not exceed ${DOCX_XML_MAX_DEPTH} entries`,\n );\n }\n estimatedBytes += path.length * 24;\n const serializablePath: Array<number | null> = [];\n for (let pathIndex = 0; pathIndex < path.length; pathIndex += 1) {\n const value = path[pathIndex];\n serializablePath.push(typeof value === \"number\" ? value : null);\n }\n serializableLocation[key] = serializablePath;\n }\n }\n serializableRewrites.push({\n location: serializableLocation,\n expectedText:\n typeof rewrite.expectedText === \"string\" ? rewrite.expectedText : null,\n replacements: serializableReplacements,\n });\n if (estimatedBytes > DOCX_UNCOMPRESSED_MAX_BYTES) {\n throw new DocxRewriteError(\n DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded,\n `DOCX rewrite plans must not exceed ${DOCX_UNCOMPRESSED_MAX_BYTES} estimated serialized bytes`,\n );\n }\n }\n return serializableRewrites;\n};\n\nexport const rewriteDocxText = (\n archive: Uint8Array,\n rewrites: readonly DocxBlockRewrite[],\n): DocxRewriteResult => {\n const rewrite = loadNativeAnonymizeBinding().rewriteDocxTextNative;\n if (rewrite === undefined) {\n throw new Error(\n \"The native anonymize binding does not expose DOCX rewriting\",\n );\n }\n let serializableRewrites: readonly unknown[];\n try {\n serializableRewrites = preflightRewritePlan(rewrites);\n } catch (error) {\n if (error instanceof DocxRewriteError) {\n throw error;\n }\n const message = error instanceof Error ? error.message : String(error);\n throw new DocxRewriteError(\n DOCX_REWRITE_ERROR_CODES.invalidReplacement,\n `DOCX rewrite plan is invalid: ${message}`,\n );\n }\n let rewritesJson: string;\n try {\n rewritesJson = JSON.stringify(serializableRewrites);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new DocxRewriteError(\n DOCX_REWRITE_ERROR_CODES.invalidReplacement,\n `DOCX rewrite plan is not serializable: ${message}`,\n );\n }\n try {\n return rewrite(archive, rewritesJson);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const separator = message.indexOf(\": \");\n const rawCode = message.slice(0, separator);\n const extractionCode = rawCode as DocxExtractionErrorCode;\n if (separator > 0 && EXTRACTION_ERROR_CODES.has(extractionCode)) {\n throw new DocxExtractionError(\n extractionCode,\n message.slice(separator + 2),\n );\n }\n const code = rawCode as DocxRewriteErrorCode;\n if (separator > 0 && REWRITE_ERROR_CODES.has(code)) {\n throw new DocxRewriteError(code, message.slice(separator + 2));\n }\n throw error;\n }\n};\n","import type { DocxCoverage, DocxWorkflowCoverage } from \"./types\";\n\nconst hasPartialCoverage = (coverage: DocxCoverage): boolean =>\n coverage.parts.some(({ status }) => status === \"unsupported\") ||\n coverage.hyperlinkTextSegmentCount > 0 ||\n coverage.revisionTextSegmentCount > 0 ||\n coverage.unsupportedAlternateContentCount > 0 ||\n coverage.unsupportedSymbolCount > 0 ||\n coverage.unsupportedFieldInstructionCount > 0;\n\nexport const docxWorkflowCoverage = (\n coverage: DocxCoverage,\n): DocxWorkflowCoverage => {\n const counts = {\n extractedPartCount: coverage.parts.filter(\n ({ status }) => status === \"extracted\",\n ).length,\n unsupportedPartCount: coverage.parts.filter(\n ({ status }) => status === \"unsupported\",\n ).length,\n hyperlinkTextSegmentCount: coverage.hyperlinkTextSegmentCount,\n revisionTextSegmentCount: coverage.revisionTextSegmentCount,\n unsupportedAlternateContentCount: coverage.unsupportedAlternateContentCount,\n unsupportedSymbolCount: coverage.unsupportedSymbolCount,\n unsupportedFieldInstructionCount: coverage.unsupportedFieldInstructionCount,\n };\n return hasPartialCoverage(coverage)\n ? { status: \"partial\", counts }\n : { status: \"full\", counts };\n};\n","import { loadNativeAnonymizeBinding } from \"@stll/anonymize\";\n\nimport { docxWorkflowCoverage } from \"./coverage\";\nimport { rewriteDocxText } from \"./rewrite\";\nimport {\n DOCX_RESTORATION_ERROR_CODES,\n type DocxBlockRewrite,\n type DocxRestorationErrorCode,\n type DocxRestorationResult,\n type DocxTextReplacement,\n type RestoreDocxTextOptions,\n} from \"./types\";\n\nexport class DocxRestorationError extends Error {\n readonly code: DocxRestorationErrorCode;\n\n constructor(code: DocxRestorationErrorCode, message: string) {\n super(message);\n this.name = \"DocxRestorationError\";\n this.code = code;\n }\n}\n\nconst restorationError = (\n code: DocxRestorationErrorCode,\n message: string,\n): DocxRestorationError => new DocxRestorationError(code, message);\n\ntype NativeRestorationPlan = {\n extraction: {\n coverage: Parameters<typeof docxWorkflowCoverage>[0];\n };\n blocks: readonly {\n location: DocxBlockRewrite[\"location\"];\n expectedText: string;\n candidates: readonly {\n start: number;\n end: number;\n candidate: string;\n }[];\n }[];\n};\n\nexport const restoreDocxText = ({\n document,\n session,\n expectedSessionId,\n observedAtEpochSeconds,\n}: RestoreDocxTextOptions): DocxRestorationResult => {\n const sessionId = session.sessionId();\n if (sessionId !== expectedSessionId) {\n throw restorationError(\n DOCX_RESTORATION_ERROR_CODES.sessionMismatch,\n \"DOCX restoration session does not match the expected session id\",\n );\n }\n\n const assertSessionAvailable = (): void => {\n if (session.restoreText(\"\", observedAtEpochSeconds) !== \"\") {\n throw restorationError(\n DOCX_RESTORATION_ERROR_CODES.invalidSession,\n \"DOCX restoration session must preserve text without placeholders\",\n );\n }\n };\n assertSessionAvailable();\n const restoredCandidates = new Map<string, string>();\n const restoreCandidate = (candidate: string): string => {\n const cached = restoredCandidates.get(candidate);\n if (cached !== undefined) {\n return cached;\n }\n const restored = session.restoreText(candidate, observedAtEpochSeconds);\n restoredCandidates.set(candidate, restored);\n return restored;\n };\n const planRestoration = loadNativeAnonymizeBinding().planDocxRestorationJson;\n if (planRestoration === undefined) {\n throw restorationError(\n DOCX_RESTORATION_ERROR_CODES.invalidSession,\n \"Native anonymize binding does not expose DOCX restoration planning\",\n );\n }\n let plan: NativeRestorationPlan;\n try {\n plan = JSON.parse(\n planRestoration(document, sessionId),\n ) as NativeRestorationPlan;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const separator = message.indexOf(\": \");\n const code = message.slice(0, separator) as DocxRestorationErrorCode;\n const knownCodes = new Set<DocxRestorationErrorCode>([\n DOCX_RESTORATION_ERROR_CODES.invalidPlaceholder,\n DOCX_RESTORATION_ERROR_CODES.restorationLimitExceeded,\n DOCX_RESTORATION_ERROR_CODES.unsupportedDocument,\n ]);\n if (separator > 0 && knownCodes.has(code)) {\n throw restorationError(code, message.slice(separator + 2));\n }\n throw error;\n }\n const rewrites: DocxBlockRewrite[] = [];\n let restoredPlaceholderCount = 0;\n for (const block of plan.blocks) {\n const replacements: DocxTextReplacement[] = block.candidates.map(\n ({ candidate, end, start }) => {\n const replacement = restoreCandidate(candidate);\n if (replacement === candidate) {\n throw restorationError(\n DOCX_RESTORATION_ERROR_CODES.invalidPlaceholder,\n \"DOCX text contains an unknown placeholder for the expected session\",\n );\n }\n return { start, end, replacement };\n },\n );\n if (replacements.length === 0) {\n continue;\n }\n restoredPlaceholderCount += replacements.length;\n rewrites.push({\n location: block.location,\n expectedText: block.expectedText,\n replacements,\n });\n }\n assertSessionAvailable();\n const restored = rewriteDocxText(document, rewrites);\n return {\n document: restored.document,\n sessionId,\n restoredBlockCount: restored.rewrittenBlockCount,\n restoredPlaceholderCount,\n coverage: docxWorkflowCoverage(plan.extraction.coverage),\n };\n};\n","import type { DocxBlockLocation } from \"./types\";\n\nconst arraysEqual = (\n left: readonly number[],\n right: readonly number[],\n): boolean =>\n left.length === right.length &&\n left.every((value, index) => value === right.at(index));\n\nexport const docxLocationsEqual = (\n left: DocxBlockLocation,\n right: DocxBlockLocation,\n): boolean => {\n if (\n left.type !== right.type ||\n left.part.type !== right.part.type ||\n left.part.path !== right.part.path ||\n left.blockIndex !== right.blockIndex ||\n !arraysEqual(left.xmlPath, right.xmlPath)\n ) {\n return false;\n }\n if (left.type === \"paragraph\" && right.type === \"paragraph\") {\n return true;\n }\n if (\n left.type === \"table-cell-paragraph\" &&\n right.type === \"table-cell-paragraph\"\n ) {\n return (\n arraysEqual(left.tablePath, right.tablePath) &&\n arraysEqual(left.rowPath, right.rowPath) &&\n arraysEqual(left.cellPath, right.cellPath)\n );\n }\n if (\n left.type === \"text-box-paragraph\" &&\n right.type === \"text-box-paragraph\"\n ) {\n return arraysEqual(left.textBoxPath, right.textBoxPath);\n }\n return false;\n};\n\nexport const docxLocationKey = ({\n blockIndex,\n part,\n}: DocxBlockLocation): string => `${part.path}\\0${blockIndex}`;\n","import { docxWorkflowCoverage } from \"./coverage\";\nimport { extractDocxText } from \"./extract\";\nimport { docxLocationKey, docxLocationsEqual } from \"./location\";\nimport { rewriteDocxText } from \"./rewrite\";\nimport {\n DOCX_ANONYMIZATION_ERROR_CODES,\n DOCX_COVERAGE_MODES,\n type AnonymizeDocxOptions,\n type DocxAnonymizationErrorCode,\n type DocxAnonymizationResult,\n type DocxBlockCallerDetections,\n type DocxBlockRewrite,\n} from \"./types\";\n\nexport const DOCX_ANONYMIZATION_MAX_CALLER_DETECTIONS = 1_000_000;\n\nexport class DocxAnonymizationError extends Error {\n readonly code: DocxAnonymizationErrorCode;\n\n constructor(code: DocxAnonymizationErrorCode, message: string) {\n super(message);\n this.name = \"DocxAnonymizationError\";\n this.code = code;\n }\n}\n\nconst anonymizationError = (\n code: DocxAnonymizationErrorCode,\n message: string,\n): DocxAnonymizationError => new DocxAnonymizationError(code, message);\n\ntype DetectionPlan = {\n detectionsByLocation: ReadonlyMap<string, DocxBlockCallerDetections>;\n callerDetectionCount: number;\n};\n\nconst planCallerDetections = (\n extractionBlocks: ReturnType<typeof extractDocxText>[\"blocks\"],\n inputs: readonly DocxBlockCallerDetections[],\n): DetectionPlan => {\n const blocksByLocation = new Map(\n extractionBlocks.map((block) => [docxLocationKey(block.location), block]),\n );\n const detectionsByLocation = new Map<string, DocxBlockCallerDetections>();\n let callerDetectionCount = 0;\n for (const input of inputs) {\n const key = docxLocationKey(input.location);\n if (detectionsByLocation.has(key)) {\n throw anonymizationError(\n DOCX_ANONYMIZATION_ERROR_CODES.invalidCallerDetections,\n \"Each DOCX block may have only one caller-detection input\",\n );\n }\n const block = blocksByLocation.get(key);\n if (\n block === undefined ||\n !docxLocationsEqual(block.location, input.location) ||\n block.text !== input.expectedText\n ) {\n throw anonymizationError(\n DOCX_ANONYMIZATION_ERROR_CODES.invalidCallerDetections,\n \"DOCX caller-detection location or expected text no longer matches\",\n );\n }\n if (\n input.detections.length >\n DOCX_ANONYMIZATION_MAX_CALLER_DETECTIONS - callerDetectionCount\n ) {\n throw anonymizationError(\n DOCX_ANONYMIZATION_ERROR_CODES.invalidCallerDetections,\n `DOCX workflows must not contain more than ${DOCX_ANONYMIZATION_MAX_CALLER_DETECTIONS} caller detections`,\n );\n }\n detectionsByLocation.set(key, input);\n callerDetectionCount += input.detections.length;\n }\n return { detectionsByLocation, callerDetectionCount };\n};\n\nexport const anonymizeDocx = ({\n document,\n session,\n expectedSessionId,\n policy,\n callerDetections = [],\n observedAtEpochSeconds,\n}: AnonymizeDocxOptions): DocxAnonymizationResult => {\n const sessionId = session.sessionId();\n if (sessionId !== expectedSessionId) {\n throw anonymizationError(\n DOCX_ANONYMIZATION_ERROR_CODES.sessionMismatch,\n \"DOCX anonymization session does not match the expected session\",\n );\n }\n\n const extraction = extractDocxText(document);\n const coverage = docxWorkflowCoverage(extraction.coverage);\n if (\n coverage.status === \"partial\" &&\n policy.coverage.mode === DOCX_COVERAGE_MODES.requireFull\n ) {\n throw anonymizationError(\n DOCX_ANONYMIZATION_ERROR_CODES.incompleteCoverage,\n \"DOCX contains content outside the fully supported anonymization coverage\",\n );\n }\n\n const { detectionsByLocation, callerDetectionCount } = planCallerDetections(\n extraction.blocks,\n callerDetections,\n );\n const plan = session.planTextBatchWithCallerDetections({\n inputs: extraction.blocks.map((block) => ({\n fullText: block.text,\n detections:\n detectionsByLocation.get(docxLocationKey(block.location))?.detections ??\n [],\n })),\n ...(policy.operators === undefined ? {} : { operators: policy.operators }),\n ...(observedAtEpochSeconds === undefined ? {} : { observedAtEpochSeconds }),\n });\n if (plan.blocks.length !== extraction.blocks.length) {\n throw anonymizationError(\n DOCX_ANONYMIZATION_ERROR_CODES.invalidCallerDetections,\n \"DOCX session redaction plan does not match the extracted block count\",\n );\n }\n\n const rewrites: DocxBlockRewrite[] = [];\n let entityCount = 0;\n let retainedCallerDetectionCount = 0;\n for (const [index, block] of extraction.blocks.entries()) {\n const blockPlan = plan.blocks.at(index);\n if (blockPlan === undefined) {\n throw anonymizationError(\n DOCX_ANONYMIZATION_ERROR_CODES.invalidCallerDetections,\n \"DOCX session redaction plan is missing an extracted block\",\n );\n }\n entityCount += blockPlan.entityCount;\n retainedCallerDetectionCount += blockPlan.callerEntityCount;\n if (blockPlan.replacements.length === 0) {\n continue;\n }\n rewrites.push({\n location: block.location,\n expectedText: block.text,\n replacements: blockPlan.replacements,\n });\n }\n\n const rewritten = rewriteDocxText(document, rewrites);\n plan.commit();\n return {\n document: rewritten.document,\n summary: {\n contractVersion: 1,\n sessionId,\n blockCount: extraction.blocks.length,\n rewrittenBlockCount: rewritten.rewrittenBlockCount,\n appliedReplacementCount: rewritten.appliedReplacementCount,\n entityCount,\n callerDetectionCount,\n retainedCallerDetectionCount,\n coverage,\n },\n };\n};\n"],"mappings":";;AAOA,MAAa,kBAAkB;CAC7B,UAAU;CACV,UAAU;CACV,QAAQ;CACR,WAAW;CACX,QAAQ;CACR,cAAc;AAChB;AAsGA,MAAa,sBAAsB;CACjC,cAAc;CACd,aAAa;AACf;AA0EA,MAAa,iCAAiC;CAC5C,oBAAoB;CACpB,yBAAyB;CACzB,iBAAiB;AACnB;AAyBA,MAAa,+BAA+B;CAC1C,oBAAoB;CACpB,gBAAgB;CAChB,0BAA0B;CAC1B,iBAAiB;CACjB,qBAAqB;AACvB;AAKA,MAAa,2BAA2B;CACtC,oBAAoB;CACpB,sBAAsB;CACtB,iBAAiB;CACjB,wBAAwB;AAC1B;AAKA,MAAa,8BAA8B;CACzC,sBAAsB;CACtB,gBAAgB;CAChB,gBAAgB;CAChB,YAAY;CACZ,iBAAiB;CACjB,2BAA2B;AAC7B;;;AClPA,MAAa,mCAAmC;AAChD,MAAa,yBAAyB,KAAK,OAAO;AAClD,MAAa,uBAAuB,KAAK,OAAO;AAChD,MAAa,8BAA8B,MAAM,OAAO;AACxD,MAAa,qBAAqB;AAElC,IAAa,sBAAb,cAAyC,MAAM;CAC7C;CAEA,YAAY,MAA+B,SAAiB;EAC1D,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,OAAO;CACd;AACF;AAEA,MAAM,6BACJ,YAC4B;CAC5B,IAAI,QAAQ,SAAS,mBAAmB,GACtC,OAAO,4BAA4B;CAErC,IAAI,QAAQ,SAAS,gCAAgC,GACnD,OAAO,4BAA4B;CAErC,IAAI,QAAQ,SAAS,WAAW,KAAK,QAAQ,SAAS,aAAa,GACjE,OAAO,4BAA4B;CAErC,IACE,QAAQ,SACN,8CACF,GAEA,OAAO,4BAA4B;CAErC,IACE,QAAQ,SAAS,iBAAiB,KAClC,QAAQ,SAAS,4BAA4B,KAC7C,QAAQ,SAAS,SAAS,GAE1B,OAAO,4BAA4B;CAErC,OAAO,4BAA4B;AACrC;AAEA,MAAa,mBAAmB,YAAwC;CACtE,MAAM,UAAU,2BAA2B,CAAC,CAAC;CAC7C,IAAI,YAAY,KAAA,GACd,MAAM,IAAI,oBACR,4BAA4B,gBAC5B,0DACF;CAEF,IAAI;EACF,OAAO,KAAK,MAAM,QAAQ,OAAO,CAAC;CACpC,SAAS,OAAO;EACd,MAAM,UACJ,iBAAiB,QAAQ,MAAM,UAAU;EAC3C,MAAM,IAAI,oBAAoB,0BAA0B,OAAO,GAAG,OAAO;CAC3E;AACF;;;ACpDA,IAAa,mBAAb,cAAsC,MAAM;CAC1C;CAEA,YAAY,MAA4B,SAAiB;EACvD,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,OAAO;CACd;AACF;AAEA,MAAM,sBAAsB,IAAI,IAC9B,OAAO,OAAO,wBAAwB,CACxC;AACA,MAAM,yBAAyB,IAAI,IACjC,OAAO,OAAO,2BAA2B,CAC3C;AACA,MAAM,0BAA0B;AAChC,MAAM,gCAAgC;AACtC,MAAM,qBAAqB;CACzB;CACA;CACA;CACA;CACA;AACF;AAEA,MAAM,wBACJ,aACuB;CACvB,MAAM,eAAe,SAAS;CAC9B,IAAI,eAAe,yBACjB,MAAM,IAAI,iBACR,yBAAyB,sBACzB,4CAA4C,wBAAwB,QACtE;CAEF,IAAI,mBAAmB;CACvB,IAAI,iBAAiB,eAAe;CACpC,MAAM,uBAAkC,CAAC;CACzC,KAAK,IAAI,eAAe,GAAG,eAAe,cAAc,gBAAgB,GAAG;EACzE,MAAM,UAAU,SAAS;EACzB,IAAI,YAAY,KAAA,GACd,MAAM,IAAI,iBACR,yBAAyB,oBACzB,mDACF;EAEF,IAAI,CAAC,MAAM,QAAQ,QAAQ,YAAY,GACrC,MAAM,IAAI,iBACR,yBAAyB,oBACzB,kDACF;EAEF,MAAM,wBAAwB,QAAQ,aAAa;EACnD,oBAAoB;EACpB,IAAI,mBAAmB,+BACrB,MAAM,IAAI,iBACR,yBAAyB,sBACzB,4CAA4C,8BAA8B,cAC5E;EAEF,mBACG,OAAO,QAAQ,iBAAiB,WAC7B,QAAQ,aAAa,SAAS,IAC9B,KACJ,wBAAwB;EAC1B,MAAM,2BAAsC,CAAC;EAC7C,KACE,IAAI,mBAAmB,GACvB,mBAAmB,uBACnB,oBAAoB,GACpB;GACA,MAAM,cAAc,QAAQ,aAAa;GACzC,IAAI,gBAAgB,KAAA,GAClB,MAAM,IAAI,iBACR,yBAAyB,oBACzB,yDACF;GAEF,MAAM,QAAQ,YAAY;GAC1B,IAAI,OAAO,UAAU,UACnB,kBAAkB,MAAM,SAAS;GAEnC,yBAAyB,KAAK;IAC5B,OAAO,OAAO,YAAY,UAAU,WAAW,YAAY,QAAQ;IACnE,KAAK,OAAO,YAAY,QAAQ,WAAW,YAAY,MAAM;IAC7D,aAAa,OAAO,UAAU,WAAW,QAAQ;GACnD,CAAC;EACH;EACA,MAAM,WAAW,QAAQ;EACzB,MAAM,OAAO,SAAS;EACtB,KAAK,MAAM,SAAS;GAAC,SAAS;GAAS,OAAO;GAAS,OAAO;EAAO,GACnE,IAAI,OAAO,UAAU,UACnB,kBAAkB,MAAM,SAAS;EAGrC,MAAM,uBAAgD;GACpD,MAAM,OAAO,SAAS,YAAY,WAAW,SAAS,UAAU;GAChE,MAAM;IACJ,MAAM,OAAO,OAAO,YAAY,WAAW,KAAK,UAAU;IAC1D,MAAM,OAAO,OAAO,YAAY,WAAW,KAAK,UAAU;GAC5D;GACA,YACE,OAAO,SAAS,kBAAkB,WAC9B,SAAS,gBACT;EACR;EACA,KAAK,MAAM,OAAO,oBAAoB;GACpC,MAAM,OAAO,SAAS;GACtB,IAAI,MAAM,QAAQ,IAAI,GAAG;IACvB,IAAI,KAAK,SAAA,KACP,MAAM,IAAI,iBACR,yBAAyB,oBACzB,yDACF;IAEF,kBAAkB,KAAK,SAAS;IAChC,MAAM,mBAAyC,CAAC;IAChD,KAAK,IAAI,YAAY,GAAG,YAAY,KAAK,QAAQ,aAAa,GAAG;KAC/D,MAAM,QAAQ,KAAK;KACnB,iBAAiB,KAAK,OAAO,UAAU,WAAW,QAAQ,IAAI;IAChE;IACA,qBAAqB,OAAO;GAC9B;EACF;EACA,qBAAqB,KAAK;GACxB,UAAU;GACV,cACE,OAAO,QAAQ,iBAAiB,WAAW,QAAQ,eAAe;GACpE,cAAc;EAChB,CAAC;EACD,IAAI,iBAAA,WACF,MAAM,IAAI,iBACR,yBAAyB,sBACzB,sCAAsC,4BAA4B,4BACpE;CAEJ;CACA,OAAO;AACT;AAEA,MAAa,mBACX,SACA,aACsB;CACtB,MAAM,UAAU,2BAA2B,CAAC,CAAC;CAC7C,IAAI,YAAY,KAAA,GACd,MAAM,IAAI,MACR,6DACF;CAEF,IAAI;CACJ,IAAI;EACF,uBAAuB,qBAAqB,QAAQ;CACtD,SAAS,OAAO;EACd,IAAI,iBAAiB,kBACnB,MAAM;EAER,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EACrE,MAAM,IAAI,iBACR,yBAAyB,oBACzB,iCAAiC,SACnC;CACF;CACA,IAAI;CACJ,IAAI;EACF,eAAe,KAAK,UAAU,oBAAoB;CACpD,SAAS,OAAO;EACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EACrE,MAAM,IAAI,iBACR,yBAAyB,oBACzB,0CAA0C,SAC5C;CACF;CACA,IAAI;EACF,OAAO,QAAQ,SAAS,YAAY;CACtC,SAAS,OAAO;EACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EACrE,MAAM,YAAY,QAAQ,QAAQ,IAAI;EACtC,MAAM,UAAU,QAAQ,MAAM,GAAG,SAAS;EAC1C,MAAM,iBAAiB;EACvB,IAAI,YAAY,KAAK,uBAAuB,IAAI,cAAc,GAC5D,MAAM,IAAI,oBACR,gBACA,QAAQ,MAAM,YAAY,CAAC,CAC7B;EAEF,MAAM,OAAO;EACb,IAAI,YAAY,KAAK,oBAAoB,IAAI,IAAI,GAC/C,MAAM,IAAI,iBAAiB,MAAM,QAAQ,MAAM,YAAY,CAAC,CAAC;EAE/D,MAAM;CACR;AACF;;;AC/MA,MAAM,sBAAsB,aAC1B,SAAS,MAAM,MAAM,EAAE,aAAa,WAAW,aAAa,KAC5D,SAAS,4BAA4B,KACrC,SAAS,2BAA2B,KACpC,SAAS,mCAAmC,KAC5C,SAAS,yBAAyB,KAClC,SAAS,mCAAmC;AAE9C,MAAa,wBACX,aACyB;CACzB,MAAM,SAAS;EACb,oBAAoB,SAAS,MAAM,QAChC,EAAE,aAAa,WAAW,WAC7B,CAAC,CAAC;EACF,sBAAsB,SAAS,MAAM,QAClC,EAAE,aAAa,WAAW,aAC7B,CAAC,CAAC;EACF,2BAA2B,SAAS;EACpC,0BAA0B,SAAS;EACnC,kCAAkC,SAAS;EAC3C,wBAAwB,SAAS;EACjC,kCAAkC,SAAS;CAC7C;CACA,OAAO,mBAAmB,QAAQ,IAC9B;EAAE,QAAQ;EAAW;CAAO,IAC5B;EAAE,QAAQ;EAAQ;CAAO;AAC/B;;;AChBA,IAAa,uBAAb,cAA0C,MAAM;CAC9C;CAEA,YAAY,MAAgC,SAAiB;EAC3D,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,OAAO;CACd;AACF;AAEA,MAAM,oBACJ,MACA,YACyB,IAAI,qBAAqB,MAAM,OAAO;AAiBjE,MAAa,mBAAmB,EAC9B,UACA,SACA,mBACA,6BACmD;CACnD,MAAM,YAAY,QAAQ,UAAU;CACpC,IAAI,cAAc,mBAChB,MAAM,iBACJ,6BAA6B,iBAC7B,iEACF;CAGF,MAAM,+BAAqC;EACzC,IAAI,QAAQ,YAAY,IAAI,sBAAsB,MAAM,IACtD,MAAM,iBACJ,6BAA6B,gBAC7B,kEACF;CAEJ;CACA,uBAAuB;CACvB,MAAM,qCAAqB,IAAI,IAAoB;CACnD,MAAM,oBAAoB,cAA8B;EACtD,MAAM,SAAS,mBAAmB,IAAI,SAAS;EAC/C,IAAI,WAAW,KAAA,GACb,OAAO;EAET,MAAM,WAAW,QAAQ,YAAY,WAAW,sBAAsB;EACtE,mBAAmB,IAAI,WAAW,QAAQ;EAC1C,OAAO;CACT;CACA,MAAM,kBAAkB,2BAA2B,CAAC,CAAC;CACrD,IAAI,oBAAoB,KAAA,GACtB,MAAM,iBACJ,6BAA6B,gBAC7B,oEACF;CAEF,IAAI;CACJ,IAAI;EACF,OAAO,KAAK,MACV,gBAAgB,UAAU,SAAS,CACrC;CACF,SAAS,OAAO;EACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EACrE,MAAM,YAAY,QAAQ,QAAQ,IAAI;EACtC,MAAM,OAAO,QAAQ,MAAM,GAAG,SAAS;EACvC,MAAM,6BAAa,IAAI,IAA8B;GACnD,6BAA6B;GAC7B,6BAA6B;GAC7B,6BAA6B;EAC/B,CAAC;EACD,IAAI,YAAY,KAAK,WAAW,IAAI,IAAI,GACtC,MAAM,iBAAiB,MAAM,QAAQ,MAAM,YAAY,CAAC,CAAC;EAE3D,MAAM;CACR;CACA,MAAM,WAA+B,CAAC;CACtC,IAAI,2BAA2B;CAC/B,KAAK,MAAM,SAAS,KAAK,QAAQ;EAC/B,MAAM,eAAsC,MAAM,WAAW,KAC1D,EAAE,WAAW,KAAK,YAAY;GAC7B,MAAM,cAAc,iBAAiB,SAAS;GAC9C,IAAI,gBAAgB,WAClB,MAAM,iBACJ,6BAA6B,oBAC7B,oEACF;GAEF,OAAO;IAAE;IAAO;IAAK;GAAY;EACnC,CACF;EACA,IAAI,aAAa,WAAW,GAC1B;EAEF,4BAA4B,aAAa;EACzC,SAAS,KAAK;GACZ,UAAU,MAAM;GAChB,cAAc,MAAM;GACpB;EACF,CAAC;CACH;CACA,uBAAuB;CACvB,MAAM,WAAW,gBAAgB,UAAU,QAAQ;CACnD,OAAO;EACL,UAAU,SAAS;EACnB;EACA,oBAAoB,SAAS;EAC7B;EACA,UAAU,qBAAqB,KAAK,WAAW,QAAQ;CACzD;AACF;;;ACtIA,MAAM,eACJ,MACA,UAEA,KAAK,WAAW,MAAM,UACtB,KAAK,OAAO,OAAO,UAAU,UAAU,MAAM,GAAG,KAAK,CAAC;AAExD,MAAa,sBACX,MACA,UACY;CACZ,IACE,KAAK,SAAS,MAAM,QACpB,KAAK,KAAK,SAAS,MAAM,KAAK,QAC9B,KAAK,KAAK,SAAS,MAAM,KAAK,QAC9B,KAAK,eAAe,MAAM,cAC1B,CAAC,YAAY,KAAK,SAAS,MAAM,OAAO,GAExC,OAAO;CAET,IAAI,KAAK,SAAS,eAAe,MAAM,SAAS,aAC9C,OAAO;CAET,IACE,KAAK,SAAS,0BACd,MAAM,SAAS,wBAEf,OACE,YAAY,KAAK,WAAW,MAAM,SAAS,KAC3C,YAAY,KAAK,SAAS,MAAM,OAAO,KACvC,YAAY,KAAK,UAAU,MAAM,QAAQ;CAG7C,IACE,KAAK,SAAS,wBACd,MAAM,SAAS,sBAEf,OAAO,YAAY,KAAK,aAAa,MAAM,WAAW;CAExD,OAAO;AACT;AAEA,MAAa,mBAAmB,EAC9B,YACA,WAC+B,GAAG,KAAK,KAAK,IAAI;;;ACjClD,MAAa,2CAA2C;AAExD,IAAa,yBAAb,cAA4C,MAAM;CAChD;CAEA,YAAY,MAAkC,SAAiB;EAC7D,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,OAAO;CACd;AACF;AAEA,MAAM,sBACJ,MACA,YAC2B,IAAI,uBAAuB,MAAM,OAAO;AAOrE,MAAM,wBACJ,kBACA,WACkB;CAClB,MAAM,mBAAmB,IAAI,IAC3B,iBAAiB,KAAK,UAAU,CAAC,gBAAgB,MAAM,QAAQ,GAAG,KAAK,CAAC,CAC1E;CACA,MAAM,uCAAuB,IAAI,IAAuC;CACxE,IAAI,uBAAuB;CAC3B,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,MAAM,gBAAgB,MAAM,QAAQ;EAC1C,IAAI,qBAAqB,IAAI,GAAG,GAC9B,MAAM,mBACJ,+BAA+B,yBAC/B,0DACF;EAEF,MAAM,QAAQ,iBAAiB,IAAI,GAAG;EACtC,IACE,UAAU,KAAA,KACV,CAAC,mBAAmB,MAAM,UAAU,MAAM,QAAQ,KAClD,MAAM,SAAS,MAAM,cAErB,MAAM,mBACJ,+BAA+B,yBAC/B,mEACF;EAEF,IACE,MAAM,WAAW,SAAA,MAC0B,sBAE3C,MAAM,mBACJ,+BAA+B,yBAC/B,6CAA6C,yCAAyC,mBACxF;EAEF,qBAAqB,IAAI,KAAK,KAAK;EACnC,wBAAwB,MAAM,WAAW;CAC3C;CACA,OAAO;EAAE;EAAsB;CAAqB;AACtD;AAEA,MAAa,iBAAiB,EAC5B,UACA,SACA,mBACA,QACA,mBAAmB,CAAC,GACpB,6BACmD;CACnD,MAAM,YAAY,QAAQ,UAAU;CACpC,IAAI,cAAc,mBAChB,MAAM,mBACJ,+BAA+B,iBAC/B,gEACF;CAGF,MAAM,aAAa,gBAAgB,QAAQ;CAC3C,MAAM,WAAW,qBAAqB,WAAW,QAAQ;CACzD,IACE,SAAS,WAAW,aACpB,OAAO,SAAS,SAAS,oBAAoB,aAE7C,MAAM,mBACJ,+BAA+B,oBAC/B,0EACF;CAGF,MAAM,EAAE,sBAAsB,yBAAyB,qBACrD,WAAW,QACX,gBACF;CACA,MAAM,OAAO,QAAQ,kCAAkC;EACrD,QAAQ,WAAW,OAAO,KAAK,WAAW;GACxC,UAAU,MAAM;GAChB,YACE,qBAAqB,IAAI,gBAAgB,MAAM,QAAQ,CAAC,CAAC,EAAE,cAC3D,CAAC;EACL,EAAE;EACF,GAAI,OAAO,cAAc,KAAA,IAAY,CAAC,IAAI,EAAE,WAAW,OAAO,UAAU;EACxE,GAAI,2BAA2B,KAAA,IAAY,CAAC,IAAI,EAAE,uBAAuB;CAC3E,CAAC;CACD,IAAI,KAAK,OAAO,WAAW,WAAW,OAAO,QAC3C,MAAM,mBACJ,+BAA+B,yBAC/B,sEACF;CAGF,MAAM,WAA+B,CAAC;CACtC,IAAI,cAAc;CAClB,IAAI,+BAA+B;CACnC,KAAK,MAAM,CAAC,OAAO,UAAU,WAAW,OAAO,QAAQ,GAAG;EACxD,MAAM,YAAY,KAAK,OAAO,GAAG,KAAK;EACtC,IAAI,cAAc,KAAA,GAChB,MAAM,mBACJ,+BAA+B,yBAC/B,2DACF;EAEF,eAAe,UAAU;EACzB,gCAAgC,UAAU;EAC1C,IAAI,UAAU,aAAa,WAAW,GACpC;EAEF,SAAS,KAAK;GACZ,UAAU,MAAM;GAChB,cAAc,MAAM;GACpB,cAAc,UAAU;EAC1B,CAAC;CACH;CAEA,MAAM,YAAY,gBAAgB,UAAU,QAAQ;CACpD,KAAK,OAAO;CACZ,OAAO;EACL,UAAU,UAAU;EACpB,SAAS;GACP,iBAAiB;GACjB;GACA,YAAY,WAAW,OAAO;GAC9B,qBAAqB,UAAU;GAC/B,yBAAyB,UAAU;GACnC;GACA;GACA;GACA;EACF;CACF;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/anonymize-docx",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Structure-aware DOCX text extraction and rewriting for stella anonymization workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -26,19 +26,19 @@
|
|
|
26
26
|
"license": "Apache-2.0",
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build": "tsdown",
|
|
29
|
-
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json",
|
|
29
|
+
"typecheck": "bun ../../scripts/tsc-native.ts --noEmit -p tsconfig.json && bun ../../scripts/tsc-native.ts --noEmit -p tsconfig.test.json",
|
|
30
30
|
"test": "bun test",
|
|
31
31
|
"format": "oxfmt ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@stll/anonymize": "^2.
|
|
35
|
-
"fflate": "^0.8.3"
|
|
36
|
-
"saxes": "^6.0.0"
|
|
34
|
+
"@stll/anonymize": "^2.3.0",
|
|
35
|
+
"fflate": "^0.8.3"
|
|
37
36
|
},
|
|
38
37
|
"devDependencies": {
|
|
39
38
|
"@types/node": "^26.1.1",
|
|
40
39
|
"bun-types": "^1.3.14",
|
|
41
|
-
"
|
|
40
|
+
"fflate": "^0.8.3",
|
|
41
|
+
"tsdown": "^0.22.7",
|
|
42
42
|
"typescript": "^6.0.3"
|
|
43
43
|
}
|
|
44
44
|
}
|