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