smartrte-core 0.2.1 → 1.0.0-beta.2

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 +24 -0
  19. package/dist/foundation/atom/formats.js +105 -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 +21 -0
  127. package/dist/foundation/list/formats.js +813 -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 +27 -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 +28 -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,568 @@
1
+ import { assertUniqueNodeIds, cloneNode, isTextNode } from "./identity.js";
2
+ import { nodeAtPath, samePath } from "./positions.js";
3
+ import { canonicalMarkOrder } from "./marks/canonical.js";
4
+ const sameValue = (left, right) => JSON.stringify(left) === JSON.stringify(right);
5
+ const parentAndIndex = (document, pos) => {
6
+ const parent = nodeAtPath(document, pos.path);
7
+ if (!parent || isTextNode(parent) || !parent.children)
8
+ throw new Error("Operation position does not resolve to a container.");
9
+ if (!Number.isInteger(pos.offset) || pos.offset < 0 || pos.offset > parent.children.length)
10
+ throw new Error("Operation child offset is out of bounds.");
11
+ return { parent, index: pos.offset };
12
+ };
13
+ /**
14
+ * Transaction-local copy-on-write tree. A node is copied at most once in one
15
+ * operation batch; unchanged siblings retain reference identity. This identity
16
+ * guarantee is the memoization contract for scope indexing and DOM rendering.
17
+ */
18
+ class PathCopySession {
19
+ constructor(input) {
20
+ this.input = input;
21
+ this.copies = new WeakMap();
22
+ this.mutable = new WeakSet();
23
+ this.root = input;
24
+ }
25
+ document() { return this.root; }
26
+ node(path) { return nodeAtPath(this.root, path); }
27
+ copy(node) {
28
+ if (this.mutable.has(node))
29
+ return node;
30
+ const existing = this.copies.get(node);
31
+ if (existing)
32
+ return existing;
33
+ const draft = { ...node, ...(node.children ? { children: [...node.children] } : {}) };
34
+ this.copies.set(node, draft);
35
+ this.mutable.add(draft);
36
+ return draft;
37
+ }
38
+ container(path) {
39
+ let current = this.copy(this.root);
40
+ this.root = current;
41
+ for (const index of path) {
42
+ const children = current.children;
43
+ const child = children?.[index];
44
+ if (!child || isTextNode(child))
45
+ throw new Error("Container path does not resolve to an element.");
46
+ const next = this.copy(child);
47
+ children[index] = next;
48
+ current = next;
49
+ }
50
+ return current;
51
+ }
52
+ replace(path, replacement) {
53
+ if (!path.length) {
54
+ if (isTextNode(replacement) || replacement.type !== "doc")
55
+ throw new Error("Document root replacement must be a doc node.");
56
+ this.root = replacement;
57
+ return;
58
+ }
59
+ const parent = this.container(path.slice(0, -1));
60
+ const index = path[path.length - 1];
61
+ if (!parent.children || index < 0 || index >= parent.children.length)
62
+ throw new Error("Replacement index is out of bounds.");
63
+ parent.children[index] = replacement;
64
+ }
65
+ }
66
+ const marksKey = (marks) => JSON.stringify(canonicalMarkOrder(marks));
67
+ const normalizeInline = (children) => {
68
+ const result = [];
69
+ children.forEach((node) => {
70
+ if (isTextNode(node) && !node.text)
71
+ return;
72
+ if (isTextNode(node) && node.marks?.length)
73
+ node = { ...node, marks: canonicalMarkOrder(node.marks) };
74
+ const previous = result[result.length - 1];
75
+ if (isTextNode(node) && previous && isTextNode(previous) && marksKey(previous.marks) === marksKey(node.marks)) {
76
+ result[result.length - 1] = { ...previous, text: previous.text + node.text };
77
+ }
78
+ else
79
+ result.push(node);
80
+ });
81
+ return result;
82
+ };
83
+ const splitInlineAt = (children, offset) => {
84
+ if (!Number.isInteger(offset) || offset < 0)
85
+ throw new Error("Inline offset is invalid.");
86
+ const before = [];
87
+ const after = [];
88
+ let consumed = 0;
89
+ let split = false;
90
+ children.forEach((node) => {
91
+ if (split)
92
+ return after.push(node);
93
+ const size = isTextNode(node) ? node.text.length : 1;
94
+ if (offset > consumed + size) {
95
+ before.push(node);
96
+ consumed += size;
97
+ return;
98
+ }
99
+ const local = offset - consumed;
100
+ if (isTextNode(node)) {
101
+ if (local > 0)
102
+ before.push({ ...node, text: node.text.slice(0, local) });
103
+ if (local < size)
104
+ after.push({ ...node, text: node.text.slice(local) });
105
+ }
106
+ else {
107
+ if (local === 0)
108
+ after.push(node);
109
+ else if (local === 1)
110
+ before.push(node);
111
+ else
112
+ throw new Error("Atomic inline offset is invalid.");
113
+ }
114
+ split = true;
115
+ consumed += size;
116
+ });
117
+ if (!split && offset !== consumed)
118
+ throw new Error("Inline offset is out of bounds.");
119
+ return [before, after];
120
+ };
121
+ const inlineContainer = (document, pos) => {
122
+ const node = nodeAtPath(document, pos.path);
123
+ if (!node || isTextNode(node) || !node.children) {
124
+ throw new Error("Text operation must target an inline container.");
125
+ }
126
+ return node;
127
+ };
128
+ const insertText = (session, pos, text, marks) => {
129
+ const container = inlineContainer(session.document(), pos);
130
+ const [before, after] = splitInlineAt(container.children || [], pos.offset);
131
+ const inserted = text ? [{ type: "text", text, ...(marks?.length ? { marks: structuredClone(marks) } : {}) }] : [];
132
+ const draft = session.container(pos.path);
133
+ draft.children = normalizeInline([...before, ...inserted, ...after]);
134
+ };
135
+ const deleteText = (session, pos, text) => {
136
+ const container = inlineContainer(session.document(), pos);
137
+ const [before, tail] = splitInlineAt(container.children || [], pos.offset);
138
+ const [removed, after] = splitInlineAt(tail, text.length);
139
+ const actual = removed.map((node) => isTextNode(node) ? node.text : "\uFFFC").join("");
140
+ if (actual !== text)
141
+ throw new Error(`deleteText payload does not match document content.`);
142
+ const draft = session.container(pos.path);
143
+ draft.children = normalizeInline([...before, ...after]);
144
+ };
145
+ const applyMark = (session, range, mark, add) => {
146
+ const comparePath = (left, right) => {
147
+ const length = Math.min(left.length, right.length);
148
+ for (let index = 0; index < length; index += 1)
149
+ if (left[index] !== right[index])
150
+ return left[index] - right[index];
151
+ return left.length - right.length;
152
+ };
153
+ if (comparePath(range.from.path, range.to.path) > 0 || (samePath(range.from.path, range.to.path) && range.from.offset > range.to.offset)) {
154
+ throw new Error("Mark operation range must be normalized from/to.");
155
+ }
156
+ const paths = [];
157
+ const visit = (node, path) => {
158
+ if (isTextNode(node))
159
+ return;
160
+ if (samePath(path, range.from.path) && samePath(path, range.to.path))
161
+ paths.push(path);
162
+ else if ((node.type === "paragraph" || node.type === "heading") &&
163
+ comparePath(path, range.from.path) >= 0 && comparePath(path, range.to.path) <= 0)
164
+ paths.push(path);
165
+ node.children?.forEach((child, index) => visit(child, [...path, index]));
166
+ };
167
+ visit(session.document(), []);
168
+ paths.forEach((path) => {
169
+ const container = inlineContainer(session.document(), { path, offset: 0 });
170
+ const size = (container.children || []).reduce((total, child) => total + (isTextNode(child) ? child.text.length : 1), 0);
171
+ const start = samePath(path, range.from.path) ? range.from.offset : 0;
172
+ const end = samePath(path, range.to.path) ? range.to.offset : size;
173
+ const [before, tail] = splitInlineAt(container.children || [], start);
174
+ const [middle, after] = splitInlineAt(tail, end - start);
175
+ const changed = middle.map((node) => {
176
+ if (!isTextNode(node))
177
+ return node;
178
+ const marks = node.marks || [];
179
+ if (add) {
180
+ if (marks.some((candidate) => sameValue(candidate, mark)))
181
+ return node;
182
+ return { ...node, marks: canonicalMarkOrder([...marks, structuredClone(mark)]) };
183
+ }
184
+ const next = marks.filter((candidate) => !sameValue(candidate, mark));
185
+ return { ...node, ...(next.length ? { marks: next } : { marks: undefined }) };
186
+ });
187
+ const draft = session.container(path);
188
+ draft.children = normalizeInline([...before, ...changed, ...after]);
189
+ });
190
+ };
191
+ const applyToSession = (session, operation) => {
192
+ const document = session.document();
193
+ if (operation.type === "insertNode") {
194
+ const { parent, index } = parentAndIndex(document, operation.pos);
195
+ const draft = session.container(operation.pos.path);
196
+ draft.children = [...(parent.children || []).slice(0, index), cloneNode(operation.node), ...(parent.children || []).slice(index)];
197
+ }
198
+ else if (operation.type === "removeNode") {
199
+ const { parent, index } = parentAndIndex(document, operation.pos);
200
+ const existing = parent.children?.[index];
201
+ if (!existing || !sameValue(existing, operation.node))
202
+ throw new Error("removeNode payload does not match document node.");
203
+ const draft = session.container(operation.pos.path);
204
+ draft.children = [...(parent.children || []).slice(0, index), ...(parent.children || []).slice(index + 1)];
205
+ }
206
+ else if (operation.type === "replaceNode") {
207
+ const { parent, index } = parentAndIndex(document, operation.pos);
208
+ if (!sameValue(parent.children?.[index], operation.before))
209
+ throw new Error("replaceNode before payload does not match document node.");
210
+ const draft = session.container(operation.pos.path);
211
+ draft.children = (parent.children || []).map((node, nodeIndex) => nodeIndex === index ? cloneNode(operation.after) : node);
212
+ }
213
+ else if (operation.type === "moveNode") {
214
+ const source = parentAndIndex(document, operation.from);
215
+ const moving = source.parent.children?.[source.index];
216
+ if (!moving || isTextNode(moving) || moving.id !== operation.nodeId)
217
+ throw new Error("moveNode source does not match nodeId.");
218
+ const sourceDraft = session.container(operation.from.path);
219
+ sourceDraft.children = [...(source.parent.children || []).slice(0, source.index), ...(source.parent.children || []).slice(source.index + 1)];
220
+ const destination = parentAndIndex(session.document(), operation.to);
221
+ const destinationDraft = session.container(operation.to.path);
222
+ destinationDraft.children = [...(destination.parent.children || []).slice(0, destination.index), moving, ...(destination.parent.children || []).slice(destination.index)];
223
+ }
224
+ else if (operation.type === "splitNode") {
225
+ const targetPath = operation.pos.path.slice(0, operation.pos.path.length - operation.depth);
226
+ const target = session.node(targetPath);
227
+ if (!target || isTextNode(target) || !target.children)
228
+ throw new Error("splitNode target must be a container.");
229
+ if (operation.pos.offset < 0 || operation.pos.offset > target.children.length)
230
+ throw new Error("splitNode offset is out of bounds.");
231
+ const left = { ...target, children: target.children.slice(0, operation.pos.offset) };
232
+ const right = { ...target, id: operation.newId, children: target.children.slice(operation.pos.offset) };
233
+ const parentPath = targetPath.slice(0, -1);
234
+ const index = targetPath[targetPath.length - 1];
235
+ const parent = session.node(parentPath);
236
+ if (!parent || isTextNode(parent) || !parent.children)
237
+ throw new Error("splitNode parent is invalid.");
238
+ const draft = session.container(parentPath);
239
+ draft.children = [...parent.children.slice(0, index), left, right, ...parent.children.slice(index + 1)];
240
+ }
241
+ else if (operation.type === "mergeNode") {
242
+ const parent = session.node(operation.pos.path);
243
+ if (!parent || isTextNode(parent) || !parent.children)
244
+ throw new Error("mergeNode parent is invalid.");
245
+ const rightIndex = operation.pos.offset;
246
+ const left = parent.children[rightIndex - 1];
247
+ const right = parent.children[rightIndex];
248
+ if (!left || !right || isTextNode(left) || isTextNode(right) || right.id !== operation.retiredId)
249
+ throw new Error("mergeNode nodes are invalid.");
250
+ const comparable = (node) => {
251
+ const { id: _id, children: _children, ...rest } = node;
252
+ return rest;
253
+ };
254
+ if (!sameValue(comparable(left), comparable(right)) || operation.splitOffset !== (left.children || []).length) {
255
+ throw new Error("mergeNode requires structurally compatible siblings and the exact split offset.");
256
+ }
257
+ const merged = { ...left, children: [...(left.children || []), ...(right.children || [])] };
258
+ const draft = session.container(operation.pos.path);
259
+ draft.children = [...parent.children.slice(0, rightIndex - 1), merged, ...parent.children.slice(rightIndex + 1)];
260
+ }
261
+ else if (operation.type === "setNodeAttributes") {
262
+ const target = session.node(operation.pos.path);
263
+ if (!target || isTextNode(target) || !sameValue(target.attrs || {}, operation.before))
264
+ throw new Error("setNodeAttributes before payload does not match.");
265
+ // Reassign in place rather than destructure-then-respread: the latter
266
+ // always re-adds `attrs` as the object's last key, silently reordering
267
+ // it even when only the value changed. sameValue elsewhere compares
268
+ // nodes via JSON.stringify, which is key-order sensitive - a later
269
+ // replaceNode/removeNode whose `before` payload was captured pre-reorder
270
+ // would then fail to match an otherwise-identical node. See
271
+ // docs/bugs/setnodeattributes-reorders-keys-breaking-exact-match.md.
272
+ const next = { ...target };
273
+ if (Object.keys(operation.after).length)
274
+ next.attrs = structuredClone(operation.after);
275
+ else
276
+ delete next.attrs;
277
+ session.replace(operation.pos.path, next);
278
+ }
279
+ else if (operation.type === "setNodeType") {
280
+ const target = session.node(operation.pos.path);
281
+ if (!target || isTextNode(target) || target.type !== operation.before || !sameValue(target.attrs || {}, operation.beforeAttrs)) {
282
+ throw new Error("setNodeType before payload does not match.");
283
+ }
284
+ session.replace(operation.pos.path, {
285
+ type: operation.after,
286
+ id: target.id,
287
+ ...(Object.keys(operation.afterAttrs).length ? { attrs: structuredClone(operation.afterAttrs) } : {}),
288
+ ...(target.children ? { children: target.children } : {}),
289
+ });
290
+ }
291
+ else if (operation.type === "insertText")
292
+ insertText(session, operation.pos, operation.text, operation.marks);
293
+ else if (operation.type === "deleteText")
294
+ deleteText(session, operation.pos, operation.text);
295
+ else if (operation.type === "addMark")
296
+ applyMark(session, operation.range, operation.mark, true);
297
+ else
298
+ applyMark(session, operation.range, operation.mark, false);
299
+ return operation.type === "insertNode" || operation.type === "replaceNode" || operation.type === "splitNode";
300
+ };
301
+ export const applyOperation = (input, operation) => {
302
+ const session = new PathCopySession(input);
303
+ const identityChanged = applyToSession(session, operation);
304
+ const document = session.document();
305
+ if (identityChanged)
306
+ assertUniqueNodeIds(document);
307
+ return document;
308
+ };
309
+ export const invertOperation = (operation) => {
310
+ if (operation.type === "insertNode")
311
+ return { type: "removeNode", pos: operation.pos, node: cloneNode(operation.node) };
312
+ if (operation.type === "removeNode")
313
+ return { type: "insertNode", pos: operation.pos, node: cloneNode(operation.node) };
314
+ if (operation.type === "replaceNode")
315
+ return { ...operation, before: cloneNode(operation.after), after: cloneNode(operation.before) };
316
+ if (operation.type === "moveNode")
317
+ return { ...operation, from: operation.to, to: operation.from };
318
+ if (operation.type === "splitNode") {
319
+ const targetPath = operation.pos.path.slice(0, operation.pos.path.length - operation.depth);
320
+ return { type: "mergeNode", pos: { path: targetPath.slice(0, -1), offset: targetPath[targetPath.length - 1] + 1 }, depth: operation.depth, retiredId: operation.newId, splitOffset: operation.pos.offset };
321
+ }
322
+ if (operation.type === "mergeNode") {
323
+ const leftPath = [...operation.pos.path, operation.pos.offset - 1];
324
+ return { type: "splitNode", pos: { path: leftPath, offset: operation.splitOffset }, depth: operation.depth, newId: operation.retiredId };
325
+ }
326
+ if (operation.type === "setNodeAttributes")
327
+ return { ...operation, before: structuredClone(operation.after), after: structuredClone(operation.before) };
328
+ if (operation.type === "setNodeType")
329
+ return {
330
+ ...operation,
331
+ before: operation.after,
332
+ after: operation.before,
333
+ beforeAttrs: structuredClone(operation.afterAttrs),
334
+ afterAttrs: structuredClone(operation.beforeAttrs),
335
+ };
336
+ if (operation.type === "insertText")
337
+ return { type: "deleteText", pos: operation.pos, text: operation.text, ...(operation.marks ? { marks: structuredClone(operation.marks) } : {}) };
338
+ if (operation.type === "deleteText")
339
+ return { type: "insertText", pos: operation.pos, text: operation.text, ...(operation.marks ? { marks: structuredClone(operation.marks) } : {}) };
340
+ if (operation.type === "addMark")
341
+ return { type: "removeMark", range: operation.range, mark: structuredClone(operation.mark) };
342
+ return { type: "addMark", range: operation.range, mark: structuredClone(operation.mark) };
343
+ };
344
+ export const applyOperations = (document, operations) => {
345
+ const session = new PathCopySession(document);
346
+ let identityChanged = false;
347
+ operations.forEach((operation) => { identityChanged = applyToSession(session, operation) || identityChanged; });
348
+ const result = session.document();
349
+ if (identityChanged)
350
+ assertUniqueNodeIds(result);
351
+ return result;
352
+ };
353
+ export const invertOperations = (operations) => [...operations].reverse().map(invertOperation);
354
+ const shiftPath = (pos, parent, delta) => {
355
+ if (!samePath(pos.path.slice(0, parent.path.length), parent.path) || pos.path.length <= parent.path.length)
356
+ return pos;
357
+ const path = [...pos.path];
358
+ const index = path[parent.path.length];
359
+ if (index >= parent.offset)
360
+ path[parent.path.length] += delta;
361
+ return { ...pos, path };
362
+ };
363
+ export const mapPosThroughOperation = (pos, operation, bias = 1) => {
364
+ if (operation.type === "insertNode") {
365
+ if (samePath(pos.path, operation.pos.path) && pos.offset === operation.pos.offset)
366
+ return { pos: { ...pos, offset: pos.offset + (bias > 0 ? 1 : 0) }, deleted: false };
367
+ // A second position/operation at the *same* parent, past the insertion
368
+ // point, needs its own offset bumped directly - shiftPath only handles a
369
+ // position strictly *deeper* than the insertion point (pos.path.length >
370
+ // parent.path.length), so a same-depth sibling reference (e.g. another
371
+ // insertNode's own .pos.offset into the identical array) fell through
372
+ // unshifted. removeNode's branch below already has this same-path check;
373
+ // insertNode's was missing it - see the TP1 property test that caught
374
+ // this (two sibling insertNode ops at offsets 2 and 5 diverged depending
375
+ // on transform order).
376
+ if (samePath(pos.path, operation.pos.path) && pos.offset > operation.pos.offset)
377
+ return { pos: { ...pos, offset: pos.offset + 1 }, deleted: false };
378
+ return { pos: shiftPath(pos, operation.pos, 1), deleted: false };
379
+ }
380
+ if (operation.type === "removeNode") {
381
+ const removedPath = [...operation.pos.path, operation.pos.offset];
382
+ if (samePath(pos.path.slice(0, removedPath.length), removedPath))
383
+ return { pos: { path: [...operation.pos.path], offset: operation.pos.offset }, deleted: true };
384
+ if (samePath(pos.path, operation.pos.path) && pos.offset > operation.pos.offset)
385
+ return { pos: { ...pos, offset: pos.offset - 1 }, deleted: false };
386
+ return { pos: shiftPath(pos, operation.pos, -1), deleted: false };
387
+ }
388
+ if (operation.type === "replaceNode") {
389
+ const replaced = [...operation.pos.path, operation.pos.offset];
390
+ if (samePath(pos.path.slice(0, replaced.length), replaced)
391
+ && !isTextNode(operation.before) && !isTextNode(operation.after)
392
+ && operation.before.id === operation.after.id) {
393
+ const relative = pos.path.slice(replaced.length);
394
+ let owner = operation.before;
395
+ for (const index of relative)
396
+ owner = !owner || isTextNode(owner) ? undefined : owner.children?.[index];
397
+ if (owner && !isTextNode(owner)) {
398
+ const find = (node, id, path = []) => {
399
+ if (!isTextNode(node) && node.id === id)
400
+ return path;
401
+ if (isTextNode(node))
402
+ return null;
403
+ for (let index = 0; index < (node.children?.length || 0); index += 1) {
404
+ const found = find(node.children[index], id, [...path, index]);
405
+ if (found)
406
+ return found;
407
+ }
408
+ return null;
409
+ };
410
+ const nextRelative = find(operation.after, owner.id);
411
+ if (nextRelative)
412
+ return { pos: { path: [...replaced, ...nextRelative], offset: pos.offset }, deleted: false };
413
+ }
414
+ }
415
+ return samePath(pos.path.slice(0, replaced.length), replaced)
416
+ ? { pos: { path: [...operation.pos.path], offset: operation.pos.offset + (bias > 0 ? 1 : 0) }, deleted: true }
417
+ : { pos, deleted: false };
418
+ }
419
+ if (operation.type === "moveNode") {
420
+ const sourcePath = [...operation.from.path, operation.from.offset];
421
+ if (samePath(pos.path.slice(0, sourcePath.length), sourcePath)) {
422
+ return { pos: { ...pos, path: [...operation.to.path, operation.to.offset, ...pos.path.slice(sourcePath.length)] }, deleted: false };
423
+ }
424
+ const removed = mapPosThroughOperation(pos, { type: "removeNode", pos: operation.from, node: { type: "unknown", id: operation.nodeId } }, bias);
425
+ return mapPosThroughOperation(removed.pos, { type: "insertNode", pos: operation.to, node: { type: "unknown", id: operation.nodeId } }, bias);
426
+ }
427
+ if (operation.type === "splitNode") {
428
+ const targetPath = operation.pos.path.slice(0, operation.pos.path.length - operation.depth);
429
+ const parentPath = targetPath.slice(0, -1);
430
+ const targetIndex = targetPath[targetPath.length - 1];
431
+ // Exact-tie case (bias>0 mirrors insertNode's/insertText's own tie
432
+ // handling above): two concurrent splitNode ops at the identical
433
+ // boundary had no resolution at all before this - pos.offset ===
434
+ // operation.pos.offset satisfied neither this branch (originally
435
+ // `>` only) nor any other, silently falling through to "unchanged" at
436
+ // the bottom of this function, which produced a swapped-identity
437
+ // divergence (found via a TP1 property test: whichever split ends up
438
+ // empty differs depending on transform order). Merging the tie into
439
+ // this existing shift condition, rather than adding a separate branch,
440
+ // keeps the same formula for both the tie and the already-correct
441
+ // greater-than case.
442
+ if (samePath(pos.path, targetPath) && (pos.offset > operation.pos.offset || (pos.offset === operation.pos.offset && bias > 0))) {
443
+ return { pos: { path: [...parentPath, targetIndex + 1], offset: pos.offset - operation.pos.offset }, deleted: false };
444
+ }
445
+ if (samePath(pos.path, parentPath) && pos.offset > targetIndex) {
446
+ return { pos: { ...pos, offset: pos.offset + 1 }, deleted: false };
447
+ }
448
+ if (samePath(pos.path.slice(0, parentPath.length), parentPath) && pos.path.length > parentPath.length && pos.path[parentPath.length] > targetIndex) {
449
+ const path = [...pos.path];
450
+ path[parentPath.length] += 1;
451
+ return { pos: { ...pos, path }, deleted: false };
452
+ }
453
+ if (samePath(pos.path.slice(0, targetPath.length), targetPath) && pos.path.length > targetPath.length) {
454
+ const childIndex = pos.path[targetPath.length];
455
+ if (childIndex >= operation.pos.offset) {
456
+ return { pos: { ...pos, path: [...parentPath, targetIndex + 1, childIndex - operation.pos.offset, ...pos.path.slice(targetPath.length + 1)] }, deleted: false };
457
+ }
458
+ }
459
+ }
460
+ if (operation.type === "mergeNode") {
461
+ const rightPath = [...operation.pos.path, operation.pos.offset];
462
+ const leftPath = [...operation.pos.path, operation.pos.offset - 1];
463
+ if (samePath(pos.path, rightPath)) {
464
+ return { pos: { path: leftPath, offset: operation.splitOffset + pos.offset }, deleted: false };
465
+ }
466
+ if (samePath(pos.path.slice(0, rightPath.length), rightPath) && pos.path.length > rightPath.length) {
467
+ return { pos: { ...pos, path: [...leftPath, operation.splitOffset + pos.path[rightPath.length], ...pos.path.slice(rightPath.length + 1)] }, deleted: false };
468
+ }
469
+ if (samePath(pos.path, operation.pos.path) && pos.offset > operation.pos.offset) {
470
+ return { pos: { ...pos, offset: pos.offset - 1 }, deleted: false };
471
+ }
472
+ if (samePath(pos.path.slice(0, operation.pos.path.length), operation.pos.path) && pos.path.length > operation.pos.path.length && pos.path[operation.pos.path.length] > operation.pos.offset) {
473
+ const path = [...pos.path];
474
+ path[operation.pos.path.length] -= 1;
475
+ return { pos: { ...pos, path }, deleted: false };
476
+ }
477
+ }
478
+ if (operation.type === "insertText" && samePath(pos.path, operation.pos.path)) {
479
+ if (pos.offset > operation.pos.offset || (pos.offset === operation.pos.offset && bias > 0))
480
+ return { pos: { ...pos, offset: pos.offset + operation.text.length }, deleted: false };
481
+ }
482
+ if (operation.type === "deleteText" && samePath(pos.path, operation.pos.path)) {
483
+ const end = operation.pos.offset + operation.text.length;
484
+ if (pos.offset > operation.pos.offset && pos.offset <= end)
485
+ return { pos: { ...pos, offset: operation.pos.offset }, deleted: true };
486
+ if (pos.offset > end)
487
+ return { pos: { ...pos, offset: pos.offset - operation.text.length }, deleted: false };
488
+ }
489
+ return { pos: { path: [...pos.path], offset: pos.offset }, deleted: false };
490
+ };
491
+ /**
492
+ * `tieBreakBias` only affects the exact-tie case (operation's own structural
493
+ * position coincides exactly with `through`'s insertion point - e.g. two
494
+ * concurrent insertNode ops at the identical offset) - every other case's
495
+ * bias is unaffected. Defaults to 1 (through's content sorts first),
496
+ * preserving this function's existing, already-tested behavior exactly for
497
+ * every caller that doesn't pass it. A concurrent-rebase caller should
498
+ * derive this from a total order over the two operations' authors (e.g.
499
+ * lexicographic authorId comparison) so both transform directions agree:
500
+ * mapOperation(A, B, {tieBreakBias: A-sorts-first ? -1 : 1}) and
501
+ * mapOperation(B, A, {tieBreakBias: B-sorts-first ? -1 : 1}) must reach the
502
+ * same final order regardless of which one is "operation" and which is
503
+ * "through". This does not affect the addMark/removeMark range-endpoint
504
+ * biases below (-1/+1), which serve an unrelated purpose (range-growth
505
+ * semantics at an insertion boundary) and are deliberately left hardcoded.
506
+ */
507
+ export const mapOperation = (operation, through, options = {}) => {
508
+ // tieBreakBias only overrides the default when this is a genuine tie
509
+ // between two operations of the *same* kind competing for the identical
510
+ // insertion point (insertNode-vs-insertNode, insertText-vs-insertText).
511
+ // For every other pairing (e.g. removeNode vs. insertNode at a
512
+ // numerically coincidental offset), the default (1) is always correct
513
+ // regardless of authorship - removeNode identifies its target by node
514
+ // identity, not by an ordering competition, so "which one sorts first"
515
+ // has no meaning there; forcing the override through anyway (confirmed
516
+ // via a TP1 property test) shifted a removeNode's target to the wrong
517
+ // slot depending on which author happened to sort first.
518
+ const isGenuineTie = operation.type === through.type && (operation.type === "insertNode" || operation.type === "insertText" || operation.type === "splitNode");
519
+ const structuralBias = isGenuineTie ? options.tieBreakBias ?? 1 : 1;
520
+ const map = (pos, bias = structuralBias) => mapPosThroughOperation(pos, through, bias);
521
+ if (operation.type === "insertNode" || operation.type === "removeNode" || operation.type === "replaceNode") {
522
+ // removeNode/replaceNode's `.pos` addresses an *occupied* slot (the
523
+ // node currently there); insertNode's `.pos` addresses a *gap* (the
524
+ // boundary before an index) - two structurally different meanings that
525
+ // happen to share the same {path, offset} shape. `mapPosThroughOperation`
526
+ // is generic pure position arithmetic with no idea which convention a
527
+ // given `pos` follows, so an exact-offset coincidence between the two
528
+ // conventions is not a real "same target" match (confirmed via a TP1
529
+ // property test: an insertNode and a removeNode at the identical offset
530
+ // are two unrelated actions - "insert before index 4" and "remove index
531
+ // 4" - not a conflict over the same node). This check is therefore
532
+ // scoped here, at the one call site that actually knows both
533
+ // operations' real types, rather than inside the shared, type-blind
534
+ // position mapper - only removeNode-vs-removeNode/replaceNode (both
535
+ // occupied-slot semantics) can mean "the exact same node," e.g. two
536
+ // concurrent removeNode ops targeting the identical node.
537
+ if (operation.type !== "insertNode" && (through.type === "removeNode" || through.type === "replaceNode")
538
+ && samePath(operation.pos.path, through.pos.path) && operation.pos.offset === through.pos.offset) {
539
+ return null;
540
+ }
541
+ const result = map(operation.pos);
542
+ return result.deleted ? null : { ...operation, pos: result.pos };
543
+ }
544
+ if (operation.type === "moveNode") {
545
+ const from = map(operation.from);
546
+ const to = map(operation.to);
547
+ return from.deleted ? null : { ...operation, from: from.pos, to: to.pos };
548
+ }
549
+ if (operation.type === "splitNode" || operation.type === "mergeNode" || operation.type === "setNodeAttributes" || operation.type === "setNodeType" || operation.type === "insertText" || operation.type === "deleteText") {
550
+ const result = map(operation.pos);
551
+ return result.deleted ? null : { ...operation, pos: result.pos };
552
+ }
553
+ const from = map(operation.range.from, -1);
554
+ // `to` used to hardcode bias=1 ("an insertion exactly at this boundary
555
+ // grows the range to include it"), which disagreed with insertText's own,
556
+ // separately-established ground truth: applyToSession's insertText (via
557
+ // splitInlineAt, ~line 96-104 above) puts a plain-marks insertion at an
558
+ // exact run boundary on the *following* side, never absorbing the
559
+ // preceding run's marks - confirmed via docs/bugs/
560
+ // addmark-boundary-bias-inconsistent-with-inserttext.md's TP1 test, which
561
+ // found the two conventions genuinely diverged (not just "look different
562
+ // but agree"). bias=-1 matches that existing, unexamined-until-now
563
+ // resolution: a mark's own `to` endpoint does not grow to include a
564
+ // same-offset insertion, exactly as insertText itself never grows the
565
+ // preceding run to include new unmarked text.
566
+ const to = map(operation.range.to, -1);
567
+ return from.deleted && to.deleted ? null : { ...operation, range: { from: from.pos, to: to.pos } };
568
+ };
@@ -0,0 +1,10 @@
1
+ import type { FoundationPlugin } from "./types.js";
2
+ /**
3
+ * One FoundationPlugin per built-in feature family. Each is filled in as
4
+ * that family is converted (Phase 10 tasks: marks -> list -> block ->
5
+ * table -> atom) - wrapping its existing, already-tested command functions
6
+ * with the required description/examples metadata, not rewriting them.
7
+ * Families not yet converted still contribute their schema (so foundationSchema
8
+ * stays complete) with an empty command set.
9
+ */
10
+ export declare const builtInPlugins: readonly FoundationPlugin[];
@@ -0,0 +1,27 @@
1
+ import { blockNodeSpecs } from "../block/schema.js";
2
+ import { blockPluginCommands, blockPluginShortcuts, blockToolbarContributions } from "../block/plugin.js";
3
+ import { listNodeSpecs } from "../list/schema.js";
4
+ import { listPluginCommands, listPluginShortcuts } from "../list/plugin.js";
5
+ import { tableNodeSpecs } from "../table/schema.js";
6
+ import { tablePluginCommands, tableContextMenuContributions } from "../table/plugin.js";
7
+ import { atomNodeSpecs } from "../atom/schema.js";
8
+ import { atomPluginCommands } from "../atom/plugin.js";
9
+ import { markPluginCommands, markToolbarContributions, inlineMarkSpecs } from "../marks/plugin.js";
10
+ /**
11
+ * One FoundationPlugin per built-in feature family. Each is filled in as
12
+ * that family is converted (Phase 10 tasks: marks -> list -> block ->
13
+ * table -> atom) - wrapping its existing, already-tested command functions
14
+ * with the required description/examples metadata, not rewriting them.
15
+ * Families not yet converted still contribute their schema (so foundationSchema
16
+ * stays complete) with an empty command set.
17
+ */
18
+ export const builtInPlugins = [
19
+ {
20
+ id: "marks", version: "1.0.0", schema: { marks: inlineMarkSpecs }, commands: markPluginCommands,
21
+ toolbar: markToolbarContributions,
22
+ },
23
+ { id: "list", version: "1.0.0", schema: { nodes: listNodeSpecs }, commands: listPluginCommands, keyboardShortcuts: listPluginShortcuts },
24
+ { id: "block", version: "1.0.0", schema: { nodes: blockNodeSpecs }, commands: blockPluginCommands, toolbar: blockToolbarContributions, keyboardShortcuts: blockPluginShortcuts },
25
+ { id: "table", version: "1.0.0", schema: { nodes: tableNodeSpecs }, commands: tablePluginCommands, contextMenu: tableContextMenuContributions },
26
+ { id: "atom", version: "1.0.0", schema: { nodes: atomNodeSpecs }, commands: atomPluginCommands },
27
+ ];
@@ -0,0 +1,33 @@
1
+ import type { SmartOperation } from "../types.js";
2
+ import type { KeyboardShortcutContribution } from "./types.js";
3
+ export interface KeyLike {
4
+ readonly key: string;
5
+ readonly metaKey?: boolean;
6
+ readonly ctrlKey?: boolean;
7
+ readonly altKey?: boolean;
8
+ readonly shiftKey?: boolean;
9
+ }
10
+ /**
11
+ * Tries every key-matching shortcut contribution in priority order (ties
12
+ * broken by registration order, which is what PluginRegistry.keyboardShortcuts
13
+ * already provides), invoking `tryShortcut` for each and returning the
14
+ * first that actually produces operations.
15
+ *
16
+ * This is a fallback chain, not a static "one scope-kind owns this key"
17
+ * lookup, because ScopeKind alone cannot distinguish every case a real
18
+ * shortcut cares about (e.g. "inside a code block" and "inside a plain
19
+ * paragraph" are both "block-range" - see scope/resolveScope.ts). It
20
+ * reproduces surface/input.ts's actual current Tab precedence (code-block
21
+ * wins if applicable, then list unless inTable, then table/native
22
+ * fallthrough if nothing applies) by asking each candidate, in order,
23
+ * "does this genuinely apply here" via `tryShortcut` - which resolves the
24
+ * scope the contribution declares and runs its command - rather than by
25
+ * pre-filtering on a single precomputed scope-kind label. `scopeKinds` on
26
+ * a contribution remains meaningful as declared metadata for callers
27
+ * (which `resolveScope({want})` to attempt) and for doc generation, it's
28
+ * just no longer what this function filters by directly.
29
+ */
30
+ export declare const resolveShortcut: (shortcuts: readonly KeyboardShortcutContribution[], event: KeyLike, tryShortcut: (shortcut: KeyboardShortcutContribution) => readonly SmartOperation[] | null) => {
31
+ shortcut: KeyboardShortcutContribution;
32
+ operations: readonly SmartOperation[];
33
+ } | null;