瀏覽代碼

Added PositionIterator character merging.

Szymon Cofalik 10 年之前
父節點
當前提交
9238f93fe2

+ 68 - 10
packages/ckeditor5-engine/src/treemodel/positioniterator.js

@@ -8,10 +8,14 @@
 import Character from './character.js';
 import Element from './element.js';
 import Position from './position.js';
+import Text from './text.js';
+import Range from './range.js';
+
 
 const ELEMENT_ENTER = 0;
 const ELEMENT_LEAVE = 1;
 const CHARACTER = 2;
+const TEXT = 3;
 
 /**
  * Position iterator class. It allows to iterate forward and backward over the tree document.
@@ -20,20 +24,37 @@ const CHARACTER = 2;
  */
 export default class PositionIterator {
 	/**
-	 * Creates a range iterator.
+	 * 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] Whether {@link treeModel.Character} nodes with same attributes
+	 * should be passed one by one or as one {@link treeModel.Text} object.
 	 * @constructor
 	 */
-	constructor( boundaries, iteratorPosition ) {
-		if ( boundaries instanceof Position ) {
-			this.position = boundaries;
+	constructor() {
+		const Range = CKEDITOR.require( 'treemodel/range' );
+		const Position = CKEDITOR.require( 'treemodel/position' );
+
+		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.boundaries =  boundaries;
-			this.position = iteratorPosition || boundaries.start;
+			this.position = this.boundaries.start;
 		}
 
+		this.mergeCharacters = !!args[ 0 ];
+
 		/**
 		 * Iterator position.
 		 *
@@ -50,22 +71,34 @@ export default class PositionIterator {
 		 *
 		 * @property {treeModel.Range} boundaries
 		 */
+
+		/**
+		 * Flag indicating whether {@link treeModel.Character} nodes with same attributes should be passed one by one
+		 * or as one {@link treeModel.Text} object.
+		 *
+		 * In this mode, all consecutive characters that has the same attributes will be passed in one iterator step.
+		 *
+		 * @property {Boolean} mergeCharacters
+		 */
 	}
 
 	/**
-	 * Moves the {@link #position} to the next position and returns the enctountered value.
+	 * 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#CHARACTER}.
+	 * {@link PositionIterator#ELEMENT_LEAVE}, {@link PositionIterator#CHARACTER} or {@link PositionIterator#TEXT}.
 	 * @returns {treeModel.Node} return.value.node Encountered node.
 	 */
 	next() {
 		const position = this.position;
 		const parent = position.parent;
 
+		// Ugh... added here because of circular deps in AMD ;<.
+		Element = CKEDITOR.require( 'treemodel/element' );
+
 		// We are at the end of the root.
 		if ( parent.parent === null && position.offset === parent.getChildCount() ) {
 			return { done: true };
@@ -82,9 +115,25 @@ export default class PositionIterator {
 
 			return formatReturnValue( ELEMENT_ENTER, nodeAfter );
 		} else if ( nodeAfter instanceof Character ) {
-			this.position = Position.createFromParentAndOffset( parent, position.offset + 1 );
+			if ( this.mergeCharacters ) {
+				let text = '';
+				let node = this.position.nodeAfter;
+
+				while ( !this.position.isEqual( this.boundaries.end ) && node instanceof Character && node.attrs.isEqual( nodeAfter.attrs ) ) {
+					text += this.position.nodeAfter.character;
+					this.position.offset++;
 
-			return formatReturnValue( CHARACTER, nodeAfter );
+					node = this.position.nodeAfter;
+				}
+				// This is so this.position is always a new object after each iterator step.
+				this.position = Position.createFromPosition( position );
+
+				return formatReturnValue( TEXT, new Text( text, this.position.nodeBefore.attrs ) );
+			} else {
+				this.position = Position.createFromParentAndOffset( parent, position.offset + 1 );
+
+				return formatReturnValue( CHARACTER, nodeAfter );
+			}
 		} else {
 			this.position = Position.createFromParentAndOffset( parent.parent, parent.getIndex() + 1 );
 
@@ -169,3 +218,12 @@ PositionIterator.ELEMENT_ENTER = ELEMENT_ENTER;
  * @property {Number}
  */
 PositionIterator.ELEMENT_LEAVE = ELEMENT_LEAVE;
+
+/**
+ * Flag for merged characters / text.
+ *
+ * @static
+ * @readonly
+ * @property {Number}
+ */
+PositionIterator.TEXT = TEXT;

+ 41 - 7
packages/ckeditor5-engine/tests/treemodel/positioniterator.js

@@ -8,21 +8,24 @@
 'use strict';
 
 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 Character from '/ckeditor5/core/treemodel/character.js';
+import Text from '/ckeditor5/core/treemodel/text.js';
 import PositionIterator from '/ckeditor5/core/treemodel/positioniterator.js';
 import Position from '/ckeditor5/core/treemodel/position.js';
 import Range from '/ckeditor5/core/treemodel/range.js';
 
 describe( 'range iterator', () => {
-	let ELEMENT_ENTER, ELEMENT_LEAVE, CHARACTER;
+	let ELEMENT_ENTER, ELEMENT_LEAVE, CHARACTER, TEXT;
 
-	let doc, expectedItems, root, img1, paragraph, b, a, r, img2, x;
+	let doc, expectedItems, expectedItemsMerged, root, img1, paragraph, b, a, r, img2, x;
 
 	before( () => {
 		ELEMENT_ENTER = PositionIterator.ELEMENT_ENTER;
 		ELEMENT_LEAVE = PositionIterator.ELEMENT_LEAVE;
 		CHARACTER = PositionIterator.CHARACTER;
+		TEXT = PositionIterator.TEXT;
 
 		doc = new Document();
 		root = doc.createRoot( 'root' );
@@ -38,8 +41,10 @@ describe( 'range iterator', () => {
 		//     |
 		//     |- X
 
-		b = new Character( 'b' );
-		a = new Character( 'a' );
+		let attrBoldTrue = new Attribute( 'bold', true );
+
+		b = new Character( 'b', [ attrBoldTrue ] );
+		a = new Character( 'a', [ attrBoldTrue ] );
 		r = new Character( 'r' );
 		img2 = new Element( 'img2' );
 		x = new Character( 'x' );
@@ -61,6 +66,18 @@ describe( 'range iterator', () => {
 			{ type: CHARACTER, node: x },
 			{ type: ELEMENT_LEAVE, node: paragraph }
 		];
+
+		expectedItemsMerged = [
+			{ type: ELEMENT_ENTER, node: img1 },
+			{ type: ELEMENT_LEAVE, node: img1 },
+			{ type: ELEMENT_ENTER, node: paragraph },
+			{ type: TEXT, node: new Text( 'ba', [ attrBoldTrue ] ) },
+			{ type: TEXT, node: new Text( 'r' ) },
+			{ type: ELEMENT_ENTER, node: img2 },
+			{ type: ELEMENT_LEAVE, node: img2 },
+			{ type: TEXT, node: new Text( 'x' ) },
+			{ type: ELEMENT_LEAVE, node: paragraph }
+		];
 	} );
 
 	it( 'should return next position', () => {
@@ -111,17 +128,34 @@ describe( 'range iterator', () => {
 	} );
 
 	it( 'should return iterate over the range', () => {
-		let start = new Position( root, [ 0 ] ); // begging of root
+		let start = new Position( root, [ 0 ] ); // beginning of root
 		let end = new Position( root, [ 2 ] ); // ending of root
 		let range = new Range( start, end );
 
 		let i = 0;
-		let value;
 
-		for ( value of range ) {
+		for ( let value of range ) {
 			expect( value ).to.deep.equal( expectedItems[ i ] );
 			i++;
 		}
 		expect( i ).to.equal( expectedItems.length );
 	} );
+
+	it( 'should merge characters when iterating over the range', () => {
+		let start = new Position( root, [ 0 ] ); // beginning of root
+		let end = new Position( root, [ 2 ] ); // ending of root
+		let range = new Range( start, end );
+
+		let iterator = new PositionIterator( range, range.start, true );
+		let step = iterator.next();
+		let i = 0;
+
+		while ( !step.done ) {
+			expect( step.value ).to.deep.equal( expectedItemsMerged[ i ] );
+			step = iterator.next();
+			i++;
+		}
+
+		expect( i ).to.equal( expectedItemsMerged.length );
+	} );
 } );