448 lines
12 KiB
JavaScript
Executable File
448 lines
12 KiB
JavaScript
Executable File
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());
|
||
}
|
||
|
||
// 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) {
|
||
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;
|