Files
homepage/node_modules/@11ty/eleventy/src/Engines/Nunjucks.js
2024-11-03 17:16:20 +01:00

448 lines
12 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import NunjucksLib from "nunjucks";
import { TemplatePath } from "@11ty/eleventy-utils";
import TemplateEngine from "./TemplateEngine.js";
import EleventyBaseError from "../Errors/EleventyBaseError.js";
import EventBusUtil from "../Util/EventBusUtil.js";
import { augmentObject } from "./Util/ContextAugmenter.js";
class EleventyNunjucksError extends EleventyBaseError {}
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);
this.cacheable = true;
}
_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 paths = new Set();
paths.add(super.getIncludesDir());
paths.add(TemplatePath.getWorkingDir());
// Filter out undefined paths
let fsLoader = new NunjucksLib.FileSystemLoader(Array.from(paths).filter(Boolean));
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);
// Correct, but overbroad. Better would be to evict more granularly, but
// resolution from paths isn't straightforward.
EventBusUtil.soloOn("eleventy.templateModified", (/*path*/) => {
this.njkEnv.invalidateCache();
});
this.setEngineLib(this.njkEnv);
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());
}
// Dont 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) {
const { parser, nodes } = NunjucksLib;
let obj = parser.parse(str, this._getParseExtensions());
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) {
return new Promise(function (resolve, reject) {
tmpl.render(data, function (err, res) {
if (err) {
reject(err);
} else {
resolve(res);
}
});
});
};
}
}
export default Nunjucks;