Compare commits
6 Commits
v4.5.0
...
jackyzha0/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bb6f09db1 | ||
|
|
40a72eba44 | ||
|
|
bdc15ecb05 | ||
|
|
5ccb9ddc70 | ||
|
|
7681a86815 | ||
|
|
f528d6139e |
@@ -1,4 +1,4 @@
|
|||||||
import { FullSlug, isFolderPath, resolveRelative } from "../util/path"
|
import { FullSlug, resolveRelative } from "../util/path"
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
import { Date, getDate } from "./Date"
|
import { Date, getDate } from "./Date"
|
||||||
import { QuartzComponent, QuartzComponentProps } from "./types"
|
import { QuartzComponent, QuartzComponentProps } from "./types"
|
||||||
@@ -8,13 +8,6 @@ export type SortFn = (f1: QuartzPluginData, f2: QuartzPluginData) => number
|
|||||||
|
|
||||||
export function byDateAndAlphabetical(cfg: GlobalConfiguration): SortFn {
|
export function byDateAndAlphabetical(cfg: GlobalConfiguration): SortFn {
|
||||||
return (f1, f2) => {
|
return (f1, f2) => {
|
||||||
// Sort folders first
|
|
||||||
const f1IsFolder = isFolderPath(f1.slug ?? "")
|
|
||||||
const f2IsFolder = isFolderPath(f2.slug ?? "")
|
|
||||||
if (f1IsFolder && !f2IsFolder) return -1
|
|
||||||
if (!f1IsFolder && f2IsFolder) return 1
|
|
||||||
|
|
||||||
// If both are folders or both are files, sort by date/alphabetical
|
|
||||||
if (f1.dates && f2.dates) {
|
if (f1.dates && f2.dates) {
|
||||||
// sort descending
|
// sort descending
|
||||||
return getDate(cfg, f2)!.getTime() - getDate(cfg, f1)!.getTime()
|
return getDate(cfg, f2)!.getTime() - getDate(cfg, f1)!.getTime()
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
import style from "../styles/listPage.scss"
|
import style from "../styles/listPage.scss"
|
||||||
import { PageList, SortFn } from "../PageList"
|
import { byDateAndAlphabetical, PageList, SortFn } from "../PageList"
|
||||||
|
import { stripSlashes, simplifySlug, joinSegments, FullSlug } from "../../util/path"
|
||||||
import { Root } from "hast"
|
import { Root } from "hast"
|
||||||
import { htmlToJsx } from "../../util/jsx"
|
import { htmlToJsx } from "../../util/jsx"
|
||||||
import { i18n } from "../../i18n"
|
import { i18n } from "../../i18n"
|
||||||
import { QuartzPluginData } from "../../plugins/vfile"
|
import { QuartzPluginData } from "../../plugins/vfile"
|
||||||
import { ComponentChildren } from "preact"
|
import { ComponentChildren } from "preact"
|
||||||
import { concatenateResources } from "../../util/resources"
|
import { concatenateResources } from "../../util/resources"
|
||||||
import { FileTrieNode } from "../../util/fileTrie"
|
|
||||||
interface FolderContentOptions {
|
interface FolderContentOptions {
|
||||||
/**
|
/**
|
||||||
* Whether to display number of folders
|
* Whether to display number of folders
|
||||||
@@ -25,88 +27,51 @@ const defaultOptions: FolderContentOptions = {
|
|||||||
|
|
||||||
export default ((opts?: Partial<FolderContentOptions>) => {
|
export default ((opts?: Partial<FolderContentOptions>) => {
|
||||||
const options: FolderContentOptions = { ...defaultOptions, ...opts }
|
const options: FolderContentOptions = { ...defaultOptions, ...opts }
|
||||||
let trie: FileTrieNode<
|
|
||||||
QuartzPluginData & {
|
|
||||||
slug: string
|
|
||||||
title: string
|
|
||||||
filePath: string
|
|
||||||
}
|
|
||||||
>
|
|
||||||
|
|
||||||
const FolderContent: QuartzComponent = (props: QuartzComponentProps) => {
|
const FolderContent: QuartzComponent = (props: QuartzComponentProps) => {
|
||||||
const { tree, fileData, allFiles, cfg } = props
|
const { tree, fileData, allFiles, cfg } = props
|
||||||
|
const folderSlug = stripSlashes(simplifySlug(fileData.slug!))
|
||||||
|
const folderParts = folderSlug.split(path.posix.sep)
|
||||||
|
|
||||||
|
const allPagesInFolder: QuartzPluginData[] = []
|
||||||
|
const allPagesInSubfolders: Map<FullSlug, QuartzPluginData[]> = new Map()
|
||||||
|
|
||||||
if (!trie) {
|
|
||||||
trie = new FileTrieNode([])
|
|
||||||
allFiles.forEach((file) => {
|
allFiles.forEach((file) => {
|
||||||
if (file.frontmatter) {
|
const fileSlug = stripSlashes(simplifySlug(file.slug!))
|
||||||
trie.add({
|
const prefixed = fileSlug.startsWith(folderSlug) && fileSlug !== folderSlug
|
||||||
...file,
|
const fileParts = fileSlug.split(path.posix.sep)
|
||||||
slug: file.slug!,
|
const isDirectChild = fileParts.length === folderParts.length + 1
|
||||||
title: file.frontmatter.title,
|
|
||||||
filePath: file.filePath!,
|
if (!prefixed) {
|
||||||
})
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDirectChild) {
|
||||||
|
allPagesInFolder.push(file)
|
||||||
|
} else if (options.showSubfolders) {
|
||||||
|
const subfolderSlug = joinSegments(
|
||||||
|
...fileParts.slice(0, folderParts.length + 1),
|
||||||
|
) as FullSlug
|
||||||
|
const pagesInFolder = allPagesInSubfolders.get(subfolderSlug) || []
|
||||||
|
allPagesInSubfolders.set(subfolderSlug, [...pagesInFolder, file])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
const folder = trie.findNode(fileData.slug!.split("/"))
|
allPagesInSubfolders.forEach((files, subfolderSlug) => {
|
||||||
if (!folder) {
|
const hasIndex = allPagesInFolder.some(
|
||||||
return null
|
(file) => subfolderSlug === stripSlashes(simplifySlug(file.slug!)),
|
||||||
}
|
|
||||||
|
|
||||||
const allPagesInFolder: QuartzPluginData[] =
|
|
||||||
folder.children
|
|
||||||
.map((node) => {
|
|
||||||
// regular file, proceed
|
|
||||||
if (node.data) {
|
|
||||||
return node.data
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.isFolder && options.showSubfolders) {
|
|
||||||
// folders that dont have data need synthetic files
|
|
||||||
const getMostRecentDates = (): QuartzPluginData["dates"] => {
|
|
||||||
let maybeDates: QuartzPluginData["dates"] | undefined = undefined
|
|
||||||
for (const child of node.children) {
|
|
||||||
if (child.data?.dates) {
|
|
||||||
// compare all dates and assign to maybeDates if its more recent or its not set
|
|
||||||
if (!maybeDates) {
|
|
||||||
maybeDates = child.data.dates
|
|
||||||
} else {
|
|
||||||
if (child.data.dates.created > maybeDates.created) {
|
|
||||||
maybeDates.created = child.data.dates.created
|
|
||||||
}
|
|
||||||
|
|
||||||
if (child.data.dates.modified > maybeDates.modified) {
|
|
||||||
maybeDates.modified = child.data.dates.modified
|
|
||||||
}
|
|
||||||
|
|
||||||
if (child.data.dates.published > maybeDates.published) {
|
|
||||||
maybeDates.published = child.data.dates.published
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
maybeDates ?? {
|
|
||||||
created: new Date(),
|
|
||||||
modified: new Date(),
|
|
||||||
published: new Date(),
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
if (!hasIndex) {
|
||||||
|
const subfolderDates = files.sort(byDateAndAlphabetical(cfg))[0].dates
|
||||||
return {
|
const subfolderTitle = subfolderSlug.split(path.posix.sep).at(-1)!
|
||||||
slug: node.slug,
|
allPagesInFolder.push({
|
||||||
dates: getMostRecentDates(),
|
slug: subfolderSlug,
|
||||||
frontmatter: {
|
dates: subfolderDates,
|
||||||
title: node.displayName,
|
frontmatter: { title: subfolderTitle, tags: ["folder"] },
|
||||||
tags: [],
|
})
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter((page) => page !== undefined) ?? []
|
|
||||||
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
|
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
|
||||||
const classes = cssClasses.join(" ")
|
const classes = cssClasses.join(" ")
|
||||||
const listProps = {
|
const listProps = {
|
||||||
|
|||||||
@@ -134,9 +134,9 @@ function createFolderNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const child of node.children) {
|
for (const child of node.children) {
|
||||||
const childNode = child.isFolder
|
const childNode = child.data
|
||||||
? createFolderNode(currentSlug, child, opts)
|
? createFileNode(currentSlug, child)
|
||||||
: createFileNode(currentSlug, child)
|
: createFolderNode(currentSlug, child, opts)
|
||||||
ul.appendChild(childNode)
|
ul.appendChild(childNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,8 +52,6 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button.mobile-explorer {
|
button.mobile-explorer {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import test, { describe, beforeEach } from "node:test"
|
import test, { describe, beforeEach } from "node:test"
|
||||||
import assert from "node:assert"
|
import assert from "node:assert"
|
||||||
import { FileTrieNode } from "./fileTrie"
|
import { FileTrieNode } from "./fileTrie"
|
||||||
import { FullSlug } from "./path"
|
|
||||||
|
|
||||||
interface TestData {
|
interface TestData {
|
||||||
title: string
|
title: string
|
||||||
@@ -193,94 +192,6 @@ describe("FileTrie", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("fromEntries", () => {
|
|
||||||
test("nested", () => {
|
|
||||||
const trie = FileTrieNode.fromEntries([
|
|
||||||
["index" as FullSlug, { title: "Root", slug: "index", filePath: "index.md" }],
|
|
||||||
[
|
|
||||||
"folder/file1" as FullSlug,
|
|
||||||
{ title: "File 1", slug: "folder/file1", filePath: "folder/file1.md" },
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"folder/index" as FullSlug,
|
|
||||||
{ title: "Folder Index", slug: "folder/index", filePath: "folder/index.md" },
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"folder/file2" as FullSlug,
|
|
||||||
{ title: "File 2", slug: "folder/file2", filePath: "folder/file2.md" },
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"folder/folder2/index" as FullSlug,
|
|
||||||
{
|
|
||||||
title: "Subfolder Index",
|
|
||||||
slug: "folder/folder2/index",
|
|
||||||
filePath: "folder/folder2/index.md",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
])
|
|
||||||
|
|
||||||
assert.strictEqual(trie.children.length, 1)
|
|
||||||
assert.strictEqual(trie.children[0].slug, "folder/index")
|
|
||||||
assert.strictEqual(trie.children[0].children.length, 3)
|
|
||||||
assert.strictEqual(trie.children[0].children[0].slug, "folder/file1")
|
|
||||||
assert.strictEqual(trie.children[0].children[1].slug, "folder/file2")
|
|
||||||
assert.strictEqual(trie.children[0].children[2].slug, "folder/folder2/index")
|
|
||||||
assert.strictEqual(trie.children[0].children[2].children.length, 0)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("findNode", () => {
|
|
||||||
test("should find root node with empty path", () => {
|
|
||||||
const data = { title: "Root", slug: "index", filePath: "index.md" }
|
|
||||||
trie.add(data)
|
|
||||||
const found = trie.findNode([])
|
|
||||||
assert.strictEqual(found, trie)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should find node at first level", () => {
|
|
||||||
const data = { title: "Test", slug: "test", filePath: "test.md" }
|
|
||||||
trie.add(data)
|
|
||||||
const found = trie.findNode(["test"])
|
|
||||||
assert.strictEqual(found?.data, data)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should find nested node", () => {
|
|
||||||
const data = {
|
|
||||||
title: "Nested",
|
|
||||||
slug: "folder/subfolder/test",
|
|
||||||
filePath: "folder/subfolder/test.md",
|
|
||||||
}
|
|
||||||
trie.add(data)
|
|
||||||
const found = trie.findNode(["folder", "subfolder", "test"])
|
|
||||||
assert.strictEqual(found?.data, data)
|
|
||||||
|
|
||||||
// should find the folder and subfolder indexes too
|
|
||||||
assert.strictEqual(
|
|
||||||
trie.findNode(["folder", "subfolder", "index"]),
|
|
||||||
trie.children[0].children[0],
|
|
||||||
)
|
|
||||||
assert.strictEqual(trie.findNode(["folder", "index"]), trie.children[0])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should return undefined for non-existent path", () => {
|
|
||||||
const data = { title: "Test", slug: "test", filePath: "test.md" }
|
|
||||||
trie.add(data)
|
|
||||||
const found = trie.findNode(["nonexistent"])
|
|
||||||
assert.strictEqual(found, undefined)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should return undefined for partial path", () => {
|
|
||||||
const data = {
|
|
||||||
title: "Nested",
|
|
||||||
slug: "folder/subfolder/test",
|
|
||||||
filePath: "folder/subfolder/test.md",
|
|
||||||
}
|
|
||||||
trie.add(data)
|
|
||||||
const found = trie.findNode(["folder"])
|
|
||||||
assert.strictEqual(found?.data, null)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("getFolderPaths", () => {
|
describe("getFolderPaths", () => {
|
||||||
test("should return all folder paths", () => {
|
test("should return all folder paths", () => {
|
||||||
const data1 = {
|
const data1 = {
|
||||||
|
|||||||
@@ -89,14 +89,6 @@ export class FileTrieNode<T extends FileTrieData = ContentDetails> {
|
|||||||
this.insert(file.slug.split("/"), file)
|
this.insert(file.slug.split("/"), file)
|
||||||
}
|
}
|
||||||
|
|
||||||
findNode(path: string[]): FileTrieNode<T> | undefined {
|
|
||||||
if (path.length === 0 || (path.length === 1 && path[0] === "index")) {
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.children.find((c) => c.slugSegment === path[0])?.findNode(path.slice(1))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter trie nodes. Behaves similar to `Array.prototype.filter()`, but modifies tree in place
|
* Filter trie nodes. Behaves similar to `Array.prototype.filter()`, but modifies tree in place
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ export function transformLink(src: FullSlug, target: string, opts: TransformOpti
|
|||||||
}
|
}
|
||||||
|
|
||||||
// path helpers
|
// path helpers
|
||||||
export function isFolderPath(fplike: string): boolean {
|
function isFolderPath(fplike: string): boolean {
|
||||||
return (
|
return (
|
||||||
fplike.endsWith("/") ||
|
fplike.endsWith("/") ||
|
||||||
endsWith(fplike, "index") ||
|
endsWith(fplike, "index") ||
|
||||||
|
|||||||
Reference in New Issue
Block a user