express-handlebars.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. /*
  2. * Copyright (c) 2015, Yahoo Inc. All rights reserved.
  3. * Copyrights licensed under the New BSD License.
  4. * See the accompanying LICENSE file for terms.
  5. */
  6. 'use strict';
  7. var Promise = global.Promise || require('promise');
  8. var glob = require('glob');
  9. var Handlebars = require('handlebars');
  10. var fs = require('graceful-fs');
  11. var path = require('path');
  12. var utils = require('./utils');
  13. module.exports = ExpressHandlebars;
  14. // -----------------------------------------------------------------------------
  15. function ExpressHandlebars(config) {
  16. // Config properties with defaults.
  17. utils.assign(this, {
  18. handlebars : Handlebars,
  19. extname : '.handlebars',
  20. layoutsDir : undefined, // Default layouts directory is relative to `express settings.view` + `layouts/`
  21. partialsDir : undefined, // Default partials directory is relative to `express settings.view` + `partials/`
  22. defaultLayout : undefined,
  23. helpers : undefined,
  24. compilerOptions: undefined,
  25. }, config);
  26. // Express view engine integration point.
  27. this.engine = this.renderView.bind(this);
  28. // Normalize `extname`.
  29. if (this.extname.charAt(0) !== '.') {
  30. this.extname = '.' + this.extname;
  31. }
  32. // Internal caches of compiled and precompiled templates.
  33. this.compiled = Object.create(null);
  34. this.precompiled = Object.create(null);
  35. // Private internal file system cache.
  36. this._fsCache = Object.create(null);
  37. }
  38. ExpressHandlebars.prototype.getPartials = function (options) {
  39. var partialsDirs = Array.isArray(this.partialsDir) ?
  40. this.partialsDir : [this.partialsDir];
  41. partialsDirs = partialsDirs.map(function (dir) {
  42. var dirPath;
  43. var dirTemplates;
  44. var dirNamespace;
  45. // Support `partialsDir` collection with object entries that contain a
  46. // templates promise and a namespace.
  47. if (typeof dir === 'string') {
  48. dirPath = dir;
  49. } else if (typeof dir === 'object') {
  50. dirTemplates = dir.templates;
  51. dirNamespace = dir.namespace;
  52. dirPath = dir.dir;
  53. }
  54. // We must have some path to templates, or templates themselves.
  55. if (!(dirPath || dirTemplates)) {
  56. throw new Error('A partials dir must be a string or config object');
  57. }
  58. // Make sure we're have a promise for the templates.
  59. var templatesPromise = dirTemplates ? Promise.resolve(dirTemplates) :
  60. this.getTemplates(dirPath, options);
  61. return templatesPromise.then(function (templates) {
  62. return {
  63. templates: templates,
  64. namespace: dirNamespace,
  65. };
  66. });
  67. }, this);
  68. return Promise.all(partialsDirs).then(function (dirs) {
  69. var getTemplateName = this._getTemplateName.bind(this);
  70. return dirs.reduce(function (partials, dir) {
  71. var templates = dir.templates;
  72. var namespace = dir.namespace;
  73. var filePaths = Object.keys(templates);
  74. filePaths.forEach(function (filePath) {
  75. var partialName = getTemplateName(filePath, namespace);
  76. partials[partialName] = templates[filePath];
  77. });
  78. return partials;
  79. }, {});
  80. }.bind(this));
  81. };
  82. ExpressHandlebars.prototype.getTemplate = function (filePath, options) {
  83. filePath = path.resolve(filePath);
  84. options || (options = {});
  85. var precompiled = options.precompiled;
  86. var cache = precompiled ? this.precompiled : this.compiled;
  87. var template = options.cache && cache[filePath];
  88. if (template) {
  89. return template;
  90. }
  91. // Optimistically cache template promise to reduce file system I/O, but
  92. // remove from cache if there was a problem.
  93. template = cache[filePath] = this._getFile(filePath, {cache: options.cache})
  94. .then(function (file) {
  95. if (precompiled) {
  96. return this._precompileTemplate(file, this.compilerOptions);
  97. }
  98. return this._compileTemplate(file, this.compilerOptions);
  99. }.bind(this));
  100. return template.catch(function (err) {
  101. delete cache[filePath];
  102. throw err;
  103. });
  104. };
  105. ExpressHandlebars.prototype.getTemplates = function (dirPath, options) {
  106. options || (options = {});
  107. var cache = options.cache;
  108. return this._getDir(dirPath, {cache: cache}).then(function (filePaths) {
  109. var templates = filePaths.map(function (filePath) {
  110. return this.getTemplate(path.join(dirPath, filePath), options);
  111. }, this);
  112. return Promise.all(templates).then(function (templates) {
  113. return filePaths.reduce(function (hash, filePath, i) {
  114. hash[filePath] = templates[i];
  115. return hash;
  116. }, {});
  117. });
  118. }.bind(this));
  119. };
  120. ExpressHandlebars.prototype.render = function (filePath, context, options) {
  121. options || (options = {});
  122. return Promise.all([
  123. this.getTemplate(filePath, {cache: options.cache}),
  124. options.partials || this.getPartials({cache: options.cache}),
  125. ]).then(function (templates) {
  126. var template = templates[0];
  127. var partials = templates[1];
  128. var helpers = options.helpers || this.helpers;
  129. // Add ExpressHandlebars metadata to the data channel so that it's
  130. // accessible within the templates and helpers, namespaced under:
  131. // `@exphbs.*`
  132. var data = utils.assign({}, options.data, {
  133. exphbs: utils.assign({}, options, {
  134. filePath: filePath,
  135. helpers : helpers,
  136. partials: partials,
  137. }),
  138. });
  139. return this._renderTemplate(template, context, {
  140. data : data,
  141. helpers : helpers,
  142. partials: partials,
  143. });
  144. }.bind(this));
  145. };
  146. ExpressHandlebars.prototype.renderView = function (viewPath, options, callback) {
  147. options || (options = {});
  148. var context = options;
  149. // Express provides `settings.views` which is the path to the views dir that
  150. // the developer set on the Express app. When this value exists, it's used
  151. // to compute the view's name. Layouts and Partials directories are relative
  152. // to `settings.view` path
  153. var view;
  154. var viewsPath = options.settings && options.settings.views;
  155. if (viewsPath) {
  156. view = this._getTemplateName(path.relative(viewsPath, viewPath));
  157. this.partialsDir = this.partialsDir || path.join(viewsPath, 'partials/');
  158. this.layoutsDir = this.layoutsDir || path.join(viewsPath, 'layouts/');
  159. }
  160. // Merge render-level and instance-level helpers together.
  161. var helpers = utils.assign({}, this.helpers, options.helpers);
  162. // Merge render-level and instance-level partials together.
  163. var partials = Promise.all([
  164. this.getPartials({cache: options.cache}),
  165. Promise.resolve(options.partials),
  166. ]).then(function (partials) {
  167. return utils.assign.apply(null, [{}].concat(partials));
  168. });
  169. // Pluck-out ExpressHandlebars-specific options and Handlebars-specific
  170. // rendering options.
  171. options = {
  172. cache : options.cache,
  173. view : view,
  174. layout: 'layout' in options ? options.layout : this.defaultLayout,
  175. data : options.data,
  176. helpers : helpers,
  177. partials: partials,
  178. };
  179. this.render(viewPath, context, options)
  180. .then(function (body) {
  181. var layoutPath = this._resolveLayoutPath(options.layout);
  182. if (layoutPath) {
  183. return this.render(
  184. layoutPath,
  185. utils.assign({}, context, {body: body}),
  186. utils.assign({}, options, {layout: undefined})
  187. );
  188. }
  189. return body;
  190. }.bind(this))
  191. .then(utils.passValue(callback))
  192. .catch(utils.passError(callback));
  193. };
  194. // -- Protected Hooks ----------------------------------------------------------
  195. ExpressHandlebars.prototype._compileTemplate = function (template, options) {
  196. return this.handlebars.compile(template.trim(), options);
  197. };
  198. ExpressHandlebars.prototype._precompileTemplate = function (template, options) {
  199. return this.handlebars.precompile(template, options);
  200. };
  201. ExpressHandlebars.prototype._renderTemplate = function (template, context, options) {
  202. return template(context, options).trim();
  203. };
  204. // -- Private ------------------------------------------------------------------
  205. ExpressHandlebars.prototype._getDir = function (dirPath, options) {
  206. dirPath = path.resolve(dirPath);
  207. options || (options = {});
  208. var cache = this._fsCache;
  209. var dir = options.cache && cache[dirPath];
  210. if (dir) {
  211. return dir.then(function (dir) {
  212. return dir.concat();
  213. });
  214. }
  215. var pattern = '**/*' + this.extname;
  216. // Optimistically cache dir promise to reduce file system I/O, but remove
  217. // from cache if there was a problem.
  218. dir = cache[dirPath] = new Promise(function (resolve, reject) {
  219. glob(pattern, {
  220. cwd : dirPath,
  221. follow: true
  222. }, function (err, dir) {
  223. if (err) {
  224. reject(err);
  225. } else {
  226. resolve(dir);
  227. }
  228. });
  229. });
  230. return dir.then(function (dir) {
  231. return dir.concat();
  232. }).catch(function (err) {
  233. delete cache[dirPath];
  234. throw err;
  235. });
  236. };
  237. ExpressHandlebars.prototype._getFile = function (filePath, options) {
  238. filePath = path.resolve(filePath);
  239. options || (options = {});
  240. var cache = this._fsCache;
  241. var file = options.cache && cache[filePath];
  242. if (file) {
  243. return file;
  244. }
  245. // Optimistically cache file promise to reduce file system I/O, but remove
  246. // from cache if there was a problem.
  247. file = cache[filePath] = new Promise(function (resolve, reject) {
  248. fs.readFile(filePath, 'utf8', function (err, file) {
  249. if (err) {
  250. reject(err);
  251. } else {
  252. resolve(file);
  253. }
  254. });
  255. });
  256. return file.catch(function (err) {
  257. delete cache[filePath];
  258. throw err;
  259. });
  260. };
  261. ExpressHandlebars.prototype._getTemplateName = function (filePath, namespace) {
  262. var extRegex = new RegExp(this.extname + '$');
  263. var name = filePath.replace(extRegex, '');
  264. if (namespace) {
  265. name = namespace + '/' + name;
  266. }
  267. return name;
  268. };
  269. ExpressHandlebars.prototype._resolveLayoutPath = function (layoutPath) {
  270. if (!layoutPath) {
  271. return null;
  272. }
  273. if (!path.extname(layoutPath)) {
  274. layoutPath += this.extname;
  275. }
  276. return path.resolve(this.layoutsDir, layoutPath);
  277. };