diff --git a/docs/features/breadcrumbs.md b/docs/features/breadcrumbs.md index 94db66ac0..20c3b8d65 100644 --- a/docs/features/breadcrumbs.md +++ b/docs/features/breadcrumbs.md @@ -16,10 +16,10 @@ For example, here's what the default configuration looks like: ```typescript title="quartz.layout.ts" Component.Breadcrumbs({ - spacerSymbol: ">", // symbol between crumbs + 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 + resolveFrontmatterTitle: true, // whether to resolve folder names through frontmatter titles + hideOnRoot: true, // whether to hide breadcrumbs on root `index.md` page }) ``` diff --git a/docs/features/callouts.md b/docs/features/callouts.md index 63051ad9d..27de687eb 100644 --- a/docs/features/callouts.md +++ b/docs/features/callouts.md @@ -33,7 +33,7 @@ See [documentation on supported types and syntax here](https://help.obsidian.md > [!question]+ Can callouts be nested? > -> > [!todo]- Yes!, they can. +> > [!todo]- Yes!, they can. And collapsed! > > > > > [!example] You can even use multiple layers of nesting. diff --git a/docs/features/table of contents.md b/docs/features/table of contents.md index f169b22d2..0298ffaab 100644 --- a/docs/features/table of contents.md +++ b/docs/features/table of contents.md @@ -8,7 +8,7 @@ tags: Quartz can automatically generate a table of contents from a list of headings on each page. It will also show you your current scroll position on the site by marking headings you've scrolled through with a different colour. By default, it will show all headers from H1 (`# Title`) all the way to H3 (`### Title`) and will only show the table of contents if there is more than 1 header on the page. -You can also hide the table of contents on a page by adding `showToc: false` to the frontmatter for that page. +You can also hide the table of contents on a page by adding `enableToc: false` to the frontmatter for that page. > [!info] > This feature requires both `Plugin.TableOfContents` in your `quartz.config.ts` and `Component.TableOfContents` in your `quartz.layout.ts` to function correctly. diff --git a/quartz/cli/args.js b/quartz/cli/args.js index 3543e2e89..7ed5b078e 100644 --- a/quartz/cli/args.js +++ b/quartz/cli/args.js @@ -41,6 +41,11 @@ export const SyncArgv = { default: true, describe: "create a git commit for your unsaved changes", }, + message: { + string: true, + alias: ["m"], + describe: "option to override the default Quartz commit message", + }, push: { boolean: true, default: true, diff --git a/quartz/cli/handlers.js b/quartz/cli/handlers.js index 48a44ec9f..96ee9bc88 100644 --- a/quartz/cli/handlers.js +++ b/quartz/cli/handlers.js @@ -483,8 +483,9 @@ export async function handleSync(argv) { dateStyle: "medium", timeStyle: "short", }) + const commitMessage = argv.message ?? `Quartz sync: ${currentTimestamp}` spawnSync("git", ["add", "."], { stdio: "inherit" }) - spawnSync("git", ["commit", "-m", `Quartz sync: ${currentTimestamp}`], { stdio: "inherit" }) + spawnSync("git", ["commit", "-m", commitMessage], { stdio: "inherit" }) if (contentStat.isSymbolicLink()) { // put symlink back diff --git a/quartz/components/Breadcrumbs.tsx b/quartz/components/Breadcrumbs.tsx index 467b5a503..29c73a81b 100644 --- a/quartz/components/Breadcrumbs.tsx +++ b/quartz/components/Breadcrumbs.tsx @@ -28,9 +28,9 @@ interface BreadcrumbOptions { } const defaultOptions: BreadcrumbOptions = { - spacerSymbol: ">", + spacerSymbol: "❯", rootName: "Home", - resolveFrontmatterTitle: false, + resolveFrontmatterTitle: true, hideOnRoot: true, } @@ -41,25 +41,13 @@ function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: Simpl } } -// 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 } + // computed index of folder name to its associated file data + let folderIndex: Map | undefined + function Breadcrumbs({ fileData, allFiles, displayClass }: QuartzComponentProps) { // Hide crumbs on root if enabled if (options.hideOnRoot && fileData.slug === "index") { @@ -70,28 +58,39 @@ export default ((opts?: Partial) => { 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) { + if (file.slug?.endsWith("index")) { + const folderParts = file.filePath?.split("/") + if (folderParts) { + const folderName = folderParts[folderParts?.length - 2] + folderIndex.set(folderName, file) + } + } + } + } + // Split slug into hierarchy/parts const slugParts = fileData.slug?.split("/") if (slugParts) { // full path until current part let currentPath = "" for (let i = 0; i < slugParts.length - 1; i++) { - let currentTitle = slugParts[i] + let curPathSegment = slugParts[i] - // TODO: performance optimizations/memoizing // Try to resolve frontmatter folder title - if (options?.resolveFrontmatterTitle) { - // try to find file for current path - const currentFile = findCurrentFile(allFiles, currentTitle) - if (currentFile) { - currentTitle = currentFile.frontmatter!.title - } + const currentFile = folderIndex?.get(curPathSegment) + if (currentFile) { + curPathSegment = currentFile.frontmatter!.title } + // Add current slug to full path currentPath += slugParts[i] + "/" // Format and add current crumb - const crumb = formatCrumb(currentTitle, fileData.slug!, currentPath as SimpleSlug) + const crumb = formatCrumb(curPathSegment, fileData.slug!, currentPath as SimpleSlug) crumbs.push(crumb) } diff --git a/quartz/components/pages/TagContent.tsx b/quartz/components/pages/TagContent.tsx index 205ba8958..4a068c6bc 100644 --- a/quartz/components/pages/TagContent.tsx +++ b/quartz/components/pages/TagContent.tsx @@ -53,7 +53,7 @@ function TagContent(props: QuartzComponentProps) { return (

- + #{tag}

diff --git a/quartz/components/scripts/popover.inline.ts b/quartz/components/scripts/popover.inline.ts index 4403719fa..b78a5e6e4 100644 --- a/quartz/components/scripts/popover.inline.ts +++ b/quartz/components/scripts/popover.inline.ts @@ -28,8 +28,11 @@ async function mouseEnterHandler( }) } + const hasAlreadyBeenFetched = () => + [...link.children].some((child) => child.classList.contains("popover")) + // dont refetch if there's already a popover - if ([...link.children].some((child) => child.classList.contains("popover"))) { + if (hasAlreadyBeenFetched()) { return setPosition(link.lastChild as HTMLElement) } @@ -49,6 +52,11 @@ async function mouseEnterHandler( console.error(err) }) + // bailout if another popover exists + if (hasAlreadyBeenFetched()) { + return + } + if (!contents) return const html = p.parseFromString(contents, "text/html") normalizeRelativeURLs(html, targetUrl) diff --git a/quartz/components/scripts/spa.inline.ts b/quartz/components/scripts/spa.inline.ts index d76e624b5..4d2515032 100644 --- a/quartz/components/scripts/spa.inline.ts +++ b/quartz/components/scripts/spa.inline.ts @@ -1,5 +1,6 @@ import micromorph from "micromorph" import { FullSlug, RelativeURL, getFullSlug } from "../../util/path" +import { normalizeRelativeURLs } from "./popover.inline" // adapted from `micromorph` // https://github.com/natemoo-re/micromorph @@ -18,6 +19,12 @@ const isLocalUrl = (href: string) => { return false } +const isSamePage = (url: URL): boolean => { + const sameOrigin = url.origin === window.location.origin + const samePath = url.pathname === window.location.pathname + return sameOrigin && samePath +} + const getOpts = ({ target }: Event): { url: URL; scroll?: boolean } | undefined => { if (!isElement(target)) return if (target.attributes.getNamedItem("target")?.value === "_blank") return @@ -46,6 +53,8 @@ async function navigate(url: URL, isBack: boolean = false) { if (!contents) return const html = p.parseFromString(contents, "text/html") + normalizeRelativeURLs(html, url) + let title = html.querySelector("title")?.textContent if (title) { document.title = title @@ -93,8 +102,16 @@ function createRouter() { if (typeof window !== "undefined") { window.addEventListener("click", async (event) => { const { url } = getOpts(event) ?? {} + // dont hijack behaviour, just let browser act normally if (!url || event.ctrlKey || event.metaKey) return event.preventDefault() + + if (isSamePage(url) && url.hash) { + const el = document.getElementById(decodeURIComponent(url.hash.substring(1))) + el?.scrollIntoView() + return + } + try { navigate(url, false) } catch (e) { @@ -140,6 +157,7 @@ if (!customElements.get("route-announcer")) { style: "position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px", } + customElements.define( "route-announcer", class RouteAnnouncer extends HTMLElement { diff --git a/quartz/plugins/emitters/aliases.ts b/quartz/plugins/emitters/aliases.ts index cc61f788d..00c0c4541 100644 --- a/quartz/plugins/emitters/aliases.ts +++ b/quartz/plugins/emitters/aliases.ts @@ -1,4 +1,4 @@ -import { FilePath, FullSlug, resolveRelative, simplifySlug } from "../../util/path" +import { FilePath, FullSlug, joinSegments, resolveRelative, simplifySlug } from "../../util/path" import { QuartzEmitterPlugin } from "../types" import path from "path" @@ -25,7 +25,12 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({ slugs.push(permalink as FullSlug) } - for (const slug of slugs) { + for (let slug of slugs) { + // fix any slugs that have trailing slash + if (slug.endsWith("/")) { + slug = joinSegments(slug, "index") as FullSlug + } + const redirUrl = resolveRelative(slug, file.data.slug!) const fp = await emit({ content: ` diff --git a/quartz/plugins/emitters/contentIndex.ts b/quartz/plugins/emitters/contentIndex.ts index 69d0d376f..c5170c64a 100644 --- a/quartz/plugins/emitters/contentIndex.ts +++ b/quartz/plugins/emitters/contentIndex.ts @@ -59,6 +59,17 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: nu ` const items = Array.from(idx) + .sort(([_, f1], [__, f2]) => { + if (f1.date && f2.date) { + return f2.date.getTime() - f1.date.getTime() + } else if (f1.date && !f2.date) { + return -1 + } else if (!f1.date && f2.date) { + return 1 + } + + return f1.title.localeCompare(f2.title) + }) .map(([slug, content]) => createURLEntry(simplifySlug(slug), content)) .slice(0, limit ?? idx.size) .join("") diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index 4c60c70aa..df7183d6b 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -65,7 +65,7 @@ a { color: var(--tertiary) !important; } - &.internal { + &.internal:not(:has(> img)) { text-decoration: none; background-color: var(--highlight); padding: 0 0.1rem; @@ -392,23 +392,33 @@ p { line-height: 1.6rem; } -table { - margin: 1rem; - padding: 1.5rem; - border-collapse: collapse; - & > * { - line-height: 2rem; +.table-container { + overflow-x: auto; + + & > table { + margin: 1rem; + padding: 1.5rem; + border-collapse: collapse; + + th, + td { + min-width: 75px; + } + + & > * { + line-height: 2rem; + } } } th { text-align: left; - padding: 0.4rem 1rem; + padding: 0.4rem 0.7rem; border-bottom: 2px solid var(--gray); } td { - padding: 0.2rem 1rem; + padding: 0.2rem 0.7rem; } tr { diff --git a/quartz/util/jsx.ts b/quartz/util/jsx.ts deleted file mode 100644 index 8cba485ab..000000000 --- a/quartz/util/jsx.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { toJsxRuntime } from "hast-util-to-jsx-runtime" -import { QuartzPluginData } from "../plugins/vfile" -import { Node, Root } from "hast" -import { Fragment, jsx, jsxs } from "preact/jsx-runtime" -import { trace } from "./trace" -import { type FilePath } from "./path" - -export function htmlToJsx(fp: FilePath, tree: Node) { - try { - // @ts-ignore (preact makes it angry) - return toJsxRuntime(tree as Root, { Fragment, jsx, jsxs, elementAttributeNameCase: "html" }) - } catch (e) { - trace(`Failed to parse Markdown in \`${fp}\` into JSX`, e as Error) - } -} diff --git a/quartz/util/jsx.tsx b/quartz/util/jsx.tsx new file mode 100644 index 000000000..fc988a034 --- /dev/null +++ b/quartz/util/jsx.tsx @@ -0,0 +1,28 @@ +import { Components, Jsx, toJsxRuntime } from "hast-util-to-jsx-runtime" +import { QuartzPluginData } from "../plugins/vfile" +import { Node, Root } from "hast" +import { Fragment, jsx, jsxs } from "preact/jsx-runtime" +import { trace } from "./trace" +import { type FilePath } from "./path" + +const customComponents: Components = { + table: (props) => ( +
+ + + ), +} + +export function htmlToJsx(fp: FilePath, tree: Node) { + try { + return toJsxRuntime(tree as Root, { + Fragment, + jsx: jsx as Jsx, + jsxs: jsxs as Jsx, + elementAttributeNameCase: "html", + components: customComponents, + }) + } catch (e) { + trace(`Failed to parse Markdown in \`${fp}\` into JSX`, e as Error) + } +}