Browse Source

Rename PositionIterator to TreeWalker. Corrected TreeWalker to use new NodeList API.

Szymon Cofalik 10 năm trước cách đây
mục cha
commit
411252401f

+ 0 - 213
packages/ckeditor5-engine/src/treemodel/positioniterator.js

@@ -1,213 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-'use strict';
-
-import TextNode from './textnode.js';
-import Element from './element.js';
-import Position from './position.js';
-import Range from './range.js';
-
-const ELEMENT_ENTER = 0;
-const ELEMENT_LEAVE = 1;
-const TEXT = 2;
-
-/**
- * Position iterator class. It allows to iterate forward and backward over the tree document.
- *
- * @class treeModel.PositionIterator
- */
-export default class PositionIterator {
-	/**
-	 * Creates a range iterator. All parameters are optional, but you have to specify either `boundaries` or
-	 * `iteratorPosition`.
-	 *
-	 * @param {treeModel.Range} [boundaries] Range to define boundaries of the iterator.
-	 * @param {treeModel.Position} [iteratorPosition] Starting position.
-	 * @param {Boolean} [mergeCharacters] Flag indicating whether all consecutive characters with the same attributes
-	 * should be returned as one {@link treeModel.TextNode} (`true`) or one by one (`false`). Defaults to `false`.
-	 * should be passed one by one or as one {@link treeModel.Text} object.
-	 * @constructor
-	 */
-	constructor() {
-		const args = Array.from( arguments );
-
-		if ( args[ 0 ] instanceof Range ) {
-			this.boundaries = args[ 0 ];
-			args.shift();
-		} else {
-			this.boundaries = null;
-		}
-
-		if ( args[ 0 ] instanceof Position ) {
-			this.position = args[ 0 ];
-			args.shift();
-		} else {
-			this.position = this.boundaries.start;
-		}
-
-		this.mergeCharacters = !!args[ 0 ];
-
-		/**
-		 * Iterator position.
-		 *
-		 * @property {treeModel.Position} position
-		 */
-
-		/**
-		 * Iterator boundaries.
-		 *
-		 * When the {@link #next} method is called on the end boundary or the {@link #previous} method
-		 * on the start boundary, then `{ done: true }` is returned.
-		 *
-		 * If boundaries are not defined they are set before first and after last child of the root node.
-		 *
-		 * @property {treeModel.Range} boundaries
-		 */
-
-		/**
-		 * Flag indicating whether all consecutive characters with the same attributes should be
-		 * returned as one {@link treeModel.TextNode} (`true`) or one by one (`false`).
-		 *
-		 * @property {Boolean} mergeCharacters
-		 */
-	}
-
-	/**
-	 * Moves the {@link #position} to the next position and returns the encountered value.
-	 *
-	 * @returns {Object} Value between the previous and the new {@link #position}.
-	 * @returns {Boolean} return.done True if iterator is done.
-	 * @returns {Object} return.value
-	 * @returns {Number} return.value.type Encountered value type, possible options: {@link PositionIterator#ELEMENT_ENTER},
-	 * {@link PositionIterator#ELEMENT_LEAVE} or {@link PositionIterator#TEXT}.
-	 * @returns {treeModel.Node} return.value.node Encountered node.
-	 */
-	next() {
-		const position = this.position;
-		const parent = position.parent;
-
-		// We are at the end of the root.
-		if ( parent.parent === null && position.offset === parent.getChildCount() ) {
-			return { done: true };
-		}
-
-		if ( this.boundaries && position.isEqual( this.boundaries.end ) ) {
-			return { done: true };
-		}
-
-		const nodeAfter = position.nodeAfter;
-
-		if ( nodeAfter instanceof Element ) {
-			this.position = Position.createFromParentAndOffset( nodeAfter, 0 );
-
-			return formatReturnValue( ELEMENT_ENTER, nodeAfter );
-		} else if ( nodeAfter instanceof TextNode ) {
-			let offset = this.mergeCharacters ? nodeAfter._textItem.text.length - nodeAfter._start : 1;
-			let nextPos = Position.createFromParentAndOffset( parent, position.offset + offset );
-
-			if ( this.boundaries && nextPos.isAfter( this.boundaries.end ) ) {
-				nextPos = Position.createFromPosition( this.boundaries.end );
-			}
-
-			let textNode = nodeAfter._textItem.getTextNode( nodeAfter._start, nextPos.offset - position.offset );
-
-			this.position = nextPos;
-
-			return formatReturnValue( TEXT, textNode );
-		} else {
-			this.position = Position.createFromParentAndOffset( parent.parent, parent.getIndex() + 1 );
-
-			return formatReturnValue( ELEMENT_LEAVE, this.position.nodeBefore );
-		}
-	}
-
-	/**
-	 * Moves the {@link #position} to the previous position and returns the encountered value.
-	 *
-	 * @returns {Object} Value between the previous and the new {@link #position}.
-	 * @returns {Boolean} return.done True if iterator is done.
-	 * @returns {Object} return.value
-	 * @returns {Number} return.value.type Encountered value type, possible options: {@link PositionIterator#ELEMENT_ENTER},
-	 * {@link PositionIterator#ELEMENT_LEAVE} or {@link PositionIterator#TEXT}.
-	 * @returns {treeModel.Node} return.value.node Scanned node.
-	 */
-	previous() {
-		const position = this.position;
-		const parent = position.parent;
-
-		// We are at the beginning of the root.
-		if ( parent.parent === null && position.offset === 0 ) {
-			return { done: true };
-		}
-
-		if ( this.boundaries && position.isEqual( this.boundaries.start ) ) {
-			return { done: true };
-		}
-
-		const nodeBefore = position.nodeBefore;
-
-		if ( nodeBefore instanceof Element ) {
-			this.position = Position.createFromParentAndOffset( nodeBefore, nodeBefore.getChildCount() );
-
-			return formatReturnValue( ELEMENT_LEAVE, nodeBefore );
-		} else if ( nodeBefore instanceof TextNode ) {
-			let offset = this.mergeCharacters ? nodeBefore._start + 1 : 1;
-			let nextPos = Position.createFromParentAndOffset( parent, position.offset - offset );
-
-			if ( this.boundaries && nextPos.isBefore( this.boundaries.start ) ) {
-				nextPos = Position.createFromPosition( this.boundaries.start );
-			}
-
-			let start = nodeBefore._start - position.offset + nextPos.offset + 1;
-			let textNode = nodeBefore._textItem.getTextNode( start, nodeBefore._start - start + 1 );
-
-			this.position = nextPos;
-
-			return formatReturnValue( TEXT, textNode );
-		} else {
-			this.position = Position.createFromParentAndOffset( parent.parent, parent.getIndex() );
-
-			return formatReturnValue( ELEMENT_ENTER, this.position.nodeAfter );
-		}
-	}
-}
-
-function formatReturnValue( type, node ) {
-	return {
-		done: false,
-		value: {
-			type: type,
-			node: node
-		}
-	};
-}
-
-/**
- * Flag for entering element.
- *
- * @static
- * @readonly
- * @property {Number}
- */
-PositionIterator.ELEMENT_ENTER = ELEMENT_ENTER;
-
-/**
- * Flag for leaving element.
- *
- * @static
- * @readonly
- * @property {Number}
- */
-PositionIterator.ELEMENT_LEAVE = ELEMENT_LEAVE;
-
-/**
- * Flag for character or text.
- *
- * @static
- * @readonly
- * @property {Number}
- */
-PositionIterator.TEXT = TEXT;

+ 234 - 0
packages/ckeditor5-engine/src/treemodel/treewalker.js

@@ -0,0 +1,234 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import CharacterProxy from './characterproxy.js';
+import TextFragment from './textfragment.js';
+import Element from './element.js';
+import Position from './position.js';
+import CKEditorError from '../ckeditorerror.js';
+
+const ELEMENT_ENTER = 0;
+const ELEMENT_LEAVE = 1;
+const TEXT = 2;
+const CHARACTER = 3;
+
+/**
+ * Position iterator class. It allows to iterate forward and backward over the tree document.
+ *
+ * @class treeModel.TreeWalker
+ */
+export default class TreeWalker {
+	/**
+	 * Creates a range iterator. All parameters are optional, but you have to specify either `boundaries` or `position`.
+	 *
+	 * @param {Object} options Object with configuration.
+	 * @param {treeModel.Range} [options.boundaries] Range to define boundaries of the iterator.
+	 * @param {treeModel.Position} [options.position] Starting position.
+	 * @param {Boolean} [options.mergeCharacters] Flag indicating whether all consecutive characters with the same attributes
+	 * should be returned as one {@link treeModel.TextFragment} (`true`) or one by one as multiple {@link treeModel.CharacterProxy}
+	 * (`false`) objects. Defaults to `false`.
+	 * @constructor
+	 */
+	constructor( options ) {
+		if ( !options || ( !options.boundaries && !options.position ) ) {
+			/**
+			 * Neither boundaries nor starting position have been defined.
+			 *
+			 * @error tree-walker-no-start-position
+			 */
+			throw new CKEditorError( 'tree-walker-no-start-position: Neither boundaries nor starting position have been defined.' );
+		}
+
+		/**
+		 * Iterator boundaries.
+		 *
+		 * When the {@link #next} method is called on the end boundary or the {@link #previous} method
+		 * on the start boundary, then `{ done: true }` is returned.
+		 *
+		 * If boundaries are not defined they are set before first and after last child of the root node.
+		 *
+		 * @property {treeModel.Range} boundaries
+		 */
+		this.boundaries = options.boundaries ? options.boundaries : null;
+
+		/**
+		 * Iterator position.
+		 *
+		 * @property {treeModel.Position} position
+		 */
+		this.position = options.position ? options.position : options.boundaries.start;
+
+		/**
+		 * Flag indicating whether all consecutive characters with the same attributes should be
+		 * returned as one {@link treeModel.CharacterProxy} (`true`) or one by one (`false`).
+		 *
+		 * @property {Boolean} mergeCharacters
+		 */
+		this.mergeCharacters = !!options.mergeCharacters;
+	}
+
+	/**
+	 * Moves the {@link #position} to the next position and returns the encountered value.
+	 *
+	 * @returns {Object} Value between the previous and the new {@link #position}.
+	 * @returns {Boolean} return.done True if iterator is done.
+	 * @returns {Object} return.value
+	 * @returns {Number} return.value.type Encountered value type, possible options: {@link TreeWalker#ELEMENT_ENTER},
+	 * {@link TreeWalker#ELEMENT_LEAVE} or {@link TreeWalker#TEXT}.
+	 * @returns {treeModel.Node} return.value.node Encountered node.
+	 */
+	next() {
+		const position = this.position;
+		const parent = position.parent;
+
+		// We are at the end of the root.
+		if ( parent.parent === null && position.offset === parent.getChildCount() ) {
+			return { done: true };
+		}
+
+		if ( this.boundaries && position.isEqual( this.boundaries.end ) ) {
+			return { done: true };
+		}
+
+		const nodeAfter = position.nodeAfter;
+
+		if ( nodeAfter instanceof Element ) {
+			this.position = Position.createFromParentAndOffset( nodeAfter, 0 );
+
+			return formatReturnValue( ELEMENT_ENTER, nodeAfter );
+		} else if ( nodeAfter instanceof CharacterProxy ) {
+			if ( this.mergeCharacters ) {
+				let charactersCount = nodeAfter._nodeListText.text.length - nodeAfter._index;
+				let offset = position.offset + charactersCount;
+
+				if ( this.boundaries && this.boundaries.end.parent == parent && this.boundaries.end.offset < offset ) {
+					offset = this.boundaries.end.offset;
+					charactersCount = offset - position.offset;
+				}
+
+				let text = nodeAfter._nodeListText.text.substr( nodeAfter._index, charactersCount );
+				let textFragment = new TextFragment( position, text );
+
+				this.position = Position.createFromParentAndOffset( parent, offset );
+
+				return formatReturnValue( TEXT, textFragment );
+			} else {
+				this.position = Position.createFromParentAndOffset( parent, position.offset + 1 );
+
+				return formatReturnValue( CHARACTER, nodeAfter );
+			}
+		} else {
+			this.position = Position.createFromParentAndOffset( parent.parent, parent.getIndex() + 1 );
+
+			return formatReturnValue( ELEMENT_LEAVE, this.position.nodeBefore );
+		}
+	}
+
+	/**
+	 * Moves the {@link #position} to the previous position and returns the encountered value.
+	 *
+	 * @returns {Object} Value between the previous and the new {@link #position}.
+	 * @returns {Boolean} return.done True if iterator is done.
+	 * @returns {Object} return.value
+	 * @returns {Number} return.value.type Encountered value type, possible options: {@link TreeWalker#ELEMENT_ENTER},
+	 * {@link TreeWalker#ELEMENT_LEAVE} or {@link TreeWalker#TEXT}.
+	 * @returns {treeModel.Node} return.value.item Scanned node.
+	 */
+	previous() {
+		const position = this.position;
+		const parent = position.parent;
+
+		// We are at the beginning of the root.
+		if ( parent.parent === null && position.offset === 0 ) {
+			return { done: true };
+		}
+
+		if ( this.boundaries && position.isEqual( this.boundaries.start ) ) {
+			return { done: true };
+		}
+
+		const nodeBefore = position.nodeBefore;
+
+		if ( nodeBefore instanceof Element ) {
+			this.position = Position.createFromParentAndOffset( nodeBefore, nodeBefore.getChildCount() );
+
+			return formatReturnValue( ELEMENT_LEAVE, nodeBefore );
+		} else if ( nodeBefore instanceof CharacterProxy ) {
+			if ( this.mergeCharacters ) {
+				let charactersCount = nodeBefore._index + 1;
+				let offset = position.offset - charactersCount;
+
+				if ( this.boundaries && this.boundaries.start.parent == parent && this.boundaries.start.offset > offset ) {
+					offset = this.boundaries.start.offset;
+					charactersCount = position.offset - offset;
+				}
+
+				let text = nodeBefore._nodeListText.text.substr( nodeBefore._index + 1 - charactersCount, charactersCount );
+
+				this.position = Position.createFromParentAndOffset( parent, offset );
+
+				let textFragment = new TextFragment( this.position, text );
+
+				return formatReturnValue( TEXT, textFragment );
+			} else {
+				this.position = Position.createFromParentAndOffset( parent, position.offset - 1 );
+
+				return formatReturnValue( CHARACTER, nodeBefore );
+			}
+		} else {
+			this.position = Position.createFromParentAndOffset( parent.parent, parent.getIndex() );
+
+			return formatReturnValue( ELEMENT_ENTER, this.position.nodeAfter );
+		}
+	}
+}
+
+function formatReturnValue( type, item ) {
+	return {
+		done: false,
+		value: {
+			type: type,
+			item: item
+		}
+	};
+}
+
+/**
+ * Flag for entering element.
+ *
+ * @static
+ * @readonly
+ * @property {Number}
+ */
+TreeWalker.ELEMENT_ENTER = ELEMENT_ENTER;
+
+/**
+ * Flag for leaving element.
+ *
+ * @static
+ * @readonly
+ * @property {Number}
+ */
+TreeWalker.ELEMENT_LEAVE = ELEMENT_LEAVE;
+
+/**
+ * Flag for text.
+ *
+ * @static
+ * @readonly
+ * @property {Number}
+ */
+TreeWalker.TEXT = TEXT;
+
+/**
+ * Flag for character.
+ *
+ * @static
+ * @readonly
+ * @property {Number}
+ */
+TreeWalker.CHARACTER = CHARACTER;

+ 45 - 30
packages/ckeditor5-engine/tests/treemodel/positioniterator.js → packages/ckeditor5-engine/tests/treemodel/treewalker.js

@@ -11,9 +11,10 @@ import Document from '/ckeditor5/core/treemodel/document.js';
 import Attribute from '/ckeditor5/core/treemodel/attribute.js';
 import Element from '/ckeditor5/core/treemodel/element.js';
 import Text from '/ckeditor5/core/treemodel/text.js';
-import PositionIterator from '/ckeditor5/core/treemodel/positioniterator.js';
+import TreeWalker from '/ckeditor5/core/treemodel/treewalker.js';
 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 ELEMENT_ENTER, ELEMENT_LEAVE, CHARACTER, TEXT;
@@ -53,46 +54,46 @@ describe( 'range iterator', () => {
 		root.insertChildren( 0, [ img1, paragraph ] );
 
 		expectedItems = [
-			{ type: ELEMENT_ENTER, node: img1 },
-			{ type: ELEMENT_LEAVE, node: img1 },
-			{ type: ELEMENT_ENTER, node: paragraph },
-			{ type: TEXT, text: 'b', attrs: [ attrBoldTrue ] },
-			{ type: TEXT, text: 'a', attrs: [ attrBoldTrue ] },
-			{ type: TEXT, text: 'r', attrs: [] },
-			{ type: ELEMENT_ENTER, node: img2 },
-			{ type: ELEMENT_LEAVE, node: img2 },
-			{ type: TEXT, text: 'x', attrs: [] },
-			{ type: ELEMENT_LEAVE, node: paragraph }
+			{ type: ELEMENT_ENTER, item: img1 },
+			{ type: ELEMENT_LEAVE, item: img1 },
+			{ type: ELEMENT_ENTER, item: paragraph },
+			{ type: CHARACTER, text: 'b', attrs: [ attrBoldTrue ] },
+			{ type: CHARACTER, text: 'a', attrs: [ attrBoldTrue ] },
+			{ type: CHARACTER, text: 'r', attrs: [] },
+			{ type: ELEMENT_ENTER, item: img2 },
+			{ type: ELEMENT_LEAVE, item: img2 },
+			{ type: CHARACTER, text: 'x', attrs: [] },
+			{ type: ELEMENT_LEAVE, item: paragraph }
 		];
 
 		expectedItemsMerged = [
-			{ type: ELEMENT_ENTER, node: img1 },
-			{ type: ELEMENT_LEAVE, node: img1 },
-			{ type: ELEMENT_ENTER, node: paragraph },
+			{ type: ELEMENT_ENTER, item: img1 },
+			{ type: ELEMENT_LEAVE, item: img1 },
+			{ type: ELEMENT_ENTER, item: paragraph },
 			{ type: TEXT, text: 'ba', attrs: [ attrBoldTrue ] },
 			{ type: TEXT, text: 'r', attrs: [] },
-			{ type: ELEMENT_ENTER, node: img2 },
-			{ type: ELEMENT_LEAVE, node: img2 },
+			{ type: ELEMENT_ENTER, item: img2 },
+			{ type: ELEMENT_LEAVE, item: img2 },
 			{ type: TEXT, text: 'x', attrs: [] },
-			{ type: ELEMENT_LEAVE, node: paragraph }
+			{ type: ELEMENT_LEAVE, item: paragraph }
 		];
 	} );
 
 	function expectItem( item, expected ) {
 		expect( item.done ).to.be.false;
 
-		if ( item.value.type == TEXT ) {
-			let text = item.value.node.text;
+		if ( item.value.type == TEXT || item.value.type == CHARACTER ) {
+			let text = item.value.item.text || item.value.item.character;
 
 			expect( text ).to.equal( expected.text );
-			expect( Array.from( item.value.node.attrs ) ).to.deep.equal( expected.attrs );
+			expect( Array.from( item.value.item.attrs ) ).to.deep.equal( expected.attrs );
 		} else {
 			expect( item.value ).to.deep.equal( expected );
 		}
 	}
 
 	it( 'should return next position', () => {
-		let iterator = new PositionIterator( new Position( root, [ 0 ] ) ); // beginning of root
+		let iterator = new TreeWalker( { position: new Position( root, [ 0 ] ) } ); // beginning of root
 		let i, len;
 
 		for ( i = 0, len = expectedItems.length; i < len; i++ ) {
@@ -102,7 +103,7 @@ describe( 'range iterator', () => {
 	} );
 
 	it( 'should return previous position', () => {
-		let iterator = new PositionIterator( new Position( root, [ 2 ] ) ); // ending of root
+		let iterator = new TreeWalker( { position: new Position( root, [ 2 ] ) } ); // ending of root
 
 		for ( let i = expectedItems.length - 1; i >= 0; i-- ) {
 			expectItem( iterator.previous(), expectedItems[ i ] );
@@ -114,7 +115,7 @@ describe( 'range iterator', () => {
 		let start = new Position( root, [ 1, 0 ] ); // p, 0
 		let end = new Position( root, [ 1, 3, 0 ] ); // img, 0
 
-		let iterator = new PositionIterator( new Range( start, end ) );
+		let iterator = new TreeWalker( { boundaries: new Range( start, end ) } );
 
 		let i, len;
 
@@ -128,7 +129,7 @@ describe( 'range iterator', () => {
 		let start = new Position( root, [ 1, 0 ] ); // p, 0
 		let end = new Position( root, [ 1, 3, 0 ] ); // img, 0
 
-		let iterator = new PositionIterator( new Range( start, end ), end );
+		let iterator = new TreeWalker( { boundaries: new Range( start, end ), position: end } );
 
 		let i, len;
 
@@ -143,7 +144,7 @@ describe( 'range iterator', () => {
 		let end = new Position( root, [ 1, 4 ] );
 		let range = new Range( start, end );
 
-		let iterator = new PositionIterator( range, range.start, true );
+		let iterator = new TreeWalker( { boundaries: range, position: range.start, mergeCharacters: true } );
 		let i;
 
 		for ( i = 2; i <= 6; i++ ) {
@@ -157,7 +158,7 @@ describe( 'range iterator', () => {
 		let end = new Position( root, [ 1, 4 ] );
 		let range = new Range( start, end );
 
-		let iterator = new PositionIterator( range, range.end, true );
+		let iterator = new TreeWalker( { boundaries: range, position: range.end, mergeCharacters: true } );
 
 		for ( let i = 6; i >= 2; i-- ) {
 			expectItem( iterator.previous(), expectedItemsMerged[ i ] );
@@ -170,11 +171,11 @@ describe( 'range iterator', () => {
 		let end = new Position( root, [ 1, 1 ] );
 		let range = new Range( start, end );
 
-		let iterator = new PositionIterator( range, range.start, true );
+		let iterator = new TreeWalker( { boundaries: range, position: range.start, mergeCharacters: true } );
 		let val = iterator.next();
 
 		expect( val.done ).to.be.false;
-		expect( val.value.node.text ).to.equal( 'b' );
+		expect( val.value.item.text ).to.equal( 'b' );
 
 		val = iterator.next();
 		expect( val.done ).to.be.true;
@@ -185,13 +186,27 @@ describe( 'range iterator', () => {
 		let end = new Position( root, [ 1, 2 ] );
 		let range = new Range( start, end );
 
-		let iterator = new PositionIterator( range, range.end, true );
+		let iterator = new TreeWalker( { boundaries: range, position: range.end, mergeCharacters: true } );
 		let val = iterator.previous();
 
 		expect( val.done ).to.be.false;
-		expect( val.value.node.text ).to.equal( 'a' );
+		expect( val.value.item.text ).to.equal( 'a' );
 
 		val = iterator.previous();
 		expect( val.done ).to.be.true;
 	} );
+
+	it( 'should throw if neither boundaries nor starting position is set', () => {
+		expect( () => {
+			new TreeWalker();
+		} ).to.throw( CKEditorError, /^tree-walker-no-start-position/ );
+
+		expect( () => {
+			new TreeWalker( {} );
+		} ).to.throw( CKEditorError, /^tree-walker-no-start-position/ );
+
+		expect( () => {
+			new TreeWalker( { mergeCharacters: true } );
+		} ).to.throw( CKEditorError, /^tree-walker-no-start-position/ );
+	} );
 } );