'use strict'; var doctypes = require('doctypes'); var makeError = require('pug-error'); var buildRuntime = require('pug-runtime/build'); var runtime = require('pug-runtime'); var compileAttrs = require('pug-attrs'); var selfClosing = require('void-elements'); var constantinople = require('constantinople'); var stringify = require('js-stringify'); var addWith = require('with'); // This is used to prevent pretty printing inside certain tags var WHITE_SPACE_SENSITIVE_TAGS = { pre: true, textarea: true }; var INTERNAL_VARIABLES = [ 'pug', 'pug_mixins', 'pug_interp', 'pug_debug_filename', 'pug_debug_line', 'pug_debug_sources', 'pug_html' ]; module.exports = generateCode; module.exports.CodeGenerator = Compiler; function generateCode(ast, options) { return (new Compiler(ast, options)).compile(); } function isConstant(src) { return constantinople(src, {pug: runtime, 'pug_interp': undefined}); } function toConstant(src) { return constantinople.toConstant(src, {pug: runtime, 'pug_interp': undefined}); } /** * Initialize `Compiler` with the given `node`. * * @param {Node} node * @param {Object} options * @api public */ function Compiler(node, options) { this.options = options = options || {}; this.node = node; this.bufferedConcatenationCount = 0; this.hasCompiledDoctype = false; this.hasCompiledTag = false; this.pp = options.pretty || false; if (this.pp && typeof this.pp !== 'string') { this.pp = ' '; } this.debug = false !== options.compileDebug; this.indents = 0; this.parentIndents = 0; this.terse = false; this.mixins = {}; this.dynamicMixins = false; this.eachCount = 0; if (options.doctype) this.setDoctype(options.doctype); this.runtimeFunctionsUsed = []; this.inlineRuntimeFunctions = options.inlineRuntimeFunctions || false; if (this.debug && this.inlineRuntimeFunctions) { this.runtimeFunctionsUsed.push('rethrow'); } }; /** * Compiler prototype. */ Compiler.prototype = { runtime: function (name) { if (this.inlineRuntimeFunctions) { this.runtimeFunctionsUsed.push(name); return 'pug_' + name; } else { return 'pug.' + name; } }, error: function (message, code, node) { var err = makeError(code, message, { line: node.line, column: node.column, filename: node.filename, }); throw err; }, /** * Compile parse tree to JavaScript. * * @api public */ compile: function(){ this.buf = []; if (this.pp) this.buf.push("var pug_indent = [];"); this.lastBufferedIdx = -1; this.visit(this.node); if (!this.dynamicMixins) { // if there are no dynamic mixins we can remove any un-used mixins var mixinNames = Object.keys(this.mixins); for (var i = 0; i < mixinNames.length; i++) { var mixin = this.mixins[mixinNames[i]]; if (!mixin.used) { for (var x = 0; x < mixin.instances.length; x++) { for (var y = mixin.instances[x].start; y < mixin.instances[x].end; y++) { this.buf[y] = ''; } } } } } var js = this.buf.join('\n'); var globals = this.options.globals ? this.options.globals.concat(INTERNAL_VARIABLES) : INTERNAL_VARIABLES; if (this.options.self) { js = 'var self = locals || {};' + js; } else { js = addWith('locals || {}', js, globals.concat(this.runtimeFunctionsUsed.map(function (name) { return 'pug_' + name; }))); } if (this.debug) { if (this.options.includeSources) { js = 'var pug_debug_sources = ' + stringify(this.options.includeSources) + ';\n' + js; } js = 'var pug_debug_filename, pug_debug_line;' + 'try {' + js + '} catch (err) {' + (this.inlineRuntimeFunctions ? 'pug_rethrow' : 'pug.rethrow') + '(err, pug_debug_filename, pug_debug_line' + ( this.options.includeSources ? ', pug_debug_sources[pug_debug_filename]' : '' ) + ');' + '}'; } return buildRuntime(this.runtimeFunctionsUsed) + 'function ' + (this.options.templateName || 'template') + '(locals) {var pug_html = "", pug_mixins = {}, pug_interp;' + js + ';return pug_html;}'; }, /** * Sets the default doctype `name`. Sets terse mode to `true` when * html 5 is used, causing self-closing tags to end with ">" vs "/>", * and boolean attributes are not mirrored. * * @param {string} name * @api public */ setDoctype: function(name){ this.doctype = doctypes[name.toLowerCase()] || ''; this.terse = this.doctype.toLowerCase() == ''; this.xml = 0 == this.doctype.indexOf(' 1 && !escapePrettyMode && block.nodes[0].type === 'Text' && block.nodes[1].type === 'Text' ) { this.prettyIndent(1, true); } for (var i = 0; i < block.nodes.length; ++i) { // Pretty print text if (pp && i > 0 && !escapePrettyMode && block.nodes[i].type === 'Text' && block.nodes[i-1].type === 'Text' && /\n$/.test(block.nodes[i - 1].val)) { this.prettyIndent(1, false); } this.visit(block.nodes[i], block); } }, /** * Visit a mixin's `block` keyword. * * @param {MixinBlock} block * @api public */ visitMixinBlock: function(block){ if (this.pp) this.buf.push("pug_indent.push('" + Array(this.indents + 1).join(this.pp) + "');"); this.buf.push('block && block();'); if (this.pp) this.buf.push("pug_indent.pop();"); }, /** * Visit `doctype`. Sets terse mode to `true` when html 5 * is used, causing self-closing tags to end with ">" vs "/>", * and boolean attributes are not mirrored. * * @param {Doctype} doctype * @api public */ visitDoctype: function(doctype){ if (doctype && (doctype.val || !this.doctype)) { this.setDoctype(doctype.val || 'html'); } if (this.doctype) this.buffer(this.doctype); this.hasCompiledDoctype = true; }, /** * Visit `mixin`, generating a function that * may be called within the template. * * @param {Mixin} mixin * @api public */ visitMixin: function(mixin){ var name = 'pug_mixins['; var args = mixin.args || ''; var block = mixin.block; var attrs = mixin.attrs; var attrsBlocks = this.attributeBlocks(mixin.attributeBlocks); var pp = this.pp; var dynamic = mixin.name[0]==='#'; var key = mixin.name; if (dynamic) this.dynamicMixins = true; name += (dynamic ? mixin.name.substr(2,mixin.name.length-3):'"'+mixin.name+'"')+']'; this.mixins[key] = this.mixins[key] || {used: false, instances: []}; if (mixin.call) { this.mixins[key].used = true; if (pp) this.buf.push("pug_indent.push('" + Array(this.indents + 1).join(pp) + "');") if (block || attrs.length || attrsBlocks.length) { this.buf.push(name + '.call({'); if (block) { this.buf.push('block: function(){'); // Render block with no indents, dynamically added when rendered this.parentIndents++; var _indents = this.indents; this.indents = 0; this.visit(mixin.block, mixin); this.indents = _indents; this.parentIndents--; if (attrs.length || attrsBlocks.length) { this.buf.push('},'); } else { this.buf.push('}'); } } if (attrsBlocks.length) { if (attrs.length) { var val = this.attrs(attrs); attrsBlocks.unshift(val); } if (attrsBlocks.length > 1) { this.buf.push('attributes: ' + this.runtime('merge') + '([' + attrsBlocks.join(',') + '])'); } else { this.buf.push('attributes: ' + attrsBlocks[0]); } } else if (attrs.length) { var val = this.attrs(attrs); this.buf.push('attributes: ' + val); } if (args) { this.buf.push('}, ' + args + ');'); } else { this.buf.push('});'); } } else { this.buf.push(name + '(' + args + ');'); } if (pp) this.buf.push("pug_indent.pop();") } else { var mixin_start = this.buf.length; args = args ? args.split(',') : []; var rest; if (args.length && /^\.\.\./.test(args[args.length - 1].trim())) { rest = args.pop().trim().replace(/^\.\.\./, ''); } // we need use pug_interp here for v8: https://code.google.com/p/v8/issues/detail?id=4165 // once fixed, use this: this.buf.push(name + ' = function(' + args.join(',') + '){'); this.buf.push(name + ' = pug_interp = function(' + args.join(',') + '){'); this.buf.push('var block = (this && this.block), attributes = (this && this.attributes) || {};'); if (rest) { this.buf.push('var ' + rest + ' = [];'); this.buf.push('for (pug_interp = ' + args.length + '; pug_interp < arguments.length; pug_interp++) {'); this.buf.push(' ' + rest + '.push(arguments[pug_interp]);'); this.buf.push('}'); } this.parentIndents++; this.visit(block, mixin); this.parentIndents--; this.buf.push('};'); var mixin_end = this.buf.length; this.mixins[key].instances.push({start: mixin_start, end: mixin_end}); } }, /** * Visit `tag` buffering tag markup, generating * attributes, visiting the `tag`'s code and block. * * @param {Tag} tag * @param {boolean} interpolated * @api public */ visitTag: function(tag, interpolated){ this.indents++; var name = tag.name , pp = this.pp , self = this; function bufferName() { if (interpolated) self.bufferExpression(tag.expr); else self.buffer(name); } if (WHITE_SPACE_SENSITIVE_TAGS[tag.name] === true) this.escapePrettyMode = true; if (!this.hasCompiledTag) { if (!this.hasCompiledDoctype && 'html' == name) { this.visitDoctype(); } this.hasCompiledTag = true; } // pretty print if (pp && !tag.isInline) this.prettyIndent(0, true); if (tag.selfClosing || (!this.xml && selfClosing[tag.name])) { this.buffer('<'); bufferName(); this.visitAttributes(tag.attrs, this.attributeBlocks(tag.attributeBlocks)); if (this.terse && !tag.selfClosing) { this.buffer('>'); } else { this.buffer('/>'); } // if it is non-empty throw an error if (tag.code || tag.block && !(tag.block.type === 'Block' && tag.block.nodes.length === 0) && tag.block.nodes.some(function (tag) { return tag.type !== 'Text' || !/^\s*$/.test(tag.val) })) { this.error(name + ' is a self closing element: <'+name+'/> but contains nested content.', 'SELF_CLOSING_CONTENT', tag); } } else { // Optimize attributes buffering this.buffer('<'); bufferName(); this.visitAttributes(tag.attrs, this.attributeBlocks(tag.attributeBlocks)); this.buffer('>'); if (tag.code) this.visitCode(tag.code); this.visit(tag.block, tag); // pretty print if (pp && !tag.isInline && WHITE_SPACE_SENSITIVE_TAGS[tag.name] !== true && !tagCanInline(tag)) this.prettyIndent(0, true); this.buffer(''); } if (WHITE_SPACE_SENSITIVE_TAGS[tag.name] === true) this.escapePrettyMode = false; this.indents--; }, /** * Visit InterpolatedTag. * * @param {InterpolatedTag} tag * @api public */ visitInterpolatedTag: function(tag) { return this.visitTag(tag, true); }, /** * Visit `text` node. * * @param {Text} text * @api public */ visitText: function(text){ this.buffer(text.val); }, /** * Visit a `comment`, only buffering when the buffer flag is set. * * @param {Comment} comment * @api public */ visitComment: function(comment){ if (!comment.buffer) return; if (this.pp) this.prettyIndent(1, true); this.buffer(''); }, /** * Visit a `YieldBlock`. * * This is necessary since we allow compiling a file with `yield`. * * @param {YieldBlock} block * @api public */ visitYieldBlock: function(block) {}, /** * Visit a `BlockComment`. * * @param {Comment} comment * @api public */ visitBlockComment: function(comment){ if (!comment.buffer) return; if (this.pp) this.prettyIndent(1, true); this.buffer(''); }, /** * Visit `code`, respecting buffer / escape flags. * If the code is followed by a block, wrap it in * a self-calling function. * * @param {Code} code * @api public */ visitCode: function(code){ // Wrap code blocks with {}. // we only wrap unbuffered code blocks ATM // since they are usually flow control // Buffer code if (code.buffer) { var val = code.val.trim(); val = 'null == (pug_interp = '+val+') ? "" : pug_interp'; if (code.mustEscape !== false) val = this.runtime('escape') + '(' + val + ')'; this.bufferExpression(val); } else { this.buf.push(code.val); } // Block support if (code.block) { if (!code.buffer) this.buf.push('{'); this.visit(code.block, code); if (!code.buffer) this.buf.push('}'); } }, /** * Visit `Conditional`. * * @param {Conditional} cond * @api public */ visitConditional: function(cond){ var test = cond.test; this.buf.push('if (' + test + ') {'); this.visit(cond.consequent, cond); this.buf.push('}') if (cond.alternate) { if (cond.alternate.type === 'Conditional') { this.buf.push('else') this.visitConditional(cond.alternate); } else { this.buf.push('else {'); this.visit(cond.alternate, cond); this.buf.push('}'); } } }, /** * Visit `While`. * * @param {While} loop * @api public */ visitWhile: function(loop){ var test = loop.test; this.buf.push('while (' + test + ') {'); this.visit(loop.block, loop); this.buf.push('}'); }, /** * Visit `each` block. * * @param {Each} each * @api public */ visitEach: function(each){ var indexVarName = each.key || 'pug_index' + this.eachCount; this.eachCount++; this.buf.push('' + '// iterate ' + each.obj + '\n' + ';(function(){\n' + ' var $$obj = ' + each.obj + ';\n' + ' if (\'number\' == typeof $$obj.length) {'); if (each.alternate) { this.buf.push(' if ($$obj.length) {'); } this.buf.push('' + ' for (var ' + indexVarName + ' = 0, $$l = $$obj.length; ' + indexVarName + ' < $$l; ' + indexVarName + '++) {\n' + ' var ' + each.val + ' = $$obj[' + indexVarName + '];'); this.visit(each.block, each); this.buf.push(' }'); if (each.alternate) { this.buf.push(' } else {'); this.visit(each.alternate, each); this.buf.push(' }'); } this.buf.push('' + ' } else {\n' + ' var $$l = 0;\n' + ' for (var ' + indexVarName + ' in $$obj) {\n' + ' $$l++;\n' + ' var ' + each.val + ' = $$obj[' + indexVarName + '];'); this.visit(each.block, each); this.buf.push(' }'); if (each.alternate) { this.buf.push(' if ($$l === 0) {'); this.visit(each.alternate, each); this.buf.push(' }'); } this.buf.push(' }\n}).call(this);\n'); }, /** * Visit `attrs`. * * @param {Array} attrs * @api public */ visitAttributes: function(attrs, attributeBlocks){ if (attributeBlocks.length) { if (attrs.length) { var val = this.attrs(attrs); attributeBlocks.unshift(val); } if (attributeBlocks.length > 1) { this.bufferExpression(this.runtime('attrs') + '(' + this.runtime('merge') + '([' + attributeBlocks.join(',') + ']), ' + stringify(this.terse) + ')'); } else { this.bufferExpression(this.runtime('attrs') + '(' + attributeBlocks[0] + ', ' + stringify(this.terse) + ')'); } } else if (attrs.length) { this.attrs(attrs, true); } }, /** * Compile attributes. */ attrs: function(attrs, buffer){ var res = compileAttrs(attrs, { terse: this.terse, format: buffer ? 'html' : 'object', runtime: this.runtime.bind(this) }); if (buffer) { this.bufferExpression(res); } return res; }, /** * Compile attribute blocks. */ attributeBlocks(attributeBlocks) { return attributeBlocks && attributeBlocks.slice().map(function(attrBlock){ return attrBlock.val; }); } }; function tagCanInline(tag) { function isInline(node){ // Recurse if the node is a block if (node.type === 'Block') return node.nodes.every(isInline); // When there is a YieldBlock here, it is an indication that the file is // expected to be included but is not. If this is the case, the block // must be empty. if (node.type === 'YieldBlock') return true; return (node.type === 'Text' && !/\n/.test(node.val)) || node.isInline; } return tag.block.nodes.every(isInline); }