Files
Monkey-Test/WebDriverAgent/Inspector/node_modules/node-balanced/index.js
T
2019-01-25 17:01:52 +08:00

345 lines
9.7 KiB
JavaScript

// Note: this currently doesn't support nested replacements because its meant to be
// greedy and grab the first head all the way to the last
//
// Node: for nested matches you can just do recursion because of the greedyness
function Balanced (config) {
config = config || {};
if (!config.open) throw new Error('Balanced: please provide a "open" property');
if (!config.close) throw new Error('Balanced: please provide a "close" property');
this.balance = config.balance || false;
this.exceptions = config.exceptions || false;
this.caseInsensitive = config.caseInsensitive;
this.head = config.head || config.open;
this.head = Array.isArray(this.head) ? this.head : [this.head];
this.open = Array.isArray(config.open) ? config.open : [config.open];
this.close = Array.isArray(config.close) ? config.close : [config.close];
if (
!Array.isArray(this.head) ||
!Array.isArray(this.open) ||
!Array.isArray(this.close) ||
!(this.head.length === this.open.length && this.open.length === this.close.length)
) {
throw new Error('Balanced: if you use arrays for a "head,open,close" you must use matching arrays for all options');
}
var headRegExp = regExpFromArray(this.head.map(this.regExpFromArrayGroupedMap, this)),
openRegExp = regExpFromArray(this.open.map(this.regExpFromArrayGroupedMap, this)),
closeRegExp = regExpFromArray(this.close.map(this.regExpFromArrayGroupedMap, this));
this.regExp = regExpFromArray([headRegExp, openRegExp, closeRegExp], 'g' + (this.caseInsensitive ? 'i' : ''));
this.regExpGroupLength = this.head.length;
}
Balanced.prototype = {
/**
* helper creating method for running regExpFromArray with one arg and grouped set to true
*
* @param {RegExp/String} value
* @return {RegExp}
*/
regExpFromArrayGroupedMap: function (value) {
return regExpFromArray([value], null, true);
},
/**
* Matches contents
*
* @param {String} string
* @param {Array} ignoreRanges
* @return {Array}
*/
matchContentsInBetweenBrackets: function (string, ignoreRanges) {
var regex = new RegExp(this.regExp),
stack = [],
matches = [],
matchedOpening = null,
match,
balanced = true;
while ((match = regex.exec(string))) {
if (ignoreRanges) {
var ignore = false;
for (var i = 0; i < ignoreRanges.length; i++) {
if (isIndexInRage(match.index, ignoreRanges[i])) {
ignore = true;
continue;
}
}
if (ignore) {
continue;
}
}
var matchResultPosition = match.indexOf(match[0], 1) - 1,
sectionIndex = Math.floor(matchResultPosition / this.regExpGroupLength),
valueIndex = matchResultPosition - (Math.floor(matchResultPosition / this.regExpGroupLength) * this.regExpGroupLength);
if (!matchedOpening && sectionIndex === 0 && (!this.balance || this.balance && !stack.length)) {
matchedOpening = match;
if (this.balance) {
stack.push(valueIndex);
} else {
stack = [valueIndex];
}
} else if (sectionIndex === 1 || sectionIndex === 0) {
stack.push(valueIndex);
} else if (sectionIndex === 2) {
var expectedValueIndex = stack.pop();
if (expectedValueIndex === valueIndex) {
if (matchedOpening !== null && stack.length === 0) {
matches.push({
index: matchedOpening.index,
length: match.index + match[0].length - matchedOpening.index,
head: matchedOpening[0],
tail: match[0]
});
matchedOpening = null;
}
} else if (this.balance) {
balanced = false;
if (this.exceptions) {
if (expectedValueIndex === undefined) {
throw errorForStringIndex('Balanced: unexpected close bracket', string, match.index);
} else if (expectedValueIndex !== valueIndex) {
throw errorForStringIndex('Balanced: mismatching close bracket, expected "' + this.close[expectedValueIndex] + '" but found "' + this.close[valueIndex] + '"', string, match.index);
}
}
}
}
}
if (this.balance) {
if (this.exceptions && !(balanced && stack.length === 0)) {
throw errorForStringIndex('Balanced: expected close bracket', string, string.length -1);
}
return balanced && stack.length === 0 ? matches : null;
} else {
return matches;
}
},
/**
* Runs replace function against matches, and source.
*
* @param {String} string
* @param {Function} replace
* @param {Array} ignoreRanges
* @return {String}
*/
replaceMatchesInBetweenBrackets: function (string, replace, ignoreRanges) {
var matches = this.matchContentsInBetweenBrackets(string, ignoreRanges);
return replaceMatchesInString(matches, string, replace);
}
};
/**
* creates an error object for the specified index
*
* @param {String} error
* @param {String} string
* @param {Number} index
* @return {Error}
*/
function errorForStringIndex (error, string, index) {
var lines = getRangesForMatch(string.substr(0, index + 1), /^.*\n?$/gim),
allLines = getRangesForMatch(string, /^.*\n?$/gim),
line = lines.length - 1,
lastLineIndex = lines.length ? lines[lines.length - 1].index : 0,
column = index + 1 - lastLineIndex,
message = '';
// show current and previous lines
for (var i = 2; i >= 0; i--) {
if (line - i >= 0 && allLines[line-i]) {
message += string.substr(allLines[line-i].index, allLines[line-i].length) + '\n';
}
}
// add carrot
for (i = 0; i < column - 1; i++) {
message += '-';
}
message += '^\n';
// show next lines
for (i = 1; i <= 2; i++) {
if (line + i >= 0 && allLines[line+i]) {
message += string.substr(allLines[line+i].index, allLines[line+i].length) + '\n';
}
}
// replace tabs with spaces
message = message.replace(/\t/g, ' ').replace(/\n$/, '');
var errorObject = new Error(error + ' at ' + (line + 1) + ':' + column + '\n\n' + message);
errorObject.line = line + 1;
errorObject.column = column;
errorObject.index = index;
return errorObject;
}
/**
* checks if index is inside of range
*
* @param {Number} index
* @param {Object} range
* @return {Boolean}
*/
function isIndexInRage (index, range) {
return index >= range.index && index <= range.index + range.length - 1;
}
/**
* generates an array of match range objects
*
* @param {String} string
* @param {RegExp} regexp
* @return {Array}
*/
function getRangesForMatch (string, regexp) {
var pattern = new RegExp(regexp),
match,
matches = [];
if (string) {
while ((match = pattern.exec(string))) {
matches.push({index: match.index, length: match[0].length, match: match[0]});
if (!match[0].length) {
pattern.lastIndex++;
}
}
}
return matches;
}
/**
* Non-destructive match replacements.
*
* @param {Array} matches
* @param {String} string
* @param {Function} replace
* @return {String}
*/
function replaceMatchesInString (matches, string, replace) {
var offset = 0;
for (var i = 0; i < matches.length; i++) {
var match = matches[i],
replacement = replace(string.substr(match.index + offset + match.head.length, match.length - match.head.length - match.tail.length), match.head, match.tail);
string = string.substr(0, match.index + offset) + replacement + string.substr(match.index + offset + match.length, (string.length) - (match.index + offset + match.length));
offset += replacement.length - match.length;
}
return string;
}
/**
* Escapes a string to be used within a RegExp
*
* @param {String} string
* @return {String}
*/
function escapeRegExp (string) {
return string.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}
/**
* creates an RegExp from an array of string or RegExp
*
* @param {Array} array
* @param {String} flags
* @param {Boolean} grouped
* @return {RegExp}
*/
function regExpFromArray (array, flags, grouped) {
var string = array.map(function (value) {
return value instanceof RegExp ? value.source : escapeRegExp(value);
}, this).join('|');
if (grouped) {
string = '(' + string + ')';
} else {
string = '(?:' + string + ')';
}
return new RegExp(string, flags || undefined);
}
/**
* returns an array of ranges that are not in the without ranges
*
* @param {Array} ranges
* @param {Array} without
* @return {Array}
*/
function rangesWithout (ranges, without) {
return ranges.filter(function (range) {
var ignored = false;
for (var i = 0; i < without.length; i++) {
if (isIndexInRage(range.index, without[i])) {
ignored = true;
break;
}
}
return !ignored;
});
}
// export generic methods
exports.replaceMatchesInString = replaceMatchesInString;
exports.getRangesForMatch = getRangesForMatch;
exports.isIndexInRage = isIndexInRage;
exports.rangesWithout = rangesWithout;
// exports.escapeRegExp = escapeRegExp;
// exports.regExpFromArray = regExpFromArray;
// allows you to create a reusable Balance object and use its `replaceMatchesInBetweenBrackets` and `matchContentsInBetweenBrackets` directly
exports.Balanced = Balanced;
exports.replacements = function (config) {
config = config || {};
var balanced = new Balanced({
head: config.head,
open: config.open,
close: config.close,
balance: config.balance,
exceptions: config.exceptions,
caseInsensitive: config.caseInsensitive
});
if (!config.source) throw new Error('Balanced: please provide a "source" property');
if (typeof config.replace !== 'function') throw new Error('Balanced: please provide a "replace" function');
return balanced.replaceMatchesInBetweenBrackets(config.source, config.replace);
};
exports.matches = function (config) {
var balanced = new Balanced({
head: config.head,
open: config.open,
close: config.close,
balance: config.balance,
exceptions: config.exceptions,
caseInsensitive: config.caseInsensitive
});
if (!config.source) throw new Error('Balanced: please provide a "source" property');
return balanced.matchContentsInBetweenBrackets(config.source, config.ignore);
};