123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577 |
- /*!
- * 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 */
- (function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
- typeof define === 'function' && define.amd ? define(['exports'], factory) :
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.jinja = {}));
- })(this, (function (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;
- var OPERATORS = /(===?|!==?|>=?|<=?|&&|\|\||[+\-\*\/%])/g;
- //extended (english) operators
- var EOPS = /(^|[^$\w])(and|or|not|is|isnot)([^$\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: '!='
- };
- 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 + " " + term);
- }
- });
- parsed3.src = parsed3.src.replace(VARIABLES, this.parseVar.bind(this));
- parsed2.src = this.injectEnt(parsed3, 'i');
- 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: 'toJson'};
- 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) ? '' : 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;
- },
- toJson: function (val) {
- if (typeof val === 'object') {
- return JSON.stringify(val);
- }
- return toString(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;
- }
- }
- }
- }));
|