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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0-beta.2
4
+
5
+ - Add an opt-in `renderFormulaHtml` option to `serializeCanonicalListHtml` that bakes real KaTeX-rendered HTML into exported formula elements, instead of leaving an empty placeholder — for consumers (e.g. a read-only preview) that display this HTML directly without also running KaTeX against it themselves. Off by default; no change for existing consumers.
6
+
7
+ ## 1.0.0-beta.1
8
+
9
+ **BREAKING CHANGE.** This is the first release built on the canonical, schema-driven document model. Every published version through 0.2.1 was built on the older discriminated-union model; none of the work below has shipped before.
10
+
11
+ - Introduce the canonical document model and editing engine (`smartrte-core/foundation`): schema-driven nodes with stable IDs, a scope/position API, structural history, and dedicated engines for lists, marks, blocks, tables, and atoms (images, formulas, media).
12
+ - Add a clipboard pipeline with source-aware normalization (Word, Google Docs, spreadsheets, Markdown, native) and a sanitize-first security boundary.
13
+ - Add DOCX, PDF, and Markdown format codecs with an explicit, tested `FeatureFormatCodec` fidelity contract (`full`/`semantic`/`lossy`/`unsupported`) per feature and format — no fidelity claim ships without a passing round-trip fixture.
14
+ - Add live KaTeX rendering for formula atoms (real HTML + MathML output, `trust:false`, `strict:"error"`), replacing plain-text LaTeX source display.
15
+ - Make the package genuinely framework-agnostic: no DOM-library dependency in the format codecs (parse5/`@xmldom/xmldom` replace real-DOM usage), zero React imports anywhere in `smartrte-core`.
16
+ - **Breaking:** the package root (`.`) no longer re-exports the legacy discriminated-union model, its plugin system, or the HTML/Markdown compatibility layers. That surface is unchanged and still fully available at `smartrte-core/legacy`; only the accidental root-level duplication was removed.
17
+ - **Breaking:** `smartrte-core/foundation` now exports `FOUNDATION_SMART_LIST_PRESETS` and related list-preset helpers (previously unreachable from the package's public exports).
18
+
3
19
  ## 0.2.1
4
20
 
5
21
  - Keep superscript and subscript mutually exclusive when converting selected text.
package/README.md CHANGED
@@ -3,3 +3,26 @@
3
3
  Framework-agnostic document model, selection, transaction, history, and
4
4
  normalization primitives. This package is not yet wired into the published
5
5
  React editor; it establishes the compatibility-safe migration boundary.
6
+
7
+ ## Foundation contract
8
+
9
+ The frozen Phase 1/C0 kernel is available from the dedicated subpath:
10
+
11
+ ```ts
12
+ import {
13
+ createFoundationEditor,
14
+ foundationSchema,
15
+ resolveScope,
16
+ type ResolvedPos,
17
+ type SmartTransaction,
18
+ } from "smartrte-core/foundation";
19
+ ```
20
+
21
+ The package root remains the compatibility API for the current React adapter.
22
+ New kernel and Phase 2 resolver work must use `smartrte-core/foundation`; do not
23
+ extend the legacy raw-path contracts. `pnpm run lint:foundation-boundary`
24
+ enforces that boundary. Root promotion is an explicit Phase 3 list-migration
25
+ exit criterion. See `docs/FOUNDATION_CONTRACT.md` in the repository for the
26
+ locked decisions and `docs/SEMANTIC_SELECTION_CONTRACT.md` for Phase 2.
27
+ The persistent apply, incremental index, renderer, and browser-input contracts
28
+ are recorded in `docs/CANONICAL_EDITING_SURFACE.md`.
package/dist/command.d.ts CHANGED
@@ -1,13 +1,13 @@
1
- import type { SmartDocument } from "./model.js";
2
- import type { SmartSelection } from "./selection.js";
3
- import type { SmartTransaction } from "./transaction.js";
4
- export interface CommandContext {
5
- document: SmartDocument;
6
- selection: SmartSelection;
1
+ import type { LegacySmartDocument } from "./model.js";
2
+ import type { LegacySmartSelection } from "./selection.js";
3
+ import type { LegacySmartTransaction } from "./transaction.js";
4
+ export interface LegacyCommandContext {
5
+ document: LegacySmartDocument;
6
+ selection: LegacySmartSelection;
7
7
  now?: () => number;
8
8
  }
9
9
  export interface SmartCommand<Input = unknown> {
10
10
  id: string;
11
- isEnabled(context: CommandContext, input?: Input): boolean;
12
- execute(context: CommandContext, input?: Input): SmartTransaction;
11
+ isEnabled(context: LegacyCommandContext, input?: Input): boolean;
12
+ execute(context: LegacyCommandContext, input?: Input): LegacySmartTransaction;
13
13
  }
@@ -0,0 +1,32 @@
1
+ import { type SmartHistoryState } from "./history.js";
2
+ import { type SmartRtePlugin } from "./plugin.js";
3
+ import { type SmartEditorState, type LegacySmartTransaction } from "./transaction.js";
4
+ export interface CreateSmartEditorOptions {
5
+ state: SmartEditorState;
6
+ plugins?: SmartRtePlugin[];
7
+ historyLimit?: number;
8
+ readOnly?: boolean;
9
+ }
10
+ export type SmartEditorListener = (state: SmartEditorState, transaction: LegacySmartTransaction) => void;
11
+ export declare class SmartEditor {
12
+ private currentState;
13
+ private currentHistory;
14
+ private readonly plugins;
15
+ private readonly schema;
16
+ private readonly commands;
17
+ private readonly listeners;
18
+ private readOnly;
19
+ constructor(options: CreateSmartEditorOptions);
20
+ get state(): SmartEditorState;
21
+ get history(): SmartHistoryState;
22
+ get pluginIds(): string[];
23
+ setReadOnly(readOnly: boolean): void;
24
+ canExecute<Input>(id: string, input?: Input): boolean;
25
+ execute<Input>(id: string, input?: Input): boolean;
26
+ dispatch(transaction: LegacySmartTransaction): boolean;
27
+ undo(): boolean;
28
+ redo(): boolean;
29
+ subscribe(listener: SmartEditorListener): () => void;
30
+ private withPluginNormalization;
31
+ }
32
+ export declare const createSmartEditor: (options: CreateSmartEditorOptions) => SmartEditor;
package/dist/editor.js ADDED
@@ -0,0 +1,121 @@
1
+ import { applyTransactionWithHistory, createHistoryState, redoHistory, undoHistory, } from "./history.js";
2
+ import { createSmartSchema, normalizeSmartDocument, validateSmartDocument, } from "./schema.js";
3
+ import { orderPlugins } from "./plugin.js";
4
+ import { applyTransaction } from "./transaction.js";
5
+ const sameDocument = (left, right) => JSON.stringify(left) === JSON.stringify(right);
6
+ export class SmartEditor {
7
+ constructor(options) {
8
+ this.commands = new Map();
9
+ this.listeners = new Set();
10
+ this.plugins = orderPlugins(options.plugins || []);
11
+ this.schema = createSmartSchema(this.plugins
12
+ .filter((plugin) => plugin.schema)
13
+ .map((plugin) => ({ id: plugin.id, ...plugin.schema })));
14
+ const document = normalizeSmartDocument(options.state.document, this.schema);
15
+ const validation = validateSmartDocument(document, this.schema);
16
+ if (!validation.valid)
17
+ throw new Error(`Initial document is invalid: ${validation.issues[0]?.message}`);
18
+ this.currentState = { document, selection: options.state.selection };
19
+ this.currentHistory = createHistoryState(options.historyLimit);
20
+ this.readOnly = options.readOnly ?? false;
21
+ this.plugins.forEach((plugin) => {
22
+ Object.entries(plugin.commands || {}).forEach(([id, command]) => {
23
+ if (this.commands.has(id))
24
+ throw new Error(`Duplicate command id "${id}".`);
25
+ this.commands.set(id, command);
26
+ });
27
+ });
28
+ }
29
+ get state() {
30
+ return this.currentState;
31
+ }
32
+ get history() {
33
+ return this.currentHistory;
34
+ }
35
+ get pluginIds() {
36
+ return this.plugins.map((plugin) => plugin.id);
37
+ }
38
+ setReadOnly(readOnly) {
39
+ this.readOnly = readOnly;
40
+ }
41
+ canExecute(id, input) {
42
+ if (this.readOnly)
43
+ return false;
44
+ const command = this.commands.get(id);
45
+ return Boolean(command?.isEnabled(this.currentState, input));
46
+ }
47
+ execute(id, input) {
48
+ if (!this.canExecute(id, input))
49
+ return false;
50
+ const command = this.commands.get(id);
51
+ return this.dispatch(command.execute(this.currentState, input));
52
+ }
53
+ dispatch(transaction) {
54
+ if (this.readOnly && transaction.source === "user")
55
+ return false;
56
+ const prepared = this.withPluginNormalization(transaction);
57
+ const result = applyTransactionWithHistory(this.currentState, this.currentHistory, prepared);
58
+ this.currentState = result.state;
59
+ this.currentHistory = result.history;
60
+ this.plugins.forEach((plugin) => plugin.onTransaction?.(prepared));
61
+ this.listeners.forEach((listener) => listener(this.currentState, prepared));
62
+ return true;
63
+ }
64
+ undo() {
65
+ if (this.readOnly)
66
+ return false;
67
+ const result = undoHistory(this.currentState, this.currentHistory);
68
+ if (!result.applied)
69
+ return false;
70
+ this.currentState = result.state;
71
+ this.currentHistory = result.history;
72
+ const transaction = result.history.redo[result.history.redo.length - 1]?.inverse;
73
+ if (transaction)
74
+ this.listeners.forEach((listener) => listener(this.currentState, transaction));
75
+ return true;
76
+ }
77
+ redo() {
78
+ if (this.readOnly)
79
+ return false;
80
+ const result = redoHistory(this.currentState, this.currentHistory);
81
+ if (!result.applied)
82
+ return false;
83
+ this.currentState = result.state;
84
+ this.currentHistory = result.history;
85
+ const transaction = result.history.undo[result.history.undo.length - 1]?.forward;
86
+ if (transaction)
87
+ this.listeners.forEach((listener) => listener(this.currentState, transaction));
88
+ return true;
89
+ }
90
+ subscribe(listener) {
91
+ this.listeners.add(listener);
92
+ return () => this.listeners.delete(listener);
93
+ }
94
+ withPluginNormalization(transaction) {
95
+ const base = applyTransaction(this.currentState, transaction);
96
+ let document = base.document;
97
+ for (let pass = 0; pass < 10; pass += 1) {
98
+ const beforePass = document;
99
+ document = normalizeSmartDocument(document, this.schema);
100
+ this.plugins.forEach((plugin) => {
101
+ (plugin.normalizers || []).forEach((normalizer) => {
102
+ document = normalizeSmartDocument(normalizer(document), this.schema);
103
+ });
104
+ });
105
+ if (sameDocument(document, beforePass))
106
+ break;
107
+ if (pass === 9)
108
+ throw new Error("Plugin normalization did not reach a fixed point.");
109
+ }
110
+ const validation = validateSmartDocument(document, this.schema);
111
+ if (!validation.valid)
112
+ throw new Error(`Plugin normalization produced an invalid document: ${validation.issues[0]?.message}`);
113
+ if (sameDocument(document, base.document))
114
+ return transaction;
115
+ return {
116
+ ...transaction,
117
+ operations: [...transaction.operations, { type: "replaceNode", path: [], node: document }],
118
+ };
119
+ }
120
+ }
121
+ export const createSmartEditor = (options) => new SmartEditor(options);
@@ -0,0 +1,2 @@
1
+ export * from "./types.js";
2
+ export * from "./range.js";
@@ -0,0 +1,2 @@
1
+ export * from "./types.js";
2
+ export * from "./range.js";
@@ -0,0 +1,54 @@
1
+ import type { SmartDocument, SmartOperation, SmartRange } from "../types.js";
2
+ import type { PositionLookup } from "../scope/types.js";
3
+ import type { AnnotationRange, MergeOrphanPolicy } from "./types.js";
4
+ /**
5
+ * Resolves an AnnotationRange against the current document. For every
6
+ * operation type except splitNode/mergeNode, node identity survives
7
+ * unchanged (move: the id follows the node; insert/remove/text/marks: never
8
+ * touch node ids; setNodeType: id explicitly preserved), so this alone is
9
+ * enough to track a range through most edits - PositionLookup's `positionOf`
10
+ * resolves it fresh each call. Returns null once an anchor id no longer
11
+ * exists (removed, or retired by a merge with no rebase applied).
12
+ */
13
+ export declare const resolveAnnotationRange: (range: AnnotationRange, positions: PositionLookup) => SmartRange | null;
14
+ /**
15
+ * Rebases an AnnotationRange through a single operation. Every operation
16
+ * type other than split/merge/an-explicitly-marked-merge-shaped-removeNode-
17
+ * or-replaceNode is a no-op here - node identity already survives them, so
18
+ * the range's stored ids stay valid and `resolveAnnotationRange` picks up
19
+ * the new position on its own next call. `positions` must resolve against
20
+ * the document as it existed immediately before `operation` is applied.
21
+ */
22
+ export declare const rebaseAnnotationRange: (range: AnnotationRange, operation: SmartOperation, positions: PositionLookup, onOrphan: MergeOrphanPolicy) => AnnotationRange | null;
23
+ /**
24
+ * Folds `rebaseAnnotationRange` over a sequence of operations, e.g. every
25
+ * operation in one transaction, in order. Each step's `positions` must
26
+ * resolve against the document as it existed immediately before that step's
27
+ * operation - the caller (which is already applying the operations to
28
+ * advance the document) is in the natural position to supply this.
29
+ */
30
+ export declare const rebaseAnnotationRangeThroughOperations: (range: AnnotationRange, steps: readonly {
31
+ readonly operation: SmartOperation;
32
+ readonly positions: PositionLookup;
33
+ }[], onOrphan: MergeOrphanPolicy) => AnnotationRange | null;
34
+ /**
35
+ * Reference `MergeOrphanPolicy`: snap to the surviving node rather than
36
+ * drop (Phase 12a's decision - a comment on content that got merged into
37
+ * something else is still relevant to that content's new home; silently
38
+ * vanishing it loses data with no recovery path). Only applicable to a
39
+ * real `mergeNode` operation (`input.ts`'s cross-block Backspace/Delete
40
+ * text-merge) - unlike `mergedInto`/`retiredInto` above, `mergeNode`'s own
41
+ * shape has no explicit survivor field, so deriving it needs the actual
42
+ * document tree (the sibling immediately preceding the merge point,
43
+ * matching how `input.ts`'s `queueRangeDeletion` constructs the
44
+ * operation: `retiredId` is the right/consumed block, the left/absorbing
45
+ * block sits one offset earlier at the same parent). `positions` alone
46
+ * (an id-keyed lookup) can't answer "what's at this path" in the reverse
47
+ * direction, so this needs `document` explicitly, unlike every other
48
+ * function in this file.
49
+ *
50
+ * Table-cell-merge and list-item-merge don't need this - they always
51
+ * populate `mergedInto`/`retiredInto` themselves, so `rebaseAnnotationRange`
52
+ * snaps them unconditionally with no caller policy involved at all.
53
+ */
54
+ export declare const defaultMergeOrphanPolicy: (document: SmartDocument) => MergeOrphanPolicy;
@@ -0,0 +1,158 @@
1
+ import { isTextNode } from "../identity.js";
2
+ import { nodeAtPath } from "../positions.js";
3
+ const samePath = (a, b) => a.length === b.length && a.every((value, index) => value === b[index]);
4
+ const resolveEndpoint = (id, offset, side, positions) => {
5
+ if (offset === undefined) {
6
+ const outer = positions.rangeOf(id);
7
+ return outer ? outer[side] : null;
8
+ }
9
+ const resolved = positions.positionOf(id);
10
+ if (!resolved)
11
+ return null;
12
+ return { path: [...resolved.pos.path, resolved.pos.offset], offset };
13
+ };
14
+ /**
15
+ * Resolves an AnnotationRange against the current document. For every
16
+ * operation type except splitNode/mergeNode, node identity survives
17
+ * unchanged (move: the id follows the node; insert/remove/text/marks: never
18
+ * touch node ids; setNodeType: id explicitly preserved), so this alone is
19
+ * enough to track a range through most edits - PositionLookup's `positionOf`
20
+ * resolves it fresh each call. Returns null once an anchor id no longer
21
+ * exists (removed, or retired by a merge with no rebase applied).
22
+ */
23
+ export const resolveAnnotationRange = (range, positions) => {
24
+ const from = resolveEndpoint(range.startId, range.startOffset, "from", positions);
25
+ const to = resolveEndpoint(range.endId, range.endOffset, "to", positions);
26
+ return from && to ? { from, to } : null;
27
+ };
28
+ const splitTargetPath = (operation) => operation.pos.path.slice(0, operation.pos.path.length - operation.depth);
29
+ /**
30
+ * Split keeps the pre-split id on the left half and mints `newId` for the
31
+ * right half. An endpoint whose offset lands at or past the split point
32
+ * moved into the right half - its id must be rewritten, or it would keep
33
+ * resolving (via `positionOf`) to the left half's now-unrelated content,
34
+ * silently pointing at the wrong node instead of failing loudly. An
35
+ * endpoint anchored to a different node entirely, or one whose offset is
36
+ * undefined (whole-node anchor, not yet split-aware), passes through
37
+ * unchanged.
38
+ */
39
+ const rebaseEndpointThroughSplit = (id, offset, operation, positions) => {
40
+ if (offset === undefined)
41
+ return { id, offset };
42
+ const resolved = positions.positionOf(id);
43
+ if (!resolved)
44
+ return { id, offset };
45
+ const path = [...resolved.pos.path, resolved.pos.offset];
46
+ if (!samePath(path, splitTargetPath(operation)))
47
+ return { id, offset };
48
+ if (offset < operation.pos.offset)
49
+ return { id, offset };
50
+ return { id: operation.newId, offset: offset - operation.pos.offset };
51
+ };
52
+ const rebaseThroughSplit = (range, operation, positions) => {
53
+ const start = rebaseEndpointThroughSplit(range.startId, range.startOffset, operation, positions);
54
+ const end = rebaseEndpointThroughSplit(range.endId, range.endOffset, operation, positions);
55
+ return { startId: start.id, startOffset: start.offset, endId: end.id, endOffset: end.offset };
56
+ };
57
+ const rebaseThroughMerge = (range, operation, positions, onOrphan) => {
58
+ if (range.startId !== operation.retiredId && range.endId !== operation.retiredId)
59
+ return range;
60
+ return onOrphan(range, operation, positions);
61
+ };
62
+ /**
63
+ * An endpoint anchored to a retired id snaps to its survivor's whole-node
64
+ * span (offset cleared, not remapped) - the survivor is a different node
65
+ * whose content was assembled by concatenating multiple sources in an
66
+ * order this generic mechanism has no way to know (e.g. `mergeItems`'
67
+ * `mergeOwners` combines two paragraphs' text in an order that depends on
68
+ * merge direction), so a stale offset into the retired node's own text
69
+ * cannot be reliably remapped into the merged text - a wrong-but-
70
+ * plausible-looking offset would be worse than an honest whole-node
71
+ * anchor. Unlike the mergeNode case above, there is no caller-supplied
72
+ * policy here: `mergedInto`/`retiredInto` are opt-in fields a command
73
+ * populates only when it knows exactly where the content went, so
74
+ * snapping is the unambiguous, mechanical consequence of the field's
75
+ * presence, not a product decision requiring caller input - the actual
76
+ * product decision already happened at the command layer, by choosing to
77
+ * populate the field at all instead of leaving the operation as a plain,
78
+ * policy-free removeNode/replaceNode.
79
+ */
80
+ const snapEndpoint = (id, offset, retiredId, survivorId) => id === retiredId ? { id: survivorId } : { id, offset };
81
+ // Preserves reference identity when neither endpoint actually matches
82
+ // retiredId - a consumer rebasing many ranges through the same operation
83
+ // (see comments/rebase.ts) relies on `===` to cheaply tell which ones
84
+ // actually changed, e.g. to avoid re-persisting or re-rendering untouched
85
+ // threads.
86
+ const rebaseThroughMergedRemove = (range, retiredId, survivorId) => {
87
+ if (range.startId !== retiredId && range.endId !== retiredId)
88
+ return range;
89
+ const start = snapEndpoint(range.startId, range.startOffset, retiredId, survivorId);
90
+ const end = snapEndpoint(range.endId, range.endOffset, retiredId, survivorId);
91
+ return { startId: start.id, startOffset: start.offset, endId: end.id, endOffset: end.offset };
92
+ };
93
+ const rebaseThroughRetiredReplace = (range, retirements) => retirements.reduce((current, { retiredId, survivorId }) => rebaseThroughMergedRemove(current, retiredId, survivorId), range);
94
+ /**
95
+ * Rebases an AnnotationRange through a single operation. Every operation
96
+ * type other than split/merge/an-explicitly-marked-merge-shaped-removeNode-
97
+ * or-replaceNode is a no-op here - node identity already survives them, so
98
+ * the range's stored ids stay valid and `resolveAnnotationRange` picks up
99
+ * the new position on its own next call. `positions` must resolve against
100
+ * the document as it existed immediately before `operation` is applied.
101
+ */
102
+ export const rebaseAnnotationRange = (range, operation, positions, onOrphan) => {
103
+ if (operation.type === "splitNode")
104
+ return rebaseThroughSplit(range, operation, positions);
105
+ if (operation.type === "mergeNode")
106
+ return rebaseThroughMerge(range, operation, positions, onOrphan);
107
+ if (operation.type === "removeNode" && operation.mergedInto && !isTextNode(operation.node)) {
108
+ return rebaseThroughMergedRemove(range, operation.node.id, operation.mergedInto);
109
+ }
110
+ if (operation.type === "replaceNode" && operation.retiredInto?.length)
111
+ return rebaseThroughRetiredReplace(range, operation.retiredInto);
112
+ return range;
113
+ };
114
+ /**
115
+ * Folds `rebaseAnnotationRange` over a sequence of operations, e.g. every
116
+ * operation in one transaction, in order. Each step's `positions` must
117
+ * resolve against the document as it existed immediately before that step's
118
+ * operation - the caller (which is already applying the operations to
119
+ * advance the document) is in the natural position to supply this.
120
+ */
121
+ export const rebaseAnnotationRangeThroughOperations = (range, steps, onOrphan) => {
122
+ let current = range;
123
+ for (const step of steps) {
124
+ if (!current)
125
+ return null;
126
+ current = rebaseAnnotationRange(current, step.operation, step.positions, onOrphan);
127
+ }
128
+ return current;
129
+ };
130
+ /**
131
+ * Reference `MergeOrphanPolicy`: snap to the surviving node rather than
132
+ * drop (Phase 12a's decision - a comment on content that got merged into
133
+ * something else is still relevant to that content's new home; silently
134
+ * vanishing it loses data with no recovery path). Only applicable to a
135
+ * real `mergeNode` operation (`input.ts`'s cross-block Backspace/Delete
136
+ * text-merge) - unlike `mergedInto`/`retiredInto` above, `mergeNode`'s own
137
+ * shape has no explicit survivor field, so deriving it needs the actual
138
+ * document tree (the sibling immediately preceding the merge point,
139
+ * matching how `input.ts`'s `queueRangeDeletion` constructs the
140
+ * operation: `retiredId` is the right/consumed block, the left/absorbing
141
+ * block sits one offset earlier at the same parent). `positions` alone
142
+ * (an id-keyed lookup) can't answer "what's at this path" in the reverse
143
+ * direction, so this needs `document` explicitly, unlike every other
144
+ * function in this file.
145
+ *
146
+ * Table-cell-merge and list-item-merge don't need this - they always
147
+ * populate `mergedInto`/`retiredInto` themselves, so `rebaseAnnotationRange`
148
+ * snaps them unconditionally with no caller policy involved at all.
149
+ */
150
+ export const defaultMergeOrphanPolicy = (document) => (range, operation) => {
151
+ const parent = nodeAtPath(document, operation.pos.path);
152
+ if (!parent || isTextNode(parent) || !parent.children)
153
+ return null;
154
+ const survivor = parent.children[operation.pos.offset - 1];
155
+ if (!survivor || isTextNode(survivor))
156
+ return null;
157
+ return rebaseThroughMergedRemove(range, operation.retiredId, survivor.id);
158
+ };
@@ -0,0 +1,33 @@
1
+ import type { SmartOperation } from "../types.js";
2
+ import type { PositionLookup } from "../scope/types.js";
3
+ /**
4
+ * A range anchored to node ids rather than a path/offset, so it survives
5
+ * structural edits elsewhere in the document without being carried through
6
+ * every transaction. Resolve it on demand via `resolveAnnotationRange`.
7
+ *
8
+ * An absent offset anchors to the whole node (its outer span) rather than a
9
+ * text position inside it. There is deliberately no `kind` discriminant yet
10
+ * (text range vs. whole-node anchor) - that is left for whichever phase
11
+ * first needs to distinguish them, so this primitive does not have to be
12
+ * retrofitted once real consumers exist.
13
+ */
14
+ export interface AnnotationRange {
15
+ readonly startId: string;
16
+ readonly startOffset?: number;
17
+ readonly endId: string;
18
+ readonly endOffset?: number;
19
+ }
20
+ export type SplitNodeOperation = Extract<SmartOperation, {
21
+ type: "splitNode";
22
+ }>;
23
+ export type MergeNodeOperation = Extract<SmartOperation, {
24
+ type: "mergeNode";
25
+ }>;
26
+ /**
27
+ * Called when a merge retires a node id an AnnotationRange endpoint was
28
+ * anchored to. foundation/ has no opinion on whether the range should be
29
+ * dropped, snapped into the surviving node, or something else - comments and
30
+ * suggestions plausibly want different answers - so that decision is left
31
+ * entirely to the caller rather than baked into the primitive.
32
+ */
33
+ export type MergeOrphanPolicy = (range: AnnotationRange, operation: MergeNodeOperation, positions: PositionLookup) => AnnotationRange | null;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import type { AtomCommand, InsertAtomParams, ResizeAtomParams, UpdateAtomParams } from "./types.js";
2
+ export declare const insertAtom: AtomCommand<InsertAtomParams>;
3
+ export declare const updateAtom: AtomCommand<UpdateAtomParams>;
4
+ export declare const deleteAtom: AtomCommand<Record<string, never>>;
5
+ export declare const resizeAtom: AtomCommand<ResizeAtomParams>;
6
+ export declare const atomCommands: {
7
+ readonly "atom.insert": AtomCommand<InsertAtomParams>;
8
+ readonly "atom.update": AtomCommand<UpdateAtomParams>;
9
+ readonly "atom.delete": AtomCommand<Record<string, never>>;
10
+ readonly "atom.resize": AtomCommand<ResizeAtomParams>;
11
+ };
@@ -0,0 +1,118 @@
1
+ import { isTextNode } from "../identity.js";
2
+ import { atomDeclarations } from "./declarations.js";
3
+ const locate = (document, id, ctx) => {
4
+ const resolved = ctx.positions.positionOf(id);
5
+ if (!resolved)
6
+ return null;
7
+ if (resolved.kind === "inline") {
8
+ let inlineOffset = 0;
9
+ for (let index = 0; index < (resolved.parent.children?.length || 0); index += 1) {
10
+ const child = resolved.parent.children[index];
11
+ if (!isTextNode(child) && child.id === id && inlineOffset === resolved.pos.offset) {
12
+ return { node: child, pos: { path: [...resolved.pos.path], offset: index } };
13
+ }
14
+ inlineOffset += isTextNode(child) ? child.text.length : 1;
15
+ }
16
+ return null;
17
+ }
18
+ const node = resolved.parent.children?.[resolved.pos.offset];
19
+ if (!node || isTextNode(node) || node.id !== id)
20
+ return null;
21
+ return { node, pos: resolved.pos };
22
+ };
23
+ const cleanAttrs = (attrs) => Object.fromEntries(Object.entries(attrs).filter(([, value]) => value !== undefined));
24
+ const inlineChildrenWithAtom = (owner, offset, atom) => {
25
+ if (!Number.isInteger(offset) || offset < 0)
26
+ return null;
27
+ const output = [];
28
+ let cursor = 0;
29
+ let inserted = false;
30
+ for (const child of owner.children || []) {
31
+ const width = isTextNode(child) ? child.text.length : 1;
32
+ if (!inserted && offset >= cursor && offset <= cursor + width) {
33
+ if (isTextNode(child)) {
34
+ const local = offset - cursor;
35
+ if (local)
36
+ output.push({ ...child, text: child.text.slice(0, local) });
37
+ output.push(atom);
38
+ if (local < child.text.length)
39
+ output.push({ ...child, text: child.text.slice(local) });
40
+ }
41
+ else {
42
+ if (offset === cursor)
43
+ output.push(atom, child);
44
+ else if (offset === cursor + 1)
45
+ output.push(child, atom);
46
+ else
47
+ return null;
48
+ }
49
+ inserted = true;
50
+ }
51
+ else
52
+ output.push(child);
53
+ cursor += width;
54
+ }
55
+ if (!inserted && offset === cursor)
56
+ output.push(atom);
57
+ return inserted || offset === cursor ? output : null;
58
+ };
59
+ export const insertAtom = (document, _scope, params, ctx) => {
60
+ const attrs = cleanAttrs(params.attrs);
61
+ if (!params.declaration.validate(attrs))
62
+ return [];
63
+ const node = { type: params.declaration.type, id: params.nodeId, attrs };
64
+ if (params.declaration.group === "inline") {
65
+ if (!params.ownerId || params.offset === undefined)
66
+ return [];
67
+ const owner = locate(document, params.ownerId, ctx);
68
+ if (!owner)
69
+ return [];
70
+ const children = inlineChildrenWithAtom(owner.node, params.offset, node);
71
+ return children ? [{ type: "replaceNode", pos: owner.pos, before: owner.node, after: { ...owner.node, children } }] : [];
72
+ }
73
+ if (!params.parentId || params.index === undefined)
74
+ return [];
75
+ const range = ctx.positions.contentRangeOf(params.parentId);
76
+ const parentPosition = ctx.positions.positionOf(params.parentId);
77
+ const parent = parentPosition && parentPosition.parent.id === params.parentId
78
+ ? parentPosition.parent
79
+ : parentPosition?.parent.children?.[parentPosition.pos.offset];
80
+ if (!range || !parent || isTextNode(parent) || params.index < 0 || params.index > (parent.children?.length || 0))
81
+ return [];
82
+ return [{ type: "insertNode", pos: { path: [...range.from.path], offset: params.index }, node }];
83
+ };
84
+ const atomIds = (scope) => scope.kind === "atomic-node" ? [scope.nodeId]
85
+ : scope.kind === "mixed" ? scope.parts.flatMap(atomIds) : [];
86
+ export const updateAtom = (document, scope, params, ctx) => atomIds(scope).flatMap((id) => {
87
+ const located = locate(document, id, ctx);
88
+ if (!located || ctx.schema.nodes[located.node.type]?.atomic !== true)
89
+ return [];
90
+ const after = cleanAttrs({ ...(located.node.attrs || {}), ...params.attrs });
91
+ const declaration = atomDeclarations.find((entry) => entry.type === located.node.type);
92
+ if (declaration && !declaration.validate(after))
93
+ return [];
94
+ return [{ type: "setNodeAttributes", pos: { path: [...located.pos.path, located.pos.offset], offset: 0 }, before: located.node.attrs || {}, after }];
95
+ });
96
+ export const deleteAtom = (document, scope, _params, ctx) => atomIds(scope).flatMap((id) => {
97
+ const located = locate(document, id, ctx);
98
+ return located && ctx.schema.nodes[located.node.type]?.atomic === true
99
+ ? [{ type: "removeNode", pos: located.pos, node: located.node }] : [];
100
+ });
101
+ export const resizeAtom = (document, scope, params, ctx) => {
102
+ const id = atomIds(scope)[0];
103
+ const located = id ? locate(document, id, ctx) : null;
104
+ if (!located)
105
+ return [];
106
+ const beforeWidth = Number(located.node.attrs?.width) || params.width;
107
+ const beforeHeight = Number(located.node.attrs?.height) || params.height;
108
+ const minWidth = Math.max(1, params.minWidth || 16);
109
+ const minHeight = Math.max(1, params.minHeight || 16);
110
+ let width = Math.max(minWidth, params.width);
111
+ let height = Math.max(minHeight, params.height);
112
+ if (params.preserveAspectRatio && beforeWidth > 0 && beforeHeight > 0)
113
+ height = width * beforeHeight / beforeWidth;
114
+ if (![width, height].every((value) => Number.isFinite(value) && value <= 100000))
115
+ return [];
116
+ return updateAtom(document, scope, { attrs: { width, height } }, ctx);
117
+ };
118
+ export const atomCommands = { "atom.insert": insertAtom, "atom.update": updateAtom, "atom.delete": deleteAtom, "atom.resize": resizeAtom };
@@ -0,0 +1,8 @@
1
+ import type { SmartElementNode } from "../types.js";
2
+ import type { CompositionToken } from "./types.js";
3
+ /** Mapping-aware tokens: atoms occupy one opaque unit and are never flattened. */
4
+ export declare const tokenizeCompositionOwner: (owner: SmartElementNode) => CompositionToken[];
5
+ export declare const compositionSegmentAt: (tokens: readonly CompositionToken[], offset: number) => {
6
+ from: number;
7
+ to: number;
8
+ };
@@ -0,0 +1,22 @@
1
+ import { isTextNode } from "../identity.js";
2
+ /** Mapping-aware tokens: atoms occupy one opaque unit and are never flattened. */
3
+ export const tokenizeCompositionOwner = (owner) => (owner.children || []).map((child) => isTextNode(child)
4
+ ? { kind: "text", text: child.text, marks: [...(child.marks || [])] }
5
+ : { kind: "atom", nodeId: child.id, atomType: child.type });
6
+ export const compositionSegmentAt = (tokens, offset) => {
7
+ let cursor = 0;
8
+ for (const token of tokens) {
9
+ const width = token.kind === "atom" ? 1 : token.text.length;
10
+ if (token.kind === "atom" && (offset === cursor || offset === cursor + 1))
11
+ return { from: offset, to: offset };
12
+ cursor += width;
13
+ }
14
+ cursor = 0;
15
+ for (const token of tokens) {
16
+ const width = token.kind === "atom" ? 1 : token.text.length;
17
+ if (token.kind === "text" && offset >= cursor && offset <= cursor + width)
18
+ return { from: cursor, to: cursor + width };
19
+ cursor += width;
20
+ }
21
+ return { from: cursor, to: cursor };
22
+ };
@@ -0,0 +1,2 @@
1
+ import type { AtomDeclaration } from "./types.js";
2
+ export declare const atomDeclarations: readonly AtomDeclaration[];