index.js 24 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010
  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 (
  110. var y = mixin.instances[x].start;
  111. y < mixin.instances[x].end;
  112. y++
  113. ) {
  114. this.buf[y] = '';
  115. }
  116. }
  117. }
  118. }
  119. }
  120. var js = this.buf.join('\n');
  121. var globals = this.options.globals
  122. ? this.options.globals.concat(INTERNAL_VARIABLES)
  123. : INTERNAL_VARIABLES;
  124. if (this.options.self) {
  125. js = 'var self = locals || {};' + js;
  126. } else {
  127. js = addWith(
  128. 'locals || {}',
  129. js,
  130. globals.concat(
  131. this.runtimeFunctionsUsed.map(function(name) {
  132. return 'pug_' + name;
  133. })
  134. )
  135. );
  136. }
  137. if (this.debug) {
  138. if (this.options.includeSources) {
  139. js =
  140. 'var pug_debug_sources = ' +
  141. stringify(this.options.includeSources) +
  142. ';\n' +
  143. js;
  144. }
  145. js =
  146. 'var pug_debug_filename, pug_debug_line;' +
  147. 'try {' +
  148. js +
  149. '} catch (err) {' +
  150. (this.inlineRuntimeFunctions ? 'pug_rethrow' : 'pug.rethrow') +
  151. '(err, pug_debug_filename, pug_debug_line' +
  152. (this.options.includeSources
  153. ? ', pug_debug_sources[pug_debug_filename]'
  154. : '') +
  155. ');' +
  156. '}';
  157. }
  158. return (
  159. buildRuntime(this.runtimeFunctionsUsed) +
  160. 'function ' +
  161. (this.options.templateName || 'template') +
  162. '(locals) {var pug_html = "", pug_mixins = {}, pug_interp;' +
  163. js +
  164. ';return pug_html;}'
  165. );
  166. },
  167. /**
  168. * Sets the default doctype `name`. Sets terse mode to `true` when
  169. * html 5 is used, causing self-closing tags to end with ">" vs "/>",
  170. * and boolean attributes are not mirrored.
  171. *
  172. * @param {string} name
  173. * @api public
  174. */
  175. setDoctype: function(name) {
  176. this.doctype = doctypes[name.toLowerCase()] || '<!DOCTYPE ' + name + '>';
  177. this.terse = this.doctype.toLowerCase() == '<!doctype html>';
  178. this.xml = 0 == this.doctype.indexOf('<?xml');
  179. },
  180. /**
  181. * Buffer the given `str` exactly as is or with interpolation
  182. *
  183. * @param {String} str
  184. * @param {Boolean} interpolate
  185. * @api public
  186. */
  187. buffer: function(str) {
  188. var self = this;
  189. str = stringify(str);
  190. str = str.substr(1, str.length - 2);
  191. if (
  192. this.lastBufferedIdx == this.buf.length &&
  193. this.bufferedConcatenationCount < 100
  194. ) {
  195. if (this.lastBufferedType === 'code') {
  196. this.lastBuffered += ' + "';
  197. this.bufferedConcatenationCount++;
  198. }
  199. this.lastBufferedType = 'text';
  200. this.lastBuffered += str;
  201. this.buf[this.lastBufferedIdx - 1] =
  202. 'pug_html = pug_html + ' +
  203. this.bufferStartChar +
  204. this.lastBuffered +
  205. '";';
  206. } else {
  207. this.bufferedConcatenationCount = 0;
  208. this.buf.push('pug_html = pug_html + "' + str + '";');
  209. this.lastBufferedType = 'text';
  210. this.bufferStartChar = '"';
  211. this.lastBuffered = str;
  212. this.lastBufferedIdx = this.buf.length;
  213. }
  214. },
  215. /**
  216. * Buffer the given `src` so it is evaluated at run time
  217. *
  218. * @param {String} src
  219. * @api public
  220. */
  221. bufferExpression: function(src) {
  222. if (isConstant(src)) {
  223. return this.buffer(toConstant(src) + '');
  224. }
  225. if (
  226. this.lastBufferedIdx == this.buf.length &&
  227. this.bufferedConcatenationCount < 100
  228. ) {
  229. this.bufferedConcatenationCount++;
  230. if (this.lastBufferedType === 'text') this.lastBuffered += '"';
  231. this.lastBufferedType = 'code';
  232. this.lastBuffered += ' + (' + src + ')';
  233. this.buf[this.lastBufferedIdx - 1] =
  234. 'pug_html = pug_html + (' +
  235. this.bufferStartChar +
  236. this.lastBuffered +
  237. ');';
  238. } else {
  239. this.bufferedConcatenationCount = 0;
  240. this.buf.push('pug_html = pug_html + (' + src + ');');
  241. this.lastBufferedType = 'code';
  242. this.bufferStartChar = '';
  243. this.lastBuffered = '(' + src + ')';
  244. this.lastBufferedIdx = this.buf.length;
  245. }
  246. },
  247. /**
  248. * Buffer an indent based on the current `indent`
  249. * property and an additional `offset`.
  250. *
  251. * @param {Number} offset
  252. * @param {Boolean} newline
  253. * @api public
  254. */
  255. prettyIndent: function(offset, newline) {
  256. offset = offset || 0;
  257. newline = newline ? '\n' : '';
  258. this.buffer(newline + Array(this.indents + offset).join(this.pp));
  259. if (this.parentIndents)
  260. this.buf.push('pug_html = pug_html + pug_indent.join("");');
  261. },
  262. /**
  263. * Visit `node`.
  264. *
  265. * @param {Node} node
  266. * @api public
  267. */
  268. visit: function(node, parent) {
  269. var debug = this.debug;
  270. if (!node) {
  271. var msg;
  272. if (parent) {
  273. msg =
  274. 'A child of ' +
  275. parent.type +
  276. ' (' +
  277. (parent.filename || 'Pug') +
  278. ':' +
  279. parent.line +
  280. ')';
  281. } else {
  282. msg = 'A top-level node';
  283. }
  284. msg += ' is ' + node + ', expected a Pug AST Node.';
  285. throw new TypeError(msg);
  286. }
  287. if (debug && node.debug !== false && node.type !== 'Block') {
  288. if (node.line) {
  289. var js = ';pug_debug_line = ' + node.line;
  290. if (node.filename)
  291. js += ';pug_debug_filename = ' + stringify(node.filename);
  292. this.buf.push(js + ';');
  293. }
  294. }
  295. if (!this['visit' + node.type]) {
  296. var msg;
  297. if (parent) {
  298. msg = 'A child of ' + parent.type;
  299. } else {
  300. msg = 'A top-level node';
  301. }
  302. msg +=
  303. ' (' +
  304. (node.filename || 'Pug') +
  305. ':' +
  306. node.line +
  307. ')' +
  308. ' is of type ' +
  309. node.type +
  310. ',' +
  311. ' which is not supported by pug-code-gen.';
  312. switch (node.type) {
  313. case 'Filter':
  314. msg += ' Please use pug-filters to preprocess this AST.';
  315. break;
  316. case 'Extends':
  317. case 'Include':
  318. case 'NamedBlock':
  319. case 'FileReference': // unlikely but for the sake of completeness
  320. msg += ' Please use pug-linker to preprocess this AST.';
  321. break;
  322. }
  323. throw new TypeError(msg);
  324. }
  325. this.visitNode(node);
  326. },
  327. /**
  328. * Visit `node`.
  329. *
  330. * @param {Node} node
  331. * @api public
  332. */
  333. visitNode: function(node) {
  334. return this['visit' + node.type](node);
  335. },
  336. /**
  337. * Visit case `node`.
  338. *
  339. * @param {Literal} node
  340. * @api public
  341. */
  342. visitCase: function(node) {
  343. this.buf.push('switch (' + node.expr + '){');
  344. this.visit(node.block, node);
  345. this.buf.push('}');
  346. },
  347. /**
  348. * Visit when `node`.
  349. *
  350. * @param {Literal} node
  351. * @api public
  352. */
  353. visitWhen: function(node) {
  354. if ('default' == node.expr) {
  355. this.buf.push('default:');
  356. } else {
  357. this.buf.push('case ' + node.expr + ':');
  358. }
  359. if (node.block) {
  360. this.visit(node.block, node);
  361. this.buf.push(' break;');
  362. }
  363. },
  364. /**
  365. * Visit literal `node`.
  366. *
  367. * @param {Literal} node
  368. * @api public
  369. */
  370. visitLiteral: function(node) {
  371. this.buffer(node.str);
  372. },
  373. visitNamedBlock: function(block) {
  374. return this.visitBlock(block);
  375. },
  376. /**
  377. * Visit all nodes in `block`.
  378. *
  379. * @param {Block} block
  380. * @api public
  381. */
  382. visitBlock: function(block) {
  383. var escapePrettyMode = this.escapePrettyMode;
  384. var pp = this.pp;
  385. // Pretty print multi-line text
  386. if (
  387. pp &&
  388. block.nodes.length > 1 &&
  389. !escapePrettyMode &&
  390. block.nodes[0].type === 'Text' &&
  391. block.nodes[1].type === 'Text'
  392. ) {
  393. this.prettyIndent(1, true);
  394. }
  395. for (var i = 0; i < block.nodes.length; ++i) {
  396. // Pretty print text
  397. if (
  398. pp &&
  399. i > 0 &&
  400. !escapePrettyMode &&
  401. block.nodes[i].type === 'Text' &&
  402. block.nodes[i - 1].type === 'Text' &&
  403. /\n$/.test(block.nodes[i - 1].val)
  404. ) {
  405. this.prettyIndent(1, false);
  406. }
  407. this.visit(block.nodes[i], block);
  408. }
  409. },
  410. /**
  411. * Visit a mixin's `block` keyword.
  412. *
  413. * @param {MixinBlock} block
  414. * @api public
  415. */
  416. visitMixinBlock: function(block) {
  417. if (this.pp)
  418. this.buf.push(
  419. 'pug_indent.push(' +
  420. stringify(Array(this.indents + 1).join(this.pp)) +
  421. ');'
  422. );
  423. this.buf.push('block && block();');
  424. if (this.pp) this.buf.push('pug_indent.pop();');
  425. },
  426. /**
  427. * Visit `doctype`. Sets terse mode to `true` when html 5
  428. * is used, causing self-closing tags to end with ">" vs "/>",
  429. * and boolean attributes are not mirrored.
  430. *
  431. * @param {Doctype} doctype
  432. * @api public
  433. */
  434. visitDoctype: function(doctype) {
  435. if (doctype && (doctype.val || !this.doctype)) {
  436. this.setDoctype(doctype.val || 'html');
  437. }
  438. if (this.doctype) this.buffer(this.doctype);
  439. this.hasCompiledDoctype = true;
  440. },
  441. /**
  442. * Visit `mixin`, generating a function that
  443. * may be called within the template.
  444. *
  445. * @param {Mixin} mixin
  446. * @api public
  447. */
  448. visitMixin: function(mixin) {
  449. var name = 'pug_mixins[';
  450. var args = mixin.args || '';
  451. var block = mixin.block;
  452. var attrs = mixin.attrs;
  453. var attrsBlocks = this.attributeBlocks(mixin.attributeBlocks);
  454. var pp = this.pp;
  455. var dynamic = mixin.name[0] === '#';
  456. var key = mixin.name;
  457. if (dynamic) this.dynamicMixins = true;
  458. name +=
  459. (dynamic
  460. ? mixin.name.substr(2, mixin.name.length - 3)
  461. : '"' + mixin.name + '"') + ']';
  462. this.mixins[key] = this.mixins[key] || {used: false, instances: []};
  463. if (mixin.call) {
  464. this.mixins[key].used = true;
  465. if (pp)
  466. this.buf.push(
  467. 'pug_indent.push(' +
  468. stringify(Array(this.indents + 1).join(pp)) +
  469. ');'
  470. );
  471. if (block || attrs.length || attrsBlocks.length) {
  472. this.buf.push(name + '.call({');
  473. if (block) {
  474. this.buf.push('block: function(){');
  475. // Render block with no indents, dynamically added when rendered
  476. this.parentIndents++;
  477. var _indents = this.indents;
  478. this.indents = 0;
  479. this.visit(mixin.block, mixin);
  480. this.indents = _indents;
  481. this.parentIndents--;
  482. if (attrs.length || attrsBlocks.length) {
  483. this.buf.push('},');
  484. } else {
  485. this.buf.push('}');
  486. }
  487. }
  488. if (attrsBlocks.length) {
  489. if (attrs.length) {
  490. var val = this.attrs(attrs);
  491. attrsBlocks.unshift(val);
  492. }
  493. if (attrsBlocks.length > 1) {
  494. this.buf.push(
  495. 'attributes: ' +
  496. this.runtime('merge') +
  497. '([' +
  498. attrsBlocks.join(',') +
  499. '])'
  500. );
  501. } else {
  502. this.buf.push('attributes: ' + attrsBlocks[0]);
  503. }
  504. } else if (attrs.length) {
  505. var val = this.attrs(attrs);
  506. this.buf.push('attributes: ' + val);
  507. }
  508. if (args) {
  509. this.buf.push('}, ' + args + ');');
  510. } else {
  511. this.buf.push('});');
  512. }
  513. } else {
  514. this.buf.push(name + '(' + args + ');');
  515. }
  516. if (pp) this.buf.push('pug_indent.pop();');
  517. } else {
  518. var mixin_start = this.buf.length;
  519. args = args ? args.split(',') : [];
  520. var rest;
  521. if (args.length && /^\.\.\./.test(args[args.length - 1].trim())) {
  522. rest = args
  523. .pop()
  524. .trim()
  525. .replace(/^\.\.\./, '');
  526. }
  527. // we need use pug_interp here for v8: https://code.google.com/p/v8/issues/detail?id=4165
  528. // once fixed, use this: this.buf.push(name + ' = function(' + args.join(',') + '){');
  529. this.buf.push(name + ' = pug_interp = function(' + args.join(',') + '){');
  530. this.buf.push(
  531. 'var block = (this && this.block), attributes = (this && this.attributes) || {};'
  532. );
  533. if (rest) {
  534. this.buf.push('var ' + rest + ' = [];');
  535. this.buf.push(
  536. 'for (pug_interp = ' +
  537. args.length +
  538. '; pug_interp < arguments.length; pug_interp++) {'
  539. );
  540. this.buf.push(' ' + rest + '.push(arguments[pug_interp]);');
  541. this.buf.push('}');
  542. }
  543. this.parentIndents++;
  544. this.visit(block, mixin);
  545. this.parentIndents--;
  546. this.buf.push('};');
  547. var mixin_end = this.buf.length;
  548. this.mixins[key].instances.push({start: mixin_start, end: mixin_end});
  549. }
  550. },
  551. /**
  552. * Visit `tag` buffering tag markup, generating
  553. * attributes, visiting the `tag`'s code and block.
  554. *
  555. * @param {Tag} tag
  556. * @param {boolean} interpolated
  557. * @api public
  558. */
  559. visitTag: function(tag, interpolated) {
  560. this.indents++;
  561. var name = tag.name,
  562. pp = this.pp,
  563. self = this;
  564. function bufferName() {
  565. if (interpolated) self.bufferExpression(tag.expr);
  566. else self.buffer(name);
  567. }
  568. if (WHITE_SPACE_SENSITIVE_TAGS[tag.name] === true)
  569. this.escapePrettyMode = true;
  570. if (!this.hasCompiledTag) {
  571. if (!this.hasCompiledDoctype && 'html' == name) {
  572. this.visitDoctype();
  573. }
  574. this.hasCompiledTag = true;
  575. }
  576. // pretty print
  577. if (pp && !tag.isInline) this.prettyIndent(0, true);
  578. if (tag.selfClosing || (!this.xml && selfClosing[tag.name])) {
  579. this.buffer('<');
  580. bufferName();
  581. this.visitAttributes(
  582. tag.attrs,
  583. this.attributeBlocks(tag.attributeBlocks)
  584. );
  585. if (this.terse && !tag.selfClosing) {
  586. this.buffer('>');
  587. } else {
  588. this.buffer('/>');
  589. }
  590. // if it is non-empty throw an error
  591. if (
  592. tag.code ||
  593. (tag.block &&
  594. !(tag.block.type === 'Block' && tag.block.nodes.length === 0) &&
  595. tag.block.nodes.some(function(tag) {
  596. return tag.type !== 'Text' || !/^\s*$/.test(tag.val);
  597. }))
  598. ) {
  599. this.error(
  600. name +
  601. ' is a self closing element: <' +
  602. name +
  603. '/> but contains nested content.',
  604. 'SELF_CLOSING_CONTENT',
  605. tag
  606. );
  607. }
  608. } else {
  609. // Optimize attributes buffering
  610. this.buffer('<');
  611. bufferName();
  612. this.visitAttributes(
  613. tag.attrs,
  614. this.attributeBlocks(tag.attributeBlocks)
  615. );
  616. this.buffer('>');
  617. if (tag.code) this.visitCode(tag.code);
  618. this.visit(tag.block, tag);
  619. // pretty print
  620. if (
  621. pp &&
  622. !tag.isInline &&
  623. WHITE_SPACE_SENSITIVE_TAGS[tag.name] !== true &&
  624. !tagCanInline(tag)
  625. )
  626. this.prettyIndent(0, true);
  627. this.buffer('</');
  628. bufferName();
  629. this.buffer('>');
  630. }
  631. if (WHITE_SPACE_SENSITIVE_TAGS[tag.name] === true)
  632. this.escapePrettyMode = false;
  633. this.indents--;
  634. },
  635. /**
  636. * Visit InterpolatedTag.
  637. *
  638. * @param {InterpolatedTag} tag
  639. * @api public
  640. */
  641. visitInterpolatedTag: function(tag) {
  642. return this.visitTag(tag, true);
  643. },
  644. /**
  645. * Visit `text` node.
  646. *
  647. * @param {Text} text
  648. * @api public
  649. */
  650. visitText: function(text) {
  651. this.buffer(text.val);
  652. },
  653. /**
  654. * Visit a `comment`, only buffering when the buffer flag is set.
  655. *
  656. * @param {Comment} comment
  657. * @api public
  658. */
  659. visitComment: function(comment) {
  660. if (!comment.buffer) return;
  661. if (this.pp) this.prettyIndent(1, true);
  662. this.buffer('<!--' + comment.val + '-->');
  663. },
  664. /**
  665. * Visit a `YieldBlock`.
  666. *
  667. * This is necessary since we allow compiling a file with `yield`.
  668. *
  669. * @param {YieldBlock} block
  670. * @api public
  671. */
  672. visitYieldBlock: function(block) {},
  673. /**
  674. * Visit a `BlockComment`.
  675. *
  676. * @param {Comment} comment
  677. * @api public
  678. */
  679. visitBlockComment: function(comment) {
  680. if (!comment.buffer) return;
  681. if (this.pp) this.prettyIndent(1, true);
  682. this.buffer('<!--' + (comment.val || ''));
  683. this.visit(comment.block, comment);
  684. if (this.pp) this.prettyIndent(1, true);
  685. this.buffer('-->');
  686. },
  687. /**
  688. * Visit `code`, respecting buffer / escape flags.
  689. * If the code is followed by a block, wrap it in
  690. * a self-calling function.
  691. *
  692. * @param {Code} code
  693. * @api public
  694. */
  695. visitCode: function(code) {
  696. // Wrap code blocks with {}.
  697. // we only wrap unbuffered code blocks ATM
  698. // since they are usually flow control
  699. // Buffer code
  700. if (code.buffer) {
  701. var val = code.val.trim();
  702. val = 'null == (pug_interp = ' + val + ') ? "" : pug_interp';
  703. if (code.mustEscape !== false)
  704. val = this.runtime('escape') + '(' + val + ')';
  705. this.bufferExpression(val);
  706. } else {
  707. this.buf.push(code.val);
  708. }
  709. // Block support
  710. if (code.block) {
  711. if (!code.buffer) this.buf.push('{');
  712. this.visit(code.block, code);
  713. if (!code.buffer) this.buf.push('}');
  714. }
  715. },
  716. /**
  717. * Visit `Conditional`.
  718. *
  719. * @param {Conditional} cond
  720. * @api public
  721. */
  722. visitConditional: function(cond) {
  723. var test = cond.test;
  724. this.buf.push('if (' + test + ') {');
  725. this.visit(cond.consequent, cond);
  726. this.buf.push('}');
  727. if (cond.alternate) {
  728. if (cond.alternate.type === 'Conditional') {
  729. this.buf.push('else');
  730. this.visitConditional(cond.alternate);
  731. } else {
  732. this.buf.push('else {');
  733. this.visit(cond.alternate, cond);
  734. this.buf.push('}');
  735. }
  736. }
  737. },
  738. /**
  739. * Visit `While`.
  740. *
  741. * @param {While} loop
  742. * @api public
  743. */
  744. visitWhile: function(loop) {
  745. var test = loop.test;
  746. this.buf.push('while (' + test + ') {');
  747. this.visit(loop.block, loop);
  748. this.buf.push('}');
  749. },
  750. /**
  751. * Visit `each` block.
  752. *
  753. * @param {Each} each
  754. * @api public
  755. */
  756. visitEach: function(each) {
  757. var indexVarName = each.key || 'pug_index' + this.eachCount;
  758. this.eachCount++;
  759. this.buf.push(
  760. '' +
  761. '// iterate ' +
  762. each.obj +
  763. '\n' +
  764. ';(function(){\n' +
  765. ' var $$obj = ' +
  766. each.obj +
  767. ';\n' +
  768. " if ('number' == typeof $$obj.length) {"
  769. );
  770. if (each.alternate) {
  771. this.buf.push(' if ($$obj.length) {');
  772. }
  773. this.buf.push(
  774. '' +
  775. ' for (var ' +
  776. indexVarName +
  777. ' = 0, $$l = $$obj.length; ' +
  778. indexVarName +
  779. ' < $$l; ' +
  780. indexVarName +
  781. '++) {\n' +
  782. ' var ' +
  783. each.val +
  784. ' = $$obj[' +
  785. indexVarName +
  786. '];'
  787. );
  788. this.visit(each.block, each);
  789. this.buf.push(' }');
  790. if (each.alternate) {
  791. this.buf.push(' } else {');
  792. this.visit(each.alternate, each);
  793. this.buf.push(' }');
  794. }
  795. this.buf.push(
  796. '' +
  797. ' } else {\n' +
  798. ' var $$l = 0;\n' +
  799. ' for (var ' +
  800. indexVarName +
  801. ' in $$obj) {\n' +
  802. ' $$l++;\n' +
  803. ' var ' +
  804. each.val +
  805. ' = $$obj[' +
  806. indexVarName +
  807. '];'
  808. );
  809. this.visit(each.block, each);
  810. this.buf.push(' }');
  811. if (each.alternate) {
  812. this.buf.push(' if ($$l === 0) {');
  813. this.visit(each.alternate, each);
  814. this.buf.push(' }');
  815. }
  816. this.buf.push(' }\n}).call(this);\n');
  817. },
  818. visitEachOf: function(each) {
  819. this.buf.push(
  820. '' +
  821. '// iterate ' +
  822. each.obj +
  823. '\n' +
  824. 'for (const ' +
  825. each.val +
  826. ' of ' +
  827. each.obj +
  828. ') {\n'
  829. );
  830. this.visit(each.block, each);
  831. this.buf.push('}\n');
  832. },
  833. /**
  834. * Visit `attrs`.
  835. *
  836. * @param {Array} attrs
  837. * @api public
  838. */
  839. visitAttributes: function(attrs, attributeBlocks) {
  840. if (attributeBlocks.length) {
  841. if (attrs.length) {
  842. var val = this.attrs(attrs);
  843. attributeBlocks.unshift(val);
  844. }
  845. if (attributeBlocks.length > 1) {
  846. this.bufferExpression(
  847. this.runtime('attrs') +
  848. '(' +
  849. this.runtime('merge') +
  850. '([' +
  851. attributeBlocks.join(',') +
  852. ']), ' +
  853. stringify(this.terse) +
  854. ')'
  855. );
  856. } else {
  857. this.bufferExpression(
  858. this.runtime('attrs') +
  859. '(' +
  860. attributeBlocks[0] +
  861. ', ' +
  862. stringify(this.terse) +
  863. ')'
  864. );
  865. }
  866. } else if (attrs.length) {
  867. this.attrs(attrs, true);
  868. }
  869. },
  870. /**
  871. * Compile attributes.
  872. */
  873. attrs: function(attrs, buffer) {
  874. var res = compileAttrs(attrs, {
  875. terse: this.terse,
  876. format: buffer ? 'html' : 'object',
  877. runtime: this.runtime.bind(this),
  878. });
  879. if (buffer) {
  880. this.bufferExpression(res);
  881. }
  882. return res;
  883. },
  884. /**
  885. * Compile attribute blocks.
  886. */
  887. attributeBlocks: function(attributeBlocks) {
  888. return (
  889. attributeBlocks &&
  890. attributeBlocks.slice().map(function(attrBlock) {
  891. return attrBlock.val;
  892. })
  893. );
  894. },
  895. };
  896. function tagCanInline(tag) {
  897. function isInline(node) {
  898. // Recurse if the node is a block
  899. if (node.type === 'Block') return node.nodes.every(isInline);
  900. // When there is a YieldBlock here, it is an indication that the file is
  901. // expected to be included but is not. If this is the case, the block
  902. // must be empty.
  903. if (node.type === 'YieldBlock') return true;
  904. return (node.type === 'Text' && !/\n/.test(node.val)) || node.isInline;
  905. }
  906. return tag.block.nodes.every(isInline);
  907. }