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