@panphora/clayjs 1.13.0 → 1.14.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 (137) hide show
  1. package/THIRD-PARTY-NOTICES.md +19 -0
  2. package/dist/clay.standalone.js +69739 -41784
  3. package/dist/server/checkpoint.cjs +70690 -0
  4. package/dist/server/checkpoint.mjs +8 -0
  5. package/dist/server/rooms.cjs +72039 -0
  6. package/dist/server/rooms.mjs +66 -0
  7. package/docs/guide.md +95 -94
  8. package/entries/clay.js +22 -0
  9. package/package.json +16 -2
  10. package/packed-contract.json +17 -3
  11. package/src/core/conflict-download.js +11 -2
  12. package/src/core/host-meta.js +75 -0
  13. package/src/core/persist.js +11 -0
  14. package/src/core/save-core.js +266 -31
  15. package/src/core/save.js +62 -32
  16. package/src/core/snapshot.js +39 -13
  17. package/src/core/style-comparisons.js +37 -0
  18. package/src/dom/insert-style-tag.js +11 -12
  19. package/src/lib/effect.js +130 -0
  20. package/src/lib/style-adoption.js +9 -0
  21. package/src/lib/style.js +44 -0
  22. package/src/live/fast-lane.js +347 -60
  23. package/src/live/local-lane.js +32 -6
  24. package/src/live/ops.js +77 -0
  25. package/src/live/provenance.js +129 -0
  26. package/src/live/save-history.js +188 -0
  27. package/src/live/server-lane.js +9 -2
  28. package/src/live/store.js +194 -16
  29. package/src/loader-logic.js +7 -3
  30. package/src/loader.js +6 -0
  31. package/src/page/address.js +20 -0
  32. package/src/page/adopt-boot.js +189 -0
  33. package/src/page/adopt.js +240 -0
  34. package/src/page/ai-tool.js +188 -0
  35. package/src/page/announce.js +21 -0
  36. package/src/page/blocks.js +1121 -0
  37. package/src/page/blocksel.js +269 -0
  38. package/src/page/board-drag.js +226 -0
  39. package/src/page/board-panel.js +353 -0
  40. package/src/page/board-style.js +146 -0
  41. package/src/page/board-view.js +447 -0
  42. package/src/page/board.js +41 -0
  43. package/src/page/chart.js +220 -0
  44. package/src/page/checklist.js +143 -0
  45. package/src/page/chrome.js +587 -0
  46. package/src/page/csv-import.js +169 -0
  47. package/src/page/drag.js +202 -0
  48. package/src/page/drop.js +112 -0
  49. package/src/page/editor.js +384 -0
  50. package/src/page/highlight.js +268 -0
  51. package/src/page/inline.js +60 -1
  52. package/src/page/items.js +159 -0
  53. package/src/page/link.js +247 -0
  54. package/src/page/markdown.js +71 -0
  55. package/src/page/media.js +570 -0
  56. package/src/page/merge.js +130 -0
  57. package/src/page/model.js +71 -9
  58. package/src/page/placeholder.js +23 -0
  59. package/src/page/projection.js +647 -0
  60. package/src/page/recover.js +193 -0
  61. package/src/page/rich/cell.js +16 -0
  62. package/src/page/rich/chart.js +104 -0
  63. package/src/page/rich/csv.js +118 -0
  64. package/src/page/rich/page-tool.js +86 -0
  65. package/src/page/rich/tool-runtime.js +195 -0
  66. package/src/page/save-projection.js +53 -0
  67. package/src/page/save-states.js +85 -0
  68. package/src/page/schema.js +12 -5
  69. package/src/page/table-kinds.js +20 -0
  70. package/src/page/table-paste.js +57 -0
  71. package/src/page/table-select.js +244 -0
  72. package/src/page/table-style.js +25 -0
  73. package/src/page/table.js +650 -0
  74. package/src/page/themes/content.css +46 -0
  75. package/src/page/themes/light-1.css +269 -0
  76. package/src/page/themes/light-2.css +211 -0
  77. package/src/page/themes/light-3.css +130 -0
  78. package/src/page/themes/light-4.css +299 -0
  79. package/src/page/tool-data.js +25 -0
  80. package/src/page/tool-view.js +68 -0
  81. package/src/page/tools.js +133 -0
  82. package/src/page/view.js +136 -0
  83. package/src/page/ybind/author.js +25 -0
  84. package/src/page/ybind/binding.js +92 -0
  85. package/src/page/ybind/board-commands.js +170 -0
  86. package/src/page/ybind/board.js +89 -0
  87. package/src/page/ybind/bridge.js +382 -0
  88. package/src/page/ybind/conversion.js +26 -0
  89. package/src/page/ybind/core.js +7 -0
  90. package/src/page/ybind/decode.js +208 -0
  91. package/src/page/ybind/doc.js +58 -0
  92. package/src/page/ybind/external.js +190 -0
  93. package/src/page/ybind/group.js +124 -0
  94. package/src/page/ybind/index.js +12 -0
  95. package/src/page/ybind/layout.js +27 -0
  96. package/src/page/ybind/marks.js +18 -0
  97. package/src/page/ybind/migrate.js +152 -0
  98. package/src/page/ybind/moves.js +288 -0
  99. package/src/page/ybind/origins.js +257 -0
  100. package/src/page/ybind/parts.js +44 -0
  101. package/src/page/ybind/projection-map.js +92 -0
  102. package/src/page/ybind/provider.js +285 -0
  103. package/src/page/ybind/recovery.js +411 -0
  104. package/src/page/ybind/seed.js +27 -0
  105. package/src/page/ybind/table.js +166 -0
  106. package/src/page/ybind/tokens.js +93 -0
  107. package/src/page/ybind/walk.js +333 -0
  108. package/src/page/ybind/writer.js +307 -0
  109. package/src/plugins/indicator.js +16 -6
  110. package/src/plugins/live.js +742 -65
  111. package/src/plugins/page.js +547 -0
  112. package/src/plugins/undo.js +60 -3
  113. package/src/plugins/visitor-drafts/prompt.js +4 -6
  114. package/src/server/checkpoint/index.js +218 -0
  115. package/src/server/rooms/index.js +59 -0
  116. package/src/server/rooms/live-data-ops.js +369 -0
  117. package/src/server/rooms/live-data-rooms.js +965 -0
  118. package/src/server/rooms/live-data-tags.js +49 -0
  119. package/src/server/rooms/live-page-history.js +41 -0
  120. package/src/server/rooms/live-provenance.js +142 -0
  121. package/src/server/rooms/page-admission.js +79 -0
  122. package/src/server/rooms/page-conversion.js +62 -0
  123. package/src/server/rooms/page-rebase.js +108 -0
  124. package/src/server/rooms/page-state.js +91 -0
  125. package/src/server/rooms/page-write-admission.js +63 -0
  126. package/src/server/rooms/page-write-guard.js +23 -0
  127. package/src/server/rooms/recent.js +18 -0
  128. package/src/server/rooms/spec-wire.js +17 -0
  129. package/src/standalone.js +22 -0
  130. package/src/sync/live-sync.js +13 -2
  131. package/src/sync/stream.js +62 -16
  132. package/src/ui/index.js +4 -0
  133. package/src/ui/toast.js +21 -5
  134. package/src/vendor/pierre/pierre.vendor.js +3 -0
  135. package/src/vendor/yjs.vendor.js +7 -0
  136. package/src/page/binding.js +0 -647
  137. package/src/page/replay.js +0 -578
@@ -0,0 +1,411 @@
1
+ import * as Y from "../../vendor/yjs.vendor.js";
2
+ import { schema } from "../schema.js";
3
+ import { attrKeys, defaultOf, sameJson } from "./layout.js";
4
+ import { marksAttrs, marksFromAttributes } from "./marks.js";
5
+ import { createOriginIndex } from "./origins.js";
6
+ import { createProjectionMap } from "./projection-map.js";
7
+ import { walk } from "./walk.js";
8
+ import { resolveTableFields } from "./table.js";
9
+ import { resolveBoardFields } from "./board.js";
10
+
11
+ export const REMOVE = "remove";
12
+ export const RESTORE = "restore";
13
+ export const COPY_ACTIVE = "copy-active";
14
+ export const COPY_LOST = "copy-lost";
15
+ export const ATTR = "attr";
16
+ export const TEXT_DELETE = "text-delete";
17
+
18
+ export class RecoveryError extends Error {
19
+ constructor(message) {
20
+ super(message);
21
+ this.name = "RecoveryError";
22
+ }
23
+ }
24
+
25
+ const fail = (message) => { throw new RecoveryError(message); };
26
+ const isClock = (value) => Number.isSafeInteger(value) && value >= 0;
27
+ const isLength = (value) => Number.isSafeInteger(value) && value > 0;
28
+ const DIGITS = /^(?:0|[1-9]\d*)$/;
29
+ const isPlain = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
30
+ const copy = (value) => (value === null || typeof value !== "object" ? value : JSON.parse(JSON.stringify(value)));
31
+ const compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
32
+ const sortedList = (list) => [...list].sort(compare);
33
+
34
+ export const physicalKeyOf = (id) => `${id.client}.${id.clock}`;
35
+ export const authorKey = (client) => JSON.stringify(["author", client]);
36
+ export const removeKey = (sourceKey, operationId) => JSON.stringify([REMOVE, sourceKey, operationId]);
37
+ export const restoreKey = (sourceKey, operationId) => JSON.stringify([RESTORE, sourceKey, operationId]);
38
+ export const copyActiveKey = (sourceKey) => JSON.stringify([COPY_ACTIVE, sourceKey]);
39
+ export const copyLostKey = (sourceKey) => JSON.stringify([COPY_LOST, sourceKey]);
40
+ export const textDeleteKey = (client, clock, operationId) => JSON.stringify([TEXT_DELETE, client, clock, operationId]);
41
+ export const attrKey = (sourceKey, field) => JSON.stringify([ATTR, sourceKey, field]);
42
+
43
+ export function parsePhysicalKey(key) {
44
+ if (typeof key !== "string") return null;
45
+ const dot = key.indexOf(".");
46
+ if (dot < 1 || dot === key.length - 1) return null;
47
+ const left = key.slice(0, dot), right = key.slice(dot + 1);
48
+ if (!DIGITS.test(left) || !DIGITS.test(right)) return null;
49
+ const client = Number(left), clock = Number(right);
50
+ return isClock(client) && isClock(clock) ? { client, clock } : null;
51
+ }
52
+
53
+ const checkFields = (value, allowed, key) => {
54
+ if (!isPlain(value)) fail(`Invalid gone record: ${key}`);
55
+ for (const field of Object.keys(value)) if (!allowed.includes(field)) fail(`Unsupported gone field ${field} in ${key}`);
56
+ };
57
+
58
+ const parseVector = (sv, key) => {
59
+ if (!Array.isArray(sv)) fail(`Invalid state vector in ${key}`);
60
+ const out = [];
61
+ let previous = -1;
62
+ for (const pair of sv) {
63
+ if (!Array.isArray(pair) || pair.length !== 2) fail(`Invalid state vector in ${key}`);
64
+ const [client, clock] = pair;
65
+ if (!isClock(client) || !isClock(clock) || client <= previous) fail(`Invalid state vector in ${key}`);
66
+ previous = client;
67
+ out.push([client, clock]);
68
+ }
69
+ return out;
70
+ };
71
+
72
+ const isRemovalKey = (key) => {
73
+ if (typeof key !== "string") return false;
74
+ if (key.charCodeAt(0) !== 91) return parsePhysicalKey(key) !== null;
75
+ let parts;
76
+ try { parts = JSON.parse(key); } catch { return false; }
77
+ if (!Array.isArray(parts) || !parsePhysicalKey(parts[1])) return false;
78
+ if (parts[0] === REMOVE) return parts.length === 3 && typeof parts[2] === "string" && parts[2] !== "";
79
+ return (parts[0] === COPY_ACTIVE || parts[0] === COPY_LOST) && parts.length === 2;
80
+ };
81
+
82
+ const recordOf = (value, key, out) => {
83
+ if (key.charCodeAt(0) === 91) {
84
+ let parts = null;
85
+ try { parts = JSON.parse(key); } catch { fail(`Malformed gone key: ${key}`); }
86
+ if (!Array.isArray(parts) || typeof parts[0] !== "string") fail(`Malformed gone key: ${key}`);
87
+ if (parts[0] === REMOVE) {
88
+ if (parts.length !== 3 || typeof parts[2] !== "string" || !parts[2]) fail(`Malformed gone key: ${key}`);
89
+ const source = parsePhysicalKey(parts[1]);
90
+ if (!source) fail(`Malformed gone key: ${key}`);
91
+ checkFields(value, ["blockId", "end", "sv"], key);
92
+ if (typeof value.blockId !== "string" || !value.blockId) fail(`Invalid removal record: ${key}`);
93
+ const end = value.end ?? null;
94
+ if (end !== null && !parsePhysicalKey(end)) fail(`Invalid removal record: ${key}`);
95
+ out.removals.push({ key, sourceKey: parts[1], source, operationId: parts[2], blockId: value.blockId, end, sv: parseVector(value.sv, key), legacy: false });
96
+ return;
97
+ }
98
+ if (parts[0] === RESTORE) {
99
+ if (parts.length !== 3 || typeof parts[2] !== "string" || !parts[2]) fail(`Malformed gone key: ${key}`);
100
+ if (!parsePhysicalKey(parts[1])) fail(`Malformed gone key: ${key}`);
101
+ checkFields(value, ["covers", "baseline"], key);
102
+ if (!Array.isArray(value.covers) || value.covers.some((cover) => !isRemovalKey(cover))) fail(`Invalid restore record: ${key}`);
103
+ const baseline = value.baseline ?? {};
104
+ checkFields(baseline, ["type", "attrs"], key);
105
+ if (typeof baseline.type !== "string" || !isPlain(baseline.attrs)) fail(`Invalid restore record: ${key}`);
106
+ out.restores.push({ key, sourceKey: parts[1], operationId: parts[2], covers: sortedList(value.covers), baseline: { type: baseline.type, attrs: copy(baseline.attrs) } });
107
+ return;
108
+ }
109
+ if (parts[0] === COPY_ACTIVE) {
110
+ if (parts.length !== 2 || !parsePhysicalKey(parts[1])) fail(`Malformed gone key: ${key}`);
111
+ if (value !== true) fail(`Invalid copy-active record: ${key}`);
112
+ out.copyActive.add(parts[1]);
113
+ return;
114
+ }
115
+ if (parts[0] === TEXT_DELETE) {
116
+ if (parts.length !== 4 || !isClock(parts[1]) || !isClock(parts[2]) || typeof parts[3] !== "string" || !parts[3]) fail(`Malformed gone key: ${key}`);
117
+ checkFields(value, ["length", "actor", "sv"], key);
118
+ if (!isLength(value.length) || !isClock(parts[2] + value.length - 1)) fail(`Invalid text-delete record: ${key}`);
119
+ if (!isClock(value.actor)) fail(`Invalid text-delete record: ${key}`);
120
+ out.textDeletes.push({ key, client: parts[1], clock: parts[2], length: value.length, actor: value.actor, sv: parseVector(value.sv, key) });
121
+ return;
122
+ }
123
+ if (parts[0] === ATTR) {
124
+ if (parts.length !== 3 || !parsePhysicalKey(parts[1]) || typeof parts[2] !== "string" || !parts[2]) fail(`Malformed gone key: ${key}`);
125
+ if (value === undefined) fail(`Invalid attribute override: ${key}`);
126
+ const fields = out.attrs.get(parts[1]) ?? new Map();
127
+ fields.set(parts[2], copy(value));
128
+ out.attrs.set(parts[1], fields);
129
+ return;
130
+ }
131
+ fail(`Unsupported gone record: ${key}`);
132
+ }
133
+ const source = parsePhysicalKey(key);
134
+ if (!source) fail(`Malformed gone key: ${key}`);
135
+ checkFields(value, ["b", "e"], key);
136
+ if (typeof value.b !== "string" || !value.b) fail(`Invalid gone record: ${key}`);
137
+ const end = value.e ?? null;
138
+ if (end !== null && !parsePhysicalKey(end)) fail(`Invalid gone record: ${key}`);
139
+ out.removals.push({ key, sourceKey: key, source, operationId: null, blockId: value.b, end, sv: [], legacy: true });
140
+ };
141
+
142
+ export function parseGoneIntents(gone, { strict = false } = {}) {
143
+ const out = { removals: [], restores: [], copyActive: new Set(), attrs: new Map(), textDeletes: [] };
144
+ gone.forEach((value, key) => {
145
+ try {
146
+ recordOf(value, key, out);
147
+ } catch (error) {
148
+ if (strict || !(error instanceof RecoveryError)) throw error;
149
+ }
150
+ });
151
+ return out;
152
+ }
153
+
154
+ const canonical = (value) => {
155
+ if (Array.isArray(value)) return value.map(canonical);
156
+ if (value !== null && typeof value === "object") {
157
+ const out = {};
158
+ for (const key of Object.keys(value).sort()) out[key] = canonical(value[key]);
159
+ return out;
160
+ }
161
+ return value;
162
+ };
163
+
164
+ export const canonicalRevision = (record) => JSON.stringify(canonical(record));
165
+
166
+ const typeNode = (name) => schema.nodes[name] ?? schema.nodes.paragraph;
167
+
168
+ const blockOf = (pd, id, overrides = null) => {
169
+ const map = pd.attrs.get(id);
170
+ const named = overrides?.get("t");
171
+ const base = (map && map.get("t")) || "paragraph";
172
+ const type = typeof named === "string" && schema.nodes[named] ? named : base;
173
+ const node = typeNode(type);
174
+ const attrs = {};
175
+ for (const key of attrKeys(node)) attrs[key] = copy(map && map.has(key) ? map.get(key) : defaultOf(node, key));
176
+ if (type === "table") Object.assign(attrs, copy(resolveTableFields(pd.tableFields, id, { cols: attrs.cols, rows: attrs.rows })));
177
+ if (type === "board") Object.assign(attrs, copy(resolveBoardFields(pd.boardFields, id)));
178
+ if (overrides) for (const [field, value] of overrides) if (field !== "t" && Object.hasOwn(attrs, field)) attrs[field] = copy(value);
179
+ return { id, type, attrs };
180
+ };
181
+
182
+ const overridesOf = (fields) => {
183
+ const out = {};
184
+ if (!fields) return out;
185
+ for (const field of [...fields.keys()].sort()) out[field] = copy(fields.get(field));
186
+ return out;
187
+ };
188
+
189
+ export const RECOVERED_PREFIX = "recovered:";
190
+ export const recoveredSource = (id) => (typeof id === "string" && id.startsWith(RECOVERED_PREFIX) ? id.slice(RECOVERED_PREFIX.length) : null);
191
+
192
+ export function recoveredRoot(sourceKey, removals, intents) {
193
+ const active = new Set(removals.map((removal) => removal.key));
194
+ let best = null;
195
+ for (const restore of intents.restores) {
196
+ if (restore.sourceKey !== sourceKey) continue;
197
+ const count = restore.covers.reduce((n, key) => n + (active.has(key) ? 1 : 0), 0);
198
+ if (!best || count > best.count || (count === best.count && compare(restore.key, best.restore.key) < 0)) best = { restore, count };
199
+ }
200
+ if (!best) return null;
201
+ const fields = intents.attrs.get(sourceKey);
202
+ const named = fields?.get("t");
203
+ const base = schema.nodes[best.restore.baseline.type] ? best.restore.baseline.type : "paragraph";
204
+ const type = typeof named === "string" && schema.nodes[named] ? named : base;
205
+ const node = typeNode(type);
206
+ const attrs = {};
207
+ for (const key of attrKeys(node)) {
208
+ if (fields && fields.has(key)) attrs[key] = copy(fields.get(key));
209
+ else if (Object.hasOwn(best.restore.baseline.attrs, key)) attrs[key] = copy(best.restore.baseline.attrs[key]);
210
+ else attrs[key] = copy(defaultOf(node, key));
211
+ }
212
+ return { type, attrs, key: best.restore.key };
213
+ }
214
+
215
+ const rawOf = (attrs) => {
216
+ const out = {};
217
+ for (const key of Object.keys(attrs).sort()) out[key] = copy(attrs[key]);
218
+ return out;
219
+ };
220
+
221
+ const displaced = (item, seen) => seen.some((vector) => item.clock >= (vector.get(item.client) ?? 0));
222
+
223
+ const addClock = (spans, clock) => {
224
+ const last = spans[spans.length - 1];
225
+ if (last && clock <= last.end) {
226
+ if (clock === last.end) last.end = clock + 1;
227
+ return;
228
+ }
229
+ spans.push({ start: clock, end: clock + 1 });
230
+ };
231
+
232
+ const mergeClocks = (clocks) => {
233
+ const spans = [];
234
+ for (const clock of [...clocks].sort(compare)) addClock(spans, clock);
235
+ return spans;
236
+ };
237
+
238
+ const holdsClock = (spans, clock) => {
239
+ let lo = 0, hi = spans.length - 1;
240
+ while (lo <= hi) {
241
+ const mid = (lo + hi) >> 1;
242
+ const span = spans[mid];
243
+ if (clock < span.start) hi = mid - 1;
244
+ else if (clock >= span.end) lo = mid + 1;
245
+ else return true;
246
+ }
247
+ return false;
248
+ };
249
+
250
+ const visibleOriginIndex = (flow, walked, index) => {
251
+ const ranges = createProjectionMap(walked, flow.length).physicalRanges(0, walked.tokens);
252
+ const clocks = new Map();
253
+ const collect = (item, from, to) => {
254
+ for (let offset = from; offset < to; offset++) {
255
+ const source = index.resolve({ client: item.id.client, clock: item.id.clock + offset });
256
+ const list = clocks.get(source.client);
257
+ if (list) list.push(source.clock);
258
+ else clocks.set(source.client, [source.clock]);
259
+ }
260
+ };
261
+ let at = 0, cursor = 0;
262
+ for (let item = flow._start; item && cursor < ranges.length; item = item.right) {
263
+ if (item.deleted || !item.countable) continue;
264
+ const start = at;
265
+ at += item.length;
266
+ const content = item.content;
267
+ const isText = content instanceof Y.ContentString;
268
+ if (!isText && !(content instanceof Y.ContentEmbed)) continue;
269
+ if (!isText && content.embed.b !== undefined) continue;
270
+ while (cursor < ranges.length && ranges[cursor][1] <= start) cursor++;
271
+ let range = cursor;
272
+ while (range < ranges.length && ranges[range][0] < at) {
273
+ collect(item, Math.max(start, ranges[range][0]) - start, Math.min(at, ranges[range][1]) - start);
274
+ if (ranges[range][1] >= at) break;
275
+ range++;
276
+ }
277
+ cursor = range;
278
+ }
279
+ const spans = new Map();
280
+ for (const [client, list] of clocks) spans.set(client, mergeClocks(list));
281
+ return (client, clock) => {
282
+ const list = spans.get(client);
283
+ return list ? holdsClock(list, clock) : false;
284
+ };
285
+ };
286
+
287
+ const runOf = (index, item, seen, type, visible) => {
288
+ const source = index.resolve({ client: item.client, clock: item.clock });
289
+ const marks = item.kind === "text" ? marksAttrs(marksFromAttributes(item.raw, type)) : {};
290
+ const baseline = !displaced(item, seen);
291
+ return { client: item.client, clock: item.clock, length: item.length, sourceClient: source.client, sourceClock: source.clock, baseline, displaced: !baseline && !visible(source.client, source.clock), kind: item.kind, text: item.kind === "text" ? item.text : null, marks, raw: rawOf(item.raw) };
292
+ };
293
+
294
+ const pushRun = (runs, run) => {
295
+ const previous = runs[runs.length - 1];
296
+ if (previous && previous.kind === run.kind && previous.baseline === run.baseline && previous.displaced === run.displaced && previous.client === run.client && previous.clock + previous.length === run.clock && previous.sourceClient === run.sourceClient && previous.sourceClock + previous.length === run.sourceClock && sameJson(previous.marks, run.marks) && sameJson(previous.raw, run.raw)) {
297
+ previous.length += run.length;
298
+ if (previous.kind === "text") previous.text += run.text;
299
+ return;
300
+ }
301
+ runs.push(run);
302
+ };
303
+
304
+ const runOut = (run, authorOf) => ({
305
+ id: physicalKeyOf(run),
306
+ length: run.length,
307
+ source: physicalKeyOf({ client: run.sourceClient, clock: run.sourceClock }),
308
+ baseline: run.baseline,
309
+ displaced: run.displaced,
310
+ kind: run.kind,
311
+ ...(run.kind === "text" ? { text: run.text } : {}),
312
+ marks: run.marks,
313
+ raw: run.raw,
314
+ author: authorOf(run.sourceClient),
315
+ });
316
+
317
+ function sourceRecord(pd, index, sourceKey, removals, hidden, intents, visible, groups) {
318
+ const effective = (blockId, marker, overrides = null) => {
319
+ const boundary = index.boundaryOf(marker);
320
+ if (!boundary || boundary.blockId !== blockId || !boundary.recoveryOf) return blockOf(pd, blockId, overrides);
321
+ const root = recoveredRoot(boundary.recoveryOf, groups.get(boundary.recoveryOf) ?? [], intents);
322
+ const type = typeof overrides?.get("t") === "string" && schema.nodes[overrides.get("t")] ? overrides.get("t") : root?.type ?? "paragraph";
323
+ const attrs = {};
324
+ for (const key of attrKeys(typeNode(type))) attrs[key] = copy(overrides?.has(key) ? overrides.get(key) : root && Object.hasOwn(root.attrs, key) ? root.attrs[key] : defaultOf(typeNode(type), key));
325
+ return { id: blockId, type, attrs };
326
+ };
327
+ const authorOf = (client) => {
328
+ const token = pd.origins.get(authorKey(client));
329
+ return typeof token === "string" && token ? token : null;
330
+ };
331
+ const ordered = [...removals].sort((a, b) => compare(a.key, b.key));
332
+ const blockId = ordered[0].blockId;
333
+ const fields = intents.attrs.get(sourceKey);
334
+ const overrides = overridesOf(fields);
335
+ const root = effective(blockId, ordered[0].source, fields);
336
+ const seenByKey = new Map(ordered.map((removal) => [removal.key, new Map(removal.sv)]));
337
+ const items = new Map();
338
+ for (const removal of ordered) {
339
+ const entry = hidden.get(removal.key);
340
+ if (!entry) continue;
341
+ const seen = seenByKey.get(removal.key);
342
+ for (const item of entry.items) {
343
+ const found = items.get(item.id);
344
+ if (found) found.vectors.push(seen);
345
+ else items.set(item.id, { item, vectors: [seen] });
346
+ }
347
+ }
348
+ const stream = [...items.values()].sort((a, b) => a.item.at - b.item.at);
349
+ const blocks = [{ ...root, author: authorOf(index.resolve(ordered[0].source).client), runs: [] }];
350
+ let current = blocks[0];
351
+ for (const entry of stream) {
352
+ const item = entry.item;
353
+ if (item.kind === "marker") {
354
+ if (index.boundaryOf({ client: item.client, clock: item.clock })?.kind === "restore") continue;
355
+ if (!item.blockId || item.blockId === current.id) continue;
356
+ const boundary = effective(item.blockId, { client: item.client, clock: item.clock });
357
+ current = { ...boundary, author: authorOf(index.resolve({ client: item.client, clock: item.clock }).client), runs: [] };
358
+ blocks.push(current);
359
+ continue;
360
+ }
361
+ if (item.kind === "text") {
362
+ for (let offset = 0; offset < item.length; offset++) {
363
+ pushRun(current.runs, runOf(index, { ...item, clock: item.clock + offset, length: 1, text: item.text[offset] }, entry.vectors, current.type, visible));
364
+ }
365
+ } else pushRun(current.runs, runOf(index, item, entry.vectors, current.type, visible));
366
+ }
367
+ const out = {
368
+ source: sourceKey,
369
+ blockId,
370
+ type: root.type,
371
+ attrs: root.attrs,
372
+ author: blocks[0].author,
373
+ blocks: blocks.map((block) => ({ id: block.id, type: block.type, attrs: block.attrs, author: block.author, runs: block.runs.map((run) => runOut(run, authorOf)) })),
374
+ removals: ordered.map((removal) => ({ key: removal.key, operationId: removal.operationId, end: removal.end, sv: removal.sv })),
375
+ restores: intents.restores.filter((restore) => restore.covers.some((cover) => ordered.some((removal) => removal.key === cover))).sort((a, b) => compare(a.key, b.key)).map((restore) => ({ key: restore.key, covers: sortedList(restore.covers) })),
376
+ copyActive: intents.copyActive.has(sourceKey),
377
+ overrides,
378
+ recoverable: blocks.some((block) => block.runs.some((run) => run.displaced)),
379
+ };
380
+ return { ...out, revision: canonicalRevision(out) };
381
+ }
382
+
383
+ export function reduceRecovery(pd, options = {}) {
384
+ const index = options.index ?? createOriginIndex(pd.origins);
385
+ try {
386
+ const walked = options.walked ?? walk(pd, index);
387
+ const intents = options.intents ?? parseGoneIntents(pd.gone, { strict: options.strict === true });
388
+ const hidden = new Map();
389
+ for (const entry of walked.hidden ?? []) hidden.set(entry.key, entry);
390
+ const groups = new Map();
391
+ for (const removal of walked.removals ?? intents.removals) {
392
+ const list = groups.get(removal.sourceKey);
393
+ if (list) list.push(removal);
394
+ else groups.set(removal.sourceKey, [removal]);
395
+ }
396
+ let visibleIndex = null;
397
+ const visible = (client, clock) => {
398
+ if (!visibleIndex) visibleIndex = visibleOriginIndex(pd.flow, walked, index);
399
+ return visibleIndex(client, clock);
400
+ };
401
+ const sources = new Map();
402
+ for (const [sourceKey, removals] of groups) sources.set(sourceKey, sourceRecord(pd, index, sourceKey, removals, hidden, intents, visible, groups));
403
+ const list = [...sources.values()].sort((a, b) => {
404
+ const left = parsePhysicalKey(a.source), right = parsePhysicalKey(b.source);
405
+ return left.client - right.client || left.clock - right.clock;
406
+ });
407
+ return { sources, list, intents };
408
+ } finally {
409
+ if (!options.index) index.dispose();
410
+ }
411
+ }
@@ -0,0 +1,27 @@
1
+ import * as Y from "../../vendor/yjs.vendor.js";
2
+ import { schema } from "../schema.js";
3
+ import { toBase64 } from "../../vendor/yjs.vendor.js";
4
+ import { SEED, attrKeys, defaultOf, createPageDoc } from "./layout.js";
5
+ import { insertTokens } from "./writer.js";
6
+
7
+ export function seedFrom(pd, blocks) {
8
+ const tx = (fn) => pd.ydoc.transact(fn, SEED);
9
+ const toks = [];
10
+ for (const b of blocks) {
11
+ const type = b.t ?? "paragraph";
12
+ const attrs = Object.fromEntries(attrKeys(schema.nodes[type]).map((k) => [k, b.attrs?.[k] ?? defaultOf(schema.nodes[type], k)]));
13
+ toks.push({ k: "b", id: b.id, t: type, attrs });
14
+ if (b.text) for (const ch of b.text) toks.push({ k: "c", ch, mk: "", ma: {} });
15
+ }
16
+ insertTokens(pd, pd.flow.length, toks, tx);
17
+ }
18
+
19
+ export function seedValue(blocks) {
20
+ const doc = new Y.Doc();
21
+ doc.clientID = 1;
22
+ const y = {};
23
+ let n = 0;
24
+ doc.on("updateV2", (update) => { y[`seed-${String(n++).padStart(4, "0")}`] = toBase64(update); });
25
+ seedFrom(createPageDoc(doc), blocks);
26
+ return { v: 1, g: 0, y };
27
+ }
@@ -0,0 +1,166 @@
1
+ import { schema } from "../schema.js";
2
+ import { marksOf } from "../inline.js";
3
+ import { ATOM, BREAK, canon, unitsOf } from "../projection.js";
4
+ import { cellText } from "../rich/cell.js";
5
+ import { YDecodeError } from "./provider.js";
6
+
7
+ export const TABLE_FIELDS = Object.freeze({ row: Object.freeze(["alive", "pos"]), col: Object.freeze(["alive", "name", "kind", "width", "pos"]) });
8
+ const COL_FIELDS = ["name", "kind", "width", "pos"];
9
+ const OPTIONAL = new Set(["kind", "width"]);
10
+ const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
11
+ const validId = (id) => typeof id === "string" && id.length > 0 && !["__proto__", "constructor", "prototype"].includes(id);
12
+ const fail = (key) => { throw new YDecodeError("unsupported-content", `Unsupported table field: ${key}`); };
13
+ const copy = (value) => (value === null || typeof value !== "object" ? value : JSON.parse(JSON.stringify(value)));
14
+
15
+ export const tableKey = (tableId, kind, entityId, field) => JSON.stringify([tableId, kind, entityId, field]);
16
+
17
+ export function validCell(value) {
18
+ if (typeof value === "string") return true;
19
+ if (!record(value) || Object.keys(value).sort().join(",") !== "c,m" || typeof value.c !== "string") return false;
20
+ if (!Array.isArray(value.m) || !value.m.every(Array.isArray)) return false;
21
+ const units = unitsOf(value.m, value.c.length);
22
+ if (!units) return false;
23
+ return units.every((key, i) => {
24
+ if (typeof key !== "string") return false;
25
+ if (key.startsWith("@")) return key === BREAK && value.c[i] === ATOM;
26
+ try { marksOf(schema, key); return true; } catch { return false; }
27
+ });
28
+ }
29
+
30
+ const validValue = (field, value) => {
31
+ if (field === "alive") return typeof value === "boolean";
32
+ if (field === "width") return value === null || (typeof value === "number" && Number.isFinite(value) && value > 0);
33
+ if (field === "kind") return value === null || typeof value === "string";
34
+ return typeof value === "string";
35
+ };
36
+
37
+ // Peers can each delete a different last column or row; a table must keep one,
38
+ // so the first removed one by position stays until someone edits the table.
39
+ function keepOne(live, dead) {
40
+ if (Object.keys(live).length) return;
41
+ const [first] = Object.keys(dead).sort((x, y) => {
42
+ const px = dead[x].pos ?? "", py = dead[y].pos ?? "";
43
+ return px !== py ? (px < py ? -1 : 1) : x < y ? -1 : x > y ? 1 : 0;
44
+ });
45
+ if (first) live[first] = dead[first];
46
+ }
47
+
48
+ export function resolveTableFields(fields, blockId = null, seed = null) {
49
+ const rows = new Map(), cols = new Map(), cells = new Map();
50
+ for (const [key, value] of fields) {
51
+ let tuple;
52
+ try { tuple = JSON.parse(key); } catch { fail(key); }
53
+ if (!Array.isArray(tuple) || tuple.length !== 4) fail(key);
54
+ const [owner, kind, entityId, field] = tuple;
55
+ if (!validId(owner) || !validId(entityId)) fail(key);
56
+ if (kind === "cell") {
57
+ if (!validId(field) || !validCell(value)) fail(key);
58
+ } else if (!Object.hasOwn(TABLE_FIELDS, kind) || !TABLE_FIELDS[kind].includes(field) || !validValue(field, value)) fail(key);
59
+ if (owner !== blockId) continue;
60
+ if (kind === "cell") {
61
+ const row = cells.get(entityId) ?? new Map();
62
+ row.set(field, value);
63
+ cells.set(entityId, row);
64
+ continue;
65
+ }
66
+ const bucket = kind === "row" ? rows : cols;
67
+ const entity = bucket.get(entityId) ?? {};
68
+ entity[field] = value;
69
+ bucket.set(entityId, entity);
70
+ }
71
+ const outCols = { ...(record(seed?.cols) ? seed.cols : {}) };
72
+ const outRows = { ...(record(seed?.rows) ? seed.rows : {}) };
73
+ const deadCols = {}, deadRows = {};
74
+ for (const [id, over] of cols) {
75
+ const col = record(outCols[id]) ? { ...outCols[id] } : {};
76
+ if (!Object.hasOwn(col, "pos")) col.pos = "";
77
+ if (!Object.hasOwn(col, "name")) col.name = "";
78
+ for (const field of COL_FIELDS) {
79
+ if (!Object.hasOwn(over, field)) continue;
80
+ if (over[field] === null) delete col[field];
81
+ else col[field] = over[field];
82
+ }
83
+ if (over.alive === false || (!Object.hasOwn(outCols, id) && over.alive !== true)) {
84
+ if (over.alive === false) deadCols[id] = col;
85
+ delete outCols[id];
86
+ continue;
87
+ }
88
+ outCols[id] = col;
89
+ }
90
+ for (const id of new Set([...rows.keys(), ...cells.keys()])) {
91
+ const over = rows.get(id) ?? {};
92
+ const row = { pos: "", cells: {}, ...(record(outRows[id]) ? outRows[id] : {}) };
93
+ if (Object.hasOwn(over, "pos")) row.pos = over.pos;
94
+ const own = cells.get(id);
95
+ if (own) row.cells = { ...(record(row.cells) ? row.cells : {}), ...Object.fromEntries([...own].map(([colId, value]) => [colId, copy(value)])) };
96
+ if (over.alive === false || (!Object.hasOwn(outRows, id) && over.alive !== true)) {
97
+ if (over.alive === false) deadRows[id] = row;
98
+ delete outRows[id];
99
+ continue;
100
+ }
101
+ outRows[id] = row;
102
+ }
103
+ keepOne(outCols, deadCols);
104
+ keepOne(outRows, deadRows);
105
+ return { cols: outCols, rows: outRows };
106
+ }
107
+
108
+ export function plainTable(table) {
109
+ const rows = {};
110
+ for (const [id, row] of Object.entries(table?.rows ?? {})) {
111
+ rows[id] = { ...row, cells: Object.fromEntries(Object.entries(record(row?.cells) ? row.cells : {}).map(([colId, value]) => [colId, cellText(value)])) };
112
+ }
113
+ return { cols: table?.cols ?? {}, rows };
114
+ }
115
+
116
+ const fieldOf = (entity, field) => entity?.[field] ?? (OPTIONAL.has(field) ? null : "");
117
+
118
+ export function tableChanges(tableId, fields, seed, before, after) {
119
+ const out = [];
120
+ const stored = (key, fallback) => (fields.has(key) ? fields.get(key) : fallback);
121
+ const push = (key, value) => out.push([key, copy(value)]);
122
+ const beforeCols = before?.cols ?? {}, afterCols = after?.cols ?? {};
123
+ const beforeRows = before?.rows ?? {}, afterRows = after?.rows ?? {};
124
+ for (const id of new Set([...Object.keys(beforeCols), ...Object.keys(afterCols)])) {
125
+ const old = beforeCols[id], next = afterCols[id];
126
+ if (!next) {
127
+ if (old) push(tableKey(tableId, "col", id, "alive"), false);
128
+ continue;
129
+ }
130
+ if (!old || stored(tableKey(tableId, "col", id, "alive"), true) === false) push(tableKey(tableId, "col", id, "alive"), true);
131
+ for (const field of COL_FIELDS) {
132
+ const key = tableKey(tableId, "col", id, field);
133
+ const prior = old ? fieldOf(old, field) : stored(key, fieldOf(seed?.cols?.[id], field));
134
+ if (canon(prior) !== canon(fieldOf(next, field))) push(key, fieldOf(next, field));
135
+ }
136
+ }
137
+ for (const id of new Set([...Object.keys(beforeRows), ...Object.keys(afterRows)])) {
138
+ const old = beforeRows[id], next = afterRows[id];
139
+ if (!next) {
140
+ if (old) push(tableKey(tableId, "row", id, "alive"), false);
141
+ continue;
142
+ }
143
+ if (!old || stored(tableKey(tableId, "row", id, "alive"), true) === false) push(tableKey(tableId, "row", id, "alive"), true);
144
+ const posKey = tableKey(tableId, "row", id, "pos");
145
+ const pos = next.pos ?? "";
146
+ if (stored(posKey, seed?.rows?.[id]?.pos ?? "") !== pos) push(posKey, pos);
147
+ const cells = record(next.cells) ? next.cells : {};
148
+ for (const colId of Object.keys(afterCols)) {
149
+ const key = tableKey(tableId, "cell", id, colId);
150
+ const value = cells[colId] ?? "";
151
+ const prior = old && beforeCols[colId] ? old.cells?.[colId] ?? "" : stored(key, seed?.rows?.[id]?.cells?.[colId] ?? "");
152
+ if (canon(prior) !== canon(value)) push(key, value);
153
+ }
154
+ }
155
+ return out;
156
+ }
157
+
158
+ export function tableSeed(pd, id) {
159
+ const map = pd.attrs.get(id);
160
+ return { cols: map?.get("cols") ?? {}, rows: map?.get("rows") ?? {} };
161
+ }
162
+
163
+ export function writeTable(pd, id, before, after) {
164
+ const seed = tableSeed(pd, id);
165
+ for (const [key, value] of tableChanges(id, pd.tableFields, seed, before, after)) pd.tableFields.set(key, value);
166
+ }