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