@soeditor/core 0.5.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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -0
  3. package/dist/commands/command-collection.d.ts +21 -0
  4. package/dist/commands/command-collection.d.ts.map +1 -0
  5. package/dist/commands/command-registry.d.ts +2 -0
  6. package/dist/commands/command-registry.d.ts.map +1 -0
  7. package/dist/commands/command.d.ts +20 -0
  8. package/dist/commands/command.d.ts.map +1 -0
  9. package/dist/config/config.d.ts +16 -0
  10. package/dist/config/config.d.ts.map +1 -0
  11. package/dist/editor/editor.d.ts +75 -0
  12. package/dist/editor/editor.d.ts.map +1 -0
  13. package/dist/errors/errors.d.ts +70 -0
  14. package/dist/errors/errors.d.ts.map +1 -0
  15. package/dist/events/core-events.d.ts +63 -0
  16. package/dist/events/core-events.d.ts.map +1 -0
  17. package/dist/events/event-bus.d.ts +16 -0
  18. package/dist/events/event-bus.d.ts.map +1 -0
  19. package/dist/index.d.ts +19 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +1132 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/internal/value.d.ts +2 -0
  24. package/dist/internal/value.d.ts.map +1 -0
  25. package/dist/plugins/plugin-collection.d.ts +13 -0
  26. package/dist/plugins/plugin-collection.d.ts.map +1 -0
  27. package/dist/plugins/plugin-manager.d.ts +2 -0
  28. package/dist/plugins/plugin-manager.d.ts.map +1 -0
  29. package/dist/plugins/plugin.d.ts +35 -0
  30. package/dist/plugins/plugin.d.ts.map +1 -0
  31. package/dist/services/service-collection.d.ts +31 -0
  32. package/dist/services/service-collection.d.ts.map +1 -0
  33. package/dist/services/service-registry.d.ts +2 -0
  34. package/dist/services/service-registry.d.ts.map +1 -0
  35. package/dist/state/document.d.ts +14 -0
  36. package/dist/state/document.d.ts.map +1 -0
  37. package/dist/state/editor-state.d.ts +15 -0
  38. package/dist/state/editor-state.d.ts.map +1 -0
  39. package/dist/transaction/operation.d.ts +18 -0
  40. package/dist/transaction/operation.d.ts.map +1 -0
  41. package/dist/transaction/transaction.d.ts +36 -0
  42. package/dist/transaction/transaction.d.ts.map +1 -0
  43. package/package.json +48 -0
package/dist/index.js ADDED
@@ -0,0 +1,1132 @@
1
+ class SoEditorError extends Error {
2
+ constructor(message, options) {
3
+ super(message, options);
4
+ this.name = new.target.name;
5
+ }
6
+ }
7
+ class EditorDestroyedError extends SoEditorError {
8
+ constructor() {
9
+ super("The editor has been destroyed and can no longer be used.");
10
+ }
11
+ }
12
+ class EditorInitializationAbortedError extends SoEditorError {
13
+ constructor() {
14
+ super(
15
+ "Editor initialization was aborted because destruction began before startup completed."
16
+ );
17
+ }
18
+ }
19
+ class ReentrantDispatchError extends SoEditorError {
20
+ constructor() {
21
+ super(
22
+ "Cannot dispatch a transaction while another dispatch is active."
23
+ );
24
+ }
25
+ }
26
+ class TransactionOwnershipError extends SoEditorError {
27
+ constructor() {
28
+ super("The transaction was not created by this editor.");
29
+ }
30
+ }
31
+ class TransactionAlreadyCommittedError extends SoEditorError {
32
+ constructor() {
33
+ super("The transaction has already been committed.");
34
+ }
35
+ }
36
+ class StaleTransactionError extends SoEditorError {
37
+ constructor(baseVersion, currentVersion) {
38
+ super(
39
+ `Transaction base version ${baseVersion} does not match current editor version ${currentVersion}.`
40
+ );
41
+ }
42
+ }
43
+ class UnsupportedConfigValueError extends SoEditorError {
44
+ constructor(path, kind) {
45
+ super(
46
+ `Configuration value at "${path}" has unsupported type "${kind}".`
47
+ );
48
+ }
49
+ }
50
+ class CyclicConfigurationError extends SoEditorError {
51
+ constructor(path) {
52
+ super(`Configuration value at "${path}" contains a cycle.`);
53
+ }
54
+ }
55
+ class UnsupportedDocumentFormatError extends SoEditorError {
56
+ constructor(format) {
57
+ super(`Document format "${format}" is not supported in this release.`);
58
+ }
59
+ }
60
+ class CommandNotFoundError extends SoEditorError {
61
+ constructor(id) {
62
+ super(`Command "${id}" is not registered.`);
63
+ }
64
+ }
65
+ class CommandAlreadyRegisteredError extends SoEditorError {
66
+ constructor(id) {
67
+ super(`Command "${id}" is already registered.`);
68
+ }
69
+ }
70
+ class PluginNotFoundError extends SoEditorError {
71
+ constructor(id) {
72
+ super(`Plugin "${id}" is not loaded.`);
73
+ }
74
+ }
75
+ class PluginDuplicateIdError extends SoEditorError {
76
+ constructor(id) {
77
+ super(`Plugin ID "${id}" is declared by multiple plugin constructors.`);
78
+ }
79
+ }
80
+ class PluginDependencyCycleError extends SoEditorError {
81
+ path;
82
+ constructor(path) {
83
+ super(`Plugin dependency cycle detected: ${path.join(" -> ")}.`);
84
+ this.path = Object.freeze([...path]);
85
+ }
86
+ }
87
+ class ServiceNotFoundError extends SoEditorError {
88
+ constructor(id) {
89
+ super(`Service "${id}" is not registered.`);
90
+ }
91
+ }
92
+ class ServiceAlreadyRegisteredError extends SoEditorError {
93
+ constructor(id) {
94
+ super(`Service "${id}" is already registered.`);
95
+ }
96
+ }
97
+ function isRecord(value) {
98
+ const prototype = Object.getPrototypeOf(value);
99
+ return prototype === Object.prototype || prototype === null;
100
+ }
101
+ function cloneConfig(value, path = "config", ancestors = /* @__PURE__ */ new Set()) {
102
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
103
+ return value;
104
+ }
105
+ if (typeof value !== "object") {
106
+ throw new UnsupportedConfigValueError(path, typeof value);
107
+ }
108
+ if (ancestors.has(value)) {
109
+ throw new CyclicConfigurationError(path);
110
+ }
111
+ ancestors.add(value);
112
+ if (Array.isArray(value)) {
113
+ try {
114
+ return cloneConfigArray(value, path, ancestors);
115
+ } finally {
116
+ ancestors.delete(value);
117
+ }
118
+ }
119
+ if (!isRecord(value)) {
120
+ ancestors.delete(value);
121
+ throw new UnsupportedConfigValueError(path, "non-plain object");
122
+ }
123
+ try {
124
+ if (Object.getOwnPropertySymbols(value).length > 0) {
125
+ throw new UnsupportedConfigValueError(path, "symbol-keyed object");
126
+ }
127
+ const copy = {};
128
+ for (const [key, descriptor] of Object.entries(
129
+ Object.getOwnPropertyDescriptors(value)
130
+ )) {
131
+ if (!("value" in descriptor)) {
132
+ throw new UnsupportedConfigValueError(
133
+ `${path}.${key}`,
134
+ "accessor"
135
+ );
136
+ }
137
+ if (!descriptor.enumerable) {
138
+ continue;
139
+ }
140
+ Object.defineProperty(copy, key, {
141
+ enumerable: true,
142
+ value: cloneConfig(
143
+ descriptor.value,
144
+ `${path}.${key}`,
145
+ ancestors
146
+ ),
147
+ writable: true
148
+ });
149
+ }
150
+ return Object.freeze(copy);
151
+ } finally {
152
+ ancestors.delete(value);
153
+ }
154
+ }
155
+ function cloneConfigArray(value, path, ancestors) {
156
+ if (Object.getOwnPropertySymbols(value).length > 0) {
157
+ throw new UnsupportedConfigValueError(path, "symbol-keyed array");
158
+ }
159
+ const propertyNames = Object.getOwnPropertyNames(value);
160
+ const indexNames = propertyNames.filter((name) => name !== "length");
161
+ for (const name of indexNames) {
162
+ const index = Number(name);
163
+ if (!Number.isInteger(index) || index < 0 || String(index) !== name) {
164
+ throw new UnsupportedConfigValueError(
165
+ `${path}.${name}`,
166
+ "custom array property"
167
+ );
168
+ }
169
+ }
170
+ if (indexNames.length !== value.length) {
171
+ throw new UnsupportedConfigValueError(path, "sparse array");
172
+ }
173
+ const copy = [];
174
+ for (let index = 0; index < value.length; index += 1) {
175
+ const descriptor = Object.getOwnPropertyDescriptor(
176
+ value,
177
+ String(index)
178
+ );
179
+ if (descriptor === void 0 || !descriptor.enumerable || !("value" in descriptor)) {
180
+ throw new UnsupportedConfigValueError(
181
+ `${path}[${index}]`,
182
+ descriptor !== void 0 && !("value" in descriptor) ? "accessor" : "nonstandard array index"
183
+ );
184
+ }
185
+ copy.push(
186
+ cloneConfig(descriptor.value, `${path}[${index}]`, ancestors)
187
+ );
188
+ }
189
+ return Object.freeze(copy);
190
+ }
191
+ class Config {
192
+ #values;
193
+ /** Creates an immutable defensive copy of supported configuration data. */
194
+ constructor(values = {}) {
195
+ this.#values = cloneConfig(values);
196
+ }
197
+ /** Returns whether a dotted configuration path exists. */
198
+ has(path) {
199
+ return this.#find(path).found;
200
+ }
201
+ /** Returns a value at a dotted path, or undefined when absent. */
202
+ get(path) {
203
+ const result = this.#find(path);
204
+ return result.found ? result.value : void 0;
205
+ }
206
+ #find(path) {
207
+ if (path.length === 0) {
208
+ return { found: false };
209
+ }
210
+ let current = this.#values;
211
+ for (const segment of path.split(".")) {
212
+ if (typeof current !== "object" || current === null || !Object.prototype.hasOwnProperty.call(current, segment)) {
213
+ return { found: false };
214
+ }
215
+ current = current[segment];
216
+ }
217
+ return { found: true, value: current };
218
+ }
219
+ }
220
+ const records$4 = /* @__PURE__ */ new WeakMap();
221
+ class EventBus {
222
+ constructor(assertAvailable = () => void 0) {
223
+ records$4.set(this, {
224
+ assertAvailable,
225
+ listeners: /* @__PURE__ */ new Map()
226
+ });
227
+ }
228
+ on(name, listener) {
229
+ const record = getRecord$4(this);
230
+ record.assertAvailable();
231
+ const listeners = record.listeners.get(name) ?? /* @__PURE__ */ new Set();
232
+ listeners.add(listener);
233
+ record.listeners.set(name, listeners);
234
+ return () => {
235
+ listeners.delete(listener);
236
+ if (listeners.size === 0) {
237
+ record.listeners.delete(name);
238
+ }
239
+ };
240
+ }
241
+ once(name, listener) {
242
+ let dispose = () => void 0;
243
+ const wrapped = (payload) => {
244
+ dispose();
245
+ listener(payload);
246
+ };
247
+ dispose = this.on(name, wrapped);
248
+ return dispose;
249
+ }
250
+ emit(name, payload) {
251
+ const record = getRecord$4(this);
252
+ record.assertAvailable();
253
+ throwListenerErrors(notify(record, name, payload));
254
+ }
255
+ }
256
+ function createEditorEvents(events) {
257
+ return Object.freeze({
258
+ on: events.on.bind(events),
259
+ once: events.once.bind(events)
260
+ });
261
+ }
262
+ function getRecord$4(events) {
263
+ const record = records$4.get(events);
264
+ if (record === void 0) {
265
+ throw new Error("Event bus storage is unavailable.");
266
+ }
267
+ return record;
268
+ }
269
+ function notify(record, name, payload) {
270
+ const listeners = record.listeners.get(name);
271
+ if (listeners === void 0) {
272
+ return [];
273
+ }
274
+ const errors = [];
275
+ for (const listener of [...listeners]) {
276
+ try {
277
+ listener(payload);
278
+ } catch (error) {
279
+ errors.push(error);
280
+ }
281
+ }
282
+ return errors;
283
+ }
284
+ function throwListenerErrors(errors) {
285
+ if (errors.length === 1) {
286
+ throw errors[0];
287
+ }
288
+ if (errors.length > 1) {
289
+ throw new AggregateError(errors, "Multiple event listeners failed.");
290
+ }
291
+ }
292
+ function emitSafely(events, name, payload) {
293
+ const record = getRecord$4(events);
294
+ const errors = notify(record, name, payload);
295
+ if (name !== "event:error") {
296
+ for (const error of errors) {
297
+ notify(record, "event:error", {
298
+ eventName: String(name),
299
+ error
300
+ });
301
+ }
302
+ }
303
+ return errors;
304
+ }
305
+ function emitInternally(events, name, payload) {
306
+ throwListenerErrors(notify(getRecord$4(events), name, payload));
307
+ }
308
+ function clearEvents(events) {
309
+ getRecord$4(events).listeners.clear();
310
+ }
311
+ const records$3 = /* @__PURE__ */ new WeakMap();
312
+ class CommandRegistry {
313
+ constructor(editor, events, assertAvailable) {
314
+ records$3.set(this, {
315
+ assertAvailable,
316
+ commands: /* @__PURE__ */ new Map(),
317
+ context: Object.freeze({ editor }),
318
+ events
319
+ });
320
+ }
321
+ register(command) {
322
+ const record = getRecord$3(this);
323
+ record.assertAvailable();
324
+ if (record.commands.has(command.id)) {
325
+ throw new CommandAlreadyRegisteredError(command.id);
326
+ }
327
+ record.commands.set(command.id, command);
328
+ }
329
+ unregister(id) {
330
+ const record = getRecord$3(this);
331
+ record.assertAvailable();
332
+ return record.commands.delete(id);
333
+ }
334
+ has(id) {
335
+ const record = getRecord$3(this);
336
+ record.assertAvailable();
337
+ return record.commands.has(id);
338
+ }
339
+ get(id) {
340
+ const record = getRecord$3(this);
341
+ record.assertAvailable();
342
+ const command = record.commands.get(id);
343
+ if (command === void 0) {
344
+ throw new CommandNotFoundError(id);
345
+ }
346
+ return command;
347
+ }
348
+ ids() {
349
+ const record = getRecord$3(this);
350
+ record.assertAvailable();
351
+ return Object.freeze([...record.commands.keys()]);
352
+ }
353
+ canExecute(id) {
354
+ const record = getRecord$3(this);
355
+ const command = this.get(id);
356
+ return command.canExecute?.(record.context) ?? true;
357
+ }
358
+ isActive(id) {
359
+ const record = getRecord$3(this);
360
+ const command = this.get(id);
361
+ return command.isActive?.(record.context) ?? false;
362
+ }
363
+ execute(id, ...args) {
364
+ const record = getRecord$3(this);
365
+ const command = this.get(id);
366
+ if (!(command.canExecute?.(record.context) ?? true)) {
367
+ return void 0;
368
+ }
369
+ const event = Object.freeze({
370
+ commandId: id,
371
+ args: Object.freeze(args)
372
+ });
373
+ emitInternally(record.events, "command:beforeExecute", event);
374
+ let result;
375
+ let then;
376
+ try {
377
+ result = command.execute(record.context, ...args);
378
+ then = getThenMethod(result);
379
+ } catch (error) {
380
+ emitSafely(
381
+ record.events,
382
+ "command:error",
383
+ Object.freeze({ ...event, error })
384
+ );
385
+ throw error;
386
+ }
387
+ if (then !== void 0) {
388
+ return assimilateThenable(result, then).then(
389
+ (value) => {
390
+ emitInternally(
391
+ record.events,
392
+ "command:afterExecute",
393
+ event
394
+ );
395
+ return value;
396
+ },
397
+ (error) => {
398
+ emitSafely(
399
+ record.events,
400
+ "command:error",
401
+ Object.freeze({ ...event, error })
402
+ );
403
+ throw error;
404
+ }
405
+ );
406
+ }
407
+ emitInternally(record.events, "command:afterExecute", event);
408
+ return result;
409
+ }
410
+ }
411
+ function getRecord$3(registry) {
412
+ const record = records$3.get(registry);
413
+ if (record === void 0) {
414
+ throw new Error("Command registry storage is unavailable.");
415
+ }
416
+ return record;
417
+ }
418
+ function getThenMethod(value) {
419
+ if ((typeof value !== "object" || value === null) && typeof value !== "function") {
420
+ return void 0;
421
+ }
422
+ const then = Reflect.get(value, "then");
423
+ return typeof then === "function" ? then : void 0;
424
+ }
425
+ function assimilateThenable(value, then) {
426
+ return new Promise((resolve, reject) => {
427
+ try {
428
+ then.call(value, resolve, reject);
429
+ } catch (error) {
430
+ reject(error);
431
+ }
432
+ });
433
+ }
434
+ function clearCommands(registry) {
435
+ getRecord$3(registry).commands.clear();
436
+ }
437
+ const records$2 = /* @__PURE__ */ new WeakMap();
438
+ class PluginManager {
439
+ constructor(editor, events, assertAvailable) {
440
+ records$2.set(this, {
441
+ assertAvailable,
442
+ editor,
443
+ events,
444
+ instances: /* @__PURE__ */ new Map(),
445
+ order: []
446
+ });
447
+ }
448
+ has(key) {
449
+ const record = getRecord$2(this);
450
+ record.assertAvailable();
451
+ return record.instances.has(typeof key === "string" ? key : key.id);
452
+ }
453
+ get(key) {
454
+ const record = getRecord$2(this);
455
+ record.assertAvailable();
456
+ const id = typeof key === "string" ? key : key.id;
457
+ const entry = record.instances.get(id);
458
+ if (entry === void 0) {
459
+ throw new PluginNotFoundError(id);
460
+ }
461
+ return entry.instance;
462
+ }
463
+ tryGet(key) {
464
+ const record = getRecord$2(this);
465
+ record.assertAvailable();
466
+ const id = typeof key === "string" ? key : key.id;
467
+ return record.instances.get(id)?.instance;
468
+ }
469
+ }
470
+ function getRecord$2(manager) {
471
+ const record = records$2.get(manager);
472
+ if (record === void 0) {
473
+ throw new Error("Plugin manager storage is unavailable.");
474
+ }
475
+ return record;
476
+ }
477
+ function emitPluginError(record, pluginId, phase, error) {
478
+ emitSafely(
479
+ record.events,
480
+ "plugin:error",
481
+ Object.freeze({ pluginId, phase, error })
482
+ );
483
+ }
484
+ async function initializePlugins(manager, constructors, startup) {
485
+ const record = getRecord$2(manager);
486
+ const order = resolvePlugins(constructors);
487
+ startup.assertInitializing();
488
+ record.order = order;
489
+ const context = Object.freeze({ editor: record.editor });
490
+ for (const constructor of record.order) {
491
+ startup.assertInitializing();
492
+ let instance;
493
+ try {
494
+ instance = new constructor(context);
495
+ } catch (error) {
496
+ startup.assertInitializing();
497
+ emitPluginError(record, constructor.id, "construct", error);
498
+ throw error;
499
+ }
500
+ startup.assertInitializing();
501
+ record.instances.set(constructor.id, {
502
+ constructor,
503
+ instance,
504
+ stage: "constructed"
505
+ });
506
+ }
507
+ for (const constructor of record.order) {
508
+ startup.assertInitializing();
509
+ const entry = record.instances.get(constructor.id);
510
+ if (entry === void 0) {
511
+ throw new Error(`Plugin "${constructor.id}" was not constructed.`);
512
+ }
513
+ try {
514
+ await waitForStartupHook(entry.instance.init?.(), startup);
515
+ } catch (error) {
516
+ startup.assertInitializing();
517
+ emitPluginError(record, constructor.id, "init", error);
518
+ throw error;
519
+ }
520
+ startup.assertInitializing();
521
+ entry.stage = "initialized";
522
+ }
523
+ for (const constructor of record.order) {
524
+ startup.assertInitializing();
525
+ const entry = record.instances.get(constructor.id);
526
+ if (entry === void 0) {
527
+ throw new Error(`Plugin "${constructor.id}" was not constructed.`);
528
+ }
529
+ try {
530
+ await waitForStartupHook(entry.instance.ready?.(), startup);
531
+ } catch (error) {
532
+ startup.assertInitializing();
533
+ emitPluginError(record, constructor.id, "ready", error);
534
+ throw error;
535
+ }
536
+ startup.assertInitializing();
537
+ entry.stage = "ready";
538
+ }
539
+ startup.assertInitializing();
540
+ }
541
+ async function waitForStartupHook(result, startup) {
542
+ const hookOutcome = Promise.resolve(
543
+ result
544
+ ).then(
545
+ () => ({ status: "fulfilled" }),
546
+ (error) => ({ status: "rejected", error })
547
+ );
548
+ const activeDestroy = startup.getDestroyPromise();
549
+ if (activeDestroy !== void 0) {
550
+ await activeDestroy;
551
+ startup.assertInitializing();
552
+ return;
553
+ }
554
+ const outcome = await Promise.race([
555
+ hookOutcome,
556
+ startup.destructionStarted.then(() => ({
557
+ status: "destroying"
558
+ }))
559
+ ]);
560
+ if (outcome.status === "rejected") {
561
+ throw outcome.error;
562
+ }
563
+ if (outcome.status === "destroying") {
564
+ const destroyPromise = startup.getDestroyPromise();
565
+ if (destroyPromise === void 0) {
566
+ throw new Error(
567
+ "Editor destruction started without a shared destroy promise."
568
+ );
569
+ }
570
+ await destroyPromise;
571
+ startup.assertInitializing();
572
+ }
573
+ }
574
+ async function destroyPlugins(manager) {
575
+ const record = getRecord$2(manager);
576
+ for (const constructor of [...record.order].reverse()) {
577
+ const entry = record.instances.get(constructor.id);
578
+ if (entry === void 0 || entry.stage === "constructed" || entry.stage === "destroyed") {
579
+ continue;
580
+ }
581
+ entry.stage = "destroyed";
582
+ try {
583
+ await entry.instance.destroy?.();
584
+ } catch (error) {
585
+ emitPluginError(record, constructor.id, "destroy", error);
586
+ }
587
+ }
588
+ record.instances.clear();
589
+ record.order = [];
590
+ }
591
+ function resolvePlugins(roots) {
592
+ const byId = /* @__PURE__ */ new Map();
593
+ const visited = /* @__PURE__ */ new Set();
594
+ const visiting = [];
595
+ const ordered = [];
596
+ const visit = (constructor) => {
597
+ const existing = byId.get(constructor.id);
598
+ if (existing !== void 0 && existing !== constructor) {
599
+ throw new PluginDuplicateIdError(constructor.id);
600
+ }
601
+ byId.set(constructor.id, constructor);
602
+ const cycleStart = visiting.indexOf(constructor);
603
+ if (cycleStart !== -1) {
604
+ const path = visiting.slice(cycleStart).map((item) => item.id);
605
+ path.push(constructor.id);
606
+ throw new PluginDependencyCycleError(path);
607
+ }
608
+ if (visited.has(constructor)) {
609
+ return;
610
+ }
611
+ visiting.push(constructor);
612
+ for (const requirement of constructor.requires ?? []) {
613
+ visit(requirement);
614
+ }
615
+ visiting.pop();
616
+ visited.add(constructor);
617
+ ordered.push(constructor);
618
+ };
619
+ for (const constructor of roots) {
620
+ visit(constructor);
621
+ }
622
+ return Object.freeze(ordered);
623
+ }
624
+ const records$1 = /* @__PURE__ */ new WeakMap();
625
+ function serviceId(key) {
626
+ return typeof key === "string" ? key : key.id;
627
+ }
628
+ class ServiceRegistry {
629
+ constructor(assertAvailable) {
630
+ records$1.set(this, { assertAvailable, services: /* @__PURE__ */ new Map() });
631
+ }
632
+ register(key, service) {
633
+ const record = getRecord$1(this);
634
+ record.assertAvailable();
635
+ const id = serviceId(key);
636
+ if (record.services.has(id)) {
637
+ throw new ServiceAlreadyRegisteredError(id);
638
+ }
639
+ record.services.set(id, service);
640
+ }
641
+ replace(key, service) {
642
+ const record = getRecord$1(this);
643
+ record.assertAvailable();
644
+ record.services.set(serviceId(key), service);
645
+ }
646
+ has(key) {
647
+ const record = getRecord$1(this);
648
+ record.assertAvailable();
649
+ return record.services.has(serviceId(key));
650
+ }
651
+ get(key) {
652
+ const record = getRecord$1(this);
653
+ record.assertAvailable();
654
+ const id = serviceId(key);
655
+ if (!record.services.has(id)) {
656
+ throw new ServiceNotFoundError(id);
657
+ }
658
+ return record.services.get(id);
659
+ }
660
+ tryGet(key) {
661
+ const record = getRecord$1(this);
662
+ record.assertAvailable();
663
+ return record.services.get(serviceId(key));
664
+ }
665
+ unregister(key) {
666
+ const record = getRecord$1(this);
667
+ record.assertAvailable();
668
+ return record.services.delete(serviceId(key));
669
+ }
670
+ }
671
+ function getRecord$1(registry) {
672
+ const record = records$1.get(registry);
673
+ if (record === void 0) {
674
+ throw new Error("Service registry storage is unavailable.");
675
+ }
676
+ return record;
677
+ }
678
+ function clearServices(registry) {
679
+ getRecord$1(registry).services.clear();
680
+ }
681
+ function createEditorDocument(source, format, revision = 0, metadata = {}) {
682
+ return Object.freeze({
683
+ format,
684
+ source,
685
+ revision,
686
+ metadata: Object.freeze({ ...metadata })
687
+ });
688
+ }
689
+ function createEditorState(values) {
690
+ return Object.freeze({ ...values });
691
+ }
692
+ const records = /* @__PURE__ */ new WeakMap();
693
+ class EditorTransaction {
694
+ constructor(record) {
695
+ records.set(this, record);
696
+ }
697
+ get origin() {
698
+ return getRecord(this).origin;
699
+ }
700
+ get operations() {
701
+ return [...getRecord(this).operations];
702
+ }
703
+ get metadata() {
704
+ return Object.freeze(Object.fromEntries(getRecord(this).metadata));
705
+ }
706
+ replaceDocument(source) {
707
+ const record = getMutableRecord(this);
708
+ record.operations.push(
709
+ Object.freeze({ type: "replace-document", source })
710
+ );
711
+ return this;
712
+ }
713
+ setMode(mode) {
714
+ const record = getMutableRecord(this);
715
+ record.operations.push(Object.freeze({ type: "set-mode", mode }));
716
+ return this;
717
+ }
718
+ setMeta(key, value) {
719
+ getMutableRecord(this).metadata.set(key, value);
720
+ return this;
721
+ }
722
+ getMeta(key) {
723
+ return getRecord(this).metadata.get(key);
724
+ }
725
+ }
726
+ function getRecord(transaction) {
727
+ const record = records.get(transaction);
728
+ if (record === void 0) {
729
+ throw new TransactionOwnershipError();
730
+ }
731
+ return record;
732
+ }
733
+ function getMutableRecord(transaction) {
734
+ const record = getRecord(transaction);
735
+ if (record.committed) {
736
+ throw new TransactionAlreadyCommittedError();
737
+ }
738
+ return record;
739
+ }
740
+ function createTransaction(owner, baseVersion, options) {
741
+ return new EditorTransaction({
742
+ baseVersion,
743
+ committed: false,
744
+ metadata: /* @__PURE__ */ new Map(),
745
+ operations: [],
746
+ origin: options.origin ?? "system",
747
+ owner
748
+ });
749
+ }
750
+ function commitTransaction(transaction, owner, currentVersion) {
751
+ const record = getMutableRecord(transaction);
752
+ if (record.owner !== owner) {
753
+ throw new TransactionOwnershipError();
754
+ }
755
+ if (record.baseVersion !== currentVersion) {
756
+ throw new StaleTransactionError(record.baseVersion, currentVersion);
757
+ }
758
+ record.committed = true;
759
+ return Object.freeze([...record.operations]);
760
+ }
761
+ class Editor {
762
+ /** Commands available to consumers and plugins. */
763
+ commands;
764
+ /** Immutable instance configuration. */
765
+ config;
766
+ /** Subscription-only access to typed editor events. */
767
+ events;
768
+ /** Loaded plugin lookup capabilities. */
769
+ plugins;
770
+ /** Cross-feature service capabilities. */
771
+ services;
772
+ #commandRegistry;
773
+ #destructionStarted;
774
+ #eventBus;
775
+ #owner = Object.freeze({});
776
+ #pluginManager;
777
+ #serviceRegistry;
778
+ #destroyPromise;
779
+ #dispatching = false;
780
+ #lifecycle = "initializing";
781
+ #resolveDestructionStarted;
782
+ #state;
783
+ #stateVersion = 0;
784
+ constructor(options) {
785
+ const format = options.format ?? "html";
786
+ if (format !== "html" && format !== "markdown") {
787
+ throw new UnsupportedDocumentFormatError(format);
788
+ }
789
+ this.#state = createEditorState({
790
+ document: createEditorDocument(options.data ?? "", format),
791
+ mode: options.mode ?? (format === "markdown" ? "markdown" : "visual"),
792
+ readonly: options.readonly ?? false,
793
+ dirty: false
794
+ });
795
+ this.#destructionStarted = new Promise((resolve) => {
796
+ this.#resolveDestructionStarted = resolve;
797
+ });
798
+ this.config = new Config(options.config);
799
+ const assertAvailable = () => this.#assertNotDestroyed();
800
+ this.#eventBus = new EventBus(assertAvailable);
801
+ this.#commandRegistry = new CommandRegistry(
802
+ this,
803
+ this.#eventBus,
804
+ assertAvailable
805
+ );
806
+ this.#serviceRegistry = new ServiceRegistry(assertAvailable);
807
+ this.#pluginManager = new PluginManager(
808
+ this,
809
+ this.#eventBus,
810
+ assertAvailable
811
+ );
812
+ this.commands = this.#commandRegistry;
813
+ this.events = createEditorEvents(this.#eventBus);
814
+ this.services = this.#serviceRegistry;
815
+ this.plugins = this.#pluginManager;
816
+ }
817
+ /** The current immutable state snapshot. */
818
+ get state() {
819
+ return this.#state;
820
+ }
821
+ /** Creates and initializes an editor and all requested plugins. */
822
+ static async create(options = {}) {
823
+ const editor = new Editor(options);
824
+ try {
825
+ await initializePlugins(
826
+ editor.#pluginManager,
827
+ options.plugins ?? [],
828
+ {
829
+ assertInitializing: () => editor.#assertInitializing(),
830
+ destructionStarted: editor.#destructionStarted,
831
+ getDestroyPromise: () => editor.#destroyPromise
832
+ }
833
+ );
834
+ editor.#transitionToReady();
835
+ let readyEventError;
836
+ let readyEventFailed = false;
837
+ try {
838
+ emitInternally(
839
+ editor.#eventBus,
840
+ "editor:ready",
841
+ Object.freeze({ editor })
842
+ );
843
+ } catch (error) {
844
+ readyEventFailed = true;
845
+ readyEventError = error;
846
+ }
847
+ editor.#assertCreationCanReturn();
848
+ if (readyEventFailed) {
849
+ throw readyEventError;
850
+ }
851
+ return editor;
852
+ } catch (error) {
853
+ await editor.destroy();
854
+ throw error;
855
+ }
856
+ }
857
+ /** Creates a mutable transaction owned by the current editor state. */
858
+ createTransaction(options = {}) {
859
+ this.#assertAlive();
860
+ return createTransaction(this.#owner, this.#stateVersion, options);
861
+ }
862
+ /**
863
+ * Applies one owned, current, uncommitted transaction.
864
+ *
865
+ * Synchronous reentrant dispatch is rejected with
866
+ * `ReentrantDispatchError` rather than queued.
867
+ */
868
+ dispatch(transaction) {
869
+ this.#assertAlive();
870
+ if (this.#dispatching) {
871
+ throw new ReentrantDispatchError();
872
+ }
873
+ this.#dispatching = true;
874
+ try {
875
+ const operations = commitTransaction(
876
+ transaction,
877
+ this.#owner,
878
+ this.#stateVersion
879
+ );
880
+ this.#applyOperations(transaction, operations);
881
+ } finally {
882
+ this.#dispatching = false;
883
+ }
884
+ }
885
+ /** Builds and dispatches one editor-owned transaction. */
886
+ update(callback, options = {}) {
887
+ this.#assertAlive();
888
+ const transaction = this.createTransaction(options);
889
+ callback(transaction);
890
+ this.dispatch(transaction);
891
+ }
892
+ /** Executes a registered command through this editor's command collection. */
893
+ execute(commandId, ...args) {
894
+ this.#assertAlive();
895
+ return this.#commandRegistry.execute(commandId, ...args);
896
+ }
897
+ /** Returns canonical source, including after editor destruction. */
898
+ getData() {
899
+ return this.#state.document.source;
900
+ }
901
+ /**
902
+ * Administratively replaces canonical source through a transaction.
903
+ *
904
+ * This remains allowed when `readonly` is true; future user-facing editing
905
+ * surfaces must enforce readonly policy before creating user transactions.
906
+ */
907
+ setData(source) {
908
+ this.update((transaction) => transaction.replaceDocument(source), {
909
+ origin: "source"
910
+ });
911
+ }
912
+ /** Marks the current state as saved without changing document revision. */
913
+ markClean() {
914
+ this.#assertAlive();
915
+ if (!this.#state.dirty) {
916
+ return;
917
+ }
918
+ const previous = this.#state;
919
+ const current = createEditorState({ ...previous, dirty: false });
920
+ this.#state = current;
921
+ this.#stateVersion += 1;
922
+ emitInternally(
923
+ this.#eventBus,
924
+ "state:change",
925
+ Object.freeze({ previous, current })
926
+ );
927
+ }
928
+ /**
929
+ * Destroys initialized plugins and clears all owned infrastructure.
930
+ *
931
+ * Calls made while destruction is pending return the same promise.
932
+ */
933
+ destroy() {
934
+ if (this.#destroyPromise !== void 0) {
935
+ return this.#destroyPromise;
936
+ }
937
+ this.#lifecycle = "destroying";
938
+ let resolveDestroy;
939
+ let rejectDestroy;
940
+ const destroyPromise = new Promise((resolve, reject) => {
941
+ resolveDestroy = resolve;
942
+ rejectDestroy = reject;
943
+ });
944
+ this.#destroyPromise = destroyPromise;
945
+ this.#resolveDestructionStarted();
946
+ void this.#performDestroy().then(resolveDestroy, rejectDestroy);
947
+ return destroyPromise;
948
+ }
949
+ #applyOperations(transaction, operations) {
950
+ const previous = this.#state;
951
+ let source = previous.document.source;
952
+ let mode = previous.mode;
953
+ for (const operation of operations) {
954
+ switch (operation.type) {
955
+ case "replace-document":
956
+ source = operation.source;
957
+ break;
958
+ case "set-mode":
959
+ mode = operation.mode;
960
+ break;
961
+ default:
962
+ assertNever(operation);
963
+ }
964
+ }
965
+ const documentChanged = source !== previous.document.source;
966
+ const modeChanged = mode !== previous.mode;
967
+ if (!documentChanged && !modeChanged) {
968
+ return;
969
+ }
970
+ const document = documentChanged ? createEditorDocument(
971
+ source,
972
+ previous.document.format,
973
+ previous.document.revision + 1,
974
+ previous.document.metadata
975
+ ) : previous.document;
976
+ const current = createEditorState({
977
+ document,
978
+ mode,
979
+ readonly: previous.readonly,
980
+ dirty: documentChanged ? true : previous.dirty
981
+ });
982
+ if (documentChanged) {
983
+ emitInternally(
984
+ this.#eventBus,
985
+ "document:beforeChange",
986
+ Object.freeze({
987
+ previous: previous.document,
988
+ current: document,
989
+ transaction
990
+ })
991
+ );
992
+ }
993
+ this.#state = current;
994
+ this.#stateVersion += 1;
995
+ const notificationErrors = [];
996
+ if (documentChanged) {
997
+ attemptNotification(
998
+ notificationErrors,
999
+ () => emitInternally(
1000
+ this.#eventBus,
1001
+ "document:change",
1002
+ Object.freeze({
1003
+ previous: previous.document,
1004
+ current: document,
1005
+ transaction
1006
+ })
1007
+ )
1008
+ );
1009
+ }
1010
+ if (modeChanged) {
1011
+ attemptNotification(
1012
+ notificationErrors,
1013
+ () => emitInternally(
1014
+ this.#eventBus,
1015
+ "mode:change",
1016
+ Object.freeze({
1017
+ previous: previous.mode,
1018
+ current: mode,
1019
+ transaction
1020
+ })
1021
+ )
1022
+ );
1023
+ }
1024
+ attemptNotification(
1025
+ notificationErrors,
1026
+ () => emitInternally(
1027
+ this.#eventBus,
1028
+ "state:change",
1029
+ Object.freeze({ previous, current, transaction })
1030
+ )
1031
+ );
1032
+ throwNotificationErrors(notificationErrors);
1033
+ }
1034
+ async #performDestroy() {
1035
+ try {
1036
+ await destroyPlugins(this.#pluginManager);
1037
+ } finally {
1038
+ this.#lifecycle = "destroyed";
1039
+ try {
1040
+ emitSafely(
1041
+ this.#eventBus,
1042
+ "editor:destroy",
1043
+ Object.freeze({ editor: this })
1044
+ );
1045
+ } finally {
1046
+ clearCommands(this.#commandRegistry);
1047
+ clearServices(this.#serviceRegistry);
1048
+ clearEvents(this.#eventBus);
1049
+ }
1050
+ }
1051
+ }
1052
+ #assertAlive() {
1053
+ if (this.#lifecycle === "destroying" || this.#lifecycle === "destroyed") {
1054
+ throw new EditorDestroyedError();
1055
+ }
1056
+ }
1057
+ #assertInitializing() {
1058
+ if (this.#lifecycle !== "initializing") {
1059
+ throw new EditorInitializationAbortedError();
1060
+ }
1061
+ }
1062
+ #transitionToReady() {
1063
+ this.#assertInitializing();
1064
+ this.#lifecycle = "ready";
1065
+ }
1066
+ #assertCreationCanReturn() {
1067
+ if (this.#lifecycle !== "ready") {
1068
+ throw new EditorInitializationAbortedError();
1069
+ }
1070
+ }
1071
+ #assertNotDestroyed() {
1072
+ if (this.#lifecycle === "destroying" || this.#lifecycle === "destroyed") {
1073
+ throw new EditorDestroyedError();
1074
+ }
1075
+ }
1076
+ }
1077
+ function assertNever(operation) {
1078
+ throw new Error(`Unsupported operation: ${JSON.stringify(operation)}.`);
1079
+ }
1080
+ function attemptNotification(errors, notify2) {
1081
+ try {
1082
+ notify2();
1083
+ } catch (error) {
1084
+ errors.push(error);
1085
+ }
1086
+ }
1087
+ function throwNotificationErrors(errors) {
1088
+ if (errors.length === 1) {
1089
+ throw errors[0];
1090
+ }
1091
+ if (errors.length > 1) {
1092
+ throw new AggregateError(
1093
+ errors,
1094
+ "Multiple editor state notifications failed."
1095
+ );
1096
+ }
1097
+ }
1098
+ class Plugin {
1099
+ /** The editor instance that owns this plugin. */
1100
+ editor;
1101
+ /** Creates a plugin owned by the editor in the supplied context. */
1102
+ constructor(context) {
1103
+ this.editor = context.editor;
1104
+ }
1105
+ }
1106
+ function createServiceToken(id) {
1107
+ return Object.freeze({ id });
1108
+ }
1109
+ export {
1110
+ CommandAlreadyRegisteredError,
1111
+ CommandNotFoundError,
1112
+ Config,
1113
+ CyclicConfigurationError,
1114
+ Editor,
1115
+ EditorDestroyedError,
1116
+ EditorInitializationAbortedError,
1117
+ Plugin,
1118
+ PluginDependencyCycleError,
1119
+ PluginDuplicateIdError,
1120
+ PluginNotFoundError,
1121
+ ReentrantDispatchError,
1122
+ ServiceAlreadyRegisteredError,
1123
+ ServiceNotFoundError,
1124
+ SoEditorError,
1125
+ StaleTransactionError,
1126
+ TransactionAlreadyCommittedError,
1127
+ TransactionOwnershipError,
1128
+ UnsupportedConfigValueError,
1129
+ UnsupportedDocumentFormatError,
1130
+ createServiceToken
1131
+ };
1132
+ //# sourceMappingURL=index.js.map