blob: 74a23f131cba3034f613fa7ec4c25e680429d804 [file] [log] [blame]
var SKIP = 'skip';
var CHECK = 'check';
var ONLY = 'only';
module.exports = function (options, callback) {
var source = options.source;
var target = options.target;
var skipComments = (options.comments) ? options.comments === SKIP : true;
var skipStrings = (options.strings) ? options.strings === SKIP : true;
var skipFunctionNames = (options.functionNames) ? options.functionNames === SKIP : true;
var skipFunctionArguments = options.functionArguments === SKIP;
var skipParentheticals = options.parentheticals === SKIP;
var onceOptionUsed = false;
Object.keys(options).forEach(function(key) {
if (options[key] !== ONLY) return;
if (!onceOptionUsed) {
onceOptionUsed = true;
} else {
throw new Error('Only one syntax feature option can be the "only" one to check');
}
});
var onlyComments = options.comments === ONLY;
var onlyStrings = options.strings === ONLY;
var onlyFunctionNames = options.functionNames === ONLY;
var onlyFunctionArguments = options.functionArguments === ONLY;
var onlyParentheticals = options.parentheticals === ONLY;
var insideString = false;
var insideComment = false;
var insideSingleLineComment = false;
var insideParens = false;
var insideFunctionArguments = false;
var openingParenCount = 0;
var matchCount = 0;
var openingQuote;
var targetIsArray = Array.isArray(target);
// If the target is just a string, it is easy to check whether
// some index of the source matches it.
// If the target is an array of strings, though, we have to
// check whether some index of the source matches *any* of
// those target strings (stopping after the first match).
var getMatch = (function () {
if (!targetIsArray) {
return getMatchBase.bind(null, target);
}
return function(index) {
for (var ti = 0, tl = target.length; ti < tl; ti++) {
var checkResult = getMatchBase(target[ti], index);
if (checkResult) return checkResult;
}
return false;
}
})();
function getMatchBase(targetString, index) {
var targetStringLength = targetString.length;
// Target is a single character
if (targetStringLength === 1 && source[index] !== targetString) return false;
// Target is multiple characters
if (source.substr(index, targetStringLength) !== targetString) return false;
return {
insideParens: insideParens,
insideFunctionArguments: insideFunctionArguments,
insideComment: insideComment,
insideString: insideString,
startIndex: index,
endIndex: index + targetStringLength,
target: targetString,
};
}
for (var i = 0, l = source.length; i < l; i++) {
var currentChar = source[i];
// Register the beginning of a comment
if (
!insideString && !insideComment
&& currentChar === "/"
&& source[i - 1] !== "\\" // escaping
) {
// standard comments
if (source[i + 1] === "*") {
insideComment = true;
continue;
}
// single-line comments
if (source[i + 1] === "/") {
insideComment = true;
insideSingleLineComment = true;
continue;
}
}
if (insideComment) {
// Register the end of a standard comment
if (
!insideSingleLineComment
&& currentChar === "*"
&& source[i - 1] !== "\\" // escaping
&& source[i + 1] === "/"
&& source[i - 1] !== "/" // don't end if it's /*/
) {
insideComment = false;
continue;
}
// Register the end of a single-line comment
if (
insideSingleLineComment
&& currentChar === "\n"
) {
insideComment = false;
insideSingleLineComment = false;
}
if (skipComments) continue;
}
// Register the beginning of a string
if (!insideComment && !insideString && (currentChar === "\"" || currentChar === "'")) {
if (source[i - 1] === "\\") continue; // escaping
openingQuote = currentChar;
insideString = true;
// For string-quotes rule
if (target === currentChar) handleMatch(getMatch(i));
continue;
}
if (insideString) {
// Register the end of a string
if (currentChar === openingQuote) {
if (source[i - 1] === "\\") continue; // escaping
insideString = false;
continue;
}
if (skipStrings) continue;
}
// Register the beginning of parens/functions
if (!insideString && !insideComment && currentChar === "(") {
// Keep track of opening parentheticals so that we
// know when the outermost function (possibly
// containing nested functions) is closing
openingParenCount++;
insideParens = true;
// Only inside a function if there is a function name
// before the opening paren
if (/[a-zA-Z]/.test(source[i - 1])) {
insideFunctionArguments = true;
}
if (target === "(") handleMatch(getMatch(i));
continue;
}
if (insideParens) {
// Register the end of a function
if (currentChar === ")") {
openingParenCount--;
// Do this here so the match is still technically inside a function
if (target === ")") handleMatch(getMatch(i));
if (openingParenCount === 0) {
insideParens = false;
insideFunctionArguments = false;
}
continue;
}
}
var isFunctionName = /^[a-zA-Z]*\(/.test(source.slice(i));
if (skipFunctionNames && isFunctionName) continue;
if (onlyFunctionNames && !isFunctionName) continue;
var match = getMatch(i);
if (!match) continue;
handleMatch(match);
if (options.once) return;
}
function handleMatch(match) {
if (onlyParentheticals && !insideParens) return;
if (skipParentheticals && insideParens) return;
if (onlyFunctionArguments && !insideFunctionArguments) return;
if (skipFunctionArguments && insideFunctionArguments) return;
if (onlyStrings && !insideString) return;
if (onlyComments && !insideComment) return;
matchCount++;
callback(match, matchCount);
}
}