index.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. var SKIP = 'skip';
  2. var CHECK = 'check';
  3. var ONLY = 'only';
  4. module.exports = function (options, callback) {
  5. var source = options.source;
  6. var target = options.target;
  7. var skipComments = (options.comments) ? options.comments === SKIP : true;
  8. var skipStrings = (options.strings) ? options.strings === SKIP : true;
  9. var skipFunctionNames = (options.functionNames) ? options.functionNames === SKIP : true;
  10. var skipFunctionArguments = options.functionArguments === SKIP;
  11. var skipParentheticals = options.parentheticals === SKIP;
  12. var onceOptionUsed = false;
  13. Object.keys(options).forEach(function(key) {
  14. if (options[key] !== ONLY) return;
  15. if (!onceOptionUsed) {
  16. onceOptionUsed = true;
  17. } else {
  18. throw new Error('Only one syntax feature option can be the "only" one to check');
  19. }
  20. });
  21. var onlyComments = options.comments === ONLY;
  22. var onlyStrings = options.strings === ONLY;
  23. var onlyFunctionNames = options.functionNames === ONLY;
  24. var onlyFunctionArguments = options.functionArguments === ONLY;
  25. var onlyParentheticals = options.parentheticals === ONLY;
  26. var insideString = false;
  27. var insideComment = false;
  28. var insideSingleLineComment = false;
  29. var insideParens = false;
  30. var insideFunctionArguments = false;
  31. var openingParenCount = 0;
  32. var matchCount = 0;
  33. var openingQuote;
  34. var targetIsArray = Array.isArray(target);
  35. // If the target is just a string, it is easy to check whether
  36. // some index of the source matches it.
  37. // If the target is an array of strings, though, we have to
  38. // check whether some index of the source matches *any* of
  39. // those target strings (stopping after the first match).
  40. var getMatch = (function () {
  41. if (!targetIsArray) {
  42. return getMatchBase.bind(null, target);
  43. }
  44. return function(index) {
  45. for (var ti = 0, tl = target.length; ti < tl; ti++) {
  46. var checkResult = getMatchBase(target[ti], index);
  47. if (checkResult) return checkResult;
  48. }
  49. return false;
  50. }
  51. })();
  52. function getMatchBase(targetString, index) {
  53. var targetStringLength = targetString.length;
  54. // Target is a single character
  55. if (targetStringLength === 1 && source[index] !== targetString) return false;
  56. // Target is multiple characters
  57. if (source.substr(index, targetStringLength) !== targetString) return false;
  58. return {
  59. insideParens: insideParens,
  60. insideFunctionArguments: insideFunctionArguments,
  61. insideComment: insideComment,
  62. insideString: insideString,
  63. startIndex: index,
  64. endIndex: index + targetStringLength,
  65. target: targetString,
  66. };
  67. }
  68. for (var i = 0, l = source.length; i < l; i++) {
  69. var currentChar = source[i];
  70. // Register the beginning of a comment
  71. if (
  72. !insideString && !insideComment
  73. && currentChar === "/"
  74. && source[i - 1] !== "\\" // escaping
  75. ) {
  76. // standard comments
  77. if (source[i + 1] === "*") {
  78. insideComment = true;
  79. continue;
  80. }
  81. // single-line comments
  82. if (source[i + 1] === "/") {
  83. insideComment = true;
  84. insideSingleLineComment = true;
  85. continue;
  86. }
  87. }
  88. if (insideComment) {
  89. // Register the end of a standard comment
  90. if (
  91. !insideSingleLineComment
  92. && currentChar === "*"
  93. && source[i - 1] !== "\\" // escaping
  94. && source[i + 1] === "/"
  95. && source[i - 1] !== "/" // don't end if it's /*/
  96. ) {
  97. insideComment = false;
  98. continue;
  99. }
  100. // Register the end of a single-line comment
  101. if (
  102. insideSingleLineComment
  103. && currentChar === "\n"
  104. ) {
  105. insideComment = false;
  106. insideSingleLineComment = false;
  107. }
  108. if (skipComments) continue;
  109. }
  110. // Register the beginning of a string
  111. if (!insideComment && !insideString && (currentChar === "\"" || currentChar === "'")) {
  112. if (source[i - 1] === "\\") continue; // escaping
  113. openingQuote = currentChar;
  114. insideString = true;
  115. // For string-quotes rule
  116. if (target === currentChar) handleMatch(getMatch(i));
  117. continue;
  118. }
  119. if (insideString) {
  120. // Register the end of a string
  121. if (currentChar === openingQuote) {
  122. if (source[i - 1] === "\\") continue; // escaping
  123. insideString = false;
  124. continue;
  125. }
  126. if (skipStrings) continue;
  127. }
  128. // Register the beginning of parens/functions
  129. if (!insideString && !insideComment && currentChar === "(") {
  130. // Keep track of opening parentheticals so that we
  131. // know when the outermost function (possibly
  132. // containing nested functions) is closing
  133. openingParenCount++;
  134. insideParens = true;
  135. // Only inside a function if there is a function name
  136. // before the opening paren
  137. if (/[a-zA-Z]/.test(source[i - 1])) {
  138. insideFunctionArguments = true;
  139. }
  140. if (target === "(") handleMatch(getMatch(i));
  141. continue;
  142. }
  143. if (insideParens) {
  144. // Register the end of a function
  145. if (currentChar === ")") {
  146. openingParenCount--;
  147. // Do this here so the match is still technically inside a function
  148. if (target === ")") handleMatch(getMatch(i));
  149. if (openingParenCount === 0) {
  150. insideParens = false;
  151. insideFunctionArguments = false;
  152. }
  153. continue;
  154. }
  155. }
  156. var isFunctionName = /^[a-zA-Z]*\(/.test(source.slice(i));
  157. if (skipFunctionNames && isFunctionName) continue;
  158. if (onlyFunctionNames && !isFunctionName) continue;
  159. var match = getMatch(i);
  160. if (!match) continue;
  161. handleMatch(match);
  162. if (options.once) return;
  163. }
  164. function handleMatch(match) {
  165. if (onlyParentheticals && !insideParens) return;
  166. if (skipParentheticals && insideParens) return;
  167. if (onlyFunctionArguments && !insideFunctionArguments) return;
  168. if (skipFunctionArguments && insideFunctionArguments) return;
  169. if (onlyStrings && !insideString) return;
  170. if (onlyComments && !insideComment) return;
  171. matchCount++;
  172. callback(match, matchCount);
  173. }
  174. }