@stll/folio-core 0.64.1 → 0.66.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/dist/ai-edits/apply.js +31 -9
- package/dist/ai-edits/headless.js +3 -3
- package/dist/compare/reproducible-package.d.ts +5 -0
- package/dist/compare/reproducible-package.js +10 -23
- package/dist/docx/archiveInflation.d.ts +8 -103
- package/dist/docx/archiveInflation.js +8 -145
- package/dist/docx/attributeRemainder.js +1 -1
- package/dist/docx/canonicalStoryRepack.js +2 -2
- package/dist/docx/documentSectionFacts.js +1 -1
- package/dist/docx/ensureParaIds.js +9 -24
- package/dist/docx/importedIdCollisions.js +1 -1
- package/dist/docx/mathToMathml.js +1 -1
- package/dist/docx/metadataPrivacy.js +38 -48
- package/dist/docx/numericIdNormalization.js +2 -2
- package/dist/docx/packageParts.d.ts +4 -4
- package/dist/docx/packageParts.js +17 -24
- package/dist/docx/parser.js +5 -6
- package/dist/docx/removeHeaderFooterParts.d.ts +2 -2
- package/dist/docx/removeHeaderFooterParts.js +19 -36
- package/dist/docx/revisionIdNormalization.js +2 -2
- package/dist/docx/rezip.d.ts +7 -7
- package/dist/docx/rezip.js +144 -532
- package/dist/docx/sdtProperties.js +1 -2
- package/dist/docx/selectedXmlIdentityScan.d.ts +1 -1
- package/dist/docx/selectedXmlIdentityScan.js +1 -1
- package/dist/docx/selectiveSave.js +37 -41
- package/dist/docx/selectiveSaveFlags.d.ts +1 -1
- package/dist/docx/selectiveSaveTripwire.js +5 -11
- package/dist/docx/selectiveXmlPatch.js +2 -2
- package/dist/docx/server/applyDocxXmlPatchProposal.js +20 -41
- package/dist/docx/server/boundedArchive.d.ts +2 -3
- package/dist/docx/server/boundedArchive.js +78 -99
- package/dist/docx/server/inspectDocxPackage.d.ts +1 -1
- package/dist/docx/server/inspectDocxPackage.js +1 -1
- package/dist/docx/server/validateDocxConformance.js +1 -1
- package/dist/docx/shapeAlternateContent.js +1 -1
- package/dist/docx/storyBlockReplay.js +1 -1
- package/dist/docx/unzip.d.ts +2 -3
- package/dist/docx/unzip.js +127 -155
- package/dist/docx/verbatimCapture.js +1 -1
- package/dist/docx/xmlParser.d.ts +5 -36
- package/dist/docx/xmlParser.js +14 -78
- package/dist/docx/xmlSafety.js +5 -5
- package/dist/layout-painter/renderPage.js +4 -2
- package/dist/markdown/countParagraphList.d.ts +18 -0
- package/dist/markdown/countParagraphList.js +90 -0
- package/dist/markdown/renderParagraph.js +2 -62
- package/dist/markdown/renderRuns.js +4 -1
- package/dist/prosemirror/conversion/fromProseDoc.js +5 -3
- package/dist/prosemirror/conversion/toProseDoc.js +4 -1
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +4 -1
- package/dist/prosemirror/replacedAnnotations.d.ts +6 -0
- package/dist/prosemirror/replacedAnnotations.js +30 -8
- package/dist/prosemirror/schema/nodes.d.ts +7 -0
- package/dist/prosemirror/textBoxAnchorAttrs.js +9 -1
- package/package.json +3 -5
- package/dist/docx/streamingXmlParser.d.ts +0 -60
- package/dist/docx/streamingXmlParser.js +0 -406
- package/dist/docx/xmlNamespaceContext.d.ts +0 -6
- package/dist/docx/xmlNamespaceContext.js +0 -53
- package/dist/docx/xmlResourceLimits.d.ts +0 -107
- package/dist/docx/xmlResourceLimits.js +0 -194
|
@@ -69,8 +69,7 @@ const W15_LOCAL_NAMES = /* @__PURE__ */ new Set(["repeatingSection", "repeatingS
|
|
|
69
69
|
* would produce invalid XML in the saved DOCX — Word refuses files
|
|
70
70
|
* with unresolved namespace prefixes.
|
|
71
71
|
*
|
|
72
|
-
* Heuristics,
|
|
73
|
-
* preserveOrder mode:
|
|
72
|
+
* Heuristics, from when the XML reader did not surface namespace URIs:
|
|
74
73
|
*
|
|
75
74
|
* 1. If the captured wrapper element itself uses a non-`w` prefix, that
|
|
76
75
|
* prefix is taken to be bound to the WP URI (the source DOCX would
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { scanStreamingXmlElements } from "
|
|
1
|
+
import { scanStreamingXmlElements } from "@stll/docx-core/xml";
|
|
2
2
|
/** Identity census follows the parser's first-Choice, otherwise-Fallback policy. */
|
|
3
3
|
export declare const scanSelectedStreamingXmlElements: (xml: string, visitor: Parameters<typeof scanStreamingXmlElements>[1]) => {
|
|
4
4
|
status: "unsupported";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { scanStreamingXmlElements } from "./streamingXmlParser.js";
|
|
2
1
|
import { NAMESPACES, getLocalName, getNamespaceUri, selectAlternateContentBranch } from "./xmlParser.js";
|
|
2
|
+
import { scanStreamingXmlElements } from "@stll/docx-core/xml";
|
|
3
3
|
// src/docx/selectedXmlIdentityScan.ts
|
|
4
4
|
const isCompatibilityElement = (element, name) => getNamespaceUri(element) === NAMESPACES.mc && getLocalName(element.name) === name;
|
|
5
5
|
/** Identity census follows the parser's first-Choice, otherwise-Fallback policy. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodeText
|
|
1
|
+
import { decodeText } from "../utils/textEncoding.js";
|
|
2
2
|
import { parseCommentsExtended } from "./commentParser.js";
|
|
3
3
|
import { withoutExhaustedComments, withoutOrphanCommentRanges } from "./commentRangeIntegrity.js";
|
|
4
4
|
import { hasUnsynthesizedReplyRanges } from "./commentReplyMarkers.js";
|
|
@@ -18,7 +18,7 @@ import { serializeNumberingXml } from "./serializer/numberingSerializer.js";
|
|
|
18
18
|
import { readRootNamespaceBindings } from "./serializer/partNamespaces.js";
|
|
19
19
|
import { buildStructuralDocumentPatch } from "./structuralXmlPatch.js";
|
|
20
20
|
import { DOCX_CONFORMANCE_CLASSES } from "@stll/docx-core/model";
|
|
21
|
-
import {
|
|
21
|
+
import { ZipArchive } from "@stll/docx-core/zip";
|
|
22
22
|
// src/docx/selectiveSave.ts
|
|
23
23
|
/**
|
|
24
24
|
* Splice edited footnote/endnote paragraphs into their parts and return the
|
|
@@ -39,22 +39,22 @@ async function patchNoteParts(zip, doc, candidateIds, updates) {
|
|
|
39
39
|
const remainingIds = new Set(candidateIds);
|
|
40
40
|
if (footnotes.length === 0 && endnotes.length === 0) return remainingIds;
|
|
41
41
|
const findPart = (conventionalLowerPath) => {
|
|
42
|
-
const direct = zip.
|
|
42
|
+
const direct = zip.entry(conventionalLowerPath);
|
|
43
43
|
if (direct) return direct;
|
|
44
|
-
for (const [path, file] of
|
|
44
|
+
for (const [path, file] of zip.entries().map((archiveEntry) => [archiveEntry.path, archiveEntry])) if (path.toLowerCase() === conventionalLowerPath) return file;
|
|
45
45
|
return null;
|
|
46
46
|
};
|
|
47
47
|
const patchPart = async (conventionalLowerPath, serialize) => {
|
|
48
48
|
const file = findPart(conventionalLowerPath);
|
|
49
49
|
if (!file) return true;
|
|
50
|
-
const originalXml = await file.
|
|
50
|
+
const originalXml = await file.bytes().then((bytes) => decodeText(bytes, { ignoreBOM: true }));
|
|
51
51
|
const originalIds = collectParaIds(originalXml);
|
|
52
52
|
const partIds = /* @__PURE__ */ new Set();
|
|
53
53
|
for (const id of remainingIds) if (originalIds.has(id)) partIds.add(id);
|
|
54
54
|
if (partIds.size === 0) return true;
|
|
55
55
|
const patched = buildPatchedNoteXml(originalXml, serialize(), partIds);
|
|
56
56
|
if (patched === null) return false;
|
|
57
|
-
updates.set(file.
|
|
57
|
+
updates.set(file.path, patched);
|
|
58
58
|
for (const id of partIds) remainingIds.delete(id);
|
|
59
59
|
return true;
|
|
60
60
|
};
|
|
@@ -63,9 +63,9 @@ async function patchNoteParts(zip, doc, candidateIds, updates) {
|
|
|
63
63
|
return remainingIds;
|
|
64
64
|
}
|
|
65
65
|
const findZipEntryCaseInsensitive = (zip, lowerPath) => {
|
|
66
|
-
const direct = zip.
|
|
66
|
+
const direct = zip.entry(lowerPath);
|
|
67
67
|
if (direct) return direct;
|
|
68
|
-
for (const [path, file] of
|
|
68
|
+
for (const [path, file] of zip.entries().map((archiveEntry) => [archiveEntry.path, archiveEntry])) if (path.toLowerCase() === lowerPath) return file;
|
|
69
69
|
return null;
|
|
70
70
|
};
|
|
71
71
|
const commentsExtendedInfoEqual = (a, b) => {
|
|
@@ -94,9 +94,9 @@ async function patchCommentsExtended(zip, plan, updates) {
|
|
|
94
94
|
const desiredXml = serializeCommentsExtended(plan);
|
|
95
95
|
const existing = findZipEntryCaseInsensitive(zip, COMMENTS_EXTENDED_PART_LOWER);
|
|
96
96
|
if (!desiredXml) return existing === null;
|
|
97
|
-
const baselineInfo = existing ? parseCommentsExtended(await existing.
|
|
97
|
+
const baselineInfo = existing ? parseCommentsExtended(await existing.bytes().then((bytes) => decodeText(bytes, { ignoreBOM: true }))) : /* @__PURE__ */ new Map();
|
|
98
98
|
if (commentsExtendedInfoEqual(parseCommentsExtended(desiredXml), baselineInfo)) return true;
|
|
99
|
-
updates.set(existing?.
|
|
99
|
+
updates.set(existing?.path ?? "word/commentsExtended.xml", desiredXml);
|
|
100
100
|
return ensureCommentsExtendedPackaging(zip, updates);
|
|
101
101
|
}
|
|
102
102
|
/**
|
|
@@ -107,12 +107,12 @@ async function patchCommentsExtended(zip, plan, updates) {
|
|
|
107
107
|
*/
|
|
108
108
|
async function ensureCommentsExtendedPackaging(zip, updates) {
|
|
109
109
|
const ctPath = "[Content_Types].xml";
|
|
110
|
-
const ctXml = updates.get(ctPath) ?? await zip.
|
|
110
|
+
const ctXml = updates.get(ctPath) ?? await zip.entry(ctPath)?.bytes().then((bytes) => decodeText(bytes, { ignoreBOM: true }));
|
|
111
111
|
if (ctXml === void 0) return false;
|
|
112
112
|
const nextCt = addCommentsExtendedOverride(ctXml);
|
|
113
113
|
if (nextCt !== ctXml) updates.set(ctPath, nextCt);
|
|
114
114
|
const relsPath = "word/_rels/document.xml.rels";
|
|
115
|
-
const relsXml = updates.get(relsPath) ?? await zip.
|
|
115
|
+
const relsXml = updates.get(relsPath) ?? await zip.entry(relsPath)?.bytes().then((bytes) => decodeText(bytes, { ignoreBOM: true }));
|
|
116
116
|
if (relsXml === void 0) return false;
|
|
117
117
|
const nextRels = addCommentsExtendedRelationship(relsXml);
|
|
118
118
|
if (nextRels !== relsXml) updates.set(relsPath, nextRels);
|
|
@@ -139,15 +139,15 @@ async function patchNumberingPart(zip, doc, updates) {
|
|
|
139
139
|
const numbering = doc.package.numbering;
|
|
140
140
|
if (!numbering || numbering.abstractNums.length === 0 && numbering.nums.length === 0) return true;
|
|
141
141
|
const conventionalLowerPath = "word/numbering.xml";
|
|
142
|
-
let file = zip.
|
|
142
|
+
let file = zip.entry(conventionalLowerPath);
|
|
143
143
|
if (!file) {
|
|
144
|
-
for (const [path, entry] of
|
|
144
|
+
for (const [path, entry] of zip.entries().map((archiveEntry) => [archiveEntry.path, archiveEntry])) if (path.toLowerCase() === conventionalLowerPath) {
|
|
145
145
|
file = entry;
|
|
146
146
|
break;
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
if (!file) return false;
|
|
150
|
-
const originalXml = await file.
|
|
150
|
+
const originalXml = await file.bytes().then((bytes) => decodeText(bytes, { ignoreBOM: true }));
|
|
151
151
|
const baselineXml = serializeNumberingXml(parseNumbering(originalXml).definitions);
|
|
152
152
|
const currentXml = serializeNumberingXml(numbering);
|
|
153
153
|
if (currentXml === baselineXml) return true;
|
|
@@ -157,7 +157,7 @@ async function patchNumberingPart(zip, doc, updates) {
|
|
|
157
157
|
currentXml
|
|
158
158
|
});
|
|
159
159
|
if (patched === null) return false;
|
|
160
|
-
if (patched !== originalXml) updates.set(file.
|
|
160
|
+
if (patched !== originalXml) updates.set(file.path, patched);
|
|
161
161
|
return true;
|
|
162
162
|
}
|
|
163
163
|
/**
|
|
@@ -175,10 +175,10 @@ async function patchNumberingPart(zip, doc, updates) {
|
|
|
175
175
|
* same package for the same source.
|
|
176
176
|
*/
|
|
177
177
|
const queueSettingsUpdates = async (zip, updates) => {
|
|
178
|
-
const settingsFile = zip.
|
|
178
|
+
const settingsFile = zip.entry("word/settings.xml");
|
|
179
179
|
if (!settingsFile) return;
|
|
180
|
-
const relsFile = zip.
|
|
181
|
-
const filtered = withoutAttachedTemplate(await settingsFile.
|
|
180
|
+
const relsFile = zip.entry("word/_rels/settings.xml.rels");
|
|
181
|
+
const filtered = withoutAttachedTemplate(await settingsFile.bytes().then((bytes) => decodeText(bytes, { ignoreBOM: true })), await relsFile?.bytes().then((bytes) => decodeText(bytes, { ignoreBOM: true })));
|
|
182
182
|
if (filtered.settingsXml !== void 0) updates.set("word/settings.xml", filtered.settingsXml);
|
|
183
183
|
if (filtered.relsXml !== void 0) updates.set("word/_rels/settings.xml.rels", filtered.relsXml);
|
|
184
184
|
};
|
|
@@ -206,21 +206,17 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
|
|
|
206
206
|
const ownsCommentsPart = hasOwnedCommentsPart(doc);
|
|
207
207
|
const commentPlan = planCommentParts(comments);
|
|
208
208
|
try {
|
|
209
|
-
const zip = await
|
|
209
|
+
const zip = await ZipArchive.read(originalBuffer);
|
|
210
210
|
if (await hasUnmaterializedHyperlinkBindings(doc, zip)) return null;
|
|
211
|
-
for (const
|
|
211
|
+
for (const path of zip.paths()) if (isUnsafePackagePath(path)) return null;
|
|
212
212
|
if (originalBuffer !== doc.originalBuffer) {
|
|
213
213
|
const sourceParts = /* @__PURE__ */ new Map();
|
|
214
|
-
for (const [path, file] of
|
|
214
|
+
for (const [path, file] of zip.entries().map((archiveEntry) => [archiveEntry.path, archiveEntry])) {
|
|
215
215
|
const lowerPath = path.toLowerCase();
|
|
216
|
-
if (
|
|
217
|
-
sourceParts.set(path, await file.
|
|
216
|
+
if (!lowerPath.startsWith("word/") || !lowerPath.endsWith(".xml")) continue;
|
|
217
|
+
sourceParts.set(path, await file.bytes().then((bytes) => decodeText(bytes, { ignoreBOM: true })));
|
|
218
218
|
}
|
|
219
|
-
for (const [path, xml] of normalizeImportedNumericIds(sourceParts)) if (xml !== sourceParts.get(path))
|
|
220
|
-
zip,
|
|
221
|
-
path,
|
|
222
|
-
data: encodeUtf8(xml)
|
|
223
|
-
});
|
|
219
|
+
for (const [path, xml] of normalizeImportedNumericIds(sourceParts)) if (xml !== sourceParts.get(path)) zip.set(path, xml);
|
|
224
220
|
}
|
|
225
221
|
if (options.bodyAuthority === "canonical") await publishCanonicalImageResources({
|
|
226
222
|
document: doc,
|
|
@@ -229,9 +225,9 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
|
|
|
229
225
|
});
|
|
230
226
|
const updates = /* @__PURE__ */ new Map();
|
|
231
227
|
if (changedParaIds.size > 0 || structuralChange) {
|
|
232
|
-
const docXmlFile = zip.
|
|
228
|
+
const docXmlFile = zip.entry("word/document.xml");
|
|
233
229
|
if (!docXmlFile) return null;
|
|
234
|
-
const originalDocXml = await docXmlFile.
|
|
230
|
+
const originalDocXml = await docXmlFile.bytes().then((bytes) => decodeText(bytes, { ignoreBOM: true }));
|
|
235
231
|
const serializedDocXml = serializeDocument(doc, readRootNamespaceBindings(originalDocXml), {
|
|
236
232
|
xml: originalDocXml,
|
|
237
233
|
bodyAuthority: options.bodyAuthority,
|
|
@@ -241,7 +237,7 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
|
|
|
241
237
|
const originalBodyParaIds = structuralChange ? collectParaIds(originalDocXml) : void 0;
|
|
242
238
|
if (structuralChange && doc.package.numbering) {
|
|
243
239
|
const numberingFile = findZipEntryCaseInsensitive(zip, "word/numbering.xml");
|
|
244
|
-
const baselineNumbering = numberingFile ? serializeNumberingXml(parseNumbering(await numberingFile.
|
|
240
|
+
const baselineNumbering = numberingFile ? serializeNumberingXml(parseNumbering(await numberingFile.bytes().then((bytes) => decodeText(bytes, { ignoreBOM: true }))).definitions) : "";
|
|
245
241
|
const added = collectAddedNumberingDefs(baselineNumbering, serializeNumberingXml(doc.package.numbering));
|
|
246
242
|
if (added.abstractNums.size > 0 || added.nums.size > 0) return null;
|
|
247
243
|
}
|
|
@@ -264,23 +260,23 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
|
|
|
264
260
|
}
|
|
265
261
|
}
|
|
266
262
|
const sourceCommentsFile = findZipEntryCaseInsensitive(zip, "word/comments.xml");
|
|
267
|
-
const commentsPartPath = sourceCommentsFile?.
|
|
268
|
-
const sourceCommentsXml = sourceCommentsFile ? await sourceCommentsFile.
|
|
263
|
+
const commentsPartPath = sourceCommentsFile?.path ?? "word/comments.xml";
|
|
264
|
+
const sourceCommentsXml = sourceCommentsFile ? await sourceCommentsFile.bytes().then((bytes) => decodeText(bytes, { ignoreBOM: true })) : void 0;
|
|
269
265
|
const commentsUnchanged = sourceCommentsXml !== void 0 && canReplayCommentsPart(doc, sourceCommentsXml);
|
|
270
266
|
if (!commentsUnchanged && (hasComments || ownsCommentsPart || sourceCommentsFile)) {
|
|
271
267
|
const commentsXml = serializeComments(commentPlan, sourceCommentsXml === void 0 ? void 0 : readRootNamespaceBindings(sourceCommentsXml));
|
|
272
268
|
if (commentsXml !== sourceCommentsXml) updates.set(commentsPartPath, commentsXml);
|
|
273
269
|
}
|
|
274
270
|
if (hasComments || ownsCommentsPart) {
|
|
275
|
-
const ctFile = zip.
|
|
271
|
+
const ctFile = zip.entry("[Content_Types].xml");
|
|
276
272
|
if (ctFile) {
|
|
277
|
-
const ctXml = await ctFile.
|
|
273
|
+
const ctXml = await ctFile.bytes().then((bytes) => decodeText(bytes, { ignoreBOM: true }));
|
|
278
274
|
if (!ctXml.toLowerCase().includes("/word/comments.xml")) updates.set("[Content_Types].xml", ctXml.replace("</Types>", `<Override PartName="/${commentsPartPath}" ContentType="${COMMENTS_CONTENT_TYPE}"/></Types>`));
|
|
279
275
|
}
|
|
280
276
|
const relsPath = "word/_rels/document.xml.rels";
|
|
281
|
-
const relsFile = zip.
|
|
277
|
+
const relsFile = zip.entry(relsPath);
|
|
282
278
|
if (relsFile) {
|
|
283
|
-
const relsXml = await relsFile.
|
|
279
|
+
const relsXml = await relsFile.bytes().then((bytes) => decodeText(bytes, { ignoreBOM: true }));
|
|
284
280
|
if (!relsXml.toLowerCase().includes("comments.xml")) {
|
|
285
281
|
const maxId = findMaxRId(relsXml);
|
|
286
282
|
updates.set(relsPath, relsXml.replace("</Relationships>", `<Relationship Id="rId${maxId + 1}" Type="${RELATIONSHIP_TYPES.comments}" Target="${commentsPartPath.slice(5)}"/></Relationships>`));
|
|
@@ -308,16 +304,16 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
|
|
|
308
304
|
onDiagnostic: options.onDiagnostic
|
|
309
305
|
})) updates.set(path, xml);
|
|
310
306
|
await queueSettingsUpdates(zip, updates);
|
|
311
|
-
const corePropsFile = zip.
|
|
307
|
+
const corePropsFile = zip.entry("docProps/core.xml");
|
|
312
308
|
if (corePropsFile) {
|
|
313
|
-
const corePropsXml = await corePropsFile.
|
|
309
|
+
const corePropsXml = await corePropsFile.bytes().then((bytes) => decodeText(bytes, { ignoreBOM: true }));
|
|
314
310
|
updates.set("docProps/core.xml", updateCoreProperties(corePropsXml, { updateModifiedDate: true }));
|
|
315
311
|
}
|
|
316
312
|
if ([...updates.keys()].some((path) => {
|
|
317
313
|
const lowerPath = path.toLowerCase();
|
|
318
314
|
return lowerPath.startsWith("word/") && (lowerPath.endsWith(".xml") || lowerPath.endsWith(".rels"));
|
|
319
315
|
})) {
|
|
320
|
-
const sourceDocumentXml = await findZipEntryCaseInsensitive(zip, "word/document.xml")?.
|
|
316
|
+
const sourceDocumentXml = await findZipEntryCaseInsensitive(zip, "word/document.xml")?.text();
|
|
321
317
|
if (detectDocxConformanceClass(sourceDocumentXml ?? null) === DOCX_CONFORMANCE_CLASSES.STRICT) return null;
|
|
322
318
|
}
|
|
323
319
|
return await applyUpdatesToZip(zip, updates);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - `selectiveSaveTripwire` — run selective + full and compare bytes for CI
|
|
12
12
|
* observability. Never blocks the user-visible save.
|
|
13
13
|
* - `selectiveSaveMaxBytes` — refuse the selective path for original buffers
|
|
14
|
-
* above this size, since holding them in memory plus the
|
|
14
|
+
* above this size, since holding them in memory plus the archive overhead is
|
|
15
15
|
* the dominant cost.
|
|
16
16
|
*/
|
|
17
17
|
export type FolioSelectiveSaveFlags = {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { panic } from "better-result";
|
|
2
|
+
import { ZipArchive } from "@stll/docx-core/zip";
|
|
2
3
|
// src/docx/selectiveSaveTripwire.ts
|
|
3
4
|
/**
|
|
4
5
|
* Selective Save Tripwire
|
|
@@ -16,17 +17,10 @@ import { panic } from "better-result";
|
|
|
16
17
|
*/
|
|
17
18
|
const IGNORED_PATHS = /* @__PURE__ */ new Set(["docProps/core.xml"]);
|
|
18
19
|
async function listEntries(buffer) {
|
|
19
|
-
const zip = await
|
|
20
|
-
const paths = [];
|
|
21
|
-
for (const [path, entry] of Object.entries(zip.files)) {
|
|
22
|
-
if (entry.dir) continue;
|
|
23
|
-
if (IGNORED_PATHS.has(path)) continue;
|
|
24
|
-
paths.push(path);
|
|
25
|
-
}
|
|
26
|
-
paths.sort();
|
|
20
|
+
const zip = await ZipArchive.read(buffer);
|
|
27
21
|
return {
|
|
28
22
|
zip,
|
|
29
|
-
paths
|
|
23
|
+
paths: zip.paths().filter((path) => !IGNORED_PATHS.has(path)).toSorted()
|
|
30
24
|
};
|
|
31
25
|
}
|
|
32
26
|
function bytesEqual(a, b) {
|
|
@@ -35,9 +29,9 @@ function bytesEqual(a, b) {
|
|
|
35
29
|
return true;
|
|
36
30
|
}
|
|
37
31
|
function readEntryBytes(zip, path, side) {
|
|
38
|
-
const entry = zip.
|
|
32
|
+
const entry = zip.entry(path);
|
|
39
33
|
if (!entry) panic(`Missing ${side} zip entry: ${path}`);
|
|
40
|
-
return entry.
|
|
34
|
+
return entry.bytes();
|
|
41
35
|
}
|
|
42
36
|
function readCommonEntryBytes(selectiveZip, fullZip) {
|
|
43
37
|
return Promise.all(selectiveZip.paths.map(async (path) => {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { patchBreaksCommentRangeBalance } from "./commentRangeIntegrity.js";
|
|
2
2
|
import { resolveParagraphIdentities } from "./paraIdAttribute.js";
|
|
3
3
|
import { readRootNamespaceBindings } from "./serializer/partNamespaces.js";
|
|
4
|
-
import { scanStreamingXmlElements } from "./streamingXmlParser.js";
|
|
5
4
|
import { captureVerbatimXml } from "./verbatimCapture.js";
|
|
6
5
|
import { PARAGRAPH_SCAN_NAMES, hasCanonicalWordprocessingPrefixes, matchCloseTag, matchOpenTag, resolveSelectiveScanPrefixes, splicesAsCanonical } from "./wordprocessingPrefixes.js";
|
|
7
|
-
import { resolveNamespaceUri } from "./xmlNamespaceContext.js";
|
|
8
6
|
import { NAMESPACES, WORDPROCESSINGML_NAMESPACE_URIS, cloneElement, findAttributeByNamespaceUri, getAttributeByNamespaceUri, getChildElements, getLocalName, getNamespacePrefix, getNamespaceUri, parseXmlDocument } from "./xmlParser.js";
|
|
7
|
+
import { scanStreamingXmlElements } from "@stll/docx-core/xml";
|
|
8
|
+
import { resolveNamespaceUri } from "@stll/docx-core/xml/namespaces";
|
|
9
9
|
// src/docx/selectiveXmlPatch.ts
|
|
10
10
|
/**
|
|
11
11
|
* Selective XML Patch Module
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { encodeUtf8 } from "../../utils/textEncoding.js";
|
|
2
1
|
import { loadDocxArchive } from "./boundedArchive.js";
|
|
3
2
|
import { FOLIO_DOCX_XML_PATCH_PROPOSAL_PROFILE, InvalidFolioDocxXmlPatchProposalError, evaluateDocxXmlPatchProposal, parseFolioDocxXmlPatchProposal } from "./evaluateDocxXmlPatchProposal.js";
|
|
4
3
|
import { FOLIO_DOCX_CONFORMANCE_PROFILE, validateDocxConformance } from "./validateDocxConformance.js";
|
|
5
4
|
import { TaggedError, panic } from "better-result";
|
|
6
5
|
import { createHash } from "node:crypto";
|
|
7
|
-
import
|
|
8
|
-
import { writeZipPart } from "@stll/docx-core/zip";
|
|
6
|
+
import { ZipArchive } from "@stll/docx-core/zip";
|
|
9
7
|
// src/docx/server/applyDocxXmlPatchProposal.ts
|
|
10
8
|
const FOLIO_DOCX_XML_PATCH_APPLICATION_VERSION = 1;
|
|
11
9
|
const FOLIO_DOCX_XML_PATCH_APPLICATION_PROFILE = "folio-xml-patch-application-v1";
|
|
@@ -60,29 +58,26 @@ const verifyArchiveContents = async (bytes, options) => {
|
|
|
60
58
|
cause
|
|
61
59
|
});
|
|
62
60
|
}
|
|
63
|
-
for (const entry of archive.entryMetadata.toSorted(({ path: left }, { path: right }) => left < right ? -1 : Number(left > right))) {
|
|
64
|
-
if (entry.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
cause
|
|
78
|
-
});
|
|
79
|
-
}
|
|
61
|
+
for (const entry of archive.entryMetadata.toSorted(({ path: left }, { path: right }) => left < right ? -1 : Number(left > right))) try {
|
|
62
|
+
if (await archive.readEntryUint8(entry.path) === null) throw new FolioDocxXmlPatchApplicationError({
|
|
63
|
+
message: "An archive entry disappeared during verification.",
|
|
64
|
+
stage: "verify",
|
|
65
|
+
part: entry.path
|
|
66
|
+
});
|
|
67
|
+
} catch (cause) {
|
|
68
|
+
if (cause instanceof FolioDocxXmlPatchApplicationError) throw cause;
|
|
69
|
+
throw new FolioDocxXmlPatchApplicationError({
|
|
70
|
+
message: "An archive entry could not be verified within the configured limits.",
|
|
71
|
+
stage: "verify",
|
|
72
|
+
part: entry.path,
|
|
73
|
+
cause
|
|
74
|
+
});
|
|
80
75
|
}
|
|
81
76
|
};
|
|
82
77
|
const applyReplacements = async ({ bytes, proposal }) => {
|
|
83
78
|
let zip;
|
|
84
79
|
try {
|
|
85
|
-
zip = await
|
|
80
|
+
zip = await ZipArchive.read(bytes);
|
|
86
81
|
} catch (cause) {
|
|
87
82
|
throw new FolioDocxXmlPatchApplicationError({
|
|
88
83
|
message: "The evaluated package could not be loaded for replacement.",
|
|
@@ -91,31 +86,15 @@ const applyReplacements = async ({ bytes, proposal }) => {
|
|
|
91
86
|
});
|
|
92
87
|
}
|
|
93
88
|
for (const replacement of proposal.replacements) {
|
|
94
|
-
const current = zip.
|
|
95
|
-
if (current ===
|
|
89
|
+
const current = zip.entry(replacement.path);
|
|
90
|
+
if (current === void 0) throw new FolioDocxXmlPatchApplicationError({
|
|
96
91
|
message: "An evaluated package part was unavailable during replacement.",
|
|
97
92
|
stage: "replace"
|
|
98
93
|
});
|
|
99
|
-
|
|
100
|
-
zip,
|
|
101
|
-
path: replacement.path,
|
|
102
|
-
data: encodeUtf8(replacement.replacementXml),
|
|
103
|
-
options: {
|
|
104
|
-
date: current.date,
|
|
105
|
-
comment: current.comment,
|
|
106
|
-
unixPermissions: current.unixPermissions,
|
|
107
|
-
dosPermissions: current.dosPermissions,
|
|
108
|
-
compression: "DEFLATE",
|
|
109
|
-
compressionOptions: { level: 6 }
|
|
110
|
-
}
|
|
111
|
-
});
|
|
94
|
+
zip.set(replacement.path, replacement.replacementXml, { modified: current.modified });
|
|
112
95
|
}
|
|
113
96
|
try {
|
|
114
|
-
return await zip.
|
|
115
|
-
type: "uint8array",
|
|
116
|
-
compression: "DEFLATE",
|
|
117
|
-
compressionOptions: { level: 6 }
|
|
118
|
-
});
|
|
97
|
+
return await zip.generate({ level: 6 });
|
|
119
98
|
} catch (cause) {
|
|
120
99
|
throw new FolioDocxXmlPatchApplicationError({
|
|
121
100
|
message: "The replacement package could not be generated.",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { XmlResourceLimits } from "../xmlResourceLimits.js";
|
|
2
1
|
import { TaggedErrorClass } from "better-result";
|
|
2
|
+
import { XmlResourceLimits } from "@stll/docx-core/xml/limits";
|
|
3
3
|
export declare const DOCX_MAX_ENTRY_BYTES: number;
|
|
4
4
|
export declare const DOCX_MAX_TOTAL_BYTES: number;
|
|
5
5
|
export declare const DOCX_MAX_ENTRIES = 4096;
|
|
@@ -37,8 +37,7 @@ export type DocxArchiveOptions = {
|
|
|
37
37
|
};
|
|
38
38
|
export type DocxArchiveEntry = {
|
|
39
39
|
readonly path: string;
|
|
40
|
-
readonly
|
|
41
|
-
readonly declaredUncompressedBytes: number | null;
|
|
40
|
+
readonly declaredUncompressedBytes: number;
|
|
42
41
|
};
|
|
43
42
|
export type DocxArchiveReadOptions = {
|
|
44
43
|
maxBytes?: number;
|