@aardarch/aardink-web 0.5.0 → 0.6.0-rc2

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 CHANGED
@@ -1,86 +1,110 @@
1
- # @aardarch/aardink-web
2
-
3
- The [Aardink](https://github.com/aardarch/aardink) code editor for the browser: Compose
4
- Multiplatform compiled to WebAssembly, behind a small Monaco-shaped API. It covers syntax
5
- highlighting for Kotlin, TypeScript, JSON, TOML, XML, HTML, CSS and Markdown, plus folding,
6
- find and replace, diagnostics, undo/redo and six themes.
7
-
8
- ## Install
9
-
10
- ```sh
11
- pnpm add @aardarch/aardink-web # or: npm install @aardarch/aardink-web
12
- ```
13
-
14
- The package is plain ES modules plus two `.wasm` files and a font. It needs a bundler that
15
- understands `new URL('./file', import.meta.url)`, which Vite, webpack 5 and Rollup all do.
16
-
17
- ## Use
18
-
19
- ```ts
20
- import { createEditor, preloadAardink } from '@aardarch/aardink-web';
21
-
22
- preloadAardink(); // optional: start loading the WebAssembly module early
23
-
24
- const editor = await createEditor(document.getElementById('editor')!, (text) => save(text), {
25
- value: source,
26
- language: 'xml',
27
- theme: 'vscode-dark',
28
- wordWrap: 'on',
29
- });
30
-
31
- editor.updateOptions({ readOnly: true });
32
- editor.setDiagnostics([
33
- { line: 3, startColumn: 5, endColumn: 9, message: 'Unknown tag', severity: 'error' },
34
- ]);
35
- const stop = editor.onDidChangeCursor((line, column) => showPosition(line, column));
36
- editor.dispose();
37
- ```
38
-
39
- The editor fills its container, so give the container a size. `createEditor` is async because
40
- the first call loads the WebAssembly module (about 13 MB, 4.7 MB gzipped); every editor on the
41
- page shares that one module. See [`index.d.ts`](./index.d.ts) for the full API.
42
-
43
- ## Vite
44
-
45
- ```js
46
- // vite.config.js
47
- import { defineConfig } from 'vite';
48
-
49
- export default defineConfig({
50
- // The dev-server pre-bundler would move the package into .vite/deps and break the
51
- // new URL(..., import.meta.url) references the loader relies on.
52
- optimizeDeps: { exclude: ['@aardarch/aardink-web'] },
53
- build: { target: 'es2022', chunkSizeWarningLimit: 3500 },
54
- });
55
- ```
56
-
57
- `vite build` prints two harmless warnings about `node:fs` and `node:url` being externalised:
58
- the Kotlin loader has Node and Deno branches that never run in a browser. Serve `.wasm` with
59
- `Content-Type: application/wasm`.
60
-
61
- ## Browser support
62
-
63
- Chrome and Edge 119+, Firefox 120+, Safari 18.2+: the module needs WebAssembly garbage
64
- collection and exception handling.
65
-
66
- ## Known limitations
67
-
68
- Read these before adopting the package; the
69
- [integration guide](https://github.com/aardarch/aardink/blob/main/docs/WEB_INTEGRATION.md) has
70
- the measurements and the device checklist.
71
-
72
- - **Typing latency grows with document size, and fast typing can lose keys in large files.**
73
- Up to a few hundred lines of highlighted code it is comfortable. At 1,000 lines each keystroke
74
- keeps the editor busy for about half a second, and typing at 10 keys a second can drop around
75
- half the keys. At 5,000 lines each keystroke keeps it busy for several seconds. 0.5.x is for
76
- small files such as configuration or markup documents; a virtualised renderer that fixes this
77
- is planned for 0.6.0.
78
- - **A disposed editor is not fully released** (about 275 KB each), because Compose
79
- Multiplatform cannot yet tear down its viewport. Reuse one editor with `setValue` and
80
- `updateOptions` rather than mounting one per view.
81
- - There is no minimap, bracket-pair colouring or multi-cursor yet.
82
-
83
- ## Licence
84
-
85
- Apache-2.0. The bundled JetBrains Mono font is under the SIL Open Font License 1.1; see
86
- `JETBRAINS_MONO_OFL.txt`.
1
+ # @aardarch/aardink-web
2
+
3
+ The [Aardink](https://github.com/aardarch/aardink) code editor for the browser: Compose
4
+ Multiplatform compiled to WebAssembly, behind a small Monaco-shaped API. It highlights Kotlin,
5
+ TypeScript, JSON, TOML, XML, HTML, CSS and Markdown, and any language you register as a Monarch
6
+ grammar. It has folding, find and replace, diagnostics, undo and redo, multiple cursors and column
7
+ selection, bracket-pair colours, a minimap and sticky scroll, and six themes. It draws only the
8
+ lines on screen, so a keystroke costs the same in a 5,000-line file as in a 50-line one.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ pnpm add @aardarch/aardink-web # or: npm install @aardarch/aardink-web
14
+ pnpm add @aardarch/aardink-web@next # the latest pre-release
15
+ ```
16
+
17
+ The package is plain ES modules plus two `.wasm` files and a font. It needs a bundler that
18
+ understands `new URL('./file', import.meta.url)`, which Vite, webpack 5 and Rollup all do. Each
19
+ version is published from GitHub Actions with npm provenance.
20
+
21
+ ## Use
22
+
23
+ ```ts
24
+ import { createEditor, preloadAardink } from '@aardarch/aardink-web';
25
+
26
+ preloadAardink(); // optional: start loading the WebAssembly module and the font early
27
+
28
+ const editor = await createEditor(document.getElementById('editor')!, (text) => save(text), {
29
+ value: source,
30
+ language: 'xml',
31
+ theme: 'vscode-dark',
32
+ wordWrap: 'on',
33
+ minimap: { enabled: true },
34
+ });
35
+
36
+ editor.updateOptions({ readOnly: true });
37
+ editor.setDiagnostics([
38
+ { line: 3, startColumn: 5, endColumn: 9, message: 'Unknown tag', severity: 'error' },
39
+ ]);
40
+ const stop = editor.onDidChangeCursor((line, column) => showPosition(line, column));
41
+ // The language's own diagnostics (shown until you call setDiagnostics; null goes back to them).
42
+ const off = editor.onDidChangeDiagnostics((markers) => showProblems(markers));
43
+ const savedAt = editor.getAlternativeVersionId(); // unsaved while it differs
44
+ editor.dispose();
45
+ ```
46
+
47
+ A language of your own is a Monarch grammar registered before use, optionally extending a built-in
48
+ language's completions, diagnostics and folding, with Monaco's `defineTheme` for its colours:
49
+
50
+ ```ts
51
+ import { defineTheme, registerLanguage } from '@aardarch/aardink-web';
52
+
53
+ await registerLanguage(
54
+ { id: 'toy', extends: 'xml', grammar: { tokenizer: { root: [[/\bshout\b/, 'keyword']] } } },
55
+ { provideCompletionItems: (text, line, column) => [{ label: 'shout', kind: 'element' }] },
56
+ );
57
+ await defineTheme('toy-dark', { base: 'vs-dark', inherit: true, rules: [{ token: 'keyword', foreground: 'ff00ff' }] });
58
+ ```
59
+
60
+ The editor fills its container, so give the container a size. `createEditor` is async because
61
+ the first call loads the WebAssembly module (about 13 MB, 4.7 MB gzipped); every editor on the
62
+ page shares that one module. See [`index.d.ts`](./index.d.ts) for the full API, and the
63
+ [integration guide](https://github.com/aardarch/aardink/blob/main/docs/WEB_INTEGRATION.md) for how
64
+ Monaco's options and calls map onto it.
65
+
66
+ ## Vite
67
+
68
+ ```js
69
+ // vite.config.js
70
+ import { defineConfig } from 'vite';
71
+
72
+ export default defineConfig({
73
+ // The dev-server pre-bundler would move the package into .vite/deps and break the
74
+ // new URL(..., import.meta.url) references the loader relies on.
75
+ optimizeDeps: { exclude: ['@aardarch/aardink-web'] },
76
+ build: { target: 'es2022', chunkSizeWarningLimit: 3500 },
77
+ });
78
+ ```
79
+
80
+ `vite build` prints two harmless warnings about `node:fs` and `node:url` being externalised:
81
+ the Kotlin loader has Node and Deno branches that never run in a browser. Serve `.wasm` with
82
+ `Content-Type: application/wasm`.
83
+
84
+ ## The page
85
+
86
+ - Mount an editor where it stays: do not move its container to another place in the DOM, as a
87
+ Svelte `{#key}` block or a React portal can. Dispose it and mount a new one instead.
88
+ - On phones, `<meta name="viewport" content="width=device-width, initial-scale=1, interactive-widget=resizes-content">`
89
+ keeps the editor above the soft keyboard.
90
+
91
+ ## Browser support
92
+
93
+ Chrome and Edge 119+, Firefox 120+, Safari 18.2+: the module needs WebAssembly garbage
94
+ collection and exception handling.
95
+
96
+ ## Known limitations
97
+
98
+ Read these before adopting the package; the
99
+ [integration guide](https://github.com/aardarch/aardink/blob/main/docs/WEB_INTEGRATION.md) has
100
+ the measurements and the device checklist.
101
+
102
+ - **A disposed editor is not fully released** (about 275 KB each) until Aardink moves to Compose
103
+ Multiplatform 1.13. Reuse one editor with `setValue` and `updateOptions` rather than mounting
104
+ one per view.
105
+ - A theme rule's `background` is ignored; its `foreground` and `fontStyle` apply.
106
+
107
+ ## Licence
108
+
109
+ Apache-2.0. The bundled JetBrains Mono font is under the SIL Open Font License 1.1; see
110
+ `JETBRAINS_MONO_OFL.txt`.
package/index.d.ts CHANGED
@@ -29,6 +29,20 @@ export interface AardinkOptions {
29
29
  showGutter?: boolean;
30
30
  showLineNumbers?: boolean;
31
31
  showFoldMarkers?: boolean;
32
+ /** Width of an indent, in spaces. 4 by default. */
33
+ tabSize?: number;
34
+ /** Tab indents with spaces rather than a tab character. On by default. */
35
+ insertSpaces?: boolean;
36
+ /** Monaco's `renderLineHighlight`: anything but `'none'` highlights each caret's line. */
37
+ renderLineHighlight?: 'none' | 'line' | 'gutter' | 'all';
38
+ /** Which spaces and tabs are drawn, as dots and arrows. `'selection'` by default. */
39
+ renderWhitespace?: 'none' | 'boundary' | 'selection' | 'trailing' | 'all';
40
+ /** Colours brackets by nesting depth. On by default. */
41
+ bracketPairColorization?: { enabled?: boolean } | boolean;
42
+ /** A picture of the whole text at the side, with a slider over what is shown. Off by default. */
43
+ minimap?: { enabled?: boolean } | boolean;
44
+ /** The first lines of the blocks the top of the view is inside stay pinned at the top. Off by default. */
45
+ stickyScroll?: { enabled?: boolean } | boolean;
32
46
  }
33
47
 
34
48
  /** Monaco-marker shape: 1-based line and columns, `endColumn` exclusive. */
@@ -40,24 +54,144 @@ export interface AardinkDiagnostic {
40
54
  severity: 'error' | 'warning' | 'info';
41
55
  }
42
56
 
57
+ /** Monaco's `Selection`: from where it started to where the caret is, 1-based, with the range from start to end. */
58
+ export interface AardinkSelection {
59
+ selectionStartLineNumber: number;
60
+ selectionStartColumn: number;
61
+ positionLineNumber: number;
62
+ positionColumn: number;
63
+ readonly startLineNumber?: number;
64
+ readonly startColumn?: number;
65
+ readonly endLineNumber?: number;
66
+ readonly endColumn?: number;
67
+ }
68
+
69
+ /** What a content change was, as Monaco's `IModelContentChangedEvent` says it. */
70
+ export interface AardinkContentChange {
71
+ /** Goes up with every change. */
72
+ versionId: number;
73
+ isUndoing: boolean;
74
+ isRedoing: boolean;
75
+ /** The whole text was replaced (`setValue`). */
76
+ isFlush: boolean;
77
+ }
78
+
43
79
  export interface AardinkEditor {
44
80
  getValue(): string;
45
81
  /** Replaces the whole text and clears undo history. Fires the change listeners. */
46
82
  setValue(text: string): void;
47
83
  updateOptions(options: Partial<AardinkOptions>): void;
48
- /** Full text after every change, at most once per frame. Returns an unsubscribe function. */
49
- onDidChangeContent(listener: (text: string) => void): () => void;
84
+ /** Full text after every change, at most once per frame, and what the change was. Returns an unsubscribe function. */
85
+ onDidChangeContent(listener: (text: string, change: AardinkContentChange) => void): () => void;
50
86
  /** 1-based caret position. Returns an unsubscribe function. */
51
87
  onDidChangeCursor(listener: (line: number, column: number) => void): () => void;
52
- setDiagnostics(diagnostics: AardinkDiagnostic[]): void;
88
+ /**
89
+ * Shows these diagnostics in place of the language's own; `null` goes back to the language's
90
+ * own. Either way they move along with the text as it is edited, until the next list.
91
+ */
92
+ setDiagnostics(diagnostics: AardinkDiagnostic[] | null): void;
93
+ /**
94
+ * The language's own diagnostics each time they are collected: 500 ms after the editor appears,
95
+ * and after each pause in typing. Not called while a list from `setDiagnostics` is shown.
96
+ * Returns an unsubscribe function.
97
+ */
98
+ onDidChangeDiagnostics(listener: (diagnostics: AardinkDiagnostic[]) => void): () => void;
53
99
  revealPosition(line: number, column: number): void;
54
100
  showFind(): void;
55
101
  undo(): boolean;
56
102
  redo(): boolean;
103
+ canUndo(): boolean;
104
+ canRedo(): boolean;
105
+ /** Ends the current undo step, so the next edit starts a new one. */
106
+ pushUndoStop(): void;
107
+ /** Comes back when undo returns the text to an earlier state: keep it on save, and the text is unsaved while it differs. */
108
+ getAlternativeVersionId(): number;
109
+ /** The text the diff lane in the gutter compares with, typically what was last saved; `''` turns it off. */
110
+ setBaseline(text: string): void;
111
+ /** Formats the document with the language's formatter, as one undo step; resolves with whether anything changed. */
112
+ format(): Promise<boolean>;
113
+ focus(): void;
114
+ /** Every selection, the primary one first. */
115
+ getSelections(): AardinkSelection[];
116
+ /** Replaces the selections; the first becomes the primary one. */
117
+ setSelections(selections: AardinkSelection[]): void;
57
118
  /** Stops the editor. Safe to call twice. Remove or reuse the container yourself. */
58
119
  dispose(): void;
59
120
  }
60
121
 
122
+ /** A language highlighted by a grammar, for `registerLanguage`. */
123
+ export interface AardinkLanguageDefinition {
124
+ /** What `createEditor`'s `language` option names it by. Registering an id again replaces it. */
125
+ id: string;
126
+ /**
127
+ * A subset of Monaco's Monarch: states in `tokenizer` (`root` first), rules of
128
+ * `[regex, action, next?]`, `cases` with `@array`/`@default`/`@eos`, capture groups (each
129
+ * with a token name, `{ token, next }` or `cases` of its own), `include`, `@rematch`,
130
+ * `defaultToken`, `ignoreCase`, `tokenPostfix`. RegExp literals are fine. Lookbehind is not
131
+ * supported, and is refused.
132
+ */
133
+ grammar: object;
134
+ /** A built-in or registered language whose completions, diagnostics, hover and folding this one takes too, e.g. `'xml'`. */
135
+ extends?: string;
136
+ displayName?: string;
137
+ extensions?: string[];
138
+ /** As in Monaco's language configuration: what Ctrl/Cmd+/ comments with. */
139
+ comments?: { lineComment?: string; blockComment?: [string, string] };
140
+ }
141
+
142
+ /** Monaco's flags for a completion's `insertText`. */
143
+ export declare const CompletionItemInsertTextRule: {
144
+ readonly None: 0;
145
+ /** Insert the snippet's whitespace as written, rather than indented as the line it lands on. */
146
+ readonly KeepWhitespace: 1;
147
+ /** `insertText` is a snippet: `$1`, `${1:placeholder}` and `${1|a,b|}` are tab stops, `$0` the final caret. */
148
+ readonly InsertAsSnippet: 4;
149
+ };
150
+
151
+ export interface AardinkCompletionItem {
152
+ label: string;
153
+ /** What accepting it types; the label when absent. A snippet with `InsertAsSnippet`. */
154
+ insertText?: string;
155
+ /** `CompletionItemInsertTextRule` flags, as in Monaco. */
156
+ insertTextRules?: number;
157
+ kind?: 'element' | 'attribute' | 'value' | 'snippet' | 'module' | 'property';
158
+ detail?: string;
159
+ documentation?: string;
160
+ }
161
+
162
+ /**
163
+ * What a registered language knows beyond its grammar. Each gets the whole text and, where it
164
+ * asks about a place, a 1-based line and column; each may answer at once or with a Promise. With
165
+ * `extends`, the built-in language's answers come after these.
166
+ */
167
+ export interface AardinkLanguageProviders {
168
+ provideCompletionItems?(text: string, line: number, column: number): AardinkCompletionItem[] | null | Promise<AardinkCompletionItem[] | null>;
169
+ provideHover?(text: string, line: number, column: number): { title?: string; contents: string } | null | Promise<{ title?: string; contents: string } | null>;
170
+ provideDiagnostics?(text: string): AardinkDiagnostic[] | Promise<AardinkDiagnostic[]>;
171
+ }
172
+
173
+ /** Adds a language for `createEditor`'s `language` option. Rejects with what is wrong with the definition. */
174
+ export function registerLanguage(definition: AardinkLanguageDefinition, providers?: AardinkLanguageProviders): Promise<void>;
175
+
176
+ /**
177
+ * Monaco's `defineTheme` data. Token rules name tokens as Monaco does, and the longest matching
178
+ * prefix wins: `tag` also colours `tag.aardflex`, `comment.doc` only documentation comments. A
179
+ * rule's `fontStyle` (`'bold'`, `'italic'`, `'underline'`, `'strikethrough'`, or `''` for none)
180
+ * applies too; its `background` is accepted, so Monaco theme data passes as it is, and ignored.
181
+ */
182
+ export interface AardinkThemeData {
183
+ base: 'vs' | 'vs-dark' | 'hc-black' | 'hc-light';
184
+ inherit?: boolean;
185
+ rules: { token: string; foreground?: string; fontStyle?: string; background?: string }[];
186
+ colors?: Record<string, string>;
187
+ }
188
+
189
+ /** Adds a theme for `createEditor`'s `theme` option: Monaco's `defineTheme` data, or a VS Code theme's JSON. */
190
+ export function registerTheme(name: string, theme: AardinkThemeData | object): Promise<void>;
191
+
192
+ /** Monaco's name for `registerTheme`. */
193
+ export const defineTheme: typeof registerTheme;
194
+
61
195
  /**
62
196
  * Mounts an editor that fills `container`. Loads the WebAssembly module on first use (see
63
197
  * `preloadAardink`). `onChange` is registered as the first content listener.
@@ -68,6 +202,9 @@ export function createEditor(
68
202
  options?: AardinkOptions,
69
203
  ): Promise<AardinkEditor>;
70
204
 
205
+ /** For debugging a grammar: the tokens of `text` in `languageId`, per line, as Monaco's `tokenize` gives them. */
206
+ export function tokenize(text: string, languageId: string): Promise<{ offset: number; type: string }[][]>;
207
+
71
208
  /** Starts loading the WebAssembly module without mounting anything. */
72
209
  export function preloadAardink(): Promise<void>;
73
210
 
package/index.js CHANGED
@@ -17,7 +17,10 @@
17
17
  // Adapts the Kotlin module's integer-handle exports (aardinkCreate, aardinkGetValue, ...) into
18
18
  // the Monaco-shaped object described in index.d.ts.
19
19
 
20
- export const version = '0.5.0';
20
+ export const version = '0.6.0-rc2';
21
+
22
+ /** Monaco's flags for a completion's `insertText`. */
23
+ export const CompletionItemInsertTextRule = Object.freeze({ None: 0, KeepWhitespace: 1, InsertAsSnippet: 4 });
21
24
 
22
25
  let kotlinModule;
23
26
 
@@ -35,18 +38,121 @@ function load() {
35
38
  }
36
39
 
37
40
  export function preloadAardink() {
38
- return load().then(() => undefined);
41
+ // The font too: an editor waits up to half a second for it rather than showing in another first.
42
+ return load().then((k) => k.aardinkPreloadFont());
39
43
  }
40
44
 
41
45
  let generatedIds = 0;
42
46
 
43
- /** Keeps only the fields the Kotlin side knows, translating Monaco's wordWrap 'on'/'off'. */
47
+ /** A selection with Monaco's range fields too: start is whichever end comes first. */
48
+ function withRange(s) {
49
+ const forward = s.selectionStartLineNumber < s.positionLineNumber ||
50
+ (s.selectionStartLineNumber === s.positionLineNumber && s.selectionStartColumn <= s.positionColumn);
51
+ return {
52
+ ...s,
53
+ startLineNumber: forward ? s.selectionStartLineNumber : s.positionLineNumber,
54
+ startColumn: forward ? s.selectionStartColumn : s.positionColumn,
55
+ endLineNumber: forward ? s.positionLineNumber : s.selectionStartLineNumber,
56
+ endColumn: forward ? s.positionColumn : s.selectionStartColumn,
57
+ };
58
+ }
59
+
60
+ /** For debugging a grammar: the tokens of `text` in `languageId`, per line, as Monaco's tokenize gives them. */
61
+ export async function tokenize(text, languageId) {
62
+ const k = await load();
63
+ return JSON.parse(k.aardinkTokenize(languageId, text));
64
+ }
65
+
66
+ /**
67
+ * A Monarch grammar as JSON: RegExp literals (/.../) become their source, as the Kotlin side reads
68
+ * regexes from strings; everything else is kept.
69
+ */
70
+ function monarchToJson(value) {
71
+ if (value instanceof RegExp) return value.source;
72
+ if (Array.isArray(value)) return value.map(monarchToJson);
73
+ if (value && typeof value === 'object') {
74
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, monarchToJson(item)]));
75
+ }
76
+ return value;
77
+ }
78
+
79
+ /** A provider's answer (a value or a Promise of one) as the Promise of JSON the Kotlin side awaits. */
80
+ function answer(provider, ...args) {
81
+ if (!provider) return Promise.resolve(null);
82
+ return Promise.resolve()
83
+ .then(() => provider(...args))
84
+ .then((value) => (value == null ? null : JSON.stringify(value)));
85
+ }
86
+
87
+ /**
88
+ * Adds a language highlighted by a Monarch grammar, for createEditor's `language` option. See
89
+ * index.d.ts for the definition and the providers.
90
+ */
91
+ export async function registerLanguage(definition, providers = {}) {
92
+ const k = await load();
93
+ const grammar = monarchToJson(definition.grammar ?? {});
94
+ if (definition.comments) grammar.comments = definition.comments;
95
+ const error = k.aardinkRegisterLanguage(
96
+ JSON.stringify({
97
+ id: definition.id,
98
+ extends: definition.extends,
99
+ displayName: definition.displayName,
100
+ extensions: definition.extensions,
101
+ grammar,
102
+ }),
103
+ (text, line, column) => answer(providers.provideCompletionItems, text, line, column),
104
+ (text, line, column) => answer(providers.provideHover, text, line, column),
105
+ (text) => answer(providers.provideDiagnostics, text),
106
+ );
107
+ if (error) throw new Error(`Aardink: ${error}`);
108
+ }
109
+
110
+ /** Monaco's defineTheme data ({ base, inherit, rules, colors }) as VS Code theme JSON; VS Code JSON as it is. */
111
+ function toVsCodeTheme(theme) {
112
+ if (!theme.rules) return theme;
113
+ const base = { vs: 'vscode-light', 'hc-light': 'vscode-light' }[theme.base] ?? 'vscode-dark';
114
+ return {
115
+ type: base === 'vscode-light' ? 'light' : 'dark',
116
+ base: theme.inherit === false ? undefined : base,
117
+ colors: theme.colors ?? {},
118
+ // A rule's colour and font style; its background is not drawn.
119
+ tokenColors: theme.rules
120
+ .filter((rule) => rule.foreground || rule.fontStyle !== undefined)
121
+ .map((rule) => ({
122
+ scope: rule.token,
123
+ settings: {
124
+ ...(rule.foreground ? { foreground: `#${rule.foreground.replace(/^#/, '')}` } : {}),
125
+ ...(rule.fontStyle !== undefined ? { fontStyle: rule.fontStyle } : {}),
126
+ },
127
+ })),
128
+ };
129
+ }
130
+
131
+ /** Adds a theme for createEditor's `theme` option, from Monaco's defineTheme data or VS Code theme JSON. */
132
+ export async function registerTheme(name, theme) {
133
+ const k = await load();
134
+ const error = k.aardinkRegisterTheme(name, JSON.stringify(toVsCodeTheme(theme)));
135
+ if (error) throw new Error(`Aardink: ${error}`);
136
+ }
137
+
138
+ /** Monaco's name for registerTheme. */
139
+ export const defineTheme = registerTheme;
140
+
141
+ /**
142
+ * Keeps only the fields the Kotlin side knows, translating Monaco's wordWrap 'on'/'off' and its
143
+ * { enabled } objects for minimap and stickyScroll (a plain boolean works too).
144
+ */
44
145
  function toKotlinOptions(options) {
45
146
  const out = {};
46
- for (const key of ['language', 'theme', 'fontSize', 'readOnly', 'showGutter', 'showLineNumbers', 'showFoldMarkers']) {
147
+ for (const key of ['language', 'theme', 'fontSize', 'readOnly', 'showGutter', 'showLineNumbers', 'showFoldMarkers', 'tabSize', 'insertSpaces', 'highlightCurrentLine', 'renderWhitespace']) {
47
148
  if (options[key] !== undefined) out[key] = options[key];
48
149
  }
49
150
  if (options.wordWrap !== undefined) out.wordWrap = options.wordWrap === 'on';
151
+ if (options.renderLineHighlight !== undefined) out.highlightCurrentLine = options.renderLineHighlight !== 'none';
152
+ for (const key of ['minimap', 'stickyScroll', 'bracketPairColorization']) {
153
+ const value = options[key];
154
+ if (value !== undefined) out[key] = typeof value === 'object' && value !== null ? value.enabled !== false : !!value;
155
+ }
50
156
  return out;
51
157
  }
52
158
 
@@ -58,8 +164,17 @@ export async function createEditor(container, onChange, options = {}) {
58
164
  // The Kotlin side holds one listener of each kind; fan out to any number here.
59
165
  const contentListeners = new Set();
60
166
  const cursorListeners = new Set();
61
- k.aardinkOnChange(id, (text) => contentListeners.forEach((listener) => listener(text)));
167
+ const diagnosticsListeners = new Set();
168
+ k.aardinkOnContentChange(id, (text, versionId, kind) => {
169
+ const event = { versionId, isUndoing: kind === 'undo', isRedoing: kind === 'redo', isFlush: kind === 'flush' };
170
+ contentListeners.forEach((listener) => listener(text, event));
171
+ });
62
172
  k.aardinkOnCursorChange(id, (line, column) => cursorListeners.forEach((listener) => listener(line, column)));
173
+ k.aardinkOnDiagnosticsChange(id, (json) => {
174
+ if (diagnosticsListeners.size === 0) return;
175
+ const diagnostics = JSON.parse(json);
176
+ diagnosticsListeners.forEach((listener) => listener(diagnostics));
177
+ });
63
178
  if (onChange) contentListeners.add(onChange);
64
179
 
65
180
  let disposed = false;
@@ -76,8 +191,30 @@ export async function createEditor(container, onChange, options = {}) {
76
191
  cursorListeners.add(listener);
77
192
  return () => cursorListeners.delete(listener);
78
193
  },
79
- setDiagnostics: (diagnostics) => k.aardinkSetDiagnostics(id, JSON.stringify(diagnostics)),
194
+ setDiagnostics: (diagnostics) => k.aardinkSetDiagnostics(id, JSON.stringify(diagnostics ?? null)),
195
+ onDidChangeDiagnostics(listener) {
196
+ diagnosticsListeners.add(listener);
197
+ return () => diagnosticsListeners.delete(listener);
198
+ },
80
199
  revealPosition: (line, column) => k.aardinkRevealPosition(id, line, column),
200
+ setBaseline: (text) => k.aardinkSetBaseline(id, text ?? ''),
201
+ format: () => new Promise((resolve) => k.aardinkFormat(id, resolve)),
202
+ focus: () => k.aardinkFocus(id),
203
+ getSelections: () => JSON.parse(k.aardinkGetSelections(id)).map(withRange),
204
+ setSelections: (selections) =>
205
+ k.aardinkSetSelections(
206
+ id,
207
+ JSON.stringify(selections.map((s) => ({
208
+ selectionStartLineNumber: s.selectionStartLineNumber,
209
+ selectionStartColumn: s.selectionStartColumn,
210
+ positionLineNumber: s.positionLineNumber,
211
+ positionColumn: s.positionColumn,
212
+ }))),
213
+ ),
214
+ canUndo: () => k.aardinkCanUndo(id),
215
+ canRedo: () => k.aardinkCanRedo(id),
216
+ pushUndoStop: () => k.aardinkPushUndoStop(id),
217
+ getAlternativeVersionId: () => k.aardinkGetAlternativeVersionId(id),
81
218
  showFind: () => k.aardinkShowFind(id),
82
219
  undo: () => k.aardinkUndo(id),
83
220
  redo: () => k.aardinkRedo(id),
@@ -86,6 +223,7 @@ export async function createEditor(container, onChange, options = {}) {
86
223
  disposed = true;
87
224
  contentListeners.clear();
88
225
  cursorListeners.clear();
226
+ diagnosticsListeners.clear();
89
227
  k.aardinkDispose(id);
90
228
  },
91
229
  };
@@ -149,6 +149,8 @@ const js_code = {
149
149
  'kotlin.js.__convertKotlinClosureToJsClosure_1' : (f, trampoline) => getCachedJsObject(f, (p0) => trampoline(f, p0)),
150
150
  'kotlin.js.then_$external_fun_1' : (_this, p0, p1) => _this.then(p0, p1),
151
151
  'kotlin.js.catch_$external_fun' : (_this, p0) => _this.catch(p0),
152
+ 'kotlin.js.Promise_$external_class_instanceof' : (x) => x instanceof Promise,
153
+ 'kotlin.js.Promise_$external_class_get' : () => Promise,
152
154
  'kotlin.random.initialSeed' : () => ((Math.random() * Math.pow(2, 32)) | 0),
153
155
  'kotlin.wasm.internal.getJsClassName' : (jsKlass) => jsKlass.name,
154
156
  'kotlin.wasm.internal.instanceOf' : (ref, jsKlass) => ref instanceof jsKlass,
@@ -558,8 +560,13 @@ const js_code = {
558
560
  'androidx.compose.material3.internal.weakMap_js_code' : () => (new WeakMap()),
559
561
  'androidx.compose.material3.internal.set_$external_fun' : (_this, p0, p1) => _this.set(p0, p1),
560
562
  'androidx.compose.material3.internal.get_$external_fun' : (_this, p0) => _this.get(p0),
563
+ 'com.aardarch.aardink.platform.readClipboardText' : () => navigator.clipboard.readText(),
561
564
  'com.aardarch.aardink.sampleweb.__callJsClosure_((String)->Unit)' : (f, p0) => f(p0),
562
565
  'com.aardarch.aardink.sampleweb.__callJsClosure_((Int,Int)->Unit)' : (f, p0, p1) => f(p0, p1),
566
+ 'com.aardarch.aardink.sampleweb.__callJsClosure_((String,Int,Int)->Js)' : (f, p0, p1, p2) => f(p0, p1, p2),
567
+ 'com.aardarch.aardink.sampleweb.__callJsClosure_((String)->Js)' : (f, p0) => f(p0),
568
+ 'com.aardarch.aardink.sampleweb.__callJsClosure_((String,Int,String)->Unit)' : (f, p0, p1, p2) => f(p0, p1, p2),
569
+ 'com.aardarch.aardink.sampleweb.__callJsClosure_((Boolean)->Unit)' : (f, p0) => f(p0),
563
570
  'com.aardarch.aardink.sampleweb.usedJsHeapBytes' : () => (performance.memory ? performance.memory.usedJSHeapSize : -1)
564
571
  }
565
572
 
@@ -77,6 +77,7 @@ export { wasmMemory as memory }
77
77
 
78
78
  export const {
79
79
  aardinkSetBundledFontUrl,
80
+ aardinkPreloadFont,
80
81
  aardinkCreate,
81
82
  aardinkGetValue,
82
83
  aardinkSetValue,
@@ -84,6 +85,20 @@ export const {
84
85
  aardinkOnChange,
85
86
  aardinkOnCursorChange,
86
87
  aardinkSetDiagnostics,
88
+ aardinkOnDiagnosticsChange,
89
+ aardinkRegisterLanguage,
90
+ aardinkRegisterTheme,
91
+ aardinkOnContentChange,
92
+ aardinkSetBaseline,
93
+ aardinkFormat,
94
+ aardinkFocus,
95
+ aardinkGetSelections,
96
+ aardinkSetSelections,
97
+ aardinkCanUndo,
98
+ aardinkCanRedo,
99
+ aardinkPushUndoStop,
100
+ aardinkGetAlternativeVersionId,
101
+ aardinkTokenize,
87
102
  aardinkRevealPosition,
88
103
  aardinkShowFind,
89
104
  aardinkUndo,
Binary file
package/package.json CHANGED
@@ -1,11 +1,27 @@
1
1
  {
2
2
  "name": "@aardarch/aardink-web",
3
- "version": "0.5.0",
3
+ "version": "0.6.0-rc2",
4
4
  "description": "The Aardink code editor for the browser: Compose Multiplatform on WebAssembly behind a Monaco-shaped API.",
5
+ "keywords": [
6
+ "code-editor",
7
+ "editor",
8
+ "syntax-highlighting",
9
+ "monaco",
10
+ "webassembly",
11
+ "wasm",
12
+ "kotlin",
13
+ "compose-multiplatform"
14
+ ],
15
+ "homepage": "https://aardarch.github.io/aardink/",
16
+ "bugs": {
17
+ "url": "https://github.com/aardarch/aardink/issues"
18
+ },
5
19
  "license": "Apache-2.0",
20
+ "author": "Aardarch",
6
21
  "repository": {
7
22
  "type": "git",
8
- "url": "git+https://github.com/aardarch/aardink.git"
23
+ "url": "git+https://github.com/aardarch/aardink.git",
24
+ "directory": "sample-web"
9
25
  },
10
26
  "type": "module",
11
27
  "main": "index.js",
@@ -20,8 +36,17 @@
20
36
  "index.js",
21
37
  "index.d.ts",
22
38
  "kotlin/",
39
+ "README.md",
40
+ "LICENSE",
23
41
  "JETBRAINS_MONO_OFL.txt"
24
42
  ],
25
43
  "sideEffects": false,
44
+ "engines": {
45
+ "node": ">=20"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public",
49
+ "provenance": true
50
+ },
26
51
  "dependencies": { "@js-joda/core": "3.2.0" }
27
52
  }