8
0
Эх сурвалжийг харах

Merge pull request #225 from ckeditor/t/224

T/224
Szymon Cofalik 9 жил өмнө
parent
commit
1fc4b23125

+ 2 - 2
packages/ckeditor5-engine/src/treemodel/characterproxy.js

@@ -13,8 +13,8 @@ import Node from './node.js';
  * the actual nodes in tree model.
  *
  * Keep in mind that CharacterProxy is static, meaning that it won't change when tree model changes. For example, if you
- * have a {treeModel.Element element} `myEl` containing text `foobar` and then assign `let b = myEl.getChild( 3 )` and
- * then remove all nodes from `myEl`, `b` will still have character `b`, parent `myEl` and offset `3`.
+ * have a {core.treeModel.Element element} `myEl` containing text `foobar` and then assign `let b = myEl.getChild( 3 )`
+ * and then remove all nodes from `myEl`, `b` will still have character `b`, parent `myEl` and offset `3`.
  *
  * CharacterProxy is created on the fly basing on tree model. It is not an explicit node in a tree model but
  * rather represents it. Because of this, it is not advised to store or compare instances of CharacterProxy class.

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

@@ -86,7 +86,7 @@ function changeNode( doc, delta, key, value, node ) {
 
 // Because attribute operation needs to have the same attribute value on the whole range, this function split the range
 // into smaller parts.
-function changeRange( doc, delta, key, value, range ) {
+function changeRange( doc, delta, attributeKey, attributeValue, range ) {
 	// Position of the last split, the beginning of the new range.
 	let lastSplitPosition = range.start;
 
@@ -94,48 +94,37 @@ function changeRange( doc, delta, key, value, range ) {
 	// position of the iterator but the previous one (we need to iterate one more time to get the value after).
 	let position;
 	// Value before the currently position.
-	let valueBefore;
+	let attributeValueBefore;
 	// Value after the currently position.
-	let valueAfter;
-
-	// Because we need not only a node, but also a position, we can not use ( value of range ).
-	const iterator = range[ Symbol.iterator ]();
-	// Iterator state.
-	let next = iterator.next();
-
-	while ( !next.done ) {
-		// We check values only when the range contains given element, that is when the iterator "enters" the element.
-		// To prevent double-checking or not needed checking, we filter-out iterator values for ELEMENT_END position.
-		if ( next.value.type != 'ELEMENT_END' ) {
-			valueAfter = next.value.item.getAttribute( key );
-
-			// At the first run of the iterator the position in undefined. We also do not have a valueBefore, but
-			// because valueAfter may be null, valueBefore may be equal valueAfter ( undefined == null ).
-			if ( position && valueBefore != valueAfter ) {
-				// if valueBefore == value there is nothing to change, so we add operation only if these values are different.
-				if ( valueBefore != value ) {
-					addOperation();
-				}
-
-				lastSplitPosition = position;
+	let attributeValueAfter;
+
+	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
+		// because attributeValueAfter may be null, attributeValueBefore may be equal attributeValueAfter ( undefined == null ).
+		if ( position && attributeValueBefore != attributeValueAfter ) {
+			// if attributeValueBefore == attributeValue there is nothing to change, so we add operation only if these values are different.
+			if ( attributeValueBefore != attributeValue ) {
+				addOperation();
 			}
 
-			position = iterator.position;
-			valueBefore = valueAfter;
+			lastSplitPosition = position;
 		}
 
-		next = iterator.next();
+		position = value.nextPosition;
+		attributeValueBefore = attributeValueAfter;
 	}
 
 	// Because position in the loop is not the iterator position (see let position comment), the last position in
 	// the while loop will be last but one position in the range. We need to check the last position manually.
-	if ( position instanceof Position && position != lastSplitPosition && valueBefore != value ) {
+	if ( position instanceof Position && position != lastSplitPosition && attributeValueBefore != attributeValue ) {
 		addOperation();
 	}
 
 	function addOperation() {
 		let range = new Range( lastSplitPosition, position );
-		const operation = new AttributeOperation( range, key, valueBefore, value, doc.version );
+		const operation = new AttributeOperation( range, attributeKey, attributeValueBefore, attributeValue, doc.version );
 
 		doc.applyOperation( operation );
 		delta.addOperation( operation );

+ 10 - 0
packages/ckeditor5-engine/src/treemodel/item.jsdoc

@@ -0,0 +1,10 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * Item is a {@link core.treeModel.Node Node} or {core.treeModel.TextFragment TextFragment}.
+ *
+ * @typedef {core.treeModel.Node|core.treeModel.TextFragment} core.treeModel.Item
+ */

+ 1 - 1
packages/ckeditor5-engine/src/treemodel/liveposition.js

@@ -48,7 +48,7 @@ export default class LivePosition extends Position {
 		 * - | sticks to previous node: `<p>f|[oo]</p><p>b^ar</p>` => `<p>f|</p><p>booar</p>`
 		 * - | sticks to next node: `<p>f|[oo]</p><p>b^ar</p>` => `<p>f</p><p>b|ooar</p>`
 		 *
-		 * @member {treeModel.PositionStickiness} core.treeModel.LivePosition#stickiness
+		 * @member {core.treeModel.PositionStickiness} core.treeModel.LivePosition#stickiness
 		 */
 		this.stickiness = stickiness || 'STICKS_TO_NEXT';
 

+ 2 - 3
packages/ckeditor5-engine/src/treemodel/nodelist.js

@@ -442,12 +442,11 @@ export default class NodeList {
 /**
  * Value that is convertible to an item kept in {@link core.treeModel.NodeList} or an iterable collection of such items.
  * In other words, this is anything that {@link core.treeModel.NodeList#constructor} is able to take and convert to node:
- * * {@link core.treeModel.Element} will be left as is
- * * {@link core.treeModel.CharacterProxy} will be left as is
+ * * {@link core.treeModel.Node} will be left as is
  * * {@link core.treeModel.Text} and {String} will be converted to a set of {@link core.treeModel.CharacterProxy}
  * * {@link core.treeModel.NodeList} will clone a node list (but not the nodes inside, so the new and passed list will
  * point to the same nodes.
  * * Iterable collection of above items will be iterated over and all items will be added to the node list.
  *
- * @typedef {treeModel.Element|treeModel.CharacterProxy|treeModel.Text|String|treeModel.NodeList|Iterable} core.treeModel.NodeSet
+ * @typedef {core.treeModel.Node|core.treeModel.Text|String|core.treeModel.NodeList|Iterable} core.treeModel.NodeSet
  */

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

@@ -90,19 +90,19 @@ export default class AttributeOperation extends Operation {
 	}
 
 	_execute() {
-		for ( let item of this.range.getAllNodes( 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.
 				 *
 				 * @error operation-attribute-no-attr-to-remove
-				 * @param {core.treeModel.Node} node
+				 * @param {core.treeModel.Item} item
 				 * @param {String} key
 				 * @param {*} value
 				 */
 				throw new CKEditorError(
 					'operation-attribute-no-attr-to-remove: The attribute which should be removed does not exists for given node.',
-					{ node: item, key: this.key, value: this.oldValue }
+					{ item: item, key: this.key, value: this.oldValue }
 				);
 			}
 

+ 5 - 1
packages/ckeditor5-engine/src/treemodel/position.js

@@ -301,7 +301,7 @@ export default class Position {
 	/**
 	 * Checks whether this position is after given position.
 	 *
-	 * **Note:** see {treeModel.Position#isBefore}.
+	 * **Note:** see {@link core.treeModel.Position#isBefore}.
 	 *
 	 * @param {core.treeModel.Position} otherPosition Position to compare with.
 	 * @returns {Boolean} True if this position is after given position.
@@ -411,6 +411,8 @@ export default class Position {
 	/**
 	 * Creates a new position after given node.
 	 *
+	 * @see {@link core.treeModel.TreeWalkerValue}
+	 *
 	 * @param {core.treeModel.Node} node Node the position should be directly after.
 	 * @returns {core.treeModel.Position}
 	 */
@@ -431,6 +433,8 @@ export default class Position {
 	/**
 	 * Creates a new position before the given node.
 	 *
+	 * @see {@link core.treeModel.TreeWalkerValue}
+	 *
 	 * @param {core.treeModel.node} node Node the position should be directly before.
 	 * @returns {core.treeModel.Position}
 	 */

+ 65 - 45
packages/ckeditor5-engine/src/treemodel/range.js

@@ -67,15 +67,6 @@ export default class Range {
 		return this.start.root;
 	}
 
-	/**
-	 * Range iterator.
-	 *
-	 * @see core.treeModel.TreeWalker
-	 */
-	[ Symbol.iterator ]() {
-		return new TreeWalker( { boundaries: this } );
-	}
-
 	/**
 	 * Checks whether this contains given {@link core.treeModel.Position position}.
 	 *
@@ -261,49 +252,78 @@ export default class Range {
 	}
 
 	/**
-	 * Returns an iterator that iterates over all {@link core.treeModel.Node nodes} that are in this range and returns them.
-	 * 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
+	 * 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}. 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.
+	 *
+	 * **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 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 {@link core.treeModel.TreeWalker}
+	 * @returns {Iterable.<core.treeModel.TreeWalkerValue>}
+	 */
+	*[ Symbol.iterator ]() {
+		const treeWalker = new TreeWalker( { boundaries: this } );
+
+		for ( let value of treeWalker ) {
+			if ( value.type != 'ELEMENT_END' ) {
+				yield value;
+			}
+		}
+	}
+
+	/**
+	 * 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'`.
+	 * **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 {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`.
-	 * @returns {Iterable.<core.treeModel.Node|treeModel.TextFragment>}
+	 * @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>}
 	 */
-	*getAllNodes( mergeCharacters ) {
-		let it = new TreeWalker( { boundaries: this, mergeCharacters: mergeCharacters } );
-		let step;
+	*getItems( singleCharacters ) {
+		const treeWalker = new TreeWalker( { boundaries: this, singleCharacters: singleCharacters } );
 
-		do {
-			step = it.next();
-
-			if ( step.value && step.value.type != 'ELEMENT_END' ) {
-				yield step.value.item;
+		for ( let value of treeWalker ) {
+			if ( value.type != 'ELEMENT_END' ) {
+				yield value.item;
 			}
-		} while ( !step.done );
+		}
 	}
 
 	/**
 	 * 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 ) {
-		let it = new TreeWalker( { boundaries: this, mergeCharacters: mergeCharacters } );
+	*getPositions( singleCharacters ) {
+		const treeWalker = new TreeWalker( { boundaries: this, singleCharacters: singleCharacters } );
 
-		do {
-			yield it.position;
-		} while ( !it.next().done );
+		yield treeWalker.position;
+
+		for ( let value of treeWalker ) {
+			yield value.nextPosition;
+		}
 	}
 
 	/**
@@ -311,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,
@@ -348,7 +368,7 @@ export default class Range {
 	}
 
 	/**
-	 * Returns an array containing one or two {treeModel.Range ranges} that are a result of transforming this
+	 * 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
 	 * returned if the insertion was inside this {@link core.treeModel.Range range}.
 	 *
@@ -366,8 +386,8 @@ export default class Range {
 	 *
 	 * @param {core.treeModel.Position} insertPosition Position where nodes are inserted.
 	 * @param {Number} howMany How many nodes are inserted.
-	 * @param {Boolean} spreadOnlyOnSameLevel Flag indicating whether this {treeModel.Range range} should be spread
-	 * if insertion was inside a node from this {treeModel.Range range} but not in the range itself.
+	 * @param {Boolean} spreadOnlyOnSameLevel Flag indicating whether this {core.treeModel.Range range} should be spread
+	 * if insertion was inside a node from this {core.treeModel.Range range} but not in the range itself.
 	 * @returns {Array.<core.treeModel.Range>} Result of the transformation.
 	 */
 	getTransformedByInsertion( insertPosition, howMany, spreadOnlyOnSameLevel ) {

+ 52 - 30
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.
@@ -93,14 +93,22 @@ export default class TreeWalker {
 		this._visitedParent = this.position.parent;
 	}
 
+	/**
+	 * Iterator interface.
+	 */
+	[ Symbol.iterator ]() {
+		return this;
+	}
+
 	/**
 	 * Makes a step forward in tree model. Moves the {@link #position} to the next 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.TreeWalkerItem} return.value Information about taken step.
+	 * @returns {core.treeModel.TreeWalkerValue} return.value Information about taken step.
 	 */
 	next() {
+		const previousPosition = this.position;
 		const position = Position.createFromPosition( this.position );
 		const parent = this._visitedParent;
 
@@ -123,9 +131,14 @@ export default class TreeWalker {
 
 			this._visitedParent = node;
 
-			return formatReturnValue( 'ELEMENT_START', node );
+			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;
 
@@ -139,12 +152,7 @@ export default class TreeWalker {
 				position.offset = offset;
 				this.position = position;
 
-				return formatReturnValue( 'TEXT', textFragment );
-			} else {
-				position.offset++;
-				this.position = position;
-
-				return formatReturnValue( 'CHARACTER', node );
+				return formatReturnValue( 'TEXT', textFragment, previousPosition, position, charactersCount );
 			}
 		} else {
 			position.path.pop();
@@ -153,7 +161,7 @@ export default class TreeWalker {
 
 			this._visitedParent = parent.parent;
 
-			return formatReturnValue( 'ELEMENT_END', parent );
+			return formatReturnValue( 'ELEMENT_END', parent, previousPosition, position );
 		}
 	}
 
@@ -162,9 +170,10 @@ export default class TreeWalker {
 	 *
 	 * @returns {Object} Object implementing iterator interface, returning information about taken step.
 	 * @returns {Boolean} return.done True if iterator is done.
-	 * @returns {core.treeModel.TreeWalkerItem} return.value Information about taken step.
+	 * @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;
 
@@ -188,9 +197,14 @@ export default class TreeWalker {
 
 			this._visitedParent = node;
 
-			return formatReturnValue( 'ELEMENT_END', node );
+			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;
 
@@ -204,12 +218,7 @@ export default class TreeWalker {
 				position.offset = offset;
 				this.position = position;
 
-				return formatReturnValue( 'TEXT', textFragment );
-			} else {
-				position.offset--;
-				this.position = position;
-
-				return formatReturnValue( 'CHARACTER', node );
+				return formatReturnValue( 'TEXT', textFragment, position, previousPosition, charactersCount );
 			}
 		} else {
 			position.path.pop();
@@ -217,17 +226,20 @@ export default class TreeWalker {
 
 			this._visitedParent = parent.parent;
 
-			return formatReturnValue( 'ELEMENT_START', parent );
+			return formatReturnValue( 'ELEMENT_START', parent, position, previousPosition, 1 );
 		}
 	}
 }
 
-function formatReturnValue( type, item ) {
+function formatReturnValue( type, item, previousPosition, nextPosition, length ) {
 	return {
 		done: false,
 		value: {
 			type: type,
-			item: item
+			item: item,
+			previousPosition: previousPosition,
+			nextPosition: nextPosition,
+			length: length
 		}
 	};
 }
@@ -238,13 +250,23 @@ function formatReturnValue( type, item ) {
  * `'CHARACTER'` if walker traversed over a character, or `'TEXT'` if walker traversed over multiple characters (available in
  * character merging mode, see {@link core.treeModel.TreeWalker#constructor}).
  *
- * @typedef {String} core.treeModel.TreeWalkerItemType
+ * @typedef {String} core.treeModel.TreeWalkerValueType
  */
 
 /**
  * Object returned by {@link core.treeModel.TreeWalker} when traversing tree model.
  *
- * @typedef {Object} core.treeModel.TreeWalkerItem
- * @property {treeModel.TreeWalkerItemType} type
- * @property {treeModel.Node|treeModel.TextFragment} item Value between old and new position of {@link core.treeModel.TreeWalker}.
+ * @typedef {Object} core.treeModel.TreeWalkerValue
+ * @property {core.treeModel.TreeWalkerValueType} type
+ * @property {core.treeModel.Item} item Item between old and new positions of {@link core.treeModel.TreeWalker}.
+ * @property {core.treeModel.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 core.treeModel.Position.createBefore}. It is also more efficient to get the position after node by shifting
+ * `previousPosition` by `length`, using {@link core.treeModel.Position#getShiftedBy}, then calculate the position using
+ * {@link core.treeModel.Position.createAfter}.
+ * @property {core.treeModel.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 {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.
  */

+ 6 - 3
packages/ckeditor5-engine/tests/treemodel/_utils/utils.js

@@ -5,6 +5,8 @@
 
 'use strict';
 
+import TreeWalker from '/ckeditor5/core/treemodel/treewalker.js';
+
 const utils = {
 	/**
 	 * Returns tree structure as a simplified string. Elements are uppercase and characters are lowercase.
@@ -13,14 +15,15 @@ const utils = {
 	 *		let element = new Element( 'div', [], [ 'abc', new Element( 'p', [], 'foo' ), 'xyz' ] );
 	 *		treemodelUtils.getNodesAndText( element ); // abcPfooPxyz
 	 *
-	 * @param {treeModel.Range} range Range to stringify.
+	 * @param {core.treeModel.Range} range Range to stringify.
 	 * @returns {String} String representing element inner structure.
 	 */
 	getNodesAndText( range ) {
 		let txt = '';
+		const treeWalker = new TreeWalker( { boundaries: range } );
 
-		for ( let step of range ) {
-			let node = step.item;
+		for ( let value of treeWalker ) {
+			let node = value.item;
 			let nodeText = node.text || node.character;
 
 			if ( nodeText ) {

+ 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.getAllNodes() );
+					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.getAllNodes() ).map( node => node.getAttribute( 'a' ) || '-' ).join( '' );
+			return Array.from( range.getItems( true ) ).map( item => item.getAttribute( 'a' ) || '-' ).join( '' );
 		}
 
 		describe( 'setAttr', () => {

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

@@ -186,8 +186,32 @@ describe( 'Range', () => {
 		} );
 	} );
 
-	describe( 'getAllNodes', () => {
-		it( 'should iterate over all nodes which "starts" in the range', () => {
+	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 ).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 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' );
@@ -205,22 +229,12 @@ describe( 'Range', () => {
 				new Position( root, [ 1, 1 ] )
 			);
 
-			let nodes = Array.from( range.getAllNodes() );
-
-			expect( nodes.length ).to.equal( 3 );
-			expect( nodes[ 0 ].character ).to.equal( 'b' );
-			expect( nodes[ 1 ] ).to.equal( e2 );
-			expect( nodes[ 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 nodes = Array.from( range.getAllNodes( true ) );
-			let nodeNames = mapNodesToNames( nodes );
+			let items = Array.from( range.getItems( true ) );
 
-			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( 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' );
 		} );
 	} );
 
@@ -440,15 +454,15 @@ describe( 'Range', () => {
 			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' ] );
 		} );
 	} );
 
@@ -460,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;
@@ -474,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;

+ 138 - 85
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 },
@@ -73,119 +73,172 @@ describe( 'range iterator', () => {
 	function expectItem( item, expected ) {
 		expect( item.done ).to.be.false;
 
-		if ( item.value.type == 'TEXT' || item.value.type == 'CHARACTER' ) {
-			let text = item.value.item.text || item.value.item.character;
-			let attrs = item.value.item._attrs || item.value.item.first._attrs;
+		expectValue( item.value, expected );
+	}
 
-			expect( text ).to.equal( expected.text );
-			expect( Array.from( attrs ) ).to.deep.equal( expected.attrs );
-		} else {
-			expect( item.value ).to.deep.equal( expected );
+	function expectValue( value, expected ) {
+		expect( value.type ).to.equal( expected.type );
+
+		if ( value.type == 'TEXT' ) {
+			expectText( value, expected );
+		} else if ( value.type == 'CHARACTER' ) {
+			expectCharacter( value, expected );
+		} else if ( value.type == 'ELEMENT_START' ) {
+			expectStart( value, expected );
+		} else if ( value.type == 'ELEMENT_END' ) {
+			expectEnd( value, expected );
 		}
 	}
 
-	it( 'should return next position', () => {
-		let iterator = new TreeWalker( { position: new Position( root, [ 0 ] ) } ); // beginning of root
-		let i, len;
+	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 ) );
+	}
 
-		for ( i = 0, len = expectedItems.length; i < len; i++ ) {
-			expectItem( iterator.next(), expectedItems[ i ] );
-		}
-		expect( iterator.next() ).to.have.property( 'done' ).that.is.true;
-	} );
+	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 ) );
+	}
 
-	it( 'should return previous position', () => {
-		let iterator = new TreeWalker( { position: new Position( root, [ 2 ] ) } ); // ending of root
+	function expectStart( value, expected ) {
+		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 ) );
+	}
 
-		for ( let i = expectedItems.length - 1; i >= 0; i-- ) {
-			expectItem( iterator.previous(), expectedItems[ i ] );
-		}
-		expect( iterator.previous() ).to.have.property( 'done' ).that.is.true;
-	} );
+	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 ) );
+	}
 
-	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
+	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 );
 
-		let iterator = new TreeWalker( { boundaries: new Range( start, end ) } );
+			let iterator = new TreeWalker( { boundaries: range, position: range.start } );
+			let i;
 
-		let i, len;
+			for ( i = 2; i <= 6; i++ ) {
+				expectItem( iterator.next(), expectedItemsMerged[ i ] );
+			}
+			expect( iterator.next() ).to.have.property( 'done' ).that.is.true;
+		} );
 
-		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 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 );
 
-	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.end } );
 
-		let iterator = new TreeWalker( { boundaries: new Range( start, end ), position: 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;
+		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 ( i = 6, len = expectedItems.length; i > 2; i-- ) {
-			expectItem( iterator.previous(), expectedItems[ i ] );
-		}
-		expect( iterator.previous() ).to.have.property( 'done' ).that.is.true;
-	} );
+			let iterator = new TreeWalker( { boundaries: range, position: range.start } );
+			let val = iterator.next();
 
-	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 );
+			expect( val.done ).to.be.false;
+			expect( val.value.item.text ).to.equal( 'b' );
 
-		let iterator = new TreeWalker( { boundaries: range, position: range.start, mergeCharacters: true } );
-		let i;
+			val = iterator.next();
+			expect( val.done ).to.be.true;
+		} );
 
-		for ( i = 2; i <= 6; i++ ) {
-			expectItem( iterator.next(), expectedItemsMerged[ i ] );
-		}
-		expect( iterator.next() ).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 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 );
+			let iterator = new TreeWalker( { boundaries: range, position: range.end } );
+			let val = iterator.previous();
 
-		let iterator = new TreeWalker( { boundaries: range, position: range.end, mergeCharacters: true } );
+			expect( val.done ).to.be.false;
+			expect( val.value.item.text ).to.equal( 'a' );
 
-		for ( let i = 6; i >= 2; i-- ) {
-			expectItem( iterator.previous(), expectedItemsMerged[ i ] );
-		}
-		expect( iterator.previous() ).to.have.property( 'done' ).that.is.true;
+			val = iterator.previous();
+			expect( val.done ).to.be.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 );
+	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;
 
-		let iterator = new TreeWalker( { boundaries: range, position: range.start, mergeCharacters: true } );
-		let val = iterator.next();
+			for ( i = 0, len = expectedItemsSingle.length; i < len; 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 iterate over the range using previous', () => {
+			let iterator = new TreeWalker( { position: new Position( root, [ 2 ] ), singleCharacters: true } ); // ending of root
 
-		val = iterator.next();
-		expect( val.done ).to.be.true;
-	} );
+			for ( let i = expectedItemsSingle.length - 1; i >= 0; i-- ) {
+				expectItem( iterator.previous(), expectedItemsSingle[ i ] );
+			}
+			expect( iterator.previous() ).to.have.property( 'done' ).that.is.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
+
+			let iterator = new TreeWalker( { boundaries: new Range( start, 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 = 3, len = expectedItemsSingle.length; i < 7; i++ ) {
+				expectItem( iterator.next(), expectedItemsSingle[ i ] );
+			}
+			expect( iterator.next() ).to.have.property( 'done' ).that.is.true;
+		} );
 
-		let iterator = new TreeWalker( { boundaries: range, position: range.end, mergeCharacters: true } );
-		let val = iterator.previous();
+		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
 
-		expect( val.done ).to.be.false;
-		expect( val.value.item.text ).to.equal( 'a' );
+			let iterator = new TreeWalker( { boundaries: new Range( start, end ), position: end, singleCharacters: true } );
+
+			let i, len;
+
+			for ( i = 6, len = expectedItemsSingle.length; i > 2; i-- ) {
+				expectItem( iterator.previous(), expectedItemsSingle[ i ] );
+			}
+			expect( iterator.previous() ).to.have.property( 'done' ).that.is.true;
+		} );
+	} );
+
+	it( 'should provide iterator interface', () => {
+		let iterator = new TreeWalker( { position: new Position( root, [ 0 ] ) } );
+		let i = 0;
+
+		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', () => {