src/tmpl.js
/**
* @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
})()