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