Selaa lähdekoodia

Added character merging to Range iterators. Added Range.*getPositions.

Szymon Cofalik 10 vuotta sitten
vanhempi
sitoutus
481f067b6c

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

@@ -204,11 +204,14 @@ export default class Range {
 	 *		 |   |- element P4
 	 *		 |   |   |- "ipsum"
 	 *
-	 * Flat ranges for range `( [ 0, 0, 3 ], [ 3, 0, 2 ] )` would be:
+	 * As it can be seen, letters contained in the range are stloremfoobarse, spread across different parents.
+	 * We are looking for minimal set of {@link #isFlat flat} ranges that contains the same nodes.
+	 *
+	 * Minimal flat ranges for above range `( [ 0, 0, 3 ], [ 3, 0, 2 ] )` will be:
 	 *
 	 *		( [ 0, 0, 3 ], [ 0, 0, 5 ] ) = "st"
-	 *		( [ 0, 1 ], [ 0, 2 ] ) = element P1
-	 *		( [ 1 ], [ 3 ] ) = element P2, element P3
+	 *		( [ 0, 1 ], [ 0, 2 ] ) = element P1 ("lorem")
+	 *		( [ 1 ], [ 3 ] ) = element P2, element P3 ("foobar")
 	 *		( [ 3, 0, 0 ], [ 3, 0, 2 ] ) = "se"
 	 *
 	 * **Note:** this method is not returning flat ranges that contain no nodes. It may also happen that not-collapsed
@@ -265,14 +268,37 @@ export default class Range {
 	 * {@link treeModel.PositionIterator#ELEMENT_LEAVE}.
 	 *
 	 * @see {treeModel.PositionIterator}
-	 * @returns {Iterable.<treeModel.Node>}
+	 * @param {Boolean} [mergeCharacters] Whether {@link treeModel.Character} nodes with same attributes
+	 * should be passed one by one or as one {@link treeModel.Text} object.
+	 * @returns {Iterable.<treeModel.Node|treeModel.Text>}
 	 */
-	*getAllNodes() {
-		for ( let value of this ) {
-			if ( value.type != PositionIterator.ELEMENT_LEAVE ) {
-				yield value.node;
+	*getAllNodes( mergeCharacters ) {
+		let it = new PositionIterator( this, mergeCharacters );
+		let step;
+
+		do {
+			step = it.next();
+
+			if ( step.value && step.value.type != PositionIterator.ELEMENT_LEAVE ) {
+				yield step.value.node;
 			}
-		}
+		} while ( !step.done );
+	}
+
+	/**
+	 * Returns an iterator that iterates over all {@link treeModel.Position positions} that are boundaries or
+	 * contained in this range.
+	 *
+	 * @param {Boolean} [mergeCharacters] Whether {@link treeModel.Character} nodes with same attributes
+	 * should be passed one by one or as one {@link treeModel.Text} object.
+	 * @returns {Iterable.<treeModel.Position>}
+	 */
+	*getPositions( mergeCharacters ) {
+		let it = new PositionIterator( this, mergeCharacters );
+
+		do {
+			yield it.position;
+		} while ( !it.next().done );
 	}
 
 	/**
@@ -280,19 +306,38 @@ 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.
 	 *
-	 * @returns {Iterable.<treeModel.Node>}
+	 * @param {Boolean} [mergeCharacters] Whether {@link treeModel.Character} nodes with same attributes
+	 * should be passed one by one or as one {@link treeModel.Text} object.
+	 * @returns {Iterable.<treeModel.Node|treeModel.Text>}
 	 */
-	*getTopLevelNodes() {
+	*getTopLevelNodes( mergeCharacters ) {
 		let flatRanges = this.getMinimalFlatRanges();
 
 		for ( let range of flatRanges ) {
-			let node = range.start.nodeAfter;
-			let offset = range.end.offset - range.start.offset;
-
-			for ( let i = 0; i < offset; i++ ) {
-				yield node;
-				node = node.nextSibling;
-			}
+			// 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 PositionIterator( range, mergeCharacters );
+			let step;
+
+			// 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;
+
+			do {
+				step = it.next();
+
+				if ( step.value ) {
+					if ( step.value.type == PositionIterator.ELEMENT_ENTER ) {
+						depth++;
+					} else if ( step.value.type == PositionIterator.ELEMENT_LEAVE ) {
+						depth--;
+					}
+
+					if ( depth === 0 ) {
+						yield step.value.node;
+					}
+				}
+			} while ( !step.done );
 		}
 	}
 

+ 68 - 8
packages/ckeditor5-engine/tests/treemodel/range.js

@@ -11,6 +11,7 @@ import Range from '/ckeditor5/core/treemodel/range.js';
 import Position from '/ckeditor5/core/treemodel/position.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 Document from '/ckeditor5/core/treemodel/document.js';
 
 describe( 'Range', () => {
@@ -188,8 +189,6 @@ describe( 'Range', () => {
 
 	describe( 'getAllNodes', () => {
 		it( 'should iterate over all nodes which "starts" in the range', () => {
-			let nodes = [];
-
 			const a = new Character( 'a' );
 			const b = new Character( 'b' );
 			const x = new Character( 'x' );
@@ -207,12 +206,20 @@ describe( 'Range', () => {
 				new Position( root, [ 1, 1 ] )
 			);
 
-			for ( let node of range.getAllNodes() ) {
-				nodes.push( node );
-			}
+			let nodes = Array.from( range.getAllNodes() );
 
 			expect( nodes ).to.deep.equal( [ b, e2, 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 );
+
+			expect( nodeNames ).to.deep.equal( [ 'T:st', 'E:p', 'T:lorem ipsum', 'E:p', 'T:foo', 'E:p', 'T:bar', 'E:div', 'E:h', 'T:se' ] );
+		} );
 	} );
 
 	describe( 'containsPosition', () => {
@@ -429,12 +436,58 @@ describe( 'Range', () => {
 		it( 'should iterate over all top-level nodes of this range', () => {
 			let range = new Range( new Position( root, [ 0, 0, 3 ] ), new Position( root, [ 3, 0, 2 ] ) );
 			let nodes = Array.from( range.getTopLevelNodes() );
-			let nodeNames = nodes.map( ( node ) => {
-				return ( node instanceof Element ) ? 'E:' + node.name : 'C:' + node.character;
-			} );
+			let nodeNames = mapNodesToNames( nodes );
 
 			expect( nodeNames ).to.deep.equal( [ 'C:s', 'C:t', 'E:p', 'E:p', 'E:p', 'C:s', 'C:e' ] );
 		} );
+
+		it( 'should merge characters with same attributes', () => {
+			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' ] );
+		} );
+	} );
+
+	describe( 'getPositions', () => {
+		beforeEach( () => {
+			prepareRichRoot( root );
+		} );
+
+		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 ]
+			];
+			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 merge characters with same attributes', () => {
+			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( true ) ) {
+				expect( position.path ).to.deep.equal( expectedPaths[ i ] );
+				i++;
+			}
+
+			expect( i ).to.equal( expectedPaths.length );
+		} );
 	} );
 
 	describe( 'isFlat', () => {
@@ -453,6 +506,13 @@ describe( 'Range', () => {
 		} );
 	} );
 
+	function mapNodesToNames( nodes ) {
+		return nodes.map( ( node ) => {
+			return ( node instanceof Element ) ? 'E:' + node.name :
+				( node instanceof Text ) ? 'T:' + node.text : 'C:' + node.character;
+		} );
+	}
+
 	function prepareRichRoot() {
 		root.insertChildren( 0, [
 			new Element( 'div', [], [