'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 };