4 * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
12 var path = require('path')
13 , extname = path.extname
14 , dirname = path.dirname
15 , basename = path.basename
16 , utils = require('connect').utils
17 , View = require('./view/view')
18 , partial = require('./view/partial')
19 , union = require('./utils').union
21 , http = require('http')
22 , res = http.ServerResponse.prototype;
25 * Expose constructors.
28 exports = module.exports = View;
31 * Export template engine registrar.
34 exports.register = View.register;
37 * Lookup and compile `view` with cache support by supplying
38 * both the `cache` object and `cid` string,
39 * followed by `options` passed to `exports.lookup()`.
41 * @param {String} view
42 * @param {Object} cache
44 * @param {Object} options
49 exports.compile = function(view, cache, cid, options){
50 if (cache && cid && cache[cid]) return cache[cid];
53 view = exports.lookup(view, options);
57 if (options.hint) hintAtViewPaths(view.original, options);
58 var err = new Error('failed to locate view "' + view.original.view + '"');
59 err.view = view.original;
64 options.filename = view.path;
65 view.fn = view.templateEngine.compile(view.contents, options);
72 * Lookup `view`, returning an instanceof `View`.
76 * - `root` root directory path
77 * - `defaultEngine` default template engine
78 * - `parentView` parent `View` object
79 * - `cache` cache object
80 * - `cacheid` optional cache id
85 * - any `<name>/index`
86 * - non-layout `../<name>/index`
87 * - any `<root>/<name>`
88 * - partial `<root>/_<name>`
90 * @param {String} view
91 * @param {Object} options
96 exports.lookup = function(view, options){
97 var orig = view = new View(view, options)
98 , partial = options.isPartial
99 , layout = options.isLayout;
101 // Try _ prefix ex: ./views/_<name>.jade
102 // taking precedence over the direct path
104 view = new View(orig.prefixPath, options);
105 if (!view.exists) view = orig;
108 // Try index ex: ./views/user/index.jade
109 if (!layout && !view.exists) view = new View(orig.indexPath, options);
111 // Try ../<name>/index ex: ../user/index.jade
112 // when calling partial('user') within the same dir
113 if (!layout && !view.exists) view = new View(orig.upIndexPath, options);
115 // Try root ex: <root>/user.jade
116 if (!view.exists) view = new View(orig.rootPath, options);
118 // Try root _ prefix ex: <root>/_user.jade
119 if (!view.exists && partial) view = new View(view.prefixPath, options);
121 view.original = orig;
126 * Partial render helper.
131 function renderPartial(res, view, options, parentLocals, parent){
132 var collection, object, locals;
136 if (options.collection) {
137 collection = options.collection;
138 delete options.collection;
139 } else if ('length' in options) {
140 collection = options;
145 if (options.locals) {
146 locals = options.locals;
147 delete options.locals;
151 if ('Object' != options.constructor.name) {
154 } else if (undefined != options.object) {
155 object = options.object;
156 delete options.object;
162 // Inherit locals from parent
163 union(options, parentLocals);
166 if (locals) merge(options, locals);
168 // Partials dont need layouts
169 options.isPartial = true;
170 options.layout = false;
172 // Deduce name from view path
173 var name = options.as || partial.resolveObjectName(view);
178 if ('string' == typeof name) {
179 options[name] = object;
180 } else if (name === global) {
181 merge(options, object);
183 options.scope = object;
186 return res.render(view, options, null, parent, true);
189 // Collection support
191 var len = collection.length
197 options.collectionLength = len;
199 if ('number' == typeof len || Array.isArray(collection)) {
200 for (var i = 0; i < len; ++i) {
202 options.firstInCollection = i == 0;
203 options.indexInCollection = i;
204 options.lastInCollection = i == len - 1;
209 keys = Object.keys(collection);
211 options.collectionLength = len;
212 options.collectionKeys = keys;
213 for (var i = 0; i < len; ++i) {
215 val = collection[key];
216 options.keyInCollection = key;
217 options.firstInCollection = i == 0;
218 options.indexInCollection = i;
219 options.lastInCollection = i == len - 1;
232 * Render `view` partial with the given `options`. Optionally a
233 * callback `fn(err, str)` may be passed instead of writing to
238 * - `object` Single object with name derived from the view (unless `as` is present)
240 * - `as` Variable name for each `collection` value, defaults to the view name.
241 * * as: 'something' will add the `something` local variable
242 * * as: this will use the collection value as the template context
243 * * as: global will merge the collection value's properties with `locals`
245 * - `collection` Array of objects, the name is derived from the view name itself.
246 * For example _video.html_ will have a object _video_ available to it.
248 * @param {String} view
249 * @param {Object|Array|Function} options, collection, callback, or object
250 * @param {Function} fn
255 res.partial = function(view, options, fn){
257 , options = options || {}
258 , viewEngine = app.set('view engine')
261 // accept callback as second argument
262 if ('function' == typeof options) {
267 // root "views" option
268 parent.dirname = app.set('views') || process.cwd() + '/views';
270 // utilize "view engine" option
271 if (viewEngine) parent.engine = viewEngine;
273 // render the partial
275 var str = renderPartial(this, view, options, null, parent);
285 // callback or transfer
294 * Render `view` with the given `options` and optional callback `fn`.
295 * When a callback function is given a response will _not_ be made
296 * automatically, however otherwise a response of _200_ and _text/html_ is given.
300 * - `scope` Template evaluation context (the value of `this`)
301 * - `debug` Output debugging information
302 * - `status` Response status code
304 * @param {String} view
305 * @param {Object|Function} options or callback function
306 * @param {Function} fn
310 res.render = function(view, opts, fn, parent, sub){
311 // support callback function as second arg
312 if ('function' == typeof opts) {
313 fn = opts, opts = null;
317 return this._render(view, opts, fn, parent, sub);
322 // unwind to root call to prevent multiple callbacks
325 // root template, next(err)
334 res._render = function(view, opts, fn, parent, sub){
338 , helpers = app._locals
339 , dynamicHelpers = app.dynamicViewHelpers
340 , viewOptions = app.set('view options')
341 , root = app.set('views') || process.cwd() + '/views';
344 var cid = app.enabled('view cache')
345 ? view + (parent ? ':' + parent.path : '')
348 // merge "view options"
349 if (viewOptions) merge(options, viewOptions);
352 if (this._locals) merge(options, this._locals);
354 // merge render() options
355 if (opts) merge(options, opts);
357 // merge render() .locals
358 if (opts && opts.locals) merge(options, opts.locals);
361 if (options.status) this.statusCode = options.status;
364 options.attempts = [];
366 var partial = options.isPartial
367 , layout = options.layout;
370 if (true === layout || undefined === layout) {
374 // Default execution scope to a plain object
375 options.scope = options.scope || {};
378 options.parentView = parent;
383 // "view engine" setting
384 options.defaultEngine = app.set('view engine');
387 if (options.charset) this.charset = options.charset;
389 // Dynamic helper support
390 if (false !== options.dynamicHelpers) {
392 if (!this.__dynamicHelpers) {
393 this.__dynamicHelpers = {};
394 for (var key in dynamicHelpers) {
395 this.__dynamicHelpers[key] = dynamicHelpers[key].call(
403 merge(options, this.__dynamicHelpers);
406 // Merge view helpers
407 union(options, helpers);
409 // Always expose partial() as a local
410 options.partial = function(path, opts){
411 return renderPartial(self, path, opts, options, view);
415 options.hint = app.enabled('hints');
416 view = exports.compile(view, app.cache, cid, options);
419 options.layout = function(path){
424 var str = view.fn.call(options.scope, options);
428 options.isLayout = true;
429 options.layout = false;
431 this.render(layout, options, fn, view, true);
433 } else if (partial) {
435 // render complete, and
446 * Hint at view path resolution, outputting the
447 * paths that Express has tried.
452 function hintAtViewPaths(view, options) {
454 console.error('failed to locate view "' + view.view + '", tried:');
455 options.attempts.forEach(function(path){
456 console.error(' - %s', path);