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