Browse Source

Merge pull request #425 from ckeditor/t/344

Added direction option to TreeWalker.
Piotrek Koszuliński 9 năm trước cách đây
mục cha
commit
b4f38fbff5

+ 18 - 14
packages/ckeditor5-engine/src/model/composer/modifyselection.js

@@ -27,48 +27,52 @@ export default function modifySelection( selection, options = {} ) {
 	const focus = selection.focus;
 	const walker = new TreeWalker( {
 		boundaries: getSearchRange( focus, isForward ),
-		singleCharacters: true
+		singleCharacters: true,
+		direction: isForward ? 'FORWARD' : 'BACKWARD'
 	} );
 
-	const items = Array.from( walker );
-	let next = items[ isForward ? 'shift' : 'pop' ]();
+	let next = walker.next();
 
 	// 1. Nothing to do here.
-	if ( !next ) {
+	if ( next.done ) {
 		return;
 	}
 
+	let value = next.value;
+
 	// 2. Consume next character.
-	if ( next.type == 'CHARACTER' ) {
-		selection.setFocus( next[ isForward ? 'nextPosition' : 'previousPosition' ] );
+	if ( value.type == 'CHARACTER' ) {
+		selection.setFocus( value.nextPosition );
 
 		return;
 	}
 
 	// 3. We're entering an element, so let's consume it fully.
-	if ( next.type == ( isForward ? 'ELEMENT_START' : 'ELEMENT_END' ) ) {
-		selection.setFocus( next.item, isForward ? 'AFTER' : 'BEFORE' );
+	if ( value.type == ( isForward ? 'ELEMENT_START' : 'ELEMENT_END' ) ) {
+		selection.setFocus( value.item, isForward ? 'AFTER' : 'BEFORE' );
 
 		return;
 	}
 
 	// 4. We're leaving an element. That's more tricky.
-
-	next = items[ isForward ? 'shift' : 'pop' ]();
+	next = walker.next();
 
 	// 4.1. Nothing left, so let's stay where we were.
-	if ( !next ) {
+	if ( next.done ) {
 		return;
 	}
 
+	// Replace TreeWalker step wrapper by clean step value.
+	value = next.value;
+
 	// 4.2. Character found after element end. Not really a valid case in our data model, but let's
 	// do something sensible and put the selection focus before that character.
-	if ( next.type == 'CHARACTER' ) {
-		selection.setFocus( next[ isForward ? 'previousPosition' : 'nextPosition' ] );
+	if ( value.type == 'CHARACTER' ) {
+		selection.setFocus( value.previousPosition );
 	}
 	// 4.3. OK, we're entering a new element. So let's place there the focus.
 	else {
-		selection.setFocus( next.item, isForward ? 0 : 'END' );
+		selection.setFocus( value.item, isForward ? 0 : 'END' );
 	}
 }
 

+ 172 - 31
packages/ckeditor5-engine/src/model/treewalker.js

@@ -12,7 +12,7 @@ import Position from './position.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
 
 /**
- * Position iterator class. It allows to iterate forward and backward over the tree document.
+ * Position iterator class. It allows to iterate forward and backward over the document.
  *
  * @memberOf engine.model
  */
@@ -20,9 +20,12 @@ 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.model.Range} [options.boundaries] Range to define boundaries of the iterator.
 	 * @param {engine.model.Position} [options.startPosition] Starting position.
+	 * @param {engine.model.Range} [options.boundaries=null] Range to define boundaries of the iterator.
+	 * @param {engine.model.Position} [options.startPosition] Starting position.
+	 * @param {'FORWARD'|'BACKWARD'} [options.direction='FORWARD'] Walking direction.
 	 * @param {Boolean} [options.singleCharacters=false] Flag indicating whether all consecutive characters with the same attributes
 	 * should be returned one by one as multiple {@link engine.model.CharacterProxy} (`true`) objects or as one
 	 * {@link engine.model.TextProxy} (`false`).
@@ -32,10 +35,18 @@ export default class TreeWalker {
 	 * tags. If the option is true walker will not return a parent node of 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'`.
-	 * @constructor
 	 */
-	constructor( options ) {
-		if ( !options || ( !options.boundaries && !options.startPosition ) ) {
+	constructor(
+		{
+			boundaries = null,
+			startPosition,
+			direction = 'FORWARD',
+			singleCharacters = false,
+			shallow = false,
+			ignoreElementEnd = false,
+		} = {}
+	) {
+		if ( !boundaries && !startPosition ) {
 			/**
 			 * Neither boundaries nor starting position have been defined.
 			 *
@@ -44,6 +55,13 @@ export default class TreeWalker {
 			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.
 		 *
@@ -52,43 +70,49 @@ export default class TreeWalker {
 		 *
 		 * If boundaries are not defined they are set before first and after last child of the root node.
 		 *
+		 * @readonly
 		 * @member {engine.model.Range} engine.model.TreeWalker#boundaries
 		 */
-		this.boundaries = options.boundaries || null;
+		this.boundaries = boundaries;
 
 		/**
-		 * End boundary cached for optimization purposes.
+		 * Iterator position. This is always static position, even if the initial position was a
+		 * {@link engine.model.LivePosition live position}.
 		 *
-		 * @private
-		 * @member {engine.model.Element} engine.model.TreeWalker#_boundaryEndParent
+		 * @readonly
+		 * @member {engine.model.Position} engine.model.TreeWalker#position
 		 */
-		this._boundaryEndParent = this.boundaries ? this.boundaries.end.parent : null;
+		if ( startPosition ) {
+			this.position = Position.createFromPosition( startPosition );
+		} else {
+			this.position = Position.createFromPosition( boundaries[ direction == 'BACKWARD' ? 'end' : 'start' ] );
+		}
 
 		/**
-		 * Iterator position. This is always static position, even if the initial position was a
-		 * {@link engine.model.LivePosition live position}.
+		 * Walking direction. Defaults `FORWARD`.
 		 *
-		 * @member {engine.model.Position} engine.model.TreeWalker#position
+		 * @readonly
+		 * @member {'BACKWARD'|'FORWARD'} engine.model.TreeWalker#direction
 		 */
-		this.position = options.startPosition ?
-			Position.createFromPosition( options.startPosition ) :
-			Position.createFromPosition( options.boundaries.start );
+		this.direction = direction;
 
 		/**
 		 * Flag indicating whether all consecutive characters with the same attributes should be
 		 * returned as one {@link engine.model.CharacterProxy} (`true`) or one by one (`false`).
 		 *
+		 * @readonly
 		 * @member {Boolean} engine.model.TreeWalker#singleCharacters
 		 */
-		this.singleCharacters = !!options.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.model.TreeWalker#shallow
 		 */
-		this.shallow = !!options.shallow;
+		this.shallow = !!shallow;
 
 		/**
 		 * Flag indicating whether iterator should ignore `ELEMENT_END` tags. If the option is true walker will not
@@ -96,9 +120,26 @@ export default class TreeWalker {
 		 * be returned once, while if the option is `false` they might be returned twice:
 		 * for `'ELEMENT_START'` and `'ELEMENT_END'`.
 		 *
+		 * @readonly
 		 * @member {Boolean} engine.model.TreeWalker#ignoreElementEnd
 		 */
-		this.ignoreElementEnd = !!options.ignoreElementEnd;
+		this.ignoreElementEnd = !!ignoreElementEnd;
+
+		/**
+		 * Start boundary cached for optimization purposes.
+		 *
+		 * @private
+		 * @member {engine.model.Element} engine.model.TreeWalker#_boundaryStartParent
+		 */
+		this._boundaryStartParent = this.boundaries ? this.boundaries.start.parent : null;
+
+		/**
+		 * End boundary cached for optimization purposes.
+		 *
+		 * @private
+		 * @member {engine.model.Element} engine.model.TreeWalker#_boundaryEndParent
+		 */
+		this._boundaryEndParent = this.boundaries ? this.boundaries.end.parent : null;
 
 		/**
 		 * Parent of the most recently visited node. Cached for optimization purposes.
@@ -117,13 +158,28 @@ 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.
+	 */
+	next() {
+		if ( this.direction == 'FORWARD' ) {
+			return this._next();
+		} else {
+			return this._previous();
+		}
+	}
+
+	/**
+	 * Makes a step forward in model. 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.model.TreeWalkerValue} return.value Information about taken step.
 	 */
-	next() {
+	_next() {
 		const previousPosition = this.position;
 		const position = Position.createFromPosition( this.position );
 		const parent = this._visitedParent;
@@ -179,16 +235,90 @@ export default class TreeWalker {
 			position.path.pop();
 			position.offset++;
 			this.position = position;
-
 			this._visitedParent = parent.parent;
 
 			if ( this.ignoreElementEnd ) {
-				return this.next();
+				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 {core.model.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, previousPosition, position );
+				}
+			} else {
+				this.position = position;
+
+				return formatReturnValue( 'ELEMENT_START', node, previousPosition, position, 1 );
+			}
+		} else if ( node instanceof CharacterProxy ) {
+			if ( this.singleCharacters ) {
+				position.offset--;
+				this.position = position;
+
+				return formatReturnValue( 'CHARACTER', node, previousPosition, position, 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, previousPosition, position, charactersCount );
+			}
+		} else {
+			position.path.pop();
+			this.position = position;
+			this._visitedParent = parent.parent;
+
+			return formatReturnValue( 'ELEMENT_START', parent, previousPosition, position, 1 );
+		}
+	}
 }
 
 function formatReturnValue( type, item, previousPosition, nextPosition, length ) {
@@ -219,14 +349,25 @@ function formatReturnValue( type, item, previousPosition, nextPosition, length )
  * @typedef {Object} engine.model.TreeWalkerValue
  * @property {engine.model.TreeWalkerValueType} type
  * @property {engine.model.Item} item Item between old and new positions of {@link engine.model.TreeWalker}.
- * @property {engine.model.Position} previousPosition Previous position of the iterator. 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.model.Position.createBefore}. It is also more efficient to get the position after node by shifting
- * `previousPosition` by `length`, using {@link engine.model.Position#getShiftedBy}, then calculate the position using
- * {@link engine.model.Position.createAfter}.
- * @property {engine.model.Position} nextPosition Next position of the iterator. For `'ELEMENT_START'` it is the first
- * position inside the element. For all other types it is the position after the item.
+ * @property {engine.model.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.model.Position.createBefore}. It is also more efficient to get the
+ * position after node by shifting `previousPosition` by `length`, using {@link engine.model.Position#getShiftedBy},
+ * then calculate the position using {@link engine.model.Position.createAfter}.
+ * * 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.model.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'} core.model.TreeWalkerDirection
+ */

+ 526 - 240
packages/ckeditor5-engine/tests/model/treewalker.js

@@ -16,11 +16,8 @@ import Range from '/ckeditor5/engine/model/range.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 
 describe( 'TreeWalker', () => {
-	let expectedSingle, expected, expectedIgnoreEnd, expectedSingleIgnoreEnd;
-	let expectedShallow, expectedCroppedStart, expectedCroppedEnd;
-
 	let doc, root, img1, paragraph, b, a, r, img2, x;
-	let rootBeginning;
+	let rootBeginning, rootEnding;
 
 	before( () => {
 		doc = new Document();
@@ -49,302 +46,591 @@ describe( 'TreeWalker', () => {
 		root.insertChildren( 0, [ img1, paragraph ] );
 
 		rootBeginning = new Position( root, [ 0 ] );
-
-		expected = [
-			{ type: 'ELEMENT_START', item: img1 },
-			{ type: 'ELEMENT_END', 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: 'ELEMENT_END', item: img2 },
-			{ type: 'TEXT', text: 'x', attrs: [] },
-			{ type: 'ELEMENT_END', item: paragraph }
-		];
-
-		expectedCroppedEnd = [
-			{ type: 'ELEMENT_START', item: img1 },
-			{ type: 'ELEMENT_END', item: img1 },
-			{ type: 'ELEMENT_START', item: paragraph },
-			{ type: 'TEXT', text: 'b', attrs: [ [ 'bold', true ] ] }
-		];
-
-		expectedCroppedStart = [
-			{ type: 'TEXT', text: 'a', attrs: [ [ 'bold', true ] ] },
-			{ type: 'TEXT', text: 'r', attrs: [] },
-			{ type: 'ELEMENT_START', item: img2 },
-			{ type: 'ELEMENT_END', item: img2 },
-			{ type: 'TEXT', text: 'x', attrs: [] },
-			{ type: 'ELEMENT_END', item: paragraph }
-		];
-
-		expectedSingle = [
-			{ 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 }
-		];
-
-		expectedIgnoreEnd = [
-			{ 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: [] }
-		];
-
-		expectedSingleIgnoreEnd = [
-			{ 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: [] }
-		];
-
-		expectedShallow = [
-			{ type: 'ELEMENT_START', item: img1 },
-			{ type: 'ELEMENT_START', item: paragraph }
-		];
+		rootEnding = new Position( root, [ 2 ] );
 	} );
 
-	function expectItem( item, expected, options ) {
-		expect( item.done ).to.be.false;
-
-		expectValue( item.value, expected, options );
-	}
-
-	function expectValue( value, expected, options ) {
-		expect( value.type ).to.equal( expected.type );
-
-		if ( value.type == 'TEXT' ) {
-			expectText( value, expected, options );
-		} else if ( value.type == 'CHARACTER' ) {
-			expectCharacter( 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.text ).to.equal( expected.text );
-		expect( Array.from( value.item.first._attrs ) ).to.deep.equal( expected.attrs );
-		expect( value.length ).to.equal( value.item.text.length );
-		expect( value.previousPosition ).to.deep.equal( Position.createBefore( value.item.first ) );
-		expect( value.nextPosition ).to.deep.equal( Position.createAfter( value.item.last ) );
-	}
+	describe( 'constructor', () => {
+		it( 'should throw if neither boundaries nor starting position is set', () => {
+			expect( () => {
+				new TreeWalker();
+			} ).to.throw( CKEditorError, /^tree-walker-no-start-position/ );
 
-	function expectCharacter( value, expected ) {
-		expect( value.item.character ).to.equal( expected.text );
-		expect( Array.from( value.item._attrs ) ).to.deep.equal( expected.attrs );
-		expect( value.length ).to.equal( value.item.character.length );
-		expect( value.previousPosition ).to.deep.equal( Position.createBefore( value.item ) );
-		expect( value.nextPosition ).to.deep.equal( Position.createAfter( value.item ) );
-	}
+			expect( () => {
+				new TreeWalker( {} );
+			} ).to.throw( CKEditorError, /^tree-walker-no-start-position/ );
 
-	function expectStart( value, expected, options ) {
-		expect( value.item ).to.equal( expected.item );
-		expect( value.length ).to.equal( 1 );
-		expect( value.previousPosition ).to.deep.equal( Position.createBefore( value.item ) );
+			expect( () => {
+				new TreeWalker( { singleCharacters: true } );
+			} ).to.throw( CKEditorError, /^tree-walker-no-start-position/ );
+		} );
 
-		if ( options && options.shallow ) {
-			expect( value.previousPosition ).to.deep.equal( Position.createBefore( value.item ) );
-		} else {
-			expect( value.nextPosition ).to.deep.equal( Position.createFromParentAndOffset( value.item, 0 ) );
-		}
-	}
+		it( 'should throw if walking direction is unknown', () => {
+			expect( () => {
+				new TreeWalker( { startPosition: rootBeginning, direction: 'UNKNOWN' } );
+			} ).to.throw( CKEditorError, /^tree-walker-unknown-direction/ );
+		} );
+	} );
 
-	function expectEnd( value, expected ) {
-		expect( value.item ).to.equal( expected.item );
-		expect( value.length ).to.be.undefined;
-		expect( value.previousPosition ).to.deep.equal(
-			Position.createFromParentAndOffset( value.item, value.item.getChildCount() ) );
-		expect( value.nextPosition ).to.deep.equal( Position.createAfter( value.item ) );
-	}
+	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: 'TEXT', text: 'ba', attrs: [ [ 'bold', true ] ] },
+				{ type: 'TEXT', text: 'r', attrs: [] },
+				{ type: 'ELEMENT_START', item: img2 },
+				{ type: 'ELEMENT_END', item: img2 },
+				{ type: 'TEXT', text: 'x', attrs: [] },
+				{ type: 'ELEMENT_END', item: paragraph }
+			];
+		} );
 
-	it( 'should provide iterator methods', () => {
-		let iterator = new TreeWalker( { startPosition: rootBeginning } );
+		it( 'should provide iterator interface with default FORWARD direction', () => {
+			let iterator = new TreeWalker( { startPosition: rootBeginning } );
+			let i = 0;
 
-		for ( let i = 0; i <= 8; i++ ) {
-			expectItem( iterator.next(), expected[ i ] );
-		}
-		expect( iterator.next() ).to.have.property( 'done' ).that.is.true;
-	} );
+			for ( let value of iterator ) {
+				expectValue( value, expected[ i ] );
+				i++;
+			}
 
-	it( 'should provide iterator interface', () => {
-		let iterator = new TreeWalker( { startPosition: rootBeginning } );
-		let i = 0;
+			expect( i ).to.equal( expected.length );
+		} );
 
-		for ( let value of iterator ) {
-			expectValue( value, expected[ i ] );
-			i++;
-		}
+		it( 'should provide iterator interface with FORWARD direction', () => {
+			let iterator = new TreeWalker( { startPosition: rootBeginning, direction: 'FORWARD' } );
+			let i = 0;
 
-		expect( i ).to.equal( expected.length );
-	} );
+			for ( let value of iterator ) {
+				expectValue( value, expected[ i ] );
+				i++;
+			}
 
-	it( 'should start at the startPosition', () => {
-		let iterator = new TreeWalker( { startPosition: new Position( root, [ 1 ] ) } );
-		let i = 2;
+			expect( i ).to.equal( expected.length );
+		} );
 
-		for ( let value of iterator ) {
-			expectValue( value, expected[ i ] );
-			i++;
-		}
+		it( 'should provide iterator interface which BACKWARD direction', () => {
+			let iterator = new TreeWalker( { startPosition: rootEnding, direction: 'BACKWARD' } );
+			let i = expected.length;
 
-		expect( i ).to.equal( expected.length );
-	} );
+			for ( let value of iterator ) {
+				expectValue( value, expected[ --i ], { direction: 'BACKWARD' } );
+			}
 
-	it( 'should iterating over the range', () => {
-		let start = new Position( root, [ 1 ] );
-		let end = new Position( root, [ 1, 4 ] );
-		let range = new Range( start, end );
+			expect( i ).to.equal( 0 );
+		} );
 
-		let iterator = new TreeWalker( { boundaries: range } );
-		let i = 2;
+		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++;
-		}
+			for ( let value of iterator ) {
+				expectValue( value, expected[ i ] );
+				i++;
+			}
 
-		expect( i ).to.equal( 7 );
-	} );
+			expect( i ).to.equal( expected.length );
+		} );
 
-	it( 'should start iterating at startPosition even if the range is defined', () => {
-		let start = new Position( root, [ 1 ] );
-		let end = new Position( root, [ 1, 4 ] );
-		let range = new Range( start, end );
+		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( { boundaries: range } );
-		let i = 2;
+			let iterator = new TreeWalker( { startPosition: new Position( root, [ 1 ] ), direction: 'BACKWARD' } );
+			let i = expected.length;
 
-		for ( let value of iterator ) {
-			expectValue( value, expected[ i ] );
-			i++;
-		}
+			for ( let value of iterator ) {
+				expectValue( value, expected[ --i ], { direction: 'BACKWARD' } );
+			}
 
-		expect( i ).to.equal( 7 );
+			expect( i ).to.equal( 0 );
+		} );
 	} );
 
-	it( 'should return part of the text if range starts inside the text', () => {
-		let iterator = new TreeWalker( { startPosition: new Position( root, [ 1, 1 ] ) } );
-		let i = 0;
+	describe( 'iterate trough the range `boundary`', () => {
+		describe( 'range starts between elements', () => {
+			let expected, range;
 
-		for ( let value of iterator ) {
-			expectValue( value, expectedCroppedStart[ i ] );
-			i++;
-		}
+			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 }
+				];
 
-		expect( i ).to.equal( expectedCroppedStart.length );
-	} );
+				range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 1, 4 ] ) );
+			} );
 
-	it( 'should return part of the text if range ends inside the text', () => {
-		let start = rootBeginning;
-		let end = new Position( root, [ 1, 1 ] );
-		let range = new Range( start, end );
+			it( 'should iterating over the range', () => {
+				let iterator = new TreeWalker( { boundaries: range } );
+				let i = 0;
 
-		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, expectedCroppedEnd[ i ] );
-			i++;
-		}
+				expect( i ).to.equal( expected.length );
+			} );
 
-		expect( i ).to.equal( expectedCroppedEnd.length );
-	} );
+			it( 'should iterating over the range going backward', () => {
+				let iterator = new TreeWalker( { boundaries: range, direction: 'BACKWARD' } );
+				let i = expected.length;
 
-	describe( 'singleCharacters', () => {
-		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 ], { direction: 'BACKWARD' } );
+				}
 
-			for ( let value of iterator ) {
-				expectValue( value, expectedSingle[ i ] );
-				i++;
-			}
+				expect( i ).to.equal( 0 );
+			} );
+		} );
 
-			expect( i ).to.equal( expectedSingle.length );
+		describe( 'range starts inside the text', () => {
+			let expected, range;
+
+			before( () => {
+				expected = [
+					{ type: 'TEXT', text: 'a', attrs: [ [ 'bold', true ] ] },
+					{ type: 'TEXT', text: 'r', attrs: [] },
+					{ type: 'ELEMENT_START', item: img2 },
+					{ type: 'ELEMENT_END', item: img2 }
+				];
+
+				range = new Range( new Position( root, [ 1, 1 ] ), new Position( root, [ 1, 4 ] ) );
+			} );
+
+			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 );
+			} );
 		} );
 
-		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', () => {
+			let expected, range;
+
+			before( () => {
+				expected = [
+					{ type: 'ELEMENT_START', item: img1 },
+					{ type: 'ELEMENT_END', item: img1 },
+					{ type: 'ELEMENT_START', item: paragraph },
+					{ type: 'TEXT', text: 'b', attrs: [ [ 'bold', true ] ] }
+				];
+
+				range = new Range( rootBeginning, new Position( root, [ 1, 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 );
+			} );
+		} );
 
-			let iterator = new TreeWalker( { boundaries: new Range( start, end ), singleCharacters: true } );
-			let i = 3;
+		describe( 'custom start position', () => {
+			it( 'should iterating from the start position', () => {
+				let expected = [
+					{ type: 'TEXT', text: 'r', attrs: [] },
+					{ type: 'ELEMENT_START', item: img2 },
+					{ type: 'ELEMENT_END', item: img2 }
+				];
+
+				let range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 1, 4 ] ) );
+
+				let iterator = new TreeWalker( {
+					boundaries: range,
+					startPosition: new Position( root, [ 1, 2 ] )
+				} );
+				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', attrs: [] },
+					{ type: 'ELEMENT_START', item: img2 },
+					{ type: 'ELEMENT_END', item: img2 }
+				];
+
+				let range = new Range( new Position( root, [ 1, 2 ] ), new Position( root, [ 1, 6 ] ) );
+
+				let iterator = new TreeWalker( {
+					boundaries: range,
+					startPosition: new Position( root, [ 1, 4 ] ),
+					direction: 'BACKWARD'
+				} );
+				let i = expected.length;
+
+				for ( let value of iterator ) {
+					expectValue( value, expected[ --i ], { direction: 'BACKWARD' } );
+				}
+
+				expect( i ).to.equal( 0 );
+			} );
+		} );
+	} );
 
-			for ( let value of iterator ) {
-				expectValue( value, expectedSingle[ i ] );
-				i++;
-			}
+	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: '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 ], { direction: 'BACKWARD' } );
+				}
+
+				expect( i ).to.equal( 0 );
+			} );
+		} );
 
-			expect( i ).to.equal( 7 );
+		describe( 'range', () => {
+			let start, end, range, expected;
+
+			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 ], { direction: 'BACKWARD' } );
+				}
+
+				expect( i ).to.equal( 0 );
+			} );
 		} );
 	} );
 
-	describe( 'shallow', () => {
+	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, expectedShallow[ i ], { shallow: true } );
+				expectValue( value, expected[ i ], { shallow: true } );
 				i++;
 			}
 
-			expect( i ).to.equal( expectedShallow.length );
+			expect( i ).to.equal( expected.length );
 		} );
-	} );
 
-	describe( 'ignoreElementEnd', () => {
-		it( 'should iterate ignoring ELEMENT_END', () => {
-			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 ) {
-				expectValue( value, expectedIgnoreEnd[ i ] );
-				i++;
+				expectValue( value, expected[ --i ], { shallow: true, direction: 'BACKWARD' } );
 			}
 
-			expect( i ).to.equal( expectedIgnoreEnd.length );
+			expect( i ).to.equal( 0 );
 		} );
+	} );
 
-		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, expectedSingleIgnoreEnd[ i ] );
-				i++;
-			}
+	describe( 'iterate omitting ELEMENT_END `ignoreElementEnd`', () => {
+		describe( 'merged text', () => {
+			let expected;
+
+			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;
+
+				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 );
+			} );
+		} );
 
-			expect( i ).to.equal( expectedSingleIgnoreEnd.length );
+		describe( 'single character', () => {
+			let expected;
+
+			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 ], { direction: 'BACKWARD' } );
+				}
+
+				expect( i ).to.equal( 0 );
+			} );
 		} );
 	} );
+} );
 
-	it( 'should throw if neither boundaries nor starting position is set', () => {
-		expect( () => {
-			new TreeWalker();
-		} ).to.throw( CKEditorError, /^tree-walker-no-start-position/ );
+function expectValue( value, expected, options ) {
+	expect( value.type ).to.equal( expected.type );
+
+	if ( value.type == 'TEXT' ) {
+		expectText( value, expected, options );
+	} else if ( value.type == 'CHARACTER' ) {
+		expectCharacter( value, expected, options );
+	} else if ( value.type == 'ELEMENT_START' ) {
+		expectStart( value, expected, options );
+	} else if ( value.type == 'ELEMENT_END' ) {
+		expectEnd( value, expected, options );
+	}
+}
 
-		expect( () => {
-			new TreeWalker( {} );
-		} ).to.throw( CKEditorError, /^tree-walker-no-start-position/ );
+function expectText( value, expected, options = {} ) {
+	let previousPosition, nextPosition;
 
-		expect( () => {
-			new TreeWalker( { mergeCharacters: true } );
-		} ).to.throw( CKEditorError, /^tree-walker-no-start-position/ );
-	} );
-} );
+	expect( value.item.text ).to.equal( expected.text );
+	expect( Array.from( value.item.first._attrs ) ).to.deep.equal( expected.attrs );
+	expect( value.length ).to.equal( value.item.text.length );
+
+	if ( options.direction == 'BACKWARD' ) {
+		previousPosition = Position.createAfter( value.item.last );
+		nextPosition = Position.createBefore( value.item.first );
+	} else {
+		previousPosition = Position.createBefore( value.item.first );
+		nextPosition = Position.createAfter( value.item.last );
+	}
+
+	expect( value.previousPosition ).to.deep.equal( previousPosition );
+	expect( value.nextPosition ).to.deep.equal( nextPosition );
+}
+
+function expectCharacter( value, expected, options = {} ) {
+	let previousPosition, nextPosition;
+
+	expect( value.item.character ).to.equal( expected.text );
+	expect( Array.from( value.item._attrs ) ).to.deep.equal( expected.attrs );
+	expect( value.length ).to.equal( value.item.character.length );
+
+	if ( options.direction == 'BACKWARD' ) {
+		previousPosition = Position.createAfter( value.item );
+		nextPosition = Position.createBefore( value.item );
+	} else {
+		previousPosition = Position.createBefore( value.item );
+		nextPosition = Position.createAfter( value.item );
+	}
+
+	expect( value.previousPosition ).to.deep.equal( previousPosition );
+	expect( value.nextPosition ).to.deep.equal( nextPosition );
+}
+
+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 = Position.createFromParentAndOffset( 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 = Position.createFromParentAndOffset( value.item, value.item.getChildCount() );
+	} else {
+		previousPosition = Position.createFromParentAndOffset( value.item, value.item.getChildCount() );
+		nextPosition = Position.createAfter( value.item );
+	}
+
+	expect( value.previousPosition ).to.deep.equal( previousPosition );
+	expect( value.nextPosition ).to.deep.equal( nextPosition );
+}