@panphora/clayjs 1.12.1 → 1.13.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.
@@ -0,0 +1,113 @@
1
+ // A text block's inline content as the live record keeps it: c, its flat text,
2
+ // and m, one mark key per UTF-16 unit of c. The replay (replay.js) merges in
3
+ // this form; the editor gets ProseMirror nodes back from it.
4
+ //
5
+ // A mark key lists a unit's marks in schema order, one code each, a mark with
6
+ // attrs followed by their values as a JSON array:
7
+ // "" no marks
8
+ // "b" "bi" "c" strong; strong and em; code
9
+ // 'a["https://x.test"]' a link
10
+ // 'ba["https://x.test"]' strong and a link
11
+ // An inline node that is not text (a hard break) is one U+FFFC whose key is
12
+ // "@" and the node's JSON. No mark key starts with "@", so a U+FFFC typed as
13
+ // text stays text. Private-use characters typed as text need nothing here:
14
+ // encodeForMerge (replay.js) gives each one a code of its own.
15
+ import { model } from "../vendor/prosemirror.vendor.js";
16
+
17
+ export const ATOM = "";
18
+ export const CODES = { strong: "b", em: "i", strike: "s", code: "c", link: "a" };
19
+ const NAMES = Object.fromEntries(Object.entries(CODES).map(([name, code]) => [code, name]));
20
+
21
+ export function markKey(marks) {
22
+ let key = "";
23
+ for (const mark of marks) {
24
+ const code = CODES[mark.type.name];
25
+ if (!code) throw new TypeError(`page: the mark ${mark.type.name} has no key code`);
26
+ key += code;
27
+ const names = Object.keys(mark.type.attrs);
28
+ if (names.length) key += JSON.stringify(names.map((name) => mark.attrs[name]));
29
+ }
30
+ return key;
31
+ }
32
+
33
+ // The end of the JSON array that starts at s[i].
34
+ function arrayEnd(s, i) {
35
+ let depth = 0, str = false;
36
+ for (; i < s.length; i++) {
37
+ const ch = s[i];
38
+ if (str) {
39
+ if (ch === "\\") i++;
40
+ else if (ch === '"') str = false;
41
+ } else if (ch === '"') str = true;
42
+ else if (ch === "[") depth++;
43
+ else if (ch === "]" && --depth === 0) return i + 1;
44
+ }
45
+ throw new SyntaxError(`page: unterminated attrs in mark key ${s}`);
46
+ }
47
+
48
+ const parsed = new WeakMap();
49
+
50
+ export function marksOf(schema, key) {
51
+ if (!key) return model.Mark.none;
52
+ let cache = parsed.get(schema);
53
+ if (!cache) parsed.set(schema, (cache = new Map()));
54
+ let marks = cache.get(key);
55
+ if (marks) return marks;
56
+ marks = [];
57
+ for (let i = 0; i < key.length;) {
58
+ const type = schema.marks[NAMES[key[i]]];
59
+ if (!type) throw new TypeError(`page: unknown mark code ${key[i]} in ${key}`);
60
+ i++;
61
+ let attrs = null;
62
+ if (key[i] === "[") {
63
+ const end = arrayEnd(key, i);
64
+ const values = JSON.parse(key.slice(i, end));
65
+ attrs = Object.fromEntries(Object.keys(type.attrs).map((name, j) => [name, values[j]]));
66
+ i = end;
67
+ }
68
+ marks = type.create(attrs).addToSet(marks);
69
+ }
70
+ cache.set(key, marks);
71
+ return marks;
72
+ }
73
+
74
+ export function flatten(content) {
75
+ let c = "";
76
+ const m = [];
77
+ content.forEach((node) => {
78
+ if (node.isText) {
79
+ const key = markKey(node.marks);
80
+ c += node.text;
81
+ for (let i = 0; i < node.text.length; i++) m.push(key);
82
+ } else {
83
+ c += ATOM;
84
+ m.push("@" + JSON.stringify(node.toJSON()));
85
+ }
86
+ });
87
+ return { c, m };
88
+ }
89
+
90
+ const isHigh = (u) => u >= 0xd800 && u <= 0xdbff;
91
+ const textKey = (key) => (key && key[0] !== "@" ? key : "");
92
+
93
+ // The inline nodes for c and m. Marks the parent node type does not allow are
94
+ // dropped; the two halves of a surrogate pair always share one text node.
95
+ export function unflatten(schema, c, m, parent = null) {
96
+ const nodes = [];
97
+ const allowed = (marks) => (parent ? marks.filter((mark) => parent.allowsMarkType(mark.type)) : marks);
98
+ for (let i = 0; i < c.length;) {
99
+ const key = (m && m[i]) || "";
100
+ if (c[i] === ATOM && key[0] === "@") {
101
+ nodes.push(model.Node.fromJSON(schema, JSON.parse(key.slice(1))));
102
+ i++;
103
+ continue;
104
+ }
105
+ const run = textKey(key);
106
+ let j = i + 1;
107
+ while (j < c.length && !(c[j] === ATOM && ((m && m[j]) || "")[0] === "@") && textKey((m && m[j]) || "") === run) j++;
108
+ if (j < c.length && isHigh(c.charCodeAt(j - 1))) j++;
109
+ nodes.push(schema.text(c.slice(i, j), allowed(marksOf(schema, run))));
110
+ i = j;
111
+ }
112
+ return nodes;
113
+ }
@@ -0,0 +1,241 @@
1
+ // The page's live record and its ProseMirror document, and the way between them.
2
+ //
3
+ // The record is a flat map of blocks keyed by id: { "b-k1": { t, pos, ...attrs, c, m } }.
4
+ // `t` is the node type, `pos` a fractional order key, `c` a text block's flat
5
+ // text and `m` its mark keys, one per UTF-16 unit, left out when there are none
6
+ // (inline.js). Attributes equal to their schema default are left out, so
7
+ // a block's JSON is as small as the block. A table keeps its rows as an object
8
+ // keyed by row id ({ pos, cells: { <col id>: text } }), never a list, because a
9
+ // list in a live store is written whole and two tabs adding rows keep one.
10
+ //
11
+ // Order is (pos, id): pos strings compare as plain strings, and the id breaks
12
+ // the tie two tabs inserting at the same spot leave behind. Ids and positions
13
+ // are only ever assigned by a local edit (identityPlugin), never on open.
14
+ import { model, state, transform } from "../vendor/prosemirror.vendor.js";
15
+ import { flatten, unflatten } from "./inline.js";
16
+
17
+ export const REMOTE = "clay-page-remote";
18
+
19
+ // ---- fractional order keys -------------------------------------------------
20
+
21
+ const DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
22
+ const VALID_KEY = /^[0-9A-Za-z]*[1-9A-Za-z]$/;
23
+
24
+ export const isKey = (key) => typeof key === "string" && VALID_KEY.test(key);
25
+
26
+ // A key strictly between a and b, read as base-62 fractions 0.a and 0.b; a ""
27
+ // is zero and b null is one. No key ends in "0", so there is always room
28
+ // between two of them.
29
+ function midpoint(a, b) {
30
+ if (b !== null) {
31
+ let n = 0;
32
+ while ((a[n] || "0") === b[n]) n++;
33
+ if (n > 0) return b.slice(0, n) + midpoint(a.slice(n), b.slice(n));
34
+ }
35
+ const da = a ? DIGITS.indexOf(a[0]) : 0;
36
+ const db = b !== null ? DIGITS.indexOf(b[0]) : DIGITS.length;
37
+ if (db - da > 1) return DIGITS[Math.round((da + db) / 2)];
38
+ if (b !== null && b.length > 1) return b.slice(0, 1);
39
+ return DIGITS[da] + midpoint(a.slice(1), null);
40
+ }
41
+
42
+ export function keyBetween(a = null, b = null) {
43
+ if (a !== null && !isKey(a)) throw new TypeError(`page: invalid order key ${a}`);
44
+ if (b !== null && !isKey(b)) throw new TypeError(`page: invalid order key ${b}`);
45
+ if (a !== null && b !== null && a >= b) throw new RangeError(`page: order key ${a} is not before ${b}`);
46
+ return midpoint(a ?? "", b);
47
+ }
48
+
49
+ // n keys between a and b, spread by halving so a thousand-row import keeps
50
+ // short keys.
51
+ export function keysBetween(a, b, n) {
52
+ if (n <= 0) return [];
53
+ if (n === 1) return [keyBetween(a, b)];
54
+ const mid = keyBetween(a, b);
55
+ const half = Math.floor(n / 2);
56
+ return [...keysBetween(a, mid, half), mid, ...keysBetween(mid, b, n - half - 1)];
57
+ }
58
+
59
+ export function orderedIds(map) {
60
+ return Object.keys(map).sort((x, y) => {
61
+ const px = map[x].pos ?? "", py = map[y].pos ?? "";
62
+ if (px !== py) return px < py ? -1 : 1;
63
+ return x < y ? -1 : x > y ? 1 : 0;
64
+ });
65
+ }
66
+
67
+ export function newId(prefix = "b") {
68
+ const bytes = new Uint8Array(4);
69
+ globalThis.crypto.getRandomValues(bytes);
70
+ return `${prefix}-${Date.now().toString(36)}${Array.from(bytes, (b) => (b % 36).toString(36)).join("")}`;
71
+ }
72
+
73
+ // ---- block JSON <-> node ---------------------------------------------------
74
+
75
+ const KEYED = new Set(["id", "pos"]);
76
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
77
+ const copy = (v) => (v === null || typeof v !== "object" ? v : JSON.parse(JSON.stringify(v)));
78
+
79
+ function attrsJson(node) {
80
+ const out = {};
81
+ for (const [name, spec] of Object.entries(node.type.spec.attrs || {})) {
82
+ if (KEYED.has(name)) continue;
83
+ if (!same(node.attrs[name], spec.default)) out[name] = copy(node.attrs[name]);
84
+ }
85
+ return out;
86
+ }
87
+
88
+ export function blockJson(node) {
89
+ const out = { t: node.type.name, pos: node.attrs.pos, ...attrsJson(node) };
90
+ if (node.type.name === "table") {
91
+ const rows = {};
92
+ node.forEach((row) => {
93
+ const cells = {};
94
+ row.forEach((cell) => { cells[cell.attrs.col] = cell.textContent; });
95
+ rows[row.attrs.id] = { pos: row.attrs.pos, cells };
96
+ });
97
+ out.rows = rows;
98
+ } else if (node.type.spec.content) {
99
+ const { c, m } = flatten(node.content);
100
+ out.c = c;
101
+ if (m.some(Boolean)) out.m = m;
102
+ }
103
+ return out;
104
+ }
105
+
106
+ export function blockMap(doc) {
107
+ const map = {};
108
+ doc.forEach((node) => { map[node.attrs.id] = blockJson(node); });
109
+ return map;
110
+ }
111
+
112
+ export function nodeFromBlock(schema, id, blockValue) {
113
+ const { t, pos, c, m, w, rows, ...attrs } = blockValue;
114
+ if (t === "table") {
115
+ const cols = orderedIds(attrs.cols || {});
116
+ const { table_row: rowType, table_cell: cellType } = schema.nodes;
117
+ const children = orderedIds(rows || {}).map((rid) => rowType.createChecked(
118
+ { id: rid, pos: rows[rid].pos },
119
+ cols.map((col) => {
120
+ const text = rows[rid].cells?.[col];
121
+ return cellType.createChecked({ col }, text ? schema.text(String(text)) : null);
122
+ }),
123
+ ));
124
+ return schema.nodes.table.createChecked({ ...attrs, id, pos }, children);
125
+ }
126
+ const type = schema.nodes[t];
127
+ if (!type) throw new RangeError(`page: unknown block type ${t}`);
128
+ // A record from before the flat form holds ProseMirror's inline JSON.
129
+ const content = typeof c === "string" ? unflatten(schema, c, m, type) : Array.isArray(c) && c.length ? model.Fragment.fromJSON(schema, c) : null;
130
+ return type.create({ ...attrs, id, pos }, content);
131
+ }
132
+
133
+ export function docFromBlocks(schema, blocks) {
134
+ const nodes = orderedIds(blocks).map((id) => nodeFromBlock(schema, id, blocks[id]));
135
+ return nodes.length ? schema.nodes.doc.create(null, nodes) : schema.nodes.doc.createAndFill();
136
+ }
137
+
138
+ // The blocks a transaction touched, before and after, for diffOps. ProseMirror
139
+ // shares every unchanged node between the two documents, so identity is enough.
140
+ export function changedBlocks(before, after) {
141
+ const index = (doc) => {
142
+ const map = new Map();
143
+ doc.forEach((node) => map.set(node.attrs.id, node));
144
+ return map;
145
+ };
146
+ const b = index(before), a = index(after);
147
+ const out = { before: {}, after: {} };
148
+ for (const [id, node] of b) if (a.get(id) !== node) out.before[id] = blockJson(node);
149
+ for (const [id, node] of a) if (b.get(id) !== node) out.after[id] = blockJson(node);
150
+ return out;
151
+ }
152
+
153
+ // ---- identity: ids and order keys after a local edit -----------------------
154
+
155
+ // Indices of the longest run of keys, in document order, that is already
156
+ // strictly increasing. Those keep their pos; only the rest get new ones, so a
157
+ // moved block rewrites one key, not every key after it.
158
+ function longestIncreasing(keys) {
159
+ const tails = [], tailAt = [], prev = new Array(keys.length).fill(-1);
160
+ keys.forEach((key, i) => {
161
+ if (key === null) return;
162
+ let lo = 0, hi = tails.length;
163
+ while (lo < hi) {
164
+ const mid = (lo + hi) >> 1;
165
+ if (tails[mid] < key) lo = mid + 1; else hi = mid;
166
+ }
167
+ if (lo > 0) prev[i] = tailAt[lo - 1];
168
+ tails[lo] = key;
169
+ tailAt[lo] = i;
170
+ });
171
+ const keep = new Set();
172
+ for (let i = tailAt[tails.length - 1] ?? -1; i >= 0; i = prev[i]) keep.add(i);
173
+ return keep;
174
+ }
175
+
176
+ // For siblings [{ id, pos, rank }] in document order: the new { id, pos } each
177
+ // needs, or null. A missing id, or a second holder of an id, gets a fresh one.
178
+ // Of several holders, the highest rank keeps the id, the first on a tie: a
179
+ // split copies attrs to the new half, and a pasted copy lands beside the original.
180
+ export function repairSiblings(items, prefix) {
181
+ const keeper = new Map();
182
+ items.forEach(({ id, rank = 0 }, i) => {
183
+ if (id && (!keeper.has(id) || rank > (items[keeper.get(id)].rank ?? 0))) keeper.set(id, i);
184
+ });
185
+ const fresh = items.map(({ id }, i) => !id || keeper.get(id) !== i);
186
+ const keep = longestIncreasing(items.map(({ pos }, i) => (!fresh[i] && isKey(pos) ? pos : null)));
187
+ const out = items.map(() => null);
188
+ for (let i = 0; i < items.length;) {
189
+ if (keep.has(i)) { i++; continue; }
190
+ let j = i;
191
+ while (j < items.length && !keep.has(j)) j++;
192
+ const lower = i > 0 ? items[i - 1].pos : null;
193
+ const upper = j < items.length ? items[j].pos : null;
194
+ const keys = keysBetween(lower, upper, j - i);
195
+ for (let k = i; k < j; k++) out[k] = { id: fresh[k] ? newId(prefix) : items[k].id, pos: keys[k - i] };
196
+ i = j;
197
+ }
198
+ return out;
199
+ }
200
+
201
+ export function identityPlugin() {
202
+ return new state.Plugin({
203
+ appendTransaction(transactions, old, next) {
204
+ if (!transactions.some((tr) => tr.docChanged && !tr.getMeta(REMOTE))) return null;
205
+ const tr = next.tr;
206
+ const fix = (node, at, change) => { if (change) tr.setNodeMarkup(at, null, { ...node.attrs, ...change }); };
207
+ // Of two holders of an id, the one with the old block's content keeps it
208
+ // (Enter at the start of a block moves its text down), then the one the
209
+ // old block maps to (a pasted copy lands beside the original).
210
+ const mapping = new transform.Mapping();
211
+ for (const t of transactions) mapping.appendMapping(t.mapping);
212
+ const was = new Map();
213
+ const track = (node, at) => {
214
+ if (!node.attrs.id) return;
215
+ const r = mapping.mapResult(at, 1);
216
+ was.set(node.attrs.id, { node, at: r.deleted ? null : r.pos });
217
+ };
218
+ old.doc.forEach((node, offset) => {
219
+ track(node, offset);
220
+ if (node.type.name === "table") node.forEach((row, rowOffset) => track(row, offset + 1 + rowOffset));
221
+ });
222
+ const rank = (node, at) => {
223
+ const w = was.get(node.attrs.id);
224
+ return w ? (w.node.content.eq(node.content) ? 2 : 0) + (w.at === at ? 1 : 0) : 0;
225
+ };
226
+ const items = (list) => list.map(({ node, at }) => ({ ...node.attrs, rank: rank(node, at) }));
227
+
228
+ const top = [];
229
+ next.doc.forEach((node, offset) => top.push({ node, at: offset }));
230
+ repairSiblings(items(top), "b").forEach((change, i) => fix(top[i].node, top[i].at, change));
231
+
232
+ for (const { node, at: offset } of top) {
233
+ if (node.type.name !== "table") continue;
234
+ const rows = [];
235
+ node.forEach((row, rowOffset) => rows.push({ node: row, at: offset + 1 + rowOffset }));
236
+ repairSiblings(items(rows), "r").forEach((change, i) => fix(rows[i].node, rows[i].at, change));
237
+ }
238
+ return tr.docChanged ? tr : null;
239
+ },
240
+ });
241
+ }