@synclineapi/mdx-editor 0.1.2 → 1.0.1
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 +403 -139
- package/dist/core/config/EditorConfig.d.ts +5 -0
- package/dist/core/config/defaults.d.ts +33 -0
- package/dist/core/config/index.d.ts +2 -0
- package/dist/core/dom/editor-pane.d.ts +7 -0
- package/dist/core/dom/editor-root.d.ts +8 -0
- package/dist/core/dom/index.d.ts +11 -0
- package/dist/core/dom/preview-pane.d.ts +5 -0
- package/dist/core/dom/skip-link.d.ts +1 -0
- package/dist/core/dom/status-bar.d.ts +13 -0
- package/dist/core/dom/toolbar-dom.d.ts +5 -0
- package/dist/core/editor.d.ts +139 -7
- package/dist/core/mdx-themes.d.ts +3 -0
- package/dist/core/platform.d.ts +31 -0
- package/dist/core/plugin-manager.d.ts +1 -24
- package/dist/core/plugins/PluginContext.d.ts +26 -0
- package/dist/core/plugins/PluginManager.d.ts +41 -0
- package/dist/core/plugins/index.d.ts +3 -0
- package/dist/core/renderer/CodeRenderer.d.ts +4 -0
- package/dist/core/renderer/ListRenderer.d.ts +5 -0
- package/dist/core/renderer/MarkdownRenderer.d.ts +7 -0
- package/dist/core/renderer/MdxValidator.d.ts +3 -0
- package/dist/core/renderer/Renderer.d.ts +10 -0
- package/dist/core/renderer/TableRenderer.d.ts +4 -0
- package/dist/core/renderer/index.d.ts +7 -0
- package/dist/core/renderer/sanitize.d.ts +1 -0
- package/dist/core/renderer.d.ts +6 -31
- package/dist/core/toolbar.d.ts +1 -0
- package/dist/core/types.d.ts +150 -2
- package/dist/core/ui/AutocompleteController.d.ts +58 -0
- package/dist/core/ui/DropdownController.d.ts +8 -0
- package/dist/core/ui/FindReplaceController.d.ts +60 -0
- package/dist/core/ui/LineNumberController.d.ts +218 -0
- package/dist/core/ui/ModeController.d.ts +9 -0
- package/dist/core/ui/PreviewController.d.ts +13 -0
- package/dist/core/ui/ResponsiveController.d.ts +30 -0
- package/dist/core/ui/SplitterController.d.ts +31 -0
- package/dist/core/ui/StatusBarController.d.ts +16 -0
- package/dist/core/ui/ThemeController.d.ts +8 -0
- package/dist/core/ui/ToolbarController.d.ts +10 -0
- package/dist/core/ui/UIController.d.ts +43 -0
- package/dist/core/ui/index.d.ts +12 -0
- package/dist/index.d.ts +29 -1
- package/dist/plugins/basic-formatting/index.d.ts +1 -0
- package/dist/plugins/callouts/index.d.ts +1 -0
- package/dist/plugins/diagrams/index.d.ts +1 -0
- package/dist/plugins/index.d.ts +1 -11
- package/dist/plugins/layout/index.d.ts +3 -0
- package/dist/plugins/lists/index.d.ts +1 -0
- package/dist/plugins/media/index.d.ts +2 -0
- package/dist/plugins/token-utils.d.ts +49 -0
- package/dist/plugins/utilities/index.d.ts +5 -0
- package/dist/style.css +1 -1
- package/dist/syncline-mdx-editor.js +9628 -1835
- package/dist/syncline-mdx-editor.umd.cjs +988 -90
- package/package.json +5 -4
package/dist/core/types.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { CompletionItem, CompletionKind, TokenSegment } from '@synclineapi/editor';
|
|
2
|
+
export type { CompletionItem, CompletionKind, TokenSegment };
|
|
3
|
+
/** Function signature for a plugin-level syntax token provider. */
|
|
4
|
+
export type PluginTokenProvider = (line: string, language: string) => TokenSegment[];
|
|
1
5
|
export interface EditorConfig {
|
|
2
6
|
container: HTMLElement | string;
|
|
3
7
|
/** Initial markdown content */
|
|
@@ -14,8 +18,6 @@ export interface EditorConfig {
|
|
|
14
18
|
placeholder?: string;
|
|
15
19
|
/** Read-only mode */
|
|
16
20
|
readOnly?: boolean;
|
|
17
|
-
/** Enable scroll sync between editor and preview */
|
|
18
|
-
scrollSync?: boolean;
|
|
19
21
|
/** Custom renderer overrides */
|
|
20
22
|
renderers?: Record<string, RendererFn>;
|
|
21
23
|
/** Locale / i18n overrides */
|
|
@@ -47,6 +49,59 @@ export interface EditorPlugin {
|
|
|
47
49
|
styles?: string;
|
|
48
50
|
/** Plugin dependencies (names) */
|
|
49
51
|
dependencies?: string[];
|
|
52
|
+
/**
|
|
53
|
+
* Autocomplete items this plugin contributes to the code editor.
|
|
54
|
+
*
|
|
55
|
+
* These are merged with the built-in MDX completions so that anything
|
|
56
|
+
* a plugin can insert via the toolbar is also reachable via autocomplete.
|
|
57
|
+
*
|
|
58
|
+
* Use `kind: 'snip'` with a `body` template to get Tab-trigger snippet
|
|
59
|
+
* expansion that matches the toolbar insertion exactly.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* completions: [
|
|
64
|
+
* {
|
|
65
|
+
* label: 'myblock',
|
|
66
|
+
* kind: 'snip',
|
|
67
|
+
* detail: '<MyBlock> component',
|
|
68
|
+
* description: 'Inserts a MyBlock component with optional title.',
|
|
69
|
+
* body: '<MyBlock title="$1Title">\n $2Content here\n</MyBlock>',
|
|
70
|
+
* },
|
|
71
|
+
* ]
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
completions?: CompletionItem[];
|
|
75
|
+
/**
|
|
76
|
+
* Custom syntax token provider for this plugin's MDX constructs.
|
|
77
|
+
*
|
|
78
|
+
* Called on every visible line in the code editor **in addition to** the
|
|
79
|
+
* built-in MDX tokeniser. Return `TokenSegment` objects for the character
|
|
80
|
+
* ranges you want coloured. Ranges not covered fall back to the base MDX
|
|
81
|
+
* tokeniser.
|
|
82
|
+
*
|
|
83
|
+
* All plugin providers are composed into a single `provideTokens` function
|
|
84
|
+
* that is passed to the underlying `SynclineEditor`. The result is LRU-
|
|
85
|
+
* cached by the editor, so the function is only called once per unique
|
|
86
|
+
* `(text, language)` pair.
|
|
87
|
+
*
|
|
88
|
+
* Use `ctx.registerTokenProvider()` inside `init()` when the segments
|
|
89
|
+
* need to be computed dynamically (e.g. based on runtime config).
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* provideTokens: (line, _lang) => {
|
|
94
|
+
* const segs: TokenSegment[] = [];
|
|
95
|
+
* // Highlight <MyBlock component name
|
|
96
|
+
* for (const m of line.matchAll(/<\/?( MyBlock)(?=[\s>/])/g)) {
|
|
97
|
+
* const s = m.index! + 1 + (m[0][1] === '/' ? 1 : 0);
|
|
98
|
+
* segs.push({ cls: 'cls', start: s, end: s + 'MyBlock'.length });
|
|
99
|
+
* }
|
|
100
|
+
* return segs;
|
|
101
|
+
* }
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
provideTokens?: PluginTokenProvider;
|
|
50
105
|
}
|
|
51
106
|
export interface PluginContext {
|
|
52
107
|
/** The editor instance */
|
|
@@ -61,6 +116,43 @@ export interface PluginContext {
|
|
|
61
116
|
registerParser(parser: ParserConfig): void;
|
|
62
117
|
/** Inject CSS */
|
|
63
118
|
injectStyles(css: string): void;
|
|
119
|
+
/**
|
|
120
|
+
* Register an autocomplete completion item for the code editor.
|
|
121
|
+
* Equivalent to declaring `completions` on the plugin definition, but
|
|
122
|
+
* useful when the items are built dynamically inside `init()`.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```ts
|
|
126
|
+
* ctx.registerCompletion({
|
|
127
|
+
* label: 'myblock',
|
|
128
|
+
* kind: 'snip',
|
|
129
|
+
* detail: '<MyBlock> component',
|
|
130
|
+
* body: '<MyBlock title="$1">\n $2\n</MyBlock>',
|
|
131
|
+
* });
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
registerCompletion(item: CompletionItem): void;
|
|
135
|
+
/**
|
|
136
|
+
* Register a dynamic token provider for this plugin's MDX syntax.
|
|
137
|
+
* Equivalent to declaring `provideTokens` on the plugin definition but
|
|
138
|
+
* useful when the provider needs to be built dynamically inside `init()`.
|
|
139
|
+
*
|
|
140
|
+
* Multiple providers registered from the same plugin are all called and
|
|
141
|
+
* their results merged into the final token list.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```ts
|
|
145
|
+
* ctx.registerTokenProvider((line, _lang) => {
|
|
146
|
+
* const segs: TokenSegment[] = [];
|
|
147
|
+
* for (const m of line.matchAll(/<\/?( MyBlock)(?=[\s>/])/g)) {
|
|
148
|
+
* const s = m.index! + 1 + (m[0][1] === '/' ? 1 : 0);
|
|
149
|
+
* segs.push({ cls: 'cls', start: s, end: s + 'MyBlock'.length });
|
|
150
|
+
* }
|
|
151
|
+
* return segs;
|
|
152
|
+
* });
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
registerTokenProvider(fn: PluginTokenProvider): void;
|
|
64
156
|
/** Emit a plugin event */
|
|
65
157
|
emit(event: string, data?: unknown): void;
|
|
66
158
|
/** Listen to events */
|
|
@@ -166,6 +258,62 @@ export interface EditorAPI {
|
|
|
166
258
|
getWordCount(): number;
|
|
167
259
|
/** Get line count */
|
|
168
260
|
getLineCount(): number;
|
|
261
|
+
/** Show or hide the line-number gutter (virtual rendering) */
|
|
262
|
+
setLineNumbers(enabled: boolean): void;
|
|
263
|
+
/** Scroll the editor to make the given 1-indexed line visible */
|
|
264
|
+
jumpToLine(lineNumber: number): void;
|
|
265
|
+
/**
|
|
266
|
+
* Register one or more custom autocomplete items on top of the built-in
|
|
267
|
+
* MDX completions. Items are merged immediately and appear in the popup
|
|
268
|
+
* the next time the user types.
|
|
269
|
+
*
|
|
270
|
+
* Use `kind: 'snip'` with a `body` template for snippet expansion — the
|
|
271
|
+
* Tab key or clicking the item inserts the full body with cursor placed
|
|
272
|
+
* at the first `$1` tab stop.
|
|
273
|
+
*
|
|
274
|
+
* Call multiple times to accumulate items. The full list (built-ins +
|
|
275
|
+
* plugin items + items registered here) is always deduplicated by label.
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* ```ts
|
|
279
|
+
* editor.registerAutoComplete([
|
|
280
|
+
* {
|
|
281
|
+
* label: 'mycard',
|
|
282
|
+
* kind: 'snip',
|
|
283
|
+
* detail: '<MyCard> component',
|
|
284
|
+
* description: 'Inserts a custom card block.',
|
|
285
|
+
* body: '<MyCard title="$1">\n $2\n</MyCard>',
|
|
286
|
+
* },
|
|
287
|
+
* { label: 'MyCard', kind: 'cls', detail: 'component' },
|
|
288
|
+
* ]);
|
|
289
|
+
* ```
|
|
290
|
+
*/
|
|
291
|
+
registerAutoComplete(items: CompletionItem | CompletionItem[]): void;
|
|
292
|
+
/**
|
|
293
|
+
* Register one or more custom syntax token providers on top of the
|
|
294
|
+
* built-in MDX tokeniser and any plugin-contributed providers.
|
|
295
|
+
*
|
|
296
|
+
* Each provider is called on every visible line in the code editor and
|
|
297
|
+
* may return `TokenSegment` objects covering the character ranges that
|
|
298
|
+
* should receive custom syntax colouring. The result is LRU-cached by
|
|
299
|
+
* the underlying `SynclineEditor`.
|
|
300
|
+
*
|
|
301
|
+
* Equivalent to declaring `provideTokens` on a custom `EditorPlugin` —
|
|
302
|
+
* use whichever is more convenient.
|
|
303
|
+
*
|
|
304
|
+
* @example
|
|
305
|
+
* ```ts
|
|
306
|
+
* editor.registerSyntaxHighlighter((line, _lang) => {
|
|
307
|
+
* const segs: TokenSegment[] = [];
|
|
308
|
+
* for (const m of line.matchAll(/<\/?( MyBlock)(?=[\s>/])/g)) {
|
|
309
|
+
* const s = m.index! + 1 + (m[0][1] === '/' ? 1 : 0);
|
|
310
|
+
* segs.push({ cls: 'cls', start: s, end: s + 'MyBlock'.length });
|
|
311
|
+
* }
|
|
312
|
+
* return segs;
|
|
313
|
+
* });
|
|
314
|
+
* ```
|
|
315
|
+
*/
|
|
316
|
+
registerSyntaxHighlighter(fn: PluginTokenProvider | PluginTokenProvider[]): void;
|
|
169
317
|
}
|
|
170
318
|
export interface SelectionState {
|
|
171
319
|
start: number;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AutocompleteController — MDX/Markdown-aware autocomplete popup.
|
|
3
|
+
*
|
|
4
|
+
* Adapted from the vreditor.html autocomplete system. Shows suggestions for:
|
|
5
|
+
* - MDX component tags (Accordion, Card, Tabs, etc.)
|
|
6
|
+
* - Markdown syntax (headings, bold, lists, code blocks, etc.)
|
|
7
|
+
* - HTML tags
|
|
8
|
+
* - Document-based word completions (words already present in the editor)
|
|
9
|
+
*
|
|
10
|
+
* The popup is positioned relative to the cursor and supports keyboard
|
|
11
|
+
* navigation (Arrow Up/Down, Tab/Enter to accept, Escape to dismiss).
|
|
12
|
+
*/
|
|
13
|
+
/** A single autocomplete suggestion. */
|
|
14
|
+
export interface AutocompleteItem {
|
|
15
|
+
/** Display label shown in the popup */
|
|
16
|
+
label: string;
|
|
17
|
+
/** Category kind — drives the badge icon */
|
|
18
|
+
kind: 'tag' | 'md' | 'html' | 'var' | 'snippet';
|
|
19
|
+
/** Short description shown next to label */
|
|
20
|
+
detail: string;
|
|
21
|
+
/** Text to insert (defaults to label if omitted) */
|
|
22
|
+
insertText?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare class AutocompleteController {
|
|
25
|
+
private popup;
|
|
26
|
+
private items;
|
|
27
|
+
private selected;
|
|
28
|
+
private prefix;
|
|
29
|
+
private startPos;
|
|
30
|
+
private textarea;
|
|
31
|
+
private editorRoot;
|
|
32
|
+
private _onAccept;
|
|
33
|
+
/** Additional items registered at runtime by plugins. */
|
|
34
|
+
private extraItems;
|
|
35
|
+
constructor(textarea: HTMLTextAreaElement, editorRoot: HTMLElement);
|
|
36
|
+
/** Register additional completion items (e.g. from plugins). */
|
|
37
|
+
registerItems(items: AutocompleteItem[]): void;
|
|
38
|
+
/** Whether the popup is currently visible. */
|
|
39
|
+
isVisible(): boolean;
|
|
40
|
+
/** Trigger autocomplete based on current cursor context. */
|
|
41
|
+
trigger(): void;
|
|
42
|
+
/** Move selection up or down. */
|
|
43
|
+
move(dir: number): void;
|
|
44
|
+
/** Accept the current selection. */
|
|
45
|
+
accept(): void;
|
|
46
|
+
/** Set a callback to fire after an item is accepted (for triggering onInput). */
|
|
47
|
+
onAccept(fn: () => void): void;
|
|
48
|
+
/** Handle keyboard events. Returns true if the event was consumed. */
|
|
49
|
+
handleKeyDown(e: KeyboardEvent): boolean;
|
|
50
|
+
/** Hide the autocomplete popup. */
|
|
51
|
+
hide(): void;
|
|
52
|
+
/** Render the popup items. */
|
|
53
|
+
private render;
|
|
54
|
+
/** Position the popup near the cursor. */
|
|
55
|
+
private position;
|
|
56
|
+
/** Clean up DOM. */
|
|
57
|
+
destroy(): void;
|
|
58
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FindReplaceController — Find & Replace panel for the editor.
|
|
3
|
+
*
|
|
4
|
+
* Adapted from the vreditor.html find/replace system. Provides:
|
|
5
|
+
* - Ctrl+F to open find, Ctrl+H to open find+replace
|
|
6
|
+
* - Case-sensitive toggle
|
|
7
|
+
* - Regex search toggle
|
|
8
|
+
* - Whole-word match toggle
|
|
9
|
+
* - Navigate between matches (Enter / Shift+Enter or arrows)
|
|
10
|
+
* - Replace current / Replace all
|
|
11
|
+
* - Escape to close
|
|
12
|
+
*/
|
|
13
|
+
export declare class FindReplaceController {
|
|
14
|
+
private panel;
|
|
15
|
+
private findInput;
|
|
16
|
+
private replaceInput;
|
|
17
|
+
private replaceRow;
|
|
18
|
+
private countLabel;
|
|
19
|
+
private caseSensitiveBtn;
|
|
20
|
+
private regexBtn;
|
|
21
|
+
private wholeWordBtn;
|
|
22
|
+
private textarea;
|
|
23
|
+
private editorRoot;
|
|
24
|
+
private matches;
|
|
25
|
+
private currentMatch;
|
|
26
|
+
private caseSensitive;
|
|
27
|
+
private useRegex;
|
|
28
|
+
private wholeWord;
|
|
29
|
+
private showReplace;
|
|
30
|
+
private _onInputCallback;
|
|
31
|
+
constructor(textarea: HTMLTextAreaElement, editorRoot: HTMLElement);
|
|
32
|
+
/** Set a callback to fire after a replace operation (for triggering onInput). */
|
|
33
|
+
onInput(fn: () => void): void;
|
|
34
|
+
/** Open the find panel. If `withReplace` is true, also show the replace row. */
|
|
35
|
+
open(withReplace?: boolean): void;
|
|
36
|
+
/** Close the find panel. */
|
|
37
|
+
close(): void;
|
|
38
|
+
/** Whether the panel is open. */
|
|
39
|
+
isOpen(): boolean;
|
|
40
|
+
/** Run the search. */
|
|
41
|
+
private search;
|
|
42
|
+
/** Go to next match. */
|
|
43
|
+
private next;
|
|
44
|
+
/** Go to previous match. */
|
|
45
|
+
private prev;
|
|
46
|
+
/** Select the current match in the textarea. */
|
|
47
|
+
private highlightCurrent;
|
|
48
|
+
/** Update the match count label. */
|
|
49
|
+
private updateCount;
|
|
50
|
+
/** Replace the current match. */
|
|
51
|
+
private replaceCurrent;
|
|
52
|
+
/** Replace all matches. */
|
|
53
|
+
private replaceAll;
|
|
54
|
+
/** Create a toggle button. */
|
|
55
|
+
private createToggle;
|
|
56
|
+
/** Create an action button. */
|
|
57
|
+
private createActionBtn;
|
|
58
|
+
/** Clean up. */
|
|
59
|
+
destroy(): void;
|
|
60
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Virtual line-number gutter — unified scroll container approach.
|
|
3
|
+
*
|
|
4
|
+
* Only ~(visible lines + 2×OVERSCAN) DOM nodes live in the DOM at any time,
|
|
5
|
+
* regardless of document size. For a 10,000-line file only ~40 <span> elements
|
|
6
|
+
* are ever created; a 100-line file and a 10,000-line file feel identical.
|
|
7
|
+
*
|
|
8
|
+
* Layout model (unified scroll container):
|
|
9
|
+
*
|
|
10
|
+
* .smdx-scroll-wrapper overflow-y:auto — SINGLE scroll container
|
|
11
|
+
* ├─ .smdx-line-gutter flex-shrink:0, position:relative
|
|
12
|
+
* │ └─ .smdx-gutter-inner position:relative, height = totalHeight
|
|
13
|
+
* │ └─ <span> position:absolute, top = lineOffset + paddingTop
|
|
14
|
+
* └─ <textarea> flex:1, overflow:hidden, height auto-sized
|
|
15
|
+
*
|
|
16
|
+
* The gutter and textarea are flex siblings inside a shared scroll wrapper.
|
|
17
|
+
* When the wrapper scrolls, both children scroll together naturally — like
|
|
18
|
+
* a flex row per line. No JS-based transform synchronisation is needed.
|
|
19
|
+
*
|
|
20
|
+
* Virtual rendering:
|
|
21
|
+
* • recyclePool() computes the visible range from the scroll wrapper's
|
|
22
|
+
* scrollTop and clientHeight.
|
|
23
|
+
* • If the range hasn't changed (OVERSCAN has buffered all visible lines),
|
|
24
|
+
* it returns immediately — zero DOM reads, zero DOM writes.
|
|
25
|
+
* • Only when lines enter or leave the overscan region are a handful of
|
|
26
|
+
* span properties updated (style.top + textContent).
|
|
27
|
+
*
|
|
28
|
+
* Multi-line wrapping:
|
|
29
|
+
* • For each logical line, rowCount() measures how many visual rows it takes
|
|
30
|
+
* using a hidden mirror div with identical font/width settings.
|
|
31
|
+
* • heights[i] = rowCount(i) × lineHeightPx (full multi-row height stored).
|
|
32
|
+
* • The line-number span for line i is only 1 row tall — pinned to the first
|
|
33
|
+
* visual row (VS Code convention). offsets[] accounts for the full height
|
|
34
|
+
* so successive numbers are correctly spaced.
|
|
35
|
+
*/
|
|
36
|
+
/** Public snapshot of the gutter's virtual-rendering state. */
|
|
37
|
+
export interface GutterStats {
|
|
38
|
+
/** Number of logical lines in the document. */
|
|
39
|
+
lineCount: number;
|
|
40
|
+
/** 1-indexed first line currently in the DOM pool. */
|
|
41
|
+
visibleFirst: number;
|
|
42
|
+
/** 1-indexed last line currently in the DOM pool. */
|
|
43
|
+
visibleLast: number;
|
|
44
|
+
/** Number of <span> elements currently in the DOM. */
|
|
45
|
+
domNodes: number;
|
|
46
|
+
/** Computed total content height (px). */
|
|
47
|
+
totalHeight: number;
|
|
48
|
+
/** Whether the gutter / virtual renderer is enabled. */
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
}
|
|
51
|
+
export declare class LineNumberController {
|
|
52
|
+
private gutter;
|
|
53
|
+
private textarea;
|
|
54
|
+
/** The shared scroll wrapper (parent of gutter + textarea). */
|
|
55
|
+
private scrollEl;
|
|
56
|
+
private enabled;
|
|
57
|
+
private mirror;
|
|
58
|
+
private rafPending;
|
|
59
|
+
private scrollRafId;
|
|
60
|
+
private offsets;
|
|
61
|
+
private heights;
|
|
62
|
+
private totalHeight;
|
|
63
|
+
private lineCount;
|
|
64
|
+
private lineHeightPx;
|
|
65
|
+
private contentWidth;
|
|
66
|
+
private avgCharWidth;
|
|
67
|
+
private paddingTop;
|
|
68
|
+
private cachedClientHeight;
|
|
69
|
+
private static readonly WRAP_THRESHOLD;
|
|
70
|
+
/**
|
|
71
|
+
* Documents above this line count skip per-line mirror-div measurement
|
|
72
|
+
* in the synchronous path and use pure character-width estimation instead.
|
|
73
|
+
* This keeps large pastes (2000+ lines) instant while keeping the gutter
|
|
74
|
+
* correct for typical editing sessions.
|
|
75
|
+
*/
|
|
76
|
+
private static readonly LARGE_DOC_THRESHOLD;
|
|
77
|
+
/**
|
|
78
|
+
* Delay (ms) before running the deferred precision pass for large documents.
|
|
79
|
+
* Short enough to feel immediate, long enough to let paste events settle.
|
|
80
|
+
*/
|
|
81
|
+
private static readonly DEFERRED_UPDATE_DELAY_MS;
|
|
82
|
+
/**
|
|
83
|
+
* Minimum content-width change (px) to trigger a full geometry recompute
|
|
84
|
+
* on resize. Smaller changes are ignored to avoid unnecessary recalculation.
|
|
85
|
+
*/
|
|
86
|
+
private static readonly WIDTH_CHANGE_THRESHOLD_PX;
|
|
87
|
+
/**
|
|
88
|
+
* Debounce timer for deferring heavy mirror-div measurement on large
|
|
89
|
+
* content changes (paste, setValue with 2000+ lines).
|
|
90
|
+
*/
|
|
91
|
+
private deferredUpdateTimer;
|
|
92
|
+
/**
|
|
93
|
+
* Cached content width used to detect width changes on resize so we
|
|
94
|
+
* can trigger a full geometry recompute (wrapping changes).
|
|
95
|
+
*/
|
|
96
|
+
private cachedContentWidth;
|
|
97
|
+
/** Cached textarea scrollHeight for optimised autoResizeTextarea(). */
|
|
98
|
+
private lastTextareaHeight;
|
|
99
|
+
private inner;
|
|
100
|
+
/**
|
|
101
|
+
* OVERSCAN — extra lines rendered above and below the visible area.
|
|
102
|
+
* 8 gives a comfortable buffer for fast momentum scrolling; the span-recycling
|
|
103
|
+
* approach (no DOM teardown) means a larger overscan has negligible cost.
|
|
104
|
+
*/
|
|
105
|
+
private readonly OVERSCAN;
|
|
106
|
+
private spanPool;
|
|
107
|
+
private poolStart;
|
|
108
|
+
private poolEnd;
|
|
109
|
+
/** Called after every pool rebuild — use to drive live status-bar stats. */
|
|
110
|
+
onViewportChange: ((stats: GutterStats) => void) | null;
|
|
111
|
+
constructor(gutter: HTMLElement, textarea: HTMLTextAreaElement);
|
|
112
|
+
/**
|
|
113
|
+
* Set the shared scroll wrapper element.
|
|
114
|
+
* Must be called before enable() so scroll events are bound to the wrapper.
|
|
115
|
+
*/
|
|
116
|
+
setScrollElement(el: HTMLElement): void;
|
|
117
|
+
private buildInner;
|
|
118
|
+
private setupMirror;
|
|
119
|
+
private destroyMirror;
|
|
120
|
+
/**
|
|
121
|
+
* Copy font metrics from the textarea into the mirror div.
|
|
122
|
+
* Returns computed layout values, or null when layout is unavailable
|
|
123
|
+
* (e.g. jsdom / hidden element).
|
|
124
|
+
*/
|
|
125
|
+
private syncMirror;
|
|
126
|
+
/**
|
|
127
|
+
* Return the number of visual rows occupied by `line`.
|
|
128
|
+
* Fast path: if the line clearly fits on one row (no wrap), return 1
|
|
129
|
+
* without touching the mirror div. Only wrapping lines pay the
|
|
130
|
+
* mirror-div measurement cost.
|
|
131
|
+
*/
|
|
132
|
+
private rowCount;
|
|
133
|
+
enable(): void;
|
|
134
|
+
disable(): void;
|
|
135
|
+
/**
|
|
136
|
+
* Refresh cached layout dimensions after the editor container is resized.
|
|
137
|
+
* Call this from a ResizeObserver on the editor pane or the scroll wrapper
|
|
138
|
+
* so that recyclePool() continues using accurate viewport dimensions
|
|
139
|
+
* without reading from the DOM on every scroll event.
|
|
140
|
+
*
|
|
141
|
+
* If the content width has changed (e.g. splitter drag, window resize),
|
|
142
|
+
* this triggers a full geometry recompute because wrapping may have changed.
|
|
143
|
+
*/
|
|
144
|
+
notifyResize(): void;
|
|
145
|
+
/**
|
|
146
|
+
* Full recompute: called whenever the document content changes.
|
|
147
|
+
* Measures each line's visual height and rebuilds the geometry cache,
|
|
148
|
+
* then re-renders the visible viewport.
|
|
149
|
+
*
|
|
150
|
+
* For large documents (> LARGE_DOC_THRESHOLD lines), this uses fast
|
|
151
|
+
* character-width estimation instead of per-line mirror-div measurement.
|
|
152
|
+
* A deferred rAF pass can refine wrapping estimates if needed.
|
|
153
|
+
*/
|
|
154
|
+
update(): void;
|
|
155
|
+
/**
|
|
156
|
+
* Deferred precision pass for large documents: uses the mirror div
|
|
157
|
+
* only for lines that likely wrap, then updates the geometry cache.
|
|
158
|
+
* Runs after a short delay so paste operations are not blocked.
|
|
159
|
+
*/
|
|
160
|
+
private refineLargeDoc;
|
|
161
|
+
/**
|
|
162
|
+
* Auto-resize the textarea so its full content is visible (no internal
|
|
163
|
+
* scrollbar). The shared scroll wrapper provides the single scrollbar
|
|
164
|
+
* for both gutter and textarea.
|
|
165
|
+
*
|
|
166
|
+
* Optimised: only resets height when the content has likely changed size,
|
|
167
|
+
* avoiding unnecessary layout thrashing on every keystroke.
|
|
168
|
+
*/
|
|
169
|
+
autoResizeTextarea(): void;
|
|
170
|
+
/**
|
|
171
|
+
* Recycle the span pool for the current viewport.
|
|
172
|
+
* Called after content changes and synchronously on every scroll event.
|
|
173
|
+
*/
|
|
174
|
+
private renderVisible;
|
|
175
|
+
/**
|
|
176
|
+
* Recycle the span pool for the new visible range.
|
|
177
|
+
*
|
|
178
|
+
* Spans are updated in-place (style.top + textContent) — zero DOM
|
|
179
|
+
* add/remove during scrolling. This runs SYNCHRONOUSLY in the scroll
|
|
180
|
+
* handler and maintains 60fps even for 10,000+ line documents.
|
|
181
|
+
*
|
|
182
|
+
* With the unified scroll wrapper the gutter and textarea share the
|
|
183
|
+
* same scrollTop so no scroll-fraction mapping is needed.
|
|
184
|
+
*/
|
|
185
|
+
private recyclePool;
|
|
186
|
+
/**
|
|
187
|
+
* Binary search: return the index of the logical line whose range
|
|
188
|
+
* contains or immediately precedes `offsetPx`.
|
|
189
|
+
*/
|
|
190
|
+
private lineIndexAtOffset;
|
|
191
|
+
/**
|
|
192
|
+
* Scroll handler — fully synchronous, no rAF deferral needed.
|
|
193
|
+
*
|
|
194
|
+
* With the unified scroll wrapper, this simply reads scrollTop from
|
|
195
|
+
* the wrapper and recycles the span pool. No transform writes are
|
|
196
|
+
* needed since both gutter and textarea scroll together naturally.
|
|
197
|
+
*/
|
|
198
|
+
private onScroll;
|
|
199
|
+
private cancelScrollRaf;
|
|
200
|
+
/**
|
|
201
|
+
* Ensure the cursor's line is visible within the scroll wrapper.
|
|
202
|
+
* Call after input, keyup, or any operation that may move the cursor.
|
|
203
|
+
*/
|
|
204
|
+
ensureCursorVisible(): void;
|
|
205
|
+
/**
|
|
206
|
+
* Return the content-space Y offset (px) for the given 0-indexed logical line.
|
|
207
|
+
* Used by the editor's jumpToLine() to scroll the textarea precisely.
|
|
208
|
+
*/
|
|
209
|
+
getLineOffset(lineIndex: number): number;
|
|
210
|
+
/**
|
|
211
|
+
* Return a snapshot of the virtual renderer's current state.
|
|
212
|
+
* Useful for live status-bar displays.
|
|
213
|
+
*/
|
|
214
|
+
getStats(): GutterStats;
|
|
215
|
+
/** Return the scroll wrapper element, if set. */
|
|
216
|
+
getScrollElement(): HTMLElement | null;
|
|
217
|
+
destroy(): void;
|
|
218
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { RendererConfig, ParserConfig } from '../types';
|
|
2
|
+
export declare class PreviewController {
|
|
3
|
+
private renderer;
|
|
4
|
+
private contentEl;
|
|
5
|
+
private renderTimer;
|
|
6
|
+
private delay;
|
|
7
|
+
constructor(contentEl: HTMLElement, delay?: number);
|
|
8
|
+
setRenderers(renderers: RendererConfig[]): void;
|
|
9
|
+
setParsers(parsers: ParserConfig[]): void;
|
|
10
|
+
render(source: string): void;
|
|
11
|
+
renderImmediate(source: string): Promise<void>;
|
|
12
|
+
destroy(): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResponsiveController
|
|
3
|
+
*
|
|
4
|
+
* Watches the editor root element's width via ResizeObserver and applies
|
|
5
|
+
* `smdx-is-mobile` / `smdx-is-tablet` CSS classes so that responsive
|
|
6
|
+
* styles work correctly for embedded editors whose width is independent
|
|
7
|
+
* of the viewport width.
|
|
8
|
+
*
|
|
9
|
+
* Breakpoints (mirrors the CSS @media queries in mobile.css):
|
|
10
|
+
* ≤ 640 px → smdx-is-mobile
|
|
11
|
+
* 641–1024 px → smdx-is-tablet
|
|
12
|
+
* > 1024 px → (no extra class)
|
|
13
|
+
*/
|
|
14
|
+
export declare class ResponsiveController {
|
|
15
|
+
private root;
|
|
16
|
+
private observer;
|
|
17
|
+
/** Breakpoint widths in pixels – must match mobile.css */
|
|
18
|
+
private static readonly MOBILE_BREAKPOINT;
|
|
19
|
+
private static readonly TABLET_BREAKPOINT;
|
|
20
|
+
constructor(root: HTMLElement);
|
|
21
|
+
/**
|
|
22
|
+
* Start observing. Call once after the editor root is in the DOM.
|
|
23
|
+
*/
|
|
24
|
+
start(): void;
|
|
25
|
+
/**
|
|
26
|
+
* Stop observing and clean up.
|
|
27
|
+
*/
|
|
28
|
+
destroy(): void;
|
|
29
|
+
private applyBreakpointClasses;
|
|
30
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare class SplitterController {
|
|
2
|
+
private splitter;
|
|
3
|
+
private left;
|
|
4
|
+
private right;
|
|
5
|
+
private container;
|
|
6
|
+
private isDragging;
|
|
7
|
+
private startX;
|
|
8
|
+
private startLeftWidth;
|
|
9
|
+
/** Percentage of container width to move per keyboard arrow press. */
|
|
10
|
+
private static readonly KEYBOARD_STEP_PERCENT;
|
|
11
|
+
/** Callback fired during drag so the editor can refresh line-number geometry. */
|
|
12
|
+
private onResizeCallback;
|
|
13
|
+
/** Throttle timer for resize callbacks during drag. */
|
|
14
|
+
private resizeRafId;
|
|
15
|
+
constructor(container: HTMLElement, left: HTMLElement, right: HTMLElement);
|
|
16
|
+
/** Set a callback to fire during drag (for refreshing line-number geometry). */
|
|
17
|
+
onResize(fn: () => void): void;
|
|
18
|
+
private onMouseDown;
|
|
19
|
+
private onMouseMove;
|
|
20
|
+
private onMouseUp;
|
|
21
|
+
private onTouchStart;
|
|
22
|
+
private onTouchMove;
|
|
23
|
+
private onTouchEnd;
|
|
24
|
+
private onKeyDown;
|
|
25
|
+
private startDrag;
|
|
26
|
+
private moveDrag;
|
|
27
|
+
private endDrag;
|
|
28
|
+
/** Throttled resize notification via rAF. */
|
|
29
|
+
private notifyResize;
|
|
30
|
+
destroy(): void;
|
|
31
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { EditorMode } from '../types';
|
|
2
|
+
export interface StatusBarControllerOptions {
|
|
3
|
+
bar: HTMLElement;
|
|
4
|
+
wordCount: HTMLElement;
|
|
5
|
+
lineCount: HTMLElement;
|
|
6
|
+
charCount: HTMLElement;
|
|
7
|
+
modeSwitcher: HTMLElement;
|
|
8
|
+
onModeChange: (mode: EditorMode) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare class StatusBarController {
|
|
11
|
+
private opts;
|
|
12
|
+
constructor(opts: StatusBarControllerOptions);
|
|
13
|
+
private buildModeSwitcher;
|
|
14
|
+
updateCounts(text: string): void;
|
|
15
|
+
updateActiveMode(mode: EditorMode): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ToolbarConfig, EditorAPI } from '../types';
|
|
2
|
+
import { PluginManager } from '../plugins/PluginManager';
|
|
3
|
+
import { EventEmitter } from '../events';
|
|
4
|
+
export declare class ToolbarController {
|
|
5
|
+
private toolbar;
|
|
6
|
+
constructor(config: ToolbarConfig, pluginManager: PluginManager, editorApi: EditorAPI, events: EventEmitter);
|
|
7
|
+
render(container: HTMLElement): HTMLElement;
|
|
8
|
+
updateActiveStates(): void;
|
|
9
|
+
destroy(): void;
|
|
10
|
+
}
|