The Trans Directory
0
fork

Configure Feed

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

fix: cleanup a href link construction, global shared trie, breadcrumbs use trie

+158 -95
+1
quartz/build.ts
··· 21 21 import { randomIdNonSecure } from "./util/random" 22 22 import { ChangeEvent } from "./plugins/types" 23 23 import { minimatch } from "minimatch" 24 + import { FileTrieNode } from "./util/fileTrie" 24 25 25 26 type ContentMap = Map< 26 27 FilePath,
+21 -67
quartz/components/Breadcrumbs.tsx
··· 1 1 import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" 2 2 import breadcrumbsStyle from "./styles/breadcrumbs.scss" 3 - import { FullSlug, SimpleSlug, joinSegments, resolveRelative } from "../util/path" 4 - import { QuartzPluginData } from "../plugins/vfile" 3 + import { FullSlug, SimpleSlug, resolveRelative, simplifySlug } from "../util/path" 5 4 import { classNames } from "../util/lang" 5 + import { trieFromAllFiles } from "../util/ctx" 6 6 7 7 type CrumbData = { 8 8 displayName: string ··· 23 23 */ 24 24 resolveFrontmatterTitle: boolean 25 25 /** 26 - * Whether to display breadcrumbs on root `index.md` 27 - */ 28 - hideOnRoot: boolean 29 - /** 30 26 * Whether to display the current page in the breadcrumbs. 31 27 */ 32 28 showCurrentPage: boolean ··· 36 32 spacerSymbol: "❯", 37 33 rootName: "Home", 38 34 resolveFrontmatterTitle: true, 39 - hideOnRoot: true, 40 35 showCurrentPage: true, 41 36 } 42 37 ··· 48 43 } 49 44 50 45 export default ((opts?: Partial<BreadcrumbOptions>) => { 51 - // Merge options with defaults 52 46 const options: BreadcrumbOptions = { ...defaultOptions, ...opts } 53 - 54 - // computed index of folder name to its associated file data 55 - let folderIndex: Map<string, QuartzPluginData> | undefined 56 - 57 47 const Breadcrumbs: QuartzComponent = ({ 58 48 fileData, 59 49 allFiles, 60 50 displayClass, 51 + ctx, 61 52 }: QuartzComponentProps) => { 62 - // Hide crumbs on root if enabled 63 - if (options.hideOnRoot && fileData.slug === "index") { 64 - return <></> 65 - } 66 - 67 - // Format entry for root element 68 - const firstEntry = formatCrumb(options.rootName, fileData.slug!, "/" as SimpleSlug) 69 - const crumbs: CrumbData[] = [firstEntry] 53 + const trie = (ctx.trie ??= trieFromAllFiles(allFiles)) 54 + const slugParts = fileData.slug!.split("/") 55 + const pathNodes = trie.ancestryChain(slugParts) 70 56 71 - if (!folderIndex && options.resolveFrontmatterTitle) { 72 - folderIndex = new Map() 73 - // construct the index for the first time 74 - for (const file of allFiles) { 75 - const folderParts = file.slug?.split("/") 76 - if (folderParts?.at(-1) === "index") { 77 - folderIndex.set(folderParts.slice(0, -1).join("/"), file) 78 - } 79 - } 57 + if (!pathNodes) { 58 + return null 80 59 } 81 60 82 - // Split slug into hierarchy/parts 83 - const slugParts = fileData.slug?.split("/") 84 - if (slugParts) { 85 - // is tag breadcrumb? 86 - const isTagPath = slugParts[0] === "tags" 87 - 88 - // full path until current part 89 - let currentPath = "" 90 - 91 - for (let i = 0; i < slugParts.length - 1; i++) { 92 - let curPathSegment = slugParts[i] 93 - 94 - // Try to resolve frontmatter folder title 95 - const currentFile = folderIndex?.get(slugParts.slice(0, i + 1).join("/")) 96 - if (currentFile) { 97 - const title = currentFile.frontmatter!.title 98 - if (title !== "index") { 99 - curPathSegment = title 100 - } 101 - } 102 - 103 - // Add current slug to full path 104 - currentPath = joinSegments(currentPath, slugParts[i]) 105 - const includeTrailingSlash = !isTagPath || i < slugParts.length - 1 106 - 107 - // Format and add current crumb 108 - const crumb = formatCrumb( 109 - curPathSegment, 110 - fileData.slug!, 111 - (currentPath + (includeTrailingSlash ? "/" : "")) as SimpleSlug, 112 - ) 113 - crumbs.push(crumb) 61 + const crumbs: CrumbData[] = pathNodes.map((node, idx) => { 62 + const crumb = formatCrumb(node.displayName, fileData.slug!, simplifySlug(node.slug)) 63 + if (idx === 0) { 64 + crumb.displayName = options.rootName 114 65 } 115 66 116 - // Add current file to crumb (can directly use frontmatter title) 117 - if (options.showCurrentPage && slugParts.at(-1) !== "index") { 118 - crumbs.push({ 119 - displayName: fileData.frontmatter!.title, 120 - path: "", 121 - }) 67 + // For last node (current page), set empty path 68 + if (idx === pathNodes.length - 1) { 69 + crumb.path = "" 122 70 } 71 + 72 + return crumb 73 + }) 74 + 75 + if (!options.showCurrentPage) { 76 + crumbs.pop() 123 77 } 124 78 125 79 return (
+2 -3
quartz/components/TagList.tsx
··· 1 - import { pathToRoot, slugTag } from "../util/path" 1 + import { FullSlug, resolveRelative } from "../util/path" 2 2 import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" 3 3 import { classNames } from "../util/lang" 4 4 5 5 const TagList: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => { 6 6 const tags = fileData.frontmatter?.tags 7 - const baseDir = pathToRoot(fileData.slug!) 8 7 if (tags && tags.length > 0) { 9 8 return ( 10 9 <ul class={classNames(displayClass, "tags")}> 11 10 {tags.map((tag) => { 12 - const linkDest = baseDir + `/tags/${slugTag(tag)}` 11 + const linkDest = resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug) 13 12 return ( 14 13 <li> 15 14 <a href={linkDest} class="internal tag-link">
+3 -22
quartz/components/pages/FolderContent.tsx
··· 8 8 import { QuartzPluginData } from "../../plugins/vfile" 9 9 import { ComponentChildren } from "preact" 10 10 import { concatenateResources } from "../../util/resources" 11 - import { FileTrieNode } from "../../util/fileTrie" 11 + import { trieFromAllFiles } from "../../util/ctx" 12 + 12 13 interface FolderContentOptions { 13 14 /** 14 15 * Whether to display number of folders ··· 25 26 26 27 export default ((opts?: Partial<FolderContentOptions>) => { 27 28 const options: FolderContentOptions = { ...defaultOptions, ...opts } 28 - let trie: FileTrieNode< 29 - QuartzPluginData & { 30 - slug: string 31 - title: string 32 - filePath: string 33 - } 34 - > 35 29 36 30 const FolderContent: QuartzComponent = (props: QuartzComponentProps) => { 37 31 const { tree, fileData, allFiles, cfg } = props 38 32 39 - if (!trie) { 40 - trie = new FileTrieNode([]) 41 - allFiles.forEach((file) => { 42 - if (file.frontmatter) { 43 - trie.add({ 44 - ...file, 45 - slug: file.slug!, 46 - title: file.frontmatter.title, 47 - filePath: file.filePath!, 48 - }) 49 - } 50 - }) 51 - } 52 - 33 + const trie = (props.ctx.trie ??= trieFromAllFiles(allFiles)) 53 34 const folder = trie.findNode(fileData.slug!.split("/")) 54 35 if (!folder) { 55 36 return null
+5 -2
quartz/components/pages/TagContent.tsx
··· 1 1 import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" 2 2 import style from "../styles/listPage.scss" 3 3 import { PageList, SortFn } from "../PageList" 4 - import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path" 4 + import { FullSlug, getAllSegmentPrefixes, resolveRelative, simplifySlug } from "../../util/path" 5 5 import { QuartzPluginData } from "../../plugins/vfile" 6 6 import { Root } from "hast" 7 7 import { htmlToJsx } from "../../util/jsx" ··· 74 74 ? contentPage?.description 75 75 : htmlToJsx(contentPage.filePath!, root) 76 76 77 + const tagListingPage = `/tags/${tag}` as FullSlug 78 + const href = resolveRelative(fileData.slug!, tagListingPage) 79 + 77 80 return ( 78 81 <div> 79 82 <h2> 80 - <a class="internal tag-link" href={`../tags/${tag}`}> 83 + <a class="internal tag-link" href={href}> 81 84 {tag} 82 85 </a> 83 86 </h2>
+26 -1
quartz/util/ctx.ts
··· 1 1 import { QuartzConfig } from "../cfg" 2 + import { QuartzPluginData } from "../plugins/vfile" 3 + import { FileTrieNode } from "./fileTrie" 2 4 import { FilePath, FullSlug } from "./path" 3 5 4 6 export interface Argv { ··· 13 15 concurrency?: number 14 16 } 15 17 18 + export type BuildTimeTrieData = QuartzPluginData & { 19 + slug: string 20 + title: string 21 + filePath: string 22 + } 23 + 16 24 export interface BuildCtx { 17 25 buildId: string 18 26 argv: Argv 19 27 cfg: QuartzConfig 20 28 allSlugs: FullSlug[] 21 29 allFiles: FilePath[] 30 + trie?: FileTrieNode<BuildTimeTrieData> 22 31 incremental: boolean 23 32 } 24 33 25 - export type WorkerSerializableBuildCtx = Omit<BuildCtx, "cfg"> 34 + export function trieFromAllFiles(allFiles: QuartzPluginData[]): FileTrieNode<BuildTimeTrieData> { 35 + const trie = new FileTrieNode<BuildTimeTrieData>([]) 36 + allFiles.forEach((file) => { 37 + if (file.frontmatter) { 38 + trie.add({ 39 + ...file, 40 + slug: file.slug!, 41 + title: file.frontmatter.title, 42 + filePath: file.filePath!, 43 + }) 44 + } 45 + }) 46 + 47 + return trie 48 + } 49 + 50 + export type WorkerSerializableBuildCtx = Omit<BuildCtx, "cfg" | "trie">
+82
quartz/util/fileTrie.test.ts
··· 330 330 ) 331 331 }) 332 332 }) 333 + 334 + describe("pathToNode", () => { 335 + test("should return root node for empty path", () => { 336 + const data = { title: "Root", slug: "index", filePath: "index.md" } 337 + trie.add(data) 338 + const path = trie.ancestryChain([]) 339 + assert.deepStrictEqual(path, [trie]) 340 + }) 341 + 342 + test("should return root node for index path", () => { 343 + const data = { title: "Root", slug: "index", filePath: "index.md" } 344 + trie.add(data) 345 + const path = trie.ancestryChain(["index"]) 346 + assert.deepStrictEqual(path, [trie]) 347 + }) 348 + 349 + test("should return path to first level node", () => { 350 + const data = { title: "Test", slug: "test", filePath: "test.md" } 351 + trie.add(data) 352 + const path = trie.ancestryChain(["test"]) 353 + assert.deepStrictEqual(path, [trie, trie.children[0]]) 354 + }) 355 + 356 + test("should return path to nested node", () => { 357 + const data = { 358 + title: "Nested", 359 + slug: "folder/subfolder/test", 360 + filePath: "folder/subfolder/test.md", 361 + } 362 + trie.add(data) 363 + const path = trie.ancestryChain(["folder", "subfolder", "test"]) 364 + assert.deepStrictEqual(path, [ 365 + trie, 366 + trie.children[0], 367 + trie.children[0].children[0], 368 + trie.children[0].children[0].children[0], 369 + ]) 370 + }) 371 + 372 + test("should return undefined for non-existent path", () => { 373 + const data = { title: "Test", slug: "test", filePath: "test.md" } 374 + trie.add(data) 375 + const path = trie.ancestryChain(["nonexistent"]) 376 + assert.strictEqual(path, undefined) 377 + }) 378 + 379 + test("should return file data for intermediate folders", () => { 380 + const data1 = { 381 + title: "Root", 382 + slug: "index", 383 + filePath: "index.md", 384 + } 385 + const data2 = { 386 + title: "Test", 387 + slug: "folder/subfolder/test", 388 + filePath: "folder/subfolder/test.md", 389 + } 390 + const data3 = { 391 + title: "Folder Index", 392 + slug: "folder/index", 393 + filePath: "folder/index.md", 394 + } 395 + 396 + trie.add(data1) 397 + trie.add(data2) 398 + trie.add(data3) 399 + const path = trie.ancestryChain(["folder", "subfolder"]) 400 + assert.deepStrictEqual(path, [trie, trie.children[0], trie.children[0].children[0]]) 401 + assert.strictEqual(path[1].data, data3) 402 + }) 403 + 404 + test("should return path for partial path", () => { 405 + const data = { 406 + title: "Nested", 407 + slug: "folder/subfolder/test", 408 + filePath: "folder/subfolder/test.md", 409 + } 410 + trie.add(data) 411 + const path = trie.ancestryChain(["folder"]) 412 + assert.deepStrictEqual(path, [trie, trie.children[0]]) 413 + }) 414 + }) 333 415 })
+18
quartz/util/fileTrie.ts
··· 97 97 return this.children.find((c) => c.slugSegment === path[0])?.findNode(path.slice(1)) 98 98 } 99 99 100 + ancestryChain(path: string[]): Array<FileTrieNode<T>> | undefined { 101 + if (path.length === 0 || (path.length === 1 && path[0] === "index")) { 102 + return [this] 103 + } 104 + 105 + const child = this.children.find((c) => c.slugSegment === path[0]) 106 + if (!child) { 107 + return undefined 108 + } 109 + 110 + const childPath = child.ancestryChain(path.slice(1)) 111 + if (!childPath) { 112 + return undefined 113 + } 114 + 115 + return [this, ...childPath] 116 + } 117 + 100 118 /** 101 119 * Filter trie nodes. Behaves similar to `Array.prototype.filter()`, but modifies tree in place 102 120 */