@voithos-labs/aragonite 0.10.2 → 0.10.4

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 (300) hide show
  1. package/README.md +6 -21
  2. package/THIRD-PARTY-NOTICES.md +25 -0
  3. package/dist/a11y-strings.d.ts +19 -0
  4. package/dist/a11y-strings.js +19 -0
  5. package/dist/action-contracts.d.ts +7 -1
  6. package/dist/ambient/ambient-dom.js +5 -1
  7. package/dist/block-component.d.ts +14 -0
  8. package/dist/components/BlockDragHandle.svelte +37 -24
  9. package/dist/components/BlockHost.svelte +18 -11
  10. package/dist/components/Editor.svelte +434 -198
  11. package/dist/components/Editor.svelte.d.ts +1 -1
  12. package/dist/components/SelectionOverlay.svelte +25 -17
  13. package/dist/components/SelectionOverlay.svelte.d.ts +1 -1
  14. package/dist/components/TailInsert.svelte +107 -0
  15. package/dist/components/TailInsert.svelte.d.ts +17 -0
  16. package/dist/components/block-content-selector.d.ts +6 -2
  17. package/dist/components/block-content-selector.js +6 -2
  18. package/dist/components/blocks/ThematicBreakBlock.svelte +19 -6
  19. package/dist/components/blocks/ThematicBreakBlock.svelte.d.ts +1 -0
  20. package/dist/components/blocks/code/CodeBlock.svelte +211 -30
  21. package/dist/components/blocks/code/CodeBlockRail.svelte +704 -0
  22. package/dist/components/blocks/code/CodeBlockRail.svelte.d.ts +26 -0
  23. package/dist/components/blocks/code/code-bootstrap.js +16 -0
  24. package/dist/components/blocks/code/code-context-actions.d.ts +1 -0
  25. package/dist/components/blocks/code/code-context-actions.js +24 -0
  26. package/dist/components/blocks/code/code-fence-exit.d.ts +15 -0
  27. package/dist/components/blocks/code/code-fence-exit.js +26 -0
  28. package/dist/components/blocks/code/code-languages.d.ts +6 -0
  29. package/dist/components/blocks/code/code-languages.js +22 -3
  30. package/dist/components/blocks/code/code-renderer.js +11 -0
  31. package/dist/components/blocks/directive/DirectiveContainerBlock.svelte +1 -1
  32. package/dist/components/blocks/editable-leaf.d.ts +37 -6
  33. package/dist/components/blocks/editable-leaf.js +248 -30
  34. package/dist/components/blocks/editable-surface.d.ts +9 -0
  35. package/dist/components/blocks/editable-surface.js +22 -3
  36. package/dist/components/blocks/list/ListBlock.svelte +1 -0
  37. package/dist/components/blocks/list/ListItemBlock.svelte +14 -5
  38. package/dist/components/blocks/list/ListItemBlock.svelte.d.ts +2 -0
  39. package/dist/components/blocks/list/task-checkbox.d.ts +2 -0
  40. package/dist/components/blocks/list/task-checkbox.js +11 -2
  41. package/dist/components/blocks/surface-wiring.svelte.d.ts +4 -0
  42. package/dist/components/blocks/surface-wiring.svelte.js +8 -1
  43. package/dist/components/blocks/table/TableActionMenu.svelte +210 -86
  44. package/dist/components/blocks/table/TableActionMenu.svelte.d.ts +5 -0
  45. package/dist/components/blocks/table/TableBlock.svelte +205 -186
  46. package/dist/components/blocks/table/TableBlock.svelte.d.ts +1 -0
  47. package/dist/components/blocks/table/TableCellBlock.svelte +87 -22
  48. package/dist/components/blocks/table/TableRowBlock.svelte +4 -18
  49. package/dist/components/blocks/table/TableRowBlock.svelte.d.ts +0 -5
  50. package/dist/components/blocks/table/cell-clipboard.d.ts +10 -0
  51. package/dist/components/blocks/table/cell-clipboard.js +34 -1
  52. package/dist/components/blocks/table/cell-keydown-plan.d.ts +1 -1
  53. package/dist/components/blocks/table/cell-keydown-plan.js +3 -1
  54. package/dist/components/blocks/table/table-cell-paste.js +2 -1
  55. package/dist/components/blocks/table/table-menu-model.d.ts +35 -8
  56. package/dist/components/blocks/table/table-menu-model.js +36 -17
  57. package/dist/components/blocks/text/TextEditableBlock.svelte +58 -11
  58. package/dist/components/blocks/text/click-snap-guard.d.ts +3 -0
  59. package/dist/components/blocks/text/click-snap-guard.js +8 -0
  60. package/dist/components/blocks/text/delimiter-autopair.d.ts +71 -0
  61. package/dist/components/blocks/text/delimiter-autopair.js +216 -0
  62. package/dist/components/blocks/text/edge-policy-dispatch.d.ts +4 -0
  63. package/dist/components/blocks/text/edge-policy-dispatch.js +89 -20
  64. package/dist/components/blocks/text/live-selection-edit.d.ts +5 -5
  65. package/dist/components/blocks/text/live-selection-edit.js +37 -7
  66. package/dist/components/blocks/text/text-clipboard.js +2 -2
  67. package/dist/components/blocks/text/text-keydown.d.ts +7 -1
  68. package/dist/components/blocks/text/text-keydown.js +11 -1
  69. package/dist/components/blocks/text/text-render.d.ts +1 -1
  70. package/dist/components/blocks/text/text-render.js +3 -1
  71. package/dist/components/blocks/text/widget-interaction.d.ts +3 -0
  72. package/dist/components/blocks/text/widget-interaction.js +181 -37
  73. package/dist/components/drag-handle.d.ts +41 -0
  74. package/dist/components/drag-handle.js +134 -0
  75. package/dist/components/editor-root-focus.d.ts +19 -0
  76. package/dist/components/editor-root-focus.js +67 -0
  77. package/dist/components/editor-root-geometry.d.ts +39 -0
  78. package/dist/components/editor-root-geometry.js +91 -0
  79. package/dist/components/editor-root-keydown.d.ts +1 -1
  80. package/dist/components/editor-root-keydown.js +12 -3
  81. package/dist/components/editor-root-listeners.d.ts +3 -6
  82. package/dist/components/editor-root-listeners.js +3 -24
  83. package/dist/components/editor-root-mode-flip.d.ts +36 -0
  84. package/dist/components/editor-root-mode-flip.js +92 -0
  85. package/dist/components/image/ImageOverlayHost.svelte +14 -6
  86. package/dist/components/image/ImageProperties.svelte +512 -70
  87. package/dist/components/image/ImageProperties.svelte.d.ts +6 -1
  88. package/dist/components/image/ImageResizeHandles.svelte +67 -34
  89. package/dist/components/image/image-crop.d.ts +39 -0
  90. package/dist/components/image/image-crop.js +74 -0
  91. package/dist/components/image/image-edit-commit.d.ts +1 -0
  92. package/dist/components/image/image-edit-commit.js +21 -5
  93. package/dist/components/image/image-source-bytes.js +10 -3
  94. package/dist/components/image/image-widget-editing.js +1 -0
  95. package/dist/components/image/widget-dom.js +5 -1
  96. package/dist/components/link-card/link-card-commit.js +1 -1
  97. package/dist/components/lrd-map-gate.js +1 -1
  98. package/dist/components/menu/BlockMenu.svelte +315 -0
  99. package/dist/components/menu/BlockMenu.svelte.d.ts +34 -0
  100. package/dist/components/menu/MenuIcon.svelte +153 -0
  101. package/dist/components/menu/MenuIcon.svelte.d.ts +51 -0
  102. package/dist/components/menu/SelectionToolbar.svelte +385 -0
  103. package/dist/components/menu/SelectionToolbar.svelte.d.ts +16 -0
  104. package/dist/components/menu/clipboard-actions.d.ts +12 -0
  105. package/dist/components/menu/clipboard-actions.js +42 -0
  106. package/dist/components/menu/default-context-actions.d.ts +15 -0
  107. package/dist/components/menu/default-context-actions.js +76 -0
  108. package/dist/components/menu/flyout-placement.d.ts +6 -0
  109. package/dist/components/menu/flyout-placement.js +25 -0
  110. package/dist/core/inline/format-toggle.d.ts +12 -4
  111. package/dist/core/inline/format-toggle.js +94 -40
  112. package/dist/core/inline/image-dimensions.d.ts +3 -0
  113. package/dist/core/inline/image-dimensions.js +46 -10
  114. package/dist/core/inline/inline-widgets.d.ts +19 -0
  115. package/dist/core/inline/inline-widgets.js +5 -0
  116. package/dist/core/inline/scan/brackets.js +1 -0
  117. package/dist/core/inline/scan/plugin-syntax.d.ts +8 -0
  118. package/dist/core/inline/scan/plugin-syntax.js +15 -1
  119. package/dist/core/inline/transparency.js +3 -3
  120. package/dist/core/inline-render.d.ts +6 -0
  121. package/dist/core/inline-render.js +32 -0
  122. package/dist/core/nodes.d.ts +14 -0
  123. package/dist/cursor/edge-affinity.js +2 -1
  124. package/dist/cursor/height-oracle.d.ts +2 -3
  125. package/dist/cursor/height-oracle.js +0 -1
  126. package/dist/cursor/overlay-remeasure.js +8 -0
  127. package/dist/cursor/reveal-source.js +7 -2
  128. package/dist/cursor/scroll-hold.d.ts +10 -0
  129. package/dist/cursor/scroll-hold.js +22 -0
  130. package/dist/cursor/scrollport.d.ts +9 -0
  131. package/dist/cursor/scrollport.js +30 -1
  132. package/dist/cursor/visual-lines.d.ts +5 -4
  133. package/dist/cursor/visual-lines.js +56 -11
  134. package/dist/cursor/widget-edge-snap.d.ts +28 -0
  135. package/dist/cursor/widget-edge-snap.js +38 -0
  136. package/dist/cursor/widget-offset.d.ts +9 -0
  137. package/dist/cursor/widget-offset.js +71 -4
  138. package/dist/debug/interaction-trace.d.ts +4 -0
  139. package/dist/debug/interaction-trace.js +15 -0
  140. package/dist/decorations/decoration-state.svelte.js +1 -1
  141. package/dist/decorations/reserved-attrs.js +1 -0
  142. package/dist/editor-actions/ancestry-folds.d.ts +2 -2
  143. package/dist/editor-actions/ancestry-folds.js +1 -1
  144. package/dist/editor-actions/block-edit-scope.js +1 -1
  145. package/dist/editor-actions/commit/text-batch.d.ts +3 -2
  146. package/dist/editor-actions/commit/text-batch.js +1 -1
  147. package/dist/editor-actions/commit/undo-controller.js +4 -2
  148. package/dist/editor-actions/container-edit.js +2 -1
  149. package/dist/editor-actions/enter-completion.d.ts +2 -0
  150. package/dist/editor-actions/enter-completion.js +23 -2
  151. package/dist/editor-actions/focus/focus-dispatch.js +7 -4
  152. package/dist/editor-actions/focus/focus-landing.d.ts +10 -1
  153. package/dist/editor-actions/focus/focus-landing.js +20 -6
  154. package/dist/editor-actions/inline-range-commit.js +1 -1
  155. package/dist/editor-actions/plugin/container.d.ts +7 -0
  156. package/dist/editor-actions/plugin/container.js +3 -1
  157. package/dist/editor-actions/reorder-action.js +19 -10
  158. package/dist/editor-actions/reorder-drag.js +29 -1
  159. package/dist/editor-actions/replacement-focus.d.ts +1 -1
  160. package/dist/editor-actions/replacement-focus.js +1 -1
  161. package/dist/editor-actions/search-replace.js +1 -1
  162. package/dist/editor-actions/table-context.d.ts +4 -1
  163. package/dist/editor-actions/table-context.js +57 -1
  164. package/dist/editor-events.d.ts +3 -0
  165. package/dist/editor-keys.d.ts +33 -0
  166. package/dist/editor-props.d.ts +24 -12
  167. package/dist/index.d.ts +1 -1
  168. package/dist/plugin.d.ts +7 -0
  169. package/dist/plugin.js +16 -0
  170. package/dist/plugins/latex/BlockMath.svelte +264 -29
  171. package/dist/plugins/latex/BlockMath.svelte.d.ts +2 -0
  172. package/dist/plugins/latex/index.d.ts +2 -1
  173. package/dist/plugins/latex/latex-kind.js +60 -10
  174. package/dist/plugins/latex/math-completion.js +4 -1
  175. package/dist/plugins/latex/math-layout.d.ts +15 -0
  176. package/dist/plugins/latex/math-layout.js +13 -0
  177. package/dist/plugins/latex/math-source.d.ts +19 -0
  178. package/dist/plugins/latex/math-source.js +103 -0
  179. package/dist/plugins/latex/register.d.ts +11 -2
  180. package/dist/plugins/latex/register.js +3 -1
  181. package/dist/plugins/latex/renderer.d.ts +3 -3
  182. package/dist/plugins/latex/renderer.js +13 -6
  183. package/dist/plugins/mermaid/MermaidBlock.svelte +19 -9
  184. package/dist/plugins/parrot/ParrotBlock.svelte +3 -1
  185. package/dist/reactivity/list-windowing.svelte.d.ts +8 -8
  186. package/dist/reactivity/list-windowing.svelte.js +75 -30
  187. package/dist/schema/block-completions.d.ts +8 -0
  188. package/dist/schema/block-completions.js +11 -0
  189. package/dist/schema/commands.d.ts +9 -5
  190. package/dist/schema/commands.js +12 -7
  191. package/dist/schema/context-actions.d.ts +31 -0
  192. package/dist/schema/context-actions.js +23 -0
  193. package/dist/schema/fenced-code-raw.js +31 -1
  194. package/dist/schema/operations.d.ts +8 -1
  195. package/dist/schema/reserved-chords.js +37 -4
  196. package/dist/schema/table-cell-raw.d.ts +1 -1
  197. package/dist/schema/table-cell-raw.js +1 -1
  198. package/dist/selection/block-hit-test.js +3 -2
  199. package/dist/selection/char-endpoint-snap.js +1 -1
  200. package/dist/selection/clipboard-text.js +6 -1
  201. package/dist/selection/covered-block.d.ts +10 -0
  202. package/dist/selection/covered-block.js +24 -0
  203. package/dist/selection/cross-block/dispatch.d.ts +3 -0
  204. package/dist/selection/cross-block/dispatch.js +13 -1
  205. package/dist/selection/cross-block/format-range.d.ts +1 -1
  206. package/dist/selection/cross-block/format-range.js +4 -15
  207. package/dist/selection/cross-block/format-toggle.js +1 -1
  208. package/dist/selection/cross-block/keydown.js +3 -29
  209. package/dist/selection/cross-block/ops.js +1 -1
  210. package/dist/selection/cross-block/paste.js +31 -35
  211. package/dist/selection/cross-block/type-replace.d.ts +3 -2
  212. package/dist/selection/cross-block/type-replace.js +52 -13
  213. package/dist/selection/dead-space-caret.d.ts +13 -0
  214. package/dist/selection/dead-space-caret.js +57 -1
  215. package/dist/selection/drag-pointer.d.ts +26 -2
  216. package/dist/selection/drag-pointer.js +103 -2
  217. package/dist/selection/gap-caret.js +1 -1
  218. package/dist/selection/keyboard-extend.d.ts +3 -2
  219. package/dist/selection/keyboard-extend.js +4 -3
  220. package/dist/selection/multi-click.d.ts +42 -0
  221. package/dist/selection/multi-click.js +203 -0
  222. package/dist/selection/native-bridge.d.ts +3 -0
  223. package/dist/selection/native-bridge.js +31 -2
  224. package/dist/selection/path-lookup.d.ts +10 -2
  225. package/dist/selection/path-lookup.js +11 -4
  226. package/dist/selection/pointer-gesture.d.ts +9 -0
  227. package/dist/selection/pointer-gesture.js +11 -0
  228. package/dist/selection/primitives.d.ts +7 -0
  229. package/dist/selection/primitives.js +19 -1
  230. package/dist/selection/range-delete-ceremony.js +4 -2
  231. package/dist/selection/range-delete-chrome.js +2 -1
  232. package/dist/selection/range-delete-table-coverage.js +2 -1
  233. package/dist/selection/range-delete-table.js +3 -2
  234. package/dist/selection/range-delete.d.ts +4 -0
  235. package/dist/selection/range-delete.js +31 -3
  236. package/dist/selection/selection-drop.d.ts +34 -0
  237. package/dist/selection/selection-drop.js +253 -0
  238. package/dist/selection/selection-restore.js +1 -1
  239. package/dist/selection/selection-state.svelte.d.ts +6 -0
  240. package/dist/selection/selection-state.svelte.js +36 -1
  241. package/dist/selection/table-endpoint-snap.js +1 -1
  242. package/dist/selection/table-rect-extend.js +1 -1
  243. package/dist/styles/editor-theme.css +75 -31
  244. package/dist/styles/editor.css +268 -18
  245. package/dist/testing/container-conformance.js +2 -2
  246. package/dist/testing/inline-conformance.js +2 -1
  247. package/dist/tree-operations/blockquote.js +1 -1
  248. package/dist/tree-operations/chain-rebuild.d.ts +63 -0
  249. package/dist/tree-operations/chain-rebuild.js +142 -0
  250. package/dist/tree-operations/children.d.ts +1 -1
  251. package/dist/tree-operations/children.js +1 -1
  252. package/dist/tree-operations/cleanup.js +1 -1
  253. package/dist/tree-operations/content-write.d.ts +50 -0
  254. package/dist/tree-operations/content-write.js +263 -0
  255. package/dist/tree-operations/index.d.ts +8 -3
  256. package/dist/tree-operations/index.js +6 -2
  257. package/dist/tree-operations/list/exit-replacement.js +1 -1
  258. package/dist/tree-operations/list/unwrap-merge.js +3 -3
  259. package/dist/tree-operations/node-ops.d.ts +17 -234
  260. package/dist/tree-operations/node-ops.js +47 -1113
  261. package/dist/tree-operations/node-primitives.d.ts +75 -0
  262. package/dist/tree-operations/node-primitives.js +117 -0
  263. package/dist/tree-operations/paste/apply.js +1 -1
  264. package/dist/tree-operations/paste/body-write.d.ts +1 -1
  265. package/dist/tree-operations/paste/body-write.js +2 -2
  266. package/dist/tree-operations/paste/container-match.js +4 -2
  267. package/dist/tree-operations/paste/dispatch.js +2 -1
  268. package/dist/tree-operations/paste/find-enclosing-list.js +1 -1
  269. package/dist/tree-operations/paste/focus-target.d.ts +1 -1
  270. package/dist/tree-operations/paste/list-absorb.js +1 -1
  271. package/dist/tree-operations/paste/list-break-out.js +1 -1
  272. package/dist/tree-operations/paste/parent-scope.js +1 -1
  273. package/dist/tree-operations/paste/paste-replacement.js +1 -1
  274. package/dist/tree-operations/paste/replace-block-at-parent.d.ts +3 -1
  275. package/dist/tree-operations/paste/replace-block-at-parent.js +5 -2
  276. package/dist/tree-operations/paste/replacement-parse.d.ts +17 -0
  277. package/dist/tree-operations/paste/replacement-parse.js +22 -0
  278. package/dist/tree-operations/path-mutate.d.ts +1 -1
  279. package/dist/tree-operations/path-mutate.js +2 -1
  280. package/dist/tree-operations/reorder-unit.js +1 -1
  281. package/dist/tree-operations/reorder.d.ts +5 -2
  282. package/dist/tree-operations/reorder.js +55 -2
  283. package/dist/tree-operations/settle.d.ts +105 -0
  284. package/dist/tree-operations/settle.js +660 -0
  285. package/dist/tree-operations/table-grid-clipboard.d.ts +21 -0
  286. package/dist/tree-operations/table-grid-clipboard.js +90 -0
  287. package/dist/tree-operations/unshare.d.ts +15 -77
  288. package/dist/tree-operations/unshare.js +15 -166
  289. package/docs/guide/consumer-guide.md +136 -104
  290. package/docs/guide/plugin-api.md +73 -29
  291. package/docs/guide/plugin-guide.md +55 -23
  292. package/package.json +9 -7
  293. package/dist/components/blocks/code/CodeLanguageChip.svelte +0 -127
  294. package/dist/components/blocks/code/CodeLanguageChip.svelte.d.ts +0 -14
  295. package/dist/components/blocks/table/TableGrip.svelte +0 -91
  296. package/dist/components/blocks/table/TableGrip.svelte.d.ts +0 -8
  297. package/dist/components/blocks/table/table-drop-target.d.ts +0 -1
  298. package/dist/components/blocks/table/table-drop-target.js +0 -16
  299. package/dist/components/blocks/table/table-reorder-drag.d.ts +0 -78
  300. package/dist/components/blocks/table/table-reorder-drag.js +0 -97
@@ -5,6 +5,15 @@
5
5
  */
6
6
  import { type EditorPlugin } from '../../plugin';
7
7
  import { type MathRenderer } from './math-renderer';
8
- export declare function latexPlugin(options: {
8
+ import { type MathBlockLayout } from './math-layout';
9
+ export interface LatexPluginOptions {
9
10
  renderer: MathRenderer;
10
- }): EditorPlugin;
11
+ /**
12
+ * How a `$$` block opens for editing: `split` (source beside the preview, the default),
13
+ * `stacked` (preview below), or `source` (no preview). Definition-time, so it is the
14
+ * bare-install default; an editor's `{ plugin, options: { blockLayout } }` entry overrides it
15
+ * per instance. The block's own toggle cycles from whichever applies.
16
+ */
17
+ blockLayout?: MathBlockLayout;
18
+ }
19
+ export declare function latexPlugin(options: LatexPluginOptions): EditorPlugin;
@@ -6,8 +6,10 @@
6
6
  import { definePlugin, registerBlockComponent, defineBlockComponent, declaredPluginKind } from '../../plugin';
7
7
  import { registerMathInline, registerMathBlock, MATH_BLOCK, MATH_FENCE } from './latex-kind';
8
8
  import { setMathRenderer } from './math-renderer';
9
+ import { isMathBlockLayout } from './math-layout';
9
10
  import BlockMath from './BlockMath.svelte';
10
11
  export function latexPlugin(options) {
12
+ const blockLayout = isMathBlockLayout(options.blockLayout) ? options.blockLayout : 'split';
11
13
  return definePlugin({
12
14
  name: 'latex',
13
15
  setup() {
@@ -15,7 +17,7 @@ export function latexPlugin(options) {
15
17
  registerMathInline();
16
18
  // registerMathBlock co-registers the ```math fence kind; both render through BlockMath.
17
19
  registerMathBlock();
18
- const blockMath = defineBlockComponent(BlockMath);
20
+ const blockMath = defineBlockComponent(BlockMath, () => ({ blockLayout }));
19
21
  registerBlockComponent(declaredPluginKind(MATH_BLOCK), blockMath);
20
22
  registerBlockComponent(declaredPluginKind(MATH_FENCE), blockMath);
21
23
  }
@@ -6,8 +6,8 @@
6
6
  import 'katex/dist/katex.min.css';
7
7
  import type { MathRenderer } from './math-renderer';
8
8
  /**
9
- * `throwOnError: false` keeps a malformed formula from crashing the editor, but its fallback
10
- * renders the source verbatim, a bare raw-source strip A5 forbids: hence the `.katex-error`
11
- * detection and the legible message swapped in for it.
9
+ * `throwOnError: false` keeps a malformed formula from crashing the editor; its fallback is
10
+ * swapped for the source painted as an error — red, in the code face, the parser's message on
11
+ * hover — so a broken formula reads as what the author typed, not as a sentence about it.
12
12
  */
13
13
  export declare const katexRenderer: MathRenderer;
@@ -8,9 +8,9 @@ import katex from 'katex';
8
8
  // a `.katex-mathml` a11y tree this CSS clips to 1px, and unloaded every equation paints twice.
9
9
  import 'katex/dist/katex.min.css';
10
10
  /**
11
- * `throwOnError: false` keeps a malformed formula from crashing the editor, but its fallback
12
- * renders the source verbatim, a bare raw-source strip A5 forbids: hence the `.katex-error`
13
- * detection and the legible message swapped in for it.
11
+ * `throwOnError: false` keeps a malformed formula from crashing the editor; its fallback is
12
+ * swapped for the source painted as an error — red, in the code face, the parser's message on
13
+ * hover — so a broken formula reads as what the author typed, not as a sentence about it.
14
14
  */
15
15
  export const katexRenderer = (source, { display }) => {
16
16
  const container = document.createElement('span');
@@ -22,13 +22,20 @@ export const katexRenderer = (source, { display }) => {
22
22
  const errorSpan = container.querySelector('.katex-error');
23
23
  if (errorSpan) {
24
24
  const message = (errorSpan.getAttribute('title') ?? 'invalid LaTeX').replace(/^ParseError:\s*/, '');
25
- return { dom: buildErrorNode(message), error: message };
25
+ return { dom: buildErrorNode(source, message), error: message };
26
26
  }
27
27
  return { dom: container };
28
28
  };
29
- function buildErrorNode(message) {
29
+ function buildErrorNode(source, message) {
30
30
  const span = document.createElement('span');
31
31
  span.className = 'math-error';
32
- span.textContent = `Math error: ${message}`;
32
+ span.textContent = source;
33
+ span.title = message;
34
+ // Inline style rather than a stylesheet: the span lands inside any prose block, where no
35
+ // plugin sheet is guaranteed to be mounted.
36
+ span.style.color = 'var(--color-error, #d03025)';
37
+ span.style.fontFamily = 'var(--font-code, ui-monospace, monospace)';
38
+ span.style.fontSize = '0.9em';
39
+ span.style.cursor = 'help';
33
40
  return span;
34
41
  }
@@ -32,6 +32,7 @@
32
32
  updateOwnMetadata,
33
33
  handleKeydown,
34
34
  moveFocusOut,
35
+ captureScrollPosition,
35
36
  getPresentationMode,
36
37
  getTheme
37
38
  } = createContainerBlock({
@@ -142,22 +143,20 @@
142
143
  const view = createPanZoom();
143
144
  const overlayView = createPanZoom();
144
145
 
145
- // Focus-gated so the in-document diagram never hijacks the page: unfocused, a bare
146
- // wheel scrolls and a stray drag cannot pan. Any descendant focus counts.
147
- const isFocused = () => !!boxEl?.contains(document.activeElement);
146
+ // Armed by focus, so an unfocused diagram hijacks neither the page nor the editor's own drag.
147
+ // Tracked rather than probed: the editor reads the same fact as markup, so there is one value.
148
+ let gestureArmed = $state(false);
148
149
 
149
150
  function onViewportWheel(e: WheelEvent): void {
150
- if (!isFocused() || !(e.ctrlKey || e.metaKey)) return;
151
+ if (!gestureArmed || !(e.ctrlKey || e.metaKey)) return;
151
152
  e.preventDefault();
152
153
  e.stopPropagation();
153
154
  view.zoomBy(e.deltaY);
154
155
  }
155
156
 
156
157
  function onViewportPointerDown(e: PointerEvent): void {
157
- // Never preventDefault, so the browser's focus-on-mousedown still lands: the
158
- // first click focuses, and only a drag on the now-focused block pans.
159
158
  e.stopPropagation();
160
- if (isFocused()) view.beginPan(e);
159
+ if (gestureArmed) view.beginPan(e);
161
160
  }
162
161
 
163
162
  function onOverlayWheel(e: WheelEvent): void {
@@ -219,8 +218,11 @@
219
218
  // there too, so this closes the command path.
220
219
  if (isReading) return;
221
220
  if (!editing) seedDraft();
221
+ // Captured BEFORE the flip: the card reaches its fitted height only after mounting, and
222
+ // a scrollport at the document's end clamps against the short layout in between.
223
+ const restoreScroll = captureScrollPosition();
222
224
  editRequested = true;
223
- void tick().then(() => textareaEl?.focus());
225
+ void restoreScroll().then(() => textareaEl?.focus());
224
226
  }
225
227
 
226
228
  function cancelEdit(): void {
@@ -306,7 +308,13 @@
306
308
  </script>
307
309
 
308
310
  <!-- svelte-ignore a11y_no_static_element_interactions -->
309
- <div class="mermaid-block" bind:this={boxEl} onkeydown={handleKeydown}>
311
+ <div
312
+ class="mermaid-block"
313
+ bind:this={boxEl}
314
+ onkeydown={handleKeydown}
315
+ onfocusin={() => (gestureArmed = true)}
316
+ onfocusout={(e) => (gestureArmed = boxEl?.contains(e.relatedTarget as Node | null) ?? false)}
317
+ >
310
318
  {#if editing}
311
319
  <textarea
312
320
  bind:this={textareaEl}
@@ -330,6 +338,7 @@
330
338
  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
331
339
  <div
332
340
  class="mermaid-viewport"
341
+ data-pointer-gesture={gestureArmed ? '' : undefined}
333
342
  tabindex="0"
334
343
  role="img"
335
344
  aria-label="Mermaid diagram"
@@ -392,6 +401,7 @@
392
401
  </div>
393
402
  <div
394
403
  class="mermaid-overlay-viewport"
404
+ data-pointer-gesture
395
405
  onwheel={onOverlayWheel}
396
406
  onpointerdown={(e) => {
397
407
  e.stopPropagation();
@@ -273,7 +273,9 @@ cNd.........................................;lOc
273
273
  font-size: 1.1em;
274
274
  line-height: 1.1;
275
275
  letter-spacing: 0.05em;
276
- /* one frame tall, in the reel's own rows so a step lands on the next frame exactly */
276
+ /* one frame tall, in the reel's own rows so a step lands on the next frame exactly; the
277
+ em line is the same height for engines without lh (Safari before 16.4) */
278
+ height: calc(var(--parrot-rows) * 1.1em);
277
279
  height: calc(var(--parrot-rows) * 1lh);
278
280
  /* wider than a phone column, and the editor root pans if it isn't contained; the bar
279
281
  would sit across the bird, which is decoration rather than a pane to scroll */
@@ -63,15 +63,15 @@ export interface ListWindowing {
63
63
  recordMeasuredChild(index: number, id: string, height: number): void;
64
64
  /** A PROPAGATED child-container subtotal: oracle + model slot, addressed by index. No anchor correction. */
65
65
  setChildSubtotal(index: number, total: number): void;
66
- /** Enroll a child in this scope's batched measure pass; returns an unregister fn to call on
67
- * the child's unmount. The scope reads ALL pending children before applying ANY write, so a
68
- * fling that mounts many costs one reflow. */
66
+ /** Enroll a child in this scope's batched measure pass, read after the flush that registered
67
+ * it; returns an unregister fn to call on the child's unmount. The scope reads ALL pending
68
+ * children before applying ANY write, so a fling that mounts many costs one reflow. */
69
69
  registerChild(id: string, child: MeasureEntry): () => void;
70
70
  /** Re-measure ONE registered child immediately (its content height changed on edit). */
71
71
  measureChildNow(id: string): void;
72
- /** ResizeObserver path: O(1)-gate `observedHeight` against the recorded height and
73
- * re-measure only on a genuine post-mount change, so the no-op mount resize on a fling
74
- * costs no DOM read. */
72
+ /** ResizeObserver path: O(1)-gate `observedHeight` against the height this scope last
73
+ * applied and re-measure only on a genuine post-mount change, so the no-op mount resize
74
+ * on a fling costs no DOM read. */
75
75
  measureChildOnResize(id: string, observedHeight: number): void;
76
76
  /**
77
77
  * True when the scroll position IS the reveal target's requested placement. Asked by
@@ -93,8 +93,8 @@ export interface ListWindowing {
93
93
  * Resize-gate decision (pure, unit-tested). Keyed on the height DIFFERING from what is
94
94
  * recorded, never on which callback delivered it: a cached remount can report the grown
95
95
  * size in the very first callback, so a callback-order heuristic would drop it.
96
- * `recorded === undefined` means the batched mount pass hasn't measured this block yet —
97
- * defer to it rather than racing a lone read in on a fling-dirtied layout.
96
+ * `recorded === undefined` means the scope has applied no height for this block yet —
97
+ * defer to the batched pass rather than racing a lone read in on a fling-dirtied layout.
98
98
  */
99
99
  export declare function shouldRemeasureOnResize(recorded: number | undefined, observed: number): boolean;
100
100
  export declare function createListWindowing(deps: ListWindowingDeps): ListWindowing;
@@ -14,8 +14,8 @@ import { runMeasureBatch } from './measure-batch';
14
14
  * Resize-gate decision (pure, unit-tested). Keyed on the height DIFFERING from what is
15
15
  * recorded, never on which callback delivered it: a cached remount can report the grown
16
16
  * size in the very first callback, so a callback-order heuristic would drop it.
17
- * `recorded === undefined` means the batched mount pass hasn't measured this block yet —
18
- * defer to it rather than racing a lone read in on a fling-dirtied layout.
17
+ * `recorded === undefined` means the scope has applied no height for this block yet —
18
+ * defer to the batched pass rather than racing a lone read in on a fling-dirtied layout.
19
19
  */
20
20
  export function shouldRemeasureOnResize(recorded, observed) {
21
21
  if (observed <= 0 || recorded === undefined)
@@ -43,7 +43,23 @@ export function createListWindowing(deps) {
43
43
  // structural rebuild needs the OLD ordering to remap the anchor by id, and by then
44
44
  // `getChildIds()` already reflects the new children. Copied — it is spliced in place.
45
45
  let modelChildIds = [];
46
- function buildModel() {
46
+ /** What the CURRENT model holds, keyed by id, for a rebuild to carry across. The two lengths
47
+ * diverge for one tick after a child change, and a slot past the model's end reads 0. */
48
+ function heightsById() {
49
+ const carried = new Map();
50
+ const known = Math.min(modelChildIds.length, model.size);
51
+ for (let i = 0; i < known; i++)
52
+ carried.set(modelChildIds[i], model.heightOf(i));
53
+ return carried;
54
+ }
55
+ /**
56
+ * A surviving block keeps the height the model measured (VR-15); `carried` is null only
57
+ * where a width or type-scale change has earned a reseed from the oracle. The cache behind
58
+ * those heights can be gone (a mode flip drops it, and a block whose box never moved reports
59
+ * no resize to put it back), so reseeding a structural rebuild would trade a whole document
60
+ * of measurements for estimates and scroll the reader by the difference.
61
+ */
62
+ function buildModel(carried) {
47
63
  const width = estimateWidth(deps.getListEl(), deps.getPort()?.contentWidth() ?? 0);
48
64
  const children = deps.getChildren();
49
65
  // Indexed, and off the snapshot: `map` pays a `has` trap beside every `get`, once per child.
@@ -51,15 +67,17 @@ export function createListWindowing(deps) {
51
67
  const count = children.length;
52
68
  const heights = new Array(count);
53
69
  for (let i = 0; i < count; i++) {
54
- heights[i] = deps.oracle.height(modelChildIds[i], children[i], width);
70
+ const id = modelChildIds[i];
71
+ heights[i] =
72
+ deps.oracle.measured(id) ?? carried?.get(id) ?? deps.oracle.estimate(children[i], width);
55
73
  }
56
74
  return new HeightModel(heights);
57
75
  }
58
- let model = $state(buildModel());
76
+ let model = $state(buildModel(null));
59
77
  let heightVersion = $state(0);
60
78
  // One scope-owned batched pass rather than a per-child effect, which would interleave a
61
79
  // layout read with the prior child's write → one forced reflow per mounted block on a
62
- // fling. `pending` holds ids awaiting their first measure; the batch effect drains it.
80
+ // fling. `pending` holds ids awaiting their first measure; registration drains it.
63
81
  const registry = new Map();
64
82
  const pending = new Set();
65
83
  /**
@@ -123,6 +141,20 @@ export function createListWindowing(deps) {
123
141
  placeRevealTarget();
124
142
  return true;
125
143
  }
144
+ /**
145
+ * Which block this correction holds still: the top-of-viewport one by default, the FOCUSED
146
+ * one when it sits at or below the fold. Clicking a construct that reveals its source (a
147
+ * math block, a paragraph holding inline math) resizes that block and folds whichever was
148
+ * revealed before it; anchored on the viewport top, both slide the clicked block out from
149
+ * under the pointer. Anchoring on the focused block holds its start offset instead, so it
150
+ * stays put and only the content after it reflows.
151
+ */
152
+ function anchorIndexFor(topIndex) {
153
+ const pinned = pinnedIndex();
154
+ if (pinned === null || pinned < topIndex || pinned >= model.size)
155
+ return topIndex;
156
+ return pinned;
157
+ }
126
158
  // Hold the anchor block's screen position across a height mutation that would otherwise
127
159
  // slide the visible content (VR-2) — native `overflow-anchor` is off wherever this runs, so
128
160
  // nothing else holds the line. The delta comes from the Fenwick model, not
@@ -134,12 +166,12 @@ export function createListWindowing(deps) {
134
166
  if (reassertRevealAnchor(mutate))
135
167
  return;
136
168
  const port = deps.getPort();
137
- const anchorIndex = model.indexAtOffset(localScrollTop());
169
+ const anchorIndex = anchorIndexFor(model.indexAtOffset(localScrollTop()));
138
170
  const before = model.offsetOf(anchorIndex);
139
171
  mutate();
140
172
  const delta = model.offsetOf(anchorIndex) - before;
141
173
  if (delta !== 0 && port)
142
- port.setScrollTop(port.scrollTop() + delta);
174
+ port.scrollBy(delta);
143
175
  }
144
176
  // The structural-rebuild variant of correctAnchor. A count change shifts every index
145
177
  // above it, so the numeric form would measure a DIFFERENT block at index N and mis-correct
@@ -168,7 +200,7 @@ export function createListWindowing(deps) {
168
200
  return;
169
201
  const delta = model.offsetOf(newIndex) - before;
170
202
  if (delta !== 0 && port)
171
- port.setScrollTop(port.scrollTop() + delta);
203
+ port.scrollBy(delta);
172
204
  }
173
205
  let lastWidthVersion = deps.getWidthVersion();
174
206
  // Rebuild on any structural child change or an editor WIDTH change, never per keystroke.
@@ -188,8 +220,10 @@ export function createListWindowing(deps) {
188
220
  // the re-measure compares measured-before against all-estimate-after, and lands the
189
221
  // anchor a block off wherever the poisoned model names a different top child (#188).
190
222
  // Width only — re-measuring on a structural edit costs a reflow per split.
223
+ // Read off the OLD model, so the carry is taken before the rebuild replaces it.
224
+ const carried = widthChanged ? null : heightsById();
191
225
  correctAnchorByStableId(() => {
192
- model = buildModel();
226
+ model = buildModel(carried);
193
227
  heightVersion++;
194
228
  if (widthChanged)
195
229
  remeasureMounted();
@@ -258,22 +292,23 @@ export function createListWindowing(deps) {
258
292
  $effect(() => {
259
293
  void heightVersion;
260
294
  const el = deps.getOwnEl?.();
261
- if (!el || !deps.reportSelfHeight)
295
+ const report = deps.reportSelfHeight;
296
+ if (!el || !report)
262
297
  return;
263
- const h = el.getBoundingClientRect().height;
264
- if (h > 0 && Math.abs(h - reportedSelfHeight) >= 1) {
265
- reportedSelfHeight = h;
266
- deps.reportSelfHeight(h);
267
- }
268
- });
269
- // The batched measure pass for NEWLY-MOUNTED children, keyed on the EFFECTIVE window: the
270
- // raw result can be identical across a collapse expand, stranding remounted children in
271
- // `pending` until the next scroll. Registration deliberately does NOT bump reactive
272
- // state — the mount that registers a child already moved the window, so a per-child
273
- // trigger would re-enter this effect O(children) times and trip the update-depth guard.
274
- $effect(() => {
275
- void effectiveWindow;
276
- untrack(() => flushMeasurements());
298
+ // After the flush, like the batch: at this effect's first run the children are unrendered.
299
+ let live = true;
300
+ void tick().then(() => {
301
+ if (!live)
302
+ return;
303
+ const h = el.getBoundingClientRect().height;
304
+ if (h > 0 && Math.abs(h - reportedSelfHeight) >= 1) {
305
+ reportedSelfHeight = h;
306
+ report(h);
307
+ }
308
+ });
309
+ return () => {
310
+ live = false;
311
+ };
277
312
  });
278
313
  // The UNCORRECTED batch: the caller owns the anchor correction, because nesting a second
279
314
  // one inside an outer `mutate` double-counts the delta.
@@ -348,11 +383,21 @@ export function createListWindowing(deps) {
348
383
  if (!skipWhileHostAnchors(write) && !reassertRevealAnchor(write))
349
384
  write();
350
385
  },
351
- // Enroll without touching reactive state: the mount that registers this child already
352
- // moved the window, which re-runs the batch effect to drain `pending`.
386
+ // Read after the flush that mounted the child, not inside it: content can land later in
387
+ // that flush (an inline widget's root flushes after this one), and an empty-host read is
388
+ // a scroll correction the observer undoes a frame later. Still ahead of paint.
353
389
  registerChild(id, child) {
354
- registry.set(id, child);
390
+ const entry = {
391
+ readHeight: child.readHeight,
392
+ applyHeight: (h) => {
393
+ entry.applied = h;
394
+ child.applyHeight(h);
395
+ },
396
+ applied: undefined
397
+ };
398
+ registry.set(id, entry);
355
399
  pending.add(id);
400
+ void tick().then(flushMeasurements);
356
401
  return () => {
357
402
  registry.delete(id);
358
403
  pending.delete(id);
@@ -362,11 +407,11 @@ export function createListWindowing(deps) {
362
407
  measureChildNow(id) {
363
408
  measureOne(id);
364
409
  },
365
- // ResizeObserver path for async growth. The gate reads the oracle's recorded height
410
+ // ResizeObserver path for async growth. The gate reads what this scope last applied
366
411
  // (O(1), no DOM), so the no-op mount resize a fling fires for every newly-mounted
367
412
  // block returns without a rect read on the spacer-dirtied layout (VR-4).
368
413
  measureChildOnResize(id, observedHeight) {
369
- if (shouldRemeasureOnResize(deps.oracle.measured(id), observedHeight))
414
+ if (shouldRemeasureOnResize(registry.get(id)?.applied, observedHeight))
370
415
  measureOne(id);
371
416
  },
372
417
  revealHoldsScroll() {
@@ -22,10 +22,18 @@ export interface CompletionResult {
22
22
  export interface BlockCompleter {
23
23
  /** Attempt to complete `line`; null declines, as does a claim whose lines would paint nothing. */
24
24
  tryComplete(line: string): CompletionResult | null;
25
+ /**
26
+ * Also consulted as the line is TYPED, not only at Enter — for a claim that can only ever mean
27
+ * one thing the moment it is complete (a lone `$$`), so the block forms as a ` ``` ` fence does.
28
+ * Off by default: a table's header row is a prefix of a longer row the user may still be typing.
29
+ */
30
+ onType?: boolean;
25
31
  }
26
32
  export declare function registerBlockCompleter(kind: AnyBlockKind, completer: BlockCompleter): void;
27
33
  /** The first claim on `line`, or null when no registered completer takes it. */
28
34
  export declare function completeTypedLine(line: string): CompletionResult | null;
35
+ /** The first claim on `line` among the completers that answer as the line is typed. */
36
+ export declare function completeLineOnType(line: string): CompletionResult | null;
29
37
  /** Whether `kind` already owns a completer — the probe a registrar re-run reads before it
30
38
  * registers, so a re-installed plugin never trips the register-once throw. */
31
39
  export declare function isBlockCompleterRegistered(kind: string): boolean;
@@ -33,6 +33,17 @@ export function completeTypedLine(line) {
33
33
  }
34
34
  return null;
35
35
  }
36
+ /** The first claim on `line` among the completers that answer as the line is typed. */
37
+ export function completeLineOnType(line) {
38
+ for (const completer of ordered()) {
39
+ if (!completer.onType)
40
+ continue;
41
+ const claim = completer.tryComplete(line);
42
+ if (claim)
43
+ return claim;
44
+ }
45
+ return null;
46
+ }
36
47
  /** Whether `kind` already owns a completer — the probe a registrar re-run reads before it
37
48
  * registers, so a re-installed plugin never trips the register-once throw. */
38
49
  export function isBlockCompleterRegistered(kind) {
@@ -19,10 +19,10 @@ export type GlobalCommandId = (typeof GLOBAL_COMMAND_IDS)[number];
19
19
  export type BlockCommandId = (typeof BLOCK_COMMAND_IDS)[number];
20
20
  export type CommandId = GlobalCommandId | BlockCommandId;
21
21
  /**
22
- * Commands whose arms rewrite one block's bytes around that block's own selection and have no
23
- * cross-block reading: the dispatch seam declines them outright while a range is painted.
24
- * Membership is the arm's shape, not the id's prefix — the link card mints its link over one
25
- * block's offsets, and a range gives it no one block to mint into.
22
+ * Commands whose arms rewrite one block and have no cross-block reading: the dispatch seam
23
+ * declines them outright while a range is painted. Membership is the arm's shape, not the id's
24
+ * prefix: the link card mints over one block's offsets and a heading level belongs to one block,
25
+ * so a range spanning blocks leaves neither of them a subject to act on.
26
26
  */
27
27
  export declare const RANGE_DECLINED_COMMAND_IDS: ReadonlySet<string>;
28
28
  /**
@@ -34,7 +34,9 @@ export declare const RANGE_DECLINED_COMMAND_IDS: ReadonlySet<string>;
34
34
  export declare const CROSS_BLOCK_RANGE_COMMAND_IDS: ReadonlySet<string>;
35
35
  /**
36
36
  * The command ids a host's selection toolbar invokes through `EditorInstance.runCommand`. The
37
- * rest of the vocabulary stays internal until the command registry unifies it.
37
+ * rest of the vocabulary stays internal until the command registry unifies it. Every id here
38
+ * answers the range question in one of the two sets above, so `canRunCommand` tells a bar which
39
+ * of its buttons a cross-block selection leaves nothing to act on.
38
40
  */
39
41
  export declare const TOOLBAR_COMMANDS: {
40
42
  readonly toggleStrong: "format.toggleStrong";
@@ -43,6 +45,8 @@ export declare const TOOLBAR_COMMANDS: {
43
45
  readonly toggleCode: "format.toggleCode";
44
46
  /** The link editor Mod+K opens: over a selection it creates, inside a link it edits. */
45
47
  readonly editLink: "link.openCard";
48
+ /** The heading picker's arm, taking the level as its argument; 0 is normal text. */
49
+ readonly setHeading: "heading.cycle";
46
50
  };
47
51
  /** Minimal context a global command needs; HistoryActions is structurally compatible. */
48
52
  export interface GlobalCommandContext {
@@ -44,13 +44,14 @@ export const BLOCK_COMMAND_IDS = [
44
44
  'chrome.descendToBody'
45
45
  ];
46
46
  /**
47
- * Commands whose arms rewrite one block's bytes around that block's own selection and have no
48
- * cross-block reading: the dispatch seam declines them outright while a range is painted.
49
- * Membership is the arm's shape, not the id's prefix — the link card mints its link over one
50
- * block's offsets, and a range gives it no one block to mint into.
47
+ * Commands whose arms rewrite one block and have no cross-block reading: the dispatch seam
48
+ * declines them outright while a range is painted. Membership is the arm's shape, not the id's
49
+ * prefix: the link card mints over one block's offsets and a heading level belongs to one block,
50
+ * so a range spanning blocks leaves neither of them a subject to act on.
51
51
  */
52
52
  export const RANGE_DECLINED_COMMAND_IDS = new Set([
53
- 'link.openCard'
53
+ 'link.openCard',
54
+ 'heading.cycle'
54
55
  ]);
55
56
  /**
56
57
  * The same one-block arms, but with a cross-block one behind them
@@ -66,7 +67,9 @@ export const CROSS_BLOCK_RANGE_COMMAND_IDS = new Set([
66
67
  ]);
67
68
  /**
68
69
  * The command ids a host's selection toolbar invokes through `EditorInstance.runCommand`. The
69
- * rest of the vocabulary stays internal until the command registry unifies it.
70
+ * rest of the vocabulary stays internal until the command registry unifies it. Every id here
71
+ * answers the range question in one of the two sets above, so `canRunCommand` tells a bar which
72
+ * of its buttons a cross-block selection leaves nothing to act on.
70
73
  */
71
74
  export const TOOLBAR_COMMANDS = {
72
75
  toggleStrong: 'format.toggleStrong',
@@ -74,7 +77,9 @@ export const TOOLBAR_COMMANDS = {
74
77
  toggleStrikethrough: 'format.toggleStrikethrough',
75
78
  toggleCode: 'format.toggleCode',
76
79
  /** The link editor Mod+K opens: over a selection it creates, inside a link it edits. */
77
- editLink: 'link.openCard'
80
+ editLink: 'link.openCard',
81
+ /** The heading picker's arm, taking the level as its argument; 0 is normal text. */
82
+ setHeading: 'heading.cycle'
78
83
  };
79
84
  const globalCommands = new Map();
80
85
  export function registerCommand(id, run) {
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The block context menu's vocabulary: what a right-click on a block of a given kind offers.
3
+ * A kind's own providers come first, then the ones registered for every kind (`'*'`: the
4
+ * editor's copy, replace and remove rows). Providers are consulted on every open, so they read
5
+ * the block as it is. The defaults register at bootstrap; a plugin registers through the barrel.
6
+ */
7
+ export declare const EVERY_KIND = "*";
8
+ import type { NodeView } from '../core/node-views';
9
+ export interface BlockActionContext {
10
+ node: NodeView;
11
+ /** The block's path from the document root. Top-level blocks only, for now. */
12
+ path: number[];
13
+ /** Remove the block. */
14
+ deleteBlock(): Promise<void>;
15
+ /** Replace the block's bytes wholesale; the result reparses to whatever those bytes are. */
16
+ replaceRaw(raw: string): Promise<void>;
17
+ }
18
+ export interface BlockContextAction {
19
+ id: string;
20
+ label: string;
21
+ /** A glyph name the menu knows (`components/menu/MenuIcon.svelte`); absent draws none. */
22
+ icon?: string;
23
+ danger?: boolean;
24
+ run(ctx: BlockActionContext): void | Promise<void>;
25
+ }
26
+ export type BlockContextActionProvider = (node: NodeView, path: number[]) => BlockContextAction[];
27
+ /** Add a provider for `kind`, or for every kind with `EVERY_KIND`. Several may stack. */
28
+ export declare function registerBlockContextActions(kind: string, provider: BlockContextActionProvider): void;
29
+ export declare function blockContextActionsFor(node: NodeView, path: number[]): BlockContextAction[];
30
+ /** Test-only. */
31
+ export declare function __resetBlockContextActionsForTests(): void;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The block context menu's vocabulary: what a right-click on a block of a given kind offers.
3
+ * A kind's own providers come first, then the ones registered for every kind (`'*'`: the
4
+ * editor's copy, replace and remove rows). Providers are consulted on every open, so they read
5
+ * the block as it is. The defaults register at bootstrap; a plugin registers through the barrel.
6
+ */
7
+ export const EVERY_KIND = '*';
8
+ const providers = new Map();
9
+ /** Add a provider for `kind`, or for every kind with `EVERY_KIND`. Several may stack. */
10
+ export function registerBlockContextActions(kind, provider) {
11
+ const list = providers.get(kind) ?? [];
12
+ list.push(provider);
13
+ providers.set(kind, list);
14
+ }
15
+ export function blockContextActionsFor(node, path) {
16
+ const own = providers.get(node.kind) ?? [];
17
+ const every = providers.get(EVERY_KIND) ?? [];
18
+ return [...own, ...every].flatMap((provider) => provider(node, path));
19
+ }
20
+ /** Test-only. */
21
+ export function __resetBlockContextActionsForTests() {
22
+ providers.clear();
23
+ }
@@ -17,7 +17,8 @@ export function reconcileFenceWrite(input) {
17
17
  const { fence, mode } = input;
18
18
  if (!fence.closed && mode === 'authored')
19
19
  return { display: input.display, caret: input.caret };
20
- return escalateFenceRuns({ ...input, ...sanitizeInfoString(input) });
20
+ const unglued = separateGluedCloser(input);
21
+ return escalateFenceRuns({ ...input, ...sanitizeInfoString({ ...input, ...unglued }) });
21
22
  }
22
23
  /**
23
24
  * A whole fencedCode `raw` made legal — the kind's `normalizeRawWrite`. Literal by
@@ -166,6 +167,35 @@ function countDroppedBefore(info, infoStart, caret) {
166
167
  }
167
168
  return dropped;
168
169
  }
170
+ /**
171
+ * Un-glue a closer the write landed on top of. A fence with NO body line — ```` ```\n``` ```` —
172
+ * has nothing between its two runs, so the body's only caret seat IS the closer's line start,
173
+ * and the first character typed there arrives ahead of the closer run: ```` ```\nAB``` ````,
174
+ * which no longer reads as closed and shows the run as body text. The fence says it is closed
175
+ * and the display no longer carries a closer line, so the write broke it; the missing line
176
+ * ending is what puts it back.
177
+ */
178
+ function separateGluedCloser(input) {
179
+ const { display, caret, fence } = input;
180
+ if (!fence.closed)
181
+ return { display, caret };
182
+ const lines = display.split('\n');
183
+ // A closer still on its own line needs nothing; so does a display too short to hold one.
184
+ if (lines.length < 2 || lastCloserIndex(lines, fence) >= 1)
185
+ return { display, caret };
186
+ const run = new RegExp(`[${fence.marker}]{${fence.length},}[ \t]*$`).exec(lines[lines.length - 1]);
187
+ // `index === 0` is a bare run that some other rule owns, not a glued one.
188
+ if (!run || run.index === 0)
189
+ return { display, caret };
190
+ const at = display.length - run[0].length;
191
+ const ending = display.includes('\r\n') ? '\r\n' : '\n';
192
+ return {
193
+ display: display.slice(0, at) + ending + display.slice(at),
194
+ // The caret sits at the typed text's end, which is where the ending is inserted: it
195
+ // stays put, and only a caret already inside the closer run rides the shift.
196
+ caret: caret > at ? caret + ending.length : caret
197
+ };
198
+ }
169
199
  function escalateFenceRuns(input) {
170
200
  const { display, caret, fence } = input;
171
201
  const lines = display.split('\n');