@kerebron/extension-yjs 0.6.7 → 0.7.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.
Files changed (103) hide show
  1. package/esm/ExtensionYjs.d.ts +3 -11
  2. package/esm/ExtensionYjs.d.ts.map +1 -1
  3. package/esm/ExtensionYjs.js +71 -45
  4. package/esm/ExtensionYjs.js.map +1 -1
  5. package/esm/WebsocketProvider.d.ts +70 -0
  6. package/esm/WebsocketProvider.d.ts.map +1 -0
  7. package/esm/WebsocketProvider.js +377 -0
  8. package/esm/WebsocketProvider.js.map +1 -0
  9. package/esm/YjsProvider.d.ts +48 -0
  10. package/esm/YjsProvider.d.ts.map +1 -0
  11. package/esm/YjsProvider.js +12 -0
  12. package/esm/YjsProvider.js.map +1 -0
  13. package/esm/_dnt.shims.d.ts +2 -0
  14. package/esm/_dnt.shims.d.ts.map +1 -0
  15. package/esm/_dnt.shims.js +58 -0
  16. package/esm/_dnt.shims.js.map +1 -0
  17. package/esm/binding/BindingMetadata.d.ts +6 -0
  18. package/esm/binding/BindingMetadata.d.ts.map +1 -0
  19. package/esm/binding/BindingMetadata.js +2 -0
  20. package/esm/binding/BindingMetadata.js.map +1 -0
  21. package/esm/binding/DiffViewer.d.ts +17 -0
  22. package/esm/binding/DiffViewer.d.ts.map +1 -0
  23. package/esm/binding/DiffViewer.js +96 -0
  24. package/esm/binding/DiffViewer.js.map +1 -0
  25. package/esm/binding/PmYjsBinding.d.ts +45 -0
  26. package/esm/binding/PmYjsBinding.d.ts.map +1 -0
  27. package/esm/binding/PmYjsBinding.js +230 -0
  28. package/esm/binding/PmYjsBinding.js.map +1 -0
  29. package/esm/binding/convertUtils.d.ts +48 -0
  30. package/esm/binding/convertUtils.d.ts.map +1 -0
  31. package/esm/binding/convertUtils.js +80 -0
  32. package/esm/binding/convertUtils.js.map +1 -0
  33. package/esm/{createNodeFromYElement.d.ts → binding/createNodeFromYElement.d.ts} +2 -2
  34. package/esm/binding/createNodeFromYElement.d.ts.map +1 -0
  35. package/esm/{createNodeFromYElement.js → binding/createNodeFromYElement.js} +2 -2
  36. package/esm/binding/createNodeFromYElement.js.map +1 -0
  37. package/esm/{updateYFragment.d.ts → binding/updateYFragment.d.ts} +3 -3
  38. package/esm/binding/updateYFragment.d.ts.map +1 -0
  39. package/esm/{updateYFragment.js → binding/updateYFragment.js} +10 -7
  40. package/esm/binding/updateYFragment.js.map +1 -0
  41. package/esm/debug.d.ts.map +1 -1
  42. package/esm/debug.js +11 -0
  43. package/esm/debug.js.map +1 -1
  44. package/esm/lib.d.ts +1 -7
  45. package/esm/lib.d.ts.map +1 -1
  46. package/esm/lib.js +1 -200
  47. package/esm/lib.js.map +1 -1
  48. package/esm/position.d.ts +8 -0
  49. package/esm/position.d.ts.map +1 -0
  50. package/esm/position.js +165 -0
  51. package/esm/position.js.map +1 -0
  52. package/esm/ui/selection.d.ts +29 -0
  53. package/esm/ui/selection.d.ts.map +1 -0
  54. package/esm/ui/selection.js +129 -0
  55. package/esm/ui/selection.js.map +1 -0
  56. package/esm/utils.d.ts +1 -1
  57. package/esm/utils.d.ts.map +1 -1
  58. package/esm/utils.js.map +1 -1
  59. package/esm/yPositionPlugin.d.ts +6 -1
  60. package/esm/yPositionPlugin.d.ts.map +1 -1
  61. package/esm/yPositionPlugin.js +91 -50
  62. package/esm/yPositionPlugin.js.map +1 -1
  63. package/esm/ySyncPlugin.d.ts +5 -22
  64. package/esm/ySyncPlugin.d.ts.map +1 -1
  65. package/esm/ySyncPlugin.js +70 -101
  66. package/esm/ySyncPlugin.js.map +1 -1
  67. package/esm/yUndoPlugin.d.ts +11 -10
  68. package/esm/yUndoPlugin.d.ts.map +1 -1
  69. package/esm/yUndoPlugin.js +90 -52
  70. package/esm/yUndoPlugin.js.map +1 -1
  71. package/package.json +9 -6
  72. package/src/ExtensionYjs.ts +98 -67
  73. package/src/WebsocketProvider.ts +528 -0
  74. package/src/YjsProvider.ts +75 -0
  75. package/src/_dnt.shims.ts +60 -0
  76. package/src/binding/BindingMetadata.ts +6 -0
  77. package/src/binding/DiffViewer.ts +138 -0
  78. package/src/binding/PmYjsBinding.ts +360 -0
  79. package/src/binding/convertUtils.ts +124 -0
  80. package/src/{createNodeFromYElement.ts → binding/createNodeFromYElement.ts} +4 -4
  81. package/src/{updateYFragment.ts → binding/updateYFragment.ts} +15 -8
  82. package/src/debug.ts +21 -0
  83. package/src/lib.ts +4 -230
  84. package/src/position.ts +191 -0
  85. package/src/ui/selection.ts +218 -0
  86. package/src/utils.ts +1 -1
  87. package/src/yPositionPlugin.ts +122 -74
  88. package/src/ySyncPlugin.ts +111 -155
  89. package/src/yUndoPlugin.ts +113 -62
  90. package/esm/ProsemirrorBinding.d.ts +0 -60
  91. package/esm/ProsemirrorBinding.d.ts.map +0 -1
  92. package/esm/ProsemirrorBinding.js +0 -405
  93. package/esm/ProsemirrorBinding.js.map +0 -1
  94. package/esm/createNodeFromYElement.d.ts.map +0 -1
  95. package/esm/createNodeFromYElement.js.map +0 -1
  96. package/esm/updateYFragment.d.ts.map +0 -1
  97. package/esm/updateYFragment.js.map +0 -1
  98. package/esm/userColors.d.ts +0 -5
  99. package/esm/userColors.d.ts.map +0 -1
  100. package/esm/userColors.js +0 -11
  101. package/esm/userColors.js.map +0 -1
  102. package/src/ProsemirrorBinding.ts +0 -607
  103. package/src/userColors.ts +0 -10
@@ -0,0 +1,138 @@
1
+ import * as Y from 'yjs';
2
+
3
+ import { Node } from 'prosemirror-model';
4
+ import { EditorState } from 'prosemirror-state';
5
+
6
+ import { isVisible } from '../utils.js';
7
+ import { YjsData } from './PmYjsBinding.js';
8
+ import { createNodeFromYElement } from './createNodeFromYElement.js';
9
+
10
+ export class DiffViewer {
11
+ prevSnapshot?: Y.Snapshot;
12
+ snapshot?: Y.Snapshot;
13
+
14
+ private historyYXmlFragment: Y.XmlFragment | undefined;
15
+ private historyDoc: Y.Doc | undefined;
16
+
17
+ constructor() {
18
+ }
19
+
20
+ isActive() {
21
+ return !!this.snapshot || !!this.prevSnapshot;
22
+ }
23
+
24
+ setSnapshot(yjs: YjsData, uSnapshot: Uint8Array, uPrevSnapshot?: Uint8Array) {
25
+ this.prevSnapshot = undefined;
26
+ if (uPrevSnapshot) {
27
+ this.prevSnapshot = Y.decodeSnapshotV2(uPrevSnapshot);
28
+ }
29
+
30
+ this.snapshot = Y.decodeSnapshotV2(uSnapshot);
31
+
32
+ this.historyDoc = Y.createDocFromSnapshot(
33
+ yjs.ydoc,
34
+ this.snapshot,
35
+ new Y.Doc({ gc: false }),
36
+ );
37
+
38
+ if (yjs.xmlFragment._item === null) {
39
+ /**
40
+ * If is a root type, we need to find the root key in the initial document
41
+ * and use it to get the history type.
42
+ */
43
+ const share: Map<string, Y.AbstractType<Y.YEvent<any>>> = yjs.ydoc.share;
44
+ const rootKey = Array.from(share.keys()).find(
45
+ (key) => share.get(key) === yjs.xmlFragment as Y.AbstractType<any>,
46
+ );
47
+ this.historyYXmlFragment = this.historyDoc.getXmlFragment(rootKey);
48
+ } else {
49
+ /**
50
+ * If it is a sub type, we use the item id to find the history type.
51
+ */
52
+ const historyStructs =
53
+ this.historyDoc.store.clients.get(yjs.xmlFragment._item.id.client) ??
54
+ [];
55
+ const itemIndex = Y.findIndexSS(
56
+ historyStructs,
57
+ yjs.xmlFragment._item.id.clock,
58
+ );
59
+ if (historyStructs[itemIndex] instanceof Y.GC) {
60
+ throw new Error('Incorrect type Y.GC');
61
+ }
62
+ const item: Y.Item = historyStructs[itemIndex];
63
+ const content: Y.ContentType = item.content;
64
+ this.historyYXmlFragment = content.type as Y.XmlFragment;
65
+ }
66
+ }
67
+
68
+ reset() {
69
+ this.snapshot = undefined;
70
+ this.prevSnapshot = undefined;
71
+ this.historyDoc = undefined;
72
+ this.historyYXmlFragment = undefined;
73
+ }
74
+
75
+ getFragmentContent(
76
+ state: EditorState,
77
+ ytr: Y.Transaction,
78
+ ): Node[] | undefined {
79
+ if (!this.historyYXmlFragment || !this.snapshot) {
80
+ return;
81
+ }
82
+
83
+ // before rendering, we are going to sanitize ops and split deleted ops
84
+ // if they were deleted by seperate users.
85
+ const pud: Y.PermanentUserData | undefined = undefined;
86
+ // pluginState.permanentUserData;
87
+ if (pud) {
88
+ pud.dss.forEach((ds) => {
89
+ Y.iterateDeletedStructs(ytr, ds, (_item) => {});
90
+ });
91
+ }
92
+ const computeYChange = (type: 'removed' | 'added', id: Y.ID) => {
93
+ const user = pud
94
+ ? (type === 'added'
95
+ ? pud.getUserByClientId(id.client)
96
+ : pud.getUserByDeletedId(id))
97
+ : undefined;
98
+ return {
99
+ user,
100
+ type,
101
+ };
102
+ };
103
+
104
+ const deleteSet = this.prevSnapshot
105
+ ? this.prevSnapshot.ds
106
+ : Y.emptySnapshot.ds;
107
+
108
+ // Create document fragment and render
109
+ const fragmentContent = Y.typeListToArraySnapshot(
110
+ this.historyYXmlFragment,
111
+ new Y.Snapshot(deleteSet, this.snapshot.sv),
112
+ ).map((t) => {
113
+ if (
114
+ !t._item.deleted || isVisible(t._item, this.snapshot) ||
115
+ isVisible(t._item, this.prevSnapshot)
116
+ ) {
117
+ return createNodeFromYElement(
118
+ t,
119
+ state.schema,
120
+ { mapping: new Map(), isOverlappingMark: new Map() },
121
+ this.snapshot,
122
+ this.prevSnapshot,
123
+ computeYChange,
124
+ );
125
+ } else {
126
+ // No need to render elements that are not visible by either snapshot.
127
+ // If a client adds and deletes content in the same snapshot the element is not visible by either snapshot.
128
+ return null;
129
+ }
130
+ }).filter((n) => n !== null);
131
+
132
+ return fragmentContent;
133
+ }
134
+
135
+ getHistoryDoc(): Y.Doc | undefined {
136
+ return this.historyDoc;
137
+ }
138
+ }
@@ -0,0 +1,360 @@
1
+ import * as Y from 'yjs';
2
+
3
+ import { Fragment, Slice } from 'prosemirror-model';
4
+ import { Transaction } from 'prosemirror-state';
5
+
6
+ import { CoreEditor } from '@kerebron/editor';
7
+ import { User } from '@kerebron/editor/user';
8
+
9
+ import { CreateYjsProvider, YjsProvider } from '../YjsProvider.js';
10
+ import { ProsemirrorMapping } from '../lib.js';
11
+ import { SelectionStash } from '../ui/selection.js';
12
+ import { ySyncPluginKey } from '../keys.js';
13
+
14
+ import { yXmlFragmentToProseMirrorRootNode } from './convertUtils.js';
15
+ import { updateYFragment } from './updateYFragment.js';
16
+ import { BindingMetadata } from './BindingMetadata.js';
17
+ import { createNodeIfNotExists } from './createNodeFromYElement.js';
18
+ import { DiffViewer } from './DiffViewer.js';
19
+
20
+ type Mutex = (f: () => void, g?: () => void) => void;
21
+
22
+ export const createMutex = (): Mutex => {
23
+ let token = true;
24
+ return (f: () => void, g?: () => void) => {
25
+ if (token) {
26
+ token = false;
27
+ try {
28
+ f();
29
+ } finally {
30
+ token = true;
31
+ }
32
+ } else if (g !== undefined) {
33
+ g();
34
+ }
35
+ };
36
+ };
37
+
38
+ type ConnectionState = 'idle' | 'joining' | 'synced' | 'leaving' | 'error';
39
+
40
+ export type YjsData = { ydoc: Y.Doc; xmlFragment: Y.XmlFragment };
41
+
42
+ export class PmYjsBinding extends EventTarget {
43
+ private provider: YjsProvider | undefined;
44
+ private connectionState: ConnectionState = 'idle';
45
+
46
+ private yjs: YjsData | undefined;
47
+ private selectionStash: SelectionStash | undefined;
48
+ public readonly diffViewer: DiffViewer;
49
+
50
+ public addToYjsHistory = true;
51
+ private hasImported = false;
52
+ private mux: Mutex;
53
+ private bindingMetadata: BindingMetadata;
54
+
55
+ private syncedHandler: (
56
+ event: CustomEvent<{
57
+ state: boolean;
58
+ }>,
59
+ ) => void;
60
+ private _observeFunction: (
61
+ events: Array<Y.YEvent<any>>,
62
+ transaction: Y.Transaction,
63
+ ) => void;
64
+
65
+ constructor(private readonly editor: CoreEditor) {
66
+ super();
67
+
68
+ this.diffViewer = new DiffViewer();
69
+ this.bindingMetadata = { mapping: new Map(), isOverlappingMark: new Map() };
70
+
71
+ this.mux = createMutex();
72
+
73
+ this.syncedHandler = (
74
+ event: CustomEvent<{
75
+ state: boolean;
76
+ }>,
77
+ ) => {
78
+ const synced = event.detail.state;
79
+ if (synced && !this.hasImported) {
80
+ this.importRemoteYdoc();
81
+ } else {
82
+ // this.ydocChanged();
83
+ }
84
+ };
85
+
86
+ this._observeFunction = (events, transaction) => {
87
+ this.yXmlChanged(events, transaction);
88
+ };
89
+ }
90
+
91
+ destroy() {
92
+ this.connectionState = 'idle';
93
+ const tr = this.editor.state.tr;
94
+ this.leaveRoom(tr);
95
+ this.editor.dispatchTransaction(tr);
96
+ this.diffViewer.reset();
97
+ this.selectionStash?.destroy();
98
+ }
99
+
100
+ changeUser(user: User) {
101
+ if (!this.provider) {
102
+ return;
103
+ }
104
+ this.provider.awareness.setLocalStateField('kerebron:user', user);
105
+ }
106
+
107
+ changeRoom(
108
+ roomId: string,
109
+ createYjsProvider: CreateYjsProvider,
110
+ tr: Transaction,
111
+ ) {
112
+ this.connectionState = 'joining';
113
+ this.hasImported = false;
114
+
115
+ if (this.provider) {
116
+ this.provider.removeEventListener('synced', this.syncedHandler);
117
+ this.provider.destroy();
118
+ this.provider = undefined;
119
+ }
120
+
121
+ const [provider, ydoc] = createYjsProvider(
122
+ roomId,
123
+ );
124
+
125
+ this.addToYjsHistory = true;
126
+ this.provider = provider;
127
+
128
+ this.bindingMetadata.mapping.clear();
129
+ this.bindingMetadata.isOverlappingMark.clear();
130
+ this.diffViewer.reset();
131
+
132
+ const fieldName = 'kerebron:' + this.editor.schema.topNodeType.name;
133
+ this.yjs = { ydoc, xmlFragment: ydoc.getXmlFragment(fieldName) };
134
+ this.selectionStash = new SelectionStash(
135
+ this.yjs,
136
+ this.getMapping(),
137
+ this.editor,
138
+ );
139
+ this.yjs.xmlFragment.observeDeep(this._observeFunction);
140
+
141
+ this.provider.addEventListener('synced', this.syncedHandler);
142
+
143
+ tr.setMeta('yjs:setAwareness', provider.awareness);
144
+ tr.setMeta('setYjs', this.yjs);
145
+ }
146
+
147
+ leaveRoom(tr: Transaction) {
148
+ this.connectionState = 'leaving';
149
+ if (this.provider) {
150
+ this.provider.removeEventListener('synced', this.syncedHandler);
151
+ this.provider.destroy();
152
+ }
153
+
154
+ this.addToYjsHistory = true;
155
+ this.provider = undefined;
156
+
157
+ this.bindingMetadata.mapping.clear();
158
+ this.bindingMetadata.isOverlappingMark.clear();
159
+ this.diffViewer.reset();
160
+
161
+ this.selectionStash?.destroy();
162
+ this.selectionStash = undefined;
163
+ this.yjs?.xmlFragment.unobserveDeep(this._observeFunction);
164
+ this.yjs = undefined;
165
+
166
+ this.hasImported = false;
167
+
168
+ tr.setMeta('addToYjsHistory', false);
169
+ tr.setMeta('yjs:removeAwareness', true);
170
+ tr.setMeta('clearYjs', true);
171
+ }
172
+
173
+ importRemoteYdoc() {
174
+ if (!this.yjs) {
175
+ return;
176
+ }
177
+
178
+ this.addToYjsHistory = this.editor.state.doc.content.size > 2; // Non empty para
179
+
180
+ if (this.yjs.xmlFragment.length === 0) {
181
+ this.createFromProseMirror(this.yjs);
182
+ } else {
183
+ this.overwriteProseMirror(this.yjs.xmlFragment);
184
+ }
185
+
186
+ this.connectionState = 'synced';
187
+ this.hasImported = true;
188
+ }
189
+
190
+ yXmlChanged(events: Array<Y.YEvent<any>>, ytr: Y.Transaction) {
191
+ this.mux(() => {
192
+ if (!this.yjs) {
193
+ return;
194
+ }
195
+
196
+ if (this.diffViewer.isActive()) {
197
+ return;
198
+ }
199
+
200
+ const state = this.editor.state;
201
+
202
+ const { xmlFragment } = this.yjs;
203
+
204
+ const mapping = this.bindingMetadata.mapping;
205
+ const delType = (_: any, type: Y.AbstractType<any>) =>
206
+ mapping.delete(type);
207
+ Y.iterateDeletedStructs(
208
+ ytr,
209
+ ytr.deleteSet,
210
+ (struct) => {
211
+ if (struct.constructor === Y.Item) {
212
+ const type = (struct as Y.Item).content.type;
213
+ type && mapping.delete(type);
214
+ }
215
+ },
216
+ );
217
+ ytr.changed.forEach(delType);
218
+ ytr.changedParentTypes.forEach(delType);
219
+
220
+ const fragmentContent = xmlFragment.toArray().map((t) =>
221
+ createNodeIfNotExists(
222
+ t as Y.XmlElement,
223
+ state.schema,
224
+ this.bindingMetadata,
225
+ )
226
+ ).filter((n) => n !== null);
227
+
228
+ const tr = state.tr;
229
+ tr.setMeta('addToYjsHistory', false);
230
+ tr.replace(
231
+ 0,
232
+ state.doc.content.size,
233
+ new Slice(Fragment.from(fragmentContent), 0, 0),
234
+ );
235
+
236
+ this.selectionStash?.restore(tr);
237
+ this.editor.dispatchTransaction(tr);
238
+ });
239
+ }
240
+
241
+ pmChanged() {
242
+ if (!this.hasImported) {
243
+ return;
244
+ }
245
+
246
+ if (!this.yjs) {
247
+ return;
248
+ }
249
+
250
+ if (this.diffViewer.isActive()) {
251
+ return;
252
+ }
253
+
254
+ const doc = this.editor.state.doc;
255
+ const { ydoc, xmlFragment } = this.yjs;
256
+
257
+ this.mux(() => {
258
+ const origin = ySyncPluginKey;
259
+
260
+ ydoc.transact((ytr) => {
261
+ ytr.meta.set('addToYjsHistory', this.addToYjsHistory);
262
+ updateYFragment(
263
+ ydoc,
264
+ xmlFragment,
265
+ doc,
266
+ this.bindingMetadata,
267
+ this.addToYjsHistory,
268
+ );
269
+ this.selectionStash?.store();
270
+ }, origin);
271
+ });
272
+ }
273
+
274
+ createFromProseMirror(yjs: YjsData) {
275
+ const doc = this.editor.state.doc;
276
+ const { ydoc, xmlFragment } = yjs;
277
+ updateYFragment(
278
+ ydoc,
279
+ xmlFragment,
280
+ doc,
281
+ this.bindingMetadata,
282
+ this.addToYjsHistory,
283
+ );
284
+ }
285
+
286
+ overwriteProseMirror(yXmlFragment: Y.XmlFragment) {
287
+ const state = this.editor.state;
288
+ const newRoot = yXmlFragmentToProseMirrorRootNode(
289
+ yXmlFragment,
290
+ state.schema,
291
+ );
292
+
293
+ const tr = state.tr;
294
+ tr.setMeta('addToYjsHistory', false);
295
+ tr.replaceWith(
296
+ 0,
297
+ state.doc.content.size,
298
+ newRoot,
299
+ );
300
+
301
+ this.editor.dispatchTransaction(tr);
302
+ }
303
+
304
+ getYjs(): YjsData | undefined {
305
+ return this.yjs;
306
+ }
307
+
308
+ getMapping(): ProsemirrorMapping {
309
+ return this.bindingMetadata.mapping;
310
+ }
311
+
312
+ getSelectionStash(): SelectionStash | undefined {
313
+ return this.selectionStash;
314
+ }
315
+
316
+ isEditable() {
317
+ return !this.diffViewer.isActive();
318
+ }
319
+
320
+ setSnapshot(
321
+ snapshot: Uint8Array<ArrayBufferLike>,
322
+ prevSnapshot: Uint8Array<ArrayBufferLike> | undefined,
323
+ ) {
324
+ if (!this.yjs) {
325
+ return;
326
+ }
327
+ this.diffViewer.setSnapshot(this.yjs, snapshot, prevSnapshot);
328
+
329
+ // clear mapping because we are going to rerender
330
+ this.bindingMetadata.mapping.clear();
331
+ this.bindingMetadata.isOverlappingMark.clear();
332
+
333
+ this.mux(() => {
334
+ const historyDoc = this.diffViewer.getHistoryDoc();
335
+ if (!historyDoc) {
336
+ return;
337
+ }
338
+
339
+ historyDoc.transact((ytr) => {
340
+ const state = this.editor.state;
341
+ const fragmentContent = this.diffViewer.getFragmentContent(state, ytr);
342
+
343
+ if (!fragmentContent) {
344
+ return;
345
+ }
346
+
347
+ const tr = state.tr;
348
+ tr.replace(
349
+ 0,
350
+ state.doc.content.size,
351
+ new Slice(Fragment.from(fragmentContent), 0, 0),
352
+ );
353
+
354
+ tr.setMeta('addToHistory', false);
355
+ tr.setMeta(ySyncPluginKey, { isChangeOrigin: true }),
356
+ this.editor.dispatchTransaction(tr);
357
+ }, ySyncPluginKey);
358
+ });
359
+ }
360
+ }
@@ -0,0 +1,124 @@
1
+ import * as Y from 'yjs';
2
+
3
+ import { Fragment, Node, type Schema } from 'prosemirror-model';
4
+
5
+ import { TransactFunc } from '../lib.js';
6
+
7
+ import { updateYFragment } from './updateYFragment.js';
8
+ import { createNodeFromYElement } from './createNodeFromYElement.js';
9
+ import { BindingMetadata } from './BindingMetadata.js';
10
+
11
+ export const createEmptyMeta = (): BindingMetadata => ({
12
+ mapping: new Map(),
13
+ isOverlappingMark: new Map(),
14
+ });
15
+
16
+ /**
17
+ * Utility function for converting an Y.Fragment to a ProseMirror fragment.
18
+ */
19
+ export const yXmlFragmentToProseMirrorFragment = (
20
+ yXmlFragment: Y.XmlFragment,
21
+ schema: Schema,
22
+ ) => {
23
+ const elements: Y.XmlElement[] = yXmlFragment.toArray().filter((i) =>
24
+ i instanceof Y.XmlElement
25
+ );
26
+ const fragmentContent = elements.map((t) =>
27
+ createNodeFromYElement(
28
+ t,
29
+ schema,
30
+ createEmptyMeta(),
31
+ )
32
+ ).filter((n) => n !== null);
33
+ return Fragment.fromArray(fragmentContent);
34
+ };
35
+
36
+ /**
37
+ * Utility function for converting an Y.Fragment to a ProseMirror node.
38
+ */
39
+ export const yXmlFragmentToProseMirrorRootNode = (
40
+ yXmlFragment: Y.XmlFragment,
41
+ schema: Schema,
42
+ ) =>
43
+ schema.topNodeType.create(
44
+ null,
45
+ yXmlFragmentToProseMirrorFragment(yXmlFragment, schema),
46
+ );
47
+
48
+ /**
49
+ * Utility method to convert a Prosemirror Doc Node into a Y.Doc.
50
+ *
51
+ * This can be used when importing existing content to Y.Doc for the first time,
52
+ * note that this should not be used to rehydrate a Y.Doc from a database once
53
+ * collaboration has begun as all history will be lost
54
+ */
55
+ export function prosemirrorToYDoc(
56
+ doc: Node,
57
+ xmlFragment: string = 'prosemirror',
58
+ ): Y.Doc {
59
+ const ydoc = new Y.Doc();
60
+ const type: Y.XmlFragment = ydoc.get(xmlFragment, Y.XmlFragment);
61
+ if (!type.doc) {
62
+ return ydoc;
63
+ }
64
+
65
+ prosemirrorToYXmlFragment(doc, type);
66
+ return type.doc;
67
+ }
68
+
69
+ /**
70
+ * Utility method to update an empty Y.XmlFragment with content from a Prosemirror Doc Node.
71
+ *
72
+ * This can be used when importing existing content to Y.Doc for the first time,
73
+ * note that this should not be used to rehydrate a Y.Doc from a database once
74
+ * collaboration has begun as all history will be lost
75
+ *
76
+ * Note: The Y.XmlFragment does not need to be part of a Y.Doc document at the time that this
77
+ * method is called, but it must be added before any other operations are performed on it.
78
+ */
79
+ export function prosemirrorToYXmlFragment(
80
+ doc: Node,
81
+ xmlFragment: Y.XmlFragment,
82
+ ): Y.XmlFragment {
83
+ const type = xmlFragment || new Y.XmlFragment();
84
+ const ydoc: { transact: TransactFunc<void> } = type.doc
85
+ ? type.doc
86
+ : { transact: (transaction) => transaction(undefined) };
87
+ updateYFragment(ydoc, type, doc, {
88
+ mapping: new Map(),
89
+ isOverlappingMark: new Map(),
90
+ });
91
+ return type;
92
+ }
93
+
94
+ /**
95
+ * Utility method to convert Prosemirror compatible JSON into a Y.Doc.
96
+ *
97
+ * This can be used when importing existing content to Y.Doc for the first time,
98
+ * note that this should not be used to rehydrate a Y.Doc from a database once
99
+ * collaboration has begun as all history will be lost
100
+ */
101
+ export function prosemirrorJSONToYDoc(
102
+ schema: Schema,
103
+ state: any,
104
+ xmlFragment: string = 'prosemirror',
105
+ ): Y.Doc {
106
+ const doc = Node.fromJSON(schema, state);
107
+ return prosemirrorToYDoc(doc, xmlFragment);
108
+ }
109
+
110
+ /**
111
+ * Utility method to convert Prosemirror compatible JSON to a Y.XmlFragment
112
+ *
113
+ * This can be used when importing existing content to Y.Doc for the first time,
114
+ * note that this should not be used to rehydrate a Y.Doc from a database once
115
+ * collaboration has begun as all history will be lost
116
+ */
117
+ export function prosemirrorJSONToYXmlFragment(
118
+ schema: Schema,
119
+ state: any,
120
+ xmlFragment: Y.XmlFragment,
121
+ ): Y.XmlFragment {
122
+ const doc = Node.fromJSON(schema, state);
123
+ return prosemirrorToYXmlFragment(doc, xmlFragment);
124
+ }
@@ -2,9 +2,9 @@ import * as PModel from 'prosemirror-model';
2
2
  import { Mark, Schema } from 'prosemirror-model';
3
3
  import * as Y from 'yjs';
4
4
 
5
- import type { BindingMetadata } from './ProsemirrorBinding.js';
6
- import { ySyncPluginKey } from './keys.js';
7
- import { isVisible } from './utils.js';
5
+ import type { BindingMetadata } from './BindingMetadata.js';
6
+ import { ySyncPluginKey } from '../keys.js';
7
+ import { isVisible } from '../utils.js';
8
8
  import { yattr2markname } from './updateYFragment.js';
9
9
 
10
10
  export const attributesToMarks = (
@@ -51,7 +51,7 @@ export const createNodeFromYElement = (
51
51
  meta: BindingMetadata,
52
52
  snapshot?: Y.Snapshot,
53
53
  prevSnapshot?: Y.Snapshot,
54
- computeYChange?: (arg0: 'removed' | 'added', arg1: Y.ID) => any,
54
+ computeYChange?: (changeType: 'removed' | 'added', yid: Y.ID) => any,
55
55
  ): PModel.Node | null => {
56
56
  const children: PModel.Node[] = [];
57
57
  const createChildren = (type: Y.XmlElement | Y.XmlText) => {
@@ -4,10 +4,10 @@ import { Mark, Node, Schema } from 'prosemirror-model';
4
4
 
5
5
  import { simpleDiff } from 'lib0/diff';
6
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';
7
+ import { ySyncPluginKey } from '../keys.js';
8
+ import * as utils from '../utils.js';
9
+ import type { BindingMetadata } from './BindingMetadata.js';
10
+ import { TransactFunc } from '../lib.js';
11
11
 
12
12
  const hashedMarkNameRegex = /(.*)(--[a-zA-Z0-9+/=]{8})$/;
13
13
  export const yattr2markname = (attrName: string) =>
@@ -21,8 +21,11 @@ const marksToAttributes = (
21
21
  marks.forEach((mark) => {
22
22
  if (mark.type.name !== 'ychange') {
23
23
  let isOverlapping = true;
24
- if (!meta.isOMark.has(mark.type.name)) {
25
- meta.isOMark.set(mark.type.name, !mark.type.excludes(mark.type));
24
+ if (!meta.isOverlappingMark.has(mark.type.name)) {
25
+ meta.isOverlappingMark.set(
26
+ mark.type.name,
27
+ !mark.type.excludes(mark.type),
28
+ );
26
29
  isOverlapping = false;
27
30
  }
28
31
 
@@ -278,7 +281,9 @@ export const updateYFragment = (
278
281
  yDomFragment: Y.XmlFragment,
279
282
  pNode: Node,
280
283
  meta: BindingMetadata,
284
+ addToYjsHistory?: boolean,
281
285
  ) => {
286
+ const origin = ySyncPluginKey;
282
287
  if (
283
288
  yDomFragment instanceof Y.XmlElement &&
284
289
  yDomFragment.nodeName !== pNode.type.name
@@ -342,7 +347,9 @@ export const updateYFragment = (
342
347
  }
343
348
  }
344
349
  }
345
- ydoc.transact(() => {
350
+ ydoc.transact((ytr) => {
351
+ ytr.meta.set('updateYFragment', 1);
352
+ ytr.meta.set('addToYjsHistory', addToYjsHistory);
346
353
  // try to compare and update
347
354
  while (yChildCnt - left - right > 0 && pChildCnt - left - right > 0) {
348
355
  const leftY: Y.XmlElement | Y.XmlText = yChildren[left];
@@ -435,5 +442,5 @@ export const updateYFragment = (
435
442
  }
436
443
  yDomFragment.insert(left, ins);
437
444
  }
438
- }, ySyncPluginKey);
445
+ }, origin);
439
446
  };