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,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
- };
@@ -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>;
@@ -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
- };
@@ -1,178 +0,0 @@
1
- /**
2
- * Layout ops (`layout.*`): the layout kind a new layout takes, where a
3
- * beside placement lands (`besideAt`), and the two layout-making ops,
4
- * `placeBeside` and `wrapInLayout`.
5
- */
6
- import { id as newId } from '../../utils.js';
7
- import { sanitizeSpec } from '../../utils/json.js';
8
- import { ref } from './plan.js';
9
- /** The layout ops of one facade, prepared. */
10
- export const layoutOps = (c) => {
11
- const { roles, view, blockTypeOf, childrenIds, positionOf, ancestorsOf, isIsland, isLines, insideIsland, isLayout, isLayoutItem, holdsLayout, insideItem, live, canPlace, isContainer, fits, fitsIn, REFUSED, plan, moveTo, landing, emptied } = c;
12
- /** The layout kind a new layout takes: `kind` when it is one, else the only one the roles declare. */
13
- const layoutKind = (kind) => {
14
- if (kind !== undefined)
15
- return roles.of(kind).layout === undefined ? undefined : kind;
16
- const kinds = roles.declared().layouts;
17
- return kinds.length === 1 ? kinds[0] : undefined;
18
- };
19
- /** A fresh empty block of `type`. */
20
- const spec = (type) => sanitizeSpec({ id: newId('b'), type, data: {} });
21
- /**
22
- * The block a beside placement at `target` stands beside
23
- * (`layout.place-beside`): a layout or an item for itself; a block whose
24
- * parent shows only its children (a list item → its list, at any depth)
25
- * or is an island of lines (a code line → its code block) for that
26
- * parent; then the block itself when it sits directly in an item or at
27
- * the root, or where a new layout of `kind` (else the only layout kind)
28
- * fits beside it (a toggle's, a callout's or a nested block's child,
29
- * outside any item and island); else its outermost block below the
30
- * root or an item.
31
- */
32
- const besideAt = (target, kind) => {
33
- let at = ref(target);
34
- if (isLayout(at) || isLayoutItem(at))
35
- return at;
36
- for (let parent = positionOf(at)?.parent ?? null; parent !== null && !isLayoutItem(parent) && (isContainer(parent) || isLines(parent)); parent = positionOf(parent)?.parent ?? null)
37
- at = parent;
38
- const parent = positionOf(at)?.parent ?? null;
39
- if (parent === null || isLayoutItem(parent))
40
- return at;
41
- const wrap = layoutKind(kind);
42
- if (wrap !== undefined &&
43
- fits(parent, wrap) &&
44
- !insideItem(parent) &&
45
- !isIsland(parent) &&
46
- !insideIsland(parent))
47
- return at;
48
- for (let up = parent; up !== null && !isLayoutItem(up); up = positionOf(up).parent)
49
- at = up;
50
- return at;
51
- };
52
- /**
53
- * Place `ids` beside `target`, to its `side` (`layout.place-beside`: a
54
- * block dragged to another block's left or right edge). The target
55
- * resolves to its outermost block below the root or below a layout
56
- * item (a list item → its list, a code line → its code block); an item
57
- * stands for itself, a layout for its first or last slot. Beside a
58
- * block in an item, a new item holding `ids` goes beside that item;
59
- * otherwise the block and a new item holding `ids` are wrapped in a new
60
- * layout of the layout `kind` (else the only one the roles declare) at
61
- * its place. Refused when `ids` holds the target or an ancestor of it,
62
- * an item, a layout or a block holding one, a block that does not
63
- * fit an item, a block or a layout inside an island, and, wrapping,
64
- * when there is no layout kind. The sources are cleaned in the same
65
- * plan (`emptying`, `dissolving`); a plain move. `ids`: the moved
66
- * blocks.
67
- */
68
- const placeBeside = (ids, target, side, kind) => {
69
- const moved = ids.map(ref);
70
- target = ref(target);
71
- const v = view();
72
- if ((side !== 'left' && side !== 'right') || !canPlace(moved) || !live(target))
73
- return REFUSED;
74
- if ([target, ...ancestorsOf(target, v)].some((a) => moved.includes(a)))
75
- return REFUSED;
76
- if (moved.some((id) => isLayoutItem(id) || holdsLayout(id)))
77
- return REFUSED;
78
- const right = side === 'right';
79
- // Where the new item goes: beside an item of a layout, or a new layout wrapping `at`.
80
- const at = besideAt(target, kind && ref(kind));
81
- let layout = null;
82
- let index = 0;
83
- if (isLayout(at))
84
- [layout, index] = [at, right ? childrenIds(at).length : 0];
85
- else {
86
- const item = isLayoutItem(at) ? at : positionOf(at).parent;
87
- if (item !== null) {
88
- const pos = positionOf(item);
89
- [layout, index] = [pos.parent, pos.index + (right ? 1 : 0)];
90
- }
91
- }
92
- const wrap = layout === null ? layoutKind(kind && ref(kind)) : blockTypeOf(layout);
93
- const item = wrap === undefined ? undefined : roles.of(wrap).layout;
94
- if (wrap === undefined || item === undefined)
95
- return REFUSED;
96
- if (moved.some((id) => !fitsIn(item, blockTypeOf(id))))
97
- return REFUSED;
98
- if (layout !== null && (isIsland(layout) || insideIsland(layout, v)))
99
- return REFUSED;
100
- const fresh = spec(item);
101
- const into = (parent) => moveTo(moved, parent, 0);
102
- let writes;
103
- let kept;
104
- if (layout !== null) {
105
- writes = [{ op: 'insertBlocks', parent: layout, index, specs: [fresh] }, ...into(fresh.id)];
106
- kept = new Set([fresh.id, ...landing(layout)]);
107
- }
108
- else {
109
- const pos = positionOf(at);
110
- const host = spec(item);
111
- const wrapper = { ...spec(wrap), children: right ? [host, fresh] : [fresh, host] };
112
- writes = [
113
- { op: 'insertBlocks', parent: pos.parent, index: pos.index, specs: [wrapper] },
114
- ...moveTo([at], host.id, 0),
115
- ...into(fresh.id)
116
- ];
117
- kept = new Set([wrapper.id, host.id, fresh.id, ...landing(pos.parent)]);
118
- }
119
- return plan(moved, emptied(moved, writes, kept));
120
- };
121
- /**
122
- * Wrap sibling blocks in a new layout of `columns` items (default: one
123
- * per block), at the first one's place (`layout.wrap`: Turn into N
124
- * columns, as Notion): the blocks fill the first items, one each, in
125
- * document order; each item after them holds one empty block of the
126
- * item's default child (one block turned into 3 columns: it, then two
127
- * empty columns). The layout is of the layout `kind` (else the only one
128
- * the roles declare). Refused for no block, fewer than two items or
129
- * fewer items than blocks, blocks of different parents, an item, a
130
- * layout or a block holding one, where the layout does not fit
131
- * (`fits`: in a list, a code block) or lands inside an item or an
132
- * island, and for a block that does not fit an item. A plain move.
133
- * `ids`: the new layout.
134
- */
135
- const wrapInLayout = (ids, kind, columns = ids.length) => {
136
- const blocks = ids.map(ref);
137
- const v = view();
138
- if (!Number.isInteger(columns) || columns < 2 || blocks.length < 1)
139
- return REFUSED;
140
- if (columns < blocks.length || !canPlace(blocks))
141
- return REFUSED;
142
- const parent = positionOf(blocks[0]).parent;
143
- if (blocks.some((id) => positionOf(id).parent !== parent))
144
- return REFUSED;
145
- if (blocks.some((id) => isLayoutItem(id) || holdsLayout(id)))
146
- return REFUSED;
147
- const wrap = layoutKind(kind && ref(kind));
148
- const item = wrap === undefined ? undefined : roles.of(wrap).layout;
149
- if (wrap === undefined || item === undefined || !fits(parent, wrap))
150
- return REFUSED;
151
- if (parent !== null && (insideItem(parent, v) || isIsland(parent) || insideIsland(parent, v)))
152
- return REFUSED;
153
- if (blocks.some((id) => !fitsIn(item, blockTypeOf(id))))
154
- return REFUSED;
155
- const fill = roles.of(item).child;
156
- if (columns > blocks.length && !fitsIn(item, fill))
157
- return REFUSED;
158
- const ordered = blocks.toSorted((a, b) => positionOf(a).index - positionOf(b).index);
159
- const items = Array.from({ length: columns }, (_, i) => i < ordered.length ? spec(item) : { ...spec(item), children: [spec(fill)] });
160
- const wrapper = { ...spec(wrap), children: items };
161
- const at = positionOf(ordered[0]).index;
162
- const writes = [
163
- { op: 'insertBlocks', parent, index: at, specs: [wrapper] },
164
- ...ordered.flatMap((id, i) => moveTo([id], items[i].id, 0))
165
- ];
166
- const kept = new Set([
167
- wrapper.id,
168
- ...items.map((it) => it.id),
169
- ...landing(parent)
170
- ]);
171
- return plan([wrapper.id], emptied(ordered, writes, kept));
172
- };
173
- return {
174
- besideAt,
175
- placeBeside,
176
- wrapInLayout
177
- };
178
- };
@@ -1,19 +0,0 @@
1
- import { type JSONBlock, type JSONInlineBlock, type JSONText } from '../../utils/json.js';
2
- import { type DataPatch } from '../data.js';
3
- import { type BlockId, type BlockSpec, type ContentItem } from '../placement/model.js';
4
- import type { Prepared, PlanStep, DataTarget } from './types.js';
5
- import type { OpsContext } from './steps.js';
6
- /** The meta ops of one facade, prepared. */
7
- export declare const metaOps: (c: OpsContext) => {
8
- dataSteps: (target: DataTarget, patches: DataPatch[]) => PlanStep[] | null;
9
- replaceData: (data: unknown) => DataPatch[];
10
- setBlockType: (id: BlockId, type: string) => Prepared;
11
- patchData: (target: DataTarget, patches: readonly DataPatch[]) => Prepared;
12
- setBlock: (id: BlockId, value: {
13
- type?: string;
14
- data?: Record<string, unknown>;
15
- content?: (ContentItem | JSONText | JSONInlineBlock)[];
16
- children?: (BlockSpec | JSONBlock)[];
17
- }) => Prepared;
18
- };
19
- export type MetaOps = ReturnType<typeof metaOps>;