548 lines
16 KiB
JavaScript
548 lines
16 KiB
JavaScript
'use strict';
|
|
|
|
function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; _setPrototypeOf(subClass, superClass); }
|
|
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
|
|
var asap = require('asap');
|
|
var _waterfall = require('a-sync-waterfall');
|
|
var lib = require('./lib');
|
|
var compiler = require('./compiler');
|
|
var filters = require('./filters');
|
|
var _require = require('./loaders'),
|
|
FileSystemLoader = _require.FileSystemLoader,
|
|
WebLoader = _require.WebLoader,
|
|
PrecompiledLoader = _require.PrecompiledLoader;
|
|
var tests = require('./tests');
|
|
var globals = require('./globals');
|
|
var _require2 = require('./object'),
|
|
Obj = _require2.Obj,
|
|
EmitterObj = _require2.EmitterObj;
|
|
var globalRuntime = require('./runtime');
|
|
var handleError = globalRuntime.handleError,
|
|
Frame = globalRuntime.Frame;
|
|
var expressApp = require('./express-app');
|
|
|
|
// If the user is using the async API, *always* call it
|
|
// asynchronously even if the template was synchronous.
|
|
function callbackAsap(cb, err, res) {
|
|
asap(function () {
|
|
cb(err, res);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* A no-op template, for use with {% include ignore missing %}
|
|
*/
|
|
var noopTmplSrc = {
|
|
type: 'code',
|
|
obj: {
|
|
root: function root(env, context, frame, runtime, cb) {
|
|
try {
|
|
cb(null, '');
|
|
} catch (e) {
|
|
cb(handleError(e, null, null));
|
|
}
|
|
}
|
|
}
|
|
};
|
|
var Environment = /*#__PURE__*/function (_EmitterObj) {
|
|
_inheritsLoose(Environment, _EmitterObj);
|
|
function Environment() {
|
|
return _EmitterObj.apply(this, arguments) || this;
|
|
}
|
|
var _proto = Environment.prototype;
|
|
_proto.init = function init(loaders, opts) {
|
|
var _this = this;
|
|
// The dev flag determines the trace that'll be shown on errors.
|
|
// If set to true, returns the full trace from the error point,
|
|
// otherwise will return trace starting from Template.render
|
|
// (the full trace from within nunjucks may confuse developers using
|
|
// the library)
|
|
// defaults to false
|
|
opts = this.opts = opts || {};
|
|
this.opts.dev = !!opts.dev;
|
|
|
|
// The autoescape flag sets global autoescaping. If true,
|
|
// every string variable will be escaped by default.
|
|
// If false, strings can be manually escaped using the `escape` filter.
|
|
// defaults to true
|
|
this.opts.autoescape = opts.autoescape != null ? opts.autoescape : true;
|
|
|
|
// If true, this will make the system throw errors if trying
|
|
// to output a null or undefined value
|
|
this.opts.throwOnUndefined = !!opts.throwOnUndefined;
|
|
this.opts.trimBlocks = !!opts.trimBlocks;
|
|
this.opts.lstripBlocks = !!opts.lstripBlocks;
|
|
this.loaders = [];
|
|
if (!loaders) {
|
|
// The filesystem loader is only available server-side
|
|
if (FileSystemLoader) {
|
|
this.loaders = [new FileSystemLoader('views')];
|
|
} else if (WebLoader) {
|
|
this.loaders = [new WebLoader('/views')];
|
|
}
|
|
} else {
|
|
this.loaders = lib.isArray(loaders) ? loaders : [loaders];
|
|
}
|
|
|
|
// It's easy to use precompiled templates: just include them
|
|
// before you configure nunjucks and this will automatically
|
|
// pick it up and use it
|
|
if (typeof window !== 'undefined' && window.nunjucksPrecompiled) {
|
|
this.loaders.unshift(new PrecompiledLoader(window.nunjucksPrecompiled));
|
|
}
|
|
this._initLoaders();
|
|
this.globals = globals();
|
|
this.filters = {};
|
|
this.tests = {};
|
|
this.asyncFilters = [];
|
|
this.extensions = {};
|
|
this.extensionsList = [];
|
|
lib._entries(filters).forEach(function (_ref) {
|
|
var name = _ref[0],
|
|
filter = _ref[1];
|
|
return _this.addFilter(name, filter);
|
|
});
|
|
lib._entries(tests).forEach(function (_ref2) {
|
|
var name = _ref2[0],
|
|
test = _ref2[1];
|
|
return _this.addTest(name, test);
|
|
});
|
|
};
|
|
_proto._initLoaders = function _initLoaders() {
|
|
var _this2 = this;
|
|
this.loaders.forEach(function (loader) {
|
|
// Caching and cache busting
|
|
loader.cache = {};
|
|
if (typeof loader.on === 'function') {
|
|
loader.on('update', function (name, fullname) {
|
|
loader.cache[name] = null;
|
|
_this2.emit('update', name, fullname, loader);
|
|
});
|
|
loader.on('load', function (name, source) {
|
|
_this2.emit('load', name, source, loader);
|
|
});
|
|
}
|
|
});
|
|
};
|
|
_proto.invalidateCache = function invalidateCache() {
|
|
this.loaders.forEach(function (loader) {
|
|
loader.cache = {};
|
|
});
|
|
};
|
|
_proto.addExtension = function addExtension(name, extension) {
|
|
extension.__name = name;
|
|
this.extensions[name] = extension;
|
|
this.extensionsList.push(extension);
|
|
return this;
|
|
};
|
|
_proto.removeExtension = function removeExtension(name) {
|
|
var extension = this.getExtension(name);
|
|
if (!extension) {
|
|
return;
|
|
}
|
|
this.extensionsList = lib.without(this.extensionsList, extension);
|
|
delete this.extensions[name];
|
|
};
|
|
_proto.getExtension = function getExtension(name) {
|
|
return this.extensions[name];
|
|
};
|
|
_proto.hasExtension = function hasExtension(name) {
|
|
return !!this.extensions[name];
|
|
};
|
|
_proto.addGlobal = function addGlobal(name, value) {
|
|
this.globals[name] = value;
|
|
return this;
|
|
};
|
|
_proto.getGlobal = function getGlobal(name) {
|
|
if (typeof this.globals[name] === 'undefined') {
|
|
throw new Error('global not found: ' + name);
|
|
}
|
|
return this.globals[name];
|
|
};
|
|
_proto.addFilter = function addFilter(name, func, async) {
|
|
var wrapped = func;
|
|
if (async) {
|
|
this.asyncFilters.push(name);
|
|
}
|
|
this.filters[name] = wrapped;
|
|
return this;
|
|
};
|
|
_proto.getFilter = function getFilter(name) {
|
|
if (!this.filters[name]) {
|
|
throw new Error('filter not found: ' + name);
|
|
}
|
|
return this.filters[name];
|
|
};
|
|
_proto.addTest = function addTest(name, func) {
|
|
this.tests[name] = func;
|
|
return this;
|
|
};
|
|
_proto.getTest = function getTest(name) {
|
|
if (!this.tests[name]) {
|
|
throw new Error('test not found: ' + name);
|
|
}
|
|
return this.tests[name];
|
|
};
|
|
_proto.resolveTemplate = function resolveTemplate(loader, parentName, filename) {
|
|
var isRelative = loader.isRelative && parentName ? loader.isRelative(filename) : false;
|
|
return isRelative && loader.resolve ? loader.resolve(parentName, filename) : filename;
|
|
};
|
|
_proto.getTemplate = function getTemplate(name, eagerCompile, parentName, ignoreMissing, cb) {
|
|
var _this3 = this;
|
|
var that = this;
|
|
var tmpl = null;
|
|
if (name && name.raw) {
|
|
// this fixes autoescape for templates referenced in symbols
|
|
name = name.raw;
|
|
}
|
|
if (lib.isFunction(parentName)) {
|
|
cb = parentName;
|
|
parentName = null;
|
|
eagerCompile = eagerCompile || false;
|
|
}
|
|
if (lib.isFunction(eagerCompile)) {
|
|
cb = eagerCompile;
|
|
eagerCompile = false;
|
|
}
|
|
if (name instanceof Template) {
|
|
tmpl = name;
|
|
} else if (typeof name !== 'string') {
|
|
throw new Error('template names must be a string: ' + name);
|
|
} else {
|
|
for (var i = 0; i < this.loaders.length; i++) {
|
|
var loader = this.loaders[i];
|
|
tmpl = loader.cache[this.resolveTemplate(loader, parentName, name)];
|
|
if (tmpl) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (tmpl) {
|
|
if (eagerCompile) {
|
|
tmpl.compile();
|
|
}
|
|
if (cb) {
|
|
cb(null, tmpl);
|
|
return undefined;
|
|
} else {
|
|
return tmpl;
|
|
}
|
|
}
|
|
var syncResult;
|
|
var createTemplate = function createTemplate(err, info) {
|
|
if (!info && !err && !ignoreMissing) {
|
|
err = new Error('template not found: ' + name);
|
|
}
|
|
if (err) {
|
|
if (cb) {
|
|
cb(err);
|
|
return;
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
var newTmpl;
|
|
if (!info) {
|
|
newTmpl = new Template(noopTmplSrc, _this3, '', eagerCompile);
|
|
} else {
|
|
newTmpl = new Template(info.src, _this3, info.path, eagerCompile);
|
|
if (!info.noCache) {
|
|
info.loader.cache[name] = newTmpl;
|
|
}
|
|
}
|
|
if (cb) {
|
|
cb(null, newTmpl);
|
|
} else {
|
|
syncResult = newTmpl;
|
|
}
|
|
};
|
|
lib.asyncIter(this.loaders, function (loader, i, next, done) {
|
|
function handle(err, src) {
|
|
if (err) {
|
|
done(err);
|
|
} else if (src) {
|
|
src.loader = loader;
|
|
done(null, src);
|
|
} else {
|
|
next();
|
|
}
|
|
}
|
|
|
|
// Resolve name relative to parentName
|
|
name = that.resolveTemplate(loader, parentName, name);
|
|
if (loader.async) {
|
|
loader.getSource(name, handle);
|
|
} else {
|
|
handle(null, loader.getSource(name));
|
|
}
|
|
}, createTemplate);
|
|
return syncResult;
|
|
};
|
|
_proto.express = function express(app) {
|
|
return expressApp(this, app);
|
|
};
|
|
_proto.render = function render(name, ctx, cb) {
|
|
if (lib.isFunction(ctx)) {
|
|
cb = ctx;
|
|
ctx = null;
|
|
}
|
|
|
|
// We support a synchronous API to make it easier to migrate
|
|
// existing code to async. This works because if you don't do
|
|
// anything async work, the whole thing is actually run
|
|
// synchronously.
|
|
var syncResult = null;
|
|
this.getTemplate(name, function (err, tmpl) {
|
|
if (err && cb) {
|
|
callbackAsap(cb, err);
|
|
} else if (err) {
|
|
throw err;
|
|
} else {
|
|
syncResult = tmpl.render(ctx, cb);
|
|
}
|
|
});
|
|
return syncResult;
|
|
};
|
|
_proto.renderString = function renderString(src, ctx, opts, cb) {
|
|
if (lib.isFunction(opts)) {
|
|
cb = opts;
|
|
opts = {};
|
|
}
|
|
opts = opts || {};
|
|
var tmpl = new Template(src, this, opts.path);
|
|
return tmpl.render(ctx, cb);
|
|
};
|
|
_proto.waterfall = function waterfall(tasks, callback, forceAsync) {
|
|
return _waterfall(tasks, callback, forceAsync);
|
|
};
|
|
return Environment;
|
|
}(EmitterObj);
|
|
var Context = /*#__PURE__*/function (_Obj) {
|
|
_inheritsLoose(Context, _Obj);
|
|
function Context() {
|
|
return _Obj.apply(this, arguments) || this;
|
|
}
|
|
var _proto2 = Context.prototype;
|
|
_proto2.init = function init(ctx, blocks, env) {
|
|
var _this4 = this;
|
|
// Has to be tied to an environment so we can tap into its globals.
|
|
this.env = env || new Environment();
|
|
|
|
// Make a duplicate of ctx
|
|
this.ctx = lib.extend({}, ctx);
|
|
this.blocks = {};
|
|
this.exported = [];
|
|
lib.keys(blocks).forEach(function (name) {
|
|
_this4.addBlock(name, blocks[name]);
|
|
});
|
|
};
|
|
_proto2.lookup = function lookup(name) {
|
|
// This is one of the most called functions, so optimize for
|
|
// the typical case where the name isn't in the globals
|
|
if (name in this.env.globals && !(name in this.ctx)) {
|
|
return this.env.globals[name];
|
|
} else {
|
|
return this.ctx[name];
|
|
}
|
|
};
|
|
_proto2.setVariable = function setVariable(name, val) {
|
|
this.ctx[name] = val;
|
|
};
|
|
_proto2.getVariables = function getVariables() {
|
|
return this.ctx;
|
|
};
|
|
_proto2.addBlock = function addBlock(name, block) {
|
|
this.blocks[name] = this.blocks[name] || [];
|
|
this.blocks[name].push(block);
|
|
return this;
|
|
};
|
|
_proto2.getBlock = function getBlock(name) {
|
|
if (!this.blocks[name]) {
|
|
throw new Error('unknown block "' + name + '"');
|
|
}
|
|
return this.blocks[name][0];
|
|
};
|
|
_proto2.getSuper = function getSuper(env, name, block, frame, runtime, cb) {
|
|
var idx = lib.indexOf(this.blocks[name] || [], block);
|
|
var blk = this.blocks[name][idx + 1];
|
|
var context = this;
|
|
if (idx === -1 || !blk) {
|
|
throw new Error('no super block available for "' + name + '"');
|
|
}
|
|
blk(env, context, frame, runtime, cb);
|
|
};
|
|
_proto2.addExport = function addExport(name) {
|
|
this.exported.push(name);
|
|
};
|
|
_proto2.getExported = function getExported() {
|
|
var _this5 = this;
|
|
var exported = {};
|
|
this.exported.forEach(function (name) {
|
|
exported[name] = _this5.ctx[name];
|
|
});
|
|
return exported;
|
|
};
|
|
return Context;
|
|
}(Obj);
|
|
var Template = /*#__PURE__*/function (_Obj2) {
|
|
_inheritsLoose(Template, _Obj2);
|
|
function Template() {
|
|
return _Obj2.apply(this, arguments) || this;
|
|
}
|
|
var _proto3 = Template.prototype;
|
|
_proto3.init = function init(src, env, path, eagerCompile) {
|
|
this.env = env || new Environment();
|
|
if (lib.isObject(src)) {
|
|
switch (src.type) {
|
|
case 'code':
|
|
this.tmplProps = src.obj;
|
|
break;
|
|
case 'string':
|
|
this.tmplStr = src.obj;
|
|
break;
|
|
default:
|
|
throw new Error("Unexpected template object type " + src.type + "; expected 'code', or 'string'");
|
|
}
|
|
} else if (lib.isString(src)) {
|
|
this.tmplStr = src;
|
|
} else {
|
|
throw new Error('src must be a string or an object describing the source');
|
|
}
|
|
this.path = path;
|
|
if (eagerCompile) {
|
|
try {
|
|
this._compile();
|
|
} catch (err) {
|
|
throw lib._prettifyError(this.path, this.env.opts.dev, err);
|
|
}
|
|
} else {
|
|
this.compiled = false;
|
|
}
|
|
};
|
|
_proto3.render = function render(ctx, parentFrame, cb) {
|
|
var _this6 = this;
|
|
if (typeof ctx === 'function') {
|
|
cb = ctx;
|
|
ctx = {};
|
|
} else if (typeof parentFrame === 'function') {
|
|
cb = parentFrame;
|
|
parentFrame = null;
|
|
}
|
|
|
|
// If there is a parent frame, we are being called from internal
|
|
// code of another template, and the internal system
|
|
// depends on the sync/async nature of the parent template
|
|
// to be inherited, so force an async callback
|
|
var forceAsync = !parentFrame;
|
|
|
|
// Catch compile errors for async rendering
|
|
try {
|
|
this.compile();
|
|
} catch (e) {
|
|
var err = lib._prettifyError(this.path, this.env.opts.dev, e);
|
|
if (cb) {
|
|
return callbackAsap(cb, err);
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
var context = new Context(ctx || {}, this.blocks, this.env);
|
|
var frame = parentFrame ? parentFrame.push(true) : new Frame();
|
|
frame.topLevel = true;
|
|
var syncResult = null;
|
|
var didError = false;
|
|
this.rootRenderFunc(this.env, context, frame, globalRuntime, function (err, res) {
|
|
// TODO: this is actually a bug in the compiled template (because waterfall
|
|
// tasks are both not passing errors up the chain of callbacks AND are not
|
|
// causing a return from the top-most render function). But fixing that
|
|
// will require a more substantial change to the compiler.
|
|
if (didError && cb && typeof res !== 'undefined') {
|
|
// prevent multiple calls to cb
|
|
return;
|
|
}
|
|
if (err) {
|
|
err = lib._prettifyError(_this6.path, _this6.env.opts.dev, err);
|
|
didError = true;
|
|
}
|
|
if (cb) {
|
|
if (forceAsync) {
|
|
callbackAsap(cb, err, res);
|
|
} else {
|
|
cb(err, res);
|
|
}
|
|
} else {
|
|
if (err) {
|
|
throw err;
|
|
}
|
|
syncResult = res;
|
|
}
|
|
});
|
|
return syncResult;
|
|
};
|
|
_proto3.getExported = function getExported(ctx, parentFrame, cb) {
|
|
// eslint-disable-line consistent-return
|
|
if (typeof ctx === 'function') {
|
|
cb = ctx;
|
|
ctx = {};
|
|
}
|
|
if (typeof parentFrame === 'function') {
|
|
cb = parentFrame;
|
|
parentFrame = null;
|
|
}
|
|
|
|
// Catch compile errors for async rendering
|
|
try {
|
|
this.compile();
|
|
} catch (e) {
|
|
if (cb) {
|
|
return cb(e);
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
var frame = parentFrame ? parentFrame.push() : new Frame();
|
|
frame.topLevel = true;
|
|
|
|
// Run the rootRenderFunc to populate the context with exported vars
|
|
var context = new Context(ctx || {}, this.blocks, this.env);
|
|
this.rootRenderFunc(this.env, context, frame, globalRuntime, function (err) {
|
|
if (err) {
|
|
cb(err, null);
|
|
} else {
|
|
cb(null, context.getExported());
|
|
}
|
|
});
|
|
};
|
|
_proto3.compile = function compile() {
|
|
if (!this.compiled) {
|
|
this._compile();
|
|
}
|
|
};
|
|
_proto3._compile = function _compile() {
|
|
var props;
|
|
if (this.tmplProps) {
|
|
props = this.tmplProps;
|
|
} else {
|
|
var source = compiler.compile(this.tmplStr, this.env.asyncFilters, this.env.extensionsList, this.path, this.env.opts);
|
|
var func = new Function(source); // eslint-disable-line no-new-func
|
|
props = func();
|
|
}
|
|
this.blocks = this._getBlocks(props);
|
|
this.rootRenderFunc = props.root;
|
|
this.compiled = true;
|
|
};
|
|
_proto3._getBlocks = function _getBlocks(props) {
|
|
var blocks = {};
|
|
lib.keys(props).forEach(function (k) {
|
|
if (k.slice(0, 2) === 'b_') {
|
|
blocks[k.slice(2)] = props[k];
|
|
}
|
|
});
|
|
return blocks;
|
|
};
|
|
return Template;
|
|
}(Obj);
|
|
module.exports = {
|
|
Environment: Environment,
|
|
Template: Template
|
|
}; |