aMarCruz/jscc

View on GitHub
src/get-expr.ts

Summary

Maintainability
A
0 mins
Test Coverage
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