qejs
Version:
Asyncronous Embedded JavaScript Templates with Q
444 lines (394 loc) • 11.9 kB
JavaScript
/*!
* EJS
* Copyright(c) 2012 TJ Holowaychuk <tj@vision-media.ca>
* MIT Licensed
*/
var resolverRead = require('./lib/filesystem.js').resolverRead;
/**
* Module dependencies.
*/
var Q = require('q');
function all(promises) {
return Q.when(promises, function (promises) {
var countDown = promises.length;
if (countDown === 0) {
return Q.resolve(promises);
}
var deferred = Q.defer();
promises.forEach(function (promise, index) {
if (Q.isFulfilled(promise)) {
promises[index] = Q.nearer(promise);
if (--countDown === 0) {
deferred.resolve(promises);
}
} else {
Q.when(promise, function (value) {
promises[index] = value;
if (--countDown === 0) {
deferred.resolve(promises);
}
})
.fail(deferred.reject);
}
});
return deferred.promise;
});
}
/**
* Escape the given string of `html`.
*
* @param {String} html
* @return {String}
* @api public
*/
exports.escape = function escape(html){
return when(html, function (html) {
return String(html)
.replace(/&(?!\w+;)/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
});
};
/**
* Library version.
*/
exports.version = '0.7.2';
/**
* Intermediate js cache.
*
* @type Object
*/
var cache = {};
/**
* Clear intermediate js cache.
*
* @api public
*/
exports.clearCache = function(){
cache = {};
};
/**
* Re-throw the given `err` in context to the
* `str` of qejs, `filename`, and `lineno`.
*
* @param {Error} err
* @param {String} str
* @param {String} filename
* @param {String} lineno
* @api private
*/
function rethrow(err, str, filename, lineno){
var lines = str.split('\n')
, start = Math.max(lineno - 3, 0)
, end = Math.min(lines.length, lineno + 3);
// Error context
var context = lines.slice(start, end).map(function(line, i){
var curr = i + start + 1;
return (curr == lineno ? ' >> ' : ' ')
+ curr
+ '| '
+ line;
}).join('\n');
// Alter exception message
err.path = filename;
err.message = (filename || 'ejs') + ':'
+ lineno + '\n'
+ context + '\n\n'
+ err.message;
throw err;
}
/**
* Parse the given `str` of qejs, returning the function body.
*
* @param {String} str
* @return {String}
* @api public
*/
var parse = exports.parse = function(str, options){
var options = options || {}
, open = options.open || exports.open || '<%'
, close = options.close || exports.close || '%>';
var buf = [
"var buf = [];"
, "\nwith (locals) {"
, "\n buf.push('"
];
var lineno = 1;
var consumeEOL = false;
for (var i = 0, len = str.length; i < len; ++i) {
if (str.slice(i, open.length + i) == open) {
i += open.length
var prefix, postfix, line = '__stack.lineno=' + lineno;
switch (str.substr(i, 1)) {
case '=':
prefix = "', escape((" + line + ', ';
postfix = ")), '";
++i;
break;
case '-':
prefix = "', (" + line + ', ';
postfix = "), '";
++i;
break;
default:
prefix = "');" + line + ';';
postfix = "; buf.push('";
}
var end = str.indexOf(close, i)
, js = str.substring(i, end)
, start = i
, n = 0;
if ('-' == js[js.length-1]){
js = js.substring(0, js.length - 1);
consumeEOL = true;
}
function searchAndSplit(js, splitter){
var output = [[]];
var ignoreNext = false;
var inSingleQuotes = false;
var inDoubleQuotes = false;
var inComment = false;
function push(c){
output[output.length-1].push(c);
}
for(var x = 0; x<js.length; x++){
if(ignoreNext){
ignoreNext = false;
push(js[x]);
} else if (!inSingleQuotes && !inDoubleQuotes && !inComment && js[x] === "/" && js[x+1] === "*"){
inComment = true;
push(js[x]);
x++;
push(js[x]);
} else if (inComment && js[x] === "*" && js[x+1] === "/"){
inComment = false;
push(js[x]);
x++;
push(js[x]);
} else if (inComment){
push(js[x]);
} else if(inSingleQuotes && js[x] === "'"){
inSingleQuotes = false;
push(js[x]);
} else if (inDoubleQuotes && js[x] === '"'){
inDoubleQuotes = false;
push(js[x]);
} else if(!inDoubleQuotes && js[x] === "'"){
inSingleQuotes = true;
push(js[x]);
} else if (!inSingleQuotes && js[x] === '"'){
inDoubleQuotes = true;
push(js[x]);
} else if (!inDoubleQuotes && !inSingleQuotes && !inComment){
var fail = false;
for (var i = 0; i < splitter.length; i++) {
if(!fail && splitter[i] !== js[i+x]){
fail = true;
}
}
if(!fail){
x += splitter.length - 1;
output.push([]);
} else {
push(js[x]);
}
} else {
if(inDoubleQuotes || inSingleQuotes){
if(js[x] === "\\"){
ignoreNext = true;
}
}
push(js[x]);
}
}
if(output.length > 1){
return output.map(function(part){
return part.join('');
});
} else {
return false;
}
}
(function(){
var split = searchAndSplit(js, '->');
if(split){
var input = split[0];
var output = split[1];
var manyIn = (/^\s*\[.*\]\s*$/g).test(input);
var manyOut = (/^\s*\[.*\]\s*$/g).test(output);
if(manyIn){
input = 'all(' + input + ')';
} else {
input = 'Q.when(' + input + ')';
}
if(manyOut){
output = (/^\s*\[(.*)\]\s*$/g).exec(output)[1];
prefix = "', (" + line + ', (function(buf){return ' + input + '.spread(function(';
}else{
prefix = "', (" + line + ', (function(buf){return ' + input + '.then(function(';
}
js = output;
postfix = '){try {'+"buf.push('";
}
}());
(function(){
var split = searchAndSplit(js, '<-');
if(split){
prefix += split[0];
prefix += ";return all(buf).invoke('join','')} catch (err) { rethrow(err, __stack.input, __stack.filename, __stack.lineno); } });}([]))));"
js = split[1];
}
}());
while (~(n = js.indexOf("\n", n))) n++, lineno++;
buf.push(prefix, js, postfix);
i += end - start + close.length - 1;
} else if (str.substr(i, 1) == "\\") {
buf.push("\\\\");
} else if (str.substr(i, 1) == "'") {
buf.push("\\'");
} else if (str.substr(i, 1) == "\r") {
buf.push(" ");
} else if (str.substr(i, 1) == "\n") {
if (consumeEOL) {
consumeEOL = false;
} else {
buf.push("\\n");
lineno++;
}
} else {
buf.push(str.substr(i, 1));
}
}
buf.push("');\n}\nreturn all(buf).invoke('join', '');");
return buf.join('');
};
/**
* Compile the given `str` of qejs into a `Function`.
*
* @param {String} str
* @param {Object} options
* @return {Function}
* @api public
*/
var compile = exports.compile = function(str, options){
options = options || {};
var input = JSON.stringify(str)
, filename = options.filename
? JSON.stringify(options.filename)
: 'undefined';
// Adds the fancy stack trace meta info
str = [
'var __stack = { lineno: 1, input: ' + input + ', filename: ' + filename + ' };',
rethrow.toString(),
'try {',
exports.parse(str, options),
'} catch (err) {',
' rethrow(err, __stack.input, __stack.filename, __stack.lineno);',
'}'
].join("\n");
if (options.debug) console.log(str);
try {
var fn = new Function('locals, escape, Q, all', str);
} catch (err) {
if ('SyntaxError' == err.name) {
err.message += options.filename
? ' in ' + filename
: ' while compiling qejs';
err.source = str;
}
throw err;
}
return function(locals){
return fn.call(this, locals || {}, exports.escape, Q, all);
}
};
/**
* Render the given `str` of qejs.
*
* Options:
*
* - `locals` Local variables object
* - `cache` Compiled functions are cached, requires `filename`
* - `filename` Used by `cache` to key caches
* - `scope` Function execution context
* - `debug` Output generated function body
* - `open` Open tag, defaulting to "<%"
* - `close` Closing tag, defaulting to "%>"
*
* @param {String} str
* @param {Object} options
* @return {>String}
* @api public
*/
exports.render = function(str, options){
try{
var fn, options = options || {};
if (typeof options.render === 'undefined' && options.filename) {
options.render = function (subpath, suboptions) {
suboptions = (suboptions || {});
Object.keys(options).forEach(function (key) {
if(key !== 'inherits' && key !== 'render' && typeof suboptions[key] === 'undefined') suboptions[key] = options[key];
});
return exports.renderFile(subpath, (suboptions || options), options.filename);
};
}
var inherits = null;
if(typeof options.inherits === 'undefined' && options.filename) {
options.inherits = function (path) {
if(inherits !== null) throw new Error("It is an error to call inherits multiple times from one QEJS file");
inherits = path;
return path;
};
}
if (options.cache) {
if (options.filename) {
fn = cache[options.filename] || (cache[options.filename] = compile(str, options));
} else {
throw new Error('"cache" option requires "filename".');
}
} else {
fn = compile(str, options);
}
options.__proto__ = options.locals;
var inner = fn.call(options.scope, options);
if (inherits && options.filename) {
var parentoptions = {};
Object.keys(options).forEach(function (key) {
if(key !== 'inherits' && key !== 'render') parentoptions[key] = options[key];
});
parentoptions.contents = inner;
return exports.renderFile(inherits, parentoptions, options.filename);
} else {
return inner;
}
}catch (ex){
return Q.reject(ex);
}
};
/**
* Render an EJS file at the given `path`
*
* @param {String} path
* @param {Object} [options]
* @return {>String}
* @api public
*/
exports.renderFile = function(path, options, source){
var key = path + ':string';
options = options || {};
try {
var file = resolverRead(options.cache, path, source);
options.filename = file.path;
return exports.render(file.str, options);
} catch (ex) {
return Q.reject(ex);
}
};
function when(promise, callback, errback) {
if (Q.isFulfilled(promise)) {
return Q.resolve(callback(Q.nearer(promise)));
} else {
return promise.then(callback, errback);
}
}