460 lines · 10.6 KB · JavaScript
RawDownload
 /*! * Express - view * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca> * 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 `_<name>` *   - any `<name>/index` *   - non-layout `../<name>/index` *   - any `<root>/<name>` *   - partial `<root>/_<name>` * * @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/_<name>.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 ../<name>/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: <root>/user.jade  if (!view.exists) view = new View(orig.rootPath, options);   // Try root _ prefix ex: <root>/_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();}