RandomEtc / ejs-locals

Express 3.x layout, partial and block template functions for the EJS template engine.
298 stars 63 forks source link

Support for theme folder #24

Open olivercox opened 11 years ago

olivercox commented 11 years ago

I've been trying to find a way of handling template overrides with a theme folder. The project I'm working on will implement themes that can override a default template. I would like to keep the syntax of calling templates so the following statement would look for the file in a theme folder first and load it if exists.

<% layout('layouts/public') -%>

So lets say that the default views folder is 'root/views' and the theme folder is 'root/themes/mytheme'. The engine would try to find 'layouts/public.ejs' relative to the theme folder and the file the template was called from. If it doesn't exist it should attempt to load the from the views folder.

If the template was called from root/views/mymodule/index.ejs it would look first for the file root/themes/mytheme/mymodule/layouts/public.ejs if not found it would look for root/views/layouts/public.ejs

I've found a possible way to handle this by editing the renderFile method in index.js. I'd appreciate any feedback on the change and how one would go about getting it included.

I've added the following lines just before the call to ejs.renderFile()


//Get the theme folder (options.settings['theme'])
    var themeFilePath = dirname(file).replace(resolve(options.settings.views), resolve(options.settings['theme']))
    //If the current file exists reletive to the theme folder set the file to that path
    if (fs.existsSync(resolve(themeFilePath, basename(file)))) file = resolve(themeFilePath, basename(file));`

This relies on the following being set when the view engine is declared, in my case in the server.js:


 app.set('theme', __dirname + [PATH_TO_THEME]);

The ejs-locals renderFile method now looks like:


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

    //Get the theme folder (options.settings['theme'])
    var themeFilePath = dirname(file).replace(resolve(options.settings.views), resolve(options.settings['theme']))
    //If the current file exists reletive to the theme folder set the file to that path
    if (fs.existsSync(resolve(themeFilePath, basename(file)))) file = resolve(themeFilePath, basename(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 (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);
        }
    });

};