From 352075ae81a3304a7bfa2512ef69b1cdacb26c12 Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Mon, 12 Jun 2023 06:26:43 +0000
Subject: [PATCH] refactor plugins to be functions instead of classes
---
quartz/plugins/types.ts | 41 +-
quartz/processors/filter.ts | 4
quartz/plugins/filters/draft.ts | 9
quartz/plugins/transformers/latex.ts | 20
quartz/plugins/transformers/description.ts | 58 +-
package-lock.json | 19
quartz/plugins/transformers/gfm.ts | 41 +-
quartz/plugins/transformers/frontmatter.ts | 48 +-
quartz/processors/parse.ts | 6
quartz/plugins/transformers/links.ts | 112 +++---
quartz/plugins/filters/explicit.ts | 9
quartz/plugins/transformers/syntax.ts | 15
quartz/components/types.ts | 3
quartz/plugins/transformers/ofm.ts | 280 ++++++++--------
quartz/plugins/transformers/toc.ts | 64 +--
package.json | 4
quartz/components/TableOfContents.tsx | 23
quartz/plugins/transformers/lastmod.ts | 85 ++--
quartz/plugins/emitters/contentPage.tsx | 108 +++---
quartz.config.ts | 22
20 files changed, 464 insertions(+), 507 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 4847103..6d922f4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,7 +14,6 @@
"chalk": "^4.1.2",
"cli-spinner": "^0.2.10",
"esbuild-sass-plugin": "^2.9.0",
- "flamethrower-router": "^0.0.0-meme.12",
"github-slugger": "^2.0.0",
"globby": "^13.1.4",
"gray-matter": "^4.0.3",
@@ -22,9 +21,12 @@
"hast-util-to-string": "^2.0.0",
"is-absolute-url": "^4.0.1",
"mdast-util-find-and-replace": "^2.2.2",
+ "mdast-util-to-string": "^3.2.0",
+ "micromorph": "^0.4.5",
"preact": "^10.14.1",
"preact-render-to-string": "^6.0.3",
"pretty-time": "^1.1.0",
+ "reading-time": "^1.5.0",
"rehype-autolink-headings": "^6.1.1",
"rehype-katex": "^6.0.3",
"rehype-pretty-code": "^0.9.6",
@@ -1523,11 +1525,6 @@
"node": ">=8"
}
},
- "node_modules/flamethrower-router": {
- "version": "0.0.0-meme.12",
- "resolved": "https://registry.npmjs.org/flamethrower-router/-/flamethrower-router-0.0.0-meme.12.tgz",
- "integrity": "sha512-PWcNrjzItwk61RTk/SbbKJNcAgl6qCXH8xkZjGjUGV/dgKAnURci+k+Yk8emubUQWTdAd1kSqujy0VRjoeEgxg=="
- },
"node_modules/foreground-child": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
@@ -3006,6 +3003,11 @@
"node": ">=8.6"
}
},
+ "node_modules/micromorph": {
+ "version": "0.4.5",
+ "resolved": "https://registry.npmjs.org/micromorph/-/micromorph-0.4.5.tgz",
+ "integrity": "sha512-Erasr0xiDvDeEhh7B/k7RFTwwfaAX10D7BMorNpokkwDh6XsRLYWDPaWF1m5JQeMSkGdqlEtQ8s68NcdDWuGgw=="
+ },
"node_modules/mime-db": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
@@ -3268,6 +3270,11 @@
"node": ">=8.10.0"
}
},
+ "node_modules/reading-time": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/reading-time/-/reading-time-1.5.0.tgz",
+ "integrity": "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg=="
+ },
"node_modules/rehype-autolink-headings": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-6.1.1.tgz",
diff --git a/package.json b/package.json
index fe5a517..810cf5e 100644
--- a/package.json
+++ b/package.json
@@ -30,7 +30,6 @@
"chalk": "^4.1.2",
"cli-spinner": "^0.2.10",
"esbuild-sass-plugin": "^2.9.0",
- "flamethrower-router": "^0.0.0-meme.12",
"github-slugger": "^2.0.0",
"globby": "^13.1.4",
"gray-matter": "^4.0.3",
@@ -38,9 +37,12 @@
"hast-util-to-string": "^2.0.0",
"is-absolute-url": "^4.0.1",
"mdast-util-find-and-replace": "^2.2.2",
+ "mdast-util-to-string": "^3.2.0",
+ "micromorph": "^0.4.5",
"preact": "^10.14.1",
"preact-render-to-string": "^6.0.3",
"pretty-time": "^1.1.0",
+ "reading-time": "^1.5.0",
"rehype-autolink-headings": "^6.1.1",
"rehype-katex": "^6.0.3",
"rehype-pretty-code": "^0.9.6",
diff --git a/quartz.config.ts b/quartz.config.ts
index 3a1d433..ab7e990 100644
--- a/quartz.config.ts
+++ b/quartz.config.ts
@@ -39,23 +39,23 @@
},
plugins: {
transformers: [
- new Plugin.FrontMatter(),
- new Plugin.Description(),
- new Plugin.TableOfContents({ showByDefault: true }),
- new Plugin.CreatedModifiedDate({
+ Plugin.FrontMatter(),
+ Plugin.Description(),
+ Plugin.TableOfContents({ showByDefault: true }),
+ Plugin.CreatedModifiedDate({
priority: ['frontmatter', 'filesystem'] // you can add 'git' here for last modified from Git but this makes the build slower
}),
- new Plugin.GitHubFlavoredMarkdown(),
- new Plugin.ObsidianFlavoredMarkdown(),
- new Plugin.ResolveLinks(),
- new Plugin.SyntaxHighlighting(),
- new Plugin.Katex(),
+ Plugin.GitHubFlavoredMarkdown(),
+ Plugin.ObsidianFlavoredMarkdown(),
+ Plugin.ResolveLinks(),
+ Plugin.SyntaxHighlighting(),
+ Plugin.Katex(),
],
filters: [
- new Plugin.RemoveDrafts()
+ Plugin.RemoveDrafts()
],
emitters: [
- new Plugin.ContentPage({
+ Plugin.ContentPage({
head: Component.Head,
header: [Component.PageTitle, Component.Spacer, Component.Darkmode],
body: [Component.ArticleTitle, Component.ReadingTime, Component.TableOfContents, Component.Content]
diff --git a/quartz/components/TableOfContents.tsx b/quartz/components/TableOfContents.tsx
index 8192da4..1f331ed 100644
--- a/quartz/components/TableOfContents.tsx
+++ b/quartz/components/TableOfContents.tsx
@@ -1,24 +1,19 @@
import { QuartzComponentProps } from "./types"
import style from "./styles/toc.scss"
-export default function TableOfContents({ fileData, position }: QuartzComponentProps) {
+export default function TableOfContents({ fileData }: QuartzComponentProps) {
if (!fileData.toc) {
return null
}
- if (position === 'body') {
- // TODO: animate this
- return <details className="toc" open>
- <summary><h3>Table of Contents</h3></summary>
- <ul>
- {fileData.toc.map(tocEntry => <li key={tocEntry.slug} className={`depth-${tocEntry.depth}`}>
- <a href={`#${tocEntry.slug}`}>{tocEntry.text}</a>
- </li>)}
- </ul>
- </details>
- } else if (position === 'sidebar') {
- // TODO
- }
+ return <details class="toc" open>
+ <summary><h3>Table of Contents</h3></summary>
+ <ul>
+ {fileData.toc.map(tocEntry => <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
+ <a href={`#${tocEntry.slug}`}>{tocEntry.text}</a>
+ </li>)}
+ </ul>
+ </details>
}
TableOfContents.css = style
diff --git a/quartz/components/types.ts b/quartz/components/types.ts
index 93f6a4b..cb84edc 100644
--- a/quartz/components/types.ts
+++ b/quartz/components/types.ts
@@ -10,7 +10,6 @@
cfg: GlobalConfiguration
children: QuartzComponent[] | JSX.Element[]
tree: Node<QuartzPluginData>
- position?: 'sidebar' | 'header' | 'body'
}
export type QuartzComponent = ComponentType<QuartzComponentProps> & {
@@ -18,3 +17,5 @@
beforeDOMLoaded?: string,
afterDOMLoaded?: string,
}
+
+export type QuartzComponentConstructor<Options extends object> = (opts: Options) => QuartzComponent
diff --git a/quartz/plugins/emitters/contentPage.tsx b/quartz/plugins/emitters/contentPage.tsx
index d44b709..b7059f8 100644
--- a/quartz/plugins/emitters/contentPage.tsx
+++ b/quartz/plugins/emitters/contentPage.tsx
@@ -15,66 +15,64 @@
body: QuartzComponent[]
}
-export class ContentPage extends QuartzEmitterPlugin {
- name = "ContentPage"
- opts: Options
-
- constructor(opts: Options) {
- super()
- this.opts = opts
+export const ContentPage: QuartzEmitterPlugin<Options> = (opts) => {
+ if (!opts) {
+ throw new Error("ContentPage must be initialized with options specifiying the components to use")
}
- getQuartzComponents(): QuartzComponent[] {
- return [this.opts.head, Header, ...this.opts.header, ...this.opts.body]
- }
+ return {
+ name: "ContentPage",
+ getQuartzComponents() {
+ return [opts.head, Header, ...opts.header, ...opts.body]
+ },
+ async emit(cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emit: EmitCallback): Promise<string[]> {
+ const fps: string[] = []
- async emit(cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emit: EmitCallback): Promise<string[]> {
- const fps: string[] = []
+ const { head: Head, header, body } = opts
+ for (const [tree, file] of content) {
+ const baseDir = resolveToRoot(file.data.slug!)
+ const pageResources: StaticResources = {
+ css: [baseDir + "/index.css", ...resources.css],
+ js: [
+ { src: baseDir + "/prescript.js", loadTime: "beforeDOMReady" },
+ ...resources.js,
+ { src: baseDir + "/postscript.js", loadTime: "afterDOMReady", type: 'module' }
+ ]
+ }
- const { head: Head, header, body } = this.opts
- for (const [tree, file] of content) {
- const baseDir = resolveToRoot(file.data.slug!)
- const pageResources: StaticResources = {
- css: [baseDir + "/index.css", ...resources.css],
- js: [
- { src: baseDir + "/prescript.js", loadTime: "beforeDOMReady" },
- ...resources.js,
- { src: baseDir + "/postscript.js", loadTime: "afterDOMReady", type: 'module' }
- ]
+ const componentData: QuartzComponentProps = {
+ fileData: file.data,
+ externalResources: pageResources,
+ cfg,
+ children: [],
+ tree
+ }
+
+ const doc = <html>
+ <Head {...componentData} />
+ <body>
+ <div id="quartz-root" class="page">
+ <Header {...componentData} >
+ {header.map(HeaderComponent => <HeaderComponent {...componentData} />)}
+ </Header>
+ <Body {...componentData}>
+ {body.map(BodyComponent => <BodyComponent {...componentData} />)}
+ </Body>
+ </div>
+ </body>
+ {pageResources.js.filter(resource => resource.loadTime === "afterDOMReady").map(resource => <script key={resource.src} {...resource} />)}
+ </html>
+
+ const fp = file.data.slug + ".html"
+ await emit({
+ content: "<!DOCTYPE html>\n" + render(doc),
+ slug: file.data.slug!,
+ ext: ".html",
+ })
+
+ fps.push(fp)
}
-
- const componentData: QuartzComponentProps = {
- fileData: file.data,
- externalResources: pageResources,
- cfg,
- children: [],
- tree
- }
-
- const doc = <html>
- <Head {...componentData} />
- <body>
- <div id="quartz-root" class="page">
- <Header {...componentData} >
- {header.map(HeaderComponent => <HeaderComponent {...componentData} position="header" />)}
- </Header>
- <Body {...componentData}>
- {body.map(BodyComponent => <BodyComponent {...componentData } position="body" />)}
- </Body>
- </div>
- </body>
- {pageResources.js.filter(resource => resource.loadTime === "afterDOMReady").map(resource => <script key={resource.src} {...resource} />)}
- </html>
-
- const fp = file.data.slug + ".html"
- await emit({
- content: "<!DOCTYPE html>\n" + render(doc),
- slug: file.data.slug!,
- ext: ".html",
- })
-
- fps.push(fp)
+ return fps
}
- return fps
}
}
diff --git a/quartz/plugins/filters/draft.ts b/quartz/plugins/filters/draft.ts
index 06083d2..630033d 100644
--- a/quartz/plugins/filters/draft.ts
+++ b/quartz/plugins/filters/draft.ts
@@ -1,10 +1,9 @@
import { QuartzFilterPlugin } from "../types"
-import { ProcessedContent } from "../vfile"
-export class RemoveDrafts extends QuartzFilterPlugin {
- name = "RemoveDrafts"
- shouldPublish([_tree, vfile]: ProcessedContent): boolean {
+export const RemoveDrafts: QuartzFilterPlugin<{}> = () => ({
+ name: "RemoveDrafts",
+ shouldPublish([_tree, vfile]) {
const draftFlag: boolean = vfile.data?.frontmatter?.draft ?? false
return !draftFlag
}
-}
+})
diff --git a/quartz/plugins/filters/explicit.ts b/quartz/plugins/filters/explicit.ts
index 7dad8cb..da5125b 100644
--- a/quartz/plugins/filters/explicit.ts
+++ b/quartz/plugins/filters/explicit.ts
@@ -1,10 +1,9 @@
import { QuartzFilterPlugin } from "../types"
-import { ProcessedContent } from "../vfile"
-export class ExplicitPublish extends QuartzFilterPlugin {
- name = "ExplicitPublish"
- shouldPublish([_tree, vfile]: ProcessedContent): boolean {
+export const ExplicitPublish: QuartzFilterPlugin = () => ({
+ name: "ExplicitPublish",
+ shouldPublish([_tree, vfile]) {
const publishFlag: boolean = vfile.data?.frontmatter?.publish ?? false
return publishFlag
}
-}
+})
diff --git a/quartz/plugins/transformers/description.ts b/quartz/plugins/transformers/description.ts
index b24dd1c..ed59f82 100644
--- a/quartz/plugins/transformers/description.ts
+++ b/quartz/plugins/transformers/description.ts
@@ -1,4 +1,3 @@
-import { PluggableList } from "unified"
import { Root as HTMLRoot } from 'hast'
import { toString } from "hast-util-to-string"
import { QuartzTransformerPlugin } from "../types"
@@ -11,41 +10,36 @@
descriptionLength: 150
}
-export class Description extends QuartzTransformerPlugin {
- name = "Description"
- opts: Options
+export const Description: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
+ const opts = { ...defaultOptions, ...userOpts }
+ return {
+ name: "Description",
+ markdownPlugins() {
+ return []
+ },
+ htmlPlugins() {
+ return [
+ () => {
+ return async (tree: HTMLRoot, file) => {
+ const frontMatterDescription = file.data.frontmatter?.description
+ const text = toString(tree)
- constructor(opts?: Partial<Options>) {
- super()
- this.opts = { ...defaultOptions, ...opts }
- }
+ const desc = frontMatterDescription ?? text
+ const sentences = desc.replace(/\s+/g, ' ').split('.')
+ let finalDesc = ""
+ let sentenceIdx = 0
+ const len = opts.descriptionLength
+ while (finalDesc.length < len) {
+ finalDesc += sentences[sentenceIdx] + '.'
+ sentenceIdx++
+ }
- markdownPlugins(): PluggableList {
- return []
- }
-
- htmlPlugins(): PluggableList {
- return [
- () => {
- return async (tree: HTMLRoot, file) => {
- const frontMatterDescription = file.data.frontmatter?.description
- const text = toString(tree)
-
- const desc = frontMatterDescription ?? text
- const sentences = desc.replace(/\s+/g, ' ').split('.')
- let finalDesc = ""
- let sentenceIdx = 0
- const len = this.opts.descriptionLength
- while (finalDesc.length < len) {
- finalDesc += sentences[sentenceIdx] + '.'
- sentenceIdx++
+ file.data.description = finalDesc
+ file.data.text = text
}
-
- file.data.description = finalDesc
- file.data.text = text
}
- }
- ]
+ ]
+ }
}
}
diff --git a/quartz/plugins/transformers/frontmatter.ts b/quartz/plugins/transformers/frontmatter.ts
index 0baec9e..5568463 100644
--- a/quartz/plugins/transformers/frontmatter.ts
+++ b/quartz/plugins/transformers/frontmatter.ts
@@ -1,4 +1,3 @@
-import { PluggableList } from "unified"
import matter from "gray-matter"
import remarkFrontmatter from 'remark-frontmatter'
import { QuartzTransformerPlugin } from "../types"
@@ -13,35 +12,30 @@
delims: '---'
}
-export class FrontMatter extends QuartzTransformerPlugin {
- name = "FrontMatter"
- opts: Options
+export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
+ const opts = { ...defaultOptions, ...userOpts }
+ return {
+ name: "FrontMatter",
+ markdownPlugins() {
+ return [
+ remarkFrontmatter,
+ () => {
+ return (_, file) => {
+ const { data } = matter(file.value, opts)
- constructor(opts?: Partial<Options>) {
- super()
- this.opts = { ...defaultOptions, ...opts }
- }
-
- markdownPlugins(): PluggableList {
- return [
- remarkFrontmatter,
- () => {
- return (_, file) => {
- const { data } = matter(file.value, this.opts)
-
- // fill in frontmatter
- file.data.frontmatter = {
- title: file.stem ?? "Untitled",
- tags: [],
- ...data
+ // fill in frontmatter
+ file.data.frontmatter = {
+ title: file.stem ?? "Untitled",
+ tags: [],
+ ...data
+ }
}
}
- }
- ]
- }
-
- htmlPlugins(): PluggableList {
- return []
+ ]
+ },
+ htmlPlugins() {
+ return []
+ }
}
}
diff --git a/quartz/plugins/transformers/gfm.ts b/quartz/plugins/transformers/gfm.ts
index 72f9870..54f8ca6 100644
--- a/quartz/plugins/transformers/gfm.ts
+++ b/quartz/plugins/transformers/gfm.ts
@@ -15,27 +15,24 @@
linkHeadings: true
}
-export class GitHubFlavoredMarkdown extends QuartzTransformerPlugin {
- name = "GitHubFlavoredMarkdown"
- opts: Options
-
- constructor(opts?: Partial<Options>) {
- super()
- this.opts = { ...defaultOptions, ...opts }
- }
-
- markdownPlugins(): PluggableList {
- return this.opts.enableSmartyPants ? [remarkGfm] : [remarkGfm, smartypants]
- }
-
- htmlPlugins(): PluggableList {
- return this.opts.linkHeadings
- ? [rehypeSlug, [rehypeAutolinkHeadings, {
- behavior: 'append', content: {
- type: 'text',
- value: ' §'
- }
- }]]
- : []
+export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
+ const opts = { ...defaultOptions, ...userOpts }
+ return {
+ name: "GitHubFlavoredMarkdown",
+ markdownPlugins() {
+ return opts.enableSmartyPants ? [remarkGfm] : [remarkGfm, smartypants]
+ },
+ htmlPlugins() {
+ if (opts.linkHeadings) {
+ return [rehypeSlug, [rehypeAutolinkHeadings, {
+ behavior: 'append', content: {
+ type: 'text',
+ value: ' §'
+ }
+ }]]
+ } else {
+ return []
+ }
+ }
}
}
diff --git a/quartz/plugins/transformers/lastmod.ts b/quartz/plugins/transformers/lastmod.ts
index ef33afe..b7514e4 100644
--- a/quartz/plugins/transformers/lastmod.ts
+++ b/quartz/plugins/transformers/lastmod.ts
@@ -1,4 +1,3 @@
-import { PluggableList } from "unified"
import fs from "fs"
import path from 'path'
import { Repository } from "@napi-rs/simple-git"
@@ -12,59 +11,51 @@
priority: ['frontmatter', 'git', 'filesystem']
}
-export class CreatedModifiedDate extends QuartzTransformerPlugin {
- name = "CreatedModifiedDate"
- opts: Options
+export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
+ const opts = { ...defaultOptions, ...userOpts }
+ return {
+ name: "CreatedModifiedDate",
+ markdownPlugins() {
+ return [
+ () => {
+ let repo: Repository | undefined = undefined
+ return async (_tree, file) => {
+ let created: undefined | Date = undefined
+ let modified: undefined | Date = undefined
+ let published: undefined | Date = undefined
- constructor(opts?: Partial<Options>) {
- super()
- this.opts = {
- ...defaultOptions,
- ...opts,
- }
- }
+ const fp = path.join(file.cwd, file.data.filePath as string)
+ for (const source of opts.priority) {
+ if (source === "filesystem") {
+ const st = await fs.promises.stat(fp)
+ created ||= new Date(st.birthtimeMs)
+ modified ||= new Date(st.mtimeMs)
+ } else if (source === "frontmatter" && file.data.frontmatter) {
+ created ||= file.data.frontmatter.date
+ modified ||= file.data.frontmatter.lastmod
+ modified ||= file.data.frontmatter["last-modified"]
+ published ||= file.data.frontmatter.publishDate
+ } else if (source === "git") {
+ if (!repo) {
+ repo = new Repository(file.cwd)
+ }
- markdownPlugins(): PluggableList {
- return [
- () => {
- let repo: Repository | undefined = undefined
- return async (_tree, file) => {
- let created: undefined | Date = undefined
- let modified: undefined | Date = undefined
- let published: undefined | Date = undefined
-
- const fp = path.join(file.cwd, file.data.filePath as string)
- for (const source of this.opts.priority) {
- if (source === "filesystem") {
- const st = await fs.promises.stat(fp)
- created ||= new Date(st.birthtimeMs)
- modified ||= new Date(st.mtimeMs)
- } else if (source === "frontmatter" && file.data.frontmatter) {
- created ||= file.data.frontmatter.date
- modified ||= file.data.frontmatter.lastmod
- modified ||= file.data.frontmatter["last-modified"]
- published ||= file.data.frontmatter.publishDate
- } else if (source === "git") {
- if (!repo) {
- repo = new Repository(file.cwd)
+ modified ||= new Date(await repo.getFileLatestModifiedDateAsync(file.data.filePath!))
}
+ }
- modified ||= new Date(await repo.getFileLatestModifiedDateAsync(file.data.filePath!))
+ file.data.dates = {
+ created: created ?? new Date(),
+ modified: modified ?? new Date(),
+ published: published ?? new Date()
}
}
-
- file.data.dates = {
- created: created ?? new Date(),
- modified: modified ?? new Date(),
- published: published ?? new Date()
- }
}
- }
- ]
- }
-
- htmlPlugins(): PluggableList {
- return []
+ ]
+ },
+ htmlPlugins() {
+ return []
+ }
}
}
diff --git a/quartz/plugins/transformers/latex.ts b/quartz/plugins/transformers/latex.ts
index f4155f6..86af69f 100644
--- a/quartz/plugins/transformers/latex.ts
+++ b/quartz/plugins/transformers/latex.ts
@@ -1,24 +1,20 @@
-import { PluggableList } from "unified"
import remarkMath from "remark-math"
import rehypeKatex from 'rehype-katex'
-import { StaticResources } from "../../resources"
import { QuartzTransformerPlugin } from "../types"
-export class Katex extends QuartzTransformerPlugin {
- name = "Katex"
- markdownPlugins(): PluggableList {
+export const Katex: QuartzTransformerPlugin = () => ({
+ name: "Katex",
+ markdownPlugins() {
return [remarkMath]
- }
-
- htmlPlugins(): PluggableList {
+ },
+ htmlPlugins() {
return [
[rehypeKatex, {
output: 'html',
}]
]
- }
-
- externalResources: Partial<StaticResources> = {
+ },
+ externalResources: {
css: [
// base css
"https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css",
@@ -31,4 +27,4 @@
}
]
}
-}
+})
diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts
index 4bcbe82..1619344 100644
--- a/quartz/plugins/transformers/links.ts
+++ b/quartz/plugins/transformers/links.ts
@@ -1,4 +1,3 @@
-import { PluggableList } from "unified"
import { QuartzTransformerPlugin } from "../types"
import { relative, relativeToRoot, slugify } from "../../path"
import path from "path"
@@ -17,65 +16,60 @@
prettyLinks: true
}
-export class ResolveLinks extends QuartzTransformerPlugin {
- name = "LinkProcessing"
- opts: Options
-
- constructor(opts?: Partial<Options>) {
- super()
- this.opts = { ...defaultOptions, ...opts }
- }
-
- markdownPlugins(): PluggableList {
- return []
- }
-
- htmlPlugins(): PluggableList {
- return [() => {
- return (tree, file) => {
- const curSlug = file.data.slug!
- const transformLink = (target: string) => {
- const targetSlug = slugify(decodeURI(target).trim())
- if (this.opts.markdownLinkResolution === 'relative' && !path.isAbsolute(targetSlug)) {
- return './' + relative(curSlug, targetSlug)
- } else {
- return './' + relativeToRoot(curSlug, targetSlug)
+export const ResolveLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
+ const opts = { ...defaultOptions, ...userOpts }
+ return {
+ name: "LinkProcessing",
+ markdownPlugins() {
+ return []
+ },
+ htmlPlugins() {
+ return [() => {
+ return (tree, file) => {
+ const curSlug = file.data.slug!
+ const transformLink = (target: string) => {
+ const targetSlug = slugify(decodeURI(target).trim())
+ if (opts.markdownLinkResolution === 'relative' && !path.isAbsolute(targetSlug)) {
+ return './' + relative(curSlug, targetSlug)
+ } else {
+ return './' + relativeToRoot(curSlug, targetSlug)
+ }
}
+
+ visit(tree, 'element', (node, _index, _parent) => {
+ // rewrite all links
+ if (
+ node.tagName === 'a' &&
+ node.properties &&
+ typeof node.properties.href === 'string'
+ ) {
+ node.properties.className = isAbsoluteUrl(node.properties.href) ? "external" : "internal"
+
+ // don't process external links or intra-document anchors
+ if (!(isAbsoluteUrl(node.properties.href) || node.properties.href.startsWith("#"))) {
+ node.properties.href = transformLink(node.properties.href)
+ }
+
+ // rewrite link internals if prettylinks is on
+ if (opts.prettyLinks && node.children.length === 1 && node.children[0].type === 'text') {
+ node.children[0].value = path.basename(node.children[0].value)
+ }
+ }
+
+ // transform all images
+ if (
+ node.tagName === 'img' &&
+ node.properties &&
+ typeof node.properties.src === 'string'
+ ) {
+ if (!isAbsoluteUrl(node.properties.src)) {
+ const ext = path.extname(node.properties.src)
+ node.properties.src = transformLink(path.join("assets", node.properties.src)) + ext
+ }
+ }
+ })
}
-
- visit(tree, 'element', (node, _index, _parent) => {
- // rewrite all links
- if (
- node.tagName === 'a' &&
- node.properties &&
- typeof node.properties.href === 'string'
- ) {
- node.properties.className = isAbsoluteUrl(node.properties.href) ? "external" : "internal"
-
- // don't process external links or intra-document anchors
- if (!(isAbsoluteUrl(node.properties.href) || node.properties.href.startsWith("#"))) {
- node.properties.href = transformLink(node.properties.href)
- }
-
- // rewrite link internals if prettylinks is on
- if (this.opts.prettyLinks && node.children.length === 1 && node.children[0].type === 'text') {
- node.children[0].value = path.basename(node.children[0].value)
- }
- }
-
- // transform all images
- if (
- node.tagName === 'img' &&
- node.properties &&
- typeof node.properties.src === 'string'
- ) {
- if (!isAbsoluteUrl(node.properties.src)) {
- const ext = path.extname(node.properties.src)
- node.properties.src = transformLink(path.join("assets", node.properties.src)) + ext
- }
- }
- })
- }
- }]
+ }]
+ }
}
}
diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts
index 23ed37c..1733b94 100644
--- a/quartz/plugins/transformers/ofm.ts
+++ b/quartz/plugins/transformers/ofm.ts
@@ -89,174 +89,168 @@
return s.substring(0, 1).toUpperCase() + s.substring(1);
}
-export class ObsidianFlavoredMarkdown extends QuartzTransformerPlugin {
- name = "ObsidianFlavoredMarkdown"
- opts: Options
+export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
+ const opts = { ...defaultOptions, ...userOpts }
+ return {
+ name: "ObsidianFlavoredMarkdown",
+ markdownPlugins() {
+ const plugins: PluggableList = []
+ if (opts.wikilinks) {
+ plugins.push(() => {
+ // Match wikilinks
+ // !? -> optional embedding
+ // \[\[ -> open brace
+ // ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name)
+ // (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link)
+ // (|[^\[\]\|\#]+)? -> | then one or more non-special characters (alias)
+ const backlinkRegex = new RegExp(/!?\[\[([^\[\]\|\#]+)(#[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/, "g")
+ return (tree: Root, _file) => {
+ findAndReplace(tree, backlinkRegex, (value: string, ...capture: string[]) => {
+ const [fp, rawHeader, rawAlias] = capture
+ const anchor = rawHeader?.trim() ?? ""
+ const alias = rawAlias?.slice(1).trim()
- constructor(opts?: Partial<Options>) {
- super()
- this.opts = { ...defaultOptions, ...opts }
- }
-
- markdownPlugins(): PluggableList {
- const plugins: PluggableList = []
-
- if (this.opts.wikilinks) {
- plugins.push(() => {
- // Match wikilinks
- // !? -> optional embedding
- // \[\[ -> open brace
- // ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name)
- // (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link)
- // (|[^\[\]\|\#]+)? -> | then one or more non-special characters (alias)
- const backlinkRegex = new RegExp(/!?\[\[([^\[\]\|\#]+)(#[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/, "g")
- return (tree: Root, _file) => {
- findAndReplace(tree, backlinkRegex, (value: string, ...capture: string[]) => {
- const [fp, rawHeader, rawAlias] = capture
- const anchor = rawHeader?.trim() ?? ""
- const alias = rawAlias?.slice(1).trim()
-
- // embed cases
- if (value.startsWith("!")) {
- const ext = path.extname(fp).toLowerCase()
- const url = slugify(fp.trim()) + ext
- if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg"].includes(ext)) {
- const dims = alias ?? ""
- let [width, height] = dims.split("x", 2)
- width ||= "auto"
- height ||= "auto"
- return {
- type: 'image',
- url,
- data: {
- hProperties: {
- width, height
+ // embed cases
+ if (value.startsWith("!")) {
+ const ext = path.extname(fp).toLowerCase()
+ const url = slugify(fp.trim()) + ext
+ if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg"].includes(ext)) {
+ const dims = alias ?? ""
+ let [width, height] = dims.split("x", 2)
+ width ||= "auto"
+ height ||= "auto"
+ return {
+ type: 'image',
+ url,
+ data: {
+ hProperties: {
+ width, height
+ }
}
}
+ } else if ([".mp4", ".webm", ".ogv", ".mov", ".mkv"].includes(ext)) {
+ return {
+ type: 'html',
+ value: `<video src="${url}" controls></video>`
+ }
+ } else if ([".mp3", ".webm", ".wav", ".m4a", ".ogg", ".3gp", ".flac"].includes(ext)) {
+ return {
+ type: 'html',
+ value: `<audio src="${url}" controls></audio>`
+ }
+ } else if ([".pdf"].includes(ext)) {
+ return {
+ type: 'html',
+ value: `<iframe src="${url}"></iframe>`
+ }
}
- } else if ([".mp4", ".webm", ".ogv", ".mov", ".mkv"].includes(ext)) {
- return {
- type: 'html',
- value: `<video src="${url}" controls></video>`
- }
- } else if ([".mp3", ".webm", ".wav", ".m4a", ".ogg", ".3gp", ".flac"].includes(ext)) {
- return {
- type: 'html',
- value: `<audio src="${url}" controls></audio>`
- }
- } else if ([".pdf"].includes(ext)) {
- return {
- type: 'html',
- value: `<iframe src="${url}"></iframe>`
- }
+ // otherwise, fall through to regular link
}
- // otherwise, fall through to regular link
- }
- // internal link
- const url = slugify(fp.trim() + anchor)
- return {
- type: 'link',
- url,
- children: [{
- type: 'text',
- value: alias ?? fp
- }]
- }
- })
+ // internal link
+ const url = slugify(fp.trim() + anchor)
+ return {
+ type: 'link',
+ url,
+ children: [{
+ type: 'text',
+ value: alias ?? fp
+ }]
+ }
+ })
+ }
}
+ )
}
- )
- }
- if (this.opts.highlight) {
- plugins.push(() => {
- // Match highlights
- const highlightRegex = new RegExp(/==(.+)==/, "g")
- return (tree: Root, _file) => {
- findAndReplace(tree, highlightRegex, (_value: string, ...capture: string[]) => {
- const [inner] = capture
- return {
- type: 'html',
- value: `<span class="text-highlight">${inner}</span>`
- }
- })
- }
- })
- }
+ if (opts.highlight) {
+ plugins.push(() => {
+ // Match highlights
+ const highlightRegex = new RegExp(/==(.+)==/, "g")
+ return (tree: Root, _file) => {
+ findAndReplace(tree, highlightRegex, (_value: string, ...capture: string[]) => {
+ const [inner] = capture
+ return {
+ type: 'html',
+ value: `<span class="text-highlight">${inner}</span>`
+ }
+ })
+ }
+ })
+ }
- if (this.opts.callouts) {
- plugins.push(() => {
- // from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts
- const calloutRegex = new RegExp(/^\[\!(\w+)\]([+-]?)/)
- return (tree: Root, _file) => {
- visit(tree, "blockquote", (node) => {
- if (node.children.length === 0) {
- return
- }
+ if (opts.callouts) {
+ plugins.push(() => {
+ // from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts
+ const calloutRegex = new RegExp(/^\[\!(\w+)\]([+-]?)/)
+ return (tree: Root, _file) => {
+ visit(tree, "blockquote", (node) => {
+ if (node.children.length === 0) {
+ return
+ }
- // find first line
- const firstChild = node.children[0]
- if (firstChild.type !== "paragraph" || firstChild.children[0]?.type !== "text") {
- return
- }
+ // find first line
+ const firstChild = node.children[0]
+ if (firstChild.type !== "paragraph" || firstChild.children[0]?.type !== "text") {
+ return
+ }
- const text = firstChild.children[0].value
- const [firstLine, ...remainingLines] = text.split("\n")
- const remainingText = remainingLines.join("\n")
+ const text = firstChild.children[0].value
+ const [firstLine, ...remainingLines] = text.split("\n")
+ const remainingText = remainingLines.join("\n")
- const match = firstLine.match(calloutRegex)
- if (match && match.input) {
- const [calloutDirective, typeString, collapseChar] = match
- const calloutType = typeString.toLowerCase() as keyof typeof callouts
- const collapse = collapseChar === "+" || collapseChar === "-"
- const defaultState = collapseChar === "-" ? "collapsed" : "expanded"
- const title = match.input.slice(calloutDirective.length).trim() || capitalize(calloutType)
+ const match = firstLine.match(calloutRegex)
+ if (match && match.input) {
+ const [calloutDirective, typeString, collapseChar] = match
+ const calloutType = typeString.toLowerCase() as keyof typeof callouts
+ const collapse = collapseChar === "+" || collapseChar === "-"
+ const defaultState = collapseChar === "-" ? "collapsed" : "expanded"
+ const title = match.input.slice(calloutDirective.length).trim() || capitalize(calloutType)
- const titleNode: HTML = {
- type: "html",
- value: `<div
+ const titleNode: HTML = {
+ type: "html",
+ value: `<div
class="callout-title"
>
<div class="callout-icon">${callouts[canonicalizeCallout(calloutType)]}</div>
<div class="callout-title-inner">${title}</div>
</div>`
- }
+ }
- const blockquoteContent: (BlockContent | DefinitionContent)[] = [titleNode]
- if (remainingText.length > 0) {
- blockquoteContent.push({
- type: 'paragraph',
- children: [{
- type: 'text',
- value: remainingText,
- }]
+ const blockquoteContent: (BlockContent | DefinitionContent)[] = [titleNode]
+ if (remainingText.length > 0) {
+ blockquoteContent.push({
+ type: 'paragraph',
+ children: [{
+ type: 'text',
+ value: remainingText,
+ }]
- })
- }
+ })
+ }
- // replace first line of blockquote with title and rest of the paragraph text
- node.children.splice(0, 1, ...blockquoteContent)
+ // replace first line of blockquote with title and rest of the paragraph text
+ node.children.splice(0, 1, ...blockquoteContent)
- // add properties to base blockquote
- node.data = {
- hProperties: {
- ...(node.data?.hProperties ?? {}),
- className: `callout ${collapse ? "is-collapsible" : ""} ${defaultState === "collapsed" ? "is-collapsed" : ""}`,
- "data-callout": calloutType,
- "data-callout-fold": collapse,
+ // add properties to base blockquote
+ node.data = {
+ hProperties: {
+ ...(node.data?.hProperties ?? {}),
+ className: `callout ${collapse ? "is-collapsible" : ""} ${defaultState === "collapsed" ? "is-collapsed" : ""}`,
+ "data-callout": calloutType,
+ "data-callout-fold": collapse,
+ }
}
}
- }
- })
- }
- })
+ })
+ }
+ })
+ }
+ return plugins
+ },
+
+ htmlPlugins() {
+ return [rehypeRaw]
}
-
- return plugins
- }
-
- htmlPlugins(): PluggableList {
- return [rehypeRaw]
}
}
diff --git a/quartz/plugins/transformers/syntax.ts b/quartz/plugins/transformers/syntax.ts
index f09daaa..16424ec 100644
--- a/quartz/plugins/transformers/syntax.ts
+++ b/quartz/plugins/transformers/syntax.ts
@@ -1,15 +1,12 @@
-import { PluggableList } from "unified"
import { QuartzTransformerPlugin } from "../types"
import rehypePrettyCode, { Options as CodeOptions } from "rehype-pretty-code"
-export class SyntaxHighlighting extends QuartzTransformerPlugin {
- name = "SyntaxHighlighting"
-
- markdownPlugins(): PluggableList {
+export const SyntaxHighlighting: QuartzTransformerPlugin = () => ({
+ name: "SyntaxHighlighting",
+ markdownPlugins() {
return []
- }
-
- htmlPlugins(): PluggableList {
+ },
+ htmlPlugins() {
return [[rehypePrettyCode, {
theme: 'css-variables',
onVisitLine(node) {
@@ -25,4 +22,4 @@
},
} satisfies Partial<CodeOptions>]]
}
-}
+})
diff --git a/quartz/plugins/transformers/toc.ts b/quartz/plugins/transformers/toc.ts
index 863e3a1..2141a87 100644
--- a/quartz/plugins/transformers/toc.ts
+++ b/quartz/plugins/transformers/toc.ts
@@ -1,4 +1,3 @@
-import { PluggableList } from "unified"
import { QuartzTransformerPlugin } from "../types"
import { Root } from "mdast"
import { visit } from "unist-util-visit"
@@ -23,44 +22,39 @@
slug: string
}
-export class TableOfContents extends QuartzTransformerPlugin {
- name = "TableOfContents"
- opts: Options
+export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
+ const opts = { ...defaultOptions, ...userOpts }
+ return {
+ name: "TableOfContents",
+ markdownPlugins() {
+ return [() => {
+ return async (tree: Root, file) => {
+ const display = file.data.frontmatter?.enableToc ?? opts.showByDefault
+ if (display) {
+ const toc: TocEntry[] = []
+ let highestDepth: number = opts.maxDepth
+ visit(tree, 'heading', (node) => {
+ if (node.depth <= opts.maxDepth) {
+ const text = toString(node)
+ highestDepth = Math.min(highestDepth, node.depth)
+ toc.push({
+ depth: node.depth,
+ text,
+ slug: slugAnchor.slug(text)
+ })
+ }
+ })
- constructor(opts?: Partial<Options>) {
- super()
- this.opts = { ...defaultOptions, ...opts }
- }
-
- markdownPlugins(): PluggableList {
- return [() => {
- return async (tree: Root, file) => {
- const display = file.data.frontmatter?.enableToc ?? this.opts.showByDefault
- if (display) {
- const toc: TocEntry[] = []
- let highestDepth: number = this.opts.maxDepth
- visit(tree, 'heading', (node) => {
- if (node.depth <= this.opts.maxDepth) {
- const text = toString(node)
- highestDepth = Math.min(highestDepth, node.depth)
- toc.push({
- depth: node.depth,
- text,
- slug: slugAnchor.slug(text)
- })
+ if (toc.length > opts.minEntries) {
+ file.data.toc = toc.map(entry => ({ ...entry, depth: entry.depth - highestDepth }))
}
- })
-
- if (toc.length > this.opts.minEntries) {
- file.data.toc = toc.map(entry => ({ ...entry, depth: entry.depth - highestDepth }))
}
}
- }
- }]
- }
-
- htmlPlugins(): PluggableList {
- return []
+ }]
+ },
+ htmlPlugins() {
+ return []
+ }
}
}
diff --git a/quartz/plugins/types.ts b/quartz/plugins/types.ts
index 11b07db..ac386c9 100644
--- a/quartz/plugins/types.ts
+++ b/quartz/plugins/types.ts
@@ -4,16 +4,32 @@
import { GlobalConfiguration } from "../cfg"
import { QuartzComponent } from "../components/types"
-export abstract class QuartzTransformerPlugin {
- abstract name: string
- abstract markdownPlugins(): PluggableList
- abstract htmlPlugins(): PluggableList
+export interface PluginTypes {
+ transformers: QuartzTransformerPluginInstance[],
+ filters: QuartzFilterPluginInstance[],
+ emitters: QuartzEmitterPluginInstance[],
+}
+
+type OptionType = object | undefined
+export type QuartzTransformerPlugin<Options extends OptionType = undefined> = (opts?: Options) => QuartzTransformerPluginInstance
+export type QuartzTransformerPluginInstance = {
+ name: string
+ markdownPlugins(): PluggableList
+ htmlPlugins(): PluggableList
externalResources?: Partial<StaticResources>
}
-export abstract class QuartzFilterPlugin {
- abstract name: string
- abstract shouldPublish(content: ProcessedContent): boolean
+export type QuartzFilterPlugin<Options extends OptionType = undefined> = (opts?: Options) => QuartzFilterPluginInstance
+export type QuartzFilterPluginInstance = {
+ name: string
+ shouldPublish(content: ProcessedContent): boolean
+}
+
+export type QuartzEmitterPlugin<Options extends OptionType = undefined> = (opts?: Options) => QuartzEmitterPluginInstance
+export type QuartzEmitterPluginInstance = {
+ name: string
+ emit(cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emitCallback: EmitCallback): Promise<string[]>
+ getQuartzComponents(): QuartzComponent[]
}
export interface EmitOptions {
@@ -23,14 +39,3 @@
}
export type EmitCallback = (data: EmitOptions) => Promise<string>
-export abstract class QuartzEmitterPlugin {
- abstract name: string
- abstract emit(cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emitCallback: EmitCallback): Promise<string[]>
- abstract getQuartzComponents(): QuartzComponent[]
-}
-
-export interface PluginTypes {
- transformers: QuartzTransformerPlugin[],
- filters: QuartzFilterPlugin[],
- emitters: QuartzEmitterPlugin[],
-}
diff --git a/quartz/processors/filter.ts b/quartz/processors/filter.ts
index 3152a04..04c14a6 100644
--- a/quartz/processors/filter.ts
+++ b/quartz/processors/filter.ts
@@ -1,8 +1,8 @@
import { PerfTimer } from "../perf"
-import { QuartzFilterPlugin } from "../plugins/types"
+import { QuartzFilterPluginInstance } from "../plugins/types"
import { ProcessedContent } from "../plugins/vfile"
-export function filterContent(plugins: QuartzFilterPlugin[], content: ProcessedContent[], verbose: boolean): ProcessedContent[] {
+export function filterContent(plugins: QuartzFilterPluginInstance[], content: ProcessedContent[], verbose: boolean): ProcessedContent[] {
const perf = new PerfTimer()
const initialLength = content.length
for (const plugin of plugins) {
diff --git a/quartz/processors/parse.ts b/quartz/processors/parse.ts
index 4fc13f8..f937701 100644
--- a/quartz/processors/parse.ts
+++ b/quartz/processors/parse.ts
@@ -11,12 +11,12 @@
import path from 'path'
import os from 'os'
import workerpool, { Promise as WorkerPromise } from 'workerpool'
-import { QuartzTransformerPlugin } from '../plugins/types'
+import { QuartzTransformerPluginInstance } from '../plugins/types'
import { QuartzLogger } from '../log'
import chalk from 'chalk'
export type QuartzProcessor = Processor<MDRoot, HTMLRoot, void>
-export function createProcessor(transformers: QuartzTransformerPlugin[]): QuartzProcessor {
+export function createProcessor(transformers: QuartzTransformerPluginInstance[]): QuartzProcessor {
// base Markdown -> MD AST
let processor = unified().use(remarkParse)
@@ -101,7 +101,7 @@
}
}
-export async function parseMarkdown(transformers: QuartzTransformerPlugin[], baseDir: string, fps: string[], verbose: boolean): Promise<ProcessedContent[]> {
+export async function parseMarkdown(transformers: QuartzTransformerPluginInstance[], baseDir: string, fps: string[], verbose: boolean): Promise<ProcessedContent[]> {
const perf = new PerfTimer()
const log = new QuartzLogger(verbose)
--
Gitblit v1.10.0