UNPKG

riot-compiler

Version:

Compiler for riot .tag files

273 lines (237 loc) 8.82 kB
'use strict' /** * Brackets support for the node.js version of the riot-compiler * @module */ var safeRegex = require('./safe-regex') var skipRegex = require('skip-regex') /** * Matches valid, multiline JavaScript comments in almost all its forms. * @const {RegExp} * @static */ var R_MLCOMMS = /\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//g /** * Matches single and double quoted strings. Don't care about inner EOLs, so it * can be used to match HTML strings, but skips escaped quotes as JavaScript does. * Useful to skip strings in values with expressions, e.g. `name={ 'John\'s' }`. * @const {RegExp} * @static */ var R_STRINGS = /"[^"\\]*(?:\\[\S\s][^"\\]*)*"|'[^'\\]*(?:\\[\S\s][^'\\]*)*'|`[^`\\]*(?:\\[\S\s][^`\\]*)*`/g /** * The {@link module:brackets.R_STRINGS|R_STRINGS} source combined with sources of * regexes matching division operators and literal regexes, for use with the RegExp * constructor. 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 */ var S_QBLOCKS = R_STRINGS.source + '|' + /(?:\breturn\s+|(?:[$\w)\]]|\+\+|--)\s*(\/)(?![*/]))/.source + '|' + /\/(?=[^*/])[^[/\\]*(?:(?:\[(?:\\.|[^\]\\]*)*\]|\\.)[^[/\\]*)*?([^<]\/)[gim]*/.source /* JS/ES6 quoted strings and start of regex (basic ES6 does not supports nested backquotes). */ var S_QBLOCK2 = R_STRINGS.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} */ var FINDBRACES = { '(': RegExp('([()])|' + S_QBLOCK2, 'g'), '[': RegExp('([[\\]])|' + S_QBLOCK2, 'g'), '{': RegExp('([{}])|' + S_QBLOCK2, 'g') } /** * The predefined riot brackets * @const {string} * @default */ var DEFAULT = '{ }' // Pre-made string and regexes for the default brackets var _pairs = [ '{', '}', '{', '}', /{[^}]*}/, /\\([{}])/g, /\\({)|{/g, RegExp('\\\\(})|([[({])|(})|' + S_QBLOCKS, 'g'), DEFAULT ] // Pre-made string and regexes for the last bracket pair var _cache = [] /* Private functions --------------------------------------------------------------------------- */ /** * Rewrite a regex with the default brackets replaced with the custom ones. * * @param {RegExp} re - RegExp with the default riot brackets * @returns {RegExp} The new regex with the default brackets replaced. */ function _rewrite (re) { return RegExp( re.source.replace(/{/g, _cache[2]).replace(/}/g, _cache[3]), re.global ? 'g' : '' ) } /* Exported methods and properties --------------------------------------------------------------------------- */ module.exports = { R_STRINGS: R_STRINGS, R_MLCOMMS: R_MLCOMMS, S_QBLOCKS: S_QBLOCKS, S_QBLOCK2: S_QBLOCK2, skipRegex: skipRegex } /** * Splits the received string in its template text and expression parts using * balanced brackets detection to avoid require escaped brackets from the users. * * _For internal use by the riot-compiler._ * * @param {string} str - Template source to split, can be one expression * @param {number} _ - unused * @param {Array} _bp - Info of custom brackets to use * @returns {Array} Array of alternating template text and expressions. * If _str_ has one unique expression, returns two elements: `["", expression]`. */ module.exports.split = function split (str, _, _bp) { /* 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[6] // start with *updated* regex for opening bracket isexpr = start = re.lastIndex = 0 // re is reused, we must reset lastIndex while ((match = re.exec(str))) { 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) continue // skip the bracketed block and loop } if (!match[3]) { // if don't have a closing bracket // look here if this "regex" is a real regex (riot#2361) if (match[4]) { re.lastIndex = skipRegex(str, match.index) } continue // search again } } /* At this point, we expect an _unescaped_ openning bracket in $2 for text, or a closing bracket in $3 for expression. $1 may be an backslash. */ 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[6 + (isexpr = !isexpr)] // 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)) } return parts /* Inner Helpers for _split() */ /** * Stores the processed string in the array `parts`. * Unescape escaped brackets from expressions. * * @param {string} s - can be template text or an expression */ function unescapeStr (s) { if (isexpr) { parts.push(s && s.replace(_bp[5], '$1')) } else { parts.push(s) } } /** * Find the closing JS bracket for the current block in the given string. * Skips strings, regexes, and other inner blocks. * * @param {string} s - The searched buffer * @param {string} ch - Opening bracket character * @param {number} ix - Position inside `str` following the opening bracket * @returns {number} Position following the closing bracket. * * @throws Will throw "Unbalanced brackets in ..." if the closing bracket is not found. */ function skipBraces (s, ch, ix) { var mm, rr = FINDBRACES[ch] rr.lastIndex = ix ix = 1 while ((mm = rr.exec(s))) { if (mm[1]) { if (mm[1] === ch) ++ix else if (!--ix) break } else if (mm[2]) { rr.lastIndex = skipRegex(str, mm.index) } } if (ix) { throw new Error('Unbalanced brackets in ...`' + s.slice(start) + '`?') } return rr.lastIndex } } var INVALIDCH = safeRegex(/[@-@<>a-zA-Z0-9'",;\\]/, 'x00', 'x1F') // invalid characters for brackets var ESCAPEDCH = /(?=[[\]()*+?.^$|])/g // this characters must be escaped /** * Returns an array with information for the given brackets using a cache for the * last custom brackets pair required by the caller. * * _For internal use by the riot-compiler._ * * @param {string} [pair=DEFAULT] - If used, take this pair as base * @returns {Array} Information about the given brackets, in internal format. * * @throws Will throw "Unsupported brackets ..." if _pair_ contains invalid characters * or is not separated by one space. */ module.exports.array = function array (pair) { if (!pair || pair === DEFAULT) return _pairs if (_cache[8] !== pair) { _cache = pair.split(' ') if (_cache.length !== 2 || INVALIDCH.test(pair)) { throw new Error('Unsupported brackets "' + pair + '"') } _cache = _cache.concat(pair.replace(ESCAPEDCH, '\\').split(' ')) _cache[4] = _rewrite(_cache[1].length > 1 ? /{[\S\s]*?}/ : _pairs[4]) _cache[5] = _rewrite(/\\({|})/g) _cache[6] = _rewrite(_pairs[6]) // for _split() _cache[7] = RegExp('\\\\(' + _cache[3] + ')|([[({])|(' + _cache[3] + ')|' + S_QBLOCKS, 'g') _cache[8] = pair } return _cache }