/*! * Jade - Compiler * Copyright(c) 2010 TJ Holowaychuk * MIT Licensed */ /** * Module dependencies. */ var nodes = require('./nodes') , filters = require('./filters') , doctypes = require('./doctypes') , selfClosing = require('./self-closing') , runtime = require('./runtime') , utils = require('./utils'); // if browser // // if (!Object.keys) { // Object.keys = function(obj){ // var arr = []; // for (var key in obj) { // if (obj.hasOwnProperty(key)) { // arr.push(key); // } // } // return arr; // } // } // // if (!String.prototype.trimLeft) { // String.prototype.trimLeft = function(){ // return this.replace(/^\s+/, ''); // } // } // // end /** * Initialize `Compiler` with the given `node`. * * @param {Node} node * @param {Object} options * @api public */ var Compiler = module.exports = function Compiler(node, options) { this.options = options = options || {}; this.node = node; this.hasCompiledDoctype = false; this.hasCompiledTag = false; this.pp = options.pretty || false; this.debug = false !== options.compileDebug; this.indents = 0; this.parentIndents = 0; if (options.doctype) this.setDoctype(options.doctype); }; /** * Compiler prototype. */ Compiler.prototype = { /** * Compile parse tree to JavaScript. * * @api public */ compile: function(){ this.buf = ['var interp;']; if (this.pp) this.buf.push("var __indent = [];"); this.lastBufferedIdx = -1; this.visit(this.node); return this.buf.join('\n'); }, /** * 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){ var doctype = doctypes[(name || 'default').toLowerCase()]; doctype = doctype || ''; this.doctype = doctype; this.terse = '5' == name || 'html' == name; this.xml = 0 == this.doctype.indexOf(' 1 && !escape && block.nodes[0].isText && block.nodes[1].isText) this.prettyIndent(1, true); for (var i = 0; i < len; ++i) { // Pretty print text if (pp && i > 0 && !escape && block.nodes[i].isText && block.nodes[i-1].isText) this.prettyIndent(1, false); this.visit(block.nodes[i]); // Multiple text nodes are separated by newlines if (block.nodes[i+1] && block.nodes[i].isText && block.nodes[i+1].isText) this.buffer('\\n'); } }, /** * 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 || 'default'); } 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 = mixin.name.replace(/-/g, '_') + '_mixin' , args = mixin.args || '' , block = mixin.block , attrs = mixin.attrs , pp = this.pp; if (mixin.call) { if (pp) this.buf.push("__indent.push('" + Array(this.indents + 1).join(' ') + "');") if (block || attrs.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); this.indents = _indents; this.parentIndents--; if (attrs.length) { this.buf.push('},'); } else { this.buf.push('}'); } } if (attrs.length) { var val = this.attrs(attrs); if (val.inherits) { this.buf.push('attributes: merge({' + val.buf + '}, attributes), escaped: merge(' + val.escaped + ', escaped, true)'); } else { this.buf.push('attributes: {' + val.buf + '}, escaped: ' + val.escaped); } } if (args) { this.buf.push('}, ' + args + ');'); } else { this.buf.push('});'); } } else { this.buf.push(name + '(' + args + ');'); } if (pp) this.buf.push("__indent.pop();") } else { this.buf.push('var ' + name + ' = function(' + args + '){'); this.buf.push('var block = this.block, attributes = this.attributes || {}, escaped = this.escaped || {};'); this.parentIndents++; this.visit(block); this.parentIndents--; this.buf.push('};'); } }, /** * Visit `tag` buffering tag markup, generating * attributes, visiting the `tag`'s code and block. * * @param {Tag} tag * @api public */ visitTag: function(tag){ this.indents++; var name = tag.name , pp = this.pp; if (tag.buffer) name = "' + (" + name + ") + '"; if (!this.hasCompiledTag) { if (!this.hasCompiledDoctype && 'html' == name) { this.visitDoctype(); } this.hasCompiledTag = true; } // pretty print if (pp && !tag.isInline()) this.prettyIndent(0, true); if ((~selfClosing.indexOf(name) || tag.selfClosing) && !this.xml) { this.buffer('<' + name); this.visitAttributes(tag.attrs); this.terse ? this.buffer('>') : this.buffer('/>'); } else { // Optimize attributes buffering if (tag.attrs.length) { this.buffer('<' + name); if (tag.attrs.length) this.visitAttributes(tag.attrs); this.buffer('>'); } else { this.buffer('<' + name + '>'); } if (tag.code) this.visitCode(tag.code); this.escape = 'pre' == tag.name; this.visit(tag.block); // pretty print if (pp && !tag.isInline() && 'pre' != tag.name && !tag.canInline()) this.prettyIndent(0, true); this.buffer(''); } this.indents--; }, /** * Visit `filter`, throwing when the filter does not exist. * * @param {Filter} filter * @api public */ visitFilter: function(filter){ var fn = filters[filter.name]; // unknown filter if (!fn) { if (filter.isASTFilter) { throw new Error('unknown ast filter "' + filter.name + ':"'); } else { throw new Error('unknown filter ":' + filter.name + '"'); } } if (filter.isASTFilter) { this.buf.push(fn(filter.block, this, filter.attrs)); } else { var text = filter.block.nodes.map(function(node){ return node.val }).join('\n'); filter.attrs = filter.attrs || {}; filter.attrs.filename = this.options.filename; this.buffer(utils.text(fn(text, filter.attrs))); } }, /** * Visit `text` node. * * @param {Text} text * @api public */ visitText: function(text){ text = utils.text(text.val.replace(/\\/g, '\\\\')); if (this.escape) text = escape(text); this.buffer(text); }, /** * 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 `BlockComment`. * * @param {Comment} comment * @api public */ visitBlockComment: function(comment){ if (!comment.buffer) return; if (0 == comment.val.trim().indexOf('if')) { this.buffer(''); } else { 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.trimLeft(); this.buf.push('var __val__ = ' + val); val = 'null == __val__ ? "" : __val__'; if (code.escape) val = 'escape(' + val + ')'; this.buf.push("buf.push(" + val + ");"); } else { this.buf.push(code.val); } // Block support if (code.block) { if (!code.buffer) this.buf.push('{'); this.visit(code.block); if (!code.buffer) this.buf.push('}'); } }, /** * Visit `each` block. * * @param {Each} each * @api public */ visitEach: function(each){ this.buf.push('' + '// iterate ' + each.obj + '\n' + ';(function(){\n' + ' if (\'number\' == typeof ' + each.obj + '.length) {\n' + ' for (var ' + each.key + ' = 0, $$l = ' + each.obj + '.length; ' + each.key + ' < $$l; ' + each.key + '++) {\n' + ' var ' + each.val + ' = ' + each.obj + '[' + each.key + '];\n'); this.visit(each.block); this.buf.push('' + ' }\n' + ' } else {\n' + ' for (var ' + each.key + ' in ' + each.obj + ') {\n' // if browser // + ' if (' + each.obj + '.hasOwnProperty(' + each.key + ')){' // end + ' var ' + each.val + ' = ' + each.obj + '[' + each.key + '];\n'); this.visit(each.block); // if browser // this.buf.push(' }\n'); // end this.buf.push(' }\n }\n}).call(this);\n'); }, /** * Visit `attrs`. * * @param {Array} attrs * @api public */ visitAttributes: function(attrs){ var val = this.attrs(attrs); if (val.inherits) { this.buf.push("buf.push(attrs(merge({ " + val.buf + " }, attributes), merge(" + val.escaped + ", escaped, true)));"); } else if (val.constant) { eval('var buf={' + val.buf + '};'); this.buffer(runtime.attrs(buf, JSON.parse(val.escaped)), true); } else { this.buf.push("buf.push(attrs({ " + val.buf + " }, " + val.escaped + "));"); } }, /** * Compile attributes. */ attrs: function(attrs){ var buf = [] , classes = [] , escaped = {} , constant = attrs.every(function(attr){ return isConstant(attr.val) }) , inherits = false; if (this.terse) buf.push('terse: true'); attrs.forEach(function(attr){ if (attr.name == 'attributes') return inherits = true; escaped[attr.name] = attr.escaped; if (attr.name == 'class') { classes.push('(' + attr.val + ')'); } else { var pair = "'" + attr.name + "':(" + attr.val + ')'; buf.push(pair); } }); if (classes.length) { classes = classes.join(" + ' ' + "); buf.push("class: " + classes); } return { buf: buf.join(', ').replace('class:', '"class":'), escaped: JSON.stringify(escaped), inherits: inherits, constant: constant }; } }; /** * Check if expression can be evaluated to a constant * * @param {String} expression * @return {Boolean} * @api private */ function isConstant(val){ // Check strings/literals if (/^ *("([^"\\]*(\\.[^"\\]*)*)"|'([^'\\]*(\\.[^'\\]*)*)'|true|false|null|undefined) *$/i.test(val)) return true; // Check numbers if (!isNaN(Number(val))) return true; // Check arrays var matches; if (matches = /^ *\[(.*)\] *$/.exec(val)) return matches[1].split(',').every(isConstant); return false; } /** * Escape the given string of `html`. * * @param {String} html * @return {String} * @api private */ function escape(html){ return String(html) .replace(/&(?!\w+;)/g, '&') .replace(//g, '>') .replace(/"/g, '"'); }