index.js
12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
var ejs = require('ejs')
, fs = require('fs')
, path = require('path')
, exists = fs.existsSync || path.existsSync
, resolve = path.resolve
, extname = path.extname
, dirname = path.dirname
, join = path.join
, basename = path.basename;
/**
* Express 3.x Layout & Partial support for EJS.
*
* The `partial` feature from Express 2.x is back as a template engine,
* along with support for `layout` and `block/script/stylesheet`.
*
*
* Example index.ejs:
*
* <% layout('boilerplate') %>
* <h1>I am the <%=what%> template</h1>
* <% script('foo.js') %>
*
*
* Example boilerplate.ejs:
*
* <html>
* <head>
* <title>It's <%=who%></title>
* <%-scripts%>
* </head>
* <body><%-body%></body>
* </html>
*
*
* Sample app:
*
* var express = require('express')
* , app = express();
*
* // use ejs-locals for all ejs templates:
* app.engine('ejs', require('ejs-locals'));
*
* // render 'index' into 'boilerplate':
* app.get('/',function(req,res,next){
* res.render('index', { what: 'best', who: 'me' });
* });
*
* app.listen(3000);
*
* Example output for GET /:
*
* <html>
* <head>
* <title>It's me</title>
* <script src="foo.js"></script>
* </head>
* <body><h1>I am the best template</h1></body>
* </html>
*
*/
var renderFile = module.exports = function(file, options, fn){
// Express used to set options.locals for us, but now we do it ourselves
// (EJS does some __proto__ magic to expose these funcs/values in the template)
if (!options.locals) {
options.locals = {};
}
if (!options.locals.blocks) {
// one set of blocks no matter how often we recurse
var blocks = { scripts: new Block(), stylesheets: new Block() };
options.locals.blocks = blocks;
options.locals.scripts = blocks.scripts;
options.locals.stylesheets = blocks.stylesheets;
options.locals.block = block.bind(blocks);
options.locals.stylesheet = stylesheet.bind(blocks.stylesheets);
options.locals.script = script.bind(blocks.scripts);
}
// override locals for layout/partial bound to current options
options.locals.layout = layout.bind(options);
options.locals.partial = partial.bind(options);
ejs.renderFile(file, options, function(err, html) {
if (err) {
return fn(err,html);
}
var layout = options.locals._layoutFile;
// for backward-compatibility, allow options to
// set a default layout file for the view or the app
// (NB:- not called `layout` any more so it doesn't
// conflict with the layout() function)
if (layout === undefined) {
layout = options._layoutFile;
}
if (layout) {
// use default extension
var engine = options.settings['view engine'] || 'ejs',
desiredExt = '.'+engine;
// apply default layout if only "true" was set
if (layout === true) {
layout = path.sep + 'layout' + desiredExt;
}
if (extname(layout) !== desiredExt) {
layout += desiredExt;
}
// clear to make sure we don't recurse forever (layouts can be nested)
delete options.locals._layoutFile;
delete options._layoutFile;
// make sure caching works inside ejs.renderFile/render
delete options.filename;
if (layout.length > 0 && layout[0] === path.sep) {
// if layout is an absolute path, find it relative to view options:
layout = join(options.settings.views, layout.slice(1));
} else {
// otherwise, find layout path relative to current template:
layout = resolve(dirname(file), layout);
}
// now recurse and use the current result as `body` in the layout:
options.locals.body = html;
renderFile(layout, options, fn);
} else {
// no layout, just do the default:
fn(null, html);
}
});
};
/**
* Memory cache for resolved object names.
*/
var cache = {};
/**
* Resolve partial object name from the view path.
*
* Examples:
*
* "user.ejs" becomes "user"
* "forum thread.ejs" becomes "forumThread"
* "forum/thread/post.ejs" becomes "post"
* "blog-post.ejs" becomes "blogPost"
*
* @return {String}
* @api private
*/
function resolveObjectName(view){
return cache[view] || (cache[view] = view
.split('/')
.slice(-1)[0]
.split('.')[0]
.replace(/^_/, '')
.replace(/[^a-zA-Z0-9 ]+/g, ' ')
.split(/ +/).map(function(word, i){
return i ? word[0].toUpperCase() + word.substr(1) : word;
}).join(''));
}
/**
* Lookup partial path from base path of current template:
*
* - partial `_<name>`
* - any `<name>/index`
* - non-layout `../<name>/index`
* - any `<root>/<name>`
* - partial `<root>/_<name>`
*
* Options:
*
* - `cache` store the resolved path for the view, to avoid disk I/O
*
* @param {String} root, full base path of calling template
* @param {String} partial, name of the partial to lookup (can be a relative path)
* @param {Object} options, for `options.cache` behavior
* @return {String}
* @api private
*/
function lookup(root, partial, options){
var engine = options.settings['view engine'] || 'ejs'
, desiredExt = '.' + engine
, ext = extname(partial) || desiredExt
, key = [ root, partial, ext ].join('-');
if (options.cache && cache[key]) return cache[key];
// Make sure we use dirname in case of relative partials
// ex: for partial('../user') look for /path/to/root/../user.ejs
var dir = dirname(partial)
, base = basename(partial, ext);
// _ prefix takes precedence over the direct path
// ex: for partial('user') look for /root/_user.ejs
partial = resolve(root, dir,'_'+base+ext);
if( exists(partial) ) return options.cache ? cache[key] = partial : partial;
// Try the direct path
// ex: for partial('user') look for /root/user.ejs
partial = resolve(root, dir, base+ext);
if( exists(partial) ) return options.cache ? cache[key] = partial : partial;
// Try index
// ex: for partial('user') look for /root/user/index.ejs
partial = resolve(root, dir, base, 'index'+ext);
if( exists(partial) ) return options.cache ? cache[key] = partial : partial;
// FIXME:
// * there are other path types that Express 2.0 used to support but
// the structure of the lookup involved View class methods that we
// don't have access to any more
// * we probaly need to pass the Express app's views folder path into
// this function if we want to support finding partials relative to
// it as well as relative to the current view
// * we have no tests for finding partials that aren't relative to
// the calling view
return null;
}
/**
* Render `view` partial with the given `options`. Optionally a
* callback `fn(err, str)` may be passed instead of writing to
* the socket.
*
* Options:
*
* - `object` Single object with name derived from the view (unless `as` is present)
*
* - `as` Variable name for each `collection` value, defaults to the view name.
* * as: 'something' will add the `something` local variable
* * as: this will use the collection value as the template context
* * as: global will merge the collection value's properties with `locals`
*
* - `collection` Array of objects, the name is derived from the view name itself.
* For example _video.html_ will have a object _video_ available to it.
*
* @param {String} view
* @param {Object|Array} options, collection or object
* @return {String}
* @api private
*/
function partial(view, options){
var collection
, object
, locals
, name;
// parse options
if( options ){
// collection
if( options.collection ){
collection = options.collection;
delete options.collection;
} else if( 'length' in options ){
collection = options;
options = {};
}
// locals
if( options.locals ){
locals = options.locals;
delete options.locals;
}
// object
if( 'Object' != options.constructor.name ){
object = options;
options = {};
} else if( options.object !== undefined ){
object = options.object;
delete options.object;
}
} else {
options = {};
}
// merge locals into options
if( locals )
options.__proto__ = locals;
// merge app locals into options
for(var k in this)
options[k] = options[k] || this[k];
// extract object name from view
name = options.as || resolveObjectName(view);
// find view, relative to this filename
// (FIXME: filename is set by ejs engine, other engines may need more help)
var root = dirname(options.filename)
, file = lookup(root, view, options)
, key = file + ':string';
if( !file )
throw new Error('Could not find partial ' + view);
// read view
var source = options.cache
? cache[key] || (cache[key] = fs.readFileSync(file, 'utf8'))
: fs.readFileSync(file, 'utf8');
options.filename = file;
// re-bind partial for relative partial paths
options.partial = partial.bind(options);
// render partial
function render(){
if (object) {
if ('string' == typeof name) {
options[name] = object;
} else if (name === global) {
// wtf?
// merge(options, object);
}
}
// TODO Support other templates (but it's sync now...)
var html = ejs.render(source, options);
return html;
}
// Collection support
if (collection) {
var len = collection.length
, buf = ''
, keys
, prop
, val
, i;
if ('number' == typeof len || Array.isArray(collection)) {
options.collectionLength = len;
for (i = 0; i < len; ++i) {
val = collection[i];
options.firstInCollection = i === 0;
options.indexInCollection = i;
options.lastInCollection = i === len - 1;
object = val;
buf += render();
}
} else {
keys = Object.keys(collection);
len = keys.length;
options.collectionLength = len;
options.collectionKeys = keys;
for (i = 0; i < len; ++i) {
prop = keys[i];
val = collection[prop];
options.keyInCollection = prop;
options.firstInCollection = i === 0;
options.indexInCollection = i;
options.lastInCollection = i === len - 1;
object = val;
buf += render();
}
}
return buf;
} else {
return render();
}
}
/**
* Apply the given `view` as the layout for the current template,
* using the current options/locals. The current template will be
* supplied to the given `view` as `body`, along with any `blocks`
* added by child templates.
*
* `options` are bound to `this` in renderFile, you just call
* `layout('myview')`
*
* @param {String} view
* @api private
*/
function layout(view){
this.locals._layoutFile = view;
}
function Block() {
this.html = [];
}
Block.prototype = {
toString: function() {
return this.html.join('\n');
},
append: function(more) {
this.html.push(more);
},
prepend: function(more) {
this.html.unshift(more);
},
replace: function(instead) {
this.html = [ instead ];
}
};
/**
* Return the block with the given name, create it if necessary.
* Optionally append the given html to the block.
*
* The returned Block can append, prepend or replace the block,
* as well as render it when included in a parent template.
*
* @param {String} name
* @param {String} html
* @return {Block}
* @api private
*/
function block(name, html) {
// bound to the blocks object in renderFile
var blk = this[name];
if (!blk) {
// always create, so if we request a
// non-existent block we'll get a new one
blk = this[name] = new Block();
}
if (html) {
blk.append(html);
}
return blk;
}
// bound to scripts Block in renderFile
function script(path, type) {
if (path) {
this.append('<script src="'+path+'"'+(type ? 'type="'+type+'"' : '')+'></script>');
}
return this;
}
// bound to stylesheets Block in renderFile
function stylesheet(path, media) {
if (path) {
this.append('<link rel="stylesheet" href="'+path+'"'+(media ? 'media="'+media+'"' : '')+' />');
}
return this;
}