This commit is contained in:
semanticdata 2023-09-29 14:22:54 -05:00
commit edefffa137
9 changed files with 220 additions and 5 deletions

View File

@ -32,6 +32,7 @@ Quartz by default doesn't understand `org-roam` files as they aren't Markdown. Y
- `replaceFigureWithMdImg`: Whether to replace `<figure/>` with `![]()`
- Formatting
- `removeHugoShortcode`: Whether to remove hugo shortcode syntax (`{{}}`)
- `replaceOrgLatex`: Whether to replace org-mode formatting for latex fragments with what `Plugin.Latex` supports.
> [!warning]
>

View File

@ -0,0 +1,35 @@
---
title: "Breadcrumbs"
tags:
- component
---
Breadcrumbs provide a way to navigate a hierarchy of pages within your site using a list of its parent folders.
By default, the element at the very top of your page is the breadcrumb navigation bar (can also be seen at the top on this page!).
## Customization
Most configuration can be done by passing in options to `Component.Breadcrumbs()`.
For example, here's what the default configuration looks like:
```typescript title="quartz.layout.ts"
Component.Breadcrumbs({
spacerSymbol: ">", // symbol between crumbs
rootName: "Home", // name of first/root element
resolveFrontmatterTitle: false, // wether to resolve folder names through frontmatter titles (more computationally expensive)
hideOnRoot: true, // wether to hide breadcrumbs on root `index.md` page
})
```
When passing in your own options, you can omit any or all of these fields if you'd like to keep the default value for that field.
You can also adjust where the breadcrumbs will be displayed by adjusting the [[layout]] (moving `Component.Breadcrumbs()` up or down)
Want to customize it even more?
- Removing breadcrumbs: delete all usages of `Component.Breadcrumbs()` from `quartz.layout.ts`.
- Component: `quartz/components/Breadcrumbs.tsx`
- Style: `quartz/components/styles/breadcrumbs.scss`
- Script: inline at `quartz/components/Breadcrumbs.tsx`

View File

@ -0,0 +1,118 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import breadcrumbsStyle from "./styles/breadcrumbs.scss"
import { FullSlug, SimpleSlug, resolveRelative } from "../util/path"
import { capitalize } from "../util/lang"
import { QuartzPluginData } from "../plugins/vfile"
type CrumbData = {
displayName: string
path: string
}
interface BreadcrumbOptions {
/**
* Symbol between crumbs
*/
spacerSymbol: string
/**
* Name of first crumb
*/
rootName: string
/**
* wether to look up frontmatter title for folders (could cause performance problems with big vaults)
*/
resolveFrontmatterTitle: boolean
/**
* Wether to display breadcrumbs on root `index.md`
*/
hideOnRoot: boolean
}
const defaultOptions: BreadcrumbOptions = {
spacerSymbol: ">",
rootName: "Home",
resolveFrontmatterTitle: false,
hideOnRoot: true,
}
function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: SimpleSlug): CrumbData {
return { displayName, path: resolveRelative(baseSlug, currentSlug) }
}
// given a folderName (e.g. "features"), search for the corresponding `index.md` file
function findCurrentFile(allFiles: QuartzPluginData[], folderName: string) {
return allFiles.find((file) => {
if (file.slug?.endsWith("index")) {
const folderParts = file.filePath?.split("/")
if (folderParts) {
const name = folderParts[folderParts?.length - 2]
if (name === folderName) {
return true
}
}
}
})
}
export default ((opts?: Partial<BreadcrumbOptions>) => {
// Merge options with defaults
const options: BreadcrumbOptions = { ...defaultOptions, ...opts }
function Breadcrumbs({ fileData, allFiles }: QuartzComponentProps) {
// Hide crumbs on root if enabled
if (options.hideOnRoot && fileData.slug === "index") {
return <></>
}
// Format entry for root element
const firstEntry = formatCrumb(capitalize(options.rootName), fileData.slug!, "/" as SimpleSlug)
const crumbs: CrumbData[] = [firstEntry]
// Get parts of filePath (every folder)
const parts = fileData.filePath?.split("/")?.splice(1)
if (parts) {
// full path until current part
let current = ""
for (let i = 0; i < parts.length - 1; i++) {
const folderName = parts[i]
let currentTitle = folderName
// TODO: performance optimizations/memoizing
// Try to resolve frontmatter folder title
if (options?.resolveFrontmatterTitle) {
// try to find file for current path
const currentFile = findCurrentFile(allFiles, folderName)
if (currentFile) {
currentTitle = currentFile.frontmatter!.title
}
}
// Add current path to full path
current += folderName + "/"
// Format and add current crumb
const crumb = formatCrumb(capitalize(currentTitle), fileData.slug!, current as SimpleSlug)
crumbs.push(crumb)
}
// Add current file to crumb (can directly use frontmatter title)
if (parts.length > 0) {
crumbs.push({
displayName: capitalize(fileData.frontmatter!.title),
path: "",
})
}
}
return (
<nav class="breadcrumb-container" aria-label="breadcrumbs">
{crumbs.map((crumb, index) => (
<div class="breadcrumb-element">
<a href={crumb.path}>{crumb.displayName}</a>
{index !== crumbs.length - 1 && <p>{` ${options.spacerSymbol} `}</p>}
</div>
))}
</nav>
)
}
Breadcrumbs.css = breadcrumbsStyle
return Breadcrumbs
}) satisfies QuartzComponentConstructor

View File

@ -28,7 +28,8 @@ function TagList({ fileData }: QuartzComponentProps) {
TagList.css = `
.tags {
list-style: none;
display: flex;
display:flex;
flex-wrap: wrap;
padding-left: 0;
gap: 0.4rem;
margin: 1rem 0;

View File

@ -18,6 +18,7 @@ import Footer from "./Footer"
import DesktopOnly from "./DesktopOnly"
import MobileOnly from "./MobileOnly"
import RecentNotes from "./RecentNotes"
import Breadcrumbs from "./Breadcrumbs"
export {
ArticleTitle,
@ -40,4 +41,5 @@ export {
MobileOnly,
RecentNotes,
NotFound,
Breadcrumbs,
}

View File

@ -0,0 +1,22 @@
.breadcrumb-container {
margin: 0;
margin-top: 0.75rem;
padding: 0;
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 0.5rem;
}
.breadcrumb-element {
p {
margin: 0;
margin-left: 0.5rem;
padding: 0;
line-height: normal;
}
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}

View File

@ -14,6 +14,7 @@ import { FilePath, pathToRoot, slugTag, slugifyFilePath } from "../../util/path"
import { toHast } from "mdast-util-to-hast"
import { toHtml } from "hast-util-to-html"
import { PhrasingContent } from "mdast-util-find-and-replace/lib"
import { capitalize } from "../../util/lang"
export interface Options {
comments: boolean
@ -104,10 +105,6 @@ function canonicalizeCallout(calloutName: string): keyof typeof callouts {
return calloutMapping[callout] ?? "note"
}
const capitalize = (s: string): string => {
return s.substring(0, 1).toUpperCase() + s.substring(1)
}
// !? -> optional embedding
// \[\[ -> open brace
// ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name)

View File

@ -9,6 +9,9 @@ export interface Options {
removeHugoShortcode: boolean
/** Replace <figure/> with ![]() */
replaceFigureWithMdImg: boolean
/** Replace org latex fragments with $ and $$ */
replaceOrgLatex: boolean
}
const defaultOptions: Options = {
@ -16,12 +19,27 @@ const defaultOptions: Options = {
removePredefinedAnchor: true,
removeHugoShortcode: true,
replaceFigureWithMdImg: true,
replaceOrgLatex: true,
}
const relrefRegex = new RegExp(/\[([^\]]+)\]\(\{\{< relref "([^"]+)" >\}\}\)/, "g")
const predefinedHeadingIdRegex = new RegExp(/(.*) {#(?:.*)}/, "g")
const hugoShortcodeRegex = new RegExp(/{{(.*)}}/, "g")
const figureTagRegex = new RegExp(/< ?figure src="(.*)" ?>/, "g")
// \\\\\( -> matches \\(
// (.+?) -> Lazy match for capturing the equation
// \\\\\) -> matches \\)
const inlineLatexRegex = new RegExp(/\\\\\((.+?)\\\\\)/, "g")
// (?:\\begin{equation}|\\\\\(|\\\\\[) -> start of equation
// ([\s\S]*?) -> Matches the block equation
// (?:\\\\\]|\\\\\)|\\end{equation}) -> end of equation
const blockLatexRegex = new RegExp(
/(?:\\begin{equation}|\\\\\(|\\\\\[)([\s\S]*?)(?:\\\\\]|\\\\\)|\\end{equation})/,
"g",
)
// \$\$[\s\S]*?\$\$ -> Matches block equations
// \$.*?\$ -> Matches inline equations
const quartzLatexRegex = new RegExp(/\$\$[\s\S]*?\$\$|\$.*?\$/, "g")
/**
* ox-hugo is an org exporter backend that exports org files to hugo-compatible
@ -67,6 +85,23 @@ export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin<Partial<Options> |
return `![](${src})`
})
}
if (opts.replaceOrgLatex) {
src = src.toString()
src = src.replaceAll(inlineLatexRegex, (value, ...capture) => {
const [eqn] = capture
return `$${eqn}$`
})
src = src.replaceAll(blockLatexRegex, (value, ...capture) => {
const [eqn] = capture
return `$$${eqn}$$`
})
// ox-hugo escapes _ as \_
src = src.replaceAll(quartzLatexRegex, (value) => {
return value.replaceAll("\\_", "_")
})
}
return src
},
}

View File

@ -5,3 +5,7 @@ export function pluralize(count: number, s: string): string {
return `${count} ${s}s`
}
}
export function capitalize(s: string): string {
return s.substring(0, 1).toUpperCase() + s.substring(1)
}