@kerebron/extension-yjs 0.5.3 → 0.5.4

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 (67) hide show
  1. package/README.md +3 -89
  2. package/esm/ExtensionYjs.d.ts +10 -1
  3. package/esm/ExtensionYjs.d.ts.map +1 -1
  4. package/esm/ExtensionYjs.js +47 -6
  5. package/esm/ExtensionYjs.js.map +1 -1
  6. package/esm/MarkYChange.d.ts +7 -0
  7. package/esm/MarkYChange.d.ts.map +1 -0
  8. package/esm/MarkYChange.js +21 -0
  9. package/esm/MarkYChange.js.map +1 -0
  10. package/esm/ProsemirrorBinding.d.ts +60 -0
  11. package/esm/ProsemirrorBinding.d.ts.map +1 -0
  12. package/esm/ProsemirrorBinding.js +405 -0
  13. package/esm/ProsemirrorBinding.js.map +1 -0
  14. package/esm/createNodeFromYElement.d.ts +10 -0
  15. package/esm/createNodeFromYElement.d.ts.map +1 -0
  16. package/esm/createNodeFromYElement.js +123 -0
  17. package/esm/createNodeFromYElement.js.map +1 -0
  18. package/esm/debug.d.ts +13 -0
  19. package/esm/debug.d.ts.map +1 -0
  20. package/esm/debug.js +147 -0
  21. package/esm/debug.js.map +1 -0
  22. package/esm/keys.d.ts +5 -8
  23. package/esm/keys.d.ts.map +1 -1
  24. package/esm/keys.js +1 -6
  25. package/esm/keys.js.map +1 -1
  26. package/esm/lib.d.ts +1 -2
  27. package/esm/lib.d.ts.map +1 -1
  28. package/esm/lib.js +12 -2
  29. package/esm/lib.js.map +1 -1
  30. package/esm/updateYFragment.d.ts +17 -0
  31. package/esm/updateYFragment.d.ts.map +1 -0
  32. package/esm/updateYFragment.js +333 -0
  33. package/esm/updateYFragment.js.map +1 -0
  34. package/esm/utils.d.ts +2 -0
  35. package/esm/utils.d.ts.map +1 -1
  36. package/esm/utils.js +4 -0
  37. package/esm/utils.js.map +1 -1
  38. package/esm/yPositionPlugin.d.ts +12 -4
  39. package/esm/yPositionPlugin.d.ts.map +1 -1
  40. package/esm/yPositionPlugin.js +114 -61
  41. package/esm/yPositionPlugin.js.map +1 -1
  42. package/esm/ySyncPlugin.d.ts +16 -78
  43. package/esm/ySyncPlugin.d.ts.map +1 -1
  44. package/esm/ySyncPlugin.js +81 -848
  45. package/esm/ySyncPlugin.js.map +1 -1
  46. package/esm/yUndoPlugin.d.ts +1 -1
  47. package/esm/yUndoPlugin.d.ts.map +1 -1
  48. package/esm/yUndoPlugin.js +1 -1
  49. package/esm/yUndoPlugin.js.map +1 -1
  50. package/package.json +9 -3
  51. package/src/ExtensionYjs.ts +65 -9
  52. package/src/MarkYChange.ts +23 -0
  53. package/src/ProsemirrorBinding.ts +607 -0
  54. package/src/createNodeFromYElement.ts +175 -0
  55. package/src/debug.ts +218 -0
  56. package/src/keys.ts +9 -9
  57. package/src/lib.ts +11 -3
  58. package/src/updateYFragment.ts +439 -0
  59. package/src/utils.ts +6 -0
  60. package/src/yPositionPlugin.ts +167 -92
  61. package/src/ySyncPlugin.ts +135 -1193
  62. package/src/yUndoPlugin.ts +1 -1
  63. package/esm/convertUtils.d.ts +0 -59
  64. package/esm/convertUtils.d.ts.map +0 -1
  65. package/esm/convertUtils.js +0 -89
  66. package/esm/convertUtils.js.map +0 -1
  67. package/src/convertUtils.ts +0 -143
@@ -0,0 +1,439 @@
1
+ import * as Y from 'yjs';
2
+ import * as PModel from 'prosemirror-model';
3
+ import { Mark, Node, Schema } from 'prosemirror-model';
4
+
5
+ import { simpleDiff } from 'lib0/diff';
6
+
7
+ import { ySyncPluginKey } from './keys.js';
8
+ import * as utils from './utils.js';
9
+ import type { BindingMetadata } from './ProsemirrorBinding.js';
10
+ import { TransactFunc } from './ySyncPlugin.js';
11
+
12
+ const hashedMarkNameRegex = /(.*)(--[a-zA-Z0-9+/=]{8})$/;
13
+ export const yattr2markname = (attrName: string) =>
14
+ hashedMarkNameRegex.exec(attrName)?.[1] ?? attrName;
15
+
16
+ const marksToAttributes = (
17
+ marks: readonly Mark[],
18
+ meta: BindingMetadata,
19
+ ): Record<string, PModel.Attrs> => {
20
+ const pattrs: Record<string, PModel.Attrs> = {};
21
+ marks.forEach((mark) => {
22
+ if (mark.type.name !== 'ychange') {
23
+ let isOverlapping = true;
24
+ if (!meta.isOMark.has(mark.type.name)) {
25
+ meta.isOMark.set(mark.type.name, !mark.type.excludes(mark.type));
26
+ isOverlapping = false;
27
+ }
28
+
29
+ pattrs[
30
+ isOverlapping
31
+ ? `${mark.type.name}--${utils.hashOfJSON(mark.toJSON())}`
32
+ : mark.type.name
33
+ ] = mark.attrs;
34
+ }
35
+ });
36
+ return pattrs;
37
+ };
38
+
39
+ const equalYTextPText = (ytext: Y.XmlText, ptexts: Array<any>): boolean => {
40
+ const delta = ytext.toDelta();
41
+ return delta.length === ptexts.length &&
42
+ delta.every((d: any, i: number): boolean =>
43
+ d.insert === /** @type {any} */ (ptexts[i]).text &&
44
+ Object.keys(d.attributes || {}).length === ptexts[i].marks.length &&
45
+ Object.entries(d.attributes || {}).every(([yattrname, attr]) => {
46
+ const markname = yattr2markname(yattrname);
47
+ const pmarks = ptexts[i].marks;
48
+ return equalAttrs(
49
+ attr,
50
+ pmarks.find((mark: Mark) => mark.type.name === markname)?.attrs,
51
+ );
52
+ })
53
+ );
54
+ };
55
+
56
+ const ytextTrans = (ytext: Y.Text) => {
57
+ let str = '';
58
+ let n: Y.Item | null = ytext._start;
59
+ const nAttrs: Record<string, null> = {};
60
+ while (n !== null) {
61
+ if (!n.deleted) {
62
+ if (n.countable && n.content instanceof Y.ContentString) {
63
+ str += n.content.str;
64
+ } else if (n.content instanceof Y.ContentFormat) {
65
+ nAttrs[n.content.key] = null;
66
+ }
67
+ }
68
+ n = n.right;
69
+ }
70
+ return {
71
+ str,
72
+ nAttrs,
73
+ };
74
+ };
75
+
76
+ type PMTextNodes = Array<PModel.Node>;
77
+
78
+ const normalizePNodeContent = (
79
+ pnode: any,
80
+ ): Array<PMTextNodes | PModel.Node> => {
81
+ const c = pnode.content.content;
82
+ const res = [];
83
+ for (let i = 0; i < c.length; i++) {
84
+ const n: PModel.Node = c[i];
85
+ if (n.isText) {
86
+ const textNodes: PMTextNodes = [];
87
+ for (let tnode = c[i]; i < c.length && tnode.isText; tnode = c[++i]) {
88
+ textNodes.push(tnode);
89
+ }
90
+ i--;
91
+ res.push(textNodes);
92
+ } else {
93
+ res.push(n);
94
+ }
95
+ }
96
+ return res;
97
+ };
98
+
99
+ /**
100
+ * @private
101
+ */
102
+ const createTypeFromTextNodes = (
103
+ nodes: PMTextNodes,
104
+ meta: BindingMetadata,
105
+ ): Y.XmlText => {
106
+ const type = new Y.XmlText();
107
+ const delta = nodes.map((node) => ({
108
+ insert: node.text,
109
+ attributes: marksToAttributes(node.marks, meta),
110
+ }));
111
+ type.applyDelta(delta);
112
+ meta.mapping.set(type, nodes);
113
+ return type;
114
+ };
115
+
116
+ type NodeResult<T> = T extends PMTextNodes ? Y.XmlText : Y.XmlElement;
117
+
118
+ function createTypeFromTextOrElementNode<T extends PModel.Node | PMTextNodes>(
119
+ node: T,
120
+ meta: BindingMetadata,
121
+ ): NodeResult<T> {
122
+ return (Array.isArray(node)
123
+ ? createTypeFromTextNodes(node, meta)
124
+ : createTypeFromElementNode(node, meta)) as NodeResult<T>;
125
+ }
126
+
127
+ const isObject = (val: any) => typeof val === 'object' && val !== null;
128
+
129
+ const equalAttrs = (pattrs: any, yattrs: any) => {
130
+ const keys = Object.keys(pattrs).filter((key) => pattrs[key] !== null);
131
+ let eq = keys.length ===
132
+ (yattrs == null
133
+ ? 0
134
+ : Object.keys(yattrs).filter((key) => yattrs[key] !== null).length);
135
+ for (let i = 0; i < keys.length && eq; i++) {
136
+ const key = keys[i];
137
+ const l = pattrs[key];
138
+ const r = yattrs[key];
139
+ eq = key === 'ychange' || l === r ||
140
+ (isObject(l) && isObject(r) && equalAttrs(l, r));
141
+ }
142
+ return eq;
143
+ };
144
+
145
+ /**
146
+ * @private
147
+ */
148
+ const createTypeFromElementNode = (
149
+ node: PModel.Node,
150
+ meta: BindingMetadata,
151
+ ): Y.XmlElement => {
152
+ const type = new Y.XmlElement(node.type.name);
153
+ for (const key in node.attrs) {
154
+ const val = node.attrs[key];
155
+ if ('undefined' !== typeof val && val !== null && key !== 'ychange') {
156
+ type.setAttribute(key, val);
157
+ }
158
+ }
159
+ const pChildren: Array<PMTextNodes | PModel.Node> = normalizePNodeContent(
160
+ node,
161
+ );
162
+ const yElems: Array<Y.XmlElement | Y.XmlText> = pChildren.map((n) =>
163
+ createTypeFromTextOrElementNode(n, meta)
164
+ );
165
+ type.insert(0, yElems);
166
+ meta.mapping.set(type, node);
167
+ return type;
168
+ };
169
+
170
+ const mappedIdentity = (
171
+ mapped: PModel.Node | Array<PModel.Node> | undefined,
172
+ pcontent: PModel.Node | Array<PModel.Node>,
173
+ ) =>
174
+ mapped === pcontent ||
175
+ (mapped instanceof Array && pcontent instanceof Array &&
176
+ mapped.length === pcontent.length &&
177
+ mapped.every((a, i) => pcontent[i] === a));
178
+
179
+ const computeChildEqualityFactor = (
180
+ ytype: Y.XmlElement,
181
+ pnode: PModel.Node,
182
+ meta: BindingMetadata,
183
+ ): { foundMappedChild: boolean; equalityFactor: number } => {
184
+ const yChildren = ytype.toArray();
185
+ const pChildren: Array<PMTextNodes | PModel.Node> = normalizePNodeContent(
186
+ pnode,
187
+ );
188
+ const pChildCnt = pChildren.length;
189
+ const yChildCnt = yChildren.length;
190
+ const minCnt = Math.min(yChildCnt, pChildCnt);
191
+ let left = 0;
192
+ let right = 0;
193
+ let foundMappedChild = false;
194
+ for (; left < minCnt; left++) {
195
+ const leftY = yChildren[left];
196
+ const leftP = pChildren[left];
197
+ if (mappedIdentity(meta.mapping.get(leftY), leftP)) {
198
+ foundMappedChild = true; // definite (good) match!
199
+ } else if (!equalYTypePNode(leftY, leftP)) {
200
+ break;
201
+ }
202
+ }
203
+ for (; left + right < minCnt; right++) {
204
+ const rightY = yChildren[yChildCnt - right - 1];
205
+ const rightP = pChildren[pChildCnt - right - 1];
206
+ if (mappedIdentity(meta.mapping.get(rightY), rightP)) {
207
+ foundMappedChild = true;
208
+ } else if (!equalYTypePNode(rightY, rightP)) {
209
+ break;
210
+ }
211
+ }
212
+ return {
213
+ equalityFactor: left + right,
214
+ foundMappedChild,
215
+ };
216
+ };
217
+
218
+ const matchNodeName = (
219
+ yElement: Y.XmlElement,
220
+ pNode: PModel.Node | PMTextNodes,
221
+ ) => !(pNode instanceof Array) && yElement.nodeName === pNode.type.name;
222
+
223
+ const equalYTypePNode = (
224
+ ytype: Y.XmlElement | Y.XmlText | Y.XmlHook,
225
+ pnode: any | Array<any>,
226
+ ): boolean => {
227
+ if (
228
+ ytype instanceof Y.XmlElement && !(pnode instanceof Array) &&
229
+ matchNodeName(ytype, pnode)
230
+ ) {
231
+ const normalizedContent: Array<PMTextNodes | PModel.Node> =
232
+ normalizePNodeContent(pnode);
233
+ return ytype._length === normalizedContent.length &&
234
+ equalAttrs(ytype.getAttributes(), pnode.attrs) &&
235
+ ytype.toArray().every((ychild, i) =>
236
+ equalYTypePNode(ychild, normalizedContent[i])
237
+ );
238
+ }
239
+ return ytype instanceof Y.XmlText && pnode instanceof Array &&
240
+ equalYTextPText(ytype, pnode);
241
+ };
242
+
243
+ /**
244
+ * @todo test this more
245
+ */
246
+ const updateYText = (
247
+ ytext: Y.Text,
248
+ ptexts: PMTextNodes,
249
+ meta: BindingMetadata,
250
+ ) => {
251
+ meta.mapping.set(ytext, ptexts);
252
+ const { nAttrs, str } = ytextTrans(ytext);
253
+ const content = ptexts.map((p) => ({
254
+ insert: p.text || '',
255
+ attributes: Object.assign({}, nAttrs, marksToAttributes(p.marks, meta)),
256
+ }));
257
+ const { insert, remove, index } = simpleDiff(
258
+ str,
259
+ content.map((c) => c.insert).join(''),
260
+ );
261
+ ytext.delete(index, remove);
262
+ ytext.insert(index, insert);
263
+ ytext.applyDelta(
264
+ content.map((c) => ({ retain: c.insert.length, attributes: c.attributes })),
265
+ );
266
+ };
267
+
268
+ /**
269
+ * Update a yDom node by syncing the current content of the prosemirror node.
270
+ *
271
+ * This is a y-prosemirror internal feature that you can use at your own risk.
272
+ *
273
+ * @private
274
+ * @unstable
275
+ */
276
+ export const updateYFragment = (
277
+ ydoc: { transact: TransactFunc<void> },
278
+ yDomFragment: Y.XmlFragment,
279
+ pNode: Node,
280
+ meta: BindingMetadata,
281
+ ) => {
282
+ if (
283
+ yDomFragment instanceof Y.XmlElement &&
284
+ yDomFragment.nodeName !== pNode.type.name
285
+ ) {
286
+ throw new Error('node name mismatch!');
287
+ }
288
+ meta.mapping.set(yDomFragment, pNode);
289
+ // update attributes
290
+ if (yDomFragment instanceof Y.XmlElement) {
291
+ const yDomAttrs = yDomFragment.getAttributes();
292
+ const pAttrs = pNode.attrs;
293
+ for (const key in pAttrs) {
294
+ if (pAttrs[key] !== null) {
295
+ if (yDomAttrs[key] !== pAttrs[key] && key !== 'ychange') {
296
+ yDomFragment.setAttribute(key, pAttrs[key]);
297
+ }
298
+ } else {
299
+ yDomFragment.removeAttribute(key);
300
+ }
301
+ }
302
+ // remove all keys that are no longer in pAttrs
303
+ for (const key in yDomAttrs) {
304
+ if (pAttrs[key] === undefined) {
305
+ yDomFragment.removeAttribute(key);
306
+ }
307
+ }
308
+ }
309
+ // update children
310
+ const pChildren: Array<PMTextNodes | PModel.Node> = normalizePNodeContent(
311
+ pNode,
312
+ );
313
+ const pChildCnt = pChildren.length;
314
+ const yChildren = yDomFragment.toArray() as Array<Y.XmlElement | Y.XmlText>;
315
+ const yChildCnt = yChildren.length;
316
+ const minCnt = Math.min(pChildCnt, yChildCnt);
317
+ let left = 0;
318
+ let right = 0;
319
+ // find number of matching elements from left
320
+ for (; left < minCnt; left++) {
321
+ const leftY = yChildren[left];
322
+ const leftP = pChildren[left];
323
+ if (!mappedIdentity(meta.mapping.get(leftY), leftP)) {
324
+ if (equalYTypePNode(leftY, leftP)) {
325
+ // update mapping
326
+ meta.mapping.set(leftY, leftP);
327
+ } else {
328
+ break;
329
+ }
330
+ }
331
+ }
332
+ // find number of matching elements from right
333
+ for (; right + left < minCnt; right++) {
334
+ const rightY = yChildren[yChildCnt - right - 1];
335
+ const rightP = pChildren[pChildCnt - right - 1];
336
+ if (!mappedIdentity(meta.mapping.get(rightY), rightP)) {
337
+ if (equalYTypePNode(rightY, rightP)) {
338
+ // update mapping
339
+ meta.mapping.set(rightY, rightP);
340
+ } else {
341
+ break;
342
+ }
343
+ }
344
+ }
345
+ ydoc.transact(() => {
346
+ // try to compare and update
347
+ while (yChildCnt - left - right > 0 && pChildCnt - left - right > 0) {
348
+ const leftY: Y.XmlElement | Y.XmlText = yChildren[left];
349
+ const leftP: PModel.Node | PMTextNodes = pChildren[left];
350
+ const rightY: Y.XmlElement | Y.XmlText = yChildren[yChildCnt - right - 1];
351
+ const rightP: PModel.Node | PMTextNodes =
352
+ pChildren[pChildCnt - right - 1];
353
+ if (leftY instanceof Y.XmlText && leftP instanceof Array) {
354
+ if (!equalYTextPText(leftY, leftP)) {
355
+ updateYText(leftY, leftP, meta);
356
+ }
357
+ left += 1;
358
+ } else {
359
+ let updateLeft = leftY instanceof Y.XmlElement &&
360
+ matchNodeName(leftY, leftP);
361
+ let updateRight = rightY instanceof Y.XmlElement &&
362
+ matchNodeName(rightY, rightP);
363
+ if (updateLeft && updateRight) {
364
+ // decide which element to update
365
+ const equalityLeft = computeChildEqualityFactor(
366
+ leftY as Y.XmlElement,
367
+ leftP as PModel.Node,
368
+ meta,
369
+ );
370
+ const equalityRight = computeChildEqualityFactor(
371
+ rightY as Y.XmlElement,
372
+ rightP as PModel.Node,
373
+ meta,
374
+ );
375
+ if (
376
+ equalityLeft.foundMappedChild && !equalityRight.foundMappedChild
377
+ ) {
378
+ updateRight = false;
379
+ } else if (
380
+ !equalityLeft.foundMappedChild && equalityRight.foundMappedChild
381
+ ) {
382
+ updateLeft = false;
383
+ } else if (
384
+ equalityLeft.equalityFactor < equalityRight.equalityFactor
385
+ ) {
386
+ updateLeft = false;
387
+ } else {
388
+ updateRight = false;
389
+ }
390
+ }
391
+ if (updateLeft) {
392
+ updateYFragment(
393
+ ydoc,
394
+ leftY as Y.XmlElement,
395
+ leftP as PModel.Node,
396
+ meta,
397
+ );
398
+ left += 1;
399
+ } else if (updateRight) {
400
+ updateYFragment(
401
+ ydoc,
402
+ rightY as Y.XmlElement,
403
+ rightP as PModel.Node,
404
+ meta,
405
+ );
406
+ right += 1;
407
+ } else {
408
+ meta.mapping.delete(yDomFragment.get(left));
409
+ yDomFragment.delete(left, 1);
410
+ yDomFragment.insert(left, [
411
+ createTypeFromTextOrElementNode(leftP, meta),
412
+ ]);
413
+ left += 1;
414
+ }
415
+ }
416
+ }
417
+ const yDelLen = yChildCnt - left - right;
418
+ if (
419
+ yChildCnt === 1 && pChildCnt === 0 && yChildren[0] instanceof Y.XmlText
420
+ ) {
421
+ meta.mapping.delete(yChildren[0]);
422
+ // Edge case handling https://github.com/yjs/y-prosemirror/issues/108
423
+ // Only delete the content of the Y.Text to retain remote changes on the same Y.Text object
424
+ yChildren[0].delete(0, yChildren[0].length);
425
+ } else if (yDelLen > 0) {
426
+ yDomFragment.slice(left, left + yDelLen).forEach((type) =>
427
+ meta.mapping.delete(type)
428
+ );
429
+ yDomFragment.delete(left, yDelLen);
430
+ }
431
+ if (left + right < pChildCnt) {
432
+ const ins = [];
433
+ for (let i = left; i < pChildCnt - right; i++) {
434
+ ins.push(createTypeFromTextOrElementNode(pChildren[i], meta));
435
+ }
436
+ yDomFragment.insert(left, ins);
437
+ }
438
+ }, ySyncPluginKey);
439
+ };
package/src/utils.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import * as Y from 'yjs';
1
2
  import * as sha256 from 'lib0/hash/sha256';
2
3
  import * as buf from 'lib0/buffer';
3
4
 
@@ -11,3 +12,8 @@ const _convolute = (digest: Uint8Array) => {
11
12
 
12
13
  export const hashOfJSON = (json: any) =>
13
14
  buf.toBase64(_convolute(sha256.digest(buf.encodeAny(json))));
15
+
16
+ export const isVisible = (item: Y.Item, snapshot: Y.Snapshot) =>
17
+ snapshot === undefined ? !item.deleted : (snapshot.sv.has(item.id.client) &&
18
+ (snapshot.sv.get(item.id.client)!) > item.id.clock &&
19
+ !Y.isDeleted(snapshot.ds, item.id));