summaryrefslogtreecommitdiff
path: root/node_modules/@11ty/eleventy/src/Plugins/RenderPlugin.js
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/@11ty/eleventy/src/Plugins/RenderPlugin.js')
-rw-r--r--node_modules/@11ty/eleventy/src/Plugins/RenderPlugin.js520
1 files changed, 520 insertions, 0 deletions
diff --git a/node_modules/@11ty/eleventy/src/Plugins/RenderPlugin.js b/node_modules/@11ty/eleventy/src/Plugins/RenderPlugin.js
new file mode 100644
index 0000000..974b0e1
--- /dev/null
+++ b/node_modules/@11ty/eleventy/src/Plugins/RenderPlugin.js
@@ -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 it’s 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 page’s 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 don’t 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 don’t 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 don’t 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 };