/*! * Express - view * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca> * MIT Licensed */ /** * Module dependencies. */ var path = require('path') , extname = path.extname , dirname = path.dirname , basename = path.basename , utils = require('connect').utils , View = require('./view/view') , partial = require('./view/partial') , union = require('./utils').union , merge = utils.merge , http = require('http') , res = http.ServerResponse.prototype; /** * Expose constructors. */ exports = module.exports = View; /** * Export template engine registrar. */ exports.register = View.register; /** * Lookup and compile `view` with cache support by supplying * both the `cache` object and `cid` string, * followed by `options` passed to `exports.lookup()`. * * @param {String} view * @param {Object} cache * @param {Object} cid * @param {Object} options * @return {View} * @api private */ exports.compile = function(view, cache, cid, options){ if (cache && cid && cache[cid]){ options.filename = cache[cid].path; return cache[cid]; } // lookup view = exports.lookup(view, options); // hints if (!view.exists) { if (options.hint) hintAtViewPaths(view.original, options); var err = new Error('failed to locate view "' + view.original.view + '"'); err.view = view.original; throw err; } // compile options.filename = view.path; view.fn = view.templateEngine.compile(view.contents, options); cache[cid] = view; return view;}; /** * Lookup `view`, returning an instanceof `View`. * * Options: * * - `root` root directory path * - `defaultEngine` default template engine * - `parentView` parent `View` object * - `cache` cache object * - `cacheid` optional cache id * * Lookup: * * - partial `_<name>` * - any `<name>/index` * - non-layout `../<name>/index` * - any `<root>/<name>` * - partial `<root>/_<name>` * * @param {String} view * @param {Object} options * @return {View} * @api private */ exports.lookup = function(view, options){ var orig = view = new View(view, options) , partial = options.isPartial , layout = options.isLayout; // Try _ prefix ex: ./views/_<name>.jade // taking precedence over the direct path if (partial) { view = new View(orig.prefixPath, options); if (!view.exists) view = orig; } // Try index ex: ./views/user/index.jade if (!layout && !view.exists) view = new View(orig.indexPath, options); // Try ../<name>/index ex: ../user/index.jade // when calling partial('user') within the same dir if (!layout && !view.exists) view = new View(orig.upIndexPath, options); // Try root ex: <root>/user.jade if (!view.exists) view = new View(orig.rootPath, options); // Try root _ prefix ex: <root>/_user.jade if (!view.exists && partial) view = new View(view.prefixPath, options); view.original = orig; return view;}; /** * Partial render helper. * * @api private */ function renderPartial(res, view, options, parentLocals, parent){ var collection, object, locals; if (options) { // collection if (options.collection) { collection = options.collection; delete options.collection; } else if ('length' in options) { collection = options; options = {}; } // locals if (options.locals) { locals = options.locals; delete options.locals; } // object if ('Object' != options.constructor.name) { object = options; options = {}; } else if (undefined != options.object) { object = options.object; delete options.object; } } else { options = {}; } // Inherit locals from parent union(options, parentLocals); // Merge locals if (locals) merge(options, locals); // Partials dont need layouts options.isPartial = true; options.layout = false; // Deduce name from view path var name = options.as || partial.resolveObjectName(view); // Render partial function render(){ if (object) { if ('string' == typeof name) { options[name] = object; } else if (name === global) { merge(options, object); } } return res.render(view, options, null, parent, true); } // Collection support if (collection) { var len = collection.length , buf = '' , keys , key , val; options.collectionLength = len; if ('number' == typeof len || Array.isArray(collection)) { for (var i = 0; i < len; ++i) { val = collection[i]; options.firstInCollection = i == 0; options.indexInCollection = i; options.lastInCollection = i == len - 1; object = val; buf += render(); } } else { keys = Object.keys(collection); len = keys.length; options.collectionLength = len; options.collectionKeys = keys; for (var i = 0; i < len; ++i) { key = keys[i]; val = collection[key]; options.keyInCollection = key; options.firstInCollection = i == 0; options.indexInCollection = i; options.lastInCollection = i == len - 1; object = val; buf += render(); } } return buf; } else { return render(); }}; /** * Render `view` partial with the given `options`. Optionally a * callback `fn(err, str)` may be passed instead of writing to * the socket. * * Options: * * - `object` Single object with name derived from the view (unless `as` is present) * * - `as` Variable name for each `collection` value, defaults to the view name. * * as: 'something' will add the `something` local variable * * as: this will use the collection value as the template context * * as: global will merge the collection value's properties with `locals` * * - `collection` Array of objects, the name is derived from the view name itself. * For example _video.html_ will have a object _video_ available to it. * * @param {String} view * @param {Object|Array|Function} options, collection, callback, or object * @param {Function} fn * @return {String} * @api public */ res.partial = function(view, options, fn){ var app = this.app , options = options || {} , viewEngine = app.set('view engine') , parent = {}; // accept callback as second argument if ('function' == typeof options) { fn = options; options = {}; } // root "views" option parent.dirname = app.set('views') || process.cwd() + '/views'; // utilize "view engine" option if (viewEngine) parent.engine = viewEngine; // render the partial try { var str = renderPartial(this, view, options, null, parent); } catch (err) { if (fn) { fn(err); } else { this.req.next(err); } return; } // callback or transfer if (fn) { fn(null, str); } else { this.send(str); }}; /** * Render `view` with the given `options` and optional callback `fn`. * When a callback function is given a response will _not_ be made * automatically, however otherwise a response of _200_ and _text/html_ is given. * * Options: * * - `scope` Template evaluation context (the value of `this`) * - `debug` Output debugging information * - `status` Response status code * * @param {String} view * @param {Object|Function} options or callback function * @param {Function} fn * @api public */ res.render = function(view, opts, fn, parent, sub){ // support callback function as second arg if ('function' == typeof opts) { fn = opts, opts = null; } try { return this._render(view, opts, fn, parent, sub); } catch (err) { // callback given if (fn) { fn(err); // unwind to root call to prevent multiple callbacks } else if (sub) { throw err; // root template, next(err) } else { this.req.next(err); } }}; // private render() res._render = function(view, opts, fn, parent, sub){ var options = {} , self = this , app = this.app , helpers = app._locals , dynamicHelpers = app.dynamicViewHelpers , viewOptions = app.set('view options') , root = app.set('views') || process.cwd() + '/views'; // cache id var cid = app.enabled('view cache') ? view + (parent ? ':' + parent.path : '') : false; // merge "view options" if (viewOptions) merge(options, viewOptions); // merge res._locals if (this._locals) merge(options, this._locals); // merge render() options if (opts) merge(options, opts); // merge render() .locals if (opts && opts.locals) merge(options, opts.locals); // status support if (options.status) this.statusCode = options.status; // capture attempts options.attempts = []; var partial = options.isPartial , layout = options.layout; // Layout support if (true === layout || undefined === layout) { layout = 'layout'; } // Default execution scope to a plain object options.scope = options.scope || {}; // Populate view options.parentView = parent; // "views" setting options.root = root; // "view engine" setting options.defaultEngine = app.set('view engine'); // charset option if (options.charset) this.charset = options.charset; // Dynamic helper support if (false !== options.dynamicHelpers) { // cache if (!this.__dynamicHelpers) { this.__dynamicHelpers = {}; for (var key in dynamicHelpers) { this.__dynamicHelpers[key] = dynamicHelpers[key].call( this.app , this.req , this); } } // apply merge(options, this.__dynamicHelpers); } // Merge view helpers union(options, helpers); // Always expose partial() as a local options.partial = function(path, opts){ return renderPartial(self, path, opts, options, view); }; // View lookup options.hint = app.enabled('hints'); view = exports.compile(view, app.cache, cid, options); // layout helper options.layout = function(path){ layout = path; }; // render var str = view.fn.call(options.scope, options); // layout expected if (layout) { options.isLayout = true; options.layout = false; options.body = str; this.render(layout, options, fn, view, true); // partial return } else if (partial) { return str; // render complete, and // callback given } else if (fn) { fn(null, str); // respond } else { this.send(str); }} /** * Hint at view path resolution, outputting the * paths that Express has tried. * * @api private */ function hintAtViewPaths(view, options) { console.error(); console.error('failed to locate view "' + view.view + '", tried:'); options.attempts.forEach(function(path){ console.error(' - %s', path); }); console.error();}
/*!
* Express - view
* Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var path = require('path')
, extname = path.extname
, dirname = path.dirname
, basename = path.basename
, utils = require('connect').utils
, View = require('./view/view')
, partial = require('./view/partial')
, union = require('./utils').union
, merge = utils.merge
, http = require('http')
, res = http.ServerResponse.prototype;
/**
* Expose constructors.
*/
exports = module.exports = View;
/**
* Export template engine registrar.
*/
exports.register = View.register;
/**
* Lookup and compile `view` with cache support by supplying
* both the `cache` object and `cid` string,
* followed by `options` passed to `exports.lookup()`.
*
* @param {String} view
* @param {Object} cache
* @param {Object} cid
* @param {Object} options
* @return {View}
* @api private
*/
exports.compile = function(view, cache, cid, options){
if (cache && cid && cache[cid]){
options.filename = cache[cid].path;
return cache[cid];
}
// lookup
view = exports.lookup(view, options);
// hints
if (!view.exists) {
if (options.hint) hintAtViewPaths(view.original, options);
var err = new Error('failed to locate view "' + view.original.view + '"');
err.view = view.original;
throw err;
}
// compile
options.filename = view.path;
view.fn = view.templateEngine.compile(view.contents, options);
cache[cid] = view;
return view;
};
/**
* Lookup `view`, returning an instanceof `View`.
*
* Options:
*
* - `root` root directory path
* - `defaultEngine` default template engine
* - `parentView` parent `View` object
* - `cache` cache object
* - `cacheid` optional cache id
*
* Lookup:
*
* - partial `_<name>`
* - any `<name>/index`
* - non-layout `../<name>/index`
* - any `<root>/<name>`
* - partial `<root>/_<name>`
*
* @param {String} view
* @param {Object} options
* @return {View}
* @api private
*/
exports.lookup = function(view, options){
var orig = view = new View(view, options)
, partial = options.isPartial
, layout = options.isLayout;
// Try _ prefix ex: ./views/_<name>.jade
// taking precedence over the direct path
if (partial) {
view = new View(orig.prefixPath, options);
if (!view.exists) view = orig;
}
// Try index ex: ./views/user/index.jade
if (!layout && !view.exists) view = new View(orig.indexPath, options);
// Try ../<name>/index ex: ../user/index.jade
// when calling partial('user') within the same dir
if (!layout && !view.exists) view = new View(orig.upIndexPath, options);
// Try root ex: <root>/user.jade
if (!view.exists) view = new View(orig.rootPath, options);
// Try root _ prefix ex: <root>/_user.jade
if (!view.exists && partial) view = new View(view.prefixPath, options);
view.original = orig;
return view;
};
/**
* Partial render helper.
*
* @api private
*/
function renderPartial(res, view, options, parentLocals, parent){
var collection, object, locals;
if (options) {
// collection
if (options.collection) {
collection = options.collection;
delete options.collection;
} else if ('length' in options) {
collection = options;
options = {};
}
// locals
if (options.locals) {
locals = options.locals;
delete options.locals;
}
// object
if ('Object' != options.constructor.name) {
object = options;
options = {};
} else if (undefined != options.object) {
object = options.object;
delete options.object;
}
} else {
options = {};
}
// Inherit locals from parent
union(options, parentLocals);
// Merge locals
if (locals) merge(options, locals);
// Partials dont need layouts
options.isPartial = true;
options.layout = false;
// Deduce name from view path
var name = options.as || partial.resolveObjectName(view);
// Render partial
function render(){
if (object) {
if ('string' == typeof name) {
options[name] = object;
} else if (name === global) {
merge(options, object);
}
}
return res.render(view, options, null, parent, true);
}
// Collection support
if (collection) {
var len = collection.length
, buf = ''
, keys
, key
, val;
options.collectionLength = len;
if ('number' == typeof len || Array.isArray(collection)) {
for (var i = 0; i < len; ++i) {
val = collection[i];
options.firstInCollection = i == 0;
options.indexInCollection = i;
options.lastInCollection = i == len - 1;
object = val;
buf += render();
}
} else {
keys = Object.keys(collection);
len = keys.length;
options.collectionLength = len;
options.collectionKeys = keys;
for (var i = 0; i < len; ++i) {
key = keys[i];
val = collection[key];
options.keyInCollection = key;
options.firstInCollection = i == 0;
options.indexInCollection = i;
options.lastInCollection = i == len - 1;
object = val;
buf += render();
}
}
return buf;
} else {
return render();
}
};
/**
* Render `view` partial with the given `options`. Optionally a
* callback `fn(err, str)` may be passed instead of writing to
* the socket.
*
* Options:
*
* - `object` Single object with name derived from the view (unless `as` is present)
*
* - `as` Variable name for each `collection` value, defaults to the view name.
* * as: 'something' will add the `something` local variable
* * as: this will use the collection value as the template context
* * as: global will merge the collection value's properties with `locals`
*
* - `collection` Array of objects, the name is derived from the view name itself.
* For example _video.html_ will have a object _video_ available to it.
*
* @param {String} view
* @param {Object|Array|Function} options, collection, callback, or object
* @param {Function} fn
* @return {String}
* @api public
*/
res.partial = function(view, options, fn){
var app = this.app
, options = options || {}
, viewEngine = app.set('view engine')
, parent = {};
// accept callback as second argument
if ('function' == typeof options) {
fn = options;
options = {};
}
// root "views" option
parent.dirname = app.set('views') || process.cwd() + '/views';
// utilize "view engine" option
if (viewEngine) parent.engine = viewEngine;
// render the partial
try {
var str = renderPartial(this, view, options, null, parent);
} catch (err) {
if (fn) {
fn(err);
} else {
this.req.next(err);
}
return;
}
// callback or transfer
if (fn) {
fn(null, str);
} else {
this.send(str);
}
};
/**
* Render `view` with the given `options` and optional callback `fn`.
* When a callback function is given a response will _not_ be made
* automatically, however otherwise a response of _200_ and _text/html_ is given.
*
* Options:
*
* - `scope` Template evaluation context (the value of `this`)
* - `debug` Output debugging information
* - `status` Response status code
*
* @param {String} view
* @param {Object|Function} options or callback function
* @param {Function} fn
* @api public
*/
res.render = function(view, opts, fn, parent, sub){
// support callback function as second arg
if ('function' == typeof opts) {
fn = opts, opts = null;
}
try {
return this._render(view, opts, fn, parent, sub);
} catch (err) {
// callback given
if (fn) {
fn(err);
// unwind to root call to prevent multiple callbacks
} else if (sub) {
throw err;
// root template, next(err)
} else {
this.req.next(err);
}
}
};
// private render()
res._render = function(view, opts, fn, parent, sub){
var options = {}
, self = this
, app = this.app
, helpers = app._locals
, dynamicHelpers = app.dynamicViewHelpers
, viewOptions = app.set('view options')
, root = app.set('views') || process.cwd() + '/views';
// cache id
var cid = app.enabled('view cache')
? view + (parent ? ':' + parent.path : '')
: false;
// merge "view options"
if (viewOptions) merge(options, viewOptions);
// merge res._locals
if (this._locals) merge(options, this._locals);
// merge render() options
if (opts) merge(options, opts);
// merge render() .locals
if (opts && opts.locals) merge(options, opts.locals);
// status support
if (options.status) this.statusCode = options.status;
// capture attempts
options.attempts = [];
var partial = options.isPartial
, layout = options.layout;
// Layout support
if (true === layout || undefined === layout) {
layout = 'layout';
}
// Default execution scope to a plain object
options.scope = options.scope || {};
// Populate view
options.parentView = parent;
// "views" setting
options.root = root;
// "view engine" setting
options.defaultEngine = app.set('view engine');
// charset option
if (options.charset) this.charset = options.charset;
// Dynamic helper support
if (false !== options.dynamicHelpers) {
// cache
if (!this.__dynamicHelpers) {
this.__dynamicHelpers = {};
for (var key in dynamicHelpers) {
this.__dynamicHelpers[key] = dynamicHelpers[key].call(
this.app
, this.req
, this);
}
}
// apply
merge(options, this.__dynamicHelpers);
}
// Merge view helpers
union(options, helpers);
// Always expose partial() as a local
options.partial = function(path, opts){
return renderPartial(self, path, opts, options, view);
};
// View lookup
options.hint = app.enabled('hints');
view = exports.compile(view, app.cache, cid, options);
// layout helper
options.layout = function(path){
layout = path;
};
// render
var str = view.fn.call(options.scope, options);
// layout expected
if (layout) {
options.isLayout = true;
options.layout = false;
options.body = str;
this.render(layout, options, fn, view, true);
// partial return
} else if (partial) {
return str;
// render complete, and
// callback given
} else if (fn) {
fn(null, str);
// respond
} else {
this.send(str);
}
}
/**
* Hint at view path resolution, outputting the
* paths that Express has tried.
*
* @api private
*/
function hintAtViewPaths(view, options) {
console.error();
console.error('failed to locate view "' + view.view + '", tried:');
options.attempts.forEach(function(path){
console.error(' - %s', path);
});
console.error();
}