index.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837
  1. 'use strict';
  2. var doctypes = require('doctypes');
  3. var makeError = require('pug-error');
  4. var buildRuntime = require('pug-runtime/build');
  5. var runtime = require('pug-runtime');
  6. var compileAttrs = require('pug-attrs');
  7. var selfClosing = require('void-elements');
  8. var constantinople = require('constantinople');
  9. var stringify = require('js-stringify');
  10. var addWith = require('with');
  11. // This is used to prevent pretty printing inside certain tags
  12. var WHITE_SPACE_SENSITIVE_TAGS = {
  13. pre: true,
  14. textarea: true
  15. };
  16. var INTERNAL_VARIABLES = [
  17. 'pug',
  18. 'pug_mixins',
  19. 'pug_interp',
  20. 'pug_debug_filename',
  21. 'pug_debug_line',
  22. 'pug_debug_sources',
  23. 'pug_html'
  24. ];
  25. module.exports = generateCode;
  26. module.exports.CodeGenerator = Compiler;
  27. function generateCode(ast, options) {
  28. return (new Compiler(ast, options)).compile();
  29. }
  30. function isConstant(src) {
  31. return constantinople(src, {pug: runtime, 'pug_interp': undefined});
  32. }
  33. function toConstant(src) {
  34. return constantinople.toConstant(src, {pug: runtime, 'pug_interp': undefined});
  35. }
  36. /**
  37. * Initialize `Compiler` with the given `node`.
  38. *
  39. * @param {Node} node
  40. * @param {Object} options
  41. * @api public
  42. */
  43. function Compiler(node, options) {
  44. this.options = options = options || {};
  45. this.node = node;
  46. this.bufferedConcatenationCount = 0;
  47. this.hasCompiledDoctype = false;
  48. this.hasCompiledTag = false;
  49. this.pp = options.pretty || false;
  50. if (this.pp && typeof this.pp !== 'string') {
  51. this.pp = ' ';
  52. }
  53. if (this.pp && !/^\s+$/.test(this.pp)) {
  54. throw new Error(
  55. 'The pretty parameter should either be a boolean or whitespace only string'
  56. );
  57. }
  58. this.debug = false !== options.compileDebug;
  59. this.indents = 0;
  60. this.parentIndents = 0;
  61. this.terse = false;
  62. this.mixins = {};
  63. this.dynamicMixins = false;
  64. this.eachCount = 0;
  65. if (options.doctype) this.setDoctype(options.doctype);
  66. this.runtimeFunctionsUsed = [];
  67. this.inlineRuntimeFunctions = options.inlineRuntimeFunctions || false;
  68. if (this.debug && this.inlineRuntimeFunctions) {
  69. this.runtimeFunctionsUsed.push('rethrow');
  70. }
  71. };
  72. /**
  73. * Compiler prototype.
  74. */
  75. Compiler.prototype = {
  76. runtime: function (name) {
  77. if (this.inlineRuntimeFunctions) {
  78. this.runtimeFunctionsUsed.push(name);
  79. return 'pug_' + name;
  80. } else {
  81. return 'pug.' + name;
  82. }
  83. },
  84. error: function (message, code, node) {
  85. var err = makeError(code, message, {
  86. line: node.line,
  87. column: node.column,
  88. filename: node.filename,
  89. });
  90. throw err;
  91. },
  92. /**
  93. * Compile parse tree to JavaScript.
  94. *
  95. * @api public
  96. */
  97. compile: function(){
  98. this.buf = [];
  99. if (this.pp) this.buf.push("var pug_indent = [];");
  100. this.lastBufferedIdx = -1;
  101. this.visit(this.node);
  102. if (!this.dynamicMixins) {
  103. // if there are no dynamic mixins we can remove any un-used mixins
  104. var mixinNames = Object.keys(this.mixins);
  105. for (var i = 0; i < mixinNames.length; i++) {
  106. var mixin = this.mixins[mixinNames[i]];
  107. if (!mixin.used) {
  108. for (var x = 0; x < mixin.instances.length; x++) {
  109. for (var y = mixin.instances[x].start; y < mixin.instances[x].end; y++) {
  110. this.buf[y] = '';
  111. }
  112. }
  113. }
  114. }
  115. }
  116. var js = this.buf.join('\n');
  117. var globals = this.options.globals ? this.options.globals.concat(INTERNAL_VARIABLES) : INTERNAL_VARIABLES;
  118. if (this.options.self) {
  119. js = 'var self = locals || {};' + js;
  120. } else {
  121. js = addWith('locals || {}', js, globals.concat(this.runtimeFunctionsUsed.map(function (name) { return 'pug_' + name; })));
  122. }
  123. if (this.debug) {
  124. if (this.options.includeSources) {
  125. js = 'var pug_debug_sources = ' + stringify(this.options.includeSources) + ';\n' + js;
  126. }
  127. js = 'var pug_debug_filename, pug_debug_line;' +
  128. 'try {' +
  129. js +
  130. '} catch (err) {' +
  131. (this.inlineRuntimeFunctions ? 'pug_rethrow' : 'pug.rethrow') +
  132. '(err, pug_debug_filename, pug_debug_line' +
  133. (
  134. this.options.includeSources
  135. ? ', pug_debug_sources[pug_debug_filename]'
  136. : ''
  137. ) +
  138. ');' +
  139. '}';
  140. }
  141. return buildRuntime(this.runtimeFunctionsUsed) + 'function ' + (this.options.templateName || 'template') + '(locals) {var pug_html = "", pug_mixins = {}, pug_interp;' + js + ';return pug_html;}';
  142. },
  143. /**
  144. * Sets the default doctype `name`. Sets terse mode to `true` when
  145. * html 5 is used, causing self-closing tags to end with ">" vs "/>",
  146. * and boolean attributes are not mirrored.
  147. *
  148. * @param {string} name
  149. * @api public
  150. */
  151. setDoctype: function(name){
  152. this.doctype = doctypes[name.toLowerCase()] || '<!DOCTYPE ' + name + '>';
  153. this.terse = this.doctype.toLowerCase() == '<!doctype html>';
  154. this.xml = 0 == this.doctype.indexOf('<?xml');
  155. },
  156. /**
  157. * Buffer the given `str` exactly as is or with interpolation
  158. *
  159. * @param {String} str
  160. * @param {Boolean} interpolate
  161. * @api public
  162. */
  163. buffer: function (str) {
  164. var self = this;
  165. str = stringify(str);
  166. str = str.substr(1, str.length - 2);
  167. if (this.lastBufferedIdx == this.buf.length && this.bufferedConcatenationCount < 100) {
  168. if (this.lastBufferedType === 'code') {
  169. this.lastBuffered += ' + "';
  170. this.bufferedConcatenationCount++;
  171. }
  172. this.lastBufferedType = 'text';
  173. this.lastBuffered += str;
  174. this.buf[this.lastBufferedIdx - 1] = 'pug_html = pug_html + ' + this.bufferStartChar + this.lastBuffered + '";';
  175. } else {
  176. this.bufferedConcatenationCount = 0;
  177. this.buf.push('pug_html = pug_html + "' + str + '";');
  178. this.lastBufferedType = 'text';
  179. this.bufferStartChar = '"';
  180. this.lastBuffered = str;
  181. this.lastBufferedIdx = this.buf.length;
  182. }
  183. },
  184. /**
  185. * Buffer the given `src` so it is evaluated at run time
  186. *
  187. * @param {String} src
  188. * @api public
  189. */
  190. bufferExpression: function (src) {
  191. if (isConstant(src)) {
  192. return this.buffer(toConstant(src) + '')
  193. }
  194. if (this.lastBufferedIdx == this.buf.length && this.bufferedConcatenationCount < 100) {
  195. this.bufferedConcatenationCount++;
  196. if (this.lastBufferedType === 'text') this.lastBuffered += '"';
  197. this.lastBufferedType = 'code';
  198. this.lastBuffered += ' + (' + src + ')';
  199. this.buf[this.lastBufferedIdx - 1] = 'pug_html = pug_html + (' + this.bufferStartChar + this.lastBuffered + ');';
  200. } else {
  201. this.bufferedConcatenationCount = 0;
  202. this.buf.push('pug_html = pug_html + (' + src + ');');
  203. this.lastBufferedType = 'code';
  204. this.bufferStartChar = '';
  205. this.lastBuffered = '(' + src + ')';
  206. this.lastBufferedIdx = this.buf.length;
  207. }
  208. },
  209. /**
  210. * Buffer an indent based on the current `indent`
  211. * property and an additional `offset`.
  212. *
  213. * @param {Number} offset
  214. * @param {Boolean} newline
  215. * @api public
  216. */
  217. prettyIndent: function(offset, newline){
  218. offset = offset || 0;
  219. newline = newline ? '\n' : '';
  220. this.buffer(newline + Array(this.indents + offset).join(this.pp));
  221. if (this.parentIndents)
  222. this.buf.push('pug_html = pug_html + pug_indent.join("");');
  223. },
  224. /**
  225. * Visit `node`.
  226. *
  227. * @param {Node} node
  228. * @api public
  229. */
  230. visit: function(node, parent){
  231. var debug = this.debug;
  232. if (!node) {
  233. var msg;
  234. if (parent) {
  235. msg = 'A child of ' + parent.type + ' (' + (parent.filename || 'Pug') + ':' + parent.line + ')';
  236. } else {
  237. msg = 'A top-level node';
  238. }
  239. msg += ' is ' + node + ', expected a Pug AST Node.';
  240. throw new TypeError(msg);
  241. }
  242. if (debug && node.debug !== false && node.type !== 'Block') {
  243. if (node.line) {
  244. var js = ';pug_debug_line = ' + node.line;
  245. if (node.filename) js += ';pug_debug_filename = ' + stringify(node.filename);
  246. this.buf.push(js + ';');
  247. }
  248. }
  249. if (!this['visit' + node.type]) {
  250. var msg;
  251. if (parent) {
  252. msg = 'A child of ' + parent.type
  253. } else {
  254. msg = 'A top-level node';
  255. }
  256. msg += ' (' + (node.filename || 'Pug') + ':' + node.line + ')'
  257. + ' is of type ' + node.type + ','
  258. + ' which is not supported by pug-code-gen.'
  259. switch (node.type) {
  260. case 'Filter':
  261. msg += ' Please use pug-filters to preprocess this AST.'
  262. break;
  263. case 'Extends':
  264. case 'Include':
  265. case 'NamedBlock':
  266. case 'FileReference': // unlikely but for the sake of completeness
  267. msg += ' Please use pug-linker to preprocess this AST.'
  268. break;
  269. }
  270. throw new TypeError(msg);
  271. }
  272. this.visitNode(node);
  273. },
  274. /**
  275. * Visit `node`.
  276. *
  277. * @param {Node} node
  278. * @api public
  279. */
  280. visitNode: function(node){
  281. return this['visit' + node.type](node);
  282. },
  283. /**
  284. * Visit case `node`.
  285. *
  286. * @param {Literal} node
  287. * @api public
  288. */
  289. visitCase: function(node){
  290. this.buf.push('switch (' + node.expr + '){');
  291. this.visit(node.block, node);
  292. this.buf.push('}');
  293. },
  294. /**
  295. * Visit when `node`.
  296. *
  297. * @param {Literal} node
  298. * @api public
  299. */
  300. visitWhen: function(node){
  301. if ('default' == node.expr) {
  302. this.buf.push('default:');
  303. } else {
  304. this.buf.push('case ' + node.expr + ':');
  305. }
  306. if (node.block) {
  307. this.visit(node.block, node);
  308. this.buf.push(' break;');
  309. }
  310. },
  311. /**
  312. * Visit literal `node`.
  313. *
  314. * @param {Literal} node
  315. * @api public
  316. */
  317. visitLiteral: function(node){
  318. this.buffer(node.str);
  319. },
  320. visitNamedBlock: function(block){
  321. return this.visitBlock(block);
  322. },
  323. /**
  324. * Visit all nodes in `block`.
  325. *
  326. * @param {Block} block
  327. * @api public
  328. */
  329. visitBlock: function(block){
  330. var escapePrettyMode = this.escapePrettyMode;
  331. var pp = this.pp;
  332. // Pretty print multi-line text
  333. if (pp && block.nodes.length > 1 && !escapePrettyMode &&
  334. block.nodes[0].type === 'Text' && block.nodes[1].type === 'Text' ) {
  335. this.prettyIndent(1, true);
  336. }
  337. for (var i = 0; i < block.nodes.length; ++i) {
  338. // Pretty print text
  339. if (pp && i > 0 && !escapePrettyMode &&
  340. block.nodes[i].type === 'Text' && block.nodes[i-1].type === 'Text' &&
  341. /\n$/.test(block.nodes[i - 1].val)) {
  342. this.prettyIndent(1, false);
  343. }
  344. this.visit(block.nodes[i], block);
  345. }
  346. },
  347. /**
  348. * Visit a mixin's `block` keyword.
  349. *
  350. * @param {MixinBlock} block
  351. * @api public
  352. */
  353. visitMixinBlock: function(block){
  354. if (this.pp) this.buf.push("pug_indent.push('" + Array(this.indents + 1).join(this.pp) + "');");
  355. this.buf.push('block && block();');
  356. if (this.pp) this.buf.push("pug_indent.pop();");
  357. },
  358. /**
  359. * Visit `doctype`. Sets terse mode to `true` when html 5
  360. * is used, causing self-closing tags to end with ">" vs "/>",
  361. * and boolean attributes are not mirrored.
  362. *
  363. * @param {Doctype} doctype
  364. * @api public
  365. */
  366. visitDoctype: function(doctype){
  367. if (doctype && (doctype.val || !this.doctype)) {
  368. this.setDoctype(doctype.val || 'html');
  369. }
  370. if (this.doctype) this.buffer(this.doctype);
  371. this.hasCompiledDoctype = true;
  372. },
  373. /**
  374. * Visit `mixin`, generating a function that
  375. * may be called within the template.
  376. *
  377. * @param {Mixin} mixin
  378. * @api public
  379. */
  380. visitMixin: function(mixin){
  381. var name = 'pug_mixins[';
  382. var args = mixin.args || '';
  383. var block = mixin.block;
  384. var attrs = mixin.attrs;
  385. var attrsBlocks = this.attributeBlocks(mixin.attributeBlocks);
  386. var pp = this.pp;
  387. var dynamic = mixin.name[0]==='#';
  388. var key = mixin.name;
  389. if (dynamic) this.dynamicMixins = true;
  390. name += (dynamic ? mixin.name.substr(2,mixin.name.length-3):'"'+mixin.name+'"')+']';
  391. this.mixins[key] = this.mixins[key] || {used: false, instances: []};
  392. if (mixin.call) {
  393. this.mixins[key].used = true;
  394. if (pp) this.buf.push("pug_indent.push('" + Array(this.indents + 1).join(pp) + "');")
  395. if (block || attrs.length || attrsBlocks.length) {
  396. this.buf.push(name + '.call({');
  397. if (block) {
  398. this.buf.push('block: function(){');
  399. // Render block with no indents, dynamically added when rendered
  400. this.parentIndents++;
  401. var _indents = this.indents;
  402. this.indents = 0;
  403. this.visit(mixin.block, mixin);
  404. this.indents = _indents;
  405. this.parentIndents--;
  406. if (attrs.length || attrsBlocks.length) {
  407. this.buf.push('},');
  408. } else {
  409. this.buf.push('}');
  410. }
  411. }
  412. if (attrsBlocks.length) {
  413. if (attrs.length) {
  414. var val = this.attrs(attrs);
  415. attrsBlocks.unshift(val);
  416. }
  417. if (attrsBlocks.length > 1) {
  418. this.buf.push('attributes: ' + this.runtime('merge') + '([' + attrsBlocks.join(',') + '])');
  419. } else {
  420. this.buf.push('attributes: ' + attrsBlocks[0]);
  421. }
  422. } else if (attrs.length) {
  423. var val = this.attrs(attrs);
  424. this.buf.push('attributes: ' + val);
  425. }
  426. if (args) {
  427. this.buf.push('}, ' + args + ');');
  428. } else {
  429. this.buf.push('});');
  430. }
  431. } else {
  432. this.buf.push(name + '(' + args + ');');
  433. }
  434. if (pp) this.buf.push("pug_indent.pop();")
  435. } else {
  436. var mixin_start = this.buf.length;
  437. args = args ? args.split(',') : [];
  438. var rest;
  439. if (args.length && /^\.\.\./.test(args[args.length - 1].trim())) {
  440. rest = args.pop().trim().replace(/^\.\.\./, '');
  441. }
  442. // we need use pug_interp here for v8: https://code.google.com/p/v8/issues/detail?id=4165
  443. // once fixed, use this: this.buf.push(name + ' = function(' + args.join(',') + '){');
  444. this.buf.push(name + ' = pug_interp = function(' + args.join(',') + '){');
  445. this.buf.push('var block = (this && this.block), attributes = (this && this.attributes) || {};');
  446. if (rest) {
  447. this.buf.push('var ' + rest + ' = [];');
  448. this.buf.push('for (pug_interp = ' + args.length + '; pug_interp < arguments.length; pug_interp++) {');
  449. this.buf.push(' ' + rest + '.push(arguments[pug_interp]);');
  450. this.buf.push('}');
  451. }
  452. this.parentIndents++;
  453. this.visit(block, mixin);
  454. this.parentIndents--;
  455. this.buf.push('};');
  456. var mixin_end = this.buf.length;
  457. this.mixins[key].instances.push({start: mixin_start, end: mixin_end});
  458. }
  459. },
  460. /**
  461. * Visit `tag` buffering tag markup, generating
  462. * attributes, visiting the `tag`'s code and block.
  463. *
  464. * @param {Tag} tag
  465. * @param {boolean} interpolated
  466. * @api public
  467. */
  468. visitTag: function(tag, interpolated){
  469. this.indents++;
  470. var name = tag.name
  471. , pp = this.pp
  472. , self = this;
  473. function bufferName() {
  474. if (interpolated) self.bufferExpression(tag.expr);
  475. else self.buffer(name);
  476. }
  477. if (WHITE_SPACE_SENSITIVE_TAGS[tag.name] === true) this.escapePrettyMode = true;
  478. if (!this.hasCompiledTag) {
  479. if (!this.hasCompiledDoctype && 'html' == name) {
  480. this.visitDoctype();
  481. }
  482. this.hasCompiledTag = true;
  483. }
  484. // pretty print
  485. if (pp && !tag.isInline)
  486. this.prettyIndent(0, true);
  487. if (tag.selfClosing || (!this.xml && selfClosing[tag.name])) {
  488. this.buffer('<');
  489. bufferName();
  490. this.visitAttributes(tag.attrs, this.attributeBlocks(tag.attributeBlocks));
  491. if (this.terse && !tag.selfClosing) {
  492. this.buffer('>');
  493. } else {
  494. this.buffer('/>');
  495. }
  496. // if it is non-empty throw an error
  497. if (tag.code ||
  498. tag.block &&
  499. !(tag.block.type === 'Block' && tag.block.nodes.length === 0) &&
  500. tag.block.nodes.some(function (tag) {
  501. return tag.type !== 'Text' || !/^\s*$/.test(tag.val)
  502. })) {
  503. this.error(name + ' is a self closing element: <'+name+'/> but contains nested content.', 'SELF_CLOSING_CONTENT', tag);
  504. }
  505. } else {
  506. // Optimize attributes buffering
  507. this.buffer('<');
  508. bufferName();
  509. this.visitAttributes(tag.attrs, this.attributeBlocks(tag.attributeBlocks));
  510. this.buffer('>');
  511. if (tag.code) this.visitCode(tag.code);
  512. this.visit(tag.block, tag);
  513. // pretty print
  514. if (pp && !tag.isInline && WHITE_SPACE_SENSITIVE_TAGS[tag.name] !== true && !tagCanInline(tag))
  515. this.prettyIndent(0, true);
  516. this.buffer('</');
  517. bufferName();
  518. this.buffer('>');
  519. }
  520. if (WHITE_SPACE_SENSITIVE_TAGS[tag.name] === true) this.escapePrettyMode = false;
  521. this.indents--;
  522. },
  523. /**
  524. * Visit InterpolatedTag.
  525. *
  526. * @param {InterpolatedTag} tag
  527. * @api public
  528. */
  529. visitInterpolatedTag: function(tag) {
  530. return this.visitTag(tag, true);
  531. },
  532. /**
  533. * Visit `text` node.
  534. *
  535. * @param {Text} text
  536. * @api public
  537. */
  538. visitText: function(text){
  539. this.buffer(text.val);
  540. },
  541. /**
  542. * Visit a `comment`, only buffering when the buffer flag is set.
  543. *
  544. * @param {Comment} comment
  545. * @api public
  546. */
  547. visitComment: function(comment){
  548. if (!comment.buffer) return;
  549. if (this.pp) this.prettyIndent(1, true);
  550. this.buffer('<!--' + comment.val + '-->');
  551. },
  552. /**
  553. * Visit a `YieldBlock`.
  554. *
  555. * This is necessary since we allow compiling a file with `yield`.
  556. *
  557. * @param {YieldBlock} block
  558. * @api public
  559. */
  560. visitYieldBlock: function(block) {},
  561. /**
  562. * Visit a `BlockComment`.
  563. *
  564. * @param {Comment} comment
  565. * @api public
  566. */
  567. visitBlockComment: function(comment){
  568. if (!comment.buffer) return;
  569. if (this.pp) this.prettyIndent(1, true);
  570. this.buffer('<!--' + (comment.val || ''));
  571. this.visit(comment.block, comment);
  572. if (this.pp) this.prettyIndent(1, true);
  573. this.buffer('-->');
  574. },
  575. /**
  576. * Visit `code`, respecting buffer / escape flags.
  577. * If the code is followed by a block, wrap it in
  578. * a self-calling function.
  579. *
  580. * @param {Code} code
  581. * @api public
  582. */
  583. visitCode: function(code){
  584. // Wrap code blocks with {}.
  585. // we only wrap unbuffered code blocks ATM
  586. // since they are usually flow control
  587. // Buffer code
  588. if (code.buffer) {
  589. var val = code.val.trim();
  590. val = 'null == (pug_interp = '+val+') ? "" : pug_interp';
  591. if (code.mustEscape !== false) val = this.runtime('escape') + '(' + val + ')';
  592. this.bufferExpression(val);
  593. } else {
  594. this.buf.push(code.val);
  595. }
  596. // Block support
  597. if (code.block) {
  598. if (!code.buffer) this.buf.push('{');
  599. this.visit(code.block, code);
  600. if (!code.buffer) this.buf.push('}');
  601. }
  602. },
  603. /**
  604. * Visit `Conditional`.
  605. *
  606. * @param {Conditional} cond
  607. * @api public
  608. */
  609. visitConditional: function(cond){
  610. var test = cond.test;
  611. this.buf.push('if (' + test + ') {');
  612. this.visit(cond.consequent, cond);
  613. this.buf.push('}')
  614. if (cond.alternate) {
  615. if (cond.alternate.type === 'Conditional') {
  616. this.buf.push('else')
  617. this.visitConditional(cond.alternate);
  618. } else {
  619. this.buf.push('else {');
  620. this.visit(cond.alternate, cond);
  621. this.buf.push('}');
  622. }
  623. }
  624. },
  625. /**
  626. * Visit `While`.
  627. *
  628. * @param {While} loop
  629. * @api public
  630. */
  631. visitWhile: function(loop){
  632. var test = loop.test;
  633. this.buf.push('while (' + test + ') {');
  634. this.visit(loop.block, loop);
  635. this.buf.push('}');
  636. },
  637. /**
  638. * Visit `each` block.
  639. *
  640. * @param {Each} each
  641. * @api public
  642. */
  643. visitEach: function(each){
  644. var indexVarName = each.key || 'pug_index' + this.eachCount;
  645. this.eachCount++;
  646. this.buf.push(''
  647. + '// iterate ' + each.obj + '\n'
  648. + ';(function(){\n'
  649. + ' var $$obj = ' + each.obj + ';\n'
  650. + ' if (\'number\' == typeof $$obj.length) {');
  651. if (each.alternate) {
  652. this.buf.push(' if ($$obj.length) {');
  653. }
  654. this.buf.push(''
  655. + ' for (var ' + indexVarName + ' = 0, $$l = $$obj.length; ' + indexVarName + ' < $$l; ' + indexVarName + '++) {\n'
  656. + ' var ' + each.val + ' = $$obj[' + indexVarName + '];');
  657. this.visit(each.block, each);
  658. this.buf.push(' }');
  659. if (each.alternate) {
  660. this.buf.push(' } else {');
  661. this.visit(each.alternate, each);
  662. this.buf.push(' }');
  663. }
  664. this.buf.push(''
  665. + ' } else {\n'
  666. + ' var $$l = 0;\n'
  667. + ' for (var ' + indexVarName + ' in $$obj) {\n'
  668. + ' $$l++;\n'
  669. + ' var ' + each.val + ' = $$obj[' + indexVarName + '];');
  670. this.visit(each.block, each);
  671. this.buf.push(' }');
  672. if (each.alternate) {
  673. this.buf.push(' if ($$l === 0) {');
  674. this.visit(each.alternate, each);
  675. this.buf.push(' }');
  676. }
  677. this.buf.push(' }\n}).call(this);\n');
  678. },
  679. /**
  680. * Visit `attrs`.
  681. *
  682. * @param {Array} attrs
  683. * @api public
  684. */
  685. visitAttributes: function(attrs, attributeBlocks){
  686. if (attributeBlocks.length) {
  687. if (attrs.length) {
  688. var val = this.attrs(attrs);
  689. attributeBlocks.unshift(val);
  690. }
  691. if (attributeBlocks.length > 1) {
  692. this.bufferExpression(this.runtime('attrs') + '(' + this.runtime('merge') + '([' + attributeBlocks.join(',') + ']), ' + stringify(this.terse) + ')');
  693. } else {
  694. this.bufferExpression(this.runtime('attrs') + '(' + attributeBlocks[0] + ', ' + stringify(this.terse) + ')');
  695. }
  696. } else if (attrs.length) {
  697. this.attrs(attrs, true);
  698. }
  699. },
  700. /**
  701. * Compile attributes.
  702. */
  703. attrs: function(attrs, buffer){
  704. var res = compileAttrs(attrs, {
  705. terse: this.terse,
  706. format: buffer ? 'html' : 'object',
  707. runtime: this.runtime.bind(this)
  708. });
  709. if (buffer) {
  710. this.bufferExpression(res);
  711. }
  712. return res;
  713. },
  714. /**
  715. * Compile attribute blocks.
  716. */
  717. attributeBlocks: function (attributeBlocks) {
  718. return attributeBlocks && attributeBlocks.slice().map(function(attrBlock){
  719. return attrBlock.val;
  720. });
  721. }
  722. };
  723. function tagCanInline(tag) {
  724. function isInline(node){
  725. // Recurse if the node is a block
  726. if (node.type === 'Block') return node.nodes.every(isInline);
  727. // When there is a YieldBlock here, it is an indication that the file is
  728. // expected to be included but is not. If this is the case, the block
  729. // must be empty.
  730. if (node.type === 'YieldBlock') return true;
  731. return (node.type === 'Text' && !/\n/.test(node.val)) || node.isInline;
  732. }
  733. return tag.block.nodes.every(isInline);
  734. }