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