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 * Partial render helper.
42 function renderPartial(res, view, options, parentLocals, parent){
43 var collection, object, locals;
45 // Inherit parent view extension when not present
46 if (parent && !~view.indexOf('.')) {
47 view += parent.extension;
52 if (options.collection) {
53 collection = options.collection;
54 delete options.collection;
55 } else if ('length' in options) {
62 locals = options.locals;
63 delete options.locals;
67 if ('Object' != options.constructor.name) {
70 } else if (undefined != options.object) {
71 object = options.object;
72 delete options.object;
78 // Inherit locals from parent
79 union(options, parentLocals);
82 if (locals) merge(options, locals);
84 // Partials dont need layouts
85 options.renderPartial = true;
86 options.layout = false;
88 // Deduce name from view path
89 var name = options.as || partial.resolveObjectName(view);
94 if ('string' == typeof name) {
95 options[name] = object;
96 } else if (name === global) {
97 merge(options, object);
99 options.scope = object;
102 return res.render(view, options, null, parent, true);
105 // Collection support
107 var len = collection.length
113 options.collectionLength = len;
115 if ('number' == typeof len || Array.isArray(collection)) {
116 for (var i = 0; i < len; ++i) {
118 options.firstInCollection = i == 0;
119 options.indexInCollection = i;
120 options.lastInCollection = i == len - 1;
125 keys = Object.keys(collection);
127 options.collectionLength = len;
128 options.collectionKeys = keys;
129 for (var i = 0; i < len; ++i) {
131 val = collection[key];
132 options.keyInCollection = key;
133 options.firstInCollection = i == 0;
134 options.indexInCollection = i;
135 options.lastInCollection = i == len - 1;
148 * Render `view` partial with the given `options`. Optionally a
149 * callback `fn(err, str)` may be passed instead of writing to
154 * - `object` Single object with name derived from the view (unless `as` is present)
156 * - `as` Variable name for each `collection` value, defaults to the view name.
157 * * as: 'something' will add the `something` local variable
158 * * as: this will use the collection value as the template context
159 * * as: global will merge the collection value's properties with `locals`
161 * - `collection` Array of objects, the name is derived from the view name itself.
162 * For example _video.html_ will have a object _video_ available to it.
164 * @param {String} view
165 * @param {Object|Array|Function} options, collection, callback, or object
166 * @param {Function} fn
171 res.partial = function(view, options, fn){
173 , options = options || {}
176 // accept callback as second argument
177 if ('function' == typeof options) {
182 // root "views" option
183 parent.dirname = app.set('views') || process.cwd() + '/views';
185 // utilize "view engine" option
186 if (app.set('view engine')) {
187 parent.extension = '.' + app.set('view engine');
190 // render the partial
192 var str = renderPartial(this, view, options, null, parent);
202 // callback or transfer
211 * Render `view` with the given `options` and optional callback `fn`.
212 * When a callback function is given a response will _not_ be made
213 * automatically, however otherwise a response of _200_ and _text/html_ is given.
217 * - `scope` Template evaluation context (the value of `this`)
218 * - `debug` Output debugging information
219 * - `status` Response status code
221 * @param {String} view
222 * @param {Object|Function} options or callback function
223 * @param {Function} fn
227 res.render = function(view, opts, fn, parent, sub){
228 // support callback function as second arg
229 if ('function' == typeof opts) {
230 fn = opts, opts = null;
234 return this._render(view, opts, fn, parent, sub);
239 // unwind to root call to prevent
240 // several next(err) calls
243 // root template, next(err)
252 res._render = function(view, opts, fn, parent, sub){
256 , helpers = app._locals
257 , dynamicHelpers = app.dynamicViewHelpers
258 , viewOptions = app.set('view options')
259 , cacheViews = app.enabled('view cache')
260 , root = app.set('views') || process.cwd() + '/views';
263 var cid = view + (parent ? ':' + parent.path : '');
265 // merge "view options"
266 if (viewOptions) merge(options, viewOptions);
269 if (this._locals) merge(options, this._locals);
271 // merge render() options
272 if (opts) merge(options, opts);
274 // merge render() .locals
275 if (opts && opts.locals) merge(options, opts.locals);
278 if (options.status) this.statusCode = options.status;
281 options.attempts = [];
283 var partial = options.renderPartial
284 , layout = options.layout;
287 if (true === layout || undefined === layout) {
291 // Default execution scope to a plain object
292 options.scope = options.scope || {};
295 options.parentView = parent;
300 // "view engine" setting
301 options.defaultEngine = app.set('view engine');
304 if (options.charset) this.charset = options.charset;
306 // Dynamic helper support
307 if (false !== options.dynamicHelpers) {
309 if (!this.__dynamicHelpers) {
310 this.__dynamicHelpers = {};
311 for (var key in dynamicHelpers) {
312 this.__dynamicHelpers[key] = dynamicHelpers[key].call(
320 merge(options, this.__dynamicHelpers);
323 // Merge view helpers
324 union(options, helpers);
326 // Always expose partial() as a local
327 options.partial = function(path, opts){
328 return renderPartial(self, path, opts, options, view);
332 if (app.cache[cid]) {
333 view = app.cache[cid];
334 options.filename = view.path;
337 var orig = view = new View(view, options);
339 // Try _ prefix ex: ./views/_<name>.jade
341 view = new View(orig.prefixPath, options);
342 if (!view.exists) view = orig;
345 // Try index ex: ./views/user/index.jade
346 if (!view.exists) view = new View(orig.indexPath, options);
348 // Try ../<name>/index ex: ../user/index.jade
349 // when calling partial('user') within the same dir
350 if (!view.exists && !options.isLayout) view = new View(orig.upIndexPath, options);
352 // Try root ex: <root>/user.jade
353 if (!view.exists) view = new View(orig.rootPath, options);
355 // Try root _ prefix ex: <root>/_user.jade
356 if (!view.exists && partial) view = new View(view.prefixPath, options);
360 if (app.enabled('hints')) hintAtViewPaths(orig, options);
361 var err = new Error('failed to locate view "' + orig.view + '"');
366 options.filename = view.path;
367 var engine = view.templateEngine;
368 view.fn = engine.compile(view.contents, options)
369 if (cacheViews) app.cache[cid] = view;
373 options.layout = function(path){
378 var str = view.fn.call(options.scope, options);
382 options.isLayout = true;
383 options.layout = false;
385 this.render(layout, options, fn, view, true);
387 } else if (partial) {
389 // render complete, and
400 * Hint at view path resolution, outputting the
401 * paths that Express has tried.
406 function hintAtViewPaths(view, options) {
408 console.error('failed to locate view "' + view.view + '", tried:');
409 options.attempts.forEach(function(path){
410 console.error(' - %s', path);