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:
fetch-depth: 0 # Fetch all history for git info
- uses: actions/setup-node@v4
node-version: 22
- name: Install Dependencies
run: npm ci
- 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:
- `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

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)
- [Gatekeeper Wiki](https://www.gatekeeper.wiki)
- [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)!

View File

@ -38,8 +38,13 @@ type BuildData = {
type FileEvent = "add" | "change" | "delete"
function newBuildId() {
return new Date().toISOString()
}
async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
const ctx: BuildCtx = {
buildId: newBuildId(),
argv,
cfg,
allSlugs: [],
@ -167,6 +172,7 @@ async function partialRebuildFromEntrypoint(
const perf = new PerfTimer()
console.log(chalk.yellow("Detected change, rebuilding..."))
ctx.buildId = newBuildId()
// UPDATE DEP GRAPH
const fp = joinSegments(argv.directory, toPosixPath(filepath)) as FilePath
@ -363,14 +369,10 @@ async function rebuildFromEntrypoint(
const perf = new PerfTimer()
console.log(chalk.yellow("Detected change, rebuilding..."))
ctx.buildId = newBuildId()
try {
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)
for (const content of parsedContent) {
const [_tree, vfile] = content
@ -384,6 +386,13 @@ async function rebuildFromEntrypoint(
const parsedFiles = [...contentMap.values()]
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
// instead of just deleting everything
await rimraf(path.join(argv.output, ".*"), { glob: true })

View File

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

View File

@ -26,7 +26,13 @@ const TableOfContents: QuartzComponent = ({
return (
<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>
<svg
xmlns="http://www.w3.org/2000/svg"

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@ const defaultOptions: Options = {
csl: "apa",
}
export const Citations: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
export const Citations: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
const opts = { ...defaultOptions, ...userOpts }
return {
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
plugins.push(() => {
return (tree, _file) => {
visit(tree, "element", (node, index, parent) => {
visit(tree, "element", (node, _index, _parent) => {
if (node.tagName === "a" && node.properties?.href?.startsWith("#bib")) {
node.properties["data-no-popover"] = true
}

View File

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

View File

@ -40,7 +40,7 @@ function coerceToArray(input: string | string[]): string[] | undefined {
.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 }
return {
name: "FrontMatter",

View File

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

View File

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

View File

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

View File

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

View File

@ -136,9 +136,7 @@ const wikilinkImageEmbedRegex = new RegExp(
/^(?<alt>(?!^\d*x?\d*$).*?)?(\|?\s*?(?<width>\d+)(x(?<height>\d+))?)?$/,
)
export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (
userOpts,
) => {
export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
const opts = { ...defaultOptions, ...userOpts }
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
* is not exhaustive.
* */
export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (
userOpts,
) => {
export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
const opts = { ...defaultOptions, ...userOpts }
return {
name: "OxHugoFlavouredMarkdown",

View File

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

View File

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

View File

@ -143,7 +143,7 @@ export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise<Pro
const childPromises: WorkerPromise<ProcessedContent[]>[] = []
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) => {

View File

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

View File

@ -7,8 +7,14 @@ import { createFileParser, createProcessor } from "./processors/parse"
import { options } from "./util/sourcemap"
// 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 = {
buildId,
cfg,
argv,
allSlugs,