@voithos-labs/aragonite 0.10.3 → 0.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/a11y-strings.d.ts +1 -0
- package/dist/a11y-strings.js +1 -0
- package/dist/components/BlockDragHandle.svelte +2 -0
- package/dist/components/BlockHost.svelte +2 -2
- package/dist/components/Editor.svelte +56 -3
- package/dist/components/SelectionOverlay.svelte +17 -23
- package/dist/components/SelectionOverlay.svelte.d.ts +1 -1
- package/dist/components/blocks/code/CodeBlockRail.svelte +42 -24
- package/dist/components/blocks/code/code-bootstrap.js +12 -0
- package/dist/components/blocks/code/code-languages.d.ts +5 -5
- package/dist/components/blocks/code/code-languages.js +20 -12
- package/dist/components/blocks/editable-leaf.d.ts +3 -3
- package/dist/components/blocks/editable-leaf.js +6 -2
- package/dist/components/blocks/list/ListBlock.svelte +1 -0
- package/dist/components/blocks/list/ListItemBlock.svelte +12 -3
- package/dist/components/blocks/list/ListItemBlock.svelte.d.ts +2 -0
- package/dist/components/blocks/text/click-snap-guard.d.ts +3 -0
- package/dist/components/blocks/text/click-snap-guard.js +8 -0
- package/dist/components/blocks/text/edge-policy-dispatch.js +33 -18
- package/dist/components/blocks/text/live-selection-edit.d.ts +5 -5
- package/dist/components/blocks/text/live-selection-edit.js +10 -7
- package/dist/components/blocks/text/text-clipboard.js +2 -2
- package/dist/components/blocks/text/widget-interaction.js +45 -23
- package/dist/components/drag-handle.d.ts +6 -0
- package/dist/components/drag-handle.js +8 -0
- package/dist/components/editor-root-geometry.d.ts +1 -1
- package/dist/components/editor-root-geometry.js +1 -1
- package/dist/components/editor-root-listeners.d.ts +0 -7
- package/dist/components/editor-root-listeners.js +0 -22
- package/dist/components/image/ImageResizeHandles.svelte +18 -10
- package/dist/components/menu/SelectionToolbar.svelte +385 -0
- package/dist/components/menu/SelectionToolbar.svelte.d.ts +16 -0
- package/dist/core/inline/inline-widgets.d.ts +12 -4
- package/dist/core/inline/inline-widgets.js +5 -0
- package/dist/core/inline/transparency.js +3 -3
- package/dist/cursor/height-oracle.d.ts +2 -3
- package/dist/cursor/height-oracle.js +0 -1
- package/dist/cursor/scroll-hold.d.ts +10 -0
- package/dist/cursor/scroll-hold.js +22 -0
- package/dist/cursor/scrollport.d.ts +9 -0
- package/dist/cursor/scrollport.js +30 -1
- package/dist/cursor/visual-lines.d.ts +5 -4
- package/dist/cursor/visual-lines.js +56 -11
- package/dist/cursor/widget-edge-snap.d.ts +28 -0
- package/dist/cursor/widget-edge-snap.js +38 -0
- package/dist/cursor/widget-offset.d.ts +3 -0
- package/dist/cursor/widget-offset.js +10 -0
- package/dist/decorations/reserved-attrs.js +1 -0
- package/dist/editor-actions/focus/focus-dispatch.js +7 -4
- package/dist/editor-actions/focus/focus-landing.d.ts +10 -1
- package/dist/editor-actions/focus/focus-landing.js +20 -6
- package/dist/editor-actions/plugin/container.d.ts +7 -0
- package/dist/editor-actions/plugin/container.js +3 -1
- package/dist/editor-props.d.ts +3 -0
- package/dist/plugin.d.ts +2 -1
- package/dist/plugin.js +6 -1
- package/dist/plugins/latex/latex-kind.js +48 -31
- package/dist/plugins/latex/math-source.d.ts +3 -3
- package/dist/plugins/latex/math-source.js +32 -26
- package/dist/plugins/mermaid/MermaidBlock.svelte +19 -9
- package/dist/plugins/parrot/ParrotBlock.svelte +3 -1
- package/dist/reactivity/list-windowing.svelte.js +26 -6
- package/dist/schema/commands.d.ts +9 -5
- package/dist/schema/commands.js +12 -7
- package/dist/schema/operations.d.ts +1 -1
- package/dist/schema/reserved-chords.js +12 -0
- package/dist/selection/cross-block/keydown.js +2 -28
- package/dist/selection/cross-block/paste.js +7 -13
- package/dist/selection/cross-block/type-replace.js +5 -15
- package/dist/selection/dead-space-caret.d.ts +3 -0
- package/dist/selection/dead-space-caret.js +12 -2
- package/dist/selection/drag-pointer.d.ts +19 -0
- package/dist/selection/drag-pointer.js +42 -0
- package/dist/selection/keyboard-extend.d.ts +3 -2
- package/dist/selection/keyboard-extend.js +3 -2
- package/dist/selection/multi-click.d.ts +42 -0
- package/dist/selection/multi-click.js +203 -0
- package/dist/selection/native-bridge.d.ts +3 -0
- package/dist/selection/native-bridge.js +31 -2
- package/dist/selection/path-lookup.d.ts +10 -2
- package/dist/selection/path-lookup.js +10 -3
- package/dist/selection/pointer-gesture.d.ts +9 -0
- package/dist/selection/pointer-gesture.js +11 -0
- package/dist/selection/primitives.d.ts +7 -0
- package/dist/selection/primitives.js +19 -1
- package/dist/selection/range-delete.d.ts +4 -0
- package/dist/selection/range-delete.js +1 -1
- package/dist/selection/selection-drop.d.ts +34 -0
- package/dist/selection/selection-drop.js +253 -0
- package/dist/styles/editor-theme.css +18 -3
- package/dist/styles/editor.css +64 -13
- package/dist/tree-operations/paste/replace-block-at-parent.d.ts +3 -1
- package/dist/tree-operations/paste/replace-block-at-parent.js +4 -1
- package/dist/tree-operations/paste/replacement-parse.d.ts +17 -0
- package/dist/tree-operations/paste/replacement-parse.js +22 -0
- package/docs/guide/consumer-guide.md +10 -8
- package/docs/guide/plugin-api.md +54 -38
- package/docs/guide/plugin-guide.md +31 -22
- package/package.json +7 -7
- package/dist/selection/double-click-trim.d.ts +0 -17
- package/dist/selection/double-click-trim.js +0 -57
package/dist/a11y-strings.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export declare const CODE_COPIED_LABEL = "Code copied";
|
|
|
26
26
|
export declare const CODE_MENU_LABEL = "Code block actions";
|
|
27
27
|
export declare const CODE_RAIL_LABEL = "Code block controls";
|
|
28
28
|
export declare const CODE_LANGUAGE_LIST = "Code block languages";
|
|
29
|
+
export declare const SELECTION_TOOLBAR_LABEL = "Selection formatting";
|
|
29
30
|
/** Chrome, not an announcement: the language chip's accessible name. */
|
|
30
31
|
export declare function codeLanguageLabel(language: string): string;
|
|
31
32
|
export declare const SEARCH_TOGGLE_REPLACE = "Toggle replace";
|
package/dist/a11y-strings.js
CHANGED
|
@@ -26,6 +26,7 @@ export const CODE_COPIED_LABEL = 'Code copied';
|
|
|
26
26
|
export const CODE_MENU_LABEL = 'Code block actions';
|
|
27
27
|
export const CODE_RAIL_LABEL = 'Code block controls';
|
|
28
28
|
export const CODE_LANGUAGE_LIST = 'Code block languages';
|
|
29
|
+
export const SELECTION_TOOLBAR_LABEL = 'Selection formatting';
|
|
29
30
|
/** Chrome, not an announcement: the language chip's accessible name. */
|
|
30
31
|
export function codeLanguageLabel(language) {
|
|
31
32
|
return `Code language: ${language}`;
|
|
@@ -80,8 +80,8 @@
|
|
|
80
80
|
|
|
81
81
|
let descriptor = $derived(registryView.descriptor(node.kind));
|
|
82
82
|
let isContainer = $derived(descriptor.isContainer);
|
|
83
|
-
// Who
|
|
84
|
-
// duplicated, the two drift and a container
|
|
83
|
+
// Who measures this block's rects, decided ONCE here and handed to both overlays —
|
|
84
|
+
// duplicated, the two drift and a container measures over its own children. Delegation
|
|
85
85
|
// needs children with hosts: a childless container and a grid have none.
|
|
86
86
|
let delegatesPainting = $derived(
|
|
87
87
|
isContainer && (node.children?.length ?? 0) > 0 && descriptor.containerContract !== 'grid'
|
|
@@ -38,6 +38,9 @@
|
|
|
38
38
|
import { createDeadSpaceCaret } from '../selection/dead-space-caret';
|
|
39
39
|
import { resetForPointerDown } from '../selection/cross-block/pointer';
|
|
40
40
|
import { installDragListener } from '../selection/drag-pointer';
|
|
41
|
+
import { installMultiClickSelect } from '../selection/multi-click';
|
|
42
|
+
import { claimsPointerGesture } from '../selection/pointer-gesture';
|
|
43
|
+
import { installSelectionDrop } from '../selection/selection-drop';
|
|
41
44
|
import { createContentVersion } from '../reactivity/content-version.svelte';
|
|
42
45
|
import { useContainerWindowing } from '../reactivity/use-container-windowing.svelte';
|
|
43
46
|
import { refSlotsOver, replaceRefs, revealChildOrWait } from '../reactivity/publish-ref.svelte';
|
|
@@ -112,7 +115,6 @@
|
|
|
112
115
|
installWidthWatcher
|
|
113
116
|
} from './editor-root-geometry';
|
|
114
117
|
import {
|
|
115
|
-
installDoubleClickWordSelect,
|
|
116
118
|
installEditorBlurAnnouncer,
|
|
117
119
|
installModActiveTracker,
|
|
118
120
|
installSelectionChangeBridge,
|
|
@@ -146,6 +148,7 @@
|
|
|
146
148
|
import { bothEnable, createRegistryView, type KindEnablement } from '../schema/registry-view';
|
|
147
149
|
import BlockList from './BlockList.svelte';
|
|
148
150
|
import SearchBar from './SearchBar.svelte';
|
|
151
|
+
import SelectionToolbar from './menu/SelectionToolbar.svelte';
|
|
149
152
|
import ImageOverlayHost from './image/ImageOverlayHost.svelte';
|
|
150
153
|
import LinkCardHost from './link-card/LinkCardHost.svelte';
|
|
151
154
|
import { createLinkCardState } from './link-card/link-card-state.svelte';
|
|
@@ -176,6 +179,7 @@
|
|
|
176
179
|
blockDragHandles = true,
|
|
177
180
|
searchBar = true,
|
|
178
181
|
searchBarAnchor,
|
|
182
|
+
selectionToolbar = true,
|
|
179
183
|
keybindings,
|
|
180
184
|
theme = 'dark',
|
|
181
185
|
presentationMode = 'source',
|
|
@@ -717,6 +721,11 @@
|
|
|
717
721
|
// press and release targets meet at the root); with a range just painted, it is no
|
|
718
722
|
// click to answer.
|
|
719
723
|
if (!anchor) {
|
|
724
|
+
// A multi-click places no caret: the ladder painted its range over this press.
|
|
725
|
+
if (e.detail >= 2) {
|
|
726
|
+
marginDrag = false;
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
720
729
|
const pressed = marginDrag;
|
|
721
730
|
const dragged =
|
|
722
731
|
pressed &&
|
|
@@ -761,6 +770,7 @@
|
|
|
761
770
|
'a, summary, [role="checkbox"], ' +
|
|
762
771
|
'.code-rail, .table-add-zone, .editor-tail, .md-menu, .block-drag-handle';
|
|
763
772
|
const dragStartsHere = (rootEl: HTMLElement, target: EventTarget | null): boolean => {
|
|
773
|
+
if (claimsPointerGesture(target)) return false;
|
|
764
774
|
if (deadSpaceCaret.isDeadSpaceTarget(rootEl, target)) return true;
|
|
765
775
|
if (!(target instanceof Element) || !rootEl.contains(target)) return false;
|
|
766
776
|
return target.closest(NOT_A_DRAG_START) === null;
|
|
@@ -771,6 +781,7 @@
|
|
|
771
781
|
// starts no selection of its own to fight it; `click` still fires for the caret placement.
|
|
772
782
|
let marginDrag = false;
|
|
773
783
|
let marginDown = { x: 0, y: 0 };
|
|
784
|
+
let marginSession: { dispose(): void } | null = null;
|
|
774
785
|
const startMarginDrag = (e: PointerEvent) => {
|
|
775
786
|
marginDrag = false;
|
|
776
787
|
marginDown = { x: e.clientX, y: e.clientY };
|
|
@@ -786,7 +797,7 @@
|
|
|
786
797
|
const component = getBlockComponent(anchor.path);
|
|
787
798
|
if (component?.startDragAtPoint?.(e.clientX, e.clientY, e)) return;
|
|
788
799
|
}
|
|
789
|
-
installDragListener(
|
|
800
|
+
marginSession = installDragListener(
|
|
790
801
|
{
|
|
791
802
|
editorRoot: root,
|
|
792
803
|
scrollContainer: getScrollHost() ?? root,
|
|
@@ -801,10 +812,38 @@
|
|
|
801
812
|
};
|
|
802
813
|
return removeAll(
|
|
803
814
|
onRoot(root, 'click', handleClick),
|
|
804
|
-
|
|
815
|
+
installMultiClickSelect({
|
|
816
|
+
editorRoot: root,
|
|
817
|
+
selection: selectionState,
|
|
818
|
+
getBlockElByPath,
|
|
819
|
+
getScrollContainer: () => getScrollHost() ?? root,
|
|
820
|
+
lifetimeSignal: lifetimeController.signal,
|
|
821
|
+
marginBlockAt: (target, x, y) =>
|
|
822
|
+
effectiveMode !== 'reading' && dragStartsHere(root, target)
|
|
823
|
+
? deadSpaceCaret.blockPathNearPoint(root, x, y)
|
|
824
|
+
: null
|
|
825
|
+
}),
|
|
826
|
+
installSelectionDrop({
|
|
827
|
+
editorRoot: root,
|
|
828
|
+
getDoc: () => doc,
|
|
829
|
+
controller,
|
|
830
|
+
coordinator: pasteCoordinator,
|
|
831
|
+
getPresentationMode: () => effectiveMode,
|
|
832
|
+
linkRef: linkRefView,
|
|
833
|
+
grammar: registryView.grammar,
|
|
834
|
+
activePlugins,
|
|
835
|
+
events,
|
|
836
|
+
isReadOnly: () => effectiveMode === 'reading'
|
|
837
|
+
}),
|
|
805
838
|
onRoot(root, 'pointerdown', startMarginDrag),
|
|
806
839
|
onRoot(root, 'mousedown', (e: MouseEvent) => {
|
|
807
840
|
deadSpaceCaret.notePress(root, e);
|
|
841
|
+
// The second press of a click run is the ladder's, which runs its own drag.
|
|
842
|
+
if (marginDrag && e.detail >= 2) {
|
|
843
|
+
marginSession?.dispose();
|
|
844
|
+
marginSession = null;
|
|
845
|
+
marginDrag = false;
|
|
846
|
+
}
|
|
808
847
|
if (marginDrag) e.preventDefault();
|
|
809
848
|
})
|
|
810
849
|
);
|
|
@@ -1845,6 +1884,20 @@
|
|
|
1845
1884
|
onClose={() => (blockMenu = null)}
|
|
1846
1885
|
/>
|
|
1847
1886
|
{/if}
|
|
1887
|
+
{#if selectionToolbar && effectiveMode !== 'reading'}
|
|
1888
|
+
<SelectionToolbar
|
|
1889
|
+
editor={{
|
|
1890
|
+
getSelection,
|
|
1891
|
+
getRects,
|
|
1892
|
+
getBlockKindAt,
|
|
1893
|
+
runCommand,
|
|
1894
|
+
canRunCommand,
|
|
1895
|
+
isCommandActive,
|
|
1896
|
+
getEvents
|
|
1897
|
+
}}
|
|
1898
|
+
root={editorEl}
|
|
1899
|
+
/>
|
|
1900
|
+
{/if}
|
|
1848
1901
|
<ImageOverlayHost
|
|
1849
1902
|
{widgetSelection}
|
|
1850
1903
|
{controller}
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
} from '../editor-keys';
|
|
10
10
|
import {
|
|
11
11
|
normalize,
|
|
12
|
+
blockPaintsWholeBox,
|
|
12
13
|
classifyBlockForSelection,
|
|
13
14
|
charOffsetOf,
|
|
14
15
|
cellIndexOf,
|
|
15
|
-
type
|
|
16
|
+
type EditorSelection
|
|
16
17
|
} from '../selection/primitives';
|
|
17
18
|
import { snapCrossBlockTableEndpoints } from '../selection/table-endpoint-snap';
|
|
18
|
-
import { pathsEqual } from '../selection/path-math';
|
|
19
19
|
import { wireOverlayRemeasure } from '../cursor/overlay-remeasure';
|
|
20
20
|
|
|
21
21
|
let {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
path: number[];
|
|
29
29
|
blockRef: BlockComponent | undefined;
|
|
30
30
|
blockEl: HTMLElement | null | undefined;
|
|
31
|
-
/** This container's children paint
|
|
31
|
+
/** This container's children paint the range's endpoint rects, so it measures none. */
|
|
32
32
|
delegatesPainting?: boolean;
|
|
33
33
|
/** This container measures its own rects instead of delegating. Both are decided
|
|
34
34
|
* at BlockHost, which hands the same pair to DecorationOverlay. */
|
|
@@ -42,32 +42,26 @@
|
|
|
42
42
|
const getEditorRoot = editorDoc?.editorRoot;
|
|
43
43
|
const getDoc = editorDoc?.doc;
|
|
44
44
|
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
return 'outside';
|
|
49
|
-
}
|
|
50
|
-
return classifyBlockForSelection(path, {
|
|
51
|
-
anchor: selection.anchor,
|
|
52
|
-
focus: selection.focus
|
|
53
|
-
});
|
|
45
|
+
const range = $derived.by<EditorSelection | null>(() => {
|
|
46
|
+
if (!selection?.isCustomRendered || !selection.anchor || !selection.focus) return null;
|
|
47
|
+
return { anchor: selection.anchor, focus: selection.focus };
|
|
54
48
|
});
|
|
55
49
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
});
|
|
50
|
+
const classification = $derived(range ? classifyBlockForSelection(path, range) : 'outside');
|
|
51
|
+
|
|
52
|
+
// Delegation-blind on purpose: a block the range holds whole paints one box over everything it
|
|
53
|
+
// renders, chrome included, and the seam already keeps its children from painting under it.
|
|
54
|
+
const paintsWholeBox = $derived(
|
|
55
|
+
range !== null && blockPaintsWholeBox(path, range, selection?.wholeUnitPath ?? null)
|
|
56
|
+
);
|
|
64
57
|
|
|
65
58
|
// The measuring effect and the template read this one predicate, so a rendered
|
|
66
59
|
// rect is always one the effect measured; two predicates render a stale box.
|
|
67
60
|
const paintsEndpoints = $derived(
|
|
68
|
-
|
|
69
|
-
classification === '
|
|
70
|
-
|
|
61
|
+
!delegatesPainting &&
|
|
62
|
+
(classification === 'start' ||
|
|
63
|
+
classification === 'end' ||
|
|
64
|
+
(classification === 'single-block' && containerPaintsRects))
|
|
71
65
|
);
|
|
72
66
|
|
|
73
67
|
interface LocalRect {
|
|
@@ -3,7 +3,7 @@ type $$ComponentProps = {
|
|
|
3
3
|
path: number[];
|
|
4
4
|
blockRef: BlockComponent | undefined;
|
|
5
5
|
blockEl: HTMLElement | null | undefined;
|
|
6
|
-
/** This container's children paint
|
|
6
|
+
/** This container's children paint the range's endpoint rects, so it measures none. */
|
|
7
7
|
delegatesPainting?: boolean;
|
|
8
8
|
/** This container measures its own rects instead of delegating. Both are decided
|
|
9
9
|
* at BlockHost, which hands the same pair to DecorationOverlay. */
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
codeLanguageLabel
|
|
11
11
|
} from '../../../a11y-strings';
|
|
12
12
|
import type { CodeMenuItem } from '../../../editor-keys';
|
|
13
|
-
import { listLanguages } from './code-languages';
|
|
13
|
+
import { getLanguageAliases, getLanguageGrammar, listLanguages } from './code-languages';
|
|
14
14
|
|
|
15
15
|
// The chrome for the modes that paint no fence: the language door plus whatever action
|
|
16
16
|
// affordances the host has earned by installing a hook. Drafts live here and only a
|
|
@@ -49,15 +49,12 @@
|
|
|
49
49
|
|
|
50
50
|
let editing = $state(false);
|
|
51
51
|
let draft = $state('');
|
|
52
|
-
//
|
|
53
|
-
// the
|
|
54
|
-
// shows with the current language highlighted, which is also what Enter would re-commit.
|
|
52
|
+
// Filtering waits for a keystroke: the field opens empty, and until then the whole list
|
|
53
|
+
// shows with the block's own language leading it, which is what a bare Enter re-commits.
|
|
55
54
|
let filtering = $state(false);
|
|
56
|
-
// Whether the LIST is what Enter should take. Until the user types or arrows, it is not:
|
|
57
|
-
// the highlight is only a where-you-stand marker, and a language the registry does not
|
|
58
|
-
// carry (an unregistered grammar, or a bare mount that registered none) would otherwise
|
|
59
|
-
// have no entry to seat on, so a bare Enter would rewrite it to the list's first row.
|
|
60
55
|
let activeIndex = $state(0);
|
|
56
|
+
/** The user moved the highlight, so the row it sits on outranks whatever they typed. */
|
|
57
|
+
let highlightMoved = false;
|
|
61
58
|
let copied = $state(false);
|
|
62
59
|
let menuOpen = $state(false);
|
|
63
60
|
let openMenuItems = $state<readonly CodeMenuItem[]>([]);
|
|
@@ -77,18 +74,31 @@
|
|
|
77
74
|
const all = ['text', ...listLanguages().filter((name) => name !== 'text')];
|
|
78
75
|
const needle = filtering ? draft.trim().toLowerCase() : '';
|
|
79
76
|
if (needle.length > 0) {
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
// An alias is a search key, never a row of its own: `rs` finds `rust`, and the list
|
|
78
|
+
// still carries one entry per language.
|
|
79
|
+
const spellings = (name: string) => [name, ...getLanguageAliases(name)];
|
|
80
|
+
const starts = all.filter((name) => spellings(name).some((s) => s.startsWith(needle)));
|
|
81
|
+
const seated = new Set(starts);
|
|
82
|
+
const contains = all.filter(
|
|
83
|
+
(name) => !seated.has(name) && spellings(name).some((s) => s.includes(needle))
|
|
84
|
+
);
|
|
82
85
|
return [...starts, ...contains];
|
|
83
86
|
}
|
|
84
|
-
// Unfiltered, the block's own language leads
|
|
85
|
-
// that matters
|
|
86
|
-
// scrolled to — which is why nothing here scrolls itself on open.
|
|
87
|
+
// Unfiltered, the block's own language leads, in the spelling the fence uses — so the
|
|
88
|
+
// row that matters is where the eye already is, and its canonical twin is not below it.
|
|
87
89
|
const current = language;
|
|
88
|
-
const rest = all.filter((name) => name !== current);
|
|
89
|
-
return
|
|
90
|
+
const rest = all.filter((name) => name !== current && !isCurrentLanguage(name));
|
|
91
|
+
return [current, ...rest];
|
|
90
92
|
});
|
|
91
93
|
|
|
94
|
+
/** One language in two spellings is still the block's own, so the mark comparing them asks
|
|
95
|
+
* the registry rather than the text. */
|
|
96
|
+
function isCurrentLanguage(name: string): boolean {
|
|
97
|
+
const canonical = (spelling: string) =>
|
|
98
|
+
getLanguageGrammar(spelling)?.name ?? spelling.toLowerCase();
|
|
99
|
+
return canonical(name) === canonical(language);
|
|
100
|
+
}
|
|
101
|
+
|
|
92
102
|
// The highlight cannot outrun a filter that shortened the list under it.
|
|
93
103
|
$effect(() => {
|
|
94
104
|
if (activeIndex >= suggestions.length) activeIndex = 0;
|
|
@@ -107,6 +117,7 @@
|
|
|
107
117
|
if (!editable) return;
|
|
108
118
|
draft = '';
|
|
109
119
|
filtering = false;
|
|
120
|
+
highlightMoved = false;
|
|
110
121
|
menuOpen = false;
|
|
111
122
|
editing = true;
|
|
112
123
|
// Row 0 is the block's own language (see `suggestions`), so the highlight starts where
|
|
@@ -148,22 +159,25 @@
|
|
|
148
159
|
if (e.isComposing) return;
|
|
149
160
|
if (e.key === 'Enter') {
|
|
150
161
|
e.preventDefault();
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
162
|
+
// A spelling the registry resolves is a name, not a query: it commits as typed, so
|
|
163
|
+
// `js` survives a picker whose rows are canonical. A moved highlight takes the row.
|
|
164
|
+
const typed = draft.trim();
|
|
165
|
+
const namesLanguage = !highlightMoved && typed !== '' && getLanguageGrammar(typed) !== null;
|
|
166
|
+
commit(
|
|
167
|
+
namesLanguage ? typed : (suggestions[activeIndex] ?? (typed === '' ? language : typed))
|
|
168
|
+
);
|
|
157
169
|
} else if (e.key === 'Escape') {
|
|
158
170
|
e.preventDefault();
|
|
159
171
|
close();
|
|
160
172
|
onCancel(true);
|
|
161
173
|
} else if (e.key === 'ArrowDown') {
|
|
162
174
|
e.preventDefault();
|
|
175
|
+
highlightMoved = true;
|
|
163
176
|
activeIndex = suggestions.length === 0 ? 0 : (activeIndex + 1) % suggestions.length;
|
|
164
177
|
scrollActiveIntoView();
|
|
165
178
|
} else if (e.key === 'ArrowUp') {
|
|
166
179
|
e.preventDefault();
|
|
180
|
+
highlightMoved = true;
|
|
167
181
|
activeIndex =
|
|
168
182
|
suggestions.length === 0 ? 0 : (activeIndex - 1 + suggestions.length) % suggestions.length;
|
|
169
183
|
scrollActiveIntoView();
|
|
@@ -431,6 +445,7 @@
|
|
|
431
445
|
placeholder="Search for a language…"
|
|
432
446
|
oninput={() => {
|
|
433
447
|
filtering = true;
|
|
448
|
+
highlightMoved = false;
|
|
434
449
|
activeIndex = 0;
|
|
435
450
|
}}
|
|
436
451
|
onkeydown={onFieldKeyDown}
|
|
@@ -444,10 +459,13 @@
|
|
|
444
459
|
<button
|
|
445
460
|
type="button"
|
|
446
461
|
role="option"
|
|
447
|
-
aria-selected={name
|
|
462
|
+
aria-selected={isCurrentLanguage(name)}
|
|
448
463
|
data-active={i === activeIndex}
|
|
449
464
|
tabindex="-1"
|
|
450
|
-
onmouseenter={() =>
|
|
465
|
+
onmouseenter={() => {
|
|
466
|
+
highlightMoved = true;
|
|
467
|
+
activeIndex = i;
|
|
468
|
+
}}
|
|
451
469
|
onmousedown={(e) => {
|
|
452
470
|
// mousedown, not click: the field's blur would otherwise cancel the edit
|
|
453
471
|
// before a click ever landed.
|
|
@@ -456,7 +474,7 @@
|
|
|
456
474
|
}}
|
|
457
475
|
>
|
|
458
476
|
<span class="code-lang-name">{name}</span>
|
|
459
|
-
{#if name
|
|
477
|
+
{#if isCurrentLanguage(name)}
|
|
460
478
|
<span class="code-lang-check">
|
|
461
479
|
{@render icon('<path d="M20 6 9 17l-5-5"/>')}
|
|
462
480
|
</span>
|
|
@@ -24,6 +24,12 @@ import ruby from 'highlight.js/lib/languages/ruby';
|
|
|
24
24
|
import markdown from 'highlight.js/lib/languages/markdown';
|
|
25
25
|
import diff from 'highlight.js/lib/languages/diff';
|
|
26
26
|
import latex from 'highlight.js/lib/languages/latex';
|
|
27
|
+
import csharp from 'highlight.js/lib/languages/csharp';
|
|
28
|
+
import kotlin from 'highlight.js/lib/languages/kotlin';
|
|
29
|
+
import swift from 'highlight.js/lib/languages/swift';
|
|
30
|
+
import php from 'highlight.js/lib/languages/php';
|
|
31
|
+
import powershell from 'highlight.js/lib/languages/powershell';
|
|
32
|
+
import dockerfile from 'highlight.js/lib/languages/dockerfile';
|
|
27
33
|
let booted = false;
|
|
28
34
|
export function bootstrapCodeLanguages() {
|
|
29
35
|
if (booted)
|
|
@@ -47,6 +53,12 @@ export function bootstrapCodeLanguages() {
|
|
|
47
53
|
registerLanguage('markdown', markdown, ['md']);
|
|
48
54
|
registerLanguage('diff', diff);
|
|
49
55
|
registerLanguage('latex', latex, ['tex']);
|
|
56
|
+
registerLanguage('csharp', csharp, ['cs', 'c#']);
|
|
57
|
+
registerLanguage('kotlin', kotlin, ['kt']);
|
|
58
|
+
registerLanguage('swift', swift);
|
|
59
|
+
registerLanguage('php', php);
|
|
60
|
+
registerLanguage('powershell', powershell, ['ps1']);
|
|
61
|
+
registerLanguage('dockerfile', dockerfile, ['docker']);
|
|
50
62
|
registerPasteSurface(codePasteSurface);
|
|
51
63
|
registerCodeContextActions();
|
|
52
64
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Language registry for code-block tokenization. Nothing outside this directory
|
|
3
3
|
* imports highlight.js directly; static vs. dynamic loading is a policy on top.
|
|
4
|
+
* Aliases fold to their canonical name here, so no caller carries its own table.
|
|
4
5
|
*/
|
|
5
6
|
import type { LanguageFn } from 'highlight.js';
|
|
6
7
|
export interface LanguageGrammar {
|
|
@@ -11,11 +12,10 @@ export interface LanguageGrammar {
|
|
|
11
12
|
export declare function registerLanguage(name: string, definition: LanguageFn, aliasList?: readonly string[]): void;
|
|
12
13
|
/** Info strings with trailing attributes (`js {1-3}`) resolve on the first token. */
|
|
13
14
|
export declare function getLanguageGrammar(infoString: string): LanguageGrammar | null;
|
|
14
|
-
/**
|
|
15
|
-
* Every registered grammar name, plus every alias, sorted — what the language picker
|
|
16
|
-
* offers. Aliases are listed as their own entries: a user typing `js` should find it
|
|
17
|
-
* rather than having to know it resolves to `javascript`.
|
|
18
|
-
*/
|
|
15
|
+
/** Every registered language once, under its canonical name, sorted — the picker's rows. */
|
|
19
16
|
export declare function listLanguages(): string[];
|
|
17
|
+
/** The alternate spellings a language answers to, from any spelling of it — a picker's filter
|
|
18
|
+
* matches `rs` to `rust` without a table of its own. */
|
|
19
|
+
export declare function getLanguageAliases(name: string): readonly string[];
|
|
20
20
|
/** Test-only: clear all registered languages. */
|
|
21
21
|
export declare function __resetRegistryForTests(): void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Language registry for code-block tokenization. Nothing outside this directory
|
|
3
3
|
* imports highlight.js directly; static vs. dynamic loading is a policy on top.
|
|
4
|
+
* Aliases fold to their canonical name here, so no caller carries its own table.
|
|
4
5
|
*/
|
|
5
6
|
const grammars = new Map();
|
|
6
7
|
const aliases = new Map();
|
|
@@ -14,25 +15,32 @@ export function registerLanguage(name, definition, aliasList = []) {
|
|
|
14
15
|
aliases.set(alias.toLowerCase(), key);
|
|
15
16
|
}
|
|
16
17
|
}
|
|
18
|
+
/** The fold: any spelling of a language to the one name it is registered under. A name of its
|
|
19
|
+
* own beats another language's alias, so no grammar is shadowed by somebody's nickname. */
|
|
20
|
+
function canonicalName(spelling) {
|
|
21
|
+
const key = spelling.toLowerCase();
|
|
22
|
+
if (grammars.has(key))
|
|
23
|
+
return key;
|
|
24
|
+
return aliases.get(key) ?? key;
|
|
25
|
+
}
|
|
17
26
|
/** Info strings with trailing attributes (`js {1-3}`) resolve on the first token. */
|
|
18
27
|
export function getLanguageGrammar(infoString) {
|
|
19
28
|
const trimmed = infoString.trim();
|
|
20
29
|
if (trimmed.length === 0)
|
|
21
30
|
return null;
|
|
22
|
-
|
|
23
|
-
const resolvedName = aliases.get(firstToken) ?? firstToken;
|
|
24
|
-
return grammars.get(resolvedName) ?? null;
|
|
31
|
+
return grammars.get(canonicalName(trimmed.split(/\s+/)[0])) ?? null;
|
|
25
32
|
}
|
|
26
|
-
/**
|
|
27
|
-
* Every registered grammar name, plus every alias, sorted — what the language picker
|
|
28
|
-
* offers. Aliases are listed as their own entries: a user typing `js` should find it
|
|
29
|
-
* rather than having to know it resolves to `javascript`.
|
|
30
|
-
*/
|
|
33
|
+
/** Every registered language once, under its canonical name, sorted — the picker's rows. */
|
|
31
34
|
export function listLanguages() {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
return [...grammars.keys()].sort();
|
|
36
|
+
}
|
|
37
|
+
/** The alternate spellings a language answers to, from any spelling of it — a picker's filter
|
|
38
|
+
* matches `rs` to `rust` without a table of its own. */
|
|
39
|
+
export function getLanguageAliases(name) {
|
|
40
|
+
const key = canonicalName(name);
|
|
41
|
+
if (!grammars.has(key))
|
|
42
|
+
return [];
|
|
43
|
+
return [...aliases].filter(([, target]) => target === key).map(([alias]) => alias);
|
|
36
44
|
}
|
|
37
45
|
/** Test-only: clear all registered languages. */
|
|
38
46
|
export function __resetRegistryForTests() {
|
|
@@ -52,9 +52,9 @@ export interface EditableLeafDeps {
|
|
|
52
52
|
onSourceEdit?(text: string): void;
|
|
53
53
|
/**
|
|
54
54
|
* A source that is only its own chrome (a `$$$$` with no body line), completed to the shape
|
|
55
|
-
* a caret can sit in, with where the caret goes. Applied as the source is revealed
|
|
56
|
-
*
|
|
57
|
-
* alone. The edit is the reveal's own, committed on blur
|
|
55
|
+
* a caret can sit in, with where the caret goes. Applied as the source is revealed AND after
|
|
56
|
+
* any edit that empties it, so the block is one the user can type into and delete however it
|
|
57
|
+
* got there; null leaves the bytes alone. The edit is the reveal's own, committed on blur.
|
|
58
58
|
*/
|
|
59
59
|
completeBareSource?(text: string): {
|
|
60
60
|
text: string;
|
|
@@ -325,11 +325,15 @@ export function createEditableLeaf(deps) {
|
|
|
325
325
|
const keystroke = end - start <= 1 && insert.length <= 1 && insert !== '\n';
|
|
326
326
|
if (deps.renderSource)
|
|
327
327
|
recordSourceEdit(text, getCursorOffset(el) ?? start, keystroke);
|
|
328
|
-
const
|
|
328
|
+
const spliced = text.slice(0, start) + insert + text.slice(end);
|
|
329
|
+
// An edit that empties the body leaves the same chrome-only source a bare block arrives
|
|
330
|
+
// as, so the edit door owes the completion the reveal door applies.
|
|
331
|
+
const completed = deps.completeBareSource?.(spliced) ?? null;
|
|
332
|
+
const next = completed?.text ?? spliced;
|
|
329
333
|
paintSource(el, next);
|
|
330
334
|
deps.onSourceEdit?.(next);
|
|
331
335
|
preEditOffset = start;
|
|
332
|
-
setCursorOffset(el, asDomTextOffset(start + insert.length));
|
|
336
|
+
setCursorOffset(el, asDomTextOffset(completed?.caret ?? start + insert.length));
|
|
333
337
|
// Armed once the edit has settled, so the window measures the gap the user leaves.
|
|
334
338
|
if (deps.renderSource && keystroke)
|
|
335
339
|
sourceBatch.armPause();
|
|
@@ -39,16 +39,20 @@
|
|
|
39
39
|
import { dispatchKindCommand } from '../../../schema/block-commands';
|
|
40
40
|
import type { AnyCommandId } from '../../../schema/command-id';
|
|
41
41
|
import BlockDragHandle from '../../BlockDragHandle.svelte';
|
|
42
|
+
import { showsListItemDragHandle } from '../../drag-handle';
|
|
42
43
|
|
|
43
44
|
let {
|
|
44
45
|
node,
|
|
45
46
|
index,
|
|
46
47
|
myPath = [],
|
|
48
|
+
itemCount,
|
|
47
49
|
slots
|
|
48
50
|
}: {
|
|
49
51
|
node: NodeView;
|
|
50
52
|
index: number;
|
|
51
53
|
myPath?: number[];
|
|
54
|
+
/** How many items the enclosing list holds; a lone item has no sibling to reorder past. */
|
|
55
|
+
itemCount: number;
|
|
52
56
|
slots?: RefSlots<BlockComponent>;
|
|
53
57
|
} = $props();
|
|
54
58
|
|
|
@@ -67,6 +71,10 @@
|
|
|
67
71
|
// $derived, not a mount-time snapshot: a runtime prop toggle must reach blocks
|
|
68
72
|
// that window in and out after the change, not just those mounted at mount.
|
|
69
73
|
const dragHandles = $derived(getDragHandles?.() ?? false);
|
|
74
|
+
// The grip is the only pointer road into a reorder, and a reorder needs a sibling: a lone
|
|
75
|
+
// item stays a reorder host (a dragged sibling never arrives, but the class costs nothing)
|
|
76
|
+
// and simply shows nothing to grab.
|
|
77
|
+
const showsHandle = $derived(showsListItemDragHandle(itemCount, dragHandles));
|
|
70
78
|
const presentationMode = $derived(getPresentationMode?.() ?? 'source');
|
|
71
79
|
const readOnly = $derived(presentationMode === 'reading');
|
|
72
80
|
|
|
@@ -260,7 +268,7 @@
|
|
|
260
268
|
<div
|
|
261
269
|
class="list-item-block"
|
|
262
270
|
class:reorder-host={dragHandles}
|
|
263
|
-
class:handle-host={
|
|
271
|
+
class:handle-host={showsHandle}
|
|
264
272
|
data-task-checked={taskCheckedAttr}
|
|
265
273
|
data-list-marker={presentationMarkerKind}
|
|
266
274
|
bind:this={boxEl}
|
|
@@ -277,8 +285,9 @@
|
|
|
277
285
|
/>
|
|
278
286
|
</div>
|
|
279
287
|
<!-- A list item IS a reorder unit; its inner content BlockList passes the
|
|
280
|
-
default reorderable={false}, so the paragraph inside gets no handle.
|
|
281
|
-
|
|
288
|
+
default reorderable={false}, so the paragraph inside gets no handle. A lone item
|
|
289
|
+
renders none either: its drag is list-scoped and there is no sibling to pass. -->
|
|
290
|
+
{#if showsHandle}
|
|
282
291
|
<BlockDragHandle />
|
|
283
292
|
{/if}
|
|
284
293
|
</div>
|
|
@@ -5,6 +5,8 @@ type $$ComponentProps = {
|
|
|
5
5
|
node: NodeView;
|
|
6
6
|
index: number;
|
|
7
7
|
myPath?: number[];
|
|
8
|
+
/** How many items the enclosing list holds; a lone item has no sibling to reorder past. */
|
|
9
|
+
itemCount: number;
|
|
8
10
|
slots?: RefSlots<BlockComponent>;
|
|
9
11
|
};
|
|
10
12
|
declare const ListItemBlock: import("svelte").Component<$$ComponentProps, {
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
/** Caret in a real text node the reader can see: the native caret renders there, so the
|
|
3
3
|
* synthetic indicator stands down. Hidden marker text renders nothing, so it does not count. */
|
|
4
4
|
export declare function caretIsInTextContent(el: HTMLElement, sel: Selection | null): boolean;
|
|
5
|
+
/** A ranged selection this surface holds — the click ladder's rungs, a shift-click, a
|
|
6
|
+
* drag-select. It is the gesture's own paint, so a click-time caret seat stands down for it. */
|
|
7
|
+
export declare function surfaceHoldsRange(el: HTMLElement, sel: Selection | null): boolean;
|
|
5
8
|
/** Ctrl/meta/alt held — a platform command, never an edit of the byte beside the caret. */
|
|
6
9
|
export declare function hasModifier(e: KeyboardEvent): boolean;
|
|
7
10
|
/** A typed character: no modifier chord, one code point. A chord is a command instead. */
|
|
@@ -9,6 +9,14 @@ export function caretIsInTextContent(el, sel) {
|
|
|
9
9
|
const node = sel.getRangeAt(0).startContainer;
|
|
10
10
|
return node.nodeType === Node.TEXT_NODE && el.contains(node) && !isHiddenMarkerText(node, el);
|
|
11
11
|
}
|
|
12
|
+
/** A ranged selection this surface holds — the click ladder's rungs, a shift-click, a
|
|
13
|
+
* drag-select. It is the gesture's own paint, so a click-time caret seat stands down for it. */
|
|
14
|
+
export function surfaceHoldsRange(el, sel) {
|
|
15
|
+
if (!sel || sel.isCollapsed || sel.rangeCount === 0)
|
|
16
|
+
return false;
|
|
17
|
+
const range = sel.getRangeAt(0);
|
|
18
|
+
return el.contains(range.startContainer) || el.contains(range.endContainer);
|
|
19
|
+
}
|
|
12
20
|
/** Ctrl/meta/alt held — a platform command, never an edit of the byte beside the caret. */
|
|
13
21
|
export function hasModifier(e) {
|
|
14
22
|
return e.ctrlKey || e.metaKey || e.altKey;
|