Jacky Zhao
2025-03-24 4e74d11b1aee8c0affa0b13ba7b174d175ca3244
fix: cleanup a href link construction, global shared trie, breadcrumbs use trie
8 files modified
251 ■■■■■ changed files
quartz/build.ts 1 ●●●● patch | view | raw | blame | history
quartz/components/Breadcrumbs.tsx 86 ●●●● patch | view | raw | blame | history
quartz/components/TagList.tsx 5 ●●●●● patch | view | raw | blame | history
quartz/components/pages/FolderContent.tsx 25 ●●●● patch | view | raw | blame | history
quartz/components/pages/TagContent.tsx 7 ●●●● patch | view | raw | blame | history
quartz/util/ctx.ts 27 ●●●●● patch | view | raw | blame | history
quartz/util/fileTrie.test.ts 82 ●●●●● patch | view | raw | blame | history
quartz/util/fileTrie.ts 18 ●●●●● patch | view | raw | blame | history
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,
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)
        }
      }
    const crumbs: CrumbData[] = pathNodes.map((node, idx) => {
      const crumb = formatCrumb(node.displayName, fileData.slug!, simplifySlug(node.slug))
      if (idx === 0) {
        crumb.displayName = options.rootName
    }
    // 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
          }
      // For last node (current page), set empty path
      if (idx === pathNodes.length - 1) {
        crumb.path = ""
        }
        // 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)
      }
      // Add current file to crumb (can directly use frontmatter title)
      if (options.showCurrentPage && slugParts.at(-1) !== "index") {
        crumbs.push({
          displayName: fileData.frontmatter!.title,
          path: "",
      return crumb
        })
      }
    if (!options.showCurrentPage) {
      crumbs.pop()
    }
    return (
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">
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
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>
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">
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]])
    })
  })
})
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
   */