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 @@ export interface Options { 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 @@ function createFileNode(currentSlug: FullSlug, node: FileTrieNode): HTMLLIElemen 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 @@ function createFolderNode( 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 @@ function createFolderNode( 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 @@ describe("FileTrie", () => { let trie: FileTrieNode beforeEach(() => { - trie = new FileTrieNode("") + trie = new FileTrieNode([]) }) 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 @@ describe("FileTrie", () => { 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 @@ describe("FileTrie", () => { 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 @@ describe("FileTrie", () => { 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 @@ describe("FileTrie", () => { trie.map((node) => { if (node.data) { - node.displayName = "Modified" + node.data.title = "Modified" } }) @@ -136,7 +135,7 @@ describe("FileTrie", () => { 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 @@ describe("FileTrie", () => { 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 @@ describe("FileTrie", () => { 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 @@ interface FileTrieData { } export class FileTrieNode { - children: Array> - slugSegment: string - displayName: string - data: T | null - depth: number isFolder: boolean + children: Array> - 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(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(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 @@ export class FileTrieNode { } static fromEntries(entries: [FullSlug, T][]) { - const trie = new FileTrieNode("") + const trie = new FileTrieNode([]) entries.forEach(([, entry]) => trie.add(entry)) return trie } @@ -98,22 +107,12 @@ export class FileTrieNode { * in the a flat array including the full path and the node */ entries(): [FullSlug, FileTrieNode][] { - const traverse = ( - node: FileTrieNode, - currentPath: string, - ): [FullSlug, FileTrieNode][] => { - 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][] = [[indexQualifiedPath, node]] - - return result.concat(...node.children.map((child) => traverse(child, fullPath))) + const traverse = (node: FileTrieNode): [FullSlug, FileTrieNode][] => { + const result: [FullSlug, FileTrieNode][] = [[node.slug, node]] + return result.concat(...node.children.map(traverse)) } - return traverse(this, "") + return traverse(this) } /**