src/brackets.js

Summary

Maintainability
D
3 days
Test Coverage
/**
 * riot.util.brackets
 *
 * - `brackets    ` - Returns a string or regex based on its parameter
 * - `brackets.set` - Change the current riot brackets
 *
 * @module
 */
//#if 0 // only in the unprocessed source
/* eslint no-unused-vars: [2, {args: "after-used", varsIgnorePattern: "^brackets$"}] */
/* global skipRegex */
//#endif

//#if ES6
/* global riot */

export
//#endif
var brackets = (function (UNDEF) {
  //
  // Closure data
  // --------------------------------------------------------------------------
  //
  //#set $_RIX_TEST  = 4
  //#set $_RIX_ESC   = 5
  //#set $_RIX_OPEN  = 6
  //#set $_RIX_CLOSE = 7
  //#set $_RIX_PAIR  = 8
  //#set $_RIX_LOOP  = 9
  //#ifndef $_RIX_TEST
  var
    $_RIX_TEST  = 4,  // DONT'T FORGET SYNC THE #set BLOCK!!!
    $_RIX_ESC   = 5,
    $_RIX_OPEN  = 6,
    $_RIX_CLOSE = 7,
    $_RIX_PAIR  = 8,
    $_RIX_LOOP  = 9
  //#endif

  var
    REGLOB = 'g',

    /**
     * Used by internal functions and shared with the riot compiler, matches valid,
     * multiline JavaScript comments in almost all its forms. Can handle embedded
     * sequences `/\*`, `*\/` and `//` inside these. Skips non-valid comments like `/*\/`
     *
     * `R_MLCOMMS` does not make captures.
     * @const {RegExp}
     * @static
     */
    R_MLCOMMS = /\/\*[^*]*\*+(?:[^*\/][^*]*\*+)*\//g,

    /**
     * Used by internal functions and shared with the riot compiler, matches single or
     * double quoted strings, handles embedded quotes and multi-line strings (not in
     * accordance with the JavaScript spec).
     * It is not for ES6 template strings.
     *
     * `R_STRINGS` does not make captures.
     * @const {RegExp}
     * @static
     */
    R_STRINGS = /"[^"\\]*(?:\\[\S\s][^"\\]*)*"|'[^'\\]*(?:\\[\S\s][^'\\]*)*'|`[^`\\]*(?:\\[\S\s][^`\\]*)*`/g,

    /**
     * For use with the RegExp constructor. Combines the
     * {@link module:brackets.R_STRINGS|R_STRINGS} source with sources of regexes matching
     * division operators and literal regexes.
     * The resulting regex captures in `$1` and `$2` a single slash, depending
     * if it matches a division operator ($1) or a literal regex ($2).
     * @const {string}
     * @static
     */
    S_QBLOCKS = R_STRINGS.source + '|' +
      /(?:\breturn\s+|(?:[$\w\)\]]|\+\+|--)\s*(\/)(?![*\/]))/.source + '|' +
      /\/(?=[^*\/])[^[\/\\]*(?:(?:\[(?:\\.|[^\]\\]*)*\]|\\.)[^[\/\\]*)*?([^<]\/)[gim]*/.source,

    /**
     * Characters not supported by the expression parser.
     * @const {RegExp}
     * @static
     */
    UNSUPPORTED = RegExp('[\\' + 'x00-\\x1F<>a-zA-Z0-9\'",;\\\\]'),

    /**
     * These characters have to be escaped - Note that '{}' is not in this list.
     * @const {RegExp}
     * @static
     */
    NEED_ESCAPE = /(?=[[\]()*+?.^$|])/g,

    /*
      JS/ES6 quoted strings and start of regex (basic ES6 does not supports nested backquotes).
    */
    S_QBLOCK2 = R_STRINGS.source + '|' + /(\/)(?![*\/])/.source,

    /**
     * Hash of regexes for matching JavaScript brackets out of quoted strings and literal
     * regexes. Used by {@link module:brackets.split|split}, these are heavy, but their
     * performance is acceptable.
     * @const {object}
     * @private
     */
    FINDBRACES = {
      '(': RegExp('([()])|'   + S_QBLOCK2, REGLOB),
      '[': RegExp('([[\\]])|' + S_QBLOCK2, REGLOB),
      '{': RegExp('([{}])|'   + S_QBLOCK2, REGLOB)
    },

    /**
     * The predefined riot brackets
     * @const {string}
     * @default
     */
    DEFAULT = '{ }'

  // pre-made string and regexes for the default brackets
  var _pairs = [
    '{', '}',                   // [0-1]: separated brackets
    '{', '}',                   // [2-3]: separated brackets (escaped)
    /{[^}]*}/,                  // $_RIX_TEST
    /\\([{}])/g,                // $_RIX_ESC
    /\\({)|{/g,                 // $_RIX_OPEN
    RegExp('\\\\(})|([[({])|(})|' + S_QBLOCK2, REGLOB),       // $_RIX_CLOSE
    DEFAULT,                    // $_RIX_PAIR
    /^\s*{\^?\s*([$\w]+)(?:\s*,\s*(\S+))?\s+in\s+(\S.*)\s*}/, // $_RIX_LOOP
    /(^|[^\\]){=[\S\s]*?}/      // $_RIX_RAW
  ]

  // Variable information about the current brackets state, initialized on first use
  // and on bracket changes.
  var
    cachedBrackets = UNDEF,     // full brackets string in use, for change detection
    _regex,                     // function for regex convertion of default brackets
    _cache = [],                // pre-made string and regexes for the current brackets
    _settings                   // mirror `riot.settings`

  //
  // Private functions
  // --------------------------------------------------------------------------

  /**
   * Rewrite function for default brackets, returns the received regex as-is.
   * Used by the main brackets function when the current brackets are the default.
   * @param   {RegExp} re RegExp instance with the default riot brackets
   * @returns {RegExp}    The received regex.
   * @private
   */
  function _loopback (re) { return re }

  /**
   * Rewrite the regex with the default brackets replaced with the custom ones.
   * Used by the main brackets function when the current brackets are not the default.
   * @param   {RegExp} re  - RegExp instance with the default riot brackets
   * @param   {Array} [bp] - Escaped brackets in elements 2-3, defaults to those in _cache
   * @returns {RegExp} Copy of the received regex, with the default brackets replaced.
   * @private
   */
  function _rewrite (re, bp) {
    if (!bp) bp = _cache
    return new RegExp(
      re.source.replace(/{/g, bp[2]).replace(/}/g, bp[3]), re.global ? REGLOB : ''
    )
  }

  /**
   * Creates an array with pre-made strings and regexes based on the received brackets.
   * With the default brackets, returns a reference to an inner static array.
   *
   * Does not accept `<, >, a-z, A-Z, 0-9` nor control characters.
   * @param   {string} pair - String with the desired brackets pair (cannot be falsy)
   * @returns {Array}  Array with information for the given brackets.
   * @throws  Will throw an "Unsupported brackets ..." if _pair_ is not separated with
   *   exactly one space, or contains an invalid character.
   * @private
   */
  function _create (pair) {
    if (pair === DEFAULT) return _pairs

    var arr = pair.split(' ')

    if (arr.length !== 2 || UNSUPPORTED.test(pair)) {
      throw new Error('Unsupported brackets "' + pair + '"')
    }
    arr = arr.concat(pair.replace(NEED_ESCAPE, '\\').split(' '))

    arr[$_RIX_TEST] = _rewrite(arr[1].length > 1 ? /{[\S\s]*?}/ : _pairs[$_RIX_TEST], arr)
    arr[$_RIX_ESC] = _rewrite(pair.length > 3 ? /\\({|})/g : _pairs[$_RIX_ESC], arr)
    arr[$_RIX_OPEN] = _rewrite(_pairs[$_RIX_OPEN], arr) // for _split()
    arr[$_RIX_CLOSE] = RegExp('\\\\(' + arr[3] + ')|([[({])|(' + arr[3] + ')|' + S_QBLOCK2, REGLOB)
    arr[$_RIX_PAIR] = pair
    return arr
  }

  //
  // "Exported" functions
  // --------------------------------------------------------------------------

  /**
   * The main function.
   *
   * With a numeric parameter, returns the current left (0) or right (1) riot brackets.
   *
   * With a regex, returns the original regex if the current brackets are the defaults,
   * or a new one with the default brackets replaced by the current custom brackets.
   * @param   {RegExp|number} reOrIdx - As noted above
   * @returns {RegExp|string} Based on the received parameter.
   * @alias   brackets
   */
  function _brackets (reOrIdx) {
    return reOrIdx instanceof RegExp ? _regex(reOrIdx) : _cache[reOrIdx]
  }

  /**
   * Splits the received string in its template text and expression parts using
   * balanced brackets detection to avoid require escaped brackets from users.
   *
   * _For internal use by tmpl and the riot-compiler._
   * @param   {string} str    - Template source to split, can be one expression
   * @param   {number} [tmpl] - 1 if called from `tmpl()`
   * @param   {Array}  [_bp]  - Info of custom brackets to use
   * @returns {Array} - Array containing template text and expressions.
   *   If str was one unique expression, returns two elements: ["", expression].
   * @private
   */
  _brackets.split = function split (str, tmpl, _bp) {
    // istanbul ignore next: _bp is for the compiler
    if (!_bp) _bp = _cache

    // Template text is easy: closing brackets are ignored, all we have to do is find
    // the first unescaped bracket. The real work is with the expressions...
    //
    // Expressions are not so easy. We can already ignore opening brackets, but finding
    // the correct closing bracket is tricky.
    // Strings and regexes can contain almost any combination of characters and we
    // can't deal with these complexity with our regexes, so let's hide and ignore
    // these. From there, all we need is to detect the bracketed parts and skip
    // them, as they contains most of the common characters used by riot brackets.
    // With that, we have a 90% reliability in the detection, although (hope few) some
    // custom brackets still requires to be escaped.
    var
      parts = [],                 // holds the resulting parts
      match,                      // reused by both outer and nested searches
      isexpr,                     // we are in ttext (0) or expression (1)
      start,                      // start position of current template or expression
      pos,                        // current position (exec() result)
      re = _bp[$_RIX_OPEN]        // start with *updated* opening bracket

    var qblocks = []              // quoted strings and regexes
    var prevStr = ''
    var mark, lastIndex

    isexpr = start = re.lastIndex = 0       // re is reused, we must reset lastIndex

    while ((match = re.exec(str))) {

      lastIndex = re.lastIndex
      pos = match.index

      if (isexpr) {
        // $1: optional escape character,
        // $2: opening js bracket `{[(`,
        // $3: closing riot bracket,
        // $4: opening slashes of regex

        if (match[2]) {                     // if have a javascript opening bracket,
          //re.lastIndex = skipBraces(str, match[2], re.lastIndex)
          var ch = match[2]
          var rech = FINDBRACES[ch]
          var ix = 1

          rech.lastIndex = lastIndex
          while ((match = rech.exec(str))) {
            if (match[1]) {
              if (match[1] === ch) ++ix
              else if (!--ix) break
            } else {
              rech.lastIndex = pushQBlock(match.index, rech.lastIndex, match[2])
            }
          }
          re.lastIndex = ix ? str.length : rech.lastIndex
          continue                          // skip the bracketed block and loop
        }

        if (!match[3]) {                    // if don't have a closing bracket
          re.lastIndex = pushQBlock(pos, lastIndex, match[4])
          continue                          // search again
        }
      }

      // At this point, we expect an _unescaped_ openning bracket in $2 for text,
      // or a closing bracket in $3 for expression.

      if (!match[1]) {                      // ignore it if have an escape char
        unescapeStr(str.slice(start, pos))  // push part, even if empty
        start = re.lastIndex                // next position is the new start
        re = _bp[$_RIX_OPEN + (isexpr ^= 1)] // switch mode and swap regexp
        re.lastIndex = start                // update the regex pointer
      }
    }

    if (str && start < str.length) {        // push remaining part, if we have one
      unescapeStr(str.slice(start))
    }

    // send the literal strings as an array property
    parts.qblocks = qblocks

    return parts

    // Inner Helpers for _split() -----

    // Store the string in the array `parts`.
    // Unescape escaped brackets from expressions and, if we are called from
    // tmpl, from the HTML part too.
    function unescapeStr (s) {
      if (prevStr) {
        s = prevStr + s
        prevStr = ''
      }
      if (tmpl || isexpr) {
        parts.push(s && s.replace(_bp[$_RIX_ESC], '$1'))
      } else {
        parts.push(s)
      }
    }

    // Find the js closing bracket for the current block.
    // Skips strings, regexes, and other inner blocks.
    function pushQBlock(_pos, _lastIndex, slash) { //eslint-disable-line
      if (slash) {
        _lastIndex = skipRegex(str, _pos)
      }
      // do not save empty strings or non-regex slashes
      if (tmpl && _lastIndex > _pos + 2) {
        mark = '\u2057' + qblocks.length + '~'
        qblocks.push(str.slice(_pos, _lastIndex))
        prevStr += str.slice(start, _pos) + mark
        start = _lastIndex
      }
      return _lastIndex
    }
  }

  // exposed by riot.util.tmpl.hasExpr
  _brackets.hasExpr = function hasExpr (str) {
    return _cache[$_RIX_TEST].test(str)
  }

  // exposed by riot.util.tmpl.loopKeys
  _brackets.loopKeys = function loopKeys (expr) {
    var m = expr.match(_cache[$_RIX_LOOP])

    return m
      ? { key: m[1], pos: m[2], val: _cache[0] + m[3].trim() + _cache[1] }
      : { val: expr.trim() }
  }

  /**
   * Returns an array with brackets information, defaults to the current brackets.
   * (the `brackets` module in the node version of the compiler allways defaults
   * to the predefined riot brackets `{ }`).
   *
   * _This function is for internal use._
   * @param   {string} [pair] - If used, returns info for this brackets
   * @returns {Array}  Brackets array in internal format.
   * @private
   */
  _brackets.array = function array (pair) {
    return pair ? _create(pair) : _cache
  }

  /**
   * Resets the _cache array with strings and regexes based on its parameter.
   * @param {string} [pair=DEFAULT] - String with the brackets pair to set
   * @alias brackets.set
   */
  function _reset (pair) {
    if ((pair || (pair = DEFAULT)) !== _cache[$_RIX_PAIR]) {
      _cache = _create(pair)
      _regex = pair === DEFAULT ? _loopback : _rewrite
      _cache[$_RIX_LOOP] = _regex(_pairs[$_RIX_LOOP])
    }
    cachedBrackets = pair  // always set these
  }

  /**
   * Allows change detection of `riot.settings.brackets`.
   * @param {object} o - Where store the `brackets` property, mostly `riot.settings`
   * @private
   */
  function _setSettings (o) {
    var b

    o = o || {}
    b = o.brackets
    Object.defineProperty(o, 'brackets', {
      set: _reset,
      get: function () { return cachedBrackets },
      enumerable: true
    })
    _settings = o   // save the new reference
    _reset(b)       // update the brackets
  }

  // Inmediate execution
  // --------------------------------------------------------------------------

  // Set the internal _settings property as reference to `riot.settings`
  Object.defineProperty(_brackets, 'settings', {
    set: _setSettings,
    get: function () { return _settings }
  })

  /* istanbul ignore next: in the browser riot is always in the scope */
  _brackets.settings = typeof riot !== 'undefined' && riot.settings || {}
  _brackets.set = _reset
  _brackets.skipRegex = skipRegex

  // Public properties, shared with `tmpl`
  _brackets.R_STRINGS = R_STRINGS
  _brackets.R_MLCOMMS = R_MLCOMMS
  _brackets.S_QBLOCKS = S_QBLOCKS
  _brackets.S_QBLOCK2 = S_QBLOCK2

  return _brackets

})()