@voithos-labs/aragonite 0.10.4 → 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 +11 -5
- package/dist/a11y-strings.js +60 -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 +12 -21
- 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 +603 -1126
- package/dist/components/Editor.svelte.d.ts +9 -20
- package/dist/components/GapCaret.svelte +26 -59
- package/dist/components/SelectionOverlay.svelte +26 -51
- package/dist/components/SelectionOverlay.svelte.d.ts +5 -3
- 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 +72 -87
- 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 +29 -34
- 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 +57 -39
- 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 +141 -203
- 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 +45 -90
- package/dist/components/blocks/list/ListItemBlock.svelte +93 -147
- package/dist/components/blocks/list/ListItemBlock.svelte.d.ts +1 -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 +8 -6
- package/dist/components/blocks/text/click-snap-guard.js +10 -7
- 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 +150 -181
- 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 -108
- 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 +212 -199
- 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 +12 -35
- package/dist/components/drag-handle.js +22 -68
- 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 -24
- package/dist/components/editor-root-listeners.js +49 -24
- 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 +14 -26
- 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 +30 -35
- package/dist/components/menu/SelectionToolbar.svelte.d.ts +3 -5
- 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 +54 -75
- package/dist/core/inline/inline-widgets.js +54 -62
- 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 +31 -15
- 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 -6
- package/dist/cursor/height-oracle.js +26 -74
- 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 +14 -15
- package/dist/cursor/scrollport.js +7 -13
- 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 -14
- package/dist/cursor/visual-lines.js +45 -46
- package/dist/cursor/widget-edge-snap.d.ts +10 -15
- package/dist/cursor/widget-edge-snap.js +13 -12
- package/dist/cursor/widget-offset.d.ts +110 -104
- package/dist/cursor/widget-offset.js +344 -179
- 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 +30 -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 +28 -50
- package/dist/editor-actions/focus/focus-landing.d.ts +8 -11
- package/dist/editor-actions/focus/focus-landing.js +15 -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 +54 -82
- package/dist/editor-actions/plugin/container.js +105 -188
- 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 +88 -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 +37 -8
- package/dist/plugin.js +100 -77
- 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 +78 -67
- 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 -81
- 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 +51 -45
- 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 +15 -6
- 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 +141 -310
- 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 +78 -84
- package/dist/schema/commands.js +152 -117
- 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 +8 -10
- 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 +63 -80
- 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 +92 -125
- 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 +59 -96
- 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 +51 -134
- package/dist/selection/dead-space-caret.d.ts +19 -30
- package/dist/selection/dead-space-caret.js +70 -88
- package/dist/selection/drag-pointer.d.ts +7 -9
- package/dist/selection/drag-pointer.js +23 -33
- 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 -40
- package/dist/selection/keyboard-extend.js +94 -117
- package/dist/selection/multi-click.d.ts +13 -18
- package/dist/selection/multi-click.js +32 -38
- package/dist/selection/native-bridge.d.ts +29 -39
- package/dist/selection/native-bridge.js +78 -127
- package/dist/selection/nearest-block.d.ts +17 -12
- package/dist/selection/nearest-block.js +37 -18
- package/dist/selection/path-lookup.d.ts +25 -16
- package/dist/selection/path-lookup.js +101 -18
- package/dist/selection/path-math.d.ts +9 -11
- package/dist/selection/path-math.js +5 -7
- package/dist/selection/pointer-gesture.d.ts +5 -5
- package/dist/selection/pointer-gesture.js +5 -5
- package/dist/selection/pointer-session.d.ts +10 -16
- package/dist/selection/pointer-session.js +6 -7
- package/dist/selection/primitives.d.ts +64 -40
- package/dist/selection/primitives.js +57 -67
- 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 -23
- 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 +34 -16
- package/dist/selection/selection-drop.js +159 -95
- 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 +60 -53
- package/dist/styles/editor.css +201 -143
- 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 +9 -7
- package/dist/tree-operations/paste/replacement-parse.js +12 -10
- 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 +327 -205
- package/docs/guide/directives.md +7 -7
- package/docs/guide/plugin-api.md +253 -185
- package/docs/guide/plugin-guide.md +332 -235
- package/docs/guide/plugin-testing.md +96 -82
- package/package.json +12 -16
- 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/cursor/scroll-hold.d.ts +0 -10
- package/dist/cursor/scroll-hold.js +0 -22
- package/dist/invariants/split-landing.d.ts +0 -8
- package/dist/invariants/split-landing.js +0 -15
- 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 -31
- package/dist/tree-operations/paste/replace-block-at-parent.js +0 -78
|
@@ -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>
|
|
@@ -269,7 +280,7 @@ cNo.....................................oc
|
|
|
269
280
|
overflow-x: auto;
|
|
270
281
|
overflow-y: hidden;
|
|
271
282
|
scrollbar-width: none;
|
|
272
|
-
/*
|
|
283
|
+
/* decoration, not content: every frame is in the DOM and none of them belong in a copy */
|
|
273
284
|
user-select: none;
|
|
274
285
|
animation: parrot-hue 0.49s step-end infinite;
|
|
275
286
|
}
|
|
@@ -337,15 +348,15 @@ The editing half is the factory call, the `revealed` flag, two spreads, and the
|
|
|
337
348
|
|
|
338
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.
|
|
339
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.
|
|
340
|
-
- `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.
|
|
341
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.
|
|
342
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.
|
|
343
354
|
|
|
344
|
-
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.
|
|
345
356
|
|
|
346
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.
|
|
347
358
|
|
|
348
|
-
**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):
|
|
349
360
|
|
|
350
361
|
```ts
|
|
351
362
|
parse('%%parrot party responsibly\n').children[0];
|
|
@@ -394,12 +405,12 @@ Each part has a defined absence, which is prob the easiest way to remember what
|
|
|
394
405
|
|
|
395
406
|
### Registration is global, and register-once
|
|
396
407
|
|
|
397
|
-
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.)
|
|
398
409
|
|
|
399
410
|
Who guarantees a registration runs only once depends on where it runs:
|
|
400
411
|
|
|
401
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.
|
|
402
|
-
- **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.
|
|
403
414
|
|
|
404
415
|
```ts
|
|
405
416
|
isBlockKindDeclared('parrot'); // false on a fresh page
|
|
@@ -409,15 +420,17 @@ isBlockKindDeclared('parrot'); // true, so a second import of this module skips
|
|
|
409
420
|
|
|
410
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.
|
|
411
422
|
|
|
412
|
-
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.
|
|
413
424
|
|
|
414
425
|
### The plugin unit
|
|
415
426
|
|
|
416
427
|
A **plugin unit** is the installable package: a name plus a `setup` that runs your `register*` calls.
|
|
417
428
|
|
|
418
|
-
**`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).
|
|
419
432
|
|
|
420
|
-
|
|
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)).
|
|
421
434
|
|
|
422
435
|
```ts
|
|
423
436
|
export function myPlugin(options?: { renderer?: Renderer }): EditorPlugin {
|
|
@@ -438,8 +451,8 @@ Install by passing units to the editor's **`plugins` prop**, set once at mount,
|
|
|
438
451
|
import { myPlugin } from './my-plugin';
|
|
439
452
|
|
|
440
453
|
// Build the array once at module scope, not inline in the markup: an inline
|
|
441
|
-
// `plugins={[myPlugin()]}`
|
|
442
|
-
//
|
|
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.
|
|
443
456
|
const plugins = [myPlugin()];
|
|
444
457
|
</script>
|
|
445
458
|
|
|
@@ -451,26 +464,28 @@ Install by passing units to the editor's **`plugins` prop**, set once at mount,
|
|
|
451
464
|
- Passing the same unit again no-ops.
|
|
452
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.
|
|
453
466
|
- Units install in array order.
|
|
454
|
-
- 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.
|
|
455
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)).
|
|
456
|
-
- **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.
|
|
457
470
|
|
|
458
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.
|
|
459
472
|
|
|
460
473
|
```ts
|
|
461
474
|
import { installPlugins } from '@voithos-labs/aragonite';
|
|
475
|
+
import { isPluginInstalled } from '@voithos-labs/aragonite/plugin';
|
|
462
476
|
|
|
477
|
+
const parrot = parrotPlugin();
|
|
463
478
|
isPluginInstalled('parrot'); // false
|
|
464
|
-
installPlugins([
|
|
479
|
+
installPlugins([parrot]);
|
|
465
480
|
isPluginInstalled('parrot'); // true
|
|
466
|
-
installPlugins([
|
|
481
|
+
installPlugins([parrot]); // no-op (a fresh parrotPlugin() here would no-op too, with a dev warning)
|
|
467
482
|
```
|
|
468
483
|
|
|
469
484
|
### What is stable, what is not
|
|
470
485
|
|
|
471
|
-
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.
|
|
472
487
|
|
|
473
|
-
- **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.
|
|
474
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.
|
|
475
490
|
|
|
476
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**.
|
|
@@ -482,11 +497,11 @@ Every surface that hands your plugin a node to **read** types it as a view: `Nod
|
|
|
482
497
|
Two lists cover the whole read side:
|
|
483
498
|
|
|
484
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.
|
|
485
|
-
- **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.
|
|
486
501
|
|
|
487
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.
|
|
488
503
|
|
|
489
|
-
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.
|
|
490
505
|
|
|
491
506
|
### `rebuildRaw`, the write hook
|
|
492
507
|
|
|
@@ -507,11 +522,11 @@ function rebuildBoxRaw(node: CstNode): void {
|
|
|
507
522
|
|
|
508
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.
|
|
509
524
|
|
|
510
|
-
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.
|
|
511
526
|
|
|
512
527
|
## One process, many editors
|
|
513
528
|
|
|
514
|
-
`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`**:
|
|
515
530
|
|
|
516
531
|
```ts
|
|
517
532
|
setup(ctx) {
|
|
@@ -525,18 +540,24 @@ setup(ctx) {
|
|
|
525
540
|
}
|
|
526
541
|
```
|
|
527
542
|
|
|
528
|
-
| Field
|
|
529
|
-
|
|
|
530
|
-
| `editorId`
|
|
531
|
-
| `document`
|
|
532
|
-
| `
|
|
533
|
-
| `
|
|
534
|
-
| `
|
|
535
|
-
| `
|
|
536
|
-
| `
|
|
537
|
-
| `
|
|
538
|
-
|
|
539
|
-
|
|
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.
|
|
540
561
|
|
|
541
562
|
### Recipe: per-instance derived state
|
|
542
563
|
|
|
@@ -562,10 +583,10 @@ function recount(editor: EditorContext<WordCountOptions>): void {
|
|
|
562
583
|
|
|
563
584
|
export const wordCountPlugin = definePlugin<WordCountOptions>({
|
|
564
585
|
name: 'word-count',
|
|
586
|
+
defaults: { live: true }, // what a bare-unit install reads
|
|
565
587
|
setup(ctx) {
|
|
566
588
|
ctx.onEditor((editor) => {
|
|
567
|
-
|
|
568
|
-
const { live } = editor.options ?? { live: true };
|
|
589
|
+
const { live } = editor.options;
|
|
569
590
|
recount(editor); // seed on mount
|
|
570
591
|
const off = live ? editor.events.on('edit', () => recount(editor)) : () => {};
|
|
571
592
|
return () => {
|
|
@@ -586,9 +607,35 @@ Two editors share one process-global registration but may still want different o
|
|
|
586
607
|
<Editor source={right} plugins={[{ plugin: wordCountPlugin, options: { live: false } }]} />
|
|
587
608
|
```
|
|
588
609
|
|
|
589
|
-
`
|
|
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`:
|
|
590
611
|
|
|
591
|
-
|
|
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.
|
|
592
639
|
|
|
593
640
|
## Walkthrough: a `:::conspiracy` container end to end
|
|
594
641
|
|
|
@@ -616,6 +663,7 @@ import {
|
|
|
616
663
|
registerChromeLeaf,
|
|
617
664
|
registerDirective,
|
|
618
665
|
setPluginMetadata,
|
|
666
|
+
trimWhitespace,
|
|
619
667
|
type CstNode,
|
|
620
668
|
type EditorPlugin,
|
|
621
669
|
type ParsedDirective
|
|
@@ -637,7 +685,7 @@ export interface ConspiracyMetadata {
|
|
|
637
685
|
// from the opener line); children 1+ are the parsed evidence. The fence bytes go to
|
|
638
686
|
// metadata so the raw can be rebuilt after an edit.
|
|
639
687
|
function conspiracyFromDirective(parsed: ParsedDirective): CstNode {
|
|
640
|
-
const theory = parsed.fence.info
|
|
688
|
+
const theory = trimWhitespace(parsed.fence.info);
|
|
641
689
|
const node: CstNode = {
|
|
642
690
|
kind: declaredPluginKind(CONSPIRACY),
|
|
643
691
|
leadingTrivia: parsed.leadingTrivia,
|
|
@@ -686,7 +734,7 @@ function registerConspiracy(): void {
|
|
|
686
734
|
}
|
|
687
735
|
}
|
|
688
736
|
|
|
689
|
-
// A block command that flips the verdict. updateMetadata is the
|
|
737
|
+
// A block command that flips the verdict. updateMetadata is the supported
|
|
690
738
|
// commit path: it merges the patch, runs rebuildRaw, and makes one undoable edit;
|
|
691
739
|
// because the name flows into raw, the verdict survives a round-trip.
|
|
692
740
|
const setVerdict = registerBlockCommand(conspiracy, 'conspiracy.setVerdict', (ctx) => {
|
|
@@ -711,26 +759,26 @@ function registerConspiracy(): void {
|
|
|
711
759
|
// trips a dev assertion the moment someone edits a conspiracy with a blank first line.
|
|
712
760
|
bodyWrap: DIRECTIVE_BODY_WRAP,
|
|
713
761
|
reservedChrome: { kind: conspiracyTitle },
|
|
714
|
-
// Child 0 is the title,
|
|
715
|
-
//
|
|
716
|
-
// `'lift-first-child-keep-container'`,
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
middleChildBackspace: 'default-merge'
|
|
720
|
-
}
|
|
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' }
|
|
721
767
|
// Declare `reorderChildren` here if your container's direct children should
|
|
722
|
-
// reorder among themselves (drag, or Alt+ArrowUp/ArrowDown). Absent, a
|
|
723
|
-
// reorder
|
|
724
|
-
//
|
|
725
|
-
// 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.
|
|
726
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',
|
|
727
775
|
keymap: [
|
|
728
776
|
{ chord: 'Mod+7', command: setVerdict, arg: 'conspiracy' }, // allege
|
|
729
777
|
{ chord: 'Mod+8', command: setVerdict, arg: 'debunked' } // debunk
|
|
730
778
|
],
|
|
731
779
|
// Required: how this kind behaves under every cross-cutting editor system. A missing
|
|
732
|
-
// cell or column is a compile error, and four more rules
|
|
733
|
-
//
|
|
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.
|
|
734
782
|
closure: {
|
|
735
783
|
roundTrip: { mode: 'implemented', via: 'container contract=opaque, rebuildConspiracyRaw' },
|
|
736
784
|
focus: { mode: 'implemented', via: 'focus walks to the title chrome / first body child' },
|
|
@@ -751,14 +799,15 @@ function registerConspiracy(): void {
|
|
|
751
799
|
mode: 'implemented',
|
|
752
800
|
via: 'byte-slice copy; a slice touching the title re-emits the conspiracy around the collected body'
|
|
753
801
|
},
|
|
754
|
-
// `inherit-default` is the honest answer unless you actually run
|
|
755
|
-
//
|
|
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
|
|
756
804
|
// admitting you inherit the generic one.
|
|
757
805
|
simOracle: { mode: 'inherit-default' }
|
|
758
806
|
}
|
|
759
807
|
});
|
|
760
808
|
|
|
761
|
-
|
|
809
|
+
// The label is what a screen reader and the block menu call the title row.
|
|
810
|
+
registerChromeLeaf(conspiracyTitle, { label: 'Theory', blockClass: 'conspiracy-title' });
|
|
762
811
|
}
|
|
763
812
|
|
|
764
813
|
// definePluginBlock wraps definePlugin around the register step and the component
|
|
@@ -841,8 +890,8 @@ Your component supplies only its own chrome: the border, the title styling, an i
|
|
|
841
890
|
.conspiracy-block {
|
|
842
891
|
/* the corkboard, with one piece of red string */
|
|
843
892
|
position: relative;
|
|
844
|
-
border: 1px solid var(--color-ui-muted, #
|
|
845
|
-
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);
|
|
846
895
|
border-radius: 6px;
|
|
847
896
|
padding: 8px 12px;
|
|
848
897
|
}
|
|
@@ -851,7 +900,7 @@ Your component supplies only its own chrome: the border, the title styling, an i
|
|
|
851
900
|
}
|
|
852
901
|
/* debunked: the string comes down, the theory gets crossed out, the stamp lands */
|
|
853
902
|
.debunked {
|
|
854
|
-
border-left-color: var(--color-ui-muted, #
|
|
903
|
+
border-left-color: var(--color-ui-muted, #93938d);
|
|
855
904
|
}
|
|
856
905
|
.debunked :global(.conspiracy-title) {
|
|
857
906
|
text-decoration: line-through;
|
|
@@ -864,7 +913,7 @@ Your component supplies only its own chrome: the border, the title styling, an i
|
|
|
864
913
|
transform: rotate(-12deg);
|
|
865
914
|
font: 600 0.75em monospace;
|
|
866
915
|
letter-spacing: 0.12em;
|
|
867
|
-
color: var(--color-error, #
|
|
916
|
+
color: var(--color-error, #ff5f57);
|
|
868
917
|
border: 2px solid currentColor;
|
|
869
918
|
border-radius: 3px;
|
|
870
919
|
padding: 1px 6px;
|
|
@@ -874,34 +923,36 @@ Your component supplies only its own chrome: the border, the title styling, an i
|
|
|
874
923
|
|
|
875
924
|
Three rules for that file, each earned the hard way:
|
|
876
925
|
|
|
877
|
-
- **`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.
|
|
878
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.
|
|
879
|
-
- **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).
|
|
880
929
|
|
|
881
930
|
The factory returns more than the walkthrough destructures:
|
|
882
931
|
|
|
883
|
-
| Return | When you reach for it
|
|
884
|
-
| ----------------------- |
|
|
885
|
-
| `updateOwnMetadata` | Your component writes its own node's metadata (a collapse toggle, an edited setting). The
|
|
886
|
-
| `moveFocusOut` | A plugin-owned editing surface whose caret ran off its own edge; hands the caret to the neighbour a plain arrow points at, through the editor's focus traversal, so the landing skips non-focusable blocks, enters containers, and reveals an unmounted target like any other arrow
|
|
887
|
-
| `getPresentationMode` | Your rendering or a gesture needs the live presentation mode ([Presentation modes](#presentation-modes))
|
|
888
|
-
| `getTheme` | Your content's colors are painted by an engine rather than styled by CSS; token-styled chrome needs neither this nor `getPresentationMode`, it rethemes through the cascade
|
|
889
|
-
| `getOptions` | This editor
|
|
890
|
-
| `
|
|
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 |
|
|
891
941
|
|
|
892
942
|
```ts
|
|
893
943
|
const { updateOwnMetadata, getPresentationMode, getTheme, getOptions, captureScrollPosition } =
|
|
894
944
|
createContainerBlock(deps);
|
|
895
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
|
|
896
947
|
getPresentationMode(); // 'source'
|
|
897
948
|
getTheme(); // 'dark'
|
|
898
|
-
getOptions(); //
|
|
949
|
+
getOptions(); // your defaults with this editor's { plugin, options } entry merged over them
|
|
899
950
|
const restore = captureScrollPosition(); // before the swap...
|
|
900
951
|
editing = true;
|
|
901
952
|
await restore(); // ...and after; a no-op when nothing moved
|
|
902
953
|
```
|
|
903
954
|
|
|
904
|
-
One dep is worth knowing about too. A marker-bearing container (a footnote definition's `[^label]: `, mirroring a list item's `- `) hands the factory a **`getAmbientPrefix`** getter. Its first child then paints that prefix as a dimmed, read-only run before its own bytes, and the caret and offset walk skip it exactly as they do a list marker. Read it live, so a marker derived from metadata re-renders after an edit. Return a string, or `{ text, interactive }` to make ranges of it clickable: each range gets its own span, class and click handler, which is how a task list's checkbox toggles and how a footnote definition's `[^label]` takes the click back to its reference.
|
|
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.
|
|
905
956
|
|
|
906
957
|
### Wire it into a page
|
|
907
958
|
|
|
@@ -948,17 +999,17 @@ Want a collapse toggle? Give `reservedChrome` an `isCollapsed` probe over the no
|
|
|
948
999
|
|
|
949
1000
|
## The closure block
|
|
950
1001
|
|
|
951
|
-
`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`:
|
|
952
1003
|
|
|
953
1004
|
- `{ mode: 'implemented', via }`: a real mechanism you can name (a `rebuildRaw`, a keymap command, `measurePartialRects`).
|
|
954
1005
|
- `{ mode: 'inherit-default' }`: the generic editor behaviour, nothing kind-specific.
|
|
955
1006
|
- `{ mode: 'not-supported', reason }`: the subsystem is structurally absent, so name the degradation.
|
|
956
1007
|
|
|
957
|
-
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:
|
|
958
1009
|
|
|
959
|
-
1. A container
|
|
1010
|
+
1. A container can't declare `roundTrip: inherit-default`; its `rebuildRaw` is the mechanism.
|
|
960
1011
|
2. A `not-mergeable` kind can't declare `mergeBackspace: inherit-default`; it has no default merge to inherit.
|
|
961
|
-
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'`.
|
|
962
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.
|
|
963
1014
|
|
|
964
1015
|
Those four plus the nine columns are the whole contract.
|
|
@@ -991,7 +1042,7 @@ simpleLeafClosure({ focus, searchPaint, undo, simOracle });
|
|
|
991
1042
|
// clipboard: { mode: 'inherit-default' }
|
|
992
1043
|
```
|
|
993
1044
|
|
|
994
|
-
**`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.
|
|
995
1046
|
|
|
996
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:
|
|
997
1048
|
|
|
@@ -1004,7 +1055,7 @@ closure: containerClosure({
|
|
|
1004
1055
|
mode: 'implemented',
|
|
1005
1056
|
via: 'updateMetadata; the verdict flip commits as one undo entry'
|
|
1006
1057
|
},
|
|
1007
|
-
// The conspiracy declares reservedChrome, so coherence rule four
|
|
1058
|
+
// The conspiracy declares reservedChrome, so coherence rule four warns about the baked
|
|
1008
1059
|
// clipboard cell; a container without reserved chrome just leaves this out.
|
|
1009
1060
|
clipboard: {
|
|
1010
1061
|
mode: 'implemented',
|
|
@@ -1014,7 +1065,7 @@ closure: containerClosure({
|
|
|
1014
1065
|
});
|
|
1015
1066
|
```
|
|
1016
1067
|
|
|
1017
|
-
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.
|
|
1018
1069
|
|
|
1019
1070
|
## Teaching the parser
|
|
1020
1071
|
|
|
@@ -1041,11 +1092,9 @@ tryOpen(ctx) {
|
|
|
1041
1092
|
|
|
1042
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.
|
|
1043
1094
|
|
|
1044
|
-
> **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 }`.
|
|
1045
|
-
|
|
1046
1095
|
### Opener priority
|
|
1047
1096
|
|
|
1048
|
-
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):
|
|
1049
1098
|
|
|
1050
1099
|
| Priority | Built-in kind |
|
|
1051
1100
|
| -------: | ------------------------- |
|
|
@@ -1065,7 +1114,7 @@ Two rules place a plugin opener on it:
|
|
|
1065
1114
|
|
|
1066
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.
|
|
1067
1116
|
|
|
1068
|
-
**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.
|
|
1069
1118
|
|
|
1070
1119
|
For your pricing map: the opt-in `:::name` directive grammar registers its container opener at 45, between `blockquote` and `list`.
|
|
1071
1120
|
|
|
@@ -1088,7 +1137,7 @@ tryOpen(ctx) {
|
|
|
1088
1137
|
|
|
1089
1138
|
Three habits complete the gate:
|
|
1090
1139
|
|
|
1091
|
-
- **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'`.
|
|
1092
1141
|
- **Declare `interruptsParagraph: false`**: a line that interrupts a paragraph has a paragraph before it, so it's never at line 0.
|
|
1093
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.
|
|
1094
1143
|
|
|
@@ -1101,17 +1150,17 @@ An opener recognizes syntax that's already there. A grammar whose lines must be
|
|
|
1101
1150
|
```ts
|
|
1102
1151
|
registerBlockCompleter(myKind, {
|
|
1103
1152
|
tryComplete: (line) =>
|
|
1104
|
-
line
|
|
1153
|
+
trimWhitespace(line) === '$$'
|
|
1105
1154
|
? { lines: ['$$', '', '$$'], caret: { path: [], line: 1, column: 0 } }
|
|
1106
1155
|
: null
|
|
1107
1156
|
});
|
|
1108
1157
|
```
|
|
1109
1158
|
|
|
1110
|
-
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.
|
|
1111
1160
|
|
|
1112
|
-
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.
|
|
1113
1162
|
|
|
1114
|
-
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.
|
|
1115
1164
|
|
|
1116
1165
|
Two bounds worth knowing:
|
|
1117
1166
|
|
|
@@ -1124,10 +1173,10 @@ Content that's _itself editable_ comes in four tiers, and each one is backed by
|
|
|
1124
1173
|
|
|
1125
1174
|
| Tier | What it hosts | Status |
|
|
1126
1175
|
| ----------------- | -------------------------------------------------------------------------------- | ---------------------- |
|
|
1127
|
-
| **Container** | Real document blocks in a nested child list; the walkthrough's body | shipped
|
|
1128
|
-
| **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)_ |
|
|
1129
1178
|
| **Editable leaf** | A standalone text surface with native caret/IME/undo/selection/clipboard parity | shipped _(pre-freeze)_ |
|
|
1130
|
-
| **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)_ |
|
|
1131
1180
|
|
|
1132
1181
|
The chrome leaf is deliberately narrow, and each limit is a guarantee its container can lean on:
|
|
1133
1182
|
|
|
@@ -1158,32 +1207,34 @@ const leaf = createEditableLeaf({
|
|
|
1158
1207
|
getEl: () => sourceEl ?? null, // null while a render-primary view is folded
|
|
1159
1208
|
mode: 'render-primary', // 'plain' is the default
|
|
1160
1209
|
singleLine: true, // a one-line kind: Enter splits the block instead of typing a newline
|
|
1161
|
-
isRevealed: () => revealed, // render-primary only: you own the swap flag
|
|
1210
|
+
isRevealed: () => revealed, // render-primary only, and required there: you own the swap flag
|
|
1162
1211
|
setRevealed: (next) => (revealed = next)
|
|
1212
|
+
// optional too: commandHooks, handed to your block commands as ctx.hooks (see Block commands)
|
|
1163
1213
|
});
|
|
1164
1214
|
leaf.sourceText; // the block's raw minus its trailing line ending
|
|
1165
1215
|
leaf.getPresentationMode(); // 'source'
|
|
1166
|
-
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
|
|
1167
1218
|
```
|
|
1168
1219
|
|
|
1169
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.
|
|
1170
1221
|
|
|
1171
|
-
**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.
|
|
1172
1223
|
|
|
1173
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.
|
|
1174
1225
|
|
|
1175
|
-
**A painted source.** By default the source is one text node. A `renderSource(text)` dep paints it as DOM instead (fence lines the marker-hiding modes collapse, highlight tokens;
|
|
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.
|
|
1176
1227
|
|
|
1177
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.
|
|
1178
1229
|
|
|
1179
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:
|
|
1180
1231
|
|
|
1181
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.
|
|
1182
|
-
- **`'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`.
|
|
1183
1234
|
|
|
1184
|
-
**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.
|
|
1185
1236
|
|
|
1186
|
-
**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:
|
|
1187
1238
|
|
|
1188
1239
|
```
|
|
1189
1240
|
commit(edited text) ── parse ──▶ same kind? update in place, caret preserved
|
|
@@ -1195,23 +1246,15 @@ commit(edited text) ── parse ──▶ same kind? update in place, ca
|
|
|
1195
1246
|
|
|
1196
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.
|
|
1197
1248
|
|
|
1198
|
-
**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.
|
|
1199
1250
|
|
|
1200
|
-
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.
|
|
1201
1252
|
|
|
1202
1253
|
## Presentation modes
|
|
1203
1254
|
|
|
1204
|
-
**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.
|
|
1205
1256
|
|
|
1206
|
-
|
|
|
1207
|
-
| ---------------- | ------- | --------------------------------------------- |
|
|
1208
|
-
| `source` | live | every marker, dimmed |
|
|
1209
|
-
| `reading` | none | no markers, no reveals |
|
|
1210
|
-
| `preview-block` | live | markers only in the focused block |
|
|
1211
|
-
| `preview-inline` | live | syntax only for the construct under the caret |
|
|
1212
|
-
| `live` | live | no markers anywhere, nothing revealed |
|
|
1213
|
-
|
|
1214
|
-
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.
|
|
1215
1258
|
|
|
1216
1259
|
How each tier reads it:
|
|
1217
1260
|
|
|
@@ -1234,12 +1277,12 @@ In `reading` mode the platform does most of it for you, which is why most plugin
|
|
|
1234
1277
|
|
|
1235
1278
|
- your editable leaf never reveals and never commits;
|
|
1236
1279
|
- chord dispatch (block commands, global commands, keymaps) is swallowed at the dispatcher;
|
|
1237
|
-
- 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);
|
|
1238
1281
|
- marker spans hide by CSS.
|
|
1239
1282
|
|
|
1240
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.
|
|
1241
1284
|
|
|
1242
|
-
`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.
|
|
1243
1286
|
|
|
1244
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:
|
|
1245
1288
|
|
|
@@ -1252,11 +1295,11 @@ You read the mode yourself in two cases: when your component owns an edit afford
|
|
|
1252
1295
|
|
|
1253
1296
|
Reactivity is **per tier, not universal**, and that's worth being upfront about.
|
|
1254
1297
|
|
|
1255
|
-
**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).
|
|
1256
1299
|
|
|
1257
|
-
**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.
|
|
1258
1301
|
|
|
1259
|
-
**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.
|
|
1260
1303
|
|
|
1261
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".
|
|
1262
1305
|
|
|
@@ -1272,46 +1315,56 @@ fence claim ──▶ opaque container, NO children ──▶ component renders
|
|
|
1272
1315
|
rebuildRaw re-emits the fence commits ride updateOwnMetadata
|
|
1273
1316
|
```
|
|
1274
1317
|
|
|
1275
|
-
- **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.
|
|
1276
|
-
- **
|
|
1277
|
-
- **
|
|
1278
|
-
- **
|
|
1279
|
-
- **
|
|
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.
|
|
1280
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.
|
|
1281
1325
|
- **View-state commands reach the component through `ctx.hooks`.** See [Block commands](#block-commands).
|
|
1282
1326
|
|
|
1283
|
-
The
|
|
1327
|
+
The helpers from that list, with what they hand back:
|
|
1284
1328
|
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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
|
|
1290
1336
|
|
|
1291
|
-
const
|
|
1292
|
-
|
|
1293
|
-
|
|
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
|
+
````
|
|
1294
1347
|
|
|
1295
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.
|
|
1296
1349
|
|
|
1297
1350
|
### Whole-block focus
|
|
1298
1351
|
|
|
1299
|
-
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.
|
|
1300
1353
|
|
|
1301
1354
|
The mechanics behind that, each with its gotcha:
|
|
1302
1355
|
|
|
1303
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.
|
|
1304
1357
|
- **Give your box `position: relative`**, or the host resolves against whatever ancestor happens to be positioned.
|
|
1305
|
-
- **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.
|
|
1306
1359
|
- **An editable declared surface keeps focus for itself** (your edit `<textarea>`), which owns its caret and IME already.
|
|
1307
1360
|
|
|
1308
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.
|
|
1309
1362
|
|
|
1310
|
-
### What
|
|
1363
|
+
### What your own editing surface has to do
|
|
1311
1364
|
|
|
1312
|
-
**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.
|
|
1313
1366
|
|
|
1314
|
-
**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.
|
|
1315
1368
|
|
|
1316
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.
|
|
1317
1370
|
|
|
@@ -1321,51 +1374,80 @@ A block component gets its own node, which is fine right up until it isn't: a ta
|
|
|
1321
1374
|
|
|
1322
1375
|
```svelte
|
|
1323
1376
|
<script lang="ts">
|
|
1324
|
-
import { getContentRange, type DocumentView } from '@voithos-labs/aragonite/plugin';
|
|
1377
|
+
import { getContentRange, walkBlocks, type DocumentView } from '@voithos-labs/aragonite/plugin';
|
|
1325
1378
|
|
|
1326
1379
|
// A component receives its own node too; this block needs only the document.
|
|
1327
1380
|
let { document }: { document?: DocumentView } = $props();
|
|
1328
1381
|
|
|
1329
1382
|
// A $derived over the prop subscribes to the CST proxy, so editing a heading
|
|
1330
1383
|
// above re-runs this and the list updates live.
|
|
1331
|
-
const headings = $derived(
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
})
|
|
1338
|
-
|
|
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
|
+
});
|
|
1339
1394
|
</script>
|
|
1340
1395
|
|
|
1341
1396
|
<nav>
|
|
1342
|
-
{#each headings as
|
|
1397
|
+
{#each headings as heading}<div>{heading.text}</div>{/each}
|
|
1343
1398
|
</nav>
|
|
1344
1399
|
```
|
|
1345
1400
|
|
|
1346
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.
|
|
1347
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
|
+
|
|
1348
1426
|
```ts
|
|
1427
|
+
blockNodeAt(doc, [1, 0])?.raw; // '## Quoted\n'
|
|
1428
|
+
blockNodeAt(doc, []); // null
|
|
1349
1429
|
getContentRange(parse('# Hi\n').children[0]); // { start: 2, end: 4 }: the two bytes of 'Hi', markers skipped
|
|
1350
1430
|
getContentRange(parse('plain text\n').children[0]); // { start: 0, end: 10 }: a paragraph has no markers to skip
|
|
1351
|
-
await rects.navigateTo([
|
|
1431
|
+
await rects.navigateTo([1, 0]); // true once the quoted heading is in view with the caret at its start
|
|
1352
1432
|
```
|
|
1353
1433
|
|
|
1354
|
-
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.
|
|
1355
1435
|
|
|
1356
1436
|
## Inline kinds
|
|
1357
1437
|
|
|
1358
1438
|
Blocks are only half the story. An inline kind takes three calls, mirroring the block tier's declare, describe, recognize:
|
|
1359
1439
|
|
|
1360
|
-
- **`declarePluginInlineKind(name)`**
|
|
1361
|
-
- **`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
|
|
1362
|
-
- **`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.
|
|
1363
1443
|
|
|
1364
|
-
The three together, for a `:shortcode:` kind
|
|
1444
|
+
The three together, for a `:shortcode:` kind:
|
|
1365
1445
|
|
|
1366
1446
|
```ts
|
|
1367
1447
|
const shortcode = declarePluginInlineKind('shortcode'); // 'shortcode', branded
|
|
1368
|
-
|
|
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 });
|
|
1369
1451
|
registerInlineWidgetKind(shortcode, {
|
|
1370
1452
|
isWidget: (node) => node.kind === shortcode,
|
|
1371
1453
|
component: ShortcodeWidget,
|
|
@@ -1373,30 +1455,42 @@ registerInlineWidgetKind(shortcode, {
|
|
|
1373
1455
|
});
|
|
1374
1456
|
```
|
|
1375
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
|
+
|
|
1376
1462
|
A widget renders through one of two paths, and the descriptor rejects declaring both:
|
|
1377
1463
|
|
|
1378
|
-
- **A `component` (recommended).** Supply a Svelte component; the editor wraps it in the
|
|
1379
|
-
- **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.
|
|
1380
1468
|
|
|
1381
|
-
|
|
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 |
|
|
1382
1476
|
|
|
1383
|
-
|
|
1384
|
-
- `getDocument`: the read-only root document.
|
|
1385
|
-
- `getContentVersion`: a number that changes whenever the document's bytes change, and is stable otherwise.
|
|
1477
|
+
Three habits for those props:
|
|
1386
1478
|
|
|
1387
|
-
|
|
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.
|
|
1388
1482
|
|
|
1389
|
-
|
|
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.
|
|
1390
1484
|
|
|
1391
|
-
|
|
1485
|
+
### Choosing a trigger
|
|
1392
1486
|
|
|
1393
|
-
**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.
|
|
1394
1488
|
|
|
1395
|
-
**
|
|
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.
|
|
1396
1490
|
|
|
1397
|
-
|
|
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.)
|
|
1398
1492
|
|
|
1399
|
-
**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):
|
|
1400
1494
|
|
|
1401
1495
|
```ts
|
|
1402
1496
|
registerInlineSyntax('[', recognizeFootnote, {
|
|
@@ -1405,13 +1499,17 @@ registerInlineSyntax('[', recognizeFootnote, {
|
|
|
1405
1499
|
});
|
|
1406
1500
|
```
|
|
1407
1501
|
|
|
1408
|
-
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.
|
|
1409
1503
|
|
|
1410
|
-
|
|
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.
|
|
1411
1505
|
|
|
1412
|
-
|
|
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.
|
|
1413
1507
|
|
|
1414
|
-
|
|
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:
|
|
1415
1513
|
|
|
1416
1514
|
```ts
|
|
1417
1515
|
const dollarAt = createScanIndex((raw) => {
|
|
@@ -1423,51 +1521,52 @@ dollarAt('pay $HOME $5 for $x$', 5); // 10, the first candidate at or after offs
|
|
|
1423
1521
|
dollarAt('pay $HOME $5 for $x$', 20); // -1, none left
|
|
1424
1522
|
```
|
|
1425
1523
|
|
|
1426
|
-
|
|
1524
|
+
### Building a built-in node
|
|
1427
1525
|
|
|
1428
|
-
**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:
|
|
1429
1527
|
|
|
1430
1528
|
```ts
|
|
1431
1529
|
registerInlineSyntax('!', recognizeEmbed, {
|
|
1432
1530
|
prefix: '![[',
|
|
1433
1531
|
priority: INLINE_PRIORITIES.prefixOverride,
|
|
1434
1532
|
rewriteImage: (source, fields) => {
|
|
1435
|
-
if (!source.startsWith('![[')) return null; // bytes this
|
|
1533
|
+
if (!source.startsWith('![[')) return null; // bytes this handler did not shape
|
|
1436
1534
|
// Decline what this grammar cannot store rather than dropping it silently: it
|
|
1437
1535
|
// holds a target and an optional width and nothing else. The alt line is THIS
|
|
1438
1536
|
// recognizer's version of that rule: it fills alt and url from the one target,
|
|
1439
1537
|
// so an alt that no longer matches is an edit with no form here. Write yours
|
|
1440
1538
|
// against however your own recognizer fills the node.
|
|
1441
1539
|
if (fields.title !== undefined || fields.label !== undefined) return null;
|
|
1540
|
+
if (fields.height !== undefined || fields.crop !== undefined) return null;
|
|
1442
1541
|
if (fields.alt !== fields.url) return null;
|
|
1443
1542
|
return `![[${fields.url}${fields.width !== undefined ? `|${fields.width}` : ''}]]`;
|
|
1444
1543
|
}
|
|
1445
1544
|
});
|
|
1446
1545
|
```
|
|
1447
1546
|
|
|
1448
|
-
`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.
|
|
1449
1548
|
|
|
1450
1549
|
Three edges the snippet above is shaped by, and each one bites if you drop it:
|
|
1451
1550
|
|
|
1452
|
-
- **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.
|
|
1453
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.
|
|
1454
|
-
- **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.
|
|
1455
1554
|
|
|
1456
|
-
|
|
1555
|
+
### The editing policy
|
|
1457
1556
|
|
|
1458
|
-
|
|
1557
|
+
The policy on your widget registration says how the caret and the delete keys treat it. Its fields, all optional:
|
|
1459
1558
|
|
|
1460
|
-
| Field | What it decides
|
|
1461
|
-
| ----------------------- |
|
|
1462
|
-
| `revealSource` | Open the source (the `$…$` bytes) for editing on caret entry; inline math's model
|
|
1463
|
-
| `revealContentSpan` | Where the editable content sits inside the source (`$x$` answers `{ start: 1, end: 2 }`), so a caret entering the source stays between the delimiters; absent, it keeps the leading edge
|
|
1464
|
-
| `revealOffsetAtPoint` | Which source offset a press on the rendered widget names, so a click
|
|
1465
|
-
| `onSelectedKey` | A handler for keys while the widget is selected; image resize rides it
|
|
1466
|
-
| `onEdge` | `'select' \| 'step-over'`: an edge press selects the whole widget, or steps transparently over it; `'step-over'` also makes a press on the widget
|
|
1467
|
-
| `deleteGranularity` | `'atomic' \| 'select-then-delete'`: one press deletes the whole widget, or the first press selects and the second deletes
|
|
1468
|
-
| `claimsActivationClick` | Your component handles the activation click itself, so the
|
|
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 |
|
|
1469
1568
|
|
|
1470
|
-
Both edge fields are live today: the built-in decoded-entity widget (`©` → ©) ships `{ deleteGranularity: 'atomic', onEdge: 'step-over' }`, so a caret-adjacent Backspace removes it whole and a plain arrow walks the caret across it like a character, the caret-edge
|
|
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.
|
|
1471
1570
|
|
|
1472
1571
|
## Decorations
|
|
1473
1572
|
|
|
@@ -1496,12 +1595,12 @@ setup(ctx) {
|
|
|
1496
1595
|
|
|
1497
1596
|
### The four decoration types
|
|
1498
1597
|
|
|
1499
|
-
| Type | Shape | Renders as
|
|
1500
|
-
| --------- | -------------------------------------------------------- |
|
|
1501
|
-
| `mark` | `{ type: 'mark', path, start, end, class }`
|
|
1502
|
-
| `widget` | `{ type: 'widget', path, offset, widget }`
|
|
1503
|
-
| `replace` | `{ type: 'replace', path, start, end, widget?, class? }` | An atomic
|
|
1504
|
-
| `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 |
|
|
1505
1604
|
|
|
1506
1605
|
One `provide` answer using two of them, shapes side by side:
|
|
1507
1606
|
|
|
@@ -1512,13 +1611,13 @@ provide: (doc) => [
|
|
|
1512
1611
|
];
|
|
1513
1612
|
```
|
|
1514
1613
|
|
|
1515
|
-
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`.
|
|
1516
1615
|
|
|
1517
|
-
|
|
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.
|
|
1518
1617
|
|
|
1519
1618
|
### Recipe: memoize the scan on `editEpoch`
|
|
1520
1619
|
|
|
1521
|
-
`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.
|
|
1522
1621
|
|
|
1523
1622
|
```ts
|
|
1524
1623
|
let lastEpoch = -1;
|
|
@@ -1543,11 +1642,11 @@ editor.events.on('selectionChange', (sel) => {
|
|
|
1543
1642
|
});
|
|
1544
1643
|
```
|
|
1545
1644
|
|
|
1546
|
-
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.
|
|
1547
1646
|
|
|
1548
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.
|
|
1549
1648
|
|
|
1550
|
-
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).
|
|
1551
1650
|
|
|
1552
1651
|
```ts
|
|
1553
1652
|
editor.rects.rangeRects([2], 4, 9); // [DOMRect { x: 96, y: 412, width: 38, height: 22, ... }], one per visual line the range crosses
|
|
@@ -1557,7 +1656,7 @@ editor.rects.rangeRects([2], 4, 9); // [DOMRect { x: 96, y: 412, width: 38, heig
|
|
|
1557
1656
|
|
|
1558
1657
|
**`registerBlockCommand(kind, name, handler)`**
|
|
1559
1658
|
|
|
1560
|
-
|
|
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.
|
|
1561
1660
|
|
|
1562
1661
|
```ts
|
|
1563
1662
|
const setVerdict = registerBlockCommand(conspiracy, 'conspiracy.setVerdict', (ctx) => {
|
|
@@ -1570,31 +1669,31 @@ setVerdict; // 'conspiracy.setVerdict', branded as a command id
|
|
|
1570
1669
|
registerBlockCommand(conspiracy, 'conspiracy.setVerdict', handler); // throws: already registered
|
|
1571
1670
|
```
|
|
1572
1671
|
|
|
1573
|
-
A
|
|
1672
|
+
A block command dispatches on the two tiers that can hand it a `BlockCommandContext` (the focused node plus a metadata-commit route):
|
|
1574
1673
|
|
|
1575
1674
|
- the **editable-leaf tier**, a `createEditableLeaf` block, resolved from the focused leaf's keymap;
|
|
1576
1675
|
- the **container-bubble tier**, a container-factory block, resolved as a chord bubbles up from an inner leaf.
|
|
1577
1676
|
|
|
1578
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.
|
|
1579
1678
|
|
|
1580
|
-
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).
|
|
1581
1680
|
|
|
1582
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.
|
|
1583
1682
|
|
|
1584
|
-
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.
|
|
1585
1684
|
|
|
1586
|
-
**`registerGlobalCommand(name, handler, { chord })`**
|
|
1685
|
+
**`registerGlobalCommand(name, handler, { chord }?)`**
|
|
1587
1686
|
|
|
1588
|
-
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`:
|
|
1589
1688
|
|
|
1590
1689
|
```ts
|
|
1591
1690
|
setup(ctx) {
|
|
1592
1691
|
registerGlobalCommand(
|
|
1593
1692
|
'wordCount.log',
|
|
1594
1693
|
(editor) => {
|
|
1595
|
-
// The
|
|
1596
|
-
// so
|
|
1597
|
-
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;
|
|
1598
1697
|
console.log(`[${editor.editorId}]`, countByEditor.get(editor.editorId), opts);
|
|
1599
1698
|
return true; // handled
|
|
1600
1699
|
},
|
|
@@ -1604,10 +1703,10 @@ setup(ctx) {
|
|
|
1604
1703
|
}
|
|
1605
1704
|
```
|
|
1606
1705
|
|
|
1607
|
-
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:
|
|
1608
1707
|
|
|
1609
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**.
|
|
1610
|
-
- 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.
|
|
1611
1710
|
- A handler throw is contained identically, surfacing as an `error` of origin `command` attributed to the owning plugin.
|
|
1612
1711
|
|
|
1613
1712
|
```ts
|
|
@@ -1617,16 +1716,16 @@ registerGlobalCommand('mine.undo', handler, { chord: 'Mod+Z' }); // throws: alre
|
|
|
1617
1716
|
registerGlobalCommand('mine.bold', handler, { chord: 'Mod+B' }); // fine: fires on a thematic break, yields to bold in a paragraph
|
|
1618
1717
|
```
|
|
1619
1718
|
|
|
1620
|
-
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.
|
|
1621
1720
|
|
|
1622
1721
|
## Block context actions
|
|
1623
1722
|
|
|
1624
|
-
**`registerBlockContextActions(kind, provider)`**
|
|
1723
|
+
**`registerBlockContextActions(kind, name, provider)`**
|
|
1625
1724
|
|
|
1626
|
-
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".
|
|
1627
1726
|
|
|
1628
1727
|
```ts
|
|
1629
|
-
registerBlockContextActions(conspiracy, (node) => [
|
|
1728
|
+
registerBlockContextActions(conspiracy, 'debunk', (node) => [
|
|
1630
1729
|
{
|
|
1631
1730
|
id: 'conspiracy.debunk',
|
|
1632
1731
|
label: 'Mark debunked',
|
|
@@ -1636,11 +1735,11 @@ registerBlockContextActions(conspiracy, (node) => [
|
|
|
1636
1735
|
]);
|
|
1637
1736
|
```
|
|
1638
1737
|
|
|
1639
|
-
`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.
|
|
1640
1739
|
|
|
1641
1740
|
## Paste transforms
|
|
1642
1741
|
|
|
1643
|
-
`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.
|
|
1644
1743
|
|
|
1645
1744
|
```ts
|
|
1646
1745
|
registerPasteTransform({
|
|
@@ -1655,7 +1754,9 @@ registerPasteTransform({ name: 'shout', transform: () => null }); // throws: "sh
|
|
|
1655
1754
|
Two habits keep a transform sound:
|
|
1656
1755
|
|
|
1657
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.
|
|
1658
|
-
- **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.
|
|
1659
1760
|
|
|
1660
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.
|
|
1661
1762
|
|
|
@@ -1694,7 +1795,7 @@ A plugin **may**:
|
|
|
1694
1795
|
- Register kinds, components, and openers, once; a duplicate throws.
|
|
1695
1796
|
- Declare a `rebuildRaw` and have the editor invoke it when the document changes.
|
|
1696
1797
|
- Build containers and chrome through the factories.
|
|
1697
|
-
- Store primitive per-node metadata, and commit metadata through the
|
|
1798
|
+
- Store primitive per-node metadata, and commit metadata through the supported update path.
|
|
1698
1799
|
- Contribute per-kind keymaps over the command vocabulary.
|
|
1699
1800
|
- Render as an unknown kind and degrade to a visible raw fallback.
|
|
1700
1801
|
- Transform pasted plain text before it's parsed ([Paste transforms](#paste-transforms)).
|
|
@@ -1721,7 +1822,3 @@ Why the dev build is where plugin development belongs, stated as what each mista
|
|
|
1721
1822
|
| An opener claims no line (`consumed < 1`) | Warns, naming the kind, and declines the opener | Declines the same way, silently; no hang |
|
|
1722
1823
|
| An opener's `raw` ≠ the lines it consumed | Parse warns, naming the kind | Silent round-trip break |
|
|
1723
1824
|
| An opener throws | Propagates uncaught (parse runs at init and on every edit) | Same; uncaught |
|
|
1724
|
-
|
|
1725
|
-
## Where to go next
|
|
1726
|
-
|
|
1727
|
-
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).
|