From d618a4e3f376028902e481b78466e8fbedd860aa Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Mon, 10 Mar 2025 06:36:10 +0000
Subject: [PATCH] fix(explorer): dont rely on data to get slug, compute it in trie

---
 quartz/components/Explorer.tsx               |    2 
 quartz/util/fileTrie.test.ts                 |   36 ++++++-----
 quartz/util/fileTrie.ts                      |  101 ++++++++++++++++-----------------
 quartz/components/scripts/explorer.inline.ts |   10 +-
 4 files changed, 76 insertions(+), 73 deletions(-)

diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx
index 9c1fbdc..9c6319a 100644
--- a/quartz/components/Explorer.tsx
+++ b/quartz/components/Explorer.tsx
@@ -23,7 +23,7 @@
 
 const defaultOptions: Options = {
   folderDefaultState: "collapsed",
-  folderClickBehavior: "collapse",
+  folderClickBehavior: "link",
   useSavedState: true,
   mapFn: (node) => {
     return node
diff --git a/quartz/components/scripts/explorer.inline.ts b/quartz/components/scripts/explorer.inline.ts
index 15f3a84..68a20bb 100644
--- a/quartz/components/scripts/explorer.inline.ts
+++ b/quartz/components/scripts/explorer.inline.ts
@@ -78,11 +78,11 @@
   const clone = template.content.cloneNode(true) as DocumentFragment
   const li = clone.querySelector("li") as HTMLLIElement
   const a = li.querySelector("a") as HTMLAnchorElement
-  a.href = resolveRelative(currentSlug, node.data?.slug!)
-  a.dataset.for = node.data?.slug
+  a.href = resolveRelative(currentSlug, node.slug)
+  a.dataset.for = node.slug
   a.textContent = node.displayName
 
-  if (currentSlug === node.data?.slug) {
+  if (currentSlug === node.slug) {
     a.classList.add("active")
   }
 
@@ -102,7 +102,7 @@
   const folderOuter = li.querySelector(".folder-outer") as HTMLElement
   const ul = folderOuter.querySelector("ul") as HTMLUListElement
 
-  const folderPath = node.data?.slug!
+  const folderPath = node.slug
   folderContainer.dataset.folderpath = folderPath
 
   if (opts.folderClickBehavior === "link") {
@@ -110,7 +110,7 @@
     const button = titleContainer.querySelector(".folder-button") as HTMLElement
     const a = document.createElement("a")
     a.href = resolveRelative(currentSlug, folderPath)
-    a.dataset.for = node.data?.slug
+    a.dataset.for = folderPath
     a.className = "folder-title"
     a.textContent = node.displayName
     button.replaceWith(a)
diff --git a/quartz/util/fileTrie.test.ts b/quartz/util/fileTrie.test.ts
index 3de3d93..e303714 100644
--- a/quartz/util/fileTrie.test.ts
+++ b/quartz/util/fileTrie.test.ts
@@ -11,16 +11,15 @@
   let trie: FileTrieNode<TestData>
 
   beforeEach(() => {
-    trie = new FileTrieNode<TestData>("")
+    trie = new FileTrieNode<TestData>([])
   })
 
   describe("constructor", () => {
     test("should create an empty trie", () => {
       assert.deepStrictEqual(trie.children, [])
-      assert.strictEqual(trie.slugSegment, "")
+      assert.strictEqual(trie.slug, "")
       assert.strictEqual(trie.displayName, "")
       assert.strictEqual(trie.data, null)
-      assert.strictEqual(trie.depth, 0)
     })
 
     test("should set displayName from data title", () => {
@@ -43,7 +42,7 @@
 
       trie.add(data)
       assert.strictEqual(trie.children.length, 1)
-      assert.strictEqual(trie.children[0].slugSegment, "test")
+      assert.strictEqual(trie.children[0].slug, "test")
       assert.strictEqual(trie.children[0].data, data)
     })
 
@@ -72,20 +71,20 @@
       trie.add(data1)
       trie.add(data2)
       assert.strictEqual(trie.children.length, 2)
-      assert.strictEqual(trie.children[0].slugSegment, "folder")
+      assert.strictEqual(trie.children[0].slug, "folder/index")
       assert.strictEqual(trie.children[0].children.length, 1)
-      assert.strictEqual(trie.children[0].children[0].slugSegment, "test")
+      assert.strictEqual(trie.children[0].children[0].slug, "folder/test")
       assert.strictEqual(trie.children[0].children[0].data, data1)
 
-      assert.strictEqual(trie.children[1].slugSegment, "a")
+      assert.strictEqual(trie.children[1].slug, "a/index")
       assert.strictEqual(trie.children[1].children.length, 1)
       assert.strictEqual(trie.children[1].data, null)
 
-      assert.strictEqual(trie.children[1].children[0].slugSegment, "b")
+      assert.strictEqual(trie.children[1].children[0].slug, "a/b/index")
       assert.strictEqual(trie.children[1].children[0].children.length, 1)
       assert.strictEqual(trie.children[1].children[0].data, null)
 
-      assert.strictEqual(trie.children[1].children[0].children[0].slugSegment, "c")
+      assert.strictEqual(trie.children[1].children[0].children[0].slug, "a/b/c/index")
       assert.strictEqual(trie.children[1].children[0].children[0].data, data2)
       assert.strictEqual(trie.children[1].children[0].children[0].children.length, 0)
     })
@@ -99,9 +98,9 @@
       trie.add(data1)
       trie.add(data2)
 
-      trie.filter((node) => node.slugSegment !== "test1")
+      trie.filter((node) => node.slug !== "test1")
       assert.strictEqual(trie.children.length, 1)
-      assert.strictEqual(trie.children[0].slugSegment, "test2")
+      assert.strictEqual(trie.children[0].slug, "test2")
     })
   })
 
@@ -115,7 +114,7 @@
 
       trie.map((node) => {
         if (node.data) {
-          node.displayName = "Modified"
+          node.data.title = "Modified"
         }
       })
 
@@ -136,7 +135,7 @@
       assert.deepStrictEqual(
         entries.map(([path, node]) => [path, node.data]),
         [
-          ["", trie.data],
+          ["index", trie.data],
           ["test1", data1],
           ["a/index", null],
           ["a/b/index", null],
@@ -166,7 +165,12 @@
       trie.add(data3)
       const paths = trie.getFolderPaths()
 
-      assert.deepStrictEqual(paths, ["folder/index", "folder/subfolder/index", "abc/index"])
+      assert.deepStrictEqual(paths, [
+        "index",
+        "folder/index",
+        "folder/subfolder/index",
+        "abc/index",
+      ])
     })
   })
 
@@ -180,9 +184,9 @@
       trie.add(data1)
       trie.add(data2)
 
-      trie.sort((a, b) => a.slugSegment.localeCompare(b.slugSegment))
+      trie.sort((a, b) => a.slug.localeCompare(b.slug))
       assert.deepStrictEqual(
-        trie.children.map((n) => n.slugSegment),
+        trie.children.map((n) => n.slug),
         ["a", "b", "c"],
       )
     })
diff --git a/quartz/util/fileTrie.ts b/quartz/util/fileTrie.ts
index ed87b4f..7195237 100644
--- a/quartz/util/fileTrie.ts
+++ b/quartz/util/fileTrie.ts
@@ -7,55 +7,64 @@
 }
 
 export class FileTrieNode<T extends FileTrieData = ContentDetails> {
-  children: Array<FileTrieNode<T>>
-  slugSegment: string
-  displayName: string
-  data: T | null
-  depth: number
   isFolder: boolean
+  children: Array<FileTrieNode<T>>
 
-  constructor(segment: string, data?: T, depth: number = 0) {
+  private slugSegments: string[]
+  data: T | null
+
+  constructor(segments: string[], data?: T) {
     this.children = []
-    this.slugSegment = segment
-    this.displayName = data?.title ?? segment
+    this.slugSegments = segments
     this.data = data ?? null
-    this.depth = depth
-    this.isFolder = segment === "index"
+    this.isFolder = false
+  }
+
+  get displayName(): string {
+    return this.data?.title ?? this.slugSegment ?? ""
+  }
+
+  get slug(): FullSlug {
+    const path = joinSegments(...this.slugSegments) as FullSlug
+    if (this.isFolder) {
+      return joinSegments(path, "index") as FullSlug
+    }
+
+    return path
+  }
+
+  get slugSegment(): string {
+    return this.slugSegments[this.slugSegments.length - 1]
+  }
+
+  private makeChild(path: string[], file?: T) {
+    const fullPath = [...this.slugSegments, path[0]]
+    const child = new FileTrieNode<T>(fullPath, file)
+    this.children.push(child)
+    return child
   }
 
   private insert(path: string[], file: T) {
-    if (path.length === 0) return
+    if (path.length === 0) {
+      throw new Error("path is empty")
+    }
 
-    const nextSegment = path[0]
-
-    // base case, insert here
+    // if we are inserting, we are a folder
+    this.isFolder = true
+    const segment = path[0]
     if (path.length === 1) {
-      if (nextSegment === "index") {
-        // index case (we are the root and we just found index.md)
+      // base case, we are at the end of the path
+      if (segment === "index") {
         this.data ??= file
-        const title = file.title
-        if (title !== "index") {
-          this.displayName = title
-        }
       } else {
-        // direct child
-        this.children.push(new FileTrieNode(nextSegment, file, this.depth + 1))
-        this.isFolder = true
+        this.makeChild(path, file)
       }
-
-      return
+    } else if (path.length > 1) {
+      // recursive case, we are not at the end of the path
+      const child =
+        this.children.find((c) => c.slugSegment === segment) ?? this.makeChild(path, undefined)
+      child.insert(path.slice(1), file)
     }
-
-    // find the right child to insert into, creating it if it doesn't exist
-    path = path.splice(1)
-    let child = this.children.find((c) => c.slugSegment === nextSegment)
-    if (!child) {
-      child = new FileTrieNode<T>(nextSegment, undefined, this.depth + 1)
-      this.children.push(child)
-      child.isFolder = true
-    }
-
-    child.insert(path, file)
   }
 
   // Add new file to trie
@@ -88,7 +97,7 @@
   }
 
   static fromEntries<T extends FileTrieData>(entries: [FullSlug, T][]) {
-    const trie = new FileTrieNode<T>("")
+    const trie = new FileTrieNode<T>([])
     entries.forEach(([, entry]) => trie.add(entry))
     return trie
   }
@@ -98,22 +107,12 @@
    * in the a flat array including the full path and the node
    */
   entries(): [FullSlug, FileTrieNode<T>][] {
-    const traverse = (
-      node: FileTrieNode<T>,
-      currentPath: string,
-    ): [FullSlug, FileTrieNode<T>][] => {
-      const segments = [currentPath, node.slugSegment]
-      const fullPath = joinSegments(...segments) as FullSlug
-
-      const indexQualifiedPath =
-        node.isFolder && node.depth > 0 ? (joinSegments(fullPath, "index") as FullSlug) : fullPath
-
-      const result: [FullSlug, FileTrieNode<T>][] = [[indexQualifiedPath, node]]
-
-      return result.concat(...node.children.map((child) => traverse(child, fullPath)))
+    const traverse = (node: FileTrieNode<T>): [FullSlug, FileTrieNode<T>][] => {
+      const result: [FullSlug, FileTrieNode<T>][] = [[node.slug, node]]
+      return result.concat(...node.children.map(traverse))
     }
 
-    return traverse(this, "")
+    return traverse(this)
   }
 
   /**

--
Gitblit v1.10.0