jinja.js 23 KB

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