@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.
- package/THIRD-PARTY-NOTICES.md +19 -0
- package/dist/clay.standalone.js +69739 -41784
- package/dist/server/checkpoint.cjs +70690 -0
- package/dist/server/checkpoint.mjs +8 -0
- package/dist/server/rooms.cjs +72039 -0
- package/dist/server/rooms.mjs +66 -0
- package/docs/guide.md +95 -94
- package/entries/clay.js +22 -0
- package/package.json +16 -2
- package/packed-contract.json +17 -3
- package/src/core/conflict-download.js +11 -2
- package/src/core/host-meta.js +75 -0
- package/src/core/persist.js +11 -0
- package/src/core/save-core.js +266 -31
- package/src/core/save.js +62 -32
- package/src/core/snapshot.js +39 -13
- package/src/core/style-comparisons.js +37 -0
- package/src/dom/insert-style-tag.js +11 -12
- package/src/lib/effect.js +130 -0
- package/src/lib/style-adoption.js +9 -0
- package/src/lib/style.js +44 -0
- package/src/live/fast-lane.js +347 -60
- package/src/live/local-lane.js +32 -6
- package/src/live/ops.js +77 -0
- package/src/live/provenance.js +129 -0
- package/src/live/save-history.js +188 -0
- package/src/live/server-lane.js +9 -2
- package/src/live/store.js +194 -16
- package/src/loader-logic.js +7 -3
- package/src/loader.js +6 -0
- package/src/page/address.js +20 -0
- package/src/page/adopt-boot.js +189 -0
- package/src/page/adopt.js +240 -0
- package/src/page/ai-tool.js +188 -0
- package/src/page/announce.js +21 -0
- package/src/page/blocks.js +1121 -0
- package/src/page/blocksel.js +269 -0
- package/src/page/board-drag.js +226 -0
- package/src/page/board-panel.js +353 -0
- package/src/page/board-style.js +146 -0
- package/src/page/board-view.js +447 -0
- package/src/page/board.js +41 -0
- package/src/page/chart.js +220 -0
- package/src/page/checklist.js +143 -0
- package/src/page/chrome.js +587 -0
- package/src/page/csv-import.js +169 -0
- package/src/page/drag.js +202 -0
- package/src/page/drop.js +112 -0
- package/src/page/editor.js +384 -0
- package/src/page/highlight.js +268 -0
- package/src/page/inline.js +60 -1
- package/src/page/items.js +159 -0
- package/src/page/link.js +247 -0
- package/src/page/markdown.js +71 -0
- package/src/page/media.js +570 -0
- package/src/page/merge.js +130 -0
- package/src/page/model.js +71 -9
- package/src/page/placeholder.js +23 -0
- package/src/page/projection.js +647 -0
- package/src/page/recover.js +193 -0
- package/src/page/rich/cell.js +16 -0
- package/src/page/rich/chart.js +104 -0
- package/src/page/rich/csv.js +118 -0
- package/src/page/rich/page-tool.js +86 -0
- package/src/page/rich/tool-runtime.js +195 -0
- package/src/page/save-projection.js +53 -0
- package/src/page/save-states.js +85 -0
- package/src/page/schema.js +12 -5
- package/src/page/table-kinds.js +20 -0
- package/src/page/table-paste.js +57 -0
- package/src/page/table-select.js +244 -0
- package/src/page/table-style.js +25 -0
- package/src/page/table.js +650 -0
- package/src/page/themes/content.css +46 -0
- package/src/page/themes/light-1.css +269 -0
- package/src/page/themes/light-2.css +211 -0
- package/src/page/themes/light-3.css +130 -0
- package/src/page/themes/light-4.css +299 -0
- package/src/page/tool-data.js +25 -0
- package/src/page/tool-view.js +68 -0
- package/src/page/tools.js +133 -0
- package/src/page/view.js +136 -0
- package/src/page/ybind/author.js +25 -0
- package/src/page/ybind/binding.js +92 -0
- package/src/page/ybind/board-commands.js +170 -0
- package/src/page/ybind/board.js +89 -0
- package/src/page/ybind/bridge.js +382 -0
- package/src/page/ybind/conversion.js +26 -0
- package/src/page/ybind/core.js +7 -0
- package/src/page/ybind/decode.js +208 -0
- package/src/page/ybind/doc.js +58 -0
- package/src/page/ybind/external.js +190 -0
- package/src/page/ybind/group.js +124 -0
- package/src/page/ybind/index.js +12 -0
- package/src/page/ybind/layout.js +27 -0
- package/src/page/ybind/marks.js +18 -0
- package/src/page/ybind/migrate.js +152 -0
- package/src/page/ybind/moves.js +288 -0
- package/src/page/ybind/origins.js +257 -0
- package/src/page/ybind/parts.js +44 -0
- package/src/page/ybind/projection-map.js +92 -0
- package/src/page/ybind/provider.js +285 -0
- package/src/page/ybind/recovery.js +411 -0
- package/src/page/ybind/seed.js +27 -0
- package/src/page/ybind/table.js +166 -0
- package/src/page/ybind/tokens.js +93 -0
- package/src/page/ybind/walk.js +333 -0
- package/src/page/ybind/writer.js +307 -0
- package/src/plugins/indicator.js +16 -6
- package/src/plugins/live.js +742 -65
- package/src/plugins/page.js +547 -0
- package/src/plugins/undo.js +60 -3
- package/src/plugins/visitor-drafts/prompt.js +4 -6
- package/src/server/checkpoint/index.js +218 -0
- package/src/server/rooms/index.js +59 -0
- package/src/server/rooms/live-data-ops.js +369 -0
- package/src/server/rooms/live-data-rooms.js +965 -0
- package/src/server/rooms/live-data-tags.js +49 -0
- package/src/server/rooms/live-page-history.js +41 -0
- package/src/server/rooms/live-provenance.js +142 -0
- package/src/server/rooms/page-admission.js +79 -0
- package/src/server/rooms/page-conversion.js +62 -0
- package/src/server/rooms/page-rebase.js +108 -0
- package/src/server/rooms/page-state.js +91 -0
- package/src/server/rooms/page-write-admission.js +63 -0
- package/src/server/rooms/page-write-guard.js +23 -0
- package/src/server/rooms/recent.js +18 -0
- package/src/server/rooms/spec-wire.js +17 -0
- package/src/standalone.js +22 -0
- package/src/sync/live-sync.js +13 -2
- package/src/sync/stream.js +62 -16
- package/src/ui/index.js +4 -0
- package/src/ui/toast.js +21 -5
- package/src/vendor/pierre/pierre.vendor.js +3 -0
- package/src/vendor/yjs.vendor.js +7 -0
- package/src/page/binding.js +0 -647
- package/src/page/replay.js +0 -578
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { schema } from "../schema.js";
|
|
2
|
+
import { nodeFromBlock } from "../model.js";
|
|
3
|
+
import { attrKeys, defaultOf } from "./layout.js";
|
|
4
|
+
import { resolveBoardFields } from "./board.js";
|
|
5
|
+
import { resolveTableFields } from "./table.js";
|
|
6
|
+
import { attrKey } from "./recovery.js";
|
|
7
|
+
|
|
8
|
+
export const fieldsOf = (pd, b, type) => {
|
|
9
|
+
const m = pd.attrs.get(b.baseId);
|
|
10
|
+
const out = {};
|
|
11
|
+
for (const k of attrKeys(type)) out[k] = m && m.has(k) ? m.get(k) : defaultOf(type, k);
|
|
12
|
+
if (type.name === "board") Object.assign(out, resolveBoardFields(pd.boardFields, b.baseId));
|
|
13
|
+
if (type.name === "table") Object.assign(out, resolveTableFields(pd.tableFields, b.baseId, { cols: out.cols, rows: out.rows }));
|
|
14
|
+
return out;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function docOf(pd, w) {
|
|
18
|
+
const blocks = w.visible.map((b) => {
|
|
19
|
+
const type = schema.nodes[b.t] ?? schema.nodes.paragraph;
|
|
20
|
+
return { b, type, fields: { ...(b.fields ?? fieldsOf(pd, b, type)) } };
|
|
21
|
+
});
|
|
22
|
+
const byId = new Map(blocks.map((block) => [block.b.id, block]));
|
|
23
|
+
const rankOf = (block) => {
|
|
24
|
+
const item = block.b.recovered
|
|
25
|
+
? pd.gone._map.get(attrKey(block.b.sourceKey, "parent"))
|
|
26
|
+
: pd.attrs.get(block.b.baseId)?._map.get("parent");
|
|
27
|
+
return item?.id ?? { clock: -1, client: -1 };
|
|
28
|
+
};
|
|
29
|
+
for (const start of byId.keys()) {
|
|
30
|
+
const positions = new Map(), chain = [];
|
|
31
|
+
let id = start;
|
|
32
|
+
while (byId.has(id) && !positions.has(id)) {
|
|
33
|
+
positions.set(id, chain.length);
|
|
34
|
+
chain.push(id);
|
|
35
|
+
id = byId.get(id).fields.parent;
|
|
36
|
+
}
|
|
37
|
+
if (!positions.has(id)) continue;
|
|
38
|
+
const cycle = chain.slice(positions.get(id));
|
|
39
|
+
const lowest = cycle.reduce((left, right) => {
|
|
40
|
+
const a = rankOf(byId.get(left)), b = rankOf(byId.get(right));
|
|
41
|
+
return a.clock < b.clock || (a.clock === b.clock && a.client < b.client) ? left : right;
|
|
42
|
+
});
|
|
43
|
+
byId.get(lowest).fields.parent = null;
|
|
44
|
+
}
|
|
45
|
+
const nodes = blocks.map(({ b, type, fields }) => {
|
|
46
|
+
const attrs = { id: b.id, pos: null, ...fields };
|
|
47
|
+
if (type.name === "table" && Object.keys(attrs.rows ?? {}).length) return nodeFromBlock(schema, b.id, { t: "table", ...attrs });
|
|
48
|
+
if (!type.isTextblock) return type.create(attrs);
|
|
49
|
+
const content = [];
|
|
50
|
+
for (const r of b.runs) {
|
|
51
|
+
if (r.hole) continue;
|
|
52
|
+
if (r.br) content.push(schema.nodes.hard_break.create());
|
|
53
|
+
else content.push(schema.text(r.text, r.marks));
|
|
54
|
+
}
|
|
55
|
+
return type.create(attrs, content);
|
|
56
|
+
});
|
|
57
|
+
return nodes.length ? schema.nodes.doc.create(null, nodes) : schema.nodes.doc.createAndFill();
|
|
58
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import * as Y from "../../vendor/yjs.vendor.js";
|
|
2
|
+
import { transform } from "../../vendor/prosemirror.vendor.js";
|
|
3
|
+
import { schema } from "../schema.js";
|
|
4
|
+
import { nodeFromBlock } from "../model.js";
|
|
5
|
+
import { ATOM, BREAK, canon, normalizeText, renderBlocks, runsOf, unitsOf } from "../projection.js";
|
|
6
|
+
import { writeBoard } from "./board.js";
|
|
7
|
+
import { validCell, writeTable } from "./table.js";
|
|
8
|
+
import { blocksFromYValue } from "./decode.js";
|
|
9
|
+
import { docOf } from "./doc.js";
|
|
10
|
+
import { createPageDoc, attrKeys, defaultOf, mintOp } from "./layout.js";
|
|
11
|
+
import { createProjectionMap } from "./projection-map.js";
|
|
12
|
+
import { docFromValue, enc, PART, YDecodeError } from "./provider.js";
|
|
13
|
+
import { tokensOf, tokenIndex } from "./tokens.js";
|
|
14
|
+
import { walk } from "./walk.js";
|
|
15
|
+
import { applyRegion } from "./writer.js";
|
|
16
|
+
|
|
17
|
+
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
18
|
+
const fail = (message) => { throw new YDecodeError("invalid-content", message); };
|
|
19
|
+
const same = (a, b) => canon(a) === canon(b);
|
|
20
|
+
const copy = (value) => JSON.parse(JSON.stringify(value));
|
|
21
|
+
const idOk = (id) => typeof id === "string" && id.length > 0 && !["__proto__", "constructor", "prototype"].includes(id);
|
|
22
|
+
|
|
23
|
+
function jsonValue(value, seen = new Set()) {
|
|
24
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
25
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
26
|
+
if (!Array.isArray(value) && !record(value) || seen.has(value)) return false;
|
|
27
|
+
seen.add(value);
|
|
28
|
+
const valid = (Array.isArray(value) ? value : Object.values(value)).every((item) => jsonValue(item, seen));
|
|
29
|
+
seen.delete(value);
|
|
30
|
+
return valid;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function tableEntities(kind, value) {
|
|
34
|
+
if (!record(value)) return false;
|
|
35
|
+
return Object.entries(value).every(([id, item]) => {
|
|
36
|
+
if (!idOk(id) || !record(item) || typeof item.pos !== "string") return false;
|
|
37
|
+
if (kind === "cols") return typeof item.name === "string" && (item.kind === undefined || typeof item.kind === "string") && (item.width === undefined || (typeof item.width === "number" && Number.isFinite(item.width) && item.width > 0)) && Object.keys(item).every((key) => ["pos", "name", "kind", "width"].includes(key));
|
|
38
|
+
return record(item.cells) && Object.entries(item.cells).every(([col, cell]) => idOk(col) && validCell(cell)) && Object.keys(item).every((key) => ["pos", "cells"].includes(key));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function validAttr(type, key, value) {
|
|
43
|
+
if (key === "columns" || key === "cards") return type === "board" && record(value);
|
|
44
|
+
if (key === "cols" || key === "rows") return tableEntities(key, value);
|
|
45
|
+
if (key === "state") return record(value);
|
|
46
|
+
if (key === "sort" || key === "artifact") return value === null || record(value);
|
|
47
|
+
if (key === "series") return Array.isArray(value) && value.every((id) => typeof id === "string");
|
|
48
|
+
if (key === "width" || key === "size") return value === null || typeof value === "number" && value >= 0;
|
|
49
|
+
if (key === "level") return Number.isInteger(value) && value >= 1 && value <= 6;
|
|
50
|
+
if (key === "indent") return Number.isInteger(value) && value >= 0;
|
|
51
|
+
if (["parent", "owner", "due", "align", "mime", "source", "x", "highlight", "rev"].includes(key)) return value === null || typeof value === "string";
|
|
52
|
+
if (key === "checked" || key === "open") return typeof value === "boolean";
|
|
53
|
+
return typeof value === "string";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function boardEntities(kind, value) {
|
|
57
|
+
if (!record(value)) fail(`Expected board ${kind} as an object`);
|
|
58
|
+
for (const [id, item] of Object.entries(value)) {
|
|
59
|
+
if (!idOk(id) || !record(item)) fail(`Invalid board ${kind}: ${id}`);
|
|
60
|
+
const required = kind === "columns" ? ["pos", "name"] : ["pos", "col", "title"];
|
|
61
|
+
const allowed = kind === "columns" ? required : [...required, "owner", "due"];
|
|
62
|
+
if (Object.keys(item).some((key) => !allowed.includes(key)) || required.some((key) => typeof item[key] !== "string") ||
|
|
63
|
+
["owner", "due"].some((key) => key in item && item[key] !== null && typeof item[key] !== "string")) fail(`Invalid board ${kind}: ${id}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function validateContent(change, block) {
|
|
68
|
+
if (!Object.hasOwn(change, "content")) return;
|
|
69
|
+
const content = change.content;
|
|
70
|
+
if (!schema.nodes[block.t].isTextblock || !record(content) || typeof content.c !== "string" || Object.keys(content).some((key) => key !== "c" && key !== "m")) fail(`Invalid content for block: ${change.id}`);
|
|
71
|
+
if (content.m !== undefined) {
|
|
72
|
+
const units = unitsOf(content.m, content.c.length);
|
|
73
|
+
if (!units || units.some((key, index) => typeof key !== "string" || (key.startsWith("@") && (key !== BREAK || content.c[index] !== ATOM)) || (block.t === "code_block" && key))) fail(`Invalid marks for block: ${change.id}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function validateChanges(changes, blocks) {
|
|
78
|
+
if (!Array.isArray(changes)) fail("Expected an array of block changes");
|
|
79
|
+
const seen = new Set();
|
|
80
|
+
return changes.map((change) => {
|
|
81
|
+
if (!record(change) || !idOk(change.id) || seen.has(change.id) || !Object.hasOwn(blocks, change.id) || Object.keys(change).some((key) => !["id", "content", "attrs"].includes(key)) || (!Object.hasOwn(change, "content") && !Object.hasOwn(change, "attrs")) || !jsonValue(change)) fail("Invalid block change");
|
|
82
|
+
seen.add(change.id);
|
|
83
|
+
const current = blocks[change.id];
|
|
84
|
+
const type = schema.nodes[current.t];
|
|
85
|
+
if (Object.hasOwn(change, "attrs") && (!record(change.attrs) || Object.entries(change.attrs).some(([key, value]) => !attrKeys(type).includes(key) || !validAttr(current.t, key, value)))) fail(`Invalid attributes for block: ${change.id}`);
|
|
86
|
+
validateContent(change, current);
|
|
87
|
+
if (current.t === "board") {
|
|
88
|
+
if (Object.hasOwn(change.attrs ?? {}, "columns")) boardEntities("columns", change.attrs.columns);
|
|
89
|
+
if (Object.hasOwn(change.attrs ?? {}, "cards")) boardEntities("cards", change.attrs.cards);
|
|
90
|
+
}
|
|
91
|
+
const next = { ...current, ...copy(change.attrs ?? {}) };
|
|
92
|
+
if (change.content) { next.c = change.content.c; delete next.m; if (change.content.m !== undefined) next.m = copy(change.content.m); }
|
|
93
|
+
try { nodeFromBlock(schema, change.id, next).check(); } catch (error) { fail(`Invalid block ${change.id}: ${error.message}`); }
|
|
94
|
+
return { id: change.id, current, next, content: Object.hasOwn(change, "content"), attrs: change.attrs ?? {} };
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function revisionOf(doc) {
|
|
99
|
+
const bytes = await globalThis.crypto.subtle.digest("SHA-256", Y.encodeStateAsUpdateV2(doc));
|
|
100
|
+
return Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const projection = Object.freeze({ ATOM, BREAK, canon, normalizeText, renderBlocks, runsOf });
|
|
104
|
+
|
|
105
|
+
export async function readYValue(value) {
|
|
106
|
+
const doc = docFromValue(value, { strict: true });
|
|
107
|
+
try {
|
|
108
|
+
const blocks = blocksFromYValue(value);
|
|
109
|
+
return { blocks, revision: await revisionOf(doc) };
|
|
110
|
+
} finally { doc.destroy(); }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function writeYValue(value, { expectedRevision, changes } = {}) {
|
|
114
|
+
if (typeof expectedRevision !== "string" || !expectedRevision) throw new YDecodeError("missing-revision", "A page revision is required");
|
|
115
|
+
let source, request;
|
|
116
|
+
try { source = structuredClone(value); request = structuredClone(changes); }
|
|
117
|
+
catch { throw new YDecodeError("invalid-value", "The page write must contain cloneable data"); }
|
|
118
|
+
const { blocks, revision } = await readYValue(source);
|
|
119
|
+
if (expectedRevision !== revision) throw new YDecodeError("stale-revision", "The page revision has changed");
|
|
120
|
+
const edits = validateChanges(request, blocks);
|
|
121
|
+
const doc = docFromValue(source, { strict: true });
|
|
122
|
+
const pending = [];
|
|
123
|
+
const onUpdate = (bytes) => pending.push(bytes);
|
|
124
|
+
doc.on("updateV2", onUpdate);
|
|
125
|
+
try {
|
|
126
|
+
const pd = createPageDoc(doc);
|
|
127
|
+
const w = walk(pd);
|
|
128
|
+
const before = docOf(pd, w);
|
|
129
|
+
const locations = new Map();
|
|
130
|
+
before.forEach((node, offset) => locations.set(node.attrs.id, { node, offset }));
|
|
131
|
+
const tr = new transform.Transform(before);
|
|
132
|
+
const contentEdits = edits.filter((edit) => edit.content && (!same(edit.current.c, edit.next.c) || !same(edit.current.m, edit.next.m)))
|
|
133
|
+
.sort((a, b) => (locations.get(b.id)?.offset ?? -1) - (locations.get(a.id)?.offset ?? -1));
|
|
134
|
+
for (const edit of contentEdits) {
|
|
135
|
+
const found = locations.get(edit.id);
|
|
136
|
+
if (!found) fail(`Block is not directly editable: ${edit.id}`);
|
|
137
|
+
const nextNode = nodeFromBlock(schema, edit.id, edit.next);
|
|
138
|
+
tr.replaceWith(found.offset + 1, found.offset + found.node.nodeSize - 1, nextNode.content);
|
|
139
|
+
}
|
|
140
|
+
const after = new Map();
|
|
141
|
+
tr.doc.forEach((node, offset) => after.set(node.attrs.id, { node, offset }));
|
|
142
|
+
const oldTokens = tokensOf(before, new Map(), w.visible);
|
|
143
|
+
const newTokens = tokensOf(tr.doc, new Map(), w.visible);
|
|
144
|
+
const oldIndex = tokenIndex(before, new Map(), w.visible);
|
|
145
|
+
const newIndex = tokenIndex(tr.doc, new Map(), w.visible);
|
|
146
|
+
const sv = [...Y.decodeStateVector(Y.encodeStateVector(doc)).entries()].sort((a, b) => a[0] - b[0]);
|
|
147
|
+
const ctx = { pd, map: createProjectionMap(w, pd.flow.length), bt: oldTokens, tx: (fn) => fn(), copies: w.copies, fences: w.fences, sv, operationId: mintOp(pd), recovered: new Map(w.visible.filter((seg) => seg.recovered).map((seg) => [seg.id, seg.sourceKey])) };
|
|
148
|
+
doc.transact(() => {
|
|
149
|
+
for (const edit of contentEdits) {
|
|
150
|
+
const old = locations.get(edit.id), next = after.get(edit.id);
|
|
151
|
+
const start = oldIndex[old.offset + 1], end = oldIndex[old.offset + old.node.nodeSize - 1];
|
|
152
|
+
const nextStart = newIndex[next.offset + 1], nextEnd = newIndex[next.offset + next.node.nodeSize - 1];
|
|
153
|
+
applyRegion(ctx, start, oldTokens.slice(start, end), newTokens.slice(nextStart, nextEnd));
|
|
154
|
+
}
|
|
155
|
+
for (const edit of edits) {
|
|
156
|
+
const { id, current, next, attrs } = edit;
|
|
157
|
+
if (current.t === "board" && (Object.hasOwn(attrs, "columns") || Object.hasOwn(attrs, "cards"))) writeBoard(pd, id, current, next);
|
|
158
|
+
const table = current.t === "table" && !ctx.recovered.has(id) && pd.attrs.get(id)?.get("t") === "table";
|
|
159
|
+
if (table && (Object.hasOwn(attrs, "cols") || Object.hasOwn(attrs, "rows"))) writeTable(pd, id, current, next);
|
|
160
|
+
const map = pd.attrs.get(id);
|
|
161
|
+
for (const key of Object.keys(attrs)) {
|
|
162
|
+
if (current.t === "board" && (key === "columns" || key === "cards")) continue;
|
|
163
|
+
if (table && (key === "cols" || key === "rows")) continue;
|
|
164
|
+
const old = map?.has(key) ? map.get(key) : defaultOf(schema.nodes[current.t], key);
|
|
165
|
+
if (!same(old, next[key])) {
|
|
166
|
+
if (map) map.set(key, copy(next[key]));
|
|
167
|
+
else { const created = new Y.Map(); created.set("t", current.t); created.set(key, copy(next[key])); pd.attrs.set(id, created); }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
const updates = {};
|
|
173
|
+
let index = 0;
|
|
174
|
+
for (const bytes of pending) {
|
|
175
|
+
const encoded = enc(bytes);
|
|
176
|
+
let key;
|
|
177
|
+
do { key = `e${doc.clientID.toString(36)}-${(index++).toString(36)}`; } while (Object.keys(source.y).some((existing) => existing === key || existing.startsWith(`${key}.`)));
|
|
178
|
+
if (encoded.length <= PART) updates[key] = encoded;
|
|
179
|
+
else {
|
|
180
|
+
const count = Math.ceil(encoded.length / PART);
|
|
181
|
+
for (let part = 0; part < count; part++) updates[`${key}.${part}.${count}`] = encoded.slice(part * PART, (part + 1) * PART);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const output = { ...source, y: { ...source.y, ...updates } };
|
|
185
|
+
const result = await readYValue(output);
|
|
186
|
+
return { value: output, updates, ...result };
|
|
187
|
+
} finally { doc.off("updateV2", onUpdate); doc.destroy(); }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export { seedPageFromArticle } from "./migrate.js";
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as Y from "../../vendor/yjs.vendor.js";
|
|
2
|
+
import { transform } from "../../vendor/prosemirror.vendor.js";
|
|
3
|
+
import { schema } from "../schema.js";
|
|
4
|
+
import { insideTextblocks } from "../model.js";
|
|
5
|
+
import { LOCAL, HISTORY_META, attrKeys, defaultOf, mintId, mintOp } from "./layout.js";
|
|
6
|
+
import { tokensOf, tokenIndex, textTokenRange, projectionOf, yAfter, sameTok } from "./tokens.js";
|
|
7
|
+
import { CHUNK_CHARS, insertTokens, applyRegion, markRange } from "./writer.js";
|
|
8
|
+
import { walk } from "./walk.js";
|
|
9
|
+
|
|
10
|
+
const { AddMarkStep, RemoveMarkStep, AttrStep, ReplaceStep, Mapping } = transform;
|
|
11
|
+
|
|
12
|
+
const tableRange = (doc, from, to) => {
|
|
13
|
+
const start = doc.resolve(from), end = doc.resolve(to);
|
|
14
|
+
return [
|
|
15
|
+
start.depth && start.node(1).type.name === "table" ? start.before(1) : from,
|
|
16
|
+
end.depth && end.node(1).type.name === "table" ? end.after(1) : to,
|
|
17
|
+
];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const touchesTable = (doc, from, to) => {
|
|
21
|
+
let found = false;
|
|
22
|
+
doc.nodesBetween(from, to, (node) => { if (node.type.name === "table") found = true; return !found && node.type.name !== "table"; });
|
|
23
|
+
return found;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function localGroup(pd, transactions, w, stats, um = null, history = true, onFirstWrite = null) {
|
|
27
|
+
const first = () => { if (onFirstWrite) { const fn = onFirstWrite; onFirstWrite = null; fn(); } };
|
|
28
|
+
const steps = [], docs = [], maps = [];
|
|
29
|
+
for (const tr of transactions) tr.steps.forEach((s, i) => { steps.push(s); docs.push(tr.docs[i]); maps.push(s.getMap()); });
|
|
30
|
+
if (!steps.length) return;
|
|
31
|
+
const mapping = new Mapping(maps);
|
|
32
|
+
const sv = [...Y.decodeStateVector(Y.encodeStateVector(pd.ydoc)).entries()].sort((a, b) => a[0] - b[0]);
|
|
33
|
+
const operationId = mintOp(pd);
|
|
34
|
+
const d0 = docs[0], dm = transactions[transactions.length - 1].doc;
|
|
35
|
+
const visible = w.visible.length === 0 && d0.childCount === 1 && d0.firstChild.type.name === "paragraph"
|
|
36
|
+
? [{ id: d0.firstChild.attrs.id, implicit: true, t: "paragraph", len: 0 }]
|
|
37
|
+
: w.visible;
|
|
38
|
+
const alias = new Map();
|
|
39
|
+
const implicit = [];
|
|
40
|
+
{ let tok = 0; visible.forEach((s) => { if (s.implicit) implicit.push({ tok, id: s.id, t: s.t }); tok += s.len; }); }
|
|
41
|
+
implicit.sort((a, b) => b.tok - a.tok);
|
|
42
|
+
for (const m of implicit) { m.alias = mintId(pd); alias.set(m.id, m.alias); }
|
|
43
|
+
const holes = alias.size ? w.holes.map((h) => ({ at: h.at + implicit.filter((m) => m.tok < h.at).length, len: h.len })) : w.holes;
|
|
44
|
+
const projection = projectionOf(w.tokens + alias.size, holes, pd.flow.length + alias.size);
|
|
45
|
+
const regions = [];
|
|
46
|
+
const markSteps = [];
|
|
47
|
+
steps.forEach((step, i) => {
|
|
48
|
+
const mark = step instanceof AddMarkStep || step instanceof RemoveMarkStep;
|
|
49
|
+
if (mark && !touchesTable(docs[i], step.from, step.to)) { markSteps.push({ i, step }); return; }
|
|
50
|
+
const back = new Mapping(maps.slice(0, i)).invert();
|
|
51
|
+
const fwd = new Mapping(maps.slice(i + 1));
|
|
52
|
+
const ranges = [];
|
|
53
|
+
if (step instanceof AttrStep) ranges.push([step.pos, step.pos + 1, step.pos, step.pos + 1]);
|
|
54
|
+
else if (mark) ranges.push([step.from, step.to, step.from, step.to]);
|
|
55
|
+
else step.getMap().forEach((os, oe, ns, ne) => ranges.push([os, oe, ns, ne]));
|
|
56
|
+
for (const [os, oe, ns, ne] of ranges) {
|
|
57
|
+
const [oa, ob] = tableRange(d0, back.map(os, -1), back.map(oe, 1));
|
|
58
|
+
const [na, nb] = tableRange(dm, fwd.map(ns, -1), fwd.map(ne, 1));
|
|
59
|
+
regions.push({ oa, ob, na, nb });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
regions.sort((x, y) => x.oa - y.oa || x.na - y.na);
|
|
63
|
+
const merged = [];
|
|
64
|
+
for (const r of regions) {
|
|
65
|
+
const last = merged[merged.length - 1];
|
|
66
|
+
if (last && (r.oa <= last.ob || r.na <= last.nb)) { last.ob = Math.max(last.ob, r.ob); last.nb = Math.max(last.nb, r.nb); }
|
|
67
|
+
else merged.push({ ...r });
|
|
68
|
+
}
|
|
69
|
+
const regional = !implicit.length && !markSteps.length && steps.every((step) => step instanceof ReplaceStep) && transactions.every(insideTextblocks);
|
|
70
|
+
const ia = regional ? null : tokenIndex(d0, alias, visible);
|
|
71
|
+
const ib = regional ? null : tokenIndex(dm, alias, visible);
|
|
72
|
+
const bt = regional ? [] : tokensOf(d0, alias, visible);
|
|
73
|
+
const at = regional ? [] : tokensOf(dm, alias, visible);
|
|
74
|
+
const pieces = regional ? merged.map((r) => ({ r, old: textTokenRange(d0, r.oa, r.ob), next: textTokenRange(dm, r.na, r.nb) })) : null;
|
|
75
|
+
const span = regional ? pieces.reduce((n, p) => n + p.next.end - p.next.start, 0) : merged.reduce((n, r) => n + (ib[r.nb] - ib[r.na]), 0);
|
|
76
|
+
const chunked = span > CHUNK_CHARS;
|
|
77
|
+
const tx = chunked ? (fn) => pd.ydoc.transact((tr) => { tr.meta.set(HISTORY_META, history); first(); fn(); }, LOCAL) : (fn) => fn();
|
|
78
|
+
const ctx = { pd, map: projection, bt, tx, copies: w.copies, fences: w.fences, sv, operationId, recovered: new Map(w.visible.filter((seg) => seg.recovered).map((seg) => [seg.id, seg.sourceKey])) };
|
|
79
|
+
const sig = (toks) => toks.filter((t) => t.k === "b").map((t) => `${t.id}|${t.t}|${JSON.stringify(t.attrs)}`).join("\n");
|
|
80
|
+
const structural = !regional && (markSteps.length > 0 || sig(bt) !== sig(at));
|
|
81
|
+
const write = () => {
|
|
82
|
+
for (const m of implicit) {
|
|
83
|
+
insertTokens(pd, yAfter(w.holes, m.tok), [{ k: "b", id: m.alias, t: m.t, attrs: Object.fromEntries(attrKeys(schema.nodes[m.t]).map((kk) => [kk, defaultOf(schema.nodes[m.t], kk)])) }], tx);
|
|
84
|
+
}
|
|
85
|
+
for (let q = merged.length - 1; q >= 0; q--) {
|
|
86
|
+
if (regional) {
|
|
87
|
+
const { r, old, next } = pieces[q];
|
|
88
|
+
const oldSurvives = old.tokens.map((token, j) => {
|
|
89
|
+
const left = mapping.mapResult(r.oa + j, 1);
|
|
90
|
+
const right = mapping.mapResult(r.oa + j + 1, -1);
|
|
91
|
+
const k = left.pos - r.na;
|
|
92
|
+
return !left.deleted && !right.deleted && right.pos === left.pos + 1 && k >= 0 && k < next.tokens.length && sameTok(token, next.tokens[k]);
|
|
93
|
+
});
|
|
94
|
+
applyRegion(ctx, old.start, old.tokens, next.tokens, oldSurvives);
|
|
95
|
+
} else {
|
|
96
|
+
const r = merged[q];
|
|
97
|
+
const ta = ia[r.oa], tb = ia[r.ob];
|
|
98
|
+
applyRegion(ctx, ta, bt.slice(ta, tb), at.slice(ib[r.na], ib[r.nb]));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const markWalk = markSteps.length ? walk(pd) : null;
|
|
102
|
+
const markCtx = markWalk ? { ...ctx, map: projectionOf(markWalk.tokens, markWalk.holes, pd.flow.length) } : null;
|
|
103
|
+
for (const { i, step } of markSteps) {
|
|
104
|
+
const fwd = new Mapping(maps.slice(i + 1));
|
|
105
|
+
const from = ib[fwd.map(step.from, 1)], to = ib[fwd.map(step.to, -1)];
|
|
106
|
+
if (to <= from) continue;
|
|
107
|
+
const name = step.mark.type.name;
|
|
108
|
+
const val = step instanceof AddMarkStep ? (step.mark.attrs && Object.keys(step.mark.attrs).length ? { ...step.mark.attrs } : true) : null;
|
|
109
|
+
tx(() => markRange(markCtx, from, to, name, val, at));
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
um?.stopCapturing();
|
|
113
|
+
const timeout = um?.captureTimeout;
|
|
114
|
+
if (um) um.captureTimeout = Infinity;
|
|
115
|
+
try {
|
|
116
|
+
if (chunked) write();
|
|
117
|
+
else pd.ydoc.transact((tr) => { tr.meta.set(HISTORY_META, history); first(); write(); }, LOCAL);
|
|
118
|
+
} finally {
|
|
119
|
+
if (um) um.captureTimeout = timeout;
|
|
120
|
+
um?.stopCapturing();
|
|
121
|
+
}
|
|
122
|
+
stats.groups++;
|
|
123
|
+
stats.lastGroup = { chunked, structural, regional, span };
|
|
124
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./core.js";
|
|
2
|
+
export * from "./tokens.js";
|
|
3
|
+
export * from "./writer.js";
|
|
4
|
+
export * from "./group.js";
|
|
5
|
+
export * from "./bridge.js";
|
|
6
|
+
export * from "./seed.js";
|
|
7
|
+
export * from "./origins.js";
|
|
8
|
+
export * from "./recovery.js";
|
|
9
|
+
export * from "./moves.js";
|
|
10
|
+
export * from "./binding.js";
|
|
11
|
+
export { authorIdentity } from "./author.js";
|
|
12
|
+
export * from "./provider.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as Y from "../../vendor/yjs.vendor.js";
|
|
2
|
+
import { REMOTE } from "../model.js";
|
|
3
|
+
|
|
4
|
+
export const LOCAL = "page-local";
|
|
5
|
+
export const NET = "page-net";
|
|
6
|
+
export const SEED = "page-seed";
|
|
7
|
+
export const REMOTE_META = REMOTE;
|
|
8
|
+
export const HISTORY_META = Symbol("page-history");
|
|
9
|
+
export const PAGE_ROOTS = Object.freeze(["flow", "attrs", "gone", "boardFields", "tableFields", "origins"]);
|
|
10
|
+
|
|
11
|
+
const SKIP = new Set(["id", "pos"]);
|
|
12
|
+
export const attrKeys = (type) => [...Object.keys(type.spec.attrs || {}).filter((k) => !SKIP.has(k)), ...(type.name === "table" ? ["rows"] : [])];
|
|
13
|
+
export const defaultOf = (type, k) => type.name === "table" && k === "rows" ? {} : type.spec.attrs[k].default;
|
|
14
|
+
export const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
15
|
+
export const idKey = (id) => `${id.client}.${id.clock}`;
|
|
16
|
+
export const parseKey = (k) => { const i = k.indexOf("."); return { client: +k.slice(0, i), clock: +k.slice(i + 1) }; };
|
|
17
|
+
export const isHigh = (u) => u >= 0xd800 && u <= 0xdbff;
|
|
18
|
+
export const isLow = (u) => u >= 0xdc00 && u <= 0xdfff;
|
|
19
|
+
|
|
20
|
+
let opCounter = 0;
|
|
21
|
+
export const mintOp = (pd) => `${pd.ydoc.clientID.toString(36)}-${(opCounter++).toString(36)}`;
|
|
22
|
+
let idCounter = 0;
|
|
23
|
+
export const mintId = (pd) => `b${pd.ydoc.clientID.toString(36)}${(idCounter++).toString(36)}`;
|
|
24
|
+
|
|
25
|
+
export function createPageDoc(ydoc = new Y.Doc()) {
|
|
26
|
+
return { ydoc, flow: ydoc.getText("flow"), attrs: ydoc.getMap("attrs"), gone: ydoc.getMap("gone"), boardFields: ydoc.getMap("boardFields"), tableFields: ydoc.getMap("tableFields"), origins: ydoc.getMap("origins") };
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { schema } from "../schema.js";
|
|
2
|
+
|
|
3
|
+
export const markName = (k) => k.split("!")[0].split("~")[0];
|
|
4
|
+
export const isCancel = (k) => k.includes("!");
|
|
5
|
+
export const marksAttrs = (marks) => { const o = {}; for (const m of marks) o[m.type.name] = m.attrs && Object.keys(m.attrs).length ? { ...m.attrs } : true; return o; };
|
|
6
|
+
export const marksKey = (marks) => marks.map((m) => m.type.name + (m.attrs && Object.keys(m.attrs).length ? JSON.stringify(m.attrs) : "")).sort().join("|");
|
|
7
|
+
export function marksFromAttributes(a, blockType) {
|
|
8
|
+
if (blockType === "code_block") return [];
|
|
9
|
+
const best = new Map();
|
|
10
|
+
const cancelled = new Set();
|
|
11
|
+
for (const k of Object.keys(a)) if (isCancel(k) && a[k]) cancelled.add(k.split("!")[0]);
|
|
12
|
+
for (const k of Object.keys(a).sort()) {
|
|
13
|
+
const v = a[k];
|
|
14
|
+
if (v === null || v === undefined || isCancel(k) || cancelled.has(k)) continue;
|
|
15
|
+
best.set(markName(k), v);
|
|
16
|
+
}
|
|
17
|
+
return [...best].filter(([n]) => schema.marks[n]).map(([n, v]) => schema.marks[n].create(v === true ? null : v));
|
|
18
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import * as Y from "../../vendor/yjs.vendor.js";
|
|
2
|
+
import { schema } from "../schema.js";
|
|
3
|
+
import { nodeFromBlock } from "../model.js";
|
|
4
|
+
import { flatten, marksOf } from "../inline.js";
|
|
5
|
+
import { ATOM, BREAK, DEFAULTS, blocksOfValues, orderedIds, unitsOf, runsOf, canon, hash64, parseModel } from "../projection.js";
|
|
6
|
+
import { blockFromItem } from "../adopt.js";
|
|
7
|
+
import { createPageDoc, attrKeys, defaultOf, SEED } from "./layout.js";
|
|
8
|
+
import { blocksFromYValue } from "./decode.js";
|
|
9
|
+
import { PART, enc } from "./provider.js";
|
|
10
|
+
import { boardKey } from "./board.js";
|
|
11
|
+
|
|
12
|
+
export class YMigrationError extends Error {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "YMigrationError";
|
|
16
|
+
this.code = "invalid-source";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const fail = (message) => { throw new YMigrationError(message); };
|
|
21
|
+
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
22
|
+
const copy = (value) => JSON.parse(canon(value));
|
|
23
|
+
|
|
24
|
+
function sourceBlocks(values) {
|
|
25
|
+
if (!Array.isArray(values) || !values.length) fail("Expected at least one hybrid page value");
|
|
26
|
+
for (const value of values) {
|
|
27
|
+
if (!record(value) || !record(value.blocks) || Object.keys(value).some((key) => key !== "blocks" && key !== "deleted")) fail("Expected a hybrid page value with blocks and optional deleted ids");
|
|
28
|
+
if (value.deleted != null && !record(value.deleted)) fail("Invalid hybrid page tombstones");
|
|
29
|
+
for (const [id, block] of Object.entries(value.blocks)) {
|
|
30
|
+
if (!value.deleted?.[id] && (!id || id === "__proto__" || !record(block))) fail(`Invalid hybrid page block: ${id}`);
|
|
31
|
+
if (!value.deleted?.[id] && (/^~|~\d+$/).test(id)) fail(`Reserved hybrid page block id: ${id}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return blocksOfValues(values);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeBlock(id, block, index) {
|
|
38
|
+
if (!Object.hasOwn(DEFAULTS, block.t)) fail(`Unsupported hybrid block type: ${block.t}`);
|
|
39
|
+
const type = schema.nodes[block.t];
|
|
40
|
+
const fields = attrKeys(type);
|
|
41
|
+
const allowed = new Set(["t", "pos", "c", "m", ...fields, ...(block.t === "table" ? ["rows"] : [])]);
|
|
42
|
+
for (const key of Object.keys(block)) if (!allowed.has(key)) fail(`Unsupported field on block ${id}: ${key}`);
|
|
43
|
+
const out = { t: block.t, pos: String(index + 1).padStart(8, "0") };
|
|
44
|
+
for (const key of fields) {
|
|
45
|
+
const value = block[key] === undefined ? defaultOf(type, key) : block[key];
|
|
46
|
+
if (canon(value) !== canon(defaultOf(type, key))) out[key] = copy(value);
|
|
47
|
+
}
|
|
48
|
+
if (block.t === "table") {
|
|
49
|
+
if (block.rows !== undefined && !record(block.rows)) fail(`Invalid table rows: ${id}`);
|
|
50
|
+
out.rows = copy(block.rows ?? {});
|
|
51
|
+
}
|
|
52
|
+
if (!type.isTextblock) {
|
|
53
|
+
if (block.c != null && block.c !== "" && !(Array.isArray(block.c) && !block.c.length)) fail(`Unexpected text on block: ${id}`);
|
|
54
|
+
if (block.m != null && !(Array.isArray(block.m) && !block.m.length)) fail(`Unexpected marks on block: ${id}`);
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
if (block.c != null && typeof block.c !== "string" && !Array.isArray(block.c)) fail(`Invalid text on block: ${id}`);
|
|
58
|
+
if (typeof block.c === "string" && block.m != null) {
|
|
59
|
+
const units = unitsOf(block.m, block.c.length);
|
|
60
|
+
if (!units) fail(`Invalid mark runs on block: ${id}`);
|
|
61
|
+
for (let i = 0; i < units.length; i++) {
|
|
62
|
+
const key = units[i];
|
|
63
|
+
if (typeof key !== "string") fail(`Invalid mark key on block: ${id}`);
|
|
64
|
+
if (key.startsWith("@") && (key !== BREAK || block.c[i] !== ATOM)) fail(`Unsupported inline atom on block: ${id}`);
|
|
65
|
+
if (block.t === "code_block" && key) fail(`Unsupported formatting in code block: ${id}`);
|
|
66
|
+
}
|
|
67
|
+
} else if (block.m != null) fail(`Mark runs require flat text on block: ${id}`);
|
|
68
|
+
const node = nodeFromBlock(schema, id, block);
|
|
69
|
+
node.check();
|
|
70
|
+
const { c, m } = flatten(node.content);
|
|
71
|
+
out.c = c;
|
|
72
|
+
if (m.some(Boolean)) out.m = runsOf(m);
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function buildPageValue(values, clientID) {
|
|
77
|
+
let doc;
|
|
78
|
+
try {
|
|
79
|
+
const source = sourceBlocks(values);
|
|
80
|
+
const ids = orderedIds(source);
|
|
81
|
+
const blocks = Object.fromEntries(ids.map((id, index) => [id, normalizeBlock(id, source[id], index)]));
|
|
82
|
+
doc = new Y.Doc();
|
|
83
|
+
doc.clientID = clientID;
|
|
84
|
+
const pd = createPageDoc(doc);
|
|
85
|
+
let op = 0;
|
|
86
|
+
doc.transact(() => {
|
|
87
|
+
for (const id of ids) {
|
|
88
|
+
const block = blocks[id];
|
|
89
|
+
const { pos, c, m, ...fields } = block;
|
|
90
|
+
if (block.t === "board") { delete fields.columns; delete fields.cards; }
|
|
91
|
+
pd.attrs.set(id, new Y.Map(Object.entries(copy(fields))));
|
|
92
|
+
if (block.t === "board") {
|
|
93
|
+
for (const [columnId, column] of Object.entries(block.columns ?? {})) {
|
|
94
|
+
pd.boardFields.set(boardKey(id, "column", columnId, "alive"), true);
|
|
95
|
+
for (const field of ["name", "pos"]) pd.boardFields.set(boardKey(id, "column", columnId, field), column[field] ?? "");
|
|
96
|
+
}
|
|
97
|
+
for (const [cardId, card] of Object.entries(block.cards ?? {})) {
|
|
98
|
+
pd.boardFields.set(boardKey(id, "card", cardId, "alive"), true);
|
|
99
|
+
for (const field of ["title", "owner", "due"]) if (card[field] !== undefined) pd.boardFields.set(boardKey(id, "card", cardId, field), card[field]);
|
|
100
|
+
pd.boardFields.set(boardKey(id, "card", cardId, "placement"), { columnId: card.col ?? "", pos: card.pos ?? "" });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
pd.flow.insertEmbed(pd.flow.length, { b: id }, {});
|
|
104
|
+
if (!c) continue;
|
|
105
|
+
let offset = 0;
|
|
106
|
+
for (const [length, key] of runsOf(unitsOf(m, c.length) ?? Array(c.length).fill(""))) {
|
|
107
|
+
if (key === BREAK) {
|
|
108
|
+
for (let i = 0; i < length; i++) pd.flow.insertEmbed(pd.flow.length, { br: 1 }, {});
|
|
109
|
+
} else {
|
|
110
|
+
const attrs = Object.fromEntries(marksOf(schema, key).map((mark) => [`${mark.type.name}~${clientID}-m${(op++).toString(36)}`, Object.keys(mark.attrs).length ? copy(mark.attrs) : true]));
|
|
111
|
+
pd.flow.insert(pd.flow.length, c.slice(offset, offset + length), attrs);
|
|
112
|
+
}
|
|
113
|
+
offset += length;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}, SEED);
|
|
117
|
+
const text = enc(Y.encodeStateAsUpdateV2(doc));
|
|
118
|
+
const count = Math.ceil(text.length / PART);
|
|
119
|
+
const y = count <= 1 ? { seed: text } : Object.fromEntries(Array.from({ length: count }, (_, i) => [`seed.${i}.${count}`, text.slice(i * PART, (i + 1) * PART)]));
|
|
120
|
+
const value = { v: 1, g: 0, y };
|
|
121
|
+
if (canon(blocksFromYValue(value)) !== canon(blocks)) fail("Hybrid page data cannot be represented without loss");
|
|
122
|
+
return value;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error instanceof YMigrationError) throw error;
|
|
125
|
+
throw new YMigrationError(`Cannot migrate hybrid page: ${error.message}`);
|
|
126
|
+
} finally {
|
|
127
|
+
doc?.destroy();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function migrateHybridValues(values) {
|
|
132
|
+
return buildPageValue(values, 1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// A page's merge state rebuilt from its article alone. The client id comes from the content,
|
|
136
|
+
// so every rebuild of the same article is byte-identical (two tabs converge) and rebuilds of
|
|
137
|
+
// different articles never share Yjs item ids.
|
|
138
|
+
export function seedClientId(blocks) {
|
|
139
|
+
const id = parseInt(hash64(`page-seed:v1:${canon(blocks)}`).slice(0, 8), 16) >>> 0;
|
|
140
|
+
return id < 2 ? id + 2 : id;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function seedPageFromArticle(article) {
|
|
144
|
+
const items = parseModel(article, { seed: article.getAttribute("data-rev") ?? "" });
|
|
145
|
+
const blocks = {};
|
|
146
|
+
items.forEach((item, i) => {
|
|
147
|
+
const block = blockFromItem(item, null);
|
|
148
|
+
block.pos = String(i + 1).padStart(8, "0");
|
|
149
|
+
blocks[item.id] = block;
|
|
150
|
+
});
|
|
151
|
+
return buildPageValue([{ blocks }], seedClientId(blocks));
|
|
152
|
+
}
|