You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
296 lines
8.8 KiB
296 lines
8.8 KiB
'use strict'; |
|
|
|
var objIsRegex = require('is-regex'); |
|
|
|
exports = (module.exports = parse); |
|
|
|
var TOKEN_TYPES = exports.TOKEN_TYPES = { |
|
LINE_COMMENT: '//', |
|
BLOCK_COMMENT: '/**/', |
|
SINGLE_QUOTE: '\'', |
|
DOUBLE_QUOTE: '"', |
|
TEMPLATE_QUOTE: '`', |
|
REGEXP: '//g' |
|
} |
|
|
|
var BRACKETS = exports.BRACKETS = { |
|
'(': ')', |
|
'{': '}', |
|
'[': ']' |
|
}; |
|
var BRACKETS_REVERSED = { |
|
')': '(', |
|
'}': '{', |
|
']': '[' |
|
}; |
|
|
|
exports.parse = parse; |
|
function parse(src, state, options) { |
|
options = options || {}; |
|
state = state || exports.defaultState(); |
|
var start = options.start || 0; |
|
var end = options.end || src.length; |
|
var index = start; |
|
while (index < end) { |
|
try { |
|
parseChar(src[index], state); |
|
} catch (ex) { |
|
ex.index = index; |
|
throw ex; |
|
} |
|
index++; |
|
} |
|
return state; |
|
} |
|
|
|
exports.parseUntil = parseUntil; |
|
function parseUntil(src, delimiter, options) { |
|
options = options || {}; |
|
var start = options.start || 0; |
|
var index = start; |
|
var state = exports.defaultState(); |
|
while (index < src.length) { |
|
if ((options.ignoreNesting || !state.isNesting(options)) && matches(src, delimiter, index)) { |
|
var end = index; |
|
return { |
|
start: start, |
|
end: end, |
|
src: src.substring(start, end) |
|
}; |
|
} |
|
try { |
|
parseChar(src[index], state); |
|
} catch (ex) { |
|
ex.index = index; |
|
throw ex; |
|
} |
|
index++; |
|
} |
|
var err = new Error('The end of the string was reached with no closing bracket found.'); |
|
err.code = 'CHARACTER_PARSER:END_OF_STRING_REACHED'; |
|
err.index = index; |
|
throw err; |
|
} |
|
|
|
exports.parseChar = parseChar; |
|
function parseChar(character, state) { |
|
if (character.length !== 1) { |
|
var err = new Error('Character must be a string of length 1'); |
|
err.name = 'InvalidArgumentError'; |
|
err.code = 'CHARACTER_PARSER:CHAR_LENGTH_NOT_ONE'; |
|
throw err; |
|
} |
|
state = state || exports.defaultState(); |
|
state.src += character; |
|
var wasComment = state.isComment(); |
|
var lastChar = state.history ? state.history[0] : ''; |
|
|
|
|
|
if (state.regexpStart) { |
|
if (character === '/' || character == '*') { |
|
state.stack.pop(); |
|
} |
|
state.regexpStart = false; |
|
} |
|
switch (state.current()) { |
|
case TOKEN_TYPES.LINE_COMMENT: |
|
if (character === '\n') { |
|
state.stack.pop(); |
|
} |
|
break; |
|
case TOKEN_TYPES.BLOCK_COMMENT: |
|
if (state.lastChar === '*' && character === '/') { |
|
state.stack.pop(); |
|
} |
|
break; |
|
case TOKEN_TYPES.SINGLE_QUOTE: |
|
if (character === '\'' && !state.escaped) { |
|
state.stack.pop(); |
|
} else if (character === '\\' && !state.escaped) { |
|
state.escaped = true; |
|
} else { |
|
state.escaped = false; |
|
} |
|
break; |
|
case TOKEN_TYPES.DOUBLE_QUOTE: |
|
if (character === '"' && !state.escaped) { |
|
state.stack.pop(); |
|
} else if (character === '\\' && !state.escaped) { |
|
state.escaped = true; |
|
} else { |
|
state.escaped = false; |
|
} |
|
break; |
|
case TOKEN_TYPES.TEMPLATE_QUOTE: |
|
if (character === '`' && !state.escaped) { |
|
state.stack.pop(); |
|
state.hasDollar = false; |
|
} else if (character === '\\' && !state.escaped) { |
|
state.escaped = true; |
|
state.hasDollar = false; |
|
} else if (character === '$' && !state.escaped) { |
|
state.hasDollar = true; |
|
} else if (character === '{' && state.hasDollar) { |
|
state.stack.push(BRACKETS[character]); |
|
} else { |
|
state.escaped = false; |
|
state.hasDollar = false; |
|
} |
|
break; |
|
case TOKEN_TYPES.REGEXP: |
|
if (character === '/' && !state.escaped) { |
|
state.stack.pop(); |
|
} else if (character === '\\' && !state.escaped) { |
|
state.escaped = true; |
|
} else { |
|
state.escaped = false; |
|
} |
|
break; |
|
default: |
|
if (character in BRACKETS) { |
|
state.stack.push(BRACKETS[character]); |
|
} else if (character in BRACKETS_REVERSED) { |
|
if (state.current() !== character) { |
|
var err = new SyntaxError('Mismatched Bracket: ' + character); |
|
err.code = 'CHARACTER_PARSER:MISMATCHED_BRACKET'; |
|
throw err; |
|
}; |
|
state.stack.pop(); |
|
} else if (lastChar === '/' && character === '/') { |
|
// Don't include comments in history |
|
state.history = state.history.substr(1); |
|
state.stack.push(TOKEN_TYPES.LINE_COMMENT); |
|
} else if (lastChar === '/' && character === '*') { |
|
// Don't include comment in history |
|
state.history = state.history.substr(1); |
|
state.stack.push(TOKEN_TYPES.BLOCK_COMMENT); |
|
} else if (character === '/' && isRegexp(state.history)) { |
|
state.stack.push(TOKEN_TYPES.REGEXP); |
|
// N.B. if the next character turns out to be a `*` or a `/` |
|
// then this isn't actually a regexp |
|
state.regexpStart = true; |
|
} else if (character === '\'') { |
|
state.stack.push(TOKEN_TYPES.SINGLE_QUOTE); |
|
} else if (character === '"') { |
|
state.stack.push(TOKEN_TYPES.DOUBLE_QUOTE); |
|
} else if (character === '`') { |
|
state.stack.push(TOKEN_TYPES.TEMPLATE_QUOTE); |
|
} |
|
break; |
|
} |
|
if (!state.isComment() && !wasComment) { |
|
state.history = character + state.history; |
|
} |
|
state.lastChar = character; // store last character for ending block comments |
|
return state; |
|
} |
|
|
|
exports.defaultState = function () { return new State() }; |
|
function State() { |
|
this.stack = []; |
|
|
|
this.regexpStart = false; |
|
this.escaped = false; |
|
this.hasDollar = false; |
|
|
|
this.src = ''; |
|
this.history = '' |
|
this.lastChar = '' |
|
} |
|
State.prototype.current = function () { |
|
return this.stack[this.stack.length - 1]; |
|
}; |
|
State.prototype.isString = function () { |
|
return ( |
|
this.current() === TOKEN_TYPES.SINGLE_QUOTE || |
|
this.current() === TOKEN_TYPES.DOUBLE_QUOTE || |
|
this.current() === TOKEN_TYPES.TEMPLATE_QUOTE |
|
); |
|
} |
|
State.prototype.isComment = function () { |
|
return this.current() === TOKEN_TYPES.LINE_COMMENT || this.current() === TOKEN_TYPES.BLOCK_COMMENT; |
|
} |
|
State.prototype.isNesting = function (opts) { |
|
if ( |
|
opts && opts.ignoreLineComment && |
|
this.stack.length === 1 && this.stack[0] === TOKEN_TYPES.LINE_COMMENT |
|
) { |
|
// if we are only inside a line comment, and line comments are ignored |
|
// don't count it as nesting |
|
return false; |
|
} |
|
return !!this.stack.length; |
|
} |
|
|
|
function matches(str, matcher, i) { |
|
if (objIsRegex(matcher)) { |
|
return matcher.test(str.substr(i || 0)); |
|
} else { |
|
return str.substr(i || 0, matcher.length) === matcher; |
|
} |
|
} |
|
|
|
exports.isPunctuator = isPunctuator |
|
function isPunctuator(c) { |
|
if (!c) return true; // the start of a string is a punctuator |
|
var code = c.charCodeAt(0) |
|
|
|
switch (code) { |
|
case 46: // . dot |
|
case 40: // ( open bracket |
|
case 41: // ) close bracket |
|
case 59: // ; semicolon |
|
case 44: // , comma |
|
case 123: // { open curly brace |
|
case 125: // } close curly brace |
|
case 91: // [ |
|
case 93: // ] |
|
case 58: // : |
|
case 63: // ? |
|
case 126: // ~ |
|
case 37: // % |
|
case 38: // & |
|
case 42: // *: |
|
case 43: // + |
|
case 45: // - |
|
case 47: // / |
|
case 60: // < |
|
case 62: // > |
|
case 94: // ^ |
|
case 124: // | |
|
case 33: // ! |
|
case 61: // = |
|
return true; |
|
default: |
|
return false; |
|
} |
|
} |
|
|
|
exports.isKeyword = isKeyword |
|
function isKeyword(id) { |
|
return (id === 'if') || (id === 'in') || (id === 'do') || (id === 'var') || (id === 'for') || (id === 'new') || |
|
(id === 'try') || (id === 'let') || (id === 'this') || (id === 'else') || (id === 'case') || |
|
(id === 'void') || (id === 'with') || (id === 'enum') || (id === 'while') || (id === 'break') || (id === 'catch') || |
|
(id === 'throw') || (id === 'const') || (id === 'yield') || (id === 'class') || (id === 'super') || |
|
(id === 'return') || (id === 'typeof') || (id === 'delete') || (id === 'switch') || (id === 'export') || |
|
(id === 'import') || (id === 'default') || (id === 'finally') || (id === 'extends') || (id === 'function') || |
|
(id === 'continue') || (id === 'debugger') || (id === 'package') || (id === 'private') || (id === 'interface') || |
|
(id === 'instanceof') || (id === 'implements') || (id === 'protected') || (id === 'public') || (id === 'static'); |
|
} |
|
|
|
function isRegexp(history) { |
|
//could be start of regexp or divide sign |
|
|
|
history = history.replace(/^\s*/, ''); |
|
|
|
//unless its an `if`, `while`, `for` or `with` it's a divide, so we assume it's a divide |
|
if (history[0] === ')') return false; |
|
//unless it's a function expression, it's a regexp, so we assume it's a regexp |
|
if (history[0] === '}') return true; |
|
//any punctuation means it's a regexp |
|
if (isPunctuator(history[0])) return true; |
|
//if the last thing was a keyword then it must be a regexp (e.g. `typeof /foo/`) |
|
if (/^\w+\b/.test(history) && isKeyword(/^\w+\b/.exec(history)[0].split('').reverse().join(''))) return true; |
|
|
|
return false; |
|
}
|
|
|