smartrte-core 0.2.0 → 1.0.0-beta.1

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 (329) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +23 -0
  3. package/dist/command.d.ts +8 -8
  4. package/dist/editor.d.ts +32 -0
  5. package/dist/editor.js +121 -0
  6. package/dist/foundation/annotations/index.d.ts +2 -0
  7. package/dist/foundation/annotations/index.js +2 -0
  8. package/dist/foundation/annotations/range.d.ts +54 -0
  9. package/dist/foundation/annotations/range.js +158 -0
  10. package/dist/foundation/annotations/types.d.ts +33 -0
  11. package/dist/foundation/annotations/types.js +1 -0
  12. package/dist/foundation/atom/commands.d.ts +11 -0
  13. package/dist/foundation/atom/commands.js +118 -0
  14. package/dist/foundation/atom/composition.d.ts +8 -0
  15. package/dist/foundation/atom/composition.js +22 -0
  16. package/dist/foundation/atom/declarations.d.ts +2 -0
  17. package/dist/foundation/atom/declarations.js +17 -0
  18. package/dist/foundation/atom/formats.d.ts +21 -0
  19. package/dist/foundation/atom/formats.js +77 -0
  20. package/dist/foundation/atom/index.d.ts +10 -0
  21. package/dist/foundation/atom/index.js +10 -0
  22. package/dist/foundation/atom/lifecycle.d.ts +12 -0
  23. package/dist/foundation/atom/lifecycle.js +41 -0
  24. package/dist/foundation/atom/persistence.d.ts +3 -0
  25. package/dist/foundation/atom/persistence.js +20 -0
  26. package/dist/foundation/atom/plugin.d.ts +20 -0
  27. package/dist/foundation/atom/plugin.js +55 -0
  28. package/dist/foundation/atom/schema.d.ts +3 -0
  29. package/dist/foundation/atom/schema.js +29 -0
  30. package/dist/foundation/atom/security.d.ts +10 -0
  31. package/dist/foundation/atom/security.js +20 -0
  32. package/dist/foundation/atom/selection.d.ts +3 -0
  33. package/dist/foundation/atom/selection.js +4 -0
  34. package/dist/foundation/atom/types.d.ts +47 -0
  35. package/dist/foundation/atom/types.js +1 -0
  36. package/dist/foundation/block/commands.d.ts +17 -0
  37. package/dist/foundation/block/commands.js +197 -0
  38. package/dist/foundation/block/formats.d.ts +33 -0
  39. package/dist/foundation/block/formats.js +57 -0
  40. package/dist/foundation/block/index.d.ts +5 -0
  41. package/dist/foundation/block/index.js +5 -0
  42. package/dist/foundation/block/input.d.ts +19 -0
  43. package/dist/foundation/block/input.js +67 -0
  44. package/dist/foundation/block/plugin.d.ts +26 -0
  45. package/dist/foundation/block/plugin.js +79 -0
  46. package/dist/foundation/block/schema.d.ts +49 -0
  47. package/dist/foundation/block/schema.js +27 -0
  48. package/dist/foundation/block/types.d.ts +40 -0
  49. package/dist/foundation/block/types.js +1 -0
  50. package/dist/foundation/boundaries.d.ts +11 -0
  51. package/dist/foundation/boundaries.js +117 -0
  52. package/dist/foundation/clipboard/detection.d.ts +3 -0
  53. package/dist/foundation/clipboard/detection.js +29 -0
  54. package/dist/foundation/clipboard/diagnostics.d.ts +24 -0
  55. package/dist/foundation/clipboard/diagnostics.js +59 -0
  56. package/dist/foundation/clipboard/index.d.ts +8 -0
  57. package/dist/foundation/clipboard/index.js +8 -0
  58. package/dist/foundation/clipboard/insertion.d.ts +22 -0
  59. package/dist/foundation/clipboard/insertion.js +210 -0
  60. package/dist/foundation/clipboard/normalizers.d.ts +8 -0
  61. package/dist/foundation/clipboard/normalizers.js +229 -0
  62. package/dist/foundation/clipboard/pipeline.d.ts +13 -0
  63. package/dist/foundation/clipboard/pipeline.js +58 -0
  64. package/dist/foundation/clipboard/sanitizer.d.ts +3 -0
  65. package/dist/foundation/clipboard/sanitizer.js +58 -0
  66. package/dist/foundation/clipboard/serialization.d.ts +5 -0
  67. package/dist/foundation/clipboard/serialization.js +33 -0
  68. package/dist/foundation/clipboard/transfer.d.ts +11 -0
  69. package/dist/foundation/clipboard/transfer.js +108 -0
  70. package/dist/foundation/clipboard/types.d.ts +65 -0
  71. package/dist/foundation/clipboard/types.js +1 -0
  72. package/dist/foundation/collab/index.d.ts +3 -0
  73. package/dist/foundation/collab/index.js +3 -0
  74. package/dist/foundation/collab/rebase.d.ts +28 -0
  75. package/dist/foundation/collab/rebase.js +82 -0
  76. package/dist/foundation/collab/transform.d.ts +73 -0
  77. package/dist/foundation/collab/transform.js +251 -0
  78. package/dist/foundation/collab/types.d.ts +52 -0
  79. package/dist/foundation/collab/types.js +1 -0
  80. package/dist/foundation/comments/fromSelection.d.ts +16 -0
  81. package/dist/foundation/comments/fromSelection.js +24 -0
  82. package/dist/foundation/comments/index.d.ts +3 -0
  83. package/dist/foundation/comments/index.js +3 -0
  84. package/dist/foundation/comments/rebase.d.ts +31 -0
  85. package/dist/foundation/comments/rebase.js +43 -0
  86. package/dist/foundation/comments/types.d.ts +37 -0
  87. package/dist/foundation/comments/types.js +1 -0
  88. package/dist/foundation/diff/documentDiff.d.ts +12 -0
  89. package/dist/foundation/diff/documentDiff.js +148 -0
  90. package/dist/foundation/diff/index.d.ts +3 -0
  91. package/dist/foundation/diff/index.js +3 -0
  92. package/dist/foundation/diff/textDiff.d.ts +12 -0
  93. package/dist/foundation/diff/textDiff.js +114 -0
  94. package/dist/foundation/diff/types.d.ts +74 -0
  95. package/dist/foundation/diff/types.js +1 -0
  96. package/dist/foundation/editor.d.ts +138 -0
  97. package/dist/foundation/editor.js +398 -0
  98. package/dist/foundation/formats/codec.d.ts +45 -0
  99. package/dist/foundation/formats/codec.js +1 -0
  100. package/dist/foundation/formats/docx/export.d.ts +4 -0
  101. package/dist/foundation/formats/docx/export.js +285 -0
  102. package/dist/foundation/formats/docx/import.d.ts +9 -0
  103. package/dist/foundation/formats/docx/import.js +37 -0
  104. package/dist/foundation/formats/docx/nestingGuard.d.ts +40 -0
  105. package/dist/foundation/formats/docx/nestingGuard.js +66 -0
  106. package/dist/foundation/formats/docx/portableAtoms.d.ts +4 -0
  107. package/dist/foundation/formats/docx/portableAtoms.js +66 -0
  108. package/dist/foundation/formats/docx/styledImport.d.ts +10 -0
  109. package/dist/foundation/formats/docx/styledImport.js +205 -0
  110. package/dist/foundation/formats/featureCodecs.d.ts +37 -0
  111. package/dist/foundation/formats/featureCodecs.js +69 -0
  112. package/dist/foundation/formats/fidelity.d.ts +31 -0
  113. package/dist/foundation/formats/fidelity.js +123 -0
  114. package/dist/foundation/formats/index.d.ts +7 -0
  115. package/dist/foundation/formats/index.js +7 -0
  116. package/dist/foundation/formats/pdf/format.d.ts +29 -0
  117. package/dist/foundation/formats/pdf/format.js +190 -0
  118. package/dist/foundation/history.d.ts +20 -0
  119. package/dist/foundation/history.js +121 -0
  120. package/dist/foundation/identity.d.ts +8 -0
  121. package/dist/foundation/identity.js +30 -0
  122. package/dist/foundation/index.d.ts +30 -0
  123. package/dist/foundation/index.js +30 -0
  124. package/dist/foundation/list/commands.d.ts +28 -0
  125. package/dist/foundation/list/commands.js +422 -0
  126. package/dist/foundation/list/formats.d.ts +20 -0
  127. package/dist/foundation/list/formats.js +795 -0
  128. package/dist/foundation/list/index.d.ts +8 -0
  129. package/dist/foundation/list/index.js +8 -0
  130. package/dist/foundation/list/input.d.ts +35 -0
  131. package/dist/foundation/list/input.js +320 -0
  132. package/dist/foundation/list/plugin.d.ts +21 -0
  133. package/dist/foundation/list/plugin.js +71 -0
  134. package/dist/foundation/list/presets.d.ts +16 -0
  135. package/dist/foundation/list/presets.js +21 -0
  136. package/dist/foundation/list/schema.d.ts +35 -0
  137. package/dist/foundation/list/schema.js +29 -0
  138. package/dist/foundation/list/shadow.d.ts +54 -0
  139. package/dist/foundation/list/shadow.js +75 -0
  140. package/dist/foundation/list/shadowCorpus.d.ts +10 -0
  141. package/dist/foundation/list/shadowCorpus.js +41 -0
  142. package/dist/foundation/list/types.d.ts +50 -0
  143. package/dist/foundation/list/types.js +1 -0
  144. package/dist/foundation/mapping.d.ts +11 -0
  145. package/dist/foundation/mapping.js +40 -0
  146. package/dist/foundation/marks/canonical.d.ts +9 -0
  147. package/dist/foundation/marks/canonical.js +72 -0
  148. package/dist/foundation/marks/commands.d.ts +21 -0
  149. package/dist/foundation/marks/commands.js +136 -0
  150. package/dist/foundation/marks/dom.d.ts +2 -0
  151. package/dist/foundation/marks/dom.js +46 -0
  152. package/dist/foundation/marks/editor.d.ts +8 -0
  153. package/dist/foundation/marks/editor.js +50 -0
  154. package/dist/foundation/marks/formats.d.ts +37 -0
  155. package/dist/foundation/marks/formats.js +49 -0
  156. package/dist/foundation/marks/hardBreak.d.ts +8 -0
  157. package/dist/foundation/marks/hardBreak.js +80 -0
  158. package/dist/foundation/marks/index.d.ts +11 -0
  159. package/dist/foundation/marks/index.js +11 -0
  160. package/dist/foundation/marks/normalization.d.ts +2 -0
  161. package/dist/foundation/marks/normalization.js +49 -0
  162. package/dist/foundation/marks/plugin.d.ts +14 -0
  163. package/dist/foundation/marks/plugin.js +63 -0
  164. package/dist/foundation/marks/resolveMarkRun.d.ts +8 -0
  165. package/dist/foundation/marks/resolveMarkRun.js +47 -0
  166. package/dist/foundation/marks/schema.d.ts +167 -0
  167. package/dist/foundation/marks/schema.js +50 -0
  168. package/dist/foundation/marks/stored.d.ts +2 -0
  169. package/dist/foundation/marks/stored.js +31 -0
  170. package/dist/foundation/marks/types.d.ts +40 -0
  171. package/dist/foundation/marks/types.js +1 -0
  172. package/dist/foundation/modelDom.d.ts +38 -0
  173. package/dist/foundation/modelDom.js +327 -0
  174. package/dist/foundation/normalization.d.ts +22 -0
  175. package/dist/foundation/normalization.js +132 -0
  176. package/dist/foundation/operations.d.ts +28 -0
  177. package/dist/foundation/operations.js +568 -0
  178. package/dist/foundation/plugin/builtins.d.ts +10 -0
  179. package/dist/foundation/plugin/builtins.js +27 -0
  180. package/dist/foundation/plugin/dispatch.d.ts +33 -0
  181. package/dist/foundation/plugin/dispatch.js +34 -0
  182. package/dist/foundation/plugin/index.d.ts +4 -0
  183. package/dist/foundation/plugin/index.js +4 -0
  184. package/dist/foundation/plugin/registry.d.ts +36 -0
  185. package/dist/foundation/plugin/registry.js +115 -0
  186. package/dist/foundation/plugin/types.d.ts +141 -0
  187. package/dist/foundation/plugin/types.js +13 -0
  188. package/dist/foundation/positions.d.ts +20 -0
  189. package/dist/foundation/positions.js +121 -0
  190. package/dist/foundation/schema.d.ts +57 -0
  191. package/dist/foundation/schema.js +371 -0
  192. package/dist/foundation/schemaBuilder.d.ts +41 -0
  193. package/dist/foundation/schemaBuilder.js +96 -0
  194. package/dist/foundation/scope/index.d.ts +2 -0
  195. package/dist/foundation/scope/index.js +2 -0
  196. package/dist/foundation/scope/resolveScope.d.ts +13 -0
  197. package/dist/foundation/scope/resolveScope.js +860 -0
  198. package/dist/foundation/scope/types.d.ts +122 -0
  199. package/dist/foundation/scope/types.js +1 -0
  200. package/dist/foundation/security/urlPolicy.d.ts +21 -0
  201. package/dist/foundation/security/urlPolicy.js +75 -0
  202. package/dist/foundation/structural/contentTarget.d.ts +21 -0
  203. package/dist/foundation/structural/contentTarget.js +51 -0
  204. package/dist/foundation/structural/index.d.ts +2 -0
  205. package/dist/foundation/structural/index.js +2 -0
  206. package/dist/foundation/structural/move.d.ts +7 -0
  207. package/dist/foundation/structural/move.js +35 -0
  208. package/dist/foundation/suggestions/index.d.ts +3 -0
  209. package/dist/foundation/suggestions/index.js +3 -0
  210. package/dist/foundation/suggestions/inline.d.ts +64 -0
  211. package/dist/foundation/suggestions/inline.js +116 -0
  212. package/dist/foundation/suggestions/structural.d.ts +40 -0
  213. package/dist/foundation/suggestions/structural.js +47 -0
  214. package/dist/foundation/suggestions/types.d.ts +19 -0
  215. package/dist/foundation/suggestions/types.js +1 -0
  216. package/dist/foundation/surface/index.d.ts +3 -0
  217. package/dist/foundation/surface/index.js +3 -0
  218. package/dist/foundation/surface/input.d.ts +127 -0
  219. package/dist/foundation/surface/input.js +1761 -0
  220. package/dist/foundation/surface/renderer.d.ts +84 -0
  221. package/dist/foundation/surface/renderer.js +974 -0
  222. package/dist/foundation/surface/types.d.ts +48 -0
  223. package/dist/foundation/surface/types.js +1 -0
  224. package/dist/foundation/table/commands.d.ts +31 -0
  225. package/dist/foundation/table/commands.js +470 -0
  226. package/dist/foundation/table/formats.d.ts +26 -0
  227. package/dist/foundation/table/formats.js +40 -0
  228. package/dist/foundation/table/grid.d.ts +8 -0
  229. package/dist/foundation/table/grid.js +178 -0
  230. package/dist/foundation/table/index.d.ts +6 -0
  231. package/dist/foundation/table/index.js +6 -0
  232. package/dist/foundation/table/plugin.d.ts +23 -0
  233. package/dist/foundation/table/plugin.js +119 -0
  234. package/dist/foundation/table/schema.d.ts +2 -0
  235. package/dist/foundation/table/schema.js +38 -0
  236. package/dist/foundation/table/selection.d.ts +7 -0
  237. package/dist/foundation/table/selection.js +35 -0
  238. package/dist/foundation/table/types.d.ts +109 -0
  239. package/dist/foundation/table/types.js +1 -0
  240. package/dist/foundation/transactions.d.ts +41 -0
  241. package/dist/foundation/transactions.js +90 -0
  242. package/dist/foundation/types.d.ts +261 -0
  243. package/dist/foundation/types.js +1 -0
  244. package/dist/foundation/versioning/diffVersions.d.ts +5 -0
  245. package/dist/foundation/versioning/diffVersions.js +3 -0
  246. package/dist/foundation/versioning/index.d.ts +2 -0
  247. package/dist/foundation/versioning/index.js +2 -0
  248. package/dist/foundation/versioning/types.d.ts +17 -0
  249. package/dist/foundation/versioning/types.js +1 -0
  250. package/dist/history.d.ts +20 -0
  251. package/dist/history.js +165 -0
  252. package/dist/html/compatibility.js +7 -1
  253. package/dist/index.d.ts +1 -14
  254. package/dist/index.js +9 -14
  255. package/dist/legacy/index.d.ts +45 -0
  256. package/dist/legacy/index.js +45 -0
  257. package/dist/legacyCommands/alignment.d.ts +20 -0
  258. package/dist/{commands → legacyCommands}/alignment.js +17 -0
  259. package/dist/legacyCommands/blocks.d.ts +14 -0
  260. package/dist/legacyCommands/blocks.js +105 -0
  261. package/dist/legacyCommands/checklist.d.ts +11 -0
  262. package/dist/legacyCommands/checklist.js +45 -0
  263. package/dist/legacyCommands/code.d.ts +15 -0
  264. package/dist/{commands → legacyCommands}/code.js +25 -1
  265. package/dist/legacyCommands/formula.d.ts +7 -0
  266. package/dist/legacyCommands/formula.js +67 -0
  267. package/dist/legacyCommands/inlineAtoms.d.ts +7 -0
  268. package/dist/legacyCommands/inlineAtoms.js +58 -0
  269. package/dist/legacyCommands/list.d.ts +38 -0
  270. package/dist/legacyCommands/list.js +477 -0
  271. package/dist/{commands → legacyCommands}/marks.d.ts +3 -0
  272. package/dist/{commands → legacyCommands}/marks.js +45 -11
  273. package/dist/legacyCommands/media.d.ts +31 -0
  274. package/dist/legacyCommands/media.js +205 -0
  275. package/dist/legacyCommands/move.d.ts +14 -0
  276. package/dist/legacyCommands/move.js +91 -0
  277. package/dist/legacyCommands/table.d.ts +91 -0
  278. package/dist/legacyCommands/table.js +547 -0
  279. package/dist/listPresets.d.ts +12 -0
  280. package/dist/listPresets.js +6 -0
  281. package/dist/listScope.d.ts +27 -0
  282. package/dist/listScope.js +104 -0
  283. package/dist/markdown/compatibility.d.ts +2 -0
  284. package/dist/markdown/compatibility.js +94 -2
  285. package/dist/marks.d.ts +5 -5
  286. package/dist/model.d.ts +65 -9
  287. package/dist/plugin.d.ts +15 -0
  288. package/dist/plugin.js +31 -0
  289. package/dist/plugins/alignment.d.ts +3 -0
  290. package/dist/plugins/alignment.js +6 -0
  291. package/dist/plugins/basicFormatting.d.ts +9 -0
  292. package/dist/plugins/basicFormatting.js +24 -0
  293. package/dist/plugins/blockType.d.ts +3 -0
  294. package/dist/plugins/blockType.js +6 -0
  295. package/dist/plugins/blockquote.d.ts +3 -0
  296. package/dist/plugins/blockquote.js +6 -0
  297. package/dist/plugins/checklist.d.ts +3 -0
  298. package/dist/plugins/checklist.js +10 -0
  299. package/dist/plugins/codeBlock.d.ts +3 -0
  300. package/dist/plugins/codeBlock.js +6 -0
  301. package/dist/plugins/formula.d.ts +3 -0
  302. package/dist/plugins/formula.js +9 -0
  303. package/dist/plugins/list.d.ts +3 -0
  304. package/dist/plugins/list.js +10 -0
  305. package/dist/plugins/media.d.ts +3 -0
  306. package/dist/plugins/media.js +12 -0
  307. package/dist/plugins/move.d.ts +3 -0
  308. package/dist/plugins/move.js +9 -0
  309. package/dist/plugins/table.d.ts +3 -0
  310. package/dist/plugins/table.js +22 -0
  311. package/dist/preset.d.ts +12 -0
  312. package/dist/preset.js +40 -0
  313. package/dist/schema.d.ts +22 -0
  314. package/dist/schema.js +333 -0
  315. package/dist/security/urlPolicy.d.ts +1 -17
  316. package/dist/security/urlPolicy.js +3 -69
  317. package/dist/selection.d.ts +1 -1
  318. package/dist/selectionMapping.d.ts +4 -0
  319. package/dist/selectionMapping.js +222 -0
  320. package/dist/table.d.ts +3 -3
  321. package/dist/transaction.d.ts +35 -12
  322. package/dist/transaction.js +174 -28
  323. package/dist/tree.d.ts +8 -2
  324. package/dist/tree.js +44 -0
  325. package/package.json +27 -8
  326. package/dist/commands/alignment.d.ts +0 -18
  327. package/dist/commands/code.d.ts +0 -13
  328. package/dist/commands/list.d.ts +0 -20
  329. package/dist/commands/list.js +0 -52
@@ -0,0 +1,82 @@
1
+ import { FoundationTransactionMap } from "../mapping.js";
2
+ import { transformOperation } from "./transform.js";
3
+ /**
4
+ * Deterministic total order for the exact-tie case (see mapOperation's own
5
+ * tieBreakBias doc comment) - lexicographic authorId comparison, computed
6
+ * once per (local, remote) transaction pair so every operation pair within
7
+ * that pairing agrees. Falls back to `1` (mapOperation's existing default)
8
+ * when either side has no authorId, matching this codebase's convention
9
+ * that authorId is optional (single-writer/no-transport usage never sets
10
+ * it, and never hits a real tie to begin with).
11
+ */
12
+ const tieBreakBias = (localAuthorId, remoteAuthorId) => {
13
+ if (!localAuthorId || !remoteAuthorId || localAuthorId === remoteAuthorId)
14
+ return 1;
15
+ return localAuthorId < remoteAuthorId ? -1 : 1;
16
+ };
17
+ /**
18
+ * Rebases `transaction` (whose `baseRevision` is behind the current
19
+ * revision) through every transaction committed since, in commit order.
20
+ * Each of `transaction`'s own operations is threaded through every
21
+ * operation of every missed transaction in sequence - the standard
22
+ * single-step operational-transform composition, built on `mapOperation`/
23
+ * `transformOperation`.
24
+ *
25
+ * Atomic per transaction: if any one operation conflicts, the whole
26
+ * transaction is rejected as a conflict rather than partially applied -
27
+ * matching `applyTransactionAtomic`'s own existing all-or-nothing
28
+ * semantics, and avoiding a confusing state where only part of a user's
29
+ * intent materializes.
30
+ *
31
+ * Does not check retention/resync - `missed` is whatever the caller
32
+ * supplies; a caller backed by a bounded revision log is responsible for
33
+ * returning `resync-required` itself before ever calling this when the
34
+ * requested history has already been evicted (see editor.ts's dispatch).
35
+ *
36
+ * `newBaseRevision` is the revision the rebased transaction should be
37
+ * stamped with (i.e. the live revision the caller is about to apply
38
+ * against) - taken explicitly from the caller rather than inferred from
39
+ * `missed`'s own entries, so a gap or reordering bug in how the caller
40
+ * assembled `missed` can't silently produce a wrong revision stamp here.
41
+ */
42
+ export const rebaseTransaction = (transaction, missed, newBaseRevision) => {
43
+ if (!missed.length)
44
+ return { kind: "ok", transaction: { ...transaction, baseRevision: newBaseRevision } };
45
+ const conflicts = [];
46
+ const rebasedOperations = transaction.operations.map((operation, operationIndex) => {
47
+ let current = operation;
48
+ for (const remote of missed) {
49
+ if (current === null)
50
+ break;
51
+ const bias = tieBreakBias(transaction.metadata.authorId, remote.metadata.authorId);
52
+ for (const through of remote.operations) {
53
+ if (current === null)
54
+ break;
55
+ const result = transformOperation(current, through, { tieBreakBias: bias });
56
+ if (result.kind === "conflict") {
57
+ conflicts.push({ operationIndex, reason: result.reason });
58
+ current = null;
59
+ }
60
+ else {
61
+ current = result.kind === "dropped" ? null : result.operation;
62
+ }
63
+ }
64
+ }
65
+ return current;
66
+ });
67
+ if (conflicts.length)
68
+ return { kind: "conflict", reasons: conflicts };
69
+ const survivors = rebasedOperations.filter((operation) => operation !== null);
70
+ if (!survivors.length)
71
+ return { kind: "dropped" };
72
+ const missedOperations = missed.flatMap((remote) => remote.operations);
73
+ const selectionMap = new FoundationTransactionMap(missedOperations);
74
+ const rebased = {
75
+ ...transaction,
76
+ baseRevision: newBaseRevision,
77
+ operations: survivors,
78
+ selectionBefore: selectionMap.mapSelection(transaction.selectionBefore),
79
+ selectionAfter: selectionMap.mapSelection(transaction.selectionAfter),
80
+ };
81
+ return { kind: "ok", transaction: rebased };
82
+ };
@@ -0,0 +1,73 @@
1
+ import type { SmartOperation } from "../types.js";
2
+ import type { TransformResult } from "./types.js";
3
+ /**
4
+ * Transforms `operation` through a single concurrent `through` operation,
5
+ * classifying the small number of shapes this codebase's operation model
6
+ * cannot safely auto-resolve rather than silently deferring to
7
+ * `mapOperation`'s uniform `null` for all of them (see this module's
8
+ * `types.ts` doc comment on `TransformResult`).
9
+ *
10
+ * Two conflict shapes, both decided explicitly (2026-08-27, product
11
+ * decision) rather than attempted automatically:
12
+ *
13
+ * 1. **Concurrent moveNode vs. *any* structural operation touching the same
14
+ * parent array as its `from` or `to`.** `moveNode.to` is defined
15
+ * relative to "the array with this move's own source already removed"
16
+ * (operations.ts's applyToSession) - a convention with no well-defined
17
+ * composition once a concurrent structural change *also* touches that
18
+ * same array, even one that doesn't touch the moved node itself.
19
+ * Confirmed empirically via property tests, in increasing generality:
20
+ * two moves of *different* nodes to nearby destinations produced
21
+ * different final orders depending on transform direction; a move
22
+ * composed with a same-node remove crashed outright; and - the broadest
23
+ * case - a moveNode transformed through a plain removeNode of a
24
+ * *completely unrelated* node in the same array still diverged (moving
25
+ * "to offset 6" means different things depending on whether that offset
26
+ * is read before or after the unrelated removal is accounted for).
27
+ * Fixing this in general means redefining `to` for every existing
28
+ * moveNode caller (table row/column reorder, list item reorder) - out of
29
+ * scope for this phase. Flagged as a conflict for *any* concurrent
30
+ * structural operation (insertNode/removeNode/replaceNode/moveNode) on
31
+ * the same parent array, not narrowed to same-node cases only.
32
+ * 2. **Concurrent edit to content a `removeNode.mergedInto`/
33
+ * `replaceNode.retiredInto` operation absorbed.** `mapOperation` already
34
+ * ignores these fields entirely (by design - see the SmartOperation type
35
+ * comment; they were built in Phase 12a only for `rebaseAnnotationRange`,
36
+ * a different, ID-keyed mechanism). Auto-redirecting an *operation*
37
+ * (rather than an annotation range's endpoint) to the survivor has its
38
+ * own correctness risk even for whole-node-targeting ops - e.g. a
39
+ * concurrent `setNodeAttributes` redirected to the survivor would carry
40
+ * a `before` payload captured against the *retired* node's attrs, which
41
+ * `applyOperation`'s strict before-check would then reject against the
42
+ * survivor's actual attrs. Flagged as a conflict, consistent with
43
+ * decision 1, rather than attempting a redirect whose correctness isn't
44
+ * established.
45
+ * 3. **Concurrent mergeNode vs. anything occupying its left (survivor) or
46
+ * right (retired) slot** (2026-08-28, required follow-up to finding #1
47
+ * in mapoperation-position-arithmetic-gaps.md). `applyOperation`'s own
48
+ * mergeNode handling validates `splitOffset === left.children.length`
49
+ * and that left/right are structurally compatible - both checks depend
50
+ * on the actual identity of whatever occupies those two slots.
51
+ * `mapPosThroughOperation`'s mergeNode-as-through branch only tracks a
52
+ * single explicit position (`operation.pos`/`.from`/`.to`), so an
53
+ * ordinary sibling-index shift correctly keeps a *retiring* target
54
+ * reachable but has no way to notice that a node's *identity* changed
55
+ * underneath an *implicit* left-neighbor reference. Confirmed
56
+ * empirically: two mergeNode ops retiring adjacent siblings (A retires
57
+ * node N into its left neighbor, B retires N's *next* sibling into N)
58
+ * crashed in one transform direction and silently produced a different
59
+ * document in the other; a mergeNode composed with a concurrent
60
+ * `removeNode` of its own left/survivor node crashed both ways. A
61
+ * concurrent `splitNode` targeting the *exact* node a mergeNode retires
62
+ * was checked and found *not* to need this treatment - it already gets
63
+ * `mapPosThroughOperation`'s explicit "this position IS the retired
64
+ * node, redirect into the survivor" case, which correctly recomputes an
65
+ * in-bounds position rather than carrying a stale content-derived
66
+ * payload the way `mergeNode.splitOffset` does. Same reasoning as
67
+ * finding 1 above: not narrowed further, since fixing this generally
68
+ * would mean giving `mapOperation` document access it deliberately
69
+ * doesn't have.
70
+ */
71
+ export declare const transformOperation: (operation: SmartOperation, through: SmartOperation, options?: {
72
+ tieBreakBias?: -1 | 1;
73
+ }) => TransformResult;
@@ -0,0 +1,251 @@
1
+ import { mapOperation } from "../operations.js";
2
+ import { samePath } from "../positions.js";
3
+ /** `[...pos.path, pos.offset]` - the full path to whatever slot/child `pos` addresses, for whole-node operations (insertNode/removeNode/replaceNode/setNodeAttributes/setNodeType). */
4
+ const slotPath = (pos) => [...pos.path, pos.offset];
5
+ /** True when `pos` (a container-relative position, e.g. an insertText/deleteText/addMark endpoint) sits at-or-within the subtree rooted at `subtreeRoot`'s slot. Mirrors the removeNode-vs-descendant check already established in mapPosThroughOperation. */
6
+ const isWithinSubtree = (pos, subtreeRoot) => {
7
+ const root = slotPath(subtreeRoot);
8
+ return samePath(pos.path.slice(0, root.length), root);
9
+ };
10
+ /** True when `pos` addresses the exact same slot as `subtreeRoot` (a whole-node operation targeting that node directly, not something inside it). */
11
+ const isExactlyAtSlot = (pos, subtreeRoot) => samePath(pos.path, subtreeRoot.path) && pos.offset === subtreeRoot.offset;
12
+ /**
13
+ * The two "occupied slot" references a mergeNode operation cares about:
14
+ * the surviving left sibling (`pos.offset - 1`) and the retired right
15
+ * sibling (`pos` itself). `applyOperation`'s own mergeNode handling
16
+ * validates `splitOffset === left.children.length` and that left/right are
17
+ * structurally compatible - both checks depend on the *actual identity* of
18
+ * whatever currently occupies these two slots, not just their positions.
19
+ */
20
+ const mergeNodeSlots = (op) => [
21
+ { path: op.pos.path, offset: op.pos.offset - 1 },
22
+ { path: op.pos.path, offset: op.pos.offset },
23
+ ];
24
+ /**
25
+ * The exact-slot reference for operation types that identify a node by
26
+ * "parent + child index" (the same convention mergeNode's own slots use) -
27
+ * restricted to the types where an exact offset match genuinely means "the
28
+ * same node," which excludes insertNode (a *gap* reference, not an
29
+ * occupied slot - two different conventions sharing the same {path,
30
+ * offset} shape, the same distinction already established for the
31
+ * removeNode/replaceNode exact-match fix in mapOperation) and excludes
32
+ * splitNode/setNodeAttributes/insertText/deleteText/addMark (deeper or
33
+ * differently-conventioned references that `mapPosThroughOperation`'s own
34
+ * mergeNode-as-through branch already redirects safely - confirmed via a
35
+ * property test that a splitNode targeting the exact node a concurrent
36
+ * mergeNode retires converges correctly without needing to be flagged
37
+ * here).
38
+ */
39
+ const occupiedSlotsOf = (op) => {
40
+ if (op.type === "removeNode" || op.type === "replaceNode")
41
+ return [op.pos];
42
+ if (op.type === "mergeNode")
43
+ return mergeNodeSlots(op);
44
+ // moveNode.from/.to use the same "occupied slot" convention, but any
45
+ // moveNode paired with a mergeNode already shares a parent array (the
46
+ // merge's own path) and is caught unconditionally by the broader moveNode
47
+ // conflict rule above, before this function is reached - listing it here
48
+ // too would be redundant, not incorrect.
49
+ return null;
50
+ };
51
+ const operationTouchesSubtree = (operation, subtreeRoot) => {
52
+ if (operation.type === "insertNode" || operation.type === "removeNode" || operation.type === "replaceNode"
53
+ || operation.type === "setNodeAttributes" || operation.type === "setNodeType"
54
+ || operation.type === "splitNode" || operation.type === "mergeNode"
55
+ || operation.type === "insertText" || operation.type === "deleteText") {
56
+ return { exact: isExactlyAtSlot(operation.pos, subtreeRoot), within: isWithinSubtree(operation.pos, subtreeRoot) };
57
+ }
58
+ if (operation.type === "moveNode") {
59
+ return {
60
+ exact: isExactlyAtSlot(operation.from, subtreeRoot) || isExactlyAtSlot(operation.to, subtreeRoot),
61
+ within: isWithinSubtree(operation.from, subtreeRoot) || isWithinSubtree(operation.to, subtreeRoot),
62
+ };
63
+ }
64
+ // addMark / removeMark
65
+ return {
66
+ exact: false,
67
+ within: isWithinSubtree(operation.range.from, subtreeRoot) || isWithinSubtree(operation.range.to, subtreeRoot),
68
+ };
69
+ };
70
+ /**
71
+ * Transforms `operation` through a single concurrent `through` operation,
72
+ * classifying the small number of shapes this codebase's operation model
73
+ * cannot safely auto-resolve rather than silently deferring to
74
+ * `mapOperation`'s uniform `null` for all of them (see this module's
75
+ * `types.ts` doc comment on `TransformResult`).
76
+ *
77
+ * Two conflict shapes, both decided explicitly (2026-08-27, product
78
+ * decision) rather than attempted automatically:
79
+ *
80
+ * 1. **Concurrent moveNode vs. *any* structural operation touching the same
81
+ * parent array as its `from` or `to`.** `moveNode.to` is defined
82
+ * relative to "the array with this move's own source already removed"
83
+ * (operations.ts's applyToSession) - a convention with no well-defined
84
+ * composition once a concurrent structural change *also* touches that
85
+ * same array, even one that doesn't touch the moved node itself.
86
+ * Confirmed empirically via property tests, in increasing generality:
87
+ * two moves of *different* nodes to nearby destinations produced
88
+ * different final orders depending on transform direction; a move
89
+ * composed with a same-node remove crashed outright; and - the broadest
90
+ * case - a moveNode transformed through a plain removeNode of a
91
+ * *completely unrelated* node in the same array still diverged (moving
92
+ * "to offset 6" means different things depending on whether that offset
93
+ * is read before or after the unrelated removal is accounted for).
94
+ * Fixing this in general means redefining `to` for every existing
95
+ * moveNode caller (table row/column reorder, list item reorder) - out of
96
+ * scope for this phase. Flagged as a conflict for *any* concurrent
97
+ * structural operation (insertNode/removeNode/replaceNode/moveNode) on
98
+ * the same parent array, not narrowed to same-node cases only.
99
+ * 2. **Concurrent edit to content a `removeNode.mergedInto`/
100
+ * `replaceNode.retiredInto` operation absorbed.** `mapOperation` already
101
+ * ignores these fields entirely (by design - see the SmartOperation type
102
+ * comment; they were built in Phase 12a only for `rebaseAnnotationRange`,
103
+ * a different, ID-keyed mechanism). Auto-redirecting an *operation*
104
+ * (rather than an annotation range's endpoint) to the survivor has its
105
+ * own correctness risk even for whole-node-targeting ops - e.g. a
106
+ * concurrent `setNodeAttributes` redirected to the survivor would carry
107
+ * a `before` payload captured against the *retired* node's attrs, which
108
+ * `applyOperation`'s strict before-check would then reject against the
109
+ * survivor's actual attrs. Flagged as a conflict, consistent with
110
+ * decision 1, rather than attempting a redirect whose correctness isn't
111
+ * established.
112
+ * 3. **Concurrent mergeNode vs. anything occupying its left (survivor) or
113
+ * right (retired) slot** (2026-08-28, required follow-up to finding #1
114
+ * in mapoperation-position-arithmetic-gaps.md). `applyOperation`'s own
115
+ * mergeNode handling validates `splitOffset === left.children.length`
116
+ * and that left/right are structurally compatible - both checks depend
117
+ * on the actual identity of whatever occupies those two slots.
118
+ * `mapPosThroughOperation`'s mergeNode-as-through branch only tracks a
119
+ * single explicit position (`operation.pos`/`.from`/`.to`), so an
120
+ * ordinary sibling-index shift correctly keeps a *retiring* target
121
+ * reachable but has no way to notice that a node's *identity* changed
122
+ * underneath an *implicit* left-neighbor reference. Confirmed
123
+ * empirically: two mergeNode ops retiring adjacent siblings (A retires
124
+ * node N into its left neighbor, B retires N's *next* sibling into N)
125
+ * crashed in one transform direction and silently produced a different
126
+ * document in the other; a mergeNode composed with a concurrent
127
+ * `removeNode` of its own left/survivor node crashed both ways. A
128
+ * concurrent `splitNode` targeting the *exact* node a mergeNode retires
129
+ * was checked and found *not* to need this treatment - it already gets
130
+ * `mapPosThroughOperation`'s explicit "this position IS the retired
131
+ * node, redirect into the survivor" case, which correctly recomputes an
132
+ * in-bounds position rather than carrying a stale content-derived
133
+ * payload the way `mergeNode.splitOffset` does. Same reasoning as
134
+ * finding 1 above: not narrowed further, since fixing this generally
135
+ * would mean giving `mapOperation` document access it deliberately
136
+ * doesn't have.
137
+ */
138
+ export const transformOperation = (operation, through, options = {}) => {
139
+ const structuralParentPaths = (candidate) => {
140
+ if (candidate.type === "insertNode" || candidate.type === "removeNode" || candidate.type === "replaceNode")
141
+ return [candidate.pos.path];
142
+ if (candidate.type === "moveNode")
143
+ return [candidate.from.path, candidate.to.path];
144
+ return [];
145
+ };
146
+ if (operation.type === "moveNode" || through.type === "moveNode") {
147
+ const moveOperation = operation.type === "moveNode" ? operation : through.type === "moveNode" ? through : null;
148
+ const other = moveOperation === operation ? through : operation;
149
+ if (moveOperation) {
150
+ const moveParents = structuralParentPaths(moveOperation);
151
+ const otherParents = structuralParentPaths(other);
152
+ const sharesArray = otherParents.some((otherPath) => moveParents.some((movePath) => samePath(otherPath, movePath)));
153
+ if (sharesArray) {
154
+ return { kind: "conflict", reason: `Concurrent structural change shares an array with moveNode of "${moveOperation.nodeId}".` };
155
+ }
156
+ }
157
+ }
158
+ if (operation.type === "mergeNode" || through.type === "mergeNode") {
159
+ const mergeOperation = operation.type === "mergeNode" ? operation : through.type === "mergeNode" ? through : null;
160
+ const other = mergeOperation === operation ? through : operation;
161
+ if (mergeOperation) {
162
+ const mergeSlots = mergeNodeSlots(mergeOperation);
163
+ const otherSlots = occupiedSlotsOf(other);
164
+ const overlaps = otherSlots?.some((otherSlot) => mergeSlots.some((mergeSlot) => isExactlyAtSlot(otherSlot, mergeSlot)));
165
+ if (overlaps) {
166
+ return { kind: "conflict", reason: "Concurrent operation occupies the same slot a mergeNode's left or right side depends on." };
167
+ }
168
+ }
169
+ }
170
+ if (through.type === "removeNode" && through.mergedInto) {
171
+ const touch = operationTouchesSubtree(operation, through.pos);
172
+ if (touch.within) {
173
+ return { kind: "conflict", reason: `Concurrent operation targets content merged into "${through.mergedInto}".` };
174
+ }
175
+ }
176
+ if (through.type === "replaceNode" && through.retiredInto?.length) {
177
+ const touch = operationTouchesSubtree(operation, through.pos);
178
+ if (touch.within) {
179
+ return { kind: "conflict", reason: "Concurrent operation targets content retired by a merge." };
180
+ }
181
+ }
182
+ // Symmetric case: `operation` (not `through`) is the merge - e.g.
183
+ // rebasing a table-cell-merge transaction through a concurrent edit to
184
+ // the cell it's about to absorb, the reverse pairing of the two checks
185
+ // above. Naively letting this "succeed" would silently discard the
186
+ // concurrent edit the instant the merge's removeNode/replaceNode
187
+ // actually applies, with nothing in the result signaling that happened.
188
+ if (operation.type === "removeNode" && operation.mergedInto) {
189
+ const touch = operationTouchesSubtree(through, operation.pos);
190
+ if (touch.within) {
191
+ return { kind: "conflict", reason: `A concurrent operation targets content this merge would absorb into "${operation.mergedInto}".` };
192
+ }
193
+ }
194
+ if (operation.type === "replaceNode" && operation.retiredInto?.length) {
195
+ const touch = operationTouchesSubtree(through, operation.pos);
196
+ if (touch.within) {
197
+ return { kind: "conflict", reason: "A concurrent operation targets content this merge would retire." };
198
+ }
199
+ }
200
+ // A deleteText operation carries the exact text it expects to remove, as
201
+ // a self-check (operations.ts's applyToSession rejects it if the live
202
+ // document doesn't match). If a concurrent insertText lands strictly
203
+ // inside that span, the stored text is now stale relative to what's
204
+ // actually there - mapOperation only shifts positions, it never touches
205
+ // this payload (it has no document access to recompute it from), so
206
+ // naively shifting the position and reapplying the original text would
207
+ // either fail this self-check outright or, worse, delete the wrong
208
+ // characters if the lengths happened to still line up. Flagged as a
209
+ // conflict, consistent with the move/merge decisions above, rather than
210
+ // guessing whether the new content should be swept into the deletion or
211
+ // preserved. Deliberately excludes the boundary case (insert exactly at
212
+ // the deletion's start or end) - only a strictly-interior insert is
213
+ // ambiguous.
214
+ if (operation.type === "deleteText" && through.type === "insertText" && samePath(operation.pos.path, through.pos.path)) {
215
+ const deleteEnd = operation.pos.offset + operation.text.length;
216
+ if (through.pos.offset > operation.pos.offset && through.pos.offset < deleteEnd) {
217
+ return { kind: "conflict", reason: "Concurrent insertion lands inside a deleted range." };
218
+ }
219
+ }
220
+ if (operation.type === "insertText" && through.type === "deleteText" && samePath(operation.pos.path, through.pos.path)) {
221
+ const deleteEnd = through.pos.offset + through.text.length;
222
+ if (operation.pos.offset > through.pos.offset && operation.pos.offset < deleteEnd) {
223
+ return { kind: "conflict", reason: "Insertion lands inside a concurrently deleted range." };
224
+ }
225
+ }
226
+ // Two overlapping deleteText ranges - same staleness problem as above:
227
+ // whichever one is transformed second no longer has an accurate `.text`
228
+ // payload for however much of its range the other one already removed
229
+ // (found via a property test: two concurrent deletes of "45" and "4" at
230
+ // the same offset - applying one first makes the other's stored text
231
+ // immediately wrong). An *identical* delete (same offset, same text - two
232
+ // authors deleting the exact same span) has no real ambiguity - the
233
+ // second one's intent is already satisfied, so it's dropped as an
234
+ // ordinary no-op rather than surfaced as a conflict. mapOperation itself
235
+ // has no such exact-match handling for deleteText (confirmed via the same
236
+ // property test - it left the position untouched, causing the identical
237
+ // no-op case to crash too), so this is handled explicitly here, not
238
+ // deferred.
239
+ if (operation.type === "deleteText" && through.type === "deleteText" && samePath(operation.pos.path, through.pos.path)) {
240
+ const opEnd = operation.pos.offset + operation.text.length;
241
+ const throughEnd = through.pos.offset + through.text.length;
242
+ const overlaps = operation.pos.offset < throughEnd && through.pos.offset < opEnd;
243
+ const identical = operation.pos.offset === through.pos.offset && operation.text === through.text;
244
+ if (identical)
245
+ return { kind: "dropped" };
246
+ if (overlaps)
247
+ return { kind: "conflict", reason: "Concurrent deletions overlap." };
248
+ }
249
+ const mapped = mapOperation(operation, through, options);
250
+ return mapped ? { kind: "ok", operation: mapped } : { kind: "dropped" };
251
+ };
@@ -0,0 +1,52 @@
1
+ import type { SmartOperation, SmartTransaction } from "../types.js";
2
+ export type MoveNodeOperation = Extract<SmartOperation, {
3
+ type: "moveNode";
4
+ }>;
5
+ export type RemoveNodeOperation = Extract<SmartOperation, {
6
+ type: "removeNode";
7
+ }>;
8
+ export type ReplaceNodeOperation = Extract<SmartOperation, {
9
+ type: "replaceNode";
10
+ }>;
11
+ /**
12
+ * Result of transforming one operation through a single concurrent
13
+ * "through" operation - `mapOperation` (operations.ts) only ever returns
14
+ * `SmartOperation | null`, collapsing "target no longer exists, this
15
+ * operation is now a harmless no-op" and "target was folded into something
16
+ * else and this operation cannot be safely, precisely redirected" into the
17
+ * same `null`. Concurrent rebase needs to tell those apart: the first is
18
+ * routine and silent, the second must be surfaced (per this phase's stop
19
+ * condition 4 - a rebase must never silently corrupt state or silently
20
+ * drop a conflicting edit without saying so).
21
+ */
22
+ export type TransformResult = {
23
+ kind: "ok";
24
+ operation: SmartOperation;
25
+ } | {
26
+ kind: "dropped";
27
+ } | {
28
+ kind: "conflict";
29
+ reason: string;
30
+ };
31
+ /** Rebasing a whole transaction through everything committed since its baseRevision. */
32
+ export type RebaseResult = {
33
+ kind: "ok";
34
+ transaction: SmartTransaction;
35
+ }
36
+ /** Every operation in the transaction turned out to be a no-op against the current state (e.g. it only edited content someone else already deleted outright). Nothing to apply; the transaction is simply dropped. */
37
+ | {
38
+ kind: "dropped";
39
+ }
40
+ /** At least one operation could not be safely transformed - see `reasons` for which operation (by index) and why. The transaction as a whole is rejected; the caller decides what happens to the user's edit (see rebaseTransaction's own doc comment). */
41
+ | {
42
+ kind: "conflict";
43
+ reasons: readonly {
44
+ operationIndex: number;
45
+ reason: string;
46
+ }[];
47
+ }
48
+ /** `sinceRevision` predates what this client has retained - a transform can't be attempted at all without the missing history. The caller must resync (reload the full document at the current revision) rather than attempt a partial rebase. */
49
+ | {
50
+ kind: "resync-required";
51
+ atRevision: number;
52
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ import type { SmartDocument, SmartSelection } from "../types.js";
2
+ import type { AnnotationRange } from "../annotations/types.js";
3
+ /**
4
+ * Builds the AnnotationRange a new comment thread should anchor to from the
5
+ * editor's current selection - the reverse direction of
6
+ * `resolveAnnotationRange` (AnnotationRange -> SmartRange). `resolvePos`'s
7
+ * `nodeId` is exactly the id `resolveAnnotationRange`'s `resolveEndpoint`
8
+ * expects for an offset-bearing endpoint (the owning node, not a text
9
+ * node), so no separate id-lookup mechanism is needed here.
10
+ *
11
+ * Returns null for a collapsed selection (nothing to attach a comment to)
12
+ * or a "none" selection (nothing selected at all). Every other selection
13
+ * type - text, node, cell - resolves the same way, since `SmartPos`/
14
+ * `resolvePos` already treat structural and inline owners uniformly.
15
+ */
16
+ export declare const commentRangeFromSelection: (document: SmartDocument, selection: SmartSelection) => AnnotationRange | null;
@@ -0,0 +1,24 @@
1
+ import { comparePos, resolvePos, selectionRange } from "../positions.js";
2
+ /**
3
+ * Builds the AnnotationRange a new comment thread should anchor to from the
4
+ * editor's current selection - the reverse direction of
5
+ * `resolveAnnotationRange` (AnnotationRange -> SmartRange). `resolvePos`'s
6
+ * `nodeId` is exactly the id `resolveAnnotationRange`'s `resolveEndpoint`
7
+ * expects for an offset-bearing endpoint (the owning node, not a text
8
+ * node), so no separate id-lookup mechanism is needed here.
9
+ *
10
+ * Returns null for a collapsed selection (nothing to attach a comment to)
11
+ * or a "none" selection (nothing selected at all). Every other selection
12
+ * type - text, node, cell - resolves the same way, since `SmartPos`/
13
+ * `resolvePos` already treat structural and inline owners uniformly.
14
+ */
15
+ export const commentRangeFromSelection = (document, selection) => {
16
+ if (selection.type === "none")
17
+ return null;
18
+ const { from, to } = selectionRange(selection);
19
+ if (comparePos(from, to) === 0)
20
+ return null;
21
+ const start = resolvePos(document, from);
22
+ const end = resolvePos(document, to);
23
+ return { startId: start.nodeId, startOffset: from.offset, endId: end.nodeId, endOffset: to.offset };
24
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./types.js";
2
+ export * from "./rebase.js";
3
+ export * from "./fromSelection.js";
@@ -0,0 +1,3 @@
1
+ export * from "./types.js";
2
+ export * from "./rebase.js";
3
+ export * from "./fromSelection.js";
@@ -0,0 +1,31 @@
1
+ import type { SmartDocument, SmartSchema, SmartTransaction } from "../types.js";
2
+ import type { MergeOrphanPolicy } from "../annotations/types.js";
3
+ import type { CommentThread } from "./types.js";
4
+ /**
5
+ * Comments must never silently disappear from a merge the way a caller
6
+ * might reasonably choose for some other annotation consumer - a lost
7
+ * comment is lost user-authored text, not just a lost position. Tries the
8
+ * standard snap-to-survivor policy first; if there's genuinely no
9
+ * preceding sibling to snap to (a real edge case `defaultMergeOrphanPolicy`
10
+ * itself can return null for), keeps the range's ids exactly as they were
11
+ * rather than dropping it. That range will simply fail to resolve once
12
+ * the retired id is gone from the document - the same "orphaned, not
13
+ * lost" state a `removeNode` with no `mergedInto` already produces
14
+ * naturally - so the thread's data survives and the UI can detect and
15
+ * surface "comment on deleted content" by the standard resolve-and-check
16
+ * pattern, not a special stored flag.
17
+ */
18
+ export declare const neverOrphanCommentPolicy: (document: SmartDocument) => MergeOrphanPolicy;
19
+ /**
20
+ * Rebases every thread's range through one committed transaction, in
21
+ * operation order. `rebaseAnnotationRange`/`rebaseAnnotationRangeThroughOperations`
22
+ * need a fresh `PositionLookup` resolved against the document as it
23
+ * existed immediately before each individual operation (not the whole
24
+ * transaction) - since a caller subscribing to a runtime's transaction
25
+ * stream only receives the transaction after it has fully committed, this
26
+ * reconstructs those intermediate states itself by replaying operations
27
+ * one at a time against a running copy, starting from `beforeDocument`
28
+ * (whatever the document was immediately before this transaction, i.e.
29
+ * exactly what the caller already had from the previous change event).
30
+ */
31
+ export declare const rebaseCommentThreadsThroughTransaction: (threads: readonly CommentThread[], beforeDocument: SmartDocument, transaction: SmartTransaction, schema: SmartSchema) => readonly CommentThread[];
@@ -0,0 +1,43 @@
1
+ import { applyOperation } from "../operations.js";
2
+ import { createScopeIndex } from "../scope/resolveScope.js";
3
+ import { defaultMergeOrphanPolicy, rebaseAnnotationRange } from "../annotations/range.js";
4
+ /**
5
+ * Comments must never silently disappear from a merge the way a caller
6
+ * might reasonably choose for some other annotation consumer - a lost
7
+ * comment is lost user-authored text, not just a lost position. Tries the
8
+ * standard snap-to-survivor policy first; if there's genuinely no
9
+ * preceding sibling to snap to (a real edge case `defaultMergeOrphanPolicy`
10
+ * itself can return null for), keeps the range's ids exactly as they were
11
+ * rather than dropping it. That range will simply fail to resolve once
12
+ * the retired id is gone from the document - the same "orphaned, not
13
+ * lost" state a `removeNode` with no `mergedInto` already produces
14
+ * naturally - so the thread's data survives and the UI can detect and
15
+ * surface "comment on deleted content" by the standard resolve-and-check
16
+ * pattern, not a special stored flag.
17
+ */
18
+ export const neverOrphanCommentPolicy = (document) => (range, operation, positions) => defaultMergeOrphanPolicy(document)(range, operation, positions) ?? range;
19
+ /**
20
+ * Rebases every thread's range through one committed transaction, in
21
+ * operation order. `rebaseAnnotationRange`/`rebaseAnnotationRangeThroughOperations`
22
+ * need a fresh `PositionLookup` resolved against the document as it
23
+ * existed immediately before each individual operation (not the whole
24
+ * transaction) - since a caller subscribing to a runtime's transaction
25
+ * stream only receives the transaction after it has fully committed, this
26
+ * reconstructs those intermediate states itself by replaying operations
27
+ * one at a time against a running copy, starting from `beforeDocument`
28
+ * (whatever the document was immediately before this transaction, i.e.
29
+ * exactly what the caller already had from the previous change event).
30
+ */
31
+ export const rebaseCommentThreadsThroughTransaction = (threads, beforeDocument, transaction, schema) => {
32
+ if (!threads.length || !transaction.operations.length)
33
+ return threads;
34
+ let runningDocument = beforeDocument;
35
+ let ranges = threads.map((thread) => thread.range);
36
+ for (const operation of transaction.operations) {
37
+ const positions = createScopeIndex().positions(runningDocument, schema);
38
+ const onOrphan = neverOrphanCommentPolicy(runningDocument);
39
+ ranges = ranges.map((range) => rebaseAnnotationRange(range, operation, positions, onOrphan) ?? range);
40
+ runningDocument = applyOperation(runningDocument, operation);
41
+ }
42
+ return threads.map((thread, index) => ranges[index] === thread.range ? thread : { ...thread, range: ranges[index] });
43
+ };
@@ -0,0 +1,37 @@
1
+ import type { AnnotationRange } from "../annotations/types.js";
2
+ /**
3
+ * One message in a comment thread. The first reply (index 0) is the
4
+ * thread's opening comment, not a distinct "comment" type layered on top -
5
+ * a thread with exactly one reply is what most people mean by "a comment,"
6
+ * and every subsequent reply is a normal continuation of the same
7
+ * conversation. Modeling it as one list, not "comment + replies[]", means
8
+ * there is exactly one code path for "add a message to a thread" instead
9
+ * of two slightly-different ones.
10
+ */
11
+ export interface CommentReply {
12
+ readonly id: string;
13
+ readonly authorId: string;
14
+ readonly text: string;
15
+ readonly createdAt: number;
16
+ }
17
+ /**
18
+ * A comment thread anchored to a Phase 8c `AnnotationRange`. Threaded
19
+ * replies (single flat list, see `CommentReply`'s own doc comment);
20
+ * resolution is thread-level state (`resolved: true`), not deletion - a
21
+ * resolved thread's replies remain in `list()` for a host that wants to
22
+ * show a resolved-comments history, and only an explicit `remove()` call
23
+ * (a distinct, more destructive action) actually discards a thread.
24
+ *
25
+ * `range` is always a well-formed `AnnotationRange` - never null, even
26
+ * once the ids it names no longer exist in the document (see
27
+ * `rebase.ts`'s orphan handling). Whether a thread is currently anchored
28
+ * to real content is derived on demand via `resolveAnnotationRange`,
29
+ * exactly like every other `AnnotationRange` consumer - not stored as a
30
+ * separate flag that could drift out of sync with the document.
31
+ */
32
+ export interface CommentThread {
33
+ readonly id: string;
34
+ readonly range: AnnotationRange;
35
+ readonly resolved: boolean;
36
+ readonly replies: readonly CommentReply[];
37
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import type { SmartDocument, SmartSchema } from "../types.js";
2
+ import type { DocumentDiff } from "./types.js";
3
+ /**
4
+ * Every `SmartElementNode` keeps a stable id across split/merge/move/type-
5
+ * change/undo (Phase 8c collab-readiness gate, assertion 1). That turns the
6
+ * structural half of a document diff into a hash-map join - id present in
7
+ * both/only-before/only-after - instead of a generic tree-alignment problem
8
+ * a Myers/patience-style algorithm would otherwise be needed to solve.
9
+ * `SmartTextNode` has no id, so text content is the one place a real
10
+ * (word-level, see textDiff.ts) diff algorithm is still needed.
11
+ */
12
+ export declare const diffDocuments: (before: SmartDocument, after: SmartDocument, schema: SmartSchema) => DocumentDiff;