UNPKG

riot-tmpl

Version:

The riot template engine

430 lines (376 loc) 15.2 kB
/** * 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 })()