refactor plugins to be functions instead of classes
This commit is contained in:
		
							parent
							
								
									b8c011410d
								
							
						
					
					
						commit
						352075ae81
					
				
					 20 changed files with 464 additions and 507 deletions
				
			
		| 
						 | 
				
			
			@ -1,4 +1,3 @@
 | 
			
		|||
import { PluggableList } from "unified"
 | 
			
		||||
import { Root as HTMLRoot } from 'hast'
 | 
			
		||||
import { toString } from "hast-util-to-string"
 | 
			
		||||
import { QuartzTransformerPlugin } from "../types"
 | 
			
		||||
| 
						 | 
				
			
			@ -11,41 +10,36 @@ const defaultOptions: Options = {
 | 
			
		|||
  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
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,3 @@
 | 
			
		|||
import { PluggableList } from "unified"
 | 
			
		||||
import matter from "gray-matter"
 | 
			
		||||
import remarkFrontmatter from 'remark-frontmatter'
 | 
			
		||||
import { QuartzTransformerPlugin } from "../types"
 | 
			
		||||
| 
						 | 
				
			
			@ -13,35 +12,30 @@ const defaultOptions: Options = {
 | 
			
		|||
  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 []
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,27 +15,24 @@ const defaultOptions: Options = {
 | 
			
		|||
  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 []
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,3 @@
 | 
			
		|||
import { PluggableList } from "unified"
 | 
			
		||||
import fs from "fs"
 | 
			
		||||
import path from 'path'
 | 
			
		||||
import { Repository } from "@napi-rs/simple-git"
 | 
			
		||||
| 
						 | 
				
			
			@ -12,59 +11,51 @@ const defaultOptions: Options = {
 | 
			
		|||
  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 []
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,24 +1,20 @@
 | 
			
		|||
import { PluggableList } from "unified"
 | 
			
		||||
import remarkMath from "remark-math"
 | 
			
		||||
import rehypeKatex from 'rehype-katex'
 | 
			
		||||
import { StaticResources } from "../../resources"
 | 
			
		||||
import { QuartzTransformerPlugin } from "../types"
 | 
			
		||||
 | 
			
		||||
export class Katex extends QuartzTransformerPlugin {
 | 
			
		||||
  name = "Katex"
 | 
			
		||||
  markdownPlugins(): PluggableList {
 | 
			
		||||
export const Katex: QuartzTransformerPlugin = () => ({
 | 
			
		||||
  name: "Katex",
 | 
			
		||||
  markdownPlugins() {
 | 
			
		||||
    return [remarkMath]
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  htmlPlugins(): PluggableList {
 | 
			
		||||
  },
 | 
			
		||||
  htmlPlugins() {
 | 
			
		||||
    return [
 | 
			
		||||
      [rehypeKatex, {
 | 
			
		||||
        output: 'html',
 | 
			
		||||
      }]
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  externalResources: Partial<StaticResources> = {
 | 
			
		||||
  },
 | 
			
		||||
  externalResources: {
 | 
			
		||||
    css: [
 | 
			
		||||
      // base css
 | 
			
		||||
      "https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css",
 | 
			
		||||
| 
						 | 
				
			
			@ -31,4 +27,4 @@ export class Katex extends QuartzTransformerPlugin {
 | 
			
		|||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,3 @@
 | 
			
		|||
import { PluggableList } from "unified"
 | 
			
		||||
import { QuartzTransformerPlugin } from "../types"
 | 
			
		||||
import { relative, relativeToRoot, slugify } from "../../path"
 | 
			
		||||
import path from "path"
 | 
			
		||||
| 
						 | 
				
			
			@ -17,65 +16,60 @@ const defaultOptions: Options = {
 | 
			
		|||
  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
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    }]
 | 
			
		||||
      }]
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -89,174 +89,168 @@ const capitalize = (s: string): string => {
 | 
			
		|||
  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]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,15 +1,12 @@
 | 
			
		|||
import { PluggableList } from "unified"
 | 
			
		||||
import { QuartzTransformerPlugin } from "../types"
 | 
			
		||||
import rehypePrettyCode, { Options as CodeOptions } from "rehype-pretty-code"
 | 
			
		||||
 | 
			
		||||
export class SyntaxHighlighting extends QuartzTransformerPlugin {
 | 
			
		||||
  name = "SyntaxHighlighting"
 | 
			
		||||
 | 
			
		||||
  markdownPlugins(): PluggableList {
 | 
			
		||||
export const SyntaxHighlighting: QuartzTransformerPlugin = () => ({
 | 
			
		||||
  name: "SyntaxHighlighting",
 | 
			
		||||
  markdownPlugins() {
 | 
			
		||||
    return []
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  htmlPlugins(): PluggableList {
 | 
			
		||||
  },
 | 
			
		||||
  htmlPlugins() {
 | 
			
		||||
    return [[rehypePrettyCode, {
 | 
			
		||||
      theme: 'css-variables',
 | 
			
		||||
      onVisitLine(node) {
 | 
			
		||||
| 
						 | 
				
			
			@ -25,4 +22,4 @@ export class SyntaxHighlighting extends QuartzTransformerPlugin {
 | 
			
		|||
      },
 | 
			
		||||
    } satisfies Partial<CodeOptions>]]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,3 @@
 | 
			
		|||
import { PluggableList } from "unified"
 | 
			
		||||
import { QuartzTransformerPlugin } from "../types"
 | 
			
		||||
import { Root } from "mdast"
 | 
			
		||||
import { visit } from "unist-util-visit"
 | 
			
		||||
| 
						 | 
				
			
			@ -23,44 +22,39 @@ interface TocEntry {
 | 
			
		|||
  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 []
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue