@liminis/editor 0.1.0 → 0.2.0
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 +126 -14
- package/dist/annotations/types.d.ts +16 -0
- package/dist/app/App.d.ts +1 -1
- package/dist/app/App.js +3 -3
- package/dist/app/editor/AnchorScrollPlugin.js +2 -34
- package/dist/app/editor/AnnotationSurface.js +2 -1
- package/dist/app/editor/CorrectionPanelPlugin.js +10 -10
- package/dist/app/editor/DocumentOutline.d.ts +17 -0
- package/dist/app/editor/DocumentOutline.js +32 -0
- package/dist/app/editor/DragHandlePlugin.js +1 -1
- package/dist/app/editor/Editor.d.ts +10 -1
- package/dist/app/editor/Editor.js +3 -2
- package/dist/app/editor/OutlinePlugin.d.ts +14 -0
- package/dist/app/editor/OutlinePlugin.js +133 -0
- package/dist/app/editor/SelectionContextMenuPlugin.js +4 -4
- package/dist/app/editor/annotation-marks.d.ts +20 -0
- package/dist/app/editor/annotation-marks.js +40 -0
- package/dist/app/editor/documentOutlineHandle.d.ts +87 -0
- package/dist/app/editor/documentOutlineHandle.js +86 -0
- package/dist/app/editor/nodes/C4Component.js +6 -6
- package/dist/app/editor/nodes/DiagramContextMenu.js +5 -5
- package/dist/app/editor/scrollContainer.d.ts +18 -0
- package/dist/app/editor/scrollContainer.js +47 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +6 -0
- package/dist/markdown/stringify.js +65 -0
- package/dist/styles.css +342 -221
- package/docs/decisions/adr-078.md +37 -2
- package/docs/decisions/adr-085.md +166 -0
- package/docs/decisions/adr-086.md +147 -0
- package/docs/decisions/adr-087.md +216 -0
- package/docs/decisions/adr-088.md +136 -0
- package/docs/decisions/adr-089.md +121 -0
- package/docs/editor-api.md +53 -1
- package/package.json +23 -46
|
@@ -36,10 +36,10 @@ const itemStyle = {
|
|
|
36
36
|
function SelectionContextMenu({ visible, x, y, selectedText, onChatAboutThis, annotationAffordances, onCreateAnnotation, onClose, }) {
|
|
37
37
|
const menuRef = useRef(null);
|
|
38
38
|
const isDark = typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
|
|
39
|
-
const bgColor = `var(--vscode-menu-background, ${isDark ? '#252526' : '#ffffff'})`;
|
|
40
|
-
const borderColor = `var(--vscode-menu-border, ${isDark ? '#454545' : '#d4d4d4'})`;
|
|
41
|
-
const textColor = `var(--vscode-menu-foreground, ${isDark ? '#cccccc' : '#333333'})`;
|
|
42
|
-
const hoverBg = `var(--vscode-menu-selectionBackground, ${isDark ? '#094771' : '#e8e8e8'})`;
|
|
39
|
+
const bgColor = `var(--liminis-editor-menu-background, var(--vscode-menu-background, ${isDark ? '#252526' : '#ffffff'}))`;
|
|
40
|
+
const borderColor = `var(--liminis-editor-menu-border, var(--vscode-menu-border, ${isDark ? '#454545' : '#d4d4d4'}))`;
|
|
41
|
+
const textColor = `var(--liminis-editor-menu-foreground, var(--vscode-menu-foreground, ${isDark ? '#cccccc' : '#333333'}))`;
|
|
42
|
+
const hoverBg = `var(--liminis-editor-menu-selectionBackground, var(--vscode-menu-selectionBackground, ${isDark ? '#094771' : '#e8e8e8'}))`;
|
|
43
43
|
useEffect(() => {
|
|
44
44
|
if (!visible)
|
|
45
45
|
return;
|
|
@@ -157,6 +157,26 @@ export declare function markElementsForId(editor: LexicalEditor, id: string): HT
|
|
|
157
157
|
* are in document order, and ids with no live mark are simply absent.
|
|
158
158
|
*/
|
|
159
159
|
export declare function markElementsByAnnotationId(editor: LexicalEditor, ids: ReadonlySet<string>): Map<string, HTMLElement[]>;
|
|
160
|
+
/**
|
|
161
|
+
* Current on-screen geometry (#73) for every live mark of the requested
|
|
162
|
+
* annotations, keyed by annotation id — one `DOMRect` per constituent
|
|
163
|
+
* `MarkNode`, in document order, computed fresh at call time via
|
|
164
|
+
* `getBoundingClientRect()` (viewport-relative, matching the only existing
|
|
165
|
+
* precedent for rect delivery in this package, `AnnotationCreateEvent.rect`
|
|
166
|
+
* in `AnnotationPlugin.tsx`). Ids with no currently live mark are simply
|
|
167
|
+
* absent from the result, never an error.
|
|
168
|
+
*
|
|
169
|
+
* `ids` omitted (or `undefined`) returns geometry for every annotation that
|
|
170
|
+
* currently has at least one live mark, mirroring
|
|
171
|
+
* {@link collectLiveAnchorSnapshots}'s own "no ids given -> collect every
|
|
172
|
+
* live id" behavior.
|
|
173
|
+
*
|
|
174
|
+
* Two different annotation ids can legitimately return an identical rect:
|
|
175
|
+
* overlapping annotations may share one `MarkNode` (`MarkNode.getIDs()` can
|
|
176
|
+
* hold more than one id), and that shared element's rect is reported under
|
|
177
|
+
* each id it carries. That is expected, not a bug.
|
|
178
|
+
*/
|
|
179
|
+
export declare function getMarkRects(editor: LexicalEditor, ids?: readonly string[]): Map<string, DOMRect[]>;
|
|
160
180
|
/**
|
|
161
181
|
* Removes every `MarkNode` wrapping `id` (annotation deleted, or composer
|
|
162
182
|
* cancelled before submit). A mark shared with other annotation ids
|
|
@@ -710,6 +710,46 @@ export function markElementsByAnnotationId(editor, ids) {
|
|
|
710
710
|
});
|
|
711
711
|
return byId;
|
|
712
712
|
}
|
|
713
|
+
/**
|
|
714
|
+
* Current on-screen geometry (#73) for every live mark of the requested
|
|
715
|
+
* annotations, keyed by annotation id — one `DOMRect` per constituent
|
|
716
|
+
* `MarkNode`, in document order, computed fresh at call time via
|
|
717
|
+
* `getBoundingClientRect()` (viewport-relative, matching the only existing
|
|
718
|
+
* precedent for rect delivery in this package, `AnnotationCreateEvent.rect`
|
|
719
|
+
* in `AnnotationPlugin.tsx`). Ids with no currently live mark are simply
|
|
720
|
+
* absent from the result, never an error.
|
|
721
|
+
*
|
|
722
|
+
* `ids` omitted (or `undefined`) returns geometry for every annotation that
|
|
723
|
+
* currently has at least one live mark, mirroring
|
|
724
|
+
* {@link collectLiveAnchorSnapshots}'s own "no ids given -> collect every
|
|
725
|
+
* live id" behavior.
|
|
726
|
+
*
|
|
727
|
+
* Two different annotation ids can legitimately return an identical rect:
|
|
728
|
+
* overlapping annotations may share one `MarkNode` (`MarkNode.getIDs()` can
|
|
729
|
+
* hold more than one id), and that shared element's rect is reported under
|
|
730
|
+
* each id it carries. That is expected, not a bug.
|
|
731
|
+
*/
|
|
732
|
+
export function getMarkRects(editor, ids) {
|
|
733
|
+
return editor.getEditorState().read(() => {
|
|
734
|
+
let targetIds;
|
|
735
|
+
if (ids === undefined) {
|
|
736
|
+
targetIds = new Set();
|
|
737
|
+
for (const markNode of collectMarkNodesInOrder($getRoot())) {
|
|
738
|
+
for (const id of markNode.getIDs())
|
|
739
|
+
targetIds.add(id);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
targetIds = new Set(ids);
|
|
744
|
+
}
|
|
745
|
+
const elementsById = markElementsByAnnotationId(editor, targetIds);
|
|
746
|
+
const rectsById = new Map();
|
|
747
|
+
for (const [id, elements] of elementsById) {
|
|
748
|
+
rectsById.set(id, elements.map((element) => element.getBoundingClientRect()));
|
|
749
|
+
}
|
|
750
|
+
return rectsById;
|
|
751
|
+
});
|
|
752
|
+
}
|
|
713
753
|
/**
|
|
714
754
|
* Removes every `MarkNode` wrapping `id` (annotation deleted, or composer
|
|
715
755
|
* cancelled before submit). A mark shared with other annotation ids
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The controller shared between one `<Editor documentOutlineHandle={…}>` and
|
|
3
|
+
* one `<DocumentOutline handle={…}>`. `OutlinePlugin` feeds it from inside the
|
|
4
|
+
* Lexical tree; `DocumentOutline` reads it via `useSyncExternalStore`.
|
|
5
|
+
*
|
|
6
|
+
* A plain `MutableRefObject` (the pattern `annotationEditorHandleRef` uses)
|
|
7
|
+
* does not fit here: `DocumentOutline` must re-render live as the reader
|
|
8
|
+
* scrolls and the document changes, and mutating a ref's `.current` does not
|
|
9
|
+
* propagate to a sibling component reading the same ref. An external-store
|
|
10
|
+
* object solves that, and — because the consumer creates one per editor
|
|
11
|
+
* instance — it is naturally scoped to that instance, so mounting more than
|
|
12
|
+
* one editor at once never aggregates headings across them.
|
|
13
|
+
*/
|
|
14
|
+
/** A single heading captured for display. */
|
|
15
|
+
export interface OutlineEntry {
|
|
16
|
+
/**
|
|
17
|
+
* Position within the document's H1–H5 heading order. Identity for
|
|
18
|
+
* selection and active-tracking is by this index, not by `text`, so
|
|
19
|
+
* duplicate heading titles are handled correctly.
|
|
20
|
+
*/
|
|
21
|
+
index: number;
|
|
22
|
+
/** Heading level. H6 is out of scope — the editor theme has no class for it. */
|
|
23
|
+
level: 1 | 2 | 3 | 4 | 5;
|
|
24
|
+
/** Heading text with inline content (including inline code) reduced to plain text. */
|
|
25
|
+
text: string;
|
|
26
|
+
}
|
|
27
|
+
export interface DocumentOutlineSnapshot {
|
|
28
|
+
entries: OutlineEntry[];
|
|
29
|
+
/** Index of the entry whose heading is at the top of the viewport, or `null` when there is no active heading (e.g. no headings at all). */
|
|
30
|
+
activeIndex: number | null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Not exported, so nothing outside this module can reference it. Its only
|
|
34
|
+
* purpose is to make `DocumentOutlineHandle` nominal instead of structural:
|
|
35
|
+
* without it, any object with matching method names would type-check as a
|
|
36
|
+
* handle, and `OutlinePlugin`'s cast to `OutlineHandleImpl` (to reach
|
|
37
|
+
* `connect`/`disconnect`/`publish`) would throw at runtime for a hand-rolled
|
|
38
|
+
* one. Only `OutlineHandleImpl`, constructed exclusively by
|
|
39
|
+
* `createDocumentOutlineHandle()`, can supply this property.
|
|
40
|
+
*/
|
|
41
|
+
declare const HANDLE_BRAND: unique symbol;
|
|
42
|
+
/**
|
|
43
|
+
* The public surface a consumer reads and passes around. Create one with
|
|
44
|
+
* `createDocumentOutlineHandle()` and pass the same instance to both
|
|
45
|
+
* `<Editor documentOutlineHandle>` and `<DocumentOutline handle>`.
|
|
46
|
+
*/
|
|
47
|
+
export interface DocumentOutlineHandle {
|
|
48
|
+
/** Brand only, not a real property to read — see `HANDLE_BRAND`. */
|
|
49
|
+
readonly [HANDLE_BRAND]: true;
|
|
50
|
+
/** React external-store subscription — see `useSyncExternalStore`. */
|
|
51
|
+
subscribe(onStoreChange: () => void): () => void;
|
|
52
|
+
/** React external-store snapshot. Stable by reference until the outline actually changes. */
|
|
53
|
+
getSnapshot(): DocumentOutlineSnapshot;
|
|
54
|
+
/** Scroll the editor to the heading at `index`. No-ops if no editor is currently connected, or `index` is out of range. */
|
|
55
|
+
scrollToHeading(index: number): void;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The concrete object `createDocumentOutlineHandle()` returns. Consumers only
|
|
59
|
+
* ever see the `DocumentOutlineHandle` surface above; `OutlinePlugin` — the
|
|
60
|
+
* only other reader of an instance, always produced by
|
|
61
|
+
* `createDocumentOutlineHandle()` — reaches `connect`/`disconnect`/`publish`
|
|
62
|
+
* directly on this class.
|
|
63
|
+
*/
|
|
64
|
+
export declare class OutlineHandleImpl implements DocumentOutlineHandle {
|
|
65
|
+
readonly [HANDLE_BRAND]: true;
|
|
66
|
+
private snapshot;
|
|
67
|
+
private readonly listeners;
|
|
68
|
+
private scrollImpl;
|
|
69
|
+
subscribe: (onStoreChange: () => void) => (() => void);
|
|
70
|
+
getSnapshot: () => DocumentOutlineSnapshot;
|
|
71
|
+
scrollToHeading: (index: number) => void;
|
|
72
|
+
/** Called by `OutlinePlugin` while its editor is mounted. */
|
|
73
|
+
connect(scrollImpl: (index: number) => void): void;
|
|
74
|
+
/** Called by `OutlinePlugin` on unmount, so a stale handle no-ops rather than scrolling a torn-down editor. */
|
|
75
|
+
disconnect(): void;
|
|
76
|
+
/** Called by `OutlinePlugin` whenever headings or the active heading change. */
|
|
77
|
+
publish(next: DocumentOutlineSnapshot): void;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Create a controller shared between one `<Editor documentOutlineHandle={…}>`
|
|
81
|
+
* and one `<DocumentOutline handle={…}>`. Create it once per editor instance
|
|
82
|
+
* (e.g. via `useState(() => createDocumentOutlineHandle())`) and pass the
|
|
83
|
+
* same object to both — that is what scopes the outline to that editor, even
|
|
84
|
+
* when a consumer mounts several editors at once.
|
|
85
|
+
*/
|
|
86
|
+
export declare function createDocumentOutlineHandle(): DocumentOutlineHandle;
|
|
87
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The controller shared between one `<Editor documentOutlineHandle={…}>` and
|
|
3
|
+
* one `<DocumentOutline handle={…}>`. `OutlinePlugin` feeds it from inside the
|
|
4
|
+
* Lexical tree; `DocumentOutline` reads it via `useSyncExternalStore`.
|
|
5
|
+
*
|
|
6
|
+
* A plain `MutableRefObject` (the pattern `annotationEditorHandleRef` uses)
|
|
7
|
+
* does not fit here: `DocumentOutline` must re-render live as the reader
|
|
8
|
+
* scrolls and the document changes, and mutating a ref's `.current` does not
|
|
9
|
+
* propagate to a sibling component reading the same ref. An external-store
|
|
10
|
+
* object solves that, and — because the consumer creates one per editor
|
|
11
|
+
* instance — it is naturally scoped to that instance, so mounting more than
|
|
12
|
+
* one editor at once never aggregates headings across them.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Not exported, so nothing outside this module can reference it. Its only
|
|
16
|
+
* purpose is to make `DocumentOutlineHandle` nominal instead of structural:
|
|
17
|
+
* without it, any object with matching method names would type-check as a
|
|
18
|
+
* handle, and `OutlinePlugin`'s cast to `OutlineHandleImpl` (to reach
|
|
19
|
+
* `connect`/`disconnect`/`publish`) would throw at runtime for a hand-rolled
|
|
20
|
+
* one. Only `OutlineHandleImpl`, constructed exclusively by
|
|
21
|
+
* `createDocumentOutlineHandle()`, can supply this property.
|
|
22
|
+
*/
|
|
23
|
+
const HANDLE_BRAND = Symbol('DocumentOutlineHandle');
|
|
24
|
+
const EMPTY_SNAPSHOT = { entries: [], activeIndex: null };
|
|
25
|
+
function snapshotsEqual(a, b) {
|
|
26
|
+
if (a.activeIndex !== b.activeIndex)
|
|
27
|
+
return false;
|
|
28
|
+
if (a.entries.length !== b.entries.length)
|
|
29
|
+
return false;
|
|
30
|
+
for (let i = 0; i < a.entries.length; i++) {
|
|
31
|
+
const x = a.entries[i];
|
|
32
|
+
const y = b.entries[i];
|
|
33
|
+
if (x.index !== y.index || x.level !== y.level || x.text !== y.text)
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The concrete object `createDocumentOutlineHandle()` returns. Consumers only
|
|
40
|
+
* ever see the `DocumentOutlineHandle` surface above; `OutlinePlugin` — the
|
|
41
|
+
* only other reader of an instance, always produced by
|
|
42
|
+
* `createDocumentOutlineHandle()` — reaches `connect`/`disconnect`/`publish`
|
|
43
|
+
* directly on this class.
|
|
44
|
+
*/
|
|
45
|
+
export class OutlineHandleImpl {
|
|
46
|
+
[HANDLE_BRAND] = true;
|
|
47
|
+
snapshot = EMPTY_SNAPSHOT;
|
|
48
|
+
listeners = new Set();
|
|
49
|
+
scrollImpl = null;
|
|
50
|
+
subscribe = (onStoreChange) => {
|
|
51
|
+
this.listeners.add(onStoreChange);
|
|
52
|
+
return () => {
|
|
53
|
+
this.listeners.delete(onStoreChange);
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
getSnapshot = () => this.snapshot;
|
|
57
|
+
scrollToHeading = (index) => {
|
|
58
|
+
this.scrollImpl?.(index);
|
|
59
|
+
};
|
|
60
|
+
/** Called by `OutlinePlugin` while its editor is mounted. */
|
|
61
|
+
connect(scrollImpl) {
|
|
62
|
+
this.scrollImpl = scrollImpl;
|
|
63
|
+
}
|
|
64
|
+
/** Called by `OutlinePlugin` on unmount, so a stale handle no-ops rather than scrolling a torn-down editor. */
|
|
65
|
+
disconnect() {
|
|
66
|
+
this.scrollImpl = null;
|
|
67
|
+
}
|
|
68
|
+
/** Called by `OutlinePlugin` whenever headings or the active heading change. */
|
|
69
|
+
publish(next) {
|
|
70
|
+
if (snapshotsEqual(this.snapshot, next))
|
|
71
|
+
return;
|
|
72
|
+
this.snapshot = next;
|
|
73
|
+
for (const listener of this.listeners)
|
|
74
|
+
listener();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Create a controller shared between one `<Editor documentOutlineHandle={…}>`
|
|
79
|
+
* and one `<DocumentOutline handle={…}>`. Create it once per editor instance
|
|
80
|
+
* (e.g. via `useState(() => createDocumentOutlineHandle())`) and pass the
|
|
81
|
+
* same object to both — that is what scopes the outline to that editor, even
|
|
82
|
+
* when a consumer mounts several editors at once.
|
|
83
|
+
*/
|
|
84
|
+
export function createDocumentOutlineHandle() {
|
|
85
|
+
return new OutlineHandleImpl();
|
|
86
|
+
}
|
|
@@ -181,11 +181,11 @@ function C4DiagramDisplay({ code, nodeKey, onDoubleClick, isEditable, }) {
|
|
|
181
181
|
border: 'none',
|
|
182
182
|
cursor: 'pointer',
|
|
183
183
|
backgroundColor: isEditingLayout
|
|
184
|
-
? 'var(--color-primary-100, rgba(59, 130, 246, 0.1))'
|
|
185
|
-
: 'var(--color-muted-100, rgba(128, 128, 128, 0.1))',
|
|
184
|
+
? 'var(--liminis-editor-primary-100, var(--color-primary-100, rgba(59, 130, 246, 0.1)))'
|
|
185
|
+
: 'var(--liminis-editor-muted-100, var(--color-muted-100, rgba(128, 128, 128, 0.1)))',
|
|
186
186
|
color: isEditingLayout
|
|
187
|
-
? 'var(--color-primary, #3b82f6)'
|
|
188
|
-
: 'var(--color-muted-foreground, #6b7280)',
|
|
187
|
+
? 'var(--liminis-editor-primary, var(--color-primary, #3b82f6))'
|
|
188
|
+
: 'var(--liminis-editor-muted-foreground, var(--color-muted-foreground, #6b7280))',
|
|
189
189
|
}, children: _jsx(Move, { size: 16 }) }), manualLayout && (_jsx("button", { type: "button", onClick: (e) => {
|
|
190
190
|
e.stopPropagation();
|
|
191
191
|
handleResetLayout();
|
|
@@ -198,8 +198,8 @@ function C4DiagramDisplay({ code, nodeKey, onDoubleClick, isEditable, }) {
|
|
|
198
198
|
borderRadius: '4px',
|
|
199
199
|
border: 'none',
|
|
200
200
|
cursor: 'pointer',
|
|
201
|
-
backgroundColor: 'var(--color-muted-100, rgba(128, 128, 128, 0.1))',
|
|
202
|
-
color: 'var(--color-muted-foreground, #6b7280)',
|
|
201
|
+
backgroundColor: 'var(--liminis-editor-muted-100, var(--color-muted-100, rgba(128, 128, 128, 0.1)))',
|
|
202
|
+
color: 'var(--liminis-editor-muted-foreground, var(--color-muted-foreground, #6b7280))',
|
|
203
203
|
}, children: _jsx(RotateCcw, { size: 16 }) }))] })), isEditingLayout && parseResult.diagram ? (_jsx(C4InteractiveRenderer, { diagram: parseResult.diagram, isDarkMode: isDarkMode, isEditMode: isEditingLayout, manualPositions: manualPositions, onPositionChange: handlePositionChange })) : (_jsx(C4Renderer, { layout: layout, isDarkMode: isDarkMode })), _jsx(DiagramContextMenu, { ...contextMenu.props, onEditText: isEditable ? onDoubleClick : undefined, onEditLayout: isEditable ? () => setIsEditingLayout(prev => !prev) : undefined, onResetLayout: isEditable ? handleResetLayout : undefined, isEditingLayout: isEditingLayout, hasManualLayout: !!manualLayout })] }));
|
|
204
204
|
}
|
|
205
205
|
// =============================================================================
|
|
@@ -75,11 +75,11 @@ const separatorStyle = {
|
|
|
75
75
|
export function DiagramContextMenu({ visible, x, y, onCopyImage, onClose, onEditText, onEditLayout, onResetLayout, isEditingLayout, hasManualLayout, }) {
|
|
76
76
|
const menuRef = useRef(null);
|
|
77
77
|
const isDark = typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
|
|
78
|
-
const bgColor = `var(--vscode-menu-background, ${isDark ? '#252526' : '#ffffff'})`;
|
|
79
|
-
const borderColor = `var(--vscode-menu-border, ${isDark ? '#454545' : '#d4d4d4'})`;
|
|
80
|
-
const textColor = `var(--vscode-menu-foreground, ${isDark ? '#cccccc' : '#333333'})`;
|
|
81
|
-
const hoverBg = `var(--vscode-menu-selectionBackground, ${isDark ? '#094771' : '#e8e8e8'})`;
|
|
82
|
-
const separatorColor = `var(--vscode-menu-separatorBackground, ${isDark ? '#454545' : '#d4d4d4'})`;
|
|
78
|
+
const bgColor = `var(--liminis-editor-menu-background, var(--vscode-menu-background, ${isDark ? '#252526' : '#ffffff'}))`;
|
|
79
|
+
const borderColor = `var(--liminis-editor-menu-border, var(--vscode-menu-border, ${isDark ? '#454545' : '#d4d4d4'}))`;
|
|
80
|
+
const textColor = `var(--liminis-editor-menu-foreground, var(--vscode-menu-foreground, ${isDark ? '#cccccc' : '#333333'}))`;
|
|
81
|
+
const hoverBg = `var(--liminis-editor-menu-selectionBackground, var(--vscode-menu-selectionBackground, ${isDark ? '#094771' : '#e8e8e8'}))`;
|
|
82
|
+
const separatorColor = `var(--liminis-editor-menu-separatorBackground, var(--vscode-menu-separatorBackground, ${isDark ? '#454545' : '#d4d4d4'}))`;
|
|
83
83
|
// Close on click outside or Escape
|
|
84
84
|
useEffect(() => {
|
|
85
85
|
if (!visible)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared DOM helpers for locating an editor's scroll container and scrolling
|
|
3
|
+
* an element into view within it. `AnchorScrollPlugin` (anchor-link
|
|
4
|
+
* navigation) and `OutlinePlugin` (table-of-contents navigation) both need to
|
|
5
|
+
* find "the nearest thing that actually scrolls" without the host declaring
|
|
6
|
+
* one explicitly (FR-006) — this is the one place that logic lives.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Find the scrollable container for an element: the known editor scroll ids
|
|
10
|
+
* first, then the nearest ancestor that actually scrolls (robust to host
|
|
11
|
+
* markup).
|
|
12
|
+
*/
|
|
13
|
+
export declare function scrollContainerFor(element: HTMLElement): HTMLElement | null;
|
|
14
|
+
/**
|
|
15
|
+
* Scroll `target` into view within its scroll container (or the viewport, if
|
|
16
|
+
* none is found), positioning it near the top rather than flush against it.
|
|
17
|
+
*/
|
|
18
|
+
export declare function scrollElementIntoView(target: HTMLElement, margin?: number): void;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared DOM helpers for locating an editor's scroll container and scrolling
|
|
3
|
+
* an element into view within it. `AnchorScrollPlugin` (anchor-link
|
|
4
|
+
* navigation) and `OutlinePlugin` (table-of-contents navigation) both need to
|
|
5
|
+
* find "the nearest thing that actually scrolls" without the host declaring
|
|
6
|
+
* one explicitly (FR-006) — this is the one place that logic lives.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Find the scrollable container for an element: the known editor scroll ids
|
|
10
|
+
* first, then the nearest ancestor that actually scrolls (robust to host
|
|
11
|
+
* markup).
|
|
12
|
+
*/
|
|
13
|
+
export function scrollContainerFor(element) {
|
|
14
|
+
// Assumes the two known ids are never nested inside one another; neither
|
|
15
|
+
// known host does. If that ever changes, the closer one should win instead.
|
|
16
|
+
const byId = element.closest('#editor-scroll-container') ||
|
|
17
|
+
element.closest('#editor-panel-scroll-container');
|
|
18
|
+
if (byId)
|
|
19
|
+
return byId;
|
|
20
|
+
let el = element.parentElement;
|
|
21
|
+
while (el) {
|
|
22
|
+
const overflowY = getComputedStyle(el).overflowY;
|
|
23
|
+
if (/(auto|scroll)/.test(overflowY) && el.scrollHeight > el.clientHeight)
|
|
24
|
+
return el;
|
|
25
|
+
el = el.parentElement;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Scroll `target` into view within its scroll container (or the viewport, if
|
|
31
|
+
* none is found), positioning it near the top rather than flush against it.
|
|
32
|
+
*/
|
|
33
|
+
export function scrollElementIntoView(target, margin = 16) {
|
|
34
|
+
const container = scrollContainerFor(target);
|
|
35
|
+
if (container) {
|
|
36
|
+
// rect-based offset is robust regardless of the target's offsetParent;
|
|
37
|
+
// leave a small margin so it sits near the top, not flush against it.
|
|
38
|
+
const top = Math.max(0, target.getBoundingClientRect().top -
|
|
39
|
+
container.getBoundingClientRect().top +
|
|
40
|
+
container.scrollTop -
|
|
41
|
+
margin);
|
|
42
|
+
container.scrollTo({ top, behavior: 'smooth' });
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
46
|
+
}
|
|
47
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ export type { CursorState } from './app/App.js';
|
|
|
18
18
|
export { Editor } from './app/editor/index.js';
|
|
19
19
|
export type { SweepFn } from './app/editor/AmbientCorrectionPlugin.js';
|
|
20
20
|
export type { SelectionContextMenuEvent } from './app/editor/SelectionContextMenuPlugin.js';
|
|
21
|
+
export { DocumentOutline } from './app/editor/DocumentOutline.js';
|
|
22
|
+
export type { DocumentOutlineProps } from './app/editor/DocumentOutline.js';
|
|
23
|
+
export { createDocumentOutlineHandle } from './app/editor/documentOutlineHandle.js';
|
|
24
|
+
export type { DocumentOutlineHandle, DocumentOutlineSnapshot, OutlineEntry, } from './app/editor/documentOutlineHandle.js';
|
|
21
25
|
export { OPEN_ANNOTATION_COMPOSER_COMMAND } from './app/editor/annotationCommands.js';
|
|
22
26
|
export type { AnnotationCreateEvent } from './app/editor/AnnotationPlugin.js';
|
|
23
27
|
export type { Annotation, AnnotationKind, AnnotationKindConfig, AnnotationKindConfigs, AnnotationCreateAffordance, AnnotationMarkerStyle, AnnotationPresentation, AnnotationEditorHandle, MarkerTarget, } from './annotations/types.js';
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,12 @@ export { createHostMessageApi, useHostMessages } from './host/messages.js';
|
|
|
15
15
|
// --- Components ------------------------------------------------------------
|
|
16
16
|
export { App } from './app/App.js';
|
|
17
17
|
export { Editor } from './app/editor/index.js';
|
|
18
|
+
// --- Document outline (issue #69) -------------------------------------------
|
|
19
|
+
// `createDocumentOutlineHandle()` makes the controller shared between one
|
|
20
|
+
// `<Editor documentOutlineHandle={…}>` and one `<DocumentOutline handle={…}>`
|
|
21
|
+
// — create it once per editor instance and pass the same object to both.
|
|
22
|
+
export { DocumentOutline } from './app/editor/DocumentOutline.js';
|
|
23
|
+
export { createDocumentOutlineHandle } from './app/editor/documentOutlineHandle.js';
|
|
18
24
|
// --- Annotations, React surface (ADR-077) ---------------------------------
|
|
19
25
|
// The kind-configuration types a host needs to turn the mechanism on, plus the
|
|
20
26
|
// create-event shape. The DOM-free anchor model, resolver and marker-target
|
|
@@ -230,6 +230,65 @@ function normalizeWikiLinkNodes(node) {
|
|
|
230
230
|
}
|
|
231
231
|
return node;
|
|
232
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Widen every GFM table delimiter-row cell mdast-util-to-markdown emits at
|
|
235
|
+
* its library-enforced minimum (a bare `-`, or `:-`, `-:`, `:-:` with
|
|
236
|
+
* alignment colons) up to the conventional three-hyphen form GFM,
|
|
237
|
+
* remark-stringify, and Prettier all produce (`---`, `:---`, `---:`,
|
|
238
|
+
* `:---:`). `tablePipeAlign: false` (see the gfmToMarkdown() call below) is
|
|
239
|
+
* the one option controlling both column-width padding and this dash-count
|
|
240
|
+
* minimum in markdown-table — there is no way to keep the former's "no
|
|
241
|
+
* cell-width padding" behavior while asking for the latter's wider default
|
|
242
|
+
* separately, so this text-level post-process patches it after the fact
|
|
243
|
+
* (#60).
|
|
244
|
+
*
|
|
245
|
+
* A GFM table's delimiter row is always exactly the *second* line of a
|
|
246
|
+
* contiguous run of pipe-led lines (the first is the header row). Widening
|
|
247
|
+
* is restricted to that position rather than to any line that merely looks
|
|
248
|
+
* delimiter-shaped — a body row whose cells' entire content happens to be a
|
|
249
|
+
* bare `-` (e.g. `| - | - |`) renders identically to a delimiter row, and a
|
|
250
|
+
* shape-only match would silently rewrite that user content instead of
|
|
251
|
+
* leaving it alone (caught in review — see PR #61). Only the exact
|
|
252
|
+
* single-dash form the library deterministically emits matches at that
|
|
253
|
+
* position, so an already-conventional delimiter row (3+ dashes) is left
|
|
254
|
+
* untouched — idempotence for free. Lines inside fenced code blocks or `$$`
|
|
255
|
+
* math blocks are excluded from the pipe-line run entirely, mirroring the
|
|
256
|
+
* intraword-underscore post-process below, so a fenced block that happens
|
|
257
|
+
* to contain a line shaped like a delimiter row is not rewritten. The
|
|
258
|
+
* pipe-line check also tolerates a leading run of `>` blockquote markers
|
|
259
|
+
* (each optionally preceded/followed by spaces or tabs) before the opening
|
|
260
|
+
* pipe, since a table nested in a blockquote (or a blockquote-wrapped list)
|
|
261
|
+
* serializes with a `> ` prefix on every line.
|
|
262
|
+
*/
|
|
263
|
+
function widenTableDelimiterDashes(markdown) {
|
|
264
|
+
const protectedRegionPattern = /^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1[ \t]*$|\$\$[\s\S]*?\$\$/gmu;
|
|
265
|
+
const protectedRanges = [];
|
|
266
|
+
let region;
|
|
267
|
+
while ((region = protectedRegionPattern.exec(markdown)) !== null) {
|
|
268
|
+
protectedRanges.push([region.index, region.index + region[0].length]);
|
|
269
|
+
}
|
|
270
|
+
const pipeLinePattern = /^(?:[ \t]*>)*[ \t]*\|/;
|
|
271
|
+
const delimiterRowPattern = /^(?:[ \t]*>)*[ \t]*\|(?:\s*:?-:?\s*\|)+[ \t]*$/;
|
|
272
|
+
const lines = markdown.split('\n');
|
|
273
|
+
let offset = 0;
|
|
274
|
+
let pipeRunLength = 0;
|
|
275
|
+
for (let i = 0; i < lines.length; i++) {
|
|
276
|
+
const line = lines[i];
|
|
277
|
+
const lineStart = offset;
|
|
278
|
+
const lineEnd = lineStart + line.length;
|
|
279
|
+
offset = lineEnd + 1; // account for the '\n' consumed by split()
|
|
280
|
+
const inProtectedRegion = protectedRanges.some(([start, end]) => lineStart < end && lineEnd > start);
|
|
281
|
+
if (inProtectedRegion || !pipeLinePattern.test(line)) {
|
|
282
|
+
pipeRunLength = 0;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
pipeRunLength += 1;
|
|
286
|
+
if (pipeRunLength === 2 && delimiterRowPattern.test(line)) {
|
|
287
|
+
lines[i] = line.replace(/-/g, '---');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return lines.join('\n');
|
|
291
|
+
}
|
|
233
292
|
export function stringifyMarkdown(root, options = {}) {
|
|
234
293
|
// Pre-process: add checkbox text to ordered list items (GFM only outputs for unordered)
|
|
235
294
|
let processedRoot = addCheckboxTextToOrderedLists(root);
|
|
@@ -330,6 +389,12 @@ export function stringifyMarkdown(root, options = {}) {
|
|
|
330
389
|
},
|
|
331
390
|
],
|
|
332
391
|
});
|
|
392
|
+
// Post-process: widen table delimiter-row dashes to the conventional
|
|
393
|
+
// three-hyphen width (#60). Must run first, before any of the other
|
|
394
|
+
// backslash-stripping/restoring post-processes below, since none of them
|
|
395
|
+
// touch dashes and this step's own verbatim-region skip logic is simplest
|
|
396
|
+
// to reason about against toMarkdown()'s raw output.
|
|
397
|
+
result = widenTableDelimiterDashes(result);
|
|
333
398
|
// Post-process: convert escaped spaces back to regular spaces
|
|
334
399
|
// mdast-util-to-markdown escapes leading/trailing spaces at paragraph
|
|
335
400
|
// boundaries as   which looks ugly in raw markdown
|