@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 { escapeXmlText } from "@stll/docx-core";
1
2
  import JSZip from "jszip";
2
3
  //#region src/ai-edits/__fixtures__/paragraphs.ts
3
4
  /**
@@ -13,8 +14,7 @@ const ZIP_ENTRY_OPTIONS = {
13
14
  date: new Date(Date.UTC(2e3, 0, 1)),
14
15
  createFolders: false
15
16
  };
16
- const escapeXml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
17
- const paragraphXml = (text) => `<w:p><w:r><w:t xml:space="preserve">${escapeXml(text)}</w:t></w:r></w:p>`;
17
+ const paragraphXml = (text) => `<w:p><w:r><w:t xml:space="preserve">${escapeXmlText(text)}</w:t></w:r></w:p>`;
18
18
  const buildParagraphsDocx = async (paragraphs, { inTableCell } = {}) => {
19
19
  const paragraphsXml = paragraphs.map(paragraphXml).join("");
20
20
  const body = inTableCell ? `<w:tbl><w:tblPr><w:tblW w:w="0" w:type="auto"/></w:tblPr><w:tblGrid><w:gridCol w:w="9360"/></w:tblGrid><w:tr><w:tc><w:tcPr><w:tcW w:w="9360" w:type="dxa"/></w:tcPr>${paragraphsXml}</w:tc></w:tr></w:tbl>` + paragraphXml("After the table.") : paragraphsXml;
@@ -10,6 +10,7 @@ import { directParagraphIndentation, paragraphIndentationAttrPatch, paragraphInd
10
10
  import { directParagraphSpacing, paragraphSpacingAttrPatch, paragraphSpacingEqual, paragraphSpacingFromFormatting, withDirectParagraphSpacing } from "../prosemirror/paragraphSpacing.js";
11
11
  import { getDocumentNumbering } from "../prosemirror/plugins/documentNumbering.js";
12
12
  import { getDocumentStyleResolver } from "../prosemirror/plugins/documentStyles.js";
13
+ import { addCarriedMarks, annotatedReplacement, hasReplacedAnnotations, inheritedReplacementMarks, surveyReplacedAnnotations } from "../prosemirror/replacedAnnotations.js";
13
14
  import { applyMarksToRunFormattingRepresentation, runFormattingInlineControlNodeName, selectRunFormattingCarrierRepresentations } from "../prosemirror/runFormattingInlineCarriers.js";
14
15
  import { readAuthoredRunFormatting, reconcileRunFormattingMarks } from "../prosemirror/runFormattingReconciliation.js";
15
16
  import { paragraphRunStyleContext, paragraphRunStyleContextAt } from "../prosemirror/runStyleFormatting.js";
@@ -949,6 +950,14 @@ const buildEmphasisInlineContent = (schema, text, baseMarks) => {
949
950
  }
950
951
  return nodes.length > 0 ? nodes : [schema.text(text, [...baseMarks])];
951
952
  };
953
+ /**
954
+ * Inline content for a block the applier rebuilds whole, carrying over the
955
+ * annotations the block's old content held (see `replacedAnnotations`).
956
+ */
957
+ const rebuiltBlockContent = ({ doc, blockFrom, blockTo, content }) => annotatedReplacement({
958
+ annotations: surveyReplacedAnnotations(doc, blockFrom + 1, blockTo - 1),
959
+ content: Fragment.fromArray([...content])
960
+ }).fragment;
952
961
  const isBatchableParagraphInsertion = (item, mode) => {
953
962
  if (item.operation.type !== "insertAfterBlock" && item.operation.type !== "insertBeforeBlock") return false;
954
963
  if (item.commentText !== void 0) return false;
@@ -1608,7 +1617,12 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1608
1617
  const replacement = item.operation.text;
1609
1618
  const paragraphType = view.state.schema.nodes["paragraph"];
1610
1619
  if (paragraphType) {
1611
- const node = paragraphType.create(null, replacement.length === 0 ? null : buildEmphasisInlineContent(view.state.schema, replacement, commentMark ? [commentMark] : []));
1620
+ const node = paragraphType.create(null, rebuiltBlockContent({
1621
+ doc: tr.doc,
1622
+ blockFrom: item.blockFrom,
1623
+ blockTo: item.blockTo,
1624
+ content: replacement.length === 0 ? [] : buildEmphasisInlineContent(view.state.schema, replacement, commentMark ? [commentMark] : [])
1625
+ }));
1612
1626
  tr = tr.replaceWith(item.blockFrom, item.blockTo, node);
1613
1627
  tr = applyReplaceBlockStyleId({
1614
1628
  item,
@@ -1619,7 +1633,12 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1619
1633
  }
1620
1634
  }
1621
1635
  if (mode === "direct" && hasInlineEmphasis(item.operation.text)) {
1622
- const node = item.blockNode.type.create({ ...item.blockNode.attrs }, buildEmphasisInlineContent(view.state.schema, item.operation.text, commentMark ? [commentMark] : []));
1636
+ const node = item.blockNode.type.create({ ...item.blockNode.attrs }, rebuiltBlockContent({
1637
+ doc: tr.doc,
1638
+ blockFrom: item.blockFrom,
1639
+ blockTo: item.blockTo,
1640
+ content: buildEmphasisInlineContent(view.state.schema, item.operation.text, commentMark ? [commentMark] : [])
1641
+ }));
1623
1642
  tr = tr.replaceWith(item.blockFrom, item.blockTo, node);
1624
1643
  tr = applyReplaceBlockStyleId({
1625
1644
  item,
@@ -2342,18 +2361,28 @@ const cleanTextInlineNodes = ({ schema, text, marks = [] }) => {
2342
2361
  if (trailing.length > 0) nodes.push(schema.text(trailing, marks));
2343
2362
  return nodes;
2344
2363
  };
2345
- const insertCleanText = ({ tr, from, to, text }) => {
2346
- if (!hasCleanTextControls(text)) return {
2364
+ const insertCleanText = ({ tr, from, to, text, standsInFor }) => {
2365
+ const annotations = surveyReplacedAnnotations(tr.doc, from, to);
2366
+ const replaced = standsInFor ? surveyReplacedAnnotations(tr.doc, standsInFor.from, standsInFor.to).carried : [];
2367
+ if (!hasReplacedAnnotations(annotations) && replaced.length === 0 && !hasCleanTextControls(text)) return {
2347
2368
  transaction: tr.insertText(text, from, to),
2369
+ start: from,
2348
2370
  end: from + text.length
2349
2371
  };
2350
- const content = Fragment.fromArray(cleanTextInlineNodes({
2351
- schema: tr.doc.type.schema,
2352
- text
2353
- }));
2372
+ const schema = tr.doc.type.schema;
2373
+ const { fragment, leadingSize, contentSize } = annotatedReplacement({
2374
+ annotations,
2375
+ content: Fragment.fromArray(cleanTextInlineNodes({
2376
+ schema,
2377
+ text,
2378
+ marks: addCarriedMarks(inheritedReplacementMarks(tr.doc, from, to), replaced)
2379
+ }))
2380
+ });
2381
+ const start = from + leadingSize;
2354
2382
  return {
2355
- transaction: tr.replaceWith(from, to, content),
2356
- end: from + content.size
2383
+ transaction: tr.replaceWith(from, to, fragment),
2384
+ start,
2385
+ end: start + contentSize
2357
2386
  };
2358
2387
  };
2359
2388
  const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete, revisionIdInsert, commentMark, suggestionId = null, initials, diffText }) => {
@@ -2365,8 +2394,11 @@ const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete,
2365
2394
  })());
2366
2395
  if (mode === "direct") {
2367
2396
  if ((item.operation.type === "replaceInBlock" || item.operation.type === "replaceRange") && hasInlineEmphasis(item.operation.replace)) {
2368
- const content = buildEmphasisInlineContent(nextTr.doc.type.schema, item.operation.replace, commentMark ? [commentMark] : []);
2369
- return nextTr.replaceWith(item.from, item.to, content);
2397
+ const { fragment } = annotatedReplacement({
2398
+ annotations: surveyReplacedAnnotations(nextTr.doc, item.from, item.to),
2399
+ content: Fragment.fromArray(buildEmphasisInlineContent(nextTr.doc.type.schema, item.operation.replace, commentMark ? [commentMark] : []))
2400
+ });
2401
+ return nextTr.replaceWith(item.from, item.to, fragment);
2370
2402
  }
2371
2403
  const inserted = insertCleanText({
2372
2404
  tr: nextTr,
@@ -2375,7 +2407,7 @@ const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete,
2375
2407
  text: replacement
2376
2408
  });
2377
2409
  nextTr = inserted.transaction;
2378
- if (commentMark && replacement.length > 0) nextTr = nextTr.addMark(item.from, inserted.end, commentMark);
2410
+ if (commentMark && replacement.length > 0) nextTr = nextTr.addMark(inserted.start, inserted.end, commentMark);
2379
2411
  return nextTr;
2380
2412
  }
2381
2413
  const insertionType = nextTr.doc.type.schema.marks["insertion"];
@@ -2465,11 +2497,15 @@ const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete,
2465
2497
  tr: nextTr,
2466
2498
  from: step.at,
2467
2499
  to: step.at,
2468
- text: step.text
2500
+ text: step.text,
2501
+ standsInFor: {
2502
+ from: item.from,
2503
+ to: item.to
2504
+ }
2469
2505
  });
2470
2506
  nextTr = inserted.transaction;
2471
- nextTr = nextTr.addMark(step.at, inserted.end, insertionType.create(insAttrs));
2472
- if (commentMark) nextTr = nextTr.addMark(step.at, inserted.end, commentMark);
2507
+ nextTr = nextTr.addMark(inserted.start, inserted.end, insertionType.create(insAttrs));
2508
+ if (commentMark) nextTr = nextTr.addMark(inserted.start, inserted.end, commentMark);
2473
2509
  }
2474
2510
  }
2475
2511
  return nextTr;
@@ -2480,11 +2516,15 @@ const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete,
2480
2516
  tr: nextTr,
2481
2517
  from: item.to,
2482
2518
  to: item.to,
2483
- text: replacement
2519
+ text: replacement,
2520
+ standsInFor: {
2521
+ from: item.from,
2522
+ to: item.to
2523
+ }
2484
2524
  });
2485
2525
  nextTr = inserted.transaction;
2486
- nextTr = nextTr.addMark(item.to, inserted.end, insertionType.create(insAttrs));
2487
- if (commentMark) nextTr = nextTr.addMark(item.to, inserted.end, commentMark);
2526
+ nextTr = nextTr.addMark(inserted.start, inserted.end, insertionType.create(insAttrs));
2527
+ if (commentMark) nextTr = nextTr.addMark(inserted.start, inserted.end, commentMark);
2488
2528
  }
2489
2529
  if (item.to > item.from && deletionType) {
2490
2530
  nextTr = nextTr.addMark(item.from, item.to, deletionType.create(delAttrs));
@@ -212,6 +212,7 @@ const sameReferencedNumberingLevels = ({ current, target, numId, levelsByNumId }
212
212
  for (const level of levelsByNumId.get(numId) ?? []) {
213
213
  const currentLevel = currentNumbering.getLevel(numId, level);
214
214
  const targetLevel = targetNumbering.getLevel(numId, level);
215
+ if (currentLevel === null && targetLevel === null) continue;
215
216
  if (!currentLevel || !targetLevel || canonicalJson(currentLevel) !== canonicalJson(targetLevel)) return false;
216
217
  }
217
218
  return true;
@@ -1543,7 +1544,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
1543
1544
  * same states, so the text now describes exactly what a save would write.
1544
1545
  */
1545
1546
  storyBlocks(state, source) {
1546
- return state ? proseDocToBlocks(state.doc, source.content, this.baseDocument.package.styles, { emptyFieldResult: "authored" }) : source.content;
1547
+ return state ? proseDocToBlocks(state.doc, source.content, this.baseDocument.package.styles) : source.content;
1547
1548
  }
1548
1549
  getHeaderFooterStoryText(story, source) {
1549
1550
  const state = this.secondaryStoryStates.get(headerFooterStoryKey(story))?.state;
@@ -15,7 +15,7 @@ import { sameAuthoredInlineProvenance } from "./inline-provenance.js";
15
15
  import { planStoryCompare } from "./plan.js";
16
16
  import { withFixedPackageDates } from "./reproducible-package.js";
17
17
  import { compareSectionBoundaryProperties } from "./section-boundary-properties.js";
18
- import { CompareDocxApplyError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, InvalidCompareDocxOptionsError } from "./types.js";
18
+ import { CompareDocxApplyError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, CompareDocxParseError, CompareDocxRoundTripError, InvalidCompareDocxOptionsError, compareDocxSerializeError } from "./types.js";
19
19
  import { classifyGeometryMismatch, classifyProjectionMismatch, revisedFinalParagraphMarks } from "./verification.js";
20
20
  import { Result, panic } from "better-result";
21
21
  //#region src/compare/compare.ts
@@ -903,10 +903,7 @@ const serializeComparison = async ({ baseBuffer, baseCarriedRevisions, reviewer,
903
903
  }));
904
904
  return await Result.tryPromise({
905
905
  try: async () => await withFixedPackageDates(await reviewer.toBuffer(), packageDate),
906
- catch: (cause) => new CompareDocxSerializeError({
907
- message: "The compared document could not be serialized.",
908
- cause
909
- })
906
+ catch: (cause) => compareDocxSerializeError("The compared document could not be serialized", cause)
910
907
  });
911
908
  };
912
909
  /**
@@ -455,6 +455,27 @@ const MAX_CONTENT_STRUCTURE_PROFILE_TEXT_CODE_UNITS = 16384;
455
455
  const MAX_CONTENT_STRUCTURE_PROFILE_TOKENS = 64;
456
456
  const MAX_CONTENT_STRUCTURE_PROFILE_KIND_CODE_UNITS = 256;
457
457
  const CONTENT_STRUCTURE_ANCHOR_MINIMUM_WORD_COUNT = 3;
458
+ /** Four independent 32-bit lanes, so a digest collision needs four at once. */
459
+ const CONTENT_DIGEST_SEEDS = [
460
+ 2166136261,
461
+ 2654435769,
462
+ 2246822507,
463
+ 3266489909
464
+ ];
465
+ const CONTENT_DIGEST_PRIMES = [
466
+ 16777619,
467
+ 1540483477,
468
+ 668265263,
469
+ 374761393
470
+ ];
471
+ const foldContentDigest = (lanes, text) => {
472
+ for (let index = 0; index < text.length; index++) {
473
+ const unit = text.charCodeAt(index);
474
+ for (let lane = 0; lane < lanes.length; lane++) lanes[lane] = Math.imul((lanes[lane] ?? 0) ^ unit, CONTENT_DIGEST_PRIMES[lane] ?? 1);
475
+ }
476
+ for (let lane = 0; lane < lanes.length; lane++) lanes[lane] = Math.imul((lanes[lane] ?? 0) ^ text.length, CONTENT_DIGEST_PRIMES[lane] ?? 1);
477
+ };
478
+ const renderContentDigest = (lanes, blockCount) => `${String(blockCount)}:${[...lanes].map((lane) => (lane >>> 0).toString(36)).join(":")}`;
458
479
  const createContentStructureProfile = ({ blocks, blockStructure, idStability, physicalCellCount, retainContainerIdentity = false }) => {
459
480
  let containerIdentity = null;
460
481
  if (retainContainerIdentity) if (blocks.length > MAX_CONTENT_STRUCTURE_PROFILE_BLOCKS) containerIdentity = { status: "unavailable" };
@@ -481,39 +502,34 @@ const createContentStructureProfile = ({ blocks, blockStructure, idStability, ph
481
502
  if (idStability(block) === "stable") stableIds.push(block.id);
482
503
  }
483
504
  }
484
- if (blocks.length > MAX_CONTENT_STRUCTURE_PROFILE_BLOCKS) return {
485
- anchorTexts: [],
505
+ const lanes = Int32Array.from(CONTENT_DIGEST_SEEDS);
506
+ for (const block of blocks) foldContentDigest(lanes, JSON.stringify([
507
+ ...blockStructure(block),
508
+ block.kind,
509
+ block.text
510
+ ]));
511
+ const identity = {
486
512
  blockIds,
487
513
  containerIdentity,
488
- exactSignature: null,
514
+ contentDigest: renderContentDigest(lanes, blocks.length),
489
515
  physicalCellCount: physicalCellCount ?? null,
490
- stableIds,
491
- tokenCounts: null,
492
- tokenCount: 0
516
+ stableIds
517
+ };
518
+ const overCap = {
519
+ ...identity,
520
+ anchors: { status: "skipped-over-cap" },
521
+ tokens: { status: "skipped-over-cap" }
493
522
  };
523
+ if (blocks.length > MAX_CONTENT_STRUCTURE_PROFILE_BLOCKS) return overCap;
494
524
  let textCodeUnits = 0;
495
525
  for (const block of blocks) {
496
- if (block.kind.length > MAX_CONTENT_STRUCTURE_PROFILE_KIND_CODE_UNITS || block.text.length > MAX_CONTENT_STRUCTURE_PROFILE_TEXT_CODE_UNITS - textCodeUnits) return {
497
- anchorTexts: [],
498
- blockIds,
499
- containerIdentity,
500
- exactSignature: null,
501
- physicalCellCount: physicalCellCount ?? null,
502
- stableIds,
503
- tokenCounts: null,
504
- tokenCount: 0
505
- };
526
+ if (block.kind.length > MAX_CONTENT_STRUCTURE_PROFILE_KIND_CODE_UNITS || block.text.length > MAX_CONTENT_STRUCTURE_PROFILE_TEXT_CODE_UNITS - textCodeUnits) return overCap;
506
527
  textCodeUnits += block.text.length;
507
528
  }
508
529
  const tokenCounts = /* @__PURE__ */ new Map();
509
530
  const anchorTexts = /* @__PURE__ */ new Set();
510
531
  let tokenCount = 0;
511
532
  let tokensComplete = true;
512
- const signature = blocks.map((block) => [
513
- ...blockStructure(block),
514
- block.kind,
515
- block.text
516
- ]);
517
533
  for (const block of blocks) {
518
534
  let blockWordCount = 0;
519
535
  for (const match of block.text.matchAll(/\S+/gu)) {
@@ -530,24 +546,31 @@ const createContentStructureProfile = ({ blocks, blockStructure, idStability, ph
530
546
  if (!tokensComplete) break;
531
547
  }
532
548
  return {
533
- anchorTexts: [...anchorTexts],
534
- blockIds,
535
- containerIdentity,
536
- exactSignature: JSON.stringify(signature),
537
- physicalCellCount: physicalCellCount ?? null,
538
- stableIds,
539
- tokenCounts: tokensComplete ? tokenCounts : null,
540
- tokenCount: tokensComplete ? tokenCount : 0
549
+ ...identity,
550
+ anchors: {
551
+ status: "computed",
552
+ texts: [...anchorTexts]
553
+ },
554
+ tokens: tokensComplete ? {
555
+ status: "computed",
556
+ counts: tokenCounts,
557
+ total: tokenCount
558
+ } : { status: "skipped-over-cap" }
541
559
  };
542
560
  };
561
+ const UNKNOWN_CONTENT_STRUCTURE_SIMILARITY = { status: "unknown" };
543
562
  const contentStructureProfileSimilarity = (base, revised, workSession) => {
544
- if (!base.tokenCounts || !revised.tokenCounts || base.tokenCount === 0 || revised.tokenCount === 0) return 0;
545
- const [tokens, counterparts] = base.tokenCounts.size <= revised.tokenCounts.size ? [base.tokenCounts, revised.tokenCounts] : [revised.tokenCounts, base.tokenCounts];
546
- if (tokens.size > workSession.remainingStructuralTokenLookups) return 0;
563
+ if (base.tokens.status === "skipped-over-cap" || revised.tokens.status === "skipped-over-cap") return UNKNOWN_CONTENT_STRUCTURE_SIMILARITY;
564
+ if (base.tokens.total === 0 || revised.tokens.total === 0) return UNKNOWN_CONTENT_STRUCTURE_SIMILARITY;
565
+ const [tokens, counterparts] = base.tokens.counts.size <= revised.tokens.counts.size ? [base.tokens.counts, revised.tokens.counts] : [revised.tokens.counts, base.tokens.counts];
566
+ if (tokens.size > workSession.remainingStructuralTokenLookups) return UNKNOWN_CONTENT_STRUCTURE_SIMILARITY;
547
567
  workSession.remainingStructuralTokenLookups -= tokens.size;
548
568
  let shared = 0;
549
569
  for (const [token, count] of tokens) shared += Math.min(count, counterparts.get(token) ?? 0);
550
- return 2 * shared / (base.tokenCount + revised.tokenCount);
570
+ return {
571
+ status: "measured",
572
+ value: 2 * shared / (base.tokens.total + revised.tokens.total)
573
+ };
551
574
  };
552
575
  const tableStructureProfile = (blocks, idStability) => {
553
576
  const tableOrdinalByIndex = /* @__PURE__ */ new Map();
@@ -634,7 +657,6 @@ const uniqueExactContentSequencePairs = ({ baseKeys, revisedKeys }) => {
634
657
  const uniqueIndexes = (keys) => {
635
658
  const indexes = /* @__PURE__ */ new Map();
636
659
  keys.forEach((key, index) => {
637
- if (key === -1) return;
638
660
  indexes.set(key, indexes.has(key) ? null : index);
639
661
  });
640
662
  return indexes;
@@ -731,16 +753,15 @@ const alignProfiledContentSequence = ({ base, revised, workSession, canPair = ()
731
753
  }
732
754
  const exactSignatureKeys = /* @__PURE__ */ new Map();
733
755
  let nextExactSignatureKey = 0;
734
- const internExactSignature = (signature) => {
735
- if (signature === null) return -1;
736
- const existing = exactSignatureKeys.get(signature);
756
+ const internContentDigest = (digest) => {
757
+ const existing = exactSignatureKeys.get(digest);
737
758
  if (existing !== void 0) return existing;
738
759
  const key = nextExactSignatureKey++;
739
- exactSignatureKeys.set(signature, key);
760
+ exactSignatureKeys.set(digest, key);
740
761
  return key;
741
762
  };
742
- const baseExactSignatureKeys = base.map(({ profile }) => internExactSignature(profile.exactSignature));
743
- const revisedExactSignatureKeys = revised.map(({ profile }) => internExactSignature(profile.exactSignature));
763
+ const baseExactSignatureKeys = base.map(({ profile }) => internContentDigest(profile.contentDigest));
764
+ const revisedExactSignatureKeys = revised.map(({ profile }) => internContentDigest(profile.contentDigest));
744
765
  let persistedPairs = /* @__PURE__ */ new Set();
745
766
  if (base.some(({ profile }) => profile.containerIdentity !== null) || revised.some(({ profile }) => profile.containerIdentity !== null)) {
746
767
  const exactPairs = uniqueExactContentSequencePairs({
@@ -781,16 +802,20 @@ const alignProfiledContentSequence = ({ base, revised, workSession, canPair = ()
781
802
  if (!revisedItem) continue;
782
803
  if (!canPair(baseItem, revisedItem)) continue;
783
804
  const pairIndex = baseIndex * revised.length + revisedIndex;
784
- const exact = baseExactSignatureKeys[baseIndex] !== -1 && baseExactSignatureKeys[baseIndex] === revisedExactSignatureKeys[revisedIndex];
805
+ const exact = baseExactSignatureKeys[baseIndex] === revisedExactSignatureKeys[revisedIndex];
785
806
  const stable = stablePairs.has(pairIndex);
786
- const profileSimilarity = exact ? 1 : contentStructureProfileSimilarity(baseItem.profile, revisedItem.profile, workSession);
787
- const similar = Math.max(0, Math.min(1, profileSimilarity * similarityFactor(baseItem, revisedItem)));
807
+ const profileSimilarity = exact ? {
808
+ status: "measured",
809
+ value: 1
810
+ } : contentStructureProfileSimilarity(baseItem.profile, revisedItem.profile, workSession);
811
+ const similar = profileSimilarity.status === "measured" ? Math.max(0, Math.min(1, profileSimilarity.value * similarityFactor(baseItem, revisedItem))) : 0;
812
+ const similarEnough = profileSimilarity.status === "measured" && similar >= CONTENT_STRUCTURE_PAIR_SIMILARITY;
788
813
  const stableAtSamePosition = stable && baseIndex === revisedIndex;
789
814
  const persistedAtSamePosition = baseIndex === revisedIndex && provenanceTransitionAtSamePosition[baseIndex] === 1;
790
815
  const shiftedPersisted = baseIndex !== revisedIndex && persistedPairs.has(pairIndex);
791
816
  const persisted = persistedAtSamePosition || shiftedPersisted;
792
817
  const soleStructuralSlot = pairSoleStructuralSlot && base.length === 1 && revised.length === 1;
793
- if (!exact && !stableAtSamePosition && !persisted && !soleStructuralSlot && similar < CONTENT_STRUCTURE_PAIR_SIMILARITY) continue;
818
+ if (!exact && !stableAtSamePosition && !persisted && !soleStructuralSlot && !similarEnough) continue;
794
819
  pairScores[pairIndex] = (stable ? stableWeight : 0) + (exact ? exactWeight : 0) + (shiftedPersisted && !stable && !exact ? secondaryWeight : 0) + (persistedAtSamePosition ? continuityBonus : 0) + Math.round(similar * CONTENT_STRUCTURE_SIMILARITY_SCALE) + (soleStructuralSlot ? 1 : 0);
795
820
  }
796
821
  }
@@ -1124,11 +1149,10 @@ const contentStructureEvidenceIndexes = (base, revised) => {
1124
1149
  let nextExactKey = 0;
1125
1150
  const internExactSignatures = (items) => {
1126
1151
  for (const { profile } of items) {
1127
- if (profile.exactSignature === null) continue;
1128
- let key = exactKeys.get(profile.exactSignature);
1152
+ let key = exactKeys.get(profile.contentDigest);
1129
1153
  if (key === void 0) {
1130
1154
  key = nextExactKey++;
1131
- exactKeys.set(profile.exactSignature, key);
1155
+ exactKeys.set(profile.contentDigest, key);
1132
1156
  }
1133
1157
  exactKeyByProfile.set(profile, key);
1134
1158
  }
@@ -1152,7 +1176,7 @@ const contentStructureEvidenceIndexes = (base, revised) => {
1152
1176
  if (indexes.at(-1) !== itemIndex) indexes.push(itemIndex);
1153
1177
  } else stableIndexes.set(id, [itemIndex]);
1154
1178
  }
1155
- for (const text of profile.anchorTexts) {
1179
+ for (const text of profile.anchors.status === "computed" ? profile.anchors.texts : []) {
1156
1180
  const indexes = anchorTextIndexes.get(text);
1157
1181
  if (indexes) {
1158
1182
  if (indexes.at(-1) !== itemIndex) indexes.push(itemIndex);
@@ -1190,12 +1214,13 @@ const sortedIndexesLeaveRange = (indexes, start, end) => {
1190
1214
  const profileHasEvidenceOutsideRange = (profile, opposite, start, end) => {
1191
1215
  const exactKey = opposite.exactKeyByProfile.get(profile);
1192
1216
  if (exactKey !== void 0 && sortedIndexesLeaveRange(opposite.exactIndexes.get(exactKey), start, end)) return true;
1193
- if (profile.anchorTexts.some((text) => sortedIndexesLeaveRange(opposite.anchorTextIndexes.get(text), start, end))) return true;
1217
+ if (profile.anchors.status === "computed" && profile.anchors.texts.some((text) => sortedIndexesLeaveRange(opposite.anchorTextIndexes.get(text), start, end))) return true;
1194
1218
  return profile.stableIds.some((id) => sortedIndexesLeaveRange(opposite.stableIndexes.get(id), start, end));
1195
1219
  };
1196
1220
  const profilesShareContentAnchor = (base, revised) => {
1197
- const baseTexts = new Set(base.anchorTexts);
1198
- return revised.anchorTexts.some((text) => baseTexts.has(text));
1221
+ if (base.anchors.status === "skipped-over-cap" || revised.anchors.status === "skipped-over-cap") return "unknown";
1222
+ const baseTexts = new Set(base.anchors.texts);
1223
+ return revised.anchors.texts.some((text) => baseTexts.has(text)) ? "shares" : "disjoint";
1199
1224
  };
1200
1225
  const isBodyDocumentSegment = (segment) => segment.kind === "body";
1201
1226
  const profileBodySegments = (segments, idStability) => segments.flatMap((segment) => isBodyDocumentSegment(segment) ? [{
@@ -1288,13 +1313,13 @@ const pairTableSegmentsInGaps = ({ base, revised, baseGaps, revisedGaps, trusted
1288
1313
  const revisedEnd = revisedStart + revisedItems.length;
1289
1314
  const baseHasExternalEvidence = new Set(baseItems.filter(({ profile }) => profileHasEvidenceOutsideRange(profile, evidence.revised, revisedStart, revisedEnd)).map(({ item }) => item));
1290
1315
  const revisedHasExternalEvidence = new Set(revisedItems.filter(({ profile }) => profileHasEvidenceOutsideRange(profile, evidence.base, baseStart, baseEnd)).map(({ item }) => item));
1291
- const solePairHasContentAnchor = baseItems.length === 1 && revisedItems.length === 1 && profilesShareContentAnchor(baseItems.at(0)?.profile ?? panic("A sole base table has no profile"), revisedItems.at(0)?.profile ?? panic("A sole revised table has no profile"));
1316
+ const solePairAnchorOverlap = baseItems.length === 1 && revisedItems.length === 1 ? profilesShareContentAnchor(baseItems.at(0)?.profile ?? panic("A sole base table has no profile"), revisedItems.at(0)?.profile ?? panic("A sole revised table has no profile")) : "disjoint";
1292
1317
  const alignments = alignProfiledContentSequence({
1293
1318
  base: baseItems,
1294
1319
  revised: revisedItems,
1295
1320
  workSession,
1296
1321
  canPair: (baseItem, revisedItem) => !baseHasExternalEvidence.has(baseItem.item) && !revisedHasExternalEvidence.has(revisedItem.item),
1297
- pairSoleStructuralSlot: trustedBodyPairCount > 0 || solePairHasContentAnchor
1322
+ pairSoleStructuralSlot: trustedBodyPairCount > 0 || solePairAnchorOverlap === "shares"
1298
1323
  });
1299
1324
  for (const alignment of alignments) {
1300
1325
  if (alignment.type !== "pair") continue;
@@ -29,6 +29,15 @@ const textBlocksOf = (doc) => {
29
29
  });
30
30
  return blocks;
31
31
  };
32
+ /**
33
+ * Clean-text offset nearest a document position.
34
+ *
35
+ * `buildCleanBlockText` returns one offset per character plus one for
36
+ * end-of-block, so there is always an offset to land on. The result was typed
37
+ * `number | null` and never was: the caller's refusal branch could not run,
38
+ * while an empty `offsets` would have produced `-1` rather than the `null` the
39
+ * type promised.
40
+ */
32
41
  const offsetAt = ({ offsets, position }) => {
33
42
  let low = 0;
34
43
  let high = offsets.length;
@@ -40,16 +49,33 @@ const offsetAt = ({ offsets, position }) => {
40
49
  }
41
50
  return Math.min(low, offsets.length - 1);
42
51
  };
52
+ /**
53
+ * Attributes a conversion mints for itself rather than reads from the package.
54
+ *
55
+ * `toProseDoc` salts text-box anchor ids with a per-load nonce so a pasted span
56
+ * cannot hijack a real anchor. A comparison reads two independent conversions,
57
+ * so a minted handle differs on every run, and a key holding one makes a
58
+ * document differ from itself. Only document facts belong in a key.
59
+ *
60
+ * The guard against a node type gaining a minted attribute without being added
61
+ * here is not this list but `compareIdentity.property.test.ts`, which compares
62
+ * generated packages carrying each atom with themselves.
63
+ */
64
+ const CONVERSION_LOCAL_ATTRS = { textBoxAnchor: ["anchorId"] };
65
+ const documentFactAttrs = (node) => {
66
+ const local = CONVERSION_LOCAL_ATTRS[node.type.name];
67
+ if (!local) return node.attrs;
68
+ return Object.fromEntries(Object.entries(node.attrs).filter(([name]) => !local.includes(name)));
69
+ };
43
70
  const atomKey = (node) => canonicalJson({
44
71
  type: node.type.name,
45
- attrs: node.attrs,
72
+ attrs: documentFactAttrs(node),
46
73
  content: node.content.toJSON()
47
74
  });
48
75
  const atomBlockOf = ({ node, from }) => {
49
76
  const clean = buildCleanBlockText(node, from, { fieldResults: "omitted" });
50
77
  const supported = [];
51
78
  const unsupportedTopology = [];
52
- let unalignable = false;
53
79
  node.descendants((child, relativePosition) => {
54
80
  if (child.isText) return false;
55
81
  if (!child.isInline || !child.isAtom) return true;
@@ -58,18 +84,14 @@ const atomBlockOf = ({ node, from }) => {
58
84
  offsets: clean.offsets,
59
85
  position
60
86
  });
61
- if (offset === null) {
62
- unalignable = true;
63
- return false;
64
- }
65
87
  if (!isSupportedAtom(child)) {
66
88
  if (child.type.name === "renderedPageBreak") return false;
67
- unsupportedTopology.push(`${offset}:${child.type.name}:${canonicalJson(child.attrs)}`);
89
+ unsupportedTopology.push(`${offset}:${child.type.name}:${canonicalJson(documentFactAttrs(child))}`);
68
90
  return false;
69
91
  }
70
92
  const prepared = prepareTargetInlineAtom(child);
71
93
  if (!prepared) {
72
- unsupportedTopology.push(`${offset}:${child.type.name}:${canonicalJson(child.attrs)}`);
94
+ unsupportedTopology.push(`${offset}:${child.type.name}:${canonicalJson(documentFactAttrs(child))}`);
73
95
  return false;
74
96
  }
75
97
  supported.push({
@@ -80,7 +102,7 @@ const atomBlockOf = ({ node, from }) => {
80
102
  });
81
103
  return false;
82
104
  });
83
- return unalignable ? null : {
105
+ return {
84
106
  node,
85
107
  from,
86
108
  cleanText: clean.text,
@@ -108,15 +130,7 @@ const targetBlockIdLookup = (snapshot) => {
108
130
  return owner?.id;
109
131
  };
110
132
  };
111
- const atomBlocksOf = (doc) => {
112
- const blocks = [];
113
- for (const block of textBlocksOf(doc)) {
114
- const atoms = atomBlockOf(block);
115
- if (!atoms) return null;
116
- blocks.push(atoms);
117
- }
118
- return blocks;
119
- };
133
+ const atomBlocksOf = (doc) => textBlocksOf(doc).map(atomBlockOf);
120
134
  const sameBlockTopology = (left, right) => left.node.type === right.node.type && left.cleanText === right.cleanText && canonicalJson(left.unsupportedTopology) === canonicalJson(right.unsupportedTopology);
121
135
  const matchingAtomGroup = ({ live, target, liveStart, targetStart }) => {
122
136
  if (live.length === target.length && live.every((atom, index) => atom.key === target[index]?.key)) return live.map((_, index) => [liveStart + index, targetStart + index]);
@@ -260,7 +274,7 @@ const matchInlineAtoms = ({ state, targetSnapshot, revisionStamp, originalRevisi
260
274
  let sourceBlocks;
261
275
  const liveBlocks = atomBlocksOf(reviewed.state.doc);
262
276
  const targetBlocks = atomBlocksOf(targetDocument);
263
- if (!liveBlocks || !targetBlocks || liveBlocks.length !== targetBlocks.length) return { status: "unalignable" };
277
+ if (liveBlocks.length !== targetBlocks.length) return { status: "unalignable" };
264
278
  const actions = [];
265
279
  for (const [index, live] of liveBlocks.entries()) {
266
280
  const target = targetBlocks[index];
@@ -366,7 +380,7 @@ const sameInlineAtoms = (leftDocument, rightDocument) => {
366
380
  if (!hasSupportedAtom(leftDocument) && !hasSupportedAtom(rightDocument)) return true;
367
381
  const leftBlocks = atomBlocksOf(leftDocument);
368
382
  const rightBlocks = atomBlocksOf(rightDocument);
369
- if (!leftBlocks || !rightBlocks || leftBlocks.length !== rightBlocks.length) return false;
383
+ if (leftBlocks.length !== rightBlocks.length) return false;
370
384
  return leftBlocks.every((left, index) => {
371
385
  const right = rightBlocks[index];
372
386
  if (left.supported.length === 0 && right?.supported.length === 0) return true;
@@ -1,6 +1,6 @@
1
1
  import { FolioDocxReviewer } from "../ai-edits/headless.js";
2
2
  import { createFolioAITextRangeHandle } from "../ai-edits/snapshot.js";
3
- import { CompareDocxParseError, CompareDocxSerializeError } from "./types.js";
3
+ import { CompareDocxParseError, compareDocxSerializeError } from "./types.js";
4
4
  import { Result, panic } from "better-result";
5
5
  //#region src/compare/scenario.ts
6
6
  /**
@@ -252,10 +252,7 @@ const applyEditScript = async (base, script) => {
252
252
  }
253
253
  const serialized = await Result.tryPromise({
254
254
  try: async () => await reviewer.toBuffer(),
255
- catch: (cause) => new CompareDocxSerializeError({
256
- message: "The edit script's target document could not be serialized.",
257
- cause
258
- })
255
+ catch: (cause) => compareDocxSerializeError("The edit script's target document could not be serialized", cause)
259
256
  });
260
257
  return serialized.isErr() ? Result.err(serialized.error) : Result.ok({
261
258
  buffer: serialized.value,
@@ -316,6 +316,14 @@ declare class CompareDocxSerializeError extends CompareDocxSerializeError_base<{
316
316
  message: string;
317
317
  cause: unknown;
318
318
  }> {}
319
+ /**
320
+ * A serialize failure whose message names the reason.
321
+ *
322
+ * `cause` is structured context a caller can branch on, but a log line, a
323
+ * report and the corpus census read the message, and a constant one collapses
324
+ * every distinct save failure into a single undiagnosable row.
325
+ */
326
+ declare const compareDocxSerializeError: (context: string, cause: unknown) => CompareDocxSerializeError;
319
327
  declare const CompareDocxFinalParagraphMarkError_base: import("better-result").TaggedErrorClass<"CompareDocxFinalParagraphMarkError">;
320
328
  /**
321
329
  * A container's final paragraph mark carries a revision, so the package would
@@ -335,4 +343,4 @@ declare class CompareDocxFinalParagraphMarkError extends CompareDocxFinalParagra
335
343
  }> {}
336
344
  type CompareDocxError = CompareDocxApplyError | CompareDocxFinalParagraphMarkError | CompareDocxOperationLimitError | CompareDocxParseError | CompareDocxRoundTripError | CompareDocxSerializeError | InvalidCompareDocxOptionsError;
337
345
  //#endregion
338
- export { COMPARE_REVISION_FORMATS, COMPARE_UNSUPPORTED_REASONS, CompareChange, CompareChangeLocation, CompareCompatibility, CompareDocxApplyError, CompareDocxError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, CompareDocxOptions, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, CompareFolioRequirement, CompareResult, CompareRevisionFormat, CompareUnsupportedPart, CompareUnsupportedReason, InvalidCompareDocxOptionsError };
346
+ export { COMPARE_REVISION_FORMATS, COMPARE_UNSUPPORTED_REASONS, CompareChange, CompareChangeLocation, CompareCompatibility, CompareDocxApplyError, CompareDocxError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, CompareDocxOptions, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, CompareFolioRequirement, CompareResult, CompareRevisionFormat, CompareUnsupportedPart, CompareUnsupportedReason, InvalidCompareDocxOptionsError, compareDocxSerializeError };
@@ -31,6 +31,17 @@ var CompareDocxRoundTripError = class extends TaggedError("CompareDocxRoundTripE
31
31
  var CompareDocxOperationLimitError = class extends TaggedError("CompareDocxOperationLimitError") {};
32
32
  var CompareDocxSerializeError = class extends TaggedError("CompareDocxSerializeError") {};
33
33
  /**
34
+ * A serialize failure whose message names the reason.
35
+ *
36
+ * `cause` is structured context a caller can branch on, but a log line, a
37
+ * report and the corpus census read the message, and a constant one collapses
38
+ * every distinct save failure into a single undiagnosable row.
39
+ */
40
+ const compareDocxSerializeError = (context, cause) => new CompareDocxSerializeError({
41
+ message: `${context}: ${cause instanceof Error ? cause.message : String(cause)}`,
42
+ cause
43
+ });
44
+ /**
34
45
  * A container's final paragraph mark carries a revision, so the package would
35
46
  * not open, or would open carrying one no reader can resolve.
36
47
  *
@@ -43,4 +54,4 @@ var CompareDocxSerializeError = class extends TaggedError("CompareDocxSerializeE
43
54
  */
44
55
  var CompareDocxFinalParagraphMarkError = class extends TaggedError("CompareDocxFinalParagraphMarkError") {};
45
56
  //#endregion
46
- export { COMPARE_REVISION_FORMATS, COMPARE_UNSUPPORTED_REASONS, CompareDocxApplyError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, InvalidCompareDocxOptionsError };
57
+ export { COMPARE_REVISION_FORMATS, COMPARE_UNSUPPORTED_REASONS, CompareDocxApplyError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, InvalidCompareDocxOptionsError, compareDocxSerializeError };