|
|
'use strict';
var nodes = require('./nodes'); var filters = require('./filters'); var doctypes = require('./doctypes'); var selfClosing = require('./self-closing'); var runtime = require('./runtime'); var utils = require('./utils'); var parseJSExpression = require('character-parser').parseMax; var isConstant = require('constantinople'); var toConstant = require('constantinople').toConstant;
/** * 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; this.terse = false; if (options.doctype) this.setDoctype(options.doctype); };
/** * Compiler prototype. */
Compiler.prototype = {
/** * Compile parse tree to JavaScript. * * @api public */
compile: function(){ this.buf = []; if (this.pp) this.buf.push("jade.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){ name = name || 'default'; this.doctype = doctypes[name.toLowerCase()] || '<!DOCTYPE ' + name + '>'; this.terse = this.doctype.toLowerCase() == '<!doctype html>'; this.xml = 0 == this.doctype.indexOf('<?xml'); },
/** * Buffer the given `str` exactly as is or with interpolation * * @param {String} str * @param {Boolean} interpolate * @api public */
buffer: function (str, interpolate) { var self = this; if (interpolate) { var match = /(\\)?([#!]){((?:.|\n)*)$/.exec(str); if (match) { this.buffer(str.substr(0, match.index), false); if (match[1]) { // escape
this.buffer(match[2] + '{', false); this.buffer(match[3], true); return; } else { try { var rest = match[3]; var range = parseJSExpression(rest); var code = ('!' == match[2] ? '' : 'jade.escape') + "((jade.interp = " + range.src + ") == null ? '' : jade.interp)"; } catch (ex) { throw ex; //didn't match, just as if escaped
this.buffer(match[2] + '{', false); this.buffer(match[3], true); return; } this.bufferExpression(code); this.buffer(rest.substr(range.end + 1), true); return; } } }
str = JSON.stringify(str); str = str.substr(1, str.length - 2);
if (this.lastBufferedIdx == this.buf.length) { if (this.lastBufferedType === 'code') this.lastBuffered += ' + "'; this.lastBufferedType = 'text'; this.lastBuffered += str; this.buf[this.lastBufferedIdx - 1] = 'buf.push(' + this.bufferStartChar + this.lastBuffered + '");' } else { this.buf.push('buf.push("' + str + '");'); this.lastBufferedType = 'text'; this.bufferStartChar = '"'; this.lastBuffered = str; this.lastBufferedIdx = this.buf.length; } },
/** * Buffer the given `src` so it is evaluated at run time * * @param {String} src * @api public */
bufferExpression: function (src) { var fn = Function('', 'return (' + src + ');'); if (isConstant(src)) { return this.buffer(fn(), false) } if (this.lastBufferedIdx == this.buf.length) { if (this.lastBufferedType === 'text') this.lastBuffered += '"'; this.lastBufferedType = 'code'; this.lastBuffered += ' + (' + src + ')'; this.buf[this.lastBufferedIdx - 1] = 'buf.push(' + this.bufferStartChar + this.lastBuffered + ');' } else { this.buf.push('buf.push(' + src + ');'); this.lastBufferedType = 'code'; this.bufferStartChar = ''; this.lastBuffered = '(' + src + ')'; 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, jade.indent);"); },
/** * Visit `node`. * * @param {Node} node * @api public */
visit: function(node){ var debug = this.debug;
if (debug) { this.buf.push('jade_debug.unshift({ lineno: ' + node.line + ', filename: ' + (node.filename ? JSON.stringify(node.filename) : 'jade_debug[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_debug.shift();'); },
/** * Visit `node`. * * @param {Node} node * @api public */
visitNode: function(node){ return this['visit' + node.type](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){ this.buffer(node.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
// 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 a mixin's `block` keyword. * * @param {MixinBlock} block * @api public */
visitMixinBlock: function(block){ if (this.pp) this.buf.push("jade.indent.push('" + Array(this.indents + 1).join(' ') + "');"); this.buf.push('block && block();'); if (this.pp) this.buf.push("jade.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 || '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 = 'jade_mixins['; var args = mixin.args || ''; var block = mixin.block; var attrs = mixin.attrs; var attrsBlocks = mixin.attributeBlocks; var pp = this.pp;
name += (mixin.name[0]=='#' ? mixin.name.substr(2,mixin.name.length-3):'"'+mixin.name+'"')+']';
if (mixin.call) { if (pp) this.buf.push("jade.indent.push('" + Array(this.indents + 1).join(' ') + "');") 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); 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); } this.buf.push('attributes: jade.merge([' + attrsBlocks.join(',') + '])'); } 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("jade.indent.pop();") } else { this.buf.push(name + ' = function(' + args + '){'); this.buf.push('var block = (this && this.block), attributes = (this && this.attributes) || {};'); 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 , self = this;
function bufferName() { if (tag.buffer) self.bufferExpression(name); else self.buffer(name); }
if ('pre' == tag.name) this.escape = 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 ((~selfClosing.indexOf(name) || tag.selfClosing) && !this.xml) { this.buffer('<'); bufferName(); this.visitAttributes(tag.attrs, tag.attributeBlocks); this.terse ? this.buffer('>') : this.buffer('/>'); // if it is non-empty throw an error
if (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)})) { throw new Error(name + ' is self closing and should not have content.'); } } else { // Optimize attributes buffering
this.buffer('<'); bufferName(); this.visitAttributes(tag.attrs, tag.attributeBlocks); this.buffer('>'); if (tag.code) this.visitCode(tag.code); this.visit(tag.block);
// pretty print
if (pp && !tag.isInline() && 'pre' != tag.name && !tag.canInline()) this.prettyIndent(0, true);
this.buffer('</'); bufferName(); this.buffer('>'); }
if ('pre' == tag.name) this.escape = false;
this.indents--; },
/** * Visit `filter`, throwing when the filter does not exist. * * @param {Filter} filter * @api public */
visitFilter: function(filter){ 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(filters(filter.name, text, filter.attrs), true); },
/** * Visit `text` node. * * @param {Text} text * @api public */
visitText: function(text){ this.buffer(text.val, true); },
/** * 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('<!--' + comment.val + '-->'); },
/** * Visit a `BlockComment`. * * @param {Comment} comment * @api public */
visitBlockComment: function(comment){ if (!comment.buffer) return; if (this.pp) this.prettyIndent(1, true); this.buffer('<!--' + comment.val); this.visit(comment.block); 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.trimLeft(); val = 'null == (jade.interp = '+val+') ? "" : jade.interp'; if (code.escape) val = 'jade.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); 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' + ' var $$obj = ' + each.obj + ';\n' + ' if (\'number\' == typeof $$obj.length) {\n');
if (each.alternative) { this.buf.push(' if ($$obj.length) {'); }
this.buf.push('' + ' for (var ' + each.key + ' = 0, $$l = $$obj.length; ' + each.key + ' < $$l; ' + each.key + '++) {\n' + ' var ' + each.val + ' = $$obj[' + each.key + '];\n');
this.visit(each.block);
this.buf.push(' }\n');
if (each.alternative) { this.buf.push(' } else {'); this.visit(each.alternative); this.buf.push(' }'); }
this.buf.push('' + ' } else {\n' + ' var $$l = 0;\n' + ' for (var ' + each.key + ' in $$obj) {\n' + ' $$l++;' + ' var ' + each.val + ' = $$obj[' + each.key + '];\n');
this.visit(each.block);
this.buf.push(' }\n'); if (each.alternative) { this.buf.push(' if ($$l === 0) {'); this.visit(each.alternative); 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); } this.bufferExpression('jade.attrs(jade.merge([' + attributeBlocks.join(',') + ']), ' + JSON.stringify(this.terse) + ')'); } else if (attrs.length) { this.attrs(attrs, true); } },
/** * Compile attributes. */
attrs: function(attrs, buffer){ var buf = []; var classes = []; var classEscaping = [];
attrs.forEach(function(attr){ var key = attr.name; var escaped = attr.escaped;
if (key === 'class') { classes.push(attr.val); classEscaping.push(attr.escaped); } else if (isConstant(attr.val)) { if (buffer) { this.buffer(runtime.attr(key, toConstant(attr.val), escaped, this.terse)); } else { var val = toConstant(attr.val); if (escaped && !(key.indexOf('data') === 0 && typeof val !== 'string')) { val = runtime.escape(val); } buf.push(JSON.stringify(key) + ': ' + JSON.stringify(val)); } } else { if (buffer) { this.bufferExpression('jade.attr("' + key + '", ' + attr.val + ', ' + JSON.stringify(escaped) + ', ' + JSON.stringify(this.terse) + ')'); } else { var val = attr.val; if (escaped && !(key.indexOf('data') === 0)) { val = 'jade.escape(' + val + ')'; } else if (escaped) { val = '(typeof (jade.interp = ' + val + ') == "string" ? jade.escape(jade.interp) : jade.interp)"'; } buf.push(JSON.stringify(key) + ': ' + val); } } }.bind(this)); if (buffer) { if (classes.every(isConstant)) { this.buffer(runtime.cls(classes.map(toConstant), classEscaping)); } else { this.bufferExpression('jade.cls([' + classes.join(',') + '], ' + JSON.stringify(classEscaping) + ')'); } } else { if (classes.every(isConstant)) { classes = JSON.stringify(runtime.joinClasses(classes.map(toConstant).map(runtime.joinClasses).map(function (cls, i) { return classEscaping[i] ? runtime.escape(cls) : cls; }))); } else if (classes.length) { classes = '(jade.interp = ' + JSON.stringify(classEscaping) + ',' + ' jade.joinClasses([' + classes.join(',') + '].map(jade.joinClasses).map(function (cls, i) {' + ' return jade.interp[i] ? jade.escape(cls) : cls' + ' }))' + ')'; } if (classes.length) buf.push('"class": ' + classes); } return '{' + buf.join(',') + '}'; } };
|