run prettier
This commit is contained in:
		@@ -1,19 +1,19 @@
 | 
			
		||||
#!/usr/bin/env node
 | 
			
		||||
import { promises, readFileSync } from 'fs'
 | 
			
		||||
import yargs from 'yargs'
 | 
			
		||||
import path from 'path'
 | 
			
		||||
import { hideBin } from 'yargs/helpers'
 | 
			
		||||
import esbuild from 'esbuild'
 | 
			
		||||
import chalk from 'chalk'
 | 
			
		||||
import { sassPlugin } from 'esbuild-sass-plugin'
 | 
			
		||||
import fs from 'fs'
 | 
			
		||||
import { intro, isCancel, outro, select, text } from '@clack/prompts'
 | 
			
		||||
import { rimraf } from 'rimraf'
 | 
			
		||||
import prettyBytes from 'pretty-bytes'
 | 
			
		||||
import { spawnSync } from 'child_process'
 | 
			
		||||
import { promises, readFileSync } from "fs"
 | 
			
		||||
import yargs from "yargs"
 | 
			
		||||
import path from "path"
 | 
			
		||||
import { hideBin } from "yargs/helpers"
 | 
			
		||||
import esbuild from "esbuild"
 | 
			
		||||
import chalk from "chalk"
 | 
			
		||||
import { sassPlugin } from "esbuild-sass-plugin"
 | 
			
		||||
import fs from "fs"
 | 
			
		||||
import { intro, isCancel, outro, select, text } from "@clack/prompts"
 | 
			
		||||
import { rimraf } from "rimraf"
 | 
			
		||||
import prettyBytes from "pretty-bytes"
 | 
			
		||||
import { spawnSync } from "child_process"
 | 
			
		||||
 | 
			
		||||
const UPSTREAM_NAME = 'upstream'
 | 
			
		||||
const QUARTZ_SOURCE_BRANCH = 'v4-alpha'
 | 
			
		||||
const UPSTREAM_NAME = "upstream"
 | 
			
		||||
const QUARTZ_SOURCE_BRANCH = "v4-alpha"
 | 
			
		||||
const cwd = process.cwd()
 | 
			
		||||
const cacheDir = path.join(cwd, ".quartz-cache")
 | 
			
		||||
const cacheFile = "./.quartz-cache/transpiled-build.mjs"
 | 
			
		||||
@@ -24,16 +24,16 @@ const contentCacheFolder = path.join(cacheDir, "content-cache")
 | 
			
		||||
const CommonArgv = {
 | 
			
		||||
  directory: {
 | 
			
		||||
    string: true,
 | 
			
		||||
    alias: ['d'],
 | 
			
		||||
    default: 'content',
 | 
			
		||||
    describe: 'directory to look for content files'
 | 
			
		||||
    alias: ["d"],
 | 
			
		||||
    default: "content",
 | 
			
		||||
    describe: "directory to look for content files",
 | 
			
		||||
  },
 | 
			
		||||
  verbose: {
 | 
			
		||||
    boolean: true,
 | 
			
		||||
    alias: ['v'],
 | 
			
		||||
    alias: ["v"],
 | 
			
		||||
    default: false,
 | 
			
		||||
    describe: 'print out extra logging information'
 | 
			
		||||
  }
 | 
			
		||||
    describe: "print out extra logging information",
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const SyncArgv = {
 | 
			
		||||
@@ -41,47 +41,46 @@ const SyncArgv = {
 | 
			
		||||
  commit: {
 | 
			
		||||
    boolean: true,
 | 
			
		||||
    default: true,
 | 
			
		||||
    describe: 'create a git commit for your unsaved changes'
 | 
			
		||||
    describe: "create a git commit for your unsaved changes",
 | 
			
		||||
  },
 | 
			
		||||
  push: {
 | 
			
		||||
    boolean: true,
 | 
			
		||||
    default: true,
 | 
			
		||||
    describe: 'push updates to your Quartz fork'
 | 
			
		||||
    describe: "push updates to your Quartz fork",
 | 
			
		||||
  },
 | 
			
		||||
  force: {
 | 
			
		||||
    boolean: true,
 | 
			
		||||
    alias: ['f'],
 | 
			
		||||
    alias: ["f"],
 | 
			
		||||
    default: true,
 | 
			
		||||
    describe: 'whether to apply the --force flag to git commands'
 | 
			
		||||
    describe: "whether to apply the --force flag to git commands",
 | 
			
		||||
  },
 | 
			
		||||
  pull: {
 | 
			
		||||
    boolean: true,
 | 
			
		||||
    default: true,
 | 
			
		||||
    describe: 'pull updates from your Quartz fork'
 | 
			
		||||
  }
 | 
			
		||||
    describe: "pull updates from your Quartz fork",
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const BuildArgv = {
 | 
			
		||||
  ...CommonArgv,
 | 
			
		||||
  output: {
 | 
			
		||||
    string: true,
 | 
			
		||||
    alias: ['o'],
 | 
			
		||||
    default: 'public',
 | 
			
		||||
    describe: 'output folder for files'
 | 
			
		||||
    alias: ["o"],
 | 
			
		||||
    default: "public",
 | 
			
		||||
    describe: "output folder for files",
 | 
			
		||||
  },
 | 
			
		||||
  serve: {
 | 
			
		||||
    boolean: true,
 | 
			
		||||
    default: false,
 | 
			
		||||
    describe: 'run a local server to live-preview your Quartz'
 | 
			
		||||
    describe: "run a local server to live-preview your Quartz",
 | 
			
		||||
  },
 | 
			
		||||
  port: {
 | 
			
		||||
    number: true,
 | 
			
		||||
    default: 8080,
 | 
			
		||||
    describe: 'port to serve Quartz on'
 | 
			
		||||
    describe: "port to serve Quartz on",
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function escapePath(fp) {
 | 
			
		||||
  return fp
 | 
			
		||||
    .replace(/\\ /g, " ") // unescape spaces
 | 
			
		||||
@@ -91,7 +90,6 @@ function escapePath(fp) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function exitIfCancel(val) {
 | 
			
		||||
 | 
			
		||||
  if (isCancel(val)) {
 | 
			
		||||
    outro(chalk.red("Exiting"))
 | 
			
		||||
    process.exit(0)
 | 
			
		||||
@@ -101,32 +99,48 @@ function exitIfCancel(val) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function stashContentFolder(contentFolder) {
 | 
			
		||||
  await fs.promises.cp(contentFolder, contentCacheFolder, { force: true, recursive: true, verbatimSymlinks: true, preserveTimestamps: true })
 | 
			
		||||
  await fs.promises.cp(contentFolder, contentCacheFolder, {
 | 
			
		||||
    force: true,
 | 
			
		||||
    recursive: true,
 | 
			
		||||
    verbatimSymlinks: true,
 | 
			
		||||
    preserveTimestamps: true,
 | 
			
		||||
  })
 | 
			
		||||
  await fs.promises.rm(contentFolder, { force: true, recursive: true })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function popContentFolder(contentFolder) {
 | 
			
		||||
  await fs.promises.cp(contentCacheFolder, contentFolder, { force: true, recursive: true, verbatimSymlinks: true, preserveTimestamps: true })
 | 
			
		||||
  await fs.promises.cp(contentCacheFolder, contentFolder, {
 | 
			
		||||
    force: true,
 | 
			
		||||
    recursive: true,
 | 
			
		||||
    verbatimSymlinks: true,
 | 
			
		||||
    preserveTimestamps: true,
 | 
			
		||||
  })
 | 
			
		||||
  await fs.promises.rm(contentCacheFolder, { force: true, recursive: true })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
yargs(hideBin(process.argv))
 | 
			
		||||
  .scriptName("quartz")
 | 
			
		||||
  .version(version)
 | 
			
		||||
  .usage('$0 <cmd> [args]')
 | 
			
		||||
  .command('create', 'Initialize Quartz', CommonArgv, async argv => {
 | 
			
		||||
  .usage("$0 <cmd> [args]")
 | 
			
		||||
  .command("create", "Initialize Quartz", CommonArgv, async (argv) => {
 | 
			
		||||
    console.log()
 | 
			
		||||
    intro(chalk.bgGreen.black(` Quartz v${version} `))
 | 
			
		||||
    const contentFolder = path.join(cwd, argv.directory)
 | 
			
		||||
    const setupStrategy = exitIfCancel(await select({
 | 
			
		||||
      message: `Choose how to initialize the content in \`${contentFolder}\``,
 | 
			
		||||
      options: [
 | 
			
		||||
        { value: 'new', label: "Empty Quartz" },
 | 
			
		||||
        { value: 'copy', label: "Replace with an existing folder", hint: "overwrites `content`" },
 | 
			
		||||
        { value: 'symlink', label: "Symlink an existing folder", hint: "don't select this unless you know what you are doing!" },
 | 
			
		||||
        { value: 'keep', label: "Keep the existing files" },
 | 
			
		||||
      ]
 | 
			
		||||
    }))
 | 
			
		||||
    const setupStrategy = exitIfCancel(
 | 
			
		||||
      await select({
 | 
			
		||||
        message: `Choose how to initialize the content in \`${contentFolder}\``,
 | 
			
		||||
        options: [
 | 
			
		||||
          { value: "new", label: "Empty Quartz" },
 | 
			
		||||
          { value: "copy", label: "Replace with an existing folder", hint: "overwrites `content`" },
 | 
			
		||||
          {
 | 
			
		||||
            value: "symlink",
 | 
			
		||||
            label: "Symlink an existing folder",
 | 
			
		||||
            hint: "don't select this unless you know what you are doing!",
 | 
			
		||||
          },
 | 
			
		||||
          { value: "keep", label: "Keep the existing files" },
 | 
			
		||||
        ],
 | 
			
		||||
      }),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    async function rmContentFolder() {
 | 
			
		||||
      const contentStat = await fs.promises.lstat(contentFolder)
 | 
			
		||||
@@ -139,54 +153,77 @@ yargs(hideBin(process.argv))
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (setupStrategy === 'copy' || setupStrategy === 'symlink') {
 | 
			
		||||
      const originalFolder = escapePath(exitIfCancel(await text({
 | 
			
		||||
        message: "Enter the full path to existing content folder",
 | 
			
		||||
        placeholder: 'On most terminal emulators, you can drag and drop a folder into the window and it will paste the full path',
 | 
			
		||||
        validate(fp) {
 | 
			
		||||
          const fullPath = escapePath(fp)
 | 
			
		||||
          if (!fs.existsSync(fullPath)) {
 | 
			
		||||
            return "The given path doesn't exist"
 | 
			
		||||
          } else if (!fs.lstatSync(fullPath).isDirectory()) {
 | 
			
		||||
            return "The given path is not a folder"
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      })))
 | 
			
		||||
    if (setupStrategy === "copy" || setupStrategy === "symlink") {
 | 
			
		||||
      const originalFolder = escapePath(
 | 
			
		||||
        exitIfCancel(
 | 
			
		||||
          await text({
 | 
			
		||||
            message: "Enter the full path to existing content folder",
 | 
			
		||||
            placeholder:
 | 
			
		||||
              "On most terminal emulators, you can drag and drop a folder into the window and it will paste the full path",
 | 
			
		||||
            validate(fp) {
 | 
			
		||||
              const fullPath = escapePath(fp)
 | 
			
		||||
              if (!fs.existsSync(fullPath)) {
 | 
			
		||||
                return "The given path doesn't exist"
 | 
			
		||||
              } else if (!fs.lstatSync(fullPath).isDirectory()) {
 | 
			
		||||
                return "The given path is not a folder"
 | 
			
		||||
              }
 | 
			
		||||
            },
 | 
			
		||||
          }),
 | 
			
		||||
        ),
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      await rmContentFolder()
 | 
			
		||||
      if (setupStrategy === 'copy') {
 | 
			
		||||
      if (setupStrategy === "copy") {
 | 
			
		||||
        await fs.promises.cp(originalFolder, contentFolder, { recursive: true })
 | 
			
		||||
      } else if (setupStrategy === 'symlink') {
 | 
			
		||||
        await fs.promises.symlink(originalFolder, contentFolder, 'dir')
 | 
			
		||||
      } else if (setupStrategy === "symlink") {
 | 
			
		||||
        await fs.promises.symlink(originalFolder, contentFolder, "dir")
 | 
			
		||||
      }
 | 
			
		||||
    } else if (setupStrategy === 'new') {
 | 
			
		||||
    } else if (setupStrategy === "new") {
 | 
			
		||||
      await rmContentFolder()
 | 
			
		||||
      await fs.promises.mkdir(contentFolder)
 | 
			
		||||
      await fs.promises.writeFile(path.join(contentFolder, "index.md"),
 | 
			
		||||
      await fs.promises.writeFile(
 | 
			
		||||
        path.join(contentFolder, "index.md"),
 | 
			
		||||
        `---
 | 
			
		||||
title: Welcome to Quartz
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
This is a blank Quartz installation.
 | 
			
		||||
See the [documentation](https://quartz.jzhao.xyz) for how to get started.
 | 
			
		||||
`
 | 
			
		||||
`,
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    // get a prefered link resolution strategy
 | 
			
		||||
    const linkResolutionStrategy = exitIfCancel(await select({
 | 
			
		||||
      message: `Choose how Quartz should resolve links in your content. You can change this later in \`quartz.config.ts\`.`,
 | 
			
		||||
      options: [
 | 
			
		||||
        { value: 'absolute', label: "Treat links as absolute path", hint: "for content made for Quartz 3 and Hugo" },
 | 
			
		||||
        { value: 'shortest', label: "Treat links as shortest path", hint: "for most Obsidian vaults" },
 | 
			
		||||
        { value: 'relative', label: "Treat links as relative paths", hint: "for just normal Markdown files" },
 | 
			
		||||
      ]
 | 
			
		||||
    }))
 | 
			
		||||
    const linkResolutionStrategy = exitIfCancel(
 | 
			
		||||
      await select({
 | 
			
		||||
        message: `Choose how Quartz should resolve links in your content. You can change this later in \`quartz.config.ts\`.`,
 | 
			
		||||
        options: [
 | 
			
		||||
          {
 | 
			
		||||
            value: "absolute",
 | 
			
		||||
            label: "Treat links as absolute path",
 | 
			
		||||
            hint: "for content made for Quartz 3 and Hugo",
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            value: "shortest",
 | 
			
		||||
            label: "Treat links as shortest path",
 | 
			
		||||
            hint: "for most Obsidian vaults",
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            value: "relative",
 | 
			
		||||
            label: "Treat links as relative paths",
 | 
			
		||||
            hint: "for just normal Markdown files",
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      }),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    // now, do config changes
 | 
			
		||||
    const configFilePath = path.join(cwd, "quartz.config.ts")
 | 
			
		||||
    let configContent = await fs.promises.readFile(configFilePath, { encoding: 'utf-8' })
 | 
			
		||||
    configContent = configContent.replace(/markdownLinkResolution: '(.+)'/, `markdownLinkResolution: '${linkResolutionStrategy}'`)
 | 
			
		||||
    let configContent = await fs.promises.readFile(configFilePath, { encoding: "utf-8" })
 | 
			
		||||
    configContent = configContent.replace(
 | 
			
		||||
      /markdownLinkResolution: '(.+)'/,
 | 
			
		||||
      `markdownLinkResolution: '${linkResolutionStrategy}'`,
 | 
			
		||||
    )
 | 
			
		||||
    await fs.promises.writeFile(configFilePath, configContent)
 | 
			
		||||
 | 
			
		||||
    outro(`You're all set! Not sure what to do next? Try:
 | 
			
		||||
@@ -195,105 +232,120 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
 | 
			
		||||
   • Hosting your Quartz online (see: https://quartz.jzhao.xyz/setup/hosting)
 | 
			
		||||
`)
 | 
			
		||||
  })
 | 
			
		||||
  .command('update', 'Get the latest Quartz updates', CommonArgv, async argv => {
 | 
			
		||||
  .command("update", "Get the latest Quartz updates", CommonArgv, async (argv) => {
 | 
			
		||||
    const contentFolder = path.join(cwd, argv.directory)
 | 
			
		||||
    console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`))
 | 
			
		||||
    console.log('Backing up your content')
 | 
			
		||||
    console.log("Backing up your content")
 | 
			
		||||
    await stashContentFolder(contentFolder)
 | 
			
		||||
    console.log("Pulling updates... you may need to resolve some `git` conflicts if you've made changes to components or plugins.")
 | 
			
		||||
    spawnSync('git', ['pull', UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH], { stdio: 'inherit' })
 | 
			
		||||
    console.log(
 | 
			
		||||
      "Pulling updates... you may need to resolve some `git` conflicts if you've made changes to components or plugins.",
 | 
			
		||||
    )
 | 
			
		||||
    spawnSync("git", ["pull", UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH], { stdio: "inherit" })
 | 
			
		||||
    await popContentFolder(contentFolder)
 | 
			
		||||
    console.log(chalk.green('Done!'))
 | 
			
		||||
    console.log(chalk.green("Done!"))
 | 
			
		||||
  })
 | 
			
		||||
  .command('sync', 'Sync your Quartz to and from GitHub.', SyncArgv, async argv => {
 | 
			
		||||
  .command("sync", "Sync your Quartz to and from GitHub.", SyncArgv, async (argv) => {
 | 
			
		||||
    const contentFolder = path.join(cwd, argv.directory)
 | 
			
		||||
    console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`))
 | 
			
		||||
    console.log('Backing up your content')
 | 
			
		||||
    console.log("Backing up your content")
 | 
			
		||||
 | 
			
		||||
    if (argv.commit) {
 | 
			
		||||
      const currentTimestamp = new Date().toLocaleString('en-US', { dateStyle: "medium", timeStyle: "short" })
 | 
			
		||||
      spawnSync('git', ['commit', '-am', `Quartz sync: ${currentTimestamp}`], { stdio: 'inherit' })
 | 
			
		||||
      const currentTimestamp = new Date().toLocaleString("en-US", {
 | 
			
		||||
        dateStyle: "medium",
 | 
			
		||||
        timeStyle: "short",
 | 
			
		||||
      })
 | 
			
		||||
      spawnSync("git", ["commit", "-am", `Quartz sync: ${currentTimestamp}`], { stdio: "inherit" })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await stashContentFolder(contentFolder)
 | 
			
		||||
 | 
			
		||||
    if (argv.pull) {
 | 
			
		||||
      console.log("Pulling updates from your repository. You may need to resolve some `git` conflicts if you've made changes to components or plugins.")
 | 
			
		||||
      spawnSync('git', ['pull', 'origin', QUARTZ_SOURCE_BRANCH], { stdio: 'inherit' })
 | 
			
		||||
      console.log(
 | 
			
		||||
        "Pulling updates from your repository. You may need to resolve some `git` conflicts if you've made changes to components or plugins.",
 | 
			
		||||
      )
 | 
			
		||||
      spawnSync("git", ["pull", "origin", QUARTZ_SOURCE_BRANCH], { stdio: "inherit" })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await popContentFolder(contentFolder)
 | 
			
		||||
    if (argv.push) {
 | 
			
		||||
      console.log("Pushing your changes")
 | 
			
		||||
      const args = argv.force ?
 | 
			
		||||
        ['push', '-f', 'origin', QUARTZ_SOURCE_BRANCH] :
 | 
			
		||||
        ['push', 'origin', QUARTZ_SOURCE_BRANCH]
 | 
			
		||||
      spawnSync('git', args, { stdio: 'inherit' })
 | 
			
		||||
      const args = argv.force
 | 
			
		||||
        ? ["push", "-f", "origin", QUARTZ_SOURCE_BRANCH]
 | 
			
		||||
        : ["push", "origin", QUARTZ_SOURCE_BRANCH]
 | 
			
		||||
      spawnSync("git", args, { stdio: "inherit" })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log(chalk.green('Done!'))
 | 
			
		||||
    console.log(chalk.green("Done!"))
 | 
			
		||||
  })
 | 
			
		||||
  .command('build', 'Build Quartz into a bundle of static HTML files', BuildArgv, async argv => {
 | 
			
		||||
    const result = await esbuild.build({
 | 
			
		||||
      entryPoints: [fp],
 | 
			
		||||
      outfile: path.join("quartz", cacheFile),
 | 
			
		||||
      bundle: true,
 | 
			
		||||
      keepNames: true,
 | 
			
		||||
      platform: "node",
 | 
			
		||||
      format: "esm",
 | 
			
		||||
      jsx: "automatic",
 | 
			
		||||
      jsxImportSource: "preact",
 | 
			
		||||
      packages: "external",
 | 
			
		||||
      metafile: true,
 | 
			
		||||
      sourcemap: true,
 | 
			
		||||
      plugins: [
 | 
			
		||||
        sassPlugin({
 | 
			
		||||
          type: 'css-text',
 | 
			
		||||
        }),
 | 
			
		||||
        {
 | 
			
		||||
          name: 'inline-script-loader',
 | 
			
		||||
          setup(build) {
 | 
			
		||||
            build.onLoad({ filter: /\.inline\.(ts|js)$/ }, async (args) => {
 | 
			
		||||
              let text = await promises.readFile(args.path, 'utf8')
 | 
			
		||||
  .command("build", "Build Quartz into a bundle of static HTML files", BuildArgv, async (argv) => {
 | 
			
		||||
    const result = await esbuild
 | 
			
		||||
      .build({
 | 
			
		||||
        entryPoints: [fp],
 | 
			
		||||
        outfile: path.join("quartz", cacheFile),
 | 
			
		||||
        bundle: true,
 | 
			
		||||
        keepNames: true,
 | 
			
		||||
        platform: "node",
 | 
			
		||||
        format: "esm",
 | 
			
		||||
        jsx: "automatic",
 | 
			
		||||
        jsxImportSource: "preact",
 | 
			
		||||
        packages: "external",
 | 
			
		||||
        metafile: true,
 | 
			
		||||
        sourcemap: true,
 | 
			
		||||
        plugins: [
 | 
			
		||||
          sassPlugin({
 | 
			
		||||
            type: "css-text",
 | 
			
		||||
          }),
 | 
			
		||||
          {
 | 
			
		||||
            name: "inline-script-loader",
 | 
			
		||||
            setup(build) {
 | 
			
		||||
              build.onLoad({ filter: /\.inline\.(ts|js)$/ }, async (args) => {
 | 
			
		||||
                let text = await promises.readFile(args.path, "utf8")
 | 
			
		||||
 | 
			
		||||
              // remove default exports that we manually inserted
 | 
			
		||||
              text = text.replace('export default', '')
 | 
			
		||||
              text = text.replace('export', '')
 | 
			
		||||
                // remove default exports that we manually inserted
 | 
			
		||||
                text = text.replace("export default", "")
 | 
			
		||||
                text = text.replace("export", "")
 | 
			
		||||
 | 
			
		||||
              const sourcefile = path.relative(path.resolve('.'), args.path)
 | 
			
		||||
              const resolveDir = path.dirname(sourcefile)
 | 
			
		||||
              const transpiled = await esbuild.build({
 | 
			
		||||
                stdin: {
 | 
			
		||||
                  contents: text,
 | 
			
		||||
                  loader: 'ts',
 | 
			
		||||
                  resolveDir,
 | 
			
		||||
                  sourcefile,
 | 
			
		||||
                },
 | 
			
		||||
                write: false,
 | 
			
		||||
                bundle: true,
 | 
			
		||||
                platform: "browser",
 | 
			
		||||
                format: "esm",
 | 
			
		||||
                const sourcefile = path.relative(path.resolve("."), args.path)
 | 
			
		||||
                const resolveDir = path.dirname(sourcefile)
 | 
			
		||||
                const transpiled = await esbuild.build({
 | 
			
		||||
                  stdin: {
 | 
			
		||||
                    contents: text,
 | 
			
		||||
                    loader: "ts",
 | 
			
		||||
                    resolveDir,
 | 
			
		||||
                    sourcefile,
 | 
			
		||||
                  },
 | 
			
		||||
                  write: false,
 | 
			
		||||
                  bundle: true,
 | 
			
		||||
                  platform: "browser",
 | 
			
		||||
                  format: "esm",
 | 
			
		||||
                })
 | 
			
		||||
                const rawMod = transpiled.outputFiles[0].text
 | 
			
		||||
                return {
 | 
			
		||||
                  contents: rawMod,
 | 
			
		||||
                  loader: "text",
 | 
			
		||||
                }
 | 
			
		||||
              })
 | 
			
		||||
              const rawMod = transpiled.outputFiles[0].text
 | 
			
		||||
              return {
 | 
			
		||||
                contents: rawMod,
 | 
			
		||||
                loader: 'text',
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    }).catch(err => {
 | 
			
		||||
      console.error(`${chalk.red("Couldn't parse Quartz configuration:")} ${fp}`)
 | 
			
		||||
      console.log(`Reason: ${chalk.grey(err)}`)
 | 
			
		||||
      console.log("hint: make sure all the required dependencies are installed (run `npm install`)")
 | 
			
		||||
      process.exit(1)
 | 
			
		||||
    })
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      })
 | 
			
		||||
      .catch((err) => {
 | 
			
		||||
        console.error(`${chalk.red("Couldn't parse Quartz configuration:")} ${fp}`)
 | 
			
		||||
        console.log(`Reason: ${chalk.grey(err)}`)
 | 
			
		||||
        console.log(
 | 
			
		||||
          "hint: make sure all the required dependencies are installed (run `npm install`)",
 | 
			
		||||
        )
 | 
			
		||||
        process.exit(1)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    if (argv.verbose) {
 | 
			
		||||
      const outputFileName = 'quartz/.quartz-cache/transpiled-build.mjs'
 | 
			
		||||
      const outputFileName = "quartz/.quartz-cache/transpiled-build.mjs"
 | 
			
		||||
      const meta = result.metafile.outputs[outputFileName]
 | 
			
		||||
      console.log(`Successfully transpiled ${Object.keys(meta.inputs).length} files (${prettyBytes(meta.bytes)})`)
 | 
			
		||||
      console.log(
 | 
			
		||||
        `Successfully transpiled ${Object.keys(meta.inputs).length} files (${prettyBytes(
 | 
			
		||||
          meta.bytes,
 | 
			
		||||
        )})`,
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const { default: buildQuartz } = await import(cacheFile)
 | 
			
		||||
@@ -302,5 +354,4 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
 | 
			
		||||
  .showHelpOnFail(false)
 | 
			
		||||
  .help()
 | 
			
		||||
  .strict()
 | 
			
		||||
  .demandCommand()
 | 
			
		||||
  .argv
 | 
			
		||||
  .demandCommand().argv
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
#!/usr/bin/env node
 | 
			
		||||
import workerpool from 'workerpool'
 | 
			
		||||
import workerpool from "workerpool"
 | 
			
		||||
const cacheFile = "./.quartz-cache/transpiled-worker.mjs"
 | 
			
		||||
const { parseFiles } = await import(cacheFile)
 | 
			
		||||
workerpool.worker({
 | 
			
		||||
  parseFiles
 | 
			
		||||
  parseFiles,
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import 'source-map-support/register.js'
 | 
			
		||||
import "source-map-support/register.js"
 | 
			
		||||
import path from "path"
 | 
			
		||||
import { PerfTimer } from "./perf"
 | 
			
		||||
import { rimraf } from "rimraf"
 | 
			
		||||
@@ -12,8 +12,8 @@ import { emitContent } from "./processors/emit"
 | 
			
		||||
import cfg from "../quartz.config"
 | 
			
		||||
import { FilePath } from "./path"
 | 
			
		||||
import chokidar from "chokidar"
 | 
			
		||||
import { ProcessedContent } from './plugins/vfile'
 | 
			
		||||
import WebSocket, { WebSocketServer } from 'ws'
 | 
			
		||||
import { ProcessedContent } from "./plugins/vfile"
 | 
			
		||||
import WebSocket, { WebSocketServer } from "ws"
 | 
			
		||||
 | 
			
		||||
interface Argv {
 | 
			
		||||
  directory: string
 | 
			
		||||
@@ -29,30 +29,38 @@ export default async function buildQuartz(argv: Argv, version: string) {
 | 
			
		||||
  const output = argv.output
 | 
			
		||||
 | 
			
		||||
  const pluginCount = Object.values(cfg.plugins).flat().length
 | 
			
		||||
  const pluginNames = (key: 'transformers' | 'filters' | 'emitters') => cfg.plugins[key].map(plugin => plugin.name)
 | 
			
		||||
  const pluginNames = (key: "transformers" | "filters" | "emitters") =>
 | 
			
		||||
    cfg.plugins[key].map((plugin) => plugin.name)
 | 
			
		||||
  if (argv.verbose) {
 | 
			
		||||
    console.log(`Loaded ${pluginCount} plugins`)
 | 
			
		||||
    console.log(`  Transformers: ${pluginNames('transformers').join(", ")}`)
 | 
			
		||||
    console.log(`  Filters: ${pluginNames('filters').join(", ")}`)
 | 
			
		||||
    console.log(`  Emitters: ${pluginNames('emitters').join(", ")}`)
 | 
			
		||||
    console.log(`  Transformers: ${pluginNames("transformers").join(", ")}`)
 | 
			
		||||
    console.log(`  Filters: ${pluginNames("filters").join(", ")}`)
 | 
			
		||||
    console.log(`  Emitters: ${pluginNames("emitters").join(", ")}`)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // clean
 | 
			
		||||
  perf.addEvent('clean')
 | 
			
		||||
  perf.addEvent("clean")
 | 
			
		||||
  await rimraf(output)
 | 
			
		||||
  console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince('clean')}`)
 | 
			
		||||
  console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`)
 | 
			
		||||
 | 
			
		||||
  // glob
 | 
			
		||||
  perf.addEvent('glob')
 | 
			
		||||
  const fps = await globby('**/*.md', {
 | 
			
		||||
  perf.addEvent("glob")
 | 
			
		||||
  const fps = await globby("**/*.md", {
 | 
			
		||||
    cwd: argv.directory,
 | 
			
		||||
    ignore: cfg.configuration.ignorePatterns,
 | 
			
		||||
    gitignore: true,
 | 
			
		||||
  })
 | 
			
		||||
  console.log(`Found ${fps.length} input files from \`${argv.directory}\` in ${perf.timeSince('glob')}`)
 | 
			
		||||
  console.log(
 | 
			
		||||
    `Found ${fps.length} input files from \`${argv.directory}\` in ${perf.timeSince("glob")}`,
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const filePaths = fps.map(fp => `${argv.directory}${path.sep}${fp}` as FilePath)
 | 
			
		||||
  const parsedFiles = await parseMarkdown(cfg.plugins.transformers, argv.directory, filePaths, argv.verbose)
 | 
			
		||||
  const filePaths = fps.map((fp) => `${argv.directory}${path.sep}${fp}` as FilePath)
 | 
			
		||||
  const parsedFiles = await parseMarkdown(
 | 
			
		||||
    cfg.plugins.transformers,
 | 
			
		||||
    argv.directory,
 | 
			
		||||
    filePaths,
 | 
			
		||||
    argv.verbose,
 | 
			
		||||
  )
 | 
			
		||||
  const filteredContent = filterContent(cfg.plugins.filters, parsedFiles, argv.verbose)
 | 
			
		||||
  await emitContent(argv.directory, output, cfg, filteredContent, argv.serve, argv.verbose)
 | 
			
		||||
  console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`))
 | 
			
		||||
@@ -60,7 +68,7 @@ export default async function buildQuartz(argv: Argv, version: string) {
 | 
			
		||||
  if (argv.serve) {
 | 
			
		||||
    const wss = new WebSocketServer({ port: 3001 })
 | 
			
		||||
    const connections: WebSocket[] = []
 | 
			
		||||
    wss.on('connection', ws => connections.push(ws))
 | 
			
		||||
    wss.on("connection", (ws) => connections.push(ws))
 | 
			
		||||
 | 
			
		||||
    const ignored = await isGitIgnored()
 | 
			
		||||
    const contentMap = new Map<FilePath, ProcessedContent>()
 | 
			
		||||
@@ -69,15 +77,20 @@ export default async function buildQuartz(argv: Argv, version: string) {
 | 
			
		||||
      contentMap.set(vfile.data.filePath!, content)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function rebuild(fp: string, action: 'add' | 'change' | 'unlink') {
 | 
			
		||||
      perf.addEvent('rebuild')
 | 
			
		||||
    async function rebuild(fp: string, action: "add" | "change" | "unlink") {
 | 
			
		||||
      perf.addEvent("rebuild")
 | 
			
		||||
      if (!ignored(fp)) {
 | 
			
		||||
        console.log(chalk.yellow(`Detected change in ${fp}, rebuilding...`))
 | 
			
		||||
        const fullPath = `${argv.directory}${path.sep}${fp}` as FilePath
 | 
			
		||||
        if (action === 'add' || action === 'change') {
 | 
			
		||||
          const [parsedContent] = await parseMarkdown(cfg.plugins.transformers, argv.directory, [fullPath], argv.verbose)
 | 
			
		||||
        if (action === "add" || action === "change") {
 | 
			
		||||
          const [parsedContent] = await parseMarkdown(
 | 
			
		||||
            cfg.plugins.transformers,
 | 
			
		||||
            argv.directory,
 | 
			
		||||
            [fullPath],
 | 
			
		||||
            argv.verbose,
 | 
			
		||||
          )
 | 
			
		||||
          contentMap.set(fullPath, parsedContent)
 | 
			
		||||
        } else if (action === 'unlink') {
 | 
			
		||||
        } else if (action === "unlink") {
 | 
			
		||||
          contentMap.delete(fullPath)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -85,21 +98,21 @@ export default async function buildQuartz(argv: Argv, version: string) {
 | 
			
		||||
        const parsedFiles = [...contentMap.values()]
 | 
			
		||||
        const filteredContent = filterContent(cfg.plugins.filters, parsedFiles, argv.verbose)
 | 
			
		||||
        await emitContent(argv.directory, output, cfg, filteredContent, argv.serve, argv.verbose)
 | 
			
		||||
        console.log(chalk.green(`Done rebuilding in ${perf.timeSince('rebuild')}`))
 | 
			
		||||
        connections.forEach(conn => conn.send('rebuild'))
 | 
			
		||||
        console.log(chalk.green(`Done rebuilding in ${perf.timeSince("rebuild")}`))
 | 
			
		||||
        connections.forEach((conn) => conn.send("rebuild"))
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const watcher = chokidar.watch('.', {
 | 
			
		||||
    const watcher = chokidar.watch(".", {
 | 
			
		||||
      persistent: true,
 | 
			
		||||
      cwd: argv.directory,
 | 
			
		||||
      ignoreInitial: true,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    watcher
 | 
			
		||||
      .on('add', fp => rebuild(fp, 'add'))
 | 
			
		||||
      .on('change', fp => rebuild(fp, 'change'))
 | 
			
		||||
      .on('unlink', fp => rebuild(fp, 'unlink'))
 | 
			
		||||
      .on("add", (fp) => rebuild(fp, "add"))
 | 
			
		||||
      .on("change", (fp) => rebuild(fp, "change"))
 | 
			
		||||
      .on("unlink", (fp) => rebuild(fp, "unlink"))
 | 
			
		||||
 | 
			
		||||
    const server = http.createServer(async (req, res) => {
 | 
			
		||||
      await serveHandler(req, res, {
 | 
			
		||||
@@ -107,15 +120,16 @@ export default async function buildQuartz(argv: Argv, version: string) {
 | 
			
		||||
        directoryListing: false,
 | 
			
		||||
      })
 | 
			
		||||
      const status = res.statusCode
 | 
			
		||||
      const statusString = (status >= 200 && status < 300) ?
 | 
			
		||||
        chalk.green(`[${status}]`) :
 | 
			
		||||
        (status >= 300 && status < 400) ?
 | 
			
		||||
          chalk.yellow(`[${status}]`) :
 | 
			
		||||
          chalk.red(`[${status}]`)
 | 
			
		||||
      const statusString =
 | 
			
		||||
        status >= 200 && status < 300
 | 
			
		||||
          ? chalk.green(`[${status}]`)
 | 
			
		||||
          : status >= 300 && status < 400
 | 
			
		||||
          ? chalk.yellow(`[${status}]`)
 | 
			
		||||
          : chalk.red(`[${status}]`)
 | 
			
		||||
      console.log(statusString + chalk.grey(` ${req.url}`))
 | 
			
		||||
    })
 | 
			
		||||
    server.listen(argv.port)
 | 
			
		||||
    console.log(chalk.cyan(`Started a Quartz server listening at http://localhost:${argv.port}`))
 | 
			
		||||
    console.log('hint: exit with ctrl+c')
 | 
			
		||||
    console.log("hint: exit with ctrl+c")
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,43 +5,43 @@ import { Theme } from "./theme"
 | 
			
		||||
export type Analytics =
 | 
			
		||||
  | null
 | 
			
		||||
  | {
 | 
			
		||||
    provider: 'plausible'
 | 
			
		||||
  }
 | 
			
		||||
      provider: "plausible"
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
    provider: 'google',
 | 
			
		||||
    tagId: string
 | 
			
		||||
  }
 | 
			
		||||
      provider: "google"
 | 
			
		||||
      tagId: string
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
export interface GlobalConfiguration {
 | 
			
		||||
  pageTitle: string,
 | 
			
		||||
  pageTitle: string
 | 
			
		||||
  /** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */
 | 
			
		||||
  enableSPA: boolean,
 | 
			
		||||
  enableSPA: boolean
 | 
			
		||||
  /** Whether to display Wikipedia-style popovers when hovering over links */
 | 
			
		||||
  enablePopovers: boolean,
 | 
			
		||||
  enablePopovers: boolean
 | 
			
		||||
  /** Analytics mode */
 | 
			
		||||
  analytics: Analytics
 | 
			
		||||
  /** Glob patterns to not search */
 | 
			
		||||
  ignorePatterns: string[],
 | 
			
		||||
  ignorePatterns: string[]
 | 
			
		||||
  /** Base URL to use for CNAME files, sitemaps, and RSS feeds that require an absolute URL.
 | 
			
		||||
  *   Quartz will avoid using this as much as possible and use relative URLs most of the time  
 | 
			
		||||
  */
 | 
			
		||||
  baseUrl?: string,
 | 
			
		||||
   *   Quartz will avoid using this as much as possible and use relative URLs most of the time
 | 
			
		||||
   */
 | 
			
		||||
  baseUrl?: string
 | 
			
		||||
  theme: Theme
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface QuartzConfig {
 | 
			
		||||
  configuration: GlobalConfiguration,
 | 
			
		||||
  plugins: PluginTypes,
 | 
			
		||||
  configuration: GlobalConfiguration
 | 
			
		||||
  plugins: PluginTypes
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface FullPageLayout {
 | 
			
		||||
  head: QuartzComponent
 | 
			
		||||
  header: QuartzComponent[],
 | 
			
		||||
  beforeBody: QuartzComponent[],
 | 
			
		||||
  pageBody: QuartzComponent,
 | 
			
		||||
  left: QuartzComponent[],
 | 
			
		||||
  right: QuartzComponent[],
 | 
			
		||||
  footer: QuartzComponent,
 | 
			
		||||
  header: QuartzComponent[]
 | 
			
		||||
  beforeBody: QuartzComponent[]
 | 
			
		||||
  pageBody: QuartzComponent
 | 
			
		||||
  left: QuartzComponent[]
 | 
			
		||||
  right: QuartzComponent[]
 | 
			
		||||
  footer: QuartzComponent
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type PageLayout = Pick<FullPageLayout, "beforeBody" | "left" | "right">
 | 
			
		||||
 
 | 
			
		||||
@@ -4,15 +4,25 @@ import { canonicalizeServer, resolveRelative } from "../path"
 | 
			
		||||
 | 
			
		||||
function Backlinks({ fileData, allFiles }: QuartzComponentProps) {
 | 
			
		||||
  const slug = canonicalizeServer(fileData.slug!)
 | 
			
		||||
  const backlinkFiles = allFiles.filter(file => file.links?.includes(slug))
 | 
			
		||||
  return <div class="backlinks">
 | 
			
		||||
    <h3>Backlinks</h3>
 | 
			
		||||
    <ul class="overflow">
 | 
			
		||||
      {backlinkFiles.length > 0 ?
 | 
			
		||||
        backlinkFiles.map(f => <li><a href={resolveRelative(slug, canonicalizeServer(f.slug!))} class="internal">{f.frontmatter?.title}</a></li>)
 | 
			
		||||
        : <li>No backlinks found</li>}
 | 
			
		||||
    </ul>
 | 
			
		||||
  </div>
 | 
			
		||||
  const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
 | 
			
		||||
  return (
 | 
			
		||||
    <div class="backlinks">
 | 
			
		||||
      <h3>Backlinks</h3>
 | 
			
		||||
      <ul class="overflow">
 | 
			
		||||
        {backlinkFiles.length > 0 ? (
 | 
			
		||||
          backlinkFiles.map((f) => (
 | 
			
		||||
            <li>
 | 
			
		||||
              <a href={resolveRelative(slug, canonicalizeServer(f.slug!))} class="internal">
 | 
			
		||||
                {f.frontmatter?.title}
 | 
			
		||||
              </a>
 | 
			
		||||
            </li>
 | 
			
		||||
          ))
 | 
			
		||||
        ) : (
 | 
			
		||||
          <li>No backlinks found</li>
 | 
			
		||||
        )}
 | 
			
		||||
      </ul>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Backlinks.css = style
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,13 @@
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import clipboardScript from './scripts/clipboard.inline'
 | 
			
		||||
import clipboardStyle from './styles/clipboard.scss'
 | 
			
		||||
import clipboardScript from "./scripts/clipboard.inline"
 | 
			
		||||
import clipboardStyle from "./styles/clipboard.scss"
 | 
			
		||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 | 
			
		||||
 | 
			
		||||
function Body({ children }: QuartzComponentProps) {
 | 
			
		||||
  return <div id="quartz-body">
 | 
			
		||||
    {children}
 | 
			
		||||
  </div>
 | 
			
		||||
  return <div id="quartz-body">{children}</div>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Body.afterDOMLoaded = clipboardScript
 | 
			
		||||
Body.css = clipboardStyle
 | 
			
		||||
 | 
			
		||||
export default (() => Body) satisfies QuartzComponentConstructor
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,50 +1,48 @@
 | 
			
		||||
// @ts-ignore: this is safe, we don't want to actually make darkmode.inline.ts a module as 
 | 
			
		||||
// @ts-ignore: this is safe, we don't want to actually make darkmode.inline.ts a module as
 | 
			
		||||
// modules are automatically deferred and we don't want that to happen for critical beforeDOMLoads
 | 
			
		||||
// see: https://v8.dev/features/modules#defer
 | 
			
		||||
import darkmodeScript from "./scripts/darkmode.inline"
 | 
			
		||||
import styles from './styles/darkmode.scss'
 | 
			
		||||
import styles from "./styles/darkmode.scss"
 | 
			
		||||
import { QuartzComponentConstructor } from "./types"
 | 
			
		||||
 | 
			
		||||
function Darkmode() {
 | 
			
		||||
  return <div class="darkmode">
 | 
			
		||||
    <input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} />
 | 
			
		||||
    <label id="toggle-label-light" for="darkmode-toggle" tabIndex={-1}>
 | 
			
		||||
      <svg
 | 
			
		||||
        xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
        xmlnsXlink="http://www.w3.org/1999/xlink"
 | 
			
		||||
        version="1.1"
 | 
			
		||||
        id="dayIcon"
 | 
			
		||||
        x="0px"
 | 
			
		||||
        y="0px"
 | 
			
		||||
        viewBox="0 0 35 35"
 | 
			
		||||
        style="enable-background:new 0 0 35 35;"
 | 
			
		||||
        xmlSpace="preserve"
 | 
			
		||||
      >
 | 
			
		||||
        <title>Light mode</title>
 | 
			
		||||
        <path
 | 
			
		||||
          d="M6,17.5C6,16.672,5.328,16,4.5,16h-3C0.672,16,0,16.672,0,17.5    S0.672,19,1.5,19h3C5.328,19,6,18.328,6,17.5z M7.5,26c-0.414,0-0.789,0.168-1.061,0.439l-2,2C4.168,28.711,4,29.086,4,29.5    C4,30.328,4.671,31,5.5,31c0.414,0,0.789-0.168,1.06-0.44l2-2C8.832,28.289,9,27.914,9,27.5C9,26.672,8.329,26,7.5,26z M17.5,6    C18.329,6,19,5.328,19,4.5v-3C19,0.672,18.329,0,17.5,0S16,0.672,16,1.5v3C16,5.328,16.671,6,17.5,6z M27.5,9    c0.414,0,0.789-0.168,1.06-0.439l2-2C30.832,6.289,31,5.914,31,5.5C31,4.672,30.329,4,29.5,4c-0.414,0-0.789,0.168-1.061,0.44    l-2,2C26.168,6.711,26,7.086,26,7.5C26,8.328,26.671,9,27.5,9z M6.439,8.561C6.711,8.832,7.086,9,7.5,9C8.328,9,9,8.328,9,7.5    c0-0.414-0.168-0.789-0.439-1.061l-2-2C6.289,4.168,5.914,4,5.5,4C4.672,4,4,4.672,4,5.5c0,0.414,0.168,0.789,0.439,1.06    L6.439,8.561z M33.5,16h-3c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5h3c0.828,0,1.5-0.672,1.5-1.5S34.328,16,33.5,16z     M28.561,26.439C28.289,26.168,27.914,26,27.5,26c-0.828,0-1.5,0.672-1.5,1.5c0,0.414,0.168,0.789,0.439,1.06l2,2    C28.711,30.832,29.086,31,29.5,31c0.828,0,1.5-0.672,1.5-1.5c0-0.414-0.168-0.789-0.439-1.061L28.561,26.439z M17.5,29    c-0.829,0-1.5,0.672-1.5,1.5v3c0,0.828,0.671,1.5,1.5,1.5s1.5-0.672,1.5-1.5v-3C19,29.672,18.329,29,17.5,29z M17.5,7    C11.71,7,7,11.71,7,17.5S11.71,28,17.5,28S28,23.29,28,17.5S23.29,7,17.5,7z M17.5,25c-4.136,0-7.5-3.364-7.5-7.5    c0-4.136,3.364-7.5,7.5-7.5c4.136,0,7.5,3.364,7.5,7.5C25,21.636,21.636,25,17.5,25z"
 | 
			
		||||
        ></path>
 | 
			
		||||
      </svg>
 | 
			
		||||
    </label>
 | 
			
		||||
    <label id="toggle-label-dark" for="darkmode-toggle" tabIndex={-1}>
 | 
			
		||||
      <svg
 | 
			
		||||
        xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
        xmlnsXlink="http://www.w3.org/1999/xlink"
 | 
			
		||||
        version="1.1"
 | 
			
		||||
        id="nightIcon"
 | 
			
		||||
        x="0px"
 | 
			
		||||
        y="0px"
 | 
			
		||||
        viewBox="0 0 100 100"
 | 
			
		||||
        style="enable-background='new 0 0 100 100'"
 | 
			
		||||
        xmlSpace="preserve"
 | 
			
		||||
      >
 | 
			
		||||
        <title>Dark mode</title>
 | 
			
		||||
        <path
 | 
			
		||||
          d="M96.76,66.458c-0.853-0.852-2.15-1.064-3.23-0.534c-6.063,2.991-12.858,4.571-19.655,4.571  C62.022,70.495,50.88,65.88,42.5,57.5C29.043,44.043,25.658,23.536,34.076,6.47c0.532-1.08,0.318-2.379-0.534-3.23  c-0.851-0.852-2.15-1.064-3.23-0.534c-4.918,2.427-9.375,5.619-13.246,9.491c-9.447,9.447-14.65,22.008-14.65,35.369  c0,13.36,5.203,25.921,14.65,35.368s22.008,14.65,35.368,14.65c13.361,0,25.921-5.203,35.369-14.65  c3.872-3.871,7.064-8.328,9.491-13.246C97.826,68.608,97.611,67.309,96.76,66.458z"
 | 
			
		||||
        ></path>
 | 
			
		||||
      </svg>
 | 
			
		||||
    </label>
 | 
			
		||||
  </div>
 | 
			
		||||
  return (
 | 
			
		||||
    <div class="darkmode">
 | 
			
		||||
      <input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} />
 | 
			
		||||
      <label id="toggle-label-light" for="darkmode-toggle" tabIndex={-1}>
 | 
			
		||||
        <svg
 | 
			
		||||
          xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
          xmlnsXlink="http://www.w3.org/1999/xlink"
 | 
			
		||||
          version="1.1"
 | 
			
		||||
          id="dayIcon"
 | 
			
		||||
          x="0px"
 | 
			
		||||
          y="0px"
 | 
			
		||||
          viewBox="0 0 35 35"
 | 
			
		||||
          style="enable-background:new 0 0 35 35;"
 | 
			
		||||
          xmlSpace="preserve"
 | 
			
		||||
        >
 | 
			
		||||
          <title>Light mode</title>
 | 
			
		||||
          <path d="M6,17.5C6,16.672,5.328,16,4.5,16h-3C0.672,16,0,16.672,0,17.5    S0.672,19,1.5,19h3C5.328,19,6,18.328,6,17.5z M7.5,26c-0.414,0-0.789,0.168-1.061,0.439l-2,2C4.168,28.711,4,29.086,4,29.5    C4,30.328,4.671,31,5.5,31c0.414,0,0.789-0.168,1.06-0.44l2-2C8.832,28.289,9,27.914,9,27.5C9,26.672,8.329,26,7.5,26z M17.5,6    C18.329,6,19,5.328,19,4.5v-3C19,0.672,18.329,0,17.5,0S16,0.672,16,1.5v3C16,5.328,16.671,6,17.5,6z M27.5,9    c0.414,0,0.789-0.168,1.06-0.439l2-2C30.832,6.289,31,5.914,31,5.5C31,4.672,30.329,4,29.5,4c-0.414,0-0.789,0.168-1.061,0.44    l-2,2C26.168,6.711,26,7.086,26,7.5C26,8.328,26.671,9,27.5,9z M6.439,8.561C6.711,8.832,7.086,9,7.5,9C8.328,9,9,8.328,9,7.5    c0-0.414-0.168-0.789-0.439-1.061l-2-2C6.289,4.168,5.914,4,5.5,4C4.672,4,4,4.672,4,5.5c0,0.414,0.168,0.789,0.439,1.06    L6.439,8.561z M33.5,16h-3c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5h3c0.828,0,1.5-0.672,1.5-1.5S34.328,16,33.5,16z     M28.561,26.439C28.289,26.168,27.914,26,27.5,26c-0.828,0-1.5,0.672-1.5,1.5c0,0.414,0.168,0.789,0.439,1.06l2,2    C28.711,30.832,29.086,31,29.5,31c0.828,0,1.5-0.672,1.5-1.5c0-0.414-0.168-0.789-0.439-1.061L28.561,26.439z M17.5,29    c-0.829,0-1.5,0.672-1.5,1.5v3c0,0.828,0.671,1.5,1.5,1.5s1.5-0.672,1.5-1.5v-3C19,29.672,18.329,29,17.5,29z M17.5,7    C11.71,7,7,11.71,7,17.5S11.71,28,17.5,28S28,23.29,28,17.5S23.29,7,17.5,7z M17.5,25c-4.136,0-7.5-3.364-7.5-7.5    c0-4.136,3.364-7.5,7.5-7.5c4.136,0,7.5,3.364,7.5,7.5C25,21.636,21.636,25,17.5,25z"></path>
 | 
			
		||||
        </svg>
 | 
			
		||||
      </label>
 | 
			
		||||
      <label id="toggle-label-dark" for="darkmode-toggle" tabIndex={-1}>
 | 
			
		||||
        <svg
 | 
			
		||||
          xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
          xmlnsXlink="http://www.w3.org/1999/xlink"
 | 
			
		||||
          version="1.1"
 | 
			
		||||
          id="nightIcon"
 | 
			
		||||
          x="0px"
 | 
			
		||||
          y="0px"
 | 
			
		||||
          viewBox="0 0 100 100"
 | 
			
		||||
          style="enable-background='new 0 0 100 100'"
 | 
			
		||||
          xmlSpace="preserve"
 | 
			
		||||
        >
 | 
			
		||||
          <title>Dark mode</title>
 | 
			
		||||
          <path d="M96.76,66.458c-0.853-0.852-2.15-1.064-3.23-0.534c-6.063,2.991-12.858,4.571-19.655,4.571  C62.022,70.495,50.88,65.88,42.5,57.5C29.043,44.043,25.658,23.536,34.076,6.47c0.532-1.08,0.318-2.379-0.534-3.23  c-0.851-0.852-2.15-1.064-3.23-0.534c-4.918,2.427-9.375,5.619-13.246,9.491c-9.447,9.447-14.65,22.008-14.65,35.369  c0,13.36,5.203,25.921,14.65,35.368s22.008,14.65,35.368,14.65c13.361,0,25.921-5.203,35.369-14.65  c3.872-3.871,7.064-8.328,9.491-13.246C97.826,68.608,97.611,67.309,96.76,66.458z"></path>
 | 
			
		||||
        </svg>
 | 
			
		||||
      </label>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Darkmode.beforeDOMLoaded = darkmodeScript
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,10 @@ interface Props {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function Date({ date }: Props) {
 | 
			
		||||
  const formattedDate = date.toLocaleDateString('en-US', {
 | 
			
		||||
  const formattedDate = date.toLocaleDateString("en-US", {
 | 
			
		||||
    year: "numeric",
 | 
			
		||||
    month: "short",
 | 
			
		||||
    day: '2-digit'
 | 
			
		||||
    day: "2-digit",
 | 
			
		||||
  })
 | 
			
		||||
  return <>{formattedDate}</>
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { QuartzComponentConstructor } from "./types"
 | 
			
		||||
import style from "./styles/footer.scss"
 | 
			
		||||
import {version} from "../../package.json"
 | 
			
		||||
import { version } from "../../package.json"
 | 
			
		||||
 | 
			
		||||
interface Options {
 | 
			
		||||
  links: Record<string, string>
 | 
			
		||||
@@ -10,13 +10,21 @@ export default ((opts?: Options) => {
 | 
			
		||||
  function Footer() {
 | 
			
		||||
    const year = new Date().getFullYear()
 | 
			
		||||
    const links = opts?.links ?? []
 | 
			
		||||
    return <footer>
 | 
			
		||||
      <hr />
 | 
			
		||||
      <p>Created with <a href="https://quartz.jzhao.xyz/">Quartz v{version}</a>, © {year}</p>
 | 
			
		||||
      <ul>{Object.entries(links).map(([text, link]) => <li>
 | 
			
		||||
        <a href={link}>{text}</a>
 | 
			
		||||
      </li>)}</ul>
 | 
			
		||||
    </footer>
 | 
			
		||||
    return (
 | 
			
		||||
      <footer>
 | 
			
		||||
        <hr />
 | 
			
		||||
        <p>
 | 
			
		||||
          Created with <a href="https://quartz.jzhao.xyz/">Quartz v{version}</a>, © {year}
 | 
			
		||||
        </p>
 | 
			
		||||
        <ul>
 | 
			
		||||
          {Object.entries(links).map(([text, link]) => (
 | 
			
		||||
            <li>
 | 
			
		||||
              <a href={link}>{text}</a>
 | 
			
		||||
            </li>
 | 
			
		||||
          ))}
 | 
			
		||||
        </ul>
 | 
			
		||||
      </footer>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Footer.css = style
 | 
			
		||||
 
 | 
			
		||||
@@ -4,19 +4,19 @@ import script from "./scripts/graph.inline"
 | 
			
		||||
import style from "./styles/graph.scss"
 | 
			
		||||
 | 
			
		||||
export interface D3Config {
 | 
			
		||||
  drag: boolean,
 | 
			
		||||
  zoom: boolean,
 | 
			
		||||
  depth: number,
 | 
			
		||||
  scale: number,
 | 
			
		||||
  repelForce: number,
 | 
			
		||||
  centerForce: number,
 | 
			
		||||
  linkDistance: number,
 | 
			
		||||
  fontSize: number,
 | 
			
		||||
  drag: boolean
 | 
			
		||||
  zoom: boolean
 | 
			
		||||
  depth: number
 | 
			
		||||
  scale: number
 | 
			
		||||
  repelForce: number
 | 
			
		||||
  centerForce: number
 | 
			
		||||
  linkDistance: number
 | 
			
		||||
  fontSize: number
 | 
			
		||||
  opacityScale: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface GraphOptions {
 | 
			
		||||
  localGraph: Partial<D3Config> | undefined,
 | 
			
		||||
  localGraph: Partial<D3Config> | undefined
 | 
			
		||||
  globalGraph: Partial<D3Config> | undefined
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -30,7 +30,7 @@ const defaultOptions: GraphOptions = {
 | 
			
		||||
    centerForce: 0.3,
 | 
			
		||||
    linkDistance: 30,
 | 
			
		||||
    fontSize: 0.6,
 | 
			
		||||
    opacityScale: 1
 | 
			
		||||
    opacityScale: 1,
 | 
			
		||||
  },
 | 
			
		||||
  globalGraph: {
 | 
			
		||||
    drag: true,
 | 
			
		||||
@@ -41,21 +41,32 @@ const defaultOptions: GraphOptions = {
 | 
			
		||||
    centerForce: 0.3,
 | 
			
		||||
    linkDistance: 30,
 | 
			
		||||
    fontSize: 0.6,
 | 
			
		||||
    opacityScale: 1
 | 
			
		||||
  }
 | 
			
		||||
    opacityScale: 1,
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ((opts?: GraphOptions) => {
 | 
			
		||||
  function Graph() {
 | 
			
		||||
    const localGraph = { ...opts?.localGraph, ...defaultOptions.localGraph }
 | 
			
		||||
    const globalGraph = { ...opts?.globalGraph, ...defaultOptions.globalGraph }
 | 
			
		||||
    return <div class="graph">
 | 
			
		||||
      <h3>Graph View</h3>
 | 
			
		||||
      <div class="graph-outer">
 | 
			
		||||
        <div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
 | 
			
		||||
        <svg version="1.1" id="global-graph-icon" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
 | 
			
		||||
          viewBox="0 0 55 55" fill="currentColor" xmlSpace="preserve">
 | 
			
		||||
          <path d="M49,0c-3.309,0-6,2.691-6,6c0,1.035,0.263,2.009,0.726,2.86l-9.829,9.829C32.542,17.634,30.846,17,29,17
 | 
			
		||||
    return (
 | 
			
		||||
      <div class="graph">
 | 
			
		||||
        <h3>Graph View</h3>
 | 
			
		||||
        <div class="graph-outer">
 | 
			
		||||
          <div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
 | 
			
		||||
          <svg
 | 
			
		||||
            version="1.1"
 | 
			
		||||
            id="global-graph-icon"
 | 
			
		||||
            xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
            xmlnsXlink="http://www.w3.org/1999/xlink"
 | 
			
		||||
            x="0px"
 | 
			
		||||
            y="0px"
 | 
			
		||||
            viewBox="0 0 55 55"
 | 
			
		||||
            fill="currentColor"
 | 
			
		||||
            xmlSpace="preserve"
 | 
			
		||||
          >
 | 
			
		||||
            <path
 | 
			
		||||
              d="M49,0c-3.309,0-6,2.691-6,6c0,1.035,0.263,2.009,0.726,2.86l-9.829,9.829C32.542,17.634,30.846,17,29,17
 | 
			
		||||
	s-3.542,0.634-4.898,1.688l-7.669-7.669C16.785,10.424,17,9.74,17,9c0-2.206-1.794-4-4-4S9,6.794,9,9s1.794,4,4,4
 | 
			
		||||
	c0.74,0,1.424-0.215,2.019-0.567l7.669,7.669C21.634,21.458,21,23.154,21,25s0.634,3.542,1.688,4.897L10.024,42.562
 | 
			
		||||
	C8.958,41.595,7.549,41,6,41c-3.309,0-6,2.691-6,6s2.691,6,6,6s6-2.691,6-6c0-1.035-0.263-2.009-0.726-2.86l12.829-12.829
 | 
			
		||||
@@ -65,13 +76,15 @@ export default ((opts?: GraphOptions) => {
 | 
			
		||||
	C46.042,11.405,47.451,12,49,12c3.309,0,6-2.691,6-6S52.309,0,49,0z M11,9c0-1.103,0.897-2,2-2s2,0.897,2,2s-0.897,2-2,2
 | 
			
		||||
	S11,10.103,11,9z M6,51c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S8.206,51,6,51z M33,49c0,2.206-1.794,4-4,4s-4-1.794-4-4
 | 
			
		||||
	s1.794-4,4-4S33,46.794,33,49z M29,31c-3.309,0-6-2.691-6-6s2.691-6,6-6s6,2.691,6,6S32.309,31,29,31z M47,41c0,1.103-0.897,2-2,2
 | 
			
		||||
	s-2-0.897-2-2s0.897-2,2-2S47,39.897,47,41z M49,10c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S51.206,10,49,10z"/>
 | 
			
		||||
        </svg>
 | 
			
		||||
	s-2-0.897-2-2s0.897-2,2-2S47,39.897,47,41z M49,10c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S51.206,10,49,10z"
 | 
			
		||||
            />
 | 
			
		||||
          </svg>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div id="global-graph-outer">
 | 
			
		||||
          <div id="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div id="global-graph-outer">
 | 
			
		||||
        <div id="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Graph.css = style
 | 
			
		||||
 
 | 
			
		||||
@@ -12,23 +12,29 @@ export default (() => {
 | 
			
		||||
    const iconPath = baseDir + "/static/icon.png"
 | 
			
		||||
    const ogImagePath = baseDir + "/static/og-image.png"
 | 
			
		||||
 | 
			
		||||
    return <head>
 | 
			
		||||
      <title>{title}</title>
 | 
			
		||||
      <meta charSet="utf-8" />
 | 
			
		||||
      <meta name="viewport" content="width=device-width, initial-scale=1" />
 | 
			
		||||
      <meta property="og:title" content={title} />
 | 
			
		||||
      <meta property="og:description" content={title} />
 | 
			
		||||
      <meta property="og:image" content={ogImagePath} />
 | 
			
		||||
      <meta property="og:width" content="1200" />
 | 
			
		||||
      <meta property="og:height" content="675" />
 | 
			
		||||
      <link rel="icon" href={iconPath} />
 | 
			
		||||
      <meta name="description" content={description} />
 | 
			
		||||
      <meta name="generator" content="Quartz" />
 | 
			
		||||
      <link rel="preconnect" href="https://fonts.googleapis.com"/>
 | 
			
		||||
      <link rel="preconnect" href="https://fonts.gstatic.com"/>
 | 
			
		||||
      {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>
 | 
			
		||||
    return (
 | 
			
		||||
      <head>
 | 
			
		||||
        <title>{title}</title>
 | 
			
		||||
        <meta charSet="utf-8" />
 | 
			
		||||
        <meta name="viewport" content="width=device-width, initial-scale=1" />
 | 
			
		||||
        <meta property="og:title" content={title} />
 | 
			
		||||
        <meta property="og:description" content={title} />
 | 
			
		||||
        <meta property="og:image" content={ogImagePath} />
 | 
			
		||||
        <meta property="og:width" content="1200" />
 | 
			
		||||
        <meta property="og:height" content="675" />
 | 
			
		||||
        <link rel="icon" href={iconPath} />
 | 
			
		||||
        <meta name="description" content={description} />
 | 
			
		||||
        <meta name="generator" content="Quartz" />
 | 
			
		||||
        <link rel="preconnect" href="https://fonts.googleapis.com" />
 | 
			
		||||
        <link rel="preconnect" href="https://fonts.gstatic.com" />
 | 
			
		||||
        {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>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return Head
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,7 @@
 | 
			
		||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 | 
			
		||||
 | 
			
		||||
function Header({ children }: QuartzComponentProps) {
 | 
			
		||||
  return (children.length > 0) ? <header>
 | 
			
		||||
    {children}
 | 
			
		||||
  </header> : null
 | 
			
		||||
  return children.length > 0 ? <header>{children}</header> : null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Header.css = `
 | 
			
		||||
 
 | 
			
		||||
@@ -17,32 +17,51 @@ function byDateAndAlphabetical(f1: QuartzPluginData, f2: QuartzPluginData): numb
 | 
			
		||||
  // otherwise, sort lexographically by title
 | 
			
		||||
  const f1Title = f1.frontmatter?.title.toLowerCase() ?? ""
 | 
			
		||||
  const f2Title = f2.frontmatter?.title.toLowerCase() ?? ""
 | 
			
		||||
  return f1Title.localeCompare(f2Title) 
 | 
			
		||||
  return f1Title.localeCompare(f2Title)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function PageList({ fileData, allFiles }: QuartzComponentProps) {
 | 
			
		||||
  const slug = canonicalizeServer(fileData.slug!)
 | 
			
		||||
  return <ul class="section-ul">
 | 
			
		||||
    {allFiles.sort(byDateAndAlphabetical).map(page => {
 | 
			
		||||
      const title = page.frontmatter?.title
 | 
			
		||||
      const pageSlug = canonicalizeServer(page.slug!)
 | 
			
		||||
      const tags = page.frontmatter?.tags ?? []
 | 
			
		||||
  return (
 | 
			
		||||
    <ul class="section-ul">
 | 
			
		||||
      {allFiles.sort(byDateAndAlphabetical).map((page) => {
 | 
			
		||||
        const title = page.frontmatter?.title
 | 
			
		||||
        const pageSlug = canonicalizeServer(page.slug!)
 | 
			
		||||
        const tags = page.frontmatter?.tags ?? []
 | 
			
		||||
 | 
			
		||||
      return <li class="section-li">
 | 
			
		||||
        <div class="section">
 | 
			
		||||
          {page.dates && <p class="meta">
 | 
			
		||||
            <Date date={page.dates.modified} />
 | 
			
		||||
          </p>}
 | 
			
		||||
          <div class="desc">
 | 
			
		||||
            <h3><a href={resolveRelative(slug, pageSlug)} class="internal">{title}</a></h3>
 | 
			
		||||
          </div>
 | 
			
		||||
          <ul class="tags">
 | 
			
		||||
            {tags.map(tag => <li><a class="internal" href={resolveRelative(slug, `tags/${tag}` as CanonicalSlug)}>#{tag}</a></li>)}
 | 
			
		||||
          </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
      </li>
 | 
			
		||||
    })}
 | 
			
		||||
  </ul>
 | 
			
		||||
        return (
 | 
			
		||||
          <li class="section-li">
 | 
			
		||||
            <div class="section">
 | 
			
		||||
              {page.dates && (
 | 
			
		||||
                <p class="meta">
 | 
			
		||||
                  <Date date={page.dates.modified} />
 | 
			
		||||
                </p>
 | 
			
		||||
              )}
 | 
			
		||||
              <div class="desc">
 | 
			
		||||
                <h3>
 | 
			
		||||
                  <a href={resolveRelative(slug, pageSlug)} class="internal">
 | 
			
		||||
                    {title}
 | 
			
		||||
                  </a>
 | 
			
		||||
                </h3>
 | 
			
		||||
              </div>
 | 
			
		||||
              <ul class="tags">
 | 
			
		||||
                {tags.map((tag) => (
 | 
			
		||||
                  <li>
 | 
			
		||||
                    <a
 | 
			
		||||
                      class="internal"
 | 
			
		||||
                      href={resolveRelative(slug, `tags/${tag}` as CanonicalSlug)}
 | 
			
		||||
                    >
 | 
			
		||||
                      #{tag}
 | 
			
		||||
                    </a>
 | 
			
		||||
                  </li>
 | 
			
		||||
                ))}
 | 
			
		||||
              </ul>
 | 
			
		||||
            </div>
 | 
			
		||||
          </li>
 | 
			
		||||
        )
 | 
			
		||||
      })}
 | 
			
		||||
    </ul>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PageList.css = `
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,11 @@ function PageTitle({ fileData, cfg }: QuartzComponentProps) {
 | 
			
		||||
  const title = cfg?.pageTitle ?? "Untitled Quartz"
 | 
			
		||||
  const slug = canonicalizeServer(fileData.slug!)
 | 
			
		||||
  const baseDir = pathToRoot(slug)
 | 
			
		||||
  return <h1 class="page-title"><a href={baseDir}>{title}</a></h1>
 | 
			
		||||
  return (
 | 
			
		||||
    <h1 class="page-title">
 | 
			
		||||
      <a href={baseDir}>{title}</a>
 | 
			
		||||
    </h1>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PageTitle.css = `
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,11 @@ function ReadingTime({ fileData }: QuartzComponentProps) {
 | 
			
		||||
  const text = fileData.text
 | 
			
		||||
  if (text) {
 | 
			
		||||
    const { text: timeTaken, words } = readingTime(text)
 | 
			
		||||
    return <p class="reading-time">{words} words, {timeTaken}</p>
 | 
			
		||||
    return (
 | 
			
		||||
      <p class="reading-time">
 | 
			
		||||
        {words} words, {timeTaken}
 | 
			
		||||
      </p>
 | 
			
		||||
    )
 | 
			
		||||
  } else {
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -5,27 +5,41 @@ import script from "./scripts/search.inline"
 | 
			
		||||
 | 
			
		||||
export default (() => {
 | 
			
		||||
  function Search() {
 | 
			
		||||
    return <div class="search">
 | 
			
		||||
      <div id="search-icon">
 | 
			
		||||
        <p>Search</p>
 | 
			
		||||
        <div></div>
 | 
			
		||||
        <svg tabIndex={0} aria-labelledby="title desc" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.9 19.7">
 | 
			
		||||
          <title id="title">Search</title>
 | 
			
		||||
          <desc id="desc">Search</desc>
 | 
			
		||||
          <g class="search-path" fill="none">
 | 
			
		||||
            <path stroke-linecap="square" d="M18.5 18.3l-5.4-5.4" />
 | 
			
		||||
            <circle cx="8" cy="8" r="7" />
 | 
			
		||||
          </g>
 | 
			
		||||
        </svg>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div id="search-container">
 | 
			
		||||
        <div id="search-space">
 | 
			
		||||
          <input autocomplete="off" id="search-bar" name="search" type="text" aria-label="Search for something" placeholder="Search for something" />
 | 
			
		||||
          <div id="results-container">
 | 
			
		||||
    return (
 | 
			
		||||
      <div class="search">
 | 
			
		||||
        <div id="search-icon">
 | 
			
		||||
          <p>Search</p>
 | 
			
		||||
          <div></div>
 | 
			
		||||
          <svg
 | 
			
		||||
            tabIndex={0}
 | 
			
		||||
            aria-labelledby="title desc"
 | 
			
		||||
            role="img"
 | 
			
		||||
            xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
            viewBox="0 0 19.9 19.7"
 | 
			
		||||
          >
 | 
			
		||||
            <title id="title">Search</title>
 | 
			
		||||
            <desc id="desc">Search</desc>
 | 
			
		||||
            <g class="search-path" fill="none">
 | 
			
		||||
              <path stroke-linecap="square" d="M18.5 18.3l-5.4-5.4" />
 | 
			
		||||
              <circle cx="8" cy="8" r="7" />
 | 
			
		||||
            </g>
 | 
			
		||||
          </svg>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div id="search-container">
 | 
			
		||||
          <div id="search-space">
 | 
			
		||||
            <input
 | 
			
		||||
              autocomplete="off"
 | 
			
		||||
              id="search-bar"
 | 
			
		||||
              name="search"
 | 
			
		||||
              type="text"
 | 
			
		||||
              aria-label="Search for something"
 | 
			
		||||
              placeholder="Search for something"
 | 
			
		||||
            />
 | 
			
		||||
            <div id="results-container"></div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Search.afterDOMLoaded = script
 | 
			
		||||
 
 | 
			
		||||
@@ -6,11 +6,11 @@ import modernStyle from "./styles/toc.scss"
 | 
			
		||||
import script from "./scripts/toc.inline"
 | 
			
		||||
 | 
			
		||||
interface Options {
 | 
			
		||||
  layout: 'modern' | 'legacy'
 | 
			
		||||
  layout: "modern" | "legacy"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultOptions: Options = {
 | 
			
		||||
  layout: 'modern'
 | 
			
		||||
  layout: "modern",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function TableOfContents({ fileData }: QuartzComponentProps) {
 | 
			
		||||
@@ -18,21 +18,38 @@ function TableOfContents({ fileData }: QuartzComponentProps) {
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return <div class="desktop-only">
 | 
			
		||||
    <button type="button" id="toc">
 | 
			
		||||
      <h3>Table of Contents</h3>
 | 
			
		||||
      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="fold">
 | 
			
		||||
        <polyline points="6 9 12 15 18 9"></polyline>
 | 
			
		||||
      </svg>
 | 
			
		||||
    </button>
 | 
			
		||||
    <div id="toc-content">
 | 
			
		||||
      <ul class="overflow">
 | 
			
		||||
        {fileData.toc.map(tocEntry => <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
 | 
			
		||||
          <a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>{tocEntry.text}</a>
 | 
			
		||||
        </li>)}
 | 
			
		||||
      </ul>
 | 
			
		||||
  return (
 | 
			
		||||
    <div class="desktop-only">
 | 
			
		||||
      <button type="button" id="toc">
 | 
			
		||||
        <h3>Table of Contents</h3>
 | 
			
		||||
        <svg
 | 
			
		||||
          xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
          width="24"
 | 
			
		||||
          height="24"
 | 
			
		||||
          viewBox="0 0 24 24"
 | 
			
		||||
          fill="none"
 | 
			
		||||
          stroke="currentColor"
 | 
			
		||||
          stroke-width="2"
 | 
			
		||||
          stroke-linecap="round"
 | 
			
		||||
          stroke-linejoin="round"
 | 
			
		||||
          class="fold"
 | 
			
		||||
        >
 | 
			
		||||
          <polyline points="6 9 12 15 18 9"></polyline>
 | 
			
		||||
        </svg>
 | 
			
		||||
      </button>
 | 
			
		||||
      <div id="toc-content">
 | 
			
		||||
        <ul class="overflow">
 | 
			
		||||
          {fileData.toc.map((tocEntry) => (
 | 
			
		||||
            <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
 | 
			
		||||
              <a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>
 | 
			
		||||
                {tocEntry.text}
 | 
			
		||||
              </a>
 | 
			
		||||
            </li>
 | 
			
		||||
          ))}
 | 
			
		||||
        </ul>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
TableOfContents.css = modernStyle
 | 
			
		||||
TableOfContents.afterDOMLoaded = script
 | 
			
		||||
@@ -42,16 +59,22 @@ function LegacyTableOfContents({ fileData }: QuartzComponentProps) {
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return <details id="toc" open>
 | 
			
		||||
    <summary>
 | 
			
		||||
      <h3>Table of Contents</h3>
 | 
			
		||||
    </summary>
 | 
			
		||||
    <ul>
 | 
			
		||||
      {fileData.toc.map(tocEntry => <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
 | 
			
		||||
        <a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>{tocEntry.text}</a>
 | 
			
		||||
      </li>)}
 | 
			
		||||
    </ul>
 | 
			
		||||
  </details>
 | 
			
		||||
  return (
 | 
			
		||||
    <details id="toc" open>
 | 
			
		||||
      <summary>
 | 
			
		||||
        <h3>Table of Contents</h3>
 | 
			
		||||
      </summary>
 | 
			
		||||
      <ul>
 | 
			
		||||
        {fileData.toc.map((tocEntry) => (
 | 
			
		||||
          <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
 | 
			
		||||
            <a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>
 | 
			
		||||
              {tocEntry.text}
 | 
			
		||||
            </a>
 | 
			
		||||
          </li>
 | 
			
		||||
        ))}
 | 
			
		||||
      </ul>
 | 
			
		||||
    </details>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
LegacyTableOfContents.css = legacyStyle
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,27 @@
 | 
			
		||||
import { canonicalizeServer, pathToRoot } from "../path"
 | 
			
		||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 | 
			
		||||
import { slug as slugAnchor } from 'github-slugger'
 | 
			
		||||
import { slug as slugAnchor } from "github-slugger"
 | 
			
		||||
 | 
			
		||||
function TagList({ fileData }: QuartzComponentProps) {
 | 
			
		||||
  const tags = fileData.frontmatter?.tags
 | 
			
		||||
  const slug = canonicalizeServer(fileData.slug!)
 | 
			
		||||
  const baseDir = pathToRoot(slug)
 | 
			
		||||
  if (tags && tags.length > 0) {
 | 
			
		||||
    return <ul class="tags">{tags.map(tag => {
 | 
			
		||||
      const display = `#${tag}`
 | 
			
		||||
      const linkDest = baseDir + `/tags/${slugAnchor(tag)}`
 | 
			
		||||
      return <li>
 | 
			
		||||
        <a href={linkDest} class="internal">{display}</a>
 | 
			
		||||
      </li>
 | 
			
		||||
    })}</ul>
 | 
			
		||||
    return (
 | 
			
		||||
      <ul class="tags">
 | 
			
		||||
        {tags.map((tag) => {
 | 
			
		||||
          const display = `#${tag}`
 | 
			
		||||
          const linkDest = baseDir + `/tags/${slugAnchor(tag)}`
 | 
			
		||||
          return (
 | 
			
		||||
            <li>
 | 
			
		||||
              <a href={linkDest} class="internal">
 | 
			
		||||
                {display}
 | 
			
		||||
              </a>
 | 
			
		||||
            </li>
 | 
			
		||||
          )
 | 
			
		||||
        })}
 | 
			
		||||
      </ul>
 | 
			
		||||
    )
 | 
			
		||||
  } else {
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ import ReadingTime from "./ReadingTime"
 | 
			
		||||
import Spacer from "./Spacer"
 | 
			
		||||
import TableOfContents from "./TableOfContents"
 | 
			
		||||
import TagList from "./TagList"
 | 
			
		||||
import Graph from "./Graph" 
 | 
			
		||||
import Graph from "./Graph"
 | 
			
		||||
import Backlinks from "./Backlinks"
 | 
			
		||||
import Search from "./Search"
 | 
			
		||||
import Footer from "./Footer"
 | 
			
		||||
@@ -33,5 +33,5 @@ export {
 | 
			
		||||
  Search,
 | 
			
		||||
  Footer,
 | 
			
		||||
  DesktopOnly,
 | 
			
		||||
  MobileOnly
 | 
			
		||||
} 
 | 
			
		||||
  MobileOnly,
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
 | 
			
		||||
import { Fragment, jsx, jsxs } from 'preact/jsx-runtime'
 | 
			
		||||
import { Fragment, jsx, jsxs } from "preact/jsx-runtime"
 | 
			
		||||
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
 | 
			
		||||
 | 
			
		||||
function Content({ tree }: QuartzComponentProps) {
 | 
			
		||||
  // @ts-ignore (preact makes it angry)
 | 
			
		||||
  const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
 | 
			
		||||
  const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: "html" })
 | 
			
		||||
  return <article class="popover-hint">{content}</article>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,16 @@
 | 
			
		||||
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
 | 
			
		||||
import { Fragment, jsx, jsxs } from 'preact/jsx-runtime'
 | 
			
		||||
import { Fragment, jsx, jsxs } from "preact/jsx-runtime"
 | 
			
		||||
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
 | 
			
		||||
import path from "path"
 | 
			
		||||
 | 
			
		||||
import style from '../styles/listPage.scss'
 | 
			
		||||
import style from "../styles/listPage.scss"
 | 
			
		||||
import { PageList } from "../PageList"
 | 
			
		||||
import { canonicalizeServer } from "../../path"
 | 
			
		||||
 | 
			
		||||
function FolderContent(props: QuartzComponentProps) {
 | 
			
		||||
  const { tree, fileData, allFiles } = props
 | 
			
		||||
  const folderSlug = canonicalizeServer(fileData.slug!)
 | 
			
		||||
  const allPagesInFolder = allFiles.filter(file => {
 | 
			
		||||
  const allPagesInFolder = allFiles.filter((file) => {
 | 
			
		||||
    const fileSlug = file.slug ?? ""
 | 
			
		||||
    const prefixed = fileSlug.startsWith(folderSlug)
 | 
			
		||||
    const folderParts = folderSlug.split(path.posix.sep)
 | 
			
		||||
@@ -21,18 +21,20 @@ function FolderContent(props: QuartzComponentProps) {
 | 
			
		||||
 | 
			
		||||
  const listProps = {
 | 
			
		||||
    ...props,
 | 
			
		||||
    allFiles: allPagesInFolder
 | 
			
		||||
    allFiles: allPagesInFolder,
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
 | 
			
		||||
  return <div class="popover-hint">
 | 
			
		||||
    <article>{content}</article>
 | 
			
		||||
    <p>{allPagesInFolder.length} items under this folder.</p>
 | 
			
		||||
    <div>
 | 
			
		||||
      <PageList {...listProps} /> 
 | 
			
		||||
  const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: "html" })
 | 
			
		||||
  return (
 | 
			
		||||
    <div class="popover-hint">
 | 
			
		||||
      <article>{content}</article>
 | 
			
		||||
      <p>{allPagesInFolder.length} items under this folder.</p>
 | 
			
		||||
      <div>
 | 
			
		||||
        <PageList {...listProps} />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
FolderContent.css = style + PageList.css
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
 | 
			
		||||
import { Fragment, jsx, jsxs } from 'preact/jsx-runtime'
 | 
			
		||||
import { Fragment, jsx, jsxs } from "preact/jsx-runtime"
 | 
			
		||||
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
 | 
			
		||||
import style from '../styles/listPage.scss'
 | 
			
		||||
import style from "../styles/listPage.scss"
 | 
			
		||||
import { PageList } from "../PageList"
 | 
			
		||||
import { ServerSlug, canonicalizeServer } from "../../path"
 | 
			
		||||
 | 
			
		||||
@@ -11,21 +11,23 @@ function TagContent(props: QuartzComponentProps) {
 | 
			
		||||
 | 
			
		||||
  if (slug?.startsWith("tags/")) {
 | 
			
		||||
    const tag = canonicalizeServer(slug.slice("tags/".length) as ServerSlug)
 | 
			
		||||
    const allPagesWithTag = allFiles.filter(file => (file.frontmatter?.tags ?? []).includes(tag))
 | 
			
		||||
    const allPagesWithTag = allFiles.filter((file) => (file.frontmatter?.tags ?? []).includes(tag))
 | 
			
		||||
    const listProps = {
 | 
			
		||||
      ...props,
 | 
			
		||||
      allFiles: allPagesWithTag
 | 
			
		||||
      allFiles: allPagesWithTag,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
 | 
			
		||||
    return <div class="popover-hint">
 | 
			
		||||
      <article>{content}</article>
 | 
			
		||||
      <p>{allPagesWithTag.length} items with this tag.</p>
 | 
			
		||||
      <div>
 | 
			
		||||
        <PageList {...listProps} />
 | 
			
		||||
    const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: "html" })
 | 
			
		||||
    return (
 | 
			
		||||
      <div class="popover-hint">
 | 
			
		||||
        <article>{content}</article>
 | 
			
		||||
        <p>{allPagesWithTag.length} items with this tag.</p>
 | 
			
		||||
        <div>
 | 
			
		||||
          <PageList {...listProps} />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    )
 | 
			
		||||
  } else {
 | 
			
		||||
    throw new Error(`Component "TagContent" tried to render a non-tag page: ${slug}`)
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,24 @@
 | 
			
		||||
import { render } from "preact-render-to-string";
 | 
			
		||||
import { QuartzComponent, QuartzComponentProps } from "./types";
 | 
			
		||||
import { render } from "preact-render-to-string"
 | 
			
		||||
import { QuartzComponent, QuartzComponentProps } from "./types"
 | 
			
		||||
import HeaderConstructor from "./Header"
 | 
			
		||||
import BodyConstructor from "./Body"
 | 
			
		||||
import { JSResourceToScriptElement, StaticResources } from "../resources";
 | 
			
		||||
import { CanonicalSlug, pathToRoot } from "../path";
 | 
			
		||||
import { JSResourceToScriptElement, StaticResources } from "../resources"
 | 
			
		||||
import { CanonicalSlug, pathToRoot } from "../path"
 | 
			
		||||
 | 
			
		||||
interface RenderComponents {
 | 
			
		||||
  head: QuartzComponent
 | 
			
		||||
  header: QuartzComponent[],
 | 
			
		||||
  beforeBody: QuartzComponent[],
 | 
			
		||||
  pageBody: QuartzComponent,
 | 
			
		||||
  left: QuartzComponent[],
 | 
			
		||||
  right: QuartzComponent[],
 | 
			
		||||
  footer: QuartzComponent,
 | 
			
		||||
  header: QuartzComponent[]
 | 
			
		||||
  beforeBody: QuartzComponent[]
 | 
			
		||||
  pageBody: QuartzComponent
 | 
			
		||||
  left: QuartzComponent[]
 | 
			
		||||
  right: QuartzComponent[]
 | 
			
		||||
  footer: QuartzComponent
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function pageResources(slug: CanonicalSlug, staticResources: StaticResources): StaticResources {
 | 
			
		||||
export function pageResources(
 | 
			
		||||
  slug: CanonicalSlug,
 | 
			
		||||
  staticResources: StaticResources,
 | 
			
		||||
): StaticResources {
 | 
			
		||||
  const baseDir = pathToRoot(slug)
 | 
			
		||||
 | 
			
		||||
  const contentIndexPath = baseDir + "/static/contentIndex.json"
 | 
			
		||||
@@ -25,52 +28,89 @@ export function pageResources(slug: CanonicalSlug, staticResources: StaticResour
 | 
			
		||||
    css: [baseDir + "/index.css", ...staticResources.css],
 | 
			
		||||
    js: [
 | 
			
		||||
      { src: baseDir + "/prescript.js", loadTime: "beforeDOMReady", contentType: "external" },
 | 
			
		||||
      { loadTime: "beforeDOMReady", contentType: "inline", spaPreserve: true, script: contentIndexScript },
 | 
			
		||||
      {
 | 
			
		||||
        loadTime: "beforeDOMReady",
 | 
			
		||||
        contentType: "inline",
 | 
			
		||||
        spaPreserve: true,
 | 
			
		||||
        script: contentIndexScript,
 | 
			
		||||
      },
 | 
			
		||||
      ...staticResources.js,
 | 
			
		||||
      { src: baseDir + "/postscript.js", loadTime: "afterDOMReady", moduleType: 'module', contentType: "external" }
 | 
			
		||||
    ]
 | 
			
		||||
      {
 | 
			
		||||
        src: baseDir + "/postscript.js",
 | 
			
		||||
        loadTime: "afterDOMReady",
 | 
			
		||||
        moduleType: "module",
 | 
			
		||||
        contentType: "external",
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderPage(slug: CanonicalSlug, componentData: QuartzComponentProps, components: RenderComponents, pageResources: StaticResources): string {
 | 
			
		||||
  const { head: Head, header, beforeBody, pageBody: Content, left, right, footer: Footer } = components
 | 
			
		||||
export function renderPage(
 | 
			
		||||
  slug: CanonicalSlug,
 | 
			
		||||
  componentData: QuartzComponentProps,
 | 
			
		||||
  components: RenderComponents,
 | 
			
		||||
  pageResources: StaticResources,
 | 
			
		||||
): string {
 | 
			
		||||
  const {
 | 
			
		||||
    head: Head,
 | 
			
		||||
    header,
 | 
			
		||||
    beforeBody,
 | 
			
		||||
    pageBody: Content,
 | 
			
		||||
    left,
 | 
			
		||||
    right,
 | 
			
		||||
    footer: Footer,
 | 
			
		||||
  } = components
 | 
			
		||||
  const Header = HeaderConstructor()
 | 
			
		||||
  const Body = BodyConstructor()
 | 
			
		||||
 | 
			
		||||
  const LeftComponent =
 | 
			
		||||
  const LeftComponent = (
 | 
			
		||||
    <div class="left sidebar">
 | 
			
		||||
      {left.map(BodyComponent => <BodyComponent {...componentData} />)}
 | 
			
		||||
      {left.map((BodyComponent) => (
 | 
			
		||||
        <BodyComponent {...componentData} />
 | 
			
		||||
      ))}
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const RightComponent =
 | 
			
		||||
  const RightComponent = (
 | 
			
		||||
    <div class="right sidebar">
 | 
			
		||||
      {right.map(BodyComponent => <BodyComponent {...componentData} />)}
 | 
			
		||||
      {right.map((BodyComponent) => (
 | 
			
		||||
        <BodyComponent {...componentData} />
 | 
			
		||||
      ))}
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const doc = <html>
 | 
			
		||||
    <Head {...componentData} />
 | 
			
		||||
    <body data-slug={slug}>
 | 
			
		||||
      <div id="quartz-root" class="page">
 | 
			
		||||
        <Body {...componentData}>
 | 
			
		||||
          {LeftComponent}
 | 
			
		||||
          <div class="center">
 | 
			
		||||
            <div class="page-header">
 | 
			
		||||
              <Header {...componentData} >
 | 
			
		||||
                {header.map(HeaderComponent => <HeaderComponent {...componentData} />)}
 | 
			
		||||
              </Header>
 | 
			
		||||
              <div class="popover-hint">
 | 
			
		||||
                {beforeBody.map(BodyComponent => <BodyComponent {...componentData} />)}
 | 
			
		||||
  const doc = (
 | 
			
		||||
    <html>
 | 
			
		||||
      <Head {...componentData} />
 | 
			
		||||
      <body data-slug={slug}>
 | 
			
		||||
        <div id="quartz-root" class="page">
 | 
			
		||||
          <Body {...componentData}>
 | 
			
		||||
            {LeftComponent}
 | 
			
		||||
            <div class="center">
 | 
			
		||||
              <div class="page-header">
 | 
			
		||||
                <Header {...componentData}>
 | 
			
		||||
                  {header.map((HeaderComponent) => (
 | 
			
		||||
                    <HeaderComponent {...componentData} />
 | 
			
		||||
                  ))}
 | 
			
		||||
                </Header>
 | 
			
		||||
                <div class="popover-hint">
 | 
			
		||||
                  {beforeBody.map((BodyComponent) => (
 | 
			
		||||
                    <BodyComponent {...componentData} />
 | 
			
		||||
                  ))}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <Content {...componentData} />
 | 
			
		||||
            </div>
 | 
			
		||||
            <Content {...componentData} />
 | 
			
		||||
          </div>
 | 
			
		||||
          {RightComponent}
 | 
			
		||||
        </Body>
 | 
			
		||||
        <Footer {...componentData} />
 | 
			
		||||
      </div>
 | 
			
		||||
    </body>
 | 
			
		||||
    {pageResources.js.filter(resource => resource.loadTime === "afterDOMReady").map(res => JSResourceToScriptElement(res))}
 | 
			
		||||
  </html>
 | 
			
		||||
            {RightComponent}
 | 
			
		||||
          </Body>
 | 
			
		||||
          <Footer {...componentData} />
 | 
			
		||||
        </div>
 | 
			
		||||
      </body>
 | 
			
		||||
      {pageResources.js
 | 
			
		||||
        .filter((resource) => resource.loadTime === "afterDOMReady")
 | 
			
		||||
        .map((res) => JSResourceToScriptElement(res))}
 | 
			
		||||
    </html>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  return "<!DOCTYPE html>\n" + render(doc)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,9 @@ function toggleCallout(this: HTMLElement) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setupCallout() {
 | 
			
		||||
  const collapsible = document.getElementsByClassName(`callout is-collapsible`) as HTMLCollectionOf<HTMLElement>
 | 
			
		||||
  const collapsible = document.getElementsByClassName(
 | 
			
		||||
    `callout is-collapsible`,
 | 
			
		||||
  ) as HTMLCollectionOf<HTMLElement>
 | 
			
		||||
  for (const div of collapsible) {
 | 
			
		||||
    const title = div.firstElementChild
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,24 +1,23 @@
 | 
			
		||||
const userPref = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'
 | 
			
		||||
const currentTheme = localStorage.getItem('theme') ?? userPref
 | 
			
		||||
document.documentElement.setAttribute('saved-theme', currentTheme)
 | 
			
		||||
const userPref = window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark"
 | 
			
		||||
const currentTheme = localStorage.getItem("theme") ?? userPref
 | 
			
		||||
document.documentElement.setAttribute("saved-theme", currentTheme)
 | 
			
		||||
 | 
			
		||||
document.addEventListener("nav", () => {
 | 
			
		||||
  const switchTheme = (e: any) => {
 | 
			
		||||
    if (e.target.checked) {
 | 
			
		||||
      document.documentElement.setAttribute('saved-theme', 'dark')
 | 
			
		||||
      localStorage.setItem('theme', 'dark')
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      document.documentElement.setAttribute('saved-theme', 'light')
 | 
			
		||||
      localStorage.setItem('theme', 'light')
 | 
			
		||||
      document.documentElement.setAttribute("saved-theme", "dark")
 | 
			
		||||
      localStorage.setItem("theme", "dark")
 | 
			
		||||
    } else {
 | 
			
		||||
      document.documentElement.setAttribute("saved-theme", "light")
 | 
			
		||||
      localStorage.setItem("theme", "light")
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Darkmode toggle
 | 
			
		||||
  const toggleSwitch = document.querySelector('#darkmode-toggle') as HTMLInputElement
 | 
			
		||||
  toggleSwitch.removeEventListener('change', switchTheme)
 | 
			
		||||
  toggleSwitch.addEventListener('change', switchTheme)
 | 
			
		||||
  if (currentTheme === 'dark') {
 | 
			
		||||
  const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement
 | 
			
		||||
  toggleSwitch.removeEventListener("change", switchTheme)
 | 
			
		||||
  toggleSwitch.addEventListener("change", switchTheme)
 | 
			
		||||
  if (currentTheme === "dark") {
 | 
			
		||||
    toggleSwitch.checked = true
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,16 @@
 | 
			
		||||
import { ContentDetails } from "../../plugins/emitters/contentIndex"
 | 
			
		||||
import * as d3 from 'd3'
 | 
			
		||||
import * as d3 from "d3"
 | 
			
		||||
import { registerEscapeHandler, removeAllChildren } from "./util"
 | 
			
		||||
import { CanonicalSlug, getCanonicalSlug, getClientSlug, resolveRelative } from "../../path"
 | 
			
		||||
 | 
			
		||||
type NodeData = {
 | 
			
		||||
  id: CanonicalSlug,
 | 
			
		||||
  text: string,
 | 
			
		||||
  id: CanonicalSlug
 | 
			
		||||
  text: string
 | 
			
		||||
  tags: string[]
 | 
			
		||||
} & d3.SimulationNodeDatum
 | 
			
		||||
 | 
			
		||||
type LinkData = {
 | 
			
		||||
  source: CanonicalSlug,
 | 
			
		||||
  source: CanonicalSlug
 | 
			
		||||
  target: CanonicalSlug
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -40,7 +40,7 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
 | 
			
		||||
    centerForce,
 | 
			
		||||
    linkDistance,
 | 
			
		||||
    fontSize,
 | 
			
		||||
    opacityScale
 | 
			
		||||
    opacityScale,
 | 
			
		||||
  } = JSON.parse(graph.dataset["cfg"]!)
 | 
			
		||||
 | 
			
		||||
  const data = await fetchData
 | 
			
		||||
@@ -66,18 +66,22 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
 | 
			
		||||
        wl.push("__SENTINEL")
 | 
			
		||||
      } else {
 | 
			
		||||
        neighbourhood.add(cur)
 | 
			
		||||
        const outgoing = links.filter(l => l.source === cur)
 | 
			
		||||
        const incoming = links.filter(l => l.target === cur)
 | 
			
		||||
        const outgoing = links.filter((l) => l.source === cur)
 | 
			
		||||
        const incoming = links.filter((l) => l.target === cur)
 | 
			
		||||
        wl.push(...outgoing.map((l) => l.target), ...incoming.map((l) => l.source))
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    Object.keys(data).forEach(id => neighbourhood.add(id as CanonicalSlug))
 | 
			
		||||
    Object.keys(data).forEach((id) => neighbourhood.add(id as CanonicalSlug))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const graphData: { nodes: NodeData[], links: LinkData[] } = {
 | 
			
		||||
    nodes: [...neighbourhood].map(url => ({ id: url, text: data[url]?.title ?? url, tags: data[url]?.tags ?? [] })),
 | 
			
		||||
    links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target))
 | 
			
		||||
  const graphData: { nodes: NodeData[]; links: LinkData[] } = {
 | 
			
		||||
    nodes: [...neighbourhood].map((url) => ({
 | 
			
		||||
      id: url,
 | 
			
		||||
      text: data[url]?.title ?? url,
 | 
			
		||||
      tags: data[url]?.tags ?? [],
 | 
			
		||||
    })),
 | 
			
		||||
    links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)),
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const simulation: d3.Simulation<NodeData, LinkData> = d3
 | 
			
		||||
@@ -96,11 +100,11 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
 | 
			
		||||
  const width = graph.offsetWidth
 | 
			
		||||
 | 
			
		||||
  const svg = d3
 | 
			
		||||
    .select<HTMLElement, NodeData>('#' + container)
 | 
			
		||||
    .select<HTMLElement, NodeData>("#" + container)
 | 
			
		||||
    .append("svg")
 | 
			
		||||
    .attr("width", width)
 | 
			
		||||
    .attr("height", height)
 | 
			
		||||
    .attr('viewBox', [-width / 2 / scale, -height / 2 / scale, width / scale, height / scale])
 | 
			
		||||
    .attr("viewBox", [-width / 2 / scale, -height / 2 / scale, width / scale, height / scale])
 | 
			
		||||
 | 
			
		||||
  // draw links between nodes
 | 
			
		||||
  const link = svg
 | 
			
		||||
@@ -145,7 +149,7 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
 | 
			
		||||
      d.fy = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const noop = () => { }
 | 
			
		||||
    const noop = () => {}
 | 
			
		||||
    return d3
 | 
			
		||||
      .drag<Element, NodeData>()
 | 
			
		||||
      .on("start", enableDrag ? dragstarted : noop)
 | 
			
		||||
@@ -170,9 +174,11 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
 | 
			
		||||
      const targ = resolveRelative(slug, d.id)
 | 
			
		||||
      window.spaNavigate(new URL(targ, getClientSlug(window)))
 | 
			
		||||
    })
 | 
			
		||||
    .on("mouseover", function(_, d) {
 | 
			
		||||
    .on("mouseover", function (_, d) {
 | 
			
		||||
      const neighbours: CanonicalSlug[] = data[slug].links ?? []
 | 
			
		||||
      const neighbourNodes = d3.selectAll<HTMLElement, NodeData>(".node").filter((d) => neighbours.includes(d.id))
 | 
			
		||||
      const neighbourNodes = d3
 | 
			
		||||
        .selectAll<HTMLElement, NodeData>(".node")
 | 
			
		||||
        .filter((d) => neighbours.includes(d.id))
 | 
			
		||||
      console.log(neighbourNodes)
 | 
			
		||||
      const currentId = d.id
 | 
			
		||||
      const linkNodes = d3
 | 
			
		||||
@@ -183,12 +189,7 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
 | 
			
		||||
      neighbourNodes.transition().duration(200).attr("fill", color)
 | 
			
		||||
 | 
			
		||||
      // highlight links
 | 
			
		||||
      linkNodes
 | 
			
		||||
        .transition()
 | 
			
		||||
        .duration(200)
 | 
			
		||||
        .attr("stroke", "var(--gray)")
 | 
			
		||||
        .attr("stroke-width", 1)
 | 
			
		||||
 | 
			
		||||
      linkNodes.transition().duration(200).attr("stroke", "var(--gray)").attr("stroke-width", 1)
 | 
			
		||||
 | 
			
		||||
      const bigFont = fontSize * 1.5
 | 
			
		||||
 | 
			
		||||
@@ -199,11 +200,11 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
 | 
			
		||||
        .select("text")
 | 
			
		||||
        .transition()
 | 
			
		||||
        .duration(200)
 | 
			
		||||
        .attr('opacityOld', d3.select(parent).select('text').style("opacity"))
 | 
			
		||||
        .style('opacity', 1)
 | 
			
		||||
        .style('font-size', bigFont + 'em')
 | 
			
		||||
        .attr("opacityOld", d3.select(parent).select("text").style("opacity"))
 | 
			
		||||
        .style("opacity", 1)
 | 
			
		||||
        .style("font-size", bigFont + "em")
 | 
			
		||||
    })
 | 
			
		||||
    .on("mouseleave", function(_, d) {
 | 
			
		||||
    .on("mouseleave", function (_, d) {
 | 
			
		||||
      const currentId = d.id
 | 
			
		||||
      const linkNodes = d3
 | 
			
		||||
        .selectAll(".link")
 | 
			
		||||
@@ -216,8 +217,8 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
 | 
			
		||||
        .select("text")
 | 
			
		||||
        .transition()
 | 
			
		||||
        .duration(200)
 | 
			
		||||
        .style('opacity', d3.select(parent).select('text').attr("opacityOld"))
 | 
			
		||||
        .style('font-size', fontSize + 'em')
 | 
			
		||||
        .style("opacity", d3.select(parent).select("text").attr("opacityOld"))
 | 
			
		||||
        .style("font-size", fontSize + "em")
 | 
			
		||||
    })
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    .call(drag(simulation))
 | 
			
		||||
@@ -228,10 +229,12 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
 | 
			
		||||
    .attr("dx", 0)
 | 
			
		||||
    .attr("dy", (d) => -nodeRadius(d) + "px")
 | 
			
		||||
    .attr("text-anchor", "middle")
 | 
			
		||||
    .text((d) => data[d.id]?.title || (d.id.charAt(1).toUpperCase() + d.id.slice(2)).replace("-", " "))
 | 
			
		||||
    .style('opacity', (opacityScale - 1) / 3.75)
 | 
			
		||||
    .text(
 | 
			
		||||
      (d) => data[d.id]?.title || (d.id.charAt(1).toUpperCase() + d.id.slice(2)).replace("-", " "),
 | 
			
		||||
    )
 | 
			
		||||
    .style("opacity", (opacityScale - 1) / 3.75)
 | 
			
		||||
    .style("pointer-events", "none")
 | 
			
		||||
    .style('font-size', fontSize + 'em')
 | 
			
		||||
    .style("font-size", fontSize + "em")
 | 
			
		||||
    .raise()
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    .call(drag(simulation))
 | 
			
		||||
@@ -249,7 +252,7 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
 | 
			
		||||
        .on("zoom", ({ transform }) => {
 | 
			
		||||
          link.attr("transform", transform)
 | 
			
		||||
          node.attr("transform", transform)
 | 
			
		||||
          const scale = transform.k * opacityScale;
 | 
			
		||||
          const scale = transform.k * opacityScale
 | 
			
		||||
          const scaledOpacity = Math.max((scale - 1) / 3.75, 0)
 | 
			
		||||
          labels.attr("transform", transform).style("opacity", scaledOpacity)
 | 
			
		||||
        }),
 | 
			
		||||
@@ -263,17 +266,13 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
 | 
			
		||||
      .attr("y1", (d: any) => d.source.y)
 | 
			
		||||
      .attr("x2", (d: any) => d.target.x)
 | 
			
		||||
      .attr("y2", (d: any) => d.target.y)
 | 
			
		||||
    node
 | 
			
		||||
      .attr("cx", (d: any) => d.x)
 | 
			
		||||
      .attr("cy", (d: any) => d.y)
 | 
			
		||||
    labels
 | 
			
		||||
      .attr("x", (d: any) => d.x)
 | 
			
		||||
      .attr("y", (d: any) => d.y)
 | 
			
		||||
    node.attr("cx", (d: any) => d.x).attr("cy", (d: any) => d.y)
 | 
			
		||||
    labels.attr("x", (d: any) => d.x).attr("y", (d: any) => d.y)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderGlobalGraph() {
 | 
			
		||||
  const slug = getCanonicalSlug(window) 
 | 
			
		||||
  const slug = getCanonicalSlug(window)
 | 
			
		||||
  const container = document.getElementById("global-graph-outer")
 | 
			
		||||
  const sidebar = container?.closest(".sidebar") as HTMLElement
 | 
			
		||||
  container?.classList.add("active")
 | 
			
		||||
@@ -305,4 +304,3 @@ document.addEventListener("nav", async (e: unknown) => {
 | 
			
		||||
  containerIcon?.removeEventListener("click", renderGlobalGraph)
 | 
			
		||||
  containerIcon?.addEventListener("click", renderGlobalGraph)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,3 @@
 | 
			
		||||
import Plausible from 'plausible-tracker'
 | 
			
		||||
import Plausible from "plausible-tracker"
 | 
			
		||||
const { trackPageview } = Plausible()
 | 
			
		||||
document.addEventListener("nav", () => trackPageview())
 | 
			
		||||
 
 | 
			
		||||
@@ -2,33 +2,25 @@ 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
 | 
			
		||||
) {
 | 
			
		||||
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('[href^="./"], [href^="../"]').forEach((item) => update(item, "href", base))
 | 
			
		||||
 | 
			
		||||
  el.querySelectorAll('[src^="./"], [src^="../"]').forEach((item) =>
 | 
			
		||||
    update(item, 'src', base)
 | 
			
		||||
  )
 | 
			
		||||
  el.querySelectorAll('[src^="./"], [src^="../"]').forEach((item) => update(item, "src", base))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const p = new DOMParser()
 | 
			
		||||
async function mouseEnterHandler(this: HTMLLinkElement, { clientX, clientY }: { clientX: number, clientY: number }) {
 | 
			
		||||
async function mouseEnterHandler(
 | 
			
		||||
  this: HTMLLinkElement,
 | 
			
		||||
  { clientX, clientY }: { clientX: number; clientY: number },
 | 
			
		||||
) {
 | 
			
		||||
  const link = this
 | 
			
		||||
  async function setPosition(popoverElement: HTMLElement) {
 | 
			
		||||
    const { x, y } = await computePosition(link, popoverElement, {
 | 
			
		||||
      middleware: [
 | 
			
		||||
        inline({ x: clientX, y: clientY }),
 | 
			
		||||
        shift(),
 | 
			
		||||
        flip()
 | 
			
		||||
      ]
 | 
			
		||||
      middleware: [inline({ x: clientX, y: clientY }), shift(), flip()],
 | 
			
		||||
    })
 | 
			
		||||
    Object.assign(popoverElement.style, {
 | 
			
		||||
      left: `${x}px`,
 | 
			
		||||
@@ -37,7 +29,7 @@ async function mouseEnterHandler(this: HTMLLinkElement, { clientX, clientY }: {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // dont refetch if there's already a popover
 | 
			
		||||
  if ([...link.children].some(child => child.classList.contains("popover"))) {
 | 
			
		||||
  if ([...link.children].some((child) => child.classList.contains("popover"))) {
 | 
			
		||||
    return setPosition(link.lastChild as HTMLElement)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -68,7 +60,7 @@ async function mouseEnterHandler(this: HTMLLinkElement, { clientX, clientY }: {
 | 
			
		||||
  const popoverInner = document.createElement("div")
 | 
			
		||||
  popoverInner.classList.add("popover-inner")
 | 
			
		||||
  popoverElement.appendChild(popoverInner)
 | 
			
		||||
  elts.forEach(elt => popoverInner.appendChild(elt))
 | 
			
		||||
  elts.forEach((elt) => popoverInner.appendChild(elt))
 | 
			
		||||
 | 
			
		||||
  setPosition(popoverElement)
 | 
			
		||||
  link.appendChild(popoverElement)
 | 
			
		||||
@@ -77,7 +69,7 @@ async function mouseEnterHandler(this: HTMLLinkElement, { clientX, clientY }: {
 | 
			
		||||
    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' })
 | 
			
		||||
      popoverInner.scroll({ top: heading.offsetTop - 12, behavior: "instant" })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,9 +4,9 @@ import { registerEscapeHandler, removeAllChildren } from "./util"
 | 
			
		||||
import { CanonicalSlug, getClientSlug, resolveRelative } from "../../path"
 | 
			
		||||
 | 
			
		||||
interface Item {
 | 
			
		||||
  slug: CanonicalSlug,
 | 
			
		||||
  title: string,
 | 
			
		||||
  content: string,
 | 
			
		||||
  slug: CanonicalSlug
 | 
			
		||||
  title: string
 | 
			
		||||
  content: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let index: Document<Item> | undefined = undefined
 | 
			
		||||
@@ -15,15 +15,17 @@ const contextWindowWords = 30
 | 
			
		||||
const numSearchResults = 5
 | 
			
		||||
function highlight(searchTerm: string, text: string, trim?: boolean) {
 | 
			
		||||
  // try to highlight longest tokens first
 | 
			
		||||
  const tokenizedTerms = searchTerm.split(/\s+/).filter(t => t !== "").sort((a, b) => b.length - a.length)
 | 
			
		||||
  let tokenizedText = text
 | 
			
		||||
  const tokenizedTerms = searchTerm
 | 
			
		||||
    .split(/\s+/)
 | 
			
		||||
    .filter(t => t !== "")
 | 
			
		||||
    .filter((t) => t !== "")
 | 
			
		||||
    .sort((a, b) => b.length - a.length)
 | 
			
		||||
  let tokenizedText = text.split(/\s+/).filter((t) => t !== "")
 | 
			
		||||
 | 
			
		||||
  let startIndex = 0
 | 
			
		||||
  let endIndex = tokenizedText.length - 1
 | 
			
		||||
  if (trim) {
 | 
			
		||||
    const includesCheck = (tok: string) => tokenizedTerms.some((term) => tok.toLowerCase().startsWith(term.toLowerCase()))
 | 
			
		||||
    const includesCheck = (tok: string) =>
 | 
			
		||||
      tokenizedTerms.some((term) => tok.toLowerCase().startsWith(term.toLowerCase()))
 | 
			
		||||
    const occurencesIndices = tokenizedText.map(includesCheck)
 | 
			
		||||
 | 
			
		||||
    let bestSum = 0
 | 
			
		||||
@@ -42,19 +44,22 @@ function highlight(searchTerm: string, text: string, trim?: boolean) {
 | 
			
		||||
    tokenizedText = tokenizedText.slice(startIndex, endIndex)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const slice = tokenizedText.map(tok => {
 | 
			
		||||
    // see if this tok is prefixed by any search terms 
 | 
			
		||||
    for (const searchTok of tokenizedTerms) {
 | 
			
		||||
      if (tok.toLowerCase().includes(searchTok.toLowerCase())) {
 | 
			
		||||
        const regex = new RegExp(searchTok.toLowerCase(), "gi")
 | 
			
		||||
        return tok.replace(regex, `<span class="highlight">$&</span>`)
 | 
			
		||||
  const slice = tokenizedText
 | 
			
		||||
    .map((tok) => {
 | 
			
		||||
      // see if this tok is prefixed by any search terms
 | 
			
		||||
      for (const searchTok of tokenizedTerms) {
 | 
			
		||||
        if (tok.toLowerCase().includes(searchTok.toLowerCase())) {
 | 
			
		||||
          const regex = new RegExp(searchTok.toLowerCase(), "gi")
 | 
			
		||||
          return tok.replace(regex, `<span class="highlight">$&</span>`)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return tok
 | 
			
		||||
  })
 | 
			
		||||
      return tok
 | 
			
		||||
    })
 | 
			
		||||
    .join(" ")
 | 
			
		||||
 | 
			
		||||
  return `${startIndex === 0 ? "" : "..."}${slice}${endIndex === tokenizedText.length - 1 ? "" : "..."}`
 | 
			
		||||
  return `${startIndex === 0 ? "" : "..."}${slice}${
 | 
			
		||||
    endIndex === tokenizedText.length - 1 ? "" : "..."
 | 
			
		||||
  }`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
 | 
			
		||||
@@ -113,7 +118,7 @@ document.addEventListener("nav", async (e: unknown) => {
 | 
			
		||||
    button.classList.add("result-card")
 | 
			
		||||
    button.id = slug
 | 
			
		||||
    button.innerHTML = `<h3>${title}</h3><p>${content}</p>`
 | 
			
		||||
    button.addEventListener('click', () => {
 | 
			
		||||
    button.addEventListener("click", () => {
 | 
			
		||||
      const targ = resolveRelative(currentSlug, slug)
 | 
			
		||||
      window.spaNavigate(new URL(targ, getClientSlug(window)))
 | 
			
		||||
    })
 | 
			
		||||
@@ -132,7 +137,6 @@ document.addEventListener("nav", async (e: unknown) => {
 | 
			
		||||
    } else {
 | 
			
		||||
      results.append(...finalResults.map(resultToHTML))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function onType(e: HTMLElementEventMap["input"]) {
 | 
			
		||||
@@ -140,12 +144,12 @@ document.addEventListener("nav", async (e: unknown) => {
 | 
			
		||||
    const searchResults = index?.search(term, numSearchResults) ?? []
 | 
			
		||||
    const getByField = (field: string): CanonicalSlug[] => {
 | 
			
		||||
      const results = searchResults.filter((x) => x.field === field)
 | 
			
		||||
      return results.length === 0 ? [] : [...results[0].result] as CanonicalSlug[]
 | 
			
		||||
      return results.length === 0 ? [] : ([...results[0].result] as CanonicalSlug[])
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // order titles ahead of content
 | 
			
		||||
    const allIds: Set<CanonicalSlug> = new Set([...getByField("title"), ...getByField("content")])
 | 
			
		||||
    const finalResults = [...allIds].map(id => formatForDisplay(term, id))
 | 
			
		||||
    const finalResults = [...allIds].map((id) => formatForDisplay(term, id))
 | 
			
		||||
    displayResults(finalResults)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -160,7 +164,7 @@ document.addEventListener("nav", async (e: unknown) => {
 | 
			
		||||
  if (!index) {
 | 
			
		||||
    index = new Document({
 | 
			
		||||
      cache: true,
 | 
			
		||||
      charset: 'latin:extra',
 | 
			
		||||
      charset: "latin:extra",
 | 
			
		||||
      optimize: true,
 | 
			
		||||
      encode: encoder,
 | 
			
		||||
      document: {
 | 
			
		||||
@@ -174,7 +178,7 @@ document.addEventListener("nav", async (e: unknown) => {
 | 
			
		||||
            field: "content",
 | 
			
		||||
            tokenize: "reverse",
 | 
			
		||||
          },
 | 
			
		||||
        ]
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
@@ -182,7 +186,7 @@ document.addEventListener("nav", async (e: unknown) => {
 | 
			
		||||
      await index.addAsync(slug, {
 | 
			
		||||
        slug: slug as CanonicalSlug,
 | 
			
		||||
        title: fileData.title,
 | 
			
		||||
        content: fileData.content
 | 
			
		||||
        content: fileData.content,
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,9 @@ import { CanonicalSlug, RelativeURL, getCanonicalSlug } from "../../path"
 | 
			
		||||
// https://github.com/natemoo-re/micromorph
 | 
			
		||||
 | 
			
		||||
const NODE_TYPE_ELEMENT = 1
 | 
			
		||||
let announcer = document.createElement('route-announcer')
 | 
			
		||||
const isElement = (target: EventTarget | null): target is Element => (target as Node)?.nodeType === NODE_TYPE_ELEMENT
 | 
			
		||||
let announcer = document.createElement("route-announcer")
 | 
			
		||||
const isElement = (target: EventTarget | null): target is Element =>
 | 
			
		||||
  (target as Node)?.nodeType === NODE_TYPE_ELEMENT
 | 
			
		||||
const isLocalUrl = (href: string) => {
 | 
			
		||||
  try {
 | 
			
		||||
    const url = new URL(href)
 | 
			
		||||
@@ -16,18 +17,18 @@ const isLocalUrl = (href: string) => {
 | 
			
		||||
      }
 | 
			
		||||
      return true
 | 
			
		||||
    }
 | 
			
		||||
  } catch (e) { }
 | 
			
		||||
  } catch (e) {}
 | 
			
		||||
  return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const getOpts = ({ target }: Event): { url: URL, scroll?: boolean } | undefined => {
 | 
			
		||||
const getOpts = ({ target }: Event): { url: URL; scroll?: boolean } | undefined => {
 | 
			
		||||
  if (!isElement(target)) return
 | 
			
		||||
  const a = target.closest("a")
 | 
			
		||||
  if (!a) return
 | 
			
		||||
  if ('routerIgnore' in a.dataset) return
 | 
			
		||||
  if ("routerIgnore" in a.dataset) return
 | 
			
		||||
  const { href } = a
 | 
			
		||||
  if (!isLocalUrl(href)) return
 | 
			
		||||
  return { url: new URL(href), scroll: 'routerNoscroll' in a.dataset ? false : undefined }
 | 
			
		||||
  return { url: new URL(href), scroll: "routerNoscroll" in a.dataset ? false : undefined }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function notifyNav(url: CanonicalSlug) {
 | 
			
		||||
@@ -44,7 +45,7 @@ async function navigate(url: URL, isBack: boolean = false) {
 | 
			
		||||
      window.location.assign(url)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
  if (!contents) return;
 | 
			
		||||
  if (!contents) return
 | 
			
		||||
  if (!isBack) {
 | 
			
		||||
    history.pushState({}, "", url)
 | 
			
		||||
    window.scrollTo({ top: 0 })
 | 
			
		||||
@@ -54,22 +55,22 @@ async function navigate(url: URL, isBack: boolean = false) {
 | 
			
		||||
  if (title) {
 | 
			
		||||
    document.title = title
 | 
			
		||||
  } else {
 | 
			
		||||
    const h1 = document.querySelector('h1')
 | 
			
		||||
    const h1 = document.querySelector("h1")
 | 
			
		||||
    title = h1?.innerText ?? h1?.textContent ?? url.pathname
 | 
			
		||||
  }
 | 
			
		||||
  if (announcer.textContent !== title) {
 | 
			
		||||
    announcer.textContent = title
 | 
			
		||||
  }
 | 
			
		||||
  announcer.dataset.persist = ''
 | 
			
		||||
  announcer.dataset.persist = ""
 | 
			
		||||
  html.body.appendChild(announcer)
 | 
			
		||||
 | 
			
		||||
  micromorph(document.body, html.body)
 | 
			
		||||
 | 
			
		||||
  // now, patch head 
 | 
			
		||||
  const elementsToRemove = document.head.querySelectorAll(':not([spa-preserve])')
 | 
			
		||||
  elementsToRemove.forEach(el => el.remove())
 | 
			
		||||
  const elementsToAdd = html.head.querySelectorAll(':not([spa-preserve])')
 | 
			
		||||
  elementsToAdd.forEach(el => document.head.appendChild(el))
 | 
			
		||||
  // now, patch head
 | 
			
		||||
  const elementsToRemove = document.head.querySelectorAll(":not([spa-preserve])")
 | 
			
		||||
  elementsToRemove.forEach((el) => el.remove())
 | 
			
		||||
  const elementsToAdd = html.head.querySelectorAll(":not([spa-preserve])")
 | 
			
		||||
  elementsToAdd.forEach((el) => document.head.appendChild(el))
 | 
			
		||||
 | 
			
		||||
  notifyNav(getCanonicalSlug(window))
 | 
			
		||||
  delete announcer.dataset.persist
 | 
			
		||||
@@ -101,7 +102,7 @@ function createRouter() {
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return new class Router {
 | 
			
		||||
  return new (class Router {
 | 
			
		||||
    go(pathname: RelativeURL) {
 | 
			
		||||
      const url = new URL(pathname, window.location.toString())
 | 
			
		||||
      return navigate(url, false)
 | 
			
		||||
@@ -114,26 +115,30 @@ function createRouter() {
 | 
			
		||||
    forward() {
 | 
			
		||||
      return window.history.forward()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  })()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
createRouter()
 | 
			
		||||
notifyNav(getCanonicalSlug(window))
 | 
			
		||||
 | 
			
		||||
if (!customElements.get('route-announcer')) {
 | 
			
		||||
if (!customElements.get("route-announcer")) {
 | 
			
		||||
  const attrs = {
 | 
			
		||||
    'aria-live': 'assertive',
 | 
			
		||||
    'aria-atomic': 'true',
 | 
			
		||||
    'style': 'position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px'
 | 
			
		||||
    "aria-live": "assertive",
 | 
			
		||||
    "aria-atomic": "true",
 | 
			
		||||
    style:
 | 
			
		||||
      "position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px",
 | 
			
		||||
  }
 | 
			
		||||
  customElements.define('route-announcer', class RouteAnnouncer extends HTMLElement {
 | 
			
		||||
    constructor() {
 | 
			
		||||
      super()
 | 
			
		||||
    }
 | 
			
		||||
    connectedCallback() {
 | 
			
		||||
      for (const [key, value] of Object.entries(attrs)) {
 | 
			
		||||
        this.setAttribute(key, value)
 | 
			
		||||
  customElements.define(
 | 
			
		||||
    "route-announcer",
 | 
			
		||||
    class RouteAnnouncer extends HTMLElement {
 | 
			
		||||
      constructor() {
 | 
			
		||||
        super()
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
      connectedCallback() {
 | 
			
		||||
        for (const [key, value] of Object.entries(attrs)) {
 | 
			
		||||
          this.setAttribute(key, value)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
const bufferPx = 150
 | 
			
		||||
const observer = new IntersectionObserver(entries => {
 | 
			
		||||
const observer = new IntersectionObserver((entries) => {
 | 
			
		||||
  for (const entry of entries) {
 | 
			
		||||
    const slug = entry.target.id
 | 
			
		||||
    const tocEntryElement = document.querySelector(`a[data-for="${slug}"]`)
 | 
			
		||||
@@ -38,5 +38,5 @@ document.addEventListener("nav", () => {
 | 
			
		||||
  // update toc entry highlighting
 | 
			
		||||
  observer.disconnect()
 | 
			
		||||
  const headers = document.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]")
 | 
			
		||||
  headers.forEach(header => observer.observe(header))
 | 
			
		||||
  headers.forEach((header) => observer.observe(header))
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb:
 | 
			
		||||
  outsideContainer?.removeEventListener("click", click)
 | 
			
		||||
  outsideContainer?.addEventListener("click", click)
 | 
			
		||||
  document.removeEventListener("keydown", esc)
 | 
			
		||||
  document.addEventListener('keydown', esc)
 | 
			
		||||
  document.addEventListener("keydown", esc)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function removeAllChildren(node: HTMLElement) {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
.graph {
 | 
			
		||||
  & > h3 {
 | 
			
		||||
    font-size: 1rem;
 | 
			
		||||
    margin: 0
 | 
			
		||||
    margin: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  & > .graph-outer {
 | 
			
		||||
@@ -26,7 +26,7 @@
 | 
			
		||||
      top: 0;
 | 
			
		||||
      right: 0;
 | 
			
		||||
      border-radius: 4px;
 | 
			
		||||
      background-color: transparent; 
 | 
			
		||||
      background-color: transparent;
 | 
			
		||||
      transition: background-color 0.5s ease;
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
      &:hover {
 | 
			
		||||
@@ -52,7 +52,7 @@
 | 
			
		||||
 | 
			
		||||
    & > #global-graph-container {
 | 
			
		||||
      border: 1px solid var(--lightgray);
 | 
			
		||||
      background-color: var(--light); 
 | 
			
		||||
      background-color: var(--light);
 | 
			
		||||
      border-radius: 5px;
 | 
			
		||||
      box-sizing: border-box;
 | 
			
		||||
      position: fixed;
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ details#toc {
 | 
			
		||||
      margin: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
  & ul {
 | 
			
		||||
    list-style: none;
 | 
			
		||||
    margin: 0.5rem 1.25rem;
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ li.section-li {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    & > .desc > h3 > a {
 | 
			
		||||
      background-color: transparent; 
 | 
			
		||||
      background-color: transparent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    & > .meta {
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,7 @@
 | 
			
		||||
    border: 1px solid var(--lightgray);
 | 
			
		||||
    background-color: var(--light);
 | 
			
		||||
    border-radius: 5px;
 | 
			
		||||
    box-shadow: 6px 6px 36px 0 rgba(0,0,0,0.25);
 | 
			
		||||
    box-shadow: 6px 6px 36px 0 rgba(0, 0, 0, 0.25);
 | 
			
		||||
    overflow: auto;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -42,14 +42,17 @@
 | 
			
		||||
 | 
			
		||||
  visibility: hidden;
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
  transition: opacity 0.3s ease, visibility 0.3s ease;
 | 
			
		||||
  transition:
 | 
			
		||||
    opacity 0.3s ease,
 | 
			
		||||
    visibility 0.3s ease;
 | 
			
		||||
 | 
			
		||||
  @media all and (max-width: $mobileBreakpoint) {
 | 
			
		||||
    display: none !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a:hover .popover, .popover:hover {
 | 
			
		||||
a:hover .popover,
 | 
			
		||||
.popover:hover {
 | 
			
		||||
  animation: dropin 0.3s ease;
 | 
			
		||||
  animation-fill-mode: forwards;
 | 
			
		||||
  animation-delay: 0.2s;
 | 
			
		||||
 
 | 
			
		||||
@@ -67,7 +67,9 @@
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        border-radius: 5px;
 | 
			
		||||
        background: var(--light);
 | 
			
		||||
        box-shadow: 0 14px 50px rgba(27, 33, 48, 0.12), 0 10px 30px rgba(27, 33, 48, 0.16);
 | 
			
		||||
        box-shadow:
 | 
			
		||||
          0 14px 50px rgba(27, 33, 48, 0.12),
 | 
			
		||||
          0 10px 30px rgba(27, 33, 48, 0.16);
 | 
			
		||||
        margin-bottom: 2em;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@@ -108,7 +110,8 @@
 | 
			
		||||
            font-weight: 700;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          &:hover, &:focus {
 | 
			
		||||
          &:hover,
 | 
			
		||||
          &:focus {
 | 
			
		||||
            background: var(--lightgray);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
@@ -127,12 +130,11 @@
 | 
			
		||||
            margin: 0;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          & > p { 
 | 
			
		||||
          & > p {
 | 
			
		||||
            margin-bottom: 0;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,16 +15,16 @@ button#toc {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  & .fold {
 | 
			
		||||
    margin-left: 0.5rem; 
 | 
			
		||||
    margin-left: 0.5rem;
 | 
			
		||||
    transition: transform 0.3s ease;
 | 
			
		||||
    opacity: 0.8;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.collapsed .fold {
 | 
			
		||||
    transform: rotateZ(-90deg)
 | 
			
		||||
    transform: rotateZ(-90deg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
#toc-content {
 | 
			
		||||
  list-style: none;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
@@ -42,7 +42,9 @@ button#toc {
 | 
			
		||||
    & > li > a {
 | 
			
		||||
      color: var(--dark);
 | 
			
		||||
      opacity: 0.35;
 | 
			
		||||
      transition: 0.5s ease opacity, 0.3s ease color;
 | 
			
		||||
      transition:
 | 
			
		||||
        0.5s ease opacity,
 | 
			
		||||
        0.3s ease color;
 | 
			
		||||
      &.in-view {
 | 
			
		||||
        opacity: 0.75;
 | 
			
		||||
      }
 | 
			
		||||
@@ -55,4 +57,3 @@ button#toc {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
  
 | 
			
		||||
 
 | 
			
		||||
@@ -11,15 +11,17 @@ export type QuartzComponentProps = {
 | 
			
		||||
  children: (QuartzComponent | JSX.Element)[]
 | 
			
		||||
  tree: Node<QuartzPluginData>
 | 
			
		||||
  allFiles: QuartzPluginData[]
 | 
			
		||||
  displayClass?: 'mobile-only' | 'desktop-only'
 | 
			
		||||
  displayClass?: "mobile-only" | "desktop-only"
 | 
			
		||||
} & JSX.IntrinsicAttributes & {
 | 
			
		||||
  [key: string]: any
 | 
			
		||||
}
 | 
			
		||||
    [key: string]: any
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
export type QuartzComponent = ComponentType<QuartzComponentProps> & {
 | 
			
		||||
  css?: string,
 | 
			
		||||
  beforeDOMLoaded?: string,
 | 
			
		||||
  afterDOMLoaded?: string,
 | 
			
		||||
  css?: string
 | 
			
		||||
  beforeDOMLoaded?: string
 | 
			
		||||
  afterDOMLoaded?: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type QuartzComponentConstructor<Options extends object | undefined = undefined> = (opts: Options) => QuartzComponent
 | 
			
		||||
export type QuartzComponentConstructor<Options extends object | undefined = undefined> = (
 | 
			
		||||
  opts: Options,
 | 
			
		||||
) => QuartzComponent
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { Spinner } from 'cli-spinner'
 | 
			
		||||
import { Spinner } from "cli-spinner"
 | 
			
		||||
 | 
			
		||||
export class QuartzLogger {
 | 
			
		||||
  verbose: boolean
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import test, { describe } from 'node:test'
 | 
			
		||||
import * as path from './path'
 | 
			
		||||
import assert from 'node:assert'
 | 
			
		||||
import test, { describe } from "node:test"
 | 
			
		||||
import * as path from "./path"
 | 
			
		||||
import assert from "node:assert"
 | 
			
		||||
 | 
			
		||||
describe('typeguards', () => {
 | 
			
		||||
  test('isClientSlug', () => {
 | 
			
		||||
describe("typeguards", () => {
 | 
			
		||||
  test("isClientSlug", () => {
 | 
			
		||||
    assert(path.isClientSlug("http://example.com"))
 | 
			
		||||
    assert(path.isClientSlug("http://example.com/index"))
 | 
			
		||||
    assert(path.isClientSlug("http://example.com/index.html"))
 | 
			
		||||
@@ -23,7 +23,7 @@ describe('typeguards', () => {
 | 
			
		||||
    assert(!path.isClientSlug("https"))
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('isCanonicalSlug', () => {
 | 
			
		||||
  test("isCanonicalSlug", () => {
 | 
			
		||||
    assert(path.isCanonicalSlug(""))
 | 
			
		||||
    assert(path.isCanonicalSlug("abc"))
 | 
			
		||||
    assert(path.isCanonicalSlug("notindex"))
 | 
			
		||||
@@ -41,7 +41,7 @@ describe('typeguards', () => {
 | 
			
		||||
    assert(!path.isCanonicalSlug("index.html"))
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('isRelativeURL', () => {
 | 
			
		||||
  test("isRelativeURL", () => {
 | 
			
		||||
    assert(path.isRelativeURL("."))
 | 
			
		||||
    assert(path.isRelativeURL(".."))
 | 
			
		||||
    assert(path.isRelativeURL("./abc/def"))
 | 
			
		||||
@@ -58,7 +58,7 @@ describe('typeguards', () => {
 | 
			
		||||
    assert(!path.isRelativeURL("./abc/def.md"))
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('isServerSlug', () => {
 | 
			
		||||
  test("isServerSlug", () => {
 | 
			
		||||
    assert(path.isServerSlug("index"))
 | 
			
		||||
    assert(path.isServerSlug("abc/def"))
 | 
			
		||||
 | 
			
		||||
@@ -72,7 +72,7 @@ describe('typeguards', () => {
 | 
			
		||||
    assert(!path.isServerSlug("note with spaces"))
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('isFilePath', () => {
 | 
			
		||||
  test("isFilePath", () => {
 | 
			
		||||
    assert(path.isFilePath("content/index.md"))
 | 
			
		||||
    assert(path.isFilePath("content/test.png"))
 | 
			
		||||
    assert(!path.isFilePath("../test.pdf"))
 | 
			
		||||
@@ -81,80 +81,112 @@ describe('typeguards', () => {
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
describe('transforms', () => {
 | 
			
		||||
  function asserts<Inp, Out>(pairs: [string, string][], transform: (inp: Inp) => Out, checkPre: (x: any) => x is Inp, checkPost: (x: any) => x is Out) {
 | 
			
		||||
describe("transforms", () => {
 | 
			
		||||
  function asserts<Inp, Out>(
 | 
			
		||||
    pairs: [string, string][],
 | 
			
		||||
    transform: (inp: Inp) => Out,
 | 
			
		||||
    checkPre: (x: any) => x is Inp,
 | 
			
		||||
    checkPost: (x: any) => x is Out,
 | 
			
		||||
  ) {
 | 
			
		||||
    for (const [inp, expected] of pairs) {
 | 
			
		||||
      assert(checkPre(inp), `${inp} wasn't the expected input type`)
 | 
			
		||||
      const actual = transform(inp)
 | 
			
		||||
      assert.strictEqual(actual, expected, `after transforming ${inp}, '${actual}' was not '${expected}'`)
 | 
			
		||||
      assert.strictEqual(
 | 
			
		||||
        actual,
 | 
			
		||||
        expected,
 | 
			
		||||
        `after transforming ${inp}, '${actual}' was not '${expected}'`,
 | 
			
		||||
      )
 | 
			
		||||
      assert(checkPost(actual), `${actual} wasn't the expected output type`)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  test('canonicalizeServer', () => {
 | 
			
		||||
    asserts([
 | 
			
		||||
      ["index", ""],
 | 
			
		||||
      ["abc/index", "abc"],
 | 
			
		||||
      ["abc/def", "abc/def"],
 | 
			
		||||
    ], path.canonicalizeServer, path.isServerSlug, path.isCanonicalSlug)
 | 
			
		||||
  test("canonicalizeServer", () => {
 | 
			
		||||
    asserts(
 | 
			
		||||
      [
 | 
			
		||||
        ["index", ""],
 | 
			
		||||
        ["abc/index", "abc"],
 | 
			
		||||
        ["abc/def", "abc/def"],
 | 
			
		||||
      ],
 | 
			
		||||
      path.canonicalizeServer,
 | 
			
		||||
      path.isServerSlug,
 | 
			
		||||
      path.isCanonicalSlug,
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('canonicalizeClient', () => {
 | 
			
		||||
    asserts([
 | 
			
		||||
      ["http://localhost:3000", ""],
 | 
			
		||||
      ["http://localhost:3000/index", ""],
 | 
			
		||||
      ["http://localhost:3000/test", "test"],
 | 
			
		||||
      ["http://example.com", ""],
 | 
			
		||||
      ["http://example.com/index", ""],
 | 
			
		||||
      ["http://example.com/index.html", ""],
 | 
			
		||||
      ["http://example.com/", ""],
 | 
			
		||||
      ["https://example.com", ""],
 | 
			
		||||
      ["https://example.com/abc/def", "abc/def"],
 | 
			
		||||
      ["https://example.com/abc/def/", "abc/def"],
 | 
			
		||||
      ["https://example.com/abc/def#cool", "abc/def"],
 | 
			
		||||
      ["https://example.com/abc/def?field=1&another=2", "abc/def"],
 | 
			
		||||
      ["https://example.com/abc/def?field=1&another=2#cool", "abc/def"],
 | 
			
		||||
      ["https://example.com/abc/def.html?field=1&another=2#cool", "abc/def"],
 | 
			
		||||
    ], path.canonicalizeClient, path.isClientSlug, path.isCanonicalSlug)
 | 
			
		||||
  test("canonicalizeClient", () => {
 | 
			
		||||
    asserts(
 | 
			
		||||
      [
 | 
			
		||||
        ["http://localhost:3000", ""],
 | 
			
		||||
        ["http://localhost:3000/index", ""],
 | 
			
		||||
        ["http://localhost:3000/test", "test"],
 | 
			
		||||
        ["http://example.com", ""],
 | 
			
		||||
        ["http://example.com/index", ""],
 | 
			
		||||
        ["http://example.com/index.html", ""],
 | 
			
		||||
        ["http://example.com/", ""],
 | 
			
		||||
        ["https://example.com", ""],
 | 
			
		||||
        ["https://example.com/abc/def", "abc/def"],
 | 
			
		||||
        ["https://example.com/abc/def/", "abc/def"],
 | 
			
		||||
        ["https://example.com/abc/def#cool", "abc/def"],
 | 
			
		||||
        ["https://example.com/abc/def?field=1&another=2", "abc/def"],
 | 
			
		||||
        ["https://example.com/abc/def?field=1&another=2#cool", "abc/def"],
 | 
			
		||||
        ["https://example.com/abc/def.html?field=1&another=2#cool", "abc/def"],
 | 
			
		||||
      ],
 | 
			
		||||
      path.canonicalizeClient,
 | 
			
		||||
      path.isClientSlug,
 | 
			
		||||
      path.isCanonicalSlug,
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('slugifyFilePath', () => {
 | 
			
		||||
    asserts([
 | 
			
		||||
      ["content/index.md", "content/index"],
 | 
			
		||||
      ["content/_index.md", "content/index"],
 | 
			
		||||
      ["/content/index.md", "content/index"],
 | 
			
		||||
      ["content/cool.png", "content/cool"],
 | 
			
		||||
      ["index.md", "index"],
 | 
			
		||||
      ["note with spaces.md", "note-with-spaces"],
 | 
			
		||||
    ], path.slugifyFilePath, path.isFilePath, path.isServerSlug)
 | 
			
		||||
  describe("slugifyFilePath", () => {
 | 
			
		||||
    asserts(
 | 
			
		||||
      [
 | 
			
		||||
        ["content/index.md", "content/index"],
 | 
			
		||||
        ["content/_index.md", "content/index"],
 | 
			
		||||
        ["/content/index.md", "content/index"],
 | 
			
		||||
        ["content/cool.png", "content/cool"],
 | 
			
		||||
        ["index.md", "index"],
 | 
			
		||||
        ["note with spaces.md", "note-with-spaces"],
 | 
			
		||||
      ],
 | 
			
		||||
      path.slugifyFilePath,
 | 
			
		||||
      path.isFilePath,
 | 
			
		||||
      path.isServerSlug,
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('transformInternalLink', () => {
 | 
			
		||||
    asserts([
 | 
			
		||||
      ["", "."],
 | 
			
		||||
      [".", "."],
 | 
			
		||||
      ["./", "."],
 | 
			
		||||
      ["./index", "."],
 | 
			
		||||
      ["./index.html", "."],
 | 
			
		||||
      ["./index.md", "."],
 | 
			
		||||
      ["content", "./content"],
 | 
			
		||||
      ["content/test.md", "./content/test"],
 | 
			
		||||
      ["./content/test.md", "./content/test"],
 | 
			
		||||
      ["../content/test.md", "../content/test"],
 | 
			
		||||
      ["tags/", "./tags"],
 | 
			
		||||
      ["/tags/", "./tags"],
 | 
			
		||||
      ["content/with spaces", "./content/with-spaces"],
 | 
			
		||||
      ["content/with spaces#and Anchor!", "./content/with-spaces#and-anchor"],
 | 
			
		||||
    ], path.transformInternalLink, (_x: string): _x is string => true, path.isRelativeURL)
 | 
			
		||||
  describe("transformInternalLink", () => {
 | 
			
		||||
    asserts(
 | 
			
		||||
      [
 | 
			
		||||
        ["", "."],
 | 
			
		||||
        [".", "."],
 | 
			
		||||
        ["./", "."],
 | 
			
		||||
        ["./index", "."],
 | 
			
		||||
        ["./index.html", "."],
 | 
			
		||||
        ["./index.md", "."],
 | 
			
		||||
        ["content", "./content"],
 | 
			
		||||
        ["content/test.md", "./content/test"],
 | 
			
		||||
        ["./content/test.md", "./content/test"],
 | 
			
		||||
        ["../content/test.md", "../content/test"],
 | 
			
		||||
        ["tags/", "./tags"],
 | 
			
		||||
        ["/tags/", "./tags"],
 | 
			
		||||
        ["content/with spaces", "./content/with-spaces"],
 | 
			
		||||
        ["content/with spaces#and Anchor!", "./content/with-spaces#and-anchor"],
 | 
			
		||||
      ],
 | 
			
		||||
      path.transformInternalLink,
 | 
			
		||||
      (_x: string): _x is string => true,
 | 
			
		||||
      path.isRelativeURL,
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('pathToRoot', () => {
 | 
			
		||||
    asserts([
 | 
			
		||||
      ["", "."],
 | 
			
		||||
      ["abc", ".."],
 | 
			
		||||
      ["abc/def", "../.."],
 | 
			
		||||
    ], path.pathToRoot, path.isCanonicalSlug, path.isRelativeURL)
 | 
			
		||||
  describe("pathToRoot", () => {
 | 
			
		||||
    asserts(
 | 
			
		||||
      [
 | 
			
		||||
        ["", "."],
 | 
			
		||||
        ["abc", ".."],
 | 
			
		||||
        ["abc/def", "../.."],
 | 
			
		||||
      ],
 | 
			
		||||
      path.pathToRoot,
 | 
			
		||||
      path.isCanonicalSlug,
 | 
			
		||||
      path.isRelativeURL,
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { slug as slugAnchor } from 'github-slugger'
 | 
			
		||||
import { trace } from './trace'
 | 
			
		||||
import { slug as slugAnchor } from "github-slugger"
 | 
			
		||||
import { trace } from "./trace"
 | 
			
		||||
 | 
			
		||||
// Quartz Paths
 | 
			
		||||
// Things in boxes are not actual types but rather sources which these types can be acquired from
 | 
			
		||||
@@ -46,7 +46,7 @@ import { trace } from './trace'
 | 
			
		||||
const STRICT_TYPE_CHECKS = false
 | 
			
		||||
const HARD_EXIT_ON_FAIL = false
 | 
			
		||||
 | 
			
		||||
function conditionCheck<T>(name: string, label: 'pre' | 'post', s: T, chk: (x: any) => x is T) {
 | 
			
		||||
function conditionCheck<T>(name: string, label: "pre" | "post", s: T, chk: (x: any) => x is T) {
 | 
			
		||||
  if (STRICT_TYPE_CHECKS && !chk(s)) {
 | 
			
		||||
    trace(`${name} failed ${label}-condition check: ${s} does not pass ${chk.name}`, new Error())
 | 
			
		||||
    if (HARD_EXIT_ON_FAIL) {
 | 
			
		||||
@@ -66,8 +66,8 @@ export function isClientSlug(s: string): s is ClientSlug {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Canonical slug, should be used whenever you need to refer to the location of a file/note.
 | 
			
		||||
  * On the client, this is normally stored in `document.body.dataset.slug`
 | 
			
		||||
  */
 | 
			
		||||
 * On the client, this is normally stored in `document.body.dataset.slug`
 | 
			
		||||
 */
 | 
			
		||||
export type CanonicalSlug = SlugLike<"canonical">
 | 
			
		||||
export function isCanonicalSlug(s: string): s is CanonicalSlug {
 | 
			
		||||
  const validStart = !(s.startsWith(".") || s.startsWith("/"))
 | 
			
		||||
@@ -76,8 +76,8 @@ export function isCanonicalSlug(s: string): s is CanonicalSlug {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** A relative link, can be found on `href`s but can also be constructed for
 | 
			
		||||
  * client-side navigation (e.g. search and graph)
 | 
			
		||||
  */
 | 
			
		||||
 * client-side navigation (e.g. search and graph)
 | 
			
		||||
 */
 | 
			
		||||
export type RelativeURL = SlugLike<"relative">
 | 
			
		||||
export function isRelativeURL(s: string): s is RelativeURL {
 | 
			
		||||
  const validStart = /^\.{1,2}/.test(s)
 | 
			
		||||
@@ -102,58 +102,58 @@ export function isFilePath(s: string): s is FilePath {
 | 
			
		||||
 | 
			
		||||
export function getClientSlug(window: Window): ClientSlug {
 | 
			
		||||
  const res = window.location.href as ClientSlug
 | 
			
		||||
  conditionCheck(getClientSlug.name, 'post', res, isClientSlug)
 | 
			
		||||
  conditionCheck(getClientSlug.name, "post", res, isClientSlug)
 | 
			
		||||
  return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getCanonicalSlug(window: Window): CanonicalSlug {
 | 
			
		||||
  const res = window.document.body.dataset.slug! as CanonicalSlug
 | 
			
		||||
  conditionCheck(getCanonicalSlug.name, 'post', res, isCanonicalSlug)
 | 
			
		||||
  conditionCheck(getCanonicalSlug.name, "post", res, isCanonicalSlug)
 | 
			
		||||
  return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function canonicalizeClient(slug: ClientSlug): CanonicalSlug {
 | 
			
		||||
  conditionCheck(canonicalizeClient.name, 'pre', slug, isClientSlug)
 | 
			
		||||
  conditionCheck(canonicalizeClient.name, "pre", slug, isClientSlug)
 | 
			
		||||
  const { pathname } = new URL(slug)
 | 
			
		||||
  let fp = pathname.slice(1)
 | 
			
		||||
  fp = fp.replace(new RegExp(_getFileExtension(fp) + '$'), '')
 | 
			
		||||
  fp = fp.replace(new RegExp(_getFileExtension(fp) + "$"), "")
 | 
			
		||||
  const res = _canonicalize(fp) as CanonicalSlug
 | 
			
		||||
  conditionCheck(canonicalizeClient.name, 'post', res, isCanonicalSlug)
 | 
			
		||||
  conditionCheck(canonicalizeClient.name, "post", res, isCanonicalSlug)
 | 
			
		||||
  return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function canonicalizeServer(slug: ServerSlug): CanonicalSlug {
 | 
			
		||||
  conditionCheck(canonicalizeServer.name, 'pre', slug, isServerSlug)
 | 
			
		||||
  conditionCheck(canonicalizeServer.name, "pre", slug, isServerSlug)
 | 
			
		||||
  let fp = slug as string
 | 
			
		||||
  const res = _canonicalize(fp) as CanonicalSlug
 | 
			
		||||
  conditionCheck(canonicalizeServer.name, 'post', res, isCanonicalSlug)
 | 
			
		||||
  conditionCheck(canonicalizeServer.name, "post", res, isCanonicalSlug)
 | 
			
		||||
  return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function slugifyFilePath(fp: FilePath): ServerSlug {
 | 
			
		||||
  conditionCheck(slugifyFilePath.name, 'pre', fp, isFilePath)
 | 
			
		||||
  conditionCheck(slugifyFilePath.name, "pre", fp, isFilePath)
 | 
			
		||||
  fp = _stripSlashes(fp) as FilePath
 | 
			
		||||
  const withoutFileExt = fp.replace(new RegExp(_getFileExtension(fp) + '$'), '')
 | 
			
		||||
  const withoutFileExt = fp.replace(new RegExp(_getFileExtension(fp) + "$"), "")
 | 
			
		||||
  let slug = withoutFileExt
 | 
			
		||||
    .split('/')
 | 
			
		||||
    .map((segment) => segment.replace(/\s/g, '-')) // slugify all segments
 | 
			
		||||
    .join('/') // always use / as sep
 | 
			
		||||
    .replace(/\/$/, '') // remove trailing slash
 | 
			
		||||
    .split("/")
 | 
			
		||||
    .map((segment) => segment.replace(/\s/g, "-")) // slugify all segments
 | 
			
		||||
    .join("/") // always use / as sep
 | 
			
		||||
    .replace(/\/$/, "") // remove trailing slash
 | 
			
		||||
 | 
			
		||||
  // treat _index as index
 | 
			
		||||
  if (_endsWith(slug, "_index")) {
 | 
			
		||||
    slug = slug.replace(/_index$/, "index")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  conditionCheck(slugifyFilePath.name, 'post', slug, isServerSlug)
 | 
			
		||||
  conditionCheck(slugifyFilePath.name, "post", slug, isServerSlug)
 | 
			
		||||
  return slug as ServerSlug
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function transformInternalLink(link: string): RelativeURL {
 | 
			
		||||
  let [fplike, anchor] = splitAnchor(decodeURI(link))
 | 
			
		||||
  let segments = fplike.split("/").filter(x => x.length > 0)
 | 
			
		||||
  let segments = fplike.split("/").filter((x) => x.length > 0)
 | 
			
		||||
  let prefix = segments.filter(_isRelativeSegment).join("/")
 | 
			
		||||
  let fp = segments.filter(seg => !_isRelativeSegment(seg)).join("/")
 | 
			
		||||
  let fp = segments.filter((seg) => !_isRelativeSegment(seg)).join("/")
 | 
			
		||||
 | 
			
		||||
  // implicit markdown
 | 
			
		||||
  if (!_hasFileExtension(fp)) {
 | 
			
		||||
@@ -164,57 +164,57 @@ export function transformInternalLink(link: string): RelativeURL {
 | 
			
		||||
  fp = _trimSuffix(fp, "index")
 | 
			
		||||
 | 
			
		||||
  let joined = joinSegments(_stripSlashes(prefix), _stripSlashes(fp))
 | 
			
		||||
  const res = _addRelativeToStart(joined) + anchor as RelativeURL
 | 
			
		||||
  conditionCheck(transformInternalLink.name, 'post', res, isRelativeURL)
 | 
			
		||||
  const res = (_addRelativeToStart(joined) + anchor) as RelativeURL
 | 
			
		||||
  conditionCheck(transformInternalLink.name, "post", res, isRelativeURL)
 | 
			
		||||
  return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// resolve /a/b/c to ../../
 | 
			
		||||
export function pathToRoot(slug: CanonicalSlug): RelativeURL {
 | 
			
		||||
  conditionCheck(pathToRoot.name, 'pre', slug, isCanonicalSlug)
 | 
			
		||||
  conditionCheck(pathToRoot.name, "pre", slug, isCanonicalSlug)
 | 
			
		||||
  let rootPath = slug
 | 
			
		||||
    .split('/')
 | 
			
		||||
    .filter(x => x !== '')
 | 
			
		||||
    .map(_ => '..')
 | 
			
		||||
    .join('/')
 | 
			
		||||
    .split("/")
 | 
			
		||||
    .filter((x) => x !== "")
 | 
			
		||||
    .map((_) => "..")
 | 
			
		||||
    .join("/")
 | 
			
		||||
 | 
			
		||||
  const res = _addRelativeToStart(rootPath) as RelativeURL
 | 
			
		||||
  conditionCheck(pathToRoot.name, 'post', res, isRelativeURL)
 | 
			
		||||
  conditionCheck(pathToRoot.name, "post", res, isRelativeURL)
 | 
			
		||||
  return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function resolveRelative(current: CanonicalSlug, target: CanonicalSlug): RelativeURL {
 | 
			
		||||
  conditionCheck(resolveRelative.name, 'pre', current, isCanonicalSlug)
 | 
			
		||||
  conditionCheck(resolveRelative.name, 'pre', target, isCanonicalSlug)
 | 
			
		||||
  conditionCheck(resolveRelative.name, "pre", current, isCanonicalSlug)
 | 
			
		||||
  conditionCheck(resolveRelative.name, "pre", target, isCanonicalSlug)
 | 
			
		||||
  const res = joinSegments(pathToRoot(current), target) as RelativeURL
 | 
			
		||||
  conditionCheck(resolveRelative.name, 'post', res, isRelativeURL)
 | 
			
		||||
  conditionCheck(resolveRelative.name, "post", res, isRelativeURL)
 | 
			
		||||
  return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function splitAnchor(link: string): [string, string] {
 | 
			
		||||
  let [fp, anchor] = link.split("#", 2)
 | 
			
		||||
  anchor = anchor === undefined ? "" : '#' + slugAnchor(anchor)
 | 
			
		||||
  anchor = anchor === undefined ? "" : "#" + slugAnchor(anchor)
 | 
			
		||||
  return [fp, anchor]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function joinSegments(...args: string[]): string {
 | 
			
		||||
  return args.filter(segment => segment !== "").join('/')
 | 
			
		||||
  return args.filter((segment) => segment !== "").join("/")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const QUARTZ = "quartz"
 | 
			
		||||
 | 
			
		||||
function _canonicalize(fp: string): string {
 | 
			
		||||
  fp = _trimSuffix(fp, "index")
 | 
			
		||||
  return _stripSlashes(fp) 
 | 
			
		||||
  return _stripSlashes(fp)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _endsWith(s: string, suffix: string): boolean {
 | 
			
		||||
  return s === suffix || s.endsWith("/" + suffix) 
 | 
			
		||||
  return s === suffix || s.endsWith("/" + suffix)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _trimSuffix(s: string, suffix: string): string {
 | 
			
		||||
  if (_endsWith(s, suffix)) {
 | 
			
		||||
    s = s.slice(0, -(suffix.length))
 | 
			
		||||
    s = s.slice(0, -suffix.length)
 | 
			
		||||
  }
 | 
			
		||||
  return s
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
import chalk from 'chalk'
 | 
			
		||||
import pretty from 'pretty-time'
 | 
			
		||||
import chalk from "chalk"
 | 
			
		||||
import pretty from "pretty-time"
 | 
			
		||||
 | 
			
		||||
export class PerfTimer {
 | 
			
		||||
  evts: { [key: string]: [number, number] }
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.evts = {}
 | 
			
		||||
    this.addEvent('start')
 | 
			
		||||
    this.addEvent("start")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  addEvent(evtName: string) {
 | 
			
		||||
@@ -14,6 +14,6 @@ export class PerfTimer {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  timeSince(evtName?: string): string {
 | 
			
		||||
    return chalk.yellow(pretty(process.hrtime(this.evts[evtName ?? 'start'])))
 | 
			
		||||
    return chalk.yellow(pretty(process.hrtime(this.evts[evtName ?? "start"])))
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,12 @@
 | 
			
		||||
import { CanonicalSlug, FilePath, ServerSlug, canonicalizeServer, resolveRelative } from "../../path"
 | 
			
		||||
import {
 | 
			
		||||
  CanonicalSlug,
 | 
			
		||||
  FilePath,
 | 
			
		||||
  ServerSlug,
 | 
			
		||||
  canonicalizeServer,
 | 
			
		||||
  resolveRelative,
 | 
			
		||||
} from "../../path"
 | 
			
		||||
import { QuartzEmitterPlugin } from "../types"
 | 
			
		||||
import path from 'path'
 | 
			
		||||
import path from "path"
 | 
			
		||||
 | 
			
		||||
export const AliasRedirects: QuartzEmitterPlugin = () => ({
 | 
			
		||||
  name: "AliasRedirects",
 | 
			
		||||
@@ -24,7 +30,7 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
 | 
			
		||||
      for (const alias of aliases) {
 | 
			
		||||
        const slug = path.posix.join(dir, alias) as ServerSlug
 | 
			
		||||
 | 
			
		||||
        const fp = slug + ".html" as FilePath
 | 
			
		||||
        const fp = (slug + ".html") as FilePath
 | 
			
		||||
        const redirUrl = resolveRelative(canonicalizeServer(slug), ogSlug)
 | 
			
		||||
        await emit({
 | 
			
		||||
          content: `
 | 
			
		||||
@@ -47,5 +53,5 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return fps
 | 
			
		||||
  }
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -5,12 +5,12 @@ import path from "path"
 | 
			
		||||
 | 
			
		||||
export type ContentIndex = Map<CanonicalSlug, ContentDetails>
 | 
			
		||||
export type ContentDetails = {
 | 
			
		||||
  title: string,
 | 
			
		||||
  links: CanonicalSlug[],
 | 
			
		||||
  tags: string[],
 | 
			
		||||
  content: string,
 | 
			
		||||
  date?: Date,
 | 
			
		||||
  description?: string,
 | 
			
		||||
  title: string
 | 
			
		||||
  links: CanonicalSlug[]
 | 
			
		||||
  tags: string[]
 | 
			
		||||
  content: string
 | 
			
		||||
  date?: Date
 | 
			
		||||
  description?: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Options {
 | 
			
		||||
@@ -31,7 +31,9 @@ function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string {
 | 
			
		||||
    <loc>https://${base}/${slug}</loc>
 | 
			
		||||
    <lastmod>${content.date?.toISOString()}</lastmod>
 | 
			
		||||
  </url>`
 | 
			
		||||
  const urls = Array.from(idx).map(([slug, content]) => createURLEntry(slug, content)).join("")
 | 
			
		||||
  const urls = Array.from(idx)
 | 
			
		||||
    .map(([slug, content]) => createURLEntry(slug, content))
 | 
			
		||||
    .join("")
 | 
			
		||||
  return `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">${urls}</urlset>`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -47,7 +49,9 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex): string {
 | 
			
		||||
    <pubDate>${content.date?.toUTCString()}</pubDate>
 | 
			
		||||
  </items>`
 | 
			
		||||
 | 
			
		||||
  const items = Array.from(idx).map(([slug, content]) => createURLEntry(slug, content)).join("")
 | 
			
		||||
  const items = Array.from(idx)
 | 
			
		||||
    .map(([slug, content]) => createURLEntry(slug, content))
 | 
			
		||||
    .join("")
 | 
			
		||||
  return `<rss xmlns:atom="http://www.w3.org/2005/atom" version="2.0">
 | 
			
		||||
    <channel>
 | 
			
		||||
      <title>${cfg.pageTitle}</title>
 | 
			
		||||
@@ -71,14 +75,14 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
 | 
			
		||||
        const slug = canonicalizeServer(file.data.slug!)
 | 
			
		||||
        const date = file.data.dates?.modified ?? new Date()
 | 
			
		||||
        if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) {
 | 
			
		||||
        linkIndex.set(slug, {
 | 
			
		||||
          title: file.data.frontmatter?.title!,
 | 
			
		||||
          links: file.data.links ?? [],
 | 
			
		||||
          tags: file.data.frontmatter?.tags ?? [],
 | 
			
		||||
          content: file.data.text ?? "",
 | 
			
		||||
          date: date,
 | 
			
		||||
          description: file.data.description ?? ""
 | 
			
		||||
        })
 | 
			
		||||
          linkIndex.set(slug, {
 | 
			
		||||
            title: file.data.frontmatter?.title!,
 | 
			
		||||
            links: file.data.links ?? [],
 | 
			
		||||
            tags: file.data.frontmatter?.tags ?? [],
 | 
			
		||||
            content: file.data.text ?? "",
 | 
			
		||||
            date: date,
 | 
			
		||||
            description: file.data.description ?? "",
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@@ -86,7 +90,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
 | 
			
		||||
        await emit({
 | 
			
		||||
          content: generateSiteMap(cfg, linkIndex),
 | 
			
		||||
          slug: "sitemap" as ServerSlug,
 | 
			
		||||
          ext: ".xml"
 | 
			
		||||
          ext: ".xml",
 | 
			
		||||
        })
 | 
			
		||||
        emitted.push("sitemap.xml" as FilePath)
 | 
			
		||||
      }
 | 
			
		||||
@@ -95,7 +99,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
 | 
			
		||||
        await emit({
 | 
			
		||||
          content: generateRSSFeed(cfg, linkIndex),
 | 
			
		||||
          slug: "index" as ServerSlug,
 | 
			
		||||
          ext: ".xml"
 | 
			
		||||
          ext: ".xml",
 | 
			
		||||
        })
 | 
			
		||||
        emitted.push("index.xml" as FilePath)
 | 
			
		||||
      }
 | 
			
		||||
@@ -109,7 +113,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
 | 
			
		||||
          delete content.description
 | 
			
		||||
          delete content.date
 | 
			
		||||
          return [slug, content]
 | 
			
		||||
        })
 | 
			
		||||
        }),
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      await emit({
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,9 @@ import { FilePath, canonicalizeServer } from "../../path"
 | 
			
		||||
 | 
			
		||||
export const ContentPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
 | 
			
		||||
  if (!opts) {
 | 
			
		||||
    throw new Error("ContentPage must be initialized with options specifiying the components to use")
 | 
			
		||||
    throw new Error(
 | 
			
		||||
      "ContentPage must be initialized with options specifiying the components to use",
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const { head: Head, header, beforeBody, pageBody: Content, left, right, footer: Footer } = opts
 | 
			
		||||
@@ -22,7 +24,7 @@ export const ContentPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
 | 
			
		||||
    },
 | 
			
		||||
    async emit(_contentDir, cfg, content, resources, emit): Promise<FilePath[]> {
 | 
			
		||||
      const fps: FilePath[] = []
 | 
			
		||||
      const allFiles = content.map(c => c[1].data)
 | 
			
		||||
      const allFiles = content.map((c) => c[1].data)
 | 
			
		||||
      for (const [tree, file] of content) {
 | 
			
		||||
        const slug = canonicalizeServer(file.data.slug!)
 | 
			
		||||
        const externalResources = pageResources(slug, resources)
 | 
			
		||||
@@ -32,17 +34,12 @@ export const ContentPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
 | 
			
		||||
          cfg,
 | 
			
		||||
          children: [],
 | 
			
		||||
          tree,
 | 
			
		||||
          allFiles
 | 
			
		||||
          allFiles,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const content = renderPage(
 | 
			
		||||
          slug,
 | 
			
		||||
          componentData,
 | 
			
		||||
          opts,
 | 
			
		||||
          externalResources
 | 
			
		||||
        )
 | 
			
		||||
        const content = renderPage(slug, componentData, opts, externalResources)
 | 
			
		||||
 | 
			
		||||
        const fp = file.data.slug + ".html" as FilePath
 | 
			
		||||
        const fp = (file.data.slug + ".html") as FilePath
 | 
			
		||||
        await emit({
 | 
			
		||||
          content,
 | 
			
		||||
          slug: file.data.slug!,
 | 
			
		||||
@@ -52,6 +49,6 @@ export const ContentPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
 | 
			
		||||
        fps.push(fp)
 | 
			
		||||
      }
 | 
			
		||||
      return fps
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -24,20 +24,28 @@ export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
 | 
			
		||||
    },
 | 
			
		||||
    async emit(_contentDir, cfg, content, resources, emit): Promise<FilePath[]> {
 | 
			
		||||
      const fps: FilePath[] = []
 | 
			
		||||
      const allFiles = content.map(c => c[1].data)
 | 
			
		||||
      const allFiles = content.map((c) => c[1].data)
 | 
			
		||||
 | 
			
		||||
      const folders: Set<CanonicalSlug> = new Set(allFiles.flatMap(data => {
 | 
			
		||||
        const slug = data.slug
 | 
			
		||||
        const folderName = path.dirname(slug ?? "") as CanonicalSlug
 | 
			
		||||
        if (slug && folderName !== "." && folderName !== "tags") {
 | 
			
		||||
          return [folderName]
 | 
			
		||||
        }
 | 
			
		||||
        return []
 | 
			
		||||
      }))
 | 
			
		||||
      const folders: Set<CanonicalSlug> = new Set(
 | 
			
		||||
        allFiles.flatMap((data) => {
 | 
			
		||||
          const slug = data.slug
 | 
			
		||||
          const folderName = path.dirname(slug ?? "") as CanonicalSlug
 | 
			
		||||
          if (slug && folderName !== "." && folderName !== "tags") {
 | 
			
		||||
            return [folderName]
 | 
			
		||||
          }
 | 
			
		||||
          return []
 | 
			
		||||
        }),
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      const folderDescriptions: Record<string, ProcessedContent> = Object.fromEntries([...folders].map(folder => ([
 | 
			
		||||
        folder, defaultProcessedContent({ slug: joinSegments(folder, "index") as ServerSlug, frontmatter: { title: `Folder: ${folder}`, tags: [] } })
 | 
			
		||||
      ])))
 | 
			
		||||
      const folderDescriptions: Record<string, ProcessedContent> = Object.fromEntries(
 | 
			
		||||
        [...folders].map((folder) => [
 | 
			
		||||
          folder,
 | 
			
		||||
          defaultProcessedContent({
 | 
			
		||||
            slug: joinSegments(folder, "index") as ServerSlug,
 | 
			
		||||
            frontmatter: { title: `Folder: ${folder}`, tags: [] },
 | 
			
		||||
          }),
 | 
			
		||||
        ]),
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      for (const [tree, file] of content) {
 | 
			
		||||
        const slug = canonicalizeServer(file.data.slug!)
 | 
			
		||||
@@ -56,17 +64,12 @@ export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
 | 
			
		||||
          cfg,
 | 
			
		||||
          children: [],
 | 
			
		||||
          tree,
 | 
			
		||||
          allFiles
 | 
			
		||||
          allFiles,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const content = renderPage(
 | 
			
		||||
          slug,
 | 
			
		||||
          componentData,
 | 
			
		||||
          opts,
 | 
			
		||||
          externalResources
 | 
			
		||||
        )
 | 
			
		||||
        const content = renderPage(slug, componentData, opts, externalResources)
 | 
			
		||||
 | 
			
		||||
        const fp = file.data.slug! + ".html" as FilePath
 | 
			
		||||
        const fp = (file.data.slug! + ".html") as FilePath
 | 
			
		||||
        await emit({
 | 
			
		||||
          content,
 | 
			
		||||
          slug: file.data.slug!,
 | 
			
		||||
@@ -76,6 +79,6 @@ export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
 | 
			
		||||
        fps.push(fp)
 | 
			
		||||
      }
 | 
			
		||||
      return fps
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
export { ContentPage } from './contentPage'
 | 
			
		||||
export { TagPage } from './tagPage'
 | 
			
		||||
export { FolderPage } from './folderPage'
 | 
			
		||||
export { ContentIndex } from './contentIndex'
 | 
			
		||||
export { AliasRedirects } from './aliases'
 | 
			
		||||
export { ContentPage } from "./contentPage"
 | 
			
		||||
export { TagPage } from "./tagPage"
 | 
			
		||||
export { FolderPage } from "./folderPage"
 | 
			
		||||
export { ContentIndex } from "./contentIndex"
 | 
			
		||||
export { AliasRedirects } from "./aliases"
 | 
			
		||||
 
 | 
			
		||||
@@ -23,12 +23,18 @@ export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
 | 
			
		||||
    },
 | 
			
		||||
    async emit(_contentDir, cfg, content, resources, emit): Promise<FilePath[]> {
 | 
			
		||||
      const fps: FilePath[] = []
 | 
			
		||||
      const allFiles = content.map(c => c[1].data)
 | 
			
		||||
      const allFiles = content.map((c) => c[1].data)
 | 
			
		||||
 | 
			
		||||
      const tags: Set<string> = new Set(allFiles.flatMap(data => data.frontmatter?.tags ?? []))
 | 
			
		||||
      const tagDescriptions: Record<string, ProcessedContent> = Object.fromEntries([...tags].map(tag => ([
 | 
			
		||||
        tag, defaultProcessedContent({ slug: `tags/${tag}/index` as ServerSlug, frontmatter: { title: `Tag: ${tag}`, tags: [] } })
 | 
			
		||||
      ])))
 | 
			
		||||
      const tags: Set<string> = new Set(allFiles.flatMap((data) => data.frontmatter?.tags ?? []))
 | 
			
		||||
      const tagDescriptions: Record<string, ProcessedContent> = Object.fromEntries(
 | 
			
		||||
        [...tags].map((tag) => [
 | 
			
		||||
          tag,
 | 
			
		||||
          defaultProcessedContent({
 | 
			
		||||
            slug: `tags/${tag}/index` as ServerSlug,
 | 
			
		||||
            frontmatter: { title: `Tag: ${tag}`, tags: [] },
 | 
			
		||||
          }),
 | 
			
		||||
        ]),
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      for (const [tree, file] of content) {
 | 
			
		||||
        const slug = file.data.slug!
 | 
			
		||||
@@ -50,17 +56,12 @@ export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
 | 
			
		||||
          cfg,
 | 
			
		||||
          children: [],
 | 
			
		||||
          tree,
 | 
			
		||||
          allFiles
 | 
			
		||||
          allFiles,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const content = renderPage(
 | 
			
		||||
          slug,
 | 
			
		||||
          componentData,
 | 
			
		||||
          opts,
 | 
			
		||||
          externalResources
 | 
			
		||||
        )
 | 
			
		||||
        const content = renderPage(slug, componentData, opts, externalResources)
 | 
			
		||||
 | 
			
		||||
        const fp = file.data.slug + ".html" as FilePath
 | 
			
		||||
        const fp = (file.data.slug + ".html") as FilePath
 | 
			
		||||
        await emit({
 | 
			
		||||
          content,
 | 
			
		||||
          slug: file.data.slug!,
 | 
			
		||||
@@ -70,6 +71,6 @@ export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
 | 
			
		||||
        fps.push(fp)
 | 
			
		||||
      }
 | 
			
		||||
      return fps
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,5 +5,5 @@ export const RemoveDrafts: QuartzFilterPlugin<{}> = () => ({
 | 
			
		||||
  shouldPublish([_tree, vfile]) {
 | 
			
		||||
    const draftFlag: boolean = vfile.data?.frontmatter?.draft ?? false
 | 
			
		||||
    return !draftFlag
 | 
			
		||||
  }
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -5,5 +5,5 @@ export const ExplicitPublish: QuartzFilterPlugin = () => ({
 | 
			
		||||
  shouldPublish([_tree, vfile]) {
 | 
			
		||||
    const publishFlag: boolean = vfile.data?.frontmatter?.publish ?? false
 | 
			
		||||
    return publishFlag
 | 
			
		||||
  }
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -1,2 +1,2 @@
 | 
			
		||||
export { RemoveDrafts } from './draft'
 | 
			
		||||
export { ExplicitPublish } from './explicit'
 | 
			
		||||
export { RemoveDrafts } from "./draft"
 | 
			
		||||
export { ExplicitPublish } from "./explicit"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,14 @@
 | 
			
		||||
import { GlobalConfiguration } from '../cfg'
 | 
			
		||||
import { QuartzComponent } from '../components/types'
 | 
			
		||||
import { StaticResources } from '../resources'
 | 
			
		||||
import { joinStyles } from '../theme'
 | 
			
		||||
import { EmitCallback, PluginTypes } from './types'
 | 
			
		||||
import styles from '../styles/base.scss'
 | 
			
		||||
import { FilePath, ServerSlug } from '../path'
 | 
			
		||||
import { GlobalConfiguration } from "../cfg"
 | 
			
		||||
import { QuartzComponent } from "../components/types"
 | 
			
		||||
import { StaticResources } from "../resources"
 | 
			
		||||
import { joinStyles } from "../theme"
 | 
			
		||||
import { EmitCallback, PluginTypes } from "./types"
 | 
			
		||||
import styles from "../styles/base.scss"
 | 
			
		||||
import { FilePath, ServerSlug } from "../path"
 | 
			
		||||
 | 
			
		||||
export type ComponentResources = {
 | 
			
		||||
  css: string[],
 | 
			
		||||
  beforeDOMLoaded: string[],
 | 
			
		||||
  css: string[]
 | 
			
		||||
  beforeDOMLoaded: string[]
 | 
			
		||||
  afterDOMLoaded: string[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -24,7 +24,7 @@ export function getComponentResources(plugins: PluginTypes): ComponentResources
 | 
			
		||||
  const componentResources = {
 | 
			
		||||
    css: new Set<string>(),
 | 
			
		||||
    beforeDOMLoaded: new Set<string>(),
 | 
			
		||||
    afterDOMLoaded: new Set<string>()
 | 
			
		||||
    afterDOMLoaded: new Set<string>(),
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (const component of allComponents) {
 | 
			
		||||
@@ -39,39 +39,42 @@ export function getComponentResources(plugins: PluginTypes): ComponentResources
 | 
			
		||||
      componentResources.afterDOMLoaded.add(afterDOMLoaded)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    css: [...componentResources.css],
 | 
			
		||||
    beforeDOMLoaded: [...componentResources.beforeDOMLoaded],
 | 
			
		||||
    afterDOMLoaded: [...componentResources.afterDOMLoaded]
 | 
			
		||||
    afterDOMLoaded: [...componentResources.afterDOMLoaded],
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function joinScripts(scripts: string[]): string {
 | 
			
		||||
  // wrap with iife to prevent scope collision
 | 
			
		||||
  return scripts.map(script => `(function () {${script}})();`).join("\n")
 | 
			
		||||
  return scripts.map((script) => `(function () {${script}})();`).join("\n")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function emitComponentResources(cfg: GlobalConfiguration, res: ComponentResources, emit: EmitCallback): Promise<FilePath[]> {
 | 
			
		||||
export async function emitComponentResources(
 | 
			
		||||
  cfg: GlobalConfiguration,
 | 
			
		||||
  res: ComponentResources,
 | 
			
		||||
  emit: EmitCallback,
 | 
			
		||||
): Promise<FilePath[]> {
 | 
			
		||||
  const fps = await Promise.all([
 | 
			
		||||
    emit({
 | 
			
		||||
      slug: "index" as ServerSlug,
 | 
			
		||||
      ext: ".css",
 | 
			
		||||
      content: joinStyles(cfg.theme, styles, ...res.css)
 | 
			
		||||
      content: joinStyles(cfg.theme, styles, ...res.css),
 | 
			
		||||
    }),
 | 
			
		||||
    emit({
 | 
			
		||||
      slug: "prescript" as ServerSlug,
 | 
			
		||||
      ext: ".js",
 | 
			
		||||
      content: joinScripts(res.beforeDOMLoaded)
 | 
			
		||||
      content: joinScripts(res.beforeDOMLoaded),
 | 
			
		||||
    }),
 | 
			
		||||
    emit({
 | 
			
		||||
      slug: "postscript" as ServerSlug,
 | 
			
		||||
      ext: ".js",
 | 
			
		||||
      content: joinScripts(res.afterDOMLoaded)
 | 
			
		||||
    })
 | 
			
		||||
      content: joinScripts(res.afterDOMLoaded),
 | 
			
		||||
    }),
 | 
			
		||||
  ])
 | 
			
		||||
  return fps
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getStaticResourcesFromPlugins(plugins: PluginTypes) {
 | 
			
		||||
@@ -93,11 +96,11 @@ export function getStaticResourcesFromPlugins(plugins: PluginTypes) {
 | 
			
		||||
  return staticResources
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export * from './transformers'
 | 
			
		||||
export * from './filters'
 | 
			
		||||
export * from './emitters'
 | 
			
		||||
export * from "./transformers"
 | 
			
		||||
export * from "./filters"
 | 
			
		||||
export * from "./emitters"
 | 
			
		||||
 | 
			
		||||
declare module 'vfile' {
 | 
			
		||||
declare module "vfile" {
 | 
			
		||||
  // inserted in processors.ts
 | 
			
		||||
  interface DataMap {
 | 
			
		||||
    slug: ServerSlug
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { Root as HTMLRoot } from 'hast'
 | 
			
		||||
import { Root as HTMLRoot } from "hast"
 | 
			
		||||
import { toString } from "hast-util-to-string"
 | 
			
		||||
import { QuartzTransformerPlugin } from "../types"
 | 
			
		||||
 | 
			
		||||
@@ -7,11 +7,16 @@ export interface Options {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultOptions: Options = {
 | 
			
		||||
  descriptionLength: 150
 | 
			
		||||
  descriptionLength: 150,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const escapeHTML = (unsafe: string) => {
 | 
			
		||||
  return unsafe.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
 | 
			
		||||
  return unsafe
 | 
			
		||||
    .replaceAll("&", "&")
 | 
			
		||||
    .replaceAll("<", "<")
 | 
			
		||||
    .replaceAll(">", ">")
 | 
			
		||||
    .replaceAll('"', """)
 | 
			
		||||
    .replaceAll("'", "'")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Description: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
 | 
			
		||||
@@ -26,30 +31,29 @@ export const Description: QuartzTransformerPlugin<Partial<Options> | undefined>
 | 
			
		||||
            const text = escapeHTML(toString(tree))
 | 
			
		||||
 | 
			
		||||
            const desc = frontMatterDescription ?? text
 | 
			
		||||
            const sentences = desc.replace(/\s+/g, ' ').split('.')
 | 
			
		||||
            const sentences = desc.replace(/\s+/g, " ").split(".")
 | 
			
		||||
            let finalDesc = ""
 | 
			
		||||
            let sentenceIdx = 0
 | 
			
		||||
            const len = opts.descriptionLength
 | 
			
		||||
            while (finalDesc.length < len) {
 | 
			
		||||
              const sentence = sentences[sentenceIdx]
 | 
			
		||||
              if (!sentence) break
 | 
			
		||||
              finalDesc += sentence + '.'
 | 
			
		||||
              finalDesc += sentence + "."
 | 
			
		||||
              sentenceIdx++
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            file.data.description = finalDesc
 | 
			
		||||
            file.data.text = text
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        },
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare module 'vfile' {
 | 
			
		||||
declare module "vfile" {
 | 
			
		||||
  interface DataMap {
 | 
			
		||||
    description: string
 | 
			
		||||
    text: string
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,17 @@
 | 
			
		||||
import matter from "gray-matter"
 | 
			
		||||
import remarkFrontmatter from 'remark-frontmatter'
 | 
			
		||||
import remarkFrontmatter from "remark-frontmatter"
 | 
			
		||||
import { QuartzTransformerPlugin } from "../types"
 | 
			
		||||
import yaml from 'js-yaml'
 | 
			
		||||
import { slug as slugAnchor } from 'github-slugger'
 | 
			
		||||
import yaml from "js-yaml"
 | 
			
		||||
import { slug as slugAnchor } from "github-slugger"
 | 
			
		||||
 | 
			
		||||
export interface Options {
 | 
			
		||||
  language: 'yaml' | 'toml',
 | 
			
		||||
  language: "yaml" | "toml"
 | 
			
		||||
  delims: string | string[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultOptions: Options = {
 | 
			
		||||
  language: 'yaml',
 | 
			
		||||
  delims: '---'
 | 
			
		||||
  language: "yaml",
 | 
			
		||||
  delims: "---",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
 | 
			
		||||
@@ -26,8 +26,8 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined>
 | 
			
		||||
            const { data } = matter(file.value, {
 | 
			
		||||
              ...opts,
 | 
			
		||||
              engines: {
 | 
			
		||||
                yaml: s => yaml.load(s, { schema: yaml.JSON_SCHEMA }) as object
 | 
			
		||||
              }
 | 
			
		||||
                yaml: (s) => yaml.load(s, { schema: yaml.JSON_SCHEMA }) as object,
 | 
			
		||||
              },
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
            // tag is an alias for tags
 | 
			
		||||
@@ -36,7 +36,10 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined>
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (data.tags && !Array.isArray(data.tags)) {
 | 
			
		||||
              data.tags = data.tags.toString().split(",").map((tag: string) => tag.trim())
 | 
			
		||||
              data.tags = data.tags
 | 
			
		||||
                .toString()
 | 
			
		||||
                .split(",")
 | 
			
		||||
                .map((tag: string) => tag.trim())
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // slug them all!!
 | 
			
		||||
@@ -46,16 +49,16 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined>
 | 
			
		||||
            file.data.frontmatter = {
 | 
			
		||||
              title: file.stem ?? "Untitled",
 | 
			
		||||
              tags: [],
 | 
			
		||||
              ...data
 | 
			
		||||
              ...data,
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        },
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare module 'vfile' {
 | 
			
		||||
declare module "vfile" {
 | 
			
		||||
  interface DataMap {
 | 
			
		||||
    frontmatter: { [key: string]: any } & {
 | 
			
		||||
      title: string
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import remarkGfm from "remark-gfm"
 | 
			
		||||
import smartypants from 'remark-smartypants'
 | 
			
		||||
import smartypants from "remark-smartypants"
 | 
			
		||||
import { QuartzTransformerPlugin } from "../types"
 | 
			
		||||
import rehypeSlug from "rehype-slug"
 | 
			
		||||
import rehypeAutolinkHeadings from "rehype-autolink-headings"
 | 
			
		||||
@@ -11,10 +11,12 @@ export interface Options {
 | 
			
		||||
 | 
			
		||||
const defaultOptions: Options = {
 | 
			
		||||
  enableSmartyPants: true,
 | 
			
		||||
  linkHeadings: true
 | 
			
		||||
  linkHeadings: true,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
 | 
			
		||||
export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (
 | 
			
		||||
  userOpts,
 | 
			
		||||
) => {
 | 
			
		||||
  const opts = { ...defaultOptions, ...userOpts }
 | 
			
		||||
  return {
 | 
			
		||||
    name: "GitHubFlavoredMarkdown",
 | 
			
		||||
@@ -23,15 +25,22 @@ export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> |
 | 
			
		||||
    },
 | 
			
		||||
    htmlPlugins() {
 | 
			
		||||
      if (opts.linkHeadings) {
 | 
			
		||||
        return [rehypeSlug, [rehypeAutolinkHeadings, {
 | 
			
		||||
          behavior: 'append', content: {
 | 
			
		||||
            type: 'text',
 | 
			
		||||
            value: ' §',
 | 
			
		||||
          }
 | 
			
		||||
        }]]
 | 
			
		||||
        return [
 | 
			
		||||
          rehypeSlug,
 | 
			
		||||
          [
 | 
			
		||||
            rehypeAutolinkHeadings,
 | 
			
		||||
            {
 | 
			
		||||
              behavior: "append",
 | 
			
		||||
              content: {
 | 
			
		||||
                type: "text",
 | 
			
		||||
                value: " §",
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        ]
 | 
			
		||||
      } else {
 | 
			
		||||
        return []
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
export { FrontMatter } from './frontmatter'
 | 
			
		||||
export { GitHubFlavoredMarkdown } from './gfm'
 | 
			
		||||
export { CreatedModifiedDate } from './lastmod'
 | 
			
		||||
export { Latex } from './latex'
 | 
			
		||||
export { Description } from './description'
 | 
			
		||||
export { CrawlLinks } from './links'
 | 
			
		||||
export { ObsidianFlavoredMarkdown } from './ofm'
 | 
			
		||||
export { SyntaxHighlighting } from './syntax'
 | 
			
		||||
export { TableOfContents } from './toc'
 | 
			
		||||
export { FrontMatter } from "./frontmatter"
 | 
			
		||||
export { GitHubFlavoredMarkdown } from "./gfm"
 | 
			
		||||
export { CreatedModifiedDate } from "./lastmod"
 | 
			
		||||
export { Latex } from "./latex"
 | 
			
		||||
export { Description } from "./description"
 | 
			
		||||
export { CrawlLinks } from "./links"
 | 
			
		||||
export { ObsidianFlavoredMarkdown } from "./ofm"
 | 
			
		||||
export { SyntaxHighlighting } from "./syntax"
 | 
			
		||||
export { TableOfContents } from "./toc"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,20 @@
 | 
			
		||||
import fs from "fs"
 | 
			
		||||
import path from 'path'
 | 
			
		||||
import path from "path"
 | 
			
		||||
import { Repository } from "@napi-rs/simple-git"
 | 
			
		||||
import { QuartzTransformerPlugin } from "../types"
 | 
			
		||||
 | 
			
		||||
export interface Options {
 | 
			
		||||
  priority: ('frontmatter' | 'git' | 'filesystem')[],
 | 
			
		||||
  priority: ("frontmatter" | "git" | "filesystem")[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultOptions: Options = {
 | 
			
		||||
  priority: ['frontmatter', 'git', 'filesystem']
 | 
			
		||||
  priority: ["frontmatter", "git", "filesystem"],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MaybeDate = undefined | string | number
 | 
			
		||||
export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
 | 
			
		||||
export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | undefined> = (
 | 
			
		||||
  userOpts,
 | 
			
		||||
) => {
 | 
			
		||||
  const opts = { ...defaultOptions, ...userOpts }
 | 
			
		||||
  return {
 | 
			
		||||
    name: "CreatedModifiedDate",
 | 
			
		||||
@@ -51,13 +53,13 @@ export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | und
 | 
			
		||||
              published: published ? new Date(published) : new Date(),
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        },
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare module 'vfile' {
 | 
			
		||||
declare module "vfile" {
 | 
			
		||||
  interface DataMap {
 | 
			
		||||
    dates: {
 | 
			
		||||
      created: Date
 | 
			
		||||
 
 | 
			
		||||
@@ -1,43 +1,39 @@
 | 
			
		||||
import remarkMath from "remark-math"
 | 
			
		||||
import rehypeKatex from 'rehype-katex'
 | 
			
		||||
import rehypeMathjax from 'rehype-mathjax/svg.js'
 | 
			
		||||
import rehypeKatex from "rehype-katex"
 | 
			
		||||
import rehypeMathjax from "rehype-mathjax/svg.js"
 | 
			
		||||
import { QuartzTransformerPlugin } from "../types"
 | 
			
		||||
 | 
			
		||||
interface Options {
 | 
			
		||||
  renderEngine: 'katex' | 'mathjax'
 | 
			
		||||
  renderEngine: "katex" | "mathjax"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Latex: QuartzTransformerPlugin<Options> = (opts?: Options) => {
 | 
			
		||||
  const engine = opts?.renderEngine ?? 'katex'
 | 
			
		||||
  const engine = opts?.renderEngine ?? "katex"
 | 
			
		||||
  return {
 | 
			
		||||
    name: "Latex",
 | 
			
		||||
    markdownPlugins() {
 | 
			
		||||
      return [remarkMath]
 | 
			
		||||
    },
 | 
			
		||||
    htmlPlugins() {
 | 
			
		||||
      return [
 | 
			
		||||
        engine === 'katex'
 | 
			
		||||
          ? [rehypeKatex, { output: 'html' }]
 | 
			
		||||
          : [rehypeMathjax]
 | 
			
		||||
      ]
 | 
			
		||||
      return [engine === "katex" ? [rehypeKatex, { output: "html" }] : [rehypeMathjax]]
 | 
			
		||||
    },
 | 
			
		||||
    externalResources() {
 | 
			
		||||
      return engine === 'katex'
 | 
			
		||||
      return engine === "katex"
 | 
			
		||||
        ? {
 | 
			
		||||
          css: [
 | 
			
		||||
            // base css
 | 
			
		||||
            "https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css",
 | 
			
		||||
          ],
 | 
			
		||||
          js: [
 | 
			
		||||
            {
 | 
			
		||||
              // fix copy behaviour: https://github.com/KaTeX/KaTeX/blob/main/contrib/copy-tex/README.md
 | 
			
		||||
              src: "https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/contrib/copy-tex.min.js",
 | 
			
		||||
              loadTime: "afterDOMReady",
 | 
			
		||||
              contentType: 'external'
 | 
			
		||||
            }
 | 
			
		||||
          ]
 | 
			
		||||
        }
 | 
			
		||||
            css: [
 | 
			
		||||
              // base css
 | 
			
		||||
              "https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css",
 | 
			
		||||
            ],
 | 
			
		||||
            js: [
 | 
			
		||||
              {
 | 
			
		||||
                // fix copy behaviour: https://github.com/KaTeX/KaTeX/blob/main/contrib/copy-tex/README.md
 | 
			
		||||
                src: "https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/contrib/copy-tex.min.js",
 | 
			
		||||
                loadTime: "afterDOMReady",
 | 
			
		||||
                contentType: "external",
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          }
 | 
			
		||||
        : {}
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,27 @@
 | 
			
		||||
import { QuartzTransformerPlugin } from "../types"
 | 
			
		||||
import { CanonicalSlug, RelativeURL, canonicalizeServer, joinSegments, pathToRoot, resolveRelative, splitAnchor, transformInternalLink } from "../../path"
 | 
			
		||||
import {
 | 
			
		||||
  CanonicalSlug,
 | 
			
		||||
  RelativeURL,
 | 
			
		||||
  canonicalizeServer,
 | 
			
		||||
  joinSegments,
 | 
			
		||||
  pathToRoot,
 | 
			
		||||
  resolveRelative,
 | 
			
		||||
  splitAnchor,
 | 
			
		||||
  transformInternalLink,
 | 
			
		||||
} from "../../path"
 | 
			
		||||
import path from "path"
 | 
			
		||||
import { visit } from 'unist-util-visit'
 | 
			
		||||
import { visit } from "unist-util-visit"
 | 
			
		||||
import isAbsoluteUrl from "is-absolute-url"
 | 
			
		||||
 | 
			
		||||
interface Options {
 | 
			
		||||
  /** How to resolve Markdown paths */
 | 
			
		||||
  markdownLinkResolution: 'absolute' | 'relative' | 'shortest'
 | 
			
		||||
  markdownLinkResolution: "absolute" | "relative" | "shortest"
 | 
			
		||||
  /** Strips folders from a link so that it looks nice */
 | 
			
		||||
  prettyLinks: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultOptions: Options = {
 | 
			
		||||
  markdownLinkResolution: 'absolute',
 | 
			
		||||
  markdownLinkResolution: "absolute",
 | 
			
		||||
  prettyLinks: true,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -21,84 +30,91 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
 | 
			
		||||
  return {
 | 
			
		||||
    name: "LinkProcessing",
 | 
			
		||||
    htmlPlugins() {
 | 
			
		||||
      return [() => {
 | 
			
		||||
        return (tree, file) => {
 | 
			
		||||
          const curSlug = canonicalizeServer(file.data.slug!)
 | 
			
		||||
          const transformLink = (target: string): RelativeURL => {
 | 
			
		||||
            const targetSlug = transformInternalLink(target).slice("./".length)
 | 
			
		||||
            let [targetCanonical, targetAnchor] = splitAnchor(targetSlug)
 | 
			
		||||
            if (opts.markdownLinkResolution === 'relative') {
 | 
			
		||||
              return targetSlug as RelativeURL
 | 
			
		||||
            } else if (opts.markdownLinkResolution === 'shortest') {
 | 
			
		||||
              // https://forum.obsidian.md/t/settings-new-link-format-what-is-shortest-path-when-possible/6748/5
 | 
			
		||||
              const allSlugs = file.data.allSlugs!
 | 
			
		||||
      return [
 | 
			
		||||
        () => {
 | 
			
		||||
          return (tree, file) => {
 | 
			
		||||
            const curSlug = canonicalizeServer(file.data.slug!)
 | 
			
		||||
            const transformLink = (target: string): RelativeURL => {
 | 
			
		||||
              const targetSlug = transformInternalLink(target).slice("./".length)
 | 
			
		||||
              let [targetCanonical, targetAnchor] = splitAnchor(targetSlug)
 | 
			
		||||
              if (opts.markdownLinkResolution === "relative") {
 | 
			
		||||
                return targetSlug as RelativeURL
 | 
			
		||||
              } else if (opts.markdownLinkResolution === "shortest") {
 | 
			
		||||
                // https://forum.obsidian.md/t/settings-new-link-format-what-is-shortest-path-when-possible/6748/5
 | 
			
		||||
                const allSlugs = file.data.allSlugs!
 | 
			
		||||
 | 
			
		||||
              // if the file name is unique, then it's just the filename
 | 
			
		||||
              const matchingFileNames = allSlugs.filter(slug => {
 | 
			
		||||
                const parts = slug.split(path.posix.sep)
 | 
			
		||||
                const fileName = parts.at(-1)
 | 
			
		||||
                return targetCanonical === fileName
 | 
			
		||||
              })
 | 
			
		||||
                // if the file name is unique, then it's just the filename
 | 
			
		||||
                const matchingFileNames = allSlugs.filter((slug) => {
 | 
			
		||||
                  const parts = slug.split(path.posix.sep)
 | 
			
		||||
                  const fileName = parts.at(-1)
 | 
			
		||||
                  return targetCanonical === fileName
 | 
			
		||||
                })
 | 
			
		||||
 | 
			
		||||
              if (matchingFileNames.length === 1) {
 | 
			
		||||
                const targetSlug = canonicalizeServer(matchingFileNames[0])
 | 
			
		||||
                return resolveRelative(curSlug, targetSlug) + targetAnchor as RelativeURL
 | 
			
		||||
                if (matchingFileNames.length === 1) {
 | 
			
		||||
                  const targetSlug = canonicalizeServer(matchingFileNames[0])
 | 
			
		||||
                  return (resolveRelative(curSlug, targetSlug) + targetAnchor) as RelativeURL
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // if it's not unique, then it's the absolute path from the vault root
 | 
			
		||||
                // (fall-through case)
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              // if it's not unique, then it's the absolute path from the vault root
 | 
			
		||||
              // (fall-through case)
 | 
			
		||||
              // treat as absolute
 | 
			
		||||
              return joinSegments(pathToRoot(curSlug), targetSlug) as RelativeURL
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // treat as absolute
 | 
			
		||||
            return joinSegments(pathToRoot(curSlug), targetSlug) as RelativeURL
 | 
			
		||||
            const outgoing: Set<CanonicalSlug> = new Set()
 | 
			
		||||
            visit(tree, "element", (node, _index, _parent) => {
 | 
			
		||||
              // rewrite all links
 | 
			
		||||
              if (
 | 
			
		||||
                node.tagName === "a" &&
 | 
			
		||||
                node.properties &&
 | 
			
		||||
                typeof node.properties.href === "string"
 | 
			
		||||
              ) {
 | 
			
		||||
                let dest = node.properties.href as RelativeURL
 | 
			
		||||
                node.properties.className = isAbsoluteUrl(dest) ? "external" : "internal"
 | 
			
		||||
 | 
			
		||||
                // don't process external links or intra-document anchors
 | 
			
		||||
                if (!(isAbsoluteUrl(dest) || dest.startsWith("#"))) {
 | 
			
		||||
                  dest = node.properties.href = transformLink(dest)
 | 
			
		||||
                  const canonicalDest = path.normalize(joinSegments(curSlug, dest))
 | 
			
		||||
                  const [destCanonical, _destAnchor] = splitAnchor(canonicalDest)
 | 
			
		||||
                  outgoing.add(destCanonical as CanonicalSlug)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // 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 other resources that may use links
 | 
			
		||||
              if (
 | 
			
		||||
                ["img", "video", "audio", "iframe"].includes(node.tagName) &&
 | 
			
		||||
                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
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
            file.data.links = [...outgoing]
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          const outgoing: Set<CanonicalSlug> = new Set()
 | 
			
		||||
          visit(tree, 'element', (node, _index, _parent) => {
 | 
			
		||||
            // rewrite all links
 | 
			
		||||
            if (
 | 
			
		||||
              node.tagName === 'a' &&
 | 
			
		||||
              node.properties &&
 | 
			
		||||
              typeof node.properties.href === 'string'
 | 
			
		||||
            ) {
 | 
			
		||||
              let dest = node.properties.href as RelativeURL
 | 
			
		||||
              node.properties.className = isAbsoluteUrl(dest) ? "external" : "internal"
 | 
			
		||||
 | 
			
		||||
              // don't process external links or intra-document anchors
 | 
			
		||||
              if (!(isAbsoluteUrl(dest) || dest.startsWith("#"))) {
 | 
			
		||||
                dest = node.properties.href = transformLink(dest)
 | 
			
		||||
                const canonicalDest = path.normalize(joinSegments(curSlug, dest))
 | 
			
		||||
                const [destCanonical, _destAnchor] = splitAnchor(canonicalDest)
 | 
			
		||||
                outgoing.add(destCanonical as CanonicalSlug)
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              // 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 other resources that may use links
 | 
			
		||||
            if (
 | 
			
		||||
              ["img", "video", "audio", "iframe"].includes(node.tagName) &&
 | 
			
		||||
              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
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
 | 
			
		||||
          file.data.links = [...outgoing]
 | 
			
		||||
        }
 | 
			
		||||
      }]
 | 
			
		||||
    }
 | 
			
		||||
        },
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare module 'vfile' {
 | 
			
		||||
declare module "vfile" {
 | 
			
		||||
  interface DataMap {
 | 
			
		||||
    links: CanonicalSlug[]
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
import { PluggableList } from "unified"
 | 
			
		||||
import { QuartzTransformerPlugin } from "../types"
 | 
			
		||||
import { Root, HTML, BlockContent, DefinitionContent, Code } from 'mdast'
 | 
			
		||||
import { Root, HTML, BlockContent, DefinitionContent, Code } from "mdast"
 | 
			
		||||
import { findAndReplace } from "mdast-util-find-and-replace"
 | 
			
		||||
import { slug as slugAnchor } from 'github-slugger'
 | 
			
		||||
import { slug as slugAnchor } from "github-slugger"
 | 
			
		||||
import rehypeRaw from "rehype-raw"
 | 
			
		||||
import { visit } from "unist-util-visit"
 | 
			
		||||
import path from "path"
 | 
			
		||||
@@ -71,7 +71,7 @@ function canonicalizeCallout(calloutName: string): keyof typeof callouts {
 | 
			
		||||
    bug: "bug",
 | 
			
		||||
    example: "example",
 | 
			
		||||
    quote: "quote",
 | 
			
		||||
    cite: "quote"
 | 
			
		||||
    cite: "quote",
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return calloutMapping[callout]
 | 
			
		||||
@@ -94,10 +94,10 @@ const callouts = {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const capitalize = (s: string): string => {
 | 
			
		||||
  return s.substring(0, 1).toUpperCase() + s.substring(1);
 | 
			
		||||
  return s.substring(0, 1).toUpperCase() + s.substring(1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Match wikilinks 
 | 
			
		||||
// Match wikilinks
 | 
			
		||||
// !?               -> optional embedding
 | 
			
		||||
// \[\[             -> open brace
 | 
			
		||||
// ([^\[\]\|\#]+)   -> one or more non-special characters ([,],|, or #) (name)
 | 
			
		||||
@@ -105,16 +105,18 @@ const capitalize = (s: string): string => {
 | 
			
		||||
// (|[^\[\]\|\#]+)? -> | then one or more non-special characters (alias)
 | 
			
		||||
const wikilinkRegex = new RegExp(/!?\[\[([^\[\]\|\#]+)(#[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/, "g")
 | 
			
		||||
 | 
			
		||||
// Match highlights 
 | 
			
		||||
// Match highlights
 | 
			
		||||
const highlightRegex = new RegExp(/==(.+)==/, "g")
 | 
			
		||||
 | 
			
		||||
// Match comments 
 | 
			
		||||
// Match comments
 | 
			
		||||
const commentRegex = new RegExp(/%%(.+)%%/, "g")
 | 
			
		||||
 | 
			
		||||
// from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts
 | 
			
		||||
const calloutRegex = new RegExp(/^\[\!(\w+)\]([+-]?)/)
 | 
			
		||||
 | 
			
		||||
export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
 | 
			
		||||
export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (
 | 
			
		||||
  userOpts,
 | 
			
		||||
) => {
 | 
			
		||||
  const opts = { ...defaultOptions, ...userOpts }
 | 
			
		||||
  return {
 | 
			
		||||
    name: "ObsidianFlavoredMarkdown",
 | 
			
		||||
@@ -154,28 +156,31 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
 | 
			
		||||
                  width ||= "auto"
 | 
			
		||||
                  height ||= "auto"
 | 
			
		||||
                  return {
 | 
			
		||||
                    type: 'image',
 | 
			
		||||
                    type: "image",
 | 
			
		||||
                    url,
 | 
			
		||||
                    data: {
 | 
			
		||||
                      hProperties: {
 | 
			
		||||
                        width, height
 | 
			
		||||
                      }
 | 
			
		||||
                    }
 | 
			
		||||
                        width,
 | 
			
		||||
                        height,
 | 
			
		||||
                      },
 | 
			
		||||
                    },
 | 
			
		||||
                  }
 | 
			
		||||
                } else if ([".mp4", ".webm", ".ogv", ".mov", ".mkv"].includes(ext)) {
 | 
			
		||||
                  return {
 | 
			
		||||
                    type: 'html',
 | 
			
		||||
                    value: `<video src="${url}" controls></video>`
 | 
			
		||||
                    type: "html",
 | 
			
		||||
                    value: `<video src="${url}" controls></video>`,
 | 
			
		||||
                  }
 | 
			
		||||
                } else if ([".mp3", ".webm", ".wav", ".m4a", ".ogg", ".3gp", ".flac"].includes(ext)) {
 | 
			
		||||
                } else if (
 | 
			
		||||
                  [".mp3", ".webm", ".wav", ".m4a", ".ogg", ".3gp", ".flac"].includes(ext)
 | 
			
		||||
                ) {
 | 
			
		||||
                  return {
 | 
			
		||||
                    type: 'html',
 | 
			
		||||
                    value: `<audio src="${url}" controls></audio>`
 | 
			
		||||
                    type: "html",
 | 
			
		||||
                    value: `<audio src="${url}" controls></audio>`,
 | 
			
		||||
                  }
 | 
			
		||||
                } else if ([".pdf"].includes(ext)) {
 | 
			
		||||
                  return {
 | 
			
		||||
                    type: 'html',
 | 
			
		||||
                    value: `<iframe src="${url}"></iframe>`
 | 
			
		||||
                    type: "html",
 | 
			
		||||
                    value: `<iframe src="${url}"></iframe>`,
 | 
			
		||||
                  }
 | 
			
		||||
                } else {
 | 
			
		||||
                  // TODO: this is the node embed case
 | 
			
		||||
@@ -187,17 +192,18 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
 | 
			
		||||
              // const url = transformInternalLink(fp + anchor)
 | 
			
		||||
              const url = fp + anchor
 | 
			
		||||
              return {
 | 
			
		||||
                type: 'link',
 | 
			
		||||
                type: "link",
 | 
			
		||||
                url,
 | 
			
		||||
                children: [{
 | 
			
		||||
                  type: 'text',
 | 
			
		||||
                  value: alias ?? fp
 | 
			
		||||
                }]
 | 
			
		||||
                children: [
 | 
			
		||||
                  {
 | 
			
		||||
                    type: "text",
 | 
			
		||||
                    value: alias ?? fp,
 | 
			
		||||
                  },
 | 
			
		||||
                ],
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        )
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (opts.highlight) {
 | 
			
		||||
@@ -206,21 +212,21 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
 | 
			
		||||
            findAndReplace(tree, highlightRegex, (_value: string, ...capture: string[]) => {
 | 
			
		||||
              const [inner] = capture
 | 
			
		||||
              return {
 | 
			
		||||
                type: 'html',
 | 
			
		||||
                value: `<span class="text-highlight">${inner}</span>`
 | 
			
		||||
                type: "html",
 | 
			
		||||
                value: `<span class="text-highlight">${inner}</span>`,
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
 | 
			
		||||
      if (opts.comments) {
 | 
			
		||||
        plugins.push(() => {
 | 
			
		||||
          return (tree: Root, _file) => {
 | 
			
		||||
            findAndReplace(tree, commentRegex, (_value: string, ..._capture: string[]) => {
 | 
			
		||||
              return {
 | 
			
		||||
                type: 'text',
 | 
			
		||||
                value: ''
 | 
			
		||||
                type: "text",
 | 
			
		||||
                value: "",
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
@@ -252,7 +258,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
 | 
			
		||||
                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 title =
 | 
			
		||||
                  match.input.slice(calloutDirective.length).trim() || capitalize(calloutType)
 | 
			
		||||
 | 
			
		||||
                const toggleIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="fold">
 | 
			
		||||
                  <polyline points="6 9 12 15 18 9"></polyline>
 | 
			
		||||
@@ -266,17 +273,20 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
 | 
			
		||||
                  <div class="callout-icon">${callouts[canonicalizeCallout(calloutType)]}</div>
 | 
			
		||||
                  <div class="callout-title-inner">${title}</div>
 | 
			
		||||
                  ${collapse ? toggleIcon : ""}
 | 
			
		||||
                </div>`
 | 
			
		||||
                </div>`,
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const blockquoteContent: (BlockContent | DefinitionContent)[] = [titleNode]
 | 
			
		||||
                if (remainingText.length > 0) {
 | 
			
		||||
                  blockquoteContent.push({
 | 
			
		||||
                    type: 'paragraph',
 | 
			
		||||
                    children: [{
 | 
			
		||||
                      type: 'text',
 | 
			
		||||
                      value: remainingText,
 | 
			
		||||
                    }, ...restChildren]
 | 
			
		||||
                    type: "paragraph",
 | 
			
		||||
                    children: [
 | 
			
		||||
                      {
 | 
			
		||||
                        type: "text",
 | 
			
		||||
                        value: remainingText,
 | 
			
		||||
                      },
 | 
			
		||||
                      ...restChildren,
 | 
			
		||||
                    ],
 | 
			
		||||
                  })
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@@ -287,10 +297,12 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
 | 
			
		||||
                node.data = {
 | 
			
		||||
                  hProperties: {
 | 
			
		||||
                    ...(node.data?.hProperties ?? {}),
 | 
			
		||||
                    className: `callout ${collapse ? "is-collapsible" : ""} ${defaultState === "collapsed" ? "is-collapsed" : ""}`,
 | 
			
		||||
                    className: `callout ${collapse ? "is-collapsible" : ""} ${
 | 
			
		||||
                      defaultState === "collapsed" ? "is-collapsed" : ""
 | 
			
		||||
                    }`,
 | 
			
		||||
                    "data-callout": calloutType,
 | 
			
		||||
                    "data-callout-fold": collapse,
 | 
			
		||||
                  }
 | 
			
		||||
                  },
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
@@ -301,12 +313,12 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
 | 
			
		||||
      if (opts.mermaid) {
 | 
			
		||||
        plugins.push(() => {
 | 
			
		||||
          return (tree: Root, _file) => {
 | 
			
		||||
            visit(tree, 'code', (node: Code) => {
 | 
			
		||||
              if (node.lang === 'mermaid') {
 | 
			
		||||
            visit(tree, "code", (node: Code) => {
 | 
			
		||||
              if (node.lang === "mermaid") {
 | 
			
		||||
                node.data = {
 | 
			
		||||
                  hProperties: {
 | 
			
		||||
                    className: 'mermaid'
 | 
			
		||||
                  }
 | 
			
		||||
                    className: "mermaid",
 | 
			
		||||
                  },
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
@@ -325,8 +337,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
 | 
			
		||||
      if (opts.callouts) {
 | 
			
		||||
        js.push({
 | 
			
		||||
          script: calloutScript,
 | 
			
		||||
          loadTime: 'afterDOMReady',
 | 
			
		||||
          contentType: 'inline'
 | 
			
		||||
          loadTime: "afterDOMReady",
 | 
			
		||||
          contentType: "inline",
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@@ -336,13 +348,13 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
 | 
			
		||||
          import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs';
 | 
			
		||||
          mermaid.initialize({ startOnLoad: true });
 | 
			
		||||
          `,
 | 
			
		||||
          loadTime: 'afterDOMReady',
 | 
			
		||||
          moduleType: 'module',
 | 
			
		||||
          contentType: 'inline'
 | 
			
		||||
          loadTime: "afterDOMReady",
 | 
			
		||||
          moduleType: "module",
 | 
			
		||||
          contentType: "inline",
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return { js }
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,13 @@ import rehypePrettyCode, { Options as CodeOptions } from "rehype-pretty-code"
 | 
			
		||||
export const SyntaxHighlighting: QuartzTransformerPlugin = () => ({
 | 
			
		||||
  name: "SyntaxHighlighting",
 | 
			
		||||
  htmlPlugins() {
 | 
			
		||||
    return [[rehypePrettyCode, {
 | 
			
		||||
      theme: 'css-variables',
 | 
			
		||||
    } satisfies Partial<CodeOptions>]]
 | 
			
		||||
  }
 | 
			
		||||
    return [
 | 
			
		||||
      [
 | 
			
		||||
        rehypePrettyCode,
 | 
			
		||||
        {
 | 
			
		||||
          theme: "css-variables",
 | 
			
		||||
        } satisfies Partial<CodeOptions>,
 | 
			
		||||
      ],
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -2,11 +2,11 @@ import { QuartzTransformerPlugin } from "../types"
 | 
			
		||||
import { Root } from "mdast"
 | 
			
		||||
import { visit } from "unist-util-visit"
 | 
			
		||||
import { toString } from "mdast-util-to-string"
 | 
			
		||||
import { slug as slugAnchor } from 'github-slugger'
 | 
			
		||||
import { slug as slugAnchor } from "github-slugger"
 | 
			
		||||
 | 
			
		||||
export interface Options {
 | 
			
		||||
  maxDepth: 1 | 2 | 3 | 4 | 5 | 6,
 | 
			
		||||
  minEntries: 1,
 | 
			
		||||
  maxDepth: 1 | 2 | 3 | 4 | 5 | 6
 | 
			
		||||
  minEntries: 1
 | 
			
		||||
  showByDefault: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -17,47 +17,53 @@ const defaultOptions: Options = {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface TocEntry {
 | 
			
		||||
  depth: number,
 | 
			
		||||
  text: string,
 | 
			
		||||
  depth: number
 | 
			
		||||
  text: string
 | 
			
		||||
  slug: string // this is just the anchor (#some-slug), not the canonical slug
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
 | 
			
		||||
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(text)
 | 
			
		||||
                })
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
      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(text),
 | 
			
		||||
                  })
 | 
			
		||||
                }
 | 
			
		||||
              })
 | 
			
		||||
 | 
			
		||||
            if (toc.length > opts.minEntries) {
 | 
			
		||||
              file.data.toc = toc.map(entry => ({ ...entry, depth: entry.depth - highestDepth }))
 | 
			
		||||
              if (toc.length > opts.minEntries) {
 | 
			
		||||
                file.data.toc = toc.map((entry) => ({
 | 
			
		||||
                  ...entry,
 | 
			
		||||
                  depth: entry.depth - highestDepth,
 | 
			
		||||
                }))
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }]
 | 
			
		||||
        },
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare module 'vfile' {
 | 
			
		||||
declare module "vfile" {
 | 
			
		||||
  interface DataMap {
 | 
			
		||||
    toc: TocEntry[]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,13 +6,15 @@ import { QuartzComponent } from "../components/types"
 | 
			
		||||
import { FilePath, ServerSlug } from "../path"
 | 
			
		||||
 | 
			
		||||
export interface PluginTypes {
 | 
			
		||||
  transformers: QuartzTransformerPluginInstance[],
 | 
			
		||||
  filters: QuartzFilterPluginInstance[],
 | 
			
		||||
  emitters: QuartzEmitterPluginInstance[],
 | 
			
		||||
  transformers: QuartzTransformerPluginInstance[]
 | 
			
		||||
  filters: QuartzFilterPluginInstance[]
 | 
			
		||||
  emitters: QuartzEmitterPluginInstance[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type OptionType = object | undefined
 | 
			
		||||
export type QuartzTransformerPlugin<Options extends OptionType = undefined> = (opts?: Options) => QuartzTransformerPluginInstance
 | 
			
		||||
export type QuartzTransformerPlugin<Options extends OptionType = undefined> = (
 | 
			
		||||
  opts?: Options,
 | 
			
		||||
) => QuartzTransformerPluginInstance
 | 
			
		||||
export type QuartzTransformerPluginInstance = {
 | 
			
		||||
  name: string
 | 
			
		||||
  textTransform?: (src: string | Buffer) => string | Buffer
 | 
			
		||||
@@ -21,16 +23,26 @@ export type QuartzTransformerPluginInstance = {
 | 
			
		||||
  externalResources?: () => Partial<StaticResources>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type QuartzFilterPlugin<Options extends OptionType = undefined> = (opts?: Options) => QuartzFilterPluginInstance 
 | 
			
		||||
export type QuartzFilterPlugin<Options extends OptionType = undefined> = (
 | 
			
		||||
  opts?: Options,
 | 
			
		||||
) => QuartzFilterPluginInstance
 | 
			
		||||
export type QuartzFilterPluginInstance = {
 | 
			
		||||
  name: string
 | 
			
		||||
  shouldPublish(content: ProcessedContent): boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type QuartzEmitterPlugin<Options extends OptionType = undefined> = (opts?: Options) => QuartzEmitterPluginInstance 
 | 
			
		||||
export type QuartzEmitterPlugin<Options extends OptionType = undefined> = (
 | 
			
		||||
  opts?: Options,
 | 
			
		||||
) => QuartzEmitterPluginInstance
 | 
			
		||||
export type QuartzEmitterPluginInstance = {
 | 
			
		||||
  name: string
 | 
			
		||||
  emit(contentDir: string, cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emitCallback: EmitCallback): Promise<FilePath[]>
 | 
			
		||||
  emit(
 | 
			
		||||
    contentDir: string,
 | 
			
		||||
    cfg: GlobalConfiguration,
 | 
			
		||||
    content: ProcessedContent[],
 | 
			
		||||
    resources: StaticResources,
 | 
			
		||||
    emitCallback: EmitCallback,
 | 
			
		||||
  ): Promise<FilePath[]>
 | 
			
		||||
  getQuartzComponents(): QuartzComponent[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
import { Node, Parent } from 'hast'
 | 
			
		||||
import { Data, VFile } from 'vfile'
 | 
			
		||||
import { Node, Parent } from "hast"
 | 
			
		||||
import { Data, VFile } from "vfile"
 | 
			
		||||
 | 
			
		||||
export type QuartzPluginData = Data
 | 
			
		||||
export type ProcessedContent = [Node<QuartzPluginData>, VFile]
 | 
			
		||||
 | 
			
		||||
export function defaultProcessedContent(vfileData: Partial<QuartzPluginData>): ProcessedContent {
 | 
			
		||||
  const root: Parent = { type: 'root', children: [] }
 | 
			
		||||
  const root: Parent = { type: "root", children: [] }
 | 
			
		||||
  const vfile = new VFile("")
 | 
			
		||||
  vfile.data = vfileData
 | 
			
		||||
  return [root, vfile]
 | 
			
		||||
 
 | 
			
		||||
@@ -2,25 +2,35 @@ import path from "path"
 | 
			
		||||
import fs from "fs"
 | 
			
		||||
import { GlobalConfiguration, QuartzConfig } from "../cfg"
 | 
			
		||||
import { PerfTimer } from "../perf"
 | 
			
		||||
import { ComponentResources, emitComponentResources, getComponentResources, getStaticResourcesFromPlugins } from "../plugins"
 | 
			
		||||
import {
 | 
			
		||||
  ComponentResources,
 | 
			
		||||
  emitComponentResources,
 | 
			
		||||
  getComponentResources,
 | 
			
		||||
  getStaticResourcesFromPlugins,
 | 
			
		||||
} from "../plugins"
 | 
			
		||||
import { EmitCallback } from "../plugins/types"
 | 
			
		||||
import { ProcessedContent } from "../plugins/vfile"
 | 
			
		||||
import { FilePath, QUARTZ, slugifyFilePath } from "../path"
 | 
			
		||||
import { globbyStream } from "globby"
 | 
			
		||||
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import spaRouterScript from '../components/scripts/spa.inline'
 | 
			
		||||
import spaRouterScript from "../components/scripts/spa.inline"
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import plausibleScript from '../components/scripts/plausible.inline'
 | 
			
		||||
import plausibleScript from "../components/scripts/plausible.inline"
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import popoverScript from '../components/scripts/popover.inline'
 | 
			
		||||
import popoverStyle from '../components/styles/popover.scss'
 | 
			
		||||
import popoverScript from "../components/scripts/popover.inline"
 | 
			
		||||
import popoverStyle from "../components/styles/popover.scss"
 | 
			
		||||
import { StaticResources } from "../resources"
 | 
			
		||||
import { QuartzLogger } from "../log"
 | 
			
		||||
import { googleFontHref } from "../theme"
 | 
			
		||||
import { trace } from "../trace"
 | 
			
		||||
 | 
			
		||||
function addGlobalPageResources(cfg: GlobalConfiguration, reloadScript: boolean, staticResources: StaticResources, componentResources: ComponentResources) {
 | 
			
		||||
function addGlobalPageResources(
 | 
			
		||||
  cfg: GlobalConfiguration,
 | 
			
		||||
  reloadScript: boolean,
 | 
			
		||||
  staticResources: StaticResources,
 | 
			
		||||
  componentResources: ComponentResources,
 | 
			
		||||
) {
 | 
			
		||||
  staticResources.css.push(googleFontHref(cfg.theme))
 | 
			
		||||
 | 
			
		||||
  // popovers
 | 
			
		||||
@@ -33,8 +43,8 @@ function addGlobalPageResources(cfg: GlobalConfiguration, reloadScript: boolean,
 | 
			
		||||
    const tagId = cfg.analytics.tagId
 | 
			
		||||
    staticResources.js.push({
 | 
			
		||||
      src: `https://www.googletagmanager.com/gtag/js?id=${tagId}`,
 | 
			
		||||
      contentType: 'external',
 | 
			
		||||
      loadTime: 'afterDOMReady',
 | 
			
		||||
      contentType: "external",
 | 
			
		||||
      loadTime: "afterDOMReady",
 | 
			
		||||
    })
 | 
			
		||||
    componentResources.afterDOMLoaded.push(`
 | 
			
		||||
    window.dataLayer = window.dataLayer || [];
 | 
			
		||||
@@ -47,8 +57,7 @@ function addGlobalPageResources(cfg: GlobalConfiguration, reloadScript: boolean,
 | 
			
		||||
        page_title: document.title,
 | 
			
		||||
        page_location: location.href,
 | 
			
		||||
      });
 | 
			
		||||
    });`
 | 
			
		||||
    )
 | 
			
		||||
    });`)
 | 
			
		||||
  } else if (cfg.analytics?.provider === "plausible") {
 | 
			
		||||
    componentResources.afterDOMLoaded.push(plausibleScript)
 | 
			
		||||
  }
 | 
			
		||||
@@ -60,8 +69,7 @@ function addGlobalPageResources(cfg: GlobalConfiguration, reloadScript: boolean,
 | 
			
		||||
    componentResources.afterDOMLoaded.push(`
 | 
			
		||||
      window.spaNavigate = (url, _) => window.location.assign(url)
 | 
			
		||||
      const event = new CustomEvent("nav", { detail: { slug: document.body.dataset.slug } })
 | 
			
		||||
      document.dispatchEvent(event)`
 | 
			
		||||
    )
 | 
			
		||||
      document.dispatchEvent(event)`)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (reloadScript) {
 | 
			
		||||
@@ -71,12 +79,19 @@ function addGlobalPageResources(cfg: GlobalConfiguration, reloadScript: boolean,
 | 
			
		||||
      script: `
 | 
			
		||||
        const socket = new WebSocket('ws://localhost:3001')
 | 
			
		||||
        socket.addEventListener('message', () => document.location.reload())
 | 
			
		||||
      `
 | 
			
		||||
      `,
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function emitContent(contentFolder: string, output: string, cfg: QuartzConfig, content: ProcessedContent[], reloadScript: boolean, verbose: boolean) {
 | 
			
		||||
export async function emitContent(
 | 
			
		||||
  contentFolder: string,
 | 
			
		||||
  output: string,
 | 
			
		||||
  cfg: QuartzConfig,
 | 
			
		||||
  content: ProcessedContent[],
 | 
			
		||||
  reloadScript: boolean,
 | 
			
		||||
  verbose: boolean,
 | 
			
		||||
) {
 | 
			
		||||
  const perf = new PerfTimer()
 | 
			
		||||
  const log = new QuartzLogger(verbose)
 | 
			
		||||
 | 
			
		||||
@@ -95,8 +110,8 @@ export async function emitContent(contentFolder: string, output: string, cfg: Qu
 | 
			
		||||
  // 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 
 | 
			
		||||
  // 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, reloadScript, staticResources, componentResources)
 | 
			
		||||
 | 
			
		||||
@@ -112,7 +127,13 @@ export async function emitContent(contentFolder: string, output: string, cfg: Qu
 | 
			
		||||
  // emitter plugins
 | 
			
		||||
  for (const emitter of cfg.plugins.emitters) {
 | 
			
		||||
    try {
 | 
			
		||||
      const emitted = await emitter.emit(contentFolder, cfg.configuration, content, staticResources, emit)
 | 
			
		||||
      const emitted = await emitter.emit(
 | 
			
		||||
        contentFolder,
 | 
			
		||||
        cfg.configuration,
 | 
			
		||||
        content,
 | 
			
		||||
        staticResources,
 | 
			
		||||
        emit,
 | 
			
		||||
      )
 | 
			
		||||
      emittedFiles += emitted.length
 | 
			
		||||
 | 
			
		||||
      if (verbose) {
 | 
			
		||||
@@ -141,7 +162,7 @@ export async function emitContent(contentFolder: string, output: string, cfg: Qu
 | 
			
		||||
    const fp = rawFp as FilePath
 | 
			
		||||
    const ext = path.extname(fp)
 | 
			
		||||
    const src = path.join(contentFolder, fp) as FilePath
 | 
			
		||||
    const name = slugifyFilePath(fp as FilePath) + ext as FilePath
 | 
			
		||||
    const name = (slugifyFilePath(fp as FilePath) + ext) as FilePath
 | 
			
		||||
    const dest = path.join(assetsPath, name) as FilePath
 | 
			
		||||
    const dir = path.dirname(dest) as FilePath
 | 
			
		||||
    await fs.promises.mkdir(dir, { recursive: true }) // ensure dir exists
 | 
			
		||||
 
 | 
			
		||||
@@ -2,14 +2,18 @@ import { PerfTimer } from "../perf"
 | 
			
		||||
import { QuartzFilterPluginInstance } from "../plugins/types"
 | 
			
		||||
import { ProcessedContent } from "../plugins/vfile"
 | 
			
		||||
 | 
			
		||||
export function filterContent(plugins: QuartzFilterPluginInstance[], content: ProcessedContent[], verbose: boolean): ProcessedContent[] {
 | 
			
		||||
export function filterContent(
 | 
			
		||||
  plugins: QuartzFilterPluginInstance[],
 | 
			
		||||
  content: ProcessedContent[],
 | 
			
		||||
  verbose: boolean,
 | 
			
		||||
): ProcessedContent[] {
 | 
			
		||||
  const perf = new PerfTimer()
 | 
			
		||||
  const initialLength = content.length
 | 
			
		||||
  for (const plugin of plugins) {
 | 
			
		||||
    const updatedContent = content.filter(plugin.shouldPublish)
 | 
			
		||||
 | 
			
		||||
    if (verbose) {
 | 
			
		||||
      const diff = content.filter(x => !updatedContent.includes(x))
 | 
			
		||||
      const diff = content.filter((x) => !updatedContent.includes(x))
 | 
			
		||||
      for (const file of diff) {
 | 
			
		||||
        console.log(`[filter:${plugin.name}] ${file[1].data.slug}`)
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,19 @@
 | 
			
		||||
import esbuild from 'esbuild'
 | 
			
		||||
import remarkParse from 'remark-parse'
 | 
			
		||||
import remarkRehype from 'remark-rehype'
 | 
			
		||||
import esbuild from "esbuild"
 | 
			
		||||
import remarkParse from "remark-parse"
 | 
			
		||||
import remarkRehype from "remark-rehype"
 | 
			
		||||
import { Processor, unified } from "unified"
 | 
			
		||||
import { Root as MDRoot } from 'remark-parse/lib'
 | 
			
		||||
import { Root as HTMLRoot } from 'hast'
 | 
			
		||||
import { ProcessedContent } from '../plugins/vfile'
 | 
			
		||||
import { PerfTimer } from '../perf'
 | 
			
		||||
import { read } from 'to-vfile'
 | 
			
		||||
import { FilePath, QUARTZ, ServerSlug, slugifyFilePath } from '../path'
 | 
			
		||||
import path from 'path'
 | 
			
		||||
import os from 'os'
 | 
			
		||||
import workerpool, { Promise as WorkerPromise } from 'workerpool'
 | 
			
		||||
import { QuartzTransformerPluginInstance } from '../plugins/types'
 | 
			
		||||
import { QuartzLogger } from '../log'
 | 
			
		||||
import { trace } from '../trace'
 | 
			
		||||
import { Root as MDRoot } from "remark-parse/lib"
 | 
			
		||||
import { Root as HTMLRoot } from "hast"
 | 
			
		||||
import { ProcessedContent } from "../plugins/vfile"
 | 
			
		||||
import { PerfTimer } from "../perf"
 | 
			
		||||
import { read } from "to-vfile"
 | 
			
		||||
import { FilePath, QUARTZ, ServerSlug, slugifyFilePath } from "../path"
 | 
			
		||||
import path from "path"
 | 
			
		||||
import os from "os"
 | 
			
		||||
import workerpool, { Promise as WorkerPromise } from "workerpool"
 | 
			
		||||
import { QuartzTransformerPluginInstance } from "../plugins/types"
 | 
			
		||||
import { QuartzLogger } from "../log"
 | 
			
		||||
import { trace } from "../trace"
 | 
			
		||||
 | 
			
		||||
export type QuartzProcessor = Processor<MDRoot, HTMLRoot, void>
 | 
			
		||||
export function createProcessor(transformers: QuartzTransformerPluginInstance[]): QuartzProcessor {
 | 
			
		||||
@@ -21,16 +21,15 @@ export function createProcessor(transformers: QuartzTransformerPluginInstance[])
 | 
			
		||||
  let processor = unified().use(remarkParse)
 | 
			
		||||
 | 
			
		||||
  // MD AST -> MD AST transforms
 | 
			
		||||
  for (const plugin of transformers.filter(p => p.markdownPlugins)) {
 | 
			
		||||
  for (const plugin of transformers.filter((p) => p.markdownPlugins)) {
 | 
			
		||||
    processor = processor.use(plugin.markdownPlugins!())
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // MD AST -> HTML AST
 | 
			
		||||
  processor = processor.use(remarkRehype, { allowDangerousHtml: true })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  // HTML AST -> HTML AST transforms
 | 
			
		||||
  for (const plugin of transformers.filter(p => p.htmlPlugins)) {
 | 
			
		||||
  for (const plugin of transformers.filter((p) => p.htmlPlugins)) {
 | 
			
		||||
    processor = processor.use(plugin.htmlPlugins!())
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -57,23 +56,29 @@ async function transpileWorkerScript() {
 | 
			
		||||
    packages: "external",
 | 
			
		||||
    plugins: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'css-and-scripts-as-text',
 | 
			
		||||
        name: "css-and-scripts-as-text",
 | 
			
		||||
        setup(build) {
 | 
			
		||||
          build.onLoad({ filter: /\.scss$/ }, (_) => ({
 | 
			
		||||
            contents: '',
 | 
			
		||||
            loader: 'text'
 | 
			
		||||
            contents: "",
 | 
			
		||||
            loader: "text",
 | 
			
		||||
          }))
 | 
			
		||||
          build.onLoad({ filter: /\.inline\.(ts|js)$/ }, (_) => ({
 | 
			
		||||
            contents: '',
 | 
			
		||||
            loader: 'text'
 | 
			
		||||
            contents: "",
 | 
			
		||||
            loader: "text",
 | 
			
		||||
          }))
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function createFileParser(transformers: QuartzTransformerPluginInstance[], baseDir: string, fps: FilePath[], allSlugs: ServerSlug[], verbose: boolean) {
 | 
			
		||||
export function createFileParser(
 | 
			
		||||
  transformers: QuartzTransformerPluginInstance[],
 | 
			
		||||
  baseDir: string,
 | 
			
		||||
  fps: FilePath[],
 | 
			
		||||
  allSlugs: ServerSlug[],
 | 
			
		||||
  verbose: boolean,
 | 
			
		||||
) {
 | 
			
		||||
  return async (processor: QuartzProcessor) => {
 | 
			
		||||
    const res: ProcessedContent[] = []
 | 
			
		||||
    for (const fp of fps) {
 | 
			
		||||
@@ -84,7 +89,7 @@ export function createFileParser(transformers: QuartzTransformerPluginInstance[]
 | 
			
		||||
        file.value = file.value.toString().trim()
 | 
			
		||||
 | 
			
		||||
        // Text -> Text transforms
 | 
			
		||||
        for (const plugin of transformers.filter(p => p.textTransform)) {
 | 
			
		||||
        for (const plugin of transformers.filter((p) => p.textTransform)) {
 | 
			
		||||
          file.value = plugin.textTransform!(file.value)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -110,7 +115,12 @@ export function createFileParser(transformers: QuartzTransformerPluginInstance[]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function parseMarkdown(transformers: QuartzTransformerPluginInstance[], baseDir: string, fps: FilePath[], verbose: boolean): Promise<ProcessedContent[]> {
 | 
			
		||||
export async function parseMarkdown(
 | 
			
		||||
  transformers: QuartzTransformerPluginInstance[],
 | 
			
		||||
  baseDir: string,
 | 
			
		||||
  fps: FilePath[],
 | 
			
		||||
  verbose: boolean,
 | 
			
		||||
): Promise<ProcessedContent[]> {
 | 
			
		||||
  const perf = new PerfTimer()
 | 
			
		||||
  const log = new QuartzLogger(verbose)
 | 
			
		||||
 | 
			
		||||
@@ -118,7 +128,9 @@ export async function parseMarkdown(transformers: QuartzTransformerPluginInstanc
 | 
			
		||||
  let concurrency = fps.length < CHUNK_SIZE ? 1 : os.availableParallelism()
 | 
			
		||||
 | 
			
		||||
  // get all slugs ahead of time as each thread needs a copy
 | 
			
		||||
  const allSlugs = fps.map(fp => slugifyFilePath(path.relative(baseDir, path.resolve(fp)) as FilePath))
 | 
			
		||||
  const allSlugs = fps.map((fp) =>
 | 
			
		||||
    slugifyFilePath(path.relative(baseDir, path.resolve(fp)) as FilePath),
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  let res: ProcessedContent[] = []
 | 
			
		||||
  log.start(`Parsing input files using ${concurrency} threads`)
 | 
			
		||||
@@ -128,18 +140,15 @@ export async function parseMarkdown(transformers: QuartzTransformerPluginInstanc
 | 
			
		||||
    res = await parse(processor)
 | 
			
		||||
  } else {
 | 
			
		||||
    await transpileWorkerScript()
 | 
			
		||||
    const pool = workerpool.pool(
 | 
			
		||||
      './quartz/bootstrap-worker.mjs',
 | 
			
		||||
      {
 | 
			
		||||
        minWorkers: 'max',
 | 
			
		||||
        maxWorkers: concurrency,
 | 
			
		||||
        workerType: 'thread'
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
    const pool = workerpool.pool("./quartz/bootstrap-worker.mjs", {
 | 
			
		||||
      minWorkers: "max",
 | 
			
		||||
      maxWorkers: concurrency,
 | 
			
		||||
      workerType: "thread",
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const childPromises: WorkerPromise<ProcessedContent[]>[] = []
 | 
			
		||||
    for (const chunk of chunks(fps, CHUNK_SIZE)) {
 | 
			
		||||
      childPromises.push(pool.exec('parseFiles', [baseDir, chunk, allSlugs, verbose]))
 | 
			
		||||
      childPromises.push(pool.exec("parseFiles", [baseDir, chunk, allSlugs, verbose]))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const results: ProcessedContent[][] = await WorkerPromise.all(childPromises)
 | 
			
		||||
 
 | 
			
		||||
@@ -2,29 +2,38 @@ import { randomUUID } from "crypto"
 | 
			
		||||
import { JSX } from "preact/jsx-runtime"
 | 
			
		||||
 | 
			
		||||
export type JSResource = {
 | 
			
		||||
  loadTime: 'beforeDOMReady' | 'afterDOMReady'
 | 
			
		||||
  moduleType?: 'module',
 | 
			
		||||
  loadTime: "beforeDOMReady" | "afterDOMReady"
 | 
			
		||||
  moduleType?: "module"
 | 
			
		||||
  spaPreserve?: boolean
 | 
			
		||||
} & ({
 | 
			
		||||
  src: string
 | 
			
		||||
  contentType: 'external'
 | 
			
		||||
} | {
 | 
			
		||||
  script: string
 | 
			
		||||
  contentType: 'inline'
 | 
			
		||||
})
 | 
			
		||||
} & (
 | 
			
		||||
  | {
 | 
			
		||||
      src: string
 | 
			
		||||
      contentType: "external"
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      script: string
 | 
			
		||||
      contentType: "inline"
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export function JSResourceToScriptElement(resource: JSResource, preserve?: boolean): JSX.Element {
 | 
			
		||||
  const scriptType = resource.moduleType ?? 'application/javascript'
 | 
			
		||||
  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={spaPreserve}/>
 | 
			
		||||
  if (resource.contentType === "external") {
 | 
			
		||||
    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={spaPreserve}>{content}</script>
 | 
			
		||||
    return (
 | 
			
		||||
      <script key={randomUUID()} type={scriptType} spa-preserve={spaPreserve}>
 | 
			
		||||
        {content}
 | 
			
		||||
      </script>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface StaticResources {
 | 
			
		||||
  css: string[],
 | 
			
		||||
  css: string[]
 | 
			
		||||
  js: JSResource[]
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,17 @@ body {
 | 
			
		||||
  border-radius: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
p, ul, text, a, tr, td, li, ol, ul, .katex, .math {
 | 
			
		||||
p,
 | 
			
		||||
ul,
 | 
			
		||||
text,
 | 
			
		||||
a,
 | 
			
		||||
tr,
 | 
			
		||||
td,
 | 
			
		||||
li,
 | 
			
		||||
ol,
 | 
			
		||||
ul,
 | 
			
		||||
.katex,
 | 
			
		||||
.math {
 | 
			
		||||
  color: var(--darkgray);
 | 
			
		||||
  fill: var(--darkgray);
 | 
			
		||||
}
 | 
			
		||||
@@ -79,7 +89,7 @@ a {
 | 
			
		||||
      font-size: 2rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    & li:has(> input[type='checkbox']) {
 | 
			
		||||
    & li:has(> input[type="checkbox"]) {
 | 
			
		||||
      list-style-type: none;
 | 
			
		||||
      padding-left: 0;
 | 
			
		||||
      margin-left: -1.4rem;
 | 
			
		||||
@@ -144,7 +154,8 @@ a {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  & .center, & footer {
 | 
			
		||||
  & .center,
 | 
			
		||||
  & footer {
 | 
			
		||||
    width: $pageWidth;
 | 
			
		||||
    margin-left: auto;
 | 
			
		||||
    margin-right: auto;
 | 
			
		||||
@@ -195,9 +206,12 @@ thead {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
h1, h2, h3, h4, h5, h6 {
 | 
			
		||||
h1,
 | 
			
		||||
h2,
 | 
			
		||||
h3,
 | 
			
		||||
h4,
 | 
			
		||||
h5,
 | 
			
		||||
h6 {
 | 
			
		||||
  &[id] > a[href^="#"] {
 | 
			
		||||
    margin: 0 0.5rem;
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
@@ -277,11 +291,11 @@ pre {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &[data-line-numbers-max-digits='2'] > [data-line]::before {
 | 
			
		||||
    &[data-line-numbers-max-digits="2"] > [data-line]::before {
 | 
			
		||||
      width: 2rem;
 | 
			
		||||
    }
 | 
			
		||||
     
 | 
			
		||||
    &[data-line-numbers-max-digits='3'] > [data-line]::before {
 | 
			
		||||
 | 
			
		||||
    &[data-line-numbers-max-digits="3"] > [data-line]::before {
 | 
			
		||||
      width: 3rem;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -296,7 +310,9 @@ code {
 | 
			
		||||
  background: var(--lightgray);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tbody, li, p {
 | 
			
		||||
tbody,
 | 
			
		||||
li,
 | 
			
		||||
p {
 | 
			
		||||
  line-height: 1.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -307,7 +323,8 @@ table {
 | 
			
		||||
  border-collapse: collapse;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
td, th {
 | 
			
		||||
td,
 | 
			
		||||
th {
 | 
			
		||||
  padding: 0.2rem 1rem;
 | 
			
		||||
  border: 1px solid var(--gray);
 | 
			
		||||
}
 | 
			
		||||
@@ -331,7 +348,8 @@ hr {
 | 
			
		||||
  background-color: var(--lightgray);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
audio, video {
 | 
			
		||||
audio,
 | 
			
		||||
video {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  border-radius: 5px;
 | 
			
		||||
}
 | 
			
		||||
@@ -340,7 +358,8 @@ audio, video {
 | 
			
		||||
  flex: 1 1 auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ul.overflow, ol.overflow {
 | 
			
		||||
ul.overflow,
 | 
			
		||||
ol.overflow {
 | 
			
		||||
  height: 400px;
 | 
			
		||||
  overflow-y: scroll;
 | 
			
		||||
 | 
			
		||||
@@ -354,9 +373,9 @@ ul.overflow, ol.overflow {
 | 
			
		||||
 | 
			
		||||
  &:after {
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
    content: '';
 | 
			
		||||
    content: "";
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 50px;    
 | 
			
		||||
    height: 50px;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,104 +1,104 @@
 | 
			
		||||
@use "sass:color";
 | 
			
		||||
 | 
			
		||||
.callout {
 | 
			
		||||
	border: 1px solid var(--border);
 | 
			
		||||
	background-color: var(--bg);
 | 
			
		||||
	border-radius: 5px;
 | 
			
		||||
	padding: 0 1rem;
 | 
			
		||||
	overflow-y: hidden;
 | 
			
		||||
  border: 1px solid var(--border);
 | 
			
		||||
  background-color: var(--bg);
 | 
			
		||||
  border-radius: 5px;
 | 
			
		||||
  padding: 0 1rem;
 | 
			
		||||
  overflow-y: hidden;
 | 
			
		||||
  transition: max-height 0.3s ease;
 | 
			
		||||
 | 
			
		||||
  & > *:nth-child(2) {
 | 
			
		||||
    margin-top: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
	&[data-callout="note"] {
 | 
			
		||||
	  --color: #448aff;
 | 
			
		||||
	  --border: #448aff22;
 | 
			
		||||
	  --bg: #448aff09;
 | 
			
		||||
	}
 | 
			
		||||
  &[data-callout="note"] {
 | 
			
		||||
    --color: #448aff;
 | 
			
		||||
    --border: #448aff22;
 | 
			
		||||
    --bg: #448aff09;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
	&[data-callout="abstract"] {
 | 
			
		||||
	  --color: #00b0ff;
 | 
			
		||||
	  --border: #00b0ff22;
 | 
			
		||||
	  --bg: #00b0ff09;
 | 
			
		||||
	}
 | 
			
		||||
  &[data-callout="abstract"] {
 | 
			
		||||
    --color: #00b0ff;
 | 
			
		||||
    --border: #00b0ff22;
 | 
			
		||||
    --bg: #00b0ff09;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
	&[data-callout="info"], &[data-callout="todo"] {
 | 
			
		||||
	  --color: #00b8d4;
 | 
			
		||||
	  --border: #00b8d422;
 | 
			
		||||
	  --bg: #00b8d409;
 | 
			
		||||
	}
 | 
			
		||||
  &[data-callout="info"],
 | 
			
		||||
  &[data-callout="todo"] {
 | 
			
		||||
    --color: #00b8d4;
 | 
			
		||||
    --border: #00b8d422;
 | 
			
		||||
    --bg: #00b8d409;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
	&[data-callout="tip"] {
 | 
			
		||||
	  --color: #00bfa5;
 | 
			
		||||
	  --border: #00bfa522;
 | 
			
		||||
	  --bg: #00bfa509;
 | 
			
		||||
	}
 | 
			
		||||
  &[data-callout="tip"] {
 | 
			
		||||
    --color: #00bfa5;
 | 
			
		||||
    --border: #00bfa522;
 | 
			
		||||
    --bg: #00bfa509;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
	&[data-callout="success"] {
 | 
			
		||||
	  --color: #09ad7a;
 | 
			
		||||
	  --border: #09ad7122;
 | 
			
		||||
	  --bg: #09ad7109;
 | 
			
		||||
	}
 | 
			
		||||
  &[data-callout="success"] {
 | 
			
		||||
    --color: #09ad7a;
 | 
			
		||||
    --border: #09ad7122;
 | 
			
		||||
    --bg: #09ad7109;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
	&[data-callout="question"] {
 | 
			
		||||
	  --color: #dba642;
 | 
			
		||||
	  --border: #dba64222;
 | 
			
		||||
	  --bg: #dba64209;
 | 
			
		||||
	}
 | 
			
		||||
  &[data-callout="question"] {
 | 
			
		||||
    --color: #dba642;
 | 
			
		||||
    --border: #dba64222;
 | 
			
		||||
    --bg: #dba64209;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
	&[data-callout="warning"] {
 | 
			
		||||
	  --color: #db8942;
 | 
			
		||||
	  --border: #db894222;
 | 
			
		||||
	  --bg: #db894209;
 | 
			
		||||
	}
 | 
			
		||||
  &[data-callout="warning"] {
 | 
			
		||||
    --color: #db8942;
 | 
			
		||||
    --border: #db894222;
 | 
			
		||||
    --bg: #db894209;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
	&[data-callout="failure"], &[data-callout="danger"], &[data-callout="bug"] {
 | 
			
		||||
	  --color: #db4242;
 | 
			
		||||
	  --border: #db424222;
 | 
			
		||||
	  --bg: #db424209;
 | 
			
		||||
	}
 | 
			
		||||
  &[data-callout="failure"],
 | 
			
		||||
  &[data-callout="danger"],
 | 
			
		||||
  &[data-callout="bug"] {
 | 
			
		||||
    --color: #db4242;
 | 
			
		||||
    --border: #db424222;
 | 
			
		||||
    --bg: #db424209;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
	&[data-callout="example"] {
 | 
			
		||||
	  --color: #7a43b5;
 | 
			
		||||
	  --border: #7a43b522;
 | 
			
		||||
	  --bg: #7a43b509;
 | 
			
		||||
	}
 | 
			
		||||
  &[data-callout="example"] {
 | 
			
		||||
    --color: #7a43b5;
 | 
			
		||||
    --border: #7a43b522;
 | 
			
		||||
    --bg: #7a43b509;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &[data-callout="quote"] {
 | 
			
		||||
    --color: var(--secondary);
 | 
			
		||||
    --border: var(--lightgray);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
	&[data-callout="quote"] {
 | 
			
		||||
	  --color: var(--secondary);
 | 
			
		||||
	  --border: var(--lightgray);
 | 
			
		||||
	}
 | 
			
		||||
  
 | 
			
		||||
  &.is-collapsed > .callout-title > .fold {
 | 
			
		||||
    transform: rotateZ(-90deg)
 | 
			
		||||
    transform: rotateZ(-90deg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.callout-title {
 | 
			
		||||
	display: flex;
 | 
			
		||||
	align-items: center;
 | 
			
		||||
	gap: 5px;
 | 
			
		||||
	padding: 1rem 0;
 | 
			
		||||
	color: var(--color);
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  gap: 5px;
 | 
			
		||||
  padding: 1rem 0;
 | 
			
		||||
  color: var(--color);
 | 
			
		||||
 | 
			
		||||
	& .fold {
 | 
			
		||||
    margin-left: 0.5rem; 
 | 
			
		||||
  & .fold {
 | 
			
		||||
    margin-left: 0.5rem;
 | 
			
		||||
    transition: transform 0.3s ease;
 | 
			
		||||
    opacity: 0.8;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.callout-icon {
 | 
			
		||||
	width: 18px;
 | 
			
		||||
	height: 18px;
 | 
			
		||||
  width: 18px;
 | 
			
		||||
  height: 18px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.callout-title-inner {
 | 
			
		||||
	font-weight: 700;
 | 
			
		||||
  font-weight: 700;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,4 +3,4 @@ $mobileBreakpoint: 600px;
 | 
			
		||||
$tabletBreakpoint: 1200px;
 | 
			
		||||
$sidePanelWidth: 400px;
 | 
			
		||||
$topSpacing: 6rem;
 | 
			
		||||
$fullPageWidth: $pageWidth + 2 * $sidePanelWidth
 | 
			
		||||
$fullPageWidth: $pageWidth + 2 * $sidePanelWidth;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,27 +1,28 @@
 | 
			
		||||
export interface ColorScheme {
 | 
			
		||||
  light: string,
 | 
			
		||||
  lightgray: string,
 | 
			
		||||
  gray: string,
 | 
			
		||||
  darkgray: string,
 | 
			
		||||
  dark: string,
 | 
			
		||||
  secondary: string,
 | 
			
		||||
  tertiary: string,
 | 
			
		||||
  light: string
 | 
			
		||||
  lightgray: string
 | 
			
		||||
  gray: string
 | 
			
		||||
  darkgray: string
 | 
			
		||||
  dark: string
 | 
			
		||||
  secondary: string
 | 
			
		||||
  tertiary: string
 | 
			
		||||
  highlight: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Theme {
 | 
			
		||||
  typography: {
 | 
			
		||||
    header: string,
 | 
			
		||||
    body: string,
 | 
			
		||||
    header: string
 | 
			
		||||
    body: string
 | 
			
		||||
    code: string
 | 
			
		||||
  },
 | 
			
		||||
  }
 | 
			
		||||
  colors: {
 | 
			
		||||
    lightMode: ColorScheme,
 | 
			
		||||
    lightMode: ColorScheme
 | 
			
		||||
    darkMode: ColorScheme
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const DEFAULT_SANS_SERIF = "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif"
 | 
			
		||||
const DEFAULT_SANS_SERIF =
 | 
			
		||||
  '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif'
 | 
			
		||||
const DEFAULT_MONO = "ui-monospace, SFMono-Regular, SF Mono, Menlo, monospace"
 | 
			
		||||
export function googleFontHref(theme: Theme) {
 | 
			
		||||
  const { code, header, body } = theme.typography
 | 
			
		||||
 
 | 
			
		||||
@@ -4,13 +4,17 @@ const rootFile = /.*at file:/
 | 
			
		||||
export function trace(msg: string, err: Error) {
 | 
			
		||||
  const stack = err.stack
 | 
			
		||||
  console.log()
 | 
			
		||||
  console.log(chalk.bgRed.white.bold(" ERROR ") + chalk.red(` ${msg}`) + (err.message.length > 0 ? `: ${err.message}` : ""))
 | 
			
		||||
  console.log(
 | 
			
		||||
    chalk.bgRed.white.bold(" ERROR ") +
 | 
			
		||||
      chalk.red(` ${msg}`) +
 | 
			
		||||
      (err.message.length > 0 ? `: ${err.message}` : ""),
 | 
			
		||||
  )
 | 
			
		||||
  if (!stack) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let reachedEndOfLegibleTrace = false
 | 
			
		||||
  for (const line of stack.split('\n').slice(1)) {
 | 
			
		||||
  for (const line of stack.split("\n").slice(1)) {
 | 
			
		||||
    if (reachedEndOfLegibleTrace) {
 | 
			
		||||
      break
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,12 @@ const transformers = config.plugins.transformers
 | 
			
		||||
const processor = createProcessor(transformers)
 | 
			
		||||
 | 
			
		||||
// only called from worker thread
 | 
			
		||||
export async function parseFiles(baseDir: string, fps: FilePath[], allSlugs: ServerSlug[], verbose: boolean) {
 | 
			
		||||
export async function parseFiles(
 | 
			
		||||
  baseDir: string,
 | 
			
		||||
  fps: FilePath[],
 | 
			
		||||
  allSlugs: ServerSlug[],
 | 
			
		||||
  verbose: boolean,
 | 
			
		||||
) {
 | 
			
		||||
  const parse = createFileParser(transformers, baseDir, fps, allSlugs, verbose)
 | 
			
		||||
  return parse(processor)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user