Преглед на файлове

Changed default TreeWalker behavior to return merged characters by default.
Removed Range.getValues, added iterator interface to Range.

Piotr Jasiun преди 9 години
родител
ревизия
8bc0981558

+ 1 - 1
packages/ckeditor5-engine/src/treemodel/delta/attributedelta.js

@@ -98,7 +98,7 @@ function changeRange( doc, delta, attributeKey, attributeValue, range ) {
 	// Value after the currently position.
 	let attributeValueAfter;
 
-	for ( let value of range.getValues( true ) ) {
+	for ( let value of range ) {
 		attributeValueAfter = value.item.getAttribute( attributeKey );
 
 		// At the first run of the iterator the position in undefined. We also do not have a attributeValueBefore, but

+ 1 - 1
packages/ckeditor5-engine/src/treemodel/operation/attributeoperation.js

@@ -90,7 +90,7 @@ export default class AttributeOperation extends Operation {
 	}
 
 	_execute() {
-		for ( let item of this.range.getItems( true ) ) {
+		for ( let item of this.range.getItems() ) {
 			if ( this.oldValue !== null && item.getAttribute( this.key ) !== this.oldValue ) {
 				/**
 				 * The attribute which should be removed does not exists for the given node.

+ 39 - 31
packages/ckeditor5-engine/src/treemodel/range.js

@@ -254,29 +254,24 @@ export default class Range {
 	/**
 	 * Returns an iterator that iterates over all {@link core.treeModel.Item items} that are in this range and returns
 	 * them together with additional information like length or {@link core.treeModel.Position positions},
-	 * grouped as {@link core.treeModel.TreeWalkerValue}.
+	 * grouped as {@link core.treeModel.TreeWalkerValue}. It iterates over all {@link core.treeModel.TextFragment texts}
+	 * that are inside the range and all the {@link core.treeModel.Element}s we enter into when iterating over this
+	 * range.
 	 *
-	 * A node is in the range when it is after a {@link core.treeModel.Position position} contained in this range.
-	 * In other words, this iterates over all text characters that are inside the range and all the
-	 * {@link core.treeModel.Element}s we enter into when iterating over this range.
-	 *
-	 * **Note:** this method will not return a parent node of start position. This is in contrary to
-	 * {@link core.treeModel.TreeWalker} which will return that node with `'ELEMENT_END'` type. This method also
+	 * **Note:** iterator will not return a parent node of start position. This is in contrary to
+	 * {@link core.treeModel.TreeWalker} which will return that node with `'ELEMENT_END'` type. Iterator also
 	 * returns each {@link core.treeModel.Element} once, while simply used {@link core.treeModel.TreeWalker} might
 	 * return it twice: for `'ELEMENT_START'` and `'ELEMENT_END'`.
 	 *
-	 * **Note:** because this method does not return {@link core.treeModel.TreeWalkerValue values} with the type of
+	 * **Note:** because iterator does not return {@link core.treeModel.TreeWalkerValue values} with the type of
 	 * `'ELEMENT_END'`, you can use {@link core.treeModel.TreeWalkerValue.previousPosition} as a position before the
 	 * item.
 	 *
-	 * @see {core.treeModel.TreeWalker}
-	 * @param {Boolean} [mergeCharacters] Flag indicating whether all consecutive characters with the same attributes
-	 * should be returned as one {@link core.treeModel.TextFragment} (`true`) or one by one as multiple
-	 * {@link core.treeModel.CharacterProxy} (`false`) objects. Defaults to `false`.
+	 * @see {@link core.treeModel.TreeWalker}
 	 * @returns {Iterable.<core.treeModel.TreeWalkerValue>}
 	 */
-	*getValues( mergeCharacters ) {
-		const treeWalker = new TreeWalker( { boundaries: this, mergeCharacters: mergeCharacters } );
+	*[ Symbol.iterator ]() {
+		const treeWalker = new TreeWalker( { boundaries: this } );
 
 		for ( let value of treeWalker ) {
 			if ( value.type != 'ELEMENT_END' ) {
@@ -286,16 +281,29 @@ export default class Range {
 	}
 
 	/**
-	 * Use {@link core.treeModel.Range#getValues}, but return only {@link core.treeModel.Item items} from
-	 * {@link core.treeModel.TreeWalkerValue}.
+	 * Returns an iterator that iterates over all {@link core.treeModel.Item items} that are in this range and returns
+	 * them. It iterates over all {@link core.treeModel.CharacterProxy characters} or
+	 * {@link core.treeModel.TextFragment texts} that are inside the range and all the {@link core.treeModel.Element}s
+	 * we enter into when iterating over this range.
 	 *
-	 * @see {core.treeModel.Range#getValues}
-	 * @param {Boolean} [mergeCharacters] See {@link core.treeModel.Range#getValues}.
+	 * **Note:** this method will not return a parent node of start position. This is in contrary to
+	 * {@link core.treeModel.TreeWalker} which will return that node with `'ELEMENT_END'` type. This method also
+	 * returns each {@link core.treeModel.Element} once, while simply used {@link core.treeModel.TreeWalker} might
+	 * return it twice: for `'ELEMENT_START'` and `'ELEMENT_END'`.
+	 *
+	 * @see {@link core.treeModel.TreeWalker}
+	 * @param {Boolean} [singleCharacters] Flag indicating whether all consecutive characters with the same attributes
+	 * should be returned one by one as multiple {@link core.treeModel.CharacterProxy} (`true`) objects or as one
+	 * {@link core.treeModel.TextFragment} (`false`). Defaults to `false`.
 	 * @returns {Iterable.<core.treeModel.Item>}
 	 */
-	*getItems( mergeCharacters ) {
-		for ( let value of this.getValues( mergeCharacters ) ) {
-			yield value.item;
+	*getItems( singleCharacters ) {
+		const treeWalker = new TreeWalker( { boundaries: this, singleCharacters: singleCharacters } );
+
+		for ( let value of treeWalker ) {
+			if ( value.type != 'ELEMENT_END' ) {
+				yield value.item;
+			}
 		}
 	}
 
@@ -303,13 +311,13 @@ export default class Range {
 	 * Returns an iterator that iterates over all {@link core.treeModel.Position positions} that are boundaries or
 	 * contained in this range.
 	 *
-	 * @param {Boolean} [mergeCharacters] Flag indicating whether all consecutive characters with the same attributes
-	 * should be returned as one {@link core.treeModel.TextFragment} (`true`) or one by one as multiple {@link core.treeModel.CharacterProxy}
-	 * (`false`) objects. Defaults to `false`.
+	 * @param {Boolean} [singleCharacters] Flag indicating whether all consecutive characters with the same attributes
+	 * should be returned one by one as multiple {@link core.treeModel.CharacterProxy} (`true`) objects or as one
+	 * {@link core.treeModel.TextFragment} (`false`). Defaults to `false`.
 	 * @returns {Iterable.<core.treeModel.Position>}
 	 */
-	*getPositions( mergeCharacters ) {
-		const treeWalker = new TreeWalker( { boundaries: this, mergeCharacters: mergeCharacters } );
+	*getPositions( singleCharacters ) {
+		const treeWalker = new TreeWalker( { boundaries: this, singleCharacters: singleCharacters } );
 
 		yield treeWalker.position;
 
@@ -323,18 +331,18 @@ export default class Range {
 	 * and returns them. A node is a top-level node when it is in the range but it's parent is not. In other words,
 	 * this function splits the range into separate sub-trees and iterates over their roots.
 	 *
-	 * @param {Boolean} [mergeCharacters] Flag indicating whether all consecutive characters with the same attributes
-	 * should be returned as one {@link core.treeModel.TextFragment} (`true`) or one by one as multiple {@link core.treeModel.CharacterProxy}
-	 * (`false`) objects. Defaults to `false`.
+	 * @param {Boolean} [singleCharacters] Flag indicating whether all consecutive characters with the same attributes
+	 * should be returned one by one as multiple {@link core.treeModel.CharacterProxy} (`true`) objects or as one
+	 * {@link core.treeModel.TextFragment} (`false`). Defaults to `false`.
 	 * @returns {Iterable.<core.treeModel.Node|treeModel.TextFragment>}
 	 */
-	*getTopLevelNodes( mergeCharacters ) {
+	*getTopLevelNodes( singleCharacters ) {
 		let flatRanges = this.getMinimalFlatRanges();
 
 		for ( let range of flatRanges ) {
 			// This loop could be much simpler as we could just iterate over siblings of node after the first
 			// position of each range. But then we would have to re-implement character merging strategy here.
-			let it = new TreeWalker( { boundaries: range, mergeCharacters: mergeCharacters } );
+			let it = new TreeWalker( { boundaries: range, singleCharacters: singleCharacters } );
 			let step;
 
 			// We will only return nodes that are on same level as node after the range start. To do this,

+ 16 - 16
packages/ckeditor5-engine/src/treemodel/treewalker.js

@@ -23,9 +23,9 @@ export default class TreeWalker {
 	 * @param {Object} options Object with configuration.
 	 * @param {core.treeModel.Range} [options.boundaries] Range to define boundaries of the iterator.
 	 * @param {core.treeModel.Position} [options.position] Starting position.
-	 * @param {Boolean} [options.mergeCharacters=false] Flag indicating whether all consecutive characters with the same attributes
-	 * should be returned as one {@link core.treeModel.TextFragment} (`true`) or one by one as multiple {@link core.treeModel.CharacterProxy}
-	 * (`false`) objects.
+	 * @param {Boolean} [options.singleCharacters=false] Flag indicating whether all consecutive characters with the same attributes
+	 * should be returned one by one as multiple {@link core.treeModel.CharacterProxy} (`true`) objects or as one
+	 * {@link core.treeModel.TextFragment} (`false`).
 	 * @constructor
 	 */
 	constructor( options ) {
@@ -82,7 +82,7 @@ export default class TreeWalker {
 		 *
 		 * @type {Boolean}
 		 */
-		this.mergeCharacters = !!options.mergeCharacters;
+		this.singleCharacters = !!options.singleCharacters;
 
 		/**
 		 * Parent of the most recently visited node. Cached for optimization purposes.
@@ -133,7 +133,12 @@ export default class TreeWalker {
 
 			return formatReturnValue( 'ELEMENT_START', node, previousPosition, position, 1 );
 		} else if ( node instanceof CharacterProxy ) {
-			if ( this.mergeCharacters ) {
+			if ( this.singleCharacters ) {
+				position.offset++;
+				this.position = position;
+
+				return formatReturnValue( 'CHARACTER', node, previousPosition, position, 1 );
+			} else {
 				let charactersCount = node._nodeListText.text.length - node._index;
 				let offset = position.offset + charactersCount;
 
@@ -148,11 +153,6 @@ export default class TreeWalker {
 				this.position = position;
 
 				return formatReturnValue( 'TEXT', textFragment, previousPosition, position, charactersCount );
-			} else {
-				position.offset++;
-				this.position = position;
-
-				return formatReturnValue( 'CHARACTER', node, previousPosition, position, 1 );
 			}
 		} else {
 			position.path.pop();
@@ -199,7 +199,12 @@ export default class TreeWalker {
 
 			return formatReturnValue( 'ELEMENT_END', node, position, previousPosition );
 		} else if ( node instanceof CharacterProxy ) {
-			if ( this.mergeCharacters ) {
+			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;
 
@@ -214,11 +219,6 @@ export default class TreeWalker {
 				this.position = position;
 
 				return formatReturnValue( 'TEXT', textFragment, position, previousPosition, charactersCount );
-			} else {
-				position.offset--;
-				this.position = position;
-
-				return formatReturnValue( 'CHARACTER', node, position, previousPosition, 1 );
 			}
 		} else {
 			position.path.pop();

+ 2 - 2
packages/ckeditor5-engine/tests/treemodel/delta/attributedelta.js

@@ -135,7 +135,7 @@ describe( 'Batch', () => {
 
 			for ( let delta of batch.deltas ) {
 				for ( let operation of delta.operations ) {
-					count += getIteratorCount( operation.range.getValues() );
+					count += getIteratorCount( operation.range.getItems( true ) );
 				}
 			}
 
@@ -146,7 +146,7 @@ describe( 'Batch', () => {
 			// default: 111---111222---1112------
 			const range = Range.createFromElement( root );
 
-			return Array.from( range.getValues() ).map( value => value.item.getAttribute( 'a' ) || '-' ).join( '' );
+			return Array.from( range.getItems( true ) ).map( item => item.getAttribute( 'a' ) || '-' ).join( '' );
 		}
 
 		describe( 'setAttr', () => {

+ 25 - 50
packages/ckeditor5-engine/tests/treemodel/range.js

@@ -186,47 +186,32 @@ describe( 'Range', () => {
 		} );
 	} );
 
-	describe( 'getValues', () => {
-		it( 'should iterate over all nodes which "starts" in the range', () => {
-			const a = new Text( 'a' );
-			const b = new Text( 'b' );
-			const x = new Text( 'x' );
-			const y = new Text( 'y' );
-
-			const e1 = new Element( 'e1' );
-			const e2 = new Element( 'e2' );
-
-			e1.insertChildren( 0, [ a, b ] );
-			e2.insertChildren( 0, [ x, y ] );
-			root.insertChildren( 0, [ e1, e2 ] );
-
-			let range = new Range(
-				new Position( root, [ 0, 1 ] ),
-				new Position( root, [ 1, 1 ] )
-			);
-
-			let values = Array.from( range.getValues() );
-
-			expect( values.length ).to.equal( 3 );
-			expect( values[ 0 ].item.character ).to.equal( 'b' );
-			expect( values[ 0 ].length ).to.equal( 1 );
-			expect( values[ 1 ].item ).to.equal( e2 );
-			expect( values[ 2 ].item.character ).to.equal( 'x' );
-		} );
-
+	describe( 'iterator', () => {
 		it( 'should merge characters with same attributes', () => {
 			prepareRichRoot( root );
 
 			let range = new Range( new Position( root, [ 0, 0, 3 ] ), new Position( root, [ 3, 0, 2 ] ) );
-			let nodes = Array.from( range.getValues( true ) ).map( value => value.item );
+			let nodes = Array.from( range ).map( value => value.item );
+			let lengths = Array.from( range ).map( value => value.length );
 			let nodeNames = mapNodesToNames( nodes );
 
 			expect( nodeNames ).to.deep.equal( [ 'T:st', 'E:p', 'T:lorem ipsum', 'E:p', 'T:foo', 'E:p', 'T:bar', 'E:div', 'E:h', 'T:se' ] );
+			expect( lengths ).to.deep.equal( [ 2, 1, 11, 1, 3, 1, 3, 1, 1, 2 ] );
 		} );
 	} );
 
 	describe( 'getItems', () => {
-		it( 'should iterate over all items which "starts" in the range', () => {
+		it( 'should iterate over all items in the range', () => {
+			prepareRichRoot( root );
+
+			let range = new Range( new Position( root, [ 0, 0, 3 ] ), new Position( root, [ 3, 0, 2 ] ) );
+			let items = Array.from( range.getItems() );
+			let nodeNames = mapNodesToNames( items );
+
+			expect( nodeNames ).to.deep.equal( [ 'T:st', 'E:p', 'T:lorem ipsum', 'E:p', 'T:foo', 'E:p', 'T:bar', 'E:div', 'E:h', 'T:se' ] );
+		} );
+
+		it( 'should iterate over all items in the range as single characters', () => {
 			const a = new Text( 'a' );
 			const b = new Text( 'b' );
 			const x = new Text( 'x' );
@@ -244,23 +229,13 @@ describe( 'Range', () => {
 				new Position( root, [ 1, 1 ] )
 			);
 
-			let items = Array.from( range.getItems() );
+			let items = Array.from( range.getItems( true ) );
 
 			expect( items.length ).to.equal( 3 );
 			expect( items[ 0 ].character ).to.equal( 'b' );
 			expect( items[ 1 ] ).to.equal( e2 );
 			expect( items[ 2 ].character ).to.equal( 'x' );
 		} );
-
-		it( 'should merge characters with same attributes', () => {
-			prepareRichRoot( root );
-
-			let range = new Range( new Position( root, [ 0, 0, 3 ] ), new Position( root, [ 3, 0, 2 ] ) );
-			let items = Array.from( range.getItems( true ) );
-			let nodeNames = mapNodesToNames( items );
-
-			expect( nodeNames ).to.deep.equal( [ 'T:st', 'E:p', 'T:lorem ipsum', 'E:p', 'T:foo', 'E:p', 'T:bar', 'E:div', 'E:h', 'T:se' ] );
-		} );
 	} );
 
 	describe( 'containsPosition', () => {
@@ -476,18 +451,18 @@ describe( 'Range', () => {
 
 		it( 'should iterate over all top-level nodes of this range', () => {
 			let range = new Range( new Position( root, [ 0, 0, 3 ] ), new Position( root, [ 3, 0, 2 ] ) );
-			let nodes = Array.from( range.getTopLevelNodes() );
+			let nodes = Array.from( range.getTopLevelNodes(  ) );
 			let nodeNames = mapNodesToNames( nodes );
 
-			expect( nodeNames ).to.deep.equal( [ 'T:s', 'T:t', 'E:p', 'E:p', 'E:p', 'T:s', 'T:e' ] );
+			expect( nodeNames ).to.deep.equal( [ 'T:st', 'E:p', 'E:p', 'E:p', 'T:se' ] );
 		} );
 
-		it( 'should merge characters with same attributes', () => {
+		it( 'should return single characters iterating over all top-level nodes of this range', () => {
 			let range = new Range( new Position( root, [ 0, 0, 3 ] ), new Position( root, [ 3, 0, 2 ] ) );
 			let nodes = Array.from( range.getTopLevelNodes( true ) );
 			let nodeNames = mapNodesToNames( nodes );
 
-			expect( nodeNames ).to.deep.equal( [ 'T:st', 'E:p', 'E:p', 'E:p', 'T:se' ] );
+			expect( nodeNames ).to.deep.equal( [ 'T:s', 'T:t', 'E:p', 'E:p', 'E:p', 'T:s', 'T:e' ] );
 		} );
 	} );
 
@@ -499,8 +474,8 @@ describe( 'Range', () => {
 		it( 'should iterate over all positions in this range', () => {
 			let expectedPaths = [
 				[ 1, 2 ], [ 1, 3 ],
-				[ 2 ], [ 2, 0 ], [ 2, 1 ], [ 2, 2 ], [ 2, 3 ],
-				[ 3 ], [ 3, 0 ], [ 3, 0, 0 ], [ 3, 0, 1 ], [ 3, 0, 2 ]
+				[ 2 ], [ 2, 0 ], [ 2, 3 ],
+				[ 3 ], [ 3, 0 ], [ 3, 0, 0 ], [ 3, 0, 2 ]
 			];
 			let range = new Range( new Position( root, [ 1, 2 ] ), new Position( root, [ 3, 0, 2 ] ) );
 			let i = 0;
@@ -513,11 +488,11 @@ describe( 'Range', () => {
 			expect( i ).to.equal( expectedPaths.length );
 		} );
 
-		it( 'should merge characters with same attributes', () => {
+		it( 'should return single nodes iterating over all positions in this range', () => {
 			let expectedPaths = [
 				[ 1, 2 ], [ 1, 3 ],
-				[ 2 ], [ 2, 0 ], [ 2, 3 ],
-				[ 3 ], [ 3, 0 ], [ 3, 0, 0 ], [ 3, 0, 2 ]
+				[ 2 ], [ 2, 0 ], [ 2, 1 ], [ 2, 2 ], [ 2, 3 ],
+				[ 3 ], [ 3, 0 ], [ 3, 0, 0 ], [ 3, 0, 1 ], [ 3, 0, 2 ]
 			];
 			let range = new Range( new Position( root, [ 1, 2 ] ), new Position( root, [ 3, 0, 2 ] ) );
 			let i = 0;

+ 92 - 88
packages/ckeditor5-engine/tests/treemodel/treewalker.js

@@ -16,7 +16,7 @@ import Range from '/ckeditor5/core/treemodel/range.js';
 import CKEditorError from '/ckeditor5/core/ckeditorerror.js';
 
 describe( 'range iterator', () => {
-	let doc, expectedItems, expectedItemsMerged, root, img1, paragraph, b, a, r, img2, x;
+	let doc, expectedItemsSingle, expectedItemsMerged, root, img1, paragraph, b, a, r, img2, x;
 
 	before( () => {
 		doc = new Document();
@@ -44,7 +44,7 @@ describe( 'range iterator', () => {
 
 		root.insertChildren( 0, [ img1, paragraph ] );
 
-		expectedItems = [
+		expectedItemsSingle = [
 			{ type: 'ELEMENT_START', item: img1 },
 			{ type: 'ELEMENT_END', item: img1 },
 			{ type: 'ELEMENT_START', item: paragraph },
@@ -121,120 +121,124 @@ describe( 'range iterator', () => {
 		expect( value.nextPosition ).to.deep.equal( Position.createAfter( value.item ) );
 	}
 
-	it( 'should return next position', () => {
-		let iterator = new TreeWalker( { position: new Position( root, [ 0 ] ) } ); // beginning of root
-		let i, len;
+	describe( 'merged characters', () => {
+		it( 'should iterating over the range using next', () => {
+			let start = new Position( root, [ 1 ] );
+			let end = new Position( root, [ 1, 4 ] );
+			let range = new Range( start, end );
 
-		for ( i = 0, len = expectedItems.length; i < len; i++ ) {
-			expectItem( iterator.next(), expectedItems[ i ] );
-		}
-		expect( iterator.next() ).to.have.property( 'done' ).that.is.true;
-	} );
+			let iterator = new TreeWalker( { boundaries: range, position: range.start } );
+			let i;
 
-	it( 'should return previous position', () => {
-		let iterator = new TreeWalker( { position: new Position( root, [ 2 ] ) } ); // ending of root
+			for ( i = 2; i <= 6; i++ ) {
+				expectItem( iterator.next(), expectedItemsMerged[ i ] );
+			}
+			expect( iterator.next() ).to.have.property( 'done' ).that.is.true;
+		} );
 
-		for ( let i = expectedItems.length - 1; i >= 0; i-- ) {
-			expectItem( iterator.previous(), expectedItems[ i ] );
-		}
-		expect( iterator.previous() ).to.have.property( 'done' ).that.is.true;
-	} );
+		it( 'should iterating over the range using previous', () => {
+			let start = new Position( root, [ 1 ] );
+			let end = new Position( root, [ 1, 4 ] );
+			let range = new Range( start, end );
 
-	it( 'should return next position in the boundaries', () => {
-		let start = new Position( root, [ 1, 0 ] ); // p, 0
-		let end = new Position( root, [ 1, 3, 0 ] ); // img, 0
+			let iterator = new TreeWalker( { boundaries: range, position: range.end } );
 
-		let iterator = new TreeWalker( { boundaries: new Range( start, end ) } );
+			for ( let i = 6; i >= 2; i-- ) {
+				expectItem( iterator.previous(), expectedItemsMerged[ i ] );
+			}
+			expect( iterator.previous() ).to.have.property( 'done' ).that.is.true;
+		} );
 
-		let i, len;
-
-		for ( i = 3, len = expectedItems.length; i < 7; i++ ) {
-			expectItem( iterator.next(), expectedItems[ i ] );
-		}
-		expect( iterator.next() ).to.have.property( 'done' ).that.is.true;
-	} );
+		it( 'should respect boundaries when iterating using next', () => {
+			let start = new Position( root, [ 1, 0 ] );
+			let end = new Position( root, [ 1, 1 ] );
+			let range = new Range( start, end );
 
-	it( 'should return previous position in the boundaries', () => {
-		let start = new Position( root, [ 1, 0 ] ); // p, 0
-		let end = new Position( root, [ 1, 3, 0 ] ); // img, 0
+			let iterator = new TreeWalker( { boundaries: range, position: range.start } );
+			let val = iterator.next();
 
-		let iterator = new TreeWalker( { boundaries: new Range( start, end ), position: end } );
+			expect( val.done ).to.be.false;
+			expect( val.value.item.text ).to.equal( 'b' );
 
-		let i, len;
+			val = iterator.next();
+			expect( val.done ).to.be.true;
+		} );
 
-		for ( i = 6, len = expectedItems.length; i > 2; i-- ) {
-			expectItem( iterator.previous(), expectedItems[ i ] );
-		}
-		expect( iterator.previous() ).to.have.property( 'done' ).that.is.true;
-	} );
+		it( 'should respect boundaries when iterating using previous', () => {
+			let start = new Position( root, [ 1, 1 ] );
+			let end = new Position( root, [ 1, 2 ] );
+			let range = new Range( start, end );
 
-	it( 'should provide iterator interface', () => {
-		let iterator = new TreeWalker( { position: new Position( root, [ 0 ] ) } );
-		let i = 0;
+			let iterator = new TreeWalker( { boundaries: range, position: range.end } );
+			let val = iterator.previous();
 
-		for ( let value of iterator ) {
-			expectValue( value, expectedItems[ i ] );
-			i++;
-		}
+			expect( val.done ).to.be.false;
+			expect( val.value.item.text ).to.equal( 'a' );
 
-		expect( i ).to.equal( 10 );
+			val = iterator.previous();
+			expect( val.done ).to.be.true;
+		} );
 	} );
 
-	it( 'should merge characters when iterating over the range using next', () => {
-		let start = new Position( root, [ 1 ] );
-		let end = new Position( root, [ 1, 4 ] );
-		let range = new Range( start, end );
+	describe( 'single characters', () => {
+		it( 'should iterating over the range using next', () => {
+			let iterator = new TreeWalker( { position: new Position( root, [ 0 ] ), singleCharacters: true } ); // beginning of root
+			let i, len;
 
-		let iterator = new TreeWalker( { boundaries: range, position: range.start, mergeCharacters: true } );
-		let i;
+			for ( i = 0, len = expectedItemsSingle.length; i < len; i++ ) {
+				expectItem( iterator.next(), expectedItemsSingle[ i ] );
+			}
+			expect( iterator.next() ).to.have.property( 'done' ).that.is.true;
+		} );
 
-		for ( i = 2; i <= 6; i++ ) {
-			expectItem( iterator.next(), expectedItemsMerged[ i ] );
-		}
-		expect( iterator.next() ).to.have.property( 'done' ).that.is.true;
-	} );
+		it( 'should iterating over the range using previous', () => {
+			let iterator = new TreeWalker( { position: new Position( root, [ 2 ] ), singleCharacters: true } ); // ending of root
 
-	it( 'should merge characters when iterating over the range using previous', () => {
-		let start = new Position( root, [ 1 ] );
-		let end = new Position( root, [ 1, 4 ] );
-		let range = new Range( start, end );
+			for ( let i = expectedItemsSingle.length - 1; i >= 0; i-- ) {
+				expectItem( iterator.previous(), expectedItemsSingle[ i ] );
+			}
+			expect( iterator.previous() ).to.have.property( 'done' ).that.is.true;
+		} );
 
-		let iterator = new TreeWalker( { boundaries: range, position: range.end, mergeCharacters: true } );
+		it( 'should respect boundaries when iterating using next', () => {
+			let start = new Position( root, [ 1, 0 ] ); // p, 0
+			let end = new Position( root, [ 1, 3, 0 ] ); // img, 0
 
-		for ( let i = 6; i >= 2; i-- ) {
-			expectItem( iterator.previous(), expectedItemsMerged[ i ] );
-		}
-		expect( iterator.previous() ).to.have.property( 'done' ).that.is.true;
-	} );
+			let iterator = new TreeWalker( { boundaries: new Range( start, end ), singleCharacters: true } );
 
-	it( 'should respect boundaries when iterating using next and merging characters', () => {
-		let start = new Position( root, [ 1, 0 ] );
-		let end = new Position( root, [ 1, 1 ] );
-		let range = new Range( start, end );
+			let i, len;
 
-		let iterator = new TreeWalker( { boundaries: range, position: range.start, mergeCharacters: true } );
-		let val = iterator.next();
+			for ( i = 3, len = expectedItemsSingle.length; i < 7; i++ ) {
+				expectItem( iterator.next(), expectedItemsSingle[ i ] );
+			}
+			expect( iterator.next() ).to.have.property( 'done' ).that.is.true;
+		} );
 
-		expect( val.done ).to.be.false;
-		expect( val.value.item.text ).to.equal( 'b' );
+		it( 'should respect boundaries when iterating using previous', () => {
+			let start = new Position( root, [ 1, 0 ] ); // p, 0
+			let end = new Position( root, [ 1, 3, 0 ] ); // img, 0
 
-		val = iterator.next();
-		expect( val.done ).to.be.true;
-	} );
+			let iterator = new TreeWalker( { boundaries: new Range( start, end ), position: end, singleCharacters: true } );
+
+			let i, len;
 
-	it( 'should respect boundaries when iterating using previous and merging characters', () => {
-		let start = new Position( root, [ 1, 1 ] );
-		let end = new Position( root, [ 1, 2 ] );
-		let range = new Range( start, end );
+			for ( i = 6, len = expectedItemsSingle.length; i > 2; i-- ) {
+				expectItem( iterator.previous(), expectedItemsSingle[ i ] );
+			}
+			expect( iterator.previous() ).to.have.property( 'done' ).that.is.true;
+		} );
+	} );
 
-		let iterator = new TreeWalker( { boundaries: range, position: range.end, mergeCharacters: true } );
-		let val = iterator.previous();
+	it( 'should provide iterator interface', () => {
+		let iterator = new TreeWalker( { position: new Position( root, [ 0 ] ) } );
+		let i = 0;
 
-		expect( val.done ).to.be.false;
-		expect( val.value.item.text ).to.equal( 'a' );
+		for ( let value of iterator ) {
+			expectValue( value, expectedItemsMerged[ i ] );
+			i++;
+		}
 
-		val = iterator.previous();
-		expect( val.done ).to.be.true;
+		expect( i ).to.equal( 9 );
 	} );
 
 	it( 'should throw if neither boundaries nor starting position is set', () => {