docs + various polish

This commit is contained in:
Jacky Zhao
2023-07-09 19:32:24 -07:00
parent 061ac027f5
commit 7c7e1f8dd5
49 changed files with 1365 additions and 196 deletions

View File

@ -54,21 +54,16 @@ export default async function buildQuartz(argv: Argv, version: string) {
if (argv.serve) {
const server = http.createServer(async (req, res) => {
let status = 200
const result = await serveHandler(req, res, {
await serveHandler(req, res, {
public: output,
directoryListing: false,
}, {
async sendError() {
status = 404
},
})
const status = res.statusCode
const statusString = status === 200 ? chalk.green(`[${status}]`) : chalk.red(`[${status}]`)
console.log(statusString + chalk.grey(` ${req.url}`))
return result
})
server.listen(argv.port)
console.log(`Started a Quartz server listening at http://localhost:${argv.port}`)
console.log(chalk.cyan(`Started a Quartz server listening at http://localhost:${argv.port}`))
console.log('hint: exit with ctrl+c')
}
}

View File

@ -2,7 +2,8 @@ import { QuartzComponent } from "./components/types"
import { PluginTypes } from "./plugins/types"
import { Theme } from "./theme"
export type Analytics = null
export type Analytics =
| null
| {
provider: 'plausible'
}
@ -18,7 +19,7 @@ export interface GlobalConfiguration {
/** Whether to display Wikipedia-style popovers when hovering over links */
enablePopovers: boolean,
/** Analytics mode */
analytics: Analytics
analytics: Analytics
/** Glob patterns to not search */
ignorePatterns: string[],
/** Base URL to use for CNAME files, sitemaps, and RSS feeds that require an absolute URL.

View File

@ -1,19 +1,18 @@
import { QuartzComponentConstructor } from "./types"
import style from "./styles/footer.scss"
import {version} from "../../package.json"
interface Options {
authorName: string,
links: Record<string, string>
}
export default ((opts?: Options) => {
function Footer() {
const year = new Date().getFullYear()
const name = opts?.authorName ?? "someone"
const links = opts?.links ?? []
return <footer>
<hr />
<p>Made by {name} using <a href="https://quartz.jzhao.xyz/">Quartz</a>, © {year}</p>
<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>

View File

@ -16,7 +16,7 @@ export interface D3Config {
}
interface GraphOptions {
localGraph: Partial<D3Config>,
localGraph: Partial<D3Config> | undefined,
globalGraph: Partial<D3Config> | undefined
}
@ -50,7 +50,7 @@ export default ((opts?: GraphOptions) => {
const localGraph = { ...opts?.localGraph, ...defaultOptions.localGraph }
const globalGraph = { ...opts?.globalGraph, ...defaultOptions.globalGraph }
return <div class="graph">
<h3>Site Graph</h3>
<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"

View File

@ -5,10 +5,11 @@ import path from "path"
import style from '../styles/listPage.scss'
import { PageList } from "../PageList"
import { clientSideSlug } from "../../path"
function FolderContent(props: QuartzComponentProps) {
const { tree, fileData, allFiles } = props
const folderSlug = fileData.slug!
const folderSlug = clientSideSlug(fileData.slug!)
const allPagesInFolder = allFiles.filter(file => {
const fileSlug = file.slug ?? ""
const prefixed = fileSlug.startsWith(folderSlug)
@ -23,12 +24,9 @@ function FolderContent(props: QuartzComponentProps) {
allFiles: allPagesInFolder
}
const desc = props.fileData.description
// @ts-ignore
const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
return <div class="popover-hint">
{desc && <p>{desc}</p>}
<article>{content}</article>
<p>{allPagesInFolder.length} items under this folder.</p>
<div>

View File

@ -17,12 +17,9 @@ function TagContent(props: QuartzComponentProps) {
allFiles: allPagesWithTag
}
const desc = props.fileData.description
// @ts-ignore
const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
return <div class="popover-hint">
{desc && <p>{desc}</p>}
<article>{content}</article>
<p>{allPagesWithTag.length} items with this tag.</p>
<div>

View File

@ -72,7 +72,7 @@ async function renderGraph(container: string, slug: string) {
}
}
} else {
links.flatMap(l => [l.source, l.target]).forEach((id) => neighbourhood.add(id))
Object.keys(data).forEach(id => neighbourhood.add(id))
}
const graphData: { nodes: NodeData[], links: LinkData[] } = {

View File

@ -10,6 +10,7 @@ interface Item {
let index: Document<Item> | undefined = undefined
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)
@ -134,7 +135,7 @@ document.addEventListener("nav", async (e: unknown) => {
function onType(e: HTMLElementEventMap["input"]) {
const term = (e.target as HTMLInputElement).value
const searchResults = index?.search(term, 5) ?? []
const searchResults = index?.search(term, numSearchResults) ?? []
const getByField = (field: string): string[] => {
const results = searchResults.filter((x) => x.field === field)
return results.length === 0 ? [] : [...results[0].result] as string[]

View File

@ -1,6 +1,7 @@
footer {
text-align: left;
margin-bottom: 4rem;
opacity: 0.7;
& ul {
list-style: none;

View File

@ -1,3 +1,5 @@
@use "../../styles/variables.scss" as *;
.graph {
& > h3 {
font-size: 1rem;
@ -59,6 +61,10 @@
transform: translate(-50%, -50%);
height: 60vh;
width: 50vw;
@media all and (max-width: $fullPageWidth) {
width: 90%;
}
}
}
}

View File

@ -23,8 +23,8 @@
& > .popover-inner {
position: relative;
width: 30rem;
height: 20rem;
padding: 0 1rem 1rem 1rem;
max-height: 20rem;
padding: 0 1rem 2rem 1rem;
font-weight: initial;
line-height: normal;
font-size: initial;

View File

@ -31,6 +31,10 @@ button#toc {
max-height: none;
transition: max-height 0.5s ease;
&.collapsed > .overflow::after {
opacity: 0;
}
& ul {
list-style: none;
margin: 0.5rem 0;

View File

@ -100,6 +100,7 @@ declare module 'vfile' {
// inserted in processors.ts
interface DataMap {
slug: string
allSlugs: string[]
filePath: string
}
}

View File

@ -2,6 +2,7 @@ import matter from "gray-matter"
import remarkFrontmatter from 'remark-frontmatter'
import { QuartzTransformerPlugin } from "../types"
import yaml from 'js-yaml'
import { slug as slugAnchor } from 'github-slugger'
export interface Options {
language: 'yaml' | 'toml',
@ -29,10 +30,18 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined>
}
})
// tag is an alias for tags
if (data.tag) {
data.tags = data.tag
}
if (data.tags && !Array.isArray(data.tags)) {
data.tags = data.tags.toString().split(",").map((tag: string) => tag.trim())
}
// slug them all!!
data.tags = data.tags?.map((tag: string) => slugAnchor(tag)) ?? []
// fill in frontmatter
file.data.frontmatter = {
title: file.stem ?? "Untitled",

View File

@ -1,7 +1,7 @@
export { FrontMatter } from './frontmatter'
export { GitHubFlavoredMarkdown } from './gfm'
export { CreatedModifiedDate } from './lastmod'
export { Katex } from './latex'
export { Latex } from './latex'
export { Description } from './description'
export { CrawlLinks } from './links'
export { ObsidianFlavoredMarkdown } from './ofm'

View File

@ -1,33 +1,43 @@
import remarkMath from "remark-math"
import rehypeKatex from 'rehype-katex'
import rehypeMathjax from 'rehype-mathjax/svg.js'
import { QuartzTransformerPlugin } from "../types"
export const Katex: QuartzTransformerPlugin = () => ({
name: "Katex",
markdownPlugins() {
return [remarkMath]
},
htmlPlugins() {
return [
[rehypeKatex, {
output: 'html',
}]
]
},
externalResources() {
return {
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'
}
interface Options {
renderEngine: 'katex' | 'mathjax'
}
export const Latex: QuartzTransformerPlugin<Options> = (opts?: Options) => {
const engine = opts?.renderEngine ?? 'katex'
return {
name: "Latex",
markdownPlugins() {
return [remarkMath]
},
htmlPlugins() {
return [
engine === 'katex'
? [rehypeKatex, { output: 'html' }]
: [rehypeMathjax]
]
},
externalResources() {
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'
}
]
}
: {}
}
}
})
}

View File

@ -29,13 +29,30 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
return (tree, file) => {
const curSlug = clientSideSlug(file.data.slug!)
const transformLink = (target: string) => {
const targetSlug = slugify(decodeURI(target).trim())
const targetSlug = clientSideSlug(slugify(decodeURI(target).trim()))
if (opts.markdownLinkResolution === 'relative' && !path.isAbsolute(targetSlug)) {
return './' + relative(curSlug, targetSlug)
} else {
return './' + relativeToRoot(curSlug, targetSlug)
} 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 = clientSideSlug(slug).split(path.posix.sep)
const fileName = parts.at(-1)
return targetSlug === fileName
})
if (matchingFileNames.length === 1) {
const targetSlug = clientSideSlug(matchingFileNames[0])
return './' + relativeToRoot(curSlug, targetSlug)
}
// if it's not unique, then it's the absolute path from the vault root
// (fall-through case)
}
// todo: handle shortest path
// treat as absolute
return './' + relativeToRoot(curSlug, targetSlug)
}
const outgoing: Set<string> = new Set()
@ -53,7 +70,7 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
if (!(isAbsoluteUrl(dest) || dest.startsWith("#"))) {
node.properties.href = transformLink(dest)
}
dest = node.properties.href
if (dest.startsWith(".")) {
const normalizedPath = path.normalize(path.join(curSlug, dest))

View File

@ -12,6 +12,7 @@ import { JSResource } from "../../resources"
import calloutScript from "../../components/scripts/callout.inline.ts"
export interface Options {
comments: boolean
highlight: boolean
wikilinks: boolean
callouts: boolean
@ -19,6 +20,7 @@ export interface Options {
}
const defaultOptions: Options = {
comments: true,
highlight: true,
wikilinks: true,
callouts: true,
@ -101,11 +103,14 @@ const capitalize = (s: string): string => {
// ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name)
// (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link)
// (|[^\[\]\|\#]+)? -> | then one or more non-special characters (alias)
const backlinkRegex = new RegExp(/!?\[\[([^\[\]\|\#]+)(#[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/, "g")
const wikilinkRegex = new RegExp(/!?\[\[([^\[\]\|\#]+)(#[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/, "g")
// Match highlights
const highlightRegex = new RegExp(/==(.+)==/, "g")
// 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+)\]([+-]?)/)
@ -117,7 +122,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
// pre-transform wikilinks (fix anchors to things that may contain illegal syntax e.g. codeblocks, latex)
if (opts.wikilinks) {
src = src.toString()
return src.replaceAll(backlinkRegex, (value, ...capture) => {
return src.replaceAll(wikilinkRegex, (value, ...capture) => {
const [fp, rawHeader, rawAlias] = capture
const anchor = rawHeader?.trim().slice(1)
const displayAnchor = anchor ? `#${slugAnchor(anchor)}` : ""
@ -133,7 +138,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
if (opts.wikilinks) {
plugins.push(() => {
return (tree: Root, _file) => {
findAndReplace(tree, backlinkRegex, (value: string, ...capture: string[]) => {
findAndReplace(tree, wikilinkRegex, (value: string, ...capture: string[]) => {
const [fp, rawHeader, rawAlias] = capture
const anchor = rawHeader?.trim() ?? ""
const alias = rawAlias?.slice(1).trim()
@ -204,6 +209,19 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
}
})
}
if (opts.comments) {
plugins.push(() => {
return (tree: Root, _file) => {
findAndReplace(tree, commentRegex, (_value: string, ..._capture: string[]) => {
return {
type: 'text',
value: ''
}
})
}
})
}
if (opts.callouts) {
plugins.push(() => {

View File

@ -6,19 +6,6 @@ export const SyntaxHighlighting: QuartzTransformerPlugin = () => ({
htmlPlugins() {
return [[rehypePrettyCode, {
theme: 'css-variables',
onVisitLine(node) {
if (node.children.length === 0) {
node.children = [{ type: 'text', value: ' ' }]
}
},
onVisitHighlightedLine(node) {
node.properties.className ??= []
node.properties.className.push('highlighted')
},
onVisitHighlightedWord(node) {
node.properties.className ??= []
node.properties.className.push('word')
},
} satisfies Partial<CodeOptions>]]
}
})

View File

@ -73,7 +73,7 @@ async function transpileWorkerScript() {
})
}
export function createFileParser(transformers: QuartzTransformerPluginInstance[], baseDir: string, fps: string[], verbose: boolean) {
export function createFileParser(transformers: QuartzTransformerPluginInstance[], baseDir: string, fps: string[], allSlugs: string[], verbose: boolean) {
return async (processor: QuartzProcessor) => {
const res: ProcessedContent[] = []
for (const fp of fps) {
@ -90,6 +90,7 @@ export function createFileParser(transformers: QuartzTransformerPluginInstance[]
// base data properties that plugins may use
file.data.slug = slugify(path.relative(baseDir, file.path))
file.data.allSlugs = allSlugs
file.data.filePath = fp
const ast = processor.parse(file)
@ -115,12 +116,16 @@ export async function parseMarkdown(transformers: QuartzTransformerPluginInstanc
const CHUNK_SIZE = 128
let concurrency = fps.length < CHUNK_SIZE ? 1 : os.availableParallelism()
let res: ProcessedContent[] = []
// get all slugs ahead of time as each thread needs a copy
// const slugs: string[] = fps.map(fp => slugify(path))
const allSlugs = fps.map(fp => slugify(path.relative(baseDir, path.resolve(fp))))
let res: ProcessedContent[] = []
log.start(`Parsing input files using ${concurrency} threads`)
if (concurrency === 1) {
const processor = createProcessor(transformers)
const parse = createFileParser(transformers, baseDir, fps, verbose)
const parse = createFileParser(transformers, baseDir, fps, allSlugs, verbose)
res = await parse(processor)
} else {
await transpileWorkerScript()
@ -135,7 +140,7 @@ export async function parseMarkdown(transformers: QuartzTransformerPluginInstanc
const childPromises: WorkerPromise<ProcessedContent[]>[] = []
for (const chunk of chunks(fps, CHUNK_SIZE)) {
childPromises.push(pool.exec('parseFiles', [baseDir, chunk, verbose]))
childPromises.push(pool.exec('parseFiles', [baseDir, chunk, allSlugs, verbose]))
}
const results: ProcessedContent[][] = await WorkerPromise.all(childPromises)

View File

@ -15,16 +15,23 @@ body {
}
.text-highlight {
background-color: #fff236aa;
background-color: #fff23688;
padding: 0 0.1rem;
border-radius: 5px;
}
p, ul, text, a, tr, td, li, ol, ul, .katex {
p, ul, text, a, tr, td, li, ol, ul, .katex, .math {
color: var(--darkgray);
fill: var(--darkgray);
}
.math {
font-size: 1.1rem;
&.math-display {
text-align: center;
}
}
a {
font-weight: 600;
text-decoration: none;
@ -76,6 +83,10 @@ a {
padding-left: 0;
margin-left: -1.4rem;
}
& li > * {
margin: 0;
}
}
& > #quartz-body {
@ -144,6 +155,11 @@ a {
}
}
.footnotes {
margin-top: 2rem;
border-top: 1px solid var(--lightgray);
}
input[type="checkbox"] {
transform: translateY(2px);
color: var(--secondary);
@ -168,7 +184,7 @@ thead {
font-family: var(--headerFont);
color: var(--dark);
font-weight: revert;
margin: 2rem 0 0;
margin-bottom: 0;
article > & > a {
color: var(--dark);
@ -178,6 +194,8 @@ thead {
}
}
h1, h2, h3, h4, h5, h6 {
&[id] > a[href^="#"] {
margin: 0 0.5rem;
@ -200,13 +218,17 @@ div[data-rehype-pretty-code-fragment] {
& > div[data-rehype-pretty-code-title] {
font-family: var(--codeFont);
font-size: 0.9rem;
padding: 0.1rem 0.8rem;
padding: 0.1rem 0.5rem;
border: 1px solid var(--lightgray);
width: max-content;
border-radius: 5px;
margin-bottom: -0.8rem;
margin-bottom: -0.5rem;
color: var(--darkgray);
}
& > pre {
padding: 0.5rem 0;
}
}
pre {
@ -228,12 +250,17 @@ pre {
counter-increment: line 0;
display: grid;
& .line {
& [data-highlighted-chars] {
background-color: var(--highlight);
border-radius: 5px;
}
& > [data-line] {
padding: 0 0.25rem;
box-sizing: border-box;
border-left: 3px solid transparent;
&.highlighted {
&[data-highlighted-line] {
background-color: var(--highlight);
border-left: 3px solid var(--secondary);
}
@ -245,9 +272,17 @@ pre {
margin-right: 1rem;
display: inline-block;
text-align: right;
color: rgba(115, 138, 148, 0.4);
color: rgba(115, 138, 148, 0.6);
}
}
&[data-line-numbers-max-digits='2'] > [data-line]::before {
width: 2rem;
}
&[data-line-numbers-max-digits='3'] > [data-line]::before {
width: 3rem;
}
}
}
@ -265,6 +300,7 @@ tbody, li, p {
}
table {
margin: 1rem 0;
border: 1px solid var(--gray);
padding: 1.5rem;
border-collapse: collapse;
@ -294,21 +330,6 @@ hr {
background-color: var(--lightgray);
}
section {
margin: 2rem auto;
border-top: 1px solid var(--lightgray);
& > #footnote-label {
& > a {
color: var(--dark);
}
}
& ol, & ul {
padding: 0 1em
}
}
audio, video {
width: 100%;
border-radius: 5px;
@ -322,6 +343,10 @@ ul.overflow, ol.overflow {
height: 400px;
overflow-y: scroll;
// clearfix
content: "";
clear: both;
& > li:last-of-type {
margin-bottom: 50px;
}
@ -334,6 +359,8 @@ ul.overflow, ol.overflow {
position: absolute;
left: 0;
bottom: 0;
opacity: 1;
transition: opacity 0.3s ease;
background: linear-gradient(transparent 0px, var(--light));
}
}

View File

@ -5,7 +5,7 @@ const transformers = config.plugins.transformers
const processor = createProcessor(transformers)
// only called from worker thread
export async function parseFiles(baseDir: string, fps: string[], verbose: boolean) {
const parse = createFileParser(transformers, baseDir, fps, verbose)
export async function parseFiles(baseDir: string, fps: string[], allSlugs: string[], verbose: boolean) {
const parse = createFileParser(transformers, baseDir, fps, allSlugs, verbose)
return parse(processor)
}