8
0
Просмотр исходного кода

Added direction option to TreeWalker.

Oskar Wrobel 9 лет назад
Родитель
Сommit
cffd162d46

+ 145 - 4
packages/ckeditor5-engine/src/treemodel/treewalker.js

@@ -44,6 +44,18 @@ export default class TreeWalker {
 			throw new CKEditorError( 'tree-walker-no-start-position: Neither boundaries nor starting position have been defined.' );
 			throw new CKEditorError( 'tree-walker-no-start-position: Neither boundaries nor starting position have been defined.' );
 		}
 		}
 
 
+		if ( options && options.direction && [ 'FORWARD', 'BACKWARD' ].indexOf( options.direction ) < 0 ) {
+			/**
+			 * Unknown direction.
+			 *
+			 * @error tree-walker-unknown-direction
+			 */
+			throw new CKEditorError(
+				'tree-walker-unknown-direction: Only `BACKWARD` and `FORWARD` direction allowed.',
+				{ direction: options.direction }
+			);
+		}
+
 		/**
 		/**
 		 * Iterator boundaries.
 		 * Iterator boundaries.
 		 *
 		 *
@@ -56,6 +68,14 @@ export default class TreeWalker {
 		 */
 		 */
 		this.boundaries = options.boundaries || null;
 		this.boundaries = options.boundaries || null;
 
 
+		/**
+		 * Start boundary cached for optimization purposes.
+		 *
+		 * @private
+		 * @member {engine.treeModel.Element} engine.treeModel.TreeWalker#_boundaryStartParent
+		 */
+		this._boundaryStartParent = this.boundaries ? this.boundaries.start.parent : null;
+
 		/**
 		/**
 		 * End boundary cached for optimization purposes.
 		 * End boundary cached for optimization purposes.
 		 *
 		 *
@@ -74,6 +94,14 @@ export default class TreeWalker {
 			Position.createFromPosition( options.startPosition ) :
 			Position.createFromPosition( options.startPosition ) :
 			Position.createFromPosition( options.boundaries.start );
 			Position.createFromPosition( options.boundaries.start );
 
 
+		/**
+		 * Walking direction. Defaults `FORWARD`.
+		 *
+		 * @member engine.treeModel.TreeWalker#direction
+		 * @type {String} core.treeModel.TreeWalkerDirection
+		 */
+		this.direction = !options.direction ? 'FORWARD' : options.direction;
+
 		/**
 		/**
 		 * Flag indicating whether all consecutive characters with the same attributes should be
 		 * Flag indicating whether all consecutive characters with the same attributes should be
 		 * returned as one {@link engine.treeModel.CharacterProxy} (`true`) or one by one (`false`).
 		 * returned as one {@link engine.treeModel.CharacterProxy} (`true`) or one by one (`false`).
@@ -107,6 +135,14 @@ export default class TreeWalker {
 		 * @member {engine.treeModel.Element|engine.treeModel.DocumentFragment} engine.treeModel.TreeWalker#_visitedParent
 		 * @member {engine.treeModel.Element|engine.treeModel.DocumentFragment} engine.treeModel.TreeWalker#_visitedParent
 		 */
 		 */
 		this._visitedParent = this.position.parent;
 		this._visitedParent = this.position.parent;
+
+		/**
+		 * Enum type that specifies relation between two positions.
+		 *
+		 * Possible values: `'AFTER'`, `'BEFORE'`, `'SAME'`.
+		 *
+		 * @typedef {String} PositionRelation
+		 */
 	}
 	}
 
 
 	/**
 	/**
@@ -117,13 +153,37 @@ export default class TreeWalker {
 	}
 	}
 
 
 	/**
 	/**
-	 * Makes a step forward in tree model. Moves the {@link #position} to the next position and returns the encountered value.
+	 * Iterator interface method.
+	 * Detects walking direction and makes step forward or backward.
 	 *
 	 *
 	 * @returns {Object} Object implementing iterator interface, returning information about taken step.
 	 * @returns {Object} Object implementing iterator interface, returning information about taken step.
+ 	 */
+	next() {
+		if ( this.direction == 'FORWARD' ) {
+			return this._next();
+		} else if ( this.direction == 'BACKWARD' ) {
+			return this._previous();
+		} else {
+			/**
+			 * Unknown direction.
+			 *
+			 * @error tree-walker-unknown-direction
+			 */
+			throw new CKEditorError(
+				'tree-walker-unknown-direction: Only `BACKWARD` and `FORWARD` direction allowed.',
+				{ direction: this.direction }
+			);
+		}
+	}
+
+	/**
+	 * Makes a step forward in tree model. Moves the {@link #position} to the next position and returns the encountered value.
+	 *
+	 * @private
 	 * @returns {Boolean} return.done True if iterator is done.
 	 * @returns {Boolean} return.done True if iterator is done.
 	 * @returns {engine.treeModel.TreeWalkerValue} return.value Information about taken step.
 	 * @returns {engine.treeModel.TreeWalkerValue} return.value Information about taken step.
 	 */
 	 */
-	next() {
+	_next() {
 		const previousPosition = this.position;
 		const previousPosition = this.position;
 		const position = Position.createFromPosition( this.position );
 		const position = Position.createFromPosition( this.position );
 		const parent = this._visitedParent;
 		const parent = this._visitedParent;
@@ -179,16 +239,89 @@ export default class TreeWalker {
 			position.path.pop();
 			position.path.pop();
 			position.offset++;
 			position.offset++;
 			this.position = position;
 			this.position = position;
-
 			this._visitedParent = parent.parent;
 			this._visitedParent = parent.parent;
 
 
 			if ( this.ignoreElementEnd ) {
 			if ( this.ignoreElementEnd ) {
-				return this.next();
+				return this._next();
 			} else {
 			} else {
 				return formatReturnValue( 'ELEMENT_END', parent, previousPosition, position );
 				return formatReturnValue( 'ELEMENT_END', parent, previousPosition, position );
 			}
 			}
 		}
 		}
 	}
 	}
+
+	/**
+	 * Makes a step backward in tree model. Moves the {@link #position} to the previous position and returns the encountered value.
+	 *
+	 * @private
+	 * @returns {Boolean} return.done True if iterator is done.
+	 * @returns {core.treeModel.TreeWalkerValue} return.value Information about taken step.
+	 */
+	_previous() {
+		const previousPosition = this.position;
+		const position = Position.createFromPosition( this.position );
+		const parent = this._visitedParent;
+
+		// 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
+		const node = parent.getChild( position.offset - 1 );
+
+		if ( node instanceof Element ) {
+			position.offset--;
+
+			if ( !this.shallow ) {
+				position.path.push( node.getChildCount() );
+				this.position = position;
+				this._visitedParent = node;
+
+				if ( this.ignoreElementEnd ) {
+					return this._previous();
+				} else {
+					return formatReturnValue( 'ELEMENT_END', node, position, previousPosition );
+				}
+			} else {
+				this.position = position;
+
+				return formatReturnValue( 'ELEMENT_START', node, position, previousPosition, 1 );
+			}
+		} else if ( node instanceof CharacterProxy ) {
+			if ( this.singleCharacters ) {
+				position.offset--;
+				this.position = position;
+
+				return formatReturnValue( 'CHARACTER', node, position, previousPosition, 1 );
+			} else {
+				let charactersCount = node._index + 1;
+				let offset = position.offset - charactersCount;
+
+				if ( this._boundaryStartParent == parent && this.boundaries.start.offset > offset ) {
+					offset = this.boundaries.start.offset;
+					charactersCount = position.offset - offset;
+				}
+
+				let textFragment = new TextProxy( parent.getChild( offset ), charactersCount );
+
+				position.offset = offset;
+				this.position = position;
+
+				return formatReturnValue( 'TEXT', textFragment, position, previousPosition, charactersCount );
+			}
+		} else {
+			position.path.pop();
+			this.position = position;
+			this._visitedParent = parent.parent;
+
+			return formatReturnValue( 'ELEMENT_START', parent, position, previousPosition, 1 );
+		}
+	}
 }
 }
 
 
 function formatReturnValue( type, item, previousPosition, nextPosition, length ) {
 function formatReturnValue( type, item, previousPosition, nextPosition, length ) {
@@ -230,3 +363,11 @@ function formatReturnValue( type, item, previousPosition, nextPosition, length )
  * @property {Number} [length] Length of the item. For `'ELEMENT_START'` and `'CHARACTER'` it is 1. For `'TEXT'` it is
  * @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.
  * the length of the text. For `'ELEMENT_END'` it is undefined.
  */
  */
+
+/**
+ * Tree walking directions.
+ *
+ * Possible values: `'FORWARD'`, `'BACKWARD'`.
+ *
+ * @typedef {String} core.treeModel.TreeWalkerDirection
+ */

+ 323 - 138
packages/ckeditor5-engine/tests/treemodel/treewalker.js

@@ -18,7 +18,7 @@ import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 describe( 'TreeWalker', () => {
 describe( 'TreeWalker', () => {
 	let expected;
 	let expected;
 	let doc, root, img1, paragraph, b, a, r, img2, x;
 	let doc, root, img1, paragraph, b, a, r, img2, x;
-	let rootBeginning;
+	let rootBeginning, rootEnding;
 
 
 	before( () => {
 	before( () => {
 		doc = new Document();
 		doc = new Document();
@@ -47,6 +47,7 @@ describe( 'TreeWalker', () => {
 		root.insertChildren( 0, [ img1, paragraph ] );
 		root.insertChildren( 0, [ img1, paragraph ] );
 
 
 		rootBeginning = new Position( root, [ 0 ] );
 		rootBeginning = new Position( root, [ 0 ] );
+		rootEnding = new Position( root, [ 2 ] );
 	} );
 	} );
 
 
 	describe( 'constructor', () => {
 	describe( 'constructor', () => {
@@ -60,12 +61,18 @@ describe( 'TreeWalker', () => {
 			} ).to.throw( CKEditorError, /^tree-walker-no-start-position/ );
 			} ).to.throw( CKEditorError, /^tree-walker-no-start-position/ );
 
 
 			expect( () => {
 			expect( () => {
-				new TreeWalker( { mergeCharacters: true } );
+				new TreeWalker( { singleCharacters: true } );
 			} ).to.throw( CKEditorError, /^tree-walker-no-start-position/ );
 			} ).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', () => {
+	describe( 'iterate from start position `startPosition`', () => {
 		beforeEach( () => {
 		beforeEach( () => {
 			expected = [
 			expected = [
 				{ type: 'ELEMENT_START', item: img1 },
 				{ type: 'ELEMENT_START', item: img1 },
@@ -80,16 +87,7 @@ describe( 'TreeWalker', () => {
 			];
 			];
 		} );
 		} );
 
 
-		it( 'should provide iterator methods', () => {
-			let iterator = new TreeWalker( { startPosition: rootBeginning } );
-
-			for ( let i = 0; i <= 8; i++ ) {
-				expectItem( iterator.next(), expected[ i ] );
-			}
-			expect( iterator.next() ).to.have.property( 'done' ).that.is.true;
-		} );
-
-		it( 'should provide iterator interface', () => {
+		it( 'should provide iterator interface with default FORWARD direction', () => {
 			let iterator = new TreeWalker( { startPosition: rootBeginning } );
 			let iterator = new TreeWalker( { startPosition: rootBeginning } );
 			let i = 0;
 			let i = 0;
 
 
@@ -101,9 +99,9 @@ describe( 'TreeWalker', () => {
 			expect( i ).to.equal( expected.length );
 			expect( i ).to.equal( expected.length );
 		} );
 		} );
 
 
-		it( 'should start at the startPosition', () => {
-			let iterator = new TreeWalker( { startPosition: new Position( root, [ 1 ] ) } );
-			let i = 2;
+		it( 'should provide iterator interface with FORWARD direction', () => {
+			let iterator = new TreeWalker( { startPosition: rootBeginning, direction: 'FORWARD' } );
+			let i = 0;
 
 
 			for ( let value of iterator ) {
 			for ( let value of iterator ) {
 				expectValue( value, expected[ i ] );
 				expectValue( value, expected[ i ] );
@@ -112,47 +110,31 @@ describe( 'TreeWalker', () => {
 
 
 			expect( i ).to.equal( expected.length );
 			expect( i ).to.equal( expected.length );
 		} );
 		} );
-	} );
-
-	describe( 'range', () => {
-		it( 'should iterating over the range', () => {
-			expected = [
-				{ type: 'ELEMENT_START', item: paragraph },
-				{ type: 'TEXT', text: 'ba', attrs: [ [ 'bold', true ] ] },
-				{ type: 'TEXT', text: 'r', attrs: [] },
-				{ type: 'ELEMENT_START', item: img2 },
-				{ type: 'ELEMENT_END', item: img2 }
-			];
 
 
-			let start = new Position( root, [ 1 ] );
-			let end = new Position( root, [ 1, 4 ] );
-			let range = new Range( start, end );
-
-			let iterator = new TreeWalker( { boundaries: range } );
-			let i = 0;
+		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 ) {
 			for ( let value of iterator ) {
-				expectValue( value, expected[ i ] );
-				i++;
+				expectValue( value, expected[ --i ] );
 			}
 			}
 
 
-			expect( i ).to.equal( expected.length );
+			expect( i ).to.equal( 0 );
 		} );
 		} );
 
 
-		it( 'should return part of the text if range starts inside the text', () => {
-			expected = [
-				{ type: 'TEXT', text: 'a', attrs: [ [ 'bold', true ] ] },
-				{ type: 'TEXT', text: 'r', attrs: [] },
-				{ type: 'ELEMENT_START', item: img2 },
-				{ type: 'ELEMENT_END', item: img2 }
-			];
+		it( 'should throw if walking direction is unknown', () => {
+			let iterator = new TreeWalker( { startPosition: rootEnding } );
 
 
-			let start = new Position( root, [ 1, 1 ] );
-			let end = new Position( root, [ 1, 4 ] );
-			let range = new Range( start, end );
+			iterator.direction = 'UNKNOWN';
 
 
-			let iterator = new TreeWalker( { boundaries: range } );
-			let i = 0;
+			expect( () => {
+				iterator.next();
+			} ).to.throw( CKEditorError, /^tree-walker-unknown-direction/ );
+		} );
+
+		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 ) {
 			for ( let value of iterator ) {
 				expectValue( value, expected[ i ] );
 				expectValue( value, expected[ i ] );
@@ -162,76 +144,235 @@ describe( 'TreeWalker', () => {
 			expect( i ).to.equal( expected.length );
 			expect( i ).to.equal( expected.length );
 		} );
 		} );
 
 
-		it( 'should return part of the text if range ends inside the text', () => {
+		it( 'should start iterating at the startPosition witch is not a root bound, going backward', () => {
 			expected = [
 			expected = [
 				{ type: 'ELEMENT_START', item: img1 },
 				{ type: 'ELEMENT_START', item: img1 },
-				{ type: 'ELEMENT_END', item: img1 },
-				{ type: 'ELEMENT_START', item: paragraph },
-				{ type: 'TEXT', text: 'b', attrs: [ [ 'bold', true ] ] }
+				{ type: 'ELEMENT_END', item: img1 }
 			];
 			];
 
 
-			let start = rootBeginning;
-			let end = new Position( root, [ 1, 1 ] );
-			let range = new Range( start, end );
-
-			let iterator = new TreeWalker( { boundaries: range } );
-			let i = 0;
+			let iterator = new TreeWalker( { startPosition: new Position( root, [ 1 ] ), direction: 'BACKWARD' } );
+			let i = expected.length;
 
 
 			for ( let value of iterator ) {
 			for ( let value of iterator ) {
-				expectValue( value, expected[ i ] );
-				i++;
+				expectValue( value, expected[ --i ] );
 			}
 			}
 
 
-			expect( i ).to.equal( expected.length );
+			expect( i ).to.equal( 0 );
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'singleCharacters', () => {
-		beforeEach( () => {
-			expected = [
-				{ type: 'ELEMENT_START', item: img1 },
-				{ type: 'ELEMENT_END', item: img1 },
-				{ type: 'ELEMENT_START', item: paragraph },
-				{ type: 'CHARACTER', text: 'b', attrs: [ [ 'bold', true ] ] },
-				{ type: 'CHARACTER', text: 'a', attrs: [ [ 'bold', true ] ] },
-				{ type: 'CHARACTER', text: 'r', attrs: [] },
-				{ type: 'ELEMENT_START', item: img2 },
-				{ type: 'ELEMENT_END', item: img2 },
-				{ type: 'CHARACTER', text: 'x', attrs: [] },
-				{ type: 'ELEMENT_END', item: paragraph }
-			];
+	describe( 'iterate trough the range `boundary`', () => {
+		let start, end, range;
+
+		describe( 'range starts between elements', () => {
+			before( () => {
+				expected = [
+					{ type: 'ELEMENT_START', item: paragraph },
+					{ type: 'TEXT', text: 'ba', attrs: [ [ 'bold', true ] ] },
+					{ type: 'TEXT', text: 'r', attrs: [] },
+					{ type: 'ELEMENT_START', item: img2 },
+					{ type: 'ELEMENT_END', item: img2 }
+				];
+
+				start = new Position( root, [ 1 ] );
+				end = new Position( root, [ 1, 4 ] );
+				range = new Range( start, end );
+			} );
+
+			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, startPosition: range.end, direction: 'BACKWARD' } );
+				let i = expected.length;
+
+				for ( let value of iterator ) {
+					expectValue( value, expected[ --i ] );
+				}
+
+				expect( i ).to.equal( 0 );
+			} );
 		} );
 		} );
 
 
-		it( 'should return single characters', () => {
-			let iterator = new TreeWalker( { startPosition: rootBeginning, singleCharacters: true } );
-			let i = 0;
+		describe( 'range starts inside the text', () => {
+			before( () => {
+				expected = [
+					{ type: 'TEXT', text: 'a', attrs: [ [ 'bold', true ] ] },
+					{ type: 'TEXT', text: 'r', attrs: [] },
+					{ type: 'ELEMENT_START', item: img2 },
+					{ type: 'ELEMENT_END', item: img2 }
+				];
+
+				start = new Position( root, [ 1, 1 ] );
+				end = new Position( root, [ 1, 4 ] );
+				range = new Range( start, end );
+			} );
 
 
-			for ( let value of iterator ) {
-				expectValue( value, expected[ i ] );
-				i++;
-			}
+			it( 'should return part of the text', () => {
+				let iterator = new TreeWalker( { boundaries: range } );
+				let i = 0;
 
 
-			expect( i ).to.equal( expected.length );
+				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 ] );
+				}
+
+				expect( i ).to.equal( 0 );
+			} );
 		} );
 		} );
 
 
-		it( 'should respect boundaries', () => {
-			let start = new Position( root, [ 1, 0 ] ); // p, 0
-			let end = new Position( root, [ 1, 3, 0 ] ); // img, 0
+		describe( 'range ends inside the text', () => {
+			before( () => {
+				expected = [
+					{ type: 'ELEMENT_START', item: img1 },
+					{ type: 'ELEMENT_END', item: img1 },
+					{ type: 'ELEMENT_START', item: paragraph },
+					{ type: 'TEXT', text: 'b', attrs: [ [ 'bold', true ] ] }
+				];
+
+				start = rootBeginning;
+				end = new Position( root, [ 1, 1 ] );
+				range = new Range( start, end );
+			} );
 
 
-			let iterator = new TreeWalker( { boundaries: new Range( start, end ), singleCharacters: true } );
-			let i = 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++;
-			}
+				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 ] );
+				}
 
 
-			expect( i ).to.equal( 7 );
+				expect( i ).to.equal( 0 );
+			} );
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'shallow', () => {
-		beforeEach( () => {
+	describe( 'iterate by every single characters `singleCharacter`', () => {
+		describe( 'whole root', () => {
+			before( () => {
+				expected = [
+					{ type: 'ELEMENT_START', item: img1 },
+					{ type: 'ELEMENT_END', item: img1 },
+					{ type: 'ELEMENT_START', item: paragraph },
+					{ type: 'CHARACTER', text: 'b', attrs: [ [ 'bold', true ] ] },
+					{ type: 'CHARACTER', text: 'a', attrs: [ [ 'bold', true ] ] },
+					{ type: 'CHARACTER', text: 'r', attrs: [] },
+					{ type: 'ELEMENT_START', item: img2 },
+					{ type: 'ELEMENT_END', item: img2 },
+					{ type: 'CHARACTER', text: 'x', attrs: [] },
+					{ 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 ] );
+				}
+
+				expect( i ).to.equal( 0 );
+			} );
+		} );
+
+		describe( 'range', () => {
+			let start, end, range;
+
+			before( () => {
+				expected = [
+					{ type: 'CHARACTER', text: 'b', attrs: [ [ 'bold', true ] ] },
+					{ type: 'CHARACTER', text: 'a', attrs: [ [ 'bold', true ] ] },
+					{ type: 'CHARACTER', text: 'r', attrs: [] },
+					{ type: 'ELEMENT_START', item: img2 }
+				];
+
+				start = new Position( root, [ 1, 0 ] ); // p, 0
+				end = new Position( root, [ 1, 3, 0 ] ); // img2, 0
+				range = new Range( start, end );
+			} );
+
+			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,
+					startPosition: range.end,
+					direction: 'BACKWARD'
+				} );
+				let i = expected.length;
+
+				for ( let value of iterator ) {
+					expectValue( value, expected[ --i ] );
+				}
+
+				expect( i ).to.equal( 0 );
+			} );
+		} );
+	} );
+
+	describe( 'iterate omitting child nodes and ELEMENT_END `shallow`', () => {
+		before( () => {
 			expected = [
 			expected = [
 				{ type: 'ELEMENT_START', item: img1 },
 				{ type: 'ELEMENT_START', item: img1 },
 				{ type: 'ELEMENT_START', item: paragraph }
 				{ type: 'ELEMENT_START', item: paragraph }
@@ -249,63 +390,108 @@ describe( 'TreeWalker', () => {
 
 
 			expect( i ).to.equal( expected.length );
 			expect( i ).to.equal( expected.length );
 		} );
 		} );
-	} );
-
-	describe( 'ignoreElementEnd', () => {
-		it( 'should iterate ignoring ELEMENT_END', () => {
-			expected = [
-				{ type: 'ELEMENT_START', item: img1 },
-				{ type: 'ELEMENT_START', item: paragraph },
-				{ type: 'TEXT', text: 'ba', attrs: [ [ 'bold', true ] ] },
-				{ type: 'TEXT', text: 'r', attrs: [] },
-				{ type: 'ELEMENT_START', item: img2 },
-				{ type: 'TEXT', text: 'x', attrs: [] }
-			];
 
 
-			let iterator = new TreeWalker( { startPosition: rootBeginning, ignoreElementEnd: true } );
-			let i = 0;
+		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 ) {
 			for ( let value of iterator ) {
-				expectValue( value, expected[ i ] );
-				i++;
+				expectValue( value, expected[ --i ], { shallow: true } );
 			}
 			}
 
 
-			expect( i ).to.equal( expected.length );
+			expect( i ).to.equal( 0 );
 		} );
 		} );
+	} );
 
 
-		it( 'should return single characters ignoring ELEMENT_END', () => {
-			expected = [
-				{ type: 'ELEMENT_START', item: img1 },
-				{ type: 'ELEMENT_START', item: paragraph },
-				{ type: 'CHARACTER', text: 'b', attrs: [ [ 'bold', true ] ] },
-				{ type: 'CHARACTER', text: 'a', attrs: [ [ 'bold', true ] ] },
-				{ type: 'CHARACTER', text: 'r', attrs: [] },
-				{ type: 'ELEMENT_START', item: img2 },
-				{ type: 'CHARACTER', text: 'x', attrs: [] }
-			];
+	describe( 'iterate omitting ELEMENT_END `ignoreElementEnd`', () => {
+		describe( 'merged text', () => {
+			before( () => {
+				expected = [
+					{ type: 'ELEMENT_START', item: img1 },
+					{ type: 'ELEMENT_START', item: paragraph },
+					{ type: 'TEXT', text: 'ba', attrs: [ [ 'bold', true ] ] },
+					{ type: 'TEXT', text: 'r', attrs: [] },
+					{ type: 'ELEMENT_START', item: img2 },
+					{ type: 'TEXT', text: 'x', attrs: [] }
+				];
+			} );
+
+			it( 'should iterate ignoring ELEMENT_END', () => {
+				let iterator = new TreeWalker( { startPosition: rootBeginning, ignoreElementEnd: true } );
+				let i = 0;
 
 
-			let iterator = new TreeWalker( {
-				startPosition: rootBeginning,
-				singleCharacters: true,
-				ignoreElementEnd: true
+				for ( let value of iterator ) {
+					expectValue( value, expected[ i ] );
+					i++;
+				}
+
+				expect( i ).to.equal( expected.length );
 			} );
 			} );
-			let i = 0;
 
 
-			for ( let value of iterator ) {
-				expectValue( value, expected[ i ] );
-				i++;
-			}
+			it( 'should iterate ignoring ELEMENT_END going backward', () => {
+				let iterator = new TreeWalker( {
+					startPosition: rootEnding,
+					ignoreElementEnd: true,
+					direction: 'BACKWARD'
+				} );
+				let i = expected.length;
 
 
-			expect( i ).to.equal( expected.length );
+				for ( let value of iterator ) {
+					expectValue( value, expected[ --i ] );
+				}
+
+				expect( i ).to.equal( 0 );
+			} );
+		} );
+
+		describe( 'single character', () => {
+			before( () => {
+				expected = [
+					{ type: 'ELEMENT_START', item: img1 },
+					{ type: 'ELEMENT_START', item: paragraph },
+					{ type: 'CHARACTER', text: 'b', attrs: [ [ 'bold', true ] ] },
+					{ type: 'CHARACTER', text: 'a', attrs: [ [ 'bold', true ] ] },
+					{ type: 'CHARACTER', text: 'r', attrs: [] },
+					{ type: 'ELEMENT_START', item: img2 },
+					{ type: 'CHARACTER', text: 'x', attrs: [] }
+				];
+			} );
+
+			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 ] );
+				}
+
+				expect( i ).to.equal( 0 );
+			} );
 		} );
 		} );
 	} );
 	} );
 } );
 } );
 
 
-function expectItem( item, expected, options ) {
-	expect( item.done ).to.be.false;
-	expectValue( item.value, expected, options );
-}
-
 function expectValue( value, expected, options ) {
 function expectValue( value, expected, options ) {
 	expect( value.type ).to.equal( expected.type );
 	expect( value.type ).to.equal( expected.type );
 
 
@@ -339,7 +525,6 @@ function expectCharacter( value, expected ) {
 function expectStart( value, expected, options ) {
 function expectStart( value, expected, options ) {
 	expect( value.item ).to.equal( expected.item );
 	expect( value.item ).to.equal( expected.item );
 	expect( value.length ).to.equal( 1 );
 	expect( value.length ).to.equal( 1 );
-	expect( value.previousPosition ).to.deep.equal( Position.createBefore( value.item ) );
 
 
 	if ( options && options.shallow ) {
 	if ( options && options.shallow ) {
 		expect( value.previousPosition ).to.deep.equal( Position.createBefore( value.item ) );
 		expect( value.previousPosition ).to.deep.equal( Position.createBefore( value.item ) );