UNPKG

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