Merge branch 'jackyzha0:v4' into v4-search-a11y

This commit is contained in:
Andrew 2024-08-09 18:43:21 -04:00 committed by GitHub
commit 7716dc86c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 94 additions and 45 deletions

View File

@ -61,6 +61,7 @@ jobs:
with: with:
fetch-depth: 0 # Fetch all history for git info fetch-depth: 0 # Fetch all history for git info
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
node-version: 22
- name: Install Dependencies - name: Install Dependencies
run: npm ci run: npm ci
- name: Build Quartz - name: Build Quartz

View File

@ -12,6 +12,7 @@ This plugin adds LaTeX support to Quartz. See [[features/Latex|Latex]] for more
This plugin accepts the following configuration options: This plugin accepts the following configuration options:
- `renderEngine`: the engine to use to render LaTeX equations. Can be `"katex"` for [KaTeX](https://katex.org/) or `"mathjax"` for [MathJax](https://www.mathjax.org/) [SVG rendering](https://docs.mathjax.org/en/latest/output/svg.html). Defaults to KaTeX. - `renderEngine`: the engine to use to render LaTeX equations. Can be `"katex"` for [KaTeX](https://katex.org/) or `"mathjax"` for [MathJax](https://www.mathjax.org/) [SVG rendering](https://docs.mathjax.org/en/latest/output/svg.html). Defaults to KaTeX.
- `customMacros`: custom macros for all LaTeX blocks. It takes the form of a key-value pair where the key is a new command name and the value is the expansion of the macro. For example: `{"\\R": "\\mathbb{R}"}`
## API ## API

View File

@ -25,5 +25,6 @@ Want to see what Quartz can do? Here are some cool community gardens:
- [🪴Aster's notebook](https://notes.asterhu.com) - [🪴Aster's notebook](https://notes.asterhu.com)
- [Gatekeeper Wiki](https://www.gatekeeper.wiki) - [Gatekeeper Wiki](https://www.gatekeeper.wiki)
- [Ellie's Notes](https://ellie.wtf) - [Ellie's Notes](https://ellie.wtf)
- [🥷🏻🌳🍃 Computer Science & Thinkering Garden](https://notes.yxy.ninja)
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)! If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)!

View File

@ -38,8 +38,13 @@ type BuildData = {
type FileEvent = "add" | "change" | "delete" type FileEvent = "add" | "change" | "delete"
function newBuildId() {
return new Date().toISOString()
}
async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) { async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
const ctx: BuildCtx = { const ctx: BuildCtx = {
buildId: newBuildId(),
argv, argv,
cfg, cfg,
allSlugs: [], allSlugs: [],
@ -167,6 +172,7 @@ async function partialRebuildFromEntrypoint(
const perf = new PerfTimer() const perf = new PerfTimer()
console.log(chalk.yellow("Detected change, rebuilding...")) console.log(chalk.yellow("Detected change, rebuilding..."))
ctx.buildId = newBuildId()
// UPDATE DEP GRAPH // UPDATE DEP GRAPH
const fp = joinSegments(argv.directory, toPosixPath(filepath)) as FilePath const fp = joinSegments(argv.directory, toPosixPath(filepath)) as FilePath
@ -363,14 +369,10 @@ async function rebuildFromEntrypoint(
const perf = new PerfTimer() const perf = new PerfTimer()
console.log(chalk.yellow("Detected change, rebuilding...")) console.log(chalk.yellow("Detected change, rebuilding..."))
ctx.buildId = newBuildId()
try { try {
const filesToRebuild = [...toRebuild].filter((fp) => !toRemove.has(fp)) const filesToRebuild = [...toRebuild].filter((fp) => !toRemove.has(fp))
const trackedSlugs = [...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets])]
.filter((fp) => !toRemove.has(fp))
.map((fp) => slugifyFilePath(path.posix.relative(argv.directory, fp) as FilePath))
ctx.allSlugs = [...new Set([...initialSlugs, ...trackedSlugs])]
const parsedContent = await parseMarkdown(ctx, filesToRebuild) const parsedContent = await parseMarkdown(ctx, filesToRebuild)
for (const content of parsedContent) { for (const content of parsedContent) {
const [_tree, vfile] = content const [_tree, vfile] = content
@ -384,6 +386,13 @@ async function rebuildFromEntrypoint(
const parsedFiles = [...contentMap.values()] const parsedFiles = [...contentMap.values()]
const filteredContent = filterContent(ctx, parsedFiles) const filteredContent = filterContent(ctx, parsedFiles)
// re-update slugs
const trackedSlugs = [...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets])]
.filter((fp) => !toRemove.has(fp))
.map((fp) => slugifyFilePath(path.posix.relative(argv.directory, fp) as FilePath))
ctx.allSlugs = [...new Set([...initialSlugs, ...trackedSlugs])]
// TODO: we can probably traverse the link graph to figure out what's safe to delete here // TODO: we can probably traverse the link graph to figure out what's safe to delete here
// instead of just deleting everything // instead of just deleting everything
await rimraf(path.join(argv.output, ".*"), { glob: true }) await rimraf(path.join(argv.output, ".*"), { glob: true })

View File

@ -44,12 +44,9 @@ export default ((userOpts?: Partial<Options>) => {
// memoized // memoized
let fileTree: FileNode let fileTree: FileNode
let jsonTree: string let jsonTree: string
let lastBuildId: string = ""
function constructFileTree(allFiles: QuartzPluginData[]) { function constructFileTree(allFiles: QuartzPluginData[]) {
if (fileTree) {
return
}
// Construct tree from allFiles // Construct tree from allFiles
fileTree = new FileNode("") fileTree = new FileNode("")
allFiles.forEach((file) => fileTree.add(file)) allFiles.forEach((file) => fileTree.add(file))
@ -76,12 +73,17 @@ export default ((userOpts?: Partial<Options>) => {
} }
const Explorer: QuartzComponent = ({ const Explorer: QuartzComponent = ({
ctx,
cfg, cfg,
allFiles, allFiles,
displayClass, displayClass,
fileData, fileData,
}: QuartzComponentProps) => { }: QuartzComponentProps) => {
constructFileTree(allFiles) if (ctx.buildId !== lastBuildId) {
lastBuildId = ctx.buildId
constructFileTree(allFiles)
}
return ( return (
<div class={classNames(displayClass, "explorer")}> <div class={classNames(displayClass, "explorer")}>
<button <button
@ -91,6 +93,8 @@ export default ((userOpts?: Partial<Options>) => {
data-collapsed={opts.folderDefaultState} data-collapsed={opts.folderDefaultState}
data-savestate={opts.useSavedState} data-savestate={opts.useSavedState}
data-tree={jsonTree} data-tree={jsonTree}
aria-controls="explorer-content"
aria-expanded={opts.folderDefaultState === "open"}
> >
<h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2> <h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2>
<svg <svg

View File

@ -26,7 +26,13 @@ const TableOfContents: QuartzComponent = ({
return ( return (
<div class={classNames(displayClass, "toc")}> <div class={classNames(displayClass, "toc")}>
<button type="button" id="toc" class={fileData.collapseToc ? "collapsed" : ""}> <button
type="button"
id="toc"
class={fileData.collapseToc ? "collapsed" : ""}
aria-controls="toc-content"
aria-expanded={!fileData.collapseToc}
>
<h3>{i18n(cfg.locale).components.tableOfContents.title}</h3> <h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@ -17,6 +17,10 @@ const observer = new IntersectionObserver((entries) => {
function toggleExplorer(this: HTMLElement) { function toggleExplorer(this: HTMLElement) {
this.classList.toggle("collapsed") this.classList.toggle("collapsed")
this.setAttribute(
"aria-expanded",
this.getAttribute("aria-expanded") === "true" ? "false" : "true",
)
const content = this.nextElementSibling as MaybeHTMLElement const content = this.nextElementSibling as MaybeHTMLElement
if (!content) return if (!content) return

View File

@ -16,6 +16,10 @@ const observer = new IntersectionObserver((entries) => {
function toggleToc(this: HTMLElement) { function toggleToc(this: HTMLElement) {
this.classList.toggle("collapsed") this.classList.toggle("collapsed")
this.setAttribute(
"aria-expanded",
this.getAttribute("aria-expanded") === "true" ? "false" : "true",
)
const content = this.nextElementSibling as HTMLElement | undefined const content = this.nextElementSibling as HTMLElement | undefined
if (!content) return if (!content) return
content.classList.toggle("collapsed") content.classList.toggle("collapsed")

View File

@ -1,7 +1,6 @@
@use "../../styles/variables.scss" as *; @use "../../styles/variables.scss" as *;
button#explorer { button#explorer {
all: unset;
background-color: transparent; background-color: transparent;
border: none; border: none;
text-align: left; text-align: left;
@ -46,8 +45,18 @@ button#explorer {
list-style: none; list-style: none;
overflow: hidden; overflow: hidden;
max-height: none; max-height: none;
transition: max-height 0.35s ease; transition:
max-height 0.35s ease,
visibility 0s linear 0s;
margin-top: 0.5rem; margin-top: 0.5rem;
visibility: visible;
&.collapsed {
transition:
max-height 0.35s ease,
visibility 0s linear 0.35s;
visibility: hidden;
}
&.collapsed > .overflow::after { &.collapsed > .overflow::after {
opacity: 0; opacity: 0;

View File

@ -29,8 +29,18 @@ button#toc {
list-style: none; list-style: none;
overflow: hidden; overflow: hidden;
max-height: none; max-height: none;
transition: max-height 0.5s ease; transition:
max-height 0.5s ease,
visibility 0s linear 0s;
position: relative; position: relative;
visibility: visible;
&.collapsed {
transition:
max-height 0.5s ease,
visibility 0s linear 0.5s;
visibility: hidden;
}
&.collapsed > .overflow::after { &.collapsed > .overflow::after {
opacity: 0; opacity: 0;

View File

@ -17,7 +17,7 @@ const defaultOptions: Options = {
csl: "apa", csl: "apa",
} }
export const Citations: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => { export const Citations: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "Citations", name: "Citations",
@ -38,7 +38,7 @@ export const Citations: QuartzTransformerPlugin<Partial<Options> | undefined> =
// using https://github.com/syntax-tree/unist-util-visit as they're just anochor links // using https://github.com/syntax-tree/unist-util-visit as they're just anochor links
plugins.push(() => { plugins.push(() => {
return (tree, _file) => { return (tree, _file) => {
visit(tree, "element", (node, index, parent) => { visit(tree, "element", (node, _index, _parent) => {
if (node.tagName === "a" && node.properties?.href?.startsWith("#bib")) { if (node.tagName === "a" && node.properties?.href?.startsWith("#bib")) {
node.properties["data-no-popover"] = true node.properties["data-no-popover"] = true
} }

View File

@ -18,7 +18,7 @@ const urlRegex = new RegExp(
"g", "g",
) )
export const Description: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => { export const Description: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "Description", name: "Description",

View File

@ -40,7 +40,7 @@ function coerceToArray(input: string | string[]): string[] | undefined {
.map((tag: string | number) => tag.toString()) .map((tag: string | number) => tag.toString())
} }
export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => { export const FrontMatter: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "FrontMatter", name: "FrontMatter",

View File

@ -14,9 +14,7 @@ const defaultOptions: Options = {
linkHeadings: true, linkHeadings: true,
} }
export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = ( export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
userOpts,
) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "GitHubFlavoredMarkdown", name: "GitHubFlavoredMarkdown",

View File

@ -27,9 +27,7 @@ function coerceDate(fp: string, d: any): Date {
} }
type MaybeDate = undefined | string | number type MaybeDate = undefined | string | number
export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | undefined> = ( export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
userOpts,
) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "CreatedModifiedDate", name: "CreatedModifiedDate",

View File

@ -5,10 +5,16 @@ import { QuartzTransformerPlugin } from "../types"
interface Options { interface Options {
renderEngine: "katex" | "mathjax" renderEngine: "katex" | "mathjax"
customMacros: MacroType
} }
export const Latex: QuartzTransformerPlugin<Options> = (opts?: Options) => { interface MacroType {
[key: string]: string
}
export const Latex: QuartzTransformerPlugin<Partial<Options>> = (opts) => {
const engine = opts?.renderEngine ?? "katex" const engine = opts?.renderEngine ?? "katex"
const macros = opts?.customMacros ?? {}
return { return {
name: "Latex", name: "Latex",
markdownPlugins() { markdownPlugins() {
@ -16,9 +22,9 @@ export const Latex: QuartzTransformerPlugin<Options> = (opts?: Options) => {
}, },
htmlPlugins() { htmlPlugins() {
if (engine === "katex") { if (engine === "katex") {
return [[rehypeKatex, { output: "html" }]] return [[rehypeKatex, { output: "html", macros }]]
} else { } else {
return [rehypeMathjax] return [[rehypeMathjax, { macros }]]
} }
}, },
externalResources() { externalResources() {

View File

@ -8,7 +8,6 @@ import {
simplifySlug, simplifySlug,
splitAnchor, splitAnchor,
transformLink, transformLink,
joinSegments,
} from "../../util/path" } from "../../util/path"
import path from "path" import path from "path"
import { visit } from "unist-util-visit" import { visit } from "unist-util-visit"
@ -33,7 +32,7 @@ const defaultOptions: Options = {
externalLinkIcon: true, externalLinkIcon: true,
} }
export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => { export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "LinkProcessing", name: "LinkProcessing",

View File

@ -136,9 +136,7 @@ const wikilinkImageEmbedRegex = new RegExp(
/^(?<alt>(?!^\d*x?\d*$).*?)?(\|?\s*?(?<width>\d+)(x(?<height>\d+))?)?$/, /^(?<alt>(?!^\d*x?\d*$).*?)?(\|?\s*?(?<width>\d+)(x(?<height>\d+))?)?$/,
) )
export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = ( export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
userOpts,
) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
const mdastToHtml = (ast: PhrasingContent | Paragraph) => { const mdastToHtml = (ast: PhrasingContent | Paragraph) => {

View File

@ -47,9 +47,7 @@ const quartzLatexRegex = new RegExp(/\$\$[\s\S]*?\$\$|\$.*?\$/, "g")
* markdown to make it compatible with quartz but the list of changes applied it * markdown to make it compatible with quartz but the list of changes applied it
* is not exhaustive. * is not exhaustive.
* */ * */
export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = ( export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
userOpts,
) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "OxHugoFlavouredMarkdown", name: "OxHugoFlavouredMarkdown",

View File

@ -19,10 +19,8 @@ const defaultOptions: Options = {
keepBackground: false, keepBackground: false,
} }
export const SyntaxHighlighting: QuartzTransformerPlugin<Options> = ( export const SyntaxHighlighting: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
userOpts?: Partial<Options>, const opts: CodeOptions = { ...defaultOptions, ...userOpts }
) => {
const opts: Partial<CodeOptions> = { ...defaultOptions, ...userOpts }
return { return {
name: "SyntaxHighlighting", name: "SyntaxHighlighting",

View File

@ -25,9 +25,7 @@ interface TocEntry {
} }
const slugAnchor = new Slugger() const slugAnchor = new Slugger()
export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefined> = ( export const TableOfContents: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
userOpts,
) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "TableOfContents", name: "TableOfContents",

View File

@ -143,7 +143,7 @@ export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise<Pro
const childPromises: WorkerPromise<ProcessedContent[]>[] = [] const childPromises: WorkerPromise<ProcessedContent[]>[] = []
for (const chunk of chunks(fps, CHUNK_SIZE)) { for (const chunk of chunks(fps, CHUNK_SIZE)) {
childPromises.push(pool.exec("parseFiles", [argv, chunk, ctx.allSlugs])) childPromises.push(pool.exec("parseFiles", [ctx.buildId, argv, chunk, ctx.allSlugs]))
} }
const results: ProcessedContent[][] = await WorkerPromise.all(childPromises).catch((err) => { const results: ProcessedContent[][] = await WorkerPromise.all(childPromises).catch((err) => {

View File

@ -14,6 +14,7 @@ export interface Argv {
} }
export interface BuildCtx { export interface BuildCtx {
buildId: string
argv: Argv argv: Argv
cfg: QuartzConfig cfg: QuartzConfig
allSlugs: FullSlug[] allSlugs: FullSlug[]

View File

@ -7,8 +7,14 @@ import { createFileParser, createProcessor } from "./processors/parse"
import { options } from "./util/sourcemap" import { options } from "./util/sourcemap"
// only called from worker thread // only called from worker thread
export async function parseFiles(argv: Argv, fps: FilePath[], allSlugs: FullSlug[]) { export async function parseFiles(
buildId: string,
argv: Argv,
fps: FilePath[],
allSlugs: FullSlug[],
) {
const ctx: BuildCtx = { const ctx: BuildCtx = {
buildId,
cfg, cfg,
argv, argv,
allSlugs, allSlugs,