Files
beall-11ty/node_modules/@11ty/eleventy/src/Template.js
2026-03-31 16:38:22 -07:00

1201 lines
33 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;