import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import breadcrumbsStyle from "./styles/breadcrumbs.scss" import { FullSlug, SimpleSlug, joinSegments, resolveRelative } from "../util/path" import { QuartzPluginData } from "../plugins/vfile" import { classNames } from "../util/lang" type CrumbData = { displayName: string path: string } interface BreadcrumbOptions { /** * Symbol between crumbs */ spacerSymbol: string /** * Name of first crumb */ rootName: string /** * Whether to look up frontmatter title for folders (could cause performance problems with big vaults) */ resolveFrontmatterTitle: boolean /** * Whether to display breadcrumbs on root `index.md` */ hideOnRoot: boolean /** * Whether to display the current page in the breadcrumbs. */ showCurrentPage: boolean } const defaultOptions: BreadcrumbOptions = { spacerSymbol: "❯", rootName: "Home", resolveFrontmatterTitle: true, hideOnRoot: true, showCurrentPage: true, } function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: SimpleSlug): CrumbData { return { displayName: displayName.replaceAll("-", " "), path: resolveRelative(baseSlug, currentSlug), } } export default ((opts?: Partial) => { // Merge options with defaults const options: BreadcrumbOptions = { ...defaultOptions, ...opts } // computed index of folder name to its associated file data let folderIndex: Map | undefined const Breadcrumbs: QuartzComponent = ({ fileData, allFiles, displayClass, }: QuartzComponentProps) => { // Hide crumbs on root if enabled if (options.hideOnRoot && fileData.slug === "index") { return <> } // Format entry for root element const firstEntry = formatCrumb(options.rootName, fileData.slug!, "/" as SimpleSlug) const crumbs: CrumbData[] = [firstEntry] if (!folderIndex && options.resolveFrontmatterTitle) { folderIndex = new Map() // construct the index for the first time for (const file of allFiles) { const folderParts = file.slug?.split("/") if (folderParts?.at(-1) === "index") { folderIndex.set(folderParts.slice(0, -1).join("/"), file) } } } // Split slug into hierarchy/parts const slugParts = fileData.slug?.split("/") if (slugParts) { // is tag breadcrumb? const isTagPath = slugParts[0] === "tags" // full path until current part let currentPath = "" for (let i = 0; i < slugParts.length - 1; i++) { let curPathSegment = slugParts[i] // Try to resolve frontmatter folder title const currentFile = folderIndex?.get(slugParts.slice(0, i + 1).join("/")) if (currentFile) { const title = currentFile.frontmatter!.title if (title !== "index") { curPathSegment = title } } // Add current slug to full path currentPath = joinSegments(currentPath, slugParts[i]) const includeTrailingSlash = !isTagPath || i < 1 // Format and add current crumb const crumb = formatCrumb( curPathSegment, fileData.slug!, (currentPath + (includeTrailingSlash ? "/" : "")) as SimpleSlug, ) crumbs.push(crumb) } // Add current file to crumb (can directly use frontmatter title) if (options.showCurrentPage && slugParts.at(-1) !== "index") { crumbs.push({ displayName: fileData.frontmatter!.title, path: "", }) } } return ( ) } Breadcrumbs.css = breadcrumbsStyle return Breadcrumbs }) satisfies QuartzComponentConstructor