├── .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 | [](https://github.com/8lueberry/express-dot-engine/releases)
4 | [](https://npmjs.com/package/express-dot-engine)
5 | [](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 |
--------------------------------------------------------------------------------