Quartz 内置一个资源管理器组件,可让您浏览站点上的所有文件和文件夹。它支持嵌套文件夹结构,并具有高度可定制性。

默认情况下,资源管理器会显示页面上的所有文件夹和文件。若要在不同位置显示资源管理器,可以编辑layout布局文件。

文件夹的显示名称由folder/index.md中的title前端元数据字段决定(更多细节详见创建内容)。如果该文件不存在或未包含前端元数据,则会使用本地文件夹名称替代。

Info

资源管理器默认使用本地存储来保存状态,这是为了确保在不同页面间导航时的流畅体验。

要清除本地存储中的资源管理器状态,请删除fileTree条目(基于Chromium的浏览器中删除本地存储键值的指南可参考此处)。您可以通过传入参数useSavedState: false来禁用此功能。

自定义配置

大部分配置可以通过向Component.Explorer()传递选项来实现。

例如,以下是默认配置的示例:

quartz.layout.ts
Component.Explorer({
  title: "Explorer", // title of the explorer component
  folderClickBehavior: "collapse", // what happens when you click a folder ("link" to navigate to folder page on click or "collapse" to collapse folder on click)
  folderDefaultState: "collapsed", // default state of folders ("collapsed" or "open")
  useSavedState: true, // whether to use local storage to save "state" (which folders are opened) of explorer
  // Sort order: folders first, then files. Sort folders and files alphabetically
  sortFn: (a, b) => {
    ... // default implementation shown later
  },
  filterFn: filterFn: (node) => node.name !== "tags", // filters out 'tags' folder
  mapFn: undefined,
  // what order to apply functions in
  order: ["filter", "map", "sort"],
})

当传入自定义选项时,如果您希望保留某个字段的默认值,可以省略该字段的全部或部分配置。

想要进行更深入的定制?

  • 移除资源管理器:在 quartz.layout.ts 中删除 Component.Explorer()
    • (可选):移除资源管理器组件后,您可以将Table of Contents组件移回布局的 left 部分
  • 修改排序、过滤和映射行为:详见高级定制
  • 组件构成
    • 包装器组件(外层组件,生成文件树等):quartz/components/Explorer.tsx
    • 资源管理器节点(递归结构,可以是文件夹或文件):quartz/components/ExplorerNode.tsx
  • 样式quartz/components/styles/explorer.scss
  • 脚本quartz/components/scripts/explorer.inline.ts

高级定制

该组件允许您完全自定义其所有行为。您可以传入自定义的 sort(排序)、filter(过滤)和 map(映射)函数。 所有可传入的函数都作用于 FileNode 类,该类具有以下属性:

quartz/components/ExplorerNode.tsx
export class FileNode {
  children: FileNode[]  // children of current node
  name: string  // last part of slug
  displayName: string // what actually should be displayed in the explorer
  file: QuartzPluginData | null // if node is a file, this is the file's metadata. See `QuartzPluginData` for more detail
  depth: number // depth of current node
 
  ... // rest of implementation
}

所有可传递的函数均为可选。默认情况下,仅会使用 sort 函数:

Default sort function
// Sort order: folders first, then files. Sort folders and files alphabetically
Component.Explorer({
  sortFn: (a, b) => {
    if ((!a.file && !b.file) || (a.file && b.file)) {
      // sensitivity: "base": Only strings that differ in base letters compare as unequal. Examples: a ≠ b, a = á, a = A
      // numeric: true: Whether numeric collation should be used, such that "1" < "2" < "10"
      return a.displayName.localeCompare(b.displayName, undefined, {
        numeric: true,
        sensitivity: "base",
      })
    }
    if (a.file && !b.file) {
      return 1
    } else {
      return -1
    }
  },
})

你可以为 sortFnfilterFnmapFn 传递自定义函数。所有函数都将按照 order 选项指定的顺序执行(详见自定义)。这些函数的行为类似于它们对应的 Array.prototype 方法,不同之处在于它们会直接修改整个 FileNode 树,而不是返回新的树。

如需了解如何使用 sortfiltermap 的更多信息,可参考:

类型定义如下所示:

interface FileNode {
  name: string
  children?: FileNode[]
  meta?: {
    [key: string]: any
    isRoot?: boolean
    isDirectory?: boolean
  }
}
sortFn: (a: FileNode, b: FileNode) => number
filterFn: (node: FileNode) => boolean
mapFn: (node: FileNode) => void

Tip

你可以像这样检查一个FileNode是文件夹还是文件:

if (node.file) {
  // node 是文件
} else {
  // node 是文件夹
}

基础示例

这些示例展示了sortmapfilter的基本用法。

使用sort将文件排在前面

通过这个示例,资源管理器会按字母顺序排序所有内容,但将所有文件排列在文件夹之上。

quartz.layout.ts
Component.Explorer({
  sortFn: (a, b) => {
    if ((!a.file && !b.file) || (a.file && b.file)) {
      return a.displayName.localeCompare(b.displayName)
    }
    if (a.file && !b.file) {
      return -1
    } else {
      return 1
    }
  },
})

更改显示名称(map

使用此示例,所有 FileNodes(文件夹 + 文件)的显示名称将被转换为全大写。

const transformedTree = {
  name: "root",
  children: [
    { name: "folder1", children: [{ name: "file1.txt" }] },
    { name: "folder2", children: [{ name: "file2.txt" }] },
  ],
}
 
const result = map(transformedTree, (node) => ({
  ...node,
  displayName: node.name.toUpperCase(),
}))
quartz.layout.ts
Component.Explorer({
  mapFn: (node) => {
    node.displayName = node.displayName.toUpperCase()
  },
})

移除元素列表 (filter)

通过此示例,您可以通过使用 omit 集合提供文件夹/文件数组来从资源管理器中移除元素。

quartz.layout.ts
Component.Explorer({
  filterFn: (node) => {
    // set containing names of everything you want to filter out
    const omit = new Set(["authoring content", "tags", "hosting"])
    return !omit.has(node.name.toLowerCase())
  },
})

您可以通过修改 omit 集合中的条目来自定义此功能。只需添加所有要移除的文件夹或文件名即可。

按标签移除文件

您可以通过 node.file?.frontmatter? 访问文件的 frontmatter。这样您就可以根据 frontmatter 来筛选文件,例如根据标签进行筛选。

quartz.layout.ts
Component.Explorer({
  filterFn: (node) => {
    // exclude files with the tag "explorerexclude"
    return node.file?.frontmatter?.tags?.includes("explorerexclude") !== true
  },
})

在资源管理器中显示所有元素

要覆盖默认过滤函数(该函数会从资源管理器中移除 tags 文件夹),可以将过滤函数设置为 undefined

quartz.layout.ts
Component.Explorer({
  filterFn: undefined, // apply no filter function, every file and folder will visible
})

Advanced examples

Tip

When writing more complicated functions, the layout file can start to look very cramped. You can fix this by defining your functions in another file.

functions.ts
import { Options } from "./quartz/components/ExplorerNode"
export const mapFn: Options["mapFn"] = (node) => {
  // implement your function here
}
export const filterFn: Options["filterFn"] = (node) => {
  // implement your function here
}
export const sortFn: Options["sortFn"] = (a, b) => {
  // implement your function here
}

You can then import them like this:

quartz.layout.ts
import { mapFn, filterFn, sortFn } from "./functions.ts"
Component.Explorer({
  mapFn: mapFn,
  filterFn: filterFn,
  sortFn: sortFn,
})

Add emoji prefix

To add emoji prefixes (📁 for folders, 📄 for files), you could use a map function like this:

quartz.layout.ts
Component.Explorer({
  mapFn: (node) => {
    // dont change name of root node
    if (node.depth > 0) {
      // set emoji for file/folder
      if (node.file) {
        node.displayName = "📄 " + node.displayName
      } else {
        node.displayName = "📁 " + node.displayName
      }
    }
  },
})

综合应用

在本示例中,我们将通过组合使用前文示例中的功能来定制文件浏览器:添加表情符号前缀过滤特定文件夹以及排序时将文件放在文件夹前面

quartz.layout.ts
Component.Explorer({
  filterFn: sampleFilterFn,
  mapFn: sampleMapFn,
  sortFn: sampleSortFn,
  order: ["filter", "sort", "map"],
})

注意我们在此处如何自定义 order 数组。这样做是因为默认排序会将 sort 函数最后应用。虽然这通常效果良好,但在此处会导致意外行为,因为我们修改了所有显示名称的首字符。在我们的示例中,sort 将基于表情符号前缀而非第一个_实际_字符进行排序。

为解决这个问题,我们调整了顺序,在 map 函数修改显示名称之前先应用 sort 函数。

使用预定义排序规则的 sort 函数

以下是另一个示例,其中使用包含文件/文件夹名称(作为 slug)的映射来定义 Quartz 资源管理器的排序顺序。所有未在 nameOrderMap 中列出的文件/文件夹将出现在该文件夹层级的顶部。

值得一提的是,nameOrderMap 中设置的数字越小,该条目在资源管理器中的位置就越靠前。通过将每个文件夹/文件递增 100,可以更轻松地在文件夹内排序文件。最后,此示例仍允许您使用 mapFn 或 frontmatter 标题来更改显示名称,因为它使用 slug 作为 nameOrderMap 的基准(这不会受显示名称更改的影响)。

quartz.layout.ts
Component.Explorer({
  sortFn: (a, b) => {
    const nameOrderMap: Record<string, number> = {
      "poetry-folder": 100,
      "essay-folder": 200,
      "research-paper-file": 201,
      "dinosaur-fossils-file": 300,
      "other-folder": 400,
    }
 
    let orderA = 0
    let orderB = 0
 
    if (a.file && a.file.slug) {
      orderA = nameOrderMap[a.file.slug] || 0
    } else if (a.name) {
      orderA = nameOrderMap[a.name] || 0
    }
 
    if (b.file && b.file.slug) {
      orderB = nameOrderMap[b.file.slug] || 0
    } else if (b.name) {
      orderB = nameOrderMap[b.name] || 0
    }
 
    return orderA - orderB
  },
})

作为参考,以下示例展示了石英资源管理器窗口的显示效果:

📖 Poetry Folder
📑 Essay Folder
    ⚗️ Research Paper File
🦴 Dinosaur Fossils File
🔮 Other Folder

文件结构如下所示:

index.md
poetry-folder
    index.md
essay-folder
    index.md
    research-paper-file.md
dinosaur-fossils-file.md
other-folder
    index.md