UNPKG

riot-tmpl

Version:

The riot template engine

402 lines (346 loc) 13.7 kB
/** * @module tmpl * * tmpl - Root function, returns the template value, render with data * tmpl.hasExpr - Test the existence of a expression inside a string * tmpl.loopKeys - Get the keys for an 'each' loop (used by `_each`) */ //#if 0 // only in the unprocessed source /* eslint no-unused-vars: [2, {args: "after-used", varsIgnorePattern: "tmpl"}] */ /* global brackets, riot */ //#endif //#define LIST_GETTERS 0 // IIFE for tmpl() //#if ES6 export //#endif var tmpl = (function () { // // Closure data // -------------------------------------------------------------------------- var _cache = {} // // Runtime Functions // -------------------------------------------------------------------------- /** * The exposed tmpl function returns the template value from the cache, render with data. * * @param {string} str - Expression or template with zero or more expressions * @param {Object} data - A Tag instance, for setting the context * @returns {*} Raw value of the expression or template to render * @private */ function _tmpl (str, data) { if (!str) return str // catch falsy values here //#if DEBUG /*eslint no-console: 0 */ if (data && data._debug_) { data._debug_ = 0 if (!_cache[str]) { _cache[str] = _create(str, 1) // request debug output var rs = typeof riot === 'undefined' ? '(riot undefined)' : JSON.stringify(riot.settings) console.log('--- DEBUG' + '\n riot.settings: ' + rs + '\n data: ' + JSON.stringify(data)) } } //#endif // At this point, the expressions must have been parsed, it only remains to construct // the function (if it is not in the cache) and call it to replace expressions with // their values. data (`this`) is a Tag instance, _logErr is the error handler. return (_cache[str] || (_cache[str] = _create(str))).call( data, _logErr.bind({ data: data, tmpl: str }) ) } /** * Checks for an expression within a string, using the current brackets. * * @param {string } str - String where to search * @returns {boolean} `true` if the string contains an expression. * @function */ _tmpl.hasExpr = brackets.hasExpr /** * Parses the `each` expression to detect how to map the collection data to the * children tags. Used by riot browser/tag/each.js * * {key, i in items} -> { key, pos, val } * * @param {String} expr - string passed in the 'each' attribute * @returns {Object} The object needed to check how the items in the collection * should be mapped to the children tags. * @function */ _tmpl.loopKeys = brackets.loopKeys /** * Clears the internal cache of compiled expressions. * * @function */ // istanbul ignore next _tmpl.clearCache = function () { _cache = {} } /** * Holds a custom function to handle evaluation errors. * * This property allows to detect errors _in the evaluation_, by setting its value to a * function that receives the generated Error object, augmented with an object `riotData` * containing the properties `tagName` and `_riot_id` of the context at error time. * * Other (usually fatal) errors, such as "Parse Error" generated by the Function * constructor, are not intercepted. * * If this property is not set, or set to falsy, as in previous versions the error * is silently ignored. * * @type {function} * @static */ _tmpl.errorHandler = null /** * Output an error message through the `_tmpl.errorHandler` function and * the console object. * @param {Error} err - The Error instance generated by the exception * @param {object} ctx - The context * @private */ function _logErr (err, ctx) { // add some data to the Error object err.riotData = { tagName: ctx && ctx.__ && ctx.__.tagName, _riot_id: ctx && ctx._riot_id //eslint-disable-line camelcase } // user error handler if (_tmpl.errorHandler) _tmpl.errorHandler(err) else if ( typeof console !== 'undefined' && typeof console.error === 'function' ) { console.error(err.message) console.log('<%s> %s', err.riotData.tagName || 'Unknown tag', this.tmpl) // eslint-disable-line console.log(this.data) // eslint-disable-line } } /** * Creates a function instance to get a value from the received template string. * * It'll halt the app if the expression has errors (Parse Error or SyntaxError). * * @param {string} str - The template. Can include zero or more expressions * @returns {Function} An instance of Function with the compiled template. * @private */ function _create (str) { var expr = _getTmpl(str) if (expr.slice(0, 11) !== 'try{return ') expr = 'return ' + expr //#if DEBUG if (arguments.length > 1) console.log('--- getter:\n `' + expr + '`\n---') //#elif LIST_GETTERS //console.log(' In: `%s`\nOUT: `%s`', str, expr) //#endif /*#if CSP return safeEval.func('E', expr + ';') //#else */ // Now, we can create the function to return by calling the Function constructor. // The parameter `E` is the error handler for runtime only. return new Function('E', expr + ';') // eslint-disable-line no-new-func //#endif } // // Compilation // -------------------------------------------------------------------------- // Regexes for `_getTmpl` and `_parseExpr` var RE_DQUOTE = /\u2057/g var RE_QBMARK = /\u2057(\d+)~/g // string or regex marker, $1: array index /** * Parses an expression or template with zero or more expressions enclosed with * the current brackets. * * @param {string} str - Raw template string, without comments * @returns {string} Processed template, ready for evaluation. * @private */ function _getTmpl (str) { var parts = brackets.split(str.replace(RE_DQUOTE, '"'), 1)// get text/expr parts var qstr = parts.qblocks // hidden qblocks var expr // We can have almost anything as expressions, except comments... hope if (parts.length > 2 || parts[0]) { var i, j, list = [] for (i = j = 0; i < parts.length; ++i) { expr = parts[i] if (expr && (expr = i & 1 // every odd element is an expression ? _parseExpr(expr, 1, qstr) // mode 1 convert falsy values to "", // except zero : '"' + expr // ttext: convert to js literal string .replace(/\\/g, '\\\\') // this is html, preserve backslashes .replace(/\r\n?|\n/g, '\\n') // normalize eols .replace(/"/g, '\\"') + // escape inner double quotes '"' // enclose in double quotes )) list[j++] = expr } expr = j < 2 ? list[0] // optimize code for 0-1 parts : '[' + list.join(',') + '].join("")' } else { expr = _parseExpr(parts[1], 0, qstr) // single expressions as raw value } // Restore quoted strings and regexes if (qstr.length) { expr = expr.replace(RE_QBMARK, function (_, pos) { return qstr[pos] .replace(/\r/g, '\\r') .replace(/\n/g, '\\n') }) } return expr } var RE_CSNAME = /^(?:(-?[_A-Za-z\xA0-\xFF][-\w\xA0-\xFF]*)|\u2057(\d+)~):/ var RE_BREND = { '(': /[()]/g, '[': /[[\]]/g, '{': /[{}]/g } /** * Parses an individual expression `{expression}` or shorthand `{name: expression, ...}` * * For shorthand names, riot supports a limited subset of the full w3c/html specs of * non-quoted identifiers (closer to CSS1 that CSS2). * * The regex used for recognition is `-?[_A-Za-z\xA0-\xFF][-\w\xA0-\xFF]*`. * * This regex accepts almost all ISO-8859-1 alphanumeric characters within an html * identifier. Doesn't works with escaped codepoints, but you can use Unicode code points * beyond `\u00FF` by quoting the names (not recommended). * * @param {string} expr - The expression, without brackets * @param {number} asText - 0: raw value, 1: falsy as "", except 0 * @param {Array} qstr - Where to store hidden quoted strings and regexes * @returns {string} Code to evaluate the expression. * @see {@link http://www.w3.org/TR/CSS21/grammar.html#scanner} * {@link http://www.w3.org/TR/CSS21/syndata.html#tokenization} * @private */ function _parseExpr (expr, asText, qstr) { // Non-empty quoted strings and literal regexes are hidden at this point. // // Now, this function converts whitespace into compacted spaces and trims // surrounding spaces and some inner tokens, mainly brackets and separators. // We need to convert embedded `\r` and `\n` as these characters breaks // the evaluation. // // WARNING: // Trim and compact is not strictly necessary, but it allows optimized regexes. // Do not touch the next block until you know how/which regexes are affected. expr = expr .replace(/\s+/g, ' ').trim() .replace(/\ ?([[\({},?\.:])\ ?/g, '$1') if (expr) { var list = [], cnt = 0, match // Try to match the first name in the possible shorthand list while (expr && (match = expr.match(RE_CSNAME)) && !match.index // index > 0 means error ) { var key, jsb, re = /,|([[{(])|$/g // Search the next unbracketed comma or the end of 'expr'. // If a openning js bracket is found ($1), skip the block, // if found the end of expr $1 will be empty and the while loop exits. expr = RegExp.rightContext // before replace key = match[2] ? qstr[match[2]].slice(1, -1).trim().replace(/\s+/g, ' ') : match[1] while (jsb = (match = re.exec(expr))[1]) skipBraces(jsb, re) jsb = expr.slice(0, match.index) expr = RegExp.rightContext list[cnt++] = _wrapExpr(jsb, 1, key) } // For shorthands, the generated code returns an array with expression-name pairs expr = !cnt ? _wrapExpr(expr, asText) : cnt > 1 ? '[' + list.join(',') + '].join(" ").trim()' : list[0] } return expr // Skip bracketed block, uses the str value in the closure function skipBraces (ch, re) { var mm, lv = 1, ir = RE_BREND[ch] ir.lastIndex = re.lastIndex while (mm = ir.exec(expr)) { if (mm[0] === ch) ++lv else if (!--lv) break } re.lastIndex = lv ? expr.length : ir.lastIndex } } // Matches a varname, excludes object keys. $1: lookahead, $2: variable name // istanbul ignore next: not both var // eslint-disable-next-line max-len JS_CONTEXT = '"in this?this:' + (typeof window !== 'object' ? 'global' : 'window') + ').', JS_VARNAME = /[,{][\$\w]+(?=:)|(^ *|[^$\w\.{])(?!(?:typeof|true|false|null|undefined|in|instanceof|is(?:Finite|NaN)|void|NaN|new|Date|RegExp|Math)(?![$\w]))([$_A-Za-z][$\w]*)/g, JS_NOPROPS = /^(?=(\.[$\w]+))\1(?:[^.[(]|$)/ /** * Generates code to evaluate an expression avoiding breaking on undefined vars. * * This function include a try..catch block only if needed, if this block is not included, * the generated code has no return statement. * * This `isFinite`, `isNaN`, `Date`, `RegExp`, and `Math` keywords are not wrapped * for context detection (defaults to the global object). * * @param {string} expr - Normalized expression, without brackets * @param {boolean} asText - If trueish, the output is converted to text, not raw values * @param {string} [key] - For shorthands, the key name * @returns {string} Compiled expression. * @private */ function _wrapExpr (expr, asText, key) { var tb expr = expr.replace(JS_VARNAME, function (match, p, mvar, pos, s) { if (mvar) { pos = tb ? 0 : pos + match.length // check only if needed // this, window, and global needs try block too if (mvar !== 'this' && mvar !== 'global' && mvar !== 'window') { match = p + '("' + mvar + JS_CONTEXT + mvar if (pos) tb = (s = s[pos]) === '.' || s === '(' || s === '[' } else if (pos) { tb = !JS_NOPROPS.test(s.slice(pos)) // needs try..catch block? } } return match }) if (tb) { expr = 'try{return ' + expr + '}catch(e){E(e,this)}' } if (key) { // shorthands // w/try : function(){try{return expr}catch(e){E(e,this)}}.call(this)?"name":"" // no try: (expr)?"name":"" // ==> 'return [' + expr_list.join(',') + '].join(" ").trim()' expr = (tb ? 'function(){' + expr + '}.call(this)' : '(' + expr + ')' ) + '?"' + key + '":""' } else if (asText) { // w/try : function(v){try{v=expr}catch(e){E(e,this)};return v||v===0?v:""}.call(this) // no try: function(v){return (v=(expr))||v===0?v:""}.call(this) // ==> 'return [' + text_and_expr_list.join(',') + '].join("")' expr = 'function(v){' + (tb ? expr.replace('return ', 'v=') : 'v=(' + expr + ')' ) + ';return v||v===0?v:""}.call(this)' } // else if (!asText) // no try: return expr // w/try : try{return expr}catch(e){E(e,this)} // returns undefined if error return expr } //#if !NODE _tmpl.version = brackets.version = 'WIP' //#endif return _tmpl })()