Jannick Knudsen 5 years ago
parent
commit
760de93068
2 changed files with 500 additions and 0 deletions
  1. 25 0
      block.js
  2. 475 0
      index.js

+ 25 - 0
block.js

@@ -0,0 +1,25 @@
+'use strict';
+
+class Block {
+  constructor() {
+    this.html = [];
+  }
+
+  toString() {
+    return this.html.join('\n');
+  }
+
+  append(more) {
+    this.html.push(more);
+  }
+
+  prepend(more) {
+    this.html.unshift(more);
+  }
+
+  replace(instead) {
+    this.html = [ instead ];
+  }
+}
+
+module.exports = Block;

+ 475 - 0
index.js

@@ -0,0 +1,475 @@
+'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;