@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,175 @@
1
+ import * as PModel from 'prosemirror-model';
2
+ import { Mark, Schema } from 'prosemirror-model';
3
+ import * as Y from 'yjs';
4
+
5
+ import type { BindingMetadata } from './ProsemirrorBinding.js';
6
+ import { ySyncPluginKey } from './keys.js';
7
+ import { isVisible } from './utils.js';
8
+ import { yattr2markname } from './updateYFragment.js';
9
+
10
+ export const attributesToMarks = (
11
+ attrs: { [s: string]: any },
12
+ schema: Schema,
13
+ ) => {
14
+ const marks: Array<Mark> = [];
15
+ for (const markName in attrs) {
16
+ // remove hashes if necessary
17
+ marks.push(schema.mark(yattr2markname(markName), attrs[markName]));
18
+ }
19
+ return marks;
20
+ };
21
+
22
+ export const createNodeIfNotExists = (
23
+ el: Y.XmlElement,
24
+ schema: PModel.Schema,
25
+ meta: BindingMetadata,
26
+ snapshot?: Y.Snapshot,
27
+ prevSnapshot?: Y.Snapshot,
28
+ computeYChange?: (arg0: 'removed' | 'added', arg1: Y.ID) => any,
29
+ ): PModel.Node | null => {
30
+ const node: PModel.Node = meta.mapping.get(el) as PModel.Node;
31
+ if (node === undefined) {
32
+ if (el instanceof Y.XmlElement) {
33
+ return createNodeFromYElement(
34
+ el,
35
+ schema,
36
+ meta,
37
+ snapshot,
38
+ prevSnapshot,
39
+ computeYChange,
40
+ );
41
+ } else {
42
+ throw new Error('methodUnimplemented'); // we are currently not handling hooks
43
+ }
44
+ }
45
+ return node;
46
+ };
47
+
48
+ export const createNodeFromYElement = (
49
+ el: Y.XmlElement,
50
+ schema: Schema,
51
+ meta: BindingMetadata,
52
+ snapshot?: Y.Snapshot,
53
+ prevSnapshot?: Y.Snapshot,
54
+ computeYChange?: (arg0: 'removed' | 'added', arg1: Y.ID) => any,
55
+ ): PModel.Node | null => {
56
+ const children: PModel.Node[] = [];
57
+ const createChildren = (type: Y.XmlElement | Y.XmlText) => {
58
+ if (type instanceof Y.XmlElement) {
59
+ const n = createNodeIfNotExists(
60
+ type,
61
+ schema,
62
+ meta,
63
+ snapshot,
64
+ prevSnapshot,
65
+ computeYChange,
66
+ );
67
+ if (n !== null) {
68
+ children.push(n);
69
+ }
70
+ } else {
71
+ // If the next ytext exists and was created by us, move the content to the current ytext.
72
+ // This is a fix for #160 -- duplication of characters when two Y.Text exist next to each
73
+ // other.
74
+ const nextytext: Y.ContentType = type._item?.right?.content?.type;
75
+
76
+ if (
77
+ nextytext &&
78
+ nextytext instanceof Y.Text && nextytext._item &&
79
+ !nextytext._item.deleted &&
80
+ nextytext._item.id.client === nextytext.doc!.clientID
81
+ ) {
82
+ type.applyDelta([
83
+ { retain: type.length },
84
+ ...nextytext.toDelta(),
85
+ ]);
86
+ nextytext.doc!.transact((tr) => {
87
+ nextytext._item?.delete(tr);
88
+ });
89
+ }
90
+
91
+ // now create the prosemirror text nodes
92
+ const ns = createTextNodesFromYText(
93
+ type,
94
+ schema,
95
+ meta,
96
+ snapshot,
97
+ prevSnapshot,
98
+ computeYChange,
99
+ );
100
+ if (ns !== null) {
101
+ ns.forEach((textchild) => {
102
+ if (textchild !== null) {
103
+ children.push(textchild);
104
+ }
105
+ });
106
+ }
107
+ }
108
+ };
109
+ if (snapshot === undefined || prevSnapshot === undefined) {
110
+ for (const item of el.toArray()) {
111
+ if (item instanceof Y.XmlHook) {
112
+ continue;
113
+ }
114
+ createChildren(item);
115
+ }
116
+ } else {
117
+ Y.typeListToArraySnapshot(el, new Y.Snapshot(prevSnapshot.ds, snapshot.sv))
118
+ .forEach(createChildren);
119
+ }
120
+ try {
121
+ const attrs = el.getAttributes(snapshot);
122
+ const item: Y.Item = el._item!;
123
+ if (snapshot !== undefined) {
124
+ const item: Y.Item = el._item!;
125
+ if (!isVisible(item, snapshot)) {
126
+ attrs.ychange = computeYChange
127
+ ? computeYChange('removed', item.id)
128
+ : { type: 'removed' };
129
+ } else if (!isVisible(item, prevSnapshot)) {
130
+ attrs.ychange = computeYChange
131
+ ? computeYChange('added', item.id)
132
+ : { type: 'added' };
133
+ }
134
+ }
135
+ const node = schema.node(el.nodeName, attrs, children);
136
+ meta.mapping.set(el, node);
137
+ return node;
138
+ } catch (e) {
139
+ // an error occured while creating the node. This is probably a result of a concurrent action.
140
+ el.doc?.transact((transaction) => {
141
+ el._item?.delete(transaction);
142
+ }, ySyncPluginKey);
143
+ meta.mapping.delete(el);
144
+ return null;
145
+ }
146
+ };
147
+
148
+ /**
149
+ * @private
150
+ */
151
+ const createTextNodesFromYText = (
152
+ text: Y.XmlText,
153
+ schema: Schema,
154
+ _meta: BindingMetadata,
155
+ snapshot: Y.Snapshot,
156
+ prevSnapshot: Y.Snapshot,
157
+ computeYChange: (arg0: 'removed' | 'added', arg1: Y.ID) => any,
158
+ ): Array<PModel.Node> | null => {
159
+ const nodes: PModel.Node[] = [];
160
+ const deltas = text.toDelta(snapshot, prevSnapshot, computeYChange);
161
+ try {
162
+ for (let i = 0; i < deltas.length; i++) {
163
+ const delta = deltas[i];
164
+ nodes.push(
165
+ schema.text(delta.insert, attributesToMarks(delta.attributes, schema)),
166
+ );
167
+ }
168
+ } catch (e) {
169
+ text.doc?.transact((transaction) => {
170
+ text._item?.delete(transaction);
171
+ }, ySyncPluginKey);
172
+ return null;
173
+ }
174
+ return nodes;
175
+ };
package/src/debug.ts ADDED
@@ -0,0 +1,218 @@
1
+ import * as Y from 'yjs';
2
+ import { isVisible } from './utils.js';
3
+
4
+ interface YDebugConfig {
5
+ STRIKE: string;
6
+ RESET: string;
7
+ DIM: string;
8
+ isVisible: (item: Y.Item | null) => boolean;
9
+ renderDeleted: (text: string, item: Y.Item | null) => string;
10
+ }
11
+
12
+ const YAnsiDebugConfig: YDebugConfig = {
13
+ STRIKE: '\x1b[9m',
14
+ RESET: '\x1b[0m',
15
+ DIM: '\x1b[2m',
16
+ isVisible: () => true,
17
+ renderDeleted(text: string, item: Y.Item | null) {
18
+ if (item?.deleted) {
19
+ return this.STRIKE + text + this.RESET;
20
+ }
21
+ return text;
22
+ },
23
+ };
24
+
25
+ function indentText(text: string, indent: number = 0): string {
26
+ const indentStr = ' '.repeat(indent);
27
+ const lines = text.split('\n');
28
+ const indentedLines = lines.map((line) => indentStr + line);
29
+
30
+ if (indentedLines[indentedLines.length - 1] === indentStr) {
31
+ indentedLines[indentedLines.length - 1] = '';
32
+ }
33
+
34
+ return indentedLines.join('\n');
35
+ }
36
+
37
+ export function debugYNode(
38
+ node: Y.AbstractType<any>,
39
+ config: YDebugConfig = YAnsiDebugConfig,
40
+ ): string {
41
+ const {
42
+ STRIKE,
43
+ RESET,
44
+ DIM,
45
+ } = config;
46
+
47
+ const yDebugClient = (item: Y.Item | null) => {
48
+ if (!item) {
49
+ return '';
50
+ }
51
+ const { client, clock } = item.id;
52
+ return `\t${DIM}[${client}:${clock}]${RESET}`;
53
+ };
54
+
55
+ let retVal = '';
56
+
57
+ if (node instanceof Y.XmlText) {
58
+ if (!config.isVisible(node._item)) {
59
+ return '';
60
+ }
61
+
62
+ retVal += config.renderDeleted('XmlText', node._item) + ' {';
63
+
64
+ retVal += yDebugClient(node._item);
65
+ retVal += '\n';
66
+
67
+ let text = '';
68
+
69
+ for (let item = node._start; item; item = item.right) {
70
+ if (!config.isVisible(item)) {
71
+ continue;
72
+ }
73
+
74
+ if (item.content instanceof Y.ContentString) {
75
+ const content = item.content?.str || '';
76
+
77
+ text += config.renderDeleted(content, item);
78
+ text += yDebugClient(node._item);
79
+ text += '\n';
80
+ } else {
81
+ text += config.renderDeleted(
82
+ `Unhandled debug: ${typeof item.content}`,
83
+ item,
84
+ );
85
+ text += yDebugClient(node._item);
86
+ text += '\n';
87
+ }
88
+ }
89
+
90
+ retVal += indentText(text, 1);
91
+ retVal += `}\n`;
92
+
93
+ return retVal;
94
+ }
95
+
96
+ if (node instanceof Y.XmlElement) {
97
+ if (!config.isVisible(node._item)) {
98
+ return '';
99
+ }
100
+
101
+ retVal += config.renderDeleted(`<${node.nodeName}>`, node._item);
102
+ retVal += yDebugClient(node._item);
103
+ retVal += '\n';
104
+
105
+ retVal += indentText(
106
+ node.toArray()
107
+ .map((child) => {
108
+ return debugYNode(child, config);
109
+ })
110
+ .join('\n'),
111
+ 1,
112
+ );
113
+
114
+ retVal += config.renderDeleted(`</${node.nodeName}>`, node._item);
115
+ return retVal;
116
+ }
117
+
118
+ if (node instanceof Y.XmlFragment) {
119
+ if (!config.isVisible(node._item)) {
120
+ return '';
121
+ }
122
+
123
+ retVal += config.renderDeleted(`Y.XmlFragment`, node._item);
124
+ retVal += yDebugClient(node._item);
125
+ retVal += '\n';
126
+
127
+ retVal += indentText(
128
+ node.toArray()
129
+ .map((child) => {
130
+ return debugYNode(child, config);
131
+ })
132
+ .join('\n'),
133
+ 1,
134
+ );
135
+ return retVal;
136
+ }
137
+
138
+ if (node instanceof Y.Map) {
139
+ if (!config.isVisible(node._item)) {
140
+ return '';
141
+ }
142
+
143
+ const entries = Array.from(node.entries());
144
+
145
+ retVal += config.renderDeleted(`Y.Map(${entries.length})`, node._item);
146
+ retVal += yDebugClient(node._item);
147
+ retVal += '\n';
148
+
149
+ retVal += indentText(
150
+ entries
151
+ .map(([key, value]: [string, any]) =>
152
+ '- ' + key + ': ' + debugYNode(value, config)
153
+ )
154
+ .join('\n'),
155
+ 1,
156
+ );
157
+ return retVal;
158
+ }
159
+
160
+ if (node instanceof Y.Array) {
161
+ if (!config.isVisible(node._item)) {
162
+ return '';
163
+ }
164
+
165
+ const arr: Y.Array<any> = node;
166
+
167
+ retVal += config.renderDeleted(`Y.Array(${arr._length})`, node._item);
168
+ retVal += yDebugClient(node._item);
169
+ retVal += '\n';
170
+
171
+ retVal += indentText(
172
+ arr
173
+ .map((value) => '- ' + debugYNode(value, config))
174
+ .join('\n'),
175
+ 1,
176
+ );
177
+ return retVal;
178
+ }
179
+
180
+ if ('object' !== typeof node) {
181
+ retVal += '' + node;
182
+ return retVal;
183
+ }
184
+
185
+ throw new Error('Unhandled debug class: ' + node.constructor.name);
186
+ }
187
+
188
+ export function debugYDoc(
189
+ ydoc: Y.Doc,
190
+ config: YDebugConfig = YAnsiDebugConfig,
191
+ ): string {
192
+ const entries = Array.from(ydoc.share.entries());
193
+ let retVal = '';
194
+ for (const [key, value] of entries) {
195
+ retVal += `${key}: `;
196
+ retVal += debugYNode(value, config);
197
+ retVal += '\n';
198
+ }
199
+ return (`Y.Doc(${entries.length})\n` + indentText(retVal, 1)).trim();
200
+ }
201
+
202
+ export function debugYDocSnapshot(
203
+ ydoc: Y.Doc,
204
+ snapshot: Y.Snapshot,
205
+ ) {
206
+ const retVal = debugYDoc(ydoc, {
207
+ ...YAnsiDebugConfig,
208
+ isVisible: (item: Y.Item | null) => {
209
+ if (!item) return true;
210
+ return isVisible(item, snapshot);
211
+ },
212
+ renderDeleted(text: string) {
213
+ return text;
214
+ },
215
+ });
216
+
217
+ return ('Y.Snapshot\n' + indentText(retVal, 1)).trim();
218
+ }
package/src/keys.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  import { PluginKey } from 'prosemirror-state';
2
- import { UndoPluginState } from './yUndoPlugin.js';
2
+ import { type UndoPluginState } from './yUndoPlugin.js';
3
+ import { type YSyncPluginState } from './ySyncPlugin.js';
4
+ import { type YPositionPluginState } from './yPositionPlugin.js';
3
5
 
4
- /**
5
- * The unique prosemirror plugin key for syncPlugin
6
- */
7
- export const ySyncPluginKey = new PluginKey('y-sync');
6
+ export const ySyncPluginKey = new PluginKey<YSyncPluginState>('y-sync');
8
7
 
9
- /**
10
- * The unique prosemirror plugin key for undoPlugin
11
- */
12
- export const yUndoPluginKey: PluginKey<UndoPluginState> = new PluginKey(
8
+ export const yPositionPluginKey = new PluginKey<YPositionPluginState>(
9
+ 'yjs-position',
10
+ );
11
+
12
+ export const yUndoPluginKey = new PluginKey<UndoPluginState>(
13
13
  'y-undo',
14
14
  );
package/src/lib.ts CHANGED
@@ -7,7 +7,7 @@ import { ySyncPluginKey } from './keys.js';
7
7
  /**
8
8
  * Either a node if type is YXmlElement or an Array of text nodes if YXmlText
9
9
  */
10
- type ProsemirrorMapping = Map<Y.AbstractType<any>, Node>;
10
+ export type ProsemirrorMapping = Map<Y.AbstractType<any>, Node | Array<Node>>;
11
11
 
12
12
  /**
13
13
  * Is null if no timeout is in progress.
@@ -192,7 +192,11 @@ export const relativePositionToAbsolutePosition = (
192
192
  pos += t._length;
193
193
  } else {
194
194
  const node = mapping.get(t);
195
- pos += node?.nodeSize || 0;
195
+ if (Array.isArray(node)) {
196
+ pos += node.reduce((prev, curr) => prev + curr?.nodeSize || 0, 0);
197
+ } else {
198
+ pos += node?.nodeSize || 0;
199
+ }
196
200
  }
197
201
  }
198
202
  n = n.right;
@@ -218,7 +222,11 @@ export const relativePositionToAbsolutePosition = (
218
222
  pos += contentType._length;
219
223
  } else {
220
224
  const node = mapping.get(contentType);
221
- pos += node?.nodeSize || 0;
225
+ if (Array.isArray(node)) {
226
+ pos += node.reduce((prev, curr) => prev + curr?.nodeSize || 0, 0);
227
+ } else {
228
+ pos += node?.nodeSize || 0;
229
+ }
222
230
  }
223
231
  }
224
232
  n = n.right;