| 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); |
| } |
| } |