This commit is contained in:
2026-04-29 08:30:52 -07:00
commit b053d27321
1660 changed files with 329972 additions and 0 deletions

520
node_modules/@11ty/eleventy/src/Plugins/RenderPlugin.js generated vendored Normal file
View File

@ -0,0 +1,520 @@
import fs from "node:fs";
import { Merge, TemplatePath, isPlainObject } from "@11ty/eleventy-utils";
import { evalToken } from "liquidjs";
// TODO add a first-class Markdown component to expose this using Markdown-only syntax (will need to be synchronous for markdown-it)
import { ProxyWrap } from "../Util/Objects/ProxyWrap.js";
import TemplateDataInitialGlobalData from "../Data/TemplateDataInitialGlobalData.js";
import EleventyBaseError from "../Errors/EleventyBaseError.js";
import TemplateRender from "../TemplateRender.js";
import ProjectDirectories from "../Util/ProjectDirectories.js";
import TemplateConfig from "../TemplateConfig.js";
import EleventyExtensionMap from "../EleventyExtensionMap.js";
import TemplateEngineManager from "../Engines/TemplateEngineManager.js";
import Liquid from "../Engines/Liquid.js";
class EleventyNunjucksError extends EleventyBaseError {}
/** @this {object} */
async function compile(content, templateLang, options = {}) {
let { templateConfig, extensionMap } = options;
let strictMode = options.strictMode ?? false;
if (!templateConfig) {
templateConfig = new TemplateConfig(null, false);
templateConfig.setDirectories(new ProjectDirectories());
await templateConfig.init();
}
// Breaking change in 2.0+, previous default was `html` and now we default to the page template syntax
if (!templateLang) {
templateLang = this.page.templateSyntax;
}
if (!extensionMap) {
if (strictMode) {
throw new Error("Internal error: missing `extensionMap` in RenderPlugin->compile.");
}
extensionMap = new EleventyExtensionMap(templateConfig);
extensionMap.engineManager = new TemplateEngineManager(templateConfig);
}
let tr = new TemplateRender(templateLang, templateConfig);
tr.extensionMap = extensionMap;
if (templateLang) {
await tr.setEngineOverride(templateLang);
} else {
await tr.init();
}
// TODO tie this to the class, not the extension
if (
tr.engine.name === "11ty.js" ||
tr.engine.name === "11ty.cjs" ||
tr.engine.name === "11ty.mjs"
) {
throw new Error(
"11ty.js is not yet supported as a template engine for `renderTemplate`. Use `renderFile` instead!",
);
}
return tr.getCompiledTemplate(content);
}
// No templateLang default, it should infer from the inputPath.
async function compileFile(inputPath, options = {}, templateLang) {
let { templateConfig, extensionMap, config } = options;
let strictMode = options.strictMode ?? false;
if (!inputPath) {
throw new Error("Missing file path argument passed to the `renderFile` shortcode.");
}
let wasTemplateConfigMissing = false;
if (!templateConfig) {
templateConfig = new TemplateConfig(null, false);
templateConfig.setDirectories(new ProjectDirectories());
wasTemplateConfigMissing = true;
}
if (config && typeof config === "function") {
await config(templateConfig.userConfig);
}
if (wasTemplateConfigMissing) {
await templateConfig.init();
}
let normalizedPath = TemplatePath.normalizeOperatingSystemFilePath(inputPath);
// Prefer the exists cache, if its available
if (!templateConfig.existsCache.exists(normalizedPath)) {
throw new Error(
"Could not find render plugin file for the `renderFile` shortcode, looking for: " + inputPath,
);
}
if (!extensionMap) {
if (strictMode) {
throw new Error("Internal error: missing `extensionMap` in RenderPlugin->compileFile.");
}
extensionMap = new EleventyExtensionMap(templateConfig);
extensionMap.engineManager = new TemplateEngineManager(templateConfig);
}
let tr = new TemplateRender(inputPath, templateConfig);
tr.extensionMap = extensionMap;
if (templateLang) {
await tr.setEngineOverride(templateLang);
} else {
await tr.init();
}
if (!tr.engine.needsToReadFileContents()) {
return tr.getCompiledTemplate(null);
}
// TODO we could make this work with full templates (with front matter?)
let content = fs.readFileSync(inputPath, "utf8");
return tr.getCompiledTemplate(content);
}
/** @this {object} */
async function renderShortcodeFn(fn, data) {
if (fn === undefined) {
return;
} else if (typeof fn !== "function") {
throw new Error(`The \`compile\` function did not return a function. Received ${fn}`);
}
// if the user passes a string or other literal, remap to an object.
if (!isPlainObject(data)) {
data = {
_: data,
};
}
if ("data" in this && isPlainObject(this.data)) {
// when options.accessGlobalData is true, this allows the global data
// to be accessed inside of the shortcode as a fallback
data = ProxyWrap(data, this.data);
} else {
// save `page` and `eleventy` for reuse
data.page = this.page;
data.eleventy = this.eleventy;
}
return fn(data);
}
/**
* @module 11ty/eleventy/Plugins/RenderPlugin
*/
/**
* A plugin to add shortcodes to render an Eleventy template
* string (or file) inside of another template. {@link https://v3.11ty.dev/docs/plugins/render/}
*
* @since 1.0.0
* @param {module:11ty/eleventy/UserConfig} eleventyConfig - User-land configuration instance.
* @param {object} options - Plugin options
*/
function eleventyRenderPlugin(eleventyConfig, options = {}) {
let templateConfig;
eleventyConfig.on("eleventy.config", (tmplConfigInstance) => {
templateConfig = tmplConfigInstance;
});
let extensionMap;
eleventyConfig.on("eleventy.extensionmap", (map) => {
extensionMap = map;
});
/**
* @typedef {object} options
* @property {string} [tagName] - The shortcode name to render a template string.
* @property {string} [tagNameFile] - The shortcode name to render a template file.
* @property {module:11ty/eleventy/TemplateConfig} [templateConfig] - Configuration object
* @property {boolean} [accessGlobalData] - Whether or not the template has access to the pages data.
*/
let defaultOptions = {
tagName: "renderTemplate",
tagNameFile: "renderFile",
filterName: "renderContent",
templateConfig: null,
accessGlobalData: false,
};
let opts = Object.assign(defaultOptions, options);
function liquidTemplateTag(liquidEngine, tagName) {
// via https://github.com/harttle/liquidjs/blob/b5a22fa0910c708fe7881ef170ed44d3594e18f3/src/builtin/tags/raw.ts
return {
parse: function (tagToken, remainTokens) {
this.name = tagToken.name;
if (eleventyConfig.liquid.parameterParsing === "builtin") {
this.orderedArgs = Liquid.parseArgumentsBuiltin(tagToken.args);
// note that Liquid does have a Hash class for name-based argument parsing but offers no easy to support both modes in one class
} else {
this.legacyArgs = tagToken.args;
}
this.tokens = [];
var stream = liquidEngine.parser
.parseStream(remainTokens)
.on("token", (token) => {
if (token.name === "end" + tagName) stream.stop();
else this.tokens.push(token);
})
.on("end", () => {
throw new Error(`tag ${tagToken.getText()} not closed`);
});
stream.start();
},
render: function* (ctx) {
let normalizedContext = {};
if (ctx) {
if (opts.accessGlobalData) {
// parent template data cascade
normalizedContext.data = ctx.getAll();
}
normalizedContext.page = ctx.get(["page"]);
normalizedContext.eleventy = ctx.get(["eleventy"]);
}
let argArray = [];
if (this.legacyArgs) {
let rawArgs = Liquid.parseArguments(null, this.legacyArgs);
for (let arg of rawArgs) {
let b = yield liquidEngine.evalValue(arg, ctx);
argArray.push(b);
}
} else if (this.orderedArgs) {
for (let arg of this.orderedArgs) {
let b = yield evalToken(arg, ctx);
argArray.push(b);
}
}
// plaintext paired shortcode content
let body = this.tokens.map((token) => token.getText()).join("");
let ret = _renderStringShortcodeFn.call(
normalizedContext,
body,
// templateLang, data
...argArray,
);
yield ret;
return ret;
},
};
}
// TODO I dont think this works with whitespace control, e.g. {%- endrenderTemplate %}
function nunjucksTemplateTag(NunjucksLib, tagName) {
return new (function () {
this.tags = [tagName];
this.parse = function (parser, nodes) {
var tok = parser.nextToken();
var args = parser.parseSignature(true, true);
const begun = parser.advanceAfterBlockEnd(tok.value);
// This code was ripped from the Nunjucks parser for `raw`
// https://github.com/mozilla/nunjucks/blob/fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/parser.js#L655
const endTagName = "end" + tagName;
// Look for upcoming raw blocks (ignore all other kinds of blocks)
const rawBlockRegex = new RegExp(
"([\\s\\S]*?){%\\s*(" + tagName + "|" + endTagName + ")\\s*(?=%})%}",
);
let rawLevel = 1;
let str = "";
let matches = null;
// Exit when there's nothing to match
// or when we've found the matching "endraw" block
while ((matches = parser.tokens._extractRegex(rawBlockRegex)) && rawLevel > 0) {
const all = matches[0];
const pre = matches[1];
const blockName = matches[2];
// Adjust rawlevel
if (blockName === tagName) {
rawLevel += 1;
} else if (blockName === endTagName) {
rawLevel -= 1;
}
// Add to str
if (rawLevel === 0) {
// We want to exclude the last "endraw"
str += pre;
// Move tokenizer to beginning of endraw block
parser.tokens.backN(all.length - pre.length);
} else {
str += all;
}
}
let body = new nodes.Output(begun.lineno, begun.colno, [
new nodes.TemplateData(begun.lineno, begun.colno, str),
]);
return new nodes.CallExtensionAsync(this, "run", args, [body]);
};
this.run = function (...args) {
let resolve = args.pop();
let body = args.pop();
let [context, ...argArray] = args;
let normalizedContext = {};
if (context.ctx?.page) {
normalizedContext.ctx = context.ctx;
// TODO .data
// if(opts.accessGlobalData) {
// normalizedContext.data = context.ctx;
// }
normalizedContext.page = context.ctx.page;
normalizedContext.eleventy = context.ctx.eleventy;
}
body(function (e, bodyContent) {
if (e) {
resolve(
new EleventyNunjucksError(`Error with Nunjucks paired shortcode \`${tagName}\``, e),
);
}
Promise.resolve(
_renderStringShortcodeFn.call(
normalizedContext,
bodyContent,
// templateLang, data
...argArray,
),
).then(
function (returnValue) {
resolve(null, new NunjucksLib.runtime.SafeString(returnValue));
},
function (e) {
resolve(
new EleventyNunjucksError(`Error with Nunjucks paired shortcode \`${tagName}\``, e),
null,
);
},
);
});
};
})();
}
/** @this {object} */
async function _renderStringShortcodeFn(content, templateLang, data = {}) {
// Default is fn(content, templateLang, data) but we want to support fn(content, data) too
if (typeof templateLang !== "string") {
data = templateLang;
templateLang = false;
}
// TODO Render plugin `templateLang` is feeding bad input paths to the addDependencies call in Custom.js
let fn = await compile.call(this, content, templateLang, {
templateConfig: opts.templateConfig || templateConfig,
extensionMap,
});
return renderShortcodeFn.call(this, fn, data);
}
/** @this {object} */
async function _renderFileShortcodeFn(inputPath, data = {}, templateLang) {
let options = {
templateConfig: opts.templateConfig || templateConfig,
extensionMap,
};
let fn = await compileFile.call(this, inputPath, options, templateLang);
return renderShortcodeFn.call(this, fn, data);
}
// Render strings
if (opts.tagName) {
// use falsy to opt-out
eleventyConfig.addJavaScriptFunction(opts.tagName, _renderStringShortcodeFn);
eleventyConfig.addLiquidTag(opts.tagName, function (liquidEngine) {
return liquidTemplateTag(liquidEngine, opts.tagName);
});
eleventyConfig.addNunjucksTag(opts.tagName, function (nunjucksLib) {
return nunjucksTemplateTag(nunjucksLib, opts.tagName);
});
}
// Filter for rendering strings
if (opts.filterName) {
eleventyConfig.addAsyncFilter(opts.filterName, _renderStringShortcodeFn);
}
// Render File
// use `false` to opt-out
if (opts.tagNameFile) {
eleventyConfig.addAsyncShortcode(opts.tagNameFile, _renderFileShortcodeFn);
}
}
// Will re-use the same configuration instance both at a top level and across any nested renders
class RenderManager {
/** @type {Promise|undefined} */
#hasConfigInitialized;
#extensionMap;
#templateConfig;
constructor() {
this.templateConfig = new TemplateConfig(null, false);
this.templateConfig.setDirectories(new ProjectDirectories());
}
get templateConfig() {
return this.#templateConfig;
}
set templateConfig(templateConfig) {
if (!templateConfig || templateConfig === this.#templateConfig) {
return;
}
this.#templateConfig = templateConfig;
// This is the only plugin running on the Edge
this.#templateConfig.userConfig.addPlugin(eleventyRenderPlugin, {
templateConfig: this.#templateConfig,
accessGlobalData: true,
});
this.#extensionMap = new EleventyExtensionMap(this.#templateConfig);
this.#extensionMap.engineManager = new TemplateEngineManager(this.#templateConfig);
}
async init() {
if (this.#hasConfigInitialized) {
return this.#hasConfigInitialized;
}
if (this.templateConfig.hasInitialized()) {
return true;
}
this.#hasConfigInitialized = this.templateConfig.init();
await this.#hasConfigInitialized;
return true;
}
// `callback` is async-friendly but requires await upstream
config(callback) {
// run an extra `function(eleventyConfig)` configuration callbacks
if (callback && typeof callback === "function") {
return callback(this.templateConfig.userConfig);
}
}
get initialGlobalData() {
if (!this._data) {
this._data = new TemplateDataInitialGlobalData(this.templateConfig);
}
return this._data;
}
// because we dont have access to the full data cascade—but
// we still want configuration data added via `addGlobalData`
async getData(...data) {
await this.init();
let globalData = await this.initialGlobalData.getData();
let merged = Merge({}, globalData, ...data);
return merged;
}
async compile(content, templateLang, options = {}) {
await this.init();
options.templateConfig = this.templateConfig;
options.extensionMap = this.#extensionMap;
options.strictMode = true;
// We dont need `compile.call(this)` here because the Edge always uses "liquid" as the template lang (instead of relying on this.page.templateSyntax)
// returns promise
return compile(content, templateLang, options);
}
async render(fn, edgeData, buildTimeData) {
await this.init();
let mergedData = await this.getData(edgeData);
// Set .data for options.accessGlobalData feature
let context = {
data: mergedData,
};
return renderShortcodeFn.call(context, fn, buildTimeData);
}
}
Object.defineProperty(eleventyRenderPlugin, "eleventyPackage", {
value: "@11ty/eleventy/render-plugin",
});
Object.defineProperty(eleventyRenderPlugin, "eleventyPluginOptions", {
value: {
unique: true,
},
});
export default eleventyRenderPlugin;
export { compileFile as File, compile as String, RenderManager };