jade
Version:
Jade template engine
314 lines (270 loc) • 6.71 kB
JavaScript
/*!
* Jade
* Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var Parser = require('./parser'),
Compiler = require('./compiler'),
fs = require('fs');
/**
* Library version.
*/
exports.version = '0.5.6';
/**
* Intermediate JavaScript cache.
*
* @type Object
*/
var cache = exports.cache = {};
/**
* Expose self closing tags.
*
* @type Object
*/
exports.selfClosing = require('./self-closing');
/**
* Default supported doctypes.
*
* @type Object
*/
exports.doctypes = require('./doctypes');
/**
* Text filters.
*
* @type Object
*/
exports.filters = require('./filters');
/**
* Utilities.
*
* @type Object
*/
exports.utils = require('./utils');
/**
* Compiler.
*
* @type Function
*/
exports.Compiler = Compiler;
/**
* Nodes.
*
* @type Object
*/
exports.nodes = require('./nodes');
/**
* Render the given attributes object.
*
* @param {Object} obj
* @return {String}
* @api private
*/
function attrs(obj){
var buf = [],
terse = obj.terse;
delete obj.terse;
var keys = Object.keys(obj),
len = keys.length;
if (len) {
buf.push('');
for (var i = 0; i < len; ++i) {
var key = keys[i],
val = obj[key];
if (typeof val === 'boolean' || val === '' || val == null) {
if (val) {
terse
? buf.push(key)
: buf.push(key + '="' + key + '"');
}
} else {
buf.push(key + '="' + escape(val) + '"');
}
}
}
return buf.join(' ');
}
/**
* 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, '"');
}
/**
* Re-throw the given `err` in context to the
* `str` of jade, `filename`, and `lineno`.
*
* @param {Error} err
* @param {String} str
* @param {String} filename
* @param {String} lineno
* @api private
*/
function rethrow(err, str, filename, lineno){
var start = lineno - 3 > 0
? lineno - 3
: 0;
// Error context
var context = str.split('\n').slice(start, lineno).map(function(line, i){
return ' '
+ (i + start + 1)
+ ". '"
+ line.replace("'", "\\'")
+ "'";
}).join('\n');
// Alter exception message
err.path = filename;
err.message = (filename || 'Jade') + ':' + lineno
+ '\n' + context + '\n\n' + err.message;
throw err;
}
/**
* 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){
var filename = options.filename;
try {
// Parse
var parser = new Parser(str, filename);
if (options.debug) parser.debug();
// Compile
var compiler = new (options.compiler || Compiler)(parser.parse(), options),
js = compiler.compile();
// Debug compiler
if (options.debug) {
console.log('\n\x1b[1mCompiled Function\x1b[0m:\n\n%s', js.replace(/^/gm, ' '));
}
try {
return ''
+ attrs.toString() + '\n\n'
+ escape.toString() + '\n\n'
+ 'var buf = [];\n'
+ 'with (locals || {}) {' + js + '}'
+ 'return buf.join("");';
} catch (err) {
process.compile(js, filename || 'Jade');
return;
}
} catch (err) {
rethrow(err, str, filename, parser.lexer.lineno);
}
}
/**
* Compile a `Function` representation of the given jade `str`.
*
* @param {String} str
* @param {Options} options
* @return {Function}
* @api public
*/
exports.compile = function(str, options){
var options = options || {},
input = JSON.stringify(str),
filename = options.filename
? JSON.stringify(options.filename)
: 'undefined';
// Reduce closure madness by injecting some locals
var fn = [
'var __ = { lineno: 1, input: ' + input + ', filename: ' + filename + ' };',
rethrow.toString(),
'try {',
parse(String(str), options || {}),
'} catch (err) {',
' rethrow(err, __.input, __.filename, __.lineno);',
'}'
].join('\n');
return new Function('locals', fn);
};
/**
* Render the given `str` of jade.
*
* Options:
*
* - `scope` Evaluation scope (`this`)
* - `locals` Local variable object
* - `filename` Used in exceptions, and required by `cache`
* - `cache` Cache intermediate JavaScript in memory keyed by `filename`
* - `compiler` Compiler to replade jade's default
*
* @param {String|Buffer} str
* @param {Object} options
* @return {String}
* @api public
*/
exports.render = function(str, options){
var fn,
options = options || {},
filename = options.filename;
// Accept Buffers
str = String(str);
// Cache support
if (options.cache) {
if (filename) {
if (cache[filename]) {
fn = cache[filename];
} else {
fn = cache[filename] = new Function('locals', parse(str, options));
}
} else {
throw new Error('filename is required when using the cache option');
}
} else {
fn = new Function('locals', parse(str, options));
}
// Render the template
try {
var locals = options.locals || {}
, meta = { lineno: 1 };
locals.__ = meta;
return fn.call(options.scope, locals);
} catch (err) {
rethrow(err, str, filename, meta.lineno);
}
};
/**
* Render jade template at the given `path`.
*
* @param {String} path
* @param {Object} options
* @param {Function} fn
* @api public
*/
exports.renderFile = function(path, options, fn){
if (typeof options === 'function') {
fn = options;
options = {};
}
options.filename = path;
// Primed cache
if (options.cache && cache[path]) {
try {
fn(null, exports.render('', options));
} catch (err) {
fn(err);
}
} else {
fs.readFile(path, 'utf8', function(err, str){
if (err) return fn(err);
try {
fn(null, exports.render(str, options));
} catch (err) {
fn(err);
}
});
}
};