@panphora/clayjs 1.12.2 → 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.
- package/dist/clay.standalone.js +3230 -2988
- package/docs/guide.md +1847 -0
- package/entries/sap.js +6 -6
- package/package.json +3 -1
- package/packed-contract.json +1 -1
- package/src/lib/unsaved-state.js +11 -2
- package/src/live/checkpoint.js +7 -0
- package/src/live/fast-lane.js +24 -10
- package/src/live/local-lane.js +14 -7
- package/src/live/moves.js +11 -0
- package/src/live/server-lane.js +2 -1
- package/src/live/store.js +32 -1
- package/src/page/binding.js +647 -0
- package/src/page/inline.js +113 -0
- package/src/page/model.js +49 -21
- package/src/page/replay.js +578 -0
- package/src/page/schema.js +1 -1
- package/src/plugins/live.js +49 -2
- package/src/vendor/hypercms.vendor.js +20 -20
|
@@ -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
|
+
}
|
package/src/page/model.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// The page's live record and its ProseMirror document, and the way between them.
|
|
2
2
|
//
|
|
3
|
-
// The record is a flat map of blocks keyed by id: { "b-k1": { t, pos, ...attrs, c } }.
|
|
4
|
-
// `t` is the node type, `pos` a fractional order key, `c`
|
|
5
|
-
//
|
|
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
|
|
6
7
|
// a block's JSON is as small as the block. A table keeps its rows as an object
|
|
7
8
|
// keyed by row id ({ pos, cells: { <col id>: text } }), never a list, because a
|
|
8
9
|
// list in a live store is written whole and two tabs adding rows keep one.
|
|
@@ -10,7 +11,8 @@
|
|
|
10
11
|
// Order is (pos, id): pos strings compare as plain strings, and the id breaks
|
|
11
12
|
// the tie two tabs inserting at the same spot leave behind. Ids and positions
|
|
12
13
|
// are only ever assigned by a local edit (identityPlugin), never on open.
|
|
13
|
-
import { model, state } from "../vendor/prosemirror.vendor.js";
|
|
14
|
+
import { model, state, transform } from "../vendor/prosemirror.vendor.js";
|
|
15
|
+
import { flatten, unflatten } from "./inline.js";
|
|
14
16
|
|
|
15
17
|
export const REMOTE = "clay-page-remote";
|
|
16
18
|
|
|
@@ -94,7 +96,9 @@ export function blockJson(node) {
|
|
|
94
96
|
});
|
|
95
97
|
out.rows = rows;
|
|
96
98
|
} else if (node.type.spec.content) {
|
|
97
|
-
|
|
99
|
+
const { c, m } = flatten(node.content);
|
|
100
|
+
out.c = c;
|
|
101
|
+
if (m.some(Boolean)) out.m = m;
|
|
98
102
|
}
|
|
99
103
|
return out;
|
|
100
104
|
}
|
|
@@ -106,7 +110,7 @@ export function blockMap(doc) {
|
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
export function nodeFromBlock(schema, id, blockValue) {
|
|
109
|
-
const { t, pos, c, rows, ...attrs } = blockValue;
|
|
113
|
+
const { t, pos, c, m, w, rows, ...attrs } = blockValue;
|
|
110
114
|
if (t === "table") {
|
|
111
115
|
const cols = orderedIds(attrs.cols || {});
|
|
112
116
|
const { table_row: rowType, table_cell: cellType } = schema.nodes;
|
|
@@ -119,7 +123,11 @@ export function nodeFromBlock(schema, id, blockValue) {
|
|
|
119
123
|
));
|
|
120
124
|
return schema.nodes.table.createChecked({ ...attrs, id, pos }, children);
|
|
121
125
|
}
|
|
122
|
-
|
|
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);
|
|
123
131
|
}
|
|
124
132
|
|
|
125
133
|
export function docFromBlocks(schema, blocks) {
|
|
@@ -165,16 +173,16 @@ function longestIncreasing(keys) {
|
|
|
165
173
|
return keep;
|
|
166
174
|
}
|
|
167
175
|
|
|
168
|
-
// For siblings [{ id, pos }] in document order: the new { id, pos } each
|
|
169
|
-
// or null. A missing id, or
|
|
170
|
-
//
|
|
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.
|
|
171
180
|
export function repairSiblings(items, prefix) {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
if (!id ||
|
|
175
|
-
seen.add(id);
|
|
176
|
-
return false;
|
|
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);
|
|
177
184
|
});
|
|
185
|
+
const fresh = items.map(({ id }, i) => !id || keeper.get(id) !== i);
|
|
178
186
|
const keep = longestIncreasing(items.map(({ pos }, i) => (!fresh[i] && isKey(pos) ? pos : null)));
|
|
179
187
|
const out = items.map(() => null);
|
|
180
188
|
for (let i = 0; i < items.length;) {
|
|
@@ -192,20 +200,40 @@ export function repairSiblings(items, prefix) {
|
|
|
192
200
|
|
|
193
201
|
export function identityPlugin() {
|
|
194
202
|
return new state.Plugin({
|
|
195
|
-
appendTransaction(transactions,
|
|
203
|
+
appendTransaction(transactions, old, next) {
|
|
196
204
|
if (!transactions.some((tr) => tr.docChanged && !tr.getMeta(REMOTE))) return null;
|
|
197
205
|
const tr = next.tr;
|
|
198
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) }));
|
|
199
227
|
|
|
200
228
|
const top = [];
|
|
201
|
-
next.doc.forEach((node, offset) => top.push({ node, offset }));
|
|
202
|
-
repairSiblings(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));
|
|
203
231
|
|
|
204
|
-
for (const { node, offset } of top) {
|
|
232
|
+
for (const { node, at: offset } of top) {
|
|
205
233
|
if (node.type.name !== "table") continue;
|
|
206
234
|
const rows = [];
|
|
207
|
-
node.forEach((row, rowOffset) => rows.push({ row, at: offset + 1 + rowOffset }));
|
|
208
|
-
repairSiblings(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));
|
|
209
237
|
}
|
|
210
238
|
return tr.docChanged ? tr : null;
|
|
211
239
|
},
|