Browse Source

Merge pull request #492 from ckeditor/t/255

View TreeWalker
Piotr Jasiun 9 years ago
parent
commit
dbdab7db3b

+ 8 - 5
packages/ckeditor5-engine/src/model/treewalker.js

@@ -65,8 +65,8 @@ export default class TreeWalker {
 		/**
 		 * 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.
+		 * When the iterator is walking `FORWARD` on the end of boundary or is walking `BACKWARD`
+		 * on the start of boundary, then `{ done: true }` is returned.
 		 *
 		 * If boundaries are not defined they are set before first and after last child of the root node.
 		 *
@@ -77,7 +77,9 @@ export default class TreeWalker {
 
 		/**
 		 * Iterator position. This is always static position, even if the initial position was a
-		 * {@link engine.model.LivePosition live position}.
+		 * {@link engine.model.LivePosition live position}. If start position is not defined then position depends
+		 * on {@link #direction}. If direction is `FORWARD` position starts form the beginning, when direction
+		 * is `BACKWARD` position starts from the end.
 		 *
 		 * @readonly
 		 * @member {engine.model.Position} engine.model.TreeWalker#position
@@ -243,7 +245,7 @@ export default class TreeWalker {
 	 * @private
 	 * @returns {Object}
 	 * @returns {Boolean} return.done True if iterator is done.
-	 * @returns {core.model.TreeWalkerValue} return.value Information about taken step.
+	 * @returns {engine.model.TreeWalkerValue} return.value Information about taken step.
 	 */
 	_previous() {
 		const previousPosition = this.position;
@@ -304,6 +306,7 @@ export default class TreeWalker {
 				return formatReturnValue( 'TEXT', textFragment, previousPosition, position, charactersCount );
 			}
 		} else {
+			// `node` is not set, we reached the beginning of current `parent`.
 			position.path.pop();
 			this.position = position;
 			this._visitedParent = parent.parent;
@@ -361,5 +364,5 @@ function formatReturnValue( type, item, previousPosition, nextPosition, length )
 /**
  * Tree walking directions.
  *
- * @typedef {'FORWARD'|'BACKWARD'} core.model.TreeWalkerDirection
+ * @typedef {'FORWARD'|'BACKWARD'} engine.view.TreeWalkerDirection
  */

+ 13 - 2
packages/ckeditor5-engine/src/view/position.js

@@ -6,6 +6,7 @@
 'use strict';
 
 import Text from './text.js';
+import TextProxy from './textproxy.js';
 
 import compareArrays from '../../utils/comparearrays.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
@@ -209,10 +210,15 @@ export default class Position {
 	/**
 	 * Creates a new position after the given node.
 	 *
-	 * @param {engine.view.Node} node Node after which the position should be located.
+	 * @param {engine.view.Node|engine.view.TextProxy} node Node or text proxy after which the position should be located.
 	 * @returns {engine.view.Position}
 	 */
 	static createAfter( node ) {
+		// {@link engine.view.TextProxy} is not a instance of {@link engine.view.Node} so we need do handle it in specific way.
+		if ( node instanceof TextProxy ) {
+			return new Position( node.textNode, node.index + node.data.length );
+		}
+
 		if ( !node.parent ) {
 			/**
 			 * You can not make a position after a root.
@@ -229,10 +235,15 @@ export default class Position {
 	/**
 	 * Creates a new position before the given node.
 	 *
-	 * @param {engine.view.node} node Node before which the position should be located.
+	 * @param {engine.view.Node|engine.view.TextProxy} node Node or text proxy before which the position should be located.
 	 * @returns {engine.view.Position}
 	 */
 	static createBefore( node ) {
+		// {@link engine.view.TextProxy} is not a instance of {@link engine.view.Node} so we need do handle it in specific way.
+		if ( node instanceof TextProxy ) {
+			return new Position( node.textNode, node.index );
+		}
+
 		if ( !node.parent ) {
 			/**
 			 * You cannot make a position before a root.

+ 105 - 0
packages/ckeditor5-engine/src/view/textproxy.js

@@ -0,0 +1,105 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+/**
+ * TextProxy is a wrapper for substring of {@link engine.view.Text}. Instance of this class is created by
+ * {@link engine.view.TreeWalker} when only a part of {@link engine.view.Text} needs to be returned.
+ *
+ * **Note:** TextProxy instances are created on the fly basing on the current state of parent {@link engine.view.Text}.
+ * Because of this it is highly unrecommended to store references to TextProxy instances because they might get
+ * invalidated due to operations on Document. Also TextProxy is not a {@link engine.view.Node} so it can not be
+ * inserted as a child of {@link engine.view.Element}.
+ *
+ * You should never create an instance of this class by your own. Instead, use string literals or {@link engine.model.Text}.
+ *
+ * @memberOf engine.view
+ */
+export default class TextProxy {
+	/**
+	 * Creates a tree view text proxy.
+	 *
+	 * @param {engine.view.Text} textNode Text node which text proxy is a substring.
+	 * @param {Number} startOffset Offset from beginning of {#textNode} and first character of {#data}.
+	 * @param {Number} [length] Length of substring. If is not set then the end offset is at the end of {#textNode}.
+	 */
+	constructor( textNode, startOffset, length ) {
+		/**
+		 * Element that is a parent of this text proxy.
+		 *
+		 * @readonly
+		 * @member {engine.view.Element|engine.view.DocumentFragment|null} engine.view.Node#parent
+		 */
+		this.parent = textNode.parent;
+
+		/**
+		 * Reference to the {@link engine.view.Text} element which TextProxy is a substring.
+		 *
+		 * @readonly
+		 * @member {engine.view.Text} engine.view.TextProxy#textNode
+		 */
+		this.textNode = textNode;
+
+		/**
+		 * Index of the substring in the `textParent`.
+		 *
+		 * @readonly
+		 * @member {Number} engine.view.TextProxy#index
+		 */
+		this.index = startOffset;
+
+		/**
+		 * The text content.
+		 *
+		 * @readonly
+		 * @member {String} engine.view.TextProxy#data
+		 */
+		this.data = textNode.data.substring(
+			startOffset,
+			startOffset + ( length || textNode.data.length - startOffset )
+		);
+	}
+
+	/**
+	 * Gets {@link engine.view.Document} reference, from the {@link engine.view.Node#getRoot root} of
+	 * {#textNode} or returns null if the root has no reference to the {@link engine.view.Document}.
+	 *
+	 * @returns {engine.view.Document|null} View Document of the text proxy or null.
+	 */
+	getDocument() {
+		return this.textNode.getDocument();
+	}
+
+	/**
+	 * Gets the top parent for the {#textNode}. If there is no parent {#textNode} is the root.
+	 *
+	 * @returns {engine.view.Node}
+	 */
+	getRoot() {
+		return this.textNode.getRoot();
+	}
+
+	/**
+	 * Returns ancestors array of this text proxy.
+	 *
+	 * @param {Object} options Options object.
+	 * @param {Boolean} [options.includeNode=false] When set to `true` {#textNode} will be also included in parent's array.
+	 * @param {Boolean} [options.parentFirst=false] When set to `true`, array will be sorted from text proxy parent to
+	 * root element, otherwise root element will be the first item in the array.
+	 * @returns {Array} Array with ancestors.
+	 */
+	getAncestors( options = { includeNode: false, parentFirst: false } ) {
+		const ancestors = [];
+		let parent = options.includeNode ? this.textNode : this.parent;
+
+		while ( parent !== null ) {
+			ancestors[ options.parentFirst ? 'push' : 'unshift' ]( parent );
+			parent = parent.parent;
+		}
+
+		return ancestors;
+	}
+}

+ 448 - 0
packages/ckeditor5-engine/src/view/treewalker.js

@@ -0,0 +1,448 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import Element from './element.js';
+import Text from './text.js';
+import TextProxy from './textproxy.js';
+import Position from './position.js';
+import CKEditorError from '../../utils/ckeditorerror.js';
+
+/**
+ * Position iterator class. It allows to iterate forward and backward over the document.
+ *
+ * @memberOf engine.view
+ */
+export default class TreeWalker {
+	/**
+	 * Creates a range iterator. All parameters are optional, but you have to specify either `boundaries` or `startPosition`.
+	 *
+	 * @constructor
+	 * @param {Object} options Object with configuration.
+	 * @param {engine.view.Range} [options.boundaries=null] Range to define boundaries of the iterator.
+	 * @param {engine.view.Position} [options.startPosition] Starting position.
+	 * @param {'FORWARD'|'BACKWARD'} [options.direction='FORWARD'] Walking direction.
+	 * @param {Boolean} [options.singleCharacters=false] Flag indicating whether all characters from
+	 * {@link engine.view.Text} should be returned as one {@link engine.view.Text} (`false`) ore one by one as
+	 * {@link engine.view.TextProxy} (`true`).
+	 * @param {Boolean} [options.shallow=false] Flag indicating whether iterator should enter elements or not. If the
+	 * iterator is shallow child nodes of any iterated node will not be returned along with `ELEMENT_END` tag.
+	 * @param {Boolean} [options.ignoreElementEnd=false] Flag indicating whether iterator should ignore `ELEMENT_END`
+	 * tags. If the option is true walker will not return a parent node of start position. If this option is `true`
+	 * each {@link engine.view.Element} will be returned once, while if the option is `false` they might be returned
+	 * twice: for `'ELEMENT_START'` and `'ELEMENT_END'`.
+	 */
+	constructor( options = {} ) {
+		if ( !options.boundaries && !options.startPosition ) {
+			/**
+			 * Neither boundaries nor starting position have been defined.
+			 *
+			 * @error tree-walker-no-start-position
+			 */
+			throw new CKEditorError( 'tree-walker-no-start-position: Neither boundaries nor starting position have been defined.' );
+		}
+
+		if ( options.direction && options.direction != 'FORWARD' && options.direction != 'BACKWARD' ) {
+			throw new CKEditorError(
+				'tree-walker-unknown-direction: Only `BACKWARD` and `FORWARD` direction allowed.',
+				{ direction: options.direction }
+			);
+		}
+
+		/**
+		 * Iterator boundaries.
+		 *
+		 * When the iterator is walking `FORWARD` on the end of boundary or is walking `BACKWARD`
+		 * on the start of boundary, then `{ done: true }` is returned.
+		 *
+		 * If boundaries are not defined they are set before first and after last child of the root node.
+		 *
+		 * @readonly
+		 * @member {engine.view.Range} engine.view.TreeWalker#boundaries
+		 */
+		this.boundaries = options.boundaries || null;
+
+		/**
+		 * Iterator position. If start position is not defined then position depends on {@link #direction}. If direction is
+		 * `FORWARD` position starts form the beginning, when direction is `BACKWARD` position starts from the end.
+		 *
+		 * @readonly
+		 * @member {engine.view.Position} engine.view.TreeWalker#position
+		 */
+		if ( options.startPosition ) {
+			this.position = Position.createFromPosition( options.startPosition );
+		} else {
+			this.position = Position.createFromPosition( options.boundaries[ options.direction == 'BACKWARD' ? 'end' : 'start' ] );
+		}
+
+		/**
+		 * Walking direction. Defaults `FORWARD`.
+		 *
+		 * @readonly
+		 * @member {'BACKWARD'|'FORWARD'} engine.view.TreeWalker#direction
+		 */
+		this.direction = options.direction || 'FORWARD';
+
+		/**
+		 * Flag indicating whether all characters from {@link engine.view.Text} should be returned as one
+		 * {@link engine.view.Text} or one by one as {@link.engine.TextProxy}.
+		 *
+		 * @readonly
+		 * @member {Boolean} engine.view.TreeWalker#singleCharacters
+		 */
+		this.singleCharacters = !!options.singleCharacters;
+
+		/**
+		 * Flag indicating whether iterator should enter elements or not. If the iterator is shallow child nodes of any
+		 * iterated node will not be returned along with `ELEMENT_END` tag.
+		 *
+		 * @readonly
+		 * @member {Boolean} engine.view.TreeWalker#shallow
+		 */
+		this.shallow = !!options.shallow;
+
+		/**
+		 * Flag indicating whether iterator should ignore `ELEMENT_END` tags. If the option is true walker will not
+		 * return a parent node of the start position. If this option is `true` each {@link engine.view.Element} will
+		 * be returned once, while if the option is `false` they might be returned twice:
+		 * for `'ELEMENT_START'` and `'ELEMENT_END'`.
+		 *
+		 * @readonly
+		 * @member {Boolean} engine.view.TreeWalker#ignoreElementEnd
+		 */
+		this.ignoreElementEnd = !!options.ignoreElementEnd;
+
+		/**
+		 * Start boundary cached for optimization purposes.
+		 *
+		 * @private
+		 * @member {engine.view.Element} engine.view.TreeWalker#_boundaryStartParent
+		 */
+		this._boundaryStartParent = this.boundaries ? this.boundaries.start.parent : null;
+
+		/**
+		 * End boundary cached for optimization purposes.
+		 *
+		 * @private
+		 * @member {engine.view.Element} engine.view.TreeWalker#_boundaryEndParent
+		 */
+		this._boundaryEndParent = this.boundaries ? this.boundaries.end.parent : null;
+	}
+
+	/**
+	 * Iterator interface.
+	 */
+	[ Symbol.iterator ]() {
+		return this;
+	}
+
+	/**
+	 * Iterator interface method.
+	 * Detects walking direction and makes step forward or backward.
+	 *
+	 * @returns {Object} Object implementing iterator interface, returning information about taken step.
+	 */
+	next() {
+		if ( this.direction == 'FORWARD' ) {
+			return this._next();
+		} else {
+			return this._previous();
+		}
+	}
+
+	/**
+	 * Makes a step forward in view. Moves the {@link #position} to the next position and returns the encountered value.
+	 *
+	 * @private
+	 * @returns {Object}
+	 * @returns {Boolean} return.done True if iterator is done.
+	 * @returns {engine.view.TreeWalkerValue} return.value Information about taken step.
+	 */
+	_next() {
+		let position = Position.createFromPosition( this.position );
+		const previousPosition = this.position;
+		const parent = position.parent;
+
+		// We are at the end of the root.
+		if ( parent.parent === null && position.offset === parent.getChildCount() ) {
+			return { done: true };
+		}
+
+		// We reached the walker boundary.
+		if ( parent === this._boundaryEndParent && position.offset == this.boundaries.end.offset ) {
+			return { done: true };
+		}
+
+		// Get node just after current position.
+		let node;
+
+		// Text is a specific parent because it contains string instead of child nodes.
+		if ( parent instanceof Text ) {
+			node = parent.data[ position.offset ];
+		} else {
+			node = parent.getChild( position.offset );
+		}
+
+		if ( node instanceof Element ) {
+			if ( !this.shallow ) {
+				position = new Position( node, 0 );
+			} else {
+				position.offset++;
+			}
+
+			this.position = position;
+
+			return this._formatReturnValue( 'ELEMENT_START', node, previousPosition, position, 1 );
+		} else if ( node instanceof Text ) {
+			if ( this.singleCharacters ) {
+				position = new Position( node, 0 );
+				this.position = position;
+
+				return this._next();
+			} else {
+				let charactersCount = node.data.length;
+				let item = node;
+
+				// If text stick out of walker range, we need to cut it and wrap by TextProxy.
+				if ( node == this._boundaryEndParent ) {
+					charactersCount = this.boundaries.end.offset;
+					item = new TextProxy( node, 0, charactersCount );
+					position = Position.createAfter( item );
+				} else {
+					// If not just keep moving forward.
+					position.offset++;
+				}
+
+				this.position = position;
+
+				return this._formatReturnValue( 'TEXT', item, previousPosition, position, charactersCount );
+			}
+		} else if ( typeof node == 'string' ) {
+			let textLength;
+
+			if ( this.singleCharacters ) {
+				textLength = 1;
+			} else {
+				// Check if text stick out of walker range.
+				const endOffset = parent === this._boundaryEndParent ? this.boundaries.end.offset : parent.data.length;
+
+				textLength = endOffset - position.offset;
+			}
+
+			const textProxy = new TextProxy( parent, position.offset, textLength );
+
+			position.offset += textLength;
+			this.position = position;
+
+			return this._formatReturnValue( 'TEXT', textProxy, previousPosition, position, textLength );
+		} else {
+			// `node` is not set, we reached the end of current `parent`.
+			position = Position.createAfter( parent );
+			this.position = position;
+
+			if ( this.ignoreElementEnd ) {
+				return this._next();
+			} else {
+				return this._formatReturnValue( 'ELEMENT_END', parent, previousPosition, position );
+			}
+		}
+	}
+
+	/**
+	 * Makes a step backward in view. Moves the {@link #position} to the previous position and returns the encountered value.
+	 *
+	 * @private
+	 * @returns {Object}
+	 * @returns {Boolean} return.done True if iterator is done.
+	 * @returns {engine.view.TreeWalkerValue} return.value Information about taken step.
+	 */
+	_previous() {
+		let position = Position.createFromPosition( this.position );
+		const previousPosition = this.position;
+		const parent = position.parent;
+
+		// We are at the beginning of the root.
+		if ( parent.parent === null && position.offset === 0 ) {
+			return { done: true };
+		}
+
+		// We reached the walker boundary.
+		if ( parent == this._boundaryStartParent && position.offset == this.boundaries.start.offset ) {
+			return { done: true };
+		}
+
+		// Get node just before current position.
+		let node;
+
+		// Text {@link engine.view.Text} element is a specific parent because contains string instead of child nodes.
+		if ( parent instanceof Text ) {
+			node = parent._data[ position.offset - 1 ];
+		} else {
+			node = parent.getChild( position.offset - 1 );
+		}
+
+		if ( node instanceof Element ) {
+			if ( !this.shallow ) {
+				position = new Position( node, node.getChildCount() );
+				this.position = position;
+
+				if ( this.ignoreElementEnd ) {
+					return this._previous();
+				} else {
+					return this._formatReturnValue( 'ELEMENT_END', node, previousPosition, position );
+				}
+			} else {
+				position.offset--;
+				this.position = position;
+
+				return this._formatReturnValue( 'ELEMENT_START', node, previousPosition, position, 1 );
+			}
+		} else if ( node instanceof Text ) {
+			if ( this.singleCharacters ) {
+				position = new Position( node, node.data.length );
+				this.position = position;
+
+				return this._previous();
+			} else {
+				let charactersCount = node.data.length;
+				let item = node;
+
+				// If text stick out of walker range, we need to cut it and wrap by TextProxy.
+				if ( node == this._boundaryStartParent ) {
+					const offset = this.boundaries.start.offset;
+
+					item = new TextProxy( node, offset );
+					charactersCount = item.data.length;
+					position = Position.createBefore( item );
+				} else {
+					// If not just keep moving backward.
+					position.offset--;
+				}
+
+				this.position = position;
+
+				return this._formatReturnValue( 'TEXT', item, previousPosition, position, charactersCount );
+			}
+		} else if ( typeof node == 'string' ) {
+			let textLength;
+
+			if ( !this.singleCharacters ) {
+				// Check if text stick out of walker range.
+				const startOffset = parent === this._boundaryStartParent ? this.boundaries.start.offset : 0;
+
+				textLength = position.offset - startOffset;
+			} else {
+				textLength = 1;
+			}
+
+			position.offset -= textLength;
+
+			const textProxy = new TextProxy( parent, position.offset, textLength );
+
+			this.position = position;
+
+			return this._formatReturnValue( 'TEXT', textProxy, previousPosition, position, textLength );
+		} else {
+			// `node` is not set, we reached the beginning of current `parent`.
+			position = Position.createBefore( parent );
+			this.position = position;
+
+			return this._formatReturnValue( 'ELEMENT_START', parent, previousPosition, position, 1 );
+		}
+	}
+
+	/**
+	 * Format returned data and adjust `previousPosition` and `nextPosition` if reach the bound of the {@link engine.view.Text}.
+	 *
+	 * @private
+	 * @param {engine.view.TreeWalkerValueType} type Type of step.
+	 * @param {engine.view.Item} item Item between old and new position.
+	 * @param {engine.view.Position} previousPosition Previous position of iterator.
+	 * @param {engine.view.Position} nextPosition Next position of iterator.
+	 * @param {Number} [length] Length of the item.
+	 * @returns {engine.view.TreeWalkerValue}
+	 */
+	_formatReturnValue( type, item, previousPosition, nextPosition, length ) {
+		// Text is a specific parent, because contains string instead of childs.
+		// Walker doesn't enter to the Text except situations when walker is iterating over every single character,
+		// or the bound starts/ends inside the Text. So when the position is at the beginning or at the end of the Text
+		// we move it just before or just after Text.
+		if ( item instanceof TextProxy ) {
+			// Position is at the end of Text.
+			if ( item.index + item.data.length == item.textNode.data.length ) {
+				if ( this.direction == 'FORWARD' ) {
+					nextPosition = Position.createAfter( item.textNode );
+					// When we change nextPosition of returned value we need also update walker current position.
+					this.position = nextPosition;
+				} else {
+					previousPosition = Position.createAfter( item.textNode );
+				}
+			}
+
+			// Position is at the begining ot the text.
+			if ( item.index === 0 ) {
+				if ( this.direction == 'FORWARD' ) {
+					previousPosition = Position.createBefore( item.textNode );
+				} else {
+					nextPosition = Position.createBefore( item.textNode );
+					// When we change nextPosition of returned value we need also update walker current position.
+					this.position = nextPosition;
+				}
+			}
+		}
+
+		return {
+			done: false,
+			value: {
+				type: type,
+				item: item,
+				previousPosition: previousPosition,
+				nextPosition: nextPosition,
+				length: length
+			}
+		};
+	}
+}
+
+/**
+ * Type of the step made by {@link engine.view.TreeWalker}.
+ * Possible values: `'ELEMENT_START'` if walker is at the beginning of a node, `'ELEMENT_END'` if walker is at the end
+ * of node, or `'TEXT'` if walker traversed over single and multiple characters.
+ * For {@link engine.view.Text} `ELEMENT_START` and `ELEMENT_END` is not returned.
+ *
+ * @typedef {String} engine.view.TreeWalkerValueType
+ */
+
+/**
+ * Object returned by {@link engine.view.TreeWalker} when traversing tree view.
+ *
+ * @typedef {Object} engine.view.TreeWalkerValue
+ * @property {engine.view.TreeWalkerValueType} type
+ * @property {engine.view.Item} item Item between old and new positions of {@link engine.view.TreeWalker}.
+ * @property {engine.view.Position} previousPosition Previous position of the iterator.
+ * * Forward iteration: For `'ELEMENT_END'` it is the last position inside the element. For all other types it is the
+ * position before the item. Note that it is more efficient to use this position then calculate the position before
+ * the node using {@link engine.view.Position.createBefore}.
+ * * Backward iteration: For `'ELEMENT_START'` it is the first position inside the element. For all other types it is
+ * the position after item.
+ * * If the position is at the beginning or at the end of the {@link engine.view.Text} it is always moved from the
+ * inside of the Text to its parent just before or just after Text.
+ * @property {engine.view.Position} nextPosition Next position of the iterator.
+ * * Forward iteration: For `'ELEMENT_START'` it is the first position inside the element. For all other types it is
+ * the position after the item.
+ * * Backward iteration: For `'ELEMENT_END'` it is last position inside element. For all other types it is the position
+ * before the item.
+ * * If the position is at the beginning or at the end of the {@link engine.view.Text} it is always moved from the
+ * inside of the Text to its parent just before or just after Text.
+ * @property {Number} [length] Length of the item. For `'ELEMENT_START'` it is 1. For `'TEXT'` it is
+ * the length of the text. For `'ELEMENT_END'` it is undefined.
+ */
+
+/**
+ * Tree walking directions.
+ *
+ * @typedef {'FORWARD'|'BACKWARD'} engine.view.TreeWalkerDirection
+ */

+ 29 - 10
packages/ckeditor5-engine/tests/view/position.js

@@ -13,6 +13,7 @@ import Element from '/ckeditor5/engine/view/element.js';
 import EditableElement from '/ckeditor5/engine/view/editableelement.js';
 import Document from '/ckeditor5/engine/view/document.js';
 import Text from '/ckeditor5/engine/view/text.js';
+import TextProxy from '/ckeditor5/engine/view/textproxy.js';
 
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 
@@ -291,7 +292,13 @@ describe( 'Position', () => {
 	} );
 
 	describe( 'createBefore', () => {
-		it( 'should create positions before nodes', () => {
+		it( 'should throw error if one try to create positions before root', () => {
+			expect( () => {
+				Position.createBefore( parse( '<p></p>' ) );
+			} ).to.throw( CKEditorError, /position-before-root/ );
+		} );
+
+		it( 'should create positions before `Node`', () => {
 			const { selection } = parse( '<p>[]<b></b></p>' );
 			const position = selection.getFirstPosition();
 			const nodeAfter = position.nodeAfter;
@@ -299,15 +306,24 @@ describe( 'Position', () => {
 			expect( Position.createBefore( nodeAfter ).isEqual( position ) ).to.be.true;
 		} );
 
-		it( 'should throw error if one try to create positions before root', () => {
-			expect( () => {
-				Position.createBefore( parse( '<p></p>' ) );
-			} ).to.throw( CKEditorError, /position-before-root/ );
+		it( 'should create positions before `TextProxy`', () => {
+			const text = new Text( 'abc' );
+
+			const textProxy = new TextProxy( text, 1, 1 );
+			const position = new Position( text, 1 );
+
+			expect( Position.createBefore( textProxy ) ).deep.equal( position );
 		} );
 	} );
 
 	describe( 'createAfter', () => {
-		it( 'should create positions after nodes', () => {
+		it( 'should throw error if one try to create positions after root', () => {
+			expect( () => {
+				Position.createAfter( parse( '<p></p>' ) );
+			} ).to.throw( CKEditorError, /position-after-root/ );
+		} );
+
+		it( 'should create positions after `Node`', () => {
 			const { selection } = parse( '<p><b></b>[]</p>' );
 			const position = selection.getFirstPosition();
 			const nodeBefore = position.nodeBefore;
@@ -315,10 +331,13 @@ describe( 'Position', () => {
 			expect( Position.createAfter( nodeBefore ).isEqual( position ) ).to.be.true;
 		} );
 
-		it( 'should throw error if one try to create positions after root', () => {
-			expect( () => {
-				Position.createAfter( parse( '<p></p>' ) );
-			} ).to.throw( CKEditorError, /position-after-root/ );
+		it( 'should create positions after `TextProxy`', () => {
+			const text = new Text( 'abcd' );
+
+			const textProxy = new TextProxy( text, 1, 2 );
+			const position = new Position( text, 3 );
+
+			expect( Position.createAfter( textProxy ) ).deep.equal( position );
 		} );
 	} );
 

+ 113 - 0
packages/ckeditor5-engine/tests/view/textproxy.js

@@ -0,0 +1,113 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: view */
+
+'use strict';
+
+import TextProxy from '/ckeditor5/engine/view/textproxy.js';
+import Text from '/ckeditor5/engine/view/text.js';
+import ContainerElement from '/ckeditor5/engine/view/containerelement.js';
+import DocumentFragment from '/ckeditor5/engine/view/documentfragment.js';
+import RootEditableElement from '/ckeditor5/engine/view/rooteditableelement.js';
+
+import createDocumentMock from '/tests/engine/view/_utils/createdocumentmock.js';
+
+describe( 'TextProxy', () => {
+	let text, parent, wrapper, textProxy;
+
+	beforeEach( () => {
+		text = new Text( 'abcdefgh' );
+		parent = new ContainerElement( 'p', [], [ text ] );
+		wrapper = new ContainerElement( 'div', [], parent );
+
+		textProxy = new TextProxy( text, 2, 3 );
+	} );
+
+	describe( 'constructor', () => {
+		it( 'should create TextProxy instance with specified properties', () => {
+			expect( textProxy ).to.have.property( 'parent' ).to.equal( parent );
+			expect( textProxy ).to.have.property( 'data' ).to.equal( 'cde' );
+			expect( textProxy ).to.have.property( 'textNode' ).to.equal( text );
+			expect( textProxy ).to.have.property( 'index' ).to.equal( 2 );
+		} );
+
+		it( 'should get text from specified offset to the end of textNode if length is not defined', () => {
+			textProxy = new TextProxy( text, 2 );
+
+			expect( textProxy ).to.have.property( 'data' ).to.equal( 'cdefgh' );
+			expect( textProxy ).to.have.property( 'index' ).to.equal( 2 );
+		} );
+	} );
+
+	describe( 'getDocument', () => {
+		it( 'should return null if any parent has not set Document', () => {
+			expect( textProxy.getDocument() ).to.be.null;
+		} );
+
+		it( 'should return Document attached to the parent element', () => {
+			const docMock = createDocumentMock();
+			const root = new RootEditableElement( docMock, 'div' );
+
+			wrapper.parent = root;
+
+			expect( textProxy.getDocument() ).to.equal( docMock );
+		} );
+
+		it( 'should return null if element is inside DocumentFragment', () => {
+			new DocumentFragment( [ wrapper ] );
+
+			expect( textProxy.getDocument() ).to.be.null;
+		} );
+	} );
+
+	describe( 'getRoot', () => {
+		it( 'should return root element', () => {
+			const root = new RootEditableElement( createDocumentMock(), 'div' );
+
+			wrapper.parent = root;
+
+			expect( textProxy.getRoot() ).to.equal( root );
+		} );
+	} );
+
+	describe( 'getAncestors', () => {
+		it( 'should return array of ancestors', () => {
+			const result = textProxy.getAncestors();
+
+			expect( result ).to.be.an( 'array' );
+			expect( result ).to.length( 2 );
+			expect( result[ 0 ] ).to.equal( wrapper );
+			expect( result[ 1 ] ).to.equal( parent );
+		} );
+
+		it( 'should return array of ancestors starting from parent `parentFirst`', () => {
+			const result = textProxy.getAncestors( { parentFirst: true } );
+
+			expect( result.length ).to.equal( 2 );
+			expect( result[ 0 ] ).to.equal( parent );
+			expect( result[ 1 ] ).to.equal( wrapper );
+		} );
+
+		it( 'should return array including node itself `includeNode`', () => {
+			const result = textProxy.getAncestors( { includeNode: true } );
+
+			expect( result ).to.be.an( 'array' );
+			expect( result ).to.length( 3 );
+			expect( result[ 0 ] ).to.equal( wrapper );
+			expect( result[ 1 ] ).to.equal( parent );
+			expect( result[ 2 ] ).to.equal( text );
+		} );
+
+		it( 'should return array of ancestors including node itself `includeNode` starting from parent `parentFirst`', () => {
+			const result = textProxy.getAncestors( { includeNode: true, parentFirst: true } );
+
+			expect( result.length ).to.equal( 3 );
+			expect( result[ 0 ] ).to.equal( text );
+			expect( result[ 1 ] ).to.equal( parent );
+			expect( result[ 2 ] ).to.equal( wrapper );
+		} );
+	} );
+} );

+ 1017 - 0
packages/ckeditor5-engine/tests/view/treewalker.js

@@ -0,0 +1,1017 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: view */
+
+'use strict';
+
+import Document from '/ckeditor5/engine/view/document.js';
+import AttributeElement from '/ckeditor5/engine/view/attributeelement.js';
+import ContainerElement from '/ckeditor5/engine/view/containerelement.js';
+import Text from '/ckeditor5/engine/view/text.js';
+import TreeWalker from '/ckeditor5/engine/view/treewalker.js';
+import Position from '/ckeditor5/engine/view/position.js';
+import Range from '/ckeditor5/engine/view/range.js';
+import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
+
+describe( 'TreeWalker', () => {
+	let doc, root, img1, paragraph, bold, textAbcd, charY, img2, charX;
+	let rootBeginning, rootEnding;
+
+	before( () => {
+		doc = new Document();
+		root = doc.createRoot( document.createElement( 'div' ) );
+
+		// root
+		//  |- img1
+		//  |- p
+		//     |- b
+		//     |  |- A
+		//     |  |- B
+		//     |  |- C
+		//     |  |- D
+		//     |
+		//     |- Y
+		//     |
+		//     |- img2
+		//     |
+		//     |- X
+
+		textAbcd = new Text( 'abcd' );
+		bold = new AttributeElement( 'b', null, [ textAbcd ] );
+		charY = new Text( 'y' );
+		img2 = new ContainerElement( 'img2' );
+		charX = new Text( 'x' );
+
+		paragraph = new ContainerElement( 'p', null, [ bold, charY, img2, charX ] );
+		img1 = new ContainerElement( 'img1' );
+
+		root.insertChildren( 0, [ img1, paragraph ] );
+
+		rootBeginning = new Position( root, 0 );
+		rootEnding = new Position( root, 2 );
+	} );
+
+	describe( 'constructor', () => {
+		it( 'should throw if neither boundaries nor starting position is set', () => {
+			expect( () => {
+				new TreeWalker();
+			} ).to.throw( CKEditorError, /^tree-walker-no-start-position/ );
+
+			expect( () => {
+				new TreeWalker( {} );
+			} ).to.throw( CKEditorError, /^tree-walker-no-start-position/ );
+
+			expect( () => {
+				new TreeWalker( { singleCharacters: true } );
+			} ).to.throw( CKEditorError, /^tree-walker-no-start-position/ );
+		} );
+
+		it( 'should throw if walking direction is unknown', () => {
+			expect( () => {
+				new TreeWalker( { startPosition: rootBeginning, direction: 'UNKNOWN' } );
+			} ).to.throw( CKEditorError, /^tree-walker-unknown-direction/ );
+		} );
+	} );
+
+	describe( 'iterate from start position `startPosition`', () => {
+		let expected;
+
+		beforeEach( () => {
+			expected = [
+				{
+					type: 'ELEMENT_START',
+					item: img1,
+					previousPosition: new Position( root, 0 ),
+					nextPosition: new Position( img1, 0 )
+				},
+				{
+					type: 'ELEMENT_END',
+					item: img1,
+					previousPosition: new Position( img1, 0 ),
+					nextPosition: new Position( root, 1 )
+				},
+				{
+					type: 'ELEMENT_START',
+					item: paragraph,
+					previousPosition: new Position( root, 1 ),
+					nextPosition: new Position( paragraph, 0 )
+				},
+				{
+					type: 'ELEMENT_START',
+					item: bold,
+					previousPosition: new Position( paragraph, 0 ),
+					nextPosition: new Position( bold, 0 )
+				},
+				{
+					type: 'TEXT',
+					text: 'abcd',
+					previousPosition: new Position( bold, 0 ),
+					nextPosition: new Position( bold, 1 )
+				},
+				{
+					type: 'ELEMENT_END',
+					item: bold,
+					previousPosition: new Position( bold, 1 ),
+					nextPosition: new Position( paragraph, 1 )
+				},
+				{
+					type: 'TEXT',
+					text: 'y',
+					previousPosition: new Position( paragraph, 1 ),
+					nextPosition: new Position( paragraph, 2 )
+				},
+				{
+					type: 'ELEMENT_START',
+					item: img2,
+					previousPosition: new Position( paragraph, 2 ),
+					nextPosition: new Position( img2, 0 )
+				},
+				{
+					type: 'ELEMENT_END',
+					item: img2,
+					previousPosition: new Position( img2, 0 ),
+					nextPosition: new Position( paragraph, 3 )
+				},
+				{
+					type: 'TEXT',
+					text: 'x',
+					previousPosition: new Position( paragraph, 3 ),
+					nextPosition: new Position( paragraph, 4 )
+				},
+				{
+					type: 'ELEMENT_END',
+					item: paragraph,
+					previousPosition: new Position( paragraph, 4 ),
+					nextPosition: new Position( root, 2 )
+				}
+			];
+		} );
+
+		it( 'should provide iterator interface with default FORWARD direction', () => {
+			let iterator = new TreeWalker( { startPosition: rootBeginning } );
+			let i = 0;
+
+			for ( let value of iterator ) {
+				expectValue( value, expected[ i++ ] );
+			}
+
+			expect( i ).to.equal( expected.length );
+		} );
+
+		it( 'should provide iterator interface with FORWARD direction', () => {
+			let iterator = new TreeWalker( { startPosition: rootBeginning, direction: 'FORWARD' } );
+			let i = 0;
+
+			for ( let value of iterator ) {
+				expectValue( value, expected[ i++ ] );
+			}
+
+			expect( i ).to.equal( expected.length );
+		} );
+
+		it( 'should provide iterator interface which BACKWARD direction', () => {
+			let iterator = new TreeWalker( { startPosition: rootEnding, direction: 'BACKWARD' } );
+			let i = expected.length;
+
+			for ( let value of iterator ) {
+				expectValue( value, expected[ --i ], { direction: 'BACKWARD' } );
+			}
+
+			expect( i ).to.equal( 0 );
+		} );
+
+		it( 'should start iterating at the startPosition witch is not a root bound', () => {
+			let iterator = new TreeWalker( { startPosition: new Position( root, 1 ) } );
+			let i = 2;
+
+			for ( let value of iterator ) {
+				expectValue( value, expected[ i++ ] );
+			}
+
+			expect( i ).to.equal( expected.length );
+		} );
+
+		it( 'should start iterating at the startPosition witch is not a root bound, going backward', () => {
+			let expected = [
+				{
+					type: 'ELEMENT_START',
+					item: img1,
+					previousPosition: new Position( root, 0 ),
+					nextPosition: new Position( img1, 0 )
+				},
+				{
+					type: 'ELEMENT_END',
+					item: img1,
+					previousPosition: new Position( img1, 0 ),
+					nextPosition: new Position( root, 1 )
+				}
+			];
+
+			let iterator = new TreeWalker( { startPosition: new Position( root, 1 ), direction: 'BACKWARD' } );
+			let i = expected.length;
+
+			for ( let value of iterator ) {
+				expectValue( value, expected[ --i ], { direction: 'BACKWARD' } );
+			}
+
+			expect( i ).to.equal( 0 );
+		} );
+	} );
+
+	describe( 'iterate trough the range `boundary`', () => {
+		describe( 'range starts between elements', () => {
+			let expected, range;
+
+			before( () => {
+				expected = [
+					{
+						type: 'ELEMENT_START',
+						item: paragraph,
+						previousPosition: new Position( root, 1 ),
+						nextPosition: new Position( paragraph, 0 )
+					},
+					{
+						type: 'ELEMENT_START',
+						item: bold,
+						previousPosition: new Position( paragraph, 0 ),
+						nextPosition: new Position( bold, 0 )
+					},
+					{
+						type: 'TEXT',
+						text: 'abcd',
+						previousPosition: new Position( bold, 0 ),
+						nextPosition: new Position( bold, 1 )
+					},
+					{
+						type: 'ELEMENT_END',
+						item: bold,
+						previousPosition: new Position( bold, 1 ),
+						nextPosition: new Position( paragraph, 1 )
+					},
+					{
+						type: 'TEXT',
+						text: 'y',
+						previousPosition: new Position( paragraph, 1 ),
+						nextPosition: new Position( paragraph, 2 )
+					},
+					{
+						type: 'ELEMENT_START',
+						item: img2,
+						previousPosition: new Position( paragraph, 2 ),
+						nextPosition: new Position( img2, 0 )
+					},
+					{
+						type: 'ELEMENT_END',
+						item: img2,
+						previousPosition: new Position( img2, 0 ),
+						nextPosition: new Position( paragraph, 3 )
+					}
+				];
+
+				range = Range.createFromParentsAndOffsets( root, 1, paragraph, 3 );
+			} );
+
+			it( 'should iterating over the range', () => {
+				let iterator = new TreeWalker( { boundaries: range } );
+				let i = 0;
+
+				for ( let value of iterator ) {
+					expectValue( value, expected[ i++ ] );
+				}
+
+				expect( i ).to.equal( expected.length );
+			} );
+
+			it( 'should iterating over the range going backward', () => {
+				let iterator = new TreeWalker( { boundaries: range, direction: 'BACKWARD' } );
+				let i = expected.length;
+
+				for ( let value of iterator ) {
+					expectValue( value, expected[ --i ], { direction: 'BACKWARD' } );
+				}
+
+				expect( i ).to.equal( 0 );
+			} );
+		} );
+
+		describe( 'range starts inside the text', () => {
+			let expected, range;
+
+			before( () => {
+				expected = [
+					{
+						type: 'TEXT',
+						text: 'bcd',
+						previousPosition: new Position( textAbcd, 1 ),
+						nextPosition: new Position( bold, 1 )
+					},
+					{
+						type: 'ELEMENT_END',
+						item: bold,
+						previousPosition: new Position( bold, 1 ),
+						nextPosition: new Position( paragraph, 1 )
+					},
+					{
+						type: 'TEXT',
+						text: 'y',
+						previousPosition: new Position( paragraph, 1 ),
+						nextPosition: new Position( paragraph, 2 )
+					},
+					{
+						type: 'ELEMENT_START',
+						item: img2,
+						previousPosition: new Position( paragraph, 2 ),
+						nextPosition: new Position( img2, 0 )
+					},
+					{
+						type: 'ELEMENT_END',
+						item: img2,
+						previousPosition: new Position( img2, 0 ),
+						nextPosition: new Position( paragraph, 3 )
+					}
+				];
+
+				range = Range.createFromParentsAndOffsets( textAbcd, 1, paragraph, 3 );
+			} );
+
+			it( 'should return part of the text', () => {
+				let iterator = new TreeWalker( { boundaries: range } );
+				let i = 0;
+
+				for ( let value of iterator ) {
+					expectValue( value, expected[ i++ ] );
+				}
+
+				expect( i ).to.equal( expected.length );
+			} );
+
+			it( 'should return part of the text going backward', () => {
+				let iterator = new TreeWalker( {
+						boundaries: range,
+						direction: 'BACKWARD'
+					}
+				);
+				let i = expected.length;
+
+				for ( let value of iterator ) {
+					expectValue( value, expected[ --i ], { direction: 'BACKWARD' } );
+				}
+
+				expect( i ).to.equal( 0 );
+			} );
+		} );
+
+		describe( 'range ends inside the text', () => {
+			let expected, range;
+
+			before( () => {
+				expected = [
+					{
+						type: 'ELEMENT_START',
+						item: img1,
+						previousPosition: new Position( root, 0 ),
+						nextPosition: new Position( img1, 0 )
+					},
+					{
+						type: 'ELEMENT_END',
+						item: img1,
+						previousPosition: new Position( img1, 0 ),
+						nextPosition: new Position( root, 1 )
+					},
+					{
+						type: 'ELEMENT_START',
+						item: paragraph,
+						previousPosition: new Position( root, 1 ),
+						nextPosition: new Position( paragraph, 0 )
+					},
+					{
+						type: 'ELEMENT_START',
+						item: bold,
+						previousPosition: new Position( paragraph, 0 ),
+						nextPosition: new Position( bold, 0 )
+					},
+					{
+						type: 'TEXT',
+						text: 'ab',
+						previousPosition: new Position( bold, 0 ),
+						nextPosition: new Position( textAbcd, 2 )
+					}
+				];
+
+				range = new Range( rootBeginning, new Position( textAbcd, 2 ) );
+			} );
+
+			it( 'should return part of the text', () => {
+				let iterator = new TreeWalker( { boundaries: range } );
+				let i = 0;
+
+				for ( let value of iterator ) {
+					expectValue( value, expected[ i++ ] );
+				}
+
+				expect( i ).to.equal( expected.length );
+			} );
+
+			it( 'should return part of the text going backward', () => {
+				let iterator = new TreeWalker( {
+					boundaries: range,
+					startPosition: range.end,
+					direction: 'BACKWARD'
+				} );
+
+				let i = expected.length;
+
+				for ( let value of iterator ) {
+					expectValue( value, expected[ --i ], { direction: 'BACKWARD' } );
+				}
+
+				expect( i ).to.equal( 0 );
+			} );
+		} );
+
+		describe( 'range starts and ends inside the same text', () => {
+			let expected, range;
+
+			before( () => {
+				expected = [
+					{
+						type: 'TEXT',
+						text: 'bc',
+						previousPosition: new Position( textAbcd, 1 ),
+						nextPosition: new Position( textAbcd, 3 )
+					}
+				];
+
+				range = new Range( new Position( textAbcd, 1 ), new Position( textAbcd, 3 ) );
+			} );
+
+			it( 'should return part of the text', () => {
+				let iterator = new TreeWalker( { boundaries: range } );
+				let i = 0;
+
+				for ( let value of iterator ) {
+					expectValue( value, expected[ i++ ] );
+				}
+
+				expect( i ).to.equal( expected.length );
+			} );
+
+			it( 'should return part of the text going backward', () => {
+				let iterator = new TreeWalker( {
+					boundaries: range,
+					startPosition: range.end,
+					direction: 'BACKWARD'
+				} );
+
+				let i = expected.length;
+
+				for ( let value of iterator ) {
+					expectValue( value, expected[ --i ], { direction: 'BACKWARD' } );
+				}
+
+				expect( i ).to.equal( 0 );
+			} );
+		} );
+
+		describe( 'custom start position', () => {
+			it( 'should iterating from the start position', () => {
+				let expected = [
+					{
+						type: 'TEXT',
+						text: 'y',
+						previousPosition: new Position( paragraph, 1 ),
+						nextPosition: new Position( paragraph, 2 )
+					},
+					{
+						type: 'ELEMENT_START',
+						item: img2,
+						previousPosition: new Position( paragraph, 2 ),
+						nextPosition: new Position( img2, 0 )
+					},
+					{
+						type: 'ELEMENT_END',
+						item: img2,
+						previousPosition: new Position( img2, 0 ),
+						nextPosition: new Position( paragraph, 3 )
+					}
+				];
+
+				let range = Range.createFromParentsAndOffsets( bold, 1, paragraph, 3 );
+
+				let iterator = new TreeWalker( {
+					boundaries: range,
+					startPosition: new Position( paragraph, 1 )
+				} );
+				let i = 0;
+
+				for ( let value of iterator ) {
+					expectValue( value, expected[ i++ ] );
+				}
+
+				expect( i ).to.equal( expected.length );
+			} );
+
+			it( 'should iterating from the start position going backward', () => {
+				let expected = [
+					{
+						type: 'TEXT',
+						text: 'bcd',
+						previousPosition: new Position( textAbcd, 1 ),
+						nextPosition: new Position( bold, 1 )
+					},
+					{
+						type: 'ELEMENT_END',
+						item: bold,
+						previousPosition: new Position( bold, 1 ),
+						nextPosition: new Position( paragraph, 1 )
+					},
+					{
+						type: 'TEXT',
+						text: 'y',
+						previousPosition: new Position( paragraph, 1 ),
+						nextPosition: new Position( paragraph, 2 )
+					}
+				];
+
+				let range = new Range( new Position( textAbcd, 1 ), new Position( paragraph, 3 ) );
+
+				let iterator = new TreeWalker( {
+					boundaries: range,
+					startPosition: new Position( paragraph, 2 ),
+					direction: 'BACKWARD'
+				} );
+				let i = expected.length;
+
+				for ( let value of iterator ) {
+					expectValue( value, expected[ --i ], { direction: 'BACKWARD' } );
+				}
+
+				expect( i ).to.equal( 0 );
+			} );
+		} );
+	} );
+
+	describe( 'iterate by every single characters `singleCharacter`', () => {
+		describe( 'whole root', () => {
+			let expected;
+
+			before( () => {
+				expected = [
+					{
+						type: 'ELEMENT_START',
+						item: img1,
+						previousPosition: new Position( root, 0 ),
+						nextPosition: new Position( img1, 0 )
+					},
+					{
+						type: 'ELEMENT_END',
+						item: img1,
+						previousPosition: new Position( img1, 0 ),
+						nextPosition: new Position( root, 1 )
+					},
+					{
+						type: 'ELEMENT_START',
+						item: paragraph,
+						previousPosition: new Position( root, 1 ),
+						nextPosition: new Position( paragraph, 0 )
+					},
+					{
+						type: 'ELEMENT_START',
+						item: bold,
+						previousPosition: new Position( paragraph, 0 ),
+						nextPosition: new Position( bold, 0 )
+					},
+					{
+						type: 'TEXT',
+						text: 'a',
+						previousPosition: new Position( bold, 0 ),
+						nextPosition: new Position( textAbcd, 1 )
+					},
+					{
+						type: 'TEXT',
+						text: 'b',
+						previousPosition: new Position( textAbcd, 1 ),
+						nextPosition: new Position( textAbcd, 2 )
+					},
+					{
+						type: 'TEXT',
+						text: 'c',
+						previousPosition: new Position( textAbcd, 2 ),
+						nextPosition: new Position( textAbcd, 3 )
+					},
+					{
+						type: 'TEXT',
+						text: 'd',
+						previousPosition: new Position( textAbcd, 3 ),
+						nextPosition: new Position( bold, 1 )
+					},
+					{
+						type: 'ELEMENT_END',
+						item: bold,
+						previousPosition: new Position( bold, 1 ),
+						nextPosition: new Position( paragraph, 1 )
+					},
+					{
+						type: 'TEXT',
+						text: 'y',
+						previousPosition: new Position( paragraph, 1 ),
+						nextPosition: new Position( paragraph, 2 )
+					},
+					{
+						type: 'ELEMENT_START',
+						item: img2,
+						previousPosition: new Position( paragraph, 2 ),
+						nextPosition: new Position( img2, 0 )
+					},
+					{
+						type: 'ELEMENT_END',
+						item: img2,
+						previousPosition: new Position( img2, 0 ),
+						nextPosition: new Position( paragraph, 3 )
+					},
+					{
+						type: 'TEXT',
+						text: 'x',
+						previousPosition: new Position( paragraph, 3 ),
+						nextPosition: new Position( paragraph, 4 )
+					},
+					{
+						type: 'ELEMENT_END',
+						item: paragraph,
+						previousPosition: new Position( paragraph, 4 ),
+						nextPosition: new Position( root, 2 )
+					}
+				];
+			} );
+
+			it( 'should return single characters', () => {
+				let iterator = new TreeWalker( { startPosition: rootBeginning, singleCharacters: true } );
+				let i = 0;
+
+				for ( let value of iterator ) {
+					expectValue( value, expected[ i++ ] );
+				}
+
+				expect( i ).to.equal( expected.length );
+			} );
+
+			it( 'should return single characters going backward', () => {
+				let iterator = new TreeWalker( {
+					startPosition: rootEnding,
+					singleCharacters: true,
+					direction: 'BACKWARD'
+				} );
+				let i = expected.length;
+
+				for ( let value of iterator ) {
+					expectValue( value, expected[ --i ], { direction: 'BACKWARD' } );
+				}
+
+				expect( i ).to.equal( 0 );
+			} );
+		} );
+
+		describe( 'range', () => {
+			let range, expected;
+
+			before( () => {
+				expected = [
+					{
+						type: 'TEXT',
+						text: 'a',
+						previousPosition: new Position( bold, 0 ),
+						nextPosition: new Position( textAbcd, 1 )
+					},
+					{
+						type: 'TEXT',
+						text: 'b',
+						previousPosition: new Position( textAbcd, 1 ),
+						nextPosition: new Position( textAbcd, 2 )
+					},
+					{
+						type: 'TEXT',
+						text: 'c',
+						previousPosition: new Position( textAbcd, 2 ),
+						nextPosition: new Position( textAbcd, 3 )
+					},
+					{
+						type: 'TEXT',
+						text: 'd',
+						previousPosition: new Position( textAbcd, 3 ),
+						nextPosition: new Position( bold, 1 )
+					},
+					{
+						type: 'ELEMENT_END',
+						item: bold,
+						previousPosition: new Position( bold, 1 ),
+						nextPosition: new Position( paragraph, 1 )
+					},
+					{
+						type: 'TEXT',
+						text: 'y',
+						previousPosition: new Position( paragraph, 1 ),
+						nextPosition: new Position( paragraph, 2 )
+					},
+					{
+						type: 'ELEMENT_START',
+						item: img2,
+						previousPosition: new Position( paragraph, 2 ),
+						nextPosition: new Position( img2, 0 )
+					}
+				];
+
+				range = new Range( new Position( bold, 0 ), new Position( img2, 0 ) );
+			} );
+
+			it( 'should respect boundaries', () => {
+				let iterator = new TreeWalker( { boundaries: range, singleCharacters: true } );
+				let i = 0;
+
+				for ( let value of iterator ) {
+					expectValue( value, expected[ i++ ] );
+				}
+
+				expect( i ).to.equal( expected.length );
+			} );
+
+			it( 'should respect boundaries going backward', () => {
+				let iterator = new TreeWalker( {
+					boundaries: range,
+					singleCharacters: true,
+					direction: 'BACKWARD'
+				} );
+				let i = expected.length;
+
+				for ( let value of iterator ) {
+					expectValue( value, expected[ --i ], { direction: 'BACKWARD' } );
+				}
+
+				expect( i ).to.equal( 0 );
+			} );
+		} );
+	} );
+
+	describe( 'iterate omitting child nodes and ELEMENT_END `shallow`', () => {
+		let expected;
+
+		before( () => {
+			expected = [
+				{
+					type: 'ELEMENT_START',
+					item: img1,
+					previousPosition: new Position( root, 0 ),
+					nextPosition: new Position( root, 1 )
+				},
+				{
+					type: 'ELEMENT_START',
+					item: paragraph,
+					previousPosition: new Position( root, 1 ),
+					nextPosition: new Position( root, 2 )
+				}
+			];
+		} );
+
+		it( 'should not enter elements', () => {
+			let iterator = new TreeWalker( { startPosition: rootBeginning, shallow: true } );
+			let i = 0;
+
+			for ( let value of iterator ) {
+				expectValue( value, expected[ i++ ] );
+			}
+
+			expect( i ).to.equal( expected.length );
+		} );
+
+		it( 'should not enter elements going backward', () => {
+			let iterator = new TreeWalker( { startPosition: rootEnding, shallow: true, direction: 'BACKWARD' } );
+			let i = expected.length;
+
+			for ( let value of iterator ) {
+				expectValue( value, expected[ --i ], { direction: 'BACKWARD' } );
+			}
+
+			expect( i ).to.equal( 0 );
+		} );
+	} );
+
+	describe( 'iterate omitting ELEMENT_END `ignoreElementEnd`', () => {
+		describe( 'merged text', () => {
+			let expected;
+
+			before( () => {
+				expected = [
+					{
+						type: 'ELEMENT_START',
+						item: img1,
+						previousPosition: new Position( root, 0 ),
+						nextPosition: new Position( img1, 0 )
+					},
+					{
+						type: 'ELEMENT_START',
+						item: paragraph,
+						previousPosition: new Position( root, 1 ),
+						nextPosition: new Position( paragraph, 0 )
+					},
+					{
+						type: 'ELEMENT_START',
+						item: bold,
+						previousPosition: new Position( paragraph, 0 ),
+						nextPosition: new Position( bold, 0 )
+					},
+					{
+						type: 'TEXT',
+						text: 'abcd',
+						previousPosition: new Position( bold, 0 ),
+						nextPosition: new Position( bold, 1 )
+					},
+					{
+						type: 'TEXT',
+						text: 'y',
+						previousPosition: new Position( paragraph, 1 ),
+						nextPosition: new Position( paragraph, 2 )
+					},
+					{
+						type: 'ELEMENT_START',
+						item: img2,
+						previousPosition: new Position( paragraph, 2 ),
+						nextPosition: new Position( img2, 0 )
+					},
+					{
+						type: 'TEXT',
+						text: 'x',
+						previousPosition: new Position( paragraph, 3 ),
+						nextPosition: new Position( paragraph, 4 )
+					}
+				];
+			} );
+
+			it( 'should iterate ignoring ELEMENT_END', () => {
+				let iterator = new TreeWalker( { startPosition: rootBeginning, ignoreElementEnd: true } );
+				let i = 0;
+
+				for ( let value of iterator ) {
+					expectValue( value, expected[ i++ ] );
+				}
+
+				expect( i ).to.equal( expected.length );
+			} );
+
+			it( 'should iterate ignoring ELEMENT_END going backward', () => {
+				let iterator = new TreeWalker( {
+					startPosition: rootEnding,
+					ignoreElementEnd: true,
+					direction: 'BACKWARD'
+				} );
+				let i = expected.length;
+
+				for ( let value of iterator ) {
+					expectValue( value, expected[ --i ], { direction: 'BACKWARD' } );
+				}
+
+				expect( i ).to.equal( 0 );
+			} );
+		} );
+
+		describe( 'single character', () => {
+			let expected;
+
+			before( () => {
+				expected = [
+					{
+						type: 'ELEMENT_START',
+						item: img1,
+						previousPosition: new Position( root, 0 ),
+						nextPosition: new Position( img1, 0 )
+					},
+					{
+						type: 'ELEMENT_START',
+						item: paragraph,
+						previousPosition: new Position( root, 1 ),
+						nextPosition: new Position( paragraph, 0 )
+					},
+					{
+						type: 'ELEMENT_START',
+						item: bold,
+						previousPosition: new Position( paragraph, 0 ),
+						nextPosition: new Position( bold, 0 )
+					},
+					{
+						type: 'TEXT',
+						text: 'a',
+						previousPosition: new Position( bold, 0 ),
+						nextPosition: new Position( textAbcd, 1 )
+					},
+					{
+						type: 'TEXT',
+						text: 'b',
+						previousPosition: new Position( textAbcd, 1 ),
+						nextPosition: new Position( textAbcd, 2 )
+					},
+					{
+						type: 'TEXT',
+						text: 'c',
+						previousPosition: new Position( textAbcd, 2 ),
+						nextPosition: new Position( textAbcd, 3 )
+					},
+					{
+						type: 'TEXT',
+						text: 'd',
+						previousPosition: new Position( textAbcd, 3 ),
+						nextPosition: new Position( bold, 1 )
+					},
+					{
+						type: 'TEXT',
+						text: 'y',
+						previousPosition: new Position( paragraph, 1 ),
+						nextPosition: new Position( paragraph, 2 )
+					},
+					{
+						type: 'ELEMENT_START',
+						item: img2,
+						previousPosition: new Position( paragraph, 2 ),
+						nextPosition: new Position( img2, 0 )
+					},
+					{
+						type: 'TEXT',
+						text: 'x',
+						previousPosition: new Position( paragraph, 3 ),
+						nextPosition: new Position( paragraph, 4 )
+					}
+				];
+			} );
+
+			it( 'should return single characters ignoring ELEMENT_END', () => {
+				let iterator = new TreeWalker( {
+					startPosition: rootBeginning,
+					singleCharacters: true,
+					ignoreElementEnd: true
+				} );
+				let i = 0;
+
+				for ( let value of iterator ) {
+					expectValue( value, expected[ i++ ] );
+				}
+
+				expect( i ).to.equal( expected.length );
+			} );
+
+			it( 'should return single characters ignoring ELEMENT_END going backward', () => {
+				let iterator = new TreeWalker( {
+					startPosition: rootEnding,
+					singleCharacters: true,
+					ignoreElementEnd: true,
+					direction: 'BACKWARD'
+				} );
+				let i = expected.length;
+
+				for ( let value of iterator ) {
+					expectValue( value, expected[ --i ], { direction: 'BACKWARD' } );
+				}
+
+				expect( i ).to.equal( 0 );
+			} );
+		} );
+	} );
+} );
+
+function expectValue( value, expected, options = {} ) {
+	let expectedPreviousPosition, expectedNextPosition;
+
+	if ( options.direction == 'BACKWARD' ) {
+		expectedNextPosition = expected.previousPosition;
+		expectedPreviousPosition = expected.nextPosition;
+	} else {
+		expectedNextPosition = expected.nextPosition;
+		expectedPreviousPosition = expected.previousPosition;
+	}
+
+	expect( value.type ).to.equal( expected.type );
+	expect( value.previousPosition ).to.deep.equal( expectedPreviousPosition );
+	expect( value.nextPosition ).to.deep.equal( expectedNextPosition );
+
+	if ( value.type == 'TEXT' ) {
+		expectText( value, expected );
+	} else if ( value.type == 'ELEMENT_START' ) {
+		expectStart( value, expected );
+	} else if ( value.type == 'ELEMENT_END' ) {
+		expectEnd( value, expected );
+	}
+}
+
+function expectText( value, expected ) {
+	expect( value.item.data ).to.equal( expected.text );
+	expect( value.length ).to.equal( value.item.data.length );
+}
+
+function expectStart( value, expected ) {
+	expect( value.item ).to.equal( expected.item );
+	expect( value.length ).to.equal( 1 );
+}
+
+function expectEnd( value, expected ) {
+	expect( value.item ).to.equal( expected.item );
+	expect( value.length ).to.be.undefined;
+}