src/get-expr.ts
import skipRegex = require('skip-regex')
import R = require('./regexes')
/** Flag for ES6 TL in the stack */
const ES6_BQ = '`'
/**
* Searches the next backtick that signals the end of the ES6 Template Literal
* or the sequence "${" that starts a sub-expression, skipping any escaped
* character.
*
* @param buffer Whole code
* @param start Starting position of the template
* @param stack To save nested ES6 TL positions
* @returns The end of the string (-1 if not found).
*/
const skipES6TL = (buffer: string, start: number, stack: string[]) => {
// Only three characters are of interest to this function
const re = /[`$\\]/g
// `start` points to the a backtick inside `code`
re.lastIndex = start + 1
while (re.exec(buffer)) {
const pos = re.lastIndex
const c = buffer[pos - 1]
if (c === ES6_BQ) {
return pos // found the end of this TL
}
/*
If a sub-expression is found, push a backtick in the stack.
When the calling loop finds a closing brace and see the backtick,
it will restore the ES6 TL parsing mode.
*/
if (c === '$' && buffer[pos] === '{') {
stack.push(ES6_BQ)
return pos + 1
}
// This is an escaped char, skip it
re.lastIndex = pos + 1
}
return buffer.length // let JS VM handles this error
}
/**
* Handles closing brackets. It can be a regular bracket or one closing an
* ES6 TL expression.
*
* @param expr Raw expression
* @param start Position of this bracket
* @param stack Brackets stack
*/
const skipBracket = (expr: string, start: number, stack: string[]) => {
const ch = stack.pop()
if (ch === '`') {
return skipES6TL(expr, start, stack)
}
// If ch==null then there's an error, returns expr.length to
// let JS VM handles this.
return ch ? start + 1 : expr.length
}
/**
* To find the comment (//), it is necessary to skip strings, es6 tl,
* brackets, and regexes
*/
const RE_EXPR = RegExp(R.S_STRINGS + '|[`/{}]', 'g')
/**
* Skip ES6 TL in expressions.
*
* @param expr Execution context
* @param start Start of the ES6 TL
*/
const extractExpr = function (expr: string, start: number) {
const stack: string[] = []
const re = new RegExp(RE_EXPR)
re.lastIndex = start
let mm
// tslint:disable-next-line:no-conditional-assignment
while (mm = re.exec(expr)) {
switch (mm[0]) {
case '{':
stack.push('}')
break
case '}':
re.lastIndex = skipBracket(expr, mm.index, stack)
break
case '`':
re.lastIndex = skipES6TL(expr, mm.index, stack)
break
case '/':
if (expr[mm.index + 1] === '/') {
return expr.slice(0, mm.index)
}
re.lastIndex = skipRegex(expr, mm.index)
}
}
return expr
}
/**
* Get an expression, removing surrounding whitespace and the trailing comment,
* if necessary.
*
* @param key Keyword for this expression
* @param expr Raw expression
*/
const getExpr = function (key: string, expr: string) {
if (expr.indexOf('/') < 0) {
return expr.trim()
}
/*
When an assignment has a regex (ex: `#set _R /\s/`), skipRegex will not
recognize it due to invalid syntax. Inserting the missing '=' solves this.
*/
if (key === 'set') {
const mm = R.ASSIGNMENT.exec(expr)!
const ss = mm && mm[2]
// beware of something like `//#set _V //cmnt`
// istanbul ignore else
if (ss) {
expr = ss.startsWith('//') ? mm[1] : `${mm[1]}=${ss}`
}
}
return extractExpr(expr, 0).trim()
}
export = getExpr