edytor 4.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 (147) 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 +115 -106
  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/capability.js +9 -13
  29. package/dist/crdt/doc/funnel.d.ts +1 -1
  30. package/dist/crdt/doc/funnel.js +5 -2
  31. package/dist/crdt/doc/gate.d.ts +1 -1
  32. package/dist/crdt/doc/history.d.ts +6 -0
  33. package/dist/crdt/doc/history.js +77 -56
  34. package/dist/crdt/doc/layout.js +5 -5
  35. package/dist/crdt/doc/meta.js +6 -7
  36. package/dist/crdt/doc/reads.d.ts +2 -2
  37. package/dist/crdt/doc/reads.js +18 -28
  38. package/dist/crdt/doc/steps.js +4 -4
  39. package/dist/crdt/doc/suggest.d.ts +167 -0
  40. package/dist/crdt/doc/suggest.js +579 -0
  41. package/dist/crdt/document.d.ts +10 -1
  42. package/dist/crdt/document.js +19 -11
  43. package/dist/crdt/edytor-doc.d.ts +355 -333
  44. package/dist/crdt/edytor-doc.js +125 -82
  45. package/dist/crdt/engine-api.d.ts +8 -2
  46. package/dist/crdt/engine.d.ts +2 -1
  47. package/dist/crdt/engine.js +3 -1
  48. package/dist/crdt/flow.d.ts +1 -1
  49. package/dist/crdt/flow.js +4 -10
  50. package/dist/crdt/index.d.ts +111 -105
  51. package/dist/crdt/index.js +3 -0
  52. package/dist/crdt/operations.d.ts +12 -3
  53. package/dist/crdt/parts.d.ts +43 -0
  54. package/dist/crdt/parts.js +44 -0
  55. package/dist/crdt/placement/display.d.ts +1 -1
  56. package/dist/crdt/placement/display.js +3 -3
  57. package/dist/crdt/placement/keys.d.ts +5 -0
  58. package/dist/crdt/placement/keys.js +5 -0
  59. package/dist/crdt/placement/resolve.js +2 -1
  60. package/dist/crdt/protocol.d.ts +1 -1
  61. package/dist/crdt/protocol.js +1 -1
  62. package/dist/crdt/protocols/auth.d.ts +11 -1
  63. package/dist/crdt/protocols/auth.js +17 -1
  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 +6 -4
  67. package/dist/crdt/providers/index.js +3 -1
  68. package/dist/crdt/providers/websocket.d.ts +6 -4
  69. package/dist/crdt/providers/websocket.js +9 -4
  70. package/dist/crdt/purge.d.ts +7 -1
  71. package/dist/crdt/purge.js +32 -2
  72. package/dist/crdt/restore.d.ts +2 -1
  73. package/dist/crdt/restore.js +9 -0
  74. package/dist/crdt/schema.d.ts +9 -1
  75. package/dist/crdt/schema.js +13 -3
  76. package/dist/crdt/structs.d.ts +2 -0
  77. package/dist/crdt/structs.js +2 -0
  78. package/dist/crdt/text/index/claims.js +1 -23
  79. package/dist/crdt/text/index/layout.d.ts +2 -4
  80. package/dist/crdt/text/index/layout.js +9 -22
  81. package/dist/crdt/text/index/placement.js +21 -24
  82. package/dist/crdt/text/index/shared.js +17 -14
  83. package/dist/crdt/text/model.d.ts +10 -0
  84. package/dist/crdt/text/model.js +8 -1
  85. package/dist/crdt/text/runs.d.ts +34 -30
  86. package/dist/crdt/vendor/yjs/UPSTREAM.md +83 -0
  87. package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
  88. package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +6 -3
  89. package/dist/crdt/vendor/yjs/dts/ynode.d.ts +13 -2
  90. package/dist/crdt/vendor/yjs/src/index.js +1 -1
  91. package/dist/crdt/vendor/yjs/src/structs/Item.js +44 -5
  92. package/dist/crdt/vendor/yjs/src/ynode.js +29 -7
  93. package/dist/edytor.svelte.d.ts +1 -1
  94. package/dist/edytor.svelte.js +100 -75
  95. package/dist/events/beforeInputCommands.d.ts +2 -2
  96. package/dist/events/nativeInteractiveControl.d.ts +11 -1
  97. package/dist/events/nativeInteractiveControl.js +41 -1
  98. package/dist/events/onBeforeInput.js +3 -3
  99. package/dist/events/onFocus.js +9 -11
  100. package/dist/export/forms.d.ts +25 -25
  101. package/dist/index.d.ts +4 -2
  102. package/dist/index.js +1 -1
  103. package/dist/labels.d.ts +8 -0
  104. package/dist/labels.js +6 -1
  105. package/dist/plugins/comments/CommentsController.svelte.d.ts +5 -1
  106. package/dist/plugins/comments/CommentsController.svelte.js +14 -4
  107. package/dist/plugins/find/search.js +2 -2
  108. package/dist/plugins/index.d.ts +1 -0
  109. package/dist/plugins/index.js +1 -0
  110. package/dist/plugins/suggestions/SuggestionBars.svelte +40 -23
  111. package/dist/plugins/suggestions/SuggestionBars.svelte.d.ts +3 -1
  112. package/dist/plugins/suggestions/SuggestionRuns.svelte +77 -0
  113. package/dist/plugins/suggestions/SuggestionRuns.svelte.d.ts +9 -0
  114. package/dist/plugins/suggestions/shared.svelte.d.ts +58 -0
  115. package/dist/plugins/suggestions/shared.svelte.js +421 -0
  116. package/dist/plugins/suggestions/suggestionsPlugin.d.ts +15 -4
  117. package/dist/selection/domSelection.d.ts +2 -0
  118. package/dist/selection/domSelection.js +6 -2
  119. package/dist/selection/pointer.svelte.js +75 -48
  120. package/dist/selection/replaceSelection.js +2 -6
  121. package/dist/selection/selection.svelte.d.ts +3 -3
  122. package/dist/selection/selection.svelte.js +39 -32
  123. package/dist/selection/selection.utils.d.ts +14 -0
  124. package/dist/selection/selection.utils.js +28 -3
  125. package/dist/session/commands.js +21 -11
  126. package/dist/session/composition.svelte.d.ts +5 -3
  127. package/dist/session/composition.svelte.js +21 -17
  128. package/dist/session/handles.d.ts +4 -17
  129. package/dist/session/handles.js +11 -37
  130. package/dist/session/navigation.js +6 -9
  131. package/dist/session/selection.d.ts +11 -5
  132. package/dist/session/selection.js +14 -48
  133. package/dist/session/suggestions.svelte.d.ts +38 -9
  134. package/dist/session/suggestions.svelte.js +37 -18
  135. package/dist/session/virtual.d.ts +1 -1
  136. package/dist/surface/attributes.d.ts +5 -0
  137. package/dist/surface/attributes.js +2 -2
  138. package/dist/surface/cells.d.ts +2 -26
  139. package/dist/surface/cells.js +2 -33
  140. package/dist/surface/observer.svelte.d.ts +6 -1
  141. package/dist/surface/observer.svelte.js +128 -81
  142. package/dist/surface/pin.svelte.d.ts +9 -9
  143. package/dist/surface/pin.svelte.js +15 -22
  144. package/dist/surface/projector.svelte.js +8 -14
  145. package/dist/text/text.svelte.js +14 -19
  146. package/dist/themes/notion.css +29 -0
  147. package/package.json +3 -1
@@ -0,0 +1,143 @@
1
+ /**
2
+ * The suggest-only rule (`room.access.suggest`, `sugg.room.suggest`): what a
3
+ * frame of a socket whose user may only suggest may write. Every struct must
4
+ * be a suggestion's, under a client id of that user that is not the socket's
5
+ * own replica (its writes are suggestion writers'):
6
+ *
7
+ * - a suggestion record (an attr of the root `suggest`), and what a record
8
+ * of its user holds (its attrs, its writer's id lists and operations);
9
+ * - its own binding and profile (the attribution root: the trust rules
10
+ * check them);
11
+ * - text in a block's backing text (an insertion; a split's boundary; a
12
+ * merge's claim of a block its user made; a mark next to its user's own
13
+ * text), and an atom's attrs of an atom its user made;
14
+ * - a block its user made (its registry entry, attrs, children's slots) and
15
+ * a home slot naming such a block in any block's sequence;
16
+ * - a hold record naming only its user's units (deleting its own text).
17
+ *
18
+ * Anything else strips the client from the frame, as a forged mark does, and
19
+ * a delete of an item another user wrote is dropped. Residuals: a mark both of
20
+ * whose ends touch its user's text covers what lies between them, and its user
21
+ * may delete its own accepted text directly (the room reads no record's
22
+ * decisions). Pure reads of the room document and the frame.
23
+ */
24
+ import { Y } from '../../crdt/engine.js';
25
+ import { ATTRIBUTION_ROOT, BLOCK_NODE, CONTENT_NODE, INLINE_NODE, REGISTRY_KEY, SUGGEST_ROOT, TEXT_DELETES_ROOT } from '../../crdt/schema.js';
26
+ import { isId } from './forged.js';
27
+ import { heldClock, runsOf, storedStruct, structAt } from './updates.js';
28
+ /** What a frame of a suggest-only socket may not write: the clients to strip, the deletes to drop. */
29
+ export const suggestOnly = (doc, { structs, ds }, skip, owns, replica) => {
30
+ const frame = runsOf(structs);
31
+ const strip = new Set();
32
+ /** The struct at `id`, the room's or the frame's. */
33
+ const at = (id) => (storedStruct(doc, id.client, id.clock) ?? structAt(frame.get(id.client) ?? [], id.clock));
34
+ /**
35
+ * Where the item at `id` sits: its map key (`null` in a sequence) and its
36
+ * parent, read from the item or, for an encoded one that names neither,
37
+ * from an origin of it (left, else right: one sequence, or one key).
38
+ */
39
+ const placeIn = (id) => {
40
+ const seen = new Set();
41
+ for (let next = id; next !== null;) {
42
+ const held = storedStruct(doc, next.client, next.clock);
43
+ if (held)
44
+ return held instanceof Y.Item ? { key: held.parentSub, parent: held.parent } : null;
45
+ const item = structAt(frame.get(next.client) ?? [], next.clock);
46
+ if (!(item instanceof Y.Item) || seen.has(item))
47
+ return null;
48
+ seen.add(item);
49
+ if (item.parent !== null || item.parentSub !== null)
50
+ return { key: item.parentSub, parent: item.parent };
51
+ next = item.origin ?? item.rightOrigin;
52
+ }
53
+ return null;
54
+ };
55
+ /** The node (and its item) a place's parent names: a root's name, or a nested node's. */
56
+ const nodeOf = (parent) => {
57
+ if (typeof parent === 'string')
58
+ return { root: parent };
59
+ if (isId(parent)) {
60
+ const item = at(parent);
61
+ return { name: item?.content?.type?.name, item: item?.id };
62
+ }
63
+ const node = parent;
64
+ if (!node)
65
+ return {};
66
+ if (!node._item) {
67
+ const root = [...doc.share].find(([, type]) => type === parent)?.[0];
68
+ return { root };
69
+ }
70
+ return { name: node.name, item: node._item.id };
71
+ };
72
+ const registry = () => doc.share.get(REGISTRY_KEY)?._map;
73
+ /** A block `id` whose node a client of this user made (in the room or the frame). */
74
+ const ownBlock = (id) => {
75
+ if (typeof id !== 'string')
76
+ return false;
77
+ const stored = registry()?.get(id);
78
+ if (stored)
79
+ return owns(stored.id.client);
80
+ return structs.some((s) => s instanceof Y.Item &&
81
+ owns(s.id.client) &&
82
+ placeIn(s.id)?.key === id &&
83
+ nodeOf(placeIn(s.id)?.parent).root === REGISTRY_KEY);
84
+ };
85
+ const allowed = (s) => {
86
+ const place = placeIn(s.id);
87
+ if (place === null)
88
+ return false;
89
+ const parent = nodeOf(place.parent);
90
+ const value = s.content.getContent?.()[0];
91
+ if (parent.root === SUGGEST_ROOT || parent.root === ATTRIBUTION_ROOT)
92
+ return true;
93
+ if (parent.name === 'suggestion')
94
+ return !!parent.item && owns(parent.item.client);
95
+ if (parent.root === TEXT_DELETES_ROOT) {
96
+ const ids = s.content.ids ?? [];
97
+ return ids.every((v, i) => i % 3 !== 0 || owns(v));
98
+ }
99
+ if (parent.name === CONTENT_NODE) {
100
+ // A mark (a format item: its key and value).
101
+ if (s.content.key !== undefined) {
102
+ const { origin, rightOrigin } = s;
103
+ const near = [origin, rightOrigin];
104
+ return near.some((id) => isId(id) && owns(id.client));
105
+ }
106
+ return value?.m === undefined || ownBlock(value.m);
107
+ }
108
+ if (parent.name === INLINE_NODE)
109
+ return !!parent.item && owns(parent.item.client);
110
+ if (parent.root === REGISTRY_KEY) {
111
+ // A top-level home slot of its own block, or its own block's entry (never another's).
112
+ if (place.key === null)
113
+ return ownBlock(value?.b);
114
+ const stored = registry()?.get(place.key);
115
+ return !stored || owns(stored.id.client);
116
+ }
117
+ if (parent.name === BLOCK_NODE) {
118
+ if (s.content.priority !== undefined)
119
+ return false;
120
+ if (place.key === null && value?.b !== undefined)
121
+ return ownBlock(value.b);
122
+ return !!parent.item && owns(parent.item.client);
123
+ }
124
+ return false;
125
+ };
126
+ for (const s of structs) {
127
+ const { client, clock } = s.id;
128
+ if (!(s instanceof Y.Item) || skip.has(client) || strip.has(client))
129
+ continue;
130
+ if (storedStruct(doc, client, clock) !== null)
131
+ continue;
132
+ if (client === replica || !owns(client) || !allowed(s))
133
+ strip.add(client);
134
+ }
135
+ // Deletes of what another user wrote are dropped.
136
+ const deletes = Y.createIdSet();
137
+ for (const [client, ranges] of ds.clients)
138
+ if (!owns(client))
139
+ for (const { clock, len } of ranges.getIds())
140
+ if (clock < heldClock(doc, client))
141
+ deletes.add(client, clock, len);
142
+ return { strip, deletes };
143
+ };
@@ -4,15 +4,24 @@ export type DocumentNamespace = {
4
4
  fetch(request: Request): Promise<Response>;
5
5
  };
6
6
  };
7
+ /**
8
+ * What a user may do in a document: `read`; `comment` (read, and comment:
9
+ * the room writes a thread's anchor); `suggest` (read, comment, and write
10
+ * suggestions, which a writer accepts or rejects); `write` (everything,
11
+ * deciding suggestions included).
12
+ */
13
+ export type DocumentAccessLevel = 'read' | 'comment' | 'suggest' | 'write';
7
14
  /**
8
15
  * What the host verified. `userId` owns every Yjs client id the socket
9
16
  * writes under; `replica` is the client's `doc.clientID` (read it from a
10
17
  * query parameter with {@link requestedReplica}; omitted, the socket's
11
- * first presence entry binds it); `readOnly` refuses every write.
18
+ * first presence entry binds it); `access` what the user may do (default
19
+ * `write`; `readOnly: true` is `access: 'read'`).
12
20
  */
13
21
  export type DocumentIdentity = {
14
22
  userId: string;
15
23
  replica?: number | null;
24
+ access?: DocumentAccessLevel;
16
25
  readOnly?: boolean;
17
26
  /**
18
27
  * When the credential expires, ms since the epoch (a token's `exp` ×
@@ -119,13 +119,15 @@ const authorized = async (request, documentId, authorize, refuse, options) => {
119
119
  return refuse(CLOSE.expired, 'expired');
120
120
  return identity;
121
121
  };
122
+ /** What `identity` may do: its `access`, else `read` for `readOnly`, else `write`. */
123
+ const accessOf = (identity) => identity.access ?? (identity.readOnly ? 'read' : 'write');
122
124
  /** A refusal as an HTTP status (a probe, a history request). */
123
125
  const statusOf = (code, reason) => new Response(reason, { status: PROBE_STATUS[code] ?? 403 });
124
126
  /** The verified identity as the headers the room trusts. */
125
127
  const identityHeaders = (identity) => {
126
128
  const headers = new Headers({
127
129
  [IDENTITY_HEADERS.user]: encodeURIComponent(identity.userId),
128
- [IDENTITY_HEADERS.access]: identity.readOnly ? 'read' : 'write'
130
+ [IDENTITY_HEADERS.access]: accessOf(identity)
129
131
  });
130
132
  const replica = identity.replica ?? null;
131
133
  if (replica !== null)
@@ -173,7 +175,7 @@ async function routeHistory(request, rooms, documentId, authorize, options) {
173
175
  const identity = await authorized(request, documentId, authorize, statusOf, options);
174
176
  if (identity instanceof Response)
175
177
  return identity;
176
- if ((op === 'restore' || op === 'undo') && identity.readOnly) {
178
+ if ((op === 'restore' || op === 'undo') && accessOf(identity) !== 'write') {
177
179
  return new Response('read-only', { status: 403 });
178
180
  }
179
181
  const headers = identityHeaders(identity);
@@ -247,7 +249,7 @@ async function routeComments(request, rooms, documentId, authorize, options) {
247
249
  const identity = await authorized(request, documentId, authorize, statusOf, options);
248
250
  if (identity instanceof Response)
249
251
  return identity;
250
- if (op === 'post' && identity.readOnly)
252
+ if (op === 'post' && accessOf(identity) === 'read')
251
253
  return new Response('read-only', { status: 403 });
252
254
  let body = null;
253
255
  if (op === 'post') {
@@ -43,7 +43,15 @@
43
43
  const placeholder = $derived(edytor.placeholderAt(id));
44
44
 
45
45
  /** An empty content's one text (`partsOf`: a content starts with a text). */
46
- const EMPTY: Segment = { kind: 'text', key: START, text: '', runs: [] };
46
+ const EMPTY: Segment = {
47
+ kind: 'text',
48
+ key: START,
49
+ text: '',
50
+ runs: [],
51
+ segOrd: 0,
52
+ start: 0,
53
+ end: 0
54
+ };
47
55
 
48
56
  /** Whether this view renders an atom of `part`'s kind (its record has a snippet). */
49
57
  const rendersAtom = (part: AtomPart) => Boolean(edytor.inlineBlocks.get(part.type)?.snippet);
@@ -1,5 +1,6 @@
1
- /** The children index order (`childrenIndex`): rank, then id. */
2
- const before = (a, b) => (a.rank === b.rank ? a.id < b.id : a.rank < b.rank);
1
+ import { bySlot } from './placement/display.js';
2
+ /** The children list's own order (`bySlot`): rank, then id. */
3
+ const before = (a, b) => bySlot(a, b) < 0;
3
4
  /**
4
5
  * Where an endpoint whose block `dead` vanished lands: a displayable stop next
5
6
  * to the slot `dead` (or its topmost dead ancestor) occupied. `dead === null`
@@ -44,6 +44,8 @@ export { ATTRIBUTION_ROOT };
44
44
  * it as bookkeeping.
45
45
  */
46
46
  export declare const ATTRIBUTION_ORIGIN: unique symbol;
47
+ export declare const ACTOR_PREFIX = "u/";
48
+ export declare const CLIENT_PREFIX = "c/";
47
49
  /** The stable application-level author identity (mirrors `DocumentActor`). */
48
50
  export type AttributionActor = {
49
51
  id: string;
@@ -113,6 +115,7 @@ type AttachOptions = {
113
115
  * attached with (`undefined`: a raw doc). The provider's `saved` reads it.
114
116
  */
115
117
  export declare const writerOf: (doc: EngineDoc) => string | undefined;
118
+ export declare const sanitizeProfile: (value: unknown) => ActorProfile;
116
119
  /** The display profile of `actor`: its name and color, those it has. */
117
120
  export declare const profileOf: (actor: AttributionActor) => ActorProfile;
118
121
  /**
@@ -9,8 +9,8 @@ export { ATTRIBUTION_ROOT };
9
9
  * it as bookkeeping.
10
10
  */
11
11
  export const ATTRIBUTION_ORIGIN = Symbol('edytor.attribution');
12
- const ACTOR_PREFIX = 'u/'; // actorId → profile records
13
- const CLIENT_PREFIX = 'c/'; // replica clientID → actorId records
12
+ export const ACTOR_PREFIX = 'u/'; // actorId → profile records
13
+ export const CLIENT_PREFIX = 'c/'; // replica clientID → actorId records
14
14
  /** The actor each attached document's replica writes for (its attribution controller's). */
15
15
  const localActors = new WeakMap();
16
16
  /**
@@ -25,7 +25,7 @@ export const writerOf = (doc) => {
25
25
  : undefined;
26
26
  return typeof bound === 'string' && bound.length > 0 ? bound : localActors.get(doc);
27
27
  };
28
- const sanitizeProfile = (value) => {
28
+ export const sanitizeProfile = (value) => {
29
29
  const v = (value ?? undefined);
30
30
  const out = {};
31
31
  if (typeof v?.name === 'string')
@@ -40,7 +40,7 @@
40
40
  import { isLiveIn } from '../placement/model.js';
41
41
  /** The capability of the facade over its reads. */
42
42
  export const docCapability = (c) => {
43
- const { roles, rendersContentOf, view, blockTypeOf, childrenIds, positionOf, ancestorsOf, isVoid, isIsland, islandOf, insideIsland, holdsLayout, insideItem, isRowKind, isCellKind, displayLength } = c;
43
+ const { roles, view, blockTypeOf, childrenIds, positionOf, ancestorsOf, isVoid, isIsland, islandOf, insideIsland, holdsLayout, insideItem, isRowKind, isCellKind, displayLength } = c;
44
44
  // ── structural capability: one answer in advance and at execution ──
45
45
  /**
46
46
  * May `ids` be placed under `parent` (`null` = the root)? Every id is
@@ -87,7 +87,7 @@ export const docCapability = (c) => {
87
87
  * A container: a block that shows only its children — no content of
88
88
  * its own, neither void nor an island (a list, a table row, a column).
89
89
  */
90
- const isContainer = (id) => !rendersContent(id) && !isVoid(id) && !isIsland(id);
90
+ const isContainer = (id) => roles.of(blockTypeOf(id) ?? '').container;
91
91
  /**
92
92
  * THE container rule: may a block of `kind` sit directly
93
93
  * under `parent` (`null` = the root)? A container whose default child is
@@ -109,15 +109,11 @@ export const docCapability = (c) => {
109
109
  const fits = (parent, kind) => parent === null || fitsIn(blockTypeOf(parent) ?? '', kind);
110
110
  /** `fits`, by the parent's kind (a block not written yet: `placeBeside`'s new item). */
111
111
  const fitsIn = (parentType, kind) => {
112
- if (!roles.container(parentType))
112
+ const { container, child: item } = roles.of(parentType);
113
+ if (!container || item === defaultChild(null) || kind === item)
113
114
  return true;
114
- const item = roles.defaultChild(parentType);
115
- if (item === defaultChild(null) || kind === item)
116
- return true;
117
- return (kind !== undefined &&
118
- roles.container(kind) &&
119
- roles.defaultChild(kind) === item &&
120
- rendersContentOf(item));
115
+ const k = kind === undefined ? undefined : roles.of(kind);
116
+ return k?.container === true && k.child === item && roles.of(item).rendersContent;
121
117
  };
122
118
  /**
123
119
  * `kind`, or — where a plain block (the document's default kind, or
@@ -135,7 +131,7 @@ export const docCapability = (c) => {
135
131
  if (fits(parent, kind) || (kind !== undefined && kind !== defaultChild(null)))
136
132
  return kind;
137
133
  const item = defaultChild(parent);
138
- return rendersContentOf(item) ? item : kind;
134
+ return roles.of(item).rendersContent ? item : kind;
139
135
  };
140
136
  /** A container that goes once it loses every child: it holds no text of its own (a peer's retype can leave some, hidden: it renders none). */
141
137
  const emptiable = (id) => isContainer(id) && displayLength(id) === 0;
@@ -184,9 +180,9 @@ export const docCapability = (c) => {
184
180
  return islandFrom === islandOf(intoId, v) && !isIsland(intoId);
185
181
  };
186
182
  /** `id`'s shown kind renders its content. */
187
- const rendersContent = (id) => rendersContentOf(blockTypeOf(id) ?? '');
183
+ const rendersContent = (id) => roles.of(blockTypeOf(id) ?? '').rendersContent;
188
184
  /** The adopted default child type under `parent` (`null` = the root). */
189
- const defaultChild = (parent) => roles.defaultChild(parent === null ? null : (blockTypeOf(parent) ?? null));
185
+ const defaultChild = (parent) => roles.of(parent === null ? null : (blockTypeOf(parent) ?? null)).child;
190
186
  /**
191
187
  * The kind a new block copies from `id` (a split tail, a flow's tail,
192
188
  * a duplicate and each of its descendants). A type a peer's retype is
@@ -20,7 +20,7 @@ export type FunnelContext = DocBase & DocReads & {
20
20
  /** The write funnel of one facade. */
21
21
  export declare const writeFunnel: (c: FunnelContext) => {
22
22
  write: <R>(fn: () => R) => R;
23
- apply: (p: Prepared) => OpResult;
23
+ apply: (p: Prepared, quiet?: boolean) => OpResult;
24
24
  /** Disposal is terminal for writes. */
25
25
  dispose: () => void;
26
26
  disposed: () => boolean;
@@ -146,8 +146,10 @@ export const writeFunnel = (c) => {
146
146
  * and fold the result from what that transaction did. A refusal writes
147
147
  * nothing. A plan is valid only at the version it was prepared against,
148
148
  * in the same synchronous turn: applying it anywhere else throws.
149
+ * `quiet`: it stamps no attribution (a suggestion's writer: its content
150
+ * is not the block's until accepted).
149
151
  */
150
- const apply = (p) => {
152
+ const apply = (p, quiet) => {
151
153
  if (!('writes' in p))
152
154
  return write(() => p);
153
155
  if (p.version !== runsView.version()) {
@@ -166,7 +168,8 @@ export const writeFunnel = (c) => {
166
168
  throw error;
167
169
  }
168
170
  const folded = frame.end();
169
- close(f, folded);
171
+ if (!quiet)
172
+ close(f, folded);
170
173
  return folded.wrote ? { status: 'applied', ids: p.ids } : NOOP;
171
174
  }));
172
175
  };
@@ -25,7 +25,7 @@
25
25
  * ```
26
26
  */
27
27
  import type { EngineDoc } from '../engine-api.js';
28
- export declare const SCHEMA_VERSION: 10;
28
+ export declare const SCHEMA_VERSION: 11;
29
29
  export declare const SCHEMA_NAME: "edytor-doc";
30
30
  export declare const META_KEY: "meta";
31
31
  /**
@@ -10,6 +10,12 @@ export declare class StackItem {
10
10
  deletes: IdSetOf;
11
11
  /** Per-view metadata (`session/history.ts`: the selections around the step). */
12
12
  readonly meta: Map<string, unknown>;
13
+ /**
14
+ * The client it was written under when that is not the replica's own (a
15
+ * suggestion's writer: its transaction's `writer`): it replays under it,
16
+ * and its records release with the history's.
17
+ */
18
+ writer?: number;
13
19
  constructor(inserts: IdSetOf, deletes: IdSetOf);
14
20
  }
15
21
  /** A step taken into a stack: added (a new step) or updated (the open one took a transaction). */
@@ -15,7 +15,7 @@
15
15
  import { getUnixTime } from 'lib0-v14/time';
16
16
  import { openSteps, ownTextIds } from '../text/model.js';
17
17
  import { readHorizon } from '../purge.js';
18
- import { BLOCK_NODE, CONTENT_NODE, DATA_LEAF_PREFIX, DEL_PREFIX, DOC_DATA_ROOT, HORIZON_ROOT, INLINE_NODE, LAST_CHANGED_ATTR, RELEASE_ROOT, TEXT_DELETES_ROOT, TYPE, WITHDRAW_PREFIX } from '../schema.js';
18
+ import { BLOCK_NODE, CONTENT_NODE, DATA_LEAF_PREFIX, DEL_PREFIX, DOC_DATA_ROOT, HORIZON_ROOT, INLINE_NODE, LAST_CHANGED_ATTR, RELEASE_ROOT, SUGGEST_ROOT, TEXT_DELETES_ROOT, TYPE, WITHDRAW_PREFIX } from '../schema.js';
19
19
  import { changedUnder, collectable, collectNow, collects, dropSearchMarkers, engineOps, walkIdSet } from '../structs.js';
20
20
  import { isInitialized } from './gate.js';
21
21
  import { DocumentDestroyedError } from './funnel.js';
@@ -30,6 +30,12 @@ export class StackItem {
30
30
  deletes;
31
31
  /** Per-view metadata (`session/history.ts`: the selections around the step). */
32
32
  meta = new Map();
33
+ /**
34
+ * The client it was written under when that is not the replica's own (a
35
+ * suggestion's writer: its transaction's `writer`): it replays under it,
36
+ * and its records release with the history's.
37
+ */
38
+ writer;
33
39
  constructor(inserts, deletes) {
34
40
  this.inserts = inserts;
35
41
  this.deletes = deletes;
@@ -235,6 +241,8 @@ export class UndoManager {
235
241
  ops.keep(item);
236
242
  });
237
243
  const step = stack.at(-1);
244
+ if (tr.writer)
245
+ step.writer = tr.writer;
238
246
  // A streamless block's own text is shared by every replica that
239
247
  // typed into it first: no step captures it, so undoing the first
240
248
  // typing removes the typing and keeps the text (and nonce).
@@ -264,60 +272,71 @@ export class UndoManager {
264
272
  const ops = this.#ops;
265
273
  const { records, withdraw, repair } = this.#rules;
266
274
  const deletedByHistory = (id) => [this.undoStack, this.redoStack].some((s) => s.some((step) => step.deletes.has(id.client, id.clock)));
267
- this.doc.transact((transaction) => {
268
- const tr = transaction;
269
- while (stack.length > 0 && this.currStackItem === null) {
270
- const step = stack.pop();
271
- const restore = new Set();
272
- const remove = [];
273
- const held = [];
274
- let changed = false;
275
- ops.iterate(tr, step.inserts, (struct) => {
276
- if (!ops.isItem(struct))
277
- return;
278
- const item = ops.latest(tr, struct);
279
- // An atom the step inserted is held whole: its attrs stay with it.
280
- const atom = item.parent?._item;
281
- if (atom?.id &&
282
- isUnit(atom) &&
283
- step.inserts.has(atom.id.client, atom.id.clock))
284
- return;
285
- if (!item.deleted && this.#inScope(item))
286
- remove.push(item);
287
- });
288
- ops.iterate(tr, step.deletes, (struct) => {
289
- if (!ops.isItem(struct) || !this.#inScope(struct))
290
- return;
291
- // Created and deleted within the step: nothing to bring back.
292
- if (step.inserts.has(struct.id.client, struct.id.clock))
293
- return;
294
- // Text a record holds stays hidden: the record decides (removed with its node).
295
- if ((struct.holds ?? 0) > 0)
296
- return;
297
- restore.add(struct);
298
- });
299
- for (const item of restore)
300
- changed = ops.redo(tr, item, restore, step.inserts, deletedByHistory) !== null || changed;
301
- // Children before parents: more is known when an item is judged.
302
- for (let i = remove.length - 1; i >= 0; i--) {
303
- const item = remove[i];
304
- // Text the step wrote is held, never removed: a redo shows it again where it was.
305
- if (isUnit(item))
306
- held.push(item.id.client, item.id.clock, item.length);
307
- else if (!withdraw(item, step))
308
- ops.remove(tr, item);
309
- changed = true;
275
+ // A step written under another client replays under it, and so does its replay's step.
276
+ const local = this.doc.clientID;
277
+ const writer = stack.at(-1)?.writer;
278
+ this.doc.clientID = writer ?? local;
279
+ try {
280
+ this.doc.transact((transaction) => {
281
+ const tr = transaction;
282
+ tr.writer = writer;
283
+ while (stack.length > 0 && this.currStackItem === null) {
284
+ const step = stack.pop();
285
+ const restore = new Set();
286
+ const remove = [];
287
+ const held = [];
288
+ let changed = false;
289
+ ops.iterate(tr, step.inserts, (struct) => {
290
+ if (!ops.isItem(struct))
291
+ return;
292
+ const item = ops.latest(tr, struct);
293
+ // An atom the step inserted is held whole: its attrs stay with it.
294
+ const atom = item.parent?._item;
295
+ if (atom?.id &&
296
+ isUnit(atom) &&
297
+ step.inserts.has(atom.id.client, atom.id.clock))
298
+ return;
299
+ if (!item.deleted && this.#inScope(item))
300
+ remove.push(item);
301
+ });
302
+ ops.iterate(tr, step.deletes, (struct) => {
303
+ if (!ops.isItem(struct) || !this.#inScope(struct))
304
+ return;
305
+ // Created and deleted within the step: nothing to bring back.
306
+ if (step.inserts.has(struct.id.client, struct.id.clock))
307
+ return;
308
+ // Text a record holds stays hidden: the record decides (removed with its node).
309
+ if ((struct.holds ?? 0) > 0)
310
+ return;
311
+ restore.add(struct);
312
+ });
313
+ for (const item of restore)
314
+ changed =
315
+ ops.redo(tr, item, restore, step.inserts, deletedByHistory) !== null || changed;
316
+ // Children before parents: more is known when an item is judged.
317
+ for (let i = remove.length - 1; i >= 0; i--) {
318
+ const item = remove[i];
319
+ // Text the step wrote is held, never removed: a redo shows it again where it was.
320
+ if (isUnit(item))
321
+ held.push(item.id.client, item.id.clock, item.length);
322
+ else if (!withdraw(item, step))
323
+ ops.remove(tr, item);
324
+ changed = true;
325
+ }
326
+ records.hold(held);
327
+ this.currStackItem = changed ? step : null;
328
+ if (changed)
329
+ repair(step);
310
330
  }
311
- records.hold(held);
312
- this.currStackItem = changed ? step : null;
313
- if (changed)
314
- repair(step);
315
- }
316
- tr.changed.forEach((keys, node) => {
317
- if (keys.has(null))
318
- dropSearchMarkers(node);
319
- });
320
- }, this);
331
+ tr.changed.forEach((keys, node) => {
332
+ if (keys.has(null))
333
+ dropSearchMarkers(node);
334
+ });
335
+ }, this);
336
+ }
337
+ finally {
338
+ this.doc.clientID = local;
339
+ }
321
340
  const step = this.currStackItem;
322
341
  this.currStackItem = null;
323
342
  return step;
@@ -364,7 +383,9 @@ export class UndoManager {
364
383
  const own = ops.idSet();
365
384
  for (const step of dropped)
366
385
  walkIdSet(Y, doc, step.inserts, (st, from, to) => {
367
- if (st.parent === records && !st.deleted && st.id.client === doc.clientID)
386
+ if (st.parent === records &&
387
+ !st.deleted &&
388
+ (st.id.client === doc.clientID || st.id.client === step.writer))
368
389
  own.add(st.id.client, from, to - from);
369
390
  });
370
391
  const held = ops.idSet();
@@ -464,7 +485,7 @@ export const historyRules = (Y, doc) => {
464
485
  return {
465
486
  Y,
466
487
  doc,
467
- scope: [registryOf(doc), records, doc.get(DOC_DATA_ROOT)],
488
+ scope: [registryOf(doc), records, doc.get(DOC_DATA_ROOT), doc.get(SUGGEST_ROOT)],
468
489
  records,
469
490
  withdraw: withdrawOnUndo(doc),
470
491
  repair: repairAttrs
@@ -12,8 +12,8 @@ export const layoutOps = (c) => {
12
12
  /** The layout kind a new layout takes: `kind` when it is one, else the only one the roles declare. */
13
13
  const layoutKind = (kind) => {
14
14
  if (kind !== undefined)
15
- return roles.layout(kind) === undefined ? undefined : kind;
16
- const kinds = [...roles.layoutKinds()];
15
+ return roles.of(kind).layout === undefined ? undefined : kind;
16
+ const kinds = roles.declared().layouts;
17
17
  return kinds.length === 1 ? kinds[0] : undefined;
18
18
  };
19
19
  /** A fresh empty block of `type`. */
@@ -90,7 +90,7 @@ export const layoutOps = (c) => {
90
90
  }
91
91
  }
92
92
  const wrap = layout === null ? layoutKind(kind && ref(kind)) : blockTypeOf(layout);
93
- const item = wrap === undefined ? undefined : roles.layout(wrap);
93
+ const item = wrap === undefined ? undefined : roles.of(wrap).layout;
94
94
  if (wrap === undefined || item === undefined)
95
95
  return REFUSED;
96
96
  if (moved.some((id) => !fitsIn(item, blockTypeOf(id))))
@@ -145,14 +145,14 @@ export const layoutOps = (c) => {
145
145
  if (blocks.some((id) => isLayoutItem(id) || holdsLayout(id)))
146
146
  return REFUSED;
147
147
  const wrap = layoutKind(kind && ref(kind));
148
- const item = wrap === undefined ? undefined : roles.layout(wrap);
148
+ const item = wrap === undefined ? undefined : roles.of(wrap).layout;
149
149
  if (wrap === undefined || item === undefined || !fits(parent, wrap))
150
150
  return REFUSED;
151
151
  if (parent !== null && (insideItem(parent, v) || isIsland(parent) || insideIsland(parent, v)))
152
152
  return REFUSED;
153
153
  if (blocks.some((id) => !fitsIn(item, blockTypeOf(id))))
154
154
  return REFUSED;
155
- const fill = roles.defaultChild(item);
155
+ const fill = roles.of(item).child;
156
156
  if (columns > blocks.length && !fitsIn(item, fill))
157
157
  return REFUSED;
158
158
  const ordered = blocks.toSorted((a, b) => positionOf(a).index - positionOf(b).index);
@@ -11,7 +11,7 @@ import { collides } from '../placement/model.js';
11
11
  import { ref, refused, sanitizeItem } from './plan.js';
12
12
  /** The meta ops of one facade, prepared. */
13
13
  export const metaOps = (c) => {
14
- const { doc, roles, roleOf, dataNode, view, blockTypeOf, blockDataOf, childrenIds, positionOf, is, isVoid, displayLength, live, contentTarget, REFUSED, plan, attr, leavingIsland, settle, remove, atomOf, writeContent } = c;
14
+ const { doc, roles, roleOf, dataNode, view, blockTypeOf, blockDataOf, childrenIds, positionOf, isLines, isVoid, displayLength, live, contentTarget, REFUSED, plan, attr, leavingIsland, settle, remove, atomOf, writeContent } = c;
15
15
  /**
16
16
  * The data step of `patches` (sanitized) on a block, one of its atoms or
17
17
  * the document (`crdt/data.ts`): none when it changes nothing, `null`
@@ -80,18 +80,17 @@ export const metaOps = (c) => {
80
80
  const moved = kids.get(id) ?? [];
81
81
  // A code block retyped to an ordinary kind keeps its lines as that kind's children
82
82
  // (`leavingIsland`: never a kind that hides their text).
83
- const lined = is(id, (t) => roles.line(t) !== undefined);
84
- if (lined && !roles.island(type) && !roles.childless(type)) {
85
- const to = roles.defaultChild(type);
83
+ const to = roles.of(type);
84
+ if (isLines(id) && !to.island && !to.childless) {
86
85
  return [
87
86
  ...steps,
88
87
  ...moved.flatMap((k) => {
89
- const kind = leavingIsland(id, blockTypeOf(k.id), to);
88
+ const kind = leavingIsland(id, blockTypeOf(k.id), to.child);
90
89
  return kind === undefined ? [] : attr(k.id, TYPE, kind);
91
90
  })
92
91
  ];
93
92
  }
94
- if (!roles.childless(type) || moved.length === 0 || pos === null)
93
+ if (!to.childless || moved.length === 0 || pos === null)
95
94
  return steps;
96
95
  // The children keep their home slots: the read shows them in the void's
97
96
  // slot (`conc.void-children`), settled for its parent.
@@ -159,7 +158,7 @@ export const metaOps = (c) => {
159
158
  if (!live(id) || (content !== undefined && !contentTarget(id)))
160
159
  return REFUSED;
161
160
  const type = value.type === undefined ? undefined : ref(value.type);
162
- const toVoid = type === undefined ? isVoid(id) : roles.childless(type);
161
+ const toVoid = type === undefined ? isVoid(id) : roles.of(type).childless;
163
162
  if (children?.length && toVoid)
164
163
  return REFUSED;
165
164
  if (children !== undefined && collides(doc, children))