'use strict'; var path = require('path'); var utils = require('./utils'); /** * Expose `layouts` */ module.exports = renderLayouts; /** * Cache compiled delimiter regex. * * If delimiters need to be generated, this ensures that * runtime compilation only happens once. */ var cache = {}; /** * Wrap one or more layouts around `string`. * * ```js * renderLayouts(string, layoutName, layouts, options, fn); * ``` * * @param {String} `string` The string to wrap with a layout. * @param {String} `layoutName` The name (key) of the layout object to use. * @param {Object} `layouts` Object of layout objects. * @param {Object} `options` Optionally define a `defaultLayout` (string), pass custom delimiters (`layoutDelims`) to use as the placeholder for the content insertion point, or change the name of the placeholder tag with the `tag` option. * @param {Function} `fn` Optionally pass a function to modify the context as each layout is applied. * @return {String} Returns the original string wrapped with one or more layouts. * @api public */ function renderLayouts(str, name, layouts, opts, fn) { if (isBuffer(str)) { str = String(str); } if (typeof str !== 'string') { throw new TypeError('expected content to be a string.'); } if (typeof name !== 'string') { throw new TypeError('expected layout name to be a string.'); } if (typeof opts === 'function') { fn = opts; opts = {}; } opts = opts || {}; var layout = {}; var depth = 0; var prev; // `view` is the object we'll use to store the result var view = {options: {}, history: []}; name = assertLayout(name, opts.defaultLayout); // recursively resolve layouts while (name && (prev !== name) && (layout = utils.getView(name, layouts))) { prev = name; var delims = opts.layoutDelims; // `data` is passed to `wrapLayout` to resolve layouts // to the values on the data object. var data = {}; var tag = opts.tag || 'body'; data[tag] = str; // get info about the current layout var obj = new Layout({ name: name, layout: layout, before: str, depth: depth++ }); // get the delimiter regex var re = makeDelimiterRegex(delims, tag); // ensure that content is a string var content = String(layout.content); if (!re.test(content)) { throw error(re, name, tag); } // inject the string into the layout str = wrapLayout(content, data, re, name, tag); obj.after = str; // if a callback is passed, allow it modify the result if (typeof fn === 'function') { fn(obj, view, depth); } // push info about the layout onto `history` view.history.push(obj); // should we recurse again? // does the `layout` specify another layout? name = assertLayout(layout.layout, opts.defaultLayout); } if (typeof name === 'string' && prev !== name) { throw new Error('could not find layout "' + name + '"'); } view.options = opts; view.result = str; return view; } /** * Create a new layout in the layout stack. * * @param {Object} `view` The layout view object * @param {Number} `depth` Current stack depth */ function Layout(view) { this.layout = view.layout; this.layout.name = view.name; this.before = view.before; this.depth = view.depth; return this; } /** * Assert whether or not a layout should be used based on * the given `value`. * * - If a layout should be used, the name of the layout is returned. * - If not, `null` is returned. * * @param {*} `value` * @return {String|Null} Returns `true` or `null`. * @api private */ function assertLayout(value, defaultLayout) { if (value === false || (value && utils.isFalsey(value))) { return null; } else if (!value || value === true) { return defaultLayout || null; } else { return value; } } /** * Resolve template strings to the values on the given * `data` object. */ function wrapLayout(str, data, re, name, tag) { return str.replace(re, function(_, tagName) { var m = data[tagName.trim()]; if (typeof m === 'undefined') { throw error(re, name, tag); } return m; }); } /** * Make delimiter regex. * * @param {Sring|Array|RegExp} `syntax` * @return {RegExp} */ function makeDelimiterRegex(syntax, tag) { if (!syntax) { syntax = makeTag(tag, ['{%', '%}']); } if (syntax instanceof RegExp) { return syntax; } var name = syntax + ''; if (cache.hasOwnProperty(name)) { return cache[name]; } if (typeof syntax === 'string') { return new RegExp(syntax, 'g'); } return (cache[name] = utils.delims(syntax)); } /** * Cast `val` to a string. */ function makeTag(val, delims) { return delims[0].trim() + ' (' + String(val).trim() + ') ' + delims[1].trim(); } /** * Format an error message */ function error(re, name, tag) { var delims = matchDelims(re, tag); return new Error('cannot find "' + delims + '" in "' + name + '"'); } /** * Only used if an error is thrown. Attempts to recreate * delimiters for the error message. */ var types = { '{%=': function(str) { return '{%= ' + str + ' %}'; }, '{%-': function(str) { return '{%- ' + str + ' %}'; }, '{%': function(str) { return '{% ' + str + ' %}'; }, '{{': function(str) { return '{{ ' + str + ' }}'; }, '<%': function(str) { return '<% ' + str + ' %>'; }, '<%=': function(str) { return '<%= ' + str + ' %>'; }, '<%-': function(str) { return '<%- ' + str + ' %>'; } }; function matchDelims(re, str) { var ch = re.source.slice(0, 4); if (/[\\]/.test(ch.charAt(0))) { ch = ch.slice(1); } if (!/[-=]/.test(ch.charAt(2))) { ch = ch.slice(0, 2); } else { ch = ch.slice(0, 3); } return types[ch](str); } /** * Return true if the given value is a buffer */ function isBuffer(val) { if (val && val.constructor && typeof val.constructor.isBuffer === 'function') { return val.constructor.isBuffer(val); } return false; }