1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
|
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;
|