pixelnode/node_modules/jade/lib/compiler.js

642 lines
15 KiB
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')
, 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 || '<!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;
}
},
/**
* Buffer an indent based on the current `indent`
* property and an additional `offset`.
*
* @param {Number} offset
* @param {Boolean} newline
* @api public
*/
prettyIndent: function(offset, newline){
offset = offset || 0;
newline = newline ? '\\n' : '';
this.buffer(newline + Array(this.indents + offset).join(' '));
if (this.parentIndents)
this.buf.push("buf.push.apply(buf, __indent);");
},
/**
* Visit `node`.
*
* @param {Node} node
* @api public
*/
visit: function(node){
var debug = this.debug;
if (debug) {
this.buf.push('__jade.unshift({ lineno: ' + node.line
+ ', filename: ' + (node.filename
? JSON.stringify(node.filename)
: '__jade[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('__jade.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
, escape = this.escape
, pp = this.pp
// Block keyword has a special meaning in mixins
if (this.parentIndents && block.mode) {
if (pp) this.buf.push("__indent.push('" + Array(this.indents + 1).join(' ') + "');")
this.buf.push('block && block();');
if (pp) this.buf.push("__indent.pop();")
return;
}
// Pretty print multi-line text
if (pp && len > 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('</' + 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.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('<!--' + 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 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}