1201 lines
33 KiB
JavaScript
1201 lines
33 KiB
JavaScript
|
|
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;
|