index.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. var fs = require('fs');
  2. var path = require('path');
  3. var glob = require('glob');
  4. /**
  5. * Shallow copy two objects into a new object
  6. *
  7. * Objects are merged from left to right. Thus, properties in objects further
  8. * to the right are preferred over those on the left.
  9. *
  10. * @param {object} obj1
  11. * @param {object} obj2
  12. * @returns {object}
  13. * @api private
  14. */
  15. var merge = function (obj1, obj2) {
  16. var c = {};
  17. var keys = Object.keys(obj2);
  18. for(var i=0; i!==keys.length; i++) {
  19. c[keys[i]] = obj2[keys[i]];
  20. }
  21. keys = Object.keys(obj1);
  22. for(i=0; i!==keys.length; i++) {
  23. if (!c.hasOwnProperty(keys[i])) {
  24. c[keys[i]] = obj1[keys[i]];
  25. }
  26. }
  27. return c;
  28. };
  29. /* Capture the layout name; thanks express-hbs */
  30. var rLayoutPattern = /{{!<\s+([A-Za-z0-9\._\-\/]+)\s*}}/;
  31. /**
  32. * file reader returning a thunk
  33. * @param filename {String} Name of file to read
  34. */
  35. var read = function (filename) {
  36. return function(done) {
  37. fs.readFile(filename, {encoding: 'utf8'}, done);
  38. };
  39. };
  40. /**
  41. * expose default instance of `Hbs`
  42. */
  43. exports = module.exports = new Hbs();
  44. /**
  45. * expose method to create additional instances of `Hbs`
  46. */
  47. exports.create = function() {
  48. return new Hbs();
  49. };
  50. /**
  51. * Create new instance of `Hbs`
  52. *
  53. * @api public
  54. */
  55. function Hbs() {
  56. if(!(this instanceof Hbs)) return new Hbs();
  57. this.handlebars = require('handlebars').create();
  58. this.Utils = this.handlebars.Utils;
  59. this.SafeString = this.handlebars.SafeString;
  60. }
  61. /**
  62. * Configure the instance.
  63. *
  64. * @api private
  65. */
  66. Hbs.prototype.configure = function (options) {
  67. var self = this;
  68. if(!options.viewPath) throw new Error("must specify view path");
  69. // Attach options
  70. var options = options || {};
  71. this.viewPath = options.viewPath;
  72. this.handlebars = options.handlebars || this.handlebars;
  73. this.templateOptions = options.templateOptions || {};
  74. this.extname = options.extname || '.hbs';
  75. this.partialsPath = options.partialsPath || '';
  76. this.contentHelperName = options.contentHelperName || 'contentFor';
  77. this.blockHelperName = options.blockHelperName || 'block';
  78. this.defaultLayout = options.defaultLayout || '';
  79. this.layoutsPath = options.layoutsPath || '';
  80. this.locals = options.locals || {};
  81. this.partialsRegistered = false;
  82. // Cache templates and layouts
  83. this.cache = {};
  84. this.blocks = {};
  85. // block helper
  86. this.registerHelper(this.blockHelperName, function(name, options) {
  87. // instead of returning self.block(name), render the default content if no
  88. // block is given
  89. val = self.block(name);
  90. if(val == '' && typeof options.fn === 'function') val = options.fn(this);
  91. return val;
  92. })
  93. // contentFor helper
  94. this.registerHelper(this.contentHelperName, function(name, options) {
  95. return self.content(name, options, this);
  96. })
  97. return this;
  98. };
  99. /**
  100. * Middleware for koa
  101. *
  102. * @api public
  103. */
  104. Hbs.prototype.middleware = function(options) {
  105. this.configure(options);
  106. var render = this.createRenderer();
  107. return function *(next) {
  108. this.render = render;
  109. yield next;
  110. };
  111. }
  112. /**
  113. * Create a render generator to be attached to koa context
  114. */
  115. Hbs.prototype.createRenderer = function() {
  116. var hbs = this;
  117. return function *(tpl, locals) {
  118. var tplPath = path.join(hbs.viewPath, tpl + hbs.extname),
  119. template, rawTemplate, layoutTemplate;
  120. locals = merge(hbs.locals, locals || {});
  121. // Initialization... move these actions into another function to remove
  122. // unnecessary checks
  123. if(!hbs.partialsRegistered && hbs.partialsPath !== '')
  124. yield hbs.registerPartials();
  125. if(!hbs.layoutTemplate)
  126. hbs.layoutTemplate = yield hbs.cacheLayout();
  127. // Load the template
  128. if(!hbs.cache[tpl]) {
  129. rawTemplate = yield read(tplPath);
  130. hbs.cache[tpl] = {
  131. template: hbs.handlebars.compile(rawTemplate)
  132. }
  133. // Load layout if specified
  134. if(rLayoutPattern.test(rawTemplate)) {
  135. var layout = rLayoutPattern.exec(rawTemplate)[1];
  136. var rawLayout = yield hbs.loadLayoutFile(layout);
  137. hbs.cache[tpl].layoutTemplate = hbs.handlebars.compile(rawLayout);
  138. }
  139. }
  140. template = hbs.cache[tpl].template;
  141. layoutTemplate = hbs.cache[tpl].layoutTemplate || hbs.layoutTemplate;
  142. // Run the compiled templates
  143. locals.body = template(locals, hbs.templateOptions);
  144. this.body = layoutTemplate(locals, hbs.templateOptions);
  145. };
  146. }
  147. /**
  148. * Get layout path
  149. */
  150. Hbs.prototype.getLayoutPath = function(layout) {
  151. if(this.layoutsPath)
  152. return path.join(this.layoutsPath, layout + this.extname);
  153. return path.join(this.viewPath, layout + this.extname);
  154. }
  155. /**
  156. * Get a default layout. If none is provided, make a noop
  157. */
  158. Hbs.prototype.cacheLayout = function(layout) {
  159. var hbs = this;
  160. return function* () {
  161. // Create a default layout to always use
  162. if(!layout && !hbs.defaultLayout)
  163. return hbs.handlebars.compile("{{{body}}}");
  164. // Compile the default layout if one not passed
  165. if(!layout) layout = hbs.defaultLayout;
  166. var layoutTemplate;
  167. try {
  168. var rawLayout = yield hbs.loadLayoutFile(layout);
  169. layoutTemplate = hbs.handlebars.compile(rawLayout);
  170. } catch (err) {
  171. console.error(err.stack);
  172. }
  173. return layoutTemplate;
  174. };
  175. }
  176. /**
  177. * Load a layout file
  178. */
  179. Hbs.prototype.loadLayoutFile = function(layout) {
  180. var hbs = this;
  181. return function(done) {
  182. var file = hbs.getLayoutPath(layout);
  183. read(file)(done);
  184. };
  185. }
  186. /**
  187. * Register helper to internal handlebars instance
  188. */
  189. Hbs.prototype.registerHelper = function() {
  190. this.handlebars.registerHelper.apply(this.handlebars, arguments);
  191. }
  192. /**
  193. * Register partial with internal handlebars instance
  194. */
  195. Hbs.prototype.registerPartial = function() {
  196. this.handlebars.registerPartial.apply(this.handlebars, arguments);
  197. }
  198. /**
  199. * Register directory of partials
  200. */
  201. Hbs.prototype.registerPartials = function () {
  202. var self = this;
  203. if(this.partialsPath == '')
  204. throw new Error('registerPartials requires partialsPath');
  205. if(!(this.partialsPath instanceof Array))
  206. this.partialsPath = [this.partialsPath];
  207. /* thunk creator for readdirp */
  208. var readdir = function(root) {
  209. return function(done) {
  210. glob("**/*"+self.extname, {
  211. cwd: root,
  212. }, done);
  213. };
  214. };
  215. /* Read in partials and register them */
  216. return function *() {
  217. try {
  218. var resultList = yield self.partialsPath.map( readdir );
  219. var files = [];
  220. var names = [];
  221. // Generate list of files and template names
  222. resultList.forEach(function(result,i) {
  223. result.forEach(function(file) {
  224. files.push(path.join(self.partialsPath[i], file));
  225. names.push(file.slice(0,-1*self.extname.length));
  226. });
  227. });
  228. // Read all the partial from disk
  229. partials = yield files.map(read);
  230. for(var i=0; i!=partials.length; i++) {
  231. self.registerPartial(names[i], partials[i]);
  232. }
  233. self.partialsRegistered = true;
  234. } catch(e) {
  235. console.error('Error caught while registering partials');
  236. console.error(e);
  237. }
  238. };
  239. };
  240. /**
  241. * The contentFor helper delegates to here to populate block content
  242. */
  243. Hbs.prototype.content = function(name, options, context) {
  244. // fetch block
  245. var block = this.blocks[name] || (this.blocks[name] = []);
  246. // render block and save for layout render
  247. block.push(options.fn(context));
  248. }
  249. /**
  250. * block helper delegates to this function to retreive content
  251. */
  252. Hbs.prototype.block = function(name) {
  253. // val = block.toString
  254. var val = (this.blocks[name] || []).join('\n');
  255. // clear the block
  256. this.blocks[name] = [];
  257. return val;
  258. }