Ver código fonte

Range new methods: intersectsWith(), clone(), containsPosition(), getTransformedByInsertion(), getDifference(), getCommon() and createFromPositionAndOffset()

Szymon Cofalik 10 anos atrás
pai
commit
2da81eba5f

+ 206 - 1
packages/ckeditor5-utils/src/document/range.js

@@ -5,7 +5,7 @@
 
 'use strict';
 
-CKEDITOR.define( [ 'document/positioniterator', 'document/position' ], ( PositionIterator, Position ) => {
+CKEDITOR.define( [ 'document/position', 'document/positioniterator', 'utils' ], ( Position, PositionIterator, utils ) => {
 	/**
 	 * Range class. Range is iterable.
 	 *
@@ -71,6 +71,211 @@ CKEDITOR.define( [ 'document/positioniterator', 'document/position' ], ( Positio
 			return this.start.isEqual( otherRange.start ) && this.end.isEqual( otherRange.end );
 		}
 
+		/**
+		 * Checks whether this {document.Range range} intersects with the given {document.Range range}.
+		 *
+		 * @param {document.Range} otherRange Range to check.
+		 * @returns {boolean} True if ranges intersect.
+		 */
+		intersectsWith( otherRange ) {
+			const isBefore = this.start.compareWith( otherRange.start ) == Position.BEFORE &&
+				this.end.compareWith( otherRange.start ) == Position.BEFORE;
+
+			const isAfter = this.start.compareWith( otherRange.end ) == Position.AFTER &&
+				this.end.compareWith( otherRange.end ) == Position.AFTER;
+
+			const touches = this.start.isEqual( otherRange.end ) || this.end.isEqual( otherRange.start );
+
+			return !isBefore && !isAfter && !touches;
+		}
+
+		/**
+		 * Creates and returns a new instance of {@link document.Range}
+		 * that is equal to this {@link document.Range range}.
+		 *
+		 * @returns {document.Position} Cloned {@link document.Range range}.
+		 */
+		clone() {
+			return new Range( this.start.clone(), this.end.clone() );
+		}
+
+		/**
+		 * Checks whether this {document.Range range} contains given (document.Position position}.
+		 *
+		 * @param {document.Position} position Position to check.
+		 * @returns {boolean} True if given {document.Position position} is contained.
+		 */
+		containsPosition( position ) {
+			return position.compareWith( this.start ) == Position.AFTER && position.compareWith( this.end ) == Position.BEFORE;
+		}
+
+		/**
+		 * Returns an array containing one or two {document.Range ranges} that are results of transforming this
+		 * {document.Range range} by inserting `howMany` nodes at `insertPosition`. Two {document.Range ranges} are
+		 * returned if the insertion was inside this {document.Range range}.
+		 *
+		 * Examples:
+		 * 	let range = new Range( new Position( [ 2, 7 ], root ), new Position( [ 4, 0, 1 ], root ) );
+		 * 	let transformed = range.getTransformedByInsertion( new Position( [ 1 ], root ), 2 );
+		 * 	// transformed array has one range from [ 4, 7 ] to [ 6, 0, 1 ]
+		 *
+		 * 	transformed = range.getTransformedByInsertion( new Position( [ 3, 2 ], root ), 4 );
+		 * 	// transformed array has two ranges: from [ 2, 7 ] to [ 3, 2 ] and from [ 3, 6 ] to [ 4, 0, 1 ]
+		 *
+		 * 	transformed = range.getTransformedByInsertion( new Position( [ 3, 2 ], root ), 4, true );
+		 * 	// transformed array has one range which is equal to `range`. This is because of spreadOnlyOnSameLevel flag.
+		 *
+		 * @param {document.Position} insertPosition Position where nodes are inserted.
+		 * @param {Number} howMany How many nodes are inserted.
+		 * @param {Boolean} spreadOnlyOnSameLevel Flag indicating whether this {document.Range range} should be spread
+		 * if insertion was inside a node from this {document.Range range} but not in the range itself.
+		 * @returns {Array.<document.Range>} Result of the transformation.
+		 */
+		getTransformedByInsertion( insertPosition, howMany, spreadOnlyOnSameLevel ) {
+			// Flag indicating whether this whole range and given insertPosition is on the same tree level.
+			const areOnSameLevel = utils.compareArrays( this.start.parentPath, this.end.parentPath ) == utils.compareArrays.SAME &&
+				utils.compareArrays( this.start.parentPath, insertPosition.parentPath ) == utils.compareArrays.SAME;
+
+			if ( this.containsPosition( insertPosition ) && ( !spreadOnlyOnSameLevel || areOnSameLevel ) ) {
+				// Range has to be spread. The first part is from original start to the spread point.
+				// The other part is from spread point to the original end, but transformed by
+				// insertion to reflect insertion changes.
+
+				return [
+					new Range(
+						insertPosition.getTransformedByInsertion( insertPosition, howMany, true ),
+						this.end.getTransformedByInsertion( insertPosition, howMany, true )
+					),
+					new Range(
+						this.start.clone(),
+						insertPosition.clone()
+					)
+				];
+			} else {
+				// If insertion is not inside the range, simply transform range boundaries (positions) by the insertion.
+				// Both, one or none of them might be affected by the insertion.
+
+				const range = this.clone();
+
+				range.start = range.start.getTransformedByInsertion( insertPosition, howMany, true );
+				range.end = range.end.getTransformedByInsertion( insertPosition, howMany, false );
+
+				return [ range ];
+			}
+		}
+
+		/**
+		 * Gets a part of this {document.Range range} that is not a part of given {document.Range range}. Returned
+		 * array contains zero, one or two {document.Range ranges}.
+		 *
+		 * Examples:
+		 * 	let range = new Range( new Position( [ 2, 7 ], root ), new Position( [ 4, 0, 1 ], root ) );
+		 * 	let otherRange = new Range( new Position( [ 1 ], root ), new Position( [ 5 ], root ) );
+		 * 	let transformed = range.getDifference( otherRange );
+		 * 	// transformed array has no ranges because `otherRange` contains `range`
+		 *
+		 * 	otherRange = new Range( new Position( [ 1 ], root ), new Position( [ 3 ], root ) );
+		 * 	transformed = range.getDifference( otherRange );
+		 * 	// transformed array has one range: from [ 3 ] to [ 4, 0, 1 ]
+		 *
+		 * 	otherRange = new Range( new Position( [ 3 ], root ), new Position( [ 4 ], root ) );
+		 * 	transformed = range.getDifference( otherRange );
+		 * 	// transformed array has two ranges: from [ 2, 7 ] to [ 3 ] and from [ 4 ] to [ 4, 0, 1 ]
+		 *
+		 * @param {document.Range} otherRange Range to differentiate against.
+		 * @returns {Array.<document.Range>} The difference between ranges.
+		 */
+		getDifference( otherRange ) {
+			// If ranges do not intersect, return the original range.
+			if ( !otherRange.intersectsWith( this ) ) {
+				return [
+					this.clone()
+				];
+			}
+
+			// At this point we know that ranges intersect but given range does not contain this range.
+			const ranges = [];
+
+			if ( this.containsPosition( otherRange.start ) ) {
+				// Given range start is inside this range. This means that we have to
+				// add shrunken range - from the start to the middle of this range.
+				ranges.push(
+					new Range(
+						this.start.clone(),
+						otherRange.start.clone()
+					)
+				);
+			}
+
+			if ( this.containsPosition( otherRange.end ) ) {
+				// Given range end is inside this range. This means that we have to
+				// add shrunken range - from the middle of this range to the end.
+				ranges.push(
+					new Range(
+						otherRange.end.clone(),
+						this.end.clone()
+					)
+				);
+			}
+
+			return ranges;
+		}
+
+		/**
+		 * Returns a part of this {document.Range range} that is also a part of given {document.Range range}. If
+		 * ranges has no common part, returns null.
+		 *
+		 * Examples:
+		 * 	let range = new Range( new Position( [ 2, 7 ], root ), new Position( [ 4, 0, 1 ], root ) );
+		 * 	let otherRange = new Range( new Position( [ 1 ], root ), new Position( [ 2 ], root ) );
+		 * 	let transformed = range.getCommon( otherRange ); // null - ranges have no common part
+		 *
+		 * 	otherRange = new Range( new Position( [ 3 ], root ), new Position( [ 5 ], root ) );
+		 * 	transformed = range.getCommon( otherRange ); // range from [ 3 ] to [ 4, 0, 1 ]
+		 *
+		 * @param {document.Range} otherRange Range to compare with.
+		 * @returns {document.Range} Range that is common part of given ranges.
+		 */
+		getCommon( otherRange ) {
+			// If ranges do not intersect, they do not have common part.
+			if ( !otherRange.intersectsWith( this ) ) {
+				return null;
+			}
+
+			// At this point we know that ranges intersect, so a common range will be returned.
+			// At most, it will be same as this range.
+			let commonRangeStart = this.start;
+			let commonRangeEnd = this.end;
+
+			if ( this.containsPosition( otherRange.start ) ) {
+				// Given range start is inside this range. This means that we have to
+				// shrink common range to the given range start.
+				commonRangeStart = otherRange.start;
+			}
+
+			if ( this.containsPosition( otherRange.end ) ) {
+				// Given range end is inside this range. This means that we have to
+				// shrink common range to the given range end.
+				commonRangeEnd = otherRange.end;
+			}
+
+			return new Range( commonRangeStart.clone(), commonRangeEnd.clone() );
+		}
+
+		/**
+		 * Creates a new range spreading from specified position to the same position moved by given offset.
+		 *
+		 * @param {document.Position} position Beginning of the range.
+		 * @param {Number} offset How long the range should be.
+		 * @returns {document.Range}
+		 */
+		static createFromPositionAndOffset( position, offset ) {
+			let endPosition = position.clone();
+			endPosition.offset += offset;
+
+			return new Range( position, endPosition );
+		}
+
 		/**
 		 * Range iterator.
 		 *

+ 239 - 10
packages/ckeditor5-utils/tests/document/range.js

@@ -17,8 +17,7 @@ const modules = bender.amd.require(
 
 describe( 'Range', () => {
 	let Range, Position, Element, Character, Document;
-
-	let start, end;
+	let start, end, root;
 
 	before( () => {
 		Position = modules[ 'document/position' ];
@@ -27,8 +26,11 @@ describe( 'Range', () => {
 		Character = modules[ 'document/character' ];
 		Document = modules[ 'document/document' ];
 
-		start = new Position( [ 0 ] );
-		end = new Position( [ 1 ] );
+		let doc = new Document();
+		root = doc.createRoot( 'root' );
+
+		start = new Position( [ 0 ], root );
+		end = new Position( [ 1 ], root );
 	} );
 
 	let range;
@@ -46,8 +48,8 @@ describe( 'Range', () => {
 
 	describe( 'isEqual', () => {
 		it( 'should return true if the ranges are the same', () => {
-			let sameStart = new Position( [ 0 ] );
-			let sameEnd = new Position( [ 1 ] );
+			let sameStart = new Position( [ 0 ], root );
+			let sameEnd = new Position( [ 1 ], root );
 
 			let sameRange = new Range( sameStart, sameEnd );
 
@@ -57,8 +59,8 @@ describe( 'Range', () => {
 		it( 'should return false if the start position is different', () => {
 			let range = new Range( start, end );
 
-			let diffStart = new Position( [ 1 ] );
-			let sameEnd = new Position( [ 1 ] );
+			let diffStart = new Position( [ 1 ], root );
+			let sameEnd = new Position( [ 1 ], root );
 
 			let diffRange = new Range( diffStart, sameEnd );
 
@@ -66,8 +68,8 @@ describe( 'Range', () => {
 		} );
 
 		it( 'should return false if the end position is different', () => {
-			let sameStart = new Position( [ 0 ] );
-			let diffEnd = new Position( [ 0 ] );
+			let sameStart = new Position( [ 0 ], root );
+			let diffEnd = new Position( [ 0 ], root );
 
 			let diffRange = new Range( sameStart, diffEnd );
 
@@ -114,5 +116,232 @@ describe( 'Range', () => {
 				expect( range.end.path ).to.deep.equal( [ 0, 2 ] );
 			} );
 		} );
+
+		describe( 'createFromPositionAndOffset', () => {
+			it( 'should make range from start position and offset', () => {
+				const position = new Position( [ 1, 2, 3 ], root );
+				const range = Range.createFromPositionAndOffset( position, 4 );
+
+				expect( range ).to.be.instanceof( Range );
+				expect( range.start.isEqual( position ) ).to.be.true;
+				expect( range.end.root ).to.equal( range.start.root );
+				expect( range.end.path ).to.deep.equal( [ 1, 2, 7 ] );
+			} );
+		} );
+	} );
+
+	describe( 'clone', () => {
+		it( 'should return a new, equal position', () => {
+			const clone = range.clone();
+
+			expect( clone ).not.to.be.equal( range ); // clone is not pointing to the same object as position
+			expect( clone.isEqual( range ) ).to.be.true; // but they are equal in the position-sense
+		} );
+	} );
+
+	describe( 'intersectsWith', () => {
+		beforeEach( () => {
+			range = new Range( new Position( [ 3, 7 ], root ), new Position( [ 4, 2, 2 ], root ) );
+		} );
+
+		// [  --  range A  --  ]  <  --  range B --  >
+		it( 'should return false if ranges are next to each other', () => {
+			const otherRange = new Range( new Position( [ 1, 3 ], root ), new Position( [ 3, 6 ], root ) );
+
+			expect( range.intersectsWith( otherRange ) ).to.be.false;
+			expect( otherRange.intersectsWith( range ) ).to.be.false;
+		} );
+
+		// [  --  range A  --  ]<  --  range B --  >
+		it( 'should return false if ranges are touching', () => {
+			const otherRange = new Range( new Position( [ 1, 3 ], root ), new Position( [ 3, 7 ], root ) );
+
+			expect( range.intersectsWith( otherRange ) ).to.be.false;
+			expect( otherRange.intersectsWith( range ) ).to.be.false;
+		} );
+
+		// [  --  range A  <  --  ]  range B --  >
+		it( 'should return true if ranges partially intersects on one of sides', () => {
+			const otherRange = new Range( new Position( [ 1, 3 ], root ), new Position( [ 3, 7, 2 ], root ) );
+
+			expect( range.intersectsWith( otherRange ) ).to.be.true;
+			expect( otherRange.intersectsWith( range ) ).to.be.true;
+		} );
+
+		// [<  --  range A  range B  --  >]
+		it( 'should return true if ranges are same', () => {
+			const otherRange = new Range( new Position( [ 3, 7 ], root ), new Position( [ 4, 2, 2 ], root ) );
+
+			expect( range.intersectsWith( otherRange ) ).to.be.true;
+			expect( otherRange.intersectsWith( range ) ).to.be.true;
+		} );
+
+		// [  --  range A  --  <  --  range B  --  >  ]
+		it( 'should return true if one range contains or is contained by the other', () => {
+			const otherRange = new Range( new Position( [ 1 ], root ), new Position( [ 4, 3 ], root ) );
+
+			expect( range.intersectsWith( otherRange ) ).to.be.true;
+			expect( otherRange.intersectsWith( range ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'containsPosition', () => {
+		beforeEach( () => {
+			range = new Range( new Position( [ 1 ], root ), new Position( [ 3 ], root ) );
+		} );
+
+		it( 'should return false if position is before range', () => {
+			const position = new Position( [ 0, 4 ], root );
+
+			expect( range.containsPosition( position ) ).to.be.false;
+		} );
+
+		it( 'should return false if position is after range', () => {
+			const position = new Position( [ 3, 0 ], root );
+
+			expect( range.containsPosition( position ) ).to.be.false;
+		} );
+
+		it( 'should return true if position is inside range', () => {
+			const position = new Position( [ 2, 2 ], root );
+
+			expect( range.containsPosition( position ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'getTransformedByInsertion', () => {
+		it( 'should return an array of Range objects', () => {
+			const range = new Range( new Position( [ 0 ], root ), new Position( [ 1 ], root ) );
+			const transformed = range.getTransformedByInsertion( new Position( [ 2 ], root ), 2 );
+
+			expect( transformed ).to.be.instanceof( Array );
+			expect( transformed[ 0 ] ).to.be.instanceof( Range );
+		} );
+
+		it( 'should update it\'s positions offsets if insertion is before range and they are affected', () => {
+			const range = new Range( new Position( [ 3, 2 ], root ), new Position( [ 3, 4 ], root ) );
+			const transformed = range.getTransformedByInsertion( new Position( [ 3, 1 ], root ), 2 );
+
+			expect( transformed[ 0 ].start.offset ).to.equal( 4 );
+			expect( transformed[ 0 ].end.offset ).to.equal( 6 );
+		} );
+
+		it( 'should update it\'s positions paths if insertion is before range and they are affected', () => {
+			const range = new Range( new Position( [ 3, 2 ], root ), new Position( [ 4, 4 ], root ) );
+			const transformed = range.getTransformedByInsertion( new Position( [ 0 ], root ), 2 );
+
+			expect( transformed[ 0 ].start.path[ 0 ] ).to.equal( 5 );
+			expect( transformed[ 0 ].end.path[ 0 ] ).to.equal( 6 );
+		} );
+
+		it( 'should return array with two ranges and updated positions if insertion was in the middle of range', () => {
+			const range = new Range( new Position( [ 3, 2 ], root ), new Position( [ 5, 4 ], root ) );
+			const transformed = range.getTransformedByInsertion( new Position( [ 4, 1, 6 ], root ), 4 );
+
+			expect( transformed.length ).to.equal( 2 );
+
+			expect( transformed[ 0 ].start.path ).to.deep.equal( [ 4, 1, 10 ] );
+			expect( transformed[ 0 ].end.path ).to.deep.equal( [ 5, 4 ] );
+
+			expect( transformed[ 1 ].start.path ).to.deep.equal( [ 3, 2 ] );
+			expect( transformed[ 1 ].end.path ).to.deep.equal( [ 4, 1, 6 ] );
+		} );
+
+		it( 'should not updated positions if insertion is after range', () => {
+			const range = new Range( new Position( [ 3, 2 ], root ), new Position( [ 4, 4 ], root ) );
+			const transformed = range.getTransformedByInsertion( new Position( [ 4, 4 ], root ), 3 );
+
+			expect( transformed[ 0 ].start.path ).to.deep.equal( [ 3, 2 ] );
+			expect( transformed[ 0 ].end.path ).to.deep.equal( [ 4, 4 ] );
+		} );
+	} );
+
+	describe( 'getDifference', () => {
+		let range;
+
+		beforeEach( () => {
+			range = new Range( new Position( [ 3, 2 ], root ), new Position( [ 5, 4 ], root ) );
+		} );
+
+		it( 'should return an array of Range objects', () => {
+			const otherRange = new Range( new Position( [ 6 ], root ), new Position( [ 7 ], root ) );
+			const diff = range.getDifference( otherRange );
+
+			expect( diff ).to.be.instanceof( Array );
+			expect( diff[ 0 ] ).to.be.instanceof( Range );
+		} );
+
+		it( 'should return original range if other range does not intersect with it', () => {
+			const otherRange = new Range( new Position( [ 6 ], root ), new Position( [ 7 ], root ) );
+			const diff = range.getDifference( otherRange );
+
+			expect( diff[ 0 ].start.path ).to.deep.equal( [ 3, 2 ] );
+			expect( diff[ 0 ].end.path ).to.deep.equal( [ 5, 4 ] );
+		} );
+
+		it( 'should return shrunken range if other range intersects with it', () => {
+			const otherRange = new Range( new Position( [ 4, 1 ], root ), new Position( [ 7 ], root ) );
+			const diff = range.getDifference( otherRange );
+
+			expect( diff[ 0 ].start.path ).to.deep.equal( [ 3, 2 ] );
+			expect( diff[ 0 ].end.path ).to.deep.equal( [ 4, 1 ] );
+		} );
+
+		it( 'should return an empty array if other range contains or is same as the original range', () => {
+			const otherRange = new Range( new Position( [ 2 ], root ), new Position( [ 6 ], root ) );
+			const diff = range.getDifference( otherRange );
+
+			expect( diff.length ).to.equal( 0 );
+		} );
+
+		it( 'should two ranges if other range is contained by the original range', () => {
+			const otherRange = new Range( new Position( [ 3, 7 ], root ), new Position( [ 4, 1 ], root ) );
+			const diff = range.getDifference( otherRange );
+
+			expect( diff.length ).to.equal( 2 );
+
+			expect( diff[ 0 ].start.path ).to.deep.equal( [ 3, 2 ] );
+			expect( diff[ 0 ].end.path ).to.deep.equal( [ 3, 7 ] );
+
+			expect( diff[ 1 ].start.path ).to.deep.equal( [ 4, 1 ] );
+			expect( diff[ 1 ].end.path ).to.deep.equal( [ 5, 4 ] );
+		} );
+	} );
+
+	describe( 'getCommon', () => {
+		let range;
+
+		beforeEach( () => {
+			range = new Range( new Position( [ 3, 2 ], root ), new Position( [ 5, 4 ], root ) );
+		} );
+
+		it( 'should return null if ranges do not intersect', () => {
+			const otherRange = new Range( new Position( [ 5, 4 ], root ), new Position( [ 7 ], root ) );
+			const common = range.getCommon( otherRange );
+
+			expect( common ).to.be.null;
+		} );
+
+		it( 'should return a range equal to the common part of ranges - original range contains the other range', () => {
+			const otherRange = new Range( new Position( [ 4 ], root ), new Position( [ 5 ], root ) );
+			const common = range.getCommon( otherRange );
+
+			expect( common.isEqual( otherRange ) ).to.be.true;
+		} );
+
+		it( 'should return a range equal to the common part of ranges - original range is contained by the other range', () => {
+			const otherRange = new Range( new Position( [ 3 ], root ), new Position( [ 6 ], root ) );
+			const common = range.getCommon( otherRange );
+
+			expect( common.isEqual( range ) ).to.be.true;
+		} );
+
+		it( 'should return a range equal to the common part of ranges - original range intersects with the other range', () => {
+			const otherRange = new Range( new Position( [ 3 ], root ), new Position( [ 4, 7 ], root ) );
+			const common = range.getCommon( otherRange );
+
+			expect( common.start.path ).to.deep.equal( [ 3, 2 ] );
+			expect( common.end.path ).to.deep.equal( [ 4, 7 ] );
+		} );
 	} );
 } );