diff --git a/docs/features/breadcrumbs.md b/docs/features/breadcrumbs.md index b8aed34db..41fece114 100644 --- a/docs/features/breadcrumbs.md +++ b/docs/features/breadcrumbs.md @@ -69,8 +69,8 @@ Multiple parents: ```yaml parent: -- [[Basics]] -- [[Reference]] + - [[Basics]] + - [[Reference]] ``` ### Configuration @@ -81,10 +81,10 @@ Default configuration: ```ts Component.ParentBreadcrumbs({ - spacerSymbol: "❯", // symbol displayed between breadcrumb levels - rootName: "Home", // label for the root (index) page + spacerSymbol: "❯", // symbol displayed between breadcrumb levels + rootName: "Home", // label for the root (index) page resolveFrontmatterTitle: true, // use frontmatter.title instead of slug - parentKey: "parent", // frontmatter key used to resolve parents + parentKey: "parent", // frontmatter key used to resolve parents }) ``` diff --git a/quartz.layout.ts b/quartz.layout.ts index 5b54f6569..cc80ba844 100644 --- a/quartz.layout.ts +++ b/quartz.layout.ts @@ -1,68 +1,68 @@ -import { PageLayout, SharedLayout } from "./quartz/cfg"; -import * as Component from "./quartz/components"; +import { PageLayout, SharedLayout } from "./quartz/cfg" +import * as Component from "./quartz/components" // components shared across all pages export const sharedPageComponents: SharedLayout = { - head: Component.Head(), - header: [], - afterBody: [], - footer: Component.Footer({ - links: { - GitHub: "https://github.com/jackyzha0/quartz", - "Discord Community": "https://discord.gg/cRFFHYye7t", - }, - }), -}; + head: Component.Head(), + header: [], + afterBody: [], + footer: Component.Footer({ + links: { + GitHub: "https://github.com/jackyzha0/quartz", + "Discord Community": "https://discord.gg/cRFFHYye7t", + }, + }), +} // components for pages that display a single page (e.g. a single note) export const defaultContentPageLayout: PageLayout = { - beforeBody: [ - Component.ConditionalRender({ - component: Component.ParentBreadcrumbs(), - condition: (page) => page.fileData.slug !== "index", - }), - Component.ArticleTitle(), - Component.ContentMeta(), - Component.TagList(), - ], - left: [ - Component.PageTitle(), - Component.MobileOnly(Component.Spacer()), - Component.Flex({ - components: [ - { - Component: Component.Search(), - grow: true, - }, - { Component: Component.Darkmode() }, - { Component: Component.ReaderMode() }, - ], - }), - Component.Explorer(), - ], - right: [ - Component.Graph(), - Component.DesktopOnly(Component.TableOfContents()), - Component.Backlinks(), - ], -}; + beforeBody: [ + Component.ConditionalRender({ + component: Component.ParentBreadcrumbs(), + condition: (page) => page.fileData.slug !== "index", + }), + Component.ArticleTitle(), + Component.ContentMeta(), + Component.TagList(), + ], + left: [ + Component.PageTitle(), + Component.MobileOnly(Component.Spacer()), + Component.Flex({ + components: [ + { + Component: Component.Search(), + grow: true, + }, + { Component: Component.Darkmode() }, + { Component: Component.ReaderMode() }, + ], + }), + Component.Explorer(), + ], + right: [ + Component.Graph(), + Component.DesktopOnly(Component.TableOfContents()), + Component.Backlinks(), + ], +} // components for pages that display lists of pages (e.g. tags or folders) export const defaultListPageLayout: PageLayout = { - beforeBody: [Component.Breadcrumbs(), Component.ArticleTitle(), Component.ContentMeta()], - left: [ - Component.PageTitle(), - Component.MobileOnly(Component.Spacer()), - Component.Flex({ - components: [ - { - Component: Component.Search(), - grow: true, - }, - { Component: Component.Darkmode() }, - ], - }), - Component.Explorer(), - ], - right: [], -}; + beforeBody: [Component.Breadcrumbs(), Component.ArticleTitle(), Component.ContentMeta()], + left: [ + Component.PageTitle(), + Component.MobileOnly(Component.Spacer()), + Component.Flex({ + components: [ + { + Component: Component.Search(), + grow: true, + }, + { Component: Component.Darkmode() }, + ], + }), + Component.Explorer(), + ], + right: [], +} diff --git a/quartz/components/ParentBreadcrumbs.tsx b/quartz/components/ParentBreadcrumbs.tsx index cb3878e0b..abde76b9b 100644 --- a/quartz/components/ParentBreadcrumbs.tsx +++ b/quartz/components/ParentBreadcrumbs.tsx @@ -1,125 +1,126 @@ -import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"; -import { QuartzPluginData } from "../plugins/vfile"; -import { classNames } from "../util/lang"; -import { resolveRelative, simplifySlug, FullSlug, SimpleSlug } from "../util/path"; -import style from "./styles/breadcrumbs.scss"; +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzPluginData } from "../plugins/vfile" +import { classNames } from "../util/lang" +import { resolveRelative, simplifySlug, FullSlug, SimpleSlug } from "../util/path" +import style from "./styles/breadcrumbs.scss" interface ParentBreadcrumbsOptions { - spacerSymbol?: string; - rootName?: string; - resolveFrontmatterTitle?: boolean; - frontmatterProp?: string, + spacerSymbol?: string + rootName?: string + resolveFrontmatterTitle?: boolean + frontmatterProp?: string } const defaultOptions: ParentBreadcrumbsOptions = { - spacerSymbol: "❯", - rootName: "Home", - resolveFrontmatterTitle: true, - frontmatterProp: "parent", -}; + spacerSymbol: "❯", + rootName: "Home", + resolveFrontmatterTitle: true, + frontmatterProp: "parent", +} export default ((opts?: ParentBreadcrumbsOptions) => { - const options = { ...defaultOptions, ...opts }; - const parentKey = options.frontmatterProp; + const options = { ...defaultOptions, ...opts } + const parentKey = options.frontmatterProp - const ParentBreadcrumbs: QuartzComponent = ({ - fileData, - allFiles, - displayClass, - }: QuartzComponentProps) => { + const ParentBreadcrumbs: QuartzComponent = ({ + fileData, + allFiles, + displayClass, + }: QuartzComponentProps) => { + const parseWikiLink = (content: string): string => { + if (!content) return "" + let clean = content.trim().replace(/^["']|["']$/g, "") + clean = clean.replace(/^\[\[|\]\]$/g, "") + return clean.split("|")[0] + } - const parseWikiLink = (content: string): string => { - if (!content) return ""; - let clean = content.trim().replace(/^["']|["']$/g, ""); - clean = clean.replace(/^\[\[|\]\]$/g, ""); - return clean.split("|")[0]; - }; + const findFile = (name: string) => { + const targetSlug = simplifySlug(name as FullSlug) + return allFiles.find((f: QuartzPluginData) => { + const fSlug = simplifySlug(f.slug!) + return fSlug === targetSlug || fSlug.endsWith(targetSlug) || f.frontmatter?.title === name + }) + } - const findFile = (name: string) => { - const targetSlug = simplifySlug(name as FullSlug); - return allFiles.find((f: QuartzPluginData) => { - const fSlug = simplifySlug(f.slug!); - return fSlug === targetSlug || fSlug.endsWith(targetSlug) || f.frontmatter?.title === name; - }); - }; + type BreadcrumbNode = { displayName: string; path: string } + const crumbs: Array = [] - type BreadcrumbNode = { displayName: string; path: string; }; - const crumbs: Array = []; + let current = fileData + const visited = new Set() + if (current.slug) visited.add(current.slug) - let current = fileData; - const visited = new Set(); - if (current.slug) visited.add(current.slug); + while (current && current.frontmatter?.[parentKey!]) { + const rawParent = current.frontmatter[parentKey!] + const parentList = Array.isArray(rawParent) ? rawParent : [rawParent] - while (current && current.frontmatter?.[parentKey!]) { - const rawParent = current.frontmatter[parentKey!]; - const parentList = Array.isArray(rawParent) ? rawParent : [rawParent]; + const currentLevelNodes: BreadcrumbNode[] = [] + let nextParent: QuartzPluginData | undefined = undefined - const currentLevelNodes: BreadcrumbNode[] = []; - let nextParent: QuartzPluginData | undefined = undefined; + for (const p of parentList) { + const linkStr = parseWikiLink(p as string) + const parentFile = findFile(linkStr) - for (const p of parentList) { - const linkStr = parseWikiLink(p as string); - const parentFile = findFile(linkStr); + if (parentFile && parentFile.slug) { + currentLevelNodes.push({ + displayName: options.resolveFrontmatterTitle + ? (parentFile.frontmatter?.title ?? parentFile.slug) + : parentFile.slug, + path: resolveRelative(fileData.slug!, parentFile.slug!), + }) - if (parentFile && parentFile.slug) { - currentLevelNodes.push({ - displayName: options.resolveFrontmatterTitle - ? parentFile.frontmatter?.title ?? parentFile.slug - : parentFile.slug, - path: resolveRelative(fileData.slug!, parentFile.slug!) - }); + if (!nextParent && !visited.has(parentFile.slug)) { + nextParent = parentFile + } + } + } - if (!nextParent && !visited.has(parentFile.slug)) { - nextParent = parentFile; - } - } - } + if (currentLevelNodes.length > 0) { + crumbs.push(currentLevelNodes) + } - if (currentLevelNodes.length > 0) { - crumbs.push(currentLevelNodes); - } + if (nextParent) { + visited.add(nextParent.slug!) + current = nextParent + } else { + break + } + } - if (nextParent) { - visited.add(nextParent.slug!); - current = nextParent; - } else { - break; - } - } + if (current.slug !== "index") { + crumbs.push([ + { + displayName: options.rootName!, + path: resolveRelative(fileData.slug!, "index" as SimpleSlug), + }, + ]) + } - if (current.slug !== "index") { - crumbs.push([{ - displayName: options.rootName!, - path: resolveRelative(fileData.slug!, "index" as SimpleSlug) - }]); - } + crumbs.reverse() - crumbs.reverse(); + if (crumbs.length === 0 && fileData.slug === "index") { + return <> + } - if (crumbs.length === 0 && fileData.slug === "index") { - return <>; - } + return ( + + ) + } - return ( - - ); - }; - - ParentBreadcrumbs.css = style; - return ParentBreadcrumbs; -}) satisfies QuartzComponentConstructor; + ParentBreadcrumbs.css = style + return ParentBreadcrumbs +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/index.ts b/quartz/components/index.ts index 1830cecac..c7cb712da 100644 --- a/quartz/components/index.ts +++ b/quartz/components/index.ts @@ -1,55 +1,55 @@ -import Content from "./pages/Content"; -import TagContent from "./pages/TagContent"; -import FolderContent from "./pages/FolderContent"; -import NotFound from "./pages/404"; -import ArticleTitle from "./ArticleTitle"; -import Darkmode from "./Darkmode"; -import ReaderMode from "./ReaderMode"; -import Head from "./Head"; -import PageTitle from "./PageTitle"; -import ContentMeta from "./ContentMeta"; -import Spacer from "./Spacer"; -import TableOfContents from "./TableOfContents"; -import Explorer from "./Explorer"; -import TagList from "./TagList"; -import Graph from "./Graph"; -import Backlinks from "./Backlinks"; -import Search from "./Search"; -import Footer from "./Footer"; -import DesktopOnly from "./DesktopOnly"; -import MobileOnly from "./MobileOnly"; -import RecentNotes from "./RecentNotes"; -import Breadcrumbs from "./Breadcrumbs"; -import Comments from "./Comments"; -import Flex from "./Flex"; -import ConditionalRender from "./ConditionalRender"; -import ParentBreadcrumbs from "./ParentBreadcrumbs"; +import Content from "./pages/Content" +import TagContent from "./pages/TagContent" +import FolderContent from "./pages/FolderContent" +import NotFound from "./pages/404" +import ArticleTitle from "./ArticleTitle" +import Darkmode from "./Darkmode" +import ReaderMode from "./ReaderMode" +import Head from "./Head" +import PageTitle from "./PageTitle" +import ContentMeta from "./ContentMeta" +import Spacer from "./Spacer" +import TableOfContents from "./TableOfContents" +import Explorer from "./Explorer" +import TagList from "./TagList" +import Graph from "./Graph" +import Backlinks from "./Backlinks" +import Search from "./Search" +import Footer from "./Footer" +import DesktopOnly from "./DesktopOnly" +import MobileOnly from "./MobileOnly" +import RecentNotes from "./RecentNotes" +import Breadcrumbs from "./Breadcrumbs" +import Comments from "./Comments" +import Flex from "./Flex" +import ConditionalRender from "./ConditionalRender" +import ParentBreadcrumbs from "./ParentBreadcrumbs" export { - ParentBreadcrumbs, - ArticleTitle, - Content, - TagContent, - FolderContent, - Darkmode, - ReaderMode, - Head, - PageTitle, - ContentMeta, - Spacer, - TableOfContents, - Explorer, - TagList, - Graph, - Backlinks, - Search, - Footer, - DesktopOnly, - MobileOnly, - RecentNotes, - NotFound, - Breadcrumbs, - Comments, - Flex, - ConditionalRender, -}; + ParentBreadcrumbs, + ArticleTitle, + Content, + TagContent, + FolderContent, + Darkmode, + ReaderMode, + Head, + PageTitle, + ContentMeta, + Spacer, + TableOfContents, + Explorer, + TagList, + Graph, + Backlinks, + Search, + Footer, + DesktopOnly, + MobileOnly, + RecentNotes, + NotFound, + Breadcrumbs, + Comments, + Flex, + ConditionalRender, +}