123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475 |
- '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') %>
- * <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>
- *
- */
- 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 `_<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) {
- 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;
|