index.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. "use strict";
  2. var ensureString = require("type/string/ensure")
  3. , ensurePlainFunction = require("type/plain-function/ensure")
  4. , from = require("es5-ext/array/from")
  5. , primitiveSet = require("es5-ext/object/primitive-set")
  6. , eventEmitter = require("event-emitter")
  7. , allOff = require("event-emitter/all-off")
  8. , d = require("d")
  9. , eolSet = require("./lib/ws-eol")
  10. , wsSet = require("./lib/ws")
  11. , identStart = require("./lib/ident-start-pattern")
  12. , identNext = require("./lib/ident-next-pattern");
  13. var objHasOwnProperty = Object.prototype.hasOwnProperty
  14. , preRegExpSet = primitiveSet.apply(null, from(";{=([,<>+-*/%&|^!~?:}"))
  15. , nonNameSet = primitiveSet.apply(null, from(";{=([,<>+-*/%&|^!~?:})].`"))
  16. , reIdentStart = new RegExp(identStart)
  17. , reIdentNext = new RegExp(identNext);
  18. var code, index, char, state, columnIndex, line, quote, scopeDepth, templateContext, previousToken
  19. , followsWhitespace, results, followsSkip, collectedScopeDatum, collectedScopeData
  20. , collectedScopeDepth, commentDatum, shouldCollectComments;
  21. var handleEol = function () {
  22. if (char === "\r" && code[index + 1] === "\n") char = code[++index];
  23. columnIndex = index + 1;
  24. ++line;
  25. };
  26. var emitter = eventEmitter();
  27. var accessor = Object.create(null, {
  28. skipCodePart: d(function (codePart) {
  29. var codePartLength = codePart.length;
  30. for (var i = 0; i < codePartLength; ++i) {
  31. if (code[index + i] !== codePart[i]) return false;
  32. }
  33. index += codePartLength;
  34. char = code[index];
  35. previousToken = code[index - 1];
  36. followsWhitespace = false;
  37. followsSkip = true;
  38. return true;
  39. }),
  40. skipIdentifier: d(function () {
  41. if (!reIdentStart.test(char)) return null;
  42. var startIndex = index;
  43. var identifier = char;
  44. while ((char = code[++index]) && reIdentNext.test(char)) identifier += char;
  45. followsWhitespace = false;
  46. followsSkip = true;
  47. previousToken = code[index - 1];
  48. return { name: identifier, start: startIndex, end: index };
  49. }),
  50. skipWhitespace: d(function () {
  51. while (char) {
  52. if (objHasOwnProperty.call(wsSet, char)) {
  53. if (objHasOwnProperty.call(eolSet, char)) handleEol();
  54. } else if (char === "/") {
  55. if (code[index + 1] === "/") {
  56. // Single line comment
  57. if (shouldCollectComments) {
  58. commentDatum = {
  59. type: "comment",
  60. point: index,
  61. line: line,
  62. column: index - columnIndex
  63. };
  64. }
  65. index += 2;
  66. char = code[index];
  67. while (char) {
  68. if (objHasOwnProperty.call(eolSet, char)) {
  69. if (commentDatum) {
  70. commentDatum.endPoint = index;
  71. results.push(commentDatum);
  72. commentDatum = null;
  73. }
  74. handleEol();
  75. break;
  76. }
  77. char = code[++index];
  78. }
  79. } else if (code[index + 1] === "*") {
  80. if (shouldCollectComments) {
  81. commentDatum = {
  82. type: "comment",
  83. point: index,
  84. line: line,
  85. column: index - columnIndex
  86. };
  87. }
  88. index += 2;
  89. char = code[index];
  90. while (char) {
  91. if (objHasOwnProperty.call(eolSet, char)) handleEol();
  92. if (char === "*" && code[index + 1] === "/") {
  93. if (commentDatum) {
  94. commentDatum.endPoint = index + 2;
  95. results.push(commentDatum);
  96. commentDatum = null;
  97. }
  98. char = code[++index];
  99. break;
  100. }
  101. char = code[++index];
  102. }
  103. } else {
  104. break;
  105. }
  106. } else {
  107. break;
  108. }
  109. followsWhitespace = true;
  110. followsSkip = true;
  111. char = code[++index];
  112. }
  113. }),
  114. collectScope: d(function () {
  115. if (char !== "(") return;
  116. previousToken = char;
  117. char = code[++index];
  118. followsSkip = true;
  119. if (collectedScopeDatum) collectedScopeData.push(collectedScopeDepth, collectedScopeDatum);
  120. collectedScopeDepth = ++scopeDepth;
  121. collectedScopeDatum = {
  122. type: "scope",
  123. point: index + 1,
  124. line: line,
  125. column: index - columnIndex + 1
  126. };
  127. }),
  128. stop: d(function () { state = null; }),
  129. index: d.gs(function () { return index; }),
  130. previousToken: d.gs(function () { return previousToken; }),
  131. scopeDepth: d.gs(function () { return scopeDepth; }),
  132. shouldCollectComments: d.gs(
  133. function () { return shouldCollectComments; },
  134. function (value) { shouldCollectComments = Boolean(value); }
  135. )
  136. });
  137. module.exports = function (userCode, executor) {
  138. code = ensureString(userCode);
  139. executor = ensurePlainFunction(executor);
  140. allOff(emitter);
  141. executor(emitter, accessor);
  142. index = -1;
  143. state = "out";
  144. columnIndex = 0;
  145. line = 1;
  146. scopeDepth = 0;
  147. templateContext = [];
  148. previousToken = null;
  149. followsWhitespace = true;
  150. results = [];
  151. followsSkip = false;
  152. collectedScopeDatum = null;
  153. collectedScopeData = [];
  154. collectedScopeDepth = null;
  155. stateLoop: while (state) {
  156. if (followsSkip) followsSkip = false;
  157. else char = code[++index];
  158. if (!char) break;
  159. switch (state) {
  160. case "out":
  161. if (objHasOwnProperty.call(wsSet, char)) {
  162. if (objHasOwnProperty.call(eolSet, char)) {
  163. handleEol();
  164. }
  165. followsWhitespace = true;
  166. continue stateLoop;
  167. }
  168. if (char === "/") {
  169. if (previousToken && objHasOwnProperty.call(preRegExpSet, previousToken)) {
  170. state = "slashOrRegexp";
  171. } else {
  172. state = "slash";
  173. }
  174. } else if (char === "'" || char === "\"") {
  175. state = "string";
  176. quote = char;
  177. } else if (char === "`") {
  178. state = "template";
  179. } else if (char === "(" || char === "{" || char === "[") {
  180. ++scopeDepth;
  181. } else if (char === ")" || char === "}" || char === "]") {
  182. if (scopeDepth === collectedScopeDepth) {
  183. collectedScopeDatum.raw = code.slice(collectedScopeDatum.point - 1, index);
  184. results.push(collectedScopeDatum);
  185. collectedScopeDatum = collectedScopeData.pop();
  186. collectedScopeDepth = collectedScopeData.pop();
  187. }
  188. --scopeDepth;
  189. if (char === "}") {
  190. if (templateContext[templateContext.length - 1] === scopeDepth + 1) {
  191. templateContext.pop();
  192. state = "template";
  193. }
  194. }
  195. }
  196. if (
  197. !previousToken ||
  198. followsWhitespace ||
  199. objHasOwnProperty.call(nonNameSet, previousToken) ||
  200. objHasOwnProperty.call(nonNameSet, char)
  201. ) {
  202. emitter.emit("trigger:" + char, accessor);
  203. if (followsSkip) continue stateLoop;
  204. }
  205. previousToken = char;
  206. followsWhitespace = false;
  207. continue stateLoop;
  208. case "slashOrRegexp":
  209. case "slash":
  210. if (char === "/") {
  211. if (shouldCollectComments) {
  212. commentDatum = {
  213. type: "comment",
  214. point: index - 1,
  215. line: line,
  216. column: index - columnIndex - 1
  217. };
  218. }
  219. state = "singleLineComment";
  220. } else if (char === "*") {
  221. if (shouldCollectComments) {
  222. commentDatum = {
  223. type: "comment",
  224. point: index - 1,
  225. line: line,
  226. column: index - columnIndex - 1
  227. };
  228. }
  229. state = "multiLineComment";
  230. } else if (objHasOwnProperty.call(eolSet, char)) {
  231. handleEol();
  232. followsWhitespace = true;
  233. state = "out";
  234. continue stateLoop;
  235. } else if (state === "slashOrRegexp") {
  236. state = "regexp";
  237. } else {
  238. state = "out";
  239. continue stateLoop;
  240. }
  241. break;
  242. case "singleLineComment":
  243. if (objHasOwnProperty.call(eolSet, char)) {
  244. if (commentDatum) {
  245. commentDatum.endPoint = index;
  246. results.push(commentDatum);
  247. commentDatum = null;
  248. }
  249. handleEol();
  250. followsWhitespace = true;
  251. state = "out";
  252. }
  253. continue stateLoop;
  254. case "multiLineComment":
  255. if (char === "*") state = "multiLineCommentStar";
  256. else if (objHasOwnProperty.call(eolSet, char)) handleEol();
  257. continue stateLoop;
  258. case "multiLineCommentStar":
  259. if (char === "/") {
  260. followsWhitespace = true;
  261. state = "out";
  262. if (commentDatum) {
  263. commentDatum.endPoint = index + 1;
  264. results.push(commentDatum);
  265. commentDatum = null;
  266. }
  267. } else if (char !== "*") {
  268. if (objHasOwnProperty.call(eolSet, char)) handleEol();
  269. state = "multiLineComment";
  270. }
  271. continue stateLoop;
  272. case "string":
  273. if (char === "\\") state = "stringEscape";
  274. else if (char === quote) state = "out";
  275. break;
  276. case "stringEscape":
  277. if (objHasOwnProperty.call(eolSet, char)) handleEol();
  278. state = "string";
  279. break;
  280. case "template":
  281. if (char === "$") state = "templateDollar";
  282. else if (char === "\\") state = "templateEscape";
  283. else if (char === "`") state = "out";
  284. else if (objHasOwnProperty.call(eolSet, char)) handleEol();
  285. break;
  286. case "templateEscape":
  287. if (objHasOwnProperty.call(eolSet, char)) handleEol();
  288. state = "template";
  289. break;
  290. case "templateDollar":
  291. if (char === "{") {
  292. templateContext.push(++scopeDepth);
  293. state = "out";
  294. } else if (char !== "$") {
  295. if (objHasOwnProperty.call(eolSet, char)) handleEol();
  296. state = "template";
  297. }
  298. break;
  299. case "regexp":
  300. if (char === "\\") state = "regexpEscape";
  301. else if (char === "/") state = "out";
  302. break;
  303. case "regexpEscape":
  304. state = "regexp";
  305. break;
  306. /* istanbul ignore next */
  307. default:
  308. throw new Error("Unexpected state " + state);
  309. }
  310. previousToken = null;
  311. followsWhitespace = false;
  312. }
  313. return results;
  314. };