@voithos-labs/aragonite 0.10.2 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -21
- package/THIRD-PARTY-NOTICES.md +25 -0
- package/dist/a11y-strings.d.ts +18 -0
- package/dist/a11y-strings.js +18 -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 +35 -24
- package/dist/components/BlockHost.svelte +16 -9
- package/dist/components/Editor.svelte +381 -198
- package/dist/components/Editor.svelte.d.ts +1 -1
- package/dist/components/SelectionOverlay.svelte +17 -3
- 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 +686 -0
- package/dist/components/blocks/code/CodeBlockRail.svelte.d.ts +26 -0
- package/dist/components/blocks/code/code-bootstrap.js +4 -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 +11 -0
- 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 +243 -29
- package/dist/components/blocks/editable-surface.d.ts +9 -0
- package/dist/components/blocks/editable-surface.js +22 -3
- package/dist/components/blocks/list/ListItemBlock.svelte +3 -3
- 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/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 +56 -2
- package/dist/components/blocks/text/live-selection-edit.js +27 -0
- 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 +144 -22
- package/dist/components/drag-handle.d.ts +35 -0
- package/dist/components/drag-handle.js +126 -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 +10 -6
- package/dist/components/editor-root-listeners.js +23 -22
- 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 +59 -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/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 +11 -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-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/overlay-remeasure.js +8 -0
- package/dist/cursor/reveal-source.js +7 -2
- package/dist/cursor/widget-offset.d.ts +6 -0
- package/dist/cursor/widget-offset.js +61 -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/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/inline-range-commit.js +1 -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 +21 -12
- package/dist/index.d.ts +1 -1
- package/dist/plugin.d.ts +6 -0
- package/dist/plugin.js +11 -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 +36 -3
- 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 +97 -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/reactivity/list-windowing.svelte.d.ts +8 -8
- package/dist/reactivity/list-windowing.svelte.js +49 -24
- package/dist/schema/block-completions.d.ts +8 -0
- package/dist/schema/block-completions.js +11 -0
- 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 +25 -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 +1 -1
- package/dist/selection/cross-block/ops.js +1 -1
- package/dist/selection/cross-block/paste.js +32 -30
- package/dist/selection/cross-block/type-replace.d.ts +3 -2
- package/dist/selection/cross-block/type-replace.js +62 -13
- package/dist/selection/dead-space-caret.d.ts +10 -0
- package/dist/selection/dead-space-caret.js +47 -1
- package/dist/selection/double-click-trim.d.ts +17 -0
- package/dist/selection/double-click-trim.js +57 -0
- package/dist/selection/drag-pointer.d.ts +7 -2
- package/dist/selection/drag-pointer.js +61 -2
- package/dist/selection/gap-caret.js +1 -1
- package/dist/selection/keyboard-extend.js +1 -1
- package/dist/selection/path-lookup.js +1 -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.js +30 -2
- 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 +57 -28
- package/dist/styles/editor.css +217 -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.js +1 -1
- 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 +126 -96
- package/docs/guide/plugin-api.md +31 -3
- package/docs/guide/plugin-guide.md +26 -3
- package/package.json +4 -2
- 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
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { EVERY_KIND, registerBlockContextActions } from '../../schema/context-actions';
|
|
2
|
+
import { applyPasteTransforms } from '../../tree-operations/paste/paste-transforms';
|
|
3
|
+
const KIND_NAMES = {
|
|
4
|
+
paragraph: 'paragraph',
|
|
5
|
+
heading: 'heading',
|
|
6
|
+
setextHeading: 'heading',
|
|
7
|
+
fencedCode: 'code block',
|
|
8
|
+
indentedCode: 'code block',
|
|
9
|
+
mathBlock: 'math block',
|
|
10
|
+
mathFence: 'math block',
|
|
11
|
+
table: 'table',
|
|
12
|
+
blockquote: 'quote',
|
|
13
|
+
list: 'list',
|
|
14
|
+
thematicBreak: 'divider',
|
|
15
|
+
html: 'HTML block',
|
|
16
|
+
linkReferenceDefinition: 'link definition'
|
|
17
|
+
};
|
|
18
|
+
const IMAGE_ONLY = /^\s*!\[[^\]]*\]\([^)]*\)\s*$/;
|
|
19
|
+
const PROSE_KINDS = new Set(['paragraph', 'heading', 'setextHeading']);
|
|
20
|
+
/** The noun the menu uses for a block: a paragraph holding nothing but an image is "image". */
|
|
21
|
+
export function blockNoun(node) {
|
|
22
|
+
if (node.kind === 'paragraph' && IMAGE_ONLY.test(node.raw))
|
|
23
|
+
return 'image';
|
|
24
|
+
return KIND_NAMES[node.kind] ?? 'block';
|
|
25
|
+
}
|
|
26
|
+
/** Prose is the page's background: it takes no block menu of its own. An image alone in a
|
|
27
|
+
* paragraph is a block, not prose. */
|
|
28
|
+
export function isProseBackground(node) {
|
|
29
|
+
return PROSE_KINDS.has(node.kind) && blockNoun(node) !== 'image';
|
|
30
|
+
}
|
|
31
|
+
let registered = false;
|
|
32
|
+
/** Test-only. */
|
|
33
|
+
export function __resetDefaultContextActionsForTests() {
|
|
34
|
+
registered = false;
|
|
35
|
+
}
|
|
36
|
+
export function registerDefaultContextActions() {
|
|
37
|
+
if (registered)
|
|
38
|
+
return;
|
|
39
|
+
registered = true;
|
|
40
|
+
registerBlockContextActions(EVERY_KIND, (node) => {
|
|
41
|
+
const noun = blockNoun(node);
|
|
42
|
+
return [
|
|
43
|
+
{
|
|
44
|
+
id: 'block.copy',
|
|
45
|
+
label: `Copy ${noun}`,
|
|
46
|
+
icon: 'copy',
|
|
47
|
+
run: () => navigator.clipboard.writeText(node.raw)
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'block.replace',
|
|
51
|
+
label: 'Replace with clipboard',
|
|
52
|
+
icon: 'clipboard',
|
|
53
|
+
run: async (ctx) => {
|
|
54
|
+
let text;
|
|
55
|
+
try {
|
|
56
|
+
text = await navigator.clipboard.readText();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (text.trim() === '')
|
|
62
|
+
return;
|
|
63
|
+
// The paste transforms first, as any paste gets, so a plugin's rewrite applies.
|
|
64
|
+
const md = applyPasteTransforms(text);
|
|
65
|
+
await ctx.replaceRaw(md.endsWith('\n') ? md : md + '\n');
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'block.remove',
|
|
70
|
+
label: `Remove ${noun}`,
|
|
71
|
+
icon: 'trash',
|
|
72
|
+
run: (ctx) => ctx.deleteBlock()
|
|
73
|
+
}
|
|
74
|
+
];
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Svelte attachment for a menu flyout: measures the surface once it is in the DOM and moves it
|
|
3
|
+
* to stay inside the viewport (`flyoutPlacement`). The element's own CSS hangs it off the row's
|
|
4
|
+
* right edge; this writes the correction inline, so the rule lives in one place for every menu.
|
|
5
|
+
*/
|
|
6
|
+
export declare function keepFlyoutOnScreen(node: HTMLElement): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Svelte attachment for a menu flyout: measures the surface once it is in the DOM and moves it
|
|
3
|
+
* to stay inside the viewport (`flyoutPlacement`). The element's own CSS hangs it off the row's
|
|
4
|
+
* right edge; this writes the correction inline, so the rule lives in one place for every menu.
|
|
5
|
+
*/
|
|
6
|
+
import { flyoutPlacement } from '../blocks/table/table-menu-model';
|
|
7
|
+
const GAP = 4;
|
|
8
|
+
export function keepFlyoutOnScreen(node) {
|
|
9
|
+
const parentMenu = node.parentElement?.closest('.md-menu');
|
|
10
|
+
if (!parentMenu)
|
|
11
|
+
return;
|
|
12
|
+
const rect = node.getBoundingClientRect();
|
|
13
|
+
const { dy, flip } = flyoutPlacement(rect, parentMenu.getBoundingClientRect(), {
|
|
14
|
+
width: window.innerWidth,
|
|
15
|
+
height: window.innerHeight
|
|
16
|
+
});
|
|
17
|
+
if (dy !== 0)
|
|
18
|
+
node.style.top = `${node.offsetTop + dy}px`;
|
|
19
|
+
if (flip) {
|
|
20
|
+
node.style.left = 'auto';
|
|
21
|
+
node.style.right = '100%';
|
|
22
|
+
node.style.marginLeft = '0';
|
|
23
|
+
node.style.marginRight = `${GAP}px`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Toggle an inline format inside a prose block. Over a SELECTION the direction is parse coverage,
|
|
3
3
|
* not edge adjacency: a covered range unapplies (aligned strip, else split, over every covering run
|
|
4
|
-
* in turn), overlapping or abutting runs apply over their union, a bare range wraps
|
|
5
|
-
* PAINTS delimiters the strip and the wrap write
|
|
6
|
-
* verifies. At a COLLAPSED CARET: unwrap the
|
|
7
|
-
* (live-mode.md § 4.3). Every write clamps to the
|
|
4
|
+
* in turn), overlapping or abutting runs apply over their union, a bare range wraps the core its
|
|
5
|
+
* boundary whitespace leaves. Where the mode PAINTS delimiters the strip and the wrap write
|
|
6
|
+
* unverified; every other candidate declines unless it verifies. At a COLLAPSED CARET: unwrap the
|
|
7
|
+
* span, else drop the empty pair, else insert one (live-mode.md § 4.3). Every write clamps to the
|
|
8
|
+
* CONTENT range: a marker in `# ` changes the kind.
|
|
8
9
|
*/
|
|
9
10
|
import { type PresentationMode } from '../../presentation-mode';
|
|
10
11
|
import { type InlineMarkKind } from '../../schema/inline-construct-policy';
|
|
@@ -48,3 +49,10 @@ export declare function inlineFormatsCovering(edit: InlineFormatEdit, candidates
|
|
|
48
49
|
* a toolbar's buttons arrive as N separate calls, and only the caller knows they are one repaint.
|
|
49
50
|
* One slot, since the previous state is dead the moment a keystroke or the caret moves. */
|
|
50
51
|
export declare function createInlineFormatActiveMemo(): (edit: InlineFormatEdit, format: InlineMarkKind) => boolean;
|
|
52
|
+
/** A range minus its boundary whitespace, null once nothing is left. One home for the trim, so the
|
|
53
|
+
* wrap here and the cross-block decomposition that trims before it ever asks
|
|
54
|
+
* (`selection/cross-block/format-range.ts`) cannot disagree about where a run may close. */
|
|
55
|
+
export declare function withoutBoundaryWhitespace(display: string, from: number, to: number): {
|
|
56
|
+
start: number;
|
|
57
|
+
end: number;
|
|
58
|
+
} | null;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Toggle an inline format inside a prose block. Over a SELECTION the direction is parse coverage,
|
|
3
3
|
* not edge adjacency: a covered range unapplies (aligned strip, else split, over every covering run
|
|
4
|
-
* in turn), overlapping or abutting runs apply over their union, a bare range wraps
|
|
5
|
-
* PAINTS delimiters the strip and the wrap write
|
|
6
|
-
* verifies. At a COLLAPSED CARET: unwrap the
|
|
7
|
-
* (live-mode.md § 4.3). Every write clamps to the
|
|
4
|
+
* in turn), overlapping or abutting runs apply over their union, a bare range wraps the core its
|
|
5
|
+
* boundary whitespace leaves. Where the mode PAINTS delimiters the strip and the wrap write
|
|
6
|
+
* unverified; every other candidate declines unless it verifies. At a COLLAPSED CARET: unwrap the
|
|
7
|
+
* span, else drop the empty pair, else insert one (live-mode.md § 4.3). Every write clamps to the
|
|
8
|
+
* CONTENT range: a marker in `# ` changes the kind.
|
|
8
9
|
*/
|
|
9
10
|
import { recordFormatCoverageRead } from '../../perf/instruments';
|
|
10
11
|
import { paintsFocusedMarkers } from '../../presentation-mode';
|
|
@@ -29,29 +30,31 @@ export function toggleInlineFormat(edit, format, mode) {
|
|
|
29
30
|
// Painted delimiters are the mode's own answer, so those modes write their candidate unverified.
|
|
30
31
|
// The preview rungs paint: this seam only writes into the block those rungs reveal.
|
|
31
32
|
const paints = paintsFocusedMarkers(mode ?? 'source');
|
|
32
|
-
const covering =
|
|
33
|
+
const { from, to, covering } = coveredReading(display, inlines, start, end, format);
|
|
33
34
|
// A strip sheds ONE run, so where a second of the same kind covers the selection too, the answer
|
|
34
35
|
// is the covering arm's split: stripping there leaves the press's own read still active.
|
|
35
|
-
const sole = covering.length > 1 ? null : soleStripCandidate(display, inlines,
|
|
36
|
+
const sole = covering.length > 1 ? null : soleStripCandidate(display, inlines, from, to, format);
|
|
36
37
|
if (sole)
|
|
37
38
|
return paints || preservesScreen(sole, edit, screenOf(display, content)) ? sole : null;
|
|
38
39
|
// The flank strip rewrites bytes OUTSIDE the selection, and byte equality can mistake a nested
|
|
39
40
|
// run's delimiters for the enclosing one's, so its candidate verifies like the split's.
|
|
40
|
-
const enclosing = enclosingSpanOf(inlines,
|
|
41
|
-
if (enclosing && flanksAreItsMarkers(display,
|
|
42
|
-
const flank = firstFlipVerified([flankStrip(display,
|
|
41
|
+
const enclosing = enclosingSpanOf(inlines, from, to, format);
|
|
42
|
+
if (enclosing && flanksAreItsMarkers(display, from, to, enclosing)) {
|
|
43
|
+
const flank = firstFlipVerified([flankStrip(display, from, to, enclosing)], edit, format, 'unapply');
|
|
43
44
|
if (flank)
|
|
44
45
|
return flank;
|
|
45
46
|
}
|
|
46
47
|
if (covering.length > 0)
|
|
47
|
-
return firstFlipVerified(covering.flatMap((span) => splitCandidates(display, inlines, span,
|
|
48
|
+
return firstFlipVerified(covering.flatMap((span) => splitCandidates(display, inlines, span, from, to, format)), edit, format, 'unapply');
|
|
48
49
|
const union = formatUnionOf(inlines, start, end, format);
|
|
49
50
|
if (union)
|
|
50
|
-
return firstFlipVerified(absorbCandidates(display, inlines, union, format, mark), edit, format, 'apply');
|
|
51
|
+
return firstFlipVerified(absorbCandidates(display, inlines, union, { start, end }, format, mark), edit, format, 'apply');
|
|
51
52
|
// A wrap whose markers do not paint owes split and absorb's coverage check: bytes that re-pair
|
|
52
53
|
// against a neighbouring run leave the range unformatted with nothing on screen to say so.
|
|
53
|
-
const
|
|
54
|
-
|
|
54
|
+
const wrap = wrapCandidate(display, inlines, start, end, mark);
|
|
55
|
+
if (!wrap)
|
|
56
|
+
return null;
|
|
57
|
+
return paints ? wrap : firstFlipVerified([wrap], edit, format, 'apply');
|
|
55
58
|
}
|
|
56
59
|
/** Whether a toggle right now would UNAPPLY — the pressed-state a toolbar paints. The same arms
|
|
57
60
|
* the toggle routes by, asked without emitting: a caret inside the construct, a selection
|
|
@@ -137,7 +140,24 @@ function coverageCarries({ display, start, end, inlines, sliceNodes }, format) {
|
|
|
137
140
|
const enclosing = enclosingSpanOf(inlines, start, end, format);
|
|
138
141
|
if (enclosing && flanksAreItsMarkers(display, start, end, enclosing))
|
|
139
142
|
return true;
|
|
140
|
-
return
|
|
143
|
+
return coveredReading(display, inlines, start, end, format).covering.length > 0;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The range an unapply reads, with the runs covering it. A run closes against a word and never
|
|
147
|
+
* whitespace, so the wrap leaves a boundary space OUTSIDE the delimiters: a selection reaching
|
|
148
|
+
* past a run by whitespace alone is still that run's own, or it could not take its own mark off.
|
|
149
|
+
*/
|
|
150
|
+
function coveredReading(display, inlines, start, end, format) {
|
|
151
|
+
const covering = coveringSpansOf(inlines, start, end, format);
|
|
152
|
+
if (covering.length > 0)
|
|
153
|
+
return { from: start, to: end, covering };
|
|
154
|
+
const trimmed = withoutBoundaryWhitespace(display, start, end);
|
|
155
|
+
if (!trimmed || (trimmed.start === start && trimmed.end === end))
|
|
156
|
+
return { from: start, to: end, covering };
|
|
157
|
+
const inner = coveringSpansOf(inlines, trimmed.start, trimmed.end, format);
|
|
158
|
+
return inner.length > 0
|
|
159
|
+
? { from: trimmed.start, to: trimmed.end, covering: inner }
|
|
160
|
+
: { from: start, to: end, covering };
|
|
141
161
|
}
|
|
142
162
|
// ── Aligned unapply ──────────────────────────────────────────────────────────
|
|
143
163
|
/** The selection carries its own flanking markers (the user selected `**word**`): exactly one
|
|
@@ -247,18 +267,37 @@ function formatUnionOf(inlines, start, end, format) {
|
|
|
247
267
|
}
|
|
248
268
|
return touched ? { start: from, end: to } : null;
|
|
249
269
|
}
|
|
250
|
-
|
|
270
|
+
/** The run grows to the union, the SELECTION does not: each endpoint is carried through the
|
|
271
|
+
* strip and re-wrap, so a press over half a run leaves that half selected. An endpoint on the
|
|
272
|
+
* union's edge keeps the new marker inside the range, as a bare wrap's selection does. */
|
|
273
|
+
function absorbCandidates(display, inlines, union, selected, format, mark) {
|
|
251
274
|
if (!cutsLandCleanly(inlines, union, union))
|
|
252
275
|
return [];
|
|
253
|
-
const
|
|
276
|
+
const cuts = kindMarkerCuts(inlines, format, union.start, union.end);
|
|
277
|
+
const stripped = spliceOutCuts(display, cuts, union.start, union.end);
|
|
254
278
|
const prefix = display.slice(0, union.start);
|
|
255
279
|
const suffix = display.slice(union.end);
|
|
256
280
|
const wrapAt = (lead, core, trail) => {
|
|
257
281
|
const wrapped = wrapSlice(core, mark);
|
|
282
|
+
// The kind may pad its fence, so the marker widths are read off the wrap, not the row.
|
|
283
|
+
const found = core ? wrapped.indexOf(core) : -1;
|
|
284
|
+
const open = found >= 0 ? found : (wrapped.length - core.length) / 2;
|
|
285
|
+
const close = wrapped.length - core.length - open;
|
|
286
|
+
const coreStart = lead.length;
|
|
287
|
+
const coreEnd = lead.length + core.length;
|
|
288
|
+
const startAt = strippedOffset(cuts, union.start, selected.start);
|
|
289
|
+
const endAt = strippedOffset(cuts, union.start, selected.end);
|
|
290
|
+
const shift = (at, edgeInclusive) => {
|
|
291
|
+
if (edgeInclusive === 'start' ? at <= coreStart : at < coreStart)
|
|
292
|
+
return at;
|
|
293
|
+
if (edgeInclusive === 'start' ? at <= coreEnd : at < coreEnd)
|
|
294
|
+
return at + open;
|
|
295
|
+
return at + open + close;
|
|
296
|
+
};
|
|
258
297
|
return {
|
|
259
298
|
newDisplay: prefix + lead + wrapped + trail + suffix,
|
|
260
|
-
newSelStart: prefix.length +
|
|
261
|
-
newSelEnd: prefix.length +
|
|
299
|
+
newSelStart: prefix.length + shift(startAt, 'start'),
|
|
300
|
+
newSelEnd: prefix.length + shift(endAt, 'end')
|
|
262
301
|
};
|
|
263
302
|
};
|
|
264
303
|
const out = [wrapAt('', stripped, '')];
|
|
@@ -288,6 +327,10 @@ function cutsLandCleanly(inlines, cuts, within) {
|
|
|
288
327
|
/** The bytes of `[from, to)` with the delimiter runs of every same-format construct lying wholly
|
|
289
328
|
* inside removed; one straddling the range keeps its markers, for the verifier to judge. */
|
|
290
329
|
function stripKindMarkers(display, inlines, format, from, to) {
|
|
330
|
+
return spliceOutCuts(display, kindMarkerCuts(inlines, format, from, to), from, to);
|
|
331
|
+
}
|
|
332
|
+
/** The marker byte ranges of every `format` run lying wholly inside [from, to), in order. */
|
|
333
|
+
function kindMarkerCuts(inlines, format, from, to) {
|
|
291
334
|
const cuts = [];
|
|
292
335
|
for (const node of inlineDescendants(inlines)) {
|
|
293
336
|
if (node.kind !== format)
|
|
@@ -297,7 +340,9 @@ function stripKindMarkers(display, inlines, format, from, to) {
|
|
|
297
340
|
continue;
|
|
298
341
|
cuts.push([span.start, span.contentStart], [span.contentEnd, span.end]);
|
|
299
342
|
}
|
|
300
|
-
cuts.sort((a, b) => a[0] - b[0]);
|
|
343
|
+
return cuts.sort((a, b) => a[0] - b[0]);
|
|
344
|
+
}
|
|
345
|
+
function spliceOutCuts(display, cuts, from, to) {
|
|
301
346
|
let out = '';
|
|
302
347
|
let at = from;
|
|
303
348
|
for (const [cutFrom, cutTo] of cuts) {
|
|
@@ -306,30 +351,27 @@ function stripKindMarkers(display, inlines, format, from, to) {
|
|
|
306
351
|
}
|
|
307
352
|
return out + display.slice(at, to);
|
|
308
353
|
}
|
|
354
|
+
/** Where a display offset lands in the stripped slice: the cut bytes before it fall away, and an
|
|
355
|
+
* offset inside a marker rides to that marker's start. */
|
|
356
|
+
function strippedOffset(cuts, from, offset) {
|
|
357
|
+
let removed = 0;
|
|
358
|
+
for (const [cutFrom, cutTo] of cuts)
|
|
359
|
+
if (cutFrom < offset)
|
|
360
|
+
removed += Math.min(cutTo, offset) - cutFrom;
|
|
361
|
+
return Math.max(0, offset - from - removed);
|
|
362
|
+
}
|
|
309
363
|
// ── Wrap ─────────────────────────────────────────────────────────────────────
|
|
310
364
|
/**
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
365
|
+
* The bare wrap, over the selection's trimmed core: markdown opens and closes a run against a word,
|
|
366
|
+
* never whitespace, so a boundary space goes to the plain text beside the run, as
|
|
367
|
+
* `live-split-rebalance` reads the same problem. Null where nothing survives the trim, or where an
|
|
368
|
+
* endpoint is no legal cut: markers spliced into a construct's bytes re-pair.
|
|
315
369
|
*/
|
|
316
|
-
function
|
|
317
|
-
const core =
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
.map((range) => wrapRange(display, range.start, range.end, mark));
|
|
322
|
-
}
|
|
323
|
-
/** The selection minus its boundary whitespace, or null when there is none to trim or nothing
|
|
324
|
-
* left once it goes. */
|
|
325
|
-
function trimmedRange(display, start, end) {
|
|
326
|
-
let from = start;
|
|
327
|
-
let to = end;
|
|
328
|
-
while (from < to && /\s/.test(display[from]))
|
|
329
|
-
from++;
|
|
330
|
-
while (to > from && /\s/.test(display[to - 1]))
|
|
331
|
-
to--;
|
|
332
|
-
return (from === start && to === end) || from === to ? null : { start: from, end: to };
|
|
370
|
+
function wrapCandidate(display, inlines, start, end, mark) {
|
|
371
|
+
const core = withoutBoundaryWhitespace(display, start, end);
|
|
372
|
+
if (!core || !cutsLandCleanly(inlines, core, core))
|
|
373
|
+
return null;
|
|
374
|
+
return wrapRange(display, core.start, core.end, mark);
|
|
333
375
|
}
|
|
334
376
|
function wrapRange(display, start, end, mark) {
|
|
335
377
|
const wrapped = wrapSlice(display.slice(start, end), mark);
|
|
@@ -527,6 +569,18 @@ function leadingWs(text) {
|
|
|
527
569
|
function trailingWs(text) {
|
|
528
570
|
return /\s*$/.exec(text)[0];
|
|
529
571
|
}
|
|
572
|
+
/** A range minus its boundary whitespace, null once nothing is left. One home for the trim, so the
|
|
573
|
+
* wrap here and the cross-block decomposition that trims before it ever asks
|
|
574
|
+
* (`selection/cross-block/format-range.ts`) cannot disagree about where a run may close. */
|
|
575
|
+
export function withoutBoundaryWhitespace(display, from, to) {
|
|
576
|
+
let start = from;
|
|
577
|
+
let end = to;
|
|
578
|
+
while (start < end && /\s/.test(display[start]))
|
|
579
|
+
start++;
|
|
580
|
+
while (end > start && /\s/.test(display[end - 1]))
|
|
581
|
+
end--;
|
|
582
|
+
return start === end ? null : { start, end };
|
|
583
|
+
}
|
|
530
584
|
// ── Content clamp ────────────────────────────────────────────────────────────
|
|
531
585
|
function clampToContent(offset, content) {
|
|
532
586
|
return Math.min(Math.max(offset, content.start), content.end);
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import type { ImageCrop } from '../nodes';
|
|
2
|
+
export declare const MAX_CROP_ZOOM = 4;
|
|
1
3
|
export interface ParsedImageAlt {
|
|
2
4
|
displayAlt: string;
|
|
3
5
|
width: number | undefined;
|
|
4
6
|
height: number | undefined;
|
|
7
|
+
crop: ImageCrop | undefined;
|
|
5
8
|
}
|
|
6
9
|
export declare function parseImageDimensions(alt: string): ParsedImageAlt;
|
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
// `|N` / `|NxM` is an Obsidian extension, not part of GFM.
|
|
1
|
+
// `|N` / `|NxM` is an Obsidian extension, not part of GFM. The `@X,Y[,Z]` tail on the `NxM`
|
|
2
|
+
// form is this editor's own: the pan and zoom of the image inside that frame.
|
|
2
3
|
const MAX_DIMENSION = 10000;
|
|
3
|
-
|
|
4
|
-
//
|
|
5
|
-
|
|
4
|
+
export const MAX_CROP_ZOOM = 4;
|
|
5
|
+
// The longest decodable suffix, `|10000x10000@100,100,4.00`. Bounding the pipe search to that
|
|
6
|
+
// tail keeps nested-label floods linear.
|
|
7
|
+
const MAX_SUFFIX_SEARCH = 26;
|
|
8
|
+
const NO_HINT = { width: undefined, height: undefined, crop: undefined };
|
|
6
9
|
export function parseImageDimensions(alt) {
|
|
7
10
|
const lastPipe = boundedLastPipe(alt);
|
|
8
11
|
const dims = lastPipe === -1 ? null : parseDimensionSuffix(alt.slice(lastPipe + 1));
|
|
9
12
|
if (dims === null)
|
|
10
|
-
return { displayAlt: alt,
|
|
11
|
-
return { displayAlt: alt.slice(0, lastPipe),
|
|
13
|
+
return { displayAlt: alt, ...NO_HINT };
|
|
14
|
+
return { displayAlt: alt.slice(0, lastPipe), ...dims };
|
|
12
15
|
}
|
|
13
16
|
function boundedLastPipe(alt) {
|
|
14
17
|
const floor = alt.length > MAX_SUFFIX_SEARCH ? alt.length - MAX_SUFFIX_SEARCH : 0;
|
|
@@ -24,16 +27,49 @@ function parseDimensionSuffix(s) {
|
|
|
24
27
|
const w = parseStrictInt(s);
|
|
25
28
|
if (w === null)
|
|
26
29
|
return null;
|
|
27
|
-
return { width: w, height: undefined };
|
|
30
|
+
return { width: w, height: undefined, crop: undefined };
|
|
28
31
|
}
|
|
32
|
+
const atIdx = s.indexOf('@');
|
|
33
|
+
const heightEnd = atIdx === -1 ? s.length : atIdx;
|
|
29
34
|
const w = parseStrictInt(s.slice(0, xIdx));
|
|
30
|
-
const h = parseStrictInt(s.slice(xIdx + 1));
|
|
35
|
+
const h = parseStrictInt(s.slice(xIdx + 1, heightEnd));
|
|
31
36
|
if (w === null || h === null)
|
|
32
37
|
return null;
|
|
33
|
-
|
|
38
|
+
if (atIdx === -1)
|
|
39
|
+
return { width: w, height: h, crop: undefined };
|
|
40
|
+
const crop = parseCropTail(s.slice(atIdx + 1));
|
|
41
|
+
// A malformed tail is not a hint at all: the whole suffix stays alt text.
|
|
42
|
+
if (crop === null)
|
|
43
|
+
return null;
|
|
44
|
+
return { width: w, height: h, crop };
|
|
45
|
+
}
|
|
46
|
+
/** `X,Y` or `X,Y,Z`: whole percents 0–100 and a zoom of 1–4 with up to two decimals. */
|
|
47
|
+
function parseCropTail(s) {
|
|
48
|
+
const parts = s.split(',');
|
|
49
|
+
if (parts.length < 2 || parts.length > 3)
|
|
50
|
+
return null;
|
|
51
|
+
const x = parsePercent(parts[0]);
|
|
52
|
+
const y = parsePercent(parts[1]);
|
|
53
|
+
if (x === null || y === null)
|
|
54
|
+
return null;
|
|
55
|
+
if (parts.length === 2)
|
|
56
|
+
return { x, y, z: 1 };
|
|
57
|
+
if (!/^[1-4](?:\.\d{1,2})?$/.test(parts[2]))
|
|
58
|
+
return null;
|
|
59
|
+
const z = Number(parts[2]);
|
|
60
|
+
if (z > MAX_CROP_ZOOM)
|
|
61
|
+
return null;
|
|
62
|
+
return { x, y, z };
|
|
63
|
+
}
|
|
64
|
+
function parsePercent(s) {
|
|
65
|
+
if (!/^\d{1,3}$/.test(s))
|
|
66
|
+
return null;
|
|
67
|
+
const n = Number(s);
|
|
68
|
+
return n > 100 ? null : n;
|
|
34
69
|
}
|
|
70
|
+
/** No leading zeros: a padded run is alt text, not a hint. */
|
|
35
71
|
function parseStrictInt(s) {
|
|
36
|
-
if (
|
|
72
|
+
if (!/^[1-9]\d*$/.test(s))
|
|
37
73
|
return null;
|
|
38
74
|
const n = Number(s);
|
|
39
75
|
if (n <= 0 || n > MAX_DIMENSION)
|
|
@@ -67,6 +67,17 @@ export declare const ON_EDGE_POLICIES: readonly ["select", "step-over"];
|
|
|
67
67
|
*/
|
|
68
68
|
export interface InlineWidgetEditingPolicy {
|
|
69
69
|
revealSource?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Where this kind's editable CONTENT sits inside its source span, as offsets relative to
|
|
72
|
+
* that span — `$x$` answers `{ start: 1, end: 2 }`. A click that reveals the source seats
|
|
73
|
+
* the caret at `end`, so typing continues the construct instead of escaping past its
|
|
74
|
+
* closing delimiter. Only the kind knows its own delimiters; absent, the caret keeps the
|
|
75
|
+
* leading edge.
|
|
76
|
+
*/
|
|
77
|
+
revealContentSpan?: (source: string) => {
|
|
78
|
+
start: number;
|
|
79
|
+
end: number;
|
|
80
|
+
} | null;
|
|
70
81
|
deleteGranularity?: (typeof DELETE_GRANULARITIES)[number];
|
|
71
82
|
onEdge?: (typeof ON_EDGE_POLICIES)[number];
|
|
72
83
|
onSelectedKey?: (e: KeyboardEvent, ctx: InlineWidgetEditingContext) => boolean;
|
|
@@ -112,6 +112,7 @@ function buildImage(raw, opener, labelEnd, target, children) {
|
|
|
112
112
|
...(target.title !== undefined ? { title: target.title } : {}),
|
|
113
113
|
...(dims.width !== undefined ? { width: dims.width } : {}),
|
|
114
114
|
...(dims.height !== undefined ? { height: dims.height } : {}),
|
|
115
|
+
...(dims.crop !== undefined ? { crop: dims.crop } : {}),
|
|
115
116
|
...(target.label !== undefined ? { label: target.label } : {})
|
|
116
117
|
};
|
|
117
118
|
}
|
|
@@ -38,6 +38,12 @@ export interface InlineSyntaxOptions {
|
|
|
38
38
|
* commit's equality guard. See the plugin guide's inline section.
|
|
39
39
|
*/
|
|
40
40
|
rewriteImage?: ImageSyntaxRewriter;
|
|
41
|
+
/**
|
|
42
|
+
* The trigger is a symmetric one-byte delimiter (`$…$`) a keystroke should close at once:
|
|
43
|
+
* typing it lands its twin after the caret, so the new opener pairs with that twin rather
|
|
44
|
+
* than with a later formula's delimiter (see `delimiter-autopair.ts`). Bare triggers only.
|
|
45
|
+
*/
|
|
46
|
+
autoPair?: boolean;
|
|
41
47
|
}
|
|
42
48
|
export interface InlineRung extends InlineSyntaxClaim {
|
|
43
49
|
recognizer: InlineSyntaxRecognizer;
|
|
@@ -57,6 +63,8 @@ export declare function hasInlineSyntax(): boolean;
|
|
|
57
63
|
/** Read once per scan, so an empty set leaves `needsScan` at its pre-plugin cost. */
|
|
58
64
|
export declare function hasScanProbeRungs(): boolean;
|
|
59
65
|
export declare function isScanProbeTrigger(char: string): boolean;
|
|
66
|
+
/** Whether a plugin asked for `char` to close itself as it is typed. */
|
|
67
|
+
export declare function isAutoPairTrigger(char: string): boolean;
|
|
60
68
|
/** False costs the scan loop nothing. */
|
|
61
69
|
export declare function hasPrefixRungs(): boolean;
|
|
62
70
|
export declare function __resetInlineSyntaxForTests(): void;
|
|
@@ -36,6 +36,9 @@ const NO_RUNGS = [];
|
|
|
36
36
|
// (`default` arm) so each dispatch path reads one map and its empty gate is one `size` check.
|
|
37
37
|
const reservedRegistry = new Map();
|
|
38
38
|
const unreservedRegistry = new Map();
|
|
39
|
+
// Triggers whose keystroke closes itself (`autoPair`); the built-in backtick is not registered
|
|
40
|
+
// here, the typing seam knows it on its own.
|
|
41
|
+
const autoPairTriggers = new Set();
|
|
39
42
|
// Triggers the fast bail (`needsScan`, scan/index.ts) must visit while a rung lives on them.
|
|
40
43
|
// Maintained at registration, so a rung on a trigger `SPECIAL_CHARS` already visits costs nothing.
|
|
41
44
|
const scanProbeTriggers = new Set();
|
|
@@ -44,7 +47,7 @@ export function registerInlineSyntax(trigger, recognizer, options) {
|
|
|
44
47
|
if (trigger.length !== 1) {
|
|
45
48
|
throw new Error('registerInlineSyntax: trigger must be a single character');
|
|
46
49
|
}
|
|
47
|
-
const { prefix, priority = INLINE_PRIORITIES.plugin, rewriteImage } = options ?? {};
|
|
50
|
+
const { prefix, priority = INLINE_PRIORITIES.plugin, rewriteImage, autoPair } = options ?? {};
|
|
48
51
|
if (prefix !== undefined && (prefix.length < 2 || !prefix.startsWith(trigger))) {
|
|
49
52
|
throw new Error(`registerInlineSyntax: prefix ${JSON.stringify(prefix)} must begin with the trigger ` +
|
|
50
53
|
`${JSON.stringify(trigger)} and be at least two characters`);
|
|
@@ -69,6 +72,10 @@ export function registerInlineSyntax(trigger, recognizer, options) {
|
|
|
69
72
|
`the built-in scanner; got ${priority}`);
|
|
70
73
|
}
|
|
71
74
|
}
|
|
75
|
+
if (autoPair && prefix !== undefined) {
|
|
76
|
+
throw new Error(`registerInlineSyntax: autoPair pairs the bare trigger ${JSON.stringify(trigger)} with ` +
|
|
77
|
+
`itself, so it cannot be combined with a prefix`);
|
|
78
|
+
}
|
|
72
79
|
const effectivePrefix = prefix ?? trigger;
|
|
73
80
|
const registry = reserved ? reservedRegistry : unreservedRegistry;
|
|
74
81
|
const existing = registry.get(trigger);
|
|
@@ -84,6 +91,8 @@ export function registerInlineSyntax(trigger, recognizer, options) {
|
|
|
84
91
|
// or the recognizer is the silent no-op this seam refuses to accept.
|
|
85
92
|
if (!reserved || SCAN_PROBED_RESERVED.has(trigger))
|
|
86
93
|
scanProbeTriggers.add(trigger);
|
|
94
|
+
if (autoPair)
|
|
95
|
+
autoPairTriggers.add(trigger);
|
|
87
96
|
}, `registerInlineSyntax: ${JSON.stringify(trigger)} already registered at prefix ` +
|
|
88
97
|
`${JSON.stringify(effectivePrefix)}, priority ${priority}`);
|
|
89
98
|
}
|
|
@@ -139,6 +148,10 @@ export function hasScanProbeRungs() {
|
|
|
139
148
|
export function isScanProbeTrigger(char) {
|
|
140
149
|
return scanProbeTriggers.has(char);
|
|
141
150
|
}
|
|
151
|
+
/** Whether a plugin asked for `char` to close itself as it is typed. */
|
|
152
|
+
export function isAutoPairTrigger(char) {
|
|
153
|
+
return autoPairTriggers.has(char);
|
|
154
|
+
}
|
|
142
155
|
/** False costs the scan loop nothing. */
|
|
143
156
|
export function hasPrefixRungs() {
|
|
144
157
|
return reservedRegistry.size > 0;
|
|
@@ -147,4 +160,5 @@ export function __resetInlineSyntaxForTests() {
|
|
|
147
160
|
reservedRegistry.clear();
|
|
148
161
|
unreservedRegistry.clear();
|
|
149
162
|
scanProbeTriggers.clear();
|
|
163
|
+
autoPairTriggers.clear();
|
|
150
164
|
}
|
|
@@ -25,6 +25,12 @@ export interface RenderInlineOptions {
|
|
|
25
25
|
* as `buildImageWidget`; absent or null falls the widget back to its raw source.
|
|
26
26
|
*/
|
|
27
27
|
buildPortalWidget?: (node: InlineNode, raw: string) => HTMLElement | null;
|
|
28
|
+
/**
|
|
29
|
+
* Paint a lone backslash ending the display as the hard break it is about to become: the
|
|
30
|
+
* byte as a (hidden) marker plus two `br` anchors so the caret has a second line to sit on.
|
|
31
|
+
* Only for a mode that hides markers; the DOM stays byte-identical elsewhere.
|
|
32
|
+
*/
|
|
33
|
+
pendingBreakSeat?: boolean;
|
|
28
34
|
/**
|
|
29
35
|
* Stamp marker spans with the construct's raw range, so preview-inline's reveal trigger can
|
|
30
36
|
* address them. Attributes only, leaving textContent and the offset walk untouched. Off by
|
|
@@ -288,8 +288,40 @@ export function renderInlineNodes(nodes, raw, opts = {}) {
|
|
|
288
288
|
if (child !== null)
|
|
289
289
|
stack.push(child);
|
|
290
290
|
}
|
|
291
|
+
if (opts.pendingBreakSeat)
|
|
292
|
+
paintPendingBreak(nodes, raw, root.content);
|
|
291
293
|
return root.content;
|
|
292
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* `insertHardBreak` at the end of a block writes `\` whose line ending IS the block's trailing
|
|
297
|
+
* one, so until the next key supplies the following line CommonMark (and the scanner) read the
|
|
298
|
+
* byte as literal text. Left as text it shows as a stray backslash and the caret stays on the
|
|
299
|
+
* first line; painted like this the user sees the new line they asked for. A `br` adds no
|
|
300
|
+
* textContent, so G1.28 holds; the walk seats offset `length` after the first anchor.
|
|
301
|
+
*/
|
|
302
|
+
function paintPendingBreak(nodes, raw, frag) {
|
|
303
|
+
const last = nodes[nodes.length - 1];
|
|
304
|
+
if (!last || last.kind !== 'text' || raw[last.end - 1] !== '\\')
|
|
305
|
+
return;
|
|
306
|
+
const rest = raw.slice(last.end);
|
|
307
|
+
if (rest !== '' && rest !== '\n' && rest !== '\r\n')
|
|
308
|
+
return;
|
|
309
|
+
const tail = frag.lastChild;
|
|
310
|
+
if (!tail || tail.nodeType !== Node.TEXT_NODE || !tail.textContent?.endsWith('\\'))
|
|
311
|
+
return;
|
|
312
|
+
if (tail.textContent.length === 1)
|
|
313
|
+
tail.remove();
|
|
314
|
+
else
|
|
315
|
+
tail.textContent = tail.textContent.slice(0, -1);
|
|
316
|
+
const marker = markerSpan('\\');
|
|
317
|
+
marker.classList.add('md-break-pending');
|
|
318
|
+
frag.appendChild(marker);
|
|
319
|
+
for (let i = 0; i < 2; i++) {
|
|
320
|
+
const anchor = document.createElement('br');
|
|
321
|
+
anchor.dataset.caretAnchor = 'break';
|
|
322
|
+
frag.appendChild(anchor);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
293
325
|
/**
|
|
294
326
|
* The leaf containing `offset`, preferring the right node at a boundary; `offset === end` only
|
|
295
327
|
* matches the last node. Model-layer, touching no DOM: the DOM counterpart is
|
package/dist/core/nodes.d.ts
CHANGED
|
@@ -259,6 +259,8 @@ export interface InlineNode {
|
|
|
259
259
|
decoded?: string;
|
|
260
260
|
width?: number;
|
|
261
261
|
height?: number;
|
|
262
|
+
/** Pan/zoom inside the `|WxH` frame, read from a `@X,Y[,Z]` tail on the hint. */
|
|
263
|
+
crop?: ImageCrop;
|
|
262
264
|
/** Discriminator for `unresolvedReference` nodes: which form they would have been. */
|
|
263
265
|
refKind?: 'link' | 'image';
|
|
264
266
|
/**
|
|
@@ -271,12 +273,24 @@ export interface InlineNode {
|
|
|
271
273
|
* Optional keys are omitted rather than set to `undefined`, so a serializer can tell "no title"
|
|
272
274
|
* from "empty title" and reproduce a node that never carried one.
|
|
273
275
|
*/
|
|
276
|
+
/**
|
|
277
|
+
* How an image sits inside its `|WxH` frame: the image point (`x`%, `y`%) pinned to the same
|
|
278
|
+
* point of the frame, at `z` times the smallest scale that fills it. `{50, 50, 1}` is a plain
|
|
279
|
+
* centred cover fit; absent, the frame stretches the image the way `<img width height>` does.
|
|
280
|
+
*/
|
|
281
|
+
export interface ImageCrop {
|
|
282
|
+
x: number;
|
|
283
|
+
y: number;
|
|
284
|
+
z: number;
|
|
285
|
+
}
|
|
274
286
|
export interface ImageFields {
|
|
275
287
|
alt: string;
|
|
276
288
|
url: string;
|
|
277
289
|
title?: string;
|
|
278
290
|
width?: number;
|
|
279
291
|
height?: number;
|
|
292
|
+
/** Needs both dimensions: a crop is a window into a fixed frame. */
|
|
293
|
+
crop?: ImageCrop;
|
|
280
294
|
/**
|
|
281
295
|
* Reference-style images only (`![alt][label]`). When set, the serializer emits the reference
|
|
282
296
|
* form and writes no url/title: those live in the LRD, and inlining them would orphan it.
|
|
@@ -39,7 +39,8 @@ export function classifyArrivalKey(key, metaKey = false) {
|
|
|
39
39
|
if (metaKey && (key === 'ArrowLeft' || key === 'ArrowRight'))
|
|
40
40
|
return 'outside';
|
|
41
41
|
// A step stops on the side of the run it approached from, so one press never changes which
|
|
42
|
-
// construct the caret is in
|
|
42
|
+
// construct the caret is in (live-mode.md § 4.2); leaving a construct is a typed closer's job
|
|
43
|
+
// (delimiter-autopair.ts) or a toggle's, not the arrow's.
|
|
43
44
|
if (key === 'ArrowRight' || key === 'ArrowDown' || key === 'PageDown')
|
|
44
45
|
return 'near';
|
|
45
46
|
if (key === 'ArrowLeft' || key === 'ArrowUp' || key === 'PageUp')
|