@voithos-labs/aragonite 0.10.2 → 0.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -21
- package/THIRD-PARTY-NOTICES.md +25 -0
- package/dist/a11y-strings.d.ts +19 -0
- package/dist/a11y-strings.js +19 -0
- package/dist/action-contracts.d.ts +7 -1
- package/dist/ambient/ambient-dom.js +5 -1
- package/dist/block-component.d.ts +14 -0
- package/dist/components/BlockDragHandle.svelte +37 -24
- package/dist/components/BlockHost.svelte +18 -11
- package/dist/components/Editor.svelte +434 -198
- package/dist/components/Editor.svelte.d.ts +1 -1
- package/dist/components/SelectionOverlay.svelte +25 -17
- package/dist/components/SelectionOverlay.svelte.d.ts +1 -1
- package/dist/components/TailInsert.svelte +107 -0
- package/dist/components/TailInsert.svelte.d.ts +17 -0
- package/dist/components/block-content-selector.d.ts +6 -2
- package/dist/components/block-content-selector.js +6 -2
- package/dist/components/blocks/ThematicBreakBlock.svelte +19 -6
- package/dist/components/blocks/ThematicBreakBlock.svelte.d.ts +1 -0
- package/dist/components/blocks/code/CodeBlock.svelte +211 -30
- package/dist/components/blocks/code/CodeBlockRail.svelte +704 -0
- package/dist/components/blocks/code/CodeBlockRail.svelte.d.ts +26 -0
- package/dist/components/blocks/code/code-bootstrap.js +16 -0
- package/dist/components/blocks/code/code-context-actions.d.ts +1 -0
- package/dist/components/blocks/code/code-context-actions.js +24 -0
- package/dist/components/blocks/code/code-fence-exit.d.ts +15 -0
- package/dist/components/blocks/code/code-fence-exit.js +26 -0
- package/dist/components/blocks/code/code-languages.d.ts +6 -0
- package/dist/components/blocks/code/code-languages.js +22 -3
- package/dist/components/blocks/code/code-renderer.js +11 -0
- package/dist/components/blocks/directive/DirectiveContainerBlock.svelte +1 -1
- package/dist/components/blocks/editable-leaf.d.ts +37 -6
- package/dist/components/blocks/editable-leaf.js +248 -30
- package/dist/components/blocks/editable-surface.d.ts +9 -0
- package/dist/components/blocks/editable-surface.js +22 -3
- package/dist/components/blocks/list/ListBlock.svelte +1 -0
- package/dist/components/blocks/list/ListItemBlock.svelte +14 -5
- package/dist/components/blocks/list/ListItemBlock.svelte.d.ts +2 -0
- package/dist/components/blocks/list/task-checkbox.d.ts +2 -0
- package/dist/components/blocks/list/task-checkbox.js +11 -2
- package/dist/components/blocks/surface-wiring.svelte.d.ts +4 -0
- package/dist/components/blocks/surface-wiring.svelte.js +8 -1
- package/dist/components/blocks/table/TableActionMenu.svelte +210 -86
- package/dist/components/blocks/table/TableActionMenu.svelte.d.ts +5 -0
- package/dist/components/blocks/table/TableBlock.svelte +205 -186
- package/dist/components/blocks/table/TableBlock.svelte.d.ts +1 -0
- package/dist/components/blocks/table/TableCellBlock.svelte +87 -22
- package/dist/components/blocks/table/TableRowBlock.svelte +4 -18
- package/dist/components/blocks/table/TableRowBlock.svelte.d.ts +0 -5
- package/dist/components/blocks/table/cell-clipboard.d.ts +10 -0
- package/dist/components/blocks/table/cell-clipboard.js +34 -1
- package/dist/components/blocks/table/cell-keydown-plan.d.ts +1 -1
- package/dist/components/blocks/table/cell-keydown-plan.js +3 -1
- package/dist/components/blocks/table/table-cell-paste.js +2 -1
- package/dist/components/blocks/table/table-menu-model.d.ts +35 -8
- package/dist/components/blocks/table/table-menu-model.js +36 -17
- package/dist/components/blocks/text/TextEditableBlock.svelte +58 -11
- package/dist/components/blocks/text/click-snap-guard.d.ts +3 -0
- package/dist/components/blocks/text/click-snap-guard.js +8 -0
- package/dist/components/blocks/text/delimiter-autopair.d.ts +71 -0
- package/dist/components/blocks/text/delimiter-autopair.js +216 -0
- package/dist/components/blocks/text/edge-policy-dispatch.d.ts +4 -0
- package/dist/components/blocks/text/edge-policy-dispatch.js +89 -20
- package/dist/components/blocks/text/live-selection-edit.d.ts +5 -5
- package/dist/components/blocks/text/live-selection-edit.js +37 -7
- package/dist/components/blocks/text/text-clipboard.js +2 -2
- package/dist/components/blocks/text/text-keydown.d.ts +7 -1
- package/dist/components/blocks/text/text-keydown.js +11 -1
- package/dist/components/blocks/text/text-render.d.ts +1 -1
- package/dist/components/blocks/text/text-render.js +3 -1
- package/dist/components/blocks/text/widget-interaction.d.ts +3 -0
- package/dist/components/blocks/text/widget-interaction.js +181 -37
- package/dist/components/drag-handle.d.ts +41 -0
- package/dist/components/drag-handle.js +134 -0
- package/dist/components/editor-root-focus.d.ts +19 -0
- package/dist/components/editor-root-focus.js +67 -0
- package/dist/components/editor-root-geometry.d.ts +39 -0
- package/dist/components/editor-root-geometry.js +91 -0
- package/dist/components/editor-root-keydown.d.ts +1 -1
- package/dist/components/editor-root-keydown.js +12 -3
- package/dist/components/editor-root-listeners.d.ts +3 -6
- package/dist/components/editor-root-listeners.js +3 -24
- package/dist/components/editor-root-mode-flip.d.ts +36 -0
- package/dist/components/editor-root-mode-flip.js +92 -0
- package/dist/components/image/ImageOverlayHost.svelte +14 -6
- package/dist/components/image/ImageProperties.svelte +512 -70
- package/dist/components/image/ImageProperties.svelte.d.ts +6 -1
- package/dist/components/image/ImageResizeHandles.svelte +67 -34
- package/dist/components/image/image-crop.d.ts +39 -0
- package/dist/components/image/image-crop.js +74 -0
- package/dist/components/image/image-edit-commit.d.ts +1 -0
- package/dist/components/image/image-edit-commit.js +21 -5
- package/dist/components/image/image-source-bytes.js +10 -3
- package/dist/components/image/image-widget-editing.js +1 -0
- package/dist/components/image/widget-dom.js +5 -1
- package/dist/components/link-card/link-card-commit.js +1 -1
- package/dist/components/lrd-map-gate.js +1 -1
- package/dist/components/menu/BlockMenu.svelte +315 -0
- package/dist/components/menu/BlockMenu.svelte.d.ts +34 -0
- package/dist/components/menu/MenuIcon.svelte +153 -0
- package/dist/components/menu/MenuIcon.svelte.d.ts +51 -0
- package/dist/components/menu/SelectionToolbar.svelte +385 -0
- package/dist/components/menu/SelectionToolbar.svelte.d.ts +16 -0
- package/dist/components/menu/clipboard-actions.d.ts +12 -0
- package/dist/components/menu/clipboard-actions.js +42 -0
- package/dist/components/menu/default-context-actions.d.ts +15 -0
- package/dist/components/menu/default-context-actions.js +76 -0
- package/dist/components/menu/flyout-placement.d.ts +6 -0
- package/dist/components/menu/flyout-placement.js +25 -0
- package/dist/core/inline/format-toggle.d.ts +12 -4
- package/dist/core/inline/format-toggle.js +94 -40
- package/dist/core/inline/image-dimensions.d.ts +3 -0
- package/dist/core/inline/image-dimensions.js +46 -10
- package/dist/core/inline/inline-widgets.d.ts +19 -0
- package/dist/core/inline/inline-widgets.js +5 -0
- package/dist/core/inline/scan/brackets.js +1 -0
- package/dist/core/inline/scan/plugin-syntax.d.ts +8 -0
- package/dist/core/inline/scan/plugin-syntax.js +15 -1
- package/dist/core/inline/transparency.js +3 -3
- package/dist/core/inline-render.d.ts +6 -0
- package/dist/core/inline-render.js +32 -0
- package/dist/core/nodes.d.ts +14 -0
- package/dist/cursor/edge-affinity.js +2 -1
- package/dist/cursor/height-oracle.d.ts +2 -3
- package/dist/cursor/height-oracle.js +0 -1
- package/dist/cursor/overlay-remeasure.js +8 -0
- package/dist/cursor/reveal-source.js +7 -2
- package/dist/cursor/scroll-hold.d.ts +10 -0
- package/dist/cursor/scroll-hold.js +22 -0
- package/dist/cursor/scrollport.d.ts +9 -0
- package/dist/cursor/scrollport.js +30 -1
- package/dist/cursor/visual-lines.d.ts +5 -4
- package/dist/cursor/visual-lines.js +56 -11
- package/dist/cursor/widget-edge-snap.d.ts +28 -0
- package/dist/cursor/widget-edge-snap.js +38 -0
- package/dist/cursor/widget-offset.d.ts +9 -0
- package/dist/cursor/widget-offset.js +71 -4
- package/dist/debug/interaction-trace.d.ts +4 -0
- package/dist/debug/interaction-trace.js +15 -0
- package/dist/decorations/decoration-state.svelte.js +1 -1
- package/dist/decorations/reserved-attrs.js +1 -0
- package/dist/editor-actions/ancestry-folds.d.ts +2 -2
- package/dist/editor-actions/ancestry-folds.js +1 -1
- package/dist/editor-actions/block-edit-scope.js +1 -1
- package/dist/editor-actions/commit/text-batch.d.ts +3 -2
- package/dist/editor-actions/commit/text-batch.js +1 -1
- package/dist/editor-actions/commit/undo-controller.js +4 -2
- package/dist/editor-actions/container-edit.js +2 -1
- package/dist/editor-actions/enter-completion.d.ts +2 -0
- package/dist/editor-actions/enter-completion.js +23 -2
- package/dist/editor-actions/focus/focus-dispatch.js +7 -4
- package/dist/editor-actions/focus/focus-landing.d.ts +10 -1
- package/dist/editor-actions/focus/focus-landing.js +20 -6
- package/dist/editor-actions/inline-range-commit.js +1 -1
- package/dist/editor-actions/plugin/container.d.ts +7 -0
- package/dist/editor-actions/plugin/container.js +3 -1
- package/dist/editor-actions/reorder-action.js +19 -10
- package/dist/editor-actions/reorder-drag.js +29 -1
- package/dist/editor-actions/replacement-focus.d.ts +1 -1
- package/dist/editor-actions/replacement-focus.js +1 -1
- package/dist/editor-actions/search-replace.js +1 -1
- package/dist/editor-actions/table-context.d.ts +4 -1
- package/dist/editor-actions/table-context.js +57 -1
- package/dist/editor-events.d.ts +3 -0
- package/dist/editor-keys.d.ts +33 -0
- package/dist/editor-props.d.ts +24 -12
- package/dist/index.d.ts +1 -1
- package/dist/plugin.d.ts +7 -0
- package/dist/plugin.js +16 -0
- package/dist/plugins/latex/BlockMath.svelte +264 -29
- package/dist/plugins/latex/BlockMath.svelte.d.ts +2 -0
- package/dist/plugins/latex/index.d.ts +2 -1
- package/dist/plugins/latex/latex-kind.js +60 -10
- package/dist/plugins/latex/math-completion.js +4 -1
- package/dist/plugins/latex/math-layout.d.ts +15 -0
- package/dist/plugins/latex/math-layout.js +13 -0
- package/dist/plugins/latex/math-source.d.ts +19 -0
- package/dist/plugins/latex/math-source.js +103 -0
- package/dist/plugins/latex/register.d.ts +11 -2
- package/dist/plugins/latex/register.js +3 -1
- package/dist/plugins/latex/renderer.d.ts +3 -3
- package/dist/plugins/latex/renderer.js +13 -6
- package/dist/plugins/mermaid/MermaidBlock.svelte +19 -9
- package/dist/plugins/parrot/ParrotBlock.svelte +3 -1
- package/dist/reactivity/list-windowing.svelte.d.ts +8 -8
- package/dist/reactivity/list-windowing.svelte.js +75 -30
- package/dist/schema/block-completions.d.ts +8 -0
- package/dist/schema/block-completions.js +11 -0
- package/dist/schema/commands.d.ts +9 -5
- package/dist/schema/commands.js +12 -7
- package/dist/schema/context-actions.d.ts +31 -0
- package/dist/schema/context-actions.js +23 -0
- package/dist/schema/fenced-code-raw.js +31 -1
- package/dist/schema/operations.d.ts +8 -1
- package/dist/schema/reserved-chords.js +37 -4
- package/dist/schema/table-cell-raw.d.ts +1 -1
- package/dist/schema/table-cell-raw.js +1 -1
- package/dist/selection/block-hit-test.js +3 -2
- package/dist/selection/char-endpoint-snap.js +1 -1
- package/dist/selection/clipboard-text.js +6 -1
- package/dist/selection/covered-block.d.ts +10 -0
- package/dist/selection/covered-block.js +24 -0
- package/dist/selection/cross-block/dispatch.d.ts +3 -0
- package/dist/selection/cross-block/dispatch.js +13 -1
- package/dist/selection/cross-block/format-range.d.ts +1 -1
- package/dist/selection/cross-block/format-range.js +4 -15
- package/dist/selection/cross-block/format-toggle.js +1 -1
- package/dist/selection/cross-block/keydown.js +3 -29
- package/dist/selection/cross-block/ops.js +1 -1
- package/dist/selection/cross-block/paste.js +31 -35
- package/dist/selection/cross-block/type-replace.d.ts +3 -2
- package/dist/selection/cross-block/type-replace.js +52 -13
- package/dist/selection/dead-space-caret.d.ts +13 -0
- package/dist/selection/dead-space-caret.js +57 -1
- package/dist/selection/drag-pointer.d.ts +26 -2
- package/dist/selection/drag-pointer.js +103 -2
- package/dist/selection/gap-caret.js +1 -1
- package/dist/selection/keyboard-extend.d.ts +3 -2
- package/dist/selection/keyboard-extend.js +4 -3
- package/dist/selection/multi-click.d.ts +42 -0
- package/dist/selection/multi-click.js +203 -0
- package/dist/selection/native-bridge.d.ts +3 -0
- package/dist/selection/native-bridge.js +31 -2
- package/dist/selection/path-lookup.d.ts +10 -2
- package/dist/selection/path-lookup.js +11 -4
- package/dist/selection/pointer-gesture.d.ts +9 -0
- package/dist/selection/pointer-gesture.js +11 -0
- package/dist/selection/primitives.d.ts +7 -0
- package/dist/selection/primitives.js +19 -1
- package/dist/selection/range-delete-ceremony.js +4 -2
- package/dist/selection/range-delete-chrome.js +2 -1
- package/dist/selection/range-delete-table-coverage.js +2 -1
- package/dist/selection/range-delete-table.js +3 -2
- package/dist/selection/range-delete.d.ts +4 -0
- package/dist/selection/range-delete.js +31 -3
- package/dist/selection/selection-drop.d.ts +34 -0
- package/dist/selection/selection-drop.js +253 -0
- package/dist/selection/selection-restore.js +1 -1
- package/dist/selection/selection-state.svelte.d.ts +6 -0
- package/dist/selection/selection-state.svelte.js +36 -1
- package/dist/selection/table-endpoint-snap.js +1 -1
- package/dist/selection/table-rect-extend.js +1 -1
- package/dist/styles/editor-theme.css +75 -31
- package/dist/styles/editor.css +268 -18
- package/dist/testing/container-conformance.js +2 -2
- package/dist/testing/inline-conformance.js +2 -1
- package/dist/tree-operations/blockquote.js +1 -1
- package/dist/tree-operations/chain-rebuild.d.ts +63 -0
- package/dist/tree-operations/chain-rebuild.js +142 -0
- package/dist/tree-operations/children.d.ts +1 -1
- package/dist/tree-operations/children.js +1 -1
- package/dist/tree-operations/cleanup.js +1 -1
- package/dist/tree-operations/content-write.d.ts +50 -0
- package/dist/tree-operations/content-write.js +263 -0
- package/dist/tree-operations/index.d.ts +8 -3
- package/dist/tree-operations/index.js +6 -2
- package/dist/tree-operations/list/exit-replacement.js +1 -1
- package/dist/tree-operations/list/unwrap-merge.js +3 -3
- package/dist/tree-operations/node-ops.d.ts +17 -234
- package/dist/tree-operations/node-ops.js +47 -1113
- package/dist/tree-operations/node-primitives.d.ts +75 -0
- package/dist/tree-operations/node-primitives.js +117 -0
- package/dist/tree-operations/paste/apply.js +1 -1
- package/dist/tree-operations/paste/body-write.d.ts +1 -1
- package/dist/tree-operations/paste/body-write.js +2 -2
- package/dist/tree-operations/paste/container-match.js +4 -2
- package/dist/tree-operations/paste/dispatch.js +2 -1
- package/dist/tree-operations/paste/find-enclosing-list.js +1 -1
- package/dist/tree-operations/paste/focus-target.d.ts +1 -1
- package/dist/tree-operations/paste/list-absorb.js +1 -1
- package/dist/tree-operations/paste/list-break-out.js +1 -1
- package/dist/tree-operations/paste/parent-scope.js +1 -1
- package/dist/tree-operations/paste/paste-replacement.js +1 -1
- package/dist/tree-operations/paste/replace-block-at-parent.d.ts +3 -1
- package/dist/tree-operations/paste/replace-block-at-parent.js +5 -2
- package/dist/tree-operations/paste/replacement-parse.d.ts +17 -0
- package/dist/tree-operations/paste/replacement-parse.js +22 -0
- package/dist/tree-operations/path-mutate.d.ts +1 -1
- package/dist/tree-operations/path-mutate.js +2 -1
- package/dist/tree-operations/reorder-unit.js +1 -1
- package/dist/tree-operations/reorder.d.ts +5 -2
- package/dist/tree-operations/reorder.js +55 -2
- package/dist/tree-operations/settle.d.ts +105 -0
- package/dist/tree-operations/settle.js +660 -0
- package/dist/tree-operations/table-grid-clipboard.d.ts +21 -0
- package/dist/tree-operations/table-grid-clipboard.js +90 -0
- package/dist/tree-operations/unshare.d.ts +15 -77
- package/dist/tree-operations/unshare.js +15 -166
- package/docs/guide/consumer-guide.md +136 -104
- package/docs/guide/plugin-api.md +73 -29
- package/docs/guide/plugin-guide.md +55 -23
- package/package.json +9 -7
- package/dist/components/blocks/code/CodeLanguageChip.svelte +0 -127
- package/dist/components/blocks/code/CodeLanguageChip.svelte.d.ts +0 -14
- package/dist/components/blocks/table/TableGrip.svelte +0 -91
- package/dist/components/blocks/table/TableGrip.svelte.d.ts +0 -8
- package/dist/components/blocks/table/table-drop-target.d.ts +0 -1
- package/dist/components/blocks/table/table-drop-target.js +0 -16
- package/dist/components/blocks/table/table-reorder-drag.d.ts +0 -78
- package/dist/components/blocks/table/table-reorder-drag.js +0 -97
|
@@ -8,10 +8,32 @@
|
|
|
8
8
|
// Render-primary editable leaf: all editing behavior lives in `createEditableLeaf`,
|
|
9
9
|
// so this component owns only the render↔source swap visuals.
|
|
10
10
|
import { createEditableLeaf, type BlockComponent, type NodeView } from '../../plugin';
|
|
11
|
+
// Plugin-local like the other labels here: bundled plugins import only the public barrel.
|
|
12
|
+
// The three ways the source and its preview can share the block while editing (see
|
|
13
|
+
// `math-layout.ts`); the host's plugin options pick the starting one. The toggle cycles
|
|
14
|
+
// them and names the NEXT layout.
|
|
15
|
+
type MathLayout = MathBlockLayout;
|
|
16
|
+
const LAYOUT_NEXT: Record<MathLayout, MathLayout> = {
|
|
17
|
+
split: 'stacked',
|
|
18
|
+
stacked: 'source',
|
|
19
|
+
source: 'split'
|
|
20
|
+
};
|
|
21
|
+
const LAYOUT_TITLE: Record<MathLayout, string> = {
|
|
22
|
+
split: 'Preview beside the source',
|
|
23
|
+
stacked: 'Preview below the source',
|
|
24
|
+
source: 'Source only'
|
|
25
|
+
};
|
|
11
26
|
import { renderDisplayMath } from './math-renderer';
|
|
12
27
|
import { mathDisplaySource } from './latex-kind';
|
|
28
|
+
import { completeBareMathSource, mathBodySpan, renderMathSource } from './math-source';
|
|
29
|
+
import { resolveDefaultLayout, type MathBlockLayout } from './math-layout';
|
|
13
30
|
|
|
14
|
-
let {
|
|
31
|
+
let {
|
|
32
|
+
node,
|
|
33
|
+
index,
|
|
34
|
+
myPath = [],
|
|
35
|
+
blockLayout = 'split'
|
|
36
|
+
}: { node: NodeView; index: number; myPath?: number[]; blockLayout?: MathBlockLayout } = $props();
|
|
15
37
|
|
|
16
38
|
// eslint-disable-next-line no-useless-assignment -- <script module> counter read by the next instance mount
|
|
17
39
|
const mountId = nextMountId++;
|
|
@@ -20,6 +42,9 @@
|
|
|
20
42
|
let sourceEl: HTMLDivElement | undefined = $state();
|
|
21
43
|
let renderEl: HTMLDivElement | undefined = $state();
|
|
22
44
|
let revealed = $state(false);
|
|
45
|
+
// The in-flight source while revealed: a render-primary edit reaches the CST only on blur,
|
|
46
|
+
// so the live preview reads the surface, not the node. Null when nothing is in flight.
|
|
47
|
+
let draft = $state<string | null>(null);
|
|
23
48
|
|
|
24
49
|
const leaf = createEditableLeaf({
|
|
25
50
|
getNode: () => node,
|
|
@@ -30,16 +55,58 @@
|
|
|
30
55
|
isRevealed: () => revealed,
|
|
31
56
|
setRevealed: (value) => {
|
|
32
57
|
revealed = value;
|
|
58
|
+
draft = null;
|
|
59
|
+
},
|
|
60
|
+
renderSource: renderMathSource,
|
|
61
|
+
completeBareSource: completeBareMathSource,
|
|
62
|
+
onSourceEdit: (text) => {
|
|
63
|
+
draft = text;
|
|
33
64
|
}
|
|
34
65
|
});
|
|
35
66
|
|
|
67
|
+
// While editing, the preview is a sibling of the surface that holds focus: its scrollbar (a
|
|
68
|
+
// wide equation overflows the half-width card) must not take that focus, since losing it is
|
|
69
|
+
// what folds the editor. Same device as the eye button; a fold click is a click while folded.
|
|
70
|
+
function keepSourceFocus(e: MouseEvent): void {
|
|
71
|
+
if (revealed) e.preventDefault();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Per-instance and per-session: a reader who changes the layout is asking about THIS equation
|
|
75
|
+
// while they edit it, not setting a preference for the document. The starting layout is the
|
|
76
|
+
// host's: this editor's plugin options, else the factory's default.
|
|
77
|
+
// svelte-ignore state_referenced_locally
|
|
78
|
+
let layout = $state<MathLayout>(resolveDefaultLayout(leaf.getOptions(), blockLayout));
|
|
79
|
+
const previewOpen = $derived(layout !== 'source');
|
|
80
|
+
|
|
81
|
+
// The edits the leaf applies itself report through `onSourceEdit`; this is the native path
|
|
82
|
+
// (a composition's commit), where the highlight goes stale until repainted. The leaf's own
|
|
83
|
+
// handler runs first so the IME bookkeeping it owns is untouched.
|
|
84
|
+
function onSourceInput(e: Event): void {
|
|
85
|
+
leaf.surfaceProps.oninput();
|
|
86
|
+
if ((e as InputEvent).isComposing) return;
|
|
87
|
+
leaf.repaintSource();
|
|
88
|
+
draft = sourceEl?.textContent ?? null;
|
|
89
|
+
}
|
|
90
|
+
|
|
36
91
|
// ── View rendering ──────────────────────────────────────────────────────────
|
|
37
92
|
|
|
38
93
|
// Re-runs on every remount of the render div and on any source change; the
|
|
39
94
|
// document-wide memo clones a cached node, so a repeat formula is cheap.
|
|
40
95
|
$effect(() => {
|
|
41
|
-
if (
|
|
42
|
-
|
|
96
|
+
if (!renderEl) return;
|
|
97
|
+
// Runs while REVEALED as well: the split keeps a live preview beside the source, so the
|
|
98
|
+
// equation re-renders as it is typed rather than only when the source folds away.
|
|
99
|
+
const text = draft ?? leaf.sourceText;
|
|
100
|
+
const source = mathDisplaySource(text);
|
|
101
|
+
renderEl.replaceChildren(renderDisplayMath(source).dom);
|
|
102
|
+
// The span a press on the glyphs lands in: the descriptor's `caretTargetAtPoint` reads the
|
|
103
|
+
// rendered element alone and has no other way to the bytes behind it.
|
|
104
|
+
const body = mathBodySpan(text);
|
|
105
|
+
renderEl.dataset.bodyStart = String(body.start);
|
|
106
|
+
renderEl.dataset.bodyEnd = String(body.end);
|
|
107
|
+
// An equation with nothing in it renders nothing, which folded would be an invisible block
|
|
108
|
+
// the user cannot find to delete; it keeps the card's fill instead, like an empty fence.
|
|
109
|
+
renderEl.toggleAttribute('data-empty', source.trim() === '');
|
|
43
110
|
renderCount += 1;
|
|
44
111
|
renderEl.dataset.renderCount = String(renderCount);
|
|
45
112
|
});
|
|
@@ -74,46 +141,214 @@
|
|
|
74
141
|
} satisfies BlockComponent);
|
|
75
142
|
</script>
|
|
76
143
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
144
|
+
<!-- Editing shows source AND render side by side rather than swapping one for the other: the
|
|
145
|
+
swap re-flowed the whole document on every click, and a preview that only appears after you
|
|
146
|
+
stop editing is the one you needed while typing. Each half is a card, and the eye sits in the
|
|
147
|
+
top-right of whichever card is showing. -->
|
|
148
|
+
<div
|
|
149
|
+
class="math-block"
|
|
150
|
+
class:math-block-editing={revealed}
|
|
151
|
+
class:math-block-split={revealed && layout === 'split'}
|
|
152
|
+
class:math-block-stacked={revealed && layout === 'stacked'}
|
|
153
|
+
>
|
|
154
|
+
{#if revealed}
|
|
155
|
+
<div class="math-block-card">
|
|
156
|
+
<div
|
|
157
|
+
bind:this={sourceEl}
|
|
158
|
+
{...leaf.surfaceProps}
|
|
159
|
+
oninput={onSourceInput}
|
|
160
|
+
class="math-block-source md-source-surface"
|
|
161
|
+
aria-label="Math source"
|
|
162
|
+
></div>
|
|
163
|
+
<!-- The toggle keeps one seat: the top-right card. Side by side that is the preview; stacked
|
|
164
|
+
and source-only it is this card. -->
|
|
165
|
+
{#if layout !== 'split'}
|
|
166
|
+
{@render layoutToggle()}
|
|
167
|
+
{/if}
|
|
168
|
+
</div>
|
|
169
|
+
{/if}
|
|
170
|
+
{#if !revealed || previewOpen}
|
|
171
|
+
<div class="math-block-card">
|
|
172
|
+
<div
|
|
173
|
+
bind:this={renderEl}
|
|
174
|
+
class="math-block-render"
|
|
175
|
+
data-mount-id={mountId}
|
|
176
|
+
role="button"
|
|
177
|
+
tabindex="-1"
|
|
178
|
+
aria-label="Math (click to edit)"
|
|
179
|
+
{...leaf.renderProps}
|
|
180
|
+
onmousedown={keepSourceFocus}
|
|
181
|
+
></div>
|
|
182
|
+
{#if revealed && layout === 'split'}
|
|
183
|
+
{@render layoutToggle()}
|
|
184
|
+
{/if}
|
|
185
|
+
</div>
|
|
186
|
+
{/if}
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
{#snippet layoutToggle()}
|
|
190
|
+
{@const next = LAYOUT_NEXT[layout]}
|
|
191
|
+
<button
|
|
192
|
+
type="button"
|
|
193
|
+
class="math-preview-toggle"
|
|
194
|
+
aria-label={LAYOUT_TITLE[next]}
|
|
195
|
+
title={LAYOUT_TITLE[next]}
|
|
196
|
+
onmousedown={(e) => e.preventDefault()}
|
|
197
|
+
onclick={() => (layout = next)}
|
|
198
|
+
>
|
|
199
|
+
<svg
|
|
200
|
+
viewBox="0 0 24 24"
|
|
201
|
+
width="13"
|
|
202
|
+
height="13"
|
|
203
|
+
fill="none"
|
|
204
|
+
stroke="currentColor"
|
|
205
|
+
stroke-width="1.75"
|
|
206
|
+
stroke-linecap="round"
|
|
207
|
+
stroke-linejoin="round"
|
|
208
|
+
aria-hidden="true"
|
|
209
|
+
>
|
|
210
|
+
{#if layout === 'split'}
|
|
211
|
+
<rect width="18" height="18" x="3" y="3" rx="2" /><path d="M12 3v18" />
|
|
212
|
+
{:else if layout === 'stacked'}
|
|
213
|
+
<rect width="18" height="18" x="3" y="3" rx="2" /><path d="M3 12h18" />
|
|
214
|
+
{:else}
|
|
215
|
+
<path
|
|
216
|
+
d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"
|
|
217
|
+
/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242" /><path
|
|
218
|
+
d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"
|
|
219
|
+
/><path d="m2 2 20 20" />
|
|
220
|
+
{/if}
|
|
221
|
+
</svg>
|
|
222
|
+
</button>
|
|
223
|
+
{/snippet}
|
|
95
224
|
|
|
96
225
|
<style>
|
|
97
|
-
/*
|
|
226
|
+
/* At rest the render alone, laid out as a plain block so it centres exactly as it did
|
|
227
|
+
before any of this existed. Editing turns the block into two equal cards. */
|
|
228
|
+
.math-block-split {
|
|
229
|
+
display: grid;
|
|
230
|
+
grid-template-columns: 1fr 1fr;
|
|
231
|
+
align-items: stretch;
|
|
232
|
+
gap: 6px;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* Stacked: the source over its preview, each the block's full width — for the equation too
|
|
236
|
+
long to read at half width. Not the default: it grows the block and reshuffles the page. */
|
|
237
|
+
.math-block-stacked {
|
|
238
|
+
display: grid;
|
|
239
|
+
grid-template-columns: 1fr;
|
|
240
|
+
gap: 6px;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* The equation's cards are boxes like a code block's, and take the same stand-off from their
|
|
244
|
+
neighbours (editor.css, fencedCode). Padding, not margin: the height model measures the
|
|
245
|
+
host's box. */
|
|
246
|
+
:global(.block-host[data-block-kind='mathBlock']) {
|
|
247
|
+
padding-block: 6px;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* Each half is its own card, and the containing block for its eye. */
|
|
251
|
+
.math-block-card {
|
|
252
|
+
position: relative;
|
|
253
|
+
min-width: 0;
|
|
254
|
+
border-radius: 8px;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* Keyed on EDITING, not on the split: folding the preview away leaves one card, and a card
|
|
258
|
+
that lost its fill the moment it stood alone would read as having left edit mode. */
|
|
259
|
+
.math-block-editing .math-block-card {
|
|
260
|
+
display: flex;
|
|
261
|
+
align-items: center;
|
|
262
|
+
background: var(--color-bg-secondary, rgba(128, 128, 128, 0.12));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* Deltas over the shared .md-source-surface (editor.css) — including its accent border,
|
|
266
|
+
which announced a state the card's own fill already carries. */
|
|
98
267
|
.math-block-source {
|
|
99
268
|
outline: none;
|
|
100
|
-
padding: 12px;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
269
|
+
padding: 10px 12px;
|
|
270
|
+
/* WRAP, rather than scroll sideways. The card is half the block's width, so any real
|
|
271
|
+
formula overflows it, and a horizontal scrollbar hides the very text being edited.
|
|
272
|
+
`pre-wrap` keeps the author's own line breaks and wraps only what is too long;
|
|
273
|
+
LaTeX carries no indentation structure for wrapping to destroy. */
|
|
274
|
+
white-space: pre-wrap;
|
|
275
|
+
overflow-wrap: anywhere;
|
|
276
|
+
/* LEFT, like every other source surface. Centring gave each line a different starting
|
|
277
|
+
x, which is exactly what makes multi-line LaTeX unreadable; the RENDER is the half
|
|
278
|
+
that is genuinely centred, and the pairing is what Overleaf and friends do. */
|
|
279
|
+
text-align: left;
|
|
280
|
+
background: transparent;
|
|
281
|
+
border-color: transparent;
|
|
282
|
+
border-radius: 8px;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/* The card stretches the source so its box fills the column; without it a short formula's
|
|
286
|
+
surface shrink-wraps and the caret only lands where the text is. */
|
|
287
|
+
.math-block-editing .math-block-card > .math-block-source {
|
|
288
|
+
flex: 1;
|
|
289
|
+
min-width: 0;
|
|
104
290
|
}
|
|
105
291
|
|
|
106
292
|
.math-block-render {
|
|
107
293
|
display: block;
|
|
108
|
-
|
|
294
|
+
width: 100%;
|
|
295
|
+
padding: 10px 12px;
|
|
109
296
|
text-align: center;
|
|
110
297
|
cursor: text;
|
|
111
298
|
border: 1px solid transparent;
|
|
112
|
-
border-radius:
|
|
299
|
+
border-radius: 8px;
|
|
113
300
|
overflow-x: auto;
|
|
114
301
|
}
|
|
115
302
|
|
|
116
|
-
|
|
303
|
+
/* Folded and empty: the fill stays, so there is a box to see and click into. */
|
|
304
|
+
.math-block:not(.math-block-editing) .math-block-render[data-empty] {
|
|
117
305
|
background: var(--color-bg-secondary, rgba(128, 128, 128, 0.12));
|
|
306
|
+
/* The editing card's height exactly — one source line at its size, plus its padding and
|
|
307
|
+
hairline — so folding and unfolding an empty equation moves nothing. */
|
|
308
|
+
min-height: calc(0.9em * 1.5 + 22px);
|
|
309
|
+
box-sizing: border-box;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/* At rest the render is the whole block and a hover tint is its only affordance; inside a
|
|
313
|
+
card the fill is already there, so the tint would double it. */
|
|
314
|
+
.math-block:not(.math-block-editing) .math-block-render:hover {
|
|
315
|
+
background: var(--color-bg-secondary, rgba(128, 128, 128, 0.12));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.math-preview-toggle {
|
|
319
|
+
position: absolute;
|
|
320
|
+
top: 4px;
|
|
321
|
+
right: 4px;
|
|
322
|
+
display: flex;
|
|
323
|
+
align-items: center;
|
|
324
|
+
justify-content: center;
|
|
325
|
+
width: 22px;
|
|
326
|
+
height: 22px;
|
|
327
|
+
padding: 0;
|
|
328
|
+
border: none;
|
|
329
|
+
border-radius: 5px;
|
|
330
|
+
background: transparent;
|
|
331
|
+
color: var(--color-ui-muted, #8f8f89);
|
|
332
|
+
cursor: pointer;
|
|
333
|
+
opacity: 0;
|
|
334
|
+
transition: opacity 120ms ease-out;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* Transient like every other affordance here: the pointer over the block, or the eye itself
|
|
338
|
+
holding focus. */
|
|
339
|
+
.math-block:hover .math-preview-toggle,
|
|
340
|
+
.math-preview-toggle:focus-visible {
|
|
341
|
+
opacity: 1;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.math-preview-toggle:hover {
|
|
345
|
+
background: var(--color-ui-faint, rgba(255, 255, 255, 0.07));
|
|
346
|
+
color: var(--color-text-secondary, #cfcfca);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
@media (prefers-reduced-motion: reduce) {
|
|
350
|
+
.math-preview-toggle {
|
|
351
|
+
transition: none;
|
|
352
|
+
}
|
|
118
353
|
}
|
|
119
354
|
</style>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { type NodeView } from '../../plugin';
|
|
2
|
+
import { type MathBlockLayout } from './math-layout';
|
|
2
3
|
type $$ComponentProps = {
|
|
3
4
|
node: NodeView;
|
|
4
5
|
index: number;
|
|
5
6
|
myPath?: number[];
|
|
7
|
+
blockLayout?: MathBlockLayout;
|
|
6
8
|
};
|
|
7
9
|
declare const BlockMath: import("svelte").Component<$$ComponentProps, {
|
|
8
10
|
editable: true;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { latexPlugin } from './register';
|
|
1
|
+
export { latexPlugin, type LatexPluginOptions } from './register';
|
|
2
2
|
export { MATH_INLINE, MATH_BLOCK, MATH_FENCE } from './latex-kind';
|
|
3
3
|
export type { MathRenderer } from './math-renderer';
|
|
4
|
+
export type { MathBlockLayout } from './math-layout';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* never imported here. Recognition is gated on registration, so with no extension
|
|
5
5
|
* loaded parsing stays byte-identical to bare GFM.
|
|
6
6
|
*/
|
|
7
|
-
import { createScanIndex, declarePluginInlineKind, declarePluginKind, registerInlineSyntax, registerInlineWidgetKind, registerBlockKind, registerBlockOpener, isInlineKindDeclared, simpleLeafClosure, matchFenceOpen, matchFenceClose, OPENER_PRIORITIES } from '../../plugin';
|
|
7
|
+
import { caretOffsetAtPoint, createScanIndex, declarePluginInlineKind, declarePluginKind, registerInlineSyntax, registerInlineWidgetKind, registerBlockKind, registerBlockOpener, isInlineKindDeclared, simpleLeafClosure, matchFenceOpen, matchFenceClose, OPENER_PRIORITIES } from '../../plugin';
|
|
8
8
|
import MathInline from './MathInline.svelte';
|
|
9
9
|
import { registerMathBlockCompleter } from './math-completion';
|
|
10
10
|
export const MATH_INLINE = 'math';
|
|
@@ -13,33 +13,44 @@ export const MATH_FENCE = 'mathFence';
|
|
|
13
13
|
// ── Recognition ──────────────────────────────────────────────────────────────
|
|
14
14
|
const isWhitespace = (ch) => /\s/.test(ch);
|
|
15
15
|
const isDigit = (ch) => ch >= '0' && ch <= '9';
|
|
16
|
-
function
|
|
16
|
+
function indexDollars(raw) {
|
|
17
17
|
const positions = [];
|
|
18
18
|
for (let i = 1; i < raw.length; i++) {
|
|
19
|
-
if (raw[i] === '$'
|
|
19
|
+
if (raw[i] === '$')
|
|
20
20
|
positions.push(i);
|
|
21
21
|
}
|
|
22
22
|
return Int32Array.from(positions);
|
|
23
23
|
}
|
|
24
24
|
// Indexed once per block, not searched per consultation: a paragraph of shell prose
|
|
25
25
|
// (`$HOME $PATH $USER …`) would otherwise cost a full block scan at every `$`.
|
|
26
|
-
const
|
|
26
|
+
const nextDollarFrom = createScanIndex(indexDollars);
|
|
27
|
+
/** Money, written the way prose writes it: a whole number between the delimiters. */
|
|
28
|
+
const isPriceSpan = (body) => /^\d[\d.,]*$/.test(body);
|
|
27
29
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
+
* Pandoc's rule, with one divergence of ours. An attempt ENDS at the next `$`, whichever it is,
|
|
31
|
+
* and a bad closer leaves the opener literal; a closer needs a non-space before it and no digit
|
|
32
|
+
* after it. Ours: a span that is only a number is a price (`$5$`), so a typed price stays prose.
|
|
30
33
|
*/
|
|
31
34
|
function recognizeMath(raw, pos, end, kind) {
|
|
32
35
|
const afterOpen = pos + 1;
|
|
33
36
|
if (afterOpen >= end)
|
|
34
37
|
return null;
|
|
35
38
|
const opener = raw[afterOpen];
|
|
36
|
-
if (isWhitespace(opener)
|
|
39
|
+
if (isWhitespace(opener))
|
|
40
|
+
return null;
|
|
41
|
+
// `$$` is the display fence, or the empty pair a keystroke just closed: never an inline
|
|
42
|
+
// opener, or the attempt would end on its own twin.
|
|
43
|
+
if (opener === '$')
|
|
37
44
|
return null;
|
|
38
45
|
// The index spans the whole block, so `end` decides the claim: a closer past the
|
|
39
46
|
// scan range leaves the `$` literal.
|
|
40
|
-
const close =
|
|
47
|
+
const close = nextDollarFrom(raw, pos + 2);
|
|
41
48
|
if (close === -1 || close >= end)
|
|
42
49
|
return null;
|
|
50
|
+
if (isWhitespace(raw[close - 1]) || isDigit(raw[close + 1] ?? ''))
|
|
51
|
+
return null;
|
|
52
|
+
if (isPriceSpan(raw.slice(afterOpen, close)))
|
|
53
|
+
return null;
|
|
43
54
|
return { kind, start: pos, end: close + 1 };
|
|
44
55
|
}
|
|
45
56
|
// ── Registration ─────────────────────────────────────────────────────────────
|
|
@@ -49,13 +60,50 @@ export function registerMathInline() {
|
|
|
49
60
|
if (isInlineKindDeclared(MATH_INLINE))
|
|
50
61
|
return;
|
|
51
62
|
const kind = declarePluginInlineKind(MATH_INLINE);
|
|
52
|
-
registerInlineSyntax('$', (raw, pos, end) => recognizeMath(raw, pos, end, kind)
|
|
63
|
+
registerInlineSyntax('$', (raw, pos, end) => recognizeMath(raw, pos, end, kind), {
|
|
64
|
+
autoPair: true
|
|
65
|
+
});
|
|
53
66
|
registerInlineWidgetKind(kind, {
|
|
54
67
|
isWidget: () => true,
|
|
55
68
|
component: MathInline,
|
|
56
|
-
editing: {
|
|
69
|
+
editing: {
|
|
70
|
+
revealSource: true,
|
|
71
|
+
revealContentSpan: mathContentSpan,
|
|
72
|
+
revealOffsetAtPoint: mathInlineOffsetAtPoint
|
|
73
|
+
}
|
|
57
74
|
});
|
|
58
75
|
}
|
|
76
|
+
// ── Caret from a press ───────────────────────────────────────────────────────
|
|
77
|
+
// KaTeX paints glyphs, not source bytes, so both forms read a press the same way: how far along
|
|
78
|
+
// the painted run it fell, walked proportionally into the span the run renders.
|
|
79
|
+
/** `$…$`: one delimiter each side, so an edit stays inside the formula. */
|
|
80
|
+
const mathContentSpan = (source) => source.length >= 2 ? { start: 1, end: source.length - 1 } : null;
|
|
81
|
+
function glyphOffsetInSpan(rendered, span, clientX, clientY) {
|
|
82
|
+
// `.katex-html` is the painted half: its MathML twin is clipped to a pixel, and the pair
|
|
83
|
+
// measured together answers for a point no reader aimed at.
|
|
84
|
+
const glyphs = rendered.querySelector('.katex-html');
|
|
85
|
+
const along = glyphs ? caretOffsetAtPoint(glyphs, clientX, clientY) : null;
|
|
86
|
+
const total = glyphs?.textContent?.length ?? 0;
|
|
87
|
+
if (along === null || total === 0)
|
|
88
|
+
return null;
|
|
89
|
+
return span.start + Math.round((along / total) * (span.end - span.start));
|
|
90
|
+
}
|
|
91
|
+
function mathInlineOffsetAtPoint(widgetEl, source, clientX, clientY) {
|
|
92
|
+
const span = mathContentSpan(source);
|
|
93
|
+
return span ? glyphOffsetInSpan(widgetEl, span, clientX, clientY) : null;
|
|
94
|
+
}
|
|
95
|
+
/** Where a press on the folded equation puts the caret; the fence lines carry no glyph of
|
|
96
|
+
* their own, so a point the glyphs cannot answer for keeps the body's end. */
|
|
97
|
+
function mathCaretAtPoint(blockEl, clientX, clientY) {
|
|
98
|
+
const render = blockEl.querySelector('.math-block-render');
|
|
99
|
+
if (!render)
|
|
100
|
+
return null;
|
|
101
|
+
const start = Number(render.dataset.bodyStart);
|
|
102
|
+
const end = Number(render.dataset.bodyEnd);
|
|
103
|
+
if (!Number.isFinite(start) || !Number.isFinite(end))
|
|
104
|
+
return null;
|
|
105
|
+
return { path: [], offset: glyphOffsetInSpan(render, { start, end }, clientX, clientY) ?? end };
|
|
106
|
+
}
|
|
59
107
|
// ── Rendered display source ────────────────────────────────────────────────────
|
|
60
108
|
/**
|
|
61
109
|
* Shared by the render component so `mathBlock` and `mathFence` display identically.
|
|
@@ -101,6 +149,7 @@ export function registerMathBlock() {
|
|
|
101
149
|
// edge can grow a sibling.
|
|
102
150
|
gapEdges: 'both',
|
|
103
151
|
conformanceFixture: '$$\nx^2\n$$\n',
|
|
152
|
+
caretTargetAtPoint: mathCaretAtPoint,
|
|
104
153
|
closure: simpleLeafClosure({
|
|
105
154
|
focus: {
|
|
106
155
|
mode: 'implemented',
|
|
@@ -170,6 +219,7 @@ export function registerMathFence() {
|
|
|
170
219
|
editable: true,
|
|
171
220
|
supportsInline: false,
|
|
172
221
|
gapEdges: 'both',
|
|
222
|
+
caretTargetAtPoint: mathCaretAtPoint,
|
|
173
223
|
conformanceFixture: '```math\nx^2\n```\n',
|
|
174
224
|
closure: simpleLeafClosure({
|
|
175
225
|
focus: {
|
|
@@ -15,5 +15,8 @@ export function tryCompleteMathBlock(line) {
|
|
|
15
15
|
export function registerMathBlockCompleter(kind) {
|
|
16
16
|
if (isBlockCompleterRegistered(kind))
|
|
17
17
|
return;
|
|
18
|
-
|
|
18
|
+
// On type as well as at Enter: a lone `$$` can only be the pair's opener, so the block forms
|
|
19
|
+
// as the second `$` lands — the way a typed ` ``` ` is a fence at once — with the caret on
|
|
20
|
+
// the body line. The one-line `$$x$$` form is still reachable by typing into that body.
|
|
21
|
+
registerBlockCompleter(kind, { tryComplete: tryCompleteMathBlock, onType: true });
|
|
19
22
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a `$$` block lays out its source and preview while it is being edited. `split` (side by
|
|
3
|
+
* side) is the default because it moves the page least; `stacked` suits long equations; `source`
|
|
4
|
+
* shows no preview. A host picks the default through `latexPlugin({ blockLayout })`, and the
|
|
5
|
+
* block's own toggle still cycles from there.
|
|
6
|
+
*/
|
|
7
|
+
export type MathBlockLayout = 'split' | 'stacked' | 'source';
|
|
8
|
+
export declare const MATH_BLOCK_LAYOUTS: readonly MathBlockLayout[];
|
|
9
|
+
export declare function isMathBlockLayout(value: unknown): value is MathBlockLayout;
|
|
10
|
+
/**
|
|
11
|
+
* The starting layout: this editor's per-instance plugin options first (`{ plugin, options:
|
|
12
|
+
* { blockLayout } }`), then the factory's own default (`latexPlugin({ blockLayout })`), then
|
|
13
|
+
* `split`. An unknown value falls through rather than throwing.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveDefaultLayout(options: unknown, fallback?: MathBlockLayout): MathBlockLayout;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const MATH_BLOCK_LAYOUTS = ['split', 'stacked', 'source'];
|
|
2
|
+
export function isMathBlockLayout(value) {
|
|
3
|
+
return MATH_BLOCK_LAYOUTS.includes(value);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* The starting layout: this editor's per-instance plugin options first (`{ plugin, options:
|
|
7
|
+
* { blockLayout } }`), then the factory's own default (`latexPlugin({ blockLayout })`), then
|
|
8
|
+
* `split`. An unknown value falls through rather than throwing.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveDefaultLayout(options, fallback = 'split') {
|
|
11
|
+
const declared = options?.blockLayout;
|
|
12
|
+
return isMathBlockLayout(declared) ? declared : fallback;
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the body sits inside the block's source. The fence lines carry no glyph of their own, so
|
|
3
|
+
* a point measured against the rendered equation names a place in this span and nowhere else.
|
|
4
|
+
*/
|
|
5
|
+
export declare function mathBodySpan(text: string): {
|
|
6
|
+
start: number;
|
|
7
|
+
end: number;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* A block with no body LINE — `$$$$`, `$$\n$$`, a ```math straight over its closer — has nowhere
|
|
11
|
+
* for a caret to sit once the fence lines hide, and Backspace has no byte it could mean. The
|
|
12
|
+
* completion every such block takes: opener, one empty body line, closer, caret on that line,
|
|
13
|
+
* rebuilt from its OWN delimiters. A block that has a body line, blank or not, is left alone.
|
|
14
|
+
*/
|
|
15
|
+
export declare function completeBareMathSource(text: string): {
|
|
16
|
+
text: string;
|
|
17
|
+
caret: number;
|
|
18
|
+
} | null;
|
|
19
|
+
export declare function renderMathSource(text: string): DocumentFragment;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The block form's source as DOM, shaped like a code block's: each fence line in a
|
|
3
|
+
* `.md-fence-line` wrapper the marker-hiding modes collapse, the body as LaTeX highlight
|
|
4
|
+
* tokens. Text-preserving by construction — opener + body + closer is the input — so the
|
|
5
|
+
* offset walk and G1.28 hold. Anything not shaped like a fence paints as plain tokens.
|
|
6
|
+
*/
|
|
7
|
+
import { highlightCode, matchFenceClose, matchFenceOpen } from '../../plugin';
|
|
8
|
+
const FENCE = '$$';
|
|
9
|
+
const unsliced = (text) => ({ opener: '', body: text, closer: '' });
|
|
10
|
+
/** Both block forms are an opener line, a body and a closer line; only the closer test differs,
|
|
11
|
+
* since GitHub's ```math carries its info string on the opener and closes on a bare fence. */
|
|
12
|
+
function sliceMathSource(text) {
|
|
13
|
+
const firstNewline = text.indexOf('\n');
|
|
14
|
+
if (text.startsWith(FENCE)) {
|
|
15
|
+
// One-line form: `$$x^2$$`.
|
|
16
|
+
if (firstNewline === -1) {
|
|
17
|
+
return text.length >= 4 && text.endsWith(FENCE)
|
|
18
|
+
? { opener: FENCE, body: text.slice(2, -2), closer: FENCE }
|
|
19
|
+
: unsliced(text);
|
|
20
|
+
}
|
|
21
|
+
if (text.slice(0, firstNewline) !== FENCE)
|
|
22
|
+
return unsliced(text);
|
|
23
|
+
return sliceFenceLines(text, firstNewline, (line) => line === FENCE);
|
|
24
|
+
}
|
|
25
|
+
if (firstNewline === -1)
|
|
26
|
+
return unsliced(text);
|
|
27
|
+
// CommonMark's fence rules stay the editor's: a closer repeats its opener's own marker, at
|
|
28
|
+
// that length or longer.
|
|
29
|
+
const fence = matchFenceOpen(text.slice(0, firstNewline));
|
|
30
|
+
if (!fence)
|
|
31
|
+
return unsliced(text);
|
|
32
|
+
return sliceFenceLines(text, firstNewline, (line) => matchFenceClose(line, fence.marker, fence.length));
|
|
33
|
+
}
|
|
34
|
+
function sliceFenceLines(text, firstNewline, isCloser) {
|
|
35
|
+
const opener = text.slice(0, firstNewline + 1);
|
|
36
|
+
const rest = text.slice(opener.length);
|
|
37
|
+
const lastNewline = rest.lastIndexOf('\n');
|
|
38
|
+
const lastLine = lastNewline === -1 ? rest : rest.slice(lastNewline + 1);
|
|
39
|
+
if (!isCloser(lastLine))
|
|
40
|
+
return { opener, body: rest, closer: '' };
|
|
41
|
+
if (lastNewline === -1)
|
|
42
|
+
return { opener, body: '', closer: lastLine };
|
|
43
|
+
// The newline before the closer belongs to the closer's line (the code block's rule), so
|
|
44
|
+
// collapsing that line leaves no blank line at the box's edge — unless the body is blank,
|
|
45
|
+
// where that newline IS the one line the caret can sit on.
|
|
46
|
+
const bodyWithEnding = rest.slice(0, lastNewline + 1);
|
|
47
|
+
if (!/\S/.test(bodyWithEnding))
|
|
48
|
+
return { opener, body: bodyWithEnding, closer: lastLine };
|
|
49
|
+
return { opener, body: rest.slice(0, lastNewline), closer: rest.slice(lastNewline) };
|
|
50
|
+
}
|
|
51
|
+
function fenceLine(text) {
|
|
52
|
+
const line = document.createElement('span');
|
|
53
|
+
line.className = 'md-fence-line';
|
|
54
|
+
const marker = document.createElement('span');
|
|
55
|
+
marker.className = 'md-marker md-fence';
|
|
56
|
+
marker.textContent = text.replace(/\n$/, '').replace(/^\n/, '');
|
|
57
|
+
if (text.startsWith('\n'))
|
|
58
|
+
line.appendChild(document.createTextNode('\n'));
|
|
59
|
+
line.appendChild(marker);
|
|
60
|
+
if (text.endsWith('\n'))
|
|
61
|
+
line.appendChild(document.createTextNode('\n'));
|
|
62
|
+
return line;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Where the body sits inside the block's source. The fence lines carry no glyph of their own, so
|
|
66
|
+
* a point measured against the rendered equation names a place in this span and nowhere else.
|
|
67
|
+
*/
|
|
68
|
+
export function mathBodySpan(text) {
|
|
69
|
+
const { opener, body } = sliceMathSource(text);
|
|
70
|
+
return { start: opener.length, end: opener.length + body.length };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A block with no body LINE — `$$$$`, `$$\n$$`, a ```math straight over its closer — has nowhere
|
|
74
|
+
* for a caret to sit once the fence lines hide, and Backspace has no byte it could mean. The
|
|
75
|
+
* completion every such block takes: opener, one empty body line, closer, caret on that line,
|
|
76
|
+
* rebuilt from its OWN delimiters. A block that has a body line, blank or not, is left alone.
|
|
77
|
+
*/
|
|
78
|
+
export function completeBareMathSource(text) {
|
|
79
|
+
const { opener, body, closer } = sliceMathSource(text);
|
|
80
|
+
if (!opener || !closer)
|
|
81
|
+
return null;
|
|
82
|
+
if (body.includes('\n') || body.trim() !== '')
|
|
83
|
+
return null;
|
|
84
|
+
const openerLine = opener.replace(/\n$/, '');
|
|
85
|
+
return { text: `${openerLine}\n\n${closer}`, caret: openerLine.length + 1 };
|
|
86
|
+
}
|
|
87
|
+
export function renderMathSource(text) {
|
|
88
|
+
const { opener, body, closer } = sliceMathSource(text);
|
|
89
|
+
const frag = document.createDocumentFragment();
|
|
90
|
+
if (opener)
|
|
91
|
+
frag.appendChild(fenceLine(opener));
|
|
92
|
+
frag.appendChild(highlightCode(body, 'latex'));
|
|
93
|
+
// An empty last body line has only the hidden closer after it; the anchor gives the caret
|
|
94
|
+
// that line (see `code-renderer.ts`).
|
|
95
|
+
if (closer.startsWith('\n') && body.endsWith('\n')) {
|
|
96
|
+
const anchor = document.createElement('br');
|
|
97
|
+
anchor.dataset.caretAnchor = 'closer';
|
|
98
|
+
frag.appendChild(anchor);
|
|
99
|
+
}
|
|
100
|
+
if (closer)
|
|
101
|
+
frag.appendChild(fenceLine(closer));
|
|
102
|
+
return frag;
|
|
103
|
+
}
|