UNPKG

kiwi

Version:

Simple, modular, fast and lightweight template engine, based on jQuery templates syntax.

359 lines (292 loc) 8.26 kB
/*! * Coolony's Kiwi * Copyright ©2012 Pierre Matri <pierre.matri@coolony.com> * MIT Licensed */ /** * Module dependencies */ // if browser //var Cache = require('./cache'); //var CappedCache = Cache; // end // if node var path = require('path'); var frame = require('frame'); var Cache = frame.Cache; var CappedCache = frame.CappedCache; var construct = frame.classes.construct; var _ = require('underscore'); // end var utils = require('./utils'); var token = require('./token'); var tools = require('./tools'); var Compiler = require('./compiler'); var filter = require('./filter'); /** * Constants */ var DEFAULTS = { lookup: utils.lookupTemplate, load: utils.loadTemplate, path: null, cache: true, cacheHandler: Cache, cacheOptions: [], lookupPaths: [], cacheTmplHandler: CappedCache, cacheTmplOptions: [1000], useIsolatedTmplCache: true, cacheContext: null, strict: true, eachCounters: true, _parent: null, _cacheAttachment: '_cache', _cacheTmplAttachment: '_nestCache' }; var TEMPLATE_EXPORTS = {filter: filter, utils: utils, tools: tools}; /** * Initializes `Template` with optionnally the given `str` and * `options`. * * @param {String} [str] * @param {Object} [options] * @api public */ function Template(str, options) { // Handle the case where the only argument passed is the `options` object if(_.isObject(str) && !options){ options = str; str = null; } // Create options if not provided options = options ? _.clone(options) : {}; // Set default cache behavior // if node if(!_.isBoolean(options.cache)) { options.cache = process.env.NODE_ENV === 'production'; } // end // Merges given `options` with `DEFAULTS` options = _.defaults(options, DEFAULTS); options.cacheContext = options.cacheContext || Template; // Sets instance variables this.template = str; this.options = options; this._compiled = null; // Creates the cache if not already done if(options.cache && !(this._getCache() instanceof options.cacheHandler)) { var cacheOptions = [options.cacheHandler].concat(options.cacheOptions); options.cacheContext[options._cacheProp] = typeof window !== 'undefined' ? new options.cacheHandler() : construct.apply(this, cacheOptions); } } /** * Get cache handler * * @return {Object} * @api private */ Template.prototype._getCache = function() { return this.options.cacheContext[this.options._cacheProp]; }; /** * Load the template from disk at `filePath`, and invoke `callback(err)`. * * @param {String} filePath * @param {Function} callback * @api public */ Template.prototype.loadFile = function(filePath, callback) { //if node var _this = this; this.options.load(filePath, function onLoad(err, data) { if(err) return callback(err); _this.options.path = path.normalize(filePath); _this.template = data; callback(); }); // end // if browser // callback(new Error('Client mode does not support reading from file.')); // end }; /** * Render the template with given `data`, and invoke `callback(err, compiled)`. * * @param {Object} [data] * @param {Function} callback * @api public */ Template.prototype.render = function(data, callback) { // Support callback as 1st arg if(_.isFunction(data) && !callback){ callback = data; data = null; } // Data defaults and / or cloning data = data ? _.clone(data) : data; // Check whether we have the compiled template ready in the object or in cache var cacheKey = 'template::' + this._cacheKey(); if(!this.compiled && this.options.cache) { this._compiled = this._getCache().get(cacheKey); } // Render it if we got it… if(this._compiled) return this._renderCompiled(data, callback); // …or compile it if we don't var _this = this; this._compile(function(err){ if(err) return callback(err); _this._renderCompiled(data, callback); }); }; /** * Load the template from disk at `filePath`, render it with given `data`, * and invoke `callback(err, compiled)`. * * @param {String} filePath * @param {Object} [data] * @param {Function} callback * @api public */ Template.prototype.loadAndRender = function(filePath, data, callback) { // if node var _this = this; // Support callback as 2nd arg if(_.isFunction(data) && !callback){ callback = data; data = {}; } this.loadFile(filePath, function onLoaded(err) { if(err) return callback(err); _this.render(data, callback); }); // end // if browser // callback(new Error('Client mode does not support reading from file.')); // end }; /** * Render the compiled template with given `data`, and invoke * `callback(err, compiled)`. * * @param {String} filePath * @param {Function} callback * @api private */ Template.prototype._renderCompiled = function onRendered(data, callback) { try { // Apply compiled function to itself (needed for function helpers) this._compiled.call(this._compiled, this, TEMPLATE_EXPORTS, _, data, token.helpers, callback); } catch(err) { callback(err); } }; /** * Compile the template, and invoke `callback(err, compiled)`. * * @param {Function} callback * @api private */ Template.prototype._compile = function(callback) { var _this = this; if(!_.isString(this.template)) { return callback(new Error('Template contents not set')); } new Compiler(this).compile(function(err, compiled) { if(err) return callback(err); // Save the compiled template in this object _this._compiled = compiled; // Cache it, if possible var cacheKey = 'template::' + _this._cacheKey(); if(_this.options.cache) { _this._getCache().cache(cacheKey, compiled); } // Invoke the callback callback(null, compiled); }); }; /** * Render `nested` template with `data`, and invoke `callback(err, rendered)`. * * @param {String} nested * @param {Object} data * @param {Function} callback * @api private */ Template.prototype._nest = function(nested, data, callback) { var options = _.clone(this.options); if(options.useIsolatedTmplCache) { options._cacheAttachment = options._cacheTmplAttachment; options.cacheHandler = options.cacheTmplHandler; options.cacheOptions = options.cacheTmplOptions; } new Template(nested, options).render(data, function onDone(err, rendered) { if(err) return callback(err); callback(null, rendered); }); }; /** * Render `nested` template with `data`, and invoke `callback(err, rendered)`. * * @param {String} nested * @param {Object} data * @param {Function} callback * @api private */ Template.prototype._renderRelative = function(name, data, rendered, callback) { var _this = this; var cacheKey; var cachedPath; var template; function onTemplateLocated(err, filePath) { if(err) return callback(err); if(_this.options.cache) { _this._getCache().cache(cacheKey, filePath); } template.loadFile(filePath, onTemplateLoaded); } function onTemplateLoaded(err, source) { if(err) return callback(err); template.render(data, onTemplateRendered); } function onTemplateRendered(err, result) { if(err) return callback(err); callback(null, result); } if(name instanceof Template) { template = name; template.options._parent = rendered; template.render(data, onTemplateRendered); } else { var options = _.clone(this.options); options._parent = rendered; template = new Template(options); cacheKey = 'path::' + this._cacheKey() + '::' + name; // Handle cache if(this.options.cache) { cachedPath = this._getCache().get(cacheKey); } if(cachedPath) { template.loadFile(cachedPath, onTemplateLoaded); } else { this.options.lookup(name, this, onTemplateLocated); } } }; /** * Calculate and return cache key. * * @return {String} Cache key. * @api private */ Template.prototype._cacheKey = function() { return this.options.path || this.template || null; }; /** * Module exports */ module.exports = Template;