The Trans Directory
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: use slugs instead of title as basis for explorer (#652)

* use slugs instead of title as basis for explorer

* fix folder persist state, better default behaviour

* use relative path instead of full path as full path is affected by -d

* dont use title in breadcrumb if it's just index lol

authored by

Jacky Zhao and committed by
GitHub
504b4471 63bf1e14

+109 -89
+6 -2
quartz/components/Breadcrumbs.tsx
··· 68 68 // construct the index for the first time 69 69 for (const file of allFiles) { 70 70 if (file.slug?.endsWith("index")) { 71 - const folderParts = file.filePath?.split("/") 71 + const folderParts = file.slug?.split("/") 72 72 if (folderParts) { 73 + // 2nd last to exclude the /index 73 74 const folderName = folderParts[folderParts?.length - 2] 74 75 folderIndex.set(folderName, file) 75 76 } ··· 88 89 // Try to resolve frontmatter folder title 89 90 const currentFile = folderIndex?.get(curPathSegment) 90 91 if (currentFile) { 91 - curPathSegment = currentFile.frontmatter!.title 92 + const title = currentFile.frontmatter!.title 93 + if (title !== "index") { 94 + curPathSegment = title 95 + } 92 96 } 93 97 94 98 // Add current slug to full path
+27 -34
quartz/components/Explorer.tsx
··· 12 12 folderClickBehavior: "collapse", 13 13 folderDefaultState: "collapsed", 14 14 useSavedState: true, 15 + mapFn: (node) => { 16 + return node 17 + }, 15 18 sortFn: (a, b) => { 16 19 // Sort order: folders first, then files. Sort folders and files alphabetically 17 20 if ((!a.file && !b.file) || (a.file && b.file)) { ··· 22 25 sensitivity: "base", 23 26 }) 24 27 } 28 + 25 29 if (a.file && !b.file) { 26 30 return 1 27 31 } else { ··· 41 45 let jsonTree: string 42 46 43 47 function constructFileTree(allFiles: QuartzPluginData[]) { 44 - if (!fileTree) { 45 - // Construct tree from allFiles 46 - fileTree = new FileNode("") 47 - allFiles.forEach((file) => fileTree.add(file, 1)) 48 + if (fileTree) { 49 + return 50 + } 48 51 49 - /** 50 - * Keys of this object must match corresponding function name of `FileNode`, 51 - * while values must be the argument that will be passed to the function. 52 - * 53 - * e.g. entry for FileNode.sort: `sort: opts.sortFn` (value is sort function from options) 54 - */ 55 - const functions = { 56 - map: opts.mapFn, 57 - sort: opts.sortFn, 58 - filter: opts.filterFn, 59 - } 52 + // Construct tree from allFiles 53 + fileTree = new FileNode("") 54 + allFiles.forEach((file) => fileTree.add(file)) 60 55 61 - // Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied) 62 - if (opts.order) { 63 - // Order is important, use loop with index instead of order.map() 64 - for (let i = 0; i < opts.order.length; i++) { 65 - const functionName = opts.order[i] 66 - if (functions[functionName]) { 67 - // for every entry in order, call matching function in FileNode and pass matching argument 68 - // e.g. i = 0; functionName = "filter" 69 - // converted to: (if opts.filterFn) => fileTree.filter(opts.filterFn) 70 - 71 - // @ts-ignore 72 - // typescript cant statically check these dynamic references, so manually make sure reference is valid and ignore warning 73 - fileTree[functionName].call(fileTree, functions[functionName]) 74 - } 56 + // Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied) 57 + if (opts.order) { 58 + // Order is important, use loop with index instead of order.map() 59 + for (let i = 0; i < opts.order.length; i++) { 60 + const functionName = opts.order[i] 61 + if (functionName === "map") { 62 + fileTree.map(opts.mapFn) 63 + } else if (functionName === "sort") { 64 + fileTree.sort(opts.sortFn) 65 + } else if (functionName === "filter") { 66 + fileTree.filter(opts.filterFn) 75 67 } 76 68 } 69 + } 77 70 78 - // Get all folders of tree. Initialize with collapsed state 79 - const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed") 71 + // Get all folders of tree. Initialize with collapsed state 72 + const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed") 80 73 81 - // Stringify to pass json tree as data attribute ([data-tree]) 82 - jsonTree = JSON.stringify(folders) 83 - } 74 + // Stringify to pass json tree as data attribute ([data-tree]) 75 + jsonTree = JSON.stringify(folders) 84 76 } 85 77 86 78 function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) { ··· 120 112 </div> 121 113 ) 122 114 } 115 + 123 116 Explorer.css = explorerStyle 124 117 Explorer.afterDOMLoaded = script 125 118 return Explorer
+69 -45
quartz/components/ExplorerNode.tsx
··· 1 1 // @ts-ignore 2 2 import { QuartzPluginData } from "../plugins/vfile" 3 - import { resolveRelative } from "../util/path" 3 + import { 4 + joinSegments, 5 + resolveRelative, 6 + clone, 7 + simplifySlug, 8 + SimpleSlug, 9 + FilePath, 10 + } from "../util/path" 4 11 5 12 type OrderEntries = "sort" | "filter" | "map" 6 13 ··· 10 17 folderClickBehavior: "collapse" | "link" 11 18 useSavedState: boolean 12 19 sortFn: (a: FileNode, b: FileNode) => number 13 - filterFn?: (node: FileNode) => boolean 14 - mapFn?: (node: FileNode) => void 15 - order?: OrderEntries[] 20 + filterFn: (node: FileNode) => boolean 21 + mapFn: (node: FileNode) => void 22 + order: OrderEntries[] 16 23 } 17 24 18 25 type DataWrapper = { ··· 25 32 collapsed: boolean 26 33 } 27 34 35 + function getPathSegment(fp: FilePath | undefined, idx: number): string | undefined { 36 + if (!fp) { 37 + return undefined 38 + } 39 + 40 + return fp.split("/").at(idx) 41 + } 42 + 28 43 // Structure to add all files into a tree 29 44 export class FileNode { 30 - children: FileNode[] 31 - name: string 45 + children: Array<FileNode> 46 + name: string // this is the slug segment 32 47 displayName: string 33 48 file: QuartzPluginData | null 34 49 depth: number 35 50 36 - constructor(name: string, file?: QuartzPluginData, depth?: number) { 51 + constructor(slugSegment: string, displayName?: string, file?: QuartzPluginData, depth?: number) { 37 52 this.children = [] 38 - this.name = name 39 - this.displayName = name 40 - this.file = file ? structuredClone(file) : null 53 + this.name = slugSegment 54 + this.displayName = displayName ?? file?.frontmatter?.title ?? slugSegment 55 + this.file = file ? clone(file) : null 41 56 this.depth = depth ?? 0 42 57 } 43 58 44 - private insert(file: DataWrapper) { 45 - if (file.path.length === 1) { 46 - if (file.path[0] !== "index.md") { 47 - this.children.push(new FileNode(file.file.frontmatter!.title, file.file, this.depth + 1)) 48 - } else { 49 - const title = file.file.frontmatter?.title 50 - if (title && title !== "index" && file.path[0] === "index.md") { 59 + private insert(fileData: DataWrapper) { 60 + if (fileData.path.length === 0) { 61 + return 62 + } 63 + 64 + const nextSegment = fileData.path[0] 65 + 66 + // base case, insert here 67 + if (fileData.path.length === 1) { 68 + if (nextSegment === "") { 69 + // index case (we are the root and we just found index.md), set our data appropriately 70 + const title = fileData.file.frontmatter?.title 71 + if (title && title !== "index") { 51 72 this.displayName = title 52 73 } 53 - } 54 - } else { 55 - const next = file.path[0] 56 - file.path = file.path.splice(1) 57 - for (const child of this.children) { 58 - if (child.name === next) { 59 - child.insert(file) 60 - return 61 - } 74 + } else { 75 + // direct child 76 + this.children.push(new FileNode(nextSegment, undefined, fileData.file, this.depth + 1)) 62 77 } 63 78 64 - const newChild = new FileNode(next, undefined, this.depth + 1) 65 - newChild.insert(file) 66 - this.children.push(newChild) 79 + return 80 + } 81 + 82 + // find the right child to insert into 83 + fileData.path = fileData.path.splice(1) 84 + const child = this.children.find((c) => c.name === nextSegment) 85 + if (child) { 86 + child.insert(fileData) 87 + return 67 88 } 89 + 90 + const newChild = new FileNode( 91 + nextSegment, 92 + getPathSegment(fileData.file.relativePath, this.depth), 93 + undefined, 94 + this.depth + 1, 95 + ) 96 + newChild.insert(fileData) 97 + this.children.push(newChild) 68 98 } 69 99 70 100 // Add new file to tree 71 - add(file: QuartzPluginData, splice: number = 0) { 72 - this.insert({ file, path: file.filePath!.split("/").splice(splice) }) 73 - } 74 - 75 - // Print tree structure (for debugging) 76 - print(depth: number = 0) { 77 - let folderChar = "" 78 - if (!this.file) folderChar = "|" 79 - console.log("-".repeat(depth), folderChar, this.name, this.depth) 80 - this.children.forEach((e) => e.print(depth + 1)) 101 + add(file: QuartzPluginData) { 102 + this.insert({ file: file, path: simplifySlug(file.slug!).split("/") }) 81 103 } 82 104 83 105 /** ··· 95 117 */ 96 118 map(mapFn: (node: FileNode) => void) { 97 119 mapFn(this) 98 - 99 120 this.children.forEach((child) => child.map(mapFn)) 100 121 } 101 122 ··· 110 131 111 132 const traverse = (node: FileNode, currentPath: string) => { 112 133 if (!node.file) { 113 - const folderPath = currentPath + (currentPath ? "/" : "") + node.name 134 + const folderPath = joinSegments(currentPath, node.name) 114 135 if (folderPath !== "") { 115 136 folderPaths.push({ path: folderPath, collapsed }) 116 137 } 138 + 117 139 node.children.forEach((child) => traverse(child, folderPath)) 118 140 } 119 141 } 120 142 121 143 traverse(this, "") 122 - 123 144 return folderPaths 124 145 } 125 146 ··· 147 168 const isDefaultOpen = opts.folderDefaultState === "open" 148 169 149 170 // Calculate current folderPath 150 - let pathOld = fullPath ? fullPath : "" 151 171 let folderPath = "" 152 172 if (node.name !== "") { 153 - folderPath = `${pathOld}/${node.name}` 173 + folderPath = joinSegments(fullPath ?? "", node.name) 154 174 } 155 175 156 176 return ( ··· 185 205 {/* render <a> tag if folderBehavior is "link", otherwise render <button> with collapse click event */} 186 206 <div key={node.name} data-folderpath={folderPath}> 187 207 {folderBehavior === "link" ? ( 188 - <a href={`${folderPath}`} data-for={node.name} class="folder-title"> 208 + <a 209 + href={resolveRelative(fileData.slug!, folderPath as SimpleSlug)} 210 + data-for={node.name} 211 + class="folder-title" 212 + > 189 213 {node.displayName} 190 214 </a> 191 215 ) : (
+2 -5
quartz/components/scripts/explorer.inline.ts
··· 59 59 // Save folder state to localStorage 60 60 const clickFolderPath = currentFolderParent.dataset.folderpath as string 61 61 62 - // Remove leading "/" 63 - const fullFolderPath = clickFolderPath.substring(1) 62 + const fullFolderPath = clickFolderPath 64 63 toggleCollapsedByPath(explorerState, fullFolderPath) 65 64 66 65 const stringifiedFileTree = JSON.stringify(explorerState) ··· 108 107 explorerState = JSON.parse(storageTree) 109 108 explorerState.map((folderUl) => { 110 109 // grab <li> element for matching folder path 111 - const folderLi = document.querySelector( 112 - `[data-folderpath='/${folderUl.path}']`, 113 - ) as HTMLElement 110 + const folderLi = document.querySelector(`[data-folderpath='${folderUl.path}']`) as HTMLElement 114 111 115 112 // Get corresponding content <ul> tag and set state 116 113 if (folderLi) {
+1
quartz/plugins/index.ts
··· 30 30 interface DataMap { 31 31 slug: FullSlug 32 32 filePath: FilePath 33 + relativePath: FilePath 33 34 } 34 35 }
+3 -2
quartz/processors/parse.ts
··· 91 91 } 92 92 93 93 // base data properties that plugins may use 94 - file.data.slug = slugifyFilePath(path.posix.relative(argv.directory, file.path) as FilePath) 95 - file.data.filePath = fp 94 + file.data.filePath = file.path as FilePath 95 + file.data.relativePath = path.posix.relative(argv.directory, file.path) as FilePath 96 + file.data.slug = slugifyFilePath(file.data.relativePath) 96 97 97 98 const ast = processor.parse(file) 98 99 const newAst = await processor.run(ast, file)
+1 -1
quartz/util/path.ts
··· 2 2 import type { Element as HastElement } from "hast" 3 3 import rfdc from "rfdc" 4 4 5 - const clone = rfdc() 5 + export const clone = rfdc() 6 6 7 7 // this file must be isomorphic so it can't use node libs (e.g. path) 8 8