浏览代码

Merge pull request #70 from cksource/t/33-tree

Tree Data Model. Fixes: #33.
Piotr Jasiun 10 年之前
父节点
当前提交
36bb96a018
共有 37 个文件被更改,包括 5224 次插入738 次删除
  1. 125 0
      packages/ckeditor5-engine/src/document/attribute.js
  2. 36 0
      packages/ckeditor5-engine/src/document/character.js
  3. 139 0
      packages/ckeditor5-engine/src/document/document.js
  4. 118 0
      packages/ckeditor5-engine/src/document/element.js
  5. 252 0
      packages/ckeditor5-engine/src/document/node.js
  6. 156 0
      packages/ckeditor5-engine/src/document/nodelist.js
  7. 130 0
      packages/ckeditor5-engine/src/document/operation/changeoperation.js
  8. 61 0
      packages/ckeditor5-engine/src/document/operation/insertoperation.js
  9. 128 0
      packages/ckeditor5-engine/src/document/operation/moveoperation.js
  10. 79 0
      packages/ckeditor5-engine/src/document/operation/operation.js
  11. 33 0
      packages/ckeditor5-engine/src/document/operation/reinsertoperation.js
  12. 45 0
      packages/ckeditor5-engine/src/document/operation/removeoperation.js
  13. 177 0
      packages/ckeditor5-engine/src/document/position.js
  14. 175 0
      packages/ckeditor5-engine/src/document/positioniterator.js
  15. 59 0
      packages/ckeditor5-engine/src/document/range.js
  16. 35 0
      packages/ckeditor5-engine/src/document/rootelement.js
  17. 42 0
      packages/ckeditor5-engine/src/document/text.js
  18. 1057 678
      packages/ckeditor5-engine/src/lib/lodash/lodash-ckeditor.js
  19. 25 1
      packages/ckeditor5-engine/src/utils-lodash.js
  20. 74 1
      packages/ckeditor5-engine/src/utils.js
  21. 81 0
      packages/ckeditor5-engine/tests/document/attribute.js
  22. 52 0
      packages/ckeditor5-engine/tests/document/character.js
  23. 109 0
      packages/ckeditor5-engine/tests/document/document.js
  24. 124 0
      packages/ckeditor5-engine/tests/document/element.js
  25. 253 0
      packages/ckeditor5-engine/tests/document/node.js
  26. 115 0
      packages/ckeditor5-engine/tests/document/nodelist.js
  27. 315 0
      packages/ckeditor5-engine/tests/document/operation/changeoperation.js
  28. 160 0
      packages/ckeditor5-engine/tests/document/operation/insertoperation.js
  29. 245 0
      packages/ckeditor5-engine/tests/document/operation/moveoperation.js
  30. 81 0
      packages/ckeditor5-engine/tests/document/operation/reinsertoperation.js
  31. 103 0
      packages/ckeditor5-engine/tests/document/operation/removeoperation.js
  32. 229 0
      packages/ckeditor5-engine/tests/document/position.js
  33. 137 0
      packages/ckeditor5-engine/tests/document/positioniterator.js
  34. 69 0
      packages/ckeditor5-engine/tests/document/range.js
  35. 38 0
      packages/ckeditor5-engine/tests/document/rootelement.js
  36. 31 0
      packages/ckeditor5-engine/tests/document/text.js
  37. 136 58
      packages/ckeditor5-engine/tests/utils/utils.js

+ 125 - 0
packages/ckeditor5-engine/src/document/attribute.js

@@ -0,0 +1,125 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [ 'utils' ], function( utils ) {
+	/**
+	 * Attributes can store any additional information for nodes in the data model.
+	 *
+	 * @class document.Attribute
+	 */
+	class Attribute {
+		/**
+		 * Creates a new instance of the `Attribute` class. Once attribute is created it is immutable.
+		 *
+		 * @param {String} key Attribute key.
+		 * @param {Mixed} value Attribute value.
+		 * @constructor
+		 */
+		constructor( key, value ) {
+			/**
+			 * Attribute key.
+			 *
+			 * @readonly
+			 * @property {String} key
+			 */
+			this.key = key;
+
+			/**
+			 * Attribute value. Note that value may be any type, including objects.
+			 *
+			 * @readonly
+			 * @property {Mixed} value
+			 */
+			this.value = value;
+
+			/**
+			 * Attribute hash. Used to compare attributes. Two attributes with the same key and value will have the same hash.
+			 *
+			 * @readonly
+			 * @private
+			 * @property {String} _hash
+			 */
+			this._hash = this.key + ': ' + JSON.stringify( this.value, sort );
+
+			// If attribute is already registered the registered one should be returned.
+			if ( Attribute._register[ this._hash ] ) {
+				return Attribute._register[ this._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 = {};
+
+					// Sort keys and fill up the sorted object.
+					Object.keys( value ).sort().forEach( function( key ) {
+						sorted[ key ] = value[ key ];
+					} );
+
+					return sorted;
+				} else {
+					return value;
+				}
+			}
+		}
+
+		/**
+		 * Compares two attributes. Returns `true` if two attributes have the same key and value even if the order of keys
+		 * in the value object 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 Attribute to compare with.
+		 * @returns {Boolean} True if attributes are equal to each other.
+		 */
+		isEqual( otherAttr ) {
+			return this._hash === otherAttr._hash;
+		}
+
+		/**
+		 * 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.
+		 *
+		 *		var attr1 = Attribute.register( 'bold', true );
+		 *		var attr2 = Attribute.register( 'bold', true );
+		 *		var attr3 = new Attribute( 'bold', true );
+		 *		attr1 === attr2 // true
+		 *		attr1 === attr3 // true
+		 *
+		 * @static
+		 * @param {String} key Attribute key.
+		 * @param {Mixed} value Attribute value.
+		 * @returns {document.Attribute} Registered attribute.
+		 */
+		static register( key, value ) {
+			var attr = new Attribute( key, value );
+
+			if ( this._register[ attr._hash ] ) {
+				return this._register[ attr._hash ];
+			} else {
+				this._register[ attr._hash ] = attr;
+
+				return attr;
+			}
+		}
+	}
+
+	/**
+	 * Register of attributes in which all registered attributes are stored.
+	 *
+	 * @static
+	 * @private
+	 * @property {String} _hash
+	 */
+	Attribute._register = {};
+
+	return Attribute;
+} );

+ 36 - 0
packages/ckeditor5-engine/src/document/character.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/node' ], function( Node ) {
+	/**
+	 * Data structure for character stored in the tree data model.
+	 *
+	 * @class Character
+	 */
+	class Character extends Node {
+		/**
+		 * Creates character linear item.
+		 *
+		 * @param {String} character Described character.
+		 * @param {Iterable} attrs Iterable collection of {@link document.Attribute attributes}.
+		 * @constructor
+		 */
+		constructor( character, attrs ) {
+			super( attrs );
+
+			/**
+			 * Described character.
+			 *
+			 * @readonly
+			 * @property {String} character
+			 */
+			this.character = character;
+		}
+	}
+
+	return Character;
+} );

+ 139 - 0
packages/ckeditor5-engine/src/document/document.js

@@ -0,0 +1,139 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [
+	'document/element',
+	'document/rootelement',
+	'emittermixin',
+	'utils',
+	'ckeditorerror'
+], function( Element, RootElement, EmitterMixin, utils, CKEditorError ) {
+	var graveyardSymbol = Symbol( 'graveyard' );
+
+	/**
+	 * Document model.
+	 *
+	 * @class document.Document
+	 */
+	class Document {
+		/**
+		 * Creates an empty document instance.
+		 *
+		 * @constructor
+		 */
+		constructor() {
+			/**
+			 * List of roots that are owned and managed by this document.
+			 *
+			 * @readonly
+			 * @property {Map} roots
+			 */
+			this.roots = new Map();
+
+			// Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
+			this.createRoot( graveyardSymbol );
+
+			/**
+			 * Document version. It starts from `0` and every operation increases the version number. It is used to ensure that
+			 * operations are applied on the proper document version. If the {@link document.operation.Operation#baseVersion} will
+			 * not match document version the {@link document-applyOperation-wrong-version} error is thrown.
+			 *
+			 * @readonly
+			 * @property {Number} version
+			 */
+			this.version = 0;
+		}
+
+		/**
+		 * This is the only entry point for all document changes.
+		 *
+		 * @param {document.operation.Operation} operation Operation to be applied.
+		 */
+		applyOperation( operation ) {
+			if ( operation.baseVersion !== this.version ) {
+				/**
+				 * Only operations with matching versions can be applied.
+				 *
+				 * @error document-applyOperation-wrong-version
+				 * @param {document.operation.Operation} operation
+				 */
+				throw new CKEditorError(
+					'document-applyOperation-wrong-version: Only operations with matching versions can be applied.',
+					{ operation: operation } );
+			}
+
+			operation._execute();
+			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.',
+					{ name: name }
+				);
+			}
+
+			var root = new RootElement( this );
+			this.roots.set( name, root );
+
+			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 ) {
+			if ( !this.roots.has( name ) ) {
+				/**
+				 * Root with specified name does not exist.
+				 *
+				 * @error document-createRoot-root-not-exist
+				 * @param {String} name
+				 */
+				throw new CKEditorError(
+					'document-createRoot-root-not-exist: Root with specified name does not exist.',
+					{ name: name }
+				);
+			}
+
+			return this.roots.get( name );
+		}
+
+		/**
+		 * Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
+		 *
+		 * @protected
+		 * @readonly
+		 * @property {document.RootElement} _graveyard
+		 */
+		get _graveyard() {
+			return this.getRoot( graveyardSymbol );
+		}
+	}
+
+	utils.extend( Document.prototype, EmitterMixin );
+
+	return Document;
+} );

+ 118 - 0
packages/ckeditor5-engine/src/document/element.js

@@ -0,0 +1,118 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [ 'document/node', 'document/nodelist' ], function( Node, NodeList ) {
+	/**
+	 * Tree data model element.
+	 *
+	 * @class document.Element
+	 */
+	class Element extends Node {
+		/**
+		 * Creates a 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} 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 ) {
+			super( attrs );
+
+			/**
+			 * Element name.
+			 *
+			 * @readonly
+			 * @property {String} name
+			 */
+			this.name = name;
+
+			/**
+			 * List of children nodes.
+			 *
+			 * @protected
+			 * @property {document.NodeList} _children
+			 */
+			this._children = new NodeList();
+
+			if ( children ) {
+				this.insertChildren( 0, children );
+			}
+		}
+
+		/**
+		 * Inserts a list of child nodes on the given index and sets the parent of these nodes to this element.
+		 *
+		 * Note that the list of children can be modified only in elements not yet attached to the document.
+		 * All attached nodes should be modified using the {@link document.operation.InsertOperation}.
+		 *
+		 * @param {Number} index Position where nodes should be inserted.
+		 * @param {document.Node|document.Text|document.NodeList|String|Iterable} nodes The list of nodes to be inserted.
+		 * The 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 ) );
+
+			for ( var node of this._children ) {
+				node.parent = this;
+			}
+		}
+
+		/**
+		 * Removes number of child nodes starting at the given index and set the parent of these nodes to `null`.
+		 *
+		 * Note that the list of children can be modified only in elements not yet attached to the document.
+		 * All attached nodes should be modified using the {@link document.operation.RemoveOperation}.
+		 *
+		 * @param {Number} index Position of the first node to remove.
+		 * @param {Number} number Number of nodes to remove.
+		 * @returns {document.NodeList} The list of removed nodes.
+		 */
+
+		removeChildren( index, number ) {
+			for ( var i = index; i < index + number; i++ ) {
+				this._children.get( i ).parent = null;
+			}
+
+			return this._children.remove( index, number );
+		}
+
+		/**
+		 * Gets child at the given index.
+		 *
+		 * @param {Number} index Index of child.
+		 * @returns {document.Node} Child node.
+		 */
+		getChild( index ) {
+			return this._children.get( index );
+		}
+
+		/**
+		 * Gets index of the given child node.
+		 *
+		 * @param {document.Node} node Child node.
+		 * @returns {Number} Index of the child node.
+		 */
+		getChildIndex( node ) {
+			return this._children.indexOf( node );
+		}
+
+		/**
+		 * Gets the number of element's children.
+		 *
+		 * @returns {Number} The number of element's children.
+		 */
+		getChildCount() {
+			return this._children.length;
+		}
+	}
+
+	return Element;
+} );

+ 252 - 0
packages/ckeditor5-engine/src/document/node.js

@@ -0,0 +1,252 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [ 'document/attribute', 'utils', 'ckeditorerror' ], function( Attribute, utils, CKEditorError ) {
+	/**
+	 * Abstract document tree node class.
+	 *
+	 * @abstract
+	 * @class document.Node
+	 */
+	class Node {
+		/**
+		 * Creates a tree node.
+		 *
+		 * This is an abstract class, so this constructor should not be used directly.
+		 *
+		 * @param {Iterable} attrs Iterable collection of {@link document.Attribute attributes}.
+		 * @constructor
+		 */
+		constructor( attrs ) {
+			/**
+			 * Parent element. Null by default. Set by {@link document.Element#insertChildren}.
+			 *
+			 * @readonly
+			 * @property {document.Element|null} parent
+			 */
+			this.parent = null;
+
+			/**
+			 * Attributes set.
+			 *
+			 * Attributes of nodes attached to the document can be changed only be the {@link document.operation.ChangeOperation}.
+			 *
+			 * @private
+			 * @property {Set} _attrs
+			 */
+			this._attrs = new Set( attrs );
+		}
+
+		/**
+		 * Index of the node in the parent element or null if the node has no parent.
+		 *
+		 * Throws error if the parent element does not contain this node.
+		 *
+		 * @returns {Number|Null} Index of the node in the parent element or null if the node has not parent.
+		 */
+		getIndex() {
+			var pos;
+
+			if ( !this.parent ) {
+				return null;
+			}
+
+			// No parent or child doesn't exist in parent's children.
+			if ( ( pos = this.parent.getChildIndex( this ) ) == -1 ) {
+				/**
+				 * The node's parent does not contain this node. It means that the document tree is corrupted.
+				 *
+				 * @error node-not-found-in-parent
+				 */
+				throw new CKEditorError( 'node-not-found-in-parent: The node\'s parent does not contain this node.' );
+			}
+
+			return pos;
+		}
+
+		/**
+		 * Depth of the node, which equals to total number of its parents.
+		 *
+		 * @readonly
+		 * @property {Number} depth
+		 */
+		get depth() {
+			var depth = 0;
+			var parent = this.parent;
+
+			while ( parent ) {
+				depth++;
+
+				parent = parent.parent;
+			}
+
+			return depth;
+		}
+
+		/**
+		 * The top parent for the node. If node has no parent it is the root itself.
+		 *
+		 * @readonly
+		 * @property {Number} depth
+		 */
+		get root() {
+			var root = this; // jscs:ignore safeContextKeyword
+
+			while ( root.parent ) {
+				root = root.parent;
+			}
+
+			return root;
+		}
+
+		/**
+		 * Nodes next sibling or `null` if it is the last child.
+		 *
+		 * @readonly
+		 * @property {document.Node|null} nextSibling
+		 */
+		get nextSibling() {
+			var index = this.getIndex();
+
+			return ( index !== null && this.parent.getChild( index + 1 ) ) || null;
+		}
+
+		/**
+		 * Nodes previous sibling or null if it is the last child.
+		 *
+		 * @readonly
+		 * @property {document.Node|null} previousSibling
+		 */
+		get previousSibling() {
+			var index = this.getIndex();
+
+			return ( index !== null && this.parent.getChild( index - 1 ) ) || null;
+		}
+
+		/**
+		 * Returns `true` if the node contains an attribute with the same key and value as given or the same key if the
+		 * given parameter is a string.
+		 *
+		 * @param {document.Attribute|String} attr An attribute or a key to compare.
+		 * @returns {Boolean} True if node contains given attribute or an attribute with the given key.
+		 */
+		hasAttr( key ) {
+			var attr;
+
+			// Attribute.
+			if ( key instanceof Attribute ) {
+				for ( attr of this._attrs ) {
+					if ( attr.isEqual( key ) ) {
+						return true;
+					}
+				}
+			}
+			// Key.
+			else {
+				for ( attr of this._attrs ) {
+					if ( attr.key == key ) {
+						return true;
+					}
+				}
+			}
+
+			return false;
+		}
+
+		/**
+		 * Finds an attribute by a key.
+		 *
+		 * @param {String} attr The attribute key.
+		 * @returns {document.Attribute} The found attribute.
+		 */
+		getAttr( key ) {
+			for ( var attr of this._attrs ) {
+				if ( attr.key == key ) {
+					return attr.value;
+				}
+			}
+
+			return null;
+		}
+
+		/**
+		 * Removes attribute from the list of attributes.
+		 *
+		 * @param {String} key The attribute key.
+		 */
+		removeAttr( key ) {
+			for ( var attr of this._attrs ) {
+				if ( attr.key == key ) {
+					this._attrs.delete( attr );
+
+					return;
+				}
+			}
+		}
+
+		/**
+		 * Sets a given attribute. If the attribute with the same key already exists it will be removed.
+		 *
+		 * @param {document.Attribute} attr Attribute to set.
+		 */
+		setAttr( attr ) {
+			this.removeAttr( attr.key );
+
+			this._attrs.add( attr );
+		}
+
+		/**
+		 * Gets path to the node. For example if the node is the second child of the first child of the root then the path
+		 * will be `[ 1, 2 ]`. This path can be used as a parameter of {@link document.Position}.
+		 *
+		 * @returns {Number[]} The path.
+		 */
+		getPath() {
+			var path = [];
+			var node = this; // jscs:ignore safeContextKeyword
+
+			while ( node.parent ) {
+				path.unshift( node.getIndex() );
+				node = node.parent;
+			}
+
+			return path;
+		}
+
+		/**
+		 * Custom toJSON method to solve child-parent circular dependencies.
+		 *
+		 * @returns {Object} Clone of this object with the parent property replaced with its name.
+		 */
+		toJSON() {
+			var json = utils.clone( this );
+
+			// Due to circular references we need to remove parent reference.
+			json.parent = this.parent ? this.parent.name : null;
+
+			return json;
+		}
+
+		/**
+		 * Gets the number of attributes.
+		 *
+		 * @protected
+		 * @returns {Number} Number of attributes.
+		 */
+		_getAttrCount() {
+			var count = 0;
+
+			for ( var attr of this._attrs ) { // jshint ignore:line
+				count++;
+			}
+
+			return count;
+		}
+	}
+
+	return Node;
+} );

+ 156 - 0
packages/ckeditor5-engine/src/document/nodelist.js

@@ -0,0 +1,156 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [
+	'document/character',
+	'document/text',
+	'document/node',
+	'utils'
+], function( Character, Text, Node, utils ) {
+	/**
+	 * List of nodes. It is used to represent multiple nodes with a given order, for example children of
+	 * {@link document.Element} object or nodes inserted using {@link document.operation.InsertOperation}.
+	 *
+	 * Thanks to the constructor, which accepts various arguments, this class lets you easily create desired list of nodes.
+	 *
+	 * It also may internally compress nodes.
+	 *
+	 * @class document.NodeList
+	 */
+	class NodeList {
+		/**
+		 * Constructor let you create a list of nodes in many ways. See examples:
+		 *
+		 *		var nodeList = new NodeList( [ new Element( p1 ), new Element( p1 ) ] );
+		 *		nodeList.length; // 2
+		 *
+		 *		var nodeList = new NodeList( new Element( p ) );
+		 *		nodeList.length; // 1
+		 *
+		 *		var nodeList = new NodeList( [ 'foo', new Element( p ), 'bar' ] );
+		 *		nodeList.length; // 7
+		 *
+		 *		var nodeList = new NodeList( 'foo' );
+		 *		nodeList.length; // 3
+		 *
+		 *		var nodeList = new NodeList( new Text( 'foo', [ new Attribute( 'bar', 'bom' ) ] ) );
+		 *		nodeList.length; // 3
+		 *		nodeList.get( 0 ).getAttr( 'bar' ); // 'bom'
+		 *		nodeList.get( 1 ).getAttr( 'bar' ); // 'bom'
+		 *		nodeList.get( 2 ).getAttr( 'bar' ); // 'bom'
+		 *
+		 *		var nodeListA = new NodeList( 'foo' );
+		 *		var nodeListB = new NodeList( nodeListA );
+		 *		nodeListA === nodeListB // true
+		 *		nodeListB.length // 3
+		 *
+		 * @param {document.Node|document.Text|document.NodeList|String|Iterable} nodes List of nodes.
+		 * @constructor
+		 */
+		constructor( nodes ) {
+			if ( nodes instanceof NodeList ) {
+				// We do not clone anything.
+				return nodes;
+			}
+
+			/**
+			 * Internal array to store nodes.
+			 *
+			 * @private
+			 * @property {Array}
+			 */
+			this._nodes = [];
+
+			if ( nodes ) {
+				var node, character;
+
+				if ( !utils.isIterable( nodes ) ) {
+					nodes = [ nodes ];
+				}
+
+				for ( node of nodes ) {
+					// Node.
+					if ( node instanceof Node ) {
+						this._nodes.push( node );
+					}
+					// Text.
+					else if ( node instanceof Text ) {
+						for ( character of node.text ) {
+							this._nodes.push( new Character( character, node.attrs ) );
+						}
+					}
+					// String.
+					else {
+						for ( character of node ) {
+							this._nodes.push( new Character( character ) );
+						}
+					}
+				}
+			}
+		}
+
+		/**
+		 * Returns node at the given index.
+		 *
+		 * @param {Number} index Node index.
+		 * @returns {document.Node} Node at given index.
+		 */
+		get( index ) {
+			return this._nodes[ index ];
+		}
+
+		/**
+		 * Inserts nodes from the given node list into this node list at the given index.
+		 *
+		 * @param {Number} index Position where nodes should be inserted.
+		 * @param {document.NodeList} nodeList List of nodes to insert.
+		 */
+		insert( index, nodeList ) {
+			this._nodes.splice.apply( this._nodes, [ index, 0 ].concat( nodeList._nodes ) );
+		}
+
+		/**
+		 * Removes number of nodes starting at the given index.
+		 *
+		 * @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 ) {
+			return new NodeList( this._nodes.splice( index, number ) );
+		}
+
+		/**
+		 * Search for the node in the node list.
+		 *
+		 * @param {document.Node} node Node to find.
+		 * @returns {Number} Position of the node in the list.
+		 */
+		indexOf( node ) {
+			return this._nodes.indexOf( node );
+		}
+
+		/**
+		 * Number of nodes in the node list.
+		 *
+		 * @readonly
+		 * @property {Number} length
+		 */
+		get length() {
+			return this._nodes.length;
+		}
+
+		/**
+		 * Node list iterator.
+		 */
+		[ Symbol.iterator ]() {
+			return this._nodes[ Symbol.iterator ]();
+		}
+	}
+
+	return NodeList;
+} );

+ 130 - 0
packages/ckeditor5-engine/src/document/operation/changeoperation.js

@@ -0,0 +1,130 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [ 'document/operation/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.ChangeOperation
+	 */
+	class ChangeOperation extends Operation {
+		/**
+		 * Creates a change operation.
+		 *
+		 * If only the new attribute is set, then it will be inserted. Note that in all nodes in ranges there must be
+		 * no attributes with the same key as the new attribute.
+		 *
+		 * If only the old attribute is set, then it will be removed. Note that this attribute must be present in all nodes in
+		 * ranges.
+		 *
+		 * If both new and old attributes are set, then the operation will change the attribute value. Note 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 {document.Range} range Range on which the operation should be applied.
+		 * @param {document.Attribute|null} oldAttr Attribute to be removed. If `null`, then the operation just inserts a new attribute.
+		 * @param {document.Attribute|null} newAttr Attribute to be added. If `null`, then the operation just removes the attribute.
+		 * @param {Number} baseVersion {@link document.Document#version} on which the operation can be applied.
+		 * @constructor
+		 */
+		constructor( range, oldAttr, newAttr, baseVersion ) {
+			super( baseVersion );
+
+			/**
+			 * Range on which operation should be applied.
+			 *
+			 * @readonly
+			 * @type {document.Range}
+			 */
+			this.range = range;
+
+			/**
+			 * Old attribute to change. Set to `null` if operation inserts a new attribute.
+			 *
+			 * @readonly
+			 * @type {document.Attribute|null}
+			 */
+			this.oldAttr = oldAttr;
+
+			/**
+			 * New attribute. Set to `null` if operation removes the attribute.
+			 *
+			 * @readonly
+			 * @type {document.Attribute|null}
+			 */
+			this.newAttr = newAttr;
+		}
+
+		_execute() {
+			var oldAttr = this.oldAttr;
+			var newAttr = this.newAttr;
+			var value;
+
+			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.Attribute} oldAttr
+				 * @param {document.Attribute} newAttr
+				 */
+				throw new CKEditorError(
+					'operation-change-different-keys: Old and new attributes should have the same keys.',
+					{ oldAttr: oldAttr, newAttr: newAttr } );
+			}
+
+			// 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 the given node.
+						 *
+						 * @error operation-change-no-attr-to-remove
+						 * @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.',
+							{ node: value.node, attr: oldAttr } );
+					}
+
+					// There is no use in removing attribute if we will overwrite it later.
+					// Still it is profitable to run through the loop to check if all nodes in the range has old attribute.
+					if ( newAttr === null ) {
+						value.node.removeAttr( oldAttr.key );
+					}
+				}
+			}
+
+			// Insert or change.
+			if ( newAttr !== null ) {
+				for ( value of this.range ) {
+					if ( oldAttr === null && value.node.hasAttr( newAttr.key ) ) {
+						/**
+						 * The attribute with given key already exists for the given node.
+						 *
+						 * @error operation-change-attr-exists
+						 * @param {document.Node} node
+						 * @param {document.Attribute} attr
+						 */
+						throw new CKEditorError(
+							'operation-change-attr-exists: The attribute with given key already exists.',
+							{ node: value.node, attr: newAttr } );
+					}
+
+					value.node.setAttr( newAttr );
+				}
+			}
+		}
+
+		getReversed() {
+			return new ChangeOperation( this.range, this.newAttr, this.oldAttr, this.baseVersion + 1 );
+		}
+	}
+
+	return ChangeOperation;
+} );

+ 61 - 0
packages/ckeditor5-engine/src/document/operation/insertoperation.js

@@ -0,0 +1,61 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [
+	'document/operation/operation',
+	'document/nodelist',
+	'document/operation/removeoperation'
+], function( Operation, NodeList ) {
+	/**
+	 * Operation to insert list of nodes on the given position in the tree data model.
+	 *
+	 * @class document.operation.InsertOperation
+	 */
+	class InsertOperation extends Operation {
+		/**
+		 * Creates an insert operation.
+		 *
+		 * @param {document.Position} position Position of insertion.
+		 * @param {document.Node|document.Text|document.NodeList|String|Iterable} nodes The list of nodes to be inserted.
+		 * List of nodes can be any type accepted by the {@link document.NodeList} constructor.
+		 * @param {Number} baseVersion {@link document.Document#version} on which operation can be applied.
+		 * @constructor
+		 */
+		constructor( position, nodes, baseVersion ) {
+			super( baseVersion );
+
+			/**
+			 * Position of insertion.
+			 *
+			 * @readonly
+			 * @type {document.Position}
+			 */
+			this.position = position;
+
+			/**
+			 * List of nodes to insert.
+			 *
+			 * @readonly
+			 * @type {document.NodeList}
+			 */
+			this.nodeList = new NodeList( nodes );
+		}
+
+		_execute() {
+			this.position.parent.insertChildren( this.position.offset, this.nodeList );
+		}
+
+		getReversed() {
+			// Because of circular dependencies we need to re-require remove operation here.
+			var RemoveOperation = CKEDITOR.require( 'document/operation/removeoperation' );
+
+			return new RemoveOperation( this.position, this.nodeList.length, this.baseVersion + 1 );
+		}
+	}
+
+	return InsertOperation;
+} );

+ 128 - 0
packages/ckeditor5-engine/src/document/operation/moveoperation.js

@@ -0,0 +1,128 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [
+	'document/operation/operation',
+	'document/nodelist',
+	'ckeditorerror',
+	'utils'
+], function( Operation, NodeList, CKEditorError, utils ) {
+	/**
+	 * Operation to move list of subsequent nodes from one position in the document to another.
+	 *
+	 * @class document.operation.MoveOperation
+	 */
+	class MoveOperation extends Operation {
+		/**
+		 * Creates a move operation.
+		 *
+		 * @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, howMany, baseVersion ) {
+			super( baseVersion );
+
+			/**
+			 * Source move position.
+			 *
+			 * @type {document.Position}
+			 */
+			this.sourcePosition = sourcePosition;
+
+			/**
+			 * Target move position.
+			 *
+			 * @type {document.Position}
+			 */
+			this.targetPosition = targetPosition;
+
+			/**
+			 * How many nodes to move.
+			 *
+			 * @type {Number}
+			 */
+			this.howMany = howMany;
+		}
+
+		_execute() {
+			var sourceElement = this.sourcePosition.parent;
+			var targetElement = this.targetPosition.parent;
+			var sourceOffset = this.sourcePosition.offset;
+			var targetOffset = this.targetPosition.offset;
+
+			// 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
+				 */
+				throw new CKEditorError(
+					'operation-move-position-invalid: Source position or target position is invalid.'
+				);
+			} else if ( sourceOffset + this.howMany > sourceElement.getChildCount() ) {
+				/**
+				 * The nodes which should be moved do not exist.
+				 *
+				 * @error operation-move-nodes-do-not-exist
+				 */
+				throw new CKEditorError(
+					'operation-move-nodes-do-not-exist: The nodes which should be moved do not exist.'
+				);
+			} else if ( sourceElement === targetElement && sourceOffset <= targetOffset && targetOffset < sourceOffset + this.howMany ) {
+				/**
+				 * Trying to move a range of nodes into the middle of that range.
+				 *
+				 * @error operation-move-range-into-itself
+				 */
+				throw new CKEditorError(
+					'operation-move-range-into-itself: Trying to move a range of nodes to the inside of that range.'
+				);
+			} else {
+				var sourcePath = this.sourcePosition.getParentPath();
+				var targetPath = this.targetPosition.getParentPath();
+
+				if ( utils.compareArrays( sourcePath, targetPath ) == utils.compareArrays.PREFIX ) {
+					var i = sourcePath.length;
+
+					if ( this.targetPosition.path[ i ] >= sourceOffset && this.targetPosition.path[ i ] < sourceOffset + this.howMany ) {
+						/**
+						 * Trying to move a range of nodes into one of nodes from that range.
+						 *
+						 * @error operation-move-node-into-itself
+						 */
+						throw new CKEditorError(
+							'operation-move-node-into-itself: Trying to move a range of nodes into one of nodes from that range.'
+						);
+					}
+				}
+			}
+			// 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 -= this.howMany;
+			}
+
+			var removedNodes = sourceElement.removeChildren( sourceOffset, this.howMany );
+
+			targetElement.insertChildren( targetOffset, removedNodes );
+		}
+
+		getReversed() {
+			return new MoveOperation( this.targetPosition, this.sourcePosition, this.howMany, this.baseVersion + 1 );
+		}
+	}
+
+	return MoveOperation;
+} );

+ 79 - 0
packages/ckeditor5-engine/src/document/operation/operation.js

@@ -0,0 +1,79 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [], function() {
+	/**
+	 * Abstract base operation class.
+	 *
+	 * @abstract
+	 * @class document.operation.Operation
+	 */
+	class Operation {
+		/**
+		 * Base operation constructor.
+		 *
+		 * @param {Number} baseVersion {@link document.Document#version} on which the operation can be applied.
+		 * @constructor
+		 */
+		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 than the
+			 * {@link document.Document#version document version} the {@link document-applyOperation-wrong-version}
+			 * error is thrown.
+			 *
+			 * @property {Number}
+			 */
+			this.baseVersion = baseVersion;
+
+			/**
+			 * Executes the operation - modifications described by the operation attributes
+			 * will be applied to the tree model.
+			 *
+			 * @method _execute
+			 * @protected
+			 */
+
+			/**
+			 * Creates and returns a reverse operation. Reverse operation when executed right after
+			 * the original operation will bring back tree model state to the point before the original
+			 * operation execution. In other words, it reverses changes done by the original operation.
+			 *
+			 * Keep in mind that tree model state may change since executing the original operation,
+			 * so reverse operation will be "outdated". In that case you will need to
+			 * {@link #getTransformedBy transform} it by all operations that were executed after the original operation.
+			 *
+			 * @method getReversed
+			 * @returns {document.operation.Operation} Reversed operation.
+			 */
+
+			/**
+			 * Creates and returns a clone of this operation which is transformed by the given operation.
+			 * When operation is transformed its parameters may change accordingly to the operation which it is
+			 * transformed by. If the given operation applied to the tree model any modifications which are
+			 * affecting ranges/positions/nodes connected with this operation, those changes will be reflected
+			 * in the parameters of the returned operation.
+			 *
+			 * Whenever the {@link document.Document document} has different {@link document.Document#baseVersion}
+			 * than an operation you want to {@link document.Document#applyOperation apply}, you need to transform
+			 * all the operations that were executed on the {@link document.Document document} since it has
+			 * {@link document.Document#baseVersion} same as the operation (transform in the same order as those
+			 * operations were executed). This way all modifications done to the tree model will be reflected
+			 * in the operation parameters and the operation will "work" on "up-to-date" version of the tree model.
+			 *
+			 * **TODO**: So far we think that transforming one operation by another one may result in more than one
+			 * operation. This needs to be clarified in this documentation.
+			 *
+			 * @method getTransformedBy
+			 * @param {document.operation.Operation} operation Operation by which this operation will be transformed.
+			 * @returns {document.operation.Operation[]} A result of transformation of this operation by the given operation.
+			 */
+		}
+	}
+
+	return Operation;
+} );

+ 33 - 0
packages/ckeditor5-engine/src/document/operation/reinsertoperation.js

@@ -0,0 +1,33 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [
+	'document/operation/moveoperation',
+	'document/operation/removeoperation'
+], function( MoveOperation ) {
+	/**
+	 * Operation to reinsert previously removed nodes back to the non-graveyard root.
+	 * This is basically {@link document.operation.MoveOperation} but it returns
+	 * {@link document.operation.RemoveOperation} when reversed.
+	 *
+	 * With this class, 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.operation.ReinsertOperation
+	 */
+	class ReinsertOperation extends MoveOperation {
+		getReversed() {
+			// Because of circular dependencies we need to re-require reinsert operation here.
+			var RemoveOperation = CKEDITOR.require( 'document/operation/removeoperation' );
+
+			return new RemoveOperation( this.targetPosition, this.howMany, this.baseVersion + 1 );
+		}
+	}
+
+	return ReinsertOperation;
+} );

+ 45 - 0
packages/ckeditor5-engine/src/document/operation/removeoperation.js

@@ -0,0 +1,45 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [
+	'document/operation/moveoperation',
+	'document/position',
+	'document/operation/reinsertoperation'
+], function( MoveOperation, Position ) {
+	/**
+	 * Operation to remove a range of nodes.
+	 *
+	 * @class document.operation.RemoveOperation
+	 */
+	class RemoveOperation extends MoveOperation {
+		/**
+		 * Creates a remove operation.
+		 *
+		 * @param {document.Position} position Position before the first node to remove.
+		 * @param {Number} howMany How many nodes to remove.
+		 * @param {Number} baseVersion {@link document.Document#version} on which operation can be applied.
+		 * @constructor
+		 */
+		constructor( position, howMany, baseVersion ) {
+			var graveyard = position.root.document._graveyard;
+
+			// Position in a graveyard where nodes were moved.
+			var graveyardPosition = Position.createFromParentAndOffset( graveyard, 0 );
+
+			super( position, graveyardPosition, howMany, baseVersion );
+		}
+
+		getReversed() {
+			// Because of circular dependencies we need to re-require reinsert operation here.
+			var ReinsertOperation = CKEDITOR.require( 'document/operation/reinsertoperation' );
+
+			return new ReinsertOperation( this.targetPosition, this.sourcePosition, this.howMany, this.baseVersion + 1 );
+		}
+	}
+
+	return RemoveOperation;
+} );

+ 177 - 0
packages/ckeditor5-engine/src/document/position.js

@@ -0,0 +1,177 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [ 'utils', 'ckeditorerror' ], function( utils, CKEditorError ) {
+	/**
+	 * Position in the tree. Position is always located before or after a node.
+	 * See {@link #path} property for more information.
+	 *
+	 * @class document.Position
+	 */
+	class Position {
+		/**
+		 * Creates a position.
+		 *
+		 * @param {Array} path Position path. See {@link #path} property for more information.
+		 * @param {document.Element} root Root element for the path. Note that this element can not have a parent.
+		 * @constructor
+		 */
+		constructor( path, root ) {
+			/**
+			 * Position of the node it the tree. For example:
+			 *
+			 * root
+			 *  |- p         Before: [ 0 ]       After: [ 1 ]
+			 *  |- ul        Before: [ 1 ]       After: [ 2 ]
+			 *     |- li     Before: [ 1, 0 ]    After: [ 1, 1 ]
+			 *     |  |- f   Before: [ 1, 0, 0 ] After: [ 1, 0, 1 ]
+			 *     |  |- o   Before: [ 1, 0, 1 ] After: [ 1, 0, 2 ]
+			 *     |  |- o   Before: [ 1, 0, 2 ] After: [ 1, 0, 3 ]
+			 *     |- li     Before: [ 1, 1 ]    After: [ 1, 2 ]
+			 *        |- b   Before: [ 1, 1, 0 ] After: [ 1, 1, 1 ]
+			 *        |- a   Before: [ 1, 1, 1 ] After: [ 1, 1, 2 ]
+			 *        |- r   Before: [ 1, 1, 2 ] After: [ 1, 1, 3 ]
+			 *
+			 * @type {Number[]}
+			 */
+			this.path = path;
+
+			/**
+			 * Root element for the path. Note that this element can not have a parent.
+			 *
+			 * @type {document.Element}
+			 */
+			this.root = root;
+		}
+
+		/**
+		 * Parent element of the position. The position is located at {@link #offset} in this element.
+		 *
+		 * @readonly
+		 * @property {document.Element} parent
+		 */
+		get parent() {
+			var parent = this.root;
+
+			var i, len;
+
+			for ( i = 0, len = this.path.length - 1; i < len; i++ ) {
+				parent = parent.getChild( this.path[ i ] );
+			}
+
+			return parent;
+		}
+
+		/**
+		 * Offset at which the position is located in the {@link #parent}.
+		 *
+		 * @readonly
+		 * @property {Number} offset
+		 */
+		get offset() {
+			return utils.last( this.path );
+		}
+
+		/**
+		 * Node directly before the position.
+		 *
+		 * @readonly
+		 * @type {document.Node}
+		 */
+		get nodeBefore() {
+			return this.parent.getChild( this.offset - 1 ) || null;
+		}
+
+		/**
+		 * Node directly after the position.
+		 *
+		 * @readonly
+		 * @property {document.Node}
+		 */
+		get nodeAfter() {
+			return this.parent.getChild( this.offset ) || null;
+		}
+
+		/**
+		 * Two positions equal if paths are equal.
+		 *
+		 * @param {document.Position} otherPosition Position to compare.
+		 * @returns {Boolean} True if positions equal.
+		 */
+		isEqual( otherPosition ) {
+			return utils.isEqual( this.path, otherPosition.path );
+		}
+
+		/**
+		 * Returns the path to the parent, which is the {@link document.Position#path} without the last element.
+		 *
+		 * This method returns the parent path even if the parent does not exists.
+		 *
+		 * @returns {Number[]} Path to the parent.
+		 */
+		getParentPath() {
+			return this.path.slice( 0, -1 );
+		}
+
+		/**
+		 * Creates a new position from the parent element and the offset in that element.
+		 *
+		 * @param {document.Element} parent Position parent element.
+		 * @param {Number} offset Position offset.
+		 * @returns {document.Position}
+		 */
+		static createFromParentAndOffset( parent, offset ) {
+			var path = parent.getPath();
+
+			path.push( offset );
+
+			return new Position( path, parent.root );
+		}
+
+		/**
+		 * Creates a new position before the given node.
+		 *
+		 * @param {document.node} node Node the position should be directly before.
+		 * @returns {document.Position}
+		 */
+		static createBefore( node ) {
+			if ( !node.parent ) {
+				/**
+				 * You can not make position before root.
+				 *
+				 * @error position-before-root
+				 * @param {document.Node} root
+				 */
+				throw new CKEditorError( 'position-before-root: You can not make position before root.', { root: node } );
+			}
+
+			return Position.createFromParentAndOffset( node.parent, node.getIndex() );
+		}
+
+		/**
+		 * Creates a new position after given node.
+		 *
+		 * @param {document.Node} node Node the position should be directly after.
+		 * @returns {document.Position}
+		 */
+		static createAfter( node ) {
+			if ( !node.parent ) {
+				/**
+				 * You can not make position after root.
+				 *
+				 * @error position-after-root
+				 * @param {document.Node} root
+				 */
+				throw new CKEditorError( 'position-after-root: You can not make position after root.', { root: node } );
+			}
+
+			return Position.createFromParentAndOffset( node.parent, node.getIndex() + 1 );
+		}
+	}
+
+	return Position;
+} );

+ 175 - 0
packages/ckeditor5-engine/src/document/positioniterator.js

@@ -0,0 +1,175 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [
+	'document/character',
+	'document/element',
+	'document/position'
+], function( Character, Element, Position ) {
+	var ELEMENT_ENTER = 0;
+	var ELEMENT_LEAVE = 1;
+	var CHARACTER = 2;
+
+	/**
+	 * Position iterator class. It allows to iterate forward and backward over the tree document.
+	 *
+	 * @class document.PositionIterator
+	 */
+	class PositionIterator {
+		/**
+		 * Creates a range iterator.
+		 *
+		 * @param {document.Range} [boundaries] Range to define boundaries of the iterator.
+		 * @param {document.Position} [iteratorPosition] Starting position.
+		 * @constructor
+		 */
+		constructor( boundaries, iteratorPosition ) {
+			if ( boundaries instanceof Position ) {
+				this.position = boundaries;
+			} else {
+				this.boundaries =  boundaries;
+				this.position = iteratorPosition || boundaries.start;
+			}
+
+			/**
+			 * Iterator position.
+			 *
+			 * @property {document.Position} position
+			 */
+
+			/**
+			 * Iterator boundaries.
+			 *
+			 * When the {@link #next} method is called on the end boundary or the {@link #previous} method
+			 * on the start boundary, then `{ done: true }` is returned.
+			 *
+			 * If boundaries are not defined they are set before first and after last child of the root node.
+			 *
+			 * @property {document.Range} boundaries
+			 */
+		}
+
+		/**
+		 * Moves the {@link #position} to the next position and returns the enctountered value.
+		 *
+		 * @returns {Object} Value between the previous and the new {@link #position}.
+		 * @returns {Boolean} return.done True if iterator is done.
+		 * @returns {Object} return.value
+		 * @returns {Number} return.value.type Encountered value type, possible options: {@link PositionIterator#ELEMENT_ENTER},
+		 * {@link PositionIterator#ELEMENT_LEAVE} or {@link PositionIterator#CHARACTER}.
+		 * @returns {document.Node} return.value.node Encountered node.
+		 */
+		next() {
+			var position = this.position;
+			var parent = position.parent;
+
+			// We are at the end of the root.
+			if ( parent.parent === null && position.offset === parent.getChildCount() ) {
+				return { done: true };
+			}
+
+			if ( this.boundaries && position.isEqual( this.boundaries.end ) ) {
+				return { done: true };
+			}
+
+			var nodeAfter = position.nodeAfter;
+
+			if ( nodeAfter instanceof Element ) {
+				this.position = Position.createFromParentAndOffset( nodeAfter, 0 );
+
+				return formatReturnValue( ELEMENT_ENTER, nodeAfter );
+			} else if ( nodeAfter instanceof Character ) {
+				this.position = Position.createFromParentAndOffset( parent, position.offset + 1 );
+
+				return formatReturnValue( CHARACTER, nodeAfter );
+			} else {
+				this.position = Position.createFromParentAndOffset( parent.parent, parent.getIndex() + 1 );
+
+				return formatReturnValue( ELEMENT_LEAVE, this.position.nodeBefore );
+			}
+		}
+
+		/**
+		 * Moves the {@link #position} to the previous position and returns the encountered value.
+		 *
+		 * @returns {Object} Value between the previous and the new {@link #position}.
+		 * @returns {Boolean} return.done True if iterator is done.
+		 * @returns {Object} return.value
+		 * @returns {Number} return.value.type Encountered value type, possible options: {@link PositionIterator#ELEMENT_ENTER},
+		 * {@link PositionIterator#ELEMENT_LEAVE} or {@link PositionIterator#CHARACTER}.
+		 * @returns {document.Node} return.value.node Scanned node.
+		 */
+		previous() {
+			var position = this.position;
+			var parent = position.parent;
+
+			// We are at the beginning of the root.
+			if ( parent.parent === null && position.offset === 0 ) {
+				return { done: true };
+			}
+
+			if ( this.boundaries && position.isEqual( this.boundaries.start ) ) {
+				return { done: true };
+			}
+
+			var nodeBefore = position.nodeBefore;
+
+			if ( nodeBefore instanceof Element ) {
+				this.position = Position.createFromParentAndOffset( nodeBefore, nodeBefore.getChildCount() );
+
+				return formatReturnValue( ELEMENT_LEAVE, nodeBefore );
+			} else if ( nodeBefore instanceof Character ) {
+				this.position = Position.createFromParentAndOffset( parent, position.offset - 1 );
+
+				return formatReturnValue( CHARACTER, nodeBefore );
+			} else {
+				this.position = Position.createFromParentAndOffset( parent.parent, parent.getIndex() );
+
+				return formatReturnValue( ELEMENT_ENTER, this.position.nodeAfter );
+			}
+		}
+	}
+
+	function formatReturnValue( type, node ) {
+		return {
+			done: false,
+			value: {
+				type: type,
+				node: node
+			}
+		};
+	}
+
+	/**
+	 * Flag for entering element.
+	 *
+	 * @static
+	 * @readonly
+	 * @property {Number}
+	 */
+	PositionIterator.ELEMENT_ENTER = ELEMENT_ENTER;
+
+	/**
+	 * Flag for leaving element.
+	 *
+	 * @static
+	 * @readonly
+	 * @property {Number}
+	 */
+	PositionIterator.ELEMENT_LEAVE = ELEMENT_LEAVE;
+
+	/**
+	 * Flag for character.
+	 *
+	 * @static
+	 * @readonly
+	 * @property {Number}
+	 */
+	PositionIterator.CHARACTER = CHARACTER;
+
+	return PositionIterator;
+} );

+ 59 - 0
packages/ckeditor5-engine/src/document/range.js

@@ -0,0 +1,59 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [ 'document/positioniterator' ], function( PositionIterator ) {
+	/**
+	 * Range class. Range is iterable.
+	 *
+	 * @class document.Range
+	 */
+	class Range {
+		/**
+		 * Creates a range.
+		 *
+		 * @param {document.Position} start Start position.
+		 * @param {document.Position} end End position.
+		 * @constructor
+		 */
+		constructor( start, end ) {
+			/**
+			 * Start position.
+			 *
+			 * @property {document.Position}
+			 */
+			this.start = start;
+
+			/**
+			 * End position.
+			 *
+			 * @property {document.Position}
+			 */
+			this.end = end;
+		}
+
+		/**
+		 * Two ranges equal if their start and end positions 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 );
+		}
+
+		/**
+		 * Range iterator.
+		 *
+		 * @see document.PositionIterator
+		 */
+		[ Symbol.iterator ]() {
+			return new PositionIterator( this );
+		}
+	}
+
+	return Range;
+} );

+ 35 - 0
packages/ckeditor5-engine/src/document/rootelement.js

@@ -0,0 +1,35 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [ 'document/element' ], function( Element ) {
+	/**
+	 * 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} doc {@link document.Document} that is an owner of the root.
+		 * @constructor
+		 */
+		constructor( doc ) {
+			super( 'root' );
+
+			/**
+			 * {@link document.Document} that is an owner of this root.
+			 *
+			 * @readonly
+			 * @property {document.Document}
+			 */
+			this.document = doc;
+		}
+	}
+
+	return RootElement;
+} );

+ 42 - 0
packages/ckeditor5-engine/src/document/text.js

@@ -0,0 +1,42 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [], function() {
+	/**
+	 * Data structure for text with attributes. Note that the `Text` is not a {@link document.Node},
+	 * because it will never be part of the document tree. {@link document.Character is a node}.
+	 *
+	 * @class document.Text
+	 */
+	class Text {
+		/**
+		 * Creates a text with attributes.
+		 *
+		 * @param {String} text Described text.
+		 * @param {Iterable} attrs Iterable collection of {@link document.Attribute attributes}.
+		 * @constructor
+		 */
+		constructor( text, attrs ) {
+			/**
+			 * Text.
+			 *
+			 * @readonly
+			 * @property {String}
+			 */
+			this.text = text;
+
+			/**
+			 * Iterable collection of {@link document.Attribute attributes}.
+			 *
+			 * @property {Iterable}
+			 */
+			this.attrs = attrs;
+		}
+	}
+
+	return Text;
+} );

文件差异内容过多而无法显示
+ 1057 - 678
packages/ckeditor5-engine/src/lib/lodash/lodash-ckeditor.js


+ 25 - 1
packages/ckeditor5-engine/src/utils-lodash.js

@@ -47,7 +47,31 @@
 		 * @member utils
 		 * @member utils
 		 * @method isObject
 		 * @method isObject
 		 */
 		 */
-		'isObject'
+		'isObject',
+
+		/**
+		 * See Lo-Dash: https://lodash.com/docs#isArray
+		 *
+		 * @member utils
+		 * @method isArray
+		 */
+		'isArray',
+
+		/**
+		 * See Lo-Dash: https://lodash.com/docs#last
+		 *
+		 * @member utils
+		 * @method last
+		 */
+		'last',
+
+		/**
+		 * See Lo-Dash: https://lodash.com/docs#isEqual
+		 *
+		 * @member utils
+		 * @method isEqual
+		 */
+		'isEqual'
 	];
 	];
 
 
 	// Make this compatible with CommonJS as well so it can be used in Node (e.g. "grunt lodash").
 	// Make this compatible with CommonJS as well so it can be used in Node (e.g. "grunt lodash").

+ 74 - 1
packages/ckeditor5-engine/src/utils.js

@@ -43,9 +43,82 @@ CKEDITOR.define( [ 'utils-lodash', 'lib/lodash/lodash-ckeditor' ], function( lod
 			return function() {
 			return function() {
 				return next++;
 				return next++;
 			};
 			};
-		} )()
+		} )(),
+
+		/**
+		 * Checks if value implements iterator interface.
+		 *
+		 * @param {Mixed} value The value to check.
+		 * @returns {Boolean} True if value implements iterator interface.
+		 */
+		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, 2, 1 ] ); // PREFIX
+		 *   compareArrays( [ 0, 2 ], [ 0 ] ); // EXTENSION
+		 *   compareArrays( [ 0, 2 ], [ 1, 2 ] ); // 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),
+		 * `a` is a {@link utils.compareArrays#EXTENSION extension}, 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 utils.compareArrays.DIFFERENT;
+				}
+			}
+
+			// Both arrays were same at all points.
+			if ( a.length == b.length ) {
+				// If their length is also same, they are the same.
+				return utils.compareArrays.SAME;
+			} else if ( a.length < b.length ) {
+				// Compared array is shorter so it is a prefix of the other array.
+				return utils.compareArrays.PREFIX;
+			} else {
+				// Compared array is longer so it is a suffix of the other array.
+				return utils.compareArrays.EXTENSION;
+			}
+		}
 	};
 	};
 
 
+	/**
+	 * 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 a suffix of" relation between arrays.
+	 *
+	 * @type {number}
+	 */
+	utils.compareArrays.EXTENSION = 2;
+	/**
+	 * Flag for "is different than" relation between arrays.
+	 *
+	 * @type {Number}
+	 */
+	utils.compareArrays.DIFFERENT = 3;
+
 	// Extend "utils" with Lo-Dash methods.
 	// Extend "utils" with Lo-Dash methods.
 	for ( var i = 0; i < lodashIncludes.length; i++ ) {
 	for ( var i = 0; i < lodashIncludes.length; i++ ) {
 		utils[ lodashIncludes[ i ] ] = lodash[ lodashIncludes[ i ] ];
 		utils[ lodashIncludes[ i ] ] = lodash[ lodashIncludes[ i ] ];

+ 81 - 0
packages/ckeditor5-engine/tests/document/attribute.js

@@ -0,0 +1,81 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document */
+
+'use strict';
+
+var modules = bender.amd.require( 'document/attribute' );
+
+describe( 'Attribute', function() {
+	var Attribute;
+
+	before( function() {
+		Attribute = modules[ 'document/attribute' ];
+	} );
+
+	beforeEach( function() {
+		Attribute._register = {};
+	} );
+
+	describe( 'constructor', function() {
+		it( 'should create attribute', function() {
+			var attr = new Attribute( 'foo', 'bar' );
+
+			expect( attr ).to.have.property( 'key' ).that.equals( 'foo' );
+			expect( attr ).to.have.property( 'value' ).that.equals( 'bar' );
+		} );
+
+		it( 'should create equal instance even if object has different order', function() {
+			var attr1 = new Attribute( 'foo', { a: 1, b: 2 } );
+			var attr2 = new Attribute( 'foo', { b: 2, a: 1 } );
+
+			expect( attr1.isEqual( attr2 ) ).to.be.true;
+		} );
+
+		it( 'should return the same object for registered objects', function() {
+			Attribute.register( 'register', true );
+
+			var attr1 = new Attribute( 'register', true );
+			var attr2 = new Attribute( 'register', true );
+
+			expect( attr1 ).to.equal( attr2 );
+			expect( attr1.isEqual( attr2 ) ).to.be.true;
+		} );
+
+		it( 'should return different objects for different values', function() {
+			Attribute.register( 'register', true );
+
+			var attr1 = new Attribute( 'register', true );
+			var attr2 = new Attribute( 'register', false );
+
+			expect( attr1 ).to.not.be.equals( attr2 );
+			expect( attr1.isEqual( attr2 ) ).to.not.be.true;
+		} );
+
+		it( 'should return different objects for not registered objects', function() {
+			Attribute.register( 'register', true );
+
+			var attr1 = new Attribute( 'register', false );
+			var attr2 = new Attribute( 'register', false );
+
+			expect( attr1 ).to.not.be.equals( attr2 );
+			expect( attr1.isEqual( attr2 ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'register', function() {
+		it( 'Attribute.register should return registered attribute', function() {
+			var attr1 = new Attribute( 'register', true );
+			var attr2 = Attribute.register( 'register', true );
+			var attr3 = Attribute.register( 'register', true );
+			var attr4 = new Attribute( 'register', true );
+
+			expect( attr1 ).to.not.be.equals( attr2 );
+			expect( attr2 ).to.equal( attr3 );
+			expect( attr3 ).to.equal( attr4 );
+		} );
+	} );
+} );

+ 52 - 0
packages/ckeditor5-engine/tests/document/character.js

@@ -0,0 +1,52 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* jshint expr: true */
+
+/* bender-tags: document */
+
+'use strict';
+
+var modules = bender.amd.require(
+	'document/character',
+	'document/node',
+	'document/element',
+	'document/attribute'
+);
+
+describe( 'Character', function() {
+	var Element, Character, Node, Attribute;
+
+	before( function() {
+		Element = modules[ 'document/element' ];
+		Character = modules[ 'document/character' ];
+		Node = modules[ 'document/node' ];
+		Attribute = modules[ 'document/attribute' ];
+	} );
+
+	describe( 'constructor', function() {
+		it( 'should create character without attributes', function() {
+			var character = new Character( 'f' );
+			var parent = new Element( 'parent', [], character );
+
+			expect( character ).to.be.an.instanceof( Node );
+			expect( character ).to.have.property( 'character' ).that.equals( 'f' );
+			expect( character ).to.have.property( 'parent' ).that.equals( parent );
+			expect( character._getAttrCount() ).to.equal( 0 );
+		} );
+
+		it( 'should create character with attributes', function() {
+			var attr = new Attribute( 'foo', 'bar' );
+			var character = new Character( 'f', [ attr ] );
+			var parent = new Element( 'parent', [], character );
+
+			expect( character ).to.be.an.instanceof( Node );
+			expect( character ).to.have.property( 'character' ).that.equals( 'f' );
+			expect( character ).to.have.property( 'parent' ).that.equals( parent );
+			expect( character._getAttrCount() ).to.equal( 1 );
+			expect( character.getAttr( attr.key ) ).to.equal( attr.value );
+		} );
+	} );
+} );

+ 109 - 0
packages/ckeditor5-engine/tests/document/document.js

@@ -0,0 +1,109 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document */
+
+'use strict';
+
+var modules = bender.amd.require(
+	'document/document',
+	'document/rootelement',
+	'ckeditorerror'
+);
+
+describe( 'Document', function() {
+	var Document, RootElement, CKEditorError;
+
+	before( function() {
+		Document = modules[ 'document/document' ];
+		RootElement = modules[ 'document/rootelement' ];
+		CKEditorError = modules.ckeditorerror;
+	} );
+
+	var document;
+
+	beforeEach( function() {
+		document = new Document();
+	} );
+
+	describe( 'constructor', function() {
+		it( 'should create Document with no data', function() {
+			expect( document ).to.have.property( 'roots' ).that.is.instanceof( Map );
+			expect( document.roots.size ).to.equal( 1 );
+			expect( document._graveyard ).to.be.instanceof( RootElement );
+			expect( document._graveyard.getChildCount() ).to.equal( 0 );
+		} );
+	} );
+
+	describe( 'createRoot', function() {
+		it( 'should create a new RootElement, add it to roots map and return it', function() {
+			var root = document.createRoot( 'root' );
+
+			expect( document.roots.size ).to.equal( 2 );
+			expect( root ).to.be.instanceof( RootElement );
+			expect( root.getChildCount() ).to.equal( 0 );
+		} );
+
+		it( 'should throw an error when trying to create a second root with the same name', function() {
+			document.createRoot( 'root' );
+
+			expect(
+				function() {
+					document.createRoot( 'root' );
+				}
+			).to.throw( CKEditorError, /document-createRoot-name-exists/ );
+		} );
+	} );
+
+	describe( 'getRoot', function() {
+		it( 'should return a RootElement previously created with given name', function() {
+			var newRoot = document.createRoot( 'root' );
+			var getRoot = document.getRoot( 'root' );
+
+			expect( getRoot ).to.equal( newRoot );
+		} );
+
+		it( 'should throw an error when trying to get non-existent root', function() {
+			expect(
+				function() {
+					document.getRoot( 'root' );
+				}
+			).to.throw( CKEditorError, /document-createRoot-root-not-exist/ );
+		} );
+	} );
+
+	describe( 'applyOperation', function() {
+		it( 'should increase document version, execute operation and fire operationApplied', function() {
+			var operationApplied = sinon.spy();
+			var operation = {
+				baseVersion: 0,
+				_execute: sinon.spy()
+			};
+
+			document.on( 'operationApplied', operationApplied );
+
+			document.applyOperation( operation );
+
+			expect( document.version ).to.equal( 1 );
+			sinon.assert.calledOnce( operationApplied );
+			sinon.assert.calledOnce( operation._execute );
+		} );
+
+		it( 'should throw an error on the operation base version and the document version is different', function() {
+			var operationApplied = sinon.spy();
+			var operation = {
+				baseVersion: 1
+			};
+
+			document.on( 'operationApplied', operationApplied );
+
+			expect(
+				function() {
+					document.applyOperation( operation );
+				}
+			).to.throw( CKEditorError, /document-applyOperation-wrong-version/ );
+		} );
+	} );
+} );

+ 124 - 0
packages/ckeditor5-engine/tests/document/element.js

@@ -0,0 +1,124 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* jshint expr: true */
+
+/* bender-tags: document */
+
+'use strict';
+
+var modules = bender.amd.require(
+	'document/node',
+	'document/nodelist',
+	'document/element',
+	'document/attribute'
+);
+
+describe( 'Element', function() {
+	var Element, Node, NodeList, Attribute;
+
+	before( function() {
+		Element = modules[ 'document/element' ];
+		Node = modules[ 'document/node' ];
+		NodeList = modules[ 'document/nodelist' ];
+		Attribute = modules[ 'document/attribute' ];
+	} );
+
+	describe( 'constructor', function() {
+		it( 'should create element without attributes', function() {
+			var element = new Element( 'elem' );
+			var parent = new Element( 'parent', [], [ element ] );
+
+			expect( element ).to.be.an.instanceof( Node );
+			expect( element ).to.have.property( 'name' ).that.equals( 'elem' );
+			expect( element ).to.have.property( 'parent' ).that.equals( parent );
+			expect( element._getAttrCount() ).to.equal( 0 );
+		} );
+
+		it( 'should create element with attributes', function() {
+			var attr = new Attribute( 'foo', 'bar' );
+
+			var element = new Element( 'elem', [ attr ] );
+			var parent = new Element( 'parent', [], [ element ] );
+
+			expect( element ).to.be.an.instanceof( Node );
+			expect( element ).to.have.property( 'name' ).that.equals( 'elem' );
+			expect( element ).to.have.property( 'parent' ).that.equals( parent );
+			expect( element._getAttrCount() ).to.equal( 1 );
+			expect( element.getAttr( attr.key ) ).to.equal( attr.value );
+		} );
+
+		it( 'should create element with children', function() {
+			var element = new Element( 'elem', [], 'foo' );
+
+			expect( element ).to.have.property( 'name' ).that.equals( 'elem' );
+			expect( element.getChildCount() ).to.equal( 3 );
+			expect( element.getChild( 0 ) ).to.have.property( 'character' ).that.equals( 'f' );
+			expect( element.getChild( 1 ) ).to.have.property( 'character' ).that.equals( 'o' );
+			expect( element.getChild( 2 ) ).to.have.property( 'character' ).that.equals( 'o' );
+		} );
+	} );
+
+	describe( 'insertChildren', function() {
+		it( 'should add children to the element', function() {
+			var element = new Element( 'elem', [], [ 'xy' ] );
+			element.insertChildren( 1, 'foo' );
+
+			expect( element ).to.have.property( 'name' ).that.equals( 'elem' );
+			expect( element.getChildCount() ).to.equal( 5 );
+			expect( element.getChild( 0 ) ).to.have.property( 'character' ).that.equals( 'x' );
+			expect( element.getChild( 1 ) ).to.have.property( 'character' ).that.equals( 'f' );
+			expect( element.getChild( 2 ) ).to.have.property( 'character' ).that.equals( 'o' );
+			expect( element.getChild( 3 ) ).to.have.property( 'character' ).that.equals( 'o' );
+			expect( element.getChild( 4 ) ).to.have.property( 'character' ).that.equals( 'y' );
+		} );
+	} );
+
+	describe( 'removeChildren', function() {
+		it( 'should remove children from the element and return them as a NodeList', function() {
+			var element = new Element( 'elem', [], [ 'foobar' ] );
+			var o = element.getChild( 2 );
+			var b = element.getChild( 3 );
+			var a = element.getChild( 4 );
+			var removed = element.removeChildren( 2, 3 );
+
+			expect( element.getChildCount() ).to.equal( 3 );
+			expect( element.getChild( 0 ) ).to.have.property( 'character' ).that.equals( 'f' );
+			expect( element.getChild( 1 ) ).to.have.property( 'character' ).that.equals( 'o' );
+			expect( element.getChild( 2 ) ).to.have.property( 'character' ).that.equals( 'r' );
+
+			expect( o ).to.have.property( 'parent' ).that.is.null;
+			expect( b ).to.have.property( 'parent' ).that.is.null;
+			expect( a ).to.have.property( 'parent' ).that.is.null;
+
+			expect( removed ).to.be.instanceof( NodeList );
+			expect( removed.length ).to.equal( 3 );
+			expect( removed.get( 0 ) ).to.equal( o );
+			expect( removed.get( 1 ) ).to.equal( b );
+			expect( removed.get( 2 ) ).to.equal( a );
+		} );
+	} );
+
+	describe( 'getChildIndex', function() {
+		it( 'should return child index', function() {
+			var element = new Element( 'elem', [], [ 'bar' ] );
+			var b = element.getChild( 0 );
+			var a = element.getChild( 1 );
+			var r = element.getChild( 2 );
+
+			expect( element.getChildIndex( b ) ).to.equal( 0 );
+			expect( element.getChildIndex( a ) ).to.equal( 1 );
+			expect( element.getChildIndex( r ) ).to.equal( 2 );
+		} );
+	} );
+
+	describe( 'getChildCount', function() {
+		it( 'should return number of children', function() {
+			var element = new Element( 'elem', [], [ 'bar' ] );
+
+			expect( element.getChildCount() ).to.equal( 3 );
+		} );
+	} );
+} );

+ 253 - 0
packages/ckeditor5-engine/tests/document/node.js

@@ -0,0 +1,253 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document */
+
+'use strict';
+
+var modules = bender.amd.require(
+	'document/element',
+	'document/character',
+	'document/attribute',
+	'document/nodelist',
+	'ckeditorerror'
+);
+
+describe( 'Node', function() {
+	var Element, Character, Attribute, NodeList, CKEditorError;
+
+	var root;
+	var one, two, three;
+	var charB, charA, charR, img;
+
+	before( function() {
+		Element = modules[ 'document/element' ];
+		Character = modules[ 'document/character' ];
+		Attribute = modules[ 'document/attribute' ];
+		NodeList = modules[ 'document/nodelist' ];
+		CKEditorError = modules.ckeditorerror;
+
+		charB = new Character( 'b' );
+		charA = new Character( 'a' );
+		img = new Element( 'img' );
+		charR = new Character( 'r' );
+
+		one = new Element( 'one' );
+		two = new Element( 'two', null, [ charB, charA, img, charR ] );
+		three = new Element( 'three' );
+
+		root = new Element( null, null, [ one, two, three ] );
+	} );
+
+	describe( 'should have a correct property', function() {
+		it( 'depth', function() {
+			expect( root ).to.have.property( 'depth' ).that.equals( 0 );
+
+			expect( one ).to.have.property( 'depth' ).that.equals( 1 );
+			expect( two ).to.have.property( 'depth' ).that.equals( 1 );
+			expect( three ).to.have.property( 'depth' ).that.equals( 1 );
+
+			expect( charB ).to.have.property( 'depth' ).that.equals( 2 );
+			expect( charA ).to.have.property( 'depth' ).that.equals( 2 );
+			expect( img ).to.have.property( 'depth' ).that.equals( 2 );
+			expect( charR ).to.have.property( 'depth' ).that.equals( 2 );
+		} );
+
+		it( 'root', function() {
+			expect( root ).to.have.property( 'root' ).that.equals( root );
+
+			expect( one ).to.have.property( 'root' ).that.equals( root );
+			expect( two ).to.have.property( 'root' ).that.equals( root );
+			expect( three ).to.have.property( 'root' ).that.equals( root );
+
+			expect( charB ).to.have.property( 'root' ).that.equals( root );
+			expect( charA ).to.have.property( 'root' ).that.equals( root );
+			expect( img ).to.have.property( 'root' ).that.equals( root );
+			expect( charR ).to.have.property( 'root' ).that.equals( root );
+		} );
+
+		it( 'nextSibling', function() {
+			expect( root ).to.have.property( 'nextSibling' ).that.is.null;
+
+			expect( one ).to.have.property( 'nextSibling' ).that.equals( two );
+			expect( two ).to.have.property( 'nextSibling' ).that.equals( three );
+			expect( three ).to.have.property( 'nextSibling' ).that.is.null;
+
+			expect( charB ).to.have.property( 'nextSibling' ).that.equals( charA );
+			expect( charA ).to.have.property( 'nextSibling' ).that.equals( img );
+			expect( img ).to.have.property( 'nextSibling' ).that.equals( charR );
+			expect( charR ).to.have.property( 'nextSibling' ).that.is.null;
+		} );
+
+		it( 'previousSibling', function() {
+			expect( root ).to.have.property( 'previousSibling' ).that.is.expect;
+
+			expect( one ).to.have.property( 'previousSibling' ).that.is.null;
+			expect( two ).to.have.property( 'previousSibling' ).that.equals( one );
+			expect( three ).to.have.property( 'previousSibling' ).that.equals( two );
+
+			expect( charB ).to.have.property( 'previousSibling' ).that.is.null;
+			expect( charA ).to.have.property( 'previousSibling' ).that.equals( charB );
+			expect( img ).to.have.property( 'previousSibling' ).that.equals( charA );
+			expect( charR ).to.have.property( 'previousSibling' ).that.equals( img );
+		} );
+	} );
+
+	describe( 'constructor', function() {
+		it( 'should copy attributes, not pass by reference', function() {
+			var attrs = [ new Attribute( 'attr', true ) ];
+			var foo = new Element( 'foo', attrs );
+			var bar = new Element( 'bar', attrs );
+
+			foo.removeAttr( 'attr' );
+
+			expect( foo._getAttrCount() ).to.equal( 0 );
+			expect( bar._getAttrCount() ).to.equal( 1 );
+		} );
+	} );
+
+	describe( 'getAttr', function() {
+		var fooAttr, element;
+
+		beforeEach( function() {
+			fooAttr = new Attribute( 'foo', true );
+			element = new Element( 'foo', [ fooAttr ] );
+		} );
+
+		it( 'should be possible to get attribute by key', function() {
+			expect( element.getAttr( 'foo' ) ).to.equal( fooAttr.value );
+		} );
+
+		it( 'should return null if attribute was not found by key', function() {
+			expect( element.getAttr( 'bar' ) ).to.be.null;
+		} );
+	} );
+
+	describe( 'setAttr', function() {
+		it( 'should insert an attribute', function() {
+			var element = new Element( 'elem' );
+			var attr = new Attribute( 'foo', 'bar' );
+
+			element.setAttr( attr );
+
+			expect( element._getAttrCount() ).to.equal( 1 );
+			expect( element.getAttr( attr.key ) ).to.equal( attr.value );
+		} );
+
+		it( 'should overwrite attribute with the same key', function() {
+			var oldAttr = new Attribute( 'foo', 'bar' );
+			var newAttr = new Attribute( 'foo', 'bar' );
+			var element = new Element( 'elem', [ oldAttr ] );
+
+			element.setAttr( newAttr );
+
+			expect( element._getAttrCount() ).to.equal( 1 );
+			expect( element.getAttr( newAttr.key ) ).to.equal( newAttr.value );
+		} );
+	} );
+
+	describe( 'removeAttr', function() {
+		it( 'should remove an attribute', function() {
+			var attrA = new Attribute( 'a', 'A' );
+			var attrB = new Attribute( 'b', 'b' );
+			var attrC = new Attribute( 'c', 'C' );
+			var element = new Element( 'elem', [ attrA, attrB, attrC ] );
+
+			element.removeAttr( attrB.key );
+
+			expect( element._getAttrCount() ).to.equal( 2 );
+			expect( element.getAttr( attrA.key ) ).to.equal( attrA.value );
+			expect( element.getAttr( attrC.key ) ).to.equal( attrC.value );
+			expect( element.getAttr( attrB.key ) ).to.be.null;
+		} );
+	} );
+
+	describe( 'hasAttr', function() {
+		it( 'should check attribute by key', function() {
+			var fooAttr = new Attribute( 'foo', true );
+			var element = new Element( 'foo', [ fooAttr ] );
+
+			expect( element.hasAttr( 'foo' ) ).to.be.true;
+		} );
+
+		it( 'should return false if attribute was not found by key', function() {
+			var fooAttr = new Attribute( 'foo', true );
+			var element = new Element( 'foo', [ fooAttr ] );
+
+			expect( element.hasAttr( 'bar' ) ).to.be.false;
+		} );
+
+		it( 'should check attribute by object', function() {
+			var fooAttr = new Attribute( 'foo', true );
+			var foo2Attr = new Attribute( 'foo', true );
+			var element = new Element( 'foo', [ fooAttr ] );
+
+			expect( element.hasAttr( foo2Attr ) ).to.be.true;
+		} );
+
+		it( 'should return false if attribute was not found by object', function() {
+			var fooAttr = new Attribute( 'foo', true );
+			var element = new Element( 'foo' );
+
+			expect( element.hasAttr( fooAttr ) ).to.be.false;
+		} );
+
+		it( 'should create proper JSON string using toJSON method', function() {
+			var b = new Character( 'b' );
+			var foo = new Element( 'foo', [], [ b ] );
+
+			var parsedFoo = JSON.parse( JSON.stringify( foo ) );
+			var parsedBar = JSON.parse( JSON.stringify( b ) );
+
+			expect( parsedFoo.parent ).to.equal( null );
+			expect( parsedBar.parent ).to.equal( 'foo' );
+		} );
+	} );
+
+	describe( 'getIndex', function() {
+		it( 'should return null if the parent is null', function() {
+			expect( root.getIndex() ).to.be.null;
+		} );
+
+		it( 'should return index in the parent', function() {
+			expect( one.getIndex() ).to.equal( 0 );
+			expect( two.getIndex() ).to.equal( 1 );
+			expect( three.getIndex() ).to.equal( 2 );
+
+			expect( charB.getIndex() ).to.equal( 0 );
+			expect( charA.getIndex() ).to.equal( 1 );
+			expect( img.getIndex() ).to.equal( 2 );
+			expect( charR.getIndex() ).to.equal( 3 );
+		} );
+
+		it( 'should throw an error if parent does not contains element', function() {
+			var f = new Character( 'f' );
+			var bar = new Element( 'bar', [], [] );
+
+			f.parent = bar;
+
+			expect(
+				function() {
+					f.getIndex();
+				}
+			).to.throw( CKEditorError, /node-not-found-in-parent/ );
+		} );
+	} );
+
+	describe( 'getPath', function() {
+		it( 'should return proper path', function() {
+			expect( root.getPath() ).to.deep.equal( [] );
+
+			expect( one.getPath() ).to.deep.equal( [ 0 ] );
+			expect( two.getPath() ).to.deep.equal( [ 1 ] );
+			expect( three.getPath() ).to.deep.equal( [ 2 ] );
+
+			expect( charB.getPath() ).to.deep.equal( [ 1, 0 ] );
+			expect( charA.getPath() ).to.deep.equal( [ 1, 1 ] );
+			expect( img.getPath() ).to.deep.equal( [ 1, 2 ] );
+			expect( charR.getPath() ).to.deep.equal( [ 1, 3 ] );
+		} );
+	} );
+} );

+ 115 - 0
packages/ckeditor5-engine/tests/document/nodelist.js

@@ -0,0 +1,115 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document */
+
+'use strict';
+
+var modules = bender.amd.require(
+	'document/nodelist',
+	'document/character',
+	'document/text',
+	'document/attribute'
+);
+
+describe( 'NodeList', function() {
+	var NodeList, Character, Text, Attribute;
+
+	before( function() {
+		NodeList = modules[ 'document/nodelist' ];
+		Character = modules[ 'document/character' ];
+		Text = modules[ 'document/text' ];
+		Attribute = modules[ 'document/attribute' ];
+	} );
+
+	describe( 'constructor', function() {
+		it( 'should change array of strings into a set of nodes', function() {
+			var nodeList = new NodeList( [ 'foo', new Character( 'x' ), 'bar' ] );
+
+			expect( nodeList.length ).to.equal( 7 );
+			expect( nodeList.get( 0 ).character ).to.equal( 'f' );
+			expect( nodeList.get( 1 ).character ).to.equal( 'o' );
+			expect( nodeList.get( 2 ).character ).to.equal( 'o' );
+			expect( nodeList.get( 3 ).character ).to.equal( 'x' );
+			expect( nodeList.get( 4 ).character ).to.equal( 'b' );
+			expect( nodeList.get( 5 ).character ).to.equal( 'a' );
+			expect( nodeList.get( 6 ).character ).to.equal( 'r' );
+		} );
+
+		it( 'should change string into a set of nodes', function() {
+			var nodeList = new NodeList( 'foo' );
+
+			expect( nodeList.length ).to.equal( 3 );
+			expect( nodeList.get( 0 ).character ).to.equal( 'f' );
+			expect( nodeList.get( 1 ).character ).to.equal( 'o' );
+			expect( nodeList.get( 2 ).character ).to.equal( 'o' );
+		} );
+
+		it( 'should change node into a set of nodes', function() {
+			var nodeList = new NodeList( new Character( 'x' ) );
+
+			expect( nodeList.length ).to.equal( 1 );
+			expect( nodeList.get( 0 ).character ).to.equal( 'x' );
+		} );
+
+		it( 'should change text with attribute into a set of nodes', function() {
+			var attr = new Attribute( 'bold', true );
+			var nodeList = new NodeList( new Text( 'foo', [ attr ] ) );
+
+			expect( nodeList.length ).to.equal( 3 );
+			expect( nodeList.get( 0 ).character ).to.equal( 'f' );
+			expect( nodeList.get( 0 ).getAttr( attr.key ) ).to.equal( attr.value );
+			expect( nodeList.get( 1 ).character ).to.equal( 'o' );
+			expect( nodeList.get( 1 ).getAttr( attr.key ) ).to.equal( attr.value );
+			expect( nodeList.get( 2 ).character ).to.equal( 'o' );
+			expect( nodeList.get( 2 ).getAttr( attr.key ) ).to.equal( attr.value );
+		} );
+	} );
+
+	describe( 'insert', function() {
+		it( 'should insert one nodelist into another', function() {
+			var outerList = new NodeList( 'foo' );
+			var innerList = new NodeList( 'xxx' );
+
+			outerList.insert( 2, innerList );
+
+			expect( outerList.length ).to.equal( 6 );
+			expect( outerList.get( 0 ).character ).to.equal( 'f' );
+			expect( outerList.get( 1 ).character ).to.equal( 'o' );
+			expect( outerList.get( 2 ).character ).to.equal( 'x' );
+			expect( outerList.get( 3 ).character ).to.equal( 'x' );
+			expect( outerList.get( 4 ).character ).to.equal( 'x' );
+			expect( outerList.get( 5 ).character ).to.equal( 'o' );
+		} );
+	} );
+
+	describe( 'remove', function() {
+		it( 'should remove part of the nodelist', function() {
+			var nodeList = new NodeList( 'foobar' );
+
+			nodeList.remove( 2, 3 );
+
+			expect( nodeList.length ).to.equal( 3 );
+			expect( nodeList.get( 0 ).character ).to.equal( 'f' );
+			expect( nodeList.get( 1 ).character ).to.equal( 'o' );
+			expect( nodeList.get( 2 ).character ).to.equal( 'r' );
+		} );
+	} );
+
+	describe( 'iterator', function() {
+		it( 'should iterate over all elements in the collection', function() {
+			var characters = 'foo';
+			var nodeList = new NodeList( characters );
+			var i = 0;
+
+			for ( var node of nodeList ) {
+				expect( node.character ).to.equal( characters[ i ] );
+				i++;
+			}
+
+			expect( i ).to.equal( 3 );
+		} );
+	} );
+} );

+ 315 - 0
packages/ckeditor5-engine/tests/document/operation/changeoperation.js

@@ -0,0 +1,315 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document */
+
+'use strict';
+
+var modules = bender.amd.require(
+	'document/document',
+	'document/operation/changeoperation',
+	'document/position',
+	'document/range',
+	'document/character',
+	'document/attribute',
+	'document/nodelist',
+	'document/text',
+	'ckeditorerror'
+);
+
+describe( 'ChangeOperation', function() {
+	var Document, ChangeOperation, Position, Range, Character, Attribute, NodeList, Text, CKEditorError;
+
+	before( function() {
+		Document = modules[ 'document/document' ];
+		ChangeOperation = modules[ 'document/operation/changeoperation' ];
+		Position = modules[ 'document/position' ];
+		Range = modules[ 'document/range' ];
+		Character = modules[ 'document/character' ];
+		Attribute = modules[ 'document/attribute' ];
+		NodeList = modules[ 'document/nodelist' ];
+		Text = modules[ 'document/text' ];
+		CKEditorError = modules.ckeditorerror;
+	} );
+
+	var doc, root;
+
+	beforeEach( function() {
+		doc = new Document();
+		root = doc.createRoot( 'root' );
+	} );
+
+	it( 'should insert attribute to the set of nodes', function() {
+		var newAttr = new Attribute( 'isNew', true );
+
+		root.insertChildren( 0, 'bar' );
+
+		doc.applyOperation(
+			new ChangeOperation(
+				new Range( new Position( [ 0 ], root ), new Position( [ 2 ], root ) ),
+				null,
+				newAttr,
+				doc.version
+			)
+		);
+
+		expect( doc.version ).to.equal( 1 );
+		expect( root.getChildCount() ).to.equal( 3 );
+		expect( root.getChild( 0 ).hasAttr( newAttr ) ).to.be.true;
+		expect( root.getChild( 1 ).hasAttr( newAttr ) ).to.be.true;
+		expect( root.getChild( 2 )._getAttrCount() ).to.equal( 0 );
+	} );
+
+	it( 'should add attribute to the existing attributes', function() {
+		var newAttr = new Attribute( 'isNew', true );
+		var fooAttr = new Attribute( 'foo', true );
+		var barAttr = new Attribute( 'bar', true );
+
+		root.insertChildren( 0, new Character( 'x', [ fooAttr, barAttr ] ) );
+
+		doc.applyOperation(
+			new ChangeOperation(
+				new Range( new Position( [ 0 ], root ), new Position( [ 1 ], root ) ),
+				null,
+				newAttr,
+				doc.version
+			)
+		);
+
+		expect( doc.version ).to.equal( 1 );
+		expect( root.getChildCount() ).to.equal( 1 );
+		expect( root.getChild( 0 )._getAttrCount() ).to.equal( 3 );
+		expect( root.getChild( 0 ).hasAttr( newAttr ) ).to.be.true;
+		expect( root.getChild( 0 ).hasAttr( fooAttr ) ).to.be.true;
+		expect( root.getChild( 0 ).hasAttr( barAttr ) ).to.be.true;
+	} );
+
+	it( 'should change attribute to the set of nodes', function() {
+		var oldAttr = new Attribute( 'isNew', false );
+		var newAttr = new Attribute( 'isNew', true );
+
+		root.insertChildren( 0, new Text( 'bar', [ oldAttr ] ) );
+
+		doc.applyOperation(
+			new ChangeOperation(
+				new Range( new Position( [ 0 ], root ), new Position( [ 2 ], root ) ),
+				oldAttr,
+				newAttr,
+				doc.version
+			)
+		);
+
+		expect( doc.version ).to.equal( 1 );
+		expect( root.getChildCount() ).to.equal( 3 );
+		expect( root.getChild( 0 )._getAttrCount() ).to.equal( 1 );
+		expect( root.getChild( 0 ).hasAttr( newAttr ) ).to.be.true;
+		expect( root.getChild( 1 )._getAttrCount() ).to.equal( 1 );
+		expect( root.getChild( 1 ).hasAttr( newAttr ) ).to.be.true;
+		expect( root.getChild( 2 )._getAttrCount() ).to.equal( 1 );
+		expect( root.getChild( 2 ).hasAttr( oldAttr ) ).to.be.true;
+	} );
+
+	it( 'should change attribute in the middle of existing attributes', function() {
+		var fooAttr = new Attribute( 'foo', true );
+		var x1Attr = new Attribute( 'x', 1 );
+		var x2Attr = new Attribute( 'x', 2 );
+		var barAttr = new Attribute( 'bar', true );
+
+		root.insertChildren( 0, new Character( 'x', [ fooAttr, x1Attr, barAttr ] ) );
+
+		doc.applyOperation(
+			new ChangeOperation(
+				new Range( new Position( [ 0 ], root ), new Position( [ 1 ], root ) ),
+				x1Attr,
+				x2Attr,
+				doc.version
+			)
+		);
+
+		expect( doc.version ).to.equal( 1 );
+		expect( root.getChildCount() ).to.equal( 1 );
+		expect( root.getChild( 0 )._getAttrCount() ).to.equal( 3 );
+		expect( root.getChild( 0 ).hasAttr( fooAttr ) ).to.be.true;
+		expect( root.getChild( 0 ).hasAttr( x2Attr ) ).to.be.true;
+		expect( root.getChild( 0 ).hasAttr( barAttr ) ).to.be.true;
+	} );
+
+	it( 'should remove attribute', function() {
+		var fooAttr = new Attribute( 'foo', true );
+		var xAttr = new Attribute( 'x', true );
+		var barAttr = new Attribute( 'bar', true );
+
+		root.insertChildren( 0, new Character( 'x', [ fooAttr, xAttr, barAttr ] ) );
+
+		doc.applyOperation(
+			new ChangeOperation(
+				new Range( new Position( [ 0 ], root ), new Position( [ 1 ], root ) ),
+				xAttr,
+				null,
+				doc.version
+			)
+		);
+
+		expect( doc.version ).to.equal( 1 );
+		expect( root.getChildCount() ).to.equal( 1 );
+		expect( root.getChild( 0 )._getAttrCount() ).to.equal( 2 );
+		expect( root.getChild( 0 ).hasAttr( fooAttr ) ).to.be.true;
+		expect( root.getChild( 0 ).hasAttr( barAttr ) ).to.be.true;
+	} );
+
+	it( 'should create a change operation as a reverse', function() {
+		var oldAttr = new Attribute( 'x', 'old' );
+		var newAttr = new Attribute( 'x', 'new' );
+		var range = new Range( new Position( [ 0 ], root ), new Position( [ 3 ], root ) );
+		var operation = new ChangeOperation( range, oldAttr, newAttr, doc.version );
+		var reverse = operation.getReversed();
+
+		expect( reverse ).to.be.an.instanceof( ChangeOperation );
+		expect( reverse.baseVersion ).to.equal( 1 );
+		expect( reverse.range ).to.equal( range );
+		expect( reverse.oldAttr ).to.equal( newAttr );
+		expect( reverse.newAttr ).to.equal( oldAttr );
+	} );
+
+	it( 'should undo adding attribute by applying reverse operation', function() {
+		var newAttr = new Attribute( 'isNew', true );
+
+		root.insertChildren( 0, 'bar' );
+
+		var operation = new ChangeOperation(
+			new Range( new Position( [ 0 ], root ), new Position( [ 3 ], root ) ),
+			null,
+			newAttr,
+			doc.version
+		);
+
+		var reverse = operation.getReversed();
+
+		doc.applyOperation( operation );
+		doc.applyOperation( reverse );
+
+		expect( doc.version ).to.equal( 2 );
+		expect( root.getChildCount() ).to.equal( 3 );
+		expect( root.getChild( 0 )._getAttrCount() ).to.equal( 0 );
+		expect( root.getChild( 1 )._getAttrCount() ).to.equal( 0 );
+		expect( root.getChild( 2 )._getAttrCount() ).to.equal( 0 );
+	} );
+
+	it( 'should undo changing attribute by applying reverse operation', function() {
+		var oldAttr = new Attribute( 'isNew', false );
+		var newAttr = new Attribute( 'isNew', true );
+
+		root.insertChildren( 0, new Text( 'bar', [ oldAttr ] ) );
+
+		var operation = new ChangeOperation(
+			new Range( new Position( [ 0 ], root ), new Position( [ 3 ], root ) ),
+			oldAttr,
+			newAttr,
+			doc.version
+		);
+
+		var reverse = operation.getReversed();
+
+		doc.applyOperation( operation );
+
+		doc.applyOperation( reverse );
+
+		expect( doc.version ).to.equal( 2 );
+		expect( root.getChildCount() ).to.equal( 3 );
+		expect( root.getChild( 0 )._getAttrCount() ).to.equal( 1 );
+		expect( root.getChild( 0 ).hasAttr( oldAttr ) ).to.be.true;
+		expect( root.getChild( 1 )._getAttrCount() ).to.equal( 1 );
+		expect( root.getChild( 1 ).hasAttr( oldAttr ) ).to.be.true;
+		expect( root.getChild( 2 )._getAttrCount() ).to.equal( 1 );
+		expect( root.getChild( 2 ).hasAttr( oldAttr ) ).to.be.true;
+	} );
+
+	it( 'should undo remove attribute by applying reverse operation', function() {
+		var fooAttr = new Attribute( 'foo', false );
+
+		root.insertChildren( 0, new Text( 'bar', [ fooAttr ] ) );
+
+		var operation = new ChangeOperation(
+			new Range( new Position( [ 0 ], root ), new Position( [ 3 ], root ) ),
+			fooAttr,
+			null,
+			doc.version
+		);
+
+		var reverse = operation.getReversed();
+
+		doc.applyOperation( operation );
+
+		doc.applyOperation( reverse );
+
+		expect( doc.version ).to.equal( 2 );
+		expect( root.getChildCount() ).to.equal( 3 );
+		expect( root.getChild( 0 )._getAttrCount() ).to.equal( 1 );
+		expect( root.getChild( 0 ).hasAttr( fooAttr ) ).to.be.true;
+		expect( root.getChild( 1 )._getAttrCount() ).to.equal( 1 );
+		expect( root.getChild( 1 ).hasAttr( fooAttr ) ).to.be.true;
+		expect( root.getChild( 2 )._getAttrCount() ).to.equal( 1 );
+		expect( root.getChild( 2 ).hasAttr( fooAttr ) ).to.be.true;
+	} );
+
+	it( 'should throw an error when one try to remove and the attribute does not exists', function() {
+		var fooAttr = new Attribute( 'foo', true );
+
+		root.insertChildren( 0, 'x' );
+
+		expect(
+			function() {
+				doc.applyOperation(
+					new ChangeOperation(
+						new Range( new Position( [ 0 ], root ), new Position( [ 1 ], root ) ),
+						fooAttr,
+						null,
+						doc.version
+					)
+				);
+			}
+		).to.throw( CKEditorError, /operation-change-no-attr-to-remove/ );
+	} );
+
+	it( 'should throw an error when one try to insert and the attribute already exists', function() {
+		var x1Attr = new Attribute( 'x', 1 );
+		var x2Attr = new Attribute( 'x', 2 );
+
+		root.insertChildren( 0, new Character( 'x', [ x1Attr ] ) );
+
+		expect(
+			function() {
+				doc.applyOperation(
+					new ChangeOperation(
+						new Range( new Position( [ 0 ], root ), new Position( [ 1 ], root ) ),
+						null,
+						x2Attr,
+						doc.version
+					)
+				);
+			}
+		).to.throw( CKEditorError, /operation-change-attr-exists/ );
+	} );
+
+	it( 'should throw an error when one try to change and the new and old attributes have different keys', function() {
+		var fooAttr = new Attribute( 'foo', true );
+		var barAttr = new Attribute( 'bar', true );
+
+		root.insertChildren( 0, 'x' );
+
+		expect(
+			function() {
+				doc.applyOperation(
+					new ChangeOperation(
+						new Range( new Position( [ 0 ], root ), new Position( [ 1 ], root ) ),
+						fooAttr,
+						barAttr,
+						doc.version
+					)
+				);
+			}
+		).to.throw( CKEditorError, /operation-change-different-keys/ );
+	} );
+} );

+ 160 - 0
packages/ckeditor5-engine/tests/document/operation/insertoperation.js

@@ -0,0 +1,160 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document */
+
+'use strict';
+
+var modules = bender.amd.require(
+	'document/document',
+	'document/operation/insertoperation',
+	'document/operation/removeoperation',
+	'document/position',
+	'document/character',
+	'document/nodelist'
+);
+
+describe( 'InsertOperation', function() {
+	var Document, InsertOperation, RemoveOperation, Position, Character;
+
+	before( function() {
+		Document = modules[ 'document/document' ];
+		InsertOperation = modules[ 'document/operation/insertoperation' ];
+		RemoveOperation = modules[ 'document/operation/removeoperation' ];
+		Position = modules[ 'document/position' ];
+		Character = modules[ 'document/character' ];
+	} );
+
+	var doc, root;
+
+	beforeEach( function() {
+		doc = new Document();
+		root = doc.createRoot( 'root' );
+	} );
+
+	it( 'should insert node', function() {
+		doc.applyOperation(
+			new InsertOperation(
+				new Position( [ 0 ], root ),
+				new Character( 'x' ),
+				doc.version
+			)
+		);
+
+		expect( doc.version ).to.equal( 1 );
+		expect( root.getChildCount() ).to.equal( 1 );
+		expect( root.getChild( 0 ).character ).to.equal( 'x' );
+	} );
+
+	it( 'should insert set of nodes', function() {
+		doc.applyOperation(
+			new InsertOperation(
+				new Position( [ 0 ], root ),
+				'bar',
+				doc.version
+			)
+		);
+
+		expect( doc.version ).to.equal( 1 );
+		expect( root.getChildCount() ).to.equal( 3 );
+		expect( root.getChild( 0 ).character ).to.equal( 'b' );
+		expect( root.getChild( 1 ).character ).to.equal( 'a' );
+		expect( root.getChild( 2 ).character ).to.equal( 'r' );
+	} );
+
+	it( 'should insert between existing nodes', function() {
+		root.insertChildren( 0, 'xy' );
+
+		doc.applyOperation(
+			new InsertOperation(
+				new Position( [ 1 ], root ),
+				'bar',
+				doc.version
+			)
+		);
+
+		expect( doc.version ).to.equal( 1 );
+		expect( root.getChildCount() ).to.equal( 5 );
+		expect( root.getChild( 0 ).character ).to.equal( 'x' );
+		expect( root.getChild( 1 ).character ).to.equal( 'b' );
+		expect( root.getChild( 2 ).character ).to.equal( 'a' );
+		expect( root.getChild( 3 ).character ).to.equal( 'r' );
+		expect( root.getChild( 4 ).character ).to.equal( 'y' );
+	} );
+
+	it( 'should insert text', function() {
+		doc.applyOperation(
+			new InsertOperation(
+				new Position( [ 0 ], root ),
+				[ 'foo', new Character( 'x' ), 'bar' ],
+				doc.version
+			)
+		);
+
+		expect( doc.version ).to.equal( 1 );
+		expect( root.getChildCount() ).to.equal( 7 );
+		expect( root.getChild( 0 ).character ).to.equal( 'f' );
+		expect( root.getChild( 1 ).character ).to.equal( 'o' );
+		expect( root.getChild( 2 ).character ).to.equal( 'o' );
+		expect( root.getChild( 3 ).character ).to.equal( 'x' );
+		expect( root.getChild( 4 ).character ).to.equal( 'b' );
+		expect( root.getChild( 5 ).character ).to.equal( 'a' );
+		expect( root.getChild( 6 ).character ).to.equal( 'r' );
+	} );
+
+	it( 'should create a remove operation as a reverse', function() {
+		var position = new Position( [ 0 ], root );
+		var operation = new InsertOperation(
+			position,
+			[ 'foo', new Character( 'x' ), 'bar' ],
+			0
+		);
+
+		var reverse = operation.getReversed();
+
+		expect( reverse ).to.be.an.instanceof( RemoveOperation );
+		expect( reverse.baseVersion ).to.equal( 1 );
+		expect( reverse.sourcePosition ).to.equal( position );
+		expect( reverse.howMany ).to.equal( 7 );
+	} );
+
+	it( 'should undo insert node by applying reverse operation', function() {
+		var operation = new InsertOperation(
+			new Position( [ 0 ], root ),
+			new Character( 'x' ),
+			doc.version
+		);
+
+		var reverse = operation.getReversed();
+
+		doc.applyOperation( operation );
+
+		expect( doc.version ).to.equal( 1 );
+
+		doc.applyOperation( reverse );
+
+		expect( doc.version ).to.equal( 2 );
+		expect( root.getChildCount() ).to.equal( 0 );
+	} );
+
+	it( 'should undo insert set of nodes by applying reverse operation', function() {
+		var operation = new InsertOperation(
+			new Position( [ 0 ], root ),
+			'bar',
+			doc.version
+		);
+
+		var reverse = operation.getReversed();
+
+		doc.applyOperation( operation );
+
+		expect( doc.version ).to.equal( 1 );
+
+		doc.applyOperation( reverse );
+
+		expect( doc.version ).to.equal( 2 );
+		expect( root.getChildCount() ).to.equal( 0 );
+	} );
+} );

+ 245 - 0
packages/ckeditor5-engine/tests/document/operation/moveoperation.js

@@ -0,0 +1,245 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document */
+
+'use strict';
+
+var modules = bender.amd.require(
+	'document/document',
+	'document/operation/moveoperation',
+	'document/position',
+	'document/element',
+	'document/nodelist',
+	'ckeditorerror'
+);
+
+describe( 'MoveOperation', function() {
+	var Document, MoveOperation, Position, Element, NodeList, CKEditorError;
+
+	before( function() {
+		Document = modules[ 'document/document' ];
+		MoveOperation = modules[ 'document/operation/moveoperation' ];
+		Position = modules[ 'document/position' ];
+		Element = modules[ 'document/element' ];
+		NodeList = modules[ 'document/nodelist' ];
+		CKEditorError = modules.ckeditorerror;
+	} );
+
+	var doc, root;
+
+	beforeEach( function() {
+		doc = new Document();
+		root = doc.createRoot( 'root' );
+	} );
+
+	it( 'should move from one node to another', function() {
+		var p1 = new Element( 'p1', [], new Element( 'x' ) );
+		var p2 = new Element( 'p2' );
+
+		root.insertChildren( 0, [ p1, p2 ] );
+
+		doc.applyOperation(
+			new MoveOperation(
+				new Position( [ 0, 0 ], root ),
+				new Position( [ 1, 0 ], root ),
+				1,
+				doc.version
+			)
+		);
+
+		expect( doc.version ).to.equal( 1 );
+		expect( root.getChildCount() ).to.equal( 2 );
+		expect( root.getChild( 0 ).name ).to.equal( 'p1' );
+		expect( root.getChild( 1 ).name ).to.equal( 'p2' );
+		expect( p1.getChildCount() ).to.equal( 0 );
+		expect( p2.getChildCount() ).to.equal( 1 );
+		expect( p2.getChild( 0 ).name ).to.equal( 'x' );
+	} );
+
+	it( 'should move position of children in one node backward', function() {
+		root.insertChildren( 0, 'xbarx' );
+
+		doc.applyOperation(
+			new MoveOperation(
+				new Position( [ 2 ], root ),
+				new Position( [ 1 ], root ),
+				2,
+				doc.version
+			)
+		);
+
+		expect( doc.version ).to.equal( 1 );
+		expect( root.getChildCount() ).to.equal( 5 );
+		expect( root.getChild( 0 ).character ).to.equal( 'x' );
+		expect( root.getChild( 1 ).character ).to.equal( 'a' );
+		expect( root.getChild( 2 ).character ).to.equal( 'r' );
+		expect( root.getChild( 3 ).character ).to.equal( 'b' );
+		expect( root.getChild( 4 ).character ).to.equal( 'x' );
+	} );
+
+	it( 'should move position of children in one node forward', function() {
+		root.insertChildren( 0, 'xbarx' );
+
+		doc.applyOperation(
+			new MoveOperation(
+				new Position( [ 1 ], root ),
+				new Position( [ 4 ], root ),
+				2,
+				doc.version
+			)
+		);
+
+		expect( doc.version ).to.equal( 1 );
+		expect( root.getChildCount() ).to.equal( 5 );
+		expect( root.getChild( 0 ).character ).to.equal( 'x' );
+		expect( root.getChild( 1 ).character ).to.equal( 'r' );
+		expect( root.getChild( 2 ).character ).to.equal( 'b' );
+		expect( root.getChild( 3 ).character ).to.equal( 'a' );
+		expect( root.getChild( 4 ).character ).to.equal( 'x' );
+	} );
+
+	it( 'should create a move operation as a reverse', function() {
+		var nodeList = new NodeList( 'bar' );
+
+		var sourcePosition = new Position( [ 0 ], root );
+		var targetPosition = new Position( [ 4 ], root );
+
+		var operation = new MoveOperation( sourcePosition, targetPosition, nodeList.length, doc.version );
+
+		var reverse = operation.getReversed();
+
+		expect( reverse ).to.be.an.instanceof( MoveOperation );
+		expect( reverse.baseVersion ).to.equal( 1 );
+		expect( reverse.howMany ).to.equal( nodeList.length );
+		expect( reverse.sourcePosition ).to.equal( targetPosition );
+		expect( reverse.targetPosition ).to.equal( sourcePosition );
+	} );
+
+	it( 'should undo move node by applying reverse operation', function() {
+		var p1 = new Element( 'p1', [], new Element( 'x' ) );
+		var p2 = new Element( 'p2' );
+
+		root.insertChildren( 0, [ p1, p2 ] );
+
+		var operation = new MoveOperation(
+			new Position( [ 0, 0 ], root ),
+			new Position( [ 1, 0 ], root ),
+			1,
+			doc.version
+		);
+
+		doc.applyOperation( operation );
+
+		expect( doc.version ).to.equal( 1 );
+		expect( root.getChildCount() ).to.equal( 2 );
+		expect( p1.getChildCount() ).to.equal( 0 );
+		expect( p2.getChildCount() ).to.equal( 1 );
+		expect( p2.getChild( 0 ).name ).to.equal( 'x' );
+
+		doc.applyOperation( operation.getReversed() );
+
+		expect( doc.version ).to.equal( 2 );
+		expect( root.getChildCount() ).to.equal( 2 );
+		expect( p1.getChildCount() ).to.equal( 1 );
+		expect( p1.getChild( 0 ).name ).to.equal( 'x' );
+		expect( p2.getChildCount() ).to.equal( 0 );
+	} );
+
+	it( 'should throw an error if number of nodes to move exceeds the number of existing nodes in given element', function() {
+		root.insertChildren( 0, 'xbarx' );
+
+		expect(
+			function() {
+				doc.applyOperation(
+					new MoveOperation(
+						new Position( [ 3 ], root ),
+						new Position( [ 1 ], root ),
+						3,
+						doc.version
+					)
+				);
+			}
+		).to.throw( CKEditorError, /operation-move-nodes-do-not-exist/ );
+	} );
+
+	it( 'should throw an error if target or source parent-element specified by position does not exist', function() {
+		var p = new Element( 'p' );
+		p.insertChildren( 0, 'foo' );
+		root.insertChildren( 0, [ 'ab', p ] );
+
+		var operation = new MoveOperation(
+			new Position( [ 2, 0 ], root ),
+			new Position( [ 1 ], root ),
+			3,
+			doc.version
+		);
+
+		root.removeChildren( 2, 1 );
+
+		expect(
+			function() {
+				doc.applyOperation( operation );
+			}
+		).to.throw( CKEditorError, /operation-move-position-invalid/ );
+	} );
+
+	it( 'should throw an error if operation tries to move a range between the beginning and the end of that range', function() {
+		root.insertChildren( 0, 'xbarx' );
+
+		var operation = new MoveOperation(
+			new Position( [ 1 ], root ),
+			new Position( [ 2 ], root ),
+			3,
+			doc.version
+		);
+
+		expect(
+			function() {
+				doc.applyOperation( operation );
+			}
+		).to.throw( CKEditorError, /operation-move-range-into-itself/ );
+	} );
+
+	it( 'should throw an error if operation tries to move a range into a sub-tree of a node that is in that range', function() {
+		var p = new Element( 'p', [], [ new Element( 'p' ) ] );
+		root.insertChildren( 0, [ 'ab', p, 'xy' ] );
+
+		var operation = new MoveOperation(
+			new Position( [ 1 ], root ),
+			new Position( [ 2, 0, 0 ], root ),
+			3,
+			doc.version
+		);
+
+		expect(
+			function() {
+				doc.applyOperation( operation );
+			}
+		).to.throw( CKEditorError, /operation-move-node-into-itself/ );
+	} );
+
+	it( 'should not throw an error if operation move a range into a sibling', function() {
+		var p = new Element( 'p' );
+		root.insertChildren( 0, [ 'ab', p, 'xy' ] );
+
+		var operation = new MoveOperation(
+			new Position( [ 1 ], root ),
+			new Position( [ 2, 0 ], root ),
+			1,
+			doc.version
+		);
+
+		expect(
+			function() {
+				doc.applyOperation( operation );
+			}
+		).not.to.throw();
+
+		expect( root.getChildCount() ).to.equal( 4 );
+		expect( p.getChildCount() ).to.equal( 1 );
+		expect( p.getChild( 0 ).character ).to.equal( 'b' );
+	} );
+} );

+ 81 - 0
packages/ckeditor5-engine/tests/document/operation/reinsertoperation.js

@@ -0,0 +1,81 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document */
+
+'use strict';
+
+var modules = bender.amd.require(
+	'document/document',
+	'document/operation/reinsertoperation',
+	'document/operation/removeoperation',
+	'document/operation/moveoperation',
+	'document/position'
+);
+
+describe( 'ReinsertOperation', function() {
+	var Document, ReinsertOperation, RemoveOperation, MoveOperation, Position;
+
+	before( function() {
+		Document = modules[ 'document/document' ];
+		ReinsertOperation = modules[ 'document/operation/reinsertoperation' ];
+		RemoveOperation = modules[ 'document/operation/removeoperation' ];
+		MoveOperation = modules[ 'document/operation/moveoperation' ];
+		Position = modules[ 'document/position' ];
+	} );
+
+	var doc, root, graveyard, operation, graveyardPosition, rootPosition;
+
+	beforeEach( function() {
+		doc = new Document();
+		root = doc.createRoot( 'root' );
+		graveyard = doc._graveyard;
+
+		graveyardPosition = new Position( [ 0 ], graveyard );
+		rootPosition = new Position( [ 0 ], root );
+
+		operation = new ReinsertOperation(
+			graveyardPosition,
+			rootPosition,
+			2,
+			doc.version
+		);
+	} );
+
+	it( 'should extend MoveOperation class', function() {
+		expect( operation ).to.be.instanceof( MoveOperation );
+	} );
+
+	it( 'should create a remove operation as a reverse', function() {
+		var reverse = operation.getReversed();
+
+		expect( reverse ).to.be.an.instanceof( RemoveOperation );
+		expect( reverse.baseVersion ).to.equal( 1 );
+		expect( reverse.howMany ).to.equal( 2 );
+		expect( reverse.sourcePosition.isEqual( rootPosition ) ).to.be.true;
+		expect( reverse.targetPosition.isEqual( graveyardPosition ) ).to.be.true;
+	} );
+
+	it( 'should undo reinsert set of nodes by applying reverse operation', function() {
+		var reverse = operation.getReversed();
+
+		graveyard.insertChildren( 0, 'bar' );
+
+		doc.applyOperation( operation );
+
+		expect( doc.version ).to.equal( 1 );
+		expect( root.getChildCount() ).to.equal( 2 );
+
+		doc.applyOperation( reverse );
+
+		expect( doc.version ).to.equal( 2 );
+		expect( root.getChildCount() ).to.equal( 0 );
+		expect( graveyard.getChildCount() ).to.equal( 3 );
+
+		expect( graveyard.getChild( 0 ).character ).to.equal( 'b' );
+		expect( graveyard.getChild( 1 ).character ).to.equal( 'a' );
+		expect( graveyard.getChild( 2 ).character ).to.equal( 'r' );
+	} );
+} );

+ 103 - 0
packages/ckeditor5-engine/tests/document/operation/removeoperation.js

@@ -0,0 +1,103 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document */
+
+'use strict';
+
+var modules = bender.amd.require(
+	'document/document',
+	'document/operation/reinsertoperation',
+	'document/operation/removeoperation',
+	'document/operation/moveoperation',
+	'document/position'
+);
+
+describe( 'RemoveOperation', function() {
+	var Document, ReinsertOperation, RemoveOperation, MoveOperation, Position;
+
+	before( function() {
+		Document = modules[ 'document/document' ];
+		ReinsertOperation = modules[ 'document/operation/reinsertoperation' ];
+		RemoveOperation = modules[ 'document/operation/removeoperation' ];
+		MoveOperation = modules[ 'document/operation/removeoperation' ];
+		Position = modules[ 'document/position' ];
+	} );
+
+	var doc, root, graveyard;
+
+	beforeEach( function() {
+		doc = new Document();
+		root = doc.createRoot( 'root' );
+		graveyard = doc._graveyard;
+	} );
+
+	it( 'should extend MoveOperation class', function() {
+		var operation = new RemoveOperation(
+			new Position( [ 2 ], root ),
+			2,
+			doc.version
+		);
+
+		expect( operation ).to.be.instanceof( MoveOperation );
+	} );
+
+	it( 'should remove set of nodes and append them to graveyard root', function() {
+		root.insertChildren( 0, 'fozbar' );
+
+		var z = root.getChild( 2 );
+		var b = root.getChild( 3 );
+		var a = root.getChild( 4 );
+
+		doc.applyOperation(
+			new RemoveOperation(
+				new Position( [ 2 ], root ),
+				2,
+				doc.version
+			)
+		);
+
+		expect( doc.version ).to.equal( 1 );
+		expect( root.getChildCount() ).to.equal( 4 );
+		expect( root.getChild( 2 ) ).to.equal( a );
+
+		expect( graveyard.getChildCount() ).to.equal( 2 );
+		expect( graveyard.getChild( 0 ) ).to.equal( z );
+		expect( graveyard.getChild( 1 ) ).to.equal( b );
+	} );
+
+	it( 'should create a reinsert operation as a reverse', function() {
+		var position = new Position( [ 0 ], root );
+		var operation = new RemoveOperation( position, 2, 0 );
+		var reverse = operation.getReversed();
+
+		expect( reverse ).to.be.an.instanceof( ReinsertOperation );
+		expect( reverse.baseVersion ).to.equal( 1 );
+		expect( reverse.howMany ).to.equal( 2 );
+		expect( reverse.sourcePosition ).to.equal( operation.targetPosition );
+		expect( reverse.targetPosition ).to.equal( position );
+	} );
+
+	it( 'should undo remove set of nodes by applying reverse operation', function() {
+		var position = new Position( [ 0 ], root );
+		var operation = new RemoveOperation( position, 3, 0 );
+		var reverse = operation.getReversed();
+
+		root.insertChildren( 0, 'bar' );
+
+		doc.applyOperation( operation );
+
+		expect( doc.version ).to.equal( 1 );
+		expect( root.getChildCount() ).to.equal( 0 );
+
+		doc.applyOperation( reverse );
+
+		expect( doc.version ).to.equal( 2 );
+		expect( root.getChildCount() ).to.equal( 3 );
+		expect( root.getChild( 0 ).character ).to.equal( 'b' );
+		expect( root.getChild( 1 ).character ).to.equal( 'a' );
+		expect( root.getChild( 2 ).character ).to.equal( 'r' );
+	} );
+} );

+ 229 - 0
packages/ckeditor5-engine/tests/document/position.js

@@ -0,0 +1,229 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document */
+
+'use strict';
+
+var modules = bender.amd.require(
+	'document/element',
+	'document/character',
+	'document/position',
+	'document/document',
+	'ckeditorerror',
+	'document/nodelist'
+);
+
+describe( 'position', function() {
+	var Element, Character, Document, NodeList, Position, CKEditorError;
+
+	var doc, root, p, ul, li1, li2, f, o, z, b, a, r;
+
+	// root
+	//  |- p         Before: [ 0 ]       After: [ 1 ]
+	//  |- ul        Before: [ 1 ]       After: [ 2 ]
+	//     |- li     Before: [ 1, 0 ]    After: [ 1, 1 ]
+	//     |  |- f   Before: [ 1, 0, 0 ] After: [ 1, 0, 1 ]
+	//     |  |- o   Before: [ 1, 0, 1 ] After: [ 1, 0, 2 ]
+	//     |  |- z   Before: [ 1, 0, 2 ] After: [ 1, 0, 3 ]
+	//     |- li     Before: [ 1, 1 ]    After: [ 1, 2 ]
+	//        |- b   Before: [ 1, 1, 0 ] After: [ 1, 1, 1 ]
+	//        |- a   Before: [ 1, 1, 1 ] After: [ 1, 1, 2 ]
+	//        |- r   Before: [ 1, 1, 2 ] After: [ 1, 1, 3 ]
+	before( function() {
+		Element = modules[ 'document/element' ];
+		Character = modules[ 'document/character' ];
+		Document = modules[ 'document/document' ];
+		NodeList = modules[ 'document/nodelist' ];
+		Position = modules[ 'document/position' ];
+		CKEditorError = modules.ckeditorerror;
+
+		doc = new Document();
+
+		root = doc.createRoot( 'root' );
+
+		f = new Character( 'f' );
+		o = new Character( 'o' );
+		z = new Character( 'z' );
+
+		li1 = new Element( 'li', [], [ f, o, z ] );
+
+		b = new Character( 'b' );
+		a = new Character( 'a' );
+		r = new Character( 'r' );
+
+		li2 = new Element( 'li', [], [ b, a, r ] );
+
+		ul = new Element( 'ul', [], [ li1, li2 ] );
+
+		p = new Element( 'p' );
+
+		root.insertChildren( 0, [ p, ul ] );
+	} );
+
+	it( 'should create a position with path and document', function() {
+		var position = new Position( [ 0 ], root );
+
+		expect( position ).to.have.property( 'path' ).that.deep.equals( [ 0 ] );
+		expect( position ).to.have.property( 'root' ).that.equals( root );
+	} );
+
+	it( 'should make positions form node and offset', function() {
+		expect( Position.createFromParentAndOffset( root, 0 ) ).to.have.property( 'path' ).that.deep.equals( [ 0 ] );
+		expect( Position.createFromParentAndOffset( root, 1 ) ).to.have.property( 'path' ).that.deep.equals( [ 1 ] );
+		expect( Position.createFromParentAndOffset( root, 2 ) ).to.have.property( 'path' ).that.deep.equals( [ 2 ] );
+
+		expect( Position.createFromParentAndOffset( p, 0 ) ).to.have.property( 'path' ).that.deep.equals( [ 0, 0 ] );
+
+		expect( Position.createFromParentAndOffset( ul, 0 ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 0 ] );
+		expect( Position.createFromParentAndOffset( ul, 1 ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 1 ] );
+		expect( Position.createFromParentAndOffset( ul, 2 ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 2 ] );
+
+		expect( Position.createFromParentAndOffset( li1, 0 ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 0, 0 ] );
+		expect( Position.createFromParentAndOffset( li1, 1 ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 0, 1 ] );
+		expect( Position.createFromParentAndOffset( li1, 2 ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 0, 2 ] );
+		expect( Position.createFromParentAndOffset( li1, 3 ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 0, 3 ] );
+	} );
+
+	it( 'should make positions before elements', function() {
+		expect( Position.createBefore( p ) ).to.have.property( 'path' ).that.deep.equals( [ 0 ] );
+
+		expect( Position.createBefore( ul ) ).to.have.property( 'path' ).that.deep.equals( [ 1 ] );
+
+		expect( Position.createBefore( li1 ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 0 ] );
+
+		expect( Position.createBefore( f ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 0, 0 ] );
+		expect( Position.createBefore( o ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 0, 1 ] );
+		expect( Position.createBefore( z ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 0, 2 ] );
+
+		expect( Position.createBefore( li2 ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 1 ] );
+
+		expect( Position.createBefore( b ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 1, 0 ] );
+		expect( Position.createBefore( a ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 1, 1 ] );
+		expect( Position.createBefore( r ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 1, 2 ] );
+	} );
+
+	it( 'should throw error if one try to make positions before root', function() {
+		expect(
+			function() {
+				Position.createBefore( root );
+			}
+		).to.throw( CKEditorError, /position-before-root/ );
+	} );
+
+	it( 'should make positions after elements', function() {
+		expect( Position.createAfter( p ) ).to.have.property( 'path' ).that.deep.equals( [ 1 ] );
+
+		expect( Position.createAfter( ul ) ).to.have.property( 'path' ).that.deep.equals( [ 2 ] );
+
+		expect( Position.createAfter( li1 ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 1 ] );
+
+		expect( Position.createAfter( f ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 0, 1 ] );
+		expect( Position.createAfter( o ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 0, 2 ] );
+		expect( Position.createAfter( z ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 0, 3 ] );
+
+		expect( Position.createAfter( li2 ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 2 ] );
+
+		expect( Position.createAfter( b ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 1, 1 ] );
+		expect( Position.createAfter( a ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 1, 2 ] );
+		expect( Position.createAfter( r ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 1, 3 ] );
+	} );
+
+	it( 'should throw error if one try to make positions after root', function() {
+		expect(
+			function() {
+				Position.createAfter( root );
+			}
+		).to.throw( CKEditorError, /position-after-root/ );
+	} );
+
+	it( 'should have parent', function() {
+		expect( new Position( [ 0 ], root ) ).to.have.property( 'parent' ).that.equals( root );
+		expect( new Position( [ 1 ], root ) ).to.have.property( 'parent' ).that.equals( root );
+		expect( new Position( [ 2 ], root ) ).to.have.property( 'parent' ).that.equals( root );
+
+		expect( new Position( [ 0, 0 ], root ) ).to.have.property( 'parent' ).that.equals( p );
+
+		expect( new Position( [ 1, 0 ], root ) ).to.have.property( 'parent' ).that.equals( ul );
+		expect( new Position( [ 1, 1 ], root ) ).to.have.property( 'parent' ).that.equals( ul );
+		expect( new Position( [ 1, 2 ], root ) ).to.have.property( 'parent' ).that.equals( ul );
+
+		expect( new Position( [ 1, 0, 0 ], root ) ).to.have.property( 'parent' ).that.equals( li1 );
+		expect( new Position( [ 1, 0, 1 ], root ) ).to.have.property( 'parent' ).that.equals( li1 );
+		expect( new Position( [ 1, 0, 2 ], root ) ).to.have.property( 'parent' ).that.equals( li1 );
+		expect( new Position( [ 1, 0, 3 ], root ) ).to.have.property( 'parent' ).that.equals( li1 );
+	} );
+
+	it( 'should have offset', function() {
+		expect( new Position( [ 0 ], root ) ).to.have.property( 'offset' ).that.equals( 0 );
+		expect( new Position( [ 1 ], root ) ).to.have.property( 'offset' ).that.equals( 1 );
+		expect( new Position( [ 2 ], root ) ).to.have.property( 'offset' ).that.equals( 2 );
+
+		expect( new Position( [ 0, 0 ], root ) ).to.have.property( 'offset' ).that.equals( 0 );
+
+		expect( new Position( [ 1, 0 ], root ) ).to.have.property( 'offset' ).that.equals( 0 );
+		expect( new Position( [ 1, 1 ], root ) ).to.have.property( 'offset' ).that.equals( 1 );
+		expect( new Position( [ 1, 2 ], root ) ).to.have.property( 'offset' ).that.equals( 2 );
+
+		expect( new Position( [ 1, 0, 0 ], root ) ).to.have.property( 'offset' ).that.equals( 0 );
+		expect( new Position( [ 1, 0, 1 ], root ) ).to.have.property( 'offset' ).that.equals( 1 );
+		expect( new Position( [ 1, 0, 2 ], root ) ).to.have.property( 'offset' ).that.equals( 2 );
+		expect( new Position( [ 1, 0, 3 ], root ) ).to.have.property( 'offset' ).that.equals( 3 );
+	} );
+
+	it( 'should have nodeBefore', function() {
+		expect( new Position( [ 0 ], root ) ).to.have.property( 'nodeBefore' ).that.is.null;
+		expect( new Position( [ 1 ], root ) ).to.have.property( 'nodeBefore' ).that.equals( p );
+		expect( new Position( [ 2 ], root ) ).to.have.property( 'nodeBefore' ).that.equals( ul );
+
+		expect( new Position( [ 0, 0 ], root ) ).to.have.property( 'nodeBefore' ).that.is.null;
+
+		expect( new Position( [ 1, 0 ], root ) ).to.have.property( 'nodeBefore' ).that.is.null;
+		expect( new Position( [ 1, 1 ], root ) ).to.have.property( 'nodeBefore' ).that.equals( li1 );
+		expect( new Position( [ 1, 2 ], root ) ).to.have.property( 'nodeBefore' ).that.equals( li2 );
+
+		expect( new Position( [ 1, 0, 0 ], root ) ).to.have.property( 'nodeBefore' ).that.is.null;
+		expect( new Position( [ 1, 0, 1 ], root ) ).to.have.property( 'nodeBefore' ).that.equals( f );
+		expect( new Position( [ 1, 0, 2 ], root ) ).to.have.property( 'nodeBefore' ).that.equals( o );
+		expect( new Position( [ 1, 0, 3 ], root ) ).to.have.property( 'nodeBefore' ).that.equals( z );
+	} );
+
+	it( 'should have nodeAfter', function() {
+		expect( new Position( [ 0 ], root ) ).to.have.property( 'nodeAfter' ).that.equals( p );
+		expect( new Position( [ 1 ], root ) ).to.have.property( 'nodeAfter' ).that.equals( ul );
+		expect( new Position( [ 2 ], root ) ).to.have.property( 'nodeAfter' ).that.is.null;
+
+		expect( new Position( [ 0, 0 ], root ) ).to.have.property( 'nodeAfter' ).that.is.null;
+
+		expect( new Position( [ 1, 0 ], root ) ).to.have.property( 'nodeAfter' ).that.equals( li1 );
+		expect( new Position( [ 1, 1 ], root ) ).to.have.property( 'nodeAfter' ).that.equals( li2 );
+		expect( new Position( [ 1, 2 ], root ) ).to.have.property( 'nodeAfter' ).that.is.null;
+
+		expect( new Position( [ 1, 0, 0 ], root ) ).to.have.property( 'nodeAfter' ).that.equals( f );
+		expect( new Position( [ 1, 0, 1 ], root ) ).to.have.property( 'nodeAfter' ).that.equals( o );
+		expect( new Position( [ 1, 0, 2 ], root ) ).to.have.property( 'nodeAfter' ).that.equals( z );
+		expect( new Position( [ 1, 0, 3 ], root ) ).to.have.property( 'nodeAfter' ).that.is.null;
+	} );
+
+	it( 'should equals another position with the same path', function() {
+		var position = new Position( [ 1, 1, 2 ], root );
+		var samePosition = new Position( [ 1, 1, 2 ], root );
+
+		expect( position.isEqual( samePosition ) ).to.be.true;
+	} );
+
+	it( 'should not equals another position with the different path', function() {
+		var position = new Position( [ 1, 1, 1 ], root );
+		var differentNode = new Position( [ 1, 2, 2 ], root );
+
+		expect( position.isEqual( differentNode ) ).to.be.false;
+	} );
+
+	it( 'should have proper parent path', function() {
+		var position = new Position( [ 1, 2, 3 ], root );
+
+		expect( position.getParentPath() ).to.deep.equal( [ 1, 2 ] );
+	} );
+} );

+ 137 - 0
packages/ckeditor5-engine/tests/document/positioniterator.js

@@ -0,0 +1,137 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document */
+
+'use strict';
+
+var modules = bender.amd.require(
+	'document/document',
+	'document/element',
+	'document/character',
+	'document/positioniterator',
+	'document/position',
+	'document/range'
+);
+
+describe( 'range iterator', function() {
+	var Document, Element, Character, PositionIterator, Position, Range;
+
+	var doc, expectedItems, root, img1, paragraph, b, a, r, img2, x;
+	var ELEMENT_ENTER, ELEMENT_LEAVE, CHARACTER;
+
+	before( function() {
+		Document = modules[ 'document/document' ];
+		Element = modules[ 'document/element' ];
+		Character = modules[ 'document/character' ];
+		PositionIterator = modules[ 'document/positioniterator' ];
+		Position = modules[ 'document/position' ];
+		Range = modules[ 'document/range' ];
+
+		ELEMENT_ENTER = PositionIterator.ELEMENT_ENTER;
+		ELEMENT_LEAVE = PositionIterator.ELEMENT_LEAVE;
+		CHARACTER = PositionIterator.CHARACTER;
+
+		doc = new Document();
+		root = doc.createRoot( 'root' );
+
+		// root
+		//  |- img1
+		//  |- p
+		//     |- B
+		//     |- A
+		//     |- R
+		//     |
+		//     |- img2
+		//     |
+		//     |- X
+
+		b = new Character( 'b' );
+		a = new Character( 'a' );
+		r = new Character( 'r' );
+		img2 = new Element( 'img2' );
+		x = new Character( 'x' );
+
+		paragraph = new Element( 'p', [], [ b, a, r, img2, x ] );
+		img1 = new Element( 'img1' );
+
+		root.insertChildren( 0, [ img1, paragraph ] );
+
+		expectedItems = [
+			{ type: ELEMENT_ENTER, node: img1 },
+			{ type: ELEMENT_LEAVE, node: img1 },
+			{ type: ELEMENT_ENTER, node: paragraph },
+			{ type: CHARACTER, node: b },
+			{ type: CHARACTER, node: a },
+			{ type: CHARACTER, node: r },
+			{ type: ELEMENT_ENTER, node: img2 },
+			{ type: ELEMENT_LEAVE, node: img2 },
+			{ type: CHARACTER, node: x },
+			{ type: ELEMENT_LEAVE, node: paragraph }
+		];
+	} );
+
+	it( 'should return next position', function() {
+		var iterator = new PositionIterator( new Position( [ 0 ], root ) ); // beginning of root
+		var i, len;
+
+		for ( i = 0, len = expectedItems.length; i < len; i++ ) {
+			expect( iterator.next() ).to.deep.equal( { done: false, value: expectedItems[ i ] } );
+		}
+		expect( iterator.next() ).to.have.property( 'done' ).that.is.true;
+	} );
+
+	it( 'should return previous position', function() {
+		var iterator = new PositionIterator( new Position( [ 2 ], root ) ); // ending of root
+
+		for ( var i = expectedItems.length - 1; i >= 0; i-- ) {
+			expect( iterator.previous() ).to.deep.equal( { done: false, value: expectedItems[ i ] } );
+		}
+		expect( iterator.previous() ).to.have.property( 'done' ).that.is.true;
+	} );
+
+	it( 'should return next position in the boundaries', function() {
+		var start = new Position( [ 1, 0 ], root ); // p, 0
+		var end = new Position( [ 1, 3, 0 ], root ); // img, 0
+
+		var iterator = new PositionIterator( new Range( start, end ) );
+
+		var i, len;
+
+		for ( i = 3, len = expectedItems.length; i < 7; i++ ) {
+			expect( iterator.next() ).to.deep.equal( { done: false, value: expectedItems[ i ] } );
+		}
+		expect( iterator.next() ).to.have.property( 'done' ).that.is.true;
+	} );
+
+	it( 'should return previous position in the boundaries', function() {
+		var start = new Position( [ 1, 0 ], root ); // p, 0
+		var end = new Position( [ 1, 3, 0 ], root ); // img, 0
+
+		var iterator = new PositionIterator( new Range( start, end ), end );
+
+		var i, len;
+
+		for ( i = 6, len = expectedItems.length; i > 2; i-- ) {
+			expect( iterator.previous() ).to.deep.equal( { done: false, value: expectedItems[ i ] } );
+		}
+		expect( iterator.previous() ).to.have.property( 'done' ).that.is.true;
+	} );
+
+	it( 'should return iterate over the range', function() {
+		var start = new Position( [ 0 ], root ); // begging of root
+		var end = new Position( [ 2 ], root ); // ending of root
+		var range = new Range( start, end );
+
+		var i = 0;
+		var value;
+
+		for ( value of range ) {
+			expect( value ).to.deep.equal( expectedItems[ i ] );
+			i++;
+		}
+		expect( i ).to.equal( expectedItems.length );
+	} );
+} );

+ 69 - 0
packages/ckeditor5-engine/tests/document/range.js

@@ -0,0 +1,69 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document */
+
+'use strict';
+
+var modules = bender.amd.require(
+	'document/range',
+	'document/position'
+);
+
+describe( 'Range', function() {
+	var Range, Position, start, end;
+
+	before( function() {
+		Position = modules[ 'document/position' ];
+		Range = modules[ 'document/range' ];
+
+		start = new Position( [ 0 ] );
+		end = new Position( [ 1 ] );
+	} );
+
+	var range;
+
+	beforeEach( function() {
+		range = new Range( start, end );
+	} );
+
+	describe( 'constructor', function() {
+		it( 'should create a range with given positions', function() {
+			expect( range ).to.have.property( 'start' ).that.equal( start );
+			expect( range ).to.have.property( 'end' ).that.equal( end );
+		} );
+	} );
+
+	describe( 'isEqual', function() {
+		it( 'should return true if the ranges are the same', function() {
+			var sameStart = new Position( [ 0 ] );
+			var sameEnd = new Position( [ 1 ] );
+
+			var sameRange = new Range( sameStart, sameEnd );
+
+			expect( range.isEqual( sameRange ) ).to.be.true;
+		} );
+
+		it( 'should return false if the start position is different', function() {
+			var range = new Range( start, end );
+
+			var diffStart = new Position( [ 1 ] );
+			var sameEnd = new Position( [ 1 ] );
+
+			var diffRange = new Range( diffStart, sameEnd );
+
+			expect( range.isEqual( diffRange ) ).to.not.be.true;
+		} );
+
+		it( 'should return false if the end position is different', function() {
+			var sameStart = new Position( [ 0 ] );
+			var diffEnd = new Position( [ 0 ] );
+
+			var diffRange = new Range( sameStart, diffEnd );
+
+			expect( range.isEqual( diffRange ) ).to.not.be.true;
+		} );
+	} );
+} );

+ 38 - 0
packages/ckeditor5-engine/tests/document/rootelement.js

@@ -0,0 +1,38 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* jshint expr: true */
+
+/* bender-tags: document */
+
+'use strict';
+
+var modules = bender.amd.require(
+	'document/document',
+	'document/element',
+	'document/rootelement'
+);
+
+describe( 'Element', function() {
+	var Document, Element, RootElement;
+
+	before( function() {
+		Document = modules[ 'document/document' ];
+		Element = modules[ 'document/element' ];
+		RootElement = modules[ 'document/rootelement' ];
+	} );
+
+	describe( 'constructor', function() {
+		it( 'should create root element without attributes', function() {
+			var doc = new Document();
+			var root = new RootElement( doc );
+
+			expect( root ).to.be.an.instanceof( Element );
+			expect( root ).to.have.property( 'document' ).that.equals( doc );
+			expect( root._getAttrCount() ).to.equal( 0 );
+			expect( root.getChildCount() ).to.equal( 0 );
+		} );
+	} );
+} );

+ 31 - 0
packages/ckeditor5-engine/tests/document/text.js

@@ -0,0 +1,31 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* jshint expr: true */
+
+/* bender-tags: document */
+
+'use strict';
+
+var modules = bender.amd.require(
+	'document/text',
+	'document/attribute'
+);
+
+describe( 'Text', function() {
+	describe( 'constructor', function() {
+		it( 'should create character without attributes', function() {
+			var Text = modules[ 'document/text' ];
+			var Attribute = modules[ 'document/attribute' ];
+
+			var attrs = [ new Attribute( 'bold', true ) ];
+			var text = new Text( 'bar', attrs );
+
+			expect( text ).to.have.property( 'text' ).that.equals( 'bar' );
+			expect( text ).to.have.property( 'attrs' ).that.is.an( 'array' );
+			expect( text.attrs ).to.be.deep.equals( attrs );
+		} );
+	} );
+} );

+ 136 - 58
packages/ckeditor5-engine/tests/utils/utils.js

@@ -7,80 +7,158 @@
 
 
 var modules = bender.amd.require( 'utils', 'utils-lodash' );
 var modules = bender.amd.require( 'utils', 'utils-lodash' );
 
 
-describe( 'extend()', function() {
-	// Properties of the subsequent objects should override properties of the preceding objects. This is critical for
-	// CKEditor so we keep this test to ensure that Lo-Dash (or whatever) implements it in the way we need it.
-	it( 'should extend by several params in the correct order', function() {
-		var utils = modules.utils;
-
-		var target = {
-			a: 0,
-			b: 0
-		};
-
-		var ext1 = {
-			b: 1,
-			c: 1
-		};
-
-		var ext2 = {
-			c: 2,
-			d: 2
-		};
-
-		utils.extend( target, ext1, ext2 );
-
-		expect( target ).to.have.property( 'a' ).to.equal( 0 );
-		expect( target ).to.have.property( 'b' ).to.equal( 1 );
-		expect( target ).to.have.property( 'c' ).to.equal( 2 );
-		expect( target ).to.have.property( 'd' ).to.equal( 2 );
+describe( 'utils', function() {
+	var utils;
+
+	before( function() {
+		utils = modules.utils;
 	} );
 	} );
-} );
 
 
-describe( 'spy', function() {
-	it( 'should not have `called` after creation', function() {
-		var utils = modules.utils;
+	describe( 'extend()', function() {
+		// Properties of the subsequent objects should override properties of the preceding objects. This is critical for
+		// CKEditor so we keep this test to ensure that Lo-Dash (or whatever) implements it in the way we need it.
+		it( 'should extend by several params in the correct order', function() {
+			var target = {
+				a: 0,
+				b: 0
+			};
+
+			var ext1 = {
+				b: 1,
+				c: 1
+			};
+
+			var ext2 = {
+				c: 2,
+				d: 2
+			};
+
+			utils.extend( target, ext1, ext2 );
+
+			expect( target ).to.have.property( 'a' ).to.equal( 0 );
+			expect( target ).to.have.property( 'b' ).to.equal( 1 );
+			expect( target ).to.have.property( 'c' ).to.equal( 2 );
+			expect( target ).to.have.property( 'd' ).to.equal( 2 );
+		} );
+	} );
+
+	describe( 'spy', function() {
+		it( 'should not have `called` after creation', function() {
+			var spy = utils.spy();
+
+			expect( spy.called ).to.not.be.true();
+		} );
+
+		it( 'should register calls', function() {
+			var fn1 = utils.spy();
+			var fn2 = utils.spy();
+
+			fn1();
+
+			expect( fn1.called ).to.be.true();
+			expect( fn2.called ).to.not.be.true();
+		} );
+	} );
 
 
-		var spy = utils.spy();
+	describe( 'uid', function() {
+		it( 'should return different ids', function() {
+			var id1 = utils.uid();
+			var id2 = utils.uid();
+			var id3 = utils.uid();
 
 
-		expect( spy.called ).to.not.be.true();
+			expect( id1 ).to.be.a( 'number' );
+			expect( id2 ).to.be.a( 'number' ).to.not.equal( id1 ).to.not.equal( id3 );
+			expect( id3 ).to.be.a( 'number' ).to.not.equal( id1 ).to.not.equal( id2 );
+		} );
 	} );
 	} );
 
 
-	it( 'should register calls', function() {
-		var utils = modules.utils;
+	describe( 'isIterable', function() {
+		it( 'should be true for string', function() {
+			var string = 'foo';
+
+			expect( utils.isIterable( string ) ).to.be.true;
+		} );
+
+		it( 'should be true for arrays', function() {
+			var array = [ 1, 2, 3 ];
+
+			expect( utils.isIterable( array ) ).to.be.true;
+		} );
+
+		it( 'should be true for iterable classes', function() {
+			class IterableClass {
+				constructor() {
+					this.array = [ 1, 2, 3 ];
+				}
+
+				[ Symbol.iterator ]() {
+					return this.array[ Symbol.iterator ]();
+				}
+			}
 
 
-		var fn1 = utils.spy();
-		var fn2 = utils.spy();
+			var instance = new IterableClass();
 
 
-		fn1();
+			expect( utils.isIterable( instance ) ).to.be.true;
+		} );
+
+		it( 'should be false for not iterable objects', function() {
+			var notIterable = { foo: 'bar' };
 
 
-		expect( fn1.called ).to.be.true();
-		expect( fn2.called ).to.not.be.true();
+			expect( utils.isIterable( notIterable ) ).to.be.false;
+		} );
+
+		it( 'should be false for undefined', function() {
+			expect( utils.isIterable() ).to.be.false;
+		} );
 	} );
 	} );
-} );
 
 
-describe( 'uid', function() {
-	it( 'should return different ids', function() {
-		var utils = modules.utils;
+	describe( 'compareArrays', function() {
+		it( 'should return SAME flag, when arrays are same', function() {
+			var a = [ 'abc', 0, 3 ];
+			var b = [ 'abc', 0, 3 ];
+
+			var result = utils.compareArrays( a, b );
+
+			expect( result ).to.equal( utils.compareArrays.SAME );
+		} );
+
+		it( 'should return PREFIX flag, when all n elements of first array are same as n first elements of the second array', function() {
+			var a = [ 'abc', 0 ];
+			var b = [ 'abc', 0, 3 ];
 
 
-		var id1 = utils.uid();
-		var id2 = utils.uid();
-		var id3 = utils.uid();
+			var result = utils.compareArrays( a, b );
 
 
-		expect( id1 ).to.be.a( 'number' );
-		expect( id2 ).to.be.a( 'number' ).to.not.equal( id1 ).to.not.equal( id3 );
-		expect( id3 ).to.be.a( 'number' ).to.not.equal( id1 ).to.not.equal( id2 );
+			expect( result ).to.equal( utils.compareArrays.PREFIX );
+		} );
+
+		it( 'should return EXTENSION flag, when n first elements of first array are same as all elements of the second array', function() {
+			var a = [ 'abc', 0, 3 ];
+			var b = [ 'abc', 0 ];
+
+			var result = utils.compareArrays( a, b );
+
+			expect( result ).to.equal( utils.compareArrays.EXTENSION );
+		} );
+
+		it( 'should return DIFFERENT flag, when arrays are not same', function() {
+			var a = [ 'abc', 0, 3 ];
+			var b = [ 'abc', 1, 3 ];
+
+			var result = utils.compareArrays( a, b );
+
+			expect( result ).to.equal( utils.compareArrays.DIFFERENT );
+		} );
 	} );
 	} );
-} );
 
 
-describe( 'Lo-Dash extensions', function() {
-	// Ensures that the required Lo-Dash extensions are available in `utils`.
-	it( 'should be exposed in utils', function() {
-		var utils = modules.utils;
-		var extensions = modules[ 'utils-lodash' ];
+	describe( 'Lo-Dash extensions', function() {
+		// Ensures that the required Lo-Dash extensions are available in `utils`.
+		it( 'should be exposed in utils', function() {
+			var utils = modules.utils;
+			var extensions = modules[ 'utils-lodash' ];
 
 
-		extensions.forEach( function( extension ) {
-			expect( utils ).to.have.property( extension ).to.not.be.undefined();
+			extensions.forEach( function( extension ) {
+				expect( utils ).to.have.property( extension ).to.not.be.undefined();
+			} );
 		} );
 		} );
 	} );
 	} );
 } );
 } );

部分文件因为文件数量过多而无法显示