edytor 4.0.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/block/block.svelte.js +6 -21
- package/dist/block/inlineBlock.svelte.js +4 -6
- package/dist/clipboard/moveText.js +6 -8
- package/dist/cloudflare/DocumentRoom.d.ts +10 -3
- package/dist/cloudflare/index.d.ts +1 -1
- package/dist/cloudflare/room/access.d.ts +8 -3
- package/dist/cloudflare/room/access.js +44 -20
- package/dist/cloudflare/room/admission.js +28 -5
- package/dist/cloudflare/room/comments.js +28 -1
- package/dist/cloudflare/room/context.d.ts +106 -109
- package/dist/cloudflare/room/context.js +10 -3
- package/dist/cloudflare/room/forged.d.ts +14 -0
- package/dist/cloudflare/room/forged.js +30 -2
- package/dist/cloudflare/room/frames.d.ts +4 -0
- package/dist/cloudflare/room/frames.js +5 -1
- package/dist/cloudflare/room/purge.js +2 -1
- package/dist/cloudflare/room/replicas.d.ts +4 -2
- package/dist/cloudflare/room/replicas.js +8 -3
- package/dist/cloudflare/room/suggest.d.ts +7 -0
- package/dist/cloudflare/room/suggest.js +143 -0
- package/dist/cloudflare/routeDocumentSocket.d.ts +10 -1
- package/dist/cloudflare/routeDocumentSocket.js +5 -3
- package/dist/components/Content.svelte +9 -1
- package/dist/crdt/anchors.js +3 -2
- package/dist/crdt/attribution/attribution.d.ts +3 -0
- package/dist/crdt/attribution/attribution.js +3 -3
- package/dist/crdt/doc/funnel.d.ts +1 -1
- package/dist/crdt/doc/funnel.js +5 -2
- package/dist/crdt/doc/gate.d.ts +1 -1
- package/dist/crdt/doc/history.d.ts +6 -0
- package/dist/crdt/doc/history.js +79 -59
- package/dist/crdt/doc/ops.d.ts +81 -0
- package/dist/crdt/doc/ops.js +1898 -0
- package/dist/crdt/doc/reads.d.ts +2 -2
- package/dist/crdt/doc/reads.js +18 -28
- package/dist/crdt/doc/suggest.d.ts +167 -0
- package/dist/crdt/doc/suggest.js +579 -0
- package/dist/crdt/document.d.ts +10 -1
- package/dist/crdt/document.js +19 -11
- package/dist/crdt/edytor-doc.d.ts +294 -272
- package/dist/crdt/edytor-doc.js +123 -108
- package/dist/crdt/engine-api.d.ts +8 -2
- package/dist/crdt/engine.d.ts +2 -1
- package/dist/crdt/engine.js +3 -1
- package/dist/crdt/flow.d.ts +5 -47
- package/dist/crdt/flow.js +3 -269
- package/dist/crdt/index.d.ts +102 -108
- package/dist/crdt/index.js +3 -0
- package/dist/crdt/operations.d.ts +13 -4
- package/dist/crdt/parts.d.ts +43 -0
- package/dist/crdt/parts.js +44 -0
- package/dist/crdt/placement/display.d.ts +1 -1
- package/dist/crdt/placement/display.js +3 -3
- package/dist/crdt/placement/keys.d.ts +5 -0
- package/dist/crdt/placement/keys.js +5 -0
- package/dist/crdt/placement/resolve.d.ts +1 -1
- package/dist/crdt/placement/resolve.js +2 -1
- package/dist/crdt/protocol.d.ts +1 -1
- package/dist/crdt/protocol.js +1 -1
- package/dist/crdt/protocols/auth.d.ts +11 -1
- package/dist/crdt/protocols/auth.js +17 -1
- package/dist/crdt/protocols/awareness.js +46 -81
- package/dist/crdt/protocols/comments.d.ts +16 -1
- package/dist/crdt/protocols/comments.js +9 -1
- package/dist/crdt/providers/index.d.ts +12 -22
- package/dist/crdt/providers/index.js +3 -1
- package/dist/crdt/providers/indexeddb.d.ts +6 -10
- package/dist/crdt/providers/indexeddb.js +10 -16
- package/dist/crdt/providers/room.d.ts +11 -0
- package/dist/crdt/providers/room.js +26 -12
- package/dist/crdt/providers/websocket.d.ts +8 -12
- package/dist/crdt/providers/websocket.js +17 -19
- package/dist/crdt/purge.d.ts +7 -1
- package/dist/crdt/purge.js +32 -2
- package/dist/crdt/rangeDelete.d.ts +2 -47
- package/dist/crdt/rangeDelete.js +1 -169
- package/dist/crdt/restore.d.ts +2 -1
- package/dist/crdt/restore.js +9 -0
- package/dist/crdt/schema.d.ts +9 -1
- package/dist/crdt/schema.js +13 -3
- package/dist/crdt/structs.d.ts +2 -0
- package/dist/crdt/structs.js +2 -0
- package/dist/crdt/text/index/build.d.ts +125 -0
- package/dist/crdt/text/index/build.js +2895 -0
- package/dist/crdt/text/index/shared.d.ts +2 -6
- package/dist/crdt/text/index/shared.js +19 -19
- package/dist/crdt/text/model.d.ts +10 -0
- package/dist/crdt/text/model.js +10 -4
- package/dist/crdt/text/runs.d.ts +43 -42
- package/dist/crdt/text/runs.js +2 -165
- package/dist/crdt/vendor/yjs/UPSTREAM.md +83 -0
- package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
- package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +6 -3
- package/dist/crdt/vendor/yjs/dts/ynode.d.ts +13 -2
- package/dist/crdt/vendor/yjs/src/index.js +1 -1
- package/dist/crdt/vendor/yjs/src/structs/Item.js +44 -5
- package/dist/crdt/vendor/yjs/src/ynode.js +29 -7
- package/dist/edytor.svelte.d.ts +1 -1
- package/dist/edytor.svelte.js +100 -75
- package/dist/events/beforeInputCommands.d.ts +2 -2
- package/dist/events/domTextOffset.d.ts +2 -0
- package/dist/events/domTextOffset.js +8 -0
- package/dist/events/nativeInteractiveControl.d.ts +11 -1
- package/dist/events/nativeInteractiveControl.js +41 -1
- package/dist/events/onBeforeInput.js +3 -3
- package/dist/events/onFocus.js +9 -11
- package/dist/export/forms.d.ts +75 -75
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -1
- package/dist/labels.d.ts +8 -0
- package/dist/labels.js +6 -1
- package/dist/plugins/comments/CommentsController.svelte.d.ts +5 -1
- package/dist/plugins/comments/CommentsController.svelte.js +14 -4
- package/dist/plugins/find/search.js +2 -2
- package/dist/plugins/index.d.ts +1 -0
- package/dist/plugins/index.js +1 -0
- package/dist/plugins/richtext/RichTextPlugin.svelte +63 -61
- package/dist/plugins/richtext/richTextOperations.js +26 -32
- package/dist/plugins/suggestions/SuggestionBars.svelte +40 -23
- package/dist/plugins/suggestions/SuggestionBars.svelte.d.ts +3 -1
- package/dist/plugins/suggestions/SuggestionRuns.svelte +77 -0
- package/dist/plugins/suggestions/SuggestionRuns.svelte.d.ts +9 -0
- package/dist/plugins/suggestions/shared.svelte.d.ts +58 -0
- package/dist/plugins/suggestions/shared.svelte.js +421 -0
- package/dist/plugins/suggestions/suggestionsPlugin.d.ts +15 -4
- package/dist/selection/domSelection.d.ts +2 -0
- package/dist/selection/domSelection.js +6 -2
- package/dist/selection/pointer.svelte.js +75 -48
- package/dist/selection/replaceSelection.js +2 -6
- package/dist/selection/selection.svelte.d.ts +3 -3
- package/dist/selection/selection.svelte.js +39 -32
- package/dist/selection/selection.utils.d.ts +14 -0
- package/dist/selection/selection.utils.js +33 -11
- package/dist/session/commands.js +21 -11
- package/dist/session/composition.svelte.d.ts +5 -3
- package/dist/session/composition.svelte.js +21 -17
- package/dist/session/handles.d.ts +4 -17
- package/dist/session/handles.js +14 -44
- package/dist/session/navigation.js +6 -9
- package/dist/session/selection.d.ts +11 -5
- package/dist/session/selection.js +14 -48
- package/dist/session/suggestions.svelte.d.ts +38 -9
- package/dist/session/suggestions.svelte.js +37 -18
- package/dist/session/virtual.d.ts +1 -1
- package/dist/surface/attributes.d.ts +5 -0
- package/dist/surface/attributes.js +2 -2
- package/dist/surface/cells.d.ts +2 -26
- package/dist/surface/cells.js +2 -33
- package/dist/surface/observer.svelte.d.ts +6 -1
- package/dist/surface/observer.svelte.js +143 -111
- package/dist/surface/pin.svelte.d.ts +9 -9
- package/dist/surface/pin.svelte.js +15 -22
- package/dist/surface/projector.svelte.js +10 -16
- package/dist/text/text.svelte.js +14 -19
- package/dist/themes/notion.css +29 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +7 -0
- package/package.json +3 -1
- package/dist/crdt/doc/capability.d.ts +0 -56
- package/dist/crdt/doc/capability.js +0 -211
- package/dist/crdt/doc/content.d.ts +0 -16
- package/dist/crdt/doc/content.js +0 -77
- package/dist/crdt/doc/delete.d.ts +0 -15
- package/dist/crdt/doc/delete.js +0 -84
- package/dist/crdt/doc/layout.d.ts +0 -9
- package/dist/crdt/doc/layout.js +0 -178
- package/dist/crdt/doc/meta.d.ts +0 -19
- package/dist/crdt/doc/meta.js +0 -199
- package/dist/crdt/doc/moves.d.ts +0 -21
- package/dist/crdt/doc/moves.js +0 -274
- package/dist/crdt/doc/split-merge.d.ts +0 -10
- package/dist/crdt/doc/split-merge.js +0 -179
- package/dist/crdt/doc/steps.d.ts +0 -65
- package/dist/crdt/doc/steps.js +0 -311
- package/dist/crdt/text/index/cache.d.ts +0 -16
- package/dist/crdt/text/index/cache.js +0 -175
- package/dist/crdt/text/index/claims.d.ts +0 -18
- package/dist/crdt/text/index/claims.js +0 -225
- package/dist/crdt/text/index/fold.d.ts +0 -21
- package/dist/crdt/text/index/fold.js +0 -656
- package/dist/crdt/text/index/layout.d.ts +0 -24
- package/dist/crdt/text/index/layout.js +0 -271
- package/dist/crdt/text/index/placement.d.ts +0 -47
- package/dist/crdt/text/index/placement.js +0 -741
- package/dist/crdt/text/index/records.d.ts +0 -13
- package/dist/crdt/text/index/records.js +0 -139
- package/dist/crdt/text/index/report.d.ts +0 -18
- package/dist/crdt/text/index/report.js +0 -337
- package/dist/crdt/text/index/state.d.ts +0 -197
- package/dist/crdt/text/index/state.js +0 -116
- package/dist/crdt/text/index/streams.d.ts +0 -25
- package/dist/crdt/text/index/streams.js +0 -248
|
@@ -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); `
|
|
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
|
|
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
|
|
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
|
|
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 = {
|
|
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);
|
package/dist/crdt/anchors.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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')
|
|
@@ -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;
|
package/dist/crdt/doc/funnel.js
CHANGED
|
@@ -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
|
-
|
|
171
|
+
if (!quiet)
|
|
172
|
+
close(f, folded);
|
|
170
173
|
return folded.wrote ? { status: 'applied', ids: p.ids } : NOOP;
|
|
171
174
|
}));
|
|
172
175
|
};
|
package/dist/crdt/doc/gate.d.ts
CHANGED
|
@@ -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). */
|
package/dist/crdt/doc/history.js
CHANGED
|
@@ -15,11 +15,12 @@
|
|
|
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';
|
|
22
22
|
import { registryOf, withdrawOnUndo } from '../placement/model.js';
|
|
23
|
+
import { getOr } from '../../utils.js';
|
|
23
24
|
/** The undo steps a document's history keeps by default: older ones are released. */
|
|
24
25
|
export const DEFAULT_HISTORY_LIMIT = 200;
|
|
25
26
|
/** The origin of the transaction that releases a dropped history step's content. */
|
|
@@ -30,6 +31,12 @@ export class StackItem {
|
|
|
30
31
|
deletes;
|
|
31
32
|
/** Per-view metadata (`session/history.ts`: the selections around the step). */
|
|
32
33
|
meta = new Map();
|
|
34
|
+
/**
|
|
35
|
+
* The client it was written under when that is not the replica's own (a
|
|
36
|
+
* suggestion's writer: its transaction's `writer`): it replays under it,
|
|
37
|
+
* and its records release with the history's.
|
|
38
|
+
*/
|
|
39
|
+
writer;
|
|
33
40
|
constructor(inserts, deletes) {
|
|
34
41
|
this.inserts = inserts;
|
|
35
42
|
this.deletes = deletes;
|
|
@@ -235,6 +242,8 @@ export class UndoManager {
|
|
|
235
242
|
ops.keep(item);
|
|
236
243
|
});
|
|
237
244
|
const step = stack.at(-1);
|
|
245
|
+
if (tr.writer)
|
|
246
|
+
step.writer = tr.writer;
|
|
238
247
|
// A streamless block's own text is shared by every replica that
|
|
239
248
|
// typed into it first: no step captures it, so undoing the first
|
|
240
249
|
// typing removes the typing and keeps the text (and nonce).
|
|
@@ -264,60 +273,71 @@ export class UndoManager {
|
|
|
264
273
|
const ops = this.#ops;
|
|
265
274
|
const { records, withdraw, repair } = this.#rules;
|
|
266
275
|
const deletedByHistory = (id) => [this.undoStack, this.redoStack].some((s) => s.some((step) => step.deletes.has(id.client, id.clock)));
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
276
|
+
// A step written under another client replays under it, and so does its replay's step.
|
|
277
|
+
const local = this.doc.clientID;
|
|
278
|
+
const writer = stack.at(-1)?.writer;
|
|
279
|
+
this.doc.clientID = writer ?? local;
|
|
280
|
+
try {
|
|
281
|
+
this.doc.transact((transaction) => {
|
|
282
|
+
const tr = transaction;
|
|
283
|
+
tr.writer = writer;
|
|
284
|
+
while (stack.length > 0 && this.currStackItem === null) {
|
|
285
|
+
const step = stack.pop();
|
|
286
|
+
const restore = new Set();
|
|
287
|
+
const remove = [];
|
|
288
|
+
const held = [];
|
|
289
|
+
let changed = false;
|
|
290
|
+
ops.iterate(tr, step.inserts, (struct) => {
|
|
291
|
+
if (!ops.isItem(struct))
|
|
292
|
+
return;
|
|
293
|
+
const item = ops.latest(tr, struct);
|
|
294
|
+
// An atom the step inserted is held whole: its attrs stay with it.
|
|
295
|
+
const atom = item.parent?._item;
|
|
296
|
+
if (atom?.id &&
|
|
297
|
+
isUnit(atom) &&
|
|
298
|
+
step.inserts.has(atom.id.client, atom.id.clock))
|
|
299
|
+
return;
|
|
300
|
+
if (!item.deleted && this.#inScope(item))
|
|
301
|
+
remove.push(item);
|
|
302
|
+
});
|
|
303
|
+
ops.iterate(tr, step.deletes, (struct) => {
|
|
304
|
+
if (!ops.isItem(struct) || !this.#inScope(struct))
|
|
305
|
+
return;
|
|
306
|
+
// Created and deleted within the step: nothing to bring back.
|
|
307
|
+
if (step.inserts.has(struct.id.client, struct.id.clock))
|
|
308
|
+
return;
|
|
309
|
+
// Text a record holds stays hidden: the record decides (removed with its node).
|
|
310
|
+
if ((struct.holds ?? 0) > 0)
|
|
311
|
+
return;
|
|
312
|
+
restore.add(struct);
|
|
313
|
+
});
|
|
314
|
+
for (const item of restore)
|
|
315
|
+
changed =
|
|
316
|
+
ops.redo(tr, item, restore, step.inserts, deletedByHistory) !== null || changed;
|
|
317
|
+
// Children before parents: more is known when an item is judged.
|
|
318
|
+
for (let i = remove.length - 1; i >= 0; i--) {
|
|
319
|
+
const item = remove[i];
|
|
320
|
+
// Text the step wrote is held, never removed: a redo shows it again where it was.
|
|
321
|
+
if (isUnit(item))
|
|
322
|
+
held.push(item.id.client, item.id.clock, item.length);
|
|
323
|
+
else if (!withdraw(item, step))
|
|
324
|
+
ops.remove(tr, item);
|
|
325
|
+
changed = true;
|
|
326
|
+
}
|
|
327
|
+
records.hold(held);
|
|
328
|
+
this.currStackItem = changed ? step : null;
|
|
329
|
+
if (changed)
|
|
330
|
+
repair(step);
|
|
310
331
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}, this);
|
|
332
|
+
tr.changed.forEach((keys, node) => {
|
|
333
|
+
if (keys.has(null))
|
|
334
|
+
dropSearchMarkers(node);
|
|
335
|
+
});
|
|
336
|
+
}, this);
|
|
337
|
+
}
|
|
338
|
+
finally {
|
|
339
|
+
this.doc.clientID = local;
|
|
340
|
+
}
|
|
321
341
|
const step = this.currStackItem;
|
|
322
342
|
this.currStackItem = null;
|
|
323
343
|
return step;
|
|
@@ -364,7 +384,9 @@ export class UndoManager {
|
|
|
364
384
|
const own = ops.idSet();
|
|
365
385
|
for (const step of dropped)
|
|
366
386
|
walkIdSet(Y, doc, step.inserts, (st, from, to) => {
|
|
367
|
-
if (st.parent === records &&
|
|
387
|
+
if (st.parent === records &&
|
|
388
|
+
!st.deleted &&
|
|
389
|
+
(st.id.client === doc.clientID || st.id.client === step.writer))
|
|
368
390
|
own.add(st.id.client, from, to - from);
|
|
369
391
|
});
|
|
370
392
|
const held = ops.idSet();
|
|
@@ -444,9 +466,7 @@ export const historyRules = (Y, doc) => {
|
|
|
444
466
|
if (step.inserts.has(s.id.client, s.id.clock))
|
|
445
467
|
return;
|
|
446
468
|
const values = s.content;
|
|
447
|
-
|
|
448
|
-
if (attrs === undefined)
|
|
449
|
-
lost.set(node, (attrs = new Map()));
|
|
469
|
+
const attrs = getOr(lost, node, () => new Map());
|
|
450
470
|
attrs.set(key, values.getContent().at(-1));
|
|
451
471
|
});
|
|
452
472
|
for (const [node, attrs] of lost) {
|
|
@@ -464,7 +484,7 @@ export const historyRules = (Y, doc) => {
|
|
|
464
484
|
return {
|
|
465
485
|
Y,
|
|
466
486
|
doc,
|
|
467
|
-
scope: [registryOf(doc), records, doc.get(DOC_DATA_ROOT)],
|
|
487
|
+
scope: [registryOf(doc), records, doc.get(DOC_DATA_ROOT), doc.get(SUGGEST_ROOT)],
|
|
468
488
|
records,
|
|
469
489
|
withdraw: withdrawOnUndo(doc),
|
|
470
490
|
repair: repairAttrs
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type JSONBlock, type JSONInlineBlock, type JSONText } from '../../utils/json.js';
|
|
2
|
+
import { type DataPatch } from '../data.js';
|
|
3
|
+
import { type BlockId, type BlockSpec, type ContentItem, type Destination, type InlineSpec, type SplitTail } from '../placement/model.js';
|
|
4
|
+
import type { DataTarget, Plan, PlanStep, Prepared } from './types.js';
|
|
5
|
+
import type { DocBase, DocReads } from './reads.js';
|
|
6
|
+
import type { DocPosition, RangeView } from '../rangeDelete.js';
|
|
7
|
+
import { type Flow, type FlowTarget, type FlowView } from '../flow.js';
|
|
8
|
+
/**
|
|
9
|
+
* A record's order among the records standing at one block, for the
|
|
10
|
+
* engine's sort of two records at one origin (fork patch YP22): what a split
|
|
11
|
+
* of a list around a block writes there — a new head list holding the items
|
|
12
|
+
* before it (`head`), new blocks placed after the items before it, which
|
|
13
|
+
* stay (`kept`), a head holding those items and the block (`with`), the
|
|
14
|
+
* block itself leaving (`self`), new blocks after it (`after`) — in the
|
|
15
|
+
* order every serial run of the gestures gives (`del.merge.container`).
|
|
16
|
+
*/
|
|
17
|
+
export declare const PART: {
|
|
18
|
+
readonly head: 0;
|
|
19
|
+
readonly kept: 1;
|
|
20
|
+
readonly with: 2;
|
|
21
|
+
readonly self: 3;
|
|
22
|
+
readonly after: 4;
|
|
23
|
+
};
|
|
24
|
+
/** An inline atom to insert: its id is minted when it names none. */
|
|
25
|
+
export type NewInline = Omit<InlineSpec, 'id'> & {
|
|
26
|
+
id?: string;
|
|
27
|
+
};
|
|
28
|
+
/** The prepared ops of one facade over its reads. */
|
|
29
|
+
export declare const docOps: (c: DocBase & DocReads) => {
|
|
30
|
+
plan: (ids: readonly BlockId[], writes: PlanStep[], at?: DocPosition) => Plan;
|
|
31
|
+
canPlace: (ids: readonly BlockId[], parent?: BlockId | null, kindOf?: (id: BlockId) => string | undefined) => boolean;
|
|
32
|
+
fits: (parent: BlockId | null, kind: string | undefined) => boolean;
|
|
33
|
+
nestParent: (ids: readonly BlockId[], parent: BlockId) => BlockId;
|
|
34
|
+
canMerge: (fromId: BlockId, intoId: BlockId) => boolean;
|
|
35
|
+
defaultChild: (parent: BlockId | null) => string;
|
|
36
|
+
landingOf: (id: BlockId, kind: string, after?: boolean) => {
|
|
37
|
+
parent: string | null;
|
|
38
|
+
levels: string[];
|
|
39
|
+
};
|
|
40
|
+
besideAt: (target: BlockId, kind?: string) => BlockId;
|
|
41
|
+
/** Every op of the families, prepared. */
|
|
42
|
+
prepare: {
|
|
43
|
+
insertBlocks: (dest: Destination, specs: readonly (BlockSpec | JSONBlock)[]) => Prepared;
|
|
44
|
+
moveBlocks: (ids: readonly BlockId[], dest: Destination) => Prepared;
|
|
45
|
+
unNestBlocks: (ids: readonly BlockId[]) => Prepared;
|
|
46
|
+
unNestBlock: (id: BlockId) => Prepared;
|
|
47
|
+
liftOut: (id: BlockId, kind: string, { keep, after }?: {
|
|
48
|
+
keep?: boolean;
|
|
49
|
+
after?: readonly BlockSpec[];
|
|
50
|
+
}) => Prepared;
|
|
51
|
+
duplicateBlock: (id: BlockId, freshId?: (oldId: string, kind: "block" | "inline") => string) => Prepared;
|
|
52
|
+
placeBeside: (ids: readonly BlockId[], target: BlockId, side: "left" | "right", kind?: string) => Prepared;
|
|
53
|
+
wrapInLayout: (ids: readonly BlockId[], kind?: string, columns?: number) => Prepared;
|
|
54
|
+
splitBlock: (id: BlockId, offset: number, newId: BlockId, tail?: SplitTail) => Prepared;
|
|
55
|
+
mergeBlocks: (fromId: BlockId, intoId: BlockId) => Prepared;
|
|
56
|
+
mergeBackward: (id: BlockId) => Prepared;
|
|
57
|
+
mergeForward: (id: BlockId) => Prepared;
|
|
58
|
+
deleteBlocks: (ids: readonly BlockId[], subtree?: boolean, filled?: BlockId | null) => Prepared;
|
|
59
|
+
deleteBlock: (id: BlockId, opts?: {
|
|
60
|
+
keepChildren?: boolean;
|
|
61
|
+
}) => Prepared;
|
|
62
|
+
setBlockType: (id: BlockId, type: string) => Prepared;
|
|
63
|
+
patchData: (target: DataTarget, patches: readonly DataPatch[]) => Prepared;
|
|
64
|
+
setBlock: (id: BlockId, value: {
|
|
65
|
+
type?: string;
|
|
66
|
+
data?: Record<string, unknown>;
|
|
67
|
+
content?: (ContentItem | JSONText | JSONInlineBlock)[];
|
|
68
|
+
children?: (BlockSpec | JSONBlock)[];
|
|
69
|
+
}) => Prepared;
|
|
70
|
+
insertText: (id: BlockId, offset: number, text: string, marks?: Record<string, unknown>) => Prepared;
|
|
71
|
+
deleteText: (id: BlockId, offset: number, length: number) => Prepared;
|
|
72
|
+
formatRange: (id: BlockId, offset: number, length: number, marks: Record<string, unknown>) => Prepared;
|
|
73
|
+
clearMarks: (id: BlockId, offset: number, length: number) => Prepared;
|
|
74
|
+
insertInline: (id: BlockId, offset: number, atom: NewInline) => Prepared;
|
|
75
|
+
removeInline: (id: BlockId, inlineId: string) => Prepared;
|
|
76
|
+
insertFlow: (target: FlowTarget, flow: Flow, view?: FlowView) => Prepared;
|
|
77
|
+
deleteRange: (from: DocPosition, to: DocPosition, view?: RangeView) => Prepared;
|
|
78
|
+
replaceRange: (from: DocPosition, to: DocPosition, view?: RangeView) => Prepared;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
export type DocOps = ReturnType<typeof docOps>;
|