smartrte-core 0.2.1 → 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 +12 -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 +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,860 @@
1
+ import { isTextNode } from "../identity.js";
2
+ import { occupancyGridFor } from "../table/index.js";
3
+ const pathKey = (path) => path.join("/");
4
+ const copyPos = (pos) => ({ path: [...pos.path], offset: pos.offset });
5
+ const copyRange = (range) => ({ from: copyPos(range.from), to: copyPos(range.to) });
6
+ const typeKey = (type) => type.toLowerCase().replace(/-/g, "_");
7
+ const fallbackRole = (type) => {
8
+ const key = typeKey(type);
9
+ if (key === "list")
10
+ return "list";
11
+ if (key === "list_item" || key === "listitem")
12
+ return "list-item";
13
+ if (key === "table")
14
+ return "table";
15
+ if (key === "table_row" || key === "tablerow")
16
+ return "table-row";
17
+ if (["table_cell", "table_header", "table_header_cell", "tablecell"].includes(key))
18
+ return "table-cell";
19
+ return null;
20
+ };
21
+ /** The single semantic-role resolution boundary: explicit role, then legacy type fallback. */
22
+ const roleOf = (node, schema) => schema.nodes[node.type]?.semanticRole ?? fallbackRole(node.type);
23
+ const isList = (entry) => entry.role === "list";
24
+ const isListItem = (entry) => entry.role === "list-item";
25
+ const isTable = (entry) => entry.role === "table";
26
+ const isTableRow = (entry) => entry.role === "table-row";
27
+ const isTableCell = (entry) => entry.role === "table-cell";
28
+ const schemaGroup = (node, schema) => {
29
+ if (node.type === "unknown") {
30
+ return node.attrs?.originalGroup === "inline" ? "inline" : "block";
31
+ }
32
+ return schema.nodes[node.type]?.group ?? "block";
33
+ };
34
+ const contentAcceptsInline = (node, schema) => {
35
+ const expression = schema.nodes[node.type]?.content;
36
+ return Boolean(expression && /(^|[^A-Za-z0-9_-])(?:inline|text)([^A-Za-z0-9_-]|$)/.test(expression));
37
+ };
38
+ const buildIndex = (document, schema) => {
39
+ const entries = [];
40
+ const byId = new Map();
41
+ const byPath = new Map();
42
+ let cursor = 0;
43
+ let preorder = 0;
44
+ const visit = (node, path, parent, index) => {
45
+ if (node.attrs?.["data-smart-ui"] === true) {
46
+ throw new Error("Editor UI nodes may not appear in the canonical model.");
47
+ }
48
+ if (byId.has(node.id))
49
+ throw new Error(`Duplicate node id "${node.id}".`);
50
+ const spec = schema.nodes[node.type];
51
+ const group = schemaGroup(node, schema);
52
+ const atomic = Boolean(spec?.atomic || node.type === "unknown");
53
+ const entry = {
54
+ node,
55
+ path: [...path],
56
+ parent,
57
+ children: [],
58
+ index,
59
+ depth: path.length,
60
+ preorder: preorder++,
61
+ open: cursor++,
62
+ contentStart: cursor,
63
+ contentEnd: cursor,
64
+ end: cursor,
65
+ inlineOwner: !atomic && contentAcceptsInline(node, schema),
66
+ group,
67
+ atomic,
68
+ isolating: Boolean(spec?.isolating),
69
+ role: roleOf(node, schema),
70
+ inlineChildren: [],
71
+ };
72
+ entries.push(entry);
73
+ byId.set(node.id, entry);
74
+ byPath.set(pathKey(path), entry);
75
+ if (entry.inlineOwner) {
76
+ let inlineOffset = 0;
77
+ for (const child of node.children || []) {
78
+ const width = isTextNode(child) ? child.text.length : 1;
79
+ entry.inlineChildren.push({ node: child, from: inlineOffset, to: inlineOffset + width });
80
+ if (isTextNode(child)) {
81
+ cursor += width;
82
+ }
83
+ else {
84
+ if (!schema.nodes[child.type]?.atomic && child.type !== "unknown") {
85
+ throw new Error(`Inline child "${child.type}" must be atomic.`);
86
+ }
87
+ const atomStart = cursor;
88
+ const atom = visit(child, [...path, entry.inlineChildren.length - 1], entry, entry.inlineChildren.length - 1);
89
+ atom.open = atomStart;
90
+ atom.contentStart = atomStart;
91
+ atom.contentEnd = atomStart + 1;
92
+ atom.end = atomStart + 1;
93
+ cursor = atomStart + 1;
94
+ }
95
+ inlineOffset += width;
96
+ }
97
+ }
98
+ else if (!atomic) {
99
+ (node.children || []).forEach((child, childIndex) => {
100
+ if (!isTextNode(child))
101
+ entry.children.push(visit(child, [...path, childIndex], entry, childIndex));
102
+ else
103
+ cursor += child.text.length;
104
+ });
105
+ }
106
+ entry.contentEnd = cursor;
107
+ entry.end = cursor++;
108
+ return entry;
109
+ };
110
+ const root = visit(document, [], null, 0);
111
+ const hasBlockDescendant = (entry) => entry.children.some((child) => (child.group === "block" && !isListItem(child) && !isTableRow(child) && !isTableCell(child)) || hasBlockDescendant(child));
112
+ const blocks = entries.filter((entry) => entry.group === "block"
113
+ && !isListItem(entry)
114
+ && !isTableRow(entry)
115
+ && !isTableCell(entry)
116
+ && (entry.atomic || !hasBlockDescendant(entry)));
117
+ const atoms = entries.filter((entry) => entry.atomic);
118
+ return { root, entries, byId, byPath, blocks, blockSet: new Set(blocks), atoms };
119
+ };
120
+ /** Reuses every cached entry/map when node identity says topology is unchanged. */
121
+ const refreshIndex = (index, document, schema) => {
122
+ const update = (entry, node) => {
123
+ if (entry.node === node)
124
+ return true;
125
+ if (entry.node.id !== node.id || entry.node.type !== node.type)
126
+ return false;
127
+ entry.node = node;
128
+ entry.isolating = Boolean(schema.nodes[node.type]?.isolating);
129
+ entry.role = roleOf(node, schema);
130
+ if (entry.inlineOwner) {
131
+ const beforeAtoms = entry.inlineChildren.filter((child) => !isTextNode(child.node)).map((child) => child.node.id);
132
+ const afterAtoms = (node.children || []).filter((child) => !isTextNode(child)).map((child) => child.id);
133
+ if (beforeAtoms.length !== afterAtoms.length || beforeAtoms.some((id, position) => id !== afterAtoms[position]))
134
+ return false;
135
+ let offset = 0;
136
+ entry.inlineChildren = (node.children || []).map((child) => {
137
+ const from = offset;
138
+ offset += isTextNode(child) ? child.text.length : 1;
139
+ if (!isTextNode(child)) {
140
+ const atom = index.byId.get(child.id);
141
+ if (!atom || !update(atom, child))
142
+ return { node: child, from, to: offset };
143
+ }
144
+ return { node: child, from, to: offset };
145
+ });
146
+ return true;
147
+ }
148
+ const children = (node.children || []).filter((child) => !isTextNode(child));
149
+ if (children.length !== entry.children.length)
150
+ return false;
151
+ for (let position = 0; position < children.length; position += 1) {
152
+ if (children[position].id !== entry.children[position].node.id || !update(entry.children[position], children[position]))
153
+ return false;
154
+ }
155
+ return true;
156
+ };
157
+ if (!update(index.root, document))
158
+ return false;
159
+ let cursor = 0;
160
+ const reflow = (entry) => {
161
+ entry.open = cursor++;
162
+ entry.contentStart = cursor;
163
+ if (entry.inlineOwner) {
164
+ entry.inlineChildren.forEach((child) => {
165
+ if (isTextNode(child.node))
166
+ cursor += child.node.text.length;
167
+ else {
168
+ const atom = index.byId.get(child.node.id);
169
+ if (atom) {
170
+ atom.open = cursor;
171
+ atom.contentStart = cursor;
172
+ atom.contentEnd = cursor + 1;
173
+ atom.end = cursor + 1;
174
+ }
175
+ cursor += 1;
176
+ }
177
+ });
178
+ }
179
+ else if (!entry.atomic)
180
+ entry.children.forEach(reflow);
181
+ entry.contentEnd = cursor;
182
+ entry.end = cursor++;
183
+ };
184
+ reflow(index.root);
185
+ return true;
186
+ };
187
+ const limitFor = (entry) => entry.inlineOwner
188
+ ? entry.inlineChildren.reduce((size, child) => Math.max(size, child.to), 0)
189
+ : entry.children.length;
190
+ const nodeAroundInlineOffset = (entry, offset, before) => {
191
+ const children = entry.inlineChildren;
192
+ if (before) {
193
+ for (let index = children.length - 1; index >= 0; index -= 1)
194
+ if (children[index].to <= offset)
195
+ return children[index].node;
196
+ }
197
+ else {
198
+ for (const child of children)
199
+ if (child.from >= offset)
200
+ return child.node;
201
+ }
202
+ return null;
203
+ };
204
+ /** The only place in the scope runtime that interprets stored SmartPos fields. */
205
+ const resolveEndpoint = (index, pos) => {
206
+ const entry = index.byPath.get(pathKey(pos.path));
207
+ if (!entry)
208
+ throw new Error("A SmartPos path must resolve to an owning non-text node.");
209
+ if (entry.atomic)
210
+ throw new Error("A position cannot resolve inside an atomic node; address its owning container boundary.");
211
+ const limit = limitFor(entry);
212
+ if (!Number.isInteger(pos.offset) || pos.offset < 0 || pos.offset > limit) {
213
+ throw new Error(`Position offset ${pos.offset} is outside 0..${limit}.`);
214
+ }
215
+ const ancestors = [];
216
+ let cursor = entry;
217
+ while (cursor) {
218
+ ancestors.unshift(cursor.node);
219
+ cursor = cursor.parent;
220
+ }
221
+ const rank = entry.inlineOwner
222
+ ? entry.contentStart + pos.offset
223
+ : pos.offset < entry.children.length ? entry.children[pos.offset].open : entry.contentEnd;
224
+ const resolved = {
225
+ pos: copyPos(pos),
226
+ kind: entry.inlineOwner ? "inline" : "structural",
227
+ nodeId: entry.node.id,
228
+ parent: entry.node,
229
+ depth: entry.depth,
230
+ affinity: "backward",
231
+ ancestors,
232
+ nodeBefore: entry.inlineOwner
233
+ ? nodeAroundInlineOffset(entry, pos.offset, true)
234
+ : pos.offset > 0 ? entry.node.children?.[pos.offset - 1] ?? null : null,
235
+ nodeAfter: entry.inlineOwner
236
+ ? nodeAroundInlineOffset(entry, pos.offset, false)
237
+ : pos.offset < limit ? entry.node.children?.[pos.offset] ?? null : null,
238
+ atStart: pos.offset === 0,
239
+ atEnd: pos.offset === limit,
240
+ indexAt(depth) {
241
+ if (!Number.isInteger(depth) || depth < 0 || depth >= pos.path.length)
242
+ throw new Error("Position depth is out of range.");
243
+ return pos.path[depth];
244
+ },
245
+ };
246
+ cursor = entry;
247
+ let isolation = null;
248
+ while (cursor) {
249
+ if (cursor.isolating) {
250
+ isolation = cursor;
251
+ break;
252
+ }
253
+ cursor = cursor.parent;
254
+ }
255
+ return { resolved, entry, rank, isolation };
256
+ };
257
+ const stablePosKey = (pos) => `${pos.path.map((part) => String(part).padStart(8, "0")).join(".")}:${String(pos.offset).padStart(12, "0")}`;
258
+ const normalizeSelection = (index, selection) => {
259
+ const anchor = resolveEndpoint(index, selection.anchor);
260
+ const head = resolveEndpoint(index, selection.head);
261
+ const anchorFirst = anchor.rank < head.rank || (anchor.rank === head.rank && stablePosKey(anchor.resolved.pos) <= stablePosKey(head.resolved.pos));
262
+ return anchorFirst
263
+ ? { from: anchor, to: head, range: { from: copyPos(anchor.resolved.pos), to: copyPos(head.resolved.pos) } }
264
+ : { from: head, to: anchor, range: { from: copyPos(head.resolved.pos), to: copyPos(anchor.resolved.pos) } };
265
+ };
266
+ const atomicParentSpan = (entry) => {
267
+ if (!entry.atomic || !entry.parent)
268
+ return null;
269
+ if (entry.parent.inlineOwner) {
270
+ const span = entry.parent.inlineChildren.find((child) => !isTextNode(child.node) && child.node.id === entry.node.id);
271
+ return span ? { parent: entry.parent, from: span.from, to: span.to } : null;
272
+ }
273
+ return { parent: entry.parent, from: entry.index, to: entry.index + 1 };
274
+ };
275
+ const entryStartPos = (entry) => {
276
+ const atom = atomicParentSpan(entry);
277
+ return atom ? { path: [...atom.parent.path], offset: atom.from } : { path: [...entry.path], offset: 0 };
278
+ };
279
+ const entryEndPos = (entry) => {
280
+ const atom = atomicParentSpan(entry);
281
+ return atom ? { path: [...atom.parent.path], offset: atom.to } : { path: [...entry.path], offset: limitFor(entry) };
282
+ };
283
+ const afterEntryPos = (entry) => entry.parent
284
+ ? { path: [...entry.parent.path], offset: entry.index + 1 }
285
+ : entryEndPos(entry);
286
+ const isAncestor = (ancestor, descendant) => {
287
+ let cursor = descendant;
288
+ while (cursor) {
289
+ if (cursor === ancestor)
290
+ return true;
291
+ cursor = cursor.parent;
292
+ }
293
+ return false;
294
+ };
295
+ const outermostIsolation = (entry) => {
296
+ let result = entry;
297
+ let cursor = entry.parent;
298
+ while (cursor) {
299
+ if (cursor.isolating)
300
+ result = cursor;
301
+ cursor = cursor.parent;
302
+ }
303
+ return result;
304
+ };
305
+ const spansIsolating = (index, from, to) => {
306
+ if (from.isolation?.node.id !== to.isolation?.node.id)
307
+ return true;
308
+ return index.entries.some((entry) => entry.isolating
309
+ && entry.open > from.rank
310
+ && entry.end < to.rank
311
+ && entry !== from.isolation);
312
+ };
313
+ /**
314
+ * Shared isolation clamp. To preserve the stronger reverse-selection invariant,
315
+ * the active side is the normalized document-order end rather than the mutable
316
+ * anchor/head direction. Table-grid may cross sibling cells in the same table.
317
+ */
318
+ const clampToIsolating = (index, from, to, request) => {
319
+ const crossCellGrid = request.want === "table-grid"
320
+ && from.isolation && to.isolation
321
+ && isTableCell(from.isolation) && isTableCell(to.isolation)
322
+ && nearestAncestor(from.entry, isTable) === nearestAncestor(to.entry, isTable);
323
+ if (!spansIsolating(index, from, to) || crossCellGrid) {
324
+ return { from, to, range: { from: copyPos(from.resolved.pos), to: copyPos(to.resolved.pos) }, clamped: false };
325
+ }
326
+ if (to.isolation) {
327
+ const start = resolveEndpoint(index, entryStartPos(to.isolation));
328
+ const end = resolveEndpoint(index, entryEndPos(to.isolation));
329
+ const clampedFrom = from.rank < start.rank ? start : from;
330
+ const clampedTo = to.rank > end.rank ? end : to;
331
+ return {
332
+ from: clampedFrom,
333
+ to: clampedTo,
334
+ range: { from: copyPos(clampedFrom.resolved.pos), to: copyPos(clampedTo.resolved.pos) },
335
+ clamped: true,
336
+ };
337
+ }
338
+ const crossed = index.entries
339
+ .filter((entry) => entry.isolating && entry.end > from.rank && entry.open < to.rank)
340
+ .map(outermostIsolation)
341
+ .filter((entry, position, all) => all.indexOf(entry) === position)
342
+ .sort((left, right) => right.end - left.end)[0];
343
+ if (!crossed)
344
+ return { from, to, range: { from: copyPos(from.resolved.pos), to: copyPos(to.resolved.pos) }, clamped: false };
345
+ const clampedFrom = resolveEndpoint(index, afterEntryPos(crossed));
346
+ return {
347
+ from: clampedFrom,
348
+ to,
349
+ range: { from: copyPos(clampedFrom.resolved.pos), to: copyPos(to.resolved.pos) },
350
+ clamped: true,
351
+ };
352
+ };
353
+ const nearestAncestor = (entry, predicate) => {
354
+ let cursor = entry;
355
+ while (cursor) {
356
+ if (predicate(cursor))
357
+ return cursor;
358
+ cursor = cursor.parent;
359
+ }
360
+ return null;
361
+ };
362
+ const commonAncestor = (entries) => {
363
+ if (!entries.length)
364
+ return null;
365
+ let cursor = entries[0];
366
+ while (cursor) {
367
+ if (entries.every((entry) => isAncestor(cursor, entry)))
368
+ return cursor;
369
+ cursor = cursor.parent;
370
+ }
371
+ return null;
372
+ };
373
+ const baseFor = (context, isolation) => ({
374
+ range: copyRange(context.range),
375
+ isolatingAncestorId: (isolation === undefined ? commonIsolation(context) : isolation)?.node.id ?? null,
376
+ clamped: context.clamped,
377
+ ...(context.clampReason ? { clampReason: context.clampReason } : {}),
378
+ });
379
+ const commonIsolation = (context) => {
380
+ const left = context.from.isolation;
381
+ const right = context.to.isolation;
382
+ return left && left === right ? left : null;
383
+ };
384
+ const emptyScope = (context) => ({ kind: "empty", ...baseFor(context) });
385
+ const overlaps = (entry, from, to) => entry.contentStart < to && entry.contentEnd >= from;
386
+ const containingBlock = (context, endpoint) => nearestAncestor(endpoint.entry, (entry) => context.index.blockSet.has(entry));
387
+ const touchedBlocks = (context) => {
388
+ if (context.from.rank === context.to.rank) {
389
+ const block = containingBlock(context, context.to);
390
+ return block ? [block] : [];
391
+ }
392
+ const first = containingBlock(context, context.from);
393
+ const last = containingBlock(context, context.to);
394
+ if (first && first === last)
395
+ return [first];
396
+ return context.index.blocks.filter((entry) => {
397
+ if (entry.contentStart === entry.contentEnd)
398
+ return entry.contentStart > context.from.rank && entry.contentStart <= context.to.rank;
399
+ return entry.contentStart < context.to.rank && entry.contentEnd >= context.from.rank;
400
+ });
401
+ };
402
+ const blockPart = (context, blocks) => {
403
+ const first = blocks[0];
404
+ const last = blocks[blocks.length - 1];
405
+ const parents = new Set(blocks.map((entry) => entry.parent?.node.id ?? null));
406
+ return {
407
+ kind: "block-range",
408
+ ...baseFor(context),
409
+ blockIds: blocks.map((entry) => entry.node.id),
410
+ promotedFromPartial: context.from.rank > first.contentStart || context.to.rank < last.contentEnd,
411
+ commonParentId: parents.size === 1 ? [...parents][0] : null,
412
+ };
413
+ };
414
+ const ownerSlice = (entry, fromRank, toRank) => ({
415
+ from: Math.max(0, Math.min(limitFor(entry), fromRank - entry.contentStart)),
416
+ to: Math.max(0, Math.min(limitFor(entry), toRank - entry.contentStart)),
417
+ });
418
+ const resolveInlineRange = (context) => {
419
+ const owners = context.from.entry === context.to.entry && context.from.entry.inlineOwner
420
+ ? [context.from.entry]
421
+ : context.index.entries.filter((entry) => entry.inlineOwner && (context.from.rank === context.to.rank
422
+ ? context.from.entry === entry
423
+ : entry.contentStart < context.to.rank && entry.contentEnd >= context.from.rank));
424
+ if (!owners.length)
425
+ return emptyScope(context);
426
+ const runs = owners.map((owner) => {
427
+ const slice = ownerSlice(owner, context.from.rank, context.to.rank);
428
+ return {
429
+ ownerNodeId: owner.node.id,
430
+ from: slice.from,
431
+ to: slice.to,
432
+ containsAtoms: owner.inlineChildren.some((child) => !isTextNode(child.node) && child.from < slice.to && child.to > slice.from),
433
+ };
434
+ });
435
+ const collapsed = context.from.rank === context.to.rank;
436
+ return {
437
+ kind: "inline-range",
438
+ ...baseFor(context),
439
+ runs,
440
+ collapsed,
441
+ ...(collapsed ? { storedMarkAnchor: context.from.resolved } : {}),
442
+ };
443
+ };
444
+ const resolveBlockRange = (context) => {
445
+ const blocks = touchedBlocks(context);
446
+ if (!blocks.length)
447
+ return emptyScope(context);
448
+ return blockPart(context, blocks);
449
+ };
450
+ const subtreeIds = (root) => {
451
+ const result = [];
452
+ const visit = (entry) => {
453
+ result.push(entry.node.id);
454
+ entry.children.forEach(visit);
455
+ entry.inlineChildren.forEach((child) => {
456
+ if (!isTextNode(child.node))
457
+ result.push(child.node.id);
458
+ });
459
+ };
460
+ visit(root);
461
+ return result;
462
+ };
463
+ const resolveContainerTree = (context, stopAt) => {
464
+ const blocks = touchedBlocks(context);
465
+ if (!blocks.length)
466
+ return emptyScope(context);
467
+ let root = commonAncestor(blocks) ?? blocks[0];
468
+ if (stopAt) {
469
+ const stops = blocks.map((block) => nearestAncestor(block, (entry) => stopAt(entry.node))).filter((entry) => Boolean(entry));
470
+ if (stops.length === blocks.length && stops.every((entry) => entry === stops[0]))
471
+ root = stops[0];
472
+ }
473
+ const first = blocks[0];
474
+ const last = blocks[blocks.length - 1];
475
+ return {
476
+ kind: "container-tree",
477
+ ...baseFor(context),
478
+ rootId: root.node.id,
479
+ nodeIds: subtreeIds(root),
480
+ promotedFromPartial: context.from.rank > first.contentStart || context.to.rank < last.contentEnd,
481
+ };
482
+ };
483
+ const atomSelectedExactly = (context, atom) => {
484
+ if (atom.group === "inline") {
485
+ const owner = atom.parent;
486
+ const span = owner?.inlineChildren.find((child) => !isTextNode(child.node) && child.node.id === atom.node.id);
487
+ return Boolean(owner && span
488
+ && context.from.entry === owner && context.to.entry === owner
489
+ && context.from.resolved.pos.offset === span.from && context.to.resolved.pos.offset === span.to);
490
+ }
491
+ const parent = atom.parent;
492
+ return Boolean(parent
493
+ && context.from.entry === parent && context.to.entry === parent
494
+ && context.from.resolved.pos.offset === atom.index
495
+ && context.to.resolved.pos.offset === atom.index + 1);
496
+ };
497
+ const atomsTouched = (context) => context.from.rank === context.to.rank ? [] : context.index.atoms.filter((atom) => {
498
+ if (atomSelectedExactly(context, atom))
499
+ return true;
500
+ if (context.from.rank === context.to.rank)
501
+ return false;
502
+ if (atom.group === "inline") {
503
+ const owner = atom.parent;
504
+ const span = owner?.inlineChildren.find((child) => !isTextNode(child.node) && child.node.id === atom.node.id);
505
+ if (!owner || !span)
506
+ return false;
507
+ const slice = ownerSlice(owner, context.from.rank, context.to.rank);
508
+ return context.from.entry === owner || context.to.entry === owner || overlaps(owner, context.from.rank, context.to.rank)
509
+ ? span.from < slice.to && span.to > slice.from
510
+ : false;
511
+ }
512
+ return atom.open < context.to.rank && atom.end > context.from.rank;
513
+ });
514
+ const atomScope = (context, atom) => ({
515
+ kind: "atomic-node",
516
+ ...baseFor(context, nearestAncestor(atom, (entry) => entry.isolating)),
517
+ nodeId: atom.node.id,
518
+ inline: atom.group === "inline",
519
+ });
520
+ const resolveAtomicNode = (context) => {
521
+ const atoms = atomsTouched(context).filter((atom) => atomSelectedExactly(context, atom));
522
+ if (!atoms.length)
523
+ return emptyScope(context);
524
+ if (atoms.length === 1)
525
+ return atomScope(context, atoms[0]);
526
+ return { kind: "mixed", ...baseFor(context), parts: atoms.map((atom) => atomScope(context, atom)) };
527
+ };
528
+ const touchedListItems = (context) => {
529
+ if (context.from.rank === context.to.rank) {
530
+ const item = nearestAncestor(context.to.entry, isListItem);
531
+ return item ? [item] : [];
532
+ }
533
+ // A ranged selection wholly inside one nested list must resolve to that
534
+ // list, not to every ancestor list item whose content interval contains the
535
+ // descendant. Ancestor intervals intentionally include nested content for
536
+ // block promotion, but list-type commands act on the nearest list whose
537
+ // items the user actually selected. When the endpoints belong to
538
+ // different list levels we retain the broader traversal below so a range
539
+ // that genuinely crosses a parent/child boundary still promotes to the
540
+ // containing list.
541
+ const fromItem = nearestAncestor(context.from.entry, isListItem);
542
+ const toItem = nearestAncestor(context.to.entry, isListItem);
543
+ const fromList = fromItem ? nearestAncestor(fromItem.parent ?? fromItem, isList) : null;
544
+ const toList = toItem ? nearestAncestor(toItem.parent ?? toItem, isList) : null;
545
+ if (fromList && fromList === toList) {
546
+ return context.index.entries.filter((entry) => isListItem(entry)
547
+ && isAncestor(fromList, entry)
548
+ && overlaps(entry, context.from.rank, context.to.rank)
549
+ && !(entry === toItem && endpointAtContentStart(context.to, entry)));
550
+ }
551
+ return context.index.entries.filter((entry) => isListItem(entry) && overlaps(entry, context.from.rank, context.to.rank));
552
+ };
553
+ const listDepth = (item, list) => {
554
+ let depth = 0;
555
+ let cursor = item.parent;
556
+ while (cursor && cursor !== list) {
557
+ if (isList(cursor))
558
+ depth += 1;
559
+ cursor = cursor.parent;
560
+ }
561
+ return depth;
562
+ };
563
+ /**
564
+ * A range ending at the first editable position of a list item has not
565
+ * entered that item yet. Entry content ranges include the item's wrapper and
566
+ * first block, so a raw rank overlap would otherwise promote the endpoint
567
+ * item as well (and can turn a one-item deletion into a whole-list deletion).
568
+ */
569
+ const firstContentEntry = (entry) => {
570
+ if (entry.inlineOwner || entry.atomic || !entry.children.length)
571
+ return entry;
572
+ return firstContentEntry(entry.children[0]);
573
+ };
574
+ const endpointAtContentStart = (endpoint, entry) => {
575
+ const first = firstContentEntry(entry);
576
+ return endpoint.entry === first && endpoint.resolved.pos.offset === 0;
577
+ };
578
+ const listPart = (context, list, items) => {
579
+ const selected = new Set(items);
580
+ const hasChildList = (item) => context.index.entries.some((entry) => isList(entry) && entry !== list && isAncestor(item, entry));
581
+ const partialSubtree = items.some((item) => hasChildList(item)
582
+ && context.index.entries.some((entry) => isListItem(entry) && isAncestor(item, entry) && entry !== item && !selected.has(entry)));
583
+ return {
584
+ kind: "list-selection",
585
+ ...baseFor(context),
586
+ listId: list.node.id,
587
+ items: items.map((item) => ({ itemId: item.node.id, depth: listDepth(item, list), hasChildList: hasChildList(item) })),
588
+ partialSubtree,
589
+ promotedFromPartial: context.from.rank > items[0].contentStart || context.to.rank < items[items.length - 1].contentEnd,
590
+ };
591
+ };
592
+ const selectedListRoot = (item, selected) => {
593
+ let list = nearestAncestor(item.parent ?? item, isList);
594
+ if (!list)
595
+ return null;
596
+ while (true) {
597
+ const ownerItem = nearestAncestor(list.parent ?? list, isListItem);
598
+ if (!ownerItem || !selected.has(ownerItem))
599
+ return list;
600
+ const outer = nearestAncestor(ownerItem.parent ?? ownerItem, isList);
601
+ if (!outer)
602
+ return list;
603
+ list = outer;
604
+ }
605
+ };
606
+ const resolveListSelection = (context) => {
607
+ const items = touchedListItems(context);
608
+ const selected = new Set(items);
609
+ const groups = new Map();
610
+ items.forEach((item) => {
611
+ const list = selectedListRoot(item, selected);
612
+ if (list)
613
+ groups.set(list, [...(groups.get(list) || []), item]);
614
+ });
615
+ const parts = [...groups].map(([list, listItems]) => ({
616
+ order: list.preorder,
617
+ scope: listPart(context, list, listItems),
618
+ }));
619
+ const plainBlocks = touchedBlocks(context).filter((block) => !nearestAncestor(block, isListItem));
620
+ if (plainBlocks.length)
621
+ parts.push({ order: plainBlocks[0].preorder, scope: blockPart(context, plainBlocks) });
622
+ parts.sort((left, right) => left.order - right.order);
623
+ if (!parts.length)
624
+ return emptyScope(context);
625
+ if (parts.length === 1)
626
+ return parts[0].scope;
627
+ return { kind: "mixed", ...baseFor(context), parts: parts.map((part) => part.scope) };
628
+ };
629
+ const endpointCell = (endpoint) => nearestAncestor(endpoint.entry, isTableCell);
630
+ const touchedCells = (context, table) => {
631
+ if (context.from.rank === context.to.rank) {
632
+ const cell = endpointCell(context.to);
633
+ return cell && isAncestor(table, cell) ? [cell] : [];
634
+ }
635
+ return context.index.entries.filter((entry) => isTableCell(entry) && isAncestor(table, entry) && overlaps(entry, context.from.rank, context.to.rank));
636
+ };
637
+ const tablePart = (context, table) => {
638
+ const grid = occupancyGridFor(table.node);
639
+ const fromCell = endpointCell(context.from);
640
+ const toCell = endpointCell(context.to);
641
+ let selected;
642
+ if (fromCell && toCell && context.from.resolved.pos && context.to.resolved.pos) {
643
+ const first = grid.anchors.find((cell) => cell.cellId === fromCell.node.id);
644
+ const last = grid.anchors.find((cell) => cell.cellId === toCell.node.id);
645
+ if (first && last && context.from.resolved.pos && context.to.resolved.pos && fromCell !== toCell) {
646
+ const bounds = {
647
+ top: Math.min(first.top, last.top), left: Math.min(first.left, last.left),
648
+ bottom: Math.max(first.bottom, last.bottom), right: Math.max(first.right, last.right),
649
+ };
650
+ selected = grid.anchors.filter((cell) => cell.top >= bounds.top && cell.left >= bounds.left && cell.top < bounds.bottom && cell.left < bounds.right);
651
+ }
652
+ else {
653
+ const touched = new Set(touchedCells(context, table).map((entry) => entry.node.id));
654
+ selected = grid.anchors.filter((cell) => touched.has(cell.cellId));
655
+ }
656
+ }
657
+ else {
658
+ const touched = new Set(touchedCells(context, table).map((entry) => entry.node.id));
659
+ selected = grid.anchors.filter((cell) => touched.has(cell.cellId));
660
+ }
661
+ if (!selected.length)
662
+ return emptyScope(context);
663
+ const rect = {
664
+ top: Math.min(...selected.map((cell) => cell.top)),
665
+ left: Math.min(...selected.map((cell) => cell.left)),
666
+ bottom: Math.max(...selected.map((cell) => cell.bottom)) - 1,
667
+ right: Math.max(...selected.map((cell) => cell.right)) - 1,
668
+ };
669
+ const selectedSet = new Set(selected.map((cell) => cell.cellId));
670
+ const covered = new Set();
671
+ let rectangular = true;
672
+ for (let row = rect.top; row <= rect.bottom; row += 1) {
673
+ for (let column = rect.left; column <= rect.right; column += 1) {
674
+ const occupant = grid.at(row, column);
675
+ if (!occupant || !selectedSet.has(occupant.cellId))
676
+ rectangular = false;
677
+ if (occupant && (occupant.top < rect.top || occupant.left < rect.left))
678
+ covered.add(occupant.cellId);
679
+ }
680
+ }
681
+ if (selected.some((cell) => cell.bottom - 1 > rect.bottom || cell.right - 1 > rect.right))
682
+ rectangular = false;
683
+ return {
684
+ kind: "table-grid",
685
+ ...baseFor(context, nearestAncestor(table, (entry) => entry.isolating)),
686
+ tableId: table.node.id,
687
+ rect,
688
+ cellIds: selected.map((cell) => cell.cellId),
689
+ coveredCellIds: [...covered],
690
+ rectangular,
691
+ };
692
+ };
693
+ const resolveTableGrid = (context) => {
694
+ const tables = new Set();
695
+ const fromTable = nearestAncestor(context.from.entry, isTable);
696
+ const toTable = nearestAncestor(context.to.entry, isTable);
697
+ if (fromTable)
698
+ tables.add(fromTable);
699
+ if (toTable)
700
+ tables.add(toTable);
701
+ if (fromTable !== toTable || !fromTable)
702
+ context.index.entries.forEach((entry) => {
703
+ if (isTable(entry) && overlaps(entry, context.from.rank, context.to.rank))
704
+ tables.add(entry);
705
+ });
706
+ const parts = [...tables].sort((a, b) => a.preorder - b.preorder).map((table) => tablePart(context, table)).filter((part) => part.kind === "table-grid");
707
+ if (!parts.length)
708
+ return emptyScope(context);
709
+ if (parts.length === 1)
710
+ return parts[0];
711
+ return { kind: "mixed", ...baseFor(context), parts };
712
+ };
713
+ const markKey = (mark) => JSON.stringify([mark.type, mark.attrs ?? null]);
714
+ const marksForRange = (context) => {
715
+ if (context.from.rank === context.to.rank && context.from.entry.inlineOwner) {
716
+ const offset = context.from.resolved.pos.offset;
717
+ // A collapsed caret strictly inside a run (not at any run boundary) has
718
+ // exactly one containing run - `from <= offset <= to` - regardless of
719
+ // how many other runs the owner has. At an exact boundary between two
720
+ // runs, both the preceding and following run satisfy that inequality;
721
+ // prefer the preceding one (`to === offset`), matching this function's
722
+ // original tie-break (continuing the run just typed/edited, not the
723
+ // one about to be entered). The previous version only ever found a run
724
+ // via that boundary tie-break, which by construction never matches a
725
+ // caret that isn't at some run's edge - a collapsed caret anywhere in
726
+ // the interior of a run (the overwhelmingly common case) silently
727
+ // returned no marks at all.
728
+ const touching = context.from.entry.inlineChildren.filter((candidate) => candidate.from <= offset && offset <= candidate.to);
729
+ const child = touching.find((candidate) => candidate.to === offset) ?? touching[0];
730
+ return child && isTextNode(child.node)
731
+ ? child.node.marks?.map((mark) => ({ mark, coverage: "all" })) || []
732
+ : [];
733
+ }
734
+ const occurrences = new Map();
735
+ let total = 0;
736
+ context.index.entries.filter((entry) => entry.inlineOwner).forEach((owner) => {
737
+ const slice = ownerSlice(owner, context.from.rank, context.to.rank);
738
+ owner.inlineChildren.forEach((child) => {
739
+ if (!isTextNode(child.node))
740
+ return;
741
+ const width = Math.max(0, Math.min(child.to, slice.to) - Math.max(child.from, slice.from));
742
+ if (!width)
743
+ return;
744
+ total += width;
745
+ (child.node.marks || []).forEach((mark) => {
746
+ const key = markKey(mark);
747
+ const current = occurrences.get(key);
748
+ occurrences.set(key, { mark, covered: (current?.covered || 0) + width });
749
+ });
750
+ });
751
+ });
752
+ return [...occurrences.values()].map(({ mark, covered }) => ({ mark, coverage: covered === total ? "all" : "partial" }));
753
+ };
754
+ const describeSelection = (unclamped) => {
755
+ const blocks = touchedBlocks(unclamped);
756
+ const canonical = unclamped.to;
757
+ const item = nearestAncestor(canonical.entry, isListItem);
758
+ const list = item ? nearestAncestor(item.parent ?? item, isList) : null;
759
+ const cell = endpointCell(canonical);
760
+ const table = cell ? nearestAncestor(cell.parent ?? cell, isTable) : null;
761
+ return {
762
+ blockTypes: [...new Set(blocks.map((entry) => entry.node.type))],
763
+ marks: marksForRange(unclamped),
764
+ inList: list && item ? { listId: list.node.id, depth: listDepth(item, list) } : null,
765
+ inTable: table && cell ? { tableId: table.node.id, cellId: cell.node.id } : null,
766
+ isolatingAncestorId: canonical.isolation?.node.id ?? null,
767
+ atoms: atomsTouched(unclamped).map((entry) => entry.node.id),
768
+ collapsed: unclamped.from.rank === unclamped.to.rank,
769
+ spansIsolatingBoundary: spansIsolating(unclamped.index, unclamped.from, unclamped.to),
770
+ };
771
+ };
772
+ const contextFor = (index, schema, original, from, to, range, clamped) => ({
773
+ index,
774
+ schema,
775
+ original: copyRange(original),
776
+ from,
777
+ to,
778
+ range: copyRange(range),
779
+ clamped,
780
+ ...(clamped ? { clampReason: "isolating" } : {}),
781
+ });
782
+ const resolveWithIndex = (index, selection, request, schema) => {
783
+ const normalized = normalizeSelection(index, selection);
784
+ const unclamped = contextFor(index, schema, normalized.range, normalized.from, normalized.to, normalized.range, false);
785
+ if (request.want === "describe")
786
+ return describeSelection(unclamped);
787
+ const clamped = clampToIsolating(index, normalized.from, normalized.to, request);
788
+ const context = contextFor(index, schema, normalized.range, clamped.from, clamped.to, clamped.range, clamped.clamped);
789
+ switch (request.want) {
790
+ case "inline-range": return resolveInlineRange(context);
791
+ case "block-range": return resolveBlockRange(context);
792
+ case "container-tree": return resolveContainerTree(context, request.stopAt);
793
+ case "list-selection": return resolveListSelection(context);
794
+ case "table-grid": return resolveTableGrid(context);
795
+ case "atomic-node": return resolveAtomicNode(context);
796
+ }
797
+ };
798
+ const outerRange = (entry) => {
799
+ if (!entry.parent)
800
+ return { from: entryStartPos(entry), to: entryEndPos(entry) };
801
+ const atom = atomicParentSpan(entry);
802
+ if (atom)
803
+ return {
804
+ from: { path: [...atom.parent.path], offset: atom.from },
805
+ to: { path: [...atom.parent.path], offset: atom.to },
806
+ };
807
+ return {
808
+ from: { path: [...entry.parent.path], offset: entry.index },
809
+ to: { path: [...entry.parent.path], offset: entry.index + 1 },
810
+ };
811
+ };
812
+ const lookupFor = (index) => ({
813
+ positionOf(nodeId) {
814
+ const entry = index.byId.get(nodeId);
815
+ if (!entry)
816
+ return null;
817
+ return resolveEndpoint(index, outerRange(entry).from).resolved;
818
+ },
819
+ rangeOf(nodeId) {
820
+ const entry = index.byId.get(nodeId);
821
+ return entry ? copyRange(outerRange(entry)) : null;
822
+ },
823
+ contentRangeOf(nodeId) {
824
+ const entry = index.byId.get(nodeId);
825
+ if (!entry)
826
+ return null;
827
+ if (entry.atomic)
828
+ return copyRange(outerRange(entry));
829
+ return { from: entryStartPos(entry), to: entryEndPos(entry) };
830
+ },
831
+ exists(nodeId) { return index.byId.has(nodeId); },
832
+ });
833
+ export class FoundationScopeIndex {
834
+ constructor() {
835
+ this.document = null;
836
+ this.schema = null;
837
+ this.index = null;
838
+ }
839
+ update(document, schema) {
840
+ if (this.document === document && this.schema === schema && this.index)
841
+ return this.index;
842
+ if (this.schema === schema && this.index && refreshIndex(this.index, document, schema)) {
843
+ this.document = document;
844
+ return this.index;
845
+ }
846
+ this.document = document;
847
+ this.schema = schema;
848
+ this.index = buildIndex(document, schema);
849
+ return this.index;
850
+ }
851
+ resolve(document, selection, request, schema) {
852
+ return resolveWithIndex(this.update(document, schema), selection, request, schema);
853
+ }
854
+ positions(document, schema) {
855
+ return lookupFor(this.update(document, schema));
856
+ }
857
+ get liveNodeCount() { return this.index?.entries.length ?? 0; }
858
+ }
859
+ export const createScopeIndex = () => new FoundationScopeIndex();
860
+ export const resolveScope = (document, selection, request, schema) => resolveWithIndex(buildIndex(document, schema), selection, request, schema);