summaryrefslogtreecommitdiff
path: root/node_modules/@11ty/eleventy/src/Engines/Nunjucks.js
diff options
context:
space:
mode:
authorShipwreckt <me@shipwreckt.co.uk>2025-10-31 20:02:14 +0000
committerShipwreckt <me@shipwreckt.co.uk>2025-10-31 20:02:14 +0000
commit7a52ddeba2a68388b544f529d2d92104420f77b0 (patch)
tree15ddd47457a2cb4a96060747437d36474e4f6b4e /node_modules/@11ty/eleventy/src/Engines/Nunjucks.js
parent53d6ae2b5568437afa5e4995580a3fb679b7b91b (diff)
Changed from static to 11ty!
Diffstat (limited to 'node_modules/@11ty/eleventy/src/Engines/Nunjucks.js')
-rwxr-xr-xnode_modules/@11ty/eleventy/src/Engines/Nunjucks.js482
1 files changed, 482 insertions, 0 deletions
diff --git a/node_modules/@11ty/eleventy/src/Engines/Nunjucks.js b/node_modules/@11ty/eleventy/src/Engines/Nunjucks.js
new file mode 100755
index 0000000..70cca17
--- /dev/null
+++ b/node_modules/@11ty/eleventy/src/Engines/Nunjucks.js
@@ -0,0 +1,482 @@
+import NunjucksLib from "nunjucks";
+import debugUtil from "debug";
+import { TemplatePath } from "@11ty/eleventy-utils";
+
+import TemplateEngine from "./TemplateEngine.js";
+import EleventyBaseError from "../Errors/EleventyBaseError.js";
+import { augmentObject } from "./Util/ContextAugmenter.js";
+import { withResolvers } from "../Util/PromiseUtil.js";
+
+const debug = debugUtil("Eleventy:Nunjucks");
+
+class EleventyNunjucksError extends EleventyBaseError {}
+
+export default class Nunjucks extends TemplateEngine {
+ constructor(name, eleventyConfig) {
+ super(name, eleventyConfig);
+
+ this.nunjucksEnvironmentOptions = this.config.nunjucksEnvironmentOptions || { dev: true };
+
+ this.nunjucksPrecompiledTemplates = this.config.nunjucksPrecompiledTemplates || {};
+ this._usingPrecompiled = Object.keys(this.nunjucksPrecompiledTemplates).length > 0;
+
+ this.setLibrary(this.config.libraryOverrides.njk);
+ }
+
+ // v3.1.0-alpha.1 we’ve moved to use Nunjucks’ internal cache instead of Eleventy’s
+ // get cacheable() {
+ // return false;
+ // }
+
+ #getFileSystemDirs() {
+ let paths = new Set();
+ paths.add(super.getIncludesDir());
+ paths.add(TemplatePath.getWorkingDir());
+
+ // Filter out undefined paths
+ return Array.from(paths).filter(Boolean);
+ }
+
+ #setEnv(override) {
+ if (override) {
+ this.njkEnv = override;
+ } else if (this._usingPrecompiled) {
+ // Precompiled templates to avoid eval!
+ const NodePrecompiledLoader = function () {};
+
+ NodePrecompiledLoader.prototype.getSource = (name) => {
+ // https://github.com/mozilla/nunjucks/blob/fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/precompiled-loader.js#L5
+ return {
+ src: {
+ type: "code",
+ obj: this.nunjucksPrecompiledTemplates[name],
+ },
+ // Maybe add this?
+ // path,
+ // noCache: true
+ };
+ };
+
+ this.njkEnv = new NunjucksLib.Environment(
+ new NodePrecompiledLoader(),
+ this.nunjucksEnvironmentOptions,
+ );
+ } else {
+ let fsLoader = new NunjucksLib.FileSystemLoader(this.#getFileSystemDirs());
+ this.njkEnv = new NunjucksLib.Environment(fsLoader, this.nunjucksEnvironmentOptions);
+ }
+
+ this.config.events.emit("eleventy.engine.njk", {
+ nunjucks: NunjucksLib,
+ environment: this.njkEnv,
+ });
+ }
+
+ setLibrary(override) {
+ this.#setEnv(override);
+
+ // Note that a new Nunjucks engine instance is created for subsequent builds
+ // Eleventy Nunjucks is set to `cacheable` false above to opt out of Eleventy cache
+ this.config.events.on("eleventy#templateModified", (templatePath) => {
+ // NunjucksEnvironment:
+ // loader.pathToNames: {'ABSOLUTE_PATH/src/_includes/components/possum-home.css': 'components/possum-home.css'}
+ // loader.cache: { 'components/possum-home.css': [Template] }
+ // Nunjucks stores these as Operating System native paths
+ let absTmplPath = TemplatePath.normalizeOperatingSystemFilePath(
+ TemplatePath.absolutePath(templatePath),
+ );
+ for (let loader of this.njkEnv.loaders) {
+ let nunjucksName = loader.pathsToNames[absTmplPath];
+ if (nunjucksName) {
+ debug(
+ "Match found in Nunjucks cache via templateModified for %o, clearing this entry",
+ templatePath,
+ );
+ delete loader.pathsToNames[absTmplPath];
+ delete loader.cache[nunjucksName];
+ }
+ }
+
+ // Behavior prior to v3.1.0-alpha.1:
+ // this.njkEnv.invalidateCache();
+ });
+
+ this.setEngineLib(this.njkEnv, Boolean(this.config.libraryOverrides.njk));
+
+ this.addFilters(this.config.nunjucksFilters);
+ this.addFilters(this.config.nunjucksAsyncFilters, true);
+
+ // TODO these all go to the same place (addTag), add warnings for overwrites
+ // TODO(zachleat): variableName should work with quotes or without quotes (same as {% set %})
+ this.addPairedShortcode("setAsync", function (content, variableName) {
+ this.ctx[variableName] = content;
+ return "";
+ });
+
+ this.addCustomTags(this.config.nunjucksTags);
+ this.addAllShortcodes(this.config.nunjucksShortcodes);
+ this.addAllShortcodes(this.config.nunjucksAsyncShortcodes, true);
+ this.addAllPairedShortcodes(this.config.nunjucksPairedShortcodes);
+ this.addAllPairedShortcodes(this.config.nunjucksAsyncPairedShortcodes, true);
+ this.addGlobals(this.config.nunjucksGlobals);
+ }
+
+ addFilters(filters, isAsync) {
+ for (let name in filters) {
+ this.njkEnv.addFilter(name, Nunjucks.wrapFilter(name, filters[name]), isAsync);
+ }
+ }
+
+ static wrapFilter(name, fn) {
+ return function (...args) {
+ try {
+ augmentObject(this, {
+ source: this.ctx,
+ lazy: false, // context.env?.opts.throwOnUndefined,
+ });
+
+ return fn.call(this, ...args);
+ } catch (e) {
+ throw new EleventyNunjucksError(
+ `Error in Nunjucks Filter \`${name}\`${this.page ? ` (${this.page.inputPath})` : ""}`,
+ e,
+ );
+ }
+ };
+ }
+
+ // Shortcodes
+ static normalizeContext(context) {
+ let obj = {};
+ if (context.ctx) {
+ obj.ctx = context.ctx;
+ obj.env = context.env;
+
+ augmentObject(obj, {
+ source: context.ctx,
+ lazy: false, // context.env?.opts.throwOnUndefined,
+ });
+ }
+ return obj;
+ }
+
+ addCustomTags(tags) {
+ for (let name in tags) {
+ this.addTag(name, tags[name]);
+ }
+ }
+
+ addTag(name, tagFn) {
+ let tagObj;
+ if (typeof tagFn === "function") {
+ tagObj = tagFn(NunjucksLib, this.njkEnv);
+ } else {
+ throw new Error(
+ "Nunjucks.addTag expects a callback function to be passed in: addTag(name, function(nunjucksEngine) {})",
+ );
+ }
+
+ this.njkEnv.addExtension(name, tagObj);
+ }
+
+ addGlobals(globals) {
+ for (let name in globals) {
+ this.addGlobal(name, globals[name]);
+ }
+ }
+
+ addGlobal(name, globalFn) {
+ this.njkEnv.addGlobal(name, globalFn);
+ }
+
+ addAllShortcodes(shortcodes, isAsync = false) {
+ for (let name in shortcodes) {
+ this.addShortcode(name, shortcodes[name], isAsync);
+ }
+ }
+
+ addAllPairedShortcodes(shortcodes, isAsync = false) {
+ for (let name in shortcodes) {
+ this.addPairedShortcode(name, shortcodes[name], isAsync);
+ }
+ }
+
+ _getShortcodeFn(shortcodeName, shortcodeFn, isAsync = false) {
+ return function ShortcodeFunction() {
+ this.tags = [shortcodeName];
+
+ this.parse = function (parser, nodes) {
+ let args;
+ let tok = parser.nextToken();
+
+ args = parser.parseSignature(true, true);
+
+ // Nunjucks bug with non-paired custom tags bug still exists even
+ // though this issue is closed. Works fine for paired.
+ // https://github.com/mozilla/nunjucks/issues/158
+ if (args.children.length === 0) {
+ args.addChild(new nodes.Literal(0, 0, ""));
+ }
+
+ parser.advanceAfterBlockEnd(tok.value);
+ if (isAsync) {
+ return new nodes.CallExtensionAsync(this, "run", args);
+ }
+ return new nodes.CallExtension(this, "run", args);
+ };
+
+ this.run = function (...args) {
+ let resolve;
+ if (isAsync) {
+ resolve = args.pop();
+ }
+
+ let [context, ...argArray] = args;
+
+ if (isAsync) {
+ let ret = shortcodeFn.call(Nunjucks.normalizeContext(context), ...argArray);
+
+ // #3286 error messaging when the shortcode is not a promise
+ if (!ret?.then) {
+ resolve(
+ new EleventyNunjucksError(
+ `Error with Nunjucks shortcode \`${shortcodeName}\`: it was defined as asynchronous but was actually synchronous. This is important for Nunjucks.`,
+ ),
+ );
+ }
+
+ ret.then(
+ function (returnValue) {
+ resolve(null, new NunjucksLib.runtime.SafeString("" + returnValue));
+ },
+ function (e) {
+ resolve(
+ new EleventyNunjucksError(`Error with Nunjucks shortcode \`${shortcodeName}\``, e),
+ );
+ },
+ );
+ } else {
+ try {
+ let ret = shortcodeFn.call(Nunjucks.normalizeContext(context), ...argArray);
+ return new NunjucksLib.runtime.SafeString("" + ret);
+ } catch (e) {
+ throw new EleventyNunjucksError(
+ `Error with Nunjucks shortcode \`${shortcodeName}\``,
+ e,
+ );
+ }
+ }
+ };
+ };
+ }
+
+ _getPairedShortcodeFn(shortcodeName, shortcodeFn, isAsync = false) {
+ return function PairedShortcodeFunction() {
+ this.tags = [shortcodeName];
+
+ this.parse = function (parser, nodes) {
+ var tok = parser.nextToken();
+
+ var args = parser.parseSignature(true, true);
+ parser.advanceAfterBlockEnd(tok.value);
+
+ var body = parser.parseUntilBlocks("end" + shortcodeName);
+ parser.advanceAfterBlockEnd();
+
+ return new nodes.CallExtensionAsync(this, "run", args, [body]);
+ };
+
+ this.run = function (...args) {
+ let resolve = args.pop();
+ let body = args.pop();
+ let [context, ...argArray] = args;
+
+ body(function (e, bodyContent) {
+ if (e) {
+ resolve(
+ new EleventyNunjucksError(
+ `Error with Nunjucks paired shortcode \`${shortcodeName}\``,
+ e,
+ ),
+ );
+ }
+
+ if (isAsync) {
+ let ret = shortcodeFn.call(
+ Nunjucks.normalizeContext(context),
+ bodyContent,
+ ...argArray,
+ );
+
+ // #3286 error messaging when the shortcode is not a promise
+ if (!ret?.then) {
+ throw new EleventyNunjucksError(
+ `Error with Nunjucks shortcode \`${shortcodeName}\`: it was defined as asynchronous but was actually synchronous. This is important for Nunjucks.`,
+ );
+ }
+
+ ret.then(
+ function (returnValue) {
+ resolve(null, new NunjucksLib.runtime.SafeString(returnValue));
+ },
+ function (e) {
+ resolve(
+ new EleventyNunjucksError(
+ `Error with Nunjucks paired shortcode \`${shortcodeName}\``,
+ e,
+ ),
+ );
+ },
+ );
+ } else {
+ try {
+ resolve(
+ null,
+ new NunjucksLib.runtime.SafeString(
+ shortcodeFn.call(Nunjucks.normalizeContext(context), bodyContent, ...argArray),
+ ),
+ );
+ } catch (e) {
+ resolve(
+ new EleventyNunjucksError(
+ `Error with Nunjucks paired shortcode \`${shortcodeName}\``,
+ e,
+ ),
+ );
+ }
+ }
+ });
+ };
+ };
+ }
+
+ addShortcode(shortcodeName, shortcodeFn, isAsync = false) {
+ let fn = this._getShortcodeFn(shortcodeName, shortcodeFn, isAsync);
+ this.njkEnv.addExtension(shortcodeName, new fn());
+ }
+
+ addPairedShortcode(shortcodeName, shortcodeFn, isAsync = false) {
+ let fn = this._getPairedShortcodeFn(shortcodeName, shortcodeFn, isAsync);
+ this.njkEnv.addExtension(shortcodeName, new fn());
+ }
+
+ // Don’t return a boolean if permalink is a function (see TemplateContent->renderPermalink)
+ permalinkNeedsCompilation(str) {
+ if (typeof str === "string") {
+ return this.needsCompilation(str);
+ }
+ }
+
+ needsCompilation(str) {
+ // Defend against syntax customisations:
+ // https://mozilla.github.io/nunjucks/api.html#customizing-syntax
+ let optsTags = this.njkEnv.opts.tags || {};
+ let blockStart = optsTags.blockStart || "{%";
+ let variableStart = optsTags.variableStart || "{{";
+ let commentStart = optsTags.variableStart || "{#";
+
+ return (
+ str.indexOf(blockStart) !== -1 ||
+ str.indexOf(variableStart) !== -1 ||
+ str.indexOf(commentStart) !== -1
+ );
+ }
+
+ _getParseExtensions() {
+ if (this._parseExtensions) {
+ return this._parseExtensions;
+ }
+
+ // add extensions so the parser knows about our custom tags/blocks
+ let ext = [];
+ for (let name in this.config.nunjucksTags) {
+ let fn = this._getShortcodeFn(name, () => {});
+ ext.push(new fn());
+ }
+ for (let name in this.config.nunjucksShortcodes) {
+ let fn = this._getShortcodeFn(name, () => {});
+ ext.push(new fn());
+ }
+ for (let name in this.config.nunjucksAsyncShortcodes) {
+ let fn = this._getShortcodeFn(name, () => {}, true);
+ ext.push(new fn());
+ }
+ for (let name in this.config.nunjucksPairedShortcodes) {
+ let fn = this._getPairedShortcodeFn(name, () => {});
+ ext.push(new fn());
+ }
+ for (let name in this.config.nunjucksAsyncPairedShortcodes) {
+ let fn = this._getPairedShortcodeFn(name, () => {}, true);
+ ext.push(new fn());
+ }
+
+ this._parseExtensions = ext;
+ return ext;
+ }
+
+ /* Outputs an Array of lodash get selectors */
+ parseForSymbols(str) {
+ if (!str) {
+ return [];
+ }
+ const { parser, nodes } = NunjucksLib;
+ let obj = parser.parse(str, this._getParseExtensions());
+ if (!obj) {
+ return [];
+ }
+ let linesplit = str.split("\n");
+ let values = obj.findAll(nodes.Value);
+ let symbols = obj.findAll(nodes.Symbol).map((entry) => {
+ let name = [entry.value];
+ let nestedIndex = -1;
+ for (let val of values) {
+ if (nestedIndex > -1) {
+ /* deep.object.syntax */
+ if (linesplit[val.lineno].charAt(nestedIndex) === ".") {
+ name.push(val.value);
+ nestedIndex += val.value.length + 1;
+ } else {
+ nestedIndex = -1;
+ }
+ } else if (
+ val.lineno === entry.lineno &&
+ val.colno === entry.colno &&
+ val.value === entry.value
+ ) {
+ nestedIndex = entry.colno + entry.value.length;
+ }
+ }
+ return name.join(".");
+ });
+
+ let uniqueSymbols = Array.from(new Set(symbols));
+ return uniqueSymbols;
+ }
+
+ async compile(str, inputPath) {
+ let tmpl;
+
+ // *All* templates are precompiled to avoid runtime eval
+ if (this._usingPrecompiled) {
+ tmpl = this.njkEnv.getTemplate(str, true);
+ } else if (!inputPath || inputPath === "njk" || inputPath === "md") {
+ tmpl = new NunjucksLib.Template(str, this.njkEnv, null, false);
+ } else {
+ tmpl = new NunjucksLib.Template(str, this.njkEnv, inputPath, false);
+ }
+
+ return function (data) {
+ let { promise, resolve, reject } = withResolvers();
+
+ tmpl.render(data, (error, result) => {
+ if (error) {
+ reject(error);
+ } else {
+ resolve(result);
+ }
+ });
+
+ return promise;
+ };
+ }
+}