@voithos-labs/aragonite 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (900) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +365 -0
  3. package/THIRD-PARTY-NOTICES.md +102 -0
  4. package/dist/a11y-strings.d.ts +46 -0
  5. package/dist/a11y-strings.js +62 -0
  6. package/dist/action-contracts.d.ts +301 -0
  7. package/dist/action-contracts.js +5 -0
  8. package/dist/active-editor.d.ts +16 -0
  9. package/dist/active-editor.js +67 -0
  10. package/dist/ambient/ambient-cursor.d.ts +41 -0
  11. package/dist/ambient/ambient-cursor.js +120 -0
  12. package/dist/ambient/ambient-dom.d.ts +9 -0
  13. package/dist/ambient/ambient-dom.js +102 -0
  14. package/dist/assert.d.ts +6 -0
  15. package/dist/assert.js +17 -0
  16. package/dist/block-component.d.ts +274 -0
  17. package/dist/block-component.js +43 -0
  18. package/dist/block-id.d.ts +14 -0
  19. package/dist/block-id.js +34 -0
  20. package/dist/bounded-memo.d.ts +14 -0
  21. package/dist/bounded-memo.js +32 -0
  22. package/dist/components/BlockDragHandle.svelte +66 -0
  23. package/dist/components/BlockDragHandle.svelte.d.ts +18 -0
  24. package/dist/components/BlockHost.svelte +328 -0
  25. package/dist/components/BlockHost.svelte.d.ts +15 -0
  26. package/dist/components/BlockList.svelte +114 -0
  27. package/dist/components/BlockList.svelte.d.ts +16 -0
  28. package/dist/components/DecorationOverlay.svelte +156 -0
  29. package/dist/components/DecorationOverlay.svelte.d.ts +14 -0
  30. package/dist/components/Editor.svelte +1849 -0
  31. package/dist/components/Editor.svelte.d.ts +49 -0
  32. package/dist/components/GapCaret.svelte +226 -0
  33. package/dist/components/GapCaret.svelte.d.ts +9 -0
  34. package/dist/components/SearchBar.svelte +243 -0
  35. package/dist/components/SearchBar.svelte.d.ts +7 -0
  36. package/dist/components/SelectionOverlay.svelte +171 -0
  37. package/dist/components/SelectionOverlay.svelte.d.ts +14 -0
  38. package/dist/components/block-content-selector.d.ts +12 -0
  39. package/dist/components/block-content-selector.js +12 -0
  40. package/dist/components/blocks/BlockquoteBlock.svelte +32 -0
  41. package/dist/components/blocks/BlockquoteBlock.svelte.d.ts +11 -0
  42. package/dist/components/blocks/ThematicBreakBlock.svelte +156 -0
  43. package/dist/components/blocks/ThematicBreakBlock.svelte.d.ts +17 -0
  44. package/dist/components/blocks/code/CodeBlock.svelte +726 -0
  45. package/dist/components/blocks/code/CodeBlock.svelte.d.ts +23 -0
  46. package/dist/components/blocks/code/CodeLanguageChip.svelte +127 -0
  47. package/dist/components/blocks/code/CodeLanguageChip.svelte.d.ts +14 -0
  48. package/dist/components/blocks/code/code-beforeinput.d.ts +30 -0
  49. package/dist/components/blocks/code/code-beforeinput.js +40 -0
  50. package/dist/components/blocks/code/code-bootstrap.d.ts +8 -0
  51. package/dist/components/blocks/code/code-bootstrap.js +54 -0
  52. package/dist/components/blocks/code/code-editing.d.ts +28 -0
  53. package/dist/components/blocks/code/code-editing.js +79 -0
  54. package/dist/components/blocks/code/code-enter.d.ts +22 -0
  55. package/dist/components/blocks/code/code-enter.js +19 -0
  56. package/dist/components/blocks/code/code-fence-boundary.d.ts +77 -0
  57. package/dist/components/blocks/code/code-fence-boundary.js +169 -0
  58. package/dist/components/blocks/code/code-fence-exit.d.ts +24 -0
  59. package/dist/components/blocks/code/code-fence-exit.js +36 -0
  60. package/dist/components/blocks/code/code-indent.d.ts +14 -0
  61. package/dist/components/blocks/code/code-indent.js +87 -0
  62. package/dist/components/blocks/code/code-languages.d.ts +15 -0
  63. package/dist/components/blocks/code/code-languages.js +30 -0
  64. package/dist/components/blocks/code/code-paste-surface.d.ts +6 -0
  65. package/dist/components/blocks/code/code-paste-surface.js +29 -0
  66. package/dist/components/blocks/code/code-paste.d.ts +24 -0
  67. package/dist/components/blocks/code/code-paste.js +21 -0
  68. package/dist/components/blocks/code/code-renderer.d.ts +18 -0
  69. package/dist/components/blocks/code/code-renderer.js +249 -0
  70. package/dist/components/blocks/directive/DirectiveContainerBlock.svelte +51 -0
  71. package/dist/components/blocks/directive/DirectiveContainerBlock.svelte.d.ts +11 -0
  72. package/dist/components/blocks/directive/activate-directives.d.ts +7 -0
  73. package/dist/components/blocks/directive/activate-directives.js +23 -0
  74. package/dist/components/blocks/editable-leaf.d.ts +126 -0
  75. package/dist/components/blocks/editable-leaf.js +410 -0
  76. package/dist/components/blocks/editable-surface.d.ts +209 -0
  77. package/dist/components/blocks/editable-surface.js +331 -0
  78. package/dist/components/blocks/list/ListBlock.svelte +162 -0
  79. package/dist/components/blocks/list/ListBlock.svelte.d.ts +11 -0
  80. package/dist/components/blocks/list/ListItemBlock.svelte +324 -0
  81. package/dist/components/blocks/list/ListItemBlock.svelte.d.ts +14 -0
  82. package/dist/components/blocks/list/task-checkbox.d.ts +3 -0
  83. package/dist/components/blocks/list/task-checkbox.js +33 -0
  84. package/dist/components/blocks/plain-text-backend.d.ts +21 -0
  85. package/dist/components/blocks/plain-text-backend.js +47 -0
  86. package/dist/components/blocks/surface-wiring.svelte.d.ts +18 -0
  87. package/dist/components/blocks/surface-wiring.svelte.js +68 -0
  88. package/dist/components/blocks/table/TableActionMenu.svelte +267 -0
  89. package/dist/components/blocks/table/TableActionMenu.svelte.d.ts +15 -0
  90. package/dist/components/blocks/table/TableBlock.svelte +765 -0
  91. package/dist/components/blocks/table/TableBlock.svelte.d.ts +30 -0
  92. package/dist/components/blocks/table/TableCellBlock.svelte +1075 -0
  93. package/dist/components/blocks/table/TableCellBlock.svelte.d.ts +30 -0
  94. package/dist/components/blocks/table/TableGrip.svelte +91 -0
  95. package/dist/components/blocks/table/TableGrip.svelte.d.ts +8 -0
  96. package/dist/components/blocks/table/TableRowBlock.svelte +233 -0
  97. package/dist/components/blocks/table/TableRowBlock.svelte.d.ts +34 -0
  98. package/dist/components/blocks/table/cell-clipboard.d.ts +23 -0
  99. package/dist/components/blocks/table/cell-clipboard.js +34 -0
  100. package/dist/components/blocks/table/cell-keydown-plan.d.ts +37 -0
  101. package/dist/components/blocks/table/cell-keydown-plan.js +66 -0
  102. package/dist/components/blocks/table/cell-pointer.d.ts +58 -0
  103. package/dist/components/blocks/table/cell-pointer.js +153 -0
  104. package/dist/components/blocks/table/cell-render.d.ts +51 -0
  105. package/dist/components/blocks/table/cell-render.js +98 -0
  106. package/dist/components/blocks/table/cell-table-commands.d.ts +12 -0
  107. package/dist/components/blocks/table/cell-table-commands.js +21 -0
  108. package/dist/components/blocks/table/cell-x-mapping.d.ts +6 -0
  109. package/dist/components/blocks/table/cell-x-mapping.js +16 -0
  110. package/dist/components/blocks/table/selected-cells.d.ts +19 -0
  111. package/dist/components/blocks/table/selected-cells.js +35 -0
  112. package/dist/components/blocks/table/table-caret-at-point.d.ts +8 -0
  113. package/dist/components/blocks/table/table-caret-at-point.js +35 -0
  114. package/dist/components/blocks/table/table-cell-paste.d.ts +16 -0
  115. package/dist/components/blocks/table/table-cell-paste.js +75 -0
  116. package/dist/components/blocks/table/table-drag-hit-test.d.ts +5 -0
  117. package/dist/components/blocks/table/table-drag-hit-test.js +20 -0
  118. package/dist/components/blocks/table/table-drop-target.d.ts +1 -0
  119. package/dist/components/blocks/table/table-drop-target.js +16 -0
  120. package/dist/components/blocks/table/table-menu-model.d.ts +52 -0
  121. package/dist/components/blocks/table/table-menu-model.js +78 -0
  122. package/dist/components/blocks/table/table-navigation.d.ts +8 -0
  123. package/dist/components/blocks/table/table-navigation.js +22 -0
  124. package/dist/components/blocks/table/table-reorder-drag.d.ts +78 -0
  125. package/dist/components/blocks/table/table-reorder-drag.js +97 -0
  126. package/dist/components/blocks/text/TextEditableBlock.svelte +1012 -0
  127. package/dist/components/blocks/text/TextEditableBlock.svelte.d.ts +33 -0
  128. package/dist/components/blocks/text/click-snap-guard.d.ts +8 -0
  129. package/dist/components/blocks/text/click-snap-guard.js +19 -0
  130. package/dist/components/blocks/text/composition-seat.d.ts +50 -0
  131. package/dist/components/blocks/text/composition-seat.js +65 -0
  132. package/dist/components/blocks/text/construct-edge-delete.d.ts +47 -0
  133. package/dist/components/blocks/text/construct-edge-delete.js +164 -0
  134. package/dist/components/blocks/text/construct-reveal.d.ts +39 -0
  135. package/dist/components/blocks/text/construct-reveal.js +164 -0
  136. package/dist/components/blocks/text/edge-policy-dispatch.d.ts +91 -0
  137. package/dist/components/blocks/text/edge-policy-dispatch.js +457 -0
  138. package/dist/components/blocks/text/edge-seat.d.ts +42 -0
  139. package/dist/components/blocks/text/edge-seat.js +197 -0
  140. package/dist/components/blocks/text/hidden-suffix.d.ts +10 -0
  141. package/dist/components/blocks/text/hidden-suffix.js +15 -0
  142. package/dist/components/blocks/text/link-at-point.d.ts +33 -0
  143. package/dist/components/blocks/text/link-at-point.js +47 -0
  144. package/dist/components/blocks/text/link-source-bytes.d.ts +37 -0
  145. package/dist/components/blocks/text/link-source-bytes.js +161 -0
  146. package/dist/components/blocks/text/live-join-seam.d.ts +15 -0
  147. package/dist/components/blocks/text/live-join-seam.js +327 -0
  148. package/dist/components/blocks/text/live-selection-edit.d.ts +68 -0
  149. package/dist/components/blocks/text/live-selection-edit.js +120 -0
  150. package/dist/components/blocks/text/live-split-rebalance.d.ts +56 -0
  151. package/dist/components/blocks/text/live-split-rebalance.js +239 -0
  152. package/dist/components/blocks/text/marker-completion.d.ts +18 -0
  153. package/dist/components/blocks/text/marker-completion.js +29 -0
  154. package/dist/components/blocks/text/pending-mark-insert.d.ts +37 -0
  155. package/dist/components/blocks/text/pending-mark-insert.js +188 -0
  156. package/dist/components/blocks/text/screen-diff.d.ts +20 -0
  157. package/dist/components/blocks/text/screen-diff.js +39 -0
  158. package/dist/components/blocks/text/text-clipboard.d.ts +70 -0
  159. package/dist/components/blocks/text/text-clipboard.js +131 -0
  160. package/dist/components/blocks/text/text-keydown.d.ts +39 -0
  161. package/dist/components/blocks/text/text-keydown.js +80 -0
  162. package/dist/components/blocks/text/text-render.d.ts +67 -0
  163. package/dist/components/blocks/text/text-render.js +215 -0
  164. package/dist/components/blocks/text/widget-adjacency.d.ts +30 -0
  165. package/dist/components/blocks/text/widget-adjacency.js +68 -0
  166. package/dist/components/blocks/text/widget-interaction.d.ts +90 -0
  167. package/dist/components/blocks/text/widget-interaction.js +591 -0
  168. package/dist/components/blocks/widget-portal.d.ts +54 -0
  169. package/dist/components/blocks/widget-portal.js +136 -0
  170. package/dist/components/built-in-blocks.d.ts +7 -0
  171. package/dist/components/built-in-blocks.js +77 -0
  172. package/dist/components/editor-root-clipboard.d.ts +29 -0
  173. package/dist/components/editor-root-clipboard.js +105 -0
  174. package/dist/components/editor-root-keydown.d.ts +47 -0
  175. package/dist/components/editor-root-keydown.js +88 -0
  176. package/dist/components/editor-root-listeners.d.ts +44 -0
  177. package/dist/components/editor-root-listeners.js +112 -0
  178. package/dist/components/image/ImageOverlayHost.svelte +115 -0
  179. package/dist/components/image/ImageOverlayHost.svelte.d.ts +20 -0
  180. package/dist/components/image/ImageProperties.svelte +153 -0
  181. package/dist/components/image/ImageProperties.svelte.d.ts +14 -0
  182. package/dist/components/image/ImageResizeHandles.svelte +188 -0
  183. package/dist/components/image/ImageResizeHandles.svelte.d.ts +10 -0
  184. package/dist/components/image/image-edit-commit.d.ts +38 -0
  185. package/dist/components/image/image-edit-commit.js +159 -0
  186. package/dist/components/image/image-resize.d.ts +11 -0
  187. package/dist/components/image/image-resize.js +38 -0
  188. package/dist/components/image/image-source-bytes.d.ts +15 -0
  189. package/dist/components/image/image-source-bytes.js +73 -0
  190. package/dist/components/image/image-widget-editing.d.ts +7 -0
  191. package/dist/components/image/image-widget-editing.js +41 -0
  192. package/dist/components/image/widget-dom.d.ts +11 -0
  193. package/dist/components/image/widget-dom.js +110 -0
  194. package/dist/components/image/widget-selection-state.svelte.d.ts +18 -0
  195. package/dist/components/image/widget-selection-state.svelte.js +26 -0
  196. package/dist/components/link-card/LinkCard.svelte +190 -0
  197. package/dist/components/link-card/LinkCard.svelte.d.ts +18 -0
  198. package/dist/components/link-card/LinkCardHost.svelte +181 -0
  199. package/dist/components/link-card/LinkCardHost.svelte.d.ts +25 -0
  200. package/dist/components/link-card/link-card-commit.d.ts +50 -0
  201. package/dist/components/link-card/link-card-commit.js +134 -0
  202. package/dist/components/link-card/link-card-entry.d.ts +39 -0
  203. package/dist/components/link-card/link-card-entry.js +51 -0
  204. package/dist/components/link-card/link-card-state.svelte.d.ts +44 -0
  205. package/dist/components/link-card/link-card-state.svelte.js +46 -0
  206. package/dist/components/lrd-map-gate.d.ts +18 -0
  207. package/dist/components/lrd-map-gate.js +22 -0
  208. package/dist/components/paste-image-arm.d.ts +29 -0
  209. package/dist/components/paste-image-arm.js +61 -0
  210. package/dist/components/portal.d.ts +11 -0
  211. package/dist/components/portal.js +29 -0
  212. package/dist/core/directive/activate.d.ts +8 -0
  213. package/dist/core/directive/activate.js +25 -0
  214. package/dist/core/directive/container-opener.d.ts +7 -0
  215. package/dist/core/directive/container-opener.js +162 -0
  216. package/dist/core/directive/grammar.d.ts +50 -0
  217. package/dist/core/directive/grammar.js +88 -0
  218. package/dist/core/directive/kinds.d.ts +31 -0
  219. package/dist/core/directive/kinds.js +137 -0
  220. package/dist/core/directive/registry.d.ts +39 -0
  221. package/dist/core/directive/registry.js +49 -0
  222. package/dist/core/directive/text-recognizer.d.ts +8 -0
  223. package/dist/core/directive/text-recognizer.js +87 -0
  224. package/dist/core/escapable.d.ts +6 -0
  225. package/dist/core/escapable.js +6 -0
  226. package/dist/core/inline/backticks.d.ts +16 -0
  227. package/dist/core/inline/backticks.js +62 -0
  228. package/dist/core/inline/character-refs.d.ts +7 -0
  229. package/dist/core/inline/character-refs.js +70 -0
  230. package/dist/core/inline/destination-bytes.d.ts +7 -0
  231. package/dist/core/inline/destination-bytes.js +14 -0
  232. package/dist/core/inline/entity-widget.d.ts +11 -0
  233. package/dist/core/inline/entity-widget.js +21 -0
  234. package/dist/core/inline/format-toggle.d.ts +50 -0
  235. package/dist/core/inline/format-toggle.js +533 -0
  236. package/dist/core/inline/html-entities.d.ts +8 -0
  237. package/dist/core/inline/html-entities.js +2134 -0
  238. package/dist/core/inline/html-tag-grammar.d.ts +16 -0
  239. package/dist/core/inline/html-tag-grammar.js +58 -0
  240. package/dist/core/inline/image-dimensions.d.ts +6 -0
  241. package/dist/core/inline/image-dimensions.js +42 -0
  242. package/dist/core/inline/index.d.ts +42 -0
  243. package/dist/core/inline/index.js +77 -0
  244. package/dist/core/inline/inline-cache.d.ts +20 -0
  245. package/dist/core/inline/inline-cache.js +28 -0
  246. package/dist/core/inline/inline-widgets.d.ts +124 -0
  247. package/dist/core/inline/inline-widgets.js +131 -0
  248. package/dist/core/inline/link-reference-resolver.d.ts +21 -0
  249. package/dist/core/inline/link-reference-resolver.js +46 -0
  250. package/dist/core/inline/raw-html-widget.d.ts +10 -0
  251. package/dist/core/inline/raw-html-widget.js +22 -0
  252. package/dist/core/inline/scan/autolinks.d.ts +23 -0
  253. package/dist/core/inline/scan/autolinks.js +354 -0
  254. package/dist/core/inline/scan/brackets.d.ts +11 -0
  255. package/dist/core/inline/scan/brackets.js +284 -0
  256. package/dist/core/inline/scan/code-spans.d.ts +7 -0
  257. package/dist/core/inline/scan/code-spans.js +27 -0
  258. package/dist/core/inline/scan/emphasis.d.ts +10 -0
  259. package/dist/core/inline/scan/emphasis.js +276 -0
  260. package/dist/core/inline/scan/index.d.ts +7 -0
  261. package/dist/core/inline/scan/index.js +176 -0
  262. package/dist/core/inline/scan/plugin-syntax.d.ts +62 -0
  263. package/dist/core/inline/scan/plugin-syntax.js +150 -0
  264. package/dist/core/inline/scan/scan-state.d.ts +47 -0
  265. package/dist/core/inline/scan/scan-state.js +48 -0
  266. package/dist/core/inline/scan/simple-nodes.d.ts +14 -0
  267. package/dist/core/inline/scan/simple-nodes.js +53 -0
  268. package/dist/core/inline/scan/url.d.ts +12 -0
  269. package/dist/core/inline/scan/url.js +82 -0
  270. package/dist/core/inline/transparency.d.ts +8 -0
  271. package/dist/core/inline/transparency.js +39 -0
  272. package/dist/core/inline/visibility.d.ts +81 -0
  273. package/dist/core/inline/visibility.js +144 -0
  274. package/dist/core/inline/walk.d.ts +14 -0
  275. package/dist/core/inline/walk.js +29 -0
  276. package/dist/core/inline-render.d.ts +48 -0
  277. package/dist/core/inline-render.js +322 -0
  278. package/dist/core/lines.d.ts +44 -0
  279. package/dist/core/lines.js +91 -0
  280. package/dist/core/node-views.d.ts +22 -0
  281. package/dist/core/node-views.js +8 -0
  282. package/dist/core/nodes.d.ts +297 -0
  283. package/dist/core/nodes.js +83 -0
  284. package/dist/core/parser.d.ts +68 -0
  285. package/dist/core/parser.js +189 -0
  286. package/dist/core/parsers/blockquote.d.ts +17 -0
  287. package/dist/core/parsers/blockquote.js +69 -0
  288. package/dist/core/parsers/built-in-openers.d.ts +8 -0
  289. package/dist/core/parsers/built-in-openers.js +124 -0
  290. package/dist/core/parsers/fence-syntax.d.ts +24 -0
  291. package/dist/core/parsers/fence-syntax.js +44 -0
  292. package/dist/core/parsers/fenced-code.d.ts +7 -0
  293. package/dist/core/parsers/fenced-code.js +29 -0
  294. package/dist/core/parsers/heading.d.ts +3 -0
  295. package/dist/core/parsers/heading.js +6 -0
  296. package/dist/core/parsers/html-block.d.ts +17 -0
  297. package/dist/core/parsers/html-block.js +94 -0
  298. package/dist/core/parsers/indented-code.d.ts +4 -0
  299. package/dist/core/parsers/indented-code.js +41 -0
  300. package/dist/core/parsers/link-reference.d.ts +7 -0
  301. package/dist/core/parsers/link-reference.js +152 -0
  302. package/dist/core/parsers/list.d.ts +19 -0
  303. package/dist/core/parsers/list.js +133 -0
  304. package/dist/core/parsers/paragraph.d.ts +10 -0
  305. package/dist/core/parsers/paragraph.js +42 -0
  306. package/dist/core/parsers/table-completion.d.ts +13 -0
  307. package/dist/core/parsers/table-completion.js +48 -0
  308. package/dist/core/parsers/table.d.ts +18 -0
  309. package/dist/core/parsers/table.js +110 -0
  310. package/dist/core/parsers/thematic-break.d.ts +1 -0
  311. package/dist/core/parsers/thematic-break.js +19 -0
  312. package/dist/core/serializer.d.ts +15 -0
  313. package/dist/core/serializer.js +11 -0
  314. package/dist/core/terminator-escalation.d.ts +8 -0
  315. package/dist/core/terminator-escalation.js +18 -0
  316. package/dist/core/url-policy.d.ts +14 -0
  317. package/dist/core/url-policy.js +44 -0
  318. package/dist/cursor/content-offsets.d.ts +29 -0
  319. package/dist/cursor/content-offsets.js +151 -0
  320. package/dist/cursor/coordinate-spaces.d.ts +65 -0
  321. package/dist/cursor/coordinate-spaces.js +66 -0
  322. package/dist/cursor/dom-walk.d.ts +18 -0
  323. package/dist/cursor/dom-walk.js +31 -0
  324. package/dist/cursor/edge-affinity.d.ts +44 -0
  325. package/dist/cursor/edge-affinity.js +60 -0
  326. package/dist/cursor/focused-caret.d.ts +10 -0
  327. package/dist/cursor/focused-caret.js +24 -0
  328. package/dist/cursor/height-model.d.ts +24 -0
  329. package/dist/cursor/height-model.js +72 -0
  330. package/dist/cursor/height-oracle.d.ts +24 -0
  331. package/dist/cursor/height-oracle.js +94 -0
  332. package/dist/cursor/overlay-rects.d.ts +8 -0
  333. package/dist/cursor/overlay-rects.js +19 -0
  334. package/dist/cursor/overlay-remeasure.d.ts +17 -0
  335. package/dist/cursor/overlay-remeasure.js +31 -0
  336. package/dist/cursor/pending-marks.d.ts +24 -0
  337. package/dist/cursor/pending-marks.js +36 -0
  338. package/dist/cursor/point-offset.d.ts +21 -0
  339. package/dist/cursor/point-offset.js +48 -0
  340. package/dist/cursor/reveal-anchor.d.ts +30 -0
  341. package/dist/cursor/reveal-anchor.js +31 -0
  342. package/dist/cursor/reveal-source.d.ts +35 -0
  343. package/dist/cursor/reveal-source.js +51 -0
  344. package/dist/cursor/scroll-ancestors.d.ts +28 -0
  345. package/dist/cursor/scroll-ancestors.js +89 -0
  346. package/dist/cursor/scrollport.d.ts +20 -0
  347. package/dist/cursor/scrollport.js +41 -0
  348. package/dist/cursor/sticky-column.d.ts +35 -0
  349. package/dist/cursor/sticky-column.js +64 -0
  350. package/dist/cursor/sticky-measure.d.ts +16 -0
  351. package/dist/cursor/sticky-measure.js +97 -0
  352. package/dist/cursor/typography-estimates.d.ts +21 -0
  353. package/dist/cursor/typography-estimates.js +21 -0
  354. package/dist/cursor/visual-lines.d.ts +25 -0
  355. package/dist/cursor/visual-lines.js +125 -0
  356. package/dist/cursor/widget-offset.d.ts +132 -0
  357. package/dist/cursor/widget-offset.js +543 -0
  358. package/dist/debug/diagnostics-report.d.ts +17 -0
  359. package/dist/debug/diagnostics-report.js +25 -0
  360. package/dist/debug/dump-tree.d.ts +6 -0
  361. package/dist/debug/dump-tree.js +114 -0
  362. package/dist/debug/inspect.d.ts +19 -0
  363. package/dist/debug/inspect.js +116 -0
  364. package/dist/debug/interaction-trace.d.ts +39 -0
  365. package/dist/debug/interaction-trace.js +105 -0
  366. package/dist/debug/operations-log.d.ts +14 -0
  367. package/dist/debug/operations-log.js +30 -0
  368. package/dist/decorations/buckets.d.ts +40 -0
  369. package/dist/decorations/buckets.js +87 -0
  370. package/dist/decorations/decoration-state.svelte.d.ts +20 -0
  371. package/dist/decorations/decoration-state.svelte.js +195 -0
  372. package/dist/decorations/island-dom.d.ts +35 -0
  373. package/dist/decorations/island-dom.js +148 -0
  374. package/dist/decorations/reserved-attrs.d.ts +12 -0
  375. package/dist/decorations/reserved-attrs.js +47 -0
  376. package/dist/decorations/types.d.ts +68 -0
  377. package/dist/decorations/types.js +5 -0
  378. package/dist/decorations/widget-dom.d.ts +15 -0
  379. package/dist/decorations/widget-dom.js +35 -0
  380. package/dist/dev-warn.d.ts +13 -0
  381. package/dist/dev-warn.js +28 -0
  382. package/dist/editor-actions/ancestry-folds.d.ts +32 -0
  383. package/dist/editor-actions/ancestry-folds.js +106 -0
  384. package/dist/editor-actions/block-edit-core.d.ts +29 -0
  385. package/dist/editor-actions/block-edit-core.js +277 -0
  386. package/dist/editor-actions/block-edit-scope.d.ts +79 -0
  387. package/dist/editor-actions/block-edit-scope.js +76 -0
  388. package/dist/editor-actions/block-edit.d.ts +8 -0
  389. package/dist/editor-actions/block-edit.js +111 -0
  390. package/dist/editor-actions/commit/history.d.ts +9 -0
  391. package/dist/editor-actions/commit/history.js +77 -0
  392. package/dist/editor-actions/commit/text-batch.d.ts +34 -0
  393. package/dist/editor-actions/commit/text-batch.js +61 -0
  394. package/dist/editor-actions/commit/undo-controller.d.ts +7 -0
  395. package/dist/editor-actions/commit/undo-controller.js +570 -0
  396. package/dist/editor-actions/container-block-component.d.ts +85 -0
  397. package/dist/editor-actions/container-block-component.js +228 -0
  398. package/dist/editor-actions/container-edit.d.ts +7 -0
  399. package/dist/editor-actions/container-edit.js +49 -0
  400. package/dist/editor-actions/container-exit-overrides.d.ts +16 -0
  401. package/dist/editor-actions/container-exit-overrides.js +38 -0
  402. package/dist/editor-actions/deps.d.ts +60 -0
  403. package/dist/editor-actions/deps.js +1 -0
  404. package/dist/editor-actions/enter-completion.d.ts +25 -0
  405. package/dist/editor-actions/enter-completion.js +76 -0
  406. package/dist/editor-actions/focus/focus-dispatch.d.ts +36 -0
  407. package/dist/editor-actions/focus/focus-dispatch.js +106 -0
  408. package/dist/editor-actions/focus/focus-landing.d.ts +7 -0
  409. package/dist/editor-actions/focus/focus-landing.js +39 -0
  410. package/dist/editor-actions/focus/focus.d.ts +7 -0
  411. package/dist/editor-actions/focus/focus.js +74 -0
  412. package/dist/editor-actions/index.d.ts +15 -0
  413. package/dist/editor-actions/index.js +19 -0
  414. package/dist/editor-actions/inline-range-commit.d.ts +20 -0
  415. package/dist/editor-actions/inline-range-commit.js +63 -0
  416. package/dist/editor-actions/list-context.d.ts +25 -0
  417. package/dist/editor-actions/list-context.js +285 -0
  418. package/dist/editor-actions/list-overrides.d.ts +13 -0
  419. package/dist/editor-actions/list-overrides.js +26 -0
  420. package/dist/editor-actions/merge-fallback.d.ts +19 -0
  421. package/dist/editor-actions/merge-fallback.js +27 -0
  422. package/dist/editor-actions/nested/nested-actions.d.ts +65 -0
  423. package/dist/editor-actions/nested/nested-actions.js +59 -0
  424. package/dist/editor-actions/nested/nested-block-edit.d.ts +10 -0
  425. package/dist/editor-actions/nested/nested-block-edit.js +181 -0
  426. package/dist/editor-actions/nested/nested-focus.d.ts +9 -0
  427. package/dist/editor-actions/nested/nested-focus.js +30 -0
  428. package/dist/editor-actions/paste-coordinator.d.ts +4 -0
  429. package/dist/editor-actions/paste-coordinator.js +20 -0
  430. package/dist/editor-actions/plugin/chrome-leaf.d.ts +25 -0
  431. package/dist/editor-actions/plugin/chrome-leaf.js +80 -0
  432. package/dist/editor-actions/plugin/container.d.ts +137 -0
  433. package/dist/editor-actions/plugin/container.js +351 -0
  434. package/dist/editor-actions/plugin/directive-container.d.ts +13 -0
  435. package/dist/editor-actions/plugin/directive-container.js +31 -0
  436. package/dist/editor-actions/reorder-action.d.ts +17 -0
  437. package/dist/editor-actions/reorder-action.js +104 -0
  438. package/dist/editor-actions/reorder-drag.d.ts +34 -0
  439. package/dist/editor-actions/reorder-drag.js +152 -0
  440. package/dist/editor-actions/replacement-focus.d.ts +27 -0
  441. package/dist/editor-actions/replacement-focus.js +60 -0
  442. package/dist/editor-actions/search-replace.d.ts +6 -0
  443. package/dist/editor-actions/search-replace.js +145 -0
  444. package/dist/editor-actions/table-context.d.ts +34 -0
  445. package/dist/editor-actions/table-context.js +292 -0
  446. package/dist/editor-actions/unwrap-strategies.d.ts +14 -0
  447. package/dist/editor-actions/unwrap-strategies.js +131 -0
  448. package/dist/editor-actions/whole-block-focus-surface.d.ts +51 -0
  449. package/dist/editor-actions/whole-block-focus-surface.js +171 -0
  450. package/dist/editor-events.d.ts +89 -0
  451. package/dist/editor-events.js +96 -0
  452. package/dist/editor-keys.d.ts +197 -0
  453. package/dist/editor-keys.js +40 -0
  454. package/dist/editor-props.d.ts +164 -0
  455. package/dist/editor-props.js +1 -0
  456. package/dist/editor-rects.d.ts +58 -0
  457. package/dist/editor-rects.js +114 -0
  458. package/dist/env.d.ts +12 -0
  459. package/dist/env.js +20 -0
  460. package/dist/index.d.ts +27 -0
  461. package/dist/index.js +19 -0
  462. package/dist/invariants/commit-paths.d.ts +10 -0
  463. package/dist/invariants/commit-paths.js +25 -0
  464. package/dist/invariants/commit-scope.d.ts +9 -0
  465. package/dist/invariants/commit-scope.js +16 -0
  466. package/dist/invariants/context-keys.d.ts +7 -0
  467. package/dist/invariants/context-keys.js +14 -0
  468. package/dist/invariants/descriptor.d.ts +10 -0
  469. package/dist/invariants/descriptor.js +24 -0
  470. package/dist/invariants/inline-transitions.d.ts +23 -0
  471. package/dist/invariants/inline-transitions.js +52 -0
  472. package/dist/invariants/install.d.ts +36 -0
  473. package/dist/invariants/install.js +66 -0
  474. package/dist/invariants/landable-caret.d.ts +12 -0
  475. package/dist/invariants/landable-caret.js +31 -0
  476. package/dist/invariants/marker-css-parity.d.ts +10 -0
  477. package/dist/invariants/marker-css-parity.js +87 -0
  478. package/dist/invariants/node-shape.d.ts +44 -0
  479. package/dist/invariants/node-shape.js +258 -0
  480. package/dist/invariants/registry.d.ts +126 -0
  481. package/dist/invariants/registry.js +349 -0
  482. package/dist/invariants/render-fidelity.d.ts +8 -0
  483. package/dist/invariants/render-fidelity.js +33 -0
  484. package/dist/invariants/selection-endpoints.d.ts +19 -0
  485. package/dist/invariants/selection-endpoints.js +78 -0
  486. package/dist/invariants/single-node-sink.d.ts +9 -0
  487. package/dist/invariants/single-node-sink.js +16 -0
  488. package/dist/invariants/snapshot-integrity.d.ts +17 -0
  489. package/dist/invariants/snapshot-integrity.js +24 -0
  490. package/dist/invariants/split-landing.d.ts +8 -0
  491. package/dist/invariants/split-landing.js +15 -0
  492. package/dist/invariants/structural-descriptor.d.ts +21 -0
  493. package/dist/invariants/structural-descriptor.js +50 -0
  494. package/dist/perf/instruments.d.ts +49 -0
  495. package/dist/perf/instruments.js +154 -0
  496. package/dist/perf/use-mount-gauge.svelte.d.ts +1 -0
  497. package/dist/perf/use-mount-gauge.svelte.js +15 -0
  498. package/dist/plugin.d.ts +102 -0
  499. package/dist/plugin.js +156 -0
  500. package/dist/plugins/admonitions/AdmonitionBlock.svelte +144 -0
  501. package/dist/plugins/admonitions/AdmonitionBlock.svelte.d.ts +11 -0
  502. package/dist/plugins/admonitions/admonition-kind.d.ts +8 -0
  503. package/dist/plugins/admonitions/admonition-kind.js +97 -0
  504. package/dist/plugins/admonitions/convert-document.d.ts +11 -0
  505. package/dist/plugins/admonitions/convert-document.js +39 -0
  506. package/dist/plugins/admonitions/gh-alert.d.ts +13 -0
  507. package/dist/plugins/admonitions/gh-alert.js +83 -0
  508. package/dist/plugins/admonitions/github-alert-kind.d.ts +12 -0
  509. package/dist/plugins/admonitions/github-alert-kind.js +125 -0
  510. package/dist/plugins/admonitions/index.d.ts +6 -0
  511. package/dist/plugins/admonitions/index.js +8 -0
  512. package/dist/plugins/admonitions/kinds.d.ts +26 -0
  513. package/dist/plugins/admonitions/kinds.js +18 -0
  514. package/dist/plugins/admonitions/register.d.ts +7 -0
  515. package/dist/plugins/admonitions/register.js +12 -0
  516. package/dist/plugins/details/DetailsBlock.svelte +129 -0
  517. package/dist/plugins/details/DetailsBlock.svelte.d.ts +11 -0
  518. package/dist/plugins/details/details-disclosure.svelte.d.ts +17 -0
  519. package/dist/plugins/details/details-disclosure.svelte.js +24 -0
  520. package/dist/plugins/details/details-kind.d.ts +25 -0
  521. package/dist/plugins/details/details-kind.js +237 -0
  522. package/dist/plugins/details/index.d.ts +2 -0
  523. package/dist/plugins/details/index.js +2 -0
  524. package/dist/plugins/details/register.d.ts +6 -0
  525. package/dist/plugins/details/register.js +15 -0
  526. package/dist/plugins/emoji/emoji-plugin.d.ts +2 -0
  527. package/dist/plugins/emoji/emoji-plugin.js +10 -0
  528. package/dist/plugins/emoji/emoji-recognizer.d.ts +18 -0
  529. package/dist/plugins/emoji/emoji-recognizer.js +63 -0
  530. package/dist/plugins/emoji/emoji-table.d.ts +1 -0
  531. package/dist/plugins/emoji/emoji-table.js +1919 -0
  532. package/dist/plugins/emoji/index.d.ts +3 -0
  533. package/dist/plugins/emoji/index.js +4 -0
  534. package/dist/plugins/footnotes/FootnoteDefinition.svelte +103 -0
  535. package/dist/plugins/footnotes/FootnoteDefinition.svelte.d.ts +13 -0
  536. package/dist/plugins/footnotes/FootnoteReference.svelte +50 -0
  537. package/dist/plugins/footnotes/FootnoteReference.svelte.d.ts +4 -0
  538. package/dist/plugins/footnotes/constants.d.ts +2 -0
  539. package/dist/plugins/footnotes/constants.js +4 -0
  540. package/dist/plugins/footnotes/footnote-definition.d.ts +14 -0
  541. package/dist/plugins/footnotes/footnote-definition.js +147 -0
  542. package/dist/plugins/footnotes/footnote-lookup.d.ts +9 -0
  543. package/dist/plugins/footnotes/footnote-lookup.js +33 -0
  544. package/dist/plugins/footnotes/footnote-numbering.d.ts +23 -0
  545. package/dist/plugins/footnotes/footnote-numbering.js +78 -0
  546. package/dist/plugins/footnotes/footnote-reference.d.ts +7 -0
  547. package/dist/plugins/footnotes/footnote-reference.js +54 -0
  548. package/dist/plugins/footnotes/footnotes-plugin.d.ts +7 -0
  549. package/dist/plugins/footnotes/footnotes-plugin.js +17 -0
  550. package/dist/plugins/footnotes/index.d.ts +5 -0
  551. package/dist/plugins/footnotes/index.js +6 -0
  552. package/dist/plugins/highlight-occurrences/highlight-occurrences-plugin.d.ts +17 -0
  553. package/dist/plugins/highlight-occurrences/highlight-occurrences-plugin.js +31 -0
  554. package/dist/plugins/highlight-occurrences/index.d.ts +1 -0
  555. package/dist/plugins/highlight-occurrences/index.js +1 -0
  556. package/dist/plugins/highlight-occurrences/occurrence-source.d.ts +26 -0
  557. package/dist/plugins/highlight-occurrences/occurrence-source.js +54 -0
  558. package/dist/plugins/highlight-occurrences/occurrences.d.ts +33 -0
  559. package/dist/plugins/highlight-occurrences/occurrences.js +108 -0
  560. package/dist/plugins/latex/BlockMath.svelte +119 -0
  561. package/dist/plugins/latex/BlockMath.svelte.d.ts +21 -0
  562. package/dist/plugins/latex/MathInline.svelte +28 -0
  563. package/dist/plugins/latex/MathInline.svelte.d.ts +4 -0
  564. package/dist/plugins/latex/index.d.ts +3 -0
  565. package/dist/plugins/latex/index.js +4 -0
  566. package/dist/plugins/latex/latex-kind.d.ts +17 -0
  567. package/dist/plugins/latex/latex-kind.js +224 -0
  568. package/dist/plugins/latex/math-completion.d.ts +10 -0
  569. package/dist/plugins/latex/math-completion.js +19 -0
  570. package/dist/plugins/latex/math-renderer.d.ts +26 -0
  571. package/dist/plugins/latex/math-renderer.js +42 -0
  572. package/dist/plugins/latex/register.d.ts +10 -0
  573. package/dist/plugins/latex/register.js +23 -0
  574. package/dist/plugins/latex/renderer.d.ts +13 -0
  575. package/dist/plugins/latex/renderer.js +34 -0
  576. package/dist/plugins/mermaid/MermaidBlock.svelte +572 -0
  577. package/dist/plugins/mermaid/MermaidBlock.svelte.d.ts +11 -0
  578. package/dist/plugins/mermaid/index.d.ts +3 -0
  579. package/dist/plugins/mermaid/index.js +3 -0
  580. package/dist/plugins/mermaid/mermaid-kind.d.ts +38 -0
  581. package/dist/plugins/mermaid/mermaid-kind.js +176 -0
  582. package/dist/plugins/mermaid/mermaid-renderer.d.ts +28 -0
  583. package/dist/plugins/mermaid/mermaid-renderer.js +35 -0
  584. package/dist/plugins/mermaid/register.d.ts +10 -0
  585. package/dist/plugins/mermaid/register.js +20 -0
  586. package/dist/plugins/mermaid/renderer.d.ts +11 -0
  587. package/dist/plugins/mermaid/renderer.js +53 -0
  588. package/dist/plugins/parrot/ParrotBlock.svelte +342 -0
  589. package/dist/plugins/parrot/ParrotBlock.svelte.d.ts +21 -0
  590. package/dist/plugins/parrot/index.d.ts +1 -0
  591. package/dist/plugins/parrot/index.js +2 -0
  592. package/dist/plugins/parrot/parrot-plugin.d.ts +3 -0
  593. package/dist/plugins/parrot/parrot-plugin.js +49 -0
  594. package/dist/plugins/toc/TocBlock.svelte +218 -0
  595. package/dist/plugins/toc/TocBlock.svelte.d.ts +24 -0
  596. package/dist/plugins/toc/heading-outline.d.ts +28 -0
  597. package/dist/plugins/toc/heading-outline.js +65 -0
  598. package/dist/plugins/toc/index.d.ts +2 -0
  599. package/dist/plugins/toc/index.js +2 -0
  600. package/dist/plugins/toc/navigation-queue.d.ts +12 -0
  601. package/dist/plugins/toc/navigation-queue.js +21 -0
  602. package/dist/plugins/toc/toc-plugin.d.ts +16 -0
  603. package/dist/plugins/toc/toc-plugin.js +71 -0
  604. package/dist/presentation-mode.d.ts +32 -0
  605. package/dist/presentation-mode.js +50 -0
  606. package/dist/reactivity/block-list-state.svelte.d.ts +22 -0
  607. package/dist/reactivity/block-list-state.svelte.js +46 -0
  608. package/dist/reactivity/block-window.svelte.d.ts +41 -0
  609. package/dist/reactivity/block-window.svelte.js +93 -0
  610. package/dist/reactivity/content-version.svelte.d.ts +14 -0
  611. package/dist/reactivity/content-version.svelte.js +16 -0
  612. package/dist/reactivity/list-windowing.svelte.d.ts +100 -0
  613. package/dist/reactivity/list-windowing.svelte.js +402 -0
  614. package/dist/reactivity/measure-batch.d.ts +15 -0
  615. package/dist/reactivity/measure-batch.js +18 -0
  616. package/dist/reactivity/publish-ref.svelte.d.ts +54 -0
  617. package/dist/reactivity/publish-ref.svelte.js +128 -0
  618. package/dist/reactivity/scope-geometry.d.ts +18 -0
  619. package/dist/reactivity/scope-geometry.js +29 -0
  620. package/dist/reactivity/state-registry.d.ts +10 -0
  621. package/dist/reactivity/state-registry.js +44 -0
  622. package/dist/reactivity/use-container-windowing.svelte.d.ts +35 -0
  623. package/dist/reactivity/use-container-windowing.svelte.js +95 -0
  624. package/dist/reactivity/window-slice.d.ts +11 -0
  625. package/dist/reactivity/window-slice.js +9 -0
  626. package/dist/scan-index.d.ts +11 -0
  627. package/dist/scan-index.js +27 -0
  628. package/dist/schema/block-commands.d.ts +105 -0
  629. package/dist/schema/block-commands.js +179 -0
  630. package/dist/schema/block-completions.d.ts +33 -0
  631. package/dist/schema/block-completions.js +49 -0
  632. package/dist/schema/block-component-registry.d.ts +33 -0
  633. package/dist/schema/block-component-registry.js +33 -0
  634. package/dist/schema/block-kind-descriptor.d.ts +281 -0
  635. package/dist/schema/block-kind-descriptor.js +187 -0
  636. package/dist/schema/block-openers.d.ts +94 -0
  637. package/dist/schema/block-openers.js +150 -0
  638. package/dist/schema/built-in-descriptors.d.ts +8 -0
  639. package/dist/schema/built-in-descriptors.js +560 -0
  640. package/dist/schema/child-spans.d.ts +25 -0
  641. package/dist/schema/child-spans.js +161 -0
  642. package/dist/schema/closure.d.ts +39 -0
  643. package/dist/schema/closure.js +55 -0
  644. package/dist/schema/command-id.d.ts +20 -0
  645. package/dist/schema/command-id.js +42 -0
  646. package/dist/schema/commands.d.ts +147 -0
  647. package/dist/schema/commands.js +294 -0
  648. package/dist/schema/container-raw.d.ts +21 -0
  649. package/dist/schema/container-raw.js +43 -0
  650. package/dist/schema/container-rebuilders.d.ts +32 -0
  651. package/dist/schema/container-rebuilders.js +95 -0
  652. package/dist/schema/define-plugin-block.d.ts +15 -0
  653. package/dist/schema/define-plugin-block.js +18 -0
  654. package/dist/schema/fenced-code-raw.d.ts +43 -0
  655. package/dist/schema/fenced-code-raw.js +220 -0
  656. package/dist/schema/global-commands.d.ts +11 -0
  657. package/dist/schema/global-commands.js +40 -0
  658. package/dist/schema/inline-construct-policy.d.ts +131 -0
  659. package/dist/schema/inline-construct-policy.js +96 -0
  660. package/dist/schema/keybinding-overrides.d.ts +39 -0
  661. package/dist/schema/keybinding-overrides.js +43 -0
  662. package/dist/schema/keybindings.d.ts +36 -0
  663. package/dist/schema/keybindings.js +75 -0
  664. package/dist/schema/merge-rules.d.ts +28 -0
  665. package/dist/schema/merge-rules.js +57 -0
  666. package/dist/schema/opener-priorities.d.ts +16 -0
  667. package/dist/schema/opener-priorities.js +16 -0
  668. package/dist/schema/operations.d.ts +110 -0
  669. package/dist/schema/operations.js +6 -0
  670. package/dist/schema/plugin-activation.d.ts +13 -0
  671. package/dist/schema/plugin-activation.js +18 -0
  672. package/dist/schema/plugin-editor-context.d.ts +33 -0
  673. package/dist/schema/plugin-editor-context.js +65 -0
  674. package/dist/schema/plugin-install.d.ts +58 -0
  675. package/dist/schema/plugin-install.js +132 -0
  676. package/dist/schema/plugin-kind.d.ts +21 -0
  677. package/dist/schema/plugin-kind.js +86 -0
  678. package/dist/schema/plugin-name.d.ts +1 -0
  679. package/dist/schema/plugin-name.js +7 -0
  680. package/dist/schema/register-once.d.ts +9 -0
  681. package/dist/schema/register-once.js +35 -0
  682. package/dist/schema/registration-checks.d.ts +32 -0
  683. package/dist/schema/registration-checks.js +122 -0
  684. package/dist/schema/registration-pending.d.ts +28 -0
  685. package/dist/schema/registration-pending.js +48 -0
  686. package/dist/schema/registry-reset.d.ts +7 -0
  687. package/dist/schema/registry-reset.js +30 -0
  688. package/dist/schema/registry-view.d.ts +30 -0
  689. package/dist/schema/registry-view.js +39 -0
  690. package/dist/schema/reserved-chords.d.ts +35 -0
  691. package/dist/schema/reserved-chords.js +313 -0
  692. package/dist/schema/reserved-chrome.d.ts +23 -0
  693. package/dist/schema/reserved-chrome.js +33 -0
  694. package/dist/schema/table-cell-raw.d.ts +14 -0
  695. package/dist/schema/table-cell-raw.js +30 -0
  696. package/dist/schema/whole-block-unit.d.ts +8 -0
  697. package/dist/schema/whole-block-unit.js +12 -0
  698. package/dist/search/document-scan.d.ts +22 -0
  699. package/dist/search/document-scan.js +39 -0
  700. package/dist/search/matcher.d.ts +31 -0
  701. package/dist/search/matcher.js +70 -0
  702. package/dist/search/regex-executor.d.ts +41 -0
  703. package/dist/search/regex-executor.js +165 -0
  704. package/dist/search/replace.d.ts +15 -0
  705. package/dist/search/replace.js +29 -0
  706. package/dist/search/search-state.svelte.d.ts +59 -0
  707. package/dist/search/search-state.svelte.js +231 -0
  708. package/dist/selection/autoscroll.d.ts +26 -0
  709. package/dist/selection/autoscroll.js +91 -0
  710. package/dist/selection/block-hit-test.d.ts +37 -0
  711. package/dist/selection/block-hit-test.js +59 -0
  712. package/dist/selection/caret-doors.d.ts +20 -0
  713. package/dist/selection/caret-doors.js +44 -0
  714. package/dist/selection/caret-restore.d.ts +15 -0
  715. package/dist/selection/caret-restore.js +32 -0
  716. package/dist/selection/char-endpoint-snap.d.ts +15 -0
  717. package/dist/selection/char-endpoint-snap.js +39 -0
  718. package/dist/selection/clipboard-text.d.ts +13 -0
  719. package/dist/selection/clipboard-text.js +314 -0
  720. package/dist/selection/cross-block/clipboard.d.ts +14 -0
  721. package/dist/selection/cross-block/clipboard.js +20 -0
  722. package/dist/selection/cross-block/dispatch.d.ts +78 -0
  723. package/dist/selection/cross-block/dispatch.js +57 -0
  724. package/dist/selection/cross-block/format-range.d.ts +38 -0
  725. package/dist/selection/cross-block/format-range.js +193 -0
  726. package/dist/selection/cross-block/format-toggle.d.ts +27 -0
  727. package/dist/selection/cross-block/format-toggle.js +109 -0
  728. package/dist/selection/cross-block/keydown.d.ts +11 -0
  729. package/dist/selection/cross-block/keydown.js +339 -0
  730. package/dist/selection/cross-block/ops.d.ts +56 -0
  731. package/dist/selection/cross-block/ops.js +210 -0
  732. package/dist/selection/cross-block/paste.d.ts +8 -0
  733. package/dist/selection/cross-block/paste.js +156 -0
  734. package/dist/selection/cross-block/pointer.d.ts +19 -0
  735. package/dist/selection/cross-block/pointer.js +80 -0
  736. package/dist/selection/cross-block/type-replace.d.ts +10 -0
  737. package/dist/selection/cross-block/type-replace.js +129 -0
  738. package/dist/selection/dead-space-caret.d.ts +39 -0
  739. package/dist/selection/dead-space-caret.js +173 -0
  740. package/dist/selection/drag-pointer.d.ts +22 -0
  741. package/dist/selection/drag-pointer.js +89 -0
  742. package/dist/selection/gap-caret.d.ts +47 -0
  743. package/dist/selection/gap-caret.js +72 -0
  744. package/dist/selection/keyboard-extend.d.ts +43 -0
  745. package/dist/selection/keyboard-extend.js +257 -0
  746. package/dist/selection/native-bridge.d.ts +47 -0
  747. package/dist/selection/native-bridge.js +204 -0
  748. package/dist/selection/nearest-block.d.ts +43 -0
  749. package/dist/selection/nearest-block.js +73 -0
  750. package/dist/selection/path-lookup.d.ts +32 -0
  751. package/dist/selection/path-lookup.js +132 -0
  752. package/dist/selection/path-math.d.ts +35 -0
  753. package/dist/selection/path-math.js +90 -0
  754. package/dist/selection/pointer-session.d.ts +43 -0
  755. package/dist/selection/pointer-session.js +111 -0
  756. package/dist/selection/primitives.d.ts +75 -0
  757. package/dist/selection/primitives.js +91 -0
  758. package/dist/selection/range-delete-ceremony.d.ts +85 -0
  759. package/dist/selection/range-delete-ceremony.js +197 -0
  760. package/dist/selection/range-delete-chrome.d.ts +39 -0
  761. package/dist/selection/range-delete-chrome.js +105 -0
  762. package/dist/selection/range-delete-table-coverage.d.ts +28 -0
  763. package/dist/selection/range-delete-table-coverage.js +168 -0
  764. package/dist/selection/range-delete-table.d.ts +16 -0
  765. package/dist/selection/range-delete-table.js +347 -0
  766. package/dist/selection/range-delete.d.ts +33 -0
  767. package/dist/selection/range-delete.js +121 -0
  768. package/dist/selection/selection-description.d.ts +7 -0
  769. package/dist/selection/selection-description.js +18 -0
  770. package/dist/selection/selection-restore.d.ts +46 -0
  771. package/dist/selection/selection-restore.js +66 -0
  772. package/dist/selection/selection-state.svelte.d.ts +72 -0
  773. package/dist/selection/selection-state.svelte.js +247 -0
  774. package/dist/selection/shared-keydown.d.ts +61 -0
  775. package/dist/selection/shared-keydown.js +150 -0
  776. package/dist/selection/table-endpoint-snap.d.ts +55 -0
  777. package/dist/selection/table-endpoint-snap.js +143 -0
  778. package/dist/selection/table-rect-extend.d.ts +23 -0
  779. package/dist/selection/table-rect-extend.js +46 -0
  780. package/dist/selection/widget-range-paint.d.ts +12 -0
  781. package/dist/selection/widget-range-paint.js +44 -0
  782. package/dist/styles/editor-theme.css +151 -0
  783. package/dist/styles/editor.css +585 -0
  784. package/dist/testing/conformance-core.d.ts +45 -0
  785. package/dist/testing/conformance-core.js +105 -0
  786. package/dist/testing/container-conformance.d.ts +137 -0
  787. package/dist/testing/container-conformance.js +533 -0
  788. package/dist/testing/headless-actions.d.ts +37 -0
  789. package/dist/testing/headless-actions.js +118 -0
  790. package/dist/testing/inline-conformance.d.ts +50 -0
  791. package/dist/testing/inline-conformance.js +442 -0
  792. package/dist/testing/kind-conformance.d.ts +54 -0
  793. package/dist/testing/kind-conformance.js +325 -0
  794. package/dist/testing/mount-dom-stubs.d.ts +8 -0
  795. package/dist/testing/mount-dom-stubs.js +19 -0
  796. package/dist/testing/parse-convergence.d.ts +14 -0
  797. package/dist/testing/parse-convergence.js +81 -0
  798. package/dist/testing.d.ts +20 -0
  799. package/dist/testing.js +61 -0
  800. package/dist/tree-operations/blockquote.d.ts +18 -0
  801. package/dist/tree-operations/blockquote.js +59 -0
  802. package/dist/tree-operations/children.d.ts +17 -0
  803. package/dist/tree-operations/children.js +56 -0
  804. package/dist/tree-operations/cleanup.d.ts +10 -0
  805. package/dist/tree-operations/cleanup.js +25 -0
  806. package/dist/tree-operations/clone.d.ts +5 -0
  807. package/dist/tree-operations/clone.js +43 -0
  808. package/dist/tree-operations/container-lift.d.ts +9 -0
  809. package/dist/tree-operations/container-lift.js +29 -0
  810. package/dist/tree-operations/index.d.ts +16 -0
  811. package/dist/tree-operations/index.js +14 -0
  812. package/dist/tree-operations/list/empty-check.d.ts +7 -0
  813. package/dist/tree-operations/list/empty-check.js +19 -0
  814. package/dist/tree-operations/list/exit-replacement.d.ts +13 -0
  815. package/dist/tree-operations/list/exit-replacement.js +49 -0
  816. package/dist/tree-operations/list/item-partition.d.ts +13 -0
  817. package/dist/tree-operations/list/item-partition.js +33 -0
  818. package/dist/tree-operations/list/list-builders.d.ts +44 -0
  819. package/dist/tree-operations/list/list-builders.js +129 -0
  820. package/dist/tree-operations/list/ordered-markers.d.ts +36 -0
  821. package/dist/tree-operations/list/ordered-markers.js +114 -0
  822. package/dist/tree-operations/list/reconcile-task.d.ts +12 -0
  823. package/dist/tree-operations/list/reconcile-task.js +47 -0
  824. package/dist/tree-operations/list/sublist-separator.d.ts +13 -0
  825. package/dist/tree-operations/list/sublist-separator.js +34 -0
  826. package/dist/tree-operations/list/terminator.d.ts +17 -0
  827. package/dist/tree-operations/list/terminator.js +60 -0
  828. package/dist/tree-operations/list/unwrap-merge.d.ts +29 -0
  829. package/dist/tree-operations/list/unwrap-merge.js +202 -0
  830. package/dist/tree-operations/node-ops.d.ts +296 -0
  831. package/dist/tree-operations/node-ops.js +1428 -0
  832. package/dist/tree-operations/parse-block.d.ts +3 -0
  833. package/dist/tree-operations/parse-block.js +8 -0
  834. package/dist/tree-operations/paste/apply.d.ts +12 -0
  835. package/dist/tree-operations/paste/apply.js +75 -0
  836. package/dist/tree-operations/paste/body-write.d.ts +19 -0
  837. package/dist/tree-operations/paste/body-write.js +53 -0
  838. package/dist/tree-operations/paste/container-match.d.ts +32 -0
  839. package/dist/tree-operations/paste/container-match.js +238 -0
  840. package/dist/tree-operations/paste/container-paste.d.ts +10 -0
  841. package/dist/tree-operations/paste/container-paste.js +27 -0
  842. package/dist/tree-operations/paste/dispatch.d.ts +61 -0
  843. package/dist/tree-operations/paste/dispatch.js +138 -0
  844. package/dist/tree-operations/paste/find-enclosing-list.d.ts +8 -0
  845. package/dist/tree-operations/paste/find-enclosing-list.js +30 -0
  846. package/dist/tree-operations/paste/focus-target.d.ts +27 -0
  847. package/dist/tree-operations/paste/focus-target.js +40 -0
  848. package/dist/tree-operations/paste/hooks.d.ts +17 -0
  849. package/dist/tree-operations/paste/hooks.js +80 -0
  850. package/dist/tree-operations/paste/list-absorb.d.ts +29 -0
  851. package/dist/tree-operations/paste/list-absorb.js +106 -0
  852. package/dist/tree-operations/paste/list-break-out.d.ts +42 -0
  853. package/dist/tree-operations/paste/list-break-out.js +136 -0
  854. package/dist/tree-operations/paste/parent-scope.d.ts +17 -0
  855. package/dist/tree-operations/paste/parent-scope.js +35 -0
  856. package/dist/tree-operations/paste/paste-deps.d.ts +20 -0
  857. package/dist/tree-operations/paste/paste-deps.js +6 -0
  858. package/dist/tree-operations/paste/paste-replacement.d.ts +8 -0
  859. package/dist/tree-operations/paste/paste-replacement.js +60 -0
  860. package/dist/tree-operations/paste/paste-transforms.d.ts +25 -0
  861. package/dist/tree-operations/paste/paste-transforms.js +72 -0
  862. package/dist/tree-operations/paste/replace-block-at-parent.d.ts +29 -0
  863. package/dist/tree-operations/paste/replace-block-at-parent.js +75 -0
  864. package/dist/tree-operations/paste/strategy.d.ts +9 -0
  865. package/dist/tree-operations/paste/strategy.js +20 -0
  866. package/dist/tree-operations/paste/table-slice.d.ts +10 -0
  867. package/dist/tree-operations/paste/table-slice.js +39 -0
  868. package/dist/tree-operations/paste-surfaces.d.ts +61 -0
  869. package/dist/tree-operations/paste-surfaces.js +15 -0
  870. package/dist/tree-operations/path-mutate.d.ts +12 -0
  871. package/dist/tree-operations/path-mutate.js +20 -0
  872. package/dist/tree-operations/reorder-unit.d.ts +17 -0
  873. package/dist/tree-operations/reorder-unit.js +37 -0
  874. package/dist/tree-operations/reorder.d.ts +13 -0
  875. package/dist/tree-operations/reorder.js +57 -0
  876. package/dist/tree-operations/sharing.d.ts +18 -0
  877. package/dist/tree-operations/sharing.js +14 -0
  878. package/dist/tree-operations/splice-many.d.ts +9 -0
  879. package/dist/tree-operations/splice-many.js +22 -0
  880. package/dist/tree-operations/structural-change.d.ts +65 -0
  881. package/dist/tree-operations/structural-change.js +117 -0
  882. package/dist/tree-operations/sub-table-copy.d.ts +6 -0
  883. package/dist/tree-operations/sub-table-copy.js +43 -0
  884. package/dist/tree-operations/table-mutations.d.ts +17 -0
  885. package/dist/tree-operations/table-mutations.js +101 -0
  886. package/dist/tree-operations/unshare.d.ts +101 -0
  887. package/dist/tree-operations/unshare.js +248 -0
  888. package/dist/undo/manager.d.ts +2 -0
  889. package/dist/undo/manager.js +50 -0
  890. package/dist/undo/types.d.ts +44 -0
  891. package/dist/undo/types.js +7 -0
  892. package/docs/guide/consumer-guide.md +1098 -0
  893. package/docs/guide/directives.md +223 -0
  894. package/docs/guide/plugin-api.md +329 -0
  895. package/docs/guide/plugin-guide/conspiracy.gif +0 -0
  896. package/docs/guide/plugin-guide/parrot-frames.md +209 -0
  897. package/docs/guide/plugin-guide/parrot.gif +0 -0
  898. package/docs/guide/plugin-guide.md +1693 -0
  899. package/docs/guide/plugin-testing.md +463 -0
  900. package/package.json +238 -0
@@ -0,0 +1,1693 @@
1
+ # Plugin Author Guide
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.
4
+
5
+ Four neighbouring docs carry what this one doesn't:
6
+
7
+ - [directives.md](directives.md): the `:::name` directive grammar the walkthrough below builds on.
8
+ - [consumer-guide.md](consumer-guide.md): embedding, theming, and events, the host app's side.
9
+ - [plugin-api.md](plugin-api.md): a catalog of every export named anywhere below, for checking a name is real.
10
+ - [plugin-testing.md](plugin-testing.md): testing what you build.
11
+
12
+ This one's long, so here's a map. Each section stands on its own; jump straight to your question.
13
+
14
+ | Section | What it covers |
15
+ | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
16
+ | [The first fifteen minutes](#the-first-fifteen-minutes) | One working plugin, a party parrot, from nothing to a passing test |
17
+ | [What a plugin is](#what-a-plugin-is) | The mental model: what you declare, what the editor does with it, and which parts of the API are stable |
18
+ | [Views: what you read, what you own](#views-what-you-read-what-you-own) | Why the document objects you get handed are read-only, and the right ways to change the document |
19
+ | [One process, many editors](#one-process-many-editors) | Two editors on one page: what they share, what stays separate, per-editor options |
20
+ | [Walkthrough: a `:::conspiracy` container](#walkthrough-a-conspiracy-container-end-to-end) | Building a conspiracy-theory box (a title, evidence, a debunk shortcut), end to end: the biggest worked example |
21
+ | [The closure block](#the-closure-block) | The checklist every block must fill in: how it behaves under undo, selection, clipboard, and friends |
22
+ | [Teaching the parser](#teaching-the-parser) | How the editor recognizes your syntax: matching lines, competing matches, multi-line constructs |
23
+ | [Editable-content tiers](#editable-content-tiers) | The four ways a plugin can host user-editable content, and the text-block tier in depth |
24
+ | [Presentation modes](#presentation-modes) | Rendering right in reading, preview, and live modes |
25
+ | [Recipe: a render-primary block](#recipe-a-render-primary-block) | A diagram-shaped block: rendered picture, plugin-owned editing |
26
+ | [Recipe: reading the document above your block](#recipe-reading-the-document-above-your-block) | A block that derives from the whole document, like a table of contents |
27
+ | [Inline kinds](#inline-kinds) | Your own inline syntax: recognizing it mid-paragraph, rendering it as a widget, editing it |
28
+ | [Decorations](#decorations) | View-only annotations over content you don't own |
29
+ | [Block commands](#block-commands) | Keyboard shortcuts and commands, for one block kind or for the whole editor |
30
+ | [Paste transforms](#paste-transforms) | Rewriting pasted text before it parses |
31
+ | [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
+ | [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 |
33
+
34
+ ## The first fifteen minutes
35
+
36
+ The `npm install @voithos-labs/aragonite` you already ran brings everything this guide uses: the package root, the `@voithos-labs/aragonite/plugin` authoring path, and the `@voithos-labs/aragonite/testing` entry your test suite will import. Nothing else to install.
37
+
38
+ The plugin we're about to build is an homage to `curl parrot.live`: a line that starts with `%%parrot` renders as an animated ASCII party parrot, and any text after the marker becomes the parrot's caption.
39
+
40
+ Before the code, two terms everything below leans on.
41
+
42
+ 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
+ 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
+
46
+ **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
+ - **`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
+ - **`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)).
52
+
53
+ The first one in action (a kind is a plain string underneath, with a type brand on top):
54
+
55
+ ```ts
56
+ const parrot = declarePluginKind('parrot'); // 'parrot', branded as a kind
57
+ declaredPluginKind('parrot') === parrot; // true, same brand
58
+ declaredPluginKind('parot'); // throws: "parot" has not been declared
59
+ declarePluginKind('paragraph'); // throws: "paragraph" is a built-in BlockKind
60
+ ```
61
+
62
+ And all four together, which is the whole plugin minus its component:
63
+
64
+ ```ts
65
+ // parrot-plugin.ts
66
+ import {
67
+ caretOffsetAtPoint,
68
+ declarePluginKind,
69
+ definePluginBlock,
70
+ registerBlockKind,
71
+ registerBlockOpener,
72
+ simpleLeafClosure,
73
+ type CaretTarget,
74
+ type EditorPlugin
75
+ } from '@voithos-labs/aragonite/plugin';
76
+ import ParrotBlock from './ParrotBlock.svelte';
77
+
78
+ export const PARROT = 'parrot';
79
+
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. */
82
+ function parrotCaretAtPoint(
83
+ blockEl: HTMLElement,
84
+ clientX: number,
85
+ clientY: number
86
+ ): CaretTarget | null {
87
+ const source = blockEl.querySelector<HTMLElement>('.parrot-source');
88
+ const view = source ?? blockEl.querySelector<HTMLElement>('.parrot-caption');
89
+ if (!view) return null;
90
+ const offset = caretOffsetAtPoint(view, clientX, clientY) ?? 0;
91
+ return { path: [], offset: source ? offset : offset + '%%parrot '.length };
92
+ }
93
+
94
+ function registerParrotBlock(): void {
95
+ const parrot = declarePluginKind(PARROT);
96
+
97
+ registerBlockKind(parrot, {
98
+ gapEdges: 'none',
99
+ mergeRole: 'not-mergeable',
100
+ editable: true,
101
+ supportsInline: false,
102
+ conformanceFixture: '%%parrot party responsibly\n',
103
+ caretTargetAtPoint: parrotCaretAtPoint,
104
+ closure: simpleLeafClosure({
105
+ focus: { mode: 'implemented', via: 'createEditableLeaf render-primary reveal' },
106
+ searchPaint: { mode: 'implemented', via: 'source raw scanned, matches painted as marks' },
107
+ undo: { mode: 'implemented', via: 'render-primary: one commit when the caret leaves' },
108
+ simOracle: { mode: 'inherit-default' }
109
+ })
110
+ });
111
+
112
+ registerBlockOpener(parrot, {
113
+ priority: 25,
114
+ interruptsParagraph: (text) => text.startsWith('%%parrot'),
115
+ tryOpen(ctx) {
116
+ if (!ctx.line.text.startsWith('%%parrot')) return null;
117
+ const node = { kind: parrot, leadingTrivia: ctx.leadingTrivia, raw: ctx.line.raw };
118
+ return { node, consumed: 1 };
119
+ }
120
+ });
121
+ }
122
+
123
+ export function parrotPlugin(): EditorPlugin {
124
+ return definePluginBlock({
125
+ name: 'parrot',
126
+ kind: PARROT,
127
+ component: ParrotBlock,
128
+ register: registerParrotBlock
129
+ });
130
+ }
131
+ ```
132
+
133
+ The object you handed `registerBlockKind` is the kind's **descriptor**. Most of its fields read as they sound. Four don't:
134
+
135
+ - `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
+ - `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
+ - `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
+
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.
141
+
142
+ 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
+
144
+ **Render.** The parrot is a **leaf**, a block with no child blocks (a container holds other blocks; the walkthrough later builds one). `createEditableLeaf` hands a leaf a native caret, IME composition (typing through an input method, the way Chinese or Japanese is typed), undo, selection, and clipboard. The parrot asks for it in `render-primary` mode, where the caption is what you see at rest and the source line only shows while the caret is in the block. The bird itself is ordinary **chrome** (a block's furniture, as opposed to its content): every frame stacked in one `<pre>` that CSS scrolls a frame at a time.
145
+
146
+ ```svelte
147
+ <!-- ParrotBlock.svelte -->
148
+ <script lang="ts">
149
+ import { createEditableLeaf, type NodeView } from '@voithos-labs/aragonite/plugin';
150
+
151
+ let { node, index, myPath = [] }: { node: NodeView; index: number; myPath?: number[] } = $props();
152
+ let sourceEl: HTMLDivElement | undefined = $state();
153
+ let revealed = $state(false);
154
+
155
+ const leaf = createEditableLeaf({
156
+ getNode: () => node,
157
+ getIndex: () => index,
158
+ getPath: () => myPath,
159
+ getEl: () => sourceEl ?? null,
160
+ mode: 'render-primary',
161
+ singleLine: true,
162
+ isRevealed: () => revealed,
163
+ setRevealed: (next) => (revealed = next)
164
+ });
165
+
166
+ // Frames 0 and 5 of the canonical ten. The full dance is in ./plugin-guide/parrot-frames.md;
167
+ // this is a guide, not an aviary.
168
+ const FRAMES = [
169
+ String.raw`
170
+ .cccc;;cc;';c.
171
+ .,:dkdc:;;:c:,:d:.
172
+ .loc'.,cc::c:::,..;:.
173
+ .cl;....;dkdccc::,...c;
174
+ .c:,';:'..ckc',;::;....;c.
175
+ .c:'.,dkkoc:ok:;llllc,,c,';:.
176
+ .;c,';okkkkkkkk:;lllll,:kd;.;:,.
177
+ co..:kkkkkkkkkk:;llllc':kkc..oNc
178
+ .cl;.,oxkkkkkkkkkc,:cll;,okkc'.cO;
179
+ ;k:..ckkkkkkkkkkkl..,;,.;xkko:',l'
180
+ .,...';dkkkkkkkkkkd;.....ckkkl'.cO;
181
+ .,,:,.;oo:ckkkkkkkkkkkdoc;;cdkkkc..cd,
182
+ .cclo;,ccdkkl;llccdkkkkkkkkkkkkkkkd,.c;
183
+ .lol:;;okkkkkxooc::coodkkkkkkkkkkkko'.oc
184
+ .c:'..lkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkd,.oc
185
+ .lo;,:cdkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkd,.c;
186
+ ,dx:..;lllllllllllllllllllllllllllllllllc'...
187
+ cNO;........................................
188
+ `,
189
+ String.raw`
190
+
191
+ .,,,,,,,,,.
192
+ .ckKxodooxOOdcc.
193
+ .cclooc'....';;cool.
194
+ .loc;;;;clllllc;;;;;:;,.
195
+ .c:'.,okd;;cdo:::::cl,..oc
196
+ .:o;';okkx;';;,';::;'....,:,.
197
+ co..ckkkkkddkc,cclll;.,c:,:o:.
198
+ co..ckkkkkkkk:,cllll;.:kkd,.':c.
199
+ .,:;.,okkkkkkkk:,cclll;.ckkkdl;;o:.
200
+ cNo..ckkkkkkkkko,.;loc,.ckkkkkc..oc
201
+ ,dd;.:kkkkkkkkkx;..;:,.'lkkkkko,.:,
202
+ ;:.ckkkkkkkkkkc.....;ldkkkkkk:.,'
203
+ ,dc..'okkkkkkkkkxoc;;cxkkkkkkkkc..,;,.
204
+ kNo..':lllllldkkkkkkkkkkkkkkkkkdcc,.;l.
205
+ KOc,c;''''''';lldkkkkkkkkkkkkkkkkkc..;lc.
206
+ xx:':;;;;,.,,...,;;cllllllllllllllc;'.;od,
207
+ cNo.....................................oc
208
+ `
209
+ ];
210
+ // One strip the CSS scrolls a frame at a time. The closing newline is load-bearing: a `pre`
211
+ // drops a trailing blank line, and a strip a row short steps a fraction off every frame.
212
+ const REEL = FRAMES.join('\n') + '\n';
213
+ // The clip window's height, which is why every frame has to be the same number of rows.
214
+ const FRAME_ROWS = FRAMES[0].split('\n').length;
215
+
216
+ const caption = $derived(node.raw.slice('%%parrot'.length).trim());
217
+
218
+ export const editable = true;
219
+ export const focusable = true;
220
+ export const focus = leaf.focus;
221
+ export const getCursorOffset = leaf.getCursorOffset;
222
+ export const parkCaret = leaf.parkCaret;
223
+ export const focusAtColumn = leaf.focusAtColumn;
224
+ export const getSelectedText = leaf.getSelectedText;
225
+ export const setSelection = leaf.setSelection;
226
+ export const measurePartialRects = leaf.measurePartialRects;
227
+ export const runCommand = leaf.runCommand;
228
+ export const insertMarkdown = leaf.insertMarkdown;
229
+ </script>
230
+
231
+ <div
232
+ class="parrot-block"
233
+ {...leaf.renderProps}
234
+ style:--parrot-rows={FRAME_ROWS}
235
+ style:--parrot-frames={FRAMES.length}
236
+ >
237
+ <div class="parrot" aria-hidden="true"><pre class="parrot-reel">{REEL}</pre></div>
238
+ {#if revealed}
239
+ <div
240
+ bind:this={sourceEl}
241
+ {...leaf.surfaceProps}
242
+ class="parrot-source"
243
+ aria-label="Party parrot source"
244
+ ></div>
245
+ {:else}
246
+ <div
247
+ class="parrot-caption"
248
+ role="button"
249
+ tabindex="-1"
250
+ aria-label="Party parrot caption (click to edit)"
251
+ >
252
+ {caption}
253
+ </div>
254
+ {/if}
255
+ </div>
256
+
257
+ <style>
258
+ .parrot {
259
+ /* a terminal cell is about twice as tall as it is wide; prose line-height stretches the bird */
260
+ font-size: 1.1em;
261
+ line-height: 1.1;
262
+ letter-spacing: 0.05em;
263
+ /* one frame tall, in the reel's own rows so a step lands on the next frame exactly */
264
+ height: calc(var(--parrot-rows) * 1lh);
265
+ /* wider than a phone column, and the editor root pans if it isn't contained; the bar
266
+ would sit across the bird, which is decoration rather than a pane to scroll */
267
+ overflow-x: auto;
268
+ overflow-y: hidden;
269
+ scrollbar-width: none;
270
+ /* chrome, not content: every frame is in the DOM and none of them belong in a copy */
271
+ user-select: none;
272
+ animation: parrot-hue 0.49s step-end infinite;
273
+ }
274
+ .parrot-reel {
275
+ /* type and rhythm come from the box above, so its `lh` is this reel's row exactly */
276
+ margin: 0;
277
+ animation-name: parrot-reel;
278
+ animation-duration: calc(var(--parrot-frames) * 70ms);
279
+ animation-timing-function: steps(var(--parrot-frames));
280
+ animation-iteration-count: infinite;
281
+ }
282
+ @keyframes parrot-reel {
283
+ to {
284
+ transform: translateY(-100%);
285
+ }
286
+ }
287
+ /* parrot.live's seven, stepped rather than blended so every run of the dance looks the same */
288
+ @keyframes parrot-hue {
289
+ 0% {
290
+ color: #ff5f5f;
291
+ }
292
+ 14.286% {
293
+ color: #ffc83d;
294
+ }
295
+ 28.571% {
296
+ color: #3fd97a;
297
+ }
298
+ 42.857% {
299
+ color: #5aa9ff;
300
+ }
301
+ 57.143% {
302
+ color: #ff6ad5;
303
+ }
304
+ 71.429% {
305
+ color: #3fd3d3;
306
+ }
307
+ 85.714% {
308
+ color: var(--color-text-primary, #ffffff);
309
+ }
310
+ }
311
+ @media (prefers-reduced-motion: reduce) {
312
+ .parrot,
313
+ .parrot-reel {
314
+ animation: none;
315
+ }
316
+ }
317
+ .parrot-caption {
318
+ margin: 0.25em 0 0;
319
+ font-weight: 700;
320
+ cursor: text;
321
+ }
322
+ .parrot-source {
323
+ /* the bytes, dimmed the way the editor dims a marker */
324
+ opacity: 0.55;
325
+ font-family: monospace;
326
+ font-size: 0.9em;
327
+ outline: none;
328
+ }
329
+ </style>
330
+ ```
331
+
332
+ The component has an editing half and a parrot half, and the parrot half never touches the editor.
333
+
334
+ The editing half is the factory call, the `revealed` flag, two spreads, and the one-line re-exports:
335
+
336
+ - `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
+ - `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.
339
+ - 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
+ - `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
+
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.
343
+
344
+ 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
+
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):
347
+
348
+ ```ts
349
+ parse('%%parrot party responsibly\n').children[0];
350
+ // { kind: 'parrot', leadingTrivia: '', raw: '%%parrot party responsibly\n' }
351
+ ```
352
+
353
+ It dances through your component.
354
+
355
+ ![The parrot block, mid-party](./plugin-guide/parrot.gif)
356
+
357
+ **Verify.** Registering a kind enrolls it in the conformance kit, which parses your fixture, round-trips it (parse, serialize, compare the bytes), and checks the closure cells it can reach without a browser against what you claimed.
358
+
359
+ ```ts
360
+ import { installPlugins } from '@voithos-labs/aragonite';
361
+ import { declaredPluginKind } from '@voithos-labs/aragonite/plugin';
362
+ import { resetPluginPlatformForTests, runKindConformance } from '@voithos-labs/aragonite/testing';
363
+ import { PARROT, parrotPlugin } from './parrot-plugin';
364
+
365
+ it('parrot conforms', async () => {
366
+ resetPluginPlatformForTests();
367
+ installPlugins([parrotPlugin()]);
368
+ await runKindConformance(declaredPluginKind(PARROT));
369
+ });
370
+ ```
371
+
372
+ That's the loop: describe the kind, render it, install it, verify the bytes. Everything after this is the same four moves at more interesting shapes.
373
+
374
+ ## What a plugin is
375
+
376
+ A plugin teaches the editor a new kind: a first-class citizen of the document tree that parses, renders, and serializes alongside the built-ins (not an embed bolted to the side). You declare the kind, then wire up to three things:
377
+
378
+ ```
379
+ declare a kind ──┬─▶ descriptor how it merges, its container shape, its keymap
380
+ ├─▶ component how it renders and hosts any editable content
381
+ └─▶ grammar how source becomes the kind:
382
+ a block opener │ a :::name directive │ an inline recognizer
383
+ ```
384
+
385
+ Each part has a defined absence, which is prob the easiest way to remember what each one is for:
386
+
387
+ | Part | It makes the kind | Leave it out and |
388
+ | ---------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
389
+ | descriptor | behave | the kind errors at first use; this one is never optional |
390
+ | component | visible | the kind renders as a visible raw-text fallback |
391
+ | grammar | parseable from Markdown | nothing ever parses to the kind, and bytes that reload as prose are no kind at all; [the menu recipe](#recipe-a-kind-only-a-menu-creates) is the honest route to a kind nobody types |
392
+
393
+ ### Registration is global, and register-once
394
+
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.)
396
+
397
+ Who guarantees a registration runs only once depends on where it runs:
398
+
399
+ - **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.
401
+
402
+ ```ts
403
+ isBlockKindDeclared('parrot'); // false on a fresh page
404
+ if (!isBlockKindDeclared(PARROT)) registerParrotBlock();
405
+ isBlockKindDeclared('parrot'); // true, so a second import of this module skips the register
406
+ ```
407
+
408
+ 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
+
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.
411
+
412
+ ### The plugin unit
413
+
414
+ A **plugin unit** is the installable package: a name plus a `setup` that runs your `register*` calls.
415
+
416
+ **`definePlugin({ name, setup })`**
417
+
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.
419
+
420
+ ```ts
421
+ export function myPlugin(options?: { renderer?: Renderer }): EditorPlugin {
422
+ return definePlugin({
423
+ name: 'my-plugin',
424
+ setup() {
425
+ registerMyKind(options?.renderer ?? defaultRenderer);
426
+ registerBlockComponent(declaredPluginKind('my-kind'), defineBlockComponent(MyBlock));
427
+ }
428
+ });
429
+ }
430
+ ```
431
+
432
+ Install by passing units to the editor's **`plugins` prop**, set once at mount, before the first parse:
433
+
434
+ ```svelte
435
+ <script module lang="ts">
436
+ import { myPlugin } from './my-plugin';
437
+
438
+ // 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.
441
+ const plugins = [myPlugin()];
442
+ </script>
443
+
444
+ <Editor {source} {plugins} />
445
+ ```
446
+
447
+ **A plugin installs once per process, keyed by name.** The consequences, one per line:
448
+
449
+ - Passing the same unit again no-ops.
450
+ - 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
+ - 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.
453
+ - 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.
455
+
456
+ 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
+
458
+ ```ts
459
+ import { installPlugins } from '@voithos-labs/aragonite';
460
+
461
+ isPluginInstalled('parrot'); // false
462
+ installPlugins([parrotPlugin()]);
463
+ isPluginInstalled('parrot'); // true
464
+ installPlugins([parrotPlugin()]); // no-op
465
+ ```
466
+
467
+ ### What is stable, what is not
468
+
469
+ The API is going to freeze, and you deserve to know which half of it is already load-bearing.
470
+
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).)
472
+ - **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
+
474
+ 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**.
475
+
476
+ ## Views: what you read, what you own
477
+
478
+ Every surface that hands your plugin a node to **read** types it as a view: `NodeView` for a block node, `DocumentView` for the root document. A view is deep-readonly on the serialized bytes: a byte write through one is a compile error. "Never mutate the tree from the view layer" isn't a rule you have to remember.
479
+
480
+ Two lists cover the whole read side:
481
+
482
+ - **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.
484
+
485
+ `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
+
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.
488
+
489
+ ### `rebuildRaw`, the write hook
490
+
491
+ You'll meet it again and again in this guide, so here's the fuller treatment.
492
+
493
+ **`rebuildRaw(node, changed?)`**
494
+
495
+ The hook a container kind declares so the editor can recompute the container's raw from its children and metadata after an edit. It receives the **owned** `CstNode` and writes the recomputed bytes onto it. Ignoring the second argument and re-deriving the whole raw is always correct, and it's what most rebuilders should do (it's also what every rebuilder does when the argument is absent).
496
+
497
+ ```ts
498
+ // A fence around its children, nothing else in the raw. serializeChildren joins each
499
+ // child's leadingTrivia + raw, the same join a save runs.
500
+ function rebuildBoxRaw(node: CstNode): void {
501
+ node.raw = `:::box\n${serializeChildren(node.children ?? [])}:::\n`;
502
+ }
503
+ // the editor calls it after a child edit; node.raw now reads ':::box\nedited line\n:::\n'
504
+ ```
505
+
506
+ 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
+
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.
509
+
510
+ ## One process, many editors
511
+
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`**:
513
+
514
+ ```ts
515
+ setup(ctx) {
516
+ ctx.onEditor((editor) => {
517
+ editor.editorId; // 'editor-1' (the next editor to mount gets 'editor-2')
518
+ editor.document.children.length; // 3, and live: read it again later and you get the current count
519
+ editor.presentationMode; // 'source'
520
+ editor.theme; // 'dark'
521
+ return () => {}; // runs at unmount
522
+ });
523
+ }
524
+ ```
525
+
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.
538
+
539
+ ### Recipe: per-instance derived state
540
+
541
+ There's no plugin-state field on the platform, and you don't need one. Keep your own map keyed on `editorId`, seed it when the editor mounts, recompute on the `edit` event, and delete the entry in the disposer. That's the whole feature.
542
+
543
+ ```ts
544
+ import { definePlugin, type EditorContext } from '@voithos-labs/aragonite/plugin';
545
+
546
+ interface WordCountOptions {
547
+ live: boolean; // recount on every edit, or only at mount
548
+ }
549
+
550
+ // Per-editor state lives in a plugin-owned map, not a platform field.
551
+ const countByEditor = new Map<string, number>();
552
+
553
+ function recount(editor: EditorContext<WordCountOptions>): void {
554
+ const words = editor.document.children.reduce(
555
+ (n, block) => n + block.raw.split(/\s+/).filter(Boolean).length,
556
+ 0
557
+ );
558
+ countByEditor.set(editor.editorId, words);
559
+ }
560
+
561
+ export const wordCountPlugin = definePlugin<WordCountOptions>({
562
+ name: 'word-count',
563
+ setup(ctx) {
564
+ ctx.onEditor((editor) => {
565
+ // A bare-unit install passes no options, so default them.
566
+ const { live } = editor.options ?? { live: true };
567
+ recount(editor); // seed on mount
568
+ const off = live ? editor.events.on('edit', () => recount(editor)) : () => {};
569
+ return () => {
570
+ off();
571
+ countByEditor.delete(editor.editorId); // per-editor cleanup
572
+ };
573
+ });
574
+ }
575
+ });
576
+ ```
577
+
578
+ ### Recipe: per-instance options (and the factory-closure trap)
579
+
580
+ Two editors share one process-global registration but may still want different options; a split-pane host is the classic case. The consumer varies them per editor through the `plugins` prop's entry form:
581
+
582
+ ```svelte
583
+ <Editor source={left} plugins={[{ plugin: wordCountPlugin, options: { live: true } }]} />
584
+ <Editor source={right} plugins={[{ plugin: wordCountPlugin, options: { live: false } }]} />
585
+ ```
586
+
587
+ `definePlugin<WordCountOptions>` carries the type through, so `editor.options` reads typed inside `onEditor` with no cast.
588
+
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.
590
+
591
+ ## Walkthrough: a `:::conspiracy` container end to end
592
+
593
+ Enough preamble. This builds a `:::conspiracy` box: a titled, editable container whose title is a real editable line carrying the theory, and whose body holds the evidence as ordinary Markdown blocks. One kind answers to two directive names, `:::conspiracy` and `:::debunked` (somebody checked), and reads which one it is from its metadata, so the verdict is a one-field edit rather than a second block kind. Every import comes from the package, and it runs unchanged in a fresh SvelteKit app that installed the editor five minutes ago.
594
+
595
+ It reuses the `:::name` directive grammar rather than a hand-written opener. The grammar's tiers, dispatch, and losslessness belong to the [directives guide](directives.md); this walkthrough owns the descriptor and the component side.
596
+
597
+ ### The registration module
598
+
599
+ One file declares the kinds, describes them, maps the directive names, binds the component, and returns the whole thing as a `conspiracyPlugin()` unit.
600
+
601
+ ```ts
602
+ // conspiracy-kind.ts
603
+ import {
604
+ activateDirectives,
605
+ chromeChild,
606
+ createDirectiveRebuild,
607
+ declarePluginKind,
608
+ declaredPluginKind,
609
+ definePluginBlock,
610
+ DIRECTIVE_BODY_WRAP,
611
+ isDirectiveRegistered,
612
+ registerBlockKind,
613
+ registerBlockCommand,
614
+ registerChromeLeaf,
615
+ registerDirective,
616
+ setPluginMetadata,
617
+ type CstNode,
618
+ type EditorPlugin,
619
+ type ParsedDirective
620
+ } from '@voithos-labs/aragonite/plugin';
621
+ import ConspiracyBlock from './ConspiracyBlock.svelte'; // the component built in the next section
622
+
623
+ const CONSPIRACY = 'conspiracy';
624
+ const CONSPIRACY_TITLE = 'conspiracy-title';
625
+
626
+ export interface ConspiracyMetadata {
627
+ name: string; // 'conspiracy' or 'debunked'; re-emitted into raw so the verdict survives
628
+ colonCount: number;
629
+ closerColonCount: number;
630
+ closerNewline: boolean;
631
+ lineEnding: string; // captured at parse; createDirectiveRebuild re-emits it (CRLF-safe)
632
+ }
633
+
634
+ // Build the node from a parsed :::conspiracy fence. Child 0 is the title (the theory,
635
+ // from the opener line); children 1+ are the parsed evidence. The fence bytes go to
636
+ // metadata so the raw can be rebuilt after an edit.
637
+ function conspiracyFromDirective(parsed: ParsedDirective): CstNode {
638
+ const theory = parsed.fence.info.trim();
639
+ const node: CstNode = {
640
+ kind: declaredPluginKind(CONSPIRACY),
641
+ leadingTrivia: parsed.leadingTrivia,
642
+ raw: parsed.raw,
643
+ innerPrefix: parsed.body?.prefix ?? '',
644
+ children: [
645
+ chromeChild(declaredPluginKind(CONSPIRACY_TITLE), theory),
646
+ ...(parsed.body?.children ?? [])
647
+ ],
648
+ innerSuffix: parsed.body?.suffix ?? ''
649
+ };
650
+ setPluginMetadata<ConspiracyMetadata>(node, {
651
+ name: parsed.fence.name,
652
+ colonCount: parsed.fence.colonCount,
653
+ closerColonCount: parsed.closerColonCount,
654
+ closerNewline: parsed.closerNewline,
655
+ lineEnding: parsed.lineEnding
656
+ });
657
+ return node;
658
+ }
659
+
660
+ // Re-emit raw from the children after any structural edit. createDirectiveRebuild owns
661
+ // the title-to-opener line, the body serialization, and the authored line ending (the
662
+ // byte a hand-written copy silently drops); you supply only the verdict-name resolver.
663
+ const rebuildConspiracyRaw = createDirectiveRebuild<ConspiracyMetadata>(
664
+ (meta) => meta?.name ?? CONSPIRACY
665
+ );
666
+
667
+ function registerConspiracy(): void {
668
+ activateDirectives(); // idempotent; the shared grammar must be live before the first parse
669
+
670
+ const conspiracy = declarePluginKind(CONSPIRACY);
671
+ const conspiracyTitle = declarePluginKind(CONSPIRACY_TITLE);
672
+
673
+ // Two names, one kind: :::conspiracy and :::debunked both resolve here, the kind
674
+ // reading its verdict back from metadata; any other name falls through to the
675
+ // generic directive fallback. The guard is habit: nothing bundled claims these
676
+ // names, but note and tip are claimed by the bundled admonitions plugin, and
677
+ // that is where an unguarded claim throws for real.
678
+ for (const name of [CONSPIRACY, 'debunked']) {
679
+ if (!isDirectiveRegistered('container', name)) {
680
+ registerDirective('container', name, {
681
+ kind: conspiracy,
682
+ fromDirective: conspiracyFromDirective
683
+ });
684
+ }
685
+ }
686
+
687
+ // A block command that flips the verdict. updateMetadata is the sanctioned
688
+ // commit path: it merges the patch, runs rebuildRaw, and makes one undoable edit;
689
+ // because the name flows into raw, the verdict survives a round-trip.
690
+ const setVerdict = registerBlockCommand(conspiracy, 'conspiracy.setVerdict', (ctx) => {
691
+ if (typeof ctx.arg !== 'string') return false;
692
+ ctx.updateMetadata({ name: ctx.arg });
693
+ return true;
694
+ });
695
+
696
+ registerBlockKind(conspiracy, {
697
+ mergeRole: 'container',
698
+ editable: true,
699
+ supportsInline: false,
700
+ // Fences leave no textual way out at either edge, so both take the gap caret. Without
701
+ // this, two adjacent conspiracies give the user nowhere to type a paragraph between them.
702
+ gapEdges: 'both',
703
+ container: {
704
+ // The title lives in the opener line, so raw is not a strip of the children:
705
+ // 'opaque' marks raw authoritative.
706
+ contract: 'opaque',
707
+ rebuildRaw: rebuildConspiracyRaw,
708
+ // Every `:::` body parses against this wrap. Skip it and a filled innerPrefix
709
+ // trips a dev assertion the moment someone edits a conspiracy with a blank first line.
710
+ bodyWrap: DIRECTIVE_BODY_WRAP,
711
+ 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
+ }
719
+ // 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.
724
+ },
725
+ keymap: [
726
+ { chord: 'Mod+7', command: setVerdict, arg: 'conspiracy' }, // allege
727
+ { chord: 'Mod+8', command: setVerdict, arg: 'debunked' } // debunk
728
+ ],
729
+ // 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.
732
+ closure: {
733
+ roundTrip: { mode: 'implemented', via: 'container contract=opaque, rebuildConspiracyRaw' },
734
+ focus: { mode: 'implemented', via: 'focus walks to the title chrome / first body child' },
735
+ mergeBackspace: { mode: 'implemented', via: 'mergeRole=container + unwrapRole' },
736
+ selectionPaint: { mode: 'implemented', via: 'body child blocks paint; container cover' },
737
+ searchPaint: {
738
+ mode: 'implemented',
739
+ via: 'children are real blocks; search descends and paints'
740
+ },
741
+ reorder: { mode: 'implemented', via: 'whole-block reorder through the parent BlockList' },
742
+ undo: {
743
+ mode: 'implemented',
744
+ via: 'updateMetadata; the verdict flip commits as one undo entry'
745
+ },
746
+ // reservedChrome means the default byte slice is wrong for this kind, so the
747
+ // clipboard cell has to name what a slice touching the title actually does.
748
+ clipboard: {
749
+ mode: 'implemented',
750
+ via: 'byte-slice copy; a slice touching the title re-emits the conspiracy around the collected body'
751
+ },
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
754
+ // admitting you inherit the generic one.
755
+ simOracle: { mode: 'inherit-default' }
756
+ }
757
+ });
758
+
759
+ registerChromeLeaf(conspiracyTitle, { blockClass: 'conspiracy-title' });
760
+ }
761
+
762
+ // definePluginBlock wraps definePlugin around the register step and the component
763
+ // binding, so you write neither the setup-then-register order nor the
764
+ // registerBlockComponent(declaredPluginKind(...), defineBlockComponent(...)) double-wrap.
765
+ export function conspiracyPlugin(): EditorPlugin {
766
+ return definePluginBlock({
767
+ name: 'conspiracy',
768
+ kind: CONSPIRACY,
769
+ component: ConspiracyBlock,
770
+ register: registerConspiracy
771
+ });
772
+ }
773
+ ```
774
+
775
+ `registerDirective`'s `(tier, name)` mapping, the `ParsedDirective` shape, and the per-tier factory rules live in the [directives guide](directives.md). This module supplies the container factory (`fromDirective`, required for the container tier) and the descriptor. Here's what `conspiracyFromDirective` actually receives for the seed document [Wire it into a page](#wire-it-into-a-page) uses, and what it hands back:
776
+
777
+ ```ts
778
+ // parsed, for ':::conspiracy Birds are government drones\nThe quickstart parrot has danced since section one and never once eaten.\n:::\n'
779
+ {
780
+ fence: { tier: 'container', colonCount: 3, name: 'conspiracy', info: ' Birds are government drones' },
781
+ body: {
782
+ kind: 'document',
783
+ prefix: '',
784
+ children: [{ kind: 'paragraph', leadingTrivia: '', raw: 'The quickstart parrot has danced since section one and never once eaten.\n' }],
785
+ suffix: ''
786
+ },
787
+ leadingTrivia: '',
788
+ raw: ':::conspiracy Birds are government drones\nThe quickstart parrot has danced since section one and never once eaten.\n:::\n',
789
+ closerColonCount: 3,
790
+ closerNewline: true,
791
+ lineEnding: '\n'
792
+ }
793
+
794
+ // the node it builds
795
+ chromeChild(conspiracyTitle, 'Birds are government drones'); // { kind: 'conspiracy-title', leadingTrivia: '', raw: 'Birds are government drones\n' }
796
+ node.children.map((child) => child.kind); // ['conspiracy-title', 'paragraph']
797
+ getPluginMetadata<ConspiracyMetadata>(node); // { name: 'conspiracy', colonCount: 3, closerColonCount: 3, closerNewline: true, lineEnding: '\n' }
798
+ ```
799
+
800
+ That nine-column `closure` literal gets its own section, [The closure block](#the-closure-block), right after this walkthrough.
801
+
802
+ ### The component
803
+
804
+ Your component supplies only its own chrome: the border, the title styling, an icon if you like. Chrome may read the node it dresses: the verdict comes off the metadata through `getPluginMetadata`, and the stamp follows it. `createContainerBlock` hides the child-list state, the ancestor wiring, and the windowing (the editor only mounts the blocks in view, and a container windows its own children too). Pass `node`, `index`, and `path` as **thunks**, meaning `getNode` / `getIndex` / `getPath` functions the factory calls to read the live value each time; a captured value would be a stale snapshot. The type won't let you pass one anyway: a function-valued field is a live read, a plain-valued field is static config.
805
+
806
+ ```svelte
807
+ <!-- ConspiracyBlock.svelte -->
808
+ <script lang="ts">
809
+ import {
810
+ BlockList,
811
+ createContainerBlock,
812
+ getPluginMetadata,
813
+ type NodeView
814
+ } from '@voithos-labs/aragonite/plugin';
815
+ import type { ConspiracyMetadata } from './conspiracy-kind';
816
+
817
+ let { node, index, myPath = [] }: { node: NodeView; index: number; myPath?: number[] } = $props();
818
+ let boxEl: HTMLElement | undefined = $state();
819
+
820
+ const { blockListProps, containerApi, handleKeydown } = createContainerBlock({
821
+ getNode: () => node,
822
+ getIndex: () => index,
823
+ getPath: () => myPath,
824
+ getBoxEl: () => boxEl
825
+ });
826
+
827
+ // The verdict is read off the node, so the stamp lands the moment the command commits.
828
+ const debunked = $derived(getPluginMetadata<ConspiracyMetadata>(node)?.name === 'debunked');
829
+
830
+ export { containerApi };
831
+ </script>
832
+
833
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
834
+ <div class="conspiracy-block" class:debunked bind:this={boxEl} onkeydown={handleKeydown}>
835
+ <BlockList {...blockListProps} />
836
+ </div>
837
+
838
+ <style>
839
+ .conspiracy-block {
840
+ /* the corkboard, with one piece of red string */
841
+ position: relative;
842
+ border: 1px solid var(--color-ui-muted, #a4a4a4);
843
+ border-left: 3px solid var(--color-error, #e06c75);
844
+ border-radius: 6px;
845
+ padding: 8px 12px;
846
+ }
847
+ .conspiracy-block :global(.conspiracy-title) {
848
+ font-weight: 600;
849
+ }
850
+ /* debunked: the string comes down, the theory gets crossed out, the stamp lands */
851
+ .debunked {
852
+ border-left-color: var(--color-ui-muted, #a4a4a4);
853
+ }
854
+ .debunked :global(.conspiracy-title) {
855
+ text-decoration: line-through;
856
+ }
857
+ .debunked::after {
858
+ content: 'DEBUNKED';
859
+ position: absolute;
860
+ top: 6px;
861
+ right: 12px;
862
+ transform: rotate(-12deg);
863
+ font: 600 0.75em monospace;
864
+ letter-spacing: 0.12em;
865
+ color: var(--color-error, #e06c75);
866
+ border: 2px solid currentColor;
867
+ border-radius: 3px;
868
+ padding: 1px 6px;
869
+ }
870
+ </style>
871
+ ```
872
+
873
+ Three rules for that file, each earned the hard way:
874
+
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.
876
+ - **`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).
878
+
879
+ The factory returns more than the walkthrough destructures:
880
+
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)) |
888
+
889
+ ```ts
890
+ const { updateOwnMetadata, getPresentationMode, getTheme, getOptions } = createContainerBlock(deps);
891
+ updateOwnMetadata({ name: 'debunked' }); // one undo entry; rebuildRaw re-emits the opener line as :::debunked
892
+ getPresentationMode(); // 'source'
893
+ getTheme(); // 'dark'
894
+ getOptions(); // whatever this editor's { plugin, options } entry carried; undefined for a bare unit
895
+ ```
896
+
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.
898
+
899
+ ### Wire it into a page
900
+
901
+ Pass the plugin to the editor's `plugins` prop. It installs before the seed parses, so `:::conspiracy` resolves to your kind:
902
+
903
+ ```svelte
904
+ <script module lang="ts">
905
+ import { conspiracyPlugin } from './conspiracy-kind';
906
+
907
+ const plugins = [conspiracyPlugin()];
908
+ </script>
909
+
910
+ <script lang="ts">
911
+ import { Editor } from '@voithos-labs/aragonite';
912
+ import '@voithos-labs/aragonite/styles/editor-theme.css';
913
+
914
+ const SEED =
915
+ ':::conspiracy Birds are government drones\nThe quickstart parrot has danced since section one and never once eaten.\n:::\n';
916
+ let editor = $state();
917
+ </script>
918
+
919
+ <Editor bind:this={editor} source={SEED} {plugins} theme="light" />
920
+ ```
921
+
922
+ The chords are live (a **chord** is a key combination, written `Mod+7` where `Mod` is Ctrl, or Cmd on a Mac). Focus the box, press `Mod+8` to debunk the theory (the string comes down and the stamp lands) and `Mod+7` to allege it again, then read `editor.getSource()` back and watch the opener line flip between the two names:
923
+
924
+ ```ts
925
+ editor.getSource();
926
+ // ':::conspiracy Birds are government drones\nThe quickstart parrot has danced since section one and never once eaten.\n:::\n'
927
+ // ...press Mod+8...
928
+ editor.getSource();
929
+ // ':::debunked Birds are government drones\nThe quickstart parrot has danced since section one and never once eaten.\n:::\n'
930
+ ```
931
+
932
+ The flip is one undoable edit, so undo un-debunks it, which is how conspiracies work anyway. And because the verdict lives in the bytes, a debunked conspiracy stays debunked across a reload.
933
+
934
+ One thing that surprises people pasting this into a fresh app: `theme="light"` flips only the editor-owned colors, so the page still looks dark. The built-in light chrome wants the `aragonite-editor-theme` class on a wrapper element; [consumer-guide.md](consumer-guide.md)'s theming section explains the two tiers.
935
+
936
+ Want a collapse toggle? Give `reservedChrome` an `isCollapsed` probe over the node, and every focus walk, merge, and windowing decision (a collapsed body stays unmounted) reads that one declaration. Add `expandPatch` beside it, returning the metadata patch that opens the node, and a reveal into the collapsed body (a table-of-contents entry, a search match) opens the container first and commits it as one undoable edit. Without it, such a reveal has nowhere to land and reports that it didn't.
937
+
938
+ ![A conspiracy, debunked on camera](./plugin-guide/conspiracy.gif)
939
+
940
+ ## The closure block
941
+
942
+ `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`:
943
+
944
+ - `{ mode: 'implemented', via }`: a real mechanism you can name (a `rebuildRaw`, a keymap command, `measurePartialRects`).
945
+ - `{ mode: 'inherit-default' }`: the generic editor behaviour, nothing kind-specific.
946
+ - `{ mode: 'not-supported', reason }`: the subsystem is structurally absent, so name the degradation.
947
+
948
+ 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:
949
+
950
+ 1. A container must declare `roundTrip: implemented`; its `rebuildRaw` is the mechanism.
951
+ 2. A `not-mergeable` kind can't declare `mergeBackspace: inherit-default`; it has no default merge to inherit.
952
+ 3. A cell claiming the focus-then-delete model must be backed by `blockFocus: 'whole-block'`.
953
+ 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.
954
+
955
+ Those four plus the nine columns are the whole contract.
956
+
957
+ **Name a mechanism your own kind carries.** `implemented` needs a `via` you can point at: your component, your `rebuildRaw`, your test. Never an internal editor mechanism you don't own. A cell you can't name honestly is `inherit-default` or `not-supported`, never an invented capability.
958
+
959
+ **Simple leaves: `simpleLeafClosure`.** A not-mergeable, childless, source-editable leaf built on `createEditableLeaf` answers five columns the same way every such leaf does: its round-trip inherits the default serialize, its `not-mergeable` merge is a focus move, its selection paints through `measurePartialRects`, it reorders by whole-block drag, and its clipboard is a byte slice. `simpleLeafClosure` bakes those five and asks only for the four your component actually determines, which are `focus`, `searchPaint`, `undo`, `simOracle`:
960
+
961
+ ```ts
962
+ closure: simpleLeafClosure({
963
+ focus: { mode: 'implemented', via: 'createEditableLeaf render-primary reveal' },
964
+ searchPaint: {
965
+ mode: 'implemented',
966
+ via: 'source raw scanned; the rendered view carries no measurable text, so a match is counted but not painted'
967
+ },
968
+ undo: { mode: 'implemented', via: 'render-primary: the reveal, edit, blur cycle commits one undo entry' },
969
+ simOracle: { mode: 'implemented', via: 'my-kind e2e' }
970
+ });
971
+ ```
972
+
973
+ Omitting one of the four is a compile error, and a baked column stays overridable (a render-primary leaf scoping its `selectionPaint` to the revealed state, say). What the preset fills in for you:
974
+
975
+ ```ts
976
+ simpleLeafClosure({ focus, searchPaint, undo, simOracle });
977
+ // returns those four plus the five baked cells:
978
+ // roundTrip: { mode: 'inherit-default' }
979
+ // mergeBackspace: implemented (not-mergeable: Backspace at the edge moves focus, never concatenates)
980
+ // selectionPaint: implemented (measurePartialRects, raw offsets)
981
+ // reorder: implemented (whole-block drag reorder through the parent BlockList)
982
+ // clipboard: { mode: 'inherit-default' }
983
+ ```
984
+
985
+ **`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.
986
+
987
+ **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:
988
+
989
+ ```ts
990
+ closure: containerClosure({
991
+ roundTripVia: 'container contract=opaque, rebuildConspiracyRaw',
992
+ focus: { mode: 'implemented', via: 'focus walks to the title chrome / first body child' },
993
+ mergeBackspace: { mode: 'implemented', via: 'mergeRole=container + unwrapRole' },
994
+ undo: {
995
+ mode: 'implemented',
996
+ via: 'updateMetadata; the verdict flip commits as one undo entry'
997
+ },
998
+ // The conspiracy declares reservedChrome, so coherence rule four refuses the baked
999
+ // clipboard cell; a container without reserved chrome just leaves this out.
1000
+ clipboard: {
1001
+ mode: 'implemented',
1002
+ via: 'byte-slice copy; a slice touching the title re-emits the conspiracy around the collected body'
1003
+ },
1004
+ simOracle: { mode: 'inherit-default' }
1005
+ });
1006
+ ```
1007
+
1008
+ 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.
1009
+
1010
+ ## Teaching the parser
1011
+
1012
+ The parrot opener at the top of this guide left two numbers unexplained (`priority: 25` and `consumed: 1`), and skipped two questions every real grammar eventually meets: how an opener knows where in the document it is, and how a construct whose lines must sit adjacent ever gets typed. This section is all four.
1013
+
1014
+ ### What an opener returns
1015
+
1016
+ `tryOpen` returns `null` to decline, or a `BlockOpenerResult`: the node it built plus `consumed`, the number of lines it claimed starting at `ctx.index`. It's a count, not a position. A single-line block returns `consumed: 1`; an opener that scanned forward to a closing line at `closeIdx` returns `closeIdx + 1 - ctx.index`.
1017
+
1018
+ Here's what the parrot's `tryOpen` sees for the third line of `'# Hi\n\n%%parrot party responsibly\n'`, and what it hands back:
1019
+
1020
+ ```ts
1021
+ tryOpen(ctx) {
1022
+ ctx.index; // 2, the line's position in ctx.lines
1023
+ ctx.line; // { raw: '%%parrot party responsibly\n', text: '%%parrot party responsibly', lineEnding: '\n', start: 6, end: 33 } (byte offsets into the source)
1024
+ ctx.leadingTrivia; // '\n', the blank line above, which the node keeps
1025
+ ctx.depth; // 0 at the document root; a blockquote body would be 1
1026
+ ctx.isDocumentParse; // true, this parse was handed a whole document
1027
+ return { node: { kind: parrot, leadingTrivia: ctx.leadingTrivia, raw: ctx.line.raw }, consumed: 1 };
1028
+ }
1029
+ ```
1030
+
1031
+ `consumed` must be at least 1. Claiming nothing is the one return that could spin the parse loop forever, so the parser declines it in every build and warns in dev ([Misuse outcomes](#misuse-outcomes)).
1032
+
1033
+ 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.
1034
+
1035
+ > **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 }`.
1036
+
1037
+ ### Opener priority
1038
+
1039
+ 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):
1040
+
1041
+ | Priority | Built-in kind |
1042
+ | -------: | ------------------------- |
1043
+ | 10 | `fencedCode` |
1044
+ | 20 | `heading` |
1045
+ | 30 | `thematicBreak` |
1046
+ | 40 | `blockquote` |
1047
+ | 50 | `list` |
1048
+ | 60 | `indentedCode` |
1049
+ | 70 | `htmlBlock` |
1050
+ | 80 | `linkReferenceDefinition` |
1051
+
1052
+ Two rules place a plugin opener on it:
1053
+
1054
+ 1. **Price _below_ a built-in whose matcher is a superset of yours.** `fencedCode` accepts every fence, ` ```mermaid ` included, so the Mermaid opener must win first: `OPENER_PRIORITIES.fencedCode - 5`. If a built-in would also match your syntax, you sit ahead of it or it claims the block.
1055
+ 2. **Otherwise slot into a gap between built-ins.** `<details>` is only ever an `htmlBlock`, so it prices into the gap just below: `OPENER_PRIORITIES.htmlBlock - 5`. Express the number as an offset from the built-in you reason about, never a bare literal.
1056
+
1057
+ 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.
1058
+
1059
+ **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.
1060
+
1061
+ For your pricing map: the opt-in `:::name` directive grammar registers its container opener at 45, between `blockquote` and `list`.
1062
+
1063
+ ### Openers and document position
1064
+
1065
+ `OpenContext.isDocumentParse` tells an opener whether the parse it's dispatching in was handed a whole document or one block's bytes:
1066
+
1067
+ - **`true`** for `parse(source)` (the default scope) and for the editor's load of the `source` prop.
1068
+ - **`false`** for every reparse the editor runs while you type, which pass `{ scope: 'fragment' }` (the scope union is exported as `ParseScope`): the content commit, split and merge, the clipboard parse, a container body.
1069
+
1070
+ Nothing else on the context answers the question, and one field reads like it does: `index === 0` says only that the block is first in the parse window, and a window starting mid-document has a first block too. A kind scoped to a document position (front matter, say) gates on the whole composition rather than the flag alone:
1071
+
1072
+ ```ts
1073
+ tryOpen(ctx) {
1074
+ if (!ctx.isDocumentParse || ctx.index !== 0 || ctx.depth !== 0 || ctx.leadingTrivia !== '')
1075
+ return null;
1076
+ // ... your syntax
1077
+ }
1078
+ ```
1079
+
1080
+ Three habits complete the gate:
1081
+
1082
+ - **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'`.
1083
+ - **Declare `interruptsParagraph: false`**: a line that interrupts a paragraph has a paragraph before it, so it's never at line 0.
1084
+ - **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.
1085
+
1086
+ The residual, stated plainly. A fragment edit that should dissolve the kind does dissolve it: break the closing fence and the block becomes whatever blocks its bytes now warrant. Restoring those bytes doesn't put the kind back in the live tree, because nothing reparses across a block boundary after a commit. `getSource()` returns the correct bytes and a reload restores the block. That limit isn't specific to position-scoped kinds (it's the general case of two blocks whose bytes jointly reparse as one), and it has a sibling: typing the syntax at the document top also needs a reload before the kind appears, since the commit reparse sees one block's bytes and declines by design.
1087
+
1088
+ ### Typing a multi-line construct into existence
1089
+
1090
+ An opener recognizes syntax that's already there. A grammar whose lines must be **adjacent** (a table's header over its delimiter, a `$$` fence over its closer) can never get there by typing at all, because Enter splits a paragraph into a blank-line-separated pair, and two adjacent prose lines would just re-parse as one paragraph. `registerBlockCompleter` closes that gap: your completer reads the one line the user typed and answers the canonical lines that complete it.
1091
+
1092
+ ```ts
1093
+ registerBlockCompleter(myKind, {
1094
+ tryComplete: (line) =>
1095
+ line.trim() === '$$'
1096
+ ? { lines: ['$$', '', '$$'], caret: { path: [], line: 1, column: 0 } }
1097
+ : null
1098
+ });
1099
+ ```
1100
+
1101
+ 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.
1102
+
1103
+ 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.
1104
+
1105
+ 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.
1106
+
1107
+ Two bounds worth knowing:
1108
+
1109
+ - Your lines are re-parsed by the ordinary parser, so a completer can only create what a reload of those bytes would produce, which means registering the opener that recognizes them first.
1110
+ - A completer sees a line, never a position, so a grammar that's only legal at one place in the document isn't a completion candidate.
1111
+
1112
+ ## Editable-content tiers
1113
+
1114
+ Content that's _itself editable_ comes in four tiers, and each one is backed by a guarantee in the tree rather than by a promise in a doc:
1115
+
1116
+ | Tier | What it hosts | Status |
1117
+ | ----------------- | -------------------------------------------------------------------------------- | ---------------------- |
1118
+ | **Container** | Real document blocks in a nested child list; the walkthrough's body | shipped |
1119
+ | **Chrome leaf** | One reserved, single-line, plain-text child whose bytes the container's raw owns | shipped |
1120
+ | **Editable leaf** | A standalone text surface with native caret/IME/undo/selection/clipboard parity | shipped _(pre-freeze)_ |
1121
+ | **Atomic widget** | An opaque, non-text embed, which the caret can address only at its edges | shipped |
1122
+
1123
+ The chrome leaf is deliberately narrow, and each limit is a guarantee its container can lean on:
1124
+
1125
+ - **Always present**: a destructive range clears it rather than deleting it.
1126
+ - **Single-line and unsplittable**: paste into it flattens to inline.
1127
+ - **Kind-stable**: it stays the same kind through every edit.
1128
+
1129
+ The contract guarantees the empty leaf's presence, not its look. An empty-state affordance (placeholder text over an untitled title, say) is yours to build with CSS on the leaf's block class.
1130
+
1131
+ **Declare `gapEdges` for every kind: it names the edges where your surface traps the caret.** A grid, a fence or an opaque embed leaves the boundary it shares with a neighbour unreachable: no caret can sit there, so no paragraph can be typed between two of them. The field's values:
1132
+
1133
+ - `'before'` / `'after'` / `'both'` opens the named edges to a between-blocks caret, where typing or Enter inserts a paragraph.
1134
+ - `'none'` says your surface already hosts insertion at both edges.
1135
+
1136
+ The field is required so the no-gap answer is a decision you wrote down, never an omission. The bundled kinds set the precedent: an opaque container whose fences leave no textual way out (the callouts, details, the generic directive container) declares `'both'`; a container whose own edges are prose the caret can already stand on (a blockquote's marked lines, a list item's) declares `'none'`.
1137
+
1138
+ Nested-editor interiors (a second editor whose state serializes as an opaque blob) are **rejected permanently**. They break the lossless round-trip. There's no version of this that gets built (and yes, people ask).
1139
+
1140
+ ### The editable leaf
1141
+
1142
+ `createEditableLeaf` is the container factory's sibling for leaves. It reads the editor's contexts itself (its deps are the same live thunks: `getNode`, `getIndex`, `getPath`, plus `getEl()` returning your source contenteditable) and hands back everything a text-editing block needs.
1143
+
1144
+ ```ts
1145
+ const leaf = createEditableLeaf({
1146
+ getNode: () => node,
1147
+ getIndex: () => index,
1148
+ getPath: () => myPath,
1149
+ getEl: () => sourceEl ?? null, // null while a render-primary view is folded
1150
+ mode: 'render-primary', // 'plain' is the default
1151
+ singleLine: true, // a one-line kind: Enter splits the block instead of typing a newline
1152
+ isRevealed: () => revealed, // render-primary only: you own the swap flag
1153
+ setRevealed: (next) => (revealed = next)
1154
+ });
1155
+ leaf.sourceText; // the block's raw minus its trailing line ending
1156
+ leaf.getPresentationMode(); // 'source'
1157
+ leaf.getOptions(); // this editor's options for your plugin, typed unknown
1158
+ ```
1159
+
1160
+ **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.
1161
+
1162
+ **One spread wires the source surface.** Write `<div {...leaf.surfaceProps}>` on your source contenteditable and the nine 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 as a **single text node** (so `textContent === source` and the walk that maps DOM positions to byte offsets stays exact), and focus is parked on the editor root when the source unmounts.
1163
+
1164
+ That single text node 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.
1165
+
1166
+ 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.
1167
+
1168
+ 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:
1169
+
1170
+ - **`'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.
1171
+ - **`'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.
1172
+
1173
+ **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.
1174
+
1175
+ **Commit semantics.** A commit parses the edited text and lands it through the editor's own edit ladder:
1176
+
1177
+ ```
1178
+ commit(edited text) ── parse ──▶ same kind? update in place, caret preserved
1179
+ different kind? remount the block
1180
+ several blocks? structural replace: the leaf becomes
1181
+ all of them, the caret following the
1182
+ edit position into its block
1183
+ ```
1184
+
1185
+ 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.
1186
+
1187
+ **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.
1188
+
1189
+ 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`.
1190
+
1191
+ ## Presentation modes
1192
+
1193
+ **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:
1194
+
1195
+ | Mode | Editing | What shows |
1196
+ | ---------------- | ------- | --------------------------------------------- |
1197
+ | `source` | live | every marker, dimmed |
1198
+ | `reading` | none | no markers, no reveals |
1199
+ | `preview-block` | live | markers only in the focused block |
1200
+ | `preview-inline` | live | syntax only for the construct under the caret |
1201
+ | `live` | live | no markers anywhere, nothing revealed |
1202
+
1203
+ 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.
1204
+
1205
+ How each tier reads it:
1206
+
1207
+ | Tier | Mode read |
1208
+ | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1209
+ | Plugin instance logic | `editor.presentationMode` on your `EditorContext` (a live getter); subscribe to the `presentationModeChange` event for flips |
1210
+ | Editable leaf | `leaf.getPresentationMode()` on the `createEditableLeaf` surface |
1211
+ | Container block (factory) | `container.getPresentationMode()` on the `createContainerBlock` surface; the live effective mode, mirroring the leaf getter, and the preferred path when the factory is in hand |
1212
+ | Inline widget (rendering) | The `getPresentationMode` prop your component is mounted with, a **live getter** beside the frozen `{ inline, source }` snapshot |
1213
+ | Inline widget (editing) | `ctx.presentationMode` on the `InlineWidgetEditingContext` your `onSelectedKey` handler receives ([Inline kinds](#inline-kinds)) |
1214
+ | Block component (DOM tier) | The `data-presentation` attribute on the editor root (`el.closest('[data-presentation]')`); **absent means `'source'`**. The fallback for a component holding only a DOM handle, and a **point-in-time** read: correct in a gesture handler or at initial render, but a live flip does not re-render a mounted block through it (the "What is live, what is point-in-time" subsection below) |
1215
+
1216
+ ```ts
1217
+ container.getPresentationMode(); // 'reading'
1218
+ leaf.getPresentationMode(); // 'reading'
1219
+ el.closest('[data-presentation]')?.getAttribute('data-presentation') ?? 'source'; // 'reading' (the attribute is absent in source mode)
1220
+ ```
1221
+
1222
+ In `reading` mode the platform does most of it for you, which is why most plugins need no mode code at all:
1223
+
1224
+ - your editable leaf never reveals and never commits;
1225
+ - chord dispatch (block commands, global commands, keymaps) is swallowed at the dispatcher;
1226
+ - the container factory gates whole-block Enter/Backspace/reorder;
1227
+ - marker spans hide by CSS.
1228
+
1229
+ 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.
1230
+
1231
+ `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.
1232
+
1233
+ `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:
1234
+
1235
+ - **A registered inline widget** (component or `buildWidget`) keeps its editing policy exactly as in every other live mode: `revealSource` opens the source on caret entry, and selection/delete semantics are untouched. The caret-proximity marker reveal covers the built-in marker-wrapped kinds (emphasis, strong, strikethrough, inline code, links, image alt syntax), not widgets, because a widget already has its own reveal or select behavior.
1236
+ - **A recognized but unwidgeted inline kind** renders as its raw source text with no marker spans, so preview-inline neither hides nor reveals anything for it; it looks the same as in source mode. If you want rendered-until-touched behavior for your inline kind, register it as a widget with `revealSource`.
1237
+
1238
+ `live` asks nothing new of you: render for it as you render for `preview-block` (hide your own source chrome) and edit in it as you edit in source.
1239
+
1240
+ ### What is live, what is point-in-time
1241
+
1242
+ Reactivity is **per tier, not universal**, and that's worth being upfront about.
1243
+
1244
+ **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.
1245
+
1246
+ **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.
1247
+
1248
+ **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.
1249
+
1250
+ **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".
1251
+
1252
+ **Never snapshot a live tier, and check the mode you handle.** Read a live getter each time; a value captured at mount is stale by construction. And gate on the specific mode you render for (`=== 'reading'` for a reading affordance), never a `'source'` check you invert: `preview-block` is a live editing mode, so a reading-style inertness gate must not fire in it, and a future mode then degrades to your default rendering instead of disappearing.
1253
+
1254
+ ## Recipe: a render-primary block
1255
+
1256
+ Some blocks aren't text at all: a diagram, a chart, an embed, content that renders as a picture and is edited through its own UI rather than through the editor's caret. The Mermaid reference plugin is the worked example, and the shape generalizes:
1257
+
1258
+ ```
1259
+ fence claim ──▶ opaque container, NO children ──▶ component renders the diagram
1260
+ code lives in metadata edit UI is plugin-owned
1261
+ rebuildRaw re-emits the fence commits ride updateOwnMetadata
1262
+ ```
1263
+
1264
+ - **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.
1265
+ - **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.
1266
+ - **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.
1267
+ - **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.
1268
+ - **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.
1269
+ - **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.
1270
+ - **View-state commands reach the component through `ctx.hooks`.** See [Block commands](#block-commands).
1271
+
1272
+ The two helpers from that list, with what they hand back:
1273
+
1274
+ `````ts
1275
+ matchFenceOpen('```mermaid'); // { marker: '`', length: 3, info: 'mermaid', indent: '', infoRaw: 'mermaid' }
1276
+ matchFenceOpen(' ~~~ js title'); // { marker: '~', length: 3, info: 'js title', indent: ' ', infoRaw: ' js title' }
1277
+ matchFenceOpen('hello'); // null, so hand the line back
1278
+ matchFenceClose('```` ', '`', 3); // true: a longer run with trailing space still closes a three-backtick fence
1279
+
1280
+ const render = createBoundedMemo<string, Promise<SVGElement>>({ cap: 32 });
1281
+ render(`${theme}\0${code}`, () => engine.render(code, theme)); // computes once per key; past 32 entries the least recently used one goes
1282
+ `````
1283
+
1284
+ **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.
1285
+
1286
+ ### Whole-block focus
1287
+
1288
+ 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.
1289
+
1290
+ The mechanics behind that, each with its gotcha:
1291
+
1292
+ - **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.
1293
+ - **Give your box `position: relative`**, or the host resolves against whatever ancestor happens to be positioned.
1294
+ - **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.
1295
+ - **An editable declared surface keeps focus for itself** (your edit `<textarea>`), which owns its caret and IME already.
1296
+
1297
+ 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.
1298
+
1299
+ ### What you owe the surface you own
1300
+
1301
+ **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.
1302
+
1303
+ **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.
1304
+
1305
+ 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.
1306
+
1307
+ ## Recipe: reading the document above your block
1308
+
1309
+ A block component gets its own node, which is fine right up until it isn't: a table-of-contents block needs the headings above it, and a cross-reference needs an id defined somewhere else entirely. `BlockComponentProps.document` delivers the read-only root document to every component, at any nesting depth:
1310
+
1311
+ ```svelte
1312
+ <script lang="ts">
1313
+ import { getContentRange, type DocumentView } from '@voithos-labs/aragonite/plugin';
1314
+
1315
+ // A component receives its own node too; this block needs only the document.
1316
+ let { document }: { document?: DocumentView } = $props();
1317
+
1318
+ // A $derived over the prop subscribes to the CST proxy, so editing a heading
1319
+ // above re-runs this and the list updates live.
1320
+ const headings = $derived(
1321
+ (document?.children ?? [])
1322
+ .filter((b) => b.kind === 'heading' || b.kind === 'setextHeading')
1323
+ .map((b) => {
1324
+ const { start, end } = getContentRange(b); // drop the `#` / underline markers
1325
+ return b.raw.slice(start, end);
1326
+ })
1327
+ );
1328
+ </script>
1329
+
1330
+ <nav>
1331
+ {#each headings as text}<div>{text}</div>{/each}
1332
+ </nav>
1333
+ ```
1334
+
1335
+ `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.
1336
+
1337
+ ```ts
1338
+ getContentRange(parse('# Hi\n').children[0]); // { start: 2, end: 4 }: the two bytes of 'Hi', markers skipped
1339
+ getContentRange(parse('plain text\n').children[0]); // { start: 0, end: 10 }: a paragraph has no markers to skip
1340
+ await rects.navigateTo([4]); // true once the block at [4] is in view with the caret at its start
1341
+ ```
1342
+
1343
+ 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.
1344
+
1345
+ ## Inline kinds
1346
+
1347
+ Blocks are only half the story. An inline kind takes three calls, mirroring the block tier's declare, describe, recognize:
1348
+
1349
+ - **`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.
1350
+ - **`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.
1351
+ - **`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.
1352
+
1353
+ The three together, for a `:shortcode:` kind on a trigger nothing else claims:
1354
+
1355
+ ```ts
1356
+ const shortcode = declarePluginInlineKind('shortcode'); // 'shortcode', branded
1357
+ registerInlineSyntax(':', recognizeShortcode); // a bare trigger; the priority defaults to INLINE_PRIORITIES.plugin (100)
1358
+ registerInlineWidgetKind(shortcode, {
1359
+ isWidget: (node) => node.kind === shortcode,
1360
+ component: ShortcodeWidget,
1361
+ editing: { deleteGranularity: 'atomic', onEdge: 'step-over' }
1362
+ });
1363
+ ```
1364
+
1365
+ A widget renders through one of two paths, and the descriptor rejects declaring both:
1366
+
1367
+ - **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.
1368
+ - **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.
1369
+
1370
+ **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.
1371
+
1372
+ - `getPresentationMode`: the effective presentation mode.
1373
+ - `getDocument`: the read-only root document.
1374
+ - `getContentVersion`: a number that changes whenever the document's bytes change, and is stable otherwise.
1375
+
1376
+ 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.
1377
+
1378
+ 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.
1379
+
1380
+ 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.
1381
+
1382
+ **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.
1383
+
1384
+ 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.
1385
+
1386
+ **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 }`):
1387
+
1388
+ ```ts
1389
+ registerInlineSyntax('[', recognizeFootnote, {
1390
+ prefix: '[^',
1391
+ priority: INLINE_PRIORITIES.prefixOverride
1392
+ });
1393
+ ```
1394
+
1395
+ 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.
1396
+
1397
+ **`!` 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.
1398
+
1399
+ 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.
1400
+
1401
+ **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:
1402
+
1403
+ ```ts
1404
+ const dollarAt = createScanIndex((raw) => {
1405
+ const hits: number[] = [];
1406
+ for (let i = 0; i < raw.length; i++) if (raw[i] === '$') hits.push(i);
1407
+ return Int32Array.from(hits);
1408
+ });
1409
+ dollarAt('pay $HOME $5 for $x$', 5); // 10, the first candidate at or after offset 5
1410
+ dollarAt('pay $HOME $5 for $x$', 20); // -1, none left
1411
+ ```
1412
+
1413
+ 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.
1414
+
1415
+ **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:
1416
+
1417
+ ```ts
1418
+ registerInlineSyntax('!', recognizeEmbed, {
1419
+ prefix: '![[',
1420
+ priority: INLINE_PRIORITIES.prefixOverride,
1421
+ rewriteImage: (source, fields) => {
1422
+ if (!source.startsWith('![[')) return null; // bytes this rung did not shape
1423
+ // Decline what this grammar cannot store rather than dropping it silently: it
1424
+ // holds a target and an optional width and nothing else. The alt line is THIS
1425
+ // recognizer's version of that rule: it fills alt and url from the one target,
1426
+ // so an alt that no longer matches is an edit with no form here. Write yours
1427
+ // against however your own recognizer fills the node.
1428
+ if (fields.title !== undefined || fields.label !== undefined) return null;
1429
+ if (fields.alt !== fields.url) return null;
1430
+ return `![[${fields.url}${fields.width !== undefined ? `|${fields.width}` : ''}]]`;
1431
+ }
1432
+ });
1433
+ ```
1434
+
1435
+ `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.
1436
+
1437
+ Three edges the snippet above is shaped by, and each one bites if you drop it:
1438
+
1439
+ - **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.
1440
+ - **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.
1441
+ - **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.
1442
+
1443
+ **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.
1444
+
1445
+ **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:
1446
+
1447
+ | Field | What it decides |
1448
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------- |
1449
+ | `revealSource` | Open the source (the `$…$` bytes) for editing on caret entry; inline math's model |
1450
+ | `onSelectedKey` | A handler for keys while the widget is selected; image resize rides it |
1451
+ | `onEdge` | `'select' \| 'step-over'`: an edge press selects the whole widget, or steps transparently over it |
1452
+ | `deleteGranularity` | `'atomic' \| 'select-then-delete'`: one press deletes the whole widget, or the first press selects and the second deletes |
1453
+ | `claimsActivationClick` | Your component handles the activation click itself, so the surface's reveal stands down for it; the footnote jump's model |
1454
+
1455
+ 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.
1456
+
1457
+ ## Decorations
1458
+
1459
+ Everything so far teaches the editor content you **own**: a kind, its grammar, its component. A decoration is the other half of the story, for annotating content you **don't own**. Highlight every occurrence of a word, ghost-complete a sentence, fold a range, badge a heading. Decorations are view-only: they never enter the document tree, never change `getSource()`, and never touch undo.
1460
+
1461
+ You register a **source** on each editor instance, from `onEditor`:
1462
+
1463
+ ```ts
1464
+ setup(ctx) {
1465
+ ctx.onEditor((editor) => {
1466
+ const handle = editor.decorations.addSource({
1467
+ name: 'my-highlights', // unique per instance; a duplicate throws
1468
+ provide: (doc) => scanForMarks(doc) // pure: document in, decorations out
1469
+ });
1470
+ return () => handle.dispose();
1471
+ });
1472
+ }
1473
+ ```
1474
+
1475
+ `provide` receives the document as a `DocumentView` ([Views](#views-what-you-read-what-you-own)) and is **pure over it plus your own state**. The editor re-runs it after every document edit, and the render layer applies whatever it returns. There's no decoration set to map forward through changes: positions are `(path, offset)` addresses into the current tree, recomputed each run. When your _own_ state changes instead (an option toggled, the selection moved, an async result arrived), call `handle.invalidate()` to re-run just your source.
1476
+
1477
+ **Two contracts to build against:**
1478
+
1479
+ - **`invalidate()` is synchronous.** Your new decorations are applied before the call returns, so an event handler can invalidate and immediately trust the view. The exception is an `edit` handler, which the editor calls mid-commit: an invalidate from there waits for the commit to publish, so your source reads a finished document rather than a half-applied one. Several of them in one commit are one re-run.
1480
+ - **Widget identity is untracked.** The renderer compares decorations by position and class, not by widget object, so swapping in a new `component` or `buildDom` at the same position with the same class re-renders nothing. Vary `class` when the widget's content changes.
1481
+
1482
+ ### The four decoration types
1483
+
1484
+ | Type | Shape | Renders as |
1485
+ | --------- | -------------------------------------------------------- | ------------------------------------------------------------------------------ |
1486
+ | `mark` | `{ type: 'mark', path, start, end, class }` | A positioned overlay span over the inline range; style it via the class |
1487
+ | `widget` | `{ type: 'widget', path, offset, widget }` | A zero-width atomic island at the offset (ghost text's shape) |
1488
+ | `replace` | `{ type: 'replace', path, start, end, widget?, class? }` | An atomic island covering the range; the hidden bytes stay in the document |
1489
+ | `block` | `{ type: 'block', path, class?, attrs?, badge? }` | A class/attrs treatment on the whole block host, plus an optional badge widget |
1490
+
1491
+ One `provide` answer using two of them, shapes side by side:
1492
+
1493
+ ```ts
1494
+ provide: (doc) => [
1495
+ { type: 'mark', path: [2], start: 4, end: 9, class: 'stale-link' }, // bytes 4..9 of block [2]
1496
+ { type: 'block', path: [0], class: 'pinned', badge: { buildDom: () => pinIcon() } }
1497
+ ];
1498
+ ```
1499
+
1500
+ 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`.
1501
+
1502
+ 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.
1503
+
1504
+ ### Recipe: memoize the scan on `editEpoch`
1505
+
1506
+ `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.
1507
+
1508
+ ```ts
1509
+ let lastEpoch = -1;
1510
+ let index = new Map<string, MarkDecoration[]>(); // word → its occurrence marks
1511
+ let caret: EditorSelection | null = null;
1512
+
1513
+ const handle = editor.decorations.addSource({
1514
+ name: 'occurrences',
1515
+ provide: (doc, { editEpoch }) => {
1516
+ if (editEpoch !== lastEpoch) {
1517
+ lastEpoch = editEpoch;
1518
+ index = buildWordIndex(doc); // one whole-document walk per edit
1519
+ }
1520
+ const word = wordUnderCaret(doc, caret);
1521
+ return word ? (index.get(word) ?? []) : []; // one map read per invalidate()
1522
+ }
1523
+ });
1524
+
1525
+ editor.events.on('selectionChange', (sel) => {
1526
+ caret = sel; // the source's own state, read on the next invalidate
1527
+ handle.invalidate();
1528
+ });
1529
+ ```
1530
+
1531
+ 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.
1532
+
1533
+ 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.
1534
+
1535
+ 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.
1536
+
1537
+ ```ts
1538
+ editor.rects.rangeRects([2], 4, 9); // [DOMRect { x: 96, y: 412, width: 38, height: 22, ... }], one per visual line the range crosses
1539
+ ```
1540
+
1541
+ ## Block commands
1542
+
1543
+ **`registerBlockCommand(kind, name, handler)`**
1544
+
1545
+ 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.
1546
+
1547
+ ```ts
1548
+ const setVerdict = registerBlockCommand(conspiracy, 'conspiracy.setVerdict', (ctx) => {
1549
+ if (typeof ctx.arg !== 'string') return false; // the binding's arg arrives as unknown
1550
+ ctx.updateMetadata({ name: ctx.arg }); // one undoable commit, through the kind's rebuildRaw
1551
+ return true;
1552
+ });
1553
+ setVerdict; // 'conspiracy.setVerdict', branded as a command id
1554
+ // where it goes, in the descriptor: keymap: [{ chord: 'Mod+8', command: setVerdict, arg: 'debunked' }]
1555
+ registerBlockCommand(conspiracy, 'conspiracy.setVerdict', handler); // throws: already registered
1556
+ ```
1557
+
1558
+ A minted command dispatches on the two tiers that can hand it a `BlockCommandContext` (the focused node plus a metadata-commit route):
1559
+
1560
+ - the **editable-leaf tier**, a `createEditableLeaf` block, resolved from the focused leaf's keymap;
1561
+ - the **container-bubble tier**, a container-factory block, resolved as a chord bubbles up from an inner leaf.
1562
+
1563
+ 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.
1564
+
1565
+ 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).
1566
+
1567
+ **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.
1568
+
1569
+ 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.
1570
+
1571
+ **`registerGlobalCommand(name, handler, { chord })`**
1572
+
1573
+ 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`:
1574
+
1575
+ ```ts
1576
+ setup(ctx) {
1577
+ registerGlobalCommand(
1578
+ 'wordCount.log',
1579
+ (editor) => {
1580
+ // The mint is not generic-bound: the handler gets EditorContext<unknown>,
1581
+ // so narrow options here (onEditor's callback is where they read typed).
1582
+ const opts = editor.options as WordCountOptions | undefined;
1583
+ console.log(`[${editor.editorId}]`, countByEditor.get(editor.editorId), opts);
1584
+ return true; // handled
1585
+ },
1586
+ { chord: 'Mod+Shift+L' }
1587
+ );
1588
+ ctx.onEditor(/* … */);
1589
+ }
1590
+ ```
1591
+
1592
+ 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:
1593
+
1594
+ - 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**.
1595
+ - 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.
1596
+ - A handler throw is contained identically, surfacing as an `error` of origin `command` attributed to the owning plugin.
1597
+
1598
+ ```ts
1599
+ registerGlobalCommand('wordCount.log', handler, { chord: 'Mod+Shift+L' }); // 'wordCount.log', branded as a command id
1600
+ registerGlobalCommand('mine.find', handler, { chord: 'Mod+F' }); // throws: reserved by the editor UI (search)
1601
+ registerGlobalCommand('mine.undo', handler, { chord: 'Mod+Z' }); // throws: already bound to "history.undo"
1602
+ registerGlobalCommand('mine.bold', handler, { chord: 'Mod+B' }); // fine: fires on a thematic break, yields to bold in a paragraph
1603
+ ```
1604
+
1605
+ 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.
1606
+
1607
+ ## Paste transforms
1608
+
1609
+ `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.
1610
+
1611
+ ```ts
1612
+ registerPasteTransform({
1613
+ name: 'shout',
1614
+ transform: (text) => (text.includes('!!') ? text.toUpperCase() : null)
1615
+ });
1616
+ // pasting 'wow!! ok' inserts 'WOW!! OK'
1617
+ // pasting 'calm' inserts 'calm': the transform declined, so the text went through untouched
1618
+ registerPasteTransform({ name: 'shout', transform: () => null }); // throws: "shout" is already registered
1619
+ ```
1620
+
1621
+ Two habits keep a transform sound:
1622
+
1623
+ - **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.
1624
+ - **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.
1625
+
1626
+ 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.
1627
+
1628
+ ## Recipe: a kind only a menu creates
1629
+
1630
+ Some kinds shouldn't be typeable: a chart card, a survey embed, a citation block whose author picks it from a menu rather than remembering syntax. The move is **not** to register a kind with no grammar, and this is the anti-pattern worth naming out loud. The saved file is just bytes, and on reload the parser is the only way back in, so a kind whose bytes no opener can recognize survives exactly until the document is saved and reloaded. On the way back in, the parser sees prose.
1631
+
1632
+ Own a `:::name` directive instead. The grammar is real, so the bytes reload as your kind, and the syntax is implausible to arrive at by typing: it needs three colons, a name, a body and a `:::` terminator, and nothing along the way paints a half-formed block. That's as close to "not typeable" as an editor that reloads from bytes can honestly get, and it costs you nothing, since the [directive walkthrough](#walkthrough-a-conspiracy-container-end-to-end) is the same registration you'd have written anyway.
1633
+
1634
+ Creation then comes from the host's own UI. The consumer's `editor.insertMarkdown(md)` inserts your kind's canonical bytes at the caret exactly as pasting them would, so a menu entry is:
1635
+
1636
+ ```ts
1637
+ editor.insertMarkdown(':::chart\ntype: bar\n:::\n');
1638
+ ```
1639
+
1640
+ That snippet is the whole integration. Bytes are the API, and a new kind adds no method for the host to adopt. The call answers `true` when the caret took the paste and `false` with no caret (reading mode included). Two notes for the recipe:
1641
+
1642
+ - **Document your canonical snippet** beside the kind. The host pastes bytes; give it the exact bytes your `rebuildRaw` would produce, so the first insertion is already canonical.
1643
+ - **The insertion is a paste**, so it carries the same placement decision, undo entry and caret landing a paste carries. A multi-line directive splices in as real blocks, which is what a block kind wants.
1644
+
1645
+ ## What a plugin may and may not do
1646
+
1647
+ The boundary, spelled out, because a plugin platform that leaves this implicit is one you shouldn't trust with somebody's notes.
1648
+
1649
+ **An editor plugin isn't an app plugin.** If you're extending an app that embeds aragonite, that app almost certainly has a plugin layer of its own, and the two own different halves:
1650
+
1651
+ | Layer | Owns |
1652
+ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
1653
+ | **aragonite plugins** | Anything touching the document or the editing surface: kinds, grammar, decorations, commands over the document, presentation |
1654
+ | **The embedding app's plugins** | Anything touching the app: ribbon, sidebar, status bar, settings tabs, modals, the command palette UI, the vault, sync |
1655
+
1656
+ Vault-wide indexing sits on the app's side of that line: aragonite hands you the raw material (`getEvents()` and `parse()`) and never the index. It's not simply "editor = view", though. Derived state over the _one_ document you're editing (a table of contents, footnote numbering, the tasks in this note) is an editor plugin's to build, which is why a block component is handed its document ([the document recipe](#recipe-reading-the-document-above-your-block)).
1657
+
1658
+ A plugin **may**:
1659
+
1660
+ - Register kinds, components, and openers, once; a duplicate throws.
1661
+ - Declare a `rebuildRaw` and have the editor invoke it when the document changes.
1662
+ - Build containers and chrome through the factories.
1663
+ - Store primitive per-node metadata, and commit metadata through the sanctioned update path.
1664
+ - Contribute per-kind keymaps over the command vocabulary.
1665
+ - Render as an unknown kind and degrade to a visible raw fallback.
1666
+ - Transform pasted plain text before it's parsed ([Paste transforms](#paste-transforms)).
1667
+
1668
+ A plugin **may not**:
1669
+
1670
+ - Treat its DOM as authoritative, or mutate the tree from the view layer. Boundary events flow up, and the tree always wins. Type-enforced since the readonly views: every plugin-visible node type is deep-readonly on its bytes ([Views](#views-what-you-read-what-you-own)).
1671
+ - Write bytes through a node reference captured before an edit. After any change, read the node back from the tree; the old reference is stale.
1672
+ - Pass reactive tree state by value across a module boundary. Hand it through a live read instead (a getter, or a `() =>` thunk as the factory deps take).
1673
+ - Invent merge-role, unwrap, or container-contract values. Those are closed sets.
1674
+ - Silently override a built-in or another plugin's registration.
1675
+ - Intercept loading or typing, or rewrite the whole document from a paste. The paste hook is **paste-scoped and pre-parse only**: it sees the clipboard text, never the load path or keystrokes. A whole-document migration belongs at the document level: read `getSource()`, transform the Markdown, and write the editor's `source` prop, which replaces the document in one step.
1676
+
1677
+ Most of that boundary is enforced by **shape**: the factories never hand you a raw context key or a mutation handle, so the disallowed move is simply unavailable. The rest is enforced by **dev-mode checks that are stripped from a production build**, so a plugin developed against a production build gets no signal whatsoever. **Develop against a dev build.**
1678
+
1679
+ ### Misuse outcomes
1680
+
1681
+ Why the dev build is where plugin development belongs, stated as what each mistake does in each build:
1682
+
1683
+ | Misuse | Dev build | Production build |
1684
+ | ----------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------- |
1685
+ | `rebuildRaw` writes the wrong bytes | Warns at edit time, naming the kind | Silent until the bytes surface in a round-trip |
1686
+ | A component throws while rendering | Contained as a failed-block fallback plus an `error` event, by path | Same containment (the boundary ships in production) |
1687
+ | An opener claims no line (`consumed < 1`) | Warns, naming the kind, and declines the opener | Declines the same way, silently; no hang |
1688
+ | An opener's `raw` ≠ the lines it consumed | Parse warns, naming the kind | Silent round-trip break |
1689
+ | An opener throws | Propagates uncaught (parse runs at init and on every edit) | Same; uncaught |
1690
+
1691
+ ## Where to go next
1692
+
1693
+ 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).