Przeglądaj źródła

Corrected typos and documentation for Tree Model classes.
Prepared Tree Model classes for OT algorithms.
Introduced ReinsertOperation class.
Introduced RootElement class.

Szymon Cofalik 10 lat temu
rodzic
commit
80a43a4f00

+ 11 - 11
packages/ckeditor5-utils/src/document/attribute.js

@@ -9,14 +9,14 @@ CKEDITOR.define( [ 'utils' ], function( utils ) {
 	/**
 	 * Attributes can store any additional information for nodes in the data model.
 	 *
-	 * @class Attribute
+	 * @class document.Attribute
 	 */
 	class Attribute {
 		/**
 		 * Create a new attribute class. Once attribute is created it should not be modified.
 		 *
-		 * @param {String} key Attribute key
-		 * @param {Mixed} value Attribute value
+		 * @param {String} key Attribute key.
+		 * @param {Mixed} value Attribute value.
 		 * @constructor
 		 */
 		constructor( key, value ) {
@@ -50,7 +50,7 @@ CKEDITOR.define( [ 'utils' ], function( utils ) {
 				return Attribute._register[ this._hash ];
 			}
 
-			// We do no care about the order so collections with the same elements should return the same hash.
+			// We do not care about the order, so collections with the same elements should return the same hash.
 			function sort( key, value ) {
 				if ( !utils.isArray( value ) && utils.isObject( value ) ) {
 					var sorted = {};
@@ -68,14 +68,14 @@ CKEDITOR.define( [ 'utils' ], function( utils ) {
 		}
 
 		/**
-		 * Compare two attributes. Returns true is two attributes have the same key and value even if the order of value
+		 * Compare two attributes. Returns true if two attributes have the same key and value even if the order of value
 		 * elements is different.
 		 *
 		 *	var attr1 = new Attribute( 'foo', { a: 1, b: 2 } );
 		 *	var attr2 = new Attribute( 'foo', { b: 2, a: 1 } );
 		 *	attr1.isEqual( attr2 ); // true
 		 *
-		 * @param {document.Attribute} otherAttr other attribute
+		 * @param {document.Attribute} otherAttr Attribute to compare with.
 		 * @returns {Boolean} True if attributes equals.
 		 */
 		isEqual( otherAttr ) {
@@ -83,7 +83,7 @@ CKEDITOR.define( [ 'utils' ], function( utils ) {
 		}
 
 		/**
-		 * To save memory common used attributes may be registered. If an attribute is registered the constructor will
+		 * To save memory, commonly used attributes may be registered. If an attribute is registered the constructor will
 		 * always return the same instance of this attribute.
 		 *
 		 * Note that attributes are registered globally.
@@ -95,8 +95,8 @@ CKEDITOR.define( [ 'utils' ], function( utils ) {
 		 *	attr1 === attr3 // true
 		 *
 		 * @static
-		 * @param {String} key Attribute key
-		 * @param {Mixed} value Attribute value
+		 * @param {String} key Attribute key.
+		 * @param {Mixed} value Attribute value.
 		 * @returns {document.Attribute} Registered attribute.
 		 */
 		static register( key, value ) {
@@ -113,7 +113,7 @@ CKEDITOR.define( [ 'utils' ], function( utils ) {
 	}
 
 	/**
-	 * Attribute register where all registered attributes are stored.
+	 * Register of attributes in which all registered attributes are stored.
 	 *
 	 * @static
 	 * @private
@@ -122,4 +122,4 @@ CKEDITOR.define( [ 'utils' ], function( utils ) {
 	Attribute._register = {};
 
 	return Attribute;
-} );
+} );

+ 61 - 84
packages/ckeditor5-utils/src/document/changeoperation.js

@@ -5,11 +5,11 @@
 
 'use strict';
 
-CKEDITOR.define( [ 'document/operation', 'ckeditorerror', 'utils' ], function( Operation, CKEditorError, utils ) {
+CKEDITOR.define( [ 'document/operation', 'ckeditorerror' ], function( Operation, CKEditorError ) {
 	/**
 	 * Operation to change nodes attribute. Using this class you can add, remove or change value of the attribute.
 	 *
-	 * @class document.Operation
+	 * @class document.ChangeOperation
 	 */
 	class ChangeOperation extends Operation {
 		/**
@@ -24,22 +24,22 @@ CKEDITOR.define( [ 'document/operation', 'ckeditorerror', 'utils' ], function( O
 		 * If both new and old attributes are set the operation will change the attribute value. Node that both new and
 		 * old attributes have to have the same key and the old attribute must be present in all nodes in ranges.
 		 *
-		 * @param {Array|document.Range} ranges Range or array of ranges on which operation should be applied.
-		 * @param {document.Attribute|null} oldAttr Old attribute to change. Null if operation inserts new attribute.
-		 * @param {document.Attribute|null} newAttr New attribute. Null if operation removes attribute.
+		 * @param {document.Range} range Range on which operation should be applied.
+		 * @param {document.Attribute|null} oldAttr Attribute to be removed. Null if operation just inserts a new attribute.
+		 * @param {document.Attribute|null} newAttr Attribute to be added. Null if operation just removes an attribute.
 		 * @param {Number} baseVersion {@link document.Document#version} on which operation can be applied.
 		 * @constructor
 		 */
-		constructor( ranges, oldAttr, newAttr, baseVersion ) {
+		constructor( range, oldAttr, newAttr, baseVersion ) {
 			super( baseVersion );
 
 			/**
-			 * Array of ranges on which operation should be applied.
+			 * Range on which operation should be applied.
 			 *
 			 * @readonly
-			 * @type {Array}
+			 * @type {document.Range}
 			 */
-			this.ranges = utils.isArray( ranges ) ? ranges : [ ranges ];
+			this.range = range;
 
 			/**
 			 * Old attribute to change. Null if operation inserts new attribute.
@@ -64,91 +64,68 @@ CKEDITOR.define( [ 'document/operation', 'ckeditorerror', 'utils' ], function( O
 		 * @protected
 		 */
 		_execute() {
-			var ranges = this.ranges;
 			var oldAttr = this.oldAttr;
 			var newAttr = this.newAttr;
+			var value;
 
-			var value, range;
+			if ( oldAttr !== null && newAttr !== null && oldAttr.key != newAttr.key ) {
+				/**
+				 * Old and new attributes should have the same keys.
+				 *
+				 * @error operation-change-different-keys
+				 * @param {document.ChangeOperation} changeOperation
+				 * @param {document.Attribute} oldAttr
+				 * @param {document.Attribute} newAttr
+				 */
+				throw new CKEditorError(
+					'operation-change-different-keys: Old and new attributes should have the same keys.',
+					{ changeOperation: this, oldAttr: oldAttr, newAttr: newAttr } );
+			}
 
-			// Remove.
-			if ( newAttr === null ) {
-				for ( range of ranges ) {
-					for ( value of range ) {
-						if ( !value.node.hasAttr( oldAttr ) ) {
-							/**
-							 * The attribute which should be removed does not exists.
-							 *
-							 * @error operation-change-no-attr-to-remove
-							 * @param {document.ChangeOperation} changeOperation
-							 * @param {document.Node} node
-							 * @param {document.Attribute} attr
-							 */
-							throw new CKEditorError(
-								'operation-change-no-attr-to-remove: The attribute which should be removed does not exists.',
-								{ changeOperation: this, node: value.node, attr: oldAttr } );
-						}
+			// Remove or change.
+			if ( oldAttr !== null ) {
+				for ( value of this.range ) {
+					if ( !value.node.hasAttr( oldAttr ) ) {
+						/**
+						 * The attribute which should be removed does not exists for given node.
+						 *
+						 * @error operation-change-no-attr-to-remove
+						 * @param {document.ChangeOperation} changeOperation
+						 * @param {document.Node} node
+						 * @param {document.Attribute} attr
+						 */
+						throw new CKEditorError(
+							'operation-change-no-attr-to-remove: The attribute which should be removed does not exists for given node.',
+							{ changeOperation: this, node: value.node, attr: oldAttr } );
+					}
 
+					// It looks like this condition should be in the if "above" but in that case we won't check if
+					// the whole range has attribute oldAttr. If we want to be super-clean, we should check it even
+					// if we don't apply any changes.
+					if ( newAttr === null ) {
 						value.node.removeAttr( oldAttr.key );
 					}
 				}
 			}
-			// Insert.
-			else if ( oldAttr === null ) {
-				for ( range of ranges ) {
-					for ( value of range ) {
-						if ( value.node.hasAttr( newAttr.key ) ) {
-							/**
-							 * The attribute with given key already exists.
-							 *
-							 * @error operation-change-attr-exists
-							 * @param {document.ChangeOperation} changeOperation
-							 * @param {document.Node} node
-							 * @param {document.Attribute} attr
-							 */
-							throw new CKEditorError(
-								'operation-change-attr-exists: The attribute with given key already exists.',
-								{ changeOperation: this, node: value.node, attr: newAttr } );
-						}
 
-						value.node.setAttr( newAttr );
+			// Insert or change.
+			if ( newAttr !== null ) {
+				for ( value of this.range ) {
+					if ( value.node.hasAttr( newAttr.key ) ) {
+						/**
+						 * The attribute with given key already exists for given node.
+						 *
+						 * @error operation-change-attr-exists
+						 * @param {document.ChangeOperation} changeOperation
+						 * @param {document.Node} node
+						 * @param {document.Attribute} attr
+						 */
+						throw new CKEditorError(
+							'operation-change-attr-exists: The attribute with given key already exists.',
+							{ changeOperation: this, node: value.node, attr: newAttr } );
 					}
-				}
-			}
-			// Change.
-			else {
-				if ( oldAttr.key != newAttr.key ) {
-					/**
-					 * Old and new attributes should have the same keys.
-					 *
-					 * @error operation-change-different-keys
-					 * @param {document.ChangeOperation} changeOperation
-					 * @param {document.Attribute} oldAttr
-					 * @param {document.Attribute} newAttr
-					 */
-					throw new CKEditorError(
-						'operation-change-different-keys: Old and new attributes should have the same keys.',
-						{ changeOperation: this, oldAttr: oldAttr, newAttr: newAttr } );
-				}
-
-				for ( range of ranges ) {
-					for ( value of range ) {
-						if ( !value.node.hasAttr( oldAttr ) ) {
-							/**
-							 * The attribute which should be changed does not exists.
-							 *
-							 * @error operation-change-no-attr-to-change
-							 * @param {document.ChangeOperation} changeOperation
-							 * @param {document.Node} node
-							 * @param {document.Attribute} oldAttr
-							 * @param {document.Attribute} newAttr
-							 */
-							throw new CKEditorError(
-								'operation-change-no-attr-to-change: The attribute which should be changed does not exists.',
-								{ changeOperation: this, node: value.node, oldAttr: oldAttr, newAttr: newAttr } );
-						}
 
-						value.node.setAttr( newAttr );
-					}
+					value.node.setAttr( newAttr );
 				}
 			}
 		}
@@ -159,9 +136,9 @@ CKEDITOR.define( [ 'document/operation', 'ckeditorerror', 'utils' ], function( O
 		 * @returns {document.ChangeOperation} Reverse operation.
 		 */
 		reverseOperation() {
-			return new ChangeOperation( this.ranges, this.newAttr, this.oldAttr, this.baseVersion + 1 );
+			return new ChangeOperation( this.range, this.newAttr, this.oldAttr, this.baseVersion + 1 );
 		}
 	}
 
 	return ChangeOperation;
-} );
+} );

+ 2 - 2
packages/ckeditor5-utils/src/document/character.js

@@ -7,7 +7,7 @@
 
 CKEDITOR.define( [ 'document/node' ], function( Node ) {
 	/**
-	 * Data structure for character stored in the linear data.
+	 * Data structure for character stored in the tree data model.
 	 *
 	 * @class Character
 	 */
@@ -33,4 +33,4 @@ CKEDITOR.define( [ 'document/node' ], function( Node ) {
 	}
 
 	return Character;
-} );
+} );

+ 61 - 5
packages/ckeditor5-utils/src/document/document.js

@@ -7,10 +7,13 @@
 
 CKEDITOR.define( [
 	'document/element',
+	'document/rootelement',
 	'emittermixin',
 	'utils',
 	'ckeditorerror'
-], function( Element, EmitterMixin, utils, CKEditorError ) {
+], function( Element, RootElement, EmitterMixin, utils, CKEditorError ) {
+	var graveyardSymbol = Symbol( 'graveyard' );
+
 	/**
 	 * Document model.
 	 *
@@ -24,12 +27,17 @@ CKEDITOR.define( [
 		 */
 		constructor() {
 			/**
-			 * Document tree root. Document always have an root document.
+			 * List of roots that are owned and managed by this document.
 			 *
 			 * @readonly
-			 * @property {String} root
+			 * @property {Map} roots
+			 */
+			this.roots = new Map();
+
+			/**
+			 * Graveyard tree root. Document always have a graveyard root, which is storing removed nodes.
 			 */
-			this.root = new Element( 'root' );
+			this.createRoot( graveyardSymbol );
 
 			/**
 			 * Document version. It starts from 0 and every operation increase the version. It is used to ensure that
@@ -67,9 +75,57 @@ CKEDITOR.define( [
 			this.version++;
 			this.fire( 'operationApplied', operation );
 		}
+
+		/**
+		 * Creates a new top-level root.
+		 *
+		 * @param {String|Symbol} name Unique root name.
+		 * @returns {document.RootElement} Created root.
+		 */
+		createRoot( name ) {
+			if ( this.roots.has( name ) ) {
+				/**
+				 * Root with specified name already exists.
+				 *
+				 * @error document-createRoot-name-exists
+				 * @param {document.Document} doc
+				 * @param {String} name
+				 */
+				throw new CKEditorError(
+					'document-createRoot-name-exists: Root with specified name already exists.',
+					{ doc: this, name: name }
+				);
+			}
+
+			var root = new RootElement( this );
+			this.roots.set( root, name );
+
+			return root;
+		}
+
+		/**
+		 * Returns top-level root by it's name.
+		 *
+		 * @param {String|Symbol} name Name of the root to get.
+		 * @returns (document.RootElement} Root registered under given name.
+		 */
+		getRoot( name ) {
+			return this.roots.get( name );
+		}
+
+		/**
+		 * Graveyard tree root. Document always have a graveyard root, which is storing removed nodes.
+		 *
+		 * @protected
+		 * @readonly
+		 * @property {document.RootElement} _graveyard
+		 */
+		get _graveyard() {
+			return this.getRoot( graveyardSymbol );
+		}
 	}
 
 	utils.extend( Document.prototype, EmitterMixin );
 
 	return Document;
-} );
+} );

+ 11 - 10
packages/ckeditor5-utils/src/document/element.js

@@ -7,20 +7,20 @@
 
 CKEDITOR.define( [ 'document/node', 'document/nodelist' ], function( Node, NodeList ) {
 	/**
-	 * Linear data element.
+	 * Tree data model element.
 	 *
 	 * @class document.Element
 	 */
 	class Element extends Node {
 		/**
-		 * Creates linear data element.
+		 * Creates tree data model element.
 		 *
 		 * This constructor should be used only internally by the document.
 		 *
 		 * @param {String} name Node name.
 		 * @param {Iterable} attrs Iterable collection of {@link document.Attribute attributes}.
-		 * @param {document.Node|document.Text|document.NodeList|String|Iterable} nodes List of nodes to be inserted.
-		 * List of nodes can be any type accepted by the {@link document.NodeList} constructor.
+		 * @param {document.Node|document.Text|document.NodeList|String|Iterable} children List of nodes to be inserted
+		 * into created element. List of nodes can be of any type accepted by the {@link document.NodeList} constructor.
 		 * @constructor
 		 */
 		constructor( name, attrs, children ) {
@@ -53,9 +53,9 @@ CKEDITOR.define( [ 'document/node', 'document/nodelist' ], function( Node, NodeL
 		 * Note that list of children can be modified only in elements not attached yet to the document.
 		 * All attached nodes should be modified using the {@link document.InsertOperation}.
 		 *
-		 * @param {Nuber} index Position where nodes should be inserted.
+		 * @param {Number} index Position where nodes should be inserted.
 		 * @param {document.Node|document.Text|document.NodeList|String|Iterable} nodes List of nodes to be inserted.
-		 * List of nodes can be any type accepted by the {@link document.NodeList} constructor.
+		 * List of nodes can be of any type accepted by the {@link document.NodeList} constructor.
 		 */
 		insertChildren( index, nodes ) {
 			this._children.insert( index, new NodeList( nodes ) );
@@ -73,6 +73,7 @@ CKEDITOR.define( [ 'document/node', 'document/nodelist' ], function( Node, NodeL
 		 *
 		 * @param {Number} index Position of the first node to remove.
 		 * @param {Number} number Number of nodes to remove.
+		 * @returns {document.NodeList} List of removed nodes.
 		 */
 
 		removeChildren( index, number ) {
@@ -80,11 +81,11 @@ CKEDITOR.define( [ 'document/node', 'document/nodelist' ], function( Node, NodeL
 				this._children.get( i ).parent = null;
 			}
 
-			this._children.remove( index, number );
+			return this._children.remove( index, number );
 		}
 
 		/**
-		 * Get child at given index.
+		 * Gets child at given index.
 		 *
 		 * @param {Number} index Index of child.
 		 * @returns {document.Node} Child node.
@@ -94,7 +95,7 @@ CKEDITOR.define( [ 'document/node', 'document/nodelist' ], function( Node, NodeL
 		}
 
 		/**
-		 * Get index of child node.
+		 * Gets index of child node.
 		 *
 		 * @param {document.Node} node Child node.
 		 * @returns {Number} Index of child.
@@ -114,4 +115,4 @@ CKEDITOR.define( [ 'document/node', 'document/nodelist' ], function( Node, NodeL
 	}
 
 	return Element;
-} );
+} );

+ 3 - 3
packages/ckeditor5-utils/src/document/insertoperation.js

@@ -51,7 +51,7 @@ CKEDITOR.define( [ 'document/operation', 'document/nodelist', 'document/removeop
 		}
 
 		/**
-		 * Creates an reverse remove operation.
+		 * Creates a reverse operation.
 		 *
 		 * @returns {document.RemoveOperation} Reverse operation.
 		 */
@@ -59,9 +59,9 @@ CKEDITOR.define( [ 'document/operation', 'document/nodelist', 'document/removeop
 			// Because of circular dependencies we need to re-require remove operation here.
 			var RemoveOperation = CKEDITOR.require( 'document/removeoperation' );
 
-			return new RemoveOperation( this.position, this.nodeList, this.baseVersion + 1 );
+			return new RemoveOperation( this.position, this.nodeList.length, this.baseVersion + 1 );
 		}
 	}
 
 	return InsertOperation;
-} );
+} );

+ 65 - 33
packages/ckeditor5-utils/src/document/moveoperation.js

@@ -9,30 +9,24 @@ CKEDITOR.define( [
 	'document/operation',
 	'document/nodelist',
 	'ckeditorerror',
-	'utils'
+	'utilis'
 ], function( Operation, NodeList, CKEditorError, utils ) {
 	/**
 	 * Operation to move list of following nodes from the one position in the document to another.
 	 *
-	 * @class document.Operation
+	 * @class document.MoveOperation
 	 */
 	class MoveOperation extends Operation {
 		/**
 		 * Creates a move operation.
 		 *
-		 * Note that this constructor is used not only to create an operation on the current state of the document,
-		 * but also to create reverse operation or the result of the operational transformation. The operation also
-		 * needs to keep data needed to transform it (creates an insert operation from the move & remove combination).
-		 * This is why this constructor contains list of nodes instead of length.
-		 *
-		 * @param {document.Position} sourcePosition Source move position.
-		 * @param {document.Position} targetPosition Target move position.
-		 * @param {document.Node|document.Text|document.NodeList|String|Iterable} nodes List of nodes to be moved.
-		 * List of nodes can be any type accepted by the {@link document.NodeList} constructor.
+		 * @param {document.Position} sourcePosition Position before the first element to move.
+		 * @param {document.Position} targetPosition Position where moved elements will be inserted.
+		 * @param {Number} howMany How many consecutive nodes to move, starting from sourcePosition.
 		 * @param {Number} baseVersion {@link document.Document#version} on which operation can be applied.
 		 * @constructor
 		 */
-		constructor( sourcePosition, targetPosition, nodes, baseVersion ) {
+		constructor( sourcePosition, targetPosition, howMany, baseVersion ) {
 			super( baseVersion );
 
 			/**
@@ -50,11 +44,11 @@ CKEDITOR.define( [
 			this.targetPosition = targetPosition;
 
 			/**
-			 * List of nodes to move.
+			 * How many nodes to move.
 			 *
-			 * @type {document.NodeList}
+			 * @type {Number}
 			 */
-			this.nodeList = new NodeList( nodes );
+			this.howMany = howMany;
 		}
 
 		/**
@@ -67,48 +61,86 @@ CKEDITOR.define( [
 			var targetElement = this.targetPosition.parent;
 			var sourceOffset = this.sourcePosition.offset;
 			var targetOffset = this.targetPosition.offset;
-			var nodeList = this.nodeList;
 
-			if ( CKEDITOR.isDebug ) {
-				var i = 0;
+			// Validate whether move operation has correct parameters.
+			// Validation is pretty complex but move operation is one of the core ways to manipulate the document state.
+			// We expect that many errors might be connected with one of scenarios described below.
+			if ( !sourceElement || !targetElement ) {
+				/**
+				 * Source position or target position is invalid.
+				 *
+				 * @error operation-move-position-invalid
+				 * @param {document.MoveOperation} moveOperation
+				 */
+				throw new CKEditorError(
+					'operation-move-source-position-invalid: Source position or target position is invalid.',
+					{ moveOperation: this }
+				);
+			} else if ( sourceOffset + this.howMany > sourceElement.getChildCount() ) {
+				/**
+				 * The nodes which should be moved do not exist.
+				 *
+				 * @error operation-move-nodes-do-not-exist
+				 * @param {document.MoveOperation} moveOperation
+				 */
+				throw new CKEditorError(
+					'operation-move-nodes-do-not-exist: The nodes which should be moved do not exist.',
+					{ moveOperation: this }
+				);
+			} else if ( sourceElement === targetElement && sourceOffset + this.howMany >= targetOffset ) {
+				/**
+				 * Trying to move a range of nodes into the middle of that range.
+				 *
+				 * @error operation-move-range-into-itself
+				 * @param {document.MoveOperation} moveOperation
+				 */
+				throw new CKEditorError(
+					'operation-move-range-into-itself: Trying to move a range of nodes to the inside of that range.',
+					{ moveOperation: this }
+				);
+			} else {
+				var sourcePath = this.sourcePosition.path.slice( 0, -1 );
+				var targetPath = this.targetPosition.path.slice( 0, -1 );
+
+				if ( utils.compareArrays( sourcePath, targetPath ) == utils.compareArrays.PREFIX ) {
+					var i = sourcePath.length;
 
-				for ( var node of this.nodeList ) {
-					if ( !utils.isEqual( sourceElement.getChild( sourceOffset + i ), node ) ) {
+					if ( this.targetPosition.path[ i ] >= sourceOffset && this.targetPosition.path[ i ] < sourceOffset + this.howMany ) {
 						/**
-						 * The node which should be removed does not exists.
+						 * Trying to move a range of nodes into one of nodes from that range.
 						 *
-						 * @error operation-move-node-does-not-exists:
+						 * @error operation-move-node-into-itself
 						 * @param {document.MoveOperation} moveOperation
-						 * @param {document.Node} node
 						 */
 						throw new CKEditorError(
-							'operation-move-node-does-not-exists: The node which should be moved does not exists.',
-							{ moveOperation: this, node: this.node } );
+							'operation-move-node-into-itself: Trying to move a range of nodes into one of nodes from that range.',
+							{ moveOperation: this }
+						);
 					}
-					i++;
 				}
 			}
-
-			sourceElement.removeChildren( sourceOffset, nodeList.length );
+			// End of validation.
 
 			// If we move children in the same element and we remove elements on the position before the target we
 			// need to update a target offset.
 			if ( sourceElement === targetElement && sourceOffset < targetOffset ) {
-				targetOffset -= nodeList.length;
+				targetOffset -= this.howMany;
 			}
 
-			targetElement.insertChildren( targetOffset, this.nodeList );
+			var removedNodes = sourceElement.removeChildren( sourceOffset, this.howMany );
+
+			targetElement.insertChildren( targetOffset, removedNodes );
 		}
 
 		/**
-		 * Creates an reverse move operation.
+		 * Creates a reverse operation.
 		 *
 		 * @returns {document.MoveOperation} Reverse operation.
 		 */
 		reverseOperation() {
-			return new MoveOperation( this.targetPosition, this.sourcePosition, this.nodeList, this.baseVersion + 1 );
+			return new MoveOperation( this.targetPosition, this.sourcePosition, this.howMany, this.baseVersion + 1 );
 		}
 	}
 
 	return MoveOperation;
-} );
+} );

+ 4 - 4
packages/ckeditor5-utils/src/document/node.js

@@ -51,7 +51,7 @@ CKEDITOR.define( [ 'document/attribute', 'utils' ], function( Attribute, utils )
 		hasAttr( key ) {
 			var attr;
 
-			// Attribute
+			// Attribute.
 			if ( key instanceof Attribute ) {
 				for ( attr of this._attrs ) {
 					if ( attr.isEqual( key ) ) {
@@ -59,7 +59,7 @@ CKEDITOR.define( [ 'document/attribute', 'utils' ], function( Attribute, utils )
 					}
 				}
 			}
-			// Key
+			// Key.
 			else {
 				for ( attr of this._attrs ) {
 					if ( attr.key == key ) {
@@ -149,7 +149,7 @@ CKEDITOR.define( [ 'document/attribute', 'utils' ], function( Attribute, utils )
 		}
 
 		/**
-		 * Dept of the node, which equals total number of its parents.
+		 * Depth of the node, which equals to total number of its parents.
 		 *
 		 * @readonly
 		 * @property {Number} depth
@@ -241,4 +241,4 @@ CKEDITOR.define( [ 'document/attribute', 'utils' ], function( Attribute, utils )
 	}
 
 	return Node;
-} );
+} );

+ 5 - 6
packages/ckeditor5-utils/src/document/nodelist.js

@@ -13,11 +13,9 @@ CKEDITOR.define( [
 ], function( Character, Text, Node, utils ) {
 	/**
 	 * List of nodes. It is used to represent multiple nodes with a given order, for example children of
-	 * elements {@link document.Element}.
+	 * {@link document.Element} object or nodes inserted using {@link document.InsertOperation}.
 	 *
-	 * This class let you modify list of nodes, for example nodes to insert and pass the reference for such list.
-	 *
-	 * Thanks to the constructor which accept various structure, this class let you easily create list of text node.
+	 * Thanks to the constructor, which accepts various arguments, this class lets you easily create desired list of nodes.
 	 *
 	 * It also may internally compress nodes.
 	 *
@@ -120,9 +118,10 @@ CKEDITOR.define( [
 		 *
 		 * @param {Number} index Position of the first node to remove.
 		 * @param {Number} number Number of nodes to remove.
+		 * @returns {document.NodeList} List of removed nodes.
 		 */
 		remove( index, number ) {
-			this._nodes.splice( index, number );
+			return new NodeList( this._nodes.splice( index, number ) );
 		}
 
 		/**
@@ -155,4 +154,4 @@ CKEDITOR.define( [
 	}
 
 	return NodeList;
-} );
+} );

+ 3 - 3
packages/ckeditor5-utils/src/document/operation.js

@@ -21,15 +21,15 @@ CKEDITOR.define( [], function() {
 		constructor( baseVersion ) {
 			/**
 			 * {@link document.Document#version} on which operation can be applied. If you try to
-			 * {@link document.Document#applyOperation apply} operation with different base version then
+			 * {@link document.Document#applyOperation apply} operation with different base version than
 			 * {@link document.Document#version document version} the {@link document-applyOperation-wrong-version}
 			 * error is thrown.
 			 *
-			 * @type {Number}
+			 * @property {Number} baseVersion
 			 */
 			this.baseVersion = baseVersion;
 		}
 	}
 
 	return Operation;
-} );
+} );

+ 5 - 5
packages/ckeditor5-utils/src/document/position.js

@@ -7,7 +7,7 @@
 
 CKEDITOR.define( [ 'utils', 'ckeditorerror' ], function( utils, CKEditorError ) {
 	/**
-	 * Position is always before of after a node.
+	 * Position is always before or after a node.
 	 * See {@link #path} property for more information.
 	 *
 	 * @class document.Position
@@ -84,7 +84,7 @@ CKEDITOR.define( [ 'utils', 'ckeditorerror' ], function( utils, CKEditorError )
 		/**
 		 * Set the position after given node.
 		 *
-		 * @param {document.node} node Node the position should be directly after.
+		 * @param {document.Node} node Node the position should be directly after.
 		 */
 		static createAfter( node ) {
 			if ( !node.parent ) {
@@ -132,7 +132,7 @@ CKEDITOR.define( [ 'utils', 'ckeditorerror' ], function( utils, CKEditorError )
 		 * Node directly before the position.
 		 *
 		 * @readonly
-		 * @type {Node}
+		 * @type {document.Node}
 		 */
 		get nodeBefore() {
 			return this.parent.getChild( this.offset - 1 ) || null;
@@ -142,7 +142,7 @@ CKEDITOR.define( [ 'utils', 'ckeditorerror' ], function( utils, CKEditorError )
 		 * Node directly after the position.
 		 *
 		 * @readonly
-		 * @type {Node}
+		 * @type {document.Node}
 		 */
 		get nodeAfter() {
 			return this.parent.getChild( this.offset ) || null;
@@ -160,4 +160,4 @@ CKEDITOR.define( [ 'utils', 'ckeditorerror' ], function( utils, CKEditorError )
 	}
 
 	return Position;
-} );
+} );

+ 16 - 16
packages/ckeditor5-utils/src/document/positioniterator.js

@@ -10,8 +10,8 @@ CKEDITOR.define( [
 	'document/element',
 	'document/position'
 ], function( Character, Element, Position ) {
-	var OPENING_TAG = 0;
-	var CLOSING_TAG = 1;
+	var ELEMENT_ENTER = 0;
+	var ELEMENT_LEAVE = 1;
 	var CHARACTER = 2;
 
 	/**
@@ -57,8 +57,8 @@ CKEDITOR.define( [
 		 * @returns {Object} Value between last and new {@link #position}.
 		 * @returns {Boolean} return.done True if iterator is done.
 		 * @returns {Object} return.value
-		 * @returns {Number} return.value.type Skipped value type, possible options: {@link PositionIterator#OPENING_TAG},
-		 * {@link PositionIterator#CLOSING_TAG} or {@link PositionIterator#CHARACTER}.
+		 * @returns {Number} return.value.type Skipped value type, possible options: {@link PositionIterator#ELEMENT_ENTER},
+		 * {@link PositionIterator#ELEMENT_LEAVE} or {@link PositionIterator#CHARACTER}.
 		 * @returns {Node} return.value.node Skipped node.
 		 */
 		next() {
@@ -79,7 +79,7 @@ CKEDITOR.define( [
 			if ( nodeAfter instanceof Element ) {
 				this.position = Position.createFromParentAndOffset( nodeAfter, 0 );
 
-				return formatReturnValue( OPENING_TAG, nodeAfter );
+				return formatReturnValue( ELEMENT_ENTER, nodeAfter );
 			} else if ( nodeAfter instanceof Character ) {
 				this.position = Position.createFromParentAndOffset( parent, position.offset + 1 );
 
@@ -87,7 +87,7 @@ CKEDITOR.define( [
 			} else {
 				this.position = Position.createFromParentAndOffset( parent.parent, parent.positionInParent + 1 );
 
-				return formatReturnValue( CLOSING_TAG, this.position.nodeBefore );
+				return formatReturnValue( ELEMENT_LEAVE, this.position.nodeBefore );
 			}
 		}
 
@@ -97,15 +97,15 @@ CKEDITOR.define( [
 		 * @returns {Object} Value between last and new {@link #position}.
 		 * @returns {Boolean} return.done True if iterator is done.
 		 * @returns {Object} return.value
-		 * @returns {Number} return.value.type Skipped value type, possible options: {@link PositionIterator#OPENING_TAG},
-		 * {@link PositionIterator#CLOSING_TAG} or {@link PositionIterator#CHARACTER}.
+		 * @returns {Number} return.value.type Skipped value type, possible options: {@link PositionIterator#ELEMENT_ENTER},
+		 * {@link PositionIterator#ELEMENT_LEAVE} or {@link PositionIterator#CHARACTER}.
 		 * @returns {Node} return.value.node Skipped node.
 		 */
 		previous() {
 			var position = this.position;
 			var parent = position.parent;
 
-			// We are at the begging of the root.
+			// We are at the beginning of the root.
 			if ( parent.parent === null && position.offset === 0 ) {
 				return { done: true };
 			}
@@ -119,7 +119,7 @@ CKEDITOR.define( [
 			if ( nodeBefore instanceof Element ) {
 				this.position = Position.createFromParentAndOffset( nodeBefore, nodeBefore.getChildCount() );
 
-				return formatReturnValue( CLOSING_TAG, nodeBefore );
+				return formatReturnValue( ELEMENT_LEAVE, nodeBefore );
 			} else if ( nodeBefore instanceof Character ) {
 				this.position = Position.createFromParentAndOffset( parent, position.offset - 1 );
 
@@ -127,7 +127,7 @@ CKEDITOR.define( [
 			} else {
 				this.position = Position.createFromParentAndOffset( parent.parent, parent.positionInParent );
 
-				return formatReturnValue( OPENING_TAG, this.position.nodeAfter );
+				return formatReturnValue( ELEMENT_ENTER, this.position.nodeAfter );
 			}
 		}
 	}
@@ -143,18 +143,18 @@ CKEDITOR.define( [
 	}
 
 	/**
-	 * Flag for opening tag.
+	 * Flag for element entering.
 	 *
 	 * @readonly
 	 */
-	PositionIterator.OPENING_TAG = OPENING_TAG;
+	PositionIterator.ELEMENT_ENTER = ELEMENT_ENTER;
 
 	/**
-	 * Flag for closing tag.
+	 * Flag for element leaving.
 	 *
 	 * @readonly
 	 */
-	PositionIterator.CLOSING_TAG = CLOSING_TAG;
+	PositionIterator.ELEMENT_LEAVE = ELEMENT_LEAVE;
 
 	/**
 	 * Flag for character.
@@ -164,4 +164,4 @@ CKEDITOR.define( [
 	PositionIterator.CHARACTER = CHARACTER;
 
 	return PositionIterator;
-} );
+} );

+ 4 - 4
packages/ckeditor5-utils/src/document/range.js

@@ -36,10 +36,10 @@ CKEDITOR.define( [ 'document/positioniterator' ], function( PositionIterator ) {
 		}
 
 		/**
-		 * Two ranges equals if first and last positions equal.
+		 * Two ranges equal if their start and end positions equal.
 		 *
-		 * @param {document.Range} otherRange Range to compare.
-		 * @returns {Boolean} true if ranges equal.
+		 * @param {document.Range} otherRange Range to compare with.
+		 * @returns {Boolean} True if ranges equal.
 		 */
 		isEqual( otherRange ) {
 			return this.start.isEqual( otherRange.start ) && this.end.isEqual( otherRange.end );
@@ -56,4 +56,4 @@ CKEDITOR.define( [ 'document/positioniterator' ], function( PositionIterator ) {
 	}
 
 	return Range;
-} );
+} );

+ 36 - 0
packages/ckeditor5-utils/src/document/reinsertoperation.js

@@ -0,0 +1,36 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [
+	'document/moveoperation',
+	'document/removeoperation'
+], function( MoveOperation ) {
+	/**
+	 * Operation to reinsert previously removed nodes back to the non-graveyard root.
+	 * This is basically MoveOperation but it returns RemoveOperation when reversed.
+	 * We achieve two goals: by having separate classes it's easier to distinguish whether move
+	 * operation is actually a remove/reinsert operation and fire proper events. Also it
+	 * will be easier to expand if we need to change operation's behavior if it is remove/reinsert.
+	 *
+	 * @class document.ReinsertOperation
+	 */
+	class ReinsertOperation extends MoveOperation {
+		/**
+		 * Creates a reverse operation.
+		 *
+		 * @returns {document.RemoveOperation} Reverse operation.
+		 */
+		reverseOperation() {
+			// Because of circular dependencies we need to re-require reinsert operation here.
+			var RemoveOperation = CKEDITOR.require( 'document/removeoperation' );
+
+			return new RemoveOperation( this.targetPosition, this.howMany, this.baseVersion + 1 );
+		}
+	}
+
+	return ReinsertOperation;
+} );

+ 18 - 65
packages/ckeditor5-utils/src/document/removeoperation.js

@@ -6,94 +6,47 @@
 'use strict';
 
 CKEDITOR.define( [
-	'document/operation',
-	'document/nodelist',
-	'ckeditorerror',
-	'utils',
-	'document/insertoperation'
-], function( Operation, NodeList, CKEditorError, utils ) {
+	'document/moveoperation',
+	'document/position',
+	'document/reinsertoperation'
+], function( MoveOperation, Position ) {
 	/**
-	 * Operation to remove list of nodes.
+	 * Operation to remove a range of nodes.
 	 *
 	 * @class document.RemoveOperation
 	 */
-	class RemoveOperation extends Operation {
+	class RemoveOperation extends MoveOperation {
 		/**
 		 * Creates a remove operation.
 		 *
-		 * Note that this constructor is used not only to create an operation on the current state of the document,
-		 * but also to create reverse operation or the result of the operational transformation. The operation also
-		 * needs to keep data needed to transform it (creates an insert operation as a reverse of the remove).
-		 * This is why this constructor contains list of nodes instead of length.
-		 *
 		 * @param {document.Position} position Position before the first node to remove.
-		 * @param {document.Node|document.Text|document.NodeList|String|Iterable} nodes List of nodes to be remove.
-		 * List of nodes can be any type accepted by the {@link document.NodeList} constructor.
+		 * @param {Number} howMany How many nodes to remove.
 		 * @param {Number} baseVersion {@link document.Document#version} on which operation can be applied.
 		 * @constructor
 		 */
-		constructor( position, nodes, baseVersion ) {
-			super( baseVersion );
-
-			/**
-			 * Position of insertion.
-			 *
-			 * @type {document.Position}
-			 */
-			this.position = position;
+		constructor( position, howMany, baseVersion ) {
+			var graveyard = position.root.document._graveyard;
 
 			/**
-			 * List of nodes to insert.
-			 *
-			 * @type {document.NodeList}
+			 * Position in a graveyard where nodes were moved.
 			 */
-			this.nodeList = new NodeList( nodes );
-		}
-
-		/**
-		 * Execute operation.
-		 *
-		 * @protected
-		 */
-		_execute() {
-			var parent = this.position.parent;
-			var offset = this.position.offset;
-
-			if ( CKEDITOR.isDebug ) {
-				var i = 0;
-
-				for ( var node of this.nodeList ) {
-					if ( !utils.isEqual( parent.getChild( offset + i ), node ) ) {
-						/**
-						 * The node which should be removed does not exists.
-						 *
-						 * @error operation-remove-node-does-not-exists:
-						 * @param {document.RemoveOperation} removeOperation
-						 * @param {document.Node} node
-						 */
-						throw new CKEditorError(
-							'operation-remove-node-does-not-exists: The node which should be removed does not exists.',
-							{ removeOperation: this, node: this.node } );
-					}
-					i++;
-				}
-			}
+			var graveyardPosition = Position.createFromParentAndOffset( graveyard, 0 );
 
-			parent.removeChildren( offset, this.nodeList.length );
+			super( position, graveyardPosition, howMany, baseVersion );
 		}
 
 		/**
-		 * Creates an reverse insert operation.
+		 * Creates a reverse operation.
 		 *
-		 * @returns {document.InsertOperation} Reverse operation.
+		 * @returns {document.ReinsertOperation} Reverse operation.
 		 */
 		reverseOperation() {
-			// Because of circular dependencies we need to re-require insert operation here.
-			var InsertOperation = CKEDITOR.require( 'document/insertoperation' );
+			// Because of circular dependencies we need to re-require reinsert operation here.
+			var ReinsertOperation = CKEDITOR.require( 'document/reinsertoperation' );
 
-			return new InsertOperation( this.position, this.nodeList, this.baseVersion + 1 );
+			return new ReinsertOperation( this.targetPosition, this.sourcePosition, this.howMany, this.baseVersion + 1 );
 		}
 	}
 
 	return RemoveOperation;
-} );
+} );

+ 46 - 0
packages/ckeditor5-utils/src/document/rootelement.js

@@ -0,0 +1,46 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [], function() {
+	/**
+	 * Class for nodes that are roots of trees in tree data model.
+	 *
+	 * @class document.RootElement
+	 */
+	class RootElement extends Element {
+		/**
+		 * Creates tree root node.
+		 *
+		 * @param {document.Document} document {@link document.Document} that is an owner of the root.
+		 * @constructor
+		 */
+		constructor( document ) {
+			super( 'root' );
+
+			/**
+			 * {@link document.Document} that is an owner of this root.
+			 *
+			 * @readonly
+			 * @protected
+			 * @property {document.Document}
+			 */
+			this._document = document;
+		}
+
+		/**
+		 * Document that is an owner of this root.
+		 *
+		 * @readonly
+		 * @property {Document} document
+		 */
+		get document() {
+			return this._document;
+		}
+	}
+
+	return RootElement;
+} );

+ 57 - 0
packages/ckeditor5-utils/src/utils.js

@@ -53,9 +53,66 @@ CKEDITOR.define( [ 'utils-lodash', 'lib/lodash/lodash-ckeditor' ], function( lod
 		 */
 		isIterable( value ) {
 			return !!( value && value[ Symbol.iterator ] );
+		},
+
+		/**
+		 * Compares how given arrays relate to each other. One array can be: same as another array, prefix of another array
+		 * or completely different.
+		 *
+		 *   compareArrays( [ 0, 2 ], [ 0, 2 ] ); // SAME
+		 *   compareArrays( [ 0, 2 ], [ 0 ] ); // PREFIX
+		 *   compareArrays( [ 0, 2 ], [ 0, 2, 1 ] ); // DIFFERENT
+		 *   compareArrays( [ 0, 2 ], [ 1, 3 ] ); // DIFFERENT
+		 *
+		 * @param {Array} a Array that is compared.
+		 * @param {Array} b Array to compare with.
+		 * @returns {Number} How array `a` is related to array `b`. Represented by one of flags:
+		 * `a` is {@link utils.compareArrays#SAME same}, `a` is a {@link utils.compareArrays#PREFIX prefix}
+		 * or `a` is {@link utils.compareArrays#DIFFERENT different}.
+		 */
+		compareArrays( a, b ) {
+			var minLen = Math.min( a.length, b.length );
+
+			for ( var i = 0; i < minLen; i++ ) {
+				if ( a[ i ] != b[ i ] ) {
+					// The arrays are different.
+					return this.DIFFERENT;
+				}
+			}
+
+			// Both arrays were same at all points.
+			if ( a.length == b.length ) {
+				// If their length is also same, they are the same.
+				return this.SAME;
+			} else if ( a.length < b.length ) {
+				// Compared array is shorter so it is a prefix of the other array.
+				return this.PREFIX;
+			}
+
+			// In other case, the arrays are different.
+			return this.DIFFERENT;
 		}
 	};
 
+	/**
+	 * Flag for "is same as" relation between arrays.
+	 *
+	 * @type {number}
+	 */
+	utils.compareArrays.SAME = 0;
+	/**
+	 * Flag for "is a prefix of" relation between arrays.
+	 *
+	 * @type {number}
+	 */
+	utils.compareArrays.PREFIX = 1;
+	/**
+	 * Flag for "is different than" relation between arrays.
+	 *
+	 * @type {number}
+	 */
+	utils.compareArrays.DIFFERENT = 2;
+
 	// Extend "utils" with Lo-Dash methods.
 	for ( var i = 0; i < lodashIncludes.length; i++ ) {
 		utils[ lodashIncludes[ i ] ] = lodash[ lodashIncludes[ i ] ];

+ 2 - 2
packages/ckeditor5-utils/tests/document/positioniterator.js

@@ -76,7 +76,7 @@ describe( 'range iterator', function() {
 		var PositionIterator = modules[ 'document/positioniterator' ];
 		var Position = modules[ 'document/position' ];
 
-		var iterator = new PositionIterator( new Position( [ 0 ], doc.root ) ); // begging of root
+		var iterator = new PositionIterator( new Position( [ 0 ], doc.root ) ); // beginning of root
 		var i, len;
 
 		for ( i = 0, len = expectedItems.length; i < len; i++ ) {
@@ -150,4 +150,4 @@ describe( 'range iterator', function() {
 		}
 		expect( i ).to.equal( expectedItems.length );
 	} );
-} );
+} );