]> git.cworth.org Git - obsolete/notmuch-web/blob - node_modules/express/lib/view.js
Install the "express" node module via npm
[obsolete/notmuch-web] / 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  * Partial render helper.
38  *
39  * @api private
40  */
41
42 function renderPartial(res, view, options, parentLocals, parent){
43   var collection, object, locals;
44
45   // Inherit parent view extension when not present
46   if (parent && !~view.indexOf('.')) {
47     view += parent.extension;
48   }
49
50   if (options) {
51     // collection
52     if (options.collection) {
53       collection = options.collection;
54       delete options.collection;
55     } else if ('length' in options) {
56       collection = options;
57       options = {};
58     }
59
60     // locals
61     if (options.locals) {
62       locals = options.locals;
63       delete options.locals;
64     }
65
66     // object
67     if ('Object' != options.constructor.name) {
68       object = options;
69       options = {};
70     } else if (undefined != options.object) {
71       object = options.object;
72       delete options.object;
73     }
74   } else {
75     options = {};
76   }
77
78   // Inherit locals from parent
79   union(options, parentLocals);
80   
81   // Merge locals
82   if (locals) merge(options, locals);
83
84   // Partials dont need layouts
85   options.renderPartial = true;
86   options.layout = false;
87
88   // Deduce name from view path
89   var name = options.as || partial.resolveObjectName(view);
90
91   // Render partial
92   function render(){
93     if (object) {
94       if ('string' == typeof name) {
95         options[name] = object;
96       } else if (name === global) {
97         merge(options, object);
98       } else {
99         options.scope = object;
100       }
101     }
102     return res.render(view, options, null, parent, true);
103   }
104
105   // Collection support
106   if (collection) {
107     var len = collection.length
108       , buf = ''
109       , keys
110       , key
111       , val;
112
113     options.collectionLength = len;
114
115     if ('number' == typeof len || Array.isArray(collection)) {
116       for (var i = 0; i < len; ++i) {
117         val = collection[i];
118         options.firstInCollection = i == 0;
119         options.indexInCollection = i;
120         options.lastInCollection = i == len - 1;
121         object = val;
122         buf += render();
123       }      
124     } else {
125       keys = Object.keys(collection);
126       len = keys.length;
127       options.collectionLength = len;
128       options.collectionKeys = keys;
129       for (var i = 0; i < len; ++i) {
130         key = keys[i];
131         val = collection[key];
132         options.keyInCollection = key;
133         options.firstInCollection = i == 0;
134         options.indexInCollection = i;
135         options.lastInCollection = i == len - 1;
136         object = val;
137         buf += render();
138       }
139     }
140
141     return buf;
142   } else {
143     return render();
144   }
145 };
146
147 /**
148  * Render `view` partial with the given `options`. Optionally a 
149  * callback `fn(err, str)` may be passed instead of writing to
150  * the socket.
151  *
152  * Options:
153  *
154  *   - `object` Single object with name derived from the view (unless `as` is present) 
155  *
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`
160  *
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.
163  *
164  * @param  {String} view
165  * @param  {Object|Array|Function} options, collection, callback, or object
166  * @param  {Function} fn
167  * @return {String}
168  * @api public
169  */
170
171 res.partial = function(view, options, fn){
172   var app = this.app
173     , options = options || {}
174     , parent = {};
175
176   // accept callback as second argument
177   if ('function' == typeof options) {
178     fn = options;
179     options = {};
180   }
181
182   // root "views" option
183   parent.dirname = app.set('views') || process.cwd() + '/views';
184
185   // utilize "view engine" option
186   if (app.set('view engine')) {
187     parent.extension = '.' + app.set('view engine');
188   }
189
190   // render the partial
191   try {
192     var str = renderPartial(this, view, options, null, parent);
193   } catch (err) {
194     if (fn) {
195       fn(err);
196     } else {
197       this.req.next(err);
198     }
199     return;
200   }
201
202   // callback or transfer
203   if (fn) {
204     fn(null, str);
205   } else {
206     this.send(str);
207   }
208 };
209
210 /**
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.
214  *
215  * Options:
216  *  
217  *  - `scope`     Template evaluation context (the value of `this`)
218  *  - `debug`     Output debugging information
219  *  - `status`    Response status code
220  *
221  * @param  {String} view
222  * @param  {Object|Function} options or callback function
223  * @param  {Function} fn
224  * @api public
225  */
226
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;
231   }
232
233   try {
234     return this._render(view, opts, fn, parent, sub);
235   } catch (err) {
236     // callback given
237     if (fn) {
238       fn(err);
239     // unwind to root call to prevent
240     // several next(err) calls
241     } else if (sub) {
242       throw err;
243     // root template, next(err)
244     } else {
245       this.req.next(err);
246     }
247   }
248 };
249
250 // private render()
251
252 res._render = function(view, opts, fn, parent, sub){
253   var options = {}
254     , self = this
255     , app = this.app
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';
261
262   // cache id
263   var cid = view + (parent ? ':' + parent.path : '');
264
265   // merge "view options"
266   if (viewOptions) merge(options, viewOptions);
267
268   // merge res._locals
269   if (this._locals) merge(options, this._locals);
270
271   // merge render() options
272   if (opts) merge(options, opts);
273
274   // merge render() .locals
275   if (opts && opts.locals) merge(options, opts.locals);
276
277   // status support
278   if (options.status) this.statusCode = options.status;
279
280   // capture attempts
281   options.attempts = [];
282
283   var partial = options.renderPartial
284     , layout = options.layout;
285
286   // Layout support
287   if (true === layout || undefined === layout) {
288     layout = 'layout';
289   }
290
291   // Default execution scope to a plain object
292   options.scope = options.scope || {};
293
294   // Populate view
295   options.parentView = parent;
296
297   // "views" setting
298   options.root = root;
299
300   // "view engine" setting
301   options.defaultEngine = app.set('view engine');
302
303   // charset option
304   if (options.charset) this.charset = options.charset;
305
306   // Dynamic helper support
307   if (false !== options.dynamicHelpers) {
308     // cache
309     if (!this.__dynamicHelpers) {
310       this.__dynamicHelpers = {};
311       for (var key in dynamicHelpers) {
312         this.__dynamicHelpers[key] = dynamicHelpers[key].call(
313             this.app
314           , this.req
315           , this);
316       }
317     }
318
319     // apply
320     merge(options, this.__dynamicHelpers);
321   }
322
323   // Merge view helpers
324   union(options, helpers);
325
326   // Always expose partial() as a local
327   options.partial = function(path, opts){
328     return renderPartial(self, path, opts, options, view);
329   };
330
331   // cached view
332   if (app.cache[cid]) {
333     view = app.cache[cid];
334     options.filename = view.path;
335   // resolve view
336   } else {
337     var orig = view = new View(view, options);
338
339     // Try _ prefix ex: ./views/_<name>.jade
340     if (partial) {
341       view = new View(orig.prefixPath, options);
342       if (!view.exists) view = orig;
343     }
344   
345     // Try index ex: ./views/user/index.jade
346     if (!view.exists) view = new View(orig.indexPath, options);
347
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);
351
352     // Try root ex: <root>/user.jade
353     if (!view.exists) view = new View(orig.rootPath, options);
354
355     // Try root _ prefix ex: <root>/_user.jade
356     if (!view.exists && partial) view = new View(view.prefixPath, options);
357
358     // Does not exist
359     if (!view.exists) {
360       if (app.enabled('hints')) hintAtViewPaths(orig, options);
361       var err = new Error('failed to locate view "' + orig.view + '"');
362       err.view = orig;
363       throw err;
364     }    
365
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;
370   }
371
372   // layout helper
373   options.layout = function(path){
374     layout = path;
375   };
376
377   // render
378   var str = view.fn.call(options.scope, options);
379
380   // layout expected
381   if (layout) {
382     options.isLayout = true;
383     options.layout = false;
384     options.body = str;
385     this.render(layout, options, fn, view, true);
386   // partial return
387   } else if (partial) {
388     return str;
389   // render complete, and 
390   // callback given
391   } else if (fn) {
392     fn(null, str);
393   // respond
394   } else {
395     this.send(str);
396   }
397 }
398
399 /**
400  * Hint at view path resolution, outputting the
401  * paths that Express has tried.
402  *
403  * @api private
404  */
405
406 function hintAtViewPaths(view, options) {
407   console.error();
408   console.error('failed to locate view "' + view.view + '", tried:');
409   options.attempts.forEach(function(path){
410     console.error('  - %s', path);
411   });
412   console.error();
413 }