/*! * Express - view * Copyright(c) 2010 TJ Holowaychuk * MIT Licensed */ /** * Module dependencies. */ var path = require( 'path' ) , extname = path.extname , dirname = path.dirname , basename = path.basename , utils = require( 'connect' ).utils , View = require( './view/view' ) , partial = require( './view/partial' ) , union = require( './utils' ).union , merge = utils.merge , http = require( 'http' ) , res = http.ServerResponse.prototype; /** * Expose constructors. */ exports = module.exports = View; /** * Export template engine registrar. */ exports.register = View.register; /** * Lookup and compile `view` with cache support by supplying * both the `cache` object and `cid` string, * followed by `options` passed to `exports.lookup()`. * * @param {String} view * @param {Object} cache * @param {Object} cid * @param {Object} options * @return {View} * @api private */ exports.compile = function ( view, cache, cid, options ) { if ( cache && cid && cache[cid] ) { options.filename = cache[cid].path; return cache[cid]; } // lookup view = exports.lookup( view, options ); // hints if ( !view.exists ) { if ( options.hint ) { hintAtViewPaths( view.original, options ); } var err = new Error( 'failed to locate view "' + view.original.view + '"' ); err.view = view.original; throw err; } // compile options.filename = view.path; view.fn = view.templateEngine.compile( view.contents, options ); cache[cid] = view; return view; }; /** * Lookup `view`, returning an instanceof `View`. * * Options: * * - `root` root directory path * - `defaultEngine` default template engine * - `parentView` parent `View` object * - `cache` cache object * - `cacheid` optional cache id * * Lookup: * * - partial `_` * - any `/index` * - non-layout `..//index` * - any `/` * - partial `/_` * * @param {String} view * @param {Object} options * @return {View} * @api private */ exports.lookup = function ( view, options ) { var orig = view = new View( view, options ) , partial = options.isPartial , layout = options.isLayout; // Try _ prefix ex: ./views/_.jade // taking precedence over the direct path if ( partial ) { view = new View( orig.prefixPath, options ); if ( !view.exists ) { view = orig; } } // Try index ex: ./views/user/index.jade if ( !layout && !view.exists ) { view = new View( orig.indexPath, options ); } // Try ..//index ex: ../user/index.jade // when calling partial('user') within the same dir if ( !layout && !view.exists ) { view = new View( orig.upIndexPath, options ); } // Try root ex: /user.jade if ( !view.exists ) { view = new View( orig.rootPath, options ); } // Try root _ prefix ex: /_user.jade if ( !view.exists && partial ) { view = new View( view.prefixPath, options ); } view.original = orig; return view; }; /** * Partial render helper. * * @api private */ function renderPartial( res, view, options, parentLocals, parent ) { var collection, object, locals; if ( options ) { // collection if ( options.collection ) { collection = options.collection; delete options.collection; } else if ( 'length' in options ) { collection = options; options = {}; } // locals if ( options.locals ) { locals = options.locals; delete options.locals; } // object if ( 'Object' != options.constructor.name ) { object = options; options = {}; } else if ( undefined != options.object ) { object = options.object; delete options.object; } } else { options = {}; } // Inherit locals from parent union( options, parentLocals ); // Merge locals if ( locals ) { merge( options, locals ); } // Partials dont need layouts options.isPartial = true; options.layout = false; // Deduce name from view path var name = options.as || partial.resolveObjectName( view ); // Render partial function render() { if ( object ) { if ( 'string' == typeof name ) { options[name] = object; } else if ( name === global ) { merge( options, object ); } } return res.render( view, options, null, parent, true ); } // Collection support if ( collection ) { var len = collection.length , buf = '' , keys , key , val; options.collectionLength = len; if ( 'number' == typeof len || Array.isArray( collection ) ) { for ( var i = 0; i < len; ++i ) { val = collection[i]; options.firstInCollection = i == 0; options.indexInCollection = i; options.lastInCollection = i == len - 1; object = val; buf += render(); } } else { keys = Object.keys( collection ); len = keys.length; options.collectionLength = len; options.collectionKeys = keys; for ( var i = 0; i < len; ++i ) { key = keys[i]; val = collection[key]; options.keyInCollection = key; options.firstInCollection = i == 0; options.indexInCollection = i; options.lastInCollection = i == len - 1; object = val; buf += render(); } } return buf; } else { return render(); } } ; /** * Render `view` partial with the given `options`. Optionally a * callback `fn(err, str)` may be passed instead of writing to * the socket. * * 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|Function} options, collection, callback, or object * @param {Function} fn * @return {String} * @api public */ res.partial = function ( view, options, fn ) { var app = this.app , options = options || {} , viewEngine = app.set( 'view engine' ) , parent = {}; // accept callback as second argument if ( 'function' == typeof options ) { fn = options; options = {}; } // root "views" option parent.dirname = app.set( 'views' ) || process.cwd() + '/views'; // utilize "view engine" option if ( viewEngine ) { parent.engine = viewEngine; } // render the partial try { var str = renderPartial( this, view, options, null, parent ); } catch ( err ) { if ( fn ) { fn( err ); } else { this.req.next( err ); } return; } // callback or transfer if ( fn ) { fn( null, str ); } else { this.send( str ); } }; /** * 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: * * - `scope` Template evaluation context (the value of `this`) * - `debug` Output debugging information * - `status` Response status code * * @param {String} view * @param {Object|Function} options or callback function * @param {Function} fn * @api public */ res.render = function ( view, opts, fn, parent, sub ) { // support callback function as second arg if ( 'function' == typeof opts ) { fn = opts, opts = null; } try { return this._render( view, opts, fn, parent, sub ); } catch ( err ) { // callback given if ( fn ) { fn( err ); // unwind to root call to prevent multiple callbacks } else if ( sub ) { throw err; // root template, next(err) } else { this.req.next( err ); } } }; // private render() res._render = function ( view, opts, fn, parent, sub ) { var options = {} , self = this , app = this.app , helpers = app._locals , dynamicHelpers = app.dynamicViewHelpers , viewOptions = app.set( 'view options' ) , root = app.set( 'views' ) || process.cwd() + '/views'; // cache id var cid = app.enabled( 'view cache' ) ? view + (parent ? ':' + parent.path : '') : false; // merge "view options" if ( viewOptions ) { merge( options, viewOptions ); } // merge res._locals if ( this._locals ) { merge( options, this._locals ); } // merge render() options if ( opts ) { merge( options, opts ); } // merge render() .locals if ( opts && opts.locals ) { merge( options, opts.locals ); } // status support if ( options.status ) { this.statusCode = options.status; } // capture attempts options.attempts = []; var partial = options.isPartial , layout = options.layout; // Layout support if ( true === layout || undefined === layout ) { layout = 'layout'; } // Default execution scope to a plain object options.scope = options.scope || {}; // Populate view options.parentView = parent; // "views" setting options.root = root; // "view engine" setting options.defaultEngine = app.set( 'view engine' ); // charset option if ( options.charset ) { this.charset = options.charset; } // Dynamic helper support if ( false !== options.dynamicHelpers ) { // cache if ( !this.__dynamicHelpers ) { this.__dynamicHelpers = {}; for ( var key in dynamicHelpers ) { this.__dynamicHelpers[key] = dynamicHelpers[key].call( this.app , this.req , this ); } } // apply merge( options, this.__dynamicHelpers ); } // Merge view helpers union( options, helpers ); // Always expose partial() as a local options.partial = function ( path, opts ) { return renderPartial( self, path, opts, options, view ); }; // View lookup options.hint = app.enabled( 'hints' ); view = exports.compile( view, app.cache, cid, options ); // layout helper options.layout = function ( path ) { layout = path; }; // render var str = view.fn.call( options.scope, options ); // layout expected if ( layout ) { options.isLayout = true; options.layout = false; options.body = str; this.render( layout, options, fn, view, true ); // partial return } else if ( partial ) { return str; // render complete, and // callback given } else if ( fn ) { fn( null, str ); // respond } else { this.send( str ); } } /** * Hint at view path resolution, outputting the * paths that Express has tried. * * @api private */ function hintAtViewPaths( view, options ) { console.error(); console.error( 'failed to locate view "' + view.view + '", tried:' ); options.attempts.forEach( function ( path ) { console.error( ' - %s', path ); } ); console.error(); }