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