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.
Files changed (75) hide show
  1. package/README.md +2 -2
  2. package/dist/cloudflare/room/context.d.ts +62 -74
  3. package/dist/crdt/doc/history.js +2 -3
  4. package/dist/crdt/doc/ops.d.ts +81 -0
  5. package/dist/crdt/doc/ops.js +1898 -0
  6. package/dist/crdt/edytor-doc.d.ts +156 -156
  7. package/dist/crdt/edytor-doc.js +14 -42
  8. package/dist/crdt/flow.d.ts +4 -46
  9. package/dist/crdt/flow.js +2 -262
  10. package/dist/crdt/index.d.ts +62 -74
  11. package/dist/crdt/operations.d.ts +1 -1
  12. package/dist/crdt/placement/resolve.d.ts +1 -1
  13. package/dist/crdt/protocols/awareness.js +46 -81
  14. package/dist/crdt/providers/index.d.ts +10 -22
  15. package/dist/crdt/providers/indexeddb.d.ts +6 -10
  16. package/dist/crdt/providers/indexeddb.js +10 -16
  17. package/dist/crdt/providers/room.d.ts +11 -0
  18. package/dist/crdt/providers/room.js +26 -12
  19. package/dist/crdt/providers/websocket.d.ts +6 -12
  20. package/dist/crdt/providers/websocket.js +8 -15
  21. package/dist/crdt/rangeDelete.d.ts +2 -47
  22. package/dist/crdt/rangeDelete.js +1 -169
  23. package/dist/crdt/text/index/build.d.ts +125 -0
  24. package/dist/crdt/text/index/build.js +2895 -0
  25. package/dist/crdt/text/index/shared.d.ts +2 -6
  26. package/dist/crdt/text/index/shared.js +2 -5
  27. package/dist/crdt/text/model.js +2 -3
  28. package/dist/crdt/text/runs.d.ts +9 -12
  29. package/dist/crdt/text/runs.js +2 -165
  30. package/dist/events/domTextOffset.d.ts +2 -0
  31. package/dist/events/domTextOffset.js +8 -0
  32. package/dist/export/forms.d.ts +50 -50
  33. package/dist/plugins/richtext/RichTextPlugin.svelte +63 -61
  34. package/dist/plugins/richtext/richTextOperations.js +26 -32
  35. package/dist/selection/selection.utils.js +5 -8
  36. package/dist/session/handles.js +3 -7
  37. package/dist/surface/observer.svelte.js +15 -30
  38. package/dist/surface/projector.svelte.js +2 -2
  39. package/dist/utils.d.ts +5 -0
  40. package/dist/utils.js +7 -0
  41. package/package.json +1 -1
  42. package/dist/crdt/doc/capability.d.ts +0 -56
  43. package/dist/crdt/doc/capability.js +0 -207
  44. package/dist/crdt/doc/content.d.ts +0 -16
  45. package/dist/crdt/doc/content.js +0 -77
  46. package/dist/crdt/doc/delete.d.ts +0 -15
  47. package/dist/crdt/doc/delete.js +0 -84
  48. package/dist/crdt/doc/layout.d.ts +0 -9
  49. package/dist/crdt/doc/layout.js +0 -178
  50. package/dist/crdt/doc/meta.d.ts +0 -19
  51. package/dist/crdt/doc/meta.js +0 -198
  52. package/dist/crdt/doc/moves.d.ts +0 -21
  53. package/dist/crdt/doc/moves.js +0 -274
  54. package/dist/crdt/doc/split-merge.d.ts +0 -10
  55. package/dist/crdt/doc/split-merge.js +0 -179
  56. package/dist/crdt/doc/steps.d.ts +0 -65
  57. package/dist/crdt/doc/steps.js +0 -311
  58. package/dist/crdt/text/index/cache.d.ts +0 -16
  59. package/dist/crdt/text/index/cache.js +0 -175
  60. package/dist/crdt/text/index/claims.d.ts +0 -18
  61. package/dist/crdt/text/index/claims.js +0 -203
  62. package/dist/crdt/text/index/fold.d.ts +0 -21
  63. package/dist/crdt/text/index/fold.js +0 -656
  64. package/dist/crdt/text/index/layout.d.ts +0 -22
  65. package/dist/crdt/text/index/layout.js +0 -258
  66. package/dist/crdt/text/index/placement.d.ts +0 -47
  67. package/dist/crdt/text/index/placement.js +0 -738
  68. package/dist/crdt/text/index/records.d.ts +0 -13
  69. package/dist/crdt/text/index/records.js +0 -139
  70. package/dist/crdt/text/index/report.d.ts +0 -18
  71. package/dist/crdt/text/index/report.js +0 -337
  72. package/dist/crdt/text/index/state.d.ts +0 -197
  73. package/dist/crdt/text/index/state.js +0 -116
  74. package/dist/crdt/text/index/streams.d.ts +0 -25
  75. package/dist/crdt/text/index/streams.js +0 -248
@@ -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
- };
@@ -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>;
@@ -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
- };