@stll/folio-core 0.32.1 → 0.33.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 (222) hide show
  1. package/dist/ai-edits/__fixtures__/paragraphs.d.ts +17 -0
  2. package/dist/ai-edits/__fixtures__/paragraphs.js +33 -0
  3. package/dist/ai-edits/apply.d.ts +49 -3
  4. package/dist/ai-edits/apply.js +692 -75
  5. package/dist/ai-edits/blockRange.d.ts +7 -3
  6. package/dist/ai-edits/blockRange.js +14 -6
  7. package/dist/ai-edits/headless.d.ts +39 -5
  8. package/dist/ai-edits/headless.js +76 -17
  9. package/dist/ai-edits/index.d.ts +6 -6
  10. package/dist/ai-edits/index.js +3 -3
  11. package/dist/ai-edits/read.d.ts +4 -0
  12. package/dist/ai-edits/read.js +30 -6
  13. package/dist/ai-edits/scoped-reading.js +4 -3
  14. package/dist/ai-edits/snapshot.d.ts +42 -2
  15. package/dist/ai-edits/snapshot.js +127 -62
  16. package/dist/ai-edits/table-row-column-mutations.d.ts +24 -1
  17. package/dist/ai-edits/table-row-column-mutations.js +98 -6
  18. package/dist/ai-edits/table-targets.d.ts +12 -3
  19. package/dist/ai-edits/table-targets.js +15 -5
  20. package/dist/ai-edits/types.d.ts +235 -7
  21. package/dist/ai-edits/word-diff.d.ts +54 -11
  22. package/dist/ai-edits/word-diff.js +221 -61
  23. package/dist/compare/__fixtures__/body-sequence.d.ts +33 -0
  24. package/dist/compare/__fixtures__/body-sequence.js +67 -0
  25. package/dist/compare/__fixtures__/nested-table.d.ts +16 -0
  26. package/dist/compare/__fixtures__/nested-table.js +50 -0
  27. package/dist/compare/__fixtures__/numbered-list.d.ts +34 -0
  28. package/dist/compare/__fixtures__/numbered-list.js +85 -0
  29. package/dist/compare/column-alignment.d.ts +27 -0
  30. package/dist/compare/column-alignment.js +162 -0
  31. package/dist/compare/compare.d.ts +107 -0
  32. package/dist/compare/compare.js +406 -0
  33. package/dist/compare/formatting.d.ts +29 -0
  34. package/dist/compare/formatting.js +84 -0
  35. package/dist/compare/plan.d.ts +22 -0
  36. package/dist/compare/plan.js +1059 -0
  37. package/dist/compare/reproducible-package.d.ts +17 -0
  38. package/dist/compare/reproducible-package.js +30 -0
  39. package/dist/compare/scenario.d.ts +77 -0
  40. package/dist/compare/scenario.js +263 -0
  41. package/dist/compare/types.d.ts +282 -0
  42. package/dist/compare/types.js +32 -0
  43. package/dist/compare/verification.d.ts +55 -0
  44. package/dist/compare/verification.js +146 -0
  45. package/dist/compat/eigenpal.d.ts +9 -6
  46. package/dist/compat/eigenpal.js +6 -3
  47. package/dist/controller/layoutPipeline.d.ts +13 -0
  48. package/dist/controller/layoutPipeline.js +15 -1
  49. package/dist/display-list/build/buildContext.d.ts +39 -0
  50. package/dist/display-list/build/buildContext.js +31 -0
  51. package/dist/display-list/build/buildDisplayList.d.ts +79 -0
  52. package/dist/display-list/build/buildDisplayList.js +283 -0
  53. package/dist/display-list/build/colors.d.ts +20 -0
  54. package/dist/display-list/build/colors.js +136 -0
  55. package/dist/display-list/build/floatingImages.d.ts +24 -0
  56. package/dist/display-list/build/floatingImages.js +41 -0
  57. package/dist/display-list/build/fontTable.d.ts +31 -0
  58. package/dist/display-list/build/fontTable.js +97 -0
  59. package/dist/display-list/build/furniture.d.ts +86 -0
  60. package/dist/display-list/build/furniture.js +98 -0
  61. package/dist/display-list/build/glyphs.d.ts +55 -0
  62. package/dist/display-list/build/glyphs.js +103 -0
  63. package/dist/display-list/build/headerFooterPrimitives.d.ts +17 -0
  64. package/dist/display-list/build/headerFooterPrimitives.js +40 -0
  65. package/dist/display-list/build/imagePrimitives.d.ts +35 -0
  66. package/dist/display-list/build/imagePrimitives.js +264 -0
  67. package/dist/display-list/build/pageBorderPrimitives.d.ts +23 -0
  68. package/dist/display-list/build/pageBorderPrimitives.js +111 -0
  69. package/dist/display-list/build/pageFurniture.d.ts +35 -0
  70. package/dist/display-list/build/pageFurniture.js +141 -0
  71. package/dist/display-list/build/paragraphPrimitives.d.ts +19 -0
  72. package/dist/display-list/build/paragraphPrimitives.js +819 -0
  73. package/dist/display-list/build/regions.d.ts +46 -0
  74. package/dist/display-list/build/regions.js +56 -0
  75. package/dist/display-list/build/storyPrimitives.d.ts +29 -0
  76. package/dist/display-list/build/storyPrimitives.js +279 -0
  77. package/dist/display-list/build/strokes.d.ts +19 -0
  78. package/dist/display-list/build/strokes.js +78 -0
  79. package/dist/display-list/build/tablePrimitives.d.ts +38 -0
  80. package/dist/display-list/build/tablePrimitives.js +409 -0
  81. package/dist/display-list/build/textBoxPrimitives.d.ts +14 -0
  82. package/dist/display-list/build/textBoxPrimitives.js +129 -0
  83. package/dist/display-list/build/textDecorations.d.ts +29 -0
  84. package/dist/display-list/build/textDecorations.js +29 -0
  85. package/dist/display-list/build/unsupported.d.ts +57 -0
  86. package/dist/display-list/build/unsupported.js +0 -0
  87. package/dist/display-list/build/watermarkPrimitives.d.ts +15 -0
  88. package/dist/display-list/build/watermarkPrimitives.js +128 -0
  89. package/dist/display-list/dom/renderDisplayListToDom.d.ts +26 -0
  90. package/dist/display-list/dom/renderDisplayListToDom.js +620 -0
  91. package/dist/display-list/editor/displayListPagePainter.d.ts +22 -0
  92. package/dist/display-list/editor/displayListPagePainter.js +63 -0
  93. package/dist/display-list/editor/pageRenderer.d.ts +24 -0
  94. package/dist/display-list/editor/pageRenderer.js +36 -0
  95. package/dist/display-list/primitives.d.ts +95 -0
  96. package/dist/display-list/primitives.js +127 -0
  97. package/dist/display-list/types.d.ts +472 -0
  98. package/dist/display-list/types.js +0 -0
  99. package/dist/document-operations.d.ts +42 -9
  100. package/dist/document-operations.js +171 -16
  101. package/dist/document-stories.d.ts +16 -0
  102. package/dist/document-stories.js +48 -4
  103. package/dist/docx/packageParts.d.ts +35 -0
  104. package/dist/docx/packageParts.js +120 -0
  105. package/dist/docx/paragraphParser.js +22 -22
  106. package/dist/docx/paragraphTraversal.js +1 -1
  107. package/dist/docx/renderedPageBreakNormalization.js +3 -1
  108. package/dist/docx/revisionIdNormalization.d.ts +13 -0
  109. package/dist/docx/revisionIdNormalization.js +103 -0
  110. package/dist/docx/rezip.js +26 -17
  111. package/dist/docx/selectiveSave.js +2 -2
  112. package/dist/docx/serializer/commentSerializer.js +6 -1
  113. package/dist/docx/serializer/paragraphSerializer.js +33 -4
  114. package/dist/docx/serializer/runSerializer.js +11 -10
  115. package/dist/docx/serializer/tableSerializer.js +2 -1
  116. package/dist/docx/server/applyDocxXmlPatchProposal.js +1 -1
  117. package/dist/docx/server/createBilingualDocx.js +3 -2
  118. package/dist/docx/streamingXmlParser.d.ts +13 -1
  119. package/dist/docx/streamingXmlParser.js +49 -11
  120. package/dist/docx/unzip.d.ts +1 -2
  121. package/dist/docx/unzip.js +25 -6
  122. package/dist/docx/xmlParser.d.ts +3 -1
  123. package/dist/docx/xmlParser.js +33 -27
  124. package/dist/export-pdf.d.ts +43 -0
  125. package/dist/export-pdf.js +98 -0
  126. package/dist/fonts/headlessMeasure.d.ts +76 -0
  127. package/dist/fonts/headlessMeasure.js +0 -0
  128. package/dist/fonts/sfnt/parse.d.ts +51 -0
  129. package/dist/fonts/sfnt/parse.js +525 -0
  130. package/dist/fonts/sfnt/subset.d.ts +20 -0
  131. package/dist/fonts/sfnt/subset.js +350 -0
  132. package/dist/fonts/sfnt/tables.d.ts +88 -0
  133. package/dist/fonts/sfnt/tables.js +131 -0
  134. package/dist/fonts/sfnt/woff.d.ts +14 -0
  135. package/dist/fonts/sfnt/woff.js +163 -0
  136. package/dist/generated/text_shaper.js +324 -0
  137. package/dist/generated/text_shaper_bg.wasm +0 -0
  138. package/dist/headless-layout.d.ts +75 -0
  139. package/dist/headless-layout.js +350 -0
  140. package/dist/index.d.ts +9 -6
  141. package/dist/index.js +6 -3
  142. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  143. package/dist/layout-bridge/convert/footnoteLayout.js +1 -0
  144. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  145. package/dist/layout-bridge/convert/headerFooterLayout.js +2 -0
  146. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +3 -1
  147. package/dist/layout-bridge/convert/toFlowBlocks.js +2 -0
  148. package/dist/layout-engine/measure/advanceComposition.d.ts +62 -0
  149. package/dist/layout-engine/measure/advanceComposition.js +148 -0
  150. package/dist/layout-engine/measure/cache.d.ts +2 -0
  151. package/dist/layout-engine/measure/measureContainer.js +13 -0
  152. package/dist/layout-engine/measure/measureParagraph.js +1 -2
  153. package/dist/layout-engine/measure/measureTypes.d.ts +12 -0
  154. package/dist/layout-engine/measure/tableInlinePlacement.d.ts +3 -2
  155. package/dist/layout-engine/measure/tableInlinePlacement.js +14 -1
  156. package/dist/layout-engine/tableIndentCompatibility.d.ts +5 -0
  157. package/dist/layout-engine/tableIndentCompatibility.js +15 -0
  158. package/dist/layout-engine/types.d.ts +8 -0
  159. package/dist/layout-painter/renderPage.d.ts +29 -1
  160. package/dist/layout-painter/renderPage.js +47 -19
  161. package/dist/layout-painter/renderParagraph.d.ts +21 -2
  162. package/dist/layout-painter/renderParagraph.js +1 -1
  163. package/dist/layout-painter/renderTable.js +37 -4
  164. package/dist/managers/editorShortcuts.d.ts +33 -1
  165. package/dist/managers/editorShortcuts.js +20 -1
  166. package/dist/markdown/renderRuns.js +7 -2
  167. package/dist/markdown/renderTable.js +5 -1
  168. package/dist/model.d.ts +3 -3
  169. package/dist/model.js +2 -2
  170. package/dist/pdf/contentStream.d.ts +73 -0
  171. package/dist/pdf/contentStream.js +143 -0
  172. package/dist/pdf/fonts.d.ts +96 -0
  173. package/dist/pdf/fonts.js +593 -0
  174. package/dist/pdf/images.d.ts +28 -0
  175. package/dist/pdf/images.js +439 -0
  176. package/dist/pdf/objects.d.ts +93 -0
  177. package/dist/pdf/objects.js +237 -0
  178. package/dist/pdf/pageSpace.d.ts +43 -0
  179. package/dist/pdf/pageSpace.js +57 -0
  180. package/dist/pdf/paint.d.ts +32 -0
  181. package/dist/pdf/paint.js +442 -0
  182. package/dist/pdf/writePdf.d.ts +56 -0
  183. package/dist/pdf/writePdf.js +345 -0
  184. package/dist/prosemirror/commands/comments.js +91 -6
  185. package/dist/prosemirror/conversion/fromProseDoc.js +41 -7
  186. package/dist/prosemirror/conversion/toProseDoc.js +41 -4
  187. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +15 -2
  188. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +35 -6
  189. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +2 -2
  190. package/dist/prosemirror/extensions/features/pasteCleanup.js +4 -3
  191. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.d.ts +3 -1
  192. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +5 -3
  193. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.d.ts +7 -1
  194. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.js +9 -3
  195. package/dist/prosemirror/extensions/nodes/TableExtension.js +56 -39
  196. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +3 -1
  197. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +5 -3
  198. package/dist/prosemirror/plugins/suggestionMode.js +1 -0
  199. package/dist/prosemirror/revisionCarriers.js +19 -2
  200. package/dist/prosemirror/utils/extractTrackedChanges.d.ts +2 -2
  201. package/dist/prosemirror/utils/extractTrackedChanges.js +59 -23
  202. package/dist/prosemirror/validation.js +55 -27
  203. package/dist/prosemirror/zeroWidthAnchors.d.ts +10 -0
  204. package/dist/prosemirror/zeroWidthAnchors.js +23 -0
  205. package/dist/redline.js +12 -79
  206. package/dist/server.d.ts +4 -4
  207. package/dist/server.js +2 -2
  208. package/dist/shaping/placeRun.d.ts +51 -0
  209. package/dist/shaping/placeRun.js +84 -0
  210. package/dist/shaping/shaper.d.ts +92 -0
  211. package/dist/shaping/shaper.js +102 -0
  212. package/dist/types/block-id.d.ts +18 -2
  213. package/dist/types/block-id.js +49 -5
  214. package/dist/types/content.d.ts +2 -2
  215. package/dist/utils/clipboard.js +4 -3
  216. package/dist/utils/fontResolver.js +183 -0
  217. package/dist/utils/htmlComments.d.ts +17 -0
  218. package/dist/utils/htmlComments.js +22 -0
  219. package/dist/utils/scriptSegments.d.ts +12 -1
  220. package/dist/utils/scriptSegments.js +17 -1
  221. package/dist/version-comparison.d.ts +1 -1
  222. package/package.json +7 -3
@@ -16,6 +16,7 @@ import { resolveEffectiveTableCellFormatting } from "./effectiveTableCellFormatt
16
16
  import { marksToTextFormatting } from "./fromProseDoc.js";
17
17
  import { shadingToRunShadingAttrs } from "./runShadingMark.js";
18
18
  import { sdtAttrsFromProperties } from "./sdtAttrs.js";
19
+ import { panic } from "better-result";
19
20
  //#region src/prosemirror/conversion/toProseDoc.ts
20
21
  /**
21
22
  * Build a `nextTextBoxGroupId()` generator salted with a random per-load
@@ -358,7 +359,7 @@ function anchorPointComment(nodes, commentId) {
358
359
  function convertTrackedChange(change, markType, nextHyperlinkInstanceIndex, getInheritedRunFormatting, styleResolver, moveKind = null, textBoxAnchors) {
359
360
  const nodes = [];
360
361
  for (const item of change.content) if (item.type === "run") nodes.push(...convertRun(item, getInheritedRunFormatting(item.formatting), styleResolver, textBoxAnchors));
361
- else {
362
+ else if (item.type === "hyperlink") {
362
363
  const currentHyperlinkIndex = nextHyperlinkInstanceIndex();
363
364
  nodes.push(...convertHyperlink(item, {
364
365
  getInheritedRunFormatting,
@@ -366,7 +367,30 @@ function convertTrackedChange(change, markType, nextHyperlinkInstanceIndex, getI
366
367
  hyperlinkIndex: currentHyperlinkIndex,
367
368
  textBoxAnchors
368
369
  }));
369
- }
370
+ } else if (item.type === "simpleField" || item.type === "complexField") {
371
+ const fieldNode = convertField(item, {
372
+ getInheritedRunFormatting,
373
+ styleResolver,
374
+ nextHyperlinkInstanceIndex,
375
+ textBoxAnchors
376
+ });
377
+ if (fieldNode) nodes.push(fieldNode);
378
+ } else if (item.type === "insertion" || item.type === "deletion" || item.type === "moveFrom" || item.type === "moveTo") {
379
+ const nestedMarkType = item.type === "insertion" || item.type === "moveTo" ? "insertion" : "deletion";
380
+ const nestedMoveKind = item.type === "moveFrom" || item.type === "moveTo" ? item.type : null;
381
+ nodes.push(...convertTrackedChange(item, nestedMarkType, nextHyperlinkInstanceIndex, getInheritedRunFormatting, styleResolver, nestedMoveKind, textBoxAnchors));
382
+ } else if (item.type === "bookmarkStart") nodes.push(schema.node("bookmarkBoundary", {
383
+ type: "start",
384
+ id: item.id,
385
+ name: item.name,
386
+ colFirst: item.colFirst,
387
+ colLast: item.colLast
388
+ }));
389
+ else if (item.type === "bookmarkEnd") nodes.push(schema.node("bookmarkBoundary", {
390
+ type: "end",
391
+ id: item.id
392
+ }));
393
+ else panic(`Unsupported tracked-run content: ${JSON.stringify(item)}`);
370
394
  const mark = schema.marks[markType].create({
371
395
  revisionId: change.info.id,
372
396
  author: change.info.author,
@@ -375,12 +399,16 @@ function convertTrackedChange(change, markType, nextHyperlinkInstanceIndex, getI
375
399
  moveKind
376
400
  });
377
401
  return nodes.map((node) => {
402
+ if (node.marks.some(({ type }) => type.name === "insertion" || type.name === "deletion")) return node;
378
403
  if (canCarryTrackedRunMark(node, mark.type)) return node.mark(mark.addToSet(node.marks));
379
404
  return node;
380
405
  });
381
406
  }
382
407
  function canCarryTrackedRunMark(node, markType) {
383
- return node.isText || node.isInline && node.type.allowsMarkType(markType) && (node.type.name === "image" || node.type.name === "shape" || node.type.name === "hardBreak" || node.type.name === "tab" || node.type.name === "symbol" || node.type.name === "bookmarkBoundary" || node.type.name === "textBoxAnchor");
408
+ if (node.isText) return true;
409
+ if (!node.isInline) return false;
410
+ if (node.type.name === "field" || node.type.name === "structuredField") return true;
411
+ return node.type.allowsMarkType(markType) && (node.type.name === "image" || node.type.name === "shape" || node.type.name === "hardBreak" || node.type.name === "tab" || node.type.name === "symbol" || node.type.name === "bookmarkBoundary" || node.type.name === "textBoxAnchor");
384
412
  }
385
413
  /**
386
414
  * Convert ParagraphFormatting to ProseMirror paragraph attrs
@@ -1706,7 +1734,7 @@ function extractTextBoxes(paragraph, textBoxGroupId) {
1706
1734
  });
1707
1735
  continue;
1708
1736
  }
1709
- visitHyperlink(item, {
1737
+ if (item.type === "hyperlink") visitHyperlink(item, {
1710
1738
  ...context,
1711
1739
  trackedChange
1712
1740
  });
@@ -1965,6 +1993,15 @@ function findParagraphPageBreakPosition(paragraph) {
1965
1993
  for (const child of children) {
1966
1994
  if (child.type === "run" && visitRun(child)) return true;
1967
1995
  if (child.type === "hyperlink" && visitHyperlinkChildren(child)) return true;
1996
+ if (child.type === "simpleField") for (const fieldChild of child.content) {
1997
+ if (fieldChild.type === "run" && visitRun(fieldChild)) return true;
1998
+ if (fieldChild.type === "hyperlink" && visitHyperlinkChildren(fieldChild)) return true;
1999
+ }
2000
+ if (child.type === "complexField") {
2001
+ for (const run of child.fieldCode) if (visitRun(run)) return true;
2002
+ for (const run of child.fieldResult) if (visitRun(run)) return true;
2003
+ }
2004
+ if ((child.type === "insertion" || child.type === "deletion" || child.type === "moveFrom" || child.type === "moveTo") && visitRunOrHyperlinkList(child.content)) return true;
1968
2005
  }
1969
2006
  return false;
1970
2007
  }
@@ -1,8 +1,21 @@
1
1
  import { Extension } from "../types.js";
2
- import { EditorState, PluginKey } from "prosemirror-state";
2
+ import { EditorState, PluginKey, Transaction } from "prosemirror-state";
3
3
  import { Node } from "prosemirror-model";
4
4
  //#region src/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts
5
5
  declare const paraIdAllocatorKey: PluginKey<any>;
6
+ /**
7
+ * Transaction meta asking this plugin to derive the ids it mints from a
8
+ * caller-supplied seed instead of `Math.random()`.
9
+ *
10
+ * A live editor wants random ids: two people typing must not collide. A
11
+ * generator whose whole output has to be reproducible — the document compare —
12
+ * cannot afford them, and it is the only party that knows its run is meant to
13
+ * be deterministic, so it says so on its own transaction rather than leaving
14
+ * the plugin to guess.
15
+ */
16
+ declare const deterministicParaIdSeedKey: PluginKey<string>;
17
+ /** Ask the allocator to mint reproducible ids for the paragraphs `tr` creates. */
18
+ declare const requestDeterministicParaIds: (tr: Transaction, seed: string) => Transaction;
6
19
  /**
7
20
  * Allocate initial paragraph IDs before constructing editor state.
8
21
  *
@@ -14,4 +27,4 @@ declare const ensureParaIdsInDoc: (doc: Node) => Node;
14
27
  declare const ensureParaIdsInState: (state: EditorState) => EditorState;
15
28
  declare const ParaIdAllocatorExtension: (options?: Partial<{}> | undefined) => Extension;
16
29
  //#endregion
17
- export { ParaIdAllocatorExtension, ensureParaIdsInDoc, ensureParaIdsInState, paraIdAllocatorKey };
30
+ export { ParaIdAllocatorExtension, deterministicParaIdSeedKey, ensureParaIdsInDoc, ensureParaIdsInState, paraIdAllocatorKey, requestDeterministicParaIds };
@@ -1,4 +1,4 @@
1
- import { generateHexId } from "../../../utils/hexId.js";
1
+ import { deterministicHexId, generateHexId } from "../../../utils/hexId.js";
2
2
  import { createExtension } from "../create.js";
3
3
  import { ignoreTrackedChanges } from "./ParagraphChangeTrackerExtension.js";
4
4
  import { Fragment } from "prosemirror-model";
@@ -42,6 +42,35 @@ import { Plugin, PluginKey } from "prosemirror-state";
42
42
  * another one.
43
43
  */
44
44
  const paraIdAllocatorKey = new PluginKey("paraIdAllocator");
45
+ /**
46
+ * Transaction meta asking this plugin to derive the ids it mints from a
47
+ * caller-supplied seed instead of `Math.random()`.
48
+ *
49
+ * A live editor wants random ids: two people typing must not collide. A
50
+ * generator whose whole output has to be reproducible — the document compare —
51
+ * cannot afford them, and it is the only party that knows its run is meant to
52
+ * be deterministic, so it says so on its own transaction rather than leaving
53
+ * the plugin to guess.
54
+ */
55
+ const deterministicParaIdSeedKey = new PluginKey("deterministicParaIdSeed");
56
+ /** Ask the allocator to mint reproducible ids for the paragraphs `tr` creates. */
57
+ const requestDeterministicParaIds = (tr, seed) => tr.setMeta(deterministicParaIdSeedKey, seed);
58
+ /**
59
+ * A fresh id not already in `taken`: derived from `seed` and the paragraph's
60
+ * position when the caller asked for reproducibility, random otherwise. Both
61
+ * loops terminate — the id space is 2^31 wide and `taken` holds at most one
62
+ * entry per paragraph.
63
+ */
64
+ const mintParaId = (taken, seed, pos) => {
65
+ if (seed === null) {
66
+ let id = generateHexId();
67
+ while (taken.has(id)) id = generateHexId();
68
+ return id;
69
+ }
70
+ let id = deterministicHexId(`${seed}:${String(pos)}`);
71
+ for (let salt = 1; taken.has(id); salt++) id = deterministicHexId(`${seed}:${String(pos)}:${String(salt)}`);
72
+ return id;
73
+ };
45
74
  const isUsableParaId = (value) => typeof value === "string" && value.length > 0 && value !== "00000000";
46
75
  /**
47
76
  * For every valid paraId in the pre-transaction doc, the position its
@@ -70,7 +99,7 @@ const mapKeeperPositions = (oldDoc, transactions) => {
70
99
  });
71
100
  return keepers;
72
101
  };
73
- const collectParaIdUpdates = (doc, keeperPositions) => {
102
+ const collectParaIdUpdates = (doc, keeperPositions, deterministicSeed = null) => {
74
103
  const missing = [];
75
104
  const occurrencesById = /* @__PURE__ */ new Map();
76
105
  doc.descendants((node, pos) => {
@@ -100,8 +129,7 @@ const collectParaIdUpdates = (doc, keeperPositions) => {
100
129
  const taken = new Set(occurrencesById.keys());
101
130
  const updates = [];
102
131
  for (const { pos, attrs } of needFreshId) {
103
- let newId = generateHexId();
104
- while (taken.has(newId)) newId = generateHexId();
132
+ const newId = mintParaId(taken, deterministicSeed, pos);
105
133
  taken.add(newId);
106
134
  updates.push({
107
135
  pos,
@@ -182,7 +210,8 @@ const createParaIdAllocatorPlugin = () => new Plugin({
182
210
  appendTransaction(transactions, oldState, newState) {
183
211
  if (!transactions.some((t) => t.docChanged)) return null;
184
212
  const keeperPositions = mapKeeperPositions(oldState.doc, transactions);
185
- const updates = collectParaIdUpdates(newState.doc, keeperPositions);
213
+ const seed = transactions.map((transaction) => transaction.getMeta(deterministicParaIdSeedKey)).find((value) => typeof value === "string") ?? null;
214
+ const updates = collectParaIdUpdates(newState.doc, keeperPositions, seed);
186
215
  if (updates.length === 0) return null;
187
216
  const tr = newState.tr;
188
217
  for (const u of updates) tr.setNodeMarkup(u.pos, void 0, u.attrs);
@@ -200,4 +229,4 @@ const ParaIdAllocatorExtension = createExtension({
200
229
  }
201
230
  });
202
231
  //#endregion
203
- export { ParaIdAllocatorExtension, ensureParaIdsInDoc, ensureParaIdsInState, paraIdAllocatorKey };
232
+ export { ParaIdAllocatorExtension, deterministicParaIdSeedKey, ensureParaIdsInDoc, ensureParaIdsInState, paraIdAllocatorKey, requestDeterministicParaIds };
@@ -1,6 +1,6 @@
1
1
  import { createExtension } from "../create.js";
2
+ import { AddMarkStep, AddNodeMarkStep, AttrStep, RemoveMarkStep, RemoveNodeMarkStep } from "prosemirror-transform";
2
3
  import { Plugin, PluginKey } from "prosemirror-state";
3
- import { AddMarkStep, AddNodeMarkStep, RemoveMarkStep, RemoveNodeMarkStep } from "prosemirror-transform";
4
4
  //#region src/prosemirror/extensions/features/ParagraphChangeTrackerExtension.ts
5
5
  /**
6
6
  * Paragraph Change Tracker Extension
@@ -121,7 +121,7 @@ function createParagraphChangeTrackerPlugin() {
121
121
  if (hasUntracked) newState.hasUntrackedChanges = true;
122
122
  continue;
123
123
  }
124
- if (step instanceof AddNodeMarkStep || step instanceof RemoveNodeMarkStep) {
124
+ if (step instanceof AddNodeMarkStep || step instanceof RemoveNodeMarkStep || step instanceof AttrStep) {
125
125
  const pos = mapStepPosition(remap, step.pos, 1);
126
126
  const node = tr.doc.nodeAt(pos);
127
127
  if (!node) continue;
@@ -1,3 +1,4 @@
1
+ import { htmlCommentEnd } from "../../../utils/htmlComments.js";
1
2
  import { stripXmlDeclarations } from "../../../utils/stripXmlDeclarations.js";
2
3
  import { readBookmarkBoundaryAttrs } from "../../bookmarkBoundaryAttrs.js";
3
4
  import { Fragment, Slice } from "prosemirror-model";
@@ -61,9 +62,9 @@ function stripHtmlComments(html) {
61
62
  continue;
62
63
  }
63
64
  result += html.slice(cursor, commentStart);
64
- const commentEnd = html.indexOf("-->", commentStart + 4);
65
- if (commentEnd === -1) break;
66
- cursor = commentEnd + 3;
65
+ const commentEnd = htmlCommentEnd(html, commentStart);
66
+ if (commentEnd === null) break;
67
+ cursor = commentEnd;
67
68
  }
68
69
  return result;
69
70
  }
@@ -4,6 +4,8 @@ import { NodeExtension } from "../types.js";
4
4
  type BookmarkBoundaryOptions = {
5
5
  getInternalClipboardToken?: () => string;
6
6
  };
7
+ /** The node name, for callers asking whether a paragraph holds any content. */
8
+ declare const BOOKMARK_BOUNDARY_NODE_NAME = "bookmarkBoundary";
7
9
  declare const BookmarkBoundaryExtension: (options?: Partial<BookmarkBoundaryOptions> | undefined) => NodeExtension;
8
10
  //#endregion
9
- export { BookmarkBoundaryExtension };
11
+ export { BOOKMARK_BOUNDARY_NODE_NAME, BookmarkBoundaryExtension };
@@ -11,9 +11,11 @@ function readOptionalColumn(dom, attribute) {
11
11
  const value = dom.getAttribute(attribute);
12
12
  return value === null ? void 0 : readNonnegativeInteger(value);
13
13
  }
14
+ /** The node name, for callers asking whether a paragraph holds any content. */
15
+ const BOOKMARK_BOUNDARY_NODE_NAME = "bookmarkBoundary";
14
16
  const BookmarkBoundaryExtension = createNodeExtension({
15
- name: "bookmarkBoundary",
16
- schemaNodeName: "bookmarkBoundary",
17
+ name: BOOKMARK_BOUNDARY_NODE_NAME,
18
+ schemaNodeName: BOOKMARK_BOUNDARY_NODE_NAME,
17
19
  nodeSpec: (options) => ({
18
20
  inline: true,
19
21
  group: "inline",
@@ -64,4 +66,4 @@ const BookmarkBoundaryExtension = createNodeExtension({
64
66
  })
65
67
  });
66
68
  //#endregion
67
- export { BookmarkBoundaryExtension };
69
+ export { BOOKMARK_BOUNDARY_NODE_NAME, BookmarkBoundaryExtension };
@@ -1,6 +1,12 @@
1
1
  import { NodeExtension } from "../types.js";
2
2
  //#region src/prosemirror/extensions/nodes/RenderedPageBreakExtension.d.ts
3
3
  /** Zero-width cached pagination boundary preserved from DOCX. */
4
+ /**
5
+ * The node name, for callers that have to ask whether anything is LEFT in a
6
+ * paragraph. This node is zero-width and carries no revision of its own, so a
7
+ * paragraph holding only these holds nothing a reader would see.
8
+ */
9
+ declare const RENDERED_PAGE_BREAK_NODE_NAME = "renderedPageBreak";
4
10
  declare const RenderedPageBreakExtension: (options?: Partial<Record<string, unknown>> | undefined) => NodeExtension;
5
11
  //#endregion
6
- export { RenderedPageBreakExtension };
12
+ export { RENDERED_PAGE_BREAK_NODE_NAME, RenderedPageBreakExtension };
@@ -1,9 +1,15 @@
1
1
  import { createNodeExtension } from "../create.js";
2
2
  //#region src/prosemirror/extensions/nodes/RenderedPageBreakExtension.ts
3
3
  /** Zero-width cached pagination boundary preserved from DOCX. */
4
+ /**
5
+ * The node name, for callers that have to ask whether anything is LEFT in a
6
+ * paragraph. This node is zero-width and carries no revision of its own, so a
7
+ * paragraph holding only these holds nothing a reader would see.
8
+ */
9
+ const RENDERED_PAGE_BREAK_NODE_NAME = "renderedPageBreak";
4
10
  const RenderedPageBreakExtension = createNodeExtension({
5
- name: "renderedPageBreak",
6
- schemaNodeName: "renderedPageBreak",
11
+ name: RENDERED_PAGE_BREAK_NODE_NAME,
12
+ schemaNodeName: RENDERED_PAGE_BREAK_NODE_NAME,
7
13
  nodeSpec: {
8
14
  inline: true,
9
15
  group: "inline",
@@ -19,4 +25,4 @@ const RenderedPageBreakExtension = createNodeExtension({
19
25
  }
20
26
  });
21
27
  //#endregion
22
- export { RenderedPageBreakExtension };
28
+ export { RENDERED_PAGE_BREAK_NODE_NAME, RenderedPageBreakExtension };
@@ -730,51 +730,68 @@ const TablePluginExtension = createExtension({
730
730
  return true;
731
731
  };
732
732
  }
733
- function addRowAbove(state, dispatch) {
733
+ /**
734
+ * Insert an empty row so that it lands ABOVE grid row `boundaryRow`.
735
+ *
736
+ * A vertically merged cell that covers both sides of the boundary is
737
+ * EXTENDED, not duplicated: Word grows the merge through the inserted row
738
+ * (`w:vMerge="continue"` gains one row) rather than splitting it. Cloning
739
+ * the caret row cell-for-cell instead emitted a cell for a column the merge
740
+ * already occupies, which widens the grid and desynchronises every later
741
+ * row — the opposite of `deleteRow`, which is merge-correct because it
742
+ * delegates to prosemirror-tables.
743
+ *
744
+ * Only the columns the boundary actually opens get a new cell, and each is
745
+ * shaped from the cell covering that column so widths, borders, margins and
746
+ * text direction survive.
747
+ */
748
+ function insertTableRow(state, dispatch, boundaryOffset) {
734
749
  const context = getTableContext(state);
735
750
  if (!context.isInTable || context.rowIndex === void 0 || !context.table || context.tablePos === void 0) return false;
736
- if (dispatch) {
737
- const tr = state.tr;
738
- const rowNode = context.table.child(context.rowIndex);
739
- const cells = [];
740
- rowNode.forEach((cell) => {
741
- const paragraph = nodeTypeParagraph.create();
742
- const cellAttrs = buildCellAttrsFromTemplate(cell);
743
- cells.push(nodeTypeTableCell.create(cellAttrs, paragraph));
744
- });
745
- const newRow = nodeTypeTableRow.create({
746
- height: rowNode.attrs["height"] ?? 360,
747
- heightRule: rowNode.attrs["heightRule"] ?? "atLeast"
748
- }, cells);
749
- let rowPos = context.tablePos + 1;
750
- for (let i = 0; i < context.rowIndex; i++) rowPos += context.table.child(i).nodeSize;
751
- tr.insert(rowPos, newRow);
752
- dispatch(tr.scrollIntoView());
751
+ if (!dispatch) return true;
752
+ const { table, tablePos, rowIndex } = context;
753
+ const boundaryRow = rowIndex + boundaryOffset;
754
+ const map = TableMap.get(table);
755
+ const tableStart = tablePos + 1;
756
+ const tr = state.tr;
757
+ const cells = [];
758
+ const extended = /* @__PURE__ */ new Set();
759
+ let lastTemplatePos = -1;
760
+ for (let column = 0; column < map.width; column += 1) {
761
+ const below = boundaryRow < map.height ? map.map[boundaryRow * map.width + column] : void 0;
762
+ const above = boundaryRow > 0 ? map.map[(boundaryRow - 1) * map.width + column] : void 0;
763
+ if (below !== void 0 && below === above) {
764
+ if (!extended.has(below)) {
765
+ extended.add(below);
766
+ const merged = table.nodeAt(below);
767
+ if (merged) tr.setNodeMarkup(tableStart + below, void 0, {
768
+ ...merged.attrs,
769
+ rowspan: (Number(merged.attrs["rowspan"]) || 1) + 1
770
+ });
771
+ }
772
+ continue;
773
+ }
774
+ const templatePos = below ?? above;
775
+ if (templatePos === void 0 || templatePos === lastTemplatePos) continue;
776
+ lastTemplatePos = templatePos;
777
+ cells.push(nodeTypeTableCell.create(buildCellAttrsFromTemplate(table.nodeAt(templatePos)), nodeTypeParagraph.create()));
753
778
  }
779
+ const rowNode = table.child(rowIndex);
780
+ const newRow = nodeTypeTableRow.create({
781
+ height: rowNode.attrs["height"] ?? 360,
782
+ heightRule: rowNode.attrs["heightRule"] ?? "atLeast"
783
+ }, cells);
784
+ let rowPos = tableStart;
785
+ for (let i = 0; i < boundaryRow; i += 1) rowPos += table.child(i).nodeSize;
786
+ tr.insert(rowPos, newRow);
787
+ dispatch(tr.scrollIntoView());
754
788
  return true;
755
789
  }
790
+ function addRowAbove(state, dispatch) {
791
+ return insertTableRow(state, dispatch, 0);
792
+ }
756
793
  function addRowBelow(state, dispatch) {
757
- const context = getTableContext(state);
758
- if (!context.isInTable || context.rowIndex === void 0 || !context.table || context.tablePos === void 0) return false;
759
- if (dispatch) {
760
- const tr = state.tr;
761
- const rowNode = context.table.child(context.rowIndex);
762
- const cells = [];
763
- rowNode.forEach((cell) => {
764
- const paragraph = nodeTypeParagraph.create();
765
- const cellAttrs = buildCellAttrsFromTemplate(cell);
766
- cells.push(nodeTypeTableCell.create(cellAttrs, paragraph));
767
- });
768
- const newRow = nodeTypeTableRow.create({
769
- height: rowNode.attrs["height"] ?? 360,
770
- heightRule: rowNode.attrs["heightRule"] ?? "atLeast"
771
- }, cells);
772
- let rowPos = context.tablePos + 1;
773
- for (let i = 0; i <= context.rowIndex; i++) rowPos += context.table.child(i).nodeSize;
774
- tr.insert(rowPos, newRow);
775
- dispatch(tr.scrollIntoView());
776
- }
777
- return true;
794
+ return insertTableRow(state, dispatch, 1);
778
795
  }
779
796
  function deleteRow(state, dispatch) {
780
797
  const context = getTableContext(state);
@@ -10,6 +10,8 @@ import { NodeExtension } from "../types.js";
10
10
  type TextBoxAnchorOptions = {
11
11
  getInternalClipboardToken?: () => string;
12
12
  };
13
+ /** The node name, for callers asking whether a paragraph holds any content. */
14
+ declare const TEXT_BOX_ANCHOR_NODE_NAME = "textBoxAnchor";
13
15
  declare const TextBoxAnchorExtension: (options?: Partial<TextBoxAnchorOptions> | undefined) => NodeExtension;
14
16
  //#endregion
15
- export { TextBoxAnchorExtension };
17
+ export { TEXT_BOX_ANCHOR_NODE_NAME, TextBoxAnchorExtension };
@@ -8,9 +8,11 @@ import { createNodeExtension } from "../create.js";
8
8
  * map its position as surrounding text is edited. The save conversion consumes
9
9
  * it when the sibling text box is restored to the DOCX run stream.
10
10
  */
11
+ /** The node name, for callers asking whether a paragraph holds any content. */
12
+ const TEXT_BOX_ANCHOR_NODE_NAME = "textBoxAnchor";
11
13
  const TextBoxAnchorExtension = createNodeExtension({
12
- name: "textBoxAnchor",
13
- schemaNodeName: "textBoxAnchor",
14
+ name: TEXT_BOX_ANCHOR_NODE_NAME,
15
+ schemaNodeName: TEXT_BOX_ANCHOR_NODE_NAME,
14
16
  nodeSpec: (options) => ({
15
17
  inline: true,
16
18
  group: "inline",
@@ -39,4 +41,4 @@ const TextBoxAnchorExtension = createNodeExtension({
39
41
  })
40
42
  });
41
43
  //#endregion
42
- export { TextBoxAnchorExtension };
44
+ export { TEXT_BOX_ANCHOR_NODE_NAME, TextBoxAnchorExtension };
@@ -76,6 +76,7 @@ function markRangeAsDeleted(tr, doc, from, to, insertionType, deletionType, plug
76
76
  const end = Math.min(pos + node.nodeSize, to);
77
77
  if (start >= end) return;
78
78
  const isOwnInsert = node.marks.some((m) => m.type === insertionType && m.attrs["author"] === pluginState.author);
79
+ if (node.marks.some((m) => m.type === deletionType)) return;
79
80
  ranges.push({
80
81
  from: start,
81
82
  to: end,
@@ -1,5 +1,21 @@
1
1
  import { expectParagraphAttrs } from "./attrs/index.js";
2
+ import { PARAGRAPH_MARK_CHANGE_KINDS } from "@stll/docx-core/model";
2
3
  //#region src/prosemirror/revisionCarriers.ts
4
+ /**
5
+ * Which carrier each paragraph-mark kind reports as. A relocation's break
6
+ * resolves as the insertion or deletion it is; the kind exists so a reader is
7
+ * told the two ends belong together, not to change what resolving does.
8
+ *
9
+ * Total over the kinds by construction, so a kind the model gains has to be
10
+ * given a carrier here rather than silently reporting none.
11
+ */
12
+ const PARAGRAPH_MARK_KIND_CARRIERS = {
13
+ ins: "paragraphMarkInserted",
14
+ moveTo: "paragraphMarkInserted",
15
+ del: "paragraphMarkDeleted",
16
+ moveFrom: "paragraphMarkDeleted"
17
+ };
18
+ const isParagraphMarkChangeKind = (value) => PARAGRAPH_MARK_CHANGE_KINDS.some((kind) => kind === value);
3
19
  const isObjectRecord = (value) => typeof value === "object" && value !== null;
4
20
  const revisionMetadata = (value) => {
5
21
  if (!isObjectRecord(value) || typeof value["id"] !== "number") return null;
@@ -37,11 +53,12 @@ const getFolioNodeRevisionCarriers = (node, nodePos) => {
37
53
  const to = nodePos + node.nodeSize;
38
54
  const paragraphAttrs = expectParagraphAttrs(node);
39
55
  const paragraphMark = node.attrs["pPrMark"];
40
- if (isObjectRecord(paragraphMark) && (paragraphMark["kind"] === "ins" || paragraphMark["kind"] === "del")) {
56
+ const paragraphMarkKind = isObjectRecord(paragraphMark) && isParagraphMarkChangeKind(paragraphMark["kind"]) ? PARAGRAPH_MARK_KIND_CARRIERS[paragraphMark["kind"]] : void 0;
57
+ if (paragraphMarkKind !== void 0 && isObjectRecord(paragraphMark)) {
41
58
  const metadata = revisionMetadata(paragraphMark["info"]);
42
59
  if (metadata) carriers.push({
43
60
  ...metadata,
44
- type: paragraphMark["kind"] === "ins" ? "paragraphMarkInserted" : "paragraphMarkDeleted",
61
+ type: paragraphMarkKind,
45
62
  text: node.textContent,
46
63
  from,
47
64
  to
@@ -94,8 +94,8 @@ type TrackedChangesResult = {
94
94
  * Walk the PM doc and extract every tracked change as a flat list of
95
95
  * `TrackedChangeEntry` plus a comment→revision overlap map. Adjacent
96
96
  * inline marks coalesce by `(type, revisionId, author, date)`; a
97
- * deletion immediately followed by an insertion (same author + same
98
- * date) collapses into a single `replacement` entry; paragraph-mark
97
+ * deletion immediately followed by the same author's insertion
98
+ * collapses into a single `replacement` entry; paragraph-mark
99
99
  * cards (`paragraphMarkInsertion` / `paragraphMarkDeletion`) are
100
100
  * hidden when an inline entry already covers their revision triple
101
101
  * (one Accept clears every site of one conceptual change).
@@ -6,11 +6,28 @@ const EMPTY_RESULT = {
6
6
  commentToRevision: /* @__PURE__ */ new Map()
7
7
  };
8
8
  /**
9
+ * Fold a run's revision mark into the enclosing row's card when both halves
10
+ * are the same change. Folio writes both under one revision id; Word mints a
11
+ * fresh `w:id` per element, so an author + timestamp match counts too. Returns
12
+ * false for a third party's edit inside the row, which keeps its own card.
13
+ */
14
+ const foldIntoRowRevision = (scope, mark) => {
15
+ if (!scope || mark.type !== scope.markType) return false;
16
+ const revisionId = mark.attrs["revisionId"];
17
+ if (!(revisionId === scope.entry.revisionId || mark.attrs["author"] === scope.entry.author && mark.attrs["date"] === scope.entry.date)) return false;
18
+ if (revisionId !== scope.entry.revisionId) {
19
+ const ids = new Set(scope.entry.coalescedRevisionIds ?? []);
20
+ ids.add(revisionId);
21
+ scope.entry.coalescedRevisionIds = [...ids];
22
+ }
23
+ return true;
24
+ };
25
+ /**
9
26
  * Walk the PM doc and extract every tracked change as a flat list of
10
27
  * `TrackedChangeEntry` plus a comment→revision overlap map. Adjacent
11
28
  * inline marks coalesce by `(type, revisionId, author, date)`; a
12
- * deletion immediately followed by an insertion (same author + same
13
- * date) collapses into a single `replacement` entry; paragraph-mark
29
+ * deletion immediately followed by the same author's insertion
30
+ * collapses into a single `replacement` entry; paragraph-mark
14
31
  * cards (`paragraphMarkInsertion` / `paragraphMarkDeletion`) are
15
32
  * hidden when an inline entry already covers their revision triple
16
33
  * (one Accept clears every site of one conceptual change).
@@ -30,7 +47,9 @@ function extractTrackedChanges(state) {
30
47
  const raw = [];
31
48
  const commentToRevision = /* @__PURE__ */ new Map();
32
49
  const commentMetadata = /* @__PURE__ */ new Map();
50
+ const rowRevisionScopes = [];
33
51
  doc.descendants((node, pos) => {
52
+ while (pos >= (rowRevisionScopes.at(-1)?.end ?? Number.POSITIVE_INFINITY)) rowRevisionScopes.pop();
34
53
  if (node.type.name === "paragraph") {
35
54
  const ins = node.attrs["pPrIns"];
36
55
  const del = node.attrs["pPrDel"];
@@ -69,24 +88,40 @@ function extractTrackedChanges(state) {
69
88
  if (node.type.name === "tableRow") {
70
89
  const trIns = node.attrs["trIns"];
71
90
  const trDel = node.attrs["trDel"];
72
- if (trIns) raw.push({
73
- type: "rowInserted",
74
- text: node.textContent || "",
75
- author: trIns.author || "",
76
- date: trIns.date ?? void 0,
77
- from: pos,
78
- to: pos + node.nodeSize,
79
- revisionId: trIns.revisionId
80
- });
81
- if (trDel) raw.push({
82
- type: "rowDeleted",
83
- text: node.textContent || "",
84
- author: trDel.author || "",
85
- date: trDel.date ?? void 0,
86
- from: pos,
87
- to: pos + node.nodeSize,
88
- revisionId: trDel.revisionId
89
- });
91
+ if (trIns) {
92
+ const entry = {
93
+ type: "rowInserted",
94
+ text: node.textContent || "",
95
+ author: trIns.author || "",
96
+ date: trIns.date ?? void 0,
97
+ from: pos,
98
+ to: pos + node.nodeSize,
99
+ revisionId: trIns.revisionId
100
+ };
101
+ raw.push(entry);
102
+ rowRevisionScopes.push({
103
+ end: pos + node.nodeSize,
104
+ markType: insertionType,
105
+ entry
106
+ });
107
+ }
108
+ if (trDel) {
109
+ const entry = {
110
+ type: "rowDeleted",
111
+ text: node.textContent || "",
112
+ author: trDel.author || "",
113
+ date: trDel.date ?? void 0,
114
+ from: pos,
115
+ to: pos + node.nodeSize,
116
+ revisionId: trDel.revisionId
117
+ };
118
+ raw.push(entry);
119
+ rowRevisionScopes.push({
120
+ end: pos + node.nodeSize,
121
+ markType: deletionType,
122
+ entry
123
+ });
124
+ }
90
125
  const trPrChange = node.attrs["trPrChange"];
91
126
  if (Array.isArray(trPrChange)) for (const entry of trPrChange) {
92
127
  if (!entry?.info || typeof entry.info.id !== "number") continue;
@@ -185,7 +220,7 @@ function extractTrackedChanges(state) {
185
220
  });
186
221
  if (allShare) {
187
222
  let visibleText = "";
188
- if (deletionType) node.descendants((child) => {
223
+ if (deletionType && sharedAttr === "trIns") node.descendants((child) => {
189
224
  if (child.isText && !child.marks.some((m) => m.type === deletionType)) visibleText += child.text || "";
190
225
  });
191
226
  else visibleText = node.textContent || "";
@@ -224,6 +259,8 @@ function extractTrackedChanges(state) {
224
259
  continue;
225
260
  }
226
261
  if (mark.type === insertionType || mark.type === deletionType) {
262
+ tcMark = mark;
263
+ if (foldIntoRowRevision(rowRevisionScopes.at(-1), mark)) continue;
227
264
  raw.push({
228
265
  type: mark.type === insertionType ? "insertion" : "deletion",
229
266
  text: inlineText,
@@ -233,7 +270,6 @@ function extractTrackedChanges(state) {
233
270
  to: pos + node.nodeSize,
234
271
  revisionId: mark.attrs["revisionId"]
235
272
  });
236
- tcMark = mark;
237
273
  }
238
274
  }
239
275
  if (commentType) {
@@ -350,7 +386,7 @@ function extractTrackedChanges(state) {
350
386
  for (let i = 0; i < merged.length; i++) {
351
387
  const curr = merged[i];
352
388
  const next = merged[i + 1];
353
- if (curr.type === "deletion" && next && next.type === "insertion" && curr.author === next.author && curr.date === next.date && curr.to === next.from) {
389
+ if (curr.type === "deletion" && next && next.type === "insertion" && curr.author === next.author && curr.to === next.from) {
354
390
  final.push({
355
391
  type: "replacement",
356
392
  text: next.text,