@voithos-labs/aragonite 0.10.3 → 0.10.7

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 (1021) hide show
  1. package/README.md +2 -2
  2. package/dist/a11y-strings.d.ts +12 -5
  3. package/dist/a11y-strings.js +61 -5
  4. package/dist/action-contracts.d.ts +242 -155
  5. package/dist/action-contracts.js +2 -2
  6. package/dist/active-editor.d.ts +3 -3
  7. package/dist/active-editor.js +8 -9
  8. package/dist/ambient/ambient-dom.d.ts +2 -5
  9. package/dist/ambient/ambient-dom.js +16 -62
  10. package/dist/assert.js +6 -5
  11. package/dist/block-component.d.ts +135 -109
  12. package/dist/block-component.js +30 -22
  13. package/dist/block-id.d.ts +6 -10
  14. package/dist/block-id.js +25 -14
  15. package/dist/bounded-memo.d.ts +5 -5
  16. package/dist/bounded-memo.js +6 -6
  17. package/dist/components/BlockDragHandle.svelte +13 -20
  18. package/dist/components/BlockHost.svelte +56 -134
  19. package/dist/components/BlockList.svelte +22 -19
  20. package/dist/components/DecorationOverlay.svelte +9 -10
  21. package/dist/components/DecorationOverlay.svelte.d.ts +2 -2
  22. package/dist/components/Editor.svelte +619 -1089
  23. package/dist/components/Editor.svelte.d.ts +9 -20
  24. package/dist/components/GapCaret.svelte +26 -59
  25. package/dist/components/SelectionOverlay.svelte +36 -67
  26. package/dist/components/SelectionOverlay.svelte.d.ts +6 -4
  27. package/dist/components/TailInsert.svelte +8 -55
  28. package/dist/components/TailInsert.svelte.d.ts +4 -6
  29. package/dist/components/block-content-selector.d.ts +11 -11
  30. package/dist/components/block-content-selector.js +11 -11
  31. package/dist/components/block-el-lookup.d.ts +6 -0
  32. package/dist/components/block-el-lookup.js +27 -0
  33. package/dist/components/blocks/BlockquoteBlock.svelte +3 -3
  34. package/dist/components/blocks/ThematicBreakBlock.svelte +22 -63
  35. package/dist/components/blocks/ThematicBreakBlock.svelte.d.ts +0 -2
  36. package/dist/components/blocks/code/CodeBlock.svelte +255 -276
  37. package/dist/components/blocks/code/CodeBlock.svelte.d.ts +2 -3
  38. package/dist/components/blocks/code/CodeBlockRail.svelte +103 -100
  39. package/dist/components/blocks/code/CodeBlockRail.svelte.d.ts +9 -4
  40. package/dist/components/blocks/code/code-beforeinput.d.ts +1 -1
  41. package/dist/components/blocks/code/code-bootstrap.d.ts +3 -5
  42. package/dist/components/blocks/code/code-bootstrap.js +35 -28
  43. package/dist/components/blocks/code/code-context-actions.js +5 -4
  44. package/dist/components/blocks/code/code-enter.js +2 -2
  45. package/dist/components/blocks/code/code-fence-boundary.d.ts +29 -46
  46. package/dist/components/blocks/code/code-fence-boundary.js +40 -66
  47. package/dist/components/blocks/code/code-fence-exit.d.ts +6 -11
  48. package/dist/components/blocks/code/code-fence-exit.js +24 -25
  49. package/dist/components/blocks/code/code-indent.d.ts +2 -2
  50. package/dist/components/blocks/code/code-indent.js +4 -7
  51. package/dist/components/blocks/code/code-languages.d.ts +16 -12
  52. package/dist/components/blocks/code/code-languages.js +58 -32
  53. package/dist/components/blocks/code/code-paste-surface.d.ts +3 -2
  54. package/dist/components/blocks/code/code-paste-surface.js +9 -18
  55. package/dist/components/blocks/code/code-renderer.d.ts +21 -5
  56. package/dist/components/blocks/code/code-renderer.js +90 -125
  57. package/dist/components/blocks/directive/DirectiveContainerBlock.svelte +5 -8
  58. package/dist/components/blocks/directive/activate-directives.d.ts +4 -4
  59. package/dist/components/blocks/directive/activate-directives.js +18 -13
  60. package/dist/components/blocks/editable-leaf.d.ts +50 -74
  61. package/dist/components/blocks/editable-leaf.js +144 -202
  62. package/dist/components/blocks/editable-surface.d.ts +96 -100
  63. package/dist/components/blocks/editable-surface.js +101 -109
  64. package/dist/components/blocks/list/ListBlock.svelte +46 -90
  65. package/dist/components/blocks/list/ListItemBlock.svelte +99 -144
  66. package/dist/components/blocks/list/ListItemBlock.svelte.d.ts +3 -0
  67. package/dist/components/blocks/list/task-checkbox.d.ts +5 -2
  68. package/dist/components/blocks/list/task-checkbox.js +12 -7
  69. package/dist/components/blocks/plain-text-backend.d.ts +4 -20
  70. package/dist/components/blocks/plain-text-backend.js +9 -43
  71. package/dist/components/blocks/surface-wiring.svelte.d.ts +8 -9
  72. package/dist/components/blocks/surface-wiring.svelte.js +15 -34
  73. package/dist/components/blocks/table/TableActionMenu.svelte +19 -18
  74. package/dist/components/blocks/table/TableActionMenu.svelte.d.ts +3 -1
  75. package/dist/components/blocks/table/TableBlock.svelte +100 -165
  76. package/dist/components/blocks/table/TableBlock.svelte.d.ts +3 -3
  77. package/dist/components/blocks/table/TableCellBlock.svelte +307 -385
  78. package/dist/components/blocks/table/TableCellBlock.svelte.d.ts +1 -0
  79. package/dist/components/blocks/table/TableRowBlock.svelte +60 -104
  80. package/dist/components/blocks/table/TableRowBlock.svelte.d.ts +0 -1
  81. package/dist/components/blocks/table/cell-keydown-plan.d.ts +4 -9
  82. package/dist/components/blocks/table/cell-keydown-plan.js +8 -12
  83. package/dist/components/blocks/table/cell-pointer.d.ts +18 -23
  84. package/dist/components/blocks/table/cell-pointer.js +49 -71
  85. package/dist/components/blocks/table/cell-render.d.ts +24 -29
  86. package/dist/components/blocks/table/cell-render.js +29 -31
  87. package/dist/components/blocks/table/selected-cells.d.ts +7 -7
  88. package/dist/components/blocks/table/selected-cells.js +9 -13
  89. package/dist/components/blocks/table/table-caret-at-point.d.ts +4 -4
  90. package/dist/components/blocks/table/table-caret-at-point.js +5 -5
  91. package/dist/components/blocks/table/table-cell-paste.d.ts +5 -11
  92. package/dist/components/blocks/table/table-cell-paste.js +27 -43
  93. package/dist/components/blocks/table/table-drag-hit-test.js +2 -1
  94. package/dist/components/blocks/table/table-menu-model.d.ts +4 -8
  95. package/dist/components/blocks/table/table-menu-model.js +6 -10
  96. package/dist/components/blocks/table/table-navigation.js +2 -2
  97. package/dist/components/blocks/text/TextEditableBlock.svelte +392 -310
  98. package/dist/components/blocks/text/TextEditableBlock.svelte.d.ts +2 -1
  99. package/dist/components/blocks/text/auto-pair-record.d.ts +23 -0
  100. package/dist/components/blocks/text/auto-pair-record.js +51 -0
  101. package/dist/components/blocks/text/click-snap-guard.d.ts +9 -4
  102. package/dist/components/blocks/text/click-snap-guard.js +16 -5
  103. package/dist/components/blocks/text/composition-seat.d.ts +17 -14
  104. package/dist/components/blocks/text/composition-seat.js +11 -11
  105. package/dist/components/blocks/text/construct-edge-delete.d.ts +20 -20
  106. package/dist/components/blocks/text/construct-edge-delete.js +50 -65
  107. package/dist/components/blocks/text/construct-reveal.d.ts +16 -21
  108. package/dist/components/blocks/text/construct-reveal.js +13 -14
  109. package/dist/components/blocks/text/delimiter-autopair.d.ts +51 -34
  110. package/dist/components/blocks/text/delimiter-autopair.js +87 -103
  111. package/dist/components/blocks/text/edge-policy-dispatch.d.ts +44 -46
  112. package/dist/components/blocks/text/edge-policy-dispatch.js +164 -180
  113. package/dist/components/blocks/text/edge-seat.d.ts +17 -26
  114. package/dist/components/blocks/text/edge-seat.js +43 -70
  115. package/dist/components/blocks/text/link-at-point.d.ts +9 -9
  116. package/dist/components/blocks/text/link-at-point.js +12 -15
  117. package/dist/components/blocks/text/live-join-seam.d.ts +6 -9
  118. package/dist/components/blocks/text/live-join-seam.js +75 -112
  119. package/dist/components/blocks/text/live-selection-edit.d.ts +14 -43
  120. package/dist/components/blocks/text/live-selection-edit.js +32 -105
  121. package/dist/components/blocks/text/live-split-rebalance.d.ts +9 -13
  122. package/dist/components/blocks/text/live-split-rebalance.js +50 -67
  123. package/dist/components/blocks/text/marker-completion.d.ts +7 -12
  124. package/dist/components/blocks/text/marker-completion.js +17 -10
  125. package/dist/components/blocks/text/pending-mark-insert.d.ts +11 -18
  126. package/dist/components/blocks/text/pending-mark-insert.js +30 -49
  127. package/dist/components/blocks/text/screen-diff.d.ts +5 -15
  128. package/dist/components/blocks/text/screen-diff.js +5 -19
  129. package/dist/components/blocks/text/text-clipboard.d.ts +23 -32
  130. package/dist/components/blocks/text/text-clipboard.js +27 -32
  131. package/dist/components/blocks/text/text-keydown.d.ts +20 -30
  132. package/dist/components/blocks/text/text-keydown.js +54 -51
  133. package/dist/components/blocks/text/text-render.d.ts +25 -35
  134. package/dist/components/blocks/text/text-render.js +82 -61
  135. package/dist/components/blocks/text/widget-adjacency.d.ts +9 -9
  136. package/dist/components/blocks/text/widget-adjacency.js +17 -17
  137. package/dist/components/blocks/text/widget-interaction.d.ts +67 -43
  138. package/dist/components/blocks/text/widget-interaction.js +243 -208
  139. package/dist/components/blocks/widget-portal.d.ts +20 -29
  140. package/dist/components/blocks/widget-portal.js +16 -14
  141. package/dist/components/built-in-blocks.d.ts +3 -4
  142. package/dist/components/built-in-blocks.js +17 -18
  143. package/dist/components/drag-handle.d.ts +13 -30
  144. package/dist/components/drag-handle.js +25 -63
  145. package/dist/components/editor-built-ins.d.ts +1 -0
  146. package/dist/components/editor-built-ins.js +16 -0
  147. package/dist/components/editor-root-clipboard.d.ts +6 -6
  148. package/dist/components/editor-root-clipboard.js +16 -27
  149. package/dist/components/editor-root-document-swap.d.ts +49 -0
  150. package/dist/components/editor-root-document-swap.js +54 -0
  151. package/dist/components/editor-root-focus.d.ts +5 -4
  152. package/dist/components/editor-root-focus.js +51 -22
  153. package/dist/components/editor-root-focused-surface.d.ts +35 -0
  154. package/dist/components/editor-root-focused-surface.js +65 -0
  155. package/dist/components/editor-root-geometry.d.ts +15 -24
  156. package/dist/components/editor-root-geometry.js +27 -31
  157. package/dist/components/editor-root-gestures.d.ts +39 -0
  158. package/dist/components/editor-root-gestures.js +189 -0
  159. package/dist/components/editor-root-keydown.d.ts +10 -25
  160. package/dist/components/editor-root-keydown.js +19 -43
  161. package/dist/components/editor-root-listeners.d.ts +22 -31
  162. package/dist/components/editor-root-listeners.js +49 -46
  163. package/dist/components/editor-root-menus.d.ts +51 -0
  164. package/dist/components/editor-root-menus.js +147 -0
  165. package/dist/components/editor-root-mode-flip.d.ts +16 -11
  166. package/dist/components/editor-root-mode-flip.js +36 -34
  167. package/dist/components/editor-root-scroll-host.d.ts +22 -0
  168. package/dist/components/editor-root-scroll-host.js +34 -0
  169. package/dist/components/editor-root-test-surface.d.ts +48 -0
  170. package/dist/components/editor-root-test-surface.js +6 -0
  171. package/dist/components/image/ImageOverlayHost.svelte +44 -35
  172. package/dist/components/image/ImageOverlayHost.svelte.d.ts +5 -6
  173. package/dist/components/image/ImageProperties.svelte +37 -41
  174. package/dist/components/image/ImageProperties.svelte.d.ts +9 -4
  175. package/dist/components/image/ImageResizeHandles.svelte +26 -30
  176. package/dist/components/image/image-crop.d.ts +5 -11
  177. package/dist/components/image/image-crop.js +5 -11
  178. package/dist/components/image/image-edit-commit.d.ts +19 -16
  179. package/dist/components/image/image-edit-commit.js +74 -38
  180. package/dist/components/image/image-resize.d.ts +2 -5
  181. package/dist/components/image/image-resize.js +3 -6
  182. package/dist/components/image/image-widget-editing.d.ts +3 -3
  183. package/dist/components/image/image-widget-editing.js +9 -9
  184. package/dist/components/image/widget-dom.d.ts +2 -3
  185. package/dist/components/image/widget-dom.js +19 -24
  186. package/dist/components/image/widget-selection-state.svelte.d.ts +10 -11
  187. package/dist/components/image/widget-selection-state.svelte.js +26 -19
  188. package/dist/components/kind-cue.svelte.d.ts +21 -0
  189. package/dist/components/kind-cue.svelte.js +35 -0
  190. package/dist/components/link-card/LinkCard.svelte +115 -64
  191. package/dist/components/link-card/LinkCard.svelte.d.ts +7 -5
  192. package/dist/components/link-card/LinkCardHost.svelte +42 -33
  193. package/dist/components/link-card/LinkCardHost.svelte.d.ts +10 -8
  194. package/dist/components/link-card/link-card-commit.d.ts +16 -19
  195. package/dist/components/link-card/link-card-commit.js +21 -30
  196. package/dist/components/link-card/link-card-entry.d.ts +13 -23
  197. package/dist/components/link-card/link-card-entry.js +16 -24
  198. package/dist/components/link-card/link-card-state.svelte.d.ts +17 -21
  199. package/dist/components/link-card/link-card-state.svelte.js +2 -2
  200. package/dist/components/lrd-map-gate.d.ts +4 -11
  201. package/dist/components/lrd-map-gate.js +5 -11
  202. package/dist/components/menu/BlockMenu.svelte +20 -81
  203. package/dist/components/menu/BlockMenu.svelte.d.ts +11 -9
  204. package/dist/components/menu/InlineMenuHost.svelte +191 -0
  205. package/dist/components/menu/InlineMenuHost.svelte.d.ts +13 -0
  206. package/dist/components/menu/MenuIcon.svelte +3 -103
  207. package/dist/components/menu/MenuIcon.svelte.d.ts +2 -44
  208. package/dist/components/menu/SelectionToolbar.svelte +380 -0
  209. package/dist/components/menu/SelectionToolbar.svelte.d.ts +14 -0
  210. package/dist/components/menu/clipboard-actions.d.ts +3 -4
  211. package/dist/components/menu/clipboard-actions.js +3 -4
  212. package/dist/components/menu/default-context-actions.d.ts +4 -13
  213. package/dist/components/menu/default-context-actions.js +49 -67
  214. package/dist/components/menu/flyout-placement.d.ts +2 -2
  215. package/dist/components/menu/flyout-placement.js +2 -2
  216. package/dist/components/menu/menu-presence.svelte.d.ts +12 -0
  217. package/dist/components/menu/menu-presence.svelte.js +19 -0
  218. package/dist/components/paste-image-arm.d.ts +6 -11
  219. package/dist/components/paste-image-arm.js +11 -18
  220. package/dist/components/portal.d.ts +2 -5
  221. package/dist/components/portal.js +4 -7
  222. package/dist/core/directive/activate.d.ts +4 -5
  223. package/dist/core/directive/activate.js +8 -9
  224. package/dist/core/directive/container-opener.js +13 -12
  225. package/dist/core/directive/grammar.d.ts +2 -5
  226. package/dist/core/directive/grammar.js +10 -10
  227. package/dist/core/directive/kinds.d.ts +4 -7
  228. package/dist/core/directive/kinds.js +14 -16
  229. package/dist/core/directive/registry.d.ts +12 -9
  230. package/dist/core/directive/registry.js +19 -20
  231. package/dist/core/directive/text-recognizer.d.ts +3 -2
  232. package/dist/core/directive/text-recognizer.js +5 -9
  233. package/dist/core/escapable.d.ts +1 -5
  234. package/dist/core/escapable.js +1 -5
  235. package/dist/core/inline/backticks.d.ts +3 -4
  236. package/dist/core/inline/backticks.js +3 -4
  237. package/dist/core/inline/character-refs.js +2 -1
  238. package/dist/core/inline/destination-bytes.d.ts +6 -3
  239. package/dist/core/inline/destination-bytes.js +50 -8
  240. package/dist/core/inline/entity-widget.d.ts +4 -3
  241. package/dist/core/inline/entity-widget.js +6 -6
  242. package/dist/core/inline/format-toggle.d.ts +22 -28
  243. package/dist/core/inline/format-toggle.js +97 -129
  244. package/dist/core/inline/html-entities.d.ts +2 -3
  245. package/dist/core/inline/html-entities.js +2 -3
  246. package/dist/core/inline/html-tag-grammar.js +6 -2
  247. package/dist/core/inline/image-dimensions.d.ts +2 -0
  248. package/dist/core/inline/image-dimensions.js +19 -2
  249. package/dist/core/inline/image-source-bytes.d.ts +11 -0
  250. package/dist/core/inline/image-source-bytes.js +71 -0
  251. package/dist/core/inline/index.d.ts +28 -21
  252. package/dist/core/inline/index.js +56 -34
  253. package/dist/core/inline/inline-cache.d.ts +12 -15
  254. package/dist/core/inline/inline-cache.js +11 -14
  255. package/dist/core/inline/inline-widgets.d.ts +55 -68
  256. package/dist/core/inline/inline-widgets.js +55 -58
  257. package/dist/core/inline/link-destination.d.ts +31 -0
  258. package/dist/core/inline/link-destination.js +129 -0
  259. package/dist/core/inline/link-reference-resolver.d.ts +8 -6
  260. package/dist/core/inline/link-reference-resolver.js +10 -6
  261. package/dist/core/inline/link-source-bytes.d.ts +31 -0
  262. package/dist/{components/blocks/text → core/inline}/link-source-bytes.js +43 -54
  263. package/dist/core/inline/live-edit/read-back.d.ts +29 -0
  264. package/dist/core/inline/live-edit/read-back.js +50 -0
  265. package/dist/core/inline/picture.d.ts +10 -0
  266. package/dist/core/inline/picture.js +40 -0
  267. package/dist/core/inline/scan/autolinks.d.ts +7 -8
  268. package/dist/core/inline/scan/autolinks.js +80 -59
  269. package/dist/core/inline/scan/brackets.d.ts +2 -2
  270. package/dist/core/inline/scan/brackets.js +6 -103
  271. package/dist/core/inline/scan/code-spans.d.ts +4 -0
  272. package/dist/core/inline/scan/code-spans.js +8 -0
  273. package/dist/core/inline/scan/emphasis.js +10 -14
  274. package/dist/core/inline/scan/index.d.ts +3 -1
  275. package/dist/core/inline/scan/index.js +32 -33
  276. package/dist/core/inline/scan/plugin-syntax.d.ts +28 -36
  277. package/dist/core/inline/scan/plugin-syntax.js +75 -89
  278. package/dist/core/inline/scan/scan-state.d.ts +2 -0
  279. package/dist/core/inline/scan/scan-state.js +1 -0
  280. package/dist/core/inline/scan/simple-nodes.d.ts +2 -5
  281. package/dist/core/inline/scan/simple-nodes.js +2 -5
  282. package/dist/core/inline/scan/triggers.d.ts +17 -0
  283. package/dist/core/inline/scan/triggers.js +26 -0
  284. package/dist/core/inline/scan/url.js +8 -10
  285. package/dist/core/inline/transparency.d.ts +2 -1
  286. package/dist/core/inline/transparency.js +33 -17
  287. package/dist/core/inline/visibility.d.ts +30 -49
  288. package/dist/core/inline/visibility.js +31 -49
  289. package/dist/core/inline/walk.d.ts +4 -10
  290. package/dist/core/inline/walk.js +4 -10
  291. package/dist/core/inline-render.d.ts +22 -27
  292. package/dist/core/inline-render.js +51 -60
  293. package/dist/core/lines.d.ts +67 -26
  294. package/dist/core/lines.js +164 -33
  295. package/dist/core/metadata-parity.d.ts +9 -0
  296. package/dist/core/metadata-parity.js +25 -0
  297. package/dist/core/node-views.d.ts +7 -6
  298. package/dist/core/node-views.js +5 -5
  299. package/dist/core/nodes.d.ts +34 -44
  300. package/dist/core/nodes.js +4 -12
  301. package/dist/core/parser.d.ts +27 -34
  302. package/dist/core/parser.js +53 -67
  303. package/dist/core/parsers/blockquote.d.ts +5 -8
  304. package/dist/core/parsers/blockquote.js +11 -10
  305. package/dist/core/parsers/built-in-openers.d.ts +2 -5
  306. package/dist/core/parsers/built-in-openers.js +5 -8
  307. package/dist/core/parsers/fence-syntax.d.ts +36 -7
  308. package/dist/core/parsers/fence-syntax.js +61 -11
  309. package/dist/core/parsers/fenced-code.d.ts +16 -0
  310. package/dist/core/parsers/fenced-code.js +16 -15
  311. package/dist/core/parsers/heading.d.ts +19 -2
  312. package/dist/core/parsers/heading.js +50 -2
  313. package/dist/core/parsers/html-block.d.ts +3 -6
  314. package/dist/core/parsers/html-block.js +10 -11
  315. package/dist/core/parsers/indented-code.d.ts +1 -1
  316. package/dist/core/parsers/indented-code.js +2 -8
  317. package/dist/core/parsers/link-reference.d.ts +5 -3
  318. package/dist/core/parsers/link-reference.js +116 -120
  319. package/dist/core/parsers/list.d.ts +12 -5
  320. package/dist/core/parsers/list.js +53 -36
  321. package/dist/core/parsers/paragraph.d.ts +3 -3
  322. package/dist/core/parsers/paragraph.js +14 -5
  323. package/dist/core/parsers/table-completion.d.ts +2 -5
  324. package/dist/core/parsers/table-completion.js +7 -10
  325. package/dist/core/parsers/table.d.ts +8 -8
  326. package/dist/core/parsers/table.js +23 -26
  327. package/dist/core/parsers/thematic-break.js +15 -14
  328. package/dist/core/paths.d.ts +13 -0
  329. package/dist/core/paths.js +31 -0
  330. package/dist/core/terminator-escalation.d.ts +0 -7
  331. package/dist/core/terminator-escalation.js +5 -8
  332. package/dist/core/url-policy.d.ts +2 -5
  333. package/dist/core/url-policy.js +8 -12
  334. package/dist/cursor/caret-memory.d.ts +34 -0
  335. package/dist/cursor/caret-memory.js +82 -0
  336. package/dist/cursor/coordinate-spaces.d.ts +26 -19
  337. package/dist/cursor/coordinate-spaces.js +32 -20
  338. package/dist/cursor/dom-walk.d.ts +7 -13
  339. package/dist/cursor/dom-walk.js +7 -13
  340. package/dist/cursor/edge-affinity.d.ts +7 -35
  341. package/dist/cursor/edge-affinity.js +13 -44
  342. package/dist/cursor/focused-caret.d.ts +6 -8
  343. package/dist/cursor/focused-caret.js +7 -16
  344. package/dist/cursor/height-model.d.ts +3 -3
  345. package/dist/cursor/height-model.js +5 -5
  346. package/dist/cursor/height-oracle.d.ts +10 -7
  347. package/dist/cursor/height-oracle.js +26 -75
  348. package/dist/cursor/observe-resize.d.ts +13 -0
  349. package/dist/cursor/observe-resize.js +43 -0
  350. package/dist/cursor/overlay-rects.d.ts +2 -5
  351. package/dist/cursor/overlay-rects.js +2 -5
  352. package/dist/cursor/overlay-remeasure.d.ts +6 -7
  353. package/dist/cursor/overlay-remeasure.js +10 -14
  354. package/dist/cursor/pending-marks.d.ts +7 -9
  355. package/dist/cursor/pending-marks.js +6 -27
  356. package/dist/cursor/point-offset.d.ts +16 -10
  357. package/dist/cursor/point-offset.js +60 -25
  358. package/dist/cursor/reveal-source.d.ts +7 -11
  359. package/dist/cursor/reveal-source.js +10 -18
  360. package/dist/cursor/scroll-ancestors.d.ts +9 -20
  361. package/dist/cursor/scroll-ancestors.js +14 -26
  362. package/dist/cursor/scroll-owner.d.ts +92 -0
  363. package/dist/cursor/scroll-owner.js +283 -0
  364. package/dist/cursor/scrollport.d.ts +16 -8
  365. package/dist/cursor/scrollport.js +28 -5
  366. package/dist/cursor/sticky-column.d.ts +6 -29
  367. package/dist/cursor/sticky-column.js +6 -49
  368. package/dist/cursor/sticky-measure.d.ts +14 -9
  369. package/dist/cursor/sticky-measure.js +99 -49
  370. package/dist/cursor/surface-backend.d.ts +41 -0
  371. package/dist/cursor/surface-backend.js +78 -0
  372. package/dist/cursor/typography-estimates.d.ts +3 -3
  373. package/dist/cursor/typography-estimates.js +3 -3
  374. package/dist/cursor/visual-lines.d.ts +25 -13
  375. package/dist/cursor/visual-lines.js +72 -28
  376. package/dist/cursor/widget-edge-snap.d.ts +23 -0
  377. package/dist/cursor/widget-edge-snap.js +39 -0
  378. package/dist/cursor/widget-offset.d.ts +111 -102
  379. package/dist/cursor/widget-offset.js +352 -177
  380. package/dist/debug/diagnostics-report.d.ts +6 -6
  381. package/dist/debug/diagnostics-report.js +5 -5
  382. package/dist/debug/dump-tree.js +2 -2
  383. package/dist/debug/editor-diagnostics.d.ts +15 -0
  384. package/dist/debug/editor-diagnostics.js +33 -0
  385. package/dist/debug/inspect.d.ts +2 -2
  386. package/dist/debug/inspect.js +4 -4
  387. package/dist/debug/interaction-trace.d.ts +14 -15
  388. package/dist/debug/interaction-trace.js +19 -20
  389. package/dist/debug/operations-log.d.ts +1 -1
  390. package/dist/debug/operations-log.js +1 -1
  391. package/dist/decorations/buckets.d.ts +6 -12
  392. package/dist/decorations/buckets.js +4 -11
  393. package/dist/decorations/decoration-state.svelte.d.ts +2 -3
  394. package/dist/decorations/decoration-state.svelte.js +24 -26
  395. package/dist/decorations/island-dom.d.ts +10 -18
  396. package/dist/decorations/island-dom.js +25 -50
  397. package/dist/decorations/reserved-attrs.d.ts +6 -8
  398. package/dist/decorations/reserved-attrs.js +31 -17
  399. package/dist/decorations/types.d.ts +6 -7
  400. package/dist/decorations/types.js +2 -1
  401. package/dist/decorations/use-block-decorations.svelte.d.ts +19 -0
  402. package/dist/decorations/use-block-decorations.svelte.js +71 -0
  403. package/dist/decorations/widget-dom.d.ts +3 -3
  404. package/dist/decorations/widget-dom.js +5 -5
  405. package/dist/dev-warn.d.ts +5 -2
  406. package/dist/dev-warn.js +16 -4
  407. package/dist/editor-actions/ancestry-folds.d.ts +9 -14
  408. package/dist/editor-actions/ancestry-folds.js +13 -18
  409. package/dist/editor-actions/block-edit-core.d.ts +49 -22
  410. package/dist/editor-actions/block-edit-core.js +279 -151
  411. package/dist/editor-actions/block-edit-scope.d.ts +63 -50
  412. package/dist/editor-actions/block-edit-scope.js +100 -48
  413. package/dist/editor-actions/block-edit.d.ts +2 -3
  414. package/dist/editor-actions/block-edit.js +14 -90
  415. package/dist/editor-actions/commit/history.d.ts +4 -4
  416. package/dist/editor-actions/commit/history.js +33 -34
  417. package/dist/editor-actions/commit/reading-write-gate.d.ts +18 -0
  418. package/dist/editor-actions/commit/reading-write-gate.js +56 -0
  419. package/dist/editor-actions/commit/text-batch.d.ts +7 -11
  420. package/dist/editor-actions/commit/text-batch.js +6 -7
  421. package/dist/editor-actions/commit/undo-controller.d.ts +6 -4
  422. package/dist/editor-actions/commit/undo-controller.js +346 -216
  423. package/dist/editor-actions/container-block-component.d.ts +32 -51
  424. package/dist/editor-actions/container-block-component.js +66 -92
  425. package/dist/editor-actions/container-edit.d.ts +4 -2
  426. package/dist/editor-actions/container-edit.js +14 -44
  427. package/dist/editor-actions/container-exit-overrides.d.ts +6 -4
  428. package/dist/editor-actions/container-exit-overrides.js +19 -21
  429. package/dist/editor-actions/deps.d.ts +25 -32
  430. package/dist/editor-actions/enter-completion.d.ts +12 -15
  431. package/dist/editor-actions/enter-completion.js +49 -51
  432. package/dist/editor-actions/focus/focus-dispatch.d.ts +24 -25
  433. package/dist/editor-actions/focus/focus-dispatch.js +33 -52
  434. package/dist/editor-actions/focus/focus-landing.d.ts +11 -5
  435. package/dist/editor-actions/focus/focus-landing.js +29 -14
  436. package/dist/editor-actions/focus/focus.d.ts +3 -2
  437. package/dist/editor-actions/focus/focus.js +42 -59
  438. package/dist/editor-actions/index.d.ts +4 -3
  439. package/dist/editor-actions/index.js +5 -4
  440. package/dist/editor-actions/inline-range-commit.d.ts +18 -16
  441. package/dist/editor-actions/inline-range-commit.js +45 -54
  442. package/dist/editor-actions/leaf-write.d.ts +11 -0
  443. package/dist/editor-actions/leaf-write.js +102 -0
  444. package/dist/editor-actions/list-context.d.ts +9 -10
  445. package/dist/editor-actions/list-context.js +115 -110
  446. package/dist/editor-actions/list-overrides.d.ts +13 -5
  447. package/dist/editor-actions/list-overrides.js +36 -12
  448. package/dist/editor-actions/merge-fallback.d.ts +10 -16
  449. package/dist/editor-actions/merge-fallback.js +11 -22
  450. package/dist/editor-actions/nested/container-actions.d.ts +37 -0
  451. package/dist/editor-actions/nested/container-actions.js +42 -0
  452. package/dist/editor-actions/nested/emptied-container.d.ts +5 -0
  453. package/dist/editor-actions/nested/emptied-container.js +5 -0
  454. package/dist/editor-actions/nested/nested-actions.d.ts +22 -25
  455. package/dist/editor-actions/nested/nested-actions.js +17 -15
  456. package/dist/editor-actions/nested/nested-block-edit.d.ts +3 -4
  457. package/dist/editor-actions/nested/nested-block-edit.js +40 -134
  458. package/dist/editor-actions/nested/nested-focus.d.ts +2 -3
  459. package/dist/editor-actions/nested/nested-focus.js +33 -21
  460. package/dist/editor-actions/paste-coordinator.d.ts +3 -2
  461. package/dist/editor-actions/paste-coordinator.js +17 -13
  462. package/dist/editor-actions/plugin/chrome-leaf.d.ts +11 -9
  463. package/dist/editor-actions/plugin/chrome-leaf.js +14 -13
  464. package/dist/editor-actions/plugin/container.d.ts +55 -76
  465. package/dist/editor-actions/plugin/container.js +105 -186
  466. package/dist/editor-actions/plugin/directive-container.d.ts +4 -4
  467. package/dist/editor-actions/plugin/directive-container.js +7 -8
  468. package/dist/editor-actions/reorder-action.d.ts +7 -12
  469. package/dist/editor-actions/reorder-action.js +32 -48
  470. package/dist/editor-actions/reorder-drag.d.ts +12 -8
  471. package/dist/editor-actions/reorder-drag.js +45 -41
  472. package/dist/editor-actions/replacement-focus.d.ts +13 -21
  473. package/dist/editor-actions/replacement-focus.js +32 -33
  474. package/dist/editor-actions/search-replace.js +71 -67
  475. package/dist/editor-actions/stored-caret.d.ts +7 -0
  476. package/dist/editor-actions/stored-caret.js +10 -0
  477. package/dist/editor-actions/table-context.d.ts +9 -11
  478. package/dist/editor-actions/table-context.js +70 -93
  479. package/dist/editor-actions/unwrap-strategies.d.ts +4 -5
  480. package/dist/editor-actions/unwrap-strategies.js +52 -63
  481. package/dist/editor-actions/whole-block-focus-surface.d.ts +19 -28
  482. package/dist/editor-actions/whole-block-focus-surface.js +43 -53
  483. package/dist/editor-events.d.ts +25 -29
  484. package/dist/editor-events.js +17 -28
  485. package/dist/editor-keys.d.ts +96 -104
  486. package/dist/editor-keys.js +11 -24
  487. package/dist/editor-props.d.ts +91 -71
  488. package/dist/editor-rects.d.ts +23 -34
  489. package/dist/editor-rects.js +9 -77
  490. package/dist/env.d.ts +6 -4
  491. package/dist/env.js +8 -4
  492. package/dist/index.d.ts +6 -2
  493. package/dist/index.js +5 -2
  494. package/dist/inline-menu/inline-menu-session.d.ts +37 -0
  495. package/dist/inline-menu/inline-menu-session.js +98 -0
  496. package/dist/inline-menu/inline-menu-state.svelte.d.ts +63 -0
  497. package/dist/inline-menu/inline-menu-state.svelte.js +409 -0
  498. package/dist/inline-menu/types.d.ts +77 -0
  499. package/dist/inline-menu/types.js +7 -0
  500. package/dist/invariants/child-id-parity.d.ts +23 -0
  501. package/dist/invariants/child-id-parity.js +39 -0
  502. package/dist/invariants/commit-paths.d.ts +4 -4
  503. package/dist/invariants/commit-paths.js +4 -4
  504. package/dist/invariants/commit-scope.d.ts +4 -4
  505. package/dist/invariants/commit-scope.js +9 -8
  506. package/dist/invariants/context-keys.d.ts +2 -5
  507. package/dist/invariants/context-keys.js +3 -6
  508. package/dist/invariants/descriptor.d.ts +2 -6
  509. package/dist/invariants/descriptor.js +2 -6
  510. package/dist/invariants/inline-transitions.d.ts +9 -16
  511. package/dist/invariants/inline-transitions.js +10 -18
  512. package/dist/invariants/install.d.ts +18 -27
  513. package/dist/invariants/install.js +29 -30
  514. package/dist/invariants/keeps-a-block.d.ts +7 -0
  515. package/dist/invariants/keeps-a-block.js +14 -0
  516. package/dist/invariants/landable-caret.d.ts +5 -7
  517. package/dist/invariants/landable-caret.js +8 -11
  518. package/dist/invariants/landing-focus-scroll.d.ts +7 -0
  519. package/dist/invariants/landing-focus-scroll.js +13 -0
  520. package/dist/invariants/landing-value.d.ts +13 -0
  521. package/dist/invariants/landing-value.js +27 -0
  522. package/dist/invariants/marker-css-parity.d.ts +5 -6
  523. package/dist/invariants/marker-css-parity.js +11 -12
  524. package/dist/invariants/measures-in-own-list.d.ts +6 -0
  525. package/dist/invariants/measures-in-own-list.js +13 -0
  526. package/dist/invariants/node-shape.d.ts +18 -37
  527. package/dist/invariants/node-shape.js +123 -79
  528. package/dist/invariants/open-tail.d.ts +7 -0
  529. package/dist/invariants/open-tail.js +45 -0
  530. package/dist/invariants/placement-ends-widget.d.ts +8 -0
  531. package/dist/invariants/placement-ends-widget.js +13 -0
  532. package/dist/invariants/registry.d.ts +27 -66
  533. package/dist/invariants/registry.js +61 -137
  534. package/dist/invariants/render-fidelity.d.ts +4 -4
  535. package/dist/invariants/render-fidelity.js +5 -5
  536. package/dist/invariants/selection-endpoints.d.ts +6 -7
  537. package/dist/invariants/selection-endpoints.js +18 -16
  538. package/dist/invariants/single-node-sink.d.ts +4 -5
  539. package/dist/invariants/single-node-sink.js +4 -5
  540. package/dist/invariants/snapshot-integrity.d.ts +5 -6
  541. package/dist/invariants/snapshot-integrity.js +1 -1
  542. package/dist/invariants/structural-descriptor.d.ts +6 -15
  543. package/dist/invariants/structural-descriptor.js +6 -15
  544. package/dist/menu-icons.d.ts +47 -0
  545. package/dist/menu-icons.js +113 -0
  546. package/dist/perf/instruments.d.ts +20 -5
  547. package/dist/perf/instruments.js +35 -15
  548. package/dist/perf/use-mount-gauge.svelte.js +4 -4
  549. package/dist/plugin.d.ts +38 -8
  550. package/dist/plugin.js +102 -74
  551. package/dist/plugins/admonitions/AdmonitionBlock.svelte +14 -11
  552. package/dist/plugins/admonitions/admonition-kind.js +12 -10
  553. package/dist/plugins/admonitions/convert-document.d.ts +5 -5
  554. package/dist/plugins/admonitions/convert-document.js +8 -7
  555. package/dist/plugins/admonitions/gh-alert.d.ts +8 -1
  556. package/dist/plugins/admonitions/gh-alert.js +21 -22
  557. package/dist/plugins/admonitions/github-alert-kind.d.ts +5 -7
  558. package/dist/plugins/admonitions/github-alert-kind.js +28 -37
  559. package/dist/plugins/admonitions/index.js +2 -2
  560. package/dist/plugins/admonitions/kinds.d.ts +1 -1
  561. package/dist/plugins/admonitions/register.js +11 -2
  562. package/dist/plugins/details/DetailsBlock.svelte +20 -20
  563. package/dist/plugins/details/details-disclosure.svelte.d.ts +5 -6
  564. package/dist/plugins/details/details-disclosure.svelte.js +6 -7
  565. package/dist/plugins/details/details-kind.d.ts +6 -7
  566. package/dist/plugins/details/details-kind.js +44 -59
  567. package/dist/plugins/emoji/emoji-recognizer.d.ts +4 -7
  568. package/dist/plugins/emoji/emoji-recognizer.js +7 -14
  569. package/dist/plugins/footnotes/FootnoteDefinition.svelte +33 -16
  570. package/dist/plugins/footnotes/FootnoteReference.svelte +39 -19
  571. package/dist/plugins/footnotes/constants.d.ts +2 -0
  572. package/dist/plugins/footnotes/constants.js +3 -0
  573. package/dist/plugins/footnotes/footnote-definition.d.ts +8 -6
  574. package/dist/plugins/footnotes/footnote-definition.js +69 -45
  575. package/dist/plugins/footnotes/footnote-lookup.d.ts +3 -3
  576. package/dist/plugins/footnotes/footnote-lookup.js +14 -26
  577. package/dist/plugins/footnotes/footnote-numbering.d.ts +11 -11
  578. package/dist/plugins/footnotes/footnote-numbering.js +22 -31
  579. package/dist/plugins/footnotes/footnote-reference.d.ts +4 -4
  580. package/dist/plugins/footnotes/footnote-reference.js +8 -13
  581. package/dist/plugins/footnotes/index.js +2 -3
  582. package/dist/plugins/highlight-occurrences/highlight-occurrences-plugin.d.ts +5 -8
  583. package/dist/plugins/highlight-occurrences/highlight-occurrences-plugin.js +8 -3
  584. package/dist/plugins/highlight-occurrences/occurrence-source.d.ts +10 -12
  585. package/dist/plugins/highlight-occurrences/occurrence-source.js +21 -18
  586. package/dist/plugins/highlight-occurrences/occurrences.d.ts +11 -11
  587. package/dist/plugins/highlight-occurrences/occurrences.js +14 -36
  588. package/dist/plugins/highlight-occurrences/word-char.d.ts +1 -0
  589. package/dist/plugins/highlight-occurrences/word-char.js +4 -0
  590. package/dist/plugins/latex/BlockMath.svelte +58 -75
  591. package/dist/plugins/latex/BlockMath.svelte.d.ts +2 -4
  592. package/dist/plugins/latex/MathInline.svelte +10 -9
  593. package/dist/plugins/latex/flanking.d.ts +1 -0
  594. package/dist/plugins/latex/flanking.js +3 -0
  595. package/dist/plugins/latex/index.d.ts +1 -1
  596. package/dist/plugins/latex/latex-kind.d.ts +4 -4
  597. package/dist/plugins/latex/latex-kind.js +113 -85
  598. package/dist/plugins/latex/math-completion.d.ts +1 -1
  599. package/dist/plugins/latex/math-completion.js +5 -8
  600. package/dist/plugins/latex/math-layout.d.ts +2 -8
  601. package/dist/plugins/latex/math-layout.js +0 -9
  602. package/dist/plugins/latex/math-renderer.d.ts +17 -20
  603. package/dist/plugins/latex/math-renderer.js +21 -38
  604. package/dist/plugins/latex/math-source.d.ts +4 -8
  605. package/dist/plugins/latex/math-source.js +36 -75
  606. package/dist/plugins/latex/register.d.ts +10 -11
  607. package/dist/plugins/latex/register.js +24 -10
  608. package/dist/plugins/latex/renderer.d.ts +9 -10
  609. package/dist/plugins/latex/renderer.js +9 -24
  610. package/dist/plugins/mermaid/MermaidBlock.svelte +63 -47
  611. package/dist/plugins/mermaid/index.js +1 -1
  612. package/dist/plugins/mermaid/mermaid-kind.d.ts +4 -7
  613. package/dist/plugins/mermaid/mermaid-kind.js +30 -47
  614. package/dist/plugins/mermaid/mermaid-renderer.d.ts +9 -16
  615. package/dist/plugins/mermaid/mermaid-renderer.js +12 -31
  616. package/dist/plugins/mermaid/register.d.ts +3 -3
  617. package/dist/plugins/mermaid/register.js +5 -5
  618. package/dist/plugins/mermaid/renderer.d.ts +5 -5
  619. package/dist/plugins/mermaid/renderer.js +8 -11
  620. package/dist/plugins/parrot/ParrotBlock.svelte +18 -7
  621. package/dist/plugins/parrot/ParrotBlock.svelte.d.ts +2 -2
  622. package/dist/plugins/parrot/parrot-plugin.js +3 -3
  623. package/dist/plugins/slash-commands/filter.d.ts +23 -0
  624. package/dist/plugins/slash-commands/filter.js +31 -0
  625. package/dist/plugins/slash-commands/index.d.ts +2 -0
  626. package/dist/plugins/slash-commands/index.js +2 -0
  627. package/dist/plugins/slash-commands/slash-commands-plugin.d.ts +12 -0
  628. package/dist/plugins/slash-commands/slash-commands-plugin.js +40 -0
  629. package/dist/plugins/slash-commands/slash-source.d.ts +36 -0
  630. package/dist/plugins/slash-commands/slash-source.js +108 -0
  631. package/dist/plugins/toc/TocBlock.svelte +23 -22
  632. package/dist/plugins/toc/TocBlock.svelte.d.ts +2 -3
  633. package/dist/plugins/toc/heading-outline.d.ts +7 -12
  634. package/dist/plugins/toc/heading-outline.js +22 -36
  635. package/dist/plugins/toc/navigation-queue.d.ts +2 -4
  636. package/dist/plugins/toc/toc-plugin.d.ts +5 -5
  637. package/dist/plugins/toc/toc-plugin.js +20 -13
  638. package/dist/presentation-mode.d.ts +15 -23
  639. package/dist/presentation-mode.js +19 -25
  640. package/dist/reactivity/block-list-state.svelte.d.ts +9 -11
  641. package/dist/reactivity/block-list-state.svelte.js +8 -9
  642. package/dist/reactivity/block-window.svelte.d.ts +12 -6
  643. package/dist/reactivity/block-window.svelte.js +44 -17
  644. package/dist/reactivity/child-list.d.ts +34 -0
  645. package/dist/reactivity/child-list.js +48 -0
  646. package/dist/reactivity/content-version.svelte.d.ts +4 -7
  647. package/dist/reactivity/content-version.svelte.js +3 -5
  648. package/dist/reactivity/hold-across.d.ts +39 -0
  649. package/dist/reactivity/hold-across.js +57 -0
  650. package/dist/reactivity/layout-state.svelte.d.ts +27 -0
  651. package/dist/reactivity/layout-state.svelte.js +51 -0
  652. package/dist/reactivity/list-tree.d.ts +39 -0
  653. package/dist/reactivity/list-tree.js +132 -0
  654. package/dist/reactivity/list-windowing.svelte.d.ts +50 -71
  655. package/dist/reactivity/list-windowing.svelte.js +143 -292
  656. package/dist/reactivity/measure-batch.d.ts +6 -8
  657. package/dist/reactivity/measure-batch.js +5 -6
  658. package/dist/reactivity/publish-ref.svelte.d.ts +16 -41
  659. package/dist/reactivity/publish-ref.svelte.js +21 -85
  660. package/dist/reactivity/scope-geometry.d.ts +6 -13
  661. package/dist/reactivity/scope-geometry.js +7 -14
  662. package/dist/reactivity/state-registry.d.ts +5 -5
  663. package/dist/reactivity/state-registry.js +11 -15
  664. package/dist/reactivity/use-container-windowing.svelte.d.ts +13 -26
  665. package/dist/reactivity/use-container-windowing.svelte.js +64 -74
  666. package/dist/reactivity/use-measured-child.svelte.d.ts +10 -0
  667. package/dist/reactivity/use-measured-child.svelte.js +41 -0
  668. package/dist/reactivity/use-window-floor.svelte.d.ts +2 -0
  669. package/dist/reactivity/use-window-floor.svelte.js +27 -0
  670. package/dist/reactivity/window-slice.d.ts +4 -4
  671. package/dist/reactivity/window-slice.js +0 -2
  672. package/dist/renderer-slot.d.ts +48 -0
  673. package/dist/renderer-slot.js +85 -0
  674. package/dist/scan-index.d.ts +4 -4
  675. package/dist/scan-index.js +4 -4
  676. package/dist/schema/block-commands.d.ts +69 -54
  677. package/dist/schema/block-commands.js +91 -104
  678. package/dist/schema/block-completions.d.ts +16 -19
  679. package/dist/schema/block-completions.js +25 -32
  680. package/dist/schema/block-component-registry.d.ts +13 -14
  681. package/dist/schema/block-component-registry.js +19 -17
  682. package/dist/schema/block-kind-descriptor.d.ts +234 -168
  683. package/dist/schema/block-kind-descriptor.js +151 -66
  684. package/dist/schema/block-openers.d.ts +48 -39
  685. package/dist/schema/block-openers.js +73 -83
  686. package/dist/schema/built-in-descriptors.d.ts +3 -5
  687. package/dist/schema/built-in-descriptors.js +96 -80
  688. package/dist/schema/child-spans.d.ts +12 -10
  689. package/dist/schema/child-spans.js +68 -34
  690. package/dist/schema/closure.d.ts +18 -16
  691. package/dist/schema/closure.js +24 -14
  692. package/dist/schema/command-id.d.ts +6 -8
  693. package/dist/schema/command-id.js +20 -25
  694. package/dist/schema/commands.d.ts +79 -81
  695. package/dist/schema/commands.js +156 -116
  696. package/dist/schema/container-raw.d.ts +51 -11
  697. package/dist/schema/container-raw.js +143 -14
  698. package/dist/schema/container-rebuilders.d.ts +17 -15
  699. package/dist/schema/container-rebuilders.js +30 -22
  700. package/dist/schema/context-actions.d.ts +25 -13
  701. package/dist/schema/context-actions.js +32 -17
  702. package/dist/schema/define-plugin-block.d.ts +8 -6
  703. package/dist/schema/define-plugin-block.js +6 -4
  704. package/dist/schema/fenced-code-raw.d.ts +30 -17
  705. package/dist/schema/fenced-code-raw.js +155 -107
  706. package/dist/schema/global-commands.d.ts +6 -6
  707. package/dist/schema/global-commands.js +14 -22
  708. package/dist/schema/height-estimates.d.ts +31 -0
  709. package/dist/schema/height-estimates.js +64 -0
  710. package/dist/schema/inline-construct-policy.d.ts +42 -55
  711. package/dist/schema/inline-construct-policy.js +36 -35
  712. package/dist/schema/insert-catalogue.d.ts +30 -0
  713. package/dist/schema/insert-catalogue.js +95 -0
  714. package/dist/schema/keybinding-overrides.d.ts +9 -11
  715. package/dist/schema/keybinding-overrides.js +1 -1
  716. package/dist/schema/keybindings.d.ts +15 -7
  717. package/dist/schema/keybindings.js +21 -9
  718. package/dist/schema/merge-rules.d.ts +5 -7
  719. package/dist/schema/merge-rules.js +7 -9
  720. package/dist/schema/opener-priorities.d.ts +4 -4
  721. package/dist/schema/opener-priorities.js +4 -4
  722. package/dist/schema/operations.d.ts +9 -11
  723. package/dist/schema/operations.js +3 -3
  724. package/dist/schema/page-role.d.ts +14 -0
  725. package/dist/schema/page-role.js +19 -0
  726. package/dist/schema/plugin-activation.d.ts +4 -6
  727. package/dist/schema/plugin-activation.js +12 -10
  728. package/dist/schema/plugin-editor-context.d.ts +25 -10
  729. package/dist/schema/plugin-editor-context.js +40 -9
  730. package/dist/schema/plugin-install.d.ts +49 -17
  731. package/dist/schema/plugin-install.js +51 -25
  732. package/dist/schema/plugin-kind.d.ts +12 -8
  733. package/dist/schema/plugin-kind.js +35 -42
  734. package/dist/schema/plugin-name.js +3 -3
  735. package/dist/schema/plugin-registry.d.ts +54 -0
  736. package/dist/schema/plugin-registry.js +70 -0
  737. package/dist/schema/reading.d.ts +23 -0
  738. package/dist/schema/reading.js +7 -0
  739. package/dist/schema/register-once.d.ts +3 -5
  740. package/dist/schema/register-once.js +7 -16
  741. package/dist/schema/registration-checks.d.ts +12 -16
  742. package/dist/schema/registration-checks.js +35 -51
  743. package/dist/schema/registration-pairs.d.ts +54 -0
  744. package/dist/schema/registration-pairs.js +48 -0
  745. package/dist/schema/registration-pending.d.ts +8 -12
  746. package/dist/schema/registration-pending.js +9 -9
  747. package/dist/schema/registry-reset.d.ts +8 -4
  748. package/dist/schema/registry-reset.js +13 -26
  749. package/dist/schema/registry-view.d.ts +25 -13
  750. package/dist/schema/registry-view.js +26 -20
  751. package/dist/schema/reserved-chords.d.ts +9 -10
  752. package/dist/schema/reserved-chords.js +73 -78
  753. package/dist/schema/reserved-chrome.d.ts +12 -11
  754. package/dist/schema/reserved-chrome.js +15 -11
  755. package/dist/schema/setext-raw.d.ts +13 -0
  756. package/dist/schema/setext-raw.js +33 -0
  757. package/dist/schema/stored-as.d.ts +20 -0
  758. package/dist/schema/stored-as.js +6 -0
  759. package/dist/schema/table-cell-raw.d.ts +18 -7
  760. package/dist/schema/table-cell-raw.js +28 -7
  761. package/dist/schema/whole-block-unit.d.ts +5 -4
  762. package/dist/schema/whole-block-unit.js +5 -4
  763. package/dist/search/document-scan.d.ts +3 -3
  764. package/dist/search/document-scan.js +2 -2
  765. package/dist/search/matcher.d.ts +1 -1
  766. package/dist/search/regex-executor.d.ts +5 -6
  767. package/dist/search/regex-executor.js +6 -8
  768. package/dist/search/replace.d.ts +2 -5
  769. package/dist/search/replace.js +2 -5
  770. package/dist/search/search-state.svelte.d.ts +5 -5
  771. package/dist/search/search-state.svelte.js +12 -16
  772. package/dist/selection/autoscroll.d.ts +6 -9
  773. package/dist/selection/block-hit-test.d.ts +16 -24
  774. package/dist/selection/block-hit-test.js +29 -22
  775. package/dist/selection/caret-doors.d.ts +15 -14
  776. package/dist/selection/caret-doors.js +37 -26
  777. package/dist/selection/caret-landing.d.ts +62 -0
  778. package/dist/selection/caret-landing.js +157 -0
  779. package/dist/selection/caret-restore.d.ts +17 -10
  780. package/dist/selection/caret-restore.js +10 -22
  781. package/dist/selection/caret-target.d.ts +30 -0
  782. package/dist/selection/caret-target.js +84 -0
  783. package/dist/selection/char-endpoint-snap.d.ts +6 -10
  784. package/dist/selection/char-endpoint-snap.js +8 -14
  785. package/dist/selection/clipboard-text.d.ts +5 -12
  786. package/dist/selection/clipboard-text.js +69 -172
  787. package/dist/selection/covered-block.d.ts +6 -8
  788. package/dist/selection/covered-block.js +8 -19
  789. package/dist/selection/cross-block/clipboard.d.ts +3 -2
  790. package/dist/selection/cross-block/clipboard.js +13 -4
  791. package/dist/selection/cross-block/dispatch.d.ts +47 -43
  792. package/dist/selection/cross-block/dispatch.js +12 -23
  793. package/dist/selection/cross-block/format-range.d.ts +18 -21
  794. package/dist/selection/cross-block/format-range.js +104 -103
  795. package/dist/selection/cross-block/format-toggle.d.ts +10 -15
  796. package/dist/selection/cross-block/format-toggle.js +28 -33
  797. package/dist/selection/cross-block/keydown.d.ts +4 -4
  798. package/dist/selection/cross-block/keydown.js +93 -152
  799. package/dist/selection/cross-block/ops.d.ts +19 -34
  800. package/dist/selection/cross-block/ops.js +110 -95
  801. package/dist/selection/cross-block/paste.d.ts +2 -3
  802. package/dist/selection/cross-block/paste.js +61 -104
  803. package/dist/selection/cross-block/pointer.d.ts +17 -15
  804. package/dist/selection/cross-block/pointer.js +74 -25
  805. package/dist/selection/cross-block/type-replace.d.ts +4 -6
  806. package/dist/selection/cross-block/type-replace.js +53 -146
  807. package/dist/selection/dead-space-caret.d.ts +20 -28
  808. package/dist/selection/dead-space-caret.js +79 -87
  809. package/dist/selection/drag-pointer.d.ts +22 -5
  810. package/dist/selection/drag-pointer.js +63 -31
  811. package/dist/selection/gap-caret.d.ts +17 -28
  812. package/dist/selection/gap-caret.js +14 -26
  813. package/dist/selection/grid-selection.d.ts +12 -0
  814. package/dist/selection/grid-selection.js +22 -0
  815. package/dist/selection/keyboard-extend.d.ts +27 -39
  816. package/dist/selection/keyboard-extend.js +94 -116
  817. package/dist/selection/multi-click.d.ts +37 -0
  818. package/dist/selection/multi-click.js +197 -0
  819. package/dist/selection/native-bridge.d.ts +30 -37
  820. package/dist/selection/native-bridge.js +80 -100
  821. package/dist/selection/nearest-block.d.ts +17 -12
  822. package/dist/selection/nearest-block.js +37 -18
  823. package/dist/selection/path-lookup.d.ts +31 -14
  824. package/dist/selection/path-lookup.js +110 -20
  825. package/dist/selection/path-math.d.ts +9 -11
  826. package/dist/selection/path-math.js +5 -7
  827. package/dist/selection/pointer-gesture.d.ts +9 -0
  828. package/dist/selection/pointer-gesture.js +11 -0
  829. package/dist/selection/pointer-session.d.ts +10 -16
  830. package/dist/selection/pointer-session.js +6 -7
  831. package/dist/selection/primitives.d.ts +64 -33
  832. package/dist/selection/primitives.js +61 -53
  833. package/dist/selection/range-coverage.d.ts +94 -0
  834. package/dist/selection/range-coverage.js +365 -0
  835. package/dist/selection/range-delete-ceremony.d.ts +28 -74
  836. package/dist/selection/range-delete-ceremony.js +100 -141
  837. package/dist/selection/range-delete-chrome.d.ts +20 -33
  838. package/dist/selection/range-delete-chrome.js +53 -78
  839. package/dist/selection/range-delete-table-coverage.d.ts +11 -25
  840. package/dist/selection/range-delete-table-coverage.js +42 -112
  841. package/dist/selection/range-delete-table.d.ts +14 -12
  842. package/dist/selection/range-delete-table.js +106 -310
  843. package/dist/selection/range-delete.d.ts +18 -19
  844. package/dist/selection/range-delete.js +83 -123
  845. package/dist/selection/round-trip-restore.d.ts +18 -0
  846. package/dist/selection/round-trip-restore.js +19 -0
  847. package/dist/selection/selection-announcer.d.ts +17 -0
  848. package/dist/selection/selection-announcer.js +25 -0
  849. package/dist/selection/selection-description.d.ts +4 -3
  850. package/dist/selection/selection-description.js +5 -4
  851. package/dist/selection/selection-drop.d.ts +52 -0
  852. package/dist/selection/selection-drop.js +317 -0
  853. package/dist/selection/selection-restore.d.ts +20 -33
  854. package/dist/selection/selection-restore.js +21 -44
  855. package/dist/selection/selection-state.svelte.d.ts +50 -42
  856. package/dist/selection/selection-state.svelte.js +144 -98
  857. package/dist/selection/shared-keydown.d.ts +22 -27
  858. package/dist/selection/shared-keydown.js +25 -45
  859. package/dist/selection/table-endpoint-snap.d.ts +17 -41
  860. package/dist/selection/table-endpoint-snap.js +54 -80
  861. package/dist/selection/table-rect-extend.d.ts +3 -6
  862. package/dist/selection/table-rect-extend.js +19 -14
  863. package/dist/styles/editor-theme.css +75 -53
  864. package/dist/styles/editor.css +248 -139
  865. package/dist/testing/conformance-core.d.ts +50 -18
  866. package/dist/testing/conformance-core.js +88 -50
  867. package/dist/testing/container-conformance.d.ts +48 -84
  868. package/dist/testing/container-conformance.js +159 -285
  869. package/dist/testing/headless-actions.d.ts +34 -19
  870. package/dist/testing/headless-actions.js +116 -65
  871. package/dist/testing/headless-block-list.svelte.d.ts +16 -0
  872. package/dist/testing/headless-block-list.svelte.js +22 -0
  873. package/dist/testing/inline-conformance.d.ts +15 -22
  874. package/dist/testing/inline-conformance.js +131 -148
  875. package/dist/testing/kind-conformance.d.ts +19 -24
  876. package/dist/testing/kind-conformance.js +204 -117
  877. package/dist/testing/kit-reading.d.ts +7 -0
  878. package/dist/testing/kit-reading.js +16 -0
  879. package/dist/testing/mount-dom-stubs.d.ts +4 -5
  880. package/dist/testing/mount-dom-stubs.js +8 -5
  881. package/dist/testing/parse-convergence.d.ts +10 -10
  882. package/dist/testing/parse-convergence.js +14 -49
  883. package/dist/testing.d.ts +13 -9
  884. package/dist/testing.js +28 -38
  885. package/dist/tree-operations/blockquote.d.ts +8 -14
  886. package/dist/tree-operations/blockquote.js +25 -45
  887. package/dist/tree-operations/chain-rebuild.d.ts +34 -25
  888. package/dist/tree-operations/chain-rebuild.js +115 -52
  889. package/dist/tree-operations/children.d.ts +5 -9
  890. package/dist/tree-operations/children.js +7 -11
  891. package/dist/tree-operations/cleanup.d.ts +6 -7
  892. package/dist/tree-operations/cleanup.js +14 -11
  893. package/dist/tree-operations/clone.js +5 -4
  894. package/dist/tree-operations/container-lift.d.ts +10 -5
  895. package/dist/tree-operations/container-lift.js +20 -14
  896. package/dist/tree-operations/container-offsets.d.ts +22 -0
  897. package/dist/tree-operations/container-offsets.js +89 -0
  898. package/dist/tree-operations/content-write.d.ts +52 -29
  899. package/dist/tree-operations/content-write.js +155 -110
  900. package/dist/tree-operations/index.d.ts +4 -4
  901. package/dist/tree-operations/index.js +4 -4
  902. package/dist/tree-operations/keep-one-block.d.ts +10 -0
  903. package/dist/tree-operations/keep-one-block.js +19 -0
  904. package/dist/tree-operations/leaf-range.d.ts +33 -0
  905. package/dist/tree-operations/leaf-range.js +67 -0
  906. package/dist/tree-operations/list/empty-check.d.ts +2 -3
  907. package/dist/tree-operations/list/empty-check.js +4 -4
  908. package/dist/tree-operations/list/exit-replacement.d.ts +4 -6
  909. package/dist/tree-operations/list/exit-replacement.js +4 -9
  910. package/dist/tree-operations/list/item-partition.d.ts +2 -2
  911. package/dist/tree-operations/list/item-partition.js +2 -2
  912. package/dist/tree-operations/list/list-builders.d.ts +18 -16
  913. package/dist/tree-operations/list/list-builders.js +59 -30
  914. package/dist/tree-operations/list/ordered-markers.d.ts +7 -16
  915. package/dist/tree-operations/list/ordered-markers.js +7 -16
  916. package/dist/tree-operations/list/reconcile-task.d.ts +16 -7
  917. package/dist/tree-operations/list/reconcile-task.js +80 -23
  918. package/dist/tree-operations/list/sublist-separator.d.ts +7 -7
  919. package/dist/tree-operations/list/sublist-separator.js +20 -27
  920. package/dist/tree-operations/list/task-paragraph.d.ts +25 -0
  921. package/dist/tree-operations/list/task-paragraph.js +54 -0
  922. package/dist/tree-operations/list/unwrap-merge.d.ts +8 -16
  923. package/dist/tree-operations/list/unwrap-merge.js +44 -79
  924. package/dist/tree-operations/node-ops.d.ts +30 -52
  925. package/dist/tree-operations/node-ops.js +152 -218
  926. package/dist/tree-operations/node-primitives.d.ts +44 -31
  927. package/dist/tree-operations/node-primitives.js +84 -60
  928. package/dist/tree-operations/open-tail.d.ts +25 -0
  929. package/dist/tree-operations/open-tail.js +100 -0
  930. package/dist/tree-operations/parse-block.d.ts +15 -2
  931. package/dist/tree-operations/parse-block.js +19 -4
  932. package/dist/tree-operations/paste/apply.d.ts +7 -8
  933. package/dist/tree-operations/paste/apply.js +33 -63
  934. package/dist/tree-operations/paste/body-write.d.ts +8 -6
  935. package/dist/tree-operations/paste/body-write.js +13 -14
  936. package/dist/tree-operations/paste/container-match.d.ts +7 -10
  937. package/dist/tree-operations/paste/container-match.js +76 -58
  938. package/dist/tree-operations/paste/container-paste.d.ts +3 -3
  939. package/dist/tree-operations/paste/container-paste.js +3 -3
  940. package/dist/tree-operations/paste/dispatch.d.ts +26 -29
  941. package/dist/tree-operations/paste/dispatch.js +68 -53
  942. package/dist/tree-operations/paste/focus-target.d.ts +11 -14
  943. package/dist/tree-operations/paste/focus-target.js +13 -18
  944. package/dist/tree-operations/paste/hooks.d.ts +7 -12
  945. package/dist/tree-operations/paste/hooks.js +49 -46
  946. package/dist/tree-operations/paste/line-ending.d.ts +11 -0
  947. package/dist/tree-operations/paste/line-ending.js +32 -0
  948. package/dist/tree-operations/paste/list-absorb.d.ts +8 -11
  949. package/dist/tree-operations/paste/list-absorb.js +17 -19
  950. package/dist/tree-operations/paste/list-break-out.d.ts +12 -11
  951. package/dist/tree-operations/paste/list-break-out.js +24 -32
  952. package/dist/tree-operations/paste/parent-scope.d.ts +8 -12
  953. package/dist/tree-operations/paste/parent-scope.js +9 -16
  954. package/dist/tree-operations/paste/paste-deps.d.ts +14 -9
  955. package/dist/tree-operations/paste/paste-replacement.d.ts +20 -3
  956. package/dist/tree-operations/paste/paste-replacement.js +73 -42
  957. package/dist/tree-operations/paste/paste-transforms.d.ts +5 -7
  958. package/dist/tree-operations/paste/paste-transforms.js +18 -27
  959. package/dist/tree-operations/paste/replacement-parse.d.ts +19 -0
  960. package/dist/tree-operations/paste/replacement-parse.js +24 -0
  961. package/dist/tree-operations/paste/strategy.d.ts +1 -1
  962. package/dist/tree-operations/paste/strategy.js +1 -1
  963. package/dist/tree-operations/paste/table-slice.js +15 -10
  964. package/dist/tree-operations/paste-surfaces.d.ts +18 -29
  965. package/dist/tree-operations/paste-surfaces.js +14 -10
  966. package/dist/tree-operations/path-mutate.d.ts +7 -8
  967. package/dist/tree-operations/path-mutate.js +4 -4
  968. package/dist/tree-operations/reorder-unit.d.ts +4 -5
  969. package/dist/tree-operations/reorder-unit.js +5 -6
  970. package/dist/tree-operations/reorder.d.ts +7 -9
  971. package/dist/tree-operations/reorder.js +39 -40
  972. package/dist/tree-operations/settle.d.ts +42 -53
  973. package/dist/tree-operations/settle.js +241 -162
  974. package/dist/tree-operations/sharing.d.ts +5 -5
  975. package/dist/tree-operations/splice-many.d.ts +5 -5
  976. package/dist/tree-operations/splice-many.js +5 -5
  977. package/dist/tree-operations/stored-as.d.ts +13 -0
  978. package/dist/tree-operations/stored-as.js +48 -0
  979. package/dist/tree-operations/structural-change.d.ts +11 -15
  980. package/dist/tree-operations/structural-change.js +10 -14
  981. package/dist/tree-operations/structural-suffix.d.ts +22 -0
  982. package/dist/tree-operations/structural-suffix.js +61 -0
  983. package/dist/tree-operations/sub-table-copy.d.ts +7 -0
  984. package/dist/tree-operations/sub-table-copy.js +40 -36
  985. package/dist/tree-operations/table-grid-clipboard.d.ts +2 -7
  986. package/dist/tree-operations/table-grid-clipboard.js +11 -36
  987. package/dist/tree-operations/table-mutations.d.ts +7 -3
  988. package/dist/tree-operations/table-mutations.js +45 -9
  989. package/dist/tree-operations/unshare.d.ts +11 -15
  990. package/dist/tree-operations/unshare.js +15 -17
  991. package/dist/undo/manager.js +1 -1
  992. package/dist/undo/types.d.ts +2 -2
  993. package/docs/guide/consumer-guide.md +329 -205
  994. package/docs/guide/directives.md +7 -7
  995. package/docs/guide/plugin-api.md +267 -183
  996. package/docs/guide/plugin-guide.md +341 -235
  997. package/docs/guide/plugin-testing.md +96 -82
  998. package/package.json +17 -21
  999. package/dist/ambient/ambient-cursor.d.ts +0 -41
  1000. package/dist/ambient/ambient-cursor.js +0 -120
  1001. package/dist/components/blocks/code/code-paste.d.ts +0 -24
  1002. package/dist/components/blocks/code/code-paste.js +0 -21
  1003. package/dist/components/blocks/table/cell-clipboard.d.ts +0 -33
  1004. package/dist/components/blocks/table/cell-clipboard.js +0 -67
  1005. package/dist/components/blocks/text/hidden-suffix.d.ts +0 -10
  1006. package/dist/components/blocks/text/hidden-suffix.js +0 -15
  1007. package/dist/components/blocks/text/link-source-bytes.d.ts +0 -37
  1008. package/dist/components/image/image-source-bytes.d.ts +0 -15
  1009. package/dist/components/image/image-source-bytes.js +0 -80
  1010. package/dist/cursor/content-offsets.d.ts +0 -29
  1011. package/dist/cursor/content-offsets.js +0 -151
  1012. package/dist/cursor/reveal-anchor.d.ts +0 -30
  1013. package/dist/cursor/reveal-anchor.js +0 -31
  1014. package/dist/invariants/split-landing.d.ts +0 -8
  1015. package/dist/invariants/split-landing.js +0 -15
  1016. package/dist/selection/double-click-trim.d.ts +0 -17
  1017. package/dist/selection/double-click-trim.js +0 -57
  1018. package/dist/tree-operations/list/terminator.d.ts +0 -17
  1019. package/dist/tree-operations/list/terminator.js +0 -60
  1020. package/dist/tree-operations/paste/replace-block-at-parent.d.ts +0 -29
  1021. package/dist/tree-operations/paste/replace-block-at-parent.js +0 -75
@@ -1,6 +1,6 @@
1
1
  # Plugin Author Guide
2
2
 
3
- This guide is for teaching the editor your own block or inline content. Everything you'll use comes from one import, `@voithos-labs/aragonite/plugin`. The package root, `@voithos-labs/aragonite`, is the embedding side, what a host app mounts the editor with.
3
+ This guide is for teaching the editor your own block or inline content. All of the authoring API comes from one import, `@voithos-labs/aragonite/plugin`. The package root, `@voithos-labs/aragonite`, is the embedding side, what a host app mounts the editor with (you'll borrow its `installPlugins` once or twice), and your test suite imports from `@voithos-labs/aragonite/testing`.
4
4
 
5
5
  Four neighbouring docs carry what this one doesn't:
6
6
 
@@ -27,6 +27,7 @@ This one's long, so here's a map. Each section stands on its own; jump straight
27
27
  | [Inline kinds](#inline-kinds) | Your own inline syntax: recognizing it mid-paragraph, rendering it as a widget, editing it |
28
28
  | [Decorations](#decorations) | View-only annotations over content you don't own |
29
29
  | [Block commands](#block-commands) | Keyboard shortcuts and commands, for one block kind or for the whole editor |
30
+ | [Block context actions](#block-context-actions) | Your own rows in the menu a right-click on your block opens |
30
31
  | [Paste transforms](#paste-transforms) | Rewriting pasted text before it parses |
31
32
  | [Recipe: a kind only a menu creates](#recipe-a-kind-only-a-menu-creates) | Blocks inserted from a menu instead of typed, without breaking save-and-reload |
32
33
  | [What a plugin may and may not do](#what-a-plugin-may-and-may-not-do) | The boundary, and what each mistake looks like when you cross it |
@@ -41,14 +42,14 @@ Before the code, two terms everything below leans on.
41
42
 
42
43
  A **kind** is aragonite's word for a block type. Paragraph is a kind, fenced code is a kind, the parrot is about to be one.
43
44
 
44
- A block's **raw** is its exact source bytes, markers included. The editor saves a document by concatenating raws and nothing else, so whatever your plugin writes into that field is exactly what lands in the user's file.
45
+ A block's **raw** is its exact source bytes, markers included. The editor saves a document by joining raws (plus the blank lines kept between blocks) and nothing else, so whatever your plugin writes into that field is exactly what lands in the user's file.
45
46
 
46
47
  **Declare and describe.** Registering a kind is four calls. The rest of the guide keeps coming back to them, and so will you. Here's each one properly:
47
48
 
48
- - **`declarePluginKind(name)`** mints a new kind and returns it (minted: created by the one authorized place; a duplicate throws). Every other call here takes that return value, and the type system won't accept the bare string in its place. A module that didn't mint the kind recovers it with `declaredPluginKind(name)`, which throws for an undeclared name (a typo, say) rather than registering against a kind that doesn't exist.
49
+ - **`declarePluginKind(name)`** creates a new kind and returns it (a name that's already taken throws). Every other call here takes that return value, and the type system won't accept the bare string in its place. A module that didn't create the kind recovers it with `declaredPluginKind(name)`, which throws for an undeclared name (a typo, say) rather than registering against a kind that doesn't exist.
49
50
  - **`registerBlockKind(kind, descriptor)`** describes how the kind behaves: does it merge, is it editable, does it host inline content, where can a caret sit beside it, and how it answers every cross-cutting editor system (the `closure` field). A leaf needs only what the sample below fills.
50
- - **`registerBlockOpener(kind, opener)`** teaches the parser to recognize the syntax. An **opener** is the part of the parser that spots the line a block starts with: you give it a `priority` (its place in the dispatch order), an `interruptsParagraph` predicate, and a `tryOpen` that claims lines or declines. [Teaching the parser](#teaching-the-parser) is its full story.
51
- - **`definePluginBlock({ name, kind, component, register })`** packages the lot as one installable unit: it runs your `register` step, then binds the component to the kind. It's the one-kind shortcut over the general `definePlugin` ([The plugin unit](#the-plugin-unit)).
51
+ - **`registerBlockOpener(kind, opener)`** teaches the parser to recognize the syntax. An **opener** is the part of the parser that spots the line a block starts with: you give it a `priority` (its place in the dispatch order), an `interruptsParagraph` predicate (or `false`, for never), and a `tryOpen` that claims lines or declines. [Teaching the parser](#teaching-the-parser) is its full story.
52
+ - **`definePluginBlock({ name, kind, component, register })`** packages the lot as one installable unit: it runs your `register` step, then binds the component to the kind. It's the one-kind shortcut over the general `definePlugin` ([The plugin unit](#the-plugin-unit)), and takes the same optional `defaults` and `parseOptions`. Its `register` gets no setup context, though, so a plugin that needs per-editor work (`onEditor`) uses `definePlugin`.
52
53
 
53
54
  The first one in action (a kind is a plain string underneath, with a type brand on top):
54
55
 
@@ -77,8 +78,8 @@ import ParrotBlock from './ParrotBlock.svelte';
77
78
 
78
79
  export const PARROT = 'parrot';
79
80
 
80
- /** Where a press in the block puts the caret. The caption renders the bytes after `%%parrot `,
81
- * so an offset in it sits that far along the source; the revealed source IS the source. */
81
+ /** Where a click in the block puts the caret. The caption says where it starts in the source,
82
+ * so an offset in it sits that far along; the shown source is the source itself. */
82
83
  function parrotCaretAtPoint(
83
84
  blockEl: HTMLElement,
84
85
  clientX: number,
@@ -88,7 +89,7 @@ function parrotCaretAtPoint(
88
89
  const view = source ?? blockEl.querySelector<HTMLElement>('.parrot-caption');
89
90
  if (!view) return null;
90
91
  const offset = caretOffsetAtPoint(view, clientX, clientY) ?? 0;
91
- return { path: [], offset: source ? offset : offset + '%%parrot '.length };
92
+ return { path: [], offset: source ? offset : offset + Number(view.dataset.captionStart) };
92
93
  }
93
94
 
94
95
  function registerParrotBlock(): void {
@@ -130,14 +131,15 @@ export function parrotPlugin(): EditorPlugin {
130
131
  }
131
132
  ```
132
133
 
133
- The object you handed `registerBlockKind` is the kind's **descriptor**. Most of its fields read as they sound. Four don't:
134
+ The object you handed `registerBlockKind` is the kind's **descriptor**. Most of its fields read as they sound. Five don't:
134
135
 
135
136
  - `gapEdges` is required so a caret can always reach the space beside your block. Answering `'none'` is a decision, not an omission ([Editable-content tiers](#editable-content-tiers) has the full story).
136
137
  - `closure` is required so every cross-cutting editor system (undo, search, selection, and the rest) gets a written answer from your kind. [The closure block](#the-closure-block) explains every cell.
137
- - `conformanceFixture` is optional. Supplying it enrolls your kind in the conformance kit, a bundled suite of checks every registered kind is run through ([plugin-testing.md](plugin-testing.md)).
138
+ - `conformanceFixture` is optional, but the conformance kits ([plugin-testing.md](plugin-testing.md)) need it: it's the Markdown their headless checks parse and round-trip. Without one, the kind checkup reports those cells `boundary` (unchecked), and the container checkup fails outright.
139
+ - `pageRole` is optional, and it's how your block reads on the page. Say `'prose'` if it reads as part of the text around it, the way a quote or a note does. A prose block gets no drag handle, and right-clicking its text gives the clipboard rows. Leave it out and your block is an object someone picks up whole, with its own handle and menu, which is what the parrot is. (If your block's text would make a silly label on the drag ghost, a formula's source say, give it a `dragLabel` too.)
138
140
  - `caretTargetAtPoint` is optional too: where a click inside your block puts the caret. Leave it out and a click on the folded view reveals the source at its first byte, which is a letdown when you clicked halfway into the caption.
139
141
 
140
- The parrot's answer is two steps. The caption and the source line are different strings, and `caretOffsetAtPoint` does the pixel half: hand it one of your own elements and the click, and it gives back the character offset nearest that point, clamped into the element's box, so a click on the bird above the caption still lands on a character. The arithmetic between the two strings is yours, and for the parrot it's the length of its own marker: an offset in the caption sits `'%%parrot '.length` further along the source.
142
+ The parrot's answer is two steps. The caption and the source line are different strings, and `caretOffsetAtPoint` does the pixel half: hand it one of your own elements and the click, and it gives back the character offset nearest that point, clamped into the element's box, so a click on the bird above the caption still lands on the character under it. The arithmetic between the two strings is yours. The parrot's caption is its line minus the marker and the whitespace around the text, so the component works out where the caption starts in the source, puts that on the caption element as `data-caption-start`, and the hook adds it to the offset. (Hardcoding `'%%parrot '.length` works right up until someone types two spaces.)
141
143
 
142
144
  On the opener, `priority` decides where you sit in the built-in openers' dispatch order ([Opener priority](#opener-priority)) and `consumed` is the number of lines you claimed ([What an opener returns](#what-an-opener-returns)).
143
145
 
@@ -146,7 +148,7 @@ On the opener, `priority` decides where you sit in the built-in openers' dispatc
146
148
  ```svelte
147
149
  <!-- ParrotBlock.svelte -->
148
150
  <script lang="ts">
149
- import { createEditableLeaf, type NodeView } from '@voithos-labs/aragonite/plugin';
151
+ import { createEditableLeaf, trimWhitespace, type NodeView } from '@voithos-labs/aragonite/plugin';
150
152
 
151
153
  let { node, index, myPath = [] }: { node: NodeView; index: number; myPath?: number[] } = $props();
152
154
  let sourceEl: HTMLDivElement | undefined = $state();
@@ -207,13 +209,21 @@ xx:':;;;;,.,,...,;;cllllllllllllllc;'.;od,
207
209
  cNo.....................................oc
208
210
  `
209
211
  ];
210
- // One strip the CSS scrolls a frame at a time. The closing newline is load-bearing: a `pre`
212
+ // One strip the CSS scrolls a frame at a time. The closing newline matters: a `pre`
211
213
  // drops a trailing blank line, and a strip a row short steps a fraction off every frame.
212
214
  const REEL = FRAMES.join('\n') + '\n';
213
215
  // The clip window's height, which is why every frame has to be the same number of rows.
214
216
  const FRAME_ROWS = FRAMES[0].split('\n').length;
215
217
 
216
- const caption = $derived(node.raw.slice('%%parrot'.length).trim());
218
+ // The caption is the rest of the marker line, trimmed, and `start` is where it sits in the
219
+ // source: `parrotCaretAtPoint` reads it off the element to map a press back to a byte.
220
+ function parrotCaption(raw: string): { text: string; start: number } {
221
+ const rest = raw.slice('%%parrot'.length);
222
+ const text = trimWhitespace(rest);
223
+ return { text, start: '%%parrot'.length + rest.indexOf(text) };
224
+ }
225
+
226
+ const caption = $derived(parrotCaption(node.raw));
217
227
 
218
228
  export const editable = true;
219
229
  export const focusable = true;
@@ -224,8 +234,8 @@ cNo.....................................oc
224
234
  export const getSelectedText = leaf.getSelectedText;
225
235
  export const setSelection = leaf.setSelection;
226
236
  export const measurePartialRects = leaf.measurePartialRects;
227
- export const runCommand = leaf.runCommand;
228
237
  export const insertMarkdown = leaf.insertMarkdown;
238
+ export const afterSourceCommit = leaf.afterSourceCommit;
229
239
  </script>
230
240
 
231
241
  <div
@@ -245,11 +255,12 @@ cNo.....................................oc
245
255
  {:else}
246
256
  <div
247
257
  class="parrot-caption"
258
+ data-caption-start={caption.start}
248
259
  role="button"
249
260
  tabindex="-1"
250
261
  aria-label="Party parrot caption (click to edit)"
251
262
  >
252
- {caption}
263
+ {caption.text}
253
264
  </div>
254
265
  {/if}
255
266
  </div>
@@ -260,14 +271,16 @@ cNo.....................................oc
260
271
  font-size: 1.1em;
261
272
  line-height: 1.1;
262
273
  letter-spacing: 0.05em;
263
- /* one frame tall, in the reel's own rows so a step lands on the next frame exactly */
274
+ /* one frame tall, in the reel's own rows so a step lands on the next frame exactly; the
275
+ em line is the same height for engines without lh (Safari before 16.4) */
276
+ height: calc(var(--parrot-rows) * 1.1em);
264
277
  height: calc(var(--parrot-rows) * 1lh);
265
278
  /* wider than a phone column, and the editor root pans if it isn't contained; the bar
266
279
  would sit across the bird, which is decoration rather than a pane to scroll */
267
280
  overflow-x: auto;
268
281
  overflow-y: hidden;
269
282
  scrollbar-width: none;
270
- /* chrome, not content: every frame is in the DOM and none of them belong in a copy */
283
+ /* decoration, not content: every frame is in the DOM and none of them belong in a copy */
271
284
  user-select: none;
272
285
  animation: parrot-hue 0.49s step-end infinite;
273
286
  }
@@ -335,15 +348,15 @@ The editing half is the factory call, the `revealed` flag, two spreads, and the
335
348
 
336
349
  - `revealed` is yours. The factory flips it through `setRevealed` (on when a click or an arrow lands in the block, off when the caret leaves), and the `{#if}` swaps the two views on it.
337
350
  - `surfaceProps` goes on the source line. `renderProps` goes on the block wrapper, so a click anywhere in the block reveals, bird included, and lands where `caretTargetAtPoint` said. Spread both; a folded view that takes the click but not the keys swallows undo while it holds focus.
338
- - `focus`, `getCursorOffset`, `editable` and `focusable` are the four every block component must export. The other seven are how `insertMarkdown`, `runCommand`, and a selection landing reach your block, so keep them.
351
+ - `focus`, `getCursorOffset`, `editable` and `focusable` are the four every block component must export. The next six are how `insertMarkdown` and a selection landing reach your block, and `afterSourceCommit` writes the open source before a move, so `editor.runCommand('block.moveDown')` with the caret in your source doesn't leave the edit behind. Keep all of them.
339
352
  - The commit happens when the caret leaves, not per keystroke. Reveal, type, arrow out: one undo entry, and the caption follows the new raw.
340
353
  - `singleLine: true` says the bytes are one line (the opener claims exactly one), so Enter ends the block instead of typing a newline nothing could show you: whatever sits after the caret becomes a paragraph below, and the caret goes with it, same as in a heading. A leaf whose bytes can span lines leaves the flag off and gives its source element `white-space: pre-wrap` instead, for a reason [The editable leaf](#the-editable-leaf) explains.
341
354
 
342
- The parrot half is the `<pre>`, its CSS, and the caption reading straight off `node.raw`. No script runs per frame, and `prefers-reduced-motion` parks the bird on its first frame for free. It does owe the document one thing, which every block wider than the text column owes: scroll inside your own box (`overflow-x: auto`, same as a code block or a table). The editor root scrolls, so an uncontained block pans the whole page sideways and takes the prose with it.
355
+ The parrot half is the `<pre>`, its CSS, and the caption reading straight off `node.raw`. No script runs per frame, and `prefers-reduced-motion` parks the bird on its first frame for free. It does have to do one thing, like every block wider than the text column: scroll inside your own box (`overflow-x: auto`, same as a code block or a table). The editor root scrolls, so an uncontained block pans the whole page sideways and takes the prose with it.
343
356
 
344
357
  And the full ten-frame dance? Go see [parrot-frames.md](plugin-guide/parrot-frames.md) for the actual frames; not gonna put them all here.
345
358
 
346
- **Install.** Pass the unit to the editor's `plugins` prop: build the array once at module scope, then `<Editor {source} {plugins} />` ([The plugin unit](#the-plugin-unit) shows the wiring and why module scope matters). This exact parrot also ships in the package, as `@voithos-labs/aragonite/plugins/parrot`, and a test keeps the shipped files identical to the fences above, so if you're building your own, rename it before the two meet. A `%%parrot` line now parses to your kind (`parse` is on the plugin path too, if you want to see it outside the editor):
359
+ **Install.** Pass the unit to the editor's `plugins` prop: build the array once at module scope, then `<Editor {source} {plugins} />` ([The plugin unit](#the-plugin-unit) shows the wiring and why module scope matters). This exact parrot also ships in the package, as `@voithos-labs/aragonite/plugins/parrot`, and a test keeps the shipped files identical to the fences above (give or take the import path and the full ten frames), so if you're building your own, rename it before the two meet. A `%%parrot` line now parses to your kind (`parse` is on the plugin path too, if you want to see it outside the editor):
347
360
 
348
361
  ```ts
349
362
  parse('%%parrot party responsibly\n').children[0];
@@ -392,12 +405,12 @@ Each part has a defined absence, which is prob the easiest way to remember what
392
405
 
393
406
  ### Registration is global, and register-once
394
407
 
395
- A kind is a definition every editor on the page shares, and it's defined exactly once. Registering the same kind, component, or opener twice **throws**, never silently overrides, whether you collided with a built-in or with another plugin. There's no unregister and no runtime replace. (If you've met the browser's `customElements.define`, it's the same model: one definition for the whole page, not one per document.)
408
+ A kind is a definition every editor on the page shares, and it's defined exactly once. Registering the same kind, component, or opener twice **throws**, never silently overrides, whether you collided with a built-in or with another plugin. There's no unregister, and the one way to change a kind you registered is `augmentBlockKind`, which merges extra descriptor fields in. (If you've met the browser's `customElements.define`, it's the same model: one definition for the whole page, not one per document.)
396
409
 
397
410
  Who guarantees a registration runs only once depends on where it runs:
398
411
 
399
412
  - **Inside a plugin unit** (the installable package the next section defines), `setup` runs at most once per process. Write each `register*` call straight; the unit owns the guarantee.
400
- - **At module scope**, meaning register calls that run when a file is imported, nothing owns the run for you. Guard each call on its probe, the matching is-it-there check: `isBlockKindDeclared`, `isBlockKindRegistered`, `isBlockComponentRegistered`, `isBlockOpenerRegistered`, `isBlockCompleterRegistered`, `isPasteTransformRegistered`, `isDirectiveRegistered`, and `isInlineKindDeclared` for the inline tier.
413
+ - **At module scope**, meaning register calls that run when a file is imported, nothing owns the run for you. Guard each call on its probe, the matching is-it-there check: `isBlockKindDeclared`, `isBlockKindRegistered`, `isBlockComponentRegistered`, `isBlockOpenerRegistered`, `isBlockCompleterRegistered`, `isPasteTransformRegistered`, `isLanguageRegistered`, `isDirectiveRegistered`, and `isInlineKindDeclared` for the inline tier.
401
414
 
402
415
  ```ts
403
416
  isBlockKindDeclared('parrot'); // false on a fresh page
@@ -407,15 +420,17 @@ isBlockKindDeclared('parrot'); // true, so a second import of this module skips
407
420
 
408
421
  Guard on the probe, never on a module-level `registered` flag: the flag survives `resetPluginPlatformForTests()` and then silently skips the re-registration your next test case needed, which is a fun half hour to spend.
409
422
 
410
- One dev-time softening. Under a dev server, re-evaluating a registration module replaces its prior registrations in place, so a changed definition takes effect on re-run (editing a plugin unit's own `definePlugin` still needs a page reload, and the replace covers every register-once registry, paste transforms included). Production builds and test runs keep the throw.
423
+ One dev-time softening. Under a dev server a duplicate registration replaces the earlier one in place, with a dev warning, so re-evaluating a registration module makes a changed definition take effect on re-run (editing a plugin unit's own `definePlugin` still needs a page reload, and the replace covers every register-once registry, paste transforms included). Production builds and test runs keep the throw.
411
424
 
412
425
  ### The plugin unit
413
426
 
414
427
  A **plugin unit** is the installable package: a name plus a `setup` that runs your `register*` calls.
415
428
 
416
- **`definePlugin({ name, setup })`**
429
+ **`definePlugin({ name, setup, version?, defaults?, parseOptions? })`**
430
+
431
+ Validates the unit at definition time (the name is a lowercase first letter followed by letters, digits, and hyphens, and `setup` has to be a function) and returns an `EditorPlugin`. `version` is only a label, printed by the warning when two units share a name. The other two optional fields are for options that can differ per editor: `defaults` is where every editor's options start, and `parseOptions` checks what an editor passes ([the options recipe](#recipe-per-instance-options-and-the-factory-closure-trap) has both).
417
432
 
418
- Validates the unit at definition time (the name is a lowercase first letter followed by letters, digits, and hyphens, and `setup` has to be a function) and returns an `EditorPlugin`. By convention you export a **factory**, meaning `export function myPlugin(deps?)` returns the unit, and the factory's argument carries any **process-global dependency** the plugin needs (a render engine, say, which is the same for every editor). Configuration that could differ per editor takes a different path ([One process, many editors](#one-process-many-editors)); the factory argument is only for what never varies between editors.
433
+ By convention you export a **factory**, meaning `export function myPlugin(deps?)` returns the unit. The factory's argument is where a **process-global dependency** comes in (a render engine, say, which is the same for every editor), and it can fill your `defaults` too. What it can't do is give two editors different values; that takes a different path ([One process, many editors](#one-process-many-editors)).
419
434
 
420
435
  ```ts
421
436
  export function myPlugin(options?: { renderer?: Renderer }): EditorPlugin {
@@ -436,8 +451,8 @@ Install by passing units to the editor's **`plugins` prop**, set once at mount,
436
451
  import { myPlugin } from './my-plugin';
437
452
 
438
453
  // Build the array once at module scope, not inline in the markup: an inline
439
- // `plugins={[myPlugin()]}` re-creates the unit every render, and the second render's
440
- // same-name/different-identity unit trips a harmless first-wins dev-warn.
454
+ // `plugins={[myPlugin()]}` builds a fresh unit for every editor that mounts, and each
455
+ // one after the first trips a harmless first-wins dev warning.
441
456
  const plugins = [myPlugin()];
442
457
  </script>
443
458
 
@@ -449,26 +464,28 @@ Install by passing units to the editor's **`plugins` prop**, set once at mount,
449
464
  - Passing the same unit again no-ops.
450
465
  - Passing a _different_ unit under a name already installed keeps the first and warns in a dev build, naming the loser as `name@version` when it carries one.
451
466
  - Units install in array order.
452
- - A `setup` that throws stays failed: a later attempt rethrows and tells you to reload, because a partial setup can't re-run against the register-once registries.
467
+ - A `setup` that throws stays failed. The throw comes out of the install (so out of the editor's mount), the units after it in the array don't install, and a later attempt rethrows and tells you to reload, because a partial setup can't re-run against the register-once registries.
453
468
  - Two editors passing the same plugin share one registration, but their _configuration_ isn't shared: an editor may pass `{ plugin, options }` and the plugin reads its own `options` off each instance ([the options recipe](#recipe-per-instance-options-and-the-factory-closure-trap)).
454
- - **The prop is also the enablement set.** Registration is process-wide; activation is not. An editor runs the `onEditor` hooks, resolves the kinds, answers the global commands and applies the paste transforms of exactly the plugins its own array lists. A plugin another editor on the page installed but this one left out does nothing here, and its blocks fall back to raw-editable text. An editor with no `plugins` prop at all is the exception: it activates everything installed.
469
+ - **The prop is also the enablement set.** Registration is process-wide; activation is not. An editor runs the `onEditor` hooks, resolves the kinds, answers the global commands and applies the paste hooks of exactly the plugins its own array lists. A plugin another editor on the page installed but this one left out does nothing here: its block and inline syntax read as the plain Markdown they are, its widgets show their source, its directive names open the generic directive block, and its completers never fire. An editor with no `plugins` prop at all (or an empty array) is the exception: it activates everything installed.
455
470
 
456
471
  Two smaller routes. For an editor-less `parse()` pipeline that needs the grammar live without mounting `<Editor>`, call `installPlugins(units)` from `@voithos-labs/aragonite`, with the same once-per-process semantics. And `isPluginInstalled(name)` probes an install, for the rare setup that has to branch on it; the prop and `installPlugins` are already safe to call twice, and most people never reach for it.
457
472
 
458
473
  ```ts
459
474
  import { installPlugins } from '@voithos-labs/aragonite';
475
+ import { isPluginInstalled } from '@voithos-labs/aragonite/plugin';
460
476
 
477
+ const parrot = parrotPlugin();
461
478
  isPluginInstalled('parrot'); // false
462
- installPlugins([parrotPlugin()]);
479
+ installPlugins([parrot]);
463
480
  isPluginInstalled('parrot'); // true
464
- installPlugins([parrotPlugin()]); // no-op
481
+ installPlugins([parrot]); // no-op (a fresh parrotPlugin() here would no-op too, with a dev warning)
465
482
  ```
466
483
 
467
484
  ### What is stable, what is not
468
485
 
469
- The API is going to freeze, and you deserve to know which half of it is already load-bearing.
486
+ The API is going to freeze, and you deserve to know which half of it has settled already.
470
487
 
471
- - **The registration base, stable.** Kind declaration, descriptor/component/opener registration, typed per-node metadata, and the probes above. These shapes won't change in a breaking way. (One exception already landed pre-freeze: an opener's return became a line count in 0.9.36, see [What an opener returns](#what-an-opener-returns).)
488
+ - **The registration base, settled.** Kind declaration, descriptor/component/opener registration, typed per-node metadata, and the probes above. The model won't change: which calls exist, that each registers once, what a kind is. The exact shapes those calls take (a descriptor field, what an opener returns) can still change before the freeze, and freeze with everything else at the public release.
472
489
  - **Pre-freeze, still moving.** Everything else. The [API reference](plugin-api.md) carries the list rather than this sentence: a section labelled _(pre-freeze / unstable)_ may still change shape until the freeze. Those labels are copied from the section headers of the `@voithos-labs/aragonite/plugin` entry point (`src/lib/plugin.ts` in the repository). The big families are the plugin unit itself, the authoring tiers (container, editable leaf, inline, directive), the grammar hooks, paste transforms, and the view surfaces (decorations, rects, selection geometry). Each is being refined against real consumers, and each freezes at the public release.
473
490
 
474
491
  After the freeze the version number carries the promise: a breaking change to a frozen surface rides a **major** version, and additive needs ship as **minors**.
@@ -480,11 +497,11 @@ Every surface that hands your plugin a node to **read** types it as a view: `Nod
480
497
  Two lists cover the whole read side:
481
498
 
482
499
  - **What the readonly covers:** `raw`, `kind`, `metadata` (the typed per-node data a plugin stores beside the bytes), trivia (the preserved blank-line bytes around a block, the `leadingTrivia` your parrot opener copied), and the children structure.
483
- - **Where views arrive:** `BlockComponentProps.node` / `document`, `EditorContext.document` (defined in the next section), a decoration source's `provide(document, …)`, the descriptor read hooks (`getContentRange`, `estimateHeight`, `reservedChrome.isCollapsed`, `reservedChrome.expandPatch`), and the command contexts.
500
+ - **Where views arrive:** `BlockComponentProps.node` / `document`, `EditorContext.document` (defined in the next section), a decoration source's `provide(document, …)`, the descriptor read hooks (`contentStart.range`, `estimateHeight`, `reservedChrome.isCollapsed`, `reservedChrome.expandPatch`), a write rule's `ctx.node`, the factories' `getNode()`, and the command and context-action contexts.
484
501
 
485
502
  `CstNode` and `Document` stay the shapes a plugin **constructs and owns**: an opener or directive factory builds a `CstNode`, and `rebuildRaw` receives one to write, because that call hands it an owned node, which is exactly when a byte write is legal. A document you parsed yourself is mutable, and feeds every view-typed parameter with no conversion.
486
503
 
487
- Mutating the **live** tree goes through the sanctioned commit paths: `updateOwnMetadata` (defined in the walkthrough), `rebuildRaw` (just below), and [Block commands](#block-commands). A **commit** is an edit the editor records as one undoable step. Never write through a view, and don't cast a view back to `CstNode` either: undo snapshots share nodes with the live tree, so a stray write through a cast corrupts history.
504
+ Mutating the **live** tree goes through the supported commit paths: `updateOwnMetadata` (defined in the walkthrough), `rebuildRaw` (just below), and [Block commands](#block-commands). A **commit** is an edit the editor records as one undoable step. Never write through a view, and don't cast a view back to `CstNode` either: undo snapshots share nodes with the live tree, so a stray write through a cast corrupts history.
488
505
 
489
506
  ### `rebuildRaw`, the write hook
490
507
 
@@ -505,11 +522,11 @@ function rebuildBoxRaw(node: CstNode): void {
505
522
 
506
523
  A directive container with a title line doesn't hand-write this at all: `createDirectiveRebuild` in the walkthrough does the same job with the fence bytes, the line ending and the title handled for you.
507
524
 
508
- The optional `changed` argument (`ChildRawChange`, shaped `{ index, previousRaw }`) is a performance opt-in: the index of the one child whose own raw just moved, plus the bytes that child held before. It exists for a container big enough that re-reading every child on every keystroke costs real time, and the built-in list and quote use it to re-emit that one child's region alone. Take it only if your kind can place a child's bytes inside its raw exactly, and keep those offsets in `node.childSpans`, the one cache the editor retires for you when its own bookkeeping moves a sibling's line. Offsets you cache anywhere else are yours to invalidate; nothing in the editor is watching them. The conformance kit compares the two paths for your kind either way.
525
+ The optional `changed` argument (`ChildRawChange`, shaped `{ index, previousRaw }`) is a performance opt-in: the index of the one child whose own raw just moved, plus the bytes that child held before. It exists for a container big enough that re-reading every child on every keystroke costs real time, and the built-in list and quote use it to re-emit that one child's region alone. Take it only if your kind can place a child's bytes inside its raw exactly, and keep those offsets in `node.childSpans` (a start and an end offset per child), the one cache the editor retires for you when its own bookkeeping moves a sibling's line; a span that no longer matches falls back to the full rebuild. Offsets you cache anywhere else are yours to invalidate; nothing in the editor is watching them. The conformance kit compares the two paths for your kind either way.
509
526
 
510
527
  ## One process, many editors
511
528
 
512
- `setup` runs once per process, but a plugin usually needs to react to _each editor_: recompute derived state on every edit, hold per-document data, read the options a given editor passed. `ctx.onEditor(cb)` is that entry point. It registers a callback fired once per mounted `<Editor>` that listed your plugin, handed that instance's **`EditorContext`**:
529
+ `setup` runs once per process, but a plugin usually needs to react to _each editor_: recompute derived state on every edit, hold per-document data, read the options a given editor passed. `ctx.onEditor(cb)` is that entry point. It registers a callback fired once per mounted `<Editor>` that listed your plugin (or that has no `plugins` prop, since those activate everything), handed that instance's **`EditorContext`**:
513
530
 
514
531
  ```ts
515
532
  setup(ctx) {
@@ -523,18 +540,24 @@ setup(ctx) {
523
540
  }
524
541
  ```
525
542
 
526
- | Field | What it gives you |
527
- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
528
- | `editorId` | A stable per-mount id. Key your own `Map` / `WeakMap` on it for per-editor state |
529
- | `document` | A live getter for the root document, as a read-only `DocumentView` ([Views](#views-what-you-read-what-you-own)) |
530
- | `events` | The subscribe-only event view; `events.on('edit', …)` returns a disposer |
531
- | `options` | The options this editor passed, typed once you write `definePlugin<Options>` (recipe below) |
532
- | `decorations` | This editor's decoration registry, where you register a source ([Decorations](#decorations)) |
533
- | `rects` | This editor's viewport-space geometry: block box, range rects, caret, reveal, navigation |
534
- | `presentationMode` | The effective presentation mode, live, paired with the `presentationModeChange` event ([Presentation modes](#presentation-modes)) |
535
- | `theme` | The editor's theme name, live, paired with the `themeChange` event, for content whose colors an engine paints |
536
-
537
- Return a disposer from the callback and the editor runs it at unmount. Registration is synchronous-only: call `onEditor` from `setup`, not from some later callback.
543
+ | Field | What it gives you |
544
+ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
545
+ | `editorId` | A stable per-mount id. Key your own `Map` / `WeakMap` on it for per-editor state |
546
+ | `document` | A live getter for the root document, as a read-only `DocumentView` ([Views](#views-what-you-read-what-you-own)) |
547
+ | `documentGeneration` | How many times a `source` write has replaced the document, live but not reactive: subscribe to the `sourceSwap` event to hear a change |
548
+ | `events` | The subscribe-only event view; `events.on('edit', …)` returns a disposer |
549
+ | `options` | Your `defaults` with this editor's options merged over them (just the defaults if it passed none), typed by your `defaults` or by `definePlugin<Options>` (recipe below) |
550
+ | `decorations` | This editor's decoration registry, where you register a source ([Decorations](#decorations)) |
551
+ | `rects` | This editor's viewport-space geometry: block box, range rects, caret, reveal, `scrollTo`, `navigateTo` |
552
+ | `inlineMenus` | This editor's registry for lists opened by a typed trigger ([Recipe: a typed-trigger menu](consumer-guide.md#recipe-a-typed-trigger-menu)) |
553
+ | `insertCatalogue` | The blocks this editor's insert menus offer, live, yours included once you `registerInsertEntry` from `setup` |
554
+ | `insertMarkdown(md, options?)` | Insert Markdown the way the instance's own call does ([Inserting Markdown at the caret](consumer-guide.md#inserting-markdown-at-the-caret)); a promise that resolves false where that call would |
555
+ | `runCommand(id, arg?)` | Run a command by id the way the instance's own call does; false where that would be |
556
+ | `computeInlineContent(node)` | Parse a prose block's inline content the way this editor draws it: syntax from a plugin its `plugins` prop left out comes back as plain text, and reference links resolve against the document's link definitions (its `[r]: /x` lines). Reach for it wherever you walk inline nodes |
557
+ | `presentationMode` | The effective presentation mode, live, paired with the `presentationModeChange` event ([Presentation modes](#presentation-modes)) |
558
+ | `theme` | The editor's theme name, live, paired with the `themeChange` event, for content whose colors an engine paints |
559
+
560
+ Return a disposer from the callback and the editor runs it at unmount. Registration is synchronous-only: call `onEditor` from `setup`, since a call after `setup` returns throws.
538
561
 
539
562
  ### Recipe: per-instance derived state
540
563
 
@@ -560,10 +583,10 @@ function recount(editor: EditorContext<WordCountOptions>): void {
560
583
 
561
584
  export const wordCountPlugin = definePlugin<WordCountOptions>({
562
585
  name: 'word-count',
586
+ defaults: { live: true }, // what a bare-unit install reads
563
587
  setup(ctx) {
564
588
  ctx.onEditor((editor) => {
565
- // A bare-unit install passes no options, so default them.
566
- const { live } = editor.options ?? { live: true };
589
+ const { live } = editor.options;
567
590
  recount(editor); // seed on mount
568
591
  const off = live ? editor.events.on('edit', () => recount(editor)) : () => {};
569
592
  return () => {
@@ -584,9 +607,35 @@ Two editors share one process-global registration but may still want different o
584
607
  <Editor source={right} plugins={[{ plugin: wordCountPlugin, options: { live: false } }]} />
585
608
  ```
586
609
 
587
- `definePlugin<WordCountOptions>` carries the type through, so `editor.options` reads typed inside `onEditor` with no cast.
610
+ Whatever an editor passes lands on your `defaults` one field at a time. A field it passes replaces yours whole (an array too, nothing gets concatenated), and a field it leaves out keeps its default. The bundled slash commands plugin shows it best, since its factory argument is its `defaults`:
588
611
 
589
- **The trap.** Don't hold per-instance config in the plugin factory's closure. `wordCountPlugin({ live: false })` looks like it configures the instance, but a plugin installs once per process, so only the first editor's factory value ever takes effect and the second is silently ignored. The question that decides it: _would two editors ever want different values?_ If yes, it's per-instance: pass it through the prop entry and read `editor.options`. If no (a render engine, a shared parser), the factory argument is the right home.
612
+ ```ts
613
+ const stamp = { id: 'stamp', label: 'Stamp', insert: 'approved' }; // one host row
614
+ slashCommandsPlugin({ entries: [stamp] }); // defaults: { entries: [stamp] }
615
+
616
+ // this editor's options editor.options
617
+ // (none, a bare unit) { entries: [stamp] }
618
+ // { exclude: ['table'] } { entries: [stamp], exclude: ['table'] }
619
+ // { entries: [] } { entries: [] }
620
+ ```
621
+
622
+ `definePlugin<WordCountOptions>` carries the type through, so `editor.options` reads typed inside `onEditor` with no cast. The type is your word, though, not a check. What checks is **`parseOptions(raw)`**: it gets an editor's options exactly as the host wrote them (once per editor, and only if the host wrote some) and returns the fields to apply. Leave a field out and it keeps its default. Throw, and the editor reports it on its `error` event (origin `subscriber`, naming your plugin) and runs your plugin on its defaults, so somebody's typo never takes their document down.
623
+
624
+ ```ts
625
+ export const wordCountPlugin = definePlugin<WordCountOptions>({
626
+ name: 'word-count',
627
+ defaults: { live: true },
628
+ parseOptions(raw) {
629
+ const live = (raw as Partial<WordCountOptions> | null)?.live;
630
+ return typeof live === 'boolean' ? { live } : {}; // { live: 'yes' } keeps live: true
631
+ },
632
+ setup(ctx) {
633
+ /* the recipe above */
634
+ }
635
+ });
636
+ ```
637
+
638
+ **The trap.** Don't hold per-instance config in the plugin factory's closure. `wordCountPlugin({ live: false })` looks like it configures the instance, but a plugin installs once per process, so only the first editor's factory value ever takes effect and the second is ignored (a dev build warns; production says nothing). The question that decides it: _would two editors ever want different values?_ If yes, it's per-instance: pass it through the prop entry and read `editor.options`. If no (a render engine, a shared parser), the factory argument is the right home. A factory argument that fills `defaults` is fine too: it's every editor's starting value, and each editor's entry can still override it.
590
639
 
591
640
  ## Walkthrough: a `:::conspiracy` container end to end
592
641
 
@@ -614,6 +663,7 @@ import {
614
663
  registerChromeLeaf,
615
664
  registerDirective,
616
665
  setPluginMetadata,
666
+ trimWhitespace,
617
667
  type CstNode,
618
668
  type EditorPlugin,
619
669
  type ParsedDirective
@@ -635,7 +685,7 @@ export interface ConspiracyMetadata {
635
685
  // from the opener line); children 1+ are the parsed evidence. The fence bytes go to
636
686
  // metadata so the raw can be rebuilt after an edit.
637
687
  function conspiracyFromDirective(parsed: ParsedDirective): CstNode {
638
- const theory = parsed.fence.info.trim();
688
+ const theory = trimWhitespace(parsed.fence.info);
639
689
  const node: CstNode = {
640
690
  kind: declaredPluginKind(CONSPIRACY),
641
691
  leadingTrivia: parsed.leadingTrivia,
@@ -684,7 +734,7 @@ function registerConspiracy(): void {
684
734
  }
685
735
  }
686
736
 
687
- // A block command that flips the verdict. updateMetadata is the sanctioned
737
+ // A block command that flips the verdict. updateMetadata is the supported
688
738
  // commit path: it merges the patch, runs rebuildRaw, and makes one undoable edit;
689
739
  // because the name flows into raw, the verdict survives a round-trip.
690
740
  const setVerdict = registerBlockCommand(conspiracy, 'conspiracy.setVerdict', (ctx) => {
@@ -709,26 +759,26 @@ function registerConspiracy(): void {
709
759
  // trips a dev assertion the moment someone edits a conspiracy with a blank first line.
710
760
  bodyWrap: DIRECTIVE_BODY_WRAP,
711
761
  reservedChrome: { kind: conspiracyTitle },
712
- // Child 0 is the title, so Backspace at its start must not lift it out of the
713
- // conspiracy. A container whose child 0 is body lifts instead:
714
- // `'lift-first-child-keep-container'`, or `'-drop-opener'` for a quote shape.
715
- unwrapRole: {
716
- firstChildBackspace: 'keep-reserved-chrome',
717
- middleChildBackspace: 'default-merge'
718
- }
762
+ // Child 0 is the title, and Backspace at its start never lifts it out, so you only
763
+ // say what Backspace does between body children. A container whose child 0 is body
764
+ // also picks a first-child strategy: `'lift-first-child-keep-container'`,
765
+ // `'lift-first-child-drop-opener'` for a quote shape, or `'list-item-cascade'`.
766
+ unwrapRole: { middleChildBackspace: 'default-merge' }
719
767
  // Declare `reorderChildren` here if your container's direct children should
720
- // reorder among themselves (drag, or Alt+ArrowUp/ArrowDown). Absent, a child's
721
- // reorder resolves at an ancestor instead, which moves the whole container
722
- // among its own siblings. The closure block does not ask about this axis, and
723
- // a behavioural test on your container passes either way.
768
+ // reorder among themselves (drag, or Alt+ArrowUp/ArrowDown). Absent, a direct
769
+ // child's reorder declines at an opaque container like this one (a strip container
770
+ // passes it up to the nearest ancestor that declares one, or the root). The closure
771
+ // block does not ask about this axis, and a behavioural test passes either way.
724
772
  },
773
+ // The Markdown the conformance kits parse: a top-level conspiracy with a title and a body.
774
+ conformanceFixture: ':::conspiracy Birds are drones\nthey never land near me\n:::\n',
725
775
  keymap: [
726
776
  { chord: 'Mod+7', command: setVerdict, arg: 'conspiracy' }, // allege
727
777
  { chord: 'Mod+8', command: setVerdict, arg: 'debunked' } // debunk
728
778
  ],
729
779
  // Required: how this kind behaves under every cross-cutting editor system. A missing
730
- // cell or column is a compile error, and four more rules are checked when the editor
731
- // boots. See the guide's "The closure block" section for all of them.
780
+ // cell or column is a compile error, and a dev build warns on four more rules when an
781
+ // editor mounts. See the guide's "The closure block" section for all of them.
732
782
  closure: {
733
783
  roundTrip: { mode: 'implemented', via: 'container contract=opaque, rebuildConspiracyRaw' },
734
784
  focus: { mode: 'implemented', via: 'focus walks to the title chrome / first body child' },
@@ -749,14 +799,15 @@ function registerConspiracy(): void {
749
799
  mode: 'implemented',
750
800
  via: 'byte-slice copy; a slice touching the title re-emits the conspiracy around the collected body'
751
801
  },
752
- // `inherit-default` is the honest answer unless you actually run a corruption
753
- // oracle over your kind. Claiming a mechanism you do not have is worse than
802
+ // `inherit-default` is the honest answer unless you actually run corruption
803
+ // checks over your kind. Claiming a mechanism you do not have is worse than
754
804
  // admitting you inherit the generic one.
755
805
  simOracle: { mode: 'inherit-default' }
756
806
  }
757
807
  });
758
808
 
759
- registerChromeLeaf(conspiracyTitle, { blockClass: 'conspiracy-title' });
809
+ // The label is what a screen reader and the block menu call the title row.
810
+ registerChromeLeaf(conspiracyTitle, { label: 'Theory', blockClass: 'conspiracy-title' });
760
811
  }
761
812
 
762
813
  // definePluginBlock wraps definePlugin around the register step and the component
@@ -839,8 +890,8 @@ Your component supplies only its own chrome: the border, the title styling, an i
839
890
  .conspiracy-block {
840
891
  /* the corkboard, with one piece of red string */
841
892
  position: relative;
842
- border: 1px solid var(--color-ui-muted, #a4a4a4);
843
- border-left: 3px solid var(--color-error, #e06c75);
893
+ border: 1px solid var(--color-ui-muted, #93938d);
894
+ border-left: 3px solid var(--color-error, #ff5f57);
844
895
  border-radius: 6px;
845
896
  padding: 8px 12px;
846
897
  }
@@ -849,7 +900,7 @@ Your component supplies only its own chrome: the border, the title styling, an i
849
900
  }
850
901
  /* debunked: the string comes down, the theory gets crossed out, the stamp lands */
851
902
  .debunked {
852
- border-left-color: var(--color-ui-muted, #a4a4a4);
903
+ border-left-color: var(--color-ui-muted, #93938d);
853
904
  }
854
905
  .debunked :global(.conspiracy-title) {
855
906
  text-decoration: line-through;
@@ -862,7 +913,7 @@ Your component supplies only its own chrome: the border, the title styling, an i
862
913
  transform: rotate(-12deg);
863
914
  font: 600 0.75em monospace;
864
915
  letter-spacing: 0.12em;
865
- color: var(--color-error, #e06c75);
916
+ color: var(--color-error, #ff5f57);
866
917
  border: 2px solid currentColor;
867
918
  border-radius: 3px;
868
919
  padding: 1px 6px;
@@ -872,29 +923,36 @@ Your component supplies only its own chrome: the border, the title styling, an i
872
923
 
873
924
  Three rules for that file, each earned the hard way:
874
925
 
875
- - **`export { containerApi }` is the whole publication.** That one instance export is your block's `BlockComponent` surface, and the editor resolves a container reference through it. Both the name and the shape are fixed: the component registry types a block's exports as either a leaf surface or a container's `containerApi`, and the container branch is `ContainerBlockComponent`, which requires the descent verbs (`focusByPath`, `revealByPath`, `parkCaret` and the rest; a caret entering a container has to descend, so they aren't optional the way a leaf's extras are). Omitting the export, or publishing a surface missing one of them, fails your typecheck (svelte-check, or `tsc` on a plain-TypeScript plugin) at the call that registers your component (`definePluginBlock` here, `registerBlockComponent` if you register by hand). The factory's surface satisfies all of it by construction; a hand-rolled one can annotate itself `satisfies ContainerBlockComponent` to get the same error at the definition instead of at the registration.
926
+ - **`export { containerApi }` is the whole publication.** That one instance export is your block's `BlockComponent` surface, and the editor resolves a container reference through it. Both the name and the shape are fixed: the component registry types a block's exports as either a leaf surface or a container's `containerApi`, and the container branch is `ContainerBlockComponent`, which requires the descent members (`focusByPath`, `parkCaret`, `childList` and the rest; a caret entering a container has to descend, so they aren't optional the way a leaf's extras are). Omitting the export, or publishing a surface missing one of them, fails your typecheck (svelte-check, or `tsc` on a plain-TypeScript plugin) at the call that registers your component (`definePluginBlock` here, `registerBlockComponent` if you register by hand). The factory's surface satisfies all of it by construction; a hand-rolled one can annotate itself `satisfies ContainerBlockComponent` to get the same error at the definition instead of at the registration.
876
927
  - **`BlockList` stays a _direct_ child of your box**, so the container's windowing finds it. Other chrome (an icon, a toggle button) may sit beside it.
877
- - **Chrome CSS reads the editor's theme tokens**, with an inline fallback on every read (`var(--color-ui-muted, #a4a4a4)`), so the block still renders outside the editor's own style scope. Match the fallback to the token's dark value; dark is the base theme. The stable token set by role is the [consumer guide's theme-token manifest](consumer-guide.md#theme-tokens).
928
+ - **Chrome CSS reads the editor's theme tokens**, with an inline fallback on every read (`var(--color-ui-muted, #93938d)`), so the block still renders outside the editor's own style scope. Match the fallback to the token's dark value; dark is the base theme. The stable token set by role is the [consumer guide's theme-token manifest](consumer-guide.md#theme-tokens).
878
929
 
879
930
  The factory returns more than the walkthrough destructures:
880
931
 
881
- | Return | When you reach for it |
882
- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
883
- | `updateOwnMetadata` | Your component writes its own node's metadata (a collapse toggle, an edited setting). The sanctioned commit path; in reading mode, which writes no bytes, it declines as a no-op and dev builds warn |
884
- | `moveFocusOut` | A plugin-owned editing surface whose caret ran off its own edge; hands the caret to the neighbour a plain arrow points at, through the editor's focus traversal, so the landing skips non-focusable blocks, enters containers, and reveals an unmounted target like any other arrow |
885
- | `getPresentationMode` | Your rendering or a gesture needs the live presentation mode ([Presentation modes](#presentation-modes)) |
886
- | `getTheme` | Your content's colors are painted by an engine rather than styled by CSS; token-styled chrome needs neither this nor `getPresentationMode`, it rethemes through the cascade |
887
- | `getOptions` | This editor instance's options for the plugin owning your kind, typed `unknown`; the per-instance channel a factory argument can't reach ([the options recipe](#recipe-per-instance-options-and-the-factory-closure-trap)) |
932
+ | Return | When you reach for it |
933
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
934
+ | `updateOwnMetadata` | Your component writes its own node's metadata (a collapse toggle, an edited setting). The supported commit path; in reading mode, which writes no bytes, it declines as a no-op and dev builds warn. If the edit should move the caret (a collapse hiding the child it sat in), pass `{ caret: { path, offset } }` and the commit puts it there once it renders. `path` is child indices from your block (`[]` for the block itself, `[0]` for its first child), and `offset` is a character offset into that child's text, or `CURSOR_END` for its end. Don't focus anything yourself afterwards |
935
+ | `moveFocusOut` | A plugin-owned editing surface whose caret ran off its own edge; hands the caret to the neighbour a plain arrow points at, through the editor's focus traversal, so the landing skips non-focusable blocks, enters containers, and reveals an unmounted target like any other arrow |
936
+ | `getPresentationMode` | Your rendering or a gesture needs the live presentation mode ([Presentation modes](#presentation-modes)) |
937
+ | `getTheme` | Your content's colors are painted by an engine rather than styled by CSS; token-styled chrome needs neither this nor `getPresentationMode`, it rethemes through the cascade |
938
+ | `getOptions` | This editor's options for the plugin that owns your kind, your `defaults` included, typed `unknown` (it's shorthand for `getEditor()?.options`). It's how a value differs per editor, which a factory argument can't do ([the options recipe](#recipe-per-instance-options-and-the-factory-closure-trap)) |
939
+ | `getEditor` | This editor's `EditorContext` for the plugin that owns your kind, undefined only in a bare test harness. Its `computeInlineContent` reads the syntax this editor draws. If your helper's reader defaults to the free `computeInlineContent`, pass it `getEditor()?.computeInlineContent` and it still parses in a bare harness (the bundled toc and footnotes do exactly this) |
940
+ | `captureScrollPosition` | Your component is about to swap its view for one of a different height (a tall diagram for its short source card) and the reader is scrolled right at it. Call it before the swap, await what it hands back after, and the page stays where the reader left it instead of clamping to the shorter layout in between |
888
941
 
889
942
  ```ts
890
- const { updateOwnMetadata, getPresentationMode, getTheme, getOptions } = createContainerBlock(deps);
943
+ const { updateOwnMetadata, getPresentationMode, getTheme, getOptions, captureScrollPosition } =
944
+ createContainerBlock(deps);
891
945
  updateOwnMetadata({ name: 'debunked' }); // one undo entry; rebuildRaw re-emits the opener line as :::debunked
946
+ updateOwnMetadata({ open: false }, { caret: { path: [0], offset: 0 } }); // ...and the caret lands on child 0's start
892
947
  getPresentationMode(); // 'source'
893
948
  getTheme(); // 'dark'
894
- getOptions(); // whatever this editor's { plugin, options } entry carried; undefined for a bare unit
949
+ getOptions(); // your defaults with this editor's { plugin, options } entry merged over them
950
+ const restore = captureScrollPosition(); // before the swap...
951
+ editing = true;
952
+ await restore(); // ...and after; a no-op when nothing moved
895
953
  ```
896
954
 
897
- One dep is worth knowing about too. A marker-bearing container (a footnote definition's `[^label]: `, mirroring a list item's `- `) hands the factory a **`getAmbientPrefix`** getter. Its first child then paints that prefix as a dimmed, read-only run before its own bytes, and the caret and offset walk skip it exactly as they do a list marker. Read it live, so a marker derived from metadata re-renders after an edit. Return a string, or `{ text, interactive }` to make ranges of it clickable: each range gets its own span, class and click handler, which is how a task list's checkbox toggles and how a footnote definition's `[^label]` takes the click back to its reference.
955
+ One dep is worth knowing about too. A marker-bearing container (a footnote definition's `[^label]: `, mirroring a list item's `- `) hands the factory a **`getAmbientPrefix`** getter. Its first child then paints that prefix as a dimmed, read-only run before its own bytes, and the caret and offset walk skip it exactly as they do a list marker. Read it live, so a marker derived from metadata re-renders after an edit. Return a string, or `{ text, interactive }` to make ranges of it clickable: each range gets its own span, class and click handler, which is how a task list's checkbox toggles and how a footnote definition's `[^label]` takes the click back to its reference. A range can also carry a role, a `label`, and a tab stop (`focusable`) together with the `onActivate` that Enter and Space run (the type won't let you declare `focusable` without it). Decide `focusable` by mode: a tab stop inside an editable block gets in the caret's way, which is why the footnote marker only takes one in reading mode.
898
956
 
899
957
  ### Wire it into a page
900
958
 
@@ -941,17 +999,17 @@ Want a collapse toggle? Give `reservedChrome` an `isCollapsed` probe over the no
941
999
 
942
1000
  ## The closure block
943
1001
 
944
- `closure` is a required field on every registration: the kind's written answer to each cross-cutting editor system, so a new kind can't ship closed under a subsystem nobody asked about. (The incident behind the field is the 0.9.18 whole-block-focus tier.) Each of the nine `ClosureColumn`s (`roundTrip`, `focus`, `mergeBackspace`, `selectionPaint`, `searchPaint`, `reorder`, `undo`, `clipboard`, `simOracle`) takes a `ClosureCell`:
1002
+ `closure` is a required field on every registration: the kind's written answer to each cross-cutting editor system, so a new kind can't ship silently broken under a subsystem nobody asked about. Each of the nine `ClosureColumn`s (`roundTrip`, `focus`, `mergeBackspace`, `selectionPaint`, `searchPaint`, `reorder`, `undo`, `clipboard`, `simOracle`) takes a `ClosureCell`:
945
1003
 
946
1004
  - `{ mode: 'implemented', via }`: a real mechanism you can name (a `rebuildRaw`, a keymap command, `measurePartialRects`).
947
1005
  - `{ mode: 'inherit-default' }`: the generic editor behaviour, nothing kind-specific.
948
1006
  - `{ mode: 'not-supported', reason }`: the subsystem is structurally absent, so name the degradation.
949
1007
 
950
- The type does the nagging: `Record<ClosureColumn, …>` makes a missing column a compile error, and the required field makes a missing block one. Four coherence rules also hold when the editor boots:
1008
+ The type does the nagging: `Record<ClosureColumn, …>` makes a missing column a compile error, and the required field makes a missing block one. Four coherence rules are checked too, by dev-build warnings when an editor mounts (or, for a kind registered after that, at the next parse). Nothing throws, a production build doesn't check, and a kind only ever registered in a headless test or an `installPlugins` + `parse` pipeline is never checked at all:
951
1009
 
952
- 1. A container must declare `roundTrip: implemented`; its `rebuildRaw` is the mechanism.
1010
+ 1. A container can't declare `roundTrip: inherit-default`; its `rebuildRaw` is the mechanism.
953
1011
  2. A `not-mergeable` kind can't declare `mergeBackspace: inherit-default`; it has no default merge to inherit.
954
- 3. A cell claiming the focus-then-delete model must be backed by `blockFocus: 'whole-block'`.
1012
+ 3. A cell claiming the focus-then-delete model (a `focus` or `mergeBackspace` `via` saying `focus-then-delete` or `a second press deletes`) must be backed by `blockFocus: 'whole-block'`.
955
1013
  4. A kind declaring `reservedChrome` can't leave `clipboard: inherit-default`; the chrome bytes live in the container's own raw, so the default byte slice is wrong for it.
956
1014
 
957
1015
  Those four plus the nine columns are the whole contract.
@@ -984,7 +1042,7 @@ simpleLeafClosure({ focus, searchPaint, undo, simOracle });
984
1042
  // clipboard: { mode: 'inherit-default' }
985
1043
  ```
986
1044
 
987
- **`simOracle` is the cell most authors hesitate over**, because the simulation suite is a repo script rather than a published kit. It answers the same way every other column does; the question is about your **mechanism**, not about who runs the tests. The example above is `implemented` because that kind has its own end-to-end tests driving it under the corruption oracles (the simulation's checks for a document gone wrong). A plugin that adds no kind-specific simulation machinery writes `inherit-default`, which is the honest answer for most plugins and what several bundled kinds declare. `inherit-default` claims no coverage; it says your kind meets the simulation exactly as the generic behaviour does. `not-supported` is for a subsystem that's structurally absent, which a caret-bearing kind's simulation never is.
1045
+ **`simOracle` is the cell most authors hesitate over**, because the simulation suite is a repo script rather than a published kit. It answers the same way every other column does; the question is about your **mechanism**, not about who runs the tests. The example above is `implemented` because that kind has its own end-to-end tests driving it under the simulation's corruption checks (its checks for a document gone wrong). A plugin that adds no kind-specific simulation machinery writes `inherit-default`, which is the honest answer for most plugins and what several bundled kinds declare. `inherit-default` claims no coverage; it says your kind meets the simulation exactly as the generic behaviour does. `not-supported` is for a subsystem that's structurally absent, which a caret-bearing kind's simulation never is.
988
1046
 
989
1047
  **Containers with real children: `containerClosure`.** A container of real child blocks answers four columns the same structural way (its children are the paint and search surfaces, it reorders whole-block through the parent `BlockList`, and it holds no clipboard anchor of its own), and its `roundTrip` is always `implemented`, because its `rebuildRaw` is the mechanism. `containerClosure` bakes those, asking for the `roundTripVia` string plus the four the container determines: `focus`, `mergeBackspace`, `undo`, `simOracle`. Here's the walkthrough's closure rewritten on it:
990
1048
 
@@ -997,7 +1055,7 @@ closure: containerClosure({
997
1055
  mode: 'implemented',
998
1056
  via: 'updateMetadata; the verdict flip commits as one undo entry'
999
1057
  },
1000
- // The conspiracy declares reservedChrome, so coherence rule four refuses the baked
1058
+ // The conspiracy declares reservedChrome, so coherence rule four warns about the baked
1001
1059
  // clipboard cell; a container without reserved chrome just leaves this out.
1002
1060
  clipboard: {
1003
1061
  mode: 'implemented',
@@ -1007,7 +1065,7 @@ closure: containerClosure({
1007
1065
  });
1008
1066
  ```
1009
1067
 
1010
- A container that synthesizes content on copy overrides the baked `clipboard` cell the same way; one that adds an indent gesture overrides the baked `reorder` cell. Whole-block-focus opaque leaves and any novel tier still hand-write the full nine, which is where the 0.9.18 lesson applies.
1068
+ A container that synthesizes content on copy overrides the baked `clipboard` cell the same way; one that adds an indent gesture overrides the baked `reorder` cell. Whole-block-focus opaque leaves and any novel tier still hand-write the full nine, since no preset knows what they do.
1011
1069
 
1012
1070
  ## Teaching the parser
1013
1071
 
@@ -1034,11 +1092,9 @@ tryOpen(ctx) {
1034
1092
 
1035
1093
  The scanners the package exports hand back positions rather than deltas, because their result is a slice bound: `blockquoteExtent` returns a `nextIndex`, and your opener subtracts once at its own return.
1036
1094
 
1037
- > **Migrating from `nextIndex` (pre-1.0 breaking change).** An opener used to return the absolute index to resume at. Return the delta instead: `{ node, nextIndex: ctx.index + 1 }` becomes `{ node, consumed: 1 }`.
1038
-
1039
1095
  ### Opener priority
1040
1096
 
1041
- An opener's `priority` decides dispatch order, and **lower runs first**. `OPENER_PRIORITIES` is the built-in ladder (a readonly map, the same constant the built-ins register with):
1097
+ An opener's `priority` decides dispatch order, and **lower runs first**. `OPENER_PRIORITIES` is the built-in priority order (a readonly map, the same constant the built-ins register with):
1042
1098
 
1043
1099
  | Priority | Built-in kind |
1044
1100
  | -------: | ------------------------- |
@@ -1058,7 +1114,7 @@ Two rules place a plugin opener on it:
1058
1114
 
1059
1115
  Ties break by kind name, never by registration order. A shared priority is a smell all the same, and the dev build warns on it. Price into a gap instead.
1060
1116
 
1061
- **Claiming ahead of a built-in is also how you replace one.** Price your kind below the built-in whose syntax you want (the Mermaid fence is exactly this), and your kind owns those bytes: its own component, its own descriptor, its own closure row. It's uninstall-safe by construction, because the built-in opener never left the ladder: remove your plugin and it takes the bytes back unchanged. There's no registry-level override of a built-in's component or descriptor, deliberately. Registries are process-global, so an override would be global and last-writer-wins.
1117
+ **Claiming ahead of a built-in is also how you replace one.** Price your kind below the built-in whose syntax you want (the Mermaid fence is exactly this), and your kind owns those bytes: its own component, its own descriptor, its own closure row. It's uninstall-safe by construction, because the built-in opener never left the priority order: remove your plugin and it takes the bytes back unchanged. There's no registry-level override of a built-in's component or descriptor, deliberately. Registries are process-global, so an override would be global and last-writer-wins.
1062
1118
 
1063
1119
  For your pricing map: the opt-in `:::name` directive grammar registers its container opener at 45, between `blockquote` and `list`.
1064
1120
 
@@ -1081,7 +1137,7 @@ tryOpen(ctx) {
1081
1137
 
1082
1138
  Three habits complete the gate:
1083
1139
 
1084
- - **The flag stays constant through nested container recursion**, so `depth` is what tells you a blockquote or list body isn't the document top. `parseContainerBody` takes the scope as a required argument for the same reason `parse` accepts one: a body is a new parse entry, and nothing in it can recover the scope. An opener reparsing a body that stays inside the dispatching parse passes its own (`ctx.isDocumentParse ? 'document' : 'fragment'`, plus `depth: ctx.depth + 1`); one that re-enters with a body it assembled itself passes `'fragment'`.
1140
+ - **The flag stays constant through nested container recursion**, so `depth` is what tells you a blockquote or list body isn't the document top. `parseContainerBody` takes the scope as a required argument for the same reason `parse` accepts one: a body is a new parse entry, and nothing in it can recover the scope. An opener reparsing a body that stays inside the dispatching parse passes its own (`ctx.isDocumentParse ? 'document' : 'fragment'`, plus `depth: ctx.depth + 1`), and `grammar: ctx.grammar`, so the editor's switches reach the body; one that re-enters with a body it assembled itself passes `'fragment'`.
1085
1141
  - **Declare `interruptsParagraph: false`**: a line that interrupts a paragraph has a paragraph before it, so it's never at line 0.
1086
1142
  - **Pair the opener with a paste transform** ([Paste transforms](#paste-transforms)): pasted text reaches `parse` as a fragment, so your opener declines it, and the transform is where you decide what pasted front matter should become (a fenced block, say) instead of leaving the syntax live mid-document.
1087
1143
 
@@ -1094,17 +1150,17 @@ An opener recognizes syntax that's already there. A grammar whose lines must be
1094
1150
  ```ts
1095
1151
  registerBlockCompleter(myKind, {
1096
1152
  tryComplete: (line) =>
1097
- line.trim() === '$$'
1153
+ trimWhitespace(line) === '$$'
1098
1154
  ? { lines: ['$$', '', '$$'], caret: { path: [], line: 1, column: 0 } }
1099
1155
  : null
1100
1156
  });
1101
1157
  ```
1102
1158
 
1103
- What the editor guarantees before your `tryComplete` is called: the block is a single line of prose whose every byte is content, and the caret sits at its end. So the line you receive is the whole typed line and never a kind's own markers. Return `null` to decline; the press then splits as usual. Claims are consulted in kind-name order, never registration order.
1159
+ What the editor guarantees before your `tryComplete` is called: the block is a single line of prose whose every byte is content, and the caret sits at its end. So the line you receive is the whole typed line and never a kind's own markers. Return `null` to decline; the press then splits as usual (a claim whose lines would render nothing is declined the same way). Claims are consulted in kind-name order, never registration order.
1104
1160
 
1105
- With that completer registered, typing `$$` into an empty paragraph and pressing Enter leaves the document holding `$$\n\n$$\n`, with the caret on the empty middle line, ready for the formula.
1161
+ With that completer registered, typing `$$` into an empty paragraph and pressing Enter leaves the document holding `$$\n\n$$\n`, with the caret on the empty middle line, ready for the formula. Add `onType: true` beside `tryComplete` and it's also tried as the line is typed, no Enter needed, which suits a line that means one thing the moment it's complete (a lone `$$`). It's off by default, since a table's header row might be a longer row someone's still typing.
1106
1162
 
1107
- Answer `lines` **without** line endings, because the editor attaches the editing block's own, so a CRLF document stays CRLF. Answer the caret as a `path` (child indices inside the completed block, empty for the block itself) plus a `line` and `column` inside that node, never a byte offset: the line ending is picked after your claim, so only the editor can count bytes. The claim lands as one block replacement and one undo entry; one undo restores the typed line with the caret back at its end, and pressing Enter there completes again.
1163
+ Answer `lines` **without** line endings, because the editor attaches the editing block's own, or the document's when the block is a last line with none, so a CRLF document stays CRLF. Answer the caret as a `path` (child indices inside the completed block, empty for the block itself) plus a `line` and `column` inside that node, never a byte offset: the line ending is picked after your claim, so only the editor can count bytes. The claim lands as one block replacement and one undo entry; one undo restores the typed line with the caret back at its end, and pressing Enter there completes again.
1108
1164
 
1109
1165
  Two bounds worth knowing:
1110
1166
 
@@ -1117,10 +1173,10 @@ Content that's _itself editable_ comes in four tiers, and each one is backed by
1117
1173
 
1118
1174
  | Tier | What it hosts | Status |
1119
1175
  | ----------------- | -------------------------------------------------------------------------------- | ---------------------- |
1120
- | **Container** | Real document blocks in a nested child list; the walkthrough's body | shipped |
1121
- | **Chrome leaf** | One reserved, single-line, plain-text child whose bytes the container's raw owns | shipped |
1176
+ | **Container** | Real document blocks in a nested child list; the walkthrough's body | shipped _(pre-freeze)_ |
1177
+ | **Chrome leaf** | One reserved, single-line, plain-text child whose bytes the container's raw owns | shipped _(pre-freeze)_ |
1122
1178
  | **Editable leaf** | A standalone text surface with native caret/IME/undo/selection/clipboard parity | shipped _(pre-freeze)_ |
1123
- | **Atomic widget** | An opaque, non-text embed, which the caret can address only at its edges | shipped |
1179
+ | **Atomic widget** | An opaque, non-text embed, which the caret can address only at its edges | shipped _(pre-freeze)_ |
1124
1180
 
1125
1181
  The chrome leaf is deliberately narrow, and each limit is a guarantee its container can lean on:
1126
1182
 
@@ -1151,32 +1207,34 @@ const leaf = createEditableLeaf({
1151
1207
  getEl: () => sourceEl ?? null, // null while a render-primary view is folded
1152
1208
  mode: 'render-primary', // 'plain' is the default
1153
1209
  singleLine: true, // a one-line kind: Enter splits the block instead of typing a newline
1154
- isRevealed: () => revealed, // render-primary only: you own the swap flag
1210
+ isRevealed: () => revealed, // render-primary only, and required there: you own the swap flag
1155
1211
  setRevealed: (next) => (revealed = next)
1212
+ // optional too: commandHooks, handed to your block commands as ctx.hooks (see Block commands)
1156
1213
  });
1157
1214
  leaf.sourceText; // the block's raw minus its trailing line ending
1158
1215
  leaf.getPresentationMode(); // 'source'
1159
- leaf.getOptions(); // this editor's options for your plugin, typed unknown
1216
+ leaf.getOptions(); // this editor's options for your plugin, defaults included, typed unknown
1217
+ leaf.getEditor(); // this editor's EditorContext for your plugin, undefined in a bare harness
1160
1218
  ```
1161
1219
 
1162
1220
  **Native parity is the tier's whole claim**: the editor's caret enters and leaves your block like any built-in text block (including keeping its column as it walks up or down lines), IME composition is respected, undo batches like prose, the clipboard is intercepted for plain-Markdown copy/cut/paste like every editable surface, and a cross-block selection sweeps through your text.
1163
1221
 
1164
- **One spread wires the source surface.** Write `<div {...leaf.surfaceProps}>` on your source contenteditable and the DOM handlers, the `contenteditable` / `role` / `tabindex` / `spellcheck` attributes, and two view-lifecycle contracts all land at once, so a forgotten handler (a dropped `oncompositionend` that silently breaks IME) simply can't happen to you. The two contracts the spread owns are the ones every consumer used to hand-write: the source is populated so that **`textContent === source`** (the walk that maps DOM positions to byte offsets depends on it), and focus is parked on the editor root when the source unmounts.
1222
+ **One spread wires the source surface.** Write `<div {...leaf.surfaceProps}>` on your source contenteditable and the DOM handlers, the `contenteditable` / `role` / `tabindex` / `spellcheck` attributes, an `aria-label` naming your kind (its descriptor's `label`), what a screen reader is told about an inline menu open in your leaf, and two view-lifecycle contracts all land at once, so a forgotten handler (a dropped `oncompositionend` that silently breaks IME) simply can't happen to you. The two contracts the spread owns are the ones every consumer used to hand-write: the source is populated so that **`textContent === source`** (the walk that maps DOM positions to byte offsets depends on it), and focus is parked on the editor root when the source unmounts.
1165
1223
 
1166
1224
  That text carries every newline your source holds, which makes **`white-space: pre-wrap` (or `pre`) on your source element part of the contract** for any leaf whose bytes can span lines. Without it the browser collapses the line breaks on screen while the offset walk goes on counting them, and the caret sits nowhere near where it looks.
1167
1225
 
1168
- **A painted source.** By default the source is one text node. A `renderSource(text)` dep paints it as DOM instead (fence lines the marker-hiding modes collapse, highlight tokens; the `highlightCode` export is the code block's own tokenizer), and the factory asserts `textContent === text` on every paint, so a painter that drops a byte fails loudly in dev rather than corrupting a commit. A painted source takes its plain-text edits from the leaf, not the browser: typing, Enter, deletes and pastes splice the text and repaint, each reported through `onSourceEdit(text)` so a live preview can follow the draft, and undo inside the open reveal walks those edits back before it reaches the document's history. `repaintSource()` re-runs the painter after a native edit (an IME commit), and `completeBareSource(text)` lets a kind complete a chrome-only source (a `$$` straight over `$$`) to the shape a caret can sit in as it is revealed. Block math is the worked example.
1226
+ **A painted source.** By default the source is one text node. A `renderSource(text)` dep paints it as DOM instead (fence lines the marker-hiding modes collapse, highlight tokens; `renderFencedSource` draws a fenced source the way the code block does, and `highlightCode` is its tokenizer), and the factory asserts `textContent === text` on every paint, so a painter that drops a byte fails loudly in dev rather than corrupting a commit. A painted source takes its plain-text edits from the leaf, not the browser: typing, Enter, deletes and pastes splice the text and repaint, each reported through `onSourceEdit(text)` so a live preview can follow the draft, and undo inside the open reveal walks those edits back before it reaches the document's history. `repaintSource()` re-runs the painter after a native edit (an IME commit), and `completeBareSource(text)` lets a kind complete a chrome-only source (a `$$` straight over `$$`) to the shape a caret can sit in, asked as the source is revealed and again after any edit that empties it, so a one-line `$$x^2$$` that loses its `x^2` never shows bare fences. Block math is the worked example.
1169
1227
 
1170
1228
  A leaf whose bytes are one line (the parrot's opener claims exactly one) declares `singleLine: true` and needs none of that. Enter in one of those ends the block: the text after the caret becomes a paragraph below and the caret goes with it, which is what Enter does in a heading. With the flag off, the default, Enter types a newline.
1171
1229
 
1172
1230
  Beyond the spread you add only your own `class` / `aria-label`, plus **`bind:this` in both modes**: the factory reaches your element only through `getEl()`, so both modes read it the same way, and they differ only in that render-primary's `getEl()` returns null while the view is folded. The two modes:
1173
1231
 
1174
1232
  - **`'plain'`**: the source is always the editable view, and every keystroke commits to the tree (with prose-like undo batching). The spread's sync mirrors external rewrites (an undo, a structural replace) into the source and gates `contenteditable` off the mode, so the always-mounted surface goes inert in reading mode; the factory owns the Chromium trailing-newline caret quirk and the caret restore.
1175
- - **`'render-primary'`**: a rendered view by default, where focus, click, or arrow-traversal reveals the raw source in your contenteditable, and leaving it commits **once**, so the whole reveal, edit, blur cycle is one undo entry. You own the swap flag (`isRevealed` / `setRevealed`) and both views' rendering. A fold writes back only the bytes the reveal opened over, so an undo or a `source` swap that lands a different block at the index declines the write rather than corrupting it.
1233
+ - **`'render-primary'`**: a rendered view by default, where focus, click, or arrow-traversal reveals the raw source in your contenteditable, and leaving it commits **once**, so the whole reveal, edit, blur cycle is one undo entry. You own the swap flag (`isRevealed` / `setRevealed`) and both views' rendering. A fold writes back only the bytes the reveal opened over, so an undo or a `source` swap that lands a different block at the index declines the write rather than corrupting it. A move while the source is up writes it first. A move chord your keymap binds does that on its own; a host's `editor.runCommand('block.moveDown')` only does it if your component re-exports `afterSourceCommit`.
1176
1234
 
1177
- **Render-primary gets a second spread.** `renderProps` goes on the folded view, and it carries the reveal click and the chord dispatch together; a view that takes the click but not the keys swallows undo while it holds focus. Put it on a wrapper the reveal never unmounts (both handlers stand down while the source is up) and the whole folded surface, chrome included, is one click target. Where in the source that click lands is your kind's `caretTargetAtPoint`; declare none and every click reveals at the first byte.
1235
+ **Render-primary gets a second spread.** `renderProps` goes on the folded view, and it carries the reveal click and the chord dispatch together; a view that takes the click but not the keys swallows undo while it holds focus. Put it on a wrapper the reveal never unmounts (both handlers do nothing while the source is up) and the whole folded surface, chrome included, is one click target. Where in the source that click lands is your kind's `caretTargetAtPoint`; declare none and every click reveals at the first byte.
1178
1236
 
1179
- **Commit semantics.** A commit parses the edited text and lands it through the editor's own edit ladder:
1237
+ **Commit semantics.** A commit parses the edited text and lands it the way the editor lands any edit, by what the parse gives back:
1180
1238
 
1181
1239
  ```
1182
1240
  commit(edited text) ── parse ──▶ same kind? update in place, caret preserved
@@ -1188,23 +1246,15 @@ commit(edited text) ── parse ──▶ same kind? update in place, ca
1188
1246
 
1189
1247
  Editing past your own fence therefore re-splits the document instead of wedging foreign text into your node, and the round-trip holds through every commit.
1190
1248
 
1191
- **Per-instance configuration.** `leaf.getOptions()` returns this editor instance's options for the plugin owning your kind, typed `unknown` for you to narrow. It's the same route as the container factory's `getOptions()`, one tier down, and the same rule applies ([the options recipe](#recipe-per-instance-options-and-the-factory-closure-trap)). The bundled toc block resolves `maxDepth` this way and falls back to the factory argument, which then serves as the default for an instance declaring none.
1249
+ **Per-instance configuration.** `leaf.getOptions()` returns this editor's options for the plugin owning your kind, already merged over your `defaults`. It's typed `unknown`, so cast it to your options type, and it's `undefined` only with no editor around (a component mounted bare in a unit test). It's the same route as the container factory's `getOptions()`, one tier down, and the same rule applies ([the options recipe](#recipe-per-instance-options-and-the-factory-closure-trap)). The bundled toc block reads its `maxDepth` this way, with `tocPlugin({ maxDepth })` filling the default.
1192
1250
 
1193
- Block math (`$$…$$` in the bundled `@voithos-labs/aragonite/plugins/latex` plugin) is the worked example, and it's smaller than you'd expect: its component script is the factory call, one render effect (KaTeX), a `{...leaf.surfaceProps}` spread on the source, and one-line re-exports of the returned surface. Registration is the ordinary leaf recipe: `registerBlockKind` (no container group), `registerBlockOpener`, `registerBlockComponent`. Its `caretTargetAtPoint` is the other half of the parrot's: where the parrot's caption is the source bytes minus a prefix, KaTeX paints glyphs no offset maps back to, so the render effect stamps the body's span on the rendered element and the hook walks that span in proportion to how far along the press fell.
1251
+ Block math (`$$…$$` in the bundled `@voithos-labs/aragonite/plugins/latex` plugin) is the worked example, and it's smaller than you'd expect: its component script is the factory call with a painted source (`renderSource`, `onSourceEdit`, `completeBareSource`), one render effect (KaTeX), a `{...leaf.surfaceProps}` spread on the source, and one-line re-exports of the returned surface. Registration is the ordinary leaf recipe (`registerBlockKind` with no container group, `registerBlockOpener`, `registerBlockComponent`) plus an on-type completer for a lone `$$` and a second kind for the ` ```math ` fence. Its `caretTargetAtPoint` is the other half of the parrot's: where the parrot's caption is the source bytes minus a prefix, KaTeX paints glyphs no offset maps back to, so the render effect stamps the body's span on the rendered element and the hook walks that span in proportion to how far along the press fell.
1194
1252
 
1195
1253
  ## Presentation modes
1196
1254
 
1197
- **The contract: every plugin tier can learn the editor's current presentation mode and render for it.** The editor isn't permanently the marker-always source view (a **marker** is the syntax itself, the `**` around bold or the `#` before a heading, which the editor shows dimmed). A consumer can flip the editor into any of these today, and a plugin that assumes source mode renders wrong the day its host flips the prop:
1255
+ **The contract: every plugin tier can learn the editor's current presentation mode and render for it.** The editor isn't permanently the marker-always source view (a **marker** is the syntax itself, the `**` around bold or the `#` before a heading, which the editor shows dimmed). A consumer can flip the editor into any of five modes, and a plugin that assumes source mode renders wrong the day its host flips the prop. What each mode looks like to a user is the [consumer guide's table](consumer-guide.md#presentation-modes); this section is what each asks of a plugin.
1198
1256
 
1199
- | Mode | Editing | What shows |
1200
- | ---------------- | ------- | --------------------------------------------- |
1201
- | `source` | live | every marker, dimmed |
1202
- | `reading` | none | no markers, no reveals |
1203
- | `preview-block` | live | markers only in the focused block |
1204
- | `preview-inline` | live | syntax only for the construct under the caret |
1205
- | `live` | live | no markers anywhere, nothing revealed |
1206
-
1207
- What each mode looks like to a user is the [consumer guide](consumer-guide.md)'s subject; this section is what each asks of a plugin. Two facts about the type first. `PresentationMode` is `'source' | 'reading' | 'preview-block' | 'preview-inline' | 'live'`, and every read below reports the **effective** mode, what the editor is actually doing, which matches the requested prop once every mode is fully built. And the union **grows by addition**, so handle it non-exhaustively: read the one property your rendering depends on (does this mode paint markers, does it write bytes) and default the rest, or the next mode renders your kind wrong the day it lands.
1257
+ Two facts about the type first. `PresentationMode` is `'source' | 'reading' | 'preview-block' | 'preview-inline' | 'live'`, and every read below reports the **effective** mode, which is the requested prop except for the moment a switch commits the outgoing mode's open edit (the getters still say the outgoing mode then; the `data-presentation` attribute already has the new one). And the union **grows by addition**, so handle it non-exhaustively: read the one property your rendering depends on (does this mode paint markers, does it write bytes) and default the rest, or the next mode renders your kind wrong the day it lands.
1208
1258
 
1209
1259
  How each tier reads it:
1210
1260
 
@@ -1227,12 +1277,12 @@ In `reading` mode the platform does most of it for you, which is why most plugin
1227
1277
 
1228
1278
  - your editable leaf never reveals and never commits;
1229
1279
  - chord dispatch (block commands, global commands, keymaps) is swallowed at the dispatcher;
1230
- - the container factory gates whole-block Enter/Backspace/reorder;
1280
+ - the container factory gates whole-block Enter and Backspace (its reorder is a keymap chord, so the line above covers it);
1231
1281
  - marker spans hide by CSS.
1232
1282
 
1233
1283
  You read the mode yourself in two cases: when your component owns an edit affordance of its own (a toolbar button, a click-to-edit swap, an interactive widget) which must go inert, the bundled mermaid block's Edit button and the details disclosure being the worked examples, or when your rendering should genuinely differ between a source view and a reading view.
1234
1284
 
1235
- `preview-block` is different: it's a **live editing** mode, so none of those reading gates fire. You type, edit, and command in it exactly as in source; only the marker visibility changes. A **render-primary** plugin block (a diagram, a chart, [the render-primary recipe](#recipe-a-render-primary-block)) gets this for free: it already renders its picture when unfocused and reveals its source only on caret entry, in every non-reading mode, which _is_ block-granular preview. A plugin block that instead renders always-visible source chrome should hide that chrome when it isn't the focused block; the built-in prose kinds do this by CSS, and the reveal-on-focus render-primary pattern (the quickstart parrot's shape) is the supported way for a plugin to match it. A reactive "am I the focused block" block-tier signal is planned but not built.
1285
+ `preview-block` is different: it's a **live editing** mode, so none of those reading gates fire. You type, edit, and command in it exactly as in source; only the marker visibility changes. A **render-primary** plugin block (a diagram, a chart, [the render-primary recipe](#recipe-a-render-primary-block)) gets this for free: it already renders its picture when unfocused and reveals its source only on caret entry, in every non-reading mode, which _is_ block-granular preview. A plugin block that instead renders always-visible source chrome should hide that chrome when it isn't the focused block; the built-in prose kinds do this by CSS, and the reveal-on-focus render-primary pattern (the quickstart parrot's shape) is the supported way for a plugin to match it, since there's no "am I the focused block" signal for a block component to read.
1236
1286
 
1237
1287
  `preview-inline` narrows the reveal to inline granularity inside the focused block: the construct under the caret shows its syntax, everything else stays rendered. For plugin inline kinds nothing changes at the API level, and what happens to each follows from how it renders:
1238
1288
 
@@ -1245,11 +1295,11 @@ You read the mode yourself in two cases: when your component owns an edit afford
1245
1295
 
1246
1296
  Reactivity is **per tier, not universal**, and that's worth being upfront about.
1247
1297
 
1248
- **The live reads.** The `EditorContext.presentationMode` getter (paired with the `presentationModeChange` event), the editable-leaf `getPresentationMode()`, the container-factory `getPresentationMode()`, and the inline-widget `getPresentationMode` prop are re-read by the render pass and the event dispatch, so those tiers track a flip on their own.
1298
+ **The live reads.** The `EditorContext.presentationMode` getter (paired with the `presentationModeChange` event), the editable-leaf `getPresentationMode()`, the container-factory `getPresentationMode()`, and the inline-widget `getPresentationMode` prop are reactive, so a read inside a `$derived` or an effect re-runs on a switch. The built-in mermaid diagram and details block both do exactly that with the container factory's getter (`$derived(getPresentationMode() === 'reading')`): mermaid checks it in its Edit handler, details uses it to pick its disclosure handler (the reading-mode paragraph below).
1249
1299
 
1250
- **The block-component DOM read is point-in-time.** `closest()` learns the mode when your code runs, but a live flip does **not** re-render a mounted block through it. If your block's _rendering_ must change with the mode, react explicitly: subscribe to `presentationModeChange` on your `EditorContext`'s `events` (from `onEditor`) and update from the handler, or re-read the mode at each gesture. The built-in mermaid diagram gates its edit affordance the gesture-read way, calling the container factory's `getPresentationMode()` at click time; the built-in details block reads the mode per render instead, because its reading-mode disclosure changes what RENDERS (two paragraphs down), not just what a click does. Reactive block-tier rendering is planned but not built; today the block tier is point-in-time by design.
1300
+ **The block-component DOM read is point-in-time.** `closest()` learns the mode when your code runs, but a live flip does **not** re-render a mounted block through it. A component holding only a DOM handle has to react explicitly: subscribe to `presentationModeChange` on your `EditorContext`'s `events` (from `onEditor`) and update from the handler, or re-read the attribute at each gesture.
1251
1301
 
1252
- **The theme rides exactly where the mode rides.** `EditorContext.theme` (paired with the `themeChange` event), the container and leaf factories' `getTheme()`, and the inline-widget `getTheme` prop are the same four routes with the same liveness. Reach for them only when your content's colors are PAINTED by an engine and so can't be reached by CSS; token-styled chrome rethemes itself through the cascade and should read none of this.
1302
+ **The theme rides exactly where the mode rides.** `EditorContext.theme` (paired with the `themeChange` event), the container and leaf factories' `getTheme()`, and the inline-widget `getTheme` prop are the same four routes with the same liveness. They're always there, and the editor picks the default (`'dark'`) when a host sets none, so call `getTheme()` as is, with no `?? 'dark'` of your own. Reach for them only when your content's colors are PAINTED by an engine and so can't be reached by CSS; token-styled chrome rethemes itself through the cascade and should read none of this.
1253
1303
 
1254
1304
  **Reading mode writes no bytes, which isn't the same as "nothing happens".** An affordance whose flip is view-only may stay live there, and the built-in `<details>` disclosure does exactly that, so a reader can open a collapsed section. The pattern is worth copying exactly: keep the transient state in a module with **no commit route in its dependencies** and choose the handler by mode, so the reading path can't commit rather than politely declining to; feed the EFFECTIVE state to the container factory's `isCollapsed` dep, so the windowing mounts what the view claims is open; and reset the transient state when the mode leaves reading, or a view state outlives the mode whose bytes agreed with it. An affordance whose flip would rewrite the document (a task checkbox) stays inert. That's the line, not "interactive vs not".
1255
1305
 
@@ -1265,46 +1315,56 @@ fence claim ──▶ opaque container, NO children ──▶ component renders
1265
1315
  rebuildRaw re-emits the fence commits ride updateOwnMetadata
1266
1316
  ```
1267
1317
 
1268
- - **Claim your grammar, decline everything else.** The opener accepts exactly the fences the built-in `fencedCode` would, gated on the info string's first word, and must price **ahead** of `fencedCode` ([Opener priority](#opener-priority)). Declining returns the fence to `fencedCode`, which is also your uninstall story: without the plugin the same bytes parse as a plain code block and round-trip unchanged. Pin both states with round-trip tests. Match the fence with `matchFenceOpen` / `matchFenceClose`, and never carry your own copy of the CommonMark fence rules.
1269
- - **Code in metadata, an empty container around it.** Register the kind with `container: { contract: 'opaque', rebuildRaw }` and give nodes `children: []`. The source text and every fence byte the rebuild needs (indent, marker, info string, closer shape) go into typed plugin metadata, primitive values only, and `rebuildRaw` re-emits the exact bytes from them. Build the parsed node's `raw` by calling your own rebuild, so opener and rebuild agree by construction.
1270
- - **Edit mode commits through `updateOwnMetadata`.** The component swaps its body to a plugin-owned `<textarea>` seeded from metadata; commit (Ctrl+Enter, blur) writes the new code with the container factory's `updateOwnMetadata`, which is one undoable entry, with your `rebuildRaw` re-emitting the fence so `getSource()` reflects the edit byte-exactly. Escape cancels without touching the tree.
1271
- - **Inject the renderer, memoize it, own its CSS.** The engine is the consumer's dependency: take it as a plugin option (`mermaidPlugin({ renderer })`) and pass it by module to the component. Wrap it in `createBoundedMemo` so re-renders of unchanged code do zero engine work. An async renderer stores the render promise as the cached value (in-flight work is shared, and a failure is cached like a success), and a renderer whose result holds a live DOM node passes a `cloneOnRead` so each caller gets its own copy. Resolve failures to a legible inline error, never a throw, and render a static code fallback with a note when no renderer is configured. The engine's stylesheet travels with the renderer module, so import it there, where no route can forget it: a KaTeX-based renderer needs `katex/dist/katex.min.css`, or its MathML accessibility tree lays out unclipped and every equation paints twice.
1272
- - **If the engine paints its own colors, the theme is a render input.** An engine that emits markup carrying color literals (a diagram SVG) can't be rethemed by a stylesheet after the fact; the diagram has to be redrawn. So the theme belongs in three places at once, and any one of them alone leaves a broken half: **the renderer's parameters** (so it can draw for the theme), **the memo key** (so a flip misses and a flip back is still a hit, never a cache reset, which throws away work you'll want again), and **the component's render read** (`getTheme()` off the container or leaf factory), because THAT read is what subscribes the block to the flip. Mermaid keys `theme\0code`; its engine adapter maps the editor theme name to a mermaid theme and re-initializes when it changes, serializing renders because that config is process-global. An engine styled by CSS variables needs none of this.
1273
- - **Interior interactivity stays inside your DOM.** Pan/zoom, buttons, overlays: anything draggable must `stopPropagation()` on pointerdown, or the drag starts a cross-block selection instead. A focus view is just a fixed-position overlay in the component's own tree, so mount it in place, focus it on open, close on Escape.
1318
+ - **Claim your grammar, decline everything else.** The opener accepts exactly the fences the built-in `fencedCode` would, gated on the info string's first word, and must price **ahead** of `fencedCode` ([Opener priority](#opener-priority)). Declining returns the fence to `fencedCode`, which is also your uninstall story: without the plugin the same bytes parse as a plain code block and round-trip unchanged. Pin both states with round-trip tests. Claim the fence with `matchFenceInfo('mermaid')` and read its extent with `scanFence`, which closes where the parser does, and never carry your own copy of the CommonMark fence rules.
1319
+ - **Declare the fence's write rule.** A find/replace or a range delete writes your block's bytes without your component, and a fence is one byte away from swallowing the document: a body line that reads as the closer ends the block early, and an opener removed while the closer stays opens a fence over everything below. `rawWrite: fenceRawWrite(fenceShapeOfRaw)` is the code block's own rule: it grows both runs past a body line that reads as the closer, puts the closer back when a write deleted it, and drops a closer whose opener a write deleted.
1320
+ - **Code in metadata, an empty container around it.** Register the kind with `container: { contract: 'opaque', rebuildRaw }` and give nodes `children: []`. The source text and every fence byte the rebuild needs (indent, marker, info string, closer shape) go into typed plugin metadata, primitive values only, and `rebuildRaw` re-emits the exact bytes from them. Build the parsed node's `raw` by calling your own rebuild, so opener and rebuild agree by construction. If the rebuild lengthens the fence past a body line (`escalatedFenceLength`), leave the stored length alone: when a rebuild moves the opener or closing line, the editor re-reads your metadata from the new bytes through your opener.
1321
+ - **Edit mode commits through `updateOwnMetadata`.** The component swaps its body to a plugin-owned `<textarea>` seeded from metadata; commit (Ctrl+Enter, blur) writes the new code with the container factory's `updateOwnMetadata`, which is one undoable entry, with your `rebuildRaw` re-emitting the fence so `getSource()` reflects the edit byte-exactly. Ctrl+Enter also passes `{ caret: { path: [], offset: 0 } }`, so the diagram gets focus back once the new code renders; a blur passes none, since you clicked somewhere else on purpose. Escape cancels without touching the tree.
1322
+ - **Inject the renderer into a slot, and own its CSS.** The engine (the library that actually draws, KaTeX or mermaid) is the consumer's dependency, so take it as a plugin option (`mermaidPlugin({ renderer })`) and put it in a **renderer slot**: a module-level `createAsyncRendererSlot` (or `createRendererSlot`, for an engine that answers right away) that your setup fills and your component renders through. The slot caches each render, and it never throws at you: with no renderer set you get your `missing` output, and a throw or a rejection gets your `failed` output, cached like a success. For anything drawn from source text, `renderSourceFallback(source, message)` makes a decent `missing` or `failed`. There's a slot in the snippet below. The engine's stylesheet travels with the renderer module, so import it there, where no route can forget it: a KaTeX-based renderer needs `katex/dist/katex.min.css`, or its MathML accessibility tree lays out unclipped and every equation paints twice.
1323
+ - **If the engine paints its own colors, the theme is a render input.** An engine that emits markup carrying color literals (a diagram SVG) can't be rethemed by a stylesheet after the fact, so the diagram has to be redrawn. The slot does most of that for you: `render` won't take a call without the theme, hands it to your renderer, and keys the cache on it, so a switch misses and a switch back is still a hit. The part left is yours. Read the theme with `getTheme()` (off the container or leaf factory, or an inline widget's props) inside the effect that renders, because that read is what re-runs the effect on a switch. Mermaid's engine adapter maps the editor theme name to a mermaid theme and re-initializes when it changes, serializing renders because that config is process-global. An engine styled by CSS variables can ignore the theme it's handed.
1324
+ - **Interior interactivity stays inside your DOM.** Pan/zoom, buttons, overlays: put `POINTER_GESTURE_ATTR` on the element whose drags are yours (only while the gesture is armed, if it isn't always), or the editor reads the press as the start of a selection and paints a range over your pan. `stopPropagation()` on pointerdown can't do this, since Svelte delivers pointer events from the app root and the editor's listener has already run. A focus view is just a fixed-position overlay in the component's own tree, so mount it in place, focus it on open, close on Escape.
1274
1325
  - **View-state commands reach the component through `ctx.hooks`.** See [Block commands](#block-commands).
1275
1326
 
1276
- The two helpers from that list, with what they hand back:
1327
+ The helpers from that list, with what they hand back:
1277
1328
 
1278
- `````ts
1279
- matchFenceOpen('```mermaid'); // { marker: '`', length: 3, info: 'mermaid', indent: '', infoRaw: 'mermaid' }
1280
- matchFenceOpen(' ~~~ js title'); // { marker: '~', length: 3, info: 'js title', indent: ' ', infoRaw: ' js title' }
1281
- matchFenceOpen('hello'); // null, so hand the line back
1282
- matchFenceClose('```` ', '`', 3); // true: a longer run with trailing space still closes a three-backtick fence
1329
+ ````ts
1330
+ const matchMermaid = matchFenceInfo('mermaid');
1331
+ matchMermaid('```mermaid'); // { marker: '`', length: 3, info: 'mermaid', indent: '', infoRaw: 'mermaid' }
1332
+ matchMermaid(' ~~~ mermaid title'); // { marker: '~', length: 3, info: 'mermaid title', indent: ' ', ... }
1333
+ matchMermaid('```js'); // null: the code block keeps it
1334
+ scanFence(ctx, fence); // { closer: 3, consumed: 4, raw: '```mermaid\n...```\n', body: '...' }
1335
+ fenceRawWrite(fenceShapeOfRaw).normalize('graph TD\n```\n', ctx); // 'graph TD\n': the stranded closer goes
1283
1336
 
1284
- const render = createBoundedMemo<string, Promise<SVGElement>>({ cap: 32 });
1285
- render(`${theme}\0${code}`, () => engine.render(code, theme)); // computes once per key; past 32 entries the least recently used one goes
1286
- `````
1337
+ const diagrams = createAsyncRendererSlot<string, { svg?: string; error?: string }>({
1338
+ key: (code) => code,
1339
+ missing: () => ({ error: 'no renderer' }),
1340
+ failed: (_code, error) => ({ error: String(error) })
1341
+ });
1342
+ diagrams.set((code, { theme }) => engine.render(code, theme).then((svg) => ({ svg }))); // in setup; null removes it
1343
+ await diagrams.render('graph TD', { theme: 'dark' }); // { svg: '<svg …>' }
1344
+ await diagrams.render('graph TD', { theme: 'dark' }); // the same result, and the engine isn't called again
1345
+ await diagrams.render('graph TD', { theme: 'light' }); // a miss: drawn again for the light theme
1346
+ ````
1287
1347
 
1288
1348
  **What you give up with the textarea.** The code text isn't editor-native: no cross-block selection through it, the textarea's caret and IME are the browser's rather than the editor's, and so is its undo. A chord raised inside your surface reaches the browser, not the editor's history, so the draft has its own undo stack and the editor's chords resume once focus leaves.
1289
1349
 
1290
1350
  ### Whole-block focus
1291
1351
 
1292
- Because the container has no children, a caret can't land _inside_ it, so the kind opts into being focused as a whole: declare `blockFocus: 'whole-block'` on the kind and hand the factory a `getFocusEl` getter returning the element that **declares** the block's focus surface, meaning the one a pointer lands on. The block then behaves like one big character: arrows stop on it (the bundled mermaid diagram is the shipped reference), a caret-adjacent Backspace/Delete focuses it before a second press deletes, Enter inserts a paragraph below, undo/redo run from the block itself, and Alt+arrows reorder it. Keyboard and click share the one focus state, and keys inside your own editing surface never trigger a block delete.
1352
+ Because the container has no children, a caret can't land _inside_ it, so the kind opts into being focused as a whole: declare `blockFocus: 'whole-block'` on the kind and hand the factory a `getFocusEl` getter returning the element that **declares** the block's focus surface, meaning the one a pointer lands on. The block then behaves like one big character: arrows stop on it (the bundled mermaid diagram is the shipped reference), a caret-adjacent Backspace/Delete focuses it before a second press deletes, Enter inserts a paragraph below, undo/redo run from the block itself, and Alt+arrows reorder it. Keyboard and click share the one focus state, and keys inside your own editing surface never trigger a block delete. Don't skip the declaration: to the editor, any other container with no children is one an edit broke, so a dev build warns (`invariant:keeps-a-block`) on every commit that touches your block, and a paste or a reparse gives it an empty paragraph to hold.
1293
1353
 
1294
1354
  The mechanics behind that, each with its gotcha:
1295
1355
 
1296
1356
  - **DOM focus goes to a hidden editing host** the factory mounts in your chrome box, because AltGr productions and IME composition arrive only through an editing host and your surface isn't one; a click or Tab onto your declared element is passed on to it. So assert containment, not identity, if you test for focus.
1297
1357
  - **Give your box `position: relative`**, or the host resolves against whatever ancestor happens to be positioned.
1298
- - **The host is the block's one tab stop**, and the editor keeps it that way: a `tabindex` on your declared element is demoted to `-1` on every read unless the element is itself an editing surface (a textarea, an input, a contenteditable). So there's no tab-order work to do on your side, and no point declaring a `tabindex="0"` button as the surface expecting Tab to land on it.
1358
+ - **The host is the block's one tab stop**, named for your kind (its descriptor's `label`), and the editor keeps it that way: a `tabindex` on your declared element is demoted to `-1` on every read unless the element is itself an editing surface (a textarea, an input, a contenteditable). So there's no tab-order work to do on your side, and no point declaring a `tabindex="0"` button as the surface expecting Tab to land on it.
1299
1359
  - **An editable declared surface keeps focus for itself** (your edit `<textarea>`), which owns its caret and IME already.
1300
1360
 
1301
1361
  Supply a focus element for **every steady state** (error, loading, and static fallbacks included), so a broken render stays keyboard-reachable. If the getter returns null anyway, the editor degrades to focusing your chrome box and warns in dev.
1302
1362
 
1303
- ### What you owe the surface you own
1363
+ ### What your own editing surface has to do
1304
1364
 
1305
- **First: an arrow that runs off your surface has to leave it.** A textarea swallows every arrow at its own boundaries, so a caret that walks in is stuck, and it's worst when your surface is the block's only view and the caret lands in it on creation, which leaves the mouse as the only way out. Call the factory's `moveFocusOut(event)` when the caret sits at the edge the key points at: first line for ArrowUp, last line for ArrowDown, offset 0 for ArrowLeft, the end for ArrowRight. It declines a modified or non-arrow key and moves nothing when it declines, so gate your own `preventDefault` on its return value and a Shift-extend or a mid-text arrow stays native. Logical lines (the newlines around the caret) are enough: a plugin surface owes an exit, not full column-keeping parity. And an exit is a blur, so a surface that commits on `focusout` already commits through it; don't add a second commit path for the arrow.
1365
+ **First: an arrow that runs off your surface has to leave it.** A textarea swallows every arrow at its own boundaries, so a caret that walks in is stuck, and it's worst when your surface is the block's only view and the caret lands in it on creation, which leaves the mouse as the only way out. Call the factory's `moveFocusOut(event)` when the caret sits at the edge the key points at: first line for ArrowUp, last line for ArrowDown, offset 0 for ArrowLeft, the end for ArrowRight. It declines a modified or non-arrow key and moves nothing when it declines, so gate your own `preventDefault` on its return value and a Shift-extend or a mid-text arrow stays native. Logical lines (the newlines around the caret) are enough: a plugin surface has to provide an exit, not full column-keeping parity. And an exit is a blur, so a surface that commits on `focusout` already commits through it; don't add a second commit path for the arrow.
1306
1366
 
1307
- **Next: your draft is a copy, so keep it fresh.** A draft seeded once at open goes stale the moment the document changes underneath it (a host undo, a structural replace, a collaborative write), and the commit on blur then writes bytes the tree has already moved past, silently reverting the change. Derive the code from the node, watch that derivation while your surface is open, and re-seed the draft when it changes to something you didn't just commit; discarding an in-flight draft is the cheap loss, reverting a committed change is the expensive one. The editable leaf does this for you (both modes mirror external raw changes into the source); a plugin-owned surface owes it itself, and the bundled mermaid block is the worked example.
1367
+ **Next: your draft is a copy, so keep it fresh.** A draft seeded once at open goes stale the moment the document changes underneath it (a host undo, a structural replace, a collaborative write), and the commit on blur then writes bytes the tree has already moved past, silently reverting the change. Derive the code from the node, watch that derivation while your surface is open, and re-seed the draft when it changes to something you didn't just commit; discarding an in-flight draft is the cheap loss, reverting a committed change is the expensive one. The editable leaf does this for you (both modes mirror external raw changes into the source); a plugin-owned surface has to do it itself, and the bundled mermaid block is the worked example.
1308
1368
 
1309
1369
  Want a source view with a native caret instead? That's [the editable-leaf tier](#the-editable-leaf), and rebuilding a render-primary block on `createEditableLeaf` (block math's shape) is this recipe's upgrade path.
1310
1370
 
@@ -1314,51 +1374,80 @@ A block component gets its own node, which is fine right up until it isn't: a ta
1314
1374
 
1315
1375
  ```svelte
1316
1376
  <script lang="ts">
1317
- import { getContentRange, type DocumentView } from '@voithos-labs/aragonite/plugin';
1377
+ import { getContentRange, walkBlocks, type DocumentView } from '@voithos-labs/aragonite/plugin';
1318
1378
 
1319
1379
  // A component receives its own node too; this block needs only the document.
1320
1380
  let { document }: { document?: DocumentView } = $props();
1321
1381
 
1322
1382
  // A $derived over the prop subscribes to the CST proxy, so editing a heading
1323
1383
  // above re-runs this and the list updates live.
1324
- const headings = $derived(
1325
- (document?.children ?? [])
1326
- .filter((b) => b.kind === 'heading' || b.kind === 'setextHeading')
1327
- .map((b) => {
1328
- const { start, end } = getContentRange(b); // drop the `#` / underline markers
1329
- return b.raw.slice(start, end);
1330
- })
1331
- );
1384
+ const headings = $derived.by(() => {
1385
+ const found: { path: number[]; text: string }[] = [];
1386
+ if (!document) return found;
1387
+ walkBlocks(document, (block, path) => {
1388
+ if (block.kind !== 'heading' && block.kind !== 'setextHeading') return;
1389
+ const { start, end } = getContentRange(block); // drop the `#` / underline markers
1390
+ found.push({ path, text: block.raw.slice(start, end) });
1391
+ });
1392
+ return found;
1393
+ });
1332
1394
  </script>
1333
1395
 
1334
1396
  <nav>
1335
- {#each headings as text}<div>{text}</div>{/each}
1397
+ {#each headings as heading}<div>{heading.text}</div>{/each}
1336
1398
  </nav>
1337
1399
  ```
1338
1400
 
1339
1401
  `document` is a **`DocumentView`**, read-only by type ([Views](#views-what-you-read-what-you-own)). Deriving from it is the whole point; mutation stays a commit concern.
1340
1402
 
1403
+ Reading `document.children` gets you the top-level blocks and nothing else, so a heading inside a quote or a list item would go missing. That's what `walkBlocks` is for.
1404
+
1405
+ **`walkBlocks(root, visit, basePath?)`**
1406
+
1407
+ Calls `visit(block, path)` for every block under `root` (a document, or any block you hold), parents before their children, in the order they sit in the document. `root` itself isn't visited. The path is the list of child indices from `root` down to the block, a fresh array per call, so keep it if you like. What `visit` returns steers the walk:
1408
+
1409
+ - nothing: carry on, children included
1410
+ - `'skip'`: leave this block's children out
1411
+ - `'stop'`: end the walk right here, and `walkBlocks` returns `true` (it returns `false` when it ran to the end)
1412
+
1413
+ Walking a block you found at some path? Pass that path as `basePath`, and every path you get back is a document path again.
1414
+
1415
+ ```ts
1416
+ const doc = parse('# Top\n\n> ## Quoted\n> text\n');
1417
+ walkBlocks(doc, (block, path) => console.log(path, block.kind));
1418
+ // [0] 'heading'
1419
+ // [1] 'blockquote'
1420
+ // [1, 0] 'heading'
1421
+ // [1, 1] 'paragraph'
1422
+ ```
1423
+
1424
+ The other direction, a path you already have to its block, is `blockNodeAt`. It answers `null` for a path that leads nowhere, and for the empty path too (that's the document, which isn't a block). Here it is next to the recipe's other calls:
1425
+
1341
1426
  ```ts
1427
+ blockNodeAt(doc, [1, 0])?.raw; // '## Quoted\n'
1428
+ blockNodeAt(doc, []); // null
1342
1429
  getContentRange(parse('# Hi\n').children[0]); // { start: 2, end: 4 }: the two bytes of 'Hi', markers skipped
1343
1430
  getContentRange(parse('plain text\n').children[0]); // { start: 0, end: 10 }: a paragraph has no markers to skip
1344
- await rects.navigateTo([4]); // true once the block at [4] is in view with the caret at its start
1431
+ await rects.navigateTo([1, 0]); // true once the quoted heading is in view with the caret at its start
1345
1432
  ```
1346
1433
 
1347
- A block that needs to _navigate_ to what it read (a table-of-contents entry jumping to its heading) receives the owning instance's geometry surface as **`BlockComponentProps.rects`**, the same object `EditorContext.rects` hands your per-instance callback. So `rects.navigateTo(path)` works from inside a block without reaching for an editor context a component doesn't have, and the navigation shares the editor's one reveal-and-place machinery rather than a second copy of the rule. `navigateTo` lands the caret at the target as well as scrolling to it; an affordance that only scrolled would leave focus on its own button, where the editor's chords don't reach and an undo typed right after the jump does nothing. Use `scrollTo(path)` where the viewport should move but the selection shouldn't. Navigation mutates no bytes, so it stays legal in reading mode, which simply has no editable target to focus. The bundled **toc** plugin is this recipe end to end.
1434
+ A block that needs to _navigate_ to what it read (a table-of-contents entry jumping to its heading) receives the owning instance's geometry surface as **`BlockComponentProps.rects`**, the same object `EditorContext.rects` hands your per-instance callback. So `rects.navigateTo(path)` (optionally with a raw offset into the target, `navigateTo(path, offset)`) works from inside a block without reaching for an editor context a component doesn't have, and the navigation shares the editor's one reveal-and-place machinery rather than a second copy of the rule. `navigateTo` lands the caret at the target as well as scrolling to it; an affordance that only scrolled would leave focus on its own button, where the editor's chords don't reach and an undo typed right after the jump does nothing. Use `scrollTo(path)` where the viewport should move but the selection shouldn't. Navigation mutates no bytes, so it stays legal in reading mode, which simply has no editable target to focus. The bundled **toc** plugin is this recipe end to end.
1348
1435
 
1349
1436
  ## Inline kinds
1350
1437
 
1351
1438
  Blocks are only half the story. An inline kind takes three calls, mirroring the block tier's declare, describe, recognize:
1352
1439
 
1353
- - **`declarePluginInlineKind(name)`** mints the inline kind and returns it, exactly as `declarePluginKind` does one level up; `declaredPluginInlineKind(name)` recovers it in a module that didn't mint it.
1354
- - **`registerInlineSyntax(trigger, recognizer, options?)`** hooks the inline scanner on a single **trigger** character: at each occurrence of the trigger, your recognizer claims the syntax by returning a node, or declines with `null`. The options carry the prefix-rung and rewrite machinery this section works through.
1355
- - **`registerInlineWidgetKind(kind, descriptor)`** says how the kind renders and edits: as a live **atomic widget**, one indivisible rendered thing the caret can sit beside but not inside, with the editing policy this section closes on.
1440
+ - **`declarePluginInlineKind(name)`** creates the inline kind and returns it, exactly as `declarePluginKind` does one level up; `declaredPluginInlineKind(name)` recovers it in a module that didn't create it.
1441
+ - **`registerInlineSyntax(trigger, recognizer, options?)`** hooks the inline scanner on a single **trigger** character: at each occurrence of the trigger, your recognizer claims the syntax by returning a node, or declines with `null`. The options carry the prefix and rewrite machinery this section works through.
1442
+ - **`registerInlineWidgetKind(kind, descriptor)`** says how the kind renders and edits: as a live **atomic widget**, one indivisible rendered thing the caret can sit beside but not inside, with the editing policy this section closes on. Typing a trigger character inside a widget's source (a `#` in a formula, say) won't open an inline menu, since that source isn't prose.
1356
1443
 
1357
- The three together, for a `:shortcode:` kind on a trigger nothing else claims:
1444
+ The three together, for a `:shortcode:` kind:
1358
1445
 
1359
1446
  ```ts
1360
1447
  const shortcode = declarePluginInlineKind('shortcode'); // 'shortcode', branded
1361
- registerInlineSyntax(':', recognizeShortcode); // a bare trigger; the priority defaults to INLINE_PRIORITIES.plugin (100)
1448
+ // A bare trigger. Once directives or the bundled emoji are on, `:` is shared with the
1449
+ // directive text tier (the default, INLINE_PRIORITIES.plugin) and emoji (plugin + 10).
1450
+ registerInlineSyntax(':', recognizeShortcode, { priority: INLINE_PRIORITIES.plugin + 20 });
1362
1451
  registerInlineWidgetKind(shortcode, {
1363
1452
  isWidget: (node) => node.kind === shortcode,
1364
1453
  component: ShortcodeWidget,
@@ -1366,30 +1455,42 @@ registerInlineWidgetKind(shortcode, {
1366
1455
  });
1367
1456
  ```
1368
1457
 
1458
+ The inline tier isn't the block surface in miniature, though. An inline kind gets recognition, rendering, atomic caret addressing at its edges, and an editing policy on its widget registration, and that's the lot: **no keymap, no commands of its own, and no per-node metadata** (`InlineNode` has no metadata field, so unlike a block kind it stores nothing at all on the node).
1459
+
1460
+ ### Rendering it as a widget
1461
+
1369
1462
  A widget renders through one of two paths, and the descriptor rejects declaring both:
1370
1463
 
1371
- - **A `component` (recommended).** Supply a Svelte component; the editor wraps it in the atomic island (the wrapper element it mounts widgets in), stamping the marker attributes the cursor and selection machinery need, and mounts it with frozen `{ inline, source }` props. A keyed reuse pool keeps one live instance per `(kind, source)` across the editor's rebuild-everything-per-keystroke render: typing next to a widget adopts its instance rather than remounting it, and the instance is remounted only when its source text changes.
1372
- - **A hand-built `buildWidget`.** Return the island DOM yourself when you need DOM-level control. Start from `mintWidgetShell`, which stamps the marker and source-span attributes the offset walk reads, then add the body. This is the lower-level path the image and emoji widgets use.
1464
+ - **A `component` (recommended).** Supply a Svelte component; the editor wraps it in the widget's wrapper element, stamping the marker attributes the cursor and selection machinery need, and mounts it with frozen `{ inline, source }` props. A reuse pool keyed by `(kind, source)` keeps each widget's instance across the editor's rebuild-everything-per-keystroke render (two identical sources are still two instances): typing next to a widget adopts its instance rather than remounting it, and the instance is remounted only when its source text changes.
1465
+ - **A hand-built `buildWidget`.** Return the wrapper's DOM yourself when you need DOM-level control. Start from `mintWidgetShell`, which stamps the marker and source-span attributes the offset walk reads, then add the body. This is the lower-level path the image and emoji widgets use.
1466
+
1467
+ Beside the frozen pair, a component gets these props, and the editor passes every one of them, so call them as given. A test that mounts your widget by hand passes its own (a fixed mode, a stub `navigateTo`), and the types won't let it forget one.
1373
1468
 
1374
- **Three live getters ride beside the frozen props.** They're getters because the pool reuses instances: one survives a mode flip and an edit elsewhere, and a captured value would go stale there.
1469
+ | Prop | What it's for |
1470
+ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1471
+ | `getPresentationMode`, `getTheme` | The effective presentation mode and the theme name. Getters, like the next two, because the pool reuses instances: one survives a mode switch and an edit elsewhere, and a captured value would go stale there |
1472
+ | `getDocument` | The read-only root document |
1473
+ | `getContentVersion` | A number that changes whenever the document's bytes change, and is stable otherwise |
1474
+ | `navigateTo(path, offset?)` | The editor's jump route: it reveals that block, scrolls it into view, and lands the caret in it (at a raw offset, if you pass one). A container path lands at the start of its first line (a quote's first paragraph, a list's first item), and a closed `<details>` on the way gets opened. For a widget that points somewhere else, the way a footnote reference points at its definition. It resolves false when there's nowhere to land |
1475
+ | `computeInlineContent` | The same parse `EditorContext.computeInlineContent` gives a plugin. Walk inline nodes through it and syntax the editor left out comes back as plain text |
1375
1476
 
1376
- - `getPresentationMode`: the effective presentation mode.
1377
- - `getDocument`: the read-only root document.
1378
- - `getContentVersion`: a number that changes whenever the document's bytes change, and is stable otherwise.
1477
+ Three habits for those props:
1379
1478
 
1380
- A fourth prop, `navigateTo`, is the editor's jump route: hand it a block path and the editor reveals that block, scrolls it into view, and lands the caret in it. Aim at a leaf: a container seats no caret, so a container path scrolls the block into view and leaves the caret where it was. Use it when your widget points at somewhere else in the document, the way a footnote reference points at its definition. It's absent in a bare harness mount, so call it optionally.
1479
+ - **Key a cache on `computeInlineContent` too.** Editing a link reference definition (a line like `[r]: /x`) can change how a block parses without touching that block's bytes, and the editor hands you a new function whenever the definitions change. The bundled footnotes plugin does exactly this, since `[t [^x]][q]` hides its footnote until `[q]` gets a definition (brackets are fun like that).
1480
+ - **Memoize a whole-document read on the content version.** Read the version inside the same `$derived` and use it as your memo key. The document itself isn't a usable key: the editor mutates it in place, so its identity never changes, and an identity-keyed memo hits forever on a stale answer. Reading the version inside the derived is also what subscribes your widget to edits anywhere, so N widgets sharing one memoized walk stay as live as N widgets each walking the document.
1481
+ - **Take a click of your own with `claimsActivationClick`.** If your `revealSource` widget handles a click itself, declare `claimsActivationClick` in its editing policy and read `isWidgetActivationClick` to decide when to act: the reveal then does nothing for exactly the gesture that predicate names, so the widget isn't swapped for its source bytes under a click meant to navigate. Without `revealSource` there's no reveal to skip, and the field does nothing.
1381
1482
 
1382
- If your `revealSource` widget takes a click of its own, declare `claimsActivationClick` in its editing policy and read `isWidgetActivationClick` to decide when to act: the surface stands its reveal down for exactly the gesture that predicate names, so the widget isn't swapped for its source bytes under a click meant to navigate. Without `revealSource` there's no reveal to stand down, and the field is inert.
1483
+ **Errors in a component widget are half yours.** A **synchronous mount-time throw** is caught, so the widget falls back to its raw source and an `error` event fires, but the component mounts as its own effect root and nothing catches its post-mount runtime errors. Render a legible error for bad input instead of throwing (the KaTeX widget shows the formula's source in red, with the parser's message on hover). A renderer slot catches a throw and hands it to your `failed`, and `renderSourceFallback(source, message)` gets you most of that view: the source in the code font and the message on hover, with the red left to you. A render engine's stylesheet is likewise yours: import it in the module that owns the renderer, so no route can forget it.
1383
1484
 
1384
- If your widget derives from the whole document, read the version inside the same `$derived` and use it as your memo key. The document itself isn't a usable key: the editor mutates it in place, so its identity never changes, and an identity-keyed memo hits forever on a stale answer. Reading the version inside the derived is also what subscribes your widget to edits anywhere, so N widgets sharing one memoized walk stay as live as N widgets each walking the document.
1485
+ ### Choosing a trigger
1385
1486
 
1386
- **A symmetric delimiter can close itself as it is typed.** Pass `autoPair: true` on a bare trigger whose construct opens and closes on the same byte, the way the bundled latex plugin does for `$…$`: typing the trigger lands its twin after the caret, typing it again over that twin steps past it, and a first body byte the recognizer rejects (a `$` followed by a digit is a price) drops the twin again. Without it a lone `$` typed ahead of an existing formula pairs with that formula's closer and wraps the prose between them. The built-in backtick, `*`, `_` and `~~` behave this way without registration.
1487
+ **A bare trigger must be a character no built-in scanner claims.** Registering a bare recognizer on a reserved trigger (`` ` ``, `&`, `<`, `*`, `_`, `~`, `[`, `]`, `!`, `\`, or newline) throws: built-in dispatch runs first, so a bare recognizer there would never fire, and a silent no-op is the one failure a public API must not have. The trigger is one character; anything longer throws too.
1387
1488
 
1388
- **A bare trigger must be a character no built-in scanner claims.** Registering a bare recognizer on a reserved trigger (`` ` ``, `&`, `<`, `*`, `_`, `~`, `[`, `]`, `!`, `\`, or newline) throws: built-in dispatch runs first, so a bare recognizer there would never fire, and a silent no-op is the one failure a public API must not have.
1489
+ **Several recognizers can share one trigger**, as long as each sits at its own priority or prefix (the same trigger, prefix and priority twice throws). The bundled **emoji** plugin (`@voithos-labs/aragonite/plugins/emoji`) is the bare-trigger recipe end to end: `:shortcode:` recognizes on the bare `:` trigger at `INLINE_PRIORITIES.plugin + 10`, next to the directive text tier's `:` at the default, renders as an atomic glyph widget through `buildWidget` + `mintWidgetShell`, and carries the `{ deleteGranularity: 'atomic', onEdge: 'step-over' }` edge policy so a caret-adjacent Backspace removes the whole `:name:` in one press and a plain arrow steps over it. Disjoint grammars coexist happily that way: a table-lookup miss declines and falls through with the bytes untouched. The literal `:name:` bytes stay in the raw, so an uninstalled document round-trips as ordinary prose.
1389
1490
 
1390
- The bundled **emoji** plugin (`@voithos-labs/aragonite/plugins/emoji`) is this bare-trigger recipe end to end and the worked reference for an inline kind on an unreserved trigger: `:shortcode:` recognizes on the bare `:` trigger, renders as an atomic glyph widget through `buildWidget` + `mintWidgetShell`, and carries the `{ deleteGranularity: 'atomic', onEdge: 'step-over' }` edge policy so a caret-adjacent Backspace removes the whole `:name:` in one press and a plain arrow steps over it. It shares the `:` trigger with the directive text tier, because disjoint grammars coexist happily on one trigger: a table-lookup miss declines and falls through with the bytes untouched. The literal `:name:` bytes stay in the raw, so an uninstalled document round-trips as ordinary prose.
1491
+ **A symmetric delimiter can close itself as it is typed.** Pass `autoPair: true` on a bare trigger whose construct opens and closes on the same byte, the way the bundled latex plugin does for `$…$`: typing the trigger lands its twin after the caret, typing it again over that twin steps past it, and a first body byte that leaves the pair no construct (`$5` is a price) drops the twin again. Without it a lone `$` typed ahead of an existing formula pairs with that formula's closer and wraps the prose between them. The built-in backtick, `*`, `_` and `~~` behave this way without registration. (`autoPair` with a `prefix` throws; it's for bare triggers.)
1391
1492
 
1392
- **To claim syntax that begins on a reserved trigger, register a prefix rung.** A rung is one entry on the ladder of recognizers consulted for a trigger character; a **prefix** rung fires only when its multi-character prefix matches at the cursor. A GFM (GitHub Flavored Markdown) `[^label]` footnote reference starts on `[`, which the link scanner owns. Pass a `prefix` that begins with the trigger and a `priority` below `INLINE_PRIORITIES.builtin`, the inline mirror of an opener pricing below a built-in (the ladder is `{ prefixOverride: 40, builtin: 50, plugin: 100 }`):
1493
+ **To claim syntax that begins on a reserved trigger, register a prefix handler.** Every trigger has recognizers asked in priority order, and a **prefix** handler is only asked where its multi-character prefix matches at the cursor. A GFM (GitHub Flavored Markdown) `[^label]` footnote reference starts on `[`, which the link scanner owns. Pass a `prefix` of two or more characters that begins with the trigger, and a `priority` below `INLINE_PRIORITIES.builtin`, the inline mirror of an opener pricing below a built-in (the order is `{ prefixOverride: 40, builtin: 50, plugin: 100 }`, and a reserved-trigger handler at or above `builtin` throws):
1393
1494
 
1394
1495
  ```ts
1395
1496
  registerInlineSyntax('[', recognizeFootnote, {
@@ -1398,13 +1499,17 @@ registerInlineSyntax('[', recognizeFootnote, {
1398
1499
  });
1399
1500
  ```
1400
1501
 
1401
- The scanner consults the rung ahead of the built-in `[` case, but only when `[^` matches at the cursor, so a plain `[` that opens a link is untouched. Your recognizer claims `[^label]` by returning a node, or declines with `null`. A `[^` that never closes declines and falls back to the built-in link reading, bytes untouched, so an unterminated reference is never a hang and never a byte change. Rungs on one trigger coexist and dispatch by priority ascending, then longer prefixes first, then lexicographic, independent of registration order (the `OPENER_PRIORITIES` model, one layer down). Reach for a replace decoration ([Decorations](#decorations)) only to annotate bytes you do **not** own; syntax that's genuinely your kind's belongs in a prefix rung.
1502
+ The scanner asks your handler ahead of the built-in `[` case, but only when `[^` matches at the cursor, so a plain `[` that opens a link is untouched. Your recognizer claims `[^label]` by returning a node, or declines with `null`. A `[^` that never closes declines and falls back to the built-in link reading, bytes untouched, so an unterminated reference is never a hang and never a byte change. Handlers on one trigger are asked by priority ascending, then longer prefixes first, then lexicographic, independent of registration order (the `OPENER_PRIORITIES` model, one layer down). Reach for a replace decoration ([Decorations](#decorations)) only to annotate bytes you do **not** own; syntax that's genuinely your kind's belongs in a prefix handler.
1402
1503
 
1403
- **`!` takes a prefix rung; `]` still rejects one.** Both sit outside the scanner's fast-bail character set (the cheap check that skips scanning where nothing could match), because they only matter inside a `[`-bearing range, so a rung on either fires only if the bail is taught to visit the character. `!` is taught on demand: registering a prefix rung on it turns on a per-character probe for as long as the registration lives, which is what lets an Obsidian-style `![[embed]]` be a real inline kind instead of a decoration painted over bytes the tree never sees. Prose exclamation marks keep the plain fast path while nothing is registered. `]` has no such route, and a prefix rung on it still throws rather than accept a silent no-op.
1504
+ The bundled **footnotes** plugin (`@voithos-labs/aragonite/plugins/footnotes`) is this recipe end to end and the worked reference to read against your own inline kind: `[^label]` recognizes through a `[^` prefix handler at `INLINE_PRIORITIES.prefixOverride`, renders as a superscript widget whose number derives reactively from the whole document (a `DocumentView` walk memoized on `getContentVersion`, so the number re-derives when a reference is added elsewhere while every mounted widget in a flush shares one walk), reveals its source to edit, and jumps to its definition on the activation click, or on Enter where reading mode gives it a tab stop. The definition's own `[^label]` marker takes the same gestures back to the first reference. The literal `[^label]` bytes stay in the block's raw, so an uninstalled document round-trips as ordinary GFM.
1404
1505
 
1405
- A rung on `!` is consulted ahead of the built-in `!` case, so it outranks the image grammar wherever its prefix matches. And the two grammars do overlap: an image whose alt text opens with `[` starts on `![[` as well, so `![[a.png]]` carrying a parenthesized destination after it is a built-in image with the alt text `[a.png]`, not an embed. Deciding that overlap is your recognizer's job. Decline it (return `null`) and the built-in image reads the bytes unchanged. **Getting it wrong fails silently.** An ungated `![[` recognizer swallows the image with no throw and no dev-warn, and since the raw bytes are untouched the document still round-trips cleanly, so no round-trip check and no conformance cell in your own suite will ever see it. The first report comes from a reader whose picture stopped rendering.
1506
+ **`!` takes a prefix handler; `]` still rejects one.** Both sit outside the scanner's fast-bail character set (the cheap check that skips scanning where nothing could match), because they only matter inside a `[`-bearing range, so a handler on either fires only if the bail is taught to visit the character. `!` is taught on demand: registering a prefix handler on it turns on a per-character probe for as long as the registration lives, which is what lets an Obsidian-style `![[embed]]` be a real inline kind instead of a decoration painted over bytes the tree never sees. Prose exclamation marks keep the plain fast path while nothing is registered. `]` has no such route, and a prefix handler on it still throws rather than accept a silent no-op.
1406
1507
 
1407
- **Bound the decline, not just the claim.** Your recognizer is consulted at every occurrence of its trigger, so a decline that searches to the end of the block costs one block scan per trigger, which goes quadratic on a large paragraph, and the trigger is often ordinary prose (`$HOME $PATH …` for `$`). Stop at the first character your grammar can't contain, the way the emoji recognizer stops at the first non-shortcode byte. Where the grammar has no such character, index the candidate positions once per block with `createScanIndex` (hand it your position collector, get back a "first candidate at or after this offset" lookup), the way the bundled math and footnote recognizers index their closers:
1508
+ A handler on `!` is asked ahead of the built-in `!` case, so it outranks the image grammar wherever its prefix matches. And the two grammars do overlap: an image whose alt text opens with `[` starts on `![[` as well, so `![[a.png]]` carrying a parenthesized destination after it is a built-in image with the alt text `[a.png]`, not an embed. Deciding that overlap is your recognizer's job. Decline it (return `null`) and the built-in image reads the bytes unchanged. **Getting it wrong fails silently.** An ungated `![[` recognizer swallows the image with no throw and no dev-warn, and since the raw bytes are untouched the document still round-trips cleanly, so no round-trip check in your own suite will ever see it (the inline kit's `overlapDecline` cell will, if you hand it the overlap). The first report otherwise comes from a reader whose picture stopped rendering.
1509
+
1510
+ ### Keeping a decline cheap
1511
+
1512
+ **Bound the decline, not just the claim.** Your recognizer is asked at every occurrence of its trigger, so a decline that searches to the end of the block costs one block scan per trigger, which goes quadratic on a large paragraph, and the trigger is often ordinary prose (`$HOME $PATH …` for `$`). Stop at the first character your grammar can't contain, the way the emoji recognizer stops at the first non-shortcode byte. Where the grammar has no such character, index the candidate positions once per block with `createScanIndex` (hand it your position collector, get back a "first candidate at or after this offset" lookup), the way the bundled math recognizer indexes every `$` and the footnote one its closers:
1408
1513
 
1409
1514
  ```ts
1410
1515
  const dollarAt = createScanIndex((raw) => {
@@ -1416,49 +1521,52 @@ dollarAt('pay $HOME $5 for $x$', 5); // 10, the first candidate at or after offs
1416
1521
  dollarAt('pay $HOME $5 for $x$', 20); // -1, none left
1417
1522
  ```
1418
1523
 
1419
- The bundled **footnotes** plugin (`@voithos-labs/aragonite/plugins/footnotes`) is this recipe end to end and the worked reference to read against your own inline kind: `[^label]` recognizes through a `[^`-prefix rung at `INLINE_PRIORITIES.prefixOverride`, renders as a superscript widget whose number derives reactively from the whole document (a `DocumentView` walk memoized on `getContentVersion`, so the number re-derives when a reference is added elsewhere while every mounted widget in a flush shares one walk), reveals its source to edit, and jumps to its definition on the activation click. The definition's own `[^label]` marker takes the same click back to the first reference. The literal `[^label]` bytes stay in the block's raw, so an uninstalled document round-trips as ordinary GFM.
1524
+ ### Building a built-in node
1420
1525
 
1421
- **If your rung builds a built-in kind's node, it owns writing those bytes back.** A rung may return a node of a kind the editor already has, say an `![[cat.png|300]]` that is a real `image`, so the widget renders it, the caret addresses it, and the resize handles appear. Every _read_ path then treats it as an image, which is the point. The _write_ paths can't: the editor's inverse for a built-in kind emits that kind's built-in grammar, so re-serializing your node's fields brings `![[cat.png|300]]` back as a GFM image, bracketed alt and parenthesized destination, and your syntax is gone. Supply a `rewriteImage` hook and the edit comes back to you instead:
1526
+ **If your handler builds a built-in kind's node, it owns writing those bytes back.** A handler may return a node of a kind the editor already has, say an `![[cat.png|300]]` that is a real `image`, so the widget renders it, the caret addresses it, and the resize handles appear. Every _read_ path then treats it as an image, which is the point. The _write_ paths can't: the editor's inverse for a built-in kind emits that kind's built-in grammar, so re-serializing your node's fields brings `![[cat.png|300]]` back as a GFM image, bracketed alt and parenthesized destination, and your syntax is gone. Supply a `rewriteImage` hook and the edit comes back to you instead:
1422
1527
 
1423
1528
  ```ts
1424
1529
  registerInlineSyntax('!', recognizeEmbed, {
1425
1530
  prefix: '![[',
1426
1531
  priority: INLINE_PRIORITIES.prefixOverride,
1427
1532
  rewriteImage: (source, fields) => {
1428
- if (!source.startsWith('![[')) return null; // bytes this rung did not shape
1533
+ if (!source.startsWith('![[')) return null; // bytes this handler did not shape
1429
1534
  // Decline what this grammar cannot store rather than dropping it silently: it
1430
1535
  // holds a target and an optional width and nothing else. The alt line is THIS
1431
1536
  // recognizer's version of that rule: it fills alt and url from the one target,
1432
1537
  // so an alt that no longer matches is an edit with no form here. Write yours
1433
1538
  // against however your own recognizer fills the node.
1434
1539
  if (fields.title !== undefined || fields.label !== undefined) return null;
1540
+ if (fields.height !== undefined || fields.crop !== undefined) return null;
1435
1541
  if (fields.alt !== fields.url) return null;
1436
1542
  return `![[${fields.url}${fields.width !== undefined ? `|${fields.width}` : ''}]]`;
1437
1543
  }
1438
1544
  });
1439
1545
  ```
1440
1546
 
1441
- `source` is the node's current bytes; return their replacement in your grammar. Return **`null` when the edit has no form in your syntax** (an embed has nowhere to put a title) and the editor declines the edit rather than writing something you didn't author. **A rung with no hook declines every such edit**, which is the safe default: the affordance is live and visibly does nothing, and a dev build logs which rung declined and why. Nothing is silently rewritten either way, and images the built-in scanner read are untouched. Bytes your rung _declines_, including the overlap above where the alt text merely begins with `[`, stay the editor's to resize as always.
1547
+ `source` is the node's current bytes; return their replacement in your grammar. Return **`null` when the edit has no form in your syntax** (an embed has nowhere to put a title) and the editor declines the edit rather than writing something you didn't author. **A handler with no hook declines every such edit**, which is the safe default: the affordance is live and visibly does nothing, and a dev build logs which handler declined and why. Nothing is silently rewritten either way, and images the built-in scanner read are untouched. Bytes your handler _declines_, including the overlap above where the alt text merely begins with `[`, stay the editor's to resize as always.
1442
1548
 
1443
1549
  Three edges the snippet above is shaped by, and each one bites if you drop it:
1444
1550
 
1445
- - **Read every field, or decline it.** A hook that ignores a field the user edited returns byte-identical bytes, and byte-identical bytes are dropped by the commit's equality guard, **silently, with no dev warn**, because your hook returned bytes rather than `null`. The Alt row of the editor's image-properties popover then simply does nothing, with no diagnostic anywhere. Decline instead, and the limit is at least visible.
1551
+ - **Read every field, or decline it.** A hook that ignores a field the user edited returns byte-identical bytes, and byte-identical bytes are dropped by the commit's equality guard, **silently, with no dev warn**, because your hook returned bytes rather than `null`. The Alt row of the editor's image-properties popover then simply does nothing, with no diagnostic anywhere, and so does an unlocked resize (it writes `height`) or a crop. Decline instead, and the limit is at least visible.
1446
1552
  - **Guard every optional field you interpolate.** `fields.width` is absent on an embed that never carried one, and an unguarded template writes the literal `|undefined` into the document.
1447
- - **Bound the hook to bytes you shaped.** The claim reaches _descendants_ of the node your recognizer returned, so a rung that returns its own kind wrapping a built-in `image` gets called with the **inner** node's slice, not the whole construct. Checking `source` before rewriting is what keeps that from nesting your syntax inside itself.
1553
+ - **Bound the hook to bytes you shaped.** The claim reaches _descendants_ of the node your recognizer returned, so a handler that returns its own kind wrapping a built-in `image` gets called with the **inner** node's slice, not the whole construct. Checking `source` before rewriting is what keeps that from nesting your syntax inside itself.
1448
1554
 
1449
- **Errors in a component widget are half yours.** A **synchronous mount-time throw** is caught, so the widget falls back to its raw source and an `error` event fires, but the component mounts as its own effect root and nothing catches its post-mount runtime errors. Render a legible error for bad input instead of throwing (the KaTeX widget shows an inline message). A render engine's stylesheet is likewise yours: import it in the module that owns the renderer, so no route can forget it.
1555
+ ### The editing policy
1450
1556
 
1451
- **The inline tier isn't the block surface in miniature.** An inline kind gets recognition, rendering, atomic caret addressing at its edges, and an editing policy on its widget registration. The policy has five fields, all optional:
1557
+ The policy on your widget registration says how the caret and the delete keys treat it. Its fields, all optional:
1452
1558
 
1453
- | Field | What it decides |
1454
- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------- |
1455
- | `revealSource` | Open the source (the `$…$` bytes) for editing on caret entry; inline math's model |
1456
- | `onSelectedKey` | A handler for keys while the widget is selected; image resize rides it |
1457
- | `onEdge` | `'select' \| 'step-over'`: an edge press selects the whole widget, or steps transparently over it |
1458
- | `deleteGranularity` | `'atomic' \| 'select-then-delete'`: one press deletes the whole widget, or the first press selects and the second deletes |
1459
- | `claimsActivationClick` | Your component handles the activation click itself, so the surface's reveal stands down for it; the footnote jump's model |
1559
+ | Field | What it decides |
1560
+ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1561
+ | `revealSource` | Open the source (the `$…$` bytes) for editing on caret entry; inline math's model |
1562
+ | `revealContentSpan` | Where the editable content sits inside the source (`$x$` answers `{ start: 1, end: 2 }`), so a caret entering the source stays between the delimiters; absent, it keeps the leading edge |
1563
+ | `revealOffsetAtPoint` | Which source offset a press on the rendered widget names, so a click puts the caret where it landed; inline math walks its KaTeX glyphs for this, and `null` falls back to the content span's end |
1564
+ | `onSelectedKey` | A handler for keys while the widget is selected; image resize rides it |
1565
+ | `onEdge` | `'select' \| 'step-over'`: an edge press selects the whole widget, or steps transparently over it; `'step-over'` also makes a press on the widget put the caret at the edge it landed by, where `'select'` leaves the widget its own click; and Up or Down onto a block holding only a step-over widget puts the caret beside it, one press in and one out |
1566
+ | `deleteGranularity` | `'atomic' \| 'select-then-delete'`: one press deletes the whole widget, or the first press selects and the second deletes |
1567
+ | `claimsActivationClick` | Your component handles the activation click itself, so the reveal does nothing for it; the footnote jump's model |
1460
1568
 
1461
- Both edge fields are live today: the built-in decoded-entity widget (`&copy;` → ©) ships `{ deleteGranularity: 'atomic', onEdge: 'step-over' }`, so a caret-adjacent Backspace removes it whole and a plain arrow walks the caret across it like a character, the caret-edge dispatch reading both off the widget registration. The inline tier gets **no keymap, no minted commands, and no per-node metadata**: `InlineNode` has no metadata field, so unlike a block kind it stores nothing at all on the node.
1569
+ Both edge fields are live today: the built-in decoded-entity widget (`&copy;` → ©) ships `{ deleteGranularity: 'atomic', onEdge: 'step-over' }`, so a caret-adjacent Backspace removes it whole and a plain arrow walks the caret across it like a character, the caret-edge handling and the click both reading off the widget registration.
1462
1570
 
1463
1571
  ## Decorations
1464
1572
 
@@ -1487,12 +1595,12 @@ setup(ctx) {
1487
1595
 
1488
1596
  ### The four decoration types
1489
1597
 
1490
- | Type | Shape | Renders as |
1491
- | --------- | -------------------------------------------------------- | ------------------------------------------------------------------------------ |
1492
- | `mark` | `{ type: 'mark', path, start, end, class }` | A positioned overlay span over the inline range; style it via the class |
1493
- | `widget` | `{ type: 'widget', path, offset, widget }` | A zero-width atomic island at the offset (ghost text's shape) |
1494
- | `replace` | `{ type: 'replace', path, start, end, widget?, class? }` | An atomic island covering the range; the hidden bytes stay in the document |
1495
- | `block` | `{ type: 'block', path, class?, attrs?, badge? }` | A class/attrs treatment on the whole block host, plus an optional badge widget |
1598
+ | Type | Shape | Renders as |
1599
+ | --------- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1600
+ | `mark` | `{ type: 'mark', path, start, end, class, attrs? }` | A positioned overlay span over the inline range; style it via the class |
1601
+ | `widget` | `{ type: 'widget', path, offset, widget, side? }` | A zero-width atomic inline widget at the offset (ghost text's shape), drawn `'after'` it by default or `'before'` |
1602
+ | `replace` | `{ type: 'replace', path, start, end, widget?, class? }` | An atomic inline widget covering the range; the hidden bytes stay in the document |
1603
+ | `block` | `{ type: 'block', path, class?, attrs?, badge? }` | A class/attrs treatment on the whole block (a list item, table row or cell included), plus an optional badge widget (not on a row or cell). Attributes the editor already uses on the block's element (the `data-` names it sets or looks up there, `role`, `tabindex` and friends) get dropped with a dev warning, so pick names of your own |
1496
1604
 
1497
1605
  One `provide` answer using two of them, shapes side by side:
1498
1606
 
@@ -1503,13 +1611,13 @@ provide: (doc) => [
1503
1611
  ];
1504
1612
  ```
1505
1613
 
1506
- Offsets are **raw offsets** into the target block, dimmed markers included, which is the same coordinate space `getContentRange` describes. A `widget`, `replace` widget, or `badge` takes a `DecorationWidgetSpec`: a Svelte `component` (receives the decoration as its prop) or a hand-built `buildDom`. An interactive mark takes `interactive: { onClick }`, not a top-level `onClick`; interactive DOM inside an island is native, so wire your own listeners in `buildDom`.
1614
+ Offsets are **raw offsets** into the target block, dimmed markers included, which is the same coordinate space `getContentRange` describes. A `widget`, `replace` widget, or `badge` takes a `DecorationWidgetSpec`: a Svelte `component` (receives the decoration as its prop) or a hand-built `buildDom`. An interactive mark takes `interactive: { onClick }`, not a top-level `onClick`; interactive DOM inside a widget is native, so wire your own listeners in `buildDom`.
1507
1615
 
1508
- Islands (`widget` / `replace`) render in prose blocks and in table cells, applied through the same machinery in both; `mark` and `block` decorations serve cells too. Island caret behavior is defined and pinned: arrows step over, destructive keys treat a widget island as transparent and select-then-delete a replace island whole, so the hidden bytes are never silently corrupted.
1616
+ `widget` and `replace` decorations render in prose blocks and in table cells, applied through the same machinery in both; `mark` and `block` decorations serve cells too. Their caret behavior is defined and pinned: arrows step over, destructive keys treat a `widget` as transparent and select-then-delete a `replace` whole, so the hidden bytes are never silently corrupted.
1509
1617
 
1510
1618
  ### Recipe: memoize the scan on `editEpoch`
1511
1619
 
1512
- `provide` runs on every document change, so an expensive scan wants a memo. Do **not** key it on `doc.children` identity, because routine typing mutates the tree in place. The second `provide` argument carries `editEpoch`, a counter that bumps once per document change (an edit, or a whole-document `source` replacement) and **never** on `invalidate()`, which is exactly the split a memo needs: epoch miss, the document changed, rescan; epoch hit, only your own state changed, remap the cached scan.
1620
+ `provide` runs on every document change, so an expensive scan wants a memo. Do **not** key it on `doc.children` identity, because routine typing mutates the tree in place. The second `provide` argument carries `editEpoch`, a counter that bumps once per document change (an edit, or a whole-document `source` replacement) and **never** on `invalidate()`, which is exactly the split a memo needs: epoch miss, the document changed, rescan; epoch hit, only your own state changed, remap the cached scan. The epoch can't tell a keystroke from a swap; the `sourceSwap` event can, since it fires ahead of the swap's epoch.
1513
1621
 
1514
1622
  ```ts
1515
1623
  let lastEpoch = -1;
@@ -1534,11 +1642,11 @@ editor.events.on('selectionChange', (sel) => {
1534
1642
  });
1535
1643
  ```
1536
1644
 
1537
- Keying the cache on an index (word to marks) rather than a flat list makes the per-invalidate step a map read, not a re-filter of every mark. The bundled `highlight-occurrences` plugin (`@voithos-labs/aragonite/plugins/highlight-occurrences`) is this recipe end to end, plus one capability gate: it indexes only inline-prose leaves (`isProseKind`, the descriptor's `supportsInline`), so a fenced code block's bytes are neither scanned nor a valid anchor. It carries a second memo inside the rebuild, because routine typing bumps the epoch on every keystroke: each leaf's token list is keyed on that leaf's own text, so a rebuild re-tokenizes only the block you are typing in and rebuilds the word map from the cached lists. And it steps its marks aside while you're typing, since a word lighting up under your own caret mid-sentence is maddening. The tell is an epoch that arrives with no `edit` event ahead of it (a keystroke announces nothing until its burst flushes), so the source serves nothing until the batched `input` event lands at the end of the burst. That's the editor's own typing pause, not a timer of the plugin's.
1645
+ Keying the cache on an index (word to marks) rather than a flat list makes the per-invalidate step a map read, not a re-filter of every mark. The bundled `highlight-occurrences` plugin (`@voithos-labs/aragonite/plugins/highlight-occurrences`) is this recipe end to end, plus one capability gate: it indexes only inline-prose leaves (`isProseKind`, the descriptor's `supportsInline`), so a fenced code block's bytes are neither scanned nor a valid anchor. It carries a second memo inside the rebuild, because routine typing bumps the epoch on every keystroke: each leaf's token list is keyed on that leaf's own text, so a rebuild re-tokenizes only the block you are typing in and rebuilds the word map from the cached lists. And it steps its marks aside while you're typing, since a word lighting up under your own caret mid-sentence is maddening. The tell is an epoch that arrives with no `edit` or `sourceSwap` event ahead of it (a keystroke announces nothing until its burst flushes), so the source serves nothing until the batched `input` event lands at the end of the burst. That's the editor's own typing pause, not a timer of the plugin's.
1538
1646
 
1539
1647
  A source that throws is contained: the editor emits an `error` event attributed to your source name and keeps the previous decorations on screen, so a throw never blanks the view.
1540
1648
 
1541
- Pair a source with `editor.rects` when you need geometry (anchor a popup to a decorated range, say): `rects.rangeRects(path, start, end)` returns viewport-space rects for any measurable range, one per visual line.
1649
+ Pair a source with `editor.rects` when you need geometry (anchor a popup to a decorated range, say): `rects.rangeRects(path, start, end)` returns viewport-space rects for any measurable range, one per visual line (one per cell in a table).
1542
1650
 
1543
1651
  ```ts
1544
1652
  editor.rects.rangeRects([2], 4, 9); // [DOMRect { x: 96, y: 412, width: 38, height: 22, ... }], one per visual line the range crosses
@@ -1548,7 +1656,7 @@ editor.rects.rangeRects([2], 4, 9); // [DOMRect { x: 96, y: 412, width: 38, heig
1548
1656
 
1549
1657
  **`registerBlockCommand(kind, name, handler)`**
1550
1658
 
1551
- Mints a `(kind, name)` command and returns its id, which a keymap binding then targets; the walkthrough's `conspiracy.setVerdict` is the worked mint. The name is process-wide, but the registry key is `(kind, name)` and dispatch is kind-scoped, so you may reuse one command name across several of your own kinds (one `conspiracy.setVerdict` on every kind your plugin ships). A name already taken by a **different** plugin is rejected.
1659
+ Creates a `(kind, name)` command and returns its id, which a keymap binding then targets; the walkthrough's `conspiracy.setVerdict` is the worked example. The name is dot-separated words that each start with a lowercase letter (`conspiracy.setVerdict`), and it can't be a built-in command's id. It's process-wide, but the registry key is `(kind, name)` and dispatch is kind-scoped, so your plugin may reuse one command name across several of its own kinds (one `conspiracy.setVerdict` on every kind it ships), as long as the registrations run inside its `setup`. A name already taken by a **different** plugin is rejected, and so is a reuse from outside any plugin's setup.
1552
1660
 
1553
1661
  ```ts
1554
1662
  const setVerdict = registerBlockCommand(conspiracy, 'conspiracy.setVerdict', (ctx) => {
@@ -1561,31 +1669,31 @@ setVerdict; // 'conspiracy.setVerdict', branded as a command id
1561
1669
  registerBlockCommand(conspiracy, 'conspiracy.setVerdict', handler); // throws: already registered
1562
1670
  ```
1563
1671
 
1564
- A minted command dispatches on the two tiers that can hand it a `BlockCommandContext` (the focused node plus a metadata-commit route):
1672
+ A block command dispatches on the two tiers that can hand it a `BlockCommandContext` (the focused node plus a metadata-commit route):
1565
1673
 
1566
1674
  - the **editable-leaf tier**, a `createEditableLeaf` block, resolved from the focused leaf's keymap;
1567
1675
  - the **container-bubble tier**, a container-factory block, resolved as a chord bubbles up from an inner leaf.
1568
1676
 
1569
1677
  Bind commands to your own plugin kinds. A command bound on a built-in kind's leaf (paragraph, code, table cell) does **not** dispatch: those surfaces supply no context, and the chord is swallowed.
1570
1678
 
1571
- The consumer route `editor.runCommand(id)` reaches neither of those tiers: it resolves the focused surface without a command context, so a **block**-minted id finds no handler and dev-warns that the command reached no handler on this dispatch path. Bind a chord, or expose an API of your own, for a block affordance a host must invoke without a keystroke. A **global** command isn't so limited: its name resolves ahead of the block tiers, so `editor.runCommand('wordCount.log')` runs it and `canRunCommand` answers `true` for it (below).
1679
+ The consumer route `editor.runCommand(id)` reaches neither of those tiers: it resolves the focused surface without a command context, so a **block** command's id finds no handler and dev-warns that the command reached no handler on this dispatch path. Bind a chord, or expose an API of your own, for a block affordance a host must invoke without a keystroke. A **global** command isn't so limited: its name resolves ahead of the block tiers, so `editor.runCommand('wordCount.log')` runs it and `canRunCommand` answers `true` for it (below).
1572
1680
 
1573
1681
  **View state rides `ctx.hooks`.** Because the context is built by the surface that owns the mounted component, it also carries the component's own view-state handles, supplied through the factory's `commandHooks` getter. A view-state command (open an editor, open a focus overlay) therefore drives the component directly, with no node-keyed side map. Hand `createContainerBlock` a `commandHooks: () => ({ openEdit, openFocusView })` getter (read live at dispatch, so an undo that replaces the node still hits the current handlers). The platform keeps `hooks` opaque (`unknown`): cast it to your own type in the handler, and decline when it's `undefined`, which means the kind is registered with no instance mounted.
1574
1682
 
1575
- A handler that throws is contained at the dispatch boundary: the gesture no-ops and the failure surfaces on `getEvents()` as an `error` of origin `command`, attributed to the kind, command id, and owning plugin.
1683
+ A handler that throws is contained at the dispatch boundary: the gesture no-ops and the failure surfaces on `getEvents()` as an `error` of origin `command`, attributed to the kind, the command id, and the plugin that registered the command. That's also the plugin whose `EditorContext` the handler gets as `ctx.editor`, even when the kind belongs to someone else.
1576
1684
 
1577
- **`registerGlobalCommand(name, handler, { chord })`**
1685
+ **`registerGlobalCommand(name, handler, { chord }?)`**
1578
1686
 
1579
- The editor-wide sibling: it mints a process-wide command whose handler receives the dispatching instance's `EditorContext` rather than a block, so it runs regardless of which block holds focus, for editor-scope actions like opening a panel. Call it from `setup`:
1687
+ The editor-wide sibling: it creates a process-wide command whose handler receives the dispatching instance's `EditorContext` rather than a block, so it runs regardless of which block holds focus, for editor-scope actions like opening a panel. Its second argument is whatever `runCommand(id, arg)` or the chord's binding passed, `undefined` when neither did. The chord is optional; without one the command runs only through `runCommand`. Call it from `setup`:
1580
1688
 
1581
1689
  ```ts
1582
1690
  setup(ctx) {
1583
1691
  registerGlobalCommand(
1584
1692
  'wordCount.log',
1585
1693
  (editor) => {
1586
- // The mint is not generic-bound: the handler gets EditorContext<unknown>,
1587
- // so narrow options here (onEditor's callback is where they read typed).
1588
- const opts = editor.options as WordCountOptions | undefined;
1694
+ // The handler isn't bound to your options type: it gets EditorContext<unknown>,
1695
+ // so cast options here (onEditor's callback is where they read typed).
1696
+ const opts = editor.options as WordCountOptions;
1589
1697
  console.log(`[${editor.editorId}]`, countByEditor.get(editor.editorId), opts);
1590
1698
  return true; // handled
1591
1699
  },
@@ -1595,10 +1703,10 @@ setup(ctx) {
1595
1703
  }
1596
1704
  ```
1597
1705
 
1598
- The chord binds in the **plugin-global tier**, the last rung of the ladder [the consumer guide's Rebinding chords](consumer-guide.md#rebinding-chords) lays out. Three consequences:
1706
+ The chord binds in the **plugin-global tier**, the last step in the chord priority order [the consumer guide's Rebinding chords](consumer-guide.md#rebinding-chords) lays out. Three consequences:
1599
1707
 
1600
1708
  - A plugin chord never shadows a built-in, and the reverse shadow is by design: a built-in kind's own chord beats your plugin chord **on that kind, not elsewhere**.
1601
- - A chord the global tier already binds (undo and redo, or another plugin's global chord) or the search bar reserves (`Mod+F` / `Mod+H`) is unstealable, and the collision **throws before the mint**, leaving no half-registered command. A built-in kind's chord doesn't throw; it just wins on that kind, per the first bullet.
1709
+ - A chord the global tier already binds (undo and redo, or another plugin's global chord) or the search bar reserves (`Mod+F` / `Mod+H`) is unstealable, and the collision **throws before the command is created**, leaving no half-registered command. A built-in kind's chord doesn't throw; it just wins on that kind, per the first bullet.
1602
1710
  - A handler throw is contained identically, surfacing as an `error` of origin `command` attributed to the owning plugin.
1603
1711
 
1604
1712
  ```ts
@@ -1608,16 +1716,16 @@ registerGlobalCommand('mine.undo', handler, { chord: 'Mod+Z' }); // throws: alre
1608
1716
  registerGlobalCommand('mine.bold', handler, { chord: 'Mod+B' }); // fine: fires on a thematic break, yields to bold in a paragraph
1609
1717
  ```
1610
1718
 
1611
- Chord strings follow the consumer guide's chord model: fixed-order `Mod` / `Alt` / `Shift` plus the key's own value. Shifted-symbol chords aren't modeled, so bind plain digits and letters.
1719
+ Chord strings follow the consumer guide's chord model: fixed-order `Mod` / `Alt` / `Shift` plus the key's own value. Shifted-symbol chords aren't modeled, so bind plain digits and letters. A chord the editor can't read throws when you register it, here and in a kind's `keymap` alike (`registerBlockKind`, `augmentBlockKind`). So `Ctrl+B` (it's `Mod+B`) fails at startup instead of quietly becoming a bare `B` that fires on every keypress.
1612
1720
 
1613
1721
  ## Block context actions
1614
1722
 
1615
- **`registerBlockContextActions(kind, provider)`**
1723
+ **`registerBlockContextActions(kind, name, provider)`**
1616
1724
 
1617
- The right-click menu on a block of `kind` (a code block, a table, a plugin's own block) lists what its providers return, ahead of the editor's own rows (copy, replace with the clipboard, remove). Register from `setup`. The provider is consulted on every open, so it reads the block as it is then; several may stack on one kind, and `EVERY_KIND` (`'*'`) registers for every kind. Prose is the page's background: a paragraph or heading keeps the browser's own menu and consults no provider.
1725
+ The right-click menu on a block of `kind` (a code block, a table, a plugin's own block) lists what its providers return, ahead of the editor's own rows (copy, replace with the clipboard, remove). Register from `setup`; the rows show only in the editors that list your plugin, and no menu opens in reading mode. The provider is consulted on every open, so it reads the block as it is then. Several providers can share one kind under different names (a taken name throws), and the kind `'*'` registers for every kind, listed after the editor's own rows. Prose never asks your provider: right-clicking the text of a block whose kind declares `pageRole: 'prose'` (a paragraph, a heading) gives the clipboard rows instead. The provider's third argument, `noun`, is what the menu calls the block ("code block", or "images" for a paragraph of two pictures), handy when you want a label that matches the editor's own "Copy code block".
1618
1726
 
1619
1727
  ```ts
1620
- registerBlockContextActions(conspiracy, (node) => [
1728
+ registerBlockContextActions(conspiracy, 'debunk', (node) => [
1621
1729
  {
1622
1730
  id: 'conspiracy.debunk',
1623
1731
  label: 'Mark debunked',
@@ -1627,11 +1735,11 @@ registerBlockContextActions(conspiracy, (node) => [
1627
1735
  ]);
1628
1736
  ```
1629
1737
 
1630
- `run` receives a `BlockActionContext`: the node, its path, `deleteBlock()`, and `replaceRaw(raw)`, which rewrites the block's bytes wholesale and reparses them, the road the default replace row takes. Each is one undo entry. `icon` names a glyph the editor's menus already draw (the same set the code rail and the table menu use); a row without one shows none. `danger` paints the row in the error colour, for an action that is not one undo away.
1738
+ `run` receives a `BlockActionContext`: the node, its path (the menu opens on top-level blocks, so a one-index path), the document's `lineEnding`, `deleteBlock()`, and `replaceRaw(raw)`, which rewrites the block's bytes wholesale through your kind's `rawWrite` rule (if it has one) and reparses them, the same path the default replace row takes. Each is one undo entry. `replaceRaw` writes every line break in the document's own ending, so a CRLF document stays CRLF whatever your bytes carry. An action that writes clipboard text runs it through `transformPaste(text)` first, so it gets the rewrites a paste into that editor would. `icon` names a glyph the editor's menus already draw (the same set the code rail and the table menu use); a row without one shows none. `danger` paints the row in the error colour, for an action that is not one undo away.
1631
1739
 
1632
1740
  ## Paste transforms
1633
1741
 
1634
- `registerPasteTransform` records a **content-keyed, pre-parse** rewrite of pasted plain text. Each transform is a `{ name, transform(text) }` unit: `transform` returns a replacement string, or `null` to decline ("not mine"). Transforms run at every paste site before the clipboard text is parsed, in **install order**, each one seeing the previous transform's output, so a plugin keys off the _content_ it recognizes rather than the block it lands in. The name is unique (register-once; a duplicate throws, naming the owning plugin) and scopes the transform for attribution.
1742
+ `registerPasteTransform` records a **content-keyed, pre-parse** rewrite of pasted plain text. Each transform is a `{ name, transform(text) }` unit: `transform` returns a replacement string, or `null` to decline ("not mine"). Transforms run at every paste site before the clipboard text is parsed, in **install order**, each one seeing the previous transform's output, so a plugin keys off the _content_ it recognizes rather than the block it lands in. The first one always gets LF line breaks, even when the clipboard held CRLF (hi, Windows), so a `^...$` pattern with the `m` flag just works. The name is unique (register-once; a duplicate throws, naming the owning plugin) and scopes the transform for attribution.
1635
1743
 
1636
1744
  ```ts
1637
1745
  registerPasteTransform({
@@ -1646,7 +1754,9 @@ registerPasteTransform({ name: 'shout', transform: () => null }); // throws: "sh
1646
1754
  Two habits keep a transform sound:
1647
1755
 
1648
1756
  - **Decline cheaply, then convert precisely.** Probe the text for your marker first and return `null` when it's absent. The pipeline runs on every paste, so a fast reject keeps the common case free.
1649
- - **Scope through the parser, not a naive text scan.** A line-level scanner rewrites marker-shaped lines that happen to sit inside a pasted code fence; a converter that parses first and rewrites only the blocks it means to is fence-safe. Keep the transform **idempotent**, meaning re-running it on its own output must decline or reproduce it. A dev warning fires otherwise, catching paste feedback loops.
1757
+ - **Scope through the parser, not a naive text scan.** A line-level scanner rewrites marker-shaped lines that happen to sit inside a pasted code fence; a converter that parses first and rewrites only the blocks it means to is fence-safe. Keep the transform **idempotent**, meaning re-running it on its own output must decline or reproduce it. A dev build checks that on every paste and warns otherwise, catching paste feedback loops.
1758
+
1759
+ A transform that throws doesn't take the paste down with it: it counts as a decline, the text carries on untouched, and a dev build warns.
1650
1760
 
1651
1761
  The admonitions plugin is the worked example. It renders `> [!NOTE]` GitHub alerts as a native container kind with their bytes untouched, so the paste transform is **opt-in** (`admonitionsPlugin({ convertAlertsOnPaste: true })`, default off): when enabled it probes for an alert blockquote and converts only the top-level ones to `:::name` directive source through a parse-scoped converter, so an alert-shaped line inside a pasted fence survives literally. The transform serves pastes; a host button running the same converter over `getSource()` serves already-loaded documents whichever way the transform is set.
1652
1762
 
@@ -1685,7 +1795,7 @@ A plugin **may**:
1685
1795
  - Register kinds, components, and openers, once; a duplicate throws.
1686
1796
  - Declare a `rebuildRaw` and have the editor invoke it when the document changes.
1687
1797
  - Build containers and chrome through the factories.
1688
- - Store primitive per-node metadata, and commit metadata through the sanctioned update path.
1798
+ - Store primitive per-node metadata, and commit metadata through the supported update path.
1689
1799
  - Contribute per-kind keymaps over the command vocabulary.
1690
1800
  - Render as an unknown kind and degrade to a visible raw fallback.
1691
1801
  - Transform pasted plain text before it's parsed ([Paste transforms](#paste-transforms)).
@@ -1712,7 +1822,3 @@ Why the dev build is where plugin development belongs, stated as what each mista
1712
1822
  | An opener claims no line (`consumed < 1`) | Warns, naming the kind, and declines the opener | Declines the same way, silently; no hang |
1713
1823
  | An opener's `raw` ≠ the lines it consumed | Parse warns, naming the kind | Silent round-trip break |
1714
1824
  | An opener throws | Propagates uncaught (parse runs at init and on every edit) | Same; uncaught |
1715
-
1716
- ## Where to go next
1717
-
1718
- Verifying what you built is [`plugin-testing.md`](plugin-testing.md): the round-trip checks, the test entry point, and the conformance kits. Every export named above is cataloged in [`plugin-api.md`](plugin-api.md).