UNPKG

express-dot-engine

Version:

Node.js engine using the ultra fast doT templating with support for layouts, partials and friendly for front-end web libraries (Angular, Ember, Backbone...)

474 lines (408 loc) 11.9 kB
var _ = require('lodash'); var fs = require('fs'); var dot = require('dot'); var path = require('path'); var yaml = require('js-yaml'); /** * Engine settings */ var settings = { config: /^---([\s\S]+?)---/g, comment: /<!--([\s\S]+?)-->/g, header: '', stripComment: false, stripWhitespace: false, // shortcut to dot.strip dot: { evaluate: /\[\[([\s\S]+?)]]/g, interpolate: /\[\[=([\s\S]+?)]]/g, encode: /\[\[!([\s\S]+?)]]/g, use: /\[\[#([\s\S]+?)]]/g, define: /\[\[##\s*([\w\.$]+)\s*(:|=)([\s\S]+?)#]]/g, conditional: /\[\[\?(\?)?\s*([\s\S]*?)\s*]]/g, iterate: /\[\[~\s*(?:]]|([\s\S]+?)\s*:\s*([\w$]+)\s*(?::\s*([\w$]+))?\s*]])/g, varname: 'layout, partial, locals, model', strip: false, append: true, selfcontained: false } }; /** * Cache store */ var cache = { cache: {}, get: function(key) { return this.cache[key]; }, set: function(key, value) { this.cache[key] = value; }, clear: function() { this.cache = {}; } }; /** * Server-side helper */ function DotDef(options) { this.options = options; this.dirname = options.dirname; this.model = options; } DotDef.prototype = { partial: function(partialPath) { console.log('DEPRECATED: ' + 'Please use the new syntax for partials' + ' [[= partial(\'path/to/partial\') ]]' ); var template = getTemplate( path.join(this.dirname || this.model.settings.views, partialPath), this.model ); return template.render({ model: this.model, isPartial: true, } ); } }; /** * @constructor Template object with a layout structure. This object is cached * if the 'options.cache' set by express is true. * @param {Object} options The constructor parameters: * * {Object} engine The option from the engine * * There are 2 options * * Case 1: A layout view * {String} master The master template filename * {Object} sections A key/value containing the sections of the template * * Case 2: A standalone view * {String} body The template string */ function Template(options) { this.options = options; // layout this.isLayout = !!options.config.layout; this.master = this.isLayout ? path.join(options.dirname, options.config.layout) : null; // build the doT templates this.templates = {}; this.settings = _.clone(settings.dot); this.def = new DotDef(options); // view data this.viewData = []; if (_.has(options.express, 'settings') && _.has(options.express.settings, 'view data') ) { this.settings.varname = _.reduce( options.express.settings['view data'], function(result, value, key) { this.viewData.push(value); return result + ', ' + key; }, settings.dot.varname, this ); } // view shortcut this.shortcuts = []; if (_.has(options.express, 'settings') && _.has(options.express.settings, 'view shortcut') ) { this.shortcuts = options.express.settings['view shortcut']; this.settings.varname += ', ' + _.keys(this.shortcuts).join(); } // doT template for (var key in options.sections) { if (options.sections.hasOwnProperty(key)) { this.templates[key] = dot.template( options.sections[key], this.settings, this.def ); } } } /** * Partial method helper * @param {Object} layout The layout to pass to the view * @param {Object} model The model to pass to the view */ Template.prototype.createPartialHelper = function(layout, model) { return function(partialPath) { var args = [].slice.call(arguments, 1); var template = getTemplate( path.join(this.options.dirname || this.options.express.settings.views, partialPath), this.options.express ); if (args.length) { model = _.assign.apply(_, [ {}, model ].concat(args)); } return template.render({ layout: layout, model: model, isPartial: true, }); }.bind(this); }; /** * Renders the template. * If callback is passed, it will be called asynchronously. * @param {Object} options Options to pass to the view * @param {Object} [options.layout] The layout key/value * @param {Object} options.model The model to pass to the view * @param {Function} [callback] (Optional) The async node style callback */ Template.prototype.render = function(options, callback) { var isAsync = callback && typeof callback === 'function'; var layout = options.layout; var model = options.model; var layoutModel = _.merge({}, this.options.config, layout); // render the sections for (var key in this.templates) { if (this.templates.hasOwnProperty(key)) { try { var viewModel = _.union( [ layoutModel, this.createPartialHelper(layoutModel, model), options.model._locals || {}, model ], this.viewData, _.chain(this.shortcuts) .keys() .map(function(shortcut) { return options.model._locals[this.shortcuts[shortcut]] || null; }, this) .valueOf() ); layoutModel[key] = this.templates[key].apply( this.templates[key], viewModel ); } catch (err) { var error = new Error( 'Failed to render with doT' + ' (' + this.options.filename + ')' + ' - ' + err.toString() ); if (isAsync) { callback(error); return; } throw error; } } } // no layout if (!this.isLayout) { // append the header to the master page var result = (!options.isPartial ? settings.header : '') + layoutModel.body; if (isAsync) { callback(null, result); } return result; } // render the master sync if (!isAsync) { var masterTemplate = getTemplate(this.master, this.options.express); return masterTemplate.render({ layout: layoutModel, model: model, }); } // render the master async getTemplate(this.master, this.options.express, function(err, masterTemplate) { if (err) { callback(err); return; } return masterTemplate.render({ layout: layoutModel, model: model, }, callback); }); }; /** * Retrieves a template given a filename. * Uses cache for optimization (if options.cache is true). * If callback is passed, it will be called asynchronously. * @param {String} filename The path to the template * @param {Object} options The option sent by express * @param {Function} [callback] (Optional) The async node style callback */ function getTemplate(filename, options, callback) { // cache if (options && options.cache) { var fromCache = cache.get(filename); if (fromCache) { //console.log('cache hit'); return callback(null, fromCache); } //console.log('cache miss'); } var isAsync = callback && typeof callback === 'function'; // function to call when retieved template function done(err, template) { // cache if (options && options.cache && template) { cache.set(filename, template); } if (isAsync) { callback(err, template); } return template; } // sync if (!isAsync) { return done(null, buildTemplate(filename, options)); } // async buildTemplate(filename, options, done); } /** * Builds a template * If callback is passed, it will be called asynchronously. * @param {String} filename The path or the name to the template * @param {Object} options The options sent by express * @param {Function} callback (Optional) The async node style callback */ function buildTemplate(filename, options, callback) { var isAsync = callback && typeof callback === 'function', getTemplateContentFn = options.getTemplate && typeof options.getTemplate === 'function' ? options.getTemplate : getTemplateContentFromFile; // sync if (!isAsync) { return builtTemplateFromString( getTemplateContentFn(filename, options), filename, options ); } // function to call when retrieved template content function done(err, templateText) { if (err) { return callback(err); } callback(null, builtTemplateFromString(templateText, filename, options)); } getTemplateContentFn(filename, options, done); } /** * Gets the template content from a file * If callback is passed, it will be called asynchronously. * @param {String} filename The path to the template * @param {Object} options The options sent by express * @param {Function} callback (Optional) The async node style callback */ function getTemplateContentFromFile(filename, options, callback) { var isAsync = callback && typeof callback === 'function'; // sync if (!isAsync) { return fs.readFileSync(filename, 'utf8'); } // async fs.readFile(filename, 'utf8', function(err, str) { if (err) { callback(new Error('Failed to open view file (' + filename + ')')); return; } try { callback(null, str); } catch (err) { callback(err); } }); } /** * Builds a template from a string * @param {String} str The template string * @param {String} filename The path to the template * @param {Object} options The options sent by express * @return {Template} The template object */ function builtTemplateFromString(str, filename, options) { try { var config = {}; // config at the beginning of the file str.replace(settings.config, function(m, conf) { config = yaml.safeLoad(conf); }); // strip comments if (settings.stripComment) { str = str.replace(settings.comment, function(m, code, assign, value) { return ''; }); } // strip whitespace if (settings.stripWhitespace) { settings.dot.strip = settings.stripWhitespace; } // layout sections var sections = {}; if (!config.layout) { sections.body = str; } else { str.replace(settings.dot.define, function(m, code, assign, value) { sections[code] = value; }); } var templateSettings = _.pick(options, ['settings']); options.getTemplate && (templateSettings.getTemplate = options.getTemplate); return new Template({ express: templateSettings, config: config, sections: sections, dirname: path.dirname(filename), filename: filename }); } catch (err) { throw new Error( 'Failed to build template' + ' (' + filename + ')' + ' - ' + err.toString() ); } } /** * Render a template * @param {String} filename The path to the file * @param {Object} options The model to pass to the view * @param {Function} callback (Optional) The async node style callback */ function render(filename, options, callback) { var isAsync = callback && typeof callback === 'function'; if (!isAsync) { return renderSync(filename, options) } getTemplate(filename, options, function(err, template) { if (err) { return callback(err); } template.render({ model: options, }, callback); }); } /** * Renders a template sync * @param {String} filename The path to the file * @param {Object} options The model to pass to the view */ function renderSync(filename, options) { var template = getTemplate(filename, options); return template.render({ model: options, }); } /** * Render directly from a string * @param {String} templateString The template string * @param {Object} options The model to pass to the view * @param {Function} callback (Optional) The async node style callback */ function renderString(templateString, options, callback) { var template = builtTemplateFromString(templateString, '', options); return template.render({ model: options, }, callback); } module.exports = { __express: render, render: render, renderString: renderString, cache: cache, settings: settings, helper: DotDef.prototype };