Files
fedi-classifieds/node_modules/@11ty/eleventy/src/Plugins/RenderPlugin.js
2026-04-29 08:30:52 -07:00

521 lines
15 KiB
JavaScript
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 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 };