@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,280 @@
1
+ import { Editor, Element, Node, Operation, Path, Text } from 'slate';
2
+ import * as Y from 'yjs';
3
+ import { Delta } from '../model/types';
4
+ import { deltaInsertToSlateNode } from '../utils/convert';
5
+ import {
6
+ getSlateNodeYLength,
7
+ getSlatePath,
8
+ yOffsetToSlateOffsets,
9
+ } from '../utils/location';
10
+ import { deepEquals, omitNullEntries, pick } from '../utils/object';
11
+ import { getProperties } from '../utils/slate';
12
+
13
+ function applyDelta(node: Element, slatePath: Path, delta: Delta): Operation[] {
14
+ const ops: Operation[] = [];
15
+
16
+ let yOffset = delta.reduce((length, change) => {
17
+ if ('retain' in change) {
18
+ return length + change.retain;
19
+ }
20
+
21
+ if ('delete' in change) {
22
+ return length + change.delete;
23
+ }
24
+
25
+ return length;
26
+ }, 0);
27
+
28
+ // Apply changes in reverse order to avoid path changes.
29
+ delta.reverse().forEach((change) => {
30
+ if ('attributes' in change && 'retain' in change) {
31
+ const [startPathOffset, startTextOffset] = yOffsetToSlateOffsets(
32
+ node,
33
+ yOffset - change.retain
34
+ );
35
+ const [endPathOffset, endTextOffset] = yOffsetToSlateOffsets(
36
+ node,
37
+ yOffset,
38
+ { assoc: -1 }
39
+ );
40
+
41
+ for (
42
+ let pathOffset = endPathOffset;
43
+ pathOffset >= startPathOffset;
44
+ pathOffset--
45
+ ) {
46
+ const child = node.children[pathOffset];
47
+ const childPath = [...slatePath, pathOffset];
48
+
49
+ if (!Text.isText(child)) {
50
+ // Ignore attribute updates on non-text nodes (which are backed by Y.XmlText)
51
+ // to be consistent with deltaInsertToSlateNode. Y.XmlText attributes don't show
52
+ // up in deltas but in key changes (YEvent.changes.keys).
53
+ continue;
54
+ }
55
+
56
+ const newProperties = change.attributes;
57
+ const properties = pick(
58
+ node,
59
+ ...(Object.keys(change.attributes) as Array<keyof Element>)
60
+ );
61
+
62
+ if (pathOffset === startPathOffset || pathOffset === endPathOffset) {
63
+ const start = pathOffset === startPathOffset ? startTextOffset : 0;
64
+ const end =
65
+ pathOffset === endPathOffset ? endTextOffset : child.text.length;
66
+
67
+ if (end !== child.text.length) {
68
+ ops.push({
69
+ type: 'split_node',
70
+ path: childPath,
71
+ position: end,
72
+ properties: getProperties(child),
73
+ });
74
+ }
75
+
76
+ if (start !== 0) {
77
+ ops.push({
78
+ type: 'split_node',
79
+ path: childPath,
80
+ position: start,
81
+ properties: omitNullEntries({
82
+ ...getProperties(child),
83
+ ...newProperties,
84
+ }),
85
+ });
86
+
87
+ continue;
88
+ }
89
+ }
90
+
91
+ ops.push({
92
+ type: 'set_node',
93
+ newProperties,
94
+ path: childPath,
95
+ properties,
96
+ });
97
+ }
98
+ }
99
+
100
+ if ('retain' in change) {
101
+ yOffset -= change.retain;
102
+ }
103
+
104
+ if ('delete' in change) {
105
+ const [startPathOffset, startTextOffset] = yOffsetToSlateOffsets(
106
+ node,
107
+ yOffset - change.delete
108
+ );
109
+ const [endPathOffset, endTextOffset] = yOffsetToSlateOffsets(
110
+ node,
111
+ yOffset,
112
+ { assoc: -1 }
113
+ );
114
+
115
+ for (
116
+ let pathOffset =
117
+ endTextOffset === 0 ? endPathOffset - 1 : endPathOffset;
118
+ pathOffset >= startPathOffset;
119
+ pathOffset--
120
+ ) {
121
+ const child = node.children[pathOffset];
122
+ const childPath = [...slatePath, pathOffset];
123
+
124
+ if (
125
+ Text.isText(child) &&
126
+ (pathOffset === startPathOffset || pathOffset === endPathOffset)
127
+ ) {
128
+ const start = pathOffset === startPathOffset ? startTextOffset : 0;
129
+ const end =
130
+ pathOffset === endPathOffset ? endTextOffset : child.text.length;
131
+
132
+ ops.push({
133
+ type: 'remove_text',
134
+ offset: start,
135
+ text: child.text.slice(start, end),
136
+ path: childPath,
137
+ });
138
+
139
+ yOffset -= end - start;
140
+ continue;
141
+ }
142
+
143
+ ops.push({
144
+ type: 'remove_node',
145
+ node: child,
146
+ path: childPath,
147
+ });
148
+ yOffset -= getSlateNodeYLength(child);
149
+ }
150
+
151
+ return;
152
+ }
153
+
154
+ if ('insert' in change) {
155
+ const [pathOffset, textOffset] = yOffsetToSlateOffsets(node, yOffset, {
156
+ insert: true,
157
+ });
158
+ const child = node.children[pathOffset];
159
+ const childPath = [...slatePath, pathOffset];
160
+
161
+ if (Text.isText(child)) {
162
+ const lastOp = ops[ops.length - 1];
163
+
164
+ /**
165
+ * The props that exist at the current path
166
+ * Since we're not actually using slate to update the node
167
+ * this is a simulation
168
+ */
169
+ const currentProps =
170
+ lastOp != null && lastOp.type === 'insert_node'
171
+ ? lastOp.node
172
+ : getProperties(child);
173
+
174
+ let lastPath: Path = [];
175
+
176
+ if (
177
+ lastOp != null &&
178
+ (lastOp.type === 'insert_node' ||
179
+ lastOp.type === 'insert_text' ||
180
+ lastOp.type === 'split_node' ||
181
+ lastOp.type === 'set_node')
182
+ ) {
183
+ lastPath = lastOp.path;
184
+ }
185
+
186
+ /**
187
+ * If the insert is a string and the attributes are the same as the
188
+ * props at the current path, we can just insert a text node
189
+ */
190
+ if (
191
+ typeof change.insert === 'string' &&
192
+ deepEquals(change.attributes ?? {}, currentProps) &&
193
+ Path.equals(childPath, lastPath)
194
+ ) {
195
+ return ops.push({
196
+ type: 'insert_text',
197
+ offset: textOffset,
198
+ text: change.insert,
199
+ path: childPath,
200
+ });
201
+ }
202
+
203
+ const toInsert = deltaInsertToSlateNode(change);
204
+ if (textOffset === 0) {
205
+ return ops.push({
206
+ type: 'insert_node',
207
+ path: childPath,
208
+ node: toInsert,
209
+ });
210
+ }
211
+
212
+ if (textOffset < child.text.length) {
213
+ ops.push({
214
+ type: 'split_node',
215
+ path: childPath,
216
+ position: textOffset,
217
+ properties: getProperties(child),
218
+ });
219
+ }
220
+
221
+ return ops.push({
222
+ type: 'insert_node',
223
+ path: Path.next(childPath),
224
+ node: toInsert,
225
+ });
226
+ }
227
+
228
+ return ops.push({
229
+ type: 'insert_node',
230
+ path: childPath,
231
+ node: deltaInsertToSlateNode(change),
232
+ });
233
+ }
234
+ });
235
+
236
+ return ops;
237
+ }
238
+
239
+ export function translateYTextEvent(
240
+ sharedRoot: Y.XmlText,
241
+ editor: Editor,
242
+ event: Y.YTextEvent
243
+ ): Operation[] {
244
+ const { target, changes } = event;
245
+ const delta = event.delta as Delta;
246
+
247
+ if (!(target instanceof Y.XmlText)) {
248
+ throw new Error('Unexpected target node type');
249
+ }
250
+
251
+ const ops: Operation[] = [];
252
+ const slatePath = getSlatePath(sharedRoot, editor, target);
253
+ const targetElement = Node.get(editor, slatePath);
254
+
255
+ if (Text.isText(targetElement)) {
256
+ throw new Error('Cannot apply yTextEvent to text node');
257
+ }
258
+
259
+ const keyChanges = Array.from(changes.keys.entries());
260
+ if (slatePath.length > 0 && keyChanges.length > 0) {
261
+ const newProperties = Object.fromEntries(
262
+ keyChanges.map(([key, info]) => [
263
+ key,
264
+ info.action === 'delete' ? null : target.getAttribute(key),
265
+ ])
266
+ );
267
+
268
+ const properties = Object.fromEntries(
269
+ keyChanges.map(([key]) => [key, targetElement[key]])
270
+ );
271
+
272
+ ops.push({ type: 'set_node', newProperties, properties, path: slatePath });
273
+ }
274
+
275
+ if (delta.length > 0) {
276
+ ops.push(...applyDelta(targetElement, slatePath, delta));
277
+ }
278
+
279
+ return ops;
280
+ }
@@ -0,0 +1,28 @@
1
+ import { Node, Operation } from 'slate';
2
+ import * as Y from 'yjs';
3
+ import { NODE_MAPPER } from './node';
4
+ import { TEXT_MAPPER } from './text';
5
+ import { ApplyFunc, OpMapper } from './types';
6
+
7
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
8
+ const NOOP = () => {};
9
+
10
+ const opMappers: OpMapper = {
11
+ ...TEXT_MAPPER,
12
+ ...NODE_MAPPER,
13
+
14
+ set_selection: NOOP,
15
+ };
16
+
17
+ export function applySlateOp(
18
+ sharedRoot: Y.XmlText,
19
+ slateRoot: Node,
20
+ op: Operation
21
+ ): void {
22
+ const apply = opMappers[op.type] as ApplyFunc<typeof op>;
23
+ if (!apply) {
24
+ throw new Error(`Unknown operation: ${op.type}`);
25
+ }
26
+
27
+ apply(sharedRoot, slateRoot, op);
28
+ }
@@ -0,0 +1,17 @@
1
+ import { NodeOperation } from 'slate';
2
+ import { OpMapper } from '../types';
3
+ import { insertNode } from './insertNode';
4
+ import { mergeNode } from './mergeNode';
5
+ import { moveNode } from './moveNode';
6
+ import { removeNode } from './removeNode';
7
+ import { setNode } from './setNode';
8
+ import { splitNode } from './splitNode';
9
+
10
+ export const NODE_MAPPER: OpMapper<NodeOperation> = {
11
+ insert_node: insertNode,
12
+ remove_node: removeNode,
13
+ set_node: setNode,
14
+ merge_node: mergeNode,
15
+ move_node: moveNode,
16
+ split_node: splitNode,
17
+ };
@@ -0,0 +1,23 @@
1
+ import { InsertNodeOperation, Node, Text } from 'slate';
2
+ import * as Y from 'yjs';
3
+ import { slateElementToYText } from '../../utils/convert';
4
+ import { getYTarget } from '../../utils/location';
5
+ import { getProperties } from '../../utils/slate';
6
+
7
+ export function insertNode(
8
+ sharedRoot: Y.XmlText,
9
+ slateRoot: Node,
10
+ op: InsertNodeOperation
11
+ ): void {
12
+ const { yParent, textRange } = getYTarget(sharedRoot, slateRoot, op.path);
13
+
14
+ if (Text.isText(op.node)) {
15
+ return yParent.insert(
16
+ textRange.start,
17
+ op.node.text,
18
+ getProperties(op.node)
19
+ );
20
+ }
21
+
22
+ yParent.insertEmbed(textRange.start, slateElementToYText(op.node));
23
+ }
@@ -0,0 +1,82 @@
1
+ import { MergeNodeOperation, Node, Path, Text } from 'slate';
2
+ import * as Y from 'yjs';
3
+ import { Delta } from '../../model/types';
4
+ import { cloneInsertDeltaDeep } from '../../utils/clone';
5
+ import { yTextToInsertDelta } from '../../utils/delta';
6
+ import { getYTarget } from '../../utils/location';
7
+ import {
8
+ getStoredPositionsInDeltaAbsolute,
9
+ restoreStoredPositionsWithDeltaAbsolute,
10
+ } from '../../utils/position';
11
+ import { getProperties } from '../../utils/slate';
12
+
13
+ export function mergeNode(
14
+ sharedRoot: Y.XmlText,
15
+ slateRoot: Node,
16
+ op: MergeNodeOperation
17
+ ): void {
18
+ const target = getYTarget(sharedRoot, slateRoot, op.path);
19
+ const prev = getYTarget(
20
+ target.yParent,
21
+ target.slateParent,
22
+ Path.previous(op.path.slice(-1))
23
+ );
24
+
25
+ if (!target.yTarget !== !prev.yTarget) {
26
+ throw new Error('Cannot merge y text with y element');
27
+ }
28
+
29
+ if (!prev.yTarget || !target.yTarget) {
30
+ const { yParent: parent, textRange, slateTarget } = target;
31
+ if (!slateTarget) {
32
+ throw new Error('Expected Slate target node for merge op.');
33
+ }
34
+
35
+ const prevSibling = Node.get(slateRoot, Path.previous(op.path));
36
+ if (!Text.isText(prevSibling)) {
37
+ throw new Error('Path points to Y.Text but not a Slate text node.');
38
+ }
39
+
40
+ const targetProps = getProperties(slateTarget);
41
+ const prevSiblingProps = getProperties(prevSibling);
42
+ const unsetProps = Object.keys(targetProps).reduce((acc, key) => {
43
+ const prevSiblingHasProp = key in prevSiblingProps;
44
+ return prevSiblingHasProp ? acc : { ...acc, [key]: null };
45
+ }, {});
46
+
47
+ return parent.format(textRange.start, textRange.end - textRange.start, {
48
+ ...unsetProps,
49
+ ...prevSiblingProps,
50
+ });
51
+ }
52
+
53
+ const deltaApplyYOffset = prev.yTarget.length;
54
+ const targetDelta = yTextToInsertDelta(target.yTarget);
55
+ const clonedDelta = cloneInsertDeltaDeep(targetDelta);
56
+
57
+ const storedPositions = getStoredPositionsInDeltaAbsolute(
58
+ sharedRoot,
59
+ target.yTarget,
60
+ targetDelta,
61
+ deltaApplyYOffset
62
+ );
63
+
64
+ const applyDelta: Delta = [{ retain: deltaApplyYOffset }, ...clonedDelta];
65
+
66
+ prev.yTarget.applyDelta(applyDelta, {
67
+ sanitize: false,
68
+ });
69
+
70
+ target.yParent.delete(
71
+ target.textRange.start,
72
+ target.textRange.end - target.textRange.start
73
+ );
74
+
75
+ restoreStoredPositionsWithDeltaAbsolute(
76
+ sharedRoot,
77
+ prev.yTarget,
78
+ storedPositions,
79
+ clonedDelta,
80
+ deltaApplyYOffset
81
+ );
82
+ }
@@ -0,0 +1,57 @@
1
+ import { MoveNodeOperation, Node, Path, Text } from 'slate';
2
+ import * as Y from 'yjs';
3
+ import { Delta } from '../../model/types';
4
+ import { cloneInsertDeltaDeep } from '../../utils/clone';
5
+ import { getInsertDeltaLength, yTextToInsertDelta } from '../../utils/delta';
6
+ import { getYTarget } from '../../utils/location';
7
+ import {
8
+ getStoredPositionsInDeltaAbsolute,
9
+ restoreStoredPositionsWithDeltaAbsolute,
10
+ } from '../../utils/position';
11
+
12
+ export function moveNode(
13
+ sharedRoot: Y.XmlText,
14
+ slateRoot: Node,
15
+ op: MoveNodeOperation
16
+ ): void {
17
+ const newParentPath = Path.parent(op.newPath);
18
+ const newPathOffset = op.newPath[op.newPath.length - 1];
19
+ const parent = Node.get(slateRoot, newParentPath);
20
+ if (Text.isText(parent)) {
21
+ throw new Error('Cannot move slate node into text element');
22
+ }
23
+ const normalizedNewPath = [
24
+ ...newParentPath,
25
+ Math.min(newPathOffset, parent.children.length),
26
+ ];
27
+
28
+ const origin = getYTarget(sharedRoot, slateRoot, op.path);
29
+ const target = getYTarget(sharedRoot, slateRoot, normalizedNewPath);
30
+ const insertDelta = cloneInsertDeltaDeep(origin.targetDelta);
31
+
32
+ const storedPositions = getStoredPositionsInDeltaAbsolute(
33
+ sharedRoot,
34
+ origin.yParent,
35
+ origin.targetDelta
36
+ );
37
+
38
+ origin.yParent.delete(
39
+ origin.textRange.start,
40
+ origin.textRange.end - origin.textRange.start
41
+ );
42
+
43
+ const targetLength = getInsertDeltaLength(yTextToInsertDelta(target.yParent));
44
+ const deltaApplyYOffset = Math.min(target.textRange.start, targetLength);
45
+ const applyDelta: Delta = [{ retain: deltaApplyYOffset }, ...insertDelta];
46
+
47
+ target.yParent.applyDelta(applyDelta, { sanitize: false });
48
+
49
+ restoreStoredPositionsWithDeltaAbsolute(
50
+ sharedRoot,
51
+ target.yParent,
52
+ storedPositions,
53
+ insertDelta,
54
+ deltaApplyYOffset,
55
+ origin.textRange.start
56
+ );
57
+ }
@@ -0,0 +1,16 @@
1
+ import { Node, RemoveNodeOperation } from 'slate';
2
+ import * as Y from 'yjs';
3
+ import { getYTarget } from '../../utils/location';
4
+
5
+ export function removeNode(
6
+ sharedRoot: Y.XmlText,
7
+ slateRoot: Node,
8
+ op: RemoveNodeOperation
9
+ ): void {
10
+ const { yParent: parent, textRange } = getYTarget(
11
+ sharedRoot,
12
+ slateRoot,
13
+ op.path
14
+ );
15
+ parent.delete(textRange.start, textRange.end - textRange.start);
16
+ }
@@ -0,0 +1,42 @@
1
+ import { Node, SetNodeOperation } from 'slate';
2
+ import * as Y from 'yjs';
3
+ import { getYTarget } from '../../utils/location';
4
+
5
+ export function setNode(
6
+ sharedRoot: Y.XmlText,
7
+ slateRoot: Node,
8
+ op: SetNodeOperation
9
+ ): void {
10
+ const { yTarget, textRange, yParent } = getYTarget(
11
+ sharedRoot,
12
+ slateRoot,
13
+ op.path
14
+ );
15
+
16
+ if (yTarget) {
17
+ Object.entries(op.newProperties).forEach(([key, value]) => {
18
+ if (value === null) {
19
+ return yTarget.removeAttribute(key);
20
+ }
21
+
22
+ yTarget.setAttribute(key, value);
23
+ });
24
+
25
+ return Object.entries(op.properties).forEach(([key]) => {
26
+ if (!op.newProperties.hasOwnProperty(key)) {
27
+ yTarget.removeAttribute(key);
28
+ }
29
+ });
30
+ }
31
+
32
+ const unset = Object.fromEntries(
33
+ Object.keys(op.properties).map((key) => [key, null])
34
+ );
35
+ const newProperties = { ...unset, ...op.newProperties };
36
+
37
+ yParent.format(
38
+ textRange.start,
39
+ textRange.end - textRange.start,
40
+ newProperties
41
+ );
42
+ }
@@ -0,0 +1,98 @@
1
+ import { Node, SplitNodeOperation, Text } from 'slate';
2
+ import * as Y from 'yjs';
3
+ import { cloneInsertDeltaDeep } from '../../utils/clone';
4
+ import { sliceInsertDelta, yTextToInsertDelta } from '../../utils/delta';
5
+ import { getSlateNodeYLength, getYTarget } from '../../utils/location';
6
+ import {
7
+ getStoredPositionsInDeltaAbsolute,
8
+ restoreStoredPositionsWithDeltaAbsolute,
9
+ } from '../../utils/position';
10
+
11
+ export function splitNode(
12
+ sharedRoot: Y.XmlText,
13
+ slateRoot: Node,
14
+ op: SplitNodeOperation
15
+ ): void {
16
+ const target = getYTarget(sharedRoot, slateRoot, op.path);
17
+
18
+ if (!target.slateTarget) {
19
+ throw new Error('Y target without corresponding slate node');
20
+ }
21
+
22
+ if (!target.yTarget) {
23
+ if (!Text.isText(target.slateTarget)) {
24
+ throw new Error('Mismatch node type between y target and slate node');
25
+ }
26
+
27
+ const unset: Record<string, null> = {};
28
+ target.targetDelta.forEach((element) => {
29
+ if (element.attributes) {
30
+ Object.keys(element.attributes).forEach((key) => {
31
+ unset[key] = null;
32
+ });
33
+ }
34
+ });
35
+
36
+ return target.yParent.format(
37
+ target.textRange.start,
38
+ target.textRange.end - target.textRange.start,
39
+ { ...unset, ...op.properties }
40
+ );
41
+ }
42
+
43
+ if (Text.isText(target.slateTarget)) {
44
+ throw new Error('Mismatch node type between y target and slate node');
45
+ }
46
+
47
+ const splitTarget = getYTarget(target.yTarget, target.slateTarget, [
48
+ op.position,
49
+ ]);
50
+
51
+ const ySplitOffset = target.slateTarget.children
52
+ .slice(0, op.position)
53
+ .reduce((length, child) => length + getSlateNodeYLength(child), 0);
54
+
55
+ const length = target.slateTarget.children.reduce(
56
+ (current, child) => current + getSlateNodeYLength(child),
57
+ 0
58
+ );
59
+
60
+ const splitDelta = sliceInsertDelta(
61
+ yTextToInsertDelta(target.yTarget),
62
+ ySplitOffset,
63
+ length - ySplitOffset
64
+ );
65
+ const clonedDelta = cloneInsertDeltaDeep(splitDelta);
66
+
67
+ const storedPositions = getStoredPositionsInDeltaAbsolute(
68
+ sharedRoot,
69
+ target.yTarget,
70
+ splitDelta,
71
+ ySplitOffset
72
+ );
73
+
74
+ const toInsert = new Y.XmlText();
75
+ toInsert.applyDelta(clonedDelta, {
76
+ sanitize: false,
77
+ });
78
+
79
+ Object.entries(op.properties).forEach(([key, value]) => {
80
+ toInsert.setAttribute(key, value);
81
+ });
82
+
83
+ target.yTarget.delete(
84
+ splitTarget.textRange.start,
85
+ target.yTarget.length - splitTarget.textRange.start
86
+ );
87
+
88
+ target.yParent.insertEmbed(target.textRange.end, toInsert);
89
+
90
+ restoreStoredPositionsWithDeltaAbsolute(
91
+ sharedRoot,
92
+ toInsert,
93
+ storedPositions,
94
+ clonedDelta,
95
+ 0,
96
+ ySplitOffset
97
+ );
98
+ }
@@ -0,0 +1,9 @@
1
+ import { TextOperation } from 'slate';
2
+ import { OpMapper } from '../types';
3
+ import { insertText } from './insertText';
4
+ import { removeText } from './removeText';
5
+
6
+ export const TEXT_MAPPER: OpMapper<TextOperation> = {
7
+ insert_text: insertText,
8
+ remove_text: removeText,
9
+ };
@@ -0,0 +1,27 @@
1
+ import { InsertTextOperation, Node, Text } from 'slate';
2
+ import type Y from 'yjs';
3
+ import { getYTarget } from '../../utils/location';
4
+ import { getProperties } from '../../utils/slate';
5
+
6
+ export function insertText(
7
+ sharedRoot: Y.XmlText,
8
+ slateRoot: Node,
9
+ op: InsertTextOperation
10
+ ): void {
11
+ const { yParent: target, textRange } = getYTarget(
12
+ sharedRoot,
13
+ slateRoot,
14
+ op.path
15
+ );
16
+
17
+ const targetNode = Node.get(slateRoot, op.path);
18
+ if (!Text.isText(targetNode)) {
19
+ throw new Error('Cannot insert text into non-text node');
20
+ }
21
+
22
+ target.insert(
23
+ textRange.start + op.offset,
24
+ op.text,
25
+ getProperties(targetNode)
26
+ );
27
+ }
@@ -0,0 +1,16 @@
1
+ import { Node, RemoveTextOperation } from 'slate';
2
+ import type Y from 'yjs';
3
+ import { getYTarget } from '../../utils/location';
4
+
5
+ export function removeText(
6
+ sharedRoot: Y.XmlText,
7
+ slateRoot: Node,
8
+ op: RemoveTextOperation
9
+ ): void {
10
+ const { yParent: target, textRange } = getYTarget(
11
+ sharedRoot,
12
+ slateRoot,
13
+ op.path
14
+ );
15
+ target.delete(textRange.start + op.offset, op.text.length);
16
+ }