/*! * Jinja Templating for JavaScript v0.1.8 * https://github.com/sstur/jinja-js * * This is a slimmed-down Jinja2 implementation [http://jinja.pocoo.org/] * * In the interest of simplicity, it deviates from Jinja2 as follows: * - Line statements, cycle, super, macro tags and block nesting are not implemented * - auto escapes html by default (the filter is "html" not "e") * - Only "html" and "safe" filters are built in * - Filters are not valid in expressions; `foo|length > 1` is not valid * - Expression Tests (`if num is odd`) not implemented (`is` translates to `==` and `isnot` to `!=`) * * Notes: * - if property is not found, but method '_get' exists, it will be called with the property name (and cached) * - `{% for n in obj %}` iterates the object's keys; get the value with `{% for n in obj %}{{ obj[n] }}{% endfor %}` * - subscript notation `a[0]` takes literals or simple variables but not `a[item.key]` * - `.2` is not a valid number literal; use `0.2` * */ /*global require, exports, module, define */ var jinja; (function(definition) { if (typeof exports === 'object' && typeof module === 'object') { // CommonJS/Node definition(require, exports, module); //backwards compatibility if (typeof define === 'function') { define('jinja', function() { this.exports = module.exports; }); } return; } if (typeof define === 'function') { //AMD or Other return define.amd ? define(['require', 'exports'], definition) : define('jinja', definition); } definition(function() {}, jinja = {}); })(function(require, jinja) { "use strict"; var STRINGS = /'(\\.|[^'])*'|"(\\.|[^"'"])*"/g; var IDENTS_AND_NUMS = /([$_a-z][$\w]*)|([+-]?\d+(\.\d+)?)/g; var NUMBER = /^[+-]?\d+(\.\d+)?$/; //non-primitive literals (array and object literals) var NON_PRIMITIVES = /\[[@#~](,[@#~])*\]|\[\]|\{([@i]:[@#~])(,[@i]:[@#~])*\}|\{\}/g; //bare identifiers such as variables and in object literals: {foo: 'value'} var IDENTIFIERS = /[$_a-z][$\w]*/ig; var VARIABLES = /i(\.i|\[[@#i]\])*/g; var ACCESSOR = /(\.i|\[[@#i]\])/g; // NOTE: We use ^ for the Python-like "in" operator. var OPERATORS = /(===?|!==?|>=?|<=?|&&|\|\||[+\-\*\/%|\^])/g; //extended (english) operators var EOPS = /(^|[^$\w])(and|or|not|is|isnot|in)([^$\w]|$)/g; var LEADING_SPACE = /^\s+/; var TRAILING_SPACE = /\s+$/; var START_TOKEN = /\{\{\{|\{\{|\{%|\{#/; var TAGS = { '{{{': /^('(\\.|[^'])*'|"(\\.|[^"'"])*"|.)+?\}\}\}/, '{{': /^('(\\.|[^'])*'|"(\\.|[^"'"])*"|.)+?\}\}/, '{%': /^('(\\.|[^'])*'|"(\\.|[^"'"])*"|.)+?%\}/, '{#': /^('(\\.|[^'])*'|"(\\.|[^"'"])*"|.)+?#\}/ }; var delimeters = { '{%': 'directive', '{{': 'output', '{#': 'comment' }; var operators = { and: '&&', or: '||', not: '!', is: '==', isnot: '!=', in: '^', }; var constants = { 'true': true, 'false': false, 'null': null }; function Parser() { this.nest = []; this.compiled = []; this.childBlocks = 0; this.parentBlocks = 0; this.isSilent = false; } Parser.prototype.push = function(line) { if (!this.isSilent) { this.compiled.push(line); } }; Parser.prototype.parse = function(src) { this.tokenize(src); return this.compiled; }; Parser.prototype.tokenize = function(src) { var lastEnd = 0, parser = this, trimLeading = false; matchAll(src, START_TOKEN, function(open, index, src) { //here we match the rest of the src against a regex for this tag var match = src.slice(index + open.length).match(TAGS[open]); match = (match ? match[0] : ''); //here we sub out strings so we don't get false matches var simplified = match.replace(STRINGS, '@'); //if we don't have a close tag or there is a nested open tag if (!match || ~simplified.indexOf(open)) { return index + 1; } var inner = match.slice(0, 0 - open.length); //check for white-space collapse syntax if (inner.charAt(0) == '-') var wsCollapseLeft = true; if (inner.slice(-1) == '-') var wsCollapseRight = true; inner = inner.replace(/^-|-$/g, '').trim(); //if we're in raw mode and we are not looking at an "endraw" tag, move along if (parser.rawMode && (open + inner) != '{%endraw') { return index + 1; } var text = src.slice(lastEnd, index); lastEnd = index + open.length + match.length; if (trimLeading) text = trimLeft(text); if (wsCollapseLeft) text = trimRight(text); if (wsCollapseRight) trimLeading = true; if (open == '{{{') { //liquid-style: make {{{x}}} => {{x|safe}} open = '{{'; inner += '|safe'; } parser.textHandler(text); parser.tokenHandler(open, inner); }); var text = src.slice(lastEnd); if (trimLeading) text = trimLeft(text); this.textHandler(text); }; Parser.prototype.textHandler = function(text) { this.push('write(' + JSON.stringify(text) + ');'); }; Parser.prototype.tokenHandler = function(open, inner) { var type = delimeters[open]; if (type == 'directive') { this.compileTag(inner); } else if (type == 'output') { var extracted = this.extractEnt(inner, STRINGS, '@'); //replace || operators with ~ extracted.src = extracted.src.replace(/\|\|/g, '~').split('|'); //put back || operators extracted.src = extracted.src.map(function(part) { return part.split('~').join('||'); }); var parts = this.injectEnt(extracted, '@'); if (parts.length > 1) { var filters = parts.slice(1).map(this.parseFilter.bind(this)); this.push('filter(' + this.parseExpr(parts[0]) + ',' + filters.join(',') + ');'); } else { this.push('filter(' + this.parseExpr(parts[0]) + ');'); } } }; Parser.prototype.compileTag = function(str) { var directive = str.split(' ')[0]; var handler = tagHandlers[directive]; if (!handler) { throw new Error('Invalid tag: ' + str); } handler.call(this, str.slice(directive.length).trim()); }; Parser.prototype.parseFilter = function(src) { src = src.trim(); var match = src.match(/[:(]/); var i = match ? match.index : -1; if (i < 0) return JSON.stringify([src]); var name = src.slice(0, i); var args = src.charAt(i) == ':' ? src.slice(i + 1) : src.slice(i + 1, -1); args = this.parseExpr(args, {terms: true}); return '[' + JSON.stringify(name) + ',' + args + ']'; }; Parser.prototype.extractEnt = function(src, regex, placeholder) { var subs = [], isFunc = typeof placeholder == 'function'; src = src.replace(regex, function(str) { var replacement = isFunc ? placeholder(str) : placeholder; if (replacement) { subs.push(str); return replacement; } return str; }); return {src: src, subs: subs}; }; Parser.prototype.injectEnt = function(extracted, placeholder) { var src = extracted.src, subs = extracted.subs, isArr = Array.isArray(src); var arr = (isArr) ? src : [src]; var re = new RegExp('[' + placeholder + ']', 'g'), i = 0; arr.forEach(function(src, index) { arr[index] = src.replace(re, function() { return subs[i++]; }); }); return isArr ? arr : arr[0]; }; //replace complex literals without mistaking subscript notation with array literals Parser.prototype.replaceComplex = function(s) { var parsed = this.extractEnt(s, /i(\.i|\[[@#i]\])+/g, 'v'); parsed.src = parsed.src.replace(NON_PRIMITIVES, '~'); return this.injectEnt(parsed, 'v'); }; //parse expression containing literals (including objects/arrays) and variables (including dot and subscript notation) //valid expressions: `a + 1 > b.c or c == null`, `a and b[1] != c`, `(a < b) or (c < d and e)`, 'a || [1]` Parser.prototype.parseExpr = function(src, opts) { opts = opts || {}; //extract string literals -> @ var parsed1 = this.extractEnt(src, STRINGS, '@'); //note: this will catch {not: 1} and a.is; could we replace temporarily and then check adjacent chars? parsed1.src = parsed1.src.replace(EOPS, function(s, before, op, after) { return (op in operators) ? before + operators[op] + after : s; }); //sub out non-string literals (numbers/true/false/null) -> # // the distinction is necessary because @ can be object identifiers, # cannot var parsed2 = this.extractEnt(parsed1.src, IDENTS_AND_NUMS, function(s) { return (s in constants || NUMBER.test(s)) ? '#' : null; }); //sub out object/variable identifiers -> i var parsed3 = this.extractEnt(parsed2.src, IDENTIFIERS, 'i'); //remove white-space parsed3.src = parsed3.src.replace(/\s+/g, ''); //the rest of this is simply to boil the expression down and check validity var simplified = parsed3.src; //sub out complex literals (objects/arrays) -> ~ // the distinction is necessary because @ and # can be subscripts but ~ cannot while (simplified != (simplified = this.replaceComplex(simplified))); //now @ represents strings, # represents other primitives and ~ represents non-primitives //replace complex variables (those with dot/subscript accessors) -> v while (simplified != (simplified = simplified.replace(/i(\.i|\[[@#i]\])+/, 'v'))); //empty subscript or complex variables in subscript, are not permitted simplified = simplified.replace(/[iv]\[v?\]/g, 'x'); //sub in "i" for @ and # and ~ and v (now "i" represents all literals, variables and identifiers) simplified = simplified.replace(/[@#~v]/g, 'i'); //sub out operators simplified = simplified.replace(OPERATORS, '%'); //allow 'not' unary operator simplified = simplified.replace(/!+[i]/g, 'i'); var terms = opts.terms ? simplified.split(',') : [simplified]; terms.forEach(function(term) { //simplify logical grouping while (term != (term = term.replace(/\(i(%i)*\)/g, 'i'))); if (!term.match(/^i(%i)*$/)) { throw new Error('Invalid expression: ' + src); } }); parsed3.src = parsed3.src.replace(VARIABLES, this.parseVar.bind(this)); parsed2.src = this.injectEnt(parsed3, 'i'); // FUDGE! We want to support the Python "in" operators, but this is problematic since there isn't // a corresponding Javascript operator, and we have to translate it into a call to indexOf(). // NOTE: Expressions of the form "'foo' in 'bar'" won't work, because we get a pseudo-expression // of "@^@", which gets translated to "@.indexOf(@) !== -1", and the call to injectEnt() later // has no way of knowing that the literals should be swapped around. However, this is not a case // we really need to worry about :-/ // NOTE: It also won't handle computed expressions (e.g. "A+B in C+D"), but the user can also calculate // these values and save them in a temporary variable, and then use those in the "in" expression. var regex = new RegExp( /(@|get\(.+?\))\^(@|get\(.+?\))/g ) ; var matches = [] ; while( (match = regex.exec( parsed2.src )) !== null ) matches.push( match ) ; while ( matches.length > 0 ) { var match = matches.pop() ; parsed2.src = parsed2.src.substring( 0, match.index ) + match[2] + ".toLowerCase().indexOf(" + match[1] + ".toLowerCase()) !== -1" + parsed2.src.substring( match.index + match[0].length ) ; } parsed1.src = this.injectEnt(parsed2, '#'); return this.injectEnt(parsed1, '@'); }; Parser.prototype.parseVar = function(src) { var args = Array.prototype.slice.call(arguments); var str = args.pop(), index = args.pop(); //quote bare object identifiers (might be a reserved word like {while: 1}) if (src == 'i' && str.charAt(index + 1) == ':') { return '"i"'; } var parts = ['"i"']; src.replace(ACCESSOR, function(part) { if (part == '.i') { parts.push('"i"'); } else if (part == '[i]') { parts.push('get("i")'); } else { parts.push(part.slice(1, -1)); } }); return 'get(' + parts.join(',') + ')'; }; //escapes a name to be used as a javascript identifier Parser.prototype.escName = function(str) { return str.replace(/\W/g, function(s) { return '$' + s.charCodeAt(0).toString(16); }); }; Parser.prototype.parseQuoted = function(str) { if (str.charAt(0) == "'") { str = str.slice(1, -1).replace(/\\.|"/, function(s) { if (s == "\\'") return "'"; return s.charAt(0) == '\\' ? s : ('\\' + s); }); str = '"' + str + '"'; } //todo: try/catch or deal with invalid characters (linebreaks, control characters) return JSON.parse(str); }; //the context 'this' inside tagHandlers is the parser instance var tagHandlers = { 'if': function(expr) { this.push('if (' + this.parseExpr(expr) + ') {'); this.nest.unshift('if'); }, 'else': function() { if (this.nest[0] == 'for') { this.push('}, function() {'); } else { this.push('} else {'); } }, 'elseif': function(expr) { this.push('} else if (' + this.parseExpr(expr) + ') {'); }, 'endif': function() { this.nest.shift(); this.push('}'); }, 'for': function(str) { var i = str.indexOf(' in '); var name = str.slice(0, i).trim(); var expr = str.slice(i + 4).trim(); this.push('each(' + this.parseExpr(expr) + ',' + JSON.stringify(name) + ',function() {'); this.nest.unshift('for'); }, 'endfor': function() { this.nest.shift(); this.push('});'); }, 'raw': function() { this.rawMode = true; }, 'endraw': function() { this.rawMode = false; }, 'set': function(stmt) { var i = stmt.indexOf('='); var name = stmt.slice(0, i).trim(); var expr = stmt.slice(i + 1).trim(); this.push('set(' + JSON.stringify(name) + ',' + this.parseExpr(expr) + ');'); }, 'block': function(name) { if (this.isParent) { ++this.parentBlocks; var blockName = 'block_' + (this.escName(name) || this.parentBlocks); this.push('block(typeof ' + blockName + ' == "function" ? ' + blockName + ' : function() {'); } else if (this.hasParent) { this.isSilent = false; ++this.childBlocks; blockName = 'block_' + (this.escName(name) || this.childBlocks); this.push('function ' + blockName + '() {'); } this.nest.unshift('block'); }, 'endblock': function() { this.nest.shift(); if (this.isParent) { this.push('});'); } else if (this.hasParent) { this.push('}'); this.isSilent = true; } }, 'extends': function(name) { name = this.parseQuoted(name); var parentSrc = this.readTemplateFile(name); this.isParent = true; this.tokenize(parentSrc); this.isParent = false; this.hasParent = true; //silence output until we enter a child block this.isSilent = true; }, 'include': function(name) { name = this.parseQuoted(name); var incSrc = this.readTemplateFile(name); this.isInclude = true; this.tokenize(incSrc); this.isInclude = false; } }; //liquid style tagHandlers.assign = tagHandlers.set; //python/django style tagHandlers.elif = tagHandlers.elseif; var getRuntime = function runtime(data, opts) { var defaults = {autoEscape: 'html'}; var _toString = Object.prototype.toString; var _hasOwnProperty = Object.prototype.hasOwnProperty; var getKeys = Object.keys || function(obj) { var keys = []; for (var n in obj) if (_hasOwnProperty.call(obj, n)) keys.push(n); return keys; }; var isArray = Array.isArray || function(obj) { return _toString.call(obj) === '[object Array]'; }; var create = Object.create || function(obj) { function F() {} F.prototype = obj; return new F(); }; var toString = function(val) { if (val == null) return ''; return (typeof val.toString == 'function') ? val.toString() : _toString.call(val); }; var extend = function(dest, src) { var keys = getKeys(src); for (var i = 0, len = keys.length; i < len; i++) { var key = keys[i]; dest[key] = src[key]; } return dest; }; //get a value, lexically, starting in current context; a.b -> get("a","b") var get = function() { var val, n = arguments[0], c = stack.length; while (c--) { val = stack[c][n]; if (typeof val != 'undefined') break; } for (var i = 1, len = arguments.length; i < len; i++) { if (val == null) continue; n = arguments[i]; val = (_hasOwnProperty.call(val, n)) ? val[n] : (typeof val._get == 'function' ? (val[n] = val._get(n)) : null); } return (val == null) ? null : val; }; var set = function(n, val) { stack[stack.length - 1][n] = val; }; var push = function(ctx) { stack.push(ctx || {}); }; var pop = function() { stack.pop(); }; var write = function(str) { output.push(str); }; var filter = function(val) { for (var i = 1, len = arguments.length; i < len; i++) { var arr = arguments[i], name = arr[0], filter = filters[name]; if (filter) { arr[0] = val; //now arr looks like [val, arg1, arg2] val = filter.apply(data, arr); } else { throw new Error('Invalid filter: ' + name); } } if (opts.autoEscape && name != opts.autoEscape && name != 'safe') { //auto escape if not explicitly safe or already escaped val = filters[opts.autoEscape].call(data, val); } output.push(val); }; var each = function(obj, loopvar, fn1, fn2) { if (obj == null) return; var arr = isArray(obj) ? obj : getKeys(obj), len = arr.length; var ctx = {loop: {length: len, first: arr[0], last: arr[len - 1]}}; push(ctx); for (var i = 0; i < len; i++) { extend(ctx.loop, {index: i + 1, index0: i}); fn1(ctx[loopvar] = arr[i]); } if (len == 0 && fn2) fn2(); pop(); }; var block = function(fn) { push(); fn(); pop(); }; var render = function() { return output.join(''); }; data = data || {}; opts = extend(defaults, opts || {}); var filters = extend({ html: function(val) { return toString(val) .split('&').join('&') .split('<').join('<') .split('>').join('>') .split('"').join('"'); }, safe: function(val) { return val; } }, opts.filters || {}); var stack = [create(data || {})], output = []; return {get: get, set: set, push: push, pop: pop, write: write, filter: filter, each: each, block: block, render: render}; }; var runtime; jinja.compile = function(markup, opts) { opts = opts || {}; var parser = new Parser(); parser.readTemplateFile = this.readTemplateFile; var code = []; code.push('function render($) {'); code.push('var get = $.get, set = $.set, push = $.push, pop = $.pop, write = $.write, filter = $.filter, each = $.each, block = $.block;'); code.push.apply(code, parser.parse(markup)); code.push('return $.render();'); code.push('}'); code = code.join('\n'); if (opts.runtime === false) { var fn = new Function('data', 'options', 'return (' + code + ')(runtime(data, options))'); } else { runtime = runtime || (runtime = getRuntime.toString()); fn = new Function('data', 'options', 'return (' + code + ')((' + runtime + ')(data, options))'); } return {render: fn}; }; jinja.render = function(markup, data, opts) { var tmpl = jinja.compile(markup); return tmpl.render(data, opts); }; jinja.templateFiles = []; jinja.readTemplateFile = function(name) { var templateFiles = this.templateFiles || []; var templateFile = templateFiles[name]; if (templateFile == null) { throw new Error('Template file not found: ' + name); } return templateFile; }; /*! * Helpers */ function trimLeft(str) { return str.replace(LEADING_SPACE, ''); } function trimRight(str) { return str.replace(TRAILING_SPACE, ''); } function matchAll(str, reg, fn) { //copy as global reg = new RegExp(reg.source, 'g' + (reg.ignoreCase ? 'i' : '') + (reg.multiline ? 'm' : '')); var match; while ((match = reg.exec(str))) { var result = fn(match[0], match.index, str); if (typeof result == 'number') { reg.lastIndex = result; } } } });