@voithos-labs/aragonite 0.10.2 → 0.10.3

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 (255) hide show
  1. package/README.md +6 -21
  2. package/THIRD-PARTY-NOTICES.md +25 -0
  3. package/dist/a11y-strings.d.ts +18 -0
  4. package/dist/a11y-strings.js +18 -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 +35 -24
  9. package/dist/components/BlockHost.svelte +16 -9
  10. package/dist/components/Editor.svelte +381 -198
  11. package/dist/components/Editor.svelte.d.ts +1 -1
  12. package/dist/components/SelectionOverlay.svelte +17 -3
  13. package/dist/components/TailInsert.svelte +107 -0
  14. package/dist/components/TailInsert.svelte.d.ts +17 -0
  15. package/dist/components/block-content-selector.d.ts +6 -2
  16. package/dist/components/block-content-selector.js +6 -2
  17. package/dist/components/blocks/ThematicBreakBlock.svelte +19 -6
  18. package/dist/components/blocks/ThematicBreakBlock.svelte.d.ts +1 -0
  19. package/dist/components/blocks/code/CodeBlock.svelte +211 -30
  20. package/dist/components/blocks/code/CodeBlockRail.svelte +686 -0
  21. package/dist/components/blocks/code/CodeBlockRail.svelte.d.ts +26 -0
  22. package/dist/components/blocks/code/code-bootstrap.js +4 -0
  23. package/dist/components/blocks/code/code-context-actions.d.ts +1 -0
  24. package/dist/components/blocks/code/code-context-actions.js +24 -0
  25. package/dist/components/blocks/code/code-fence-exit.d.ts +15 -0
  26. package/dist/components/blocks/code/code-fence-exit.js +26 -0
  27. package/dist/components/blocks/code/code-languages.d.ts +6 -0
  28. package/dist/components/blocks/code/code-languages.js +11 -0
  29. package/dist/components/blocks/code/code-renderer.js +11 -0
  30. package/dist/components/blocks/directive/DirectiveContainerBlock.svelte +1 -1
  31. package/dist/components/blocks/editable-leaf.d.ts +37 -6
  32. package/dist/components/blocks/editable-leaf.js +243 -29
  33. package/dist/components/blocks/editable-surface.d.ts +9 -0
  34. package/dist/components/blocks/editable-surface.js +22 -3
  35. package/dist/components/blocks/list/ListItemBlock.svelte +3 -3
  36. package/dist/components/blocks/list/task-checkbox.d.ts +2 -0
  37. package/dist/components/blocks/list/task-checkbox.js +11 -2
  38. package/dist/components/blocks/surface-wiring.svelte.d.ts +4 -0
  39. package/dist/components/blocks/surface-wiring.svelte.js +8 -1
  40. package/dist/components/blocks/table/TableActionMenu.svelte +210 -86
  41. package/dist/components/blocks/table/TableActionMenu.svelte.d.ts +5 -0
  42. package/dist/components/blocks/table/TableBlock.svelte +205 -186
  43. package/dist/components/blocks/table/TableBlock.svelte.d.ts +1 -0
  44. package/dist/components/blocks/table/TableCellBlock.svelte +87 -22
  45. package/dist/components/blocks/table/TableRowBlock.svelte +4 -18
  46. package/dist/components/blocks/table/TableRowBlock.svelte.d.ts +0 -5
  47. package/dist/components/blocks/table/cell-clipboard.d.ts +10 -0
  48. package/dist/components/blocks/table/cell-clipboard.js +34 -1
  49. package/dist/components/blocks/table/cell-keydown-plan.d.ts +1 -1
  50. package/dist/components/blocks/table/cell-keydown-plan.js +3 -1
  51. package/dist/components/blocks/table/table-cell-paste.js +2 -1
  52. package/dist/components/blocks/table/table-menu-model.d.ts +35 -8
  53. package/dist/components/blocks/table/table-menu-model.js +36 -17
  54. package/dist/components/blocks/text/TextEditableBlock.svelte +58 -11
  55. package/dist/components/blocks/text/delimiter-autopair.d.ts +71 -0
  56. package/dist/components/blocks/text/delimiter-autopair.js +216 -0
  57. package/dist/components/blocks/text/edge-policy-dispatch.d.ts +4 -0
  58. package/dist/components/blocks/text/edge-policy-dispatch.js +56 -2
  59. package/dist/components/blocks/text/live-selection-edit.js +27 -0
  60. package/dist/components/blocks/text/text-keydown.d.ts +7 -1
  61. package/dist/components/blocks/text/text-keydown.js +11 -1
  62. package/dist/components/blocks/text/text-render.d.ts +1 -1
  63. package/dist/components/blocks/text/text-render.js +3 -1
  64. package/dist/components/blocks/text/widget-interaction.d.ts +3 -0
  65. package/dist/components/blocks/text/widget-interaction.js +144 -22
  66. package/dist/components/drag-handle.d.ts +35 -0
  67. package/dist/components/drag-handle.js +126 -0
  68. package/dist/components/editor-root-focus.d.ts +19 -0
  69. package/dist/components/editor-root-focus.js +67 -0
  70. package/dist/components/editor-root-geometry.d.ts +39 -0
  71. package/dist/components/editor-root-geometry.js +91 -0
  72. package/dist/components/editor-root-keydown.d.ts +1 -1
  73. package/dist/components/editor-root-keydown.js +12 -3
  74. package/dist/components/editor-root-listeners.d.ts +10 -6
  75. package/dist/components/editor-root-listeners.js +23 -22
  76. package/dist/components/editor-root-mode-flip.d.ts +36 -0
  77. package/dist/components/editor-root-mode-flip.js +92 -0
  78. package/dist/components/image/ImageOverlayHost.svelte +14 -6
  79. package/dist/components/image/ImageProperties.svelte +512 -70
  80. package/dist/components/image/ImageProperties.svelte.d.ts +6 -1
  81. package/dist/components/image/ImageResizeHandles.svelte +59 -34
  82. package/dist/components/image/image-crop.d.ts +39 -0
  83. package/dist/components/image/image-crop.js +74 -0
  84. package/dist/components/image/image-edit-commit.d.ts +1 -0
  85. package/dist/components/image/image-edit-commit.js +21 -5
  86. package/dist/components/image/image-source-bytes.js +10 -3
  87. package/dist/components/image/image-widget-editing.js +1 -0
  88. package/dist/components/image/widget-dom.js +5 -1
  89. package/dist/components/link-card/link-card-commit.js +1 -1
  90. package/dist/components/lrd-map-gate.js +1 -1
  91. package/dist/components/menu/BlockMenu.svelte +315 -0
  92. package/dist/components/menu/BlockMenu.svelte.d.ts +34 -0
  93. package/dist/components/menu/MenuIcon.svelte +153 -0
  94. package/dist/components/menu/MenuIcon.svelte.d.ts +51 -0
  95. package/dist/components/menu/clipboard-actions.d.ts +12 -0
  96. package/dist/components/menu/clipboard-actions.js +42 -0
  97. package/dist/components/menu/default-context-actions.d.ts +15 -0
  98. package/dist/components/menu/default-context-actions.js +76 -0
  99. package/dist/components/menu/flyout-placement.d.ts +6 -0
  100. package/dist/components/menu/flyout-placement.js +25 -0
  101. package/dist/core/inline/format-toggle.d.ts +12 -4
  102. package/dist/core/inline/format-toggle.js +94 -40
  103. package/dist/core/inline/image-dimensions.d.ts +3 -0
  104. package/dist/core/inline/image-dimensions.js +46 -10
  105. package/dist/core/inline/inline-widgets.d.ts +11 -0
  106. package/dist/core/inline/scan/brackets.js +1 -0
  107. package/dist/core/inline/scan/plugin-syntax.d.ts +8 -0
  108. package/dist/core/inline/scan/plugin-syntax.js +15 -1
  109. package/dist/core/inline-render.d.ts +6 -0
  110. package/dist/core/inline-render.js +32 -0
  111. package/dist/core/nodes.d.ts +14 -0
  112. package/dist/cursor/edge-affinity.js +2 -1
  113. package/dist/cursor/overlay-remeasure.js +8 -0
  114. package/dist/cursor/reveal-source.js +7 -2
  115. package/dist/cursor/widget-offset.d.ts +6 -0
  116. package/dist/cursor/widget-offset.js +61 -4
  117. package/dist/debug/interaction-trace.d.ts +4 -0
  118. package/dist/debug/interaction-trace.js +15 -0
  119. package/dist/decorations/decoration-state.svelte.js +1 -1
  120. package/dist/editor-actions/ancestry-folds.d.ts +2 -2
  121. package/dist/editor-actions/ancestry-folds.js +1 -1
  122. package/dist/editor-actions/block-edit-scope.js +1 -1
  123. package/dist/editor-actions/commit/text-batch.d.ts +3 -2
  124. package/dist/editor-actions/commit/text-batch.js +1 -1
  125. package/dist/editor-actions/commit/undo-controller.js +4 -2
  126. package/dist/editor-actions/container-edit.js +2 -1
  127. package/dist/editor-actions/enter-completion.d.ts +2 -0
  128. package/dist/editor-actions/enter-completion.js +23 -2
  129. package/dist/editor-actions/inline-range-commit.js +1 -1
  130. package/dist/editor-actions/reorder-action.js +19 -10
  131. package/dist/editor-actions/reorder-drag.js +29 -1
  132. package/dist/editor-actions/replacement-focus.d.ts +1 -1
  133. package/dist/editor-actions/replacement-focus.js +1 -1
  134. package/dist/editor-actions/search-replace.js +1 -1
  135. package/dist/editor-actions/table-context.d.ts +4 -1
  136. package/dist/editor-actions/table-context.js +57 -1
  137. package/dist/editor-events.d.ts +3 -0
  138. package/dist/editor-keys.d.ts +33 -0
  139. package/dist/editor-props.d.ts +21 -12
  140. package/dist/index.d.ts +1 -1
  141. package/dist/plugin.d.ts +6 -0
  142. package/dist/plugin.js +11 -0
  143. package/dist/plugins/latex/BlockMath.svelte +264 -29
  144. package/dist/plugins/latex/BlockMath.svelte.d.ts +2 -0
  145. package/dist/plugins/latex/index.d.ts +2 -1
  146. package/dist/plugins/latex/latex-kind.js +36 -3
  147. package/dist/plugins/latex/math-completion.js +4 -1
  148. package/dist/plugins/latex/math-layout.d.ts +15 -0
  149. package/dist/plugins/latex/math-layout.js +13 -0
  150. package/dist/plugins/latex/math-source.d.ts +19 -0
  151. package/dist/plugins/latex/math-source.js +97 -0
  152. package/dist/plugins/latex/register.d.ts +11 -2
  153. package/dist/plugins/latex/register.js +3 -1
  154. package/dist/plugins/latex/renderer.d.ts +3 -3
  155. package/dist/plugins/latex/renderer.js +13 -6
  156. package/dist/reactivity/list-windowing.svelte.d.ts +8 -8
  157. package/dist/reactivity/list-windowing.svelte.js +49 -24
  158. package/dist/schema/block-completions.d.ts +8 -0
  159. package/dist/schema/block-completions.js +11 -0
  160. package/dist/schema/context-actions.d.ts +31 -0
  161. package/dist/schema/context-actions.js +23 -0
  162. package/dist/schema/fenced-code-raw.js +31 -1
  163. package/dist/schema/operations.d.ts +8 -1
  164. package/dist/schema/reserved-chords.js +25 -4
  165. package/dist/schema/table-cell-raw.d.ts +1 -1
  166. package/dist/schema/table-cell-raw.js +1 -1
  167. package/dist/selection/block-hit-test.js +3 -2
  168. package/dist/selection/char-endpoint-snap.js +1 -1
  169. package/dist/selection/clipboard-text.js +6 -1
  170. package/dist/selection/covered-block.d.ts +10 -0
  171. package/dist/selection/covered-block.js +24 -0
  172. package/dist/selection/cross-block/dispatch.d.ts +3 -0
  173. package/dist/selection/cross-block/dispatch.js +13 -1
  174. package/dist/selection/cross-block/format-range.d.ts +1 -1
  175. package/dist/selection/cross-block/format-range.js +4 -15
  176. package/dist/selection/cross-block/format-toggle.js +1 -1
  177. package/dist/selection/cross-block/keydown.js +1 -1
  178. package/dist/selection/cross-block/ops.js +1 -1
  179. package/dist/selection/cross-block/paste.js +32 -30
  180. package/dist/selection/cross-block/type-replace.d.ts +3 -2
  181. package/dist/selection/cross-block/type-replace.js +62 -13
  182. package/dist/selection/dead-space-caret.d.ts +10 -0
  183. package/dist/selection/dead-space-caret.js +47 -1
  184. package/dist/selection/double-click-trim.d.ts +17 -0
  185. package/dist/selection/double-click-trim.js +57 -0
  186. package/dist/selection/drag-pointer.d.ts +7 -2
  187. package/dist/selection/drag-pointer.js +61 -2
  188. package/dist/selection/gap-caret.js +1 -1
  189. package/dist/selection/keyboard-extend.js +1 -1
  190. package/dist/selection/path-lookup.js +1 -1
  191. package/dist/selection/range-delete-ceremony.js +4 -2
  192. package/dist/selection/range-delete-chrome.js +2 -1
  193. package/dist/selection/range-delete-table-coverage.js +2 -1
  194. package/dist/selection/range-delete-table.js +3 -2
  195. package/dist/selection/range-delete.js +30 -2
  196. package/dist/selection/selection-restore.js +1 -1
  197. package/dist/selection/selection-state.svelte.d.ts +6 -0
  198. package/dist/selection/selection-state.svelte.js +36 -1
  199. package/dist/selection/table-endpoint-snap.js +1 -1
  200. package/dist/selection/table-rect-extend.js +1 -1
  201. package/dist/styles/editor-theme.css +57 -28
  202. package/dist/styles/editor.css +217 -18
  203. package/dist/testing/container-conformance.js +2 -2
  204. package/dist/testing/inline-conformance.js +2 -1
  205. package/dist/tree-operations/blockquote.js +1 -1
  206. package/dist/tree-operations/chain-rebuild.d.ts +63 -0
  207. package/dist/tree-operations/chain-rebuild.js +142 -0
  208. package/dist/tree-operations/children.d.ts +1 -1
  209. package/dist/tree-operations/children.js +1 -1
  210. package/dist/tree-operations/cleanup.js +1 -1
  211. package/dist/tree-operations/content-write.d.ts +50 -0
  212. package/dist/tree-operations/content-write.js +263 -0
  213. package/dist/tree-operations/index.d.ts +8 -3
  214. package/dist/tree-operations/index.js +6 -2
  215. package/dist/tree-operations/list/exit-replacement.js +1 -1
  216. package/dist/tree-operations/list/unwrap-merge.js +3 -3
  217. package/dist/tree-operations/node-ops.d.ts +17 -234
  218. package/dist/tree-operations/node-ops.js +47 -1113
  219. package/dist/tree-operations/node-primitives.d.ts +75 -0
  220. package/dist/tree-operations/node-primitives.js +117 -0
  221. package/dist/tree-operations/paste/apply.js +1 -1
  222. package/dist/tree-operations/paste/body-write.d.ts +1 -1
  223. package/dist/tree-operations/paste/body-write.js +2 -2
  224. package/dist/tree-operations/paste/container-match.js +4 -2
  225. package/dist/tree-operations/paste/dispatch.js +2 -1
  226. package/dist/tree-operations/paste/find-enclosing-list.js +1 -1
  227. package/dist/tree-operations/paste/focus-target.d.ts +1 -1
  228. package/dist/tree-operations/paste/list-absorb.js +1 -1
  229. package/dist/tree-operations/paste/list-break-out.js +1 -1
  230. package/dist/tree-operations/paste/parent-scope.js +1 -1
  231. package/dist/tree-operations/paste/paste-replacement.js +1 -1
  232. package/dist/tree-operations/paste/replace-block-at-parent.js +1 -1
  233. package/dist/tree-operations/path-mutate.d.ts +1 -1
  234. package/dist/tree-operations/path-mutate.js +2 -1
  235. package/dist/tree-operations/reorder-unit.js +1 -1
  236. package/dist/tree-operations/reorder.d.ts +5 -2
  237. package/dist/tree-operations/reorder.js +55 -2
  238. package/dist/tree-operations/settle.d.ts +105 -0
  239. package/dist/tree-operations/settle.js +660 -0
  240. package/dist/tree-operations/table-grid-clipboard.d.ts +21 -0
  241. package/dist/tree-operations/table-grid-clipboard.js +90 -0
  242. package/dist/tree-operations/unshare.d.ts +15 -77
  243. package/dist/tree-operations/unshare.js +15 -166
  244. package/docs/guide/consumer-guide.md +126 -96
  245. package/docs/guide/plugin-api.md +31 -3
  246. package/docs/guide/plugin-guide.md +26 -3
  247. package/package.json +4 -2
  248. package/dist/components/blocks/code/CodeLanguageChip.svelte +0 -127
  249. package/dist/components/blocks/code/CodeLanguageChip.svelte.d.ts +0 -14
  250. package/dist/components/blocks/table/TableGrip.svelte +0 -91
  251. package/dist/components/blocks/table/TableGrip.svelte.d.ts +0 -8
  252. package/dist/components/blocks/table/table-drop-target.d.ts +0 -1
  253. package/dist/components/blocks/table/table-drop-target.js +0 -16
  254. package/dist/components/blocks/table/table-reorder-drag.d.ts +0 -78
  255. package/dist/components/blocks/table/table-reorder-drag.js +0 -97
@@ -45,6 +45,35 @@ export interface PastedImage {
45
45
  /** Host import hook for pasted images: resolves to the markdown to insert, or null
46
46
  * to skip that image. Called once per image file, in clipboard order. */
47
47
  export type PasteImageHook = (image: PastedImage) => Promise<string | null>;
48
+ /** What a code block hands its host when the run affordance is pressed. */
49
+ export interface CodeRunRequest {
50
+ /** The fence body alone — opener and closer lines excluded. */
51
+ code: string;
52
+ /** The opener's full info string, untrimmed of trailing attributes (`py {1-3}`). */
53
+ info: string;
54
+ /** Child indices from the document root to this block. */
55
+ path: number[];
56
+ }
57
+ /**
58
+ * Host hook for executing a code block. The editor runs nothing itself: installing this is
59
+ * what puts the run affordance on the block's rail, and the host owns everything after —
60
+ * the engine, the result, and where output goes. Absent, no run affordance renders.
61
+ */
62
+ export type RunCodeHook = (request: CodeRunRequest) => void;
63
+ /** One entry in a code block's overflow menu. `run` is called with the menu already closed. */
64
+ export interface CodeMenuItem {
65
+ id: string;
66
+ label: string;
67
+ run: () => void;
68
+ /** Renders dimmed and refuses activation. */
69
+ disabled?: boolean;
70
+ }
71
+ /**
72
+ * Host hook for the code block's overflow menu, consulted each time the menu opens so the
73
+ * items can read live state. Absent — or returning nothing — renders no overflow affordance:
74
+ * the editor has no app-level actions of its own to put there.
75
+ */
76
+ export type CodeMenuItemsHook = (request: CodeRunRequest) => readonly CodeMenuItem[];
48
77
  export type PresentationModeGetter = () => PresentationMode;
49
78
  /** The editor's theme name, as reflected to `data-editor-theme`. An open string:
50
79
  * built-ins are `'dark'`/`'light'`, and a consumer may name its own. */
@@ -150,6 +179,10 @@ export interface EditorPolicies {
150
179
  /** Set-once host import hook for image-bearing pastes. Required-nullable: a mount must
151
180
  * answer, and `undefined` deliberately leaves the paste on the text/plain path. */
152
181
  onPasteImage: PasteImageHook | undefined;
182
+ /** Set-once host execution hook; its presence is what renders the run affordance. */
183
+ onRunCode: RunCodeHook | undefined;
184
+ /** Set-once host menu hook; its presence is what renders the overflow affordance. */
185
+ codeMenuItems: CodeMenuItemsHook | undefined;
153
186
  /** Resolved image URLs that failed to load this session. One Set per instance, so a
154
187
  * failed load never suppresses another editor's broken-state recompute
155
188
  * (`components/image/widget-dom.ts`). */
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import type { Snippet } from 'svelte';
6
6
  import type { AnyBlockKind } from './core/nodes';
7
- import type { PasteImageHook, ResolveImageUrl, ResolveLinkUrl } from './editor-keys';
7
+ import type { CodeMenuItemsHook, PasteImageHook, ResolveImageUrl, ResolveLinkUrl, RunCodeHook } from './editor-keys';
8
8
  import type { ImageLoadPolicy } from './core/inline-render';
9
9
  import type { PresentationMode } from './presentation-mode';
10
10
  import type { KeybindingOverride } from './schema/keybinding-overrides';
@@ -27,14 +27,24 @@ export interface EditorProps {
27
27
  * in order and the markdown returned is inserted at the caret; `null` skips it.
28
28
  * Installing it takes the WHOLE paste — the clipboard's `text/plain` is not pasted. */
29
29
  onPasteImage?: PasteImageHook;
30
+ /** Execution hook for code blocks, set once at mount. The editor runs nothing itself:
31
+ * installing this is what puts the run affordance on a code block's rail, and the host
32
+ * owns the engine, the result, and where output goes. Absent, no run affordance renders. */
33
+ onRunCode?: RunCodeHook;
34
+ /** Overflow-menu hook for code blocks, set once at mount and consulted each time a menu
35
+ * opens so items can read live state. Absent, or returning nothing, renders no overflow
36
+ * affordance — the editor has no app-level actions of its own to offer there. */
37
+ codeMenuItems?: CodeMenuItemsHook;
30
38
  /** Host chrome rendered INSIDE the editor's scroll container, above the first block
31
39
  * (a title, properties panel, tag row). It scrolls away with the document rather than
32
40
  * pinning, which is what lets the editor keep its own scrollport and windowing. */
33
41
  header?: Snippet;
34
- /** Opt into the pointer affordances: the block drag handle and the table's row and column
35
- * grips (default off, so the surface stays gutter-free). A hover reveals them; touch, which
36
- * has none, shows them outright. Keyboard reorder (Alt+Arrow) and the cell menu are always
37
- * available and need no opt-in. */
42
+ /** The block drag handle (default on; reading mode never shows it). A hover reveals it;
43
+ * touch, which has none, shows it outright. Only the blocks a reader picks up whole carry
44
+ * one — code, tables, equations, diagrams, pictures, list items, dividers, cards — never
45
+ * prose (paragraph, heading, quote, note). `false` removes them, except on a picture,
46
+ * whose grip is the only pointer road to move it. Keyboard reorder (Alt+Arrow) is always
47
+ * available, as is the table's right-click cell menu. */
38
48
  blockDragHandles?: boolean;
39
49
  searchBar?: boolean;
40
50
  /** Where the editor's own find/replace bar renders. Default (absent) keeps it pinned inside
@@ -102,14 +112,13 @@ export interface EditorInstance {
102
112
  */
103
113
  insertMarkdown(md: string): boolean;
104
114
  /**
105
- * Run a command by id at the focused surface, no chord in the path, so a consumer's
106
- * `keybindings` rebind cannot rewire a toolbar button. `TOOLBAR_COMMANDS` names the built-in
107
- * ids; a plugin's global name resolves ahead of the focused block, its per-block one stays
108
- * chord-only. Semantics match the chord: one undo entry, same caret — over a cross-block range
109
- * a format toggle marks every block it touches, a table by its cells. False, and nothing mutates,
110
- * on an unknown id, in reading mode, with nothing focused, and on the link editor over a range.
115
+ * Run a command by id at the focused surface, or across a painted range where the id has a
116
+ * cross-block arm (a format toggle marks every block it touches, a table by its cells). False,
117
+ * and nothing mutates, on an unknown id, in reading mode, with nothing focused, and on the link
118
+ * editor over a range. `arg` reaches the arm as a keybinding's argument would (`heading.cycle`
119
+ * takes the level, 0 for plain text); an arm that takes none ignores it.
111
120
  */
112
- runCommand(commandId: string): boolean;
121
+ runCommand(commandId: string, arg?: unknown): boolean;
113
122
  /**
114
123
  * Whether `runCommand(id)` would reach that command's arm right now, asked at the seam that
115
124
  * would run it, so a host can grey a toolbar button out instead of hiding the affordance.
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export { default as Editor } from './components/Editor.svelte';
2
2
  export type { EditorProps, EditorInstance } from './editor-props';
3
3
  export type { EditorDiagnostics, InteractionTraceEntry } from './editor-props';
4
4
  export type { BlockComponent } from './block-component';
5
- export type { ResolveImageUrl, ResolveLinkUrl, PastedImage, PasteImageHook } from './editor-keys';
5
+ export type { ResolveImageUrl, ResolveLinkUrl, PastedImage, PasteImageHook, CodeRunRequest, RunCodeHook, CodeMenuItem, CodeMenuItemsHook } from './editor-keys';
6
6
  export type { ImageLoadPolicy } from './core/inline-render';
7
7
  export type { PresentationMode } from './presentation-mode';
8
8
  export type { SearchState, SearchOptions } from './search/search-state.svelte';
package/dist/plugin.d.ts CHANGED
@@ -29,9 +29,15 @@ export type { OuterBlockScan } from './schema/block-openers';
29
29
  export { OPENER_PRIORITIES } from './schema/opener-priorities';
30
30
  export { registerBlockCompleter } from './schema/block-completions';
31
31
  export type { BlockCompleter, CompletionResult } from './schema/block-completions';
32
+ export { registerLanguage, listLanguages } from './components/blocks/code/code-languages';
33
+ export type { LanguageGrammar } from './components/blocks/code/code-languages';
34
+ export { tokenizeBody as highlightCode } from './components/blocks/code/code-renderer';
35
+ export type { LanguageFn } from 'highlight.js';
32
36
  export type { CommandId } from './schema/commands';
33
37
  export type { KeyBinding } from './schema/keybindings';
34
38
  export { registerBlockCommand } from './schema/block-commands';
39
+ export { registerBlockContextActions } from './schema/context-actions';
40
+ export type { BlockContextAction, BlockActionContext, BlockContextActionProvider } from './schema/context-actions';
35
41
  export type { BlockCommandContext, BlockCommandHandler } from './schema/block-commands';
36
42
  export type { PluginCommandId, AnyCommandId } from './schema/command-id';
37
43
  export { registerGlobalCommand } from './schema/global-commands';
package/dist/plugin.js CHANGED
@@ -38,9 +38,20 @@ export { OPENER_PRIORITIES } from './schema/opener-priorities';
38
38
  // never type into existence: a completer reads one typed line and answers the lines that
39
39
  // complete it, plus where the caret seats inside the mint.
40
40
  export { registerBlockCompleter } from './schema/block-completions';
41
+ // ── Code-block languages (pre-freeze) ────────────────────────────────────────
42
+ // The registry behind fenced-code highlighting. The editor bootstraps a curated set (every
43
+ // grammar is bundle weight for every consumer), so a host needing more registers them itself,
44
+ // BEFORE mounting an editor: a block on screen re-tokenizes only when its bytes next change.
45
+ // An unregistered language is not an error — the fence still round-trips, just untokenized.
46
+ export { registerLanguage, listLanguages } from './components/blocks/code/code-languages';
47
+ // The code block's own tokenizer, for a plugin whose source surface wants the same highlighting
48
+ // (block math paints its LaTeX with it). Text-preserving: the fragment's textContent is `body`.
49
+ export { tokenizeBody as highlightCode } from './components/blocks/code/code-renderer';
41
50
  // ── Command mint (pre-freeze) ────────────────────────────────────────────────
42
51
  // A (kind, name) block-command mints a PluginCommandId; AnyCommandId spans built-in and minted.
43
52
  export { registerBlockCommand } from './schema/block-commands';
53
+ // The block context menu: a kind's right-click actions, empty unless something registers them.
54
+ export { registerBlockContextActions } from './schema/context-actions';
44
55
  // A global command is process-wide but runs against the dispatching instance's EditorContext.
45
56
  export { registerGlobalCommand } from './schema/global-commands';
46
57
  // ── Parse / serialize helpers (pre-freeze) ───────────────────────────────────
@@ -8,10 +8,32 @@
8
8
  // Render-primary editable leaf: all editing behavior lives in `createEditableLeaf`,
9
9
  // so this component owns only the render↔source swap visuals.
10
10
  import { createEditableLeaf, type BlockComponent, type NodeView } from '../../plugin';
11
+ // Plugin-local like the other labels here: bundled plugins import only the public barrel.
12
+ // The three ways the source and its preview can share the block while editing (see
13
+ // `math-layout.ts`); the host's plugin options pick the starting one. The toggle cycles
14
+ // them and names the NEXT layout.
15
+ type MathLayout = MathBlockLayout;
16
+ const LAYOUT_NEXT: Record<MathLayout, MathLayout> = {
17
+ split: 'stacked',
18
+ stacked: 'source',
19
+ source: 'split'
20
+ };
21
+ const LAYOUT_TITLE: Record<MathLayout, string> = {
22
+ split: 'Preview beside the source',
23
+ stacked: 'Preview below the source',
24
+ source: 'Source only'
25
+ };
11
26
  import { renderDisplayMath } from './math-renderer';
12
27
  import { mathDisplaySource } from './latex-kind';
28
+ import { completeBareMathSource, mathBodySpan, renderMathSource } from './math-source';
29
+ import { resolveDefaultLayout, type MathBlockLayout } from './math-layout';
13
30
 
14
- let { node, index, myPath = [] }: { node: NodeView; index: number; myPath?: number[] } = $props();
31
+ let {
32
+ node,
33
+ index,
34
+ myPath = [],
35
+ blockLayout = 'split'
36
+ }: { node: NodeView; index: number; myPath?: number[]; blockLayout?: MathBlockLayout } = $props();
15
37
 
16
38
  // eslint-disable-next-line no-useless-assignment -- <script module> counter read by the next instance mount
17
39
  const mountId = nextMountId++;
@@ -20,6 +42,9 @@
20
42
  let sourceEl: HTMLDivElement | undefined = $state();
21
43
  let renderEl: HTMLDivElement | undefined = $state();
22
44
  let revealed = $state(false);
45
+ // The in-flight source while revealed: a render-primary edit reaches the CST only on blur,
46
+ // so the live preview reads the surface, not the node. Null when nothing is in flight.
47
+ let draft = $state<string | null>(null);
23
48
 
24
49
  const leaf = createEditableLeaf({
25
50
  getNode: () => node,
@@ -30,16 +55,58 @@
30
55
  isRevealed: () => revealed,
31
56
  setRevealed: (value) => {
32
57
  revealed = value;
58
+ draft = null;
59
+ },
60
+ renderSource: renderMathSource,
61
+ completeBareSource: completeBareMathSource,
62
+ onSourceEdit: (text) => {
63
+ draft = text;
33
64
  }
34
65
  });
35
66
 
67
+ // While editing, the preview is a sibling of the surface that holds focus: its scrollbar (a
68
+ // wide equation overflows the half-width card) must not take that focus, since losing it is
69
+ // what folds the editor. Same device as the eye button; a fold click is a click while folded.
70
+ function keepSourceFocus(e: MouseEvent): void {
71
+ if (revealed) e.preventDefault();
72
+ }
73
+
74
+ // Per-instance and per-session: a reader who changes the layout is asking about THIS equation
75
+ // while they edit it, not setting a preference for the document. The starting layout is the
76
+ // host's: this editor's plugin options, else the factory's default.
77
+ // svelte-ignore state_referenced_locally
78
+ let layout = $state<MathLayout>(resolveDefaultLayout(leaf.getOptions(), blockLayout));
79
+ const previewOpen = $derived(layout !== 'source');
80
+
81
+ // The edits the leaf applies itself report through `onSourceEdit`; this is the native path
82
+ // (a composition's commit), where the highlight goes stale until repainted. The leaf's own
83
+ // handler runs first so the IME bookkeeping it owns is untouched.
84
+ function onSourceInput(e: Event): void {
85
+ leaf.surfaceProps.oninput();
86
+ if ((e as InputEvent).isComposing) return;
87
+ leaf.repaintSource();
88
+ draft = sourceEl?.textContent ?? null;
89
+ }
90
+
36
91
  // ── View rendering ──────────────────────────────────────────────────────────
37
92
 
38
93
  // Re-runs on every remount of the render div and on any source change; the
39
94
  // document-wide memo clones a cached node, so a repeat formula is cheap.
40
95
  $effect(() => {
41
- if (revealed || !renderEl) return;
42
- renderEl.replaceChildren(renderDisplayMath(mathDisplaySource(leaf.sourceText)).dom);
96
+ if (!renderEl) return;
97
+ // Runs while REVEALED as well: the split keeps a live preview beside the source, so the
98
+ // equation re-renders as it is typed rather than only when the source folds away.
99
+ const text = draft ?? leaf.sourceText;
100
+ const source = mathDisplaySource(text);
101
+ renderEl.replaceChildren(renderDisplayMath(source).dom);
102
+ // The span a press on the glyphs lands in: the descriptor's `caretTargetAtPoint` reads the
103
+ // rendered element alone and has no other way to the bytes behind it.
104
+ const body = mathBodySpan(text);
105
+ renderEl.dataset.bodyStart = String(body.start);
106
+ renderEl.dataset.bodyEnd = String(body.end);
107
+ // An equation with nothing in it renders nothing, which folded would be an invisible block
108
+ // the user cannot find to delete; it keeps the card's fill instead, like an empty fence.
109
+ renderEl.toggleAttribute('data-empty', source.trim() === '');
43
110
  renderCount += 1;
44
111
  renderEl.dataset.renderCount = String(renderCount);
45
112
  });
@@ -74,46 +141,214 @@
74
141
  } satisfies BlockComponent);
75
142
  </script>
76
143
 
77
- {#if revealed}
78
- <div
79
- bind:this={sourceEl}
80
- {...leaf.surfaceProps}
81
- class="math-block-source md-source-surface"
82
- aria-label="Math source"
83
- ></div>
84
- {:else}
85
- <div
86
- bind:this={renderEl}
87
- class="math-block-render"
88
- data-mount-id={mountId}
89
- role="button"
90
- tabindex="-1"
91
- aria-label="Math (click to edit)"
92
- {...leaf.renderProps}
93
- ></div>
94
- {/if}
144
+ <!-- Editing shows source AND render side by side rather than swapping one for the other: the
145
+ swap re-flowed the whole document on every click, and a preview that only appears after you
146
+ stop editing is the one you needed while typing. Each half is a card, and the eye sits in the
147
+ top-right of whichever card is showing. -->
148
+ <div
149
+ class="math-block"
150
+ class:math-block-editing={revealed}
151
+ class:math-block-split={revealed && layout === 'split'}
152
+ class:math-block-stacked={revealed && layout === 'stacked'}
153
+ >
154
+ {#if revealed}
155
+ <div class="math-block-card">
156
+ <div
157
+ bind:this={sourceEl}
158
+ {...leaf.surfaceProps}
159
+ oninput={onSourceInput}
160
+ class="math-block-source md-source-surface"
161
+ aria-label="Math source"
162
+ ></div>
163
+ <!-- The toggle keeps one seat: the top-right card. Side by side that is the preview; stacked
164
+ and source-only it is this card. -->
165
+ {#if layout !== 'split'}
166
+ {@render layoutToggle()}
167
+ {/if}
168
+ </div>
169
+ {/if}
170
+ {#if !revealed || previewOpen}
171
+ <div class="math-block-card">
172
+ <div
173
+ bind:this={renderEl}
174
+ class="math-block-render"
175
+ data-mount-id={mountId}
176
+ role="button"
177
+ tabindex="-1"
178
+ aria-label="Math (click to edit)"
179
+ {...leaf.renderProps}
180
+ onmousedown={keepSourceFocus}
181
+ ></div>
182
+ {#if revealed && layout === 'split'}
183
+ {@render layoutToggle()}
184
+ {/if}
185
+ </div>
186
+ {/if}
187
+ </div>
188
+
189
+ {#snippet layoutToggle()}
190
+ {@const next = LAYOUT_NEXT[layout]}
191
+ <button
192
+ type="button"
193
+ class="math-preview-toggle"
194
+ aria-label={LAYOUT_TITLE[next]}
195
+ title={LAYOUT_TITLE[next]}
196
+ onmousedown={(e) => e.preventDefault()}
197
+ onclick={() => (layout = next)}
198
+ >
199
+ <svg
200
+ viewBox="0 0 24 24"
201
+ width="13"
202
+ height="13"
203
+ fill="none"
204
+ stroke="currentColor"
205
+ stroke-width="1.75"
206
+ stroke-linecap="round"
207
+ stroke-linejoin="round"
208
+ aria-hidden="true"
209
+ >
210
+ {#if layout === 'split'}
211
+ <rect width="18" height="18" x="3" y="3" rx="2" /><path d="M12 3v18" />
212
+ {:else if layout === 'stacked'}
213
+ <rect width="18" height="18" x="3" y="3" rx="2" /><path d="M3 12h18" />
214
+ {:else}
215
+ <path
216
+ d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"
217
+ /><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242" /><path
218
+ d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"
219
+ /><path d="m2 2 20 20" />
220
+ {/if}
221
+ </svg>
222
+ </button>
223
+ {/snippet}
95
224
 
96
225
  <style>
97
- /* Deltas over the shared .md-source-surface (editor.css). */
226
+ /* At rest the render alone, laid out as a plain block so it centres exactly as it did
227
+ before any of this existed. Editing turns the block into two equal cards. */
228
+ .math-block-split {
229
+ display: grid;
230
+ grid-template-columns: 1fr 1fr;
231
+ align-items: stretch;
232
+ gap: 6px;
233
+ }
234
+
235
+ /* Stacked: the source over its preview, each the block's full width — for the equation too
236
+ long to read at half width. Not the default: it grows the block and reshuffles the page. */
237
+ .math-block-stacked {
238
+ display: grid;
239
+ grid-template-columns: 1fr;
240
+ gap: 6px;
241
+ }
242
+
243
+ /* The equation's cards are boxes like a code block's, and take the same stand-off from their
244
+ neighbours (editor.css, fencedCode). Padding, not margin: the height model measures the
245
+ host's box. */
246
+ :global(.block-host[data-block-kind='mathBlock']) {
247
+ padding-block: 6px;
248
+ }
249
+
250
+ /* Each half is its own card, and the containing block for its eye. */
251
+ .math-block-card {
252
+ position: relative;
253
+ min-width: 0;
254
+ border-radius: 8px;
255
+ }
256
+
257
+ /* Keyed on EDITING, not on the split: folding the preview away leaves one card, and a card
258
+ that lost its fill the moment it stood alone would read as having left edit mode. */
259
+ .math-block-editing .math-block-card {
260
+ display: flex;
261
+ align-items: center;
262
+ background: var(--color-bg-secondary, rgba(128, 128, 128, 0.12));
263
+ }
264
+
265
+ /* Deltas over the shared .md-source-surface (editor.css) — including its accent border,
266
+ which announced a state the card's own fill already carries. */
98
267
  .math-block-source {
99
268
  outline: none;
100
- padding: 12px;
101
- white-space: pre;
102
- overflow-x: auto;
103
- overflow-y: hidden;
269
+ padding: 10px 12px;
270
+ /* WRAP, rather than scroll sideways. The card is half the block's width, so any real
271
+ formula overflows it, and a horizontal scrollbar hides the very text being edited.
272
+ `pre-wrap` keeps the author's own line breaks and wraps only what is too long;
273
+ LaTeX carries no indentation structure for wrapping to destroy. */
274
+ white-space: pre-wrap;
275
+ overflow-wrap: anywhere;
276
+ /* LEFT, like every other source surface. Centring gave each line a different starting
277
+ x, which is exactly what makes multi-line LaTeX unreadable; the RENDER is the half
278
+ that is genuinely centred, and the pairing is what Overleaf and friends do. */
279
+ text-align: left;
280
+ background: transparent;
281
+ border-color: transparent;
282
+ border-radius: 8px;
283
+ }
284
+
285
+ /* The card stretches the source so its box fills the column; without it a short formula's
286
+ surface shrink-wraps and the caret only lands where the text is. */
287
+ .math-block-editing .math-block-card > .math-block-source {
288
+ flex: 1;
289
+ min-width: 0;
104
290
  }
105
291
 
106
292
  .math-block-render {
107
293
  display: block;
108
- padding: 8px 12px;
294
+ width: 100%;
295
+ padding: 10px 12px;
109
296
  text-align: center;
110
297
  cursor: text;
111
298
  border: 1px solid transparent;
112
- border-radius: 4px;
299
+ border-radius: 8px;
113
300
  overflow-x: auto;
114
301
  }
115
302
 
116
- .math-block-render:hover {
303
+ /* Folded and empty: the fill stays, so there is a box to see and click into. */
304
+ .math-block:not(.math-block-editing) .math-block-render[data-empty] {
117
305
  background: var(--color-bg-secondary, rgba(128, 128, 128, 0.12));
306
+ /* The editing card's height exactly — one source line at its size, plus its padding and
307
+ hairline — so folding and unfolding an empty equation moves nothing. */
308
+ min-height: calc(0.9em * 1.5 + 22px);
309
+ box-sizing: border-box;
310
+ }
311
+
312
+ /* At rest the render is the whole block and a hover tint is its only affordance; inside a
313
+ card the fill is already there, so the tint would double it. */
314
+ .math-block:not(.math-block-editing) .math-block-render:hover {
315
+ background: var(--color-bg-secondary, rgba(128, 128, 128, 0.12));
316
+ }
317
+
318
+ .math-preview-toggle {
319
+ position: absolute;
320
+ top: 4px;
321
+ right: 4px;
322
+ display: flex;
323
+ align-items: center;
324
+ justify-content: center;
325
+ width: 22px;
326
+ height: 22px;
327
+ padding: 0;
328
+ border: none;
329
+ border-radius: 5px;
330
+ background: transparent;
331
+ color: var(--color-ui-muted, #8f8f89);
332
+ cursor: pointer;
333
+ opacity: 0;
334
+ transition: opacity 120ms ease-out;
335
+ }
336
+
337
+ /* Transient like every other affordance here: the pointer over the block, or the eye itself
338
+ holding focus. */
339
+ .math-block:hover .math-preview-toggle,
340
+ .math-preview-toggle:focus-visible {
341
+ opacity: 1;
342
+ }
343
+
344
+ .math-preview-toggle:hover {
345
+ background: var(--color-ui-faint, rgba(255, 255, 255, 0.07));
346
+ color: var(--color-text-secondary, #cfcfca);
347
+ }
348
+
349
+ @media (prefers-reduced-motion: reduce) {
350
+ .math-preview-toggle {
351
+ transition: none;
352
+ }
118
353
  }
119
354
  </style>
@@ -1,8 +1,10 @@
1
1
  import { type NodeView } from '../../plugin';
2
+ import { type MathBlockLayout } from './math-layout';
2
3
  type $$ComponentProps = {
3
4
  node: NodeView;
4
5
  index: number;
5
6
  myPath?: number[];
7
+ blockLayout?: MathBlockLayout;
6
8
  };
7
9
  declare const BlockMath: import("svelte").Component<$$ComponentProps, {
8
10
  editable: true;
@@ -1,3 +1,4 @@
1
- export { latexPlugin } from './register';
1
+ export { latexPlugin, type LatexPluginOptions } from './register';
2
2
  export { MATH_INLINE, MATH_BLOCK, MATH_FENCE } from './latex-kind';
3
3
  export type { MathRenderer } from './math-renderer';
4
+ export type { MathBlockLayout } from './math-layout';
@@ -4,7 +4,7 @@
4
4
  * never imported here. Recognition is gated on registration, so with no extension
5
5
  * loaded parsing stays byte-identical to bare GFM.
6
6
  */
7
- import { createScanIndex, declarePluginInlineKind, declarePluginKind, registerInlineSyntax, registerInlineWidgetKind, registerBlockKind, registerBlockOpener, isInlineKindDeclared, simpleLeafClosure, matchFenceOpen, matchFenceClose, OPENER_PRIORITIES } from '../../plugin';
7
+ import { caretOffsetAtPoint, createScanIndex, declarePluginInlineKind, declarePluginKind, registerInlineSyntax, registerInlineWidgetKind, registerBlockKind, registerBlockOpener, isInlineKindDeclared, simpleLeafClosure, matchFenceOpen, matchFenceClose, OPENER_PRIORITIES } from '../../plugin';
8
8
  import MathInline from './MathInline.svelte';
9
9
  import { registerMathBlockCompleter } from './math-completion';
10
10
  export const MATH_INLINE = 'math';
@@ -35,6 +35,10 @@ function recognizeMath(raw, pos, end, kind) {
35
35
  const opener = raw[afterOpen];
36
36
  if (isWhitespace(opener) || isDigit(opener))
37
37
  return null;
38
+ // `$$` is the display fence, or the empty pair a keystroke just closed: never an inline
39
+ // opener, or its closer search would jump to the far end of the next formula on the line.
40
+ if (opener === '$')
41
+ return null;
38
42
  // The index spans the whole block, so `end` decides the claim: a closer past the
39
43
  // scan range leaves the `$` literal.
40
44
  const close = firstCloserFrom(raw, pos + 2);
@@ -49,11 +53,18 @@ export function registerMathInline() {
49
53
  if (isInlineKindDeclared(MATH_INLINE))
50
54
  return;
51
55
  const kind = declarePluginInlineKind(MATH_INLINE);
52
- registerInlineSyntax('$', (raw, pos, end) => recognizeMath(raw, pos, end, kind));
56
+ registerInlineSyntax('$', (raw, pos, end) => recognizeMath(raw, pos, end, kind), {
57
+ autoPair: true
58
+ });
53
59
  registerInlineWidgetKind(kind, {
54
60
  isWidget: () => true,
55
61
  component: MathInline,
56
- editing: { revealSource: true }
62
+ editing: {
63
+ revealSource: true,
64
+ // `$…$`: one delimiter each side, so a revealing click seats the caret on the last
65
+ // character of the formula rather than past its closing `$`.
66
+ revealContentSpan: (source) => source.length >= 2 ? { start: 1, end: source.length - 1 } : null
67
+ }
57
68
  });
58
69
  }
59
70
  // ── Rendered display source ────────────────────────────────────────────────────
@@ -101,6 +112,7 @@ export function registerMathBlock() {
101
112
  // edge can grow a sibling.
102
113
  gapEdges: 'both',
103
114
  conformanceFixture: '$$\nx^2\n$$\n',
115
+ caretTargetAtPoint: mathCaretAtPoint,
104
116
  closure: simpleLeafClosure({
105
117
  focus: {
106
118
  mode: 'implemented',
@@ -155,6 +167,26 @@ export function registerMathBlock() {
155
167
  // Co-registered so one install teaches both forms (the admonition/githubAlert precedent).
156
168
  registerMathFence();
157
169
  }
170
+ /** Where a press on the folded equation puts the caret. KaTeX paints glyphs, not source bytes,
171
+ * so the press walks the body span in proportion to how far along the equation it fell; the
172
+ * fence lines carry no glyph of their own. */
173
+ function mathCaretAtPoint(blockEl, clientX, clientY) {
174
+ const render = blockEl.querySelector('.math-block-render');
175
+ if (!render)
176
+ return null;
177
+ const start = Number(render.dataset.bodyStart);
178
+ const end = Number(render.dataset.bodyEnd);
179
+ if (!Number.isFinite(start) || !Number.isFinite(end))
180
+ return null;
181
+ // `.katex-html` is the painted half: its MathML twin is clipped to a pixel, and the pair
182
+ // measured together answers for a point no reader aimed at.
183
+ const glyphs = render.querySelector('.katex-html');
184
+ const along = glyphs ? caretOffsetAtPoint(glyphs, clientX, clientY) : null;
185
+ const total = glyphs?.textContent?.length ?? 0;
186
+ if (along === null || total === 0)
187
+ return { path: [], offset: end };
188
+ return { path: [], offset: start + Math.round((along / total) * (end - start)) };
189
+ }
158
190
  // ── Fenced ```math display math ─────────────────────────────────────────────────
159
191
  // GitHub's third math form: a source-holding leaf like the `$$` block, rendered by
160
192
  // the same component.
@@ -170,6 +202,7 @@ export function registerMathFence() {
170
202
  editable: true,
171
203
  supportsInline: false,
172
204
  gapEdges: 'both',
205
+ caretTargetAtPoint: mathCaretAtPoint,
173
206
  conformanceFixture: '```math\nx^2\n```\n',
174
207
  closure: simpleLeafClosure({
175
208
  focus: {
@@ -15,5 +15,8 @@ export function tryCompleteMathBlock(line) {
15
15
  export function registerMathBlockCompleter(kind) {
16
16
  if (isBlockCompleterRegistered(kind))
17
17
  return;
18
- registerBlockCompleter(kind, { tryComplete: tryCompleteMathBlock });
18
+ // On type as well as at Enter: a lone `$$` can only be the pair's opener, so the block forms
19
+ // as the second `$` lands — the way a typed ` ``` ` is a fence at once — with the caret on
20
+ // the body line. The one-line `$$x$$` form is still reachable by typing into that body.
21
+ registerBlockCompleter(kind, { tryComplete: tryCompleteMathBlock, onType: true });
19
22
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * How a `$$` block lays out its source and preview while it is being edited. `split` (side by
3
+ * side) is the default because it moves the page least; `stacked` suits long equations; `source`
4
+ * shows no preview. A host picks the default through `latexPlugin({ blockLayout })`, and the
5
+ * block's own toggle still cycles from there.
6
+ */
7
+ export type MathBlockLayout = 'split' | 'stacked' | 'source';
8
+ export declare const MATH_BLOCK_LAYOUTS: readonly MathBlockLayout[];
9
+ export declare function isMathBlockLayout(value: unknown): value is MathBlockLayout;
10
+ /**
11
+ * The starting layout: this editor's per-instance plugin options first (`{ plugin, options:
12
+ * { blockLayout } }`), then the factory's own default (`latexPlugin({ blockLayout })`), then
13
+ * `split`. An unknown value falls through rather than throwing.
14
+ */
15
+ export declare function resolveDefaultLayout(options: unknown, fallback?: MathBlockLayout): MathBlockLayout;
@@ -0,0 +1,13 @@
1
+ export const MATH_BLOCK_LAYOUTS = ['split', 'stacked', 'source'];
2
+ export function isMathBlockLayout(value) {
3
+ return MATH_BLOCK_LAYOUTS.includes(value);
4
+ }
5
+ /**
6
+ * The starting layout: this editor's per-instance plugin options first (`{ plugin, options:
7
+ * { blockLayout } }`), then the factory's own default (`latexPlugin({ blockLayout })`), then
8
+ * `split`. An unknown value falls through rather than throwing.
9
+ */
10
+ export function resolveDefaultLayout(options, fallback = 'split') {
11
+ const declared = options?.blockLayout;
12
+ return isMathBlockLayout(declared) ? declared : fallback;
13
+ }