|
|
@@ -54,7 +54,9 @@ export function transform( a, b, context = {} ) {
|
|
|
const transformationFunction = getTransformation( a, b );
|
|
|
|
|
|
try {
|
|
|
- return transformationFunction( a.clone(), b, context );
|
|
|
+ a = a.clone();
|
|
|
+
|
|
|
+ return transformationFunction( a, b, context );
|
|
|
} catch ( e ) {
|
|
|
log.error( 'Error during operation transformation!' );
|
|
|
log.error( 'Transformed operation', a );
|
|
|
@@ -77,6 +79,12 @@ export function transformSets( operationsA, operationsB, options ) {
|
|
|
return { operationsA, operationsB };
|
|
|
}
|
|
|
|
|
|
+ const nextTransformIndex = new WeakMap();
|
|
|
+
|
|
|
+ for ( const op of operationsA ) {
|
|
|
+ nextTransformIndex.set( op, 0 );
|
|
|
+ }
|
|
|
+
|
|
|
const data = {
|
|
|
nextBaseVersionA: operationsA[ operationsA.length - 1 ].baseVersion + 1,
|
|
|
nextBaseVersionB: operationsB[ operationsB.length - 1 ].baseVersion + 1,
|
|
|
@@ -86,70 +94,57 @@ export function transformSets( operationsA, operationsB, options ) {
|
|
|
|
|
|
const context = initializeContext( operationsA, operationsB, options );
|
|
|
|
|
|
- // For each operation from `operationsA` set...
|
|
|
- for ( let i = 0; i < operationsA.length; ) {
|
|
|
- const opsA = [ operationsA[ i ] ];
|
|
|
-
|
|
|
- // Get an operation from `operationsB` set, and transform them.
|
|
|
- for ( let j = 0; j < operationsB.length; ) {
|
|
|
- const opsB = [ operationsB[ j ] ];
|
|
|
-
|
|
|
- // After transformation both operation `a` and `b` might be broken into two operations.
|
|
|
- // When operation `a` is broken on two operations, it is needed to continue transforming both operations
|
|
|
- // by operations from `operationsB` set. Hence, there is a need for additional loop, that will loop
|
|
|
- // through all currently processed operation. For the same reason, additional loop is needed for operation
|
|
|
- // `b` which may be broken into two operations after transforming by first operation from `opsA`.
|
|
|
- for ( let k = 0; k < opsA.length; ) {
|
|
|
- for ( let l = 0; l < opsB.length; ) {
|
|
|
- const opA = opsA[ k ];
|
|
|
- const opB = opsB[ l ];
|
|
|
-
|
|
|
- if ( options.useContext ) {
|
|
|
- updateRelations( context, opA, opB );
|
|
|
- }
|
|
|
+ let i = 0;
|
|
|
|
|
|
- const contextAB = {
|
|
|
- aIsStrong: true,
|
|
|
- aWasUndone: context.wasUndone( opA ),
|
|
|
- bWasUndone: context.wasUndone( opB ),
|
|
|
- abRelation: context.getRelation( opA, opB ),
|
|
|
- baRelation: context.getRelation( opB, opA )
|
|
|
- };
|
|
|
-
|
|
|
- const contextBA = {
|
|
|
- aIsStrong: false,
|
|
|
- aWasUndone: context.wasUndone( opB ),
|
|
|
- bWasUndone: context.wasUndone( opA ),
|
|
|
- abRelation: context.getRelation( opB, opA ),
|
|
|
- baRelation: context.getRelation( opA, opB )
|
|
|
- };
|
|
|
-
|
|
|
- const newOpA = transform( opA, opB, contextAB );
|
|
|
- const newOpB = transform( opB, opA, contextBA );
|
|
|
-
|
|
|
- if ( options.useContext ) {
|
|
|
- updateOriginalOperation( context, opA, newOpA );
|
|
|
- updateOriginalOperation( context, opB, newOpB );
|
|
|
-
|
|
|
- updateRelations( context, opA, opB );
|
|
|
- }
|
|
|
+ while ( i < operationsA.length ) {
|
|
|
+ const opA = operationsA[ i ];
|
|
|
|
|
|
- opsA.splice( k, 1, ...newOpA );
|
|
|
- k = k + newOpA.length;
|
|
|
+ const transformByIndex = nextTransformIndex.get( opA );
|
|
|
|
|
|
- opsB.splice( l, 1, ...newOpB );
|
|
|
- l = l + newOpB.length;
|
|
|
- }
|
|
|
- }
|
|
|
+ if ( transformByIndex == operationsB.length ) {
|
|
|
+ i++;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ const opB = operationsB[ transformByIndex ];
|
|
|
|
|
|
- operationsB.splice( j, 1, ...opsB );
|
|
|
- j = j + opsB.length;
|
|
|
- data.extraOpsB += opsB.length - 1;
|
|
|
+ if ( options.useContext ) {
|
|
|
+ updateRelations( context, opA, opB );
|
|
|
}
|
|
|
|
|
|
- operationsA.splice( i, 1, ...opsA );
|
|
|
- i = i + opsA.length;
|
|
|
- data.extraOpsA += opsA.length - 1;
|
|
|
+ const contextAB = {
|
|
|
+ aIsStrong: true,
|
|
|
+ aWasUndone: context.wasUndone( opA ),
|
|
|
+ bWasUndone: context.wasUndone( opB ),
|
|
|
+ abRelation: context.getRelation( opA, opB ),
|
|
|
+ baRelation: context.getRelation( opB, opA )
|
|
|
+ };
|
|
|
+
|
|
|
+ const contextBA = {
|
|
|
+ aIsStrong: false,
|
|
|
+ aWasUndone: context.wasUndone( opB ),
|
|
|
+ bWasUndone: context.wasUndone( opA ),
|
|
|
+ abRelation: context.getRelation( opB, opA ),
|
|
|
+ baRelation: context.getRelation( opA, opB )
|
|
|
+ };
|
|
|
+
|
|
|
+ const newOpsA = transform( opA, opB, contextAB );
|
|
|
+ const newOpsB = transform( opB, opA, contextBA );
|
|
|
+
|
|
|
+ if ( options.useContext ) {
|
|
|
+ updateOriginalOperation( context, opA, newOpsA );
|
|
|
+ updateOriginalOperation( context, opB, newOpsB );
|
|
|
+ }
|
|
|
+
|
|
|
+ for ( const newOpA of newOpsA ) {
|
|
|
+ nextTransformIndex.set( newOpA, transformByIndex + newOpsB.length );
|
|
|
+ }
|
|
|
+
|
|
|
+ operationsA.splice( i, 1, ...newOpsA );
|
|
|
+ operationsB.splice( transformByIndex, 1, ...newOpsB );
|
|
|
+
|
|
|
+ data.extraOpsA += newOpsA.length - 1;
|
|
|
+ data.extraOpsB += newOpsB.length - 1;
|
|
|
}
|
|
|
|
|
|
if ( options.padWithNoOps ) {
|
|
|
@@ -224,6 +219,8 @@ function updateRelations( context, opA, opB ) {
|
|
|
if ( opA.targetPosition.isEqual( opB.sourcePosition ) || opB.movedRange.containsPosition( opA.targetPosition ) ) {
|
|
|
setRelation( context, opA, opB, 'insertAtSource' );
|
|
|
setRelation( context, opB, opA, 'splitBefore' );
|
|
|
+ } else if ( opA.targetPosition.isEqual( opB.deletionPosition ) ) {
|
|
|
+ setRelation( context, opA, opB, 'insertBetween' );
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
@@ -294,6 +291,8 @@ function updateRelations( context, opA, opB ) {
|
|
|
case MergeOperation: {
|
|
|
if ( opA.position.isEqual( opB.sourcePosition ) || opB.movedRange.containsPosition( opA.position ) ) {
|
|
|
setRelation( context, opA, opB, 'insertAtSource' );
|
|
|
+ } else if ( opA.position.isEqual( opB.deletionPosition ) ) {
|
|
|
+ setRelation( context, opA, opB, 'insertBetween' );
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
@@ -390,10 +389,10 @@ setTransformation( AttributeOperation, InsertOperation, ( a, b ) => {
|
|
|
// operation range needs to be split. For text insertion, extra operation needs to be generated.
|
|
|
//
|
|
|
if ( a.range.start.hasSameParentAs( b.position ) && a.range.containsPosition( b.position ) ) {
|
|
|
- // This will spread the range into two.
|
|
|
- const ranges = a.range._getTransformedByInsertion( b.position, b.howMany, true );
|
|
|
- const result = ranges.map( range => {
|
|
|
- return new AttributeOperation( range, a.key, a.oldValue, a.newValue, a.baseVersion );
|
|
|
+ // This will return an array with a single, expanded range.
|
|
|
+ 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 );
|
|
|
} );
|
|
|
|
|
|
// `AttributeOperation#range` includes some newly inserted text.
|
|
|
@@ -408,39 +407,50 @@ setTransformation( AttributeOperation, InsertOperation, ( a, b ) => {
|
|
|
// Bold should be applied also on the new text:
|
|
|
// <p>Fo<$text bold=true>zxxb</$text>ar</p>
|
|
|
//
|
|
|
- // Instead of expanding the attribute operation range, it is needed to create a new attribute operation.
|
|
|
- // This is because the inserted text might have already an attribute applied and the `oldValue` property
|
|
|
- // of the attribute operation might be wrong:
|
|
|
+ // There is a special case to consider here, which will dictate how the final solution will look like. The inserted
|
|
|
+ // text might have already an attribute 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>
|
|
|
//
|
|
|
- // New text with `highlight="red"` is typed:
|
|
|
+ // But before that, new text with `highlight="red"` is typed:
|
|
|
// <p>Fo[z<$text highlight="red">x</$text>a]r</p>
|
|
|
//
|
|
|
- // In this case three operations are needed: `oldValue=null, newValue="yellow"` for `z`, `oldValue="red",
|
|
|
- // newValue="yellow"` for `x` and `oldValue=null, newValue="yellow"` for `a`. It could even happen that
|
|
|
- // there are multiple nodes with different `oldValue`s are inserted, so multiple new operations might be added.
|
|
|
+ // 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.
|
|
|
+ //
|
|
|
+ // So, the attribute range will be expanded, no matter what attributes are set on the inserted nodes:
|
|
|
+ //
|
|
|
+ // <p>Fo[z<$text highlight="red">x</$text>a]r</p> <--- Change from `null` to `yellow`, still throwing an exception.
|
|
|
+ //
|
|
|
+ // But before that operation would be applied, we will add an additional attribute operation that will change
|
|
|
+ // attributes on inserted nodes in a way which would make the original operation correct:
|
|
|
+ //
|
|
|
+ // <p>Fo[z{<$text highlight="red">}x</$text>a]r</p> <--- Change `{}` from `red` to `null`.
|
|
|
+ // <p>Fo[zxa]r</p> <--- Now change from `null` to `yellow` is completely fine.
|
|
|
//
|
|
|
if ( b.shouldReceiveAttributes ) {
|
|
|
- result.splice( 1, 0, ..._getComplementaryAttributeOperations( a, b ) );
|
|
|
+ // Be sure to add those operations before the original operation.
|
|
|
+ result.unshift( ..._getComplementaryAttributeOperations( b, a.key, a.oldValue ) );
|
|
|
}
|
|
|
// If nodes should not receive new attribute, just leave the spread ranges as they are.
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
- // If insert operation is not spreading the attribute operation range, simply transform the range.
|
|
|
- a.range = a.range._getTransformedByInsertOperation( b )[ 0 ];
|
|
|
+ // 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 ];
|
|
|
} );
|
|
|
|
|
|
-function _getComplementaryAttributeOperations( attributeOperation, insertOperation ) {
|
|
|
+function _getComplementaryAttributeOperations( insertOperation, key, newValue ) {
|
|
|
const nodes = insertOperation.nodes;
|
|
|
const result = [];
|
|
|
|
|
|
// At the beginning we store the attribute value from the first node.
|
|
|
- let val = nodes.getNode( 0 ).getAttribute( attributeOperation.key );
|
|
|
+ let val = nodes.getNode( 0 ).getAttribute( key );
|
|
|
|
|
|
// This stores the last index of node list where the attribute value has changed.
|
|
|
// We need it to create separate `AttributeOperation`s for nodes with different attribute values.
|
|
|
@@ -451,14 +461,14 @@ function _getComplementaryAttributeOperations( attributeOperation, insertOperati
|
|
|
|
|
|
for ( let i = 1; i < nodes.length; i++ ) {
|
|
|
const node = nodes.getNode( i );
|
|
|
- const nodeAttrVal = node.getAttribute( attributeOperation.key );
|
|
|
+ const nodeAttrVal = node.getAttribute( key );
|
|
|
|
|
|
// If previous node has different attribute value, we will create an operation to the point before current node.
|
|
|
// So all nodes with the same attributes up to this point will be included in one `AttributeOperation`.
|
|
|
if ( nodeAttrVal != val ) {
|
|
|
// New operation is created only when it is needed. If given node already has proper value for this
|
|
|
// attribute we simply skip it without adding a new operation.
|
|
|
- if ( val != attributeOperation.newValue ) {
|
|
|
+ if ( val != newValue ) {
|
|
|
addOperation();
|
|
|
}
|
|
|
|
|
|
@@ -482,7 +492,7 @@ function _getComplementaryAttributeOperations( attributeOperation, insertOperati
|
|
|
);
|
|
|
|
|
|
result.push(
|
|
|
- new AttributeOperation( range, attributeOperation.key, val, attributeOperation.newValue, 0 )
|
|
|
+ new AttributeOperation( range, key, val, newValue, 0 )
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
@@ -522,52 +532,39 @@ setTransformation( AttributeOperation, MoveOperation, ( a, b ) => {
|
|
|
} );
|
|
|
|
|
|
function breakRangeByMoveOperation( range, moveOp, includeCommon ) {
|
|
|
- let ranges;
|
|
|
-
|
|
|
- const movedRange = Range.createFromPositionAndShift( moveOp.sourcePosition, moveOp.howMany );
|
|
|
-
|
|
|
- if ( movedRange.containsRange( range, true ) ) {
|
|
|
- ranges = [ ...range._getTransformedByMoveOperation( moveOp ) ];
|
|
|
- } else if ( range.start.hasSameParentAs( moveOp.sourcePosition ) ) {
|
|
|
- ranges = range.getDifference( movedRange );
|
|
|
+ let common = null;
|
|
|
+ let difference = [];
|
|
|
|
|
|
- _flatMoveTransform( ranges, moveOp );
|
|
|
+ const moveRange = Range.createFromPositionAndShift( moveOp.sourcePosition, moveOp.howMany );
|
|
|
|
|
|
- if ( includeCommon ) {
|
|
|
- const common = range.getIntersection( movedRange );
|
|
|
-
|
|
|
- if ( common ) {
|
|
|
- ranges.push( ...common._getTransformedByMoveOperation( moveOp ) );
|
|
|
- }
|
|
|
- }
|
|
|
+ if ( moveRange.containsRange( range, true ) ) {
|
|
|
+ common = range;
|
|
|
+ } else if ( range.start.hasSameParentAs( moveRange.start ) ) {
|
|
|
+ difference = range.getDifference( moveRange );
|
|
|
+ common = includeCommon ? range.getIntersection( moveRange ) : null;
|
|
|
} else {
|
|
|
- ranges = [ range ];
|
|
|
-
|
|
|
- _flatMoveTransform( ranges, moveOp );
|
|
|
+ difference = [ range ];
|
|
|
}
|
|
|
|
|
|
- return ranges;
|
|
|
-}
|
|
|
-
|
|
|
-function _flatMoveTransform( ranges, moveOp ) {
|
|
|
- const targetPosition = moveOp.getMovedRangeStart();
|
|
|
+ const result = [];
|
|
|
|
|
|
- for ( let i = 0; i < ranges.length; i++ ) {
|
|
|
- let range = ranges[ i ];
|
|
|
+ for ( let diff of difference ) {
|
|
|
+ diff = diff._getTransformedByDeletion( moveOp.sourcePosition, moveOp.howMany );
|
|
|
|
|
|
- if ( range.start.hasSameParentAs( moveOp.sourcePosition ) ) {
|
|
|
- range = range._getTransformedByDeletion( moveOp.sourcePosition, moveOp.howMany );
|
|
|
- }
|
|
|
+ const targetPosition = moveOp.getMovedRangeStart();
|
|
|
+ const spread = diff.start.hasSameParentAs( targetPosition );
|
|
|
+ diff = diff._getTransformedByInsertion( targetPosition, moveOp.howMany, spread );
|
|
|
|
|
|
- if ( range.start.hasSameParentAs( targetPosition ) ) {
|
|
|
- const result = range._getTransformedByInsertion( targetPosition, moveOp.howMany, true );
|
|
|
+ result.push( ...diff );
|
|
|
+ }
|
|
|
|
|
|
- ranges.splice( i, 1, ...result );
|
|
|
- i = i - 1 + result.length;
|
|
|
- } else {
|
|
|
- ranges[ i ] = range;
|
|
|
- }
|
|
|
+ if ( common ) {
|
|
|
+ result.push(
|
|
|
+ common._getTransformedByMove( moveOp.sourcePosition, moveOp.targetPosition, moveOp.howMany, false )[ 0 ]
|
|
|
+ );
|
|
|
}
|
|
|
+
|
|
|
+ return result;
|
|
|
}
|
|
|
|
|
|
setTransformation( AttributeOperation, SplitOperation, ( a, b ) => {
|
|
|
@@ -687,8 +684,8 @@ setTransformation( AttributeOperation, UnwrapOperation, ( a, b ) => {
|
|
|
setTransformation( InsertOperation, AttributeOperation, ( a, b ) => {
|
|
|
const result = [ a ];
|
|
|
|
|
|
- if ( a.shouldReceiveAttributes && b.range.containsPosition( a.position ) ) {
|
|
|
- result.push( ..._getComplementaryAttributeOperations( b, a ) );
|
|
|
+ if ( a.shouldReceiveAttributes && a.position.hasSameParentAs( b.range.start ) && b.range.containsPosition( a.position ) ) {
|
|
|
+ result.push( ..._getComplementaryAttributeOperations( a, b.key, b.newValue ) );
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
@@ -839,6 +836,10 @@ setTransformation( MarkerOperation, UnwrapOperation, ( a, b ) => {
|
|
|
// -----------------------
|
|
|
|
|
|
setTransformation( MergeOperation, InsertOperation, ( a, b ) => {
|
|
|
+ if ( a.sourcePosition.hasSameParentAs( b.position ) ) {
|
|
|
+ a.howMany += b.howMany;
|
|
|
+ }
|
|
|
+
|
|
|
a.sourcePosition = a.sourcePosition._getTransformedByInsertOperation( b );
|
|
|
a.targetPosition = a.targetPosition._getTransformedByInsertOperation( b );
|
|
|
|
|
|
@@ -855,6 +856,7 @@ setTransformation( MergeOperation, MergeOperation, ( a, b, context ) => {
|
|
|
path.push( 0 );
|
|
|
|
|
|
a.sourcePosition = new Position( b.graveyardPosition.root, path );
|
|
|
+ a.howMany = 0;
|
|
|
|
|
|
return [ a ];
|
|
|
}
|
|
|
@@ -864,6 +866,10 @@ setTransformation( MergeOperation, MergeOperation, ( a, b, context ) => {
|
|
|
|
|
|
// The default case.
|
|
|
//
|
|
|
+ if ( a.sourcePosition.hasSameParentAs( b.targetPosition ) ) {
|
|
|
+ a.howMany += b.howMany;
|
|
|
+ }
|
|
|
+
|
|
|
a.sourcePosition = a.sourcePosition._getTransformedByMergeOperation( b );
|
|
|
a.targetPosition = a.targetPosition._getTransformedByMergeOperation( b );
|
|
|
|
|
|
@@ -892,6 +898,14 @@ setTransformation( MergeOperation, MoveOperation, ( a, b, context ) => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ if ( a.sourcePosition.hasSameParentAs( b.targetPosition ) ) {
|
|
|
+ a.howMany += b.howMany;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ( a.sourcePosition.hasSameParentAs( b.sourcePosition ) ) {
|
|
|
+ a.howMany -= b.howMany;
|
|
|
+ }
|
|
|
+
|
|
|
a.sourcePosition = a.sourcePosition._getTransformedByMoveOperation( b );
|
|
|
a.targetPosition = a.targetPosition._getTransformedByMoveOperation( b );
|
|
|
|
|
|
@@ -904,32 +918,69 @@ setTransformation( MergeOperation, MoveOperation, ( a, b, context ) => {
|
|
|
|
|
|
setTransformation( MergeOperation, SplitOperation, ( a, b ) => {
|
|
|
if ( b.graveyardPosition ) {
|
|
|
+ if ( a.deletionPosition.isEqual( b.graveyardPosition ) ) {
|
|
|
+ a.howMany = b.howMany;
|
|
|
+ }
|
|
|
+
|
|
|
a.graveyardPosition = a.graveyardPosition._getTransformedByDeletion( b.graveyardPosition, 1 );
|
|
|
}
|
|
|
|
|
|
// Case 1: Merge operation moves nodes to the place where split happens.
|
|
|
+ // This is a classic situation when there are two paragraphs, and there is a split (enter) after the first
|
|
|
+ // paragraph and there is a merge (delete) at the beginning of the second paragraph:
|
|
|
//
|
|
|
- // Split is after `Foo`, while merge is from `Bar` to the end of `Foo`:
|
|
|
- // <p>Foo</p><p>Bar</p>
|
|
|
+ // <p>Foo{}</p><p>[]Bar</p>.
|
|
|
//
|
|
|
- // After split:
|
|
|
+ // Split is after `Foo`, while merge is from `Bar` to the end of `Foo`.
|
|
|
+ //
|
|
|
+ // State after split:
|
|
|
// <p>Foo</p><p></p><p>Bar</p>
|
|
|
//
|
|
|
- // In this case, `Bar` should be merged to the new paragraph:
|
|
|
+ // Now, `Bar` should be merged to the new paragraph:
|
|
|
// <p>Foo</p><p>Bar</p>
|
|
|
//
|
|
|
+ // Instead of merging it to the original paragraph:
|
|
|
+ // <p>FooBar</p><p></p>
|
|
|
+ //
|
|
|
// This means that `targetPosition` needs to be transformed. This is the default case though.
|
|
|
// For example, if the split would be after `F`, `targetPosition` should also be transformed.
|
|
|
//
|
|
|
- // There is an exception though. It is when merge operation targets into inside of an element.
|
|
|
- // Such merge operation can be a result of merge x merge transformation, when merges are identical.
|
|
|
- // Such merge operation's source position is in graveyard and we will use that to recognize it
|
|
|
- // (although a more precise method would be more correct).
|
|
|
+ // There are two exception, though, when we want to keep `targetPosition` as it was.
|
|
|
+ //
|
|
|
+ // First exception is when the merge target position is inside an element (not at the end, as usual). This
|
|
|
+ // happens when the merge operation earlier was transformed by "the same" merge operation. If merge operation
|
|
|
+ // targets inside the element we want to keep the original target position (and not transform it) because
|
|
|
+ // we have additional context telling us that we want to merge to the original element. We can check if the
|
|
|
+ // merge operation points inside element by checking what is `SplitOperation#howMany`. Since merge target position
|
|
|
+ // is same as split position, if `howMany` is non-zero, it means that the merge target position is inside an element.
|
|
|
+ //
|
|
|
+ // Second exception is when the element to merge is in the graveyard and split operation uses it. In that case
|
|
|
+ // if target position would be transformed, the merge operation would target at the source position:
|
|
|
+ //
|
|
|
+ // root: <p>Foo</p> graveyard: <p></p>
|
|
|
+ //
|
|
|
+ // SplitOperation: root [ 0, 3 ] using graveyard [ 0 ] (howMany = 0)
|
|
|
+ // MergeOperation: graveyard [ 0, 0 ] -> root [ 0, 3 ] (howMany = 0)
|
|
|
+ //
|
|
|
+ // Since split operation moves the graveyard node back to the root, the merge operation source position changes.
|
|
|
+ // We would like to merge from the empty <p> to the "Foo" <p>:
|
|
|
+ //
|
|
|
+ // root: <p>Foo</p><p></p> graveyard:
|
|
|
//
|
|
|
- if ( a.targetPosition.isEqual( b.position ) && a.sourcePosition.root.rootName == '$graveyard' ) {
|
|
|
- a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation( b );
|
|
|
+ // MergeOperation#sourcePosition = root [ 1, 0 ]
|
|
|
+ //
|
|
|
+ // If `targetPosition` is transformed, it would become root [ 1, 0 ] as well. It has to be kept as it was.
|
|
|
+ //
|
|
|
+ if ( a.targetPosition.isEqual( b.position ) ) {
|
|
|
+ if ( b.howMany != 0 || ( b.graveyardPosition && a.deletionPosition.isEqual( b.graveyardPosition ) ) ) {
|
|
|
+ a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation( b );
|
|
|
|
|
|
- return [ a ];
|
|
|
+ return [ a ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if ( a.sourcePosition.hasSameParentAs( b.position ) ) {
|
|
|
+ a.howMany = b.position.offset;
|
|
|
}
|
|
|
|
|
|
a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation( b );
|
|
|
@@ -943,6 +994,10 @@ setTransformation( MergeOperation, WrapOperation, ( a, b ) => {
|
|
|
a.graveyardPosition = a.graveyardPosition._getTransformedByDeletion( b.graveyardPosition, 1 );
|
|
|
}
|
|
|
|
|
|
+ if ( a.sourcePosition.hasSameParentAs( b.position ) ) {
|
|
|
+ a.howMany = a.howMany + 1 - b.howMany;
|
|
|
+ }
|
|
|
+
|
|
|
// Case 1: Wrap is with an element from graveyard, which also is merge operation target. This happens
|
|
|
// in some undo scenarios. In this case, the target position needs to be properly transformed.
|
|
|
//
|
|
|
@@ -995,10 +1050,15 @@ setTransformation( MergeOperation, UnwrapOperation, ( a, b, context ) => {
|
|
|
path.push( 0 );
|
|
|
|
|
|
a.sourcePosition = new Position( b.graveyardPosition.root, path );
|
|
|
+ a.howMany = 0;
|
|
|
|
|
|
return [ a ];
|
|
|
}
|
|
|
|
|
|
+ if ( a.sourcePosition.hasSameParentAs( b.targetPosition ) ) {
|
|
|
+ a.howMany = a.howMany - 1 + b.howMany;
|
|
|
+ }
|
|
|
+
|
|
|
a.sourcePosition = a.sourcePosition._getTransformedByUnwrapOperation( b );
|
|
|
a.targetPosition = a.targetPosition._getTransformedByUnwrapOperation( b );
|
|
|
|
|
|
@@ -1038,17 +1098,18 @@ setTransformation( MoveOperation, MoveOperation, ( a, b, context ) => {
|
|
|
// Assign `context.aIsStrong` to a different variable, because the value may change during execution of
|
|
|
// this algorithm and we do not want to override original `context.aIsStrong` that will be used in later transformations.
|
|
|
let aIsStrong = context.aIsStrong;
|
|
|
+ let insertBefore = !context.aIsStrong;
|
|
|
|
|
|
if ( context.abRelation == 'insertBefore' ) {
|
|
|
- aIsStrong = true;
|
|
|
+ insertBefore = true;
|
|
|
} else if ( context.abRelation == 'insertAfter' ) {
|
|
|
- aIsStrong = false;
|
|
|
+ insertBefore = false;
|
|
|
}
|
|
|
|
|
|
// `a.targetPosition` could be affected by the `b` operation. We will transform it.
|
|
|
let newTargetPosition;
|
|
|
|
|
|
- if ( a.targetPosition.isEqual( b.targetPosition ) && aIsStrong ) {
|
|
|
+ if ( a.targetPosition.isEqual( b.targetPosition ) && insertBefore ) {
|
|
|
newTargetPosition = a.targetPosition._getTransformedByDeletion(
|
|
|
b.sourcePosition,
|
|
|
b.howMany
|
|
|
@@ -1140,9 +1201,9 @@ setTransformation( MoveOperation, MoveOperation, ( a, b, context ) => {
|
|
|
//
|
|
|
// If only one of operations is a remove operation, we force remove operation to be the "stronger" one
|
|
|
// to provide more expected results.
|
|
|
- if ( a.type == 'remove' && b.type != 'remove' ) {
|
|
|
+ if ( a.type == 'remove' && b.type != 'remove' && !context.aWasUndone ) {
|
|
|
aIsStrong = true;
|
|
|
- } else if ( a.type != 'remove' && b.type == 'remove' ) {
|
|
|
+ } else if ( a.type != 'remove' && b.type == 'remove' && !context.bWasUndone ) {
|
|
|
aIsStrong = false;
|
|
|
}
|
|
|
|
|
|
@@ -1255,10 +1316,13 @@ setTransformation( MoveOperation, SplitOperation, ( a, b, context ) => {
|
|
|
|
|
|
a.sourcePosition = transformed.start;
|
|
|
a.howMany = transformed.end.offset - transformed.start.offset;
|
|
|
- a.targetPosition = newTargetPosition;
|
|
|
|
|
|
if ( a.targetPosition.isEqual( b.position ) && context.abRelation == 'insertAtSource' ) {
|
|
|
- a.targetPosition = b.targetPosition;
|
|
|
+ a.targetPosition = b.moveTargetPosition;
|
|
|
+ } else if ( a.targetPosition.isEqual( b.insertionPosition ) && context.abRelation == 'insertBetween' ) {
|
|
|
+ a.targetPosition = a.targetPosition;
|
|
|
+ } else {
|
|
|
+ a.targetPosition = newTargetPosition;
|
|
|
}
|
|
|
|
|
|
return [ a ];
|
|
|
@@ -1284,7 +1348,14 @@ setTransformation( MoveOperation, MergeOperation, ( a, b, context ) => {
|
|
|
// it would be a move operation that moves 0 nodes, so maybe it is better just to return no-op.
|
|
|
//
|
|
|
if ( a.howMany == 1 ) {
|
|
|
- return getNoOp();
|
|
|
+ if ( !context.bWasUndone ) {
|
|
|
+ return getNoOp();
|
|
|
+ } else {
|
|
|
+ a.sourcePosition = Position.createFromPosition( b.graveyardPosition );
|
|
|
+ a.targetPosition = a.targetPosition._getTransformedByMergeOperation( b );
|
|
|
+
|
|
|
+ return [ a ];
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -1466,19 +1537,29 @@ setTransformation( RootAttributeOperation, RootAttributeOperation, ( a, b, conte
|
|
|
|
|
|
setTransformation( SplitOperation, InsertOperation, ( a, b, context ) => {
|
|
|
if ( a.position.isEqual( b.position ) && context.baRelation == 'insertAtSource' ) {
|
|
|
+ a.howMany += b.howMany;
|
|
|
+
|
|
|
return [ a ];
|
|
|
}
|
|
|
|
|
|
+ if ( a.position.hasSameParentAs( b.position ) && a.position.offset < b.position.offset ) {
|
|
|
+ a.howMany += b.howMany;
|
|
|
+ }
|
|
|
+
|
|
|
a.position = a.position._getTransformedByInsertOperation( b );
|
|
|
|
|
|
return [ a ];
|
|
|
} );
|
|
|
|
|
|
setTransformation( SplitOperation, MergeOperation, ( a, b ) => {
|
|
|
+ if ( a.position.hasSameParentAs( b.targetPosition ) ) {
|
|
|
+ a.howMany += b.howMany;
|
|
|
+ }
|
|
|
+
|
|
|
a.position = a.position._getTransformedByMergeOperation( b );
|
|
|
|
|
|
if ( a.graveyardPosition ) {
|
|
|
- a.graveyardPosition = a.graveyardPosition._getTransformedByInsertion( b.graveyardPosition, 1 );
|
|
|
+ a.graveyardPosition = a.graveyardPosition._getTransformedByMergeOperation( b );
|
|
|
}
|
|
|
|
|
|
return [ a ];
|
|
|
@@ -1507,12 +1588,20 @@ setTransformation( SplitOperation, MoveOperation, ( a, b, context ) => {
|
|
|
const rangeToMove = Range.createFromPositionAndShift( b.sourcePosition, b.howMany );
|
|
|
|
|
|
if ( a.position.hasSameParentAs( b.sourcePosition ) && rangeToMove.containsPosition( a.position ) ) {
|
|
|
+ const howManyRemoved = b.howMany - ( a.position.offset - b.sourcePosition.offset );
|
|
|
+ a.howMany -= howManyRemoved;
|
|
|
+
|
|
|
+ if ( a.position.hasSameParentAs( b.targetPosition ) && a.position.offset < b.targetPosition.offset ) {
|
|
|
+ a.howMany += b.howMany;
|
|
|
+ }
|
|
|
+
|
|
|
a.position = Position.createFromPosition( b.sourcePosition );
|
|
|
|
|
|
return [ a ];
|
|
|
}
|
|
|
|
|
|
if ( a.position.isEqual( b.targetPosition ) && context.abRelation == 'splitBefore' ) {
|
|
|
+ a.howMany += b.howMany;
|
|
|
a.position = a.position._getTransformedByDeletion( b.sourcePosition, b.howMany );
|
|
|
|
|
|
return [ a ];
|
|
|
@@ -1520,6 +1609,14 @@ setTransformation( SplitOperation, MoveOperation, ( a, b, context ) => {
|
|
|
|
|
|
// The default case.
|
|
|
//
|
|
|
+ if ( a.position.hasSameParentAs( b.sourcePosition ) && a.position.offset < b.sourcePosition.offset ) {
|
|
|
+ a.howMany -= b.howMany;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ( a.position.hasSameParentAs( b.targetPosition ) && a.position.offset < b.targetPosition.offset ) {
|
|
|
+ a.howMany += b.howMany;
|
|
|
+ }
|
|
|
+
|
|
|
a.position = a.position._getTransformedByMoveOperation( b );
|
|
|
|
|
|
return [ a ];
|
|
|
@@ -1534,9 +1631,15 @@ setTransformation( SplitOperation, SplitOperation, ( a, b, context ) => {
|
|
|
if ( a.graveyardPosition && b.graveyardPosition && a.graveyardPosition.isEqual( b.graveyardPosition ) ) {
|
|
|
return getNoOp();
|
|
|
}
|
|
|
+
|
|
|
+ a.howMany = 0;
|
|
|
} else if ( a.position.isEqual( b.insertionPosition ) && context.abRelation == 'splitBefore' ) {
|
|
|
return [ a ];
|
|
|
} else {
|
|
|
+ if ( a.position.hasSameParentAs( b.position ) && a.position.offset < b.position.offset ) {
|
|
|
+ a.howMany -= b.howMany;
|
|
|
+ }
|
|
|
+
|
|
|
a.position = a.position._getTransformedBySplitOperation( b );
|
|
|
}
|
|
|
|
|
|
@@ -1567,7 +1670,12 @@ setTransformation( SplitOperation, WrapOperation, ( a, b, context ) => {
|
|
|
|
|
|
if ( a.position.isEqual( b.position ) && context.abRelation == 'splitInside' ) {
|
|
|
a.position = b.targetPosition;
|
|
|
+ a.howMany = b.howMany;
|
|
|
} else {
|
|
|
+ if ( a.position.hasSameParentAs( b.position ) && a.position.offset < b.position.offset ) {
|
|
|
+ a.howMany = a.howMany + 1 - b.howMany;
|
|
|
+ }
|
|
|
+
|
|
|
a.position = a.position._getTransformedByWrapOperation( b );
|
|
|
}
|
|
|
|
|
|
@@ -1586,7 +1694,12 @@ setTransformation( SplitOperation, UnwrapOperation, ( a, b, context ) => {
|
|
|
path.push( 0 );
|
|
|
|
|
|
a.position = new Position( b.graveyardPosition.root, path );
|
|
|
+ a.howMany = 0;
|
|
|
} else {
|
|
|
+ if ( a.position.hasSameParentAs( b.targetPosition ) && a.position.offset < b.targetPosition.offset ) {
|
|
|
+ a.howMany = a.howMany - 1 + b.howMany;
|
|
|
+ }
|
|
|
+
|
|
|
a.position = a.position._getTransformedByUnwrapOperation( b );
|
|
|
}
|
|
|
|
|
|
@@ -1648,14 +1761,20 @@ setTransformation( WrapOperation, MoveOperation, ( a, b, context ) => {
|
|
|
return [ a ];
|
|
|
}
|
|
|
|
|
|
- const ranges = breakRangeByMoveOperation( a.wrappedRange, b, false );
|
|
|
+ const moveRange = Range.createFromPositionAndShift( b.sourcePosition, b.howMany );
|
|
|
+ const wrappedRange = a.wrappedRange;
|
|
|
|
|
|
- return ranges.reverse().map( range => {
|
|
|
- const howMany = range.end.offset - range.start.offset;
|
|
|
- const elementOrGraveyardPosition = a.graveyardPosition ? a.graveyardPosition : a.element;
|
|
|
+ if ( moveRange.containsRange( wrappedRange, true ) ) {
|
|
|
+ a.position = a.position._getCombined( b.sourcePosition, b.getMovedRangeStart() );
|
|
|
+ } else {
|
|
|
+ let transformed = wrappedRange._getTransformedByDeletion( b.sourcePosition, b.howMany );
|
|
|
+ transformed = transformed._getTransformedByInsertion( b.targetPosition, b.howMany, false )[ 0 ];
|
|
|
|
|
|
- return new WrapOperation( range.start, howMany, elementOrGraveyardPosition, 0 );
|
|
|
- } );
|
|
|
+ a.position = transformed.start;
|
|
|
+ a.howMany = transformed.end.offset - transformed.start.offset;
|
|
|
+ }
|
|
|
+
|
|
|
+ return [ a ];
|
|
|
} );
|
|
|
|
|
|
setTransformation( WrapOperation, SplitOperation, ( a, b, context ) => {
|
|
|
@@ -1776,23 +1895,53 @@ setTransformation( WrapOperation, WrapOperation, ( a, b, context ) => {
|
|
|
return [ a ];
|
|
|
}
|
|
|
// Ranges intersect.
|
|
|
- else if ( ranges.length == 1 ) {
|
|
|
- if ( context.aIsStrong ) {
|
|
|
- // If the incoming wrap operation is strong, we need to reverse the previous wrap, then apply the incoming
|
|
|
- // operation as is, then re-wrap the other nodes that were wrapped in the previous wrap.
|
|
|
- //
|
|
|
- // Content already wrapped into `blockQuote` but that wrap is not strong:
|
|
|
- // <blockQuote><p>Foo</p><p>Bar</p></blockQuote><p>Xyz</p>
|
|
|
- //
|
|
|
- // Unwrap:
|
|
|
- // <p>Foo</p><p>Bar</p><p>Xyz</p>
|
|
|
- //
|
|
|
- // Wrap with stronger wrap (`a`):
|
|
|
- // <p>Foo</p><div><p>Bar</p><p>Xyz</p></div>
|
|
|
- //
|
|
|
- // Re-wrap:
|
|
|
- // <blockQuote><p>Foo</p></blockQuote><div><p>Bar</p><p>Xyz</p></div>
|
|
|
- //
|
|
|
+ else {
|
|
|
+ // Range from `b` has some extra nodes other than nodes from `a`.
|
|
|
+ if ( !a.wrappedRange.containsRange( b.wrappedRange, true ) ) {
|
|
|
+ if ( context.aIsStrong ) {
|
|
|
+ // If the incoming wrap operation is strong, we need to reverse the previous wrap, then apply the incoming
|
|
|
+ // operation as is, then re-wrap the other nodes that were wrapped in the previous wrap.
|
|
|
+ //
|
|
|
+ // Content already wrapped into `blockQuote` but that wrap is not strong:
|
|
|
+ // <blockQuote><p>Foo</p><p>Bar</p></blockQuote><p>Xyz</p>
|
|
|
+ //
|
|
|
+ // Unwrap:
|
|
|
+ // <p>Foo</p><p>Bar</p><p>Xyz</p>
|
|
|
+ //
|
|
|
+ // Wrap with stronger wrap:
|
|
|
+ // <p>Foo</p><div><p>Bar</p><p>Xyz</p></div>
|
|
|
+ //
|
|
|
+ // Re-wrap:
|
|
|
+ // <blockQuote><p>Foo</p></blockQuote><div><p>Bar</p><p>Xyz</p></div>
|
|
|
+ //
|
|
|
+ const reversed = b.getReversed();
|
|
|
+
|
|
|
+ // Unwrap operation (reversed wrap) always puts a node into a graveyard. Not every wrap operation pulls a node
|
|
|
+ // from the graveyard, though. This means that after reversing a wrap operation, there might be a need to
|
|
|
+ // update a position in graveyard.
|
|
|
+ if ( !b.graveyardPosition && a.graveyardPosition ) {
|
|
|
+ a.graveyardPosition = a.graveyardPosition._getTransformedByInsertion( reversed.graveyardPosition, 1 );
|
|
|
+ }
|
|
|
+
|
|
|
+ const bOnlyRange = b.wrappedRange.getDifference( a.wrappedRange )[ 0 ];
|
|
|
+ const rewrapRange = bOnlyRange._getTransformedByWrapOperation( a );
|
|
|
+ const rewrapHowMany = rewrapRange.end.offset - rewrapRange.start.offset;
|
|
|
+ const rewrap = new WrapOperation( rewrapRange.start, rewrapHowMany, reversed.graveyardPosition, 0 );
|
|
|
+
|
|
|
+ return [ reversed, a, rewrap ];
|
|
|
+ } else {
|
|
|
+ // If the incoming wrap operation is not strong, just wrap those nodes which were not wrapped already.
|
|
|
+ const range = ranges[ 0 ]._getTransformedByWrapOperation( b );
|
|
|
+
|
|
|
+ a.position = range.start;
|
|
|
+ a.howMany = range.end.offset - range.start.offset;
|
|
|
+ a.graveyardPosition = newGraveyardPosition;
|
|
|
+
|
|
|
+ return [ a ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // Range from `b` is contained in range from `a`. Reverse operation `b` in addition to operation `a`.
|
|
|
+ else {
|
|
|
const reversed = b.getReversed();
|
|
|
|
|
|
// Unwrap operation (reversed wrap) always puts a node into a graveyard. Not every wrap operation pulls a node
|
|
|
@@ -1802,35 +1951,8 @@ setTransformation( WrapOperation, WrapOperation, ( a, b, context ) => {
|
|
|
a.graveyardPosition = a.graveyardPosition._getTransformedByInsertion( reversed.graveyardPosition, 1 );
|
|
|
}
|
|
|
|
|
|
- const bOnlyRange = b.wrappedRange.getDifference( a.wrappedRange )[ 0 ];
|
|
|
- const rewrapRange = bOnlyRange._getTransformedByWrapOperation( a );
|
|
|
- const rewrapHowMany = rewrapRange.end.offset - rewrapRange.start.offset;
|
|
|
- const rewrap = new WrapOperation( rewrapRange.start, rewrapHowMany, reversed.graveyardPosition, 0 );
|
|
|
-
|
|
|
- return [ reversed, a, rewrap ];
|
|
|
- } else {
|
|
|
- // If the incoming wrap operation is not strong, just wrap those nodes which were not wrapped already.
|
|
|
- const range = ranges[ 0 ]._getTransformedByWrapOperation( b );
|
|
|
-
|
|
|
- a.position = range.start;
|
|
|
- a.howMany = range.end.offset - range.start.offset;
|
|
|
- a.graveyardPosition = newGraveyardPosition;
|
|
|
-
|
|
|
- return [ a ];
|
|
|
- }
|
|
|
- }
|
|
|
- // Range from `b` is contained in range from `a`. Reverse operation `b` in addition to operation `a`.
|
|
|
- else {
|
|
|
- const reversed = b.getReversed();
|
|
|
-
|
|
|
- // Unwrap operation (reversed wrap) always puts a node into a graveyard. Not every wrap operation pulls a node
|
|
|
- // from the graveyard, though. This means that after reversing a wrap operation, there might be a need to
|
|
|
- // update a position in graveyard.
|
|
|
- if ( !b.graveyardPosition && a.graveyardPosition ) {
|
|
|
- a.graveyardPosition = a.graveyardPosition._getTransformedByInsertion( reversed.graveyardPosition, 1 );
|
|
|
+ return [ reversed, a ];
|
|
|
}
|
|
|
-
|
|
|
- return [ reversed, a ];
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1884,11 +2006,11 @@ setTransformation( UnwrapOperation, MergeOperation, ( a, b, context ) => {
|
|
|
return [ b.getReversed(), a ];
|
|
|
}
|
|
|
|
|
|
- // // Case 2: The element to unwrap was merged-to and has new nodes.
|
|
|
- // //
|
|
|
- // if ( a.position.hasSameParentAs( b.targetPosition ) ) {
|
|
|
- // // Merge operation needs `howMany`!
|
|
|
- // }
|
|
|
+ // Case 2: The element to unwrap was merged-to and has new nodes.
|
|
|
+ //
|
|
|
+ if ( a.position.hasSameParentAs( b.targetPosition ) ) {
|
|
|
+ a.howMany += b.howMany;
|
|
|
+ }
|
|
|
|
|
|
if ( a.position.hasSameParentAs( b.graveyardPosition ) ) {
|
|
|
a.howMany++;
|