@stll/folio-core 0.59.0 → 0.61.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 (226) hide show
  1. package/README.md +23 -1
  2. package/dist/ai-edits/apply.js +139 -32
  3. package/dist/ai-edits/headless.d.ts +3 -0
  4. package/dist/ai-edits/headless.js +57 -63
  5. package/dist/ai-edits/note-story-inventory.d.ts +7 -0
  6. package/dist/ai-edits/note-story-inventory.js +11 -0
  7. package/dist/ai-edits/read.js +16 -9
  8. package/dist/ai-edits/revisionStretches.js +52 -1
  9. package/dist/ai-edits/table-row-column-mutations.js +6 -1
  10. package/dist/compare/reproducible-package.js +2 -1
  11. package/dist/compare/verification.js +1 -0
  12. package/dist/compat/eigenpal.d.ts +4 -2
  13. package/dist/compat/eigenpal.js +2 -2
  14. package/dist/controller/canonicalClipboard.js +2 -1
  15. package/dist/controller/canonicalListRendering.d.ts +4 -6
  16. package/dist/controller/canonicalListRendering.js +3 -157
  17. package/dist/controller/canonicalSession.js +3 -2
  18. package/dist/controller/canonicalStructure.js +10 -7
  19. package/dist/controller/fontReadiness.js +1 -1
  20. package/dist/controller/headerFooterEditorManager.d.ts +1 -0
  21. package/dist/controller/headerFooterEditorManager.js +11 -0
  22. package/dist/controller/hiddenEditorManager.d.ts +1 -1
  23. package/dist/controller/hiddenEditorManager.js +3 -1
  24. package/dist/controller/layoutPipeline.d.ts +3 -1
  25. package/dist/controller/layoutPipeline.js +19 -4
  26. package/dist/controller/layoutSession.d.ts +4 -1
  27. package/dist/display-list/build/fontTable.js +7 -5
  28. package/dist/display-list/build/furniture.d.ts +1 -0
  29. package/dist/display-list/build/furniture.js +1 -0
  30. package/dist/display-list/build/imagePrimitives.js +3 -6
  31. package/dist/display-list/build/watermarkPrimitives.d.ts +2 -1
  32. package/dist/display-list/build/watermarkPrimitives.js +9 -3
  33. package/dist/display-list/editor/pageRenderer.js +1 -0
  34. package/dist/docx/bookmarkIdNormalization.d.ts +10 -0
  35. package/dist/docx/bookmarkIdNormalization.js +100 -0
  36. package/dist/docx/bookmarkParser.js +4 -2
  37. package/dist/docx/canonicalStoryRepack.js +3 -2
  38. package/dist/docx/commentTextParagraph.d.ts +6 -0
  39. package/dist/docx/commentTextParagraph.js +15 -0
  40. package/dist/docx/conformance.js +6 -1
  41. package/dist/docx/diagramPreview.js +2 -1
  42. package/dist/docx/documentParser.d.ts +13 -1
  43. package/dist/docx/documentParser.js +1 -1
  44. package/dist/docx/encryption/compoundFile.d.ts +0 -8
  45. package/dist/docx/encryption/compoundFile.js +2 -1
  46. package/dist/docx/encryption/cryptoBytes.d.ts +2 -2
  47. package/dist/docx/encryption/cryptoBytes.js +3 -18
  48. package/dist/docx/encryption/encryptionInfo.d.ts +0 -5
  49. package/dist/docx/encryption/encryptionInfo.js +2 -1
  50. package/dist/docx/ensureParaIds.js +2 -1
  51. package/dist/docx/groupDrawingParser.js +6 -1
  52. package/dist/docx/importedIdCollisions.d.ts +136 -0
  53. package/dist/docx/importedIdCollisions.js +215 -0
  54. package/dist/docx/listModelNormalization.d.ts +10 -0
  55. package/dist/docx/listModelNormalization.js +166 -0
  56. package/dist/docx/metadataPrivacy.js +3 -2
  57. package/dist/docx/metafileRaster.js +2 -1
  58. package/dist/docx/numberingParser.js +2 -2
  59. package/dist/docx/packageParts.js +6 -5
  60. package/dist/docx/paragraphParser.js +6 -4
  61. package/dist/docx/paragraphPropertySource.js +1 -2
  62. package/dist/docx/parseContext.d.ts +2 -1
  63. package/dist/docx/parseContext.js +1 -0
  64. package/dist/docx/parseWarningMessage.js +1 -0
  65. package/dist/docx/parser.js +10 -5
  66. package/dist/docx/preservedRunContent.js +11 -2
  67. package/dist/docx/relsParser.js +1 -1
  68. package/dist/docx/removeHeaderFooterParts.js +8 -7
  69. package/dist/docx/replyToComment.js +3 -13
  70. package/dist/docx/rezip.js +90 -90
  71. package/dist/docx/runParser.js +12 -6
  72. package/dist/docx/runPropertyPresence.d.ts +11 -0
  73. package/dist/docx/runPropertyPresence.js +14 -0
  74. package/dist/docx/selectedXmlIdentityScan.d.ts +10 -0
  75. package/dist/docx/selectedXmlIdentityScan.js +36 -0
  76. package/dist/docx/selectiveSave.js +19 -17
  77. package/dist/docx/selectiveXmlPatch.js +44 -52
  78. package/dist/docx/serializer/commentSerializer.d.ts +3 -1
  79. package/dist/docx/serializer/commentSerializer.js +11 -2
  80. package/dist/docx/serializer/runSerializer.js +5 -4
  81. package/dist/docx/serializer/tableSerializer.js +2 -1
  82. package/dist/docx/serializer/trackedChangeAttributes.d.ts +1 -3
  83. package/dist/docx/serializer/trackedChangeAttributes.js +1 -11
  84. package/dist/docx/server/applyDocxXmlPatchProposal.js +2 -2
  85. package/dist/docx/server/boundedArchive.js +2 -1
  86. package/dist/docx/server/evaluateDocxXmlPatchProposal.js +2 -1
  87. package/dist/docx/server/inspectDocxPackage.js +5 -1
  88. package/dist/docx/server/validateDocxConformance.js +2 -1
  89. package/dist/docx/tableParser.js +7 -4
  90. package/dist/docx/tableRowPropertyPresence.d.ts +9 -0
  91. package/dist/docx/tableRowPropertyPresence.js +15 -0
  92. package/dist/docx/unzip.js +2 -1
  93. package/dist/docx/xmlEncoding.js +6 -5
  94. package/dist/fonts/embeddedFontFingerprint.d.ts +5 -0
  95. package/dist/fonts/embeddedFontFingerprint.js +18 -0
  96. package/dist/fonts/embeddedFonts.d.ts +1 -1
  97. package/dist/fonts/embeddedFonts.js +1 -1
  98. package/dist/fonts/fontsourceFaces.js +2 -1
  99. package/dist/fonts/headlessMeasure.js +0 -0
  100. package/dist/fonts/sfnt/parse.js +6 -9
  101. package/dist/fonts/sfnt/subset.js +2 -1
  102. package/dist/fonts/sfnt/tables.js +2 -3
  103. package/dist/fonts/sfnt/woff.js +2 -1
  104. package/dist/headless-layout.js +1 -0
  105. package/dist/index.d.ts +4 -2
  106. package/dist/index.js +2 -2
  107. package/dist/internal/revisionResolutionInline.d.ts +2 -1
  108. package/dist/internal/revisionResolutionInline.js +22 -1
  109. package/dist/internal/wholeStoryRevisionResolution.js +24 -7
  110. package/dist/layout-bridge/convert/flowConversionShared.d.ts +4 -0
  111. package/dist/layout-bridge/convert/footnoteLayout.d.ts +5 -1
  112. package/dist/layout-bridge/convert/footnoteLayout.js +16 -3
  113. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +6 -0
  114. package/dist/layout-bridge/convert/headerFooterLayout.js +13 -14
  115. package/dist/layout-bridge/convert/paragraphConversion.js +8 -1
  116. package/dist/layout-bridge/convert/paragraphRuns.js +2 -1
  117. package/dist/layout-bridge/convert/toFlowBlocks.js +25 -1
  118. package/dist/layout-engine/measure/cache.js +39 -7
  119. package/dist/layout-engine/measure/crossRunWords.js +12 -2
  120. package/dist/layout-engine/measure/lineTypography.js +1 -0
  121. package/dist/layout-engine/measure/listMarkerWidth.d.ts +1 -0
  122. package/dist/layout-engine/measure/listMarkerWidth.js +1 -0
  123. package/dist/layout-engine/measure/measureContainer.js +3 -4
  124. package/dist/layout-engine/measure/measureHelpers.d.ts +6 -9
  125. package/dist/layout-engine/measure/measureHelpers.js +21 -18
  126. package/dist/layout-engine/measure/measureParagraph.js +2 -0
  127. package/dist/layout-engine/measure/measureTypes.d.ts +2 -0
  128. package/dist/layout-engine/types.d.ts +4 -1
  129. package/dist/layout-painter/renderPage.d.ts +1 -0
  130. package/dist/layout-painter/renderPage.js +11 -3
  131. package/dist/layout-painter/renderParagraph.js +13 -3
  132. package/dist/layout-painter/renderWatermark.d.ts +1 -0
  133. package/dist/layout-painter/renderWatermark.js +7 -3
  134. package/dist/managers/AutoSaveManager.js +2 -1
  135. package/dist/managers/EditorModeManager.d.ts +10 -1
  136. package/dist/managers/EditorModeManager.js +15 -1
  137. package/dist/managers/TableSelectionManager.d.ts +1 -1
  138. package/dist/managers/autoSaveCodec.js +2 -6
  139. package/dist/managers/editorShortcuts.d.ts +2 -1
  140. package/dist/managers/editorShortcuts.js +2 -1
  141. package/dist/managers/reviewDisplayMode.d.ts +8 -0
  142. package/dist/managers/reviewDisplayMode.js +15 -0
  143. package/dist/paged-layout/pageText.d.ts +11 -2
  144. package/dist/paged-layout/pageText.js +8 -3
  145. package/dist/pdf/fonts.js +5 -14
  146. package/dist/pdf/images.js +3 -2
  147. package/dist/pdf/objects.d.ts +0 -11
  148. package/dist/pdf/objects.js +7 -9
  149. package/dist/pdf/writePdf.js +2 -1
  150. package/dist/prosemirror/canonicalSelectionRange.d.ts +4 -1
  151. package/dist/prosemirror/canonicalSelectionRange.js +9 -3
  152. package/dist/prosemirror/commands/comments.js +51 -13
  153. package/dist/prosemirror/containerFinalParagraph.d.ts +8 -8
  154. package/dist/prosemirror/containerFinalParagraph.js +8 -8
  155. package/dist/prosemirror/conversion/fromProseDoc.js +9 -4
  156. package/dist/prosemirror/conversion/toProseDoc.js +4 -4
  157. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.d.ts +8 -2
  158. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +39 -3
  159. package/dist/prosemirror/extensions/features/EmptyParagraphFormatExtension.d.ts +8 -2
  160. package/dist/prosemirror/extensions/features/EmptyParagraphFormatExtension.js +4 -3
  161. package/dist/prosemirror/extensions/features/ImageDragExtension.js +4 -3
  162. package/dist/prosemirror/extensions/features/JoinedRunStyleExtension.js +4 -3
  163. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +9 -3
  164. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +6 -5
  165. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +8 -2
  166. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +11 -3
  167. package/dist/prosemirror/extensions/marks/RunIdentityExtension.js +4 -3
  168. package/dist/prosemirror/extensions/marks/markUtils.js +1 -1
  169. package/dist/prosemirror/extensions/nodes/CommentReferenceExtension.js +16 -2
  170. package/dist/prosemirror/findReplaceSelection.d.ts +11 -1
  171. package/dist/prosemirror/findReplaceSelection.js +15 -1
  172. package/dist/prosemirror/markupViewProjection.d.ts +10 -1
  173. package/dist/prosemirror/markupViewProjection.js +21 -3
  174. package/dist/prosemirror/pluginKeyOwner.d.ts +13 -0
  175. package/dist/prosemirror/pluginKeyOwner.js +35 -0
  176. package/dist/prosemirror/plugins/aiCitationDecorations.d.ts +11 -4
  177. package/dist/prosemirror/plugins/aiCitationDecorations.js +16 -3
  178. package/dist/prosemirror/plugins/aiSuggestionDecorations.d.ts +11 -4
  179. package/dist/prosemirror/plugins/aiSuggestionDecorations.js +18 -3
  180. package/dist/prosemirror/plugins/anonymizationDecorations.d.ts +11 -11
  181. package/dist/prosemirror/plugins/anonymizationDecorations.js +23 -13
  182. package/dist/prosemirror/plugins/autocompleteSuggestion.d.ts +8 -2
  183. package/dist/prosemirror/plugins/autocompleteSuggestion.js +27 -5
  184. package/dist/prosemirror/plugins/contentControlWidgets.d.ts +8 -2
  185. package/dist/prosemirror/plugins/contentControlWidgets.js +4 -3
  186. package/dist/prosemirror/plugins/createDocScanPlugin.d.ts +6 -5
  187. package/dist/prosemirror/plugins/createDocScanPlugin.js +1 -1
  188. package/dist/prosemirror/plugins/documentNumbering.js +5 -3
  189. package/dist/prosemirror/plugins/documentStyleState.d.ts +8 -2
  190. package/dist/prosemirror/plugins/documentStyleState.js +2 -2
  191. package/dist/prosemirror/plugins/documentStyles.js +1 -1
  192. package/dist/prosemirror/plugins/selectionTracker.d.ts +8 -2
  193. package/dist/prosemirror/plugins/selectionTracker.js +4 -3
  194. package/dist/prosemirror/plugins/suggestionMode.d.ts +8 -2
  195. package/dist/prosemirror/plugins/suggestionMode.js +4 -3
  196. package/dist/prosemirror/plugins/templateDirectives.d.ts +8 -2
  197. package/dist/prosemirror/plugins/templateDirectives.js +19 -4
  198. package/dist/prosemirror/plugins/templatePreviewValues.d.ts +8 -2
  199. package/dist/prosemirror/plugins/templatePreviewValues.js +27 -5
  200. package/dist/prosemirror/plugins/templateSlashMenu.d.ts +8 -2
  201. package/dist/prosemirror/plugins/templateSlashMenu.js +23 -5
  202. package/dist/prosemirror/plugins/transactionInvariants.js +29 -4
  203. package/dist/prosemirror/replacedAnnotations.d.ts +15 -1
  204. package/dist/prosemirror/replacedAnnotations.js +71 -12
  205. package/dist/prosemirror/runFormattingReconciliation.d.ts +3 -1
  206. package/dist/prosemirror/runFormattingReconciliation.js +10 -2
  207. package/dist/prosemirror/runIdentityAcrossRevisions.d.ts +5 -7
  208. package/dist/prosemirror/runIdentityAcrossRevisions.js +66 -38
  209. package/dist/prosemirror/runPropertyChangeResolution.js +13 -1
  210. package/dist/utils/base64.d.ts +1 -31
  211. package/dist/utils/base64.js +19 -33
  212. package/dist/utils/clipboard.js +5 -4
  213. package/dist/utils/colorResolver.js +11 -2
  214. package/dist/utils/findReplace.d.ts +5 -0
  215. package/dist/utils/fontLoader.d.ts +2 -2
  216. package/dist/utils/fontLoader.js +2 -7
  217. package/dist/utils/fontResolver.d.ts +7 -3
  218. package/dist/utils/fontResolver.js +6 -8
  219. package/dist/utils/replaceText.js +21 -4
  220. package/dist/utils/sliceText.d.ts +2 -0
  221. package/dist/utils/sliceText.js +2 -0
  222. package/dist/utils/textEncoding.d.ts +2 -0
  223. package/dist/utils/textEncoding.js +2 -0
  224. package/dist/utils/tiffConverter.d.ts +0 -9
  225. package/dist/utils/tiffConverter.js +2 -4
  226. package/package.json +73 -31
@@ -1,4 +1,5 @@
1
1
  import { parseHorizontalScalePercent } from "../utils/horizontalScale.js";
2
+ import { sliceText } from "../utils/sliceText.js";
2
3
  import { NO_MODELLED_ATTRIBUTES, attributeRemainder } from "./attributeRemainder.js";
3
4
  import { CAPTURE, dispatchChildrenWithContext, keptUnless, ownedElsewhere, sequencePositions } from "./containerChildren.js";
4
5
  import { parseDiagramPreview } from "./diagramPreview.js";
@@ -10,6 +11,7 @@ import { EmphasisMarkSchema, FontHintSchema, FontThemeSchema, HighlightColorSche
10
11
  import { preserveRunChild } from "./preservedRunContent.js";
11
12
  import { standalonePreviewLedger } from "./previewBudget.js";
12
13
  import { parseBoundaryJoins } from "./reviewResolutionProvenance.js";
14
+ import { isEmptyRunPropertiesAttribute, omitsPreviousRunProperties, retainsEmptyRunProperties } from "./runPropertyPresence.js";
13
15
  import { parseShading } from "./shadingParser.js";
14
16
  import { captureShapeAlternateContent } from "./shapeAlternateContent.js";
15
17
  import { parseShapeFromDrawing, shouldPreserveRawShapeDrawing } from "./shapeParser.js";
@@ -48,7 +50,11 @@ import { DRAWING_RAW_XML_MODES } from "@stll/docx-core/model";
48
50
  * attacker-sized string per run.
49
51
  */
50
52
  const MAX_LANGUAGE_TAG_LENGTH = 35;
51
- const truncateLanguageTag = (value) => value === void 0 ? void 0 : value.slice(0, MAX_LANGUAGE_TAG_LENGTH);
53
+ const truncateLanguageTag = (value) => value === void 0 ? void 0 : sliceText({
54
+ text: value,
55
+ units: "codeUnits",
56
+ end: MAX_LANGUAGE_TAG_LENGTH
57
+ });
52
58
  function parseColorValue({ rgb, themeColor, themeTint, themeShade, element }) {
53
59
  const color = {};
54
60
  if (rgb && rgb !== "auto") color.rgb = rgb;
@@ -429,7 +435,7 @@ function parseRunProperties(rPr, theme, owner) {
429
435
  });
430
436
  if (preserved) formatting.preserved = preserved;
431
437
  const children = getChildElements(rPr);
432
- if (owner === RUN_PROPERTY_OWNERS.paragraphMark && Object.keys(formatting).length === 0 && children.length > 0 && children.every((child) => WORDPROCESSINGML_NAMESPACE_URIS.has(getNamespaceUri(child) ?? "") && Object.hasOwn(OWNED_BY_A_SIBLING_RECORD.paragraphMark, getLocalName(child.name)))) return;
438
+ if (owner !== RUN_PROPERTY_OWNERS.standalone && Object.keys(formatting).length === 0 && children.length > 0 && children.every((child) => WORDPROCESSINGML_NAMESPACE_URIS.has(getNamespaceUri(child) ?? "") && Object.hasOwn(OWNED_BY_A_SIBLING_RECORD[owner], getLocalName(child.name)))) return;
433
439
  return formatting;
434
440
  }
435
441
  function parseRunPropertyChanges(rPr, theme, currentFormatting) {
@@ -443,7 +449,7 @@ function parseRunPropertyChanges(rPr, theme, currentFormatting) {
443
449
  const boundaryJoins = parseBoundaryJoins(changeElement);
444
450
  if (boundaryJoins !== void 0) change.boundaryJoins = boundaryJoins;
445
451
  const previousFormatting = parseRunProperties(previousRPr, theme, RUN_PROPERTY_OWNERS.standalone);
446
- if (previousFormatting) change.previousFormatting = previousFormatting;
452
+ if (previousFormatting && (!omitsPreviousRunProperties(changeElement) || Object.keys(previousFormatting).length > 0)) change.previousFormatting = previousFormatting;
447
453
  if (currentFormatting) change.currentFormatting = currentFormatting;
448
454
  return change;
449
455
  });
@@ -789,7 +795,7 @@ function parseRun(node, _styles, theme, rels = null, media = null, rootXmlns = {
789
795
  };
790
796
  const rPr = findChild(node, "w", "rPr");
791
797
  if (rPr) {
792
- const formattingResult = parseRunProperties(rPr, theme, RUN_PROPERTY_OWNERS.run);
798
+ const formattingResult = parseRunProperties(rPr, theme, RUN_PROPERTY_OWNERS.run) ?? (retainsEmptyRunProperties(node) ? {} : void 0);
793
799
  if (formattingResult) run.formatting = formattingResult;
794
800
  const propertyChangesResult = parseRunPropertyChanges(rPr, theme, run.formatting);
795
801
  if (propertyChangesResult) run.propertyChanges = propertyChangesResult;
@@ -798,8 +804,8 @@ function parseRun(node, _styles, theme, rels = null, media = null, rootXmlns = {
798
804
  const remainder = attributeRemainder({
799
805
  element: node,
800
806
  modelled: NO_MODELLED_ATTRIBUTES
801
- });
802
- if (remainder) run.preservedAttributes = remainder;
807
+ })?.filter((attribute) => !isEmptyRunPropertiesAttribute(attribute));
808
+ if (remainder?.length) run.preservedAttributes = remainder;
803
809
  return run;
804
810
  }
805
811
  /**
@@ -0,0 +1,11 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+ import { XmlElement } from "./xmlParser.js";
3
+ //#region src/docx/runPropertyPresence.d.ts
4
+ declare const EMPTY_RUN_PROPERTIES_ATTRIBUTE = "emptyRunProperties";
5
+ declare const retainsEmptyRunProperties: (element: XmlElement) => boolean;
6
+ declare const omitsPreviousRunProperties: (element: XmlElement) => boolean;
7
+ declare const serializePreviousRunProperties: (properties: document_d_exports.TextFormatting | undefined) => string;
8
+ declare const serializeEmptyRunProperties: (run: document_d_exports.Run) => string[];
9
+ declare const isEmptyRunPropertiesAttribute: ({ namespace, name }: Pick<document_d_exports.PreservedAttribute, "namespace" | "name">) => boolean;
10
+ //#endregion
11
+ export { EMPTY_RUN_PROPERTIES_ATTRIBUTE, isEmptyRunPropertiesAttribute, omitsPreviousRunProperties, retainsEmptyRunProperties, serializeEmptyRunProperties, serializePreviousRunProperties };
@@ -0,0 +1,14 @@
1
+ import { OOXML_NAMESPACES } from "./serializer/partNamespaces.js";
2
+ import { getAttributeByNamespaceUri } from "./xmlParser.js";
3
+ import { EMPTY_PROPERTY_SETS } from "@stll/docx-core/ops";
4
+ //#region src/docx/runPropertyPresence.ts
5
+ const EMPTY_RUN_PROPERTIES_ATTRIBUTE = "emptyRunProperties";
6
+ const PREVIOUS_PROPERTIES_ATTRIBUTE = "previousProperties";
7
+ const HISTORY_NAMESPACES = /* @__PURE__ */ new Set([OOXML_NAMESPACES.folio.uri]);
8
+ const retainsEmptyRunProperties = (element) => getAttributeByNamespaceUri(element, HISTORY_NAMESPACES, EMPTY_RUN_PROPERTIES_ATTRIBUTE) === EMPTY_PROPERTY_SETS.KEEP;
9
+ const omitsPreviousRunProperties = (element) => getAttributeByNamespaceUri(element, HISTORY_NAMESPACES, PREVIOUS_PROPERTIES_ATTRIBUTE) === EMPTY_PROPERTY_SETS.OMIT;
10
+ const serializePreviousRunProperties = (properties) => properties === void 0 ? ` folio:${PREVIOUS_PROPERTIES_ATTRIBUTE}="${EMPTY_PROPERTY_SETS.OMIT}"` : "";
11
+ const serializeEmptyRunProperties = (run) => run.propertyChanges?.length && run.formatting !== void 0 && Object.keys(run.formatting).length === 0 ? [`folio:${EMPTY_RUN_PROPERTIES_ATTRIBUTE}="${EMPTY_PROPERTY_SETS.KEEP}"`] : [];
12
+ const isEmptyRunPropertiesAttribute = ({ namespace, name }) => namespace === OOXML_NAMESPACES.folio.uri && name === "emptyRunProperties";
13
+ //#endregion
14
+ export { EMPTY_RUN_PROPERTIES_ATTRIBUTE, isEmptyRunPropertiesAttribute, omitsPreviousRunProperties, retainsEmptyRunProperties, serializeEmptyRunProperties, serializePreviousRunProperties };
@@ -0,0 +1,10 @@
1
+ import { scanStreamingXmlElements } from "./streamingXmlParser.js";
2
+ //#region src/docx/selectedXmlIdentityScan.d.ts
3
+ /** Identity census follows the parser's first-Choice, otherwise-Fallback policy. */
4
+ declare const scanSelectedStreamingXmlElements: (xml: string, visitor: Parameters<typeof scanStreamingXmlElements>[1]) => {
5
+ status: "unsupported";
6
+ } | {
7
+ status: "scanned";
8
+ };
9
+ //#endregion
10
+ export { scanSelectedStreamingXmlElements };
@@ -0,0 +1,36 @@
1
+ import { scanStreamingXmlElements } from "./streamingXmlParser.js";
2
+ import { NAMESPACES, getLocalName, getNamespaceUri, selectAlternateContentBranch } from "./xmlParser.js";
3
+ //#region src/docx/selectedXmlIdentityScan.ts
4
+ const isCompatibilityElement = (element, name) => getNamespaceUri(element) === NAMESPACES.mc && getLocalName(element.name) === name;
5
+ /** Identity census follows the parser's first-Choice, otherwise-Fallback policy. */
6
+ const scanSelectedStreamingXmlElements = (xml, visitor) => {
7
+ if (!xml.includes("AlternateContent")) return scanStreamingXmlElements(xml, visitor);
8
+ const alternatives = /* @__PURE__ */ new Map();
9
+ const positions = /* @__PURE__ */ new WeakMap();
10
+ const scanned = scanStreamingXmlElements(xml, ({ element, parent, nameEnd }) => {
11
+ positions.set(element, nameEnd);
12
+ if (isCompatibilityElement(element, "AlternateContent")) {
13
+ element.elements = [];
14
+ alternatives.set(nameEnd, element);
15
+ }
16
+ if (!parent || !isCompatibilityElement(parent, "AlternateContent")) return;
17
+ parent.elements?.push(element);
18
+ });
19
+ if (scanned.status === "unsupported") return scanned;
20
+ const selectedPositions = /* @__PURE__ */ new Map();
21
+ for (const [position, alternative] of alternatives) {
22
+ const selected = selectAlternateContentBranch(alternative);
23
+ selectedPositions.set(position, selected === void 0 ? void 0 : positions.get(selected));
24
+ }
25
+ const active = /* @__PURE__ */ new WeakMap();
26
+ return scanStreamingXmlElements(xml, (entry) => {
27
+ const { element, parent, nameEnd } = entry;
28
+ positions.set(element, nameEnd);
29
+ let included = parent === void 0 || active.get(parent) === true;
30
+ if (parent && isCompatibilityElement(parent, "AlternateContent")) included &&= nameEnd === selectedPositions.get(positions.get(parent) ?? -1);
31
+ active.set(element, included);
32
+ if (included) visitor(entry);
33
+ });
34
+ };
35
+ //#endregion
36
+ export { scanSelectedStreamingXmlElements };
@@ -1,3 +1,4 @@
1
+ import { decodeText, encodeUtf8 } from "../utils/textEncoding.js";
1
2
  import { parseCommentsExtended } from "./commentParser.js";
2
3
  import { withoutOrphanCommentRanges } from "./commentRangeIntegrity.js";
3
4
  import { hasUnsynthesizedReplyRanges } from "./commentReplyMarkers.js";
@@ -10,7 +11,7 @@ import { RELATIONSHIP_TYPES } from "./relsParser.js";
10
11
  import { COMMENTS_CONTENT_TYPE, COMMENTS_EXTENDED_PART_LOWER, addCommentsExtendedOverride, addCommentsExtendedRelationship, applyUpdatesToZip, collectHeaderFooterUpdates, findMaxRId, hasModelDrivenPictureWatermark, hasUnmaterializedHeaderFooter, hasUnmaterializedHyperlinkBindings, hasUnmaterializedInlineResources, planAddedStyles, publishCanonicalImageResources, updateCoreProperties, withoutAttachedTemplate } from "./rezip.js";
11
12
  import "./selectiveSaveFlags.js";
12
13
  import { buildPatchedDocumentXml, buildPatchedNoteXml, collectAddedNumberingDefs, collectParaIds, patchNumberingDefinitions } from "./selectiveXmlPatch.js";
13
- import { hasOwnedCommentsPart, planCommentParts, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
14
+ import { canReplayCommentsPart, hasOwnedCommentsPart, planCommentParts, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
14
15
  import { serializeDocument } from "./serializer/documentSerializer.js";
15
16
  import { serializeEndnotes, serializeFootnotes } from "./serializer/noteSerializer.js";
16
17
  import { serializeNumberingXml } from "./serializer/numberingSerializer.js";
@@ -46,7 +47,7 @@ async function patchNoteParts(zip, doc, candidateIds, updates) {
46
47
  const patchPart = async (conventionalLowerPath, serialize) => {
47
48
  const file = findPart(conventionalLowerPath);
48
49
  if (!file) return true;
49
- const originalXml = await file.async("text");
50
+ const originalXml = await file.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true }));
50
51
  const originalIds = collectParaIds(originalXml);
51
52
  const partIds = /* @__PURE__ */ new Set();
52
53
  for (const id of remainingIds) if (originalIds.has(id)) partIds.add(id);
@@ -93,7 +94,7 @@ async function patchCommentsExtended(zip, plan, updates) {
93
94
  const desiredXml = serializeCommentsExtended(plan);
94
95
  const existing = findZipEntryCaseInsensitive(zip, COMMENTS_EXTENDED_PART_LOWER);
95
96
  if (!desiredXml) return existing === null;
96
- const baselineInfo = existing ? parseCommentsExtended(await existing.async("text")) : /* @__PURE__ */ new Map();
97
+ const baselineInfo = existing ? parseCommentsExtended(await existing.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true }))) : /* @__PURE__ */ new Map();
97
98
  if (commentsExtendedInfoEqual(parseCommentsExtended(desiredXml), baselineInfo)) return true;
98
99
  updates.set(existing?.name ?? "word/commentsExtended.xml", desiredXml);
99
100
  return ensureCommentsExtendedPackaging(zip, updates);
@@ -106,12 +107,12 @@ async function patchCommentsExtended(zip, plan, updates) {
106
107
  */
107
108
  async function ensureCommentsExtendedPackaging(zip, updates) {
108
109
  const ctPath = "[Content_Types].xml";
109
- const ctXml = updates.get(ctPath) ?? await zip.file(ctPath)?.async("text");
110
+ const ctXml = updates.get(ctPath) ?? await zip.file(ctPath)?.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true }));
110
111
  if (ctXml === void 0) return false;
111
112
  const nextCt = addCommentsExtendedOverride(ctXml);
112
113
  if (nextCt !== ctXml) updates.set(ctPath, nextCt);
113
114
  const relsPath = "word/_rels/document.xml.rels";
114
- const relsXml = updates.get(relsPath) ?? await zip.file(relsPath)?.async("text");
115
+ const relsXml = updates.get(relsPath) ?? await zip.file(relsPath)?.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true }));
115
116
  if (relsXml === void 0) return false;
116
117
  const nextRels = addCommentsExtendedRelationship(relsXml);
117
118
  if (nextRels !== relsXml) updates.set(relsPath, nextRels);
@@ -146,7 +147,7 @@ async function patchNumberingPart(zip, doc, updates) {
146
147
  }
147
148
  }
148
149
  if (!file) return false;
149
- const originalXml = await file.async("text");
150
+ const originalXml = await file.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true }));
150
151
  const baselineXml = serializeNumberingXml(parseNumbering(originalXml).definitions);
151
152
  const currentXml = serializeNumberingXml(numbering);
152
153
  if (currentXml === baselineXml) return true;
@@ -177,7 +178,7 @@ const queueSettingsUpdates = async (zip, updates) => {
177
178
  const settingsFile = zip.file("word/settings.xml");
178
179
  if (!settingsFile) return;
179
180
  const relsFile = zip.file("word/_rels/settings.xml.rels");
180
- const filtered = withoutAttachedTemplate(await settingsFile.async("text"), await relsFile?.async("text"));
181
+ const filtered = withoutAttachedTemplate(await settingsFile.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true })), await relsFile?.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true })));
181
182
  if (filtered.settingsXml !== void 0) updates.set("word/settings.xml", filtered.settingsXml);
182
183
  if (filtered.relsXml !== void 0) updates.set("word/_rels/settings.xml.rels", filtered.relsXml);
183
184
  };
@@ -212,12 +213,12 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
212
213
  for (const [path, file] of Object.entries(zip.files)) {
213
214
  const lowerPath = path.toLowerCase();
214
215
  if (file.dir || !lowerPath.startsWith("word/") || !lowerPath.endsWith(".xml")) continue;
215
- sourceParts.set(path, await file.async("text"));
216
+ sourceParts.set(path, await file.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true })));
216
217
  }
217
218
  for (const [path, xml] of normalizeImportedNumericIds(sourceParts)) if (xml !== sourceParts.get(path)) writeZipPart({
218
219
  zip,
219
220
  path,
220
- data: xml
221
+ data: encodeUtf8(xml)
221
222
  });
222
223
  }
223
224
  if (options.bodyAuthority === "canonical") await publishCanonicalImageResources({
@@ -229,7 +230,7 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
229
230
  if (changedParaIds.size > 0 || structuralChange) {
230
231
  const docXmlFile = zip.file("word/document.xml");
231
232
  if (!docXmlFile) return null;
232
- const originalDocXml = await docXmlFile.async("text");
233
+ const originalDocXml = await docXmlFile.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true }));
233
234
  const serializedDocXml = serializeDocument(doc, readRootNamespaceBindings(originalDocXml), {
234
235
  xml: originalDocXml,
235
236
  bodyAuthority: options.bodyAuthority,
@@ -239,7 +240,7 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
239
240
  const originalBodyParaIds = structuralChange ? collectParaIds(originalDocXml) : void 0;
240
241
  if (structuralChange && doc.package.numbering) {
241
242
  const numberingFile = findZipEntryCaseInsensitive(zip, "word/numbering.xml");
242
- const added = collectAddedNumberingDefs(numberingFile ? serializeNumberingXml(parseNumbering(await numberingFile.async("text")).definitions) : "", serializeNumberingXml(doc.package.numbering));
243
+ const added = collectAddedNumberingDefs(numberingFile ? serializeNumberingXml(parseNumbering(await numberingFile.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true }))).definitions) : "", serializeNumberingXml(doc.package.numbering));
243
244
  if (added.abstractNums.size > 0 || added.nums.size > 0) return null;
244
245
  }
245
246
  const bodyChangedIds = /* @__PURE__ */ new Set();
@@ -262,28 +263,29 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
262
263
  }
263
264
  const sourceCommentsFile = findZipEntryCaseInsensitive(zip, "word/comments.xml");
264
265
  const commentsPartPath = sourceCommentsFile?.name ?? "word/comments.xml";
265
- if (hasComments || ownsCommentsPart || sourceCommentsFile) {
266
- const sourceCommentsXml = sourceCommentsFile ? await sourceCommentsFile.async("text") : void 0;
266
+ const sourceCommentsXml = sourceCommentsFile ? await sourceCommentsFile.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true })) : void 0;
267
+ const commentsUnchanged = sourceCommentsXml !== void 0 && canReplayCommentsPart(doc, sourceCommentsXml);
268
+ if (!commentsUnchanged && (hasComments || ownsCommentsPart || sourceCommentsFile)) {
267
269
  const commentsXml = serializeComments(commentPlan, sourceCommentsXml === void 0 ? void 0 : readRootNamespaceBindings(sourceCommentsXml));
268
270
  if (commentsXml !== sourceCommentsXml) updates.set(commentsPartPath, commentsXml);
269
271
  }
270
272
  if (hasComments || ownsCommentsPart) {
271
273
  const ctFile = zip.file("[Content_Types].xml");
272
274
  if (ctFile) {
273
- const ctXml = await ctFile.async("text");
275
+ const ctXml = await ctFile.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true }));
274
276
  if (!ctXml.toLowerCase().includes("/word/comments.xml")) updates.set("[Content_Types].xml", ctXml.replace("</Types>", `<Override PartName="/${commentsPartPath}" ContentType="${COMMENTS_CONTENT_TYPE}"/></Types>`));
275
277
  }
276
278
  const relsPath = "word/_rels/document.xml.rels";
277
279
  const relsFile = zip.file(relsPath);
278
280
  if (relsFile) {
279
- const relsXml = await relsFile.async("text");
281
+ const relsXml = await relsFile.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true }));
280
282
  if (!relsXml.toLowerCase().includes("comments.xml")) {
281
283
  const maxId = findMaxRId(relsXml);
282
284
  updates.set(relsPath, relsXml.replace("</Relationships>", `<Relationship Id="rId${maxId + 1}" Type="${RELATIONSHIP_TYPES.comments}" Target="${commentsPartPath.slice(5)}"/></Relationships>`));
283
285
  }
284
286
  }
285
287
  }
286
- if (!await patchCommentsExtended(zip, commentPlan, updates)) return null;
288
+ if (!commentsUnchanged && !await patchCommentsExtended(zip, commentPlan, updates)) return null;
287
289
  if (!await patchNumberingPart(zip, doc, updates)) return null;
288
290
  const styles = await planAddedStyles(doc, zip);
289
291
  switch (styles.type) {
@@ -306,7 +308,7 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
306
308
  await queueSettingsUpdates(zip, updates);
307
309
  const corePropsFile = zip.file("docProps/core.xml");
308
310
  if (corePropsFile) {
309
- const corePropsXml = await corePropsFile.async("text");
311
+ const corePropsXml = await corePropsFile.async("uint8array").then((bytes) => decodeText(bytes, { ignoreBOM: true }));
310
312
  updates.set("docProps/core.xml", updateCoreProperties(corePropsXml, { updateModifiedDate: true }));
311
313
  }
312
314
  if ([...updates.keys()].some((path) => {
@@ -1,6 +1,7 @@
1
1
  import { patchBreaksCommentRangeBalance } from "./commentRangeIntegrity.js";
2
2
  import { resolveParagraphIdentities } from "./paraIdAttribute.js";
3
3
  import { readRootNamespaceBindings } from "./serializer/partNamespaces.js";
4
+ import { scanStreamingXmlElements } from "./streamingXmlParser.js";
4
5
  import { captureVerbatimXml } from "./verbatimCapture.js";
5
6
  import { PARAGRAPH_SCAN_NAMES, hasCanonicalWordprocessingPrefixes, matchCloseTag, matchOpenTag, resolveSelectiveScanPrefixes, splicesAsCanonical } from "./wordprocessingPrefixes.js";
6
7
  import { resolveNamespaceUri } from "./xmlNamespaceContext.js";
@@ -650,36 +651,32 @@ function extractElementByIdAttr(xml, openLiteral, closeTag, idAttr, id) {
650
651
  const qualifiedName = (prefix, localName) => prefix.length === 0 ? localName : `${prefix}:${localName}`;
651
652
  const collectNoteElementSyntax = (xml, elementName) => {
652
653
  const byId = /* @__PURE__ */ new Map();
653
- const root = parseXmlDocument(xml);
654
- for (const element of getChildElements(root)) {
655
- if (getLocalName(element.name) !== elementName || !WORDPROCESSINGML_NAMESPACE_URIS.has(getNamespaceUri(element) ?? "")) continue;
654
+ let root;
655
+ scanStreamingXmlElements(xml, ({ element, parent, nameEnd }) => {
656
+ if (parent === void 0) {
657
+ root = element;
658
+ return;
659
+ }
660
+ if (parent !== root || getLocalName(element.name) !== elementName || !WORDPROCESSINGML_NAMESPACE_URIS.has(getNamespaceUri(element) ?? "")) return;
656
661
  const idAttribute = findAttributeByNamespaceUri(element, WORDPROCESSINGML_NAMESPACE_URIS, "id");
657
- if (!element.name || !idAttribute) continue;
662
+ if (!element.name || !idAttribute) return;
663
+ const spelling = idAttribute.value.trim();
664
+ if (!/^[+-]?\d+$/u.test(spelling) || !Number.isSafeInteger(Number(spelling))) return;
665
+ const id = String(Number(spelling));
666
+ const offsets = scanElementRange(xml, nameEnd - element.name.length - 1, `<${element.name}`, `</${element.name}>`);
667
+ if (!offsets) return;
658
668
  const syntax = {
659
- elementName: element.name,
660
- idAttributeName: idAttribute.name,
669
+ offsets,
661
670
  elementPrefix: getNamespacePrefix(element.name) ?? "",
662
671
  attributePrefix: getNamespacePrefix(idAttribute.name) ?? ""
663
672
  };
664
- const entries = byId.get(idAttribute.value);
673
+ const entries = byId.get(id);
665
674
  if (entries) entries.push(syntax);
666
- else byId.set(idAttribute.value, [syntax]);
667
- }
675
+ else byId.set(id, [syntax]);
676
+ });
668
677
  return byId;
669
678
  };
670
- const syntaxLiteral = (syntax) => ({
671
- openLiteral: `<${syntax.elementName}`,
672
- closeTag: `</${syntax.elementName}>`,
673
- idAttr: syntax.idAttributeName
674
- });
675
- const extractNoteElement = (xml, syntax, id) => {
676
- const { openLiteral, closeTag, idAttr } = syntaxLiteral(syntax);
677
- return extractElementByIdAttr(xml, openLiteral, closeTag, idAttr, id);
678
- };
679
- const findNoteElement = (xml, syntax, id) => {
680
- const { openLiteral, closeTag, idAttr } = syntaxLiteral(syntax);
681
- return findElementByIdAttr(xml, openLiteral, closeTag, idAttr, id);
682
- };
679
+ const extractNoteElement = (xml, syntax) => xml.slice(syntax.offsets.start, syntax.offsets.end);
683
680
  const rewriteWordprocessingPrefixes = (xml, { source, target, sourceXmlnsDeclarations }) => {
684
681
  let rewritten = "";
685
682
  let quote = null;
@@ -824,7 +821,7 @@ function buildPatchedNotePartXml({ originalXml, baselineXml, serializedXml, repl
824
821
  const replacementSyntaxEntries = replacementElements.get(id);
825
822
  if (originalSyntaxEntries === void 0) {
826
823
  const replacementSyntax = currentSyntaxEntries.length === 1 && replacementSyntaxEntries?.length === 1 ? replacementSyntaxEntries[0] : void 0;
827
- const replacementNote = replacementSyntax ? extractNoteElement(replacementXml, replacementSyntax, id) : null;
824
+ const replacementNote = replacementSyntax ? extractNoteElement(replacementXml, replacementSyntax) : null;
828
825
  if (!replacementSyntax || !replacementNote) return {
829
826
  type: "refused",
830
827
  reason: "unroutable-paragraph"
@@ -836,24 +833,20 @@ function buildPatchedNotePartXml({ originalXml, baselineXml, serializedXml, repl
836
833
  });
837
834
  continue;
838
835
  }
839
- if (currentSyntaxEntries.length !== 1 || originalSyntaxEntries?.length !== 1 || replacementSyntaxEntries?.length !== 1) return {
836
+ if (currentSyntaxEntries.length !== 1 || !originalSyntaxEntries?.length || replacementSyntaxEntries?.length !== 1) return {
840
837
  type: "refused",
841
838
  reason: "unroutable-paragraph"
842
839
  };
843
840
  const currentSyntax = currentSyntaxEntries[0];
844
- const originalSyntax = originalSyntaxEntries[0];
841
+ const originalSyntax = originalSyntaxEntries.at(0);
845
842
  const replacementSyntax = replacementSyntaxEntries[0];
846
843
  if (!currentSyntax || !originalSyntax || !replacementSyntax) return {
847
844
  type: "refused",
848
845
  reason: "unroutable-paragraph"
849
846
  };
850
- const currentNote = extractNoteElement(serializedXml, currentSyntax, id);
851
- const originalOffsets = findNoteElement(originalXml, originalSyntax, id);
852
- const replacementNote = extractNoteElement(replacementXml, replacementSyntax, id);
853
- if (!currentNote || !originalOffsets || !replacementNote) return {
854
- type: "refused",
855
- reason: "unroutable-paragraph"
856
- };
847
+ const currentNote = extractNoteElement(serializedXml, currentSyntax);
848
+ const originalOffsets = originalSyntax.offsets;
849
+ const replacementNote = extractNoteElement(replacementXml, replacementSyntax);
857
850
  const originalNote = originalXml.slice(originalOffsets.start, originalOffsets.end);
858
851
  const currentParagraphs = paragraphRanges(currentNote, currentSyntax.elementPrefix);
859
852
  const originalParagraphs = paragraphRanges(originalNote, originalSyntax.elementPrefix);
@@ -861,7 +854,7 @@ function buildPatchedNotePartXml({ originalXml, baselineXml, serializedXml, repl
861
854
  const noteChangedParaIds = [...collectParaIds(currentNote).keys()].filter((paraId) => unroutedChangedParaIds.has(paraId));
862
855
  const baselineSyntaxEntries = baselineElements.get(id);
863
856
  const baselineSyntax = baselineSyntaxEntries?.length === 1 ? baselineSyntaxEntries[0] : void 0;
864
- const baselineNote = baselineSyntax ? extractNoteElement(baselineXml, baselineSyntax, id) : null;
857
+ const baselineNote = baselineSyntax ? extractNoteElement(baselineXml, baselineSyntax) : null;
865
858
  const reshaped = baselineSyntax !== void 0 && baselineNote !== null && baselineNote !== currentNote && noteShapeChanged({
866
859
  note: baselineNote,
867
860
  prefix: baselineSyntax.elementPrefix
@@ -910,26 +903,29 @@ function buildPatchedNotePartXml({ originalXml, baselineXml, serializedXml, repl
910
903
  for (const [id, baselineSyntaxEntries] of baselineElements) {
911
904
  if (currentElements.has(id) || baselineSyntaxEntries.length !== 1) continue;
912
905
  const originalSyntaxEntries = originalElements.get(id);
913
- const originalSyntax = originalSyntaxEntries?.length === 1 ? originalSyntaxEntries[0] : void 0;
914
- const originalOffsets = originalSyntax && findNoteElement(originalXml, originalSyntax, id);
915
- if (!originalOffsets) return {
906
+ if (!originalSyntaxEntries?.length) return {
916
907
  type: "refused",
917
908
  reason: "unroutable-paragraph"
918
909
  };
919
- const removal = {
920
- start: originalOffsets.start,
921
- end: originalOffsets.end,
922
- newXml: ""
923
- };
924
- paragraphSplices.push(removal);
925
- noteSplices.push(removal);
910
+ for (const { offsets } of originalSyntaxEntries) {
911
+ const removal = {
912
+ ...offsets,
913
+ newXml: ""
914
+ };
915
+ paragraphSplices.push(removal);
916
+ noteSplices.push(removal);
917
+ }
926
918
  }
927
919
  if (unroutedChangedParaIds.size > 0) return {
928
920
  type: "refused",
929
921
  reason: "unroutable-paragraph"
930
922
  };
931
923
  if (addedNotes.length > 0) {
932
- const appended = appendNotesSplice(originalXml, originalElements, addedNotes, { sourceXmlnsDeclarations: replacementXmlnsDeclarations });
924
+ const appended = appendNotesSplice({
925
+ originalElements,
926
+ addedNotes,
927
+ sourceXmlnsDeclarations: replacementXmlnsDeclarations
928
+ });
933
929
  if (!appended) return {
934
930
  type: "refused",
935
931
  reason: "unroutable-paragraph"
@@ -951,15 +947,11 @@ function buildPatchedNotePartXml({ originalXml, baselineXml, serializedXml, repl
951
947
  xml: wholeNotes
952
948
  };
953
949
  }
954
- /**
955
- * Insert new notes after the part's last note, spelled with that note's
956
- * prefixes. Null when the part holds no note to follow.
957
- */
958
- const appendNotesSplice = (originalXml, originalElements, addedNotes, { sourceXmlnsDeclarations }) => {
950
+ const appendNotesSplice = ({ originalElements, addedNotes, sourceXmlnsDeclarations }) => {
959
951
  let last = null;
960
- for (const [id, entries] of originalElements) for (const syntax of entries) {
961
- const offsets = findNoteElement(originalXml, syntax, id);
962
- if (offsets && (!last || offsets.end > last.end)) last = {
952
+ for (const entries of originalElements.values()) for (const syntax of entries) {
953
+ const offsets = syntax.offsets;
954
+ if (!last || offsets.end > last.end) last = {
963
955
  end: offsets.end,
964
956
  syntax
965
957
  };
@@ -1,6 +1,8 @@
1
1
  import { document_d_exports } from "../../types/document.js";
2
2
  import { Comment } from "../../types/content.js";
3
3
  //#region src/docx/serializer/commentSerializer.d.ts
4
+ /** An unrelated body edit must retain unmodelled duplicate comment definitions. */
5
+ declare const canReplayCommentsPart: (document: document_d_exports.Document, sourceXml: string) => boolean;
4
6
  /** An owned empty part survives deletion of its final thread. */
5
7
  declare const hasOwnedCommentsPart: (document: document_d_exports.Document) => boolean;
6
8
  /**
@@ -55,4 +57,4 @@ declare function serializeComments({ order, byId }: CommentPartPlan, sourceBindi
55
57
  */
56
58
  declare function serializeCommentsExtended(plan: CommentPartPlan): string | null;
57
59
  //#endregion
58
- export { CommentPartPlan, hasOwnedCommentsPart, planCommentParts, serializeComments, serializeCommentsExtended };
60
+ export { CommentPartPlan, canReplayCommentsPart, hasOwnedCommentsPart, planCommentParts, serializeComments, serializeCommentsExtended };
@@ -1,10 +1,13 @@
1
+ import { canonicalJson } from "../../utils/canonicalJson.js";
1
2
  import { deterministicHexId } from "../../utils/hexId.js";
2
3
  import { commentThreadParaId } from "../commentThreadKey.js";
3
4
  import { serializeWithPreservedChildren } from "../containerChildren.js";
5
+ import { getDocumentSourceBaseline } from "../headerFooterVerbatim.js";
4
6
  import { serializeParagraph } from "./paragraphSerializer.js";
5
7
  import { serializePartElement } from "./partNamespaces.js";
6
8
  import { serializeTextFormatting } from "./textFormattingSerializer.js";
7
- import { escapeXmlAttribute } from "@stll/docx-core";
9
+ import { Result } from "better-result";
10
+ import { assertValidOoxmlNumericIds, escapeXmlAttribute } from "@stll/docx-core";
8
11
  import { COMMENT_PART_RELATIONSHIPS } from "@stll/docx-core/model";
9
12
  //#region src/docx/serializer/commentSerializer.ts
10
13
  /**
@@ -12,6 +15,12 @@ import { COMMENT_PART_RELATIONSHIPS } from "@stll/docx-core/model";
12
15
  *
13
16
  * Serializes Comment[] to OOXML comments.xml format.
14
17
  */
18
+ /** An unrelated body edit must retain unmodelled duplicate comment definitions. */
19
+ const canReplayCommentsPart = (document, sourceXml) => {
20
+ if (Result.try(() => assertValidOoxmlNumericIds(sourceXml, "word/comments.xml")).isErr()) return false;
21
+ const baseline = getDocumentSourceBaseline(document);
22
+ return baseline.type === "captured" && canonicalJson(document.package.document.comments ?? []) === canonicalJson(baseline.body.comments ?? []);
23
+ };
15
24
  /** An owned empty part survives deletion of its final thread. */
16
25
  const hasOwnedCommentsPart = (document) => [...document.package.relationships?.values() ?? []].some(({ type }) => type === COMMENT_PART_RELATIONSHIPS.comments.type);
17
26
  const DEFAULT_ANNOTATION_REFERENCE_PROPERTIES = "<w:rPr><w:rStyle w:val=\"CommentReference\"/></w:rPr>";
@@ -200,4 +209,4 @@ function serializeCommentsExtended(plan) {
200
209
  });
201
210
  }
202
211
  //#endregion
203
- export { hasOwnedCommentsPart, planCommentParts, serializeComments, serializeCommentsExtended };
212
+ export { canReplayCommentsPart, hasOwnedCommentsPart, planCommentParts, serializeComments, serializeCommentsExtended };
@@ -9,16 +9,17 @@ import { canReplayEditableImageRawXml } from "../imageRawXml.js";
9
9
  import { serializeNonVisualDrawingNames } from "../nonVisualDrawingProps.js";
10
10
  import { serializeBoundaryJoins } from "../reviewResolutionProvenance.js";
11
11
  import { runHoldsPayload } from "../runPayload.js";
12
+ import { serializeEmptyRunProperties, serializePreviousRunProperties } from "../runPropertyPresence.js";
12
13
  import { replayableShapeAlternateContent } from "../shapeAlternateContent.js";
13
14
  import { requiredWrapPolygon, serializeWrapPolygon } from "../wrapPolygon.js";
14
15
  import { serializeParagraph } from "./paragraphSerializer.js";
15
16
  import { serializeTable } from "./tableSerializer.js";
16
17
  import { serializeTextFormatting } from "./textFormattingSerializer.js";
17
- import { getSingularRunPropertyChange, serializeTrackedChangeAttributes } from "./trackedChangeAttributes.js";
18
+ import { serializeTrackedChangeAttributes } from "./trackedChangeAttributes.js";
18
19
  import { intAttr } from "./xmlUtils.js";
19
20
  import { panic } from "better-result";
20
21
  import { escapeXmlAttribute, escapeXmlText, isValidOoxmlNumericId, requiresXmlSpacePreserve } from "@stll/docx-core";
21
- import { SCHEME_COLOR_VALUE_BY_THEME_COLOR, knownThemeColor, presetLineDashToken } from "@stll/docx-core/model";
22
+ import { SCHEME_COLOR_VALUE_BY_THEME_COLOR, getSingularRunPropertyChange, knownThemeColor, presetLineDashToken } from "@stll/docx-core/model";
22
23
  //#region src/docx/serializer/runSerializer.ts
23
24
  /**
24
25
  * Run Serializer - Serialize runs to OOXML XML
@@ -52,7 +53,7 @@ function getUniqueId(id) {
52
53
  }
53
54
  function serializeRunPropertyChange(change) {
54
55
  const previousRPrXml = serializeTextFormatting(change.previousFormatting) || "<w:rPr/>";
55
- return `<w:rPrChange ${serializeTrackedChangeAttributes(change.info)}${serializeBoundaryJoins(change.boundaryJoins)}>${previousRPrXml}</w:rPrChange>`;
56
+ return `<w:rPrChange ${serializeTrackedChangeAttributes(change.info)}${serializeBoundaryJoins(change.boundaryJoins)}${serializePreviousRunProperties(change.previousFormatting)}>${previousRPrXml}</w:rPrChange>`;
56
57
  }
57
58
  function serializeRunProperties(formatting, propertyChanges) {
58
59
  const propertyChange = getSingularRunPropertyChange(propertyChanges);
@@ -567,7 +568,7 @@ function serializeRun(run) {
567
568
  const contentXml = serializeRunContent(content);
568
569
  if (contentXml) parts.push(contentXml);
569
570
  }
570
- const attrs = serializePreservedAttributes([], run.preservedAttributes);
571
+ const attrs = serializePreservedAttributes(serializeEmptyRunProperties(run), run.preservedAttributes);
571
572
  return `<w:r${attrs.length > 0 ? ` ${attrs.join(" ")}` : ""}>${parts.join("")}</w:r>`;
572
573
  }
573
574
  /**
@@ -5,6 +5,7 @@ import { withPreservedChildren } from "../containerChildren.js";
5
5
  import { TRANSITIONAL_NAME_BY_STRICT_NAME } from "../strictNames.gen.js";
6
6
  import { TABLE_LOOK_FLAGS } from "../tableLook.js";
7
7
  import { parseTableCellProperties, parseTableGrid, parseTableProperties, parseTablePropertyExceptions, parseTableRowProperties } from "../tableParser.js";
8
+ import { serializeEmptyRowProperties } from "../tableRowPropertyPresence.js";
8
9
  import { OOXML_NAMESPACE_SCOPE, parseXml } from "../xmlParser.js";
9
10
  import { serializeBlockCustomXml } from "./blockCustomXmlSerializer.js";
10
11
  import { serializeBlockSdt } from "./blockSdtSerializer.js";
@@ -555,7 +556,7 @@ function serializeTableRow(row, serializeParagraph) {
555
556
  carriers: carriersFor(cell),
556
557
  xml: serializeTableCell(cell, serializeParagraph)
557
558
  })), positionedChildren(row.preserved, row.bookmarks), preservedChild), 0));
558
- const attrs = serializePreservedAttributes([], row.preservedAttributes);
559
+ const attrs = serializePreservedAttributes(serializeEmptyRowProperties(row), row.preservedAttributes);
559
560
  return `<w:tr${attrs.length > 0 ? ` ${attrs.join(" ")}` : ""}>${parts.join("")}</w:tr>`;
560
561
  }
561
562
  /**
@@ -1,11 +1,9 @@
1
1
  import { document_d_exports } from "../../types/document.js";
2
2
  //#region src/docx/serializer/trackedChangeAttributes.d.ts
3
3
  type SerializableTrackedChangeInfo = document_d_exports.TrackedChangeInfo | document_d_exports.PropertyChangeInfo;
4
- /** Enforces the singular `w:rPrChange` child in a run-property container. */
5
- declare const getSingularRunPropertyChange: (propertyChanges: readonly document_d_exports.RunPropertyChange[] | undefined) => document_d_exports.RunPropertyChange | undefined;
6
4
  /** Normalized, unescaped attributes in schema order for an XML element or string serializer. */
7
5
  declare const trackedChangeAttributeEntries: (info: SerializableTrackedChangeInfo) => readonly (readonly [string, string])[];
8
6
  declare const trackedChangeAttributeRecord: (info: SerializableTrackedChangeInfo) => Record<string, string>;
9
7
  declare const serializeTrackedChangeAttributes: (info: SerializableTrackedChangeInfo) => string;
10
8
  //#endregion
11
- export { getSingularRunPropertyChange, serializeTrackedChangeAttributes, trackedChangeAttributeEntries, trackedChangeAttributeRecord };
9
+ export { serializeTrackedChangeAttributes, trackedChangeAttributeEntries, trackedChangeAttributeRecord };
@@ -1,17 +1,7 @@
1
1
  import { DATE_UTC_ATTRIBUTE } from "../trackedChangeInfo.js";
2
- import { panic } from "better-result";
3
2
  import { assertValidOoxmlNumericId, escapeXmlAttribute } from "@stll/docx-core";
4
3
  import { normalizeRevisionId } from "@stll/docx-core/model";
5
4
  //#region src/docx/serializer/trackedChangeAttributes.ts
6
- /** Enforces the singular `w:rPrChange` child in a run-property container. */
7
- const getSingularRunPropertyChange = (propertyChanges) => {
8
- const propertyChangeCount = propertyChanges?.length ?? 0;
9
- if (propertyChangeCount > 1) panic("A run-property container cannot serialize more than one w:rPrChange", {
10
- elementName: "w:rPrChange",
11
- propertyChangeCount
12
- });
13
- return propertyChanges?.at(0);
14
- };
15
5
  /** Normalized, unescaped attributes in schema order for an XML element or string serializer. */
16
6
  const trackedChangeAttributeEntries = (info) => {
17
7
  assertValidOoxmlNumericId({
@@ -34,4 +24,4 @@ const trackedChangeAttributeEntries = (info) => {
34
24
  const trackedChangeAttributeRecord = (info) => Object.fromEntries(trackedChangeAttributeEntries(info));
35
25
  const serializeTrackedChangeAttributes = (info) => trackedChangeAttributeEntries(info).map(([name, value]) => `${name}="${escapeXmlAttribute(value)}"`).join(" ");
36
26
  //#endregion
37
- export { getSingularRunPropertyChange, serializeTrackedChangeAttributes, trackedChangeAttributeEntries, trackedChangeAttributeRecord };
27
+ export { serializeTrackedChangeAttributes, trackedChangeAttributeEntries, trackedChangeAttributeRecord };
@@ -1,3 +1,4 @@
1
+ import { encodeUtf8 } from "../../utils/textEncoding.js";
1
2
  import { loadDocxArchive } from "./boundedArchive.js";
2
3
  import { FOLIO_DOCX_XML_PATCH_PROPOSAL_PROFILE, InvalidFolioDocxXmlPatchProposalError, evaluateDocxXmlPatchProposal, parseFolioDocxXmlPatchProposal } from "./evaluateDocxXmlPatchProposal.js";
3
4
  import { FOLIO_DOCX_CONFORMANCE_PROFILE, validateDocxConformance } from "./validateDocxConformance.js";
@@ -96,9 +97,8 @@ const applyReplacements = async ({ bytes, proposal }) => {
96
97
  writeZipPart({
97
98
  zip,
98
99
  path: replacement.path,
99
- data: new TextEncoder().encode(replacement.replacementXml),
100
+ data: encodeUtf8(replacement.replacementXml),
100
101
  options: {
101
- binary: true,
102
102
  date: current.date,
103
103
  comment: current.comment,
104
104
  unixPermissions: current.unixPermissions,
@@ -1,3 +1,4 @@
1
+ import { decodeText } from "../../utils/textEncoding.js";
1
2
  import { compressionRatioLimitFor, countCentralDirectoryRecords, createInflationBudget, exceedsCompressionRatio, getZipEntrySizes, inflateEntryWithinLimits } from "../archiveInflation.js";
2
3
  import { FOLIO_XML_RESOURCE_LIMITS, assertXmlResourceLimits, createXmlPackageBudget } from "../xmlResourceLimits.js";
3
4
  import { TaggedError } from "better-result";
@@ -170,7 +171,7 @@ const loadDocxArchive = async (bytes, options = {}) => {
170
171
  async readEntryString(path) {
171
172
  const content = await readEntry(path);
172
173
  if (content === null) return null;
173
- const xml = new TextDecoder("utf-8", { ignoreBOM: true }).decode(content);
174
+ const xml = decodeText(content, { ignoreBOM: true });
174
175
  const lower = path.toLowerCase();
175
176
  if (!lower.endsWith(".xml") && !lower.endsWith(".rels")) return xml;
176
177
  const charged = countedParts.has(path);