'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;