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