204 lines
6 KiB
JavaScript
204 lines
6 KiB
JavaScript
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);
|
|
}
|
|
}
|