edytor 5.0.0 → 7.0.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 (137) hide show
  1. package/README.md +3 -3
  2. package/dist/cloudflare/DocumentRoom.d.ts +77 -2
  3. package/dist/cloudflare/DocumentRoom.js +37 -1
  4. package/dist/cloudflare/index.d.ts +1 -1
  5. package/dist/cloudflare/index.js +1 -1
  6. package/dist/cloudflare/room/access.d.ts +6 -1
  7. package/dist/cloudflare/room/access.js +30 -4
  8. package/dist/cloudflare/room/admission.d.ts +11 -0
  9. package/dist/cloudflare/room/admission.js +43 -10
  10. package/dist/cloudflare/room/context.d.ts +73 -76
  11. package/dist/cloudflare/room/context.js +17 -6
  12. package/dist/cloudflare/room/forged.d.ts +1 -1
  13. package/dist/cloudflare/room/forged.js +11 -9
  14. package/dist/cloudflare/room/frames.d.ts +2 -0
  15. package/dist/cloudflare/room/frames.js +2 -0
  16. package/dist/cloudflare/room/presence.js +1 -4
  17. package/dist/cloudflare/room/reseed.d.ts +93 -0
  18. package/dist/cloudflare/room/reseed.js +500 -0
  19. package/dist/cloudflare/room/scheduler.d.ts +1 -1
  20. package/dist/cloudflare/room/scheduler.js +9 -1
  21. package/dist/cloudflare/room/shared.d.ts +10 -0
  22. package/dist/cloudflare/room/shared.js +10 -0
  23. package/dist/cloudflare/room/storage.d.ts +7 -0
  24. package/dist/cloudflare/room/storage.js +33 -1
  25. package/dist/components/Edytor.svelte +18 -3
  26. package/dist/components/Edytor.svelte.d.ts +10 -1
  27. package/dist/components/EdytorRoot.svelte +17 -0
  28. package/dist/components/EdytorRoot.svelte.d.ts +4 -0
  29. package/dist/components/Suggestion.svelte +1 -1
  30. package/dist/crdt/attribution/block.d.ts +3 -0
  31. package/dist/crdt/attribution/block.js +3 -2
  32. package/dist/crdt/doc/history.js +2 -3
  33. package/dist/crdt/doc/ops.d.ts +81 -0
  34. package/dist/crdt/doc/ops.js +1898 -0
  35. package/dist/crdt/doc/suggest.d.ts +6 -2
  36. package/dist/crdt/doc/suggest.js +6 -5
  37. package/dist/crdt/edytor-doc.d.ts +157 -157
  38. package/dist/crdt/edytor-doc.js +15 -43
  39. package/dist/crdt/engine-room.d.ts +4 -0
  40. package/dist/crdt/engine-room.js +6 -0
  41. package/dist/crdt/flow.d.ts +4 -46
  42. package/dist/crdt/flow.js +2 -262
  43. package/dist/crdt/index.d.ts +64 -75
  44. package/dist/crdt/operations.d.ts +1 -1
  45. package/dist/crdt/placement/resolve.d.ts +1 -1
  46. package/dist/crdt/protocol.d.ts +2 -2
  47. package/dist/crdt/protocol.js +2 -2
  48. package/dist/crdt/protocols/auth.d.ts +8 -1
  49. package/dist/crdt/protocols/auth.js +14 -1
  50. package/dist/crdt/protocols/awareness.js +46 -81
  51. package/dist/crdt/providers/index.d.ts +11 -22
  52. package/dist/crdt/providers/index.js +22 -3
  53. package/dist/crdt/providers/indexeddb.d.ts +6 -10
  54. package/dist/crdt/providers/indexeddb.js +10 -16
  55. package/dist/crdt/providers/room.d.ts +26 -0
  56. package/dist/crdt/providers/room.js +41 -12
  57. package/dist/crdt/providers/websocket.d.ts +10 -14
  58. package/dist/crdt/providers/websocket.js +43 -23
  59. package/dist/crdt/rangeDelete.d.ts +2 -47
  60. package/dist/crdt/rangeDelete.js +1 -169
  61. package/dist/crdt/reseed.d.ts +127 -0
  62. package/dist/crdt/reseed.js +533 -0
  63. package/dist/crdt/restore.d.ts +13 -1
  64. package/dist/crdt/restore.js +8 -3
  65. package/dist/crdt/schema.d.ts +6 -0
  66. package/dist/crdt/schema.js +6 -0
  67. package/dist/crdt/structs.d.ts +8 -11
  68. package/dist/crdt/structs.js +25 -44
  69. package/dist/crdt/text/index/build.d.ts +119 -0
  70. package/dist/crdt/text/index/build.js +2801 -0
  71. package/dist/crdt/text/index/shared.d.ts +2 -6
  72. package/dist/crdt/text/index/shared.js +2 -5
  73. package/dist/crdt/text/model.js +2 -3
  74. package/dist/crdt/text/runs.d.ts +9 -12
  75. package/dist/crdt/text/runs.js +2 -165
  76. package/dist/crdt/vendor/yjs/UPSTREAM.md +101 -5
  77. package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
  78. package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +10 -10
  79. package/dist/crdt/vendor/yjs/dts/utils/encoding.d.ts +1 -0
  80. package/dist/crdt/vendor/yjs/src/index.js +1 -1
  81. package/dist/crdt/vendor/yjs/src/structs/Item.js +24 -13
  82. package/dist/crdt/vendor/yjs/src/utils/Transaction.js +12 -10
  83. package/dist/crdt/vendor/yjs/src/utils/encoding.js +42 -1
  84. package/dist/events/domTextOffset.d.ts +2 -0
  85. package/dist/events/domTextOffset.js +8 -0
  86. package/dist/export/forms.d.ts +50 -50
  87. package/dist/index.d.ts +1 -1
  88. package/dist/index.js +1 -1
  89. package/dist/plugins/richtext/RichTextPlugin.svelte +63 -61
  90. package/dist/plugins/richtext/richTextOperations.js +26 -32
  91. package/dist/selection/selection.svelte.d.ts +1 -1
  92. package/dist/selection/selection.svelte.js +87 -106
  93. package/dist/selection/selection.utils.js +5 -8
  94. package/dist/session/commands.js +49 -51
  95. package/dist/session/handles.js +3 -7
  96. package/dist/surface/attributes.d.ts +3 -3
  97. package/dist/surface/attributes.js +2 -2
  98. package/dist/surface/observer.svelte.d.ts +7 -7
  99. package/dist/surface/observer.svelte.js +225 -336
  100. package/dist/surface/projector.svelte.js +2 -2
  101. package/dist/utils.d.ts +5 -0
  102. package/dist/utils.js +7 -0
  103. package/package.json +1 -1
  104. package/dist/crdt/doc/capability.d.ts +0 -56
  105. package/dist/crdt/doc/capability.js +0 -207
  106. package/dist/crdt/doc/content.d.ts +0 -16
  107. package/dist/crdt/doc/content.js +0 -77
  108. package/dist/crdt/doc/delete.d.ts +0 -15
  109. package/dist/crdt/doc/delete.js +0 -84
  110. package/dist/crdt/doc/layout.d.ts +0 -9
  111. package/dist/crdt/doc/layout.js +0 -178
  112. package/dist/crdt/doc/meta.d.ts +0 -19
  113. package/dist/crdt/doc/meta.js +0 -198
  114. package/dist/crdt/doc/moves.d.ts +0 -21
  115. package/dist/crdt/doc/moves.js +0 -274
  116. package/dist/crdt/doc/split-merge.d.ts +0 -10
  117. package/dist/crdt/doc/split-merge.js +0 -179
  118. package/dist/crdt/doc/steps.d.ts +0 -65
  119. package/dist/crdt/doc/steps.js +0 -311
  120. package/dist/crdt/text/index/cache.d.ts +0 -16
  121. package/dist/crdt/text/index/cache.js +0 -175
  122. package/dist/crdt/text/index/claims.d.ts +0 -18
  123. package/dist/crdt/text/index/claims.js +0 -203
  124. package/dist/crdt/text/index/fold.d.ts +0 -21
  125. package/dist/crdt/text/index/fold.js +0 -656
  126. package/dist/crdt/text/index/layout.d.ts +0 -22
  127. package/dist/crdt/text/index/layout.js +0 -258
  128. package/dist/crdt/text/index/placement.d.ts +0 -47
  129. package/dist/crdt/text/index/placement.js +0 -738
  130. package/dist/crdt/text/index/records.d.ts +0 -13
  131. package/dist/crdt/text/index/records.js +0 -139
  132. package/dist/crdt/text/index/report.d.ts +0 -18
  133. package/dist/crdt/text/index/report.js +0 -337
  134. package/dist/crdt/text/index/state.d.ts +0 -197
  135. package/dist/crdt/text/index/state.js +0 -116
  136. package/dist/crdt/text/index/streams.d.ts +0 -25
  137. package/dist/crdt/text/index/streams.js +0 -248
@@ -44,10 +44,11 @@ import { INTENTS } from '../session/attempt.js';
44
44
  import { runOccurrence } from '../events/onBeforeInput.js';
45
45
  import { getDomSelectionSnapshot } from '../selection/domSelection.js';
46
46
  import { domOffsetIn } from '../selection/selection.utils.js';
47
- import { diverges, heal, ownedOf } from './attributes.js';
48
- import { getTextContentOffsetAtPoint } from '../events/domTextOffset.js';
47
+ import { diverges, heal, ownText, ownedOf } from './attributes.js';
48
+ import { getTextContentOffsetAtPoint, textLeaves } from '../events/domTextOffset.js';
49
49
  import { rendersMark } from './cells.js';
50
- const ZWSP = /\u200B/g;
50
+ /** The empty filler (and the trailing newline's marker). */
51
+ const ZWSP = '\u200B';
51
52
  const ATOM = '\uFFFC';
52
53
  /** Attributes that claim a core element's identity: never on an unregistered element. */
53
54
  const IDENTITY_ATTRIBUTES = [
@@ -66,21 +67,9 @@ const REST = '[data-edytor-composition-rest]';
66
67
  const BOUND = 100;
67
68
  const WINDOW_MS = 250;
68
69
  /** The text a cell renders: characters, an atom one unit, fillers stripped. */
69
- const textOf = (cell) => cell.runs
70
- .map((run) => (run.kind === 'text' ? run.text : ATOM))
71
- .join('')
72
- .replace(ZWSP, '');
70
+ const textOf = (cell) => ownText(cell.runs, ATOM).replaceAll(ZWSP, '');
73
71
  /** Svelte's anchors: comments and empty text nodes are never input and never removed. */
74
72
  const isAnchor = (node) => node.nodeType === Node.COMMENT_NODE || (node.nodeType === Node.TEXT_NODE && !node.nodeValue);
75
- /** The `nth` non-empty text node under `node`. */
76
- const textLeaf = (node, nth = 0) => {
77
- const walker = (node.ownerDocument ?? document).createTreeWalker(node, NodeFilter.SHOW_TEXT);
78
- while (walker.nextNode())
79
- if (walker.currentNode.data && nth-- === 0)
80
- return walker.currentNode;
81
- return null;
82
- };
83
- const ZERO_WIDTH_SPACE = '\u200B';
84
73
  /**
85
74
  * What the browser shows for `text`: its element's text nodes in order (WebKit's
86
75
  * converted-space wrappers read as plain spaces), the empty filler and the
@@ -88,40 +77,23 @@ const ZERO_WIDTH_SPACE = '\u200B';
88
77
  */
89
78
  export const readDomText = (text) => {
90
79
  let value = '';
91
- const node = text.node;
92
- if (node) {
93
- const walker = (node.ownerDocument ?? document).createTreeWalker(node, NodeFilter.SHOW_TEXT);
94
- while (walker.nextNode()) {
95
- const leaf = walker.currentNode;
96
- value += leaf.parentElement?.closest('span.Apple-converted-space')
97
- ? leaf.data.replaceAll('\u00A0', ' ')
98
- : leaf.data;
99
- }
100
- }
80
+ for (const leaf of text.node ? textLeaves(text.node) : [])
81
+ value += leaf.parentElement?.closest('span.Apple-converted-space')
82
+ ? leaf.data.replaceAll('\u00A0', ' ')
83
+ : leaf.data;
101
84
  // An empty text's filler shares its node with what the browser typed, on either side.
102
85
  if (text.isEmpty)
103
- return value.replaceAll(ZERO_WIDTH_SPACE, '');
104
- if (text.endsWithNewline && value.endsWith(ZERO_WIDTH_SPACE)) {
105
- value = value.slice(0, -ZERO_WIDTH_SPACE.length);
106
- }
107
- return value;
86
+ return value.replaceAll(ZWSP, '');
87
+ return text.endsWithNewline && value.endsWith(ZWSP) ? value.slice(0, -1) : value;
108
88
  };
109
89
  export const getCollapsedDomTextSelection = (edytor) => {
110
90
  const selection = getDomSelectionSnapshot(edytor.node);
111
- if (!selection?.anchorNode || !selection.isCollapsed) {
112
- return null;
113
- }
114
- if (!edytor.node?.contains(selection.anchorNode)) {
115
- return null;
116
- }
117
- const text = edytor.selection.getTextOfNode(selection.anchorNode);
118
- if (!text?.node) {
119
- return null;
120
- }
91
+ const node = selection?.isCollapsed ? selection.anchorNode : null;
92
+ const text = node && edytor.node?.contains(node) ? edytor.selection.getTextOfNode(node) : null;
121
93
  // No model rule here: the DOM caret is read BEFORE the reconcile writes
122
94
  // the model, so a native insertion legitimately sits past `text.length`
123
95
  // (`selectText` clamps the final write).
124
- return { text, offset: domOffsetIn(text, selection.anchorNode, selection.anchorOffset) };
96
+ return text?.node ? { text, offset: domOffsetIn(text, node, selection.anchorOffset) } : null;
125
97
  };
126
98
  /** A native line break (a diff inserting only newlines at `at`) runs as the key's occurrence. */
127
99
  export const lineBreak = async (edytor, text, value, at) => {
@@ -137,17 +109,14 @@ export const lineBreak = async (edytor, text, value, at) => {
137
109
  const settle = (promises) => Promise.all(promises).then(() => undefined);
138
110
  /** The collapsed DOM caret's text offset inside `nodes` (a replacement's), if it is there. */
139
111
  const caretIn = (nodes) => {
140
- const selection = nodes[0]?.ownerDocument?.getSelection();
141
- const anchor = selection?.isCollapsed ? selection.anchorNode : null;
142
- if (!anchor)
143
- return undefined;
112
+ const selection = nodes[0].ownerDocument?.getSelection();
144
113
  let offset = 0;
145
- for (const node of nodes) {
146
- if (node.contains(anchor))
147
- return offset + getTextContentOffsetAtPoint(node, anchor, selection.anchorOffset);
148
- offset += node.textContent?.length ?? 0;
149
- }
150
- return undefined;
114
+ if (selection?.isCollapsed)
115
+ for (const node of nodes) {
116
+ if (node.contains(selection.anchorNode))
117
+ return (offset + getTextContentOffsetAtPoint(node, selection.anchorNode, selection.anchorOffset));
118
+ offset += node.textContent.length;
119
+ }
151
120
  };
152
121
  export class SurfaceObserver {
153
122
  edytor;
@@ -196,32 +165,23 @@ export class SurfaceObserver {
196
165
  #rendering = false;
197
166
  /** Passes under way (a pass's own repairs; an adoption's synchronous flush nests one). */
198
167
  #passes = 0;
199
- #window = null;
168
+ #window;
200
169
  constructor(edytor) {
201
170
  this.edytor = edytor;
202
171
  }
203
172
  /** Observe `root` (the host): records name what to compare. */
204
173
  attach = (root) => {
205
174
  const mo = (this.#mo = new MutationObserver((records) => {
206
- for (const record of records)
207
- this.#intake(record, this.#own());
175
+ this.#take(records);
208
176
  this.signal();
209
177
  }));
210
178
  for (const child of root.childNodes)
211
179
  this.#rooted.add(child);
212
180
  this.#rendering = false;
213
- mo.observe(root, {
214
- attributes: true,
215
- attributeOldValue: true,
216
- characterData: true,
217
- childList: true,
218
- subtree: true
219
- });
181
+ mo.observe(root, { attributes: true, characterData: true, childList: true, subtree: true });
220
182
  return () => {
221
183
  mo.disconnect();
222
- if (this.#mo === mo)
223
- this.#mo = null;
224
- clearTimeout(this.#window ?? undefined);
184
+ clearTimeout(this.#window);
225
185
  };
226
186
  };
227
187
  /**
@@ -249,16 +209,14 @@ export class SurfaceObserver {
249
209
  this.#contents.get(block)?.delete(node);
250
210
  };
251
211
  };
252
- /** The render anchor (`Edytor.svelte`): a registered child of the strict root. */
253
- anchor = (node) => {
254
- const release = this.register(node, 'anchor', '');
255
- return { destroy: release };
256
- };
257
- /** A suggestion's preview group (`components/Suggestion`): registered, view-only, never compared. */
258
- preview = (node, id) => {
259
- const release = this.register(node, 'preview', id);
260
- return { destroy: release };
261
- };
212
+ /**
213
+ * A view-only element the core renders in the host, never compared: the
214
+ * render anchor (`Edytor.svelte`, a registered child of the strict root),
215
+ * or with its suggestion's `id`, a preview group (`components/Suggestion`).
216
+ */
217
+ own = (node, id) => ({
218
+ destroy: this.register(node, id ? 'preview' : 'anchor', id ?? '')
219
+ });
262
220
  /** Extension view state rendered into the host, or a re-display request: an epoch bump. */
263
221
  update = () => void this.epoch++;
264
222
  /** Cells patched by a commit: the contents the flush re-renders (`before`: what the DOM shows). */
@@ -315,15 +273,14 @@ export class SurfaceObserver {
315
273
  void [edytor.selection.value, edytor.suggestions.revision];
316
274
  };
317
275
  /**
318
- * The editor writes the host now: the flush (from the pre pass to the end
319
- * of its compare pass) or a pass's own repairs. A record taken then is the
320
- * render's (`dom.anchors.foreign`); every other one is the browser's, a
321
- * page script's or an extension's.
276
+ * Take `records` (the callback's; else the queue's): each names elements to
277
+ * compare. The editor writes the host now: the flush (from the pre pass to
278
+ * the end of its compare pass) or a pass's own repairs. A record taken then
279
+ * is the render's (`dom.anchors.foreign`); every other one is the browser's,
280
+ * a page script's or an extension's.
322
281
  */
323
- #own = () => this.#rendering || this.#passes > 0;
324
- #take = () => {
325
- const records = this.#mo?.takeRecords() ?? [];
326
- const own = this.#own();
282
+ #take = (records = this.#mo?.takeRecords() ?? []) => {
283
+ const own = this.#rendering || this.#passes > 0;
327
284
  for (const record of records)
328
285
  this.#intake(record, own);
329
286
  return records.length > 0;
@@ -338,10 +295,13 @@ export class SurfaceObserver {
338
295
  return entry;
339
296
  }
340
297
  };
341
- /** The block an element belongs to: its closest registered element's (`null`: the root). */
298
+ /**
299
+ * The block an element belongs to: its closest registered element's
300
+ * (`null`: the root; `undefined`: a preview, the view's own render, or
301
+ * outside the host).
302
+ */
342
303
  #owner = (node) => {
343
304
  const entry = this.#near(node);
344
- // What happens in a preview is the view's own render: nothing to compare.
345
305
  return (entry && (entry.kind === 'preview' ? undefined : entry.kind === 'anchor' ? null : entry.block));
346
306
  };
347
307
  /** The handle a registered element of `kind` renders (`null`: another element). */
@@ -351,14 +311,11 @@ export class SurfaceObserver {
351
311
  };
352
312
  /** The block `node` is in (its closest registered element's), none in a preview or outside the host. */
353
313
  blockOf = (node) => {
354
- const entry = this.#near(node);
355
- return entry && entry.kind !== 'preview' && entry.kind !== 'anchor'
356
- ? (this.edytor.idToBlock.get(entry.block) ?? null)
357
- : null;
314
+ const id = node && this.#owner(node);
315
+ return id ? (this.edytor.idToBlock.get(id) ?? null) : null;
358
316
  };
359
- #registered = (node) => node instanceof Element && this.#registry.has(node);
360
317
  /** `node` is, or holds, an element the core still renders. */
361
- #holds = (node) => this.#registered(node) ||
318
+ #holds = (node) => this.#registry.has(node) ||
362
319
  (node instanceof Element &&
363
320
  [...node.querySelectorAll('*')].some((child) => this.#registry.has(child)));
364
321
  /** A record names elements to compare (never a verdict); `own`: the editor wrote it. */
@@ -370,32 +327,28 @@ export class SurfaceObserver {
370
327
  this.#dirty.add(id);
371
328
  if (id)
372
329
  this.#seen(id);
373
- if (record.type === 'attributes')
330
+ const { type, target: parent, nextSibling: next, previousSibling: prev } = record;
331
+ if (type === 'attributes')
374
332
  this.#attributes.add(record);
375
- if (record.type !== 'childList')
333
+ if (type !== 'childList')
376
334
  return;
377
- const rooted = record.target === this.edytor.node;
378
- for (const node of record.addedNodes) {
335
+ const added = [...record.addedNodes];
336
+ for (const node of added) {
379
337
  this.#added.add(node);
380
- if (rooted)
338
+ if (parent === this.edytor.node)
381
339
  this.#rooted.add(node);
382
340
  }
383
- const removed = [...record.removedNodes];
384
- if (!removed.some((node) => isAnchor(node) || this.#holds(node)))
341
+ const nodes = [...record.removedNodes];
342
+ if (!nodes.some((node) => isAnchor(node) || this.#holds(node)))
385
343
  return;
386
- const added = [...record.addedNodes];
387
- const { target: parent, nextSibling: next, previousSibling: prev } = record;
388
- this.#removed.push({ parent, nodes: removed, added, next, prev, foreign: !own });
344
+ this.#removed.push({ parent, nodes, added, next, prev, foreign: !own });
389
345
  // Wrapper replacement: the browser replaced a text element with its own nodes.
390
- const [text] = removed;
391
- if (removed.length === 1 &&
392
- this.#registry.get(text)?.kind === 'text' &&
393
- added.length &&
394
- !added.some((node) => this.#holds(node) || node.nodeName === 'BR')) {
395
- const value = added.map((node) => node.textContent ?? '').join('');
396
- if (value)
397
- this.#replaced.set(text, { nodes: added, value, caret: caretIn(added) });
398
- }
346
+ const value = added.map((node) => node.textContent).join('');
347
+ if (nodes.length === 1 &&
348
+ this.#registry.get(nodes[0])?.kind === 'text' &&
349
+ value &&
350
+ !added.some((node) => this.#holds(node) || node.nodeName === 'BR'))
351
+ this.#replaced.set(nodes[0], { nodes: added, value, caret: caretIn(added) });
399
352
  };
400
353
  /**
401
354
  * Records name a content while the model still equals its render (no patch
@@ -407,15 +360,17 @@ export class SurfaceObserver {
407
360
  if (!cell || this.#patched.has(id))
408
361
  return;
409
362
  const text = textOf(cell);
410
- if (this.#bases.get(id)?.text !== text)
411
- this.#bases.set(id, { text, edit: null });
363
+ let base = this.#bases.get(id);
364
+ if (base?.text !== text)
365
+ this.#bases.set(id, (base = { text, edit: null }));
412
366
  const dom = this.#read(id)?.text;
413
- const known = this.#bases.get(id);
414
- if (dom === undefined || dom === text || known.edit?.dom === dom)
367
+ if (dom === undefined || dom === text || base.edit?.dom === dom)
415
368
  return;
416
369
  const at = diffText(text, dom).at;
417
- const anchor = this.edytor.facade.anchorAt(id, at, at < text.length ? 'right' : 'left');
418
- known.edit = { dom, anchor };
370
+ base.edit = {
371
+ dom,
372
+ anchor: this.edytor.facade.anchorAt(id, at, at < text.length ? 'right' : 'left')
373
+ };
419
374
  };
420
375
  /**
421
376
  * A record that removed registered elements is inverted whole (newest
@@ -438,77 +393,57 @@ export class SurfaceObserver {
438
393
  const root = this.edytor.node;
439
394
  const removals = this.#removed.splice(0).reverse();
440
395
  const held = ({ parent, nodes }) => nodes.some((node) => node.parentNode !== parent && this.#holds(node));
396
+ // A foreign record took only the render's anchors, and they are gone from
397
+ // the page (not moved): they can return beside a recorded sibling still
398
+ // there (never at the parent's end).
399
+ const lost = ({ parent, nodes, next, prev, foreign }) => foreign &&
400
+ nodes.every((node) => isAnchor(node) && !node.isConnected) &&
401
+ (next?.parentNode === parent || prev?.parentNode === parent);
441
402
  const parents = new Set(removals.filter(held).map(({ parent }) => parent));
442
- const crossed = removals.some((removal) => this.#lost(removal) && this.#list(removal.parent));
403
+ // Took anchors from a block list: the root, or a parent of block elements (a block's children).
404
+ const crossed = removals.some((removal) => lost(removal) &&
405
+ (removal.parent === root ||
406
+ [...removal.parent.childNodes].some((child) => this.#registry.get(child)?.kind === 'block')));
443
407
  for (const removal of removals) {
444
408
  const { parent, nodes, added, next, prev } = removal;
445
409
  if (!root?.contains(parent))
446
410
  continue;
447
411
  const anchors = parents.has(parent) && nodes.every(isAnchor);
448
- if (!held(removal) && !anchors && !(crossed && this.#lost(removal)))
412
+ if (!held(removal) && !anchors && !(crossed && lost(removal)))
449
413
  continue;
450
414
  // A browser-owned attempt's host: its change is adopted whatever the
451
415
  // browser did to its structure, which the re-render then restores.
452
416
  const near = this.#near(parent);
453
- const inside = near?.kind === 'text' && near.handle;
454
- if (inside && this.edytor.attempts.on(inside))
417
+ if (near?.kind === 'text' && this.edytor.attempts.on(near.handle))
455
418
  continue;
456
419
  for (const node of added)
457
420
  if (node.parentNode === parent && !this.#holds(node))
458
421
  parent.removeChild(node);
459
422
  const before = next?.parentNode === parent ? next : prev?.parentNode === parent ? prev.nextSibling : null;
460
- for (const node of nodes)
423
+ for (const node of nodes) {
461
424
  if (node.parentNode !== parent)
462
425
  parent.insertBefore(node, before);
463
- this.#wrote = true;
464
- for (const node of nodes) {
465
- const replaced = node instanceof Element && this.#replaced.get(node);
466
426
  // The replacement's text is the browser's edit: adopted into the element's text.
427
+ const replaced = this.#replaced.get(node);
467
428
  if (replaced)
468
429
  this.#replacements.set(node, replaced);
469
430
  }
431
+ this.#wrote = true;
470
432
  }
471
433
  this.#replaced.clear();
472
434
  };
473
- /**
474
- * A foreign record took only the render's anchors, and they are gone from
475
- * the page (not moved): they can return beside a recorded sibling still
476
- * there (never at the parent's end).
477
- */
478
- #lost = ({ parent, nodes, next, prev, foreign }) => foreign &&
479
- nodes.every((node) => isAnchor(node) && !node.isConnected) &&
480
- (next?.parentNode === parent || prev?.parentNode === parent);
481
- /** A block list: the root, or a parent of block elements (a block's children). */
482
- #list = (parent) => parent === this.edytor.node ||
483
- [...parent.childNodes].some((child) => child instanceof Element && this.#registry.get(child)?.kind === 'block');
484
- /**
485
- * Where an edit made at `at` of the base lands now: through the anchor
486
- * minted at it (it follows its text through a re-key, split, move or
487
- * retype), else rebased within the block over the model's own change
488
- * (three-way prefix/suffix).
489
- */
490
- #place = (id, base, dom, at, now) => {
491
- const to = base.edit?.dom === dom &&
492
- base.edit.anchor &&
493
- this.edytor.facade.resolveAnchor(base.edit.anchor);
494
- if (to)
495
- return { block: to.blockId, at: to.offset };
496
- const model = diffText(base.text, now);
497
- const before = model && model.at + model.remove <= at;
498
- return { block: id, at: at + (before ? model.insert.length - model.remove : 0) };
499
- };
500
435
  /** Only a content this flush re-renders can lose an edit to its writes: snapshot it. */
501
436
  #snapshot = () => {
502
- if (this.edytor.readonly)
437
+ const { edytor } = this;
438
+ if (edytor.readonly)
503
439
  return;
504
440
  for (const id of this.#rerendered) {
505
- if (id === this.#host)
506
- continue;
507
441
  const base = this.#bases.get(id);
508
- const dom = base && this.#read(id);
509
- const cell = this.edytor.cells?.get(id);
510
- if (!dom || !cell || dom.text === base.text || dom.text === textOf(cell))
442
+ const read = base && id !== this.#host && this.#read(id);
443
+ const cell = edytor.cells?.get(id);
444
+ if (!read || !cell || read.text === base.text || read.text === textOf(cell))
511
445
  continue;
446
+ const dom = read.text;
512
447
  // An expectation claims the edit, or it is placed through the anchor minted at it.
513
448
  const verdict = this.#divergence(id);
514
449
  // An edit the window defers is drift: the flush's render restores the cell.
@@ -516,9 +451,17 @@ export class SurfaceObserver {
516
451
  this.#snapshots.set(id, verdict === 'defer' ? 'invert' : verdict);
517
452
  continue;
518
453
  }
519
- const change = diffText(base.text, dom.text);
520
- const place = this.#place(id, base, dom.text, change.at, textOf(cell));
521
- this.#snapshots.set(id, { ...change, ...place });
454
+ // Where it lands now: through the anchor minted at it (it follows its text
455
+ // through a re-key, split, move or retype), else rebased within the block
456
+ // over the model's own change (three-way prefix/suffix).
457
+ const change = diffText(base.text, dom);
458
+ const to = base.edit?.dom === dom && base.edit.anchor && edytor.facade.resolveAnchor(base.edit.anchor);
459
+ const model = !to && diffText(base.text, textOf(cell));
460
+ const at = to
461
+ ? to.offset
462
+ : change.at +
463
+ (model && model.at + model.remove <= change.at ? model.insert.length - model.remove : 0);
464
+ this.#snapshots.set(id, { ...change, block: to ? to.blockId : id, at });
522
465
  }
523
466
  };
524
467
  /** The live composition host's block. */
@@ -547,8 +490,9 @@ export class SurfaceObserver {
547
490
  #compare = (flushed) => {
548
491
  const { edytor } = this;
549
492
  const cells = edytor.cells;
493
+ const work = [];
550
494
  if (!cells || !edytor.node?.isConnected || edytor.destroyed)
551
- return [];
495
+ return work;
552
496
  const live = this.#live();
553
497
  // The hand-back: the IME host returns to the compare pass at the session's end.
554
498
  if (this.#host !== live) {
@@ -558,71 +502,62 @@ export class SurfaceObserver {
558
502
  }
559
503
  this.#heal();
560
504
  if (edytor.readonly) {
505
+ // The root is checked at the flip back (its strict check finds what diverged).
561
506
  for (const id of this.#dirty)
562
- if (this.#diverges(id))
507
+ if (id === null || this.#diverges(id))
563
508
  this.#readonly.add(id);
564
509
  this.#dirty.clear();
565
- if (this.#wrote)
566
- this.update();
567
- this.#wrote = false;
568
- return [];
569
510
  }
570
- // The flip back: what diverged while read-only is compared again; and
571
- // what waited for a model-owned attempt's drift window, once it closed.
572
- for (const id of this.#readonly)
573
- this.#dirty.add(id);
574
- if (!edytor.attempts.busy) {
575
- for (const id of this.#waiting)
511
+ else {
512
+ // The flip back: what diverged while read-only is compared again; and
513
+ // what waited for a model-owned attempt's drift window, once it closed.
514
+ for (const id of this.#readonly)
576
515
  this.#dirty.add(id);
577
- this.#waiting.clear();
578
- }
579
- this.#restore();
580
- this.#clones();
581
- const work = [];
582
- const dirty = [...this.#dirty].filter((id) => flushed || id === null || !this.#patched.has(id));
583
- for (const id of dirty)
584
- this.#dirty.delete(id);
585
- if (flushed) {
586
- this.#patched.clear();
587
- this.#rerendered.clear();
588
- }
589
- for (const id of dirty) {
590
- if (id === null)
591
- this.#strictRoot();
592
- // A content this pass's own adoption re-rendered waits for that flush.
593
- else if (!this.#patched.has(id))
594
- work.push(...this.#resolve(id, cells.get(id)));
516
+ if (!edytor.attempts.busy) {
517
+ for (const id of this.#waiting)
518
+ this.#dirty.add(id);
519
+ this.#waiting.clear();
520
+ }
521
+ this.#restore();
522
+ this.#clones();
523
+ const dirty = [...this.#dirty].filter((id) => flushed || id === null || !this.#patched.has(id));
524
+ for (const id of dirty)
525
+ this.#dirty.delete(id);
526
+ if (flushed) {
527
+ this.#patched.clear();
528
+ this.#rerendered.clear();
529
+ }
530
+ for (const id of dirty) {
531
+ if (id === null)
532
+ this.#strictRoot();
533
+ // A content this pass's own adoption re-rendered waits for that flush.
534
+ else if (!this.#patched.has(id))
535
+ work.push(...this.#resolve(id, cells.get(id)));
536
+ }
537
+ this.#readonly.clear();
538
+ if (this.#fresh) {
539
+ this.#fresh = false;
540
+ void settle(work).then(edytor.projector.rendered);
541
+ }
595
542
  }
596
- this.#readonly.clear();
597
543
  if (this.#wrote) {
598
544
  this.#wrote = false;
599
545
  this.update();
600
546
  }
601
- if (this.#fresh) {
602
- this.#fresh = false;
603
- void settle(work).then(edytor.projector.rendered);
604
- }
605
547
  return work;
606
548
  };
607
549
  /** A content differs from its cell (text, structure or a missing element). */
608
- #diverges = (id) => {
609
- if (id === null)
610
- return this.#rootDiverges();
550
+ #diverges = (id, dom = id !== this.#host && this.#read(id)) => {
611
551
  const cell = this.edytor.cells?.get(id);
612
- const dom = cell && id !== this.#host && this.#read(id);
613
- return Boolean(dom && (dom.broken || dom.text !== textOf(cell) || this.#shape(id)));
552
+ return !!(cell && dom && (dom.broken || dom.text !== textOf(cell) || this.#shape(id)));
614
553
  };
615
554
  /** One content against its cell, and what that asks for. */
616
555
  #resolve = (id, cell) => {
617
556
  const snapshot = this.#snapshots.get(id);
618
557
  this.#snapshots.delete(id);
619
- if (!cell) {
558
+ if (!cell)
620
559
  this.#bases.delete(id);
621
- return [];
622
- }
623
- if (id === this.#host)
624
- return [];
625
- const dom = this.#read(id);
560
+ const dom = cell && id !== this.#host && this.#read(id);
626
561
  if (!dom)
627
562
  return [];
628
563
  // What the DOM shows once this pass acted: the base of the next edit.
@@ -636,26 +571,21 @@ export class SurfaceObserver {
636
571
  this.#invert(id);
637
572
  return [];
638
573
  }
639
- const want = textOf(cell);
640
- const replaced = this.#texts(id).some((text) => text.node && this.#replacements.has(text.node));
641
- if (dom.broken || this.#readonly.has(id)) {
642
- if (dom.broken || dom.text !== want || this.#shape(id))
643
- this.#invert(id);
644
- return [];
645
- }
646
- if (dom.text === want && !replaced) {
647
- if (this.#shape(id))
574
+ const replaced = this.#texts(id).some((text) => this.#replacements.has(text.node));
575
+ // Broken, read-only or equal text: the cell is restored when it differs.
576
+ if (dom.broken || this.#readonly.has(id) || (dom.text === textOf(cell) && !replaced)) {
577
+ if (this.#diverges(id, dom))
648
578
  this.#invert(id);
649
579
  return [];
650
580
  }
651
581
  const verdict = this.#divergence(id);
652
582
  if (verdict === 'defer')
653
583
  this.#waiting.add(id);
654
- if (verdict === 'invert')
584
+ else if (verdict === 'invert')
655
585
  this.#invert(id);
656
- if (verdict === 'defer' || verdict === 'invert')
657
- return [];
658
- return this.#adoptContent(id);
586
+ else
587
+ return this.#adoptContent(id);
588
+ return [];
659
589
  };
660
590
  /**
661
591
  * A text divergence: an expectation on its host claims it (the composition
@@ -691,9 +621,6 @@ export class SurfaceObserver {
691
621
  else
692
622
  this.update();
693
623
  };
694
- /** A text element's children that stand for render deltas (Svelte's anchors and the newline marker aside). */
695
- #nodes = (element) => [...element.childNodes].filter((node) => !isAnchor(node) &&
696
- !(node instanceof Element && node.hasAttribute('data-edytor-trailing-newline')));
697
624
  /**
698
625
  * Adopt the browser's text of each text element of `id` (one command each,
699
626
  * through the dispatcher); a native line break runs as its occurrence. A
@@ -705,20 +632,16 @@ export class SurfaceObserver {
705
632
  const caret = getCollapsedDomTextSelection(edytor);
706
633
  const work = [];
707
634
  for (const text of this.#texts(id)) {
708
- const replaced = text.node && this.#replacements.get(text.node);
709
- if (replaced)
710
- this.#replacements.delete(text.node);
635
+ const replaced = this.#replacements.get(text.node);
636
+ this.#replacements.delete(text.node);
711
637
  const dom = replaced ? replaced.value : readDomText(text);
712
638
  if (dom === text.stringContent)
713
639
  continue;
714
- broken ||= Boolean(replaced);
715
- const at = replaced ? replaced.caret : caret?.text === text ? caret.offset : undefined;
640
+ broken ||= !!replaced;
716
641
  const line = diffText(text.stringContent, dom);
717
- if (line && !line.remove && /^[\r\n]+$/.test(line.insert)) {
718
- work.push(lineBreak(edytor, text, dom, line.at));
719
- continue;
720
- }
721
- work.push(adopt(edytor, text, dom, at));
642
+ work.push(line && !line.remove && /^[\r\n]+$/.test(line.insert)
643
+ ? lineBreak(edytor, text, dom, line.at)
644
+ : adopt(edytor, text, dom, replaced ? replaced.caret : caret?.text === text ? caret.offset : undefined));
722
645
  }
723
646
  if (broken)
724
647
  this.#invert(id);
@@ -745,17 +668,15 @@ export class SurfaceObserver {
745
668
  let text = '';
746
669
  let broken = !!host && !host.isConnected;
747
670
  for (const element of elements) {
748
- const kind = this.#registry.get(element)?.kind;
671
+ const entry = this.#registry.get(element);
749
672
  // A mark is inside its text element: its presence is the shape check's.
750
- if (kind === 'mark')
673
+ if (entry?.kind === 'mark')
751
674
  continue;
752
675
  broken ||= !element.isConnected || (!!host && !host.contains(element));
753
- if (kind === 'atom')
676
+ if (entry?.kind === 'atom')
754
677
  text += ATOM;
755
- else if (kind === 'text') {
756
- const found = this.handleOf(element, 'text');
757
- text += (found ? readDomText(found) : (element.textContent ?? '')).replace(ZWSP, '');
758
- }
678
+ else if (entry?.kind === 'text')
679
+ text += readDomText(entry.handle).replaceAll(ZWSP, '');
759
680
  }
760
681
  return { text, broken };
761
682
  };
@@ -763,19 +684,21 @@ export class SurfaceObserver {
763
684
  * Equal text, compared structurally: each text element (a strict
764
685
  * container) holds one node per render delta (the empty filler's one) — a
765
686
  * text node, or a registered mark element whose registered marks follow
766
- * the delta's chain and that renders one text node. Answers whether it
767
- * differs; `write`: a content that does not gets each delta's text back
768
- * in its node.
687
+ * the delta's chain and that renders one text node. Svelte's anchors and
688
+ * the newline marker stand for none. Answers whether it differs; `write`:
689
+ * a content that does not gets each delta's text back in its node.
769
690
  */
770
691
  #shape = (id, write) => {
692
+ const { edytor } = this;
771
693
  const writes = [];
772
694
  for (const text of this.#texts(id)) {
773
695
  const element = text.node;
774
696
  if (!element)
775
697
  continue;
776
- const deltas = this.edytor.deltasOf(text);
777
- const shown = deltas.length ? deltas : [{ text: '\u200B', marks: [] }];
778
- const nodes = this.#nodes(element);
698
+ const deltas = edytor.deltasOf(text);
699
+ const shown = deltas.length ? deltas : [{ text: ZWSP, marks: [] }];
700
+ const nodes = [...element.childNodes].filter((node) => !isAnchor(node) &&
701
+ !(node instanceof Element && node.hasAttribute('data-edytor-trailing-newline')));
779
702
  if (nodes.length !== shown.length)
780
703
  return true;
781
704
  for (const [i, delta] of shown.entries()) {
@@ -783,17 +706,18 @@ export class SurfaceObserver {
783
706
  let leaf = node;
784
707
  const chain = delta.marks
785
708
  .map(([name]) => name)
786
- .filter((name) => rendersMark(this.edytor.marks, name));
709
+ .filter((name) => rendersMark(edytor.marks, name));
787
710
  if (chain.length) {
788
711
  if (!this.#registry.has(node))
789
712
  return true;
790
713
  const marks = [node, ...node.querySelectorAll('[data-edytor-mark]')]
791
714
  .map((el) => this.#registry.get(el)?.name)
792
715
  .filter(Boolean);
793
- leaf = textLeaf(node);
716
+ const leaves = textLeaves(node).filter((leaf) => leaf.data);
794
717
  // The snippet's markup is its own, but the delta renders one text node.
795
- if (marks.join() !== chain.join() || !leaf || textLeaf(node, 1))
718
+ if (marks.join() !== chain.join() || leaves.length !== 1)
796
719
  return true;
720
+ leaf = leaves[0];
797
721
  }
798
722
  else if (node.nodeType !== Node.TEXT_NODE)
799
723
  return true;
@@ -806,34 +730,14 @@ export class SurfaceObserver {
806
730
  leaf.data = value;
807
731
  return false;
808
732
  };
809
- /** The root's children differ from its registered block elements in cell order, the anchor, Svelte's anchors. */
810
- #rootDiverges = () => {
811
- const { node, cells } = this.edytor;
812
- if (!node || !cells)
813
- return false;
814
- const shown = [];
815
- for (const child of node.childNodes) {
816
- if (isAnchor(child))
817
- continue;
818
- const entry = child instanceof Element ? this.#registry.get(child) : undefined;
819
- if (entry?.kind === 'block')
820
- shown.push(entry.block);
821
- else if (entry?.kind !== 'anchor' && entry?.kind !== 'preview')
822
- return true;
823
- }
824
- return (cells.rootIds.some((id) => this.#blocks.get(id)?.parentNode !== node) ||
825
- shown.join() !== cells.rootIds.join());
826
- };
827
733
  /**
828
734
  * The root is a strict container: a child it does not render is removed.
829
735
  * Only a node a record put there can be one (`#rooted`).
830
736
  */
831
737
  #strictRoot = () => {
832
738
  const node = this.edytor.node;
833
- if (!node)
834
- return;
835
739
  for (const child of this.#rooted)
836
- if (child.parentNode === node && !isAnchor(child) && !this.#registered(child)) {
740
+ if (child.parentNode === node && !isAnchor(child) && !this.#registry.has(child)) {
837
741
  node.removeChild(child);
838
742
  this.#wrote = true;
839
743
  }
@@ -849,7 +753,7 @@ export class SurfaceObserver {
849
753
  #clones = () => {
850
754
  const root = this.edytor.node;
851
755
  for (const node of this.#added) {
852
- if (!node.isConnected || !root.contains(node) || isAnchor(node) || this.#holds(node))
756
+ if (!root.contains(node) || isAnchor(node) || this.#holds(node))
853
757
  continue;
854
758
  if (node instanceof Element && node.matches(REST))
855
759
  continue;
@@ -860,7 +764,7 @@ export class SurfaceObserver {
860
764
  this.#claims(element));
861
765
  if (!clone && !this.#inRun(node))
862
766
  continue;
863
- node.parentNode?.removeChild(node);
767
+ node.remove();
864
768
  this.#wrote = true;
865
769
  }
866
770
  this.#added.clear();
@@ -868,21 +772,20 @@ export class SurfaceObserver {
868
772
  /** `node` sits between two registered text or atom elements of one content. */
869
773
  #inRun = (node) => {
870
774
  const side = (step) => {
871
- for (let at = step(node); at; at = step(at)) {
872
- const entry = at instanceof Element ? this.#registry.get(at) : undefined;
775
+ for (let at = node[step]; at; at = at[step]) {
776
+ const entry = this.#registry.get(at);
873
777
  if (entry)
874
778
  return entry.kind === 'text' || entry.kind === 'atom' ? entry.block : null;
875
779
  }
876
780
  return null;
877
781
  };
878
- const before = side((at) => at.previousSibling);
879
- return before !== null && before === side((at) => at.nextSibling);
782
+ const before = side('previousSibling');
783
+ return before !== null && before === side('nextSibling');
880
784
  };
881
785
  /** A `data-edytor-mark` naming no declared mark claims nothing: its element is foreign input. */
882
- #claims = (element) => IDENTITY_ATTRIBUTES.some((name) => element.hasAttribute(name) &&
883
- (name !== 'data-edytor-mark' ||
884
- this.edytor.marks.has(element.getAttribute(name) ?? '') ||
885
- element.hasAttribute('data-edytor'))) || element.hasAttribute('data-edytor');
786
+ #claims = (element) => element.hasAttribute('data-edytor') ||
787
+ IDENTITY_ATTRIBUTES.some((name) => element.hasAttribute(name) &&
788
+ (name !== 'data-edytor-mark' || this.edytor.marks.has(element.getAttribute(name))));
886
789
  /**
887
790
  * Owned attributes against the table (read-only views included); an
888
791
  * identity attribute on an unregistered element is stripped; damage on a
@@ -897,35 +800,33 @@ export class SurfaceObserver {
897
800
  this.#attributes.delete(record);
898
801
  continue;
899
802
  }
900
- const entry = this.#registry.get(element) ?? null;
803
+ const entry = this.#registry.get(element);
901
804
  const owner = this.#owner(element);
902
805
  if (owner && owner === this.#host)
903
806
  continue;
904
807
  if (this.#heals > BOUND)
905
808
  break;
906
809
  this.#attributes.delete(record);
907
- const name = record.attributeName ?? '';
810
+ const name = record.attributeName;
908
811
  const table = ownedOf(edytor, element, entry);
909
812
  // A mark element's own attributes are its render: damage there re-renders the content.
910
813
  if (entry?.kind === 'mark') {
911
814
  if (table && owner && diverges(element, table))
912
815
  edytor.cells?.remount(owner);
913
- continue;
914
816
  }
915
- if (!table) {
916
- if (IDENTITY_ATTRIBUTES.includes(name) &&
917
- element.hasAttribute(name) &&
918
- !element.closest(SUGGESTION)) {
919
- element.removeAttribute(name);
817
+ else if (table) {
818
+ if (diverges(element, table) && heal(element, table)) {
920
819
  this.#counted();
820
+ if (element === edytor.node)
821
+ this.#refocus();
921
822
  }
922
- continue;
923
823
  }
924
- if (!diverges(element, table) || !heal(element, table))
925
- continue;
926
- this.#counted();
927
- if (element === edytor.node)
928
- this.#refocus();
824
+ else if (IDENTITY_ATTRIBUTES.includes(name) &&
825
+ element.hasAttribute(name) &&
826
+ !element.closest(SUGGESTION)) {
827
+ element.removeAttribute(name);
828
+ this.#counted();
829
+ }
929
830
  }
930
831
  };
931
832
  /**
@@ -946,9 +847,8 @@ export class SurfaceObserver {
946
847
  };
947
848
  if (again())
948
849
  setTimeout(() => {
949
- if (edytor.intentSerial !== serial)
950
- return;
951
- if (!edytor.node?.ownerDocument.getSelection()?.rangeCount)
850
+ if (edytor.intentSerial === serial &&
851
+ !edytor.node?.ownerDocument.getSelection()?.rangeCount)
952
852
  again();
953
853
  });
954
854
  };
@@ -957,7 +857,6 @@ export class SurfaceObserver {
957
857
  if (++this.#heals === 1)
958
858
  this.#window = setTimeout(() => {
959
859
  this.#heals = 0;
960
- this.#window = null;
961
860
  if (this.#attributes.size)
962
861
  this.signal();
963
862
  }, WINDOW_MS);
@@ -966,23 +865,18 @@ export class SurfaceObserver {
966
865
  /** The rendered marks (a tag or a snippet) the browser shows at `at` of `text`: the element chain of its node there. */
967
866
  const shownMarks = (text, at) => {
968
867
  const element = text.node;
969
- if (!element)
970
- return null;
971
- const walker = element.ownerDocument.createTreeWalker(element, NodeFilter.SHOW_TEXT);
972
868
  let offset = 0;
973
- for (let node = walker.nextNode(); node; node = walker.nextNode()) {
974
- offset += node.data.replace(ZWSP, '').length;
975
- if (offset <= at)
976
- continue;
977
- const names = [];
978
- for (let el = node.parentElement; el && el !== element; el = el.parentElement) {
979
- const name = el.getAttribute('data-edytor-mark');
980
- if (name)
981
- names.push(name);
982
- }
983
- return names.sort().join();
869
+ const node = element &&
870
+ textLeaves(element).find((leaf) => (offset += leaf.data.replaceAll(ZWSP, '').length) > at);
871
+ if (!node)
872
+ return null;
873
+ const names = [];
874
+ for (let el = node.parentElement; el && el !== element; el = el.parentElement) {
875
+ const name = el.getAttribute('data-edytor-mark');
876
+ if (name)
877
+ names.push(name);
984
878
  }
985
- return null;
879
+ return names.sort().join();
986
880
  };
987
881
  /** The rendered marks the model gives the character at `at` of `text`. */
988
882
  const modelMarks = (edytor, text, at) => Object.keys(activeMarks(text.getMarksAtRange(at, at + 1)[0]?.marks))
@@ -1003,35 +897,35 @@ const modelMarks = (edytor, text, at) => Object.keys(activeMarks(text.getMarksAt
1003
897
  */
1004
898
  export const adopt = async (edytor, text, dom, domCaret, prefer) => {
1005
899
  if (!text.isInDocument)
1006
- return false;
900
+ return;
901
+ const { attempts, selection } = edytor;
1007
902
  const serial = edytor.intentSerial;
1008
- const attempt = edytor.attempts.on(text);
903
+ const attempt = attempts.on(text);
1009
904
  const after = attempt?.expect?.kind === 'change' ? attempt.expect.after : null;
1010
- const drifted = after !== null && dom !== after && edytor.attempts.drifting(text);
905
+ const drifted = after !== null && dom !== after && attempts.drifting(text);
1011
906
  const value = drifted ? after : dom;
1012
907
  const caret = drifted ? undefined : domCaret;
1013
908
  if (attempt)
1014
- edytor.selection.select(attempt.target);
1015
- const { startText, yStart } = edytor.selection.textRange();
909
+ selection.select(attempt.target);
910
+ const { startText, yStart } = selection.textRange();
1016
911
  const grow = value.length - text.length;
1017
912
  const back = attempt?.isCollapsed && INTENTS[attempt.inputType]?.dir === 'back';
1018
913
  const change = diffText(text.stringContent, value, attempt && startText === text ? yStart + (back ? grow : Math.max(0, grow)) : (prefer ?? caret));
1019
914
  if (!change)
1020
- return false;
915
+ return;
1021
916
  const { at, remove, insert } = change;
1022
917
  // The command runs at the change (a hook reads the selection).
1023
918
  if (startText !== text && (attempt || caret !== undefined))
1024
- edytor.selection.select(edytor.selection.textValue(text, at));
919
+ selection.select(selection.textValue(text, at));
920
+ const replaced = remove ? text.getMarksAtRange(at, at + remove) : undefined;
1025
921
  // A deletion of uniformly marked text keeps its marks pending.
1026
- const removed = insert ? [] : text.getMarksAtRange(at, at + remove);
922
+ const removed = insert ? [] : replaced;
1027
923
  const marks = activeMarks(removed[0]?.marks);
1028
924
  const same = removed.every((part) => jsonEquals(activeMarks(part.marks), marks));
1029
925
  const shown = insert && shownMarks(text, at);
1030
- const kind = attempt ? attempt.inputType || (insert ? 'insertText' : 'deleteContent') : null;
1031
- edytor.dispatcher.run(kind ?? 'foreignChange', () => {
926
+ edytor.dispatcher.run(attempt ? attempt.inputType || (insert ? 'insertText' : 'deleteContent') : 'foreignChange', () => {
1032
927
  const [id, from] = [text.parent.id, text.segStart + at];
1033
928
  // A replaced range's common marks; else the view's insertion rule.
1034
- const replaced = remove ? text.getMarksAtRange(at, at + remove) : undefined;
1035
929
  const marks = replaced && marksForInsertion(text, at, { replaced });
1036
930
  if (remove)
1037
931
  edytor.deleteText(id, from, remove);
@@ -1042,28 +936,23 @@ export const adopt = async (edytor, text, dom, domCaret, prefer) => {
1042
936
  if (attempt)
1043
937
  attempt.phase = adopted ? 'applied' : 'failed';
1044
938
  if (!text.isInDocument)
1045
- return true;
939
+ return;
1046
940
  // The render patches only the runs the command changed: when the one
1047
941
  // insertion rule (`marksForInsertion`) gave the text other marks than the
1048
942
  // node the browser wrote, that node keeps its copy — re-render.
1049
- const moved = adopted && insert !== '' && shown !== modelMarks(edytor, text, at);
1050
- if (!adopted || drifted || moved)
943
+ if (!adopted || drifted || (insert && shown !== modelMarks(edytor, text, at)))
1051
944
  text.refreshFromModel();
1052
945
  // Deferred past the re-render (selecting in the same turn left the
1053
946
  // native caret at 0), so it checks the gesture serial: a gesture since the
1054
947
  // adoption owns the selection, and its caret and pending marks stand
1055
948
  // (review 2026-09-29).
1056
949
  await tick();
1057
- if (edytor.intentSerial !== serial)
1058
- return true;
950
+ if (!adopted || edytor.intentSerial !== serial)
951
+ return;
1059
952
  const end = caret ?? (attempt ? at + insert.length : undefined);
1060
- if (adopted && end !== undefined)
1061
- edytor.selection.selectText(text, Math.min(end, text.length));
953
+ if (end !== undefined)
954
+ selection.selectText(text, Math.min(end, text.length));
1062
955
  // A deletion of uniformly marked text keeps its marks pending at the caret.
1063
- if (adopted &&
1064
- same &&
1065
- Object.keys(marks).length > 0 &&
1066
- edytor.selection.textRange().startText === text)
1067
- edytor.selection.stage(marks);
1068
- return true;
956
+ if (same && Object.keys(marks).length > 0 && selection.textRange().startText === text)
957
+ selection.stage(marks);
1069
958
  };