edytor 5.0.0 → 7.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 +3 -3
- package/dist/cloudflare/DocumentRoom.d.ts +77 -2
- package/dist/cloudflare/DocumentRoom.js +37 -1
- package/dist/cloudflare/index.d.ts +1 -1
- package/dist/cloudflare/index.js +1 -1
- package/dist/cloudflare/room/access.d.ts +6 -1
- package/dist/cloudflare/room/access.js +30 -4
- package/dist/cloudflare/room/admission.d.ts +11 -0
- package/dist/cloudflare/room/admission.js +43 -10
- package/dist/cloudflare/room/context.d.ts +73 -76
- package/dist/cloudflare/room/context.js +17 -6
- package/dist/cloudflare/room/forged.d.ts +1 -1
- package/dist/cloudflare/room/forged.js +11 -9
- package/dist/cloudflare/room/frames.d.ts +2 -0
- package/dist/cloudflare/room/frames.js +2 -0
- package/dist/cloudflare/room/presence.js +1 -4
- package/dist/cloudflare/room/reseed.d.ts +93 -0
- package/dist/cloudflare/room/reseed.js +500 -0
- package/dist/cloudflare/room/scheduler.d.ts +1 -1
- package/dist/cloudflare/room/scheduler.js +9 -1
- package/dist/cloudflare/room/shared.d.ts +10 -0
- package/dist/cloudflare/room/shared.js +10 -0
- package/dist/cloudflare/room/storage.d.ts +7 -0
- package/dist/cloudflare/room/storage.js +33 -1
- package/dist/components/Edytor.svelte +18 -3
- package/dist/components/Edytor.svelte.d.ts +10 -1
- package/dist/components/EdytorRoot.svelte +17 -0
- package/dist/components/EdytorRoot.svelte.d.ts +4 -0
- package/dist/components/Suggestion.svelte +1 -1
- package/dist/crdt/attribution/block.d.ts +3 -0
- package/dist/crdt/attribution/block.js +3 -2
- package/dist/crdt/doc/history.js +2 -3
- package/dist/crdt/doc/ops.d.ts +81 -0
- package/dist/crdt/doc/ops.js +1898 -0
- package/dist/crdt/doc/suggest.d.ts +6 -2
- package/dist/crdt/doc/suggest.js +6 -5
- package/dist/crdt/edytor-doc.d.ts +157 -157
- package/dist/crdt/edytor-doc.js +15 -43
- package/dist/crdt/engine-room.d.ts +4 -0
- package/dist/crdt/engine-room.js +6 -0
- package/dist/crdt/flow.d.ts +4 -46
- package/dist/crdt/flow.js +2 -262
- package/dist/crdt/index.d.ts +64 -75
- package/dist/crdt/operations.d.ts +1 -1
- package/dist/crdt/placement/resolve.d.ts +1 -1
- package/dist/crdt/protocol.d.ts +2 -2
- package/dist/crdt/protocol.js +2 -2
- package/dist/crdt/protocols/auth.d.ts +8 -1
- package/dist/crdt/protocols/auth.js +14 -1
- package/dist/crdt/protocols/awareness.js +46 -81
- package/dist/crdt/providers/index.d.ts +11 -22
- package/dist/crdt/providers/index.js +22 -3
- package/dist/crdt/providers/indexeddb.d.ts +6 -10
- package/dist/crdt/providers/indexeddb.js +10 -16
- package/dist/crdt/providers/room.d.ts +26 -0
- package/dist/crdt/providers/room.js +41 -12
- package/dist/crdt/providers/websocket.d.ts +10 -14
- package/dist/crdt/providers/websocket.js +43 -23
- package/dist/crdt/rangeDelete.d.ts +2 -47
- package/dist/crdt/rangeDelete.js +1 -169
- package/dist/crdt/reseed.d.ts +127 -0
- package/dist/crdt/reseed.js +533 -0
- package/dist/crdt/restore.d.ts +13 -1
- package/dist/crdt/restore.js +8 -3
- package/dist/crdt/schema.d.ts +6 -0
- package/dist/crdt/schema.js +6 -0
- package/dist/crdt/structs.d.ts +8 -11
- package/dist/crdt/structs.js +25 -44
- package/dist/crdt/text/index/build.d.ts +119 -0
- package/dist/crdt/text/index/build.js +2801 -0
- package/dist/crdt/text/index/shared.d.ts +2 -6
- package/dist/crdt/text/index/shared.js +2 -5
- package/dist/crdt/text/model.js +2 -3
- package/dist/crdt/text/runs.d.ts +9 -12
- package/dist/crdt/text/runs.js +2 -165
- package/dist/crdt/vendor/yjs/UPSTREAM.md +101 -5
- package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
- package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +10 -10
- package/dist/crdt/vendor/yjs/dts/utils/encoding.d.ts +1 -0
- package/dist/crdt/vendor/yjs/src/index.js +1 -1
- package/dist/crdt/vendor/yjs/src/structs/Item.js +24 -13
- package/dist/crdt/vendor/yjs/src/utils/Transaction.js +12 -10
- package/dist/crdt/vendor/yjs/src/utils/encoding.js +42 -1
- package/dist/events/domTextOffset.d.ts +2 -0
- package/dist/events/domTextOffset.js +8 -0
- package/dist/export/forms.d.ts +50 -50
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugins/richtext/RichTextPlugin.svelte +63 -61
- package/dist/plugins/richtext/richTextOperations.js +26 -32
- package/dist/selection/selection.svelte.d.ts +1 -1
- package/dist/selection/selection.svelte.js +87 -106
- package/dist/selection/selection.utils.js +5 -8
- package/dist/session/commands.js +49 -51
- package/dist/session/handles.js +3 -7
- package/dist/surface/attributes.d.ts +3 -3
- package/dist/surface/attributes.js +2 -2
- package/dist/surface/observer.svelte.d.ts +7 -7
- package/dist/surface/observer.svelte.js +225 -336
- package/dist/surface/projector.svelte.js +2 -2
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +7 -0
- package/package.json +1 -1
- package/dist/crdt/doc/capability.d.ts +0 -56
- package/dist/crdt/doc/capability.js +0 -207
- 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 -198
- 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 -203
- 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 -22
- package/dist/crdt/text/index/layout.js +0 -258
- package/dist/crdt/text/index/placement.d.ts +0 -47
- package/dist/crdt/text/index/placement.js +0 -738
- 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
|
@@ -44,6 +44,7 @@ import { getMarkEdgeSide, getYIndex, isTextBoundSelectionPoint } from '../select
|
|
|
44
44
|
import { project, sameEnds } from '../session/selection.js';
|
|
45
45
|
import { isAndroidChromeBrowser } from '../events/events.utils.js';
|
|
46
46
|
import { isKindControl, isOwnPoint } from '../events/nativeInteractiveControl.js';
|
|
47
|
+
import { textLeaves } from '../events/domTextOffset.js';
|
|
47
48
|
/**
|
|
48
49
|
* Named rule: Android Chrome mutates the DOM after a
|
|
49
50
|
* canceled `deleteContentBackward` and reports the caret one position right
|
|
@@ -54,9 +55,8 @@ const ANDROID_SNAP_BACK_MS = 250;
|
|
|
54
55
|
const IME_POST_COMMIT_JUMP_MS = 100;
|
|
55
56
|
/** The DOM point of a text-segment offset: its text leaf, else the element itself. */
|
|
56
57
|
export const domPointOf = (element, offset) => {
|
|
57
|
-
const walker = element.ownerDocument.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
|
58
58
|
let at = 0;
|
|
59
|
-
for (
|
|
59
|
+
for (const leaf of textLeaves(element)) {
|
|
60
60
|
const end = at + leaf.length;
|
|
61
61
|
if (offset <= end)
|
|
62
62
|
return [leaf, offset - at];
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export declare const id: (prefix: "t" | "b" | "i" | "v" | "s" | "c") => string;
|
|
2
|
+
/** `key`'s value in `map`, made (`make`) and stored when it holds none. */
|
|
3
|
+
export declare const getOr: <K, V>(map: {
|
|
4
|
+
get(key: K): V | undefined;
|
|
5
|
+
set(key: K, value: V): unknown;
|
|
6
|
+
}, key: K, make: () => V) => V;
|
|
2
7
|
/**
|
|
3
8
|
* A hook's veto as the dispatcher carries it: what `prevent(cb?)` recorded,
|
|
4
9
|
* and the extension that recorded it. Hooks never see one thrown at them:
|
package/dist/utils.js
CHANGED
|
@@ -6,6 +6,13 @@ export const id = (prefix) => {
|
|
|
6
6
|
t += alphabet[61 & r[n]];
|
|
7
7
|
return `${prefix}_${t}`;
|
|
8
8
|
};
|
|
9
|
+
/** `key`'s value in `map`, made (`make`) and stored when it holds none. */
|
|
10
|
+
export const getOr = (map, key, make) => {
|
|
11
|
+
let value = map.get(key);
|
|
12
|
+
if (value === undefined)
|
|
13
|
+
map.set(key, (value = make()));
|
|
14
|
+
return value;
|
|
15
|
+
};
|
|
9
16
|
/**
|
|
10
17
|
* A hook's veto as the dispatcher carries it: what `prevent(cb?)` recorded,
|
|
11
18
|
* and the extension that recorded it. Hooks never see one thrown at them:
|
package/package.json
CHANGED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Structural capability: one answer in advance and at execution.
|
|
3
|
-
*
|
|
4
|
-
* ── Island/void enforcement (operation-layer, not stored) ──────────────
|
|
5
|
-
*
|
|
6
|
-
* Roles are resolved operation-time via `config.roleOf(type)`, through one
|
|
7
|
-
* role table (`DisplayRoles`) that the index's display and every guard
|
|
8
|
-
* below ask of a block's shown kind — the editor derives them from plugin
|
|
9
|
-
* block definitions, exactly like the baseline's
|
|
10
|
-
* `block.definition.island/void`. They are deliberately NOT replicated block
|
|
11
|
-
* data: the schema stays policy-free, and a replica with different plugins
|
|
12
|
-
* reads the same document. Enforced rules (baseline semantics, see
|
|
13
|
-
* `plugins.ts` + `block.utils.ts`):
|
|
14
|
-
*
|
|
15
|
-
* - `void`: cannot accept children (insert/move/nest into void rejected),
|
|
16
|
-
* cannot merge either direction, cannot be split. Content edits ARE
|
|
17
|
-
* allowed — void rendered content (captions) stays editable per the
|
|
18
|
-
* baseline contract. Deletion is allowed (delete is not editing).
|
|
19
|
-
* - `island`: editable content, but its subtree is structurally sealed —
|
|
20
|
-
* a block inside an island (`insideIsland`) cannot be moved, nested,
|
|
21
|
-
* unnested, or merged across the island boundary. Merges INSIDE one
|
|
22
|
-
* island are allowed; merging an island child into the island itself is
|
|
23
|
-
* allowed when the island renders its content. Moving or merging
|
|
24
|
-
* INTO an island subtree is rejected. An island declared `lines` (code)
|
|
25
|
-
* holds only lines of its `defaultChild` kind, and a line holds no
|
|
26
|
-
* children (the display enforces it against undo,
|
|
27
|
-
* `insertBlocks` refuses a line parent); any other island keeps its
|
|
28
|
-
* structure. `canPlace` and `canMerge` are the one answer, asked in
|
|
29
|
-
* advance or by the ops.
|
|
30
|
-
* - Island merge: when an island block itself is merged (backward or
|
|
31
|
-
* forward), its children are unnested to the vacated sibling slot and
|
|
32
|
-
* reset to the default child of that slot's parent (`defaultChild`).
|
|
33
|
-
* - Baseline merges NEVER adopt the merged block's children — they unnest
|
|
34
|
-
* to the vacated slot. The facade exposes both: `mergeBlocks` is the
|
|
35
|
-
* engine primitive (children adopt into the target
|
|
36
|
-
* by staying under the source, which the target's claim displays),
|
|
37
|
-
* `mergeBackward`/`mergeForward` reproduce the baseline command shape.
|
|
38
|
-
*
|
|
39
|
-
*/
|
|
40
|
-
import { type BlockId } from '../placement/model.js';
|
|
41
|
-
import type { DocBase, DocReads } from './reads.js';
|
|
42
|
-
/** The capability of the facade over its reads. */
|
|
43
|
-
export declare const docCapability: (c: DocBase & DocReads) => {
|
|
44
|
-
canPlace: (ids: readonly BlockId[], parent?: BlockId | null, kindOf?: (id: BlockId) => string | undefined) => boolean;
|
|
45
|
-
isContainer: (id: BlockId) => boolean;
|
|
46
|
-
fits: (parent: BlockId | null, kind: string | undefined) => boolean;
|
|
47
|
-
fitsIn: (parentType: string, kind: string | undefined) => boolean;
|
|
48
|
-
fitted: (parent: BlockId | null, kind: string | undefined) => string | undefined;
|
|
49
|
-
emptiable: (id: BlockId) => boolean;
|
|
50
|
-
nestParent: (ids: readonly BlockId[], parent: BlockId) => BlockId;
|
|
51
|
-
canMerge: (fromId: BlockId, intoId: BlockId) => boolean;
|
|
52
|
-
rendersContent: (id: BlockId) => boolean;
|
|
53
|
-
defaultChild: (parent: BlockId | null) => string;
|
|
54
|
-
kindToCopy: (id: BlockId) => string;
|
|
55
|
-
};
|
|
56
|
-
export type DocCapability = ReturnType<typeof docCapability>;
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Structural capability: one answer in advance and at execution.
|
|
3
|
-
*
|
|
4
|
-
* ── Island/void enforcement (operation-layer, not stored) ──────────────
|
|
5
|
-
*
|
|
6
|
-
* Roles are resolved operation-time via `config.roleOf(type)`, through one
|
|
7
|
-
* role table (`DisplayRoles`) that the index's display and every guard
|
|
8
|
-
* below ask of a block's shown kind — the editor derives them from plugin
|
|
9
|
-
* block definitions, exactly like the baseline's
|
|
10
|
-
* `block.definition.island/void`. They are deliberately NOT replicated block
|
|
11
|
-
* data: the schema stays policy-free, and a replica with different plugins
|
|
12
|
-
* reads the same document. Enforced rules (baseline semantics, see
|
|
13
|
-
* `plugins.ts` + `block.utils.ts`):
|
|
14
|
-
*
|
|
15
|
-
* - `void`: cannot accept children (insert/move/nest into void rejected),
|
|
16
|
-
* cannot merge either direction, cannot be split. Content edits ARE
|
|
17
|
-
* allowed — void rendered content (captions) stays editable per the
|
|
18
|
-
* baseline contract. Deletion is allowed (delete is not editing).
|
|
19
|
-
* - `island`: editable content, but its subtree is structurally sealed —
|
|
20
|
-
* a block inside an island (`insideIsland`) cannot be moved, nested,
|
|
21
|
-
* unnested, or merged across the island boundary. Merges INSIDE one
|
|
22
|
-
* island are allowed; merging an island child into the island itself is
|
|
23
|
-
* allowed when the island renders its content. Moving or merging
|
|
24
|
-
* INTO an island subtree is rejected. An island declared `lines` (code)
|
|
25
|
-
* holds only lines of its `defaultChild` kind, and a line holds no
|
|
26
|
-
* children (the display enforces it against undo,
|
|
27
|
-
* `insertBlocks` refuses a line parent); any other island keeps its
|
|
28
|
-
* structure. `canPlace` and `canMerge` are the one answer, asked in
|
|
29
|
-
* advance or by the ops.
|
|
30
|
-
* - Island merge: when an island block itself is merged (backward or
|
|
31
|
-
* forward), its children are unnested to the vacated sibling slot and
|
|
32
|
-
* reset to the default child of that slot's parent (`defaultChild`).
|
|
33
|
-
* - Baseline merges NEVER adopt the merged block's children — they unnest
|
|
34
|
-
* to the vacated slot. The facade exposes both: `mergeBlocks` is the
|
|
35
|
-
* engine primitive (children adopt into the target
|
|
36
|
-
* by staying under the source, which the target's claim displays),
|
|
37
|
-
* `mergeBackward`/`mergeForward` reproduce the baseline command shape.
|
|
38
|
-
*
|
|
39
|
-
*/
|
|
40
|
-
import { isLiveIn } from '../placement/model.js';
|
|
41
|
-
/** The capability of the facade over its reads. */
|
|
42
|
-
export const docCapability = (c) => {
|
|
43
|
-
const { roles, view, blockTypeOf, childrenIds, positionOf, ancestorsOf, isVoid, isIsland, islandOf, insideIsland, holdsLayout, insideItem, isRowKind, isCellKind, displayLength } = c;
|
|
44
|
-
// ── structural capability: one answer in advance and at execution ──
|
|
45
|
-
/**
|
|
46
|
-
* May `ids` be placed under `parent` (`null` = the root)? Every id is
|
|
47
|
-
* live, distinct and outside any island interior (island subtrees are
|
|
48
|
-
* sealed); the destination is live, neither void nor an island nor
|
|
49
|
-
* inside one, and not inside any moved block's own subtree; and every
|
|
50
|
-
* block fits it as the kind `kindOf` gives (`fits`; a move keeps its
|
|
51
|
-
* kind) or already sits in it (a reorder changes nothing a list holds:
|
|
52
|
-
* an image shed into a list still moves among its items); and
|
|
53
|
-
* no layout, nor a block holding one, lands inside a layout item
|
|
54
|
-
* (`layout.nest`). A table's cell never moves, and its row only
|
|
55
|
-
* within its table (`table.fits`).
|
|
56
|
-
* Without a `parent`: may these blocks move at all (the drag
|
|
57
|
-
* affordance). The move ops refuse exactly when this answers `false`.
|
|
58
|
-
* (`insertBlock` is looser — island interiors are built by inserting
|
|
59
|
-
* into them.)
|
|
60
|
-
*/
|
|
61
|
-
const canPlace = (ids, parent, kindOf = blockTypeOf) => {
|
|
62
|
-
const v = view();
|
|
63
|
-
if (ids.length === 0 || new Set(ids).size !== ids.length)
|
|
64
|
-
return false;
|
|
65
|
-
if (ids.some((id) => !isLiveIn(v, id) || insideIsland(id, v)))
|
|
66
|
-
return false;
|
|
67
|
-
// A cell moves only with its column, never as a block (`table.fits`).
|
|
68
|
-
if (ids.some((id) => isCellKind(blockTypeOf(id))))
|
|
69
|
-
return false;
|
|
70
|
-
// A row moves only within its table: a table's rows are reordered, never re-homed.
|
|
71
|
-
const rows = ids.filter((id) => isRowKind(blockTypeOf(id)));
|
|
72
|
-
if (rows.length > 0 && parent !== undefined)
|
|
73
|
-
if (parent === null || rows.some((id) => positionOf(id)?.parent !== parent))
|
|
74
|
-
return false;
|
|
75
|
-
if (parent === undefined || parent === null)
|
|
76
|
-
return true;
|
|
77
|
-
if (!isLiveIn(v, parent) || isVoid(parent))
|
|
78
|
-
return false;
|
|
79
|
-
const stays = (id) => positionOf(id)?.parent === parent;
|
|
80
|
-
if (!ids.every((id) => stays(id) || fits(parent, kindOf(id))))
|
|
81
|
-
return false;
|
|
82
|
-
if (insideItem(parent, v) && ids.some(holdsLayout))
|
|
83
|
-
return false;
|
|
84
|
-
return ![parent, ...ancestorsOf(parent, v)].some((a) => isIsland(a) || ids.includes(a));
|
|
85
|
-
};
|
|
86
|
-
/**
|
|
87
|
-
* A container: a block that shows only its children — no content of
|
|
88
|
-
* its own, neither void nor an island (a list, a table row, a column).
|
|
89
|
-
*/
|
|
90
|
-
const isContainer = (id) => roles.of(blockTypeOf(id) ?? '').container;
|
|
91
|
-
/**
|
|
92
|
-
* THE container rule: may a block of `kind` sit directly
|
|
93
|
-
* under `parent` (`null` = the root)? A container whose default child is
|
|
94
|
-
* a kind of its own — its item (a list's `list-item`, a columns
|
|
95
|
-
* layout's `column`) — holds only its items, and containers of them
|
|
96
|
-
* when the item renders content (a list directly in a list, from JSON
|
|
97
|
-
* or the API; a layout holds no layout, `layout.fits`); one whose
|
|
98
|
-
* default child is the document's (a column) holds any block. Every
|
|
99
|
-
* structural placement asks it: a move is refused where its blocks do
|
|
100
|
-
* not fit (`canPlace`), Tab nests under a container's last item
|
|
101
|
-
* (`nestParent`), an outdent or a lift is refused where the block would
|
|
102
|
-
* not fit, and a block a merge, delete or range sheds into a container
|
|
103
|
-
* takes its item kind when it is a plain block (`fitted`). Explicit kind
|
|
104
|
-
* writes (`insertBlocks`, a retype) place what they are told (the
|
|
105
|
-
* view's Turn into places the kind where it fits first, `liftOut`); a
|
|
106
|
-
* plain block stored directly in a list still shows as its item (the
|
|
107
|
-
* index's `typeOf`), whatever write or race put it there.
|
|
108
|
-
*/
|
|
109
|
-
const fits = (parent, kind) => parent === null || fitsIn(blockTypeOf(parent) ?? '', kind);
|
|
110
|
-
/** `fits`, by the parent's kind (a block not written yet: `placeBeside`'s new item). */
|
|
111
|
-
const fitsIn = (parentType, kind) => {
|
|
112
|
-
const { container, child: item } = roles.of(parentType);
|
|
113
|
-
if (!container || item === defaultChild(null) || kind === item)
|
|
114
|
-
return true;
|
|
115
|
-
const k = kind === undefined ? undefined : roles.of(kind);
|
|
116
|
-
return k?.container === true && k.child === item && roles.of(item).rendersContent;
|
|
117
|
-
};
|
|
118
|
-
/**
|
|
119
|
-
* `kind`, or — where a plain block (the document's default kind, or
|
|
120
|
-
* none: a pasted run) does not fit `parent` — `parent`'s item, when
|
|
121
|
-
* that renders content (a paragraph landing in a list is its item; one
|
|
122
|
-
* in a columns layout keeps its kind: its text never vanishes). Any
|
|
123
|
-
* other kind keeps its kind and data wherever a merge, a delete or a
|
|
124
|
-
* paste sheds it — an image under a bullet stays an image, a heading a
|
|
125
|
-
* heading, a to-do keeps its check — as a peer's
|
|
126
|
-
* concurrent promotion shows it (`typeOf` resets only the default
|
|
127
|
-
* kind); an outdent or a move that would place one directly in a list
|
|
128
|
-
* is refused (`fits`).
|
|
129
|
-
*/
|
|
130
|
-
const fitted = (parent, kind) => {
|
|
131
|
-
if (fits(parent, kind) || (kind !== undefined && kind !== defaultChild(null)))
|
|
132
|
-
return kind;
|
|
133
|
-
const item = defaultChild(parent);
|
|
134
|
-
return roles.of(item).rendersContent ? item : kind;
|
|
135
|
-
};
|
|
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). */
|
|
137
|
-
const emptiable = (id) => isContainer(id) && displayLength(id) === 0;
|
|
138
|
-
/**
|
|
139
|
-
* Where `ids` nest when nested into `parent` (Tab, a drop inside it):
|
|
140
|
-
* `parent`, or — a container they are no items of — its last child,
|
|
141
|
-
* and so on down. Tab after a list nests under its last item (Notion);
|
|
142
|
-
* a last child that holds no children (an image, a code block) is
|
|
143
|
-
* answered as it is, and `canPlace` refuses it, as Tab right under
|
|
144
|
-
* that block is refused.
|
|
145
|
-
*/
|
|
146
|
-
const nestParent = (ids, parent) => {
|
|
147
|
-
let at = parent;
|
|
148
|
-
while (!ids.every((id) => fits(at, blockTypeOf(id)))) {
|
|
149
|
-
const last = childrenIds(at).at(-1);
|
|
150
|
-
if (last === undefined)
|
|
151
|
-
break;
|
|
152
|
-
at = last;
|
|
153
|
-
}
|
|
154
|
-
return at;
|
|
155
|
-
};
|
|
156
|
-
/**
|
|
157
|
-
* May `fromId`'s content merge into `intoId`? Both live and distinct,
|
|
158
|
-
* neither void, `intoId` renders its content (a list, a table row or a
|
|
159
|
-
* code block shows none, so a first item, cell or line never merges
|
|
160
|
-
* into it), `fromId` renders its own unless it is
|
|
161
|
-
* an island (a list or a row never merges as a whole: its items would
|
|
162
|
-
* leave it, `del.merge.container`), and the merge stays on one side of an island
|
|
163
|
-
* boundary (a block may merge into its own island root — that stays
|
|
164
|
-
* inside — but nothing from outside merges into an island).
|
|
165
|
-
*/
|
|
166
|
-
const canMerge = (fromId, intoId) => {
|
|
167
|
-
const v = view();
|
|
168
|
-
if (fromId === intoId || !isLiveIn(v, fromId) || !isLiveIn(v, intoId))
|
|
169
|
-
return false;
|
|
170
|
-
if (isVoid(fromId) || isVoid(intoId))
|
|
171
|
-
return false;
|
|
172
|
-
// Nothing merges into or out of a table's cell (`table.merge`).
|
|
173
|
-
if (isCellKind(blockTypeOf(fromId)) || isCellKind(blockTypeOf(intoId)))
|
|
174
|
-
return false;
|
|
175
|
-
if (!rendersContent(intoId) || !(rendersContent(fromId) || isIsland(fromId)))
|
|
176
|
-
return false;
|
|
177
|
-
const islandFrom = islandOf(fromId, v);
|
|
178
|
-
if (intoId === islandFrom)
|
|
179
|
-
return true;
|
|
180
|
-
return islandFrom === islandOf(intoId, v) && !isIsland(intoId);
|
|
181
|
-
};
|
|
182
|
-
/** `id`'s shown kind renders its content. */
|
|
183
|
-
const rendersContent = (id) => roles.of(blockTypeOf(id) ?? '').rendersContent;
|
|
184
|
-
/** The adopted default child type under `parent` (`null` = the root). */
|
|
185
|
-
const defaultChild = (parent) => roles.of(parent === null ? null : (blockTypeOf(parent) ?? null)).child;
|
|
186
|
-
/**
|
|
187
|
-
* The kind a new block copies from `id` (a split tail, a flow's tail,
|
|
188
|
-
* a duplicate and each of its descendants). A type a peer's retype is
|
|
189
|
-
* replacing can be missing while its new value is pending: the copy
|
|
190
|
-
* then takes its parent's default child, never a missing type
|
|
191
|
-
* (it showed as `unknown` everywhere, for good).
|
|
192
|
-
*/
|
|
193
|
-
const kindToCopy = (id) => blockTypeOf(id) ?? defaultChild(positionOf(id)?.parent ?? null);
|
|
194
|
-
return {
|
|
195
|
-
canPlace,
|
|
196
|
-
isContainer,
|
|
197
|
-
fits,
|
|
198
|
-
fitsIn,
|
|
199
|
-
fitted,
|
|
200
|
-
emptiable,
|
|
201
|
-
nestParent,
|
|
202
|
-
canMerge,
|
|
203
|
-
rendersContent,
|
|
204
|
-
defaultChild,
|
|
205
|
-
kindToCopy
|
|
206
|
-
};
|
|
207
|
-
};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { BlockId, InlineSpec } from '../placement/model.js';
|
|
2
|
-
import type { Prepared } from './types.js';
|
|
3
|
-
import type { OpsContext } from './steps.js';
|
|
4
|
-
/** An inline atom to insert: its id is minted when it names none. */
|
|
5
|
-
export type NewInline = Omit<InlineSpec, 'id'> & {
|
|
6
|
-
id?: string;
|
|
7
|
-
};
|
|
8
|
-
/** The content ops of one facade, prepared. */
|
|
9
|
-
export declare const contentOps: (c: OpsContext) => {
|
|
10
|
-
insertText: (id: BlockId, offset: number, text: string, marks?: Record<string, unknown>) => Prepared;
|
|
11
|
-
deleteText: (id: BlockId, offset: number, length: number) => Prepared;
|
|
12
|
-
formatRange: (id: BlockId, offset: number, length: number, marks: Record<string, unknown>) => Prepared;
|
|
13
|
-
clearMarks: (id: BlockId, offset: number, length: number) => Prepared;
|
|
14
|
-
insertInline: (id: BlockId, offset: number, atom: NewInline) => Prepared;
|
|
15
|
-
removeInline: (id: BlockId, inlineId: string) => Prepared;
|
|
16
|
-
};
|
package/dist/crdt/doc/content.js
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Content ops: text and inline atoms in a block's display (allowed
|
|
3
|
-
* inside voids — the caption contract), and marks over a range.
|
|
4
|
-
*/
|
|
5
|
-
import { sanitizeWireJson, jsonEquals } from '../../utils/json.js';
|
|
6
|
-
import { id as newId } from '../../utils.js';
|
|
7
|
-
import { ref, sanitizeInline } from './plan.js';
|
|
8
|
-
/** The content ops of one facade, prepared. */
|
|
9
|
-
export const contentOps = (c) => {
|
|
10
|
-
const { contentTarget, REFUSED, plan, clamp, atomOf, textIn } = c;
|
|
11
|
-
// content ops (allowed inside voids — caption contract)
|
|
12
|
-
const insertText = (id, offset, text, marks) => {
|
|
13
|
-
id = ref(id);
|
|
14
|
-
const clean = ref(text);
|
|
15
|
-
if (!contentTarget(id))
|
|
16
|
-
return REFUSED;
|
|
17
|
-
const [at] = clamp(id, offset, 0);
|
|
18
|
-
const m = marks && sanitizeWireJson(marks);
|
|
19
|
-
return plan([id], clean === '' ? [] : [{ op: 'insertText', id, offset: at, text: clean, marks: m }]);
|
|
20
|
-
};
|
|
21
|
-
const deleteText = (id, offset, length) => {
|
|
22
|
-
id = ref(id);
|
|
23
|
-
if (!contentTarget(id))
|
|
24
|
-
return REFUSED;
|
|
25
|
-
const [at, end] = clamp(id, offset, length);
|
|
26
|
-
return plan([id], end > at ? [{ op: 'deleteText', id, offset: at, length: end - at }] : []);
|
|
27
|
-
};
|
|
28
|
-
/**
|
|
29
|
-
* Multi-mark format over a range (values may be null = unset); planned
|
|
30
|
-
* only when some text atom in the range carries a different value.
|
|
31
|
-
*/
|
|
32
|
-
const formatRange = (id, offset, length, marks) => {
|
|
33
|
-
id = ref(id);
|
|
34
|
-
const clean = sanitizeWireJson(marks);
|
|
35
|
-
if (!contentTarget(id))
|
|
36
|
-
return REFUSED;
|
|
37
|
-
const [at, end] = clamp(id, offset, length);
|
|
38
|
-
// Planned only when some text atom carries another value (the same-value guard).
|
|
39
|
-
const differs = textIn(id, at, end).some((item) => Object.keys(clean).some((k) => !jsonEquals(item.marks?.[k] ?? null, clean[k] ?? null)));
|
|
40
|
-
return plan([id], differs ? [{ op: 'formatRange', id, offset: at, length: end - at, marks: clean }] : []);
|
|
41
|
-
};
|
|
42
|
-
/**
|
|
43
|
-
* Baseline `removeMarksFromText`: every mark present anywhere in the
|
|
44
|
-
* range is unset over the range. Names are discovered from the content.
|
|
45
|
-
*/
|
|
46
|
-
const clearMarks = (id, offset, length) => {
|
|
47
|
-
const clears = {};
|
|
48
|
-
for (const item of textIn(ref(id), offset, offset + length)) {
|
|
49
|
-
for (const k of Object.keys(item.marks ?? {}))
|
|
50
|
-
clears[k] = null;
|
|
51
|
-
}
|
|
52
|
-
return formatRange(id, offset, length, clears);
|
|
53
|
-
};
|
|
54
|
-
/** An atom without an id gets a fresh one. */
|
|
55
|
-
const insertInline = (id, offset, atom) => {
|
|
56
|
-
id = ref(id);
|
|
57
|
-
const clean = sanitizeInline({ ...atom, id: atom.id ?? newId('i') });
|
|
58
|
-
if (!contentTarget(id))
|
|
59
|
-
return REFUSED;
|
|
60
|
-
return plan([id], [{ op: 'insertInline', id, offset: clamp(id, offset, 0)[0], atom: clean }]);
|
|
61
|
-
};
|
|
62
|
-
const removeInline = (id, inlineId) => {
|
|
63
|
-
id = ref(id);
|
|
64
|
-
const atom = atomOf(id, ref(inlineId));
|
|
65
|
-
if (atom === undefined)
|
|
66
|
-
return REFUSED;
|
|
67
|
-
return plan([id], [{ op: 'removeInline', id, offset: atom.at, inlineId: ref(inlineId) }]);
|
|
68
|
-
};
|
|
69
|
-
return {
|
|
70
|
-
insertText,
|
|
71
|
-
deleteText,
|
|
72
|
-
formatRange,
|
|
73
|
-
clearMarks,
|
|
74
|
-
insertInline,
|
|
75
|
-
removeInline
|
|
76
|
-
};
|
|
77
|
-
};
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Block deletion (`del.blocks.promote`): only the members leave, the
|
|
3
|
-
* unselected children of a deleted block take its slot.
|
|
4
|
-
*/
|
|
5
|
-
import type { BlockId } from '../placement/model.js';
|
|
6
|
-
import type { Prepared } from './types.js';
|
|
7
|
-
import type { OpsContext } from './steps.js';
|
|
8
|
-
/** The delete ops of one facade, prepared. */
|
|
9
|
-
export declare const deleteOps: (c: OpsContext) => {
|
|
10
|
-
deleteBlocks: (ids: readonly BlockId[], subtree?: boolean, filled?: BlockId | null) => Prepared;
|
|
11
|
-
deleteBlock: (id: BlockId, opts?: {
|
|
12
|
-
keepChildren?: boolean;
|
|
13
|
-
}) => Prepared;
|
|
14
|
-
};
|
|
15
|
-
export type DeleteOps = ReturnType<typeof deleteOps>;
|
package/dist/crdt/doc/delete.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { ref } from './plan.js';
|
|
2
|
-
/** The delete ops of one facade, prepared. */
|
|
3
|
-
export const deleteOps = (c) => {
|
|
4
|
-
const { view, blockTypeOf, childrenIds, positionOf, ancestorsOf, itemKindOf, isLayout, isLayoutItem, isTable, isTableRow, live, REFUSED, plan, settle, emptyingAll, emptied, deleting, remove } = c;
|
|
5
|
-
/**
|
|
6
|
-
* Delete a set of blocks (`del.blocks.promote`): this writer's mark on
|
|
7
|
-
* every member and on what it displays through merge claims (wins over
|
|
8
|
-
* concurrent moves). Only the members leave: the unselected children of a
|
|
9
|
-
* deleted block take its slot, in order, with their subtrees, as the
|
|
10
|
-
* kind they show there (`settledKind`: a deleted island's children take
|
|
11
|
-
* the slot parent's default child, a deleted list's items leave its
|
|
12
|
-
* kind, a block promoted into a list is its item). A container the delete leaves with no child goes too
|
|
13
|
-
* (`del.range.empty-container`). `subtree` is the explicit whole-subtree
|
|
14
|
-
* delete. `filled`: a parent the same plan fills again (a flow placed in
|
|
15
|
-
* the deleted blocks' slot, `flow.slot`), which is never emptied, so its
|
|
16
|
-
* layout never dissolves (`layout.flow-slot`).
|
|
17
|
-
*/
|
|
18
|
-
const deleteBlocks = (ids, subtree = false, filled) => {
|
|
19
|
-
const set = new Set(ids.map(ref));
|
|
20
|
-
if ([...set].some((id) => !live(id)))
|
|
21
|
-
return REFUSED;
|
|
22
|
-
// A layout goes with its items: their blocks take its slot (`layout.dissolving`).
|
|
23
|
-
for (const id of [...set])
|
|
24
|
-
if (isLayout(id))
|
|
25
|
-
for (const kid of childrenIds(id))
|
|
26
|
-
if (isLayoutItem(kid))
|
|
27
|
-
set.add(kid);
|
|
28
|
-
// A table goes with its rows and cells, a row with its cells: none shows outside them (`table.delete-row`).
|
|
29
|
-
const whole = (id) => {
|
|
30
|
-
for (const kid of childrenIds(id)) {
|
|
31
|
-
set.add(kid);
|
|
32
|
-
whole(kid);
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
for (const id of [...set])
|
|
36
|
-
if (isTable(id) || isTableRow(id))
|
|
37
|
-
whole(id);
|
|
38
|
-
if (subtree) {
|
|
39
|
-
const roots = [...set].filter((id) => !ancestorsOf(id).some((a) => set.has(a)));
|
|
40
|
-
return plan(roots, emptied(roots, roots.map((id) => remove(id))));
|
|
41
|
-
}
|
|
42
|
-
const roots = [...set].filter((id) => !set.has(positionOf(id).parent));
|
|
43
|
-
const { kids } = view();
|
|
44
|
-
const promoting = new Set();
|
|
45
|
-
const members = [];
|
|
46
|
-
const writes = roots.flatMap((root) => {
|
|
47
|
-
const pos = positionOf(root);
|
|
48
|
-
const chunk = [];
|
|
49
|
-
const kept = [];
|
|
50
|
-
// The kept children keep their home slots: the read shows them in the
|
|
51
|
-
// root's slot, in its order (a block a peer puts under a member
|
|
52
|
-
// meanwhile takes the slot in that order too); only their kinds are
|
|
53
|
-
// written, as the slot's parent settles them.
|
|
54
|
-
const walk = (b) => {
|
|
55
|
-
chunk.push(b);
|
|
56
|
-
for (const kid of kids.get(b) ?? []) {
|
|
57
|
-
if (set.has(kid.id))
|
|
58
|
-
walk(kid.id);
|
|
59
|
-
else
|
|
60
|
-
kept.push({ id: kid.id, from: b });
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
walk(root);
|
|
64
|
-
members.push(...chunk);
|
|
65
|
-
if (kept.length > 0)
|
|
66
|
-
promoting.add(root);
|
|
67
|
-
// A layout shows only its items: a deleted item's blocks show right
|
|
68
|
-
// after the layout (`layout.only-items`), settled for its parent.
|
|
69
|
-
const item = pos.parent === null ? undefined : itemKindOf(pos.parent);
|
|
70
|
-
const out = (k) => item !== undefined && blockTypeOf(k.id) !== item;
|
|
71
|
-
return [
|
|
72
|
-
...kept.flatMap((k) => settle(k.from, [k.id], out(k) ? positionOf(pos.parent).parent : pos.parent)),
|
|
73
|
-
deleting(root, chunk)
|
|
74
|
-
];
|
|
75
|
-
});
|
|
76
|
-
return plan(roots, emptyingAll(roots.filter((r) => !promoting.has(r)).map((id) => positionOf(id).parent), roots.filter((r) => !promoting.has(r)), writes, filled === undefined ? undefined : new Set([filled]), members));
|
|
77
|
-
};
|
|
78
|
-
/** Delete `id` — its children take its slot (`keepChildren: false`: the whole subtree). */
|
|
79
|
-
const deleteBlock = (id, opts = {}) => deleteBlocks([id], opts.keepChildren === false);
|
|
80
|
-
return {
|
|
81
|
-
deleteBlocks,
|
|
82
|
-
deleteBlock
|
|
83
|
-
};
|
|
84
|
-
};
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { BlockId } from '../placement/model.js';
|
|
2
|
-
import type { Prepared } from './types.js';
|
|
3
|
-
import type { OpsContext } from './steps.js';
|
|
4
|
-
/** The layout ops of one facade, prepared. */
|
|
5
|
-
export declare const layoutOps: (c: OpsContext) => {
|
|
6
|
-
besideAt: (target: BlockId, kind?: string) => BlockId;
|
|
7
|
-
placeBeside: (ids: readonly BlockId[], target: BlockId, side: "left" | "right", kind?: string) => Prepared;
|
|
8
|
-
wrapInLayout: (ids: readonly BlockId[], kind?: string, columns?: number) => Prepared;
|
|
9
|
-
};
|