(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+)?$/; var NON_PRIMITIVES = /\[[@#~](,[@#~])*\]|\[\]|\{([@i]:[@#~])(,[@i]:[@#~])*\}|\{\}/g; var IDENTIFIERS = /[$_a-z][$\w]*/gi; var VARIABLES = /i(\.i|\[[@#i]\])*/g; var ACCESSOR = /(\.i|\[[@#i]\])/g; var OPERATORS = /(===?|!==?|>=?|<=?|&&|\|\||[+\-\*\/%])/g; 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) { var match = src.slice(index + open.length).match(TAGS[open]); match = match ? match[0] : ""; var simplified = match.replace(STRINGS, "@"); if (!match || ~simplified.indexOf(open)) { return index + 1 } var inner = match.slice(0, 0 - open.length); if (inner.charAt(0) === "-") var wsCollapseLeft = true; if (inner.slice(-1) === "-") var wsCollapseRight = true; inner = inner.replace(/^-|-$/g, "").trim(); 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 === "{{{") { 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, "@"); extracted.src = extracted.src.replace(/\|\|/g, "~").split("|"); 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] }; 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") }; Parser.prototype.parseExpr = function(src, opts) { opts = opts || {}; var parsed1 = this.extractEnt(src, STRINGS, "@"); parsed1.src = parsed1.src.replace(EOPS, function(s, before, op, after) { return op in operators ? before + operators[op] + after : s }); var parsed2 = this.extractEnt(parsed1.src, IDENTS_AND_NUMS, function(s) { return s in constants || NUMBER.test(s) ? "#" : null }); var parsed3 = this.extractEnt(parsed2.src, IDENTIFIERS, "i"); parsed3.src = parsed3.src.replace(/\s+/g, ""); var simplified = parsed3.src; while (simplified !== (simplified = this.replaceComplex(simplified))); while (simplified !== (simplified = simplified.replace(/i(\.i|\[[@#i]\])+/, "v"))); simplified = simplified.replace(/[iv]\[v?\]/g, "x"); simplified = simplified.replace(/[@#~v]/g, "i"); simplified = simplified.replace(OPERATORS, "%"); simplified = simplified.replace(/!+[i]/g, "i"); var terms = opts.terms ? simplified.split(",") : [simplified]; terms.forEach(function(term) { 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(); 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(",") + ")" }; 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 + '"' } return JSON.parse(str) }; 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; this.isSilent = true }, include: function(name) { name = this.parseQuoted(name); var incSrc = this.readTemplateFile(name); this.isInclude = true; this.tokenize(incSrc); this.isInclude = false } }; tagHandlers.assign = tagHandlers.set; 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 }; 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; val = filter.apply(data, arr) } else { throw new Error("Invalid filter: " + name) } } if (opts.autoEscape && name !== opts.autoEscape && name !== "safe") { 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 }; function trimLeft(str) { return str.replace(LEADING_SPACE, "") } function trimRight(str) { return str.replace(TRAILING_SPACE, "") } function matchAll(str, reg, fn) { 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 } } } });