From 4e74d11b1aee8c0affa0b13ba7b174d175ca3244 Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Mon, 24 Mar 2025 00:24:43 +0000
Subject: [PATCH] fix: cleanup a href link construction, global shared trie, breadcrumbs use trie
---
quartz/build.ts | 1
quartz/components/Breadcrumbs.tsx | 90 ++++-------------
quartz/util/ctx.ts | 27 +++++
quartz/util/fileTrie.test.ts | 82 ++++++++++++++++
quartz/components/TagList.tsx | 5
quartz/components/pages/TagContent.tsx | 7 +
quartz/components/pages/FolderContent.tsx | 25 ----
quartz/util/fileTrie.ts | 18 +++
8 files changed, 159 insertions(+), 96 deletions(-)
diff --git a/quartz/build.ts b/quartz/build.ts
index 7cf4405..032063f 100644
--- a/quartz/build.ts
+++ b/quartz/build.ts
@@ -21,6 +21,7 @@
import { randomIdNonSecure } from "./util/random"
import { ChangeEvent } from "./plugins/types"
import { minimatch } from "minimatch"
+import { FileTrieNode } from "./util/fileTrie"
type ContentMap = Map<
FilePath,
diff --git a/quartz/components/Breadcrumbs.tsx b/quartz/components/Breadcrumbs.tsx
index d0faeab..5144a31 100644
--- a/quartz/components/Breadcrumbs.tsx
+++ b/quartz/components/Breadcrumbs.tsx
@@ -1,8 +1,8 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import breadcrumbsStyle from "./styles/breadcrumbs.scss"
-import { FullSlug, SimpleSlug, joinSegments, resolveRelative } from "../util/path"
-import { QuartzPluginData } from "../plugins/vfile"
+import { FullSlug, SimpleSlug, resolveRelative, simplifySlug } from "../util/path"
import { classNames } from "../util/lang"
+import { trieFromAllFiles } from "../util/ctx"
type CrumbData = {
displayName: string
@@ -23,10 +23,6 @@
*/
resolveFrontmatterTitle: boolean
/**
- * Whether to display breadcrumbs on root `index.md`
- */
- hideOnRoot: boolean
- /**
* Whether to display the current page in the breadcrumbs.
*/
showCurrentPage: boolean
@@ -36,7 +32,6 @@
spacerSymbol: "❯",
rootName: "Home",
resolveFrontmatterTitle: true,
- hideOnRoot: true,
showCurrentPage: true,
}
@@ -48,78 +43,37 @@
}
export default ((opts?: Partial<BreadcrumbOptions>) => {
- // Merge options with defaults
const options: BreadcrumbOptions = { ...defaultOptions, ...opts }
-
- // computed index of folder name to its associated file data
- let folderIndex: Map<string, QuartzPluginData> | undefined
-
const Breadcrumbs: QuartzComponent = ({
fileData,
allFiles,
displayClass,
+ ctx,
}: QuartzComponentProps) => {
- // Hide crumbs on root if enabled
- if (options.hideOnRoot && fileData.slug === "index") {
- return <></>
+ const trie = (ctx.trie ??= trieFromAllFiles(allFiles))
+ const slugParts = fileData.slug!.split("/")
+ const pathNodes = trie.ancestryChain(slugParts)
+
+ if (!pathNodes) {
+ return null
}
- // Format entry for root element
- 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) {
- const folderParts = file.slug?.split("/")
- if (folderParts?.at(-1) === "index") {
- folderIndex.set(folderParts.slice(0, -1).join("/"), file)
- }
- }
- }
-
- // Split slug into hierarchy/parts
- const slugParts = fileData.slug?.split("/")
- if (slugParts) {
- // is tag breadcrumb?
- const isTagPath = slugParts[0] === "tags"
-
- // full path until current part
- let currentPath = ""
-
- for (let i = 0; i < slugParts.length - 1; i++) {
- let curPathSegment = slugParts[i]
-
- // Try to resolve frontmatter folder title
- const currentFile = folderIndex?.get(slugParts.slice(0, i + 1).join("/"))
- if (currentFile) {
- const title = currentFile.frontmatter!.title
- if (title !== "index") {
- curPathSegment = title
- }
- }
-
- // Add current slug to full path
- currentPath = joinSegments(currentPath, slugParts[i])
- const includeTrailingSlash = !isTagPath || i < slugParts.length - 1
-
- // Format and add current crumb
- const crumb = formatCrumb(
- curPathSegment,
- fileData.slug!,
- (currentPath + (includeTrailingSlash ? "/" : "")) as SimpleSlug,
- )
- crumbs.push(crumb)
+ const crumbs: CrumbData[] = pathNodes.map((node, idx) => {
+ const crumb = formatCrumb(node.displayName, fileData.slug!, simplifySlug(node.slug))
+ if (idx === 0) {
+ crumb.displayName = options.rootName
}
- // Add current file to crumb (can directly use frontmatter title)
- if (options.showCurrentPage && slugParts.at(-1) !== "index") {
- crumbs.push({
- displayName: fileData.frontmatter!.title,
- path: "",
- })
+ // For last node (current page), set empty path
+ if (idx === pathNodes.length - 1) {
+ crumb.path = ""
}
+
+ return crumb
+ })
+
+ if (!options.showCurrentPage) {
+ crumbs.pop()
}
return (
diff --git a/quartz/components/TagList.tsx b/quartz/components/TagList.tsx
index 4a89fbd..c73ed39 100644
--- a/quartz/components/TagList.tsx
+++ b/quartz/components/TagList.tsx
@@ -1,15 +1,14 @@
-import { pathToRoot, slugTag } from "../util/path"
+import { FullSlug, resolveRelative } from "../util/path"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { classNames } from "../util/lang"
const TagList: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => {
const tags = fileData.frontmatter?.tags
- const baseDir = pathToRoot(fileData.slug!)
if (tags && tags.length > 0) {
return (
<ul class={classNames(displayClass, "tags")}>
{tags.map((tag) => {
- const linkDest = baseDir + `/tags/${slugTag(tag)}`
+ const linkDest = resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug)
return (
<li>
<a href={linkDest} class="internal tag-link">
diff --git a/quartz/components/pages/FolderContent.tsx b/quartz/components/pages/FolderContent.tsx
index 9621f4f..afd4f5d 100644
--- a/quartz/components/pages/FolderContent.tsx
+++ b/quartz/components/pages/FolderContent.tsx
@@ -8,7 +8,8 @@
import { QuartzPluginData } from "../../plugins/vfile"
import { ComponentChildren } from "preact"
import { concatenateResources } from "../../util/resources"
-import { FileTrieNode } from "../../util/fileTrie"
+import { trieFromAllFiles } from "../../util/ctx"
+
interface FolderContentOptions {
/**
* Whether to display number of folders
@@ -25,31 +26,11 @@
export default ((opts?: Partial<FolderContentOptions>) => {
const options: FolderContentOptions = { ...defaultOptions, ...opts }
- let trie: FileTrieNode<
- QuartzPluginData & {
- slug: string
- title: string
- filePath: string
- }
- >
const FolderContent: QuartzComponent = (props: QuartzComponentProps) => {
const { tree, fileData, allFiles, cfg } = props
- if (!trie) {
- trie = new FileTrieNode([])
- allFiles.forEach((file) => {
- if (file.frontmatter) {
- trie.add({
- ...file,
- slug: file.slug!,
- title: file.frontmatter.title,
- filePath: file.filePath!,
- })
- }
- })
- }
-
+ const trie = (props.ctx.trie ??= trieFromAllFiles(allFiles))
const folder = trie.findNode(fileData.slug!.split("/"))
if (!folder) {
return null
diff --git a/quartz/components/pages/TagContent.tsx b/quartz/components/pages/TagContent.tsx
index 03051d3..5e81901 100644
--- a/quartz/components/pages/TagContent.tsx
+++ b/quartz/components/pages/TagContent.tsx
@@ -1,7 +1,7 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
import style from "../styles/listPage.scss"
import { PageList, SortFn } from "../PageList"
-import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path"
+import { FullSlug, getAllSegmentPrefixes, resolveRelative, simplifySlug } from "../../util/path"
import { QuartzPluginData } from "../../plugins/vfile"
import { Root } from "hast"
import { htmlToJsx } from "../../util/jsx"
@@ -74,10 +74,13 @@
? contentPage?.description
: htmlToJsx(contentPage.filePath!, root)
+ const tagListingPage = `/tags/${tag}` as FullSlug
+ const href = resolveRelative(fileData.slug!, tagListingPage)
+
return (
<div>
<h2>
- <a class="internal tag-link" href={`../tags/${tag}`}>
+ <a class="internal tag-link" href={href}>
{tag}
</a>
</h2>
diff --git a/quartz/util/ctx.ts b/quartz/util/ctx.ts
index b3e7a37..80115ec 100644
--- a/quartz/util/ctx.ts
+++ b/quartz/util/ctx.ts
@@ -1,4 +1,6 @@
import { QuartzConfig } from "../cfg"
+import { QuartzPluginData } from "../plugins/vfile"
+import { FileTrieNode } from "./fileTrie"
import { FilePath, FullSlug } from "./path"
export interface Argv {
@@ -13,13 +15,36 @@
concurrency?: number
}
+export type BuildTimeTrieData = QuartzPluginData & {
+ slug: string
+ title: string
+ filePath: string
+}
+
export interface BuildCtx {
buildId: string
argv: Argv
cfg: QuartzConfig
allSlugs: FullSlug[]
allFiles: FilePath[]
+ trie?: FileTrieNode<BuildTimeTrieData>
incremental: boolean
}
-export type WorkerSerializableBuildCtx = Omit<BuildCtx, "cfg">
+export function trieFromAllFiles(allFiles: QuartzPluginData[]): FileTrieNode<BuildTimeTrieData> {
+ const trie = new FileTrieNode<BuildTimeTrieData>([])
+ allFiles.forEach((file) => {
+ if (file.frontmatter) {
+ trie.add({
+ ...file,
+ slug: file.slug!,
+ title: file.frontmatter.title,
+ filePath: file.filePath!,
+ })
+ }
+ })
+
+ return trie
+}
+
+export type WorkerSerializableBuildCtx = Omit<BuildCtx, "cfg" | "trie">
diff --git a/quartz/util/fileTrie.test.ts b/quartz/util/fileTrie.test.ts
index d66e142..c747f34 100644
--- a/quartz/util/fileTrie.test.ts
+++ b/quartz/util/fileTrie.test.ts
@@ -330,4 +330,86 @@
)
})
})
+
+ describe("pathToNode", () => {
+ test("should return root node for empty path", () => {
+ const data = { title: "Root", slug: "index", filePath: "index.md" }
+ trie.add(data)
+ const path = trie.ancestryChain([])
+ assert.deepStrictEqual(path, [trie])
+ })
+
+ test("should return root node for index path", () => {
+ const data = { title: "Root", slug: "index", filePath: "index.md" }
+ trie.add(data)
+ const path = trie.ancestryChain(["index"])
+ assert.deepStrictEqual(path, [trie])
+ })
+
+ test("should return path to first level node", () => {
+ const data = { title: "Test", slug: "test", filePath: "test.md" }
+ trie.add(data)
+ const path = trie.ancestryChain(["test"])
+ assert.deepStrictEqual(path, [trie, trie.children[0]])
+ })
+
+ test("should return path to nested node", () => {
+ const data = {
+ title: "Nested",
+ slug: "folder/subfolder/test",
+ filePath: "folder/subfolder/test.md",
+ }
+ trie.add(data)
+ const path = trie.ancestryChain(["folder", "subfolder", "test"])
+ assert.deepStrictEqual(path, [
+ trie,
+ trie.children[0],
+ trie.children[0].children[0],
+ trie.children[0].children[0].children[0],
+ ])
+ })
+
+ test("should return undefined for non-existent path", () => {
+ const data = { title: "Test", slug: "test", filePath: "test.md" }
+ trie.add(data)
+ const path = trie.ancestryChain(["nonexistent"])
+ assert.strictEqual(path, undefined)
+ })
+
+ test("should return file data for intermediate folders", () => {
+ const data1 = {
+ title: "Root",
+ slug: "index",
+ filePath: "index.md",
+ }
+ const data2 = {
+ title: "Test",
+ slug: "folder/subfolder/test",
+ filePath: "folder/subfolder/test.md",
+ }
+ const data3 = {
+ title: "Folder Index",
+ slug: "folder/index",
+ filePath: "folder/index.md",
+ }
+
+ trie.add(data1)
+ trie.add(data2)
+ trie.add(data3)
+ const path = trie.ancestryChain(["folder", "subfolder"])
+ assert.deepStrictEqual(path, [trie, trie.children[0], trie.children[0].children[0]])
+ assert.strictEqual(path[1].data, data3)
+ })
+
+ test("should return path for partial path", () => {
+ const data = {
+ title: "Nested",
+ slug: "folder/subfolder/test",
+ filePath: "folder/subfolder/test.md",
+ }
+ trie.add(data)
+ const path = trie.ancestryChain(["folder"])
+ assert.deepStrictEqual(path, [trie, trie.children[0]])
+ })
+ })
})
diff --git a/quartz/util/fileTrie.ts b/quartz/util/fileTrie.ts
index e3dc2e7..9e6706f 100644
--- a/quartz/util/fileTrie.ts
+++ b/quartz/util/fileTrie.ts
@@ -97,6 +97,24 @@
return this.children.find((c) => c.slugSegment === path[0])?.findNode(path.slice(1))
}
+ ancestryChain(path: string[]): Array<FileTrieNode<T>> | undefined {
+ if (path.length === 0 || (path.length === 1 && path[0] === "index")) {
+ return [this]
+ }
+
+ const child = this.children.find((c) => c.slugSegment === path[0])
+ if (!child) {
+ return undefined
+ }
+
+ const childPath = child.ancestryChain(path.slice(1))
+ if (!childPath) {
+ return undefined
+ }
+
+ return [this, ...childPath]
+ }
+
/**
* Filter trie nodes. Behaves similar to `Array.prototype.filter()`, but modifies tree in place
*/
--
Gitblit v1.10.0