UNPKG

express-handlebars

Version:

A Handlebars view engine for Express which doesn't suck.

328 lines (257 loc) 9.89 kB
'use strict'; var Promise = global.Promise || require('promise'); var glob = require('glob'), Handlebars = require('handlebars'), fs = require('graceful-fs'), path = require('path'), semver = require('semver'), utils = require('./utils'); module.exports = ExpressHandlebars; // ----------------------------------------------------------------------------- function ExpressHandlebars(config) { config || (config = {}); this.handlebars = config.handlebars || this.handlebars; this.extname = config.extname || this.extname; this.layoutsDir = config.layoutsDir || this.layoutsDir; this.partialsDir = config.partialsDir || this.partialsDir; this.handlebarsVersion = ExpressHandlebars.getHandlebarsSemver(this.handlebars); if (this.extname.charAt(0) !== '.') { this.extname = '.' + this.extname; } this.defaultLayout = config.defaultLayout; this.helpers = config.helpers; this.compiled = {}; this.precompiled = {}; this.engine = this.renderView.bind(this); } ExpressHandlebars._fsCache = {}; ExpressHandlebars.getHandlebarsSemver = function (handlebars) { var version = handlebars.VERSION || ''; // Makes sure the Handlebars version is a valid semver. if (version && !semver.valid(version)) { version = version.replace(/(\d\.\d)\.(\D.*)/, '$1.0-$2'); } return version; }; ExpressHandlebars.prototype.handlebars = Handlebars; ExpressHandlebars.prototype.extname = '.handlebars'; ExpressHandlebars.prototype.layoutsDir = 'views/layouts/'; ExpressHandlebars.prototype.partialsDir = 'views/partials/'; ExpressHandlebars.prototype.compileTemplate = function (template, options) { options || (options = {}); var compiler = options.precompiled ? 'precompile' : 'compile', compile = this.handlebars[compiler]; return compile(template); }; ExpressHandlebars.prototype.getPartials = function (options) { options || (options = {}); var partialsDirs = Array.isArray(this.partialsDir) ? this.partialsDir : [this.partialsDir]; partialsDirs = partialsDirs.map(function (dir) { var dirPath, dirTemplates, dirNamespace; // Support `partialsDir` collection with object entries that contain a // templates promise and a namespace. if (typeof dir === 'string') { dirPath = dir; } else if (typeof dir === 'object') { dirTemplates = dir.templates; dirNamespace = dir.namespace; dirPath = dir.dir; } // We must have some path to templates, or templates themselves. if (!(dirPath || dirTemplates)) { throw new Error('A partials dir must be a string or config object'); } // Make sure we're have a promise for the templates. var templatesPromise = dirTemplates ? Promise.resolve(dirTemplates) : this.getTemplates(dirPath, options); return templatesPromise.then(function (templates) { return { templates: templates, namespace: dirNamespace }; }); }, this); return Promise.all(partialsDirs).then(function (dirs) { var getPartialName = this._getPartialName.bind(this); return dirs.reduce(function (partials, dir) { var templates = dir.templates, namespace = dir.namespace, filePaths = Object.keys(templates); filePaths.forEach(function (filePath) { var partialName = getPartialName(filePath, namespace); partials[partialName] = templates[filePath]; }); return partials; }, {}); }.bind(this)); }; ExpressHandlebars.prototype.getTemplate = function (filePath, options) { filePath = path.resolve(filePath); options || (options = {}); var precompiled = options.precompiled, cache = precompiled ? this.precompiled : this.compiled, template = options.cache && cache[filePath]; if (template) { return template; } // Optimistically cache template promise to reduce file system I/O, but // remove from cache if there was a problem. template = cache[filePath] = this._getFile(filePath, options) .then(function (file) { return this.compileTemplate(file, options); }.bind(this)); return template.catch(function (err) { delete cache[filePath]; throw err; }); }; ExpressHandlebars.prototype.getTemplates = function (dirPath, options) { options || (options = {}); return this._getDir(dirPath, options).then(function (filePaths) { var templates = filePaths.map(function (filePath) { return this.getTemplate(path.join(dirPath, filePath), options); }, this); return Promise.all(templates).then(function (templates) { return filePaths.reduce(function (map, filePath, i) { map[filePath] = templates[i]; return map; }, {}); }); }.bind(this)); }; ExpressHandlebars.prototype.render = function (filePath, context, options) { options || (options = {}); // Force `precompiled` to `false` since we're rendering to HTML. if (options.precompiled) { options = utils.extend({}, options, {precompiled: false}); } return Promise.all([ this.getTemplate(filePath, options), options.partials || this.getPartials(options) ]).then(function (templates) { var template = templates[0], partials = templates[1], data = options.data; var helpers = options.helpers || utils.extend({}, this.handlebars.helpers, this.helpers); return this._renderTemplate(template, context, { data : data, helpers : helpers, partials: partials }); }.bind(this)); }; ExpressHandlebars.prototype.renderView = function (viewPath, options, callback) { var context = options, data = options.data; var helpers = utils.extend({}, this.handlebars.helpers, this.helpers, options.helpers); // Pluck-out ExpressHandlebars-specific options. options = { cache : options.cache, layout : 'layout' in options ? options.layout : this.defaultLayout, precompiled: false }; // Extend `options` with Handlebars-specific rendering options. utils.extend(options, { data : data, helpers : helpers, partials: this.getPartials(options) }); this.render(viewPath, context, options) .then(function (body) { var layoutPath = this._resolveLayoutPath(options.layout); if (layoutPath) { context = utils.extend({}, context, {body: body}); return this.render(layoutPath, context, options); } return body; }.bind(this)) .then(utils.passValue(callback)) .catch(utils.passError(callback)); }; ExpressHandlebars.prototype._getDir = function (dirPath, options) { dirPath = path.resolve(dirPath); var cache = ExpressHandlebars._fsCache, dir = options.cache && cache[dirPath]; if (dir) { return dir.then(function (dir) { return dir.concat(); }); } var pattern = '**/*' + this.extname; // Optimistically cache dir promise to reduce file system I/O, but remove // from cache if there was a problem. dir = cache[dirPath] = new Promise(function (resolve, reject) { glob(pattern, {cwd: dirPath}, function (err, dir) { if (err) { reject(err); } else { resolve(dir); } }); }); return dir.then(function (dir) { return dir.concat(); }).catch(function (err) { delete cache[dirPath]; throw err; }); }; ExpressHandlebars.prototype._getFile = function (filePath, options) { filePath = path.resolve(filePath); var cache = ExpressHandlebars._fsCache, file = options.cache && cache[filePath]; if (file) { return file; } // Optimistically cache file promise to reduce file system I/O, but remove // from cache if there was a problem. file = cache[filePath] = new Promise(function (resolve, reject) { fs.readFile(filePath, 'utf8', function (err, file) { if (err) { reject(err); } else { resolve(file); } }); }); return file.catch(function (err) { delete cache[filePath]; throw err; }); }; ExpressHandlebars.prototype._getPartialName = function (filePath, namespace) { var extRegex = new RegExp(this.extname + '$'), name = filePath.replace(extRegex, ''), version = this.handlebarsVersion; if (namespace) { name = namespace + '/' + name; } // Fixes a Handlebars bug in versions prior to 1.0.rc.2 which caused // partials with "/"s in their name to not be found. // https://github.com/wycats/handlebars.js/pull/389 if (version && !semver.satisfies(version, '>=1.0.0-rc.2')) { name = name.replace(/\//g, '.'); } return name; }; ExpressHandlebars.prototype._renderTemplate = function (template, context, options) { return template(context, options); }; ExpressHandlebars.prototype._resolveLayoutPath = function (layoutPath) { if (!layoutPath) { return null; } if (!path.extname(layoutPath)) { layoutPath += this.extname; } if (layoutPath.charAt(0) !== '/') { layoutPath = path.join(this.layoutsDir, layoutPath); } return layoutPath; };