UNPKG

templates

Version:

System for creating and managing template collections, and rendering templates with any node.js template engine. Can be used as the basis for creating a static site generator or blog framework.

573 lines (483 loc) 12 kB
'use strict'; var path = require('path'); /** * Lazily required module dependencies */ var lazy = require('lazy-cache')(require); // object/array/type utils lazy('clone'); lazy('is-buffer'); lazy('paginationator'); lazy('array-sort', 'sortBy'); lazy('group-array', 'groupBy'); lazy('define-property', 'define'); lazy('mixin-deep', 'merge'); lazy('extend-shallow', 'extend'); lazy('object.reduce', 'reduce'); // routing lazy('en-route', 'router'); // engines, templates and helpers lazy('load-helpers', 'loader'); lazy('engine-base', 'engine'); lazy('engine-cache', 'Engines'); lazy('helper-cache', 'Helpers'); lazy('template-error', 'rethrow'); lazy('inflection', 'inflect'); lazy('layouts'); lazy('dest'); // glob/matching utils lazy('globby'); lazy('micromatch', 'mm'); lazy('is-valid-glob'); lazy('has-glob'); /** * Utils */ var utils = lazy; /** * Default router methods used in all Template instances */ utils.methods = [ 'onLoad', 'preCompile', 'preLayout', 'onLayout', 'postLayout', 'onMerge', 'postCompile', 'preRender', 'postRender' ]; /** * Options keys */ utils.optsKeys = [ 'renameKey', 'namespaceData', 'mergePartials', 'rethrow', 'nocase', 'nonull', 'rename', 'cwd' ]; /** * FILE / GLOB UTILS * -------------------------------- */ /** * Resolve the absolute file paths for a glob of files. */ utils.resolveGlob = function resolveGlob(patterns, options) { var opts = utils.extend({cwd: process.cwd()}, options); return utils.globby.sync(patterns, opts).map(function (fp) { return path.resolve(opts.cwd, fp); }); }; /** * Require a glob of files */ utils.requireGlob = function requireGlob(patterns, options) { var renameKey = function (key) { return utils.rename(key, options); }; return utils.resolveGlob(patterns, options).reduce(function (acc, fp) { if (/\.(js(?:on)?)/.test(fp)) { var key = renameKey(fp); acc[key] = utils.tryRequire(fp); } return acc; }, {}); }; /** * Require a glob of data */ utils.requireData = function requireData(patterns, opts) { opts.rename = opts.namespaceData || opts.renameKey || function (key) { return path.basename(key, path.extname(key)); }; return utils.requireGlob(patterns, opts); }; /** * Attempt to require a file. Fail silently. */ utils.tryRequire = function tryRequire(fp, opts) { try { return require(fp); } catch(err) { try { opts = opts || {}; fp = path.resolve(fp); return require(fp); } catch(err) {} } return null; }; /** * OBJECT / ARRAY / TYPE UTILS * -------------------------------- */ /** * Format an error object. */ utils.error = function error(msg, val) { return new Error(msg + JSON.stringify(val)); }; /** * Do nothing. */ utils.noop = function noop() {}; /** * Return the given value as-is. */ utils.identity = function identity(val) { return val; }; /** * Arrayify the given value by casting it to an array. */ utils.arrayify = function arrayify(val) { return Array.isArray(val) ? val : [val]; }; /** * Returns true if an array has the given element, or an * object has the given key. * * @return {Boolean} */ utils.has = function has(val, key) { if (Array.isArray(val)) { return val.indexOf(key) > -1; } return val.hasOwnProperty(key); }; /** * Returns true if an array or object has any of the given keys. * @return {Boolean} */ utils.hasAny = function hasAny(val, keys) { keys = utils.arrayify(keys); var len = keys.length; while (len--) { if (utils.has(val, keys[len])) { return true; } } return false; }; /** * Return true if the given value is an object. * @return {Boolean} */ utils.isObject = function isObject(val) { return val && (typeof val === 'function' || typeof val === 'object') && !Array.isArray(val); }; /** * Return true if the given value is a stream. */ utils.isStream = function isStream(val) { return val && (typeof val === 'function' || typeof val === 'object') && !Array.isArray(val) && (typeof val.pipe === 'function') && (typeof val.on === 'function'); }; /** * Bind a `thisArg` to all the functions on the target * * @param {Object|Array} `target` Object or Array with functions as values that will be bound. * @param {Object} `thisArg` Object to bind to the functions * @return {Object|Array} Object or Array with bound functions. */ utils.bindAll = function bindAll(target, thisArg) { return utils.reduce(target, function (acc, fn, key) { if (typeof fn === 'object') { acc[key] = utils.bindAll(fn, thisArg); } else if (typeof fn === 'function') { acc[key] = fn.bind(thisArg); // get `async` flag or any other helper options on `fn` for (var k in fn) acc[key][k] = fn[k]; } return acc; }, {}); }; /** * VIEW UTILS * -------------------------------- */ /** * Singularize the given `name` */ utils.single = function single(name) { return utils.inflect.singularize(name); }; /** * Pluralize the given `name` */ utils.plural = function plural(name) { return utils.inflect.pluralize(name); }; /** * Return true if the given value is a view. */ utils.isView = function isView(val) { return val && val.hasOwnProperty('content') || val.hasOwnProperty('contents') || val.hasOwnProperty('path'); }; /** * Return the first object with a key that matches * the given glob pattern. * * @param {Object} `object` * @param {String|Array} `patterns` * @param {Object} `options` * @return {Object} */ utils.matchKey = function matchKey(obj, patterns, options) { if (!utils.isObject(obj)) return null; var keys = utils.mm(Object.keys(obj), patterns, options); return obj[keys[0]]; }; /** * Return all objects with keys that match * the given glob pattern. * * @param {Object} `object` * @param {String|Array} `patterns` * @param {Object} `options` * @return {Object} */ utils.matchKeys = function matchKeys(obj, patterns, options) { var keys = utils.mm(Object.keys(obj), patterns, options).sort(); var len = keys.length, i = 0; var res = {}; while (len--) { var key = keys[i++]; res[key] = obj[key]; } return res; }; /** * Sync the _content and _contents properties on a view to ensure * both are set when setting one. * * @param {Object} `view` instance of a `View` * @param {String|Buffer|Stream|null} `contents` contents to set on both properties */ utils.syncContents = function syncContents(view, contents) { if (contents === null) { view._contents = null; view._content = null; } if (typeof contents === 'string') { view._contents = new Buffer(contents); view._content = contents; } if (utils.isBuffer(contents)) { view._contents = contents; view._content = contents.toString(); } if (utils.isStream(contents)) { view._contents = contents; // what should be done here? view._content = contents; } }; /** * Create a dest function to use on an instance of view. * Useful in a getter method on `view.dest`. * * @param {Object} `view` An instance of View * @return {Function} Function suitable for `file.dest` in the [dest][] plugin. */ utils.destWrapper = function destWrapper(view) { if (typeof view._dest === 'function') { return view._dest; } return function (dir, options, cb) { // when function is called with just the callback if (typeof dir === 'function' && arguments.length === 1) { cb = dir; dir = ''; options = {}; } if (typeof options === 'function') { cb = options; options = {}; } var opts = utils.extend({ cwd: view.cwd || process.cwd(), mode: (view.stat ? view.stat.mode : null), dirMode: null, overwrite: true }, options); if (view._dest && typeof view._dest === 'string') { var orig = dir; dir = function (file) { file.path = view._dest; return typeof orig === 'function' ? orig(file) : orig; }; } return utils.dest.normalize(dir, view, opts, cb); }; }; /** * Rename a file */ utils.rename = function rename(fp, opts) { opts = opts || {}; var renameFn = opts.renameFn || opts.rename || utils.identity; return renameFn(fp); }; /** * Return true if the given value is a view. */ utils.renameKey = function(app) { utils.define(app, 'renameKey', function renameKey(key, fn) { if (typeof key === 'function') { fn = key; key = null; } if (this.option && typeof fn !== 'function') { fn = this.option('renameKey'); } if (typeof fn !== 'function') { fn = utils.identity; } this.options = this.options || {}; this.options.renameKey = fn; if (typeof key === 'string') { return fn(key); } return fn; }.bind(app)); }; /** * Decorate a `view` method onto the given `app` object. * * @param {Object} app */ utils.viewFactory = function (app, method, CtorName) { app.define(method, function(key, value) { if (typeof value !== 'object' && typeof key === 'string') { return this[method](this.renameKey(key), {path: key}); } if (utils.isObject(key) && key.path) { return this[method](key.path, key); } if (typeof value !== 'object') { throw new TypeError('expected value to be an object.'); } var View = this.get(CtorName); var view = !(value instanceof View) ? new View(value) : value; view.options = view.options || value.options || {}; view.locals = view.locals || value.locals || {}; view.data = view.data || value.data || {}; // get renameKey fn if defined on view opts if (view.options && view.options.renameKey) { this.option('renameKey', view.options.renameKey); } view.key = this.renameKey(view.key || key); view.path = view.path || key; this.plugins.forEach(function (fn) { view.use(fn); }); this.emit(method, view, this); return view; }); }; /** * Set or get an option value. This is a factory for * adding an `option` method to a class */ utils.option = function option(app) { utils.define(app, 'option', function(key, value) { if (typeof key === 'string') { if (arguments.length === 1) { return this.get('options.' + key); } this.set('options.' + key, value); this.emit('option', key, value); return this; } if (typeof key !== 'object') { throw new TypeError('expected a string or object.'); } this.visit('option', key); return this; }.bind(app)); }; /** * Ensure file extensions are formatted properly for lookups. * * ```js * utils.formatExt('hbs'); * //=> '.hbs' * * utils.formatExt('.hbs'); * //=> '.hbs' * ``` * * @param {String} `ext` File extension * @return {String} * @api public */ utils.formatExt = function formatExt(ext) { if (typeof ext !== 'string') { throw new Error('utils.formatExt() expects `ext` to be a string.'); } if (ext.charAt(0) !== '.') { return '.' + ext; } return ext; }; /** * Strip the dot from a file extension * * ```js * utils.stripDot('.hbs'); * //=> 'hbs' * ``` * * @param {String} `ext` extension * @return {String} * @api public */ utils.stripDot = function stripDot(ext) { if (typeof ext !== 'string') { throw new Error('utils.stripDot() expects `ext` to be a string.'); } if (ext.charAt(0) === '.') { return ext.slice(1); } return ext; }; /** * Get locals from helper arguments. * * @param {Object} `locals` * @param {Object} `options` */ utils.getLocals = function getLocals(locals, options) { options = options || {}; locals = locals || {}; var ctx = {}; if (options.hasOwnProperty('hash')) { utils.merge(ctx, options.hash); delete options.hash; } if (locals.hasOwnProperty('hash')) { utils.merge(ctx, locals.hash); delete locals.hash; } utils.merge(ctx, options); utils.merge(ctx, locals); return ctx; }; /** * Expose utils */ module.exports = lazy;