├── .gitignore ├── .npmignore ├── CHANGES.md ├── MIT-LICENSE ├── README.md ├── example ├── app.coffee ├── server.js ├── spec │ └── example-spec.js └── views │ ├── head.html │ ├── index.html │ ├── layout.html │ └── temp.html ├── hogan-express.coffee ├── hogan-express.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | _* 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.coffee 2 | example -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ### 0.5.1 2 | - partials didn't work in yield tags [fixed by @summerisgone] 3 | ### 0.5.1 4 | 5 | - fix bugs with lambdas 6 | 7 | ### 0.5.0 8 | 9 | - add better lambda/filter support 10 | 11 | ### 0.4.0 12 | 13 | - add cutstom yield tags support -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Andrew Volkov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## HOGAN-EXPRESS 2 | 3 | [Mustache][1] template engine for the [express 3.x][2] web framework. 4 | 5 | Uses twitter's [hogan.js][3] engine. 6 | 7 | Supports 8 | - Partials (Allows you to modularize, to move pieces of templates to their own file - think of these as "included" templates) 9 | - Layouts (Allows you to consolidate common elements of your templates - think of these as "parent" templates) 10 | - Caching (Makes your app more efficient by reducing unnecessary rendering) 11 | - Lambdas (Allows you to create custom filters/lambdas) 12 | 13 | ### Install 14 | 15 | Install hogan-express using [npm][4]: 16 | 17 | `npm install hogan-express` 18 | 19 | ### Usage 20 | 21 | #### Setup 22 | To use hogan-express, map the file extension of your choice to the 23 | hogan-express engine in your app setup. For example: 24 | 25 | ```coffeescript 26 | app.set 'view engine', 'html' # use .html extension for templates 27 | app.set 'layout', 'layout' # use layout.html as the default layout 28 | app.set 'partials', foo: 'foo' # define partials available to all pages 29 | app.enable 'view cache' 30 | app.engine 'html', require('hogan-express') 31 | ``` 32 | 33 | #### Rendering a template 34 | 35 | Within your app route callback, define `res.locals` and call `res.render`, passing any partials required by your template. For example: 36 | 37 | ```coffeescript 38 | app.get '/', (req,res)-> 39 | res.locals = name: 'Andrew' 40 | res.render 'template', partials: {message: 'message'} 41 | ``` 42 | 43 | This would render the layout (`layout.html`, defined in setup) using the template (`template.html`) and the specified partials (`message.html`). 44 | 45 | If `layout.html` contained: 46 | 47 | ```html 48 |

49 | Message Layout 50 | {{{ yield }}} 51 |

52 | ``` 53 | 54 | and `template.html` contained: 55 | 56 | ```html 57 | {{ name }} says {{> message }} 58 | ``` 59 | 60 | and `message.html` contained: 61 | 62 | ```html 63 | Hello World. 64 | ``` 65 | 66 | the callback would produce: 67 | 68 | ```html 69 |

70 | Message Layout 71 | Andrew says Hello World. 72 |

73 | ``` 74 | 75 | The special `{{{ yield }}}` variable in `layout.html` indicates the location in your layout file where your template is rendered. You can define your layout using `app.set 'layout', ...` or specify it when calling `res.render`. If a layout is not provided, the template is rendered directly. 76 | 77 | #### Custom yield tags 78 | 79 | You can define more extension points in `layout.html` using custom tags ``{{yield-}}``. For example: 80 | 81 | layout: 82 | 83 | ```html 84 | 85 | ... 86 | {{{yield-styles}}} 87 | {{{yield-scripts}}} 88 | ... 89 | 90 | ``` 91 | 92 | index: 93 | 94 | ```html 95 | {{#yield-styles}} 96 | 99 | {{/yield-styles}} 100 | 101 | {{#yield-scripts}} 102 | 105 | {{/yield-scripts}} 106 | ``` 107 | 108 | The page `index.html` will be rendered into ``{{yield}}`` without the content in ``{{#yield-styles}}...{{/yield-styles}`` and ``{{#yield-scripts}}...{{/yield-scripts}}``. That content goes into accordingly named tags in `layout.html`. If ``{{{yield-styles}}}`` is missing, the styles tag content will not be rendered. 109 | 110 | #### Custom layouts 111 | 112 | To render a page with custom layout, just specify it in the options: `res.render "admin.html", layout: "admin-layout"` 113 | 114 | #### Custom Lambdas / Filters 115 | 116 | To create custom filters (or lambdas) you can just specify your filter functions in the options: 117 | 118 | ```coffeescript 119 | app.get '/', (req,res)-> 120 | 121 | res.locals = myDefaultLabel: "oops" # here to show a little of how scoping works 122 | 123 | res.render 'template', 124 | message: 'This is a message. HERE.' 125 | mylist: [{label: "one", num: 1},{label: "two", num: 2},{num: 3}] 126 | 127 | lambdas: 128 | lowercase: (text) -> 129 | return text.toLowerCase() 130 | reverseString: (text) -> 131 | return text.split("").reverse().join("") 132 | ``` 133 | 134 | template: 135 | 136 | ```html 137 |

Lowercase {{message}}: {{#lambdas.lowercase}}{{message}}{{/lambdas.lowercase}}

138 | 143 | ``` 144 | 145 | rendered html: 146 | 147 | ```html 148 |

Lowercase This is a message. HERE.: this is a message. here.

149 | 154 | ``` 155 | 156 | ### Contributors 157 | 158 | [Contributors list](https://github.com/vol4ok/hogan-express/graphs/contributors) 159 | 160 | Thank you for your participation! 161 | 162 | ### License 163 | hogan-express is released under an [MIT License][5]. 164 | 165 | [1]: http://mustache.github.io/mustache.5.html 166 | [2]: http://expressjs.com/ 167 | [3]: https://github.com/twitter/hogan.js 168 | [4]: https://npmjs.org/ 169 | [5]: http://opensource.org/licenses/MIT 170 | -------------------------------------------------------------------------------- /example/app.coffee: -------------------------------------------------------------------------------- 1 | express = require 'express' 2 | 3 | app = module.exports = express() 4 | 5 | app.set('view engine', 'html') 6 | app.set('layout', 'layout') 7 | app.set('partials', head: "head") 8 | 9 | #app.enable('view cache') 10 | 11 | app.engine 'html', require('../hogan-express.coffee') 12 | app.set('views', __dirname + '/views') 13 | 14 | app.use(express.bodyParser()) 15 | app.use(app.router) 16 | 17 | app.get '/', (req,res)-> 18 | res.locals = what: 'World' 19 | 20 | res.locals.data = "default data" 21 | 22 | res.render "index", 23 | list: [ {title: "first", data: "custom data"}, {title: "Second"}, {title: "third"} ] 24 | partials: {temp: 'temp'} 25 | lambdas: 26 | reverseString: (text) -> 27 | return text.split("").reverse().join("") 28 | uppercase: (text) -> 29 | return text.toUpperCase() 30 | 31 | app.listen(4020) 32 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | require('coffee-script'); 2 | require('./app.coffee'); -------------------------------------------------------------------------------- /example/spec/example-spec.js: -------------------------------------------------------------------------------- 1 | var request = require("supertest"); 2 | var cheerio = require("cheerio"); 3 | var coffeescript = require("coffee-script"); 4 | var app = require("./../app.coffee"); 5 | var expect = require('expect.js'); 6 | 7 | describe("example-page", function() { 8 | var cheer; 9 | //render and parse page 10 | before(function(done) { 11 | request(app).get('/').end(function(err,res) { 12 | if(err) { 13 | console.error(err); 14 | } 15 | else { 16 | cheer = cheerio.load(res.text); 17 | } 18 | done(); 19 | }); 20 | }); 21 | 22 | it("should have a body", function() { 23 | expect(cheer("body").length).to.be(1); 24 | }); 25 | it("should have a h1 = Test", function() { 26 | expect(cheer("h1").text()).to.be("Test"); 27 | }); 28 | 29 | it("should be able to handle lambdas (reverse)", function() { 30 | expect(cheer("[rel='test-reverse-lambda']").text().trim()).to.be("dlroW"); 31 | }); 32 | it("should be able to handle lambdas within arrays (reverse)", function() { 33 | expect(cheer("[rel='test-reverse-lambda-with-context'] tr:first-child th").text().trim()).to.be("tsrif") 34 | }); 35 | it("should be able to handle lambdas within arrays (reverse) - including 'locals' data", function() { 36 | expect(cheer("[rel='test-reverse-lambda-with-context'] tr:nth-child(1) td").text().trim()).to.be("atad motsuc") 37 | expect(cheer("[rel='test-reverse-lambda-with-context'] tr:nth-child(2) td").text().trim()).to.be("atad tluafed") 38 | }); 39 | 40 | it("should be able to have more than one lambda defined", function() { 41 | expect(cheer("[rel='test-uppercase-lambda']").text().trim()).to.be("WORLD") 42 | }); 43 | 44 | it("should be able to have nested lambdas", function() { 45 | expect(cheer("[rel='test-nested-lambdas']").text().trim()).to.be("DLROW") 46 | }); 47 | 48 | }); 49 | -------------------------------------------------------------------------------- /example/views/head.html: -------------------------------------------------------------------------------- 1 |

Test

-------------------------------------------------------------------------------- /example/views/index.html: -------------------------------------------------------------------------------- 1 | Hello {{what}}! 2 | 3 |

Test array of objects

4 |
5 | {{#list}} 6 |
{{title}}:
7 |
{{data}}
8 | {{/list}} 9 |
10 | 11 |

Test Lambdas (should reverse "{{what}}")

12 |
13 | {{#lambdas.reverseString}}{{what}}{{/lambdas.reverseString}} 14 |
15 | 16 |

Test Lambdas within array context

17 | 18 | {{#list}} 19 | 20 | 21 | 22 | 23 | {{/list}} 24 |
{{#lambdas.reverseString}}{{title}}{{/lambdas.reverseString}}{{#lambdas.reverseString}}{{data}}{{/lambdas.reverseString}}
25 | 26 |
This second lambda is here to test that you can have more than one type of lambda
27 |
28 | {{#lambdas.uppercase}}world{{/lambdas.uppercase}} 29 |
30 | 31 |
You can even nest your lambdas!
32 |
33 | {{#lambdas.reverseString}}{{#lambdas.uppercase}}{{what}}{{/lambdas.uppercase}}{{/lambdas.reverseString}} 34 |
35 | 36 |

-----[ > temp ]-----

37 | 38 | {{> temp}} 39 | 40 | {{#yield-footer}} 41 |

I'm a footer, defined in index.html

42 | {{/yield-footer}} 43 | -------------------------------------------------------------------------------- /example/views/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Test 6 | 7 | 8 | 9 |

-----[ > head ]-----

10 | 11 | {{> head}} 12 | 13 |

-----[ { yield } ]-----

14 | 15 | {{{ yield }}} 16 | 17 |

-----[ { yield-footer } ]-----

18 | 19 | {{{ yield-footer }}} 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/views/temp.html: -------------------------------------------------------------------------------- 1 |

I'm a temp.html partial

-------------------------------------------------------------------------------- /hogan-express.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | * Copyright (c) 2012 Andrew Volkov 3 | ### 4 | 5 | $ = {} 6 | $ extends require 'fs' 7 | $ extends require 'util' 8 | $ extends require 'path' 9 | hogan = require 'hogan.js' 10 | 11 | cache = {} 12 | ctx = {} 13 | 14 | read = (path, options, fn) -> 15 | str = cache[path] 16 | return fn(null, str) if (options.cache and str) 17 | $.readFile path, 'utf8', (err, str) -> 18 | return fn(err) if (err) 19 | # Remove potential UTF Byte Order Mark 20 | str = str.replace(/^\uFEFF/, '') 21 | cache[path] = str if (options.cache) 22 | fn(null, str) 23 | 24 | renderPartials = (partials, opt, fn) -> 25 | count = 1 26 | result = {} 27 | for name, path of partials 28 | continue unless typeof path is 'string' 29 | path += ctx.ext unless $.extname(path) 30 | path = ctx.lookup(path) 31 | count++ 32 | read path, opt, ((name, path) -> 33 | return (err, str) -> 34 | return unless count 35 | if err 36 | count = 0 37 | fn(err) 38 | result[name] = str 39 | fn(null, result) unless --count 40 | )(name, path) 41 | fn(null, result) unless --count 42 | 43 | renderLayout = (path, opt, fn) -> 44 | return fn(null, false) unless path 45 | path += ctx.ext unless $.extname(path) 46 | path = ctx.lookup(path) 47 | return fn(null, false) unless path 48 | read path, opt, (err, str) -> 49 | return fn(err) if (err) 50 | fn(null, str) 51 | 52 | customContent = (str, tag, opt, partials) -> 53 | oTag = "{{##{tag}}}" 54 | cTag = "{{/#{tag}}}" 55 | text = str.substring(str.indexOf(oTag) + oTag.length, str.indexOf(cTag)) 56 | hogan.compile(text, opt).render(opt, partials) 57 | 58 | render = (path, opt, fn) -> 59 | ctx = this 60 | partials = opt.settings.partials or {} 61 | partials = partials extends opt.partials if opt.partials 62 | 63 | lambdas = opt.settings.lambdas or {} 64 | lambdas = lambdas extends opt.lambdas if opt.lambdas 65 | # get rid of junk from "extends" - make it a normal object again 66 | delete lambdas['prototype'] 67 | delete lambdas['__super__'] 68 | 69 | # create the lambdafied functions 70 | # this way of dealing with lambdas assumes you'll want 71 | # to call your function on the rendered content instead 72 | # of the original template string 73 | opt.lambdas = {} 74 | for name, lambda of lambdas 75 | do (name, lambda) -> 76 | opt.lambdas[name] = -> 77 | lcontext = @ 78 | return (text) -> 79 | # getting the context right here is important 80 | # it must account for "locals" and values in the current context 81 | # ... particually interesting when applying within a list 82 | lctx= {} 83 | lctx = lctx extends opt._locals if opt._locals 84 | lctx = lctx extends lcontext 85 | return lambda(hogan.compile(text).render(lctx)) 86 | 87 | renderPartials partials, opt, (err, partials) -> 88 | return fn(err) if (err) 89 | layout = if opt.layout is undefined 90 | opt.settings.layout 91 | else 92 | layout = opt.layout 93 | renderLayout layout, opt, (err, layout) -> 94 | read path, opt, (err, str) -> 95 | return fn(err) if (err) 96 | try 97 | tmpl = hogan.compile(str, opt) 98 | result = tmpl.render(opt, partials) 99 | customTags = str.match(/({{#yield-\w+}})/g) 100 | yields = {} 101 | if customTags 102 | for customTag in customTags 103 | tag = customTag.match(/{{#([\w-]+)}}/)[1] 104 | if tag 105 | if layout 106 | opt[tag] = customContent(str, tag, opt, partials) 107 | else 108 | yields[tag.replace('yield-', '')] = customContent(str, tag, opt, partials) 109 | if layout 110 | opt.yield = result 111 | tmpl = hogan.compile(layout, opt) 112 | result = tmpl.render(opt, partials) 113 | return fn(null, result) 114 | return fn(null, result, yields) 115 | catch err 116 | fn(err) 117 | 118 | module.exports = render 119 | -------------------------------------------------------------------------------- /hogan-express.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.7.1 2 | 3 | /*! 4 | * Copyright (c) 2012 Andrew Volkov 5 | */ 6 | 7 | (function() { 8 | var $, cache, ctx, customContent, hogan, read, render, renderLayout, renderPartials, 9 | __hasProp = {}.hasOwnProperty, 10 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; 11 | 12 | $ = {}; 13 | 14 | __extends($, require('fs')); 15 | 16 | __extends($, require('util')); 17 | 18 | __extends($, require('path')); 19 | 20 | hogan = require('hogan.js'); 21 | 22 | cache = {}; 23 | 24 | ctx = {}; 25 | 26 | read = function(path, options, fn) { 27 | var str; 28 | str = cache[path]; 29 | if (options.cache && str) { 30 | return fn(null, str); 31 | } 32 | return $.readFile(path, 'utf8', function(err, str) { 33 | if (err) { 34 | return fn(err); 35 | } 36 | str = str.replace(/^\uFEFF/, ''); 37 | if (options.cache) { 38 | cache[path] = str; 39 | } 40 | return fn(null, str); 41 | }); 42 | }; 43 | 44 | renderPartials = function(partials, opt, fn) { 45 | var count, name, path, result; 46 | count = 1; 47 | result = {}; 48 | for (name in partials) { 49 | path = partials[name]; 50 | if (typeof path !== 'string') { 51 | continue; 52 | } 53 | if (!$.extname(path)) { 54 | path += ctx.ext; 55 | } 56 | path = ctx.lookup(path); 57 | count++; 58 | read(path, opt, (function(name, path) { 59 | return function(err, str) { 60 | if (!count) { 61 | return; 62 | } 63 | if (err) { 64 | count = 0; 65 | fn(err); 66 | } 67 | result[name] = str; 68 | if (!--count) { 69 | return fn(null, result); 70 | } 71 | }; 72 | })(name, path)); 73 | } 74 | if (!--count) { 75 | return fn(null, result); 76 | } 77 | }; 78 | 79 | renderLayout = function(path, opt, fn) { 80 | if (!path) { 81 | return fn(null, false); 82 | } 83 | if (!$.extname(path)) { 84 | path += ctx.ext; 85 | } 86 | path = ctx.lookup(path); 87 | if (!path) { 88 | return fn(null, false); 89 | } 90 | return read(path, opt, function(err, str) { 91 | if (err) { 92 | return fn(err); 93 | } 94 | return fn(null, str); 95 | }); 96 | }; 97 | 98 | customContent = function(str, tag, opt, partials) { 99 | var cTag, oTag, text; 100 | oTag = "{{#" + tag + "}}"; 101 | cTag = "{{/" + tag + "}}"; 102 | text = str.substring(str.indexOf(oTag) + oTag.length, str.indexOf(cTag)); 103 | return hogan.compile(text, opt).render(opt, partials); 104 | }; 105 | 106 | render = function(path, opt, fn) { 107 | var lambda, lambdas, name, partials, _fn; 108 | ctx = this; 109 | partials = opt.settings.partials || {}; 110 | if (opt.partials) { 111 | partials = __extends(partials, opt.partials); 112 | } 113 | lambdas = opt.settings.lambdas || {}; 114 | if (opt.lambdas) { 115 | lambdas = __extends(lambdas, opt.lambdas); 116 | } 117 | delete lambdas['prototype']; 118 | delete lambdas['__super__']; 119 | opt.lambdas = {}; 120 | _fn = function(name, lambda) { 121 | return opt.lambdas[name] = function() { 122 | var lcontext; 123 | lcontext = this; 124 | return function(text) { 125 | var lctx; 126 | lctx = {}; 127 | if (opt._locals) { 128 | lctx = __extends(lctx, opt._locals); 129 | } 130 | lctx = __extends(lctx, lcontext); 131 | return lambda(hogan.compile(text).render(lctx)); 132 | }; 133 | }; 134 | }; 135 | for (name in lambdas) { 136 | lambda = lambdas[name]; 137 | _fn(name, lambda); 138 | } 139 | return renderPartials(partials, opt, function(err, partials) { 140 | var layout; 141 | if (err) { 142 | return fn(err); 143 | } 144 | layout = opt.layout === void 0 ? opt.settings.layout : layout = opt.layout; 145 | return renderLayout(layout, opt, function(err, layout) { 146 | return read(path, opt, function(err, str) { 147 | var customTag, customTags, result, tag, tmpl, yields, _i, _len; 148 | if (err) { 149 | return fn(err); 150 | } 151 | try { 152 | tmpl = hogan.compile(str, opt); 153 | result = tmpl.render(opt, partials); 154 | customTags = str.match(/({{#yield-\w+}})/g); 155 | yields = {}; 156 | if (customTags) { 157 | for (_i = 0, _len = customTags.length; _i < _len; _i++) { 158 | customTag = customTags[_i]; 159 | tag = customTag.match(/{{#([\w-]+)}}/)[1]; 160 | if (tag) { 161 | if (layout) { 162 | opt[tag] = customContent(str, tag, opt, partials); 163 | } else { 164 | yields[tag.replace('yield-', '')] = customContent(str, tag, opt, partials); 165 | } 166 | } 167 | } 168 | } 169 | if (layout) { 170 | opt["yield"] = result; 171 | tmpl = hogan.compile(layout, opt); 172 | result = tmpl.render(opt, partials); 173 | return fn(null, result); 174 | } 175 | return fn(null, result, yields); 176 | } catch (_error) { 177 | err = _error; 178 | return fn(err); 179 | } 180 | }); 181 | }); 182 | }); 183 | }; 184 | 185 | module.exports = render; 186 | 187 | }).call(this); 188 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hogan-express", 3 | "version": "0.5.2", 4 | "description": "Mustache template engine for express 3.x. Support partials and layout", 5 | "main": "hogan-express.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:vol4ok/hogan-express.git" 12 | }, 13 | "keywords": [ 14 | "mustache", 15 | "hogan", 16 | "partials", 17 | "layout", 18 | "template", 19 | "engine" 20 | ], 21 | "author": "Andrew Volkov ", 22 | "license": "MIT", 23 | "dependencies": { 24 | "hogan.js": ">=2.0.0" 25 | }, 26 | "devDependencies": { 27 | "coffee-script": "1.x.x", 28 | "colors": "0.6.x", 29 | "supertest": "~0.8.0", 30 | "cheerio": "~0.12.2", 31 | "expect.js": "~0.2.0", 32 | "express": "~3.4.0" 33 | } 34 | } 35 | --------------------------------------------------------------------------------