summaryrefslogtreecommitdiff
path: root/node_modules/@11ty/eleventy/src/Template.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/Template.js
parent53d6ae2b5568437afa5e4995580a3fb679b7b91b (diff)
Changed from static to 11ty!
Diffstat (limited to 'node_modules/@11ty/eleventy/src/Template.js')
-rwxr-xr-xnode_modules/@11ty/eleventy/src/Template.js1200
1 files changed, 1200 insertions, 0 deletions
diff --git a/node_modules/@11ty/eleventy/src/Template.js b/node_modules/@11ty/eleventy/src/Template.js
new file mode 100755
index 0000000..64c4709
--- /dev/null
+++ b/node_modules/@11ty/eleventy/src/Template.js
@@ -0,0 +1,1200 @@
+import util from "node:util";
+import os from "node:os";
+import path from "node:path";
+import fs from "node:fs";
+
+import lodash from "@11ty/lodash-custom";
+import { DateTime } from "luxon";
+import { TemplatePath, isPlainObject } from "@11ty/eleventy-utils";
+import debugUtil from "debug";
+import chalk from "kleur";
+
+import ConsoleLogger from "./Util/ConsoleLogger.js";
+import getDateFromGitLastUpdated from "./Util/DateGitLastUpdated.js";
+import getDateFromGitFirstAdded from "./Util/DateGitFirstAdded.js";
+import TemplateData from "./Data/TemplateData.js";
+import TemplateContent from "./TemplateContent.js";
+import TemplatePermalink from "./TemplatePermalink.js";
+import TemplateLayout from "./TemplateLayout.js";
+import TemplateFileSlug from "./TemplateFileSlug.js";
+import ComputedData from "./Data/ComputedData.js";
+import Pagination from "./Plugins/Pagination.js";
+import TemplateBehavior from "./TemplateBehavior.js";
+import TemplateContentPrematureUseError from "./Errors/TemplateContentPrematureUseError.js";
+import TemplateContentUnrenderedTemplateError from "./Errors/TemplateContentUnrenderedTemplateError.js";
+import EleventyBaseError from "./Errors/EleventyBaseError.js";
+import ReservedData from "./Util/ReservedData.js";
+import TransformsUtil from "./Util/TransformsUtil.js";
+import { FileSystemManager } from "./Util/FileSystemManager.js";
+
+const { set: lodashSet, get: lodashGet } = lodash;
+const fsStat = util.promisify(fs.stat);
+
+const debug = debugUtil("Eleventy:Template");
+const debugDev = debugUtil("Dev:Eleventy:Template");
+
+class Template extends TemplateContent {
+ #logger;
+ #fsManager;
+
+ constructor(templatePath, templateData, extensionMap, config) {
+ debugDev("new Template(%o)", templatePath);
+ super(templatePath, config);
+
+ this.parsed = path.parse(templatePath);
+
+ // for pagination
+ this.extraOutputSubdirectory = "";
+
+ this.extensionMap = extensionMap;
+ this.templateData = templateData;
+ this.#initFileSlug();
+
+ this.linters = [];
+ this.transforms = {};
+
+ this.isVerbose = true;
+ this.isDryRun = false;
+ this.writeCount = 0;
+
+ this.fileSlug = new TemplateFileSlug(this.inputPath, this.extensionMap, this.eleventyConfig);
+ this.fileSlugStr = this.fileSlug.getSlug();
+ this.filePathStem = this.fileSlug.getFullPathWithoutExtension();
+
+ this.outputFormat = "fs";
+
+ this.behavior = new TemplateBehavior(this.config);
+ this.behavior.setOutputFormat(this.outputFormat);
+ }
+
+ #initFileSlug() {
+ this.fileSlug = new TemplateFileSlug(this.inputPath, this.extensionMap, this.eleventyConfig);
+ this.fileSlugStr = this.fileSlug.getSlug();
+ this.filePathStem = this.fileSlug.getFullPathWithoutExtension();
+ }
+
+ /* mimic constructor arg order */
+ resetCachedTemplate({ templateData, extensionMap, eleventyConfig }) {
+ super.resetCachedTemplate({ eleventyConfig });
+ this.templateData = templateData;
+ this.extensionMap = extensionMap;
+ // this.#fsManager = undefined;
+ this.#initFileSlug();
+ }
+
+ get fsManager() {
+ if (!this.#fsManager) {
+ this.#fsManager = new FileSystemManager(this.eleventyConfig);
+ }
+ return this.#fsManager;
+ }
+
+ get logger() {
+ if (!this.#logger) {
+ this.#logger = new ConsoleLogger();
+ this.#logger.isVerbose = this.isVerbose;
+ }
+ return this.#logger;
+ }
+
+ /* Setter for Logger */
+ set logger(logger) {
+ this.#logger = logger;
+ }
+
+ isRenderable() {
+ return this.behavior.isRenderable();
+ }
+
+ isRenderableDisabled() {
+ return this.behavior.isRenderableDisabled();
+ }
+
+ isRenderableOptional() {
+ // A template that is lazily rendered once if used by a second order dependency of another template dependency.
+ // e.g. You change firstpost.md, which is used by feed.xml, but secondpost.md (also used by feed.xml)
+ // has not yet rendered and needs to be rendered once to populate the cache.
+ return this.behavior.isRenderableOptional();
+ }
+
+ setRenderableOverride(renderableOverride) {
+ this.behavior.setRenderableOverride(renderableOverride);
+ }
+
+ reset() {
+ this.renderCount = 0;
+ this.writeCount = 0;
+ }
+
+ resetCaches(types) {
+ types = this.getResetTypes(types);
+
+ super.resetCaches(types);
+
+ if (types.data) {
+ delete this._dataCache;
+ // delete this._usePermalinkRoot;
+ // delete this._stats;
+ }
+
+ if (types.render) {
+ delete this._cacheRenderedPromise;
+ delete this._cacheRenderedTransformsAndLayoutsPromise;
+ }
+ }
+
+ setOutputFormat(to) {
+ this.outputFormat = to;
+ this.behavior.setOutputFormat(to);
+ }
+
+ setIsVerbose(isVerbose) {
+ this.isVerbose = isVerbose;
+ this.logger.isVerbose = isVerbose;
+ }
+
+ setDryRunViaIncremental(isIncremental) {
+ this.isDryRun = isIncremental;
+ this.isIncremental = isIncremental;
+ }
+
+ setDryRun(isDryRun) {
+ this.isDryRun = !!isDryRun;
+ }
+
+ setExtraOutputSubdirectory(dir) {
+ this.extraOutputSubdirectory = dir + "/";
+ }
+
+ getTemplateSubfolder() {
+ let dir = TemplatePath.absolutePath(this.parsed.dir);
+ let inputDir = TemplatePath.absolutePath(this.inputDir);
+ return TemplatePath.stripLeadingSubPath(dir, inputDir);
+ }
+
+ templateUsesLayouts(pageData) {
+ if (this.hasTemplateRender()) {
+ return pageData?.[this.config.keys.layout] && this.templateRender.engine.useLayouts();
+ }
+
+ // If `layout` prop is set, default to true when engine is unknown
+ return Boolean(pageData?.[this.config.keys.layout]);
+ }
+
+ getLayout(layoutKey) {
+ // already cached downstream in TemplateLayout -> TemplateCache
+ try {
+ return TemplateLayout.getTemplate(layoutKey, this.eleventyConfig, this.extensionMap);
+ } catch (e) {
+ throw new EleventyBaseError(
+ `Problem creating an Eleventy Layout for the "${this.inputPath}" template file.`,
+ e,
+ );
+ }
+ }
+
+ get baseFile() {
+ return this.extensionMap.removeTemplateExtension(this.parsed.base);
+ }
+
+ async _getRawPermalinkInstance(permalinkValue) {
+ let perm = new TemplatePermalink(permalinkValue, this.extraOutputSubdirectory);
+ perm.setUrlTransforms(this.config.urlTransforms);
+
+ this.behavior.setFromPermalink(perm);
+
+ return perm;
+ }
+
+ async _getLink(data) {
+ if (!data) {
+ throw new Error("Internal error: data argument missing in Template->_getLink");
+ }
+
+ let permalink =
+ data[this.config.keys.permalink] ??
+ data?.[this.config.keys.computed]?.[this.config.keys.permalink];
+ let permalinkValue;
+
+ // `permalink: false` means render but no file system write, e.g. use in collections only)
+ // `permalink: true` throws an error
+ if (typeof permalink === "boolean") {
+ debugDev("Using boolean permalink %o", permalink);
+ permalinkValue = permalink;
+ } else if (permalink && (!this.config.dynamicPermalinks || data.dynamicPermalink === false)) {
+ debugDev("Not using dynamic permalinks, using %o", permalink);
+ permalinkValue = permalink;
+ } else if (isPlainObject(permalink)) {
+ // Empty permalink {} object should act as if no permalink was set at all
+ // and inherit the default behavior
+ let isEmptyObject = Object.keys(permalink).length === 0;
+ if (!isEmptyObject) {
+ let promises = [];
+ let keys = [];
+ for (let key in permalink) {
+ keys.push(key);
+ if (key !== "build" && Array.isArray(permalink[key])) {
+ promises.push(
+ Promise.all([...permalink[key]].map((entry) => super.renderPermalink(entry, data))),
+ );
+ } else {
+ promises.push(super.renderPermalink(permalink[key], data));
+ }
+ }
+
+ let results = await Promise.all(promises);
+
+ permalinkValue = {};
+ for (let j = 0, k = keys.length; j < k; j++) {
+ let key = keys[j];
+ permalinkValue[key] = results[j];
+ debug(
+ "Rendering permalink.%o for %o: %s becomes %o",
+ key,
+ this.inputPath,
+ permalink[key],
+ results[j],
+ );
+ }
+ }
+ } else if (permalink) {
+ // render variables inside permalink front matter, bypass markdown
+ permalinkValue = await super.renderPermalink(permalink, data);
+ debug("Rendering permalink for %o: %s becomes %o", this.inputPath, permalink, permalinkValue);
+ debugDev("Permalink rendered with data: %o", data);
+ }
+
+ // Override default permalink behavior. Only do this if permalink was _not_ in the data cascade
+ if (!permalink && this.config.dynamicPermalinks && data.dynamicPermalink !== false) {
+ let tr = await this.getTemplateRender();
+ let permalinkCompilation = tr.engine.permalinkNeedsCompilation("");
+ if (typeof permalinkCompilation === "function") {
+ let ret = await this._renderFunction(permalinkCompilation, permalinkValue, this.inputPath);
+ if (ret !== undefined) {
+ if (typeof ret === "function") {
+ // function
+ permalinkValue = await this._renderFunction(ret, data);
+ } else {
+ // scalar
+ permalinkValue = ret;
+ }
+ }
+ }
+ }
+
+ if (permalinkValue !== undefined) {
+ return this._getRawPermalinkInstance(permalinkValue);
+ }
+
+ // No `permalink` specified in data cascade, do the default
+ let p = TemplatePermalink.generate(
+ this.getTemplateSubfolder(),
+ this.baseFile,
+ this.extraOutputSubdirectory,
+ this.engine.defaultTemplateFileExtension,
+ );
+ p.setUrlTransforms(this.config.urlTransforms);
+ return p;
+ }
+
+ async usePermalinkRoot() {
+ // @cachedproperty
+ if (this._usePermalinkRoot === undefined) {
+ // TODO this only works with immediate front matter and not data files
+ let { data } = await this.getFrontMatterData();
+ this._usePermalinkRoot = data[this.config.keys.permalinkRoot];
+ }
+
+ return this._usePermalinkRoot;
+ }
+
+ async getOutputLocations(data) {
+ this.bench.get("(count) getOutputLocations").incrementCount();
+ let link = await this._getLink(data);
+
+ let path;
+ if (await this.usePermalinkRoot()) {
+ path = link.toPathFromRoot();
+ } else {
+ path = link.toPath(this.outputDir);
+ }
+
+ return {
+ linkInstance: link,
+ rawPath: link.toOutputPath(),
+ href: link.toHref(),
+ path: path,
+ };
+ }
+
+ // This is likely now a test-only method
+ // Preferred to use the singular `getOutputLocations` above.
+ async getRawOutputPath(data) {
+ this.bench.get("(count) getRawOutputPath").incrementCount();
+ let link = await this._getLink(data);
+ return link.toOutputPath();
+ }
+
+ // Preferred to use the singular `getOutputLocations` above.
+ async getOutputHref(data) {
+ this.bench.get("(count) getOutputHref").incrementCount();
+ let link = await this._getLink(data);
+ return link.toHref();
+ }
+
+ // Preferred to use the singular `getOutputLocations` above.
+ async getOutputPath(data) {
+ this.bench.get("(count) getOutputPath").incrementCount();
+ let link = await this._getLink(data);
+ if (await this.usePermalinkRoot()) {
+ return link.toPathFromRoot();
+ }
+ return link.toPath(this.outputDir);
+ }
+
+ async _testGetAllLayoutFrontMatterData() {
+ let { data: frontMatterData } = await this.getFrontMatterData();
+
+ if (frontMatterData[this.config.keys.layout]) {
+ let layout = this.getLayout(frontMatterData[this.config.keys.layout]);
+ return await layout.getData();
+ }
+ return {};
+ }
+
+ async #getData() {
+ debugDev("%o getData", this.inputPath);
+ let localData = {};
+ let globalData = {};
+
+ if (this.templateData) {
+ localData = await this.templateData.getTemplateDirectoryData(this.inputPath);
+ globalData = await this.templateData.getGlobalData(this.inputPath);
+ debugDev("%o getData getTemplateDirectoryData and getGlobalData", this.inputPath);
+ }
+
+ let { data: frontMatterData } = await this.getFrontMatterData();
+
+ let mergedLayoutData = {};
+ let tr = await this.getTemplateRender();
+ if (tr.engine.useLayouts()) {
+ let layoutKey =
+ frontMatterData[this.config.keys.layout] ||
+ localData[this.config.keys.layout] ||
+ globalData[this.config.keys.layout];
+
+ // Layout front matter data
+ if (layoutKey) {
+ let layout = this.getLayout(layoutKey);
+
+ mergedLayoutData = await layout.getData();
+ debugDev("%o getData merged layout chain front matter", this.inputPath);
+ }
+ }
+
+ try {
+ let mergedData = TemplateData.mergeDeep(
+ this.config.dataDeepMerge,
+ {},
+ globalData,
+ mergedLayoutData,
+ localData,
+ frontMatterData,
+ );
+
+ if (this.config.freezeReservedData) {
+ ReservedData.check(mergedData);
+ }
+
+ await this.addPage(mergedData);
+
+ debugDev("%o getData mergedData", this.inputPath);
+
+ return mergedData;
+ } catch (e) {
+ if (
+ ReservedData.isReservedDataError(e) ||
+ (e instanceof TypeError &&
+ e.message.startsWith("Cannot add property") &&
+ e.message.endsWith("not extensible"))
+ ) {
+ throw new EleventyBaseError(
+ `You attempted to set one of Eleventy’s reserved data property names${e.reservedNames ? `: ${e.reservedNames.join(", ")}` : ""}. You can opt-out of this behavior with \`eleventyConfig.setFreezeReservedData(false)\` or rename/remove the property in your data cascade that conflicts with Eleventy’s reserved property names (e.g. \`eleventy\`, \`pkg\`, and others). Learn more: https://v3.11ty.dev/docs/data-eleventy-supplied/`,
+ e,
+ );
+ }
+
+ throw e;
+ }
+ }
+
+ async getData() {
+ if (!this._dataCache) {
+ // @cachedproperty
+ this._dataCache = this.#getData();
+ }
+
+ return this._dataCache;
+ }
+
+ async addPage(data) {
+ if (!("page" in data)) {
+ data.page = {};
+ }
+
+ // Make sure to keep these keys synchronized in src/Util/ReservedData.js
+ data.page.inputPath = this.inputPath;
+ data.page.fileSlug = this.fileSlugStr;
+ data.page.filePathStem = this.filePathStem;
+ data.page.outputFileExtension = this.engine.defaultTemplateFileExtension;
+ data.page.templateSyntax = this.templateRender.getEnginesList(
+ data[this.config.keys.engineOverride],
+ );
+
+ let newDate = await this.getMappedDate(data);
+ // Skip date assignment if custom date is falsy.
+ if (newDate) {
+ data.page.date = newDate;
+ }
+
+ // data.page.url
+ // data.page.outputPath
+ // data.page.excerpt from gray-matter and Front Matter
+ // data.page.lang from I18nPlugin
+ }
+
+ // Tests only
+ async render() {
+ throw new Error("Internal error: `Template->render` was removed in Eleventy 3.0.");
+ }
+
+ // Tests only
+ async renderLayout() {
+ throw new Error("Internal error: `Template->renderLayout` was removed in Eleventy 3.0.");
+ }
+
+ async renderDirect(str, data, bypassMarkdown) {
+ return super.render(str, data, bypassMarkdown);
+ }
+
+ // This is the primary render mechanism, called via TemplateMap->populateContentDataInMap
+ async renderPageEntryWithoutLayout(pageEntry) {
+ // @cachedproperty
+ if (!this._cacheRenderedPromise) {
+ this._cacheRenderedPromise = this.renderDirect(pageEntry.rawInput, pageEntry.data);
+ this.renderCount++;
+ }
+
+ return this._cacheRenderedPromise;
+ }
+
+ setLinters(linters) {
+ if (!isPlainObject(linters)) {
+ throw new Error("Object expected in setLinters");
+ }
+ // this acts as a reset
+ this.linters = [];
+ for (let linter of Object.values(linters).filter((l) => typeof l === "function")) {
+ this.addLinter(linter);
+ }
+ }
+
+ addLinter(callback) {
+ this.linters.push(callback);
+ }
+
+ async runLinters(str, page) {
+ let { inputPath, outputPath, url } = page;
+ let pageData = page.data.page;
+
+ for (let linter of this.linters) {
+ // these can be asynchronous but no guarantee of order when they run
+ linter.call(
+ {
+ inputPath,
+ outputPath,
+ url,
+ page: pageData,
+ },
+ str,
+ inputPath,
+ outputPath,
+ );
+ }
+ }
+
+ setTransforms(transforms) {
+ if (!isPlainObject(transforms)) {
+ throw new Error("Object expected in setTransforms");
+ }
+ this.transforms = transforms;
+ }
+
+ async runTransforms(str, pageEntry) {
+ return TransformsUtil.runAll(str, pageEntry.data.page, this.transforms, {
+ logger: this.logger,
+ });
+ }
+
+ async #renderComputedUnit(entry, data) {
+ if (typeof entry === "string") {
+ return this.renderComputedData(entry, data);
+ }
+
+ if (isPlainObject(entry)) {
+ for (let key in entry) {
+ entry[key] = await this.#renderComputedUnit(entry[key], data);
+ }
+ }
+
+ if (Array.isArray(entry)) {
+ for (let j = 0, k = entry.length; j < k; j++) {
+ entry[j] = await this.#renderComputedUnit(entry[j], data);
+ }
+ }
+
+ return entry;
+ }
+
+ _addComputedEntry(computedData, obj, parentKey, declaredDependencies) {
+ // this check must come before isPlainObject
+ if (typeof obj === "function") {
+ computedData.add(parentKey, obj, declaredDependencies);
+ } else if (Array.isArray(obj) || typeof obj === "string") {
+ // Arrays are treated as one entry in the dependency graph now, Issue #3728
+ computedData.addTemplateString(
+ parentKey,
+ async function (innerData) {
+ return this.tmpl.#renderComputedUnit(obj, innerData);
+ },
+ declaredDependencies,
+ this.getParseForSymbolsFunction(obj),
+ this,
+ );
+ } else if (isPlainObject(obj)) {
+ // Arrays used to be computed here
+ for (let key in obj) {
+ let keys = [];
+ if (parentKey) {
+ keys.push(parentKey);
+ }
+ keys.push(key);
+ this._addComputedEntry(computedData, obj[key], keys.join("."), declaredDependencies);
+ }
+ } else {
+ // Numbers, booleans, etc
+ computedData.add(parentKey, obj, declaredDependencies);
+ }
+ }
+
+ async addComputedData(data) {
+ if (isPlainObject(data?.[this.config.keys.computed])) {
+ this.computedData = new ComputedData(this.config);
+
+ // Note that `permalink` is only a thing that gets consumed—it does not go directly into generated data
+ // this allows computed entries to use page.url or page.outputPath and they’ll be resolved properly
+
+ // TODO Room for optimization here—we don’t need to recalculate `getOutputHref` and `getOutputPath`
+ // TODO Why are these using addTemplateString instead of add
+ this.computedData.addTemplateString(
+ "page.url",
+ async function (data) {
+ return this.tmpl.getOutputHref(data);
+ },
+ data.permalink ? ["permalink"] : undefined,
+ false, // skip symbol resolution
+ this,
+ );
+
+ this.computedData.addTemplateString(
+ "page.outputPath",
+ async function (data) {
+ return this.tmpl.getOutputPath(data);
+ },
+ data.permalink ? ["permalink"] : undefined,
+ false, // skip symbol resolution
+ this,
+ );
+
+ // Check for reserved properties in computed data
+ if (this.config.freezeReservedData) {
+ ReservedData.check(data[this.config.keys.computed]);
+ }
+
+ // actually add the computed data
+ this._addComputedEntry(this.computedData, data[this.config.keys.computed]);
+
+ // limited run of computed data—save the stuff that relies on collections for later.
+ debug("First round of computed data for %o", this.inputPath);
+ await this.computedData.setupData(data, function (entry) {
+ return !this.isUsesStartsWith(entry, "collections.");
+
+ // TODO possible improvement here is to only process page.url, page.outputPath, permalink
+ // instead of only punting on things that rely on collections.
+ // let firstPhaseComputedData = ["page.url", "page.outputPath", ...this.getOrderFor("page.url"), ...this.getOrderFor("page.outputPath")];
+ // return firstPhaseComputedData.indexOf(entry) > -1;
+ });
+ } else {
+ if (!("page" in data)) {
+ data.page = {};
+ }
+
+ // pagination will already have these set via Pagination->getPageTemplates
+ if (data.page.url && data.page.outputPath) {
+ return;
+ }
+
+ let { href, path } = await this.getOutputLocations(data);
+ data.page.url = href;
+ data.page.outputPath = path;
+ }
+ }
+
+ // Computed data consuming collections!
+ async resolveRemainingComputedData(data) {
+ // If it doesn’t exist, computed data is not used for this template
+ if (this.computedData) {
+ debug("Second round of computed data for %o", this.inputPath);
+ return this.computedData.processRemainingData(data);
+ }
+ }
+
+ static augmentWithTemplateContentProperty(obj) {
+ return Object.defineProperties(obj, {
+ needsCheck: {
+ enumerable: false,
+ writable: true,
+ value: true,
+ },
+ _templateContent: {
+ enumerable: false,
+ writable: true,
+ value: undefined,
+ },
+ templateContent: {
+ enumerable: true,
+ set(content) {
+ if (content === undefined) {
+ this.needsCheck = false;
+ }
+ this._templateContent = content;
+ },
+ get() {
+ if (this.needsCheck && this._templateContent === undefined) {
+ if (this.template.isRenderable()) {
+ // should at least warn here
+ throw new TemplateContentPrematureUseError(
+ `Tried to use templateContent too early on ${this.inputPath}${
+ this.pageNumber ? ` (page ${this.pageNumber})` : ""
+ }`,
+ );
+ } else {
+ throw new TemplateContentUnrenderedTemplateError(
+ `Tried to use templateContent on unrendered template: ${
+ this.inputPath
+ }${this.pageNumber ? ` (page ${this.pageNumber})` : ""}`,
+ );
+ }
+ }
+ return this._templateContent;
+ },
+ },
+ // Alias for templateContent for consistency
+ content: {
+ enumerable: true,
+ get() {
+ return this.templateContent;
+ },
+ set() {
+ throw new Error("Setter not available for `content`. Use `templateContent` instead.");
+ },
+ },
+ });
+ }
+
+ static async runPreprocessors(inputPath, content, data, preprocessors) {
+ let skippedVia = false;
+ for (let [name, preprocessor] of Object.entries(preprocessors)) {
+ let { filter, callback } = preprocessor;
+
+ let filters;
+ if (Array.isArray(filter)) {
+ filters = filter;
+ } else if (typeof filter === "string") {
+ filters = filter.split(",");
+ } else {
+ throw new Error(
+ `Expected file extensions passed to "${name}" content preprocessor to be a string or array. Received: ${filter}`,
+ );
+ }
+
+ filters = filters.map((extension) => {
+ if (extension.startsWith(".") || extension === "*") {
+ return extension;
+ }
+
+ return `.${extension}`;
+ });
+
+ if (!filters.some((extension) => extension === "*" || inputPath.endsWith(extension))) {
+ // skip
+ continue;
+ }
+
+ try {
+ let ret = await callback.call(
+ {
+ inputPath,
+ },
+ data,
+ content,
+ );
+
+ // Returning explicit false is the same as ignoring the template
+ if (ret === false) {
+ skippedVia = name;
+ continue;
+ }
+
+ // Different from transforms: returning falsy (not false) here does nothing (skips the preprocessor)
+ if (ret) {
+ content = ret;
+ }
+ } catch (e) {
+ throw new EleventyBaseError(
+ `Preprocessor \`${name}\` encountered an error when transforming ${inputPath}.`,
+ e,
+ );
+ }
+ }
+
+ return {
+ skippedVia,
+ content,
+ };
+ }
+
+ async getTemplates(data) {
+ let content = await this.getPreRender();
+ let { skippedVia, content: rawInput } = await Template.runPreprocessors(
+ this.inputPath,
+ content,
+ data,
+ this.config.preprocessors,
+ );
+
+ if (skippedVia) {
+ debug(
+ "Skipping %o, the %o preprocessor returned an explicit `false`",
+ this.inputPath,
+ skippedVia,
+ );
+ return [];
+ }
+
+ // Raw Input *includes* preprocessor modifications
+ // https://github.com/11ty/eleventy/issues/1206
+ data.page.rawInput = rawInput;
+
+ if (!Pagination.hasPagination(data)) {
+ await this.addComputedData(data);
+
+ let obj = {
+ template: this, // not on the docs but folks are relying on it
+ rawInput,
+ groupNumber: 0, // i18n plugin
+ data,
+
+ page: data.page,
+ inputPath: this.inputPath,
+ fileSlug: this.fileSlugStr,
+ filePathStem: this.filePathStem,
+ date: data.page.date,
+ outputPath: data.page.outputPath,
+ url: data.page.url,
+ };
+
+ obj = Template.augmentWithTemplateContentProperty(obj);
+
+ return [obj];
+ } else {
+ // needs collections for pagination items
+ // but individual pagination entries won’t be part of a collection
+ this.paging = new Pagination(this, data, this.config);
+
+ let pageTemplates = await this.paging.getPageTemplates();
+ let objects = [];
+
+ for (let pageEntry of pageTemplates) {
+ await pageEntry.template.addComputedData(pageEntry.data);
+
+ let obj = {
+ template: pageEntry.template, // not on the docs but folks are relying on it
+ rawInput,
+ pageNumber: pageEntry.pageNumber,
+ groupNumber: pageEntry.groupNumber || 0,
+
+ data: pageEntry.data,
+
+ inputPath: this.inputPath,
+ fileSlug: this.fileSlugStr,
+ filePathStem: this.filePathStem,
+
+ page: pageEntry.data.page,
+ date: pageEntry.data.page.date,
+ outputPath: pageEntry.data.page.outputPath,
+ url: pageEntry.data.page.url,
+ };
+
+ obj = Template.augmentWithTemplateContentProperty(obj);
+
+ objects.push(obj);
+ }
+
+ return objects;
+ }
+ }
+
+ async _write({ url, outputPath, data, rawInput }, finalContent) {
+ let lang = {
+ start: "Writing",
+ finished: "written",
+ };
+
+ if (!this.isDryRun) {
+ if (this.logger.isLoggingEnabled()) {
+ let isVirtual = this.isVirtualTemplate();
+ let tr = await this.getTemplateRender();
+ let engineList = tr.getReadableEnginesListDifferingFromFileExtension();
+ let suffix = `${isVirtual ? " (virtual)" : ""}${engineList ? ` (${engineList})` : ""}`;
+ this.logger.log(
+ `${lang.start} ${outputPath} ${chalk.gray(`from ${this.inputPath}${suffix}`)}`,
+ );
+ }
+ } else if (this.isDryRun) {
+ return;
+ }
+
+ let templateBenchmarkDir = this.bench.get("Template make parent directory");
+ templateBenchmarkDir.before();
+
+ if (this.eleventyConfig.templateHandling?.writeMode === "async") {
+ await this.fsManager.createDirectoryForFile(outputPath);
+ } else {
+ this.fsManager.createDirectoryForFileSync(outputPath);
+ }
+
+ templateBenchmarkDir.after();
+
+ if (!Buffer.isBuffer(finalContent) && typeof finalContent !== "string") {
+ throw new Error(
+ `The return value from the render function for the ${this.engine.name} template was not a String or Buffer. Received ${finalContent}`,
+ );
+ }
+
+ let templateBenchmark = this.bench.get("Template Write");
+ templateBenchmark.before();
+
+ if (this.eleventyConfig.templateHandling?.writeMode === "async") {
+ await this.fsManager.writeFile(outputPath, finalContent);
+ } else {
+ this.fsManager.writeFileSync(outputPath, finalContent);
+ }
+
+ templateBenchmark.after();
+ this.writeCount++;
+ debug(`${outputPath} ${lang.finished}.`);
+
+ let ret = {
+ inputPath: this.inputPath,
+ outputPath: outputPath,
+ url,
+ content: finalContent,
+ rawInput,
+ };
+
+ if (data && this.config.dataFilterSelectors?.size > 0) {
+ ret.data = this.retrieveDataForJsonOutput(data, this.config.dataFilterSelectors);
+ }
+
+ return ret;
+ }
+
+ async #renderPageEntryWithLayoutsAndTransforms(pageEntry) {
+ let content;
+ let layoutKey = pageEntry.data[this.config.keys.layout];
+ if (this.engine.useLayouts() && layoutKey) {
+ let layout = pageEntry.template.getLayout(layoutKey);
+ content = await layout.renderPageEntry(pageEntry);
+ } else {
+ content = pageEntry.templateContent;
+ }
+
+ await this.runLinters(content, pageEntry);
+
+ content = await this.runTransforms(content, pageEntry);
+ return content;
+ }
+
+ async renderPageEntry(pageEntry) {
+ // @cachedproperty
+ if (!pageEntry.template._cacheRenderedTransformsAndLayoutsPromise) {
+ pageEntry.template._cacheRenderedTransformsAndLayoutsPromise =
+ this.#renderPageEntryWithLayoutsAndTransforms(pageEntry);
+ }
+
+ return pageEntry.template._cacheRenderedTransformsAndLayoutsPromise;
+ }
+
+ retrieveDataForJsonOutput(data, selectors) {
+ let filtered = {};
+ for (let selector of selectors) {
+ let value = lodashGet(data, selector);
+ lodashSet(filtered, selector, value);
+ }
+ return filtered;
+ }
+
+ async generateMapEntry(mapEntry, to) {
+ let ret = [];
+
+ for (let page of mapEntry._pages) {
+ let content;
+
+ // Note that behavior.render is overridden when using json or ndjson output
+ if (page.template.isRenderable()) {
+ // this reuses page.templateContent, it doesn’t render it
+ content = await page.template.renderPageEntry(page);
+ }
+
+ if (to === "json" || to === "ndjson") {
+ let obj = {
+ url: page.url,
+ inputPath: page.inputPath,
+ outputPath: page.outputPath,
+ rawInput: page.rawInput,
+ content: content,
+ };
+
+ if (this.config.dataFilterSelectors?.size > 0) {
+ obj.data = this.retrieveDataForJsonOutput(page.data, this.config.dataFilterSelectors);
+ }
+
+ if (to === "ndjson") {
+ let jsonString = JSON.stringify(obj);
+ this.logger.toStream(jsonString + os.EOL);
+ continue;
+ }
+
+ // json
+ ret.push(obj);
+ continue;
+ }
+
+ if (!page.template.isRenderable()) {
+ debug("Template not written %o from %o.", page.outputPath, page.template.inputPath);
+ continue;
+ }
+
+ if (!page.template.behavior.isWriteable()) {
+ debug(
+ "Template not written %o from %o (via permalink: false, permalink.build: false, or a permalink object without a build property).",
+ page.outputPath,
+ page.template.inputPath,
+ );
+ continue;
+ }
+
+ // compile returned undefined
+ if (content !== undefined) {
+ ret.push(this._write(page, content));
+ }
+ }
+
+ return Promise.all(ret);
+ }
+
+ async clone() {
+ // TODO do we need to even run the constructor here or can we simplify it even more
+ let tmpl = new Template(
+ this.inputPath,
+ this.templateData,
+ this.extensionMap,
+ this.eleventyConfig,
+ );
+
+ // We use this cheap property setter below instead
+ // await tmpl.getTemplateRender();
+
+ // preserves caches too, e.g. _frontMatterDataCache
+ // Does not yet include .computedData
+ for (let key in this) {
+ tmpl[key] = this[key];
+ }
+
+ return tmpl;
+ }
+
+ getWriteCount() {
+ return this.writeCount;
+ }
+
+ getRenderCount() {
+ return this.renderCount;
+ }
+
+ async getInputFileStat() {
+ // @cachedproperty
+ if (!this._stats) {
+ this._stats = fsStat(this.inputPath);
+ }
+
+ return this._stats;
+ }
+
+ async _getDateInstance(key = "birthtimeMs") {
+ let stat = await this.getInputFileStat();
+
+ // Issue 1823: https://github.com/11ty/eleventy/issues/1823
+ // return current Date in a Lambda
+ // otherwise ctime would be "1980-01-01T00:00:00.000Z"
+ // otherwise birthtime would be "1970-01-01T00:00:00.000Z"
+ if (stat.birthtimeMs === 0) {
+ return new Date();
+ }
+
+ let newDate = new Date(stat[key]);
+
+ debug(
+ "Template date: using file’s %o for %o of %o (from %o)",
+ key,
+ this.inputPath,
+ newDate,
+ stat.birthtimeMs,
+ );
+
+ return newDate;
+ }
+
+ async getMappedDate(data) {
+ let dateValue = data?.date;
+
+ // These can return a Date object, or a string.
+ // Already type checked to be functions in UserConfig
+ for (let fn of this.config.customDateParsing) {
+ let ret = fn.call(
+ {
+ page: data.page,
+ },
+ dateValue,
+ );
+
+ if (ret) {
+ debug("getMappedDate: date value override via `addDateParsing` callback to %o", ret);
+ dateValue = ret;
+ }
+ }
+
+ if (dateValue) {
+ debug("getMappedDate: using a date in the data for %o of %o", this.inputPath, data.date);
+ if (dateValue?.constructor?.name === "DateTime") {
+ // YAML does its own date parsing
+ debug("getMappedDate: found DateTime instance: %o", dateValue);
+ return dateValue.toJSDate();
+ }
+
+ if (dateValue instanceof Date) {
+ // YAML does its own date parsing
+ debug("getMappedDate: found Date instance (maybe from YAML): %o", dateValue);
+ return dateValue;
+ }
+
+ if (typeof dateValue !== "string") {
+ throw new Error(
+ `Data cascade value for \`date\` (${dateValue}) is invalid for ${this.inputPath}. Expected a JavaScript Date instance, luxon DateTime instance, or String value.`,
+ );
+ }
+
+ // special strings
+ if (!this.isVirtualTemplate()) {
+ if (dateValue.toLowerCase() === "git last modified") {
+ let d = await getDateFromGitLastUpdated(this.inputPath);
+ if (d) {
+ return d;
+ }
+
+ // return now if this file is not yet available in `git`
+ return new Date();
+ }
+ if (dateValue.toLowerCase() === "last modified") {
+ return this._getDateInstance("ctimeMs");
+ }
+ if (dateValue.toLowerCase() === "git created") {
+ let d = await getDateFromGitFirstAdded(this.inputPath);
+ if (d) {
+ return d;
+ }
+
+ // return now if this file is not yet available in `git`
+ return new Date();
+ }
+ if (dateValue.toLowerCase() === "created") {
+ return this._getDateInstance("birthtimeMs");
+ }
+ }
+
+ // try to parse with Luxon
+ let date = DateTime.fromISO(dateValue, { zone: "utc" });
+ if (!date.isValid) {
+ throw new Error(
+ `Data cascade value for \`date\` (${dateValue}) is invalid for ${this.inputPath}`,
+ );
+ }
+ debug("getMappedDate: Luxon parsed %o: %o and %o", dateValue, date, date.toJSDate());
+
+ return date.toJSDate();
+ }
+
+ // No Date supplied in the Data Cascade, try to find the date in the file name
+ let filepathRegex = this.inputPath.match(/(\d{4}-\d{2}-\d{2})/);
+ if (filepathRegex !== null) {
+ // if multiple are found in the path, use the first one for the date
+ let dateObj = DateTime.fromISO(filepathRegex[1], {
+ zone: "utc",
+ }).toJSDate();
+ debug(
+ "getMappedDate: using filename regex time for %o of %o: %o",
+ this.inputPath,
+ filepathRegex[1],
+ dateObj,
+ );
+ return dateObj;
+ }
+
+ // No Date supplied in the Data Cascade
+ if (this.isVirtualTemplate()) {
+ return new Date();
+ }
+
+ return this._getDateInstance("birthtimeMs");
+ }
+
+ // Important reminder: Template data is first generated in TemplateMap
+ async getTemplateMapEntries(data) {
+ debugDev("%o getMapped()", this.inputPath);
+
+ this.behavior.setRenderViaDataCascade(data);
+
+ let entries = [];
+ // does not return outputPath or url, we don’t want to render permalinks yet
+ entries.push({
+ template: this,
+ inputPath: this.inputPath,
+ data,
+ });
+
+ return entries;
+ }
+}
+
+export default Template;