diff --git a/Makefile b/Makefile
index cb6f235..a8ed7d6 100644
--- a/Makefile
+++ b/Makefile
@@ -3,6 +3,6 @@ test:
--harmony-generators \
-R spec \
test/render \
- --bail
+ test/unit
.PHONY: test
diff --git a/README.md b/README.md
index 47302d1..0a1a37b 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,27 @@
koa-hbs
=======
-Handlebars Templates via Generators for [Koa](https://github.com/koajs/koa/)
+[Handlebars](http://handlebarsjs.com) Templates via Generators for
+[Koa](https://github.com/koajs/koa/)
-## Foreward
-This is package offers minimum viability. Registering partials and synchronous
-helpers is supported, but asynchronous helpers and layouts are not. Layouts are
-next on the list.
+[![Build Status][travis-badge]][repo-url]
+
+## Forward
+Things that are supported:
+- Registering helpers
+- Registering partials
+- Specify a directory or multiple directories of partials to register
+- Single default layout
+- Alternative layouts
+- Template caching (actually, this cannot currently be disabled)
+
+Things that will be, but are **not** yet supported:
+- Asynchronous helpers
+- Content blocks
## Usage
-koa-hbs is middleware. Configure the default instance by passing an options hash to #middleware, or create an independent instance using #create().
+koa-hbs is middleware. Configure the default instance by passing an options hash
+to #middleware or on an independant from #create().
```javascript
var koa = require('koa');
@@ -17,13 +29,13 @@ var hbs = require('koa-hbs');
var app = koa();
-// koa-hbs is middleware. Use it before you want to render a view
+// koa-hbs is middleware. `use` it before you want to render a view
app.use(hbs.middleware({
viewPath: __dirname + '/views'
}));
-// Render is attached to the koa context. Call this.render in your middleware
-// to attach your rendered html to the response body.
+// Render is attached to the koa context. Call `this.render` in your middleware
+// to attach rendered html to the koa response body.
app.use(function *() {
yield this.render('main', {title: 'koa-hbs'});
})
@@ -32,22 +44,94 @@ app.listen(3000);
```
+After a template has been rendered, the template function is cached. `#render#
+accepts two arguements - the template to render, and an object containing local
+variables to be inserted into the template. The result is assigned to Koa's
+`this.response.body`.
+
+### Registering Helpers
+Helpers are registered using the #registerHelper method. Here is an example
+using the default instance (helper stolen from official Handlebars
+[docs](http://handlebarsjs.com):
+
+```javascript
+hbs = require('koa-hbs');
+
+hbs.registerHelper('link', function(text, url) {
+ text = hbs.Utils.escapeExpression(text);
+ url = hbs.Utils.escapeExpression(url);
+
+ var result = '' + text + '';
+
+ return new hbs.SafeString(result);
+});
+```
+
+registerHelper, Utils, and SafeString all proxy to an internal Handlebars
+instance. If passing an alternative instance of Handlebars to the middleware
+configurator, make sure to do so before registering your helpers via the koa-hbs
+proxies, or just register your helpers directly via your Handlebars instance.
+
+### Registering Partials
+The simple way to register partials is to stick them all in a directory, and
+pass the `partialsPath` option when generating the middleware. Say your views
+are in `./views`, and your partials are in `./views/partials`. Configuring the
+middleware as
+
+### Layouts
+Passing `defaultLayout` with the a layout name will cause all templates to be
+inserted into the `{{{body}}}` expression of the layout. This might look like
+the following.
+
+```html
+
+
+
+ {{title}}
+
+
+ {{{body}}}
+
+
+```
+
+In addition to, or alternatively, you may specify a layout to render a template
+into. Simply specify `{{!< layoutName }}` somewhere in your template. koa-hbs
+will load your layout from `layoutsPath` if defined, or from `viewPath`
+otherwise.
+
+At this time, only a single content block (`{{{body}}}`) is supported. Block and
+contentFor helpers are on the list of features to implement.
+
### Options
-The plan for koa-hbs is to offer identical functionality as express-hbs (eventaully). These options are supported _now_.
+The plan for koa-hbs is to offer identical functionality as express-hbs
+(eventaully). These options are supported _now_.
-- `viewPath`: [_required_] Where to load templates from
+- `viewPath`: [_required_] Full path from which to load templates
+ (`Array|String`)
- `handlebars`: Pass your own instance of handlebars
-- `templateOptions`: Options to pass to `template()`
-- `extname`: Alter the default template extension (default: `.hbs`)
-- `partialsPath`: Use this directory for partials
+- `templateOptions`: Hash of
+ [options](http://handlebarsjs.com/execution.html#Options) to pass to
+ `template()`
+- `extname`: Alter the default template extension (default: `'.hbs'`)
+- `partialsPath`: Full path to partials directory (`Array|String`)
+- `defaultLayout`: Name of the default layout
+- `layoutsPath`: Full path to layouts directory (`String`)
-These options are **NOT** supported (because we don't support layouts ... yet).
+These options are **NOT** supported yet.
- `contentHelperName`: Alter `contentFor` helper name
- `blockHelperName`: Alter `block` helper name
-- `defaultLayout`: Name of the default layout
-- `layoutsDir`: Load layouts from here
+
+
## Example
-You can run the included example via `npm install koa` and `node --harmony app.js` from the example folder.
+You can run the included example via `npm install koa` and
+`node --harmony app.js` from the example folder.
+
+## Credits
+Functionality and code were inspired/taken from
+[express-hbs](https://github.com/barc/express-hbs/).
+[travis-badge]: https://travis-ci.org/jwilm/koa-hbs.png?branch=master
+[repo-url]: https://travis-ci.org/jwilm/koa-hbs
diff --git a/index.js b/index.js
index 3b73dac..3d80c83 100644
--- a/index.js
+++ b/index.js
@@ -3,6 +3,9 @@ var path = require('path');
var co = require('co');
var readdirp = require('readdirp');
+/* Capture the layout name; thanks express-hbs */
+var rLayoutPattern = /{{!<\s+([A-Za-z0-9\._\-\/]+)\s*}}/;
+
/**
* file reader returning a thunk
* @param filename {String} Name of file to read
@@ -37,6 +40,11 @@ exports.create = function() {
function Hbs() {
if(!(this instanceof Hbs)) return new Hbs();
+
+ this.handlebars = require('handlebars').create();
+
+ this.Utils = this.handlebars.Utils;
+ this.SafeString = this.handlebars.SafeString;
}
/**
@@ -52,19 +60,19 @@ Hbs.prototype.configure = function (options) {
// Attach options
var options = options || {};
this.viewPath = options.viewPath;
- this.handlebars = options.handlebars || require('handlebars').create();
+ this.handlebars = options.handlebars || this.handlebars;
this.templateOptions = options.templateOptions || {};
this.extname = options.extname || '.hbs';
this.partialsPath = options.partialsPath || '';
-
- // Support for these options is planned, but not yet supported:
this.contentHelperName = options.contentHelperName || 'contentFor';
this.blockHelperName = options.blockHelperName || 'block';
this.defaultLayout = options.defaultLayout || '';
- this.layoutsDir = options.layoutsDir || '';
+ this.layoutsPath = options.layoutsPath || '';
- // Register partials in options partialsPath(s)
- this.registerPartials();
+ this.partialsRegistered = false;
+
+ // Cache templates and layouts
+ this.cache = {};
return this;
};
@@ -74,18 +82,11 @@ Hbs.prototype.configure = function (options) {
*
* @api public
*/
+
Hbs.prototype.middleware = function(options) {
- var hbs = this;
this.configure(options);
- var render = function *(templateName, args) {
- var templatePath = path.join(hbs.viewPath, templateName + hbs.extname);
- // No caching yet
- var tplFile = yield read(templatePath);
- var template = hbs.handlebars.compile(tplFile.toString());
-
- this.body = template(args, hbs.templateOptions);
- };
+ var render = this.createRenderer();
return function *(next) {
this.render = render;
@@ -93,6 +94,101 @@ Hbs.prototype.middleware = function(options) {
};
}
+/**
+ * Create a render generator to be attached to koa context
+ */
+
+Hbs.prototype.createRenderer = function() {
+ var hbs = this;
+
+ return function *(tpl, locals) {
+ var tplPath = path.join(hbs.viewPath, tpl + hbs.extname),
+ template, rawTemplate, layoutTemplate;
+
+ locals = locals || {};
+
+ // Initialization... move these actions into another function to remove
+ // unnecessary checks
+ if(!hbs.partialsRegistered)
+ yield hbs.registerPartials();
+
+ if(!hbs.layoutTemplate)
+ hbs.layoutTemplate = yield hbs.cacheLayout();
+
+ // Load the template
+ if(!hbs.cache[tpl]) {
+ rawTemplate = yield read(tplPath);
+ hbs.cache[tpl] = {
+ template: hbs.handlebars.compile(rawTemplate)
+ }
+
+ // Load layout if specified
+ if(rLayoutPattern.test(rawTemplate)) {
+ var layout = rLayoutPattern.exec(rawTemplate)[1];
+ console.log(layout);
+ var rawLayout = yield hbs.loadLayoutFile(layout);
+ hbs.cache[tpl].layoutTemplate = hbs.handlebars.compile(rawLayout);
+ }
+ }
+
+ template = hbs.cache[tpl].template;
+ layoutTemplate = hbs.cache[tpl].layoutTemplate || hbs.layoutTemplate;
+
+ // Run the compiled templates
+ locals.body = template(locals, hbs.templateOptions);
+ this.body = layoutTemplate(locals, hbs.templateOptions);
+ };
+}
+
+/**
+ * Get layout path
+ */
+
+Hbs.prototype.getLayoutPath = function(layout) {
+ if(this.layoutsPath)
+ return path.join(this.layoutsPath, layout + this.extname);
+
+ return path.join(this.viewPath, layout + this.extname);
+}
+
+/**
+ * Get a default layout. If none is provided, make a noop
+ */
+
+Hbs.prototype.cacheLayout = function(layout) {
+ var hbs = this;
+ return co(function* () {
+ // Create a default layout to always use
+ if(!layout && !hbs.defaultLayout)
+ return hbs.handlebars.compile("{{{body}}}");
+
+ // Compile the default layout if one not passed
+ if(!layout) layout = hbs.defaultLayout;
+
+ var layoutTemplate;
+ try {
+ var rawLayout = yield hbs.loadLayoutFile(layout);
+ layoutTemplate = hbs.handlebars.compile(rawLayout);
+ } catch (err) {
+ console.error(err.stack);
+ }
+
+ return layoutTemplate;
+ });
+}
+
+/**
+ * Load a layout file
+ */
+
+Hbs.prototype.loadLayoutFile = function(layout) {
+ var hbs = this;
+ return function(done) {
+ var file = hbs.getLayoutPath(layout);
+ read(file)(done);
+ };
+}
+
/**
* Register helper to internal handlebars instance
*/
@@ -105,15 +201,15 @@ Hbs.prototype.registerHelper = function() {
* Register partial with internal handlebars instance
*/
- Hbs.prototype.registerPartial = function() {
+Hbs.prototype.registerPartial = function() {
this.handlebars.registerPartial.apply(this.handlebars, arguments);
- }
+}
/**
* Register directory of partials
*/
-Hbs.prototype.registerPartials = function(cb) {
+Hbs.prototype.registerPartials = function (cb) {
var self = this, partials, dirpArray, files = [], names = [], partials,
rname = /^[a-zA-Z_-]+/, readdir;
@@ -131,7 +227,7 @@ Hbs.prototype.registerPartials = function(cb) {
};
/* Read in partials and register them */
- co(function *() {
+ return co(function *() {
try {
readdirpResults = yield self.partialsPath.map(readdir);
@@ -149,9 +245,12 @@ Hbs.prototype.registerPartials = function(cb) {
self.registerPartial(names[i], partials[i]);
}
+ self.partialsRegistered = true;
} catch(e) {
console.error('Error caught while registering partials');
console.error(e);
}
- })();
-};
\ No newline at end of file
+
+ return cb && cb(null, self);
+ });
+};
diff --git a/package.json b/package.json
index 9f65e3f..3a9ca39 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "koa-hbs",
- "version": "0.2.0",
+ "version": "0.3.0",
"description": "Handlebars Templates via Generators for Koa",
"main": "index.js",
"repository": {
diff --git a/test/app/assets/layouts/alternative.hbs b/test/app/assets/layouts/alternative.hbs
new file mode 100644
index 0000000..139e286
--- /dev/null
+++ b/test/app/assets/layouts/alternative.hbs
@@ -0,0 +1,12 @@
+
+
+
+ Alternative {{title}}
+
+
+ ALTERNATIVE LAYOUT
+
+ {{{body}}}
+
+
+
\ No newline at end of file
diff --git a/test/app/assets/layouts/default.hbs b/test/app/assets/layouts/default.hbs
new file mode 100644
index 0000000..8ecf83e
--- /dev/null
+++ b/test/app/assets/layouts/default.hbs
@@ -0,0 +1,10 @@
+
+
+
+ {{title}}
+
+
+ DEFAULT LAYOUT
+ {{{body}}}
+
+
\ No newline at end of file
diff --git a/test/app/assets/useAlternativeLayout.hbs b/test/app/assets/useAlternativeLayout.hbs
new file mode 100644
index 0000000..f6a981a
--- /dev/null
+++ b/test/app/assets/useAlternativeLayout.hbs
@@ -0,0 +1,2 @@
+{{!< alternative }}
+ALTERNATIVE CONTENT
\ No newline at end of file
diff --git a/test/app/assets/useDefaultLayout.hbs b/test/app/assets/useDefaultLayout.hbs
new file mode 100644
index 0000000..276dc3a
--- /dev/null
+++ b/test/app/assets/useDefaultLayout.hbs
@@ -0,0 +1 @@
+DEFAULT CONTENT
\ No newline at end of file
diff --git a/test/app/index.js b/test/app/index.js
index 75251b2..d0f577c 100644
--- a/test/app/index.js
+++ b/test/app/index.js
@@ -1,7 +1,7 @@
var hbs = require('../../index');
var koa = require('koa');
-var create = function() {
+var create = function(opts) {
var app = koa();
var _hbs = hbs.create();
@@ -9,10 +9,7 @@ var create = function() {
console.error(err.stack);
});
- app.use(_hbs.middleware({
- viewPath: __dirname + '/assets',
- partialsPath: __dirname + '/assets/partials'
- }));
+ app.use(_hbs.middleware(opts));
app.use(function*(next) {
if(this.path == '/')
@@ -35,6 +32,20 @@ var create = function() {
}
});
+ app.use(function*(next) {
+ if(this.path == '/layout')
+ yield this.render('useDefaultLayout');
+ else
+ yield next;
+ });
+
+ app.use(function*(next) {
+ if(this.path == '/altLayout')
+ yield this.render('useAlternativeLayout');
+ else
+ yield next;
+ });
+
return app;
}
diff --git a/test/render.js b/test/render.js
index 338bf90..f459671 100644
--- a/test/render.js
+++ b/test/render.js
@@ -10,11 +10,13 @@ describe('without required options', function() {
});
});
-describe('render', function() {
+describe('simple render', function() {
var app;
- before(function(done) {
- app = testApp.create();
- setTimeout(done, 200); // hack to make sure partials are loaded
+ before(function() {
+ app = testApp.create({
+ viewPath: __dirname + '/app/assets',
+ partialsPath: __dirname + '/app/assets/partials'
+ });
});
it('should put html in koa response body', function(done) {
@@ -41,5 +43,46 @@ describe('render', function() {
});
});
});
+});
+
+describe('when using layouts', function() {
+ var app;
+ before(function() {
+ // Create app which specifies layouts
+ app = testApp.create({
+ viewPath: __dirname + '/app/assets',
+ partialsPath: __dirname + '/app/assets/partials',
+ layoutsPath: __dirname + '/app/assets/layouts',
+ defaultLayout: 'default'
+ });
+ });
+
+ describe('with the default layout', function() {
+ it('should insert rendered content', function(done) {
+ request(app.listen())
+ .get('/layout')
+ .expect(200)
+ .end(function(err, content) {
+ if(err) return done(err);
+ assert.ok(/DEFAULT LAYOUT/.test(content.text));
+ assert.ok(/DEFAULT CONTENT/.test(content.text));
+ done();
+ });
+ });
+
+ it('should support alternative layouts', function(done) {
+ request(app.listen())
+ .get('/altLayout')
+ .expect(200)
+ .end(function(err, content) {
+ if(err) return done(err);
+ assert.ok(/ALTERNATIVE LAYOUT/.test(content.text));
+ assert.ok(/ALTERNATIVE CONTENT/.test(content.text));
+ done();
+ })
+ })
+ });
});
+
+
diff --git a/test/unit.js b/test/unit.js
new file mode 100644
index 0000000..35d44ac
--- /dev/null
+++ b/test/unit.js
@@ -0,0 +1,19 @@
+var assert = require('assert');
+var path = require('path');
+
+describe('getLayoutPath', function() {
+ var hbs;
+ before(function() {
+ hbs = require('..').create();
+ hbs.middleware({
+ viewPath: __dirname + '/app/assets',
+ layoutsPath: __dirname + '/app/assets/layouts',
+ partialsPath: __dirname + '/app/assets/partials'
+ });
+ });
+
+ it('should return the correct path', function() {
+ var layoutPath = path.join(__dirname, '/app/assets/layouts/default.hbs');
+ assert.equal(hbs.getLayoutPath('default'), layoutPath);
+ });
+});