Skip to main content
Module

x/nunjucks/src/environment.js

A powerful templating engine with inheritance, asynchronous control, and more (Jinja 2 inspired)
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
import waterfall from "./waterfall.js";import * as lib from "./lib.js";import compiler from "./compiler.js";import filters from "./filters.js";import { FileSystemLoader, PrecompiledLoader } from "./loaders.js";import tests from "./tests.js";import globals from "./globals.js";import { EmitterObj, Obj } from "./object.js";import * as globalRuntime from "./runtime.js";const { handleError, Frame } = globalRuntime;import expressApp from "./express_app.js";
// If the user is using the async API, *always* call it// asynchronously even if the template was synchronous.function callbackAsap(cb, err, res) { queueMicrotask(() => { cb(err, res); });}
/** * A no-op template, for use with {% include ignore missing %} */const noopTmplSrc = { type: "code", obj: { root(_0, _1, _2, _3, cb) { try { cb(null, ""); } catch (e) { cb(handleError(e, null, null)); } }, },};
class Environment extends EmitterObj { init(loaders, opts) { // 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 { 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(([name, filter]) => this.addFilter(name, filter) ); lib._entries(tests).forEach(([name, test]) => this.addTest(name, test)); }
_initLoaders() { this.loaders.forEach((loader) => { // Caching and cache busting loader.cache = {}; if (typeof loader.on === "function") { loader.on("update", (name, fullname) => { loader.cache[name] = null; this.emit("update", name, fullname, loader); }); loader.on("load", (name, source) => { this.emit("load", name, source, loader); }); } }); }
invalidateCache() { this.loaders.forEach((loader) => { loader.cache = {}; }); }
addExtension(name, extension) { extension.__name = name; this.extensions[name] = extension; this.extensionsList.push(extension); return this; }
removeExtension(name) { const extension = this.getExtension(name); if (!extension) { return; }
this.extensionsList = lib.without(this.extensionsList, extension); delete this.extensions[name]; }
getExtension(name) { return this.extensions[name]; }
hasExtension(name) { return !!this.extensions[name]; }
addGlobal(name, value) { this.globals[name] = value; return this; }
getGlobal(name) { if (typeof this.globals[name] === "undefined") { throw new Error("global not found: " + name); } return this.globals[name]; }
addFilter(name, func, async) { const wrapped = func;
if (async) { this.asyncFilters.push(name); } this.filters[name] = wrapped; return this; }
getFilter(name) { if (!this.filters[name]) { throw new Error("filter not found: " + name); } return this.filters[name]; }
addTest(name, func) { this.tests[name] = func; return this; }
getTest(name) { if (!this.tests[name]) { throw new Error("test not found: " + name); } return this.tests[name]; }
resolveTemplate(loader, parentName, filename) { const isRelative = (loader.isRelative && parentName) ? loader.isRelative(filename) : false; return (isRelative && loader.resolve) ? loader.resolve(parentName, filename) : filename; }
getTemplate(name, eagerCompile, parentName, ignoreMissing, cb) { const that = this; let 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 (let i = 0; i < this.loaders.length; i++) { const 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; } } let syncResult;
const createTemplate = (err, info) => { if (!info && !err && !ignoreMissing) { err = new Error("template not found: " + name); }
if (err) { if (cb) { cb(err); return; } else { throw err; } } let newTmpl; if (!info) { newTmpl = new Template(noopTmplSrc, this, "", eagerCompile); } else { newTmpl = new Template(info.src, this, info.path, eagerCompile); if (!info.noCache) { info.loader.cache[name] = newTmpl; } } if (cb) { cb(null, newTmpl); } else { syncResult = newTmpl; } };
lib.asyncIter(this.loaders, (loader, _, 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; }
express(app) { return expressApp(this, app); }
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. let syncResult = null;
this.getTemplate(name, (err, tmpl) => { if (err && cb) { callbackAsap(cb, err); } else if (err) { throw err; } else { syncResult = tmpl.render(ctx, cb); } });
return syncResult; }
renderString(src, ctx, opts, cb) { if (lib.isFunction(opts)) { cb = opts; opts = {}; } opts = opts || {};
const tmpl = new Template(src, this, opts.path); return tmpl.render(ctx, cb); }
waterfall(tasks, callback, forceAsync) { return waterfall(tasks, callback, forceAsync); }}
class Context extends Obj { init(ctx, blocks, env) { // 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((name) => { this.addBlock(name, blocks[name]); }); }
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]; } }
setVariable(name, val) { this.ctx[name] = val; }
getVariables() { return this.ctx; }
addBlock(name, block) { this.blocks[name] = this.blocks[name] || []; this.blocks[name].push(block); return this; }
getBlock(name) { if (!this.blocks[name]) { throw new Error('unknown block "' + name + '"'); }
return this.blocks[name][0]; }
getSuper(env, name, block, frame, runtime, cb) { const idx = lib.indexOf(this.blocks[name] || [], block); const blk = this.blocks[name][idx + 1]; const context = this;
if (idx === -1 || !blk) { throw new Error('no super block available for "' + name + '"'); }
blk(env, context, frame, runtime, cb); }
addExport(name) { this.exported.push(name); }
getExported() { const exported = {}; this.exported.forEach((name) => { exported[name] = this.ctx[name]; }); return exported; }}
class Template extends Obj { 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; } }
render(ctx, parentFrame, cb) { 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 const forceAsync = !parentFrame;
// Catch compile errors for async rendering try { this.compile(); } catch (e) { const err = lib._prettifyError(this.path, this.env.opts.dev, e); if (cb) { return callbackAsap(cb, err); } else { throw err; } }
const context = new Context(ctx || {}, this.blocks, this.env); const frame = parentFrame ? parentFrame.push(true) : new Frame(); frame.topLevel = true; let syncResult = null; let didError = false;
this.rootRenderFunc(this.env, context, frame, globalRuntime, (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(this.path, this.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; }
getExported(ctx, parentFrame, cb) { 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; } }
const frame = parentFrame ? parentFrame.push() : new Frame(); frame.topLevel = true;
// Run the rootRenderFunc to populate the context with exported vars const context = new Context(ctx || {}, this.blocks, this.env); this.rootRenderFunc(this.env, context, frame, globalRuntime, (err) => { if (err) { cb(err, null); } else { cb(null, context.getExported()); } }); }
compile() { if (!this.compiled) { this._compile(); } }
_compile() { let props;
if (this.tmplProps) { props = this.tmplProps; } else { const source = compiler.compile( this.tmplStr, this.env.asyncFilters, this.env.extensionsList, this.path, this.env.opts, );
const func = new Function(source); props = func(); }
this.blocks = this._getBlocks(props); this.rootRenderFunc = props.root; this.compiled = true; }
_getBlocks(props) { const blocks = {};
lib.keys(props).forEach((k) => { if (k.slice(0, 2) === "b_") { blocks[k.slice(2)] = props[k]; } });
return blocks; }}
export { Environment, Template };