templates
Version:
System for creating and managing template collections, and rendering templates with any node.js template engine. Can be used as the basis for creating a static site generator or blog framework.
1,218 lines (1,054 loc) • 32.1 kB
JavaScript
/*!
* templates <https://github.com/jonschlinkert/templates>
*
* Copyright (c) 2015, Jon Schlinkert.
* Licensed under the MIT License.
*/
'use strict';
var path = require('path');
var Base = require('base-methods');
var helpers = require('./lib/helpers/');
var utils = require('./lib/utils');
var Views = require('./lib/views');
var List = require('./lib/list');
var View = require('./lib/view');
var lib = require('./lib/');
/**
* This function is the main export of the templates module.
* Initialize an instance of `templates` to create your
* application.
*
* ```js
* var templates = require('templates');
* var app = templates();
* ```
* @param {Object} `options`
* @api public
*/
function Templates(options) {
if (!(this instanceof Templates)) {
return new Templates(options);
}
Base.call(this);
this.options = options || {};
this.define('plugins', []);
utils.renameKey(this);
this.defaultConfig();
}
/**
* `Templates` prototype methods
*/
Base.extend(Templates, {
constructor: Templates,
defaultConfig: function () {
// used in plugins to verify the app instance
this.isApp = true;
// decorate `option` method onto instance
utils.option(this);
for (var key in this.options.mixins) {
this.mixin(key, this.options.mixins[key]);
}
this.define('_', {});
this.engines = {};
lib.helpers(this);
this._.engines = new utils.Engines(this.engines);
this.engine('default', utils.engine);
this.define('errors', {
compile: {
callback: 'is sync and does not take a callback function',
engine: 'cannot find an engine for: ',
method: 'expects engines to have a compile method',
},
render: {
callback: 'is async and expects a callback function',
engine: 'cannot find an engine for: ',
method: 'expects engines to have a render method',
},
layouts: {
registered: 'no layouts are registered, but one is defined: '
}
});
this.cache = {};
this.cache.data = {};
this.cache.context = {};
this.items = {};
this.views = {};
this.viewTypes = {
layout: [],
renderable: [],
partial: []
};
this.inflections = {};
this.listen();
},
/**
* Initialize defaults. Exposes constructors on
* app instance.
*/
initialize: function () {
this.define('Base', Base);
this.define('View', this.options.View || View);
this.define('List', this.options.List || List);
this.define('Views', this.options.Views || Views);
this.define('initialized', true);
},
/**
* Listen for events
*/
listen: function () {
this.on('option', function (key, value) {
if (key === 'mixins') {
this.visit('mixin', value);
}
});
this.on('error', function (err) {
if (err && err.id === 'rethrow') {
console.error(err.reason);
}
});
},
/**
* Run a plugin on the instance. Plugins
* are invoked immediately upon creating the collection
* in the order in which they were defined.
*
* ```js
* var app = assemble()
* .use(require('foo'))
* .use(require('bar'))
* .use(require('baz'))
* ```
*
* @name .use
* @param {Function} `fn` Plugin function. If the plugin returns a function it will be passed to the `use` method of each collection created on the instance.
* @return {Object} Returns the instance for chaining.
* @api public
*/
use: function (fn) {
var plugin = fn.call(this, this, this.options);
if (typeof plugin === 'function') {
this.plugins.push(plugin);
}
return this;
},
/**
* Set, get and load data to be passed to templates as
* context at render-time.
*
* ```js
* app.data('a', 'b');
* app.data({c: 'd'});
* console.log(app.cache.data);
* //=> {a: 'b', c: 'd'}
* ```
*
* @name .data
* @param {String|Object} `key` Pass a key-value pair or an object to set.
* @param {any} `val` Any value when a key-value pair is passed. This can also be options if a glob pattern is passed as the first value.
* @return {Object} Returns an instance of `Templates` for chaining.
* @api public
*/
data: function (key, val) {
if (utils.isObject(key)) {
this.visit('data', key);
return this;
}
var isGlob = typeof val === 'undefined' || utils.hasGlob(key);
if (utils.isValidGlob(key) && isGlob) {
var opts = utils.extend({}, this.options, val);
var data = utils.requireData(key, opts);
if (data) this.visit('data', data);
return this;
}
key = 'cache.data.' + key;
this.set(key, val);
return this;
},
/**
* Create a new view collection. View collections are decorated
* with special methods for getting, setting and rendering
* views from that collection. Collections created with this method
* are not stored on `app.views` as with the [create](#create) method.
*
* ```js
* var collection = app.collection();
* collection.addViews({...}); // add an object of views
* collection.addView('foo', {content: '...'}); // add a single view
*
* // collection methods are chainable too
* collection.addView('home.hbs', {content: 'foo <%= title %> bar'})
* .render({title: 'Home'}, function(err, res) {
* //=> 'foo Home bar'
* });
* ```
* @name .collection
* @param {Object} `opts` Collection options
* @return {Object} Returns the `collection` instance for chaining.
* @api public
*/
collection: function (opts) {
if (!this.initialized) this.initialize();
var Views = this.get('Views');
var collection;
if (opts instanceof Views) {
collection = opts;
opts = this.options;
} else {
opts = opts || {};
opts.View = opts.View || this.get('View');
collection = new Views(opts);
}
// emit the collection
this.emit('collection', collection, opts);
this.extendViews(collection, opts);
return collection;
},
/**
* Create a new view collection that is stored on the `app.views` object. For example, if you create a collection named `posts`:
*
* - all `posts` will be stored on `app.views.posts`
* - a `post` method will be added to `app`, allowing you to add a single view to the `posts` collection using `app.post()` (equivalent to `collection.addView()`)
* - a `posts` method will be added to `app`, allowing you to add views to the `posts` collection using `app.posts()` (equivalent to `collection.addViews()`)
*
* ```js
* app.create('posts');
* app.posts({...}); // add an object of views
* app.post('foo', {content: '...'}); // add a single view
*
* // collection methods are chainable too
* app.post('home.hbs', {content: 'foo <%= title %> bar'})
* .render({title: 'Home'}, function(err, res) {
* //=> 'foo Home bar'
* });
* ```
* @name .create
* @param {String} `name` The name of the collection. Plural or singular form.
* @param {Object} `opts` Collection options
* @param {String|Array|Function} `loaders` Loaders to use for adding views to the created collection.
* @return {Object} Returns the `collection` instance for chaining.
* @api public
*/
create: function(name, opts) {
opts = opts || {};
if (!opts.views && !opts.options) {
opts = utils.merge({}, this.options, opts);
}
var collection = this.collection(opts);
// get the collection inflections, e.g. page/pages
var single = utils.single(name);
var plural = utils.plural(name);
// map the inflections for lookups
this.inflections[single] = plural;
// add inflections to collection options
collection.option('inflection', single);
collection.option('plural', plural);
// prime the viewType(s) for the collection
this.viewType(plural, collection.viewType());
// add the collection to `app.views`
this.views[plural] = collection.views;
// create loader functions for adding views to this collection
this.define(plural, collection.addViews.bind(collection));
this.define(single, collection.addView.bind(collection));
// decorate loader methods with collection methods
this[plural].__proto__ = collection;
this[single].__proto__ = collection;
// create aliases on the collection for
// addView/addViews to support chaining
collection.define(plural, this[plural]);
collection.define(single, this[single]);
// run collection plugins
this.plugins.forEach(function (fn) {
collection.use(fn, opts);
}.bind(this));
// emit create
this.emit('create', collection, opts);
// add collection and view helpers
helpers.plural(this, this[plural], opts);
helpers.single(this, this[single], opts);
return collection;
},
/**
* Decorate `view` instances in the collection.
*/
extendView: function (view, options) {
var opts = utils.merge({}, this.options, options);
var app = this;
// decorate `option` method onto `view`
utils.option(view);
// decorate `compile` method onto `view`
view.compile = function () {
var args = [this].concat([].slice.call(arguments));
app.compile.apply(app, args);
return this;
};
// decorate `render` method onto `view`
view.render = function () {
var args = [this].concat([].slice.call(arguments));
app.render.apply(app, args);
return this;
};
// decorate `context` method onto `view`
view.context = function(locals) {
return utils.merge({}, this.locals, this.data, locals);
};
},
/**
* Decorate `collection` intances.
*/
extendViews: function (collection, options) {
var opts = utils.merge({}, this.options, options);
var app = this;
if (!collection.options.hasOwnProperty('renameKey')) {
collection.option('renameKey', this.renameKey);
}
collection.on('view', function (view) {
utils.define(view, 'addView', collection.addView.bind(collection));
app.extendView(view, opts);
app.handleView('onLoad', view);
app.emit('view', view);
});
},
/**
* Set view types for a collection.
*
* @param {String} `plural` e.g. `pages`
* @param {Object} `options`
*/
viewType: function(plural, types) {
var len = types.length, i = 0;
while (len--) {
var type = types[i++];
this.viewTypes[type] = this.viewTypes[type] || [];
if (this.viewTypes[type].indexOf(plural) === -1) {
this.viewTypes[type].push(plural);
}
}
return types;
},
/**
* Find a view by `name`, optionally passing a `collection` to limit
* the search. If no collection is passed all `renderable` collections
* will be searched.
*
* ```js
* var page = app.find('my-page.hbs');
*
* // optionally pass a collection name as the second argument
* var page = app.find('my-page.hbs', 'pages');
* ```
* @name .find
* @param {String} `name` The name/key of the view to find
* @param {String} `colleciton` Optionally pass a collection name (e.g. pages)
* @return {Object|undefined} Returns the view if found, or `undefined` if not.
* @api public
*/
find: function (name, collection) {
if (typeof name !== 'string') {
throw new TypeError('expected name to be a string.');
}
if (typeof collection === 'string') {
return this[collection].getView(name);
}
var collections = this.viewTypes.renderable;
var len = collections.length, i = 0;
while (len--) {
var plural = collections[i++];
var views = this.views[plural];
var res;
if (res = views[name]) {
return res;
}
}
},
/**
* Get view `key` from the specified `collection`.
*
* ```js
* var view = app.getView('pages', 'a/b/c.hbs');
*
* // optionally pass a `renameKey` function to modify the lookup
* var view = app.getView('pages', 'a/b/c.hbs', function(fp) {
* return path.basename(fp);
* });
* ```
* @name .getView
* @param {String} `collection` Collection name, e.g. `pages`
* @param {String} `key` Template name
* @param {Function} `fn` Optionally pass a `renameKey` function
* @return {Object}
* @api public
*/
getView: function(collection, key, fn) {
var views = this.getViews(collection);
// use custom renameKey function
if (typeof fn === 'function') {
key = fn(key);
}
if (views.hasOwnProperty(key)) {
return views[key];
}
// try again with the default renameKey function
fn = this.option('renameKey');
var name;
if (typeof fn === 'function') {
name = fn(key);
}
if (name && name !== key && views.hasOwnProperty(name)) {
return views[name];
}
return null;
},
/**
* Get all views from a `collection` using the collection's
* singular or plural name.
*
* ```js
* var pages = app.getViews('pages');
* //=> { pages: {'home.hbs': { ... }}
*
* var posts = app.getViews('posts');
* //=> { posts: {'2015-10-10.md': { ... }}
* ```
*
* @name .getViews
* @param {String} `name` The collection name, e.g. `pages` or `page`
* @return {Object}
* @api public
*/
getViews: function(name) {
var orig = name;
if (utils.isObject(name)) return name;
if (!this.views.hasOwnProperty(name)) {
name = this.inflections[name];
}
if (!this.views.hasOwnProperty(name)) {
throw new Error('getViews cannot find collection: ' + orig);
}
return this.views[name];
},
/**
* Returns the first view from `collection` with a key
* that matches the given glob pattern.
*
* ```js
* var pages = app.matchView('pages', 'home.*');
* //=> {'home.hbs': { ... }, ...}
*
* var posts = app.matchView('posts', '2010-*');
* //=> {'2015-10-10.md': { ... }, ...}
* ```
*
* @name .matchView
* @param {String} `collection` Collection name.
* @param {String} `pattern` glob pattern
* @param {Object} `options` options to pass to [micromatch][]
* @return {Object}
* @api public
*/
matchView: function(collection, pattern, options) {
var views = this.getViews(collection);
if (views.hasOwnProperty(pattern)) {
return views[pattern];
}
return utils.matchKey(views, pattern, options);
},
/**
* Returns any views from the specified collection with keys
* that match the given glob pattern.
*
* ```js
* var pages = app.matchViews('pages', 'home.*');
* //=> {'home.hbs': { ... }, ...}
*
* var posts = app.matchViews('posts', '2010-*');
* //=> {'2015-10-10.md': { ... }, ...}
* ```
*
* @name .matchViews
* @param {String} `collection` Collection name.
* @param {String} `pattern` glob pattern
* @param {Object} `options` options to pass to [micromatch]
* @return {Object}
* @api public
*/
matchViews: function(collection, pattern, options) {
var views = this.getViews(collection);
return utils.matchKeys(views, pattern, options);
},
/**
* Add `Router` and `Route` to the prototype
*/
Router: utils.router.Router,
Route: utils.router.Route,
/**
* Lazily initalize `router`, to allow options to
* be passed in after init.
*/
lazyRouter: function(methods) {
if (typeof this.router === 'undefined') {
this.define('router', new this.Router({
methods: utils.methods
}));
}
if (typeof methods !== 'undefined') {
this.router.method(methods);
}
},
/**
* Handle a middleware `method` for `view`.
*
* ```js
* app.handle('customMethod', view, callback);
* ```
* @name .handle
* @param {String} `method` Name of the router method to handle. See [router methods](./docs/router.md)
* @param {Object} `view` View object
* @param {Function} `callback` Callback function
* @return {Object}
* @api public
*/
handle: function (method, view, locals, cb) {
if (typeof locals === 'function') {
cb = locals;
locals = {};
}
this.lazyRouter();
if (!view.options.handled) {
view.options.handled = [];
}
if (typeof cb !== 'function') {
cb = this.handleError(method, view);
}
view.options.method = method;
view.options.handled.push(method);
if (view.emit) {
view.emit('handle', method);
}
this.router.handle(view, this.handleError(method, view, cb));
return this;
},
/**
* Run the given middleware handler only if the view has not
* already been handled by the method.
*
* @name .handleView
* @param {Object} `method`
* @param {Object} `view`
* @param {Object} `locals`
*/
handleView: function (method, view, locals/*, cb*/) {
view.options = view.options || {};
if (!view.options.handled) {
view.options.handled = [];
}
if (view.options.handled.indexOf(method) < 0) {
this.handle.apply(this, arguments);
this.emit(method, view, locals);
}
return this;
},
/**
* Handle middleware errors.
*/
handleError: function(method, view, cb) {
if (typeof cb !== 'function') cb = utils.noop;
var app = this;
return function (err) {
if (err) {
if (err._handled) return cb();
err.reason = 'Templates#handle' + method + ': ' + view.path;
err._handled = true;
app.emit('error', err);
return cb(err);
}
cb(null, view);
};
},
/**
* Create a new Route for the given path. Each route contains
* a separate middleware stack.
*
* See the [route API documentation][route-api] for details on
* adding handlers and middleware to routes.
*
* ```js
* app.create('posts');
* app.route(/blog/)
* .all(function(view, next) {
* // do something with view
* next();
* });
*
* app.post('whatever', {path: 'blog/foo.bar', content: 'bar baz'});
* ```
* @name .route
* @param {String} `path`
* @return {Object} `Route` for chaining
* @api public
*/
route: function(path) {
this.lazyRouter();
return this.router.route.apply(this.router, arguments);
},
/**
* Special route method that works just like the `router.METHOD()`
* methods, except that it matches all verbs.
*
* ```js
* app.all(/\.hbs$/, function(view, next) {
* // do stuff to view
* next();
* });
* ```
* @name .all
* @param {String} `path`
* @param {Function} `callback`
* @return {Object} `this` for chaining
* @api public
*/
all: function(path/*, callback*/) {
var route = this.route(path);
route.all.apply(route, [].slice.call(arguments, 1));
return this;
},
/**
* Add callback triggers to route parameters, where
* `name` is the name of the parameter and `fn` is the
* callback function.
*
* ```js
* app.param('title', function (view, next, title) {
* //=> title === 'foo.js'
* next();
* });
*
* app.onLoad('/blog/:title', function (view, next) {
* //=> view.path === '/blog/foo.js'
* next();
* });
* ```
* @name .param
* @param {String} `name`
* @param {Function} `fn`
* @return {Object} Returns the instance of `Templates` for chaining.
* @api public
*/
param: function(name/*, fn*/) {
this.lazyRouter();
this.router.param.apply(this.router, arguments);
return this;
},
/**
* Register a view engine callback `fn` as `ext`.
*
* ```js
* app.engine('hbs', require('engine-handlebars'));
*
* // using consolidate.js
* var engine = require('consolidate');
* app.engine('jade', engine.jade);
* app.engine('swig', engine.swig);
*
* // get a registered engine
* var swig = app.engine('swig');
* ```
* @name .engine
* @param {String|Array} `exts` String or array of file extensions.
* @param {Function|Object} `fn` or `settings`
* @param {Object} `settings` Optionally pass engine options as the last argument.
* @api public
*/
engine: function(exts, fn, settings) {
if (arguments.length === 1 && typeof exts === 'string') {
return this.getEngine(exts);
}
if (!Array.isArray(exts) && typeof exts !== 'string') {
throw new TypeError('expected engine ext to be a string or array.');
}
utils.arrayify(exts).forEach(function (ext) {
this.setEngine(ext, fn, settings);
}.bind(this));
return this;
},
/**
* Register an engine for `ext` with the given `settings`
*
* @param {String} `ext` The engine to get.
*/
setEngine: function(ext, fn, settings) {
ext = utils.formatExt(ext);
this._.engines.setEngine(ext, fn, settings);
return this;
},
/**
* Get the engine settings registered for the given `ext`.
*
* @param {String} `ext` The engine to get.
*/
getEngine: function(ext) {
ext = ext ? utils.formatExt(ext) : null;
var engine = this._.engines.getEngine(ext);
if (engine) {
return engine;
}
ext = this.option('view engine');
return this._.engines.getEngine(ext);
},
/**
* Apply a layout to the given `view`.
*
* @name .applyLayout
* @param {Object} `view`
* @return {Object} Returns a `view` object.
*/
applyLayout: function(view, locals) {
if (view.options.layoutApplied) {
return view;
}
// handle pre-layout middleware
this.handle('preLayout', view);
// get the layout stack
var stack = {}, registered = 0;
var alias = this.viewTypes.layout;
var len = alias.length, i = 0;
while (len--) {
var views = this.views[alias[i++]];
for (var key in views) {
stack[key] = views[key];
registered++;
}
}
// get the name of the first layout
var self = this;
var name = view.layout;
var str = view.content;
// if no layout is defined, move on
if (typeof name === 'undefined') {
return view;
}
if (registered === 0 || !stack.hasOwnProperty(name)) {
throw this.error('layouts', 'registered', name);
}
var opts = {};
utils.extend(opts, this.options);
utils.extend(opts, view.options);
utils.extend(opts, view.context());
// Handle each layout before it's applied to a view
function handleLayout(obj, stats/*, depth*/) {
view.currentLayout = obj.layout;
view.define('layoutStack', stats.history);
self.handle('onLayout', view);
delete view.currentLayout;
}
// actually apply the layout
var res = utils.layouts(str, name, stack, opts, handleLayout);
if (res.result === str) {
throw new Error('layout was not applied to: ' + view.path);
}
view.option('layoutApplied', true);
view.option('layoutStack', res.history);
view.contents = new Buffer(res.result);
// handle post-layout middleware
this.handle('postLayout', view);
return view;
},
/**
* Compile `content` with the given `locals`.
*
* ```js
* var indexPage = app.page('some-index-page.hbs');
* var view = app.compile(indexPage);
* // view.fn => [function]
*
* // you can call the compiled function more than once
* // to render the view with different data
* view.fn({title: 'Foo'});
* view.fn({title: 'Bar'});
* view.fn({title: 'Baz'});
* ```
*
* @name .compile
* @param {Object|String} `view` View object.
* @param {Object} `locals`
* @param {Boolean} `isAsync` Load async helpers
* @return {Object} View object with `fn` property with the compiled function.
* @api public
*/
compile: function(view, locals, isAsync) {
if (typeof locals === 'boolean') {
isAsync = locals;
locals = {};
}
if (typeof locals === 'function' || typeof isAsync === 'function') {
throw this.error('compile', 'callback');
}
// get the engine to use
locals = utils.merge({settings: {}}, locals);
var ext = locals.engine
|| view.engine
|| view.ext
|| (view.ext = path.extname(view.path));
var engine = this.getEngine(ext);
if (typeof engine === 'undefined') {
throw this.error('compile', 'engine', view.ext);
}
if (engine && engine.options) {
locals.settings = utils.merge({}, locals.settings, engine.options);
}
var ctx = view.context(locals);
// apply layout
view = this.applyLayout(view, ctx);
// handle `preCompile` middleware
this.handleView('preCompile', view, locals);
// Bind context to helpers before passing to the engine.
this.bindHelpers(view, locals, ctx, (locals.async = isAsync));
// shallow clone the context and locals
var settings = utils.extend({}, ctx, locals);
// compile the string
var str = view.contents.toString();
view.fn = engine.compile(str, settings);
// handle `postCompile` middleware
this.handleView('postCompile', view, locals);
return view;
},
/**
* Render a view with the given `locals` and `callback`.
*
* ```js
* var blogPost = app.post.getView('2015-09-01-foo-bar');
* app.render(blogPost, {title: 'Foo'}, function(err, view) {
* // `view` is an object with a rendered `content` property
* });
* ```
* @name .render
* @param {Object|String} `view` Instance of `View`
* @param {Object} `locals` Locals to pass to template engine.
* @param {Function} `callback`
* @api public
*/
render: function (view, locals, cb) {
if (typeof locals === 'function') {
cb = locals;
locals = {};
}
if (typeof cb !== 'function') {
throw this.error('render', 'callback');
}
// if `view` is a function, it's probably from chaining
// a collection method
if (typeof view === 'function') {
return view.call(this);
}
// if `view` is a string, see if it's a cached view
if (typeof view === 'string') {
view = this.find(view);
}
view.locals = utils.merge({}, view.locals, locals);
locals = utils.merge({}, this.cache.data, view.locals);
// handle `preRender` middleware
this.handleView('preRender', view, locals);
// get the engine
var extname = view.ext || (view.ext = path.extname(view.path));
var ext = locals.engines || view.engine || extname;
var engine = this.getEngine(ext);
if (!engine) {
return cb(this.error('render', 'engine', ext));
}
var isAsync = typeof cb === 'function';
// if it's not already compiled, do that first
if (typeof view.fn !== 'function') {
try {
view = this.compile(view, locals, isAsync);
return this.render(view, locals, cb);
} catch(err) {
this.emit('error', err);
return cb.call(this, err);
}
}
var opts = this.options;
var ctx = view.context(locals);
var context = this.context(view, ctx, locals);
// render the view
return engine.render(view.fn, context, function (err, res) {
if (err) {
if (opts.rethrow !== false) {
err = this.rethrow('render', err, view, context);
}
this.emit('error', err);
return cb.call(this, err);
}
view.contents = res;
// handle `postRender` middleware
this.handle('postRender', view, locals, cb);
}.bind(this));
},
/**
* Merge "partials" view types. This is necessary for template
* engines that only support one class of partials.
*
* @name .mergePartials
* @param {Object} `locals`
* @param {Array} `viewTypes` Optionally pass an array of viewTypes to include.
* @return {Object} Merged partials
*/
mergePartials: function (locals, viewTypes) {
var names = viewTypes || this.viewTypes.partial;
var opts = utils.extend({}, this.options, locals);
var partials = {};
var self = this;
names.forEach(function (name) {
var collection = self.views[name];
for (var key in collection) {
var view = collection[key];
// handle `onMerge` middleware
self.handleView('onMerge', view, locals);
if (view.options.nomerge) return;
if (opts.mergePartials !== false) {
name = 'partials';
}
// convert the partial to:
//=> {'foo.hbs': 'some content...'};
partials[name] = partials[name] || {};
partials[name][key] = view.content;
}
});
return partials;
},
/**
* Build the context for the given `view` and `locals`.
*
* @name .context
* @param {Object} `view` Templates object
* @param {Object} `locals`
* @return {Object} The object to be passed to engines/views as context.
*/
context: function (view, ctx, locals) {
var obj = {};
utils.extend(obj, ctx);
utils.extend(obj, this.cache.data);
utils.extend(obj, view.locals);
utils.extend(obj, view.data);
utils.extend(obj, locals);
obj.view = view;
return obj;
},
/**
* Bind context to helpers.
*/
bindHelpers: function (view, locals, context, isAsync) {
var helpers = {};
utils.extend(helpers, this.options.helpers);
utils.extend(helpers, this._.helpers.sync);
if (isAsync) utils.extend(helpers, this._.helpers.async);
utils.extend(helpers, locals.helpers);
// build the context to expose as `this` in helpers
var thisArg = {};
thisArg.options = utils.extend({}, this.options, locals);
thisArg.context = context || {};
thisArg.context.view = view;
thisArg.app = this;
// bind template helpers to the instance
locals.helpers = utils.bindAll(helpers, thisArg);
},
/**
* Add a router handler.
*
* @param {String} `method` Method name.
*/
handler: function (methods) {
this.handlers(methods);
},
/**
* Add default Router handlers to Templates.
*/
handlers: function (methods) {
this.lazyRouter(methods);
methods.forEach(function (method) {
this.define(method, function(path) {
var route = this.route(path);
var args = [].slice.call(arguments, 1);
route[method].apply(route, args);
return this;
}.bind(this));
}.bind(this));
},
/**
* Format an error
*
* TODO:
* - add engine info to error
*/
error: function(method, id, msg) {
var ctx = this.errors[method][id];
var reason = 'Templates#' + method + ' ' + ctx + (msg || '');
var err = new Error(reason);
err.reason = reason;
err.id = id;
err.msg = msg;
return err;
},
/**
* Rethrow an error in the given context to
* get better error messages.
*/
rethrow: function(method, err, view, context) {
try {
utils.rethrow(view.contents.toString(), {
data: context,
fp: view.path
});
} catch (msg) {
err.method = method;
err.reason = msg;
err.id = 'rethrow';
err._called = true;
return err;
}
},
/**
* Mix in a prototype method
*/
mixin: function(key, value) {
Templates.prototype[key] = value;
}
});
// Add router methods to Templates
utils.methods.forEach(function (method) {
Templates.prototype[method] = function(path) {
var route = this.route(path);
var args = [].slice.call(arguments, 1);
route[method].apply(route, args);
return this;
};
});
/**
* Returns a new view, using the `View` class
* currently defined on the instance.
*
* ```js
* var view = app.view('foo', {conetent: '...'});
* // or
* var view = app.view({path: 'foo', conetent: '...'});
* ```
* @name .view
* @param {String|Object} `key` View key or object
* @param {Object} `value` If key is a string, value is the view object.
* @return {Object} returns the `view` object
* @api public
*/
utils.viewFactory(Templates.prototype, 'view', 'View');
/**
* Expose `Templates`
*/
module.exports = Templates;
/**
* Expose constructors
*/
module.exports.Group = require('./lib/group');
module.exports.View = View;
module.exports.List = List;
module.exports.Views = Views;
/**
* Expose utils
*/
module.exports.utils = utils;