A Twitch.tv viewer reward and games system.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

672 lines
17 KiB

  1. 'use strict';
  2. var nodes = require('./nodes');
  3. var filters = require('./filters');
  4. var doctypes = require('./doctypes');
  5. var selfClosing = require('./self-closing');
  6. var runtime = require('./runtime');
  7. var utils = require('./utils');
  8. var parseJSExpression = require('character-parser').parseMax;
  9. var isConstant = require('constantinople');
  10. var toConstant = require('constantinople').toConstant;
  11. /**
  12. * Initialize `Compiler` with the given `node`.
  13. *
  14. * @param {Node} node
  15. * @param {Object} options
  16. * @api public
  17. */
  18. var Compiler = module.exports = function Compiler(node, options) {
  19. this.options = options = options || {};
  20. this.node = node;
  21. this.hasCompiledDoctype = false;
  22. this.hasCompiledTag = false;
  23. this.pp = options.pretty || false;
  24. this.debug = false !== options.compileDebug;
  25. this.indents = 0;
  26. this.parentIndents = 0;
  27. this.terse = false;
  28. if (options.doctype) this.setDoctype(options.doctype);
  29. };
  30. /**
  31. * Compiler prototype.
  32. */
  33. Compiler.prototype = {
  34. /**
  35. * Compile parse tree to JavaScript.
  36. *
  37. * @api public
  38. */
  39. compile: function(){
  40. this.buf = [];
  41. if (this.pp) this.buf.push("jade.indent = [];");
  42. this.lastBufferedIdx = -1;
  43. this.visit(this.node);
  44. return this.buf.join('\n');
  45. },
  46. /**
  47. * Sets the default doctype `name`. Sets terse mode to `true` when
  48. * html 5 is used, causing self-closing tags to end with ">" vs "/>",
  49. * and boolean attributes are not mirrored.
  50. *
  51. * @param {string} name
  52. * @api public
  53. */
  54. setDoctype: function(name){
  55. name = name || 'default';
  56. this.doctype = doctypes[name.toLowerCase()] || '<!DOCTYPE ' + name + '>';
  57. this.terse = this.doctype.toLowerCase() == '<!doctype html>';
  58. this.xml = 0 == this.doctype.indexOf('<?xml');
  59. },
  60. /**
  61. * Buffer the given `str` exactly as is or with interpolation
  62. *
  63. * @param {String} str
  64. * @param {Boolean} interpolate
  65. * @api public
  66. */
  67. buffer: function (str, interpolate) {
  68. var self = this;
  69. if (interpolate) {
  70. var match = /(\\)?([#!]){((?:.|\n)*)$/.exec(str);
  71. if (match) {
  72. this.buffer(str.substr(0, match.index), false);
  73. if (match[1]) { // escape
  74. this.buffer(match[2] + '{', false);
  75. this.buffer(match[3], true);
  76. return;
  77. } else {
  78. try {
  79. var rest = match[3];
  80. var range = parseJSExpression(rest);
  81. var code = ('!' == match[2] ? '' : 'jade.escape') + "((jade.interp = " + range.src + ") == null ? '' : jade.interp)";
  82. } catch (ex) {
  83. throw ex;
  84. //didn't match, just as if escaped
  85. this.buffer(match[2] + '{', false);
  86. this.buffer(match[3], true);
  87. return;
  88. }
  89. this.bufferExpression(code);
  90. this.buffer(rest.substr(range.end + 1), true);
  91. return;
  92. }
  93. }
  94. }
  95. str = JSON.stringify(str);
  96. str = str.substr(1, str.length - 2);
  97. if (this.lastBufferedIdx == this.buf.length) {
  98. if (this.lastBufferedType === 'code') this.lastBuffered += ' + "';
  99. this.lastBufferedType = 'text';
  100. this.lastBuffered += str;
  101. this.buf[this.lastBufferedIdx - 1] = 'buf.push(' + this.bufferStartChar + this.lastBuffered + '");'
  102. } else {
  103. this.buf.push('buf.push("' + str + '");');
  104. this.lastBufferedType = 'text';
  105. this.bufferStartChar = '"';
  106. this.lastBuffered = str;
  107. this.lastBufferedIdx = this.buf.length;
  108. }
  109. },
  110. /**
  111. * Buffer the given `src` so it is evaluated at run time
  112. *
  113. * @param {String} src
  114. * @api public
  115. */
  116. bufferExpression: function (src) {
  117. var fn = Function('', 'return (' + src + ');');
  118. if (isConstant(src)) {
  119. return this.buffer(fn(), false)
  120. }
  121. if (this.lastBufferedIdx == this.buf.length) {
  122. if (this.lastBufferedType === 'text') this.lastBuffered += '"';
  123. this.lastBufferedType = 'code';
  124. this.lastBuffered += ' + (' + src + ')';
  125. this.buf[this.lastBufferedIdx - 1] = 'buf.push(' + this.bufferStartChar + this.lastBuffered + ');'
  126. } else {
  127. this.buf.push('buf.push(' + src + ');');
  128. this.lastBufferedType = 'code';
  129. this.bufferStartChar = '';
  130. this.lastBuffered = '(' + src + ')';
  131. this.lastBufferedIdx = this.buf.length;
  132. }
  133. },
  134. /**
  135. * Buffer an indent based on the current `indent`
  136. * property and an additional `offset`.
  137. *
  138. * @param {Number} offset
  139. * @param {Boolean} newline
  140. * @api public
  141. */
  142. prettyIndent: function(offset, newline){
  143. offset = offset || 0;
  144. newline = newline ? '\n' : '';
  145. this.buffer(newline + Array(this.indents + offset).join(' '));
  146. if (this.parentIndents)
  147. this.buf.push("buf.push.apply(buf, jade.indent);");
  148. },
  149. /**
  150. * Visit `node`.
  151. *
  152. * @param {Node} node
  153. * @api public
  154. */
  155. visit: function(node){
  156. var debug = this.debug;
  157. if (debug) {
  158. this.buf.push('jade_debug.unshift({ lineno: ' + node.line
  159. + ', filename: ' + (node.filename
  160. ? JSON.stringify(node.filename)
  161. : 'jade_debug[0].filename')
  162. + ' });');
  163. }
  164. // Massive hack to fix our context
  165. // stack for - else[ if] etc
  166. if (false === node.debug && this.debug) {
  167. this.buf.pop();
  168. this.buf.pop();
  169. }
  170. this.visitNode(node);
  171. if (debug) this.buf.push('jade_debug.shift();');
  172. },
  173. /**
  174. * Visit `node`.
  175. *
  176. * @param {Node} node
  177. * @api public
  178. */
  179. visitNode: function(node){
  180. return this['visit' + node.type](node);
  181. },
  182. /**
  183. * Visit case `node`.
  184. *
  185. * @param {Literal} node
  186. * @api public
  187. */
  188. visitCase: function(node){
  189. var _ = this.withinCase;
  190. this.withinCase = true;
  191. this.buf.push('switch (' + node.expr + '){');
  192. this.visit(node.block);
  193. this.buf.push('}');
  194. this.withinCase = _;
  195. },
  196. /**
  197. * Visit when `node`.
  198. *
  199. * @param {Literal} node
  200. * @api public
  201. */
  202. visitWhen: function(node){
  203. if ('default' == node.expr) {
  204. this.buf.push('default:');
  205. } else {
  206. this.buf.push('case ' + node.expr + ':');
  207. }
  208. this.visit(node.block);
  209. this.buf.push(' break;');
  210. },
  211. /**
  212. * Visit literal `node`.
  213. *
  214. * @param {Literal} node
  215. * @api public
  216. */
  217. visitLiteral: function(node){
  218. this.buffer(node.str);
  219. },
  220. /**
  221. * Visit all nodes in `block`.
  222. *
  223. * @param {Block} block
  224. * @api public
  225. */
  226. visitBlock: function(block){
  227. var len = block.nodes.length
  228. , escape = this.escape
  229. , pp = this.pp
  230. // Pretty print multi-line text
  231. if (pp && len > 1 && !escape && block.nodes[0].isText && block.nodes[1].isText)
  232. this.prettyIndent(1, true);
  233. for (var i = 0; i < len; ++i) {
  234. // Pretty print text
  235. if (pp && i > 0 && !escape && block.nodes[i].isText && block.nodes[i-1].isText)
  236. this.prettyIndent(1, false);
  237. this.visit(block.nodes[i]);
  238. // Multiple text nodes are separated by newlines
  239. if (block.nodes[i+1] && block.nodes[i].isText && block.nodes[i+1].isText)
  240. this.buffer('\n');
  241. }
  242. },
  243. /**
  244. * Visit a mixin's `block` keyword.
  245. *
  246. * @param {MixinBlock} block
  247. * @api public
  248. */
  249. visitMixinBlock: function(block){
  250. if (this.pp) this.buf.push("jade.indent.push('" + Array(this.indents + 1).join(' ') + "');");
  251. this.buf.push('block && block();');
  252. if (this.pp) this.buf.push("jade.indent.pop();");
  253. },
  254. /**
  255. * Visit `doctype`. Sets terse mode to `true` when html 5
  256. * is used, causing self-closing tags to end with ">" vs "/>",
  257. * and boolean attributes are not mirrored.
  258. *
  259. * @param {Doctype} doctype
  260. * @api public
  261. */
  262. visitDoctype: function(doctype){
  263. if (doctype && (doctype.val || !this.doctype)) {
  264. this.setDoctype(doctype.val || 'default');
  265. }
  266. if (this.doctype) this.buffer(this.doctype);
  267. this.hasCompiledDoctype = true;
  268. },
  269. /**
  270. * Visit `mixin`, generating a function that
  271. * may be called within the template.
  272. *
  273. * @param {Mixin} mixin
  274. * @api public
  275. */
  276. visitMixin: function(mixin){
  277. var name = 'jade_mixins[';
  278. var args = mixin.args || '';
  279. var block = mixin.block;
  280. var attrs = mixin.attrs;
  281. var attrsBlocks = mixin.attributeBlocks;
  282. var pp = this.pp;
  283. name += (mixin.name[0]=='#' ? mixin.name.substr(2,mixin.name.length-3):'"'+mixin.name+'"')+']';
  284. if (mixin.call) {
  285. if (pp) this.buf.push("jade.indent.push('" + Array(this.indents + 1).join(' ') + "');")
  286. if (block || attrs.length || attrsBlocks.length) {
  287. this.buf.push(name + '.call({');
  288. if (block) {
  289. this.buf.push('block: function(){');
  290. // Render block with no indents, dynamically added when rendered
  291. this.parentIndents++;
  292. var _indents = this.indents;
  293. this.indents = 0;
  294. this.visit(mixin.block);
  295. this.indents = _indents;
  296. this.parentIndents--;
  297. if (attrs.length || attrsBlocks.length) {
  298. this.buf.push('},');
  299. } else {
  300. this.buf.push('}');
  301. }
  302. }
  303. if (attrsBlocks.length) {
  304. if (attrs.length) {
  305. var val = this.attrs(attrs);
  306. attrsBlocks.unshift(val);
  307. }
  308. this.buf.push('attributes: jade.merge([' + attrsBlocks.join(',') + '])');
  309. } else if (attrs.length) {
  310. var val = this.attrs(attrs);
  311. this.buf.push('attributes: ' + val);
  312. }
  313. if (args) {
  314. this.buf.push('}, ' + args + ');');
  315. } else {
  316. this.buf.push('});');
  317. }
  318. } else {
  319. this.buf.push(name + '(' + args + ');');
  320. }
  321. if (pp) this.buf.push("jade.indent.pop();")
  322. } else {
  323. this.buf.push(name + ' = function(' + args + '){');
  324. this.buf.push('var block = (this && this.block), attributes = (this && this.attributes) || {};');
  325. this.parentIndents++;
  326. this.visit(block);
  327. this.parentIndents--;
  328. this.buf.push('};');
  329. }
  330. },
  331. /**
  332. * Visit `tag` buffering tag markup, generating
  333. * attributes, visiting the `tag`'s code and block.
  334. *
  335. * @param {Tag} tag
  336. * @api public
  337. */
  338. visitTag: function(tag){
  339. this.indents++;
  340. var name = tag.name
  341. , pp = this.pp
  342. , self = this;
  343. function bufferName() {
  344. if (tag.buffer) self.bufferExpression(name);
  345. else self.buffer(name);
  346. }
  347. if ('pre' == tag.name) this.escape = true;
  348. if (!this.hasCompiledTag) {
  349. if (!this.hasCompiledDoctype && 'html' == name) {
  350. this.visitDoctype();
  351. }
  352. this.hasCompiledTag = true;
  353. }
  354. // pretty print
  355. if (pp && !tag.isInline())
  356. this.prettyIndent(0, true);
  357. if ((~selfClosing.indexOf(name) || tag.selfClosing) && !this.xml) {
  358. this.buffer('<');
  359. bufferName();
  360. this.visitAttributes(tag.attrs, tag.attributeBlocks);
  361. this.terse
  362. ? this.buffer('>')
  363. : this.buffer('/>');
  364. // if it is non-empty throw an error
  365. if (tag.block && !(tag.block.type === 'Block' && tag.block.nodes.length === 0)
  366. && tag.block.nodes.some(function (tag) { return tag.type !== 'Text' || !/^\s*$/.test(tag.val)})) {
  367. throw new Error(name + ' is self closing and should not have content.');
  368. }
  369. } else {
  370. // Optimize attributes buffering
  371. this.buffer('<');
  372. bufferName();
  373. this.visitAttributes(tag.attrs, tag.attributeBlocks);
  374. this.buffer('>');
  375. if (tag.code) this.visitCode(tag.code);
  376. this.visit(tag.block);
  377. // pretty print
  378. if (pp && !tag.isInline() && 'pre' != tag.name && !tag.canInline())
  379. this.prettyIndent(0, true);
  380. this.buffer('</');
  381. bufferName();
  382. this.buffer('>');
  383. }
  384. if ('pre' == tag.name) this.escape = false;
  385. this.indents--;
  386. },
  387. /**
  388. * Visit `filter`, throwing when the filter does not exist.
  389. *
  390. * @param {Filter} filter
  391. * @api public
  392. */
  393. visitFilter: function(filter){
  394. var text = filter.block.nodes.map(
  395. function(node){ return node.val; }
  396. ).join('\n');
  397. filter.attrs = filter.attrs || {};
  398. filter.attrs.filename = this.options.filename;
  399. this.buffer(filters(filter.name, text, filter.attrs), true);
  400. },
  401. /**
  402. * Visit `text` node.
  403. *
  404. * @param {Text} text
  405. * @api public
  406. */
  407. visitText: function(text){
  408. this.buffer(text.val, true);
  409. },
  410. /**
  411. * Visit a `comment`, only buffering when the buffer flag is set.
  412. *
  413. * @param {Comment} comment
  414. * @api public
  415. */
  416. visitComment: function(comment){
  417. if (!comment.buffer) return;
  418. if (this.pp) this.prettyIndent(1, true);
  419. this.buffer('<!--' + comment.val + '-->');
  420. },
  421. /**
  422. * Visit a `BlockComment`.
  423. *
  424. * @param {Comment} comment
  425. * @api public
  426. */
  427. visitBlockComment: function(comment){
  428. if (!comment.buffer) return;
  429. if (this.pp) this.prettyIndent(1, true);
  430. this.buffer('<!--' + comment.val);
  431. this.visit(comment.block);
  432. if (this.pp) this.prettyIndent(1, true);
  433. this.buffer('-->');
  434. },
  435. /**
  436. * Visit `code`, respecting buffer / escape flags.
  437. * If the code is followed by a block, wrap it in
  438. * a self-calling function.
  439. *
  440. * @param {Code} code
  441. * @api public
  442. */
  443. visitCode: function(code){
  444. // Wrap code blocks with {}.
  445. // we only wrap unbuffered code blocks ATM
  446. // since they are usually flow control
  447. // Buffer code
  448. if (code.buffer) {
  449. var val = code.val.trimLeft();
  450. val = 'null == (jade.interp = '+val+') ? "" : jade.interp';
  451. if (code.escape) val = 'jade.escape(' + val + ')';
  452. this.bufferExpression(val);
  453. } else {
  454. this.buf.push(code.val);
  455. }
  456. // Block support
  457. if (code.block) {
  458. if (!code.buffer) this.buf.push('{');
  459. this.visit(code.block);
  460. if (!code.buffer) this.buf.push('}');
  461. }
  462. },
  463. /**
  464. * Visit `each` block.
  465. *
  466. * @param {Each} each
  467. * @api public
  468. */
  469. visitEach: function(each){
  470. this.buf.push(''
  471. + '// iterate ' + each.obj + '\n'
  472. + ';(function(){\n'
  473. + ' var $$obj = ' + each.obj + ';\n'
  474. + ' if (\'number\' == typeof $$obj.length) {\n');
  475. if (each.alternative) {
  476. this.buf.push(' if ($$obj.length) {');
  477. }
  478. this.buf.push(''
  479. + ' for (var ' + each.key + ' = 0, $$l = $$obj.length; ' + each.key + ' < $$l; ' + each.key + '++) {\n'
  480. + ' var ' + each.val + ' = $$obj[' + each.key + '];\n');
  481. this.visit(each.block);
  482. this.buf.push(' }\n');
  483. if (each.alternative) {
  484. this.buf.push(' } else {');
  485. this.visit(each.alternative);
  486. this.buf.push(' }');
  487. }
  488. this.buf.push(''
  489. + ' } else {\n'
  490. + ' var $$l = 0;\n'
  491. + ' for (var ' + each.key + ' in $$obj) {\n'
  492. + ' $$l++;'
  493. + ' var ' + each.val + ' = $$obj[' + each.key + '];\n');
  494. this.visit(each.block);
  495. this.buf.push(' }\n');
  496. if (each.alternative) {
  497. this.buf.push(' if ($$l === 0) {');
  498. this.visit(each.alternative);
  499. this.buf.push(' }');
  500. }
  501. this.buf.push(' }\n}).call(this);\n');
  502. },
  503. /**
  504. * Visit `attrs`.
  505. *
  506. * @param {Array} attrs
  507. * @api public
  508. */
  509. visitAttributes: function(attrs, attributeBlocks){
  510. if (attributeBlocks.length) {
  511. if (attrs.length) {
  512. var val = this.attrs(attrs);
  513. attributeBlocks.unshift(val);
  514. }
  515. this.bufferExpression('jade.attrs(jade.merge([' + attributeBlocks.join(',') + ']), ' + JSON.stringify(this.terse) + ')');
  516. } else if (attrs.length) {
  517. this.attrs(attrs, true);
  518. }
  519. },
  520. /**
  521. * Compile attributes.
  522. */
  523. attrs: function(attrs, buffer){
  524. var buf = [];
  525. var classes = [];
  526. var classEscaping = [];
  527. attrs.forEach(function(attr){
  528. var key = attr.name;
  529. var escaped = attr.escaped;
  530. if (key === 'class') {
  531. classes.push(attr.val);
  532. classEscaping.push(attr.escaped);
  533. } else if (isConstant(attr.val)) {
  534. if (buffer) {
  535. this.buffer(runtime.attr(key, toConstant(attr.val), escaped, this.terse));
  536. } else {
  537. var val = toConstant(attr.val);
  538. if (escaped && !(key.indexOf('data') === 0 && typeof val !== 'string')) {
  539. val = runtime.escape(val);
  540. }
  541. buf.push(JSON.stringify(key) + ': ' + JSON.stringify(val));
  542. }
  543. } else {
  544. if (buffer) {
  545. this.bufferExpression('jade.attr("' + key + '", ' + attr.val + ', ' + JSON.stringify(escaped) + ', ' + JSON.stringify(this.terse) + ')');
  546. } else {
  547. var val = attr.val;
  548. if (escaped && !(key.indexOf('data') === 0)) {
  549. val = 'jade.escape(' + val + ')';
  550. } else if (escaped) {
  551. val = '(typeof (jade.interp = ' + val + ') == "string" ? jade.escape(jade.interp) : jade.interp)"';
  552. }
  553. buf.push(JSON.stringify(key) + ': ' + val);
  554. }
  555. }
  556. }.bind(this));
  557. if (buffer) {
  558. if (classes.every(isConstant)) {
  559. this.buffer(runtime.cls(classes.map(toConstant), classEscaping));
  560. } else {
  561. this.bufferExpression('jade.cls([' + classes.join(',') + '], ' + JSON.stringify(classEscaping) + ')');
  562. }
  563. } else {
  564. if (classes.every(isConstant)) {
  565. classes = JSON.stringify(runtime.joinClasses(classes.map(toConstant).map(runtime.joinClasses).map(function (cls, i) {
  566. return classEscaping[i] ? runtime.escape(cls) : cls;
  567. })));
  568. } else if (classes.length) {
  569. classes = '(jade.interp = ' + JSON.stringify(classEscaping) + ',' +
  570. ' jade.joinClasses([' + classes.join(',') + '].map(jade.joinClasses).map(function (cls, i) {' +
  571. ' return jade.interp[i] ? jade.escape(cls) : cls' +
  572. ' }))' +
  573. ')';
  574. }
  575. if (classes.length)
  576. buf.push('"class": ' + classes);
  577. }
  578. return '{' + buf.join(',') + '}';
  579. }
  580. };