// @ts-ignore import { QuartzPluginData } from "vfile" import { resolveRelative } from "../util/path" export interface Options { title: string folderDefaultState: "collapsed" | "open" folderClickBehavior: "collapse" | "link" useSavedState: boolean sortFn: (a: FileNode, b: FileNode) => number } type DataWrapper = { file: QuartzPluginData path: string[] } export type FolderState = { path: string collapsed: boolean } // Structure to add all files into a tree export class FileNode { children: FileNode[] name: string file: QuartzPluginData | null depth: number constructor(name: string, file?: QuartzPluginData, depth?: number) { this.children = [] this.name = name this.file = file ?? null this.depth = depth ?? 0 } private insert(file: DataWrapper) { if (file.path.length === 1) { this.children.push(new FileNode(file.file.frontmatter!.title, file.file, this.depth + 1)) } else { const next = file.path[0] file.path = file.path.splice(1) for (const child of this.children) { if (child.name === next) { child.insert(file) return } } const newChild = new FileNode(next, undefined, this.depth + 1) newChild.insert(file) this.children.push(newChild) } } // Add new file to tree add(file: QuartzPluginData, splice: number = 0) { this.insert({ file, path: file.filePath!.split("/").splice(splice) }) } // Print tree structure (for debugging) print(depth: number = 0) { let folderChar = "" if (!this.file) folderChar = "|" console.log("-".repeat(depth), folderChar, this.name, this.depth) this.children.forEach((e) => e.print(depth + 1)) } filter(filterFn: (node: FileNode) => boolean) { const filteredNodes: FileNode[] = [] const traverse = (node: FileNode) => { if (filterFn(node)) { filteredNodes.push(node) } node.children.forEach(traverse) } traverse(this) this.children = filteredNodes } /** * Get folder representation with state of tree. * Intended to only be called on root node before changes to the tree are made * @param collapsed default state of folders (collapsed by default or not) * @returns array containing folder state for tree */ getFolderPaths(collapsed: boolean): FolderState[] { const folderPaths: FolderState[] = [] const traverse = (node: FileNode, currentPath: string) => { if (!node.file) { const folderPath = currentPath + (currentPath ? "/" : "") + node.name if (folderPath !== "") { folderPaths.push({ path: folderPath, collapsed }) } node.children.forEach((child) => traverse(child, folderPath)) } } traverse(this, "") return folderPaths } // Sort order: folders first, then files. Sort folders and files alphabetically /** * Sorts tree according to sort/compare function * @param sortFn compare function used for `.sort()`, also used recursively for children */ sort(sortFn: (a: FileNode, b: FileNode) => number) { this.children = this.children.sort(sortFn) this.children.forEach((e) => e.sort(sortFn)) } } type ExplorerNodeProps = { node: FileNode opts: Options fileData: QuartzPluginData fullPath?: string } export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodeProps) { // Get options const folderBehavior = opts.folderClickBehavior const isDefaultOpen = opts.folderDefaultState === "open" // Calculate current folderPath let pathOld = fullPath ? fullPath : "" let folderPath = "" if (node.name !== "") { folderPath = `${pathOld}/${node.name}` } return (