jade
Version:
Jade template engine
502 lines (427 loc) • 11.4 kB
JavaScript
/*!
* Jade - Compiler
* Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var nodes = require('./nodes')
, filters = require('./filters')
, doctypes = require('./doctypes')
, selfClosing = require('./self-closing')
, inlineTags = require('./inline-tags')
, 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(obj);
// }
// }
// 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;
if (options.doctype) this.setDoctype(options.doctype);
};
/**
* Compiler prototype.
*/
Compiler.prototype = {
/**
* Compile parse tree to JavaScript.
*
* @api public
*/
compile: function(){
this.buf = ['var interp;'];
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 || '<!DOCTYPE ' + name + '>';
this.doctype = doctype;
this.terse = '5' == name || 'html' == name;
this.xml = 0 == this.doctype.indexOf('<?xml');
},
/**
* Buffer the given `str` optionally escaped.
*
* @param {String} str
* @param {Boolean} esc
* @api public
*/
buffer: function(str, esc){
if (esc) str = utils.escape(str);
if (this.lastBufferedIdx == this.buf.length) {
this.lastBuffered += str;
this.buf[this.lastBufferedIdx - 1] = "buf.push('" + this.lastBuffered + "');"
} else {
this.buf.push("buf.push('" + str + "');");
this.lastBuffered = str;
this.lastBufferedIdx = this.buf.length;
}
},
/**
* Visit `node`.
*
* @param {Node} node
* @api public
*/
visit: function(node){
var debug = this.debug;
if (debug) {
this.buf.push('__.unshift({ lineno: ' + node.line
+ ', filename: ' + (node.filename
? '"' + node.filename + '"'
: '__[0].filename')
+ ' });');
}
// Massive hack to fix our context
// stack for - else[ if] etc
if (false === node.debug && this.debug) {
this.buf.pop();
this.buf.pop();
}
this.visitNode(node);
if (debug) this.buf.push('__.shift();');
},
/**
* Visit `node`.
*
* @param {Node} node
* @api public
*/
visitNode: function(node){
var name = node.constructor.name
|| node.constructor.toString().match(/function ([^(\s]+)()/)[1];
return this['visit' + name](node);
},
/**
* Visit case `node`.
*
* @param {Literal} node
* @api public
*/
visitCase: function(node){
var _ = this.withinCase;
this.withinCase = true;
this.buf.push('switch (' + node.expr + '){');
this.visit(node.block);
this.buf.push('}');
this.withinCase = _;
},
/**
* Visit when `node`.
*
* @param {Literal} node
* @api public
*/
visitWhen: function(node){
if ('default' == node.expr) {
this.buf.push('default:');
} else {
this.buf.push('case ' + node.expr + ':');
}
this.visit(node.block);
this.buf.push(' break;');
},
/**
* Visit literal `node`.
*
* @param {Literal} node
* @api public
*/
visitLiteral: function(node){
var str = node.str.replace(/\n/g, '\\\\n');
this.buffer(str);
},
/**
* Visit all nodes in `block`.
*
* @param {Block} block
* @api public
*/
visitBlock: function(block){
var len = block.nodes.length;
for (var i = 0; i < len; ++i) {
this.visit(block.nodes[i]);
}
},
/**
* 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 || '';
if (mixin.block) {
this.buf.push('var ' + name + ' = function(' + args + '){');
this.visit(mixin.block);
this.buf.push('}');
} else {
this.buf.push(name + '(' + args + ');');
}
},
/**
* 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;
if (!this.hasCompiledTag) {
if (!this.hasCompiledDoctype && 'html' == name) {
this.visitDoctype();
}
this.hasCompiledTag = true;
}
// pretty print
if (this.pp && inlineTags.indexOf(name) == -1) {
this.buffer('\\n' + Array(this.indents).join(' '));
}
if (~selfClosing.indexOf(name) && !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);
if (tag.text) this.buffer(utils.text(tag.text.nodes[0].trimLeft()));
this.escape = 'pre' == tag.name;
this.visit(tag.block);
// pretty print
if (this.pp && !~inlineTags.indexOf(name) && !tag.textOnly) {
this.buffer('\\n' + Array(this.indents).join(' '));
}
this.buffer('</' + name + '>');
}
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.join('');
this.buffer(utils.text(fn(text, filter.attrs)));
}
},
/**
* Visit `text` node.
*
* @param {Text} text
* @api public
*/
visitText: function(text){
text = utils.text(text.nodes.join(''));
if (this.escape) text = escape(text);
this.buffer(text);
this.buffer('\\n');
},
/**
* 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.buffer('\\n' + Array(this.indents + 1).join(' '));
this.buffer('<!--' + utils.escape(comment.val) + '-->');
},
/**
* Visit a `BlockComment`.
*
* @param {Comment} comment
* @api public
*/
visitBlockComment: function(comment){
if (!comment.buffer) return;
if (0 == comment.val.trim().indexOf('if')) {
this.buffer('<!--[' + comment.val.trim() + ']>');
this.visit(comment.block);
this.buffer('<![endif]-->');
} else {
this.buffer('<!--' + comment.val);
this.visit(comment.block);
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 buf = []
, classes = [];
if (this.terse) buf.push('terse: true');
attrs.forEach(function(attr){
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);
}
buf = buf.join(', ').replace('class:', '"class":');
this.buf.push("buf.push(attrs({ " + buf + " }));");
}
};
/**
* 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, '>')
.replace(/"/g, '"');
}