UNPKG

express

Version:

Sinatra inspired web development framework

352 lines (305 loc) 8.63 kB
/*! * Express - View * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca> * MIT Licensed */ /** * Module dependencies. */ var extname = require('path').extname , utils = require('connect').utils , http = require('http') , fs = require('fs') , mime = utils.mime; /** * Cache supported template engine exports to * increase performance by lowering the number * of calls to `require()`. * * @type Object */ var cache = {}; /** * Cache view contents to prevent I/O hits. * * @type Object */ var viewCache = {}; /** * Cache view path derived names. * * @type Object */ var viewNameCache = {}; /** * Synchronously cache view at the given `path`. * * @param {String} path * @return {String} * @api private */ function cacheViewSync(path) { return viewCache[path] = fs.readFileSync(path, 'utf8'); } /** * Return view root path for the given `app`. * * @param {express.Server} app * @return {String} * @api private */ function viewRoot(app) { return app.set('views') || process.cwd() + '/views'; } /** * Return object name deduced from the given `view` path. * * Examples: * * "movie/director" -> director * "movie.director" -> director * "forum/thread/post" -> post * "forum/thread.post" -> post * * @param {String} view * @return {String} * @api private */ function objectName(view) { return view.split('/').slice(-1)[0].split('.')[0]; } /** * Register the given template engine `exports` * as `ext`. For example we may wish to map ".html" * files to jade: * * app.register('.html', require('jade')); * * This is also useful for libraries that may not * match extensions correctly. For example my haml.js * library is installed from npm as "hamljs" so instead * of layout.hamljs, we can register the engine as ".haml": * * app.register('.haml', require('haml-js')); * * For engines that do not comply with the Express * specification, we can also wrap their api this way. * * app.register('.foo', { * render: function(str, options) { * // perhaps their api is * // return foo.toHTML(str, options); * } * }); * * @param {String} ext * @param {Object} obj * @api public */ exports.register = function(ext, exports) { cache[ext] = exports; }; /** * Render `view` partial with the given `options`. * * 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} options, collection, or object * @return {String} * @api public */ http.ServerResponse.prototype.partial = function(view, options, ext, locals){ locals = locals || {}; // Inherit parent view extension when not present if (ext && view.indexOf('.') < 0) { view += ext; } // Allow collection to be passed as second param if (options) { if ('length' in options) { options = { collection: options }; } else if (!options.collection && !options.locals && !options.object) { options = { object: options }; } } else { options = {}; } // Inherit locals from parent options.locals = options.locals ? utils.merge(locals, options.locals) : locals; // Partials dont need layouts options.partial = true; options.layout = false; // Deduce name from view path var name = options.as || viewNameCache[view] || (viewNameCache[view] = objectName(view)); // Collection support var collection = options.collection; if (collection) { var len = collection.length , buf = ''; delete options.collection; options.locals.collectionLength = len; for (var i = 0; i < len; ++i) { var val = collection[i]; options.locals.firstInCollection = i === 0; options.locals.indexInCollection = i; options.locals.lastInCollection = i === len - 1; options.object = val; buf += this.partial(view, options); } return buf; } else { if (options.object) { if ('string' == typeof name) { options.locals[name] = options.object; } else if (name === global) { utils.merge(options.locals, options.object); } else { options.scope = options.object; } } return this.render(view, options); } }; /** * 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: * * Most engines accept one or more of the following options, * both _haml_ and _jade_ accept all: * * - `scope` Template evaluation context (the value of `this`) * - `locals` Object containing local variables * - `debug` Output debugging information * - `status` Response status code, defaults to 200 * - `headers` Response headers object * * @param {String} view * @param {Object|Function} options or callback function * @param {Function} fn * @api public */ http.ServerResponse.prototype.render = function(view, options, fn){ // Support callback function as second arg if (typeof options === 'function') { fn = options, options = {}; } var options = options || {} , app = this.app , viewOptions = app.settings['view options'] , defaultEngine = app.settings['view engine']; // Mixin "view options" if (viewOptions) options.__proto__ = viewOptions; // Support "view engine" setting if (view.indexOf('.') < 0 && defaultEngine) { view += '.' + defaultEngine; } // Defaults var self = this , helpers = this.app.viewHelpers , dynamicHelpers = this.app.dynamicViewHelpers , root = viewRoot(this.app) , ext = extname(view) , partial = options.partial , layout = options.layout === undefined ? true : options.layout , layout = layout === true ? 'layout' + ext : layout; // Allow layout name without extension if (typeof layout === 'string' && layout.indexOf('.') < 0) { layout += ext; } // Default execution scope to the response options.scope = options.scope || this.req; // Auto-cache in production if ('production' == app.settings.env) { options.cache = true; } // Partials support if (options.partial) { root = app.settings.partials || root + '/partials'; } // View path var path = view[0] === '/' ? view : root + '/' + view; // Pass filename to the engine and view var locals = options.locals = options.locals || {}; options.locals.__filename = options.filename = path; // Dynamic helper support if (false !== options.dynamicHelpers) { // cache if (!this.__dynamicHelpers) { this.__dynamicHelpers = {}; var keys = Object.keys(dynamicHelpers); for (var i = 0, len = keys.length; i < len; ++i) { var key = keys[i] , val = dynamicHelpers[key]; if (typeof val === 'function') { this.__dynamicHelpers[key] = val.call( this.app , this.req , this); } } } // apply helpers.__proto__ = this.__dynamicHelpers; } // Merge view helpers options.locals.__proto__ = helpers; // Always expose partial() as a local options.locals.partial = function(view, options){ return self.partial.call(self, view, options, ext, locals); }; function error(err) { if (fn) { fn(err); } else { self.req.next(err); } } // Cache contents try { var str = (options.cache ? viewCache[path] : null) || cacheViewSync(path); } catch (err) { return error(err); } // Cache template engine exports var engine = cache[ext] || (cache[ext] = require(ext.substr(1))); // Attempt render try { var str = engine.render(str, options); } catch (err) { return error(err); } // Layout support if (layout) { options.layout = false; options.locals.body = str; options.isLayout = true; self.render(layout, options, fn); } else if (partial) { return str; } else if (fn) { fn(null, str); } else { this.send(str, options.headers, options.status); } };