277 lines
8.3 KiB
JavaScript
277 lines
8.3 KiB
JavaScript
|
|
const DepGraph = require("dependency-graph").DepGraph;
|
|||
|
|
|
|||
|
|
function findNavigationEntries(nodes = [], key = "") {
|
|||
|
|
let keys = key.split(",").filter(k => Boolean(k));
|
|||
|
|
let pages = {};
|
|||
|
|
for(let entry of nodes) {
|
|||
|
|
let data = entry?.data || {};
|
|||
|
|
if(data?.eleventyNavigation) {
|
|||
|
|
let {eleventyNavigation} = data || {};
|
|||
|
|
|
|||
|
|
let pageKey;
|
|||
|
|
if(!key && !eleventyNavigation.parent) { // top level (no parents)
|
|||
|
|
pageKey = "__default";
|
|||
|
|
} else if(keys.includes(eleventyNavigation.parent)) {
|
|||
|
|
pageKey = eleventyNavigation.parent;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if(pageKey) {
|
|||
|
|
if(!pages[pageKey]) {
|
|||
|
|
pages[pageKey] = [];
|
|||
|
|
}
|
|||
|
|
let url = eleventyNavigation.url ?? data?.page?.url;
|
|||
|
|
|
|||
|
|
pages[pageKey].push(Object.assign({ data }, eleventyNavigation, {
|
|||
|
|
...(url ? { url } : {}),
|
|||
|
|
pluginType: "eleventy-navigation",
|
|||
|
|
...(keys.length > 0 ? { parentKey: eleventyNavigation.parent } : {}),
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return Object.values(pages).flat().sort(function(a, b) {
|
|||
|
|
if(a.pinned && b.pinned) {
|
|||
|
|
return (a.order || 0) - (b.order || 0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let order = [a.order, b.order];
|
|||
|
|
if(a.pinned) {
|
|||
|
|
order[0] = -Infinity;
|
|||
|
|
}
|
|||
|
|
if(b.pinned) {
|
|||
|
|
order[1] = -Infinity;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if(order[0] === undefined && order[1] === undefined) {
|
|||
|
|
return 0;
|
|||
|
|
}
|
|||
|
|
if(order[1] === undefined) {
|
|||
|
|
return -1;
|
|||
|
|
}
|
|||
|
|
if(order[0] === undefined) {
|
|||
|
|
return 1;
|
|||
|
|
}
|
|||
|
|
return order[0] - order[1];
|
|||
|
|
}).map(function(entry) {
|
|||
|
|
if(!entry.title) {
|
|||
|
|
entry.title = entry.key;
|
|||
|
|
}
|
|||
|
|
if(entry.key) {
|
|||
|
|
entry.children = findNavigationEntries(nodes, entry.key);
|
|||
|
|
}
|
|||
|
|
return entry;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function findDependencies(pages, depGraph, parentKey) {
|
|||
|
|
for( let page of pages ) {
|
|||
|
|
depGraph.addNode(page.key, page);
|
|||
|
|
if(parentKey) {
|
|||
|
|
depGraph.addDependency(page.key, parentKey);
|
|||
|
|
}
|
|||
|
|
if(page.children) {
|
|||
|
|
findDependencies(page.children, depGraph, page.key);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getDependencyGraph(nodes) {
|
|||
|
|
let pages = findNavigationEntries(nodes);
|
|||
|
|
let graph = new DepGraph();
|
|||
|
|
findDependencies(pages, graph);
|
|||
|
|
return graph;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function isOptionMatch(options, name) {
|
|||
|
|
// Liquid.js issue #35
|
|||
|
|
if(Array.isArray(options)) {
|
|||
|
|
return options[options.indexOf(name)]
|
|||
|
|
}
|
|||
|
|
return options[name];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function findBreadcrumbEntries(nodes, activeKey, options = {}) {
|
|||
|
|
let graph = getDependencyGraph(nodes);
|
|||
|
|
if (isOptionMatch(options, "allowMissing") && !graph.hasNode(activeKey)) {
|
|||
|
|
// Fail gracefully if the key isn't in the graph
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
let deps = graph.dependenciesOf(activeKey);
|
|||
|
|
if(isOptionMatch(options, "includeSelf")) {
|
|||
|
|
deps.push(activeKey);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return activeKey ? deps.map(key => {
|
|||
|
|
let data = Object.assign({}, graph.getNodeData(key));
|
|||
|
|
delete data.children;
|
|||
|
|
data._isBreadcrumb = true;
|
|||
|
|
return data;
|
|||
|
|
}) : [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getUrlFilter(eleventyConfig) {
|
|||
|
|
// eleventyConfig.pathPrefix was first available in Eleventy 2.0.0-canary.15
|
|||
|
|
// And in Eleventy 2.0.0-canary.15 we recommend the a built-in transform for pathPrefix
|
|||
|
|
if(eleventyConfig.pathPrefix !== undefined) {
|
|||
|
|
return function(url) {
|
|||
|
|
return url;
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if("getFilter" in eleventyConfig) {
|
|||
|
|
// v0.10.0 and above
|
|||
|
|
return eleventyConfig.getFilter("url");
|
|||
|
|
} else if("nunjucksFilters" in eleventyConfig) {
|
|||
|
|
// backwards compat, hardcoded key
|
|||
|
|
return eleventyConfig.nunjucksFilters.url;
|
|||
|
|
} else {
|
|||
|
|
// Theoretically we could just move on here with a `url => url` but then `pathPrefix`
|
|||
|
|
// would not work and it wouldn’t be obvious why—so let’s fail loudly to avoid that.
|
|||
|
|
throw new Error("Could not find a `url` filter for the eleventy-navigation plugin in eleventyNavigationToHtml filter.");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function buildHtmlAttr(name, values) {
|
|||
|
|
// values could be array or string
|
|||
|
|
if (!values || !values.length) {
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
const valueStr = Array.isArray(values) ? values.join(" ") : values;
|
|||
|
|
return ` ${name}="${valueStr}"`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function buildAllHtmlAttrs(attrs) {
|
|||
|
|
return attrs.reduce((acc, { name, values }) => acc + buildHtmlAttr(name, values), '');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function navigationToHtml(pages, options = {}) {
|
|||
|
|
options = Object.assign({
|
|||
|
|
listElement: "ul",
|
|||
|
|
listItemElement: "li",
|
|||
|
|
listClass: "",
|
|||
|
|
listItemClass: "",
|
|||
|
|
listItemHasChildrenClass: "",
|
|||
|
|
activeKey: "",
|
|||
|
|
activeListItemClass: "",
|
|||
|
|
anchorClass: "",
|
|||
|
|
activeAnchorClass: "",
|
|||
|
|
useAriaCurrentAttr: false,
|
|||
|
|
showExcerpt: false,
|
|||
|
|
isChildList: false,
|
|||
|
|
useTopLevelDetails: false,
|
|||
|
|
anchorElementWithoutHref: "a", // default, better to use span
|
|||
|
|
}, options);
|
|||
|
|
|
|||
|
|
let isChildList = !!options.isChildList;
|
|||
|
|
options.isChildList = true;
|
|||
|
|
|
|||
|
|
let urlFilter;
|
|||
|
|
|
|||
|
|
if(pages.length && pages[0].pluginType !== "eleventy-navigation") {
|
|||
|
|
throw new Error("Incorrect argument passed to eleventyNavigationToHtml filter. You must call `eleventyNavigation` or `eleventyNavigationBreadcrumb` first, like: `collection.all | eleventyNavigation | eleventyNavigationToHtml | safe`");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return pages.length ? `<${options.listElement}${!isChildList && options.listClass ? ` class="${options.listClass}"` : ''}>${pages.map(entry => {
|
|||
|
|
let liClass = [];
|
|||
|
|
let aClass = [];
|
|||
|
|
let aAttrs = [];
|
|||
|
|
if(options.listItemClass) {
|
|||
|
|
liClass.push(options.listItemClass);
|
|||
|
|
}
|
|||
|
|
if(options.anchorClass) {
|
|||
|
|
aClass.push(options.anchorClass);
|
|||
|
|
}
|
|||
|
|
if(entry.url) {
|
|||
|
|
if(!urlFilter) {
|
|||
|
|
// don’t get if not used
|
|||
|
|
urlFilter = getUrlFilter(this);
|
|||
|
|
}
|
|||
|
|
aAttrs.push({name: "href", values: urlFilter(entry.url)})
|
|||
|
|
}
|
|||
|
|
if(options.activeKey === entry.key) {
|
|||
|
|
if(options.activeListItemClass) {
|
|||
|
|
liClass.push(options.activeListItemClass);
|
|||
|
|
}
|
|||
|
|
if(options.activeAnchorClass) {
|
|||
|
|
aClass.push(options.activeAnchorClass);
|
|||
|
|
}
|
|||
|
|
if(options.useAriaCurrentAttr) {
|
|||
|
|
aAttrs.push({ name: "aria-current", values: "page" });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if(options.listItemHasChildrenClass && entry.children && entry.children.length) {
|
|||
|
|
liClass.push(options.listItemHasChildrenClass);
|
|||
|
|
}
|
|||
|
|
if(aClass.length) {
|
|||
|
|
aAttrs.push({ name: "class", values: aClass });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let postfix = "";
|
|||
|
|
|
|||
|
|
// Helper to show pin/order in text:
|
|||
|
|
// let hasOrder = entry.order || entry.order === 0;
|
|||
|
|
// if(process.env.ELEVENTY_RUN_MODE === "serve" && (hasOrder || entry.pinned)) {
|
|||
|
|
// postfix = ` (${entry.pinned ? "📌" : ""}${entry.order ?? ""})`;
|
|||
|
|
// }
|
|||
|
|
|
|||
|
|
let aAttrsStr = buildAllHtmlAttrs(aAttrs);
|
|||
|
|
let hasLink = aAttrs.find(entry => entry.name === "href");
|
|||
|
|
let itemTitle = entry.title + postfix;
|
|||
|
|
|
|||
|
|
let titleHtmlStart = `<a${aAttrsStr}>${itemTitle}</a>`;
|
|||
|
|
|
|||
|
|
// purely defensive use of `useTopLevelDetails` here
|
|||
|
|
if(options.anchorElementWithoutHref && !hasLink) {
|
|||
|
|
titleHtmlStart = `<${options.anchorElementWithoutHref}>${itemTitle}</${options.anchorElementWithoutHref}>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let titleHtmlEnd = "";
|
|||
|
|
if(options.useTopLevelDetails && !isChildList && entry.children) {
|
|||
|
|
if(hasLink) {
|
|||
|
|
// `<a>` must be sibling: no other interactive elements in <summary>
|
|||
|
|
titleHtmlStart = `${titleHtmlStart}<details><summary>${itemTitle}</summary>`;
|
|||
|
|
} else {
|
|||
|
|
titleHtmlStart = `<details><summary>${itemTitle}</summary>`;
|
|||
|
|
}
|
|||
|
|
titleHtmlEnd = "</details>";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let childContentStr = entry.children ? navigationToHtml.call(this, entry.children, options) : "";
|
|||
|
|
|
|||
|
|
return `<${options.listItemElement}${buildHtmlAttr("class", liClass)}>${titleHtmlStart}${options.showExcerpt && entry.excerpt ? `: ${entry.excerpt}` : ""}${childContentStr}${titleHtmlEnd}</${options.listItemElement}>`;
|
|||
|
|
}).join("\n")}</${options.listElement}>` : "";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function navigationToMarkdown(pages, options = {}) {
|
|||
|
|
options = Object.assign({
|
|||
|
|
showExcerpt: false,
|
|||
|
|
childDepth: 0
|
|||
|
|
}, options);
|
|||
|
|
|
|||
|
|
let childDepth = 1 + options.childDepth;
|
|||
|
|
options.childDepth++;
|
|||
|
|
|
|||
|
|
let urlFilter;
|
|||
|
|
|
|||
|
|
if(pages.length && pages[0].pluginType !== "eleventy-navigation") {
|
|||
|
|
throw new Error("Incorrect argument passed to eleventyNavigationToMarkdown filter. You must call `eleventyNavigation` or `eleventyNavigationBreadcrumb` first, like: `collection.all | eleventyNavigation | eleventyNavigationToMarkdown | safe`");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let indent = (new Array(childDepth)).join(" ") || "";
|
|||
|
|
return pages.length ? `${pages.map(entry => {
|
|||
|
|
if(entry.url && !urlFilter) {
|
|||
|
|
// don’t get if not used
|
|||
|
|
urlFilter = getUrlFilter(this);
|
|||
|
|
}
|
|||
|
|
return `${indent}* ${entry.url ? `[` : ""}${entry.title}${entry.url ? `](${urlFilter(entry.url)})` : ""}${options.showExcerpt && entry.excerpt ? `: ${entry.excerpt}` : ""}\n${entry.children ? navigationToMarkdown.call(this, entry.children, options) : ""}`;
|
|||
|
|
}).join("")}` : "";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
module.exports = {
|
|||
|
|
getDependencyGraph,
|
|||
|
|
findNavigationEntries,
|
|||
|
|
findBreadcrumbEntries,
|
|||
|
|
toHtml: navigationToHtml,
|
|||
|
|
toMarkdown: navigationToMarkdown
|
|||
|
|
};
|