Warning
本文档的这部分将假定您对TypeScript有实际了解,并将包含描述Quartz插件接口的代码片段。
Quartz的插件是对内容进行的一系列转换。下图展示了处理流程的示意图:
所有插件都被定义为一个函数,该函数接受一个选项参数type OptionType = object | undefined
,并返回一个与其插件类型对应的对象。
type OptionType = object | undefined
type QuartzPlugin<Options extends OptionType = undefined> = (opts?: Options) => QuartzPluginInstance
type QuartzPluginInstance =
| QuartzTransformerPluginInstance
| QuartzFilterPluginInstance
| QuartzEmitterPluginInstance
以下部分将详细介绍每种插件类型可以实现的方法。在此之前,我们先澄清几个较为模糊的类型:
BuildCtx
定义于quartz/ctx.ts
,包含:StaticResources
定义于quartz/resources.tsx
,包含:css
:需要加载的 CSS 样式定义列表。CSS 样式由同样定义在quartz/resources.tsx
中的CSSResource
类型描述,可接受样式表源 URL 或内联内容。js
:需要加载的脚本列表。脚本由同样定义在quartz/resources.tsx
中的JSResource
类型描述,允许定义加载时机(DOM 加载前或加载后)、是否为模块类型,以及脚本源 URL 或内联内容。
转换器
转换器对内容进行映射处理,接收 Markdown 文件后输出修改后的内容,或为文件本身添加元数据。
export type QuartzTransformerPluginInstance = {
name: string
textTransform?: (ctx: BuildCtx, src: string | Buffer) => string | Buffer
markdownPlugins?: (ctx: BuildCtx) => PluggableList
htmlPlugins?: (ctx: BuildCtx) => PluggableList
externalResources?: (ctx: BuildCtx) => Partial<StaticResources>
}
所有转换器插件必须至少定义一个 name
字段来注册插件,以及几个可选函数用于挂钩到单个 Markdown 文件转换的不同阶段:
textTransform
在文件被解析为 Markdown AST 之前 执行文本到文本的转换markdownPlugins
定义 remark 插件列表。remark
是一个以结构化方式转换 Markdown 到 Markdown 的工具htmlPlugins
定义 rehype 插件列表。与remark
类似,rehype
是以结构化方式转换 HTML 到 HTML 的工具externalResources
定义插件正常工作可能需要客户端加载的外部资源
通常情况下,对于 remark
和 rehype
都可以找到现有的插件直接使用。如果你想创建自己的 remark
或 rehype
插件,可以通过 unified
(底层 AST 解析器和转换库)的创建插件指南进行学习。
Latex 插件是一个很好的转换器插件示例,它借鉴了 remark
和 rehype
生态系统:
import remarkMath from "remark-math"
import rehypeKatex from "rehype-katex"
import rehypeMathjax from "rehype-mathjax/svg"
import { QuartzTransformerPlugin } from "../types"
interface Options {
renderEngine: "katex" | "mathjax"
}
export const Latex: QuartzTransformerPlugin<Options> = (opts?: Options) => {
const engine = opts?.renderEngine ?? "katex"
return {
name: "Latex",
markdownPlugins() {
return [remarkMath]
},
htmlPlugins() {
if (engine === "katex") {
// if you need to pass options into a plugin, you
// can use a tuple of [plugin, options]
return [[rehypeKatex, { output: "html" }]]
} else {
return [rehypeMathjax]
}
},
externalResources() {
if (engine === "katex") {
return {
css: [
{
// base css
content: "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css",
},
],
js: [
{
// fix copy behaviour: https://github.com/KaTeX/KaTeX/blob/main/contrib/copy-tex/README.md
src: "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/contrib/copy-tex.min.js",
loadTime: "afterDOMReady",
contentType: "external",
},
],
}
} else {
return {}
}
},
}
}
转换器插件的另一个常见功能是解析文件并为该文件添加额外数据:
export const AddWordCount: QuartzTransformerPlugin = () => {
return {
name: "AddWordCount",
markdownPlugins() {
return [
() => {
return (tree, file) => {
// tree is an `mdast` root element
// file is a `vfile`
const text = file.value
const words = text.split(" ").length
file.data.wordcount = words
}
},
]
},
}
}
// tell typescript about our custom data fields we are adding
// other plugins will then also be aware of this data field
declare module "vfile" {
interface DataMap {
wordcount: number
}
}
最后,你还可以使用 unist-util-visit
包中的 visit
函数或 mdast-util-find-and-replace
包中的 findAndReplace
函数对 Markdown 或 HTML 的抽象语法树(AST)进行转换操作。
export const TextTransforms: QuartzTransformerPlugin = () => {
return {
name: "TextTransforms",
markdownPlugins() {
return [() => {
return (tree, file) => {
// replace _text_ with the italics version
findAndReplace(tree, /_(.+)_/, (_value: string, ...capture: string[]) => {
// inner is the text inside of the () of the regex
const [inner] = capture
// return an mdast node
// https://github.com/syntax-tree/mdast
return {
type: "emphasis",
children: [{ type: 'text', value: inner }]
}
})
// remove all links (replace with just the link content)
// match by 'type' field on an mdast node
// https://github.com/syntax-tree/mdast#link in this example
visit(tree, "link", (link: Link) => {
return {
type: "paragraph"
children: [{ type: 'text', value: link.title }]
}
})
}
}]
}
}
}
所有转换器插件均位于 quartz/plugins/transformers
目录下。如果您决定编写自己的转换器插件,请记得在 quartz/plugins/transformers/index.ts
中重新导出该插件。
最后提醒:转换器插件较为复杂,如果初次接触不必着急。建议参考内置转换器的实现方式,观察它们如何处理内容,这将帮助您更好地理解如何实现所需功能。
过滤器
过滤器的主要作用是筛选内容,它会接收所有转换器的输出结果,并决定实际保留哪些文件、舍弃哪些文件。
export type QuartzFilterPlugin<Options extends OptionType = undefined> = (
opts?: Options,
) => QuartzFilterPluginInstance
export type QuartzFilterPluginInstance = {
name: string
shouldPublish(ctx: BuildCtx, content: ProcessedContent): boolean
}
过滤器插件必须定义一个 name
字段和一个 shouldPublish
函数。该函数接收经过所有转换器处理后的内容片段,并根据是否应该将其传递给发射器插件返回 true
或 false
。
例如,以下是用于移除草稿的内置插件:
const draftFilterPlugin = {
name: "draft-filter",
shouldPublish(content) {
return !content.frontmatter.draft;
},
};
import { QuartzFilterPlugin } from "../types"
export const RemoveDrafts: QuartzFilterPlugin<{}> = () => ({
name: "RemoveDrafts",
shouldPublish(_ctx, [_tree, vfile]) {
// uses frontmatter parsed from transformers
const draftFlag: boolean = vfile.data?.frontmatter?.draft ?? false
return !draftFlag
},
})
发射器
发射器对内容进行归约处理,接收所有经过转换和过滤的内容列表,并生成输出文件。
export type QuartzEmitterPlugin<Options extends OptionType = undefined> = (
opts?: Options,
) => QuartzEmitterPluginInstance
export type QuartzEmitterPluginInstance = {
name: string
emit(ctx: BuildCtx, content: ProcessedContent[], resources: StaticResources): Promise<FilePath[]>
getQuartzComponents(ctx: BuildCtx): QuartzComponent[]
}
发射器插件必须定义以下内容:一个 name
字段、一个 emit
函数和一个 getQuartzComponents
函数。其中 emit
函数负责检查所有解析和过滤后的内容,并据此创建文件,最后返回该插件创建的文件路径列表。
创建新文件可以通过常规的 Node fs 模块(例如使用 fs.cp
或 fs.writeFile
)实现。若创建的是包含文本内容的文件,也可以通过 quartz/plugins/emitters/helpers.ts
中的 write
函数来实现。write
函数的签名如下:
export type WriteOptions = (data: {
// the build context
ctx: BuildCtx
// the name of the file to emit (not including the file extension)
slug: ServerSlug
// the file extension
ext: `.${string}` | ""
// the file content to add
content: string
}) => Promise<FilePath>
这是一个轻量级封装,主要用于写入正确的输出文件夹并确保中间目录存在。如果您选择使用原生的 Node fs
API,请确保也将内容输出到 argv.output
文件夹。
如果您需要创建一个需要渲染组件的发射器插件,还需要注意以下三点:
- 您的组件应使用
getQuartzComponents
来声明用于构建页面的QuartzComponents
列表。更多信息请参阅创建组件页面。 - 您可以使用
quartz/components/renderPage.tsx
中定义的renderPage
函数将 Quartz 组件渲染为 HTML。 - 如果需要将 HTML AST 渲染为 JSX,可以使用
quartz/util/jsx.ts
中的htmlToJsx
函数。相关示例可在quartz/components/pages/Content.tsx
中找到。
例如,以下是渲染每个页面的内容页插件的简化版本。
export const ContentPage: QuartzEmitterPlugin = () => {
// construct the layout
const layout: FullPageLayout = {
...sharedPageComponents,
...defaultContentPageLayout,
pageBody: Content(),
}
const { head, header, beforeBody, pageBody, afterBody, left, right, footer } = layout
return {
name: "ContentPage",
getQuartzComponents() {
return [head, ...header, ...beforeBody, pageBody, ...afterBody, ...left, ...right, footer]
},
async emit(ctx, content, resources, emit): Promise<FilePath[]> {
const cfg = ctx.cfg.configuration
const fps: FilePath[] = []
const allFiles = content.map((c) => c[1].data)
for (const [tree, file] of content) {
const slug = canonicalizeServer(file.data.slug!)
const externalResources = pageResources(slug, file.data, resources)
const componentData: QuartzComponentProps = {
fileData: file.data,
externalResources,
cfg,
children: [],
tree,
allFiles,
}
const content = renderPage(cfg, slug, componentData, opts, externalResources)
const fp = await emit({
content,
slug: file.data.slug!,
ext: ".html",
})
fps.push(fp)
}
return fps
},
}
}
注意,它接受一个 FullPageLayout
作为选项。该布局由通过 quartz.layout.ts
文件提供的 SharedLayout
和 PageLayout
组合而成。
[!提示] 查看
quartz/plugins
目录以获取 Quartz 中更多插件示例,作为您自己开发插件时的参考!