Files
beall-11ty/node_modules/@11ty/eleventy/src/Template.js

1201 lines
33 KiB
JavaScript
Raw Normal View History

2026-03-31 16:38:22 -07:00
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 Eleventys 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 Eleventys 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 theyll be resolved properly
// TODO Room for optimization here—we dont 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 doesnt 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 wont 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 doesnt 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 files %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 dont want to render permalinks yet
entries.push({
template: this,
inputPath: this.inputPath,
data,
});
return entries;
}
}
export default Template;