Jacky Zhao
2023-07-02 e0ebee5aa9b3646de722f139f1d8d15591df538e
various polish
1 files added
29 files modified
447 ■■■■■ changed files
quartz.config.ts 36 ●●●●● patch | view | raw | blame | history
quartz/bootstrap-cli.mjs 2 ●●● patch | view | raw | blame | history
quartz/cfg.ts 11 ●●●●● patch | view | raw | blame | history
quartz/components/ArticleTitle.tsx 5 ●●●●● patch | view | raw | blame | history
quartz/components/Footer.tsx 2 ●●● patch | view | raw | blame | history
quartz/components/Head.tsx 15 ●●●●● patch | view | raw | blame | history
quartz/components/Header.tsx 1 ●●●● patch | view | raw | blame | history
quartz/components/PageList.tsx 5 ●●●●● patch | view | raw | blame | history
quartz/components/TagList.tsx 13 ●●●● patch | view | raw | blame | history
quartz/components/pages/Content.tsx 2 ●●● patch | view | raw | blame | history
quartz/components/renderPage.tsx 35 ●●●● patch | view | raw | blame | history
quartz/components/scripts/darkmode.inline.ts 5 ●●●●● patch | view | raw | blame | history
quartz/components/scripts/graph.inline.ts 13 ●●●● patch | view | raw | blame | history
quartz/components/scripts/plausible.inline.ts 3 ●●●●● patch | view | raw | blame | history
quartz/components/scripts/popover.inline.ts 22 ●●●●● patch | view | raw | blame | history
quartz/components/styles/backlinks.scss 6 ●●●● patch | view | raw | blame | history
quartz/components/styles/footer.scss 2 ●●●●● patch | view | raw | blame | history
quartz/components/styles/graph.scss 5 ●●●● patch | view | raw | blame | history
quartz/components/styles/listPage.scss 27 ●●●●● patch | view | raw | blame | history
quartz/components/styles/popover.scss 2 ●●● patch | view | raw | blame | history
quartz/components/styles/search.scss 3 ●●●● patch | view | raw | blame | history
quartz/components/types.ts 2 ●●● patch | view | raw | blame | history
quartz/path.ts 13 ●●●● patch | view | raw | blame | history
quartz/plugins/emitters/contentIndex.ts 1 ●●●● patch | view | raw | blame | history
quartz/plugins/index.ts 48 ●●●●● patch | view | raw | blame | history
quartz/plugins/transformers/gfm.ts 5 ●●●●● patch | view | raw | blame | history
quartz/plugins/transformers/links.ts 5 ●●●●● patch | view | raw | blame | history
quartz/processors/emit.ts 78 ●●●●● patch | view | raw | blame | history
quartz/resources.tsx 8 ●●●●● patch | view | raw | blame | history
quartz/styles/base.scss 72 ●●●● patch | view | raw | blame | history
quartz.config.ts
@@ -4,12 +4,7 @@
const sharedPageComponents = {
  head: Component.Head(),
  header: [
    Component.PageTitle(),
    Component.Spacer(),
    Component.Search(),
    Component.Darkmode()
  ],
  header: [],
  footer: Component.Footer({
    authorName: "Jacky",
    links: {
@@ -25,11 +20,15 @@
    Component.ReadingTime(),
    Component.TagList(),
  ],
  left: [],
  left: [
    Component.PageTitle(),
    Component.Search(),
    Component.TableOfContents(),
    Component.Darkmode()
  ],
  right: [
    Component.Graph(),
    Component.TableOfContents(),
    Component.Backlinks()
    Component.Backlinks(),
  ],
}
@@ -37,7 +36,11 @@
  beforeBody: [
    Component.ArticleTitle()
  ],
  left: [],
  left: [
    Component.PageTitle(),
    Component.Search(),
    Component.Darkmode()
  ],
  right: [],
}
@@ -46,6 +49,9 @@
    pageTitle: "🪴 Quartz 4.0",
    enableSPA: true,
    enablePopovers: true,
    analytics: {
      provider: 'plausible',
    },
    canonicalUrl: "quartz.jzhao.xyz",
    ignorePatterns: ["private", "templates"],
    theme: {
@@ -102,16 +108,16 @@
        ...contentPageLayout,
        pageBody: Component.Content(),
      }),
      Plugin.TagPage({
        ...sharedPageComponents,
        ...listPageLayout,
        pageBody: Component.TagContent(),
      }),
      Plugin.FolderPage({
        ...sharedPageComponents,
        ...listPageLayout,
        pageBody: Component.FolderContent(),
      }),
      Plugin.TagPage({
        ...sharedPageComponents,
        ...listPageLayout,
        pageBody: Component.TagContent(),
      }),
      Plugin.ContentIndex({
        enableSiteMap: true,
        enableRSS: true,
quartz/bootstrap-cli.mjs
@@ -64,7 +64,7 @@
      packages: "external",
      plugins: [
        sassPlugin({
          type: 'css-text'
          type: 'css-text',
        }),
        {
          name: 'inline-script-loader',
quartz/cfg.ts
@@ -2,12 +2,23 @@
import { PluginTypes } from "./plugins/types"
import { Theme } from "./theme"
export type Analytics = null
  | {
    provider: 'plausible'
  }
  | {
    provider: 'google',
    tagId: string
  }
export interface GlobalConfiguration {
  pageTitle: string,
  /** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */
  enableSPA: boolean,
  /** Whether to display Wikipedia-style popovers when hovering over links */
  enablePopovers: boolean,
  /** Analytics mode */
  analytics: Analytics
  /** Glob patterns to not search */
  ignorePatterns: string[],
  /** Base URL to use for CNAME files, sitemaps, and RSS feeds that require an absolute URL.
quartz/components/ArticleTitle.tsx
@@ -2,9 +2,8 @@
function ArticleTitle({ fileData }: QuartzComponentProps) {
  const title = fileData.frontmatter?.title
  const displayTitle = fileData.slug === "index" ? undefined : title
  if (displayTitle) {
    return <h1 class="article-title">{displayTitle}</h1>
  if (title) {
    return <h1 class="article-title">{title}</h1>
  } else {
    return null
  }
quartz/components/Footer.tsx
@@ -14,7 +14,7 @@
    return <>
      <hr />
      <footer>
        <p>Made by {name} using <a>Quartz</a>, Â© {year}</p>
        <p>Made by {name} using <a href="https://quartz.jzhao.xyz/">Quartz</a>, Â© {year}</p>
        <ul>{Object.entries(links).map(([text, link]) => <li>
          <a href={link}>{text}</a>
        </li>)}</ul>
quartz/components/Head.tsx
@@ -2,15 +2,7 @@
import { JSResourceToScriptElement } from "../resources"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
interface Options {
  prefetchContentIndex: boolean
}
const defaultOptions: Options = {
  prefetchContentIndex: true
}
export default ((opts?: Options) => {
export default (() => {
  function Head({ fileData, externalResources }: QuartzComponentProps) {
    const slug = fileData.slug!
    const title = fileData.frontmatter?.title ?? "Untitled"
@@ -20,10 +12,6 @@
    const iconPath = baseDir + "/static/icon.png"
    const ogImagePath = baseDir + "/static/og-image.png"
    const prefetchContentIndex = opts?.prefetchContentIndex ?? defaultOptions.prefetchContentIndex
    const contentIndexPath = baseDir + "/static/contentIndex.json"
    const contentIndexScript = `const fetchData = fetch(\`${contentIndexPath}\`).then(data => data.json())`
    return <head>
      <title>{title}</title>
      <meta charSet="utf-8" />
@@ -38,7 +26,6 @@
      <meta name="generator" content="Quartz" />
      <link rel="preconnect" href="https://fonts.googleapis.com" />
      <link rel="preconnect" href="https://fonts.gstatic.com" />
      {prefetchContentIndex && <script spa-preserve>{contentIndexScript}</script>}
      {css.map(href => <link key={href} href={href} rel="stylesheet" type="text/css" spa-preserve />)}
      {js.filter(resource => resource.loadTime === "beforeDOMReady").map(res => JSResourceToScriptElement(res, true))}
    </head>
quartz/components/Header.tsx
@@ -12,6 +12,7 @@
  flex-direction: row;
  align-items: center;
  margin: 2em 0;
  gap: 1.5rem;
}
header h1 {
quartz/components/PageList.tsx
@@ -23,7 +23,7 @@
export function PageList({ fileData, allFiles }: QuartzComponentProps) {
  const slug = fileData.slug!
  return <ul class="section-ul">
  return <ul class="section-ul popover-hint">
    {allFiles.sort(byDateAndAlphabetical).map(page => {
      const title = page.frontmatter?.title
      const pageSlug = page.slug!
@@ -36,9 +36,8 @@
          <div class="desc">
            <h3><a href={stripIndex(relativeToRoot(slug, pageSlug))} class="internal">{title}</a></h3>
          </div>
          <div class="spacer"></div>
          <ul class="tags">
            {tags.map(tag => <li><a href={relativeToRoot(slug, `tags/${tag}`)}>#{tag}</a></li>)}
            {tags.map(tag => <li><a class="internal" href={relativeToRoot(slug, `tags/${tag}`)}>#{tag}</a></li>)}
          </ul>
        </div>
      </li>
quartz/components/TagList.tsx
@@ -11,7 +11,7 @@
      const display = `#${tag}`
      const linkDest = baseDir + `/tags/${slugAnchor(tag)}`
      return <li>
        <a href={linkDest}>{display}</a>
        <a href={linkDest} class="internal">{display}</a>
      </li>
    })}</ul>
  } else {
@@ -25,18 +25,19 @@
  display: flex;
  padding-left: 0;
  gap: 0.4rem;
}
  & > li {
.tags > li {
    display: inline-block;
    margin: 0;
  overflow-wrap: normal;
}
    & > a {
.tags > li > a {
      border-radius: 8px;
      border: var(--lightgray) 1px solid;
  background-color: var(--highlight);
      padding: 0.2rem 0.5rem;
    }
  }
}
`
export default (() => TagList) satisfies QuartzComponentConstructor
quartz/components/pages/Content.tsx
@@ -5,7 +5,7 @@
function Content({ tree }: QuartzComponentProps) {
  // @ts-ignore (preact makes it angry)
  const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
  return <article>{content}</article>
  return <article class="popover-hint">{content}</article>
}
export default (() => Content) satisfies QuartzComponentConstructor
quartz/components/renderPage.tsx
@@ -17,10 +17,15 @@
export function pageResources(slug: string, staticResources: StaticResources): StaticResources {
  const baseDir = resolveToRoot(slug)
  const contentIndexPath = baseDir + "/static/contentIndex.json"
  const contentIndexScript = `const fetchData = fetch(\`${contentIndexPath}\`).then(data => data.json())`
  return {
    css: [baseDir + "/index.css", ...staticResources.css],
    js: [
      { src: baseDir + "/prescript.js", loadTime: "beforeDOMReady", contentType: "external" },
      { loadTime: "afterDOMReady", contentType: "inline", spaPreserve: true, script: contentIndexScript },
      ...staticResources.js,
      { src: baseDir + "/postscript.js", loadTime: "afterDOMReady", moduleType: 'module', contentType: "external" }
    ]
@@ -32,29 +37,41 @@
  const Header = HeaderConstructor()
  const Body = BodyConstructor()
  const LeftComponent =
    <div class="left">
      <div class="left-inner">
        {left.map(BodyComponent => <BodyComponent {...componentData} />)}
      </div>
    </div>
  const RightComponent =
    <div class="right">
      <div class="right-inner">
        {right.map(BodyComponent => <BodyComponent {...componentData} />)}
      </div>
    </div>
  const doc = <html>
    <Head {...componentData} />
    <body data-slug={slug}>
      <div id="quartz-root" class="page">
        <div class="page-header">
        <Header {...componentData} >
          {header.map(HeaderComponent => <HeaderComponent {...componentData} />)}
        </Header>
        <div class="popover-hint">
          {beforeBody.map(BodyComponent => <BodyComponent {...componentData} />)}
        </div>
        </div>
        <Body {...componentData}>
          <div class="left">
            {left.map(BodyComponent => <BodyComponent {...componentData} />)}
          </div>
          <div class="center popover-hint">
          {LeftComponent}
          <div class="center">
            <Content {...componentData} />
          </div>
          <div class="right">
            {right.map(BodyComponent => <BodyComponent {...componentData} />)}
          </div>
        </Body>
        <Footer {...componentData} />
      </div>
          {RightComponent}
        </Body>
      </div>
    </body>
    {pageResources.js.filter(resource => resource.loadTime === "afterDOMReady").map(res => JSResourceToScriptElement(res))}
  </html>
quartz/components/scripts/darkmode.inline.ts
@@ -2,7 +2,7 @@
const currentTheme = localStorage.getItem('theme') ?? userPref
document.documentElement.setAttribute('saved-theme', currentTheme)
window.addEventListener('DOMContentLoaded', () => {
document.addEventListener("nav", () => {
  const switchTheme = (e: any) => {
    if (e.target.checked) {
      document.documentElement.setAttribute('saved-theme', 'dark')
@@ -16,7 +16,8 @@
  // Darkmode toggle
  const toggleSwitch = document.querySelector('#darkmode-toggle') as HTMLInputElement
  toggleSwitch.addEventListener('change', switchTheme, false)
  toggleSwitch.removeEventListener('change', switchTheme)
  toggleSwitch.addEventListener('change', switchTheme)
  if (currentTheme === 'dark') {
    toggleSwitch.checked = true
  }
quartz/components/scripts/graph.inline.ts
@@ -266,9 +266,9 @@
  })
}
function renderGlobalGraph() {
async function renderGlobalGraph() {
  const slug = document.body.dataset["slug"]!
  renderGraph("global-graph-container", slug)
  await renderGraph("global-graph-container", slug)
  const container = document.getElementById("global-graph-outer")
  container?.classList.add("active")
@@ -293,7 +293,14 @@
  containerIcon?.addEventListener("click", renderGlobalGraph)
})
window.addEventListener('resize', async () => {
let resizeEventDebounce: number | undefined = undefined
window.addEventListener('resize', () => {
  if (resizeEventDebounce) {
    clearTimeout(resizeEventDebounce)
  }
  resizeEventDebounce = window.setTimeout(async () => {
  const slug = document.body.dataset["slug"]!
  await renderGraph("graph-container", slug)
  }, 50)
})
quartz/components/scripts/plausible.inline.ts
New file
@@ -0,0 +1,3 @@
import Plausible from 'plausible-tracker'
const { trackPageview } = Plausible()
document.addEventListener("nav", () => trackPageview())
quartz/components/scripts/popover.inline.ts
@@ -1,5 +1,24 @@
import { computePosition, flip, inline, shift } from "@floating-ui/dom"
// from micromorph/src/utils.ts
// https://github.com/natemoo-re/micromorph/blob/main/src/utils.ts#L5
export function normalizeRelativeURLs(
  el: Element | Document,
  base: string | URL
) {
  const update = (el: Element, attr: string, base: string | URL) => {
    el.setAttribute(attr, new URL(el.getAttribute(attr)!, base).pathname)
  }
  el.querySelectorAll('[href^="./"], [href^="../"]').forEach((item) =>
    update(item, 'href', base)
  )
  el.querySelectorAll('[src^="./"], [src^="../"]').forEach((item) =>
    update(item, 'src', base)
  )
}
document.addEventListener("nav", () => {
  const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[]
  const p = new DOMParser()
@@ -41,6 +60,7 @@
      if (!contents) return
      const html = p.parseFromString(contents, "text/html")
      normalizeRelativeURLs(html, targetUrl)
      const elts = [...html.getElementsByClassName("popover-hint")]
      if (elts.length === 0) return
@@ -55,11 +75,13 @@
      link.appendChild(popoverElement)
      link.dataset.fetchedPopover = "true"
      
      if (hash !== "") {
      const heading = popoverInner.querySelector(hash) as HTMLElement | null
      if (heading) {
        // leave ~12px of buffer when scrolling to a heading
        popoverInner.scroll({ top: heading.offsetTop - 12, behavior: 'instant' })
      }
      }
    })
  }
})
quartz/components/styles/backlinks.scss
@@ -7,13 +7,9 @@
  & > ul {
    list-style: none;
    padding: 0;
    margin: 0;
    margin: 0.5rem 0;
    & > li {
      margin: 0.5rem 0;
      padding: 0.25rem 1rem;
      border: var(--lightgray) 1px solid;
      border-radius: 5px;
      & > a {
        background-color: transparent;
      }
quartz/components/styles/footer.scss
@@ -1,6 +1,8 @@
footer {
  text-align: left;
  opacity: 0.8;
  margin-bottom: 4rem;
  & ul {
    list-style: none;
    margin: 0;
quartz/components/styles/graph.scss
@@ -11,6 +11,7 @@
    height: 250px;
    margin: 0.5em 0;
    position: relative;
    overflow: hidden;
    & > #global-graph-icon {
      color: var(--dark);
@@ -30,10 +31,6 @@
        background-color: var(--lightgray);
      }
    }
    & > #graph-container > svg {
      margin-bottom: -5px;
    }
  }
  & > #global-graph-outer {
quartz/components/styles/listPage.scss
@@ -8,29 +8,36 @@
  margin-bottom: 1em;
  & > .section {
    display: flex;
    align-items: center;
    display: grid;
    grid-template-columns: 6em 3fr 1fr;
    @media all and (max-width: 600px) {
      & .tags {
      & > .tags {
        display: none;
      }
    }
    & h3 > a {
      font-weight: 700;
      margin: 0;
    & > .tags {
      justify-self: end;
      margin-left: 1rem;
    }
    & > .desc a {
      background-color: transparent;
    }
    & p {
    & > .meta {
      margin: 0;
      padding-right: 1em;
      flex-basis: 6em;
      opacity: 0.6;
    }
    }
  }
  & .meta {
    opacity: 0.6;
// modifications in popover context
.popover .section {
  grid-template-columns: 6em 1fr !important;
  & > .tags {
    display: none;
  }
}
quartz/components/styles/popover.scss
@@ -24,7 +24,7 @@
    height: 20rem;
    padding: 0 1rem 1rem 1rem;
    font-weight: initial;
    line-height: initial;
    line-height: normal;
    font-size: initial;
    font-family: var(--bodyFont);
    border: 1px solid var(--gray);
quartz/components/styles/search.scss
@@ -1,8 +1,7 @@
.search {
  min-width: 5rem;
  max-width: 12rem;
  max-width: 14rem;
  flex-grow: 0.3;
  margin: 0 1.5rem;
  & > #search-icon {
    background-color: var(--lightgray);
quartz/components/types.ts
@@ -8,7 +8,7 @@
  externalResources: StaticResources
  fileData: QuartzPluginData
  cfg: GlobalConfiguration
  children: QuartzComponent[] | JSX.Element[]
  children: (QuartzComponent | JSX.Element)[]
  tree: Node<QuartzPluginData>
  allFiles: QuartzPluginData[]
}
quartz/path.ts
@@ -5,7 +5,17 @@
  return s.replace(/\s/g, '-')
}
// on the client, 'index' isn't ever rendered so we should clean it up
export function clientSideSlug(fp: string): string {
  if (fp.endsWith("index")) {
    fp = fp.slice(0, -"index".length)
  }
  return fp
}
export function trimPathSuffix(fp: string): string {
  fp = clientSideSlug(fp)
  let [cleanPath, anchor] = fp.split("#", 2)
  anchor = anchor === undefined ? "" : "#" + anchor
@@ -27,9 +37,6 @@
// resolve /a/b/c to ../../
export function resolveToRoot(slug: string): string {
  let fp = trimPathSuffix(slug)
  if (fp.endsWith("index")) {
    fp = fp.slice(0, -"index".length)
  }
  if (fp === "") {
    return "."
quartz/plugins/emitters/contentIndex.ts
@@ -36,7 +36,6 @@
  const base = cfg.canonicalUrl ?? ""
  const root = `https://${base}`
  // TODO: ogimage
  const createURLEntry = (slug: string, content: ContentDetails): string => `<items>
    <title>${content.title}</title>
    <link>${root}/${slug}</link>
quartz/plugins/index.ts
@@ -1,29 +1,17 @@
import { GlobalConfiguration } from '../cfg'
import { QuartzComponent } from '../components/types'
import { StaticResources } from '../resources'
import { googleFontHref, joinStyles } from '../theme'
import { joinStyles } from '../theme'
import { EmitCallback, PluginTypes } from './types'
import styles from '../styles/base.scss'
// @ts-ignore
import spaRouterScript from '../components/scripts/spa.inline'
// @ts-ignore
import popoverScript from '../components/scripts/popover.inline'
import popoverStyle from '../components/styles/popover.scss'
export type ComponentResources = {
  css: string[],
  beforeDOMLoaded: string[],
  afterDOMLoaded: string[]
}
function joinScripts(scripts: string[]): string {
  // wrap with iife to prevent scope collision
  return scripts.map(script => `(function () {${script}})();`).join("\n")
}
export function emitComponentResources(cfg: GlobalConfiguration, resources: StaticResources, plugins: PluginTypes, emit: EmitCallback) {
  const fps: string[] = []
export function getComponentResources(plugins: PluginTypes): ComponentResources {
  const allComponents: Set<QuartzComponent> = new Set()
  for (const emitter of plugins.emitters) {
    const components = emitter.getQuartzComponents()
@@ -51,40 +39,34 @@
    }
  }
  
  if (cfg.enablePopovers) {
    componentResources.afterDOMLoaded.push(popoverScript)
    componentResources.css.push(popoverStyle)
  return componentResources
  }
  if (cfg.enableSPA) {
    componentResources.afterDOMLoaded.push(spaRouterScript)
  } else {
    componentResources.afterDOMLoaded.push(`
      window.spaNavigate = (url, _) => window.location.assign(url)
      const event = new CustomEvent("nav", { detail: { slug: document.body.dataset.slug } })
      document.dispatchEvent(event)`
    )
function joinScripts(scripts: string[]): string {
  // wrap with iife to prevent scope collision
  return scripts.map(script => `(function () {${script}})();`).join("\n")
  }
export async function emitComponentResources(cfg: GlobalConfiguration, res: ComponentResources, emit: EmitCallback): Promise<string[]> {
  const fps = await Promise.all([
  emit({
    slug: "index",
    ext: ".css",
    content: joinStyles(cfg.theme, styles, ...componentResources.css)
  })
      content: joinStyles(cfg.theme, styles, ...res.css)
    }),
  emit({
    slug: "prescript",
    ext: ".js",
    content: joinScripts(componentResources.beforeDOMLoaded)
  })
      content: joinScripts(res.beforeDOMLoaded)
    }),
  emit({
    slug: "postscript",
    ext: ".js",
    content: joinScripts(componentResources.afterDOMLoaded)
      content: joinScripts(res.afterDOMLoaded)
  })
  fps.push("index.css", "prescript.js", "postscript.js")
  resources.css.push(googleFontHref(cfg.theme))
  ])
  return fps
}
export function getStaticResourcesFromPlugins(plugins: PluginTypes) {
quartz/plugins/transformers/gfm.ts
@@ -1,4 +1,3 @@
import { PluggableList } from "unified"
import remarkGfm from "remark-gfm"
import smartypants from 'remark-smartypants'
import { QuartzTransformerPlugin } from "../types"
@@ -20,14 +19,14 @@
  return {
    name: "GitHubFlavoredMarkdown",
    markdownPlugins() {
      return opts.enableSmartyPants ? [remarkGfm] : [remarkGfm, smartypants]
      return opts.enableSmartyPants ? [remarkGfm, smartypants] : [remarkGfm]
    },
    htmlPlugins() {
      if (opts.linkHeadings) {
        return [rehypeSlug, [rehypeAutolinkHeadings, {
          behavior: 'append', content: {
            type: 'text',
            value: ' Â§'
            value: ' Â§',
          }
        }]]
      } else {
quartz/plugins/transformers/links.ts
@@ -1,5 +1,5 @@
import { QuartzTransformerPlugin } from "../types"
import { relativeToRoot, slugify, trimPathSuffix } from "../../path"
import { clientSideSlug, relativeToRoot, slugify, trimPathSuffix } from "../../path"
import path from "path"
import { visit } from 'unist-util-visit'
import isAbsoluteUrl from "is-absolute-url"
@@ -27,7 +27,7 @@
    htmlPlugins() {
      return [() => {
        return (tree, file) => {
          const curSlug = file.data.slug!
          const curSlug = clientSideSlug(file.data.slug!)
          const transformLink = (target: string) => {
            const targetSlug = slugify(decodeURI(target).trim())
            if (opts.markdownLinkResolution === 'relative' && !path.isAbsolute(targetSlug)) {
@@ -49,7 +49,6 @@
              let dest = node.properties.href
              node.properties.className = isAbsoluteUrl(dest) ? "external" : "internal"
              // don't process external links or intra-document anchors
              if (!(isAbsoluteUrl(dest) || dest.startsWith("#"))) {
                node.properties.href = transformLink(dest)
quartz/processors/emit.ts
@@ -1,13 +1,69 @@
import path from "path"
import fs from "fs"
import { QuartzConfig } from "../cfg"
import { GlobalConfiguration, QuartzConfig } from "../cfg"
import { PerfTimer } from "../perf"
import { emitComponentResources, getStaticResourcesFromPlugins } from "../plugins"
import { ComponentResources, emitComponentResources, getComponentResources, getStaticResourcesFromPlugins } from "../plugins"
import { EmitCallback } from "../plugins/types"
import { ProcessedContent } from "../plugins/vfile"
import { QUARTZ, slugify } from "../path"
import { globbyStream } from "globby"
import chalk from "chalk"
import { googleFontHref } from '../theme'
// @ts-ignore
import spaRouterScript from '../components/scripts/spa.inline'
// @ts-ignore
import plausibleScript from '../components/scripts/plausible.inline'
// @ts-ignore
import popoverScript from '../components/scripts/popover.inline'
import popoverStyle from '../components/styles/popover.scss'
import { StaticResources } from "../resources"
function addGlobalPageResources(cfg: GlobalConfiguration, staticResources: StaticResources, componentResources: ComponentResources) {
  // font and other resources
  staticResources.css.push(googleFontHref(cfg.theme))
  // popovers
  if (cfg.enablePopovers) {
    componentResources.afterDOMLoaded.push(popoverScript)
    componentResources.css.push(popoverStyle)
  }
  if (cfg.analytics?.provider === "google") {
    const tagId = cfg.analytics.tagId
    staticResources.js.push({
      src: `https://www.googletagmanager.com/gtag/js?id=${tagId}`,
      contentType: 'external',
      loadTime: 'afterDOMReady',
    })
    componentResources.afterDOMLoaded.push(`
    window.dataLayer = window.dataLayer || [];
    function gtag() { dataLayer.push(arguments); }
    gtag(\`js\`, new Date());
    gtag(\`config\`, \`${tagId}\`, { send_page_view: false });
    document.addEventListener(\`nav\`, () => {
      gtag(\`event\`, \`page_view\`, {
        page_title: document.title,
        page_location: location.href,
      });
    });`
    )
  } else if (cfg.analytics?.provider === "plausible") {
    componentResources.afterDOMLoaded.push(plausibleScript)
  }
  // spa
  if (cfg.enableSPA) {
    componentResources.afterDOMLoaded.push(spaRouterScript)
  } else {
    componentResources.afterDOMLoaded.push(`
      window.spaNavigate = (url, _) => window.location.assign(url)
      const event = new CustomEvent("nav", { detail: { slug: document.body.dataset.slug } })
      document.dispatchEvent(event)`
    )
  }
}
export async function emitContent(contentFolder: string, output: string, cfg: QuartzConfig, content: ProcessedContent[], verbose: boolean) {
  const perf = new PerfTimer()
@@ -19,9 +75,25 @@
    return pathToPage
  }
  // initialize from plugins
  const staticResources = getStaticResourcesFromPlugins(cfg.plugins)
  emitComponentResources(cfg.configuration, staticResources, cfg.plugins, emit)
  // component specific scripts and styles
  const componentResources = getComponentResources(cfg.plugins)
  // important that this goes *after* component scripts
  // as the "nav" event gets triggered here and we should make sure
  // that everyone else had the chance to register a listener for it
  addGlobalPageResources(cfg.configuration, staticResources, componentResources)
  // emit in one go
  const emittedResources = await emitComponentResources(cfg.configuration, componentResources, emit)
  if (verbose) {
    for (const file of emittedResources) {
      console.log(`[emit:Resources] ${file}`)
    }
  }
  // emitter plugins
  let emittedFiles = 0
  for (const emitter of cfg.plugins.emitters) {
    try {
quartz/resources.tsx
@@ -3,7 +3,8 @@
export type JSResource = {
  loadTime: 'beforeDOMReady' | 'afterDOMReady'
  moduleType?: 'module'
  moduleType?: 'module',
  spaPreserve?: boolean
} & ({
  src: string
  contentType: 'external'
@@ -14,11 +15,12 @@
export function JSResourceToScriptElement(resource: JSResource, preserve?: boolean): JSX.Element {
  const scriptType = resource.moduleType ?? 'application/javascript'
  const spaPreserve = preserve ?? resource.spaPreserve
  if (resource.contentType === 'external') {
    return <script key={resource.src} src={resource.src} type={scriptType} spa-preserve={preserve} />
    return <script key={resource.src} src={resource.src} type={scriptType} spa-preserve={spaPreserve} />
  } else {
    const content = resource.script
    return <script key={randomUUID()} type={scriptType} spa-preserve={preserve}>{content}</script>
    return <script key={randomUUID()} type={scriptType} spa-preserve={spaPreserve}>{content}</script>
  }
}
quartz/styles/base.scss
@@ -11,6 +11,9 @@
  box-sizing: border-box;
  background-color: var(--light);
  font-family: var(--bodyFont);
  --pageWidth: 800px;
  --sidePanelWidth: 400px;
  --topSpacing: 6rem;
}
.text-highlight {
@@ -27,7 +30,7 @@
a {
  font-weight: 600;
  text-decoration: none;
  transition: all 0.2s ease;
  transition: color 0.2s ease;
  color: var(--secondary);
  &:hover {
@@ -43,34 +46,48 @@
}
.page {
  margin: 6rem 35vw 6rem 20vw;
  max-width: 1000px;
  position: relative;
  & > .page-header {
    max-width: var(--pageWidth);
    margin: var(--topSpacing) auto 0 auto;
  }
  & > #quartz-body {
    width: 100%;
    display: flex;
  & .left, & .right {
    position: fixed;
    height: 100vh;
    overflow-y: scroll;
    box-sizing: border-box;
      flex: 1;
      width: calc(calc(100vw - var(--pageWidth)) / 2);
    }
    & .left-inner, & .right-inner {
    display: flex;
    flex-direction: column;
    top: 0;
    gap: 2rem;
    padding: 6rem;
      top: 0;
      width: var(--sidePanelWidth);
      margin-top: calc(var(--topSpacing));
      box-sizing: border-box;
      padding: 0 4rem;
      position: fixed;
  }
  
  & .left {
    left: 0;
    padding-left: 10vw;
    width: 20vw;
    & .left-inner {
      left: calc(calc(100vw - var(--pageWidth)) / 2 - var(--sidePanelWidth));
  }
  & .right {
    right: 0;
    padding-right: 10vw;
    width: 35vw;
    & .right-inner {
      right: calc(calc(100vw - var(--pageWidth)) / 2 - var(--sidePanelWidth));
  }
    & .center {
      width: var(--pageWidth);
      margin: 0 auto;
    }
  }
}
.page {
  @media all and (max-width: 1200px) {
    margin: 25px 5vw;
    & .left, & .right {
@@ -89,7 +106,24 @@
    & > h1 {
      font-size: 2rem;
    }
    // darkmode diagrams
    & svg {
      stroke: var(--dark);
  }
    & ul:has(input[type='checkbox']) {
      list-style-type: none;
      padding-left: 0;
    }
  }
}
input[type="checkbox"] {
  transform: translateY(2px);
  color: var(--secondary);
  border-color: var(--lightgray);
  background-color: var(--light);
}
blockquote {
@@ -120,7 +154,7 @@
}
h1, h2, h3, h4, h5, h6 {
  &[id] > a {
  &[id] > a[href^="#"] {
    margin: 0 0.5rem;
    opacity: 0;
    transition: opacity 0.2s ease;