@panphora/clayjs 1.12.2 → 1.13.1

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
+ }
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` the inline content in
5
- // ProseMirror's JSON. Attributes equal to their schema default are left out, so
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
- out.c = node.content.toJSON() || [];
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
- return model.Node.fromJSON(schema, { type: t, attrs: { ...attrs, id, pos }, ...(c && c.length ? { content: c } : {}) });
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 needs,
169
- // or null. A missing id, or the second holder of an id (a split copies attrs
170
- // to the new half, so the retained half keeps its id), gets a fresh one.
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 seen = new Set();
173
- const fresh = items.map(({ id }) => {
174
- if (!id || seen.has(id)) return true;
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, _old, next) {
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.map(({ node }) => node.attrs), "b").forEach((change, i) => fix(top[i].node, top[i].offset, change));
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.map(({ row }) => row.attrs), "r").forEach((change, i) => fix(rows[i].row, rows[i].at, change));
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
  },