Merge commit '76f2664277e07a7d1b011fac236840c6e8e69fdd' into v4
This commit is contained in:
		
							
								
								
									
										59
									
								
								quartz/plugins/emitters/404.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								quartz/plugins/emitters/404.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
import { QuartzEmitterPlugin } from "../types"
 | 
			
		||||
import { QuartzComponentProps } from "../../components/types"
 | 
			
		||||
import BodyConstructor from "../../components/Body"
 | 
			
		||||
import { pageResources, renderPage } from "../../components/renderPage"
 | 
			
		||||
import { FullPageLayout } from "../../cfg"
 | 
			
		||||
import { FilePath, FullSlug } from "../../util/path"
 | 
			
		||||
import { sharedPageComponents } from "../../../quartz.layout"
 | 
			
		||||
import { NotFound } from "../../components"
 | 
			
		||||
import { defaultProcessedContent } from "../vfile"
 | 
			
		||||
 | 
			
		||||
export const NotFoundPage: QuartzEmitterPlugin = () => {
 | 
			
		||||
  const opts: FullPageLayout = {
 | 
			
		||||
    ...sharedPageComponents,
 | 
			
		||||
    pageBody: NotFound(),
 | 
			
		||||
    beforeBody: [],
 | 
			
		||||
    left: [],
 | 
			
		||||
    right: [],
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const { head: Head, pageBody, footer: Footer } = opts
 | 
			
		||||
  const Body = BodyConstructor()
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    name: "404Page",
 | 
			
		||||
    getQuartzComponents() {
 | 
			
		||||
      return [Head, Body, pageBody, Footer]
 | 
			
		||||
    },
 | 
			
		||||
    async emit(ctx, _content, resources, emit): Promise<FilePath[]> {
 | 
			
		||||
      const cfg = ctx.cfg.configuration
 | 
			
		||||
      const slug = "404" as FullSlug
 | 
			
		||||
 | 
			
		||||
      const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
 | 
			
		||||
      const path = url.pathname as FullSlug
 | 
			
		||||
      const externalResources = pageResources(path, resources)
 | 
			
		||||
      const [tree, vfile] = defaultProcessedContent({
 | 
			
		||||
        slug,
 | 
			
		||||
        text: "Not Found",
 | 
			
		||||
        description: "Not Found",
 | 
			
		||||
        frontmatter: { title: "Not Found", tags: [] },
 | 
			
		||||
      })
 | 
			
		||||
      const componentData: QuartzComponentProps = {
 | 
			
		||||
        fileData: vfile.data,
 | 
			
		||||
        externalResources,
 | 
			
		||||
        cfg,
 | 
			
		||||
        children: [],
 | 
			
		||||
        tree,
 | 
			
		||||
        allFiles: [],
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return [
 | 
			
		||||
        await emit({
 | 
			
		||||
          content: renderPage(slug, componentData, opts, externalResources),
 | 
			
		||||
          slug,
 | 
			
		||||
          ext: ".html",
 | 
			
		||||
        }),
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { FilePath, FullSlug, resolveRelative, simplifySlug } from "../../util/path"
 | 
			
		||||
import { FilePath, FullSlug, joinSegments, resolveRelative, simplifySlug } from "../../util/path"
 | 
			
		||||
import { QuartzEmitterPlugin } from "../types"
 | 
			
		||||
import path from "path"
 | 
			
		||||
 | 
			
		||||
@@ -12,15 +12,25 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
 | 
			
		||||
 | 
			
		||||
    for (const [_tree, file] of content) {
 | 
			
		||||
      const ogSlug = simplifySlug(file.data.slug!)
 | 
			
		||||
      const dir = path.posix.relative(argv.directory, file.dirname ?? argv.directory)
 | 
			
		||||
      const dir = path.posix.relative(argv.directory, path.dirname(file.data.filePath!))
 | 
			
		||||
 | 
			
		||||
      let aliases: FullSlug[] = file.data.frontmatter?.aliases ?? file.data.frontmatter?.alias ?? []
 | 
			
		||||
      if (typeof aliases === "string") {
 | 
			
		||||
        aliases = [aliases]
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      for (const alias of aliases) {
 | 
			
		||||
        const slug = path.posix.join(dir, alias) as FullSlug
 | 
			
		||||
      const slugs: FullSlug[] = aliases.map((alias) => path.posix.join(dir, alias) as FullSlug)
 | 
			
		||||
      const permalink = file.data.frontmatter?.permalink
 | 
			
		||||
      if (typeof permalink === "string") {
 | 
			
		||||
        slugs.push(permalink as FullSlug)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      for (let slug of slugs) {
 | 
			
		||||
        // fix any slugs that have trailing slash
 | 
			
		||||
        if (slug.endsWith("/")) {
 | 
			
		||||
          slug = joinSegments(slug, "index") as FullSlug
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const redirUrl = resolveRelative(slug, file.data.slug!)
 | 
			
		||||
        const fp = await emit({
 | 
			
		||||
          content: `
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ import spaRouterScript from "../../components/scripts/spa.inline"
 | 
			
		||||
import plausibleScript from "../../components/scripts/plausible.inline"
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import popoverScript from "../../components/scripts/popover.inline"
 | 
			
		||||
import styles from "../../styles/base.scss"
 | 
			
		||||
import styles from "../../styles/custom.scss"
 | 
			
		||||
import popoverStyle from "../../components/styles/popover.scss"
 | 
			
		||||
import { BuildCtx } from "../../util/ctx"
 | 
			
		||||
import { StaticResources } from "../../util/resources"
 | 
			
		||||
@@ -96,6 +96,15 @@ function addGlobalPageResources(
 | 
			
		||||
      });`)
 | 
			
		||||
  } else if (cfg.analytics?.provider === "plausible") {
 | 
			
		||||
    componentResources.afterDOMLoaded.push(plausibleScript)
 | 
			
		||||
  } else if (cfg.analytics?.provider === "umami") {
 | 
			
		||||
    componentResources.afterDOMLoaded.push(`
 | 
			
		||||
      const umamiScript = document.createElement("script")
 | 
			
		||||
      umamiScript.src = "https://analytics.umami.is/script.js"
 | 
			
		||||
      umamiScript.setAttribute("data-website-id", "${cfg.analytics.websiteId}")
 | 
			
		||||
      umamiScript.async = true
 | 
			
		||||
  
 | 
			
		||||
      document.head.appendChild(umamiScript)
 | 
			
		||||
    `)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (cfg.enableSPA) {
 | 
			
		||||
@@ -107,12 +116,18 @@ function addGlobalPageResources(
 | 
			
		||||
        document.dispatchEvent(event)`)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let wsUrl = `ws://localhost:${ctx.argv.wsPort}`
 | 
			
		||||
 | 
			
		||||
  if (ctx.argv.remoteDevHost) {
 | 
			
		||||
    wsUrl = `wss://${ctx.argv.remoteDevHost}:${ctx.argv.wsPort}`
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (reloadScript) {
 | 
			
		||||
    staticResources.js.push({
 | 
			
		||||
      loadTime: "afterDOMReady",
 | 
			
		||||
      contentType: "inline",
 | 
			
		||||
      script: `
 | 
			
		||||
          const socket = new WebSocket('ws://localhost:3001')
 | 
			
		||||
          const socket = new WebSocket('${wsUrl}')
 | 
			
		||||
          socket.addEventListener('message', () => document.location.reload())
 | 
			
		||||
        `,
 | 
			
		||||
    })
 | 
			
		||||
@@ -149,7 +164,7 @@ export const ComponentResources: QuartzEmitterPlugin<Options> = (opts?: Partial<
 | 
			
		||||
 | 
			
		||||
      addGlobalPageResources(ctx, resources, componentResources)
 | 
			
		||||
 | 
			
		||||
      const stylesheet = joinStyles(ctx.cfg.configuration.theme, styles, ...componentResources.css)
 | 
			
		||||
      const stylesheet = joinStyles(ctx.cfg.configuration.theme, ...componentResources.css, styles)
 | 
			
		||||
      const prescript = joinScripts(componentResources.beforeDOMLoaded)
 | 
			
		||||
      const postscript = joinScripts(componentResources.afterDOMLoaded)
 | 
			
		||||
      const fps = await Promise.all([
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,10 @@
 | 
			
		||||
import { Root } from "hast"
 | 
			
		||||
import { GlobalConfiguration } from "../../cfg"
 | 
			
		||||
import { getDate } from "../../components/Date"
 | 
			
		||||
import { escapeHTML } from "../../util/escape"
 | 
			
		||||
import { FilePath, FullSlug, SimpleSlug, simplifySlug } from "../../util/path"
 | 
			
		||||
import { QuartzEmitterPlugin } from "../types"
 | 
			
		||||
import { toHtml } from "hast-util-to-html"
 | 
			
		||||
import path from "path"
 | 
			
		||||
 | 
			
		||||
export type ContentIndex = Map<FullSlug, ContentDetails>
 | 
			
		||||
@@ -10,6 +13,7 @@ export type ContentDetails = {
 | 
			
		||||
  links: SimpleSlug[]
 | 
			
		||||
  tags: string[]
 | 
			
		||||
  content: string
 | 
			
		||||
  richContent?: string
 | 
			
		||||
  date?: Date
 | 
			
		||||
  description?: string
 | 
			
		||||
}
 | 
			
		||||
@@ -17,19 +21,23 @@ export type ContentDetails = {
 | 
			
		||||
interface Options {
 | 
			
		||||
  enableSiteMap: boolean
 | 
			
		||||
  enableRSS: boolean
 | 
			
		||||
  rssLimit?: number
 | 
			
		||||
  rssFullHtml: boolean
 | 
			
		||||
  includeEmptyFiles: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultOptions: Options = {
 | 
			
		||||
  enableSiteMap: true,
 | 
			
		||||
  enableRSS: true,
 | 
			
		||||
  rssLimit: 10,
 | 
			
		||||
  rssFullHtml: false,
 | 
			
		||||
  includeEmptyFiles: true,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string {
 | 
			
		||||
  const base = cfg.baseUrl ?? ""
 | 
			
		||||
  const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<url>
 | 
			
		||||
    <loc>https://${base}/${slug}</loc>
 | 
			
		||||
    <loc>https://${base}/${encodeURI(slug)}</loc>
 | 
			
		||||
    <lastmod>${content.date?.toISOString()}</lastmod>
 | 
			
		||||
  </url>`
 | 
			
		||||
  const urls = Array.from(idx)
 | 
			
		||||
@@ -38,27 +46,42 @@ function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string {
 | 
			
		||||
  return `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">${urls}</urlset>`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex): string {
 | 
			
		||||
function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: number): string {
 | 
			
		||||
  const base = cfg.baseUrl ?? ""
 | 
			
		||||
  const root = `https://${base}`
 | 
			
		||||
 | 
			
		||||
  const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<item>
 | 
			
		||||
    <title>${content.title}</title>
 | 
			
		||||
    <link>${root}/${slug}</link>
 | 
			
		||||
    <guid>${root}/${slug}</guid>
 | 
			
		||||
    <description>${content.description}</description>
 | 
			
		||||
    <title>${escapeHTML(content.title)}</title>
 | 
			
		||||
    <link>${root}/${encodeURI(slug)}</link>
 | 
			
		||||
    <guid>${root}/${encodeURI(slug)}</guid>
 | 
			
		||||
    <description>${content.richContent ?? content.description}</description>
 | 
			
		||||
    <pubDate>${content.date?.toUTCString()}</pubDate>
 | 
			
		||||
  </item>`
 | 
			
		||||
 | 
			
		||||
  const items = Array.from(idx)
 | 
			
		||||
    .sort(([_, f1], [__, f2]) => {
 | 
			
		||||
      if (f1.date && f2.date) {
 | 
			
		||||
        return f2.date.getTime() - f1.date.getTime()
 | 
			
		||||
      } else if (f1.date && !f2.date) {
 | 
			
		||||
        return -1
 | 
			
		||||
      } else if (!f1.date && f2.date) {
 | 
			
		||||
        return 1
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return f1.title.localeCompare(f2.title)
 | 
			
		||||
    })
 | 
			
		||||
    .map(([slug, content]) => createURLEntry(simplifySlug(slug), content))
 | 
			
		||||
    .slice(0, limit ?? idx.size)
 | 
			
		||||
    .join("")
 | 
			
		||||
 | 
			
		||||
  return `<?xml version="1.0" encoding="UTF-8" ?>
 | 
			
		||||
<rss version="2.0">
 | 
			
		||||
    <channel>
 | 
			
		||||
      <title>${cfg.pageTitle}</title>
 | 
			
		||||
      <title>${escapeHTML(cfg.pageTitle)}</title>
 | 
			
		||||
      <link>${root}</link>
 | 
			
		||||
      <description>Recent content on ${cfg.pageTitle}</description>
 | 
			
		||||
      <description>${!!limit ? `Last ${limit} notes` : "Recent notes"} on ${escapeHTML(
 | 
			
		||||
        cfg.pageTitle,
 | 
			
		||||
      )}</description>
 | 
			
		||||
      <generator>Quartz -- quartz.jzhao.xyz</generator>
 | 
			
		||||
      ${items}
 | 
			
		||||
    </channel>
 | 
			
		||||
@@ -73,7 +96,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
 | 
			
		||||
      const cfg = ctx.cfg.configuration
 | 
			
		||||
      const emitted: FilePath[] = []
 | 
			
		||||
      const linkIndex: ContentIndex = new Map()
 | 
			
		||||
      for (const [_tree, file] of content) {
 | 
			
		||||
      for (const [tree, file] of content) {
 | 
			
		||||
        const slug = file.data.slug!
 | 
			
		||||
        const date = getDate(ctx.cfg.configuration, file.data) ?? new Date()
 | 
			
		||||
        if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) {
 | 
			
		||||
@@ -82,6 +105,9 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
 | 
			
		||||
            links: file.data.links ?? [],
 | 
			
		||||
            tags: file.data.frontmatter?.tags ?? [],
 | 
			
		||||
            content: file.data.text ?? "",
 | 
			
		||||
            richContent: opts?.rssFullHtml
 | 
			
		||||
              ? escapeHTML(toHtml(tree as Root, { allowDangerousHtml: true }))
 | 
			
		||||
              : undefined,
 | 
			
		||||
            date: date,
 | 
			
		||||
            description: file.data.description ?? "",
 | 
			
		||||
          })
 | 
			
		||||
@@ -101,7 +127,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
 | 
			
		||||
      if (opts?.enableRSS) {
 | 
			
		||||
        emitted.push(
 | 
			
		||||
          await emit({
 | 
			
		||||
            content: generateRSSFeed(cfg, linkIndex),
 | 
			
		||||
            content: generateRSSFeed(cfg, linkIndex, opts.rssLimit),
 | 
			
		||||
            slug: "index" as FullSlug,
 | 
			
		||||
            ext: ".xml",
 | 
			
		||||
          }),
 | 
			
		||||
 
 | 
			
		||||
@@ -4,9 +4,10 @@ import HeaderConstructor from "../../components/Header"
 | 
			
		||||
import BodyConstructor from "../../components/Body"
 | 
			
		||||
import { pageResources, renderPage } from "../../components/renderPage"
 | 
			
		||||
import { FullPageLayout } from "../../cfg"
 | 
			
		||||
import { FilePath } from "../../util/path"
 | 
			
		||||
import { FilePath, pathToRoot } from "../../util/path"
 | 
			
		||||
import { defaultContentPageLayout, sharedPageComponents } from "../../../quartz.layout"
 | 
			
		||||
import { Content } from "../../components"
 | 
			
		||||
import chalk from "chalk"
 | 
			
		||||
 | 
			
		||||
export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
 | 
			
		||||
  const opts: FullPageLayout = {
 | 
			
		||||
@@ -29,9 +30,15 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
 | 
			
		||||
      const cfg = ctx.cfg.configuration
 | 
			
		||||
      const fps: FilePath[] = []
 | 
			
		||||
      const allFiles = content.map((c) => c[1].data)
 | 
			
		||||
 | 
			
		||||
      let containsIndex = false
 | 
			
		||||
      for (const [tree, file] of content) {
 | 
			
		||||
        const slug = file.data.slug!
 | 
			
		||||
        const externalResources = pageResources(slug, resources)
 | 
			
		||||
        if (slug === "index") {
 | 
			
		||||
          containsIndex = true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const externalResources = pageResources(pathToRoot(slug), resources)
 | 
			
		||||
        const componentData: QuartzComponentProps = {
 | 
			
		||||
          fileData: file.data,
 | 
			
		||||
          externalResources,
 | 
			
		||||
@@ -50,6 +57,15 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
 | 
			
		||||
 | 
			
		||||
        fps.push(fp)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!containsIndex) {
 | 
			
		||||
        console.log(
 | 
			
		||||
          chalk.yellow(
 | 
			
		||||
            `\nWarning: you seem to be missing an \`index.md\` home page file at the root of your \`${ctx.argv.directory}\` folder. This may cause errors when deploying.`,
 | 
			
		||||
          ),
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return fps
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ import {
 | 
			
		||||
  SimpleSlug,
 | 
			
		||||
  _stripSlashes,
 | 
			
		||||
  joinSegments,
 | 
			
		||||
  pathToRoot,
 | 
			
		||||
  simplifySlug,
 | 
			
		||||
} from "../../util/path"
 | 
			
		||||
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
 | 
			
		||||
@@ -69,7 +70,7 @@ export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
 | 
			
		||||
 | 
			
		||||
      for (const folder of folders) {
 | 
			
		||||
        const slug = joinSegments(folder, "index") as FullSlug
 | 
			
		||||
        const externalResources = pageResources(slug, resources)
 | 
			
		||||
        const externalResources = pageResources(pathToRoot(slug), resources)
 | 
			
		||||
        const [tree, file] = folderDescriptions[folder]
 | 
			
		||||
        const componentData: QuartzComponentProps = {
 | 
			
		||||
          fileData: file.data,
 | 
			
		||||
 
 | 
			
		||||
@@ -6,3 +6,4 @@ export { AliasRedirects } from "./aliases"
 | 
			
		||||
export { Assets } from "./assets"
 | 
			
		||||
export { Static } from "./static"
 | 
			
		||||
export { ComponentResources } from "./componentResources"
 | 
			
		||||
export { NotFoundPage } from "./404"
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,13 @@ import BodyConstructor from "../../components/Body"
 | 
			
		||||
import { pageResources, renderPage } from "../../components/renderPage"
 | 
			
		||||
import { ProcessedContent, defaultProcessedContent } from "../vfile"
 | 
			
		||||
import { FullPageLayout } from "../../cfg"
 | 
			
		||||
import { FilePath, FullSlug, getAllSegmentPrefixes, joinSegments } from "../../util/path"
 | 
			
		||||
import {
 | 
			
		||||
  FilePath,
 | 
			
		||||
  FullSlug,
 | 
			
		||||
  getAllSegmentPrefixes,
 | 
			
		||||
  joinSegments,
 | 
			
		||||
  pathToRoot,
 | 
			
		||||
} from "../../util/path"
 | 
			
		||||
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
 | 
			
		||||
import { TagContent } from "../../components"
 | 
			
		||||
 | 
			
		||||
@@ -62,7 +68,7 @@ export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
 | 
			
		||||
 | 
			
		||||
      for (const tag of tags) {
 | 
			
		||||
        const slug = joinSegments("tags", tag) as FullSlug
 | 
			
		||||
        const externalResources = pageResources(slug, resources)
 | 
			
		||||
        const externalResources = pageResources(pathToRoot(slug), resources)
 | 
			
		||||
        const [tree, file] = tagDescriptions[tag]
 | 
			
		||||
        const componentData: QuartzComponentProps = {
 | 
			
		||||
          fileData: file.data,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import { Root as HTMLRoot } from "hast"
 | 
			
		||||
import { toString } from "hast-util-to-string"
 | 
			
		||||
import { QuartzTransformerPlugin } from "../types"
 | 
			
		||||
import { escapeHTML } from "../../util/escape"
 | 
			
		||||
 | 
			
		||||
export interface Options {
 | 
			
		||||
  descriptionLength: number
 | 
			
		||||
@@ -10,15 +11,6 @@ const defaultOptions: Options = {
 | 
			
		||||
  descriptionLength: 150,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const escapeHTML = (unsafe: string) => {
 | 
			
		||||
  return unsafe
 | 
			
		||||
    .replaceAll("&", "&")
 | 
			
		||||
    .replaceAll("<", "<")
 | 
			
		||||
    .replaceAll(">", ">")
 | 
			
		||||
    .replaceAll('"', """)
 | 
			
		||||
    .replaceAll("'", "'")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Description: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
 | 
			
		||||
  const opts = { ...defaultOptions, ...userOpts }
 | 
			
		||||
  return {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,14 +2,17 @@ import matter from "gray-matter"
 | 
			
		||||
import remarkFrontmatter from "remark-frontmatter"
 | 
			
		||||
import { QuartzTransformerPlugin } from "../types"
 | 
			
		||||
import yaml from "js-yaml"
 | 
			
		||||
import toml from "toml"
 | 
			
		||||
import { slugTag } from "../../util/path"
 | 
			
		||||
 | 
			
		||||
export interface Options {
 | 
			
		||||
  delims: string | string[]
 | 
			
		||||
  language: "yaml" | "toml"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultOptions: Options = {
 | 
			
		||||
  delims: "---",
 | 
			
		||||
  language: "yaml",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
 | 
			
		||||
@@ -18,13 +21,14 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined>
 | 
			
		||||
    name: "FrontMatter",
 | 
			
		||||
    markdownPlugins() {
 | 
			
		||||
      return [
 | 
			
		||||
        remarkFrontmatter,
 | 
			
		||||
        [remarkFrontmatter, ["yaml", "toml"]],
 | 
			
		||||
        () => {
 | 
			
		||||
          return (_, file) => {
 | 
			
		||||
            const { data } = matter(file.value, {
 | 
			
		||||
              ...opts,
 | 
			
		||||
              engines: {
 | 
			
		||||
                yaml: (s) => yaml.load(s, { schema: yaml.JSON_SCHEMA }) as object,
 | 
			
		||||
                toml: (s) => toml.parse(s) as object,
 | 
			
		||||
              },
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
@@ -33,6 +37,11 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined>
 | 
			
		||||
              data.tags = data.tag
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // coerce title to string
 | 
			
		||||
            if (data.title) {
 | 
			
		||||
              data.title = data.title.toString()
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (data.tags && !Array.isArray(data.tags)) {
 | 
			
		||||
              data.tags = data.tags
 | 
			
		||||
                .toString()
 | 
			
		||||
 
 | 
			
		||||
@@ -5,5 +5,7 @@ export { Latex } from "./latex"
 | 
			
		||||
export { Description } from "./description"
 | 
			
		||||
export { CrawlLinks } from "./links"
 | 
			
		||||
export { ObsidianFlavoredMarkdown } from "./ofm"
 | 
			
		||||
export { OxHugoFlavouredMarkdown } from "./oxhugofm"
 | 
			
		||||
export { SyntaxHighlighting } from "./syntax"
 | 
			
		||||
export { TableOfContents } from "./toc"
 | 
			
		||||
export { HardLineBreaks } from "./linebreaks"
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ import fs from "fs"
 | 
			
		||||
import path from "path"
 | 
			
		||||
import { Repository } from "@napi-rs/simple-git"
 | 
			
		||||
import { QuartzTransformerPlugin } from "../types"
 | 
			
		||||
import chalk from "chalk"
 | 
			
		||||
 | 
			
		||||
export interface Options {
 | 
			
		||||
  priority: ("frontmatter" | "git" | "filesystem")[]
 | 
			
		||||
@@ -11,6 +12,20 @@ const defaultOptions: Options = {
 | 
			
		||||
  priority: ["frontmatter", "git", "filesystem"],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function coerceDate(fp: string, d: any): Date {
 | 
			
		||||
  const dt = new Date(d)
 | 
			
		||||
  const invalidDate = isNaN(dt.getTime()) || dt.getTime() === 0
 | 
			
		||||
  if (invalidDate && d !== undefined) {
 | 
			
		||||
    console.log(
 | 
			
		||||
      chalk.yellow(
 | 
			
		||||
        `\nWarning: found invalid date "${d}" in \`${fp}\`. Supported formats: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format`,
 | 
			
		||||
      ),
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return invalidDate ? new Date() : dt
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MaybeDate = undefined | string | number
 | 
			
		||||
export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | undefined> = (
 | 
			
		||||
  userOpts,
 | 
			
		||||
@@ -27,10 +42,11 @@ export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | und
 | 
			
		||||
            let modified: MaybeDate = undefined
 | 
			
		||||
            let published: MaybeDate = undefined
 | 
			
		||||
 | 
			
		||||
            const fp = path.posix.join(file.cwd, file.data.filePath as string)
 | 
			
		||||
            const fp = file.data.filePath!
 | 
			
		||||
            const fullFp = path.posix.join(file.cwd, fp)
 | 
			
		||||
            for (const source of opts.priority) {
 | 
			
		||||
              if (source === "filesystem") {
 | 
			
		||||
                const st = await fs.promises.stat(fp)
 | 
			
		||||
                const st = await fs.promises.stat(fullFp)
 | 
			
		||||
                created ||= st.birthtimeMs
 | 
			
		||||
                modified ||= st.mtimeMs
 | 
			
		||||
              } else if (source === "frontmatter" && file.data.frontmatter) {
 | 
			
		||||
@@ -49,9 +65,9 @@ export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | und
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            file.data.dates = {
 | 
			
		||||
              created: created ? new Date(created) : new Date(),
 | 
			
		||||
              modified: modified ? new Date(modified) : new Date(),
 | 
			
		||||
              published: published ? new Date(published) : new Date(),
 | 
			
		||||
              created: coerceDate(fp, created),
 | 
			
		||||
              modified: coerceDate(fp, modified),
 | 
			
		||||
              published: coerceDate(fp, published),
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								quartz/plugins/transformers/linebreaks.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								quartz/plugins/transformers/linebreaks.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
import { QuartzTransformerPlugin } from "../types"
 | 
			
		||||
import remarkBreaks from "remark-breaks"
 | 
			
		||||
 | 
			
		||||
export const HardLineBreaks: QuartzTransformerPlugin = () => {
 | 
			
		||||
  return {
 | 
			
		||||
    name: "HardLineBreaks",
 | 
			
		||||
    markdownPlugins() {
 | 
			
		||||
      return [remarkBreaks]
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -5,7 +5,6 @@ import {
 | 
			
		||||
  SimpleSlug,
 | 
			
		||||
  TransformOptions,
 | 
			
		||||
  _stripSlashes,
 | 
			
		||||
  joinSegments,
 | 
			
		||||
  simplifySlug,
 | 
			
		||||
  splitAnchor,
 | 
			
		||||
  transformLink,
 | 
			
		||||
@@ -19,11 +18,13 @@ interface Options {
 | 
			
		||||
  markdownLinkResolution: TransformOptions["strategy"]
 | 
			
		||||
  /** Strips folders from a link so that it looks nice */
 | 
			
		||||
  prettyLinks: boolean
 | 
			
		||||
  openLinksInNewTab: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultOptions: Options = {
 | 
			
		||||
  markdownLinkResolution: "absolute",
 | 
			
		||||
  prettyLinks: true,
 | 
			
		||||
  openLinksInNewTab: false,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
 | 
			
		||||
@@ -53,8 +54,13 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
 | 
			
		||||
                node.properties.className ??= []
 | 
			
		||||
                node.properties.className.push(isAbsoluteUrl(dest) ? "external" : "internal")
 | 
			
		||||
 | 
			
		||||
                if (opts.openLinksInNewTab) {
 | 
			
		||||
                  node.properties.target = "_blank"
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // don't process external links or intra-document anchors
 | 
			
		||||
                if (!(isAbsoluteUrl(dest) || dest.startsWith("#"))) {
 | 
			
		||||
                const isInternal = !(isAbsoluteUrl(dest) || dest.startsWith("#"))
 | 
			
		||||
                if (isInternal) {
 | 
			
		||||
                  dest = node.properties.href = transformLink(
 | 
			
		||||
                    file.data.slug!,
 | 
			
		||||
                    dest,
 | 
			
		||||
@@ -72,11 +78,13 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
 | 
			
		||||
                    simplifySlug(destCanonical as FullSlug),
 | 
			
		||||
                  ) as SimpleSlug
 | 
			
		||||
                  outgoing.add(simple)
 | 
			
		||||
                  node.properties["data-slug"] = simple
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // rewrite link internals if prettylinks is on
 | 
			
		||||
                if (
 | 
			
		||||
                  opts.prettyLinks &&
 | 
			
		||||
                  isInternal &&
 | 
			
		||||
                  node.children.length === 1 &&
 | 
			
		||||
                  node.children[0].type === "text" &&
 | 
			
		||||
                  !node.children[0].value.startsWith("#")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import { PluggableList } from "unified"
 | 
			
		||||
import { QuartzTransformerPlugin } from "../types"
 | 
			
		||||
import { Root, HTML, BlockContent, DefinitionContent, Code, Paragraph } from "mdast"
 | 
			
		||||
import { Element, Literal, Root as HtmlRoot } from "hast"
 | 
			
		||||
import { Replace, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace"
 | 
			
		||||
import { slug as slugAnchor } from "github-slugger"
 | 
			
		||||
import rehypeRaw from "rehype-raw"
 | 
			
		||||
@@ -13,6 +14,7 @@ import { FilePath, pathToRoot, slugTag, slugifyFilePath } from "../../util/path"
 | 
			
		||||
import { toHast } from "mdast-util-to-hast"
 | 
			
		||||
import { toHtml } from "hast-util-to-html"
 | 
			
		||||
import { PhrasingContent } from "mdast-util-find-and-replace/lib"
 | 
			
		||||
import { capitalize } from "../../util/lang"
 | 
			
		||||
 | 
			
		||||
export interface Options {
 | 
			
		||||
  comments: boolean
 | 
			
		||||
@@ -21,6 +23,7 @@ export interface Options {
 | 
			
		||||
  callouts: boolean
 | 
			
		||||
  mermaid: boolean
 | 
			
		||||
  parseTags: boolean
 | 
			
		||||
  parseBlockReferences: boolean
 | 
			
		||||
  enableInHtmlEmbed: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -31,6 +34,7 @@ const defaultOptions: Options = {
 | 
			
		||||
  callouts: true,
 | 
			
		||||
  mermaid: true,
 | 
			
		||||
  parseTags: true,
 | 
			
		||||
  parseBlockReferences: true,
 | 
			
		||||
  enableInHtmlEmbed: false,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -69,6 +73,8 @@ const callouts = {
 | 
			
		||||
const calloutMapping: Record<string, keyof typeof callouts> = {
 | 
			
		||||
  note: "note",
 | 
			
		||||
  abstract: "abstract",
 | 
			
		||||
  summary: "abstract",
 | 
			
		||||
  tldr: "abstract",
 | 
			
		||||
  info: "info",
 | 
			
		||||
  todo: "todo",
 | 
			
		||||
  tip: "tip",
 | 
			
		||||
@@ -96,11 +102,7 @@ const calloutMapping: Record<string, keyof typeof callouts> = {
 | 
			
		||||
 | 
			
		||||
function canonicalizeCallout(calloutName: string): keyof typeof callouts {
 | 
			
		||||
  let callout = calloutName.toLowerCase() as keyof typeof calloutMapping
 | 
			
		||||
  return calloutMapping[callout] ?? calloutName
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const capitalize = (s: string): string => {
 | 
			
		||||
  return s.substring(0, 1).toUpperCase() + s.substring(1)
 | 
			
		||||
  return calloutMapping[callout] ?? "note"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// !?               -> optional embedding
 | 
			
		||||
@@ -109,14 +111,17 @@ const capitalize = (s: string): string => {
 | 
			
		||||
// (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link)
 | 
			
		||||
// (|[^\[\]\|\#]+)? -> | then one or more non-special characters (alias)
 | 
			
		||||
const wikilinkRegex = new RegExp(/!?\[\[([^\[\]\|\#]+)?(#[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/, "g")
 | 
			
		||||
const highlightRegex = new RegExp(/==(.+)==/, "g")
 | 
			
		||||
const highlightRegex = new RegExp(/==([^=]+)==/, "g")
 | 
			
		||||
const commentRegex = new RegExp(/%%(.+)%%/, "g")
 | 
			
		||||
// from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts
 | 
			
		||||
const calloutRegex = new RegExp(/^\[\!(\w+)\]([+-]?)/)
 | 
			
		||||
const calloutLineRegex = new RegExp(/^> *\[\!\w+\][+-]?.*$/, "gm")
 | 
			
		||||
// (?:^| )   -> non-capturing group, tag should start be separated by a space or be the start of the line
 | 
			
		||||
// #(\w+)    -> tag itself is # followed by a string of alpha-numeric characters
 | 
			
		||||
const tagRegex = new RegExp(/(?:^| )#(\p{L}+)/, "gu")
 | 
			
		||||
// (?:^| )              -> non-capturing group, tag should start be separated by a space or be the start of the line
 | 
			
		||||
// #(...)               -> capturing group, tag itself must start with #
 | 
			
		||||
// (?:[-_\p{L}])+       -> non-capturing group, non-empty string of (Unicode-aware) alpha-numeric characters, hyphens and/or underscores
 | 
			
		||||
// (?:\/[-_\p{L}]+)*)   -> non-capturing group, matches an arbitrary number of tag strings separated by "/"
 | 
			
		||||
const tagRegex = new RegExp(/(?:^| )#((?:[-_\p{L}\d])+(?:\/[-_\p{L}\d]+)*)/, "gu")
 | 
			
		||||
const blockReferenceRegex = new RegExp(/\^([A-Za-z0-9]+)$/, "g")
 | 
			
		||||
 | 
			
		||||
export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (
 | 
			
		||||
  userOpts,
 | 
			
		||||
@@ -230,8 +235,16 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
 | 
			
		||||
                    value: `<iframe src="${url}"></iframe>`,
 | 
			
		||||
                  }
 | 
			
		||||
                } else if (ext === "") {
 | 
			
		||||
                  // TODO: note embed
 | 
			
		||||
                  const block = anchor
 | 
			
		||||
                  return {
 | 
			
		||||
                    type: "html",
 | 
			
		||||
                    data: { hProperties: { transclude: true } },
 | 
			
		||||
                    value: `<blockquote class="transclude" data-url="${url}" data-block="${block}"><a href="${
 | 
			
		||||
                      url + anchor
 | 
			
		||||
                    }" class="transclude-inner">Transclude of ${url}${block}</a></blockquote>`,
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // otherwise, fall through to regular link
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
@@ -320,7 +333,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
 | 
			
		||||
 | 
			
		||||
                const titleHtml: HTML = {
 | 
			
		||||
                  type: "html",
 | 
			
		||||
                  value: `<div 
 | 
			
		||||
                  value: `<div
 | 
			
		||||
                  class="callout-title"
 | 
			
		||||
                >
 | 
			
		||||
                  <div class="callout-icon">${callouts[calloutType]}</div>
 | 
			
		||||
@@ -383,13 +396,18 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
 | 
			
		||||
          return (tree: Root, file) => {
 | 
			
		||||
            const base = pathToRoot(file.data.slug!)
 | 
			
		||||
            findAndReplace(tree, tagRegex, (_value: string, tag: string) => {
 | 
			
		||||
              // Check if the tag only includes numbers
 | 
			
		||||
              if (/^\d+$/.test(tag)) {
 | 
			
		||||
                return false
 | 
			
		||||
              }
 | 
			
		||||
              tag = slugTag(tag)
 | 
			
		||||
              if (file.data.frontmatter && !file.data.frontmatter.tags.includes(tag)) {
 | 
			
		||||
                file.data.frontmatter.tags.push(tag)
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              return {
 | 
			
		||||
                type: "link",
 | 
			
		||||
                url: base + `/tags/${slugTag(tag)}`,
 | 
			
		||||
                url: base + `/tags/${tag}`,
 | 
			
		||||
                data: {
 | 
			
		||||
                  hProperties: {
 | 
			
		||||
                    className: ["tag-link"],
 | 
			
		||||
@@ -406,11 +424,64 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return plugins
 | 
			
		||||
    },
 | 
			
		||||
    htmlPlugins() {
 | 
			
		||||
      return [rehypeRaw]
 | 
			
		||||
      const plugins = [rehypeRaw]
 | 
			
		||||
 | 
			
		||||
      if (opts.parseBlockReferences) {
 | 
			
		||||
        plugins.push(() => {
 | 
			
		||||
          const inlineTagTypes = new Set(["p", "li"])
 | 
			
		||||
          const blockTagTypes = new Set(["blockquote"])
 | 
			
		||||
          return (tree, file) => {
 | 
			
		||||
            file.data.blocks = {}
 | 
			
		||||
            file.data.htmlAst = tree
 | 
			
		||||
 | 
			
		||||
            visit(tree, "element", (node, index, parent) => {
 | 
			
		||||
              if (blockTagTypes.has(node.tagName)) {
 | 
			
		||||
                const nextChild = parent?.children.at(index! + 2) as Element
 | 
			
		||||
                if (nextChild && nextChild.tagName === "p") {
 | 
			
		||||
                  const text = nextChild.children.at(0) as Literal
 | 
			
		||||
                  if (text && text.value && text.type === "text") {
 | 
			
		||||
                    const matches = text.value.match(blockReferenceRegex)
 | 
			
		||||
                    if (matches && matches.length >= 1) {
 | 
			
		||||
                      parent!.children.splice(index! + 2, 1)
 | 
			
		||||
                      const block = matches[0].slice(1)
 | 
			
		||||
 | 
			
		||||
                      if (!Object.keys(file.data.blocks!).includes(block)) {
 | 
			
		||||
                        node.properties = {
 | 
			
		||||
                          ...node.properties,
 | 
			
		||||
                          id: block,
 | 
			
		||||
                        }
 | 
			
		||||
                        file.data.blocks![block] = node
 | 
			
		||||
                      }
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              } else if (inlineTagTypes.has(node.tagName)) {
 | 
			
		||||
                const last = node.children.at(-1) as Literal
 | 
			
		||||
                if (last && last.value && typeof last.value === "string") {
 | 
			
		||||
                  const matches = last.value.match(blockReferenceRegex)
 | 
			
		||||
                  if (matches && matches.length >= 1) {
 | 
			
		||||
                    last.value = last.value.slice(0, -matches[0].length)
 | 
			
		||||
                    const block = matches[0].slice(1)
 | 
			
		||||
 | 
			
		||||
                    if (!Object.keys(file.data.blocks!).includes(block)) {
 | 
			
		||||
                      node.properties = {
 | 
			
		||||
                        ...node.properties,
 | 
			
		||||
                        id: block,
 | 
			
		||||
                      }
 | 
			
		||||
                      file.data.blocks![block] = node
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return plugins
 | 
			
		||||
    },
 | 
			
		||||
    externalResources() {
 | 
			
		||||
      const js: JSResource[] = []
 | 
			
		||||
@@ -428,7 +499,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
 | 
			
		||||
          script: `
 | 
			
		||||
          import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs';
 | 
			
		||||
          const darkMode = document.documentElement.getAttribute('saved-theme') === 'dark'
 | 
			
		||||
          mermaid.initialize({ 
 | 
			
		||||
          mermaid.initialize({
 | 
			
		||||
            startOnLoad: false,
 | 
			
		||||
            securityLevel: 'loose',
 | 
			
		||||
            theme: darkMode ? 'dark' : 'default'
 | 
			
		||||
@@ -449,3 +520,10 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare module "vfile" {
 | 
			
		||||
  interface DataMap {
 | 
			
		||||
    blocks: Record<string, Element>
 | 
			
		||||
    htmlAst: HtmlRoot
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										108
									
								
								quartz/plugins/transformers/oxhugofm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								quartz/plugins/transformers/oxhugofm.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,108 @@
 | 
			
		||||
import { QuartzTransformerPlugin } from "../types"
 | 
			
		||||
 | 
			
		||||
export interface Options {
 | 
			
		||||
  /** Replace {{ relref }} with quartz wikilinks []() */
 | 
			
		||||
  wikilinks: boolean
 | 
			
		||||
  /** Remove pre-defined anchor (see https://ox-hugo.scripter.co/doc/anchors/) */
 | 
			
		||||
  removePredefinedAnchor: boolean
 | 
			
		||||
  /** Remove hugo shortcode syntax */
 | 
			
		||||
  removeHugoShortcode: boolean
 | 
			
		||||
  /** Replace <figure/> with ![]() */
 | 
			
		||||
  replaceFigureWithMdImg: boolean
 | 
			
		||||
 | 
			
		||||
  /** Replace org latex fragments with $ and $$ */
 | 
			
		||||
  replaceOrgLatex: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultOptions: Options = {
 | 
			
		||||
  wikilinks: true,
 | 
			
		||||
  removePredefinedAnchor: true,
 | 
			
		||||
  removeHugoShortcode: true,
 | 
			
		||||
  replaceFigureWithMdImg: true,
 | 
			
		||||
  replaceOrgLatex: true,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const relrefRegex = new RegExp(/\[([^\]]+)\]\(\{\{< relref "([^"]+)" >\}\}\)/, "g")
 | 
			
		||||
const predefinedHeadingIdRegex = new RegExp(/(.*) {#(?:.*)}/, "g")
 | 
			
		||||
const hugoShortcodeRegex = new RegExp(/{{(.*)}}/, "g")
 | 
			
		||||
const figureTagRegex = new RegExp(/< ?figure src="(.*)" ?>/, "g")
 | 
			
		||||
// \\\\\( -> matches \\(
 | 
			
		||||
// (.+?) -> Lazy match for capturing the equation
 | 
			
		||||
// \\\\\) -> matches \\)
 | 
			
		||||
const inlineLatexRegex = new RegExp(/\\\\\((.+?)\\\\\)/, "g")
 | 
			
		||||
// (?:\\begin{equation}|\\\\\(|\\\\\[) -> start of equation
 | 
			
		||||
// ([\s\S]*?) -> Matches the block equation
 | 
			
		||||
// (?:\\\\\]|\\\\\)|\\end{equation}) -> end of equation
 | 
			
		||||
const blockLatexRegex = new RegExp(
 | 
			
		||||
  /(?:\\begin{equation}|\\\\\(|\\\\\[)([\s\S]*?)(?:\\\\\]|\\\\\)|\\end{equation})/,
 | 
			
		||||
  "g",
 | 
			
		||||
)
 | 
			
		||||
// \$\$[\s\S]*?\$\$ -> Matches block equations
 | 
			
		||||
// \$.*?\$ -> Matches inline equations
 | 
			
		||||
const quartzLatexRegex = new RegExp(/\$\$[\s\S]*?\$\$|\$.*?\$/, "g")
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * ox-hugo is an org exporter backend that exports org files to hugo-compatible
 | 
			
		||||
 * markdown in an opinionated way. This plugin adds some tweaks to the generated
 | 
			
		||||
 * markdown to make it compatible with quartz but the list of changes applied it
 | 
			
		||||
 * is not exhaustive.
 | 
			
		||||
 * */
 | 
			
		||||
export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (
 | 
			
		||||
  userOpts,
 | 
			
		||||
) => {
 | 
			
		||||
  const opts = { ...defaultOptions, ...userOpts }
 | 
			
		||||
  return {
 | 
			
		||||
    name: "OxHugoFlavouredMarkdown",
 | 
			
		||||
    textTransform(_ctx, src) {
 | 
			
		||||
      if (opts.wikilinks) {
 | 
			
		||||
        src = src.toString()
 | 
			
		||||
        src = src.replaceAll(relrefRegex, (value, ...capture) => {
 | 
			
		||||
          const [text, link] = capture
 | 
			
		||||
          return `[${text}](${link})`
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (opts.removePredefinedAnchor) {
 | 
			
		||||
        src = src.toString()
 | 
			
		||||
        src = src.replaceAll(predefinedHeadingIdRegex, (value, ...capture) => {
 | 
			
		||||
          const [headingText] = capture
 | 
			
		||||
          return headingText
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (opts.removeHugoShortcode) {
 | 
			
		||||
        src = src.toString()
 | 
			
		||||
        src = src.replaceAll(hugoShortcodeRegex, (value, ...capture) => {
 | 
			
		||||
          const [scContent] = capture
 | 
			
		||||
          return scContent
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (opts.replaceFigureWithMdImg) {
 | 
			
		||||
        src = src.toString()
 | 
			
		||||
        src = src.replaceAll(figureTagRegex, (value, ...capture) => {
 | 
			
		||||
          const [src] = capture
 | 
			
		||||
          return ``
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (opts.replaceOrgLatex) {
 | 
			
		||||
        src = src.toString()
 | 
			
		||||
        src = src.replaceAll(inlineLatexRegex, (value, ...capture) => {
 | 
			
		||||
          const [eqn] = capture
 | 
			
		||||
          return `$${eqn}$`
 | 
			
		||||
        })
 | 
			
		||||
        src = src.replaceAll(blockLatexRegex, (value, ...capture) => {
 | 
			
		||||
          const [eqn] = capture
 | 
			
		||||
          return `$$${eqn}$$`
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        // ox-hugo escapes _ as \_
 | 
			
		||||
        src = src.replaceAll(quartzLatexRegex, (value) => {
 | 
			
		||||
          return value.replaceAll("\\_", "_")
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
      return src
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -8,12 +8,14 @@ export interface Options {
 | 
			
		||||
  maxDepth: 1 | 2 | 3 | 4 | 5 | 6
 | 
			
		||||
  minEntries: 1
 | 
			
		||||
  showByDefault: boolean
 | 
			
		||||
  collapseByDefault: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultOptions: Options = {
 | 
			
		||||
  maxDepth: 3,
 | 
			
		||||
  minEntries: 1,
 | 
			
		||||
  showByDefault: true,
 | 
			
		||||
  collapseByDefault: false,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface TocEntry {
 | 
			
		||||
@@ -54,6 +56,7 @@ export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefin
 | 
			
		||||
                  ...entry,
 | 
			
		||||
                  depth: entry.depth - highestDepth,
 | 
			
		||||
                }))
 | 
			
		||||
                file.data.collapseToc = opts.collapseByDefault
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
@@ -66,5 +69,6 @@ export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefin
 | 
			
		||||
declare module "vfile" {
 | 
			
		||||
  interface DataMap {
 | 
			
		||||
    toc: TocEntry[]
 | 
			
		||||
    collapseToc: boolean
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user