refactor plugins to be functions instead of classes
| | |
| | | "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", |
| | |
| | | "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", |
| | |
| | | "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", |
| | |
| | | "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", |
| | |
| | | "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", |
| | |
| | | "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", |
| | |
| | | "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", |
| | |
| | | }, |
| | | 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] |
| | |
| | | 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 |
| | |
| | | cfg: GlobalConfiguration |
| | | children: QuartzComponent[] | JSX.Element[] |
| | | tree: Node<QuartzPluginData> |
| | | position?: 'sidebar' | 'header' | 'body' |
| | | } |
| | | |
| | | export type QuartzComponent = ComponentType<QuartzComponentProps> & { |
| | |
| | | beforeDOMLoaded?: string, |
| | | afterDOMLoaded?: string, |
| | | } |
| | | |
| | | export type QuartzComponentConstructor<Options extends object> = (opts: Options) => QuartzComponent |
| | |
| | | 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 |
| | | } |
| | | } |
| | |
| | | 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 |
| | | } |
| | | } |
| | | }) |
| | |
| | | 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 |
| | | } |
| | | } |
| | | }) |
| | |
| | | import { PluggableList } from "unified" |
| | | import { Root as HTMLRoot } from 'hast' |
| | | import { toString } from "hast-util-to-string" |
| | | import { QuartzTransformerPlugin } from "../types" |
| | |
| | | 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 |
| | | } |
| | | } |
| | | ] |
| | | ] |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | import { PluggableList } from "unified" |
| | | import matter from "gray-matter" |
| | | import remarkFrontmatter from 'remark-frontmatter' |
| | | import { QuartzTransformerPlugin } from "../types" |
| | |
| | | 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 [] |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | 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 [] |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | import { PluggableList } from "unified" |
| | | import fs from "fs" |
| | | import path from 'path' |
| | | import { Repository } from "@napi-rs/simple-git" |
| | |
| | | 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 [] |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | 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", |
| | |
| | | } |
| | | ] |
| | | } |
| | | } |
| | | }) |
| | |
| | | import { PluggableList } from "unified" |
| | | import { QuartzTransformerPlugin } from "../types" |
| | | import { relative, relativeToRoot, slugify } from "../../path" |
| | | import path from "path" |
| | |
| | | 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 |
| | | } |
| | | } |
| | | }) |
| | | } |
| | | }] |
| | | }] |
| | | } |
| | | } |
| | | } |
| | |
| | | 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] |
| | | } |
| | | } |
| | |
| | | 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) { |
| | |
| | | }, |
| | | } satisfies Partial<CodeOptions>]] |
| | | } |
| | | } |
| | | }) |
| | |
| | | import { PluggableList } from "unified" |
| | | import { QuartzTransformerPlugin } from "../types" |
| | | import { Root } from "mdast" |
| | | import { visit } from "unist-util-visit" |
| | |
| | | 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 [] |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | 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 { |
| | |
| | | } |
| | | |
| | | 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[], |
| | | } |
| | |
| | | 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) { |
| | |
| | | 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) |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | 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) |
| | | |