edytor 3.0.0 → 5.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 (231) 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 +23 -12
  6. package/dist/cloudflare/DocumentRoom.js +10 -6
  7. package/dist/cloudflare/index.d.ts +1 -1
  8. package/dist/cloudflare/room/access.d.ts +8 -3
  9. package/dist/cloudflare/room/access.js +44 -20
  10. package/dist/cloudflare/room/admission.js +28 -5
  11. package/dist/cloudflare/room/comments.js +28 -1
  12. package/dist/cloudflare/room/context.d.ts +139 -129
  13. package/dist/cloudflare/room/context.js +11 -4
  14. package/dist/cloudflare/room/forged.d.ts +14 -0
  15. package/dist/cloudflare/room/forged.js +30 -2
  16. package/dist/cloudflare/room/frames.d.ts +4 -0
  17. package/dist/cloudflare/room/frames.js +5 -1
  18. package/dist/cloudflare/room/purge.d.ts +6 -3
  19. package/dist/cloudflare/room/purge.js +45 -19
  20. package/dist/cloudflare/room/replicas.d.ts +4 -2
  21. package/dist/cloudflare/room/replicas.js +8 -3
  22. package/dist/cloudflare/room/suggest.d.ts +7 -0
  23. package/dist/cloudflare/room/suggest.js +143 -0
  24. package/dist/cloudflare/room/updates.d.ts +3 -4
  25. package/dist/cloudflare/room/updates.js +6 -8
  26. package/dist/cloudflare/routeDocumentSocket.d.ts +10 -1
  27. package/dist/cloudflare/routeDocumentSocket.js +5 -3
  28. package/dist/components/Content.svelte +9 -1
  29. package/dist/crdt/anchors.js +3 -2
  30. package/dist/crdt/attribution/attribution.d.ts +34 -12
  31. package/dist/crdt/attribution/attribution.js +59 -21
  32. package/dist/crdt/attribution/block.d.ts +3 -3
  33. package/dist/crdt/attribution/block.js +7 -8
  34. package/dist/crdt/attribution/index.d.ts +1 -1
  35. package/dist/crdt/attribution/index.js +1 -1
  36. package/dist/crdt/data.d.ts +62 -31
  37. package/dist/crdt/data.js +211 -103
  38. package/dist/crdt/doc/capability.js +9 -13
  39. package/dist/crdt/doc/delete.d.ts +1 -1
  40. package/dist/crdt/doc/delete.js +13 -30
  41. package/dist/crdt/doc/funnel.d.ts +1 -1
  42. package/dist/crdt/doc/funnel.js +34 -26
  43. package/dist/crdt/doc/gate.d.ts +3 -3
  44. package/dist/crdt/doc/history.d.ts +31 -9
  45. package/dist/crdt/doc/history.js +204 -73
  46. package/dist/crdt/doc/layout.js +16 -27
  47. package/dist/crdt/doc/meta.js +17 -23
  48. package/dist/crdt/doc/moves.d.ts +1 -1
  49. package/dist/crdt/doc/moves.js +52 -32
  50. package/dist/crdt/doc/reads.d.ts +2 -2
  51. package/dist/crdt/doc/reads.js +20 -30
  52. package/dist/crdt/doc/seed.js +2 -2
  53. package/dist/crdt/doc/split-merge.js +12 -15
  54. package/dist/crdt/doc/steps.d.ts +25 -13
  55. package/dist/crdt/doc/steps.js +55 -125
  56. package/dist/crdt/doc/suggest.d.ts +167 -0
  57. package/dist/crdt/doc/suggest.js +579 -0
  58. package/dist/crdt/doc/types.d.ts +21 -9
  59. package/dist/crdt/document.d.ts +26 -6
  60. package/dist/crdt/document.js +40 -15
  61. package/dist/crdt/edytor-doc.d.ts +410 -390
  62. package/dist/crdt/edytor-doc.js +143 -95
  63. package/dist/crdt/engine-api.d.ts +39 -1
  64. package/dist/crdt/engine.d.ts +2 -2
  65. package/dist/crdt/engine.js +4 -5
  66. package/dist/crdt/flow.d.ts +2 -2
  67. package/dist/crdt/flow.js +29 -37
  68. package/dist/crdt/index.d.ts +134 -127
  69. package/dist/crdt/index.js +4 -1
  70. package/dist/crdt/operations.d.ts +12 -3
  71. package/dist/crdt/parts.d.ts +43 -0
  72. package/dist/crdt/parts.js +44 -0
  73. package/dist/crdt/placement/display.d.ts +6 -31
  74. package/dist/crdt/placement/display.js +41 -146
  75. package/dist/crdt/placement/keys.d.ts +63 -0
  76. package/dist/crdt/placement/keys.js +95 -0
  77. package/dist/crdt/placement/model.d.ts +119 -118
  78. package/dist/crdt/placement/model.js +242 -142
  79. package/dist/crdt/placement/resolve.d.ts +123 -35
  80. package/dist/crdt/placement/resolve.js +188 -153
  81. package/dist/crdt/placement/slots.d.ts +134 -0
  82. package/dist/crdt/placement/slots.js +152 -0
  83. package/dist/crdt/protocol.d.ts +1 -1
  84. package/dist/crdt/protocol.js +1 -1
  85. package/dist/crdt/protocols/auth.d.ts +11 -1
  86. package/dist/crdt/protocols/auth.js +17 -1
  87. package/dist/crdt/protocols/comments.d.ts +16 -1
  88. package/dist/crdt/protocols/comments.js +9 -1
  89. package/dist/crdt/providers/index.d.ts +6 -4
  90. package/dist/crdt/providers/index.js +3 -1
  91. package/dist/crdt/providers/room.d.ts +16 -2
  92. package/dist/crdt/providers/room.js +64 -17
  93. package/dist/crdt/providers/websocket.d.ts +6 -4
  94. package/dist/crdt/providers/websocket.js +14 -7
  95. package/dist/crdt/purge.d.ts +24 -9
  96. package/dist/crdt/purge.js +266 -126
  97. package/dist/crdt/restore.d.ts +4 -3
  98. package/dist/crdt/restore.js +27 -34
  99. package/dist/crdt/schema.d.ts +30 -21
  100. package/dist/crdt/schema.js +39 -23
  101. package/dist/crdt/storage.d.ts +1 -1
  102. package/dist/crdt/storage.js +1 -1
  103. package/dist/crdt/structs.d.ts +17 -15
  104. package/dist/crdt/structs.js +10 -22
  105. package/dist/crdt/text/index/cache.js +4 -0
  106. package/dist/crdt/text/index/claims.js +13 -23
  107. package/dist/crdt/text/index/fold.d.ts +2 -3
  108. package/dist/crdt/text/index/fold.js +194 -44
  109. package/dist/crdt/text/index/layout.d.ts +2 -4
  110. package/dist/crdt/text/index/layout.js +14 -30
  111. package/dist/crdt/text/index/placement.d.ts +26 -18
  112. package/dist/crdt/text/index/placement.js +420 -157
  113. package/dist/crdt/text/index/records.d.ts +3 -3
  114. package/dist/crdt/text/index/records.js +24 -31
  115. package/dist/crdt/text/index/shared.d.ts +11 -3
  116. package/dist/crdt/text/index/shared.js +29 -17
  117. package/dist/crdt/text/index/state.d.ts +21 -17
  118. package/dist/crdt/text/index/state.js +15 -17
  119. package/dist/crdt/text/index/streams.d.ts +10 -5
  120. package/dist/crdt/text/index/streams.js +78 -27
  121. package/dist/crdt/text/items.d.ts +10 -3
  122. package/dist/crdt/text/items.js +7 -0
  123. package/dist/crdt/text/model.d.ts +87 -62
  124. package/dist/crdt/text/model.js +91 -169
  125. package/dist/crdt/text/ranges.d.ts +1 -1
  126. package/dist/crdt/text/ranges.js +3 -3
  127. package/dist/crdt/text/rows.d.ts +13 -7
  128. package/dist/crdt/text/rows.js +24 -7
  129. package/dist/crdt/text/runs.d.ts +51 -42
  130. package/dist/crdt/text/runs.js +24 -7
  131. package/dist/crdt/text/streams.d.ts +26 -13
  132. package/dist/crdt/text/streams.js +41 -14
  133. package/dist/crdt/vendor/yjs/UPSTREAM.md +651 -21
  134. package/dist/crdt/vendor/yjs/dts/global.d.ts +2 -0
  135. package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
  136. package/dist/crdt/vendor/yjs/dts/structs/AbstractStruct.d.ts +7 -0
  137. package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +197 -10
  138. package/dist/crdt/vendor/yjs/dts/utils/Doc.d.ts +6 -3
  139. package/dist/crdt/vendor/yjs/dts/utils/StructStore.d.ts +5 -18
  140. package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +15 -1
  141. package/dist/crdt/vendor/yjs/dts/utils/UpdateDecoder.d.ts +14 -6
  142. package/dist/crdt/vendor/yjs/dts/utils/UpdateEncoder.d.ts +33 -6
  143. package/dist/crdt/vendor/yjs/dts/utils/ids.d.ts +1 -65
  144. package/dist/crdt/vendor/yjs/dts/utils/marks.d.ts +6 -1
  145. package/dist/crdt/vendor/yjs/dts/ynode.d.ts +54 -7
  146. package/dist/crdt/vendor/yjs/global.d.ts +2 -0
  147. package/dist/crdt/vendor/yjs/src/index.js +1 -1
  148. package/dist/crdt/vendor/yjs/src/structs/AbstractStruct.js +10 -0
  149. package/dist/crdt/vendor/yjs/src/structs/Item.js +649 -51
  150. package/dist/crdt/vendor/yjs/src/utils/BlockSet.js +18 -9
  151. package/dist/crdt/vendor/yjs/src/utils/Doc.js +5 -3
  152. package/dist/crdt/vendor/yjs/src/utils/StructStore.js +5 -0
  153. package/dist/crdt/vendor/yjs/src/utils/Transaction.js +36 -13
  154. package/dist/crdt/vendor/yjs/src/utils/UpdateDecoder.js +88 -18
  155. package/dist/crdt/vendor/yjs/src/utils/UpdateEncoder.js +119 -32
  156. package/dist/crdt/vendor/yjs/src/utils/encoding.js +13 -1
  157. package/dist/crdt/vendor/yjs/src/utils/ids.js +7 -7
  158. package/dist/crdt/vendor/yjs/src/utils/marks.js +193 -8
  159. package/dist/crdt/vendor/yjs/src/utils/redo.js +1 -1
  160. package/dist/crdt/vendor/yjs/src/utils/transaction-helpers.js +1 -1
  161. package/dist/crdt/vendor/yjs/src/utils/updates.js +3 -3
  162. package/dist/crdt/vendor/yjs/src/ynode.js +199 -12
  163. package/dist/edytor.svelte.d.ts +1 -1
  164. package/dist/edytor.svelte.js +103 -75
  165. package/dist/events/beforeInputCommands.d.ts +2 -2
  166. package/dist/events/beforeInputCommands.js +2 -2
  167. package/dist/events/nativeInteractiveControl.d.ts +11 -1
  168. package/dist/events/nativeInteractiveControl.js +41 -1
  169. package/dist/events/onBeforeInput.js +3 -3
  170. package/dist/events/onFocus.js +9 -11
  171. package/dist/events/onPageHide.d.ts +3 -0
  172. package/dist/events/onPageHide.js +16 -0
  173. package/dist/export/forms.d.ts +50 -50
  174. package/dist/index.d.ts +4 -2
  175. package/dist/index.js +1 -1
  176. package/dist/labels.d.ts +8 -0
  177. package/dist/labels.js +6 -1
  178. package/dist/plugins/comments/CommentsController.svelte.d.ts +5 -1
  179. package/dist/plugins/comments/CommentsController.svelte.js +14 -4
  180. package/dist/plugins/find/search.js +2 -2
  181. package/dist/plugins/index.d.ts +1 -0
  182. package/dist/plugins/index.js +1 -0
  183. package/dist/plugins/suggestions/SuggestionBars.svelte +40 -23
  184. package/dist/plugins/suggestions/SuggestionBars.svelte.d.ts +3 -1
  185. package/dist/plugins/suggestions/SuggestionRuns.svelte +77 -0
  186. package/dist/plugins/suggestions/SuggestionRuns.svelte.d.ts +9 -0
  187. package/dist/plugins/suggestions/shared.svelte.d.ts +58 -0
  188. package/dist/plugins/suggestions/shared.svelte.js +421 -0
  189. package/dist/plugins/suggestions/suggestionsPlugin.d.ts +15 -4
  190. package/dist/selection/domSelection.d.ts +2 -0
  191. package/dist/selection/domSelection.js +6 -2
  192. package/dist/selection/pointer.svelte.js +75 -48
  193. package/dist/selection/replaceSelection.js +2 -6
  194. package/dist/selection/selection.svelte.d.ts +3 -3
  195. package/dist/selection/selection.svelte.js +39 -32
  196. package/dist/selection/selection.utils.d.ts +14 -0
  197. package/dist/selection/selection.utils.js +28 -3
  198. package/dist/session/commands.js +21 -11
  199. package/dist/session/composition.svelte.d.ts +5 -3
  200. package/dist/session/composition.svelte.js +21 -17
  201. package/dist/session/handles.d.ts +4 -17
  202. package/dist/session/handles.js +11 -37
  203. package/dist/session/navigation.js +6 -9
  204. package/dist/session/selection.d.ts +11 -5
  205. package/dist/session/selection.js +14 -48
  206. package/dist/session/suggestions.svelte.d.ts +38 -9
  207. package/dist/session/suggestions.svelte.js +37 -18
  208. package/dist/session/virtual.d.ts +1 -1
  209. package/dist/surface/attributes.d.ts +5 -0
  210. package/dist/surface/attributes.js +2 -2
  211. package/dist/surface/cells.d.ts +2 -26
  212. package/dist/surface/cells.js +2 -33
  213. package/dist/surface/observer.svelte.d.ts +6 -1
  214. package/dist/surface/observer.svelte.js +130 -83
  215. package/dist/surface/pin.svelte.d.ts +9 -9
  216. package/dist/surface/pin.svelte.js +15 -22
  217. package/dist/surface/projector.svelte.d.ts +15 -3
  218. package/dist/surface/projector.svelte.js +67 -22
  219. package/dist/text/text.svelte.js +14 -19
  220. package/dist/themes/notion.css +29 -0
  221. package/package.json +4 -2
  222. package/dist/crdt/placement/rank.d.ts +0 -87
  223. package/dist/crdt/placement/rank.js +0 -332
  224. package/dist/crdt/placement/source.d.ts +0 -43
  225. package/dist/crdt/placement/source.js +0 -66
  226. package/dist/crdt/text/delete-index.d.ts +0 -129
  227. package/dist/crdt/text/delete-index.js +0 -365
  228. package/dist/crdt/text/deletes.d.ts +0 -76
  229. package/dist/crdt/text/deletes.js +0 -362
  230. package/dist/crdt/text/index/anchored.d.ts +0 -17
  231. package/dist/crdt/text/index/anchored.js +0 -197
@@ -39,37 +39,14 @@ export type Cells = {
39
39
  * reports (`onPatch` is told which cells each report replaced).
40
40
  */
41
41
  export declare const createCells: (source: CellSource, onPatch?: (patched: Patched, before: Replaced) => void) => Cells;
42
- export declare const START = "start";
43
- type TextRun = Extract<ContentRun, {
44
- kind: 'text';
45
- }>;
46
- /** The text between two inline atoms (or a content edge), keyed causally. */
47
- export type Segment = {
48
- readonly kind: 'text';
49
- /** The id of the atom before this segment, or {@link START}. */
50
- readonly key: string;
51
- readonly text: string;
52
- readonly runs: readonly TextRun[];
53
- };
42
+ export { START, partsOf, type Segment, type AtomPart, type Part } from '../crdt/parts.js';
43
+ import { type Segment } from '../crdt/parts.js';
54
44
  /** A suggestion's block as a cell (`components/Suggestion`): rendered like one, never registered. */
55
45
  export type PreviewCell = Omit<Cell, 'childIds'> & {
56
46
  readonly children: readonly PreviewCell[];
57
47
  };
58
48
  /** `blocks` as preview cells, keyed by position under `key` (stable while a stream grows). */
59
49
  export declare const previewCells: (blocks: readonly JSONBlock[], key: string) => PreviewCell[];
60
- export type AtomPart = {
61
- readonly kind: 'inline';
62
- readonly key: string;
63
- readonly id: string;
64
- readonly type: string;
65
- readonly data: Data;
66
- };
67
- export type Part = Segment | AtomPart;
68
- /**
69
- * A cell's content as segments and atoms, alternating, starting and ending
70
- * with a segment (an empty content is one empty `start` segment).
71
- */
72
- export declare const partsOf: (runs: readonly ContentRun[]) => readonly Part[];
73
50
  /** Whether a view renders mark `name` (its record has a tag or a snippet): an unknown mark renders its text only. */
74
51
  export declare const rendersMark: (marks: ReadonlyMap<string, {
75
52
  tag?: unknown;
@@ -105,4 +82,3 @@ export declare const segmentDeltas: (cell: Cell, segment: Segment, transform?: T
105
82
  * composition is in it.
106
83
  */
107
84
  export declare const placeholderOf: (cell: Cell, composing: boolean) => boolean;
108
- export {};
@@ -125,7 +125,8 @@ export const createCells = (source, onPatch) => {
125
125
  };
126
126
  };
127
127
  // ── segments and render deltas ──────────────────────────────────────────
128
- export const START = 'start';
128
+ export { START, partsOf } from '../crdt/parts.js';
129
+ import { partsOf } from '../crdt/parts.js';
129
130
  /** `blocks` as preview cells, keyed by position under `key` (stable while a stream grows). */
130
131
  export const previewCells = (blocks, key) => blocks.map((block, index) => {
131
132
  const id = `${key}.${index}`;
@@ -139,38 +140,6 @@ export const previewCells = (blocks, key) => blocks.map((block, index) => {
139
140
  children: Object.freeze(previewCells(block.children ?? [], id))
140
141
  });
141
142
  });
142
- const partsMemo = new WeakMap();
143
- /**
144
- * A cell's content as segments and atoms, alternating, starting and ending
145
- * with a segment (an empty content is one empty `start` segment).
146
- */
147
- export const partsOf = (runs) => {
148
- const memo = partsMemo.get(runs);
149
- if (memo)
150
- return memo;
151
- const parts = [];
152
- let key = START;
153
- let segment = [];
154
- const close = () => parts.push(Object.freeze({
155
- kind: 'text',
156
- key,
157
- text: segment.map((run) => run.text).join(''),
158
- runs: Object.freeze(segment)
159
- }));
160
- for (const run of runs) {
161
- if (run.kind === 'text') {
162
- segment.push(run);
163
- continue;
164
- }
165
- close();
166
- parts.push(Object.freeze({ kind: 'inline', key: run.id, id: run.id, type: run.type, data: run.data }));
167
- key = run.id;
168
- segment = [];
169
- }
170
- close();
171
- partsMemo.set(runs, Object.freeze(parts));
172
- return parts;
173
- };
174
143
  /** Whether a view renders mark `name` (its record has a tag or a snippet): an unknown mark renders its text only. */
175
144
  export const rendersMark = (marks, name) => {
176
145
  const mark = marks.get(name);
@@ -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
@@ -187,8 +190,12 @@ export class SurfaceObserver {
187
190
  #heals = 0;
188
191
  /** The pass wrote the host itself: a render (the projector displays after it). */
189
192
  #wrote = false;
190
- /** Records arrived since the last pass (a display waiting on them gets a pass). */
193
+ /** Records arrived since the last pass (the projector reads what they did: `rendered`). */
191
194
  #fresh = false;
195
+ /** The flush writes the host: from the pre pass to the end of its compare pass. */
196
+ #rendering = false;
197
+ /** Passes under way (a pass's own repairs; an adoption's synchronous flush nests one). */
198
+ #passes = 0;
192
199
  #window = null;
193
200
  constructor(edytor) {
194
201
  this.edytor = edytor;
@@ -196,11 +203,13 @@ export class SurfaceObserver {
196
203
  /** Observe `root` (the host): records name what to compare. */
197
204
  attach = (root) => {
198
205
  const mo = (this.#mo = new MutationObserver((records) => {
199
- records.forEach(this.#intake);
206
+ for (const record of records)
207
+ this.#intake(record, this.#own());
200
208
  this.signal();
201
209
  }));
202
210
  for (const child of root.childNodes)
203
211
  this.#rooted.add(child);
212
+ this.#rendering = false;
204
213
  mo.observe(root, {
205
214
  attributes: true,
206
215
  attributeOldValue: true,
@@ -220,8 +229,8 @@ export class SurfaceObserver {
220
229
  * release. A text element takes the table's attributes now (its id, its
221
230
  * editing host inside a void).
222
231
  */
223
- register = (node, kind, block, name, owned) => {
224
- const entry = { kind, block, name, owned };
232
+ register = (node, kind, block, name, owned, handle) => {
233
+ const entry = { kind, block, name, owned, handle };
225
234
  this.#registry.set(node, entry);
226
235
  if (kind === 'text')
227
236
  heal(node, ownedOf(this.edytor, node, entry));
@@ -289,50 +298,71 @@ export class SurfaceObserver {
289
298
  if (!this.edytor.readonly)
290
299
  this.#restore();
291
300
  this.#snapshot();
301
+ // From here to the end of the compare pass, what the host records is the flush's.
302
+ this.#rendering = true;
292
303
  });
293
304
  };
294
305
  post = () => {
295
306
  this.#deps();
296
- untrack(() => void this.#pass(true));
307
+ untrack(() => {
308
+ void this.#pass(true);
309
+ this.#rendering = false;
310
+ });
297
311
  };
298
312
  #deps = () => {
299
313
  const { edytor } = this;
300
314
  void [this.epoch, this.records, edytor.readonly, edytor.composition.phase];
301
315
  void [edytor.selection.value, edytor.suggestions.revision];
302
316
  };
317
+ /**
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.
322
+ */
323
+ #own = () => this.#rendering || this.#passes > 0;
303
324
  #take = () => {
304
325
  const records = this.#mo?.takeRecords() ?? [];
305
- records.forEach(this.#intake);
326
+ const own = this.#own();
327
+ for (const record of records)
328
+ this.#intake(record, own);
306
329
  return records.length > 0;
307
330
  };
308
- /** The block an element belongs to: its closest registered element's (`null`: the root). */
309
- #owner = (node) => {
331
+ /** The closest registered element holding `node` in the host: its entry (`null`: the host). */
332
+ #near = (node) => {
310
333
  for (let at = node; at; at = at.parentNode) {
311
334
  if (at === this.edytor.node)
312
335
  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.
336
+ const entry = this.#registry.get(at);
315
337
  if (entry)
316
- return entry.kind === 'preview' ? undefined : entry.kind === 'anchor' ? null : entry.block;
338
+ return entry;
317
339
  }
318
- return undefined;
319
340
  };
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;
341
+ /** The block an element belongs to: its closest registered element's (`null`: the root). */
342
+ #owner = (node) => {
343
+ const entry = this.#near(node);
344
+ // What happens in a preview is the view's own render: nothing to compare.
345
+ return (entry && (entry.kind === 'preview' ? undefined : entry.kind === 'anchor' ? null : entry.block));
346
+ };
347
+ /** The handle a registered element of `kind` renders (`null`: another element). */
348
+ handleOf = (element, kind) => {
349
+ const entry = element && this.#registry.get(element);
350
+ return (entry?.kind === kind ? entry.handle : null);
351
+ };
352
+ /** The block `node` is in (its closest registered element's), none in a preview or outside the host. */
353
+ 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;
328
358
  };
329
359
  #registered = (node) => node instanceof Element && this.#registry.has(node);
330
360
  /** `node` is, or holds, an element the core still renders. */
331
361
  #holds = (node) => this.#registered(node) ||
332
362
  (node instanceof Element &&
333
363
  [...node.querySelectorAll('*')].some((child) => this.#registry.has(child)));
334
- /** A record names elements to compare (never a verdict). */
335
- #intake = (record) => {
364
+ /** A record names elements to compare (never a verdict); `own`: the editor wrote it. */
365
+ #intake = (record, own) => {
336
366
  this.#fresh = true;
337
367
  const id = this.#owner(record.target);
338
368
  if (id === undefined)
@@ -355,7 +385,7 @@ export class SurfaceObserver {
355
385
  return;
356
386
  const added = [...record.addedNodes];
357
387
  const { target: parent, nextSibling: next, previousSibling: prev } = record;
358
- this.#removed.push({ parent, nodes: removed, added, next, prev });
388
+ this.#removed.push({ parent, nodes: removed, added, next, prev, foreign: !own });
359
389
  // Wrapper replacement: the browser replaced a text element with its own nodes.
360
390
  const [text] = removed;
361
391
  if (removed.length === 1 &&
@@ -394,21 +424,33 @@ export class SurfaceObserver {
394
424
  * browser removed them one record each (Firefox): an anchor-only record is
395
425
  * inverted in a parent that gets a registered element back, else the
396
426
  * element's recorded siblings are gone and it lands outside Svelte's block.
427
+ * A foreign range across blocks (`dom.anchors.foreign`: a script's
428
+ * `Range.deleteContents()`) takes the render's anchors between the blocks
429
+ * from their list (the root, a block's children) and those after its start
430
+ * and before its end inside the blocks it enters, and leaves the elements:
431
+ * Svelte's next removal of either block would start at a detached anchor
432
+ * and leave its element, a child added to one would find no anchor to
433
+ * render before. When a foreign record took anchors from a block list,
434
+ * every foreign anchor-only record of the batch is inverted. The render's
435
+ * own removals (an item leaving the list) are never undone.
397
436
  */
398
437
  #restore = () => {
399
438
  const root = this.edytor.node;
400
439
  const removals = this.#removed.splice(0).reverse();
401
440
  const held = ({ parent, nodes }) => nodes.some((node) => node.parentNode !== parent && this.#holds(node));
402
441
  const parents = new Set(removals.filter(held).map(({ parent }) => parent));
442
+ const crossed = removals.some((removal) => this.#lost(removal) && this.#list(removal.parent));
403
443
  for (const removal of removals) {
404
444
  const { parent, nodes, added, next, prev } = removal;
405
445
  if (!root?.contains(parent))
406
446
  continue;
407
- if (!held(removal) && !(parents.has(parent) && nodes.every(isAnchor)))
447
+ const anchors = parents.has(parent) && nodes.every(isAnchor);
448
+ if (!held(removal) && !anchors && !(crossed && this.#lost(removal)))
408
449
  continue;
409
450
  // A browser-owned attempt's host: its change is adopted whatever the
410
451
  // browser did to its structure, which the re-render then restores.
411
- const inside = this.#textOf(parent);
452
+ const near = this.#near(parent);
453
+ const inside = near?.kind === 'text' && near.handle;
412
454
  if (inside && this.edytor.attempts.on(inside))
413
455
  continue;
414
456
  for (const node of added)
@@ -428,6 +470,17 @@ export class SurfaceObserver {
428
470
  }
429
471
  this.#replaced.clear();
430
472
  };
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');
431
484
  /**
432
485
  * Where an edit made at `at` of the base lands now: through the anchor
433
486
  * minted at it (it follows its text through a re-key, split, move or
@@ -479,6 +532,19 @@ export class SurfaceObserver {
479
532
  * `$effect`); otherwise contents waiting for a flush are left to it.
480
533
  */
481
534
  #pass = (flushed) => {
535
+ this.#passes++;
536
+ try {
537
+ return this.#compare(flushed);
538
+ }
539
+ finally {
540
+ // What the flush and this pass wrote: the render's records, compared
541
+ // at the next pass (as the observer's callback would have them).
542
+ if (this.#take())
543
+ queueMicrotask(this.signal);
544
+ this.#passes--;
545
+ }
546
+ };
547
+ #compare = (flushed) => {
482
548
  const { edytor } = this;
483
549
  const cells = edytor.cells;
484
550
  if (!cells || !edytor.node?.isConnected || edytor.destroyed)
@@ -534,7 +600,7 @@ export class SurfaceObserver {
534
600
  }
535
601
  if (this.#fresh) {
536
602
  this.#fresh = false;
537
- void settle(work).then(edytor.projector.inputHandled);
603
+ void settle(work).then(edytor.projector.rendered);
538
604
  }
539
605
  return work;
540
606
  };
@@ -620,31 +686,11 @@ export class SurfaceObserver {
620
686
  #invert = (id) => {
621
687
  if (id === this.#live())
622
688
  return;
623
- if (this.#shape(id) || !this.#rewrite(id))
689
+ if (this.#shape(id, true))
624
690
  this.edytor.cells?.remount(id);
625
691
  else
626
692
  this.update();
627
693
  };
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
694
  /** A text element's children that stand for render deltas (Svelte's anchors and the newline marker aside). */
649
695
  #nodes = (element) => [...element.childNodes].filter((node) => !isAnchor(node) &&
650
696
  !(node instanceof Element && node.hasAttribute('data-edytor-trailing-newline')));
@@ -655,7 +701,7 @@ export class SurfaceObserver {
655
701
  */
656
702
  #adoptContent = (id) => {
657
703
  const { edytor } = this;
658
- let broken = Boolean(this.#shape(id));
704
+ let broken = this.#shape(id);
659
705
  const caret = getCollapsedDomTextSelection(edytor);
660
706
  const work = [];
661
707
  for (const text of this.#texts(id)) {
@@ -688,7 +734,7 @@ export class SurfaceObserver {
688
734
  const value = now.slice(0, local) + insert + now.slice(local + remove);
689
735
  return adopt(this.edytor, text, value, undefined, local + insert.length);
690
736
  };
691
- #texts = (id) => [...(this.#contents.get(id) ?? [])].flatMap((element) => this.edytor.nodeToText.get(element) ?? []);
737
+ #texts = (id) => [...(this.#contents.get(id) ?? [])].flatMap((element) => this.handleOf(element, 'text') ?? []);
692
738
  /** A content's DOM: its registered text and atom elements in order; null when not shown. */
693
739
  #read = (id) => {
694
740
  const elements = [...(this.#contents.get(id) ?? [])];
@@ -697,20 +743,17 @@ export class SurfaceObserver {
697
743
  elements.sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
698
744
  const host = this.#blocks.get(id);
699
745
  let text = '';
700
- let broken = host && !host.isConnected ? 'missing' : null;
746
+ let broken = !!host && !host.isConnected;
701
747
  for (const element of elements) {
702
748
  const kind = this.#registry.get(element)?.kind;
703
749
  // A mark is inside its text element: its presence is the shape check's.
704
750
  if (kind === 'mark')
705
751
  continue;
706
- if (!element.isConnected)
707
- broken ??= 'missing';
708
- else if (host && !host.contains(element))
709
- broken ??= 'escaped';
752
+ broken ||= !element.isConnected || (!!host && !host.contains(element));
710
753
  if (kind === 'atom')
711
754
  text += ATOM;
712
755
  else if (kind === 'text') {
713
- const found = this.edytor.nodeToText.get(element);
756
+ const found = this.handleOf(element, 'text');
714
757
  text += (found ? readDomText(found) : (element.textContent ?? '')).replace(ZWSP, '');
715
758
  }
716
759
  }
@@ -718,46 +761,50 @@ export class SurfaceObserver {
718
761
  };
719
762
  /**
720
763
  * 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.
764
+ * container) holds one node per render delta (the empty filler's one) — a
765
+ * 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.
723
769
  */
724
- #shape = (id) => {
770
+ #shape = (id, write) => {
771
+ const writes = [];
725
772
  for (const text of this.#texts(id)) {
726
773
  const element = text.node;
727
774
  if (!element)
728
775
  continue;
729
776
  const deltas = this.edytor.deltasOf(text);
777
+ const shown = deltas.length ? deltas : [{ text: '\u200B', marks: [] }];
730
778
  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()) {
779
+ if (nodes.length !== shown.length)
780
+ return true;
781
+ for (const [i, delta] of shown.entries()) {
739
782
  const node = nodes[i];
783
+ let leaf = node;
740
784
  const chain = delta.marks
741
785
  .map(([name]) => name)
742
786
  .filter((name) => rendersMark(this.edytor.marks, name));
743
- if (!chain.length) {
744
- if (node.nodeType !== Node.TEXT_NODE)
745
- return 'foreign-child';
746
- continue;
787
+ if (chain.length) {
788
+ if (!this.#registry.has(node))
789
+ return true;
790
+ const marks = [node, ...node.querySelectorAll('[data-edytor-mark]')]
791
+ .map((el) => this.#registry.get(el)?.name)
792
+ .filter(Boolean);
793
+ leaf = textLeaf(node);
794
+ // The snippet's markup is its own, but the delta renders one text node.
795
+ if (marks.join() !== chain.join() || !leaf || textLeaf(node, 1))
796
+ return true;
747
797
  }
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';
798
+ else if (node.nodeType !== Node.TEXT_NODE)
799
+ return true;
800
+ if (leaf.data !== delta.text)
801
+ writes.push([leaf, delta.text]);
758
802
  }
759
803
  }
760
- return null;
804
+ if (write)
805
+ for (const [leaf, value] of writes)
806
+ leaf.data = value;
807
+ return false;
761
808
  };
762
809
  /** The root's children differ from its registered block elements in cell order, the anchor, Svelte's anchors. */
763
810
  #rootDiverges = () => {
@@ -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)
@@ -28,11 +28,23 @@ export declare class Projector {
28
28
  * removal re-rendering the text under a range) runs now. The flag is no
29
29
  * reactive source, so no later flush is owed: without this, the range the
30
30
  * render moved (Firefox keeps it where the removed element stood) would be
31
- * adopted as the gesture's when its `selectionchange` lands. The observer
32
- * calls it too once it processed DOM records: a display that waits gets a
33
- * pass.
31
+ * adopted as the gesture's when its `selectionchange` lands.
34
32
  */
35
33
  inputHandled: () => void;
34
+ /**
35
+ * The observer compared the records of a DOM change (a render, a browser
36
+ * edit): a display that waits on them gets a pass (`inputHandled`), and a
37
+ * DOM selection they moved off the last observation with no gesture since
38
+ * is render drift no event reports (`sel.drift.unreported`). A render no
39
+ * commit asked for (a kind's decoration reading state of its own: a code
40
+ * grammar that lands re-tokenizes its lines) replaced the text node the
41
+ * caret was in; the browser left the caret where that node stood (the
42
+ * line's start) and Chromium and WebKit send no `selectionchange` for it,
43
+ * while one that comes would read as foreign (no flush ran the projector
44
+ * since). The value is displayed again now, in the render's own task:
45
+ * before a key, or its `beforeinput` declaring that caret, can read it.
46
+ */
47
+ rendered: () => void;
36
48
  /**
37
49
  * Classify a `selectionchange` by comparing the DOM selection with the
38
50
  * last display, the render epoch and the gesture serial: