@stll/folio-core 0.32.2 → 0.33.1

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 (227) 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 +652 -86
  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 +56 -0
  24. package/dist/compare/__fixtures__/body-sequence.js +136 -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 +412 -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 +1141 -0
  37. package/dist/compare/reproducible-package.d.ts +21 -0
  38. package/dist/compare/reproducible-package.js +47 -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 +297 -0
  42. package/dist/compare/types.js +42 -0
  43. package/dist/compare/verification.d.ts +83 -0
  44. package/dist/compare/verification.js +189 -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/hyperlinkParser.d.ts +9 -1
  104. package/dist/docx/hyperlinkParser.js +19 -13
  105. package/dist/docx/packageParts.d.ts +35 -0
  106. package/dist/docx/packageParts.js +120 -0
  107. package/dist/docx/paraIdRangeNormalization.d.ts +40 -0
  108. package/dist/docx/paraIdRangeNormalization.js +64 -0
  109. package/dist/docx/paragraphParser.js +101 -26
  110. package/dist/docx/paragraphTraversal.js +1 -1
  111. package/dist/docx/renderedPageBreakNormalization.js +3 -1
  112. package/dist/docx/revisionIdNormalization.d.ts +27 -0
  113. package/dist/docx/revisionIdNormalization.js +121 -0
  114. package/dist/docx/rezip.d.ts +6 -0
  115. package/dist/docx/rezip.js +51 -19
  116. package/dist/docx/selectiveSave.js +2 -2
  117. package/dist/docx/serializer/commentSerializer.js +6 -1
  118. package/dist/docx/serializer/paragraphSerializer.js +73 -20
  119. package/dist/docx/serializer/runSerializer.js +11 -10
  120. package/dist/docx/serializer/tableSerializer.js +26 -10
  121. package/dist/docx/server/applyDocxXmlPatchProposal.js +1 -1
  122. package/dist/docx/server/createBilingualDocx.js +3 -2
  123. package/dist/docx/streamingXmlParser.d.ts +13 -1
  124. package/dist/docx/streamingXmlParser.js +49 -11
  125. package/dist/docx/unzip.d.ts +1 -2
  126. package/dist/docx/unzip.js +25 -6
  127. package/dist/docx/xmlParser.d.ts +3 -1
  128. package/dist/docx/xmlParser.js +33 -27
  129. package/dist/export-pdf.d.ts +43 -0
  130. package/dist/export-pdf.js +98 -0
  131. package/dist/fonts/headlessMeasure.d.ts +76 -0
  132. package/dist/fonts/headlessMeasure.js +0 -0
  133. package/dist/fonts/sfnt/parse.d.ts +51 -0
  134. package/dist/fonts/sfnt/parse.js +525 -0
  135. package/dist/fonts/sfnt/subset.d.ts +20 -0
  136. package/dist/fonts/sfnt/subset.js +350 -0
  137. package/dist/fonts/sfnt/tables.d.ts +88 -0
  138. package/dist/fonts/sfnt/tables.js +131 -0
  139. package/dist/fonts/sfnt/woff.d.ts +14 -0
  140. package/dist/fonts/sfnt/woff.js +163 -0
  141. package/dist/generated/text_shaper.js +324 -0
  142. package/dist/generated/text_shaper_bg.wasm +0 -0
  143. package/dist/headless-layout.d.ts +75 -0
  144. package/dist/headless-layout.js +350 -0
  145. package/dist/index.d.ts +9 -6
  146. package/dist/index.js +6 -3
  147. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  148. package/dist/layout-bridge/convert/footnoteLayout.js +1 -0
  149. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  150. package/dist/layout-bridge/convert/headerFooterLayout.js +2 -0
  151. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +3 -1
  152. package/dist/layout-bridge/convert/toFlowBlocks.js +2 -0
  153. package/dist/layout-engine/measure/advanceComposition.d.ts +62 -0
  154. package/dist/layout-engine/measure/advanceComposition.js +148 -0
  155. package/dist/layout-engine/measure/cache.d.ts +2 -0
  156. package/dist/layout-engine/measure/measureContainer.js +13 -0
  157. package/dist/layout-engine/measure/measureParagraph.js +1 -2
  158. package/dist/layout-engine/measure/measureTypes.d.ts +12 -0
  159. package/dist/layout-engine/measure/tableInlinePlacement.d.ts +3 -2
  160. package/dist/layout-engine/measure/tableInlinePlacement.js +14 -1
  161. package/dist/layout-engine/tableIndentCompatibility.d.ts +5 -0
  162. package/dist/layout-engine/tableIndentCompatibility.js +15 -0
  163. package/dist/layout-engine/types.d.ts +8 -0
  164. package/dist/layout-painter/renderPage.d.ts +29 -1
  165. package/dist/layout-painter/renderPage.js +47 -19
  166. package/dist/layout-painter/renderParagraph.d.ts +21 -2
  167. package/dist/layout-painter/renderParagraph.js +1 -1
  168. package/dist/layout-painter/renderTable.js +37 -4
  169. package/dist/managers/editorShortcuts.d.ts +33 -1
  170. package/dist/managers/editorShortcuts.js +20 -1
  171. package/dist/markdown/renderRuns.js +7 -2
  172. package/dist/markdown/renderTable.js +5 -1
  173. package/dist/model.d.ts +3 -3
  174. package/dist/model.js +2 -2
  175. package/dist/pdf/contentStream.d.ts +73 -0
  176. package/dist/pdf/contentStream.js +143 -0
  177. package/dist/pdf/fonts.d.ts +96 -0
  178. package/dist/pdf/fonts.js +593 -0
  179. package/dist/pdf/images.d.ts +28 -0
  180. package/dist/pdf/images.js +439 -0
  181. package/dist/pdf/objects.d.ts +93 -0
  182. package/dist/pdf/objects.js +237 -0
  183. package/dist/pdf/pageSpace.d.ts +43 -0
  184. package/dist/pdf/pageSpace.js +57 -0
  185. package/dist/pdf/paint.d.ts +32 -0
  186. package/dist/pdf/paint.js +442 -0
  187. package/dist/pdf/writePdf.d.ts +56 -0
  188. package/dist/pdf/writePdf.js +345 -0
  189. package/dist/prosemirror/commands/comments.js +96 -6
  190. package/dist/prosemirror/conversion/fromProseDoc.js +41 -7
  191. package/dist/prosemirror/conversion/toProseDoc.js +41 -4
  192. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +15 -2
  193. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +35 -6
  194. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +2 -2
  195. package/dist/prosemirror/extensions/features/pasteCleanup.js +4 -3
  196. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.d.ts +3 -1
  197. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +5 -3
  198. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.d.ts +7 -1
  199. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.js +9 -3
  200. package/dist/prosemirror/extensions/nodes/TableExtension.js +56 -39
  201. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +3 -1
  202. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +5 -3
  203. package/dist/prosemirror/plugins/suggestionMode.js +1 -0
  204. package/dist/prosemirror/revisionCarriers.js +19 -2
  205. package/dist/prosemirror/utils/extractTrackedChanges.d.ts +2 -2
  206. package/dist/prosemirror/utils/extractTrackedChanges.js +59 -23
  207. package/dist/prosemirror/validation.js +55 -27
  208. package/dist/prosemirror/zeroWidthAnchors.d.ts +10 -0
  209. package/dist/prosemirror/zeroWidthAnchors.js +23 -0
  210. package/dist/redline.js +12 -79
  211. package/dist/server.d.ts +3 -3
  212. package/dist/server.js +2 -2
  213. package/dist/shaping/placeRun.d.ts +51 -0
  214. package/dist/shaping/placeRun.js +84 -0
  215. package/dist/shaping/shaper.d.ts +92 -0
  216. package/dist/shaping/shaper.js +102 -0
  217. package/dist/types/block-id.d.ts +18 -2
  218. package/dist/types/block-id.js +49 -5
  219. package/dist/types/content.d.ts +2 -2
  220. package/dist/utils/clipboard.js +4 -3
  221. package/dist/utils/fontResolver.js +183 -0
  222. package/dist/utils/htmlComments.d.ts +17 -0
  223. package/dist/utils/htmlComments.js +22 -0
  224. package/dist/utils/scriptSegments.d.ts +12 -1
  225. package/dist/utils/scriptSegments.js +17 -1
  226. package/dist/version-comparison.d.ts +1 -1
  227. package/package.json +7 -3
@@ -0,0 +1,64 @@
1
+ import { deterministicHexId } from "../utils/hexId.js";
2
+ //#region src/docx/paraIdRangeNormalization.ts
3
+ /**
4
+ * Keep every paragraph id a package carries inside the range the schema gives
5
+ * it.
6
+ *
7
+ * `w14:paraId`, `w14:textId` and the comment-part ids that reference a
8
+ * paragraph are `ST_LongHexNumber` with a maximum: the value has to be below
9
+ * `0x80000000`, so the ids are 31-bit. Producers exist that write eight hex
10
+ * digits without that bound, and folio preserves the ids a document arrives
11
+ * with — so a package can carry an out-of-range id in, and a save that copies
12
+ * it through hands a consumer a package it will refuse.
13
+ *
14
+ * {@link paraIdInRange} is the one mapping, and it is a pure function of the
15
+ * value alone. That is what lets the parser and the save agree without
16
+ * consulting each other: a paragraph's id in the model is the id the file gets,
17
+ * so bringing an id into range does not make a document's own identity move
18
+ * under it between reading and writing. The package pass below is the same
19
+ * mapping applied to every attribute that carries such an id, so a paragraph
20
+ * and every reference to it move together.
21
+ */
22
+ /** Exclusive upper bound on a paragraph id: the values are 31-bit. */
23
+ const MAX_PARA_ID_EXCLUSIVE = 2147483648;
24
+ /**
25
+ * Every attribute that carries a paragraph id or the text-revision marker
26
+ * written beside one: the paragraph's own `w14:paraId` / `w14:textId` (the
27
+ * parser accepts a `w:` prefix too), the comment part's `w15:paraId` and the
28
+ * `w15:paraIdParent` that links a reply to its thread, and the durable-comment
29
+ * part's `w16cid:paraId`.
30
+ */
31
+ const PARA_ID_ATTRIBUTE = /\b(w|w14|w15|w16cid):(paraId|paraIdParent|textId)=(?<quote>["'])([0-9A-Fa-f]{8})\k<quote>/gu;
32
+ /** A candidate part is one that mentions any of those attributes at all. */
33
+ const PARA_ID_CANDIDATE = /\b(?:w|w14|w15|w16cid):(?:paraId|paraIdParent|textId)=/u;
34
+ /**
35
+ * `value` when it already fits, and a value derived from it when it does not.
36
+ *
37
+ * The replacement is derived from the id being replaced and from nothing else.
38
+ * A package-aware search for a free id would read better here and would be
39
+ * wrong: the parser has no package to search, and an id that means one
40
+ * paragraph while reading and another while writing is worse than the
41
+ * vanishing chance of two rewritten ids landing on one value, which is a
42
+ * duplicate rather than a package a consumer refuses.
43
+ */
44
+ const paraIdInRange = (value) => Number.parseInt(value, 16) < MAX_PARA_ID_EXCLUSIVE ? value : deterministicHexId(value);
45
+ /**
46
+ * Rewrite out-of-range paragraph ids across a whole package.
47
+ *
48
+ * Returns the parts unchanged when every id already fits, so a save of a
49
+ * document that never carried one is byte-identical.
50
+ */
51
+ const normalizeParaIdRangeInXmlParts = (parts) => {
52
+ const normalized = new Map(parts);
53
+ for (const [path, xml] of parts) {
54
+ if (!PARA_ID_CANDIDATE.test(xml)) continue;
55
+ const rewritten = xml.replaceAll(PARA_ID_ATTRIBUTE, (whole, prefix, name, quote, value) => {
56
+ const replacement = paraIdInRange(value);
57
+ return replacement === value ? whole : `${prefix}:${name}=${quote}${replacement}${quote}`;
58
+ });
59
+ if (rewritten !== xml) normalized.set(path, rewritten);
60
+ }
61
+ return normalized;
62
+ };
63
+ //#endregion
64
+ export { normalizeParaIdRangeInXmlParts, paraIdInRange };
@@ -2,16 +2,17 @@ import { isValidHexColor } from "../utils/colorResolver.js";
2
2
  import { isValidHexId } from "../utils/hexId.js";
3
3
  import { parseBookmarkEnd as parseBookmarkEnd$1, parseBookmarkStart as parseBookmarkStart$1 } from "./bookmarkParser.js";
4
4
  import { parseFieldType } from "./fieldParser.js";
5
- import { parseHyperlink as parseHyperlink$1 } from "./hyperlinkParser.js";
5
+ import { parseHyperlink as parseHyperlink$1, parseHyperlinkChild } from "./hyperlinkParser.js";
6
6
  import { markerFormattingFromLevel } from "./numberingParser.js";
7
+ import { paraIdInRange } from "./paraIdRangeNormalization.js";
7
8
  import { BorderStyleSchema, FrameWrapSchema, FrameXAlignSchema, FrameYAlignSchema, LineSpacingRuleSchema, ParagraphAlignmentSchema, ShadingPatternSchema, TabLeaderSchema, TabStopAlignmentSchema, ThemeColorSlotSchema, narrowEnum } from "./parserEnums.js";
8
9
  import { consolidateParagraphContent } from "./runConsolidator.js";
9
10
  import { parseRun, parseRunProperties } from "./runParser.js";
10
11
  import { parseSdtProperties } from "./sdtProperties.js";
11
12
  import { parseSectionProperties } from "./sectionParser.js";
12
- import { elementToXml, findChild, findChildren, getAttribute, getChildElements, getLocalName, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseNumberingLevelAttribute, parseNumericAttribute } from "./xmlParser.js";
13
+ import { WORDPROCESSINGML_NAMESPACE_URIS, elementToXml, findChild, findChildByNamespaceUri, findChildren, getAttribute, getChildElements, getLocalName, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseNumberingLevelAttribute, parseNumericAttribute } from "./xmlParser.js";
13
14
  import { panic } from "better-result";
14
- import { normalizeRevisionId } from "@stll/docx-core/model";
15
+ import { PARAGRAPH_MARK_CHANGE_KINDS, normalizeRevisionId } from "@stll/docx-core/model";
15
16
  //#region src/docx/paragraphParser.ts
16
17
  /**
17
18
  * Extract plain text from a math element (recursive text content extraction)
@@ -528,19 +529,16 @@ function parseParagraphMarkChange(pPr) {
528
529
  if (!pPr) return;
529
530
  const rPr = findChild(pPr, "w", "rPr");
530
531
  if (!rPr) return;
531
- const ins = findChild(rPr, "w", "ins");
532
- if (ins) return {
533
- kind: "ins",
534
- info: parseTrackedChangeInfo(ins)
535
- };
536
- const del = findChild(rPr, "w", "del");
537
- if (del) return {
538
- kind: "del",
539
- info: parseTrackedChangeInfo(del)
540
- };
532
+ for (const kind of PARAGRAPH_MARK_CHANGE_KINDS) {
533
+ const element = findChildByNamespaceUri(rPr, WORDPROCESSINGML_NAMESPACE_URIS, kind);
534
+ if (element) return {
535
+ kind,
536
+ info: parseTrackedChangeInfo(element)
537
+ };
538
+ }
541
539
  }
542
540
  function isTrackedChangeWrapperChild(content) {
543
- return content.type === "run" || content.type === "hyperlink";
541
+ return content.type === "run" || content.type === "hyperlink" || content.type === "bookmarkStart" || content.type === "bookmarkEnd" || content.type === "simpleField" || content.type === "complexField" || content.type === "insertion" || content.type === "deletion" || content.type === "moveFrom" || content.type === "moveTo";
544
542
  }
545
543
  function isInlineSdtContent(content) {
546
544
  return content.type === "run" || content.type === "hyperlink" || content.type === "simpleField" || content.type === "complexField" || content.type === "inlineSdt" || content.type === "insertion" || content.type === "deletion" || content.type === "moveFrom" || content.type === "moveTo" || content.type === "mathEquation";
@@ -649,6 +647,80 @@ function pushInlineSdtSegments({ contents, properties, parsedContent }) {
649
647
  function parseHyperlink(node, rels, styles, theme, media, rootXmlns = {}) {
650
648
  return parseHyperlink$1(node, rels, styles, theme, media, rootXmlns);
651
649
  }
650
+ /** The revision wrapper a `w:hyperlink` child is, when it is one. */
651
+ const hyperlinkRevisionWrapperType = (node) => {
652
+ switch (getLocalName(node.name)) {
653
+ case "ins": return "insertion";
654
+ case "del": return "deletion";
655
+ case "moveFrom": return "moveFrom";
656
+ case "moveTo": return "moveTo";
657
+ default: return;
658
+ }
659
+ };
660
+ const isHyperlinkChildContent = (content) => content.type === "run" || content.type === "bookmarkStart" || content.type === "bookmarkEnd";
661
+ /**
662
+ * A `w:hyperlink` as paragraph content, with any revision wrapper it holds
663
+ * hoisted around it.
664
+ *
665
+ * OOXML nests `w:ins`/`w:del` INSIDE `w:hyperlink`; the model nests the
666
+ * hyperlink inside the revision, because a revision is the unit a redline
667
+ * reads and a link that is half deleted is two links to it. This is the exact
668
+ * inverse of what the serializer writes, so a package survives the round trip.
669
+ */
670
+ function parseHyperlinkParagraphContents(node, rels, styles, theme, media, rootXmlns) {
671
+ const children = getChildElements(node);
672
+ if (!children.some((child) => hyperlinkRevisionWrapperType(child) !== void 0)) return [parseHyperlink(node, rels, styles, theme, media, rootXmlns)];
673
+ const inScopeXmlns = mergeXmlnsDeclarations(rootXmlns, node);
674
+ const shell = parseHyperlink(node, rels, styles, theme, media, rootXmlns);
675
+ const linkOver = (linkChildren) => ({
676
+ ...shell,
677
+ children: [...linkChildren]
678
+ });
679
+ const contents = [];
680
+ let plain = [];
681
+ const flushPlain = () => {
682
+ if (plain.length > 0) {
683
+ contents.push(linkOver(plain));
684
+ plain = [];
685
+ }
686
+ };
687
+ for (const child of children) {
688
+ const wrapperType = hyperlinkRevisionWrapperType(child);
689
+ if (wrapperType === void 0) {
690
+ const parsed = parseHyperlinkChild(child, styles, theme, rels, media, inScopeXmlns);
691
+ if (parsed) plain.push(parsed);
692
+ continue;
693
+ }
694
+ flushPlain();
695
+ const wrapped = parseParagraphContents(child, styles, theme, null, rels, media, wrapperType === "deletion" || wrapperType === "moveFrom" ? "deletion" : "default", inScopeXmlns);
696
+ const content = [];
697
+ let linked = [];
698
+ const flushLinked = () => {
699
+ if (linked.length > 0) {
700
+ content.push(linkOver(linked));
701
+ linked = [];
702
+ }
703
+ };
704
+ for (const item of wrapped) {
705
+ if (isHyperlinkChildContent(item)) {
706
+ linked.push(item);
707
+ continue;
708
+ }
709
+ flushLinked();
710
+ if (isTrackedChangeWrapperChild(item)) content.push(item);
711
+ }
712
+ flushLinked();
713
+ pushTrackedChangeWrapper({
714
+ contents,
715
+ type: wrapperType,
716
+ info: parseTrackedChangeInfo(child),
717
+ content,
718
+ preserveEmpty: true
719
+ });
720
+ }
721
+ flushPlain();
722
+ return contents;
723
+ }
652
724
  /**
653
725
  * Parse bookmark start (w:bookmarkStart)
654
726
  * Delegates to bookmarkParser module.
@@ -801,7 +873,7 @@ function parseParagraphContents(paraElement, styles, theme, _numbering, rels, me
801
873
  break;
802
874
  }
803
875
  case "hyperlink":
804
- contents.push(parseHyperlink(child, rels, styles, theme, media, inScopeXmlns));
876
+ contents.push(...parseHyperlinkParagraphContents(child, rels, styles, theme, media, inScopeXmlns));
805
877
  break;
806
878
  case "bookmarkStart":
807
879
  contents.push(parseBookmarkStart(child));
@@ -964,9 +1036,9 @@ function parseParagraph(node, styles, theme, numbering, rels = null, media = nul
964
1036
  content: []
965
1037
  };
966
1038
  const paraId = getAttribute(node, "w14", "paraId") ?? getAttribute(node, "w", "paraId");
967
- if (paraId && isValidHexId(paraId)) paragraph.paraId = paraId;
1039
+ if (paraId && isValidHexId(paraId)) paragraph.paraId = paraIdInRange(paraId);
968
1040
  const textId = getAttribute(node, "w14", "textId") ?? getAttribute(node, "w", "textId");
969
- if (textId && isValidHexId(textId)) paragraph.textId = textId;
1041
+ if (textId && isValidHexId(textId)) paragraph.textId = paraIdInRange(textId);
970
1042
  if (!options?.inHeaderFooter && paragraphStartsWithRenderedPageBreak(node)) paragraph.renderedPageBreakBefore = true;
971
1043
  const pPr = findChild(node, "w", "pPr");
972
1044
  if (pPr) {
@@ -980,16 +1052,19 @@ function parseParagraph(node, styles, theme, numbering, rels = null, media = nul
980
1052
  if (sectPr) paragraph.sectionProperties = parseSectionProperties(sectPr);
981
1053
  }
982
1054
  paragraph.content = consolidateParagraphContent(parseParagraphContents(node, styles, theme, numbering, rels, media, "default", options?.rootXmlns ?? {}));
983
- let effectiveNumPr = paragraph.formatting?.numPr;
1055
+ const paragraphFormatting = paragraph.formatting;
1056
+ const directNumPr = paragraphFormatting?.numPr;
1057
+ const styleNumPr = paragraphFormatting?.styleId && styles ? styles.get(paragraphFormatting.styleId)?.pPr?.numPr : void 0;
1058
+ let effectiveNumPr = directNumPr;
984
1059
  let numPrFromStyle = false;
985
- if (!effectiveNumPr && paragraph.formatting?.styleId && styles) {
986
- const style = styles.get(paragraph.formatting.styleId);
987
- if (style?.pPr?.numPr) {
988
- effectiveNumPr = style.pPr.numPr;
989
- numPrFromStyle = true;
990
- paragraph.formatting.numPr = effectiveNumPr;
991
- paragraph.formatting.numPrFromStyle = effectiveNumPr;
992
- }
1060
+ if (paragraphFormatting && styleNumPr && directNumPr?.numId === void 0) {
1061
+ effectiveNumPr = {
1062
+ ...styleNumPr,
1063
+ ...directNumPr
1064
+ };
1065
+ numPrFromStyle = true;
1066
+ paragraphFormatting.numPr = effectiveNumPr;
1067
+ paragraphFormatting.numPrFromStyle = styleNumPr;
993
1068
  }
994
1069
  if (effectiveNumPr && numbering) {
995
1070
  const { numId, ilvl = 0 } = effectiveNumPr;
@@ -13,7 +13,7 @@ const visitParagraphRuns = (paragraph, visit) => {
13
13
  visitRun(child);
14
14
  continue;
15
15
  }
16
- visitHyperlink(child);
16
+ if (child.type === "hyperlink") visitHyperlink(child);
17
17
  }
18
18
  };
19
19
  const visitParagraphContent = (content) => {
@@ -37,7 +37,9 @@ const scanHyperlink = (hyperlink, scan) => {
37
37
  };
38
38
  const scanInlineContent = (content, scan) => {
39
39
  for (const child of content) {
40
- const result = child.type === "run" ? scanRun(child, scan) : scanHyperlink(child, scan);
40
+ let result;
41
+ if (child.type === "run") result = scanRun(child, scan);
42
+ else if (child.type === "hyperlink") result = scanHyperlink(child, scan);
41
43
  if (result !== void 0) return result;
42
44
  }
43
45
  };
@@ -0,0 +1,27 @@
1
+ //#region src/docx/revisionIdNormalization.d.ts
2
+ declare const RevisionIdCollisionError_base: import("better-result").TaggedErrorClass<"RevisionIdCollisionError">;
3
+ /**
4
+ * Two revision elements in one package claimed the same `w:id`.
5
+ *
6
+ * `w:id` on a revision element is unique across the package, so a collision is
7
+ * a package a consumer may reject rather than a cosmetic detail. The
8
+ * normalization below hands every id it emits to one choke point, which throws
9
+ * this rather than letting the duplicate reach the ZIP.
10
+ */
11
+ declare class RevisionIdCollisionError extends RevisionIdCollisionError_base<{
12
+ message: string;
13
+ revisionId: number;
14
+ part: string;
15
+ }> {}
16
+ declare const REVISION_ELEMENT_NAMES: Set<string>;
17
+ /**
18
+ * Keep physical tracked-change element ids unique across a package.
19
+ *
20
+ * Live editor marks and operation receipts keep their logical revision IDs.
21
+ * Saving assigns fresh IDs only where one logical change was split into
22
+ * multiple physical OOXML wrappers; reopening therefore exposes the physical
23
+ * wrapper IDs that the file format requires.
24
+ */
25
+ declare const normalizeRevisionIdsInXmlParts: (parts: ReadonlyMap<string, string>) => Map<string, string>;
26
+ //#endregion
27
+ export { REVISION_ELEMENT_NAMES, RevisionIdCollisionError, normalizeRevisionIdsInXmlParts };
@@ -0,0 +1,121 @@
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
+ import { TaggedError } from "better-result";
5
+ //#region src/docx/revisionIdNormalization.ts
6
+ /**
7
+ * Two revision elements in one package claimed the same `w:id`.
8
+ *
9
+ * `w:id` on a revision element is unique across the package, so a collision is
10
+ * a package a consumer may reject rather than a cosmetic detail. The
11
+ * normalization below hands every id it emits to one choke point, which throws
12
+ * this rather than letting the duplicate reach the ZIP.
13
+ */
14
+ var RevisionIdCollisionError = class extends TaggedError("RevisionIdCollisionError") {};
15
+ const REVISION_ELEMENT_NAMES = /* @__PURE__ */ new Set([
16
+ "cellDel",
17
+ "cellIns",
18
+ "cellMerge",
19
+ "del",
20
+ "ins",
21
+ "moveFrom",
22
+ "moveTo",
23
+ "numberingChange",
24
+ "pPrChange",
25
+ "rPrChange",
26
+ "sectPrChange",
27
+ "tblGridChange",
28
+ "tblPrChange",
29
+ "tblPrExChange",
30
+ "tcPrChange",
31
+ "trPrChange"
32
+ ]);
33
+ const REVISION_ELEMENT_CANDIDATE = new RegExp(`<(?:[^\\s<>/:]+:)?(?:${[...REVISION_ELEMENT_NAMES].join("|")})(?:[\\s/>])`, "u");
34
+ const revisionAttribute = (element) => {
35
+ if (!element.name || !WORDPROCESSINGML_NAMESPACE_URIS.has(getNamespaceUri(element) ?? "") || !REVISION_ELEMENT_NAMES.has(getLocalName(element.name))) return null;
36
+ const attribute = findAttributeByNamespaceUri(element, WORDPROCESSINGML_NAMESPACE_URIS, "id");
37
+ if (!attribute) return null;
38
+ const id = Number(attribute.value);
39
+ return Number.isSafeInteger(id) && id >= 0 ? {
40
+ name: attribute.name,
41
+ id
42
+ } : null;
43
+ };
44
+ /**
45
+ * Keep physical tracked-change element ids unique across a package.
46
+ *
47
+ * Live editor marks and operation receipts keep their logical revision IDs.
48
+ * Saving assigns fresh IDs only where one logical change was split into
49
+ * multiple physical OOXML wrappers; reopening therefore exposes the physical
50
+ * wrapper IDs that the file format requires.
51
+ */
52
+ const normalizeRevisionIdsInXmlParts = (parts) => {
53
+ const candidates = [...parts].filter(([, xml]) => REVISION_ELEMENT_CANDIDATE.test(xml));
54
+ const occurrencesByPath = /* @__PURE__ */ new Map();
55
+ const reserved = /* @__PURE__ */ new Set();
56
+ for (const [path, xml] of candidates) {
57
+ assertXmlResourceLimits(xml);
58
+ const ids = [];
59
+ if (rewriteStreamingXmlDecimalAttributes(xml, (element) => {
60
+ const attribute = revisionAttribute(element);
61
+ if (attribute) {
62
+ ids.push(attribute.id);
63
+ reserved.add(attribute.id);
64
+ }
65
+ return null;
66
+ }).status === "unsupported") throw new XmlResourceLimitError({
67
+ message: `Revision-id normalization could not safely scan ${path}`,
68
+ limit: "syntax"
69
+ });
70
+ occurrencesByPath.set(path, ids);
71
+ }
72
+ const repeatedPaths = /* @__PURE__ */ new Set();
73
+ const firstSeen = /* @__PURE__ */ new Set();
74
+ for (const [path, ids] of occurrencesByPath) for (const id of ids) if (firstSeen.has(id)) repeatedPaths.add(path);
75
+ else firstSeen.add(id);
76
+ let nextId = 0;
77
+ const allocate = () => {
78
+ while (reserved.has(nextId)) nextId += 1;
79
+ const allocated = nextId;
80
+ reserved.add(allocated);
81
+ nextId += 1;
82
+ return allocated;
83
+ };
84
+ const seen = /* @__PURE__ */ new Set();
85
+ const normalized = new Map(parts);
86
+ for (const [path, xml] of candidates) {
87
+ const ids = occurrencesByPath.get(path);
88
+ if (!ids) continue;
89
+ const claim = (id) => {
90
+ if (seen.has(id)) throw new RevisionIdCollisionError({
91
+ message: `Revision id ${String(id)} is claimed twice in ${path}`,
92
+ revisionId: id,
93
+ part: path
94
+ });
95
+ seen.add(id);
96
+ };
97
+ if (!repeatedPaths.has(path) && ids.every((id) => !seen.has(id))) {
98
+ for (const id of ids) claim(id);
99
+ continue;
100
+ }
101
+ const rewritten = rewriteStreamingXmlDecimalAttributes(xml, (element) => {
102
+ const attribute = revisionAttribute(element);
103
+ if (!attribute) return null;
104
+ if (!seen.has(attribute.id)) {
105
+ claim(attribute.id);
106
+ return null;
107
+ }
108
+ const replacement = allocate();
109
+ claim(replacement);
110
+ return /* @__PURE__ */ new Map([[attribute.name, String(replacement)]]);
111
+ });
112
+ if (rewritten.status === "unsupported") throw new XmlResourceLimitError({
113
+ message: `Revision-id normalization could not safely rewrite ${path}`,
114
+ limit: "syntax"
115
+ });
116
+ normalized.set(path, rewritten.value);
117
+ }
118
+ return normalized;
119
+ };
120
+ //#endregion
121
+ export { REVISION_ELEMENT_NAMES, RevisionIdCollisionError, normalizeRevisionIdsInXmlParts };
@@ -105,6 +105,12 @@ declare function updateMultipleFiles(originalBuffer: ArrayBuffer, updates: Map<s
105
105
  /**
106
106
  * Apply file updates to an already-loaded JSZip instance and generate the output.
107
107
  * Use this when the zip is already loaded to avoid a redundant decompression pass.
108
+ *
109
+ * This is the selective save's exit, so it owes the package the same id passes
110
+ * {@link generateDocxZip} runs: a save that rewrites only the changed
111
+ * paragraphs still has to see the parts it left alone, both to know which
112
+ * revision ids are free and because an out-of-range paragraph id can sit in a
113
+ * part it never touched.
108
114
  */
109
115
  declare function applyUpdatesToZip(zip: JSZip, updates: Map<string, string | ArrayBuffer>, options?: RepackOptions): Promise<ArrayBuffer>;
110
116
  /**
@@ -5,7 +5,10 @@ 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";
9
+ import { normalizeParaIdRangeInXmlParts } from "./paraIdRangeNormalization.js";
8
10
  import { RELATIONSHIP_TYPES, parseRelationships, resolveRelativePath } from "./relsParser.js";
11
+ import { normalizeRevisionIdsInXmlParts } from "./revisionIdNormalization.js";
9
12
  import { appendNumberingDefs, buildPatchedNotePartXml, buildPatchedNumberingXml, collectAddedNumberingDefs, collectChangedNoteParaIds, collectChangedNumberingDefs, collectParaIds } from "./selectiveXmlPatch.js";
10
13
  import { ensureThreadedCommentParaIds, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
11
14
  import { serializeDocument } from "./serializer/documentSerializer.js";
@@ -17,7 +20,6 @@ import { serializeSettingsXml } from "./serializer/settingsSerializer.js";
17
20
  import { serializeStyle, serializeStylesXml } from "./serializer/stylesSerializer.js";
18
21
  import { serializeThemeXml } from "./serializer/themeSerializer.js";
19
22
  import { escapeXml } from "./serializer/xmlUtils.js";
20
- import { isPreservableDocxEntry } from "./unzip.js";
21
23
  import { WORDPROCESSINGML_NAMESPACE_URIS, findChild, getAttribute, getChildElements, getLocalName, getNamespaceUri, matchesName, parseXml, parseXmlDocument } from "./xmlParser.js";
22
24
  import { assertXmlResourceLimits } from "./xmlResourceLimits.js";
23
25
  import { panic } from "better-result";
@@ -405,20 +407,47 @@ async function processNewHyperlinks(parts, zip, compressionLevel) {
405
407
  });
406
408
  }
407
409
  }
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);
410
+ /**
411
+ * Bring the ids a package addresses itself by inside the bounds the format
412
+ * gives them.
413
+ *
414
+ * Both bounds belong to the package rather than to a part, and neither is
415
+ * something one serializer can see on its own. A revision `w:id` is unique
416
+ * across the package, and one logical revision serializes as several physical
417
+ * wrappers a word diff cut around unchanged words, a revision split around a
418
+ * hyperlink. A paragraph id is 31-bit, and a paragraph is referenced by id
419
+ * from parts other than the one it lives in. So the passes run at the exits,
420
+ * over every `word/*.xml` part including the ones a save left untouched.
421
+ */
422
+ const normalizePackageIdsInZip = async (zip, compressionLevel) => {
423
+ const xmlParts = /* @__PURE__ */ new Map();
424
+ 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"));
425
+ const normalizedParts = normalizeParaIdRangeInXmlParts(normalizeRevisionIdsInXmlParts(xmlParts));
426
+ for (const [path, xml] of normalizedParts) if (xml !== xmlParts.get(path)) zip.file(path, xml, {
427
+ compression: "DEFLATE",
428
+ compressionOptions: { level: compressionLevel }
429
+ });
430
+ };
431
+ /**
432
+ * The single exit for a repacked package. Reconciliation runs here rather than
433
+ * at each caller so no save path can emit a package whose relationships or
434
+ * content types name a part it does not hold.
435
+ */
436
+ const generateDocxZip = async (zip, compressionLevel) => {
437
+ await reconcilePackageReferences(zip, compressionLevel);
438
+ await normalizePackageIdsInZip(zip, compressionLevel);
439
+ return zip.generateAsync({
440
+ type: "arraybuffer",
441
+ compression: "DEFLATE",
442
+ compressionOptions: { level: compressionLevel }
443
+ });
416
444
  };
445
+ const parsedZipSources = /* @__PURE__ */ new WeakMap();
417
446
  const loadParsedZipSource = async (document, buffer) => {
418
447
  const cached = parsedZipSources.get(document);
419
448
  if (cached?.buffer === buffer) return cached;
420
449
  const zip = await JSZip.loadAsync(buffer);
421
- removeNonPreservableEntries(zip);
450
+ removeUnsafeEntries(zip);
422
451
  const [documentXml, corePropertiesXml] = await Promise.all([zip.file("word/document.xml")?.async("text"), zip.file("docProps/core.xml")?.async("text")]);
423
452
  const source = {
424
453
  buffer,
@@ -486,7 +515,7 @@ async function repackDocx(doc, options = {}) {
486
515
  const exportDocument = withoutOrphanCommentRanges(doc);
487
516
  const originalZip = await JSZip.loadAsync(doc.originalBuffer);
488
517
  const [originalDocumentXml, originalCorePropertiesXml] = await Promise.all([originalZip.file("word/document.xml")?.async("text"), originalZip.file("docProps/core.xml")?.async("text")]);
489
- removeNonPreservableEntries(originalZip);
518
+ removeUnsafeEntries(originalZip);
490
519
  const newZip = cloneDocxZip(originalZip);
491
520
  return finishRepack({
492
521
  document: exportDocument,
@@ -517,7 +546,7 @@ async function repackDocxFromRaw(doc, rawContent, options = {}) {
517
546
  newZip.folder(path.replace(/\/$/u, ""));
518
547
  continue;
519
548
  }
520
- if (!isPreservableDocxEntry(path)) continue;
549
+ if (isUnsafePackagePath(path)) continue;
521
550
  const content = await file.async("arraybuffer");
522
551
  newZip.file(path, content, {
523
552
  compression: "DEFLATE",
@@ -559,11 +588,7 @@ async function repackDocxFromRaw(doc, rawContent, options = {}) {
559
588
  compressionOptions: { level: compressionLevel }
560
589
  });
561
590
  }
562
- return await newZip.generateAsync({
563
- type: "arraybuffer",
564
- compression: "DEFLATE",
565
- compressionOptions: { level: compressionLevel }
566
- });
591
+ return generateDocxZip(newZip, compressionLevel);
567
592
  }
568
593
  const COMMENTS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml";
569
594
  const COMMENTS_EXTENDED_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml";
@@ -749,14 +774,21 @@ async function updateMultipleFiles(originalBuffer, updates, options = {}) {
749
774
  /**
750
775
  * Apply file updates to an already-loaded JSZip instance and generate the output.
751
776
  * Use this when the zip is already loaded to avoid a redundant decompression pass.
777
+ *
778
+ * This is the selective save's exit, so it owes the package the same id passes
779
+ * {@link generateDocxZip} runs: a save that rewrites only the changed
780
+ * paragraphs still has to see the parts it left alone, both to know which
781
+ * revision ids are free and because an out-of-range paragraph id can sit in a
782
+ * part it never touched.
752
783
  */
753
- function applyUpdatesToZip(zip, updates, options = {}) {
784
+ async function applyUpdatesToZip(zip, updates, options = {}) {
754
785
  const { compressionLevel = 6 } = options;
755
786
  for (const [path, content] of updates) zip.file(path, content, {
756
787
  compression: "DEFLATE",
757
788
  compressionOptions: { level: compressionLevel }
758
789
  });
759
- return zip.generateAsync({
790
+ await normalizePackageIdsInZip(zip, compressionLevel);
791
+ return await zip.generateAsync({
760
792
  type: "arraybuffer",
761
793
  compression: "DEFLATE",
762
794
  compressionOptions: { level: compressionLevel }
@@ -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