fruitstand
Version:
2,012 lines (1,730 loc) • 491 kB
JavaScript
!function(e){if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.jade=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
'use strict';
var nodes = _dereq_('./nodes');
var filters = _dereq_('./filters');
var doctypes = _dereq_('./doctypes');
var runtime = _dereq_('./runtime');
var utils = _dereq_('./utils');
var selfClosing = _dereq_('./self-closing');
var parseJSExpression = _dereq_('character-parser').parseMax;
var constantinople = _dereq_('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;
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 = 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) {
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(' '));
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 + ':');
}
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.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;
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(' ') + "');")
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;
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('};');
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.indexOf(tag.name) !== -1)) {
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 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);
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.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.length) {
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 {
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(',') + '}';
}
};
},{"./doctypes":2,"./filters":3,"./nodes":16,"./runtime":24,"./self-closing":25,"./utils":26,"character-parser":33,"constantinople":34}],2:[function(_dereq_,module,exports){
'use strict';
module.exports = {
'default': '<!DOCTYPE html>'
, 'xml': '<?xml version="1.0" encoding="utf-8" ?>'
, 'transitional': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
, 'strict': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
, 'frameset': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
, '1.1': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
, 'basic': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">'
, 'mobile': '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">'
};
},{}],3:[function(_dereq_,module,exports){
'use strict';
module.exports = filter;
function filter(name, str, options) {
if (typeof filter[name] === 'function') {
var res = filter[name](str, options);
} else {
throw new Error('unknown filter ":' + name + '"');
}
return res;
}
},{}],4:[function(_dereq_,module,exports){
'use strict';
module.exports = [
'a'
, 'abbr'
, 'acronym'
, 'b'
, 'br'
, 'code'
, 'em'
, 'font'
, 'i'
, 'img'
, 'ins'
, 'kbd'
, 'map'
, 'samp'
, 'small'
, 'span'
, 'strong'
, 'sub'
, 'sup'
];
},{}],5:[function(_dereq_,module,exports){
'use strict';
/*!
* Jade
* Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var Parser = _dereq_('./parser')
, Lexer = _dereq_('./lexer')
, Compiler = _dereq_('./compiler')
, runtime = _dereq_('./runtime')
, addWith = _dereq_('with')
, fs = _dereq_('fs');
/**
* Expose self closing tags.
*/
exports.selfClosing = _dereq_('./self-closing');
/**
* Default supported doctypes.
*/
exports.doctypes = _dereq_('./doctypes');
/**
* Text filters.
*/
exports.filters = _dereq_('./filters');
/**
* Utilities.
*/
exports.utils = _dereq_('./utils');
/**
* Expose `Compiler`.
*/
exports.Compiler = Compiler;
/**
* Expose `Parser`.
*/
exports.Parser = Parser;
/**
* Expose `Lexer`.
*/
exports.Lexer = Lexer;
/**
* Nodes.
*/
exports.nodes = _dereq_('./nodes');
/**
* Jade runtime helpers.
*/
exports.runtime = runtime;
/**
* Template function cache.
*/
exports.cache = {};
/**
* Parse the given `str` of jade and return a function body.
*
* @param {String} str
* @param {Object} options
* @return {String}
* @api private
*/
function parse(str, options){
// Parse
var parser = new (options.parser || Parser)(str, options.filename, options);
var tokens;
try {
// Parse
tokens = parser.parse();
} catch (err) {
parser = parser.context();
runtime.rethrow(err, parser.filename, parser.lexer.lineno, parser.input);
}
// Compile
var compiler = new (options.compiler || Compiler)(tokens, options);
var js;
try {
js = compiler.compile();
} catch (err) {
if (err.line && (err.filename || !options.filename)) {
runtime.rethrow(err, err.filename, err.line, parser.input);
}
}
// Debug compiler
if (options.debug) {
console.error('\nCompiled Function:\n\n\u001b[90m%s\u001b[0m', js.replace(/^/gm, ' '));
}
var globals = [];
globals.push('jade');
globals.push('jade_mixins');
globals.push('jade_interp');
globals.push('jade_debug');
globals.push('buf');
return ''
+ 'var buf = [];\n'
+ 'var jade_mixins = {};\n'
+ 'var jade_interp;\n'
+ (options.self
? 'var self = locals || {};\n' + js
: addWith('locals || {}', '\n' + js, globals)) + ';'
+ 'return buf.join("");';
}
/**
* Compile a `Function` representation of the given jade `str`.
*
* Options:
*
* - `compileDebug` when `false` debugging code is stripped from the compiled
template, when it is explicitly `true`, the source code is included in
the compiled template for better accuracy.
* - `filename` used to improve errors when `compileDebug` is not `false` and to resolve imports/extends
*
* @param {String} str
* @param {Options} options
* @return {Function}
* @api public
*/
exports.compile = function(str, options){
var options = options || {}
, filename = options.filename
? JSON.stringify(options.filename)
: 'undefined'
, fn;
str = String(str);
if (options.compileDebug !== false) {
fn = [
'var jade_debug = [{ lineno: 1, filename: ' + filename + ' }];'
, 'try {'
, parse(str, options)
, '} catch (err) {'
, ' jade.rethrow(err, jade_debug[0].filename, jade_debug[0].lineno' + (options.compileDebug === true ? ',' + JSON.stringify(str) : '') + ');'
, '}'
].join('\n');
} else {
fn = parse(str, options);
}
fn = new Function('locals, jade', fn)
var res = function(locals){ return fn(locals, Object.create(runtime)) };
if (options.client) {
res.toString = function () {
var err = new Error('The `client` option is deprecated, use `jade.compileClient`');
console.error(err.stack || err.message);
return exports.compileClient(str, options);
};
}
return res;
};
/**
* Compile a JavaScript source representation of the given jade `str`.
*
* Options:
*
* - `compileDebug` When it is `true`, the source code is included in
the compiled template for better error messages.
* - `filename` used to improve errors when `compileDebug` is not `true` and to resolve imports/extends
*
* @param {String} str
* @param {Options} options
* @return {String}
* @api public
*/
exports.compileClient = function(str, options){
var options = options || {}
, filename = options.filename
? JSON.stringify(options.filename)
: 'undefined'
, fn;
str = String(str);
if (options.compileDebug) {
options.compileDebug = true;
fn = [
'var jade_debug = [{ lineno: 1, filename: ' + filename + ' }];'
, 'try {'
, parse(str, options)
, '} catch (err) {'
, ' jade.rethrow(err, jade_debug[0].filename, jade_debug[0].lineno, ' + JSON.stringify(str) + ');'
, '}'
].join('\n');
} else {
options.compileDebug = false;
fn = parse(str, options);
}
return 'function template(locals) {\n' + fn + '\n}';
};
/**
* Render the given `str` of jade.
*
* Options:
*
* - `cache` enable template caching
* - `filename` filename required for `include` / `extends` and caching
*
* @param {String} str
* @param {Object|Function} options or fn
* @param {Function|undefined} fn
* @returns {String}
* @api public
*/
exports.render = function(str, options, fn){
// support callback API
if ('function' == typeof options) {
fn = options, options = undefined;
}
if (typeof fn === 'function') {
var res
try {
res = exports.render(str, options);
} catch (ex) {
return fn(ex);
}
return fn(null, res);
}
options = options || {};
// cache requires .filename
if (options.cache && !options.filename) {
throw new Error('the "filename" option is required for caching');
}
var path = options.filename;
var tmpl = options.cache
? exports.cache[path] || (exports.cache[path] = exports.compile(str, options))
: exports.compile(str, options);
return tmpl(options);
};
/**
* Render a Jade file at the given `path`.
*
* @param {String} path
* @param {Object|Function} options or callback
* @param {Function|undefined} fn
* @returns {String}
* @api public
*/
exports.renderFile = function(path, options, fn){
// support callback API
if ('function' == typeof options) {
fn = options, options = undefined;
}
if (typeof fn === 'function') {
var res
try {
res = exports.renderFile(path, options);
} catch (ex) {
return fn(ex);
}
return fn(null, res);
}
options = options || {};
var key = path + ':string';
options.filename = path;
var str = options.cache
? exports.cache[key] || (exports.cache[key] = fs.readFileSync(path, 'utf8'))
: fs.readFileSync(path, 'utf8');
return exports.render(str, options);
};
/**
* Compile a Jade file at the given `path` for use on the client.
*
* @param {String} path
* @param {Object} options
* @returns {String}
* @api public
*/
exports.compileFileClient = function(path, options){
options = options || {};
var key = path + ':string';
options.filename = path;
var str = options.cache
? exports.cache[key] || (exports.cache[key] = fs.readFileSync(path, 'utf8'))
: fs.readFileSync(path, 'utf8');
return exports.compileClient(str, options);
};
/**
* Express support.
*/
exports.__express = exports.renderFile;
},{"./compiler":1,"./doctypes":2,"./filters":3,"./lexer":6,"./nodes":16,"./parser":23,"./runtime":24,"./self-closing":25,"./utils":26,"fs":27,"with":46}],6:[function(_dereq_,module,exports){
'use strict';
var utils = _dereq_('./utils');
var characterParser = _dereq_('character-parser');
/**
* Initialize `Lexer` with the given `str`.
*
* @param {String} str
* @param {String} filename
* @api private
*/
var Lexer = module.exports = function Lexer(str, filename) {
this.input = str.replace(/\r\n|\r/g, '\n');
this.filename = filename;
this.deferredTokens = [];
this.lastIndents = 0;
this.lineno = 1;
this.stash = [];
this.indentStack = [];
this.indentRe = null;
this.pipeless = false;
};
function assertExpression(exp) {
//this verifies that a JavaScript expression is valid
Function('', 'return (' + exp + ')');
}
function assertNestingCorrect(exp) {
//this verifies that code is properly nested, but allows
//invalid JavaScript such as the contents of `attributes`
var res = characterParser(exp)
if (res.isNesting()) {
throw new Error('Nesting must match on expression `' + exp + '`')
}
}
/**
* Lexer prototype.
*/
Lexer.prototype = {
/**
* Construct a token with the given `type` and `val`.
*
* @param {String} type
* @param {String} val
* @return {Object}
* @api private
*/
tok: function(type, val){
return {
type: type
, line: this.lineno
, val: val
}
},
/**
* Consume the given `len` of input.
*
* @param {Number} len
* @api private
*/
consume: function(len){
this.input = this.input.substr(len);
},
/**
* Scan for `type` with the given `regexp`.
*
* @param {String} type
* @param {RegExp} regexp
* @return {Object}
* @api private
*/
scan: function(regexp, type){
var captures;
if (captures = regexp.exec(this.input)) {
this.consume(captures[0].length);
return this.tok(type, captures[1]);
}
},
/**
* Defer the given `tok`.
*
* @param {Object} tok
* @api private
*/
defer: function(tok){
this.deferredTokens.push(tok);
},
/**
* Lookahead `n` tokens.
*
* @param {Number} n
* @return {Object}
* @api private
*/
lookahead: function(n){
var fetch = n - this.stash.length;
while (fetch-- > 0) this.stash.push(this.next());
return this.stash[--n];
},
/**
* Return the indexOf `(` or `{` or `[` / `)` or `}` or `]` delimiters.
*
* @return {Number}
* @api private
*/
bracketExpression: function(skip){
skip = skip || 0;
var start = this.input[skip];
if (start != '(' && start != '{' && start != '[') throw new Error('unrecognized start character');
var end = ({'(': ')', '{': '}', '[': ']'})[start];
var range = characterParser.parseMax(this.input, {start: skip + 1});
if (this.input[range.end] !== end) throw new Error('start character ' + start + ' does not match end character ' + this.input[range.end]);
return range;
},
/**
* Stashed token.
*/
stashed: function() {
return this.stash.length
&& this.stash.shift();
},
/**
* Deferred token.
*/
deferred: function() {
return this.deferredTokens.length
&& this.deferredTokens.shift();
},
/**
* end-of-source.
*/
eos: function() {
if (this.input.length) return;
if (this.indentStack.length) {
this.indentStack.shift();
return this.tok('outdent');
} else {
return this.tok('eos');
}
},
/**
* Blank line.
*/
blank: function() {
var captures;
if (captures = /^\n *\n/.exec(this.input)) {
this.consume(captures[0].length - 1);
++this.lineno;
if (this.pipeless) return this.tok('text', '');
return this.next();
}
},
/**
* Comment.
*/
comment: function() {
var captures;
if (captures = /^\/\/(-)?([^\n]*)/.exec(this.input)) {
this.consume(captures[0].length);
var tok = this.tok('comment', captures[2]);
tok.buffer = '-' != captures[1];
return tok;
}
},
/**
* Interpolated tag.
*/
interpolation: function() {
if (/^#\{/.test(this.input)) {
var match;
try {
match = this.bracketExpression(1);
} catch (ex) {
return;//not an interpolation expression, just an unmatched open interpolation
}
this.consume(match.end + 1);
return this.tok('interpolation', match.src);
}
},
/**
* Tag.
*/
tag: function() {
var captures;
if (captures = /^(\w[-:\w]*)(\/?)/.exec(this.input)) {
this.consume(captures[0].length);
var tok, name = captures[1];
if (':' == name[name.length - 1]) {
name = name.slice(0, -1);
tok = this.tok('tag', name);
this.defer(this.tok(':'));
while (' ' == this.input[0]) this.input = this.input.substr(1);
} else {
tok = this.tok('tag', name);
}
tok.selfClosing = !!captures[2];
return tok;
}
},
/**
* Filter.
*/
filter: function() {
return this.scan(/^:([\w\-]+)/, 'filter');
},
/**
* Doctype.
*/
doctype: function() {
if (this.scan(/^!!! *([^\n]+)?/, 'doctype')) {
throw new Error('`!!!` is deprecated, you must now use `doctype`');
}
var node = this.scan(/^(?:doctype) *([^\n]+)?/, 'doctype');
if (node && node.val && node.val.trim() === '5') {
throw new Error('`doctype 5` is deprecated, you must now use `doctype html`');
}
return node;
},
/**
* Id.
*/
id: function() {
return this.scan(/^#([\w-]+)/, 'id');
},
/**
* Class.
*/
className: function() {
return this.scan(/^\.([\w-]+)/, 'class');
},
/**
* Text.
*/
text: function() {
return this.scan(/^(?:\| ?| )([^\n]+)/, 'text') || this.scan(/^(<[^\n]*)/, 'text');
},
textFail: function () {
var tok;
if (tok = this.scan(/^([^\.\n][^\n]+)/, 'text')) {
console.warn('Warning: missing space before text for line ' + this.lineno +
' of jade file "' + this.filename + '"');
return tok;
}
},
/**
* Dot.
*/
dot: function() {
return this.scan(/^\./, 'dot');
},
/**
* Extends.
*/
"extends": function() {
return this.scan(/^extends? +([^\n]+)/, 'extends');
},
/**
* Block prepend.
*/
prepend: function() {
var captures;
if (captures = /^prepend +([^\n]+)/.exec(this.input)) {
this.consume(captures[0].length);
var mode = 'prepend'
, name = captures[1]
, tok = this.tok('block', name);
tok.mode = mode;
return tok;
}
},
/**
* Block append.
*/
append: function() {
var captures;
if (captures = /^append +([^\n]+)/.exec(this.input)) {
this.consume(captures[0].length);
var mode = 'append'
, name = captures[1]
, tok = this.tok('block', name);
tok.mode = mode;
return tok;
}
},
/**
* Block.
*/
block: function() {
var captures;
if (captures = /^block\b *(?:(prepend|append) +)?([^\n]+)/.exec(this.input)) {
this.consume(captures[0].length);
var mode = captures[1] || 'replace'
, name = captures[2]
, tok = this.tok('block', name);
tok.mode = mode;
return tok;
}
},
/**
* Mixin Block.
*/
mixinBlock: function() {
var captures;
if (captures = /^block\s*(\n|$)/.exec(this.input)) {
this.consume(captures[0].length - 1);
return this.tok('mixin-block');
}
},
/**
* Yield.
*/
yield: function() {
return this.scan(/^yield */, 'yield');
},
/**
* Include.
*/
include: function() {
return this.scan(/^include +([^\n]+)/, 'include');
},
/**
* Include with filter
*/
includeFiltered: function() {
var captures;
if (captures = /^include:([\w\-]+) +([^\n]+)/.exec(this.input)) {
this.consume(captures[0].length);
var filter = captures[1];
var path = captures[2];
var tok = this.tok('include', path);
tok.filter = filter;
return tok;
}
},
/**
* Case.
*/
"case": function() {
return this.scan(/^case +([^\n]+)/, 'case');
},
/**
* When.
*/
when: function() {
return this.scan(/^when +([^:\n]+)/, 'when');
},
/**
* Default.
*/
"default": function() {
return this.scan(/^default */, 'default');
},
/**
* Call mixin.
*/
call: function(){
var tok, captures;
if (captures = /^\+(([-\w]+)|(#\{))/.exec(this.input)) {
// try to consume simple or interpolated call
if (captures[2]) {
// simple call
this.consume(captures[0].length);
tok = this.tok('call', captures[2]);
} else {
// interpolated call
var match;
try {
match = this.bracketExpression(2);
} catch (ex) {
return;//not an interpolation expression, just an unmatched open interpolation
}
this.consume(match.end + 1);
assertExpression(match.src);
tok = this.tok('call', '#{'+match.src+'}');
}
// Check for args (not attributes)
if (captures = /^ *\(/.exec(this.input)) {
try {
var range = this.bracketExpression(captures[0].length - 1);
if (!/^ *[-\w]+ *=/.test(range.src)) { // not attributes
this.consume(range.end + 1);
tok.args = range.src;
}
} catch (ex) {
//not a bracket expcetion, just unmatched open parens
}
}
return tok;
}
},
/**
* Mixin.
*/
mixin: function(){
var captures;
if (captures = /^mixin +([-\w]+)(?: *\((.*)\))? */.exec(this.input)) {
this.consume(captures[0].length);
var tok = this.tok('mixin', captures[1]);
tok.args = captures[2];
return tok;
}
},
/**
* Conditional.
*/
conditional: function() {
var captures;
if (captures = /^(if|unless|else if|else)\b([^\n]*)/.exec(this.input)) {
this.consume(captures[0].length);
var type = captures[1]
var js = captures[2];
var isIf = false;
var isElse = false;
switch (type) {
case 'if':
assertExpression(js)
js = 'if (' + js + ')';
isIf = true;
break;
case 'unless':
assertExpression(js)
js = 'if (!(' + js + '))';
isIf = true;
break;
case 'else if':
assertExpression(js)
js = 'else if (' + js + ')';
isIf = true;
isElse = true;
break;
case 'else':
if (js && js.trim()) {
throw new Error('`else` cannot have a condition, perhaps you meant `else if`');
}
js = 'else';
isElse = true;
break;
}
var tok = this.tok('code', js);
tok.isElse = isElse;
tok.isIf = isIf;
tok.requiresBlock = true;
return tok;
}
},
/**
* While.
*/
"while": function() {
var captures;
if (captures = /^while +([^\n]+)/.exec(this.input)) {
this.consume(captures[0].length);
assertExpression(captures[1])
var tok = this.tok('code', 'while (' + captures[1] + ')');
tok.requiresBlock = true;
return tok;
}
},
/**
* Each.
*/
each: function() {
var captures;
if (captures = /^(?:- *)?(?:each|for) +([a-zA-Z_$][\w$]*)(?: *, *([a-zA-Z_$][\w$]*))? * in *([^\n]+)/.exec(this.input)) {
this.consume(captures[0].length);
var tok = this.tok('each', captures[1]);
tok.key = captures[2] || '$index';
assertExpression(captures[3])
tok.code = captures[3];
return tok;
}
},
/**
* Code.
*/
code: function() {
var captures;
if (captures = /^(!?=|-)[ \t]*([^\n]+)/.exec(this.input)) {
this.consume(captures[0].length);
var flags = captures[1];
captures[1] = captures[2];
var tok = this.tok('code', captures[1]);
tok.escape = flags.charAt(0) === '=';
tok.buffer = flags.charAt(0) === '=' || flags.charAt(1) === '=';
if (tok.buffer) assertExpression(captures[1])
return tok;
}
},
/**
* Attributes.
*/
attrs: function() {
if ('(' == this.input.charAt(0)) {
var index = this.bracketExpression().end
, str = this.input.substr(1, index-1)
, tok = this.tok('attrs');
assertNestingCorrect(str);
var quote = '';
var interpolate = function (attr) {
return attr.replace(/(\\)?#\{(.+)/g, function(_, escape, expr){
if (escape) return _;
try {
var range = characterParser.parseMax(expr);
if (expr[range.end] !== '}') return _.substr(0, 2) + interpolate(_.substr(2));
assertExpression(range.src)
return quote + " + (" + range.src + ") + " + quote + interpolate(expr.substr(range.end + 1));
} catch (ex) {
return _.substr(0, 2) + interpolate(_.substr(2));
}
});
}
this.consume(index + 1);
tok.attrs = [];
var escapedAttr = true
var key = '';
var val = '';
var interpolatable = '';
var state = characterParser.defaultState();
var loc = 'key';
var isEndOfAttribute = function (i) {
if (key.trim() === '') return false;
if (i === str.length) return true;
if (loc === 'key') {
if (str[i] === ' ' || str[i] === '\n') {
for (var x = i; x < str.length; x++) {
if (str[x] != ' ' && str[x] != '\n') {
if (str[x] === '=' || str[x] === '!' || str[x] === ',') return false;
else return true;
}
}
}
return str[i] === ','
} else if (loc === 'value' && !state.isNesting()) {
try {
Function('', 'return (' + val + ');');
if (str[i] === ' ' || str[i] === '\n') {
for (var x = i; x < str.length; x++) {
if (str[x] != ' ' && str[x] != '\n') {
if (characterParser.isPunctuator(str[x]) && str[x] != '"' && str[x] != "'") return false;
else return true;
}
}
}
return str[i] === ',';
} catch (ex) {
return false;
}
}
}
this.lineno += str.split("\n").length - 1;
for (var i = 0; i <= str.length; i++) {
if (isEndOfAttribute(i)) {
val = val.trim();
if (val) assertExpression(val)
key = key.trim();
key = key.replace(/^['"]|['"]$/g, '');
tok.attrs.push({
name: key,
val: '' == val ? true : val,
escaped: escapedAttr
});
key = val = '';
loc = 'key';
escapedAttr = false;
} else {
switch (loc) {
case 'key-char':
if (str[i] === quote) {
loc = 'key';
if (i + 1 < str.length && [' ', ',', '!', '=', '\n'].indexOf(str[i + 1]) === -1)
throw new Error('Unexpected character ' + str[i + 1] + ' expected ` `, `\\n`, `,`, `!` or `=`');
} else {
key += str[i];
}
break;
case 'key':
if (key === '' && (str[i] === '"' || str[i] === "'")) {
loc = 'key-char';
quote = str[i];
} else if (str[i] === '!' || str[i] === '=') {
escapedAttr = str[i] !== '!';
if (str[i] === '!') i++;
if (str[i] !== '=') throw new Error('Unexpected character ' + str[i] + ' expected `=`');
loc = 'value';
state = characterParser.defaultState();
} else {
key += str[i]
}
break;
case 'value':
state = characterParser.parseChar(str[i], state);
if (state.isString()) {
loc = 'string';
quote = str[i];
interpolatable = str[i];
} else {
val += str[i];
}
break;
case 'string':
state = characterParser.parseChar(str[i], state);
interpolatable += str[i];
if (!state.isString()) {
loc = 'value';
val += interpolate(interpolatable);
}
break;
}
}
}
if ('/' == this.input.charAt(0)) {
this.consume(1);
tok.selfClosing = true;
}
return tok;
}
},
/**
* &attributes block
*/
attributesBlock: function () {
var captures;
if (/^&attributes\b/.test(this.input)) {
this.consume(11);
var args = this.bracketExpression();
this.consume(args.end + 1);
return this.tok('&attributes', args.src);
}
},
/**
* Indent | Outdent | Newline.
*/
indent: function() {
var captures, re;
// established regexp
if (this.indentRe) {
captures = this.indentRe.exec(this.input);
// determine regexp
} else {
// tabs
re = /^\n(\t*) */;
captures = re.exec(this.input);
// spaces
if (captures && !captures[1].length) {
re = /^\n( *)/;
captures = re.exec(this.input);
}
// established
if (captures && captures[1].length) this.indentRe = re;
}
if (captures) {
var tok
, indents = captures[1].length;
++this.lineno;
this.consume(indents + 1);
if (' ' == this.input[0] || '\t' == this.input[0]) {
throw new Error('Invalid indentation, you can use tabs or spaces but not both');
}
// blank line
if ('\n' == this.input[0]) return this.tok('newline');
// outdent
if (this.indentStack.length && indents < this.indentStack[0]) {
while (this.indentStack.length && this.indentStack[0] > indents) {
this.stash.push(this.tok('outdent'));
this.indentStack.shift();
}
tok = this.stash.pop();
// indent
} else if (indents && indents != this.indentStack[0]) {
this.indentStack.unshift(indents);
tok = this.tok('indent', indents);
// newline
} else {
tok = this.tok('newline');
}
return tok;
}
},
/**
* Pipe-less text consumed only when
* pipeless is true;
*/
pipelessText: function() {
if (this.pipeless) {
if ('\n' == this.input[0]) return;
var i = this.input.indexOf('\n');
if (-1 == i) i = this.input.length;
var str = this.input.substr(0, i);
this.consume(str.length);
return this.tok('text', str);
}
},
/**
* ':'
*/
colon: function() {
return this.scan(/^: */, ':');
},
fail: function () {
if (/^ ($|\n)/.test(this.input)) {
this.consume(1);
return this.next();
}
throw new Error('unexpected text ' + this.input.substr(0, 5));
},
/**
* Return the next token object, or those
* previously stashed by lookahead.
*
* @return {Object}
* @api private
*/
advance: function(){
return this.stashed()
|| this.next();
},
/**
* Return the next token object.
*
* @return {Object}
* @api private
*/
next: function() {
return this.deferred()
|| this.blank()
|| this.eos()
|| this.pipelessText()
|| this.yield()
|| this.doctype()
|| this.interpolation()
|| this["case"]()
|| this.when()
|| this["default"]()
|| this["extends"]()
|| this.append()
|| this.prepend()
|| this.block()
|| this.mixinBlock()
|| this.include()
|| this.includeFiltered()
|| this.mixin()
|| this.call()
|| this.conditional()
|| this.each()
|| this["while"]()
|| this.tag()
|| this.filter()
|| this.code()
|| this.id()
|| this.className()
|| this.attrs()
|| this.attributesBlock()
|| this.indent()
|| this.text()
|| this.comment()
|| this.colon()
|| this.dot()
|| this.textFail()
|| this.fail();
}
};
},{"./utils":26,"character-parser":33}],7:[function(_dereq_,module,exports){
'use strict';
var Node = _dereq_('./node');
/**
* Initialize a `Attrs` node.
*
* @api public
*/
var Attrs = module.exports = function Attrs() {
this.attributeNames = [];
this.attrs = [];
this.attributeBlocks = [];
};
// Inherit from `Node`.
Attrs.prototype = Object.create(Node.prototype);
Attrs.prototype.constructor = Attrs;
Attrs.prototype.type = 'Attrs';
/**
* Set attribute `name` to `val`, keep in mind these become
* part of a raw js object literal, so to quote a value you must
* '"quote me"', otherwise or example 'user.name' is literal JavaScript.
*
* @param {String} name
* @param {String} val
* @param {Boolean} escaped
* @return {Tag} for chaining
* @api public
*/
Attrs.prototype.setAttribute = function(name, val, escaped){
if (name !== 'class' && this.attributeNames.indexOf(name) !== -1) {
throw new Error('Duplicate attribute "' + name + '" is not allowed.');
}
this.attributeNames.push(name);
this.attrs.push({ name: name, val: val, escaped: escaped })