Compare commits

...

3 Commits

Author SHA1 Message Date
Anton Bulakh
99027dad59
Merge 0bafa4c94a0b63b30b456a6fd02c6bdd701cdaee into 7be47742a6dc86f22d148ca9d304f7a9eea318cf 2025-01-31 06:46:47 -05:00
dependabot[bot]
7be47742a6
chore(deps): bump the production-dependencies group across 1 directory with 3 updates (#1744)
Some checks failed
Build and Test / build-and-test (macos-latest) (push) Has been cancelled
Build and Test / build-and-test (ubuntu-latest) (push) Has been cancelled
Build and Test / build-and-test (windows-latest) (push) Has been cancelled
Build and Test / publish-tag (push) Has been cancelled
Docker build & push image / build (push) Has been cancelled
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-31 06:46:45 -05:00
Anton Bulakh
0bafa4c94a
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.
2025-01-24 03:38:58 +02:00
5 changed files with 91 additions and 69 deletions

27
package-lock.json generated
View File

@ -34,7 +34,7 @@
"mdast-util-to-hast": "^13.2.0", "mdast-util-to-hast": "^13.2.0",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"micromorph": "^0.4.5", "micromorph": "^0.4.5",
"pixi.js": "^8.6.6", "pixi.js": "^8.7.3",
"preact": "^10.25.4", "preact": "^10.25.4",
"preact-render-to-string": "^6.5.13", "preact-render-to-string": "^6.5.13",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
@ -79,10 +79,10 @@
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/hast": "^3.0.4", "@types/hast": "^3.0.4",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/node": "^22.10.6", "@types/node": "^22.12.0",
"@types/pretty-time": "^1.1.5", "@types/pretty-time": "^1.1.5",
"@types/source-map-support": "^0.5.10", "@types/source-map-support": "^0.5.10",
"@types/ws": "^8.5.13", "@types/ws": "^8.5.14",
"@types/yargs": "^17.0.33", "@types/yargs": "^17.0.33",
"esbuild": "^0.24.2", "esbuild": "^0.24.2",
"prettier": "^3.4.2", "prettier": "^3.4.2",
@ -1914,10 +1914,11 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.10.6", "version": "22.12.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.6.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.12.0.tgz",
"integrity": "sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==", "integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.20.0" "undici-types": "~6.20.0"
} }
@ -1943,10 +1944,11 @@
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
}, },
"node_modules/@types/ws": { "node_modules/@types/ws": {
"version": "8.5.13", "version": "8.5.14",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz",
"integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", "integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
} }
@ -5583,9 +5585,10 @@
} }
}, },
"node_modules/pixi.js": { "node_modules/pixi.js": {
"version": "8.6.6", "version": "8.7.3",
"resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.6.6.tgz", "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.7.3.tgz",
"integrity": "sha512-o5pw7G2yuIrnBx0G4npBlmFp+XGNcapI/Ufs62rRj/4XKxc1Zo74YJr/BtEXcXTraTKd+pQvYOLvnfxRjxBMvQ==", "integrity": "sha512-wfWlhJYnGx1s4f2yoouevQjaeacbJ12LTkJGa+n9AIYNIjOnmJylBtZ2mARX7iFk3mr2xv0wuo//XPe2hk5OBw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@pixi/colord": "^2.9.6", "@pixi/colord": "^2.9.6",
"@types/css-font-loading-module": "^0.0.12", "@types/css-font-loading-module": "^0.0.12",

View File

@ -60,7 +60,7 @@
"mdast-util-to-hast": "^13.2.0", "mdast-util-to-hast": "^13.2.0",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"micromorph": "^0.4.5", "micromorph": "^0.4.5",
"pixi.js": "^8.6.6", "pixi.js": "^8.7.3",
"preact": "^10.25.4", "preact": "^10.25.4",
"preact-render-to-string": "^6.5.13", "preact-render-to-string": "^6.5.13",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
@ -102,10 +102,10 @@
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/hast": "^3.0.4", "@types/hast": "^3.0.4",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/node": "^22.10.6", "@types/node": "^22.12.0",
"@types/pretty-time": "^1.1.5", "@types/pretty-time": "^1.1.5",
"@types/source-map-support": "^0.5.10", "@types/source-map-support": "^0.5.10",
"@types/ws": "^8.5.13", "@types/ws": "^8.5.14",
"@types/yargs": "^17.0.33", "@types/yargs": "^17.0.33",
"esbuild": "^0.24.2", "esbuild": "^0.24.2",
"prettier": "^3.4.2", "prettier": "^3.4.2",

View File

@ -40,11 +40,8 @@ const defaultOptions: BreadcrumbOptions = {
showCurrentPage: true, showCurrentPage: true,
} }
function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: SimpleSlug): CrumbData { function newCrumb(displayName: string, baseSlug: FullSlug, currentSlug: SimpleSlug): CrumbData {
return { return { displayName, path: resolveRelative(baseSlug, currentSlug) }
displayName: displayName.replaceAll("-", " "),
path: resolveRelative(baseSlug, currentSlug),
}
} }
export default ((opts?: Partial<BreadcrumbOptions>) => { export default ((opts?: Partial<BreadcrumbOptions>) => {
@ -65,7 +62,7 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
} }
// Format entry for root element // 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] const crumbs: CrumbData[] = [firstEntry]
if (!folderIndex && options.resolveFrontmatterTitle) { if (!folderIndex && options.resolveFrontmatterTitle) {
@ -81,6 +78,7 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
// Split slug into hierarchy/parts // Split slug into hierarchy/parts
const slugParts = fileData.slug?.split("/") const slugParts = fileData.slug?.split("/")
const pathParts = fileData.relativePath?.split("/")
if (slugParts) { if (slugParts) {
// is tag breadcrumb? // is tag breadcrumb?
const isTagPath = slugParts[0] === "tags" const isTagPath = slugParts[0] === "tags"
@ -89,7 +87,7 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
let currentPath = "" let currentPath = ""
for (let i = 0; i < slugParts.length - 1; i++) { 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 // Try to resolve frontmatter folder title
const currentFile = folderIndex?.get(slugParts.slice(0, i + 1).join("/")) const currentFile = folderIndex?.get(slugParts.slice(0, i + 1).join("/"))
@ -105,7 +103,7 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
const includeTrailingSlash = !isTagPath || i < 1 const includeTrailingSlash = !isTagPath || i < 1
// Format and add current crumb // Format and add current crumb
const crumb = formatCrumb( const crumb = newCrumb(
curPathSegment, curPathSegment,
fileData.slug!, fileData.slug!,
(currentPath + (includeTrailingSlash ? "/" : "")) as SimpleSlug, (currentPath + (includeTrailingSlash ? "/" : "")) as SimpleSlug,

View File

@ -23,6 +23,11 @@ const defaultOptions: FolderContentOptions = {
showSubfolders: true, showSubfolders: true,
} }
type Subfolder = {
name: string
contents: QuartzPluginData[]
}
export default ((opts?: Partial<FolderContentOptions>) => { export default ((opts?: Partial<FolderContentOptions>) => {
const options: FolderContentOptions = { ...defaultOptions, ...opts } const options: FolderContentOptions = { ...defaultOptions, ...opts }
@ -31,51 +36,56 @@ export default ((opts?: Partial<FolderContentOptions>) => {
const folderSlug = stripSlashes(simplifySlug(fileData.slug!)) const folderSlug = stripSlashes(simplifySlug(fileData.slug!))
const folderParts = folderSlug.split(path.posix.sep) const folderParts = folderSlug.split(path.posix.sep)
const allPagesInFolder: QuartzPluginData[] = [] const shownPages: QuartzPluginData[] = []
const allPagesInSubfolders: Map<FullSlug, QuartzPluginData[]> = new Map() const subfolders: Map<FullSlug, Subfolder> = new Map()
allFiles.forEach((file) => { for (const file of allFiles) {
const fileSlug = stripSlashes(simplifySlug(file.slug!)) const fileSlug = stripSlashes(simplifySlug(file.slug!))
const prefixed = fileSlug.startsWith(folderSlug) && fileSlug !== folderSlug // check only files in our folder or nested folders
const fileParts = fileSlug.split(path.posix.sep) if (!fileSlug.startsWith(folderSlug) || fileSlug === folderSlug) {
const isDirectChild = fileParts.length === folderParts.length + 1 continue
if (!prefixed) {
return
} }
if (isDirectChild) { const fileParts = fileSlug.split(path.posix.sep)
allPagesInFolder.push(file)
} else if (options.showSubfolders) { // 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( const subfolderSlug = joinSegments(
...fileParts.slice(0, folderParts.length + 1), ...fileParts.slice(0, folderParts.length + 1),
) as FullSlug ) as FullSlug
const pagesInFolder = allPagesInSubfolders.get(subfolderSlug) || []
allPagesInSubfolders.set(subfolderSlug, [...pagesInFolder, file])
}
})
allPagesInSubfolders.forEach((files, subfolderSlug) => { let subfolder = subfolders.get(subfolderSlug)
const hasIndex = allPagesInFolder.some( if (!subfolder) {
(file) => subfolderSlug === stripSlashes(simplifySlug(file.slug!)), 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) { if (!hasIndex) {
const subfolderDates = files.sort(byDateAndAlphabetical(cfg))[0].dates const subfolderDates = subfolder.contents.sort(byDateAndAlphabetical(cfg))[0].dates
const subfolderTitle = subfolderSlug.split(path.posix.sep).at(-1)! shownPages.push({
allPagesInFolder.push({ slug: slug,
slug: subfolderSlug,
dates: subfolderDates, dates: subfolderDates,
frontmatter: { title: subfolderTitle, tags: ["folder"] }, frontmatter: { title: subfolder.name, tags: ["folder"] },
}) })
} }
}) }
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? [] const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
const classes = cssClasses.join(" ") const classes = cssClasses.join(" ")
const listProps = { const listProps = {
...props, ...props,
sort: options.sort, sort: options.sort,
allFiles: allPagesInFolder, allFiles: shownPages,
} }
const content = const content =
@ -90,7 +100,7 @@ export default ((opts?: Partial<FolderContentOptions>) => {
{options.showFolderCount && ( {options.showFolderCount && (
<p> <p>
{i18n(cfg.locale).pages.folderContent.itemsUnderFolder({ {i18n(cfg.locale).pages.folderContent.itemsUnderFolder({
count: allPagesInFolder.length, count: shownPages.length,
})} })}
</p> </p>
)} )}

View File

@ -74,13 +74,27 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (user
const allFiles = content.map((c) => c[1].data) const allFiles = content.map((c) => c[1].data)
const cfg = ctx.cfg.configuration const cfg = ctx.cfg.configuration
const folderNames: Record<SimpleSlug, string> = {}
const folders: Set<SimpleSlug> = new Set( const folders: Set<SimpleSlug> = new Set(
allFiles.flatMap((data) => { allFiles.flatMap((data) => {
return data.slug if (!data.slug || !data.relativePath) {
? _getFolders(data.slug).filter( return []
(folderName) => folderName !== "." && folderName !== "tags", }
) 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<Partial<FolderPageOptions>> = (user
folder, folder,
defaultProcessedContent({ defaultProcessedContent({
slug: joinSegments(folder, "index") as FullSlug, slug: joinSegments(folder, "index") as FullSlug,
relativePath: joinSegments(folderNames[folder], "index.html") as FilePath, // this is used by breadcrumbs
frontmatter: { frontmatter: {
title: `${i18n(cfg.locale).pages.folderContent.folder}: ${folder}`, title: `${i18n(cfg.locale).pages.folderContent.folder}: ${folderNames[folder]}`,
tags: [], tags: [],
}, },
}), }),
@ -100,7 +115,14 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (user
for (const [tree, file] of content) { for (const [tree, file] of content) {
const slug = stripSlashes(simplifySlug(file.data.slug!)) as SimpleSlug const slug = stripSlashes(simplifySlug(file.data.slug!)) as SimpleSlug
if (folders.has(slug)) { 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<Partial<FolderPageOptions>> = (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
}