edytor 4.0.0 → 6.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 (193) hide show
  1. package/README.md +2 -2
  2. package/dist/block/block.svelte.js +6 -21
  3. package/dist/block/inlineBlock.svelte.js +4 -6
  4. package/dist/clipboard/moveText.js +6 -8
  5. package/dist/cloudflare/DocumentRoom.d.ts +10 -3
  6. package/dist/cloudflare/index.d.ts +1 -1
  7. package/dist/cloudflare/room/access.d.ts +8 -3
  8. package/dist/cloudflare/room/access.js +44 -20
  9. package/dist/cloudflare/room/admission.js +28 -5
  10. package/dist/cloudflare/room/comments.js +28 -1
  11. package/dist/cloudflare/room/context.d.ts +106 -109
  12. package/dist/cloudflare/room/context.js +10 -3
  13. package/dist/cloudflare/room/forged.d.ts +14 -0
  14. package/dist/cloudflare/room/forged.js +30 -2
  15. package/dist/cloudflare/room/frames.d.ts +4 -0
  16. package/dist/cloudflare/room/frames.js +5 -1
  17. package/dist/cloudflare/room/purge.js +2 -1
  18. package/dist/cloudflare/room/replicas.d.ts +4 -2
  19. package/dist/cloudflare/room/replicas.js +8 -3
  20. package/dist/cloudflare/room/suggest.d.ts +7 -0
  21. package/dist/cloudflare/room/suggest.js +143 -0
  22. package/dist/cloudflare/routeDocumentSocket.d.ts +10 -1
  23. package/dist/cloudflare/routeDocumentSocket.js +5 -3
  24. package/dist/components/Content.svelte +9 -1
  25. package/dist/crdt/anchors.js +3 -2
  26. package/dist/crdt/attribution/attribution.d.ts +3 -0
  27. package/dist/crdt/attribution/attribution.js +3 -3
  28. package/dist/crdt/doc/funnel.d.ts +1 -1
  29. package/dist/crdt/doc/funnel.js +5 -2
  30. package/dist/crdt/doc/gate.d.ts +1 -1
  31. package/dist/crdt/doc/history.d.ts +6 -0
  32. package/dist/crdt/doc/history.js +79 -59
  33. package/dist/crdt/doc/ops.d.ts +81 -0
  34. package/dist/crdt/doc/ops.js +1898 -0
  35. package/dist/crdt/doc/reads.d.ts +2 -2
  36. package/dist/crdt/doc/reads.js +18 -28
  37. package/dist/crdt/doc/suggest.d.ts +167 -0
  38. package/dist/crdt/doc/suggest.js +579 -0
  39. package/dist/crdt/document.d.ts +10 -1
  40. package/dist/crdt/document.js +19 -11
  41. package/dist/crdt/edytor-doc.d.ts +294 -272
  42. package/dist/crdt/edytor-doc.js +123 -108
  43. package/dist/crdt/engine-api.d.ts +8 -2
  44. package/dist/crdt/engine.d.ts +2 -1
  45. package/dist/crdt/engine.js +3 -1
  46. package/dist/crdt/flow.d.ts +5 -47
  47. package/dist/crdt/flow.js +3 -269
  48. package/dist/crdt/index.d.ts +102 -108
  49. package/dist/crdt/index.js +3 -0
  50. package/dist/crdt/operations.d.ts +13 -4
  51. package/dist/crdt/parts.d.ts +43 -0
  52. package/dist/crdt/parts.js +44 -0
  53. package/dist/crdt/placement/display.d.ts +1 -1
  54. package/dist/crdt/placement/display.js +3 -3
  55. package/dist/crdt/placement/keys.d.ts +5 -0
  56. package/dist/crdt/placement/keys.js +5 -0
  57. package/dist/crdt/placement/resolve.d.ts +1 -1
  58. package/dist/crdt/placement/resolve.js +2 -1
  59. package/dist/crdt/protocol.d.ts +1 -1
  60. package/dist/crdt/protocol.js +1 -1
  61. package/dist/crdt/protocols/auth.d.ts +11 -1
  62. package/dist/crdt/protocols/auth.js +17 -1
  63. package/dist/crdt/protocols/awareness.js +46 -81
  64. package/dist/crdt/protocols/comments.d.ts +16 -1
  65. package/dist/crdt/protocols/comments.js +9 -1
  66. package/dist/crdt/providers/index.d.ts +12 -22
  67. package/dist/crdt/providers/index.js +3 -1
  68. package/dist/crdt/providers/indexeddb.d.ts +6 -10
  69. package/dist/crdt/providers/indexeddb.js +10 -16
  70. package/dist/crdt/providers/room.d.ts +11 -0
  71. package/dist/crdt/providers/room.js +26 -12
  72. package/dist/crdt/providers/websocket.d.ts +8 -12
  73. package/dist/crdt/providers/websocket.js +17 -19
  74. package/dist/crdt/purge.d.ts +7 -1
  75. package/dist/crdt/purge.js +32 -2
  76. package/dist/crdt/rangeDelete.d.ts +2 -47
  77. package/dist/crdt/rangeDelete.js +1 -169
  78. package/dist/crdt/restore.d.ts +2 -1
  79. package/dist/crdt/restore.js +9 -0
  80. package/dist/crdt/schema.d.ts +9 -1
  81. package/dist/crdt/schema.js +13 -3
  82. package/dist/crdt/structs.d.ts +2 -0
  83. package/dist/crdt/structs.js +2 -0
  84. package/dist/crdt/text/index/build.d.ts +125 -0
  85. package/dist/crdt/text/index/build.js +2895 -0
  86. package/dist/crdt/text/index/shared.d.ts +2 -6
  87. package/dist/crdt/text/index/shared.js +19 -19
  88. package/dist/crdt/text/model.d.ts +10 -0
  89. package/dist/crdt/text/model.js +10 -4
  90. package/dist/crdt/text/runs.d.ts +43 -42
  91. package/dist/crdt/text/runs.js +2 -165
  92. package/dist/crdt/vendor/yjs/UPSTREAM.md +83 -0
  93. package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
  94. package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +6 -3
  95. package/dist/crdt/vendor/yjs/dts/ynode.d.ts +13 -2
  96. package/dist/crdt/vendor/yjs/src/index.js +1 -1
  97. package/dist/crdt/vendor/yjs/src/structs/Item.js +44 -5
  98. package/dist/crdt/vendor/yjs/src/ynode.js +29 -7
  99. package/dist/edytor.svelte.d.ts +1 -1
  100. package/dist/edytor.svelte.js +100 -75
  101. package/dist/events/beforeInputCommands.d.ts +2 -2
  102. package/dist/events/domTextOffset.d.ts +2 -0
  103. package/dist/events/domTextOffset.js +8 -0
  104. package/dist/events/nativeInteractiveControl.d.ts +11 -1
  105. package/dist/events/nativeInteractiveControl.js +41 -1
  106. package/dist/events/onBeforeInput.js +3 -3
  107. package/dist/events/onFocus.js +9 -11
  108. package/dist/export/forms.d.ts +75 -75
  109. package/dist/index.d.ts +4 -2
  110. package/dist/index.js +1 -1
  111. package/dist/labels.d.ts +8 -0
  112. package/dist/labels.js +6 -1
  113. package/dist/plugins/comments/CommentsController.svelte.d.ts +5 -1
  114. package/dist/plugins/comments/CommentsController.svelte.js +14 -4
  115. package/dist/plugins/find/search.js +2 -2
  116. package/dist/plugins/index.d.ts +1 -0
  117. package/dist/plugins/index.js +1 -0
  118. package/dist/plugins/richtext/RichTextPlugin.svelte +63 -61
  119. package/dist/plugins/richtext/richTextOperations.js +26 -32
  120. package/dist/plugins/suggestions/SuggestionBars.svelte +40 -23
  121. package/dist/plugins/suggestions/SuggestionBars.svelte.d.ts +3 -1
  122. package/dist/plugins/suggestions/SuggestionRuns.svelte +77 -0
  123. package/dist/plugins/suggestions/SuggestionRuns.svelte.d.ts +9 -0
  124. package/dist/plugins/suggestions/shared.svelte.d.ts +58 -0
  125. package/dist/plugins/suggestions/shared.svelte.js +421 -0
  126. package/dist/plugins/suggestions/suggestionsPlugin.d.ts +15 -4
  127. package/dist/selection/domSelection.d.ts +2 -0
  128. package/dist/selection/domSelection.js +6 -2
  129. package/dist/selection/pointer.svelte.js +75 -48
  130. package/dist/selection/replaceSelection.js +2 -6
  131. package/dist/selection/selection.svelte.d.ts +3 -3
  132. package/dist/selection/selection.svelte.js +39 -32
  133. package/dist/selection/selection.utils.d.ts +14 -0
  134. package/dist/selection/selection.utils.js +33 -11
  135. package/dist/session/commands.js +21 -11
  136. package/dist/session/composition.svelte.d.ts +5 -3
  137. package/dist/session/composition.svelte.js +21 -17
  138. package/dist/session/handles.d.ts +4 -17
  139. package/dist/session/handles.js +14 -44
  140. package/dist/session/navigation.js +6 -9
  141. package/dist/session/selection.d.ts +11 -5
  142. package/dist/session/selection.js +14 -48
  143. package/dist/session/suggestions.svelte.d.ts +38 -9
  144. package/dist/session/suggestions.svelte.js +37 -18
  145. package/dist/session/virtual.d.ts +1 -1
  146. package/dist/surface/attributes.d.ts +5 -0
  147. package/dist/surface/attributes.js +2 -2
  148. package/dist/surface/cells.d.ts +2 -26
  149. package/dist/surface/cells.js +2 -33
  150. package/dist/surface/observer.svelte.d.ts +6 -1
  151. package/dist/surface/observer.svelte.js +143 -111
  152. package/dist/surface/pin.svelte.d.ts +9 -9
  153. package/dist/surface/pin.svelte.js +15 -22
  154. package/dist/surface/projector.svelte.js +10 -16
  155. package/dist/text/text.svelte.js +14 -19
  156. package/dist/themes/notion.css +29 -0
  157. package/dist/utils.d.ts +5 -0
  158. package/dist/utils.js +7 -0
  159. package/package.json +3 -1
  160. package/dist/crdt/doc/capability.d.ts +0 -56
  161. package/dist/crdt/doc/capability.js +0 -211
  162. package/dist/crdt/doc/content.d.ts +0 -16
  163. package/dist/crdt/doc/content.js +0 -77
  164. package/dist/crdt/doc/delete.d.ts +0 -15
  165. package/dist/crdt/doc/delete.js +0 -84
  166. package/dist/crdt/doc/layout.d.ts +0 -9
  167. package/dist/crdt/doc/layout.js +0 -178
  168. package/dist/crdt/doc/meta.d.ts +0 -19
  169. package/dist/crdt/doc/meta.js +0 -199
  170. package/dist/crdt/doc/moves.d.ts +0 -21
  171. package/dist/crdt/doc/moves.js +0 -274
  172. package/dist/crdt/doc/split-merge.d.ts +0 -10
  173. package/dist/crdt/doc/split-merge.js +0 -179
  174. package/dist/crdt/doc/steps.d.ts +0 -65
  175. package/dist/crdt/doc/steps.js +0 -311
  176. package/dist/crdt/text/index/cache.d.ts +0 -16
  177. package/dist/crdt/text/index/cache.js +0 -175
  178. package/dist/crdt/text/index/claims.d.ts +0 -18
  179. package/dist/crdt/text/index/claims.js +0 -225
  180. package/dist/crdt/text/index/fold.d.ts +0 -21
  181. package/dist/crdt/text/index/fold.js +0 -656
  182. package/dist/crdt/text/index/layout.d.ts +0 -24
  183. package/dist/crdt/text/index/layout.js +0 -271
  184. package/dist/crdt/text/index/placement.d.ts +0 -47
  185. package/dist/crdt/text/index/placement.js +0 -741
  186. package/dist/crdt/text/index/records.d.ts +0 -13
  187. package/dist/crdt/text/index/records.js +0 -139
  188. package/dist/crdt/text/index/report.d.ts +0 -18
  189. package/dist/crdt/text/index/report.js +0 -337
  190. package/dist/crdt/text/index/state.d.ts +0 -197
  191. package/dist/crdt/text/index/state.js +0 -116
  192. package/dist/crdt/text/index/streams.d.ts +0 -25
  193. package/dist/crdt/text/index/streams.js +0 -248
@@ -1,5 +1,6 @@
1
1
  import type { Edytor } from '../edytor.svelte.js';
2
2
  import type { Text } from '../text/text.svelte.js';
3
+ import type { InlineBlock } from '../block/inlineBlock.svelte.js';
3
4
  import { type Kind, type Owned } from './attributes.js';
4
5
  import { type Patched, type Replaced } from './cells.js';
5
6
  /**
@@ -29,7 +30,7 @@ export declare class SurfaceObserver {
29
30
  * release. A text element takes the table's attributes now (its id, its
30
31
  * editing host inside a void).
31
32
  */
32
- register: (node: Element, kind: Kind, block: string, name?: string, owned?: () => Owned) => () => void;
33
+ register: (node: Element, kind: Kind, block: string, name?: string, owned?: () => Owned, handle?: Text | InlineBlock) => () => void;
33
34
  /** The render anchor (`Edytor.svelte`): a registered child of the strict root. */
34
35
  anchor: (node: HTMLElement) => {
35
36
  destroy: () => void;
@@ -53,6 +54,10 @@ export declare class SurfaceObserver {
53
54
  flush: () => Promise<void>;
54
55
  pre: () => void;
55
56
  post: () => void;
57
+ /** The handle a registered element of `kind` renders (`null`: another element). */
58
+ handleOf: <K extends "text" | "atom">(element: Node | null | undefined, kind: K) => (K extends "text" ? Text : InlineBlock) | null;
59
+ /** The block `node` is in (its closest registered element's), none in a preview or outside the host. */
60
+ blockOf: (node: Node | null) => import("../index.js").Block | null;
56
61
  }
57
62
  /**
58
63
  * Adopt what the browser made of `text` — the only adopter: one
@@ -18,8 +18,11 @@
18
18
  * - The pre pass (root `$effect.pre`, before the flush's writes) inverts a
19
19
  * record that removed registered elements (its added nodes go, its removed
20
20
  * nodes return at their recorded siblings, before Svelte reconciles around
21
- * them) and snapshots a content the flush re-renders whose DOM holds an edit
22
- * the flush would overwrite.
21
+ * them), and one that removed only the render's anchors from a block list
22
+ * when the editor did not write it (`dom.anchors.foreign`: the records taken
23
+ * from the pre pass to the end of a compare pass are the editor's own), and
24
+ * snapshots a content the flush re-renders whose DOM holds an edit the
25
+ * flush would overwrite.
23
26
  * - The compare pass (root `$effect`, after every DOM write of the flush)
24
27
  * removes identity clones, nodes inside a content's run and the root's
25
28
  * foreign children, heals owned attributes (bounded), and compares each
@@ -42,7 +45,7 @@ import { runOccurrence } from '../events/onBeforeInput.js';
42
45
  import { getDomSelectionSnapshot } from '../selection/domSelection.js';
43
46
  import { domOffsetIn } from '../selection/selection.utils.js';
44
47
  import { diverges, heal, ownedOf } from './attributes.js';
45
- import { getTextContentOffsetAtPoint } from '../events/domTextOffset.js';
48
+ import { getTextContentOffsetAtPoint, textLeaves } from '../events/domTextOffset.js';
46
49
  import { rendersMark } from './cells.js';
47
50
  const ZWSP = /\u200B/g;
48
51
  const ATOM = '\uFFFC';
@@ -70,13 +73,7 @@ const textOf = (cell) => cell.runs
70
73
  /** Svelte's anchors: comments and empty text nodes are never input and never removed. */
71
74
  const isAnchor = (node) => node.nodeType === Node.COMMENT_NODE || (node.nodeType === Node.TEXT_NODE && !node.nodeValue);
72
75
  /** The `nth` non-empty text node under `node`. */
73
- const textLeaf = (node, nth = 0) => {
74
- const walker = (node.ownerDocument ?? document).createTreeWalker(node, NodeFilter.SHOW_TEXT);
75
- while (walker.nextNode())
76
- if (walker.currentNode.data && nth-- === 0)
77
- return walker.currentNode;
78
- return null;
79
- };
76
+ const textLeaf = (node, nth = 0) => textLeaves(node).filter((leaf) => leaf.data)[nth] ?? null;
80
77
  const ZERO_WIDTH_SPACE = '\u200B';
81
78
  /**
82
79
  * What the browser shows for `text`: its element's text nodes in order (WebKit's
@@ -86,15 +83,10 @@ const ZERO_WIDTH_SPACE = '\u200B';
86
83
  export const readDomText = (text) => {
87
84
  let value = '';
88
85
  const node = text.node;
89
- if (node) {
90
- const walker = (node.ownerDocument ?? document).createTreeWalker(node, NodeFilter.SHOW_TEXT);
91
- while (walker.nextNode()) {
92
- const leaf = walker.currentNode;
93
- value += leaf.parentElement?.closest('span.Apple-converted-space')
94
- ? leaf.data.replaceAll('\u00A0', ' ')
95
- : leaf.data;
96
- }
97
- }
86
+ for (const leaf of node ? textLeaves(node) : [])
87
+ value += leaf.parentElement?.closest('span.Apple-converted-space')
88
+ ? leaf.data.replaceAll('\u00A0', ' ')
89
+ : leaf.data;
98
90
  // An empty text's filler shares its node with what the browser typed, on either side.
99
91
  if (text.isEmpty)
100
92
  return value.replaceAll(ZERO_WIDTH_SPACE, '');
@@ -189,6 +181,10 @@ export class SurfaceObserver {
189
181
  #wrote = false;
190
182
  /** Records arrived since the last pass (the projector reads what they did: `rendered`). */
191
183
  #fresh = false;
184
+ /** The flush writes the host: from the pre pass to the end of its compare pass. */
185
+ #rendering = false;
186
+ /** Passes under way (a pass's own repairs; an adoption's synchronous flush nests one). */
187
+ #passes = 0;
192
188
  #window = null;
193
189
  constructor(edytor) {
194
190
  this.edytor = edytor;
@@ -196,11 +192,13 @@ export class SurfaceObserver {
196
192
  /** Observe `root` (the host): records name what to compare. */
197
193
  attach = (root) => {
198
194
  const mo = (this.#mo = new MutationObserver((records) => {
199
- records.forEach(this.#intake);
195
+ for (const record of records)
196
+ this.#intake(record, this.#own());
200
197
  this.signal();
201
198
  }));
202
199
  for (const child of root.childNodes)
203
200
  this.#rooted.add(child);
201
+ this.#rendering = false;
204
202
  mo.observe(root, {
205
203
  attributes: true,
206
204
  attributeOldValue: true,
@@ -220,8 +218,8 @@ export class SurfaceObserver {
220
218
  * release. A text element takes the table's attributes now (its id, its
221
219
  * editing host inside a void).
222
220
  */
223
- register = (node, kind, block, name, owned) => {
224
- const entry = { kind, block, name, owned };
221
+ register = (node, kind, block, name, owned, handle) => {
222
+ const entry = { kind, block, name, owned, handle };
225
223
  this.#registry.set(node, entry);
226
224
  if (kind === 'text')
227
225
  heal(node, ownedOf(this.edytor, node, entry));
@@ -289,50 +287,71 @@ export class SurfaceObserver {
289
287
  if (!this.edytor.readonly)
290
288
  this.#restore();
291
289
  this.#snapshot();
290
+ // From here to the end of the compare pass, what the host records is the flush's.
291
+ this.#rendering = true;
292
292
  });
293
293
  };
294
294
  post = () => {
295
295
  this.#deps();
296
- untrack(() => void this.#pass(true));
296
+ untrack(() => {
297
+ void this.#pass(true);
298
+ this.#rendering = false;
299
+ });
297
300
  };
298
301
  #deps = () => {
299
302
  const { edytor } = this;
300
303
  void [this.epoch, this.records, edytor.readonly, edytor.composition.phase];
301
304
  void [edytor.selection.value, edytor.suggestions.revision];
302
305
  };
306
+ /**
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.
311
+ */
312
+ #own = () => this.#rendering || this.#passes > 0;
303
313
  #take = () => {
304
314
  const records = this.#mo?.takeRecords() ?? [];
305
- records.forEach(this.#intake);
315
+ const own = this.#own();
316
+ for (const record of records)
317
+ this.#intake(record, own);
306
318
  return records.length > 0;
307
319
  };
308
- /** The block an element belongs to: its closest registered element's (`null`: the root). */
309
- #owner = (node) => {
320
+ /** The closest registered element holding `node` in the host: its entry (`null`: the host). */
321
+ #near = (node) => {
310
322
  for (let at = node; at; at = at.parentNode) {
311
323
  if (at === this.edytor.node)
312
324
  return null;
313
- const entry = at instanceof Element ? this.#registry.get(at) : undefined;
314
- // What happens in a preview is the view's own render: nothing to compare.
325
+ const entry = this.#registry.get(at);
315
326
  if (entry)
316
- return entry.kind === 'preview' ? undefined : entry.kind === 'anchor' ? null : entry.block;
327
+ return entry;
317
328
  }
318
- return undefined;
319
329
  };
320
- /** The text whose element holds `node`. */
321
- #textOf = (node) => {
322
- for (let at = node; at && at !== this.edytor.node; at = at.parentNode) {
323
- const entry = at instanceof Element ? this.#registry.get(at) : undefined;
324
- if (entry)
325
- return entry.kind === 'text' ? this.edytor.nodeToText.get(at) : undefined;
326
- }
327
- return undefined;
330
+ /** The block an element belongs to: its closest registered element's (`null`: the root). */
331
+ #owner = (node) => {
332
+ const entry = this.#near(node);
333
+ // What happens in a preview is the view's own render: nothing to compare.
334
+ return (entry && (entry.kind === 'preview' ? undefined : entry.kind === 'anchor' ? null : entry.block));
335
+ };
336
+ /** The handle a registered element of `kind` renders (`null`: another element). */
337
+ handleOf = (element, kind) => {
338
+ const entry = element && this.#registry.get(element);
339
+ return (entry?.kind === kind ? entry.handle : null);
340
+ };
341
+ /** The block `node` is in (its closest registered element's), none in a preview or outside the host. */
342
+ 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;
328
347
  };
329
348
  #registered = (node) => node instanceof Element && this.#registry.has(node);
330
349
  /** `node` is, or holds, an element the core still renders. */
331
350
  #holds = (node) => this.#registered(node) ||
332
351
  (node instanceof Element &&
333
352
  [...node.querySelectorAll('*')].some((child) => this.#registry.has(child)));
334
- /** A record names elements to compare (never a verdict). */
335
- #intake = (record) => {
353
+ /** A record names elements to compare (never a verdict); `own`: the editor wrote it. */
354
+ #intake = (record, own) => {
336
355
  this.#fresh = true;
337
356
  const id = this.#owner(record.target);
338
357
  if (id === undefined)
@@ -355,7 +374,7 @@ export class SurfaceObserver {
355
374
  return;
356
375
  const added = [...record.addedNodes];
357
376
  const { target: parent, nextSibling: next, previousSibling: prev } = record;
358
- this.#removed.push({ parent, nodes: removed, added, next, prev });
377
+ this.#removed.push({ parent, nodes: removed, added, next, prev, foreign: !own });
359
378
  // Wrapper replacement: the browser replaced a text element with its own nodes.
360
379
  const [text] = removed;
361
380
  if (removed.length === 1 &&
@@ -394,21 +413,33 @@ export class SurfaceObserver {
394
413
  * browser removed them one record each (Firefox): an anchor-only record is
395
414
  * inverted in a parent that gets a registered element back, else the
396
415
  * element's recorded siblings are gone and it lands outside Svelte's block.
416
+ * A foreign range across blocks (`dom.anchors.foreign`: a script's
417
+ * `Range.deleteContents()`) takes the render's anchors between the blocks
418
+ * from their list (the root, a block's children) and those after its start
419
+ * and before its end inside the blocks it enters, and leaves the elements:
420
+ * Svelte's next removal of either block would start at a detached anchor
421
+ * and leave its element, a child added to one would find no anchor to
422
+ * render before. When a foreign record took anchors from a block list,
423
+ * every foreign anchor-only record of the batch is inverted. The render's
424
+ * own removals (an item leaving the list) are never undone.
397
425
  */
398
426
  #restore = () => {
399
427
  const root = this.edytor.node;
400
428
  const removals = this.#removed.splice(0).reverse();
401
429
  const held = ({ parent, nodes }) => nodes.some((node) => node.parentNode !== parent && this.#holds(node));
402
430
  const parents = new Set(removals.filter(held).map(({ parent }) => parent));
431
+ const crossed = removals.some((removal) => this.#lost(removal) && this.#list(removal.parent));
403
432
  for (const removal of removals) {
404
433
  const { parent, nodes, added, next, prev } = removal;
405
434
  if (!root?.contains(parent))
406
435
  continue;
407
- if (!held(removal) && !(parents.has(parent) && nodes.every(isAnchor)))
436
+ const anchors = parents.has(parent) && nodes.every(isAnchor);
437
+ if (!held(removal) && !anchors && !(crossed && this.#lost(removal)))
408
438
  continue;
409
439
  // A browser-owned attempt's host: its change is adopted whatever the
410
440
  // browser did to its structure, which the re-render then restores.
411
- const inside = this.#textOf(parent);
441
+ const near = this.#near(parent);
442
+ const inside = near?.kind === 'text' && near.handle;
412
443
  if (inside && this.edytor.attempts.on(inside))
413
444
  continue;
414
445
  for (const node of added)
@@ -428,6 +459,17 @@ export class SurfaceObserver {
428
459
  }
429
460
  this.#replaced.clear();
430
461
  };
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');
431
473
  /**
432
474
  * Where an edit made at `at` of the base lands now: through the anchor
433
475
  * minted at it (it follows its text through a re-key, split, move or
@@ -479,6 +521,19 @@ export class SurfaceObserver {
479
521
  * `$effect`); otherwise contents waiting for a flush are left to it.
480
522
  */
481
523
  #pass = (flushed) => {
524
+ this.#passes++;
525
+ try {
526
+ return this.#compare(flushed);
527
+ }
528
+ finally {
529
+ // What the flush and this pass wrote: the render's records, compared
530
+ // at the next pass (as the observer's callback would have them).
531
+ if (this.#take())
532
+ queueMicrotask(this.signal);
533
+ this.#passes--;
534
+ }
535
+ };
536
+ #compare = (flushed) => {
482
537
  const { edytor } = this;
483
538
  const cells = edytor.cells;
484
539
  if (!cells || !edytor.node?.isConnected || edytor.destroyed)
@@ -620,31 +675,11 @@ export class SurfaceObserver {
620
675
  #invert = (id) => {
621
676
  if (id === this.#live())
622
677
  return;
623
- if (this.#shape(id) || !this.#rewrite(id))
678
+ if (this.#shape(id, true))
624
679
  this.edytor.cells?.remount(id);
625
680
  else
626
681
  this.update();
627
682
  };
628
- /** Write each render delta's text back into its node; false when a node is not there. */
629
- #rewrite = (id) => {
630
- const writes = [];
631
- for (const text of this.#texts(id)) {
632
- const deltas = this.edytor.deltasOf(text);
633
- const nodes = this.#nodes(text.node);
634
- const shown = deltas.length ? deltas : [{ text: '\u200B' }];
635
- for (const [i, delta] of shown.entries()) {
636
- const node = nodes[i];
637
- const leaf = node?.nodeType === Node.TEXT_NODE ? node : node && textLeaf(node);
638
- if (!leaf)
639
- return false;
640
- if (leaf.data !== delta.text)
641
- writes.push([leaf, delta.text]);
642
- }
643
- }
644
- for (const [leaf, value] of writes)
645
- leaf.data = value;
646
- return true;
647
- };
648
683
  /** A text element's children that stand for render deltas (Svelte's anchors and the newline marker aside). */
649
684
  #nodes = (element) => [...element.childNodes].filter((node) => !isAnchor(node) &&
650
685
  !(node instanceof Element && node.hasAttribute('data-edytor-trailing-newline')));
@@ -655,7 +690,7 @@ export class SurfaceObserver {
655
690
  */
656
691
  #adoptContent = (id) => {
657
692
  const { edytor } = this;
658
- let broken = Boolean(this.#shape(id));
693
+ let broken = this.#shape(id);
659
694
  const caret = getCollapsedDomTextSelection(edytor);
660
695
  const work = [];
661
696
  for (const text of this.#texts(id)) {
@@ -688,7 +723,7 @@ export class SurfaceObserver {
688
723
  const value = now.slice(0, local) + insert + now.slice(local + remove);
689
724
  return adopt(this.edytor, text, value, undefined, local + insert.length);
690
725
  };
691
- #texts = (id) => [...(this.#contents.get(id) ?? [])].flatMap((element) => this.edytor.nodeToText.get(element) ?? []);
726
+ #texts = (id) => [...(this.#contents.get(id) ?? [])].flatMap((element) => this.handleOf(element, 'text') ?? []);
692
727
  /** A content's DOM: its registered text and atom elements in order; null when not shown. */
693
728
  #read = (id) => {
694
729
  const elements = [...(this.#contents.get(id) ?? [])];
@@ -697,20 +732,17 @@ export class SurfaceObserver {
697
732
  elements.sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
698
733
  const host = this.#blocks.get(id);
699
734
  let text = '';
700
- let broken = host && !host.isConnected ? 'missing' : null;
735
+ let broken = !!host && !host.isConnected;
701
736
  for (const element of elements) {
702
737
  const kind = this.#registry.get(element)?.kind;
703
738
  // A mark is inside its text element: its presence is the shape check's.
704
739
  if (kind === 'mark')
705
740
  continue;
706
- if (!element.isConnected)
707
- broken ??= 'missing';
708
- else if (host && !host.contains(element))
709
- broken ??= 'escaped';
741
+ broken ||= !element.isConnected || (!!host && !host.contains(element));
710
742
  if (kind === 'atom')
711
743
  text += ATOM;
712
744
  else if (kind === 'text') {
713
- const found = this.edytor.nodeToText.get(element);
745
+ const found = this.handleOf(element, 'text');
714
746
  text += (found ? readDomText(found) : (element.textContent ?? '')).replace(ZWSP, '');
715
747
  }
716
748
  }
@@ -718,46 +750,50 @@ export class SurfaceObserver {
718
750
  };
719
751
  /**
720
752
  * Equal text, compared structurally: each text element (a strict
721
- * container) holds one node per render delta — a text node, or a
722
- * registered mark element whose registered marks follow the delta's chain.
753
+ * container) holds one node per render delta (the empty filler's one) — a
754
+ * 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.
723
758
  */
724
- #shape = (id) => {
759
+ #shape = (id, write) => {
760
+ const writes = [];
725
761
  for (const text of this.#texts(id)) {
726
762
  const element = text.node;
727
763
  if (!element)
728
764
  continue;
729
765
  const deltas = this.edytor.deltasOf(text);
766
+ const shown = deltas.length ? deltas : [{ text: '\u200B', marks: [] }];
730
767
  const nodes = this.#nodes(element);
731
- if (!deltas.length) {
732
- if (nodes.length !== 1 || nodes[0].nodeType !== Node.TEXT_NODE)
733
- return 'filler';
734
- continue;
735
- }
736
- if (nodes.length !== deltas.length)
737
- return 'partition';
738
- for (const [i, delta] of deltas.entries()) {
768
+ if (nodes.length !== shown.length)
769
+ return true;
770
+ for (const [i, delta] of shown.entries()) {
739
771
  const node = nodes[i];
772
+ let leaf = node;
740
773
  const chain = delta.marks
741
774
  .map(([name]) => name)
742
775
  .filter((name) => rendersMark(this.edytor.marks, name));
743
- if (!chain.length) {
744
- if (node.nodeType !== Node.TEXT_NODE)
745
- return 'foreign-child';
746
- continue;
776
+ if (chain.length) {
777
+ if (!this.#registry.has(node))
778
+ return true;
779
+ const marks = [node, ...node.querySelectorAll('[data-edytor-mark]')]
780
+ .map((el) => this.#registry.get(el)?.name)
781
+ .filter(Boolean);
782
+ leaf = textLeaf(node);
783
+ // The snippet's markup is its own, but the delta renders one text node.
784
+ if (marks.join() !== chain.join() || !leaf || textLeaf(node, 1))
785
+ return true;
747
786
  }
748
- if (!(node instanceof Element) || !this.#registry.has(node))
749
- return 'foreign-child';
750
- const marks = [node, ...node.querySelectorAll('[data-edytor-mark]')]
751
- .map((el) => this.#registry.get(el)?.name)
752
- .filter(Boolean);
753
- if (marks.join() !== chain.join())
754
- return 'mark-chain';
755
- // The snippet's markup is its own, but the delta renders one text node.
756
- if (!textLeaf(node) || textLeaf(node, 1))
757
- return 'mark-text';
787
+ else if (node.nodeType !== Node.TEXT_NODE)
788
+ return true;
789
+ if (leaf.data !== delta.text)
790
+ writes.push([leaf, delta.text]);
758
791
  }
759
792
  }
760
- return null;
793
+ if (write)
794
+ for (const [leaf, value] of writes)
795
+ leaf.data = value;
796
+ return false;
761
797
  };
762
798
  /** The root's children differ from its registered block elements in cell order, the anchor, Svelte's anchors. */
763
799
  #rootDiverges = () => {
@@ -921,21 +957,17 @@ const shownMarks = (text, at) => {
921
957
  const element = text.node;
922
958
  if (!element)
923
959
  return null;
924
- const walker = element.ownerDocument.createTreeWalker(element, NodeFilter.SHOW_TEXT);
925
960
  let offset = 0;
926
- for (let node = walker.nextNode(); node; node = walker.nextNode()) {
927
- offset += node.data.replace(ZWSP, '').length;
928
- if (offset <= at)
929
- continue;
930
- const names = [];
931
- for (let el = node.parentElement; el && el !== element; el = el.parentElement) {
932
- const name = el.getAttribute('data-edytor-mark');
933
- if (name)
934
- names.push(name);
935
- }
936
- return names.sort().join();
961
+ const node = textLeaves(element).find((leaf) => (offset += leaf.data.replace(ZWSP, '').length) > at);
962
+ if (!node)
963
+ return null;
964
+ const names = [];
965
+ for (let el = node.parentElement; el && el !== element; el = el.parentElement) {
966
+ const name = el.getAttribute('data-edytor-mark');
967
+ if (name)
968
+ names.push(name);
937
969
  }
938
- return null;
970
+ return names.sort().join();
939
971
  };
940
972
  /** The rendered marks the model gives the character at `at` of `text`. */
941
973
  const modelMarks = (edytor, text, at) => Object.keys(activeMarks(text.getMarksAtRange(at, at + 1)[0]?.marks))
@@ -23,15 +23,15 @@ export declare class Pin {
23
23
  /** The session's preview (the browser draws it itself when `native`). */
24
24
  show: (text: string, marks: Record<string, unknown> | undefined, native: boolean) => void;
25
25
  /**
26
- * The first write replaced a range across texts: the host's
27
- * cell lost the segments and atoms it covered, and the rest of its end text
28
- * now follows the preview in the host, as its own nodes, so the IME's node
29
- * is never rewritten. Across blocks they follow the frozen render inside the
30
- * host element; inside one block (`beside`: across an inline atom) they go
31
- * in their own element right after it (`rest`), as there the IME's write
32
- * moves the host element's render anchors into the next segment's (Chromium).
26
+ * The first write replaced a range across an inline atom inside one block:
27
+ * the host's cell lost the segments and atoms it covered, and the rest of
28
+ * its end text now follows the preview in its own element right after the
29
+ * host (`rest`), so the IME's node is never rewritten (the IME's write
30
+ * moves the host element's render anchors into the next segment's,
31
+ * Chromium). A range across blocks never merges: the session replaces it
32
+ * at its start (`Composition.#rangeless`).
33
33
  */
34
- merge: (cell: Cell, items: Item[], beside: boolean) => void;
34
+ merge: (cell: Cell, items: Item[]) => void;
35
35
  /** The session ended: the host renders from its cell again (the catch-up patch). */
36
36
  release: () => null;
37
37
  /** `node` lies inside the pinned element. */
@@ -48,7 +48,7 @@ export declare class Pin {
48
48
  deltas: RenderDelta[];
49
49
  empty: boolean;
50
50
  };
51
- /** What follows the pinned segment `key` of `block` in its own element (`merge`, `beside`). */
51
+ /** What follows the pinned segment `key` of `block` in its own element (`merge`). */
52
52
  rest: (block: BlockId, key: string) => RenderDelta[];
53
53
  /**
54
54
  * What the IME shows in the pinned element: its text between the frozen
@@ -29,7 +29,6 @@ export class Pin {
29
29
  head: cut(frozen, from)[0],
30
30
  tail: cut(frozen, Math.max(from, to))[1],
31
31
  merged: [],
32
- beside: false,
33
32
  element,
34
33
  // Until the first preview the render stays as the IME found it: a
35
34
  // write now would collapse its DOM range before it replaces it.
@@ -44,18 +43,18 @@ export class Pin {
44
43
  this.#held = { ...held, preview: { text, marks: set && Object.fromEntries(set), native } };
45
44
  };
46
45
  /**
47
- * The first write replaced a range across texts: the host's
48
- * cell lost the segments and atoms it covered, and the rest of its end text
49
- * now follows the preview in the host, as its own nodes, so the IME's node
50
- * is never rewritten. Across blocks they follow the frozen render inside the
51
- * host element; inside one block (`beside`: across an inline atom) they go
52
- * in their own element right after it (`rest`), as there the IME's write
53
- * moves the host element's render anchors into the next segment's (Chromium).
46
+ * The first write replaced a range across an inline atom inside one block:
47
+ * the host's cell lost the segments and atoms it covered, and the rest of
48
+ * its end text now follows the preview in its own element right after the
49
+ * host (`rest`), so the IME's node is never rewritten (the IME's write
50
+ * moves the host element's render anchors into the next segment's,
51
+ * Chromium). A range across blocks never merges: the session replaces it
52
+ * at its start (`Composition.#rangeless`).
54
53
  */
55
- merge = (cell, items, beside) => {
54
+ merge = (cell, items) => {
56
55
  const held = this.#held;
57
56
  if (held)
58
- this.#held = { ...held, parts: partsOf(cell.runs), merged: items, beside };
57
+ this.#held = { ...held, parts: partsOf(cell.runs), merged: items };
59
58
  };
60
59
  /** The session ended: the host renders from its cell again (the catch-up patch). */
61
60
  release = () => (this.#held = null);
@@ -68,14 +67,10 @@ export class Pin {
68
67
  const held = this.#held;
69
68
  if (!held || held.block !== block || held.key !== key)
70
69
  return null;
71
- const { preview, head, tail, beside } = held;
72
- const merged = beside ? [] : held.merged;
70
+ const { preview, head, tail } = held;
73
71
  if (!preview || preview.native)
74
- return {
75
- deltas: [...held.frozen, ...renderDeltas(merged)],
76
- empty: held.empty && !textOf(merged)
77
- };
78
- const items = [...head, preview, ...tail, ...merged];
72
+ return { deltas: [...held.frozen], empty: held.empty };
73
+ const items = [...head, preview, ...tail];
79
74
  return { deltas: renderDeltas(items), empty: !textOf(items) };
80
75
  };
81
76
  /** The render of `segment` of `cell`: pinned while it hosts the IME, else its kind's (`segmentDeltas`). */
@@ -83,11 +78,10 @@ export class Pin {
83
78
  deltas: segmentDeltas(cell, segment, transform),
84
79
  empty: segment.text === ''
85
80
  };
86
- /** What follows the pinned segment `key` of `block` in its own element (`merge`, `beside`). */
81
+ /** What follows the pinned segment `key` of `block` in its own element (`merge`). */
87
82
  rest = (block, key) => {
88
83
  const held = this.#held;
89
- const shown = held?.beside && held.block === block && held.key === key;
90
- return shown ? renderDeltas(held.merged) : [];
84
+ return held?.block === block && held.key === key ? renderDeltas(held.merged) : [];
91
85
  };
92
86
  /**
93
87
  * What the IME shows in the pinned element: its text between the frozen
@@ -102,8 +96,7 @@ export class Pin {
102
96
  const dom = (held.element.textContent ?? '').replace(/\u200B/g, '');
103
97
  if (!held.preview && dom === textOf(held.frozen))
104
98
  return null;
105
- const inside = held.beside ? held.tail : [...held.tail, ...held.merged];
106
- const [head, tail] = [textOf(held.head), textOf(inside)];
99
+ const [head, tail] = [textOf(held.head), textOf(held.tail)];
107
100
  if (dom.length < head.length + tail.length)
108
101
  return null;
109
102
  return dom.startsWith(head) && dom.endsWith(tail)