@stll/folio-core 0.32.2 → 0.33.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 (222) hide show
  1. package/dist/ai-edits/__fixtures__/paragraphs.d.ts +17 -0
  2. package/dist/ai-edits/__fixtures__/paragraphs.js +33 -0
  3. package/dist/ai-edits/apply.d.ts +49 -3
  4. package/dist/ai-edits/apply.js +649 -85
  5. package/dist/ai-edits/blockRange.d.ts +7 -3
  6. package/dist/ai-edits/blockRange.js +14 -6
  7. package/dist/ai-edits/headless.d.ts +39 -5
  8. package/dist/ai-edits/headless.js +76 -17
  9. package/dist/ai-edits/index.d.ts +6 -6
  10. package/dist/ai-edits/index.js +3 -3
  11. package/dist/ai-edits/read.d.ts +4 -0
  12. package/dist/ai-edits/read.js +30 -6
  13. package/dist/ai-edits/scoped-reading.js +4 -3
  14. package/dist/ai-edits/snapshot.d.ts +42 -2
  15. package/dist/ai-edits/snapshot.js +127 -62
  16. package/dist/ai-edits/table-row-column-mutations.d.ts +24 -1
  17. package/dist/ai-edits/table-row-column-mutations.js +98 -6
  18. package/dist/ai-edits/table-targets.d.ts +12 -3
  19. package/dist/ai-edits/table-targets.js +15 -5
  20. package/dist/ai-edits/types.d.ts +219 -15
  21. package/dist/ai-edits/word-diff.d.ts +54 -11
  22. package/dist/ai-edits/word-diff.js +221 -61
  23. package/dist/compare/__fixtures__/body-sequence.d.ts +33 -0
  24. package/dist/compare/__fixtures__/body-sequence.js +67 -0
  25. package/dist/compare/__fixtures__/nested-table.d.ts +16 -0
  26. package/dist/compare/__fixtures__/nested-table.js +50 -0
  27. package/dist/compare/__fixtures__/numbered-list.d.ts +34 -0
  28. package/dist/compare/__fixtures__/numbered-list.js +85 -0
  29. package/dist/compare/column-alignment.d.ts +27 -0
  30. package/dist/compare/column-alignment.js +162 -0
  31. package/dist/compare/compare.d.ts +107 -0
  32. package/dist/compare/compare.js +406 -0
  33. package/dist/compare/formatting.d.ts +29 -0
  34. package/dist/compare/formatting.js +84 -0
  35. package/dist/compare/plan.d.ts +22 -0
  36. package/dist/compare/plan.js +1059 -0
  37. package/dist/compare/reproducible-package.d.ts +17 -0
  38. package/dist/compare/reproducible-package.js +30 -0
  39. package/dist/compare/scenario.d.ts +77 -0
  40. package/dist/compare/scenario.js +263 -0
  41. package/dist/compare/types.d.ts +282 -0
  42. package/dist/compare/types.js +32 -0
  43. package/dist/compare/verification.d.ts +55 -0
  44. package/dist/compare/verification.js +146 -0
  45. package/dist/compat/eigenpal.d.ts +9 -6
  46. package/dist/compat/eigenpal.js +6 -3
  47. package/dist/controller/layoutPipeline.d.ts +13 -0
  48. package/dist/controller/layoutPipeline.js +15 -1
  49. package/dist/display-list/build/buildContext.d.ts +39 -0
  50. package/dist/display-list/build/buildContext.js +31 -0
  51. package/dist/display-list/build/buildDisplayList.d.ts +79 -0
  52. package/dist/display-list/build/buildDisplayList.js +283 -0
  53. package/dist/display-list/build/colors.d.ts +20 -0
  54. package/dist/display-list/build/colors.js +136 -0
  55. package/dist/display-list/build/floatingImages.d.ts +24 -0
  56. package/dist/display-list/build/floatingImages.js +41 -0
  57. package/dist/display-list/build/fontTable.d.ts +31 -0
  58. package/dist/display-list/build/fontTable.js +97 -0
  59. package/dist/display-list/build/furniture.d.ts +86 -0
  60. package/dist/display-list/build/furniture.js +98 -0
  61. package/dist/display-list/build/glyphs.d.ts +55 -0
  62. package/dist/display-list/build/glyphs.js +103 -0
  63. package/dist/display-list/build/headerFooterPrimitives.d.ts +17 -0
  64. package/dist/display-list/build/headerFooterPrimitives.js +40 -0
  65. package/dist/display-list/build/imagePrimitives.d.ts +35 -0
  66. package/dist/display-list/build/imagePrimitives.js +264 -0
  67. package/dist/display-list/build/pageBorderPrimitives.d.ts +23 -0
  68. package/dist/display-list/build/pageBorderPrimitives.js +111 -0
  69. package/dist/display-list/build/pageFurniture.d.ts +35 -0
  70. package/dist/display-list/build/pageFurniture.js +141 -0
  71. package/dist/display-list/build/paragraphPrimitives.d.ts +19 -0
  72. package/dist/display-list/build/paragraphPrimitives.js +819 -0
  73. package/dist/display-list/build/regions.d.ts +46 -0
  74. package/dist/display-list/build/regions.js +56 -0
  75. package/dist/display-list/build/storyPrimitives.d.ts +29 -0
  76. package/dist/display-list/build/storyPrimitives.js +279 -0
  77. package/dist/display-list/build/strokes.d.ts +19 -0
  78. package/dist/display-list/build/strokes.js +78 -0
  79. package/dist/display-list/build/tablePrimitives.d.ts +38 -0
  80. package/dist/display-list/build/tablePrimitives.js +409 -0
  81. package/dist/display-list/build/textBoxPrimitives.d.ts +14 -0
  82. package/dist/display-list/build/textBoxPrimitives.js +129 -0
  83. package/dist/display-list/build/textDecorations.d.ts +29 -0
  84. package/dist/display-list/build/textDecorations.js +29 -0
  85. package/dist/display-list/build/unsupported.d.ts +57 -0
  86. package/dist/display-list/build/unsupported.js +0 -0
  87. package/dist/display-list/build/watermarkPrimitives.d.ts +15 -0
  88. package/dist/display-list/build/watermarkPrimitives.js +128 -0
  89. package/dist/display-list/dom/renderDisplayListToDom.d.ts +26 -0
  90. package/dist/display-list/dom/renderDisplayListToDom.js +620 -0
  91. package/dist/display-list/editor/displayListPagePainter.d.ts +22 -0
  92. package/dist/display-list/editor/displayListPagePainter.js +63 -0
  93. package/dist/display-list/editor/pageRenderer.d.ts +24 -0
  94. package/dist/display-list/editor/pageRenderer.js +36 -0
  95. package/dist/display-list/primitives.d.ts +95 -0
  96. package/dist/display-list/primitives.js +127 -0
  97. package/dist/display-list/types.d.ts +472 -0
  98. package/dist/display-list/types.js +0 -0
  99. package/dist/document-operations.d.ts +39 -8
  100. package/dist/document-operations.js +169 -16
  101. package/dist/document-stories.d.ts +16 -0
  102. package/dist/document-stories.js +48 -4
  103. package/dist/docx/packageParts.d.ts +35 -0
  104. package/dist/docx/packageParts.js +120 -0
  105. package/dist/docx/paragraphParser.js +22 -22
  106. package/dist/docx/paragraphTraversal.js +1 -1
  107. package/dist/docx/renderedPageBreakNormalization.js +3 -1
  108. package/dist/docx/revisionIdNormalization.d.ts +13 -0
  109. package/dist/docx/revisionIdNormalization.js +103 -0
  110. package/dist/docx/rezip.js +26 -17
  111. package/dist/docx/selectiveSave.js +2 -2
  112. package/dist/docx/serializer/commentSerializer.js +6 -1
  113. package/dist/docx/serializer/paragraphSerializer.js +33 -4
  114. package/dist/docx/serializer/runSerializer.js +11 -10
  115. package/dist/docx/serializer/tableSerializer.js +2 -1
  116. package/dist/docx/server/applyDocxXmlPatchProposal.js +1 -1
  117. package/dist/docx/server/createBilingualDocx.js +3 -2
  118. package/dist/docx/streamingXmlParser.d.ts +13 -1
  119. package/dist/docx/streamingXmlParser.js +49 -11
  120. package/dist/docx/unzip.d.ts +1 -2
  121. package/dist/docx/unzip.js +25 -6
  122. package/dist/docx/xmlParser.d.ts +3 -1
  123. package/dist/docx/xmlParser.js +33 -27
  124. package/dist/export-pdf.d.ts +43 -0
  125. package/dist/export-pdf.js +98 -0
  126. package/dist/fonts/headlessMeasure.d.ts +76 -0
  127. package/dist/fonts/headlessMeasure.js +0 -0
  128. package/dist/fonts/sfnt/parse.d.ts +51 -0
  129. package/dist/fonts/sfnt/parse.js +525 -0
  130. package/dist/fonts/sfnt/subset.d.ts +20 -0
  131. package/dist/fonts/sfnt/subset.js +350 -0
  132. package/dist/fonts/sfnt/tables.d.ts +88 -0
  133. package/dist/fonts/sfnt/tables.js +131 -0
  134. package/dist/fonts/sfnt/woff.d.ts +14 -0
  135. package/dist/fonts/sfnt/woff.js +163 -0
  136. package/dist/generated/text_shaper.js +324 -0
  137. package/dist/generated/text_shaper_bg.wasm +0 -0
  138. package/dist/headless-layout.d.ts +75 -0
  139. package/dist/headless-layout.js +350 -0
  140. package/dist/index.d.ts +9 -6
  141. package/dist/index.js +6 -3
  142. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  143. package/dist/layout-bridge/convert/footnoteLayout.js +1 -0
  144. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  145. package/dist/layout-bridge/convert/headerFooterLayout.js +2 -0
  146. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +3 -1
  147. package/dist/layout-bridge/convert/toFlowBlocks.js +2 -0
  148. package/dist/layout-engine/measure/advanceComposition.d.ts +62 -0
  149. package/dist/layout-engine/measure/advanceComposition.js +148 -0
  150. package/dist/layout-engine/measure/cache.d.ts +2 -0
  151. package/dist/layout-engine/measure/measureContainer.js +13 -0
  152. package/dist/layout-engine/measure/measureParagraph.js +1 -2
  153. package/dist/layout-engine/measure/measureTypes.d.ts +12 -0
  154. package/dist/layout-engine/measure/tableInlinePlacement.d.ts +3 -2
  155. package/dist/layout-engine/measure/tableInlinePlacement.js +14 -1
  156. package/dist/layout-engine/tableIndentCompatibility.d.ts +5 -0
  157. package/dist/layout-engine/tableIndentCompatibility.js +15 -0
  158. package/dist/layout-engine/types.d.ts +8 -0
  159. package/dist/layout-painter/renderPage.d.ts +29 -1
  160. package/dist/layout-painter/renderPage.js +47 -19
  161. package/dist/layout-painter/renderParagraph.d.ts +21 -2
  162. package/dist/layout-painter/renderParagraph.js +1 -1
  163. package/dist/layout-painter/renderTable.js +37 -4
  164. package/dist/managers/editorShortcuts.d.ts +33 -1
  165. package/dist/managers/editorShortcuts.js +20 -1
  166. package/dist/markdown/renderRuns.js +7 -2
  167. package/dist/markdown/renderTable.js +5 -1
  168. package/dist/model.d.ts +3 -3
  169. package/dist/model.js +2 -2
  170. package/dist/pdf/contentStream.d.ts +73 -0
  171. package/dist/pdf/contentStream.js +143 -0
  172. package/dist/pdf/fonts.d.ts +96 -0
  173. package/dist/pdf/fonts.js +593 -0
  174. package/dist/pdf/images.d.ts +28 -0
  175. package/dist/pdf/images.js +439 -0
  176. package/dist/pdf/objects.d.ts +93 -0
  177. package/dist/pdf/objects.js +237 -0
  178. package/dist/pdf/pageSpace.d.ts +43 -0
  179. package/dist/pdf/pageSpace.js +57 -0
  180. package/dist/pdf/paint.d.ts +32 -0
  181. package/dist/pdf/paint.js +442 -0
  182. package/dist/pdf/writePdf.d.ts +56 -0
  183. package/dist/pdf/writePdf.js +345 -0
  184. package/dist/prosemirror/commands/comments.js +91 -6
  185. package/dist/prosemirror/conversion/fromProseDoc.js +41 -7
  186. package/dist/prosemirror/conversion/toProseDoc.js +41 -4
  187. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +15 -2
  188. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +35 -6
  189. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +2 -2
  190. package/dist/prosemirror/extensions/features/pasteCleanup.js +4 -3
  191. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.d.ts +3 -1
  192. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +5 -3
  193. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.d.ts +7 -1
  194. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.js +9 -3
  195. package/dist/prosemirror/extensions/nodes/TableExtension.js +56 -39
  196. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +3 -1
  197. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +5 -3
  198. package/dist/prosemirror/plugins/suggestionMode.js +1 -0
  199. package/dist/prosemirror/revisionCarriers.js +19 -2
  200. package/dist/prosemirror/utils/extractTrackedChanges.d.ts +2 -2
  201. package/dist/prosemirror/utils/extractTrackedChanges.js +59 -23
  202. package/dist/prosemirror/validation.js +55 -27
  203. package/dist/prosemirror/zeroWidthAnchors.d.ts +10 -0
  204. package/dist/prosemirror/zeroWidthAnchors.js +23 -0
  205. package/dist/redline.js +12 -79
  206. package/dist/server.d.ts +3 -3
  207. package/dist/server.js +2 -2
  208. package/dist/shaping/placeRun.d.ts +51 -0
  209. package/dist/shaping/placeRun.js +84 -0
  210. package/dist/shaping/shaper.d.ts +92 -0
  211. package/dist/shaping/shaper.js +102 -0
  212. package/dist/types/block-id.d.ts +18 -2
  213. package/dist/types/block-id.js +49 -5
  214. package/dist/types/content.d.ts +2 -2
  215. package/dist/utils/clipboard.js +4 -3
  216. package/dist/utils/fontResolver.js +183 -0
  217. package/dist/utils/htmlComments.d.ts +17 -0
  218. package/dist/utils/htmlComments.js +22 -0
  219. package/dist/utils/scriptSegments.d.ts +12 -1
  220. package/dist/utils/scriptSegments.js +17 -1
  221. package/dist/version-comparison.d.ts +1 -1
  222. package/package.json +7 -3
@@ -0,0 +1,103 @@
1
+ import { rewriteStreamingXmlDecimalAttributes } from "./streamingXmlParser.js";
2
+ import { WORDPROCESSINGML_NAMESPACE_URIS, findAttributeByNamespaceUri, getLocalName, getNamespaceUri } from "./xmlParser.js";
3
+ import { XmlResourceLimitError, assertXmlResourceLimits } from "./xmlResourceLimits.js";
4
+ //#region src/docx/revisionIdNormalization.ts
5
+ const REVISION_ELEMENT_NAMES = /* @__PURE__ */ new Set([
6
+ "cellDel",
7
+ "cellIns",
8
+ "cellMerge",
9
+ "del",
10
+ "ins",
11
+ "moveFrom",
12
+ "moveTo",
13
+ "numberingChange",
14
+ "pPrChange",
15
+ "rPrChange",
16
+ "sectPrChange",
17
+ "tblGridChange",
18
+ "tblPrChange",
19
+ "tblPrExChange",
20
+ "tcPrChange",
21
+ "trPrChange"
22
+ ]);
23
+ const REVISION_ELEMENT_CANDIDATE = new RegExp(`<(?:[^\\s<>/:]+:)?(?:${[...REVISION_ELEMENT_NAMES].join("|")})(?:[\\s/>])`, "u");
24
+ const revisionAttribute = (element) => {
25
+ if (!element.name || !WORDPROCESSINGML_NAMESPACE_URIS.has(getNamespaceUri(element) ?? "") || !REVISION_ELEMENT_NAMES.has(getLocalName(element.name))) return null;
26
+ const attribute = findAttributeByNamespaceUri(element, WORDPROCESSINGML_NAMESPACE_URIS, "id");
27
+ if (!attribute) return null;
28
+ const id = Number(attribute.value);
29
+ return Number.isSafeInteger(id) && id >= 0 ? {
30
+ name: attribute.name,
31
+ id
32
+ } : null;
33
+ };
34
+ /**
35
+ * Keep physical tracked-change element ids unique across a package.
36
+ *
37
+ * Live editor marks and operation receipts keep their logical revision IDs.
38
+ * Saving assigns fresh IDs only where one logical change was split into
39
+ * multiple physical OOXML wrappers; reopening therefore exposes the physical
40
+ * wrapper IDs that the file format requires.
41
+ */
42
+ const normalizeRevisionIdsInXmlParts = (parts) => {
43
+ const candidates = [...parts].filter(([, xml]) => REVISION_ELEMENT_CANDIDATE.test(xml));
44
+ const occurrencesByPath = /* @__PURE__ */ new Map();
45
+ const reserved = /* @__PURE__ */ new Set();
46
+ for (const [path, xml] of candidates) {
47
+ assertXmlResourceLimits(xml);
48
+ const ids = [];
49
+ if (rewriteStreamingXmlDecimalAttributes(xml, (element) => {
50
+ const attribute = revisionAttribute(element);
51
+ if (attribute) {
52
+ ids.push(attribute.id);
53
+ reserved.add(attribute.id);
54
+ }
55
+ return null;
56
+ }).status === "unsupported") throw new XmlResourceLimitError({
57
+ message: `Revision-id normalization could not safely scan ${path}`,
58
+ limit: "syntax"
59
+ });
60
+ occurrencesByPath.set(path, ids);
61
+ }
62
+ const repeatedPaths = /* @__PURE__ */ new Set();
63
+ const firstSeen = /* @__PURE__ */ new Set();
64
+ for (const [path, ids] of occurrencesByPath) for (const id of ids) if (firstSeen.has(id)) repeatedPaths.add(path);
65
+ else firstSeen.add(id);
66
+ let nextId = 0;
67
+ const allocate = () => {
68
+ while (reserved.has(nextId)) nextId += 1;
69
+ const allocated = nextId;
70
+ reserved.add(allocated);
71
+ nextId += 1;
72
+ return allocated;
73
+ };
74
+ const seen = /* @__PURE__ */ new Set();
75
+ const normalized = new Map(parts);
76
+ for (const [path, xml] of candidates) {
77
+ const ids = occurrencesByPath.get(path);
78
+ if (!ids) continue;
79
+ if (!repeatedPaths.has(path) && ids.every((id) => !seen.has(id))) {
80
+ for (const id of ids) seen.add(id);
81
+ continue;
82
+ }
83
+ const rewritten = rewriteStreamingXmlDecimalAttributes(xml, (element) => {
84
+ const attribute = revisionAttribute(element);
85
+ if (!attribute) return null;
86
+ if (!seen.has(attribute.id)) {
87
+ seen.add(attribute.id);
88
+ return null;
89
+ }
90
+ const replacement = allocate();
91
+ seen.add(replacement);
92
+ return /* @__PURE__ */ new Map([[attribute.name, String(replacement)]]);
93
+ });
94
+ if (rewritten.status === "unsupported") throw new XmlResourceLimitError({
95
+ message: `Revision-id normalization could not safely rewrite ${path}`,
96
+ limit: "syntax"
97
+ });
98
+ normalized.set(path, rewritten.value);
99
+ }
100
+ return normalized;
101
+ };
102
+ //#endregion
103
+ export { REVISION_ELEMENT_NAMES, normalizeRevisionIdsInXmlParts };
@@ -5,7 +5,9 @@ import { parseEndnotes, parseFootnotes } from "./footnoteParser.js";
5
5
  import { assertValidFolioDocumentModel } from "./modelValidation.js";
6
6
  import { isNewDataUrlDrawing } from "./newImage.js";
7
7
  import { parseNumbering } from "./numberingParser.js";
8
+ import { isUnsafePackagePath, reconcilePackageReferences, removeUnsafeEntries } from "./packageParts.js";
8
9
  import { RELATIONSHIP_TYPES, parseRelationships, resolveRelativePath } from "./relsParser.js";
10
+ import { normalizeRevisionIdsInXmlParts } from "./revisionIdNormalization.js";
9
11
  import { appendNumberingDefs, buildPatchedNotePartXml, buildPatchedNumberingXml, collectAddedNumberingDefs, collectChangedNoteParaIds, collectChangedNumberingDefs, collectParaIds } from "./selectiveXmlPatch.js";
10
12
  import { ensureThreadedCommentParaIds, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
11
13
  import { serializeDocument } from "./serializer/documentSerializer.js";
@@ -17,7 +19,6 @@ import { serializeSettingsXml } from "./serializer/settingsSerializer.js";
17
19
  import { serializeStyle, serializeStylesXml } from "./serializer/stylesSerializer.js";
18
20
  import { serializeThemeXml } from "./serializer/themeSerializer.js";
19
21
  import { escapeXml } from "./serializer/xmlUtils.js";
20
- import { isPreservableDocxEntry } from "./unzip.js";
21
22
  import { WORDPROCESSINGML_NAMESPACE_URIS, findChild, getAttribute, getChildElements, getLocalName, getNamespaceUri, matchesName, parseXml, parseXmlDocument } from "./xmlParser.js";
22
23
  import { assertXmlResourceLimits } from "./xmlResourceLimits.js";
23
24
  import { panic } from "better-result";
@@ -405,20 +406,32 @@ async function processNewHyperlinks(parts, zip, compressionLevel) {
405
406
  });
406
407
  }
407
408
  }
408
- const generateDocxZip = (zip, compressionLevel) => zip.generateAsync({
409
- type: "arraybuffer",
410
- compression: "DEFLATE",
411
- compressionOptions: { level: compressionLevel }
412
- });
413
- const parsedZipSources = /* @__PURE__ */ new WeakMap();
414
- const removeNonPreservableEntries = (zip) => {
415
- for (const [path, file] of Object.entries(zip.files)) if (!file.dir && !isPreservableDocxEntry(path)) zip.remove(path);
409
+ /**
410
+ * The single exit for a repacked package. Reconciliation runs here rather than
411
+ * at each caller so no save path can emit a package whose relationships or
412
+ * content types name a part it does not hold.
413
+ */
414
+ const generateDocxZip = async (zip, compressionLevel) => {
415
+ await reconcilePackageReferences(zip, compressionLevel);
416
+ const xmlParts = /* @__PURE__ */ new Map();
417
+ for (const [path, file] of Object.entries(zip.files)) if (!file.dir && path.startsWith("word/") && path.endsWith(".xml")) xmlParts.set(path, await file.async("text"));
418
+ const normalizedParts = normalizeRevisionIdsInXmlParts(xmlParts);
419
+ for (const [path, xml] of normalizedParts) if (xml !== xmlParts.get(path)) zip.file(path, xml, {
420
+ compression: "DEFLATE",
421
+ compressionOptions: { level: compressionLevel }
422
+ });
423
+ return zip.generateAsync({
424
+ type: "arraybuffer",
425
+ compression: "DEFLATE",
426
+ compressionOptions: { level: compressionLevel }
427
+ });
416
428
  };
429
+ const parsedZipSources = /* @__PURE__ */ new WeakMap();
417
430
  const loadParsedZipSource = async (document, buffer) => {
418
431
  const cached = parsedZipSources.get(document);
419
432
  if (cached?.buffer === buffer) return cached;
420
433
  const zip = await JSZip.loadAsync(buffer);
421
- removeNonPreservableEntries(zip);
434
+ removeUnsafeEntries(zip);
422
435
  const [documentXml, corePropertiesXml] = await Promise.all([zip.file("word/document.xml")?.async("text"), zip.file("docProps/core.xml")?.async("text")]);
423
436
  const source = {
424
437
  buffer,
@@ -486,7 +499,7 @@ async function repackDocx(doc, options = {}) {
486
499
  const exportDocument = withoutOrphanCommentRanges(doc);
487
500
  const originalZip = await JSZip.loadAsync(doc.originalBuffer);
488
501
  const [originalDocumentXml, originalCorePropertiesXml] = await Promise.all([originalZip.file("word/document.xml")?.async("text"), originalZip.file("docProps/core.xml")?.async("text")]);
489
- removeNonPreservableEntries(originalZip);
502
+ removeUnsafeEntries(originalZip);
490
503
  const newZip = cloneDocxZip(originalZip);
491
504
  return finishRepack({
492
505
  document: exportDocument,
@@ -517,7 +530,7 @@ async function repackDocxFromRaw(doc, rawContent, options = {}) {
517
530
  newZip.folder(path.replace(/\/$/u, ""));
518
531
  continue;
519
532
  }
520
- if (!isPreservableDocxEntry(path)) continue;
533
+ if (isUnsafePackagePath(path)) continue;
521
534
  const content = await file.async("arraybuffer");
522
535
  newZip.file(path, content, {
523
536
  compression: "DEFLATE",
@@ -559,11 +572,7 @@ async function repackDocxFromRaw(doc, rawContent, options = {}) {
559
572
  compressionOptions: { level: compressionLevel }
560
573
  });
561
574
  }
562
- return await newZip.generateAsync({
563
- type: "arraybuffer",
564
- compression: "DEFLATE",
565
- compressionOptions: { level: compressionLevel }
566
- });
575
+ return generateDocxZip(newZip, compressionLevel);
567
576
  }
568
577
  const COMMENTS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml";
569
578
  const COMMENTS_EXTENDED_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml";
@@ -4,6 +4,7 @@ import { hasUnsynthesizedReplyRanges } from "./commentReplyMarkers.js";
4
4
  import { validateFolioDocumentModel } from "./modelValidation.js";
5
5
  import { isNewDataUrlDrawing } from "./newImage.js";
6
6
  import { parseNumbering } from "./numberingParser.js";
7
+ import { isUnsafePackagePath } from "./packageParts.js";
7
8
  import { RELATIONSHIP_TYPES } from "./relsParser.js";
8
9
  import { COMMENTS_CONTENT_TYPE, COMMENTS_EXTENDED_PART_LOWER, addCommentsExtendedOverride, addCommentsExtendedRelationship, applyUpdatesToZip, collectHeaderFooterUpdates, findMaxRId, hasModelDrivenPictureWatermark, hasUnmaterializedHeaderFooter, updateCoreProperties, withoutAttachedTemplate } from "./rezip.js";
9
10
  import "./selectiveSaveFlags.js";
@@ -12,7 +13,6 @@ import { ensureThreadedCommentParaIds, serializeComments, serializeCommentsExten
12
13
  import { serializeDocument } from "./serializer/documentSerializer.js";
13
14
  import { serializeEndnotes, serializeFootnotes } from "./serializer/noteSerializer.js";
14
15
  import { serializeNumberingXml } from "./serializer/numberingSerializer.js";
15
- import { isPreservableDocxEntry } from "./unzip.js";
16
16
  //#region src/docx/selectiveSave.ts
17
17
  /**
18
18
  * Check if document content has new images (data: URL without rId) or
@@ -212,7 +212,7 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
212
212
  const headerFooterUpdates = collectHeaderFooterUpdates(doc);
213
213
  try {
214
214
  const zip = await (await import("jszip")).default.loadAsync(originalBuffer);
215
- for (const [path, file] of Object.entries(zip.files)) if (!file.dir && !isPreservableDocxEntry(path)) return null;
215
+ for (const [path, file] of Object.entries(zip.files)) if (!file.dir && isUnsafePackagePath(path)) return null;
216
216
  const updates = /* @__PURE__ */ new Map();
217
217
  if (changedParaIds.size > 0) {
218
218
  const docXmlFile = zip.file("word/document.xml");
@@ -36,7 +36,12 @@ function serializeComment(comment) {
36
36
  xml += "</w:comment>";
37
37
  return xml;
38
38
  }
39
- const COMMENTS_HEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<w:comments xmlns:wpc=\"http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas\" xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\" xmlns:o=\"urn:schemas-microsoft-com:office:office\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\" xmlns:m=\"http://schemas.openxmlformats.org/officeDocument/2006/math\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:wp=\"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing\" xmlns:w10=\"urn:schemas-microsoft-com:office:word\" xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\" xmlns:w14=\"http://schemas.microsoft.com/office/word/2010/wordml\" xmlns:wpg=\"http://schemas.microsoft.com/office/word/2010/wordprocessingGroup\" xmlns:wpi=\"http://schemas.microsoft.com/office/word/2010/wordprocessingInk\" xmlns:wne=\"http://schemas.microsoft.com/office/word/2006/wordml\" xmlns:wps=\"http://schemas.microsoft.com/office/word/2010/wordprocessingShape\" mc:Ignorable=\"w14 wp14\">";
39
+ const COMMENT_EXTENSION_NAMESPACES = {
40
+ w14: "http://schemas.microsoft.com/office/word/2010/wordml",
41
+ wp14: "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing"
42
+ };
43
+ const COMMENTS_HEADER = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
44
+ <w:comments xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" ${Object.entries(COMMENT_EXTENSION_NAMESPACES).map(([prefix, namespace]) => `xmlns:${prefix}="${namespace}"`).join(" ")} xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="${Object.keys(COMMENT_EXTENSION_NAMESPACES).join(" ")}">`;
40
45
  /**
41
46
  * Serialize comments array to comments.xml content. Returns a valid empty
42
47
  * `<w:comments/>` document for an empty array so callers can overwrite an
@@ -368,6 +368,14 @@ function serializeMoveRangeStart(tag, marker) {
368
368
  function rewriteRunTextAsDeleted(xml) {
369
369
  return xml.replace(/<w:t\b/gu, "<w:delText").replace(/<\/w:t>/gu, "</w:delText>").replace(/<w:instrText\b/gu, "<w:delInstrText").replace(/<\/w:instrText>/gu, "</w:delInstrText>");
370
370
  }
371
+ function trackedChangeTag(change) {
372
+ switch (change.type) {
373
+ case "insertion": return "ins";
374
+ case "deletion": return "del";
375
+ case "moveFrom": return "moveFrom";
376
+ case "moveTo": return "moveTo";
377
+ }
378
+ }
371
379
  function serializeTrackedChange(tag, change) {
372
380
  const info = change.info;
373
381
  const normalizedId = normalizeRevisionId(info.id);
@@ -394,7 +402,13 @@ function serializeTrackedChange(tag, change) {
394
402
  if (tag === "del" || tag === "moveFrom") return serializeDeletedRun(item);
395
403
  return serializeRun(item);
396
404
  }
397
- return serializeHyperlink(item);
405
+ if (item.type === "hyperlink") return serializeHyperlink(item);
406
+ if (item.type === "simpleField" || item.type === "complexField") {
407
+ const xml = item.type === "simpleField" ? serializeSimpleField(item) : serializeComplexField(item);
408
+ return tag === "del" || tag === "moveFrom" ? rewriteRunTextAsDeleted(xml) : xml;
409
+ }
410
+ if (item.type === "insertion" || item.type === "deletion" || item.type === "moveFrom" || item.type === "moveTo") return serializeTrackedChange(trackedChangeTag(item), item);
411
+ return item.type === "bookmarkStart" ? serializeBookmarkStart(item) : serializeBookmarkEnd(item);
398
412
  }).join("");
399
413
  return `<w:${tag} ${attrs.join(" ")}>${contentXml}</w:${tag}>`;
400
414
  }
@@ -470,12 +484,27 @@ function serializeParagraph(paragraph) {
470
484
  return `<w:p${attrsStr}>${parts.join("")}</w:p>`;
471
485
  }
472
486
  function injectRenderedPageBreakIntoFirstRun(xml) {
473
- const runOpeningTag = /<w:r(?=[\s>/])[^>]*>/u;
474
- const openingTag = runOpeningTag.exec(xml);
487
+ const openingTag = /<w:r(?=[\s>/])[^>]*>/u.exec(xml);
475
488
  if (!openingTag) return null;
476
489
  const runEnd = xml.indexOf("</w:r>", openingTag.index + openingTag[0].length);
477
490
  if (runEnd !== -1 && xml.slice(openingTag.index + openingTag[0].length, runEnd).includes("<w:lastRenderedPageBreak/>")) return xml;
478
- return xml.replace(runOpeningTag, (match) => `${match}<w:lastRenderedPageBreak/>`);
491
+ const contentStart = openingTag.index + openingTag[0].length;
492
+ let insertionOffset = contentStart;
493
+ if (xml.startsWith("<w:rPr", contentStart)) {
494
+ const propertyTag = /<\/?w:rPr(?=[\s>/])[^>]*>/gu;
495
+ propertyTag.lastIndex = contentStart;
496
+ let depth = 0;
497
+ for (const match of xml.matchAll(propertyTag)) {
498
+ if (match.index !== contentStart && depth === 0) break;
499
+ if (match[0].startsWith("</")) depth--;
500
+ else if (!match[0].endsWith("/>")) depth++;
501
+ if (depth === 0) {
502
+ insertionOffset = match.index + match[0].length;
503
+ break;
504
+ }
505
+ }
506
+ }
507
+ return `${xml.slice(0, insertionOffset)}<w:lastRenderedPageBreak/>${xml.slice(insertionOffset)}`;
479
508
  }
480
509
  /**
481
510
  * Serialize multiple paragraphs to OOXML XML
@@ -92,13 +92,6 @@ function serializeTextFormatting(formatting) {
92
92
  if (formatting.fontFamily.csTheme) fontAttrs.push(`w:cstheme="${escapeXml(formatting.fontFamily.csTheme)}"`);
93
93
  if (fontAttrs.length > 0) parts.push(`<w:rFonts ${fontAttrs.join(" ")}/>`);
94
94
  }
95
- if (formatting.language) {
96
- const languageAttrs = [];
97
- if (formatting.language.val) languageAttrs.push(`w:val="${escapeXml(formatting.language.val)}"`);
98
- if (formatting.language.eastAsia) languageAttrs.push(`w:eastAsia="${escapeXml(formatting.language.eastAsia)}"`);
99
- if (formatting.language.bidi) languageAttrs.push(`w:bidi="${escapeXml(formatting.language.bidi)}"`);
100
- if (languageAttrs.length > 0) parts.push(`<w:lang ${languageAttrs.join(" ")}/>`);
101
- }
102
95
  if (formatting.bold === true) parts.push("<w:b/>");
103
96
  else if (formatting.bold === false) parts.push("<w:b w:val=\"0\"/>");
104
97
  if (formatting.boldCs === true) parts.push("<w:bCs/>");
@@ -134,11 +127,12 @@ function serializeTextFormatting(formatting) {
134
127
  if (formatting.position !== void 0) parts.push(`<w:position w:val="${intAttr(formatting.position)}"/>`);
135
128
  if (formatting.fontSize !== void 0) parts.push(`<w:sz w:val="${intAttr(formatting.fontSize)}"/>`);
136
129
  if (formatting.fontSizeCs !== void 0) parts.push(`<w:szCs w:val="${intAttr(formatting.fontSizeCs)}"/>`);
130
+ let customHighlightShadingXml = "";
137
131
  if (formatting.highlight) {
138
132
  if (VALID_HIGHLIGHT_COLORS.has(formatting.highlight)) parts.push(`<w:highlight w:val="${formatting.highlight}"/>`);
139
133
  else if (!formatting.shading) {
140
134
  const hex = formatting.highlight.replace(/^#/u, "");
141
- if (/^[0-9a-fA-F]{6}$/u.test(hex)) parts.push(`<w:shd w:val="clear" w:color="auto" w:fill="${hex}"/>`);
135
+ if (/^[0-9a-fA-F]{6}$/u.test(hex)) customHighlightShadingXml = `<w:shd w:val="clear" w:color="auto" w:fill="${hex}"/>`;
142
136
  }
143
137
  }
144
138
  if (formatting.underline) {
@@ -152,14 +146,21 @@ function serializeTextFormatting(formatting) {
152
146
  parts.push(`<w:u ${uAttrs.join(" ")}/>`);
153
147
  }
154
148
  if (formatting.effect && formatting.effect !== "none") parts.push(`<w:effect w:val="${formatting.effect}"/>`);
155
- if (formatting.emphasisMark) parts.push(`<w:em w:val="${formatting.emphasisMark}"/>`);
156
- const shadingXml = serializeShading(formatting.shading);
149
+ const shadingXml = serializeShading(formatting.shading) || customHighlightShadingXml;
157
150
  if (shadingXml) parts.push(shadingXml);
158
151
  if (formatting.vertAlign) parts.push(`<w:vertAlign w:val="${formatting.vertAlign}"/>`);
159
152
  if (formatting.rtl === true) parts.push("<w:rtl/>");
160
153
  else if (formatting.rtl === false) parts.push("<w:rtl w:val=\"0\"/>");
161
154
  if (formatting.cs === true) parts.push("<w:cs/>");
162
155
  else if (formatting.cs === false) parts.push("<w:cs w:val=\"0\"/>");
156
+ if (formatting.emphasisMark) parts.push(`<w:em w:val="${formatting.emphasisMark}"/>`);
157
+ if (formatting.language) {
158
+ const languageAttrs = [];
159
+ if (formatting.language.val) languageAttrs.push(`w:val="${escapeXml(formatting.language.val)}"`);
160
+ if (formatting.language.eastAsia) languageAttrs.push(`w:eastAsia="${escapeXml(formatting.language.eastAsia)}"`);
161
+ if (formatting.language.bidi) languageAttrs.push(`w:bidi="${escapeXml(formatting.language.bidi)}"`);
162
+ if (languageAttrs.length > 0) parts.push(`<w:lang ${languageAttrs.join(" ")}/>`);
163
+ }
163
164
  if (parts.length === 0) return "";
164
165
  return `<w:rPr>${parts.join("")}</w:rPr>`;
165
166
  }
@@ -250,7 +250,8 @@ function serializeTableCellFormatting(formatting, propertyChanges, structuralCha
250
250
  if (formatting.textDirection) parts.push(`<w:textDirection w:val="${formatting.textDirection}"/>`);
251
251
  if (formatting.fitText) parts.push("<w:tcFitText/>");
252
252
  if (formatting.verticalAlign) parts.push(`<w:vAlign w:val="${formatting.verticalAlign}"/>`);
253
- if (formatting.hideMark !== void 0) parts.push(formatting.hideMark ? "<w:hideMark/>" : "<w:hideMark w:val=\"false\"/>");
253
+ if (formatting.hideMark === true) parts.push("<w:hideMark/>");
254
+ else if (formatting.hideMark === false) parts.push("<w:hideMark w:val=\"off\"/>");
254
255
  }
255
256
  if (structuralChange) {
256
257
  if (structuralChange.type === "tableCellInsertion") parts.push(`<w:cellIns ${serializeTrackedChangeAttributes(structuralChange.info)}/>`);
@@ -2,8 +2,8 @@ import { loadDocxArchive } from "./boundedArchive.js";
2
2
  import { FOLIO_DOCX_XML_PATCH_PROPOSAL_PROFILE, InvalidFolioDocxXmlPatchProposalError, evaluateDocxXmlPatchProposal, parseFolioDocxXmlPatchProposal } from "./evaluateDocxXmlPatchProposal.js";
3
3
  import { FOLIO_DOCX_CONFORMANCE_PROFILE, validateDocxConformance } from "./validateDocxConformance.js";
4
4
  import { TaggedError, panic } from "better-result";
5
- import JSZip from "jszip";
6
5
  import { createHash } from "node:crypto";
6
+ import JSZip from "jszip";
7
7
  //#region src/docx/server/applyDocxXmlPatchProposal.ts
8
8
  const FOLIO_DOCX_XML_PATCH_APPLICATION_VERSION = 1;
9
9
  const FOLIO_DOCX_XML_PATCH_APPLICATION_PROFILE = "folio-xml-patch-application-v1";
@@ -1,4 +1,5 @@
1
1
  import { FolioDocxReviewer } from "../../ai-edits/headless.js";
2
+ import { isFolioAIContentBlock } from "../../ai-edits/snapshot.js";
2
3
  import { toArrayBuffer } from "../../utils/docxInput.js";
3
4
  import { ensureParaIds } from "../ensureParaIds.js";
4
5
  import { parseDocx } from "../parser.js";
@@ -14,7 +15,7 @@ import { createBilingualDocument, readBilingualDocument } from "./createBilingua
14
15
  */
15
16
  async function createBilingualDocx(input, options) {
16
17
  const stampedBuffer = await toArrayBuffer((await ensureParaIds(input)).docx);
17
- const editableParagraphIds = new Set((await FolioDocxReviewer.fromBuffer(stampedBuffer)).snapshot().blocks.map(({ id }) => id));
18
+ const editableParagraphIds = new Set((await FolioDocxReviewer.fromBuffer(stampedBuffer)).snapshot().blocks.filter(isFolioAIContentBlock).map(({ id }) => id));
18
19
  const { document, warnings } = createBilingualDocument(await parseDocx(stampedBuffer, { preloadFonts: false }), {
19
20
  ...options,
20
21
  editableParagraphIds
@@ -29,7 +30,7 @@ async function createBilingualDocx(input, options) {
29
30
  /** Bytes-in form of {@link readBilingualDocument}. */
30
31
  async function readBilingualDocx(input) {
31
32
  const buffer = await toArrayBuffer(input);
32
- return readBilingualDocument(await parseDocx(buffer, { preloadFonts: false }), new Set((await FolioDocxReviewer.fromBuffer(buffer)).snapshot().blocks.map(({ id }) => id)));
33
+ return readBilingualDocument(await parseDocx(buffer, { preloadFonts: false }), new Set((await FolioDocxReviewer.fromBuffer(buffer)).snapshot().blocks.filter(isFolioAIContentBlock).map(({ id }) => id)));
33
34
  }
34
35
  //#endregion
35
36
  export { createBilingualDocx, readBilingualDocx };
@@ -11,6 +11,18 @@ type ParseXmlResult = {
11
11
  * pass. Unsupported or malformed constructs return a sentinel so callers can
12
12
  * retain the general-purpose parser as a compatibility fallback.
13
13
  */
14
+ type AttributeValueSpan = {
15
+ start: number;
16
+ end: number;
17
+ };
18
+ type OpenTagVisitor = (element: XmlElement, attributeValueSpans: ReadonlyMap<string, AttributeValueSpan>) => ReadonlyMap<string, string> | null;
14
19
  declare const parseStreamingXml: (xml: string) => ParseXmlResult;
20
+ /** Rewrite selected decimal attribute values while preserving every other source byte. */
21
+ declare const rewriteStreamingXmlDecimalAttributes: (xml: string, visitOpenTag: OpenTagVisitor) => {
22
+ status: "rewritten";
23
+ value: string;
24
+ } | {
25
+ status: "unsupported";
26
+ };
15
27
  //#endregion
16
- export { parseStreamingXml };
28
+ export { parseStreamingXml, rewriteStreamingXmlDecimalAttributes };
@@ -1,3 +1,4 @@
1
+ import { attachXmlNamespaceContext } from "./xmlParser.js";
1
2
  import { FOLIO_XML_RESOURCE_LIMITS } from "./xmlResourceLimits.js";
2
3
  //#region src/docx/streamingXmlParser.ts
3
4
  const BUILT_IN_ENTITIES = {
@@ -7,14 +8,10 @@ const BUILT_IN_ENTITIES = {
7
8
  lt: "<",
8
9
  quot: "\""
9
10
  };
10
- /**
11
- * Parse ordinary OOXML into Folio's existing element representation in one
12
- * pass. Unsupported or malformed constructs return a sentinel so callers can
13
- * retain the general-purpose parser as a compatibility fallback.
14
- */
15
- const parseStreamingXml = (xml) => {
11
+ const parseStreamingXmlInternal = (xml, visitOpenTag) => {
16
12
  const root = { elements: [] };
17
13
  const stack = [];
14
+ const replacements = [];
18
15
  let cursor = 0;
19
16
  let mergeAdjacentText = false;
20
17
  while (cursor < xml.length) {
@@ -55,9 +52,19 @@ const parseStreamingXml = (xml) => {
55
52
  mergeAdjacentText = false;
56
53
  continue;
57
54
  }
58
- const parsedTag = parseOpenTag(xml, open + 1, close);
55
+ const parsedTag = parseOpenTag(xml, open + 1, close, visitOpenTag !== void 0);
59
56
  if (parsedTag.status === "unsupported") return parsedTag;
60
57
  const parent = stack.at(-1)?.element ?? root;
58
+ attachXmlNamespaceContext(parsedTag.element, parent.namespaceScope);
59
+ const rewritten = visitOpenTag?.(parsedTag.element, parsedTag.attributeValueSpans ?? /* @__PURE__ */ new Map());
60
+ if (rewritten) for (const [attributeName, value] of rewritten) {
61
+ const span = parsedTag.attributeValueSpans?.get(attributeName);
62
+ if (!span) return { status: "unsupported" };
63
+ replacements.push({
64
+ ...span,
65
+ value
66
+ });
67
+ }
61
68
  appendElement(parent, parsedTag.element);
62
69
  if (!parsedTag.selfClosing) {
63
70
  if (stack.length >= FOLIO_XML_RESOURCE_LIMITS.maxDepth) return { status: "unsupported" };
@@ -72,10 +79,35 @@ const parseStreamingXml = (xml) => {
72
79
  if (stack.length > 0) return { status: "unsupported" };
73
80
  return {
74
81
  status: "parsed",
75
- value: root
82
+ value: root,
83
+ replacements
84
+ };
85
+ };
86
+ const parseStreamingXml = (xml) => {
87
+ const parsed = parseStreamingXmlInternal(xml);
88
+ return parsed.status === "parsed" ? {
89
+ status: "parsed",
90
+ value: parsed.value
91
+ } : { status: "unsupported" };
92
+ };
93
+ /** Rewrite selected decimal attribute values while preserving every other source byte. */
94
+ const rewriteStreamingXmlDecimalAttributes = (xml, visitOpenTag) => {
95
+ const parsed = parseStreamingXmlInternal(xml, visitOpenTag);
96
+ if (parsed.status === "unsupported") return parsed;
97
+ const chunks = [];
98
+ let cursor = 0;
99
+ for (const replacement of parsed.replacements.toSorted((left, right) => left.start - right.start)) {
100
+ if (!/^\d+$/u.test(replacement.value)) return { status: "unsupported" };
101
+ chunks.push(xml.slice(cursor, replacement.start), replacement.value);
102
+ cursor = replacement.end;
103
+ }
104
+ chunks.push(xml.slice(cursor));
105
+ return {
106
+ status: "rewritten",
107
+ value: chunks.join("")
76
108
  };
77
109
  };
78
- const parseOpenTag = (xml, start, close) => {
110
+ const parseOpenTag = (xml, start, close, captureAttributeSpans) => {
79
111
  let cursor = skipWhitespace(xml, start, close);
80
112
  const nameStart = cursor;
81
113
  cursor = scanName(xml, cursor, close);
@@ -83,6 +115,7 @@ const parseOpenTag = (xml, start, close) => {
83
115
  const name = xml.slice(nameStart, cursor);
84
116
  if (isUnsafePropertyName(name)) return { status: "unsupported" };
85
117
  let attributes;
118
+ const attributeValueSpans = captureAttributeSpans ? /* @__PURE__ */ new Map() : void 0;
86
119
  let selfClosing = false;
87
120
  while (cursor < close) {
88
121
  cursor = skipWhitespace(xml, cursor, close);
@@ -110,6 +143,10 @@ const parseOpenTag = (xml, start, close) => {
110
143
  if (decoded === null) return { status: "unsupported" };
111
144
  attributes ??= {};
112
145
  attributes[attributeName] = decoded;
146
+ attributeValueSpans?.set(attributeName, {
147
+ start: valueStart,
148
+ end: cursor
149
+ });
113
150
  cursor += 1;
114
151
  }
115
152
  const element = {
@@ -121,7 +158,8 @@ const parseOpenTag = (xml, start, close) => {
121
158
  status: "parsed",
122
159
  element,
123
160
  name,
124
- selfClosing
161
+ selfClosing,
162
+ attributeValueSpans
125
163
  };
126
164
  };
127
165
  const appendElement = (parent, child) => {
@@ -246,4 +284,4 @@ const isUnsafePropertyName = (name) => {
246
284
  }
247
285
  };
248
286
  //#endregion
249
- export { parseStreamingXml };
287
+ export { parseStreamingXml, rewriteStreamingXmlDecimalAttributes };
@@ -59,7 +59,6 @@ type RawDocxContent = {
59
59
  * @returns Promise resolving to extracted content
60
60
  */
61
61
  declare function unzipDocx(buffer: ArrayBuffer, options?: DocxUnzipOptions): Promise<RawDocxContent>;
62
- declare function isPreservableDocxEntry(path: string): boolean;
63
62
  /**
64
63
  * Get a list of all files in the DOCX
65
64
  *
@@ -120,4 +119,4 @@ declare function getContentSummary(content: RawDocxContent): {
120
119
  totalFiles: number;
121
120
  };
122
121
  //#endregion
123
- export { DocxSecurityError, DocxUnzipLimits, DocxUnzipOptions, RawDocxContent, extractFile, getContentSummary, getFileList, getMediaMimeType, hasFile, isPreservableDocxEntry, mediaToDataUrl, unzipDocx };
122
+ export { DocxSecurityError, DocxUnzipLimits, DocxUnzipOptions, RawDocxContent, extractFile, getContentSummary, getFileList, getMediaMimeType, hasFile, mediaToDataUrl, unzipDocx };
@@ -137,13 +137,13 @@ async function unzipDocx(buffer, options = {}) {
137
137
  const extractionTasks = [];
138
138
  for (const [path, file] of entries) {
139
139
  if (!isSafeDocxPath(path)) throw new DocxSecurityError("DOCX file contains an unsafe entry path");
140
- if (!isPreservableDocxEntry(path)) continue;
141
- const lowerPath = path.toLowerCase();
142
140
  const declaredSize = getEntryUncompressedSize(file);
143
141
  if (declaredSize !== null) {
144
142
  totalUncompressedBytes += declaredSize;
145
143
  if (totalUncompressedBytes > limits.maxTotalUncompressedBytes) throw new DocxSecurityError("DOCX file expands beyond the maximum allowed size");
146
144
  }
145
+ if (!isParsedDocxEntry(path)) continue;
146
+ const lowerPath = path.toLowerCase();
147
147
  if (lowerPath.endsWith(".xml") || lowerPath.endsWith(".rels")) {
148
148
  assertEntrySize(path, declaredSize, limits.maxXmlBytes);
149
149
  if (options.extractAllXml === false && !shouldExtractXmlPart(lowerPath)) continue;
@@ -328,9 +328,28 @@ function isSafeDocxPath(path) {
328
328
  if (!path || path.startsWith("/") || path.includes("\\")) return false;
329
329
  return !path.split("/").some((part) => part === "..");
330
330
  }
331
- function isPreservableDocxEntry(path) {
331
+ /**
332
+ * Parts folio never looks inside. A macro project and an ActiveX control carry
333
+ * code; folio models neither, so it reads neither — not into the document
334
+ * model, not even into the raw XML map. The save path carries them anyway,
335
+ * straight from the source archive, which is what keeps a `.docm` a `.docm`.
336
+ */
337
+ const UNINTERPRETED_PART_NAMES = /* @__PURE__ */ new Set(["word/vbaproject.bin", "word/vbadata.xml"]);
338
+ const isUninterpretedPart = (lowerPath) => UNINTERPRETED_PART_NAMES.has(lowerPath) || lowerPath.startsWith("word/activex/");
339
+ /**
340
+ * Whether this entry is one the PARSER reads into the document model. It says
341
+ * nothing about what a save keeps: a repack hands back every part of the
342
+ * package (see `packageParts.ts`), including the ones this returns false for.
343
+ *
344
+ * That asymmetry is the point. A macro project, an ActiveX control and an OLE
345
+ * embedding are opaque bytes to folio — never decoded, never interpreted,
346
+ * never executed — and they travel from the source archive to the saved one
347
+ * without any code here looking inside them.
348
+ */
349
+ function isParsedDocxEntry(path) {
332
350
  if (!isSafeDocxPath(path)) return false;
333
351
  const lowerPath = path.toLowerCase();
352
+ if (isUninterpretedPart(lowerPath)) return false;
334
353
  if (lowerPath.startsWith("word/media/")) return PRESERVABLE_MEDIA_MIME_TYPES.has(getMediaMimeType(path));
335
354
  if (lowerPath.startsWith("docprops/thumbnail.")) return PRESERVABLE_MEDIA_MIME_TYPES.has(getMediaMimeType(path));
336
355
  if (lowerPath.startsWith("word/fonts/")) return true;
@@ -374,7 +393,7 @@ function isMediaContentAllowed(data, mimeType) {
374
393
  */
375
394
  function getFileList(content) {
376
395
  const files = [];
377
- for (const path of Object.keys(content.originalZip.files)) if (!content.originalZip.files[path]?.dir && isPreservableDocxEntry(path)) files.push(path);
396
+ for (const path of Object.keys(content.originalZip.files)) if (!content.originalZip.files[path]?.dir && isParsedDocxEntry(path)) files.push(path);
378
397
  return files.toSorted();
379
398
  }
380
399
  /**
@@ -425,7 +444,7 @@ function mediaToDataUrl(data, mimeType) {
425
444
  */
426
445
  function extractFile(content, path) {
427
446
  const file = content.originalZip.file(path);
428
- if (!file || !isPreservableDocxEntry(path)) return Promise.resolve(null);
447
+ if (!file || !isParsedDocxEntry(path)) return Promise.resolve(null);
429
448
  const lowerPath = path.toLowerCase();
430
449
  if (lowerPath.endsWith(".xml") || lowerPath.endsWith(".rels")) return file.async("text");
431
450
  return file.async("arraybuffer");
@@ -464,4 +483,4 @@ function getContentSummary(content) {
464
483
  };
465
484
  }
466
485
  //#endregion
467
- export { DocxSecurityError, extractFile, getContentSummary, getFileList, getMediaMimeType, hasFile, isPreservableDocxEntry, mediaToDataUrl, unzipDocx };
486
+ export { DocxSecurityError, extractFile, getContentSummary, getFileList, getMediaMimeType, hasFile, mediaToDataUrl, unzipDocx };
@@ -47,6 +47,8 @@ type XmlNamespaceScope = {
47
47
  bindings: ReadonlyMap<string, string>;
48
48
  parent?: XmlNamespaceScope;
49
49
  };
50
+ /** Attach the element's resolved namespace metadata from its in-scope declarations. */
51
+ declare const attachXmlNamespaceContext: (element: XmlElement, inheritedNamespaceScope?: XmlNamespaceScope) => XmlNamespaceScope;
50
52
  /**
51
53
  * Common OOXML namespace URIs — re-exported from @stll/docx-utils.
52
54
  */
@@ -344,4 +346,4 @@ declare function mergeXmlnsDeclarations(inherited: Record<string, string>, eleme
344
346
  */
345
347
  declare function cloneWithXmlnsDeclarations(element: XmlElement, xmlnsDecls: Record<string, string>): XmlElement;
346
348
  //#endregion
347
- export { NAMESPACES, WORDPROCESSINGML_NAMESPACE_URIS, XmlAttributeMatch, XmlElement, XmlNamespaceScope, cloneWithXmlnsDeclarations, collectXmlnsDeclarations, elementToXml, findAllDeep, findAttributeByNamespaceUri, findByFullName, findChild, findChildByLocalName, findChildByNamespaceUri, findChildren, findChildrenByLocalName, findDeep, getAttribute, getAttributeAny, getAttributeAnyPrefix, getAttributeByNamespaceUri, getAttributes, getChildElements, getLocalName, getNamespacePrefix, getNamespaceUri, getTextContent, hasChild, hasFlag, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseColorElement, parseNumberingLevelAttribute, parseNumericAttribute, parseOnOffValue, parseTableMeasurementValue, parseXml, parseXmlDocument };
349
+ export { NAMESPACES, WORDPROCESSINGML_NAMESPACE_URIS, XmlAttributeMatch, XmlElement, XmlNamespaceScope, attachXmlNamespaceContext, cloneWithXmlnsDeclarations, collectXmlnsDeclarations, elementToXml, findAllDeep, findAttributeByNamespaceUri, findByFullName, findChild, findChildByLocalName, findChildByNamespaceUri, findChildren, findChildrenByLocalName, findDeep, getAttribute, getAttributeAny, getAttributeAnyPrefix, getAttributeByNamespaceUri, getAttributes, getChildElements, getLocalName, getNamespacePrefix, getNamespaceUri, getTextContent, hasChild, hasFlag, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseColorElement, parseNumberingLevelAttribute, parseNumericAttribute, parseOnOffValue, parseTableMeasurementValue, parseXml, parseXmlDocument };