浏览代码

Introduced API for deltas.

Szymon Cofalik 9 年之前
父节点
当前提交
9d475ef963

+ 52 - 1
packages/ckeditor5-engine/src/treemodel/delta/attributedelta.js

@@ -19,7 +19,58 @@ import Element from '../element.js';
  * @memberOf core.treeModel.delta
  * @extends core.treeModel.delta.Delta
  */
-export default class AttributeDelta extends Delta {}
+export default class AttributeDelta extends Delta {
+	/**
+	 * @see core.treeModel.delta.Delta#_reverseDeltaClass
+	 * @private
+	 * @type {Object}
+	 */
+	get _reverseDeltaClass() {
+		return AttributeDelta;
+	}
+
+	/**
+	 * The attribute key that is changed by the delta or `null` if the delta has no operations.
+	 *
+	 * @type {String|null}
+	 */
+	get key() {
+		return this.operations[ 0 ] ? this.operations[ 0 ].key : null;
+	}
+
+	/**
+	 * The attribute value that is set by the delta or `null` if the delta has no operations.
+	 *
+	 * @type {*|null}
+	 */
+	get value() {
+		return this.operations[ 0 ] ? this.operations[ 0 ].newValue : null;
+	}
+
+	/**
+	 * The range on which delta operates or `null` if the delta has no operations.
+	 *
+	 * @type {core.treeModel.Range|null}
+	 */
+	get range() {
+		// Check if it is cached.
+		if ( this._range ) {
+			return this._range;
+		}
+
+		// If it is not cached we will evaluate it and cache it.
+		let firstOperation = this.operations[ 0 ];
+		let lastOperation = this.operations[ this.operations.length - 1 ];
+
+		if ( firstOperation ) {
+			this._range = new Range( firstOperation.range.start, lastOperation.range.end );
+
+			return this._range;
+		}
+
+		return null;
+	}
+}
 
 /**
  * Sets the value of the attribute of the node or on the range.

+ 32 - 2
packages/ckeditor5-engine/src/treemodel/delta/insertdelta.js

@@ -6,6 +6,7 @@
 'use strict';
 
 import Delta from './delta.js';
+import RemoveDelta from './removedelta.js';
 import { register } from '../batch-base.js';
 import InsertOperation from '../operation/insertoperation.js';
 
@@ -17,12 +18,41 @@ import InsertOperation from '../operation/insertoperation.js';
  * @memberOf core.treeModel.delta
  */
 export default class InsertDelta extends Delta {
-	get insertOperation() {
+	/**
+	 * Insert operation that is saved in this delta or `null` if there are no operations in the delta.
+	 *
+	 * @protected
+	 * @type {core.treeModel.operation.InsertOperation|null}
+	 */
+	get _insertOperation() {
 		return this.operations[ 0 ] || null;
 	}
 
+	/**
+	 * @see core.treeModel.delta.Delta#_reverseDeltaClass
+	 * @private
+	 * @type {Object}
+	 */
+	get _reverseDeltaClass() {
+		return RemoveDelta;
+	}
+
+	/**
+	 * Position where the delta inserts nodes or `null` if there are no operations in the delta.
+	 *
+	 * @type {core.treeModel.Position|null}
+	 */
 	get position() {
-		return this.insertOperation ? this.insertOperation.position : null;
+		return this._insertOperation ? this._insertOperation.position : null;
+	}
+
+	/**
+	 * Node list containing all the nodes inserted by the delta or `null` if there are no operations in the delta.
+	 *
+	 * @type {core.treeModel.NodeList|null}
+	 */
+	get nodeList() {
+		return this._insertOperation ? this._insertOperation.nodeList : null;
 	}
 }
 

+ 51 - 1
packages/ckeditor5-engine/src/treemodel/delta/movedelta.js

@@ -19,7 +19,57 @@ import CKEditorError from '../../ckeditorerror.js';
  *
  * @memberOf core.treeModel.delta
  */
-export default class MoveDelta extends Delta {}
+export default class MoveDelta extends Delta {
+	/**
+	 * Move operation that is saved in this delta or `null` if there are no operations in the delta.
+	 *
+	 * @protected
+	 * @type {core.treeModel.operation.MoveOperation|null}
+	 */
+	get _moveOperation() {
+		return this.operations[ 0 ] || null;
+	}
+
+	/**
+	 * @see core.treeModel.delta.Delta#_reverseDeltaClass
+	 * @private
+	 * @type {Object}
+	 */
+	get _reverseDeltaClass() {
+		return MoveDelta;
+	}
+
+	/**
+	 * {@link core.treeModel.delta.MoveDelta#_moveOperation Move operation}
+	 * {@link core.treeModel.operation.MoveOperation#sourcePosition source position} or `null` if there are
+	 * no operations in the delta.
+	 *
+	 * @type {core.treeModel.Position|null}
+	 */
+	get sourcePosition() {
+		return this._moveOperation ? this._moveOperation.sourcePosition : null;
+	}
+
+	/**
+	 * How many nodes are moved by the delta or `null` if there are no operations in the delta.
+	 *
+	 * @type {Number|null}
+	 */
+	get howMany() {
+		return this._moveOperation ? this._moveOperation.howMany : null;
+	}
+
+	/**
+	 * {@link core.treeModel.delta.MoveDelta#_moveOperation Move operation}
+	 * {@link core.treeModel.operation.MoveOperation#targetPosition target position} or `null` if there are
+	 * no operations in the delta.
+	 *
+	 * @type {core.treeModel.Position|null}
+	 */
+	get targetPosition() {
+		return this._moveOperation ? this._moveOperation.targetPosition : null;
+	}
+}
 
 function addMoveOperation( batch, delta, sourcePosition, howMany, targetPosition ) {
 	const operation = new MoveOperation( sourcePosition, howMany, targetPosition, batch.doc.version );

+ 36 - 3
packages/ckeditor5-engine/src/treemodel/delta/splitdelta.js

@@ -12,6 +12,7 @@ import Element from '../element.js';
 import InsertOperation from '../operation/insertoperation.js';
 import MoveOperation from '../operation/moveoperation.js';
 import CKEditorError from '../../ckeditorerror.js';
+import MergeDelta from '../delta/mergedelta.js';
 
 /**
  * @classdesc
@@ -21,16 +22,48 @@ import CKEditorError from '../../ckeditorerror.js';
  * @memberOf core.treeModel.delta
  */
 export default class SplitDelta extends Delta {
-	get cloneOperation() {
+	/**
+	 * Operation in the delta that adds a node to the tree model where split elements will be moved to or `null` if
+	 * there are no operations in the delta.
+	 *
+	 * Most commonly this will be insert operation, as `SplitDelta` has to create a new node. If `SplitDelta` was created
+	 * through {@link core.treeModel.delta.MergeDelta MergeDelta} {@link core.treeModel.delta.Delta#getReversed reversing},
+	 * this will be a reinsert operation, as we will want to "insert-back" the node that was removed by `MergeDelta`.
+	 *
+	 * @protected
+	 * @type {core.treeModel.operation.InsertOpertaion|core.treeModel.operation.ReinsertOperation|null}
+	 */
+	get _cloneOperation() {
 		return this.operations[ 0 ] || null;
 	}
 
-	get moveOperation() {
+	/**
+	 * Operation in the delta that moves nodes from after split position to their new parent
+	 * or `null` if there are no operations in the delta.
+	 *
+	 * @protected
+	 * @type {core.treeModel.operation.MoveOperation|null}
+	 */
+	get _moveOperation() {
 		return this.operations[ 1 ] || null;
 	}
 
+	/**
+	 * @see core.treeModel.delta.Delta#_reverseDeltaClass
+	 * @private
+	 * @type {Object}
+	 */
+	get _reverseDeltaClass() {
+		return MergeDelta;
+	}
+
+	/**
+	 * Position of split or `null` if there are no operations in the delta.
+	 *
+	 * @type {core.treeModel.Position|null}
+	 */
 	get position() {
-		return this.moveOperation ? this.moveOperation.sourcePosition : null;
+		return this._moveOperation ? this._moveOperation.sourcePosition : null;
 	}
 }
 

+ 31 - 3
packages/ckeditor5-engine/src/treemodel/delta/unwrapdelta.js

@@ -6,6 +6,7 @@
 'use strict';
 
 import Delta from './delta.js';
+import WrapDelta from './wrapdelta.js';
 import { register } from '../batch-base.js';
 import Position from '../position.js';
 import RemoveOperation from '../operation/removeoperation.js';
@@ -19,7 +20,35 @@ import CKEditorError from '../../ckeditorerror.js';
  *
  * @memberOf core.treeModel.delta
  */
-export default class UnwrapDelta extends Delta {}
+export default class UnwrapDelta extends Delta {
+	/**
+	 * Operation in the delta that moves unwrapped nodes to their new parent or `null` if there are no operations in the delta.
+	 *
+	 * @protected
+	 * @type {core.treeModel.operation.MoveOperation|null}
+	 */
+	get _moveOperation() {
+		return this.operations[ 0 ] || null;
+	}
+
+	/**
+	 * @see core.treeModel.delta.Delta#_reverseDeltaClass
+	 * @private
+	 * @type {Object}
+	 */
+	get _reverseDeltaClass() {
+		return WrapDelta;
+	}
+
+	/**
+	 * Position before unwrapped element or `null` if there are no operations in the delta.
+	 *
+	 * @type {core.treeModel.Position|null}
+	 */
+	get position() {
+		return this._moveOperation ? this._moveOperation.targetPosition : null;
+	}
+}
 
 /**
  * Unwraps specified element, that is moves all it's children before it and then removes it. Throws
@@ -36,8 +65,7 @@ register( 'unwrap', function( element ) {
 		 *
 		 * @error batch-unwrap-element-no-parent
 		 */
-		throw new CKEditorError(
-			'batch-unwrap-element-no-parent: Trying to unwrap an element that has no parent.' );
+		throw new CKEditorError( 'batch-unwrap-element-no-parent: Trying to unwrap an element that has no parent.' );
 	}
 
 	const delta = new UnwrapDelta();

+ 33 - 1
packages/ckeditor5-engine/src/treemodel/delta/wrapdelta.js

@@ -6,8 +6,10 @@
 'use strict';
 
 import Delta from './delta.js';
+import UnwrapDelta from './unwrapdelta.js';
 import { register } from '../batch-base.js';
 import Position from '../position.js';
+import Range from '../range.js';
 import Element from '../element.js';
 import InsertOperation from '../operation/insertoperation.js';
 import MoveOperation from '../operation/moveoperation.js';
@@ -20,7 +22,37 @@ import CKEditorError from '../../ckeditorerror.js';
  *
  * @memberOf core.treeModel.delta
  */
-export default class WrapDelta extends Delta {}
+export default class WrapDelta extends Delta {
+	/**
+	 * Operation that moves wrapped nodes to their new parent or `null` if there are no operations in the delta.
+	 *
+	 * @protected
+	 * @type {core.treeModel.operation.MoveOperation|null}
+	 */
+	get _moveOperation() {
+		return this.operations[ 1 ] || null;
+	}
+
+	/**
+	 * @see core.treeModel.delta.Delta#_reverseDeltaClass
+	 * @private
+	 * @type {Object}
+	 */
+	get _reverseDeltaClass() {
+		return UnwrapDelta;
+	}
+
+	/**
+	 * Range to wrap or `null` if there are no operations in the delta.
+	 *
+	 * @type {core.treeModel.Range|null}
+	 */
+	get range() {
+		let moveOp = this._moveOperation;
+
+		return moveOp ? Range.createFromPositionAndShift( moveOp.sourcePosition, moveOp.howMany ) : null;
+	}
+}
 
 /**
  * Wraps given range with given element or with a new element of specified name if string has been passed.

+ 102 - 3
packages/ckeditor5-engine/tests/treemodel/delta/attributedelta.js

@@ -14,14 +14,22 @@ import Range from '/ckeditor5/core/treemodel/range.js';
 import Position from '/ckeditor5/core/treemodel/position.js';
 import Element from '/ckeditor5/core/treemodel/element.js';
 
+import AttributeDelta from '/ckeditor5/core/treemodel/delta/attributedelta.js';
+import AttributeOperation from '/ckeditor5/core/treemodel/operation/attributeoperation.js';
+
 const getIteratorCount = coreTestUtils.getIteratorCount;
 
+let doc, root;
+
+beforeEach( () => {
+	doc = new Document();
+	root = doc.createRoot( 'root' );
+} );
+
 describe( 'Batch', () => {
-	let doc, root, batch;
+	let batch;
 
 	beforeEach( () => {
-		doc = new Document();
-		root = doc.createRoot( 'root' );
 		batch = doc.batch();
 	} );
 
@@ -328,3 +336,94 @@ describe( 'Batch', () => {
 		} );
 	} );
 } );
+
+describe( 'AttributeDelta', () => {
+	let delta;
+
+	beforeEach( () => {
+		delta = new AttributeDelta();
+	} );
+
+	describe( 'key', () => {
+		it( 'should be null if there are no operations in delta', () => {
+			expect( delta.key ).to.be.null;
+		} );
+
+		it( 'should be equal to attribute operations key that are in delta', () => {
+			let range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) );
+			delta.addOperation( new AttributeOperation( range, 'key', 'old', 'new', 0 ) );
+
+			expect( delta.key ).to.equal( 'key' );
+		} );
+	} );
+
+	describe( 'value', () => {
+		it( 'should be null if there are no operations in delta', () => {
+			expect( delta.value ).to.be.null;
+		} );
+
+		it( 'should be equal to the value set by the delta operations', () => {
+			let range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) );
+			delta.addOperation( new AttributeOperation( range, 'key', 'old', 'new', 0 ) );
+
+			expect( delta.value ).to.equal( 'new' );
+		} );
+	} );
+
+	describe( 'range', () => {
+		it( 'should be null if there are no operations in delta', () => {
+			expect( delta.range ).to.be.null;
+		} );
+
+		it( 'should be equal to the range on which delta operates', () => {
+			// Delta operates on range [ 1 ] to [ 6 ] but omits [ 4 ] - [ 5 ] for "a reason".
+			// Still the range should be from [ 1 ] to [ 6 ]. Delta may not apply anything on [ 4 ] - [ 5 ]
+			// because it already has proper attribute.
+			let rangeA = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) );
+			let rangeB = new Range( new Position( root, [ 2 ] ), new Position( root, [ 4 ] ) );
+			let rangeC = new Range( new Position( root, [ 5 ] ), new Position( root, [ 6 ] ) );
+
+			delta.addOperation( new AttributeOperation( rangeA, 'key', 'oldA', 'new', 0 ) );
+			delta.addOperation( new AttributeOperation( rangeB, 'key', 'oldB', 'new', 1 ) );
+			delta.addOperation( new AttributeOperation( rangeC, 'key', 'oldC', 'new', 2 ) );
+
+			expect( delta.range.start.path ).to.deep.equal( [ 1 ] );
+			expect( delta.range.end.path ).to.deep.equal( [ 6 ] );
+		} );
+	} );
+
+	describe( 'getReversed', () => {
+		it( 'should return empty AttributeDelta if there are no operations in delta', () => {
+			let reversed = delta.getReversed();
+
+			expect( reversed ).to.be.instanceof( AttributeDelta );
+			expect( reversed.operations.length ).to.equal( 0 );
+		} );
+
+		it( 'should return correct AttributeDelta', () => {
+			let rangeA = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) );
+			let rangeB = new Range( new Position( root, [ 2 ] ), new Position( root, [ 4 ] ) );
+
+			delta.addOperation( new AttributeOperation( rangeA, 'key', 'oldA', 'new', 0 ) );
+			delta.addOperation( new AttributeOperation( rangeB, 'key', 'oldB', 'new', 1 ) );
+
+			let reversed = delta.getReversed();
+
+			expect( reversed ).to.be.instanceof( AttributeDelta );
+			expect( reversed.operations.length ).to.equal( 2 );
+
+			// Remember about reversed operations order.
+			expect( reversed.operations[ 0 ] ).to.be.instanceof( AttributeOperation );
+			expect( reversed.operations[ 0 ].range.isEqual( rangeB ) ).to.be.true;
+			expect( reversed.operations[ 0 ].key ).to.equal( 'key' );
+			expect( reversed.operations[ 0 ].oldValue ).to.equal( 'new' );
+			expect( reversed.operations[ 0 ].newValue ).to.equal( 'oldB' );
+
+			expect( reversed.operations[ 1 ] ).to.be.instanceof( AttributeOperation );
+			expect( reversed.operations[ 1 ].range.isEqual( rangeA ) ).to.be.true;
+			expect( reversed.operations[ 1 ].key ).to.equal( 'key' );
+			expect( reversed.operations[ 1 ].oldValue ).to.equal( 'new' );
+			expect( reversed.operations[ 1 ].newValue ).to.equal( 'oldA' );
+		} );
+	} );
+} );

+ 73 - 0
packages/ckeditor5-engine/tests/treemodel/delta/insertdelta.js

@@ -11,6 +11,12 @@ import Document from '/ckeditor5/core/treemodel/document.js';
 import Element from '/ckeditor5/core/treemodel/element.js';
 import Position from '/ckeditor5/core/treemodel/position.js';
 
+import InsertOperation from '/ckeditor5/core/treemodel/operation/insertoperation.js';
+import InsertDelta from '/ckeditor5/core/treemodel/delta/insertdelta.js';
+
+import RemoveDelta from '/ckeditor5/core/treemodel/delta/removedelta.js';
+import RemoveOperation from '/ckeditor5/core/treemodel/operation/removeoperation.js';
+
 describe( 'Batch', () => {
 	let doc, root, batch, p, ul, chain;
 
@@ -39,3 +45,70 @@ describe( 'Batch', () => {
 		} );
 	} );
 } );
+
+describe( 'InsertDelta', () => {
+	let insertDelta, doc, root;
+
+	beforeEach( () => {
+		doc = new Document();
+		root = doc.createRoot( 'root' );
+		insertDelta = new InsertDelta();
+	} );
+
+	describe( 'constructor', () => {
+		it( 'should create insert delta with no operations added', () => {
+			expect( insertDelta.operations.length ).to.equal( 0 );
+		} );
+	} );
+
+	describe( 'position', () => {
+		it( 'should be null if there are no operations in delta', () => {
+			expect( insertDelta.position ).to.be.null;
+		} );
+
+		it( 'should be equal to the position where node is inserted', () => {
+			insertDelta.operations.push( new InsertOperation( new Position( root, [ 1, 2, 3 ] ), new Element( 'x' ), 0 ) );
+
+			expect( insertDelta.position.root ).to.equal( root );
+			expect( insertDelta.position.path ).to.deep.equal( [ 1, 2, 3 ] );
+		} );
+	} );
+
+	describe( 'nodeList', () => {
+		it( 'should be null if there are no operations in delta', () => {
+			expect( insertDelta.nodeList ).to.be.null;
+		} );
+
+		it( 'should be equal to the node list inserted by the delta', () => {
+			let elementX = new Element( 'x' );
+			insertDelta.operations.push( new InsertOperation( new Position( root, [ 1, 2, 3 ] ), elementX, 0 ) );
+
+			expect( insertDelta.nodeList.length ).to.equal( 1 );
+			expect( insertDelta.nodeList.get( 0 ) ).to.equal( elementX );
+		} );
+	} );
+
+	describe( 'getReversed', () => {
+		it( 'should return empty RemoveDelta if there are no operations in delta', () => {
+			let reversed = insertDelta.getReversed();
+
+			expect( reversed ).to.be.instanceof( RemoveDelta );
+			expect( reversed.operations.length ).to.equal( 0 );
+		} );
+
+		it( 'should return correct RemoveDelta', () => {
+			let position = new Position( root, [ 1, 2, 3 ] );
+			let elementX = new Element( 'x' );
+			insertDelta.operations.push( new InsertOperation( position, elementX, 0 ) );
+
+			let reversed = insertDelta.getReversed();
+
+			expect( reversed ).to.be.instanceof( RemoveDelta );
+			expect( reversed.operations.length ).to.equal( 1 );
+
+			expect( reversed.operations[ 0 ] ).to.be.instanceof( RemoveOperation );
+			expect( reversed.operations[ 0 ].sourcePosition.isEqual( position ) ).to.be.true;
+			expect( reversed.operations[ 0 ].howMany ).to.equal( 1 );
+		} );
+	} );
+} );

+ 80 - 0
packages/ckeditor5-engine/tests/treemodel/delta/movedelta.js

@@ -14,6 +14,9 @@ import Range from '/ckeditor5/core/treemodel/range.js';
 import Element from '/ckeditor5/core/treemodel/element.js';
 import CKEditorError from '/ckeditor5/core/ckeditorerror.js';
 
+import MoveDelta from '/ckeditor5/core/treemodel/delta/movedelta.js';
+import MoveOperation from '/ckeditor5/core/treemodel/operation/moveoperation.js';
+
 const getNodesAndText = treeModelTestUtils.getNodesAndText;
 
 describe( 'Batch', () => {
@@ -66,3 +69,80 @@ describe( 'Batch', () => {
 		} );
 	} );
 } );
+
+describe( 'MoveDelta', () => {
+	let moveDelta, doc, root;
+
+	beforeEach( () => {
+		doc = new Document();
+		root = doc.createRoot( 'root' );
+		moveDelta = new MoveDelta();
+	} );
+
+	describe( 'constructor', () => {
+		it( 'should create move delta with no operations added', () => {
+			expect( moveDelta.operations.length ).to.equal( 0 );
+		} );
+	} );
+
+	describe( 'sourcePosition', () => {
+		it( 'should be null if there are no operations in delta', () => {
+			expect( moveDelta.sourcePosition ).to.be.null;
+		} );
+
+		it( 'should be equal to the position where node is inserted', () => {
+			moveDelta.operations.push( new MoveOperation( new Position( root, [ 1, 1 ] ), 2, new Position( root, [ 2, 2 ] ), 0 ) );
+
+			expect( moveDelta.sourcePosition.root ).to.equal( root );
+			expect( moveDelta.sourcePosition.path ).to.deep.equal( [ 1, 1 ] );
+		} );
+	} );
+
+	describe( 'howMany', () => {
+		it( 'should be null if there are no operations in delta', () => {
+			expect( moveDelta.howMany ).to.be.null;
+		} );
+
+		it( 'should be equal to the position where node is inserted', () => {
+			moveDelta.operations.push( new MoveOperation( new Position( root, [ 1, 1 ] ), 2, new Position( root, [ 2, 2 ] ), 0 ) );
+
+			expect( moveDelta.howMany ).to.equal( 2 );
+		} );
+	} );
+
+	describe( 'targetPosition', () => {
+		it( 'should be null if there are no operations in delta', () => {
+			expect( moveDelta.targetPosition ).to.be.null;
+		} );
+
+		it( 'should be equal to the move operation\'s target position', () => {
+			moveDelta.operations.push( new MoveOperation( new Position( root, [ 1, 1 ] ), 2, new Position( root, [ 2, 2 ] ), 0 ) );
+
+			expect( moveDelta.targetPosition.root ).to.equal( root );
+			expect( moveDelta.targetPosition.path ).to.deep.equal( [ 2, 2 ] );
+		} );
+	} );
+
+	describe( 'getReversed', () => {
+		it( 'should return empty MoveDelta if there are no operations in delta', () => {
+			let reversed = moveDelta.getReversed();
+
+			expect( reversed ).to.be.instanceof( MoveDelta );
+			expect( reversed.operations.length ).to.equal( 0 );
+		} );
+
+		it( 'should return correct MoveDelta', () => {
+			moveDelta.operations.push( new MoveOperation( new Position( root, [ 1, 1 ] ), 2, new Position( root, [ 2, 2 ] ), 0 ) );
+
+			let reversed = moveDelta.getReversed();
+
+			expect( reversed ).to.be.instanceof( MoveDelta );
+			expect( reversed.operations.length ).to.equal( 1 );
+
+			expect( reversed.operations[ 0 ] ).to.be.instanceof( MoveOperation );
+			expect( reversed.operations[ 0 ].sourcePosition.path ).to.deep.equal( [ 2, 2 ] );
+			expect( reversed.operations[ 0 ].howMany ).to.equal( 2 );
+			expect( reversed.operations[ 0 ].targetPosition.path ).to.deep.equal( [ 1, 1 ] );
+		} );
+	} );
+} );

+ 82 - 0
packages/ckeditor5-engine/tests/treemodel/delta/splitdelta.js

@@ -12,6 +12,13 @@ import Position from '/ckeditor5/core/treemodel/position.js';
 import Element from '/ckeditor5/core/treemodel/element.js';
 import CKEditorError from '/ckeditor5/core/ckeditorerror.js';
 
+import MergeDelta from '/ckeditor5/core/treemodel/delta/mergedelta.js';
+import SplitDelta from '/ckeditor5/core/treemodel/delta/splitdelta.js';
+
+import InsertOperation from '/ckeditor5/core/treemodel/operation/insertoperation.js';
+import MoveOperation from '/ckeditor5/core/treemodel/operation/moveoperation.js';
+import RemoveOperation from '/ckeditor5/core/treemodel/operation/removeoperation.js';
+
 describe( 'Batch', () => {
 	let doc, root, p;
 
@@ -83,3 +90,78 @@ describe( 'Batch', () => {
 		} );
 	} );
 } );
+
+describe( 'SplitDelta', () => {
+	let splitDelta, doc, root;
+
+	beforeEach( () => {
+		doc = new Document();
+		root = doc.createRoot( 'root' );
+		splitDelta = new SplitDelta();
+	} );
+
+	describe( 'constructor', () => {
+		it( 'should create split delta with no operations added', () => {
+			expect( splitDelta.operations.length ).to.equal( 0 );
+		} );
+	} );
+
+	describe( 'position', () => {
+		it( 'should be null if there are no operations in delta', () => {
+			expect( splitDelta.position ).to.be.null;
+		} );
+
+		it( 'should be equal to the position where node is split', () => {
+			splitDelta.operations.push( new InsertOperation( new Position( root, [ 1, 2 ] ), new Element( 'p' ), 0 ) );
+			splitDelta.operations.push( new MoveOperation( new Position( root, [ 1, 1, 4 ] ), 4, new Position( root, [ 1, 2, 0 ] ), 1 ) );
+
+			expect( splitDelta.position.root ).to.equal( root );
+			expect( splitDelta.position.path ).to.deep.equal( [ 1, 1, 4 ] );
+		} );
+	} );
+
+	describe( 'getReversed', () => {
+		it( 'should return empty MergeDelta if there are no operations in delta', () => {
+			let reversed = splitDelta.getReversed();
+
+			expect( reversed ).to.be.instanceof( MergeDelta );
+			expect( reversed.operations.length ).to.equal( 0 );
+		} );
+
+		it( 'should return correct SplitDelta', () => {
+			splitDelta.operations.push( new InsertOperation( new Position( root, [ 1, 2 ] ), new Element( 'p' ), 0 ) );
+			splitDelta.operations.push( new MoveOperation( new Position( root, [ 1, 1, 4 ] ), 4, new Position( root, [ 1, 2, 0 ] ), 1 ) );
+
+			let reversed = splitDelta.getReversed();
+
+			expect( reversed ).to.be.instanceof( MergeDelta );
+			expect( reversed.operations.length ).to.equal( 2 );
+
+			expect( reversed.operations[ 0 ] ).to.be.instanceof( MoveOperation );
+			expect( reversed.operations[ 0 ].sourcePosition.path ).to.deep.equal( [ 1, 2, 0 ] );
+			expect( reversed.operations[ 0 ].howMany ).to.equal( 4 );
+			expect( reversed.operations[ 0 ].targetPosition.path ).to.deep.equal( [ 1, 1, 4 ] );
+
+			expect( reversed.operations[ 1 ] ).to.be.instanceof( RemoveOperation );
+			expect( reversed.operations[ 1 ].sourcePosition.path ).to.deep.equal( [ 1, 2 ] );
+			expect( reversed.operations[ 1 ].howMany ).to.equal( 1 );
+		} );
+	} );
+
+	describe( '_cloneOperation', () => {
+		it( 'should return null if delta has no operations', () => {
+			expect( splitDelta._cloneOperation ).to.be.null;
+		} );
+
+		it( 'should return the first operation in the delta, which is InsertOperation or ReinsertOperation', () => {
+			let p = new Element( 'p' );
+			splitDelta.operations.push( new InsertOperation( new Position( root, [ 1, 2 ] ), p, 0 ) );
+			splitDelta.operations.push( new MoveOperation( new Position( root, [ 1, 1, 4 ] ), 4, new Position( root, [ 1, 2, 0 ] ), 1 ) );
+
+			expect( splitDelta._cloneOperation ).to.be.instanceof( InsertOperation );
+			expect( splitDelta._cloneOperation.nodeList.get( 0 ) ).to.equal( p );
+			expect( splitDelta._cloneOperation.position.path ).to.deep.equal( [ 1, 2 ] );
+		} );
+	} );
+} );
+

+ 69 - 0
packages/ckeditor5-engine/tests/treemodel/delta/unwrapdelta.js

@@ -9,8 +9,16 @@
 
 import Document from '/ckeditor5/core/treemodel/document.js';
 import Element from '/ckeditor5/core/treemodel/element.js';
+import Position from '/ckeditor5/core/treemodel/position.js';
 import CKEditorError from '/ckeditor5/core/ckeditorerror.js';
 
+import UnwrapDelta from '/ckeditor5/core/treemodel/delta/unwrapdelta.js';
+import WrapDelta from '/ckeditor5/core/treemodel/delta/wrapdelta.js';
+
+import MoveOperation from '/ckeditor5/core/treemodel/operation/moveoperation.js';
+import RemoveOperation from '/ckeditor5/core/treemodel/operation/removeoperation.js';
+import ReinsertOperation from '/ckeditor5/core/treemodel/operation/reinsertoperation.js';
+
 describe( 'Batch', () => {
 	let doc, root, p;
 
@@ -50,3 +58,64 @@ describe( 'Batch', () => {
 		} );
 	} );
 } );
+
+describe( 'UnwrapDelta', () => {
+	let unwrapDelta, doc, root;
+
+	beforeEach( () => {
+		doc = new Document();
+		root = doc.createRoot( 'root' );
+		unwrapDelta = new UnwrapDelta();
+	} );
+
+	describe( 'constructor', () => {
+		it( 'should create unwrap delta with no operations added', () => {
+			expect( unwrapDelta.operations.length ).to.equal( 0 );
+		} );
+	} );
+
+	describe( 'position', () => {
+		it( 'should be null if there are no operations in delta', () => {
+			expect( unwrapDelta.position ).to.be.null;
+		} );
+
+		it( 'should be equal to the position before unwrapped node', () => {
+			unwrapDelta.operations.push( new MoveOperation( new Position( root, [ 1, 2, 0 ] ), 4, new Position( root, [ 1, 2 ] ) ) );
+			unwrapDelta.operations.push( new RemoveOperation( new Position( root, [ 1, 6 ] ), 1 ) );
+
+			expect( unwrapDelta.position.root ).to.equal( root );
+			expect( unwrapDelta.position.path ).to.deep.equal( [ 1, 2 ] );
+		} );
+	} );
+
+	describe( 'getReversed', () => {
+		it( 'should return empty WrapDelta if there are no operations in delta', () => {
+			let reversed = unwrapDelta.getReversed();
+
+			expect( reversed ).to.be.instanceof( WrapDelta );
+			expect( reversed.operations.length ).to.equal( 0 );
+		} );
+
+		it( 'should return correct WrapDelta', () => {
+			unwrapDelta.operations.push( new MoveOperation( new Position( root, [ 1, 2, 0 ] ), 4, new Position( root, [ 1, 2 ] ) ) );
+			unwrapDelta.operations.push( new RemoveOperation( new Position( root, [ 1, 6 ] ), 1 ) );
+
+			let reversed = unwrapDelta.getReversed();
+
+			expect( reversed ).to.be.instanceof( WrapDelta );
+			expect( reversed.operations.length ).to.equal( 2 );
+
+			// WrapDelta which is an effect of reversing UnwrapDelta has ReinsertOperation instead of InsertOperation.
+			// This is because we will "wrap" nodes into the element in which they were in the first place.
+			// That element has been removed so we reinsert it from the graveyard.
+			expect( reversed.operations[ 0 ] ).to.be.instanceof( ReinsertOperation );
+			expect( reversed.operations[ 0 ].howMany ).to.equal( 1 );
+			expect( reversed.operations[ 0 ].targetPosition.path ).to.deep.equal( [ 1, 6 ] );
+
+			expect( reversed.operations[ 1 ] ).to.be.instanceof( MoveOperation );
+			expect( reversed.operations[ 1 ].sourcePosition.path ).to.deep.equal( [ 1, 2 ] );
+			expect( reversed.operations[ 1 ].howMany ).to.equal( 4 );
+			expect( reversed.operations[ 1 ].targetPosition.path ).to.deep.equal( [ 1, 6, 0 ] );
+		} );
+	} );
+} );

+ 66 - 0
packages/ckeditor5-engine/tests/treemodel/delta/wrapdelta.js

@@ -13,6 +13,13 @@ import Range from '/ckeditor5/core/treemodel/range.js';
 import Element from '/ckeditor5/core/treemodel/element.js';
 import CKEditorError from '/ckeditor5/core/ckeditorerror.js';
 
+import WrapDelta from '/ckeditor5/core/treemodel/delta/wrapdelta.js';
+import UnwrapDelta from '/ckeditor5/core/treemodel/delta/unwrapdelta.js';
+
+import InsertOperation from '/ckeditor5/core/treemodel/operation/insertoperation.js';
+import MoveOperation from '/ckeditor5/core/treemodel/operation/moveoperation.js';
+import RemoveOperation from '/ckeditor5/core/treemodel/operation/removeoperation.js';
+
 describe( 'Batch', () => {
 	let doc, root, range;
 
@@ -87,3 +94,62 @@ describe( 'Batch', () => {
 		} );
 	} );
 } );
+
+describe( 'WrapDelta', () => {
+	let wrapDelta, doc, root;
+
+	beforeEach( () => {
+		doc = new Document();
+		root = doc.createRoot( 'root' );
+		wrapDelta = new WrapDelta();
+	} );
+
+	describe( 'constructor', () => {
+		it( 'should create wrap delta with no operations added', () => {
+			expect( wrapDelta.operations.length ).to.equal( 0 );
+		} );
+	} );
+
+	describe( 'range', () => {
+		it( 'should be equal to null if there are no operations in delta', () => {
+			expect( wrapDelta.range ).to.be.null;
+		} );
+
+		it( 'should be equal to wrapped range', () => {
+			wrapDelta.operations.push( new InsertOperation( new Position( root, [ 1, 6 ] ), 1 ) );
+			wrapDelta.operations.push( new MoveOperation( new Position( root, [ 1, 1 ] ), 5, new Position( root, [ 1, 6, 0 ] ) ) );
+
+			expect( wrapDelta.range.start.isEqual( new Position( root, [ 1, 1 ] ) ) ).to.be.true;
+			expect( wrapDelta.range.end.isEqual( new Position( root, [ 1, 6 ] ) ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'getReversed', () => {
+		it( 'should return empty UnwrapDelta if there are no operations in delta', () => {
+			let reversed = wrapDelta.getReversed();
+
+			expect( reversed ).to.be.instanceof( UnwrapDelta );
+			expect( reversed.operations.length ).to.equal( 0 );
+		} );
+
+		it( 'should return correct UnwrapDelta', () => {
+			wrapDelta.operations.push( new InsertOperation( new Position( root, [ 1, 6 ] ), 1 ) );
+			wrapDelta.operations.push( new MoveOperation( new Position( root, [ 1, 1 ] ), 5, new Position( root, [ 1, 6, 0 ] ) ) );
+
+			let reversed = wrapDelta.getReversed();
+
+			expect( reversed ).to.be.instanceof( UnwrapDelta );
+			expect( reversed.operations.length ).to.equal( 2 );
+
+			expect( reversed.operations[ 0 ] ).to.be.instanceof( MoveOperation );
+			expect( reversed.operations[ 0 ].sourcePosition.path ).to.deep.equal( [ 1, 1, 0 ] );
+			expect( reversed.operations[ 0 ].howMany ).to.equal( 5 );
+			expect( reversed.operations[ 0 ].targetPosition.path ).to.deep.equal( [ 1, 1 ] );
+
+			expect( reversed.operations[ 1 ] ).to.be.instanceof( RemoveOperation );
+			expect( reversed.operations[ 1 ].sourcePosition.path ).to.deep.equal( [ 1, 6 ] );
+			expect( reversed.operations[ 1 ].howMany ).to.equal( 1 );
+		} );
+	} );
+} );
+