├── .travis.yml ├── examples ├── views │ ├── partial │ │ ├── partials │ │ │ └── hello.dot │ │ └── index.dot │ ├── cascade │ │ ├── me.dot │ │ ├── boss.dot │ │ └── ceo.dot │ ├── layout │ │ ├── index.dot │ │ └── master.dot │ ├── helper │ │ └── index.dot │ └── index.dot └── index.js ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── package.json ├── test └── main.js ├── README.md └── index.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "0.10" 5 | -------------------------------------------------------------------------------- /examples/views/partial/partials/hello.dot: -------------------------------------------------------------------------------- 1 | Hello from partial 2 | -------------------------------------------------------------------------------- /examples/views/cascade/me.dot: -------------------------------------------------------------------------------- 1 | --- 2 | layout: boss.dot 3 | title: Page title 4 | --- 5 | 6 | [[##section: 7 | Hello from me.dot 8 | #]] 9 | -------------------------------------------------------------------------------- /examples/views/cascade/boss.dot: -------------------------------------------------------------------------------- 1 | --- 2 | layout: ceo.dot 3 | --- 4 | 5 | [[##section: 6 | Hello from Boss.dot
7 | [[= layout.section ]] 8 | #]] 9 | -------------------------------------------------------------------------------- /examples/views/layout/index.dot: -------------------------------------------------------------------------------- 1 | --- 2 | layout: master.dot 3 | title: Index page 4 | --- 5 | 6 | [[##section1: 7 | Hello from index.dot 8 | #]] 9 | 10 | [[##section2: 11 | Hello from index.dot again 12 | #]] 13 | -------------------------------------------------------------------------------- /examples/views/cascade/ceo.dot: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | [[= layout.title ]] 5 | 6 | 7 | Hello from CEO.dot
8 | [[= layout.section ]] 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/views/partial/index.dot: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | [[= layout.title ]] 5 | 6 | 7 |
8 | Message from partial: [[= partial('partials/hello.dot') ]] 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/views/layout/master.dot: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | [[= layout.title ]] 5 | 6 | 7 | Hello from master.dot
8 | [[= layout.section1 ]]
9 | [[= layout.section2 ]] 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/views/helper/index.dot: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Helper example 5 | 6 | 7 | 8 | model: [[= model.fromServer ]]
9 | helper property: [[# def.myHelperProperty ]]
10 | helper method: [[# def.myHelperMethod('Hello as a parameter') ]]
11 | helper in view: [[# def.myHelperInView ]] 12 | 13 | 14 | 15 | 16 | [[##def.myHelperInView: 17 | Hello from view helper ([[= model.fromServer ]]) 18 | #]] 19 | -------------------------------------------------------------------------------- /examples/views/index.dot: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Examples 5 | 6 | 7 |

Example

8 | 9 | Server says: [[= model.fromServer ]] 10 | 11 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | .idea/ 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.8 2 | - Handle render failure correctly (@maxiruani) 3 | - Use updated dependencies (@cjsturgess) 4 | 5 | # 1.0.7 6 | - Custom template provider (@mihaislobozeanu) 7 | 8 | # 1.0.5 9 | - Pass [[=layout]] to the partials 10 | 11 | # 1.0.4 12 | - Support > v0.10 node 13 | 14 | # 1.0.3 15 | - Add locals (and shortcut with express options 'view shortcut') 16 | 17 | # 1.0.2 18 | - Whitespace strip settings 19 | - Comment strip settings 20 | 21 | # 1.0.1 22 | - Fix the cache in production 23 | - Partial to be supported with caching on (new syntax structure) 24 | 25 | # 1.0.0 26 | - Reads the 'view data' settings from express and make it available [[= foo ]] 27 | 28 | # 0.2.1 29 | - Supports custom helper 30 | - Exposed method for email templating (or anything) 31 | 32 | # 0.2.0 33 | - Breaking change: yaml config 34 | - partials and layout file path, relative to the current file 35 | - Tests 36 | 37 | # 0.1.0 38 | - Initial version 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Dan Le Van 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-dot-engine", 3 | "version": "1.0.8", 4 | "description": "Node.js engine using the ultra fast doT templating with support for layouts, partials and friendly for front-end web libraries (Angular, Ember, Backbone...)", 5 | "author": "Dan Le Van", 6 | "homepage": "https://github.com/8lueberry/express-dot-engine", 7 | "keywords": [ 8 | "express", 9 | "doT", 10 | "engine", 11 | "template" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/8lueberry/express-dot-engine.git" 16 | }, 17 | "bugs": "https://github.com/8lueberry/express-dot-engine/issues", 18 | "contributors": [ 19 | "CJ Sturgess (https://sturgess.co/)" 20 | ], 21 | "license": "MIT", 22 | "main": "index.js", 23 | "scripts": { 24 | "test": "mocha" 25 | }, 26 | "engines": { 27 | "node": ">=0.10.0" 28 | }, 29 | "engineStrict": true, 30 | "dependencies": { 31 | "dot": "^1.1.2", 32 | "js-yaml": "^3.13.1", 33 | "lodash": "^4.17.15" 34 | }, 35 | "optionalDependencies": {}, 36 | "devDependencies": { 37 | "mocha": "^2.2.1", 38 | "mock-fs": "^2.5.0", 39 | "should": "^5.2.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | var engine = require('../'); 2 | var express = require('express'); 3 | var path = require('path'); 4 | 5 | var app = express(); 6 | 7 | app.engine('dot', engine.__express); 8 | app.set('views', path.join(__dirname, './views')); 9 | app.set('view engine', 'dot'); 10 | 11 | app.get('/', function(req, res) { 12 | res.render('index', { fromServer: 'Hello from server', }); 13 | }); 14 | 15 | app.get('/layout', function(req, res) { 16 | res.render('layout/index'); 17 | }); 18 | 19 | app.get('/cascade', function(req, res) { 20 | res.render('cascade/me'); 21 | }); 22 | 23 | app.get('/partial', function(req, res) { 24 | res.render('partial/index'); 25 | }); 26 | 27 | app.get('/helper', function(req, res) { 28 | 29 | // helper as a property 30 | engine.helper.myHelperProperty = 'Hello from server property helper'; 31 | 32 | // helper as a method 33 | engine.helper.myHelperMethod = function(param) { 34 | return 'Hello from server method helper (parameter: ' + param + ', server model: ' + this.model.fromServer + ')'; 35 | } 36 | 37 | res.render('helper/index', { fromServer: 'Hello from server', }); 38 | }); 39 | 40 | var server = app.listen(2015, function() { 41 | console.log('Run the example at http://locahost:%d', server.address().port); 42 | }); 43 | -------------------------------------------------------------------------------- /test/main.js: -------------------------------------------------------------------------------- 1 | var engine = require('../'); 2 | var mock = require('mock-fs'); 3 | var path = require('path'); 4 | var should = require('should'); 5 | 6 | var expressOptions = {}; 7 | 8 | describe('express-dot-engine', function() { 9 | 10 | afterEach(function() { 11 | mock.restore(); 12 | }); 13 | 14 | ////////////////////////////////////////////////////////////////////////////// 15 | // SERVER MODEL 16 | ////////////////////////////////////////////////////////////////////////////// 17 | describe('server model', function() { 18 | 19 | it('should have access to server model', function(done) { 20 | // prepare 21 | mock({ 22 | 'path/views': { 23 | 'child.dot': 'test-view [[= model.test ]]', 24 | }, 25 | }); 26 | 27 | // run 28 | engine.__express( 29 | 'path/views/child.dot', 30 | { test: 'test-model', }, 31 | function(err, result) { 32 | should(err).not.be.ok; 33 | should(result).equal('test-view test-model'); 34 | done(); 35 | }); 36 | }); 37 | 38 | it('should have access to server model in a layout', function(done) { 39 | // prepare 40 | mock({ 41 | 'path/views': { 42 | 'master.dot': 'test-master [[= model.test ]]', 43 | 'child.dot': '---\nlayout: master.dot\n---\n', 44 | }, 45 | }); 46 | 47 | // run 48 | engine.__express( 49 | 'path/views/child.dot', 50 | { test: 'test-model', }, 51 | function(err, result) { 52 | should(err).not.be.ok; 53 | should(result).equal('test-master test-model'); 54 | done(); 55 | }); 56 | }); 57 | 58 | it('should have access to server model in a partial', function(done) { 59 | // prepare 60 | mock({ 61 | 'path/views': { 62 | 'partial.dot': 'test-partial [[= model.test ]]', 63 | 'child.dot': 'test-child [[=partial(\'partial.dot\')]]', 64 | }, 65 | }); 66 | 67 | // run 68 | engine.__express( 69 | 'path/views/child.dot', 70 | { test: 'test-model', }, 71 | function(err, result) { 72 | should(err).not.be.ok; 73 | should(result).equal('test-child test-partial test-model'); 74 | done(); 75 | }); 76 | }); 77 | 78 | }); 79 | 80 | ////////////////////////////////////////////////////////////////////////////// 81 | // LAYOUT 82 | ////////////////////////////////////////////////////////////////////////////// 83 | describe('layout', function() { 84 | 85 | it('should support 2 levels', function(done) { 86 | // prepare 87 | mock({ 88 | 'path/views': { 89 | 'master.dot': 'test-master [[= layout.section ]]', 90 | 'child.dot': '---\nlayout: master.dot\n---\n[[##section:test-child#]]', 91 | }, 92 | }); 93 | 94 | // run 95 | engine.__express( 96 | 'path/views/child.dot', {}, 97 | function(err, result) { 98 | should(err).not.be.ok; 99 | should(result).equal('test-master test-child'); 100 | done(); 101 | }); 102 | }); 103 | 104 | it('should support 3 levels', function(done) { 105 | // prepare 106 | mock({ 107 | 'path/views': { 108 | 'master.dot': 'test-master [[= layout.section ]]', 109 | 'middle.dot': '---\nlayout: master.dot\n---\n[[##section:test-middle [[= layout.section ]]#]]', 110 | 'child.dot': '---\nlayout: middle.dot\n---\n[[##section:test-child#]]', 111 | }, 112 | }); 113 | 114 | // run 115 | engine.__express( 116 | 'path/views/child.dot', {}, 117 | function(err, result) { 118 | should(err).not.be.ok; 119 | should(result).equal('test-master test-middle test-child'); 120 | done(); 121 | }); 122 | }); 123 | 124 | }); 125 | 126 | ////////////////////////////////////////////////////////////////////////////// 127 | // PARTIAL 128 | ////////////////////////////////////////////////////////////////////////////// 129 | describe('partial', function() { 130 | 131 | it('should work', function(done) { 132 | // prepare 133 | mock({ 134 | 'path/views': { 135 | 'partial.dot': 'test-partial', 136 | 'child.dot': 'test-child [[=partial(\'partial.dot\')]]', 137 | }, 138 | }); 139 | 140 | // run 141 | engine.__express( 142 | 'path/views/child.dot', 143 | { test: 'test-model', }, 144 | function(err, result) { 145 | should(err).not.be.ok; 146 | should(result).equal('test-child test-partial'); 147 | done(); 148 | }); 149 | }); 150 | 151 | it('should allow to pass additional data to the partial', function(done) { 152 | // prepare 153 | mock({ 154 | 'path/views': { 155 | 'partial.dot': 'test-partial [[=model.media]]', 156 | 'child.dot': 'test-child [[=partial(\'partial.dot\', { media: model.test, })]]', 157 | }, 158 | }); 159 | 160 | // run 161 | engine.__express( 162 | 'path/views/child.dot', 163 | { test: 'test-model', }, 164 | function(err, result) { 165 | should(err).not.be.ok; 166 | should(result).equal('test-child test-partial test-model'); 167 | done(); 168 | }); 169 | }); 170 | 171 | }); 172 | 173 | ////////////////////////////////////////////////////////////////////////////// 174 | // TEMPLATE 175 | ////////////////////////////////////////////////////////////////////////////// 176 | describe('render', function() { 177 | 178 | it('should work async', function(done) { 179 | // prepare 180 | mock({ 181 | 'path/views': { 182 | 'child.dot': 'test-template [[= model.test ]]', 183 | }, 184 | }); 185 | 186 | // run 187 | engine.render( 188 | 'path/views/child.dot', 189 | { test: 'test-model', }, 190 | function(err, result) { 191 | should(err).not.be.ok; 192 | should(result).equal('test-template test-model'); 193 | done(); 194 | }); 195 | }); 196 | 197 | it('should work sync', function() { 198 | // prepare 199 | mock({ 200 | 'path/views': { 201 | 'child.dot': 'test-template [[= model.test ]]', 202 | }, 203 | }); 204 | 205 | // run 206 | var result = engine.render( 207 | 'path/views/child.dot', 208 | { test: 'test-model', }); 209 | 210 | // result 211 | should(result).equal('test-template test-model'); 212 | }); 213 | 214 | }); 215 | 216 | ////////////////////////////////////////////////////////////////////////////// 217 | // TEMPLATE STRING 218 | ////////////////////////////////////////////////////////////////////////////// 219 | describe('renderString', function() { 220 | 221 | it('should work async', function(done) { 222 | 223 | // run 224 | engine.renderString( 225 | 'test-template [[= model.test ]]', 226 | { test: 'test-model', }, 227 | function(err, result) { 228 | should(err).not.be.ok; 229 | should(result).equal('test-template test-model'); 230 | done(); 231 | }); 232 | }); 233 | 234 | it('should work sync', function() { 235 | 236 | // run 237 | var result = engine.renderString( 238 | 'test-template [[= model.test ]]', 239 | { test: 'test-model', }); 240 | 241 | // result 242 | should(result).equal('test-template test-model'); 243 | }); 244 | 245 | }); 246 | 247 | ////////////////////////////////////////////////////////////////////////////// 248 | // TEMPLATE PROVIDER 249 | ////////////////////////////////////////////////////////////////////////////// 250 | describe('render with template provider', function() { 251 | 252 | var templatename = 'render.with.template.provider', 253 | template = 'test-template [[= model.test ]]', 254 | getTemplate = function(name, options, callback) { 255 | var isAsync = callback && typeof callback === 'function'; 256 | if (name === templatename) { 257 | if(!isAsync){ 258 | return template; 259 | } 260 | callback(null, template); 261 | } 262 | }; 263 | 264 | it('should work async', function(done) { 265 | // run 266 | engine.render( 267 | templatename, 268 | { getTemplate: getTemplate, test: 'test-model', }, 269 | function(err, result) { 270 | should(err).not.be.ok; 271 | should(result).equal('test-template test-model'); 272 | done(); 273 | }); 274 | }); 275 | 276 | it('should work sync', function() { 277 | // run 278 | var result = engine.render( 279 | templatename, 280 | { getTemplate: getTemplate, test: 'test-model', }); 281 | 282 | // result 283 | should(result).equal('test-template test-model'); 284 | }); 285 | 286 | }); 287 | 288 | ////////////////////////////////////////////////////////////////////////////// 289 | // CACHE 290 | ////////////////////////////////////////////////////////////////////////////// 291 | describe('cache', function() { 292 | 293 | it('should work', function(done) { 294 | // prepare 295 | mock({ 296 | 'path/views': { 297 | 'child.dot': 'test-child [[= model.test ]]', 298 | }, 299 | }); 300 | 301 | // run 302 | function test(data, cb) { 303 | engine.__express( 304 | 'path/views/child.dot', 305 | { 306 | cache: true, 307 | test: data, 308 | }, 309 | function(err, result) { 310 | should(err).not.be.ok; 311 | should(result).equal('test-child ' + data); 312 | cb(); 313 | } 314 | ); 315 | } 316 | 317 | test('test-model1', 318 | function() { test('test-model2', done); } 319 | ); 320 | }); 321 | 322 | }); 323 | 324 | }); 325 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # express-dot-engine 2 | 3 | [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/8lueberry/express-dot-engine)](https://github.com/8lueberry/express-dot-engine/releases) 4 | [![node](https://img.shields.io/node/v/express-dot-engine)](https://npmjs.com/package/express-dot-engine) 5 | [![Build Status](https://travis-ci.org/8lueberry/express-dot-engine.svg)](https://travis-ci.org/8lueberry/express-dot-engine) 6 | 7 | > Node.js engine using the ultra fast [doT](http://olado.github.io/doT/) templating with support for layouts, partials. It's friendly for front-end web libraries (Angular, Ember, Backbone...) 8 | 9 | ## Important 10 | 11 | The default settings of doT has been change to use `[[ ]]` instead of `{{ }}`. This is to support client side templates (Angular, Ember, ...). You can change it back by changing the Settings (see below). 12 | 13 | ## Features 14 | 15 | - extremely fast ([see jsperf](http://jsperf.com/dom-vs-innerhtml-based-templating/998)) 16 | - all the advantage of [doT](http://olado.github.io/doT/) 17 | - layout and partial support 18 | - uses `[[ ]]` by default, not clashing with `{{ }}` (Angular, Ember...) 19 | - custom helpers to your views 20 | - conditional, array iterators, custom delimiters... 21 | - use it as logic-less or with logic, it is up to you 22 | - use it also for your email (or anything) templates 23 | - automatic caching in production 24 | 25 | ### Great for 26 | 27 | - √ Purists that wants html as their templates but with full access to javascript 28 | - √ Minimum server-side logic, passing server models to client-side frameworks like Angular, Ember, Backbone... 29 | - √ Clean and fast templating with support for layouts and partials 30 | - √ Email templating 31 | 32 | ### Not so much for 33 | 34 | - Jade style lovers (http://jade-lang.com/) 35 | - Full blown templating with everything already coded for you (you can however provide any custom functions to your views) 36 | 37 | ## Installation 38 | 39 | Install with npm 40 | 41 | ```sh 42 | $ npm install express-dot-engine --save 43 | ``` 44 | 45 | Then set the engine in express 46 | 47 | ```javascript 48 | var engine = require('express-dot-engine'); 49 | ... 50 | 51 | app.engine('dot', engine.__express); 52 | app.set('views', path.join(__dirname, './views')); 53 | app.set('view engine', 'dot'); 54 | ``` 55 | 56 | To use a different extension for your templates, for example to get better syntax highlighting in your IDE, replace `'dot'` with your extension of choice. See express' [documentation](https://expressjs.com/en/guide/using-template-engines.html) 57 | ```javascript 58 | app.engine('html', engine.__express); 59 | app.set('views', path.join(__dirname, './views')); 60 | app.set('view engine', 'html'); 61 | ``` 62 | 63 | ## Settings 64 | 65 | By default, the engine uses `[[ ]]` instead of `{{ }}` on the backend. This allows the use of front-end templating libraries that already use `{{ }}`. 66 | 67 | ``` 68 | [[ ]] for evaluation 69 | [[= ]] for interpolation 70 | [[! ]] for interpolation with encoding 71 | [[# ]] for compile-time evaluation/includes and partials 72 | [[## #]] for compile-time defines 73 | [[? ]] for conditionals 74 | [[~ ]] for array iteration 75 | ``` 76 | 77 | If you want to configure this you can change the exposed [doT settings](http://olado.github.io/doT/). 78 | 79 | ```javascript 80 | // doT settings 81 | engine.settings.dot = { 82 | evaluate: /\[\[([\s\S]+?)\]\]/g, 83 | interpolate: /\[\[=([\s\S]+?)\]\]/g, 84 | encode: /\[\[!([\s\S]+?)\]\]/g, 85 | use: /\[\[#([\s\S]+?)\]\]/g, 86 | useParams: /(^|[^\w$])def(?:\.|\[[\'\"])([\w$\.]+)(?:[\'\"]\])?\s*\:\s*([\w$\.]+|\"[^\"]+\"|\'[^\']+\'|\[[^\]]+\])/g, 87 | define: /\[\[##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\]\]/g, 88 | defineParams: /^\s*([\w$]+):([\s\S]+)/, 89 | conditional: /\[\[\?(\?)?\s*([\s\S]*?)\s*\]\]/g, 90 | iterate: /\[\[~\s*(?:\]\]|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\]\])/g, 91 | varname: 'layout, partial, locals, model', 92 | strip: false, 93 | append: true, 94 | selfcontained: false, 95 | doNotSkipEncoded: false 96 | }; 97 | ``` 98 | 99 | ## Layout 100 | 101 | You can specify the layout using [yaml](http://yaml.org/) and refer to the section as you would from a model. 102 | 103 | You can also define any extra configurations (like a page title) that are inherited to the master. 104 | 105 | ### Multiple section support 106 | 107 | `master.dot` 108 | 109 | ```html 110 | 111 | 112 | 113 | [[= layout.title ]] 114 | 115 | 116 | Hello from master.dot
117 | [[= layout.section1 ]]
118 | [[= layout.section2 ]] 119 | 120 | 121 | ``` 122 | 123 | `index.dot` 124 | ```html 125 | --- 126 | layout: master.dot 127 | title: Index page 128 | --- 129 | 130 | [[##section1: 131 | Hello from index.dot 132 | #]] 133 | 134 | [[##section2: 135 | Hello from index.dot again 136 | #]] 137 | ``` 138 | 139 | #### Result 140 | ```html 141 | 142 | 143 | 144 | Index page 145 | 146 | 147 | Hello from master.dot
148 | Hello from index.dot
149 | Hello from index.dot again 150 | 151 | 152 | ``` 153 | 154 | ### Cascading layout support 155 | 156 | `CEO.dot` 157 | 158 | ```html 159 | 160 | 161 | 162 | [[= layout.title ]] 163 | 164 | 165 | Hello from CEO.dot
166 | [[= layout.section ]] 167 | 168 | 169 | ``` 170 | 171 | `Boss.dot` 172 | 173 | ```html 174 | --- 175 | layout: ceo.dot 176 | --- 177 | 178 | [[##section: 179 | Hello from Boss.dot
180 | [[= layout.section ]] 181 | #]] 182 | ``` 183 | 184 | `me.dot` 185 | 186 | ```html 187 | --- 188 | layout: boss.dot 189 | title: Page title 190 | --- 191 | 192 | [[##section: 193 | Hello from me.dot 194 | #]] 195 | ``` 196 | 197 | #### Result 198 | ```html 199 | 200 | 201 | 202 | Boss page 203 | 204 | 205 | Hello from CEO.dot
206 | Hello from Boss.dot
207 | Hello from me.dot 208 | 209 | 210 | ``` 211 | 212 | ## Partials 213 | 214 | Partials are supported. The path is relative to the path of the current file. 215 | 216 | `index.dot` 217 | 218 | ```html 219 |
220 | Message from partial: [[= partial('partials/hello.dot') ]] 221 |
222 | ``` 223 | 224 | `partials/hello.dot` 225 | 226 | ```html 227 | Hello from partial 228 | ``` 229 | 230 | ### Result 231 | 232 | ```html 233 |
234 | My partial says: Hello from partial 235 |
236 | ``` 237 | 238 | ## Server model 239 | 240 | In your node application, the model passed to the engine will be available through `[[= model. ]]` in your template. Layouts and Partials also has access to the server models. 241 | 242 | `server.js` 243 | ```javascript 244 | app.get('/', function(req, res){ 245 | res.render('index', { fromServer: 'Hello from server', }); 246 | }); 247 | ``` 248 | 249 | `view.dot` 250 | ```html 251 |
252 | Server says: [[= model.fromServer ]] 253 |
254 | ``` 255 | 256 | ### Result 257 | 258 | ```html 259 |
260 | Server says: Hello from server 261 |
262 | ``` 263 | 264 | > Pro tip 265 | 266 | If you want to make the whole model available in the client (to use in angular for example), you can render the model as JSON in a variable on the view. 267 | 268 | ```html 269 | 272 | ``` 273 | 274 | ## Helper 275 | 276 | You can provide custom helper properties or methods to your views. 277 | 278 | `server` 279 | 280 | ```javascript 281 | var engine = require('express-dot-engine'); 282 | 283 | engine.helper.myHelperProperty = 'Hello from server property helper'; 284 | 285 | engine.helper.myHelperMethod = function(param) { 286 | 287 | // you have access to the server model 288 | var message = this.model.fromServer; 289 | 290 | // .. any logic you want 291 | return 'Server model: ' + message; 292 | } 293 | 294 | ... 295 | 296 | app.get('/', function(req, res) { 297 | res.render('helper/index', { fromServer: 'Hello from server', }); 298 | }); 299 | 300 | ``` 301 | 302 | `view` 303 | 304 | ```html 305 | 306 | 307 | 308 | Helper example 309 | 310 | 311 | 312 | model: [[= model.fromServer ]]
313 | helper property: [[# def.myHelperProperty ]]
314 | helper method: [[# def.myHelperMethod('Hello as a parameter') ]]
315 | helper in view: [[# def.helperInView ]] 316 | 317 | 318 | 319 | 320 | [[##def.helperInView: 321 | Hello from view helper ([[= model.fromServer ]]) 322 | #]] 323 | 324 | ``` 325 | 326 | ## Templating for email (or anything) 327 | 328 | - `render(filename, model, [callback])` 329 | - `renderString(templateStr, model, [callback])` 330 | 331 | The callback is optional. The callback is in node style `function(err, result) {}` 332 | 333 | Example 334 | ```javascript 335 | var engine = require('express-dot-engine'); 336 | var model = { message: 'Hello', }; 337 | 338 | // render from a file 339 | var rendered = engine.render('path/to/file', model); 340 | email.send('Subject', rendered); 341 | 342 | // async render from template string 343 | engine.renderString( 344 | '
[[= model.message ]]
', 345 | model, 346 | function(err, rendered) { 347 | email.send('Subject', rendered); 348 | } 349 | ); 350 | 351 | ... 352 | ``` 353 | 354 | ## Custom template provider 355 | 356 | You can provide a custom template provider 357 | 358 | `server` 359 | 360 | ```javascript 361 | 362 | function getTemplate(name, options, callback) { 363 | var isAsync = callback && typeof callback === 'function', 364 | template = '
custom template, you can store templates in the database
'; 365 | if(!isAsync){ 366 | return template; 367 | } 368 | callback(null, template); 369 | }; 370 | 371 | app.get('/', function(req, res) { 372 | res.render('helper/index', { getTemplate: getTemplate, }); 373 | }); 374 | 375 | ``` 376 | 377 | ## Caching 378 | 379 | Caching is enabled when express is running in production via the 'view cache' variable in express. This is done automatically. If you want to enable cache in development, you can add this 380 | 381 | ```javascript 382 | app.set('view cache', true); 383 | ``` 384 | 385 | ## How to run the examples 386 | 387 | ### 1. Install express-dot-engine 388 | 389 | ```sh 390 | $ npm install express-dot-engine 391 | ``` 392 | 393 | ### 2. Install express 394 | 395 | ```sh 396 | $ npm install express 397 | ``` 398 | 399 | ### 3. Run the example 400 | 401 | ```sh 402 | $ node examples 403 | ``` 404 | 405 | Open your browser to `http://localhost:2015` 406 | 407 | ## Roadmap 408 | 409 | - Move to ES6+ 410 | 411 | ## License 412 | [MIT](LICENSE) 413 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var fs = require('fs'); 3 | var dot = require('dot'); 4 | var path = require('path'); 5 | var yaml = require('js-yaml'); 6 | 7 | /** 8 | * Engine settings 9 | */ 10 | var settings = { 11 | config: /^---([\s\S]+?)---/g, 12 | comment: //g, 13 | header: '', 14 | 15 | stripComment: false, 16 | stripWhitespace: false, // shortcut to dot.strip 17 | 18 | dot: { 19 | evaluate: /\[\[([\s\S]+?)]]/g, 20 | interpolate: /\[\[=([\s\S]+?)]]/g, 21 | encode: /\[\[!([\s\S]+?)]]/g, 22 | use: /\[\[#([\s\S]+?)]]/g, 23 | useParams: /(^|[^\w$])def(?:\.|\[[\'\"])([\w$\.]+)(?:[\'\"]\])?\s*\:\s*([\w$\.]+|\"[^\"]+\"|\'[^\']+\'|\[[^\]]+\])/g, 24 | define: /\[\[##\s*([\w\.$]+)\s*(:|=)([\s\S]+?)#]]/g, 25 | defineParams: /^\s*([\w$]+):([\s\S]+)/, 26 | conditional: /\[\[\?(\?)?\s*([\s\S]*?)\s*]]/g, 27 | iterate: /\[\[~\s*(?:]]|([\s\S]+?)\s*:\s*([\w$]+)\s*(?::\s*([\w$]+))?\s*]])/g, 28 | varname: 'layout, partial, locals, model', 29 | strip: false, 30 | append: true, 31 | selfcontained: false, 32 | doNotSkipEncoded: false 33 | } 34 | }; 35 | 36 | /** 37 | * Cache store 38 | */ 39 | var cache = { 40 | cache: {}, 41 | 42 | get: function(key) { 43 | return this.cache[key]; 44 | }, 45 | set: function(key, value) { 46 | this.cache[key] = value; 47 | }, 48 | clear: function() { 49 | this.cache = {}; 50 | } 51 | }; 52 | 53 | /** 54 | * Server-side helper 55 | */ 56 | function DotDef(options) { 57 | this.options = options; 58 | this.dirname = options.dirname; 59 | this.model = options; 60 | } 61 | 62 | DotDef.prototype = { 63 | 64 | partial: function(partialPath) { 65 | 66 | console.log('DEPRECATED: ' + 67 | 'Please use the new syntax for partials' + 68 | ' [[= partial(\'path/to/partial\') ]]' 69 | ); 70 | 71 | var template = getTemplate( 72 | path.join(this.dirname || this.model.settings.views, partialPath), 73 | this.model 74 | ); 75 | 76 | return template.render({ model: this.model, isPartial: true, } ); 77 | } 78 | 79 | }; 80 | 81 | /** 82 | * @constructor Template object with a layout structure. This object is cached 83 | * if the 'options.cache' set by express is true. 84 | * @param {Object} options The constructor parameters: 85 | * 86 | * {Object} engine The option from the engine 87 | * 88 | * There are 2 options 89 | * 90 | * Case 1: A layout view 91 | * {String} master The master template filename 92 | * {Object} sections A key/value containing the sections of the template 93 | * 94 | * Case 2: A standalone view 95 | * {String} body The template string 96 | */ 97 | function Template(options) { 98 | this.options = options; 99 | 100 | // layout 101 | this.isLayout = !!options.config.layout; 102 | this.master = this.isLayout ? 103 | path.join(options.dirname, options.config.layout) : 104 | null; 105 | 106 | // build the doT templates 107 | this.templates = {}; 108 | this.settings = _.clone(settings.dot); 109 | this.def = new DotDef(options); 110 | 111 | // view data 112 | this.viewData = []; 113 | if (_.has(options.express, 'settings') 114 | && _.has(options.express.settings, 'view data') 115 | ) { 116 | this.settings.varname = _.reduce( 117 | options.express.settings['view data'], 118 | function(result, value, key) { 119 | this.viewData.push(value); 120 | return result + ', ' + key; 121 | }, 122 | settings.dot.varname, 123 | this 124 | ); 125 | } 126 | 127 | // view shortcut 128 | this.shortcuts = []; 129 | if (_.has(options.express, 'settings') 130 | && _.has(options.express.settings, 'view shortcut') 131 | ) { 132 | this.shortcuts = options.express.settings['view shortcut']; 133 | this.settings.varname += ', ' + _.keys(this.shortcuts).join(); 134 | } 135 | 136 | // doT template 137 | for (var key in options.sections) { 138 | if (options.sections.hasOwnProperty(key)) { 139 | this.templates[key] = dot.template( 140 | options.sections[key], 141 | this.settings, 142 | this.def 143 | ); 144 | } 145 | } 146 | } 147 | 148 | /** 149 | * Partial method helper 150 | * @param {Object} layout The layout to pass to the view 151 | * @param {Object} model The model to pass to the view 152 | */ 153 | Template.prototype.createPartialHelper = function(layout, model) { 154 | return function(partialPath) { 155 | var args = [].slice.call(arguments, 1); 156 | var template = getTemplate( 157 | path.join(this.options.dirname || this.options.express.settings.views, partialPath), 158 | this.options.express 159 | ); 160 | 161 | if (args.length) { 162 | model = _.assign.apply(_, [ 163 | {}, 164 | model 165 | ].concat(args)); 166 | } 167 | 168 | return template.render({ layout: layout, model: model, isPartial: true, }); 169 | }.bind(this); 170 | }; 171 | 172 | /** 173 | * Renders the template. 174 | * If callback is passed, it will be called asynchronously. 175 | * @param {Object} options Options to pass to the view 176 | * @param {Object} [options.layout] The layout key/value 177 | * @param {Object} options.model The model to pass to the view 178 | * @param {Function} [callback] (Optional) The async node style callback 179 | */ 180 | Template.prototype.render = function(options, callback) { 181 | var isAsync = callback && typeof callback === 'function'; 182 | var layout = options.layout; 183 | var model = options.model; 184 | var layoutModel = _.merge({}, this.options.config, layout); 185 | 186 | // render the sections 187 | for (var key in this.templates) { 188 | if (this.templates.hasOwnProperty(key)) { 189 | try { 190 | 191 | var viewModel = _.union( 192 | [ 193 | layoutModel, 194 | this.createPartialHelper(layoutModel, model), 195 | options.model._locals || {}, 196 | model 197 | ], 198 | this.viewData, 199 | _.chain(this.shortcuts) 200 | .keys() 201 | .map(function(shortcut) { 202 | return options.model._locals[this.shortcuts[shortcut]] || null; 203 | }, this) 204 | .valueOf() 205 | ); 206 | 207 | layoutModel[key] = this.templates[key].apply( 208 | this.templates[key], 209 | viewModel 210 | ); 211 | } 212 | catch (err) { 213 | var error = new Error( 214 | 'Failed to render with doT' + 215 | ' (' + this.options.filename + ')' + 216 | ' - ' + err.toString() 217 | ); 218 | 219 | if (isAsync) { 220 | callback(error); 221 | return; 222 | } 223 | throw error; 224 | } 225 | } 226 | } 227 | 228 | // no layout 229 | if (!this.isLayout) { 230 | 231 | // append the header to the master page 232 | var result = (!options.isPartial ? settings.header : '') + layoutModel.body; 233 | 234 | if (isAsync) { 235 | callback(null, result); 236 | } 237 | return result; 238 | } 239 | 240 | // render the master sync 241 | if (!isAsync) { 242 | var masterTemplate = getTemplate(this.master, this.options.express); 243 | return masterTemplate.render({ layout: layoutModel, model: model, }); 244 | } 245 | 246 | // render the master async 247 | getTemplate(this.master, this.options.express, function(err, masterTemplate) { 248 | if (err) { 249 | callback(err); 250 | return; 251 | } 252 | 253 | return masterTemplate.render({ layout: layoutModel, model: model, }, callback); 254 | }); 255 | }; 256 | 257 | /** 258 | * Retrieves a template given a filename. 259 | * Uses cache for optimization (if options.cache is true). 260 | * If callback is passed, it will be called asynchronously. 261 | * @param {String} filename The path to the template 262 | * @param {Object} options The option sent by express 263 | * @param {Function} [callback] (Optional) The async node style callback 264 | */ 265 | function getTemplate(filename, options, callback) { 266 | 267 | // cache 268 | if (options && options.cache) { 269 | var fromCache = cache.get(filename); 270 | if (fromCache) { 271 | //console.log('cache hit'); 272 | return callback(null, fromCache); 273 | } 274 | //console.log('cache miss'); 275 | } 276 | 277 | var isAsync = callback && typeof callback === 'function'; 278 | 279 | // function to call when retieved template 280 | function done(err, template) { 281 | 282 | // cache 283 | if (options && options.cache && template) { 284 | cache.set(filename, template); 285 | } 286 | 287 | if (isAsync) { 288 | callback(err, template); 289 | } 290 | 291 | return template; 292 | } 293 | 294 | // sync 295 | if (!isAsync) { 296 | return done(null, buildTemplate(filename, options)); 297 | } 298 | 299 | // async 300 | buildTemplate(filename, options, done); 301 | } 302 | 303 | 304 | /** 305 | * Builds a template 306 | * If callback is passed, it will be called asynchronously. 307 | * @param {String} filename The path or the name to the template 308 | * @param {Object} options The options sent by express 309 | * @param {Function} callback (Optional) The async node style callback 310 | */ 311 | function buildTemplate(filename, options, callback) { 312 | var isAsync = callback && typeof callback === 'function', 313 | getTemplateContentFn = options.getTemplate && typeof options.getTemplate === 'function' ? options.getTemplate : getTemplateContentFromFile; 314 | 315 | // sync 316 | if (!isAsync) { 317 | return builtTemplateFromString( 318 | getTemplateContentFn(filename, options), 319 | filename, 320 | options 321 | ); 322 | } 323 | 324 | // function to call when retrieved template content 325 | function done(err, templateText) { 326 | if (err) { 327 | return callback(err); 328 | } 329 | callback(null, builtTemplateFromString(templateText, filename, options)); 330 | } 331 | 332 | getTemplateContentFn(filename, options, done); 333 | } 334 | 335 | /** 336 | * Gets the template content from a file 337 | * If callback is passed, it will be called asynchronously. 338 | * @param {String} filename The path to the template 339 | * @param {Object} options The options sent by express 340 | * @param {Function} callback (Optional) The async node style callback 341 | */ 342 | function getTemplateContentFromFile(filename, options, callback) { 343 | var isAsync = callback && typeof callback === 'function'; 344 | 345 | // sync 346 | if (!isAsync) { 347 | return fs.readFileSync(filename, 'utf8'); 348 | } 349 | 350 | // async 351 | fs.readFile(filename, 'utf8', function(err, str) { 352 | if (err) { 353 | callback(new Error('Failed to open view file (' + filename + ')')); 354 | return; 355 | } 356 | 357 | try { 358 | callback(null, str); 359 | } 360 | catch (err) { 361 | callback(err); 362 | } 363 | }); 364 | } 365 | 366 | /** 367 | * Builds a template from a string 368 | * @param {String} str The template string 369 | * @param {String} filename The path to the template 370 | * @param {Object} options The options sent by express 371 | * @return {Template} The template object 372 | */ 373 | function builtTemplateFromString(str, filename, options) { 374 | 375 | try { 376 | var config = {}; 377 | 378 | // config at the beginning of the file 379 | str.replace(settings.config, function(m, conf) { 380 | config = yaml.safeLoad(conf); 381 | }); 382 | 383 | // strip comments 384 | if (settings.stripComment) { 385 | str = str.replace(settings.comment, function(m, code, assign, value) { 386 | return ''; 387 | }); 388 | } 389 | 390 | // strip whitespace 391 | if (settings.stripWhitespace) { 392 | settings.dot.strip = settings.stripWhitespace; 393 | } 394 | 395 | // layout sections 396 | var sections = {}; 397 | 398 | if (!config.layout) { 399 | sections.body = str; 400 | } 401 | else { 402 | str.replace(settings.dot.define, function(m, code, assign, value) { 403 | sections[code] = value; 404 | }); 405 | } 406 | 407 | var templateSettings = _.pick(options, ['settings']); 408 | options.getTemplate && (templateSettings.getTemplate = options.getTemplate); 409 | return new Template({ 410 | express: templateSettings, 411 | config: config, 412 | sections: sections, 413 | dirname: path.dirname(filename), 414 | filename: filename 415 | }); 416 | } 417 | catch (err) { 418 | throw new Error( 419 | 'Failed to build template' + 420 | ' (' + filename + ')' + 421 | ' - ' + err.toString() 422 | ); 423 | } 424 | } 425 | 426 | /** 427 | * Render a template 428 | * @param {String} filename The path to the file 429 | * @param {Object} options The model to pass to the view 430 | * @param {Function} callback (Optional) The async node style callback 431 | */ 432 | function render(filename, options, callback) { 433 | var isAsync = callback && typeof callback === 'function'; 434 | 435 | if (!isAsync) { 436 | return renderSync(filename, options) 437 | } 438 | 439 | getTemplate(filename, options, function(err, template) { 440 | if (err) { 441 | return callback(err); 442 | } 443 | 444 | template.render({ model: options, }, callback); 445 | }); 446 | } 447 | 448 | /** 449 | * Renders a template sync 450 | * @param {String} filename The path to the file 451 | * @param {Object} options The model to pass to the view 452 | */ 453 | function renderSync(filename, options) { 454 | var template = getTemplate(filename, options); 455 | return template.render({ model: options, }); 456 | } 457 | 458 | /** 459 | * Render directly from a string 460 | * @param {String} templateString The template string 461 | * @param {Object} options The model to pass to the view 462 | * @param {Function} callback (Optional) The async node style callback 463 | */ 464 | function renderString(templateString, options, callback) { 465 | var template = builtTemplateFromString(templateString, '', options); 466 | return template.render({ model: options, }, callback); 467 | } 468 | 469 | module.exports = { 470 | __express: render, 471 | render: render, 472 | renderString: renderString, 473 | cache: cache, 474 | settings: settings, 475 | helper: DotDef.prototype 476 | }; 477 | --------------------------------------------------------------------------------