Inital Commit
[oweals/finalsclub.git] / node_modules / express / lib / view.js
1
2 /*!
3  * Express - view
4  * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
5  * MIT Licensed
6  */
7
8 /**
9  * Module dependencies.
10  */
11
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
20   , merge = utils.merge
21   , http = require('http')
22   , res = http.ServerResponse.prototype;
23
24 /**
25  * Expose constructors.
26  */
27
28 exports = module.exports = View;
29
30 /**
31  * Export template engine registrar.
32  */
33
34 exports.register = View.register;
35
36 /**
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()`.
40  *
41  * @param {String} view
42  * @param {Object} cache
43  * @param {Object} cid
44  * @param {Object} options
45  * @return {View}
46  * @api private
47  */
48
49 exports.compile = function(view, cache, cid, options){
50   if (cache && cid && cache[cid]) return cache[cid];
51
52   // lookup
53   view = exports.lookup(view, options);
54
55   // hints
56   if (!view.exists) {
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;
60     throw err;
61   }    
62
63   // compile
64   options.filename = view.path;
65   view.fn = view.templateEngine.compile(view.contents, options);
66   cache[cid] = view;
67   
68   return view;
69 };
70
71 /**
72  * Lookup `view`, returning an instanceof `View`.
73  *
74  * Options:
75  *
76  *   - `root` root directory path
77  *   - `defaultEngine` default template engine
78  *   - `parentView` parent `View` object
79  *   - `cache` cache object
80  *   - `cacheid` optional cache id
81  *
82  * Lookup:
83  *
84  *   - partial `_<name>` 
85  *   - any `<name>/index` 
86  *   - non-layout `../<name>/index` 
87  *   - any `<root>/<name>` 
88  *   - partial `<root>/_<name>` 
89  *
90  * @param {String} view
91  * @param {Object} options
92  * @return {View}
93  * @api private
94  */
95
96 exports.lookup = function(view, options){
97   var orig = view = new View(view, options)
98     , partial = options.isPartial
99     , layout = options.isLayout;
100
101   // Try _ prefix ex: ./views/_<name>.jade
102   // taking precedence over the direct path
103   if (partial) {
104     view = new View(orig.prefixPath, options);
105     if (!view.exists) view = orig;
106   }
107
108   // Try index ex: ./views/user/index.jade
109   if (!layout && !view.exists) view = new View(orig.indexPath, options);
110
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);
114
115   // Try root ex: <root>/user.jade
116   if (!view.exists) view = new View(orig.rootPath, options);
117
118   // Try root _ prefix ex: <root>/_user.jade
119   if (!view.exists && partial) view = new View(view.prefixPath, options);
120
121   view.original = orig;
122   return view;
123 };
124
125 /**
126  * Partial render helper.
127  *
128  * @api private
129  */
130
131 function renderPartial(res, view, options, parentLocals, parent){
132   var collection, object, locals;
133
134   if (options) {
135     // collection
136     if (options.collection) {
137       collection = options.collection;
138       delete options.collection;
139     } else if ('length' in options) {
140       collection = options;
141       options = {};
142     }
143
144     // locals
145     if (options.locals) {
146       locals = options.locals;
147       delete options.locals;
148     }
149
150     // object
151     if ('Object' != options.constructor.name) {
152       object = options;
153       options = {};
154     } else if (undefined != options.object) {
155       object = options.object;
156       delete options.object;
157     }
158   } else {
159     options = {};
160   }
161
162   // Inherit locals from parent
163   union(options, parentLocals);
164   
165   // Merge locals
166   if (locals) merge(options, locals);
167
168   // Partials dont need layouts
169   options.isPartial = true;
170   options.layout = false;
171
172   // Deduce name from view path
173   var name = options.as || partial.resolveObjectName(view);
174
175   // Render partial
176   function render(){
177     if (object) {
178       if ('string' == typeof name) {
179         options[name] = object;
180       } else if (name === global) {
181         merge(options, object);
182       } else {
183         options.scope = object;
184       }
185     }
186     return res.render(view, options, null, parent, true);
187   }
188
189   // Collection support
190   if (collection) {
191     var len = collection.length
192       , buf = ''
193       , keys
194       , key
195       , val;
196
197     options.collectionLength = len;
198
199     if ('number' == typeof len || Array.isArray(collection)) {
200       for (var i = 0; i < len; ++i) {
201         val = collection[i];
202         options.firstInCollection = i == 0;
203         options.indexInCollection = i;
204         options.lastInCollection = i == len - 1;
205         object = val;
206         buf += render();
207       }      
208     } else {
209       keys = Object.keys(collection);
210       len = keys.length;
211       options.collectionLength = len;
212       options.collectionKeys = keys;
213       for (var i = 0; i < len; ++i) {
214         key = keys[i];
215         val = collection[key];
216         options.keyInCollection = key;
217         options.firstInCollection = i == 0;
218         options.indexInCollection = i;
219         options.lastInCollection = i == len - 1;
220         object = val;
221         buf += render();
222       }
223     }
224
225     return buf;
226   } else {
227     return render();
228   }
229 };
230
231 /**
232  * Render `view` partial with the given `options`. Optionally a 
233  * callback `fn(err, str)` may be passed instead of writing to
234  * the socket.
235  *
236  * Options:
237  *
238  *   - `object` Single object with name derived from the view (unless `as` is present) 
239  *
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`
244  *
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.
247  *
248  * @param  {String} view
249  * @param  {Object|Array|Function} options, collection, callback, or object
250  * @param  {Function} fn
251  * @return {String}
252  * @api public
253  */
254
255 res.partial = function(view, options, fn){
256   var app = this.app
257     , options = options || {}
258     , viewEngine = app.set('view engine')
259     , parent = {};
260
261   // accept callback as second argument
262   if ('function' == typeof options) {
263     fn = options;
264     options = {};
265   }
266
267   // root "views" option
268   parent.dirname = app.set('views') || process.cwd() + '/views';
269
270   // utilize "view engine" option
271   if (viewEngine) parent.engine = viewEngine;
272
273   // render the partial
274   try {
275     var str = renderPartial(this, view, options, null, parent);
276   } catch (err) {
277     if (fn) {
278       fn(err);
279     } else {
280       this.req.next(err);
281     }
282     return;
283   }
284
285   // callback or transfer
286   if (fn) {
287     fn(null, str);
288   } else {
289     this.send(str);
290   }
291 };
292
293 /**
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.
297  *
298  * Options:
299  *  
300  *  - `scope`     Template evaluation context (the value of `this`)
301  *  - `debug`     Output debugging information
302  *  - `status`    Response status code
303  *
304  * @param  {String} view
305  * @param  {Object|Function} options or callback function
306  * @param  {Function} fn
307  * @api public
308  */
309
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;
314   }
315
316   try {
317     return this._render(view, opts, fn, parent, sub);
318   } catch (err) {
319     // callback given
320     if (fn) {
321       fn(err);
322     // unwind to root call to prevent multiple callbacks
323     } else if (sub) {
324       throw err;
325     // root template, next(err)
326     } else {
327       this.req.next(err);
328     }
329   }
330 };
331
332 // private render()
333
334 res._render = function(view, opts, fn, parent, sub){
335   var options = {}
336     , self = this
337     , app = this.app
338     , helpers = app._locals
339     , dynamicHelpers = app.dynamicViewHelpers
340     , viewOptions = app.set('view options')
341     , root = app.set('views') || process.cwd() + '/views';
342
343   // cache id
344   var cid = app.enabled('view cache')
345     ? view + (parent ? ':' + parent.path : '')
346     : false;
347
348   // merge "view options"
349   if (viewOptions) merge(options, viewOptions);
350
351   // merge res._locals
352   if (this._locals) merge(options, this._locals);
353
354   // merge render() options
355   if (opts) merge(options, opts);
356
357   // merge render() .locals
358   if (opts && opts.locals) merge(options, opts.locals);
359
360   // status support
361   if (options.status) this.statusCode = options.status;
362
363   // capture attempts
364   options.attempts = [];
365
366   var partial = options.isPartial
367     , layout = options.layout;
368
369   // Layout support
370   if (true === layout || undefined === layout) {
371     layout = 'layout';
372   }
373
374   // Default execution scope to a plain object
375   options.scope = options.scope || {};
376
377   // Populate view
378   options.parentView = parent;
379
380   // "views" setting
381   options.root = root;
382
383   // "view engine" setting
384   options.defaultEngine = app.set('view engine');
385
386   // charset option
387   if (options.charset) this.charset = options.charset;
388
389   // Dynamic helper support
390   if (false !== options.dynamicHelpers) {
391     // cache
392     if (!this.__dynamicHelpers) {
393       this.__dynamicHelpers = {};
394       for (var key in dynamicHelpers) {
395         this.__dynamicHelpers[key] = dynamicHelpers[key].call(
396             this.app
397           , this.req
398           , this);
399       }
400     }
401
402     // apply
403     merge(options, this.__dynamicHelpers);
404   }
405
406   // Merge view helpers
407   union(options, helpers);
408
409   // Always expose partial() as a local
410   options.partial = function(path, opts){
411     return renderPartial(self, path, opts, options, view);
412   };
413
414   // View lookup
415   options.hint = app.enabled('hints');
416   view = exports.compile(view, app.cache, cid, options);
417
418   // layout helper
419   options.layout = function(path){
420     layout = path;
421   };
422
423   // render
424   var str = view.fn.call(options.scope, options);
425
426   // layout expected
427   if (layout) {
428     options.isLayout = true;
429     options.layout = false;
430     options.body = str;
431     this.render(layout, options, fn, view, true);
432   // partial return
433   } else if (partial) {
434     return str;
435   // render complete, and 
436   // callback given
437   } else if (fn) {
438     fn(null, str);
439   // respond
440   } else {
441     this.send(str);
442   }
443 }
444
445 /**
446  * Hint at view path resolution, outputting the
447  * paths that Express has tried.
448  *
449  * @api private
450  */
451
452 function hintAtViewPaths(view, options) {
453   console.error();
454   console.error('failed to locate view "' + view.view + '", tried:');
455   options.attempts.forEach(function(path){
456     console.error('  - %s', path);
457   });
458   console.error();
459 }