@ckeditor/ckeditor5-engine 35.0.1 → 35.1.0

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 (124) hide show
  1. package/CHANGELOG.md +4 -4
  2. package/package.json +30 -24
  3. package/src/controller/datacontroller.js +467 -561
  4. package/src/controller/editingcontroller.js +168 -204
  5. package/src/conversion/conversion.js +541 -565
  6. package/src/conversion/conversionhelpers.js +24 -28
  7. package/src/conversion/downcastdispatcher.js +457 -686
  8. package/src/conversion/downcasthelpers.js +1583 -1965
  9. package/src/conversion/mapper.js +518 -707
  10. package/src/conversion/modelconsumable.js +240 -283
  11. package/src/conversion/upcastdispatcher.js +372 -718
  12. package/src/conversion/upcasthelpers.js +707 -818
  13. package/src/conversion/viewconsumable.js +524 -581
  14. package/src/dataprocessor/basichtmlwriter.js +12 -16
  15. package/src/dataprocessor/dataprocessor.js +5 -0
  16. package/src/dataprocessor/htmldataprocessor.js +100 -116
  17. package/src/dataprocessor/htmlwriter.js +1 -18
  18. package/src/dataprocessor/xmldataprocessor.js +116 -137
  19. package/src/dev-utils/model.js +260 -352
  20. package/src/dev-utils/operationreplayer.js +106 -126
  21. package/src/dev-utils/utils.js +34 -51
  22. package/src/dev-utils/view.js +632 -753
  23. package/src/index.js +0 -11
  24. package/src/model/batch.js +111 -127
  25. package/src/model/differ.js +988 -1233
  26. package/src/model/document.js +340 -449
  27. package/src/model/documentfragment.js +327 -364
  28. package/src/model/documentselection.js +996 -1189
  29. package/src/model/element.js +306 -410
  30. package/src/model/history.js +224 -262
  31. package/src/model/item.js +5 -0
  32. package/src/model/liveposition.js +84 -145
  33. package/src/model/liverange.js +108 -185
  34. package/src/model/markercollection.js +379 -480
  35. package/src/model/model.js +883 -1034
  36. package/src/model/node.js +419 -463
  37. package/src/model/nodelist.js +175 -201
  38. package/src/model/operation/attributeoperation.js +153 -182
  39. package/src/model/operation/detachoperation.js +64 -83
  40. package/src/model/operation/insertoperation.js +135 -166
  41. package/src/model/operation/markeroperation.js +114 -140
  42. package/src/model/operation/mergeoperation.js +163 -191
  43. package/src/model/operation/moveoperation.js +157 -187
  44. package/src/model/operation/nooperation.js +28 -38
  45. package/src/model/operation/operation.js +106 -125
  46. package/src/model/operation/operationfactory.js +30 -34
  47. package/src/model/operation/renameoperation.js +109 -135
  48. package/src/model/operation/rootattributeoperation.js +155 -188
  49. package/src/model/operation/splitoperation.js +196 -232
  50. package/src/model/operation/transform.js +1833 -2204
  51. package/src/model/operation/utils.js +140 -204
  52. package/src/model/position.js +899 -1053
  53. package/src/model/range.js +910 -1028
  54. package/src/model/rootelement.js +77 -97
  55. package/src/model/schema.js +1189 -1835
  56. package/src/model/selection.js +745 -862
  57. package/src/model/text.js +90 -114
  58. package/src/model/textproxy.js +204 -240
  59. package/src/model/treewalker.js +316 -397
  60. package/src/model/typecheckable.js +16 -0
  61. package/src/model/utils/autoparagraphing.js +32 -44
  62. package/src/model/utils/deletecontent.js +334 -418
  63. package/src/model/utils/findoptimalinsertionrange.js +25 -36
  64. package/src/model/utils/getselectedcontent.js +96 -118
  65. package/src/model/utils/insertcontent.js +654 -773
  66. package/src/model/utils/insertobject.js +96 -119
  67. package/src/model/utils/modifyselection.js +120 -158
  68. package/src/model/utils/selection-post-fixer.js +153 -201
  69. package/src/model/writer.js +1305 -1474
  70. package/src/view/attributeelement.js +189 -225
  71. package/src/view/containerelement.js +75 -85
  72. package/src/view/document.js +172 -215
  73. package/src/view/documentfragment.js +200 -249
  74. package/src/view/documentselection.js +338 -367
  75. package/src/view/domconverter.js +1370 -1617
  76. package/src/view/downcastwriter.js +1747 -2076
  77. package/src/view/editableelement.js +81 -97
  78. package/src/view/element.js +739 -890
  79. package/src/view/elementdefinition.js +5 -0
  80. package/src/view/emptyelement.js +82 -92
  81. package/src/view/filler.js +35 -50
  82. package/src/view/item.js +5 -0
  83. package/src/view/matcher.js +260 -559
  84. package/src/view/node.js +274 -360
  85. package/src/view/observer/arrowkeysobserver.js +19 -28
  86. package/src/view/observer/bubblingemittermixin.js +120 -263
  87. package/src/view/observer/bubblingeventinfo.js +47 -55
  88. package/src/view/observer/clickobserver.js +7 -13
  89. package/src/view/observer/compositionobserver.js +14 -24
  90. package/src/view/observer/domeventdata.js +57 -67
  91. package/src/view/observer/domeventobserver.js +40 -64
  92. package/src/view/observer/fakeselectionobserver.js +81 -96
  93. package/src/view/observer/focusobserver.js +45 -61
  94. package/src/view/observer/inputobserver.js +7 -13
  95. package/src/view/observer/keyobserver.js +17 -27
  96. package/src/view/observer/mouseobserver.js +7 -14
  97. package/src/view/observer/mutationobserver.js +220 -315
  98. package/src/view/observer/observer.js +81 -102
  99. package/src/view/observer/selectionobserver.js +191 -246
  100. package/src/view/observer/tabobserver.js +23 -36
  101. package/src/view/placeholder.js +128 -173
  102. package/src/view/position.js +350 -401
  103. package/src/view/range.js +453 -513
  104. package/src/view/rawelement.js +85 -112
  105. package/src/view/renderer.js +874 -1018
  106. package/src/view/rooteditableelement.js +80 -90
  107. package/src/view/selection.js +608 -689
  108. package/src/view/styles/background.js +43 -44
  109. package/src/view/styles/border.js +220 -276
  110. package/src/view/styles/margin.js +8 -17
  111. package/src/view/styles/padding.js +8 -16
  112. package/src/view/styles/utils.js +127 -160
  113. package/src/view/stylesmap.js +728 -905
  114. package/src/view/text.js +102 -126
  115. package/src/view/textproxy.js +144 -170
  116. package/src/view/treewalker.js +383 -479
  117. package/src/view/typecheckable.js +19 -0
  118. package/src/view/uielement.js +166 -187
  119. package/src/view/upcastwriter.js +395 -449
  120. package/src/view/view.js +569 -664
  121. package/src/dataprocessor/dataprocessor.jsdoc +0 -64
  122. package/src/model/item.jsdoc +0 -14
  123. package/src/view/elementdefinition.jsdoc +0 -59
  124. package/src/view/item.jsdoc +0 -14
@@ -2,7 +2,6 @@
2
2
  * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
-
6
5
  import InsertOperation from './insertoperation';
7
6
  import AttributeOperation from './attributeoperation';
8
7
  import RenameOperation from './renameoperation';
@@ -14,15 +13,11 @@ import SplitOperation from './splitoperation';
14
13
  import NoOperation from './nooperation';
15
14
  import Range from '../range';
16
15
  import Position from '../position';
17
-
18
16
  import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
19
-
20
17
  const transformations = new Map();
21
-
22
18
  /**
23
19
  * @module engine/model/operation/transform
24
20
  */
25
-
26
21
  /**
27
22
  * Sets a transformation function to be be used to transform instances of class `OperationA` by instances of class `OperationB`.
28
23
  *
@@ -41,17 +36,14 @@ const transformations = new Map();
41
36
  * @param {Function} OperationB
42
37
  * @param {Function} transformationFunction Function to use for transforming.
43
38
  */
44
- function setTransformation( OperationA, OperationB, transformationFunction ) {
45
- let aGroup = transformations.get( OperationA );
46
-
47
- if ( !aGroup ) {
48
- aGroup = new Map();
49
- transformations.set( OperationA, aGroup );
50
- }
51
-
52
- aGroup.set( OperationB, transformationFunction );
39
+ function setTransformation(OperationA, OperationB, transformationFunction) {
40
+ let aGroup = transformations.get(OperationA);
41
+ if (!aGroup) {
42
+ aGroup = new Map();
43
+ transformations.set(OperationA, aGroup);
44
+ }
45
+ aGroup.set(OperationB, transformationFunction);
53
46
  }
54
-
55
47
  /**
56
48
  * Returns a previously set transformation function for transforming an instance of `OperationA` by an instance of `OperationB`.
57
49
  *
@@ -64,16 +56,13 @@ function setTransformation( OperationA, OperationB, transformationFunction ) {
64
56
  * @param {Function} OperationB
65
57
  * @returns {Function} Function set to transform an instance of `OperationA` by an instance of `OperationB`.
66
58
  */
67
- function getTransformation( OperationA, OperationB ) {
68
- const aGroup = transformations.get( OperationA );
69
-
70
- if ( aGroup && aGroup.has( OperationB ) ) {
71
- return aGroup.get( OperationB );
72
- }
73
-
74
- return noUpdateTransformation;
59
+ function getTransformation(OperationA, OperationB) {
60
+ const aGroup = transformations.get(OperationA);
61
+ if (aGroup && aGroup.has(OperationB)) {
62
+ return aGroup.get(OperationB);
63
+ }
64
+ return noUpdateTransformation;
75
65
  }
76
-
77
66
  /**
78
67
  * A transformation function that only clones operation to transform, without changing it.
79
68
  *
@@ -81,41 +70,37 @@ function getTransformation( OperationA, OperationB ) {
81
70
  * @param {module:engine/model/operation/operation~Operation} a Operation to transform.
82
71
  * @returns {Array.<module:engine/model/operation/operation~Operation>}
83
72
  */
84
- function noUpdateTransformation( a ) {
85
- return [ a ];
73
+ function noUpdateTransformation(a) {
74
+ return [a];
86
75
  }
87
-
88
76
  /**
89
77
  * Transforms operation `a` by operation `b`.
90
78
  *
91
79
  * @param {module:engine/model/operation/operation~Operation} a Operation to be transformed.
92
80
  * @param {module:engine/model/operation/operation~Operation} b Operation to transform by.
93
- * @param {module:engine/model/operation/transform~TransformationContext} context Transformation context for this transformation.
81
+ * @param {module:engine/model/operation/transform~TransformationContext} [context] Transformation context for this transformation.
94
82
  * @returns {Array.<module:engine/model/operation/operation~Operation>} Transformation result.
95
83
  */
96
- export function transform( a, b, context = {} ) {
97
- const transformationFunction = getTransformation( a.constructor, b.constructor );
98
-
99
- /* eslint-disable no-useless-catch */
100
- try {
101
- a = a.clone();
102
-
103
- return transformationFunction( a, b, context );
104
- } catch ( e ) {
105
- // @if CK_DEBUG // console.warn( 'Error during operation transformation!', e.message );
106
- // @if CK_DEBUG // console.warn( 'Transformed operation', a );
107
- // @if CK_DEBUG // console.warn( 'Operation transformed by', b );
108
- // @if CK_DEBUG // console.warn( 'context.aIsStrong', context.aIsStrong );
109
- // @if CK_DEBUG // console.warn( 'context.aWasUndone', context.aWasUndone );
110
- // @if CK_DEBUG // console.warn( 'context.bWasUndone', context.bWasUndone );
111
- // @if CK_DEBUG // console.warn( 'context.abRelation', context.abRelation );
112
- // @if CK_DEBUG // console.warn( 'context.baRelation', context.baRelation );
113
-
114
- throw e;
115
- }
116
- /* eslint-enable no-useless-catch */
84
+ export function transform(a, b, context = {}) {
85
+ const transformationFunction = getTransformation(a.constructor, b.constructor);
86
+ /* eslint-disable no-useless-catch */
87
+ try {
88
+ a = a.clone();
89
+ return transformationFunction(a, b, context);
90
+ }
91
+ catch (e) {
92
+ // @if CK_DEBUG // console.warn( 'Error during operation transformation!', e.message );
93
+ // @if CK_DEBUG // console.warn( 'Transformed operation', a );
94
+ // @if CK_DEBUG // console.warn( 'Operation transformed by', b );
95
+ // @if CK_DEBUG // console.warn( 'context.aIsStrong', context.aIsStrong );
96
+ // @if CK_DEBUG // console.warn( 'context.aWasUndone', context.aWasUndone );
97
+ // @if CK_DEBUG // console.warn( 'context.bWasUndone', context.bWasUndone );
98
+ // @if CK_DEBUG // console.warn( 'context.abRelation', context.abRelation );
99
+ // @if CK_DEBUG // console.warn( 'context.baRelation', context.baRelation );
100
+ throw e;
101
+ }
102
+ /* eslint-enable no-useless-catch */
117
103
  }
118
-
119
104
  /**
120
105
  * Performs a transformation of two sets of operations - `operationsA` and `operationsB`. The transformation is two-way -
121
106
  * both transformed `operationsA` and transformed `operationsB` are returned.
@@ -141,569 +126,474 @@ export function transform( a, b, context = {} ) {
141
126
  * @param {Array.<module:engine/model/operation/operation~Operation>} operationsA
142
127
  * @param {Array.<module:engine/model/operation/operation~Operation>} operationsB
143
128
  * @param {Object} options Additional transformation options.
144
- * @param {module:engine/model/document~Document|null} options.document Document which the operations change.
129
+ * @param {module:engine/model/document~Document} options.document Document which the operations change.
145
130
  * @param {Boolean} [options.useRelations=false] Whether during transformation relations should be used (used during undo for
146
131
  * better conflict resolution).
147
132
  * @param {Boolean} [options.padWithNoOps=false] Whether additional {@link module:engine/model/operation/nooperation~NoOperation}s
148
133
  * should be added to the transformation results to force the same last base version for both transformed sets (in case
149
134
  * if some operations got broken into multiple operations during transformation).
135
+ * @param {Boolean} [options.forceWeakRemove] If set to `false`, remove operation will be always stronger than move operation,
136
+ * so the removed nodes won't end up back in the document root. When set to `true`, context data will be used.
150
137
  * @returns {Object} Transformation result.
151
138
  * @returns {Array.<module:engine/model/operation/operation~Operation>} return.operationsA Transformed `operationsA`.
152
139
  * @returns {Array.<module:engine/model/operation/operation~Operation>} return.operationsB Transformed `operationsB`.
153
140
  * @returns {Map} return.originalOperations A map that links transformed operations to original operations. The keys are the transformed
154
141
  * operations and the values are the original operations from the input (`operationsA` and `operationsB`).
155
142
  */
156
- export function transformSets( operationsA, operationsB, options ) {
157
- // Create new arrays so the originally passed arguments are not changed.
158
- // No need to clone operations, they are cloned as they are transformed.
159
- operationsA = operationsA.slice();
160
- operationsB = operationsB.slice();
161
-
162
- const contextFactory = new ContextFactory( options.document, options.useRelations, options.forceWeakRemove );
163
- contextFactory.setOriginalOperations( operationsA );
164
- contextFactory.setOriginalOperations( operationsB );
165
-
166
- const originalOperations = contextFactory.originalOperations;
167
-
168
- // If one of sets is empty there is simply nothing to transform, so return sets as they are.
169
- if ( operationsA.length == 0 || operationsB.length == 0 ) {
170
- return { operationsA, operationsB, originalOperations };
171
- }
172
- //
173
- // Following is a description of transformation process:
174
- //
175
- // There are `operationsA` and `operationsB` to be transformed, both by both.
176
- //
177
- // So, suppose we have sets of two operations each: `operationsA` = `[ a1, a2 ]`, `operationsB` = `[ b1, b2 ]`.
178
- //
179
- // Remember, that we can only transform operations that base on the same context. We assert that `a1` and `b1` base on
180
- // the same context and we transform them. Then, we get `a1'` and `b1'`. `a2` bases on a context with `a1` -- `a2`
181
- // is an operation that followed `a1`. Similarly, `b2` bases on a context with `b1`.
182
- //
183
- // However, since `a1'` is a result of transformation by `b1`, `a1'` now also has a context with `b1`. This means that
184
- // we can safely transform `a1'` by `b2`. As we finish transforming `a1`, we also transformed all `operationsB`.
185
- // All `operationsB` also have context including `a1`. Now, we can properly transform `a2` by those operations.
186
- //
187
- // The transformation process can be visualized on a transformation diagram ("diamond diagram"):
188
- //
189
- // [the initial state]
190
- // [common for a1 and b1]
191
- //
192
- // *
193
- // / \
194
- // / \
195
- // b1 a1
196
- // / \
197
- // / \
198
- // * *
199
- // / \ / \
200
- // / \ / \
201
- // b2 a1' b1' a2
202
- // / \ / \
203
- // / \ / \
204
- // * * *
205
- // \ / \ /
206
- // \ / \ /
207
- // a1'' b2' a2' b1''
208
- // \ / \ /
209
- // \ / \ /
210
- // * *
211
- // \ /
212
- // \ /
213
- // a2'' b2''
214
- // \ /
215
- // \ /
216
- // *
217
- //
218
- // [the final state]
219
- //
220
- // The final state can be reached from the initial state by applying `a1`, `a2`, `b1''` and `b2''`, as well as by
221
- // applying `b1`, `b2`, `a1''`, `a2''`. Note how the operations get to a proper common state before each pair is
222
- // transformed.
223
- //
224
- // Another thing to consider is that an operation during transformation can be broken into multiple operations.
225
- // Suppose that `a1` * `b1` = `[ a11', a12' ]` (instead of `a1'` that we considered previously).
226
- //
227
- // In that case, we leave `a12'` for later and we continue transforming `a11'` until it is transformed by all `operationsB`
228
- // (in our case it is just `b2`). At this point, `b1` is transformed by "whole" `a1`, while `b2` is only transformed
229
- // by `a11'`. Similarly, `a12'` is only transformed by `b1`. This leads to a conclusion that we need to start transforming `a12'`
230
- // from the moment just after it was broken. So, `a12'` is transformed by `b2`. Now, "the whole" `a1` is transformed
231
- // by `operationsB`, while all `operationsB` are transformed by "the whole" `a1`. This means that we can continue with
232
- // following `operationsA` (in our case it is just `a2`).
233
- //
234
- // Of course, also `operationsB` can be broken. However, since we focus on transforming operation `a` to the end,
235
- // the only thing to do is to store both pieces of operation `b`, so that the next transformed operation `a` will
236
- // be transformed by both of them.
237
- //
238
- // *
239
- // / \
240
- // / \
241
- // / \
242
- // b1 a1
243
- // / \
244
- // / \
245
- // / \
246
- // * *
247
- // / \ / \
248
- // / a11' / \
249
- // / \ / \
250
- // b2 * b1' a2
251
- // / / \ / \
252
- // / / a12' / \
253
- // / / \ / \
254
- // * b2' * *
255
- // \ / / \ /
256
- // a11'' / b21'' \ /
257
- // \ / / \ /
258
- // * * a2' b1''
259
- // \ / \ \ /
260
- // a12'' b22''\ \ /
261
- // \ / \ \ /
262
- // * a2'' *
263
- // \ \ /
264
- // \ \ b21'''
265
- // \ \ /
266
- // a2''' *
267
- // \ /
268
- // \ b22'''
269
- // \ /
270
- // *
271
- //
272
- // Note, how `a1` is broken and transformed into `a11'` and `a12'`, while `b2'` got broken and transformed into `b21''` and `b22''`.
273
- //
274
- // Having all that on mind, here is an outline for the transformation process algorithm:
275
- //
276
- // 1. We have `operationsA` and `operationsB` array, which we dynamically update as the transformation process goes.
277
- //
278
- // 2. We take next (or first) operation from `operationsA` and check from which operation `b` we need to start transforming it.
279
- // All original `operationsA` are set to be transformed starting from the first operation `b`.
280
- //
281
- // 3. We take operations from `operationsB`, one by one, starting from the correct one, and transform operation `a`
282
- // by operation `b` (and vice versa). We update `operationsA` and `operationsB` by replacing the original operations
283
- // with the transformation results.
284
- //
285
- // 4. If operation is broken into multiple operations, we save all the new operations in the place of the
286
- // original operation.
287
- //
288
- // 5. Additionally, if operation `a` was broken, for the "new" operation, we remember from which operation `b` it should
289
- // be transformed by.
290
- //
291
- // 6. We continue transforming "current" operation `a` until it is transformed by all `operationsB`. Then, go to 2.
292
- // unless the last operation `a` was transformed.
293
- //
294
- // The actual implementation of the above algorithm is slightly different, as only one loop (while) is used.
295
- // The difference is that we have "current" `a` operation to transform and we store the index of the next `b` operation
296
- // to transform by. Each loop operates on two indexes then: index pointing to currently processed `a` operation and
297
- // index pointing to next `b` operation. Each loop is just one `a * b` + `b * a` transformation. After each loop
298
- // operation `b` index is updated. If all `b` operations were visited for the current `a` operation, we change
299
- // current `a` operation index to the next one.
300
- //
301
-
302
- // For each operation `a`, keeps information what is the index in `operationsB` from which the transformation should start.
303
- const nextTransformIndex = new WeakMap();
304
-
305
- // For all the original `operationsA`, set that they should be transformed starting from the first of `operationsB`.
306
- for ( const op of operationsA ) {
307
- nextTransformIndex.set( op, 0 );
308
- }
309
-
310
- // Additional data that is used for some postprocessing after the main transformation process is done.
311
- const data = {
312
- nextBaseVersionA: operationsA[ operationsA.length - 1 ].baseVersion + 1,
313
- nextBaseVersionB: operationsB[ operationsB.length - 1 ].baseVersion + 1,
314
- originalOperationsACount: operationsA.length,
315
- originalOperationsBCount: operationsB.length
316
- };
317
-
318
- // Index of currently transformed operation `a`.
319
- let i = 0;
320
-
321
- // While not all `operationsA` are transformed...
322
- while ( i < operationsA.length ) {
323
- // Get "current" operation `a`.
324
- const opA = operationsA[ i ];
325
-
326
- // For the "current" operation `a`, get the index of the next operation `b` to transform by.
327
- const indexB = nextTransformIndex.get( opA );
328
-
329
- // If operation `a` was already transformed by every operation `b`, change "current" operation `a` to the next one.
330
- if ( indexB == operationsB.length ) {
331
- i++;
332
- continue;
333
- }
334
-
335
- const opB = operationsB[ indexB ];
336
-
337
- // Transform `a` by `b` and `b` by `a`.
338
- const newOpsA = transform( opA, opB, contextFactory.getContext( opA, opB, true ) );
339
- const newOpsB = transform( opB, opA, contextFactory.getContext( opB, opA, false ) );
340
- // As a result we get one or more `newOpsA` and one or more `newOpsB` operations.
341
-
342
- // Update contextual information about operations.
343
- contextFactory.updateRelation( opA, opB );
344
-
345
- contextFactory.setOriginalOperations( newOpsA, opA );
346
- contextFactory.setOriginalOperations( newOpsB, opB );
347
-
348
- // For new `a` operations, update their index of the next operation `b` to transform them by.
349
- //
350
- // This is needed even if there was only one result (`a` was not broken) because that information is used
351
- // at the beginning of this loop every time.
352
- for ( const newOpA of newOpsA ) {
353
- // Acknowledge, that operation `b` also might be broken into multiple operations.
354
- //
355
- // This is why we raise `indexB` not just by 1. If `newOpsB` are multiple operations, they will be
356
- // spliced in the place of `opB`. So we need to change `transformBy` accordingly, so that an operation won't
357
- // be transformed by the same operation (part of it) again.
358
- nextTransformIndex.set( newOpA, indexB + newOpsB.length );
359
- }
360
-
361
- // Update `operationsA` and `operationsB` with the transformed versions.
362
- operationsA.splice( i, 1, ...newOpsA );
363
- operationsB.splice( indexB, 1, ...newOpsB );
364
- }
365
-
366
- if ( options.padWithNoOps ) {
367
- // If no-operations padding is enabled, count how many extra `a` and `b` operations were generated.
368
- const brokenOperationsACount = operationsA.length - data.originalOperationsACount;
369
- const brokenOperationsBCount = operationsB.length - data.originalOperationsBCount;
370
-
371
- // Then, if that number is not the same, pad `operationsA` or `operationsB` with correct number of no-ops so
372
- // that the base versions are equalled.
373
- //
374
- // Note that only one array will be updated, as only one of those subtractions can be greater than zero.
375
- padWithNoOps( operationsA, brokenOperationsBCount - brokenOperationsACount );
376
- padWithNoOps( operationsB, brokenOperationsACount - brokenOperationsBCount );
377
- }
378
-
379
- // Finally, update base versions of transformed operations.
380
- updateBaseVersions( operationsA, data.nextBaseVersionB );
381
- updateBaseVersions( operationsB, data.nextBaseVersionA );
382
-
383
- return { operationsA, operationsB, originalOperations };
143
+ export function transformSets(operationsA, operationsB, options) {
144
+ // Create new arrays so the originally passed arguments are not changed.
145
+ // No need to clone operations, they are cloned as they are transformed.
146
+ operationsA = operationsA.slice();
147
+ operationsB = operationsB.slice();
148
+ const contextFactory = new ContextFactory(options.document, options.useRelations, options.forceWeakRemove);
149
+ contextFactory.setOriginalOperations(operationsA);
150
+ contextFactory.setOriginalOperations(operationsB);
151
+ const originalOperations = contextFactory.originalOperations;
152
+ // If one of sets is empty there is simply nothing to transform, so return sets as they are.
153
+ if (operationsA.length == 0 || operationsB.length == 0) {
154
+ return { operationsA, operationsB, originalOperations };
155
+ }
156
+ //
157
+ // Following is a description of transformation process:
158
+ //
159
+ // There are `operationsA` and `operationsB` to be transformed, both by both.
160
+ //
161
+ // So, suppose we have sets of two operations each: `operationsA` = `[ a1, a2 ]`, `operationsB` = `[ b1, b2 ]`.
162
+ //
163
+ // Remember, that we can only transform operations that base on the same context. We assert that `a1` and `b1` base on
164
+ // the same context and we transform them. Then, we get `a1'` and `b1'`. `a2` bases on a context with `a1` -- `a2`
165
+ // is an operation that followed `a1`. Similarly, `b2` bases on a context with `b1`.
166
+ //
167
+ // However, since `a1'` is a result of transformation by `b1`, `a1'` now also has a context with `b1`. This means that
168
+ // we can safely transform `a1'` by `b2`. As we finish transforming `a1`, we also transformed all `operationsB`.
169
+ // All `operationsB` also have context including `a1`. Now, we can properly transform `a2` by those operations.
170
+ //
171
+ // The transformation process can be visualized on a transformation diagram ("diamond diagram"):
172
+ //
173
+ // [the initial state]
174
+ // [common for a1 and b1]
175
+ //
176
+ // *
177
+ // / \
178
+ // / \
179
+ // b1 a1
180
+ // / \
181
+ // / \
182
+ // * *
183
+ // / \ / \
184
+ // / \ / \
185
+ // b2 a1' b1' a2
186
+ // / \ / \
187
+ // / \ / \
188
+ // * * *
189
+ // \ / \ /
190
+ // \ / \ /
191
+ // a1'' b2' a2' b1''
192
+ // \ / \ /
193
+ // \ / \ /
194
+ // * *
195
+ // \ /
196
+ // \ /
197
+ // a2'' b2''
198
+ // \ /
199
+ // \ /
200
+ // *
201
+ //
202
+ // [the final state]
203
+ //
204
+ // The final state can be reached from the initial state by applying `a1`, `a2`, `b1''` and `b2''`, as well as by
205
+ // applying `b1`, `b2`, `a1''`, `a2''`. Note how the operations get to a proper common state before each pair is
206
+ // transformed.
207
+ //
208
+ // Another thing to consider is that an operation during transformation can be broken into multiple operations.
209
+ // Suppose that `a1` * `b1` = `[ a11', a12' ]` (instead of `a1'` that we considered previously).
210
+ //
211
+ // In that case, we leave `a12'` for later and we continue transforming `a11'` until it is transformed by all `operationsB`
212
+ // (in our case it is just `b2`). At this point, `b1` is transformed by "whole" `a1`, while `b2` is only transformed
213
+ // by `a11'`. Similarly, `a12'` is only transformed by `b1`. This leads to a conclusion that we need to start transforming `a12'`
214
+ // from the moment just after it was broken. So, `a12'` is transformed by `b2`. Now, "the whole" `a1` is transformed
215
+ // by `operationsB`, while all `operationsB` are transformed by "the whole" `a1`. This means that we can continue with
216
+ // following `operationsA` (in our case it is just `a2`).
217
+ //
218
+ // Of course, also `operationsB` can be broken. However, since we focus on transforming operation `a` to the end,
219
+ // the only thing to do is to store both pieces of operation `b`, so that the next transformed operation `a` will
220
+ // be transformed by both of them.
221
+ //
222
+ // *
223
+ // / \
224
+ // / \
225
+ // / \
226
+ // b1 a1
227
+ // / \
228
+ // / \
229
+ // / \
230
+ // * *
231
+ // / \ / \
232
+ // / a11' / \
233
+ // / \ / \
234
+ // b2 * b1' a2
235
+ // / / \ / \
236
+ // / / a12' / \
237
+ // / / \ / \
238
+ // * b2' * *
239
+ // \ / / \ /
240
+ // a11'' / b21'' \ /
241
+ // \ / / \ /
242
+ // * * a2' b1''
243
+ // \ / \ \ /
244
+ // a12'' b22''\ \ /
245
+ // \ / \ \ /
246
+ // * a2'' *
247
+ // \ \ /
248
+ // \ \ b21'''
249
+ // \ \ /
250
+ // a2''' *
251
+ // \ /
252
+ // \ b22'''
253
+ // \ /
254
+ // *
255
+ //
256
+ // Note, how `a1` is broken and transformed into `a11'` and `a12'`, while `b2'` got broken and transformed into `b21''` and `b22''`.
257
+ //
258
+ // Having all that on mind, here is an outline for the transformation process algorithm:
259
+ //
260
+ // 1. We have `operationsA` and `operationsB` array, which we dynamically update as the transformation process goes.
261
+ //
262
+ // 2. We take next (or first) operation from `operationsA` and check from which operation `b` we need to start transforming it.
263
+ // All original `operationsA` are set to be transformed starting from the first operation `b`.
264
+ //
265
+ // 3. We take operations from `operationsB`, one by one, starting from the correct one, and transform operation `a`
266
+ // by operation `b` (and vice versa). We update `operationsA` and `operationsB` by replacing the original operations
267
+ // with the transformation results.
268
+ //
269
+ // 4. If operation is broken into multiple operations, we save all the new operations in the place of the
270
+ // original operation.
271
+ //
272
+ // 5. Additionally, if operation `a` was broken, for the "new" operation, we remember from which operation `b` it should
273
+ // be transformed by.
274
+ //
275
+ // 6. We continue transforming "current" operation `a` until it is transformed by all `operationsB`. Then, go to 2.
276
+ // unless the last operation `a` was transformed.
277
+ //
278
+ // The actual implementation of the above algorithm is slightly different, as only one loop (while) is used.
279
+ // The difference is that we have "current" `a` operation to transform and we store the index of the next `b` operation
280
+ // to transform by. Each loop operates on two indexes then: index pointing to currently processed `a` operation and
281
+ // index pointing to next `b` operation. Each loop is just one `a * b` + `b * a` transformation. After each loop
282
+ // operation `b` index is updated. If all `b` operations were visited for the current `a` operation, we change
283
+ // current `a` operation index to the next one.
284
+ //
285
+ // For each operation `a`, keeps information what is the index in `operationsB` from which the transformation should start.
286
+ const nextTransformIndex = new WeakMap();
287
+ // For all the original `operationsA`, set that they should be transformed starting from the first of `operationsB`.
288
+ for (const op of operationsA) {
289
+ nextTransformIndex.set(op, 0);
290
+ }
291
+ // Additional data that is used for some postprocessing after the main transformation process is done.
292
+ const data = {
293
+ nextBaseVersionA: operationsA[operationsA.length - 1].baseVersion + 1,
294
+ nextBaseVersionB: operationsB[operationsB.length - 1].baseVersion + 1,
295
+ originalOperationsACount: operationsA.length,
296
+ originalOperationsBCount: operationsB.length
297
+ };
298
+ // Index of currently transformed operation `a`.
299
+ let i = 0;
300
+ // While not all `operationsA` are transformed...
301
+ while (i < operationsA.length) {
302
+ // Get "current" operation `a`.
303
+ const opA = operationsA[i];
304
+ // For the "current" operation `a`, get the index of the next operation `b` to transform by.
305
+ const indexB = nextTransformIndex.get(opA);
306
+ // If operation `a` was already transformed by every operation `b`, change "current" operation `a` to the next one.
307
+ if (indexB == operationsB.length) {
308
+ i++;
309
+ continue;
310
+ }
311
+ const opB = operationsB[indexB];
312
+ // Transform `a` by `b` and `b` by `a`.
313
+ const newOpsA = transform(opA, opB, contextFactory.getContext(opA, opB, true));
314
+ const newOpsB = transform(opB, opA, contextFactory.getContext(opB, opA, false));
315
+ // As a result we get one or more `newOpsA` and one or more `newOpsB` operations.
316
+ // Update contextual information about operations.
317
+ contextFactory.updateRelation(opA, opB);
318
+ contextFactory.setOriginalOperations(newOpsA, opA);
319
+ contextFactory.setOriginalOperations(newOpsB, opB);
320
+ // For new `a` operations, update their index of the next operation `b` to transform them by.
321
+ //
322
+ // This is needed even if there was only one result (`a` was not broken) because that information is used
323
+ // at the beginning of this loop every time.
324
+ for (const newOpA of newOpsA) {
325
+ // Acknowledge, that operation `b` also might be broken into multiple operations.
326
+ //
327
+ // This is why we raise `indexB` not just by 1. If `newOpsB` are multiple operations, they will be
328
+ // spliced in the place of `opB`. So we need to change `transformBy` accordingly, so that an operation won't
329
+ // be transformed by the same operation (part of it) again.
330
+ nextTransformIndex.set(newOpA, indexB + newOpsB.length);
331
+ }
332
+ // Update `operationsA` and `operationsB` with the transformed versions.
333
+ operationsA.splice(i, 1, ...newOpsA);
334
+ operationsB.splice(indexB, 1, ...newOpsB);
335
+ }
336
+ if (options.padWithNoOps) {
337
+ // If no-operations padding is enabled, count how many extra `a` and `b` operations were generated.
338
+ const brokenOperationsACount = operationsA.length - data.originalOperationsACount;
339
+ const brokenOperationsBCount = operationsB.length - data.originalOperationsBCount;
340
+ // Then, if that number is not the same, pad `operationsA` or `operationsB` with correct number of no-ops so
341
+ // that the base versions are equalled.
342
+ //
343
+ // Note that only one array will be updated, as only one of those subtractions can be greater than zero.
344
+ padWithNoOps(operationsA, brokenOperationsBCount - brokenOperationsACount);
345
+ padWithNoOps(operationsB, brokenOperationsACount - brokenOperationsBCount);
346
+ }
347
+ // Finally, update base versions of transformed operations.
348
+ updateBaseVersions(operationsA, data.nextBaseVersionB);
349
+ updateBaseVersions(operationsB, data.nextBaseVersionA);
350
+ return { operationsA, operationsB, originalOperations };
384
351
  }
385
-
386
352
  // Gathers additional data about operations processed during transformation. Can be used to obtain contextual information
387
353
  // about two operations that are about to be transformed. This contextual information can be used for better conflict resolution.
388
354
  class ContextFactory {
389
- // Creates `ContextFactory` instance.
390
- //
391
- // @param {module:engine/model/document~Document} document Document which the operations change.
392
- // @param {Boolean} useRelations Whether during transformation relations should be used (used during undo for
393
- // better conflict resolution).
394
- // @param {Boolean} [forceWeakRemove=false] If set to `false`, remove operation will be always stronger than move operation,
395
- // so the removed nodes won't end up back in the document root. When set to `true`, context data will be used.
396
- constructor( document, useRelations, forceWeakRemove = false ) {
397
- // For each operation that is created during transformation process, we keep a reference to the original operation
398
- // which it comes from. The original operation works as a kind of "identifier". Every contextual information
399
- // gathered during transformation that we want to save for given operation, is actually saved for the original operation.
400
- // This way no matter if operation `a` is cloned, then transformed, even breaks, we still have access to the previously
401
- // gathered data through original operation reference.
402
- this.originalOperations = new Map();
403
-
404
- // `model.History` instance which information about undone operations will be taken from.
405
- this._history = document.history;
406
-
407
- // Whether additional context should be used.
408
- this._useRelations = useRelations;
409
-
410
- this._forceWeakRemove = !!forceWeakRemove;
411
-
412
- // Relations is a double-map structure (maps in map) where for two operations we store how those operations were related
413
- // to each other. Those relations are evaluated during transformation process. For every transformated pair of operations
414
- // we keep relations between them.
415
- this._relations = new Map();
416
- }
417
-
418
- // Sets "original operation" for given operations.
419
- //
420
- // During transformation process, operations are cloned, then changed, then processed again, sometimes broken into two
421
- // or multiple operations. When gathering additional data it is important that all operations can be somehow linked
422
- // so a cloned and transformed "version" still kept track of the data assigned earlier to it.
423
- //
424
- // The original operation object will be used as such an universal linking id. Throughout the transformation process
425
- // all cloned operations will refer to "the original operation" when storing and reading additional data.
426
- //
427
- // If `takeFrom` is not set, each operation from `operations` array will be assigned itself as "the original operation".
428
- // This should be used as an initialization step.
429
- //
430
- // If `takeFrom` is set, each operation from `operations` will be assigned the same original operation as assigned
431
- // for `takeFrom` operation. This should be used to update original operations. It should be used in a way that
432
- // `operations` are the result of `takeFrom` transformation to ensure proper "original operation propagation".
433
- //
434
- // @param {Array.<module:engine/model/operation/operation~Operation>} operations
435
- // @param {module:engine/model/operation/operation~Operation|null} [takeFrom=null]
436
- setOriginalOperations( operations, takeFrom = null ) {
437
- const originalOperation = takeFrom ? this.originalOperations.get( takeFrom ) : null;
438
-
439
- for ( const operation of operations ) {
440
- this.originalOperations.set( operation, originalOperation || operation );
441
- }
442
- }
443
-
444
- // Saves a relation between operations `opA` and `opB`.
445
- //
446
- // Relations are then later used to help solve conflicts when operations are transformed.
447
- //
448
- // @param {module:engine/model/operation/operation~Operation} opA
449
- // @param {module:engine/model/operation/operation~Operation} opB
450
- updateRelation( opA, opB ) {
451
- // The use of relations is described in a bigger detail in transformation functions.
452
- //
453
- // In brief, this function, for specified pairs of operation types, checks how positions defined in those operations relate.
454
- // Then those relations are saved. For example, for two move operations, it is saved if one of those operations target
455
- // position is before the other operation source position. This kind of information gives contextual information when
456
- // transformation is used during undo. Similar checks are done for other pairs of operations.
457
- //
458
- switch ( opA.constructor ) {
459
- case MoveOperation: {
460
- switch ( opB.constructor ) {
461
- case MergeOperation: {
462
- if ( opA.targetPosition.isEqual( opB.sourcePosition ) || opB.movedRange.containsPosition( opA.targetPosition ) ) {
463
- this._setRelation( opA, opB, 'insertAtSource' );
464
- } else if ( opA.targetPosition.isEqual( opB.deletionPosition ) ) {
465
- this._setRelation( opA, opB, 'insertBetween' );
466
- } else if ( opA.targetPosition.isAfter( opB.sourcePosition ) ) {
467
- this._setRelation( opA, opB, 'moveTargetAfter' );
468
- }
469
-
470
- break;
471
- }
472
-
473
- case MoveOperation: {
474
- if ( opA.targetPosition.isEqual( opB.sourcePosition ) || opA.targetPosition.isBefore( opB.sourcePosition ) ) {
475
- this._setRelation( opA, opB, 'insertBefore' );
476
- } else {
477
- this._setRelation( opA, opB, 'insertAfter' );
478
- }
479
-
480
- break;
481
- }
482
- }
483
-
484
- break;
485
- }
486
-
487
- case SplitOperation: {
488
- switch ( opB.constructor ) {
489
- case MergeOperation: {
490
- if ( opA.splitPosition.isBefore( opB.sourcePosition ) ) {
491
- this._setRelation( opA, opB, 'splitBefore' );
492
- }
493
-
494
- break;
495
- }
496
-
497
- case MoveOperation: {
498
- if ( opA.splitPosition.isEqual( opB.sourcePosition ) || opA.splitPosition.isBefore( opB.sourcePosition ) ) {
499
- this._setRelation( opA, opB, 'splitBefore' );
500
- } else {
501
- const range = Range._createFromPositionAndShift( opB.sourcePosition, opB.howMany );
502
-
503
- if ( opA.splitPosition.hasSameParentAs( opB.sourcePosition ) && range.containsPosition( opA.splitPosition ) ) {
504
- const howMany = range.end.offset - opA.splitPosition.offset;
505
- const offset = opA.splitPosition.offset - range.start.offset;
506
-
507
- this._setRelation( opA, opB, { howMany, offset } );
508
- }
509
- }
510
- }
511
- }
512
-
513
- break;
514
- }
515
-
516
- case MergeOperation: {
517
- switch ( opB.constructor ) {
518
- case MergeOperation: {
519
- if ( !opA.targetPosition.isEqual( opB.sourcePosition ) ) {
520
- this._setRelation( opA, opB, 'mergeTargetNotMoved' );
521
- }
522
-
523
- if ( opA.sourcePosition.isEqual( opB.targetPosition ) ) {
524
- this._setRelation( opA, opB, 'mergeSourceNotMoved' );
525
- }
526
-
527
- if ( opA.sourcePosition.isEqual( opB.sourcePosition ) ) {
528
- this._setRelation( opA, opB, 'mergeSameElement' );
529
- }
530
-
531
- break;
532
- }
533
-
534
- case SplitOperation: {
535
- if ( opA.sourcePosition.isEqual( opB.splitPosition ) ) {
536
- this._setRelation( opA, opB, 'splitAtSource' );
537
- }
538
- }
539
- }
540
-
541
- break;
542
- }
543
-
544
- case MarkerOperation: {
545
- const markerRange = opA.newRange;
546
-
547
- if ( !markerRange ) {
548
- return;
549
- }
550
-
551
- switch ( opB.constructor ) {
552
- case MoveOperation: {
553
- const movedRange = Range._createFromPositionAndShift( opB.sourcePosition, opB.howMany );
554
-
555
- const affectedLeft = movedRange.containsPosition( markerRange.start ) ||
556
- movedRange.start.isEqual( markerRange.start );
557
-
558
- const affectedRight = movedRange.containsPosition( markerRange.end ) ||
559
- movedRange.end.isEqual( markerRange.end );
560
-
561
- if ( ( affectedLeft || affectedRight ) && !movedRange.containsRange( markerRange ) ) {
562
- this._setRelation( opA, opB, {
563
- side: affectedLeft ? 'left' : 'right',
564
- path: affectedLeft ? markerRange.start.path.slice() : markerRange.end.path.slice()
565
- } );
566
- }
567
-
568
- break;
569
- }
570
-
571
- case MergeOperation: {
572
- const wasInLeftElement = markerRange.start.isEqual( opB.targetPosition );
573
- const wasStartBeforeMergedElement = markerRange.start.isEqual( opB.deletionPosition );
574
- const wasEndBeforeMergedElement = markerRange.end.isEqual( opB.deletionPosition );
575
- const wasInRightElement = markerRange.end.isEqual( opB.sourcePosition );
576
-
577
- if ( wasInLeftElement || wasStartBeforeMergedElement || wasEndBeforeMergedElement || wasInRightElement ) {
578
- this._setRelation( opA, opB, {
579
- wasInLeftElement,
580
- wasStartBeforeMergedElement,
581
- wasEndBeforeMergedElement,
582
- wasInRightElement
583
- } );
584
- }
585
-
586
- break;
587
- }
588
- }
589
-
590
- break;
591
- }
592
- }
593
- }
594
-
595
- // Evaluates and returns contextual information about two given operations `opA` and `opB` which are about to be transformed.
596
- //
597
- // @param {module:engine/model/operation/operation~Operation} opA
598
- // @param {module:engine/model/operation/operation~Operation} opB
599
- // @returns {module:engine/model/operation/transform~TransformationContext}
600
- getContext( opA, opB, aIsStrong ) {
601
- return {
602
- aIsStrong,
603
- aWasUndone: this._wasUndone( opA ),
604
- bWasUndone: this._wasUndone( opB ),
605
- abRelation: this._useRelations ? this._getRelation( opA, opB ) : null,
606
- baRelation: this._useRelations ? this._getRelation( opB, opA ) : null,
607
- forceWeakRemove: this._forceWeakRemove
608
- };
609
- }
610
-
611
- // Returns whether given operation `op` has already been undone.
612
- //
613
- // Information whether an operation was undone gives more context when making a decision when two operations are in conflict.
614
- //
615
- // @param {module:engine/model/operation/operation~Operation} op
616
- // @returns {Boolean}
617
- _wasUndone( op ) {
618
- // For `op`, get its original operation. After all, if `op` is a clone (or even transformed clone) of another
619
- // operation, literally `op` couldn't be undone. It was just generated. If anything, it was the operation it origins
620
- // from which was undone. So get that original operation.
621
- const originalOp = this.originalOperations.get( op );
622
-
623
- // And check with the document if the original operation was undone.
624
- return originalOp.wasUndone || this._history.isUndoneOperation( originalOp );
625
- }
626
-
627
- // Returns a relation between `opA` and an operation which is undone by `opB`. This can be `String` value if a relation
628
- // was set earlier or `null` if there was no relation between those operations.
629
- //
630
- // This is a little tricky to understand, so let's compare it to `ContextFactory#_wasUndone`.
631
- //
632
- // When `wasUndone( opB )` is used, we check if the `opB` has already been undone. It is obvious, that the
633
- // undoing operation must happen after the undone operation. So, essentially, we have `opB`, we take document history,
634
- // we look forward in the future and ask if in that future `opB` was undone.
635
- //
636
- // Relations is a backward process to `wasUndone()`.
637
- //
638
- // Long story short - using relations is asking what happened in the past. Looking back. This time we have an undoing
639
- // operation `opB` which has undone some other operation. When there is a transformation `opA` x `opB` and there is
640
- // a conflict to solve and `opB` is an undoing operation, we can look back in the history and see what was a relation
641
- // between `opA` and the operation which `opB` undone. Basing on that relation from the past, we can now make
642
- // a better decision when resolving a conflict between two operations, because we know more about the context of
643
- // those two operations.
644
- //
645
- // This is why this function does not return a relation directly between `opA` and `opB` because we need to look
646
- // back to search for a meaningful contextual information.
647
- //
648
- // @param {module:engine/model/operation/operation~Operation} opA
649
- // @param {module:engine/model/operation/operation~Operation} opB
650
- // @returns {String|null}
651
- _getRelation( opA, opB ) {
652
- // Get the original operation. Similarly as in `wasUndone()` it is used as an universal identifier for stored data.
653
- const origB = this.originalOperations.get( opB );
654
- const undoneB = this._history.getUndoneOperation( origB );
655
-
656
- // If `opB` is not undoing any operation, there is no relation.
657
- if ( !undoneB ) {
658
- return null;
659
- }
660
-
661
- const origA = this.originalOperations.get( opA );
662
- const relationsA = this._relations.get( origA );
663
-
664
- // Get all relations for `opA`, and check if there is a relation with `opB`-undone-counterpart. If so, return it.
665
- if ( relationsA ) {
666
- return relationsA.get( undoneB ) || null;
667
- }
668
-
669
- return null;
670
- }
671
-
672
- // Helper function for `ContextFactory#updateRelations`.
673
- //
674
- // @private
675
- // @param {module:engine/model/operation/operation~Operation} opA
676
- // @param {module:engine/model/operation/operation~Operation} opB
677
- // @param {String} relation
678
- _setRelation( opA, opB, relation ) {
679
- // As always, setting is for original operations, not the clones/transformed operations.
680
- const origA = this.originalOperations.get( opA );
681
- const origB = this.originalOperations.get( opB );
682
-
683
- let relationsA = this._relations.get( origA );
684
-
685
- if ( !relationsA ) {
686
- relationsA = new Map();
687
- this._relations.set( origA, relationsA );
688
- }
689
-
690
- relationsA.set( origB, relation );
691
- }
355
+ // Creates `ContextFactory` instance.
356
+ //
357
+ // @param {module:engine/model/document~Document} document Document which the operations change.
358
+ // @param {Boolean} useRelations Whether during transformation relations should be used (used during undo for
359
+ // better conflict resolution).
360
+ // @param {Boolean} [forceWeakRemove=false] If set to `false`, remove operation will be always stronger than move operation,
361
+ // so the removed nodes won't end up back in the document root. When set to `true`, context data will be used.
362
+ constructor(document, useRelations, forceWeakRemove = false) {
363
+ // For each operation that is created during transformation process, we keep a reference to the original operation
364
+ // which it comes from. The original operation works as a kind of "identifier". Every contextual information
365
+ // gathered during transformation that we want to save for given operation, is actually saved for the original operation.
366
+ // This way no matter if operation `a` is cloned, then transformed, even breaks, we still have access to the previously
367
+ // gathered data through original operation reference.
368
+ this.originalOperations = new Map();
369
+ // `model.History` instance which information about undone operations will be taken from.
370
+ this._history = document.history;
371
+ // Whether additional context should be used.
372
+ this._useRelations = useRelations;
373
+ this._forceWeakRemove = !!forceWeakRemove;
374
+ // Relations is a double-map structure (maps in map) where for two operations we store how those operations were related
375
+ // to each other. Those relations are evaluated during transformation process. For every transformated pair of operations
376
+ // we keep relations between them.
377
+ this._relations = new Map();
378
+ }
379
+ // Sets "original operation" for given operations.
380
+ //
381
+ // During transformation process, operations are cloned, then changed, then processed again, sometimes broken into two
382
+ // or multiple operations. When gathering additional data it is important that all operations can be somehow linked
383
+ // so a cloned and transformed "version" still kept track of the data assigned earlier to it.
384
+ //
385
+ // The original operation object will be used as such an universal linking id. Throughout the transformation process
386
+ // all cloned operations will refer to "the original operation" when storing and reading additional data.
387
+ //
388
+ // If `takeFrom` is not set, each operation from `operations` array will be assigned itself as "the original operation".
389
+ // This should be used as an initialization step.
390
+ //
391
+ // If `takeFrom` is set, each operation from `operations` will be assigned the same original operation as assigned
392
+ // for `takeFrom` operation. This should be used to update original operations. It should be used in a way that
393
+ // `operations` are the result of `takeFrom` transformation to ensure proper "original operation propagation".
394
+ //
395
+ // @param {Array.<module:engine/model/operation/operation~Operation>} operations
396
+ // @param {module:engine/model/operation/operation~Operation|null} [takeFrom=null]
397
+ setOriginalOperations(operations, takeFrom = null) {
398
+ const originalOperation = takeFrom ? this.originalOperations.get(takeFrom) : null;
399
+ for (const operation of operations) {
400
+ this.originalOperations.set(operation, originalOperation || operation);
401
+ }
402
+ }
403
+ // Saves a relation between operations `opA` and `opB`.
404
+ //
405
+ // Relations are then later used to help solve conflicts when operations are transformed.
406
+ //
407
+ // @param {module:engine/model/operation/operation~Operation} opA
408
+ // @param {module:engine/model/operation/operation~Operation} opB
409
+ updateRelation(opA, opB) {
410
+ // The use of relations is described in a bigger detail in transformation functions.
411
+ //
412
+ // In brief, this function, for specified pairs of operation types, checks how positions defined in those operations relate.
413
+ // Then those relations are saved. For example, for two move operations, it is saved if one of those operations target
414
+ // position is before the other operation source position. This kind of information gives contextual information when
415
+ // transformation is used during undo. Similar checks are done for other pairs of operations.
416
+ //
417
+ if (opA instanceof MoveOperation) {
418
+ if (opB instanceof MergeOperation) {
419
+ if (opA.targetPosition.isEqual(opB.sourcePosition) || opB.movedRange.containsPosition(opA.targetPosition)) {
420
+ this._setRelation(opA, opB, 'insertAtSource');
421
+ }
422
+ else if (opA.targetPosition.isEqual(opB.deletionPosition)) {
423
+ this._setRelation(opA, opB, 'insertBetween');
424
+ }
425
+ else if (opA.targetPosition.isAfter(opB.sourcePosition)) {
426
+ this._setRelation(opA, opB, 'moveTargetAfter');
427
+ }
428
+ }
429
+ else if (opB instanceof MoveOperation) {
430
+ if (opA.targetPosition.isEqual(opB.sourcePosition) || opA.targetPosition.isBefore(opB.sourcePosition)) {
431
+ this._setRelation(opA, opB, 'insertBefore');
432
+ }
433
+ else {
434
+ this._setRelation(opA, opB, 'insertAfter');
435
+ }
436
+ }
437
+ }
438
+ else if (opA instanceof SplitOperation) {
439
+ if (opB instanceof MergeOperation) {
440
+ if (opA.splitPosition.isBefore(opB.sourcePosition)) {
441
+ this._setRelation(opA, opB, 'splitBefore');
442
+ }
443
+ }
444
+ else if (opB instanceof MoveOperation) {
445
+ if (opA.splitPosition.isEqual(opB.sourcePosition) || opA.splitPosition.isBefore(opB.sourcePosition)) {
446
+ this._setRelation(opA, opB, 'splitBefore');
447
+ }
448
+ else {
449
+ const range = Range._createFromPositionAndShift(opB.sourcePosition, opB.howMany);
450
+ if (opA.splitPosition.hasSameParentAs(opB.sourcePosition) && range.containsPosition(opA.splitPosition)) {
451
+ const howMany = range.end.offset - opA.splitPosition.offset;
452
+ const offset = opA.splitPosition.offset - range.start.offset;
453
+ this._setRelation(opA, opB, { howMany, offset });
454
+ }
455
+ }
456
+ }
457
+ }
458
+ else if (opA instanceof MergeOperation) {
459
+ if (opB instanceof MergeOperation) {
460
+ if (!opA.targetPosition.isEqual(opB.sourcePosition)) {
461
+ this._setRelation(opA, opB, 'mergeTargetNotMoved');
462
+ }
463
+ if (opA.sourcePosition.isEqual(opB.targetPosition)) {
464
+ this._setRelation(opA, opB, 'mergeSourceNotMoved');
465
+ }
466
+ if (opA.sourcePosition.isEqual(opB.sourcePosition)) {
467
+ this._setRelation(opA, opB, 'mergeSameElement');
468
+ }
469
+ }
470
+ else if (opB instanceof SplitOperation) {
471
+ if (opA.sourcePosition.isEqual(opB.splitPosition)) {
472
+ this._setRelation(opA, opB, 'splitAtSource');
473
+ }
474
+ }
475
+ }
476
+ else if (opA instanceof MarkerOperation) {
477
+ const markerRange = opA.newRange;
478
+ if (!markerRange) {
479
+ return;
480
+ }
481
+ if (opB instanceof MoveOperation) {
482
+ const movedRange = Range._createFromPositionAndShift(opB.sourcePosition, opB.howMany);
483
+ const affectedLeft = movedRange.containsPosition(markerRange.start) ||
484
+ movedRange.start.isEqual(markerRange.start);
485
+ const affectedRight = movedRange.containsPosition(markerRange.end) ||
486
+ movedRange.end.isEqual(markerRange.end);
487
+ if ((affectedLeft || affectedRight) && !movedRange.containsRange(markerRange)) {
488
+ this._setRelation(opA, opB, {
489
+ side: affectedLeft ? 'left' : 'right',
490
+ path: affectedLeft ? markerRange.start.path.slice() : markerRange.end.path.slice()
491
+ });
492
+ }
493
+ }
494
+ else if (opB instanceof MergeOperation) {
495
+ const wasInLeftElement = markerRange.start.isEqual(opB.targetPosition);
496
+ const wasStartBeforeMergedElement = markerRange.start.isEqual(opB.deletionPosition);
497
+ const wasEndBeforeMergedElement = markerRange.end.isEqual(opB.deletionPosition);
498
+ const wasInRightElement = markerRange.end.isEqual(opB.sourcePosition);
499
+ if (wasInLeftElement || wasStartBeforeMergedElement || wasEndBeforeMergedElement || wasInRightElement) {
500
+ this._setRelation(opA, opB, {
501
+ wasInLeftElement,
502
+ wasStartBeforeMergedElement,
503
+ wasEndBeforeMergedElement,
504
+ wasInRightElement
505
+ });
506
+ }
507
+ }
508
+ }
509
+ }
510
+ // Evaluates and returns contextual information about two given operations `opA` and `opB` which are about to be transformed.
511
+ //
512
+ // @param {module:engine/model/operation/operation~Operation} opA
513
+ // @param {module:engine/model/operation/operation~Operation} opB
514
+ // @returns {module:engine/model/operation/transform~TransformationContext}
515
+ getContext(opA, opB, aIsStrong) {
516
+ return {
517
+ aIsStrong,
518
+ aWasUndone: this._wasUndone(opA),
519
+ bWasUndone: this._wasUndone(opB),
520
+ abRelation: this._useRelations ? this._getRelation(opA, opB) : null,
521
+ baRelation: this._useRelations ? this._getRelation(opB, opA) : null,
522
+ forceWeakRemove: this._forceWeakRemove
523
+ };
524
+ }
525
+ // Returns whether given operation `op` has already been undone.
526
+ //
527
+ // Information whether an operation was undone gives more context when making a decision when two operations are in conflict.
528
+ //
529
+ // @param {module:engine/model/operation/operation~Operation} op
530
+ // @returns {Boolean}
531
+ _wasUndone(op) {
532
+ // For `op`, get its original operation. After all, if `op` is a clone (or even transformed clone) of another
533
+ // operation, literally `op` couldn't be undone. It was just generated. If anything, it was the operation it origins
534
+ // from which was undone. So get that original operation.
535
+ const originalOp = this.originalOperations.get(op);
536
+ // And check with the document if the original operation was undone.
537
+ return originalOp.wasUndone || this._history.isUndoneOperation(originalOp);
538
+ }
539
+ // Returns a relation between `opA` and an operation which is undone by `opB`. This can be `String` value if a relation
540
+ // was set earlier or `null` if there was no relation between those operations.
541
+ //
542
+ // This is a little tricky to understand, so let's compare it to `ContextFactory#_wasUndone`.
543
+ //
544
+ // When `wasUndone( opB )` is used, we check if the `opB` has already been undone. It is obvious, that the
545
+ // undoing operation must happen after the undone operation. So, essentially, we have `opB`, we take document history,
546
+ // we look forward in the future and ask if in that future `opB` was undone.
547
+ //
548
+ // Relations is a backward process to `wasUndone()`.
549
+ //
550
+ // Long story short - using relations is asking what happened in the past. Looking back. This time we have an undoing
551
+ // operation `opB` which has undone some other operation. When there is a transformation `opA` x `opB` and there is
552
+ // a conflict to solve and `opB` is an undoing operation, we can look back in the history and see what was a relation
553
+ // between `opA` and the operation which `opB` undone. Basing on that relation from the past, we can now make
554
+ // a better decision when resolving a conflict between two operations, because we know more about the context of
555
+ // those two operations.
556
+ //
557
+ // This is why this function does not return a relation directly between `opA` and `opB` because we need to look
558
+ // back to search for a meaningful contextual information.
559
+ //
560
+ // @param {module:engine/model/operation/operation~Operation} opA
561
+ // @param {module:engine/model/operation/operation~Operation} opB
562
+ // @returns {String|null}
563
+ _getRelation(opA, opB) {
564
+ // Get the original operation. Similarly as in `wasUndone()` it is used as an universal identifier for stored data.
565
+ const origB = this.originalOperations.get(opB);
566
+ const undoneB = this._history.getUndoneOperation(origB);
567
+ // If `opB` is not undoing any operation, there is no relation.
568
+ if (!undoneB) {
569
+ return null;
570
+ }
571
+ const origA = this.originalOperations.get(opA);
572
+ const relationsA = this._relations.get(origA);
573
+ // Get all relations for `opA`, and check if there is a relation with `opB`-undone-counterpart. If so, return it.
574
+ if (relationsA) {
575
+ return relationsA.get(undoneB) || null;
576
+ }
577
+ return null;
578
+ }
579
+ // Helper function for `ContextFactory#updateRelations`.
580
+ //
581
+ // @private
582
+ // @param {module:engine/model/operation/operation~Operation} opA
583
+ // @param {module:engine/model/operation/operation~Operation} opB
584
+ // @param {String} relation
585
+ _setRelation(opA, opB, relation) {
586
+ // As always, setting is for original operations, not the clones/transformed operations.
587
+ const origA = this.originalOperations.get(opA);
588
+ const origB = this.originalOperations.get(opB);
589
+ let relationsA = this._relations.get(origA);
590
+ if (!relationsA) {
591
+ relationsA = new Map();
592
+ this._relations.set(origA, relationsA);
593
+ }
594
+ relationsA.set(origB, relation);
595
+ }
692
596
  }
693
-
694
- /**
695
- * Holds additional contextual information about a transformed pair of operations (`a` and `b`). Those information
696
- * can be used for better conflict resolving.
697
- *
698
- * @typedef {Object} module:engine/model/operation/transform~TransformationContext
699
- *
700
- * @property {Boolean} aIsStrong Whether `a` is strong operation in this transformation, or weak.
701
- * @property {Boolean} aWasUndone Whether `a` operation was undone.
702
- * @property {Boolean} bWasUndone Whether `b` operation was undone.
703
- * @property {String|null} abRelation The relation between `a` operation and an operation undone by `b` operation.
704
- * @property {String|null} baRelation The relation between `b` operation and an operation undone by `a` operation.
705
- */
706
-
707
597
  /**
708
598
  * An utility function that updates {@link module:engine/model/operation/operation~Operation#baseVersion base versions}
709
599
  * of passed operations.
@@ -715,12 +605,11 @@ class ContextFactory {
715
605
  * @param {Array.<module:engine/model/operation/operation~Operation>} operations Operations to update.
716
606
  * @param {Number} baseVersion Base version to set for the first operation in `operations`.
717
607
  */
718
- function updateBaseVersions( operations, baseVersion ) {
719
- for ( const operation of operations ) {
720
- operation.baseVersion = baseVersion++;
721
- }
608
+ function updateBaseVersions(operations, baseVersion) {
609
+ for (const operation of operations) {
610
+ operation.baseVersion = baseVersion++;
611
+ }
722
612
  }
723
-
724
613
  /**
725
614
  * Adds `howMany` instances of {@link module:engine/model/operation/nooperation~NoOperation} to `operations` set.
726
615
  *
@@ -728,127 +617,114 @@ function updateBaseVersions( operations, baseVersion ) {
728
617
  * @param {Array.<module:engine/model/operation/operation~Operation>} operations
729
618
  * @param {Number} howMany
730
619
  */
731
- function padWithNoOps( operations, howMany ) {
732
- for ( let i = 0; i < howMany; i++ ) {
733
- operations.push( new NoOperation( 0 ) );
734
- }
620
+ function padWithNoOps(operations, howMany) {
621
+ for (let i = 0; i < howMany; i++) {
622
+ operations.push(new NoOperation(0));
623
+ }
735
624
  }
736
-
737
625
  // -----------------------
738
-
739
- setTransformation( AttributeOperation, AttributeOperation, ( a, b, context ) => {
740
- // If operations in conflict, check if their ranges intersect and manage them properly.
741
- //
742
- // Operations can be in conflict only if:
743
- //
744
- // * their key is the same (they change the same attribute), and
745
- // * they are in the same parent (operations for ranges [ 1 ] - [ 3 ] and [ 2, 0 ] - [ 2, 5 ] change different
746
- // elements and can't be in conflict).
747
- if ( a.key === b.key && a.range.start.hasSameParentAs( b.range.start ) ) {
748
- // First, we want to apply change to the part of a range that has not been changed by the other operation.
749
- const operations = a.range.getDifference( b.range ).map( range => {
750
- return new AttributeOperation( range, a.key, a.oldValue, a.newValue, 0 );
751
- } );
752
-
753
- // Then we take care of the common part of ranges.
754
- const common = a.range.getIntersection( b.range );
755
-
756
- if ( common ) {
757
- // If this operation is more important, we also want to apply change to the part of the
758
- // original range that has already been changed by the other operation. Since that range
759
- // got changed we also have to update `oldValue`.
760
- if ( context.aIsStrong ) {
761
- operations.push( new AttributeOperation( common, b.key, b.newValue, a.newValue, 0 ) );
762
- }
763
- }
764
-
765
- if ( operations.length == 0 ) {
766
- return [ new NoOperation( 0 ) ];
767
- }
768
-
769
- return operations;
770
- } else {
771
- // If operations don't conflict, simply return an array containing just a clone of this operation.
772
- return [ a ];
773
- }
774
- } );
775
-
776
- setTransformation( AttributeOperation, InsertOperation, ( a, b ) => {
777
- // Case 1:
778
- //
779
- // The attribute operation range includes the position where nodes were inserted.
780
- // There are two possible scenarios: the inserted nodes were text and they should receive attributes or
781
- // the inserted nodes were elements and they should not receive attributes.
782
- //
783
- if ( a.range.start.hasSameParentAs( b.position ) && a.range.containsPosition( b.position ) ) {
784
- // If new nodes should not receive attributes, two separated ranges will be returned.
785
- // Otherwise, one expanded range will be returned.
786
- const range = a.range._getTransformedByInsertion( b.position, b.howMany, !b.shouldReceiveAttributes );
787
- const result = range.map( r => {
788
- return new AttributeOperation( r, a.key, a.oldValue, a.newValue, a.baseVersion );
789
- } );
790
-
791
- if ( b.shouldReceiveAttributes ) {
792
- // `AttributeOperation#range` includes some newly inserted text.
793
- // The operation should also change the attribute of that text. An example:
794
- //
795
- // Bold should be applied on the following range:
796
- // <p>Fo[zb]ar</p>
797
- //
798
- // In meantime, new text is typed:
799
- // <p>Fozxxbar</p>
800
- //
801
- // Bold should be applied also on the new text:
802
- // <p>Fo[zxxb]ar</p>
803
- // <p>Fo<$text bold="true">zxxb</$text>ar</p>
804
- //
805
- // There is a special case to consider here to consider.
806
- //
807
- // Consider setting an attribute with multiple possible values, for example `highlight`. The inserted text might
808
- // have already an attribute value applied and the `oldValue` property of the attribute operation might be wrong:
809
- //
810
- // Attribute `highlight="yellow"` should be applied on the following range:
811
- // <p>Fo[zb]ar<p>
812
- //
813
- // In meantime, character `x` with `highlight="red"` is typed:
814
- // <p>Fo[z<$text highlight="red">x</$text>b]ar</p>
815
- //
816
- // In this case we cannot simply apply operation changing the attribute value from `null` to `"yellow"` for the whole range
817
- // because that would lead to an exception (`oldValue` is incorrect for `x`).
818
- //
819
- // We also cannot break the original range as this would mess up a scenario when there are multiple following
820
- // insert operations, because then only the first inserted character is included in those ranges:
821
- // <p>Fo[z][x][b]ar</p> --> <p>Fo[z][x]x[b]ar</p> --> <p>Fo[z][x]xx[b]ar</p>
822
- //
823
- // So, the attribute range needs be expanded, no matter what attributes are set on the inserted nodes:
824
- //
825
- // <p>Fo[z<$text highlight="red">x</$text>b]ar</p> <--- Change from `null` to `yellow`, throwing an exception.
826
- //
827
- // But before that operation would be applied, we will add an additional attribute operation that will change
828
- // attributes on the inserted nodes in a way which would make the original operation correct:
829
- //
830
- // <p>Fo[z{<$text highlight="red">}x</$text>b]ar</p> <--- Change range `{}` from `red` to `null`.
831
- // <p>Fo[zxb]ar</p> <--- Now change from `null` to `yellow` is completely fine.
832
- //
833
-
834
- // Generate complementary attribute operation. Be sure to add it before the original operation.
835
- const op = _getComplementaryAttributeOperations( b, a.key, a.oldValue );
836
-
837
- if ( op ) {
838
- result.unshift( op );
839
- }
840
- }
841
-
842
- // If nodes should not receive new attribute, we are done here.
843
- return result;
844
- }
845
-
846
- // If insert operation is not expanding the attribute operation range, simply transform the range.
847
- a.range = a.range._getTransformedByInsertion( b.position, b.howMany, false )[ 0 ];
848
-
849
- return [ a ];
850
- } );
851
-
626
+ setTransformation(AttributeOperation, AttributeOperation, (a, b, context) => {
627
+ // If operations in conflict, check if their ranges intersect and manage them properly.
628
+ //
629
+ // Operations can be in conflict only if:
630
+ //
631
+ // * their key is the same (they change the same attribute), and
632
+ // * they are in the same parent (operations for ranges [ 1 ] - [ 3 ] and [ 2, 0 ] - [ 2, 5 ] change different
633
+ // elements and can't be in conflict).
634
+ if (a.key === b.key && a.range.start.hasSameParentAs(b.range.start)) {
635
+ // First, we want to apply change to the part of a range that has not been changed by the other operation.
636
+ const operations = a.range.getDifference(b.range).map(range => {
637
+ return new AttributeOperation(range, a.key, a.oldValue, a.newValue, 0);
638
+ });
639
+ // Then we take care of the common part of ranges.
640
+ const common = a.range.getIntersection(b.range);
641
+ if (common) {
642
+ // If this operation is more important, we also want to apply change to the part of the
643
+ // original range that has already been changed by the other operation. Since that range
644
+ // got changed we also have to update `oldValue`.
645
+ if (context.aIsStrong) {
646
+ operations.push(new AttributeOperation(common, b.key, b.newValue, a.newValue, 0));
647
+ }
648
+ }
649
+ if (operations.length == 0) {
650
+ return [new NoOperation(0)];
651
+ }
652
+ return operations;
653
+ }
654
+ else {
655
+ // If operations don't conflict, simply return an array containing just a clone of this operation.
656
+ return [a];
657
+ }
658
+ });
659
+ setTransformation(AttributeOperation, InsertOperation, (a, b) => {
660
+ // Case 1:
661
+ //
662
+ // The attribute operation range includes the position where nodes were inserted.
663
+ // There are two possible scenarios: the inserted nodes were text and they should receive attributes or
664
+ // the inserted nodes were elements and they should not receive attributes.
665
+ //
666
+ if (a.range.start.hasSameParentAs(b.position) && a.range.containsPosition(b.position)) {
667
+ // If new nodes should not receive attributes, two separated ranges will be returned.
668
+ // Otherwise, one expanded range will be returned.
669
+ const range = a.range._getTransformedByInsertion(b.position, b.howMany, !b.shouldReceiveAttributes);
670
+ const result = range.map(r => {
671
+ return new AttributeOperation(r, a.key, a.oldValue, a.newValue, a.baseVersion);
672
+ });
673
+ if (b.shouldReceiveAttributes) {
674
+ // `AttributeOperation#range` includes some newly inserted text.
675
+ // The operation should also change the attribute of that text. An example:
676
+ //
677
+ // Bold should be applied on the following range:
678
+ // <p>Fo[zb]ar</p>
679
+ //
680
+ // In meantime, new text is typed:
681
+ // <p>Fozxxbar</p>
682
+ //
683
+ // Bold should be applied also on the new text:
684
+ // <p>Fo[zxxb]ar</p>
685
+ // <p>Fo<$text bold="true">zxxb</$text>ar</p>
686
+ //
687
+ // There is a special case to consider here to consider.
688
+ //
689
+ // Consider setting an attribute with multiple possible values, for example `highlight`. The inserted text might
690
+ // have already an attribute value applied and the `oldValue` property of the attribute operation might be wrong:
691
+ //
692
+ // Attribute `highlight="yellow"` should be applied on the following range:
693
+ // <p>Fo[zb]ar<p>
694
+ //
695
+ // In meantime, character `x` with `highlight="red"` is typed:
696
+ // <p>Fo[z<$text highlight="red">x</$text>b]ar</p>
697
+ //
698
+ // In this case we cannot simply apply operation changing the attribute value from `null` to `"yellow"` for the whole range
699
+ // because that would lead to an exception (`oldValue` is incorrect for `x`).
700
+ //
701
+ // We also cannot break the original range as this would mess up a scenario when there are multiple following
702
+ // insert operations, because then only the first inserted character is included in those ranges:
703
+ // <p>Fo[z][x][b]ar</p> --> <p>Fo[z][x]x[b]ar</p> --> <p>Fo[z][x]xx[b]ar</p>
704
+ //
705
+ // So, the attribute range needs be expanded, no matter what attributes are set on the inserted nodes:
706
+ //
707
+ // <p>Fo[z<$text highlight="red">x</$text>b]ar</p> <--- Change from `null` to `yellow`, throwing an exception.
708
+ //
709
+ // But before that operation would be applied, we will add an additional attribute operation that will change
710
+ // attributes on the inserted nodes in a way which would make the original operation correct:
711
+ //
712
+ // <p>Fo[z{<$text highlight="red">}x</$text>b]ar</p> <--- Change range `{}` from `red` to `null`.
713
+ // <p>Fo[zxb]ar</p> <--- Now change from `null` to `yellow` is completely fine.
714
+ //
715
+ // Generate complementary attribute operation. Be sure to add it before the original operation.
716
+ const op = _getComplementaryAttributeOperations(b, a.key, a.oldValue);
717
+ if (op) {
718
+ result.unshift(op);
719
+ }
720
+ }
721
+ // If nodes should not receive new attribute, we are done here.
722
+ return result;
723
+ }
724
+ // If insert operation is not expanding the attribute operation range, simply transform the range.
725
+ a.range = a.range._getTransformedByInsertion(b.position, b.howMany, false)[0];
726
+ return [a];
727
+ });
852
728
  /**
853
729
  * Helper function for `AttributeOperation` x `InsertOperation` (and reverse) transformation.
854
730
  *
@@ -861,55 +737,43 @@ setTransformation( AttributeOperation, InsertOperation, ( a, b ) => {
861
737
  * @param {*} newValue
862
738
  * @returns {module:engine/model/operation/attributeoperation~AttributeOperation|null}
863
739
  */
864
- function _getComplementaryAttributeOperations( insertOperation, key, newValue ) {
865
- const nodes = insertOperation.nodes;
866
-
867
- // At the beginning we store the attribute value from the first node.
868
- const insertValue = nodes.getNode( 0 ).getAttribute( key );
869
-
870
- if ( insertValue == newValue ) {
871
- return null;
872
- }
873
-
874
- const range = new Range( insertOperation.position, insertOperation.position.getShiftedBy( insertOperation.howMany ) );
875
-
876
- return new AttributeOperation( range, key, insertValue, newValue, 0 );
740
+ function _getComplementaryAttributeOperations(insertOperation, key, newValue) {
741
+ const nodes = insertOperation.nodes;
742
+ // At the beginning we store the attribute value from the first node.
743
+ const insertValue = nodes.getNode(0).getAttribute(key);
744
+ if (insertValue == newValue) {
745
+ return null;
746
+ }
747
+ const range = new Range(insertOperation.position, insertOperation.position.getShiftedBy(insertOperation.howMany));
748
+ return new AttributeOperation(range, key, insertValue, newValue, 0);
877
749
  }
878
-
879
- setTransformation( AttributeOperation, MergeOperation, ( a, b ) => {
880
- const ranges = [];
881
-
882
- // Case 1:
883
- //
884
- // Attribute change on the merged element. In this case, the merged element was moved to the graveyard.
885
- // An additional attribute operation that will change the (re)moved element needs to be generated.
886
- //
887
- if ( a.range.start.hasSameParentAs( b.deletionPosition ) ) {
888
- if ( a.range.containsPosition( b.deletionPosition ) || a.range.start.isEqual( b.deletionPosition ) ) {
889
- ranges.push( Range._createFromPositionAndShift( b.graveyardPosition, 1 ) );
890
- }
891
- }
892
-
893
- const range = a.range._getTransformedByMergeOperation( b );
894
-
895
- // Do not add empty (collapsed) ranges to the result. `range` may be collapsed if it contained only the merged element.
896
- if ( !range.isCollapsed ) {
897
- ranges.push( range );
898
- }
899
-
900
- // Create `AttributeOperation`s out of the ranges.
901
- return ranges.map( range => {
902
- return new AttributeOperation( range, a.key, a.oldValue, a.newValue, a.baseVersion );
903
- } );
904
- } );
905
-
906
- setTransformation( AttributeOperation, MoveOperation, ( a, b ) => {
907
- const ranges = _breakRangeByMoveOperation( a.range, b );
908
-
909
- // Create `AttributeOperation`s out of the ranges.
910
- return ranges.map( range => new AttributeOperation( range, a.key, a.oldValue, a.newValue, a.baseVersion ) );
911
- } );
912
-
750
+ setTransformation(AttributeOperation, MergeOperation, (a, b) => {
751
+ const ranges = [];
752
+ // Case 1:
753
+ //
754
+ // Attribute change on the merged element. In this case, the merged element was moved to the graveyard.
755
+ // An additional attribute operation that will change the (re)moved element needs to be generated.
756
+ //
757
+ if (a.range.start.hasSameParentAs(b.deletionPosition)) {
758
+ if (a.range.containsPosition(b.deletionPosition) || a.range.start.isEqual(b.deletionPosition)) {
759
+ ranges.push(Range._createFromPositionAndShift(b.graveyardPosition, 1));
760
+ }
761
+ }
762
+ const range = a.range._getTransformedByMergeOperation(b);
763
+ // Do not add empty (collapsed) ranges to the result. `range` may be collapsed if it contained only the merged element.
764
+ if (!range.isCollapsed) {
765
+ ranges.push(range);
766
+ }
767
+ // Create `AttributeOperation`s out of the ranges.
768
+ return ranges.map(range => {
769
+ return new AttributeOperation(range, a.key, a.oldValue, a.newValue, a.baseVersion);
770
+ });
771
+ });
772
+ setTransformation(AttributeOperation, MoveOperation, (a, b) => {
773
+ const ranges = _breakRangeByMoveOperation(a.range, b);
774
+ // Create `AttributeOperation`s out of the ranges.
775
+ return ranges.map(range => new AttributeOperation(range, a.key, a.oldValue, a.newValue, a.baseVersion));
776
+ });
913
777
  // Helper function for `AttributeOperation` x `MoveOperation` transformation.
914
778
  //
915
779
  // Takes the passed `range` and transforms it by move operation `moveOp` in a specific way. Only top-level nodes of `range`
@@ -924,1415 +788,1190 @@ setTransformation( AttributeOperation, MoveOperation, ( a, b ) => {
924
788
  // @param {module:engine/model/range~Range} range
925
789
  // @param {module:engine/model/operation/moveoperation~MoveOperation} moveOp
926
790
  // @returns {Array.<module:engine/model/range~Range>}
927
- function _breakRangeByMoveOperation( range, moveOp ) {
928
- const moveRange = Range._createFromPositionAndShift( moveOp.sourcePosition, moveOp.howMany );
929
-
930
- // We are transforming `range` (original range) by `moveRange` (range moved by move operation). As usual when it comes to
931
- // transforming a ranges, we may have a common part of the ranges and we may have a difference part (zero to two ranges).
932
- let common = null;
933
- let difference = [];
934
-
935
- // Let's compare the ranges.
936
- if ( moveRange.containsRange( range, true ) ) {
937
- // If the whole original range is moved, treat it whole as a common part. There's also no difference part.
938
- common = range;
939
- } else if ( range.start.hasSameParentAs( moveRange.start ) ) {
940
- // If the ranges are "on the same level" (in the same parent) then move operation may move exactly those nodes
941
- // that are changed by the attribute operation. In this case we get common part and difference part in the usual way.
942
- difference = range.getDifference( moveRange );
943
- common = range.getIntersection( moveRange );
944
- } else {
945
- // In any other situation we assume that original range is different than move range, that is that move operation
946
- // moves other nodes that attribute operation change. Even if the moved range is deep inside in the original range.
947
- //
948
- // Note that this is different than in `.getIntersection` (we would get a common part in that case) and different
949
- // than `.getDifference` (we would get two ranges).
950
- difference = [ range ];
951
- }
952
-
953
- const result = [];
954
-
955
- // The default behaviour of `_getTransformedByMove` might get wrong results for difference part, though, so
956
- // we do it by hand.
957
- for ( let diff of difference ) {
958
- // First, transform the range by removing moved nodes. Since this is a difference, this is safe, `null` won't be returned
959
- // as the range is different than the moved range.
960
- diff = diff._getTransformedByDeletion( moveOp.sourcePosition, moveOp.howMany );
961
-
962
- // Transform also `targetPosition`.
963
- const targetPosition = moveOp.getMovedRangeStart();
964
-
965
- // Spread the range only if moved nodes are inserted only between the top-level nodes of the `diff` range.
966
- const spread = diff.start.hasSameParentAs( targetPosition );
967
-
968
- // Transform by insertion of moved nodes.
969
- diff = diff._getTransformedByInsertion( targetPosition, moveOp.howMany, spread );
970
-
971
- result.push( ...diff );
972
- }
973
-
974
- // Common part can be simply transformed by the move operation. This is because move operation will not target to
975
- // that common part (the operation would have to target inside its own moved range).
976
- if ( common ) {
977
- result.push(
978
- common._getTransformedByMove( moveOp.sourcePosition, moveOp.targetPosition, moveOp.howMany, false )[ 0 ]
979
- );
980
- }
981
-
982
- return result;
791
+ function _breakRangeByMoveOperation(range, moveOp) {
792
+ const moveRange = Range._createFromPositionAndShift(moveOp.sourcePosition, moveOp.howMany);
793
+ // We are transforming `range` (original range) by `moveRange` (range moved by move operation). As usual when it comes to
794
+ // transforming a ranges, we may have a common part of the ranges and we may have a difference part (zero to two ranges).
795
+ let common = null;
796
+ let difference = [];
797
+ // Let's compare the ranges.
798
+ if (moveRange.containsRange(range, true)) {
799
+ // If the whole original range is moved, treat it whole as a common part. There's also no difference part.
800
+ common = range;
801
+ }
802
+ else if (range.start.hasSameParentAs(moveRange.start)) {
803
+ // If the ranges are "on the same level" (in the same parent) then move operation may move exactly those nodes
804
+ // that are changed by the attribute operation. In this case we get common part and difference part in the usual way.
805
+ difference = range.getDifference(moveRange);
806
+ common = range.getIntersection(moveRange);
807
+ }
808
+ else {
809
+ // In any other situation we assume that original range is different than move range, that is that move operation
810
+ // moves other nodes that attribute operation change. Even if the moved range is deep inside in the original range.
811
+ //
812
+ // Note that this is different than in `.getIntersection` (we would get a common part in that case) and different
813
+ // than `.getDifference` (we would get two ranges).
814
+ difference = [range];
815
+ }
816
+ const result = [];
817
+ // The default behaviour of `_getTransformedByMove` might get wrong results for difference part, though, so
818
+ // we do it by hand.
819
+ for (let diff of difference) {
820
+ // First, transform the range by removing moved nodes. Since this is a difference, this is safe, `null` won't be returned
821
+ // as the range is different than the moved range.
822
+ diff = diff._getTransformedByDeletion(moveOp.sourcePosition, moveOp.howMany);
823
+ // Transform also `targetPosition`.
824
+ const targetPosition = moveOp.getMovedRangeStart();
825
+ // Spread the range only if moved nodes are inserted only between the top-level nodes of the `diff` range.
826
+ const spread = diff.start.hasSameParentAs(targetPosition);
827
+ // Transform by insertion of moved nodes.
828
+ const diffs = diff._getTransformedByInsertion(targetPosition, moveOp.howMany, spread);
829
+ result.push(...diffs);
830
+ }
831
+ // Common part can be simply transformed by the move operation. This is because move operation will not target to
832
+ // that common part (the operation would have to target inside its own moved range).
833
+ if (common) {
834
+ result.push(common._getTransformedByMove(moveOp.sourcePosition, moveOp.targetPosition, moveOp.howMany, false)[0]);
835
+ }
836
+ return result;
983
837
  }
984
-
985
- setTransformation( AttributeOperation, SplitOperation, ( a, b ) => {
986
- // Case 1:
987
- //
988
- // Split node is the last node in `AttributeOperation#range`.
989
- // `AttributeOperation#range` needs to be expanded to include the new (split) node.
990
- //
991
- // Attribute `type` to be changed to `numbered` but the `listItem` is split.
992
- // <listItem type="bulleted">foobar</listItem>
993
- //
994
- // After split:
995
- // <listItem type="bulleted">foo</listItem><listItem type="bulleted">bar</listItem>
996
- //
997
- // After attribute change:
998
- // <listItem type="numbered">foo</listItem><listItem type="numbered">foo</listItem>
999
- //
1000
- if ( a.range.end.isEqual( b.insertionPosition ) ) {
1001
- if ( !b.graveyardPosition ) {
1002
- a.range.end.offset++;
1003
- }
1004
-
1005
- return [ a ];
1006
- }
1007
-
1008
- // Case 2:
1009
- //
1010
- // Split position is inside `AttributeOperation#range`, at the same level, so the nodes to change are
1011
- // not going to make a flat range.
1012
- //
1013
- // Content with range-to-change and split position:
1014
- // <p>Fo[zb^a]r</p>
1015
- //
1016
- // After split:
1017
- // <p>Fozb</p><p>ar</p>
1018
- //
1019
- // Make two separate ranges containing all nodes to change:
1020
- // <p>Fo[zb]</p><p>[a]r</p>
1021
- //
1022
- if ( a.range.start.hasSameParentAs( b.splitPosition ) && a.range.containsPosition( b.splitPosition ) ) {
1023
- const secondPart = a.clone();
1024
-
1025
- secondPart.range = new Range(
1026
- b.moveTargetPosition.clone(),
1027
- a.range.end._getCombined( b.splitPosition, b.moveTargetPosition )
1028
- );
1029
-
1030
- a.range.end = b.splitPosition.clone();
1031
- a.range.end.stickiness = 'toPrevious';
1032
-
1033
- return [ a, secondPart ];
1034
- }
1035
-
1036
- // The default case.
1037
- //
1038
- a.range = a.range._getTransformedBySplitOperation( b );
1039
-
1040
- return [ a ];
1041
- } );
1042
-
1043
- setTransformation( InsertOperation, AttributeOperation, ( a, b ) => {
1044
- const result = [ a ];
1045
-
1046
- // Case 1:
1047
- //
1048
- // The attribute operation range includes the position where nodes were inserted.
1049
- // There are two possible scenarios: the inserted nodes were text and they should receive attributes or
1050
- // the inserted nodes were elements and they should not receive attributes.
1051
- //
1052
- // This is a mirror scenario to the one described in `AttributeOperation` x `InsertOperation` transformation,
1053
- // although this case is a little less complicated. In this case we simply need to change attributes of the
1054
- // inserted nodes and that's it.
1055
- //
1056
- if ( a.shouldReceiveAttributes && a.position.hasSameParentAs( b.range.start ) && b.range.containsPosition( a.position ) ) {
1057
- const op = _getComplementaryAttributeOperations( a, b.key, b.newValue );
1058
-
1059
- if ( op ) {
1060
- result.push( op );
1061
- }
1062
- }
1063
-
1064
- // The default case is: do nothing.
1065
- // `AttributeOperation` does not change the model tree structure so `InsertOperation` does not need to be changed.
1066
- //
1067
- return result;
1068
- } );
1069
-
1070
- setTransformation( InsertOperation, InsertOperation, ( a, b, context ) => {
1071
- // Case 1:
1072
- //
1073
- // Two insert operations insert nodes at the same position. Since they are the same, it needs to be decided
1074
- // what will be the order of inserted nodes. However, there is no additional information to help in that
1075
- // decision. Also, when `b` will be transformed by `a`, the same order must be maintained.
1076
- //
1077
- // To achieve that, we will check if the operation is strong.
1078
- // If it is, it won't get transformed. If it is not, it will be moved.
1079
- //
1080
- if ( a.position.isEqual( b.position ) && context.aIsStrong ) {
1081
- return [ a ];
1082
- }
1083
-
1084
- // The default case.
1085
- //
1086
- a.position = a.position._getTransformedByInsertOperation( b );
1087
-
1088
- return [ a ];
1089
- } );
1090
-
1091
- setTransformation( InsertOperation, MoveOperation, ( a, b ) => {
1092
- // The default case.
1093
- //
1094
- a.position = a.position._getTransformedByMoveOperation( b );
1095
-
1096
- return [ a ];
1097
- } );
1098
-
1099
- setTransformation( InsertOperation, SplitOperation, ( a, b ) => {
1100
- // The default case.
1101
- //
1102
- a.position = a.position._getTransformedBySplitOperation( b );
1103
-
1104
- return [ a ];
1105
- } );
1106
-
1107
- setTransformation( InsertOperation, MergeOperation, ( a, b ) => {
1108
- a.position = a.position._getTransformedByMergeOperation( b );
1109
-
1110
- return [ a ];
1111
- } );
1112
-
838
+ setTransformation(AttributeOperation, SplitOperation, (a, b) => {
839
+ // Case 1:
840
+ //
841
+ // Split node is the last node in `AttributeOperation#range`.
842
+ // `AttributeOperation#range` needs to be expanded to include the new (split) node.
843
+ //
844
+ // Attribute `type` to be changed to `numbered` but the `listItem` is split.
845
+ // <listItem type="bulleted">foobar</listItem>
846
+ //
847
+ // After split:
848
+ // <listItem type="bulleted">foo</listItem><listItem type="bulleted">bar</listItem>
849
+ //
850
+ // After attribute change:
851
+ // <listItem type="numbered">foo</listItem><listItem type="numbered">foo</listItem>
852
+ //
853
+ if (a.range.end.isEqual(b.insertionPosition)) {
854
+ if (!b.graveyardPosition) {
855
+ a.range.end.offset++;
856
+ }
857
+ return [a];
858
+ }
859
+ // Case 2:
860
+ //
861
+ // Split position is inside `AttributeOperation#range`, at the same level, so the nodes to change are
862
+ // not going to make a flat range.
863
+ //
864
+ // Content with range-to-change and split position:
865
+ // <p>Fo[zb^a]r</p>
866
+ //
867
+ // After split:
868
+ // <p>Fozb</p><p>ar</p>
869
+ //
870
+ // Make two separate ranges containing all nodes to change:
871
+ // <p>Fo[zb]</p><p>[a]r</p>
872
+ //
873
+ if (a.range.start.hasSameParentAs(b.splitPosition) && a.range.containsPosition(b.splitPosition)) {
874
+ const secondPart = a.clone();
875
+ secondPart.range = new Range(b.moveTargetPosition.clone(), a.range.end._getCombined(b.splitPosition, b.moveTargetPosition));
876
+ a.range.end = b.splitPosition.clone();
877
+ a.range.end.stickiness = 'toPrevious';
878
+ return [a, secondPart];
879
+ }
880
+ // The default case.
881
+ //
882
+ a.range = a.range._getTransformedBySplitOperation(b);
883
+ return [a];
884
+ });
885
+ setTransformation(InsertOperation, AttributeOperation, (a, b) => {
886
+ const result = [a];
887
+ // Case 1:
888
+ //
889
+ // The attribute operation range includes the position where nodes were inserted.
890
+ // There are two possible scenarios: the inserted nodes were text and they should receive attributes or
891
+ // the inserted nodes were elements and they should not receive attributes.
892
+ //
893
+ // This is a mirror scenario to the one described in `AttributeOperation` x `InsertOperation` transformation,
894
+ // although this case is a little less complicated. In this case we simply need to change attributes of the
895
+ // inserted nodes and that's it.
896
+ //
897
+ if (a.shouldReceiveAttributes && a.position.hasSameParentAs(b.range.start) && b.range.containsPosition(a.position)) {
898
+ const op = _getComplementaryAttributeOperations(a, b.key, b.newValue);
899
+ if (op) {
900
+ result.push(op);
901
+ }
902
+ }
903
+ // The default case is: do nothing.
904
+ // `AttributeOperation` does not change the model tree structure so `InsertOperation` does not need to be changed.
905
+ //
906
+ return result;
907
+ });
908
+ setTransformation(InsertOperation, InsertOperation, (a, b, context) => {
909
+ // Case 1:
910
+ //
911
+ // Two insert operations insert nodes at the same position. Since they are the same, it needs to be decided
912
+ // what will be the order of inserted nodes. However, there is no additional information to help in that
913
+ // decision. Also, when `b` will be transformed by `a`, the same order must be maintained.
914
+ //
915
+ // To achieve that, we will check if the operation is strong.
916
+ // If it is, it won't get transformed. If it is not, it will be moved.
917
+ //
918
+ if (a.position.isEqual(b.position) && context.aIsStrong) {
919
+ return [a];
920
+ }
921
+ // The default case.
922
+ //
923
+ a.position = a.position._getTransformedByInsertOperation(b);
924
+ return [a];
925
+ });
926
+ setTransformation(InsertOperation, MoveOperation, (a, b) => {
927
+ // The default case.
928
+ //
929
+ a.position = a.position._getTransformedByMoveOperation(b);
930
+ return [a];
931
+ });
932
+ setTransformation(InsertOperation, SplitOperation, (a, b) => {
933
+ // The default case.
934
+ //
935
+ a.position = a.position._getTransformedBySplitOperation(b);
936
+ return [a];
937
+ });
938
+ setTransformation(InsertOperation, MergeOperation, (a, b) => {
939
+ a.position = a.position._getTransformedByMergeOperation(b);
940
+ return [a];
941
+ });
1113
942
  // -----------------------
1114
-
1115
- setTransformation( MarkerOperation, InsertOperation, ( a, b ) => {
1116
- if ( a.oldRange ) {
1117
- a.oldRange = a.oldRange._getTransformedByInsertOperation( b )[ 0 ];
1118
- }
1119
-
1120
- if ( a.newRange ) {
1121
- a.newRange = a.newRange._getTransformedByInsertOperation( b )[ 0 ];
1122
- }
1123
-
1124
- return [ a ];
1125
- } );
1126
-
1127
- setTransformation( MarkerOperation, MarkerOperation, ( a, b, context ) => {
1128
- if ( a.name == b.name ) {
1129
- if ( context.aIsStrong ) {
1130
- a.oldRange = b.newRange ? b.newRange.clone() : null;
1131
- } else {
1132
- return [ new NoOperation( 0 ) ];
1133
- }
1134
- }
1135
-
1136
- return [ a ];
1137
- } );
1138
-
1139
- setTransformation( MarkerOperation, MergeOperation, ( a, b ) => {
1140
- if ( a.oldRange ) {
1141
- a.oldRange = a.oldRange._getTransformedByMergeOperation( b );
1142
- }
1143
-
1144
- if ( a.newRange ) {
1145
- a.newRange = a.newRange._getTransformedByMergeOperation( b );
1146
- }
1147
-
1148
- return [ a ];
1149
- } );
1150
-
1151
- setTransformation( MarkerOperation, MoveOperation, ( a, b, context ) => {
1152
- if ( a.oldRange ) {
1153
- a.oldRange = Range._createFromRanges( a.oldRange._getTransformedByMoveOperation( b ) );
1154
- }
1155
-
1156
- if ( a.newRange ) {
1157
- if ( context.abRelation ) {
1158
- const aNewRange = Range._createFromRanges( a.newRange._getTransformedByMoveOperation( b ) );
1159
-
1160
- if ( context.abRelation.side == 'left' && b.targetPosition.isEqual( a.newRange.start ) ) {
1161
- a.newRange.start.path = context.abRelation.path;
1162
- a.newRange.end = aNewRange.end;
1163
-
1164
- return [ a ];
1165
- } else if ( context.abRelation.side == 'right' && b.targetPosition.isEqual( a.newRange.end ) ) {
1166
- a.newRange.start = aNewRange.start;
1167
- a.newRange.end.path = context.abRelation.path;
1168
-
1169
- return [ a ];
1170
- }
1171
- }
1172
-
1173
- a.newRange = Range._createFromRanges( a.newRange._getTransformedByMoveOperation( b ) );
1174
- }
1175
-
1176
- return [ a ];
1177
- } );
1178
-
1179
- setTransformation( MarkerOperation, SplitOperation, ( a, b, context ) => {
1180
- if ( a.oldRange ) {
1181
- a.oldRange = a.oldRange._getTransformedBySplitOperation( b );
1182
- }
1183
-
1184
- if ( a.newRange ) {
1185
- if ( context.abRelation ) {
1186
- const aNewRange = a.newRange._getTransformedBySplitOperation( b );
1187
-
1188
- if ( a.newRange.start.isEqual( b.splitPosition ) && context.abRelation.wasStartBeforeMergedElement ) {
1189
- a.newRange.start = Position._createAt( b.insertionPosition );
1190
- } else if ( a.newRange.start.isEqual( b.splitPosition ) && !context.abRelation.wasInLeftElement ) {
1191
- a.newRange.start = Position._createAt( b.moveTargetPosition );
1192
- }
1193
-
1194
- if ( a.newRange.end.isEqual( b.splitPosition ) && context.abRelation.wasInRightElement ) {
1195
- a.newRange.end = Position._createAt( b.moveTargetPosition );
1196
- } else if ( a.newRange.end.isEqual( b.splitPosition ) && context.abRelation.wasEndBeforeMergedElement ) {
1197
- a.newRange.end = Position._createAt( b.insertionPosition );
1198
- } else {
1199
- a.newRange.end = aNewRange.end;
1200
- }
1201
-
1202
- return [ a ];
1203
- }
1204
-
1205
- a.newRange = a.newRange._getTransformedBySplitOperation( b );
1206
- }
1207
-
1208
- return [ a ];
1209
- } );
1210
-
943
+ setTransformation(MarkerOperation, InsertOperation, (a, b) => {
944
+ if (a.oldRange) {
945
+ a.oldRange = a.oldRange._getTransformedByInsertOperation(b)[0];
946
+ }
947
+ if (a.newRange) {
948
+ a.newRange = a.newRange._getTransformedByInsertOperation(b)[0];
949
+ }
950
+ return [a];
951
+ });
952
+ setTransformation(MarkerOperation, MarkerOperation, (a, b, context) => {
953
+ if (a.name == b.name) {
954
+ if (context.aIsStrong) {
955
+ a.oldRange = b.newRange ? b.newRange.clone() : null;
956
+ }
957
+ else {
958
+ return [new NoOperation(0)];
959
+ }
960
+ }
961
+ return [a];
962
+ });
963
+ setTransformation(MarkerOperation, MergeOperation, (a, b) => {
964
+ if (a.oldRange) {
965
+ a.oldRange = a.oldRange._getTransformedByMergeOperation(b);
966
+ }
967
+ if (a.newRange) {
968
+ a.newRange = a.newRange._getTransformedByMergeOperation(b);
969
+ }
970
+ return [a];
971
+ });
972
+ setTransformation(MarkerOperation, MoveOperation, (a, b, context) => {
973
+ if (a.oldRange) {
974
+ a.oldRange = Range._createFromRanges(a.oldRange._getTransformedByMoveOperation(b));
975
+ }
976
+ if (a.newRange) {
977
+ if (context.abRelation) {
978
+ const aNewRange = Range._createFromRanges(a.newRange._getTransformedByMoveOperation(b));
979
+ if (context.abRelation.side == 'left' && b.targetPosition.isEqual(a.newRange.start)) {
980
+ a.newRange.end = aNewRange.end;
981
+ a.newRange.start.path = context.abRelation.path;
982
+ return [a];
983
+ }
984
+ else if (context.abRelation.side == 'right' && b.targetPosition.isEqual(a.newRange.end)) {
985
+ a.newRange.start = aNewRange.start;
986
+ a.newRange.end.path = context.abRelation.path;
987
+ return [a];
988
+ }
989
+ }
990
+ a.newRange = Range._createFromRanges(a.newRange._getTransformedByMoveOperation(b));
991
+ }
992
+ return [a];
993
+ });
994
+ setTransformation(MarkerOperation, SplitOperation, (a, b, context) => {
995
+ if (a.oldRange) {
996
+ a.oldRange = a.oldRange._getTransformedBySplitOperation(b);
997
+ }
998
+ if (a.newRange) {
999
+ if (context.abRelation) {
1000
+ const aNewRange = a.newRange._getTransformedBySplitOperation(b);
1001
+ if (a.newRange.start.isEqual(b.splitPosition) && context.abRelation.wasStartBeforeMergedElement) {
1002
+ a.newRange.start = Position._createAt(b.insertionPosition);
1003
+ }
1004
+ else if (a.newRange.start.isEqual(b.splitPosition) && !context.abRelation.wasInLeftElement) {
1005
+ a.newRange.start = Position._createAt(b.moveTargetPosition);
1006
+ }
1007
+ if (a.newRange.end.isEqual(b.splitPosition) && context.abRelation.wasInRightElement) {
1008
+ a.newRange.end = Position._createAt(b.moveTargetPosition);
1009
+ }
1010
+ else if (a.newRange.end.isEqual(b.splitPosition) && context.abRelation.wasEndBeforeMergedElement) {
1011
+ a.newRange.end = Position._createAt(b.insertionPosition);
1012
+ }
1013
+ else {
1014
+ a.newRange.end = aNewRange.end;
1015
+ }
1016
+ return [a];
1017
+ }
1018
+ a.newRange = a.newRange._getTransformedBySplitOperation(b);
1019
+ }
1020
+ return [a];
1021
+ });
1211
1022
  // -----------------------
1212
-
1213
- setTransformation( MergeOperation, InsertOperation, ( a, b ) => {
1214
- if ( a.sourcePosition.hasSameParentAs( b.position ) ) {
1215
- a.howMany += b.howMany;
1216
- }
1217
-
1218
- a.sourcePosition = a.sourcePosition._getTransformedByInsertOperation( b );
1219
- a.targetPosition = a.targetPosition._getTransformedByInsertOperation( b );
1220
-
1221
- return [ a ];
1222
- } );
1223
-
1224
- setTransformation( MergeOperation, MergeOperation, ( a, b, context ) => {
1225
- // Case 1:
1226
- //
1227
- // Same merge operations.
1228
- //
1229
- // Both operations have same source and target positions. So the element already got merged and there is
1230
- // theoretically nothing to do.
1231
- //
1232
- if ( a.sourcePosition.isEqual( b.sourcePosition ) && a.targetPosition.isEqual( b.targetPosition ) ) {
1233
- // There are two ways that we can provide a do-nothing operation.
1234
- //
1235
- // First is simply a NoOperation instance. We will use it if `b` operation was not undone.
1236
- //
1237
- // Second is a merge operation that has the source operation in the merged element - in the graveyard -
1238
- // same target position and `howMany` equal to `0`. So it is basically merging an empty element from graveyard
1239
- // which is almost the same as NoOperation.
1240
- //
1241
- // This way the merge operation can be later transformed by split operation
1242
- // to provide correct undo. This will be used if `b` operation was undone (only then it is correct).
1243
- //
1244
- if ( !context.bWasUndone ) {
1245
- return [ new NoOperation( 0 ) ];
1246
- } else {
1247
- const path = b.graveyardPosition.path.slice();
1248
- path.push( 0 );
1249
-
1250
- a.sourcePosition = new Position( b.graveyardPosition.root, path );
1251
- a.howMany = 0;
1252
-
1253
- return [ a ];
1254
- }
1255
- }
1256
-
1257
- // Case 2:
1258
- //
1259
- // Same merge source position but different target position.
1260
- //
1261
- // This can happen during collaboration. For example, if one client merged a paragraph to the previous paragraph
1262
- // and the other person removed that paragraph and merged the same paragraph to something before:
1263
- //
1264
- // Client A:
1265
- // <p>Foo</p><p>Bar</p><p>[]Xyz</p>
1266
- // <p>Foo</p><p>BarXyz</p>
1267
- //
1268
- // Client B:
1269
- // <p>Foo</p>[<p>Bar</p>]<p>Xyz</p>
1270
- // <p>Foo</p><p>[]Xyz</p>
1271
- // <p>FooXyz</p>
1272
- //
1273
- // In this case we need to decide where finally "Xyz" will land:
1274
- //
1275
- // <p>FooXyz</p> graveyard: <p>Bar</p>
1276
- // <p>Foo</p> graveyard: <p>BarXyz</p>
1277
- //
1278
- // Let's move it in a way so that a merge operation that does not target to graveyard is more important so that
1279
- // nodes does not end up in the graveyard. It makes sense. Both for Client A and for Client B "Xyz" finally did not
1280
- // end up in the graveyard (see above).
1281
- //
1282
- // If neither or both operations point to graveyard, then let `aIsStrong` decide.
1283
- //
1284
- if (
1285
- a.sourcePosition.isEqual( b.sourcePosition ) && !a.targetPosition.isEqual( b.targetPosition ) &&
1286
- !context.bWasUndone && context.abRelation != 'splitAtSource'
1287
- ) {
1288
- const aToGraveyard = a.targetPosition.root.rootName == '$graveyard';
1289
- const bToGraveyard = b.targetPosition.root.rootName == '$graveyard';
1290
-
1291
- // If `aIsWeak` it means that `a` points to graveyard while `b` doesn't. Don't move nodes then.
1292
- const aIsWeak = aToGraveyard && !bToGraveyard;
1293
-
1294
- // If `bIsWeak` it means that `b` points to graveyard while `a` doesn't. Force moving nodes then.
1295
- const bIsWeak = bToGraveyard && !aToGraveyard;
1296
-
1297
- // Force move if `b` is weak or neither operation is weak but `a` is stronger through `context.aIsStrong`.
1298
- const forceMove = bIsWeak || ( !aIsWeak && context.aIsStrong );
1299
-
1300
- if ( forceMove ) {
1301
- const sourcePosition = b.targetPosition._getTransformedByMergeOperation( b );
1302
- const targetPosition = a.targetPosition._getTransformedByMergeOperation( b );
1303
-
1304
- return [ new MoveOperation( sourcePosition, a.howMany, targetPosition, 0 ) ];
1305
- } else {
1306
- return [ new NoOperation( 0 ) ];
1307
- }
1308
- }
1309
-
1310
- // The default case.
1311
- //
1312
- if ( a.sourcePosition.hasSameParentAs( b.targetPosition ) ) {
1313
- a.howMany += b.howMany;
1314
- }
1315
-
1316
- a.sourcePosition = a.sourcePosition._getTransformedByMergeOperation( b );
1317
- a.targetPosition = a.targetPosition._getTransformedByMergeOperation( b );
1318
-
1319
- // Handle positions in graveyard.
1320
- // If graveyard positions are same and `a` operation is strong - do not transform.
1321
- if ( !a.graveyardPosition.isEqual( b.graveyardPosition ) || !context.aIsStrong ) {
1322
- a.graveyardPosition = a.graveyardPosition._getTransformedByMergeOperation( b );
1323
- }
1324
-
1325
- return [ a ];
1326
- } );
1327
-
1328
- setTransformation( MergeOperation, MoveOperation, ( a, b, context ) => {
1329
- // Case 1:
1330
- //
1331
- // The element to merge got removed.
1332
- //
1333
- // Merge operation does support merging elements which are not siblings. So it would not be a problem
1334
- // from technical point of view. However, if the element was removed, the intention of the user deleting it
1335
- // was to have it all deleted, together with its children. From user experience point of view, moving back the
1336
- // removed nodes might be unexpected. This means that in this scenario we will block the merging.
1337
- //
1338
- // The exception of this rule would be if the remove operation was later undone.
1339
- //
1340
- const removedRange = Range._createFromPositionAndShift( b.sourcePosition, b.howMany );
1341
-
1342
- if ( b.type == 'remove' && !context.bWasUndone && !context.forceWeakRemove ) {
1343
- if ( a.deletionPosition.hasSameParentAs( b.sourcePosition ) && removedRange.containsPosition( a.sourcePosition ) ) {
1344
- return [ new NoOperation( 0 ) ];
1345
- }
1346
- }
1347
-
1348
- // The default case.
1349
- //
1350
- if ( a.sourcePosition.hasSameParentAs( b.targetPosition ) ) {
1351
- a.howMany += b.howMany;
1352
- }
1353
-
1354
- if ( a.sourcePosition.hasSameParentAs( b.sourcePosition ) ) {
1355
- a.howMany -= b.howMany;
1356
- }
1357
-
1358
- a.sourcePosition = a.sourcePosition._getTransformedByMoveOperation( b );
1359
- a.targetPosition = a.targetPosition._getTransformedByMoveOperation( b );
1360
-
1361
- // `MergeOperation` graveyard position is like `MoveOperation` target position. It is a position where element(s) will
1362
- // be moved. Like in other similar cases, we need to consider the scenario when those positions are same.
1363
- // Here, we will treat `MergeOperation` like it is always strong (see `InsertOperation` x `InsertOperation` for comparison).
1364
- // This means that we won't transform graveyard position if it is equal to move operation target position.
1365
- if ( !a.graveyardPosition.isEqual( b.targetPosition ) ) {
1366
- a.graveyardPosition = a.graveyardPosition._getTransformedByMoveOperation( b );
1367
- }
1368
-
1369
- return [ a ];
1370
- } );
1371
-
1372
- setTransformation( MergeOperation, SplitOperation, ( a, b, context ) => {
1373
- if ( b.graveyardPosition ) {
1374
- // If `b` operation defines graveyard position, a node from graveyard will be moved. This means that we need to
1375
- // transform `a.graveyardPosition` accordingly.
1376
- a.graveyardPosition = a.graveyardPosition._getTransformedByDeletion( b.graveyardPosition, 1 );
1377
-
1378
- // This is a scenario foreseen in `MergeOperation` x `MergeOperation`, with two identical merge operations.
1379
- //
1380
- // So, there was `MergeOperation` x `MergeOperation` transformation earlier. Now, `a` is a merge operation which
1381
- // source position is in graveyard. Interestingly, split operation wants to use the node to be merged by `a`. This
1382
- // means that `b` is undoing that merge operation from earlier, which caused `a` to be in graveyard.
1383
- //
1384
- // If that's the case, at this point, we will only "fix" `a.howMany`. It was earlier set to `0` in
1385
- // `MergeOperation` x `MergeOperation` transformation. Later transformations in this function will change other
1386
- // properties.
1387
- //
1388
- if ( a.deletionPosition.isEqual( b.graveyardPosition ) ) {
1389
- a.howMany = b.howMany;
1390
- }
1391
- }
1392
-
1393
- // Case 1:
1394
- //
1395
- // Merge operation moves nodes to the place where split happens.
1396
- // This is a classic situation when there are two paragraphs, and there is a split (enter) after the first
1397
- // paragraph and there is a merge (delete) at the beginning of the second paragraph:
1398
- //
1399
- // <p>Foo{}</p><p>[]Bar</p>.
1400
- //
1401
- // Split is after `Foo`, while merge is from `Bar` to the end of `Foo`.
1402
- //
1403
- // State after split:
1404
- // <p>Foo</p><p></p><p>Bar</p>
1405
- //
1406
- // Now, `Bar` should be merged to the new paragraph:
1407
- // <p>Foo</p><p>Bar</p>
1408
- //
1409
- // Instead of merging it to the original paragraph:
1410
- // <p>FooBar</p><p></p>
1411
- //
1412
- // This means that `targetPosition` needs to be transformed. This is the default case though.
1413
- // For example, if the split would be after `F`, `targetPosition` should also be transformed.
1414
- //
1415
- // There are three exceptions, though, when we want to keep `targetPosition` as it was.
1416
- //
1417
- // First exception is when the merge target position is inside an element (not at the end, as usual). This
1418
- // happens when the merge operation earlier was transformed by "the same" merge operation. If merge operation
1419
- // targets inside the element we want to keep the original target position (and not transform it) because
1420
- // we have additional context telling us that we want to merge to the original element. We can check if the
1421
- // merge operation points inside element by checking what is `SplitOperation#howMany`. Since merge target position
1422
- // is same as split position, if `howMany` is non-zero, it means that the merge target position is inside an element.
1423
- //
1424
- // Second exception is when the element to merge is in the graveyard and split operation uses it. In that case
1425
- // if target position would be transformed, the merge operation would target at the source position:
1426
- //
1427
- // root: <p>Foo</p> graveyard: <p></p>
1428
- //
1429
- // SplitOperation: root [ 0, 3 ] using graveyard [ 0 ] (howMany = 0)
1430
- // MergeOperation: graveyard [ 0, 0 ] -> root [ 0, 3 ] (howMany = 0)
1431
- //
1432
- // Since split operation moves the graveyard node back to the root, the merge operation source position changes.
1433
- // We would like to merge from the empty <p> to the "Foo" <p>:
1434
- //
1435
- // root: <p>Foo</p><p></p> graveyard:
1436
- //
1437
- // MergeOperation#sourcePosition = root [ 1, 0 ]
1438
- //
1439
- // If `targetPosition` is transformed, it would become root [ 1, 0 ] as well. It has to be kept as it was.
1440
- //
1441
- // Third exception is connected with relations. If this happens during undo and we have explicit information
1442
- // that target position has not been affected by the operation which is undone by this split then this split should
1443
- // not move the target position either.
1444
- //
1445
- if ( a.targetPosition.isEqual( b.splitPosition ) ) {
1446
- const mergeInside = b.howMany != 0;
1447
- const mergeSplittingElement = b.graveyardPosition && a.deletionPosition.isEqual( b.graveyardPosition );
1448
-
1449
- if ( mergeInside || mergeSplittingElement || context.abRelation == 'mergeTargetNotMoved' ) {
1450
- a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation( b );
1451
-
1452
- return [ a ];
1453
- }
1454
- }
1455
-
1456
- // Case 2:
1457
- //
1458
- // Merge source is at the same position as split position. This sometimes happen, mostly during undo.
1459
- // The decision here is mostly to choose whether merge source position should stay where it is (so it will be at the end of the
1460
- // split element) or should be move to the beginning of the new element.
1461
- //
1462
- if ( a.sourcePosition.isEqual( b.splitPosition ) ) {
1463
- // Use context to check if `SplitOperation` is not undoing a merge operation, that didn't change the `a` operation.
1464
- // This scenario happens the undone merge operation moved nodes at the source position of `a` operation.
1465
- // In that case `a` operation source position should stay where it is.
1466
- if ( context.abRelation == 'mergeSourceNotMoved' ) {
1467
- a.howMany = 0;
1468
- a.targetPosition = a.targetPosition._getTransformedBySplitOperation( b );
1469
-
1470
- return [ a ];
1471
- }
1472
-
1473
- // This merge operation might have been earlier transformed by a merge operation which both merged the same element.
1474
- // See that case in `MergeOperation` x `MergeOperation` transformation. In that scenario, if the merge operation has been undone,
1475
- // the special case is not applied.
1476
- //
1477
- // Now, the merge operation is transformed by the split which has undone that previous merge operation.
1478
- // So now we are fixing situation which was skipped in `MergeOperation` x `MergeOperation` case.
1479
- //
1480
- if ( context.abRelation == 'mergeSameElement' || a.sourcePosition.offset > 0 ) {
1481
- a.sourcePosition = b.moveTargetPosition.clone();
1482
- a.targetPosition = a.targetPosition._getTransformedBySplitOperation( b );
1483
-
1484
- return [ a ];
1485
- }
1486
- }
1487
-
1488
- // The default case.
1489
- //
1490
- if ( a.sourcePosition.hasSameParentAs( b.splitPosition ) ) {
1491
- a.howMany = b.splitPosition.offset;
1492
- }
1493
-
1494
- a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation( b );
1495
- a.targetPosition = a.targetPosition._getTransformedBySplitOperation( b );
1496
-
1497
- return [ a ];
1498
- } );
1499
-
1023
+ setTransformation(MergeOperation, InsertOperation, (a, b) => {
1024
+ if (a.sourcePosition.hasSameParentAs(b.position)) {
1025
+ a.howMany += b.howMany;
1026
+ }
1027
+ a.sourcePosition = a.sourcePosition._getTransformedByInsertOperation(b);
1028
+ a.targetPosition = a.targetPosition._getTransformedByInsertOperation(b);
1029
+ return [a];
1030
+ });
1031
+ setTransformation(MergeOperation, MergeOperation, (a, b, context) => {
1032
+ // Case 1:
1033
+ //
1034
+ // Same merge operations.
1035
+ //
1036
+ // Both operations have same source and target positions. So the element already got merged and there is
1037
+ // theoretically nothing to do.
1038
+ //
1039
+ if (a.sourcePosition.isEqual(b.sourcePosition) && a.targetPosition.isEqual(b.targetPosition)) {
1040
+ // There are two ways that we can provide a do-nothing operation.
1041
+ //
1042
+ // First is simply a NoOperation instance. We will use it if `b` operation was not undone.
1043
+ //
1044
+ // Second is a merge operation that has the source operation in the merged element - in the graveyard -
1045
+ // same target position and `howMany` equal to `0`. So it is basically merging an empty element from graveyard
1046
+ // which is almost the same as NoOperation.
1047
+ //
1048
+ // This way the merge operation can be later transformed by split operation
1049
+ // to provide correct undo. This will be used if `b` operation was undone (only then it is correct).
1050
+ //
1051
+ if (!context.bWasUndone) {
1052
+ return [new NoOperation(0)];
1053
+ }
1054
+ else {
1055
+ const path = b.graveyardPosition.path.slice();
1056
+ path.push(0);
1057
+ a.sourcePosition = new Position(b.graveyardPosition.root, path);
1058
+ a.howMany = 0;
1059
+ return [a];
1060
+ }
1061
+ }
1062
+ // Case 2:
1063
+ //
1064
+ // Same merge source position but different target position.
1065
+ //
1066
+ // This can happen during collaboration. For example, if one client merged a paragraph to the previous paragraph
1067
+ // and the other person removed that paragraph and merged the same paragraph to something before:
1068
+ //
1069
+ // Client A:
1070
+ // <p>Foo</p><p>Bar</p><p>[]Xyz</p>
1071
+ // <p>Foo</p><p>BarXyz</p>
1072
+ //
1073
+ // Client B:
1074
+ // <p>Foo</p>[<p>Bar</p>]<p>Xyz</p>
1075
+ // <p>Foo</p><p>[]Xyz</p>
1076
+ // <p>FooXyz</p>
1077
+ //
1078
+ // In this case we need to decide where finally "Xyz" will land:
1079
+ //
1080
+ // <p>FooXyz</p> graveyard: <p>Bar</p>
1081
+ // <p>Foo</p> graveyard: <p>BarXyz</p>
1082
+ //
1083
+ // Let's move it in a way so that a merge operation that does not target to graveyard is more important so that
1084
+ // nodes does not end up in the graveyard. It makes sense. Both for Client A and for Client B "Xyz" finally did not
1085
+ // end up in the graveyard (see above).
1086
+ //
1087
+ // If neither or both operations point to graveyard, then let `aIsStrong` decide.
1088
+ //
1089
+ if (a.sourcePosition.isEqual(b.sourcePosition) && !a.targetPosition.isEqual(b.targetPosition) &&
1090
+ !context.bWasUndone && context.abRelation != 'splitAtSource') {
1091
+ const aToGraveyard = a.targetPosition.root.rootName == '$graveyard';
1092
+ const bToGraveyard = b.targetPosition.root.rootName == '$graveyard';
1093
+ // If `aIsWeak` it means that `a` points to graveyard while `b` doesn't. Don't move nodes then.
1094
+ const aIsWeak = aToGraveyard && !bToGraveyard;
1095
+ // If `bIsWeak` it means that `b` points to graveyard while `a` doesn't. Force moving nodes then.
1096
+ const bIsWeak = bToGraveyard && !aToGraveyard;
1097
+ // Force move if `b` is weak or neither operation is weak but `a` is stronger through `context.aIsStrong`.
1098
+ const forceMove = bIsWeak || (!aIsWeak && context.aIsStrong);
1099
+ if (forceMove) {
1100
+ const sourcePosition = b.targetPosition._getTransformedByMergeOperation(b);
1101
+ const targetPosition = a.targetPosition._getTransformedByMergeOperation(b);
1102
+ return [new MoveOperation(sourcePosition, a.howMany, targetPosition, 0)];
1103
+ }
1104
+ else {
1105
+ return [new NoOperation(0)];
1106
+ }
1107
+ }
1108
+ // The default case.
1109
+ //
1110
+ if (a.sourcePosition.hasSameParentAs(b.targetPosition)) {
1111
+ a.howMany += b.howMany;
1112
+ }
1113
+ a.sourcePosition = a.sourcePosition._getTransformedByMergeOperation(b);
1114
+ a.targetPosition = a.targetPosition._getTransformedByMergeOperation(b);
1115
+ // Handle positions in graveyard.
1116
+ // If graveyard positions are same and `a` operation is strong - do not transform.
1117
+ if (!a.graveyardPosition.isEqual(b.graveyardPosition) || !context.aIsStrong) {
1118
+ a.graveyardPosition = a.graveyardPosition._getTransformedByMergeOperation(b);
1119
+ }
1120
+ return [a];
1121
+ });
1122
+ setTransformation(MergeOperation, MoveOperation, (a, b, context) => {
1123
+ // Case 1:
1124
+ //
1125
+ // The element to merge got removed.
1126
+ //
1127
+ // Merge operation does support merging elements which are not siblings. So it would not be a problem
1128
+ // from technical point of view. However, if the element was removed, the intention of the user deleting it
1129
+ // was to have it all deleted, together with its children. From user experience point of view, moving back the
1130
+ // removed nodes might be unexpected. This means that in this scenario we will block the merging.
1131
+ //
1132
+ // The exception of this rule would be if the remove operation was later undone.
1133
+ //
1134
+ const removedRange = Range._createFromPositionAndShift(b.sourcePosition, b.howMany);
1135
+ if (b.type == 'remove' && !context.bWasUndone && !context.forceWeakRemove) {
1136
+ if (a.deletionPosition.hasSameParentAs(b.sourcePosition) && removedRange.containsPosition(a.sourcePosition)) {
1137
+ return [new NoOperation(0)];
1138
+ }
1139
+ }
1140
+ // The default case.
1141
+ //
1142
+ if (a.sourcePosition.hasSameParentAs(b.targetPosition)) {
1143
+ a.howMany += b.howMany;
1144
+ }
1145
+ if (a.sourcePosition.hasSameParentAs(b.sourcePosition)) {
1146
+ a.howMany -= b.howMany;
1147
+ }
1148
+ a.sourcePosition = a.sourcePosition._getTransformedByMoveOperation(b);
1149
+ a.targetPosition = a.targetPosition._getTransformedByMoveOperation(b);
1150
+ // `MergeOperation` graveyard position is like `MoveOperation` target position. It is a position where element(s) will
1151
+ // be moved. Like in other similar cases, we need to consider the scenario when those positions are same.
1152
+ // Here, we will treat `MergeOperation` like it is always strong (see `InsertOperation` x `InsertOperation` for comparison).
1153
+ // This means that we won't transform graveyard position if it is equal to move operation target position.
1154
+ if (!a.graveyardPosition.isEqual(b.targetPosition)) {
1155
+ a.graveyardPosition = a.graveyardPosition._getTransformedByMoveOperation(b);
1156
+ }
1157
+ return [a];
1158
+ });
1159
+ setTransformation(MergeOperation, SplitOperation, (a, b, context) => {
1160
+ if (b.graveyardPosition) {
1161
+ // If `b` operation defines graveyard position, a node from graveyard will be moved. This means that we need to
1162
+ // transform `a.graveyardPosition` accordingly.
1163
+ a.graveyardPosition = a.graveyardPosition._getTransformedByDeletion(b.graveyardPosition, 1);
1164
+ // This is a scenario foreseen in `MergeOperation` x `MergeOperation`, with two identical merge operations.
1165
+ //
1166
+ // So, there was `MergeOperation` x `MergeOperation` transformation earlier. Now, `a` is a merge operation which
1167
+ // source position is in graveyard. Interestingly, split operation wants to use the node to be merged by `a`. This
1168
+ // means that `b` is undoing that merge operation from earlier, which caused `a` to be in graveyard.
1169
+ //
1170
+ // If that's the case, at this point, we will only "fix" `a.howMany`. It was earlier set to `0` in
1171
+ // `MergeOperation` x `MergeOperation` transformation. Later transformations in this function will change other
1172
+ // properties.
1173
+ //
1174
+ if (a.deletionPosition.isEqual(b.graveyardPosition)) {
1175
+ a.howMany = b.howMany;
1176
+ }
1177
+ }
1178
+ // Case 1:
1179
+ //
1180
+ // Merge operation moves nodes to the place where split happens.
1181
+ // This is a classic situation when there are two paragraphs, and there is a split (enter) after the first
1182
+ // paragraph and there is a merge (delete) at the beginning of the second paragraph:
1183
+ //
1184
+ // <p>Foo{}</p><p>[]Bar</p>.
1185
+ //
1186
+ // Split is after `Foo`, while merge is from `Bar` to the end of `Foo`.
1187
+ //
1188
+ // State after split:
1189
+ // <p>Foo</p><p></p><p>Bar</p>
1190
+ //
1191
+ // Now, `Bar` should be merged to the new paragraph:
1192
+ // <p>Foo</p><p>Bar</p>
1193
+ //
1194
+ // Instead of merging it to the original paragraph:
1195
+ // <p>FooBar</p><p></p>
1196
+ //
1197
+ // This means that `targetPosition` needs to be transformed. This is the default case though.
1198
+ // For example, if the split would be after `F`, `targetPosition` should also be transformed.
1199
+ //
1200
+ // There are three exceptions, though, when we want to keep `targetPosition` as it was.
1201
+ //
1202
+ // First exception is when the merge target position is inside an element (not at the end, as usual). This
1203
+ // happens when the merge operation earlier was transformed by "the same" merge operation. If merge operation
1204
+ // targets inside the element we want to keep the original target position (and not transform it) because
1205
+ // we have additional context telling us that we want to merge to the original element. We can check if the
1206
+ // merge operation points inside element by checking what is `SplitOperation#howMany`. Since merge target position
1207
+ // is same as split position, if `howMany` is non-zero, it means that the merge target position is inside an element.
1208
+ //
1209
+ // Second exception is when the element to merge is in the graveyard and split operation uses it. In that case
1210
+ // if target position would be transformed, the merge operation would target at the source position:
1211
+ //
1212
+ // root: <p>Foo</p> graveyard: <p></p>
1213
+ //
1214
+ // SplitOperation: root [ 0, 3 ] using graveyard [ 0 ] (howMany = 0)
1215
+ // MergeOperation: graveyard [ 0, 0 ] -> root [ 0, 3 ] (howMany = 0)
1216
+ //
1217
+ // Since split operation moves the graveyard node back to the root, the merge operation source position changes.
1218
+ // We would like to merge from the empty <p> to the "Foo" <p>:
1219
+ //
1220
+ // root: <p>Foo</p><p></p> graveyard:
1221
+ //
1222
+ // MergeOperation#sourcePosition = root [ 1, 0 ]
1223
+ //
1224
+ // If `targetPosition` is transformed, it would become root [ 1, 0 ] as well. It has to be kept as it was.
1225
+ //
1226
+ // Third exception is connected with relations. If this happens during undo and we have explicit information
1227
+ // that target position has not been affected by the operation which is undone by this split then this split should
1228
+ // not move the target position either.
1229
+ //
1230
+ if (a.targetPosition.isEqual(b.splitPosition)) {
1231
+ const mergeInside = b.howMany != 0;
1232
+ const mergeSplittingElement = b.graveyardPosition && a.deletionPosition.isEqual(b.graveyardPosition);
1233
+ if (mergeInside || mergeSplittingElement || context.abRelation == 'mergeTargetNotMoved') {
1234
+ a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation(b);
1235
+ return [a];
1236
+ }
1237
+ }
1238
+ // Case 2:
1239
+ //
1240
+ // Merge source is at the same position as split position. This sometimes happen, mostly during undo.
1241
+ // The decision here is mostly to choose whether merge source position should stay where it is (so it will be at the end of the
1242
+ // split element) or should be move to the beginning of the new element.
1243
+ //
1244
+ if (a.sourcePosition.isEqual(b.splitPosition)) {
1245
+ // Use context to check if `SplitOperation` is not undoing a merge operation, that didn't change the `a` operation.
1246
+ // This scenario happens the undone merge operation moved nodes at the source position of `a` operation.
1247
+ // In that case `a` operation source position should stay where it is.
1248
+ if (context.abRelation == 'mergeSourceNotMoved') {
1249
+ a.howMany = 0;
1250
+ a.targetPosition = a.targetPosition._getTransformedBySplitOperation(b);
1251
+ return [a];
1252
+ }
1253
+ // This merge operation might have been earlier transformed by a merge operation which both merged the same element.
1254
+ // See that case in `MergeOperation` x `MergeOperation` transformation. In that scenario, if the merge operation has been undone,
1255
+ // the special case is not applied.
1256
+ //
1257
+ // Now, the merge operation is transformed by the split which has undone that previous merge operation.
1258
+ // So now we are fixing situation which was skipped in `MergeOperation` x `MergeOperation` case.
1259
+ //
1260
+ if (context.abRelation == 'mergeSameElement' || a.sourcePosition.offset > 0) {
1261
+ a.sourcePosition = b.moveTargetPosition.clone();
1262
+ a.targetPosition = a.targetPosition._getTransformedBySplitOperation(b);
1263
+ return [a];
1264
+ }
1265
+ }
1266
+ // The default case.
1267
+ //
1268
+ if (a.sourcePosition.hasSameParentAs(b.splitPosition)) {
1269
+ a.howMany = b.splitPosition.offset;
1270
+ }
1271
+ a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation(b);
1272
+ a.targetPosition = a.targetPosition._getTransformedBySplitOperation(b);
1273
+ return [a];
1274
+ });
1500
1275
  // -----------------------
1501
-
1502
- setTransformation( MoveOperation, InsertOperation, ( a, b ) => {
1503
- const moveRange = Range._createFromPositionAndShift( a.sourcePosition, a.howMany );
1504
- const transformed = moveRange._getTransformedByInsertOperation( b, false )[ 0 ];
1505
-
1506
- a.sourcePosition = transformed.start;
1507
- a.howMany = transformed.end.offset - transformed.start.offset;
1508
-
1509
- // See `InsertOperation` x `MoveOperation` transformation for details on this case.
1510
- //
1511
- // In summary, both operations point to the same place, so the order of nodes needs to be decided.
1512
- // `MoveOperation` is considered weaker, so it is always transformed, unless there was a certain relation
1513
- // between operations.
1514
- //
1515
- if ( !a.targetPosition.isEqual( b.position ) ) {
1516
- a.targetPosition = a.targetPosition._getTransformedByInsertOperation( b );
1517
- }
1518
-
1519
- return [ a ];
1520
- } );
1521
-
1522
- setTransformation( MoveOperation, MoveOperation, ( a, b, context ) => {
1523
- //
1524
- // Setting and evaluating some variables that will be used in special cases and default algorithm.
1525
- //
1526
- // Create ranges from `MoveOperations` properties.
1527
- const rangeA = Range._createFromPositionAndShift( a.sourcePosition, a.howMany );
1528
- const rangeB = Range._createFromPositionAndShift( b.sourcePosition, b.howMany );
1529
-
1530
- // Assign `context.aIsStrong` to a different variable, because the value may change during execution of
1531
- // this algorithm and we do not want to override original `context.aIsStrong` that will be used in later transformations.
1532
- let aIsStrong = context.aIsStrong;
1533
-
1534
- // This will be used to decide the order of nodes if both operations target at the same position.
1535
- // By default, use strong/weak operation mechanism.
1536
- let insertBefore = !context.aIsStrong;
1537
-
1538
- // If the relation is set, then use it to decide nodes order.
1539
- if ( context.abRelation == 'insertBefore' || context.baRelation == 'insertAfter' ) {
1540
- insertBefore = true;
1541
- } else if ( context.abRelation == 'insertAfter' || context.baRelation == 'insertBefore' ) {
1542
- insertBefore = false;
1543
- }
1544
-
1545
- // `a.targetPosition` could be affected by the `b` operation. We will transform it.
1546
- let newTargetPosition;
1547
-
1548
- if ( a.targetPosition.isEqual( b.targetPosition ) && insertBefore ) {
1549
- newTargetPosition = a.targetPosition._getTransformedByDeletion(
1550
- b.sourcePosition,
1551
- b.howMany
1552
- );
1553
- } else {
1554
- newTargetPosition = a.targetPosition._getTransformedByMove(
1555
- b.sourcePosition,
1556
- b.targetPosition,
1557
- b.howMany
1558
- );
1559
- }
1560
-
1561
- //
1562
- // Special case #1 + mirror.
1563
- //
1564
- // Special case when both move operations' target positions are inside nodes that are
1565
- // being moved by the other move operation. So in other words, we move ranges into inside of each other.
1566
- // This case can't be solved reasonably (on the other hand, it should not happen often).
1567
- if ( _moveTargetIntoMovedRange( a, b ) && _moveTargetIntoMovedRange( b, a ) ) {
1568
- // Instead of transforming operation, we return a reverse of the operation that we transform by.
1569
- // So when the results of this "transformation" will be applied, `b` MoveOperation will get reversed.
1570
- return [ b.getReversed() ];
1571
- }
1572
- //
1573
- // End of special case #1.
1574
- //
1575
-
1576
- //
1577
- // Special case #2.
1578
- //
1579
- // Check if `b` operation targets inside `rangeA`.
1580
- const bTargetsToA = rangeA.containsPosition( b.targetPosition );
1581
-
1582
- // If `b` targets to `rangeA` and `rangeA` contains `rangeB`, `b` operation has no influence on `a` operation.
1583
- // You might say that operation `b` is captured inside operation `a`.
1584
- if ( bTargetsToA && rangeA.containsRange( rangeB, true ) ) {
1585
- // There is a mini-special case here, where `rangeB` is on other level than `rangeA`. That's why
1586
- // we need to transform `a` operation anyway.
1587
- rangeA.start = rangeA.start._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany );
1588
- rangeA.end = rangeA.end._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany );
1589
-
1590
- return _makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition );
1591
- }
1592
-
1593
- //
1594
- // Special case #2 mirror.
1595
- //
1596
- const aTargetsToB = rangeB.containsPosition( a.targetPosition );
1597
-
1598
- if ( aTargetsToB && rangeB.containsRange( rangeA, true ) ) {
1599
- // `a` operation is "moved together" with `b` operation.
1600
- // Here, just move `rangeA` "inside" `rangeB`.
1601
- rangeA.start = rangeA.start._getCombined( b.sourcePosition, b.getMovedRangeStart() );
1602
- rangeA.end = rangeA.end._getCombined( b.sourcePosition, b.getMovedRangeStart() );
1603
-
1604
- return _makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition );
1605
- }
1606
- //
1607
- // End of special case #2.
1608
- //
1609
-
1610
- //
1611
- // Special case #3 + mirror.
1612
- //
1613
- // `rangeA` has a node which is an ancestor of `rangeB`. In other words, `rangeB` is inside `rangeA`
1614
- // but not on the same tree level. In such case ranges have common part but we have to treat it
1615
- // differently, because in such case those ranges are not really conflicting and should be treated like
1616
- // two separate ranges. Also we have to discard two difference parts.
1617
- const aCompB = compareArrays( a.sourcePosition.getParentPath(), b.sourcePosition.getParentPath() );
1618
-
1619
- if ( aCompB == 'prefix' || aCompB == 'extension' ) {
1620
- // Transform `rangeA` by `b` operation and make operation out of it, and that's all.
1621
- // Note that this is a simplified version of default case, but here we treat the common part (whole `rangeA`)
1622
- // like a one difference part.
1623
- rangeA.start = rangeA.start._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany );
1624
- rangeA.end = rangeA.end._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany );
1625
-
1626
- return _makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition );
1627
- }
1628
- //
1629
- // End of special case #3.
1630
- //
1631
-
1632
- //
1633
- // Default case - ranges are on the same level or are not connected with each other.
1634
- //
1635
- // Modifier for default case.
1636
- // Modifies `aIsStrong` flag in certain conditions.
1637
- //
1638
- // If only one of operations is a remove operation, we force remove operation to be the "stronger" one
1639
- // to provide more expected results.
1640
- if ( a.type == 'remove' && b.type != 'remove' && !context.aWasUndone && !context.forceWeakRemove ) {
1641
- aIsStrong = true;
1642
- } else if ( a.type != 'remove' && b.type == 'remove' && !context.bWasUndone && !context.forceWeakRemove ) {
1643
- aIsStrong = false;
1644
- }
1645
-
1646
- // Handle operation's source ranges - check how `rangeA` is affected by `b` operation.
1647
- // This will aggregate transformed ranges.
1648
- const ranges = [];
1649
-
1650
- // Get the "difference part" of `a` operation source range.
1651
- // This is an array with one or two ranges. Two ranges if `rangeB` is inside `rangeA`.
1652
- const difference = rangeA.getDifference( rangeB );
1653
-
1654
- for ( const range of difference ) {
1655
- // Transform those ranges by `b` operation. For example if `b` moved range from before those ranges, fix those ranges.
1656
- range.start = range.start._getTransformedByDeletion( b.sourcePosition, b.howMany );
1657
- range.end = range.end._getTransformedByDeletion( b.sourcePosition, b.howMany );
1658
-
1659
- // If `b` operation targets into `rangeA` on the same level, spread `rangeA` into two ranges.
1660
- const shouldSpread = compareArrays( range.start.getParentPath(), b.getMovedRangeStart().getParentPath() ) == 'same';
1661
- const newRanges = range._getTransformedByInsertion( b.getMovedRangeStart(), b.howMany, shouldSpread );
1662
-
1663
- ranges.push( ...newRanges );
1664
- }
1665
-
1666
- // Then, we have to manage the "common part" of both move ranges.
1667
- const common = rangeA.getIntersection( rangeB );
1668
-
1669
- if ( common !== null && aIsStrong ) {
1670
- // Calculate the new position of that part of original range.
1671
- common.start = common.start._getCombined( b.sourcePosition, b.getMovedRangeStart() );
1672
- common.end = common.end._getCombined( b.sourcePosition, b.getMovedRangeStart() );
1673
-
1674
- // Take care of proper range order.
1675
- //
1676
- // Put `common` at appropriate place. Keep in mind that we are interested in original order.
1677
- // Basically there are only three cases: there is zero, one or two difference ranges.
1678
- //
1679
- // If there is zero difference ranges, just push `common` in the array.
1680
- if ( ranges.length === 0 ) {
1681
- ranges.push( common );
1682
- }
1683
- // If there is one difference range, we need to check whether common part was before it or after it.
1684
- else if ( ranges.length == 1 ) {
1685
- if ( rangeB.start.isBefore( rangeA.start ) || rangeB.start.isEqual( rangeA.start ) ) {
1686
- ranges.unshift( common );
1687
- } else {
1688
- ranges.push( common );
1689
- }
1690
- }
1691
- // If there are more ranges (which means two), put common part between them. This is the only scenario
1692
- // where there could be two difference ranges so we don't have to make any comparisons.
1693
- else {
1694
- ranges.splice( 1, 0, common );
1695
- }
1696
- }
1697
-
1698
- if ( ranges.length === 0 ) {
1699
- // If there are no "source ranges", nothing should be changed.
1700
- // Note that this can happen only if `aIsStrong == false` and `rangeA.isEqual( rangeB )`.
1701
- return [ new NoOperation( a.baseVersion ) ];
1702
- }
1703
-
1704
- return _makeMoveOperationsFromRanges( ranges, newTargetPosition );
1705
- } );
1706
-
1707
- setTransformation( MoveOperation, SplitOperation, ( a, b, context ) => {
1708
- let newTargetPosition = a.targetPosition.clone();
1709
-
1710
- // Do not transform if target position is same as split insertion position and this split comes from undo.
1711
- // This should be done on relations but it is too much work for now as it would require relations working in collaboration.
1712
- // We need to make a decision how we will resolve such conflict and this is less harmful way.
1713
- if ( !a.targetPosition.isEqual( b.insertionPosition ) || !b.graveyardPosition || context.abRelation == 'moveTargetAfter' ) {
1714
- newTargetPosition = a.targetPosition._getTransformedBySplitOperation( b );
1715
- }
1716
-
1717
- // Case 1:
1718
- //
1719
- // Last element in the moved range got split.
1720
- //
1721
- // In this case the default range transformation will not work correctly as the element created by
1722
- // split operation would be outside the range. The range to move needs to be fixed manually.
1723
- //
1724
- const moveRange = Range._createFromPositionAndShift( a.sourcePosition, a.howMany );
1725
-
1726
- if ( moveRange.end.isEqual( b.insertionPosition ) ) {
1727
- // Do it only if this is a "natural" split, not a one that comes from undo.
1728
- // If this is undo split, only `targetPosition` needs to be changed (if the move is a remove).
1729
- if ( !b.graveyardPosition ) {
1730
- a.howMany++;
1731
- }
1732
-
1733
- a.targetPosition = newTargetPosition;
1734
-
1735
- return [ a ];
1736
- }
1737
-
1738
- // Case 2:
1739
- //
1740
- // Split happened between the moved nodes. In this case two ranges to move need to be generated.
1741
- //
1742
- // Characters `ozba` are moved to the end of paragraph `Xyz` but split happened.
1743
- // <p>F[oz|ba]r</p><p>Xyz</p>
1744
- //
1745
- // After split:
1746
- // <p>F[oz</p><p>ba]r</p><p>Xyz</p>
1747
- //
1748
- // Correct ranges:
1749
- // <p>F[oz]</p><p>[ba]r</p><p>Xyz</p>
1750
- //
1751
- // After move:
1752
- // <p>F</p><p>r</p><p>Xyzozba</p>
1753
- //
1754
- if ( moveRange.start.hasSameParentAs( b.splitPosition ) && moveRange.containsPosition( b.splitPosition ) ) {
1755
- let rightRange = new Range( b.splitPosition, moveRange.end );
1756
- rightRange = rightRange._getTransformedBySplitOperation( b );
1757
-
1758
- const ranges = [
1759
- new Range( moveRange.start, b.splitPosition ),
1760
- rightRange
1761
- ];
1762
-
1763
- return _makeMoveOperationsFromRanges( ranges, newTargetPosition );
1764
- }
1765
-
1766
- // Case 3:
1767
- //
1768
- // Move operation targets at the split position. We need to decide if the nodes should be inserted
1769
- // at the end of the split element or at the beginning of the new element.
1770
- //
1771
- if ( a.targetPosition.isEqual( b.splitPosition ) && context.abRelation == 'insertAtSource' ) {
1772
- newTargetPosition = b.moveTargetPosition;
1773
- }
1774
-
1775
- // Case 4:
1776
- //
1777
- // Move operation targets just after the split element. We need to decide if the nodes should be inserted
1778
- // between two parts of split element, or after the new element.
1779
- //
1780
- // Split at `|`, while move operation moves `<p>Xyz</p>` and targets at `^`:
1781
- // <p>Foo|bar</p>^<p>baz</p>
1782
- // <p>Foo</p>^<p>bar</p><p>baz</p> or <p>Foo</p><p>bar</p>^<p>baz</p>?
1783
- //
1784
- // If there is no contextual information between operations (for example, they come from collaborative
1785
- // editing), we don't want to put some unrelated content (move) between parts of related content (split parts).
1786
- // However, if the split is from undo, in the past, the moved content might be targeting between the
1787
- // split parts, meaning that was exactly user's intention:
1788
- //
1789
- // <p>Foo</p>^<p>bar</p> <--- original situation, in "past".
1790
- // <p>Foobar</p>^ <--- after merge target position is transformed.
1791
- // <p>Foo|bar</p>^ <--- then the merge is undone, and split happens, which leads us to current situation.
1792
- //
1793
- // In this case it is pretty clear that the intention was to put new paragraph between those nodes,
1794
- // so we need to transform accordingly. We can detect this scenario thanks to relations.
1795
- //
1796
- if ( a.targetPosition.isEqual( b.insertionPosition ) && context.abRelation == 'insertBetween' ) {
1797
- newTargetPosition = a.targetPosition;
1798
- }
1799
-
1800
- // The default case.
1801
- //
1802
- const transformed = moveRange._getTransformedBySplitOperation( b );
1803
- const ranges = [ transformed ];
1804
-
1805
- // Case 5:
1806
- //
1807
- // Moved range contains graveyard element used by split operation. Add extra move operation to the result.
1808
- //
1809
- if ( b.graveyardPosition ) {
1810
- const movesGraveyardElement = moveRange.start.isEqual( b.graveyardPosition ) || moveRange.containsPosition( b.graveyardPosition );
1811
-
1812
- if ( a.howMany > 1 && movesGraveyardElement && !context.aWasUndone ) {
1813
- ranges.push( Range._createFromPositionAndShift( b.insertionPosition, 1 ) );
1814
- }
1815
- }
1816
-
1817
- return _makeMoveOperationsFromRanges( ranges, newTargetPosition );
1818
- } );
1819
-
1820
- setTransformation( MoveOperation, MergeOperation, ( a, b, context ) => {
1821
- const movedRange = Range._createFromPositionAndShift( a.sourcePosition, a.howMany );
1822
-
1823
- if ( b.deletionPosition.hasSameParentAs( a.sourcePosition ) && movedRange.containsPosition( b.sourcePosition ) ) {
1824
- if ( a.type == 'remove' && !context.forceWeakRemove ) {
1825
- // Case 1:
1826
- //
1827
- // The element to remove got merged.
1828
- //
1829
- // Merge operation does support merging elements which are not siblings. So it would not be a problem
1830
- // from technical point of view. However, if the element was removed, the intention of the user
1831
- // deleting it was to have it all deleted. From user experience point of view, moving back the
1832
- // removed nodes might be unexpected. This means that in this scenario we will reverse merging and remove the element.
1833
- //
1834
- if ( !context.aWasUndone ) {
1835
- const results = [];
1836
-
1837
- let gyMoveSource = b.graveyardPosition.clone();
1838
- let splitNodesMoveSource = b.targetPosition._getTransformedByMergeOperation( b );
1839
-
1840
- if ( a.howMany > 1 ) {
1841
- results.push( new MoveOperation( a.sourcePosition, a.howMany - 1, a.targetPosition, 0 ) );
1842
-
1843
- gyMoveSource = gyMoveSource._getTransformedByMove( a.sourcePosition, a.targetPosition, a.howMany - 1 );
1844
- splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove( a.sourcePosition, a.targetPosition, a.howMany - 1 );
1845
- }
1846
-
1847
- const gyMoveTarget = b.deletionPosition._getCombined( a.sourcePosition, a.targetPosition );
1848
- const gyMove = new MoveOperation( gyMoveSource, 1, gyMoveTarget, 0 );
1849
-
1850
- const splitNodesMoveTargetPath = gyMove.getMovedRangeStart().path.slice();
1851
- splitNodesMoveTargetPath.push( 0 );
1852
-
1853
- const splitNodesMoveTarget = new Position( gyMove.targetPosition.root, splitNodesMoveTargetPath );
1854
- splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove( gyMoveSource, gyMoveTarget, 1 );
1855
- const splitNodesMove = new MoveOperation( splitNodesMoveSource, b.howMany, splitNodesMoveTarget, 0 );
1856
-
1857
- results.push( gyMove );
1858
- results.push( splitNodesMove );
1859
-
1860
- return results;
1861
- }
1862
- } else {
1863
- // Case 2:
1864
- //
1865
- // The element to move got merged and it was the only element to move.
1866
- // In this case just don't do anything, leave the node in the graveyard. Without special case
1867
- // it would be a move operation that moves 0 nodes, so maybe it is better just to return no-op.
1868
- //
1869
- if ( a.howMany == 1 ) {
1870
- if ( !context.bWasUndone ) {
1871
- return [ new NoOperation( 0 ) ];
1872
- } else {
1873
- a.sourcePosition = b.graveyardPosition.clone();
1874
- a.targetPosition = a.targetPosition._getTransformedByMergeOperation( b );
1875
-
1876
- return [ a ];
1877
- }
1878
- }
1879
- }
1880
- }
1881
-
1882
- // The default case.
1883
- //
1884
- const moveRange = Range._createFromPositionAndShift( a.sourcePosition, a.howMany );
1885
- const transformed = moveRange._getTransformedByMergeOperation( b );
1886
-
1887
- a.sourcePosition = transformed.start;
1888
- a.howMany = transformed.end.offset - transformed.start.offset;
1889
- a.targetPosition = a.targetPosition._getTransformedByMergeOperation( b );
1890
-
1891
- return [ a ];
1892
- } );
1893
-
1276
+ setTransformation(MoveOperation, InsertOperation, (a, b) => {
1277
+ const moveRange = Range._createFromPositionAndShift(a.sourcePosition, a.howMany);
1278
+ const transformed = moveRange._getTransformedByInsertOperation(b, false)[0];
1279
+ a.sourcePosition = transformed.start;
1280
+ a.howMany = transformed.end.offset - transformed.start.offset;
1281
+ // See `InsertOperation` x `MoveOperation` transformation for details on this case.
1282
+ //
1283
+ // In summary, both operations point to the same place, so the order of nodes needs to be decided.
1284
+ // `MoveOperation` is considered weaker, so it is always transformed, unless there was a certain relation
1285
+ // between operations.
1286
+ //
1287
+ if (!a.targetPosition.isEqual(b.position)) {
1288
+ a.targetPosition = a.targetPosition._getTransformedByInsertOperation(b);
1289
+ }
1290
+ return [a];
1291
+ });
1292
+ setTransformation(MoveOperation, MoveOperation, (a, b, context) => {
1293
+ //
1294
+ // Setting and evaluating some variables that will be used in special cases and default algorithm.
1295
+ //
1296
+ // Create ranges from `MoveOperations` properties.
1297
+ const rangeA = Range._createFromPositionAndShift(a.sourcePosition, a.howMany);
1298
+ const rangeB = Range._createFromPositionAndShift(b.sourcePosition, b.howMany);
1299
+ // Assign `context.aIsStrong` to a different variable, because the value may change during execution of
1300
+ // this algorithm and we do not want to override original `context.aIsStrong` that will be used in later transformations.
1301
+ let aIsStrong = context.aIsStrong;
1302
+ // This will be used to decide the order of nodes if both operations target at the same position.
1303
+ // By default, use strong/weak operation mechanism.
1304
+ let insertBefore = !context.aIsStrong;
1305
+ // If the relation is set, then use it to decide nodes order.
1306
+ if (context.abRelation == 'insertBefore' || context.baRelation == 'insertAfter') {
1307
+ insertBefore = true;
1308
+ }
1309
+ else if (context.abRelation == 'insertAfter' || context.baRelation == 'insertBefore') {
1310
+ insertBefore = false;
1311
+ }
1312
+ // `a.targetPosition` could be affected by the `b` operation. We will transform it.
1313
+ let newTargetPosition;
1314
+ if (a.targetPosition.isEqual(b.targetPosition) && insertBefore) {
1315
+ newTargetPosition = a.targetPosition._getTransformedByDeletion(b.sourcePosition, b.howMany);
1316
+ }
1317
+ else {
1318
+ newTargetPosition = a.targetPosition._getTransformedByMove(b.sourcePosition, b.targetPosition, b.howMany);
1319
+ }
1320
+ //
1321
+ // Special case #1 + mirror.
1322
+ //
1323
+ // Special case when both move operations' target positions are inside nodes that are
1324
+ // being moved by the other move operation. So in other words, we move ranges into inside of each other.
1325
+ // This case can't be solved reasonably (on the other hand, it should not happen often).
1326
+ if (_moveTargetIntoMovedRange(a, b) && _moveTargetIntoMovedRange(b, a)) {
1327
+ // Instead of transforming operation, we return a reverse of the operation that we transform by.
1328
+ // So when the results of this "transformation" will be applied, `b` MoveOperation will get reversed.
1329
+ return [b.getReversed()];
1330
+ }
1331
+ //
1332
+ // End of special case #1.
1333
+ //
1334
+ //
1335
+ // Special case #2.
1336
+ //
1337
+ // Check if `b` operation targets inside `rangeA`.
1338
+ const bTargetsToA = rangeA.containsPosition(b.targetPosition);
1339
+ // If `b` targets to `rangeA` and `rangeA` contains `rangeB`, `b` operation has no influence on `a` operation.
1340
+ // You might say that operation `b` is captured inside operation `a`.
1341
+ if (bTargetsToA && rangeA.containsRange(rangeB, true)) {
1342
+ // There is a mini-special case here, where `rangeB` is on other level than `rangeA`. That's why
1343
+ // we need to transform `a` operation anyway.
1344
+ rangeA.start = rangeA.start._getTransformedByMove(b.sourcePosition, b.targetPosition, b.howMany);
1345
+ rangeA.end = rangeA.end._getTransformedByMove(b.sourcePosition, b.targetPosition, b.howMany);
1346
+ return _makeMoveOperationsFromRanges([rangeA], newTargetPosition);
1347
+ }
1348
+ //
1349
+ // Special case #2 mirror.
1350
+ //
1351
+ const aTargetsToB = rangeB.containsPosition(a.targetPosition);
1352
+ if (aTargetsToB && rangeB.containsRange(rangeA, true)) {
1353
+ // `a` operation is "moved together" with `b` operation.
1354
+ // Here, just move `rangeA` "inside" `rangeB`.
1355
+ rangeA.start = rangeA.start._getCombined(b.sourcePosition, b.getMovedRangeStart());
1356
+ rangeA.end = rangeA.end._getCombined(b.sourcePosition, b.getMovedRangeStart());
1357
+ return _makeMoveOperationsFromRanges([rangeA], newTargetPosition);
1358
+ }
1359
+ //
1360
+ // End of special case #2.
1361
+ //
1362
+ //
1363
+ // Special case #3 + mirror.
1364
+ //
1365
+ // `rangeA` has a node which is an ancestor of `rangeB`. In other words, `rangeB` is inside `rangeA`
1366
+ // but not on the same tree level. In such case ranges have common part but we have to treat it
1367
+ // differently, because in such case those ranges are not really conflicting and should be treated like
1368
+ // two separate ranges. Also we have to discard two difference parts.
1369
+ const aCompB = compareArrays(a.sourcePosition.getParentPath(), b.sourcePosition.getParentPath());
1370
+ if (aCompB == 'prefix' || aCompB == 'extension') {
1371
+ // Transform `rangeA` by `b` operation and make operation out of it, and that's all.
1372
+ // Note that this is a simplified version of default case, but here we treat the common part (whole `rangeA`)
1373
+ // like a one difference part.
1374
+ rangeA.start = rangeA.start._getTransformedByMove(b.sourcePosition, b.targetPosition, b.howMany);
1375
+ rangeA.end = rangeA.end._getTransformedByMove(b.sourcePosition, b.targetPosition, b.howMany);
1376
+ return _makeMoveOperationsFromRanges([rangeA], newTargetPosition);
1377
+ }
1378
+ //
1379
+ // End of special case #3.
1380
+ //
1381
+ //
1382
+ // Default case - ranges are on the same level or are not connected with each other.
1383
+ //
1384
+ // Modifier for default case.
1385
+ // Modifies `aIsStrong` flag in certain conditions.
1386
+ //
1387
+ // If only one of operations is a remove operation, we force remove operation to be the "stronger" one
1388
+ // to provide more expected results.
1389
+ if (a.type == 'remove' && b.type != 'remove' && !context.aWasUndone && !context.forceWeakRemove) {
1390
+ aIsStrong = true;
1391
+ }
1392
+ else if (a.type != 'remove' && b.type == 'remove' && !context.bWasUndone && !context.forceWeakRemove) {
1393
+ aIsStrong = false;
1394
+ }
1395
+ // Handle operation's source ranges - check how `rangeA` is affected by `b` operation.
1396
+ // This will aggregate transformed ranges.
1397
+ const ranges = [];
1398
+ // Get the "difference part" of `a` operation source range.
1399
+ // This is an array with one or two ranges. Two ranges if `rangeB` is inside `rangeA`.
1400
+ const difference = rangeA.getDifference(rangeB);
1401
+ for (const range of difference) {
1402
+ // Transform those ranges by `b` operation. For example if `b` moved range from before those ranges, fix those ranges.
1403
+ range.start = range.start._getTransformedByDeletion(b.sourcePosition, b.howMany);
1404
+ range.end = range.end._getTransformedByDeletion(b.sourcePosition, b.howMany);
1405
+ // If `b` operation targets into `rangeA` on the same level, spread `rangeA` into two ranges.
1406
+ const shouldSpread = compareArrays(range.start.getParentPath(), b.getMovedRangeStart().getParentPath()) == 'same';
1407
+ const newRanges = range._getTransformedByInsertion(b.getMovedRangeStart(), b.howMany, shouldSpread);
1408
+ ranges.push(...newRanges);
1409
+ }
1410
+ // Then, we have to manage the "common part" of both move ranges.
1411
+ const common = rangeA.getIntersection(rangeB);
1412
+ if (common !== null && aIsStrong) {
1413
+ // Calculate the new position of that part of original range.
1414
+ common.start = common.start._getCombined(b.sourcePosition, b.getMovedRangeStart());
1415
+ common.end = common.end._getCombined(b.sourcePosition, b.getMovedRangeStart());
1416
+ // Take care of proper range order.
1417
+ //
1418
+ // Put `common` at appropriate place. Keep in mind that we are interested in original order.
1419
+ // Basically there are only three cases: there is zero, one or two difference ranges.
1420
+ //
1421
+ // If there is zero difference ranges, just push `common` in the array.
1422
+ if (ranges.length === 0) {
1423
+ ranges.push(common);
1424
+ }
1425
+ // If there is one difference range, we need to check whether common part was before it or after it.
1426
+ else if (ranges.length == 1) {
1427
+ if (rangeB.start.isBefore(rangeA.start) || rangeB.start.isEqual(rangeA.start)) {
1428
+ ranges.unshift(common);
1429
+ }
1430
+ else {
1431
+ ranges.push(common);
1432
+ }
1433
+ }
1434
+ // If there are more ranges (which means two), put common part between them. This is the only scenario
1435
+ // where there could be two difference ranges so we don't have to make any comparisons.
1436
+ else {
1437
+ ranges.splice(1, 0, common);
1438
+ }
1439
+ }
1440
+ if (ranges.length === 0) {
1441
+ // If there are no "source ranges", nothing should be changed.
1442
+ // Note that this can happen only if `aIsStrong == false` and `rangeA.isEqual( rangeB )`.
1443
+ return [new NoOperation(a.baseVersion)];
1444
+ }
1445
+ return _makeMoveOperationsFromRanges(ranges, newTargetPosition);
1446
+ });
1447
+ setTransformation(MoveOperation, SplitOperation, (a, b, context) => {
1448
+ let newTargetPosition = a.targetPosition.clone();
1449
+ // Do not transform if target position is same as split insertion position and this split comes from undo.
1450
+ // This should be done on relations but it is too much work for now as it would require relations working in collaboration.
1451
+ // We need to make a decision how we will resolve such conflict and this is less harmful way.
1452
+ if (!a.targetPosition.isEqual(b.insertionPosition) || !b.graveyardPosition || context.abRelation == 'moveTargetAfter') {
1453
+ newTargetPosition = a.targetPosition._getTransformedBySplitOperation(b);
1454
+ }
1455
+ // Case 1:
1456
+ //
1457
+ // Last element in the moved range got split.
1458
+ //
1459
+ // In this case the default range transformation will not work correctly as the element created by
1460
+ // split operation would be outside the range. The range to move needs to be fixed manually.
1461
+ //
1462
+ const moveRange = Range._createFromPositionAndShift(a.sourcePosition, a.howMany);
1463
+ if (moveRange.end.isEqual(b.insertionPosition)) {
1464
+ // Do it only if this is a "natural" split, not a one that comes from undo.
1465
+ // If this is undo split, only `targetPosition` needs to be changed (if the move is a remove).
1466
+ if (!b.graveyardPosition) {
1467
+ a.howMany++;
1468
+ }
1469
+ a.targetPosition = newTargetPosition;
1470
+ return [a];
1471
+ }
1472
+ // Case 2:
1473
+ //
1474
+ // Split happened between the moved nodes. In this case two ranges to move need to be generated.
1475
+ //
1476
+ // Characters `ozba` are moved to the end of paragraph `Xyz` but split happened.
1477
+ // <p>F[oz|ba]r</p><p>Xyz</p>
1478
+ //
1479
+ // After split:
1480
+ // <p>F[oz</p><p>ba]r</p><p>Xyz</p>
1481
+ //
1482
+ // Correct ranges:
1483
+ // <p>F[oz]</p><p>[ba]r</p><p>Xyz</p>
1484
+ //
1485
+ // After move:
1486
+ // <p>F</p><p>r</p><p>Xyzozba</p>
1487
+ //
1488
+ if (moveRange.start.hasSameParentAs(b.splitPosition) && moveRange.containsPosition(b.splitPosition)) {
1489
+ let rightRange = new Range(b.splitPosition, moveRange.end);
1490
+ rightRange = rightRange._getTransformedBySplitOperation(b);
1491
+ const ranges = [
1492
+ new Range(moveRange.start, b.splitPosition),
1493
+ rightRange
1494
+ ];
1495
+ return _makeMoveOperationsFromRanges(ranges, newTargetPosition);
1496
+ }
1497
+ // Case 3:
1498
+ //
1499
+ // Move operation targets at the split position. We need to decide if the nodes should be inserted
1500
+ // at the end of the split element or at the beginning of the new element.
1501
+ //
1502
+ if (a.targetPosition.isEqual(b.splitPosition) && context.abRelation == 'insertAtSource') {
1503
+ newTargetPosition = b.moveTargetPosition;
1504
+ }
1505
+ // Case 4:
1506
+ //
1507
+ // Move operation targets just after the split element. We need to decide if the nodes should be inserted
1508
+ // between two parts of split element, or after the new element.
1509
+ //
1510
+ // Split at `|`, while move operation moves `<p>Xyz</p>` and targets at `^`:
1511
+ // <p>Foo|bar</p>^<p>baz</p>
1512
+ // <p>Foo</p>^<p>bar</p><p>baz</p> or <p>Foo</p><p>bar</p>^<p>baz</p>?
1513
+ //
1514
+ // If there is no contextual information between operations (for example, they come from collaborative
1515
+ // editing), we don't want to put some unrelated content (move) between parts of related content (split parts).
1516
+ // However, if the split is from undo, in the past, the moved content might be targeting between the
1517
+ // split parts, meaning that was exactly user's intention:
1518
+ //
1519
+ // <p>Foo</p>^<p>bar</p> <--- original situation, in "past".
1520
+ // <p>Foobar</p>^ <--- after merge target position is transformed.
1521
+ // <p>Foo|bar</p>^ <--- then the merge is undone, and split happens, which leads us to current situation.
1522
+ //
1523
+ // In this case it is pretty clear that the intention was to put new paragraph between those nodes,
1524
+ // so we need to transform accordingly. We can detect this scenario thanks to relations.
1525
+ //
1526
+ if (a.targetPosition.isEqual(b.insertionPosition) && context.abRelation == 'insertBetween') {
1527
+ newTargetPosition = a.targetPosition;
1528
+ }
1529
+ // The default case.
1530
+ //
1531
+ const transformed = moveRange._getTransformedBySplitOperation(b);
1532
+ const ranges = [transformed];
1533
+ // Case 5:
1534
+ //
1535
+ // Moved range contains graveyard element used by split operation. Add extra move operation to the result.
1536
+ //
1537
+ if (b.graveyardPosition) {
1538
+ const movesGraveyardElement = moveRange.start.isEqual(b.graveyardPosition) || moveRange.containsPosition(b.graveyardPosition);
1539
+ if (a.howMany > 1 && movesGraveyardElement && !context.aWasUndone) {
1540
+ ranges.push(Range._createFromPositionAndShift(b.insertionPosition, 1));
1541
+ }
1542
+ }
1543
+ return _makeMoveOperationsFromRanges(ranges, newTargetPosition);
1544
+ });
1545
+ setTransformation(MoveOperation, MergeOperation, (a, b, context) => {
1546
+ const movedRange = Range._createFromPositionAndShift(a.sourcePosition, a.howMany);
1547
+ if (b.deletionPosition.hasSameParentAs(a.sourcePosition) && movedRange.containsPosition(b.sourcePosition)) {
1548
+ if (a.type == 'remove' && !context.forceWeakRemove) {
1549
+ // Case 1:
1550
+ //
1551
+ // The element to remove got merged.
1552
+ //
1553
+ // Merge operation does support merging elements which are not siblings. So it would not be a problem
1554
+ // from technical point of view. However, if the element was removed, the intention of the user
1555
+ // deleting it was to have it all deleted. From user experience point of view, moving back the
1556
+ // removed nodes might be unexpected. This means that in this scenario we will reverse merging and remove the element.
1557
+ //
1558
+ if (!context.aWasUndone) {
1559
+ const results = [];
1560
+ let gyMoveSource = b.graveyardPosition.clone();
1561
+ let splitNodesMoveSource = b.targetPosition._getTransformedByMergeOperation(b);
1562
+ if (a.howMany > 1) {
1563
+ results.push(new MoveOperation(a.sourcePosition, a.howMany - 1, a.targetPosition, 0));
1564
+ gyMoveSource = gyMoveSource._getTransformedByMove(a.sourcePosition, a.targetPosition, a.howMany - 1);
1565
+ splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove(a.sourcePosition, a.targetPosition, a.howMany - 1);
1566
+ }
1567
+ const gyMoveTarget = b.deletionPosition._getCombined(a.sourcePosition, a.targetPosition);
1568
+ const gyMove = new MoveOperation(gyMoveSource, 1, gyMoveTarget, 0);
1569
+ const splitNodesMoveTargetPath = gyMove.getMovedRangeStart().path.slice();
1570
+ splitNodesMoveTargetPath.push(0);
1571
+ const splitNodesMoveTarget = new Position(gyMove.targetPosition.root, splitNodesMoveTargetPath);
1572
+ splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove(gyMoveSource, gyMoveTarget, 1);
1573
+ const splitNodesMove = new MoveOperation(splitNodesMoveSource, b.howMany, splitNodesMoveTarget, 0);
1574
+ results.push(gyMove);
1575
+ results.push(splitNodesMove);
1576
+ return results;
1577
+ }
1578
+ }
1579
+ else {
1580
+ // Case 2:
1581
+ //
1582
+ // The element to move got merged and it was the only element to move.
1583
+ // In this case just don't do anything, leave the node in the graveyard. Without special case
1584
+ // it would be a move operation that moves 0 nodes, so maybe it is better just to return no-op.
1585
+ //
1586
+ if (a.howMany == 1) {
1587
+ if (!context.bWasUndone) {
1588
+ return [new NoOperation(0)];
1589
+ }
1590
+ else {
1591
+ a.sourcePosition = b.graveyardPosition.clone();
1592
+ a.targetPosition = a.targetPosition._getTransformedByMergeOperation(b);
1593
+ return [a];
1594
+ }
1595
+ }
1596
+ }
1597
+ }
1598
+ // The default case.
1599
+ //
1600
+ const moveRange = Range._createFromPositionAndShift(a.sourcePosition, a.howMany);
1601
+ const transformed = moveRange._getTransformedByMergeOperation(b);
1602
+ a.sourcePosition = transformed.start;
1603
+ a.howMany = transformed.end.offset - transformed.start.offset;
1604
+ a.targetPosition = a.targetPosition._getTransformedByMergeOperation(b);
1605
+ return [a];
1606
+ });
1894
1607
  // -----------------------
1895
-
1896
- setTransformation( RenameOperation, InsertOperation, ( a, b ) => {
1897
- a.position = a.position._getTransformedByInsertOperation( b );
1898
-
1899
- return [ a ];
1900
- } );
1901
-
1902
- setTransformation( RenameOperation, MergeOperation, ( a, b ) => {
1903
- // Case 1:
1904
- //
1905
- // Element to rename got merged, so it was moved to `b.graveyardPosition`.
1906
- //
1907
- if ( a.position.isEqual( b.deletionPosition ) ) {
1908
- a.position = b.graveyardPosition.clone();
1909
- a.position.stickiness = 'toNext';
1910
-
1911
- return [ a ];
1912
- }
1913
-
1914
- a.position = a.position._getTransformedByMergeOperation( b );
1915
-
1916
- return [ a ];
1917
- } );
1918
-
1919
- setTransformation( RenameOperation, MoveOperation, ( a, b ) => {
1920
- a.position = a.position._getTransformedByMoveOperation( b );
1921
-
1922
- return [ a ];
1923
- } );
1924
-
1925
- setTransformation( RenameOperation, RenameOperation, ( a, b, context ) => {
1926
- if ( a.position.isEqual( b.position ) ) {
1927
- if ( context.aIsStrong ) {
1928
- a.oldName = b.newName;
1929
- } else {
1930
- return [ new NoOperation( 0 ) ];
1931
- }
1932
- }
1933
-
1934
- return [ a ];
1935
- } );
1936
-
1937
- setTransformation( RenameOperation, SplitOperation, ( a, b ) => {
1938
- // Case 1:
1939
- //
1940
- // The element to rename has been split. In this case, the new element should be also renamed.
1941
- //
1942
- // User decides to change the paragraph to a list item:
1943
- // <paragraph>Foobar</paragraph>
1944
- //
1945
- // However, in meantime, split happens:
1946
- // <paragraph>Foo</paragraph><paragraph>bar</paragraph>
1947
- //
1948
- // As a result, rename both elements:
1949
- // <listItem>Foo</listItem><listItem>bar</listItem>
1950
- //
1951
- const renamePath = a.position.path;
1952
- const splitPath = b.splitPosition.getParentPath();
1953
-
1954
- if ( compareArrays( renamePath, splitPath ) == 'same' && !b.graveyardPosition ) {
1955
- const extraRename = new RenameOperation( a.position.getShiftedBy( 1 ), a.oldName, a.newName, 0 );
1956
-
1957
- return [ a, extraRename ];
1958
- }
1959
-
1960
- // The default case.
1961
- //
1962
- a.position = a.position._getTransformedBySplitOperation( b );
1963
-
1964
- return [ a ];
1965
- } );
1966
-
1608
+ setTransformation(RenameOperation, InsertOperation, (a, b) => {
1609
+ a.position = a.position._getTransformedByInsertOperation(b);
1610
+ return [a];
1611
+ });
1612
+ setTransformation(RenameOperation, MergeOperation, (a, b) => {
1613
+ // Case 1:
1614
+ //
1615
+ // Element to rename got merged, so it was moved to `b.graveyardPosition`.
1616
+ //
1617
+ if (a.position.isEqual(b.deletionPosition)) {
1618
+ a.position = b.graveyardPosition.clone();
1619
+ a.position.stickiness = 'toNext';
1620
+ return [a];
1621
+ }
1622
+ a.position = a.position._getTransformedByMergeOperation(b);
1623
+ return [a];
1624
+ });
1625
+ setTransformation(RenameOperation, MoveOperation, (a, b) => {
1626
+ a.position = a.position._getTransformedByMoveOperation(b);
1627
+ return [a];
1628
+ });
1629
+ setTransformation(RenameOperation, RenameOperation, (a, b, context) => {
1630
+ if (a.position.isEqual(b.position)) {
1631
+ if (context.aIsStrong) {
1632
+ a.oldName = b.newName;
1633
+ }
1634
+ else {
1635
+ return [new NoOperation(0)];
1636
+ }
1637
+ }
1638
+ return [a];
1639
+ });
1640
+ setTransformation(RenameOperation, SplitOperation, (a, b) => {
1641
+ // Case 1:
1642
+ //
1643
+ // The element to rename has been split. In this case, the new element should be also renamed.
1644
+ //
1645
+ // User decides to change the paragraph to a list item:
1646
+ // <paragraph>Foobar</paragraph>
1647
+ //
1648
+ // However, in meantime, split happens:
1649
+ // <paragraph>Foo</paragraph><paragraph>bar</paragraph>
1650
+ //
1651
+ // As a result, rename both elements:
1652
+ // <listItem>Foo</listItem><listItem>bar</listItem>
1653
+ //
1654
+ const renamePath = a.position.path;
1655
+ const splitPath = b.splitPosition.getParentPath();
1656
+ if (compareArrays(renamePath, splitPath) == 'same' && !b.graveyardPosition) {
1657
+ const extraRename = new RenameOperation(a.position.getShiftedBy(1), a.oldName, a.newName, 0);
1658
+ return [a, extraRename];
1659
+ }
1660
+ // The default case.
1661
+ //
1662
+ a.position = a.position._getTransformedBySplitOperation(b);
1663
+ return [a];
1664
+ });
1967
1665
  // -----------------------
1968
-
1969
- setTransformation( RootAttributeOperation, RootAttributeOperation, ( a, b, context ) => {
1970
- if ( a.root === b.root && a.key === b.key ) {
1971
- if ( !context.aIsStrong || a.newValue === b.newValue ) {
1972
- return [ new NoOperation( 0 ) ];
1973
- } else {
1974
- a.oldValue = b.newValue;
1975
- }
1976
- }
1977
-
1978
- return [ a ];
1979
- } );
1980
-
1666
+ setTransformation(RootAttributeOperation, RootAttributeOperation, (a, b, context) => {
1667
+ if (a.root === b.root && a.key === b.key) {
1668
+ if (!context.aIsStrong || a.newValue === b.newValue) {
1669
+ return [new NoOperation(0)];
1670
+ }
1671
+ else {
1672
+ a.oldValue = b.newValue;
1673
+ }
1674
+ }
1675
+ return [a];
1676
+ });
1981
1677
  // -----------------------
1982
-
1983
- setTransformation( SplitOperation, InsertOperation, ( a, b ) => {
1984
- // The default case.
1985
- //
1986
- if ( a.splitPosition.hasSameParentAs( b.position ) && a.splitPosition.offset < b.position.offset ) {
1987
- a.howMany += b.howMany;
1988
- }
1989
-
1990
- a.splitPosition = a.splitPosition._getTransformedByInsertOperation( b );
1991
- a.insertionPosition = a.insertionPosition._getTransformedByInsertOperation( b );
1992
-
1993
- return [ a ];
1994
- } );
1995
-
1996
- setTransformation( SplitOperation, MergeOperation, ( a, b, context ) => {
1997
- // Case 1:
1998
- //
1999
- // Split element got merged. If two different elements were merged, clients will have different content.
2000
- //
2001
- // Example. Merge at `{}`, split at `[]`:
2002
- // <heading>Foo</heading>{}<paragraph>B[]ar</paragraph>
2003
- //
2004
- // On merge side it will look like this:
2005
- // <heading>FooB[]ar</heading>
2006
- // <heading>FooB</heading><heading>ar</heading>
2007
- //
2008
- // On split side it will look like this:
2009
- // <heading>Foo</heading>{}<paragraph>B</paragraph><paragraph>ar</paragraph>
2010
- // <heading>FooB</heading><paragraph>ar</paragraph>
2011
- //
2012
- // Clearly, the second element is different for both clients.
2013
- //
2014
- // We could use the removed merge element from graveyard as a split element but then clients would have a different
2015
- // model state (in graveyard), because the split side client would still have an element in graveyard (removed by merge).
2016
- //
2017
- // To overcome this, in `SplitOperation` x `MergeOperation` transformation we will add additional `SplitOperation`
2018
- // in the graveyard, which will actually clone the merged-and-deleted element. Then, that cloned element will be
2019
- // used for splitting. Example below.
2020
- //
2021
- // Original state:
2022
- // <heading>Foo</heading>{}<paragraph>B[]ar</paragraph>
2023
- //
2024
- // Merge side client:
2025
- //
2026
- // After merge:
2027
- // <heading>FooB[]ar</heading> graveyard: <paragraph></paragraph>
2028
- //
2029
- // Extra split:
2030
- // <heading>FooB[]ar</heading> graveyard: <paragraph></paragraph><paragraph></paragraph>
2031
- //
2032
- // Use the "cloned" element from graveyard:
2033
- // <heading>FooB</heading><paragraph>ar</paragraph> graveyard: <paragraph></paragraph>
2034
- //
2035
- // Split side client:
2036
- //
2037
- // After split:
2038
- // <heading>Foo</heading>{}<paragraph>B</paragraph><paragraph>ar</paragraph>
2039
- //
2040
- // After merge:
2041
- // <heading>FooB</heading><paragraph>ar</paragraph> graveyard: <paragraph></paragraph>
2042
- //
2043
- // This special case scenario only applies if the original split operation clones the split element.
2044
- // If the original split operation has `graveyardPosition` set, it all doesn't have sense because split operation
2045
- // knows exactly which element it should use. So there would be no original problem with different contents.
2046
- //
2047
- // Additionally, the special case applies only if the merge wasn't already undone.
2048
- //
2049
- if ( !a.graveyardPosition && !context.bWasUndone && a.splitPosition.hasSameParentAs( b.sourcePosition ) ) {
2050
- const splitPath = b.graveyardPosition.path.slice();
2051
- splitPath.push( 0 );
2052
-
2053
- const splitPosition = new Position( b.graveyardPosition.root, splitPath );
2054
- const insertionPosition = SplitOperation.getInsertionPosition( new Position( b.graveyardPosition.root, splitPath ) );
2055
-
2056
- const additionalSplit = new SplitOperation( splitPosition, 0, insertionPosition, null, 0 );
2057
-
2058
- a.splitPosition = a.splitPosition._getTransformedByMergeOperation( b );
2059
- a.insertionPosition = SplitOperation.getInsertionPosition( a.splitPosition );
2060
- a.graveyardPosition = additionalSplit.insertionPosition.clone();
2061
- a.graveyardPosition.stickiness = 'toNext';
2062
-
2063
- return [ additionalSplit, a ];
2064
- }
2065
-
2066
- // The default case.
2067
- //
2068
- if ( a.splitPosition.hasSameParentAs( b.deletionPosition ) && !a.splitPosition.isAfter( b.deletionPosition ) ) {
2069
- a.howMany--;
2070
- }
2071
-
2072
- if ( a.splitPosition.hasSameParentAs( b.targetPosition ) ) {
2073
- a.howMany += b.howMany;
2074
- }
2075
-
2076
- a.splitPosition = a.splitPosition._getTransformedByMergeOperation( b );
2077
- a.insertionPosition = SplitOperation.getInsertionPosition( a.splitPosition );
2078
-
2079
- if ( a.graveyardPosition ) {
2080
- a.graveyardPosition = a.graveyardPosition._getTransformedByMergeOperation( b );
2081
- }
2082
-
2083
- return [ a ];
2084
- } );
2085
-
2086
- setTransformation( SplitOperation, MoveOperation, ( a, b, context ) => {
2087
- const rangeToMove = Range._createFromPositionAndShift( b.sourcePosition, b.howMany );
2088
-
2089
- if ( a.graveyardPosition ) {
2090
- // Case 1:
2091
- //
2092
- // Split operation graveyard node was moved. In this case move operation is stronger. Since graveyard element
2093
- // is already moved to the correct position, we need to only move the nodes after the split position.
2094
- // This will be done by `MoveOperation` instead of `SplitOperation`.
2095
- //
2096
- const gyElementMoved = rangeToMove.start.isEqual( a.graveyardPosition ) || rangeToMove.containsPosition( a.graveyardPosition );
2097
-
2098
- if ( !context.bWasUndone && gyElementMoved ) {
2099
- const sourcePosition = a.splitPosition._getTransformedByMoveOperation( b );
2100
-
2101
- const newParentPosition = a.graveyardPosition._getTransformedByMoveOperation( b );
2102
- const newTargetPath = newParentPosition.path.slice();
2103
- newTargetPath.push( 0 );
2104
-
2105
- const newTargetPosition = new Position( newParentPosition.root, newTargetPath );
2106
- const moveOp = new MoveOperation( sourcePosition, a.howMany, newTargetPosition, 0 );
2107
-
2108
- return [ moveOp ];
2109
- }
2110
-
2111
- a.graveyardPosition = a.graveyardPosition._getTransformedByMoveOperation( b );
2112
- }
2113
-
2114
- // Case 2:
2115
- //
2116
- // Split is at a position where nodes were moved.
2117
- //
2118
- // This is a scenario described in `MoveOperation` x `SplitOperation` transformation but from the
2119
- // "split operation point of view".
2120
- //
2121
- const splitAtTarget = a.splitPosition.isEqual( b.targetPosition );
2122
-
2123
- if ( splitAtTarget && ( context.baRelation == 'insertAtSource' || context.abRelation == 'splitBefore' ) ) {
2124
- a.howMany += b.howMany;
2125
- a.splitPosition = a.splitPosition._getTransformedByDeletion( b.sourcePosition, b.howMany );
2126
- a.insertionPosition = SplitOperation.getInsertionPosition( a.splitPosition );
2127
-
2128
- return [ a ];
2129
- }
2130
-
2131
- if ( splitAtTarget && context.abRelation && context.abRelation.howMany ) {
2132
- const { howMany, offset } = context.abRelation;
2133
-
2134
- a.howMany += howMany;
2135
- a.splitPosition = a.splitPosition.getShiftedBy( offset );
2136
-
2137
- return [ a ];
2138
- }
2139
-
2140
- // Case 3:
2141
- //
2142
- // If the split position is inside the moved range, we need to shift the split position to a proper place.
2143
- // The position cannot be moved together with moved range because that would result in splitting of an incorrect element.
2144
- //
2145
- // Characters `bc` should be moved to the second paragraph while split position is between them:
2146
- // <paragraph>A[b|c]d</paragraph><paragraph>Xyz</paragraph>
2147
- //
2148
- // After move, new split position is incorrect:
2149
- // <paragraph>Ad</paragraph><paragraph>Xb|cyz</paragraph>
2150
- //
2151
- // Correct split position:
2152
- // <paragraph>A|d</paragraph><paragraph>Xbcyz</paragraph>
2153
- //
2154
- // After split:
2155
- // <paragraph>A</paragraph><paragraph>d</paragraph><paragraph>Xbcyz</paragraph>
2156
- //
2157
- if ( a.splitPosition.hasSameParentAs( b.sourcePosition ) && rangeToMove.containsPosition( a.splitPosition ) ) {
2158
- const howManyRemoved = b.howMany - ( a.splitPosition.offset - b.sourcePosition.offset );
2159
- a.howMany -= howManyRemoved;
2160
-
2161
- if ( a.splitPosition.hasSameParentAs( b.targetPosition ) && a.splitPosition.offset < b.targetPosition.offset ) {
2162
- a.howMany += b.howMany;
2163
- }
2164
-
2165
- a.splitPosition = b.sourcePosition.clone();
2166
- a.insertionPosition = SplitOperation.getInsertionPosition( a.splitPosition );
2167
-
2168
- return [ a ];
2169
- }
2170
-
2171
- // The default case.
2172
- // Don't change `howMany` if move operation does not really move anything.
2173
- //
2174
- if ( !b.sourcePosition.isEqual( b.targetPosition ) ) {
2175
- if ( a.splitPosition.hasSameParentAs( b.sourcePosition ) && a.splitPosition.offset <= b.sourcePosition.offset ) {
2176
- a.howMany -= b.howMany;
2177
- }
2178
-
2179
- if ( a.splitPosition.hasSameParentAs( b.targetPosition ) && a.splitPosition.offset < b.targetPosition.offset ) {
2180
- a.howMany += b.howMany;
2181
- }
2182
- }
2183
-
2184
- // Change position stickiness to force a correct transformation.
2185
- a.splitPosition.stickiness = 'toNone';
2186
- a.splitPosition = a.splitPosition._getTransformedByMoveOperation( b );
2187
- a.splitPosition.stickiness = 'toNext';
2188
-
2189
- if ( a.graveyardPosition ) {
2190
- a.insertionPosition = a.insertionPosition._getTransformedByMoveOperation( b );
2191
- } else {
2192
- a.insertionPosition = SplitOperation.getInsertionPosition( a.splitPosition );
2193
- }
2194
-
2195
- return [ a ];
2196
- } );
2197
-
2198
- setTransformation( SplitOperation, SplitOperation, ( a, b, context ) => {
2199
- // Case 1:
2200
- //
2201
- // Split at the same position.
2202
- //
2203
- // If there already was a split at the same position as in `a` operation, it means that the intention
2204
- // conveyed by `a` operation has already been fulfilled and `a` should not do anything (to avoid double split).
2205
- //
2206
- // However, there is a difference if these are new splits or splits created by undo. These have different
2207
- // intentions. Also splits moving back different elements from graveyard have different intentions. They
2208
- // are just different operations.
2209
- //
2210
- // So we cancel split operation only if it was really identical.
2211
- //
2212
- // Also, there is additional case, where split operations aren't identical and should not be cancelled, however the
2213
- // default transformation is incorrect too.
2214
- //
2215
- if ( a.splitPosition.isEqual( b.splitPosition ) ) {
2216
- if ( !a.graveyardPosition && !b.graveyardPosition ) {
2217
- return [ new NoOperation( 0 ) ];
2218
- }
2219
-
2220
- if ( a.graveyardPosition && b.graveyardPosition && a.graveyardPosition.isEqual( b.graveyardPosition ) ) {
2221
- return [ new NoOperation( 0 ) ];
2222
- }
2223
-
2224
- // Use context to know that the `a.splitPosition` should stay where it is.
2225
- // This happens during undo when first a merge operation moved nodes to `a.splitPosition` and now `b` operation undoes that merge.
2226
- if ( context.abRelation == 'splitBefore' ) {
2227
- // Since split is at the same position, there are no nodes left to split.
2228
- a.howMany = 0;
2229
-
2230
- // Note: there was `if ( a.graveyardPosition )` here but it was uncovered in tests and I couldn't find any scenarios for now.
2231
- // That would have to be a `SplitOperation` that didn't come from undo but is transformed by operations that were undone.
2232
- // It could happen if `context` is enabled in collaboration.
2233
- a.graveyardPosition = a.graveyardPosition._getTransformedBySplitOperation( b );
2234
-
2235
- return [ a ];
2236
- }
2237
- }
2238
-
2239
- // Case 2:
2240
- //
2241
- // Same node is using to split different elements. This happens in undo when previously same element was merged to
2242
- // two different elements. This is described in `MergeOperation` x `MergeOperation` transformation.
2243
- //
2244
- // In this case we will follow the same logic. We will assume that `insertionPosition` is same for both
2245
- // split operations. This might not always be true but in the real cases that were experienced it was. After all,
2246
- // if these splits are reverses of merge operations that were merging the same element, then the `insertionPosition`
2247
- // should be same for both of those splits.
2248
- //
2249
- // Again, we will decide which operation is stronger by checking if split happens in graveyard or in non-graveyard root.
2250
- //
2251
- if ( a.graveyardPosition && b.graveyardPosition && a.graveyardPosition.isEqual( b.graveyardPosition ) ) {
2252
- const aInGraveyard = a.splitPosition.root.rootName == '$graveyard';
2253
- const bInGraveyard = b.splitPosition.root.rootName == '$graveyard';
2254
-
2255
- // If `aIsWeak` it means that `a` points to graveyard while `b` doesn't. Don't move nodes then.
2256
- const aIsWeak = aInGraveyard && !bInGraveyard;
2257
-
2258
- // If `bIsWeak` it means that `b` points to graveyard while `a` doesn't. Force moving nodes then.
2259
- const bIsWeak = bInGraveyard && !aInGraveyard;
2260
-
2261
- // Force move if `b` is weak or neither operation is weak but `a` is stronger through `context.aIsStrong`.
2262
- const forceMove = bIsWeak || ( !aIsWeak && context.aIsStrong );
2263
-
2264
- if ( forceMove ) {
2265
- const result = [];
2266
-
2267
- // First we need to move any nodes split by `b` back to where they were.
2268
- // Do it only if `b` actually moved something.
2269
- if ( b.howMany ) {
2270
- result.push( new MoveOperation( b.moveTargetPosition, b.howMany, b.splitPosition, 0 ) );
2271
- }
2272
-
2273
- // Then we need to move nodes from `a` split position to their new element.
2274
- // Do it only if `a` actually should move something.
2275
- if ( a.howMany ) {
2276
- result.push( new MoveOperation( a.splitPosition, a.howMany, a.moveTargetPosition, 0 ) );
2277
- }
2278
-
2279
- return result;
2280
- } else {
2281
- return [ new NoOperation( 0 ) ];
2282
- }
2283
- }
2284
-
2285
- if ( a.graveyardPosition ) {
2286
- a.graveyardPosition = a.graveyardPosition._getTransformedBySplitOperation( b );
2287
- }
2288
-
2289
- // Case 3:
2290
- //
2291
- // Position where operation `b` inserted a new node after split is the same as the operation `a` split position.
2292
- // As in similar cases, there is ambiguity if the split should be before the new node (created by `b`) or after.
2293
- //
2294
- if ( a.splitPosition.isEqual( b.insertionPosition ) && context.abRelation == 'splitBefore' ) {
2295
- a.howMany++;
2296
-
2297
- return [ a ];
2298
- }
2299
-
2300
- // Case 4:
2301
- //
2302
- // This is a mirror to the case 2. above.
2303
- //
2304
- if ( b.splitPosition.isEqual( a.insertionPosition ) && context.baRelation == 'splitBefore' ) {
2305
- const newPositionPath = b.insertionPosition.path.slice();
2306
- newPositionPath.push( 0 );
2307
-
2308
- const newPosition = new Position( b.insertionPosition.root, newPositionPath );
2309
- const moveOp = new MoveOperation( a.insertionPosition, 1, newPosition, 0 );
2310
-
2311
- return [ a, moveOp ];
2312
- }
2313
-
2314
- // The default case.
2315
- //
2316
- if ( a.splitPosition.hasSameParentAs( b.splitPosition ) && a.splitPosition.offset < b.splitPosition.offset ) {
2317
- a.howMany -= b.howMany;
2318
- }
2319
-
2320
- a.splitPosition = a.splitPosition._getTransformedBySplitOperation( b );
2321
- a.insertionPosition = SplitOperation.getInsertionPosition( a.splitPosition );
2322
-
2323
- return [ a ];
2324
- } );
2325
-
1678
+ setTransformation(SplitOperation, InsertOperation, (a, b) => {
1679
+ // The default case.
1680
+ //
1681
+ if (a.splitPosition.hasSameParentAs(b.position) && a.splitPosition.offset < b.position.offset) {
1682
+ a.howMany += b.howMany;
1683
+ }
1684
+ a.splitPosition = a.splitPosition._getTransformedByInsertOperation(b);
1685
+ a.insertionPosition = a.insertionPosition._getTransformedByInsertOperation(b);
1686
+ return [a];
1687
+ });
1688
+ setTransformation(SplitOperation, MergeOperation, (a, b, context) => {
1689
+ // Case 1:
1690
+ //
1691
+ // Split element got merged. If two different elements were merged, clients will have different content.
1692
+ //
1693
+ // Example. Merge at `{}`, split at `[]`:
1694
+ // <heading>Foo</heading>{}<paragraph>B[]ar</paragraph>
1695
+ //
1696
+ // On merge side it will look like this:
1697
+ // <heading>FooB[]ar</heading>
1698
+ // <heading>FooB</heading><heading>ar</heading>
1699
+ //
1700
+ // On split side it will look like this:
1701
+ // <heading>Foo</heading>{}<paragraph>B</paragraph><paragraph>ar</paragraph>
1702
+ // <heading>FooB</heading><paragraph>ar</paragraph>
1703
+ //
1704
+ // Clearly, the second element is different for both clients.
1705
+ //
1706
+ // We could use the removed merge element from graveyard as a split element but then clients would have a different
1707
+ // model state (in graveyard), because the split side client would still have an element in graveyard (removed by merge).
1708
+ //
1709
+ // To overcome this, in `SplitOperation` x `MergeOperation` transformation we will add additional `SplitOperation`
1710
+ // in the graveyard, which will actually clone the merged-and-deleted element. Then, that cloned element will be
1711
+ // used for splitting. Example below.
1712
+ //
1713
+ // Original state:
1714
+ // <heading>Foo</heading>{}<paragraph>B[]ar</paragraph>
1715
+ //
1716
+ // Merge side client:
1717
+ //
1718
+ // After merge:
1719
+ // <heading>FooB[]ar</heading> graveyard: <paragraph></paragraph>
1720
+ //
1721
+ // Extra split:
1722
+ // <heading>FooB[]ar</heading> graveyard: <paragraph></paragraph><paragraph></paragraph>
1723
+ //
1724
+ // Use the "cloned" element from graveyard:
1725
+ // <heading>FooB</heading><paragraph>ar</paragraph> graveyard: <paragraph></paragraph>
1726
+ //
1727
+ // Split side client:
1728
+ //
1729
+ // After split:
1730
+ // <heading>Foo</heading>{}<paragraph>B</paragraph><paragraph>ar</paragraph>
1731
+ //
1732
+ // After merge:
1733
+ // <heading>FooB</heading><paragraph>ar</paragraph> graveyard: <paragraph></paragraph>
1734
+ //
1735
+ // This special case scenario only applies if the original split operation clones the split element.
1736
+ // If the original split operation has `graveyardPosition` set, it all doesn't have sense because split operation
1737
+ // knows exactly which element it should use. So there would be no original problem with different contents.
1738
+ //
1739
+ // Additionally, the special case applies only if the merge wasn't already undone.
1740
+ //
1741
+ if (!a.graveyardPosition && !context.bWasUndone && a.splitPosition.hasSameParentAs(b.sourcePosition)) {
1742
+ const splitPath = b.graveyardPosition.path.slice();
1743
+ splitPath.push(0);
1744
+ const splitPosition = new Position(b.graveyardPosition.root, splitPath);
1745
+ const insertionPosition = SplitOperation.getInsertionPosition(new Position(b.graveyardPosition.root, splitPath));
1746
+ const additionalSplit = new SplitOperation(splitPosition, 0, insertionPosition, null, 0);
1747
+ a.splitPosition = a.splitPosition._getTransformedByMergeOperation(b);
1748
+ a.insertionPosition = SplitOperation.getInsertionPosition(a.splitPosition);
1749
+ a.graveyardPosition = additionalSplit.insertionPosition.clone();
1750
+ a.graveyardPosition.stickiness = 'toNext';
1751
+ return [additionalSplit, a];
1752
+ }
1753
+ // The default case.
1754
+ //
1755
+ if (a.splitPosition.hasSameParentAs(b.deletionPosition) && !a.splitPosition.isAfter(b.deletionPosition)) {
1756
+ a.howMany--;
1757
+ }
1758
+ if (a.splitPosition.hasSameParentAs(b.targetPosition)) {
1759
+ a.howMany += b.howMany;
1760
+ }
1761
+ a.splitPosition = a.splitPosition._getTransformedByMergeOperation(b);
1762
+ a.insertionPosition = SplitOperation.getInsertionPosition(a.splitPosition);
1763
+ if (a.graveyardPosition) {
1764
+ a.graveyardPosition = a.graveyardPosition._getTransformedByMergeOperation(b);
1765
+ }
1766
+ return [a];
1767
+ });
1768
+ setTransformation(SplitOperation, MoveOperation, (a, b, context) => {
1769
+ const rangeToMove = Range._createFromPositionAndShift(b.sourcePosition, b.howMany);
1770
+ if (a.graveyardPosition) {
1771
+ // Case 1:
1772
+ //
1773
+ // Split operation graveyard node was moved. In this case move operation is stronger. Since graveyard element
1774
+ // is already moved to the correct position, we need to only move the nodes after the split position.
1775
+ // This will be done by `MoveOperation` instead of `SplitOperation`.
1776
+ //
1777
+ const gyElementMoved = rangeToMove.start.isEqual(a.graveyardPosition) || rangeToMove.containsPosition(a.graveyardPosition);
1778
+ if (!context.bWasUndone && gyElementMoved) {
1779
+ const sourcePosition = a.splitPosition._getTransformedByMoveOperation(b);
1780
+ const newParentPosition = a.graveyardPosition._getTransformedByMoveOperation(b);
1781
+ const newTargetPath = newParentPosition.path.slice();
1782
+ newTargetPath.push(0);
1783
+ const newTargetPosition = new Position(newParentPosition.root, newTargetPath);
1784
+ const moveOp = new MoveOperation(sourcePosition, a.howMany, newTargetPosition, 0);
1785
+ return [moveOp];
1786
+ }
1787
+ a.graveyardPosition = a.graveyardPosition._getTransformedByMoveOperation(b);
1788
+ }
1789
+ // Case 2:
1790
+ //
1791
+ // Split is at a position where nodes were moved.
1792
+ //
1793
+ // This is a scenario described in `MoveOperation` x `SplitOperation` transformation but from the
1794
+ // "split operation point of view".
1795
+ //
1796
+ const splitAtTarget = a.splitPosition.isEqual(b.targetPosition);
1797
+ if (splitAtTarget && (context.baRelation == 'insertAtSource' || context.abRelation == 'splitBefore')) {
1798
+ a.howMany += b.howMany;
1799
+ a.splitPosition = a.splitPosition._getTransformedByDeletion(b.sourcePosition, b.howMany);
1800
+ a.insertionPosition = SplitOperation.getInsertionPosition(a.splitPosition);
1801
+ return [a];
1802
+ }
1803
+ if (splitAtTarget && context.abRelation && context.abRelation.howMany) {
1804
+ const { howMany, offset } = context.abRelation;
1805
+ a.howMany += howMany;
1806
+ a.splitPosition = a.splitPosition.getShiftedBy(offset);
1807
+ return [a];
1808
+ }
1809
+ // Case 3:
1810
+ //
1811
+ // If the split position is inside the moved range, we need to shift the split position to a proper place.
1812
+ // The position cannot be moved together with moved range because that would result in splitting of an incorrect element.
1813
+ //
1814
+ // Characters `bc` should be moved to the second paragraph while split position is between them:
1815
+ // <paragraph>A[b|c]d</paragraph><paragraph>Xyz</paragraph>
1816
+ //
1817
+ // After move, new split position is incorrect:
1818
+ // <paragraph>Ad</paragraph><paragraph>Xb|cyz</paragraph>
1819
+ //
1820
+ // Correct split position:
1821
+ // <paragraph>A|d</paragraph><paragraph>Xbcyz</paragraph>
1822
+ //
1823
+ // After split:
1824
+ // <paragraph>A</paragraph><paragraph>d</paragraph><paragraph>Xbcyz</paragraph>
1825
+ //
1826
+ if (a.splitPosition.hasSameParentAs(b.sourcePosition) && rangeToMove.containsPosition(a.splitPosition)) {
1827
+ const howManyRemoved = b.howMany - (a.splitPosition.offset - b.sourcePosition.offset);
1828
+ a.howMany -= howManyRemoved;
1829
+ if (a.splitPosition.hasSameParentAs(b.targetPosition) && a.splitPosition.offset < b.targetPosition.offset) {
1830
+ a.howMany += b.howMany;
1831
+ }
1832
+ a.splitPosition = b.sourcePosition.clone();
1833
+ a.insertionPosition = SplitOperation.getInsertionPosition(a.splitPosition);
1834
+ return [a];
1835
+ }
1836
+ // The default case.
1837
+ // Don't change `howMany` if move operation does not really move anything.
1838
+ //
1839
+ if (!b.sourcePosition.isEqual(b.targetPosition)) {
1840
+ if (a.splitPosition.hasSameParentAs(b.sourcePosition) && a.splitPosition.offset <= b.sourcePosition.offset) {
1841
+ a.howMany -= b.howMany;
1842
+ }
1843
+ if (a.splitPosition.hasSameParentAs(b.targetPosition) && a.splitPosition.offset < b.targetPosition.offset) {
1844
+ a.howMany += b.howMany;
1845
+ }
1846
+ }
1847
+ // Change position stickiness to force a correct transformation.
1848
+ a.splitPosition.stickiness = 'toNone';
1849
+ a.splitPosition = a.splitPosition._getTransformedByMoveOperation(b);
1850
+ a.splitPosition.stickiness = 'toNext';
1851
+ if (a.graveyardPosition) {
1852
+ a.insertionPosition = a.insertionPosition._getTransformedByMoveOperation(b);
1853
+ }
1854
+ else {
1855
+ a.insertionPosition = SplitOperation.getInsertionPosition(a.splitPosition);
1856
+ }
1857
+ return [a];
1858
+ });
1859
+ setTransformation(SplitOperation, SplitOperation, (a, b, context) => {
1860
+ // Case 1:
1861
+ //
1862
+ // Split at the same position.
1863
+ //
1864
+ // If there already was a split at the same position as in `a` operation, it means that the intention
1865
+ // conveyed by `a` operation has already been fulfilled and `a` should not do anything (to avoid double split).
1866
+ //
1867
+ // However, there is a difference if these are new splits or splits created by undo. These have different
1868
+ // intentions. Also splits moving back different elements from graveyard have different intentions. They
1869
+ // are just different operations.
1870
+ //
1871
+ // So we cancel split operation only if it was really identical.
1872
+ //
1873
+ // Also, there is additional case, where split operations aren't identical and should not be cancelled, however the
1874
+ // default transformation is incorrect too.
1875
+ //
1876
+ if (a.splitPosition.isEqual(b.splitPosition)) {
1877
+ if (!a.graveyardPosition && !b.graveyardPosition) {
1878
+ return [new NoOperation(0)];
1879
+ }
1880
+ if (a.graveyardPosition && b.graveyardPosition && a.graveyardPosition.isEqual(b.graveyardPosition)) {
1881
+ return [new NoOperation(0)];
1882
+ }
1883
+ // Use context to know that the `a.splitPosition` should stay where it is.
1884
+ // This happens during undo when first a merge operation moved nodes to `a.splitPosition` and now `b` operation undoes that merge.
1885
+ if (context.abRelation == 'splitBefore') {
1886
+ // Since split is at the same position, there are no nodes left to split.
1887
+ a.howMany = 0;
1888
+ // Note: there was `if ( a.graveyardPosition )` here but it was uncovered in tests and I couldn't find any scenarios for now.
1889
+ // That would have to be a `SplitOperation` that didn't come from undo but is transformed by operations that were undone.
1890
+ // It could happen if `context` is enabled in collaboration.
1891
+ a.graveyardPosition = a.graveyardPosition._getTransformedBySplitOperation(b);
1892
+ return [a];
1893
+ }
1894
+ }
1895
+ // Case 2:
1896
+ //
1897
+ // Same node is using to split different elements. This happens in undo when previously same element was merged to
1898
+ // two different elements. This is described in `MergeOperation` x `MergeOperation` transformation.
1899
+ //
1900
+ // In this case we will follow the same logic. We will assume that `insertionPosition` is same for both
1901
+ // split operations. This might not always be true but in the real cases that were experienced it was. After all,
1902
+ // if these splits are reverses of merge operations that were merging the same element, then the `insertionPosition`
1903
+ // should be same for both of those splits.
1904
+ //
1905
+ // Again, we will decide which operation is stronger by checking if split happens in graveyard or in non-graveyard root.
1906
+ //
1907
+ if (a.graveyardPosition && b.graveyardPosition && a.graveyardPosition.isEqual(b.graveyardPosition)) {
1908
+ const aInGraveyard = a.splitPosition.root.rootName == '$graveyard';
1909
+ const bInGraveyard = b.splitPosition.root.rootName == '$graveyard';
1910
+ // If `aIsWeak` it means that `a` points to graveyard while `b` doesn't. Don't move nodes then.
1911
+ const aIsWeak = aInGraveyard && !bInGraveyard;
1912
+ // If `bIsWeak` it means that `b` points to graveyard while `a` doesn't. Force moving nodes then.
1913
+ const bIsWeak = bInGraveyard && !aInGraveyard;
1914
+ // Force move if `b` is weak or neither operation is weak but `a` is stronger through `context.aIsStrong`.
1915
+ const forceMove = bIsWeak || (!aIsWeak && context.aIsStrong);
1916
+ if (forceMove) {
1917
+ const result = [];
1918
+ // First we need to move any nodes split by `b` back to where they were.
1919
+ // Do it only if `b` actually moved something.
1920
+ if (b.howMany) {
1921
+ result.push(new MoveOperation(b.moveTargetPosition, b.howMany, b.splitPosition, 0));
1922
+ }
1923
+ // Then we need to move nodes from `a` split position to their new element.
1924
+ // Do it only if `a` actually should move something.
1925
+ if (a.howMany) {
1926
+ result.push(new MoveOperation(a.splitPosition, a.howMany, a.moveTargetPosition, 0));
1927
+ }
1928
+ return result;
1929
+ }
1930
+ else {
1931
+ return [new NoOperation(0)];
1932
+ }
1933
+ }
1934
+ if (a.graveyardPosition) {
1935
+ a.graveyardPosition = a.graveyardPosition._getTransformedBySplitOperation(b);
1936
+ }
1937
+ // Case 3:
1938
+ //
1939
+ // Position where operation `b` inserted a new node after split is the same as the operation `a` split position.
1940
+ // As in similar cases, there is ambiguity if the split should be before the new node (created by `b`) or after.
1941
+ //
1942
+ if (a.splitPosition.isEqual(b.insertionPosition) && context.abRelation == 'splitBefore') {
1943
+ a.howMany++;
1944
+ return [a];
1945
+ }
1946
+ // Case 4:
1947
+ //
1948
+ // This is a mirror to the case 2. above.
1949
+ //
1950
+ if (b.splitPosition.isEqual(a.insertionPosition) && context.baRelation == 'splitBefore') {
1951
+ const newPositionPath = b.insertionPosition.path.slice();
1952
+ newPositionPath.push(0);
1953
+ const newPosition = new Position(b.insertionPosition.root, newPositionPath);
1954
+ const moveOp = new MoveOperation(a.insertionPosition, 1, newPosition, 0);
1955
+ return [a, moveOp];
1956
+ }
1957
+ // The default case.
1958
+ //
1959
+ if (a.splitPosition.hasSameParentAs(b.splitPosition) && a.splitPosition.offset < b.splitPosition.offset) {
1960
+ a.howMany -= b.howMany;
1961
+ }
1962
+ a.splitPosition = a.splitPosition._getTransformedBySplitOperation(b);
1963
+ a.insertionPosition = SplitOperation.getInsertionPosition(a.splitPosition);
1964
+ return [a];
1965
+ });
2326
1966
  // Checks whether `MoveOperation` `targetPosition` is inside a node from the moved range of the other `MoveOperation`.
2327
1967
  //
2328
1968
  // @private
2329
1969
  // @param {module:engine/model/operation/moveoperation~MoveOperation} a
2330
1970
  // @param {module:engine/model/operation/moveoperation~MoveOperation} b
2331
1971
  // @returns {Boolean}
2332
- function _moveTargetIntoMovedRange( a, b ) {
2333
- return a.targetPosition._getTransformedByDeletion( b.sourcePosition, b.howMany ) === null;
1972
+ function _moveTargetIntoMovedRange(a, b) {
1973
+ return a.targetPosition._getTransformedByDeletion(b.sourcePosition, b.howMany) === null;
2334
1974
  }
2335
-
2336
1975
  // Helper function for `MoveOperation` x `MoveOperation` transformation. Converts given ranges and target position to
2337
1976
  // move operations and returns them.
2338
1977
  //
@@ -2346,44 +1985,34 @@ function _moveTargetIntoMovedRange( a, b ) {
2346
1985
  // @param {Array.<module:engine/model/range~Range>} ranges
2347
1986
  // @param {module:engine/model/position~Position} targetPosition
2348
1987
  // @returns {Array.<module:engine/model/operation/moveoperation~MoveOperation>}
2349
- function _makeMoveOperationsFromRanges( ranges, targetPosition ) {
2350
- // At this moment we have some ranges and a target position, to which those ranges should be moved.
2351
- // Order in `ranges` array is the go-to order of after transformation.
2352
- //
2353
- // We are almost done. We have `ranges` and `targetPosition` to make operations from.
2354
- // Unfortunately, those operations may affect each other. Precisely, first operation after move
2355
- // may affect source range and target position of second and third operation. Same with second
2356
- // operation affecting third.
2357
- //
2358
- // We need to fix those source ranges and target positions once again, before converting `ranges` to operations.
2359
- const operations = [];
2360
-
2361
- // Keep in mind that nothing will be transformed if there is just one range in `ranges`.
2362
- for ( let i = 0; i < ranges.length; i++ ) {
2363
- // Create new operation out of a range and target position.
2364
- const range = ranges[ i ];
2365
- const op = new MoveOperation(
2366
- range.start,
2367
- range.end.offset - range.start.offset,
2368
- targetPosition,
2369
- 0
2370
- );
2371
-
2372
- operations.push( op );
2373
-
2374
- // Transform other ranges by the generated operation.
2375
- for ( let j = i + 1; j < ranges.length; j++ ) {
2376
- // All ranges in `ranges` array should be:
2377
- //
2378
- // * non-intersecting (these are part of original operation source range), and
2379
- // * `targetPosition` does not target into them (opposite would mean that transformed operation targets "inside itself").
2380
- //
2381
- // This means that the transformation will be "clean" and always return one result.
2382
- ranges[ j ] = ranges[ j ]._getTransformedByMove( op.sourcePosition, op.targetPosition, op.howMany )[ 0 ];
2383
- }
2384
-
2385
- targetPosition = targetPosition._getTransformedByMove( op.sourcePosition, op.targetPosition, op.howMany );
2386
- }
2387
-
2388
- return operations;
1988
+ function _makeMoveOperationsFromRanges(ranges, targetPosition) {
1989
+ // At this moment we have some ranges and a target position, to which those ranges should be moved.
1990
+ // Order in `ranges` array is the go-to order of after transformation.
1991
+ //
1992
+ // We are almost done. We have `ranges` and `targetPosition` to make operations from.
1993
+ // Unfortunately, those operations may affect each other. Precisely, first operation after move
1994
+ // may affect source range and target position of second and third operation. Same with second
1995
+ // operation affecting third.
1996
+ //
1997
+ // We need to fix those source ranges and target positions once again, before converting `ranges` to operations.
1998
+ const operations = [];
1999
+ // Keep in mind that nothing will be transformed if there is just one range in `ranges`.
2000
+ for (let i = 0; i < ranges.length; i++) {
2001
+ // Create new operation out of a range and target position.
2002
+ const range = ranges[i];
2003
+ const op = new MoveOperation(range.start, range.end.offset - range.start.offset, targetPosition, 0);
2004
+ operations.push(op);
2005
+ // Transform other ranges by the generated operation.
2006
+ for (let j = i + 1; j < ranges.length; j++) {
2007
+ // All ranges in `ranges` array should be:
2008
+ //
2009
+ // * non-intersecting (these are part of original operation source range), and
2010
+ // * `targetPosition` does not target into them (opposite would mean that transformed operation targets "inside itself").
2011
+ //
2012
+ // This means that the transformation will be "clean" and always return one result.
2013
+ ranges[j] = ranges[j]._getTransformedByMove(op.sourcePosition, op.targetPosition, op.howMany)[0];
2014
+ }
2015
+ targetPosition = targetPosition._getTransformedByMove(op.sourcePosition, op.targetPosition, op.howMany);
2016
+ }
2017
+ return operations;
2389
2018
  }