first
This commit is contained in:
520
node_modules/@11ty/eleventy/src/Plugins/RenderPlugin.js
generated
vendored
Normal file
520
node_modules/@11ty/eleventy/src/Plugins/RenderPlugin.js
generated
vendored
Normal 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 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 };
|
||||
Reference in New Issue
Block a user