@getguru/slate-yjs-core 1.0.3

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 (126) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +3 -0
  3. package/dist/applyToSlate/index.d.ts +23 -0
  4. package/dist/applyToSlate/textEvent.d.ts +4 -0
  5. package/dist/applyToYjs/index.d.ts +4 -0
  6. package/dist/applyToYjs/node/index.d.ts +4 -0
  7. package/dist/applyToYjs/node/insertNode.d.ts +4 -0
  8. package/dist/applyToYjs/node/mergeNode.d.ts +4 -0
  9. package/dist/applyToYjs/node/moveNode.d.ts +4 -0
  10. package/dist/applyToYjs/node/removeNode.d.ts +4 -0
  11. package/dist/applyToYjs/node/setNode.d.ts +4 -0
  12. package/dist/applyToYjs/node/splitNode.d.ts +4 -0
  13. package/dist/applyToYjs/text/index.d.ts +4 -0
  14. package/dist/applyToYjs/text/insertText.d.ts +4 -0
  15. package/dist/applyToYjs/text/removeText.d.ts +4 -0
  16. package/dist/applyToYjs/types.d.ts +9 -0
  17. package/dist/index.cjs +1360 -0
  18. package/dist/index.cjs.map +1 -0
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.global.js +10365 -0
  21. package/dist/index.global.js.map +1 -0
  22. package/dist/index.js +1338 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/model/types.d.ts +38 -0
  25. package/dist/plugins/index.d.ts +4 -0
  26. package/dist/plugins/withCursors.d.ts +39 -0
  27. package/dist/plugins/withYHistory.d.ts +20 -0
  28. package/dist/plugins/withYjs.d.ts +43 -0
  29. package/dist/utils/clone.d.ts +5 -0
  30. package/dist/utils/convert.d.ts +8 -0
  31. package/dist/utils/delta.d.ts +8 -0
  32. package/dist/utils/errors.d.ts +2 -0
  33. package/dist/utils/location.d.ts +12 -0
  34. package/dist/utils/object.d.ts +10 -0
  35. package/dist/utils/position.d.ts +20 -0
  36. package/dist/utils/slate.d.ts +3 -0
  37. package/dist/utils/yjs.d.ts +5 -0
  38. package/package.json +47 -0
  39. package/src/applyToSlate/index.ts +118 -0
  40. package/src/applyToSlate/textEvent.ts +280 -0
  41. package/src/applyToYjs/index.ts +28 -0
  42. package/src/applyToYjs/node/index.ts +17 -0
  43. package/src/applyToYjs/node/insertNode.ts +23 -0
  44. package/src/applyToYjs/node/mergeNode.ts +82 -0
  45. package/src/applyToYjs/node/moveNode.ts +57 -0
  46. package/src/applyToYjs/node/removeNode.ts +16 -0
  47. package/src/applyToYjs/node/setNode.ts +42 -0
  48. package/src/applyToYjs/node/splitNode.ts +98 -0
  49. package/src/applyToYjs/text/index.ts +9 -0
  50. package/src/applyToYjs/text/insertText.ts +27 -0
  51. package/src/applyToYjs/text/removeText.ts +16 -0
  52. package/src/applyToYjs/types.ts +12 -0
  53. package/src/index.ts +47 -0
  54. package/src/model/types.ts +50 -0
  55. package/src/plugins/index.ts +3 -0
  56. package/src/plugins/withCursors.ts +269 -0
  57. package/src/plugins/withYHistory.ts +183 -0
  58. package/src/plugins/withYjs.ts +284 -0
  59. package/src/utils/clone.ts +29 -0
  60. package/src/utils/convert.ts +48 -0
  61. package/src/utils/delta.ts +97 -0
  62. package/src/utils/errors.ts +20 -0
  63. package/src/utils/location.ts +157 -0
  64. package/src/utils/object.ts +93 -0
  65. package/src/utils/position.ts +300 -0
  66. package/src/utils/slate.ts +11 -0
  67. package/src/utils/yjs.ts +10 -0
  68. package/test/collaboration/addMark/acrossMarks.tsx +40 -0
  69. package/test/collaboration/addMark/acrossMarksSame.tsx +36 -0
  70. package/test/collaboration/addMark/atBeginningOfDocument.tsx +27 -0
  71. package/test/collaboration/addMark/atEndOfDocument.tsx +30 -0
  72. package/test/collaboration/addMark/withOtherMarks.tsx +31 -0
  73. package/test/collaboration/insertNode/atBeginningOfDocument.tsx +28 -0
  74. package/test/collaboration/insertNode/atEndOfDocument.tsx +28 -0
  75. package/test/collaboration/insertNode/inTheMiddle.tsx +30 -0
  76. package/test/collaboration/insertText/atBeginningOfBlock.tsx +26 -0
  77. package/test/collaboration/insertText/atBeginningOfDocument.tsx +26 -0
  78. package/test/collaboration/insertText/atEndOfBlock.tsx +26 -0
  79. package/test/collaboration/insertText/atEndOfDocument.tsx +26 -0
  80. package/test/collaboration/insertText/inTheMiddle.tsx +32 -0
  81. package/test/collaboration/insertText/inTheMiddleOfNestedBlock.tsx +30 -0
  82. package/test/collaboration/insertText/insideMarks.tsx +28 -0
  83. package/test/collaboration/insertText/withEmptyString.tsx +25 -0
  84. package/test/collaboration/insertText/withEntities.tsx +28 -0
  85. package/test/collaboration/insertText/withMarks.tsx +25 -0
  86. package/test/collaboration/insertText/withUnicode.tsx +28 -0
  87. package/test/collaboration/mergeNode/afterADeleteBackward.tsx +27 -0
  88. package/test/collaboration/mergeNode/inSameParent.tsx +34 -0
  89. package/test/collaboration/mergeNode/onMixedNestedNodes.tsx +29 -0
  90. package/test/collaboration/mergeNode/onMixedTypeNodes.tsx +27 -0
  91. package/test/collaboration/mergeNode/withUnicode.tsx +25 -0
  92. package/test/collaboration/moveNode/downward/whenBlockBecomesNested.tsx +43 -0
  93. package/test/collaboration/moveNode/downward/whenBlockBecomesNonNested.tsx +41 -0
  94. package/test/collaboration/moveNode/downward/whenBlockStaysNested.tsx +38 -0
  95. package/test/collaboration/moveNode/downward/whenBlockStaysNonNested.tsx +30 -0
  96. package/test/collaboration/moveNode/upward/whenBlockBecomesNested.tsx +43 -0
  97. package/test/collaboration/moveNode/upward/whenBlockBecomesNonNested.tsx +41 -0
  98. package/test/collaboration/moveNode/upward/whenBlockStaysNested.tsx +38 -0
  99. package/test/collaboration/moveNode/upward/whenBlockStaysNonNested.tsx +30 -0
  100. package/test/collaboration/removeMark/inTheMiddleOfText.tsx +43 -0
  101. package/test/collaboration/removeMark/withAddMark.tsx +31 -0
  102. package/test/collaboration/removeMark/withOtherMarks.tsx +47 -0
  103. package/test/collaboration/removeNode/atBeginningOfDocument.tsx +26 -0
  104. package/test/collaboration/removeNode/atEndOfDocument.tsx +26 -0
  105. package/test/collaboration/removeNode/nestedBlock.tsx +28 -0
  106. package/test/collaboration/removeNode/wrapperBlock.tsx +28 -0
  107. package/test/collaboration/removeText/atBeginningOfDocument.tsx +34 -0
  108. package/test/collaboration/removeText/atEndOfDocument.tsx +37 -0
  109. package/test/collaboration/removeText/withUnicode.tsx +29 -0
  110. package/test/collaboration/setNode/atBeginningOfDocument.tsx +27 -0
  111. package/test/collaboration/setNode/atEndOfDocument.tsx +27 -0
  112. package/test/collaboration/setNode/onDataChange.tsx +35 -0
  113. package/test/collaboration/setNode/onDataChangeOnInline.tsx +35 -0
  114. package/test/collaboration/setNode/onResetBlock.tsx +27 -0
  115. package/test/collaboration/setNode/withAChangeOfType.tsx +26 -0
  116. package/test/collaboration/splitNode/atBeginningOfDocument.tsx +28 -0
  117. package/test/collaboration/splitNode/atEndOfBlock.tsx +27 -0
  118. package/test/collaboration/splitNode/atEndOfDocument.tsx +27 -0
  119. package/test/collaboration/splitNode/onNonDefaultBlock.tsx +27 -0
  120. package/test/collaboration/splitNode/withMultipleSubNodes.tsx +32 -0
  121. package/test/collaboration/splitNode/withUnicode.tsx +29 -0
  122. package/test/index.test.ts +65 -0
  123. package/test/slate.d.ts +8 -0
  124. package/test/withTestingElements.ts +46 -0
  125. package/tsconfig.json +11 -0
  126. package/tsup.config.ts +32 -0
@@ -0,0 +1,284 @@
1
+ import { BaseEditor, Descendant, Editor, Operation, Point } from 'slate';
2
+ import * as Y from 'yjs';
3
+ import { applyYjsEvents } from '../applyToSlate';
4
+ import { applySlateOp } from '../applyToYjs';
5
+ import { yTextToSlateElement } from '../utils/convert';
6
+ import {
7
+ getStoredPosition,
8
+ getStoredPositions,
9
+ relativePositionToSlatePoint,
10
+ removeStoredPosition,
11
+ setStoredPosition,
12
+ slatePointToRelativePosition,
13
+ } from '../utils/position';
14
+ import { assertDocumentAttachment } from '../utils/yjs';
15
+
16
+ type LocalChange = {
17
+ op: Operation;
18
+ doc: Descendant[];
19
+ origin: unknown;
20
+ };
21
+
22
+ const DEFAULT_LOCAL_ORIGIN = Symbol('slate-yjs-operation');
23
+ const DEFAULT_POSITION_STORAGE_ORIGIN = Symbol('slate-yjs-position-storage');
24
+
25
+ const ORIGIN: WeakMap<Editor, unknown> = new WeakMap();
26
+ const LOCAL_CHANGES: WeakMap<Editor, LocalChange[]> = new WeakMap();
27
+ const CONNECTED: WeakSet<Editor> = new WeakSet();
28
+
29
+ export type YjsEditor = BaseEditor & {
30
+ sharedRoot: Y.XmlText;
31
+
32
+ localOrigin: unknown;
33
+ positionStorageOrigin: unknown;
34
+
35
+ applyRemoteEvents: (events: Y.YEvent<Y.XmlText>[], origin: unknown) => void;
36
+
37
+ storeLocalChange: (op: Operation) => void;
38
+ flushLocalChanges: () => void;
39
+
40
+ isLocalOrigin: (origin: unknown) => boolean;
41
+
42
+ connect: () => void;
43
+ disconnect: () => void;
44
+ };
45
+
46
+ export const YjsEditor = {
47
+ isYjsEditor(value: unknown): value is YjsEditor {
48
+ return (
49
+ Editor.isEditor(value) &&
50
+ (value as YjsEditor).sharedRoot instanceof Y.XmlText &&
51
+ 'localOrigin' in value &&
52
+ 'positionStorageOrigin' in value &&
53
+ typeof (value as YjsEditor).applyRemoteEvents === 'function' &&
54
+ typeof (value as YjsEditor).storeLocalChange === 'function' &&
55
+ typeof (value as YjsEditor).flushLocalChanges === 'function' &&
56
+ typeof (value as YjsEditor).isLocalOrigin === 'function' &&
57
+ typeof (value as YjsEditor).connect === 'function' &&
58
+ typeof (value as YjsEditor).disconnect === 'function'
59
+ );
60
+ },
61
+
62
+ localChanges(editor: YjsEditor): LocalChange[] {
63
+ return LOCAL_CHANGES.get(editor) ?? [];
64
+ },
65
+
66
+ applyRemoteEvents(
67
+ editor: YjsEditor,
68
+ events: Y.YEvent<Y.XmlText>[],
69
+ origin: unknown
70
+ ): void {
71
+ editor.applyRemoteEvents(events, origin);
72
+ },
73
+
74
+ storeLocalChange(editor: YjsEditor, op: Operation): void {
75
+ editor.storeLocalChange(op);
76
+ },
77
+
78
+ flushLocalChanges(editor: YjsEditor): void {
79
+ editor.flushLocalChanges();
80
+ },
81
+
82
+ connected(editor: YjsEditor): boolean {
83
+ return CONNECTED.has(editor);
84
+ },
85
+
86
+ connect(editor: YjsEditor): void {
87
+ editor.connect();
88
+ },
89
+
90
+ disconnect(editor: YjsEditor): void {
91
+ editor.disconnect();
92
+ },
93
+
94
+ isLocal(editor: YjsEditor): boolean {
95
+ return editor.isLocalOrigin(YjsEditor.origin(editor));
96
+ },
97
+
98
+ origin(editor: YjsEditor): unknown {
99
+ const origin = ORIGIN.get(editor);
100
+ return origin !== undefined ? origin : editor.localOrigin;
101
+ },
102
+
103
+ withOrigin(editor: YjsEditor, origin: unknown, fn: () => void): void {
104
+ const prev = YjsEditor.origin(editor);
105
+ ORIGIN.set(editor, origin);
106
+ fn();
107
+ ORIGIN.set(editor, prev);
108
+ },
109
+
110
+ storePosition(editor: YjsEditor, key: string, point: Point): void {
111
+ const { sharedRoot, positionStorageOrigin: locationStorageOrigin } = editor;
112
+ assertDocumentAttachment(sharedRoot);
113
+
114
+ const position = slatePointToRelativePosition(sharedRoot, editor, point);
115
+
116
+ sharedRoot.doc.transact(() => {
117
+ setStoredPosition(sharedRoot, key, position);
118
+ }, locationStorageOrigin);
119
+ },
120
+
121
+ removeStoredPosition(editor: YjsEditor, key: string): void {
122
+ const { sharedRoot, positionStorageOrigin: locationStorageOrigin } = editor;
123
+ assertDocumentAttachment(sharedRoot);
124
+
125
+ sharedRoot.doc.transact(() => {
126
+ removeStoredPosition(sharedRoot, key);
127
+ }, locationStorageOrigin);
128
+ },
129
+
130
+ position(editor: YjsEditor, key: string): Point | null | undefined {
131
+ const position = getStoredPosition(editor.sharedRoot, key);
132
+ if (!position) {
133
+ return undefined;
134
+ }
135
+
136
+ return relativePositionToSlatePoint(editor.sharedRoot, editor, position);
137
+ },
138
+
139
+ storedPositionsRelative(
140
+ editor: YjsEditor
141
+ ): Record<string, Y.RelativePosition> {
142
+ return getStoredPositions(editor.sharedRoot);
143
+ },
144
+ };
145
+
146
+ export type WithYjsOptions = {
147
+ autoConnect?: boolean;
148
+
149
+ // Origin used when applying local slate operations to yjs
150
+ localOrigin?: unknown;
151
+
152
+ // Origin used when storing positions
153
+ positionStorageOrigin?: unknown;
154
+ };
155
+
156
+ export function withYjs<T extends Editor>(
157
+ editor: T,
158
+ sharedRoot: Y.XmlText,
159
+ {
160
+ localOrigin,
161
+ positionStorageOrigin,
162
+ autoConnect = false,
163
+ }: WithYjsOptions = {}
164
+ ): T & YjsEditor {
165
+ const e = editor as T & YjsEditor;
166
+
167
+ e.sharedRoot = sharedRoot;
168
+
169
+ e.localOrigin = localOrigin ?? DEFAULT_LOCAL_ORIGIN;
170
+ e.positionStorageOrigin =
171
+ positionStorageOrigin ?? DEFAULT_POSITION_STORAGE_ORIGIN;
172
+
173
+ e.applyRemoteEvents = (events, origin) => {
174
+ YjsEditor.flushLocalChanges(e);
175
+
176
+ Editor.withoutNormalizing(e, () => {
177
+ YjsEditor.withOrigin(e, origin, () => {
178
+ applyYjsEvents(e.sharedRoot, e, events);
179
+ });
180
+ });
181
+ };
182
+
183
+ e.isLocalOrigin = (origin) => origin === e.localOrigin;
184
+
185
+ const handleYEvents = (
186
+ events: Y.YEvent<Y.XmlText>[],
187
+ transaction: Y.Transaction
188
+ ) => {
189
+ if (e.isLocalOrigin(transaction.origin)) {
190
+ return;
191
+ }
192
+
193
+ YjsEditor.applyRemoteEvents(e, events, transaction.origin);
194
+ };
195
+
196
+ let autoConnectTimeoutId: ReturnType<typeof setTimeout> | null = null;
197
+ if (autoConnect) {
198
+ autoConnectTimeoutId = setTimeout(() => {
199
+ autoConnectTimeoutId = null;
200
+ YjsEditor.connect(e);
201
+ });
202
+ }
203
+
204
+ e.connect = () => {
205
+ if (YjsEditor.connected(e)) {
206
+ throw new Error('already connected');
207
+ }
208
+
209
+ e.sharedRoot.observeDeep(handleYEvents);
210
+ const content = yTextToSlateElement(e.sharedRoot);
211
+ e.children = content.children;
212
+ CONNECTED.add(e);
213
+
214
+ Editor.normalize(editor, { force: true });
215
+ if (!editor.operations.length) {
216
+ editor.onChange();
217
+ }
218
+ };
219
+
220
+ e.disconnect = () => {
221
+ if (autoConnectTimeoutId) {
222
+ clearTimeout(autoConnectTimeoutId);
223
+ }
224
+
225
+ YjsEditor.flushLocalChanges(e);
226
+ e.sharedRoot.unobserveDeep(handleYEvents);
227
+ CONNECTED.delete(e);
228
+ };
229
+
230
+ e.storeLocalChange = (op) => {
231
+ LOCAL_CHANGES.set(e, [
232
+ ...YjsEditor.localChanges(e),
233
+ { op, doc: editor.children, origin: YjsEditor.origin(e) },
234
+ ]);
235
+ };
236
+
237
+ e.flushLocalChanges = () => {
238
+ assertDocumentAttachment(e.sharedRoot);
239
+ const localChanges = YjsEditor.localChanges(e);
240
+ LOCAL_CHANGES.delete(e);
241
+
242
+ // Group local changes by origin so we can apply them in the correct order
243
+ // with the correct origin with a minimal amount of transactions.
244
+ const txGroups: LocalChange[][] = [];
245
+ localChanges.forEach((change) => {
246
+ const currentGroup = txGroups[txGroups.length - 1];
247
+ if (currentGroup && currentGroup[0].origin === change.origin) {
248
+ return currentGroup.push(change);
249
+ }
250
+
251
+ txGroups.push([change]);
252
+ });
253
+
254
+ txGroups.forEach((txGroup) => {
255
+ assertDocumentAttachment(e.sharedRoot);
256
+
257
+ e.sharedRoot.doc.transact(() => {
258
+ txGroup.forEach((change) => {
259
+ assertDocumentAttachment(e.sharedRoot);
260
+ applySlateOp(e.sharedRoot, { children: change.doc }, change.op);
261
+ });
262
+ }, txGroup[0].origin);
263
+ });
264
+ };
265
+
266
+ const { apply, onChange } = e;
267
+ e.apply = (op) => {
268
+ if (YjsEditor.connected(e) && YjsEditor.isLocal(e)) {
269
+ YjsEditor.storeLocalChange(e, op);
270
+ }
271
+
272
+ apply(op);
273
+ };
274
+
275
+ e.onChange = () => {
276
+ if (YjsEditor.connected(e)) {
277
+ YjsEditor.flushLocalChanges(e);
278
+ }
279
+
280
+ onChange();
281
+ };
282
+
283
+ return e;
284
+ }
@@ -0,0 +1,29 @@
1
+ import * as Y from 'yjs';
2
+ import { InsertDelta } from '../model/types';
3
+ import { yTextToInsertDelta } from './delta';
4
+
5
+ export function cloneInsertDeltaDeep(delta: InsertDelta): InsertDelta {
6
+ return delta.map((element) => {
7
+ if (typeof element.insert === 'string') {
8
+ return element;
9
+ }
10
+
11
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
12
+ return { ...element, insert: cloneDeep(element.insert) };
13
+ });
14
+ }
15
+
16
+ export function cloneDeep(yText: Y.XmlText): Y.XmlText {
17
+ const clone = new Y.XmlText();
18
+
19
+ const attributes = yText.getAttributes();
20
+ Object.entries(attributes).forEach(([key, value]) => {
21
+ clone.setAttribute(key, value);
22
+ });
23
+
24
+ clone.applyDelta(cloneInsertDeltaDeep(yTextToInsertDelta(yText)), {
25
+ sanitize: false,
26
+ });
27
+
28
+ return clone;
29
+ }
@@ -0,0 +1,48 @@
1
+ import { Element, Node, Text } from 'slate';
2
+ import * as Y from 'yjs';
3
+ import { DeltaInsert, InsertDelta } from '../model/types';
4
+ import { yTextToInsertDelta } from './delta';
5
+ import { getProperties } from './slate';
6
+
7
+ export function yTextToSlateElement(yText: Y.XmlText): Element {
8
+ const delta = yTextToInsertDelta(yText);
9
+
10
+ const children =
11
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
12
+ delta.length > 0 ? delta.map(deltaInsertToSlateNode) : [{ text: '' }];
13
+
14
+ return { ...yText.getAttributes(), children };
15
+ }
16
+
17
+ export function deltaInsertToSlateNode(insert: DeltaInsert): Node {
18
+ if (typeof insert.insert === 'string') {
19
+ return { ...insert.attributes, text: insert.insert };
20
+ }
21
+
22
+ return yTextToSlateElement(insert.insert);
23
+ }
24
+
25
+ export function slateNodesToInsertDelta(nodes: Node[]): InsertDelta {
26
+ return nodes.map((node) => {
27
+ if (Text.isText(node)) {
28
+ return { insert: node.text, attributes: getProperties(node) };
29
+ }
30
+
31
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
32
+ return { insert: slateElementToYText(node) };
33
+ });
34
+ }
35
+
36
+ export function slateElementToYText({
37
+ children,
38
+ ...attributes
39
+ }: Element): Y.XmlText {
40
+ const yElement = new Y.XmlText();
41
+
42
+ Object.entries(attributes).forEach(([key, value]) => {
43
+ yElement.setAttribute(key, value);
44
+ });
45
+
46
+ yElement.applyDelta(slateNodesToInsertDelta(children), { sanitize: false });
47
+ return yElement;
48
+ }
@@ -0,0 +1,97 @@
1
+ import * as Y from 'yjs';
2
+ import { DeltaInsert, InsertDelta } from '../model/types';
3
+ import { deepEquals } from './object';
4
+
5
+ export function normalizeInsertDelta(delta: InsertDelta): InsertDelta {
6
+ const normalized: InsertDelta = [];
7
+
8
+ for (const element of delta) {
9
+ if (typeof element.insert === 'string' && element.insert.length === 0) {
10
+ continue;
11
+ }
12
+
13
+ const prev = normalized[normalized.length - 1];
14
+ if (
15
+ !prev ||
16
+ typeof prev.insert !== 'string' ||
17
+ typeof element.insert !== 'string'
18
+ ) {
19
+ normalized.push(element);
20
+ continue;
21
+ }
22
+
23
+ const merge =
24
+ prev.attributes === element.attributes ||
25
+ (!prev.attributes === !element.attributes &&
26
+ deepEquals(prev.attributes ?? {}, element.attributes ?? {}));
27
+
28
+ if (merge) {
29
+ prev.insert += element.insert;
30
+ continue;
31
+ }
32
+
33
+ normalized.push(element);
34
+ }
35
+
36
+ return normalized;
37
+ }
38
+
39
+ export function yTextToInsertDelta(yText: Y.XmlText): InsertDelta {
40
+ return normalizeInsertDelta(yText.toDelta());
41
+ }
42
+
43
+ export function getInsertLength({ insert }: DeltaInsert): number {
44
+ return typeof insert === 'string' ? insert.length : 1;
45
+ }
46
+
47
+ export function getInsertDeltaLength(delta: InsertDelta): number {
48
+ return delta.reduce((curr, element) => curr + getInsertLength(element), 0);
49
+ }
50
+
51
+ export function sliceInsertDelta(
52
+ delta: InsertDelta,
53
+ start: number,
54
+ length: number
55
+ ): InsertDelta {
56
+ if (length < 1) {
57
+ return [];
58
+ }
59
+
60
+ let currentOffset = 0;
61
+ const sliced: InsertDelta = [];
62
+ const end = start + length;
63
+
64
+ for (let i = 0; i < delta.length; i++) {
65
+ if (currentOffset >= end) {
66
+ break;
67
+ }
68
+
69
+ const element = delta[i];
70
+ const elementLength = getInsertLength(element);
71
+
72
+ if (currentOffset + elementLength <= start) {
73
+ currentOffset += elementLength;
74
+ continue;
75
+ }
76
+
77
+ if (typeof element.insert !== 'string') {
78
+ currentOffset += elementLength;
79
+ sliced.push(element);
80
+ continue;
81
+ }
82
+
83
+ const startOffset = Math.max(0, start - currentOffset);
84
+ const endOffset = Math.min(
85
+ elementLength,
86
+ elementLength - (currentOffset + elementLength - end)
87
+ );
88
+
89
+ sliced.push({
90
+ ...element,
91
+ insert: element.insert.slice(startOffset, endOffset),
92
+ });
93
+ currentOffset += elementLength;
94
+ }
95
+
96
+ return sliced;
97
+ }
@@ -0,0 +1,20 @@
1
+ const SYNC_SKEW_ERROR_PATTERNS = [
2
+ 'Cannot find a descendant at path',
3
+ 'yOffset out of bounds',
4
+ 'Cannot descent into slate text',
5
+ "yText isn't a descendant of root element",
6
+ "Path doesn't match yText",
7
+ 'Unexpected y parent type',
8
+ 'Path has to a have a length >= 1',
9
+ 'yTarget spans multiple nodes',
10
+ ];
11
+
12
+ export function isSyncSkewError(error: unknown): boolean {
13
+ if (!(error instanceof Error)) {
14
+ return false;
15
+ }
16
+
17
+ return SYNC_SKEW_ERROR_PATTERNS.some((pattern) =>
18
+ error.message.includes(pattern)
19
+ );
20
+ }
@@ -0,0 +1,157 @@
1
+ import { Element, Node, Path, Text } from 'slate';
2
+ import * as Y from 'yjs';
3
+ import { YTarget } from '../model/types';
4
+ import { sliceInsertDelta, yTextToInsertDelta } from './delta';
5
+
6
+ export function getSlateNodeYLength(node: Node | undefined): number {
7
+ if (!node) {
8
+ return 0;
9
+ }
10
+
11
+ return Text.isText(node) ? node.text.length : 1;
12
+ }
13
+
14
+ export function slatePathOffsetToYOffset(element: Element, pathOffset: number) {
15
+ return element.children
16
+ .slice(0, pathOffset)
17
+ .reduce((yOffset, node) => yOffset + getSlateNodeYLength(node), 0);
18
+ }
19
+
20
+ export function getYTarget(
21
+ yRoot: Y.XmlText,
22
+ slateRoot: Node,
23
+ path: Path
24
+ ): YTarget {
25
+ if (path.length === 0) {
26
+ throw new Error('Path has to a have a length >= 1');
27
+ }
28
+
29
+ if (Text.isText(slateRoot)) {
30
+ throw new Error('Cannot descent into slate text');
31
+ }
32
+
33
+ const [pathOffset, ...childPath] = path;
34
+
35
+ const yOffset = slatePathOffsetToYOffset(slateRoot, pathOffset);
36
+ const targetNode = slateRoot.children[pathOffset];
37
+
38
+ const delta = yTextToInsertDelta(yRoot);
39
+ const targetLength = getSlateNodeYLength(targetNode);
40
+
41
+ const targetDelta = sliceInsertDelta(delta, yOffset, targetLength);
42
+ if (targetDelta.length > 1) {
43
+ throw new Error("Path doesn't match yText, yTarget spans multiple nodes");
44
+ }
45
+
46
+ const yTarget = targetDelta[0]?.insert;
47
+ if (childPath.length > 0) {
48
+ if (!(yTarget instanceof Y.XmlText)) {
49
+ throw new Error(
50
+ "Path doesn't match yText, cannot descent into non-yText"
51
+ );
52
+ }
53
+
54
+ return getYTarget(yTarget, targetNode, childPath);
55
+ }
56
+
57
+ return {
58
+ yParent: yRoot,
59
+ textRange: { start: yOffset, end: yOffset + targetLength },
60
+ yTarget: yTarget instanceof Y.XmlText ? yTarget : undefined,
61
+ slateParent: slateRoot,
62
+ slateTarget: targetNode,
63
+ targetDelta,
64
+ };
65
+ }
66
+
67
+ export function yOffsetToSlateOffsets(
68
+ parent: Element,
69
+ yOffset: number,
70
+ opts: { assoc?: number; insert?: boolean } = {}
71
+ ): [number, number] {
72
+ const { assoc = 0, insert = false } = opts;
73
+
74
+ let currentOffset = 0;
75
+ let lastNonEmptyPathOffset = 0;
76
+ for (let pathOffset = 0; pathOffset < parent.children.length; pathOffset++) {
77
+ const child = parent.children[pathOffset];
78
+ const nodeLength = Text.isText(child) ? child.text.length : 1;
79
+
80
+ if (nodeLength > 0) {
81
+ lastNonEmptyPathOffset = pathOffset;
82
+ }
83
+
84
+ const endOffset = currentOffset + nodeLength;
85
+ if (
86
+ nodeLength > 0 &&
87
+ (assoc >= 0 ? endOffset > yOffset : endOffset >= yOffset)
88
+ ) {
89
+ return [pathOffset, yOffset - currentOffset];
90
+ }
91
+
92
+ currentOffset += nodeLength;
93
+ }
94
+
95
+ if (yOffset > currentOffset + (insert ? 1 : 0)) {
96
+ throw new Error('yOffset out of bounds');
97
+ }
98
+
99
+ if (insert) {
100
+ return [parent.children.length, 0];
101
+ }
102
+
103
+ const child = parent.children[lastNonEmptyPathOffset];
104
+ const textOffset = Text.isText(child) ? child.text.length : 1;
105
+ return [lastNonEmptyPathOffset, textOffset];
106
+ }
107
+
108
+ export function getSlatePath(
109
+ sharedRoot: Y.XmlText,
110
+ slateRoot: Node,
111
+ yText: Y.XmlText
112
+ ): Path {
113
+ const yNodePath = [yText];
114
+ while (yNodePath[0] !== sharedRoot) {
115
+ const { parent: yParent } = yNodePath[0];
116
+
117
+ if (!yParent) {
118
+ throw new Error("yText isn't a descendant of root element");
119
+ }
120
+
121
+ if (!(yParent instanceof Y.XmlText)) {
122
+ throw new Error('Unexpected y parent type');
123
+ }
124
+
125
+ yNodePath.unshift(yParent);
126
+ }
127
+
128
+ if (yNodePath.length < 2) {
129
+ return [];
130
+ }
131
+
132
+ let slateParent = slateRoot;
133
+ return yNodePath.reduce<Path>((path, yParent, idx) => {
134
+ const yChild = yNodePath[idx + 1];
135
+ if (!yChild) {
136
+ return path;
137
+ }
138
+
139
+ let yOffset = 0;
140
+ const currentDelta = yTextToInsertDelta(yParent);
141
+ for (const element of currentDelta) {
142
+ if (element.insert === yChild) {
143
+ break;
144
+ }
145
+
146
+ yOffset += typeof element.insert === 'string' ? element.insert.length : 1;
147
+ }
148
+
149
+ if (Text.isText(slateParent)) {
150
+ throw new Error('Cannot descent into slate text');
151
+ }
152
+
153
+ const [pathOffset] = yOffsetToSlateOffsets(slateParent, yOffset);
154
+ slateParent = slateParent.children[pathOffset];
155
+ return path.concat(pathOffset);
156
+ }, []);
157
+ }