-
Notifications
You must be signed in to change notification settings - Fork 3.6k
/
transform.ts
2455 lines (2101 loc) · 98.4 KB
/
transform.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @module engine/model/operation/transform
*/
import InsertOperation from './insertoperation.js';
import AttributeOperation from './attributeoperation.js';
import RenameOperation from './renameoperation.js';
import MarkerOperation from './markeroperation.js';
import MoveOperation from './moveoperation.js';
import RootAttributeOperation from './rootattributeoperation.js';
import RootOperation from './rootoperation.js';
import MergeOperation from './mergeoperation.js';
import SplitOperation from './splitoperation.js';
import NoOperation from './nooperation.js';
import Range from '../range.js';
import Position from '../position.js';
import type Operation from './operation.js';
import type Document from '../document.js';
import type History from '../history.js';
import { compareArrays } from '@ckeditor/ckeditor5-utils';
type TransformationFunction = ( a: Operation, b: Operation, context: TransformationContext ) => Array<Operation>;
const transformations = new Map<Function, Map<Function, TransformationFunction>>();
/**
* Sets a transformation function to be be used to transform instances of class `OperationA` by instances of class `OperationB`.
*
* The `transformationFunction` is passed three parameters:
*
* * `a` - operation to be transformed, an instance of `OperationA`,
* * `b` - operation to be transformed by, an instance of `OperationB`,
* * {@link module:engine/model/operation/transform~TransformationContext `context`} - object with additional information about
* transformation context.
*
* The `transformationFunction` should return transformation result, which is an array with one or multiple
* {@link module:engine/model/operation/operation~Operation operation} instances.
*
* @param transformationFunction Function to use for transforming.
*/
function setTransformation<
ClassA extends new ( ...args: Array<any> ) => Operation,
ClassB extends new ( ...args: Array<any> ) => Operation
>(
OperationA: ClassA,
OperationB: ClassB,
transformationFunction: ( a: InstanceType<ClassA>, b: InstanceType<ClassB>, context: TransformationContext ) => Array<Operation>
) {
let aGroup = transformations.get( OperationA );
if ( !aGroup ) {
aGroup = new Map();
transformations.set( OperationA, aGroup );
}
aGroup.set( OperationB, transformationFunction as TransformationFunction );
}
/**
* Returns a previously set transformation function for transforming an instance of `OperationA` by an instance of `OperationB`.
*
* If no transformation was set for given pair of operations, {@link module:engine/model/operation/transform~noUpdateTransformation}
* is returned. This means that if no transformation was set, the `OperationA` instance will not change when transformed
* by the `OperationB` instance.
*
* @returns Function set to transform an instance of `OperationA` by an instance of `OperationB`.
*/
function getTransformation( OperationA: Function, OperationB: Function ): TransformationFunction {
const aGroup = transformations.get( OperationA );
if ( aGroup && aGroup.has( OperationB ) ) {
return aGroup.get( OperationB )!;
}
return noUpdateTransformation;
}
/**
* A transformation function that only clones operation to transform, without changing it.
*/
function noUpdateTransformation( a: Operation ): Array<Operation> {
return [ a ];
}
/**
* Transforms operation `a` by operation `b`.
*
* @param a Operation to be transformed.
* @param b Operation to transform by.
* @param context Transformation context for this transformation.
* @returns Transformation result.
*/
export function transform( a: Operation, b: Operation, context: TransformationContext = {} ): Array<Operation> {
const transformationFunction = getTransformation( a.constructor, b.constructor );
/* eslint-disable no-useless-catch */
try {
a = a.clone();
return transformationFunction( a, b, context );
} catch ( e: any ) {
// @if CK_DEBUG // console.warn( 'Error during operation transformation!', e.message );
// @if CK_DEBUG // console.warn( 'Transformed operation', a );
// @if CK_DEBUG // console.warn( 'Operation transformed by', b );
// @if CK_DEBUG // console.warn( 'context.aIsStrong', context.aIsStrong );
// @if CK_DEBUG // console.warn( 'context.aWasUndone', context.aWasUndone );
// @if CK_DEBUG // console.warn( 'context.bWasUndone', context.bWasUndone );
// @if CK_DEBUG // console.warn( 'context.abRelation', context.abRelation );
// @if CK_DEBUG // console.warn( 'context.baRelation', context.baRelation );
throw e;
}
/* eslint-enable no-useless-catch */
}
/**
* Performs a transformation of two sets of operations - `operationsA` and `operationsB`. The transformation is two-way -
* both transformed `operationsA` and transformed `operationsB` are returned.
*
* Note, that the first operation in each set should base on the same document state (
* {@link module:engine/model/document~Document#version document version}).
*
* It is assumed that `operationsA` are "more important" during conflict resolution between two operations.
*
* New copies of both passed arrays and operations inside them are returned. Passed arguments are not altered.
*
* Base versions of the transformed operations sets are updated accordingly. For example, assume that base versions are `4`
* and there are `3` operations in `operationsA` and `5` operations in `operationsB`. Then:
*
* * transformed `operationsA` will start from base version `9` (`4` base version + `5` operations B),
* * transformed `operationsB` will start from base version `7` (`4` base version + `3` operations A).
*
* If no operation was broken into two during transformation, then both sets will end up with an operation that bases on version `11`:
*
* * transformed `operationsA` start from `9` and there are `3` of them, so the last will have `baseVersion` equal to `11`,
* * transformed `operationsB` start from `7` and there are `5` of them, so the last will have `baseVersion` equal to `11`.
*
* @param operationsA
* @param operationsB
* @param options Additional transformation options.
* @param options.document Document which the operations change.
* @param options.useRelations Whether during transformation relations should be used (used during undo for better conflict resolution).
* @param options.padWithNoOps Whether additional {@link module:engine/model/operation/nooperation~NoOperation}s
* should be added to the transformation results to force the same last base version for both transformed sets (in case
* if some operations got broken into multiple operations during transformation).
* @param options.forceWeakRemove If set to `false`, remove operation will be always stronger than move operation,
* so the removed nodes won't end up back in the document root. When set to `true`, context data will be used.
* @returns Transformation result.
*/
export function transformSets(
operationsA: Array<Operation>,
operationsB: Array<Operation>,
options: {
document: Document;
useRelations?: boolean;
padWithNoOps?: boolean;
forceWeakRemove?: boolean;
}
): TransformSetsResult {
// Create new arrays so the originally passed arguments are not changed.
// No need to clone operations, they are cloned as they are transformed.
operationsA = operationsA.slice();
operationsB = operationsB.slice();
const contextFactory = new ContextFactory( options.document, options.useRelations, options.forceWeakRemove );
contextFactory.setOriginalOperations( operationsA );
contextFactory.setOriginalOperations( operationsB );
const originalOperations = contextFactory.originalOperations;
// If one of sets is empty there is simply nothing to transform, so return sets as they are.
if ( operationsA.length == 0 || operationsB.length == 0 ) {
return { operationsA, operationsB, originalOperations };
}
//
// Following is a description of transformation process:
//
// There are `operationsA` and `operationsB` to be transformed, both by both.
//
// So, suppose we have sets of two operations each: `operationsA` = `[ a1, a2 ]`, `operationsB` = `[ b1, b2 ]`.
//
// Remember, that we can only transform operations that base on the same context. We assert that `a1` and `b1` base on
// the same context and we transform them. Then, we get `a1'` and `b1'`. `a2` bases on a context with `a1` -- `a2`
// is an operation that followed `a1`. Similarly, `b2` bases on a context with `b1`.
//
// However, since `a1'` is a result of transformation by `b1`, `a1'` now also has a context with `b1`. This means that
// we can safely transform `a1'` by `b2`. As we finish transforming `a1`, we also transformed all `operationsB`.
// All `operationsB` also have context including `a1`. Now, we can properly transform `a2` by those operations.
//
// The transformation process can be visualized on a transformation diagram ("diamond diagram"):
//
// [the initial state]
// [common for a1 and b1]
//
// *
// / \
// / \
// b1 a1
// / \
// / \
// * *
// / \ / \
// / \ / \
// b2 a1' b1' a2
// / \ / \
// / \ / \
// * * *
// \ / \ /
// \ / \ /
// a1'' b2' a2' b1''
// \ / \ /
// \ / \ /
// * *
// \ /
// \ /
// a2'' b2''
// \ /
// \ /
// *
//
// [the final state]
//
// The final state can be reached from the initial state by applying `a1`, `a2`, `b1''` and `b2''`, as well as by
// applying `b1`, `b2`, `a1''`, `a2''`. Note how the operations get to a proper common state before each pair is
// transformed.
//
// Another thing to consider is that an operation during transformation can be broken into multiple operations.
// Suppose that `a1` * `b1` = `[ a11', a12' ]` (instead of `a1'` that we considered previously).
//
// In that case, we leave `a12'` for later and we continue transforming `a11'` until it is transformed by all `operationsB`
// (in our case it is just `b2`). At this point, `b1` is transformed by "whole" `a1`, while `b2` is only transformed
// by `a11'`. Similarly, `a12'` is only transformed by `b1`. This leads to a conclusion that we need to start transforming `a12'`
// from the moment just after it was broken. So, `a12'` is transformed by `b2`. Now, "the whole" `a1` is transformed
// by `operationsB`, while all `operationsB` are transformed by "the whole" `a1`. This means that we can continue with
// following `operationsA` (in our case it is just `a2`).
//
// Of course, also `operationsB` can be broken. However, since we focus on transforming operation `a` to the end,
// the only thing to do is to store both pieces of operation `b`, so that the next transformed operation `a` will
// be transformed by both of them.
//
// *
// / \
// / \
// / \
// b1 a1
// / \
// / \
// / \
// * *
// / \ / \
// / a11' / \
// / \ / \
// b2 * b1' a2
// / / \ / \
// / / a12' / \
// / / \ / \
// * b2' * *
// \ / / \ /
// a11'' / b21'' \ /
// \ / / \ /
// * * a2' b1''
// \ / \ \ /
// a12'' b22''\ \ /
// \ / \ \ /
// * a2'' *
// \ \ /
// \ \ b21'''
// \ \ /
// a2''' *
// \ /
// \ b22'''
// \ /
// *
//
// Note, how `a1` is broken and transformed into `a11'` and `a12'`, while `b2'` got broken and transformed into `b21''` and `b22''`.
//
// Having all that on mind, here is an outline for the transformation process algorithm:
//
// 1. We have `operationsA` and `operationsB` array, which we dynamically update as the transformation process goes.
//
// 2. We take next (or first) operation from `operationsA` and check from which operation `b` we need to start transforming it.
// All original `operationsA` are set to be transformed starting from the first operation `b`.
//
// 3. We take operations from `operationsB`, one by one, starting from the correct one, and transform operation `a`
// by operation `b` (and vice versa). We update `operationsA` and `operationsB` by replacing the original operations
// with the transformation results.
//
// 4. If operation is broken into multiple operations, we save all the new operations in the place of the
// original operation.
//
// 5. Additionally, if operation `a` was broken, for the "new" operation, we remember from which operation `b` it should
// be transformed by.
//
// 6. We continue transforming "current" operation `a` until it is transformed by all `operationsB`. Then, go to 2.
// unless the last operation `a` was transformed.
//
// The actual implementation of the above algorithm is slightly different, as only one loop (while) is used.
// The difference is that we have "current" `a` operation to transform and we store the index of the next `b` operation
// to transform by. Each loop operates on two indexes then: index pointing to currently processed `a` operation and
// index pointing to next `b` operation. Each loop is just one `a * b` + `b * a` transformation. After each loop
// operation `b` index is updated. If all `b` operations were visited for the current `a` operation, we change
// current `a` operation index to the next one.
//
// For each operation `a`, keeps information what is the index in `operationsB` from which the transformation should start.
const nextTransformIndex = new WeakMap<Operation, number>();
// For all the original `operationsA`, set that they should be transformed starting from the first of `operationsB`.
for ( const op of operationsA ) {
nextTransformIndex.set( op, 0 );
}
// Additional data that is used for some postprocessing after the main transformation process is done.
const data = {
nextBaseVersionA: operationsA[ operationsA.length - 1 ].baseVersion! + 1,
nextBaseVersionB: operationsB[ operationsB.length - 1 ].baseVersion! + 1,
originalOperationsACount: operationsA.length,
originalOperationsBCount: operationsB.length
};
// Index of currently transformed operation `a`.
let i = 0;
// While not all `operationsA` are transformed...
while ( i < operationsA.length ) {
// Get "current" operation `a`.
const opA = operationsA[ i ];
// For the "current" operation `a`, get the index of the next operation `b` to transform by.
const indexB = nextTransformIndex.get( opA )!;
// If operation `a` was already transformed by every operation `b`, change "current" operation `a` to the next one.
if ( indexB == operationsB.length ) {
i++;
continue;
}
const opB = operationsB[ indexB ];
// Transform `a` by `b` and `b` by `a`.
const newOpsA = transform( opA, opB, contextFactory.getContext( opA, opB, true ) );
const newOpsB = transform( opB, opA, contextFactory.getContext( opB, opA, false ) );
// As a result we get one or more `newOpsA` and one or more `newOpsB` operations.
// Update contextual information about operations.
contextFactory.updateRelation( opA, opB );
contextFactory.setOriginalOperations( newOpsA, opA );
contextFactory.setOriginalOperations( newOpsB, opB );
// For new `a` operations, update their index of the next operation `b` to transform them by.
//
// This is needed even if there was only one result (`a` was not broken) because that information is used
// at the beginning of this loop every time.
for ( const newOpA of newOpsA ) {
// Acknowledge, that operation `b` also might be broken into multiple operations.
//
// This is why we raise `indexB` not just by 1. If `newOpsB` are multiple operations, they will be
// spliced in the place of `opB`. So we need to change `transformBy` accordingly, so that an operation won't
// be transformed by the same operation (part of it) again.
nextTransformIndex.set( newOpA, indexB + newOpsB.length );
}
// Update `operationsA` and `operationsB` with the transformed versions.
operationsA.splice( i, 1, ...newOpsA );
operationsB.splice( indexB, 1, ...newOpsB );
}
if ( options.padWithNoOps ) {
// If no-operations padding is enabled, count how many extra `a` and `b` operations were generated.
const brokenOperationsACount = operationsA.length - data.originalOperationsACount;
const brokenOperationsBCount = operationsB.length - data.originalOperationsBCount;
// Then, if that number is not the same, pad `operationsA` or `operationsB` with correct number of no-ops so
// that the base versions are equalled.
//
// Note that only one array will be updated, as only one of those subtractions can be greater than zero.
padWithNoOps( operationsA, brokenOperationsBCount - brokenOperationsACount );
padWithNoOps( operationsB, brokenOperationsACount - brokenOperationsBCount );
}
// Finally, update base versions of transformed operations.
updateBaseVersions( operationsA, data.nextBaseVersionB );
updateBaseVersions( operationsB, data.nextBaseVersionA );
return { operationsA, operationsB, originalOperations };
}
/**
* The result of {@link module:engine/model/operation/transform~transformSets}.
*/
export interface TransformSetsResult {
/**
* Transformed `operationsA`.
*/
operationsA: Array<Operation>;
/**
* Transformed `operationsB`.
*/
operationsB: Array<Operation>;
/**
* A map that links transformed operations to original operations. The keys are the transformed
* operations and the values are the original operations from the input (`operationsA` and `operationsB`).
*/
originalOperations: Map<Operation, Operation>;
}
/**
* Gathers additional data about operations processed during transformation. Can be used to obtain contextual information
* about two operations that are about to be transformed. This contextual information can be used for better conflict resolution.
*/
class ContextFactory {
public readonly originalOperations: Map<Operation, Operation>;
private readonly _history: History;
private readonly _useRelations?: boolean;
private readonly _forceWeakRemove: boolean;
private readonly _relations: Map<Operation, Map<Operation, any>>;
/**
* Creates `ContextFactory` instance.
*
* @param document Document which the operations change.
* @param useRelations Whether during transformation relations should be used (used during undo for
* better conflict resolution).
* @param forceWeakRemove If set to `false`, remove operation will be always stronger than move operation,
* so the removed nodes won't end up back in the document root. When set to `true`, context data will be used.
*/
constructor( document: Document, useRelations: boolean | undefined, forceWeakRemove = false ) {
// For each operation that is created during transformation process, we keep a reference to the original operation
// which it comes from. The original operation works as a kind of "identifier". Every contextual information
// gathered during transformation that we want to save for given operation, is actually saved for the original operation.
// This way no matter if operation `a` is cloned, then transformed, even breaks, we still have access to the previously
// gathered data through original operation reference.
this.originalOperations = new Map();
// `model.History` instance which information about undone operations will be taken from.
this._history = document.history;
// Whether additional context should be used.
this._useRelations = useRelations;
this._forceWeakRemove = !!forceWeakRemove;
// Relations is a double-map structure (maps in map) where for two operations we store how those operations were related
// to each other. Those relations are evaluated during transformation process. For every transformated pair of operations
// we keep relations between them.
this._relations = new Map();
}
/**
* Sets "original operation" for given operations.
*
* During transformation process, operations are cloned, then changed, then processed again, sometimes broken into two
* or multiple operations. When gathering additional data it is important that all operations can be somehow linked
* so a cloned and transformed "version" still kept track of the data assigned earlier to it.
*
* The original operation object will be used as such an universal linking id. Throughout the transformation process
* all cloned operations will refer to "the original operation" when storing and reading additional data.
*
* If `takeFrom` is not set, each operation from `operations` array will be assigned itself as "the original operation".
* This should be used as an initialization step.
*
* If `takeFrom` is set, each operation from `operations` will be assigned the same original operation as assigned
* for `takeFrom` operation. This should be used to update original operations. It should be used in a way that
* `operations` are the result of `takeFrom` transformation to ensure proper "original operation propagation".
*/
public setOriginalOperations( operations: Array<Operation>, takeFrom: Operation | null = null ): void {
const originalOperation = takeFrom ? this.originalOperations.get( takeFrom ) : null;
for ( const operation of operations ) {
this.originalOperations.set( operation, originalOperation || operation );
}
}
/**
* Saves a relation between operations `opA` and `opB`.
*
* Relations are then later used to help solve conflicts when operations are transformed.
*/
public updateRelation( opA: Operation, opB: Operation ): void {
// The use of relations is described in a bigger detail in transformation functions.
//
// In brief, this function, for specified pairs of operation types, checks how positions defined in those operations relate.
// Then those relations are saved. For example, for two move operations, it is saved if one of those operations target
// position is before the other operation source position. This kind of information gives contextual information when
// transformation is used during undo. Similar checks are done for other pairs of operations.
//
if ( opA instanceof MoveOperation ) {
if ( opB instanceof MergeOperation ) {
if ( opA.targetPosition.isEqual( opB.sourcePosition ) || opB.movedRange.containsPosition( opA.targetPosition ) ) {
this._setRelation( opA, opB, 'insertAtSource' );
} else if ( opA.targetPosition.isEqual( opB.deletionPosition ) ) {
this._setRelation( opA, opB, 'insertBetween' );
} else if ( opA.targetPosition.isAfter( opB.sourcePosition ) ) {
this._setRelation( opA, opB, 'moveTargetAfter' );
}
} else if ( opB instanceof MoveOperation ) {
if ( opA.targetPosition.isEqual( opB.sourcePosition ) || opA.targetPosition.isBefore( opB.sourcePosition ) ) {
this._setRelation( opA, opB, 'insertBefore' );
} else {
this._setRelation( opA, opB, 'insertAfter' );
}
}
} else if ( opA instanceof SplitOperation ) {
if ( opB instanceof MergeOperation ) {
if ( opA.splitPosition.isBefore( opB.sourcePosition ) ) {
this._setRelation( opA, opB, 'splitBefore' );
}
} else if ( opB instanceof MoveOperation ) {
if ( opA.splitPosition.isEqual( opB.sourcePosition ) || opA.splitPosition.isBefore( opB.sourcePosition ) ) {
this._setRelation( opA, opB, 'splitBefore' );
} else {
const range = Range._createFromPositionAndShift( opB.sourcePosition, opB.howMany );
if ( opA.splitPosition.hasSameParentAs( opB.sourcePosition ) && range.containsPosition( opA.splitPosition ) ) {
const howMany = range.end.offset - opA.splitPosition.offset;
const offset = opA.splitPosition.offset - range.start.offset;
this._setRelation( opA, opB, { howMany, offset } );
}
}
}
} else if ( opA instanceof MergeOperation ) {
if ( opB instanceof MergeOperation ) {
if ( !opA.targetPosition.isEqual( opB.sourcePosition ) ) {
this._setRelation( opA, opB, 'mergeTargetNotMoved' );
}
if ( opA.sourcePosition.isEqual( opB.targetPosition ) ) {
this._setRelation( opA, opB, 'mergeSourceNotMoved' );
}
if ( opA.sourcePosition.isEqual( opB.sourcePosition ) ) {
this._setRelation( opA, opB, 'mergeSameElement' );
}
} else if ( opB instanceof SplitOperation ) {
if ( opA.sourcePosition.isEqual( opB.splitPosition ) ) {
this._setRelation( opA, opB, 'splitAtSource' );
}
} else if ( opB instanceof MoveOperation && opB.howMany > 0 ) {
if ( opA.sourcePosition.isEqual( opB.sourcePosition.getShiftedBy( opB.howMany ) ) ) {
this._setRelation( opA, opB, 'mergeSourceAffected' );
}
if ( opA.targetPosition.isEqual( opB.sourcePosition ) ) {
this._setRelation( opA, opB, 'mergeTargetWasBefore' );
}
}
} else if ( opA instanceof MarkerOperation ) {
const markerRange = opA.newRange;
if ( !markerRange ) {
return;
}
if ( opB instanceof MoveOperation ) {
const movedRange = Range._createFromPositionAndShift( opB.sourcePosition, opB.howMany );
const affectedLeft = movedRange.containsPosition( markerRange.start ) ||
movedRange.start.isEqual( markerRange.start );
const affectedRight = movedRange.containsPosition( markerRange.end ) ||
movedRange.end.isEqual( markerRange.end );
if ( ( affectedLeft || affectedRight ) && !movedRange.containsRange( markerRange ) ) {
this._setRelation( opA, opB, {
side: affectedLeft ? 'left' : 'right',
path: affectedLeft ? markerRange.start.path.slice() : markerRange.end.path.slice()
} );
}
} else if ( opB instanceof MergeOperation ) {
const wasInLeftElement = markerRange.start.isEqual( opB.targetPosition );
const wasStartBeforeMergedElement = markerRange.start.isEqual( opB.deletionPosition );
const wasEndBeforeMergedElement = markerRange.end.isEqual( opB.deletionPosition );
const wasInRightElement = markerRange.end.isEqual( opB.sourcePosition );
if ( wasInLeftElement || wasStartBeforeMergedElement || wasEndBeforeMergedElement || wasInRightElement ) {
this._setRelation( opA, opB, {
wasInLeftElement,
wasStartBeforeMergedElement,
wasEndBeforeMergedElement,
wasInRightElement
} );
}
}
}
}
/**
* Evaluates and returns contextual information about two given operations `opA` and `opB` which are about to be transformed.
*/
public getContext( opA: Operation, opB: Operation, aIsStrong: boolean ): TransformationContext {
return {
aIsStrong,
aWasUndone: this._wasUndone( opA ),
bWasUndone: this._wasUndone( opB ),
abRelation: this._useRelations ? this._getRelation( opA, opB ) : null,
baRelation: this._useRelations ? this._getRelation( opB, opA ) : null,
forceWeakRemove: this._forceWeakRemove
};
}
/**
* Returns whether given operation `op` has already been undone.
*
* Information whether an operation was undone gives more context when making a decision when two operations are in conflict.
*/
public _wasUndone( op: Operation ): boolean {
// For `op`, get its original operation. After all, if `op` is a clone (or even transformed clone) of another
// operation, literally `op` couldn't be undone. It was just generated. If anything, it was the operation it origins
// from which was undone. So get that original operation.
const originalOp = this.originalOperations.get( op )!;
// And check with the document if the original operation was undone.
return ( originalOp as any ).wasUndone || this._history.isUndoneOperation( originalOp );
}
/**
* Returns a relation between `opA` and an operation which is undone by `opB`. This can be `String` value if a relation
* was set earlier or `null` if there was no relation between those operations.
*
* This is a little tricky to understand, so let's compare it to `ContextFactory#_wasUndone`.
*
* When `wasUndone( opB )` is used, we check if the `opB` has already been undone. It is obvious, that the
* undoing operation must happen after the undone operation. So, essentially, we have `opB`, we take document history,
* we look forward in the future and ask if in that future `opB` was undone.
*
* Relations is a backward process to `wasUndone()`.
*
* Long story short - using relations is asking what happened in the past. Looking back. This time we have an undoing
* operation `opB` which has undone some other operation. When there is a transformation `opA` x `opB` and there is
* a conflict to solve and `opB` is an undoing operation, we can look back in the history and see what was a relation
* between `opA` and the operation which `opB` undone. Basing on that relation from the past, we can now make
* a better decision when resolving a conflict between two operations, because we know more about the context of
* those two operations.
*
* This is why this function does not return a relation directly between `opA` and `opB` because we need to look
* back to search for a meaningful contextual information.
*/
public _getRelation( opA: Operation, opB: Operation ): any {
// Get the original operation. Similarly as in `wasUndone()` it is used as an universal identifier for stored data.
const origB = this.originalOperations.get( opB )!;
const undoneB = this._history.getUndoneOperation( origB );
// If `opB` is not undoing any operation, there is no relation.
if ( !undoneB ) {
return null;
}
const origA = this.originalOperations.get( opA )!;
const relationsA = this._relations.get( origA );
// Get all relations for `opA`, and check if there is a relation with `opB`-undone-counterpart. If so, return it.
if ( relationsA ) {
return relationsA.get( undoneB ) || null;
}
return null;
}
/**
* Helper function for `ContextFactory#updateRelations`.
*/
private _setRelation( opA: Operation, opB: Operation, relation: any ): void {
// As always, setting is for original operations, not the clones/transformed operations.
const origA = this.originalOperations.get( opA )!;
const origB = this.originalOperations.get( opB )!;
let relationsA = this._relations.get( origA );
if ( !relationsA ) {
relationsA = new Map();
this._relations.set( origA, relationsA );
}
relationsA.set( origB, relation );
}
}
/**
* Holds additional contextual information about a transformed pair of operations (`a` and `b`). Those information
* can be used for better conflict resolving.
*/
export type TransformationContext = {
/**
* Whether `a` is strong operation in this transformation, or weak.
*/
aIsStrong?: boolean;
/**
* Whether `a` operation was undone.
*/
aWasUndone?: boolean;
/**
* Whether `b` operation was undone.
*/
bWasUndone?: boolean;
/**
* The relation between `a` operation and an operation undone by `b` operation.
*/
abRelation?: any;
/**
* The relation between `b` operation and an operation undone by `a` operation.
*/
baRelation?: any;
forceWeakRemove?: boolean;
};
/**
* An utility function that updates {@link module:engine/model/operation/operation~Operation#baseVersion base versions}
* of passed operations.
*
* The function simply sets `baseVersion` as a base version of the first passed operation and then increments it for
* each following operation in `operations`.
*
* @param operations Operations to update.
* @param baseVersion Base version to set for the first operation in `operations`.
*/
function updateBaseVersions( operations: ReadonlyArray<Operation>, baseVersion: number ) {
for ( const operation of operations ) {
operation.baseVersion = baseVersion++;
}
}
/**
* Adds `howMany` instances of {@link module:engine/model/operation/nooperation~NoOperation} to `operations` set.
*/
function padWithNoOps( operations: Array<Operation>, howMany: number ) {
for ( let i = 0; i < howMany; i++ ) {
operations.push( new NoOperation( 0 ) );
}
}
// -----------------------
setTransformation( AttributeOperation, AttributeOperation, ( a, b, context ) => {
// If operations in conflict, check if their ranges intersect and manage them properly.
//
// Operations can be in conflict only if:
//
// * their key is the same (they change the same attribute), and
// * they are in the same parent (operations for ranges [ 1 ] - [ 3 ] and [ 2, 0 ] - [ 2, 5 ] change different
// elements and can't be in conflict).
if ( a.key === b.key && a.range.start.hasSameParentAs( b.range.start ) ) {
// First, we want to apply change to the part of a range that has not been changed by the other operation.
const operations = a.range.getDifference( b.range ).map( range => {
return new AttributeOperation( range, a.key, a.oldValue, a.newValue, 0 );
} );
// Then we take care of the common part of ranges.
const common = a.range.getIntersection( b.range );
if ( common ) {
// If this operation is more important, we also want to apply change to the part of the
// original range that has already been changed by the other operation. Since that range
// got changed we also have to update `oldValue`.
if ( context.aIsStrong ) {
operations.push( new AttributeOperation( common, b.key, b.newValue, a.newValue, 0 ) );
}
}
if ( operations.length == 0 ) {
return [ new NoOperation( 0 ) ];
}
return operations;
} else {
// If operations don't conflict, simply return an array containing just a clone of this operation.
return [ a ];
}
} );
setTransformation( AttributeOperation, InsertOperation, ( a, b ) => {
// Case 1:
//
// The attribute operation range includes the position where nodes were inserted.
// There are two possible scenarios: the inserted nodes were text and they should receive attributes or
// the inserted nodes were elements and they should not receive attributes.
//
if ( a.range.start.hasSameParentAs( b.position ) && a.range.containsPosition( b.position ) ) {
// If new nodes should not receive attributes, two separated ranges will be returned.
// Otherwise, one expanded range will be returned.
const range = a.range._getTransformedByInsertion( b.position, b.howMany, !b.shouldReceiveAttributes );
const result = range.map( r => {
return new AttributeOperation( r, a.key, a.oldValue, a.newValue, a.baseVersion );
} );
if ( b.shouldReceiveAttributes ) {
// `AttributeOperation#range` includes some newly inserted text.
// The operation should also change the attribute of that text. An example:
//
// Bold should be applied on the following range:
// <p>Fo[zb]ar</p>
//
// In meantime, new text is typed:
// <p>Fozxxbar</p>
//
// Bold should be applied also on the new text:
// <p>Fo[zxxb]ar</p>
// <p>Fo<$text bold="true">zxxb</$text>ar</p>
//
// There is a special case to consider here to consider.
//
// Consider setting an attribute with multiple possible values, for example `highlight`. The inserted text might
// have already an attribute value applied and the `oldValue` property of the attribute operation might be wrong:
//
// Attribute `highlight="yellow"` should be applied on the following range:
// <p>Fo[zb]ar<p>
//
// In meantime, character `x` with `highlight="red"` is typed:
// <p>Fo[z<$text highlight="red">x</$text>b]ar</p>
//
// In this case we cannot simply apply operation changing the attribute value from `null` to `"yellow"` for the whole range
// because that would lead to an exception (`oldValue` is incorrect for `x`).
//
// We also cannot break the original range as this would mess up a scenario when there are multiple following
// insert operations, because then only the first inserted character is included in those ranges:
// <p>Fo[z][x][b]ar</p> --> <p>Fo[z][x]x[b]ar</p> --> <p>Fo[z][x]xx[b]ar</p>
//
// So, the attribute range needs be expanded, no matter what attributes are set on the inserted nodes:
//
// <p>Fo[z<$text highlight="red">x</$text>b]ar</p> <--- Change from `null` to `yellow`, throwing an exception.
//
// But before that operation would be applied, we will add an additional attribute operation that will change
// attributes on the inserted nodes in a way which would make the original operation correct:
//
// <p>Fo[z{<$text highlight="red">}x</$text>b]ar</p> <--- Change range `{}` from `red` to `null`.
// <p>Fo[zxb]ar</p> <--- Now change from `null` to `yellow` is completely fine.
//
// Generate complementary attribute operation. Be sure to add it before the original operation.
const op = _getComplementaryAttributeOperations( b, a.key, a.oldValue );
if ( op ) {
result.unshift( op );
}
}
// If nodes should not receive new attribute, we are done here.
return result;
}
// If insert operation is not expanding the attribute operation range, simply transform the range.
a.range = a.range._getTransformedByInsertion( b.position, b.howMany, false )[ 0 ];
return [ a ];
} );
/**
* Helper function for `AttributeOperation` x `InsertOperation` (and reverse) transformation.
*
* For given `insertOperation` it checks the inserted node if it has an attribute `key` set to a value different
* than `newValue`. If so, it generates an `AttributeOperation` which changes the value of `key` attribute to `newValue`.
*/
function _getComplementaryAttributeOperations( insertOperation: InsertOperation, key: string, newValue: unknown ) {
const nodes = insertOperation.nodes;
// At the beginning we store the attribute value from the first node.
const insertValue = nodes.getNode( 0 )!.getAttribute( key );
if ( insertValue == newValue ) {
return null;
}
const range = new Range( insertOperation.position, insertOperation.position.getShiftedBy( insertOperation.howMany ) );
return new AttributeOperation( range, key, insertValue, newValue, 0 );
}
setTransformation( AttributeOperation, MergeOperation, ( a, b ) => {
const ranges = [];
// Case 1:
//
// Attribute change on the merged element. In this case, the merged element was moved to the graveyard.
// An additional attribute operation that will change the (re)moved element needs to be generated.
//
if ( a.range.start.hasSameParentAs( b.deletionPosition ) ) {
if ( a.range.containsPosition( b.deletionPosition ) || a.range.start.isEqual( b.deletionPosition ) ) {
ranges.push( Range._createFromPositionAndShift( b.graveyardPosition, 1 ) );
}
}
const range = a.range._getTransformedByMergeOperation( b );
// Do not add empty (collapsed) ranges to the result. `range` may be collapsed if it contained only the merged element.
if ( !range.isCollapsed ) {
ranges.push( range );
}
// Create `AttributeOperation`s out of the ranges.
return ranges.map( range => {
return new AttributeOperation( range, a.key, a.oldValue, a.newValue, a.baseVersion );
} );
} );
setTransformation( AttributeOperation, MoveOperation, ( a, b ) => {
const ranges = _breakRangeByMoveOperation( a.range, b );
// Create `AttributeOperation`s out of the ranges.
return ranges.map( range => new AttributeOperation( range, a.key, a.oldValue, a.newValue, a.baseVersion ) );
} );
/**
* Helper function for `AttributeOperation` x `MoveOperation` transformation.
*
* Takes the passed `range` and transforms it by move operation `moveOp` in a specific way. Only top-level nodes of `range`
* are considered to be in the range. If move operation moves nodes deep from inside of the range, those nodes won't
* be included in the result. In other words, top-level nodes of the ranges from the result are exactly the same as
* top-level nodes of the original `range`.
*
* This is important for `AttributeOperation` because, for its range, it changes only the top-level nodes. So we need to
* track only how those nodes have been affected by `MoveOperation`.
*/
function _breakRangeByMoveOperation( range: Range, moveOp: MoveOperation ) {
const moveRange = Range._createFromPositionAndShift( moveOp.sourcePosition, moveOp.howMany );
// We are transforming `range` (original range) by `moveRange` (range moved by move operation). As usual when it comes to
// transforming a ranges, we may have a common part of the ranges and we may have a difference part (zero to two ranges).
let common = null;
let difference: Array<Range> = [];
// Let's compare the ranges.
if ( moveRange.containsRange( range, true ) ) {
// If the whole original range is moved, treat it whole as a common part. There's also no difference part.
common = range;
} else if ( range.start.hasSameParentAs( moveRange.start ) ) {
// If the ranges are "on the same level" (in the same parent) then move operation may move exactly those nodes
// that are changed by the attribute operation. In this case we get common part and difference part in the usual way.
difference = range.getDifference( moveRange );
common = range.getIntersection( moveRange );
} else {
// In any other situation we assume that original range is different than move range, that is that move operation
// moves other nodes that attribute operation change. Even if the moved range is deep inside in the original range.
//
// Note that this is different than in `.getIntersection` (we would get a common part in that case) and different
// than `.getDifference` (we would get two ranges).
difference = [ range ];
}
const result: Array<Range> = [];
// The default behaviour of `_getTransformedByMove` might get wrong results for difference part, though, so
// we do it by hand.
for ( let diff of difference ) {
// First, transform the range by removing moved nodes. Since this is a difference, this is safe, `null` won't be returned
// as the range is different than the moved range.
diff = diff._getTransformedByDeletion( moveOp.sourcePosition, moveOp.howMany )!;
// Transform also `targetPosition`.
const targetPosition = moveOp.getMovedRangeStart();
// Spread the range only if moved nodes are inserted only between the top-level nodes of the `diff` range.
const spread = diff.start.hasSameParentAs( targetPosition );
// Transform by insertion of moved nodes.
const diffs = diff._getTransformedByInsertion( targetPosition, moveOp.howMany, spread );
result.push( ...diffs );
}
// Common part can be simply transformed by the move operation. This is because move operation will not target to
// that common part (the operation would have to target inside its own moved range).
if ( common ) {
result.push(
common._getTransformedByMove( moveOp.sourcePosition, moveOp.targetPosition, moveOp.howMany, false )[ 0 ]
);
}
return result;
}
setTransformation( AttributeOperation, SplitOperation, ( a, b ) => {
// Case 1:
//
// Split node is the last node in `AttributeOperation#range`.
// `AttributeOperation#range` needs to be expanded to include the new (split) node.
//
// Attribute `type` to be changed to `numbered` but the `listItem` is split.
// <listItem type="bulleted">foobar</listItem>
//
// After split:
// <listItem type="bulleted">foo</listItem><listItem type="bulleted">bar</listItem>