From 0c199975f2d469ecdfd7efcf2ddd16ffa1dc492b Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Thu, 17 Aug 2023 07:55:28 +0000
Subject: [PATCH] various path fixes for links to extensions, fix relative paths in links
---
content/advanced/making plugins.md | 294 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 files changed, 288 insertions(+), 6 deletions(-)
diff --git a/content/advanced/making plugins.md b/content/advanced/making plugins.md
index bdd9194..1f1616f 100644
--- a/content/advanced/making plugins.md
+++ b/content/advanced/making plugins.md
@@ -2,19 +2,301 @@
title: Making your own plugins
---
-This part of the documentation will assume you have some basic coding knowledge and will include code snippets that describe the interface of what Quartz plugins should look like.
+> [!warning]
+> This part of the documentation will assume you have working knowledge in TypeScript and will include code snippets that describe the interface of what Quartz plugins should look like.
+
+Quartz's plugins are a series of transformations over content. This is illustrated in the diagram of the processing pipeline below:
+
+![[quartz transform pipeline.png]]
+
+All plugins are defined as a function that takes in a single parameter for options `type OptionType = object | undefined` and return an object that corresponds to the type of plugin it is.
+
+```ts
+type OptionType = object | undefined
+type QuartzPlugin<Options extends OptionType = undefined> = (opts?: Options) => QuartzPluginInstance
+type QuartzPluginInstance =
+ | QuartzTransformerPluginInstance
+ | QuartzFilterPluginInstance
+ | QuartzEmitterPluginInstance
+```
+
+The following sections will go into detail for what methods can be implemented for each plugin type. Before we do that, let's clarify a few more ambiguous types:
+
+- `BuildCtx` is defined in `quartz/ctx.ts`. It consists of
+ - `argv`: The command line arguments passed to the Quartz [[build]] command
+ - `cfg`: The full Quartz [[configuration]]
+ - `allSlugs`: a list of all the valid content slugs (see [[paths]] for more information on what a `ServerSlug` is)
+- `StaticResources` is defined in `quartz/resources.tsx`. It consists of
+ - `css`: a list of URLs for stylesheets that should be loaded
+ - `js`: a list of scripts that should be loaded. A script is described with the `JSResource` type which is also defined in `quartz/resources.tsx`. It allows you to define a load time (either before or after the DOM has been loaded), whether it should be a module, and either the source URL or the inline content of the script.
## Transformers
+
+Transformers **map** over content, taking a Markdown file and outputting modified content or adding metadata to the file itself.
+
```ts
export type QuartzTransformerPluginInstance = {
name: string
- textTransform?: (src: string | Buffer) => string | Buffer
- markdownPlugins?: () => PluggableList
- htmlPlugins?: () => PluggableList
- externalResources?: () => Partial<StaticResources>
+ textTransform?: (ctx: BuildCtx, src: string | Buffer) => string | Buffer
+ markdownPlugins?: (ctx: BuildCtx) => PluggableList
+ htmlPlugins?: (ctx: BuildCtx) => PluggableList
+ externalResources?: (ctx: BuildCtx) => Partial<StaticResources>
}
```
+All transformer plugins must define at least a `name` field to register the plugin and a few optional functions that allow you to hook into various parts of transforming a single Markdown file.
+
+- `textTransform` performs a text-to-text transformation _before_ a file is parsed into the [Markdown AST](https://github.com/syntax-tree/mdast).
+- `markdownPlugins` defines a list of [remark plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md). `remark` is a tool that transforms Markdown to Markdown in a structured way.
+- `htmlPlugins` defines a list of [rehype plugins](https://github.com/rehypejs/rehype/blob/main/doc/plugins.md). Similar to how `remark` works, `rehype` is a tool that transforms HTML to HTML in a structured way.
+- `externalResources` defines any external resources the plugin may need to load on the client-side for it to work properly.
+
+Normally for both `remark` and `rehype`, you can find existing plugins that you can use to . If you'd like to create your own `remark` or `rehype` plugin, checkout the [guide to creating a plugin](https://unifiedjs.com/learn/guide/create-a-plugin/) using `unified` (the underlying AST parser and transformer library).
+
+A good example of a transformer plugin that borrows from the `remark` and `rehype` ecosystems is the [[Latex]] plugin:
+
+```ts title="quartz/plugins/transformers/latex.ts"
+import remarkMath from "remark-math"
+import rehypeKatex from "rehype-katex"
+import rehypeMathjax from "rehype-mathjax/svg.js"
+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: ["https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css"],
+ js: [
+ {
+ src: "https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/contrib/copy-tex.min.js",
+ loadTime: "afterDOMReady",
+ contentType: "external",
+ },
+ ],
+ }
+ } else {
+ return {}
+ }
+ },
+ }
+}
+```
+
+Another common thing that transformer plugins will do is parse a file and add extra data for that file:
+
+```ts
+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
+ }
+}
+```
+
+Finally, you can also perform transformations over Markdown or HTML ASTs using the `visit` function from the `unist-util-visit` package or the `findAndReplace` function from the `mdast-util-find-and-replace` package.
+
+```ts
+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 }]
+ }
+ })
+ }
+ }]
+ }
+ }
+}
+```
+
+All transformer plugins can be found under `quartz/plugins/transformers`. If you decide to write your own transformer plugin, don't forget to re-export it under `quartz/plugins/transformers/index.ts`
+
+A parting word: transformer plugins are quite complex so don't worry if you don't get them right away. Take a look at the built in transformers and see how they operate over content to get a better sense for how to accomplish what you are trying to do.
+
## Filters
-## Emitters
\ No newline at end of file
+Filters **filter** content, taking the output of all the transformers and determining what files to actually keep and what to discard.
+
+```ts
+export type QuartzFilterPlugin<Options extends OptionType = undefined> = (
+ opts?: Options,
+) => QuartzFilterPluginInstance
+
+export type QuartzFilterPluginInstance = {
+ name: string
+ shouldPublish(ctx: BuildCtx, content: ProcessedContent): boolean
+}
+```
+
+A filter plugin must define a `name` field and a `shouldPublish` function that takes in a piece of content that has been processed by all the transformers and returns a `true` or `false` depending on whether it should be passed to the emitter plugins or not.
+
+For example, here is the built-in plugin for removing drafts:
+
+```ts title="quartz/plugins/filters/draft.ts"
+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
+ },
+})
+```
+
+## Emitters
+
+Emitters **reduce** over content, taking in a list of all the transformed and filtered content and creating output files.
+
+```ts
+export type QuartzEmitterPlugin<Options extends OptionType = undefined> = (
+ opts?: Options,
+) => QuartzEmitterPluginInstance
+
+export type QuartzEmitterPluginInstance = {
+ name: string
+ emit(
+ ctx: BuildCtx,
+ content: ProcessedContent[],
+ resources: StaticResources,
+ emitCallback: EmitCallback,
+ ): Promise<FilePath[]>
+ getQuartzComponents(ctx: BuildCtx): QuartzComponent[]
+}
+```
+
+An emitter plugin must define a `name` field an `emit` function and a `getQuartzComponents` function. `emit` is responsible for looking at all the parsed and filtered content and then appropriately creating files and returning a list of paths to files the plugin created.
+
+Creating new files can be done via regular Node [fs module](https://nodejs.org/api/fs.html) (i.e. `fs.cp` or `fs.writeFile`) or via the `emitCallback` if you are creating files that contain text. The `emitCallback` function is the 4th argument of the emit function. It's interface looks something like this:
+
+```ts
+export type EmitCallback = (data: {
+ // 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>
+```
+
+This is a thin wrapper around writing to the appropriate output folder and ensuring that intermediate directories exist. If you choose to use the native Node `fs` APIs, ensure you emit to the `argv.output` folder as well.
+
+If you are creating an emitter plugin that needs to render components, there are three more things to be aware of:
+
+- Your component should use `getQuartzComponents` to declare a list of `QuartzComponents` that it uses to construct the page. See the page on [[creating components]] for more information.
+- You can use the `renderPage` function defined in `quartz/components/renderPage.tsx` to render Quartz components into HTML.
+- If you need to render an HTML AST to JSX, you can use the `toJsxRuntime` function from `hast-util-to-jsx-runtime` library. An example of this can be found in `quartz/components/pages/Content.tsx`.
+
+For example, the following is a simplified version of the content page plugin that renders every single page.
+
+```tsx title="quartz/plugins/emitters/contentPage.tsx"
+export const ContentPage: QuartzEmitterPlugin = () => {
+ // construct the layout
+ const layout: FullPageLayout = {
+ ...sharedPageComponents,
+ ...defaultContentPageLayout,
+ pageBody: Content(),
+ }
+ const { head, header, beforeBody, pageBody, left, right, footer } = layout
+ return {
+ name: "ContentPage",
+ getQuartzComponents() {
+ return [head, ...header, ...beforeBody, pageBody, ...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, resources)
+ const componentData: QuartzComponentProps = {
+ fileData: file.data,
+ externalResources,
+ cfg,
+ children: [],
+ tree,
+ allFiles,
+ }
+
+ const content = renderPage(slug, componentData, opts, externalResources)
+ const fp = await emit({
+ content,
+ slug: file.data.slug!,
+ ext: ".html",
+ })
+
+ fps.push(fp)
+ }
+ return fps
+ },
+ }
+}
+```
+
+Note that it takes in a `FullPageLayout` as the options. It's made by combining a `SharedLayout` and a `PageLayout` both of which are provided through the `quartz.layout.ts` file.
+
+> [!hint]
+> Look in `quartz/plugins` for more examples of plugins in Quartz as reference for your own plugins!
--
Gitblit v1.10.0