From c538c151c7462ad0395ff2c15c5e11e89e362aa8 Mon Sep 17 00:00:00 2001
From: Striven <sg.striven@cutecat.club>
Date: Sat, 04 Apr 2026 19:47:16 +0000
Subject: [PATCH] Initial commit
---
quartz/util/fileTrie.ts | 142 ++++++++++++++++++++++++++++++----------------
1 files changed, 92 insertions(+), 50 deletions(-)
diff --git a/quartz/util/fileTrie.ts b/quartz/util/fileTrie.ts
index ed87b4f..9e6706f 100644
--- a/quartz/util/fileTrie.ts
+++ b/quartz/util/fileTrie.ts
@@ -4,58 +4,84 @@
interface FileTrieData {
slug: string
title: string
+ filePath: string
}
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[]
+ // prefer showing the file path segment over the slug segment
+ // so that folders that dont have index files can be shown as is
+ // without dashes in the slug
+ private fileSegmentHint?: string
+ private displayNameOverride?: 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
+ this.displayNameOverride = undefined
+ }
+
+ get displayName(): string {
+ const nonIndexTitle = this.data?.title === "index" ? undefined : this.data?.title
+ return (
+ this.displayNameOverride ?? nonIndexTitle ?? this.fileSegmentHint ?? this.slugSegment ?? ""
+ )
+ }
+
+ set displayName(name: string) {
+ this.displayNameOverride = name
+ }
+
+ 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)
}
+ } 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)
- return
+ const fileParts = file.filePath.split("/")
+ child.fileSegmentHint = fileParts.at(-path.length)
+ 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
@@ -63,6 +89,32 @@
this.insert(file.slug.split("/"), file)
}
+ findNode(path: string[]): FileTrieNode<T> | undefined {
+ if (path.length === 0 || (path.length === 1 && path[0] === "index")) {
+ return this
+ }
+
+ 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
*/
@@ -88,7 +140,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 +150,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