@twip/web 0.0.0-canary-f16fa12

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 (82) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +35 -0
  3. package/dist/THIRD-PARTY-NOTICES.md +5457 -0
  4. package/dist/commands.d.ts +637 -0
  5. package/dist/commands.js +188 -0
  6. package/dist/editor.d.ts +218 -0
  7. package/dist/editor.js +460 -0
  8. package/dist/errors.d.ts +21 -0
  9. package/dist/errors.js +26 -0
  10. package/dist/events.d.ts +126 -0
  11. package/dist/events.js +1 -0
  12. package/dist/fonts/google.d.ts +3 -0
  13. package/dist/fonts/google.js +39 -0
  14. package/dist/fonts/library.d.ts +65 -0
  15. package/dist/fonts/library.js +130 -0
  16. package/dist/host/a11y.d.ts +3 -0
  17. package/dist/host/a11y.js +62 -0
  18. package/dist/host/chrome.d.ts +28 -0
  19. package/dist/host/chrome.js +144 -0
  20. package/dist/host/clipboard.d.ts +16 -0
  21. package/dist/host/clipboard.js +128 -0
  22. package/dist/host/cursors.d.ts +15 -0
  23. package/dist/host/cursors.js +5 -0
  24. package/dist/host/dom.d.ts +45 -0
  25. package/dist/host/dom.js +122 -0
  26. package/dist/host/frames.d.ts +14 -0
  27. package/dist/host/frames.js +87 -0
  28. package/dist/host/gpu.d.ts +58 -0
  29. package/dist/host/gpu.js +79 -0
  30. package/dist/host/ime.d.ts +3 -0
  31. package/dist/host/ime.js +11 -0
  32. package/dist/host/input.d.ts +12 -0
  33. package/dist/host/input.js +86 -0
  34. package/dist/host/keys.d.ts +16 -0
  35. package/dist/host/keys.js +27 -0
  36. package/dist/host/mount.d.ts +11 -0
  37. package/dist/host/mount.js +72 -0
  38. package/dist/host/pointer.d.ts +4 -0
  39. package/dist/host/pointer.js +196 -0
  40. package/dist/host/scroll.d.ts +14 -0
  41. package/dist/host/scroll.js +95 -0
  42. package/dist/index.d.ts +27 -0
  43. package/dist/index.js +10 -0
  44. package/dist/review.d.ts +156 -0
  45. package/dist/review.js +38 -0
  46. package/dist/services/accessibility.d.ts +52 -0
  47. package/dist/services/accessibility.js +71 -0
  48. package/dist/services/dialogs.d.ts +181 -0
  49. package/dist/services/dialogs.js +18 -0
  50. package/dist/services/elements.d.ts +146 -0
  51. package/dist/services/elements.js +18 -0
  52. package/dist/services/find.d.ts +50 -0
  53. package/dist/services/find.js +12 -0
  54. package/dist/services/menus.d.ts +82 -0
  55. package/dist/services/menus.js +8 -0
  56. package/dist/services/navigation.d.ts +58 -0
  57. package/dist/services/navigation.js +12 -0
  58. package/dist/services/ruler.d.ts +95 -0
  59. package/dist/services/ruler.js +16 -0
  60. package/dist/services/spelling.d.ts +34 -0
  61. package/dist/services/spelling.js +14 -0
  62. package/dist/services/view.d.ts +70 -0
  63. package/dist/services/view.js +19 -0
  64. package/dist/state.d.ts +483 -0
  65. package/dist/state.js +1 -0
  66. package/dist/transport/direct.d.ts +31 -0
  67. package/dist/transport/direct.js +405 -0
  68. package/dist/transport/module.d.ts +51 -0
  69. package/dist/transport/module.js +17 -0
  70. package/dist/transport/protocol.d.ts +265 -0
  71. package/dist/transport/protocol.js +132 -0
  72. package/dist/transport/worker.d.ts +54 -0
  73. package/dist/transport/worker.js +203 -0
  74. package/dist/twip-wasm.js +2 -0
  75. package/dist/twip_wasm.wasm +0 -0
  76. package/dist/types.d.ts +134 -0
  77. package/dist/types.js +16 -0
  78. package/dist/worker/entry.d.ts +1 -0
  79. package/dist/worker/entry.js +53 -0
  80. package/dist/worker.d.ts +1 -0
  81. package/dist/worker.js +2 -0
  82. package/package.json +30 -0
package/dist/editor.js ADDED
@@ -0,0 +1,460 @@
1
+ import { createCommands } from './commands.js';
2
+ import { EditorError } from './errors.js';
3
+ import { FontLibrary, useEngine } from './fonts/library.js';
4
+ import { claim } from './host/dom.js';
5
+ import { mountHost } from './host/mount.js';
6
+ import { createMountedReview, createReview } from './review.js';
7
+ import { createAccessibility, createShortcuts } from './services/accessibility.js';
8
+ import { createDialogs } from './services/dialogs.js';
9
+ import { createDataParts, createElements } from './services/elements.js';
10
+ import { createFind } from './services/find.js';
11
+ import { createMenus } from './services/menus.js';
12
+ import { createNavigation, createStyles } from './services/navigation.js';
13
+ import { createRuler } from './services/ruler.js';
14
+ import { createSpelling } from './services/spelling.js';
15
+ import { createGestures } from './services/view.js';
16
+ import { directTransport } from './transport/direct.js';
17
+ import { chooseExecution, workerTransport } from './transport/worker.js';
18
+ import { decodeEvent, Kind, Method } from './transport/protocol.js';
19
+ function destroyed() {
20
+ return new EditorError('destroyed', 'the editor was destroyed');
21
+ }
22
+ function aborted() {
23
+ return new EditorError('aborted', 'the operation was aborted');
24
+ }
25
+ /** Bytes as standard base64, as the engine takes them. */
26
+ function base64(bytes) {
27
+ let s = '';
28
+ for (let i = 0; i < bytes.length; i += 0x8000)
29
+ s += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
30
+ return btoa(s);
31
+ }
32
+ /** A request in the engine's JSON shape: bytes travel as base64. */
33
+ export function toWire(value) {
34
+ if (value instanceof Uint8Array)
35
+ return base64(value);
36
+ if (Array.isArray(value))
37
+ return value.map(toWire);
38
+ if (value && typeof value === 'object')
39
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, toWire(v)]));
40
+ return value;
41
+ }
42
+ function deepFreeze(value) {
43
+ if (value && typeof value === 'object' && !Object.isFrozen(value)) {
44
+ Object.freeze(value);
45
+ for (const v of Object.values(value))
46
+ deepFreeze(v);
47
+ }
48
+ return value;
49
+ }
50
+ /** One editor's session over a transport: the state cache, the listeners and the calls. Internal. */
51
+ export class Session {
52
+ transport;
53
+ id;
54
+ state;
55
+ catalog;
56
+ destroyed = false;
57
+ #subscribers = new Set();
58
+ #listeners = new Map();
59
+ /** Mounted hosts listen for frame and host requests here. */
60
+ hostListeners = new Set();
61
+ /** A mounted host's view of an engine event before it is emitted: its viewport points become client points. */
62
+ adjust;
63
+ /** Run once on destroy, last added first. */
64
+ disposers = [];
65
+ #closing;
66
+ /** Set by a fatal `error` event: later operations reject with it. */
67
+ #fault;
68
+ /** Commits waiting for the state cache to reach their revision. */
69
+ #waiting = [];
70
+ constructor(transport, id, state, catalog) {
71
+ this.transport = transport;
72
+ this.id = id;
73
+ this.state = deepFreeze(state);
74
+ this.catalog = deepFreeze(catalog);
75
+ this.disposers.push(transport.listen(id, (m) => this.#receive(m)));
76
+ }
77
+ /** Creates a session on the page's engine: on this thread, or in the page's engine worker. */
78
+ static async start(options = {}, execution = 'main') {
79
+ const { readOnly, mode, author, language, trackChanges } = options;
80
+ const transport = execution === 'worker' ? workerTransport() : await directTransport();
81
+ useEngine(transport);
82
+ const id = await transport.createSession({ readOnly, mode, author, language, trackChanges });
83
+ try {
84
+ const state = (await transport.call(id, Method.State));
85
+ const catalog = (await transport.call(id, Method.Catalog).catch((e) => {
86
+ // shortcut: an engine without the catalog call gives an empty catalog; drop once every engine has it.
87
+ if (e instanceof EditorError && e.code === 'unsupported')
88
+ return [];
89
+ throw e;
90
+ }));
91
+ const session = new Session(transport, id, state, catalog);
92
+ for (const dictionary of options.dictionaries ?? [])
93
+ await session.call(Method.SpellingAddDictionary, dictionary);
94
+ return session;
95
+ }
96
+ catch (e) {
97
+ await transport.closeSession(id).catch(() => { });
98
+ throw e;
99
+ }
100
+ }
101
+ /** Rejects `destroyed` once destroyed, or with the fault after one, else runs `call`. */
102
+ guard(call) {
103
+ if (this.destroyed)
104
+ return Promise.reject(destroyed());
105
+ return this.#fault ? Promise.reject(this.#fault) : call();
106
+ }
107
+ /** A structured call, with the request in the engine's JSON shape. */
108
+ call(method, request) {
109
+ return this.guard(() => this.transport.call(this.id, method, toWire(request)));
110
+ }
111
+ /** A call that commits: resolves with its value once the state cache holds the revision it committed. */
112
+ async commit(method, request) {
113
+ const commit = await this.call(method, request);
114
+ const revision = commit?.record?.revision;
115
+ if (revision !== undefined && this.state.revision < revision && !this.destroyed) {
116
+ await new Promise((resolve) => this.#waiting.push({ revision, resolve }));
117
+ }
118
+ return (commit?.value ?? undefined);
119
+ }
120
+ run(op) {
121
+ return this.commit(Method.Run, op);
122
+ }
123
+ input(input) {
124
+ return this.guard(() => this.transport.input(this.id, input));
125
+ }
126
+ frame(width, height) {
127
+ return this.guard(() => this.transport.frame(this.id, width, height));
128
+ }
129
+ subscribe(listener) {
130
+ this.#subscribers.add(listener);
131
+ this.#isolate(() => listener(this.state));
132
+ return () => this.#subscribers.delete(listener);
133
+ }
134
+ on(type, listener) {
135
+ let set = this.#listeners.get(type);
136
+ if (!set)
137
+ this.#listeners.set(type, (set = new Set()));
138
+ set.add(listener);
139
+ return () => set.delete(listener);
140
+ }
141
+ /** Whether anything listens to `type`. */
142
+ listening(type) {
143
+ return (this.#listeners.get(type)?.size ?? 0) > 0;
144
+ }
145
+ emit(type, event) {
146
+ const listeners = [...(this.#listeners.get(type) ?? [])];
147
+ if (type === 'error' && !listeners.length)
148
+ console.error(event);
149
+ for (const listener of listeners) {
150
+ if (this.destroyed)
151
+ return;
152
+ if (type === 'error') {
153
+ try {
154
+ listener(event);
155
+ }
156
+ catch (e) {
157
+ globalThis.reportError?.(e);
158
+ }
159
+ }
160
+ else
161
+ this.#isolate(() => listener(event));
162
+ }
163
+ }
164
+ destroy() {
165
+ return (this.#closing ??= (async () => {
166
+ this.destroyed = true;
167
+ for (const { resolve } of this.#waiting.splice(0))
168
+ resolve();
169
+ for (const dispose of this.disposers.splice(0).reverse())
170
+ this.#isolate(dispose);
171
+ await this.transport.closeSession(this.id).catch(() => { });
172
+ })());
173
+ }
174
+ /** Runs application code: an exception is reported on `error`, never thrown into the engine or other listeners. */
175
+ #isolate(code) {
176
+ try {
177
+ code();
178
+ }
179
+ catch (cause) {
180
+ this.emit('error', new EditorError('internal', `a listener threw: ${String(cause)}`, { cause }));
181
+ }
182
+ }
183
+ #receive(message) {
184
+ if (this.destroyed)
185
+ return;
186
+ switch (message.kind) {
187
+ case Kind.State: {
188
+ this.state = deepFreeze(message.payload);
189
+ const revision = this.state.revision;
190
+ for (const w of this.#waiting)
191
+ if (w.revision <= revision)
192
+ w.resolve();
193
+ this.#waiting = this.#waiting.filter((w) => w.revision > revision);
194
+ for (const listener of [...this.#subscribers])
195
+ if (!this.destroyed)
196
+ this.#isolate(() => listener(this.state));
197
+ break;
198
+ }
199
+ case Kind.Event: {
200
+ const { type, event } = decodeEvent(message.payload);
201
+ if (event instanceof EditorError && event.fatal)
202
+ this.#fault = event;
203
+ const adjusted = this.adjust && !(event instanceof EditorError) ? this.adjust(type, event) : event;
204
+ this.emit(type, (adjusted instanceof EditorError ? adjusted : deepFreeze(adjusted)));
205
+ break;
206
+ }
207
+ default:
208
+ for (const listener of [...this.hostListeners])
209
+ if (!this.destroyed)
210
+ this.#isolate(() => listener(message));
211
+ }
212
+ }
213
+ }
214
+ const sessions = new WeakMap();
215
+ let constructing;
216
+ /** The session behind an editor. Internal: for the SDK's own hosts and tests. */
217
+ export function sessionOf(editor) {
218
+ const session = sessions.get(editor);
219
+ if (!session)
220
+ throw new TypeError('not an editor');
221
+ return session;
222
+ }
223
+ /** Constructs an editor over `session`. */
224
+ function build(session, make) {
225
+ constructing = session;
226
+ try {
227
+ return make();
228
+ }
229
+ finally {
230
+ constructing = undefined;
231
+ }
232
+ }
233
+ /** A session with no DOM: Node, workers, tests and servers. */
234
+ export class HeadlessEditor {
235
+ /** Every edit, one method per command. */
236
+ commands;
237
+ /** Every command's id, label, group, shortcuts and undo grouping. */
238
+ catalog;
239
+ /** The context menu. */
240
+ menus;
241
+ /** The dialogs. */
242
+ dialogs;
243
+ /** Find and replace. */
244
+ find;
245
+ /** Spell checking. */
246
+ spelling;
247
+ /** The document's headings. */
248
+ navigation;
249
+ /** The document's styles. */
250
+ styles;
251
+ /** Tracked changes and comments. */
252
+ review;
253
+ /** The merge designer's elements. */
254
+ elements;
255
+ /** The custom XML data parts. */
256
+ data;
257
+ /** The ruler model. */
258
+ ruler;
259
+ /** The accessibility tree. */
260
+ accessibility;
261
+ /** Keyboard shortcuts. */
262
+ shortcuts;
263
+ /** Editors are made by {@link HeadlessEditor.create} and {@link Editor.mount}. */
264
+ constructor() {
265
+ const session = constructing;
266
+ if (!session)
267
+ throw new TypeError('use HeadlessEditor.create() or Editor.mount()');
268
+ constructing = undefined;
269
+ sessions.set(this, session);
270
+ this.commands = createCommands((op) => session.run(op));
271
+ this.catalog = session.catalog;
272
+ this.menus = createMenus(session);
273
+ this.dialogs = createDialogs(session);
274
+ this.find = createFind(session);
275
+ this.spelling = createSpelling(session);
276
+ this.navigation = createNavigation(session);
277
+ this.styles = createStyles(session);
278
+ this.review = createReview(session);
279
+ this.elements = createElements(session);
280
+ this.data = createDataParts(session);
281
+ this.ruler = { model: () => session.state.ruler };
282
+ this.accessibility = createAccessibility(session);
283
+ this.shortcuts = createShortcuts(session);
284
+ }
285
+ /** Creates a session with a blank document. */
286
+ static async create(options) {
287
+ const session = await Session.start(options);
288
+ return build(session, () => new HeadlessEditor());
289
+ }
290
+ /** Replaces the document, only after it was read successfully. Resolves with the read's warnings. */
291
+ open(bytes, options) {
292
+ const s = sessionOf(this);
293
+ const signal = options?.signal;
294
+ return s.guard(async () => {
295
+ if (signal?.aborted)
296
+ throw aborted();
297
+ const token = await s.transport.openPrepare(s.id, bytes);
298
+ // A task between the read and the replace, for an abort to land in.
299
+ await new Promise((resolve) => setTimeout(resolve));
300
+ if (signal?.aborted) {
301
+ await s.transport.openDiscard(s.id, token).catch(() => { });
302
+ throw aborted();
303
+ }
304
+ return { warnings: await s.guard(() => s.transport.openCommit(s.id, token)) };
305
+ });
306
+ }
307
+ /** Replaces the document with a blank one. */
308
+ newDocument() {
309
+ return sessionOf(this).call(Method.NewDocument);
310
+ }
311
+ /** Exports the document as DOCX or PDF, with the revision it captures. */
312
+ export(options) {
313
+ const s = sessionOf(this);
314
+ const { format, signal } = options;
315
+ return s.guard(async () => {
316
+ const result = await s.transport.export(s.id, { format }, signal);
317
+ if (signal?.aborted)
318
+ throw aborted();
319
+ return result;
320
+ });
321
+ }
322
+ /** Runs several commands atomically: one transaction and one undo step, or nothing when one fails. */
323
+ batch(ops, options) {
324
+ return sessionOf(this).commit(Method.Batch, { commands: ops, label: options?.label });
325
+ }
326
+ /** The latest state snapshot: synchronous and immutable. */
327
+ getState() {
328
+ return sessionOf(this).state;
329
+ }
330
+ /** Calls `listener` with the current state now, then after every change. Returns the unsubscribe function. */
331
+ subscribe(listener) {
332
+ return sessionOf(this).subscribe(listener);
333
+ }
334
+ /** Listens to an event. Returns the unsubscribe function. */
335
+ on(type, listener) {
336
+ return sessionOf(this).on(type, listener);
337
+ }
338
+ /** Makes the editor read only, or editable again, without reopening. */
339
+ setReadOnly(value) {
340
+ return this.#set(Method.SetReadOnly, value);
341
+ }
342
+ /** Switches between editing and design mode. */
343
+ setMode(mode) {
344
+ return this.#set(Method.SetMode, mode);
345
+ }
346
+ /** Sets who makes the next changes. */
347
+ setAuthor(author) {
348
+ return this.#set(Method.SetAuthor, author);
349
+ }
350
+ /** Counts words, characters, paragraphs, lines and pages. */
351
+ wordCount() {
352
+ return sessionOf(this).call(Method.WordCount);
353
+ }
354
+ /** Releases the editor. Idempotent; later calls reject `destroyed`. */
355
+ destroy() {
356
+ return sessionOf(this).destroy();
357
+ }
358
+ /** A setting: a structured call whose response is `null`. */
359
+ async #set(method, value) {
360
+ await sessionOf(this).call(method, value);
361
+ }
362
+ }
363
+ /** An editor mounted in a page: it owns input, rendering, scrolling and its DOM inside the container. */
364
+ export class Editor extends HeadlessEditor {
365
+ /** The element the editor was mounted in. */
366
+ container;
367
+ /** Where the engine runs. */
368
+ executionMode;
369
+ /** The rulers: the model, the built-in rulers and the viewport mapping. */
370
+ ruler;
371
+ /** Review, with the markup pane and balloons. */
372
+ review;
373
+ /** Elements, with drop targets and hit testing. */
374
+ elements;
375
+ /** The view. */
376
+ view;
377
+ /** The pointer gestures. */
378
+ gestures;
379
+ #focus;
380
+ /** Editors are made by {@link Editor.mount}. */
381
+ constructor() {
382
+ const session = constructing;
383
+ super();
384
+ const { container, host, rulerView, gestures } = mounting;
385
+ this.container = container;
386
+ this.executionMode = mounting.execution;
387
+ this.#focus = host.focus;
388
+ this.ruler = createRuler(session, rulerView);
389
+ this.review = Object.assign(createReview(session), createMountedReview(session), {
390
+ setMarkupPaneWidth: host.setMarkupPaneWidth,
391
+ balloonLayer: host.balloonLayer,
392
+ attachBalloon: host.attachBalloon,
393
+ });
394
+ const { hitTest, dragOver, dragLeave, drop } = host;
395
+ this.elements = Object.assign(createElements(session), { hitTest, dragOver, dragLeave, drop });
396
+ this.view = host.view;
397
+ this.gestures = createGestures(session, gestures);
398
+ // A paste from the system clipboard is the host's: the browser's clipboard is read here.
399
+ this.commands = Object.freeze({ ...this.commands, paste: host.paste });
400
+ // So is the context menu's Paste.
401
+ const { invoke } = this.menus;
402
+ this.menus = Object.freeze({
403
+ ...this.menus,
404
+ invoke: (action) => {
405
+ const command = action.command;
406
+ return command?.type === 'paste' ? host.paste(command.as ? { as: command.as } : undefined) : invoke(action);
407
+ },
408
+ });
409
+ }
410
+ /** Mounts an editor in `container`, which must be empty of other editors. Resolves when it accepts commands. */
411
+ static async mount(container, options = {}) {
412
+ const release = claim(container);
413
+ let session;
414
+ try {
415
+ const execution = chooseExecution(options.execution);
416
+ const abort = () => {
417
+ if (options.signal?.aborted)
418
+ throw new EditorError('aborted', 'the mount was aborted');
419
+ };
420
+ abort();
421
+ session = await Session.start(options, execution);
422
+ const s = session;
423
+ s.disposers.push(release);
424
+ abort();
425
+ // The shortcuts the editor claims, read through the editor's own lookup once it exists.
426
+ let lookup;
427
+ const host = await mountHost(container, s, options, (keys) => lookup?.(keys) != null);
428
+ const [rulerView, gestures] = await Promise.all([s.call(Method.HostRulerView), s.call(Method.GesturesBindings)]);
429
+ mounting = { container, execution, host, rulerView: s.adjust?.('rulerChanged', rulerView), gestures };
430
+ const editor = build(s, () => new Editor());
431
+ lookup = editor.shortcuts.lookup;
432
+ const { rulers, rulerUnit } = options;
433
+ if (rulers)
434
+ await editor.ruler.setVisible(rulers === true ? { horizontal: true, vertical: true } : rulers);
435
+ if (rulerUnit)
436
+ await editor.ruler.setUnit(rulerUnit);
437
+ if (options.document)
438
+ await editor.open(options.document, { signal: options.signal });
439
+ abort();
440
+ await host.ready();
441
+ abort();
442
+ return editor;
443
+ }
444
+ catch (e) {
445
+ if (session)
446
+ await session.destroy();
447
+ else
448
+ release();
449
+ throw e;
450
+ }
451
+ finally {
452
+ mounting = undefined;
453
+ }
454
+ }
455
+ /** Focuses the editor, keeping the selection. Mounting never takes focus by itself. */
456
+ focus() {
457
+ this.#focus();
458
+ }
459
+ }
460
+ let mounting;
@@ -0,0 +1,21 @@
1
+ /** The stable code of an {@link EditorError}. */
2
+ export type ErrorCode = 'destroyed' | 'invalid-argument' | 'unavailable' | 'read-only' | 'import-failed' | 'export-failed' | 'aborted' | 'not-allowed' | 'unsupported' | 'internal';
3
+ /** Why an operation failed. Every rejected promise of the API rejects with one. */
4
+ export declare class EditorError extends Error {
5
+ /** The stable code: branch on this, not on the message. */
6
+ readonly code: ErrorCode;
7
+ /** Structured details, for some codes. */
8
+ readonly details?: unknown;
9
+ /** A fault: the editor rejects new operations and stays destroyable. */
10
+ readonly fatal: boolean;
11
+ /** Creates an error. Applications don't usually need to. */
12
+ constructor(code: ErrorCode, message: string, options?: {
13
+ details?: unknown;
14
+ fatal?: boolean;
15
+ cause?: unknown;
16
+ });
17
+ }
18
+ /** The error a member outside what this build implements rejects or throws with. */
19
+ export declare function unsupported(what: string): EditorError;
20
+ /** A rejected promise with {@link unsupported}. */
21
+ export declare function rejectUnsupported(what: string): Promise<never>;
package/dist/errors.js ADDED
@@ -0,0 +1,26 @@
1
+ /** Why an operation failed. Every rejected promise of the API rejects with one. */
2
+ export class EditorError extends Error {
3
+ /** The stable code: branch on this, not on the message. */
4
+ code;
5
+ /** Structured details, for some codes. */
6
+ details;
7
+ /** A fault: the editor rejects new operations and stays destroyable. */
8
+ fatal;
9
+ /** Creates an error. Applications don't usually need to. */
10
+ constructor(code, message, options) {
11
+ super(message, options?.cause === undefined ? undefined : { cause: options.cause });
12
+ this.name = 'EditorError';
13
+ this.code = code;
14
+ if (options?.details !== undefined)
15
+ this.details = options.details;
16
+ this.fatal = options?.fatal ?? false;
17
+ }
18
+ }
19
+ /** The error a member outside what this build implements rejects or throws with. */
20
+ export function unsupported(what) {
21
+ return new EditorError('unsupported', `${what} is not supported yet`);
22
+ }
23
+ /** A rejected promise with {@link unsupported}. */
24
+ export function rejectUnsupported(what) {
25
+ return Promise.reject(unsupported(what));
26
+ }
@@ -0,0 +1,126 @@
1
+ import type { EditorError } from './errors.js';
2
+ import type { BalloonPlacement, CommentAnchor } from './review.js';
3
+ import type { DialogKind } from './services/dialogs.js';
4
+ import type { ElementInfo } from './services/elements.js';
5
+ import type { ContextMenuEvent } from './services/menus.js';
6
+ import type { RulerView } from './services/ruler.js';
7
+ import type { GestureEvent } from './services/view.js';
8
+ import type { SelectionSummary } from './state.js';
9
+ import type { Author, ClientPoint, ClientRect, CommentId, ElementId, RevisionId, Warning } from './types.js';
10
+ /** Every event of `editor.on(type, listener)`, keyed by type. */
11
+ export interface TwipEvents {
12
+ /** A transaction committed: exactly one per commit, in order, never coalesced. */
13
+ documentChanged: TransactionRecord;
14
+ /** The selection changed; coalesced per frame. */
15
+ selectionChanged: SelectionSummary;
16
+ /** A link was activated: a click when read only, Ctrl+click otherwise. */
17
+ linkActivated: {
18
+ address: string;
19
+ via: 'ctrlClick' | 'click';
20
+ };
21
+ /** An unfilled picture placeholder was clicked. */
22
+ placeholderActivated: {
23
+ key: string;
24
+ element: ElementId | null;
25
+ };
26
+ /** The user asked for a context menu. */
27
+ contextMenu: ContextMenuEvent;
28
+ /** A key or gesture asked for a dialog. */
29
+ dialogRequested: {
30
+ kind: DialogKind;
31
+ };
32
+ /** A key the application owns was pressed. */
33
+ hostCommand: {
34
+ command: HostCommand;
35
+ };
36
+ /** An application shortcut from `shortcuts.register` was pressed. */
37
+ shortcut: {
38
+ id: string;
39
+ };
40
+ /** Mounted only: a gesture bound to `'emit'`. */
41
+ gesture: GestureEvent;
42
+ /** A word was added to the user's dictionary: persist it. */
43
+ wordAdded: {
44
+ word: string;
45
+ language: string;
46
+ };
47
+ /** Fonts started or finished loading, or could not be found. */
48
+ fontsChanged: {
49
+ pending: string[];
50
+ unresolved: string[];
51
+ };
52
+ /** Design mode: Enter or double-click on an element. */
53
+ elementActivated: {
54
+ id: ElementId;
55
+ };
56
+ /** Elements were added, removed or changed. */
57
+ elementsChanged: {
58
+ added: ElementInfo[];
59
+ removed: ElementId[];
60
+ changed: ElementInfo[];
61
+ };
62
+ /** Fields the application computes need results: answer with `setFieldResults`. */
63
+ fieldsUpdateRequested: {
64
+ token: number;
65
+ fields: {
66
+ id: ElementId;
67
+ code: string;
68
+ }[];
69
+ };
70
+ /** The tracked revisions changed. */
71
+ revisionsChanged: {};
72
+ /** The comments changed. */
73
+ commentsChanged: {};
74
+ /** Mounted only: the visible comments, after layout, zoom or scroll. */
75
+ commentAnchors: CommentAnchor[];
76
+ /** Mounted only: the SDK placed the attached balloons. */
77
+ balloonsLaidOut: BalloonPlacement[];
78
+ /** Mounted only: the rulers changed; coalesced per frame. */
79
+ rulerChanged: RulerView;
80
+ /** Mounted only: hover over a link, element mark, revision or comment, or `null` when it ends. */
81
+ tooltip: Tooltip | null;
82
+ /** What the editor said to screen readers. */
83
+ announcement: {
84
+ text: string;
85
+ };
86
+ /** A fidelity warning outside an operation. */
87
+ warning: Warning;
88
+ /** A listener's exception, or a fault (then `fatal` is true). */
89
+ error: EditorError;
90
+ }
91
+ /** A committed transaction. */
92
+ export interface TransactionRecord {
93
+ /** Unique per session. */
94
+ id: number;
95
+ /** The revision it committed. */
96
+ revision: number;
97
+ /** Who started it: the user's input, an API call, undo or redo. */
98
+ origin: 'user' | 'api' | 'undo' | 'redo';
99
+ /** The session's author at the time. */
100
+ author: Author;
101
+ /** When, ISO 8601 UTC. */
102
+ time: string;
103
+ /** The undo label. */
104
+ label: string;
105
+ /** What it touched. */
106
+ summary: {
107
+ paragraphs: number;
108
+ tables: number;
109
+ elements: ElementId[];
110
+ revisions: RevisionId[];
111
+ comments: CommentId[];
112
+ };
113
+ }
114
+ /** A key the application owns. */
115
+ export type HostCommand = 'save' | 'saveAs' | 'print' | 'open' | 'new' | 'find' | 'replace' | 'goTo' | 'insertHyperlink' | 'nextMisspelling' | 'focusFont' | 'focusFontSize' | 'help';
116
+ /** A hover tooltip to show. */
117
+ export interface Tooltip {
118
+ /** What is hovered. */
119
+ kind: 'link' | 'mark' | 'revision' | 'comment';
120
+ /** The text to show. */
121
+ text: string;
122
+ /** The pointer. */
123
+ at: ClientPoint;
124
+ /** The hovered thing's box. */
125
+ rect: ClientRect;
126
+ }
package/dist/events.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { FaceInfo, FontSource } from './library.js';
2
+ /** A source that adds a family's faces from Google Fonts, each family fetched once; `families` limits it to those. */
3
+ export declare function googleFonts(add: (url: string, signal: AbortSignal) => Promise<FaceInfo[]>, families?: string[]): FontSource;
@@ -0,0 +1,39 @@
1
+ /** The styles asked for, most first: a family without italics or weights fails the fuller requests. */
2
+ // shortcut: only regular, bold, italic and bold italic are requested
3
+ const styles = [':ital,wght@0,400;0,700;1,400;1,700', ':wght@400;700', ''];
4
+ /** A source that adds a family's faces from Google Fonts, each family fetched once; `families` limits it to those. */
5
+ export function googleFonts(add, families) {
6
+ const only = families && new Set(families.map((f) => f.toLowerCase()));
7
+ const loads = new Map();
8
+ return (request, signal) => {
9
+ const key = request.family.toLowerCase();
10
+ if (only && !only.has(key))
11
+ return Promise.resolve(false);
12
+ let load = loads.get(key);
13
+ if (!load) {
14
+ loads.set(key, (load = fetchFamily(request.family, add, signal)));
15
+ load.catch(() => loads.delete(key));
16
+ }
17
+ return load;
18
+ };
19
+ }
20
+ /** Adds every face of `family` the stylesheet names; false when Google doesn't have the family. */
21
+ async function fetchFamily(family, add, signal) {
22
+ const name = encodeURIComponent(family).replace(/%20/g, '+');
23
+ for (const style of styles) {
24
+ // Google answers a family it doesn't have with a 400 that has no CORS header, so in a browser the fetch rejects:
25
+ // a miss, not an error.
26
+ const css = await fetch(`https://fonts.googleapis.com/css2?family=${name}${style}`, { signal }).catch((e) => {
27
+ if (signal.aborted)
28
+ throw e;
29
+ });
30
+ if (!css)
31
+ return false;
32
+ if (!css.ok)
33
+ continue;
34
+ const urls = [...(await css.text()).matchAll(/src:\s*url\((['"]?)([^'")]+)\1\)/g)].map((m) => m[2]);
35
+ const faces = await Promise.all([...new Set(urls)].map((url) => add(url, signal)));
36
+ return faces.flat().length > 0;
37
+ }
38
+ return false;
39
+ }