From 4e0c34730df586ed3a602d44eaf75d6ce9d73694 Mon Sep 17 00:00:00 2001 From: Anton Bulakh Date: Sat, 28 Dec 2024 17:35:52 +0200 Subject: [PATCH] fix(folders): Use real folder names instead of splicing slugs In breadcrumbs and folder pages (both folder page titles and page lists) the name of the folder was derived from the slug unless overriden, which is.. wonky. This is much more noticeable if you change the slugify function to make all slugs lowercase - which I did, and which may be a followup PR. The patch was pretty straightforward though, we just use the real folder names from the relativePath. --- quartz/components/Breadcrumbs.tsx | 14 +++-- quartz/components/pages/FolderContent.tsx | 66 +++++++++++++---------- quartz/plugins/emitters/folderPage.tsx | 47 +++++++++------- 3 files changed, 73 insertions(+), 54 deletions(-) diff --git a/quartz/components/Breadcrumbs.tsx b/quartz/components/Breadcrumbs.tsx index 9ccfb9a6a..3f1b13a05 100644 --- a/quartz/components/Breadcrumbs.tsx +++ b/quartz/components/Breadcrumbs.tsx @@ -40,11 +40,8 @@ const defaultOptions: BreadcrumbOptions = { showCurrentPage: true, } -function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: SimpleSlug): CrumbData { - return { - displayName: displayName.replaceAll("-", " "), - path: resolveRelative(baseSlug, currentSlug), - } +function newCrumb(displayName: string, baseSlug: FullSlug, currentSlug: SimpleSlug): CrumbData { + return { displayName, path: resolveRelative(baseSlug, currentSlug) } } export default ((opts?: Partial) => { @@ -65,7 +62,7 @@ export default ((opts?: Partial) => { } // Format entry for root element - const firstEntry = formatCrumb(options.rootName, fileData.slug!, "/" as SimpleSlug) + const firstEntry = newCrumb(options.rootName, fileData.slug!, "/" as SimpleSlug) const crumbs: CrumbData[] = [firstEntry] if (!folderIndex && options.resolveFrontmatterTitle) { @@ -81,6 +78,7 @@ export default ((opts?: Partial) => { // Split slug into hierarchy/parts const slugParts = fileData.slug?.split("/") + const pathParts = fileData.relativePath?.split("/") if (slugParts) { // is tag breadcrumb? const isTagPath = slugParts[0] === "tags" @@ -89,7 +87,7 @@ export default ((opts?: Partial) => { let currentPath = "" for (let i = 0; i < slugParts.length - 1; i++) { - let curPathSegment = slugParts[i] + let curPathSegment = pathParts?.[i] ?? slugParts[i] // Try to resolve frontmatter folder title const currentFile = folderIndex?.get(slugParts.slice(0, i + 1).join("/")) @@ -105,7 +103,7 @@ export default ((opts?: Partial) => { const includeTrailingSlash = !isTagPath || i < 1 // Format and add current crumb - const crumb = formatCrumb( + const crumb = newCrumb( curPathSegment, fileData.slug!, (currentPath + (includeTrailingSlash ? "/" : "")) as SimpleSlug, diff --git a/quartz/components/pages/FolderContent.tsx b/quartz/components/pages/FolderContent.tsx index 593073b96..bc81423f3 100644 --- a/quartz/components/pages/FolderContent.tsx +++ b/quartz/components/pages/FolderContent.tsx @@ -23,6 +23,11 @@ const defaultOptions: FolderContentOptions = { showSubfolders: true, } +type Subfolder = { + name: string + contents: QuartzPluginData[] +} + export default ((opts?: Partial) => { const options: FolderContentOptions = { ...defaultOptions, ...opts } @@ -31,51 +36,56 @@ export default ((opts?: Partial) => { const folderSlug = stripSlashes(simplifySlug(fileData.slug!)) const folderParts = folderSlug.split(path.posix.sep) - const allPagesInFolder: QuartzPluginData[] = [] - const allPagesInSubfolders: Map = new Map() + const shownPages: QuartzPluginData[] = [] + const subfolders: Map = new Map() - allFiles.forEach((file) => { + for (const file of allFiles) { const fileSlug = stripSlashes(simplifySlug(file.slug!)) - const prefixed = fileSlug.startsWith(folderSlug) && fileSlug !== folderSlug - const fileParts = fileSlug.split(path.posix.sep) - const isDirectChild = fileParts.length === folderParts.length + 1 - - if (!prefixed) { - return + // check only files in our folder or nested folders + if (!fileSlug.startsWith(folderSlug) || fileSlug === folderSlug) { + continue } - if (isDirectChild) { - allPagesInFolder.push(file) - } else if (options.showSubfolders) { + const fileParts = fileSlug.split(path.posix.sep) + + // If the file is directly in the folder we just show it + if (fileParts.length === folderParts.length + 1) { + shownPages.push(file) + continue + } + + if (options.showSubfolders) { const subfolderSlug = joinSegments( ...fileParts.slice(0, folderParts.length + 1), ) as FullSlug - const pagesInFolder = allPagesInSubfolders.get(subfolderSlug) || [] - allPagesInSubfolders.set(subfolderSlug, [...pagesInFolder, file]) - } - }) - allPagesInSubfolders.forEach((files, subfolderSlug) => { - const hasIndex = allPagesInFolder.some( - (file) => subfolderSlug === stripSlashes(simplifySlug(file.slug!)), - ) + let subfolder = subfolders.get(subfolderSlug) + if (!subfolder) { + const subfolderName = file.relativePath!.split(path.posix.sep).at(folderParts.length)! + subfolders.set(subfolderSlug, (subfolder = { name: subfolderName, contents: [] })) + } + subfolder.contents.push(file) + } + } + + for (const [slug, subfolder] of subfolders.entries()) { + const hasIndex = shownPages.some((file) => slug === stripSlashes(simplifySlug(file.slug!))) if (!hasIndex) { - const subfolderDates = files.sort(byDateAndAlphabetical(cfg))[0].dates - const subfolderTitle = subfolderSlug.split(path.posix.sep).at(-1)! - allPagesInFolder.push({ - slug: subfolderSlug, + const subfolderDates = subfolder.contents.sort(byDateAndAlphabetical(cfg))[0].dates + shownPages.push({ + slug: slug, dates: subfolderDates, - frontmatter: { title: subfolderTitle, tags: ["folder"] }, + frontmatter: { title: subfolder.name, tags: ["folder"] }, }) } - }) + } const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? [] const classes = cssClasses.join(" ") const listProps = { ...props, sort: options.sort, - allFiles: allPagesInFolder, + allFiles: shownPages, } const content = @@ -90,7 +100,7 @@ export default ((opts?: Partial) => { {options.showFolderCount && (

{i18n(cfg.locale).pages.folderContent.itemsUnderFolder({ - count: allPagesInFolder.length, + count: shownPages.length, })}

)} diff --git a/quartz/plugins/emitters/folderPage.tsx b/quartz/plugins/emitters/folderPage.tsx index bafaec916..a6170e364 100644 --- a/quartz/plugins/emitters/folderPage.tsx +++ b/quartz/plugins/emitters/folderPage.tsx @@ -74,13 +74,27 @@ export const FolderPage: QuartzEmitterPlugin> = (user const allFiles = content.map((c) => c[1].data) const cfg = ctx.cfg.configuration + const folderNames: Record = {} + const folders: Set = new Set( allFiles.flatMap((data) => { - return data.slug - ? _getFolders(data.slug).filter( - (folderName) => folderName !== "." && folderName !== "tags", - ) - : [] + if (!data.slug || !data.relativePath) { + return [] + } + let folderSlug = path.dirname(data.slug) as SimpleSlug + let folderFs = path.dirname(data.relativePath) as SimpleSlug + folderNames[folderSlug] = folderFs + + const folders = [folderSlug] + while (folderSlug !== ".") { + folderSlug = path.dirname(folderSlug) as SimpleSlug + folders.push(folderSlug) + + folderFs = path.dirname(folderFs) as SimpleSlug + folderNames[folderSlug] = folderFs + } + + return folders.filter((f) => f !== "." && f !== "tags") }), ) @@ -89,8 +103,9 @@ export const FolderPage: QuartzEmitterPlugin> = (user folder, defaultProcessedContent({ slug: joinSegments(folder, "index") as FullSlug, + relativePath: joinSegments(folderNames[folder], "index.html") as FilePath, // this is used by breadcrumbs frontmatter: { - title: `${i18n(cfg.locale).pages.folderContent.folder}: ${folder}`, + title: `${i18n(cfg.locale).pages.folderContent.folder}: ${folderNames[folder]}`, tags: [], }, }), @@ -100,7 +115,14 @@ export const FolderPage: QuartzEmitterPlugin> = (user for (const [tree, file] of content) { const slug = stripSlashes(simplifySlug(file.data.slug!)) as SimpleSlug if (folders.has(slug)) { - folderDescriptions[slug] = [tree, file] + if (file.data.frontmatter?.title === "index") { + // sadly we need to avoid changing the original file title for things like explorer to work + const clonedFile = structuredClone(file) + clonedFile.data.frontmatter!.title = `${i18n(cfg.locale).pages.folderContent.folder}: ${folderNames[slug]}` + folderDescriptions[slug] = [tree, clonedFile] + } else { + folderDescriptions[slug] = [tree, file] + } } } @@ -132,14 +154,3 @@ export const FolderPage: QuartzEmitterPlugin> = (user }, } } - -function _getFolders(slug: FullSlug): SimpleSlug[] { - var folderName = path.dirname(slug ?? "") as SimpleSlug - const parentFolderNames = [folderName] - - while (folderName !== ".") { - folderName = path.dirname(folderName ?? "") as SimpleSlug - parentFolderNames.push(folderName) - } - return parentFolderNames -}