@book.dev/sdk 3.7.0 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai.d.ts +12 -2
- package/dist/ai.js.map +1 -1
- package/dist/blockCatalogue.d.ts +560 -0
- package/dist/blockCatalogue.js +336 -0
- package/dist/blockCatalogue.js.map +1 -0
- package/dist/bookFolder.d.ts +9 -1
- package/dist/bookFolder.js +64 -2
- package/dist/bookFolder.js.map +1 -1
- package/dist/client.d.ts +22 -2
- package/dist/client.js +6 -2
- package/dist/client.js.map +1 -1
- package/dist/content.d.ts +40 -1
- package/dist/content.js +29 -7
- package/dist/content.js.map +1 -1
- package/dist/forwarding/forwardingClient.d.ts +68 -2
- package/dist/forwarding/forwardingClient.js +126 -12
- package/dist/forwarding/forwardingClient.js.map +1 -1
- package/dist/forwarding/index.d.ts +2 -1
- package/dist/forwarding/index.js +2 -1
- package/dist/forwarding/index.js.map +1 -1
- package/dist/forwarding/namespacedKeyStore.d.ts +52 -0
- package/dist/forwarding/namespacedKeyStore.js +99 -0
- package/dist/forwarding/namespacedKeyStore.js.map +1 -0
- package/dist/index.d.ts +10 -5
- package/dist/index.js +10 -5
- package/dist/index.js.map +1 -1
- package/dist/ledger.d.ts +29 -0
- package/dist/ledger.js +43 -0
- package/dist/ledger.js.map +1 -1
- package/dist/ledgerExportSection.d.ts +160 -0
- package/dist/ledgerExportSection.js +594 -0
- package/dist/ledgerExportSection.js.map +1 -0
- package/dist/orderKeys.d.ts +36 -0
- package/dist/orderKeys.js +88 -0
- package/dist/orderKeys.js.map +1 -0
- package/dist/plugins.d.ts +76 -4
- package/dist/plugins.js +95 -12
- package/dist/plugins.js.map +1 -1
- package/dist/provenance.d.ts +13 -0
- package/dist/registryClient.d.ts +227 -0
- package/dist/registryClient.js +397 -0
- package/dist/registryClient.js.map +1 -0
- package/dist/routes.d.ts +9 -0
- package/dist/routes.js +9 -0
- package/dist/routes.js.map +1 -1
- package/dist/suggestions.d.ts +12 -2
- package/dist/tableSnapshot.d.ts +233 -0
- package/dist/tableSnapshot.js +680 -0
- package/dist/tableSnapshot.js.map +1 -0
- package/dist/templates.d.ts +8 -0
- package/dist/templates.js +7 -2
- package/dist/templates.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table STRUCTURE ops over a stored page snapshot (API-3).
|
|
3
|
+
*
|
|
4
|
+
* The editor owns the live-document versions of these ops (`packages/ui/src/
|
|
5
|
+
* blockeditor/model.ts` — `tableInsertRow`, `tableMoveColumn`, …), which mutate a
|
|
6
|
+
* Y.Doc inside one transaction. This module is their SERVER-SIDE twin: the same
|
|
7
|
+
* seven structural ops (plus cell text and row/column tints) applied to the
|
|
8
|
+
* `blockdoc` JSON projection, for the paths that have no live editor — the MCP
|
|
9
|
+
* write tools, and the agent bridge's stored-page fallback.
|
|
10
|
+
*
|
|
11
|
+
* ── THE `col:` / `ord` DECISION ──────────────────────────────────────────────
|
|
12
|
+
* A keyed table carries FRACTIONAL ORDER KEYS: `table.props['col:<colId>']` per
|
|
13
|
+
* column and `row.props.ord` per row (see the model's table contract). Render
|
|
14
|
+
* order is those keys sorted — never array order. A table built by
|
|
15
|
+
* `append_blocks` has NO keys (an MCP client can't invent them), so it is a
|
|
16
|
+
* "legacy" table that renders in array order until something migrates it.
|
|
17
|
+
*
|
|
18
|
+
* These ops MIGRATE EAGERLY: every op runs {@link ensureSnapshotTableOrder}
|
|
19
|
+
* first — a line-for-line mirror of the editor's `ensureTableOrderInTx`, using
|
|
20
|
+
* the SAME shared key algebra (`./orderKeys`, which the editor now imports from
|
|
21
|
+
* here) and the SAME deterministic column ids (`c0…cN-1`) and
|
|
22
|
+
* `keysBetween(null, null, n)` spread. So a table migrated by an MCP op and the
|
|
23
|
+
* same table migrated by the editor end up with IDENTICAL keys.
|
|
24
|
+
*
|
|
25
|
+
* We do NOT take the alternative — "stay positional and let
|
|
26
|
+
* `ensureTableOrderInTx` migrate later" — because three of the ops
|
|
27
|
+
* (`move_row`, `move_column`, and every insert) are DEFINED as order-key edits.
|
|
28
|
+
* A positional-only snapshot op would have to reorder the arrays instead, which
|
|
29
|
+
* (a) gives a different result than the editor for an already-keyed table, and
|
|
30
|
+
* (b) rewrites nodes a concurrent peer is editing, losing the ops' convergence
|
|
31
|
+
* property. Migrating first costs one deterministic pass and makes the editor,
|
|
32
|
+
* agent-proposal and snapshot paths produce the same grid — which is exactly
|
|
33
|
+
* what the cross-path invariant test asserts.
|
|
34
|
+
*
|
|
35
|
+
* Ops here mutate a DEEP COPY of the snapshot's blocks and drop the stale CRDT
|
|
36
|
+
* `update` (like every other snapshot writer), so the next reader rebuilds from
|
|
37
|
+
* the projection.
|
|
38
|
+
*/
|
|
39
|
+
import { shortId } from './database';
|
|
40
|
+
import { isOrderKey, keyBetween, keysBetween, ORDER_KEY_REBALANCE_LENGTH } from './orderKeys';
|
|
41
|
+
/** Prefix of the column-registry entries in a table block's props. */
|
|
42
|
+
export const TABLE_COL_PREFIX = 'col:';
|
|
43
|
+
/** Prefix of the per-column colour entries (`colbg:<colId>` → palette token). */
|
|
44
|
+
export const TABLE_COLBG_PREFIX = 'colbg:';
|
|
45
|
+
/**
|
|
46
|
+
* The table order-contract PRIVATE keys (TBL-1): `row.props.ord`,
|
|
47
|
+
* `cell.props.col`, the `col:<id>` column registry, and the `colbg:<id>`
|
|
48
|
+
* column tints. They encode a table's cell order/identity — writing them
|
|
49
|
+
* through a generic props tool corrupts or hides cells, so EVERY
|
|
50
|
+
* update_block_props surface (MCP server AND the in-app agent) refuses them
|
|
51
|
+
* with {@link tableOrderContractRefusal} and points at the sanctioned table
|
|
52
|
+
* structure tools. Returns the offending key, or null when `props` touches
|
|
53
|
+
* none.
|
|
54
|
+
*/
|
|
55
|
+
export function tableOrderContractKey(props) {
|
|
56
|
+
return (Object.keys(props).find((k) => k === 'ord' || k === 'col' || k.startsWith(TABLE_COL_PREFIX) || k.startsWith(TABLE_COLBG_PREFIX)) ?? null);
|
|
57
|
+
}
|
|
58
|
+
/** The shared refusal message for a {@link tableOrderContractKey} hit — the
|
|
59
|
+
* SAME wording on both write surfaces, naming the table tools that own the
|
|
60
|
+
* keys. */
|
|
61
|
+
export const tableOrderContractRefusal = (key) => `"${key}" is a private table order-contract key — use the table tools (inspect_table, then table_insert_row / table_move_row / table_insert_column / table_move_column / table_set_row_color / table_set_column_color) to change a table's structure, not update_block_props.`;
|
|
62
|
+
/** Every {@link TableOpKind}, for schema/description generation. */
|
|
63
|
+
export const TABLE_OP_KINDS = [
|
|
64
|
+
'table_insert_row',
|
|
65
|
+
'table_delete_row',
|
|
66
|
+
'table_duplicate_row',
|
|
67
|
+
'table_insert_column',
|
|
68
|
+
'table_delete_column',
|
|
69
|
+
'table_move_row',
|
|
70
|
+
'table_move_column',
|
|
71
|
+
'table_set_cell',
|
|
72
|
+
'table_set_row_color',
|
|
73
|
+
'table_set_column_color',
|
|
74
|
+
];
|
|
75
|
+
const ordinal = (n) => `${n}`;
|
|
76
|
+
/**
|
|
77
|
+
* Validate a resolved op against the table's shape — the ONE definition of the
|
|
78
|
+
* table ops' server-side invariants, shared by the agent bridge and the MCP
|
|
79
|
+
* tools so both refuse exactly the same requests with exactly the same words.
|
|
80
|
+
* Returns an actionable message, or null when the op is legal.
|
|
81
|
+
*
|
|
82
|
+
* Mirrors the editor's context-menu guards (`BlockEditor.tsx`,
|
|
83
|
+
* `TableCellMenuContent`):
|
|
84
|
+
* · insert-row-above is HIDDEN on the header row, because rendering is
|
|
85
|
+
* positional — the blank new row would become the header and silently demote
|
|
86
|
+
* the real one. Here that is a refusal, not a silent clamp.
|
|
87
|
+
* · move up/down and move left/right are DISABLED at the extremes, which is the
|
|
88
|
+
* same thing as a target index outside `0…n-1`.
|
|
89
|
+
* · row 0 is otherwise an ordinary row: it can be deleted, duplicated, tinted,
|
|
90
|
+
* and another row may be MOVED into position 0 (that promotes it to header —
|
|
91
|
+
* the documented behaviour of contract note 5).
|
|
92
|
+
* Unlike the editor's ops (which CLAMP indices and no-op on a miss), these
|
|
93
|
+
* refuse: a remote caller that mis-addresses a table should hear about it rather
|
|
94
|
+
* than have the edit land somewhere else.
|
|
95
|
+
*/
|
|
96
|
+
export function tableOpError(shape, op) {
|
|
97
|
+
const { rows, cols, header } = shape;
|
|
98
|
+
const int = (v, name) => typeof v !== 'number' || !Number.isInteger(v) ? `${name} must be an integer.` : null;
|
|
99
|
+
const inRange = (v, name, max) => v < 0 || v > max ? `${name} ${ordinal(v)} is out of range — this table has ${rows} row(s) and ${cols} column(s).` : null;
|
|
100
|
+
switch (op.kind) {
|
|
101
|
+
case 'table_insert_row': {
|
|
102
|
+
const bad = int(op.rowIndex, 'rowIndex') ?? inRange(op.rowIndex, 'rowIndex', rows);
|
|
103
|
+
if (bad)
|
|
104
|
+
return bad;
|
|
105
|
+
if (header && op.rowIndex === 0) {
|
|
106
|
+
return 'Cannot insert a row above the header row — rendering is positional, so the new blank row would become the header. Insert at rowIndex 1 to add a row directly below the header.';
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
case 'table_insert_column':
|
|
111
|
+
return int(op.colIndex, 'colIndex') ?? inRange(op.colIndex, 'colIndex', cols);
|
|
112
|
+
case 'table_delete_row':
|
|
113
|
+
case 'table_duplicate_row':
|
|
114
|
+
case 'table_set_row_color':
|
|
115
|
+
return int(op.rowIndex, 'rowIndex') ?? inRange(op.rowIndex, 'rowIndex', rows - 1);
|
|
116
|
+
case 'table_delete_column':
|
|
117
|
+
case 'table_set_column_color':
|
|
118
|
+
return int(op.colIndex, 'colIndex') ?? inRange(op.colIndex, 'colIndex', cols - 1);
|
|
119
|
+
case 'table_move_row': {
|
|
120
|
+
const bad = int(op.rowIndex, 'rowIndex') ??
|
|
121
|
+
inRange(op.rowIndex, 'rowIndex', rows - 1) ??
|
|
122
|
+
int(op.toIndex, 'toIndex') ??
|
|
123
|
+
inRange(op.toIndex, 'toIndex', rows - 1);
|
|
124
|
+
return bad;
|
|
125
|
+
}
|
|
126
|
+
case 'table_move_column':
|
|
127
|
+
return (int(op.colIndex, 'colIndex') ??
|
|
128
|
+
inRange(op.colIndex, 'colIndex', cols - 1) ??
|
|
129
|
+
int(op.toIndex, 'toIndex') ??
|
|
130
|
+
inRange(op.toIndex, 'toIndex', cols - 1));
|
|
131
|
+
case 'table_set_cell': {
|
|
132
|
+
const bad = int(op.rowIndex, 'rowIndex') ??
|
|
133
|
+
inRange(op.rowIndex, 'rowIndex', rows - 1) ??
|
|
134
|
+
int(op.colIndex, 'colIndex') ??
|
|
135
|
+
inRange(op.colIndex, 'colIndex', cols - 1);
|
|
136
|
+
if (bad)
|
|
137
|
+
return bad;
|
|
138
|
+
return typeof op.text === 'string' ? null : 'text must be a string.';
|
|
139
|
+
}
|
|
140
|
+
default:
|
|
141
|
+
return `Unknown table op "${String(op.kind)}".`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** True for the two ops whose "last one" case removes the whole table block. */
|
|
145
|
+
export function tableOpRemovesTable(shape, op) {
|
|
146
|
+
return (op.kind === 'table_delete_row' && shape.rows === 1) || (op.kind === 'table_delete_column' && shape.cols === 1);
|
|
147
|
+
}
|
|
148
|
+
// ── The sorted grid over the JSON projection (mirrors model.ts `tableGrid`) ────
|
|
149
|
+
const propOf = (b, key) => b.props?.[key];
|
|
150
|
+
const strProp = (b, key) => {
|
|
151
|
+
const v = propOf(b, key);
|
|
152
|
+
return typeof v === 'string' && v.length > 0 ? v : null;
|
|
153
|
+
};
|
|
154
|
+
const setProp = (b, key, value) => {
|
|
155
|
+
if (value === undefined) {
|
|
156
|
+
if (b.props)
|
|
157
|
+
delete b.props[key];
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
b.props = { ...(b.props ?? {}), [key]: value };
|
|
161
|
+
};
|
|
162
|
+
const kids = (b) => {
|
|
163
|
+
b.children = b.children ?? [];
|
|
164
|
+
return b.children;
|
|
165
|
+
};
|
|
166
|
+
/** The table's column registry, sorted into render order (key, then id). */
|
|
167
|
+
export function snapshotTableColumns(table) {
|
|
168
|
+
const out = [];
|
|
169
|
+
for (const [k, v] of Object.entries(table.props ?? {})) {
|
|
170
|
+
if (k.startsWith(TABLE_COL_PREFIX) && typeof v === 'string' && v.length > 0) {
|
|
171
|
+
out.push({ id: k.slice(TABLE_COL_PREFIX.length), key: v });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
out.sort((a, b) => (a.key !== b.key ? (a.key < b.key ? -1 : 1) : a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
export function snapshotTableGrid(table) {
|
|
178
|
+
const rawRows = [...(table.children ?? [])];
|
|
179
|
+
const columns = snapshotTableColumns(table);
|
|
180
|
+
const keyed = columns.length > 0;
|
|
181
|
+
const rows = rawRows
|
|
182
|
+
.map((b, i) => ({ b, i, k: strProp(b, 'ord') }))
|
|
183
|
+
.sort((x, y) => {
|
|
184
|
+
if (x.k !== null && y.k !== null) {
|
|
185
|
+
if (x.k !== y.k)
|
|
186
|
+
return x.k < y.k ? -1 : 1;
|
|
187
|
+
const xi = x.b.id ?? '';
|
|
188
|
+
const yi = y.b.id ?? '';
|
|
189
|
+
if (xi !== yi)
|
|
190
|
+
return xi < yi ? -1 : 1;
|
|
191
|
+
return x.i - y.i;
|
|
192
|
+
}
|
|
193
|
+
if (x.k !== null)
|
|
194
|
+
return -1;
|
|
195
|
+
if (y.k !== null)
|
|
196
|
+
return 1;
|
|
197
|
+
return x.i - y.i;
|
|
198
|
+
})
|
|
199
|
+
.map((e) => e.b);
|
|
200
|
+
const colIndex = new Map(columns.map((c, i) => [c.id, i]));
|
|
201
|
+
const cells = rows.map((row) => {
|
|
202
|
+
const raw = [...(row.children ?? [])];
|
|
203
|
+
if (!keyed)
|
|
204
|
+
return raw;
|
|
205
|
+
const slots = columns.map(() => null);
|
|
206
|
+
const loose = [];
|
|
207
|
+
for (const cell of raw) {
|
|
208
|
+
const col = strProp(cell, 'col');
|
|
209
|
+
if (col === null) {
|
|
210
|
+
loose.push(cell);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
const idx = colIndex.get(col);
|
|
214
|
+
if (idx === undefined)
|
|
215
|
+
continue; // column deleted concurrently → hidden
|
|
216
|
+
if (slots[idx] === null)
|
|
217
|
+
slots[idx] = cell;
|
|
218
|
+
else
|
|
219
|
+
loose.push(cell);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
let s = 0;
|
|
223
|
+
for (const cell of loose) {
|
|
224
|
+
while (s < slots.length && slots[s] !== null)
|
|
225
|
+
s += 1;
|
|
226
|
+
if (s < slots.length)
|
|
227
|
+
slots[s] = cell;
|
|
228
|
+
else
|
|
229
|
+
slots.push(cell);
|
|
230
|
+
}
|
|
231
|
+
return slots;
|
|
232
|
+
});
|
|
233
|
+
const width = cells.reduce((m, r) => Math.max(m, r.length), 0);
|
|
234
|
+
return { keyed, rows, colIds: columns.map((c) => c.id), cells, width };
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Lazy migration + backfill of a snapshot table's order keys — the mirror of the
|
|
238
|
+
* editor's `ensureTableOrderInTx`, deterministic in exactly the same way so a
|
|
239
|
+
* migration performed here and one performed in the editor converge. Idempotent.
|
|
240
|
+
*/
|
|
241
|
+
export function ensureSnapshotTableOrder(table) {
|
|
242
|
+
const rawRows = [...(table.children ?? [])];
|
|
243
|
+
if (snapshotTableColumns(table).length === 0) {
|
|
244
|
+
const width = Math.max(1, ...rawRows.map((r) => r.children?.length ?? 0));
|
|
245
|
+
const colIds = Array.from({ length: width }, (_, i) => `c${i}`);
|
|
246
|
+
const colKeys = keysBetween(null, null, width);
|
|
247
|
+
colIds.forEach((id, i) => setProp(table, TABLE_COL_PREFIX + id, colKeys[i]));
|
|
248
|
+
const rowKeys = keysBetween(null, null, rawRows.length);
|
|
249
|
+
rawRows.forEach((row, r) => {
|
|
250
|
+
if (strProp(row, 'ord') === null)
|
|
251
|
+
setProp(row, 'ord', rowKeys[r]);
|
|
252
|
+
const cells = row.children ?? [];
|
|
253
|
+
for (let c = 0; c < cells.length && c < width; c += 1) {
|
|
254
|
+
if (cells[c].type === 'cell' && strProp(cells[c], 'col') === null)
|
|
255
|
+
setProp(cells[c], 'col', colIds[c]);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const grid = snapshotTableGrid(table);
|
|
261
|
+
let prev = null;
|
|
262
|
+
for (const row of grid.rows) {
|
|
263
|
+
const k = strProp(row, 'ord');
|
|
264
|
+
if (k !== null) {
|
|
265
|
+
prev = k;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (prev !== null && !isOrderKey(prev)) {
|
|
269
|
+
const keys = keysBetween(null, null, grid.rows.length);
|
|
270
|
+
grid.rows.forEach((r, i) => setProp(r, 'ord', keys[i]));
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
const next = keyBetween(prev, null);
|
|
274
|
+
setProp(row, 'ord', next);
|
|
275
|
+
prev = next;
|
|
276
|
+
}
|
|
277
|
+
const columns = snapshotTableColumns(table);
|
|
278
|
+
const colIds = columns.map((c) => c.id);
|
|
279
|
+
let lastKey = columns.length > 0 ? columns[columns.length - 1].key : null;
|
|
280
|
+
const maxSlots = grid.cells.reduce((m, r) => Math.max(m, r.length), 0);
|
|
281
|
+
if (colIds.length < maxSlots && lastKey !== null && !isOrderKey(lastKey)) {
|
|
282
|
+
const keys = keysBetween(null, null, columns.length);
|
|
283
|
+
columns.forEach((col, i) => setProp(table, TABLE_COL_PREFIX + col.id, keys[i]));
|
|
284
|
+
lastKey = keys[keys.length - 1] ?? null;
|
|
285
|
+
}
|
|
286
|
+
while (colIds.length < maxSlots) {
|
|
287
|
+
const id = shortId('col');
|
|
288
|
+
lastKey = keyBetween(lastKey, null);
|
|
289
|
+
setProp(table, TABLE_COL_PREFIX + id, lastKey);
|
|
290
|
+
colIds.push(id);
|
|
291
|
+
}
|
|
292
|
+
grid.cells.forEach((slots) => {
|
|
293
|
+
slots.forEach((cell, c) => {
|
|
294
|
+
if (cell && cell.type === 'cell' && strProp(cell, 'col') === null && c < colIds.length) {
|
|
295
|
+
setProp(cell, 'col', colIds[c]);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
/** A key between two bounds, or null when the axis needs a rebalance. */
|
|
301
|
+
function insertionKey(before, after) {
|
|
302
|
+
if (before !== null && after !== null && before >= after)
|
|
303
|
+
return null;
|
|
304
|
+
try {
|
|
305
|
+
const key = keyBetween(before, after);
|
|
306
|
+
return key.length > ORDER_KEY_REBALANCE_LENGTH ? null : key;
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// ── Reading a snapshot's tables ───────────────────────────────────────────────
|
|
313
|
+
const runsText = (b) => (Array.isArray(b.text) ? b.text.map((r) => r.t).join('') : '');
|
|
314
|
+
function blockdocBlocks(data) {
|
|
315
|
+
if (!data || data.editor !== 'blocks')
|
|
316
|
+
return null;
|
|
317
|
+
const bd = data.blockdoc;
|
|
318
|
+
return bd?.blocks ?? [];
|
|
319
|
+
}
|
|
320
|
+
/** Depth-first search for a block by id, plus the list it lives in. */
|
|
321
|
+
function locate(list, id) {
|
|
322
|
+
for (const [i, b] of list.entries()) {
|
|
323
|
+
if (b.id === id)
|
|
324
|
+
return { block: b, parent: list, index: i };
|
|
325
|
+
if (b.children) {
|
|
326
|
+
const hit = locate(b.children, id);
|
|
327
|
+
if (hit)
|
|
328
|
+
return hit;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
/** The block whose `children` contain `id`, at any depth. */
|
|
334
|
+
function parentOf(list, id) {
|
|
335
|
+
for (const b of list) {
|
|
336
|
+
if (!b.children)
|
|
337
|
+
continue;
|
|
338
|
+
if (b.children.some((c) => c.id === id))
|
|
339
|
+
return b;
|
|
340
|
+
const deeper = parentOf(b.children, id);
|
|
341
|
+
if (deeper)
|
|
342
|
+
return deeper;
|
|
343
|
+
}
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
/** The `table` block a `cell` id sits in, with its owning `row`. */
|
|
347
|
+
function cellHome(blocks, cellId) {
|
|
348
|
+
const row = parentOf(blocks, cellId);
|
|
349
|
+
if (!row || row.type !== 'row' || !row.id)
|
|
350
|
+
return null;
|
|
351
|
+
const table = parentOf(blocks, row.id);
|
|
352
|
+
if (!table || table.type !== 'table' || !table.id)
|
|
353
|
+
return null;
|
|
354
|
+
return { row, table };
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Read a table by id from a page snapshot, in SORTED render order. Read-only —
|
|
358
|
+
* it never migrates (like the model's `cellPosition`), so `colIds` is empty for
|
|
359
|
+
* a legacy table even though `cols` already reports its rendered width. Returns
|
|
360
|
+
* null when `tableId` isn't a `table` block on the page.
|
|
361
|
+
*/
|
|
362
|
+
export function snapshotTableView(data, tableId) {
|
|
363
|
+
const blocks = blockdocBlocks(data);
|
|
364
|
+
if (!blocks)
|
|
365
|
+
return null;
|
|
366
|
+
const hit = locate(blocks, tableId);
|
|
367
|
+
if (!hit || hit.block.type !== 'table')
|
|
368
|
+
return null;
|
|
369
|
+
const grid = snapshotTableGrid(hit.block);
|
|
370
|
+
return {
|
|
371
|
+
tableId,
|
|
372
|
+
header: propOf(hit.block, 'header') === true,
|
|
373
|
+
rows: grid.rows.length,
|
|
374
|
+
cols: grid.width,
|
|
375
|
+
rowIds: grid.rows.map((r) => r.id ?? ''),
|
|
376
|
+
colIds: grid.colIds,
|
|
377
|
+
cellIds: grid.cells.map((row) => row.map((c) => c?.id ?? null)),
|
|
378
|
+
cells: grid.cells.map((row) => row.map((c) => (c ? runsText(c) : ''))),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/** Grid coordinates of a cell id — the snapshot mirror of `cellPosition`. */
|
|
382
|
+
export function snapshotCellPosition(data, cellId) {
|
|
383
|
+
const blocks = blockdocBlocks(data);
|
|
384
|
+
if (!blocks)
|
|
385
|
+
return null;
|
|
386
|
+
const cell = locate(blocks, cellId);
|
|
387
|
+
if (!cell || cell.block.type !== 'cell')
|
|
388
|
+
return null;
|
|
389
|
+
const home = cellHome(blocks, cellId);
|
|
390
|
+
if (!home)
|
|
391
|
+
return null;
|
|
392
|
+
const grid = snapshotTableGrid(home.table);
|
|
393
|
+
const r = grid.rows.indexOf(home.row);
|
|
394
|
+
if (r < 0)
|
|
395
|
+
return null;
|
|
396
|
+
const c = grid.cells[r].indexOf(cell.block);
|
|
397
|
+
if (c < 0)
|
|
398
|
+
return null;
|
|
399
|
+
return { tableId: home.table.id, row: r, col: c, rows: grid.rows.length, cols: grid.width };
|
|
400
|
+
}
|
|
401
|
+
/** The id of the table that contains `blockId` (a table, row, or cell id). */
|
|
402
|
+
export function snapshotTableIdFor(data, blockId) {
|
|
403
|
+
const blocks = blockdocBlocks(data);
|
|
404
|
+
if (!blocks)
|
|
405
|
+
return null;
|
|
406
|
+
const hit = locate(blocks, blockId);
|
|
407
|
+
if (!hit)
|
|
408
|
+
return null;
|
|
409
|
+
if (hit.block.type === 'table')
|
|
410
|
+
return blockId;
|
|
411
|
+
if (hit.block.type === 'row') {
|
|
412
|
+
const table = parentOf(blocks, blockId);
|
|
413
|
+
return table?.type === 'table' ? (table.id ?? null) : null;
|
|
414
|
+
}
|
|
415
|
+
if (hit.block.type === 'cell')
|
|
416
|
+
return cellHome(blocks, blockId)?.table.id ?? null;
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Resolve id-based addressing to SORTED indices against a table view. An id that
|
|
421
|
+
* isn't in this table is an error, never a silent fallback.
|
|
422
|
+
*
|
|
423
|
+
* Precedence: for ops that TARGET AN EXISTING NODE, an id wins over the matching
|
|
424
|
+
* index — the id is that node's identity, so a caller who sent both meant the
|
|
425
|
+
* node. For the two INSERT ops the index is a POSITION, not a node, so an id
|
|
426
|
+
* there only serves to name the table and never overrides the position (a
|
|
427
|
+
* `cellId` on `table_insert_row` means "the table this cell is in", not "insert
|
|
428
|
+
* at this cell's row").
|
|
429
|
+
*/
|
|
430
|
+
export function resolveTableOp(view, kind, address) {
|
|
431
|
+
const op = { kind, rowIndex: address.rowIndex, colIndex: address.colIndex, toIndex: address.toIndex };
|
|
432
|
+
if (address.text !== undefined)
|
|
433
|
+
op.text = address.text;
|
|
434
|
+
if (address.color !== undefined)
|
|
435
|
+
op.color = address.color;
|
|
436
|
+
const positional = kind === 'table_insert_row' || kind === 'table_insert_column';
|
|
437
|
+
if (address.cellId !== undefined) {
|
|
438
|
+
let foundRow = -1;
|
|
439
|
+
let foundCol = -1;
|
|
440
|
+
for (let r = 0; r < view.cellIds.length && foundRow < 0; r += 1) {
|
|
441
|
+
const c = view.cellIds[r].indexOf(address.cellId);
|
|
442
|
+
if (c >= 0) {
|
|
443
|
+
foundRow = r;
|
|
444
|
+
foundCol = c;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (foundRow < 0)
|
|
448
|
+
return { error: `No cell "${address.cellId}" in table ${view.tableId} — use inspect_table.` };
|
|
449
|
+
if (!positional) {
|
|
450
|
+
op.rowIndex = foundRow;
|
|
451
|
+
op.colIndex = foundCol;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (address.rowId !== undefined) {
|
|
455
|
+
const r = view.rowIds.indexOf(address.rowId);
|
|
456
|
+
if (r < 0)
|
|
457
|
+
return { error: `No row "${address.rowId}" in table ${view.tableId} — use inspect_table.` };
|
|
458
|
+
if (!positional)
|
|
459
|
+
op.rowIndex = r;
|
|
460
|
+
}
|
|
461
|
+
if (address.colId !== undefined) {
|
|
462
|
+
const c = view.colIds.indexOf(address.colId);
|
|
463
|
+
if (c < 0)
|
|
464
|
+
return { error: `No column "${address.colId}" in table ${view.tableId} — use inspect_table.` };
|
|
465
|
+
if (!positional)
|
|
466
|
+
op.colIndex = c;
|
|
467
|
+
}
|
|
468
|
+
return { op };
|
|
469
|
+
}
|
|
470
|
+
/** The shape an op is validated against, from a read view. */
|
|
471
|
+
export const tableShapeOf = (view) => ({ rows: view.rows, cols: view.cols, header: view.header });
|
|
472
|
+
// ── Applying an op ───────────────────────────────────────────────────────────
|
|
473
|
+
const newCell = (colId) => ({ id: shortId('b'), type: 'cell', props: { col: colId } });
|
|
474
|
+
/** Deep-clone the projection so callers keep an untouched snapshot. */
|
|
475
|
+
const cloneBlocks = (blocks) => JSON.parse(JSON.stringify(blocks));
|
|
476
|
+
/**
|
|
477
|
+
* Apply ONE resolved table op to a page snapshot, returning the new snapshot.
|
|
478
|
+
* Coordinates are SORTED indices ({@link resolveTableOp} turns ids into them);
|
|
479
|
+
* validate with {@link tableOpError} first — this function assumes a legal op
|
|
480
|
+
* and clamps like the editor rather than reporting.
|
|
481
|
+
*
|
|
482
|
+
* Returns null when `tableId` isn't a table on the page. `removedTable` is true
|
|
483
|
+
* when the op deleted the LAST row or column, which removes the whole table
|
|
484
|
+
* block — the editor's behaviour (`tableDeleteRow` / `tableDeleteColumn`), kept
|
|
485
|
+
* identical here so the three paths can't diverge.
|
|
486
|
+
*/
|
|
487
|
+
export function applyTableOpToSnapshot(data, tableId, op) {
|
|
488
|
+
const source = blockdocBlocks(data);
|
|
489
|
+
if (!source)
|
|
490
|
+
return null;
|
|
491
|
+
const blocks = cloneBlocks(source);
|
|
492
|
+
const hit = locate(blocks, tableId);
|
|
493
|
+
if (!hit || hit.block.type !== 'table')
|
|
494
|
+
return null;
|
|
495
|
+
const table = hit.block;
|
|
496
|
+
ensureSnapshotTableOrder(table);
|
|
497
|
+
let removedTable = false;
|
|
498
|
+
const grid = snapshotTableGrid(table);
|
|
499
|
+
const rowsArr = kids(table);
|
|
500
|
+
switch (op.kind) {
|
|
501
|
+
case 'table_insert_row': {
|
|
502
|
+
const at = Math.max(0, Math.min(op.rowIndex ?? 0, grid.rows.length));
|
|
503
|
+
const before = at > 0 ? strProp(grid.rows[at - 1], 'ord') : null;
|
|
504
|
+
const after = at < grid.rows.length ? strProp(grid.rows[at], 'ord') : null;
|
|
505
|
+
let ord = insertionKey(before, after);
|
|
506
|
+
if (ord === null) {
|
|
507
|
+
const keys = keysBetween(null, null, grid.rows.length + 1);
|
|
508
|
+
grid.rows.forEach((row, i) => setProp(row, 'ord', keys[i < at ? i : i + 1]));
|
|
509
|
+
ord = keys[at];
|
|
510
|
+
}
|
|
511
|
+
rowsArr.splice(Math.min(at, rowsArr.length), 0, {
|
|
512
|
+
id: shortId('b'),
|
|
513
|
+
type: 'row',
|
|
514
|
+
props: { ord },
|
|
515
|
+
children: snapshotTableColumns(table).map((c) => newCell(c.id)),
|
|
516
|
+
});
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
case 'table_duplicate_row': {
|
|
520
|
+
const from = op.rowIndex ?? 0;
|
|
521
|
+
const source_ = grid.rows[from];
|
|
522
|
+
const before = strProp(source_, 'ord');
|
|
523
|
+
const after = from + 1 < grid.rows.length ? strProp(grid.rows[from + 1], 'ord') : null;
|
|
524
|
+
let ord = insertionKey(before, after);
|
|
525
|
+
if (ord === null) {
|
|
526
|
+
const keys = keysBetween(null, null, grid.rows.length + 1);
|
|
527
|
+
grid.rows.forEach((row, i) => setProp(row, 'ord', keys[i <= from ? i : i + 1]));
|
|
528
|
+
ord = keys[from + 1];
|
|
529
|
+
}
|
|
530
|
+
const columns = snapshotTableColumns(table);
|
|
531
|
+
const children = columns.map((c, i) => {
|
|
532
|
+
const src = grid.cells[from][i];
|
|
533
|
+
return src && src.type === 'cell'
|
|
534
|
+
? { id: shortId('b'), type: 'cell', ...(src.text ? { text: JSON.parse(JSON.stringify(src.text)) } : {}), props: { ...(src.props ?? {}), col: c.id } }
|
|
535
|
+
: newCell(c.id);
|
|
536
|
+
});
|
|
537
|
+
const arrayIndex = rowsArr.indexOf(source_);
|
|
538
|
+
const at = arrayIndex >= 0 ? arrayIndex + 1 : rowsArr.length;
|
|
539
|
+
rowsArr.splice(Math.min(at, rowsArr.length), 0, { id: shortId('b'), type: 'row', props: { ...(source_.props ?? {}), ord }, children });
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
case 'table_insert_column': {
|
|
543
|
+
const columns = snapshotTableColumns(table);
|
|
544
|
+
const at = Math.max(0, Math.min(op.colIndex ?? 0, columns.length));
|
|
545
|
+
const before = at > 0 ? columns[at - 1].key : null;
|
|
546
|
+
const after = at < columns.length ? columns[at].key : null;
|
|
547
|
+
let key = insertionKey(before, after);
|
|
548
|
+
if (key === null) {
|
|
549
|
+
const keys = keysBetween(null, null, columns.length + 1);
|
|
550
|
+
columns.forEach((c, i) => setProp(table, TABLE_COL_PREFIX + c.id, keys[i < at ? i : i + 1]));
|
|
551
|
+
key = keys[at];
|
|
552
|
+
}
|
|
553
|
+
const id = shortId('col');
|
|
554
|
+
setProp(table, TABLE_COL_PREFIX + id, key);
|
|
555
|
+
for (const row of rowsArr) {
|
|
556
|
+
const cells = kids(row);
|
|
557
|
+
cells.splice(Math.max(0, Math.min(at, cells.length)), 0, newCell(id));
|
|
558
|
+
}
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
case 'table_delete_row': {
|
|
562
|
+
const at = op.rowIndex ?? 0;
|
|
563
|
+
if (grid.rows.length === 1) {
|
|
564
|
+
hit.parent.splice(hit.index, 1);
|
|
565
|
+
removedTable = true;
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
const arrayIndex = rowsArr.indexOf(grid.rows[at]);
|
|
569
|
+
if (arrayIndex >= 0)
|
|
570
|
+
rowsArr.splice(arrayIndex, 1);
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
case 'table_delete_column': {
|
|
574
|
+
const at = op.colIndex ?? 0;
|
|
575
|
+
if (grid.colIds.length === 1) {
|
|
576
|
+
hit.parent.splice(hit.index, 1);
|
|
577
|
+
removedTable = true;
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
setProp(table, TABLE_COL_PREFIX + grid.colIds[at], undefined);
|
|
581
|
+
setProp(table, TABLE_COLBG_PREFIX + grid.colIds[at], undefined);
|
|
582
|
+
grid.rows.forEach((row, r) => {
|
|
583
|
+
const cell = grid.cells[r][at];
|
|
584
|
+
if (!cell)
|
|
585
|
+
return;
|
|
586
|
+
const cells = kids(row);
|
|
587
|
+
const idx = cells.indexOf(cell);
|
|
588
|
+
if (idx >= 0)
|
|
589
|
+
cells.splice(idx, 1);
|
|
590
|
+
});
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
case 'table_move_row': {
|
|
594
|
+
const from = op.rowIndex ?? 0;
|
|
595
|
+
const moved = grid.rows[from];
|
|
596
|
+
const rest = grid.rows.filter((_, i) => i !== from);
|
|
597
|
+
const at = Math.max(0, Math.min(op.toIndex ?? 0, rest.length));
|
|
598
|
+
const before = at > 0 ? strProp(rest[at - 1], 'ord') : null;
|
|
599
|
+
const after = at < rest.length ? strProp(rest[at], 'ord') : null;
|
|
600
|
+
const ord = insertionKey(before, after);
|
|
601
|
+
if (ord !== null) {
|
|
602
|
+
setProp(moved, 'ord', ord);
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
const final = [...rest.slice(0, at), moved, ...rest.slice(at)];
|
|
606
|
+
const keys = keysBetween(null, null, final.length);
|
|
607
|
+
final.forEach((row, i) => setProp(row, 'ord', keys[i]));
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
case 'table_move_column': {
|
|
611
|
+
const columns = snapshotTableColumns(table);
|
|
612
|
+
const from = op.colIndex ?? 0;
|
|
613
|
+
const rest = columns.filter((_, i) => i !== from);
|
|
614
|
+
const at = Math.max(0, Math.min(op.toIndex ?? 0, rest.length));
|
|
615
|
+
const before = at > 0 ? rest[at - 1].key : null;
|
|
616
|
+
const after = at < rest.length ? rest[at].key : null;
|
|
617
|
+
const key = insertionKey(before, after);
|
|
618
|
+
if (key !== null) {
|
|
619
|
+
setProp(table, TABLE_COL_PREFIX + columns[from].id, key);
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
const final = [...rest.slice(0, at), columns[from], ...rest.slice(at)];
|
|
623
|
+
const keys = keysBetween(null, null, final.length);
|
|
624
|
+
final.forEach((c, i) => setProp(table, TABLE_COL_PREFIX + c.id, keys[i]));
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
case 'table_set_cell': {
|
|
628
|
+
// Writes ONE plain run, so any inline formatting in the cell is dropped —
|
|
629
|
+
// the same trade the snapshot `update_block` path makes (a JSON projection
|
|
630
|
+
// has no cursor or diff to preserve marks against; the live-editor path's
|
|
631
|
+
// `replaceText` splices minimally and does keep marks on unchanged ends).
|
|
632
|
+
const cell = grid.cells[op.rowIndex ?? 0]?.[op.colIndex ?? 0];
|
|
633
|
+
// A merge gap has no cell node — materialize one bound to that column, so
|
|
634
|
+
// set_cell can fill a ragged/legacy table instead of failing on a hole.
|
|
635
|
+
if (!cell) {
|
|
636
|
+
const row = grid.rows[op.rowIndex ?? 0];
|
|
637
|
+
const colId = grid.colIds[op.colIndex ?? 0];
|
|
638
|
+
if (row && colId)
|
|
639
|
+
kids(row).push({ ...newCell(colId), text: [{ t: op.text ?? '' }] });
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
cell.text = [{ t: op.text ?? '' }];
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
case 'table_set_row_color': {
|
|
646
|
+
const row = grid.rows[op.rowIndex ?? 0];
|
|
647
|
+
if (row)
|
|
648
|
+
setProp(row, 'bg', op.color ?? undefined);
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
case 'table_set_column_color': {
|
|
652
|
+
const colId = grid.colIds[op.colIndex ?? 0];
|
|
653
|
+
if (colId)
|
|
654
|
+
setProp(table, TABLE_COLBG_PREFIX + colId, op.color ?? undefined);
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
const bd = data.blockdoc;
|
|
659
|
+
return { data: { ...data, blockdoc: { ...bd, update: undefined, blocks } }, removedTable };
|
|
660
|
+
}
|
|
661
|
+
/** Every `table` block on a page, in document order (for `list_tables`). */
|
|
662
|
+
export function snapshotTables(data) {
|
|
663
|
+
const blocks = blockdocBlocks(data);
|
|
664
|
+
if (!blocks)
|
|
665
|
+
return [];
|
|
666
|
+
const out = [];
|
|
667
|
+
const walk = (list) => {
|
|
668
|
+
for (const b of list) {
|
|
669
|
+
if (b.type === 'table' && b.id) {
|
|
670
|
+
const grid = snapshotTableGrid(b);
|
|
671
|
+
out.push({ id: b.id, rows: grid.rows.length, cols: grid.width, header: propOf(b, 'header') === true });
|
|
672
|
+
}
|
|
673
|
+
if (b.children)
|
|
674
|
+
walk(b.children);
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
walk(blocks);
|
|
678
|
+
return out;
|
|
679
|
+
}
|
|
680
|
+
//# sourceMappingURL=tableSnapshot.js.map
|