diff options
Diffstat (limited to 'node_modules/@11ty/eleventy/src/Plugins')
7 files changed, 1729 insertions, 0 deletions
diff --git a/node_modules/@11ty/eleventy/src/Plugins/HtmlBasePlugin.js b/node_modules/@11ty/eleventy/src/Plugins/HtmlBasePlugin.js new file mode 100644 index 0000000..304c0a2 --- /dev/null +++ b/node_modules/@11ty/eleventy/src/Plugins/HtmlBasePlugin.js @@ -0,0 +1,160 @@ +import { DeepCopy } from "@11ty/eleventy-utils"; +import urlFilter from "../Filters/Url.js"; +import PathPrefixer from "../Util/PathPrefixer.js"; +import { HtmlTransformer } from "../Util/HtmlTransformer.js"; +import isValidUrl from "../Util/ValidUrl.js"; + +function addPathPrefixToUrl(url, pathPrefix, base) { + let u; + if (base) { + u = new URL(url, base); + } else { + u = new URL(url); + } + + // Add pathPrefix **after** url is transformed using base + if (pathPrefix) { + u.pathname = PathPrefixer.joinUrlParts(pathPrefix, u.pathname); + } + return u.toString(); +} + +// pathprefix is only used when overrideBase is a full URL +function transformUrl(url, base, opts = {}) { + let { pathPrefix, pageUrl, htmlContext } = opts; + + // Warning, this will not work with HtmlTransformer, as we’ll receive "false" (string) here instead of `false` (boolean) + if (url === false) { + throw new Error( + `Invalid url transformed in the HTML \`<base>\` plugin.${url === false ? ` Did you attempt to link to a \`permalink: false\` page?` : ""} Received: ${url}`, + ); + } + + // full URL, return as-is + if (isValidUrl(url)) { + return url; + } + + // Not a full URL, but with a full base URL + // e.g. relative urls like "subdir/", "../subdir", "./subdir" + if (isValidUrl(base)) { + // convert relative paths to absolute path first using pageUrl + if (pageUrl && !url.startsWith("/")) { + let urlObj = new URL(url, `http://example.com${pageUrl}`); + url = urlObj.pathname + (urlObj.hash || ""); + } + + return addPathPrefixToUrl(url, pathPrefix, base); + } + + // Not a full URL, nor a full base URL (call the built-in `url` filter) + return urlFilter(url, base); +} + +function eleventyHtmlBasePlugin(eleventyConfig, defaultOptions = {}) { + let opts = DeepCopy( + { + // eleventyConfig.pathPrefix is new in Eleventy 2.0.0-canary.15 + // `base` can be a directory (for path prefix transformations) + // OR a full URL with origin and pathname + baseHref: eleventyConfig.pathPrefix, + + extensions: "html", + }, + defaultOptions, + ); + + // `filters` option to rename filters was removed in 3.0.0-alpha.13 + // Renaming these would cause issues in other plugins (e.g. RSS) + if (opts.filters !== undefined) { + throw new Error( + "The `filters` option in the HTML Base plugin was removed to prevent future cross-plugin compatibility issues.", + ); + } + + if (opts.baseHref === undefined) { + throw new Error("The `baseHref` option is required in the HTML Base plugin."); + } + + eleventyConfig.addFilter("addPathPrefixToFullUrl", function (url) { + return addPathPrefixToUrl(url, eleventyConfig.pathPrefix); + }); + + // Apply to one URL + eleventyConfig.addFilter( + "htmlBaseUrl", + + /** @this {object} */ + function (url, baseOverride, pageUrlOverride) { + let base = baseOverride || opts.baseHref; + + // Do nothing with a default base + if (base === "/") { + return url; + } + + return transformUrl(url, base, { + pathPrefix: eleventyConfig.pathPrefix, + pageUrl: pageUrlOverride || this.page?.url, + }); + }, + ); + + // Apply to a block of HTML + eleventyConfig.addAsyncFilter( + "transformWithHtmlBase", + + /** @this {object} */ + function (content, baseOverride, pageUrlOverride) { + let base = baseOverride || opts.baseHref; + + // Do nothing with a default base + if (base === "/") { + return content; + } + + return HtmlTransformer.transformStandalone(content, (url, htmlContext) => { + return transformUrl(url.trim(), base, { + pathPrefix: eleventyConfig.pathPrefix, + pageUrl: pageUrlOverride || this.page?.url, + htmlContext, + }); + }); + }, + ); + + // Apply to all HTML output in your project + eleventyConfig.htmlTransformer.addUrlTransform( + opts.extensions, + + /** @this {object} */ + function (urlInMarkup, htmlContext) { + // baseHref override is via renderTransforms filter for adding the absolute URL (e.g. https://example.com/pathPrefix/) for RSS/Atom/JSON feeds + return transformUrl(urlInMarkup.trim(), this.baseHref || opts.baseHref, { + pathPrefix: eleventyConfig.pathPrefix, + pageUrl: this.url, + htmlContext, + }); + }, + { + priority: -2, // priority is descending, so this runs last (especially after AutoCopy and InputPathToUrl transform) + enabled: function (context) { + // Enabled when pathPrefix is non-default or via renderTransforms + return Boolean(context.baseHref) || opts.baseHref !== "/"; + }, + }, + ); +} + +Object.defineProperty(eleventyHtmlBasePlugin, "eleventyPackage", { + value: "@11ty/eleventy/html-base-plugin", +}); + +Object.defineProperty(eleventyHtmlBasePlugin, "eleventyPluginOptions", { + value: { + unique: true, + }, +}); + +export default eleventyHtmlBasePlugin; +export { transformUrl as applyBaseToUrl }; diff --git a/node_modules/@11ty/eleventy/src/Plugins/HtmlRelativeCopyPlugin.js b/node_modules/@11ty/eleventy/src/Plugins/HtmlRelativeCopyPlugin.js new file mode 100644 index 0000000..ac1391d --- /dev/null +++ b/node_modules/@11ty/eleventy/src/Plugins/HtmlRelativeCopyPlugin.js @@ -0,0 +1,52 @@ +import { HtmlRelativeCopy } from "../Util/HtmlRelativeCopy.js"; + +// one HtmlRelativeCopy instance per entry +function init(eleventyConfig, options) { + let opts = Object.assign( + { + extensions: "html", + match: false, // can be one glob string or an array of globs + paths: [], // directories to also look in for files + failOnError: true, // fails when a path matches (via `match`) but not found on file system + copyOptions: undefined, + }, + options, + ); + + let htmlrel = new HtmlRelativeCopy(); + htmlrel.setUserConfig(eleventyConfig); + htmlrel.addMatchingGlob(opts.match); + htmlrel.setFailOnError(opts.failOnError); + htmlrel.setCopyOptions(opts.copyOptions); + + eleventyConfig.htmlTransformer.addUrlTransform( + opts.extensions, + function (targetFilepathOrUrl) { + // @ts-ignore + htmlrel.copy(targetFilepathOrUrl, this.page.inputPath, this.page.outputPath); + + // TODO front matter option for manual copy + return targetFilepathOrUrl; + }, + { + enabled: () => htmlrel.isEnabled(), + // - MUST run after other plugins but BEFORE HtmlBase plugin + priority: -1, + }, + ); + + htmlrel.addPaths(opts.paths); +} + +function HtmlRelativeCopyPlugin(eleventyConfig) { + // Important: if this is empty, no URL transforms are added + for (let options of eleventyConfig.passthroughCopiesHtmlRelative) { + init(eleventyConfig, options); + } +} + +Object.defineProperty(HtmlRelativeCopyPlugin, "eleventyPackage", { + value: "@11ty/eleventy/html-relative-copy-plugin", +}); + +export { HtmlRelativeCopyPlugin }; diff --git a/node_modules/@11ty/eleventy/src/Plugins/I18nPlugin.js b/node_modules/@11ty/eleventy/src/Plugins/I18nPlugin.js new file mode 100644 index 0000000..6f53825 --- /dev/null +++ b/node_modules/@11ty/eleventy/src/Plugins/I18nPlugin.js @@ -0,0 +1,317 @@ +import { bcp47Normalize } from "bcp-47-normalize"; +import iso639 from "iso-639-1"; +import { DeepCopy } from "@11ty/eleventy-utils"; + +// pathPrefix note: +// When using `locale_url` filter with the `url` filter, `locale_url` must run first like +// `| locale_url | url`. If you run `| url | locale_url` it won’t match correctly. + +// TODO improvement would be to throw an error if `locale_url` finds a url with the +// path prefix at the beginning? Would need a better way to know `url` has transformed a string +// rather than just raw comparison. +// e.g. --pathprefix=/en/ should return `/en/en/` for `/en/index.liquid` + +class LangUtils { + static getLanguageCodeFromInputPath(filepath) { + return (filepath || "").split("/").find((entry) => Comparator.isLangCode(entry)); + } + + static getLanguageCodeFromUrl(url) { + let s = (url || "").split("/"); + return s.length > 0 && Comparator.isLangCode(s[1]) ? s[1] : ""; + } + + static swapLanguageCodeNoCheck(str, langCode) { + let found = false; + return str + .split("/") + .map((entry) => { + // only match the first one + if (!found && Comparator.isLangCode(entry)) { + found = true; + return langCode; + } + return entry; + }) + .join("/"); + } + + static swapLanguageCode(str, langCode) { + if (!Comparator.isLangCode(langCode)) { + return str; + } + + return LangUtils.swapLanguageCodeNoCheck(str, langCode); + } +} + +class Comparator { + // https://en.wikipedia.org/wiki/IETF_language_tag#Relation_to_other_standards + // Requires a ISO-639-1 language code at the start (2 characters before the first -) + static isLangCode(code) { + let [s] = (code || "").split("-"); + if (!iso639.validate(s)) { + return false; + } + if (!bcp47Normalize(code)) { + return false; + } + return true; + } + + static urlHasLangCode(url, code) { + if (!Comparator.isLangCode(code)) { + return false; + } + + return url.split("/").some((entry) => entry === code); + } +} + +function normalizeInputPath(inputPath, extensionMap) { + if (extensionMap) { + return extensionMap.removeTemplateExtension(inputPath); + } + return inputPath; +} + +/* + * Input: { + * '/en-us/test/': './test/stubs-i18n/en-us/test.11ty.js', + * '/en/test/': './test/stubs-i18n/en/test.liquid', + * '/es/test/': './test/stubs-i18n/es/test.njk', + * '/non-lang-file/': './test/stubs-i18n/non-lang-file.njk' + * } + * + * Output: { + * '/en-us/test/': [ { url: '/en/test/' }, { url: '/es/test/' } ], + * '/en/test/': [ { url: '/en-us/test/' }, { url: '/es/test/' } ], + * '/es/test/': [ { url: '/en-us/test/' }, { url: '/en/test/' } ] + * } + */ +function getLocaleUrlsMap(urlToInputPath, extensionMap, options = {}) { + let filemap = {}; + + for (let url in urlToInputPath) { + // Group number comes from Pagination.js + let { inputPath: originalFilepath, groupNumber } = urlToInputPath[url]; + let filepath = normalizeInputPath(originalFilepath, extensionMap); + let replaced = + LangUtils.swapLanguageCodeNoCheck(filepath, "__11ty_i18n") + `_group:${groupNumber}`; + + if (!filemap[replaced]) { + filemap[replaced] = []; + } + + let langCode = LangUtils.getLanguageCodeFromInputPath(originalFilepath); + if (!langCode) { + langCode = LangUtils.getLanguageCodeFromUrl(url); + } + if (!langCode) { + langCode = options.defaultLanguage; + } + + if (langCode) { + filemap[replaced].push({ + url, + lang: langCode, + label: iso639.getNativeName(langCode.split("-")[0]), + }); + } else { + filemap[replaced].push({ url }); + } + } + + // Default sorted by lang code + for (let key in filemap) { + filemap[key].sort(function (a, b) { + if (a.lang < b.lang) { + return -1; + } + if (a.lang > b.lang) { + return 1; + } + return 0; + }); + } + + // map of input paths => array of localized urls + let urlMap = {}; + for (let filepath in filemap) { + for (let entry of filemap[filepath]) { + let url = entry.url; + if (!urlMap[url]) { + urlMap[url] = filemap[filepath].filter((entry) => { + if (entry.lang) { + return true; + } + return entry.url !== url; + }); + } + } + } + + return urlMap; +} + +function eleventyI18nPlugin(eleventyConfig, opts = {}) { + let options = DeepCopy( + { + defaultLanguage: "", + filters: { + url: "locale_url", + links: "locale_links", + }, + errorMode: "strict", // allow-fallback, never + }, + opts, + ); + + if (!options.defaultLanguage) { + throw new Error( + "You must specify a `defaultLanguage` in Eleventy’s Internationalization (I18N) plugin.", + ); + } + + let extensionMap; + eleventyConfig.on("eleventy.extensionmap", (map) => { + extensionMap = map; + }); + + let bench = eleventyConfig.benchmarkManager.get("Aggregate"); + let contentMaps = {}; + eleventyConfig.on("eleventy.contentMap", function ({ urlToInputPath, inputPathToUrl }) { + let b = bench.get("(i18n Plugin) Setting up content map."); + b.before(); + contentMaps.inputPathToUrl = inputPathToUrl; + contentMaps.urlToInputPath = urlToInputPath; + + contentMaps.localeUrlsMap = getLocaleUrlsMap(urlToInputPath, extensionMap, options); + b.after(); + }); + + eleventyConfig.addGlobalData("eleventyComputed.page.lang", () => { + // if addGlobalData receives a function it will execute it immediately, + // so we return a nested function for computed data + return (data) => { + return LangUtils.getLanguageCodeFromUrl(data.page.url) || options.defaultLanguage; + }; + }); + + // Normalize a theoretical URL based on the current page’s language + // If a non-localized file exists, returns the URL without a language assigned + // Fails if no file exists (localized and not localized) + eleventyConfig.addFilter(options.filters.url, function (url, langCodeOverride) { + let langCode = + langCodeOverride || + LangUtils.getLanguageCodeFromUrl(this.page?.url) || + options.defaultLanguage; + + // Already has a language code on it and has a relevant url with the target language code + if ( + contentMaps.localeUrlsMap[url] || + (!url.endsWith("/") && contentMaps.localeUrlsMap[`${url}/`]) + ) { + for (let existingUrlObj of contentMaps.localeUrlsMap[url] || + contentMaps.localeUrlsMap[`${url}/`]) { + if (Comparator.urlHasLangCode(existingUrlObj.url, langCode)) { + return existingUrlObj.url; + } + } + } + + // Needs the language code prepended to the URL + let prependedLangCodeUrl = `/${langCode}${url}`; + if ( + contentMaps.localeUrlsMap[prependedLangCodeUrl] || + (!prependedLangCodeUrl.endsWith("/") && contentMaps.localeUrlsMap[`${prependedLangCodeUrl}/`]) + ) { + return prependedLangCodeUrl; + } + + if ( + contentMaps.urlToInputPath[url] || + (!url.endsWith("/") && contentMaps.urlToInputPath[`${url}/`]) + ) { + // this is not a localized file (independent of a language code) + if (options.errorMode === "strict") { + throw new Error( + `Localized file for URL ${prependedLangCodeUrl} was not found in your project. A non-localized version does exist—are you sure you meant to use the \`${options.filters.url}\` filter for this? You can bypass this error using the \`errorMode\` option in the I18N plugin (current value: "${options.errorMode}").`, + ); + } + } else if (options.errorMode === "allow-fallback") { + // You’re linking to a localized file that doesn’t exist! + throw new Error( + `Localized file for URL ${prependedLangCodeUrl} was not found in your project! You will need to add it if you want to link to it using the \`${options.filters.url}\` filter. You can bypass this error using the \`errorMode\` option in the I18N plugin (current value: "${options.errorMode}").`, + ); + } + + return url; + }); + + // Refactor to use url + // Find the links that are localized alternates to the inputPath argument + eleventyConfig.addFilter(options.filters.links, function (urlOverride) { + let url = urlOverride || this.page?.url; + return (contentMaps.localeUrlsMap[url] || []).filter((entry) => { + return entry.url !== url; + }); + }); + + // Returns a `page`-esque variable for the root default language page + // If paginated, returns first result only + eleventyConfig.addFilter( + "locale_page", // This is not exposed in `options` because it is an Eleventy internals filter (used in get*CollectionItem filters) + function (pageOverride, languageCode) { + // both args here are optional + if (!languageCode) { + languageCode = options.defaultLanguage; + } + + let page = pageOverride || this.page; + let url; // new url + if (contentMaps.localeUrlsMap[page.url]) { + for (let entry of contentMaps.localeUrlsMap[page.url]) { + if (entry.lang === languageCode) { + url = entry.url; + } + } + } + + let inputPath = LangUtils.swapLanguageCode(page.inputPath, languageCode); + + if ( + !url || + !Array.isArray(contentMaps.inputPathToUrl[inputPath]) || + contentMaps.inputPathToUrl[inputPath].length === 0 + ) { + // no internationalized pages found + return page; + } + + let result = { + // // note that the permalink/slug may be different for the localized file! + url, + inputPath, + filePathStem: LangUtils.swapLanguageCode(page.filePathStem, languageCode), + // outputPath is omitted here, not necessary for GetCollectionItem.js if url is provided + __locale_page_resolved: true, + }; + return result; + }, + ); +} + +export { Comparator, LangUtils }; + +Object.defineProperty(eleventyI18nPlugin, "eleventyPackage", { + value: "@11ty/eleventy/i18n-plugin", +}); + +Object.defineProperty(eleventyI18nPlugin, "eleventyPluginOptions", { + value: { + unique: true, + }, +}); + +export default eleventyI18nPlugin; diff --git a/node_modules/@11ty/eleventy/src/Plugins/IdAttributePlugin.js b/node_modules/@11ty/eleventy/src/Plugins/IdAttributePlugin.js new file mode 100644 index 0000000..a55a13e --- /dev/null +++ b/node_modules/@11ty/eleventy/src/Plugins/IdAttributePlugin.js @@ -0,0 +1,110 @@ +import matchHelper from "posthtml-match-helper"; +import { decodeHTML } from "entities"; + +import slugifyFilter from "../Filters/Slugify.js"; +import MemoizeUtil from "../Util/MemoizeFunction.js"; + +const POSTHTML_PLUGIN_NAME = "11ty/eleventy/id-attribute"; + +function getTextNodeContent(node) { + if (node.attrs?.["eleventy:id-ignore"] === "") { + delete node.attrs["eleventy:id-ignore"]; + return ""; + } + if (!node.content) { + return ""; + } + + return node.content + .map((entry) => { + if (typeof entry === "string") { + return entry; + } + if (Array.isArray(entry.content)) { + return getTextNodeContent(entry); + } + return ""; + }) + .join(""); +} + +function IdAttributePlugin(eleventyConfig, options = {}) { + if (!options.slugify) { + options.slugify = MemoizeUtil(slugifyFilter); + } + if (!options.selector) { + options.selector = "[id],h1,h2,h3,h4,h5,h6"; + } + options.decodeEntities = options.decodeEntities ?? true; + options.checkDuplicates = options.checkDuplicates ?? "error"; + + eleventyConfig.htmlTransformer.addPosthtmlPlugin( + "html", + function idAttributePosthtmlPlugin(pluginOptions = {}) { + if (typeof options.filter === "function") { + if (options.filter(pluginOptions) === false) { + return function () {}; + } + } + + return function (tree) { + // One per page + let conflictCheck = {}; + // Cache heading nodes for conflict resolution + let headingNodes = {}; + + tree.match(matchHelper(options.selector), function (node) { + if (node.attrs?.id) { + let id = node.attrs?.id; + if (conflictCheck[id]) { + conflictCheck[id]++; + if (headingNodes[id]) { + // Rename conflicting assigned heading id + let newId = `${id}-${conflictCheck[id]}`; + headingNodes[newId] = headingNodes[id]; + headingNodes[newId].attrs.id = newId; + delete headingNodes[id]; + } else if (options.checkDuplicates === "error") { + // Existing `id` conflicts with assigned heading id, throw error + throw new Error( + 'You have more than one HTML `id` attribute using the same value (id="' + + id + + '") in your template (' + + pluginOptions.page.inputPath + + "). You can disable this error in the IdAttribute plugin with the `checkDuplicates: false` option.", + ); + } + } else { + conflictCheck[id] = 1; + } + } else if (!node.attrs?.id && node.content) { + node.attrs = node.attrs || {}; + let textContent = getTextNodeContent(node); + if (options.decodeEntities) { + textContent = decodeHTML(textContent); + } + let id = options.slugify(textContent); + + if (conflictCheck[id]) { + conflictCheck[id]++; + id = `${id}-${conflictCheck[id]}`; + } else { + conflictCheck[id] = 1; + } + + headingNodes[id] = node; + node.attrs.id = id; + } + + return node; + }); + }; + }, + { + // pluginOptions + name: POSTHTML_PLUGIN_NAME, + }, + ); +} + +export { IdAttributePlugin }; diff --git a/node_modules/@11ty/eleventy/src/Plugins/InputPathToUrl.js b/node_modules/@11ty/eleventy/src/Plugins/InputPathToUrl.js new file mode 100644 index 0000000..aca148b --- /dev/null +++ b/node_modules/@11ty/eleventy/src/Plugins/InputPathToUrl.js @@ -0,0 +1,191 @@ +import path from "node:path"; +import { TemplatePath } from "@11ty/eleventy-utils"; +import isValidUrl from "../Util/ValidUrl.js"; + +function getValidPath(contentMap, testPath) { + // if the path is coming from Markdown, it may be encoded + let normalized = TemplatePath.addLeadingDotSlash(decodeURIComponent(testPath)); + + // it must exist in the content map to be valid + if (contentMap[normalized]) { + return normalized; + } +} + +function normalizeInputPath(targetInputPath, inputDir, sourceInputPath, contentMap) { + // inputDir is optional at the beginning of the developer supplied-path + + // Input directory already on the input path + if (TemplatePath.join(targetInputPath).startsWith(TemplatePath.join(inputDir))) { + let absolutePath = getValidPath(contentMap, targetInputPath); + if (absolutePath) { + return absolutePath; + } + } + + // Relative to project input directory + let relativeToInputDir = getValidPath(contentMap, TemplatePath.join(inputDir, targetInputPath)); + if (relativeToInputDir) { + return relativeToInputDir; + } + + if (targetInputPath && !path.isAbsolute(targetInputPath)) { + // Relative to source file’s input path + let sourceInputDir = TemplatePath.getDirFromFilePath(sourceInputPath); + let relativeToSourceFile = getValidPath( + contentMap, + TemplatePath.join(sourceInputDir, targetInputPath), + ); + if (relativeToSourceFile) { + return relativeToSourceFile; + } + } + + // the transform may have sent in a URL so we just return it as-is + return targetInputPath; +} + +function parseFilePath(filepath) { + if (filepath.startsWith("#") || filepath.startsWith("?")) { + return [filepath, ""]; + } + + try { + /* u: URL { + href: 'file:///tmpl.njk#anchor', + origin: 'null', + protocol: 'file:', + username: '', + password: '', + host: '', + hostname: '', + port: '', + pathname: '/tmpl.njk', + search: '', + searchParams: URLSearchParams {}, + hash: '#anchor' + } */ + + // Note that `node:url` -> pathToFileURL creates an absolute path, which we don’t want + // URL(`file:#anchor`) gives back a pathname of `/` + let u = new URL(`file:${filepath}`); + filepath = filepath.replace(u.search, ""); // includes ? + filepath = filepath.replace(u.hash, ""); // includes # + + return [ + // search includes ?, hash includes # + u.search + u.hash, + filepath, + ]; + } catch (e) { + return ["", filepath]; + } +} + +function FilterPlugin(eleventyConfig) { + let contentMap; + eleventyConfig.on("eleventy.contentMap", function ({ inputPathToUrl }) { + contentMap = inputPathToUrl; + }); + + eleventyConfig.addFilter("inputPathToUrl", function (targetFilePath) { + if (!contentMap) { + throw new Error("Internal error: contentMap not available for `inputPathToUrl` filter."); + } + + if (isValidUrl(targetFilePath)) { + return targetFilePath; + } + + let inputDir = eleventyConfig.directories.input; + let suffix = ""; + [suffix, targetFilePath] = parseFilePath(targetFilePath); + if (targetFilePath) { + targetFilePath = normalizeInputPath( + targetFilePath, + inputDir, + // @ts-ignore + this.page.inputPath, + contentMap, + ); + } + + let urls = contentMap[targetFilePath]; + if (!urls || urls.length === 0) { + throw new Error( + "`inputPathToUrl` filter could not find a matching target for " + targetFilePath, + ); + } + + return `${urls[0]}${suffix}`; + }); +} + +function TransformPlugin(eleventyConfig, defaultOptions = {}) { + let opts = Object.assign( + { + extensions: "html", + }, + defaultOptions, + ); + + let contentMap = null; + eleventyConfig.on("eleventy.contentMap", function ({ inputPathToUrl }) { + contentMap = inputPathToUrl; + }); + + eleventyConfig.htmlTransformer.addUrlTransform(opts.extensions, function (targetFilepathOrUrl) { + if (!contentMap) { + throw new Error("Internal error: contentMap not available for the `pathToUrl` Transform."); + } + if (isValidUrl(targetFilepathOrUrl)) { + return targetFilepathOrUrl; + } + + let inputDir = eleventyConfig.directories.input; + + let suffix = ""; + [suffix, targetFilepathOrUrl] = parseFilePath(targetFilepathOrUrl); + if (targetFilepathOrUrl) { + targetFilepathOrUrl = normalizeInputPath( + targetFilepathOrUrl, + inputDir, + // @ts-ignore + this.page.inputPath, + contentMap, + ); + } + + let urls = contentMap[targetFilepathOrUrl]; + if (!targetFilepathOrUrl || !urls || urls.length === 0) { + // fallback, transforms don’t error on missing paths (though the pathToUrl filter does) + return `${targetFilepathOrUrl}${suffix}`; + } + + return `${urls[0]}${suffix}`; + }); +} + +Object.defineProperty(FilterPlugin, "eleventyPackage", { + value: "@11ty/eleventy/inputpath-to-url-filter-plugin", +}); + +Object.defineProperty(FilterPlugin, "eleventyPluginOptions", { + value: { + unique: true, + }, +}); + +Object.defineProperty(TransformPlugin, "eleventyPackage", { + value: "@11ty/eleventy/inputpath-to-url-transform-plugin", +}); + +Object.defineProperty(TransformPlugin, "eleventyPluginOptions", { + value: { + unique: true, + }, +}); + +export default TransformPlugin; + +export { FilterPlugin, TransformPlugin }; diff --git a/node_modules/@11ty/eleventy/src/Plugins/Pagination.js b/node_modules/@11ty/eleventy/src/Plugins/Pagination.js new file mode 100755 index 0000000..8e5b1de --- /dev/null +++ b/node_modules/@11ty/eleventy/src/Plugins/Pagination.js @@ -0,0 +1,379 @@ +import { isPlainObject } from "@11ty/eleventy-utils"; +import lodash from "@11ty/lodash-custom"; +import { DeepCopy } from "@11ty/eleventy-utils"; + +import EleventyBaseError from "../Errors/EleventyBaseError.js"; +import { ProxyWrap } from "../Util/Objects/ProxyWrap.js"; +// import { DeepFreeze } from "../Util/Objects/DeepFreeze.js"; +import TemplateData from "../Data/TemplateData.js"; + +const { set: lodashSet, get: lodashGet, chunk: lodashChunk } = lodash; + +class PaginationConfigError extends EleventyBaseError {} +class PaginationError extends EleventyBaseError {} + +class Pagination { + constructor(tmpl, data, config) { + if (!config) { + throw new PaginationConfigError("Expected `config` argument to Pagination class."); + } + + this.config = config; + + this.setTemplate(tmpl); + this.setData(data); + } + + get inputPathForErrorMessages() { + if (this.template) { + return ` (${this.template.inputPath})`; + } + return ""; + } + + static hasPagination(data) { + return "pagination" in data; + } + + hasPagination() { + if (!this.data) { + throw new Error( + `Missing \`setData\` call for Pagination object${this.inputPathForErrorMessages}`, + ); + } + return Pagination.hasPagination(this.data); + } + + circularReferenceCheck(data) { + let key = data.pagination.data; + let includedTags = TemplateData.getIncludedTagNames(data); + + for (let tag of includedTags) { + if (`collections.${tag}` === key) { + throw new PaginationError( + `Pagination circular reference${this.inputPathForErrorMessages}, data:\`${key}\` iterates over both the \`${tag}\` collection and also supplies pages to that collection.`, + ); + } + } + } + + setData(data) { + this.data = data || {}; + this.target = []; + + if (!this.hasPagination()) { + return; + } + + if (!data.pagination) { + throw new Error( + `Misconfigured pagination data in template front matter${this.inputPathForErrorMessages} (YAML front matter precaution: did you use tabs and not spaces for indentation?).`, + ); + } else if (!("size" in data.pagination)) { + throw new Error( + `Missing pagination size in front matter data${this.inputPathForErrorMessages}`, + ); + } + this.circularReferenceCheck(data); + + this.size = data.pagination.size; + this.alias = data.pagination.alias; + this.fullDataSet = this._get(this.data, this._getDataKey()); + // this returns an array + this.target = this._resolveItems(); + this.chunkedItems = this.pagedItems; + } + + setTemplate(tmpl) { + this.template = tmpl; + } + + _getDataKey() { + return this.data.pagination.data; + } + + shouldResolveDataToObjectValues() { + if ("resolve" in this.data.pagination) { + return this.data.pagination.resolve === "values"; + } + return false; + } + + isFiltered(value) { + if ("filter" in this.data.pagination) { + let filtered = this.data.pagination.filter; + if (Array.isArray(filtered)) { + return filtered.indexOf(value) > -1; + } + + return filtered === value; + } + + return false; + } + + _has(target, key) { + let notFoundValue = "__NOT_FOUND_ERROR__"; + let data = lodashGet(target, key, notFoundValue); + return data !== notFoundValue; + } + + _get(target, key) { + let notFoundValue = "__NOT_FOUND_ERROR__"; + let data = lodashGet(target, key, notFoundValue); + if (data === notFoundValue) { + throw new Error( + `Could not find pagination data${this.inputPathForErrorMessages}, went looking for: ${key}`, + ); + } + return data; + } + + _resolveItems() { + let keys; + if (Array.isArray(this.fullDataSet)) { + keys = this.fullDataSet; + this.paginationTargetType = "array"; + } else if (isPlainObject(this.fullDataSet)) { + this.paginationTargetType = "object"; + if (this.shouldResolveDataToObjectValues()) { + keys = Object.values(this.fullDataSet); + } else { + keys = Object.keys(this.fullDataSet); + } + } else { + throw new Error( + `Unexpected data found in pagination target${this.inputPathForErrorMessages}: expected an Array or an Object.`, + ); + } + + // keys must be an array + let result = keys.slice(); + + if (this.data.pagination.before && typeof this.data.pagination.before === "function") { + // we don’t need to make a copy of this because we .slice() above to create a new copy + let fns = {}; + if (this.config) { + fns = this.config.javascriptFunctions; + } + result = this.data.pagination.before.call(fns, result, this.data); + } + + if (this.data.pagination.reverse === true) { + result = result.reverse(); + } + + if (this.data.pagination.filter) { + result = result.filter((value) => !this.isFiltered(value)); + } + + return result; + } + + get pagedItems() { + if (!this.data) { + throw new Error( + `Missing \`setData\` call for Pagination object${this.inputPathForErrorMessages}`, + ); + } + + const chunks = lodashChunk(this.target, this.size); + if (this.data.pagination?.generatePageOnEmptyData) { + return chunks.length ? chunks : [[]]; + } else { + return chunks; + } + } + + getPageCount() { + if (!this.hasPagination()) { + return 0; + } + + return this.chunkedItems.length; + } + + getNormalizedItems(pageItems) { + return this.size === 1 ? pageItems[0] : pageItems; + } + + getOverrideDataPages(items, pageNumber) { + return { + // See Issue #345 for more examples + page: { + previous: pageNumber > 0 ? this.getNormalizedItems(items[pageNumber - 1]) : null, + next: pageNumber < items.length - 1 ? this.getNormalizedItems(items[pageNumber + 1]) : null, + first: items.length ? this.getNormalizedItems(items[0]) : null, + last: items.length ? this.getNormalizedItems(items[items.length - 1]) : null, + }, + + pageNumber, + }; + } + + getOverrideDataLinks(pageNumber, templateCount, links) { + let obj = {}; + + // links are okay but hrefs are better + obj.previousPageLink = pageNumber > 0 ? links[pageNumber - 1] : null; + obj.previous = obj.previousPageLink; + + obj.nextPageLink = pageNumber < templateCount - 1 ? links[pageNumber + 1] : null; + obj.next = obj.nextPageLink; + + obj.firstPageLink = links.length > 0 ? links[0] : null; + obj.lastPageLink = links.length > 0 ? links[links.length - 1] : null; + + obj.links = links; + // todo deprecated, consistency with collections and use links instead + obj.pageLinks = links; + return obj; + } + + getOverrideDataHrefs(pageNumber, templateCount, hrefs) { + let obj = {}; + + // hrefs are better than links + obj.previousPageHref = pageNumber > 0 ? hrefs[pageNumber - 1] : null; + obj.nextPageHref = pageNumber < templateCount - 1 ? hrefs[pageNumber + 1] : null; + + obj.firstPageHref = hrefs.length > 0 ? hrefs[0] : null; + obj.lastPageHref = hrefs.length > 0 ? hrefs[hrefs.length - 1] : null; + + obj.hrefs = hrefs; + + // better names + obj.href = { + previous: obj.previousPageHref, + next: obj.nextPageHref, + first: obj.firstPageHref, + last: obj.lastPageHref, + }; + + return obj; + } + + async getPageTemplates() { + if (!this.data) { + throw new Error( + `Missing \`setData\` call for Pagination object${this.inputPathForErrorMessages}`, + ); + } + + if (!this.hasPagination()) { + return []; + } + + let entries = []; + let items = this.chunkedItems; + let pages = this.size === 1 ? items.map((entry) => entry[0]) : items; + + let links = []; + let hrefs = []; + + let hasPermalinkField = + Boolean(this.data[this.config.keys.permalink]) || + Boolean(this.data.eleventyComputed?.[this.config.keys.permalink]); + + // Do *not* pass collections through DeepCopy, we’ll re-add them back in later. + let collections = this.data.collections; + if (collections) { + delete this.data.collections; + } + + let parentData = DeepCopy( + { + pagination: { + data: this.data.pagination.data, + size: this.data.pagination.size, + alias: this.alias, + pages, + }, + }, + this.data, + ); + + // Restore skipped collections + if (collections) { + this.data.collections = collections; + // Keep the original reference to the collections, no deep copy!! + parentData.collections = collections; + } + + // TODO this does work fine but let’s wait on enabling it. + // DeepFreeze(parentData, ["collections"]); + + // TODO future improvement dea: use a light Template wrapper for paged template clones (PagedTemplate?) + // so that we don’t have the memory cost of the full template (and can reuse the parent + // template for some things) + + let indices = new Set(); + for (let j = 0; j <= items.length - 1; j++) { + indices.add(j); + } + + for (let pageNumber of indices) { + let cloned = await this.template.clone(); + + if (pageNumber > 0 && !hasPermalinkField) { + cloned.setExtraOutputSubdirectory(pageNumber); + } + + let paginationData = { + pagination: { + items: items[pageNumber], + }, + page: {}, + }; + Object.assign(paginationData.pagination, this.getOverrideDataPages(items, pageNumber)); + + if (this.alias) { + lodashSet(paginationData, this.alias, this.getNormalizedItems(items[pageNumber])); + } + + // Do *not* deep merge pagination data! See https://github.com/11ty/eleventy/issues/147#issuecomment-440802454 + let clonedData = ProxyWrap(paginationData, parentData); + + // Previous method: + // let clonedData = DeepCopy(paginationData, parentData); + + let { /*linkInstance,*/ rawPath, path, href } = await cloned.getOutputLocations(clonedData); + // TODO subdirectory to links if the site doesn’t live at / + if (rawPath) { + links.push("/" + rawPath); + } + + hrefs.push(href); + + // page.url and page.outputPath are used to avoid another getOutputLocations call later, see Template->addComputedData + clonedData.page.url = href; + clonedData.page.outputPath = path; + + entries.push({ + pageNumber, + + // This is used by i18n Plugin to allow subgroups of nested pagination to be separate + groupNumber: items[pageNumber]?.[0]?.eleventyPaginationGroupNumber, + + template: cloned, + data: clonedData, + }); + } + + // we loop twice to pass in the appropriate prev/next links (already full generated now) + let index = 0; + for (let pageEntry of entries) { + let linksObj = this.getOverrideDataLinks(index, items.length, links); + + Object.assign(pageEntry.data.pagination, linksObj); + + let hrefsObj = this.getOverrideDataHrefs(index, items.length, hrefs); + Object.assign(pageEntry.data.pagination, hrefsObj); + index++; + } + + return entries; + } +} + +export default Pagination; 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 }; |
