724 lines
19 KiB
JavaScript
724 lines
19 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
var nodes = require('./nodes');
|
||
|
var filters = require('./filters');
|
||
|
var doctypes = require('./doctypes');
|
||
|
var runtime = require('./runtime');
|
||
|
var utils = require('./utils');
|
||
|
var selfClosing = require('void-elements');
|
||
|
var parseJSExpression = require('character-parser').parseMax;
|
||
|
var constantinople = require('constantinople');
|
||
|
|
||
|
function isConstant(src) {
|
||
|
return constantinople(src, {jade: runtime, 'jade_interp': undefined});
|
||
|
}
|
||
|
function toConstant(src) {
|
||
|
return constantinople.toConstant(src, {jade: runtime, 'jade_interp': undefined});
|
||
|
}
|
||
|
function errorAtNode(node, error) {
|
||
|
error.line = node.line;
|
||
|
error.filename = node.filename;
|
||
|
return error;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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;
|
||
|
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;
|
||
|
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("var jade_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] = '';
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
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){
|
||
|
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 {
|
||
|
var rest = match[3];
|
||
|
var range = parseJSExpression(rest);
|
||
|
var code = ('!' == match[2] ? '' : 'jade.escape') + "((jade_interp = " + range.src + ") == null ? '' : jade_interp)";
|
||
|
this.bufferExpression(code);
|
||
|
this.buffer(rest.substr(range.end + 1), true);
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
str = utils.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) {
|
||
|
if (isConstant(src)) {
|
||
|
return this.buffer(toConstant(src) + '', 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(this.pp));
|
||
|
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(new jade.DebugItem( ' + node.line
|
||
|
+ ', ' + (node.filename
|
||
|
? utils.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 + ':');
|
||
|
}
|
||
|
if (node.block) {
|
||
|
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.pp) + "');");
|
||
|
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.slice();
|
||
|
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("jade_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);
|
||
|
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 {
|
||
|
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 jade_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 + ' = jade_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 (jade_interp = ' + args.length + '; jade_interp < arguments.length; jade_interp++) {');
|
||
|
this.buf.push(' ' + rest + '.push(arguments[jade_interp]);');
|
||
|
this.buf.push('}');
|
||
|
}
|
||
|
this.parentIndents++;
|
||
|
this.visit(block);
|
||
|
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
|
||
|
* @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 (tag.selfClosing || (!this.xml && selfClosing[tag.name])) {
|
||
|
this.buffer('<');
|
||
|
bufferName();
|
||
|
this.visitAttributes(tag.attrs, tag.attributeBlocks.slice());
|
||
|
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 errorAtNode(tag, new Error(name + ' is self closing and should not have content.'));
|
||
|
}
|
||
|
} else {
|
||
|
// Optimize attributes buffering
|
||
|
this.buffer('<');
|
||
|
bufferName();
|
||
|
this.visitAttributes(tag.attrs, tag.attributeBlocks.slice());
|
||
|
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.filename = this.options.filename;
|
||
|
try {
|
||
|
this.buffer(filters(filter.name, text, filter.attrs), true);
|
||
|
} catch (err) {
|
||
|
throw errorAtNode(filter, err);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* 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.trim();
|
||
|
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(',') + ']), ' + utils.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 (key === 'style') val = runtime.style(val);
|
||
|
if (escaped && !(key.indexOf('data') === 0 && typeof val !== 'string')) {
|
||
|
val = runtime.escape(val);
|
||
|
}
|
||
|
buf.push(utils.stringify(key) + ': ' + utils.stringify(val));
|
||
|
}
|
||
|
} else {
|
||
|
if (buffer) {
|
||
|
this.bufferExpression('jade.attr("' + key + '", ' + attr.val + ', ' + utils.stringify(escaped) + ', ' + utils.stringify(this.terse) + ')');
|
||
|
} else {
|
||
|
var val = attr.val;
|
||
|
if (key === 'style') {
|
||
|
val = 'jade.style(' + 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(utils.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(',') + '], ' + utils.stringify(classEscaping) + ')');
|
||
|
}
|
||
|
} else if (classes.length) {
|
||
|
if (classes.every(isConstant)) {
|
||
|
classes = utils.stringify(runtime.joinClasses(classes.map(toConstant).map(runtime.joinClasses).map(function (cls, i) {
|
||
|
return classEscaping[i] ? runtime.escape(cls) : cls;
|
||
|
})));
|
||
|
} else {
|
||
|
classes = '(jade_interp = ' + utils.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(',') + '}';
|
||
|
}
|
||
|
};
|