'use strict';
const ejs = require('ejs');
const fs = require('fs');
const path = require('path');
const Block = require('./block');
/**
* 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._layoutFile = view;
}
/**
* 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;
}
/**
* 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') %>
*
I am the <%=what%> template
* <% script('foo.js') %>
*
*
* Example boilerplate.ejs:
*
*
*
* It's <%=who%>
* <%-scripts%>
*
* <%-body%>
*
*
*
* 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 /:
*
*
*
* It's me
*
*
* I am the best template
*
*
*/
function compile(file, options, cb) {
// 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 = {};
options.blocks = blocks;
options.block = block.bind(blocks);
}
// override locals for layout/partial bound to current options
options.locals.layout = layout.bind(options);
options.locals.partial = partial.bind(options);
try {
var fn = ejs.compile(file, options);
} catch (ex) {
cb(ex);
return;
}
cb(null, fn.toString());
}
// var renderFile = function (file, locals, options) {
// return new Promise((resolve, reject) => {
// ejs.renderFile(file, locals, options, (err, html) => {
// if (err) {
// return reject(err);
// }
// resolve(html);
// });
// });
// };
var render = function(file, locals, options, fn) {
ejs.renderFile(file, locals, options, function(err, html) {
if (err) {
return fn(err, html);
}
var layout = options._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 (path.extname(layout) !== desiredExt) {
layout += desiredExt;
}
// clear to make sure we don't recurse forever (layouts can be nested)
delete options._layoutFile;
// make sure caching works inside ejs.renderFile/render
delete options.filename;
if (layout.length > 0) {
var views = options.settings.views;
var l = layout;
if (!Array.isArray(views)) {
views = [views];
}
for (var i = 0; i < views.length; i++) {
layout = path.join(views[i], l);
// use the first found layout
if (fs.existsSync(layout)) {
break;
}
}
}
// now recurse and use the current result as `body` in the layout:
options.body = html;
renderFile(layout, options, fn);
} else {
// no layout, just do the default:
fn(null, html);
}
});
};
function renderFile(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 = {};
options.locals.blocks = blocks;
options.block = block.bind(blocks);
}
// override locals for layout/partial bound to current options
options.layout = layout.bind(options);
options.partial = partial.bind(options);
options.filename = file;
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 (path.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
options.filename;
if (layout.length > 0) {
var views = options.settings.views;
var l = layout;
if (!Array.isArray(views)) {
views = [views];
}
for (var i = 0; i < views.length; i++) {
layout = path.join(views[i], l);
// use the first found layout
if (fs.existsSync(layout)) {
break;
}
}
}
// now recurse and use the current result as `body` in the layout:
options.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 `_`
* - any `/index`
* - non-layout `..//index`
* - any `/`
* - partial `/_`
*
* 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) {
const engine = options.settings['view engine'] || 'ejs';
const desiredExt = '.' + engine;
const ext = path.extname(partial) || desiredExt;
const key = [root, partial, ext].join('-');
const partialPath = partial;
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 = path.dirname(partial);
var base = path.basename(partial, ext);
if (!options._isRelativeToViews) {
var views = options.settings.views;
options._isRelativeToViews = true;
if (!Array.isArray(views)) {
views = [views];
}
for (var i = 0; i < views.length; i++) {
partial = lookup(views[i], partialPath, options);
if (partial) {
// reset state for when the partial has a partial lookup of its own
options._isRelativeToViews = false;
return partial;
}
}
}
// _ prefix takes precedence over the direct path
// ex: for partial('user') look for /root/_user.ejs
partial = path.resolve(root, dir, '_' + base + ext);
if (fs.existsSync(partial)) {
return options.cache ? cache[key] = partial : partial;
}
// Try the direct path
// ex: for partial('user') look for /root/user.ejs
partial = path.resolve(root, dir, base + ext);
if (fs.existsSync(partial)) {
return options.cache ? cache[key] = partial : partial;
}
// Try index
// ex: for partial('user') look for /root/user/index.ejs
partial = path.resolve(root, dir, base, 'index' + ext);
if (fs.existsSync(partial)) {
return options.cache ? cache[key] = partial : partial;
}
// Try relative to the app views
// 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 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) {
var collection;
var object;
// find view, relative to this filename
// (FIXME: filename is set by ejs engine, other engines may need more help)
var root = path.dirname(this.filename);
var file = lookup(root, view, this);
var key = file + ':string';
if (!file) {
throw new Error(`Could not find partial '${view}'`);
}
// read view
var source = this.cache ? cache[key] || (cache[key] = fs.readFileSync(file, 'utf8')) : fs.readFileSync(file, 'utf8');
return ejs.render(source, this);
}
renderFile.compile = compile;
renderFile.partial = partial;
renderFile.block = block;
renderFile.layout = layout;
module.exports = renderFile;