Initial commit: 11ty website with Fire/Frost branding

This commit is contained in:
The Trinity
2026-04-02 18:39:00 -05:00
commit 40b45dff2e
1646 changed files with 329080 additions and 0 deletions

View File

@@ -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 well 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 };

View File

@@ -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 };

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

@@ -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 wont 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 Eleventys 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 pages 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") {
// Youre linking to a localized file that doesnt 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;

View File

@@ -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 };

View File

@@ -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 files 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 dont 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 dont 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 };

379
node_modules/@11ty/eleventy/src/Plugins/Pagination.js generated vendored Executable file
View File

@@ -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 dont 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, well 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 lets wait on enabling it.
// DeepFreeze(parentData, ["collections"]);
// TODO future improvement dea: use a light Template wrapper for paged template clones (PagedTemplate?)
// so that we dont 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 doesnt 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;

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

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