@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.
Files changed (62) hide show
  1. package/dist/ai-edits/apply.js +31 -9
  2. package/dist/ai-edits/headless.js +3 -3
  3. package/dist/compare/reproducible-package.d.ts +5 -0
  4. package/dist/compare/reproducible-package.js +10 -23
  5. package/dist/docx/archiveInflation.d.ts +8 -103
  6. package/dist/docx/archiveInflation.js +8 -145
  7. package/dist/docx/attributeRemainder.js +1 -1
  8. package/dist/docx/canonicalStoryRepack.js +2 -2
  9. package/dist/docx/documentSectionFacts.js +1 -1
  10. package/dist/docx/ensureParaIds.js +9 -24
  11. package/dist/docx/importedIdCollisions.js +1 -1
  12. package/dist/docx/mathToMathml.js +1 -1
  13. package/dist/docx/metadataPrivacy.js +38 -48
  14. package/dist/docx/numericIdNormalization.js +2 -2
  15. package/dist/docx/packageParts.d.ts +4 -4
  16. package/dist/docx/packageParts.js +17 -24
  17. package/dist/docx/parser.js +5 -6
  18. package/dist/docx/removeHeaderFooterParts.d.ts +2 -2
  19. package/dist/docx/removeHeaderFooterParts.js +19 -36
  20. package/dist/docx/revisionIdNormalization.js +2 -2
  21. package/dist/docx/rezip.d.ts +7 -7
  22. package/dist/docx/rezip.js +144 -532
  23. package/dist/docx/sdtProperties.js +1 -2
  24. package/dist/docx/selectedXmlIdentityScan.d.ts +1 -1
  25. package/dist/docx/selectedXmlIdentityScan.js +1 -1
  26. package/dist/docx/selectiveSave.js +37 -41
  27. package/dist/docx/selectiveSaveFlags.d.ts +1 -1
  28. package/dist/docx/selectiveSaveTripwire.js +5 -11
  29. package/dist/docx/selectiveXmlPatch.js +2 -2
  30. package/dist/docx/server/applyDocxXmlPatchProposal.js +20 -41
  31. package/dist/docx/server/boundedArchive.d.ts +2 -3
  32. package/dist/docx/server/boundedArchive.js +78 -99
  33. package/dist/docx/server/inspectDocxPackage.d.ts +1 -1
  34. package/dist/docx/server/inspectDocxPackage.js +1 -1
  35. package/dist/docx/server/validateDocxConformance.js +1 -1
  36. package/dist/docx/shapeAlternateContent.js +1 -1
  37. package/dist/docx/storyBlockReplay.js +1 -1
  38. package/dist/docx/unzip.d.ts +2 -3
  39. package/dist/docx/unzip.js +127 -155
  40. package/dist/docx/verbatimCapture.js +1 -1
  41. package/dist/docx/xmlParser.d.ts +5 -36
  42. package/dist/docx/xmlParser.js +14 -78
  43. package/dist/docx/xmlSafety.js +5 -5
  44. package/dist/layout-painter/renderPage.js +4 -2
  45. package/dist/markdown/countParagraphList.d.ts +18 -0
  46. package/dist/markdown/countParagraphList.js +90 -0
  47. package/dist/markdown/renderParagraph.js +2 -62
  48. package/dist/markdown/renderRuns.js +4 -1
  49. package/dist/prosemirror/conversion/fromProseDoc.js +5 -3
  50. package/dist/prosemirror/conversion/toProseDoc.js +4 -1
  51. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +4 -1
  52. package/dist/prosemirror/replacedAnnotations.d.ts +6 -0
  53. package/dist/prosemirror/replacedAnnotations.js +30 -8
  54. package/dist/prosemirror/schema/nodes.d.ts +7 -0
  55. package/dist/prosemirror/textBoxAnchorAttrs.js +9 -1
  56. package/package.json +3 -5
  57. package/dist/docx/streamingXmlParser.d.ts +0 -60
  58. package/dist/docx/streamingXmlParser.js +0 -406
  59. package/dist/docx/xmlNamespaceContext.d.ts +0 -6
  60. package/dist/docx/xmlNamespaceContext.js +0 -53
  61. package/dist/docx/xmlResourceLimits.d.ts +0 -107
  62. 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, since fast-xml-parser does not surface namespace URIs in
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 "./streamingXmlParser.js";
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, encodeUtf8 } from "../utils/textEncoding.js";
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 { writeZipPart } from "@stll/docx-core/zip";
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.file(conventionalLowerPath);
42
+ const direct = zip.entry(conventionalLowerPath);
43
43
  if (direct) return direct;
44
- for (const [path, file] of Object.entries(zip.files)) if (!file.dir && path.toLowerCase() === conventionalLowerPath) return file;
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.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true }));
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.name, patched);
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.file(lowerPath);
66
+ const direct = zip.entry(lowerPath);
67
67
  if (direct) return direct;
68
- for (const [path, file] of Object.entries(zip.files)) if (!file.dir && path.toLowerCase() === lowerPath) return file;
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.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true }))) : /* @__PURE__ */ new Map();
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?.name ?? "word/commentsExtended.xml", desiredXml);
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.file(ctPath)?.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true }));
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.file(relsPath)?.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true }));
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.file(conventionalLowerPath);
142
+ let file = zip.entry(conventionalLowerPath);
143
143
  if (!file) {
144
- for (const [path, entry] of Object.entries(zip.files)) if (!entry.dir && path.toLowerCase() === conventionalLowerPath) {
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.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true }));
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.name, patched);
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.file("word/settings.xml");
178
+ const settingsFile = zip.entry("word/settings.xml");
179
179
  if (!settingsFile) return;
180
- const relsFile = zip.file("word/_rels/settings.xml.rels");
181
- const filtered = withoutAttachedTemplate(await settingsFile.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true })), await relsFile?.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true })));
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 (await import("jszip")).default.loadAsync(originalBuffer);
209
+ const zip = await ZipArchive.read(originalBuffer);
210
210
  if (await hasUnmaterializedHyperlinkBindings(doc, zip)) return null;
211
- for (const [path, file] of Object.entries(zip.files)) if (!file.dir && isUnsafePackagePath(path)) return null;
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 Object.entries(zip.files)) {
214
+ for (const [path, file] of zip.entries().map((archiveEntry) => [archiveEntry.path, archiveEntry])) {
215
215
  const lowerPath = path.toLowerCase();
216
- if (file.dir || !lowerPath.startsWith("word/") || !lowerPath.endsWith(".xml")) continue;
217
- sourceParts.set(path, await file.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true })));
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)) writeZipPart({
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.file("word/document.xml");
228
+ const docXmlFile = zip.entry("word/document.xml");
233
229
  if (!docXmlFile) return null;
234
- const originalDocXml = await docXmlFile.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true }));
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.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true }))).definitions) : "";
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?.name ?? "word/comments.xml";
268
- const sourceCommentsXml = sourceCommentsFile ? await sourceCommentsFile.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true })) : void 0;
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.file("[Content_Types].xml");
271
+ const ctFile = zip.entry("[Content_Types].xml");
276
272
  if (ctFile) {
277
- const ctXml = await ctFile.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true }));
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.file(relsPath);
277
+ const relsFile = zip.entry(relsPath);
282
278
  if (relsFile) {
283
- const relsXml = await relsFile.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true }));
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.file("docProps/core.xml");
307
+ const corePropsFile = zip.entry("docProps/core.xml");
312
308
  if (corePropsFile) {
313
- const corePropsXml = await corePropsFile.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true }));
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")?.async("text");
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 JSZip overhead is
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 (await import("jszip")).default.loadAsync(buffer);
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.file(path);
32
+ const entry = zip.entry(path);
39
33
  if (!entry) panic(`Missing ${side} zip entry: ${path}`);
40
- return entry.async("uint8array");
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 JSZip from "jszip";
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.directory) continue;
65
- try {
66
- if (await archive.readEntryUint8(entry.path) === null) throw new FolioDocxXmlPatchApplicationError({
67
- message: "An archive entry disappeared during verification.",
68
- stage: "verify",
69
- part: entry.path
70
- });
71
- } catch (cause) {
72
- if (cause instanceof FolioDocxXmlPatchApplicationError) throw cause;
73
- throw new FolioDocxXmlPatchApplicationError({
74
- message: "An archive entry could not be verified within the configured limits.",
75
- stage: "verify",
76
- part: entry.path,
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 JSZip.loadAsync(bytes);
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.file(replacement.path);
95
- if (current === null) throw new FolioDocxXmlPatchApplicationError({
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
- writeZipPart({
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.generateAsync({
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 directory: boolean;
41
- readonly declaredUncompressedBytes: number | null;
40
+ readonly declaredUncompressedBytes: number;
42
41
  };
43
42
  export type DocxArchiveReadOptions = {
44
43
  maxBytes?: number;