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