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
@@ -34,10 +34,12 @@
34
34
  * the render epoch and the gesture serial: echo, render drift (displayed
35
35
  * again), composition, foreign write or intent (adopted). Two named, counted,
36
36
  * time-bounded browser rules are the only signatures:
37
- * the Android post-delete snap-back and the IME post-commit jump.
37
+ * the Android post-delete snap-back and the IME post-commit jump. A render
38
+ * that moved the DOM selection with no event to say so is drift too, read
39
+ * from the observer's records (`rendered`, `sel.drift.unreported`).
38
40
  */
39
41
  import { untrack } from 'svelte';
40
- import { clearDomSelection, createDomRange, getActiveElement, getDomSelection, getDomSelectionSnapshot, scrollCaretIntoView } from '../selection/domSelection.js';
42
+ import { clearDomSelection, createDomRange, focusIn, getActiveElement, getDomSelection, getDomSelectionSnapshot, scrollCaretIntoView } from '../selection/domSelection.js';
41
43
  import { getMarkEdgeSide, getYIndex, isTextBoundSelectionPoint } from '../selection/selection.utils.js';
42
44
  import { project, sameEnds } from '../session/selection.js';
43
45
  import { isAndroidChromeBrowser } from '../events/events.utils.js';
@@ -88,6 +90,8 @@ export class Projector {
88
90
  #opening = false;
89
91
  /** The gesture serial at the last observation (a display or an adopted `selectionchange`). */
90
92
  #serial = -1;
93
+ /** Where the DOM selection was at that observation (`#note`; null: no range). */
94
+ #at = null;
91
95
  /** The named rules' evidence: the last model-owned delete and the last IME commit. */
92
96
  #deleted = null;
93
97
  #committed = null;
@@ -112,9 +116,7 @@ export class Projector {
112
116
  pre = () => {
113
117
  void this.#deps();
114
118
  untrack(() => {
115
- const node = this.edytor.node;
116
- const active = node && getActiveElement(node);
117
- this.#noted = active && node.contains(active) ? active : null;
119
+ this.#noted = focusIn(this.edytor.node);
118
120
  });
119
121
  };
120
122
  /** Root post-flush `$effect`: every DOM write of the flush has landed. */
@@ -183,14 +185,51 @@ export class Projector {
183
185
  * removal re-rendering the text under a range) runs now. The flag is no
184
186
  * reactive source, so no later flush is owed: without this, the range the
185
187
  * render moved (Firefox keeps it where the removed element stood) would be
186
- * adopted as the gesture's when its `selectionchange` lands. The observer
187
- * calls it too once it processed DOM records: a display that waits gets a
188
- * pass.
188
+ * adopted as the gesture's when its `selectionchange` lands.
189
189
  */
190
190
  inputHandled = () => {
191
191
  if (this.#pending)
192
192
  this.edytor.surface.update();
193
193
  };
194
+ /**
195
+ * The observer compared the records of a DOM change (a render, a browser
196
+ * edit): a display that waits on them gets a pass (`inputHandled`), and a
197
+ * DOM selection they moved off the last observation with no gesture since
198
+ * is render drift no event reports (`sel.drift.unreported`). A render no
199
+ * commit asked for (a kind's decoration reading state of its own: a code
200
+ * grammar that lands re-tokenizes its lines) replaced the text node the
201
+ * caret was in; the browser left the caret where that node stood (the
202
+ * line's start) and Chromium and WebKit send no `selectionchange` for it,
203
+ * while one that comes would read as foreign (no flush ran the projector
204
+ * since). The value is displayed again now, in the render's own task:
205
+ * before a key, or its `beforeinput` declaring that caret, can read it.
206
+ */
207
+ rendered = () => {
208
+ if (this.#unreported())
209
+ this.edytor.selection.display();
210
+ else
211
+ this.inputHandled();
212
+ };
213
+ /**
214
+ * `rendered`'s drift: a text value, no display waiting, no gesture, drag
215
+ * or composition since the last observation, and the DOM selection in the
216
+ * host no longer where that observation found it.
217
+ */
218
+ #unreported = () => {
219
+ const { edytor } = this;
220
+ const { selection } = edytor;
221
+ const at = this.#at;
222
+ const node = edytor.node;
223
+ if (!at || !node || selection.value.kind !== 'text' || edytor.intentSerial !== this.#serial)
224
+ return false;
225
+ if (selection.request !== this.#request || selection.dragging || edytor.composition.live)
226
+ return false;
227
+ const dom = getDomSelection(node);
228
+ if (!dom || !isOwnPoint(node, dom.anchorNode))
229
+ return false;
230
+ return !(samePoint(at.anchor, dom.anchorNode, dom.anchorOffset) &&
231
+ samePoint(at.focus, dom.focusNode, dom.focusOffset));
232
+ };
194
233
  /**
195
234
  * Classify a `selectionchange` by comparing the DOM selection with the
196
235
  * last display, the render epoch and the gesture serial:
@@ -283,9 +322,19 @@ export class Projector {
283
322
  /** The DOM selection is observed now (adopted, or ignored by the adopter's own rules). */
284
323
  #observed = (observation) => {
285
324
  this.#seen = this.#flushes;
286
- this.#serial = this.edytor.intentSerial;
325
+ this.#note();
287
326
  return observation;
288
327
  };
328
+ /** An observation: its gesture serial, and where the DOM selection is (`#at`). */
329
+ #note = () => {
330
+ const { edytor } = this;
331
+ this.#serial = edytor.intentSerial;
332
+ const dom = edytor.node && getDomSelection(edytor.node);
333
+ this.#at =
334
+ dom?.anchorNode && dom.focusNode
335
+ ? { anchor: [dom.anchorNode, dom.anchorOffset], focus: [dom.focusNode, dom.focusOffset] }
336
+ : null;
337
+ };
289
338
  /** The DOM selection is our last display unchanged, or a text-bound range that shows the value. */
290
339
  #echoes = (dom) => {
291
340
  const displayed = this.#displayed;
@@ -344,7 +393,7 @@ export class Projector {
344
393
  observe = () => {
345
394
  this.#parking = false;
346
395
  this.#seen = this.#flushes;
347
- this.#serial = this.edytor.intentSerial;
396
+ this.#note();
348
397
  this.#displayed = null;
349
398
  };
350
399
  /** Answers whether the pass completed (false: wait for a later flush). */
@@ -379,8 +428,7 @@ export class Projector {
379
428
  // whose `selectionchange` can land after that key counted as a
380
429
  // gesture) leaves any caret the browser parks later its own
381
430
  // (`#parking`, `placed`), whatever key comes before it.
382
- const active = getActiveElement(node);
383
- if (requested && value.kind !== 'blocks' && active && node.contains(active))
431
+ if (requested && value.kind !== 'blocks' && focusIn(node))
384
432
  this.#parking = true;
385
433
  const inside = !!dom?.anchorNode && node.contains(dom.anchorNode);
386
434
  const clears = value.kind === 'none' ? requested && inside : inside || dom?.rangeCount === 0;
@@ -388,7 +436,7 @@ export class Projector {
388
436
  if (inside)
389
437
  clearDomSelection(node);
390
438
  this.#displayed = null;
391
- this.#serial = edytor.intentSerial;
439
+ this.#note();
392
440
  }
393
441
  return true;
394
442
  }
@@ -408,15 +456,11 @@ export class Projector {
408
456
  if (!this.#shows(dom, range, isReversed)) {
409
457
  if (!this.#ours(requested))
410
458
  return true;
411
- const active = getActiveElement(node);
412
- if (!(active && node.contains(active))) {
459
+ if (!focusIn(node)) {
413
460
  // Focus first (focusing a host can move the selection): the editing
414
461
  // host, never a focusable element inside the text such as a link.
415
- edytor.expectInternalFocus();
416
462
  const element = start[0] instanceof HTMLElement ? start[0] : start[0].parentElement;
417
- (element?.closest('[contenteditable="true"]') ?? node).focus({
418
- preventScroll: true
419
- });
463
+ edytor.expectInternalFocus(element?.closest('[contenteditable="true"]') ?? node);
420
464
  }
421
465
  this.#write(getDomSelection(node) ?? dom, points, isCollapsed);
422
466
  if (selection.scrollOnDisplay)
@@ -425,7 +469,7 @@ export class Projector {
425
469
  selection.scrollOnDisplay = false;
426
470
  this.#displayed = { ...points, epoch: selection.epoch };
427
471
  this.#seen = this.#flushes;
428
- this.#serial = edytor.intentSerial;
472
+ this.#note();
429
473
  selection.observed({
430
474
  edge: isCollapsed ? getMarkEdgeSide(startText, start[0], yStart) : undefined
431
475
  });
@@ -500,10 +544,11 @@ export class Projector {
500
544
  #ours = (requested) => {
501
545
  const node = this.edytor.node;
502
546
  const active = getActiveElement(node);
547
+ const inside = focusIn(node);
503
548
  // A form control or a foreign editable in a kind's chrome (a field bound to
504
549
  // `block.data`) owns its focus and its selection: never written over.
505
- if (active && node.contains(active))
506
- return !isKindControl(node, active);
550
+ if (inside)
551
+ return !isKindControl(node, inside);
507
552
  const document = node.ownerDocument;
508
553
  if (active && active !== document.body && active !== document.documentElement)
509
554
  return false;
@@ -27,28 +27,26 @@ export class Text {
27
27
  get parent() {
28
28
  return this.edytor.idToBlock.block(this.blockId);
29
29
  }
30
- /** This segment in its block's parts, and its position among them. */
31
- get #at() {
30
+ /** This segment in its block's parts. */
31
+ get #parts() {
32
32
  if ($effect.tracking())
33
33
  this.edytor.cells?.get(this.blockId);
34
- const parts = this.edytor.idToBlock.parts(this.blockId);
35
- for (let index = 0, k = 0; index < parts.length; index++) {
36
- const part = parts[index];
37
- if (part.kind === 'text' && k++ === this.ordinal)
38
- return { part, index };
39
- }
40
- return { index: -1 };
34
+ return this.edytor.idToBlock.parts(this.blockId);
35
+ }
36
+ get #part() {
37
+ return this.#parts.find((part) => part.kind === 'text' && part.segOrd === this.ordinal);
41
38
  }
42
39
  /** Position among the block's content parts. */
43
40
  get index() {
44
- return this.#at.index;
41
+ const parts = this.#parts;
42
+ return parts.indexOf(this.#part);
45
43
  }
46
44
  get value() {
47
- return segmentJSON(this.#at.part?.items ?? []);
45
+ return segmentJSON(this.#part?.runs ?? []);
48
46
  }
49
47
  /** The segment's text. */
50
48
  get stringContent() {
51
- return this.#at.part?.items.map((item) => item.text).join('') ?? '';
49
+ return this.#part?.text ?? '';
52
50
  }
53
51
  get isEmpty() {
54
52
  return this.length === 0;
@@ -57,15 +55,15 @@ export class Text {
57
55
  return this.stringContent.endsWith('\n');
58
56
  }
59
57
  get length() {
60
- return this.#at.part?.length ?? 0;
58
+ return this.stringContent.length;
61
59
  }
62
60
  /** Display offset of this segment inside its block's content (atoms count 1). */
63
61
  get segStart() {
64
- return this.#at.part?.start ?? 0;
62
+ return this.#part?.start ?? 0;
65
63
  }
66
64
  /** The segment is shown by a live block. */
67
65
  get isInDocument() {
68
- return this.parent.isInTree && this.#at.part !== undefined;
66
+ return this.parent.isInTree && this.#part !== undefined;
69
67
  }
70
68
  /**
71
69
  * Re-create this block's text elements from its cell — the repair of a
@@ -101,9 +99,8 @@ export class Text {
101
99
  /** @internal */
102
100
  attach = (node) => {
103
101
  this.node = node;
104
- this.edytor.nodeToText.set(node, this);
105
102
  // Registered, it takes the attribute table's attributes (`surface/attributes.ts`).
106
- const release = this.edytor.surface.register(node, 'text', this.parent.id);
103
+ const release = this.edytor.surface.register(node, 'text', this.parent.id, undefined, undefined, this);
107
104
  const pluginDestroy = cleanups(this.edytor.plugins, (plugin) => plugin.onTextAttached?.({ node, text: this }));
108
105
  // A display that waited for a mounted destination runs in the next pass.
109
106
  this.edytor.projector?.mounted(this);
@@ -111,8 +108,6 @@ export class Text {
111
108
  destroy: () => {
112
109
  if (this.node === node)
113
110
  this.node = undefined;
114
- if (this.edytor.nodeToText.get(node) === this)
115
- this.edytor.nodeToText.delete(node);
116
111
  release();
117
112
  pluginDestroy.forEach((destroy) => destroy());
118
113
  }
@@ -807,6 +807,35 @@
807
807
  color: var(--notion-placeholder);
808
808
  }
809
809
 
810
+ /*
811
+ * Shared suggestions: a block a suggestion made or proposes to delete
812
+ * (`data-edytor-suggested`), and its runs in the overlay, in its author's
813
+ * colour (`--edytor-range-color`).
814
+ */
815
+ .edytor-notion [data-edytor-suggested='inserted'],
816
+ .edytor-notion [data-edytor-suggested='split'] {
817
+ box-shadow: inset 2px 0 0 var(--edytor-suggestion-inserted, rgb(68 131 97));
818
+ background: var(--edytor-suggestion-inserted-background, rgba(68, 131, 97, 0.07));
819
+ }
820
+ .edytor-notion [data-edytor-suggested='removed'] {
821
+ background: var(--edytor-suggestion-removed);
822
+ }
823
+ .edytor-notion [data-edytor-suggested='removed'] [data-edytor-text] {
824
+ text-decoration: line-through;
825
+ text-decoration-color: var(--edytor-suggestion-removed-line, rgba(235, 87, 87, 0.6));
826
+ }
827
+ :is(.edytor-notion, .edytor-notion + [data-edytor-overlay]) [data-edytor-suggestion-inserted] {
828
+ border-bottom: 2px solid var(--edytor-range-color, rgb(68 131 97));
829
+ background: color-mix(in srgb, var(--edytor-range-color, rgb(68 131 97)) 10%, transparent);
830
+ }
831
+ :is(.edytor-notion, .edytor-notion + [data-edytor-overlay]) [data-edytor-suggestion-deleted] {
832
+ background: linear-gradient(
833
+ transparent calc(50% - 1px),
834
+ var(--edytor-range-color, rgb(235 87 87)) calc(50% - 1px) calc(50% + 1px),
835
+ transparent calc(50% + 1px)
836
+ );
837
+ }
838
+
810
839
  /* ── Page (a subpage link) and table of contents ────────────────────── */
811
840
  .edytor-notion [data-edytor-type='page'] {
812
841
  margin: 1px 0;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "edytor",
3
3
  "license": "MIT",
4
- "version": "3.0.0",
4
+ "version": "5.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/beynar/edytor.git"
@@ -23,6 +23,7 @@
23
23
  "check:deps": "node scripts/deps.mjs",
24
24
  "check:docs": "node scripts/check-docs.mjs",
25
25
  "check:api": "node scripts/api-report.mjs --check",
26
+ "check:size": "node scripts/bundle-size.mjs --budget bundle-budget.json --top 0",
26
27
  "api:report": "node scripts/api-report.mjs",
27
28
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
28
29
  "lint": "prettier --check . && eslint .",
@@ -50,6 +51,7 @@
50
51
  "bench:browser": "node bench/browser.js",
51
52
  "bench:runs": "BENCH_RUNS=1 vitest --config ./vitest.crdt.config.ts --run src/tests/crdt/runs/runs.bench.test.ts",
52
53
  "size": "node scripts/bundle-size.mjs --by dir",
54
+ "bundle:attribution": "node scripts/bundle-attribution.mjs",
53
55
  "registry:build": "shadcn-svelte registry build ./registry.json --output site/public/r",
54
56
  "check:registry": "node scripts/check-registry.mjs",
55
57
  "test:registry-install": "sh tests/registry-install/run.sh",
@@ -163,7 +165,7 @@
163
165
  }
164
166
  },
165
167
  "dependencies": {
166
- "@tanstack/highlight": "^0.1.0",
168
+ "@tanstack/highlight": "^1.1.0",
167
169
  "@tanstack/markdown": "1.0.0",
168
170
  "esm-env": "^1.2.2",
169
171
  "lib0-v14": "npm:lib0@1.0.0-rc.32"
@@ -1,87 +0,0 @@
1
- /**
2
- * Rank keys for the placement model — a Logoot-style dense ordering.
3
- *
4
- * A rank is a non-empty sequence of *segments* `(v, t)`:
5
- *
6
- * - `v` — a signed digit in `[VMIN, VMAX]` (±2^40). There is always room
7
- * between two distinct digits (`rv - lv > 1`); when there is not, the
8
- * generator descends one level instead of renumbering.
9
- * - `t` — the allocator's `doc.clientID` (uint53), the *tiebreak* that makes
10
- * concurrent allocations of the same digit path distinct, or none
11
- * (`Infinity`: sorts above every client) on a segment that only leads to
12
- * deeper ones. The last segment always carries one (schema
13
- * generation 5): a rank's own client sits on its last segment, and a
14
- * copied segment keeps its tie only where the order needs it (between two
15
- * keys that differ only by their ties).
16
- *
17
- * Encoding is variable-length and order-preserving, so the persisted rank
18
- * is a plain string and the comparator is `a < b` (CRDT study 2026-10;
19
- * up to generation 4 every segment was 16 characters, 9 of them a tie):
20
- *
21
- * - `v` → a length character (`'H'` for zero, `'H' + k` for a positive
22
- * digit of `k` base-64 places, `'H' - k` for a negative one, its places
23
- * complemented) and its places — small digits are short;
24
- * - a tie → `'!'`, then `t` as a digit (`'!'` sorts below every length
25
- * character, so a tied segment sorts before the same digit leading on);
26
- * - rank = segments concatenated; a prefix sorts before its extension, as
27
- * string `<` does.
28
- *
29
- * Allocation (`rankBetween(left, right, clientId)`):
30
- *
31
- * - gap at level `i` (`rv - lv > 1`): emit `lv + 1 + floor(rand·(rv-lv-1))`,
32
- * own tie — random-in-gap keeps sequential inserts shallow and concurrent
33
- * same-gap inserts deterministic-distinct.
34
- * - adjacent or equal digits (`rv - lv ≤ 1`): the left digit or the right
35
- * one with the allocator's own tie when that tie sorts between theirs;
36
- * else copy the left segment and descend — untied when that already sorts
37
- * below the right bound (the result is then above left and below right,
38
- * and the next level is free), tied otherwise.
39
- * - left exhausted/open: emit `rSeg.v - 1` (boundary-minus). Sequential
40
- * prepends stay at depth 1; the bound can only underflow after ~2^40
41
- * consecutive boundary pushes — documented unreachable, guarded by a
42
- * descriptive throw.
43
- *
44
- * No floating point, no global renumbering, no wall clocks.
45
- */
46
- /** Digit range. VMIN = -2^40, VMAX = 2^40 - 1. */
47
- export declare const RANK_VMIN: number;
48
- export declare const RANK_VMAX: number;
49
- export type RankSeg = {
50
- v: number;
51
- t: number;
52
- };
53
- /** Thrown when rank generation would need a digit below VMIN (unreachable). */
54
- export declare class RankSpaceExhausted extends Error {
55
- constructor();
56
- }
57
- /** Decode a rank string into its segment array (`t: Infinity` where a segment has no tie). */
58
- export declare const decodeRank: (rank: string) => RankSeg[];
59
- /** Encode segments back to the persisted string form. */
60
- export declare const encodeRank: (segs: readonly RankSeg[]) => string;
61
- /**
62
- * Allocate a rank strictly between `left` and `right` (either may be
63
- * `undefined` for the open end of a sibling list). `clientId` is the
64
- * allocating doc's clientID; `rand` supplies the in-gap pick (defaults to
65
- * `Math.random`, tests inject a seeded stream).
66
- *
67
- * Guaranteed: `left < result < right` for any `left < right`, including
68
- * equal-prefix/concurrent-collision inputs — the tiebreak dimension makes a
69
- * strictly-between key always exist (modulo the documented VMIN bound).
70
- */
71
- export declare const rankBetween: (left: string | undefined, right: string | undefined, clientId: number, rand?: () => number, compact?: boolean) => string;
72
- /** The base of the run band: a run member's digit lies just above it. */
73
- export declare const RANK_RUN: number;
74
- /**
75
- * A rank in `(left, right)` for an insert right after `left` — one rule for
76
- * array items (`crdt/data.ts`) and inserted blocks (`order.insert.run`).
77
- * When `client` made `left`, the rank is in the client's run after it: `left`
78
- * itself as the prefix, then one segment of the run band — or, when `left`
79
- * is already a member of the client's run, that run's prefix and a member
80
- * between `left` and the next one. So what one client inserts after its own
81
- * items stays together whatever a peer inserts in that gap meanwhile: a
82
- * peer's rank there is either no extension of the run's prefix (it sorts
83
- * after the whole run) or one whose next digit is above the band (YATA's
84
- * origin rule, as `Y.Array` keeps an insert after its origin). Elsewhere, or
85
- * when the run rank would not sort below `right`, a plain {@link rankBetween}.
86
- */
87
- export declare const rankAfter: (left: string | undefined, right: string | undefined, client: number, rand?: () => number) => string;