From a897cc1f531844a2e4da36f8712b7aedec0c5824 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Fri, 22 Sep 2023 09:43:34 -0700 Subject: [PATCH 01/12] feat: add warning for missing home page --- quartz/plugins/emitters/contentPage.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/quartz/plugins/emitters/contentPage.tsx b/quartz/plugins/emitters/contentPage.tsx index 4542446b0..338bfae44 100644 --- a/quartz/plugins/emitters/contentPage.tsx +++ b/quartz/plugins/emitters/contentPage.tsx @@ -7,6 +7,7 @@ import { FullPageLayout } from "../../cfg" import { FilePath, pathToRoot } from "../../util/path" import { defaultContentPageLayout, sharedPageComponents } from "../../../quartz.layout" import { Content } from "../../components" +import chalk from "chalk" export const ContentPage: QuartzEmitterPlugin> = (userOpts) => { const opts: FullPageLayout = { @@ -29,8 +30,14 @@ export const ContentPage: QuartzEmitterPlugin> = (userOp const cfg = ctx.cfg.configuration const fps: FilePath[] = [] const allFiles = content.map((c) => c[1].data) + + let containsIndex = false for (const [tree, file] of content) { const slug = file.data.slug! + if (slug === "index") { + containsIndex = true + } + const externalResources = pageResources(pathToRoot(slug), resources) const componentData: QuartzComponentProps = { fileData: file.data, @@ -50,6 +57,15 @@ export const ContentPage: QuartzEmitterPlugin> = (userOp fps.push(fp) } + + if (!containsIndex) { + console.log( + chalk.yellow( + `\nWarning: you seem to be missing an \`index.md\` home page file at the root of your \`${ctx.argv.directory}\` folder. This may cause errors when deploying.`, + ), + ) + } + return fps }, } From 13c867322629f5ee59c54b95679585b370e1442b Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Fri, 22 Sep 2023 10:04:37 -0700 Subject: [PATCH 02/12] feat: add warning for invalid date format --- quartz/plugins/transformers/lastmod.ts | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/quartz/plugins/transformers/lastmod.ts b/quartz/plugins/transformers/lastmod.ts index 015c350a5..7deeefb0d 100644 --- a/quartz/plugins/transformers/lastmod.ts +++ b/quartz/plugins/transformers/lastmod.ts @@ -2,6 +2,7 @@ import fs from "fs" import path from "path" import { Repository } from "@napi-rs/simple-git" import { QuartzTransformerPlugin } from "../types" +import chalk from "chalk" export interface Options { priority: ("frontmatter" | "git" | "filesystem")[] @@ -11,9 +12,18 @@ const defaultOptions: Options = { priority: ["frontmatter", "git", "filesystem"], } -function coerceDate(d: any): Date { +function coerceDate(fp: string, d: any): Date { const dt = new Date(d) - return isNaN(dt.getTime()) ? new Date() : dt + const invalidDate = isNaN(dt.getTime()) + if (invalidDate && d !== undefined) { + console.log( + chalk.yellow( + `\nWarning: found invalid date "${d}" in \`${fp}\`. Supported formats: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format`, + ), + ) + } + + return invalidDate ? new Date() : dt } type MaybeDate = undefined | string | number @@ -32,10 +42,11 @@ export const CreatedModifiedDate: QuartzTransformerPlugin | und let modified: MaybeDate = undefined let published: MaybeDate = undefined - const fp = path.posix.join(file.cwd, file.data.filePath as string) + const fp = file.data.filePath! + const fullFp = path.posix.join(file.cwd, fp) for (const source of opts.priority) { if (source === "filesystem") { - const st = await fs.promises.stat(fp) + const st = await fs.promises.stat(fullFp) created ||= st.birthtimeMs modified ||= st.mtimeMs } else if (source === "frontmatter" && file.data.frontmatter) { @@ -54,9 +65,9 @@ export const CreatedModifiedDate: QuartzTransformerPlugin | und } file.data.dates = { - created: coerceDate(created), - modified: coerceDate(modified), - published: coerceDate(published), + created: coerceDate(fp, created), + modified: coerceDate(fp, modified), + published: coerceDate(fp, published), } } }, From c5b9137f12ea372d9196e41ffffc91eee7ad772e Mon Sep 17 00:00:00 2001 From: Vince Imbat <96913392+vinceimbat@users.noreply.github.com> Date: Sat, 23 Sep 2023 10:39:02 +0800 Subject: [PATCH 03/12] docs: Adds Vince Imbat to showcase (#501) --- docs/showcase.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/showcase.md b/docs/showcase.md index 9d84a0633..f3f14e207 100644 --- a/docs/showcase.md +++ b/docs/showcase.md @@ -17,5 +17,6 @@ Want to see what Quartz can do? Here are some cool community gardens: - [Mike's AI Garden 🤖🪴](https://mwalton.me/) - [Matt Dunn's Second Brain](https://mattdunn.info/) - [Pelayo Arbues' Notes](https://pelayoarbues.github.io/) +- [Vince Imbat's Talahardin](https://vinceimbat.com/) If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)! From 95eec5b49db53801e23b4b47778cb0b6153db83f Mon Sep 17 00:00:00 2001 From: Chad Lee Date: Sun, 24 Sep 2023 12:27:42 -0500 Subject: [PATCH 04/12] add site to showcase (#504) --- docs/showcase.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/showcase.md b/docs/showcase.md index f3f14e207..7dbc6e99b 100644 --- a/docs/showcase.md +++ b/docs/showcase.md @@ -18,5 +18,6 @@ Want to see what Quartz can do? Here are some cool community gardens: - [Matt Dunn's Second Brain](https://mattdunn.info/) - [Pelayo Arbues' Notes](https://pelayoarbues.github.io/) - [Vince Imbat's Talahardin](https://vinceimbat.com/) +- [🧠🌳 Chad's Mind Garden](https://www.chadly.net/) If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)! From ea5742c328c97a20f7add4994aa7e443fc6f7f39 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sun, 24 Sep 2023 10:31:47 -0700 Subject: [PATCH 05/12] fix: mermaid copy source position --- quartz/styles/base.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index 9d553622d..4aff2419a 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -328,6 +328,7 @@ pre { &:has(> code.mermaid) { border: none; + position: relative; } & > code { From 697bffdb8b1bef143823f77a118de90286fe325a Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sun, 24 Sep 2023 14:47:30 -0700 Subject: [PATCH 06/12] fix: treat the 0 time as invalid too --- quartz/plugins/transformers/lastmod.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/plugins/transformers/lastmod.ts b/quartz/plugins/transformers/lastmod.ts index 7deeefb0d..feca4b52f 100644 --- a/quartz/plugins/transformers/lastmod.ts +++ b/quartz/plugins/transformers/lastmod.ts @@ -14,7 +14,7 @@ const defaultOptions: Options = { function coerceDate(fp: string, d: any): Date { const dt = new Date(d) - const invalidDate = isNaN(dt.getTime()) + const invalidDate = isNaN(dt.getTime()) || dt.getTime() === 0 if (invalidDate && d !== undefined) { console.log( chalk.yellow( From d22c3c107a9c6422ef251bd1076ddd9c4fe47a42 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Mon, 25 Sep 2023 18:15:55 -0700 Subject: [PATCH 07/12] fix: coerce title to string --- quartz/plugins/transformers/frontmatter.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/quartz/plugins/transformers/frontmatter.ts b/quartz/plugins/transformers/frontmatter.ts index 571aa04d0..04b1105c3 100644 --- a/quartz/plugins/transformers/frontmatter.ts +++ b/quartz/plugins/transformers/frontmatter.ts @@ -37,6 +37,11 @@ export const FrontMatter: QuartzTransformerPlugin | undefined> data.tags = data.tag } + // coerce title to string + if (data.title) { + data.title = data.title.toString() + } + if (data.tags && !Array.isArray(data.tags)) { data.tags = data.tags .toString() From d4c122646ccd6fc989b4436e16b2dffdc931dee6 Mon Sep 17 00:00:00 2001 From: Ben Schlegel <31989404+benschlegel@users.noreply.github.com> Date: Thu, 28 Sep 2023 17:39:44 +0200 Subject: [PATCH 08/12] fix(explorer): default sortFn implementation (#511) * fix: use `numeric` + `base` for localeCompare * docs(explorer): update default sortFn --- docs/features/explorer.md | 7 ++++++- quartz/components/Explorer.tsx | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/features/explorer.md b/docs/features/explorer.md index 91766a999..8937b25c0 100644 --- a/docs/features/explorer.md +++ b/docs/features/explorer.md @@ -75,7 +75,12 @@ Every function you can pass is optional. By default, only a `sort` function will Component.Explorer({ sortFn: (a, b) => { if ((!a.file && !b.file) || (a.file && b.file)) { - return a.displayName.localeCompare(b.displayName) + // sensitivity: "base": Only strings that differ in base letters compare as unequal. Examples: a ≠ b, a = á, a = A + // numeric: true: Whether numeric collation should be used, such that "1" < "2" < "10" + return a.displayName.localeCompare(b.displayName, undefined, { + numeric: true, + sensitivity: "base", + }) } if (a.file && !b.file) { return 1 diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx index de6b5e0ae..79d878127 100644 --- a/quartz/components/Explorer.tsx +++ b/quartz/components/Explorer.tsx @@ -15,7 +15,12 @@ const defaultOptions = { sortFn: (a, b) => { // Sort order: folders first, then files. Sort folders and files alphabetically if ((!a.file && !b.file) || (a.file && b.file)) { - return a.displayName.localeCompare(b.displayName) + // numeric: true: Whether numeric collation should be used, such that "1" < "2" < "10" + // sensitivity: "base": Only strings that differ in base letters compare as unequal. Examples: a ≠ b, a = á, a = A + return a.displayName.localeCompare(b.displayName, undefined, { + numeric: true, + sensitivity: "base", + }) } if (a.file && !b.file) { return 1 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 09/12] 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 10/12] 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 11/12] 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 12/12] 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 }, }