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