@stll/folio-core 0.44.0 → 0.46.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 (263) hide show
  1. package/dist/ai-edits/__fixtures__/paragraphs.js +2 -2
  2. package/dist/ai-edits/apply.js +59 -19
  3. package/dist/ai-edits/headless.js +2 -1
  4. package/dist/compare/compare.js +2 -5
  5. package/dist/compare/content-alignment.js +78 -53
  6. package/dist/compare/inline-atoms.js +34 -20
  7. package/dist/compare/scenario.js +2 -5
  8. package/dist/compare/types.d.ts +9 -1
  9. package/dist/compare/types.js +12 -1
  10. package/dist/compat/eigenpal.d.ts +2 -1
  11. package/dist/compat/eigenpal.js +2 -1
  12. package/dist/content-controls/checkboxDisplay.d.ts +11 -0
  13. package/dist/content-controls/checkboxDisplay.js +40 -0
  14. package/dist/content-controls/findContentControls.js +3 -1
  15. package/dist/content-controls/mutateContentControls.js +21 -4
  16. package/dist/controller/contentControlWidgetController.d.ts +1 -0
  17. package/dist/controller/contentControlWidgetController.js +3 -0
  18. package/dist/controller/hiddenEditorApi.js +3 -1
  19. package/dist/controller/hiddenEditorManager.d.ts +1 -1
  20. package/dist/controller/hiddenEditorManager.js +6 -3
  21. package/dist/display-list/build/buildDisplayList.js +20 -1
  22. package/dist/display-list/build/imagePrimitives.d.ts +1 -1
  23. package/dist/display-list/build/imagePrimitives.js +3 -1
  24. package/dist/display-list/build/paragraphPrimitives.js +21 -2
  25. package/dist/display-list/dom/renderDisplayListToDom.js +18 -10
  26. package/dist/display-list/types.d.ts +17 -0
  27. package/dist/document-operations.js +14 -3
  28. package/dist/docx/attributeRemainder.d.ts +43 -0
  29. package/dist/docx/attributeRemainder.js +65 -0
  30. package/dist/docx/blockContentParser.d.ts +1 -1
  31. package/dist/docx/blockContentParser.js +87 -63
  32. package/dist/docx/blockPlainText.d.ts +3 -4
  33. package/dist/docx/blockPlainText.js +1 -0
  34. package/dist/docx/bookmarkPlacement.js +2 -0
  35. package/dist/docx/borderParser.js +5 -5
  36. package/dist/docx/commentAnchorIndex.d.ts +30 -0
  37. package/dist/docx/commentAnchorIndex.js +50 -0
  38. package/dist/docx/commentParser.d.ts +1 -1
  39. package/dist/docx/commentParser.js +78 -50
  40. package/dist/docx/commentRangeIntegrity.d.ts +16 -1
  41. package/dist/docx/commentRangeIntegrity.js +42 -1
  42. package/dist/docx/commentRangeJoin.d.ts +9 -0
  43. package/dist/docx/commentRangeJoin.js +33 -0
  44. package/dist/docx/commentReferenceCompletion.d.ts +9 -0
  45. package/dist/docx/commentReferenceCompletion.js +47 -0
  46. package/dist/docx/commentReferenceNormalization.js +1 -0
  47. package/dist/docx/commentReplyMarkers.js +11 -19
  48. package/dist/docx/commentThreadKey.d.ts +18 -0
  49. package/dist/docx/commentThreadKey.js +22 -0
  50. package/dist/docx/compatibility.js +1 -0
  51. package/dist/docx/containerChildren.d.ts +111 -0
  52. package/dist/docx/containerChildren.gen.d.ts +28 -0
  53. package/dist/docx/containerChildren.gen.js +246 -0
  54. package/dist/docx/containerChildren.js +103 -0
  55. package/dist/docx/diagramPreview.js +87 -27
  56. package/dist/docx/documentParser.d.ts +1 -1
  57. package/dist/docx/documentParser.js +3 -3
  58. package/dist/docx/ensureParaIds.js +17 -7
  59. package/dist/docx/fieldParser.d.ts +1 -1
  60. package/dist/docx/fieldParser.js +4 -4
  61. package/dist/docx/fieldState.d.ts +26 -0
  62. package/dist/docx/fieldState.js +53 -0
  63. package/dist/docx/footnoteParser.d.ts +1 -1
  64. package/dist/docx/graphicFrameLocks.d.ts +16 -4
  65. package/dist/docx/graphicFrameLocks.js +19 -6
  66. package/dist/docx/groupDrawingParser.js +3 -3
  67. package/dist/docx/headerFooterParser.js +1 -1
  68. package/dist/docx/headerFooterReferenceNormalization.js +1 -0
  69. package/dist/docx/hyperlinkParser.d.ts +27 -8
  70. package/dist/docx/hyperlinkParser.js +78 -20
  71. package/dist/docx/imageParser.d.ts +9 -1
  72. package/dist/docx/imageParser.js +91 -20
  73. package/dist/docx/imageRawXml.d.ts +14 -1
  74. package/dist/docx/imageRawXml.js +30 -6
  75. package/dist/docx/inlineWrapperContent.d.ts +85 -0
  76. package/dist/docx/inlineWrapperContent.js +84 -0
  77. package/dist/docx/mathToMathml.js +12 -14
  78. package/dist/docx/nonVisualDrawingProps.d.ts +34 -0
  79. package/dist/docx/nonVisualDrawingProps.js +46 -0
  80. package/dist/docx/normalizeBaseDirection.js +10 -1
  81. package/dist/docx/paraIdAttribute.d.ts +21 -0
  82. package/dist/docx/paraIdAttribute.js +64 -0
  83. package/dist/docx/paragraphParser.d.ts +1 -1
  84. package/dist/docx/paragraphParser.js +277 -168
  85. package/dist/docx/paragraphPropertySource.js +5 -1
  86. package/dist/docx/paragraphTextBoxEnrichment.d.ts +1 -1
  87. package/dist/docx/paragraphTextBoxEnrichment.js +9 -52
  88. package/dist/docx/paragraphTraversal.js +7 -4
  89. package/dist/docx/parseWarningMessage.js +2 -1
  90. package/dist/docx/parser.js +2 -2
  91. package/dist/docx/preservedRunContent.d.ts +32 -0
  92. package/dist/docx/preservedRunContent.js +86 -0
  93. package/dist/docx/previewBudget.d.ts +64 -0
  94. package/dist/docx/previewBudget.js +88 -0
  95. package/dist/docx/renderedPageBreakNormalization.js +2 -4
  96. package/dist/docx/revisionIdNormalization.js +17 -5
  97. package/dist/docx/rezip.js +91 -36
  98. package/dist/docx/runParser.d.ts +1 -1
  99. package/dist/docx/runParser.js +16 -12
  100. package/dist/docx/sdtPropertiesPatch.js +24 -18
  101. package/dist/docx/sectionParser.js +8 -1
  102. package/dist/docx/sectionReferenceHistory.js +2 -2
  103. package/dist/docx/selectiveSave.js +6 -6
  104. package/dist/docx/selectiveXmlPatch.d.ts +46 -2
  105. package/dist/docx/selectiveXmlPatch.js +86 -39
  106. package/dist/docx/serializer/blockSdtSerializer.js +38 -26
  107. package/dist/docx/serializer/borderSerializer.d.ts +1 -1
  108. package/dist/docx/serializer/borderSerializer.js +15 -14
  109. package/dist/docx/serializer/commentSerializer.d.ts +41 -16
  110. package/dist/docx/serializer/commentSerializer.js +87 -92
  111. package/dist/docx/serializer/documentSerializer.js +8 -11
  112. package/dist/docx/serializer/fontTableSerializer.js +6 -6
  113. package/dist/docx/serializer/headerFooterSerializer.js +12 -14
  114. package/dist/docx/serializer/markupRangeAttributes.js +2 -2
  115. package/dist/docx/serializer/noteSerializer.js +8 -8
  116. package/dist/docx/serializer/numberingSerializer.js +7 -6
  117. package/dist/docx/serializer/paragraphSerializer.js +93 -72
  118. package/dist/docx/serializer/partNamespaces.js +2 -2
  119. package/dist/docx/serializer/runSerializer.js +73 -44
  120. package/dist/docx/serializer/sectionPropertiesSerializer.js +19 -14
  121. package/dist/docx/serializer/settingsSerializer.js +4 -3
  122. package/dist/docx/serializer/stylesSerializer.js +6 -6
  123. package/dist/docx/serializer/tableSerializer.js +28 -17
  124. package/dist/docx/serializer/textFormattingSerializer.js +29 -28
  125. package/dist/docx/serializer/themeSerializer.js +6 -6
  126. package/dist/docx/serializer/trackedChangeAttributes.js +2 -2
  127. package/dist/docx/serializer/xmlUtils.d.ts +1 -2
  128. package/dist/docx/serializer/xmlUtils.js +1 -13
  129. package/dist/docx/server/boundedArchive.d.ts +12 -0
  130. package/dist/docx/server/boundedArchive.js +20 -1
  131. package/dist/docx/server/createBilingualDocument.js +3 -1
  132. package/dist/docx/server/materializeYjsDocx.d.ts +1 -1
  133. package/dist/docx/server/materializeYjsDocx.js +9 -1
  134. package/dist/docx/server/migrateYjsAttrSchema.d.ts +55 -0
  135. package/dist/docx/server/migrateYjsAttrSchema.js +95 -0
  136. package/dist/docx/server/validateDocxConformance.js +22 -1
  137. package/dist/docx/shapeParser.js +12 -10
  138. package/dist/docx/tableParser.d.ts +1 -1
  139. package/dist/docx/tableParser.js +206 -85
  140. package/dist/docx/textBoxParser.d.ts +25 -2
  141. package/dist/docx/textBoxParser.js +59 -4
  142. package/dist/docx/unzip.d.ts +23 -0
  143. package/dist/docx/unzip.js +37 -26
  144. package/dist/docx/verbatimCapture.js +1 -1
  145. package/dist/docx/vmlImageParser.d.ts +17 -1
  146. package/dist/docx/vmlImageParser.js +20 -3
  147. package/dist/docx/vmlPreview.d.ts +1 -3
  148. package/dist/docx/vmlPreview.js +2 -30
  149. package/dist/docx/xmlEncoding.d.ts +5 -0
  150. package/dist/docx/xmlEncoding.js +12 -0
  151. package/dist/docx/xmlParser.d.ts +26 -2
  152. package/dist/docx/xmlParser.js +70 -27
  153. package/dist/docx/xmlResourceLimits.d.ts +89 -9
  154. package/dist/docx/xmlResourceLimits.js +105 -24
  155. package/dist/headless-layout.js +10 -1
  156. package/dist/index.d.ts +2 -1
  157. package/dist/index.js +2 -1
  158. package/dist/internal/pageBreakRunSourceDescendantIndex.js +5 -2
  159. package/dist/internal/paragraphFormattingSerialization.js +3 -2
  160. package/dist/layout-bridge/convert/toFlowBlocks.js +102 -12
  161. package/dist/layout-engine/measure/tableCellFloating.d.ts +2 -0
  162. package/dist/layout-engine/measure/tableCellFloating.js +2 -0
  163. package/dist/layout-engine/types.d.ts +23 -1
  164. package/dist/layout-painter/renderImage.d.ts +5 -1
  165. package/dist/layout-painter/renderImage.js +14 -6
  166. package/dist/layout-painter/renderPage.d.ts +2 -0
  167. package/dist/layout-painter/renderPage.js +4 -0
  168. package/dist/layout-painter/renderParagraph.d.ts +13 -1
  169. package/dist/layout-painter/renderParagraph.js +26 -6
  170. package/dist/managers/autoSaveCodec.d.ts +9 -1
  171. package/dist/managers/autoSaveCodec.js +14 -12
  172. package/dist/markdown/images.js +1 -4
  173. package/dist/markdown/renderBlock.js +1 -0
  174. package/dist/markdown/renderRuns.d.ts +1 -1
  175. package/dist/markdown/renderRuns.js +28 -5
  176. package/dist/markdown/renderTable.js +22 -5
  177. package/dist/markdown/trailers.js +4 -1
  178. package/dist/pdf/images.js +142 -6
  179. package/dist/prosemirror/attrs/index.d.ts +14 -3
  180. package/dist/prosemirror/attrs/index.js +224 -2
  181. package/dist/prosemirror/authoredTransformAttrs.d.ts +28 -0
  182. package/dist/prosemirror/authoredTransformAttrs.js +64 -0
  183. package/dist/prosemirror/commands/contentControls.js +10 -2
  184. package/dist/prosemirror/commands/image.d.ts +9 -2
  185. package/dist/prosemirror/commands/image.js +43 -28
  186. package/dist/prosemirror/commands/pageBreak.js +1 -1
  187. package/dist/prosemirror/commentReferenceAttrs.d.ts +8 -0
  188. package/dist/prosemirror/commentReferenceAttrs.js +33 -0
  189. package/dist/prosemirror/commentReferenceIntegrity.d.ts +47 -0
  190. package/dist/prosemirror/commentReferenceIntegrity.js +43 -0
  191. package/dist/prosemirror/conversion/fromProseDoc.d.ts +2 -13
  192. package/dist/prosemirror/conversion/fromProseDoc.js +338 -99
  193. package/dist/prosemirror/conversion/index.d.ts +2 -2
  194. package/dist/prosemirror/conversion/toProseDoc.d.ts +7 -9
  195. package/dist/prosemirror/conversion/toProseDoc.js +551 -327
  196. package/dist/prosemirror/extensions/StarterKit.js +8 -58
  197. package/dist/prosemirror/extensions/core/DocExtension.js +1 -1
  198. package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -1
  199. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +45 -27
  200. package/dist/prosemirror/extensions/features/EmptyParagraphFormatExtension.js +3 -1
  201. package/dist/prosemirror/extensions/features/ImagePasteExtension.js +5 -3
  202. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +1 -0
  203. package/dist/prosemirror/extensions/markRegistry.d.ts +41 -0
  204. package/dist/prosemirror/extensions/markRegistry.js +75 -0
  205. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +2 -3
  206. package/dist/prosemirror/extensions/marks/InlineWrapperExtension.d.ts +18 -0
  207. package/dist/prosemirror/extensions/marks/InlineWrapperExtension.js +63 -0
  208. package/dist/prosemirror/extensions/marks/markUtils.d.ts +2 -1
  209. package/dist/prosemirror/extensions/marks/markUtils.js +47 -4
  210. package/dist/prosemirror/extensions/nodes/CommentReferenceExtension.d.ts +11 -0
  211. package/dist/prosemirror/extensions/nodes/CommentReferenceExtension.js +87 -0
  212. package/dist/prosemirror/extensions/nodes/FieldExtension.js +9 -7
  213. package/dist/prosemirror/extensions/nodes/ImageExtension.js +20 -0
  214. package/dist/prosemirror/extensions/nodes/PreservedBlockExtension.d.ts +32 -0
  215. package/dist/prosemirror/extensions/nodes/PreservedBlockExtension.js +67 -0
  216. package/dist/prosemirror/extensions/nodes/PreservedXmlExtension.d.ts +16 -0
  217. package/dist/prosemirror/extensions/nodes/PreservedXmlExtension.js +60 -0
  218. package/dist/prosemirror/extensions/nodes/ShapeExtension.js +10 -2
  219. package/dist/prosemirror/extensions/nodes/TableExtension.js +3 -2
  220. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +13 -5
  221. package/dist/prosemirror/inlineWrapperStack.d.ts +39 -0
  222. package/dist/prosemirror/inlineWrapperStack.js +75 -0
  223. package/dist/prosemirror/pageBreakRunProjection.d.ts +17 -7
  224. package/dist/prosemirror/pageBreakRunProjection.js +19 -9
  225. package/dist/prosemirror/paragraphFormattingProvenance.d.ts +6 -3
  226. package/dist/prosemirror/paragraphFormattingProvenance.js +12 -3
  227. package/dist/prosemirror/replacedAnnotations.d.ts +59 -0
  228. package/dist/prosemirror/replacedAnnotations.js +165 -0
  229. package/dist/prosemirror/runFormattingInlineCarriers.d.ts +3 -1
  230. package/dist/prosemirror/runFormattingInlineCarriers.js +5 -1
  231. package/dist/prosemirror/schema/index.d.ts +3 -3
  232. package/dist/prosemirror/schema/marks.d.ts +25 -1
  233. package/dist/prosemirror/schema/nodes.d.ts +139 -3
  234. package/dist/prosemirror/schema/nodes.js +16 -1
  235. package/dist/prosemirror/trackedRunInlineAtoms.d.ts +2 -0
  236. package/dist/prosemirror/trackedRunInlineAtoms.js +2 -0
  237. package/dist/prosemirror/validation.js +16 -2
  238. package/dist/prosemirror/yjsDocumentMetadata.d.ts +73 -0
  239. package/dist/prosemirror/yjsDocumentMetadata.js +171 -0
  240. package/dist/prosemirror/zeroWidthAnchors.js +2 -0
  241. package/dist/render-dom/commentAnchorAttributes.d.ts +37 -0
  242. package/dist/render-dom/commentAnchorAttributes.js +54 -0
  243. package/dist/server.d.ts +3 -1
  244. package/dist/server.js +3 -1
  245. package/dist/types/content.d.ts +2 -2
  246. package/dist/utils/base64.d.ts +36 -0
  247. package/dist/utils/base64.js +40 -0
  248. package/dist/utils/clipboard.d.ts +9 -1
  249. package/dist/utils/clipboard.js +29 -2
  250. package/dist/utils/findReplace.js +1 -1
  251. package/dist/utils/imageLuminance.d.ts +15 -0
  252. package/dist/utils/imageLuminance.js +32 -0
  253. package/dist/utils/mergeDocumentContent.js +7 -17
  254. package/dist/utils/replaceText.js +1 -0
  255. package/dist/utils/units.d.ts +10 -1
  256. package/dist/utils/units.js +12 -1
  257. package/dist/utils/urlSecurity.d.ts +8 -2
  258. package/dist/utils/urlSecurity.js +21 -3
  259. package/package.json +2 -2
  260. package/dist/docx/blockRangeMarkers.d.ts +0 -36
  261. package/dist/docx/blockRangeMarkers.js +0 -59
  262. package/dist/prosemirror/yjsParagraphSourceContract.d.ts +0 -9
  263. package/dist/prosemirror/yjsParagraphSourceContract.js +0 -26
@@ -1,3 +1,4 @@
1
+ import { patchBreaksCommentRangeBalance } from "./commentRangeIntegrity.js";
1
2
  import { captureVerbatimXml } from "./verbatimCapture.js";
2
3
  import { WORDPROCESSINGML_NAMESPACE_URIS, cloneElement, findAttributeByNamespaceUri, getAttributeByNamespaceUri, getChildElements, getLocalName, getNamespacePrefix, getNamespaceUri, parseXmlDocument } from "./xmlParser.js";
3
4
  //#region src/docx/selectiveXmlPatch.ts
@@ -237,10 +238,27 @@ function buildPatchedNoteXml(originalXml, serializedXml, changedIds) {
237
238
  return spliceChangedParagraphs(originalXml, serializedXml, changedIds);
238
239
  }
239
240
  /**
241
+ * Apply `splices` to `xml`, end-to-start so earlier offsets stay valid.
242
+ *
243
+ * Every selective patch is a splice, and every splice goes through here, so
244
+ * the refusal below is a property of the operation rather than a check each
245
+ * site has to remember. A patch rewrites the regions an edit touched and keeps
246
+ * the rest of the part byte-for-byte, so it can write half a comment range:
247
+ * invalid OOXML that anchors the comment to nothing. Answers null when it
248
+ * would, leaving the caller to rewrite a wider region — ultimately the whole
249
+ * part from the model, which is balanced with itself.
250
+ */
251
+ const spliceXml = (xml, splices) => {
252
+ let result = xml;
253
+ for (const { start, end, newXml } of [...splices].toSorted((a, b) => b.start - a.start)) result = result.slice(0, start) + newXml + result.slice(end);
254
+ return patchBreaksCommentRangeBalance(xml, result) ? null : result;
255
+ };
256
+ /**
240
257
  * Replace each changed paragraph in `originalXml` with its re-serialized form
241
- * extracted from `serializedXml`, splicing end-to-start so earlier offsets stay
242
- * valid. Assumes safety has already been validated. Returns null if an offset
243
- * or extraction unexpectedly fails.
258
+ * extracted from `serializedXml`. Assumes safety has already been validated.
259
+ * Returns null if an offset or extraction unexpectedly fails, or if
260
+ * {@link spliceXml} refuses the result; the caller then falls back to a full
261
+ * repack, whose parts are all re-serialized from one model.
244
262
  */
245
263
  function spliceChangedParagraphs(originalXml, serializedXml, changedIds) {
246
264
  const replacements = [];
@@ -255,10 +273,7 @@ function spliceChangedParagraphs(originalXml, serializedXml, changedIds) {
255
273
  newXml
256
274
  });
257
275
  }
258
- replacements.sort((a, b) => b.start - a.start);
259
- let result = originalXml;
260
- for (const { start, end, newXml } of replacements) result = result.slice(0, start) + newXml + result.slice(end);
261
- return result;
276
+ return spliceXml(originalXml, replacements);
262
277
  }
263
278
  /**
264
279
  * Depth-count the end of the element that opens at `start` (an `<openLiteral…>`
@@ -424,11 +439,6 @@ const collectChangedNoteParaIds = (baselineXml, currentXml) => {
424
439
  }
425
440
  return changed;
426
441
  };
427
- const replaceRanges = (xml, replacements) => {
428
- let result = xml;
429
- for (const { start, end, newXml } of [...replacements].toSorted((a, b) => b.start - a.start)) result = result.slice(0, start) + newXml + result.slice(end);
430
- return result;
431
- };
432
442
  /**
433
443
  * Patch an existing note part from its model serialization.
434
444
  *
@@ -440,56 +450,79 @@ const replaceRanges = (xml, replacements) => {
440
450
  * notes, and unaffected equal-shape paragraphs remain byte-exact.
441
451
  * `replacementXml` also supplies synthesized automatic note-reference marks,
442
452
  * which the parsed model intentionally omits.
453
+ *
454
+ * A comment can be anchored on a note's own text, so its range spans that
455
+ * note's paragraphs and an edit inside the span moves a range half from one
456
+ * paragraph to another. Replacing only the dirty paragraph then drops the half
457
+ * it held and leaves the other standing, so {@link spliceXml} refuses the
458
+ * result and the changed notes are rewritten whole instead — model content on
459
+ * both sides of the range, with every other note still byte-exact.
443
460
  */
444
461
  function buildPatchedNotePartXml({ originalXml, baselineXml, serializedXml, replacementXml, elementName, changedParaIds }) {
445
462
  const currentElements = collectNoteElementSyntax(serializedXml, elementName);
446
463
  const originalElements = collectNoteElementSyntax(originalXml, elementName);
447
464
  const replacementElements = collectNoteElementSyntax(replacementXml, elementName);
448
465
  const replacementXmlnsDeclarations = collectXmlnsFromOpeningTag(replacementXml);
449
- const ordinalReplacements = [];
466
+ const paragraphSplices = [];
467
+ /** The same edits at note granularity, should the paragraph splices be refused. */
468
+ const noteSplices = [];
450
469
  const serializedParaIds = collectParaIds(serializedXml);
451
470
  const effectiveChangedParaIds = changedParaIds ?? collectChangedNoteParaIds(baselineXml, serializedXml);
452
471
  const unroutedChangedParaIds = new Set([...effectiveChangedParaIds].filter((paraId) => serializedParaIds.has(paraId)));
453
472
  for (const [id, currentSyntaxEntries] of currentElements) {
454
473
  const originalSyntaxEntries = originalElements.get(id);
455
474
  const replacementSyntaxEntries = replacementElements.get(id);
456
- if (currentSyntaxEntries.length !== 1 || originalSyntaxEntries?.length !== 1 || replacementSyntaxEntries?.length !== 1) return null;
475
+ if (currentSyntaxEntries.length !== 1 || originalSyntaxEntries?.length !== 1 || replacementSyntaxEntries?.length !== 1) return {
476
+ type: "refused",
477
+ reason: "unroutable-paragraph"
478
+ };
457
479
  const currentSyntax = currentSyntaxEntries[0];
458
480
  const originalSyntax = originalSyntaxEntries[0];
459
481
  const replacementSyntax = replacementSyntaxEntries[0];
460
- if (!currentSyntax || !originalSyntax || !replacementSyntax) return null;
482
+ if (!currentSyntax || !originalSyntax || !replacementSyntax) return {
483
+ type: "refused",
484
+ reason: "unroutable-paragraph"
485
+ };
461
486
  const currentNote = extractNoteElement(serializedXml, currentSyntax, id);
462
487
  const originalOffsets = findNoteElement(originalXml, originalSyntax, id);
463
488
  const replacementNote = extractNoteElement(replacementXml, replacementSyntax, id);
464
- if (!currentNote || !originalOffsets || !replacementNote) return null;
489
+ if (!currentNote || !originalOffsets || !replacementNote) return {
490
+ type: "refused",
491
+ reason: "unroutable-paragraph"
492
+ };
465
493
  const originalNote = originalXml.slice(originalOffsets.start, originalOffsets.end);
466
494
  const currentParagraphs = paragraphRanges(currentNote, currentSyntax.elementPrefix);
467
495
  const originalParagraphs = paragraphRanges(originalNote, originalSyntax.elementPrefix);
468
496
  const replacementParagraphs = paragraphRanges(replacementNote, replacementSyntax.elementPrefix);
469
497
  const noteChangedParaIds = [...collectParaIds(currentNote).keys()].filter((paraId) => unroutedChangedParaIds.has(paraId));
470
498
  if (noteChangedParaIds.length === 0) continue;
499
+ const wholeNoteSplice = {
500
+ start: originalOffsets.start,
501
+ end: originalOffsets.end,
502
+ newXml: rewriteWordprocessingPrefixes(replacementNote, {
503
+ source: replacementSyntax,
504
+ target: originalSyntax,
505
+ sourceXmlnsDeclarations: replacementXmlnsDeclarations
506
+ })
507
+ };
508
+ noteSplices.push(wholeNoteSplice);
471
509
  if (currentParagraphs.length !== originalParagraphs.length || currentParagraphs.length !== replacementParagraphs.length) {
472
510
  for (const paraId of noteChangedParaIds) unroutedChangedParaIds.delete(paraId);
473
- ordinalReplacements.push({
474
- start: originalOffsets.start,
475
- end: originalOffsets.end,
476
- newXml: rewriteWordprocessingPrefixes(replacementNote, {
477
- source: replacementSyntax,
478
- target: originalSyntax,
479
- sourceXmlnsDeclarations: replacementXmlnsDeclarations
480
- })
481
- });
511
+ paragraphSplices.push(wholeNoteSplice);
482
512
  continue;
483
513
  }
484
514
  for (let index = 0; index < currentParagraphs.length; index++) {
485
515
  const currentRange = currentParagraphs[index];
486
516
  const originalRange = originalParagraphs[index];
487
517
  const replacementRange = replacementParagraphs[index];
488
- if (!currentRange || !originalRange || !replacementRange) return null;
518
+ if (!currentRange || !originalRange || !replacementRange) return {
519
+ type: "refused",
520
+ reason: "unroutable-paragraph"
521
+ };
489
522
  const routedIds = [...collectParaIds(currentNote.slice(currentRange.start, currentRange.end)).keys()].filter((paraId) => unroutedChangedParaIds.has(paraId));
490
523
  if (routedIds.length === 0) continue;
491
524
  for (const paraId of routedIds) unroutedChangedParaIds.delete(paraId);
492
- ordinalReplacements.push({
525
+ paragraphSplices.push({
493
526
  start: originalOffsets.start + originalRange.start,
494
527
  end: originalOffsets.start + originalRange.end,
495
528
  newXml: rewriteWordprocessingPrefixes(replacementNote.slice(replacementRange.start, replacementRange.end), {
@@ -500,8 +533,23 @@ function buildPatchedNotePartXml({ originalXml, baselineXml, serializedXml, repl
500
533
  });
501
534
  }
502
535
  }
503
- if (unroutedChangedParaIds.size > 0) return null;
504
- return replaceRanges(originalXml, ordinalReplacements);
536
+ if (unroutedChangedParaIds.size > 0) return {
537
+ type: "refused",
538
+ reason: "unroutable-paragraph"
539
+ };
540
+ const patched = spliceXml(originalXml, paragraphSplices);
541
+ if (patched !== null) return {
542
+ type: "patched",
543
+ xml: patched
544
+ };
545
+ const wholeNotes = spliceXml(originalXml, noteSplices);
546
+ return wholeNotes === null ? {
547
+ type: "refused",
548
+ reason: "comment-range-balance"
549
+ } : {
550
+ type: "patched",
551
+ xml: wholeNotes
552
+ };
505
553
  }
506
554
  /**
507
555
  * The full range of the first `<openLiteral …>…</closeTag>` element, or null.
@@ -695,17 +743,19 @@ function restoreLevelNumFmts(originalDefXml, currentDefXml) {
695
743
  ...collectXmlnsFromOpeningTag(origLevel)
696
744
  });
697
745
  else replacement = reconstructCustomNumFmt(currentWidth);
698
- const restoredLevel = curLevel.slice(0, synthetic.index) + replacement + curLevel.slice(synthetic.index + synthetic[0].length);
746
+ const restoredLevel = spliceXml(curLevel, [{
747
+ start: synthetic.index,
748
+ end: synthetic.index + synthetic[0].length,
749
+ newXml: replacement
750
+ }]);
751
+ if (restoredLevel === null) continue;
699
752
  replacements.push({
700
753
  start: curOffsets.start,
701
754
  end: curOffsets.end,
702
755
  newXml: restoredLevel
703
756
  });
704
757
  }
705
- replacements.sort((a, b) => b.start - a.start);
706
- let result = currentDefXml;
707
- for (const { start, end, newXml } of replacements) result = result.slice(0, start) + newXml + result.slice(end);
708
- return result;
758
+ return spliceXml(currentDefXml, replacements) ?? currentDefXml;
709
759
  }
710
760
  /**
711
761
  * Build a patched `word/numbering.xml` by splicing the changed `w:abstractNum` /
@@ -733,10 +783,7 @@ function buildPatchedNumberingXml(originalXml, currentXml, changed) {
733
783
  return true;
734
784
  };
735
785
  if (!collect("abstractNum", changed.abstractNums) || !collect("num", changed.nums)) return null;
736
- replacements.sort((a, b) => b.start - a.start);
737
- let result = originalXml;
738
- for (const { start, end, newXml } of replacements) result = result.slice(0, start) + newXml + result.slice(end);
739
- return result;
786
+ return spliceXml(originalXml, replacements);
740
787
  }
741
788
  /**
742
789
  * Numbering definitions present in `currentXml` (the model's serialization)
@@ -872,4 +919,4 @@ function escapeRegExp(str) {
872
919
  return str.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
873
920
  }
874
921
  //#endregion
875
- export { appendNumberingDefs, buildParagraphOffsetIndex, buildPatchedDocumentXml, buildPatchedNotePartXml, buildPatchedNoteXml, buildPatchedNumberingXml, collectAddedNumberingDefs, collectChangedNoteParaIds, collectChangedNumberingDefs, collectParaIds, countParagraphElements, extractParagraphXml, findParagraphOffsets, isXmlNameBoundary, patchNumberingDefinitions, validatePatchSafety };
922
+ export { appendNumberingDefs, buildParagraphOffsetIndex, buildPatchedDocumentXml, buildPatchedNotePartXml, buildPatchedNoteXml, buildPatchedNumberingXml, collectAddedNumberingDefs, collectChangedNoteParaIds, collectChangedNumberingDefs, collectParaIds, countParagraphElements, extractParagraphXml, findParagraphOffsets, isXmlNameBoundary, patchNumberingDefinitions, spliceXml, validatePatchSafety };
@@ -1,24 +1,39 @@
1
1
  import { reconcileRawSdtPr } from "../sdtPropertiesPatch.js";
2
2
  import { isSingleWellFormedElement } from "./xmlUtils.js";
3
+ import { escapeXmlAttribute } from "@stll/docx-core";
3
4
  //#region src/docx/serializer/blockSdtSerializer.ts
4
- function escapeXmlAttr(value) {
5
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
6
- }
5
+ /**
6
+ * Block-level SDT serializer.
7
+ *
8
+ * Replays `<w:sdtPr>` (and `<w:sdtEndPr>` when present) verbatim from the
9
+ * `rawPropertiesXml` / `rawEndPropertiesXml` strings captured by the parser.
10
+ * That keeps OOXML element order intact (`CT_SdtPr` is an `xsd:sequence`,
11
+ * ECMA-376 §17.5.2) and round-trips unmodeled features — `w:dataBinding`,
12
+ * `w15:repeatingSection`, `@lastValue`, custom XML mappings — without us
13
+ * having to enumerate them.
14
+ *
15
+ * If a `BlockSdt` was constructed programmatically (no parsed snapshot to
16
+ * replay), fall back to a minimal projection from the modeled fields so the
17
+ * result is still a valid `<w:sdt>`.
18
+ *
19
+ * Sharing the helper between the document body and the header/footer
20
+ * serializers keeps body↔HF parity in one place.
21
+ */
7
22
  function serializeFallbackSdtPr(props) {
8
23
  const parts = [];
9
24
  if (props.id !== void 0) parts.push(`<w:id w:val="${props.id}"/>`);
10
- if (props.alias) parts.push(`<w:alias w:val="${escapeXmlAttr(props.alias)}"/>`);
11
- if (props.tag) parts.push(`<w:tag w:val="${escapeXmlAttr(props.tag)}"/>`);
25
+ if (props.alias) parts.push(`<w:alias w:val="${escapeXmlAttribute(props.alias)}"/>`);
26
+ if (props.tag) parts.push(`<w:tag w:val="${escapeXmlAttribute(props.tag)}"/>`);
12
27
  if (props.lock) parts.push(`<w:lock w:val="${props.lock}"/>`);
13
- if (props.placeholder) parts.push(`<w:placeholder><w:docPart w:val="${escapeXmlAttr(props.placeholder)}"/></w:placeholder>`);
28
+ if (props.placeholder) parts.push(`<w:placeholder><w:docPart w:val="${escapeXmlAttribute(props.placeholder)}"/></w:placeholder>`);
14
29
  if (props.showingPlaceholder) parts.push("<w:showingPlcHdr/>");
15
30
  switch (props.sdtType) {
16
31
  case "plainText":
17
32
  parts.push("<w:text/>");
18
33
  break;
19
34
  case "date": {
20
- const fullDateAttr = props.dateValueISO ? ` w:fullDate="${escapeXmlAttr(props.dateValueISO)}"` : "";
21
- const formatChild = props.dateFormat ? `<w:dateFormat w:val="${escapeXmlAttr(props.dateFormat)}"/>` : "";
35
+ const fullDateAttr = props.dateValueISO ? ` w:fullDate="${escapeXmlAttribute(props.dateValueISO)}"` : "";
36
+ const formatChild = props.dateFormat ? `<w:dateFormat w:val="${escapeXmlAttribute(props.dateFormat)}"/>` : "";
22
37
  if (fullDateAttr || formatChild) parts.push(`<w:date${fullDateAttr}>${formatChild}</w:date>`);
23
38
  else parts.push("<w:date/>");
24
39
  break;
@@ -26,7 +41,7 @@ function serializeFallbackSdtPr(props) {
26
41
  case "dropdown":
27
42
  case "comboBox": {
28
43
  const tag = props.sdtType === "dropdown" ? "w:dropDownList" : "w:comboBox";
29
- const items = (props.listItems ?? []).map((item) => `<w:listItem w:displayText="${escapeXmlAttr(item.displayText)}" w:value="${escapeXmlAttr(item.value)}"/>`).join("");
44
+ const items = (props.listItems ?? []).map((item) => `<w:listItem w:displayText="${escapeXmlAttribute(item.displayText)}" w:value="${escapeXmlAttribute(item.value)}"/>`).join("");
30
45
  parts.push(`<${tag}>${items}</${tag}>`);
31
46
  break;
32
47
  }
@@ -48,25 +63,22 @@ function serializeFallbackSdtPr(props) {
48
63
  }
49
64
  return `<w:sdtPr>${parts.join("")}</w:sdtPr>`;
50
65
  }
66
+ /**
67
+ * `properties.dropdownLastValue` is the only record of what was selected.
68
+ *
69
+ * The XSD default of `@w:lastValue` is the empty string, so "never selected",
70
+ * "cleared" and "selected" are three distinguishable states, and the body's
71
+ * display text is evidence for none of them: it is equally the placeholder of
72
+ * a dropdown nobody has touched, and a displayText shared by two list items
73
+ * picks the wrong sibling. The parser records the authored `@w:lastValue` and
74
+ * `setContentControlValue` records a pick; what neither wrote is no selection.
75
+ *
76
+ * `""` is a value a producer can author (`<w:listItem w:value=""/>`), so
77
+ * presence is the test, not truthiness.
78
+ */
51
79
  function extractDropdownLastValue(blockSdt) {
52
80
  if (blockSdt.properties.sdtType !== "dropdown" && blockSdt.properties.sdtType !== "comboBox") return;
53
- const modeled = blockSdt.properties.dropdownLastValue;
54
- if (modeled !== void 0) return modeled;
55
- if (blockSdt.properties.rawPropertiesXml !== void 0) return;
56
- const firstBlock = blockSdt.content[0];
57
- if (!firstBlock || firstBlock.type !== "paragraph") return;
58
- const parts = [];
59
- for (const child of firstBlock.content) if (child.type === "run") {
60
- for (const item of child.content) if (item.type === "text") parts.push(item.text);
61
- }
62
- const text = parts.join("");
63
- if (text.length === 0) return;
64
- const items = blockSdt.properties.listItems;
65
- if (items) {
66
- const match = items.find((item) => item.displayText === text);
67
- if (match) return match.value;
68
- }
69
- return text;
81
+ return blockSdt.properties.dropdownLastValue;
70
82
  }
71
83
  function extractDateFullDate(blockSdt) {
72
84
  if (blockSdt.properties.sdtType !== "date") return;
@@ -18,7 +18,7 @@ type ExhaustiveBorder = document_d_exports.ExhaustiveFields<document_d_exports.B
18
18
  *
19
19
  * `style` and the color values come straight from the parsed DOCX (the parser
20
20
  * casts `w:val`/`w:color` without validating the enum), so they are
21
- * untrusted and are `escapeXml`'d before re-entering XML attributes; for valid
21
+ * untrusted and are `escapeXmlAttribute`'d before re-entering XML attributes; for valid
22
22
  * documents these are enum/hex values, so escaping is a no-op.
23
23
  */
24
24
  declare function serializeBorder(input: ExhaustiveBorder | undefined, elementName: string): string;
@@ -1,4 +1,5 @@
1
- import { escapeXml, intAttr } from "./xmlUtils.js";
1
+ import { intAttr } from "./xmlUtils.js";
2
+ import { escapeXmlAttribute } from "@stll/docx-core";
2
3
  //#region src/docx/serializer/borderSerializer.ts
3
4
  /**
4
5
  * Serialize a single border element (`<w:top .../>`, `<w:left .../>`, ...).
@@ -16,30 +17,30 @@ import { escapeXml, intAttr } from "./xmlUtils.js";
16
17
  *
17
18
  * `style` and the color values come straight from the parsed DOCX (the parser
18
19
  * casts `w:val`/`w:color` without validating the enum), so they are
19
- * untrusted and are `escapeXml`'d before re-entering XML attributes; for valid
20
+ * untrusted and are `escapeXmlAttribute`'d before re-entering XML attributes; for valid
20
21
  * documents these are enum/hex values, so escaping is a no-op.
21
22
  */
22
23
  function serializeBorder(input, elementName) {
23
24
  if (!input) return "";
24
25
  const { style, color, size, space, shadow, frame, artRelationshipId, topLeftArtRelationshipId, topRightArtRelationshipId, bottomLeftArtRelationshipId, bottomRightArtRelationshipId } = input;
25
- const attrs = [`w:val="${escapeXml(style)}"`];
26
+ const attrs = [`w:val="${escapeXmlAttribute(style)}"`];
26
27
  if (size !== void 0) attrs.push(`w:sz="${intAttr(size)}"`);
27
28
  if (space !== void 0) attrs.push(`w:space="${intAttr(space)}"`);
28
29
  if (color) {
29
30
  const { auto, rgb, themeColor, themeTint, themeShade } = color;
30
31
  if (auto) attrs.push("w:color=\"auto\"");
31
- else if (rgb) attrs.push(`w:color="${escapeXml(rgb)}"`);
32
- if (themeColor) attrs.push(`w:themeColor="${escapeXml(themeColor)}"`);
33
- if (themeTint) attrs.push(`w:themeTint="${escapeXml(themeTint)}"`);
34
- if (themeShade) attrs.push(`w:themeShade="${escapeXml(themeShade)}"`);
32
+ else if (rgb) attrs.push(`w:color="${escapeXmlAttribute(rgb)}"`);
33
+ if (themeColor) attrs.push(`w:themeColor="${escapeXmlAttribute(themeColor)}"`);
34
+ if (themeTint) attrs.push(`w:themeTint="${escapeXmlAttribute(themeTint)}"`);
35
+ if (themeShade) attrs.push(`w:themeShade="${escapeXmlAttribute(themeShade)}"`);
35
36
  }
36
- if (shadow) attrs.push("w:shadow=\"true\"");
37
- if (frame) attrs.push("w:frame=\"true\"");
38
- if (artRelationshipId) attrs.push(`w:id="${escapeXml(artRelationshipId)}"`);
39
- if (topLeftArtRelationshipId) attrs.push(`w:topLeft="${escapeXml(topLeftArtRelationshipId)}"`);
40
- if (topRightArtRelationshipId) attrs.push(`w:topRight="${escapeXml(topRightArtRelationshipId)}"`);
41
- if (bottomLeftArtRelationshipId) attrs.push(`w:bottomLeft="${escapeXml(bottomLeftArtRelationshipId)}"`);
42
- if (bottomRightArtRelationshipId) attrs.push(`w:bottomRight="${escapeXml(bottomRightArtRelationshipId)}"`);
37
+ if (shadow !== void 0) attrs.push(`w:shadow="${shadow ? "1" : "0"}"`);
38
+ if (frame !== void 0) attrs.push(`w:frame="${frame ? "1" : "0"}"`);
39
+ if (artRelationshipId) attrs.push(`r:id="${escapeXmlAttribute(artRelationshipId)}"`);
40
+ if (topLeftArtRelationshipId) attrs.push(`r:topLeft="${escapeXmlAttribute(topLeftArtRelationshipId)}"`);
41
+ if (topRightArtRelationshipId) attrs.push(`r:topRight="${escapeXmlAttribute(topRightArtRelationshipId)}"`);
42
+ if (bottomLeftArtRelationshipId) attrs.push(`r:bottomLeft="${escapeXmlAttribute(bottomLeftArtRelationshipId)}"`);
43
+ if (bottomRightArtRelationshipId) attrs.push(`r:bottomRight="${escapeXmlAttribute(bottomRightArtRelationshipId)}"`);
43
44
  return `<w:${elementName} ${attrs.join(" ")}/>`;
44
45
  }
45
46
  //#endregion
@@ -1,30 +1,55 @@
1
1
  import { Comment } from "../../types/content.js";
2
2
  //#region src/docx/serializer/commentSerializer.d.ts
3
3
  /**
4
- * Serialize comments array to comments.xml content. Returns a valid empty
5
- * `<w:comments/>` document for an empty array so callers can overwrite an
4
+ * What every comment part is written from: one ordered list of comment ids, and
5
+ * the facts each id carries.
6
+ *
7
+ * `word/comments.xml` and `word/commentsExtended.xml` describe the same
8
+ * comments and have to agree about which is which. They agreed by each walking
9
+ * the `Comment[]` they were handed and pairing entries up by position, so any
10
+ * disagreement about order — one part reordered, the other not — silently moved
11
+ * a comment's thread link, resolved state and paraId onto a different comment.
12
+ * Positions cannot disagree if neither part has one: both walk `order` and look
13
+ * every fact up by `w:id`, the only identity the package itself has.
14
+ */
15
+ type CommentPartPlan = {
16
+ /** The `w:id`s to write, in order, each exactly once. */
17
+ readonly order: readonly number[];
18
+ readonly byId: ReadonlyMap<number, Comment>;
19
+ /** The `w14:paraId` each comment is threaded by, minted where the model had none. */
20
+ readonly threadParaIdById: ReadonlyMap<number, string>;
21
+ /** The comments needing a `commentsExtended.xml` entry: replies, their parents, resolved comments. */
22
+ readonly threadedIds: ReadonlySet<number>;
23
+ };
24
+ /**
25
+ * Plan both comment parts from the model, in the model's own order.
26
+ *
27
+ * The order is the document's: the order `word/comments.xml` listed the
28
+ * comments in is the order it is written back in, so a save neither reshuffles
29
+ * a reviewer's threads nor hands the next parse a different `comments[]` than
30
+ * the one it read.
31
+ *
32
+ * Planning also mints the `w14:paraId` a thread needs when the model has none —
33
+ * a comment written in the editor has no Word-authored id, and without one the
34
+ * `commentsExtended.xml` link would be dropped. Minting is deterministic
35
+ * (comment id and text derived) so repeated saves mint the same id, and it
36
+ * happens here rather than at each call site because both parts must see it.
37
+ */
38
+ declare const planCommentParts: (comments: readonly Comment[]) => CommentPartPlan;
39
+ /**
40
+ * Serialize a comment plan to comments.xml content. Returns a valid empty
41
+ * `<w:comments/>` document for an empty plan so callers can overwrite an
6
42
  * existing `word/comments.xml` part when the editor has removed the last
7
43
  * comment — leaving the previous file in place would otherwise re-emit
8
44
  * the orphaned comment threads on every save.
9
45
  */
10
- declare function serializeComments(comments: Comment[], sourceBindings?: ReadonlyMap<string, string>): string;
11
- /**
12
- * Assign a deterministic `w14:paraId` to the LAST paragraph of every comment
13
- * that needs a commentsExtended entry (a reply, a reply's parent, or a resolved
14
- * comment) but has none. A document authored or loaded without comment paraIds
15
- * would otherwise have no stable key to thread through commentsExtended.xml, and
16
- * the thread link would be silently dropped. Word-authored ids are preserved;
17
- * only threaded, id-less paragraphs are filled. Deterministic (content- and
18
- * id-derived) so repeated saves mint the SAME id. MUST run before serializing
19
- * BOTH comments.xml and commentsExtended.xml so the two reference the same id.
20
- */
21
- declare function ensureThreadedCommentParaIds(comments: readonly Comment[]): void;
46
+ declare function serializeComments({ order, byId }: CommentPartPlan, sourceBindings?: ReadonlyMap<string, string>): string;
22
47
  /**
23
48
  * Serialize `commentsExtended.xml` (`w15:commentsEx`) for reply threading and
24
49
  * resolved state, or `null` when no comment needs an entry (see
25
50
  * {@link buildCommentExtendedEntries}). This is the part that makes Word render
26
51
  * a comment as a REPLY rather than a separate top-level thread.
27
52
  */
28
- declare function serializeCommentsExtended(comments: readonly Comment[]): string | null;
53
+ declare function serializeCommentsExtended(plan: CommentPartPlan): string | null;
29
54
  //#endregion
30
- export { ensureThreadedCommentParaIds, serializeComments, serializeCommentsExtended };
55
+ export { CommentPartPlan, planCommentParts, serializeComments, serializeCommentsExtended };