@stll/folio-core 0.54.2 → 0.56.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 (267) hide show
  1. package/dist/ai-edits/apply.js +185 -33
  2. package/dist/ai-edits/headless.js +3 -2
  3. package/dist/ai-edits/types.d.ts +7 -1
  4. package/dist/compare/compare.js +1 -0
  5. package/dist/compare/content-types.d.ts +3 -15
  6. package/dist/compare/reproducible-package.d.ts +0 -17
  7. package/dist/compare/reproducible-package.js +12 -18
  8. package/dist/controller/canonicalClipboard.d.ts +23 -0
  9. package/dist/controller/canonicalClipboard.js +476 -0
  10. package/dist/controller/canonicalClipboardResources.d.ts +21 -0
  11. package/dist/controller/canonicalClipboardResources.js +382 -0
  12. package/dist/controller/canonicalComposition.d.ts +53 -0
  13. package/dist/controller/canonicalComposition.js +296 -0
  14. package/dist/controller/canonicalInlineProjection.d.ts +113 -0
  15. package/dist/controller/canonicalInlineProjection.js +393 -0
  16. package/dist/controller/canonicalInput.d.ts +54 -0
  17. package/dist/controller/canonicalInput.js +377 -0
  18. package/dist/controller/canonicalInputTimer.d.ts +7 -0
  19. package/dist/controller/canonicalInputTimer.js +11 -0
  20. package/dist/controller/canonicalOperations.d.ts +22 -0
  21. package/dist/controller/canonicalOperations.js +85 -0
  22. package/dist/controller/canonicalPublicOperations.d.ts +48 -0
  23. package/dist/controller/canonicalPublicOperations.js +556 -0
  24. package/dist/controller/canonicalReview.d.ts +12 -0
  25. package/dist/controller/canonicalReview.js +19 -0
  26. package/dist/controller/canonicalSession.d.ts +167 -0
  27. package/dist/controller/canonicalSession.js +1186 -0
  28. package/dist/controller/canonicalStoryEditor.d.ts +36 -0
  29. package/dist/controller/canonicalStoryEditor.js +87 -0
  30. package/dist/controller/canonicalStructure.d.ts +16 -0
  31. package/dist/controller/canonicalStructure.js +322 -0
  32. package/dist/controller/canonicalTocSelection.d.ts +11 -0
  33. package/dist/controller/canonicalTocSelection.js +43 -0
  34. package/dist/controller/folioEditor.d.ts +3 -10
  35. package/dist/controller/folioEditor.js +14 -4
  36. package/dist/controller/headerFooterEditorManager.d.ts +5 -0
  37. package/dist/controller/headerFooterEditorManager.js +72 -3
  38. package/dist/controller/hiddenEditorApi.d.ts +45 -0
  39. package/dist/controller/hiddenEditorApi.js +29 -8
  40. package/dist/controller/hiddenEditorManager.d.ts +13 -2
  41. package/dist/controller/hiddenEditorManager.js +639 -31
  42. package/dist/controller/layoutPipeline.d.ts +1 -1
  43. package/dist/controller/layoutPipeline.js +2 -0
  44. package/dist/controller/noteEditorManager.d.ts +5 -0
  45. package/dist/controller/noteEditorManager.js +114 -15
  46. package/dist/document-operations.d.ts +8 -1
  47. package/dist/document-operations.js +6 -4
  48. package/dist/docx/attributeRemainder.js +1 -0
  49. package/dist/docx/blockPlainText.d.ts +1 -1
  50. package/dist/docx/blockPlainText.js +11 -3
  51. package/dist/docx/canonicalResourceSave.d.ts +16 -0
  52. package/dist/docx/canonicalResourceSave.js +27 -0
  53. package/dist/docx/canonicalSave.d.ts +29 -0
  54. package/dist/docx/canonicalSave.js +101 -0
  55. package/dist/docx/canonicalSessionInput.d.ts +11 -0
  56. package/dist/docx/canonicalSessionInput.js +27 -0
  57. package/dist/docx/canonicalStoryRepack.d.ts +10 -0
  58. package/dist/docx/canonicalStoryRepack.js +74 -0
  59. package/dist/docx/contentTypeRegistry.d.ts +8 -0
  60. package/dist/docx/contentTypeRegistry.js +64 -0
  61. package/dist/docx/documentParser.d.ts +1 -13
  62. package/dist/docx/documentParser.js +1 -47
  63. package/dist/docx/ensureParaIds.d.ts +7 -1
  64. package/dist/docx/ensureParaIds.js +173 -215
  65. package/dist/docx/foldedListNumberFields.d.ts +85 -0
  66. package/dist/docx/foldedListNumberFields.js +267 -0
  67. package/dist/docx/footnoteParser.d.ts +1 -11
  68. package/dist/docx/footnoteParser.js +1 -16
  69. package/dist/docx/headerFooterParser.d.ts +1 -9
  70. package/dist/docx/headerFooterParser.js +1 -12
  71. package/dist/docx/headerFooterVerbatim.d.ts +39 -1
  72. package/dist/docx/headerFooterVerbatim.js +173 -2
  73. package/dist/docx/imageParser.d.ts +13 -1
  74. package/dist/docx/imageParser.js +20 -11
  75. package/dist/docx/metadataPrivacy.js +6 -1
  76. package/dist/docx/noteIds.d.ts +5 -0
  77. package/dist/docx/noteIds.js +6 -2
  78. package/dist/docx/noteReferenceMark.d.ts +3 -1
  79. package/dist/docx/noteReferenceMark.js +15 -14
  80. package/dist/docx/numberingParser.d.ts +3 -1
  81. package/dist/docx/numberingParser.js +17 -3
  82. package/dist/docx/packageParts.d.ts +3 -1
  83. package/dist/docx/packageParts.js +74 -19
  84. package/dist/docx/paragraphMarkPropertyPresence.d.ts +9 -0
  85. package/dist/docx/paragraphMarkPropertyPresence.js +21 -0
  86. package/dist/docx/paragraphParser.js +101 -53
  87. package/dist/docx/paragraphPropertySource.d.ts +15 -1
  88. package/dist/docx/paragraphPropertySource.js +77 -22
  89. package/dist/docx/parser.js +3 -1
  90. package/dist/docx/removeHeaderFooterParts.js +17 -6
  91. package/dist/docx/reviewHistoryNamespace.d.ts +4 -0
  92. package/dist/docx/reviewHistoryNamespace.js +4 -0
  93. package/dist/docx/reviewResolutionProvenance.d.ts +25 -0
  94. package/dist/docx/reviewResolutionProvenance.js +153 -0
  95. package/dist/docx/rezip.d.ts +54 -6
  96. package/dist/docx/rezip.js +835 -257
  97. package/dist/docx/runConsolidator.d.ts +2 -58
  98. package/dist/docx/runConsolidator.js +3 -175
  99. package/dist/docx/runParser.d.ts +2 -1
  100. package/dist/docx/runParser.js +156 -116
  101. package/dist/docx/saveDiagnostics.d.ts +22 -0
  102. package/dist/docx/saveDiagnostics.js +0 -0
  103. package/dist/docx/selectiveSave.d.ts +3 -1
  104. package/dist/docx/selectiveSave.js +36 -4
  105. package/dist/docx/selectiveXmlPatch.d.ts +16 -8
  106. package/dist/docx/selectiveXmlPatch.js +81 -7
  107. package/dist/docx/serializer/documentSerializer.d.ts +9 -2
  108. package/dist/docx/serializer/documentSerializer.js +29 -10
  109. package/dist/docx/serializer/headerFooterSerializer.d.ts +5 -1
  110. package/dist/docx/serializer/headerFooterSerializer.js +19 -11
  111. package/dist/docx/serializer/noteSerializer.d.ts +2 -2
  112. package/dist/docx/serializer/noteSerializer.js +24 -8
  113. package/dist/docx/serializer/paragraphSerializer.js +41 -4
  114. package/dist/docx/serializer/partNamespaces.js +2 -1
  115. package/dist/docx/serializer/runSerializer.js +3 -1
  116. package/dist/docx/server/applyDocxXmlPatchProposal.js +14 -9
  117. package/dist/docx/settingsHeaderFooterUpdate.d.ts +5 -0
  118. package/dist/docx/settingsHeaderFooterUpdate.js +90 -0
  119. package/dist/docx/settingsParser.d.ts +1 -2
  120. package/dist/docx/settingsParser.js +5 -6
  121. package/dist/docx/storyBlockReplay.d.ts +31 -0
  122. package/dist/docx/storyBlockReplay.js +471 -0
  123. package/dist/docx/storyPlainText.d.ts +20 -0
  124. package/dist/docx/storyPlainText.js +23 -0
  125. package/dist/docx/streamingXmlParser.d.ts +22 -1
  126. package/dist/docx/streamingXmlParser.js +49 -3
  127. package/dist/docx/structuralXmlPatch.js +18 -23
  128. package/dist/docx/styleParser.d.ts +5 -1
  129. package/dist/docx/styleParser.js +1 -1
  130. package/dist/docx/tableParser.d.ts +1 -8
  131. package/dist/docx/tableParser.js +1 -28
  132. package/dist/docx/textBoxParser.d.ts +1 -5
  133. package/dist/docx/textBoxParser.js +1 -20
  134. package/dist/docx/unzip.js +6 -1
  135. package/dist/docx/verbatimCapture.d.ts +3 -1
  136. package/dist/docx/verbatimCapture.js +49 -6
  137. package/dist/i18n/messages/ar.gen.d.ts +5 -0
  138. package/dist/i18n/messages/ar.gen.js +656 -0
  139. package/dist/i18n/messages/cs.gen.d.ts +5 -0
  140. package/dist/i18n/messages/cs.gen.js +656 -0
  141. package/dist/i18n/messages/de.gen.d.ts +5 -0
  142. package/dist/i18n/messages/de.gen.js +656 -0
  143. package/dist/i18n/messages/en.gen.d.ts +5 -0
  144. package/dist/i18n/messages/en.gen.js +656 -0
  145. package/dist/i18n/messages/es.gen.d.ts +5 -0
  146. package/dist/i18n/messages/es.gen.js +656 -0
  147. package/dist/i18n/messages/et.gen.d.ts +5 -0
  148. package/dist/i18n/messages/et.gen.js +656 -0
  149. package/dist/i18n/messages/fr.gen.d.ts +5 -0
  150. package/dist/i18n/messages/fr.gen.js +656 -0
  151. package/dist/i18n/messages/he.gen.d.ts +5 -0
  152. package/dist/i18n/messages/he.gen.js +656 -0
  153. package/dist/i18n/messages/hi.gen.d.ts +5 -0
  154. package/dist/i18n/messages/hi.gen.js +656 -0
  155. package/dist/i18n/messages/hu.gen.d.ts +5 -0
  156. package/dist/i18n/messages/hu.gen.js +656 -0
  157. package/dist/i18n/messages/locales.d.ts +5 -0
  158. package/dist/i18n/messages/locales.js +22 -0
  159. package/dist/i18n/messages/lt.gen.d.ts +5 -0
  160. package/dist/i18n/messages/lt.gen.js +656 -0
  161. package/dist/i18n/messages/lv.gen.d.ts +5 -0
  162. package/dist/i18n/messages/lv.gen.js +656 -0
  163. package/dist/i18n/messages/pl.gen.d.ts +5 -0
  164. package/dist/i18n/messages/pl.gen.js +656 -0
  165. package/dist/i18n/messages/pt-BR.gen.d.ts +5 -0
  166. package/dist/i18n/messages/pt-BR.gen.js +656 -0
  167. package/dist/i18n/messages/sk.gen.d.ts +5 -0
  168. package/dist/i18n/messages/sk.gen.js +656 -0
  169. package/dist/i18n/messages/tr.gen.d.ts +5 -0
  170. package/dist/i18n/messages/tr.gen.js +656 -0
  171. package/dist/i18n/messages/zh-CN.gen.d.ts +5 -0
  172. package/dist/i18n/messages/zh-CN.gen.js +656 -0
  173. package/dist/i18n/messages.d.ts +2 -3
  174. package/dist/i18n/messages.js +1 -19
  175. package/dist/internal/acceptedBlockProjection.d.ts +12 -0
  176. package/dist/internal/acceptedBlockProjection.js +45 -0
  177. package/dist/internal/noteMarkerAttrs.d.ts +8 -0
  178. package/dist/internal/noteMarkerAttrs.js +16 -0
  179. package/dist/internal/pageBreakRunSourceDescendantIndex.js +1 -0
  180. package/dist/internal/prosemirrorAttrBoundary.d.ts +21 -0
  181. package/dist/internal/prosemirrorAttrBoundary.js +50 -0
  182. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  183. package/dist/layout-bridge/convert/footnoteLayout.js +3 -3
  184. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  185. package/dist/layout-bridge/convert/headerFooterLayout.js +1 -0
  186. package/dist/layout-bridge/convert/noteReferences.d.ts +1 -3
  187. package/dist/layout-bridge/convert/noteReferences.js +1 -6
  188. package/dist/layout-bridge/convert/paragraphRuns.js +2 -4
  189. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +2 -2
  190. package/dist/layout-bridge/convert/toFlowBlocks.js +2 -2
  191. package/dist/managers/DocumentLoaderManager.d.ts +1 -0
  192. package/dist/managers/DocumentLoaderManager.js +9 -1
  193. package/dist/markdown/escape.d.ts +3 -1
  194. package/dist/markdown/escape.js +9 -2
  195. package/dist/markdown/renderBlock.js +20 -121
  196. package/dist/markdown/renderParagraph.js +13 -15
  197. package/dist/markdown/renderRuns.d.ts +7 -1
  198. package/dist/markdown/renderRuns.js +35 -15
  199. package/dist/markdown/renderTable.js +2 -0
  200. package/dist/markdown/types.d.ts +3 -9
  201. package/dist/prosemirror/attrs/index.d.ts +2 -12
  202. package/dist/prosemirror/attrs/index.js +22 -48
  203. package/dist/prosemirror/bookmarkBoundaryAttrs.d.ts +2 -1
  204. package/dist/prosemirror/canonicalCommands.d.ts +121 -0
  205. package/dist/prosemirror/canonicalCommands.js +35 -0
  206. package/dist/prosemirror/canonicalSelectionRange.d.ts +9 -0
  207. package/dist/prosemirror/canonicalSelectionRange.js +15 -0
  208. package/dist/prosemirror/clipboardIntent.d.ts +7 -0
  209. package/dist/prosemirror/clipboardIntent.js +9 -0
  210. package/dist/prosemirror/commands/pageBreak.js +15 -1
  211. package/dist/prosemirror/commands/pastePlainText.js +2 -1
  212. package/dist/prosemirror/commentReferenceAttrs.d.ts +2 -1
  213. package/dist/prosemirror/conversion/fromProseDoc.js +91 -19
  214. package/dist/prosemirror/conversion/hyphenTextCarriers.d.ts +8 -0
  215. package/dist/prosemirror/conversion/hyphenTextCarriers.js +8 -0
  216. package/dist/prosemirror/conversion/listRenderingDefinition.d.ts +6 -0
  217. package/dist/prosemirror/conversion/listRenderingDefinition.js +97 -0
  218. package/dist/prosemirror/conversion/toProseDoc.d.ts +11 -4
  219. package/dist/prosemirror/conversion/toProseDoc.js +55 -14
  220. package/dist/prosemirror/executeEditorCommand.d.ts +11 -0
  221. package/dist/prosemirror/executeEditorCommand.js +26 -0
  222. package/dist/prosemirror/extensions/StarterKit.js +2 -0
  223. package/dist/prosemirror/extensions/core/ParagraphExtension.js +88 -48
  224. package/dist/prosemirror/extensions/features/ListExtension.js +18 -5
  225. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +5 -2
  226. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +13 -1
  227. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +56 -28
  228. package/dist/prosemirror/extensions/marks/TextColorExtension.js +3 -2
  229. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +2 -0
  230. package/dist/prosemirror/extensions/marks/markUtils.js +72 -8
  231. package/dist/prosemirror/extensions/nodes/NoteMarkerExtension.d.ts +6 -0
  232. package/dist/prosemirror/extensions/nodes/NoteMarkerExtension.js +36 -0
  233. package/dist/prosemirror/extensions/nodes/PreservedXmlExtension.js +4 -2
  234. package/dist/prosemirror/insertOperations.js +2 -1
  235. package/dist/prosemirror/listMarker.d.ts +1 -0
  236. package/dist/prosemirror/listMarker.js +16 -7
  237. package/dist/prosemirror/listNumbering.js +26 -5
  238. package/dist/prosemirror/listRenderingAttrs.d.ts +1 -0
  239. package/dist/prosemirror/listRenderingAttrs.js +3 -0
  240. package/dist/prosemirror/markupViewNotes.js +3 -2
  241. package/dist/prosemirror/moveRangeBoundaryAttrs.d.ts +2 -1
  242. package/dist/prosemirror/noteReferenceReview.js +5 -3
  243. package/dist/prosemirror/paragraphPropertyCarry.d.ts +7 -1
  244. package/dist/prosemirror/paragraphPropertyCarry.js +8 -3
  245. package/dist/prosemirror/plugins/suggestionMode.js +57 -7
  246. package/dist/prosemirror/rangeAnchorAttrs.d.ts +2 -1
  247. package/dist/prosemirror/runFormattingFromMarks.d.ts +4 -1
  248. package/dist/prosemirror/runFormattingFromMarks.js +15 -1
  249. package/dist/prosemirror/runFormattingInlineCarriers.d.ts +2 -1
  250. package/dist/prosemirror/runFormattingInlineCarriers.js +4 -1
  251. package/dist/prosemirror/schema/marks.d.ts +2 -0
  252. package/dist/prosemirror/schema/nodes.d.ts +10 -2
  253. package/dist/prosemirror/textBoxAnchorAttrs.d.ts +2 -1
  254. package/dist/prosemirror/textInput.js +5 -1
  255. package/dist/prosemirror/trackedRevisionPath.js +2 -1
  256. package/dist/prosemirror/trackedRunInlineAtoms.d.ts +1 -0
  257. package/dist/prosemirror/trackedRunInlineAtoms.js +1 -0
  258. package/dist/prosemirror/validation.js +4 -0
  259. package/dist/types/canonicalCapabilities.d.ts +215 -0
  260. package/dist/types/canonicalCapabilities.js +212 -0
  261. package/dist/types/canonicalSave.d.ts +11 -0
  262. package/dist/types/canonicalSave.js +0 -0
  263. package/dist/types/content.d.ts +2 -2
  264. package/dist/types/docxSerialization.d.ts +14 -0
  265. package/dist/types/docxSerialization.js +7 -0
  266. package/dist/utils/createDocument.js +2 -2
  267. package/package.json +16 -4
@@ -7,7 +7,7 @@ import { parseNumbering } from "./numberingParser.js";
7
7
  import { normalizeImportedNumericIds } from "./numericIdNormalization.js";
8
8
  import { isUnsafePackagePath } from "./packageParts.js";
9
9
  import { RELATIONSHIP_TYPES } from "./relsParser.js";
10
- import { COMMENTS_CONTENT_TYPE, COMMENTS_EXTENDED_PART_LOWER, addCommentsExtendedOverride, addCommentsExtendedRelationship, applyUpdatesToZip, collectHeaderFooterUpdates, findMaxRId, hasModelDrivenPictureWatermark, hasUnmaterializedHeaderFooter, hasUnmaterializedInlineResources, updateCoreProperties, withoutAttachedTemplate } from "./rezip.js";
10
+ 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
11
  import "./selectiveSaveFlags.js";
12
12
  import { buildPatchedDocumentXml, buildPatchedNoteXml, collectAddedNumberingDefs, collectParaIds, patchNumberingDefinitions } from "./selectiveXmlPatch.js";
13
13
  import { planCommentParts, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
@@ -17,6 +17,7 @@ import { serializeNumberingXml } from "./serializer/numberingSerializer.js";
17
17
  import { readRootNamespaceBindings } from "./serializer/partNamespaces.js";
18
18
  import { buildStructuralDocumentPatch } from "./structuralXmlPatch.js";
19
19
  import { DOCX_CONFORMANCE_CLASSES } from "@stll/docx-core/model";
20
+ import { writeZipPart } from "@stll/docx-core/zip";
20
21
  //#region src/docx/selectiveSave.ts
21
22
  /**
22
23
  * Splice edited footnote/endnote paragraphs into their parts and return the
@@ -203,6 +204,7 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
203
204
  const commentPlan = planCommentParts(comments);
204
205
  try {
205
206
  const zip = await (await import("jszip")).default.loadAsync(originalBuffer);
207
+ if (await hasUnmaterializedHyperlinkBindings(doc, zip)) return null;
206
208
  for (const [path, file] of Object.entries(zip.files)) if (!file.dir && isUnsafePackagePath(path)) return null;
207
209
  if (originalBuffer !== doc.originalBuffer) {
208
210
  const sourceParts = /* @__PURE__ */ new Map();
@@ -211,14 +213,27 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
211
213
  if (file.dir || !lowerPath.startsWith("word/") || !lowerPath.endsWith(".xml")) continue;
212
214
  sourceParts.set(path, await file.async("text"));
213
215
  }
214
- for (const [path, xml] of normalizeImportedNumericIds(sourceParts)) if (xml !== sourceParts.get(path)) zip.file(path, xml);
216
+ for (const [path, xml] of normalizeImportedNumericIds(sourceParts)) if (xml !== sourceParts.get(path)) writeZipPart({
217
+ zip,
218
+ path,
219
+ data: xml
220
+ });
215
221
  }
222
+ if (options.bodyAuthority === "canonical") await publishCanonicalImageResources({
223
+ document: doc,
224
+ zip,
225
+ compressionLevel: 6
226
+ });
216
227
  const updates = /* @__PURE__ */ new Map();
217
228
  if (changedParaIds.size > 0 || structuralChange) {
218
229
  const docXmlFile = zip.file("word/document.xml");
219
230
  if (!docXmlFile) return null;
220
231
  const originalDocXml = await docXmlFile.async("text");
221
- const serializedDocXml = serializeDocument(doc, readRootNamespaceBindings(originalDocXml));
232
+ const serializedDocXml = serializeDocument(doc, readRootNamespaceBindings(originalDocXml), {
233
+ xml: originalDocXml,
234
+ bodyAuthority: options.bodyAuthority,
235
+ onDiagnostic: options.onDiagnostic
236
+ });
222
237
  const bodyParaIds = collectParaIds(serializedDocXml);
223
238
  const originalBodyParaIds = structuralChange ? collectParaIds(originalDocXml) : void 0;
224
239
  if (structuralChange && doc.package.numbering) {
@@ -268,7 +283,24 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
268
283
  }
269
284
  if (!await patchCommentsExtended(zip, commentPlan, updates)) return null;
270
285
  if (!await patchNumberingPart(zip, doc, updates)) return null;
271
- for (const [path, xml] of await collectHeaderFooterUpdates(doc, zip)) updates.set(path, xml);
286
+ const styles = await planAddedStyles(doc, zip);
287
+ switch (styles.type) {
288
+ case "unchanged": break;
289
+ case "patch":
290
+ updates.set(styles.path, styles.xml);
291
+ break;
292
+ case "materialize":
293
+ options.onDiagnostic?.({
294
+ type: "selectiveSaveRefused",
295
+ part: "word/styles.xml"
296
+ });
297
+ return null;
298
+ default: return styles;
299
+ }
300
+ for (const [path, xml] of await collectHeaderFooterUpdates(doc, {
301
+ sourceZip: zip,
302
+ onDiagnostic: options.onDiagnostic
303
+ })) updates.set(path, xml);
272
304
  await queueSettingsUpdates(zip, updates);
273
305
  const corePropsFile = zip.file("docProps/core.xml");
274
306
  if (corePropsFile) {
@@ -1,11 +1,5 @@
1
+ import { XmlElement } from "./xmlParser.js";
1
2
  //#region src/docx/selectiveXmlPatch.d.ts
2
- /**
3
- * Selective XML Patch Module
4
- *
5
- * Patches only changed paragraphs in document.xml, preserving
6
- * unchanged content byte-for-byte. Uses string offset tracking
7
- * with proper tag depth counting (not regex) to handle nested elements.
8
- */
9
3
  /**
10
4
  * Whether `char` ends an element's tag name in XML — a whitespace separator
11
5
  * (space, tab, CR, or LF, all valid before attributes per XML 1.0 §3.1), the
@@ -170,6 +164,20 @@ type XmlSplice = {
170
164
  * end, so its original offsets cannot be applied to the resulting string.
171
165
  */
172
166
  declare const spliceXml: (xml: string, splices: readonly XmlSplice[]) => string | null;
167
+ /**
168
+ * A paragraph splice cannot change the owning part's root declarations. Refuse
169
+ * fragments that inherit a prefix from the serialized root when the source
170
+ * root does not bind that prefix to the same URI; the caller can then use the
171
+ * full serializer, which owns the root namespace set.
172
+ */
173
+ type SpliceNamespaceOptions = {
174
+ originalXml: string;
175
+ serializedXml: string;
176
+ fragments: readonly string[];
177
+ originalRoot?: XmlElement;
178
+ serializedRoot?: XmlElement;
179
+ };
180
+ declare const spliceSupportsRootNamespaces: ({ originalXml, serializedXml, fragments, originalRoot, serializedRoot }: SpliceNamespaceOptions) => boolean;
173
181
  type NoteElementName = "footnote" | "endnote";
174
182
  declare const collectChangedNoteParaIds: (baselineXml: string, currentXml: string) => Set<string>;
175
183
  type BuildPatchedNotePartXmlOptions = {
@@ -265,4 +273,4 @@ declare const patchNumberingDefinitions: ({ originalXml, baselineXml, currentXml
265
273
  */
266
274
  declare function appendNumberingDefs(xml: string, currentXml: string, added: ChangedNumberingDefs): string | null;
267
275
  //#endregion
268
- export { ChangedNumberingDefs, NotePartPatch, NotePartPatchRefusal, ParagraphContainer, ParagraphOffsets, PatchValidationResult, ScannedParagraph, XmlSplice, appendNumberingDefs, buildParagraphOffsetIndex, buildPatchedDocumentXml, buildPatchedNotePartXml, buildPatchedNoteXml, buildPatchedNumberingXml, collectAddedNumberingDefs, collectChangedNoteParaIds, collectChangedNumberingDefs, collectParaIds, countParagraphElements, extractParagraphXml, findParagraphOffsets, isXmlNameBoundary, patchNumberingDefinitions, scanParagraphs, spliceXml, validatePatchSafety };
276
+ export { ChangedNumberingDefs, NotePartPatch, NotePartPatchRefusal, ParagraphContainer, ParagraphOffsets, PatchValidationResult, ScannedParagraph, XmlSplice, appendNumberingDefs, buildParagraphOffsetIndex, buildPatchedDocumentXml, buildPatchedNotePartXml, buildPatchedNoteXml, buildPatchedNumberingXml, collectAddedNumberingDefs, collectChangedNoteParaIds, collectChangedNumberingDefs, collectParaIds, countParagraphElements, extractParagraphXml, findParagraphOffsets, isXmlNameBoundary, patchNumberingDefinitions, scanParagraphs, spliceSupportsRootNamespaces, spliceXml, validatePatchSafety };
@@ -1,8 +1,10 @@
1
1
  import { patchBreaksCommentRangeBalance } from "./commentRangeIntegrity.js";
2
2
  import { resolveParagraphIdentities } from "./paraIdAttribute.js";
3
+ import { readRootNamespaceBindings } from "./serializer/partNamespaces.js";
3
4
  import { captureVerbatimXml } from "./verbatimCapture.js";
4
5
  import { PARAGRAPH_SCAN_NAMES, hasCanonicalWordprocessingPrefixes, matchCloseTag, matchOpenTag, resolveSelectiveScanPrefixes, splicesAsCanonical } from "./wordprocessingPrefixes.js";
5
- import { WORDPROCESSINGML_NAMESPACE_URIS, cloneElement, findAttributeByNamespaceUri, getAttributeByNamespaceUri, getChildElements, getLocalName, getNamespacePrefix, getNamespaceUri, parseXmlDocument } from "./xmlParser.js";
6
+ import { resolveNamespaceUri } from "./xmlNamespaceContext.js";
7
+ import { NAMESPACES, WORDPROCESSINGML_NAMESPACE_URIS, cloneElement, findAttributeByNamespaceUri, getAttributeByNamespaceUri, getChildElements, getLocalName, getNamespacePrefix, getNamespaceUri, parseXmlDocument } from "./xmlParser.js";
6
8
  //#region src/docx/selectiveXmlPatch.ts
7
9
  /**
8
10
  * Selective XML Patch Module
@@ -33,7 +35,7 @@ function isXmlNameBoundary(char) {
33
35
  */
34
36
  function findParagraphOffsets(xml, paraId) {
35
37
  const escaped = escapeRegExp(paraId);
36
- const pattern = new RegExp(`<w:p[\\s][^>]*w14:paraId="${escaped}"`, "gu");
38
+ const pattern = new RegExp(`<w:p[\\s][^>]*w14:paraId\\s*=\\s*(["'])${escaped}\\1`, "gu");
37
39
  const matches = [];
38
40
  let match;
39
41
  while ((match = pattern.exec(xml)) !== null) matches.push(match.index);
@@ -189,7 +191,7 @@ function scanParagraphs(xml) {
189
191
  const tagEnd = xml.indexOf(">", tagStart);
190
192
  if (tagEnd === -1) break;
191
193
  const openTag = xml.slice(tagStart, tagEnd + 1);
192
- const paraId = /\bw14:paraId="(?<id>[^"]+)"/u.exec(openTag)?.groups?.["id"];
194
+ const paraId = /\bw14:paraId\s*=\s*(?<quote>["'])(?<id>[^"']+)\k<quote>/u.exec(openTag)?.groups?.["id"];
193
195
  const container = {
194
196
  inFallback: depth.fallback > 0,
195
197
  inAlternateContent: depth.alternateContent > 0,
@@ -234,7 +236,7 @@ function countParagraphElements(xml) {
234
236
  */
235
237
  function collectParaIds(xml) {
236
238
  const ids = /* @__PURE__ */ new Map();
237
- const pattern = /w14:paraId="(?<id>[^"]+)"/gu;
239
+ const pattern = /w14:paraId\s*=\s*(?<quote>["'])(?<id>[^"']+)\k<quote>/gu;
238
240
  let match;
239
241
  while ((match = pattern.exec(xml)) !== null) {
240
242
  const id = match.groups["id"];
@@ -243,7 +245,7 @@ function collectParaIds(xml) {
243
245
  return ids;
244
246
  }
245
247
  /** Paragraph ids folio writes and this module may have to remove again. */
246
- const MINTED_PARA_ID_ATTRIBUTE = /\sw14:(?:para|text)Id="[^"]*"/gu;
248
+ const MINTED_PARA_ID_ATTRIBUTE = /\sw14:(paraId|textId)="[^"]*"/gu;
247
249
  /**
248
250
  * The replacement for a paragraph whose source open tag carries no paraId.
249
251
  *
@@ -260,7 +262,9 @@ const MINTED_PARA_ID_ATTRIBUTE = /\sw14:(?:para|text)Id="[^"]*"/gu;
260
262
  const withoutMintedIds = (paragraphXml, sourceOpenTag) => {
261
263
  const tagEnd = paragraphXml.indexOf(">");
262
264
  if (tagEnd === -1) return paragraphXml;
263
- return paragraphXml.slice(0, tagEnd + 1).replace(MINTED_PARA_ID_ATTRIBUTE, (attribute) => sourceOpenTag.includes(attribute.trim()) ? attribute : "") + paragraphXml.slice(tagEnd + 1);
265
+ return paragraphXml.slice(0, tagEnd + 1).replace(MINTED_PARA_ID_ATTRIBUTE, (attribute, name) => {
266
+ return new RegExp(`\\bw14:${name}\\s*=\\s*(["'])[^"']*\\1`, "u").test(sourceOpenTag) ? attribute : "";
267
+ }) + paragraphXml.slice(tagEnd + 1);
264
268
  };
265
269
  const storyOf = ({ textBoxDepth }) => textBoxDepth > 0 ? "text-box" : "main";
266
270
  const sameContainer = (a, b) => a.inAlternateContent === b.inAlternateContent && a.textBoxDepth === b.textBoxDepth && a.tableDepth === b.tableDepth;
@@ -425,6 +429,14 @@ const routeChangedParagraphs = (originalXml, serializedXml, changedIds) => {
425
429
  newXml: authored ? newXml : withoutMintedIds(newXml, sourceXml)
426
430
  });
427
431
  }
432
+ if (!spliceSupportsRootNamespaces({
433
+ originalXml,
434
+ serializedXml,
435
+ fragments: splices.map(({ newXml }) => newXml)
436
+ })) return {
437
+ type: "refused",
438
+ reason: "root-namespace-requirements-changed"
439
+ };
428
440
  const candidate = spliceXml(originalXml, splices);
429
441
  if (candidate === null) return {
430
442
  type: "refused",
@@ -514,6 +526,68 @@ const spliceXml = (xml, splices) => {
514
526
  }
515
527
  return patchBreaksCommentRangeBalance(xml, result) ? null : result;
516
528
  };
529
+ const spliceSupportsRootNamespaces = ({ originalXml, serializedXml, fragments, originalRoot, serializedRoot }) => {
530
+ const original = originalRoot ?? parseXmlDocument(originalXml);
531
+ const serialized = serializedRoot ?? parseXmlDocument(serializedXml);
532
+ if (!original || !serialized) return false;
533
+ const originalBindings = readRootNamespaceBindings(originalXml);
534
+ const serializedBindings = readRootNamespaceBindings(serializedXml);
535
+ const mcNamespace = /* @__PURE__ */ new Set([NAMESPACES.mc]);
536
+ const ignorableUris = (root) => {
537
+ const uris = /* @__PURE__ */ new Set();
538
+ for (const prefix of (getAttributeByNamespaceUri(root, mcNamespace, "Ignorable") ?? "").split(/\s+/u).filter(Boolean)) {
539
+ const uri = resolveNamespaceUri(root.namespaceScope, prefix);
540
+ if (uri) uris.add(uri);
541
+ }
542
+ return uris;
543
+ };
544
+ const originalNamespaceUses = /* @__PURE__ */ new Set();
545
+ const collectSourceNamespaces = (element) => {
546
+ const uri = getNamespaceUri(element);
547
+ if (uri) originalNamespaceUses.add(uri);
548
+ for (const attribute of Object.keys(element.attributes ?? {})) {
549
+ if (attribute === "xmlns" || attribute.startsWith("xmlns:")) continue;
550
+ const prefix = getNamespacePrefix(attribute);
551
+ const attributeUri = prefix ? resolveNamespaceUri(element.namespaceScope, prefix) : void 0;
552
+ if (attributeUri) originalNamespaceUses.add(attributeUri);
553
+ }
554
+ for (const child of element.elements ?? []) collectSourceNamespaces(child);
555
+ };
556
+ collectSourceNamespaces(original);
557
+ const originalIgnorable = ignorableUris(original);
558
+ const serializedIgnorable = ignorableUris(serialized);
559
+ const inheritedPrefixes = /* @__PURE__ */ new Set();
560
+ const visit = (element, ancestorIgnorableUris) => {
561
+ const localIgnorableUris = new Set(ancestorIgnorableUris);
562
+ for (const uri of ignorableUris(element)) localIgnorableUris.add(uri);
563
+ const requirePrefix = (prefix) => {
564
+ const localUri = resolveNamespaceUri(element.namespaceScope, prefix);
565
+ if (!localUri) inheritedPrefixes.add(prefix);
566
+ const uri = localUri ?? serializedBindings.get(prefix);
567
+ if (!uri) return false;
568
+ return !serializedIgnorable.has(uri) || originalNamespaceUses.has(uri) || originalIgnorable.has(uri) || localIgnorableUris.has(uri);
569
+ };
570
+ const namePrefix = element.name?.split(":").at(0);
571
+ if (namePrefix && namePrefix !== element.name && !requirePrefix(namePrefix)) return false;
572
+ for (const attribute of Object.keys(element.attributes ?? {})) {
573
+ if (attribute === "xmlns" || attribute.startsWith("xmlns:")) continue;
574
+ const prefix = attribute.split(":").at(0);
575
+ if (prefix && prefix !== attribute && !requirePrefix(prefix)) return false;
576
+ }
577
+ for (const child of element.elements ?? []) if (!visit(child, localIgnorableUris)) return false;
578
+ return true;
579
+ };
580
+ for (const fragment of fragments) {
581
+ const parsed = parseXmlDocument(fragment);
582
+ if (!parsed) return false;
583
+ if (!visit(parsed, /* @__PURE__ */ new Set())) return false;
584
+ }
585
+ for (const prefix of inheritedPrefixes) {
586
+ const serializedUri = serializedBindings.get(prefix);
587
+ if (serializedUri === void 0 || originalBindings.get(prefix) !== serializedUri) return false;
588
+ }
589
+ return true;
590
+ };
517
591
  /**
518
592
  * Depth-count the end of the element that opens at `start` (an `<openLiteral…>`
519
593
  * offset), returning its full range. Nested same-name opens increment depth;
@@ -1269,4 +1343,4 @@ function escapeRegExp(str) {
1269
1343
  return str.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
1270
1344
  }
1271
1345
  //#endregion
1272
- export { appendNumberingDefs, buildParagraphOffsetIndex, buildPatchedDocumentXml, buildPatchedNotePartXml, buildPatchedNoteXml, buildPatchedNumberingXml, collectAddedNumberingDefs, collectChangedNoteParaIds, collectChangedNumberingDefs, collectParaIds, countParagraphElements, extractParagraphXml, findParagraphOffsets, isXmlNameBoundary, patchNumberingDefinitions, scanParagraphs, spliceXml, validatePatchSafety };
1346
+ export { appendNumberingDefs, buildParagraphOffsetIndex, buildPatchedDocumentXml, buildPatchedNotePartXml, buildPatchedNoteXml, buildPatchedNumberingXml, collectAddedNumberingDefs, collectChangedNoteParaIds, collectChangedNumberingDefs, collectParaIds, countParagraphElements, extractParagraphXml, findParagraphOffsets, isXmlNameBoundary, patchNumberingDefinitions, scanParagraphs, spliceSupportsRootNamespaces, spliceXml, validatePatchSafety };
@@ -1,4 +1,5 @@
1
1
  import { document_d_exports } from "../../types/document.js";
2
+ import { SaveDiagnosticOptions } from "../saveDiagnostics.js";
2
3
  //#region src/docx/serializer/documentSerializer.d.ts
3
4
  /**
4
5
  * Serialize a DocumentBody to document.xml body content
@@ -15,7 +16,13 @@ declare function serializeDocumentBody(body: document_d_exports.DocumentBody): s
15
16
  * prefix only the source document bound keeps its URI
16
17
  * @returns Complete XML string for document.xml
17
18
  */
18
- declare function serializeDocument(doc: document_d_exports.Document, sourceBindings?: ReadonlyMap<string, string>): string;
19
+ type DocumentBodyAuthorityOptions = {
20
+ /** Canonical saves preserve trusted source blocks; default saves serialize the model. */
21
+ bodyAuthority?: "canonical" | "model" | undefined;
22
+ };
23
+ declare function serializeDocument(doc: document_d_exports.Document, sourceBindings?: ReadonlyMap<string, string>, source?: SaveDiagnosticOptions & DocumentBodyAuthorityOptions & {
24
+ xml?: string | undefined;
25
+ }): string;
19
26
  /**
20
27
  * Serialize just the document body (useful for partial updates)
21
28
  *
@@ -59,4 +66,4 @@ declare function createSimpleDocument(paragraphs: {
59
66
  styleId?: string;
60
67
  }[]): document_d_exports.Document;
61
68
  //#endregion
62
- export { createEmptyDocument, createSimpleDocument, getDocumentContentCount, getDocumentParagraphCount, getDocumentTableCount, hasDocumentContent, hasDocumentSections, hasSectionProperties, serializeDocument, serializeDocumentBody, serializeDocumentBodyElement };
69
+ export { DocumentBodyAuthorityOptions, createEmptyDocument, createSimpleDocument, getDocumentContentCount, getDocumentParagraphCount, getDocumentTableCount, hasDocumentContent, hasDocumentSections, hasSectionProperties, serializeDocument, serializeDocumentBody, serializeDocumentBodyElement };
@@ -1,3 +1,5 @@
1
+ import { getDocumentSourceBaseline } from "../headerFooterVerbatim.js";
2
+ import { buildDocumentBlockReplay, canReplayDocumentSource } from "../storyBlockReplay.js";
1
3
  import { serializeBlockCustomXml } from "./blockCustomXmlSerializer.js";
2
4
  import { serializeBlockSdt } from "./blockSdtSerializer.js";
3
5
  import { serializeBookmarkMarker } from "./markupRangeAttributes.js";
@@ -84,24 +86,41 @@ function serializeDocumentBody(body) {
84
86
  if (body.finalSectionProperties) parts.push(serializeSectionProperties(body.finalSectionProperties));
85
87
  return parts.join("");
86
88
  }
87
- /**
88
- * Serialize a complete Document to valid document.xml
89
- *
90
- * @param doc - The document to serialize
91
- * @param sourceBindings - Root `xmlns:*` of the part being replaced, so a
92
- * prefix only the source document bound keeps its URI
93
- * @returns Complete XML string for document.xml
94
- */
95
- function serializeDocument(doc, sourceBindings) {
89
+ const replayableDocumentSources = /* @__PURE__ */ new WeakMap();
90
+ function serializeDocument(doc, sourceBindings, source) {
96
91
  resetAutoIdCounter();
92
+ const baseline = source?.bodyAuthority === "canonical" ? getDocumentSourceBaseline(doc) : { type: "missing" };
93
+ const sourceMatches = baseline.type === "captured" && (source?.xml === void 0 || source.xml === baseline.xml);
94
+ if (baseline.type === "captured" && sourceMatches && JSON.stringify(doc.package.document) === baseline.fingerprint) {
95
+ if (replayableDocumentSources.get(baseline.body) === baseline.xml) return baseline.xml;
96
+ if (canReplayDocumentSource(baseline.xml, baseline.body)) {
97
+ replayableDocumentSources.set(baseline.body, baseline.xml);
98
+ return baseline.xml;
99
+ }
100
+ }
97
101
  const body = serializeDocumentBackground(doc.package.document.background) + `<w:body>${serializeDocumentBody(doc.package.document)}</w:body>`;
98
- return "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" + serializePartElement({
102
+ const serializedXml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" + serializePartElement({
99
103
  partPath: "word/document.xml",
100
104
  rootName: "w:document",
101
105
  baselinePrefixes: DOCUMENT_BASELINE_PREFIXES,
102
106
  sourceBindings,
103
107
  body
104
108
  });
109
+ if (baseline.type === "missing") return serializedXml;
110
+ if (baseline.type === "captured" && sourceMatches) {
111
+ const replay = buildDocumentBlockReplay({
112
+ sourceXml: baseline.xml,
113
+ baseline: baseline.body,
114
+ current: doc.package.document,
115
+ serializedXml
116
+ });
117
+ if (replay !== null) return replay;
118
+ }
119
+ source?.onDiagnostic?.({
120
+ type: "sourceReplayMismatch",
121
+ part: "word/document.xml"
122
+ });
123
+ return serializedXml;
105
124
  }
106
125
  /**
107
126
  * Serialize just the document body (useful for partial updates)
@@ -1,4 +1,5 @@
1
1
  import { document_d_exports } from "../../types/document.js";
2
+ import { SaveDiagnosticOptions } from "../saveDiagnostics.js";
2
3
  import { SourcePart } from "./partNamespaces.js";
3
4
  //#region src/docx/serializer/headerFooterSerializer.d.ts
4
5
  /**
@@ -9,6 +10,9 @@ import { SourcePart } from "./partNamespaces.js";
9
10
  * document bound keeps its URI
10
11
  * @returns Complete XML string for header*.xml or footer*.xml
11
12
  */
12
- declare function serializeHeaderFooter(hf: document_d_exports.HeaderFooter, source?: SourcePart): string;
13
+ type HeaderFooterSerializeOptions = SourcePart & SaveDiagnosticOptions & {
14
+ originalBuffer?: ArrayBuffer | undefined;
15
+ };
16
+ declare function serializeHeaderFooter(hf: document_d_exports.HeaderFooter, source?: HeaderFooterSerializeOptions): string;
13
17
  //#endregion
14
18
  export { serializeHeaderFooter };
@@ -1,5 +1,6 @@
1
- import { canReplayHeaderFooterVerbatim, getHeaderFooterVerbatimXml } from "../headerFooterVerbatim.js";
1
+ import { canReplayHeaderFooterBlocks, canReplayHeaderFooterVerbatim, getHeaderFooterSourceBaseline, getHeaderFooterVerbatimXml } from "../headerFooterVerbatim.js";
2
2
  import { isEmptyParagraph } from "../paragraphParser.js";
3
+ import { buildStoryBlockReplay } from "../storyBlockReplay.js";
3
4
  import { captureVerbatimXml } from "../verbatimCapture.js";
4
5
  import { getLocalName, parseXmlDocument } from "../xmlParser.js";
5
6
  import { serializeBlockCustomXml } from "./blockCustomXmlSerializer.js";
@@ -49,17 +50,14 @@ function serializeBlock(block) {
49
50
  default: return block;
50
51
  }
51
52
  }
52
- /**
53
- * Serialize a HeaderFooter object to valid OOXML XML
54
- *
55
- * @param hf - HeaderFooter object to serialize
56
- * @param source - The part being replaced, so a prefix only the source
57
- * document bound keeps its URI
58
- * @returns Complete XML string for header*.xml or footer*.xml
59
- */
60
53
  function serializeHeaderFooter(hf, source) {
54
+ const baseline = getHeaderFooterSourceBaseline(hf, source?.originalBuffer);
55
+ if (baseline.type === "mismatch") source?.onDiagnostic?.({
56
+ type: "sourceReplayMismatch",
57
+ part: source.path
58
+ });
61
59
  const verbatim = getHeaderFooterVerbatimXml(hf);
62
- if (verbatim && canReplayHeaderFooterVerbatim(hf)) return verbatim;
60
+ if (baseline.type === "captured" && verbatim && canReplayHeaderFooterVerbatim(hf)) return verbatim;
63
61
  const rootTag = hf.type === "header" ? "w:hdr" : "w:ftr";
64
62
  const watermarkXml = serializeWatermarkParagraph(hf);
65
63
  const watermarkInsertIndex = hf.watermarkBlockIndex !== void 0 ? Math.max(0, Math.min(hf.watermarkBlockIndex, hf.content.length)) : 0;
@@ -75,13 +73,23 @@ function serializeHeaderFooter(hf, source) {
75
73
  });
76
74
  contentXml = blocksXml.slice(0, watermarkInsertIndex).join("") + (mergesIntoRetainedHost ? "" : watermarkXml) + blocksXml.slice(watermarkInsertIndex).join("");
77
75
  } else contentXml = blocksXml.join("");
78
- return "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n" + serializePartElement({
76
+ const serializedXml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n" + serializePartElement({
79
77
  partPath: source?.path ?? `word/${hf.type}.xml`,
80
78
  rootName: rootTag,
81
79
  baselinePrefixes: HEADER_FOOTER_BASELINE_PREFIXES,
82
80
  sourceBindings: source?.bindings,
83
81
  body: contentXml
84
82
  });
83
+ if (baseline.type === "captured" && verbatim && canReplayHeaderFooterBlocks(hf, baseline.content)) {
84
+ const replayed = buildStoryBlockReplay({
85
+ sourceXml: verbatim,
86
+ baselineContent: baseline.content,
87
+ currentContent: hf.content,
88
+ serializedXml
89
+ });
90
+ if (replayed !== null) return replayed;
91
+ }
92
+ return serializedXml;
85
93
  }
86
94
  function serializeRawWatermarkIntoHost({ hostXml, rawWatermarkXml }) {
87
95
  const rawHost = parseXmlDocument(rawWatermarkXml);
@@ -16,9 +16,9 @@ declare function serializeFootnotes(footnotes: readonly document_d_exports.Footn
16
16
  declare function serializeEndnotes(endnotes: readonly document_d_exports.Endnote[]): string;
17
17
  /** Serialize a brand-new footnote part, including Word's required separators
18
18
  * and the automatic reference mark at the start of every normal note. */
19
- declare function serializeNewFootnotesPart(footnotes: readonly document_d_exports.Footnote[]): string;
19
+ declare function serializeNewFootnotesPart(footnotes: readonly document_d_exports.Footnote[], document?: document_d_exports.Document): string;
20
20
  /** Serialize a brand-new endnote part, including Word's required separators
21
21
  * and the automatic reference mark at the start of every normal note. */
22
- declare function serializeNewEndnotesPart(endnotes: readonly document_d_exports.Endnote[]): string;
22
+ declare function serializeNewEndnotesPart(endnotes: readonly document_d_exports.Endnote[], document?: document_d_exports.Document): string;
23
23
  //#endregion
24
24
  export { serializeEndnotes, serializeFootnotes, serializeNewEndnotesPart, serializeNewFootnotesPart };
@@ -1,9 +1,11 @@
1
+ import { hasNoteReferenceMark } from "../noteReferenceMark.js";
1
2
  import { serializeBlockCustomXml } from "./blockCustomXmlSerializer.js";
2
3
  import { serializeBlockSdt } from "./blockSdtSerializer.js";
3
4
  import { serializeBookmarkMarker } from "./markupRangeAttributes.js";
4
5
  import { serializeParagraph } from "./paragraphSerializer.js";
5
6
  import { serializePartElement } from "./partNamespaces.js";
6
7
  import { serializeTable } from "./tableSerializer.js";
8
+ import { noteUsesCustomMark } from "@stll/docx-core/ops";
7
9
  //#region src/docx/serializer/noteSerializer.ts
8
10
  const NOTE_BASELINE_PREFIXES = [
9
11
  "wpc",
@@ -66,8 +68,9 @@ const NOTE_SEPARATOR_BODY = "<w:p><w:pPr><w:spacing w:after=\"0\" w:line=\"240\"
66
68
  function serializeRequiredNoteSeparators(elementName) {
67
69
  return `<w:${elementName} w:type="separator" w:id="-1">${NOTE_SEPARATOR_BODY}<w:r><w:separator/></w:r></w:p></w:${elementName}><w:${elementName} w:type="continuationSeparator" w:id="0">${NOTE_SEPARATOR_BODY}<w:r><w:continuationSeparator/></w:r></w:p></w:${elementName}>`;
68
70
  }
69
- function insertNoteReferenceMark(xml, elementName) {
70
- if (new RegExp(`<(?:[\\w.-]+:)?${elementName}Ref[\\s/>]`, "u").test(xml)) return xml;
71
+ function insertNoteReferenceMark({ xml, elementName, note, customMark }) {
72
+ if (customMark) return xml;
73
+ if (hasNoteReferenceMark(elementName, note.content)) return xml;
71
74
  const paragraphOpen = /<w:p(?=[\s>])[^>]*>/u.exec(xml);
72
75
  if (!paragraphOpen) return `${`<w:p><w:r><w:rPr><w:rStyle w:val="${elementName === "footnote" ? "FootnoteReference" : "EndnoteReference"}"/></w:rPr><w:${elementName}Ref/></w:r></w:p>`}${xml}`;
73
76
  const paragraphStart = paragraphOpen.index + paragraphOpen[0].length;
@@ -77,8 +80,13 @@ function insertNoteReferenceMark(xml, elementName) {
77
80
  const referenceRun = `<w:r><w:rPr><w:rStyle w:val="${elementName === "footnote" ? "FootnoteReference" : "EndnoteReference"}"/></w:rPr><w:${elementName}Ref/></w:r>`;
78
81
  return `${xml.slice(0, insertAt)}${referenceRun}${xml.slice(insertAt)}`;
79
82
  }
80
- function serializeNewNotePart(elementName, notes) {
81
- const serializedNotes = notes.map((note) => insertNoteReferenceMark(serializeNote(elementName, note), elementName)).join("");
83
+ function serializeNewNotePart({ elementName, notes, document }) {
84
+ const serializedNotes = notes.map((note) => insertNoteReferenceMark({
85
+ xml: serializeNote(elementName, note),
86
+ elementName,
87
+ note,
88
+ customMark: document ? noteUsesCustomMark(document, note) : false
89
+ })).join("");
82
90
  return serializeNotePart(elementName, serializeRequiredNoteSeparators(elementName) + serializedNotes);
83
91
  }
84
92
  /**
@@ -101,13 +109,21 @@ function serializeEndnotes(endnotes) {
101
109
  }
102
110
  /** Serialize a brand-new footnote part, including Word's required separators
103
111
  * and the automatic reference mark at the start of every normal note. */
104
- function serializeNewFootnotesPart(footnotes) {
105
- return serializeNewNotePart("footnote", footnotes);
112
+ function serializeNewFootnotesPart(footnotes, document) {
113
+ return serializeNewNotePart({
114
+ elementName: "footnote",
115
+ notes: footnotes,
116
+ ...document ? { document } : {}
117
+ });
106
118
  }
107
119
  /** Serialize a brand-new endnote part, including Word's required separators
108
120
  * and the automatic reference mark at the start of every normal note. */
109
- function serializeNewEndnotesPart(endnotes) {
110
- return serializeNewNotePart("endnote", endnotes);
121
+ function serializeNewEndnotesPart(endnotes, document) {
122
+ return serializeNewNotePart({
123
+ elementName: "endnote",
124
+ notes: endnotes,
125
+ ...document ? { document } : {}
126
+ });
111
127
  }
112
128
  //#endregion
113
129
  export { serializeEndnotes, serializeFootnotes, serializeNewEndnotesPart, serializeNewFootnotesPart };
@@ -2,7 +2,9 @@ import { modelParagraphFormattingEmission, serializeParagraphPropertySet } from
2
2
  import { serializePreservedAttributes } from "../attributeRemainder.js";
3
3
  import { CONTAINER_CHILDREN } from "../containerChildren.gen.js";
4
4
  import { fieldStateAttributes } from "../fieldState.js";
5
+ import { serializeEmptyMarkProperties } from "../paragraphMarkPropertyPresence.js";
5
6
  import { getParagraphPropertySource, paragraphPropertySourceMatchesEmission } from "../paragraphPropertySource.js";
7
+ import { ReviewResolutionProvenanceError, paragraphMarkResolutionJoinAttributes, serializeParagraphMarkResolutionJoin, serializeResolutionJoins } from "../reviewResolutionProvenance.js";
6
8
  import { DATE_UTC_ATTRIBUTE } from "../trackedChangeInfo.js";
7
9
  import { toTransitionalNamespaceUri } from "../transitionalSpelling.js";
8
10
  import { captureVerbatimXml, createCapturedXmlSanitizer } from "../verbatimCapture.js";
@@ -20,11 +22,22 @@ import { PARAGRAPH_MARK_CHANGE_KINDS } from "@stll/docx-core/model";
20
22
  import { SEQUENCE_CHILDREN } from "@stll/docx-core/schema";
21
23
  //#region src/docx/serializer/paragraphSerializer.ts
22
24
  /**
25
+ * Paragraph Serializer - Serialize paragraphs to OOXML XML
26
+ *
27
+ * Converts Paragraph objects back to <w:p> XML format for DOCX files.
28
+ * Handles all paragraph properties and child content (runs, hyperlinks, fields, bookmarks).
29
+ *
30
+ * OOXML Reference:
31
+ * - Paragraph: w:p
32
+ * - Paragraph properties: w:pPr
33
+ * - Runs, hyperlinks, bookmarks, fields as child elements
34
+ */
35
+ /**
23
36
  * Serialize paragraph formatting properties to w:pPr XML
24
37
  */
25
38
  function serializeParagraphMarkChange(mark) {
26
39
  const attrs = serializeTrackedChangeAttributes(mark.info);
27
- return `<w:${mark.kind} ${attrs}/>`;
40
+ return `<w:${mark.kind} ${attrs}${serializeParagraphMarkResolutionJoin(mark)}/>`;
28
41
  }
29
42
  const RESERVED_PARAGRAPH_PROPERTY_CHILDREN = /* @__PURE__ */ new Set(["pPrChange", "sectPr"]);
30
43
  /**
@@ -212,7 +225,10 @@ const withTrailingParagraphPropertyChildren = (sourceXml, children) => {
212
225
  const withParagraphMarkChange = (sourceXml, mark) => {
213
226
  const root = parseXml(sourceXml, OOXML_NAMESPACE_SCOPE).elements?.at(0);
214
227
  if (!root || root.type !== "element") panic("A validated paragraph-property capture could not be parsed for composition");
215
- const attributes = trackedChangeAttributeRecord(mark.info);
228
+ const attributes = {
229
+ ...trackedChangeAttributeRecord(mark.info),
230
+ ...paragraphMarkResolutionJoinAttributes(mark)
231
+ };
216
232
  const markElement = cloneElement(root, {
217
233
  name: `w:${mark.kind}`,
218
234
  attributes,
@@ -339,6 +355,20 @@ function serializeComplexField(field) {
339
355
  parts.push(`<w:r>${rPrXml}<w:fldChar w:fldCharType="end"/></w:r>`);
340
356
  return parts.join("");
341
357
  }
358
+ var UnrepresentableTrackedCaptureError = class extends TaggedError("UnrepresentableTrackedCaptureError") {};
359
+ /**
360
+ * Preserved markup inside a removal. Markup captured from a removal already
361
+ * spells its text as deleted text. Markup captured outside one and put under
362
+ * a removal afterwards does not: plain runs are respelled here, as a deleted
363
+ * run is. A folded list-number capture that cannot be respelled is refused,
364
+ * rather than written as live text inside the removal.
365
+ */
366
+ function serializeRemovedPreservedInline(item) {
367
+ if (!/<w:(?:t|instrText)[\s>]/u.test(item.xml)) return item.xml;
368
+ if (/^<w:r[\s>]/u.test(item.xml) && !/<w:(?:drawing|pict|object)\b|<mc:AlternateContent\b/u.test(item.xml)) return rewriteRunTextAsDeleted(item.xml);
369
+ if (item.foldedListNumber) throw new UnrepresentableTrackedCaptureError({ message: "A folded list-number field holds markup that cannot be written as deleted content." });
370
+ return item.xml;
371
+ }
342
372
  var UnrepresentableTrackedSimpleFieldError = class extends TaggedError("UnrepresentableTrackedSimpleFieldError") {};
343
373
  /** A revision can contain run-level field characters, but not `w:fldSimple`. */
344
374
  function serializeTrackedSimpleField(field) {
@@ -429,7 +459,12 @@ function trackedChangeTag(change) {
429
459
  }
430
460
  }
431
461
  function serializeTrackedChange(tag, change) {
432
- const attrs = serializeTrackedChangeAttributes(change.info);
462
+ if (change.resolutionJoins !== void 0 && change.content.some((item) => item.type === "hyperlink")) throw new ReviewResolutionProvenanceError({
463
+ attribute: "resolutionJoins",
464
+ reason: "unrepresentable",
465
+ message: "Tracked-resolution provenance cannot be repartitioned across serialized hyperlinks."
466
+ });
467
+ const attrs = serializeTrackedChangeAttributes(change.info) + serializeResolutionJoins(change.resolutionJoins);
433
468
  const disposition = tag === "del" || tag === "moveFrom" ? "removed" : "kept";
434
469
  const serializeContentRun = (run) => serializeInlineRun(run, disposition);
435
470
  const serializeWrappedItem = (item) => {
@@ -451,7 +486,7 @@ function serializeTrackedChange(tag, change) {
451
486
  case "moveFromRangeEnd": return `<w:moveFromRangeEnd ${markupRangeAttributes(item).join(" ")}/>`;
452
487
  case "moveToRangeStart": return serializeMoveRangeStart("moveToRangeStart", item);
453
488
  case "moveToRangeEnd": return `<w:moveToRangeEnd ${markupRangeAttributes(item).join(" ")}/>`;
454
- case "preservedInline": return item.xml;
489
+ case "preservedInline": return disposition === "removed" ? serializeRemovedPreservedInline(item) : item.xml;
455
490
  case "inlineWrapper":
456
491
  case "inlineSdt": return serializeParagraphContent(item, disposition);
457
492
  default: return item;
@@ -574,6 +609,8 @@ function serializeParagraph(paragraph) {
574
609
  if (paragraph.paraId) attrs.push(`w14:paraId="${escapeXmlAttribute(paragraph.paraId)}"`);
575
610
  if (paragraph.textId) attrs.push(`w14:textId="${escapeXmlAttribute(paragraph.textId)}"`);
576
611
  if (paragraph.reviewCarrier) attrs.push(`folio:reviewCarrier="${paragraph.reviewCarrier}"`);
612
+ const emptyMarkProperties = serializeEmptyMarkProperties(paragraph);
613
+ if (emptyMarkProperties !== void 0) attrs.push(emptyMarkProperties);
577
614
  const written = serializePreservedAttributes(attrs, paragraph.preservedAttributes);
578
615
  const attrsStr = written.length > 0 ? ` ${written.join(" ")}` : "";
579
616
  parts.push(serializeParagraphFormattingWithOptions(paragraph.formatting, {
@@ -1,3 +1,4 @@
1
+ import { FOLIO_REVIEW_HISTORY_NAMESPACE } from "../reviewHistoryNamespace.js";
1
2
  import { toTransitionalNamespaceUri } from "../transitionalSpelling.js";
2
3
  import { TaggedError } from "better-result";
3
4
  import { assertValidOoxmlNumericIds, escapeXmlAttribute } from "@stll/docx-core";
@@ -144,7 +145,7 @@ const OOXML_NAMESPACES = {
144
145
  ignorable: true
145
146
  },
146
147
  folio: {
147
- uri: "urn:stella:folio:review-history:1",
148
+ uri: FOLIO_REVIEW_HISTORY_NAMESPACE,
148
149
  ignorable: true
149
150
  },
150
151
  wpg: {
@@ -7,6 +7,7 @@ import { serializeGraphicFrameLocks } from "../graphicFrameLocks.js";
7
7
  import { DECORATIVE_EXTENSION_URI, DECORATIVE_NAMESPACE } from "../imageParser.js";
8
8
  import { canReplayEditableImageRawXml } from "../imageRawXml.js";
9
9
  import { serializeNonVisualDrawingNames } from "../nonVisualDrawingProps.js";
10
+ import { serializeBoundaryJoins } from "../reviewResolutionProvenance.js";
10
11
  import { runHoldsPayload } from "../runPayload.js";
11
12
  import { replayableShapeAlternateContent } from "../shapeAlternateContent.js";
12
13
  import { requiredWrapPolygon, serializeWrapPolygon } from "../wrapPolygon.js";
@@ -51,7 +52,7 @@ function getUniqueId(id) {
51
52
  }
52
53
  function serializeRunPropertyChange(change) {
53
54
  const previousRPrXml = serializeTextFormatting(change.previousFormatting) || "<w:rPr/>";
54
- return `<w:rPrChange ${serializeTrackedChangeAttributes(change.info)}>${previousRPrXml}</w:rPrChange>`;
55
+ return `<w:rPrChange ${serializeTrackedChangeAttributes(change.info)}${serializeBoundaryJoins(change.boundaryJoins)}>${previousRPrXml}</w:rPrChange>`;
55
56
  }
56
57
  function serializeRunProperties(formatting, propertyChanges) {
57
58
  const propertyChange = getSingularRunPropertyChange(propertyChanges);
@@ -537,6 +538,7 @@ function serializeRunContent(content) {
537
538
  case "symbol": return serializeSymbolContent(content);
538
539
  case "footnoteRef":
539
540
  case "endnoteRef": return serializeNoteReference(content);
541
+ case "noteMarker": return content.kind === "footnote" ? "<w:footnoteRef/>" : "<w:endnoteRef/>";
540
542
  case "fieldChar": return serializeFieldChar(content);
541
543
  case "instrText": return serializeInstrText(content);
542
544
  case "softHyphen": return serializeSoftHyphen(content);