Parcourir la source

Added TreeWalker.

Oskar Wrobel il y a 9 ans
Parent
commit
d0e79be262

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

@@ -0,0 +1,400 @@
+/**
+ * @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( {
+		boundaries = null,
+		startPosition,
+		direction = 'FORWARD',
+		singleCharacters = false,
+		shallow = false,
+		ignoreElementEnd = false,
+	} = {} ) {
+		if ( !boundaries && !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 ( direction != 'FORWARD' && direction != 'BACKWARD' ) {
+			throw new CKEditorError(
+				'tree-walker-unknown-direction: Only `BACKWARD` and `FORWARD` direction allowed.',
+				{ 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 = boundaries;
+
+		/**
+		 * 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 ( startPosition ) {
+			this.position = Position.createFromPosition( startPosition );
+		} else {
+			this.position = Position.createFromPosition( boundaries[ direction == 'BACKWARD' ? 'end' : 'start' ] );
+		}
+
+		/**
+		 * Walking direction. Defaults `FORWARD`.
+		 *
+		 * @readonly
+		 * @member {'BACKWARD'|'FORWARD'} engine.view.TreeWalker#direction
+		 */
+		this.direction = direction;
+
+		/**
+		 * 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 = !!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 = !!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.model.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 = !!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 {@link engine.view.Text} element 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 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.
+				if ( node == this._boundaryEndParent ) {
+					const offset = this.boundaries.end.offset;
+					const textFragment = node._data.substring( 0, offset );
+
+					charactersCount = textFragment.length;
+					item = new TextProxy( textFragment, node.parent, node, 0 );
+					position = Position.createAfter( item );
+				} else {
+					// If not just move forward.
+					position.offset++;
+				}
+
+				this.position = position;
+
+				return formatReturnValue( 'TEXT', item, previousPosition, position, charactersCount );
+			}
+		} else if ( typeof node == 'string' ) {
+			position.offset++;
+
+			const textProxy = new TextProxy( node, parent.parent, parent, position.offset );
+
+			this.position = position;
+
+			return formatReturnValue( 'TEXT', textProxy, previousPosition, position, 1 );
+		} else {
+			// `node` is not set, we reached the end of current `parent`.
+			position = Position.createAfter( parent );
+			this.position = position;
+
+			// We don't return `ELEMENT_END` for {@link engine.view.Text} element.
+			if ( this.ignoreElementEnd || parent instanceof Text ) {
+				return this._next();
+			} else {
+				return formatReturnValue( 'ELEMENT_END', parent, previousPosition, position );
+			}
+		}
+	}
+
+	/**
+	 * Makes a step backward in model. 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 formatReturnValue( 'ELEMENT_END', node, previousPosition, position );
+				}
+			} else {
+				position.offset--;
+				this.position = position;
+
+				return 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.
+				if ( node == this._boundaryStartParent ) {
+					const offset = this.boundaries.start.offset;
+					const textFragment = node._data.substring( offset, charactersCount );
+
+					charactersCount = textFragment.length;
+					item = new TextProxy( textFragment, node.parent, node, offset );
+					position = Position.createBefore( item );
+				} else {
+					// If not just move backward.
+					position.offset--;
+				}
+
+				this.position = position;
+
+				return formatReturnValue( 'TEXT', item, previousPosition, position, charactersCount );
+			}
+		} else if ( typeof node == 'string' ) {
+			position.offset--;
+
+			const textProxy = new TextProxy( node, parent.parent, parent, position.offset );
+
+			this.position = position;
+
+			return formatReturnValue( 'TEXT', textProxy, previousPosition, position, 1 );
+		} else {
+			// `node` is not set, we reached the beginning of current `parent`.
+			position = Position.createBefore( parent );
+			this.position = position;
+
+			// We don't return `ELEMENT_START` for {@link engine.view.Text} element.
+			if ( parent instanceof Text ) {
+				return this._previous();
+			}
+
+			return formatReturnValue( 'ELEMENT_START', parent, previousPosition, position, 1 );
+		}
+	}
+}
+
+function formatReturnValue( type, item, previousPosition, nextPosition, length ) {
+	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,
+ * `'CHARACTER'` if walker traversed over a character, or `'TEXT'` if walker traversed over multiple characters.
+ *
+ * @typedef {String} engine.view.TreeWalkerValueType
+ */
+
+/**
+ * Object returned by {@link engine.view.TreeWalker} when traversing tree model.
+ *
+ * @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.
+ * @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.
+ * @property {Number} [length] Length of the item. For `'ELEMENT_START'` and `'CHARACTER'` 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
+ */

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

@@ -0,0 +1,619 @@
+/**
+ * @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, ba, r, img2, x;
+	let rootBeginning, rootEnding;
+
+	before( () => {
+		doc = new Document();
+		root = doc.createRoot( document.createElement( 'div' ) );
+
+		// root
+		//  |- img1
+		//  |- p
+		//     |- b
+		//     |  |- B
+		//     |  |- A
+		//     |
+		//     |- R
+		//     |
+		//     |- img2
+		//     |
+		//     |- X
+
+		ba = new Text( 'ba' );
+		bold = new AttributeElement( 'b', [], [ ba ] );
+		r = new Text( 'r' );
+		img2 = new AttributeElement( 'img2' );
+		x = new Text( 'x' );
+
+		paragraph = new ContainerElement( 'p', [], [ bold, r, img2, x ] );
+		img1 = new AttributeElement( '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 },
+				{ type: 'ELEMENT_END', item: img1 },
+				{ type: 'ELEMENT_START', item: paragraph },
+				{ type: 'ELEMENT_START', item: bold },
+				{ type: 'TEXT', text: 'ba' },
+				{ type: 'ELEMENT_END', item: bold },
+				{ type: 'TEXT', text: 'r' },
+				{ type: 'ELEMENT_START', item: img2 },
+				{ type: 'ELEMENT_END', item: img2 },
+				{ type: 'TEXT', text: 'x' },
+				{ type: 'ELEMENT_END', item: paragraph }
+			];
+		} );
+
+		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 ] );
+				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 ] );
+				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 ] );
+				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 },
+				{ type: 'ELEMENT_END', item: img1 }
+			];
+
+			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 },
+					{ type: 'ELEMENT_START', item: bold },
+					{ type: 'TEXT', text: 'ba' },
+					{ type: 'ELEMENT_END', item: bold },
+					{ type: 'TEXT', text: 'r' },
+					{ type: 'ELEMENT_START', item: img2 },
+					{ type: 'ELEMENT_END', item: img2 }
+				];
+
+				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 ] );
+					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: 'a' },
+					{ type: 'ELEMENT_END', item: bold },
+					{ type: 'TEXT', text: 'r' },
+					{ type: 'ELEMENT_START', item: img2 },
+					{ type: 'ELEMENT_END', item: img2 }
+				];
+
+				range = Range.createFromParentsAndOffsets( ba, 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 ] );
+					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 },
+					{ type: 'ELEMENT_END', item: img1 },
+					{ type: 'ELEMENT_START', item: paragraph },
+					{ type: 'ELEMENT_START', item: bold },
+					{ type: 'TEXT', text: 'b' }
+				];
+
+				range = new Range( rootBeginning, new Position( ba, 1 ) );
+			} );
+
+			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 ] );
+					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: 'r' },
+					{ type: 'ELEMENT_START', item: img2 },
+					{ type: 'ELEMENT_END', item: img2 }
+				];
+
+				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 ] );
+					i++;
+				}
+
+				expect( i ).to.equal( expected.length );
+			} );
+
+			it( 'should iterating from the start position going backward', () => {
+				let expected = [
+					{ type: 'TEXT', text: 'r' },
+					{ type: 'ELEMENT_END', item: bold },
+					{ type: 'TEXT', text: 'a' }
+				];
+
+				let range = new Range( new Position( ba, 1 ), new Position( paragraph, 3 ) );
+
+				let iterator = new TreeWalker( {
+					boundaries: range,
+					startPosition: new Position( paragraph, 2 ),
+					direction: 'BACKWARD'
+				} );
+				let i = 0;
+
+				for ( let value of iterator ) {
+					expectValue( value, expected[ i ], { direction: 'BACKWARD' } );
+					i++;
+				}
+
+				expect( i ).to.equal( expected.length );
+			} );
+		} );
+	} );
+
+	describe( 'iterate by every single characters `singleCharacter`', () => {
+		describe( 'whole root', () => {
+			let expected;
+
+			before( () => {
+				expected = [
+					{ type: 'ELEMENT_START', item: img1 },
+					{ type: 'ELEMENT_END', item: img1 },
+					{ type: 'ELEMENT_START', item: paragraph },
+					{ type: 'ELEMENT_START', item: bold },
+					{ type: 'TEXT', text: 'b' },
+					{ type: 'TEXT', text: 'a' },
+					{ type: 'ELEMENT_END', item: bold },
+					{ type: 'TEXT', text: 'r' },
+					{ type: 'ELEMENT_START', item: img2 },
+					{ type: 'ELEMENT_END', item: img2 },
+					{ type: 'TEXT', text: 'x' },
+					{ type: 'ELEMENT_END', item: paragraph }
+				];
+			} );
+
+			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 ] );
+					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: 'b' },
+					{ type: 'TEXT', text: 'a' },
+					{ type: 'ELEMENT_END', item: bold },
+					{ type: 'TEXT', text: 'r' },
+					{ type: 'ELEMENT_START', item: img2 }
+				];
+
+				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 ] );
+					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 },
+				{ type: 'ELEMENT_START', item: paragraph }
+			];
+		} );
+
+		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 ], { shallow: true } );
+				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 ], { shallow: true, 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 },
+					{ type: 'ELEMENT_START', item: paragraph },
+					{ type: 'ELEMENT_START', item: bold },
+					{ type: 'TEXT', text: 'ba' },
+					{ type: 'TEXT', text: 'r' },
+					{ type: 'ELEMENT_START', item: img2 },
+					{ type: 'TEXT', text: 'x' }
+				];
+			} );
+
+			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 ] );
+					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 },
+					{ type: 'ELEMENT_START', item: paragraph },
+					{ type: 'ELEMENT_START', item: bold },
+					{ type: 'TEXT', text: 'b' },
+					{ type: 'TEXT', text: 'a' },
+					{ type: 'TEXT', text: 'r' },
+					{ type: 'ELEMENT_START', item: img2 },
+					{ type: 'TEXT', text: 'x' }
+				];
+			} );
+
+			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 ] );
+					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 ) {
+	expect( value.type ).to.equal( expected.type );
+
+	if ( value.type == 'TEXT' ) {
+		expectText( value, expected, options );
+	} else if ( value.type == 'ELEMENT_START' ) {
+		expectStart( value, expected, options );
+	} else if ( value.type == 'ELEMENT_END' ) {
+		expectEnd( value, expected, options );
+	}
+}
+
+function expectText( value, expected ) {
+	expect( value.item._data ).to.equal( expected.text );
+	expect( value.length ).to.equal( value.item._data.length );
+
+	/**
+	 * @TODO: Checking (next|prev)Position
+	 */
+}
+
+function expectStart( value, expected, options = {} ) {
+	let previousPosition, nextPosition;
+
+	expect( value.item ).to.equal( expected.item );
+	expect( value.length ).to.equal( 1 );
+
+	if ( options.direction == 'BACKWARD' ) {
+		previousPosition = Position.createAfter( value.item );
+		nextPosition = Position.createBefore( value.item );
+	} else {
+		previousPosition = Position.createBefore( value.item );
+		nextPosition = new Position( value.item, 0 );
+	}
+
+	if ( options.shallow ) {
+		expect( value.previousPosition ).to.deep.equal( previousPosition );
+	} else {
+		expect( value.nextPosition ).to.deep.equal( nextPosition );
+	}
+}
+
+function expectEnd( value, expected, options = {} ) {
+	let previousPosition, nextPosition;
+
+	expect( value.item ).to.equal( expected.item );
+	expect( value.length ).to.be.undefined;
+
+	if ( options.direction == 'BACKWARD' ) {
+		previousPosition = Position.createAfter( value.item );
+		nextPosition = new Position( value.item, value.item.getChildCount() );
+	} else {
+		previousPosition = new Position( value.item, value.item.getChildCount() );
+		nextPosition = Position.createAfter( value.item );
+	}
+
+	expect( value.previousPosition ).to.deep.equal( previousPosition );
+	expect( value.nextPosition ).to.deep.equal( nextPosition );
+}