index.js 9.44 KB
var path = require('path')
  , fs = require('fs')
  , exists = fs.existsSync || path.existsSync
  , resolve = path.resolve
  , dirname = path.dirname
  , extname = path.extname
  , basename = path.basename;


/**
 * Express 3.x Layout & Partial support.
 *
 * The beloved feature from Express 2.x is back as a middleware.
 *
 * Example:
 *    
 *    var express = require('express')
 *      , partials = require('express-partials')
 *      , app = express();
 *    app.use(partials());
 *    // three ways to register a template engine:
 *    partials.register('coffee','coffeekup');
 *    partials.register('coffee',require('coffeekup'));
 *    partials.register('coffee',require('coffeekup').render);
 *    app.get('/',function(req,res,next){
 *      res.render('index.ejs') // renders layout.ejs with index.ejs as `body`.
 *    })
 * 
 * Options:
 *
 *    none
 *
 */

module.exports = function(){
  return function(req,res,next){
    // res.partial(view,options) -> res.render() (ignores any layouts)
    res.partial = res.render;

    // in template partial(view,options)
    res.locals.partial = partial.bind(res);

    // layout support
    var _render = res.render.bind(res);
    res.render = function(name, options, fn){
      var layout = options && options.layout;

      // default layout
      if( layout === true || layout === undefined ) {
        // Try to find default layout in view options, if not found, seek for 'layout'
        var viewOptions = res.app.get('view options');
        layout = viewOptions && viewOptions.defaultLayout || 'layout';
      }
      
      // layout
      if( layout ){
        // first render normally
        _render(name, options, function(err, body){
          if( err )
            return fn ? fn(err) : req.next(err);

          options = options || {};
          options.body = body;

          // calculate the layout vars
          var ext = extname(name) || '.'+(res.app.get('view engine') || 'ejs');
          var root = req.app.get('views') || process.cwd() + '/views';
          var dir = dirname(layout) == '.' ? root : resolve(root,dirname(layout));
          var filename = dir+(path.sep||'/')+basename(layout,ext)+ext;

          // See if we even have a layout to use
          // If so, render it. If not, then fallback to just the original template
          if (exists(filename)) {
            layout = dirname(lookup(dir, layout, ext))+(path.sep||'/')+basename(layout,ext)+ext;
            _render(layout, options, fn);
          } else {
            // layout may be in the same folder than the view
            dir = dirname(name) == '.' ? root : resolve(root,dirname(name));
            filename = dir+(path.sep||'/')+basename(layout,ext)+ext;

            if(exists(filename)) {
              layout = dirname(lookup(dir, layout, ext))+(path.sep||'/')+basename(layout,ext)+ext;
              _render(layout, options, fn);
            } else {
              _render(name, options, fn);
            }
          }
        })

      // no layout
      } else {
        _render(name, options, fn);
      }
    }

    // done
    next();
  }
}

/*** 
 * Allow to register a specific rendering
 * function for a given extension.
 * (Similar to Express 2.x register() function.)
 *
 * The second argument might be:
 *   a template module's name
 *   a module with a `render` method
 *   a synchronous `render` method
 */

function register(ext,render){
  if(ext[0] != '.') {
    ext = '.' + ext;
  }
  if(typeof render == 'string') {
    render = require(render);
  }
  if(typeof render.render != 'undefined') {
    register[ext] = render.render;
  } else {
    register[ext] = render;
  }
};

module.exports.register = register;

/**
 * Automatically assign a render() function
 * from a module of the same name if none
 * has been registered.
 */

function renderer(ext){
  if(ext[0] !== '.'){
    ext = '.' + ext;
  }
  return register[ext] != null
    ? register[ext]
    : register[ext] = require(ext.slice(1)).render;
};

module.exports.renderer = renderer;

/**
 * Memory cache for resolved object names.
 */

var cache = {};

/**
 * Resolve partial object name from the view path.
 *
 * Examples:
 *
 *   "user.ejs" becomes "user"
 *   "forum thread.ejs" becomes "forumThread"
 *   "forum/thread/post.ejs" becomes "post"
 *   "blog-post.ejs" becomes "blogPost"
 *
 * @return {String}
 * @api private
 */

function resolveObjectName(view){
  return cache[view] || (cache[view] = view
    .split(path.sep || '/')
    .slice(-1)[0]
    .split('.')[0]
    .replace(/^_/, '')
    .replace(/[^a-zA-Z0-9 ]+/g, ' ')
    .split(/ +/).map(function(word, i){
      return i
        ? word[0].toUpperCase() + word.substr(1)
        : word;
    }).join(''));
};

/**
 * Lookup:
 *
 *   - partial `_<name>`
 *   - any `<name>/index`
 *   - non-layout `../<name>/index`
 *   - any `<root>/<name>`
 *   - partial `<root>/_<name>`
 *
 * @param {View} view
 * @return {String}
 * @api private
 */

function lookup(root, view, ext){
  var name = resolveObjectName(view);
  var original = view;

  // Try root ex: <root>/user.jade
  view = resolve(root, basename(original,ext)+ext);
  if( exists(view) ) return view;

  // Try subdir ex: <root>/subdir/user.jade
  view = resolve(root, dirname(original), basename(original,ext)+ext);
  if( exists(view) ) return view;

  // Try _ prefix ex: ./views/_<name>.jade
  // taking precedence over the direct path
  view = resolve(root,'_'+name+ext)
  if( exists(view) ) return view;

  // Try index ex: ./views/user/index.jade
  view = resolve(root,name,'index'+ext);
  if( exists(view) ) return view;

  // Try ../<name>/index ex: ../user/index.jade
  // when calling partial('user') within the same dir
  view = resolve(root,'..',name,'index'+ext);
  if( exists(view) ) return view;

  // Try root ex: <root>/user.jade
  view = resolve(root,name+ext);
  if( exists(view) ) return view;

  return null;
};
module.exports.lookup = lookup;

/**
 * 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} options, collection or object
 * @return {String}
 * @api public
 */

function partial(view, options){
  var collection
    , object
    , locals
    , name;

  // parse options
  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( options.object != undefined ){
      object = options.object;
      delete options.object;
    }
  } else {
    options = {};
  }

  // merge locals into options
  if( locals )
    options.__proto__ = locals;

  // merge app locals into 
  for(var k in this.app.locals)
    options[k] = options[k] || this.app.locals[k];

  // merge locals, which as set using app.use(function(...){ res.locals = X; }) 
  for(var k in this.req.res.locals)
    options[k] = options[k] || this.req.res.locals[k];

  // let partials render partials
  options.partial = partial.bind(this);

  // extract object name from view
  name = options.as || resolveObjectName(view);

  // find view
  var root = this.app.get('views') || process.cwd() + '/views'
    , ext = extname(view) || '.' + (this.app.get('view engine')||'ejs')
    , file = lookup(root, view, ext);
  
  // read view
  var source = fs.readFileSync(file,'utf8');

  // set filename option for renderer (Jade requires this for includes)
  options.filename = file;

  // render partial
  function render(){
    if (object) {
      if ('string' == typeof name) {
        options[name] = object;
      } else if (name === global) {
        // wtf?
        // merge(options, object);
      }
    }
    options.locals = locals
    return renderer(ext)(source, options);
  }

  // Collection support
  if (collection) {
    var len = collection.length
      , buf = ''
      , keys
      , key
      , val;

    if ('number' == typeof len || Array.isArray(collection)) {
      options.collectionLength = len;
      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();
  }
}