edytor 5.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/cloudflare/room/context.d.ts +62 -74
- 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/edytor-doc.d.ts +156 -156
- package/dist/crdt/edytor-doc.js +14 -42
- package/dist/crdt/flow.d.ts +4 -46
- package/dist/crdt/flow.js +2 -262
- package/dist/crdt/index.d.ts +62 -74
- package/dist/crdt/operations.d.ts +1 -1
- package/dist/crdt/placement/resolve.d.ts +1 -1
- package/dist/crdt/protocols/awareness.js +46 -81
- package/dist/crdt/providers/index.d.ts +10 -22
- 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 +6 -12
- package/dist/crdt/providers/websocket.js +8 -15
- package/dist/crdt/rangeDelete.d.ts +2 -47
- package/dist/crdt/rangeDelete.js +1 -169
- 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 +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/events/domTextOffset.d.ts +2 -0
- package/dist/events/domTextOffset.js +8 -0
- package/dist/export/forms.d.ts +50 -50
- package/dist/plugins/richtext/RichTextPlugin.svelte +63 -61
- package/dist/plugins/richtext/richTextOperations.js +26 -32
- package/dist/selection/selection.utils.js +5 -8
- package/dist/session/handles.js +3 -7
- package/dist/surface/observer.svelte.js +15 -30
- 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
package/dist/crdt/doc/meta.js
DELETED
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A block's meta: its type (`setBlockType`, the steps a retype implies) and
|
|
3
|
-
* its data, a block's, an atom's or the document's (`patchData`, the data
|
|
4
|
-
* steps of a patch), and the baseline `setBlock`.
|
|
5
|
-
*/
|
|
6
|
-
import { randOf } from '../rand.js';
|
|
7
|
-
import { TYPE } from '../schema.js';
|
|
8
|
-
import { asBlockSpec, asContentItem, sanitizeSpec, sanitizeWireJson } from '../../utils/json.js';
|
|
9
|
-
import { patchWrites, leafKey, isObject } from '../data.js';
|
|
10
|
-
import { collides } from '../placement/model.js';
|
|
11
|
-
import { ref, refused, sanitizeItem } from './plan.js';
|
|
12
|
-
/** The meta ops of one facade, prepared. */
|
|
13
|
-
export const metaOps = (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
|
-
/**
|
|
16
|
-
* The data step of `patches` (sanitized) on a block, one of its atoms or
|
|
17
|
-
* the document (`crdt/data.ts`): none when it changes nothing, `null`
|
|
18
|
-
* when a patch is refused.
|
|
19
|
-
*/
|
|
20
|
-
const dataSteps = (target, patches) => {
|
|
21
|
-
const [id, inlineId] = typeof target === 'object' && target ? [target.block, target.atom] : [target ?? undefined];
|
|
22
|
-
const node = dataNode(id, inlineId);
|
|
23
|
-
// A block kind's atomic paths are written as one leaf each (`data.atomic`).
|
|
24
|
-
const type = inlineId === undefined && id !== undefined ? blockTypeOf(id) : undefined;
|
|
25
|
-
const atomic = (type === undefined ? undefined : roleOf(type)?.atomic) ?? [];
|
|
26
|
-
const writes = node
|
|
27
|
-
? patchWrites(node, patches, doc.clientID, randOf(doc), atomic.map((p) => leafKey(typeof p === 'string' ? [p] : p)))
|
|
28
|
-
: [];
|
|
29
|
-
if (writes === null)
|
|
30
|
-
return null; // a patch fits no value there
|
|
31
|
-
if (writes.length === 0)
|
|
32
|
-
return [];
|
|
33
|
-
const offset = inlineId === undefined ? undefined : atomOf(id, inlineId)?.at;
|
|
34
|
-
return [
|
|
35
|
-
{
|
|
36
|
-
op: 'patchData',
|
|
37
|
-
...(id !== undefined && { id }),
|
|
38
|
-
...(inlineId !== undefined && { inlineId, offset }),
|
|
39
|
-
ops: patches,
|
|
40
|
-
writes
|
|
41
|
-
}
|
|
42
|
-
];
|
|
43
|
-
};
|
|
44
|
-
/** A whole-data replace: a patch of the root. */
|
|
45
|
-
const replaceData = (data) => [{ path: [], value: sanitizeWireJson(data) }];
|
|
46
|
-
/**
|
|
47
|
-
* `data`'s leaves as sets over `current` (`data.retype.keep`): a
|
|
48
|
-
* plain object's keys, recursively where `current` holds an object
|
|
49
|
-
* there too; anything else (an array, a primitive, an empty object, an
|
|
50
|
-
* object where `current` holds none) as one value at its path. Nothing
|
|
51
|
-
* else under the root is touched.
|
|
52
|
-
*/
|
|
53
|
-
const leafSets = (data, current) => {
|
|
54
|
-
const out = [];
|
|
55
|
-
const walk = (v, at, path) => {
|
|
56
|
-
if (isObject(v) && Object.keys(v).length > 0 && (path.length === 0 || isObject(at)))
|
|
57
|
-
for (const [k, x] of Object.entries(v))
|
|
58
|
-
walk(x, isObject(at) ? at[k] : undefined, [...path, k]);
|
|
59
|
-
else if (path.length > 0)
|
|
60
|
-
out.push({ path, value: v });
|
|
61
|
-
};
|
|
62
|
-
walk(sanitizeWireJson(data), current, []);
|
|
63
|
-
return out;
|
|
64
|
-
};
|
|
65
|
-
/**
|
|
66
|
-
* The target role decides: nothing renders a void's children,
|
|
67
|
-
* so a block retyped to a void kind hands them to the slot right after
|
|
68
|
-
* it, as the kind they show there (`settledKind`), in their order (where
|
|
69
|
-
* the read-time shedding shows a child a peer adds meanwhile). An island
|
|
70
|
-
* declared `lines` retyped to an ordinary kind keeps its lines, each
|
|
71
|
-
* retyped to the new kind's default child (the document's where that
|
|
72
|
-
* renders no content): no line kind outside its island. Any other
|
|
73
|
-
* island's children keep their kinds (a table's rows stay rows), as
|
|
74
|
-
* `typeOf` shows a child a peer adds meanwhile.
|
|
75
|
-
*/
|
|
76
|
-
const retypeSteps = (id, type) => {
|
|
77
|
-
const { kids } = view();
|
|
78
|
-
const pos = positionOf(id);
|
|
79
|
-
const steps = attr(id, TYPE, type);
|
|
80
|
-
const moved = kids.get(id) ?? [];
|
|
81
|
-
// A code block retyped to an ordinary kind keeps its lines as that kind's children
|
|
82
|
-
// (`leavingIsland`: never a kind that hides their text).
|
|
83
|
-
const to = roles.of(type);
|
|
84
|
-
if (isLines(id) && !to.island && !to.childless) {
|
|
85
|
-
return [
|
|
86
|
-
...steps,
|
|
87
|
-
...moved.flatMap((k) => {
|
|
88
|
-
const kind = leavingIsland(id, blockTypeOf(k.id), to.child);
|
|
89
|
-
return kind === undefined ? [] : attr(k.id, TYPE, kind);
|
|
90
|
-
})
|
|
91
|
-
];
|
|
92
|
-
}
|
|
93
|
-
if (!to.childless || moved.length === 0 || pos === null)
|
|
94
|
-
return steps;
|
|
95
|
-
// The children keep their home slots: the read shows them in the void's
|
|
96
|
-
// slot (`conc.void-children`), settled for its parent.
|
|
97
|
-
return [
|
|
98
|
-
...steps,
|
|
99
|
-
...settle(id, moved.map((k) => k.id), pos.parent)
|
|
100
|
-
];
|
|
101
|
-
};
|
|
102
|
-
/** Set the block type (attr write — the block keeps its identity; see `retypeSteps`). */
|
|
103
|
-
const setBlockType = (id, type) => {
|
|
104
|
-
id = ref(id);
|
|
105
|
-
return live(id) ? plan([id], retypeSteps(id, ref(type))) : REFUSED;
|
|
106
|
-
};
|
|
107
|
-
/**
|
|
108
|
-
* Patch the data of a block, one of its inline atoms or the document
|
|
109
|
-
* (`null`), in order (`crdt/data.ts`): each patch replaces the value at
|
|
110
|
-
* its path (`value` absent deletes it), `splice` and `order` edit the
|
|
111
|
-
* array there; a path addresses array items by index, resolved here
|
|
112
|
-
* to the items' ids. Only the leaves it changes are written, so a
|
|
113
|
-
* peer's edit of another key or item is kept. Refused for an absent
|
|
114
|
-
* target, a path that is no array of strings, a root value that is no
|
|
115
|
-
* object, or an op that fits no value at its path.
|
|
116
|
-
*/
|
|
117
|
-
const patchData = (target, patches) => {
|
|
118
|
-
const valid = (p) => Array.isArray(p?.path) &&
|
|
119
|
-
p.path.every((k) => typeof k === 'string') &&
|
|
120
|
-
(p.path.length > 0 || p.value === undefined || isObject(p.value));
|
|
121
|
-
if (!Array.isArray(patches) || !patches.every(valid))
|
|
122
|
-
return REFUSED;
|
|
123
|
-
// Only the fields a patch has: an absent one is no non-JSON value to report.
|
|
124
|
-
const clean = patches.map(({ path, value, splice, order }) => sanitizeWireJson({
|
|
125
|
-
path,
|
|
126
|
-
...(value !== undefined && { value }),
|
|
127
|
-
...(splice !== undefined && { splice }),
|
|
128
|
-
...(order !== undefined && { order })
|
|
129
|
-
}));
|
|
130
|
-
const t = target === null
|
|
131
|
-
? null
|
|
132
|
-
: typeof target === 'string'
|
|
133
|
-
? ref(target)
|
|
134
|
-
: { block: ref(target.block), atom: ref(target.atom) };
|
|
135
|
-
if (typeof t === 'string' ? !live(t) : t !== null && atomOf(t.block, t.atom) === undefined)
|
|
136
|
-
return REFUSED;
|
|
137
|
-
const steps = dataSteps(t, clean);
|
|
138
|
-
return steps ? plan(t === null ? [] : [typeof t === 'string' ? t : t.block], steps) : REFUSED;
|
|
139
|
-
};
|
|
140
|
-
/**
|
|
141
|
-
* Baseline `setBlock`: `type` updates the block in place and `data`
|
|
142
|
-
* sets the leaves it names, removing none (`data.retype.keep`: a
|
|
143
|
-
* retype keeps the block's properties, as Notion's Turn into does;
|
|
144
|
-
* `setBlockData` replaces them); `content`/`children` REPLACE
|
|
145
|
-
* wholesale — explicit replacement is a
|
|
146
|
-
* new-identity operation; a retype to a void kind without `children`
|
|
147
|
-
* unnests the current ones (`retypeSteps`). All-or-nothing: children
|
|
148
|
-
* for a block that is `void` after the write, or a replacement id that
|
|
149
|
-
* is already taken (a live or deleted
|
|
150
|
-
* block, the replaced children included, or a duplicate inside the
|
|
151
|
-
* replacement), refuses before any write — `id-collision` for the
|
|
152
|
-
* latter. A caller wanting a child back re-creates it with a fresh id.
|
|
153
|
-
*/
|
|
154
|
-
const setBlock = (id, value) => {
|
|
155
|
-
id = ref(id);
|
|
156
|
-
const content = value.content?.map((item) => sanitizeItem(asContentItem(item)));
|
|
157
|
-
const children = value.children?.map((child) => sanitizeSpec(asBlockSpec(child)));
|
|
158
|
-
if (!live(id) || (content !== undefined && !contentTarget(id)))
|
|
159
|
-
return REFUSED;
|
|
160
|
-
const type = value.type === undefined ? undefined : ref(value.type);
|
|
161
|
-
const toVoid = type === undefined ? isVoid(id) : roles.of(type).childless;
|
|
162
|
-
if (children?.length && toVoid)
|
|
163
|
-
return REFUSED;
|
|
164
|
-
if (children !== undefined && collides(doc, children))
|
|
165
|
-
return refused('id-collision');
|
|
166
|
-
const data = value.data === undefined ? [] : dataSteps(id, leafSets(value.data, blockDataOf(id)));
|
|
167
|
-
if (data === null)
|
|
168
|
-
return REFUSED;
|
|
169
|
-
const writes = [
|
|
170
|
-
...(type === undefined
|
|
171
|
-
? []
|
|
172
|
-
: children === undefined
|
|
173
|
-
? retypeSteps(id, type)
|
|
174
|
-
: attr(id, TYPE, type)),
|
|
175
|
-
...data
|
|
176
|
-
];
|
|
177
|
-
if (content !== undefined) {
|
|
178
|
-
const length = displayLength(id);
|
|
179
|
-
if (length > 0)
|
|
180
|
-
writes.push({ op: 'deleteText', id, offset: 0, length });
|
|
181
|
-
writeContent(writes, id, 0, content);
|
|
182
|
-
}
|
|
183
|
-
if (children !== undefined) {
|
|
184
|
-
for (const kid of childrenIds(id))
|
|
185
|
-
writes.push(remove(kid));
|
|
186
|
-
if (children.length > 0)
|
|
187
|
-
writes.push({ op: 'insertBlocks', parent: id, index: 0, specs: children });
|
|
188
|
-
}
|
|
189
|
-
return plan([id], writes);
|
|
190
|
-
};
|
|
191
|
-
return {
|
|
192
|
-
dataSteps,
|
|
193
|
-
replaceData,
|
|
194
|
-
setBlockType,
|
|
195
|
-
patchData,
|
|
196
|
-
setBlock
|
|
197
|
-
};
|
|
198
|
-
};
|
package/dist/crdt/doc/moves.d.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { type JSONBlock } from '../../utils/json.js';
|
|
2
|
-
import { type BlockId, type BlockSpec, type Destination } from '../placement/model.js';
|
|
3
|
-
import type { Prepared } from './types.js';
|
|
4
|
-
import { type OpsContext } from './steps.js';
|
|
5
|
-
/** The moves ops of one facade, prepared. */
|
|
6
|
-
export declare const moveOps: (c: OpsContext) => {
|
|
7
|
-
insertBlocks: (dest: Destination, specs: readonly (BlockSpec | JSONBlock)[]) => Prepared;
|
|
8
|
-
moveBlocks: (ids: readonly BlockId[], dest: Destination) => Prepared;
|
|
9
|
-
unNestBlocks: (ids: readonly BlockId[]) => Prepared;
|
|
10
|
-
unNestBlock: (id: BlockId) => Prepared;
|
|
11
|
-
landingOf: (id: BlockId, kind: string, after?: boolean) => {
|
|
12
|
-
parent: string | null;
|
|
13
|
-
levels: string[];
|
|
14
|
-
};
|
|
15
|
-
liftOut: (id: BlockId, kind: string, { keep, after }?: {
|
|
16
|
-
keep?: boolean;
|
|
17
|
-
after?: readonly BlockSpec[];
|
|
18
|
-
}) => Prepared;
|
|
19
|
-
duplicateBlock: (id: BlockId, freshId?: (oldId: string, kind: "block" | "inline") => string) => Prepared;
|
|
20
|
-
};
|
|
21
|
-
export type MoveOps = ReturnType<typeof moveOps>;
|
package/dist/crdt/doc/moves.js
DELETED
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Block placement ops: insert (`insertBlocks`, `duplicateBlock`), move
|
|
3
|
-
* (`moveBlocks`), outdent (`unNestBlocks`) and the lift out of the
|
|
4
|
-
* containers a kind does not fit (`landingOf`, `splitOut`, `liftOut`).
|
|
5
|
-
*/
|
|
6
|
-
import { id as newId } from '../../utils.js';
|
|
7
|
-
import { asBlockSpec, sanitizeSpec } from '../../utils/json.js';
|
|
8
|
-
import { collides } from '../placement/model.js';
|
|
9
|
-
import { ref } from './plan.js';
|
|
10
|
-
import { PART } from './steps.js';
|
|
11
|
-
/** The moves ops of one facade, prepared. */
|
|
12
|
-
export const moveOps = (c) => {
|
|
13
|
-
const { doc, blockTypeOf, blockDataOf, childrenIds, positionOf, isVoid, isIsland, isLine, contentItems, live, canPlace, isContainer, fits, kindToCopy, REFUSED, plan, moveTo, move, leaving, settledKind, settle, landing, emptying, emptied } = c;
|
|
14
|
-
/**
|
|
15
|
-
* Insert blocks (specs may carry children/content/data; the batch is
|
|
16
|
-
* all-or-nothing). A spec is a `BlockSpec` or the `JSONBlock` that
|
|
17
|
-
* `toJSON` and the view's `value` speak (`{ type, content: [{ text }] }`):
|
|
18
|
-
* its ids are kept, minted where missing (`asBlockSpec`). Ids must be fresh.
|
|
19
|
-
* Refused when the parent is not live, is `void` or is an island's
|
|
20
|
-
* line (it holds no children), or any id collides.
|
|
21
|
-
* Inserting INSIDE an island is allowed — island interiors are built
|
|
22
|
-
* this way. `ids`: the inserted roots.
|
|
23
|
-
*/
|
|
24
|
-
const insertBlocks = (dest, specs) => {
|
|
25
|
-
const parent = ref(dest.parent);
|
|
26
|
-
const clean = specs.map((spec) => sanitizeSpec(asBlockSpec(spec)));
|
|
27
|
-
if (parent !== null && (isVoid(parent) || isLine(parent)))
|
|
28
|
-
return REFUSED;
|
|
29
|
-
if (clean.length === 0)
|
|
30
|
-
return plan([], []);
|
|
31
|
-
if ((parent !== null && !live(parent)) || collides(doc, clean))
|
|
32
|
-
return REFUSED;
|
|
33
|
-
return plan(clean.map((s) => s.id), [{ op: 'insertBlocks', parent, index: dest.index, specs: clean }]);
|
|
34
|
-
};
|
|
35
|
-
/**
|
|
36
|
-
* Grouped move — one step, per-member conflict resolution; refused
|
|
37
|
-
* exactly when `canPlace` refuses the group. `ids`: the moved blocks,
|
|
38
|
-
* in request order.
|
|
39
|
-
*/
|
|
40
|
-
const moveBlocks = (ids, dest) => {
|
|
41
|
-
const moved = ids.map(ref);
|
|
42
|
-
const parent = ref(dest.parent);
|
|
43
|
-
if (moved.length === 0)
|
|
44
|
-
return plan([], []);
|
|
45
|
-
if (!canPlace(moved, parent))
|
|
46
|
-
return REFUSED;
|
|
47
|
-
// A list the move leaves with no item goes (not one it moves or lands in).
|
|
48
|
-
const into = new Set([...moved, ...landing(parent)]);
|
|
49
|
-
return plan(moved, emptied(moved, move(moved, parent, dest.index), into));
|
|
50
|
-
};
|
|
51
|
-
/**
|
|
52
|
-
* Outdent: move sibling blocks `ids` (document order) right
|
|
53
|
-
* after their parent, and hand the siblings that followed the last of
|
|
54
|
-
* them to it as its last children — every outliner's Shift+Tab. A last
|
|
55
|
-
* block that cannot adopt them (void, island, a container they do not
|
|
56
|
-
* fit) leaves them with the parent. The blocks take the kind they show
|
|
57
|
-
* in their new slot (`settledKind`: a list's item becomes a paragraph,
|
|
58
|
-
* a paragraph outdented into a list its item); refused where one would
|
|
59
|
-
* not fit (a paragraph out of a column into its columns layout).
|
|
60
|
-
* Items leaving a container never take the items after them:
|
|
61
|
-
* the list splits around them, Notion's way — outdented first items go
|
|
62
|
-
* before it, last ones after it; from the middle, the list keeps the
|
|
63
|
-
* items after them and a new list of its kind takes the ones before
|
|
64
|
-
* them, so an item a peer appends meanwhile stays with the items it
|
|
65
|
-
* follows (`splitOut`, the split Turn into's `liftOut` makes). A
|
|
66
|
-
* container left with no child goes. One plan; refused as the
|
|
67
|
-
* move is. `ids`: the moved blocks.
|
|
68
|
-
*/
|
|
69
|
-
const unNestBlocks = (ids) => {
|
|
70
|
-
const moved = ids.map(ref);
|
|
71
|
-
const last = moved.at(-1);
|
|
72
|
-
const pos = last === undefined ? null : positionOf(last);
|
|
73
|
-
const ppos = pos?.parent != null ? positionOf(pos.parent) : null;
|
|
74
|
-
if (!ppos || moved.some((id) => positionOf(id)?.parent !== pos.parent))
|
|
75
|
-
return REFUSED;
|
|
76
|
-
const from = pos.parent;
|
|
77
|
-
if (!canPlace(moved, ppos.parent, (id) => settledKind(from, id, ppos.parent)))
|
|
78
|
-
return REFUSED;
|
|
79
|
-
const after = childrenIds(from).slice(pos.index + 1);
|
|
80
|
-
const retype = settle(from, moved, ppos.parent);
|
|
81
|
-
if (isContainer(from))
|
|
82
|
-
return splitOut(moved, [from], [], false, retype);
|
|
83
|
-
const writes = [...moveTo(moved, ppos.parent, ppos.index + 1, leaving(true)), ...retype];
|
|
84
|
-
const adopts = after.length > 0 &&
|
|
85
|
-
!isVoid(last) &&
|
|
86
|
-
!isIsland(last) &&
|
|
87
|
-
after.every((id) => fits(last, blockTypeOf(id)));
|
|
88
|
-
if (adopts)
|
|
89
|
-
writes.push(...move(after, last, Infinity));
|
|
90
|
-
return plan(moved, writes);
|
|
91
|
-
};
|
|
92
|
-
/** Outdent one block (`unNestBlocks`). */
|
|
93
|
-
const unNestBlock = (id) => unNestBlocks([id]);
|
|
94
|
-
/**
|
|
95
|
-
* Where a block of `kind` at `id`'s place lands: `id`'s parent, or the
|
|
96
|
-
* first one up that `kind` fits (`fits`: a list holds only its items),
|
|
97
|
-
* and the containers it leaves on the way, innermost first (`levels`).
|
|
98
|
-
* Its own kind keeps a block where it is, as a reorder
|
|
99
|
-
* does (`canPlace`); a block inserted after it (`after`) has no place
|
|
100
|
-
* yet.
|
|
101
|
-
*/
|
|
102
|
-
const landingOf = (id, kind, after = false) => {
|
|
103
|
-
const levels = [];
|
|
104
|
-
let parent = positionOf(id)?.parent ?? null;
|
|
105
|
-
const own = !after && blockTypeOf(id) === kind;
|
|
106
|
-
for (; !own && parent !== null && !fits(parent, kind); parent = positionOf(parent).parent)
|
|
107
|
-
levels.push(parent);
|
|
108
|
-
return { parent, levels };
|
|
109
|
-
};
|
|
110
|
-
/**
|
|
111
|
-
* Lift sibling blocks `ids` (document order) out of `levels`, the
|
|
112
|
-
* containers around them, innermost first: each level
|
|
113
|
-
* splits around them as an outdent splits one list — the blocks
|
|
114
|
-
* before them go to a new container of its kind, which the level above
|
|
115
|
-
* holds the same way, and the level keeps the ones after them, so an
|
|
116
|
-
* item a peer appends meanwhile stays with the items it follows; one
|
|
117
|
-
* left with no child goes. `specs` (new blocks) land right after them.
|
|
118
|
-
* With `keep`, `ids` stay before the split and only `specs` go out.
|
|
119
|
-
* `retype` (the caller's kind steps) joins the plan. The one split
|
|
120
|
-
* `unNestBlocks` and `liftOut` share. `ids`: the placed blocks.
|
|
121
|
-
*/
|
|
122
|
-
const splitOut = (ids, levels, specs, keep, retype = []) => {
|
|
123
|
-
const moved = keep ? [] : [...ids];
|
|
124
|
-
const placed = [...moved, ...specs.map((s) => s.id)];
|
|
125
|
-
const insert = (at, index, s) => s.length ? [{ op: 'insertBlocks', parent: at, index, specs: s }] : [];
|
|
126
|
-
const last = ids.at(-1);
|
|
127
|
-
const { parent, index } = positionOf(levels.at(-1) ?? last);
|
|
128
|
-
if (levels.length === 0)
|
|
129
|
-
return plan(placed, [...insert(parent, index + 1, specs), ...retype]);
|
|
130
|
-
// Bottom up: what stays before the split at each level (the level
|
|
131
|
-
// itself, or a new head holding it), and whether anything follows it.
|
|
132
|
-
const heads = [];
|
|
133
|
-
let before = null;
|
|
134
|
-
let follows = false;
|
|
135
|
-
// The block right after `last` (what the split ends at when `keep`).
|
|
136
|
-
let next;
|
|
137
|
-
let child = last;
|
|
138
|
-
for (const level of levels) {
|
|
139
|
-
const kids = childrenIds(level);
|
|
140
|
-
const at = kids.indexOf(child);
|
|
141
|
-
const stays = (keep && child === last) || (before !== null && !before.head);
|
|
142
|
-
const lead = kids.slice(0, at + (stays ? 1 : 0)).filter((k) => !moved.includes(k));
|
|
143
|
-
if (!follows && at + 1 < kids.length)
|
|
144
|
-
[follows, next] = [true, kids[at + 1]];
|
|
145
|
-
if ((lead.length > 0 || before?.head) && follows) {
|
|
146
|
-
const spec = sanitizeSpec({
|
|
147
|
-
id: newId('b'),
|
|
148
|
-
type: kindToCopy(level),
|
|
149
|
-
data: blockDataOf(level) ?? {}
|
|
150
|
-
});
|
|
151
|
-
heads.push({ spec, kids: lead, inner: before?.head });
|
|
152
|
-
before = { id: spec.id, head: spec };
|
|
153
|
-
}
|
|
154
|
-
else
|
|
155
|
-
before = lead.length > 0 ? { id: level } : null;
|
|
156
|
-
child = level;
|
|
157
|
-
}
|
|
158
|
-
// Written in one gap, each right after the block before it, as an
|
|
159
|
-
// after-record out of the level's slot or a before-record into the gap
|
|
160
|
-
// before it (`leaving`): the engine's sort decides their order against
|
|
161
|
-
// a peer's records there (fork patch YP22, `sourceOrder`: the block a
|
|
162
|
-
// record stands at, then its part), so a peer's heads, pieces, lifts
|
|
163
|
-
// and new blocks in the same gap keep the serial order whatever the
|
|
164
|
-
// client ids (`del.merge.container`). The head stands at the first block
|
|
165
|
-
// (with `keep`, at it: `with`), the blocks at themselves, the new
|
|
166
|
-
// blocks after the last (`after`) — or, kept before the block that
|
|
167
|
-
// follows, at that block (`kept`). The records are written last first,
|
|
168
|
-
// so each stands before the one written before it, in the order the
|
|
169
|
-
// sort gives them; the head and the new blocks are created in the
|
|
170
|
-
// innermost level right after the block they stand at (their home,
|
|
171
|
-
// hidden while their records show them) and moved out by them.
|
|
172
|
-
const outside = before !== null && !before.head;
|
|
173
|
-
const side = leaving(outside);
|
|
174
|
-
const gap = index + (outside ? 1 : 0);
|
|
175
|
-
const at = moved[0] ?? last;
|
|
176
|
-
const kids0 = childrenIds(levels[0]);
|
|
177
|
-
const home = kids0.indexOf(last) + 1;
|
|
178
|
-
const specIds = specs.map((sp) => sp.id);
|
|
179
|
-
const [stands, part] = keep && !outside ? [next, PART.kept] : [last, PART.after];
|
|
180
|
-
// One record per run of adjacent blocks, each standing at its first: a
|
|
181
|
-
// block a peer's move takes out of a run leaves the others at their own place.
|
|
182
|
-
const runs = [];
|
|
183
|
-
for (const id of moved) {
|
|
184
|
-
const run = runs.at(-1);
|
|
185
|
-
if (run !== undefined && kids0.indexOf(run.at(-1)) + 1 === kids0.indexOf(id))
|
|
186
|
-
run.push(id);
|
|
187
|
-
else
|
|
188
|
-
runs.push([id]);
|
|
189
|
-
}
|
|
190
|
-
const writes = [
|
|
191
|
-
...insert(levels[0], home, specs),
|
|
192
|
-
...(before?.head ? insert(levels[0], home, [before.head]) : []),
|
|
193
|
-
...moveTo(specIds, parent, gap, side, stands, part),
|
|
194
|
-
...runs.reverse().flatMap((run) => moveTo(run, parent, gap, side, run[0], PART.self)),
|
|
195
|
-
...(before?.head
|
|
196
|
-
? moveTo([before.head.id], parent, gap, side, at, keep ? PART.with : PART.head)
|
|
197
|
-
: [])
|
|
198
|
-
];
|
|
199
|
-
for (const { spec, kids, inner } of heads.reverse()) {
|
|
200
|
-
writes.push(...moveTo(kids, spec.id, 0), ...(inner ? insert(spec.id, kids.length, [inner]) : []));
|
|
201
|
-
}
|
|
202
|
-
writes.push(...retype);
|
|
203
|
-
void next;
|
|
204
|
-
return plan(placed, emptying(levels[0], moved, writes, landing(parent)));
|
|
205
|
-
};
|
|
206
|
-
/**
|
|
207
|
-
* Place `id` where a block of `kind` fits, in one plan:
|
|
208
|
-
* out of every container around it that `kind` does not fit
|
|
209
|
-
* (`landingOf`: a list, and a list holding that list directly, for a
|
|
210
|
-
* heading or a divider), each split around it (`splitOut`). `after`
|
|
211
|
-
* (new blocks) lands right after it. With `keep`, `id` stays and only
|
|
212
|
-
* `after` goes out, the split right after `id` (a divider inserted
|
|
213
|
-
* after an item). Its own kind never moves it. The kinds are the
|
|
214
|
-
* caller's: it composes the retype (`setBlock`). Refused where `id`
|
|
215
|
-
* may not move there (`canPlace`). `ids`: the placed blocks.
|
|
216
|
-
*/
|
|
217
|
-
const liftOut = (id, kind, { keep = false, after = [] } = {}) => {
|
|
218
|
-
id = ref(id);
|
|
219
|
-
kind = ref(kind);
|
|
220
|
-
const specs = after.map(sanitizeSpec);
|
|
221
|
-
const { parent, levels } = landingOf(id, kind, keep);
|
|
222
|
-
if (!positionOf(id) || collides(doc, specs) || !canPlace([id], parent, () => kind))
|
|
223
|
-
return REFUSED;
|
|
224
|
-
return splitOut([id], levels, specs, keep);
|
|
225
|
-
};
|
|
226
|
-
/**
|
|
227
|
-
* Explicit fresh-identity copy of a subtree (paste / drag-clone):
|
|
228
|
-
* serializes `id`, remaps every block and inline atom id through
|
|
229
|
-
* `freshId`, inserts the copy right after `id`. Text atoms get new
|
|
230
|
-
* identity too — duplication is a creation op, not a relocation. An
|
|
231
|
-
* inline id `freshId` does not answer (a block-only callback) is
|
|
232
|
-
* minted; a block id it does not answer refuses the copy. `ids`: the
|
|
233
|
-
* copy's root.
|
|
234
|
-
*/
|
|
235
|
-
const duplicateBlock = (id, freshId = (_, kind) => newId(kind === 'block' ? 'b' : 'i')) => {
|
|
236
|
-
id = ref(id);
|
|
237
|
-
const pos = positionOf(id);
|
|
238
|
-
if (pos === null)
|
|
239
|
-
return REFUSED;
|
|
240
|
-
const spec = (b) => {
|
|
241
|
-
const fresh = freshId(b, 'block');
|
|
242
|
-
if (typeof fresh !== 'string')
|
|
243
|
-
return null;
|
|
244
|
-
const content = contentItems(b).map((item) => {
|
|
245
|
-
if (item.kind !== 'inline')
|
|
246
|
-
return item;
|
|
247
|
-
const atom = freshId(item.id, 'inline');
|
|
248
|
-
return { ...item, id: typeof atom === 'string' ? atom : newId('i') };
|
|
249
|
-
});
|
|
250
|
-
const children = childrenIds(b).map(spec);
|
|
251
|
-
if (children.includes(null))
|
|
252
|
-
return null;
|
|
253
|
-
const data = blockDataOf(b);
|
|
254
|
-
return {
|
|
255
|
-
id: fresh,
|
|
256
|
-
type: kindToCopy(b),
|
|
257
|
-
...(data !== undefined && { data }),
|
|
258
|
-
content,
|
|
259
|
-
children: children
|
|
260
|
-
};
|
|
261
|
-
};
|
|
262
|
-
const copy = spec(id);
|
|
263
|
-
return copy ? insertBlocks({ parent: pos.parent, index: pos.index + 1 }, [copy]) : REFUSED;
|
|
264
|
-
};
|
|
265
|
-
return {
|
|
266
|
-
insertBlocks,
|
|
267
|
-
moveBlocks,
|
|
268
|
-
unNestBlocks,
|
|
269
|
-
unNestBlock,
|
|
270
|
-
landingOf,
|
|
271
|
-
liftOut,
|
|
272
|
-
duplicateBlock
|
|
273
|
-
};
|
|
274
|
-
};
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { type BlockId, type SplitTail } from '../placement/model.js';
|
|
2
|
-
import type { Prepared } from './types.js';
|
|
3
|
-
import type { OpsContext } from './steps.js';
|
|
4
|
-
/** The split-merge ops of one facade, prepared. */
|
|
5
|
-
export declare const splitMergeOps: (c: OpsContext) => {
|
|
6
|
-
splitBlock: (id: BlockId, offset: number, newId: BlockId, tail?: SplitTail) => Prepared;
|
|
7
|
-
mergeBlocks: (fromId: BlockId, intoId: BlockId) => Prepared;
|
|
8
|
-
mergeBackward: (id: BlockId) => Prepared;
|
|
9
|
-
mergeForward: (id: BlockId) => Prepared;
|
|
10
|
-
};
|