@stll/folio-core 0.55.0 → 0.57.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 (225) hide show
  1. package/dist/ai-edits/apply.js +214 -52
  2. package/dist/ai-edits/batch-claims.d.ts +3 -1
  3. package/dist/ai-edits/batch-claims.js +25 -1
  4. package/dist/ai-edits/headless.js +2 -2
  5. package/dist/ai-edits/table-row-column-mutations.js +17 -3
  6. package/dist/ai-edits/types.d.ts +7 -1
  7. package/dist/ai-suggestions/apply.js +20 -1
  8. package/dist/compare/compare.js +1 -0
  9. package/dist/compare/inline-atoms.js +40 -16
  10. package/dist/compare/inline-provenance.js +12 -5
  11. package/dist/compare/reproducible-package.d.ts +0 -17
  12. package/dist/compare/reproducible-package.js +12 -18
  13. package/dist/compat/eigenpal.d.ts +2 -1
  14. package/dist/compat/eigenpal.js +2 -1
  15. package/dist/controller/canonicalClipboard.d.ts +23 -0
  16. package/dist/controller/canonicalClipboard.js +482 -0
  17. package/dist/controller/canonicalClipboardResources.d.ts +21 -0
  18. package/dist/controller/canonicalClipboardResources.js +382 -0
  19. package/dist/controller/canonicalComments.d.ts +41 -0
  20. package/dist/controller/canonicalComments.js +152 -0
  21. package/dist/controller/canonicalComposition.d.ts +24 -3
  22. package/dist/controller/canonicalComposition.js +208 -48
  23. package/dist/controller/canonicalInlineProjection.d.ts +118 -0
  24. package/dist/controller/canonicalInlineProjection.js +476 -0
  25. package/dist/controller/canonicalInput.d.ts +13 -4
  26. package/dist/controller/canonicalInput.js +62 -14
  27. package/dist/controller/canonicalInputTimer.d.ts +16 -0
  28. package/dist/controller/canonicalInputTimer.js +34 -0
  29. package/dist/controller/canonicalPublicOperations.d.ts +48 -0
  30. package/dist/controller/canonicalPublicOperations.js +648 -0
  31. package/dist/controller/canonicalSelection.d.ts +18 -0
  32. package/dist/controller/canonicalSelection.js +32 -0
  33. package/dist/controller/canonicalSession.d.ts +27 -6
  34. package/dist/controller/canonicalSession.js +298 -116
  35. package/dist/controller/canonicalStoryEditor.d.ts +7 -3
  36. package/dist/controller/canonicalStoryEditor.js +27 -2
  37. package/dist/controller/canonicalStructure.js +57 -4
  38. package/dist/controller/canonicalTocSelection.d.ts +11 -0
  39. package/dist/controller/canonicalTocSelection.js +43 -0
  40. package/dist/controller/folioEditor.d.ts +2 -9
  41. package/dist/controller/folioEditor.js +18 -4
  42. package/dist/controller/headerFooterEditorManager.d.ts +2 -1
  43. package/dist/controller/headerFooterEditorManager.js +26 -11
  44. package/dist/controller/hiddenEditorApi.d.ts +24 -3
  45. package/dist/controller/hiddenEditorApi.js +8 -0
  46. package/dist/controller/hiddenEditorManager.d.ts +5 -1
  47. package/dist/controller/hiddenEditorManager.js +378 -43
  48. package/dist/controller/noteEditorManager.d.ts +2 -1
  49. package/dist/controller/noteEditorManager.js +29 -13
  50. package/dist/document-operations.d.ts +8 -1
  51. package/dist/document-operations.js +6 -4
  52. package/dist/docx/blockPlainText.d.ts +1 -1
  53. package/dist/docx/blockPlainText.js +11 -3
  54. package/dist/docx/builtInStyles.js +13 -6
  55. package/dist/docx/canonicalResourceSave.d.ts +16 -0
  56. package/dist/docx/canonicalResourceSave.js +27 -0
  57. package/dist/docx/canonicalSave.d.ts +29 -0
  58. package/dist/docx/canonicalSave.js +101 -0
  59. package/dist/docx/canonicalStoryRepack.d.ts +1 -1
  60. package/dist/docx/canonicalStoryRepack.js +38 -5
  61. package/dist/docx/containerChildren.gen.d.ts +3 -1
  62. package/dist/docx/containerChildren.gen.js +33 -1
  63. package/dist/docx/documentParser.d.ts +1 -13
  64. package/dist/docx/documentParser.js +1 -47
  65. package/dist/docx/ensureParaIds.js +10 -4
  66. package/dist/docx/fieldParser.js +14 -2
  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 +26 -1
  72. package/dist/docx/headerFooterVerbatim.js +81 -3
  73. package/dist/docx/hyperlinkParser.d.ts +1 -1
  74. package/dist/docx/hyperlinkParser.js +20 -1
  75. package/dist/docx/imageParser.d.ts +13 -1
  76. package/dist/docx/imageParser.js +20 -11
  77. package/dist/docx/inlineWrapperContent.d.ts +4 -4
  78. package/dist/docx/inlineWrapperContent.js +4 -4
  79. package/dist/docx/linkedSdtPreservation.d.ts +10 -0
  80. package/dist/docx/linkedSdtPreservation.js +37 -0
  81. package/dist/docx/metadataPrivacy.js +6 -1
  82. package/dist/docx/packageParts.js +9 -3
  83. package/dist/docx/paragraphParser.js +88 -67
  84. package/dist/docx/paragraphPropertySource.d.ts +22 -1
  85. package/dist/docx/paragraphPropertySource.js +109 -2
  86. package/dist/docx/parser.js +2 -1
  87. package/dist/docx/preservedRunContent.js +2 -1
  88. package/dist/docx/relsParser.js +26 -2
  89. package/dist/docx/removeHeaderFooterParts.js +17 -6
  90. package/dist/docx/rezip.d.ts +49 -5
  91. package/dist/docx/rezip.js +754 -235
  92. package/dist/docx/saveDiagnostics.d.ts +11 -0
  93. package/dist/docx/selectiveSave.d.ts +2 -1
  94. package/dist/docx/selectiveSave.js +42 -11
  95. package/dist/docx/selectiveXmlPatch.js +7 -5
  96. package/dist/docx/serializer/commentSerializer.d.ts +4 -1
  97. package/dist/docx/serializer/commentSerializer.js +7 -3
  98. package/dist/docx/serializer/documentSerializer.d.ts +9 -2
  99. package/dist/docx/serializer/documentSerializer.js +29 -10
  100. package/dist/docx/serializer/paragraphSerializer.js +207 -94
  101. package/dist/docx/server/applyDocxXmlPatchProposal.js +14 -9
  102. package/dist/docx/server/materializeYjsDocx.js +14 -6
  103. package/dist/docx/server/migrateYjsAttrSchema.js +17 -5
  104. package/dist/docx/storyBlockReplay.d.ts +21 -1
  105. package/dist/docx/storyBlockReplay.js +113 -9
  106. package/dist/docx/storyPlainText.d.ts +20 -0
  107. package/dist/docx/storyPlainText.js +23 -0
  108. package/dist/docx/structuralXmlPatch.js +10 -22
  109. package/dist/docx/styleParser.d.ts +5 -1
  110. package/dist/docx/styleParser.js +1 -1
  111. package/dist/docx/tableParser.d.ts +1 -8
  112. package/dist/docx/tableParser.js +1 -28
  113. package/dist/docx/textBoxParser.d.ts +1 -5
  114. package/dist/docx/textBoxParser.js +1 -20
  115. package/dist/docx/unzip.js +6 -1
  116. package/dist/i18n/messages/ar.gen.js +4 -0
  117. package/dist/i18n/messages/catalogs.gen.d.ts +68 -0
  118. package/dist/i18n/messages/catalogs.gen.js +68 -0
  119. package/dist/i18n/messages/cs.gen.js +4 -0
  120. package/dist/i18n/messages/de.gen.js +4 -0
  121. package/dist/i18n/messages/en.gen.js +4 -0
  122. package/dist/i18n/messages/es.gen.js +4 -0
  123. package/dist/i18n/messages/et.gen.js +4 -0
  124. package/dist/i18n/messages/fr.gen.js +4 -0
  125. package/dist/i18n/messages/he.gen.js +4 -0
  126. package/dist/i18n/messages/hi.gen.js +4 -0
  127. package/dist/i18n/messages/hu.gen.js +4 -0
  128. package/dist/i18n/messages/lt.gen.js +4 -0
  129. package/dist/i18n/messages/lv.gen.js +4 -0
  130. package/dist/i18n/messages/messages.gen.d.ts +4 -0
  131. package/dist/i18n/messages/pl.gen.js +4 -0
  132. package/dist/i18n/messages/pt-BR.gen.js +4 -0
  133. package/dist/i18n/messages/sk.gen.js +4 -0
  134. package/dist/i18n/messages/tr.gen.js +4 -0
  135. package/dist/i18n/messages/zh-CN.gen.js +4 -0
  136. package/dist/index.d.ts +2 -1
  137. package/dist/index.js +2 -1
  138. package/dist/internal/acceptedBlockProjection.d.ts +12 -0
  139. package/dist/internal/acceptedBlockProjection.js +45 -0
  140. package/dist/internal/revisionResolutionInline.js +25 -3
  141. package/dist/managers/DocumentLoaderManager.js +2 -1
  142. package/dist/markdown/escape.d.ts +3 -1
  143. package/dist/markdown/escape.js +9 -2
  144. package/dist/markdown/renderBlock.js +5 -120
  145. package/dist/markdown/renderParagraph.js +4 -2
  146. package/dist/markdown/renderRuns.d.ts +7 -1
  147. package/dist/markdown/renderRuns.js +33 -24
  148. package/dist/panel-layout.d.ts +11 -9
  149. package/dist/panel-layout.js +13 -9
  150. package/dist/prosemirror/addTrackedDeletionMark.d.ts +18 -0
  151. package/dist/prosemirror/addTrackedDeletionMark.js +87 -0
  152. package/dist/prosemirror/attrs/index.js +5 -0
  153. package/dist/prosemirror/canonicalCommands.d.ts +28 -4
  154. package/dist/prosemirror/canonicalCommands.js +11 -4
  155. package/dist/prosemirror/canonicalSelectionRange.d.ts +9 -0
  156. package/dist/prosemirror/canonicalSelectionRange.js +15 -0
  157. package/dist/prosemirror/clipboardIntent.d.ts +7 -0
  158. package/dist/prosemirror/clipboardIntent.js +9 -0
  159. package/dist/prosemirror/commands/comments.js +60 -35
  160. package/dist/prosemirror/commands/pageBreak.js +2 -2
  161. package/dist/prosemirror/commands/pastePlainText.js +2 -1
  162. package/dist/prosemirror/commands/propertyChangeScope.js +3 -6
  163. package/dist/prosemirror/commands/resolveParagraphProperties.js +22 -13
  164. package/dist/prosemirror/commentOps.js +15 -1
  165. package/dist/prosemirror/conversion/fromProseDoc.js +22 -3
  166. package/dist/prosemirror/conversion/hyphenTextCarriers.d.ts +8 -0
  167. package/dist/prosemirror/conversion/hyphenTextCarriers.js +8 -0
  168. package/dist/prosemirror/conversion/toProseDoc.d.ts +7 -1
  169. package/dist/prosemirror/conversion/toProseDoc.js +124 -22
  170. package/dist/prosemirror/extensions/core/ParagraphExtension.js +61 -35
  171. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +74 -25
  172. package/dist/prosemirror/extensions/marks/FootnoteRefExtension.js +11 -0
  173. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +65 -29
  174. package/dist/prosemirror/extensions/marks/markUtils.js +11 -8
  175. package/dist/prosemirror/extensions/marks/noteReferenceDeletion.js +5 -2
  176. package/dist/prosemirror/fieldRepresentation.d.ts +8 -0
  177. package/dist/prosemirror/fieldRepresentation.js +18 -0
  178. package/dist/prosemirror/hyperlinkRemoval.d.ts +13 -1
  179. package/dist/prosemirror/hyperlinkRemoval.js +28 -9
  180. package/dist/prosemirror/insertOperations.js +2 -1
  181. package/dist/prosemirror/listNumbering.js +11 -5
  182. package/dist/prosemirror/noteReferenceOccurrenceId.d.ts +5 -0
  183. package/dist/prosemirror/noteReferenceOccurrenceId.js +5 -0
  184. package/dist/prosemirror/noteReferenceOccurrences.d.ts +21 -0
  185. package/dist/prosemirror/noteReferenceOccurrences.js +185 -0
  186. package/dist/prosemirror/noteReferenceReview.js +14 -1
  187. package/dist/prosemirror/paragraphPropertyCarry.d.ts +7 -1
  188. package/dist/prosemirror/paragraphPropertyCarry.js +8 -3
  189. package/dist/prosemirror/plugins/documentStyles.d.ts +4 -1
  190. package/dist/prosemirror/plugins/documentStyles.js +17 -10
  191. package/dist/prosemirror/plugins/suggestionMode.js +48 -48
  192. package/dist/prosemirror/preservedXmlReview.d.ts +6 -0
  193. package/dist/prosemirror/preservedXmlReview.js +35 -0
  194. package/dist/prosemirror/runFormattingFromMarks.js +2 -2
  195. package/dist/prosemirror/runFormattingReconciliation.js +5 -4
  196. package/dist/prosemirror/runPropertyChangeResolution.d.ts +8 -1
  197. package/dist/prosemirror/runPropertyChangeResolution.js +24 -2
  198. package/dist/prosemirror/runStyleFormatting.d.ts +3 -1
  199. package/dist/prosemirror/runStyleFormatting.js +3 -1
  200. package/dist/prosemirror/schema/marks.d.ts +1 -0
  201. package/dist/prosemirror/schema/nodes.d.ts +2 -0
  202. package/dist/prosemirror/styles/paragraphStyleCascade.js +4 -1
  203. package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +15 -15
  204. package/dist/prosemirror/styles/resolvedStyleAttrs.js +69 -28
  205. package/dist/prosemirror/textInput.d.ts +5 -1
  206. package/dist/prosemirror/textInput.js +3 -3
  207. package/dist/prosemirror/trackedRevisionPath.d.ts +5 -1
  208. package/dist/prosemirror/trackedRevisionPath.js +39 -1
  209. package/dist/prosemirror/validation.js +3 -5
  210. package/dist/prosemirror/yjsDocumentMetadata.d.ts +9 -3
  211. package/dist/prosemirror/yjsDocumentMetadata.js +37 -8
  212. package/dist/types/canonicalCapabilities.d.ts +201 -0
  213. package/dist/types/canonicalCapabilities.js +198 -0
  214. package/dist/types/canonicalComments.d.ts +51 -0
  215. package/dist/types/canonicalComments.js +0 -0
  216. package/dist/types/canonicalSave.d.ts +11 -0
  217. package/dist/types/canonicalSave.js +0 -0
  218. package/dist/types/canonicalSections.d.ts +12 -0
  219. package/dist/types/canonicalSections.js +0 -0
  220. package/dist/types/docxSerialization.d.ts +14 -0
  221. package/dist/types/docxSerialization.js +7 -0
  222. package/dist/utils/headingCollector.js +45 -4
  223. package/dist/utils/outlineDepth.d.ts +9 -0
  224. package/dist/utils/outlineDepth.js +9 -0
  225. package/package.json +4 -4
@@ -1,8 +1,19 @@
1
+ import { CANONICAL_GAP } from "../types/canonicalCapabilities.js";
1
2
  //#region src/docx/saveDiagnostics.d.ts
2
3
  /** Save diagnostics describe fidelity fallbacks without including document content. */
3
4
  type SaveDiagnostic = {
4
5
  type: "sourceReplayMismatch";
5
6
  part: string;
7
+ } | {
8
+ type: "selectiveSaveRefused";
9
+ part: string;
10
+ } | {
11
+ type: "sourceReplayUnavailable";
12
+ part: string;
13
+ } | {
14
+ type: "canonicalResourceReplacement";
15
+ gap: typeof CANONICAL_GAP.resourceReplacement;
16
+ part: string;
6
17
  };
7
18
  type SaveDiagnosticOptions = {
8
19
  onDiagnostic?: ((diagnostic: SaveDiagnostic) => void) | undefined;
@@ -1,7 +1,8 @@
1
1
  import { document_d_exports } from "../types/document.js";
2
2
  import { SaveDiagnosticOptions } from "./saveDiagnostics.js";
3
+ import { DocumentBodyAuthorityOptions } from "./serializer/documentSerializer.js";
3
4
  //#region src/docx/selectiveSave.d.ts
4
- type SelectiveSaveOptions = SaveDiagnosticOptions & {
5
+ type SelectiveSaveOptions = SaveDiagnosticOptions & DocumentBodyAuthorityOptions & {
5
6
  /** Changed paragraph IDs to selectively patch */
6
7
  changedParaIds: Set<string>;
7
8
  /** Whether paragraph membership, order, or block structure changed. */
@@ -7,16 +7,17 @@ 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
- import { planCommentParts, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
13
+ import { hasOwnedCommentsPart, planCommentParts, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
14
14
  import { serializeDocument } from "./serializer/documentSerializer.js";
15
15
  import { serializeEndnotes, serializeFootnotes } from "./serializer/noteSerializer.js";
16
16
  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
@@ -200,9 +201,11 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
200
201
  if (!validateFolioDocumentModel(doc).valid) return null;
201
202
  const comments = doc.package.document.comments ?? [];
202
203
  const hasComments = comments.length > 0;
204
+ const ownsCommentsPart = hasOwnedCommentsPart(doc);
203
205
  const commentPlan = planCommentParts(comments);
204
206
  try {
205
207
  const zip = await (await import("jszip")).default.loadAsync(originalBuffer);
208
+ if (await hasUnmaterializedHyperlinkBindings(doc, zip)) return null;
206
209
  for (const [path, file] of Object.entries(zip.files)) if (!file.dir && isUnsafePackagePath(path)) return null;
207
210
  if (originalBuffer !== doc.originalBuffer) {
208
211
  const sourceParts = /* @__PURE__ */ new Map();
@@ -211,14 +214,27 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
211
214
  if (file.dir || !lowerPath.startsWith("word/") || !lowerPath.endsWith(".xml")) continue;
212
215
  sourceParts.set(path, await file.async("text"));
213
216
  }
214
- for (const [path, xml] of normalizeImportedNumericIds(sourceParts)) if (xml !== sourceParts.get(path)) zip.file(path, xml);
217
+ for (const [path, xml] of normalizeImportedNumericIds(sourceParts)) if (xml !== sourceParts.get(path)) writeZipPart({
218
+ zip,
219
+ path,
220
+ data: xml
221
+ });
215
222
  }
223
+ if (options.bodyAuthority === "canonical") await publishCanonicalImageResources({
224
+ document: doc,
225
+ zip,
226
+ compressionLevel: 6
227
+ });
216
228
  const updates = /* @__PURE__ */ new Map();
217
229
  if (changedParaIds.size > 0 || structuralChange) {
218
230
  const docXmlFile = zip.file("word/document.xml");
219
231
  if (!docXmlFile) return null;
220
232
  const originalDocXml = await docXmlFile.async("text");
221
- const serializedDocXml = serializeDocument(doc, readRootNamespaceBindings(originalDocXml));
233
+ const serializedDocXml = serializeDocument(doc, readRootNamespaceBindings(originalDocXml), {
234
+ xml: originalDocXml,
235
+ bodyAuthority: options.bodyAuthority,
236
+ onDiagnostic: options.onDiagnostic
237
+ });
222
238
  const bodyParaIds = collectParaIds(serializedDocXml);
223
239
  const originalBodyParaIds = structuralChange ? collectParaIds(originalDocXml) : void 0;
224
240
  if (structuralChange && doc.package.numbering) {
@@ -244,30 +260,45 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
244
260
  updates.set("word/document.xml", patchedDocXml);
245
261
  }
246
262
  }
247
- const sourceCommentsFile = zip.file("word/comments.xml");
248
- if (hasComments || sourceCommentsFile) {
263
+ const sourceCommentsFile = findZipEntryCaseInsensitive(zip, "word/comments.xml");
264
+ const commentsPartPath = sourceCommentsFile?.name ?? "word/comments.xml";
265
+ if (hasComments || ownsCommentsPart || sourceCommentsFile) {
249
266
  const sourceCommentsXml = sourceCommentsFile ? await sourceCommentsFile.async("text") : void 0;
250
267
  const commentsXml = serializeComments(commentPlan, sourceCommentsXml === void 0 ? void 0 : readRootNamespaceBindings(sourceCommentsXml));
251
- if (commentsXml !== sourceCommentsXml) updates.set("word/comments.xml", commentsXml);
268
+ if (commentsXml !== sourceCommentsXml) updates.set(commentsPartPath, commentsXml);
252
269
  }
253
- if (hasComments) {
270
+ if (hasComments || ownsCommentsPart) {
254
271
  const ctFile = zip.file("[Content_Types].xml");
255
272
  if (ctFile) {
256
273
  const ctXml = await ctFile.async("text");
257
- if (!ctXml.includes("/word/comments.xml")) updates.set("[Content_Types].xml", ctXml.replace("</Types>", `<Override PartName="/word/comments.xml" ContentType="${COMMENTS_CONTENT_TYPE}"/></Types>`));
274
+ if (!ctXml.toLowerCase().includes("/word/comments.xml")) updates.set("[Content_Types].xml", ctXml.replace("</Types>", `<Override PartName="/${commentsPartPath}" ContentType="${COMMENTS_CONTENT_TYPE}"/></Types>`));
258
275
  }
259
276
  const relsPath = "word/_rels/document.xml.rels";
260
277
  const relsFile = zip.file(relsPath);
261
278
  if (relsFile) {
262
279
  const relsXml = await relsFile.async("text");
263
- if (!relsXml.includes("comments.xml")) {
280
+ if (!relsXml.toLowerCase().includes("comments.xml")) {
264
281
  const maxId = findMaxRId(relsXml);
265
- updates.set(relsPath, relsXml.replace("</Relationships>", `<Relationship Id="rId${maxId + 1}" Type="${RELATIONSHIP_TYPES.comments}" Target="comments.xml"/></Relationships>`));
282
+ updates.set(relsPath, relsXml.replace("</Relationships>", `<Relationship Id="rId${maxId + 1}" Type="${RELATIONSHIP_TYPES.comments}" Target="${commentsPartPath.slice(5)}"/></Relationships>`));
266
283
  }
267
284
  }
268
285
  }
269
286
  if (!await patchCommentsExtended(zip, commentPlan, updates)) return null;
270
287
  if (!await patchNumberingPart(zip, doc, updates)) return null;
288
+ const styles = await planAddedStyles(doc, zip);
289
+ switch (styles.type) {
290
+ case "unchanged": break;
291
+ case "patch":
292
+ updates.set(styles.path, styles.xml);
293
+ break;
294
+ case "materialize":
295
+ options.onDiagnostic?.({
296
+ type: "selectiveSaveRefused",
297
+ part: "word/styles.xml"
298
+ });
299
+ return null;
300
+ default: return styles;
301
+ }
271
302
  for (const [path, xml] of await collectHeaderFooterUpdates(doc, {
272
303
  sourceZip: zip,
273
304
  onDiagnostic: options.onDiagnostic
@@ -35,7 +35,7 @@ function isXmlNameBoundary(char) {
35
35
  */
36
36
  function findParagraphOffsets(xml, paraId) {
37
37
  const escaped = escapeRegExp(paraId);
38
- 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");
39
39
  const matches = [];
40
40
  let match;
41
41
  while ((match = pattern.exec(xml)) !== null) matches.push(match.index);
@@ -191,7 +191,7 @@ function scanParagraphs(xml) {
191
191
  const tagEnd = xml.indexOf(">", tagStart);
192
192
  if (tagEnd === -1) break;
193
193
  const openTag = xml.slice(tagStart, tagEnd + 1);
194
- 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"];
195
195
  const container = {
196
196
  inFallback: depth.fallback > 0,
197
197
  inAlternateContent: depth.alternateContent > 0,
@@ -236,7 +236,7 @@ function countParagraphElements(xml) {
236
236
  */
237
237
  function collectParaIds(xml) {
238
238
  const ids = /* @__PURE__ */ new Map();
239
- const pattern = /w14:paraId="(?<id>[^"]+)"/gu;
239
+ const pattern = /w14:paraId\s*=\s*(?<quote>["'])(?<id>[^"']+)\k<quote>/gu;
240
240
  let match;
241
241
  while ((match = pattern.exec(xml)) !== null) {
242
242
  const id = match.groups["id"];
@@ -245,7 +245,7 @@ function collectParaIds(xml) {
245
245
  return ids;
246
246
  }
247
247
  /** Paragraph ids folio writes and this module may have to remove again. */
248
- const MINTED_PARA_ID_ATTRIBUTE = /\sw14:(?:para|text)Id="[^"]*"/gu;
248
+ const MINTED_PARA_ID_ATTRIBUTE = /\sw14:(paraId|textId)="[^"]*"/gu;
249
249
  /**
250
250
  * The replacement for a paragraph whose source open tag carries no paraId.
251
251
  *
@@ -262,7 +262,9 @@ const MINTED_PARA_ID_ATTRIBUTE = /\sw14:(?:para|text)Id="[^"]*"/gu;
262
262
  const withoutMintedIds = (paragraphXml, sourceOpenTag) => {
263
263
  const tagEnd = paragraphXml.indexOf(">");
264
264
  if (tagEnd === -1) return paragraphXml;
265
- 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);
266
268
  };
267
269
  const storyOf = ({ textBoxDepth }) => textBoxDepth > 0 ? "text-box" : "main";
268
270
  const sameContainer = (a, b) => a.inAlternateContent === b.inAlternateContent && a.textBoxDepth === b.textBoxDepth && a.tableDepth === b.tableDepth;
@@ -1,5 +1,8 @@
1
+ import { document_d_exports } from "../../types/document.js";
1
2
  import { Comment } from "../../types/content.js";
2
3
  //#region src/docx/serializer/commentSerializer.d.ts
4
+ /** An owned empty part survives deletion of its final thread. */
5
+ declare const hasOwnedCommentsPart: (document: document_d_exports.Document) => boolean;
3
6
  /**
4
7
  * What every comment part is written from: one ordered list of comment ids, and
5
8
  * the facts each id carries.
@@ -52,4 +55,4 @@ declare function serializeComments({ order, byId }: CommentPartPlan, sourceBindi
52
55
  */
53
56
  declare function serializeCommentsExtended(plan: CommentPartPlan): string | null;
54
57
  //#endregion
55
- export { CommentPartPlan, planCommentParts, serializeComments, serializeCommentsExtended };
58
+ export { CommentPartPlan, hasOwnedCommentsPart, planCommentParts, serializeComments, serializeCommentsExtended };
@@ -5,12 +5,15 @@ import { serializeParagraph } from "./paragraphSerializer.js";
5
5
  import { serializePartElement } from "./partNamespaces.js";
6
6
  import { serializeTextFormatting } from "./textFormattingSerializer.js";
7
7
  import { escapeXmlAttribute } from "@stll/docx-core";
8
+ import { COMMENT_PART_RELATIONSHIPS } from "@stll/docx-core/model";
8
9
  //#region src/docx/serializer/commentSerializer.ts
9
10
  /**
10
11
  * Comment Serializer
11
12
  *
12
13
  * Serializes Comment[] to OOXML comments.xml format.
13
14
  */
15
+ /** An owned empty part survives deletion of its final thread. */
16
+ const hasOwnedCommentsPart = (document) => [...document.package.relationships?.values() ?? []].some(({ type }) => type === COMMENT_PART_RELATIONSHIPS.comments.type);
14
17
  const DEFAULT_ANNOTATION_REFERENCE_PROPERTIES = "<w:rPr><w:rStyle w:val=\"CommentReference\"/></w:rPr>";
15
18
  const PARAGRAPH_PROPERTIES_END = "</w:pPr>";
16
19
  const serializeAnnotationReference = (formatting) => {
@@ -169,7 +172,7 @@ function buildCommentExtendedEntries({ order, byId, threadParaIdById, threadedId
169
172
  entries.push({
170
173
  paraId,
171
174
  ...parentParaId !== void 0 ? { paraIdParent: parentParaId } : {},
172
- done: done ?? false
175
+ ...done === void 0 ? {} : { done }
173
176
  });
174
177
  }
175
178
  return entries.length > 0 ? entries : null;
@@ -185,7 +188,8 @@ function serializeCommentsExtended(plan) {
185
188
  if (!entries) return null;
186
189
  const body = entries.map((entry) => {
187
190
  const parentAttr = entry.paraIdParent !== void 0 ? ` w15:paraIdParent="${escapeXmlAttribute(entry.paraIdParent)}"` : "";
188
- return `<w15:commentEx w15:paraId="${escapeXmlAttribute(entry.paraId)}"${parentAttr} w15:done="${entry.done ? "1" : "0"}"/>`;
191
+ const doneAttr = entry.done === void 0 ? "" : ` w15:done="${entry.done ? "1" : "0"}"`;
192
+ return `<w15:commentEx w15:paraId="${escapeXmlAttribute(entry.paraId)}"${parentAttr}${doneAttr}/>`;
189
193
  }).join("");
190
194
  return XML_DECLARATION + serializePartElement({
191
195
  partPath: "word/commentsExtended.xml",
@@ -196,4 +200,4 @@ function serializeCommentsExtended(plan) {
196
200
  });
197
201
  }
198
202
  //#endregion
199
- export { planCommentParts, serializeComments, serializeCommentsExtended };
203
+ export { hasOwnedCommentsPart, planCommentParts, serializeComments, serializeCommentsExtended };
@@ -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)