@canvist/web 0.0.0-stage → 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.
Files changed (45) hide show
  1. package/dist/collab.d.ts +99 -0
  2. package/dist/collab.js +211 -0
  3. package/dist/edit-context.d.ts +59 -0
  4. package/dist/edit-context.js +16 -0
  5. package/dist/editor.d.ts +301 -0
  6. package/dist/editor.js +776 -0
  7. package/dist/font-watcher.d.ts +28 -0
  8. package/dist/font-watcher.js +88 -0
  9. package/dist/fonts.d.ts +130 -0
  10. package/dist/fonts.js +343 -0
  11. package/dist/full.d.ts +40 -0
  12. package/dist/full.js +25 -0
  13. package/dist/input.d.ts +68 -0
  14. package/dist/input.js +551 -0
  15. package/dist/load.d.ts +9 -0
  16. package/dist/load.js +21 -0
  17. package/dist/measure.d.ts +56 -0
  18. package/dist/measure.js +524 -0
  19. package/dist/menus.d.ts +71 -0
  20. package/dist/menus.js +173 -0
  21. package/dist/mirror.d.ts +24 -0
  22. package/dist/mirror.js +73 -0
  23. package/dist/mod.d.ts +30 -0
  24. package/dist/mod.js +7 -0
  25. package/dist/placeholder.d.ts +27 -0
  26. package/dist/placeholder.js +69 -0
  27. package/dist/platform.d.ts +9 -0
  28. package/dist/platform.js +12 -0
  29. package/dist/plugin.d.ts +39 -0
  30. package/dist/plugin.js +1 -0
  31. package/package.json +86 -5
  32. package/readme.md +92 -0
  33. package/wasm/canvist_web.d.ts +358 -0
  34. package/wasm/canvist_web.js +1175 -0
  35. package/wasm/canvist_web_bg.wasm +0 -0
  36. package/wasm/canvist_web_bg.wasm.d.ts +59 -0
  37. package/wasm-collab/canvist_web.d.ts +391 -0
  38. package/wasm-collab/canvist_web.js +1343 -0
  39. package/wasm-collab/canvist_web_bg.wasm +0 -0
  40. package/wasm-collab/canvist_web_bg.wasm.d.ts +64 -0
  41. package/wasm-full/canvist_web.d.ts +358 -0
  42. package/wasm-full/canvist_web.js +1175 -0
  43. package/wasm-full/canvist_web_bg.wasm +0 -0
  44. package/wasm-full/canvist_web_bg.wasm.d.ts +59 -0
  45. package/README.md +0 -3
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Collaborative editing over y-websocket, out of the box.
3
+ *
4
+ * `createEditor` here makes an editor from a build of canvist with Yjs
5
+ * inside (a second WebAssembly module, so editors that don't collaborate
6
+ * never download it), and `connect` joins it to a room on any server that
7
+ * speaks y-websocket's protocol: y-websocket's own server, canvist's
8
+ * `receiveSync` servers, or canvist Cloud Sync. The editor is the Yjs peer
9
+ * itself, so there is no `Y.Doc` to keep in step, and JavaScript Yjs peers
10
+ * in the same room see the same document.
11
+ *
12
+ * ```ts
13
+ * import { connect, createEditor } from "@canvist/web/collab";
14
+ *
15
+ * const editor = await createEditor(document.querySelector("#editor")!);
16
+ * const connection = connect(editor, {
17
+ * url: "wss://sync.example.com",
18
+ * room: "team-notes",
19
+ * });
20
+ * await connection.whenSynced();
21
+ * ```
22
+ *
23
+ * @module
24
+ */
25
+ import type { Extensions, StarterKit } from "@canvist/headless";
26
+ import { WebEditor } from "../wasm-collab/canvist_web.js";
27
+ import { Editor, type EditorOptions } from "./editor.ts";
28
+ import type { WasmSource } from "./load.ts";
29
+ import type { Plugins } from "./plugin.ts";
30
+ /**
31
+ * Load the collaborative WebAssembly module once. Without a source it is
32
+ * fetched from next to this file, which bundlers such as Vite follow.
33
+ */
34
+ export declare function loadCollab(source?: WasmSource): Promise<void>;
35
+ /**
36
+ * Create an editor that can collaborate: `@canvist/web`'s `createEditor`,
37
+ * with Yjs inside. `options.wasm` is where the collaborative module comes
38
+ * from.
39
+ */
40
+ export declare function createEditor<const E extends Extensions = StarterKit, const P extends Plugins = []>(element: HTMLElement, options?: EditorOptions<E, P>): Promise<Editor<E, P>>;
41
+ /** What a connection needs from its editor. */
42
+ type Peer = Pick<Editor<StarterKit>, "documentChanged" | "subscribe">;
43
+ /** Where and how to connect. */
44
+ export interface ConnectOptions {
45
+ /**
46
+ * The server's WebSocket URL, such as `wss://sync.example.com`. The room
47
+ * is added as a path segment, as y-websocket's `WebsocketProvider` does.
48
+ */
49
+ readonly url: string;
50
+ /** The room: every peer in it edits one document. */
51
+ readonly room: string;
52
+ /** Query parameters for the URL, such as a short-lived token. */
53
+ readonly params?: Readonly<Record<string, string>>;
54
+ /** This peer's Yjs client id, unique in the room. Random by default. */
55
+ readonly clientId?: number;
56
+ /** Reconnect, with backoff, when the connection drops. Default `true`. */
57
+ readonly reconnect?: boolean;
58
+ /** The `WebSocket` class to use. Default the global one. */
59
+ readonly WebSocket?: typeof WebSocket;
60
+ }
61
+ /** Whether a {@link Connection} is open. */
62
+ export type ConnectionStatus = "connecting" | "connected" | "disconnected";
63
+ /**
64
+ * Join `editor` to a room. Until the room's document arrives, the editor
65
+ * keeps showing what it had; then the room's document replaces it (an
66
+ * empty room gets one empty paragraph). Edits travel both ways from then
67
+ * on, and undo reverts only this peer's changes.
68
+ *
69
+ * `editor` must come from this module's {@link createEditor}.
70
+ */
71
+ export declare function connect<E extends Extensions, P extends Plugins>(editor: Editor<E, P>, options: ConnectOptions): Connection;
72
+ /** A live link between an editor and a room. Create one with {@link connect}. */
73
+ export declare class Connection {
74
+ #private;
75
+ /** @internal Use {@link connect}. */
76
+ constructor(editor: Peer, build: WebEditor, options: ConnectOptions);
77
+ /** Whether the connection is open. */
78
+ get status(): ConnectionStatus;
79
+ /** Whether the room's document has arrived. */
80
+ get synced(): boolean;
81
+ /** This peer's Yjs client id. */
82
+ get clientId(): number;
83
+ /**
84
+ * Call `listener` whenever {@link Connection.status} or
85
+ * {@link Connection.synced} changes. Returns a function that
86
+ * unsubscribes.
87
+ */
88
+ subscribe(listener: () => void): () => void;
89
+ /** Resolve once the room's document has arrived. */
90
+ whenSynced(): Promise<void>;
91
+ /**
92
+ * Close the connection for good. The editor keeps the shared document;
93
+ * {@link connect} it again to carry on, and it catches up with what it
94
+ * missed.
95
+ */
96
+ disconnect(): void;
97
+ [Symbol.dispose](): void;
98
+ }
99
+ export {};
package/dist/collab.js ADDED
@@ -0,0 +1,211 @@
1
+ import init, { WebEditor } from "../wasm-collab/canvist_web.js";
2
+ import { Editor } from "./editor.js";
3
+ /** The longest wait between reconnection attempts, in milliseconds. */
4
+ const MAX_BACKOFF_MS = 10_000;
5
+ /** The first wait before reconnecting, in milliseconds; it doubles each time. */
6
+ const FIRST_BACKOFF_MS = 250;
7
+ /** The collaborative build of each editor `createEditor` here made. */
8
+ const builds = new WeakMap();
9
+ let loading;
10
+ /**
11
+ * Load the collaborative WebAssembly module once. Without a source it is
12
+ * fetched from next to this file, which bundlers such as Vite follow.
13
+ */
14
+ export function loadCollab(source) {
15
+ loading ??= init({
16
+ module_or_path: source ??
17
+ new URL("../wasm-collab/canvist_web_bg.wasm", import.meta.url),
18
+ }).then(() => undefined);
19
+ return loading;
20
+ }
21
+ /**
22
+ * Create an editor that can collaborate: `@canvist/web`'s `createEditor`,
23
+ * with Yjs inside. `options.wasm` is where the collaborative module comes
24
+ * from.
25
+ */
26
+ export async function createEditor(element, options = {}) {
27
+ await loadCollab(options.wasm);
28
+ let build;
29
+ const editor = new Editor(element, options, (...args) => {
30
+ build = new WebEditor(...args);
31
+ return build;
32
+ });
33
+ if (build === undefined) {
34
+ throw new Error("the editor did not make its WebAssembly half");
35
+ }
36
+ builds.set(editor, build);
37
+ return editor;
38
+ }
39
+ /**
40
+ * Join `editor` to a room. Until the room's document arrives, the editor
41
+ * keeps showing what it had; then the room's document replaces it (an
42
+ * empty room gets one empty paragraph). Edits travel both ways from then
43
+ * on, and undo reverts only this peer's changes.
44
+ *
45
+ * `editor` must come from this module's {@link createEditor}.
46
+ */
47
+ export function connect(editor, options) {
48
+ const build = builds.get(editor);
49
+ if (build === undefined) {
50
+ throw new TypeError("connect() needs an editor made by createEditor from @canvist/web/collab");
51
+ }
52
+ return new Connection(editor, build, options);
53
+ }
54
+ /** A live link between an editor and a room. Create one with {@link connect}. */
55
+ export class Connection {
56
+ #editor;
57
+ #build;
58
+ #options;
59
+ #clientId;
60
+ #listeners = new Set();
61
+ #unsubscribe;
62
+ #socket;
63
+ #status = "connecting";
64
+ #synced = false;
65
+ #closed = false;
66
+ #backoff = FIRST_BACKOFF_MS;
67
+ #retry = 0;
68
+ /** @internal Use {@link connect}. */
69
+ constructor(editor, build, options) {
70
+ this.#editor = editor;
71
+ this.#build = build;
72
+ this.#options = options;
73
+ this.#clientId = options.clientId ?? randomClientId();
74
+ // Every local change goes out as soon as it is made.
75
+ this.#unsubscribe = editor.subscribe(() => this.#flush());
76
+ this.#open();
77
+ }
78
+ /** Whether the connection is open. */
79
+ get status() {
80
+ return this.#status;
81
+ }
82
+ /** Whether the room's document has arrived. */
83
+ get synced() {
84
+ return this.#synced;
85
+ }
86
+ /** This peer's Yjs client id. */
87
+ get clientId() {
88
+ return this.#clientId;
89
+ }
90
+ /**
91
+ * Call `listener` whenever {@link Connection.status} or
92
+ * {@link Connection.synced} changes. Returns a function that
93
+ * unsubscribes.
94
+ */
95
+ subscribe(listener) {
96
+ this.#listeners.add(listener);
97
+ return () => this.#listeners.delete(listener);
98
+ }
99
+ /** Resolve once the room's document has arrived. */
100
+ whenSynced() {
101
+ if (this.#synced) {
102
+ return Promise.resolve();
103
+ }
104
+ return new Promise((resolve) => {
105
+ const unsubscribe = this.subscribe(() => {
106
+ if (this.#synced) {
107
+ unsubscribe();
108
+ resolve();
109
+ }
110
+ });
111
+ });
112
+ }
113
+ /**
114
+ * Close the connection for good. The editor keeps the shared document;
115
+ * {@link connect} it again to carry on, and it catches up with what it
116
+ * missed.
117
+ */
118
+ disconnect() {
119
+ this.#closed = true;
120
+ clearTimeout(this.#retry);
121
+ this.#unsubscribe();
122
+ this.#socket?.close();
123
+ this.#setStatus("disconnected");
124
+ }
125
+ [Symbol.dispose]() {
126
+ this.disconnect();
127
+ }
128
+ #open() {
129
+ const Socket = this.#options.WebSocket ?? WebSocket;
130
+ const socket = new Socket(roomUrl(this.#options));
131
+ socket.binaryType = "arraybuffer";
132
+ this.#socket = socket;
133
+ this.#setStatus("connecting");
134
+ socket.addEventListener("open", () => {
135
+ this.#backoff = FIRST_BACKOFF_MS;
136
+ this.#setStatus("connected");
137
+ // A shared document asks for what it missed and sends what it
138
+ // changed offline; a new one asks for the whole document.
139
+ this.#send(this.#build.isShared()
140
+ ? this.#build.syncStart()
141
+ : this.#build.join(this.#clientId));
142
+ this.#flush();
143
+ });
144
+ socket.addEventListener("message", (event) => {
145
+ if (event.data instanceof ArrayBuffer) {
146
+ this.#receive(new Uint8Array(event.data));
147
+ }
148
+ });
149
+ socket.addEventListener("close", () => this.#closedBy(socket));
150
+ }
151
+ #receive(message) {
152
+ const [replies, changed] = this.#build.receiveSync(message);
153
+ for (const reply of replies) {
154
+ this.#send(reply);
155
+ }
156
+ if (changed) {
157
+ this.#editor.documentChanged();
158
+ }
159
+ if (!this.#synced && this.#build.isShared()) {
160
+ this.#synced = true;
161
+ // Joining an empty room wrote its first paragraph.
162
+ this.#flush();
163
+ this.#notify();
164
+ }
165
+ }
166
+ #flush() {
167
+ if (this.#status !== "connected") {
168
+ return;
169
+ }
170
+ for (const message of this.#build.flushSync()) {
171
+ this.#send(message);
172
+ }
173
+ }
174
+ #send(message) {
175
+ if (this.#socket?.readyState === WebSocket.OPEN) {
176
+ // A copy the socket owns: the view's buffer type is too wide to send.
177
+ this.#socket.send(new Uint8Array(message));
178
+ }
179
+ }
180
+ #closedBy(socket) {
181
+ if (socket !== this.#socket || this.#closed) {
182
+ return;
183
+ }
184
+ this.#setStatus("disconnected");
185
+ if (this.#options.reconnect ?? true) {
186
+ this.#retry = window.setTimeout(() => this.#open(), this.#backoff);
187
+ this.#backoff = Math.min(this.#backoff * 2, MAX_BACKOFF_MS);
188
+ }
189
+ }
190
+ #setStatus(status) {
191
+ if (status !== this.#status) {
192
+ this.#status = status;
193
+ this.#notify();
194
+ }
195
+ }
196
+ #notify() {
197
+ for (const listener of this.#listeners) {
198
+ listener();
199
+ }
200
+ }
201
+ }
202
+ /** The room's URL, as y-websocket's `WebsocketProvider` builds it. */
203
+ function roomUrl(options) {
204
+ const base = options.url.replace(/\/+$/, "");
205
+ const query = new URLSearchParams(options.params ?? {}).toString();
206
+ return `${base}/${options.room}${query === "" ? "" : `?${query}`}`;
207
+ }
208
+ /** A random 32-bit Yjs client id, as Yjs picks its own. */
209
+ function randomClientId() {
210
+ return crypto.getRandomValues(new Uint32Array(1))[0] ?? 0;
211
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The parts of the EditContext API the editor uses.
3
+ *
4
+ * EditContext (Chromium 121+) lets a canvas receive text input, IME
5
+ * composition, and the platform's text services as if it were a text field,
6
+ * without a hidden element. TypeScript's DOM library doesn't declare it
7
+ * yet, so these declarations cover what `input.ts` calls.
8
+ *
9
+ * @see https://w3c.github.io/edit-context/
10
+ * @module
11
+ */
12
+ /** Text input arriving at an {@link EditContext}. */
13
+ export interface TextUpdateEvent extends Event {
14
+ /** The UTF-16 range of the context's text being replaced. */
15
+ readonly updateRangeStart: number;
16
+ readonly updateRangeEnd: number;
17
+ /** The replacement. */
18
+ readonly text: string;
19
+ /** The selection afterwards. */
20
+ readonly selectionStart: number;
21
+ readonly selectionEnd: number;
22
+ }
23
+ /** The platform asks where the characters in a range are drawn. */
24
+ export interface CharacterBoundsUpdateEvent extends Event {
25
+ readonly rangeStart: number;
26
+ readonly rangeEnd: number;
27
+ }
28
+ interface EditContextEvents {
29
+ textupdate: TextUpdateEvent;
30
+ characterboundsupdate: CharacterBoundsUpdateEvent;
31
+ compositionstart: CompositionEvent;
32
+ compositionend: CompositionEvent;
33
+ }
34
+ /** A text model the platform's input methods edit. */
35
+ export interface EditContext extends EventTarget {
36
+ readonly text: string;
37
+ readonly selectionStart: number;
38
+ readonly selectionEnd: number;
39
+ updateText(start: number, end: number, text: string): void;
40
+ updateSelection(start: number, end: number): void;
41
+ updateControlBounds(bounds: DOMRect): void;
42
+ updateSelectionBounds(bounds: DOMRect): void;
43
+ updateCharacterBounds(start: number, bounds: DOMRect[]): void;
44
+ addEventListener<K extends keyof EditContextEvents>(type: K, listener: (event: EditContextEvents[K]) => void, options?: AddEventListenerOptions): void;
45
+ addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
46
+ }
47
+ interface EditContextInit {
48
+ readonly text?: string;
49
+ readonly selectionStart?: number;
50
+ readonly selectionEnd?: number;
51
+ }
52
+ type EditContextConstructor = new (init?: EditContextInit) => EditContext;
53
+ /** An element that takes an {@link EditContext}. */
54
+ export type EditContextHost = HTMLElement & {
55
+ editContext: EditContext | null;
56
+ };
57
+ /** The browser's EditContext constructor, where it has one. */
58
+ export declare function editContextApi(): EditContextConstructor | undefined;
59
+ export {};
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The parts of the EditContext API the editor uses.
3
+ *
4
+ * EditContext (Chromium 121+) lets a canvas receive text input, IME
5
+ * composition, and the platform's text services as if it were a text field,
6
+ * without a hidden element. TypeScript's DOM library doesn't declare it
7
+ * yet, so these declarations cover what `input.ts` calls.
8
+ *
9
+ * @see https://w3c.github.io/edit-context/
10
+ * @module
11
+ */
12
+ /** The browser's EditContext constructor, where it has one. */
13
+ export function editContextApi() {
14
+ const scope = globalThis;
15
+ return scope.EditContext;
16
+ }
@@ -0,0 +1,301 @@
1
+ /**
2
+ * The canvas editor: a DOM element that hosts the WebAssembly editor's
3
+ * canvas, keeps it sized to the visible part of the document, and turns
4
+ * scrolling, resizing, zoom, and font loading into repaints on the next
5
+ * animation frame.
6
+ *
7
+ * @module
8
+ */
9
+ import type { CommandName, CommandParams, Commands, Content, DocJson, Extensions, Format, Position, Queries, QueryName, QueryParams, QueryResult, Range, Registered, StarterKit, ThemeOptions } from "@canvist/headless";
10
+ import { WebEditor } from "../wasm/canvist_web.js";
11
+ import { type InputMode } from "./input.ts";
12
+ import { type WasmSource } from "./load.ts";
13
+ import { type KeyPlatform } from "./platform.ts";
14
+ import type { PluginApis, Plugins } from "./plugin.ts";
15
+ /** Which line-breaking rules layout follows: Blink's tailoring, or ICU's defaults (Firefox and WebKit). */
16
+ export type LineBreaks = "chromium" | "icu";
17
+ /** Options for {@link createEditor}. */
18
+ export interface EditorOptions<E extends Extensions, P extends Plugins> {
19
+ /** The extensions, TipTap's StarterKit by default. */
20
+ readonly extensions?: E;
21
+ /** Plugins, such as `fonts()` from `@canvist/web/fonts`. */
22
+ readonly plugins?: P;
23
+ /** The initial document. */
24
+ readonly content?: Content<E>;
25
+ /** The theme. Server rendering must use the same one. */
26
+ readonly theme?: ThemeOptions;
27
+ /** Line-breaking tailoring; detected from the browser by default. */
28
+ readonly breaks?: LineBreaks;
29
+ /** The client block ids are allocated under. Default 1. */
30
+ readonly client?: number;
31
+ /** Pixels beyond the viewport to lay out ahead of scrolling. Default one screen. */
32
+ readonly overscan?: number;
33
+ /** Where to load the WebAssembly module from. */
34
+ readonly wasm?: WasmSource;
35
+ /**
36
+ * How text input arrives: `EditContext` where the browser has it
37
+ * (`"auto"`, the default), or always a hidden textarea (`"textarea"`),
38
+ * the kill switch for EditContext problems.
39
+ */
40
+ readonly input?: InputMode;
41
+ /** The editor's accessible name. Default `"Editor"`. */
42
+ readonly label?: string;
43
+ /**
44
+ * Whether people can change the document. `false` makes it read-only:
45
+ * they can still select (with the pointer and navigation keys) and copy,
46
+ * and commands and the API still change it. Default `true`; change it
47
+ * later with {@link Editor.setEditable}.
48
+ */
49
+ readonly editable?: boolean;
50
+ /**
51
+ * Text shown, dimmed, where the first line starts while the document is
52
+ * empty, and announced as the field's placeholder. Change it later with
53
+ * {@link Editor.setPlaceholder}.
54
+ */
55
+ readonly placeholder?: string;
56
+ /**
57
+ * Whether `Mod` in key bindings is Cmd (`"apple"`) or Ctrl (`"other"`);
58
+ * detected from the browser by default.
59
+ */
60
+ readonly platform?: KeyPlatform;
61
+ }
62
+ /**
63
+ * How long input took to reach the canvas: from each input event's
64
+ * timestamp until the change was painted, in milliseconds, over the most
65
+ * recent events.
66
+ */
67
+ export interface InputLatency {
68
+ readonly samples: number;
69
+ readonly p50: number;
70
+ readonly p95: number;
71
+ readonly max: number;
72
+ }
73
+ /** Measurement statistics, from {@link Editor.stats}. */
74
+ export interface EditorStats {
75
+ readonly hits: number;
76
+ readonly misses: number;
77
+ /** The share of segment lookups answered from the cache, 0 to 1. */
78
+ readonly hitRate: number;
79
+ /** Approximate bytes held by the measurement cache. */
80
+ readonly bytes: number;
81
+ readonly measuredSegments: number;
82
+ readonly measureCalls: number;
83
+ readonly fonts: readonly {
84
+ readonly font: string | null;
85
+ readonly segments: number;
86
+ readonly bytes: number;
87
+ }[];
88
+ }
89
+ /** The laid-out document, from {@link Editor.geometry}. */
90
+ export interface Geometry {
91
+ readonly height: number;
92
+ readonly blocks: readonly {
93
+ readonly id: string;
94
+ readonly rect: readonly [number, number, number, number] | null;
95
+ readonly text?: string;
96
+ /** `[utf16, downstream x, upstream x, line top, line height]` per grapheme boundary. */
97
+ readonly stops?: readonly (readonly [number, number, number, number, number])[];
98
+ }[];
99
+ readonly runs: readonly {
100
+ readonly block: string;
101
+ /** The run's UTF-16 offset in its block. */
102
+ readonly start: number;
103
+ readonly text: string;
104
+ readonly x: number;
105
+ readonly width: number;
106
+ readonly baseline: number;
107
+ /** The content area's height above the baseline. */
108
+ readonly ascent: number;
109
+ readonly font: string;
110
+ readonly rtl: boolean;
111
+ }[];
112
+ }
113
+ /** What the last frame did, for tests and profiling. */
114
+ export interface FrameReport {
115
+ readonly height: number;
116
+ readonly laidOut: number;
117
+ readonly measured: number;
118
+ readonly drawn: number;
119
+ readonly full: boolean;
120
+ readonly painted: boolean;
121
+ }
122
+ /**
123
+ * Create an editor that renders into `element`.
124
+ *
125
+ * The element becomes the editor: give it a width (the document wraps to
126
+ * its content box) and, for a scrolling editor, a height and
127
+ * `overflow: auto`. Without a height it grows with the document and the
128
+ * page scrolls. Don't put padding on it; pad a wrapper instead, as with
129
+ * server-rendered `.cv-doc`.
130
+ *
131
+ * ```ts
132
+ * const editor = await createEditor(element, {
133
+ * content: { format: "markdown", input: "# Hello" },
134
+ * });
135
+ * editor.commands.toggleBold();
136
+ * ```
137
+ */
138
+ export declare function createEditor<const E extends Extensions = StarterKit, const P extends Plugins = []>(element: HTMLElement, options?: EditorOptions<E, P>): Promise<Editor<E, P>>;
139
+ /**
140
+ * Makes an editor's WebAssembly half. `@canvist/web/collab` passes one that
141
+ * makes a build with collaboration.
142
+ *
143
+ * @internal
144
+ */
145
+ export type WebEditorFactory = (...args: ConstructorParameters<typeof WebEditor>) => WebEditor;
146
+ /** A canvas editor. Create one with {@link createEditor}. */
147
+ export declare class Editor<E extends Extensions, P extends Plugins = []> {
148
+ #private;
149
+ /** Run a registered command; returns whether it applied. */
150
+ readonly commands: Commands<Registered<E>>;
151
+ /** Whether a registered command would apply. Nothing changes. */
152
+ readonly can: Commands<Registered<E>>;
153
+ /** Answer a registered query. */
154
+ readonly query: Queries<Registered<E>>;
155
+ /** Each registered plugin's API, by name. */
156
+ readonly plugins: PluginApis<P>;
157
+ /** @internal Use {@link createEditor}, which loads the WebAssembly module first. */
158
+ constructor(element: HTMLElement, options: EditorOptions<E, P>, create?: WebEditorFactory);
159
+ /** The registered extensions, including the ones every editor has. */
160
+ get extensions(): Registered<E>;
161
+ /** The canvas the editor paints. */
162
+ get canvas(): HTMLCanvasElement;
163
+ /** What the last frame did. */
164
+ get lastFrame(): FrameReport | undefined;
165
+ /** How text input arrives in this browser: through `EditContext` or a hidden textarea. */
166
+ get inputMode(): "editcontext" | "textarea";
167
+ /** Focus the editor without scrolling. */
168
+ focus(): void;
169
+ /** Whether the editor has focus. */
170
+ get focused(): boolean;
171
+ /** Whether people can change the document; see {@link EditorOptions.editable}. */
172
+ get editable(): boolean;
173
+ /**
174
+ * Let people change the document, or make it read-only. A read-only
175
+ * editor shows no caret, and subscribers hear of the change.
176
+ */
177
+ setEditable(editable: boolean): void;
178
+ /** Show `text` while the document is empty; an empty string shows none. */
179
+ setPlaceholder(text: string): void;
180
+ /** Whether the document is a single empty text block, as a new one is. */
181
+ get isEmpty(): boolean;
182
+ /**
183
+ * The text of the text block holding the selection's head, or `null`
184
+ * when the head is not in a text block. A floating menu shows on an
185
+ * empty one.
186
+ */
187
+ blockText(): string | null;
188
+ /**
189
+ * Input-to-paint times over the most recent input events: from each
190
+ * keystroke, composition, paste, drop, or pointer event's timestamp
191
+ * until the editor painted the change.
192
+ */
193
+ inputLatency(): InputLatency;
194
+ /**
195
+ * Resolve once fonts the document uses have loaded (or failed), plugins
196
+ * have finished their setup work, and the result is painted.
197
+ */
198
+ ready(): Promise<void>;
199
+ /**
200
+ * Call `listener` after every change to the document, the selection,
201
+ * focus, or {@link Editor.editable}, from input, commands, or the API.
202
+ * Returns a function that unsubscribes. Its shape suits React's
203
+ * `useSyncExternalStore`.
204
+ */
205
+ subscribe(listener: () => void): () => void;
206
+ /** Whether server-rendered HTML still stands in for the canvas. */
207
+ get hydrating(): boolean;
208
+ /** Run the command named `name`, for callers that pick commands at runtime. */
209
+ run<N extends CommandName<Registered<E>>>(name: N, ...args: CommandParams<Registered<E>, N>): boolean;
210
+ /** Answer the query named `name`, for callers that pick queries at runtime. */
211
+ ask<N extends QueryName<Registered<E>>>(name: N, ...args: QueryParams<Registered<E>, N>): QueryResult<Registered<E>, N>;
212
+ /** Replace the document. */
213
+ setContent(content: Content<E>): void;
214
+ /** The document as `format`. */
215
+ export(format: Format): string;
216
+ /** The document as ProseMirror JSON. */
217
+ toJson(): DocJson<Registered<E>>;
218
+ /** Server-rendered HTML for the document at the editor's width, and its stylesheet. */
219
+ toHtml(): {
220
+ readonly html: string;
221
+ readonly css: string;
222
+ };
223
+ /** The whole document as SVG, painted from the same display list as the canvas. */
224
+ toSvg(): string;
225
+ /**
226
+ * Where layout put every block, line, caret stop, and text run, in
227
+ * document coordinates: what the parity tests compare with the DOM.
228
+ */
229
+ geometry(): Geometry;
230
+ /** The text selection, or `null` for a block selection. */
231
+ get selection(): Range | null;
232
+ /** Select from `anchor` to `head`, or place a caret at `anchor`. Offsets count UTF-16 code units. */
233
+ select(anchor: Position, head?: Position): void;
234
+ /** Replace the selection with `text`, as typing does (input rules apply). */
235
+ insertText(text: string): boolean;
236
+ /** Undo the last change. */
237
+ undo(): boolean;
238
+ /** Redo the last undone change. */
239
+ redo(): boolean;
240
+ /**
241
+ * Show the caret and blink it, as when the editor has focus. Subscribers
242
+ * hear of the change.
243
+ */
244
+ setFocused(focused: boolean): void;
245
+ /** The caret's rectangle in document coordinates, or `null` when the selection is a range. */
246
+ caretRect(): DOMRect | null;
247
+ /**
248
+ * The rectangle of a caret at `position` in document coordinates, or
249
+ * `null` when the document has no such position or it is scrolled too far
250
+ * away to be laid out. Collaborators' carets are drawn with it: positions
251
+ * from another peer may not exist here yet.
252
+ */
253
+ caretRectAt(position: Position): DOMRect | null;
254
+ /**
255
+ * The smallest rectangle around the selection in document coordinates:
256
+ * the caret's for a caret, or `null` while the selection is scrolled too
257
+ * far away to be laid out. Pass it to {@link Editor.toClientRect} to
258
+ * anchor a menu, or use `@canvist/web/menus`.
259
+ */
260
+ selectionRect(): DOMRect | null;
261
+ /** `rect`, in document coordinates, in the viewport's coordinates, as `getBoundingClientRect` reports them. */
262
+ toClientRect(rect: DOMRectReadOnly): DOMRect;
263
+ /** Measurement cache statistics. */
264
+ stats(): EditorStats;
265
+ /** The CSS `font` of every font the layout has used. */
266
+ fonts(): string[];
267
+ /**
268
+ * The document changed outside this editor's own calls, as a
269
+ * collaborator's change does: repaint, and tell subscribers.
270
+ *
271
+ * @internal Called by `@canvist/web/collab`.
272
+ */
273
+ documentChanged(): void;
274
+ /** Lay out and paint now instead of on the next animation frame. */
275
+ render(): FrameReport | undefined;
276
+ /** Remove the editor's elements and listeners and release its memory. */
277
+ destroy(): void;
278
+ [Symbol.dispose](): void;
279
+ }
280
+ /**
281
+ * The grid this browser snaps DOM text baselines and backgrounds to.
282
+ *
283
+ * Firefox and WebKit snap to device pixels. Chromium does too when it lays
284
+ * out in device units, as it does on real high-density screens; when it
285
+ * lays out in CSS units (as under device emulation), it snaps to whole CSS
286
+ * pixels and scales. Layout in device units can represent a box 1/64 of a
287
+ * device pixel wide, which CSS-unit layout rounds to zero.
288
+ */
289
+ export declare function pixelGrid(document: Document, userAgent: string): "device" | "css";
290
+ /**
291
+ * How many units per CSS pixel this browser snaps border widths down to: the
292
+ * device pixel ratio, 1 when it lays out in CSS pixels, or `null` when it
293
+ * keeps exact widths. A 1.7 px border tells them apart.
294
+ */
295
+ export declare function borderScale(document: Document): number | null;
296
+ /**
297
+ * The line-breaking tailoring of the browser running this code. CSS leaves
298
+ * the details to each engine and there is no feature test for them, so this
299
+ * reads the user agent.
300
+ */
301
+ export declare function detectBreaks(userAgent: string): LineBreaks;