194 lines
6.3 KiB
JavaScript
194 lines
6.3 KiB
JavaScript
|
|
import debugUtil from "debug";
|
|||
|
|
import EleventyBaseError from "../Errors/EleventyBaseError.js";
|
|||
|
|
|
|||
|
|
const debug = debugUtil("Eleventy:TemplateEngineManager");
|
|||
|
|
|
|||
|
|
class TemplateEngineManager {
|
|||
|
|
constructor(eleventyConfig) {
|
|||
|
|
if (!eleventyConfig || eleventyConfig.constructor.name !== "TemplateConfig") {
|
|||
|
|
throw new EleventyBaseError("Missing or invalid `config` argument.");
|
|||
|
|
}
|
|||
|
|
this.eleventyConfig = eleventyConfig;
|
|||
|
|
|
|||
|
|
this.engineCache = {};
|
|||
|
|
this.importCache = {};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
get config() {
|
|||
|
|
return this.eleventyConfig.getConfig();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static isAlias(entry) {
|
|||
|
|
if (entry.aliasKey) {
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return entry.key !== entry.extension;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static isSimpleAlias(entry) {
|
|||
|
|
if (!this.isAlias(entry)) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// has keys other than key, extension, and aliasKey
|
|||
|
|
return (
|
|||
|
|
Object.keys(entry).some((key) => {
|
|||
|
|
return key !== "key" && key !== "extension" && key !== "aliasKey";
|
|||
|
|
}) === false
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
get keyToClassNameMap() {
|
|||
|
|
if (!this._keyToClassNameMap) {
|
|||
|
|
this._keyToClassNameMap = {
|
|||
|
|
md: "Markdown",
|
|||
|
|
html: "Html",
|
|||
|
|
njk: "Nunjucks",
|
|||
|
|
liquid: "Liquid",
|
|||
|
|
"11ty.js": "JavaScript",
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Custom entries *can* overwrite default entries above
|
|||
|
|
if ("extensionMap" in this.config) {
|
|||
|
|
for (let entry of this.config.extensionMap) {
|
|||
|
|
// either the key does not already exist or it is not a simple alias and is an override: https://v3.11ty.dev/docs/languages/custom/#overriding-an-existing-template-language
|
|||
|
|
let existingTarget = this._keyToClassNameMap[entry.key];
|
|||
|
|
let isAlias = TemplateEngineManager.isAlias(entry);
|
|||
|
|
|
|||
|
|
if (!existingTarget && isAlias) {
|
|||
|
|
throw new Error(
|
|||
|
|
`An attempt to alias ${entry.aliasKey} to ${entry.key} was made, but ${entry.key} is not a recognized template syntax.`,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (isAlias) {
|
|||
|
|
// only `key` and `extension`, not `compile` or other options
|
|||
|
|
if (!TemplateEngineManager.isSimpleAlias(entry)) {
|
|||
|
|
this._keyToClassNameMap[entry.aliasKey] = "Custom";
|
|||
|
|
} else {
|
|||
|
|
this._keyToClassNameMap[entry.aliasKey] = this._keyToClassNameMap[entry.key];
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// not an alias, so `key` and `extension` are the same here.
|
|||
|
|
// *can* override a built-in extension!
|
|||
|
|
this._keyToClassNameMap[entry.key] = "Custom";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return this._keyToClassNameMap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
reset() {
|
|||
|
|
this.engineCache = {};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getClassNameFromTemplateKey(key) {
|
|||
|
|
return this.keyToClassNameMap[key];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
hasEngine(name) {
|
|||
|
|
return !!this.getClassNameFromTemplateKey(name);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async getEngineClassByExtension(extension) {
|
|||
|
|
if (this.importCache[extension]) {
|
|||
|
|
return this.importCache[extension];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let promise;
|
|||
|
|
|
|||
|
|
// We include these as raw strings (and not more readable variables) so they’re parsed by a bundler.
|
|||
|
|
if (extension === "md") {
|
|||
|
|
promise = import("./Markdown.js").then((mod) => mod.default);
|
|||
|
|
} else if (extension === "html") {
|
|||
|
|
promise = import("./Html.js").then((mod) => mod.default);
|
|||
|
|
} else if (extension === "njk") {
|
|||
|
|
promise = import("./Nunjucks.js").then((mod) => mod.default);
|
|||
|
|
} else if (extension === "liquid") {
|
|||
|
|
promise = import("./Liquid.js").then((mod) => mod.default);
|
|||
|
|
} else if (extension === "11ty.js") {
|
|||
|
|
promise = import("./JavaScript.js").then((mod) => mod.default);
|
|||
|
|
} else {
|
|||
|
|
promise = this.getCustomEngineClass();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.importCache[extension] = promise;
|
|||
|
|
|
|||
|
|
return promise;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async getCustomEngineClass() {
|
|||
|
|
if (!this._CustomEngine) {
|
|||
|
|
this._CustomEngine = import("./Custom.js").then((mod) => mod.default);
|
|||
|
|
}
|
|||
|
|
return this._CustomEngine;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async #getEngine(name, extensionMap) {
|
|||
|
|
let cls = await this.getEngineClassByExtension(name);
|
|||
|
|
let instance = new cls(name, this.eleventyConfig);
|
|||
|
|
instance.extensionMap = extensionMap;
|
|||
|
|
instance.engineManager = this;
|
|||
|
|
|
|||
|
|
let extensionEntry = extensionMap.getExtensionEntry(name);
|
|||
|
|
|
|||
|
|
// Override a built-in extension (md => md)
|
|||
|
|
// If provided a "Custom" engine using addExtension, but that engine's instance is *not* custom,
|
|||
|
|
// The user must be overriding a built-in engine i.e. addExtension('md', { ...overrideBehavior })
|
|||
|
|
let className = this.getClassNameFromTemplateKey(name);
|
|||
|
|
|
|||
|
|
if (className === "Custom" && instance.constructor.name !== "CustomEngine") {
|
|||
|
|
let CustomEngine = await this.getCustomEngineClass();
|
|||
|
|
let overrideCustomEngine = new CustomEngine(name, this.eleventyConfig);
|
|||
|
|
|
|||
|
|
// Keep track of the "default" engine 11ty would normally use
|
|||
|
|
// This allows the user to access the default engine in their override
|
|||
|
|
overrideCustomEngine.setDefaultEngine(instance);
|
|||
|
|
|
|||
|
|
instance = overrideCustomEngine;
|
|||
|
|
// Alias to a built-in extension (11ty.tsx => 11ty.js)
|
|||
|
|
} else if (
|
|||
|
|
instance.constructor.name === "CustomEngine" &&
|
|||
|
|
TemplateEngineManager.isAlias(extensionEntry)
|
|||
|
|
) {
|
|||
|
|
// add defaultRenderer for complex aliases with their own compile functions.
|
|||
|
|
let originalEngineInstance = await this.getEngine(extensionEntry.key, extensionMap);
|
|||
|
|
instance.setDefaultEngine(originalEngineInstance);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return instance;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isEngineRemovedFromCore(name) {
|
|||
|
|
return ["ejs", "hbs", "mustache", "haml", "pug"].includes(name) && !this.hasEngine(name);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async getEngine(name, extensionMap) {
|
|||
|
|
// Bundled engine deprecation
|
|||
|
|
if (this.isEngineRemovedFromCore(name)) {
|
|||
|
|
throw new Error(
|
|||
|
|
`Per the 11ty Community Survey (2023), the "${name}" template language was moved from core to an officially supported plugin in v3.0. These plugins live here: https://github.com/11ty/eleventy-plugin-template-languages and are documented on their respective template language docs at https://v3.11ty.dev/docs/languages/ You are also empowered to implement *any* template language yourself using https://v3.11ty.dev/docs/languages/custom/`,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!this.hasEngine(name)) {
|
|||
|
|
throw new Error(`Template Engine ${name} does not exist in getEngine()`);
|
|||
|
|
}
|
|||
|
|
// TODO these cached engines should be based on extensions not name, then we can remove the error in
|
|||
|
|
// "Double override (not aliases) throws an error" test in TemplateRenderCustomTest.js
|
|||
|
|
if (!this.engineCache[name]) {
|
|||
|
|
debug("Engine cache miss %o (should only happen once per engine type)", name);
|
|||
|
|
// Make sure cache key is based on name and not path
|
|||
|
|
// Custom class is used for all plugins, cache once per plugin
|
|||
|
|
this.engineCache[name] = this.#getEngine(name, extensionMap);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return this.engineCache[name];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default TemplateEngineManager;
|