refactor plugins to be functions instead of classes
This commit is contained in:
parent
3c5ecbaaf4
commit
3531ee4bee
19
package-lock.json
generated
19
package-lock.json
generated
@ -14,7 +14,6 @@
|
|||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"cli-spinner": "^0.2.10",
|
"cli-spinner": "^0.2.10",
|
||||||
"esbuild-sass-plugin": "^2.9.0",
|
"esbuild-sass-plugin": "^2.9.0",
|
||||||
"flamethrower-router": "^0.0.0-meme.12",
|
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"globby": "^13.1.4",
|
"globby": "^13.1.4",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
@ -22,9 +21,12 @@
|
|||||||
"hast-util-to-string": "^2.0.0",
|
"hast-util-to-string": "^2.0.0",
|
||||||
"is-absolute-url": "^4.0.1",
|
"is-absolute-url": "^4.0.1",
|
||||||
"mdast-util-find-and-replace": "^2.2.2",
|
"mdast-util-find-and-replace": "^2.2.2",
|
||||||
|
"mdast-util-to-string": "^3.2.0",
|
||||||
|
"micromorph": "^0.4.5",
|
||||||
"preact": "^10.14.1",
|
"preact": "^10.14.1",
|
||||||
"preact-render-to-string": "^6.0.3",
|
"preact-render-to-string": "^6.0.3",
|
||||||
"pretty-time": "^1.1.0",
|
"pretty-time": "^1.1.0",
|
||||||
|
"reading-time": "^1.5.0",
|
||||||
"rehype-autolink-headings": "^6.1.1",
|
"rehype-autolink-headings": "^6.1.1",
|
||||||
"rehype-katex": "^6.0.3",
|
"rehype-katex": "^6.0.3",
|
||||||
"rehype-pretty-code": "^0.9.6",
|
"rehype-pretty-code": "^0.9.6",
|
||||||
@ -1523,11 +1525,6 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/foreground-child": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
|
||||||
@ -3006,6 +3003,11 @@
|
|||||||
"node": ">=8.6"
|
"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": {
|
"node_modules/mime-db": {
|
||||||
"version": "1.33.0",
|
"version": "1.33.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
|
||||||
@ -3268,6 +3270,11 @@
|
|||||||
"node": ">=8.10.0"
|
"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": {
|
"node_modules/rehype-autolink-headings": {
|
||||||
"version": "6.1.1",
|
"version": "6.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-6.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-6.1.1.tgz",
|
||||||
|
@ -30,7 +30,6 @@
|
|||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"cli-spinner": "^0.2.10",
|
"cli-spinner": "^0.2.10",
|
||||||
"esbuild-sass-plugin": "^2.9.0",
|
"esbuild-sass-plugin": "^2.9.0",
|
||||||
"flamethrower-router": "^0.0.0-meme.12",
|
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"globby": "^13.1.4",
|
"globby": "^13.1.4",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
@ -38,9 +37,12 @@
|
|||||||
"hast-util-to-string": "^2.0.0",
|
"hast-util-to-string": "^2.0.0",
|
||||||
"is-absolute-url": "^4.0.1",
|
"is-absolute-url": "^4.0.1",
|
||||||
"mdast-util-find-and-replace": "^2.2.2",
|
"mdast-util-find-and-replace": "^2.2.2",
|
||||||
|
"mdast-util-to-string": "^3.2.0",
|
||||||
|
"micromorph": "^0.4.5",
|
||||||
"preact": "^10.14.1",
|
"preact": "^10.14.1",
|
||||||
"preact-render-to-string": "^6.0.3",
|
"preact-render-to-string": "^6.0.3",
|
||||||
"pretty-time": "^1.1.0",
|
"pretty-time": "^1.1.0",
|
||||||
|
"reading-time": "^1.5.0",
|
||||||
"rehype-autolink-headings": "^6.1.1",
|
"rehype-autolink-headings": "^6.1.1",
|
||||||
"rehype-katex": "^6.0.3",
|
"rehype-katex": "^6.0.3",
|
||||||
"rehype-pretty-code": "^0.9.6",
|
"rehype-pretty-code": "^0.9.6",
|
||||||
|
@ -39,23 +39,23 @@ const config: QuartzConfig = {
|
|||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
transformers: [
|
transformers: [
|
||||||
new Plugin.FrontMatter(),
|
Plugin.FrontMatter(),
|
||||||
new Plugin.Description(),
|
Plugin.Description(),
|
||||||
new Plugin.TableOfContents({ showByDefault: true }),
|
Plugin.TableOfContents({ showByDefault: true }),
|
||||||
new Plugin.CreatedModifiedDate({
|
Plugin.CreatedModifiedDate({
|
||||||
priority: ['frontmatter', 'filesystem'] // you can add 'git' here for last modified from Git but this makes the build slower
|
priority: ['frontmatter', 'filesystem'] // you can add 'git' here for last modified from Git but this makes the build slower
|
||||||
}),
|
}),
|
||||||
new Plugin.GitHubFlavoredMarkdown(),
|
Plugin.GitHubFlavoredMarkdown(),
|
||||||
new Plugin.ObsidianFlavoredMarkdown(),
|
Plugin.ObsidianFlavoredMarkdown(),
|
||||||
new Plugin.ResolveLinks(),
|
Plugin.ResolveLinks(),
|
||||||
new Plugin.SyntaxHighlighting(),
|
Plugin.SyntaxHighlighting(),
|
||||||
new Plugin.Katex(),
|
Plugin.Katex(),
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
new Plugin.RemoveDrafts()
|
Plugin.RemoveDrafts()
|
||||||
],
|
],
|
||||||
emitters: [
|
emitters: [
|
||||||
new Plugin.ContentPage({
|
Plugin.ContentPage({
|
||||||
head: Component.Head,
|
head: Component.Head,
|
||||||
header: [Component.PageTitle, Component.Spacer, Component.Darkmode],
|
header: [Component.PageTitle, Component.Spacer, Component.Darkmode],
|
||||||
body: [Component.ArticleTitle, Component.ReadingTime, Component.TableOfContents, Component.Content]
|
body: [Component.ArticleTitle, Component.ReadingTime, Component.TableOfContents, Component.Content]
|
||||||
|
@ -1,24 +1,19 @@
|
|||||||
import { QuartzComponentProps } from "./types"
|
import { QuartzComponentProps } from "./types"
|
||||||
import style from "./styles/toc.scss"
|
import style from "./styles/toc.scss"
|
||||||
|
|
||||||
export default function TableOfContents({ fileData, position }: QuartzComponentProps) {
|
export default function TableOfContents({ fileData }: QuartzComponentProps) {
|
||||||
if (!fileData.toc) {
|
if (!fileData.toc) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (position === 'body') {
|
return <details class="toc" open>
|
||||||
// TODO: animate this
|
<summary><h3>Table of Contents</h3></summary>
|
||||||
return <details className="toc" open>
|
<ul>
|
||||||
<summary><h3>Table of Contents</h3></summary>
|
{fileData.toc.map(tocEntry => <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
|
||||||
<ul>
|
<a href={`#${tocEntry.slug}`}>{tocEntry.text}</a>
|
||||||
{fileData.toc.map(tocEntry => <li key={tocEntry.slug} className={`depth-${tocEntry.depth}`}>
|
</li>)}
|
||||||
<a href={`#${tocEntry.slug}`}>{tocEntry.text}</a>
|
</ul>
|
||||||
</li>)}
|
</details>
|
||||||
</ul>
|
|
||||||
</details>
|
|
||||||
} else if (position === 'sidebar') {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TableOfContents.css = style
|
TableOfContents.css = style
|
||||||
|
@ -10,7 +10,6 @@ export type QuartzComponentProps = {
|
|||||||
cfg: GlobalConfiguration
|
cfg: GlobalConfiguration
|
||||||
children: QuartzComponent[] | JSX.Element[]
|
children: QuartzComponent[] | JSX.Element[]
|
||||||
tree: Node<QuartzPluginData>
|
tree: Node<QuartzPluginData>
|
||||||
position?: 'sidebar' | 'header' | 'body'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QuartzComponent = ComponentType<QuartzComponentProps> & {
|
export type QuartzComponent = ComponentType<QuartzComponentProps> & {
|
||||||
@ -18,3 +17,5 @@ export type QuartzComponent = ComponentType<QuartzComponentProps> & {
|
|||||||
beforeDOMLoaded?: string,
|
beforeDOMLoaded?: string,
|
||||||
afterDOMLoaded?: string,
|
afterDOMLoaded?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type QuartzComponentConstructor<Options extends object> = (opts: Options) => QuartzComponent
|
||||||
|
@ -15,66 +15,64 @@ interface Options {
|
|||||||
body: QuartzComponent[]
|
body: QuartzComponent[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ContentPage extends QuartzEmitterPlugin {
|
export const ContentPage: QuartzEmitterPlugin<Options> = (opts) => {
|
||||||
name = "ContentPage"
|
if (!opts) {
|
||||||
opts: Options
|
throw new Error("ContentPage must be initialized with options specifiying the components to use")
|
||||||
|
|
||||||
constructor(opts: Options) {
|
|
||||||
super()
|
|
||||||
this.opts = opts
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getQuartzComponents(): QuartzComponent[] {
|
return {
|
||||||
return [this.opts.head, Header, ...this.opts.header, ...this.opts.body]
|
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 { head: Head, header, body } = opts
|
||||||
const fps: string[] = []
|
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
|
const componentData: QuartzComponentProps = {
|
||||||
for (const [tree, file] of content) {
|
fileData: file.data,
|
||||||
const baseDir = resolveToRoot(file.data.slug!)
|
externalResources: pageResources,
|
||||||
const pageResources: StaticResources = {
|
cfg,
|
||||||
css: [baseDir + "/index.css", ...resources.css],
|
children: [],
|
||||||
js: [
|
tree
|
||||||
{ src: baseDir + "/prescript.js", loadTime: "beforeDOMReady" },
|
}
|
||||||
...resources.js,
|
|
||||||
{ src: baseDir + "/postscript.js", loadTime: "afterDOMReady", type: 'module' }
|
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)
|
||||||
}
|
}
|
||||||
|
return fps
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { QuartzFilterPlugin } from "../types"
|
import { QuartzFilterPlugin } from "../types"
|
||||||
import { ProcessedContent } from "../vfile"
|
|
||||||
|
|
||||||
export class RemoveDrafts extends QuartzFilterPlugin {
|
export const RemoveDrafts: QuartzFilterPlugin<{}> = () => ({
|
||||||
name = "RemoveDrafts"
|
name: "RemoveDrafts",
|
||||||
shouldPublish([_tree, vfile]: ProcessedContent): boolean {
|
shouldPublish([_tree, vfile]) {
|
||||||
const draftFlag: boolean = vfile.data?.frontmatter?.draft ?? false
|
const draftFlag: boolean = vfile.data?.frontmatter?.draft ?? false
|
||||||
return !draftFlag
|
return !draftFlag
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { QuartzFilterPlugin } from "../types"
|
import { QuartzFilterPlugin } from "../types"
|
||||||
import { ProcessedContent } from "../vfile"
|
|
||||||
|
|
||||||
export class ExplicitPublish extends QuartzFilterPlugin {
|
export const ExplicitPublish: QuartzFilterPlugin = () => ({
|
||||||
name = "ExplicitPublish"
|
name: "ExplicitPublish",
|
||||||
shouldPublish([_tree, vfile]: ProcessedContent): boolean {
|
shouldPublish([_tree, vfile]) {
|
||||||
const publishFlag: boolean = vfile.data?.frontmatter?.publish ?? false
|
const publishFlag: boolean = vfile.data?.frontmatter?.publish ?? false
|
||||||
return publishFlag
|
return publishFlag
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { PluggableList } from "unified"
|
|
||||||
import { Root as HTMLRoot } from 'hast'
|
import { Root as HTMLRoot } from 'hast'
|
||||||
import { toString } from "hast-util-to-string"
|
import { toString } from "hast-util-to-string"
|
||||||
import { QuartzTransformerPlugin } from "../types"
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
@ -11,41 +10,36 @@ const defaultOptions: Options = {
|
|||||||
descriptionLength: 150
|
descriptionLength: 150
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Description extends QuartzTransformerPlugin {
|
export const Description: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
||||||
name = "Description"
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
opts: Options
|
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>) {
|
const desc = frontMatterDescription ?? text
|
||||||
super()
|
const sentences = desc.replace(/\s+/g, ' ').split('.')
|
||||||
this.opts = { ...defaultOptions, ...opts }
|
let finalDesc = ""
|
||||||
}
|
let sentenceIdx = 0
|
||||||
|
const len = opts.descriptionLength
|
||||||
|
while (finalDesc.length < len) {
|
||||||
|
finalDesc += sentences[sentenceIdx] + '.'
|
||||||
|
sentenceIdx++
|
||||||
|
}
|
||||||
|
|
||||||
markdownPlugins(): PluggableList {
|
file.data.description = finalDesc
|
||||||
return []
|
file.data.text = text
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { PluggableList } from "unified"
|
|
||||||
import matter from "gray-matter"
|
import matter from "gray-matter"
|
||||||
import remarkFrontmatter from 'remark-frontmatter'
|
import remarkFrontmatter from 'remark-frontmatter'
|
||||||
import { QuartzTransformerPlugin } from "../types"
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
@ -13,35 +12,30 @@ const defaultOptions: Options = {
|
|||||||
delims: '---'
|
delims: '---'
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FrontMatter extends QuartzTransformerPlugin {
|
export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
||||||
name = "FrontMatter"
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
opts: Options
|
return {
|
||||||
|
name: "FrontMatter",
|
||||||
|
markdownPlugins() {
|
||||||
|
return [
|
||||||
|
remarkFrontmatter,
|
||||||
|
() => {
|
||||||
|
return (_, file) => {
|
||||||
|
const { data } = matter(file.value, opts)
|
||||||
|
|
||||||
constructor(opts?: Partial<Options>) {
|
// fill in frontmatter
|
||||||
super()
|
file.data.frontmatter = {
|
||||||
this.opts = { ...defaultOptions, ...opts }
|
title: file.stem ?? "Untitled",
|
||||||
}
|
tags: [],
|
||||||
|
...data
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
]
|
},
|
||||||
}
|
htmlPlugins() {
|
||||||
|
return []
|
||||||
htmlPlugins(): PluggableList {
|
}
|
||||||
return []
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,27 +15,24 @@ const defaultOptions: Options = {
|
|||||||
linkHeadings: true
|
linkHeadings: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GitHubFlavoredMarkdown extends QuartzTransformerPlugin {
|
export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
||||||
name = "GitHubFlavoredMarkdown"
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
opts: Options
|
return {
|
||||||
|
name: "GitHubFlavoredMarkdown",
|
||||||
constructor(opts?: Partial<Options>) {
|
markdownPlugins() {
|
||||||
super()
|
return opts.enableSmartyPants ? [remarkGfm] : [remarkGfm, smartypants]
|
||||||
this.opts = { ...defaultOptions, ...opts }
|
},
|
||||||
}
|
htmlPlugins() {
|
||||||
|
if (opts.linkHeadings) {
|
||||||
markdownPlugins(): PluggableList {
|
return [rehypeSlug, [rehypeAutolinkHeadings, {
|
||||||
return this.opts.enableSmartyPants ? [remarkGfm] : [remarkGfm, smartypants]
|
behavior: 'append', content: {
|
||||||
}
|
type: 'text',
|
||||||
|
value: ' §'
|
||||||
htmlPlugins(): PluggableList {
|
}
|
||||||
return this.opts.linkHeadings
|
}]]
|
||||||
? [rehypeSlug, [rehypeAutolinkHeadings, {
|
} else {
|
||||||
behavior: 'append', content: {
|
return []
|
||||||
type: 'text',
|
}
|
||||||
value: ' §'
|
}
|
||||||
}
|
|
||||||
}]]
|
|
||||||
: []
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { PluggableList } from "unified"
|
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { Repository } from "@napi-rs/simple-git"
|
import { Repository } from "@napi-rs/simple-git"
|
||||||
@ -12,59 +11,51 @@ const defaultOptions: Options = {
|
|||||||
priority: ['frontmatter', 'git', 'filesystem']
|
priority: ['frontmatter', 'git', 'filesystem']
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreatedModifiedDate extends QuartzTransformerPlugin {
|
export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
||||||
name = "CreatedModifiedDate"
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
opts: Options
|
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>) {
|
const fp = path.join(file.cwd, file.data.filePath as string)
|
||||||
super()
|
for (const source of opts.priority) {
|
||||||
this.opts = {
|
if (source === "filesystem") {
|
||||||
...defaultOptions,
|
const st = await fs.promises.stat(fp)
|
||||||
...opts,
|
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 {
|
modified ||= new Date(await repo.getFileLatestModifiedDateAsync(file.data.filePath!))
|
||||||
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!))
|
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() {
|
||||||
|
return []
|
||||||
htmlPlugins(): PluggableList {
|
}
|
||||||
return []
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,24 +1,20 @@
|
|||||||
import { PluggableList } from "unified"
|
|
||||||
import remarkMath from "remark-math"
|
import remarkMath from "remark-math"
|
||||||
import rehypeKatex from 'rehype-katex'
|
import rehypeKatex from 'rehype-katex'
|
||||||
import { StaticResources } from "../../resources"
|
|
||||||
import { QuartzTransformerPlugin } from "../types"
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
|
|
||||||
export class Katex extends QuartzTransformerPlugin {
|
export const Katex: QuartzTransformerPlugin = () => ({
|
||||||
name = "Katex"
|
name: "Katex",
|
||||||
markdownPlugins(): PluggableList {
|
markdownPlugins() {
|
||||||
return [remarkMath]
|
return [remarkMath]
|
||||||
}
|
},
|
||||||
|
htmlPlugins() {
|
||||||
htmlPlugins(): PluggableList {
|
|
||||||
return [
|
return [
|
||||||
[rehypeKatex, {
|
[rehypeKatex, {
|
||||||
output: 'html',
|
output: 'html',
|
||||||
}]
|
}]
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
externalResources: {
|
||||||
externalResources: Partial<StaticResources> = {
|
|
||||||
css: [
|
css: [
|
||||||
// base css
|
// base css
|
||||||
"https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css",
|
"https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css",
|
||||||
@ -31,4 +27,4 @@ export class Katex extends QuartzTransformerPlugin {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { PluggableList } from "unified"
|
|
||||||
import { QuartzTransformerPlugin } from "../types"
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
import { relative, relativeToRoot, slugify } from "../../path"
|
import { relative, relativeToRoot, slugify } from "../../path"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
@ -17,65 +16,60 @@ const defaultOptions: Options = {
|
|||||||
prettyLinks: true
|
prettyLinks: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ResolveLinks extends QuartzTransformerPlugin {
|
export const ResolveLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
||||||
name = "LinkProcessing"
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
opts: Options
|
return {
|
||||||
|
name: "LinkProcessing",
|
||||||
constructor(opts?: Partial<Options>) {
|
markdownPlugins() {
|
||||||
super()
|
return []
|
||||||
this.opts = { ...defaultOptions, ...opts }
|
},
|
||||||
}
|
htmlPlugins() {
|
||||||
|
return [() => {
|
||||||
markdownPlugins(): PluggableList {
|
return (tree, file) => {
|
||||||
return []
|
const curSlug = file.data.slug!
|
||||||
}
|
const transformLink = (target: string) => {
|
||||||
|
const targetSlug = slugify(decodeURI(target).trim())
|
||||||
htmlPlugins(): PluggableList {
|
if (opts.markdownLinkResolution === 'relative' && !path.isAbsolute(targetSlug)) {
|
||||||
return [() => {
|
return './' + relative(curSlug, targetSlug)
|
||||||
return (tree, file) => {
|
} else {
|
||||||
const curSlug = file.data.slug!
|
return './' + relativeToRoot(curSlug, targetSlug)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,174 +89,168 @@ const capitalize = (s: string): string => {
|
|||||||
return s.substring(0, 1).toUpperCase() + s.substring(1);
|
return s.substring(0, 1).toUpperCase() + s.substring(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ObsidianFlavoredMarkdown extends QuartzTransformerPlugin {
|
export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
||||||
name = "ObsidianFlavoredMarkdown"
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
opts: Options
|
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>) {
|
// embed cases
|
||||||
super()
|
if (value.startsWith("!")) {
|
||||||
this.opts = { ...defaultOptions, ...opts }
|
const ext = path.extname(fp).toLowerCase()
|
||||||
}
|
const url = slugify(fp.trim()) + ext
|
||||||
|
if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg"].includes(ext)) {
|
||||||
markdownPlugins(): PluggableList {
|
const dims = alias ?? ""
|
||||||
const plugins: PluggableList = []
|
let [width, height] = dims.split("x", 2)
|
||||||
|
width ||= "auto"
|
||||||
if (this.opts.wikilinks) {
|
height ||= "auto"
|
||||||
plugins.push(() => {
|
return {
|
||||||
// Match wikilinks
|
type: 'image',
|
||||||
// !? -> optional embedding
|
url,
|
||||||
// \[\[ -> open brace
|
data: {
|
||||||
// ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name)
|
hProperties: {
|
||||||
// (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link)
|
width, height
|
||||||
// (|[^\[\]\|\#]+)? -> | 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} 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)) {
|
// otherwise, fall through to regular link
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// internal link
|
// internal link
|
||||||
const url = slugify(fp.trim() + anchor)
|
const url = slugify(fp.trim() + anchor)
|
||||||
return {
|
return {
|
||||||
type: 'link',
|
type: 'link',
|
||||||
url,
|
url,
|
||||||
children: [{
|
children: [{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
value: alias ?? fp
|
value: alias ?? fp
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.opts.highlight) {
|
if (opts.highlight) {
|
||||||
plugins.push(() => {
|
plugins.push(() => {
|
||||||
// Match highlights
|
// Match highlights
|
||||||
const highlightRegex = new RegExp(/==(.+)==/, "g")
|
const highlightRegex = new RegExp(/==(.+)==/, "g")
|
||||||
return (tree: Root, _file) => {
|
return (tree: Root, _file) => {
|
||||||
findAndReplace(tree, highlightRegex, (_value: string, ...capture: string[]) => {
|
findAndReplace(tree, highlightRegex, (_value: string, ...capture: string[]) => {
|
||||||
const [inner] = capture
|
const [inner] = capture
|
||||||
return {
|
return {
|
||||||
type: 'html',
|
type: 'html',
|
||||||
value: `<span class="text-highlight">${inner}</span>`
|
value: `<span class="text-highlight">${inner}</span>`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.opts.callouts) {
|
if (opts.callouts) {
|
||||||
plugins.push(() => {
|
plugins.push(() => {
|
||||||
// from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts
|
// from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts
|
||||||
const calloutRegex = new RegExp(/^\[\!(\w+)\]([+-]?)/)
|
const calloutRegex = new RegExp(/^\[\!(\w+)\]([+-]?)/)
|
||||||
return (tree: Root, _file) => {
|
return (tree: Root, _file) => {
|
||||||
visit(tree, "blockquote", (node) => {
|
visit(tree, "blockquote", (node) => {
|
||||||
if (node.children.length === 0) {
|
if (node.children.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// find first line
|
// find first line
|
||||||
const firstChild = node.children[0]
|
const firstChild = node.children[0]
|
||||||
if (firstChild.type !== "paragraph" || firstChild.children[0]?.type !== "text") {
|
if (firstChild.type !== "paragraph" || firstChild.children[0]?.type !== "text") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = firstChild.children[0].value
|
const text = firstChild.children[0].value
|
||||||
const [firstLine, ...remainingLines] = text.split("\n")
|
const [firstLine, ...remainingLines] = text.split("\n")
|
||||||
const remainingText = remainingLines.join("\n")
|
const remainingText = remainingLines.join("\n")
|
||||||
|
|
||||||
const match = firstLine.match(calloutRegex)
|
const match = firstLine.match(calloutRegex)
|
||||||
if (match && match.input) {
|
if (match && match.input) {
|
||||||
const [calloutDirective, typeString, collapseChar] = match
|
const [calloutDirective, typeString, collapseChar] = match
|
||||||
const calloutType = typeString.toLowerCase() as keyof typeof callouts
|
const calloutType = typeString.toLowerCase() as keyof typeof callouts
|
||||||
const collapse = collapseChar === "+" || collapseChar === "-"
|
const collapse = collapseChar === "+" || collapseChar === "-"
|
||||||
const defaultState = collapseChar === "-" ? "collapsed" : "expanded"
|
const defaultState = collapseChar === "-" ? "collapsed" : "expanded"
|
||||||
const title = match.input.slice(calloutDirective.length).trim() || capitalize(calloutType)
|
const title = match.input.slice(calloutDirective.length).trim() || capitalize(calloutType)
|
||||||
|
|
||||||
const titleNode: HTML = {
|
const titleNode: HTML = {
|
||||||
type: "html",
|
type: "html",
|
||||||
value: `<div
|
value: `<div
|
||||||
class="callout-title"
|
class="callout-title"
|
||||||
>
|
>
|
||||||
<div class="callout-icon">${callouts[canonicalizeCallout(calloutType)]}</div>
|
<div class="callout-icon">${callouts[canonicalizeCallout(calloutType)]}</div>
|
||||||
<div class="callout-title-inner">${title}</div>
|
<div class="callout-title-inner">${title}</div>
|
||||||
</div>`
|
</div>`
|
||||||
}
|
}
|
||||||
|
|
||||||
const blockquoteContent: (BlockContent | DefinitionContent)[] = [titleNode]
|
const blockquoteContent: (BlockContent | DefinitionContent)[] = [titleNode]
|
||||||
if (remainingText.length > 0) {
|
if (remainingText.length > 0) {
|
||||||
blockquoteContent.push({
|
blockquoteContent.push({
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
children: [{
|
children: [{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
value: remainingText,
|
value: remainingText,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// replace first line of blockquote with title and rest of the paragraph text
|
// replace first line of blockquote with title and rest of the paragraph text
|
||||||
node.children.splice(0, 1, ...blockquoteContent)
|
node.children.splice(0, 1, ...blockquoteContent)
|
||||||
|
|
||||||
// add properties to base blockquote
|
// add properties to base blockquote
|
||||||
node.data = {
|
node.data = {
|
||||||
hProperties: {
|
hProperties: {
|
||||||
...(node.data?.hProperties ?? {}),
|
...(node.data?.hProperties ?? {}),
|
||||||
className: `callout ${collapse ? "is-collapsible" : ""} ${defaultState === "collapsed" ? "is-collapsed" : ""}`,
|
className: `callout ${collapse ? "is-collapsible" : ""} ${defaultState === "collapsed" ? "is-collapsed" : ""}`,
|
||||||
"data-callout": calloutType,
|
"data-callout": calloutType,
|
||||||
"data-callout-fold": collapse,
|
"data-callout-fold": collapse,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
})
|
}
|
||||||
|
return plugins
|
||||||
|
},
|
||||||
|
|
||||||
|
htmlPlugins() {
|
||||||
|
return [rehypeRaw]
|
||||||
}
|
}
|
||||||
|
|
||||||
return plugins
|
|
||||||
}
|
|
||||||
|
|
||||||
htmlPlugins(): PluggableList {
|
|
||||||
return [rehypeRaw]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
import { PluggableList } from "unified"
|
|
||||||
import { QuartzTransformerPlugin } from "../types"
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
import rehypePrettyCode, { Options as CodeOptions } from "rehype-pretty-code"
|
import rehypePrettyCode, { Options as CodeOptions } from "rehype-pretty-code"
|
||||||
|
|
||||||
export class SyntaxHighlighting extends QuartzTransformerPlugin {
|
export const SyntaxHighlighting: QuartzTransformerPlugin = () => ({
|
||||||
name = "SyntaxHighlighting"
|
name: "SyntaxHighlighting",
|
||||||
|
markdownPlugins() {
|
||||||
markdownPlugins(): PluggableList {
|
|
||||||
return []
|
return []
|
||||||
}
|
},
|
||||||
|
htmlPlugins() {
|
||||||
htmlPlugins(): PluggableList {
|
|
||||||
return [[rehypePrettyCode, {
|
return [[rehypePrettyCode, {
|
||||||
theme: 'css-variables',
|
theme: 'css-variables',
|
||||||
onVisitLine(node) {
|
onVisitLine(node) {
|
||||||
@ -25,4 +22,4 @@ export class SyntaxHighlighting extends QuartzTransformerPlugin {
|
|||||||
},
|
},
|
||||||
} satisfies Partial<CodeOptions>]]
|
} satisfies Partial<CodeOptions>]]
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { PluggableList } from "unified"
|
|
||||||
import { QuartzTransformerPlugin } from "../types"
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
import { Root } from "mdast"
|
import { Root } from "mdast"
|
||||||
import { visit } from "unist-util-visit"
|
import { visit } from "unist-util-visit"
|
||||||
@ -23,44 +22,39 @@ interface TocEntry {
|
|||||||
slug: string
|
slug: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TableOfContents extends QuartzTransformerPlugin {
|
export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
||||||
name = "TableOfContents"
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
opts: Options
|
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>) {
|
if (toc.length > opts.minEntries) {
|
||||||
super()
|
file.data.toc = toc.map(entry => ({ ...entry, depth: entry.depth - highestDepth }))
|
||||||
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 > this.opts.minEntries) {
|
|
||||||
file.data.toc = toc.map(entry => ({ ...entry, depth: entry.depth - highestDepth }))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}]
|
||||||
}]
|
},
|
||||||
}
|
htmlPlugins() {
|
||||||
|
return []
|
||||||
htmlPlugins(): PluggableList {
|
}
|
||||||
return []
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,16 +4,32 @@ import { ProcessedContent } from "./vfile"
|
|||||||
import { GlobalConfiguration } from "../cfg"
|
import { GlobalConfiguration } from "../cfg"
|
||||||
import { QuartzComponent } from "../components/types"
|
import { QuartzComponent } from "../components/types"
|
||||||
|
|
||||||
export abstract class QuartzTransformerPlugin {
|
export interface PluginTypes {
|
||||||
abstract name: string
|
transformers: QuartzTransformerPluginInstance[],
|
||||||
abstract markdownPlugins(): PluggableList
|
filters: QuartzFilterPluginInstance[],
|
||||||
abstract htmlPlugins(): PluggableList
|
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>
|
externalResources?: Partial<StaticResources>
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class QuartzFilterPlugin {
|
export type QuartzFilterPlugin<Options extends OptionType = undefined> = (opts?: Options) => QuartzFilterPluginInstance
|
||||||
abstract name: string
|
export type QuartzFilterPluginInstance = {
|
||||||
abstract shouldPublish(content: ProcessedContent): boolean
|
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 {
|
export interface EmitOptions {
|
||||||
@ -23,14 +39,3 @@ export interface EmitOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type EmitCallback = (data: EmitOptions) => Promise<string>
|
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[],
|
|
||||||
}
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { PerfTimer } from "../perf"
|
import { PerfTimer } from "../perf"
|
||||||
import { QuartzFilterPlugin } from "../plugins/types"
|
import { QuartzFilterPluginInstance } from "../plugins/types"
|
||||||
import { ProcessedContent } from "../plugins/vfile"
|
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 perf = new PerfTimer()
|
||||||
const initialLength = content.length
|
const initialLength = content.length
|
||||||
for (const plugin of plugins) {
|
for (const plugin of plugins) {
|
||||||
|
@ -11,12 +11,12 @@ import { slugify } from '../path'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import os from 'os'
|
import os from 'os'
|
||||||
import workerpool, { Promise as WorkerPromise } from 'workerpool'
|
import workerpool, { Promise as WorkerPromise } from 'workerpool'
|
||||||
import { QuartzTransformerPlugin } from '../plugins/types'
|
import { QuartzTransformerPluginInstance } from '../plugins/types'
|
||||||
import { QuartzLogger } from '../log'
|
import { QuartzLogger } from '../log'
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
|
|
||||||
export type QuartzProcessor = Processor<MDRoot, HTMLRoot, void>
|
export type QuartzProcessor = Processor<MDRoot, HTMLRoot, void>
|
||||||
export function createProcessor(transformers: QuartzTransformerPlugin[]): QuartzProcessor {
|
export function createProcessor(transformers: QuartzTransformerPluginInstance[]): QuartzProcessor {
|
||||||
// base Markdown -> MD AST
|
// base Markdown -> MD AST
|
||||||
let processor = unified().use(remarkParse)
|
let processor = unified().use(remarkParse)
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ export function createFileParser(baseDir: string, fps: string[], verbose: boolea
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 perf = new PerfTimer()
|
||||||
const log = new QuartzLogger(verbose)
|
const log = new QuartzLogger(verbose)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user