1'use strict';
2
3var objIsRegex = require('is-regex');
4
5exports = (module.exports = parse);
6
7var TOKEN_TYPES = exports.TOKEN_TYPES = {
8  LINE_COMMENT: '//',
9  BLOCK_COMMENT: '/**/',
10  SINGLE_QUOTE: '\'',
11  DOUBLE_QUOTE: '"',
12  TEMPLATE_QUOTE: '`',
13  REGEXP: '//g'
14}
15
16var BRACKETS = exports.BRACKETS = {
17  '(': ')',
18  '{': '}',
19  '[': ']'
20};
21var BRACKETS_REVERSED = {
22  ')': '(',
23  '}': '{',
24  ']': '['
25};
26
27exports.parse = parse;
28function parse(src, state, options) {
29  options = options || {};
30  state = state || exports.defaultState();
31  var start = options.start || 0;
32  var end = options.end || src.length;
33  var index = start;
34  while (index < end) {
35    try {
36      parseChar(src[index], state);
37    } catch (ex) {
38      ex.index = index;
39      throw ex;
40    }
41    index++;
42  }
43  return state;
44}
45
46exports.parseUntil = parseUntil;
47function parseUntil(src, delimiter, options) {
48  options = options || {};
49  var start = options.start || 0;
50  var index = start;
51  var state = exports.defaultState();
52  while (index < src.length) {
53    if ((options.ignoreNesting || !state.isNesting(options)) && matches(src, delimiter, index)) {
54      var end = index;
55      return {
56        start: start,
57        end: end,
58        src: src.substring(start, end)
59      };
60    }
61    try {
62      parseChar(src[index], state);
63    } catch (ex) {
64      ex.index = index;
65      throw ex;
66    }
67    index++;
68  }
69  var err = new Error('The end of the string was reached with no closing bracket found.');
70  err.code = 'CHARACTER_PARSER:END_OF_STRING_REACHED';
71  err.index = index;
72  throw err;
73}
74
75exports.parseChar = parseChar;
76function parseChar(character, state) {
77  if (character.length !== 1) {
78    var err = new Error('Character must be a string of length 1');
79    err.name = 'InvalidArgumentError';
80    err.code = 'CHARACTER_PARSER:CHAR_LENGTH_NOT_ONE';
81    throw err;
82  }
83  state = state || exports.defaultState();
84  state.src += character;
85  var wasComment = state.isComment();
86  var lastChar = state.history ? state.history[0] : '';
87
88
89  if (state.regexpStart) {
90    if (character === '/' || character == '*') {
91      state.stack.pop();
92    }
93    state.regexpStart = false;
94  }
95  switch (state.current()) {
96    case TOKEN_TYPES.LINE_COMMENT:
97      if (character === '\n') {
98        state.stack.pop();
99      }
100      break;
101    case TOKEN_TYPES.BLOCK_COMMENT:
102      if (state.lastChar === '*' && character === '/') {
103        state.stack.pop();
104      }
105      break;
106    case TOKEN_TYPES.SINGLE_QUOTE:
107      if (character === '\'' && !state.escaped) {
108        state.stack.pop();
109      } else if (character === '\\' && !state.escaped) {
110        state.escaped = true;
111      } else {
112        state.escaped = false;
113      }
114      break;
115    case TOKEN_TYPES.DOUBLE_QUOTE:
116      if (character === '"' && !state.escaped) {
117        state.stack.pop();
118      } else if (character === '\\' && !state.escaped) {
119        state.escaped = true;
120      } else {
121        state.escaped = false;
122      }
123      break;
124    case TOKEN_TYPES.TEMPLATE_QUOTE:
125      if (character === '`' && !state.escaped) {
126        state.stack.pop();
127        state.hasDollar = false;
128      } else if (character === '\\' && !state.escaped) {
129        state.escaped = true;
130        state.hasDollar = false;
131      } else if (character === '$' && !state.escaped) {
132        state.hasDollar = true;
133      } else if (character === '{' && state.hasDollar) {
134        state.stack.push(BRACKETS[character]);
135      } else {
136        state.escaped = false;
137        state.hasDollar = false;
138      }
139      break;
140    case TOKEN_TYPES.REGEXP:
141      if (character === '/' && !state.escaped) {
142        state.stack.pop();
143      } else if (character === '\\' && !state.escaped) {
144        state.escaped = true;
145      } else {
146        state.escaped = false;
147      }
148      break;
149    default:
150      if (character in BRACKETS) {
151        state.stack.push(BRACKETS[character]);
152      } else if (character in BRACKETS_REVERSED) {
153        if (state.current() !== character) {
154          var err = new SyntaxError('Mismatched Bracket: ' + character);
155          err.code = 'CHARACTER_PARSER:MISMATCHED_BRACKET';
156          throw err;
157        };
158        state.stack.pop();
159      } else if (lastChar === '/' && character === '/') {
160        // Don't include comments in history
161        state.history = state.history.substr(1);
162        state.stack.push(TOKEN_TYPES.LINE_COMMENT);
163      } else if (lastChar === '/' && character === '*') {
164        // Don't include comment in history
165        state.history = state.history.substr(1);
166        state.stack.push(TOKEN_TYPES.BLOCK_COMMENT);
167      } else if (character === '/' && isRegexp(state.history)) {
168        state.stack.push(TOKEN_TYPES.REGEXP);
169        // N.B. if the next character turns out to be a `*` or a `/`
170        //      then this isn't actually a regexp
171        state.regexpStart = true;
172      } else if (character === '\'') {
173        state.stack.push(TOKEN_TYPES.SINGLE_QUOTE);
174      } else if (character === '"') {
175        state.stack.push(TOKEN_TYPES.DOUBLE_QUOTE);
176      } else if (character === '`') {
177        state.stack.push(TOKEN_TYPES.TEMPLATE_QUOTE);
178      }
179      break;
180  }
181  if (!state.isComment() && !wasComment) {
182    state.history = character + state.history;
183  }
184  state.lastChar = character; // store last character for ending block comments
185  return state;
186}
187
188exports.defaultState = function () { return new State() };
189function State() {
190  this.stack = [];
191
192  this.regexpStart = false;
193  this.escaped = false;
194  this.hasDollar = false;
195
196  this.src = '';
197  this.history = ''
198  this.lastChar = ''
199}
200State.prototype.current = function () {
201  return this.stack[this.stack.length - 1];
202};
203State.prototype.isString = function () {
204  return (
205    this.current() === TOKEN_TYPES.SINGLE_QUOTE ||
206    this.current() === TOKEN_TYPES.DOUBLE_QUOTE ||
207    this.current() === TOKEN_TYPES.TEMPLATE_QUOTE
208  );
209}
210State.prototype.isComment = function () {
211  return this.current() === TOKEN_TYPES.LINE_COMMENT || this.current() === TOKEN_TYPES.BLOCK_COMMENT;
212}
213State.prototype.isNesting = function (opts) {
214  if (
215    opts && opts.ignoreLineComment &&
216    this.stack.length === 1 && this.stack[0] === TOKEN_TYPES.LINE_COMMENT
217  ) {
218    // if we are only inside a line comment, and line comments are ignored
219    // don't count it as nesting
220    return false;
221  }
222  return !!this.stack.length;
223}
224
225function matches(str, matcher, i) {
226  if (objIsRegex(matcher)) {
227    return matcher.test(str.substr(i || 0));
228  } else {
229    return str.substr(i || 0, matcher.length) === matcher;
230  }
231}
232
233exports.isPunctuator = isPunctuator
234function isPunctuator(c) {
235  if (!c) return true; // the start of a string is a punctuator
236  var code = c.charCodeAt(0)
237
238  switch (code) {
239    case 46:   // . dot
240    case 40:   // ( open bracket
241    case 41:   // ) close bracket
242    case 59:   // ; semicolon
243    case 44:   // , comma
244    case 123:  // { open curly brace
245    case 125:  // } close curly brace
246    case 91:   // [
247    case 93:   // ]
248    case 58:   // :
249    case 63:   // ?
250    case 126:  // ~
251    case 37:   // %
252    case 38:   // &
253    case 42:   // *:
254    case 43:   // +
255    case 45:   // -
256    case 47:   // /
257    case 60:   // <
258    case 62:   // >
259    case 94:   // ^
260    case 124:  // |
261    case 33:   // !
262    case 61:   // =
263      return true;
264    default:
265      return false;
266  }
267}
268
269exports.isKeyword = isKeyword
270function isKeyword(id) {
271  return (id === 'if') || (id === 'in') || (id === 'do') || (id === 'var') || (id === 'for') || (id === 'new') ||
272         (id === 'try') || (id === 'let') || (id === 'this') || (id === 'else') || (id === 'case') ||
273         (id === 'void') || (id === 'with') || (id === 'enum') || (id === 'while') || (id === 'break') || (id === 'catch') ||
274         (id === 'throw') || (id === 'const') || (id === 'yield') || (id === 'class') || (id === 'super') ||
275         (id === 'return') || (id === 'typeof') || (id === 'delete') || (id === 'switch') || (id === 'export') ||
276         (id === 'import') || (id === 'default') || (id === 'finally') || (id === 'extends') || (id === 'function') ||
277         (id === 'continue') || (id === 'debugger') || (id === 'package') || (id === 'private') || (id === 'interface') ||
278         (id === 'instanceof') || (id === 'implements') || (id === 'protected') || (id === 'public') || (id === 'static');
279}
280
281function isRegexp(history) {
282  //could be start of regexp or divide sign
283
284  history = history.replace(/^\s*/, '');
285
286  //unless its an `if`, `while`, `for` or `with` it's a divide, so we assume it's a divide
287  if (history[0] === ')') return false;
288  //unless it's a function expression, it's a regexp, so we assume it's a regexp
289  if (history[0] === '}') return true;
290  //any punctuation means it's a regexp
291  if (isPunctuator(history[0])) return true;
292  //if the last thing was a keyword then it must be a regexp (e.g. `typeof /foo/`)
293  if (/^\w+\b/.test(history) && isKeyword(/^\w+\b/.exec(history)[0].split('').reverse().join(''))) return true;
294
295  return false;
296}
297