jinja.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. /*!
  2. * Jinja Templating for JavaScript v0.1.8
  3. * https://github.com/sstur/jinja-js
  4. *
  5. * This is a slimmed-down Jinja2 implementation [http://jinja.pocoo.org/]
  6. *
  7. * In the interest of simplicity, it deviates from Jinja2 as follows:
  8. * - Line statements, cycle, super, macro tags and block nesting are not implemented
  9. * - auto escapes html by default (the filter is "html" not "e")
  10. * - Only "html" and "safe" filters are built in
  11. * - Filters are not valid in expressions; `foo|length > 1` is not valid
  12. * - Expression Tests (`if num is odd`) not implemented (`is` translates to `==` and `isnot` to `!=`)
  13. *
  14. * Notes:
  15. * - if property is not found, but method '_get' exists, it will be called with the property name (and cached)
  16. * - `{% for n in obj %}` iterates the object's keys; get the value with `{% for n in obj %}{{ obj[n] }}{% endfor %}`
  17. * - subscript notation `a[0]` takes literals or simple variables but not `a[item.key]`
  18. * - `.2` is not a valid number literal; use `0.2`
  19. *
  20. */
  21. /*global require, exports, module, define */
  22. (function (global, factory) {
  23. typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
  24. typeof define === 'function' && define.amd ? define(['exports'], factory) :
  25. (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.jinja = {}));
  26. })(this, (function (jinja) {
  27. "use strict";
  28. var STRINGS = /'(\\.|[^'])*'|"(\\.|[^"'"])*"/g;
  29. var IDENTS_AND_NUMS = /([$_a-z][$\w]*)|([+-]?\d+(\.\d+)?)/g;
  30. var NUMBER = /^[+-]?\d+(\.\d+)?$/;
  31. //non-primitive literals (array and object literals)
  32. var NON_PRIMITIVES = /\[[@#~](,[@#~])*\]|\[\]|\{([@i]:[@#~])(,[@i]:[@#~])*\}|\{\}/g;
  33. //bare identifiers such as variables and in object literals: {foo: 'value'}
  34. var IDENTIFIERS = /[$_a-z][$\w]*/ig;
  35. var VARIABLES = /i(\.i|\[[@#i]\])*/g;
  36. var ACCESSOR = /(\.i|\[[@#i]\])/g;
  37. var OPERATORS = /(===?|!==?|>=?|<=?|&&|\|\||[+\-\*\/%])/g;
  38. //extended (english) operators
  39. var EOPS = /(^|[^$\w])(and|or|not|is|isnot)([^$\w]|$)/g;
  40. var LEADING_SPACE = /^\s+/;
  41. var TRAILING_SPACE = /\s+$/;
  42. var START_TOKEN = /\{\{\{|\{\{|\{%|\{#/;
  43. var TAGS = {
  44. '{{{': /^('(\\.|[^'])*'|"(\\.|[^"'"])*"|.)+?\}\}\}/,
  45. '{{': /^('(\\.|[^'])*'|"(\\.|[^"'"])*"|.)+?\}\}/,
  46. '{%': /^('(\\.|[^'])*'|"(\\.|[^"'"])*"|.)+?%\}/,
  47. '{#': /^('(\\.|[^'])*'|"(\\.|[^"'"])*"|.)+?#\}/
  48. };
  49. var delimeters = {
  50. '{%': 'directive',
  51. '{{': 'output',
  52. '{#': 'comment'
  53. };
  54. var operators = {
  55. and: '&&',
  56. or: '||',
  57. not: '!',
  58. is: '==',
  59. isnot: '!='
  60. };
  61. var constants = {
  62. 'true': true,
  63. 'false': false,
  64. 'null': null
  65. };
  66. function Parser() {
  67. this.nest = [];
  68. this.compiled = [];
  69. this.childBlocks = 0;
  70. this.parentBlocks = 0;
  71. this.isSilent = false;
  72. }
  73. Parser.prototype.push = function (line) {
  74. if (!this.isSilent) {
  75. this.compiled.push(line);
  76. }
  77. };
  78. Parser.prototype.parse = function (src) {
  79. this.tokenize(src);
  80. return this.compiled;
  81. };
  82. Parser.prototype.tokenize = function (src) {
  83. var lastEnd = 0, parser = this, trimLeading = false;
  84. matchAll(src, START_TOKEN, function (open, index, src) {
  85. //here we match the rest of the src against a regex for this tag
  86. var match = src.slice(index + open.length).match(TAGS[open]);
  87. match = (match ? match[0] : '');
  88. //here we sub out strings so we don't get false matches
  89. var simplified = match.replace(STRINGS, '@');
  90. //if we don't have a close tag or there is a nested open tag
  91. if (!match || ~simplified.indexOf(open)) {
  92. return index + 1;
  93. }
  94. var inner = match.slice(0, 0 - open.length);
  95. //check for white-space collapse syntax
  96. if (inner.charAt(0) === '-') var wsCollapseLeft = true;
  97. if (inner.slice(-1) === '-') var wsCollapseRight = true;
  98. inner = inner.replace(/^-|-$/g, '').trim();
  99. //if we're in raw mode and we are not looking at an "endraw" tag, move along
  100. if (parser.rawMode && (open + inner) !== '{%endraw') {
  101. return index + 1;
  102. }
  103. var text = src.slice(lastEnd, index);
  104. lastEnd = index + open.length + match.length;
  105. if (trimLeading) text = trimLeft(text);
  106. if (wsCollapseLeft) text = trimRight(text);
  107. if (wsCollapseRight) trimLeading = true;
  108. if (open === '{{{') {
  109. //liquid-style: make {{{x}}} => {{x|safe}}
  110. open = '{{';
  111. inner += '|safe';
  112. }
  113. parser.textHandler(text);
  114. parser.tokenHandler(open, inner);
  115. });
  116. var text = src.slice(lastEnd);
  117. if (trimLeading) text = trimLeft(text);
  118. this.textHandler(text);
  119. };
  120. Parser.prototype.textHandler = function (text) {
  121. this.push('write(' + JSON.stringify(text) + ');');
  122. };
  123. Parser.prototype.tokenHandler = function (open, inner) {
  124. var type = delimeters[open];
  125. if (type === 'directive') {
  126. this.compileTag(inner);
  127. } else if (type === 'output') {
  128. var extracted = this.extractEnt(inner, STRINGS, '@');
  129. //replace || operators with ~
  130. extracted.src = extracted.src.replace(/\|\|/g, '~').split('|');
  131. //put back || operators
  132. extracted.src = extracted.src.map(function (part) {
  133. return part.split('~').join('||');
  134. });
  135. var parts = this.injectEnt(extracted, '@');
  136. if (parts.length > 1) {
  137. var filters = parts.slice(1).map(this.parseFilter.bind(this));
  138. this.push('filter(' + this.parseExpr(parts[0]) + ',' + filters.join(',') + ');');
  139. } else {
  140. this.push('filter(' + this.parseExpr(parts[0]) + ');');
  141. }
  142. }
  143. };
  144. Parser.prototype.compileTag = function (str) {
  145. var directive = str.split(' ')[0];
  146. var handler = tagHandlers[directive];
  147. if (!handler) {
  148. throw new Error('Invalid tag: ' + str);
  149. }
  150. handler.call(this, str.slice(directive.length).trim());
  151. };
  152. Parser.prototype.parseFilter = function (src) {
  153. src = src.trim();
  154. var match = src.match(/[:(]/);
  155. var i = match ? match.index : -1;
  156. if (i < 0) return JSON.stringify([src]);
  157. var name = src.slice(0, i);
  158. var args = src.charAt(i) === ':' ? src.slice(i + 1) : src.slice(i + 1, -1);
  159. args = this.parseExpr(args, {terms: true});
  160. return '[' + JSON.stringify(name) + ',' + args + ']';
  161. };
  162. Parser.prototype.extractEnt = function (src, regex, placeholder) {
  163. var subs = [], isFunc = typeof placeholder == 'function';
  164. src = src.replace(regex, function (str) {
  165. var replacement = isFunc ? placeholder(str) : placeholder;
  166. if (replacement) {
  167. subs.push(str);
  168. return replacement;
  169. }
  170. return str;
  171. });
  172. return {src: src, subs: subs};
  173. };
  174. Parser.prototype.injectEnt = function (extracted, placeholder) {
  175. var src = extracted.src, subs = extracted.subs, isArr = Array.isArray(src);
  176. var arr = (isArr) ? src : [src];
  177. var re = new RegExp('[' + placeholder + ']', 'g'), i = 0;
  178. arr.forEach(function (src, index) {
  179. arr[index] = src.replace(re, function () {
  180. return subs[i++];
  181. });
  182. });
  183. return isArr ? arr : arr[0];
  184. };
  185. //replace complex literals without mistaking subscript notation with array literals
  186. Parser.prototype.replaceComplex = function (s) {
  187. var parsed = this.extractEnt(s, /i(\.i|\[[@#i]\])+/g, 'v');
  188. parsed.src = parsed.src.replace(NON_PRIMITIVES, '~');
  189. return this.injectEnt(parsed, 'v');
  190. };
  191. //parse expression containing literals (including objects/arrays) and variables (including dot and subscript notation)
  192. //valid expressions: `a + 1 > b.c or c == null`, `a and b[1] != c`, `(a < b) or (c < d and e)`, 'a || [1]`
  193. Parser.prototype.parseExpr = function (src, opts) {
  194. opts = opts || {};
  195. //extract string literals -> @
  196. var parsed1 = this.extractEnt(src, STRINGS, '@');
  197. //note: this will catch {not: 1} and a.is; could we replace temporarily and then check adjacent chars?
  198. parsed1.src = parsed1.src.replace(EOPS, function (s, before, op, after) {
  199. return (op in operators) ? before + operators[op] + after : s;
  200. });
  201. //sub out non-string literals (numbers/true/false/null) -> #
  202. // the distinction is necessary because @ can be object identifiers, # cannot
  203. var parsed2 = this.extractEnt(parsed1.src, IDENTS_AND_NUMS, function (s) {
  204. return (s in constants || NUMBER.test(s)) ? '#' : null;
  205. });
  206. //sub out object/variable identifiers -> i
  207. var parsed3 = this.extractEnt(parsed2.src, IDENTIFIERS, 'i');
  208. //remove white-space
  209. parsed3.src = parsed3.src.replace(/\s+/g, '');
  210. //the rest of this is simply to boil the expression down and check validity
  211. var simplified = parsed3.src;
  212. //sub out complex literals (objects/arrays) -> ~
  213. // the distinction is necessary because @ and # can be subscripts but ~ cannot
  214. while (simplified !== (simplified = this.replaceComplex(simplified))) ;
  215. //now @ represents strings, # represents other primitives and ~ represents non-primitives
  216. //replace complex variables (those with dot/subscript accessors) -> v
  217. while (simplified !== (simplified = simplified.replace(/i(\.i|\[[@#i]\])+/, 'v'))) ;
  218. //empty subscript or complex variables in subscript, are not permitted
  219. simplified = simplified.replace(/[iv]\[v?\]/g, 'x');
  220. //sub in "i" for @ and # and ~ and v (now "i" represents all literals, variables and identifiers)
  221. simplified = simplified.replace(/[@#~v]/g, 'i');
  222. //sub out operators
  223. simplified = simplified.replace(OPERATORS, '%');
  224. //allow 'not' unary operator
  225. simplified = simplified.replace(/!+[i]/g, 'i');
  226. var terms = opts.terms ? simplified.split(',') : [simplified];
  227. terms.forEach(function (term) {
  228. //simplify logical grouping
  229. while (term !== (term = term.replace(/\(i(%i)*\)/g, 'i'))) ;
  230. if (!term.match(/^i(%i)*/)) {
  231. throw new Error('Invalid expression: ' + src + " " + term);
  232. }
  233. });
  234. parsed3.src = parsed3.src.replace(VARIABLES, this.parseVar.bind(this));
  235. parsed2.src = this.injectEnt(parsed3, 'i');
  236. parsed1.src = this.injectEnt(parsed2, '#');
  237. return this.injectEnt(parsed1, '@');
  238. };
  239. Parser.prototype.parseVar = function (src) {
  240. var args = Array.prototype.slice.call(arguments);
  241. var str = args.pop(), index = args.pop();
  242. //quote bare object identifiers (might be a reserved word like {while: 1})
  243. if (src === 'i' && str.charAt(index + 1) === ':') {
  244. return '"i"';
  245. }
  246. var parts = ['"i"'];
  247. src.replace(ACCESSOR, function (part) {
  248. if (part === '.i') {
  249. parts.push('"i"');
  250. } else if (part === '[i]') {
  251. parts.push('get("i")');
  252. } else {
  253. parts.push(part.slice(1, -1));
  254. }
  255. });
  256. return 'get(' + parts.join(',') + ')';
  257. };
  258. //escapes a name to be used as a javascript identifier
  259. Parser.prototype.escName = function (str) {
  260. return str.replace(/\W/g, function (s) {
  261. return '$' + s.charCodeAt(0).toString(16);
  262. });
  263. };
  264. Parser.prototype.parseQuoted = function (str) {
  265. if (str.charAt(0) === "'") {
  266. str = str.slice(1, -1).replace(/\\.|"/, function (s) {
  267. if (s === "\\'") return "'";
  268. return s.charAt(0) === '\\' ? s : ('\\' + s);
  269. });
  270. str = '"' + str + '"';
  271. }
  272. //todo: try/catch or deal with invalid characters (linebreaks, control characters)
  273. return JSON.parse(str);
  274. };
  275. //the context 'this' inside tagHandlers is the parser instance
  276. var tagHandlers = {
  277. 'if': function (expr) {
  278. this.push('if (' + this.parseExpr(expr) + ') {');
  279. this.nest.unshift('if');
  280. },
  281. 'else': function () {
  282. if (this.nest[0] === 'for') {
  283. this.push('}, function() {');
  284. } else {
  285. this.push('} else {');
  286. }
  287. },
  288. 'elseif': function (expr) {
  289. this.push('} else if (' + this.parseExpr(expr) + ') {');
  290. },
  291. 'endif': function () {
  292. this.nest.shift();
  293. this.push('}');
  294. },
  295. 'for': function (str) {
  296. var i = str.indexOf(' in ');
  297. var name = str.slice(0, i).trim();
  298. var expr = str.slice(i + 4).trim();
  299. this.push('each(' + this.parseExpr(expr) + ',' + JSON.stringify(name) + ',function() {');
  300. this.nest.unshift('for');
  301. },
  302. 'endfor': function () {
  303. this.nest.shift();
  304. this.push('});');
  305. },
  306. 'raw': function () {
  307. this.rawMode = true;
  308. },
  309. 'endraw': function () {
  310. this.rawMode = false;
  311. },
  312. 'set': function (stmt) {
  313. var i = stmt.indexOf('=');
  314. var name = stmt.slice(0, i).trim();
  315. var expr = stmt.slice(i + 1).trim();
  316. this.push('set(' + JSON.stringify(name) + ',' + this.parseExpr(expr) + ');');
  317. },
  318. 'block': function (name) {
  319. if (this.isParent) {
  320. ++this.parentBlocks;
  321. var blockName = 'block_' + (this.escName(name) || this.parentBlocks);
  322. this.push('block(typeof ' + blockName + ' == "function" ? ' + blockName + ' : function() {');
  323. } else if (this.hasParent) {
  324. this.isSilent = false;
  325. ++this.childBlocks;
  326. blockName = 'block_' + (this.escName(name) || this.childBlocks);
  327. this.push('function ' + blockName + '() {');
  328. }
  329. this.nest.unshift('block');
  330. },
  331. 'endblock': function () {
  332. this.nest.shift();
  333. if (this.isParent) {
  334. this.push('});');
  335. } else if (this.hasParent) {
  336. this.push('}');
  337. this.isSilent = true;
  338. }
  339. },
  340. 'extends': function (name) {
  341. name = this.parseQuoted(name);
  342. var parentSrc = this.readTemplateFile(name);
  343. this.isParent = true;
  344. this.tokenize(parentSrc);
  345. this.isParent = false;
  346. this.hasParent = true;
  347. //silence output until we enter a child block
  348. this.isSilent = true;
  349. },
  350. 'include': function (name) {
  351. name = this.parseQuoted(name);
  352. var incSrc = this.readTemplateFile(name);
  353. this.isInclude = true;
  354. this.tokenize(incSrc);
  355. this.isInclude = false;
  356. }
  357. };
  358. //liquid style
  359. tagHandlers.assign = tagHandlers.set;
  360. //python/django style
  361. tagHandlers.elif = tagHandlers.elseif;
  362. var getRuntime = function runtime(data, opts) {
  363. var defaults = {autoEscape: 'toJson'};
  364. var _toString = Object.prototype.toString;
  365. var _hasOwnProperty = Object.prototype.hasOwnProperty;
  366. var getKeys = Object.keys || function (obj) {
  367. var keys = [];
  368. for (var n in obj) if (_hasOwnProperty.call(obj, n)) keys.push(n);
  369. return keys;
  370. };
  371. var isArray = Array.isArray || function (obj) {
  372. return _toString.call(obj) === '[object Array]';
  373. };
  374. var create = Object.create || function (obj) {
  375. function F() {
  376. }
  377. F.prototype = obj;
  378. return new F();
  379. };
  380. var toString = function (val) {
  381. if (val == null) return '';
  382. return (typeof val.toString == 'function') ? val.toString() : _toString.call(val);
  383. };
  384. var extend = function (dest, src) {
  385. var keys = getKeys(src);
  386. for (var i = 0, len = keys.length; i < len; i++) {
  387. var key = keys[i];
  388. dest[key] = src[key];
  389. }
  390. return dest;
  391. };
  392. //get a value, lexically, starting in current context; a.b -> get("a","b")
  393. var get = function () {
  394. var val, n = arguments[0], c = stack.length;
  395. while (c--) {
  396. val = stack[c][n];
  397. if (typeof val != 'undefined') break;
  398. }
  399. for (var i = 1, len = arguments.length; i < len; i++) {
  400. if (val == null) continue;
  401. n = arguments[i];
  402. val = (_hasOwnProperty.call(val, n)) ? val[n] : (typeof val._get == 'function' ? (val[n] = val._get(n)) : null);
  403. }
  404. return (val == null) ? '' : val;
  405. };
  406. var set = function (n, val) {
  407. stack[stack.length - 1][n] = val;
  408. };
  409. var push = function (ctx) {
  410. stack.push(ctx || {});
  411. };
  412. var pop = function () {
  413. stack.pop();
  414. };
  415. var write = function (str) {
  416. output.push(str);
  417. };
  418. var filter = function (val) {
  419. for (var i = 1, len = arguments.length; i < len; i++) {
  420. var arr = arguments[i], name = arr[0], filter = filters[name];
  421. if (filter) {
  422. arr[0] = val;
  423. //now arr looks like [val, arg1, arg2]
  424. val = filter.apply(data, arr);
  425. } else {
  426. throw new Error('Invalid filter: ' + name);
  427. }
  428. }
  429. if (opts.autoEscape && name !== opts.autoEscape && name !== 'safe') {
  430. //auto escape if not explicitly safe or already escaped
  431. val = filters[opts.autoEscape].call(data, val);
  432. }
  433. output.push(val);
  434. };
  435. var each = function (obj, loopvar, fn1, fn2) {
  436. if (obj == null) return;
  437. var arr = isArray(obj) ? obj : getKeys(obj), len = arr.length;
  438. var ctx = {loop: {length: len, first: arr[0], last: arr[len - 1]}};
  439. push(ctx);
  440. for (var i = 0; i < len; i++) {
  441. extend(ctx.loop, {index: i + 1, index0: i});
  442. fn1(ctx[loopvar] = arr[i]);
  443. }
  444. if (len === 0 && fn2) fn2();
  445. pop();
  446. };
  447. var block = function (fn) {
  448. push();
  449. fn();
  450. pop();
  451. };
  452. var render = function () {
  453. return output.join('');
  454. };
  455. data = data || {};
  456. opts = extend(defaults, opts || {});
  457. var filters = extend({
  458. html: function (val) {
  459. return toString(val)
  460. .split('&').join('&amp;')
  461. .split('<').join('&lt;')
  462. .split('>').join('&gt;')
  463. .split('"').join('&quot;');
  464. },
  465. safe: function (val) {
  466. return val;
  467. },
  468. toJson: function (val) {
  469. if (typeof val === 'object') {
  470. return JSON.stringify(val);
  471. }
  472. return toString(val);
  473. }
  474. }, opts.filters || {});
  475. var stack = [create(data || {})], output = [];
  476. return {
  477. get: get,
  478. set: set,
  479. push: push,
  480. pop: pop,
  481. write: write,
  482. filter: filter,
  483. each: each,
  484. block: block,
  485. render: render
  486. };
  487. };
  488. var runtime;
  489. jinja.compile = function (markup, opts) {
  490. opts = opts || {};
  491. var parser = new Parser();
  492. parser.readTemplateFile = this.readTemplateFile;
  493. var code = [];
  494. code.push('function render($) {');
  495. code.push('var get = $.get, set = $.set, push = $.push, pop = $.pop, write = $.write, filter = $.filter, each = $.each, block = $.block;');
  496. code.push.apply(code, parser.parse(markup));
  497. code.push('return $.render();');
  498. code.push('}');
  499. code = code.join('\n');
  500. if (opts.runtime === false) {
  501. var fn = new Function('data', 'options', 'return (' + code + ')(runtime(data, options))');
  502. } else {
  503. runtime = runtime || (runtime = getRuntime.toString());
  504. fn = new Function('data', 'options', 'return (' + code + ')((' + runtime + ')(data, options))');
  505. }
  506. return {render: fn};
  507. };
  508. jinja.render = function (markup, data, opts) {
  509. var tmpl = jinja.compile(markup);
  510. return tmpl.render(data, opts);
  511. };
  512. jinja.templateFiles = [];
  513. jinja.readTemplateFile = function (name) {
  514. var templateFiles = this.templateFiles || [];
  515. var templateFile = templateFiles[name];
  516. if (templateFile == null) {
  517. throw new Error('Template file not found: ' + name);
  518. }
  519. return templateFile;
  520. };
  521. /*!
  522. * Helpers
  523. */
  524. function trimLeft(str) {
  525. return str.replace(LEADING_SPACE, '');
  526. }
  527. function trimRight(str) {
  528. return str.replace(TRAILING_SPACE, '');
  529. }
  530. function matchAll(str, reg, fn) {
  531. //copy as global
  532. reg = new RegExp(reg.source, 'g' + (reg.ignoreCase ? 'i' : '') + (reg.multiline ? 'm' : ''));
  533. var match;
  534. while ((match = reg.exec(str))) {
  535. var result = fn(match[0], match.index, str);
  536. if (typeof result == 'number') {
  537. reg.lastIndex = result;
  538. }
  539. }
  540. }
  541. }));