@voithos-labs/aragonite 0.10.3 → 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.
- package/dist/a11y-strings.d.ts +1 -0
- package/dist/a11y-strings.js +1 -0
- package/dist/components/BlockDragHandle.svelte +2 -0
- package/dist/components/BlockHost.svelte +2 -2
- package/dist/components/Editor.svelte +56 -3
- package/dist/components/SelectionOverlay.svelte +17 -23
- package/dist/components/SelectionOverlay.svelte.d.ts +1 -1
- package/dist/components/blocks/code/CodeBlockRail.svelte +42 -24
- package/dist/components/blocks/code/code-bootstrap.js +12 -0
- package/dist/components/blocks/code/code-languages.d.ts +5 -5
- package/dist/components/blocks/code/code-languages.js +20 -12
- package/dist/components/blocks/editable-leaf.d.ts +3 -3
- package/dist/components/blocks/editable-leaf.js +6 -2
- package/dist/components/blocks/list/ListBlock.svelte +1 -0
- package/dist/components/blocks/list/ListItemBlock.svelte +12 -3
- package/dist/components/blocks/list/ListItemBlock.svelte.d.ts +2 -0
- package/dist/components/blocks/text/click-snap-guard.d.ts +3 -0
- package/dist/components/blocks/text/click-snap-guard.js +8 -0
- package/dist/components/blocks/text/edge-policy-dispatch.js +33 -18
- package/dist/components/blocks/text/live-selection-edit.d.ts +5 -5
- package/dist/components/blocks/text/live-selection-edit.js +10 -7
- package/dist/components/blocks/text/text-clipboard.js +2 -2
- package/dist/components/blocks/text/widget-interaction.js +45 -23
- package/dist/components/drag-handle.d.ts +6 -0
- package/dist/components/drag-handle.js +8 -0
- package/dist/components/editor-root-geometry.d.ts +1 -1
- package/dist/components/editor-root-geometry.js +1 -1
- package/dist/components/editor-root-listeners.d.ts +0 -7
- package/dist/components/editor-root-listeners.js +0 -22
- package/dist/components/image/ImageResizeHandles.svelte +18 -10
- package/dist/components/menu/SelectionToolbar.svelte +385 -0
- package/dist/components/menu/SelectionToolbar.svelte.d.ts +16 -0
- package/dist/core/inline/inline-widgets.d.ts +12 -4
- package/dist/core/inline/inline-widgets.js +5 -0
- package/dist/core/inline/transparency.js +3 -3
- package/dist/cursor/height-oracle.d.ts +2 -3
- package/dist/cursor/height-oracle.js +0 -1
- package/dist/cursor/scroll-hold.d.ts +10 -0
- package/dist/cursor/scroll-hold.js +22 -0
- package/dist/cursor/scrollport.d.ts +9 -0
- package/dist/cursor/scrollport.js +30 -1
- package/dist/cursor/visual-lines.d.ts +5 -4
- package/dist/cursor/visual-lines.js +56 -11
- package/dist/cursor/widget-edge-snap.d.ts +28 -0
- package/dist/cursor/widget-edge-snap.js +38 -0
- package/dist/cursor/widget-offset.d.ts +3 -0
- package/dist/cursor/widget-offset.js +10 -0
- package/dist/decorations/reserved-attrs.js +1 -0
- package/dist/editor-actions/focus/focus-dispatch.js +7 -4
- package/dist/editor-actions/focus/focus-landing.d.ts +10 -1
- package/dist/editor-actions/focus/focus-landing.js +20 -6
- package/dist/editor-actions/plugin/container.d.ts +7 -0
- package/dist/editor-actions/plugin/container.js +3 -1
- package/dist/editor-props.d.ts +3 -0
- package/dist/plugin.d.ts +2 -1
- package/dist/plugin.js +6 -1
- package/dist/plugins/latex/latex-kind.js +48 -31
- package/dist/plugins/latex/math-source.d.ts +3 -3
- package/dist/plugins/latex/math-source.js +32 -26
- package/dist/plugins/mermaid/MermaidBlock.svelte +19 -9
- package/dist/plugins/parrot/ParrotBlock.svelte +3 -1
- package/dist/reactivity/list-windowing.svelte.js +26 -6
- package/dist/schema/commands.d.ts +9 -5
- package/dist/schema/commands.js +12 -7
- package/dist/schema/operations.d.ts +1 -1
- package/dist/schema/reserved-chords.js +12 -0
- package/dist/selection/cross-block/keydown.js +2 -28
- package/dist/selection/cross-block/paste.js +7 -13
- package/dist/selection/cross-block/type-replace.js +5 -15
- package/dist/selection/dead-space-caret.d.ts +3 -0
- package/dist/selection/dead-space-caret.js +12 -2
- package/dist/selection/drag-pointer.d.ts +19 -0
- package/dist/selection/drag-pointer.js +42 -0
- package/dist/selection/keyboard-extend.d.ts +3 -2
- package/dist/selection/keyboard-extend.js +3 -2
- package/dist/selection/multi-click.d.ts +42 -0
- package/dist/selection/multi-click.js +203 -0
- package/dist/selection/native-bridge.d.ts +3 -0
- package/dist/selection/native-bridge.js +31 -2
- package/dist/selection/path-lookup.d.ts +10 -2
- package/dist/selection/path-lookup.js +10 -3
- package/dist/selection/pointer-gesture.d.ts +9 -0
- package/dist/selection/pointer-gesture.js +11 -0
- package/dist/selection/primitives.d.ts +7 -0
- package/dist/selection/primitives.js +19 -1
- package/dist/selection/range-delete.d.ts +4 -0
- package/dist/selection/range-delete.js +1 -1
- package/dist/selection/selection-drop.d.ts +34 -0
- package/dist/selection/selection-drop.js +253 -0
- package/dist/styles/editor-theme.css +18 -3
- package/dist/styles/editor.css +64 -13
- package/dist/tree-operations/paste/replace-block-at-parent.d.ts +3 -1
- package/dist/tree-operations/paste/replace-block-at-parent.js +4 -1
- package/dist/tree-operations/paste/replacement-parse.d.ts +17 -0
- package/dist/tree-operations/paste/replacement-parse.js +22 -0
- package/docs/guide/consumer-guide.md +10 -8
- package/docs/guide/plugin-api.md +54 -38
- package/docs/guide/plugin-guide.md +31 -22
- package/package.json +7 -7
- package/dist/selection/double-click-trim.d.ts +0 -17
- package/dist/selection/double-click-trim.js +0 -57
package/docs/guide/plugin-api.md
CHANGED
|
@@ -40,6 +40,7 @@ The groups, in page order:
|
|
|
40
40
|
| [Events](#events) | What the editor tells your plugin has happened, and the shapes it says it in |
|
|
41
41
|
| [Rects](#rects) | Where things are on screen: block boxes, ranges, the caret, scrolling to a block |
|
|
42
42
|
| [Caret geometry](#caret-geometry) | Answering where a press inside your block puts the caret |
|
|
43
|
+
| [Pointer gestures](#pointer-gestures) | Keeping a drag that belongs to your block (a pan, a brush) from starting a selection |
|
|
43
44
|
| [Selection geometry](#selection-geometry) | The shapes that describe what the user has selected |
|
|
44
45
|
| [Parse and serialize](#parse-and-serialize) | Markdown in, tree out, and back again |
|
|
45
46
|
| [Grammar scanners](#grammar-scanners) | The editor's own code-fence, HTML-tag, and blockquote rules, reusable so you never fork them |
|
|
@@ -153,17 +154,17 @@ _(pre-freeze / unstable)_ The `:::name` grammar itself is the [directives guide]
|
|
|
153
154
|
|
|
154
155
|
_(pre-freeze / unstable)_ A container's **chrome** is its own furniture: the border, the title line, an icon if you like. The factory hides everything else (child-list state, ancestor wiring, the mounting of only what's visible), so your component only has to supply the chrome. Worked end to end in [the walkthrough](plugin-guide.md#walkthrough-a-conspiracy-container-end-to-end).
|
|
155
156
|
|
|
156
|
-
| Export | Role
|
|
157
|
-
| ----------------------------------------------- |
|
|
158
|
-
| `createContainerBlock` | Wire a nested-child-list container so your component is as thin as the built-in blockquote's
|
|
159
|
-
| `BlockList` | The child-list component your container renders, spread with the factory's props, as a direct child of your box
|
|
160
|
-
| `registerChromeLeaf` | Register a container's title or summary line as a kind of its own, with a sensible default keymap
|
|
161
|
-
| `chromeChild` | Build the reserved child-0 node for that line: the title text plus its newline (an empty title keeps the bare newline)
|
|
162
|
-
| `isCollapsedContainer` | Read a container's collapse state through its descriptor, so your component and the editor's own walks agree
|
|
163
|
-
| `ContainerBlock`, `ContainerBlockComponent` | What the factory returns (the child-list props, the `containerApi` you publish, the keydown handler, plus the commit, focus-exit, mode, theme and
|
|
164
|
-
| `ContainerBlockDeps`, `ContainerBlockListProps` | The factory's inputs, live getters rather than captured values, and the props `BlockList` takes
|
|
165
|
-
| `RefSlots` | The per-child reference accessors the child-list props carry
|
|
166
|
-
| `ChromeLeafOptions` | `registerChromeLeaf`'s options: a CSS class for styling the line, keymap overrides, the merge role
|
|
157
|
+
| Export | Role |
|
|
158
|
+
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
159
|
+
| `createContainerBlock` | Wire a nested-child-list container so your component is as thin as the built-in blockquote's |
|
|
160
|
+
| `BlockList` | The child-list component your container renders, spread with the factory's props, as a direct child of your box |
|
|
161
|
+
| `registerChromeLeaf` | Register a container's title or summary line as a kind of its own, with a sensible default keymap |
|
|
162
|
+
| `chromeChild` | Build the reserved child-0 node for that line: the title text plus its newline (an empty title keeps the bare newline) |
|
|
163
|
+
| `isCollapsedContainer` | Read a container's collapse state through its descriptor, so your component and the editor's own walks agree |
|
|
164
|
+
| `ContainerBlock`, `ContainerBlockComponent` | What the factory returns (the child-list props, the `containerApi` you publish, the keydown handler, plus the commit, focus-exit, mode, theme, options and scroll-hold entries), and the shape that `containerApi` must satisfy |
|
|
165
|
+
| `ContainerBlockDeps`, `ContainerBlockListProps` | The factory's inputs, live getters rather than captured values, and the props `BlockList` takes |
|
|
166
|
+
| `RefSlots` | The per-child reference accessors the child-list props carry |
|
|
167
|
+
| `ChromeLeafOptions` | `registerChromeLeaf`'s options: a CSS class for styling the line, keymap overrides, the merge role |
|
|
167
168
|
|
|
168
169
|
### Editable-leaf authoring
|
|
169
170
|
|
|
@@ -180,17 +181,18 @@ _(pre-freeze / unstable)_ The container factory's sibling for leaves; the full s
|
|
|
180
181
|
|
|
181
182
|
### Code-block languages
|
|
182
183
|
|
|
183
|
-
_(pre-freeze / unstable)_ The syntax-highlighting registry behind fenced code. The editor bootstraps a curated
|
|
184
|
+
_(pre-freeze / unstable)_ The syntax-highlighting registry behind fenced code. The editor bootstraps a curated couple dozen (the usual suspects: javascript, python, rust, bash, sql, the C family, and friends) plus their aliases, because every grammar is static bundle weight for every consumer. `listLanguages()` tells you exactly which ones you have; a host needing more registers them itself.
|
|
184
185
|
|
|
185
|
-
Register **before mounting an editor**: a block already on screen re-tokenizes only when its own bytes next change. An unregistered language is not an error
|
|
186
|
+
Register **before mounting an editor**: a block already on screen re-tokenizes only when its own bytes next change. An unregistered language is not an error: the fence still authors, commits and round-trips, and its body renders untokenized.
|
|
186
187
|
|
|
187
|
-
| Export
|
|
188
|
-
|
|
|
189
|
-
| `registerLanguage`
|
|
190
|
-
| `listLanguages`
|
|
191
|
-
| `
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
188
|
+
| Export | Role |
|
|
189
|
+
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
190
|
+
| `registerLanguage` | Add a grammar under a name, with optional aliases; idempotent, so a repeat call with the same name is a no-op |
|
|
191
|
+
| `listLanguages` | Every registered language once, under its canonical name, sorted: the rows the code block's language picker offers |
|
|
192
|
+
| `getLanguageAliases` | The other spellings a language answers to, asked by any of them: what a picker's filter matches on |
|
|
193
|
+
| `highlightCode` | The code block's tokenizer: `(body, language)` to a text-preserving fragment of `code-tok-*` spans, for a plugin's own source surface |
|
|
194
|
+
| `LanguageGrammar` | The registry's read shape: the resolved name and its definition |
|
|
195
|
+
| `LanguageFn` | highlight.js's grammar-definition type, re-exported so you needn't import highlight.js directly (you hold it only as a transitive dep) |
|
|
194
196
|
|
|
195
197
|
```ts
|
|
196
198
|
import { registerLanguage } from '@voithos-labs/aragonite/plugin';
|
|
@@ -199,29 +201,29 @@ import elixir from 'highlight.js/lib/languages/elixir';
|
|
|
199
201
|
registerLanguage('elixir', elixir, ['ex', 'exs']);
|
|
200
202
|
```
|
|
201
203
|
|
|
202
|
-
|
|
204
|
+
The picker shows `elixir` once, and `ex` and `exs` are search keys for that row, so a user typing `ex` finds it without knowing the full name.
|
|
203
205
|
|
|
204
206
|
### Inline authoring
|
|
205
207
|
|
|
206
208
|
_(pre-freeze / unstable)_ Syntax inside a paragraph: recognize it at a trigger character, render it as an **atomic widget** (one indivisible rendered thing the caret can sit beside but not inside), give it an editing policy. A **rung** is one level in the ordered ladder of recognizers a trigger consults. The render paths and the tier's limits: [Inline kinds](plugin-guide.md#inline-kinds).
|
|
207
209
|
|
|
208
|
-
| Export | Role
|
|
209
|
-
| --------------------------------------------------------- |
|
|
210
|
-
| `declarePluginInlineKind` | Mint an inline kind, the one-level-down twin of `declarePluginKind`
|
|
211
|
-
| `declaredPluginInlineKind` | Recover a declared inline kind in another module
|
|
212
|
-
| `isInlineKindDeclared` | The is-it-there check for an inline kind
|
|
213
|
-
| `registerInlineSyntax` | Hook the inline scanner on one trigger character with your recognizer; a reserved trigger (one a built-in owns, like `[`) takes a prefix rung
|
|
214
|
-
| `INLINE_PRIORITIES` | The inline ladder, lower consulted first: `prefixOverride` outranks a reserved trigger's built-in case, `plugin` is the bare-trigger default
|
|
215
|
-
| `InlineSyntaxRecognizer` | The recognizer contract: inspect the raw at the trigger, claim a span by returning a node, or decline with null
|
|
216
|
-
| `InlineSyntaxOptions` | The options bag: the multi-character `prefix`, the `priority`, `rewriteImage`, and `autoPair`
|
|
217
|
-
| `ImageSyntaxRewriter`, `ImageFields` | The `rewriteImage` contract, for a rung whose recognizer builds built-in image nodes and must write edits back in its own syntax, and the edited fields it receives
|
|
218
|
-
| `registerInlineWidgetKind` | Render an inline kind as a live atomic widget: a Svelte `component` (recommended) or a hand-built `buildWidget`, never both
|
|
219
|
-
| `mintWidgetShell` | Mint the marked, source-stamped span a `buildWidget` returns; its attributes are what the caret's position walk reads
|
|
220
|
-
| `PluginInlineKind`, `InlineNode` | The inline kind type, and the node your recognizer builds
|
|
221
|
-
| `InlineWidgetDescriptor` | The widget registration: the is-this-a-widget test, one render path, the editing policy
|
|
222
|
-
| `InlineWidgetComponentProps` | A component widget's props: frozen `{ inline, source }`, plus live getters for the mode, the theme, the document, and the content version, and `navigateTo` to jump to another block, optionally at an offset in it (aim at a leaf: a container path scrolls into view but seats no caret)
|
|
223
|
-
| `InlineWidgetEditingPolicy`, `InlineWidgetEditingContext` | The policy (reveal source on caret entry, where the content sits inside the delimiters so a
|
|
224
|
-
| `isWidgetActivationClick` | Whether a click activates a widget: a Ctrl/Cmd chord while editing, a plain click in reading mode
|
|
210
|
+
| Export | Role |
|
|
211
|
+
| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
212
|
+
| `declarePluginInlineKind` | Mint an inline kind, the one-level-down twin of `declarePluginKind` |
|
|
213
|
+
| `declaredPluginInlineKind` | Recover a declared inline kind in another module |
|
|
214
|
+
| `isInlineKindDeclared` | The is-it-there check for an inline kind |
|
|
215
|
+
| `registerInlineSyntax` | Hook the inline scanner on one trigger character with your recognizer; a reserved trigger (one a built-in owns, like `[`) takes a prefix rung |
|
|
216
|
+
| `INLINE_PRIORITIES` | The inline ladder, lower consulted first: `prefixOverride` outranks a reserved trigger's built-in case, `plugin` is the bare-trigger default |
|
|
217
|
+
| `InlineSyntaxRecognizer` | The recognizer contract: inspect the raw at the trigger, claim a span by returning a node, or decline with null |
|
|
218
|
+
| `InlineSyntaxOptions` | The options bag: the multi-character `prefix`, the `priority`, `rewriteImage`, and `autoPair` |
|
|
219
|
+
| `ImageSyntaxRewriter`, `ImageFields` | The `rewriteImage` contract, for a rung whose recognizer builds built-in image nodes and must write edits back in its own syntax, and the edited fields it receives |
|
|
220
|
+
| `registerInlineWidgetKind` | Render an inline kind as a live atomic widget: a Svelte `component` (recommended) or a hand-built `buildWidget`, never both |
|
|
221
|
+
| `mintWidgetShell` | Mint the marked, source-stamped span a `buildWidget` returns; its attributes are what the caret's position walk reads |
|
|
222
|
+
| `PluginInlineKind`, `InlineNode` | The inline kind type, and the node your recognizer builds |
|
|
223
|
+
| `InlineWidgetDescriptor` | The widget registration: the is-this-a-widget test, one render path, the editing policy |
|
|
224
|
+
| `InlineWidgetComponentProps` | A component widget's props: frozen `{ inline, source }`, plus live getters for the mode, the theme, the document, and the content version, and `navigateTo` to jump to another block, optionally at an offset in it (aim at a leaf: a container path scrolls into view but seats no caret) |
|
|
225
|
+
| `InlineWidgetEditingPolicy`, `InlineWidgetEditingContext` | The policy (reveal source on caret entry, where the content sits inside the delimiters, which offset a press on the rendered widget names via `revealOffsetAtPoint` so a click seats the caret where it landed, delete granularity, edge behavior, a selected-key handler, whether the widget claims the activation click), and the context that handler receives |
|
|
226
|
+
| `isWidgetActivationClick` | Whether a click activates a widget: a Ctrl/Cmd chord while editing, a plain click in reading mode |
|
|
225
227
|
|
|
226
228
|
### Commands and keybindings
|
|
227
229
|
|
|
@@ -292,6 +294,20 @@ _(pre-freeze / unstable)_ What a kind fills its descriptor's `caretTargetAtPoint
|
|
|
292
294
|
| `CaretTarget` | What the hook answers: the child path to the leaf (empty when your block is the leaf) and the offset inside it |
|
|
293
295
|
| `CURSOR_END`, `CursorEnd` | The offset meaning "wherever that leaf ends", and its type; a plain `0` is the other end, since that one is a real offset |
|
|
294
296
|
|
|
297
|
+
### Pointer gestures
|
|
298
|
+
|
|
299
|
+
_(pre-freeze / unstable)_ One attribute, for a block whose drags are its own (a diagram you pan, a canvas you draw on). Without it the editor reads the press as the start of a selection, and your pan runs under a painted block range. Calling `stopPropagation()` in your own handler doesn't help: Svelte delivers pointer events from the app root, so the editor's listener has already run.
|
|
300
|
+
|
|
301
|
+
| Export | Role |
|
|
302
|
+
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
303
|
+
| `POINTER_GESTURE_ATTR` | Put it on the element whose drags are yours. A press inside it is left alone by drag-to-select, and by the double and triple click too, as long as the element sits outside any `contenteditable` (an island inside your own editable text is the inline widget's door instead). The editor reads it at press time, so declare it always or only while your gesture is armed |
|
|
304
|
+
|
|
305
|
+
```svelte
|
|
306
|
+
<div class="viewport" {...{ [POINTER_GESTURE_ATTR]: focused ? '' : undefined }} onpointerdown={beginPan}>
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
That is what mermaid does: unfocused, a drag on the diagram selects like it would on any block; focused, it pans.
|
|
310
|
+
|
|
295
311
|
### Selection geometry
|
|
296
312
|
|
|
297
313
|
_(pre-freeze / unstable)_ The selection shapes a decoration source or geometry consumer reads.
|
|
@@ -260,7 +260,9 @@ cNo.....................................oc
|
|
|
260
260
|
font-size: 1.1em;
|
|
261
261
|
line-height: 1.1;
|
|
262
262
|
letter-spacing: 0.05em;
|
|
263
|
-
/* one frame tall, in the reel's own rows so a step lands on the next frame exactly
|
|
263
|
+
/* one frame tall, in the reel's own rows so a step lands on the next frame exactly; the
|
|
264
|
+
em line is the same height for engines without lh (Safari before 16.4) */
|
|
265
|
+
height: calc(var(--parrot-rows) * 1.1em);
|
|
264
266
|
height: calc(var(--parrot-rows) * 1lh);
|
|
265
267
|
/* wider than a phone column, and the editor root pans if it isn't contained; the bar
|
|
266
268
|
would sit across the bird, which is decoration rather than a pane to scroll */
|
|
@@ -878,20 +880,25 @@ Three rules for that file, each earned the hard way:
|
|
|
878
880
|
|
|
879
881
|
The factory returns more than the walkthrough destructures:
|
|
880
882
|
|
|
881
|
-
| Return
|
|
882
|
-
|
|
|
883
|
-
| `updateOwnMetadata`
|
|
884
|
-
| `moveFocusOut`
|
|
885
|
-
| `getPresentationMode`
|
|
886
|
-
| `getTheme`
|
|
887
|
-
| `getOptions`
|
|
883
|
+
| Return | When you reach for it |
|
|
884
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
885
|
+
| `updateOwnMetadata` | Your component writes its own node's metadata (a collapse toggle, an edited setting). The sanctioned commit path; in reading mode, which writes no bytes, it declines as a no-op and dev builds warn |
|
|
886
|
+
| `moveFocusOut` | A plugin-owned editing surface whose caret ran off its own edge; hands the caret to the neighbour a plain arrow points at, through the editor's focus traversal, so the landing skips non-focusable blocks, enters containers, and reveals an unmounted target like any other arrow |
|
|
887
|
+
| `getPresentationMode` | Your rendering or a gesture needs the live presentation mode ([Presentation modes](#presentation-modes)) |
|
|
888
|
+
| `getTheme` | Your content's colors are painted by an engine rather than styled by CSS; token-styled chrome needs neither this nor `getPresentationMode`, it rethemes through the cascade |
|
|
889
|
+
| `getOptions` | This editor instance's options for the plugin owning your kind, typed `unknown`; the per-instance channel a factory argument can't reach ([the options recipe](#recipe-per-instance-options-and-the-factory-closure-trap)) |
|
|
890
|
+
| `captureScrollPosition` | Your component is about to swap its view for one of a different height (a tall diagram for its short source card) and the reader is scrolled right at it. Call it before the swap, await what it hands back after, and the page stays where the reader left it instead of clamping to the shorter layout in between |
|
|
888
891
|
|
|
889
892
|
```ts
|
|
890
|
-
const { updateOwnMetadata, getPresentationMode, getTheme, getOptions } =
|
|
893
|
+
const { updateOwnMetadata, getPresentationMode, getTheme, getOptions, captureScrollPosition } =
|
|
894
|
+
createContainerBlock(deps);
|
|
891
895
|
updateOwnMetadata({ name: 'debunked' }); // one undo entry; rebuildRaw re-emits the opener line as :::debunked
|
|
892
896
|
getPresentationMode(); // 'source'
|
|
893
897
|
getTheme(); // 'dark'
|
|
894
898
|
getOptions(); // whatever this editor's { plugin, options } entry carried; undefined for a bare unit
|
|
899
|
+
const restore = captureScrollPosition(); // before the swap...
|
|
900
|
+
editing = true;
|
|
901
|
+
await restore(); // ...and after; a no-op when nothing moved
|
|
895
902
|
```
|
|
896
903
|
|
|
897
904
|
One dep is worth knowing about too. A marker-bearing container (a footnote definition's `[^label]: `, mirroring a list item's `- `) hands the factory a **`getAmbientPrefix`** getter. Its first child then paints that prefix as a dimmed, read-only run before its own bytes, and the caret and offset walk skip it exactly as they do a list marker. Read it live, so a marker derived from metadata re-renders after an edit. Return a string, or `{ text, interactive }` to make ranges of it clickable: each range gets its own span, class and click handler, which is how a task list's checkbox toggles and how a footnote definition's `[^label]` takes the click back to its reference.
|
|
@@ -1165,7 +1172,7 @@ leaf.getOptions(); // this editor's options for your plugin, typed unknown
|
|
|
1165
1172
|
|
|
1166
1173
|
That text carries every newline your source holds, which makes **`white-space: pre-wrap` (or `pre`) on your source element part of the contract** for any leaf whose bytes can span lines. Without it the browser collapses the line breaks on screen while the offset walk goes on counting them, and the caret sits nowhere near where it looks.
|
|
1167
1174
|
|
|
1168
|
-
**A painted source.** By default the source is one text node. A `renderSource(text)` dep paints it as DOM instead (fence lines the marker-hiding modes collapse, highlight tokens; the `highlightCode` export is the code block's own tokenizer), and the factory asserts `textContent === text` on every paint, so a painter that drops a byte fails loudly in dev rather than corrupting a commit. A painted source takes its plain-text edits from the leaf, not the browser: typing, Enter, deletes and pastes splice the text and repaint, each reported through `onSourceEdit(text)` so a live preview can follow the draft, and undo inside the open reveal walks those edits back before it reaches the document's history. `repaintSource()` re-runs the painter after a native edit (an IME commit), and `completeBareSource(text)` lets a kind complete a chrome-only source (a `$$` straight over `$$`) to the shape a caret can sit in as
|
|
1175
|
+
**A painted source.** By default the source is one text node. A `renderSource(text)` dep paints it as DOM instead (fence lines the marker-hiding modes collapse, highlight tokens; the `highlightCode` export is the code block's own tokenizer), and the factory asserts `textContent === text` on every paint, so a painter that drops a byte fails loudly in dev rather than corrupting a commit. A painted source takes its plain-text edits from the leaf, not the browser: typing, Enter, deletes and pastes splice the text and repaint, each reported through `onSourceEdit(text)` so a live preview can follow the draft, and undo inside the open reveal walks those edits back before it reaches the document's history. `repaintSource()` re-runs the painter after a native edit (an IME commit), and `completeBareSource(text)` lets a kind complete a chrome-only source (a `$$` straight over `$$`) to the shape a caret can sit in, asked as the source is revealed and again after any edit that empties it, so a one-line `$$x^2$$` that loses its `x^2` never shows bare fences. Block math is the worked example.
|
|
1169
1176
|
|
|
1170
1177
|
A leaf whose bytes are one line (the parrot's opener claims exactly one) declares `singleLine: true` and needs none of that. Enter in one of those ends the block: the text after the caret becomes a paragraph below and the caret goes with it, which is what Enter does in a heading. With the flag off, the default, Enter types a newline.
|
|
1171
1178
|
|
|
@@ -1270,7 +1277,7 @@ fence claim ──▶ opaque container, NO children ──▶ component renders
|
|
|
1270
1277
|
- **Edit mode commits through `updateOwnMetadata`.** The component swaps its body to a plugin-owned `<textarea>` seeded from metadata; commit (Ctrl+Enter, blur) writes the new code with the container factory's `updateOwnMetadata`, which is one undoable entry, with your `rebuildRaw` re-emitting the fence so `getSource()` reflects the edit byte-exactly. Escape cancels without touching the tree.
|
|
1271
1278
|
- **Inject the renderer, memoize it, own its CSS.** The engine is the consumer's dependency: take it as a plugin option (`mermaidPlugin({ renderer })`) and pass it by module to the component. Wrap it in `createBoundedMemo` so re-renders of unchanged code do zero engine work. An async renderer stores the render promise as the cached value (in-flight work is shared, and a failure is cached like a success), and a renderer whose result holds a live DOM node passes a `cloneOnRead` so each caller gets its own copy. Resolve failures to a legible inline error, never a throw, and render a static code fallback with a note when no renderer is configured. The engine's stylesheet travels with the renderer module, so import it there, where no route can forget it: a KaTeX-based renderer needs `katex/dist/katex.min.css`, or its MathML accessibility tree lays out unclipped and every equation paints twice.
|
|
1272
1279
|
- **If the engine paints its own colors, the theme is a render input.** An engine that emits markup carrying color literals (a diagram SVG) can't be rethemed by a stylesheet after the fact; the diagram has to be redrawn. So the theme belongs in three places at once, and any one of them alone leaves a broken half: **the renderer's parameters** (so it can draw for the theme), **the memo key** (so a flip misses and a flip back is still a hit, never a cache reset, which throws away work you'll want again), and **the component's render read** (`getTheme()` off the container or leaf factory), because THAT read is what subscribes the block to the flip. Mermaid keys `theme\0code`; its engine adapter maps the editor theme name to a mermaid theme and re-initializes when it changes, serializing renders because that config is process-global. An engine styled by CSS variables needs none of this.
|
|
1273
|
-
- **Interior interactivity stays inside your DOM.** Pan/zoom, buttons, overlays:
|
|
1280
|
+
- **Interior interactivity stays inside your DOM.** Pan/zoom, buttons, overlays: put `POINTER_GESTURE_ATTR` on the element whose drags are yours (only while the gesture is armed, if it isn't always), or the editor reads the press as the start of a selection and paints a range over your pan. `stopPropagation()` on pointerdown can't do this, since Svelte delivers pointer events from the app root and the editor's listener has already run. A focus view is just a fixed-position overlay in the component's own tree, so mount it in place, focus it on open, close on Escape.
|
|
1274
1281
|
- **View-state commands reach the component through `ctx.hooks`.** See [Block commands](#block-commands).
|
|
1275
1282
|
|
|
1276
1283
|
The two helpers from that list, with what they hand back:
|
|
@@ -1383,7 +1390,7 @@ If your `revealSource` widget takes a click of its own, declare `claimsActivatio
|
|
|
1383
1390
|
|
|
1384
1391
|
If your widget derives from the whole document, read the version inside the same `$derived` and use it as your memo key. The document itself isn't a usable key: the editor mutates it in place, so its identity never changes, and an identity-keyed memo hits forever on a stale answer. Reading the version inside the derived is also what subscribes your widget to edits anywhere, so N widgets sharing one memoized walk stay as live as N widgets each walking the document.
|
|
1385
1392
|
|
|
1386
|
-
**A symmetric delimiter can close itself as it is typed.** Pass `autoPair: true` on a bare trigger whose construct opens and closes on the same byte, the way the bundled latex plugin does for `$…$`: typing the trigger lands its twin after the caret, typing it again over that twin steps past it, and a first body byte
|
|
1393
|
+
**A symmetric delimiter can close itself as it is typed.** Pass `autoPair: true` on a bare trigger whose construct opens and closes on the same byte, the way the bundled latex plugin does for `$…$`: typing the trigger lands its twin after the caret, typing it again over that twin steps past it, and a first body byte that leaves the pair no construct (`$5` is a price) drops the twin again. Without it a lone `$` typed ahead of an existing formula pairs with that formula's closer and wraps the prose between them. The built-in backtick, `*`, `_` and `~~` behave this way without registration.
|
|
1387
1394
|
|
|
1388
1395
|
**A bare trigger must be a character no built-in scanner claims.** Registering a bare recognizer on a reserved trigger (`` ` ``, `&`, `<`, `*`, `_`, `~`, `[`, `]`, `!`, `\`, or newline) throws: built-in dispatch runs first, so a bare recognizer there would never fire, and a silent no-op is the one failure a public API must not have.
|
|
1389
1396
|
|
|
@@ -1404,7 +1411,7 @@ The scanner consults the rung ahead of the built-in `[` case, but only when `[^`
|
|
|
1404
1411
|
|
|
1405
1412
|
A rung on `!` is consulted ahead of the built-in `!` case, so it outranks the image grammar wherever its prefix matches. And the two grammars do overlap: an image whose alt text opens with `[` starts on `![[` as well, so `![[a.png]]` carrying a parenthesized destination after it is a built-in image with the alt text `[a.png]`, not an embed. Deciding that overlap is your recognizer's job. Decline it (return `null`) and the built-in image reads the bytes unchanged. **Getting it wrong fails silently.** An ungated `![[` recognizer swallows the image with no throw and no dev-warn, and since the raw bytes are untouched the document still round-trips cleanly, so no round-trip check and no conformance cell in your own suite will ever see it. The first report comes from a reader whose picture stopped rendering.
|
|
1406
1413
|
|
|
1407
|
-
**Bound the decline, not just the claim.** Your recognizer is consulted at every occurrence of its trigger, so a decline that searches to the end of the block costs one block scan per trigger, which goes quadratic on a large paragraph, and the trigger is often ordinary prose (`$HOME $PATH …` for `$`). Stop at the first character your grammar can't contain, the way the emoji recognizer stops at the first non-shortcode byte. Where the grammar has no such character, index the candidate positions once per block with `createScanIndex` (hand it your position collector, get back a "first candidate at or after this offset" lookup), the way the bundled math and footnote
|
|
1414
|
+
**Bound the decline, not just the claim.** Your recognizer is consulted at every occurrence of its trigger, so a decline that searches to the end of the block costs one block scan per trigger, which goes quadratic on a large paragraph, and the trigger is often ordinary prose (`$HOME $PATH …` for `$`). Stop at the first character your grammar can't contain, the way the emoji recognizer stops at the first non-shortcode byte. Where the grammar has no such character, index the candidate positions once per block with `createScanIndex` (hand it your position collector, get back a "first candidate at or after this offset" lookup), the way the bundled math recognizer indexes every `$` and the footnote one its closers:
|
|
1408
1415
|
|
|
1409
1416
|
```ts
|
|
1410
1417
|
const dollarAt = createScanIndex((raw) => {
|
|
@@ -1448,17 +1455,19 @@ Three edges the snippet above is shaped by, and each one bites if you drop it:
|
|
|
1448
1455
|
|
|
1449
1456
|
**Errors in a component widget are half yours.** A **synchronous mount-time throw** is caught, so the widget falls back to its raw source and an `error` event fires, but the component mounts as its own effect root and nothing catches its post-mount runtime errors. Render a legible error for bad input instead of throwing (the KaTeX widget shows an inline message). A render engine's stylesheet is likewise yours: import it in the module that owns the renderer, so no route can forget it.
|
|
1450
1457
|
|
|
1451
|
-
**The inline tier isn't the block surface in miniature.** An inline kind gets recognition, rendering, atomic caret addressing at its edges, and an editing policy on its widget registration. The policy
|
|
1458
|
+
**The inline tier isn't the block surface in miniature.** An inline kind gets recognition, rendering, atomic caret addressing at its edges, and an editing policy on its widget registration. The policy's fields, all optional:
|
|
1452
1459
|
|
|
1453
|
-
| Field | What it decides
|
|
1454
|
-
| ----------------------- |
|
|
1455
|
-
| `revealSource` | Open the source (the `$…$` bytes) for editing on caret entry; inline math's model
|
|
1456
|
-
| `
|
|
1457
|
-
| `
|
|
1458
|
-
| `
|
|
1459
|
-
| `
|
|
1460
|
+
| Field | What it decides |
|
|
1461
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1462
|
+
| `revealSource` | Open the source (the `$…$` bytes) for editing on caret entry; inline math's model |
|
|
1463
|
+
| `revealContentSpan` | Where the editable content sits inside the source (`$x$` answers `{ start: 1, end: 2 }`), so a caret entering the source stays between the delimiters; absent, it keeps the leading edge |
|
|
1464
|
+
| `revealOffsetAtPoint` | Which source offset a press on the rendered widget names, so a click seats the caret where it landed; inline math walks its KaTeX glyphs for this, and `null` falls back to the content span's end |
|
|
1465
|
+
| `onSelectedKey` | A handler for keys while the widget is selected; image resize rides it |
|
|
1466
|
+
| `onEdge` | `'select' \| 'step-over'`: an edge press selects the whole widget, or steps transparently over it; `'step-over'` also makes a press on the widget seat the caret at the edge it landed by, where `'select'` leaves the island its own click; and Up or Down onto a block holding only a step-over widget seats the caret beside it, one press in and one out |
|
|
1467
|
+
| `deleteGranularity` | `'atomic' \| 'select-then-delete'`: one press deletes the whole widget, or the first press selects and the second deletes |
|
|
1468
|
+
| `claimsActivationClick` | Your component handles the activation click itself, so the surface's reveal stands down for it; the footnote jump's model |
|
|
1460
1469
|
|
|
1461
|
-
Both edge fields are live today: the built-in decoded-entity widget (`©` → ©) ships `{ deleteGranularity: 'atomic', onEdge: 'step-over' }`, so a caret-adjacent Backspace removes it whole and a plain arrow walks the caret across it like a character, the caret-edge dispatch reading both off the widget registration. The inline tier gets **no keymap, no minted commands, and no per-node metadata**: `InlineNode` has no metadata field, so unlike a block kind it stores nothing at all on the node.
|
|
1470
|
+
Both edge fields are live today: the built-in decoded-entity widget (`©` → ©) ships `{ deleteGranularity: 'atomic', onEdge: 'step-over' }`, so a caret-adjacent Backspace removes it whole and a plain arrow walks the caret across it like a character, the caret-edge dispatch and the click seat reading both off the widget registration. The inline tier gets **no keymap, no minted commands, and no per-node metadata**: `InlineNode` has no metadata field, so unlike a block kind it stores nothing at all on the node.
|
|
1462
1471
|
|
|
1463
1472
|
## Decorations
|
|
1464
1473
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voithos-labs/aragonite",
|
|
3
|
-
"version": "0.10.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.10.4",
|
|
4
|
+
"description": "cute svelte-based markdown editor lib",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
7
7
|
"gfm",
|
|
@@ -188,7 +188,7 @@
|
|
|
188
188
|
},
|
|
189
189
|
"peerDependencies": {
|
|
190
190
|
"katex": "^0.17.0 || ^0.18.0",
|
|
191
|
-
"mermaid": "^11.16.0",
|
|
191
|
+
"mermaid": "^11.16.0 || ^12.0.0",
|
|
192
192
|
"svelte": "^5.29.0"
|
|
193
193
|
},
|
|
194
194
|
"peerDependenciesMeta": {
|
|
@@ -211,7 +211,7 @@
|
|
|
211
211
|
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
212
212
|
"@types/commonmark": "^0.27.10",
|
|
213
213
|
"@types/node": "^26.2.0",
|
|
214
|
-
"@vitest/browser": "^
|
|
214
|
+
"@vitest/browser": "^5.0.0",
|
|
215
215
|
"commonmark": "0.31.2",
|
|
216
216
|
"eslint": "^10.9.0",
|
|
217
217
|
"eslint-plugin-svelte": "^3.23.0",
|
|
@@ -219,7 +219,7 @@
|
|
|
219
219
|
"globals": "^17.11.0",
|
|
220
220
|
"jsdom": "^30.0.0",
|
|
221
221
|
"katex": "^0.18.4",
|
|
222
|
-
"mermaid": "^
|
|
222
|
+
"mermaid": "^12.0.0",
|
|
223
223
|
"prettier": "^3.9.6",
|
|
224
224
|
"prettier-plugin-svelte": "^4.1.1",
|
|
225
225
|
"svelte": "^5.56.10",
|
|
@@ -228,8 +228,8 @@
|
|
|
228
228
|
"typescript": "^6.0.3",
|
|
229
229
|
"typescript-eslint": "^8.67.0",
|
|
230
230
|
"vite": "^8.2.2",
|
|
231
|
-
"vitest": "^
|
|
232
|
-
"wrangler": "4.131.
|
|
231
|
+
"vitest": "^5.0.0",
|
|
232
|
+
"wrangler": "4.131.1"
|
|
233
233
|
},
|
|
234
234
|
"overrides": {
|
|
235
235
|
"cookie": "^0.7.2"
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Word selection without its trailing space. Windows Chromium's double-click takes the whitespace
|
|
3
|
-
* after a word (the desktop-Word convention); a formatting toggle over that range would wrap the
|
|
4
|
-
* space too, and every other platform selects the bare word. The browser paints its own range
|
|
5
|
-
* before any `dblclick` handler runs, so the word is selected HERE, on the second press, with
|
|
6
|
-
* the native selection suppressed: the trimmed range is the first one ever painted.
|
|
7
|
-
*/
|
|
8
|
-
/** Trim trailing whitespace off the document's current selection when it ends in a text node.
|
|
9
|
-
* A selection that is only whitespace is left alone: collapsing it would undo the click. */
|
|
10
|
-
export declare function trimDoubleClickSelection(doc: Document): boolean;
|
|
11
|
-
/**
|
|
12
|
-
* Select the word under the point of a double-click's second press, trimmed, synchronously.
|
|
13
|
-
* Word edges come from the engine's own `modify` walk so they match what its double-click would
|
|
14
|
-
* have chosen. False when the point names no text position; the caller then leaves the press to
|
|
15
|
-
* the browser.
|
|
16
|
-
*/
|
|
17
|
-
export declare function selectWordAtPoint(doc: Document, x: number, y: number): boolean;
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Word selection without its trailing space. Windows Chromium's double-click takes the whitespace
|
|
3
|
-
* after a word (the desktop-Word convention); a formatting toggle over that range would wrap the
|
|
4
|
-
* space too, and every other platform selects the bare word. The browser paints its own range
|
|
5
|
-
* before any `dblclick` handler runs, so the word is selected HERE, on the second press, with
|
|
6
|
-
* the native selection suppressed: the trimmed range is the first one ever painted.
|
|
7
|
-
*/
|
|
8
|
-
/** Trim trailing whitespace off the document's current selection when it ends in a text node.
|
|
9
|
-
* A selection that is only whitespace is left alone: collapsing it would undo the click. */
|
|
10
|
-
export function trimDoubleClickSelection(doc) {
|
|
11
|
-
const sel = doc.getSelection();
|
|
12
|
-
if (!sel || sel.rangeCount === 0 || sel.isCollapsed)
|
|
13
|
-
return false;
|
|
14
|
-
const range = sel.getRangeAt(0);
|
|
15
|
-
const endNode = range.endContainer;
|
|
16
|
-
if (!(endNode instanceof Text))
|
|
17
|
-
return false;
|
|
18
|
-
const text = range.toString();
|
|
19
|
-
const trimmed = text.trimEnd().length;
|
|
20
|
-
if (trimmed === 0 || trimmed === text.length)
|
|
21
|
-
return false;
|
|
22
|
-
const cut = text.length - trimmed;
|
|
23
|
-
if (cut > range.endOffset)
|
|
24
|
-
return false;
|
|
25
|
-
sel.setBaseAndExtent(range.startContainer, range.startOffset, endNode, range.endOffset - cut);
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Select the word under the point of a double-click's second press, trimmed, synchronously.
|
|
30
|
-
* Word edges come from the engine's own `modify` walk so they match what its double-click would
|
|
31
|
-
* have chosen. False when the point names no text position; the caller then leaves the press to
|
|
32
|
-
* the browser.
|
|
33
|
-
*/
|
|
34
|
-
export function selectWordAtPoint(doc, x, y) {
|
|
35
|
-
const sel = doc.getSelection();
|
|
36
|
-
const hit = textPositionAt(doc, x, y);
|
|
37
|
-
if (!sel || !hit || typeof sel.modify !== 'function')
|
|
38
|
-
return false;
|
|
39
|
-
sel.setBaseAndExtent(hit.node, hit.offset, hit.node, hit.offset);
|
|
40
|
-
sel.modify('move', 'backward', 'word');
|
|
41
|
-
sel.modify('extend', 'forward', 'word');
|
|
42
|
-
if (sel.isCollapsed)
|
|
43
|
-
return false;
|
|
44
|
-
trimDoubleClickSelection(doc);
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
function textPositionAt(doc, x, y) {
|
|
48
|
-
const position = doc.caretPositionFromPoint?.(x, y);
|
|
49
|
-
if (position?.offsetNode instanceof Text) {
|
|
50
|
-
return { node: position.offsetNode, offset: position.offset };
|
|
51
|
-
}
|
|
52
|
-
const range = doc.caretRangeFromPoint?.(x, y);
|
|
53
|
-
if (range?.startContainer instanceof Text) {
|
|
54
|
-
return { node: range.startContainer, offset: range.startOffset };
|
|
55
|
-
}
|
|
56
|
-
return null;
|
|
57
|
-
}
|