From 0b61f6fbfd20556102ce23444ae7eb9348472952 Mon Sep 17 00:00:00 2001 From: Ben Schlegel <31989404+benschlegel@users.noreply.github.com> Date: Fri, 29 Sep 2023 10:26:15 +0200 Subject: [PATCH 1/4] feat: implement breadcrumb component (#508) * feat: implement breadcrumbs * style: fix styling, move breadcrumbs to top * refactor: move `capitalize to `lang.ts`` * refactor: clean breadcrumb generation * feat: add options to breadcrumbs * feat: implement `resolveFrontmatterTitle` * feat: add `hideOnRoot` option * feat(consistency): capitalize every crumb * style: add `flex-wrap` to parent container * refactor: clean `Breadcrumbs.tsx` * feat(accessibility): use `nav`, add aria label * style: improve look in popovers by adding margin * docs: write docs for breadcrumb component * refactor: collapse `if` condition for hideOnRoot * chore: add todo for perf optimization * docs: update introduction --- docs/features/breadcrumbs.md | 35 +++++++ quartz.layout.ts | 7 +- quartz/components/Breadcrumbs.tsx | 118 ++++++++++++++++++++++ quartz/components/index.ts | 2 + quartz/components/styles/breadcrumbs.scss | 22 ++++ quartz/plugins/transformers/ofm.ts | 5 +- quartz/util/lang.ts | 4 + 7 files changed, 188 insertions(+), 5 deletions(-) create mode 100644 docs/features/breadcrumbs.md create mode 100644 quartz/components/Breadcrumbs.tsx create mode 100644 quartz/components/styles/breadcrumbs.scss diff --git a/docs/features/breadcrumbs.md b/docs/features/breadcrumbs.md new file mode 100644 index 000000000..9f6b64551 --- /dev/null +++ b/docs/features/breadcrumbs.md @@ -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 graph view: 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` diff --git a/quartz.layout.ts b/quartz.layout.ts index 8c1c6c114..8b6edd8f8 100644 --- a/quartz.layout.ts +++ b/quartz.layout.ts @@ -15,7 +15,12 @@ export const sharedPageComponents: SharedLayout = { // components for pages that display a single page (e.g. a single note) export const defaultContentPageLayout: PageLayout = { - beforeBody: [Component.ArticleTitle(), Component.ContentMeta(), Component.TagList()], + beforeBody: [ + Component.Breadcrumbs(), + Component.ArticleTitle(), + Component.ContentMeta(), + Component.TagList(), + ], left: [ Component.PageTitle(), Component.MobileOnly(Component.Spacer()), diff --git a/quartz/components/Breadcrumbs.tsx b/quartz/components/Breadcrumbs.tsx new file mode 100644 index 000000000..f928a4cf9 --- /dev/null +++ b/quartz/components/Breadcrumbs.tsx @@ -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) => { + // 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 ( + + ) + } + Breadcrumbs.css = breadcrumbsStyle + return Breadcrumbs +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/index.ts b/quartz/components/index.ts index d7b6a1c5e..b3db76bed 100644 --- a/quartz/components/index.ts +++ b/quartz/components/index.ts @@ -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, } diff --git a/quartz/components/styles/breadcrumbs.scss b/quartz/components/styles/breadcrumbs.scss new file mode 100644 index 000000000..789808baf --- /dev/null +++ b/quartz/components/styles/breadcrumbs.scss @@ -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; +} diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index 4d55edad8..226e9394e 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -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) diff --git a/quartz/util/lang.ts b/quartz/util/lang.ts index eb03a2436..5211b5d9a 100644 --- a/quartz/util/lang.ts +++ b/quartz/util/lang.ts @@ -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) +} From 0138085c16856d20d1d2cad5670f1f61c8e500d1 Mon Sep 17 00:00:00 2001 From: Catchears <57631841+Catchears@users.noreply.github.com> Date: Fri, 29 Sep 2023 17:19:10 +0200 Subject: [PATCH 2/4] docs: fix typo in breadcrumbs documentation (#513) --- docs/features/breadcrumbs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/breadcrumbs.md b/docs/features/breadcrumbs.md index 9f6b64551..94db66ac0 100644 --- a/docs/features/breadcrumbs.md +++ b/docs/features/breadcrumbs.md @@ -29,7 +29,7 @@ You can also adjust where the breadcrumbs will be displayed by adjusting the [[l Want to customize it even more? -- Removing graph view: delete all usages of `Component.Breadcrumbs()` from `quartz.layout.ts`. +- 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` From 5232d09af520e12bc421cf19ae5d231a7e36cd4d Mon Sep 17 00:00:00 2001 From: ArtfulAzeria <146041757+ArtfulAzeria@users.noreply.github.com> Date: Fri, 29 Sep 2023 20:17:48 +0200 Subject: [PATCH 3/4] feat: Better and more responsive tag behavior (#515) * fix(explorer): default sortFn implementation (#511) * fix: use `numeric` + `base` for localeCompare * docs(explorer): update default sortFn * fix: better and more responsive tag behavior * tags css moved to TagList.tsx * used npm run format * merged tag declarations --------- Co-authored-by: Ben Schlegel <31989404+benschlegel@users.noreply.github.com> --- quartz/components/TagList.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quartz/components/TagList.tsx b/quartz/components/TagList.tsx index a4dfac701..36c841843 100644 --- a/quartz/components/TagList.tsx +++ b/quartz/components/TagList.tsx @@ -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; From 2f99339dcf93ef50b766263297785a32d9c35250 Mon Sep 17 00:00:00 2001 From: Hrishikesh Barman Date: Sat, 30 Sep 2023 00:05:26 +0530 Subject: [PATCH 4/4] feat: add transformations for latex in oxhugofm (#510) ox-hugo currently supports the following syntax for latex equations: - https://orgmode.org/manual/LaTeX-fragments.html - https://ox-hugo.scripter.co/doc/equations This syntax is supported by mathjax as is mentioned in the ox-hugo documentation. But quartz uses remark-math which has some issues with the \( \) syntax. See https://github.com/remarkjs/remark-math/issues/39 This change adds few more transformations to the OxHugoFlavouredMarkdown plugin, which makes a best effort conversion of this syntax into what the Quartz Latex transformer plugin supports. With these changes, the generated files show latex formatting with default quartz configuration. Sidenote on `\_` escape by ox-hugo: ox-hugo escapes, _ using \_, we match against it after we transform equations into what quartz supports($$ and $). This could be achieved using lookaround like regex as follows ```js (?<=(\$|\$\$)[\s\S]*) -> Positive lookbehind for $ or $$ \\_ -> Matches \_ (?=[\s\S]*(?:\1)) Positive lookahead for $ or $$ if matched const escapedUnderscoreRegex = new RegExp(/(?<=(\$|\$\$)[\s\S]*)\\_(?=[\s\S]*(?:\1))/, "g") ```` But since lookahead/behind can slow things down on large files, we just look up all equations with $ and $$ delimiters and then try replacing \_ --- docs/features/OxHugo compatibility.md | 1 + quartz/plugins/transformers/oxhugofm.ts | 35 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/docs/features/OxHugo compatibility.md b/docs/features/OxHugo compatibility.md index b25167f8d..3143eb1b5 100644 --- a/docs/features/OxHugo compatibility.md +++ b/docs/features/OxHugo compatibility.md @@ -32,6 +32,7 @@ Quartz by default doesn't understand `org-roam` files as they aren't Markdown. Y - `replaceFigureWithMdImg`: Whether to replace `
` 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] > diff --git a/quartz/plugins/transformers/oxhugofm.ts b/quartz/plugins/transformers/oxhugofm.ts index 0d7b9199a..6e70bb190 100644 --- a/quartz/plugins/transformers/oxhugofm.ts +++ b/quartz/plugins/transformers/oxhugofm.ts @@ -9,6 +9,9 @@ export interface Options { removeHugoShortcode: boolean /** Replace
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 | 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 }, }