jinja.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  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) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.jinja = {}));
  24. })(this, (function(jinja) {
  25. "use strict";
  26. var STRINGS = /'(\\.|[^'])*'|"(\\.|[^"'"])*"/g;
  27. var IDENTS_AND_NUMS = /([$_a-z][$\w]*)|([+-]?\d+(\.\d+)?)/g;
  28. var NUMBER = /^[+-]?\d+(\.\d+)?$/;
  29. //non-primitive literals (array and object literals)
  30. var NON_PRIMITIVES = /\[[@#~](,[@#~])*\]|\[\]|\{([@i]:[@#~])(,[@i]:[@#~])*\}|\{\}/g;
  31. //bare identifiers such as variables and in object literals: {foo: 'value'}
  32. var IDENTIFIERS = /[$_a-z][$\w]*/ig;
  33. var VARIABLES = /i(\.i|\[[@#i]\])*/g;
  34. var ACCESSOR = /(\.i|\[[@#i]\])/g;
  35. var OPERATORS = /(===?|!==?|>=?|<=?|&&|\|\||[+\-\*\/%])/g;
  36. //extended (english) operators
  37. var EOPS = /(^|[^$\w])(and|or|not|is|isnot)([^$\w]|$)/g;
  38. var LEADING_SPACE = /^\s+/;
  39. var TRAILING_SPACE = /\s+$/;
  40. var START_TOKEN = /\{\{\{|\{\{|\{%|\{#/;
  41. var TAGS = {
  42. '{{{': /^('(\\.|[^'])*'|"(\\.|[^"'"])*"|.)+?\}\}\}/,
  43. '{{': /^('(\\.|[^'])*'|"(\\.|[^"'"])*"|.)+?\}\}/,
  44. '{%': /^('(\\.|[^'])*'|"(\\.|[^"'"])*"|.)+?%\}/,
  45. '{#': /^('(\\.|[^'])*'|"(\\.|[^"'"])*"|.)+?#\}/
  46. };
  47. var delimeters = {
  48. '{%': 'directive',
  49. '{{': 'output',
  50. '{#': 'comment'
  51. };
  52. var operators = {
  53. and: '&&',
  54. or: '||',
  55. not: '!',
  56. is: '==',
  57. isnot: '!='
  58. };
  59. var constants = {
  60. 'true': true,
  61. 'false': false,
  62. 'null': null
  63. };
  64. function Parser() {
  65. this.nest = [];
  66. this.compiled = [];
  67. this.childBlocks = 0;
  68. this.parentBlocks = 0;
  69. this.isSilent = false;
  70. }
  71. Parser.prototype.push = function(line) {
  72. if (!this.isSilent) {
  73. this.compiled.push(line);
  74. }
  75. };
  76. Parser.prototype.parse = function(src) {
  77. this.tokenize(src);
  78. return this.compiled;
  79. };
  80. Parser.prototype.tokenize = function(src) {
  81. var lastEnd = 0,
  82. parser = this,
  83. 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, {
  160. terms: true
  161. });
  162. return '[' + JSON.stringify(name) + ',' + args + ']';
  163. };
  164. Parser.prototype.extractEnt = function(src, regex, placeholder) {
  165. var subs = [],
  166. isFunc = typeof placeholder == 'function';
  167. src = src.replace(regex, function(str) {
  168. var replacement = isFunc ? placeholder(str) : placeholder;
  169. if (replacement) {
  170. subs.push(str);
  171. return replacement;
  172. }
  173. return str;
  174. });
  175. return {
  176. src: src,
  177. subs: subs
  178. };
  179. };
  180. Parser.prototype.injectEnt = function(extracted, placeholder) {
  181. var src = extracted.src,
  182. subs = extracted.subs,
  183. isArr = Array.isArray(src);
  184. var arr = (isArr) ? src : [src];
  185. var re = new RegExp('[' + placeholder + ']', 'g'),
  186. i = 0;
  187. arr.forEach(function(src, index) {
  188. arr[index] = src.replace(re, function() {
  189. return subs[i++];
  190. });
  191. });
  192. return isArr ? arr : arr[0];
  193. };
  194. //replace complex literals without mistaking subscript notation with array literals
  195. Parser.prototype.replaceComplex = function(s) {
  196. var parsed = this.extractEnt(s, /i(\.i|\[[@#i]\])+/g, 'v');
  197. parsed.src = parsed.src.replace(NON_PRIMITIVES, '~');
  198. return this.injectEnt(parsed, 'v');
  199. };
  200. //parse expression containing literals (including objects/arrays) and variables (including dot and subscript notation)
  201. //valid expressions: `a + 1 > b.c or c == null`, `a and b[1] != c`, `(a < b) or (c < d and e)`, 'a || [1]`
  202. Parser.prototype.parseExpr = function(src, opts) {
  203. opts = opts || {};
  204. //extract string literals -> @
  205. var parsed1 = this.extractEnt(src, STRINGS, '@');
  206. //note: this will catch {not: 1} and a.is; could we replace temporarily and then check adjacent chars?
  207. parsed1.src = parsed1.src.replace(EOPS, function(s, before, op, after) {
  208. return (op in operators) ? before + operators[op] + after : s;
  209. });
  210. //sub out non-string literals (numbers/true/false/null) -> #
  211. // the distinction is necessary because @ can be object identifiers, # cannot
  212. var parsed2 = this.extractEnt(parsed1.src, IDENTS_AND_NUMS, function(s) {
  213. return (s in constants || NUMBER.test(s)) ? '#' : null;
  214. });
  215. //sub out object/variable identifiers -> i
  216. var parsed3 = this.extractEnt(parsed2.src, IDENTIFIERS, 'i');
  217. //remove white-space
  218. parsed3.src = parsed3.src.replace(/\s+/g, '');
  219. //the rest of this is simply to boil the expression down and check validity
  220. var simplified = parsed3.src;
  221. //sub out complex literals (objects/arrays) -> ~
  222. // the distinction is necessary because @ and # can be subscripts but ~ cannot
  223. while (simplified !== (simplified = this.replaceComplex(simplified)));
  224. //now @ represents strings, # represents other primitives and ~ represents non-primitives
  225. //replace complex variables (those with dot/subscript accessors) -> v
  226. while (simplified !== (simplified = simplified.replace(/i(\.i|\[[@#i]\])+/, 'v')));
  227. //empty subscript or complex variables in subscript, are not permitted
  228. simplified = simplified.replace(/[iv]\[v?\]/g, 'x');
  229. //sub in "i" for @ and # and ~ and v (now "i" represents all literals, variables and identifiers)
  230. simplified = simplified.replace(/[@#~v]/g, 'i');
  231. //sub out operators
  232. simplified = simplified.replace(OPERATORS, '%');
  233. //allow 'not' unary operator
  234. simplified = simplified.replace(/!+[i]/g, 'i');
  235. var terms = opts.terms ? simplified.split(',') : [simplified];
  236. terms.forEach(function(term) {
  237. //simplify logical grouping
  238. while (term !== (term = term.replace(/\(i(%i)*\)/g, 'i')));
  239. if (!term.match(/^i(%i)*/)) {
  240. throw new Error('Invalid expression: ' + src + " " + term);
  241. }
  242. });
  243. parsed3.src = parsed3.src.replace(VARIABLES, this.parseVar.bind(this));
  244. parsed2.src = this.injectEnt(parsed3, 'i');
  245. parsed1.src = this.injectEnt(parsed2, '#');
  246. return this.injectEnt(parsed1, '@');
  247. };
  248. Parser.prototype.parseVar = function(src) {
  249. var args = Array.prototype.slice.call(arguments);
  250. var str = args.pop(),
  251. index = args.pop();
  252. //quote bare object identifiers (might be a reserved word like {while: 1})
  253. if (src === 'i' && str.charAt(index + 1) === ':') {
  254. return '"i"';
  255. }
  256. var parts = ['"i"'];
  257. src.replace(ACCESSOR, function(part) {
  258. if (part === '.i') {
  259. parts.push('"i"');
  260. } else if (part === '[i]') {
  261. parts.push('get("i")');
  262. } else {
  263. parts.push(part.slice(1, -1));
  264. }
  265. });
  266. return 'get(' + parts.join(',') + ')';
  267. };
  268. //escapes a name to be used as a javascript identifier
  269. Parser.prototype.escName = function(str) {
  270. return str.replace(/\W/g, function(s) {
  271. return '$' + s.charCodeAt(0).toString(16);
  272. });
  273. };
  274. Parser.prototype.parseQuoted = function(str) {
  275. if (str.charAt(0) === "'") {
  276. str = str.slice(1, -1).replace(/\\.|"/, function(s) {
  277. if (s === "\\'") return "'";
  278. return s.charAt(0) === '\\' ? s : ('\\' + s);
  279. });
  280. str = '"' + str + '"';
  281. }
  282. //todo: try/catch or deal with invalid characters (linebreaks, control characters)
  283. return JSON.parse(str);
  284. };
  285. //the context 'this' inside tagHandlers is the parser instance
  286. var tagHandlers = {
  287. 'if': function(expr) {
  288. this.push('if (' + this.parseExpr(expr) + ') {');
  289. this.nest.unshift('if');
  290. },
  291. 'else': function() {
  292. if (this.nest[0] === 'for') {
  293. this.push('}, function() {');
  294. } else {
  295. this.push('} else {');
  296. }
  297. },
  298. 'elseif': function(expr) {
  299. this.push('} else if (' + this.parseExpr(expr) + ') {');
  300. },
  301. 'endif': function() {
  302. this.nest.shift();
  303. this.push('}');
  304. },
  305. 'for': function(str) {
  306. var i = str.indexOf(' in ');
  307. var name = str.slice(0, i).trim();
  308. var expr = str.slice(i + 4).trim();
  309. this.push('each(' + this.parseExpr(expr) + ',' + JSON.stringify(name) + ',function() {');
  310. this.nest.unshift('for');
  311. },
  312. 'endfor': function() {
  313. this.nest.shift();
  314. this.push('});');
  315. },
  316. 'raw': function() {
  317. this.rawMode = true;
  318. },
  319. 'endraw': function() {
  320. this.rawMode = false;
  321. },
  322. 'set': function(stmt) {
  323. var i = stmt.indexOf('=');
  324. var name = stmt.slice(0, i).trim();
  325. var expr = stmt.slice(i + 1).trim();
  326. this.push('set(' + JSON.stringify(name) + ',' + this.parseExpr(expr) + ');');
  327. },
  328. 'block': function(name) {
  329. if (this.isParent) {
  330. ++this.parentBlocks;
  331. var blockName = 'block_' + (this.escName(name) || this.parentBlocks);
  332. this.push('block(typeof ' + blockName + ' == "function" ? ' + blockName + ' : function() {');
  333. } else if (this.hasParent) {
  334. this.isSilent = false;
  335. ++this.childBlocks;
  336. blockName = 'block_' + (this.escName(name) || this.childBlocks);
  337. this.push('function ' + blockName + '() {');
  338. }
  339. this.nest.unshift('block');
  340. },
  341. 'endblock': function() {
  342. this.nest.shift();
  343. if (this.isParent) {
  344. this.push('});');
  345. } else if (this.hasParent) {
  346. this.push('}');
  347. this.isSilent = true;
  348. }
  349. },
  350. 'extends': function(name) {
  351. name = this.parseQuoted(name);
  352. var parentSrc = this.readTemplateFile(name);
  353. this.isParent = true;
  354. this.tokenize(parentSrc);
  355. this.isParent = false;
  356. this.hasParent = true;
  357. //silence output until we enter a child block
  358. this.isSilent = true;
  359. },
  360. 'include': function(name) {
  361. name = this.parseQuoted(name);
  362. var incSrc = this.readTemplateFile(name);
  363. this.isInclude = true;
  364. this.tokenize(incSrc);
  365. this.isInclude = false;
  366. }
  367. };
  368. //liquid style
  369. tagHandlers.assign = tagHandlers.set;
  370. //python/django style
  371. tagHandlers.elif = tagHandlers.elseif;
  372. var getRuntime = function runtime(data, opts) {
  373. var defaults = {
  374. autoEscape: 'toJson'
  375. };
  376. var _toString = Object.prototype.toString;
  377. var _hasOwnProperty = Object.prototype.hasOwnProperty;
  378. var getKeys = Object.keys || function(obj) {
  379. var keys = [];
  380. for (var n in obj) if (_hasOwnProperty.call(obj, n)) keys.push(n);
  381. return keys;
  382. };
  383. var isArray = Array.isArray || function(obj) {
  384. return _toString.call(obj) === '[object Array]';
  385. };
  386. var create = Object.create || function(obj) {
  387. function F() {}
  388. F.prototype = obj;
  389. return new F();
  390. };
  391. var toString = function(val) {
  392. if (val == null) return '';
  393. return (typeof val.toString == 'function') ? val.toString() : _toString.call(val);
  394. };
  395. var extend = function(dest, src) {
  396. var keys = getKeys(src);
  397. for (var i = 0, len = keys.length; i < len; i++) {
  398. var key = keys[i];
  399. dest[key] = src[key];
  400. }
  401. return dest;
  402. };
  403. //get a value, lexically, starting in current context; a.b -> get("a","b")
  404. var get = function() {
  405. var val, n = arguments[0],
  406. c = stack.length;
  407. while (c--) {
  408. val = stack[c][n];
  409. if (typeof val != 'undefined') break;
  410. }
  411. for (var i = 1, len = arguments.length; i < len; i++) {
  412. if (val == null) continue;
  413. n = arguments[i];
  414. val = (_hasOwnProperty.call(val, n)) ? val[n] : (typeof val._get == 'function' ? (val[n] = val._get(n)) : null);
  415. }
  416. return (val == null) ? '' : val;
  417. };
  418. var set = function(n, val) {
  419. stack[stack.length - 1][n] = val;
  420. };
  421. var push = function(ctx) {
  422. stack.push(ctx || {});
  423. };
  424. var pop = function() {
  425. stack.pop();
  426. };
  427. var write = function(str) {
  428. output.push(str);
  429. };
  430. var filter = function(val) {
  431. for (var i = 1, len = arguments.length; i < len; i++) {
  432. var arr = arguments[i],
  433. name = arr[0],
  434. filter = filters[name];
  435. if (filter) {
  436. arr[0] = val;
  437. //now arr looks like [val, arg1, arg2]
  438. val = filter.apply(data, arr);
  439. } else {
  440. throw new Error('Invalid filter: ' + name);
  441. }
  442. }
  443. if (opts.autoEscape && name !== opts.autoEscape && name !== 'safe') {
  444. //auto escape if not explicitly safe or already escaped
  445. val = filters[opts.autoEscape].call(data, val);
  446. }
  447. output.push(val);
  448. };
  449. var each = function(obj, loopvar, fn1, fn2) {
  450. if (obj == null) return;
  451. var arr = isArray(obj) ? obj : getKeys(obj),
  452. len = arr.length;
  453. var ctx = {
  454. loop: {
  455. length: len,
  456. first: arr[0],
  457. last: arr[len - 1]
  458. }
  459. };
  460. push(ctx);
  461. for (var i = 0; i < len; i++) {
  462. extend(ctx.loop, {
  463. index: i + 1,
  464. index0: i
  465. });
  466. fn1(ctx[loopvar] = arr[i]);
  467. }
  468. if (len === 0 && fn2) fn2();
  469. pop();
  470. };
  471. var block = function(fn) {
  472. push();
  473. fn();
  474. pop();
  475. };
  476. var render = function() {
  477. return output.join('');
  478. };
  479. data = data || {};
  480. opts = extend(defaults, opts || {});
  481. var filters = extend({
  482. html: function(val) {
  483. return toString(val)
  484. .split('&').join('&amp;')
  485. .split('<').join('&lt;')
  486. .split('>').join('&gt;')
  487. .split('"').join('&quot;');
  488. },
  489. safe: function(val) {
  490. return val;
  491. },
  492. toJson: function(val) {
  493. if (typeof val === 'object') {
  494. return JSON.stringify(val);
  495. }
  496. return toString(val);
  497. }
  498. }, opts.filters || {});
  499. var stack = [create(data || {})],
  500. output = [];
  501. return {
  502. get: get,
  503. set: set,
  504. push: push,
  505. pop: pop,
  506. write: write,
  507. filter: filter,
  508. each: each,
  509. block: block,
  510. render: render
  511. };
  512. };
  513. var runtime;
  514. jinja.compile = function(markup, opts) {
  515. opts = opts || {};
  516. var parser = new Parser();
  517. parser.readTemplateFile = this.readTemplateFile;
  518. var code = [];
  519. code.push('function render($) {');
  520. code.push('var get = $.get, set = $.set, push = $.push, pop = $.pop, write = $.write, filter = $.filter, each = $.each, block = $.block;');
  521. code.push.apply(code, parser.parse(markup));
  522. code.push('return $.render();');
  523. code.push('}');
  524. code = code.join('\n');
  525. if (opts.runtime === false) {
  526. var fn = new Function('data', 'options', 'return (' + code + ')(runtime(data, options))');
  527. } else {
  528. runtime = runtime || (runtime = getRuntime.toString());
  529. fn = new Function('data', 'options', 'return (' + code + ')((' + runtime + ')(data, options))');
  530. }
  531. return {
  532. render: fn
  533. };
  534. };
  535. jinja.render = function(markup, data, opts) {
  536. var tmpl = jinja.compile(markup);
  537. return tmpl.render(data, opts);
  538. };
  539. jinja.templateFiles = [];
  540. jinja.readTemplateFile = function(name) {
  541. var templateFiles = this.templateFiles || [];
  542. var templateFile = templateFiles[name];
  543. if (templateFile == null) {
  544. throw new Error('Template file not found: ' + name);
  545. }
  546. return templateFile;
  547. };
  548. /*!
  549. * Helpers
  550. */
  551. function trimLeft(str) {
  552. return str.replace(LEADING_SPACE, '');
  553. }
  554. function trimRight(str) {
  555. return str.replace(TRAILING_SPACE, '');
  556. }
  557. function matchAll(str, reg, fn) {
  558. //copy as global
  559. reg = new RegExp(reg.source, 'g' + (reg.ignoreCase ? 'i' : '') + (reg.multiline ? 'm' : ''));
  560. var match;
  561. while ((match = reg.exec(str))) {
  562. var result = fn(match[0], match.index, str);
  563. if (typeof result == 'number') {
  564. reg.lastIndex = result;
  565. }
  566. }
  567. }
  568. }));