You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
460 lines
11 KiB
460 lines
11 KiB
|
|
/*! |
|
* 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(); |
|
}
|
|
|