Procházet zdrojové kódy

InsertOperation.clone() and InsertOperation OT introduced.

Szymon Cofalik před 10 roky
rodič
revize
d396e16de9

+ 79 - 1
packages/ckeditor5-utils/src/document/operation/insertoperation.js

@@ -8,7 +8,8 @@
 CKEDITOR.define( [
 	'document/operation/operation',
 	'document/nodelist',
-	'document/operation/removeoperation'
+	'document/operation/removeoperation',
+	'document/operation/moveoperation'
 ], ( Operation, NodeList ) => {
 	/**
 	 * Operation to insert list of nodes on the given position in the tree data model.
@@ -55,6 +56,83 @@ CKEDITOR.define( [
 
 			return new RemoveOperation( this.position, this.nodeList.length, this.baseVersion + 1 );
 		}
+
+		getTransformedBy( operation, isStrong ) {
+			// Circular dependency re-require.
+			const MoveOperation = CKEDITOR.require( 'document/operation/moveoperation' );
+
+			if ( operation instanceof InsertOperation ) {
+				return getTransformedByInsertOperation.call( this, operation, !!isStrong );
+			} else if ( operation instanceof MoveOperation ) {
+				return getTransformedByMoveOperation.call( this, operation, !!isStrong );
+			}
+
+			return [ this.clone( this.baseVersion + 1 ) ];
+		}
+
+		clone( baseVersion ) {
+			if ( !baseVersion ) {
+				baseVersion = this.baseVersion;
+			}
+
+			return new InsertOperation( this.position, this.nodeList, baseVersion );
+		}
+	}
+
+	/**
+	 * Returns an array containing the result of transforming this operation by given {document.operation.InsertOperation}.
+	 *
+	 * @method getTransformedByInsertOperation
+	 * @param {document.operation.InsertOperation} insert Operation to transform by.
+	 * @param {Boolean} isStrong Flag indicating whether this operation should be treated as more important
+	 * when resolving conflicts.
+	 * @returns {Array.<document.operation.ChangeOperation>} Result of the transformation.
+	 * @private
+	 */
+	function getTransformedByInsertOperation( insert, isStrong ) {
+		/*jshint validthis:true */
+
+		// Transformed operations are always new instances, not references to the original operations.
+		const transformed = this.clone( this.baseVersion + 1 );
+
+		// Transform this operation's position by given operation's position.
+		transformed.position = transformed.position.getTransformedByInsertion( insert.position, insert.nodeList.length, !isStrong );
+
+		return [ transformed ];
+	}
+
+	/**
+	 * Returns an array containing the result of transforming this operation by given {document.operation.MoveOperation}.
+	 *
+	 * @method getTransformedByMoveOperation
+	 * @param {document.operation.MoveOperation} move Operation to transform by.
+	 * @param {Boolean} isStrong Flag indicating whether this operation should be treated as more important
+	 * when resolving conflicts.
+	 * @returns {Array.<document.operation.InsertOperation>} Result of the transformation.
+	 * @private
+	 */
+	function getTransformedByMoveOperation( move, isStrong ) {
+		/*jshint validthis:true */
+
+		// Transformed operations are always new instances, not references to the original operations.
+		const transformed = this.clone( this.baseVersion + 1 );
+
+		// MoveOperation removes nodes from their original position. We acknowledge this by proper transformation.
+		const newPosition = this.position.getTransformedByDeletion( move.sourcePosition, move.howMany );
+
+		if ( newPosition == null ) {
+			// This operation's position was inside a node moved by MoveOperation. We substitute that position by
+			// the combination of move target position and insert position. This reflects changes done by MoveOperation.
+
+			transformed.position = transformed.position.getCombined( move.sourcePosition, move.targetPosition );
+		} else {
+			// Here we have the insert position after some nodes has been removed by MoveOperation.
+			// Next step is to reflect pasting nodes by MoveOperation, which might further affect the position.
+
+			transformed.position = newPosition.getTransformedByInsertion( move.targetPosition, move.howMany, !isStrong );
+		}
+
+		return [ transformed ];
 	}
 
 	return InsertOperation;

+ 367 - 2
packages/ckeditor5-utils/tests/document/operation/insertoperation.js

@@ -4,27 +4,43 @@
  */
 
 /* bender-tags: document */
+/* global describe, before, beforeEach, it, expect */
 
 'use strict';
 
 const modules = bender.amd.require(
 	'document/document',
+	'document/node',
+	'document/nodelist',
 	'document/operation/insertoperation',
 	'document/operation/removeoperation',
+	'document/operation/changeoperation',
+	'document/operation/moveoperation',
+	'document/operation/nooperation',
 	'document/position',
+	'document/range',
 	'document/character',
-	'document/nodelist'
+	'document/nodelist',
+	'document/attribute'
 );
 
 describe( 'InsertOperation', () => {
-	let Document, InsertOperation, RemoveOperation, Position, Character;
+	let Document, Node, NodeList, InsertOperation, RemoveOperation, ChangeOperation,
+		MoveOperation, NoOperation, Position, Range, Character, Attribute;
 
 	before( () => {
 		Document = modules[ 'document/document' ];
+		Node = modules[ 'document/node' ];
+		NodeList = modules[ 'document/nodelist' ];
 		InsertOperation = modules[ 'document/operation/insertoperation' ];
 		RemoveOperation = modules[ 'document/operation/removeoperation' ];
+		ChangeOperation = modules[ 'document/operation/changeoperation' ];
+		MoveOperation = modules[ 'document/operation/moveoperation' ];
+		NoOperation = modules[ 'document/operation/nooperation' ];
 		Position = modules[ 'document/position' ];
+		Range = modules[ 'document/range' ];
 		Character = modules[ 'document/character' ];
+		Attribute = modules[ 'document/attribute' ];
 	} );
 
 	let doc, root;
@@ -157,4 +173,353 @@ describe( 'InsertOperation', () => {
 		expect( doc.version ).to.equal( 2 );
 		expect( root.getChildCount() ).to.equal( 0 );
 	} );
+
+	it( 'should create operation with the same parameters when cloned', () => {
+		let position = new Position( [ 0 ], root );
+		let nodeA = new Node();
+		let nodeB = new Node();
+		let nodes = new NodeList( [ nodeA, nodeB ] );
+		let baseVersion = doc.version;
+
+		let op = new InsertOperation( position, nodes, baseVersion );
+
+		let clone = op.clone();
+
+		// New instance rather than a pointer to the old instance.
+		expect( clone ).not.to.be.equal( op );
+
+		expect( clone ).to.be.instanceof( InsertOperation );
+		expect( clone.position.isEqual( position ) ).to.be.true;
+		expect( clone.nodeList.get( 0 ) ).to.equal( nodeA );
+		expect( clone.nodeList.get( 1 ) ).to.equal( nodeB );
+		expect( clone.nodeList.length ).to.equal( 2 );
+		expect( clone.baseVersion ).to.equal( baseVersion );
+	} );
+
+	describe( 'getTransformedBy', () => {
+		let nodeA, nodeB, nodeC, nodeD, position, op, baseVersion, expected;
+
+		beforeEach( () => {
+			nodeA = new Node();
+			nodeB = new Node();
+			nodeC = new Node();
+			nodeD = new Node();
+
+			baseVersion = doc.version;
+
+			position = new Position( [ 0, 2, 1 ], root );
+
+			op = new InsertOperation( position, [ nodeA, nodeB ], baseVersion );
+
+			expected = {
+				type: InsertOperation,
+				position: position.clone(),
+				baseVersion: baseVersion + 1
+			}
+		} );
+
+		function expectOperation( op, params ) {
+			for ( let i in params ) {
+				if ( params.hasOwnProperty( i ) ) {
+					if ( i == 'type' ) {
+						expect( op ).to.be.instanceof( params[ i ] );
+					}
+					else if ( params[ i ] instanceof Array ) {
+						expect( op[ i ].length ).to.equal( params[ i ].length );
+
+						for ( let j = 0; j < params[ i ].length; j++ ) {
+							expect( op[ i ][ j ] ).to.equal( params[ i ][ j ] );
+						}
+					} else if ( params[ i ] instanceof Position || params[ i ] instanceof Range ) {
+						expect( op[ i ].isEqual( params[ i ] ) ).to.be.true;
+					} else {
+						expect( op[ i ] ).to.equal( params[ i ] );
+					}
+				}
+			}
+		}
+
+		describe( 'InsertOperation', () => {
+			it( 'should not change when positions are different', () => {
+				let transformBy = new InsertOperation(
+					new Position( [ 1, 3, 2 ], root ),
+					[ nodeC, nodeD ],
+					baseVersion
+				);
+
+				let transOp = op.getTransformedBy( transformBy );
+
+				expectOperation( transOp[ 0 ], expected );
+			} );
+
+			it( 'should increment offset if addresses are same and offset is after applied operation', () => {
+				let transformBy = new InsertOperation(
+					new Position( [ 0, 2, 0 ], root ),
+					[ nodeC, nodeD ],
+					baseVersion
+				);
+
+				let transOp = op.getTransformedBy( transformBy );
+				expected.position.offset += 2;
+
+				expectOperation( transOp[ 0 ], expected );
+			} );
+
+			it( 'should increment offset if positions are same and operation to transform is weaker', () => {
+				let transformBy = new InsertOperation(
+					new Position( [ 0, 2, 1 ], root ),
+					[ nodeC, nodeD ],
+					baseVersion
+				);
+
+				let transOp = op.getTransformedBy( transformBy );
+				expected.position.offset += 2;
+
+				expectOperation( transOp[ 0 ], expected );
+			} );
+
+			it( 'should not increment offset if positions are same and operation to transform is stronger', () => {
+				let transformBy = new InsertOperation(
+					new Position( [ 0, 2, 1 ], root ),
+					[ nodeC, nodeD ],
+					baseVersion
+				);
+
+				let transOp = op.getTransformedBy( transformBy, true );
+
+				expectOperation( transOp[ 0 ], expected );
+			} );
+
+			it( 'should not increment offset if addresses are same and offset is before applied operation', () => {
+				let transformBy = new InsertOperation(
+					new Position( [ 0, 2, 2 ], root ),
+					[ nodeC, nodeD ],
+					baseVersion
+				);
+
+				let transOp = op.getTransformedBy( transformBy );
+
+				expectOperation( transOp[ 0 ], expected );
+			} );
+
+			it( 'should update address at node(i) if applied operation\'s address was a prefix and its offset is before node(i)', () => {
+				let transformBy = new InsertOperation(
+					new Position( [ 0, 1 ], root ),
+					[ nodeC, nodeD ],
+					baseVersion
+				);
+
+				let transOp = op.getTransformedBy( transformBy );
+				expected.position.path[ 1 ] += 2;
+
+				expectOperation( transOp[ 0 ], expected );
+			} );
+
+			it( 'should not update address at node(i) if applied operation\'s address was a prefix and its offset is after node(i)', () => {
+				let transformBy = new InsertOperation(
+					new Position( [ 0, 6 ], root ),
+					[ nodeC, nodeD ],
+					baseVersion
+				);
+
+				let transOp = op.getTransformedBy( transformBy );
+
+				expectOperation( transOp[ 0 ], expected );
+			} );
+		} );
+
+		describe( 'ChangeOperation', () => {
+			it( 'should not get updated', () => {
+				let rangeStart = position.clone();
+				let rangeEnd = position.clone();
+				rangeEnd.offset += 2;
+
+				let transformBy = new ChangeOperation(
+					new Range( rangeStart, rangeEnd ),
+					null,
+					new Attribute( 'foo', 'bar' ),
+					baseVersion
+				);
+
+				let transOp = op.getTransformedBy( transformBy );
+
+				expectOperation( transOp[ 0 ], expected );
+			} );
+		} );
+
+		describe( 'MoveOperation', () => {
+			it( 'should not change if insert is in different path than move origin and destination', () => {
+				let transformBy = new MoveOperation(
+					new Position( [ 1, 3, 2 ], root ),
+					new Position( [ 2, 1 ], root ),
+					2
+				);
+
+				let transOp = op.getTransformedBy( transformBy );
+
+				expectOperation( transOp[ 0 ], expected );
+			} );
+
+			it( 'should have it\'s address merged with destination address if insert was inside moved node sub-tree', () => {
+				let transformBy = new MoveOperation(
+					new Position( [ 0, 1 ], root ),
+					new Position( [ 1, 1 ], root ),
+					2
+				);
+
+				let transOp = op.getTransformedBy( transformBy );
+				expected.position.path = [ 1, 2, 1 ];
+
+				expectOperation( transOp[ 0 ], expected );
+			} );
+
+			it( 'should decrement offset if address is same as move origin and insert offset is after moved node offset', () => {
+				let transformBy = new MoveOperation(
+					new Position( [ 0, 2, 0 ], root ),
+					new Position( [ 1, 1 ], root ),
+					1
+				);
+
+				let transOp = op.getTransformedBy( transformBy );
+				expected.position.offset--;
+
+				expectOperation( transOp[ 0 ], expected );
+			} );
+
+			it( 'should not decrement offset if address is same as move origin and insert offset is after moved node offset', () => {
+				let transformBy = new MoveOperation(
+					new Position( [ 0, 2, 4 ], root ),
+					new Position( [ 1, 1 ], root ),
+					1
+				);
+
+				let transOp = op.getTransformedBy( transformBy );
+
+				expectOperation( transOp[ 0 ], expected );
+			} );
+
+			it( 'should increment offset if address is same as move destination and insert offset is after move-to offset', () => {
+				let transformBy = new MoveOperation(
+					new Position( [ 1, 1 ], root ),
+					new Position( [ 0, 2, 0 ], root ),
+					2
+				);
+
+				let transOp = op.getTransformedBy( transformBy );
+				expected.position.offset += 2;
+
+				expectOperation( transOp[ 0 ], expected );
+			} );
+
+			it( 'should not increment offset if address is same as move destination and insert offset is before move-to offset', () => {
+				let transformBy = new MoveOperation(
+					new Position( [ 1, 1 ], root ),
+					new Position( [ 0, 2, 4 ], root ),
+					2
+				);
+
+				let transOp = op.getTransformedBy( transformBy );
+
+				expectOperation( transOp[ 0 ], expected );
+			} );
+
+			it( 'should increment offset if position is same as move operation and insert operation is weaker', () => {
+				let transformBy = new MoveOperation(
+					new Position( [ 1, 1 ], root ),
+					new Position( [ 0, 2, 1 ], root ),
+					2
+				);
+
+				let transOp = op.getTransformedBy( transformBy );
+				expected.position.offset += 2;
+
+				expectOperation( transOp[ 0 ], expected );
+			} );
+
+			it( 'should not increment offset if position is same as move operation and insert operation is stronger', () => {
+				let transformBy = new MoveOperation(
+					new Position( [ 1, 1 ], root ),
+					new Position( [ 0, 2, 1 ], root ),
+					2
+				);
+
+				let transOp = op.getTransformedBy( transformBy, true );
+
+				expectOperation( transOp[ 0 ], expected );
+			} );
+
+			it( 'should update address if moved node is before a node from insert path', () => {
+				let transformBy = new MoveOperation(
+					new Position( [ 0, 0 ], root ),
+					new Position( [ 1, 0 ], root ),
+					2
+				);
+
+				let transOp = op.getTransformedBy( transformBy );
+				expected.position.path[ 1 ] -= 2;
+
+				expectOperation( transOp[ 0 ], expected );
+			} );
+
+			it( 'should not update address if moved node is after a node from insert path', () => {
+				let transformBy = new MoveOperation(
+					new Position( [ 0, 4 ], root ),
+					new Position( [ 1, 0 ], root ),
+					2
+				);
+
+				let transOp = op.getTransformedBy( transformBy );
+
+				expectOperation( transOp[ 0 ], expected );
+			} );
+
+			it( 'should update address if move-in destination is before a node from insert path', () => {
+				let transformBy = new MoveOperation(
+					new Position( [ 1, 0 ], root ),
+					new Position( [ 0, 0 ], root ),
+					2
+				);
+
+				let transOp = op.getTransformedBy( transformBy );
+				expected.position.path[ 1 ] += 2;
+
+				expectOperation( transOp[ 0 ], expected );
+			} );
+
+			it( 'should not update address if move-in destination after a node from insert path', () => {
+				let transformBy = new MoveOperation(
+					new Position( [ 1, 0 ], root ),
+					new Position( [ 0, 4 ], root ),
+					2
+				);
+
+				let transOp = op.getTransformedBy( transformBy );
+
+				expectOperation( transOp[ 0 ], expected );
+			} );
+
+			it( 'should decrement offset if address is same as move origin and insert offset is in the middle of moved range', () => {
+				let transformBy = new MoveOperation(
+					new Position( [ 0, 2, 0 ], root ),
+					new Position( [ 1, 0 ], root ),
+					3
+				);
+
+				let transOp = op.getTransformedBy( transformBy );
+				expected.position.offset = 0;
+
+				expectOperation( transOp[ 0 ], expected );
+			} );
+		} );
+
+		describe( 'NoOperation', () => {
+			it( 'should not get updated', () => {
+				let transformBy = new NoOperation( baseVersion );
+
+				let transOp = op.getTransformedBy( transformBy );
+
+				expectOperation( transOp[ 0 ], expected );
+			} );
+		} );
+	} );
 } );