summaryrefslogtreecommitdiff
path: root/node_modules/@11ty/eleventy/src/TemplateContent.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/TemplateContent.js
parent53d6ae2b5568437afa5e4995580a3fb679b7b91b (diff)
Changed from static to 11ty!
Diffstat (limited to 'node_modules/@11ty/eleventy/src/TemplateContent.js')
-rw-r--r--node_modules/@11ty/eleventy/src/TemplateContent.js748
1 files changed, 748 insertions, 0 deletions
diff --git a/node_modules/@11ty/eleventy/src/TemplateContent.js b/node_modules/@11ty/eleventy/src/TemplateContent.js
new file mode 100644
index 0000000..97d440f
--- /dev/null
+++ b/node_modules/@11ty/eleventy/src/TemplateContent.js
@@ -0,0 +1,748 @@
+import os from "node:os";
+
+import fs from "node:fs";
+import matter from "gray-matter";
+import lodash from "@11ty/lodash-custom";
+import { TemplatePath } from "@11ty/eleventy-utils";
+import debugUtil from "debug";
+
+import TemplateData from "./Data/TemplateData.js";
+import TemplateRender from "./TemplateRender.js";
+import EleventyBaseError from "./Errors/EleventyBaseError.js";
+import EleventyErrorUtil from "./Errors/EleventyErrorUtil.js";
+import eventBus from "./EventBus.js";
+
+import { withResolvers } from "./Util/PromiseUtil.js";
+
+const { set: lodashSet } = lodash;
+const debug = debugUtil("Eleventy:TemplateContent");
+const debugDev = debugUtil("Dev:Eleventy:TemplateContent");
+
+class TemplateContentFrontMatterError extends EleventyBaseError {}
+class TemplateContentCompileError extends EleventyBaseError {}
+class TemplateContentRenderError extends EleventyBaseError {}
+
+class TemplateContent {
+ #initialized = false;
+ #config;
+ #templateRender;
+ #preprocessorEngine;
+ #extensionMap;
+ #configOptions;
+
+ constructor(inputPath, templateConfig) {
+ if (!templateConfig || templateConfig.constructor.name !== "TemplateConfig") {
+ throw new Error("Missing or invalid `templateConfig` argument");
+ }
+ this.eleventyConfig = templateConfig;
+ this.inputPath = inputPath;
+ }
+
+ async asyncTemplateInitialization() {
+ if (!this.hasTemplateRender()) {
+ await this.getTemplateRender();
+ }
+
+ if (this.#initialized) {
+ return;
+ }
+ this.#initialized = true;
+
+ let preprocessorEngineName = this.templateRender.getPreprocessorEngineName();
+ if (preprocessorEngineName && this.templateRender.engine.getName() !== preprocessorEngineName) {
+ let engine = await this.templateRender.getEngineByName(preprocessorEngineName);
+ this.#preprocessorEngine = engine;
+ }
+ }
+
+ resetCachedTemplate({ eleventyConfig }) {
+ this.eleventyConfig = eleventyConfig;
+ }
+
+ get dirs() {
+ return this.eleventyConfig.directories;
+ }
+
+ get inputDir() {
+ return this.dirs.input;
+ }
+
+ get outputDir() {
+ return this.dirs.output;
+ }
+
+ getResetTypes(types) {
+ if (types) {
+ return Object.assign(
+ {
+ data: false,
+ read: false,
+ render: false,
+ },
+ types,
+ );
+ }
+
+ return {
+ data: true,
+ read: true,
+ render: true,
+ };
+ }
+
+ // Called during an incremental build when the template instance is cached but needs to be reset because it has changed
+ resetCaches(types) {
+ types = this.getResetTypes(types);
+
+ if (types.read) {
+ delete this.readingPromise;
+ delete this.inputContent;
+ delete this._frontMatterDataCache;
+ }
+ if (types.render) {
+ this.#templateRender = undefined;
+ }
+ }
+
+ get extensionMap() {
+ if (!this.#extensionMap) {
+ throw new Error("Internal error: Missing `extensionMap` in TemplateContent.");
+ }
+ return this.#extensionMap;
+ }
+
+ set extensionMap(map) {
+ this.#extensionMap = map;
+ }
+
+ set eleventyConfig(config) {
+ this.#config = config;
+
+ if (this.#config.constructor.name === "TemplateConfig") {
+ this.#configOptions = this.#config.getConfig();
+ } else {
+ throw new Error("Tried to get an TemplateConfig but none was found.");
+ }
+ }
+
+ get eleventyConfig() {
+ if (this.#config.constructor.name === "TemplateConfig") {
+ return this.#config;
+ }
+ throw new Error("Tried to get an TemplateConfig but none was found.");
+ }
+
+ get config() {
+ if (this.#config.constructor.name === "TemplateConfig" && !this.#configOptions) {
+ this.#configOptions = this.#config.getConfig();
+ }
+
+ return this.#configOptions;
+ }
+
+ get bench() {
+ return this.config.benchmarkManager.get("Aggregate");
+ }
+
+ get engine() {
+ return this.templateRender.engine;
+ }
+
+ get templateRender() {
+ if (!this.hasTemplateRender()) {
+ throw new Error(`\`templateRender\` has not yet initialized on ${this.inputPath}`);
+ }
+
+ return this.#templateRender;
+ }
+
+ hasTemplateRender() {
+ return !!this.#templateRender;
+ }
+
+ async getTemplateRender() {
+ if (!this.#templateRender) {
+ this.#templateRender = new TemplateRender(this.inputPath, this.eleventyConfig);
+ this.#templateRender.extensionMap = this.extensionMap;
+
+ return this.#templateRender.init().then(() => {
+ return this.#templateRender;
+ });
+ }
+
+ return this.#templateRender;
+ }
+
+ // For monkey patchers
+ get frontMatter() {
+ if (this.frontMatterOverride) {
+ return this.frontMatterOverride;
+ } else {
+ throw new Error(
+ "Unfortunately you’re using code that monkey patched some Eleventy internals and it isn’t async-friendly. Change your code to use the async `read()` method on the template instead!",
+ );
+ }
+ }
+
+ // For monkey patchers
+ set frontMatter(contentOverride) {
+ this.frontMatterOverride = contentOverride;
+ }
+
+ getInputPath() {
+ return this.inputPath;
+ }
+
+ getInputDir() {
+ return this.inputDir;
+ }
+
+ isVirtualTemplate() {
+ let def = this.getVirtualTemplateDefinition();
+ return !!def;
+ }
+
+ getVirtualTemplateDefinition() {
+ let inputDirRelativeInputPath =
+ this.eleventyConfig.directories.getInputPathRelativeToInputDirectory(this.inputPath);
+ return this.config.virtualTemplates[inputDirRelativeInputPath];
+ }
+
+ async #read() {
+ let content = await this.inputContent;
+
+ if (content || content === "") {
+ let tr = await this.getTemplateRender();
+ if (tr.engine.useJavaScriptImport()) {
+ return {
+ data: {},
+ content,
+ };
+ }
+
+ let options = this.config.frontMatterParsingOptions || {};
+ let fm;
+ try {
+ // Added in 3.0, passed along to front matter engines
+ options.filePath = this.inputPath;
+ fm = matter(content, options);
+ } catch (e) {
+ throw new TemplateContentFrontMatterError(
+ `Having trouble reading front matter from template ${this.inputPath}`,
+ e,
+ );
+ }
+
+ if (options.excerpt && fm.excerpt) {
+ let excerptString = fm.excerpt + (options.excerpt_separator || "---");
+ if (fm.content.startsWith(excerptString + os.EOL)) {
+ // with an os-specific newline after excerpt separator
+ fm.content = fm.excerpt.trim() + "\n" + fm.content.slice((excerptString + os.EOL).length);
+ } else if (fm.content.startsWith(excerptString + "\n")) {
+ // with a newline (\n) after excerpt separator
+ // This is necessary for some git configurations on windows
+ fm.content = fm.excerpt.trim() + "\n" + fm.content.slice((excerptString + 1).length);
+ } else if (fm.content.startsWith(excerptString)) {
+ // no newline after excerpt separator
+ fm.content = fm.excerpt + fm.content.slice(excerptString.length);
+ }
+
+ // alias, defaults to page.excerpt
+ let alias = options.excerpt_alias || "page.excerpt";
+ lodashSet(fm.data, alias, fm.excerpt);
+ }
+
+ // For monkey patchers that used `frontMatter` 🤧
+ // https://github.com/11ty/eleventy/issues/613#issuecomment-999637109
+ // https://github.com/11ty/eleventy/issues/2710#issuecomment-1373854834
+ // Removed this._frontMatter monkey patcher help in 3.0.0-alpha.7
+
+ return fm;
+ } else {
+ return {
+ data: {},
+ content: "",
+ excerpt: "",
+ };
+ }
+ }
+
+ async read() {
+ if (!this.readingPromise) {
+ if (!this.inputContent) {
+ // @cachedproperty
+ this.inputContent = this.getInputContent();
+ }
+
+ // @cachedproperty
+ this.readingPromise = this.#read();
+ }
+
+ return this.readingPromise;
+ }
+
+ /* Incremental builds cache the Template instances (in TemplateWriter) but
+ * these template specific caches are important for Pagination */
+ static cache(path, content) {
+ this._inputCache.set(TemplatePath.absolutePath(path), content);
+ }
+
+ static getCached(path) {
+ return this._inputCache.get(TemplatePath.absolutePath(path));
+ }
+
+ static deleteFromInputCache(path) {
+ this._inputCache.delete(TemplatePath.absolutePath(path));
+ }
+
+ // Used via clone
+ setInputContent(content) {
+ this.inputContent = content;
+ }
+
+ async getInputContent() {
+ let tr = await this.getTemplateRender();
+
+ let virtualTemplateDefinition = this.getVirtualTemplateDefinition();
+ if (virtualTemplateDefinition) {
+ let { content } = virtualTemplateDefinition;
+ return content;
+ }
+
+ if (
+ tr.engine.useJavaScriptImport() &&
+ typeof tr.engine.getInstanceFromInputPath === "function"
+ ) {
+ return tr.engine.getInstanceFromInputPath(this.inputPath);
+ }
+
+ if (!tr.engine.needsToReadFileContents()) {
+ return "";
+ }
+
+ let templateBenchmark = this.bench.get("Template Read");
+ templateBenchmark.before();
+
+ let content;
+
+ if (this.config.useTemplateCache) {
+ content = TemplateContent.getCached(this.inputPath);
+ }
+
+ if (!content && content !== "") {
+ let contentBuffer = fs.readFileSync(this.inputPath);
+
+ content = contentBuffer.toString("utf8");
+
+ if (this.config.useTemplateCache) {
+ TemplateContent.cache(this.inputPath, content);
+ }
+ }
+
+ templateBenchmark.after();
+
+ return content;
+ }
+
+ async _testGetFrontMatter() {
+ let fm = this.frontMatterOverride ? this.frontMatterOverride : await this.read();
+
+ return fm;
+ }
+
+ async getPreRender() {
+ let fm = this.frontMatterOverride ? this.frontMatterOverride : await this.read();
+
+ return fm.content;
+ }
+
+ async #getFrontMatterData() {
+ let fm = await this.read();
+
+ // gray-matter isn’t async-friendly but can return a promise from custom front matter
+ if (fm.data instanceof Promise) {
+ fm.data = await fm.data;
+ }
+
+ let tr = await this.getTemplateRender();
+ let extraData = await tr.engine.getExtraDataFromFile(this.inputPath);
+
+ let virtualTemplateDefinition = this.getVirtualTemplateDefinition();
+ let virtualTemplateData;
+ if (virtualTemplateDefinition) {
+ virtualTemplateData = virtualTemplateDefinition.data;
+ }
+
+ let data = Object.assign(fm.data, extraData, virtualTemplateData);
+
+ TemplateData.cleanupData(data, {
+ file: this.inputPath,
+ isVirtualTemplate: Boolean(virtualTemplateData),
+ });
+
+ return {
+ data,
+ excerpt: fm.excerpt,
+ };
+ }
+
+ async getFrontMatterData() {
+ if (!this._frontMatterDataCache) {
+ // @cachedproperty
+ this._frontMatterDataCache = this.#getFrontMatterData();
+ }
+
+ return this._frontMatterDataCache;
+ }
+
+ async getEngineOverride() {
+ return this.getFrontMatterData().then((data) => {
+ return data[this.config.keys.engineOverride];
+ });
+ }
+
+ // checks engines
+ isTemplateCacheable() {
+ if (this.#preprocessorEngine) {
+ return this.#preprocessorEngine.cacheable;
+ }
+ return this.engine.cacheable;
+ }
+
+ _getCompileCache(str) {
+ // Caches used to be bifurcated based on engine name, now they’re based on inputPath
+ // TODO does `cacheable` need to help inform whether a cache is used here?
+ let inputPathMap = TemplateContent._compileCache.get(this.inputPath);
+ if (!inputPathMap) {
+ inputPathMap = new Map();
+ TemplateContent._compileCache.set(this.inputPath, inputPathMap);
+ }
+
+ let cacheable = this.isTemplateCacheable();
+ let { useCache, key } = this.engine.getCompileCacheKey(str, this.inputPath);
+
+ // We also tie the compile cache key to the UserConfig instance, to alleviate issues with global template cache
+ // Better to move the cache to the Eleventy instance instead, no?
+ // (This specifically failed I18nPluginTest cases with filters being cached across tests and not having access to each plugin’s options)
+ key = this.eleventyConfig.userConfig._getUniqueId() + key;
+
+ return [cacheable, key, inputPathMap, useCache];
+ }
+
+ async compile(str, options = {}) {
+ let { type, bypassMarkdown, engineOverride } = options;
+
+ // Must happen before cacheable fetch below
+ // Likely only necessary for Eleventy Layouts, see TemplateMap->initDependencyMap
+ await this.asyncTemplateInitialization();
+
+ // this.templateRender is guaranteed here
+ let tr = await this.getTemplateRender();
+ if (engineOverride !== undefined) {
+ debugDev("%o overriding template engine to use %o", this.inputPath, engineOverride);
+ await tr.setEngineOverride(engineOverride, bypassMarkdown);
+ } else {
+ tr.setUseMarkdown(!bypassMarkdown);
+ }
+ if (bypassMarkdown && !this.engine.needsCompilation(str)) {
+ return function () {
+ return str;
+ };
+ }
+
+ debugDev("%o compile() using engine: %o", this.inputPath, tr.engineName);
+
+ try {
+ let res;
+ if (this.config.useTemplateCache) {
+ let [cacheable, key, cache, useCache] = this._getCompileCache(str);
+ if (cacheable && key) {
+ if (useCache && cache.has(key)) {
+ this.bench.get("(count) Template Compile Cache Hit").incrementCount();
+ return cache.get(key);
+ }
+
+ this.bench.get("(count) Template Compile Cache Miss").incrementCount();
+
+ // Compile cache is cleared when the resource is modified (below)
+
+ // Compilation is async, so we eagerly cache a Promise that eventually
+ // resolves to the compiled function
+ let withRes = withResolvers();
+ res = withRes.resolve;
+
+ cache.set(key, withRes.promise);
+ }
+ }
+
+ let typeStr = type ? ` ${type}` : "";
+ let templateBenchmark = this.bench.get(`Template Compile${typeStr}`);
+ let inputPathBenchmark = this.bench.get(`> Compile${typeStr} > ${this.inputPath}`);
+ templateBenchmark.before();
+ inputPathBenchmark.before();
+
+ let fn = await tr.getCompiledTemplate(str);
+ inputPathBenchmark.after();
+ templateBenchmark.after();
+ debugDev("%o getCompiledTemplate function created", this.inputPath);
+ if (this.config.useTemplateCache && res) {
+ res(fn);
+ }
+ return fn;
+ } catch (e) {
+ let [cacheable, key, cache] = this._getCompileCache(str);
+ if (cacheable && key) {
+ cache.delete(key);
+ }
+ debug(`Having trouble compiling template ${this.inputPath}: %O`, str);
+ throw new TemplateContentCompileError(
+ `Having trouble compiling template ${this.inputPath}`,
+ e,
+ );
+ }
+ }
+
+ getParseForSymbolsFunction(str) {
+ let engine = this.engine;
+
+ // Don’t use markdown as the engine to parse for symbols
+ // TODO pass in engineOverride here
+ if (this.#preprocessorEngine) {
+ engine = this.#preprocessorEngine;
+ }
+
+ if ("parseForSymbols" in engine) {
+ return () => {
+ if (Array.isArray(str)) {
+ return str
+ .filter((entry) => typeof entry === "string")
+ .map((entry) => engine.parseForSymbols(entry))
+ .flat();
+ }
+ if (typeof str === "string") {
+ return engine.parseForSymbols(str);
+ }
+ return [];
+ };
+ }
+ }
+
+ // used by computed data or for permalink functions
+ async _renderFunction(fn, ...args) {
+ let mixins = Object.assign({}, this.config.javascriptFunctions);
+ let result = await fn.call(mixins, ...args);
+
+ // normalize Buffer away if returned from permalink
+ if (Buffer.isBuffer(result)) {
+ return result.toString();
+ }
+
+ return result;
+ }
+
+ async renderComputedData(str, data) {
+ if (typeof str === "function") {
+ return this._renderFunction(str, data);
+ }
+
+ return this._render(str, data, {
+ type: "Computed Data",
+ bypassMarkdown: true,
+ });
+ }
+
+ async renderPermalink(permalink, data) {
+ let tr = await this.getTemplateRender();
+ let permalinkCompilation = tr.engine.permalinkNeedsCompilation(permalink);
+
+ // No string compilation:
+ // ({ compileOptions: { permalink: "raw" }})
+ // These mean `permalink: false`, which is no file system writing:
+ // ({ compileOptions: { permalink: false }})
+ // ({ compileOptions: { permalink: () => false }})
+ // ({ compileOptions: { permalink: () => (() = > false) }})
+ if (permalinkCompilation === false && typeof permalink !== "function") {
+ return permalink;
+ }
+
+ /* Custom `compile` function for permalinks, usage:
+ permalink: function(permalinkString, inputPath) {
+ return async function(data) {
+ return "THIS IS MY RENDERED PERMALINK";
+ }
+ }
+ */
+ if (permalinkCompilation && typeof permalinkCompilation === "function") {
+ permalink = await this._renderFunction(permalinkCompilation, permalink, this.inputPath);
+ }
+
+ // Raw permalink function (in the app code data cascade)
+ if (typeof permalink === "function") {
+ return this._renderFunction(permalink, data);
+ }
+
+ return this._render(permalink, data, {
+ type: "Permalink",
+ bypassMarkdown: true,
+ });
+ }
+
+ async render(str, data, bypassMarkdown) {
+ return this._render(str, data, {
+ type: "Content",
+ bypassMarkdown,
+ });
+ }
+
+ _getPaginationLogSuffix(data) {
+ let suffix = [];
+ if ("pagination" in data) {
+ suffix.push(" (");
+ if (data.pagination.pages) {
+ suffix.push(
+ `${data.pagination.pages.length} page${data.pagination.pages.length !== 1 ? "s" : ""}`,
+ );
+ } else {
+ suffix.push("Pagination");
+ }
+ suffix.push(")");
+ }
+ return suffix.join("");
+ }
+
+ async _render(str, data, options = {}) {
+ let { bypassMarkdown, type } = options;
+
+ try {
+ if (bypassMarkdown && !this.engine.needsCompilation(str)) {
+ return str;
+ }
+
+ let fn = await this.compile(str, {
+ bypassMarkdown,
+ engineOverride: data[this.config.keys.engineOverride],
+ type,
+ });
+
+ if (fn === undefined) {
+ return;
+ } else if (typeof fn !== "function") {
+ throw new Error(`The \`compile\` function did not return a function. Received ${fn}`);
+ }
+
+ // Benchmark
+ let templateBenchmark = this.bench.get("Render");
+ let inputPathBenchmark = this.bench.get(
+ `> Render${type ? ` ${type}` : ""} > ${this.inputPath}${this._getPaginationLogSuffix(data)}`,
+ );
+
+ templateBenchmark.before();
+ if (inputPathBenchmark) {
+ inputPathBenchmark.before();
+ }
+
+ let rendered = await fn(data);
+
+ if (inputPathBenchmark) {
+ inputPathBenchmark.after();
+ }
+ templateBenchmark.after();
+ debugDev("%o getCompiledTemplate called, rendered content created", this.inputPath);
+ return rendered;
+ } catch (e) {
+ if (EleventyErrorUtil.isPrematureTemplateContentError(e)) {
+ return Promise.reject(e);
+ } else {
+ let tr = await this.getTemplateRender();
+ let engine = tr.getReadableEnginesList();
+ debug(`Having trouble rendering ${engine} template ${this.inputPath}: %O`, str);
+ return Promise.reject(
+ new TemplateContentRenderError(
+ `Having trouble rendering ${engine} template ${this.inputPath}`,
+ e,
+ ),
+ );
+ }
+ }
+ }
+
+ getExtensionEntries() {
+ return this.engine.extensionEntries;
+ }
+
+ isFileRelevantToThisTemplate(incrementalFile, metadata = {}) {
+ // always relevant if incremental file not set (build everything)
+ if (!incrementalFile) {
+ return true;
+ }
+
+ let hasDependencies = this.engine.hasDependencies(incrementalFile);
+
+ let isRelevant = this.engine.isFileRelevantTo(this.inputPath, incrementalFile);
+
+ debug(
+ "Test dependencies to see if %o is relevant to %o: %o",
+ this.inputPath,
+ incrementalFile,
+ isRelevant,
+ );
+
+ let extensionEntries = this.getExtensionEntries().filter((entry) => !!entry.isIncrementalMatch);
+ if (extensionEntries.length) {
+ for (let entry of extensionEntries) {
+ if (
+ entry.isIncrementalMatch.call(
+ {
+ inputPath: this.inputPath,
+ isFullTemplate: metadata.isFullTemplate,
+ isFileRelevantToInputPath: isRelevant,
+ doesFileHaveDependencies: hasDependencies,
+ },
+ incrementalFile,
+ )
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+ } else {
+ // Not great way of building all templates if this is a layout, include, JS dependency.
+ // TODO improve this for default template syntaxes
+
+ // This is the fallback way of determining if something is incremental (no isIncrementalMatch available)
+ // This will be true if the inputPath and incrementalFile are the same
+ if (isRelevant) {
+ return true;
+ }
+
+ // only return true here if dependencies are not known
+ if (!hasDependencies && !metadata.isFullTemplate) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
+
+TemplateContent._inputCache = new Map();
+TemplateContent._compileCache = new Map();
+eventBus.on("eleventy.resourceModified", (path) => {
+ // delete from input cache
+ TemplateContent.deleteFromInputCache(path);
+
+ // delete from compile cache
+ let normalized = TemplatePath.addLeadingDotSlash(path);
+ let compileCache = TemplateContent._compileCache.get(normalized);
+ if (compileCache) {
+ compileCache.clear();
+ }
+});
+
+// Used when the configuration file reset https://github.com/11ty/eleventy/issues/2147
+eventBus.on("eleventy.compileCacheReset", () => {
+ TemplateContent._compileCache = new Map();
+});
+
+export default TemplateContent;