edytor 4.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/README.md +2 -2
  2. package/dist/block/block.svelte.js +6 -21
  3. package/dist/block/inlineBlock.svelte.js +4 -6
  4. package/dist/clipboard/moveText.js +6 -8
  5. package/dist/cloudflare/DocumentRoom.d.ts +10 -3
  6. package/dist/cloudflare/index.d.ts +1 -1
  7. package/dist/cloudflare/room/access.d.ts +8 -3
  8. package/dist/cloudflare/room/access.js +44 -20
  9. package/dist/cloudflare/room/admission.js +28 -5
  10. package/dist/cloudflare/room/comments.js +28 -1
  11. package/dist/cloudflare/room/context.d.ts +106 -109
  12. package/dist/cloudflare/room/context.js +10 -3
  13. package/dist/cloudflare/room/forged.d.ts +14 -0
  14. package/dist/cloudflare/room/forged.js +30 -2
  15. package/dist/cloudflare/room/frames.d.ts +4 -0
  16. package/dist/cloudflare/room/frames.js +5 -1
  17. package/dist/cloudflare/room/purge.js +2 -1
  18. package/dist/cloudflare/room/replicas.d.ts +4 -2
  19. package/dist/cloudflare/room/replicas.js +8 -3
  20. package/dist/cloudflare/room/suggest.d.ts +7 -0
  21. package/dist/cloudflare/room/suggest.js +143 -0
  22. package/dist/cloudflare/routeDocumentSocket.d.ts +10 -1
  23. package/dist/cloudflare/routeDocumentSocket.js +5 -3
  24. package/dist/components/Content.svelte +9 -1
  25. package/dist/crdt/anchors.js +3 -2
  26. package/dist/crdt/attribution/attribution.d.ts +3 -0
  27. package/dist/crdt/attribution/attribution.js +3 -3
  28. package/dist/crdt/doc/funnel.d.ts +1 -1
  29. package/dist/crdt/doc/funnel.js +5 -2
  30. package/dist/crdt/doc/gate.d.ts +1 -1
  31. package/dist/crdt/doc/history.d.ts +6 -0
  32. package/dist/crdt/doc/history.js +79 -59
  33. package/dist/crdt/doc/ops.d.ts +81 -0
  34. package/dist/crdt/doc/ops.js +1898 -0
  35. package/dist/crdt/doc/reads.d.ts +2 -2
  36. package/dist/crdt/doc/reads.js +18 -28
  37. package/dist/crdt/doc/suggest.d.ts +167 -0
  38. package/dist/crdt/doc/suggest.js +579 -0
  39. package/dist/crdt/document.d.ts +10 -1
  40. package/dist/crdt/document.js +19 -11
  41. package/dist/crdt/edytor-doc.d.ts +294 -272
  42. package/dist/crdt/edytor-doc.js +123 -108
  43. package/dist/crdt/engine-api.d.ts +8 -2
  44. package/dist/crdt/engine.d.ts +2 -1
  45. package/dist/crdt/engine.js +3 -1
  46. package/dist/crdt/flow.d.ts +5 -47
  47. package/dist/crdt/flow.js +3 -269
  48. package/dist/crdt/index.d.ts +102 -108
  49. package/dist/crdt/index.js +3 -0
  50. package/dist/crdt/operations.d.ts +13 -4
  51. package/dist/crdt/parts.d.ts +43 -0
  52. package/dist/crdt/parts.js +44 -0
  53. package/dist/crdt/placement/display.d.ts +1 -1
  54. package/dist/crdt/placement/display.js +3 -3
  55. package/dist/crdt/placement/keys.d.ts +5 -0
  56. package/dist/crdt/placement/keys.js +5 -0
  57. package/dist/crdt/placement/resolve.d.ts +1 -1
  58. package/dist/crdt/placement/resolve.js +2 -1
  59. package/dist/crdt/protocol.d.ts +1 -1
  60. package/dist/crdt/protocol.js +1 -1
  61. package/dist/crdt/protocols/auth.d.ts +11 -1
  62. package/dist/crdt/protocols/auth.js +17 -1
  63. package/dist/crdt/protocols/awareness.js +46 -81
  64. package/dist/crdt/protocols/comments.d.ts +16 -1
  65. package/dist/crdt/protocols/comments.js +9 -1
  66. package/dist/crdt/providers/index.d.ts +12 -22
  67. package/dist/crdt/providers/index.js +3 -1
  68. package/dist/crdt/providers/indexeddb.d.ts +6 -10
  69. package/dist/crdt/providers/indexeddb.js +10 -16
  70. package/dist/crdt/providers/room.d.ts +11 -0
  71. package/dist/crdt/providers/room.js +26 -12
  72. package/dist/crdt/providers/websocket.d.ts +8 -12
  73. package/dist/crdt/providers/websocket.js +17 -19
  74. package/dist/crdt/purge.d.ts +7 -1
  75. package/dist/crdt/purge.js +32 -2
  76. package/dist/crdt/rangeDelete.d.ts +2 -47
  77. package/dist/crdt/rangeDelete.js +1 -169
  78. package/dist/crdt/restore.d.ts +2 -1
  79. package/dist/crdt/restore.js +9 -0
  80. package/dist/crdt/schema.d.ts +9 -1
  81. package/dist/crdt/schema.js +13 -3
  82. package/dist/crdt/structs.d.ts +2 -0
  83. package/dist/crdt/structs.js +2 -0
  84. package/dist/crdt/text/index/build.d.ts +125 -0
  85. package/dist/crdt/text/index/build.js +2895 -0
  86. package/dist/crdt/text/index/shared.d.ts +2 -6
  87. package/dist/crdt/text/index/shared.js +19 -19
  88. package/dist/crdt/text/model.d.ts +10 -0
  89. package/dist/crdt/text/model.js +10 -4
  90. package/dist/crdt/text/runs.d.ts +43 -42
  91. package/dist/crdt/text/runs.js +2 -165
  92. package/dist/crdt/vendor/yjs/UPSTREAM.md +83 -0
  93. package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
  94. package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +6 -3
  95. package/dist/crdt/vendor/yjs/dts/ynode.d.ts +13 -2
  96. package/dist/crdt/vendor/yjs/src/index.js +1 -1
  97. package/dist/crdt/vendor/yjs/src/structs/Item.js +44 -5
  98. package/dist/crdt/vendor/yjs/src/ynode.js +29 -7
  99. package/dist/edytor.svelte.d.ts +1 -1
  100. package/dist/edytor.svelte.js +100 -75
  101. package/dist/events/beforeInputCommands.d.ts +2 -2
  102. package/dist/events/domTextOffset.d.ts +2 -0
  103. package/dist/events/domTextOffset.js +8 -0
  104. package/dist/events/nativeInteractiveControl.d.ts +11 -1
  105. package/dist/events/nativeInteractiveControl.js +41 -1
  106. package/dist/events/onBeforeInput.js +3 -3
  107. package/dist/events/onFocus.js +9 -11
  108. package/dist/export/forms.d.ts +75 -75
  109. package/dist/index.d.ts +4 -2
  110. package/dist/index.js +1 -1
  111. package/dist/labels.d.ts +8 -0
  112. package/dist/labels.js +6 -1
  113. package/dist/plugins/comments/CommentsController.svelte.d.ts +5 -1
  114. package/dist/plugins/comments/CommentsController.svelte.js +14 -4
  115. package/dist/plugins/find/search.js +2 -2
  116. package/dist/plugins/index.d.ts +1 -0
  117. package/dist/plugins/index.js +1 -0
  118. package/dist/plugins/richtext/RichTextPlugin.svelte +63 -61
  119. package/dist/plugins/richtext/richTextOperations.js +26 -32
  120. package/dist/plugins/suggestions/SuggestionBars.svelte +40 -23
  121. package/dist/plugins/suggestions/SuggestionBars.svelte.d.ts +3 -1
  122. package/dist/plugins/suggestions/SuggestionRuns.svelte +77 -0
  123. package/dist/plugins/suggestions/SuggestionRuns.svelte.d.ts +9 -0
  124. package/dist/plugins/suggestions/shared.svelte.d.ts +58 -0
  125. package/dist/plugins/suggestions/shared.svelte.js +421 -0
  126. package/dist/plugins/suggestions/suggestionsPlugin.d.ts +15 -4
  127. package/dist/selection/domSelection.d.ts +2 -0
  128. package/dist/selection/domSelection.js +6 -2
  129. package/dist/selection/pointer.svelte.js +75 -48
  130. package/dist/selection/replaceSelection.js +2 -6
  131. package/dist/selection/selection.svelte.d.ts +3 -3
  132. package/dist/selection/selection.svelte.js +39 -32
  133. package/dist/selection/selection.utils.d.ts +14 -0
  134. package/dist/selection/selection.utils.js +33 -11
  135. package/dist/session/commands.js +21 -11
  136. package/dist/session/composition.svelte.d.ts +5 -3
  137. package/dist/session/composition.svelte.js +21 -17
  138. package/dist/session/handles.d.ts +4 -17
  139. package/dist/session/handles.js +14 -44
  140. package/dist/session/navigation.js +6 -9
  141. package/dist/session/selection.d.ts +11 -5
  142. package/dist/session/selection.js +14 -48
  143. package/dist/session/suggestions.svelte.d.ts +38 -9
  144. package/dist/session/suggestions.svelte.js +37 -18
  145. package/dist/session/virtual.d.ts +1 -1
  146. package/dist/surface/attributes.d.ts +5 -0
  147. package/dist/surface/attributes.js +2 -2
  148. package/dist/surface/cells.d.ts +2 -26
  149. package/dist/surface/cells.js +2 -33
  150. package/dist/surface/observer.svelte.d.ts +6 -1
  151. package/dist/surface/observer.svelte.js +143 -111
  152. package/dist/surface/pin.svelte.d.ts +9 -9
  153. package/dist/surface/pin.svelte.js +15 -22
  154. package/dist/surface/projector.svelte.js +10 -16
  155. package/dist/text/text.svelte.js +14 -19
  156. package/dist/themes/notion.css +29 -0
  157. package/dist/utils.d.ts +5 -0
  158. package/dist/utils.js +7 -0
  159. package/package.json +3 -1
  160. package/dist/crdt/doc/capability.d.ts +0 -56
  161. package/dist/crdt/doc/capability.js +0 -211
  162. package/dist/crdt/doc/content.d.ts +0 -16
  163. package/dist/crdt/doc/content.js +0 -77
  164. package/dist/crdt/doc/delete.d.ts +0 -15
  165. package/dist/crdt/doc/delete.js +0 -84
  166. package/dist/crdt/doc/layout.d.ts +0 -9
  167. package/dist/crdt/doc/layout.js +0 -178
  168. package/dist/crdt/doc/meta.d.ts +0 -19
  169. package/dist/crdt/doc/meta.js +0 -199
  170. package/dist/crdt/doc/moves.d.ts +0 -21
  171. package/dist/crdt/doc/moves.js +0 -274
  172. package/dist/crdt/doc/split-merge.d.ts +0 -10
  173. package/dist/crdt/doc/split-merge.js +0 -179
  174. package/dist/crdt/doc/steps.d.ts +0 -65
  175. package/dist/crdt/doc/steps.js +0 -311
  176. package/dist/crdt/text/index/cache.d.ts +0 -16
  177. package/dist/crdt/text/index/cache.js +0 -175
  178. package/dist/crdt/text/index/claims.d.ts +0 -18
  179. package/dist/crdt/text/index/claims.js +0 -225
  180. package/dist/crdt/text/index/fold.d.ts +0 -21
  181. package/dist/crdt/text/index/fold.js +0 -656
  182. package/dist/crdt/text/index/layout.d.ts +0 -24
  183. package/dist/crdt/text/index/layout.js +0 -271
  184. package/dist/crdt/text/index/placement.d.ts +0 -47
  185. package/dist/crdt/text/index/placement.js +0 -741
  186. package/dist/crdt/text/index/records.d.ts +0 -13
  187. package/dist/crdt/text/index/records.js +0 -139
  188. package/dist/crdt/text/index/report.d.ts +0 -18
  189. package/dist/crdt/text/index/report.js +0 -337
  190. package/dist/crdt/text/index/state.d.ts +0 -197
  191. package/dist/crdt/text/index/state.js +0 -116
  192. package/dist/crdt/text/index/streams.d.ts +0 -25
  193. package/dist/crdt/text/index/streams.js +0 -248
@@ -0,0 +1,1898 @@
1
+ /**
2
+ * The prepared ops of one facade, in one closure over its reads: the
3
+ * structural capability (`canPlace`, `fits`, `canMerge`: one answer in
4
+ * advance and at execution), the step writers every op composes, and the
5
+ * op families — placement (insert, move, outdent, lift), layout, split and
6
+ * merge, block deletion, meta (type and data) and content (text, atoms,
7
+ * marks) — then range deletion (`del.range.*`) and flow placement
8
+ * (`flow.*`), which compose them; the types they read are in
9
+ * `../rangeDelete.ts` and `../flow.ts`.
10
+ *
11
+ * ── Island/void enforcement (operation-layer, not stored) ──────────────
12
+ *
13
+ * Roles are resolved operation-time via `config.roleOf(type)`, through one
14
+ * role table (`DisplayRoles`) that the index's display and every guard
15
+ * below ask of a block's shown kind — the editor derives them from plugin
16
+ * block definitions, exactly like the baseline's
17
+ * `block.definition.island/void`. They are deliberately NOT replicated block
18
+ * data: the schema stays policy-free, and a replica with different plugins
19
+ * reads the same document. Enforced rules (baseline semantics, see
20
+ * `plugins.ts` + `block.utils.ts`):
21
+ *
22
+ * - `void`: cannot accept children (insert/move/nest into void rejected),
23
+ * cannot merge either direction, cannot be split. Content edits ARE
24
+ * allowed — void rendered content (captions) stays editable per the
25
+ * baseline contract. Deletion is allowed (delete is not editing).
26
+ * - `island`: editable content, but its subtree is structurally sealed —
27
+ * a block inside an island (`insideIsland`) cannot be moved, nested,
28
+ * unnested, or merged across the island boundary. Merges INSIDE one
29
+ * island are allowed; merging an island child into the island itself is
30
+ * allowed when the island renders its content. Moving or merging
31
+ * INTO an island subtree is rejected. An island declared `lines` (code)
32
+ * holds only lines of its `defaultChild` kind, and a line holds no
33
+ * children (the display enforces it against undo,
34
+ * `insertBlocks` refuses a line parent); any other island keeps its
35
+ * structure. `canPlace` and `canMerge` are the one answer, asked in
36
+ * advance or by the ops.
37
+ * - Island merge: when an island block itself is merged (backward or
38
+ * forward), its children are unnested to the vacated sibling slot and
39
+ * reset to the default child of that slot's parent (`defaultChild`).
40
+ * - Baseline merges NEVER adopt the merged block's children — they unnest
41
+ * to the vacated slot. The facade exposes both: `mergeBlocks` is the
42
+ * engine primitive (children adopt into the target
43
+ * by staying under the source, which the target's claim displays),
44
+ * `mergeBackward`/`mergeForward` reproduce the baseline command shape.
45
+ */
46
+ import { id as newId } from '../../utils.js';
47
+ import { asBlockSpec, asContentItem, jsonEquals, sanitizeSpec, sanitizeWireJson } from '../../utils/json.js';
48
+ import { isIncarnationId } from '../incarnations.js';
49
+ import { randOf } from '../rand.js';
50
+ import { TYPE } from '../schema.js';
51
+ import { isObject, leafKey, patchWrites, readData } from '../data.js';
52
+ import { blockNodeOf, collides, isLiveIn, isSelfOrDescendant } from '../placement/model.js';
53
+ import { REFUSED, effectOf, ref, refused, sanitizeInline, sanitizeItem } from './plan.js';
54
+ import { placedEnd, lengthOf } from '../flow.js';
55
+ /**
56
+ * A record's order among the records standing at one block, for the
57
+ * engine's sort of two records at one origin (fork patch YP22): what a split
58
+ * of a list around a block writes there — a new head list holding the items
59
+ * before it (`head`), new blocks placed after the items before it, which
60
+ * stay (`kept`), a head holding those items and the block (`with`), the
61
+ * block itself leaving (`self`), new blocks after it (`after`) — in the
62
+ * order every serial run of the gestures gives (`del.merge.container`).
63
+ */
64
+ export const PART = { head: 0, kept: 1, with: 2, self: 3, after: 4 };
65
+ /** The prepared ops of one facade over its reads. */
66
+ export const docOps = (c) => {
67
+ const { doc, runsView, roles, roleOf, dataNode, view, blockTypeOf, blockDataOf, childrenIds, positionOf, ancestorsOf, isVoid, isIsland, isLines, isLine, islandOf, insideIsland, itemKindOf, isLayout, isLayoutItem, holdsLayout, insideItem, isTable, isTableRow, isRowKind, isCellKind, isTableCell, tableOf, next, previous, displayLength, contentItems, live, contentTarget } = c;
68
+ // ── structural capability: one answer in advance and at execution ──
69
+ /**
70
+ * May `ids` be placed under `parent` (`null` = the root)? Every id is
71
+ * live, distinct and outside any island interior (island subtrees are
72
+ * sealed); the destination is live, neither void nor an island nor
73
+ * inside one, and not inside any moved block's own subtree; and every
74
+ * block fits it as the kind `kindOf` gives (`fits`; a move keeps its
75
+ * kind) or already sits in it (a reorder changes nothing a list holds:
76
+ * an image shed into a list still moves among its items); and
77
+ * no layout, nor a block holding one, lands inside a layout item
78
+ * (`layout.nest`). A table's cell never moves, and its row only
79
+ * within its table (`table.fits`).
80
+ * Without a `parent`: may these blocks move at all (the drag
81
+ * affordance). The move ops refuse exactly when this answers `false`.
82
+ * (`insertBlock` is looser — island interiors are built by inserting
83
+ * into them.)
84
+ */
85
+ const canPlace = (ids, parent, kindOf = blockTypeOf) => {
86
+ const v = view();
87
+ if (ids.length === 0 || new Set(ids).size !== ids.length)
88
+ return false;
89
+ if (ids.some((id) => !isLiveIn(v, id) || insideIsland(id, v)))
90
+ return false;
91
+ // A cell moves only with its column, never as a block (`table.fits`).
92
+ if (ids.some((id) => isCellKind(blockTypeOf(id))))
93
+ return false;
94
+ // A row moves only within its table: a table's rows are reordered, never re-homed.
95
+ const rows = ids.filter((id) => isRowKind(blockTypeOf(id)));
96
+ if (rows.length > 0 && parent !== undefined)
97
+ if (parent === null || rows.some((id) => positionOf(id)?.parent !== parent))
98
+ return false;
99
+ if (parent === undefined || parent === null)
100
+ return true;
101
+ if (!isLiveIn(v, parent) || isVoid(parent))
102
+ return false;
103
+ const stays = (id) => positionOf(id)?.parent === parent;
104
+ if (!ids.every((id) => stays(id) || fits(parent, kindOf(id))))
105
+ return false;
106
+ if (insideItem(parent, v) && ids.some(holdsLayout))
107
+ return false;
108
+ return ![parent, ...ancestorsOf(parent, v)].some((a) => isIsland(a) || ids.includes(a));
109
+ };
110
+ /**
111
+ * A container: a block that shows only its children — no content of
112
+ * its own, neither void nor an island (a list, a table row, a column).
113
+ */
114
+ const isContainer = (id) => roles.of(blockTypeOf(id) ?? '').container;
115
+ /**
116
+ * THE container rule: may a block of `kind` sit directly
117
+ * under `parent` (`null` = the root)? A container whose default child is
118
+ * a kind of its own — its item (a list's `list-item`, a columns
119
+ * layout's `column`) — holds only its items, and containers of them
120
+ * when the item renders content (a list directly in a list, from JSON
121
+ * or the API; a layout holds no layout, `layout.fits`); one whose
122
+ * default child is the document's (a column) holds any block. Every
123
+ * structural placement asks it: a move is refused where its blocks do
124
+ * not fit (`canPlace`), Tab nests under a container's last item
125
+ * (`nestParent`), an outdent or a lift is refused where the block would
126
+ * not fit, and a block a merge, delete or range sheds into a container
127
+ * takes its item kind when it is a plain block (`fitted`). Explicit kind
128
+ * writes (`insertBlocks`, a retype) place what they are told (the
129
+ * view's Turn into places the kind where it fits first, `liftOut`); a
130
+ * plain block stored directly in a list still shows as its item (the
131
+ * index's `typeOf`), whatever write or race put it there.
132
+ */
133
+ const fits = (parent, kind) => parent === null || fitsIn(blockTypeOf(parent) ?? '', kind);
134
+ /** `fits`, by the parent's kind (a block not written yet: `placeBeside`'s new item). */
135
+ const fitsIn = (parentType, kind) => {
136
+ const { container, child: item } = roles.of(parentType);
137
+ if (!container || item === defaultChild(null) || kind === item)
138
+ return true;
139
+ const k = kind === undefined ? undefined : roles.of(kind);
140
+ return k?.container === true && k.child === item && roles.of(item).rendersContent;
141
+ };
142
+ /**
143
+ * `kind`, or — where a plain block (the document's default kind, or
144
+ * none: a pasted run) does not fit `parent` — `parent`'s item, when
145
+ * that renders content (a paragraph landing in a list is its item; one
146
+ * in a columns layout keeps its kind: its text never vanishes). Any
147
+ * other kind keeps its kind and data wherever a merge, a delete or a
148
+ * paste sheds it — an image under a bullet stays an image, a heading a
149
+ * heading, a to-do keeps its check — as a peer's
150
+ * concurrent promotion shows it (`typeOf` resets only the default
151
+ * kind); an outdent or a move that would place one directly in a list
152
+ * is refused (`fits`).
153
+ */
154
+ const fitted = (parent, kind) => {
155
+ if (fits(parent, kind) || (kind !== undefined && kind !== defaultChild(null)))
156
+ return kind;
157
+ const item = defaultChild(parent);
158
+ return roles.of(item).rendersContent ? item : kind;
159
+ };
160
+ /** 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). */
161
+ const emptiable = (id) => isContainer(id) && displayLength(id) === 0;
162
+ /**
163
+ * Where `ids` nest when nested into `parent` (Tab, a drop inside it):
164
+ * `parent`, or — a container they are no items of — its last child,
165
+ * and so on down. Tab after a list nests under its last item (Notion);
166
+ * a last child that holds no children (an image, a code block) is
167
+ * answered as it is, and `canPlace` refuses it, as Tab right under
168
+ * that block is refused.
169
+ */
170
+ const nestParent = (ids, parent) => {
171
+ let at = parent;
172
+ while (!ids.every((id) => fits(at, blockTypeOf(id)))) {
173
+ const last = childrenIds(at).at(-1);
174
+ if (last === undefined)
175
+ break;
176
+ at = last;
177
+ }
178
+ return at;
179
+ };
180
+ /**
181
+ * May `fromId`'s content merge into `intoId`? Both live and distinct,
182
+ * neither void, `intoId` renders its content (a list, a table row or a
183
+ * code block shows none, so a first item, cell or line never merges
184
+ * into it), `fromId` renders its own unless it is
185
+ * an island (a list or a row never merges as a whole: its items would
186
+ * leave it, `del.merge.container`), and the merge stays on one side of an island
187
+ * boundary (a block may merge into its own island root — that stays
188
+ * inside — but nothing from outside merges into an island).
189
+ */
190
+ const canMerge = (fromId, intoId) => {
191
+ const v = view();
192
+ if (fromId === intoId || !isLiveIn(v, fromId) || !isLiveIn(v, intoId))
193
+ return false;
194
+ if (isVoid(fromId) || isVoid(intoId))
195
+ return false;
196
+ // Nothing merges into or out of a table's cell (`table.merge`).
197
+ if (isCellKind(blockTypeOf(fromId)) || isCellKind(blockTypeOf(intoId)))
198
+ return false;
199
+ if (!rendersContent(intoId) || !(rendersContent(fromId) || isIsland(fromId)))
200
+ return false;
201
+ const islandFrom = islandOf(fromId, v);
202
+ if (intoId === islandFrom)
203
+ return true;
204
+ return islandFrom === islandOf(intoId, v) && !isIsland(intoId);
205
+ };
206
+ /** `id`'s shown kind renders its content. */
207
+ const rendersContent = (id) => roles.of(blockTypeOf(id) ?? '').rendersContent;
208
+ /** The adopted default child type under `parent` (`null` = the root). */
209
+ const defaultChild = (parent) => roles.of(parent === null ? null : (blockTypeOf(parent) ?? null)).child;
210
+ /**
211
+ * The kind a new block copies from `id` (a split tail, a flow's tail,
212
+ * a duplicate and each of its descendants). A type a peer's retype is
213
+ * replacing can be missing while its new value is pending: the copy
214
+ * then takes its parent's default child, never a missing type
215
+ * (it showed as `unknown` everywhere, for good).
216
+ */
217
+ const kindToCopy = (id) => blockTypeOf(id) ?? defaultChild(positionOf(id)?.parent ?? null);
218
+ // ── the step writers every op composes ──────────────────────────────
219
+ // The plan itself, moves (a move's place and side), type steps and the
220
+ // kind a block shows where it lands (`settle`), the containers and layouts
221
+ // a write leaves empty (`emptying`, `dissolving`), delete and merge steps,
222
+ // and display spans of a block's content.
223
+ // ── prepared ops ─────────────────────────────────────────────
224
+ // Every op is `prepare` (pure: `refused`, or a plan of named steps plus
225
+ // its effect summary, against the current version) then `apply(plan)`;
226
+ // composites compose their steps into one plan, so a hook sees the
227
+ // whole command before any write and a refusal refuses before one.
228
+ // Each prepare normalizes its inputs once, here at ingress: ids
229
+ // and strings as the wire would deliver them, payloads cloned.
230
+ /** The plan of `writes` touching `ids`; `at`, where it leaves the caret when it decides one. */
231
+ const plan = (ids, writes, at) => ({
232
+ ids,
233
+ writes,
234
+ effect: effectOf(writes),
235
+ version: runsView.version(),
236
+ ...(at && { at })
237
+ });
238
+ /** The step deleting `length` display units of `id` from `offset` (none for nothing). */
239
+ const cutText = (id, offset, length) => length > 0 ? [{ op: 'deleteText', id, offset, length }] : [];
240
+ /** The step inserting `specs` at `index` under `parent` (none for no spec). */
241
+ const insertAt = (parent, index, specs, split) => specs.length
242
+ ? [{ op: 'insertBlocks', parent, index, specs, ...(split !== undefined && { split }) }]
243
+ : [];
244
+ /**
245
+ * Move `ids` to display index `index` among `parent`'s children (the
246
+ * slot hooks see). `side`: the move record's class among the items of
247
+ * its origin (`placement/slots.ts`) — `0`, the blocks stand after what
248
+ * precedes them (a move; blocks leaving a block for the gap after it:
249
+ * its children, a list's last items); `1`, before what follows them
250
+ * (blocks leaving a block for the gap before it: a list's first item
251
+ * lifted out). In one gap every serial order of the gestures puts a
252
+ * split's pieces first, then the after-records, then the before-records
253
+ * (`del.merge.container`), which the engine's sort gives whatever the
254
+ * client ids.
255
+ * Every planned move carries the type steps that keep what the moved
256
+ * blocks show: one displayed out of an island as another kind than its
257
+ * stored one (`displayType`) gets that kind written, so leaving the
258
+ * island's slot never brings the island's child kind back.
259
+ * `at`, `part`: the block the record stands at and its order among the
260
+ * records standing there, for the engine's sort of two records at one
261
+ * origin (`PART`; by default the first moved block, as itself).
262
+ */
263
+ const moveTo = (ids, parent, index, side = 0, at, part) => ids.length === 0
264
+ ? []
265
+ : [
266
+ {
267
+ op: 'moveBlocks',
268
+ ids,
269
+ parent,
270
+ index,
271
+ ...(side === 1 && { side }),
272
+ ...(at !== undefined && { at }),
273
+ ...(part !== undefined && { part })
274
+ },
275
+ ...ids.flatMap((id) => {
276
+ const shown = runsView.displayType(id);
277
+ return shown === undefined ? [] : attr(id, TYPE, shown);
278
+ })
279
+ ];
280
+ /** Move `ids` to `index` among `parent`'s children. */
281
+ const move = (ids, parent, index) => moveTo(ids, parent, index);
282
+ /** The side of blocks leaving a block for the gap right after it (`after`) or right before it. */
283
+ const leaving = (after) => (after ? 0 : 1);
284
+ /**
285
+ * A type step, planned only when the kind differs (the one same-value
286
+ * guard). A kind counts as the same only when the block is stored and
287
+ * shown as it: a move in the same plan pins the kind a block shows
288
+ * (`moveTo`), so a paragraph shown as its list's item (`itemOf`) that
289
+ * an outdent settles back to a paragraph is written back.
290
+ */
291
+ const attr = (id, key, value) => {
292
+ const same = blockNodeOf(doc, id).getAttr(key) === value;
293
+ if (same && (runsView.displayType(id) ?? value) === value)
294
+ return [];
295
+ return [{ op: 'setBlockType', id, type: value }];
296
+ };
297
+ /**
298
+ * The kind `kid` shows once it leaves `from` for a slot under `parent`
299
+ * — the island and container rules, one answer:
300
+ * - an island's child takes `parent`'s default child (it leaves the
301
+ * island's kinds) — unless that renders no content while the child
302
+ * does: a line then takes the document's default kind (a code line
303
+ * shed into a columns layout is a paragraph), any other child
304
+ * keeps its kind, as `typeOf` shows one a peer adds meanwhile;
305
+ * - a container's item (its default child) takes `parent`'s default
306
+ * child — an item never shows outside its list — unless it
307
+ * stays inside an outer container of that kind (a nested list),
308
+ * or that kind renders no content (a column in a
309
+ * columns layout: its text would vanish);
310
+ * - then a plain block that does not fit `parent` (`fits`: a paragraph
311
+ * shed into a list) becomes its item, when that item renders content
312
+ * (`fitted`). A block that cannot (a paragraph in a columns
313
+ * layout) keeps its kind: its text never vanishes; any other kind (an
314
+ * image, a code block, a heading) keeps its kind.
315
+ */
316
+ /**
317
+ * The kind a child of `island` (now `kind`) takes where the default
318
+ * child is `to`: `to` — unless `to` renders no content while the child
319
+ * does (its text would vanish): a line then takes the document's
320
+ * default kind, any other child keeps its kind (as `typeOf`).
321
+ */
322
+ const leavingIsland = (island, kind, to) => {
323
+ if (roles.of(to).rendersContent || (kind !== undefined && !roles.of(kind).rendersContent))
324
+ return to;
325
+ return isLines(island) ? defaultChild(null) : kind;
326
+ };
327
+ const settledKind = (from, kid, parent) => {
328
+ let kind = blockTypeOf(kid);
329
+ if (from !== null && isIsland(from))
330
+ kind = leavingIsland(from, kind, defaultChild(parent));
331
+ else if (from !== null && isContainer(from) && kind === defaultChild(from)) {
332
+ const within = parent === null ? [] : [parent, ...ancestorsOf(parent)];
333
+ const to = defaultChild(parent);
334
+ // An outer container of its kind keeps it an item (a nested list); a
335
+ // column's default child is the document's: it holds no items of its own.
336
+ const outer = kind !== defaultChild(null) &&
337
+ within.some((a) => isContainer(a) && defaultChild(a) === kind);
338
+ if (!outer && roles.of(to).rendersContent)
339
+ kind = to;
340
+ }
341
+ return fitted(parent, kind);
342
+ };
343
+ /** The kind steps for the `kids` of `from` (`null`: the root) landing under `parent` (`settledKind`). */
344
+ const settle = (from, kids, parent) => kids.flatMap((kid) => {
345
+ const kind = settledKind(from, kid, parent);
346
+ return kind === undefined || kind === blockTypeOf(kid) ? [] : attr(kid, TYPE, kind);
347
+ });
348
+ /** `parent` and its display ancestors: where blocks landing under `parent` keep alive. */
349
+ const landing = (parent) => new Set(parent === null ? [] : [parent, ...ancestorsOf(parent)]);
350
+ /**
351
+ * `writes`, then every container one of `from` (the blocks' former
352
+ * parents) is left with no child but `leaving` removed
353
+ * (`del.range.empty-container`), and so on upward — never one of
354
+ * `kept`, and only an `emptiable` one (a container holding text of its
355
+ * own stays). The containers are found together, deepest first: a
356
+ * list that loses its last item along with a list nested in it goes
357
+ * too. Then every layout the plan leaves with one
358
+ * item or none dissolves (`dissolving`); `removed`: blocks the plan
359
+ * deletes besides (a block delete's members), which only that counts.
360
+ */
361
+ const emptyingAll = (from, leaving, writes, kept = new Set(), removed = []) => {
362
+ const gone = new Set(leaving);
363
+ const tops = [];
364
+ const deepest = [...new Set(from)]
365
+ .filter((c) => c !== null)
366
+ .sort((a, b) => ancestorsOf(b).length - ancestorsOf(a).length);
367
+ for (const start of deepest) {
368
+ let top = null;
369
+ for (let c = start; c !== null && !gone.has(c) && !kept.has(c) && emptiable(c); c = positionOf(c).parent) {
370
+ if (!childrenIds(c).every((kid) => gone.has(kid)))
371
+ break;
372
+ gone.add((top = c));
373
+ }
374
+ if (top !== null)
375
+ tops.push(top);
376
+ }
377
+ const roots = tops.filter((t) => !ancestorsOf(t).some((a) => tops.includes(a)));
378
+ const steps = [...writes, ...roots.map((t) => remove(t, leaving))];
379
+ for (const id of removed)
380
+ gone.add(id);
381
+ return [...steps, ...dissolving(gone, leaving, steps)];
382
+ };
383
+ /**
384
+ * `layout.dissolving`: the steps that delete each layout a plan leaves
385
+ * with one item or none — `gone` (blocks the plan removes) and
386
+ * `leaving` (blocks it moves) no longer count, an item `writes` move or
387
+ * insert into it does. With one left, the layout and that item are
388
+ * deleted and the item's children moved right after the layout, where
389
+ * `layout.single` shows them, as the kind they show there (`settle`);
390
+ * with none, the layout is deleted. One plan with the write that caused
391
+ * it.
392
+ */
393
+ const dissolving = (gone, leaving, writes) => {
394
+ const away = new Set([...gone, ...leaving]);
395
+ const layouts = new Set();
396
+ for (const id of away) {
397
+ const parent = positionOf(id)?.parent;
398
+ if (parent != null && isLayout(parent))
399
+ layouts.add(parent);
400
+ }
401
+ const { kids } = view();
402
+ const out = [];
403
+ for (const layout of layouts) {
404
+ if ([layout, ...ancestorsOf(layout)].some((a) => gone.has(a)))
405
+ continue;
406
+ const item = itemKindOf(layout);
407
+ const arriving = writes.reduce((n, w) => w.op === 'moveBlocks' && w.parent === layout
408
+ ? n + w.ids.filter((id) => blockTypeOf(id) === item).length
409
+ : w.op === 'insertBlocks' && w.parent === layout
410
+ ? n + w.specs.filter((spec) => spec.type === item).length
411
+ : n, 0);
412
+ const slots = kids.get(layout) ?? [];
413
+ const items = slots.filter((k) => !away.has(k.id) && blockTypeOf(k.id) === item);
414
+ if (items.length + arriving > 1)
415
+ continue;
416
+ const { parent } = positionOf(layout);
417
+ const last = items[0];
418
+ if (last === undefined) {
419
+ out.push(deleting(layout, [layout]));
420
+ continue;
421
+ }
422
+ // Its blocks keep their home slots: the read shows them in the layout's
423
+ // slot (`layout.single`), settled for its parent.
424
+ const moved = (kids.get(last.id) ?? []).filter((k) => !away.has(k.id));
425
+ const ids = moved.map((k) => k.id);
426
+ out.push(...settle(last.id, ids, parent), deleting(layout, [layout, last.id]));
427
+ }
428
+ return out;
429
+ };
430
+ /** `writes`, then `container` removed when they leave it no child but `leaving` (`emptyingAll`). */
431
+ const emptying = (container, leaving, writes, kept) => emptyingAll([container], leaving, writes, kept);
432
+ /**
433
+ * Delete: `removes` leave. Every one is marked with what it
434
+ * displays — an unmarked one would be promoted into the deleted slot
435
+ * (`displaySlotOf`).
436
+ */
437
+ const deleting = (id, removes) => {
438
+ const marks = [...new Set(removes.flatMap((b) => view().displays(b)))];
439
+ return { op: 'deleteBlock', id, marks, removes };
440
+ };
441
+ /** Delete `id` and its subtree, `kept` children aside. */
442
+ const remove = (id, kept = []) => {
443
+ const removes = [];
444
+ const skip = new Set(kept);
445
+ const walk = (b) => {
446
+ removes.push(b);
447
+ for (const kid of childrenIds(b))
448
+ if (!skip.has(kid))
449
+ walk(kid);
450
+ };
451
+ walk(id);
452
+ return deleting(id, removes);
453
+ };
454
+ const merge = (from, into) => ({
455
+ op: 'mergeBlocks',
456
+ from,
457
+ into,
458
+ at: displayLength(into),
459
+ length: displayLength(from)
460
+ });
461
+ /** `[at, end)` — `[offset, offset + length)` clamped to `id`'s display. */
462
+ const clamp = (id, offset, length) => {
463
+ const total = displayLength(id);
464
+ const at = Math.max(0, Math.min(offset, total));
465
+ return [at, Math.min(total, at + Math.max(0, length))];
466
+ };
467
+ /** `id`'s display items, each with its `[at, end)` display offsets. */
468
+ const spans = (id) => {
469
+ let end = 0;
470
+ return contentItems(id).map((item) => {
471
+ const at = end;
472
+ end += item.kind === 'text' ? item.text.length : 1;
473
+ return { item, at, end };
474
+ });
475
+ };
476
+ /** The live inline atom `atom` in `id`'s display, or undefined. */
477
+ const atomOf = (id, atom) => live(id) ? spans(id).find(({ item }) => item.kind === 'inline' && item.id === atom) : undefined;
478
+ /** Text items overlapping `[at, end)` of `id` (inline atoms carry no marks). */
479
+ const textIn = (id, at, end) => spans(id).flatMap((s) => (s.item.kind === 'text' && s.at < end && s.end > at ? [s.item] : []));
480
+ /** Push the steps writing `items` into `id` from `at` (text runs and atoms in order); the end offset. */
481
+ const writeContent = (writes, id, at, items = []) => {
482
+ for (const item of items) {
483
+ if (item.kind === 'inline')
484
+ writes.push({ op: 'insertInline', id, offset: at++, atom: item });
485
+ else if (item.text !== '') {
486
+ writes.push({ op: 'insertText', id, offset: at, text: item.text, marks: item.marks });
487
+ at += item.text.length;
488
+ }
489
+ }
490
+ return at;
491
+ };
492
+ /** `writes`, then every container the `leaving` blocks leave with no child removed. */
493
+ const emptied = (leaving, writes, kept) => emptyingAll(leaving.map((id) => positionOf(id).parent), leaving, writes, kept);
494
+ // ── placement: insert (`insertBlocks`, `duplicateBlock`), move
495
+ // (`moveBlocks`), outdent (`unNestBlocks`) and the lift out of the
496
+ // containers a kind does not fit (`landingOf`, `splitOut`, `liftOut`) ──
497
+ /**
498
+ * Insert blocks (specs may carry children/content/data; the batch is
499
+ * all-or-nothing). A spec is a `BlockSpec` or the `JSONBlock` that
500
+ * `toJSON` and the view's `value` speak (`{ type, content: [{ text }] }`):
501
+ * its ids are kept, minted where missing (`asBlockSpec`). Ids must be fresh.
502
+ * Refused when the parent is not live, is `void` or is an island's
503
+ * line (it holds no children), or any id collides.
504
+ * Inserting INSIDE an island is allowed — island interiors are built
505
+ * this way. `ids`: the inserted roots.
506
+ */
507
+ const insertBlocks = (dest, specs) => {
508
+ const parent = ref(dest.parent);
509
+ const clean = specs.map((spec) => sanitizeSpec(asBlockSpec(spec)));
510
+ if (parent !== null && (isVoid(parent) || isLine(parent)))
511
+ return REFUSED;
512
+ if (clean.length === 0)
513
+ return plan([], []);
514
+ if ((parent !== null && !live(parent)) || collides(doc, clean))
515
+ return REFUSED;
516
+ return plan(clean.map((s) => s.id), insertAt(parent, dest.index, clean));
517
+ };
518
+ /**
519
+ * Grouped move — one step, per-member conflict resolution; refused
520
+ * exactly when `canPlace` refuses the group. `ids`: the moved blocks,
521
+ * in request order.
522
+ */
523
+ const moveBlocks = (ids, dest) => {
524
+ const moved = ids.map(ref);
525
+ const parent = ref(dest.parent);
526
+ if (moved.length === 0)
527
+ return plan([], []);
528
+ if (!canPlace(moved, parent))
529
+ return REFUSED;
530
+ // A list the move leaves with no item goes (not one it moves or lands in).
531
+ const into = new Set([...moved, ...landing(parent)]);
532
+ return plan(moved, emptied(moved, move(moved, parent, dest.index), into));
533
+ };
534
+ /**
535
+ * Outdent: move sibling blocks `ids` (document order) right
536
+ * after their parent, and hand the siblings that followed the last of
537
+ * them to it as its last children — every outliner's Shift+Tab. A last
538
+ * block that cannot adopt them (void, island, a container they do not
539
+ * fit) leaves them with the parent. The blocks take the kind they show
540
+ * in their new slot (`settledKind`: a list's item becomes a paragraph,
541
+ * a paragraph outdented into a list its item); refused where one would
542
+ * not fit (a paragraph out of a column into its columns layout).
543
+ * Items leaving a container never take the items after them:
544
+ * the list splits around them, Notion's way — outdented first items go
545
+ * before it, last ones after it; from the middle, the list keeps the
546
+ * items after them and a new list of its kind takes the ones before
547
+ * them, so an item a peer appends meanwhile stays with the items it
548
+ * follows (`splitOut`, the split Turn into's `liftOut` makes). A
549
+ * container left with no child goes. One plan; refused as the
550
+ * move is. `ids`: the moved blocks.
551
+ */
552
+ const unNestBlocks = (ids) => {
553
+ const moved = ids.map(ref);
554
+ const last = moved.at(-1);
555
+ const pos = last === undefined ? null : positionOf(last);
556
+ const ppos = pos?.parent != null ? positionOf(pos.parent) : null;
557
+ if (!ppos || moved.some((id) => positionOf(id)?.parent !== pos.parent))
558
+ return REFUSED;
559
+ const from = pos.parent;
560
+ if (!canPlace(moved, ppos.parent, (id) => settledKind(from, id, ppos.parent)))
561
+ return REFUSED;
562
+ const after = childrenIds(from).slice(pos.index + 1);
563
+ const retype = settle(from, moved, ppos.parent);
564
+ if (isContainer(from))
565
+ return splitOut(moved, [from], [], false, retype);
566
+ const writes = [...moveTo(moved, ppos.parent, ppos.index + 1, leaving(true)), ...retype];
567
+ const adopts = after.length > 0 &&
568
+ !isVoid(last) &&
569
+ !isIsland(last) &&
570
+ after.every((id) => fits(last, blockTypeOf(id)));
571
+ if (adopts)
572
+ writes.push(...move(after, last, Infinity));
573
+ return plan(moved, writes);
574
+ };
575
+ /** Outdent one block (`unNestBlocks`). */
576
+ const unNestBlock = (id) => unNestBlocks([id]);
577
+ /**
578
+ * Where a block of `kind` at `id`'s place lands: `id`'s parent, or the
579
+ * first one up that `kind` fits (`fits`: a list holds only its items),
580
+ * and the containers it leaves on the way, innermost first (`levels`).
581
+ * Its own kind keeps a block where it is, as a reorder
582
+ * does (`canPlace`); a block inserted after it (`after`) has no place
583
+ * yet.
584
+ */
585
+ const landingOf = (id, kind, after = false) => {
586
+ const levels = [];
587
+ let parent = positionOf(id)?.parent ?? null;
588
+ const own = !after && blockTypeOf(id) === kind;
589
+ for (; !own && parent !== null && !fits(parent, kind); parent = positionOf(parent).parent)
590
+ levels.push(parent);
591
+ return { parent, levels };
592
+ };
593
+ /**
594
+ * Lift sibling blocks `ids` (document order) out of `levels`, the
595
+ * containers around them, innermost first: each level
596
+ * splits around them as an outdent splits one list — the blocks
597
+ * before them go to a new container of its kind, which the level above
598
+ * holds the same way, and the level keeps the ones after them, so an
599
+ * item a peer appends meanwhile stays with the items it follows; one
600
+ * left with no child goes. `specs` (new blocks) land right after them.
601
+ * With `keep`, `ids` stay before the split and only `specs` go out.
602
+ * `retype` (the caller's kind steps) joins the plan. The one split
603
+ * `unNestBlocks` and `liftOut` share. `ids`: the placed blocks.
604
+ */
605
+ const splitOut = (ids, levels, specs, keep, retype = []) => {
606
+ const moved = keep ? [] : [...ids];
607
+ const placed = [...moved, ...specs.map((s) => s.id)];
608
+ const last = ids.at(-1);
609
+ const { parent, index } = positionOf(levels.at(-1) ?? last);
610
+ if (levels.length === 0)
611
+ return plan(placed, [...insertAt(parent, index + 1, specs), ...retype]);
612
+ // Bottom up: what stays before the split at each level (the level
613
+ // itself, or a new head holding it), and whether anything follows it.
614
+ const heads = [];
615
+ let before = null;
616
+ let follows = false;
617
+ // The block right after `last` (what the split ends at when `keep`).
618
+ let next;
619
+ let child = last;
620
+ for (const level of levels) {
621
+ const kids = childrenIds(level);
622
+ const at = kids.indexOf(child);
623
+ const stays = (keep && child === last) || (before !== null && !before.head);
624
+ const lead = kids.slice(0, at + (stays ? 1 : 0)).filter((k) => !moved.includes(k));
625
+ if (!follows && at + 1 < kids.length)
626
+ [follows, next] = [true, kids[at + 1]];
627
+ if ((lead.length > 0 || before?.head) && follows) {
628
+ const spec = sanitizeSpec({
629
+ id: newId('b'),
630
+ type: kindToCopy(level),
631
+ data: blockDataOf(level) ?? {}
632
+ });
633
+ heads.push({ spec, kids: lead, inner: before?.head });
634
+ before = { id: spec.id, head: spec };
635
+ }
636
+ else
637
+ before = lead.length > 0 ? { id: level } : null;
638
+ child = level;
639
+ }
640
+ // Written in one gap, each right after the block before it, as an
641
+ // after-record out of the level's slot or a before-record into the gap
642
+ // before it (`leaving`): the engine's sort decides their order against
643
+ // a peer's records there (fork patch YP22, `sourceOrder`: the block a
644
+ // record stands at, then its part), so a peer's heads, pieces, lifts
645
+ // and new blocks in the same gap keep the serial order whatever the
646
+ // client ids (`del.merge.container`). The head stands at the first block
647
+ // (with `keep`, at it: `with`), the blocks at themselves, the new
648
+ // blocks after the last (`after`) — or, kept before the block that
649
+ // follows, at that block (`kept`). The records are written last first,
650
+ // so each stands before the one written before it, in the order the
651
+ // sort gives them; the head and the new blocks are created in the
652
+ // innermost level right after the block they stand at (their home,
653
+ // hidden while their records show them) and moved out by them.
654
+ const outside = before !== null && !before.head;
655
+ const side = leaving(outside);
656
+ const gap = index + (outside ? 1 : 0);
657
+ const at = moved[0] ?? last;
658
+ const kids0 = childrenIds(levels[0]);
659
+ const home = kids0.indexOf(last) + 1;
660
+ const specIds = specs.map((sp) => sp.id);
661
+ const [stands, part] = keep && !outside ? [next, PART.kept] : [last, PART.after];
662
+ // One record per run of adjacent blocks, each standing at its first: a
663
+ // block a peer's move takes out of a run leaves the others at their own place.
664
+ const runs = [];
665
+ for (const id of moved) {
666
+ const run = runs.at(-1);
667
+ if (run !== undefined && kids0.indexOf(run.at(-1)) + 1 === kids0.indexOf(id))
668
+ run.push(id);
669
+ else
670
+ runs.push([id]);
671
+ }
672
+ const writes = [
673
+ ...insertAt(levels[0], home, specs),
674
+ ...(before?.head ? insertAt(levels[0], home, [before.head]) : []),
675
+ ...moveTo(specIds, parent, gap, side, stands, part),
676
+ ...runs.reverse().flatMap((run) => moveTo(run, parent, gap, side, run[0], PART.self)),
677
+ ...(before?.head
678
+ ? moveTo([before.head.id], parent, gap, side, at, keep ? PART.with : PART.head)
679
+ : [])
680
+ ];
681
+ for (const { spec, kids, inner } of heads.reverse()) {
682
+ writes.push(...moveTo(kids, spec.id, 0), ...(inner ? insertAt(spec.id, kids.length, [inner]) : []));
683
+ }
684
+ writes.push(...retype);
685
+ void next;
686
+ return plan(placed, emptying(levels[0], moved, writes, landing(parent)));
687
+ };
688
+ /**
689
+ * Place `id` where a block of `kind` fits, in one plan:
690
+ * out of every container around it that `kind` does not fit
691
+ * (`landingOf`: a list, and a list holding that list directly, for a
692
+ * heading or a divider), each split around it (`splitOut`). `after`
693
+ * (new blocks) lands right after it. With `keep`, `id` stays and only
694
+ * `after` goes out, the split right after `id` (a divider inserted
695
+ * after an item). Its own kind never moves it. The kinds are the
696
+ * caller's: it composes the retype (`setBlock`). Refused where `id`
697
+ * may not move there (`canPlace`). `ids`: the placed blocks.
698
+ */
699
+ const liftOut = (id, kind, { keep = false, after = [] } = {}) => {
700
+ id = ref(id);
701
+ kind = ref(kind);
702
+ const specs = after.map(sanitizeSpec);
703
+ const { parent, levels } = landingOf(id, kind, keep);
704
+ if (!positionOf(id) || collides(doc, specs) || !canPlace([id], parent, () => kind))
705
+ return REFUSED;
706
+ return splitOut([id], levels, specs, keep);
707
+ };
708
+ /**
709
+ * Explicit fresh-identity copy of a subtree (paste / drag-clone):
710
+ * serializes `id`, remaps every block and inline atom id through
711
+ * `freshId`, inserts the copy right after `id`. Text atoms get new
712
+ * identity too — duplication is a creation op, not a relocation. An
713
+ * inline id `freshId` does not answer (a block-only callback) is
714
+ * minted; a block id it does not answer refuses the copy. `ids`: the
715
+ * copy's root.
716
+ */
717
+ const duplicateBlock = (id, freshId = (_, kind) => newId(kind === 'block' ? 'b' : 'i')) => {
718
+ id = ref(id);
719
+ const pos = positionOf(id);
720
+ if (pos === null)
721
+ return REFUSED;
722
+ const spec = (b) => {
723
+ const fresh = freshId(b, 'block');
724
+ if (typeof fresh !== 'string')
725
+ return null;
726
+ const content = contentItems(b).map((item) => {
727
+ if (item.kind !== 'inline')
728
+ return item;
729
+ const atom = freshId(item.id, 'inline');
730
+ return { ...item, id: typeof atom === 'string' ? atom : newId('i') };
731
+ });
732
+ const children = childrenIds(b).map(spec);
733
+ if (children.includes(null))
734
+ return null;
735
+ const data = blockDataOf(b);
736
+ return {
737
+ id: fresh,
738
+ type: kindToCopy(b),
739
+ ...(data !== undefined && { data }),
740
+ content,
741
+ children: children
742
+ };
743
+ };
744
+ const copy = spec(id);
745
+ return copy ? insertBlocks({ parent: pos.parent, index: pos.index + 1 }, [copy]) : REFUSED;
746
+ };
747
+ // ── layout (`layout.*`): the layout kind a new layout takes, where a
748
+ // beside placement lands (`besideAt`), `placeBeside` and `wrapInLayout` ──
749
+ /** The layout kind a new layout takes: `kind` when it is one, else the only one the roles declare. */
750
+ const layoutKind = (kind) => {
751
+ if (kind !== undefined)
752
+ return roles.of(kind).layout === undefined ? undefined : kind;
753
+ const kinds = roles.declared().layouts;
754
+ return kinds.length === 1 ? kinds[0] : undefined;
755
+ };
756
+ /** A fresh empty block of `type`. */
757
+ const spec = (type) => sanitizeSpec({ id: newId('b'), type, data: {} });
758
+ /**
759
+ * The block a beside placement at `target` stands beside
760
+ * (`layout.place-beside`): a layout or an item for itself; a block whose
761
+ * parent shows only its children (a list item → its list, at any depth)
762
+ * or is an island of lines (a code line → its code block) for that
763
+ * parent; then the block itself when it sits directly in an item or at
764
+ * the root, or where a new layout of `kind` (else the only layout kind)
765
+ * fits beside it (a toggle's, a callout's or a nested block's child,
766
+ * outside any item and island); else its outermost block below the
767
+ * root or an item.
768
+ */
769
+ const besideAt = (target, kind) => {
770
+ let at = ref(target);
771
+ if (isLayout(at) || isLayoutItem(at))
772
+ return at;
773
+ for (let parent = positionOf(at)?.parent ?? null; parent !== null && !isLayoutItem(parent) && (isContainer(parent) || isLines(parent)); parent = positionOf(parent)?.parent ?? null)
774
+ at = parent;
775
+ const parent = positionOf(at)?.parent ?? null;
776
+ if (parent === null || isLayoutItem(parent))
777
+ return at;
778
+ const wrap = layoutKind(kind);
779
+ if (wrap !== undefined &&
780
+ fits(parent, wrap) &&
781
+ !insideItem(parent) &&
782
+ !isIsland(parent) &&
783
+ !insideIsland(parent))
784
+ return at;
785
+ for (let up = parent; up !== null && !isLayoutItem(up); up = positionOf(up).parent)
786
+ at = up;
787
+ return at;
788
+ };
789
+ /**
790
+ * Place `ids` beside `target`, to its `side` (`layout.place-beside`: a
791
+ * block dragged to another block's left or right edge). The target
792
+ * resolves to its outermost block below the root or below a layout
793
+ * item (a list item → its list, a code line → its code block); an item
794
+ * stands for itself, a layout for its first or last slot. Beside a
795
+ * block in an item, a new item holding `ids` goes beside that item;
796
+ * otherwise the block and a new item holding `ids` are wrapped in a new
797
+ * layout of the layout `kind` (else the only one the roles declare) at
798
+ * its place. Refused when `ids` holds the target or an ancestor of it,
799
+ * an item, a layout or a block holding one, a block that does not
800
+ * fit an item, a block or a layout inside an island, and, wrapping,
801
+ * when there is no layout kind. The sources are cleaned in the same
802
+ * plan (`emptying`, `dissolving`); a plain move. `ids`: the moved
803
+ * blocks.
804
+ */
805
+ const placeBeside = (ids, target, side, kind) => {
806
+ const moved = ids.map(ref);
807
+ target = ref(target);
808
+ const v = view();
809
+ if ((side !== 'left' && side !== 'right') || !canPlace(moved) || !live(target))
810
+ return REFUSED;
811
+ if ([target, ...ancestorsOf(target, v)].some((a) => moved.includes(a)))
812
+ return REFUSED;
813
+ if (moved.some((id) => isLayoutItem(id) || holdsLayout(id)))
814
+ return REFUSED;
815
+ const right = side === 'right';
816
+ // Where the new item goes: beside an item of a layout, or a new layout wrapping `at`.
817
+ const at = besideAt(target, kind && ref(kind));
818
+ let layout = null;
819
+ let index = 0;
820
+ if (isLayout(at))
821
+ [layout, index] = [at, right ? childrenIds(at).length : 0];
822
+ else {
823
+ const item = isLayoutItem(at) ? at : positionOf(at).parent;
824
+ if (item !== null) {
825
+ const pos = positionOf(item);
826
+ [layout, index] = [pos.parent, pos.index + (right ? 1 : 0)];
827
+ }
828
+ }
829
+ const wrap = layout === null ? layoutKind(kind && ref(kind)) : blockTypeOf(layout);
830
+ const item = wrap === undefined ? undefined : roles.of(wrap).layout;
831
+ if (wrap === undefined || item === undefined)
832
+ return REFUSED;
833
+ if (moved.some((id) => !fitsIn(item, blockTypeOf(id))))
834
+ return REFUSED;
835
+ if (layout !== null && (isIsland(layout) || insideIsland(layout, v)))
836
+ return REFUSED;
837
+ const fresh = spec(item);
838
+ const into = (parent) => moveTo(moved, parent, 0);
839
+ let writes;
840
+ let kept;
841
+ if (layout !== null) {
842
+ writes = [...insertAt(layout, index, [fresh]), ...into(fresh.id)];
843
+ kept = new Set([fresh.id, ...landing(layout)]);
844
+ }
845
+ else {
846
+ const pos = positionOf(at);
847
+ const host = spec(item);
848
+ const wrapper = { ...spec(wrap), children: right ? [host, fresh] : [fresh, host] };
849
+ writes = [
850
+ ...insertAt(pos.parent, pos.index, [wrapper]),
851
+ ...moveTo([at], host.id, 0),
852
+ ...into(fresh.id)
853
+ ];
854
+ kept = new Set([wrapper.id, host.id, fresh.id, ...landing(pos.parent)]);
855
+ }
856
+ return plan(moved, emptied(moved, writes, kept));
857
+ };
858
+ /**
859
+ * Wrap sibling blocks in a new layout of `columns` items (default: one
860
+ * per block), at the first one's place (`layout.wrap`: Turn into N
861
+ * columns, as Notion): the blocks fill the first items, one each, in
862
+ * document order; each item after them holds one empty block of the
863
+ * item's default child (one block turned into 3 columns: it, then two
864
+ * empty columns). The layout is of the layout `kind` (else the only one
865
+ * the roles declare). Refused for no block, fewer than two items or
866
+ * fewer items than blocks, blocks of different parents, an item, a
867
+ * layout or a block holding one, where the layout does not fit
868
+ * (`fits`: in a list, a code block) or lands inside an item or an
869
+ * island, and for a block that does not fit an item. A plain move.
870
+ * `ids`: the new layout.
871
+ */
872
+ const wrapInLayout = (ids, kind, columns = ids.length) => {
873
+ const blocks = ids.map(ref);
874
+ const v = view();
875
+ if (!Number.isInteger(columns) || columns < 2 || blocks.length < 1)
876
+ return REFUSED;
877
+ if (columns < blocks.length || !canPlace(blocks))
878
+ return REFUSED;
879
+ const parent = positionOf(blocks[0]).parent;
880
+ if (blocks.some((id) => positionOf(id).parent !== parent))
881
+ return REFUSED;
882
+ if (blocks.some((id) => isLayoutItem(id) || holdsLayout(id)))
883
+ return REFUSED;
884
+ const wrap = layoutKind(kind && ref(kind));
885
+ const item = wrap === undefined ? undefined : roles.of(wrap).layout;
886
+ if (wrap === undefined || item === undefined || !fits(parent, wrap))
887
+ return REFUSED;
888
+ if (parent !== null && (insideItem(parent, v) || isIsland(parent) || insideIsland(parent, v)))
889
+ return REFUSED;
890
+ if (blocks.some((id) => !fitsIn(item, blockTypeOf(id))))
891
+ return REFUSED;
892
+ const fill = roles.of(item).child;
893
+ if (columns > blocks.length && !fitsIn(item, fill))
894
+ return REFUSED;
895
+ const ordered = blocks.toSorted((a, b) => positionOf(a).index - positionOf(b).index);
896
+ const items = Array.from({ length: columns }, (_, i) => i < ordered.length ? spec(item) : { ...spec(item), children: [spec(fill)] });
897
+ const wrapper = { ...spec(wrap), children: items };
898
+ const at = positionOf(ordered[0]).index;
899
+ const writes = [
900
+ ...insertAt(parent, at, [wrapper]),
901
+ ...ordered.flatMap((id, i) => moveTo([id], items[i].id, 0))
902
+ ];
903
+ const kept = new Set([
904
+ wrapper.id,
905
+ ...items.map((it) => it.id),
906
+ ...landing(parent)
907
+ ]);
908
+ return plan([wrapper.id], emptied(ordered, writes, kept));
909
+ };
910
+ // ── split and merge: `splitBlock` (one boundary item, no text copied),
911
+ // the engine merge primitive `mergeBlocks`, and the baseline-shaped
912
+ // `mergeBackward`/`mergeForward` (a first item lifts out of its container,
913
+ // a layout item's first block merges across items) ──
914
+ /**
915
+ * Split `id` at content `offset` into a new sibling `newId` (one boundary
916
+ * item and the claims that follow it, no text copied; children follow).
917
+ * `tail` decides the sibling's type/data once (default: the source's —
918
+ * the kind it displays; an empty `tail.type` is the default too).
919
+ * Refused on `void` blocks and on blocks that render no content (a
920
+ * list, a code block: nothing to split). `ids`: the new
921
+ * block.
922
+ */
923
+ const splitBlock = (id, offset, newId, tail) => {
924
+ id = ref(id);
925
+ const born = ref(newId);
926
+ const pos = positionOf(id);
927
+ const rec = view().blocks.get(id);
928
+ if (pos === null || isVoid(id) || !rendersContent(id) || rec === undefined)
929
+ return REFUSED;
930
+ // A table's cell holds its lines as line breaks: it never splits (`table.split`).
931
+ if (isCellKind(blockTypeOf(id)))
932
+ return REFUSED;
933
+ if (isIncarnationId(born) || blockNodeOf(doc, born) !== null)
934
+ return REFUSED;
935
+ const [at] = clamp(id, offset, 0);
936
+ // An empty tail type is the one a view reads mid-retype: copy the kind.
937
+ const type = tail?.type ? ref(tail.type) : kindToCopy(id);
938
+ const t = tail
939
+ ? { type, data: tail.data && sanitizeWireJson(tail.data) }
940
+ : { type, data: readData(rec.node) };
941
+ const length = displayLength(id) - at;
942
+ const split = {
943
+ op: 'splitBlock',
944
+ id,
945
+ offset: at,
946
+ length,
947
+ newId: born,
948
+ tail: t,
949
+ parent: pos.parent,
950
+ index: pos.index + 1
951
+ };
952
+ return plan([born], [split, ...move(childrenIds(id), born, 0)]);
953
+ };
954
+ /**
955
+ * Engine merge primitive: `from`'s content is claimed by `into`, its
956
+ * children ADOPTED as `into`'s last children — an island's take
957
+ * `into`'s default child, like a baseline merge's — and `from` is
958
+ * hidden via the claim (undo restores it). The children stay under
959
+ * `from` and display under `into` through the claim, after `into`'s
960
+ * own children (the display's claim order): a concurrent delete of
961
+ * `into` voids the claim and `from` comes back with its children,
962
+ * never below them. Role rules are `canMerge`'s; a merge that would
963
+ * close a display cycle is refused. `ids`: `into`.
964
+ */
965
+ const mergeBlocks = (fromId, intoId) => {
966
+ const from = ref(fromId);
967
+ const into = ref(intoId);
968
+ const v = view();
969
+ if (!canMerge(from, into) || !contentTarget(into))
970
+ return REFUSED;
971
+ if (isSelfOrDescendant(v.placements, v.own, into, from))
972
+ return REFUSED;
973
+ const kids = childrenIds(from);
974
+ const retype = settle(from, kids, into);
975
+ // A list its only item leaves goes, as with every sibling op.
976
+ return plan([into], emptied([from], [merge(from, into), ...retype], landing(into)));
977
+ };
978
+ /**
979
+ * Baseline-shaped merge (both `mergeBackward` and `mergeForward`):
980
+ * `from`'s children unnest right after `from`'s vacated sibling slot —
981
+ * NOT adopted into `into` — and take the kind they show there
982
+ * (`settledKind`: an island's children its parent's default child, a
983
+ * paragraph in a list its item); then `from`'s content claims
984
+ * into `into`. Placed after `from`, not at it: a concurrent delete of
985
+ * `into` revives `from` above its former children.
986
+ */
987
+ const mergeUnnesting = (from, into) => {
988
+ const pos = canMerge(from, into) ? positionOf(from) : null;
989
+ if (pos === null || !contentTarget(into))
990
+ return REFUSED;
991
+ const kids = childrenIds(from);
992
+ const retype = settle(from, kids, pos.parent);
993
+ return plan([into], [...moveTo(kids, pos.parent, pos.index + 1, leaving(true)), ...retype, merge(from, into)]);
994
+ };
995
+ /** Remove `container`, left empty, for the key at `id` (refused if it holds text). */
996
+ const gone = (id, container) => {
997
+ const writes = emptying(container, [], []);
998
+ return writes.length > 0 ? plan([id], writes) : REFUSED;
999
+ };
1000
+ /**
1001
+ * Baseline `mergeBlockBackward`: merge `id` into the previous block in
1002
+ * document order. No previous block → an empty block merges forward,
1003
+ * else refused. The first item of a container (`isContainer`) outside
1004
+ * any island lifts out of it instead (Notion): it takes the
1005
+ * container's slot, as its new parent's default child, with its
1006
+ * children (a first cell stays: nothing leaves an island; a block that
1007
+ * would land directly in a container it is no item of stays too).
1008
+ * The first block of a layout item merges across items
1009
+ * instead, in reading order (`layout.merge`, Notion): into the
1010
+ * previous item's last line, or, in the first item, the line before
1011
+ * the layout; an item it empties goes and the layout dissolves.
1012
+ * `ids`: the surviving block.
1013
+ */
1014
+ const mergeBackward = (id) => {
1015
+ id = ref(id);
1016
+ const prev = previous(id);
1017
+ if (prev === null && childrenIds(id).length === 0 && displayLength(id) === 0) {
1018
+ return mergeForward(id);
1019
+ }
1020
+ // A container a concurrent edit left empty shows nothing: the key removes it.
1021
+ if (prev !== null && isContainer(prev) && childrenIds(prev).length === 0)
1022
+ return gone(id, prev);
1023
+ if (prev !== null && prev === positionOf(id)?.parent && isLayoutItem(prev)) {
1024
+ const layout = positionOf(prev).parent;
1025
+ let into = previous(prev);
1026
+ if (into === layout)
1027
+ into = previous(layout);
1028
+ const merged = into === null ? REFUSED : mergeUnnesting(id, into);
1029
+ if (!('writes' in merged) || childrenIds(id).length > 0)
1030
+ return merged;
1031
+ return plan(merged.ids, emptying(prev, [id], merged.writes));
1032
+ }
1033
+ if (prev !== null &&
1034
+ prev === positionOf(id)?.parent &&
1035
+ isContainer(prev) &&
1036
+ !insideIsland(prev)) {
1037
+ // It lands only where it fits: never directly in a container it is no item of.
1038
+ const slot = positionOf(prev);
1039
+ if (!fits(slot.parent, settledKind(prev, id, slot.parent)))
1040
+ return REFUSED;
1041
+ const lift = [
1042
+ ...moveTo([id], slot.parent, slot.index, leaving(false)),
1043
+ ...settle(prev, [id], slot.parent)
1044
+ ];
1045
+ return plan([id], emptying(prev, [id], lift, landing(slot.parent)));
1046
+ }
1047
+ return prev ? mergeUnnesting(id, prev) : REFUSED;
1048
+ };
1049
+ /**
1050
+ * Baseline `mergeBlockForward`: pull the next block in document order
1051
+ * into `id`. A container passes the merge to its first item (`del.merge.container`:
1052
+ * Delete above a list pulls the item's text up and the list keeps the
1053
+ * rest, or goes when that was its only item). A container left
1054
+ * empty (by concurrent edits) is removed instead, by either key.
1055
+ */
1056
+ const mergeForward = (id) => {
1057
+ id = ref(id);
1058
+ let after = next(id);
1059
+ while (after !== null && isContainer(after)) {
1060
+ const first = childrenIds(after)[0];
1061
+ // A container a concurrent edit left empty shows nothing: the key removes it.
1062
+ if (first === undefined)
1063
+ return gone(id, after);
1064
+ after = first;
1065
+ }
1066
+ const out = after ? mergeUnnesting(after, id) : REFUSED;
1067
+ const parent = after && positionOf(after)?.parent;
1068
+ if (!('writes' in out) || !parent || childrenIds(after).length > 0)
1069
+ return out;
1070
+ return plan(out.ids, emptying(parent, [after], out.writes));
1071
+ };
1072
+ // ── block deletion (`del.blocks.promote`): only the members leave, the
1073
+ // unselected children of a deleted block take its slot ──
1074
+ /**
1075
+ * Delete a set of blocks (`del.blocks.promote`): this writer's mark on
1076
+ * every member and on what it displays through merge claims (wins over
1077
+ * concurrent moves). Only the members leave: the unselected children of a
1078
+ * deleted block take its slot, in order, with their subtrees, as the
1079
+ * kind they show there (`settledKind`: a deleted island's children take
1080
+ * the slot parent's default child, a deleted list's items leave its
1081
+ * kind, a block promoted into a list is its item). A container the delete leaves with no child goes too
1082
+ * (`del.range.empty-container`). `subtree` is the explicit whole-subtree
1083
+ * delete. `filled`: a parent the same plan fills again (a flow placed in
1084
+ * the deleted blocks' slot, `flow.slot`), which is never emptied, so its
1085
+ * layout never dissolves (`layout.flow-slot`).
1086
+ */
1087
+ const deleteBlocks = (ids, subtree = false, filled) => {
1088
+ const set = new Set(ids.map(ref));
1089
+ if ([...set].some((id) => !live(id)))
1090
+ return REFUSED;
1091
+ // A layout goes with its items: their blocks take its slot (`layout.dissolving`).
1092
+ for (const id of [...set])
1093
+ if (isLayout(id))
1094
+ for (const kid of childrenIds(id))
1095
+ if (isLayoutItem(kid))
1096
+ set.add(kid);
1097
+ // A table goes with its rows and cells, a row with its cells: none shows outside them (`table.delete-row`).
1098
+ const whole = (id) => {
1099
+ for (const kid of childrenIds(id)) {
1100
+ set.add(kid);
1101
+ whole(kid);
1102
+ }
1103
+ };
1104
+ for (const id of [...set])
1105
+ if (isTable(id) || isTableRow(id))
1106
+ whole(id);
1107
+ if (subtree) {
1108
+ const roots = [...set].filter((id) => !ancestorsOf(id).some((a) => set.has(a)));
1109
+ return plan(roots, emptied(roots, roots.map((id) => remove(id))));
1110
+ }
1111
+ const roots = [...set].filter((id) => !set.has(positionOf(id).parent));
1112
+ const { kids } = view();
1113
+ const promoting = new Set();
1114
+ const members = [];
1115
+ const writes = roots.flatMap((root) => {
1116
+ const pos = positionOf(root);
1117
+ const chunk = [];
1118
+ const kept = [];
1119
+ // The kept children keep their home slots: the read shows them in the
1120
+ // root's slot, in its order (a block a peer puts under a member
1121
+ // meanwhile takes the slot in that order too); only their kinds are
1122
+ // written, as the slot's parent settles them.
1123
+ const walk = (b) => {
1124
+ chunk.push(b);
1125
+ for (const kid of kids.get(b) ?? []) {
1126
+ if (set.has(kid.id))
1127
+ walk(kid.id);
1128
+ else
1129
+ kept.push({ id: kid.id, from: b });
1130
+ }
1131
+ };
1132
+ walk(root);
1133
+ members.push(...chunk);
1134
+ if (kept.length > 0)
1135
+ promoting.add(root);
1136
+ // A layout shows only its items: a deleted item's blocks show right
1137
+ // after the layout (`layout.only-items`), settled for its parent.
1138
+ const item = pos.parent === null ? undefined : itemKindOf(pos.parent);
1139
+ const out = (k) => item !== undefined && blockTypeOf(k.id) !== item;
1140
+ return [
1141
+ ...kept.flatMap((k) => settle(k.from, [k.id], out(k) ? positionOf(pos.parent).parent : pos.parent)),
1142
+ deleting(root, chunk)
1143
+ ];
1144
+ });
1145
+ 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));
1146
+ };
1147
+ /** Delete `id` — its children take its slot (`keepChildren: false`: the whole subtree). */
1148
+ const deleteBlock = (id, opts = {}) => deleteBlocks([id], opts.keepChildren === false);
1149
+ // ── meta: a block's type (`setBlockType`, the steps a retype implies),
1150
+ // its data, a block's, an atom's or the document's (`patchData`, the
1151
+ // data steps of a patch), and the baseline `setBlock` ──
1152
+ /**
1153
+ * The data step of `patches` (sanitized) on a block, one of its atoms or
1154
+ * the document (`crdt/data.ts`): none when it changes nothing, `null`
1155
+ * when a patch is refused.
1156
+ */
1157
+ const dataSteps = (target, patches) => {
1158
+ const [id, inlineId] = typeof target === 'object' && target ? [target.block, target.atom] : [target ?? undefined];
1159
+ const node = dataNode(id, inlineId);
1160
+ // A block kind's atomic paths are written as one leaf each (`data.atomic`).
1161
+ const type = inlineId === undefined && id !== undefined ? blockTypeOf(id) : undefined;
1162
+ const atomic = (type === undefined ? undefined : roleOf(type)?.atomic) ?? [];
1163
+ const writes = node
1164
+ ? patchWrites(node, patches, doc.clientID, randOf(doc), atomic.map((p) => leafKey(typeof p === 'string' ? [p] : p)))
1165
+ : [];
1166
+ if (writes === null)
1167
+ return null; // a patch fits no value there
1168
+ if (writes.length === 0)
1169
+ return [];
1170
+ const offset = inlineId === undefined ? undefined : atomOf(id, inlineId)?.at;
1171
+ return [
1172
+ {
1173
+ op: 'patchData',
1174
+ ...(id !== undefined && { id }),
1175
+ ...(inlineId !== undefined && { inlineId, offset }),
1176
+ ops: patches,
1177
+ writes
1178
+ }
1179
+ ];
1180
+ };
1181
+ /** A whole-data replace: a patch of the root. */
1182
+ const replaceData = (data) => [{ path: [], value: sanitizeWireJson(data) }];
1183
+ /**
1184
+ * `data`'s leaves as sets over `current` (`data.retype.keep`): a
1185
+ * plain object's keys, recursively where `current` holds an object
1186
+ * there too; anything else (an array, a primitive, an empty object, an
1187
+ * object where `current` holds none) as one value at its path. Nothing
1188
+ * else under the root is touched.
1189
+ */
1190
+ const leafSets = (data, current) => {
1191
+ const out = [];
1192
+ const walk = (v, at, path) => {
1193
+ if (isObject(v) && Object.keys(v).length > 0 && (path.length === 0 || isObject(at)))
1194
+ for (const [k, x] of Object.entries(v))
1195
+ walk(x, isObject(at) ? at[k] : undefined, [...path, k]);
1196
+ else if (path.length > 0)
1197
+ out.push({ path, value: v });
1198
+ };
1199
+ walk(sanitizeWireJson(data), current, []);
1200
+ return out;
1201
+ };
1202
+ /**
1203
+ * The target role decides: nothing renders a void's children,
1204
+ * so a block retyped to a void kind hands them to the slot right after
1205
+ * it, as the kind they show there (`settledKind`), in their order (where
1206
+ * the read-time shedding shows a child a peer adds meanwhile). An island
1207
+ * declared `lines` retyped to an ordinary kind keeps its lines, each
1208
+ * retyped to the new kind's default child (the document's where that
1209
+ * renders no content): no line kind outside its island. Any other
1210
+ * island's children keep their kinds (a table's rows stay rows), as
1211
+ * `typeOf` shows a child a peer adds meanwhile.
1212
+ */
1213
+ const retypeSteps = (id, type) => {
1214
+ const { kids } = view();
1215
+ const pos = positionOf(id);
1216
+ const steps = attr(id, TYPE, type);
1217
+ const moved = kids.get(id) ?? [];
1218
+ // A code block retyped to an ordinary kind keeps its lines as that kind's children
1219
+ // (`leavingIsland`: never a kind that hides their text).
1220
+ const to = roles.of(type);
1221
+ if (isLines(id) && !to.island && !to.childless) {
1222
+ return [
1223
+ ...steps,
1224
+ ...moved.flatMap((k) => {
1225
+ const kind = leavingIsland(id, blockTypeOf(k.id), to.child);
1226
+ return kind === undefined ? [] : attr(k.id, TYPE, kind);
1227
+ })
1228
+ ];
1229
+ }
1230
+ if (!to.childless || moved.length === 0 || pos === null)
1231
+ return steps;
1232
+ // The children keep their home slots: the read shows them in the void's
1233
+ // slot (`conc.void-children`), settled for its parent.
1234
+ return [
1235
+ ...steps,
1236
+ ...settle(id, moved.map((k) => k.id), pos.parent)
1237
+ ];
1238
+ };
1239
+ /** Set the block type (attr write — the block keeps its identity; see `retypeSteps`). */
1240
+ const setBlockType = (id, type) => {
1241
+ id = ref(id);
1242
+ return live(id) ? plan([id], retypeSteps(id, ref(type))) : REFUSED;
1243
+ };
1244
+ /**
1245
+ * Patch the data of a block, one of its inline atoms or the document
1246
+ * (`null`), in order (`crdt/data.ts`): each patch replaces the value at
1247
+ * its path (`value` absent deletes it), `splice` and `order` edit the
1248
+ * array there; a path addresses array items by index, resolved here
1249
+ * to the items' ids. Only the leaves it changes are written, so a
1250
+ * peer's edit of another key or item is kept. Refused for an absent
1251
+ * target, a path that is no array of strings, a root value that is no
1252
+ * object, or an op that fits no value at its path.
1253
+ */
1254
+ const patchData = (target, patches) => {
1255
+ const valid = (p) => Array.isArray(p?.path) &&
1256
+ p.path.every((k) => typeof k === 'string') &&
1257
+ (p.path.length > 0 || p.value === undefined || isObject(p.value));
1258
+ if (!Array.isArray(patches) || !patches.every(valid))
1259
+ return REFUSED;
1260
+ // Only the fields a patch has: an absent one is no non-JSON value to report.
1261
+ const clean = patches.map(({ path, value, splice, order }) => sanitizeWireJson({
1262
+ path,
1263
+ ...(value !== undefined && { value }),
1264
+ ...(splice !== undefined && { splice }),
1265
+ ...(order !== undefined && { order })
1266
+ }));
1267
+ const t = target === null
1268
+ ? null
1269
+ : typeof target === 'string'
1270
+ ? ref(target)
1271
+ : { block: ref(target.block), atom: ref(target.atom) };
1272
+ if (typeof t === 'string' ? !live(t) : t !== null && atomOf(t.block, t.atom) === undefined)
1273
+ return REFUSED;
1274
+ const steps = dataSteps(t, clean);
1275
+ return steps ? plan(t === null ? [] : [typeof t === 'string' ? t : t.block], steps) : REFUSED;
1276
+ };
1277
+ /**
1278
+ * Baseline `setBlock`: `type` updates the block in place and `data`
1279
+ * sets the leaves it names, removing none (`data.retype.keep`: a
1280
+ * retype keeps the block's properties, as Notion's Turn into does;
1281
+ * `setBlockData` replaces them); `content`/`children` REPLACE
1282
+ * wholesale — explicit replacement is a
1283
+ * new-identity operation; a retype to a void kind without `children`
1284
+ * unnests the current ones (`retypeSteps`). All-or-nothing: children
1285
+ * for a block that is `void` after the write, or a replacement id that
1286
+ * is already taken (a live or deleted
1287
+ * block, the replaced children included, or a duplicate inside the
1288
+ * replacement), refuses before any write — `id-collision` for the
1289
+ * latter. A caller wanting a child back re-creates it with a fresh id.
1290
+ */
1291
+ const setBlock = (id, value) => {
1292
+ id = ref(id);
1293
+ const content = value.content?.map((item) => sanitizeItem(asContentItem(item)));
1294
+ const children = value.children?.map((child) => sanitizeSpec(asBlockSpec(child)));
1295
+ if (!live(id) || (content !== undefined && !contentTarget(id)))
1296
+ return REFUSED;
1297
+ const type = value.type === undefined ? undefined : ref(value.type);
1298
+ const toVoid = type === undefined ? isVoid(id) : roles.of(type).childless;
1299
+ if (children?.length && toVoid)
1300
+ return REFUSED;
1301
+ if (children !== undefined && collides(doc, children))
1302
+ return refused('id-collision');
1303
+ const data = value.data === undefined ? [] : dataSteps(id, leafSets(value.data, blockDataOf(id)));
1304
+ if (data === null)
1305
+ return REFUSED;
1306
+ const writes = [
1307
+ ...(type === undefined
1308
+ ? []
1309
+ : children === undefined
1310
+ ? retypeSteps(id, type)
1311
+ : attr(id, TYPE, type)),
1312
+ ...data
1313
+ ];
1314
+ if (content !== undefined) {
1315
+ writes.push(...cutText(id, 0, displayLength(id)));
1316
+ writeContent(writes, id, 0, content);
1317
+ }
1318
+ if (children !== undefined) {
1319
+ for (const kid of childrenIds(id))
1320
+ writes.push(remove(kid));
1321
+ writes.push(...insertAt(id, 0, children));
1322
+ }
1323
+ return plan([id], writes);
1324
+ };
1325
+ // ── content: text and inline atoms in a block's display (allowed inside
1326
+ // voids — the caption contract), and marks over a range ──
1327
+ // content ops (allowed inside voids — caption contract)
1328
+ const insertText = (id, offset, text, marks) => {
1329
+ id = ref(id);
1330
+ const clean = ref(text);
1331
+ if (!contentTarget(id))
1332
+ return REFUSED;
1333
+ const [at] = clamp(id, offset, 0);
1334
+ const m = marks && sanitizeWireJson(marks);
1335
+ return plan([id], clean === '' ? [] : [{ op: 'insertText', id, offset: at, text: clean, marks: m }]);
1336
+ };
1337
+ const deleteText = (id, offset, length) => {
1338
+ id = ref(id);
1339
+ if (!contentTarget(id))
1340
+ return REFUSED;
1341
+ const [at, end] = clamp(id, offset, length);
1342
+ return plan([id], cutText(id, at, end - at));
1343
+ };
1344
+ /**
1345
+ * Multi-mark format over a range (values may be null = unset); planned
1346
+ * only when some text atom in the range carries a different value.
1347
+ */
1348
+ const formatRange = (id, offset, length, marks) => {
1349
+ id = ref(id);
1350
+ const clean = sanitizeWireJson(marks);
1351
+ if (!contentTarget(id))
1352
+ return REFUSED;
1353
+ const [at, end] = clamp(id, offset, length);
1354
+ // Planned only when some text atom carries another value (the same-value guard).
1355
+ const differs = textIn(id, at, end).some((item) => Object.keys(clean).some((k) => !jsonEquals(item.marks?.[k] ?? null, clean[k] ?? null)));
1356
+ return plan([id], differs ? [{ op: 'formatRange', id, offset: at, length: end - at, marks: clean }] : []);
1357
+ };
1358
+ /**
1359
+ * Baseline `removeMarksFromText`: every mark present anywhere in the
1360
+ * range is unset over the range. Names are discovered from the content.
1361
+ */
1362
+ const clearMarks = (id, offset, length) => {
1363
+ const clears = {};
1364
+ for (const item of textIn(ref(id), offset, offset + length)) {
1365
+ for (const k of Object.keys(item.marks ?? {}))
1366
+ clears[k] = null;
1367
+ }
1368
+ return formatRange(id, offset, length, clears);
1369
+ };
1370
+ /** An atom without an id gets a fresh one. */
1371
+ const insertInline = (id, offset, atom) => {
1372
+ id = ref(id);
1373
+ const clean = sanitizeInline({ ...atom, id: atom.id ?? newId('i') });
1374
+ if (!contentTarget(id))
1375
+ return REFUSED;
1376
+ return plan([id], [{ op: 'insertInline', id, offset: clamp(id, offset, 0)[0], atom: clean }]);
1377
+ };
1378
+ const removeInline = (id, inlineId) => {
1379
+ id = ref(id);
1380
+ const atom = atomOf(id, ref(inlineId));
1381
+ if (atom === undefined)
1382
+ return REFUSED;
1383
+ return plan([id], [{ op: 'removeInline', id, offset: atom.at, inlineId: ref(inlineId) }]);
1384
+ };
1385
+ // ── range deletion (`deleteRange`, `replaceRange`) ─────────────────
1386
+ // Here `view` is what the view knows (`RangeView`, `FlowView`); the
1387
+ // document's own view is `c.view()`.
1388
+ /**
1389
+ * Range deletion between two positions — one prepared document operation
1390
+ * whose merge is the document's `canMerge`. The rules are the
1391
+ * `del.range.*` rows of `docs/editor-delete-contract.md`:
1392
+ *
1393
+ * - the head keeps `[0, start)`, the tail keeps `[end, len)`, and every
1394
+ * block strictly between them in document order dies;
1395
+ * - the head dies iff its prefix is empty (never for a replacement,
1396
+ * `del.range.replace`, nor when nothing else would be left to hold the
1397
+ * caret, `del.range.whole-doc`: the head is kept, emptied, with its id,
1398
+ * type and data, so peers deleting the same range keep the same block)
1399
+ * and the tail then keeps its id; the tail dies iff
1400
+ * its suffix is empty; otherwise the tail's suffix merges into the head
1401
+ * when `canMerge(tail, head)` allows it (`del.range.island-seal`) — an end
1402
+ * at the tail's start included: the seam between them is deleted;
1403
+ * - what follows the range end survives: the dying tail's children and the
1404
+ * later siblings inside every container the range dies through take the
1405
+ * topmost such container's slot, unless that would cross an island seal —
1406
+ * then those containers stay (`del.range.outside-survives`);
1407
+ * - a container (a list: the document's `isContainer`) never dies because
1408
+ * the range starts before it: it keeps what follows the range end, like
1409
+ * the keys (`del.merge.container`); it dies only when it loses every
1410
+ * child and holds no text of its own — the document's `emptiable`, the
1411
+ * keys' rule (`del.range.empty-container`). An island that renders
1412
+ * no content dies when it loses every child too, except a `lines` island
1413
+ * (a code block) the range lies in: its head is then kept, emptied
1414
+ * (`del.range.island-kept`);
1415
+ * - what a range rescues takes the kind it shows in its new slot (the
1416
+ * document's `settle`: a paragraph rescued into a list is its item);
1417
+ * - blocks the view hides (a closed toggle's body) are not in the range:
1418
+ * they go only with a block that goes, so a surviving head keeps its
1419
+ * hidden children, unless it is kept only to hold the caret; a dying
1420
+ * closed tail's body takes its place, shown (`del.range.hidden-body`);
1421
+ * - a layout the range leaves with one column dissolves, in the same plan
1422
+ * (`layout.dissolving`);
1423
+ * - a range with an end in a table's cell keeps every cell and row: each
1424
+ * cell it crosses loses the text it covers, nothing merges across a
1425
+ * cell's edge, the blocks outside the table between the ends go, a
1426
+ * table wholly between them goes whole, and the caret lands at the start
1427
+ * (`table.range`);
1428
+ * - the plan's `at` is where the caret lands (`del.range.caret`).
1429
+ *
1430
+ * `headSpent`: a whole-doc retry keeps the head, but what it hid goes as if it went.
1431
+ */
1432
+ const rangeDelete = (keepHead, headSpent = false) => (from, to, view = {}) => {
1433
+ // A position names shown text: a live block that renders its own content.
1434
+ const holds = (id) => contentTarget(id) && rendersContent(id);
1435
+ const pos = (p) => {
1436
+ const block = ref(p.block);
1437
+ const len = holds(block) ? displayLength(block) : -1;
1438
+ return { block, offset: Math.max(0, Math.min(p.offset, len)) };
1439
+ };
1440
+ let [s, e] = [pos(from), pos(to)];
1441
+ if (!holds(s.block) || !holds(e.block))
1442
+ return REFUSED;
1443
+ const { ids, at } = c.view().order;
1444
+ if (at.get(s.block) > at.get(e.block) || (s.block === e.block && s.offset > e.offset))
1445
+ [s, e] = [e, s];
1446
+ const S = s.block;
1447
+ const E = e.block;
1448
+ if (S === E) {
1449
+ return plan([S], cutText(S, s.offset, e.offset - s.offset), s);
1450
+ }
1451
+ const parent = (id) => ancestorsOf(id)[0] ?? null;
1452
+ const chain = ancestorsOf(E);
1453
+ if (isTableCell(S) || isTableCell(E))
1454
+ return inTables(s, e, view);
1455
+ // What the view hides once `removed` go (nothing: as it is now).
1456
+ const hiddenOnce = (removed) => {
1457
+ const set = new Set(removed);
1458
+ return (id) => !!view.hidden?.(id, set);
1459
+ };
1460
+ const headDies = !keepHead && s.offset === 0;
1461
+ // What the view hides is not in the range: it goes only with a block that goes,
1462
+ // a dying head's body included.
1463
+ const hidden = hiddenOnce(headDies || headSpent ? [S] : []);
1464
+ const between = ids.slice(at.get(S) + 1, at.get(E)).filter((id) => !hidden(id));
1465
+ const lenE = displayLength(E);
1466
+ const tailDies = e.offset === lenE;
1467
+ const merges = !headDies && !tailDies && canMerge(E, S);
1468
+ const tailGone = tailDies || merges;
1469
+ // E's ancestors the range starts before die unless the rescue would cross an island.
1470
+ // A container (a list) is not one: it dies only when the range empties it, and
1471
+ // keeps its later items like the keys do (`del.merge.container`).
1472
+ const partial = chain.filter((a) => (a === (headDies ? S : null) || between.includes(a)) && !isContainer(a));
1473
+ const top = partial.at(-1);
1474
+ const sealed = top !== undefined && chain.slice(0, chain.indexOf(top) + 1).some(isIsland);
1475
+ const spared = new Set(sealed ? chain : []);
1476
+ const doomed = new Set(between.filter((id) => !spared.has(id) && !chain.includes(id)));
1477
+ if (!sealed)
1478
+ partial.forEach((a) => doomed.add(a));
1479
+ if (headDies && !spared.has(S))
1480
+ doomed.add(S);
1481
+ if (tailDies)
1482
+ doomed.add(E);
1483
+ // Rescue what follows the range end right after the topmost dying container's slot
1484
+ // (after, not at it: a concurrent delete of S revives a merged E above them).
1485
+ const home = sealed || top === undefined ? (tailGone ? E : undefined) : top;
1486
+ const rescued = home === undefined ? [] : tailGone ? childrenIds(E) : [E];
1487
+ for (let cur = E; home !== undefined && cur !== home; cur = parent(cur)) {
1488
+ const sibs = childrenIds(parent(cur));
1489
+ rescued.push(...sibs.slice(sibs.indexOf(cur) + 1));
1490
+ }
1491
+ const dest = home === undefined || rescued.length === 0 ? null : positionOf(home);
1492
+ // A container (or an island that renders nothing) losing every child dies too.
1493
+ const gone = (id) => doomed.has(id) || (merges && id === E);
1494
+ const dies = (id) => emptiable(id) || (isIsland(id) && !rendersContent(id));
1495
+ const emptied = (id) => {
1496
+ if (id === null || gone(id) || !dies(id) || id === dest?.parent)
1497
+ return;
1498
+ if (!childrenIds(id).every(gone))
1499
+ return;
1500
+ doomed.add(id);
1501
+ emptied(parent(id));
1502
+ };
1503
+ [...doomed, ...(merges ? [E] : [])].forEach((id) => emptied(parent(id)));
1504
+ // A range inside one `lines` island never removes it: the head stays, emptied
1505
+ // (a code block's Mod+A, `del.range.island-kept`).
1506
+ const island = ancestorsOf(S).find(isIsland);
1507
+ const linesIsland = island !== undefined && isLines(island);
1508
+ if (!keepHead && linesIsland && doomed.has(island) && chain.includes(island))
1509
+ return rangeDelete(true, headDies)(from, to, view);
1510
+ const lenS = displayLength(S);
1511
+ const writes = [];
1512
+ if (!doomed.has(S))
1513
+ writes.push(...cutText(S, s.offset, lenS - s.offset));
1514
+ if (!gone(E))
1515
+ writes.push(...cutText(E, 0, e.offset));
1516
+ if (dest !== null) {
1517
+ writes.push(...move(rescued, dest.parent, dest.index + 1));
1518
+ // Each shows the kind of its new slot (an island's line, a list's item leave theirs).
1519
+ for (const id of rescued)
1520
+ writes.push(...settle(parent(id), [id], dest.parent));
1521
+ }
1522
+ if (merges) {
1523
+ // The whole tail merges, then its cut prefix goes: E's own text is never written.
1524
+ writes.push({ op: 'mergeBlocks', from: E, into: S, at: s.offset, length: lenE });
1525
+ writes.push(...cutText(S, s.offset, e.offset));
1526
+ }
1527
+ const kept = [...rescued, ...(merges ? [E] : [])];
1528
+ for (const id of doomed)
1529
+ if (!doomed.has(parent(id)))
1530
+ writes.push(remove(id, kept));
1531
+ // A layout the range leaves with one column dissolves (`layout.dissolving`).
1532
+ writes.push(...dissolving(new Set([...doomed, ...(merges ? [E] : [])]), rescued, writes));
1533
+ const caret = (() => {
1534
+ if (!doomed.has(S))
1535
+ return s;
1536
+ if (!gone(E))
1537
+ return { block: E, offset: 0 };
1538
+ const survives = (id) => id === null || rescued.includes(id) || (!gone(id) && survives(parent(id)));
1539
+ // Shown once the plan runs: a dying closed tail's rescued body is.
1540
+ const hiddenAfter = hiddenOnce([...doomed, ...(merges ? [E] : [])]);
1541
+ const shown = (id) => survives(id) && holds(id) && !hiddenAfter(id);
1542
+ // Before the tail: blocks a sealed rescue spared between S and E count too.
1543
+ const prev = ids.slice(0, at.get(E)).findLast(shown);
1544
+ if (prev !== undefined)
1545
+ return { block: prev, offset: displayLength(prev) };
1546
+ const next = ids.slice(at.get(E) + 1).find(shown);
1547
+ return next === undefined ? null : { block: next, offset: 0 };
1548
+ })();
1549
+ // Nothing else left to hold the caret: the head stays, emptied (whole-doc).
1550
+ if (caret === null)
1551
+ return rangeDelete(true, headDies)(from, to, view);
1552
+ return plan([caret.block], writes, caret);
1553
+ };
1554
+ /**
1555
+ * `table.range`: a range with an end in a cell. Every cell it crosses
1556
+ * loses the text it covers and stays, with its row; the ends keep what
1557
+ * lies outside the range, merging nothing; any other block between the
1558
+ * ends goes (a table wholly between them whole), but the ancestors of
1559
+ * the end, which stay and lose their text (the range covers it: they
1560
+ * come after the start in reading order); the caret lands at the start.
1561
+ */
1562
+ const inTables = (s, e, view) => {
1563
+ const { ids, at } = c.view().order;
1564
+ const [S, E] = [s.block, e.block];
1565
+ const kept = new Set(ancestorsOf(E));
1566
+ const between = ids.slice(at.get(S) + 1, at.get(E)).filter((id) => !view.hidden?.(id));
1567
+ const writes = [];
1568
+ const lenS = displayLength(S);
1569
+ writes.push(...cutText(S, s.offset, lenS - s.offset));
1570
+ const [tableS, tableE] = [tableOf(S), tableOf(E)];
1571
+ const doomed = new Set();
1572
+ for (const id of between) {
1573
+ const table = tableOf(id);
1574
+ if (kept.has(id) || (table !== null && (table === tableS || table === tableE))) {
1575
+ const len = isTableCell(id) || kept.has(id) ? displayLength(id) : 0;
1576
+ writes.push(...cutText(id, 0, len));
1577
+ }
1578
+ else
1579
+ doomed.add(id);
1580
+ }
1581
+ writes.push(...cutText(E, 0, e.offset));
1582
+ const parentOf = (id) => ancestorsOf(id)[0] ?? null;
1583
+ const roots = [...doomed].filter((id) => !doomed.has(parentOf(id)));
1584
+ for (const id of roots)
1585
+ writes.push(remove(id, []));
1586
+ writes.push(...dissolving(new Set(roots), [], writes));
1587
+ return plan([S], writes, s);
1588
+ };
1589
+ // ── flow placement (`insertFlow`) ────────────────────────────────
1590
+ /** What a kind is in a table: the table, its row or its cell. */
1591
+ const tableKind = (kind) => roles.of(kind).table !== undefined
1592
+ ? 'table'
1593
+ : isRowKind(kind)
1594
+ ? 'row'
1595
+ : isCellKind(kind)
1596
+ ? 'cell'
1597
+ : undefined;
1598
+ /**
1599
+ * Placement of an admitted flow — one prepared document operation for
1600
+ * paste, drop and fragment insertion. The rules are the `flow.*` rows of
1601
+ * `docs/editor-delete-contract.md`:
1602
+ *
1603
+ * - a flow is lines with fresh ids: a kinded block, or an inline run (no
1604
+ * `type`); an empty flow changes nothing (`flow.shape`);
1605
+ * - one line joins the text at the position; an empty block takes a kinded
1606
+ * line's kind (`flow.inline`);
1607
+ * - several lines split the block: the first joins the head, the last is the
1608
+ * tail's block and takes the text after the position and the block's
1609
+ * children, the rest go between (`flow.split`) — children the view
1610
+ * hides (a closed toggle's body) stay with the head, as Enter keeps them;
1611
+ * - a line that joins no text (a list, a code block, a divider: it renders
1612
+ * none of its own, or is a void or an island) is placed as a block, never
1613
+ * joined; the text after the position stays in a shown line of the
1614
+ * block's kind (`flow.apart`), and the caret ends the last pasted
1615
+ * shown line;
1616
+ * - in a code line (a `lines` island) the lines, nested ones included, are
1617
+ * placed as plain lines (`flow.lines`);
1618
+ * - a `whole` flow (a block-selection copy) goes after the block, replacing
1619
+ * it when empty (`flow.whole`); over selected blocks the lines take their
1620
+ * slot (`flow.slot`), and at a slot they are placed with nothing replaced
1621
+ * (`flow.place`: an accepted suggestion), as plain inserts (`insertBlocks`: their order
1622
+ * against a peer's split beside them is not claimed); a void
1623
+ * takes one run (`flow.void`);
1624
+ * - a table's cell is never split: it takes the lines a flow shows, nested
1625
+ * ones and a pasted table's cells included, as one text joined by line
1626
+ * breaks; a cell outside a row (cells copied out of a table) is a line of
1627
+ * text, and a row outside a table its cells (`table.paste`);
1628
+ * - a plain line that lands directly in a container takes its item kind
1629
+ * (the document's `fitted`: a pasted paragraph in a list is its item),
1630
+ * and so does a line of the list's flat item kind (the view's
1631
+ * `itemKind`: a pasted numbered item in an `ordered-list`); any
1632
+ * other kind keeps its kind and data (a pasted image stays an image, a
1633
+ * bulleted item in an `ordered-list` a bulleted item).
1634
+ */
1635
+ const insertFlow = (target, flow, view = {}) => {
1636
+ // Ingress: a run carries a placeholder kind through the spec sanitizer.
1637
+ let lines = flow.lines
1638
+ .map((l) => sanitizeSpec({ ...l, type: l.type ?? '' }))
1639
+ .map((s) => ({ ...s, type: s.type || undefined }));
1640
+ if (lines.length === 0)
1641
+ return plan([], []);
1642
+ // A cell outside a row is its text, a row outside a table its cells (`table.paste`).
1643
+ const unbare = (l, holder) => {
1644
+ const kind = l.type ? tableKind(l.type) : undefined;
1645
+ const kids = (next) => (l.children ?? []).flatMap((kid) => unbare(kid, next));
1646
+ if (kind === 'cell' && holder !== 'row')
1647
+ return [{ id: l.id, content: l.content }, ...kids(null)];
1648
+ if (kind === 'row' && holder !== 'table')
1649
+ return kids(null);
1650
+ if (!l.children)
1651
+ return [l];
1652
+ return [{ ...l, children: kids(kind === 'cell' ? null : (kind ?? null)) }];
1653
+ };
1654
+ lines = lines.flatMap((l) => unbare(l, null));
1655
+ /** A line's kind under `parent`: the list's item for its flat item kind, then `fitted`. */
1656
+ const fit = (parent, kind) => fitted(parent, parent !== null && kind !== undefined && kind === view.itemKind?.(parent)
1657
+ ? defaultChild(parent)
1658
+ : kind);
1659
+ const specs = (parent, of = lines) => of.map((l) => ({ ...l, type: fit(parent, l.type) ?? defaultChild(parent) }));
1660
+ const last = () => lines.at(-1);
1661
+ /** `l` and its nested lines as runs, each line that shows its own content one run. */
1662
+ const plain = (l) => [
1663
+ ...(!l.type || roles.of(l.type).rendersContent ? [{ id: l.id, content: l.content }] : []),
1664
+ ...(l.children ?? []).flatMap(plain)
1665
+ ];
1666
+ const endOf = placedEnd(roles.of);
1667
+ /**
1668
+ * A layout line, here or nested, as its items' lines in reading order, and
1669
+ * any other child it holds (`flow.layout`): no layout lands in an item.
1670
+ */
1671
+ const unwrap = (l) => {
1672
+ const item = l.type ? roles.of(l.type).layout : undefined;
1673
+ if (item === undefined)
1674
+ return [l.children ? { ...l, children: l.children.flatMap(unwrap) } : l];
1675
+ return (l.children ?? []).flatMap((kid) => kid.type === item ? (kid.children ?? []).flatMap(unwrap) : unwrap(kid));
1676
+ };
1677
+ const intoItem = (parent) => {
1678
+ if (parent !== null && insideItem(parent))
1679
+ lines = lines.flatMap(unwrap);
1680
+ };
1681
+ /** Whole blocks at a slot, after `pre`; the caret ends the last one's shown content. */
1682
+ const atSlot = (dest, pre) => {
1683
+ const placed = specs(dest.parent);
1684
+ const p = insertBlocks(dest, placed);
1685
+ const at = endOf(placed.at(-1)) ?? { block: last().id, offset: lengthOf(last()) };
1686
+ // The lines are placed while the replaced blocks still show (each step reads the
1687
+ // display as it stands), then those go.
1688
+ return 'writes' in p ? plan(p.ids, [...p.writes, ...pre], at) : p;
1689
+ };
1690
+ if ('replace' in target || 'slot' in target) {
1691
+ let slot;
1692
+ let pre = [];
1693
+ if ('slot' in target)
1694
+ slot = target.slot;
1695
+ else {
1696
+ // The lines fill the first block's slot: its parent is never emptied
1697
+ // by the delete, so a column keeps its layout (`layout.flow-slot`).
1698
+ const at = c.view().order.at;
1699
+ const first = target.replace
1700
+ .filter((id) => at.has(id))
1701
+ .sort((a, b) => at.get(a) - at.get(b))[0];
1702
+ const del = deleteBlocks(target.replace, false, first === undefined ? undefined : positionOf(first)?.parent);
1703
+ if (!('writes' in del))
1704
+ return del;
1705
+ if (first === undefined)
1706
+ return REFUSED;
1707
+ slot = positionOf(first);
1708
+ pre = [...del.writes];
1709
+ }
1710
+ intoItem(slot.parent);
1711
+ if (lines.length === 0)
1712
+ return plan([], []);
1713
+ // In a code block (selected lines, a slot), the lines are plain lines too (`flow.lines`).
1714
+ if (slot.parent !== null && isLines(slot.parent)) {
1715
+ const shown = lines.flatMap(plain);
1716
+ lines = shown.length ? shown : [{ id: lines[0].id, content: [] }];
1717
+ }
1718
+ return atSlot(slot, pre);
1719
+ }
1720
+ const B = ref(target.block);
1721
+ if (!contentTarget(B) || !rendersContent(B))
1722
+ return REFUSED;
1723
+ const len = displayLength(B);
1724
+ const o = Math.max(0, Math.min(target.offset, len));
1725
+ const { parent, index } = positionOf(B);
1726
+ intoItem(B);
1727
+ const empty = len === 0 && childrenIds(B).length === 0;
1728
+ // A line of a lines island (a code line) takes plain lines: every line the flow
1729
+ // shows, nested ones included, in order; no block lands in the island (`flow.lines`).
1730
+ const inLines = parent !== null && isLines(parent);
1731
+ if (inLines)
1732
+ lines = lines.flatMap(plain);
1733
+ // A table's cell is never split: the lines it shows join its text (`table.paste`).
1734
+ const cell = isTableCell(B);
1735
+ if (cell)
1736
+ lines = lines.flatMap(plain);
1737
+ // A line that joins no text under `under` (`flow.apart`): its kind renders
1738
+ // none of its own (a list, a code block), or is a void or an island.
1739
+ const apart = (l, under) => {
1740
+ const kind = l.type && fit(under, l.type);
1741
+ const role = kind ? roles.of(kind) : undefined;
1742
+ return !!role && (!role.rendersContent || role.childless || role.island);
1743
+ };
1744
+ // At the end of a container's header whose body shows, what follows the caret
1745
+ // leads its children and the body stays, as Enter opens a first child
1746
+ // (`flow.header`), unless the first line stands apart and replaces an empty
1747
+ // one (`flow.apart`); elsewhere it follows `B` among its siblings.
1748
+ const header = view.header?.(B);
1749
+ // A closed header shows no children: a line it takes brings none into its hidden
1750
+ // body; they follow it, shown (`flow.header`).
1751
+ const shut = header === 'closed';
1752
+ const inside = o === len && !inLines && !flow.whole && header === true && !(empty && apart(lines[0], B));
1753
+ const home = inside ? B : parent;
1754
+ if (lines.length === 0)
1755
+ return plan([B], [], { block: B, offset: o });
1756
+ if (flow.whole && !inLines && !cell)
1757
+ return atSlot({ parent, index: index + 1 }, empty ? [remove(B, [])] : []);
1758
+ // A void is never split: its caption takes the lines as one run; nor is a cell.
1759
+ const br = { kind: 'text', text: '\n' };
1760
+ if (isVoid(B) || cell) {
1761
+ const content = lines.flatMap((l, i) => [...(i ? [br] : []), ...(l.content ?? [])]);
1762
+ lines = [{ id: lines[0].id, content }];
1763
+ }
1764
+ // So a first line it joins leaves its nested lines as lines of the flow after it.
1765
+ if (shut && lines[0].children?.length && !apart(lines[0], home))
1766
+ lines = [{ ...lines[0], children: undefined }, ...lines[0].children, ...lines.slice(1)];
1767
+ const first = lines[0];
1768
+ const [joinsHead, joinsTail] = [!apart(first, home), lines.length > 1 && !apart(last(), home)];
1769
+ const all = specs(home);
1770
+ const placed = all.slice(joinsHead ? 1 : 0, joinsTail ? -1 : undefined);
1771
+ const ids = placed.map((s) => s.id);
1772
+ if (collides(doc, [...(joinsHead ? (first.children ?? []) : []), ...all.slice(joinsHead ? 1 : 0)]))
1773
+ return REFUSED;
1774
+ const writes = [];
1775
+ /** A joined line's children lead the block it joins; `moved` follow them. */
1776
+ const kids = (to, l, moved = []) => {
1777
+ const lead = l?.children ?? [];
1778
+ writes.push(...insertAt(to, 0, lead));
1779
+ if (moved.length)
1780
+ writes.push({ op: 'moveBlocks', ids: moved, parent: to, index: lead.length });
1781
+ };
1782
+ /**
1783
+ * `l` joins `B` at `at`: its content, its kind when `B` shows no text (a header
1784
+ * keeps its own, as Enter does), its children first.
1785
+ */
1786
+ const join = (l, at) => {
1787
+ const end = writeContent(writes, B, at, l.content);
1788
+ const kind = l.type && fit(parent, l.type);
1789
+ if (len === 0 && !header && kind && kind === l.type)
1790
+ writes.push(...attr(B, TYPE, kind), ...(dataSteps(B, replaceData(l.data ?? {})) ?? []));
1791
+ if (!shut)
1792
+ kids(B, l);
1793
+ return end;
1794
+ };
1795
+ /** The placed lines at `at` under `home` (`split`: the tail of the split they come with). */
1796
+ const place = (at, split) => writes.push(...insertAt(home, at, placed, split));
1797
+ // Under a header, the first line's children, the placed lines and the rest, in order.
1798
+ const k = joinsHead ? (first.children?.length ?? 0) : 0;
1799
+ const slot = inside ? k : index + 1;
1800
+ // Where the placed lines end the caret: after their last shown line (`flow.apart`).
1801
+ const end = placed.length ? endOf(placed.at(-1)) : undefined;
1802
+ // Nothing before the position and the first line stands apart: the lines go before `B`,
1803
+ // which keeps its text and takes a joining last line, or goes when empty and not needed.
1804
+ if (!joinsHead && o === 0 && !inside) {
1805
+ place(index);
1806
+ if (joinsTail) {
1807
+ const at = join(last(), 0);
1808
+ // A closed header's taken line's nested lines follow it, the caret ending them.
1809
+ const after = shut ? specs(parent, last().children ?? []) : [];
1810
+ writes.push(...insertAt(parent, index + placed.length + 1, after));
1811
+ const caret = after.length ? endOf(after.at(-1)) : undefined;
1812
+ const touched = [...ids, B, ...after.map((s) => s.id)];
1813
+ return plan(touched, writes, caret ?? { block: B, offset: at });
1814
+ }
1815
+ if (empty && end)
1816
+ return plan(ids, [...writes, remove(B, [])], end);
1817
+ return plan([...ids, B], writes, end ?? { block: B, offset: 0 });
1818
+ }
1819
+ const head = joinsHead ? join(first, o) : o;
1820
+ if (lines.length === 1 && joinsHead)
1821
+ return plan([B], writes, { block: B, offset: head });
1822
+ const moved = inside ? [] : childrenIds(B).filter((id) => !view.hidden?.(id));
1823
+ // Nothing after the position to keep, and the caret has a line: no split.
1824
+ if (!joinsTail && o === len && moved.length === 0 && end) {
1825
+ place(slot);
1826
+ return plan([B, ...ids], writes, end);
1827
+ }
1828
+ // The rest of `B` stays in a shown line: the last line's block when it joins, else
1829
+ // a line of `B`'s kind, as Enter makes; one made only for the caret (after a
1830
+ // divider at the end) or under a header is a fresh line of its parent's default kind.
1831
+ const T = joinsTail ? last().id : newId('b');
1832
+ const lastType = joinsTail && last().type && fit(home, last().type);
1833
+ const bare = !joinsTail && o === len && moved.length === 0;
1834
+ const tail = lastType
1835
+ ? { type: lastType, data: last().data }
1836
+ : bare || inside
1837
+ ? { type: defaultChild(home), data: {} }
1838
+ : { type: kindToCopy(B), data: blockDataOf(B) };
1839
+ // After `B` (under a header: after the first line's children); the placed lines go before it.
1840
+ const length = len - o;
1841
+ writes.push({
1842
+ op: 'splitBlock',
1843
+ id: B,
1844
+ offset: head,
1845
+ length,
1846
+ newId: T,
1847
+ tail,
1848
+ parent: home,
1849
+ index: slot
1850
+ });
1851
+ kids(T, joinsTail ? last() : undefined, moved);
1852
+ place(slot, T);
1853
+ const at = joinsTail
1854
+ ? { block: T, offset: writeContent(writes, T, 0, last().content) }
1855
+ : (end ?? { block: T, offset: 0 });
1856
+ return plan([B, ...ids, T], writes, at);
1857
+ };
1858
+ return {
1859
+ // what the facade composes
1860
+ plan,
1861
+ canPlace,
1862
+ fits,
1863
+ nestParent,
1864
+ canMerge,
1865
+ defaultChild,
1866
+ landingOf,
1867
+ besideAt,
1868
+ /** Every op of the families, prepared. */
1869
+ prepare: {
1870
+ insertBlocks,
1871
+ moveBlocks,
1872
+ unNestBlocks,
1873
+ unNestBlock,
1874
+ liftOut,
1875
+ duplicateBlock,
1876
+ placeBeside,
1877
+ wrapInLayout,
1878
+ splitBlock,
1879
+ mergeBlocks,
1880
+ mergeBackward,
1881
+ mergeForward,
1882
+ deleteBlocks,
1883
+ deleteBlock,
1884
+ setBlockType,
1885
+ patchData,
1886
+ setBlock,
1887
+ insertText,
1888
+ deleteText,
1889
+ formatRange,
1890
+ clearMarks,
1891
+ insertInline,
1892
+ removeInline,
1893
+ insertFlow,
1894
+ deleteRange: rangeDelete(false),
1895
+ replaceRange: rangeDelete(true)
1896
+ }
1897
+ };
1898
+ };