/*!
* VisualEditor Range class.
*
* @copyright See AUTHORS.txt
*/
/**
* @class
*
* @constructor
* @param {number} [from=0] Anchor offset
* @param {number} [to=from] Focus offset
*/
ve.Range = function VeRange( from, to ) {
// For ease of debugging, check arguments.length when applying defaults, to preserve
// invalid arguments such as undefined and NaN that indicate a programming error.
// Range calculation errors can often propagate quite far before surfacing, so the
// indication is important.
this.from = arguments.length >= 1 ? from : 0;
this.to = arguments.length >= 2 ? to : this.from;
this.start = this.from < this.to ? this.from : this.to;
this.end = this.from < this.to ? this.to : this.from;
};
/* Inheritance */
OO.initClass( ve.Range );
/**
* @property {number} from Starting offset
*/
/**
* @property {number} to Ending offset
*/
/**
* @property {number} start Starting offset (the lesser of #to and #from)
*/
/**
* @property {number} end Ending offset (the greater of #to and #from)
*/
/* Static Methods */
/**
* Create a new range from a JSON serialization of a range
*
* @see ve.Range#toJSON
*
* @param {string} json JSON serialization
* @return {ve.Range} New range
*/
ve.Range.static.newFromJSON = function ( json ) {
return this.newFromHash( JSON.parse( json ) );
};
/**
* Create a new range from a range hash object
*
* @see ve.Range#toJSON
*
* @param {Object} hash
* @return {ve.Range} New range
*/
ve.Range.static.newFromHash = function ( hash ) {
return new ve.Range( hash.from, hash.to );
};
/**
* Create a range object that covers all of the given ranges.
*
* @static
* @param {ve.Range[]} ranges Array of ve.Range objects (at least one)
* @param {boolean} backwards Return a backwards range
* @return {ve.Range} Range that spans all of the given ranges
*/
ve.Range.static.newCoveringRange = function ( ranges, backwards ) {
if ( ranges.length === 0 ) {
throw new Error( 'newCoveringRange() requires at least one range' );
}
let minStart = ranges[ 0 ].start;
let maxEnd = ranges[ 0 ].end;
ranges.forEach( ( range, i ) => {
if ( i === 0 ) {
return;
}
if ( range.start < minStart ) {
minStart = range.start;
}
if ( range.end > maxEnd ) {
maxEnd = range.end;
}
} );
return backwards ? new ve.Range( maxEnd, minStart ) : new ve.Range( minStart, maxEnd );
};
/* Methods */
/**
* Check if an offset is within the range.
*
* Specifically we mean the whole element at a specific offset, so in effect
* this is the same as #containsRange( new ve.Range( offset, offset + 1 ) ).
*
* @param {number} offset Offset to check
* @return {boolean} If offset is within the range
*/
ve.Range.prototype.containsOffset = function ( offset ) {
return offset >= this.start && offset < this.end;
};
/**
* Check if another range is within the range.
*
* @param {ve.Range} range Range to check
* @return {boolean} If other range is within the range
*/
ve.Range.prototype.containsRange = function ( range ) {
return range.start >= this.start && range.end <= this.end;
};
/**
* Check if another range is touching this one
*
* This includes ranges which touch this one, e.g. [1,3] & [3,5],
* ranges which overlap this one, and ranges which cover
* this one completely, e.g. [1,3] & [0,5].
*
* Useful for testing if two ranges can be joined (using #expand)
*
* @param {ve.Range} range Range to check
* @return {boolean} If other range touches this range
*/
ve.Range.prototype.touchesRange = function ( range ) {
return range.end >= this.start && range.start <= this.end;
};
/**
* Check if another range overlaps this one
*
* This includes ranges which intersect this one, e.g. [1,3] & [2,4],
* and ranges which cover this one completely, e.g. [1,3] & [0,5],
* but *not* ranges which only touch, e.g. [0,2] & [2,4].
*
* @param {ve.Range} range Range to check
* @return {boolean} If other range overlaps this range
*/
ve.Range.prototype.overlapsRange = function ( range ) {
return range.end > this.start && range.start < this.end;
};
/**
* Get the length of the range.
*
* @return {number} Length of range
*/
ve.Range.prototype.getLength = function () {
return this.end - this.start;
};
/**
* Gets a range with reversed direction.
*
* @return {ve.Range} A new range
*/
ve.Range.prototype.flip = function () {
return new ve.Range( this.to, this.from );
};
/**
* Get a range that's a translated version of this one.
*
* @param {number} distance Distance to move range by
* @return {ve.Range} New translated range
*/
ve.Range.prototype.translate = function ( distance ) {
return new ve.Range( this.from + distance, this.to + distance );
};
/**
* Check if two ranges are equal, taking direction into account.
*
* @param {ve.Range|null} other
* @return {boolean}
*/
ve.Range.prototype.equals = function ( other ) {
return other && this.from === other.from && this.to === other.to;
};
/**
* Check if two ranges are equal, ignoring direction.
*
* @param {ve.Range|null} other
* @return {boolean}
*/
ve.Range.prototype.equalsSelection = function ( other ) {
return other && this.end === other.end && this.start === other.start;
};
/**
* Create a new range with a truncated length.
*
* @param {number} limit Maximum length of new range (negative for left-side truncation)
* @return {ve.Range} A new range
*/
ve.Range.prototype.truncate = function ( limit ) {
if ( limit >= 0 ) {
return new ve.Range(
this.start, Math.min( this.start + limit, this.end )
);
} else {
return new ve.Range(
Math.max( this.end + limit, this.start ), this.end
);
}
};
/**
* Expand a range to include another range, preserving direction.
*
* @param {ve.Range} other Range to expand to include
* @return {ve.Range} Range covering this range and other
*/
ve.Range.prototype.expand = function ( other ) {
return ve.Range.static.newCoveringRange( [ this, other ], this.isBackwards() );
};
/**
* Check if the range is collapsed.
*
* A collapsed range has equal start and end values making its length zero.
*
* @return {boolean} Range is collapsed
*/
ve.Range.prototype.isCollapsed = function () {
return this.from === this.to;
};
/**
* Check if the range is backwards, i.e. from > to
*
* @return {boolean} Range is backwards
*/
ve.Range.prototype.isBackwards = function () {
return this.from > this.to;
};
/**
* Get a object summarizing the range for JSON serialization
*
* @param {string} [key] Key in parent object
* @return {Object} Object for JSON serialization
*/
ve.Range.prototype.toJSON = function () {
return {
type: 'range',
from: this.from,
to: this.to
};
};
/**
* Iterate over the range, calling a function for each index, to build an array.
*
* @param {function(number): any} callback Function called for each index
* @return {Array} Array of results
*/
ve.Range.prototype.map = function ( callback ) {
const result = [];
for ( let i = this.start; i < this.end; i++ ) {
result.push( callback( i ) );
}
return result;
};
/**
* Iterate over the range, calling a function for each index
*
* @param {function(number): void} callback Function called for each index
*/
ve.Range.prototype.forEach = function ( callback ) {
for ( let i = this.start; i < this.end; i++ ) {
callback( i );
}
};
/**
* Test whether at least one index in the range passes the test implemented by the provided function.
*
* @param {function(number): boolean} callback Function called for each index
* @return {boolean} True if the callback returns true for any index
*/
ve.Range.prototype.some = function ( callback ) {
for ( let i = this.start; i < this.end; i++ ) {
if ( callback( i ) ) {
return true;
}
}
return false;
};
/**
* Test whether all indices in the range pass the test implemented by the provided function.
*
* @param {function(number): boolean} callback Function called for each index
* @return {boolean} True if the callback returns true for all indices
*/
ve.Range.prototype.every = function ( callback ) {
for ( let i = this.start; i < this.end; i++ ) {
if ( !callback( i ) ) {
return false;
}
}
return true;
};
/**
* Trim elements from both ends of the range using a callback.
*
* Walks from start forwards and end backwards, removing elements for which callback returns true.
* Stops trimming at each end when callback returns false. Returns a new trimmed range.
*
* @param {function(number): boolean} callback Called with index
* @return {ve.Range} New trimmed range
*/
ve.Range.prototype.trimCallback = function ( callback ) {
let newStart = this.start;
let newEnd = this.end;
while ( newEnd > newStart && callback( newEnd - 1 ) ) {
newEnd--;
}
while ( newStart < newEnd && callback( newStart ) ) {
newStart++;
}
return this.isBackwards() ? new ve.Range( newEnd, newStart ) : new ve.Range( newStart, newEnd );
};
/**
* Expand the range outwards from both ends while callback returns true.
* Walks from start-1 backwards and end forwards, expanding as long as callback returns true.
* Stops expanding at each end when callback returns false. Returns a new expanded range.
*
* @param {function(number, number): boolean} callback Called with (index, iFromStartOrEnd)
* @param {ve.Range} maxRange Maximum range to expand to
* @return {ve.Range} New expanded range
*/
ve.Range.prototype.expandCallback = function ( callback, maxRange ) {
let newStart = this.start;
let newEnd = this.end;
while ( newEnd < maxRange.end && callback( newEnd ) ) {
newEnd++;
}
while ( newStart > maxRange.start && callback( newStart - 1 ) ) {
newStart--;
}
return this.isBackwards() ? new ve.Range( newEnd, newStart ) : new ve.Range( newStart, newEnd );
};