rittenhop-ghost/versions/5.94.2/node_modules/express-hbs/lib/hbs.js

674 lines
20 KiB
JavaScript

'use strict';
var fs = require('fs');
var path = require('path');
var readdirp = require('readdirp');
var handlebars = require('handlebars');
var resolver = require('./resolver');
var _ = require('lodash');
/**
* Regex pattern for layout directive. {{!< layout }}
*/
var layoutPattern = /{{!<\s+([A-Za-z0-9\._\-\/]+)\s*}}/;
/**
* Constructor
*/
var ExpressHbs = function() {
this.handlebars = handlebars.create();
this.SafeString = this.handlebars.SafeString;
this.Utils = this.handlebars.Utils;
this.beautify = null;
this.beautifyrc = null;
this.cwd = process.cwd();
};
/**
* Defines content for a named block declared in layout.
*
* @example
*
* {{#contentFor "pageStylesheets"}}
* <link rel="stylesheet" href='{{{URL "css/style.css"}}}' />
* {{/contentFor}}
*/
ExpressHbs.prototype.content = function(name, options, context) {
var block = options.data.root.blockCache[name] || (options.data.root.blockCache[name] = []);
block.push(options.fn(context));
};
/**
* Returns the layout filepath given the template filename and layout used.
* Backward compatible with specifying layouts in locals like 'layouts/foo',
* but if you have specified a layoutsDir you can specify layouts in locals with just the layout name.
*
* @param {String} filename Path to template file.
* @param {String} layout Layout path.
*/
ExpressHbs.prototype.layoutPath = function(filename, layout) {
var dirs,
layoutPath;
if (layout[0] === '.') {
dirs = path.dirname(filename);
} else if (this.layoutsDir) {
dirs = this.layoutsDir;
} else {
dirs = this.viewsDir;
}
[].concat(dirs).forEach(function (dir) {
if (!layoutPath) {
layoutPath = path.resolve(dir, layout);
}
});
return layoutPath;
};
/**
* Find the path of the declared layout in `str`, if any
*
* @param {String} str The template string to parse
* @param {String} filename Path to template
* @returns {String|undefined} Returns the path to layout.
*/
ExpressHbs.prototype.declaredLayoutFile = function(str, filename) {
var matches = str.match(layoutPattern);
if (matches) {
var layout = matches[1];
// behave like `require`, if '.' then relative, else look in
// usual location (layoutsDir)
if (this.layoutsDir && layout[0] !== '.') {
layout = path.resolve(this.layoutsDir, layout);
}
return path.resolve(path.dirname(filename), layout);
}
};
/**
* Compiles a layout file.
*
* The function checks whether the layout file declares a parent layout.
* If it does, the parent layout is loaded recursively and checked as well
* for a parent layout, and so on, until the top layout is reached.
* All layouts are then returned as a stack to the caller via the callback.
*
* @param {String} layoutFile The path to the layout file to compile
* @param {Boolean} useCache Cache the compiled layout?
* @param {Function} cb Callback called with layouts stack
*/
ExpressHbs.prototype.cacheLayout = function(layoutFile, useCache, cb) {
var self = this;
if (this.restrictLayoutsTo) {
if (!layoutFile.startsWith(this.restrictLayoutsTo)) {
var err = new Error('Cannot read ' + layoutFile + ' it does not reside in ' + this.restrictLayoutsTo);
return cb(err, null);
}
}
// assume hbs extension
if (path.extname(layoutFile) === '') layoutFile += this._options.extname;
// path is relative in directive, make it absolute
var layoutTemplates = this.cache[layoutFile];
if (layoutTemplates) return cb(null, layoutTemplates);
fs.readFile(layoutFile, 'utf8', function(err, str) {
if (err) return cb(err);
// File path of eventual declared parent layout
var parentLayoutFile = self.declaredLayoutFile(str, layoutFile);
// This function returns the current layout stack to the caller
var _returnLayouts = function(layouts) {
var currentLayout;
layouts = layouts.slice(0);
currentLayout = self.compile(str, layoutFile);
layouts.push(currentLayout);
if (useCache) {
self.cache[layoutFile] = layouts.slice(0);
}
cb(null, layouts);
};
if (parentLayoutFile) {
// Recursively compile/cache parent layouts
self.cacheLayout(parentLayoutFile, useCache, function(err, parentLayouts) {
if (err) return cb(err);
_returnLayouts(parentLayouts);
});
} else {
// No parent layout: return current layout with an empty stack
_returnLayouts([]);
}
});
};
/**
* Cache partial templates found under directories configure in partialsDir.
*/
ExpressHbs.prototype.cachePartials = function(cb) {
var self = this;
if (!(this.partialsDir instanceof Array)) {
this.partialsDir = [this.partialsDir];
}
// Use to iterate all folder in series
var count = 0;
function readNext() {
readdirp(self.partialsDir[count], {fileFilter: '*' + self._options.extname})
.on('warn', function(err) {
console.warn('Non-fatal error in express-hbs cachePartials.', err);
})
.on('error', function(err) {
console.error('Fatal error in express-hbs cachePartials', err);
return cb(err);
})
.on('data', function(entry) {
if (!entry) return;
var source = fs.readFileSync(entry.fullPath, 'utf8');
var dirname = path.dirname(entry.path);
dirname = dirname === '.' ? '' : dirname + '/';
var name = dirname + path.basename(entry.basename, self._options.extname);
// fix the path in windows
name = name.split('\\').join('/');
self.registerPartial(name, source, entry.fullPath);
})
.on('end', function() {
count += 1;
// If all directories aren't read, read the next directory
if (count < self.partialsDir.length) {
readNext();
} else {
self.isPartialCachingComplete = true;
if (cb) cb(null, true);
}
});
}
readNext();
};
/**
* Express 3.x template engine compliance.
*
* @param {Object} options = {
* handlebars: "override handlebars",
* defaultLayout: "path to default layout",
* partialsDir: "absolute path to partials (one path or an array of paths)",
* layoutsDir: "absolute path to the layouts",
* extname: "extension to use",
* contentHelperName: "contentFor",
* blockHelperName: "block",
* beautify: "{Boolean} whether to pretty print HTML",
* onCompile: function(self, source, filename) {
* return self.handlebars.compile(source);
* }
* }
*/
ExpressHbs.prototype.express3 = function(options) {
var self = this;
// Set defaults
if (!options) options = {};
if (!options.extname) options.extname = '.hbs';
if (!options.contentHelperName) options.contentHelperName = 'contentFor';
if (!options.blockHelperName) options.blockHelperName = 'block';
if (!options.templateOptions) options.templateOptions = {};
if (options.handlebars) this.handlebars = options.handlebars;
if (options.onCompile) this.onCompile = options.onCompile;
this._options = options;
if (this._options.handlebars) this.handlebars = this._options.handlebars;
if (options.i18n) {
var i18n = options.i18n;
this.handlebars.registerHelper('__', function() {
var args = Array.prototype.slice.call(arguments);
var options = args.pop();
return i18n.__.apply(options.data.root, args);
});
this.handlebars.registerHelper('__n', function() {
var args = Array.prototype.slice.call(arguments);
var options = args.pop();
return i18n.__n.apply(options.data.root, args);
});
}
this.handlebars.registerHelper(this._options.blockHelperName, function(name, options) {
var val = options.data.root.blockCache[name];
if (val === undefined && typeof options.fn === 'function') {
val = options.fn(this);
}
if (Array.isArray(val)) {
val = val.join('\n');
}
return val;
});
// Pass 'this' as context of helper function to don't lose context call of helpers.
this.handlebars.registerHelper(this._options.contentHelperName, function(name, options) {
return self.content(name, options, this);
});
// Absolute path to partials directory.
this.partialsDir = this._options.partialsDir;
// Absolute path to the layouts directory
this.layoutsDir = this._options.layoutsDir;
this.restrictLayoutsTo = this._options.restrictLayoutsTo;
// express passes this through ___express func, gulp pass in an option
this.viewsDir = null;
this.viewsDirOpt = this._options.viewsDir;
// Cache for templates, express 3.x doesn't do this for us
this.cache = {};
// Holds the default compiled layout if specified in options configuration.
this.defaultLayoutTemplates = null;
// Keep track of if partials have been cached already or not.
this.isPartialCachingComplete = false;
return this.___express.bind(this);
};
/**
* Express 4.x template engine compliance.
*
* @param {Object} options = {
* handlebars: "override handlebars",
* defaultLayout: "path to default layout",
* partialsDir: "absolute path to partials (one path or an array of paths)",
* layoutsDir: "absolute path to the layouts",
* extname: "extension to use",
* contentHelperName: "contentFor",
* blockHelperName: "block",
* beautify: "{Boolean} whether to pretty print HTML"
* }
*/
ExpressHbs.prototype.express4 = ExpressHbs.prototype.express3;
/**
* Tries to load the default layout.
*
* @param {Boolean} useCache Whether to cache.
*/
ExpressHbs.prototype.loadDefaultLayout = function(useCache, cb) {
var self = this;
if (!this._options.defaultLayout) return cb();
if (useCache && this.defaultLayoutTemplates) return cb(null, this.defaultLayoutTemplates);
this.cacheLayout(this._options.defaultLayout, useCache, function(err, templates) {
if (err) return cb(err);
self.defaultLayoutTemplates = templates.slice(0);
return cb(null, templates);
});
};
/**
* Expose useful methods.
*/
ExpressHbs.prototype.registerHelper = function(name, fn) {
this.handlebars.registerHelper(name, fn);
};
/**
* Registers a partial.
*
* @param {String} name The name of the partial as used in a template.
* @param {String} source String source of the partial.
*/
ExpressHbs.prototype.registerPartial = function(name, source, filename) {
this.handlebars.registerPartial(name, this.compile(source, filename));
};
/**
* Compiles a string.
*
* @param {String} source The source to compile.
* @param {String} filename The path used to embed into __filename for errors.
*/
ExpressHbs.prototype.compile = function(source, filename) {
// Handlebars has a bug with comment only partial causes errors. This must
// be a string so the block below can add a space.
if (typeof source !== 'string') {
throw new Error('registerPartial must be a string for empty comment workaround');
}
if (source.indexOf('}}') === source.length - 2) {
source += ' ';
}
var compiled;
if (this.onCompile) {
compiled = this.onCompile(this, source, filename);
} else {
compiled = this.handlebars.compile(source);
}
if (filename) {
if (Array.isArray(this.viewsDir) && this.viewsDir.length > 0) {
compiled.__filename = path.relative(this.cwd, filename).replace(path.sep, '/');
} else {
compiled.__filename = path.relative(this.viewsDir || '', filename).replace(path.sep, '/');
}
}
return compiled;
};
/**
* Registers an asynchronous helper.
*
* @param {String} name The name of the partial as used in a template.
* @param {String} fn The `function(options, cb)`
*/
ExpressHbs.prototype.registerAsyncHelper = function(name, fn) {
this.handlebars.registerHelper(name, function(context, options) {
var resolverCache = this.resolverCache ||
_.get(context, 'data.root.resolverCache') ||
_.get(options, 'data.root.resolverCache');
if (!resolverCache) {
throw new Error('Could not find resolver cache in async helper ' + name + '.');
}
if (options && fn.length > 2) {
var resolveFunc = function(arr, cb) {
return fn.call(this, arr[0], arr[1], cb);
};
return resolver.resolve(
resolverCache,
resolveFunc.bind(this),
[context, options]
);
}
return resolver.resolve(
resolverCache,
fn.bind(this),
context
);
});
};
ExpressHbs.prototype.getTemplateOptions = function() {
return this._options.templateOptions;
};
ExpressHbs.prototype.updateTemplateOptions = function(templateOptions) {
this._options.templateOptions = templateOptions;
};
ExpressHbs.prototype.getLocalTemplateOptions = function(locals) {
return locals._templateOptions || {};
};
ExpressHbs.prototype.updateLocalTemplateOptions = function(locals, localTemplateOptions) {
return locals._templateOptions = localTemplateOptions;
};
/**
* Creates a new instance of ExpressHbs.
*/
ExpressHbs.prototype.create = function() {
return new ExpressHbs();
};
/**
* express 3.x, 4.x template engine compliance
*
* @param {String} filename Full path to template.
* @param {Object} options Is the context or locals for templates. {
* {Object} settings - subset of Express settings, `settings.views` is
* the views directory
* }
* @param {Function} cb The callback expecting the rendered template as a string.
*
* @example
*
* Example options from express
*
* {
* settings: {
* 'x-powered-by': true,
* env: 'production',
* views: '/home/coder/barc/code/express-hbs/example/views',
* 'jsonp callback name': 'callback',
* 'view cache': true,
* 'view engine': 'hbs'
* },
* cache: true,
*
* // the rest are app-defined locals
* title: 'My favorite veggies',
* layout: 'layout/veggie'
* }
*/
ExpressHbs.prototype.___express = function ___express(filename, source, options, cb) {
// support running as a gulp/grunt filter outside of express
if (arguments.length === 3) {
cb = options;
options = source;
source = null;
}
options.blockCache = {};
options.resolverCache = {};
this.viewsDir = options.settings.views || this.viewsDirOpt;
var self = this;
/**
* Allow a layout to be declared as a handlebars comment to remain spec
* compatible with handlebars.
*
* Valid directives
*
* {{!< foo}} # foo.hbs in same directory as template
* {{!< ../layouts/default}} # default.hbs in parent layout directory
* {{!< ../layouts/default.html}} # default.html in parent layout directory
*/
function parseLayout(str, filename, cb) {
var layoutFile = self.declaredLayoutFile(str, filename);
if (layoutFile) {
self.cacheLayout(layoutFile, options.cache, cb);
} else {
cb(null, null);
}
}
/**
* Renders `template` with given `locals` and calls `cb` with the
* resulting HTML string.
*
* @param template
* @param locals
* @param cb
*/
function renderTemplate(template, locals, cb) {
var res;
try {
var localTemplateOptions = self.getLocalTemplateOptions(locals);
var localsClone = _.extend({}, locals);
self.updateLocalTemplateOptions(localsClone, undefined);
res = template(localsClone, _.merge({}, self._options.templateOptions, localTemplateOptions));
} catch (err) {
if (err.message) {
err.message = '[' + template.__filename + '] ' + err.message;
} else if (typeof err === 'string') {
return cb('[' + template.__filename + '] ' + err, null);
}
return cb(err, null);
}
cb(null, res);
}
/**
* Renders `template` with an optional set of nested `layoutTemplates` using
* data in `locals`.
*/
function render(template, locals, layoutTemplates, cb) {
if (!layoutTemplates) layoutTemplates = [];
// We'll render templates from bottom to top of the stack, each template
// being passed the rendered string of the previous ones as `body`
var i = layoutTemplates.length - 1;
var _stackRenderer = function(err, htmlStr) {
if (err) return cb(err);
if (i >= 0) {
locals.body = htmlStr;
renderTemplate(layoutTemplates[i--], locals, _stackRenderer);
} else {
cb(null, htmlStr);
}
};
// Start the rendering with the innermost page template
renderTemplate(template, locals, _stackRenderer);
}
/**
* Lazy loads js-beautify, which should not be used in production env.
*/
function loadBeautify() {
if (!self.beautify) {
self.beautify = require('js-beautify').html;
var rc = path.join(process.cwd(), '.jsbeautifyrc');
if (fs.existsSync(rc)) {
self.beautifyrc = JSON.parse(fs.readFileSync(rc, 'utf8'));
}
}
}
/**
* Gets the source and compiled template for filename either from the cache
* or compiling it on the fly.
*/
function getSourceTemplate(cb) {
if (options.cache) {
var info = self.cache[filename];
if (info) {
return cb(null, info.source, info.template);
}
}
fs.readFile(filename, 'utf8', function(err, source) {
if (err) return cb(err);
var template = self.compile(source, filename);
if (options.cache) {
self.cache[filename] = {
source: source,
template: template
};
}
return cb(null, source, template);
});
}
/**
* Compiles a file into a template and a layoutTemplate, then renders it above.
*/
function compileFile(locals, cb) {
getSourceTemplate(function(err, source, template) {
if (err) return cb(err);
// Try to get the layout
parseLayout(source, filename, function(err, layoutTemplates) {
if (err) return cb(err);
function renderIt(layoutTemplates) {
if (self._options.beautify) {
return render(template, locals, layoutTemplates, function(err, html) {
if (err) return cb(err);
loadBeautify();
return cb(null, self.beautify(html, self.beautifyrc));
});
}
return render(template, locals, layoutTemplates, cb);
}
// Determine which layout to use
if (typeof options.layout !== 'undefined' && !options.layout) {
// If options.layout is falsy, behave as if no layout should be used - suppress defaults
renderIt(null);
} else if (layoutTemplates) {
// 1. Layout specified in template
renderIt(layoutTemplates);
} else if (typeof options.layout !== 'undefined' && options.layout) {
// 2. Layout specified by options from render
var layoutFile = self.layoutPath(filename, options.layout);
self.cacheLayout(layoutFile, options.cache, function(err, layoutTemplates) {
if (err) return cb(err);
renderIt(layoutTemplates);
});
} else if (self.defaultLayoutTemplates) {
// 3. Default layout specified when middleware was configured.
renderIt(self.defaultLayoutTemplates);
} else {
// render without a template
renderIt(null);
}
});
});
}
function replaceValue(values, text) {
if (typeof text === 'string') {
Object.keys(values).forEach(function(id) {
text = text.replace(id, function() {
return values[id];
});
text = text.replace(self.Utils.escapeExpression(id), function() {
return self.Utils.escapeExpression(values[id]);
});
});
}
return text;
}
// Handles waiting for async helpers
function handleAsync(err, res) {
if (err) return cb(err);
resolver.done(options.resolverCache, function(err, values) {
if (err) return cb(err);
Object.keys(values).forEach(function(key) {
values[key] = replaceValue(values, values[key]);
});
res = replaceValue(values, res);
if (resolver.hasResolvers(res)) {
return handleAsync(null, res);
}
cb(null, res);
});
}
// kick it off by loading default template (if any)
this.loadDefaultLayout(options.cache, function(err) {
if (err) return cb(err);
// Force reloading of all partials if caching is not used. Inefficient but there
// is no loading partial event.
if (self.partialsDir && (!options.cache || !self.isPartialCachingComplete)) {
return self.cachePartials(function(err) {
if (err) return cb(err);
return compileFile(options, handleAsync);
});
}
return compileFile(options, handleAsync);
});
};
module.exports = new ExpressHbs();