Explorar o código

Merge pull request #241 from ckeditor/t/100

t/100: Range iteration refactoring.
Szymon Cofalik %!s(int64=9) %!d(string=hai) anos
pai
achega
d6bc50e200

+ 35 - 63
packages/ckeditor5-engine/src/treemodel/range.js

@@ -267,43 +267,49 @@ export default class Range {
 	 * `'ELEMENT_END'`, you can use {@link core.treeModel.TreeWalkerValue.previousPosition} as a position before the
 	 * item.
 	 *
-	 * @see {@link core.treeModel.TreeWalker}
+	 * @see core.treeModel.TreeWalker
 	 * @returns {Iterable.<core.treeModel.TreeWalkerValue>}
 	 */
 	*[ Symbol.iterator ]() {
-		const treeWalker = new TreeWalker( { boundaries: this } );
+		yield* new TreeWalker( { boundaries: this, ignoreElementEnd: true } );
+	}
 
-		for ( let value of treeWalker ) {
-			if ( value.type != 'ELEMENT_END' ) {
-				yield value;
-			}
-		}
+	/**
+	 * Creates a {@link core.treeModel.TreeWalker} instance with this range as a boundary.
+	 *
+	 * @param {Object} options Object with configuration options. See {@link core.treeModel.TreeWalker}.
+	 * @param {core.treeModel.Position} [options.startPosition]
+	 * @param {Boolean} [options.singleCharacters=false]
+	 * @param {Boolean} [options.shallow=false]
+	 * @param {Boolean} [options.ignoreElementEnd=false]
+	 */
+	getWalker( options = {} ) {
+		options.boundaries = this;
+
+		return new TreeWalker( options );
 	}
 
 	/**
 	 * 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.
-	 *
-	 * **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'`.
+	 * we enter into when iterating over this range. Note that it use {@link core.treeModel.TreeWalker} with the
+	 * {@link core.treeModel.TreeWalker#ignoreElementEnd ignoreElementEnd} option set to true.
 	 *
-	 * @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`.
+	 * @param {Object} options Object with configuration options. See {@link core.treeModel.TreeWalker}.
+	 * @param {core.treeModel.Position} [options.startPosition]
+	 * @param {Boolean} [options.singleCharacters=false]
+	 * @param {Boolean} [options.shallow=false]
 	 * @returns {Iterable.<core.treeModel.Item>}
 	 */
-	*getItems( singleCharacters ) {
-		const treeWalker = new TreeWalker( { boundaries: this, singleCharacters: singleCharacters } );
+	*getItems( options = {} ) {
+		options.boundaries = this;
+		options.ignoreElementEnd = true;
+
+		const treeWalker = new TreeWalker( options );
 
 		for ( let value of treeWalker ) {
-			if ( value.type != 'ELEMENT_END' ) {
-				yield value.item;
-			}
+			yield value.item;
 		}
 	}
 
@@ -311,13 +317,15 @@ 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} [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`.
+	 * @param {Object} options Object with configuration options. See {@link core.treeModel.TreeWalker}.
+	 * @param {Boolean} [options.singleCharacters=false]
+	 * @param {Boolean} [options.shallow=false]
 	 * @returns {Iterable.<core.treeModel.Position>}
 	 */
-	*getPositions( singleCharacters ) {
-		const treeWalker = new TreeWalker( { boundaries: this, singleCharacters: singleCharacters } );
+	*getPositions( options = {} ) {
+		options.boundaries = this;
+
+		const treeWalker = new TreeWalker( options );
 
 		yield treeWalker.position;
 
@@ -326,42 +334,6 @@ export default class Range {
 		}
 	}
 
-	/**
-	 * Returns an iterator that iterates over all {@link core.treeModel.Node nodes} that are top-level nodes in this 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} [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( 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 walker = new TreeWalker( { boundaries: range, singleCharacters: singleCharacters } );
-
-			// We will only return nodes that are on same level as node after the range start. To do this,
-			// we keep "depth" counter.
-			let depth = 0;
-
-			for ( let value of walker ) {
-				if ( value.type == 'ELEMENT_START' ) {
-					depth++;
-				} else if ( value.type == 'ELEMENT_END' ) {
-					depth--;
-				}
-
-				if ( depth === 0 ) {
-					yield value.item;
-				}
-			}
-		}
-	}
-
 	/**
 	 * Returns an array containing one or two {core.treeModel.Range ranges} that are a result of transforming this
 	 * {@link core.treeModel.Range range} by inserting `howMany` nodes at `insertPosition`. Two {@link core.treeModel.Range ranges} are

+ 49 - 89
packages/ckeditor5-engine/src/treemodel/treewalker.js

@@ -18,18 +18,24 @@ import CKEditorError from '../ckeditorerror.js';
  */
 export default class TreeWalker {
 	/**
-	 * Creates a range iterator. All parameters are optional, but you have to specify either `boundaries` or `position`.
+	 * Creates a range iterator. All parameters are optional, but you have to specify either `boundaries` or `startPosition`.
 	 *
 	 * @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 {core.treeModel.Position} [options.startPosition] Starting position.
 	 * @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`).
+	 * @param {Boolean} [options.shallow=false] Flag indicating whether iterator should enter elements or not. If the
+	 * iterator is shallow child nodes of any iterated node will not be returned along with `ELEMENT_END` tag.
+	 * @param {Boolean} [options.ignoreElementEnd=false] Flag indicating whether iterator should ignore `ELEMENT_END`
+	 * tags. If the option is true walker will not return a parent node of start position. If this option is `true`
+	 * each {@link core.treeModel.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.position ) ) {
+		if ( !options || ( !options.boundaries && !options.startPosition ) ) {
 			/**
 			 * Neither boundaries nor starting position have been defined.
 			 *
@@ -46,49 +52,59 @@ export default class TreeWalker {
 		 *
 		 * If boundaries are not defined they are set before first and after last child of the root node.
 		 *
-		 * @type {core.treeModel.Range}
+		 * @member {core.treeModel.Range} core.treeModel.TreeWalker#boundaries
 		 */
 		this.boundaries = options.boundaries || null;
 
-		/**
-		 * Start boundary cached for optimization purposes.
-		 *
-		 * @private
-		 * @type {core.treeModel.Element}
-		 */
-		this._boundaryStartParent = this.boundaries ? this.boundaries.start.parent : null;
-
 		/**
 		 * End boundary cached for optimization purposes.
 		 *
 		 * @private
-		 * @type {core.treeModel.Element}
+		 * @member {core.treeModel.Element} core.treeModel.TreeWalker#_boundaryEndParent
 		 */
 		this._boundaryEndParent = this.boundaries ? this.boundaries.end.parent : null;
 
 		/**
-		 * Iterator position. This is alway static position, even if the initial position was a
+		 * Iterator position. This is always static position, even if the initial position was a
 		 * {@link core.treeModel.LivePosition live position}.
 		 *
-		 * @type {core.treeModel.Position}
+		 * @member {core.treeModel.Position} core.treeModel.TreeWalker#position
 		 */
-		this.position = options.position ?
-			Position.createFromPosition( options.position ) :
+		this.position = options.startPosition ?
+			Position.createFromPosition( options.startPosition ) :
 			Position.createFromPosition( options.boundaries.start );
 
 		/**
 		 * Flag indicating whether all consecutive characters with the same attributes should be
 		 * returned as one {@link core.treeModel.CharacterProxy} (`true`) or one by one (`false`).
 		 *
-		 * @type {Boolean}
+		 * @member {Boolean} core.treeModel.TreeWalker#singleCharacters
 		 */
 		this.singleCharacters = !!options.singleCharacters;
 
+		/**
+		 * Flag indicating whether iterator should enter elements or not. If the iterator is shallow child nodes of any
+		 * iterated node will not be returned along with `ELEMENT_END` tag.
+		 *
+		 * @member {Boolean} core.treeModel.TreeWalker#shallow
+		 */
+		this.shallow = !!options.shallow;
+
+		/**
+		 * Flag indicating whether iterator should ignore `ELEMENT_END` tags. If the option is true walker will not
+		 * return a parent node of the start position. If this option is `true` each {@link core.treeModel.Element} will
+		 * be returned once, while if the option is `false` they might be returned twice:
+		 * for `'ELEMENT_START'` and `'ELEMENT_END'`.
+		 *
+		 * @member {Boolean} core.treeModel.TreeWalker#ignoreElementEnd
+		 */
+		this.ignoreElementEnd = !!options.ignoreElementEnd;
+
 		/**
 		 * Parent of the most recently visited node. Cached for optimization purposes.
 		 *
 		 * @private
-		 * @type {core.treeModel.Element}
+		 * @member {core.treeModel.Element} core.treeModel.TreeWalker#_visitedParent
 		 */
 		this._visitedParent = this.position.parent;
 	}
@@ -117,19 +133,23 @@ export default class TreeWalker {
 			return { done: true };
 		}
 
-		// Parent can't be null so by comparing with boundaryParent we check if boundaryParent is set at all.
-		if ( parent == this._boundaryEndParent && position.offset == this.boundaries.end.offset ) {
+		// We reached the walker boundary.
+		if ( parent === this._boundaryEndParent && position.offset == this.boundaries.end.offset ) {
 			return { done: true };
 		}
 
 		const node = parent.getChild( position.offset );
 
 		if ( node instanceof Element ) {
-			// Manual operations on path internals for optimization purposes. Here and in the rest of the method.
-			position.path.push( 0 );
-			this.position = position;
+			if ( !this.shallow ) {
+				// Manual operations on path internals for optimization purposes. Here and in the rest of the method.
+				position.path.push( 0 );
+				this._visitedParent = node;
+			} else {
+				position.offset++;
+			}
 
-			this._visitedParent = node;
+			this.position = position;
 
 			return formatReturnValue( 'ELEMENT_START', node, previousPosition, position, 1 );
 		} else if ( node instanceof CharacterProxy ) {
@@ -155,78 +175,18 @@ export default class TreeWalker {
 				return formatReturnValue( 'TEXT', textFragment, previousPosition, position, charactersCount );
 			}
 		} else {
+			// `node` is not set, we reached the end of current `parent`.
 			position.path.pop();
 			position.offset++;
 			this.position = position;
 
 			this._visitedParent = parent.parent;
 
-			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.
-	 *
-	 * @returns {Object} Object implementing iterator interface, returning information about taken step.
-	 * @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 end of the root.
-		if ( parent.parent === null && position.offset === 0 ) {
-			return { done: true };
-		}
-
-		// Parent can't be null so by comparing with boundaryParent we check if boundaryParent is set at all.
-		if ( parent == this._boundaryStartParent && position.offset == this.boundaries.start.offset ) {
-			return { done: true };
-		}
-
-		const node = parent.getChild( position.offset - 1 );
-
-		if ( node instanceof Element ) {
-			// Manual operations on path internals for optimization purposes. Here and in the rest of the method.
-			position.offset--;
-			position.path.push( node.getChildCount() );
-			this.position = position;
-
-			this._visitedParent = node;
-
-			return formatReturnValue( 'ELEMENT_END', node, position, previousPosition );
-		} else if ( node instanceof CharacterProxy ) {
-			if ( this.singleCharacters ) {
-				position.offset--;
-				this.position = position;
-
-				return formatReturnValue( 'CHARACTER', node, position, previousPosition, 1 );
+			if ( this.ignoreElementEnd ) {
+				return this.next();
 			} 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 TextFragment( parent.getChild( offset ), charactersCount );
-
-				position.offset = offset;
-				this.position = position;
-
-				return formatReturnValue( 'TEXT', textFragment, position, previousPosition, charactersCount );
+				return formatReturnValue( 'ELEMENT_END', parent, previousPosition, position );
 			}
-		} else {
-			position.path.pop();
-			this.position = position;
-
-			this._visitedParent = parent.parent;
-
-			return formatReturnValue( 'ELEMENT_START', parent, position, previousPosition, 1 );
 		}
 	}
 }

+ 4 - 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.getItems( true ) );
+					count += getIteratorCount( operation.range.getItems( { singleCharacters: true } ) );
 				}
 			}
 
@@ -146,7 +146,9 @@ describe( 'Batch', () => {
 			// default: 111---111222---1112------
 			const range = Range.createFromElement( root );
 
-			return Array.from( range.getItems( true ) ).map( item => item.getAttribute( 'a' ) || '-' ).join( '' );
+			return Array.from( range.getItems( { singleCharacters: true } ) )
+				.map( item => item.getAttribute( 'a' ) || '-' )
+				.join( '' );
 		}
 
 		describe( 'setAttr', () => {

+ 76 - 65
packages/ckeditor5-engine/tests/treemodel/range.js

@@ -12,6 +12,7 @@ import Position from '/ckeditor5/core/treemodel/position.js';
 import Element from '/ckeditor5/core/treemodel/element.js';
 import Text from '/ckeditor5/core/treemodel/text.js';
 import Document from '/ckeditor5/core/treemodel/document.js';
+import TreeWalker from '/ckeditor5/core/treemodel/treewalker.js';
 
 describe( 'Range', () => {
 	let range, start, end, root, otherRoot;
@@ -195,11 +196,42 @@ describe( 'Range', () => {
 			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( 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( 'getWalker', () => {
+		it( 'should be possible to iterate using this method', () => {
+			prepareRichRoot( root );
+
+			const range = new Range( new Position( root, [ 0, 0, 3 ] ), new Position( root, [ 3, 0, 2 ] ) );
+			const items = [];
+			const walker = range.getWalker();
+
+			for ( let value of walker ) {
+				items.push( value.item );
+			}
+
+			expect( mapNodesToNames( items ) ).to.deep.equal(
+				[ 'T:st', 'E:h', 'E:p', 'T:lorem ipsum', 'E:p', 'E:div', 'E:p',
+				'T:foo', 'E:p', 'E:p', 'T:bar', 'E:p', 'E:div', 'E:h', 'T:se' ] );
+		} );
+
+		it( 'should return treewalker with given options', () => {
+			prepareRichRoot( root );
+
+			const range = new Range( new Position( root, [ 0, 0, 3 ] ), new Position( root, [ 3, 0, 2 ] ) );
+			const walker = range.getWalker( { singleCharacters: true } );
+
+			expect( walker ).to.be.instanceof( TreeWalker );
+			expect( walker ).to.have.property( 'singleCharacters' ).that.is.true;
+			expect( walker ).to.have.property( 'boundaries' ).that.equals( range );
+			expect( walker ).to.have.property( 'shallow' ).that.is.false;
+		} );
+	} );
+
 	describe( 'getItems', () => {
 		it( 'should iterate over all items in the range', () => {
 			prepareRichRoot( root );
@@ -208,7 +240,8 @@ describe( 'Range', () => {
 			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' ] );
+			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', () => {
@@ -229,7 +262,7 @@ describe( 'Range', () => {
 				new Position( root, [ 1, 1 ] )
 			);
 
-			let items = Array.from( range.getItems( true ) );
+			let items = Array.from( range.getItems( { singleCharacters: true } ) );
 
 			expect( items.length ).to.equal( 3 );
 			expect( items[ 0 ].character ).to.equal( 'b' );
@@ -238,6 +271,46 @@ describe( 'Range', () => {
 		} );
 	} );
 
+	describe( 'getPositions', () => {
+		beforeEach( () => {
+			prepareRichRoot( root );
+		} );
+
+		it( 'should iterate 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 ]
+			];
+			let range = new Range( new Position( root, [ 1, 2 ] ), new Position( root, [ 3, 0, 2 ] ) );
+			let i = 0;
+
+			for ( let position of range.getPositions() ) {
+				expect( position.path ).to.deep.equal( expectedPaths[ i ] );
+				i++;
+			}
+
+			expect( i ).to.equal( expectedPaths.length );
+		} );
+
+		it( 'should return single nodes iterating 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 ]
+			];
+			let range = new Range( new Position( root, [ 1, 2 ] ), new Position( root, [ 3, 0, 2 ] ) );
+			let i = 0;
+
+			for ( let position of range.getPositions( { singleCharacters: true } ) ) {
+				expect( position.path ).to.deep.equal( expectedPaths[ i ] );
+				i++;
+			}
+
+			expect( i ).to.equal( expectedPaths.length );
+		} );
+	} );
+
 	describe( 'containsPosition', () => {
 		beforeEach( () => {
 			range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 3 ] ) );
@@ -444,68 +517,6 @@ describe( 'Range', () => {
 		} );
 	} );
 
-	describe( 'getTopLevelNodes', () => {
-		beforeEach( () => {
-			prepareRichRoot( root );
-		} );
-
-		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 nodeNames = mapNodesToNames( nodes );
-
-			expect( nodeNames ).to.deep.equal( [ 'T:st', 'E:p', 'E:p', 'E:p', 'T:se' ] );
-		} );
-
-		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:s', 'T:t', 'E:p', 'E:p', 'E:p', 'T:s', 'T:e' ] );
-		} );
-	} );
-
-	describe( 'getPositions', () => {
-		beforeEach( () => {
-			prepareRichRoot( root );
-		} );
-
-		it( 'should iterate 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 ]
-			];
-			let range = new Range( new Position( root, [ 1, 2 ] ), new Position( root, [ 3, 0, 2 ] ) );
-			let i = 0;
-
-			for ( let position of range.getPositions() ) {
-				expect( position.path ).to.deep.equal( expectedPaths[ i ] );
-				i++;
-			}
-
-			expect( i ).to.equal( expectedPaths.length );
-		} );
-
-		it( 'should return single nodes iterating 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 ]
-			];
-			let range = new Range( new Position( root, [ 1, 2 ] ), new Position( root, [ 3, 0, 2 ] ) );
-			let i = 0;
-
-			for ( let position of range.getPositions( true ) ) {
-				expect( position.path ).to.deep.equal( expectedPaths[ i ] );
-				i++;
-			}
-
-			expect( i ).to.equal( expectedPaths.length );
-		} );
-	} );
-
 	describe( 'isFlat', () => {
 		beforeEach( () => {
 			prepareRichRoot( root );

+ 193 - 100
packages/ckeditor5-engine/tests/treemodel/treewalker.js

@@ -15,8 +15,12 @@ import Position from '/ckeditor5/core/treemodel/position.js';
 import Range from '/ckeditor5/core/treemodel/range.js';
 import CKEditorError from '/ckeditor5/core/ckeditorerror.js';
 
-describe( 'range iterator', () => {
-	let doc, expectedItemsSingle, expectedItemsMerged, root, img1, paragraph, b, a, r, img2, x;
+describe( 'TreeWalker', () => {
+	let expectedSingle, expected, expectedIgnoreEnd, expectedSingleIgnoreEnd;
+	let expectedShallow, expectedCroppedStart, expectedCroppedEnd;
+
+	let doc, root, img1, paragraph, b, a, r, img2, x;
+	let rootBeginning;
 
 	before( () => {
 		doc = new Document();
@@ -25,8 +29,8 @@ describe( 'range iterator', () => {
 		// root
 		//  |- img1
 		//  |- p
-		//     |- B
-		//     |- A
+		//     |- B -bold
+		//     |- A -bold
 		//     |- R
 		//     |
 		//     |- img2
@@ -44,7 +48,37 @@ describe( 'range iterator', () => {
 
 		root.insertChildren( 0, [ img1, paragraph ] );
 
-		expectedItemsSingle = [
+		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 },
@@ -57,36 +91,48 @@ describe( 'range iterator', () => {
 			{ type: 'ELEMENT_END', item: paragraph }
 		];
 
-		expectedItemsMerged = [
+		expectedIgnoreEnd = [
 			{ 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 }
+			{ 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 }
 		];
 	} );
 
-	function expectItem( item, expected ) {
+	function expectItem( item, expected, options ) {
 		expect( item.done ).to.be.false;
 
-		expectValue( item.value, expected );
+		expectValue( item.value, expected, options );
 	}
 
-	function expectValue( value, expected ) {
+	function expectValue( value, expected, options ) {
 		expect( value.type ).to.equal( expected.type );
 
 		if ( value.type == 'TEXT' ) {
-			expectText( value, expected );
+			expectText( value, expected, options );
 		} else if ( value.type == 'CHARACTER' ) {
-			expectCharacter( value, expected );
+			expectCharacter( value, expected, options );
 		} else if ( value.type == 'ELEMENT_START' ) {
-			expectStart( value, expected );
+			expectStart( value, expected, options );
 		} else if ( value.type == 'ELEMENT_END' ) {
-			expectEnd( value, expected );
+			expectEnd( value, expected, options );
 		}
 	}
 
@@ -106,11 +152,16 @@ describe( 'range iterator', () => {
 		expect( value.nextPosition ).to.deep.equal( Position.createAfter( value.item ) );
 	}
 
-	function expectStart( value, expected ) {
+	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( value.nextPosition ).to.deep.equal( Position.createFromParentAndOffset( value.item, 0 ) );
+
+		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 ) );
+		}
 	}
 
 	function expectEnd( value, expected ) {
@@ -121,124 +172,166 @@ describe( 'range iterator', () => {
 		expect( value.nextPosition ).to.deep.equal( Position.createAfter( value.item ) );
 	}
 
-	describe( 'merged characters', () => {
-		it( 'should iterate over the range using next', () => {
-			let start = new Position( root, [ 1 ] );
-			let end = new Position( root, [ 1, 4 ] );
-			let range = new Range( start, end );
+	it( 'should provide iterator methods', () => {
+		let iterator = new TreeWalker( { startPosition: rootBeginning } );
 
-			let iterator = new TreeWalker( { boundaries: range, position: range.start } );
-			let i;
+		for ( let i = 0; i <= 8; i++ ) {
+			expectItem( iterator.next(), expected[ 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 provide iterator interface', () => {
+		let iterator = new TreeWalker( { startPosition: rootBeginning } );
+		let i = 0;
 
-		it( 'should iterate 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 value of iterator ) {
+			expectValue( value, expected[ i ] );
+			i++;
+		}
 
-			let iterator = new TreeWalker( { boundaries: range, position: range.end } );
+		expect( i ).to.equal( expected.length );
+	} );
 
-			for ( let i = 6; i >= 2; i-- ) {
-				expectItem( iterator.previous(), expectedItemsMerged[ i ] );
-			}
-			expect( iterator.previous() ).to.have.property( 'done' ).that.is.true;
-		} );
+	it( 'should start at the startPosition', () => {
+		let iterator = new TreeWalker( { startPosition: new Position( root, [ 1 ] ) } );
+		let i = 2;
 
-		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 );
+		for ( let value of iterator ) {
+			expectValue( value, expected[ i ] );
+			i++;
+		}
 
-			let iterator = new TreeWalker( { boundaries: range, position: range.start } );
-			let val = iterator.next();
+		expect( i ).to.equal( expected.length );
+	} );
 
-			expect( val.done ).to.be.false;
-			expect( val.value.item.text ).to.equal( 'b' );
+	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 );
 
-			val = iterator.next();
-			expect( val.done ).to.be.true;
-		} );
+		let iterator = new TreeWalker( { boundaries: range } );
+		let i = 2;
 
-		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 );
+		for ( let value of iterator ) {
+			expectValue( value, expected[ i ] );
+			i++;
+		}
+
+		expect( i ).to.equal( 7 );
+	} );
 
-			let iterator = new TreeWalker( { boundaries: range, position: range.end } );
-			let val = iterator.previous();
+	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 );
 
-			expect( val.done ).to.be.false;
-			expect( val.value.item.text ).to.equal( 'a' );
+		let iterator = new TreeWalker( { boundaries: range } );
+		let i = 2;
 
-			val = iterator.previous();
-			expect( val.done ).to.be.true;
-		} );
+		for ( let value of iterator ) {
+			expectValue( value, expected[ i ] );
+			i++;
+		}
+
+		expect( i ).to.equal( 7 );
 	} );
 
-	describe( 'single characters', () => {
-		it( 'should iterate over the range using next', () => {
-			let iterator = new TreeWalker( { position: new Position( root, [ 0 ] ), singleCharacters: true } ); // beginning of root
-			let i, len;
+	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;
 
-			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 ( let value of iterator ) {
+			expectValue( value, expectedCroppedStart[ i ] );
+			i++;
+		}
+
+		expect( i ).to.equal( expectedCroppedStart.length );
+	} );
+
+	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 );
+
+		let iterator = new TreeWalker( { boundaries: range } );
+		let i = 0;
+
+		for ( let value of iterator ) {
+			expectValue( value, expectedCroppedEnd[ i ] );
+			i++;
+		}
+
+		expect( i ).to.equal( expectedCroppedEnd.length );
+	} );
 
-		it( 'should iterate over the range using previous', () => {
-			let iterator = new TreeWalker( { position: new Position( root, [ 2 ] ), singleCharacters: true } ); // ending of root
+	describe( 'singleCharacters', () => {
+		it( 'should return single characters', () => {
+			let iterator = new TreeWalker( { startPosition: rootBeginning, singleCharacters: true } );
+			let i = 0;
 
-			for ( let i = expectedItemsSingle.length - 1; i >= 0; i-- ) {
-				expectItem( iterator.previous(), expectedItemsSingle[ i ] );
+			for ( let value of iterator ) {
+				expectValue( value, expectedSingle[ i ] );
+				i++;
 			}
-			expect( iterator.previous() ).to.have.property( 'done' ).that.is.true;
+
+			expect( i ).to.equal( expectedSingle.length );
 		} );
 
-		it( 'should respect boundaries when iterating using next', () => {
+		it( 'should respect 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: new Range( start, end ), singleCharacters: true } );
+			let i = 3;
 
-			let i, len;
-
-			for ( i = 3, len = expectedItemsSingle.length; i < 7; i++ ) {
-				expectItem( iterator.next(), expectedItemsSingle[ i ] );
+			for ( let value of iterator ) {
+				expectValue( value, expectedSingle[ i ] );
+				i++;
 			}
-			expect( iterator.next() ).to.have.property( 'done' ).that.is.true;
+
+			expect( i ).to.equal( 7 );
 		} );
+	} );
 
-		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
+	describe( 'shallow', () => {
+		it( 'should not enter elements', () => {
+			let iterator = new TreeWalker( { startPosition: rootBeginning, shallow: true } );
+			let i = 0;
 
-			let iterator = new TreeWalker( { boundaries: new Range( start, end ), position: end, singleCharacters: true } );
+			for ( let value of iterator ) {
+				expectValue( value, expectedShallow[ i ], { shallow: true } );
+				i++;
+			}
+
+			expect( i ).to.equal( expectedShallow.length );
+		} );
+	} );
 
-			let i, len;
+	describe( 'ignoreElementEnd', () => {
+		it( 'should iterate ignoring ELEMENT_END', () => {
+			let iterator = new TreeWalker( { startPosition: rootBeginning, ignoreElementEnd: true } );
+			let i = 0;
 
-			for ( i = 6, len = expectedItemsSingle.length; i > 2; i-- ) {
-				expectItem( iterator.previous(), expectedItemsSingle[ i ] );
+			for ( let value of iterator ) {
+				expectValue( value, expectedIgnoreEnd[ i ] );
+				i++;
 			}
-			expect( iterator.previous() ).to.have.property( 'done' ).that.is.true;
+
+			expect( i ).to.equal( expectedIgnoreEnd.length );
 		} );
-	} );
 
-	it( 'should provide iterator interface', () => {
-		let iterator = new TreeWalker( { position: new Position( root, [ 0 ] ) } );
-		let i = 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, expectedItemsMerged[ i ] );
-			i++;
-		}
+			for ( let value of iterator ) {
+				expectValue( value, expectedSingleIgnoreEnd[ i ] );
+				i++;
+			}
 
-		expect( i ).to.equal( 9 );
+			expect( i ).to.equal( expectedSingleIgnoreEnd.length );
+		} );
 	} );
 
 	it( 'should throw if neither boundaries nor starting position is set', () => {