From b69556c918e2a4a27b047e8de6b02861f04d5a9e Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Mon, 21 Aug 2023 16:43:22 -0700 Subject: [PATCH 001/140] fix: async-mutex not exclusively locking correectly --- content/layout.md | 2 +- quartz/bootstrap-cli.mjs | 12 +++++++++--- quartz/build.ts | 11 +++++++---- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/content/layout.md b/content/layout.md index 8a74b9119..3fabeb79c 100644 --- a/content/layout.md +++ b/content/layout.md @@ -30,7 +30,7 @@ These correspond to following parts of the page: Quartz **components**, like plugins, can take in additional properties as configuration options. If you're familiar with React terminology, you can think of them as Higher-order Components. -See [a list of all the components](./tags/component) for all available components along with their configuration options. You can also checkout the guide on [[creating components]] if you're interested in further customizing the behaviour of Quartz. +See [a list of all the components](component.md) for all available components along with their configuration options. You can also checkout the guide on [[creating components]] if you're interested in further customizing the behaviour of Quartz. ### Style diff --git a/quartz/bootstrap-cli.mjs b/quartz/bootstrap-cli.mjs index cb0ff2e02..808deba13 100755 --- a/quartz/bootstrap-cli.mjs +++ b/quartz/bootstrap-cli.mjs @@ -394,8 +394,15 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. const buildMutex = new Mutex() const timeoutIds = new Set() + let firstBuild = true const build = async (clientRefresh) => { - await buildMutex.acquire() + const release = await buildMutex.acquire() + if (firstBuild) { + firstBuild = false + } else { + console.log(chalk.yellow("Detected a source code change, doing a hard rebuild...")) + } + const result = await ctx.rebuild().catch((err) => { console.error(`${chalk.red("Couldn't parse Quartz configuration:")} ${fp}`) console.log(`Reason: ${chalk.grey(err)}`) @@ -418,7 +425,7 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. const { default: buildQuartz } = await import(cacheFile + `?update=${randomUUID()}`) await buildQuartz(argv, clientRefresh) clientRefresh() - buildMutex.release() + release() } const rebuild = (clientRefresh) => { @@ -526,7 +533,6 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. ignoreInitial: true, }) .on("all", async () => { - console.log(chalk.yellow("Detected a source code change, doing a hard rebuild...")) rebuild(clientRefresh) }) } else { diff --git a/quartz/build.ts b/quartz/build.ts index 78437f8aa..0af39d008 100644 --- a/quartz/build.ts +++ b/quartz/build.ts @@ -108,12 +108,13 @@ async function startServing( toRemove.add(filePath) } - timeoutIds.forEach((id) => clearTimeout(id)) - // debounce rebuilds every 250ms timeoutIds.add( setTimeout(async () => { - await buildMutex.acquire() + const release = await buildMutex.acquire() + timeoutIds.forEach((id) => clearTimeout(id)) + timeoutIds.clear() + const perf = new PerfTimer() console.log(chalk.yellow("Detected change, rebuilding...")) try { @@ -134,6 +135,8 @@ async function startServing( contentMap.delete(fp) } + // TODO: we can probably traverse the link graph to figure out what's safe to delete here + // instead of just deleting everything await rimraf(argv.output) const parsedFiles = [...contentMap.values()] const filteredContent = filterContent(ctx, parsedFiles) @@ -146,7 +149,7 @@ async function startServing( clientRefresh() toRebuild.clear() toRemove.clear() - buildMutex.release() + release() }, 250), ) } From e10de3febffd3e3b7eaa3aed611aea03153e6a82 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Mon, 21 Aug 2023 17:01:18 -0700 Subject: [PATCH 002/140] fix: server-handler crash from filename (closes #386) --- quartz/bootstrap-cli.mjs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/quartz/bootstrap-cli.mjs b/quartz/bootstrap-cli.mjs index 808deba13..d068cd893 100755 --- a/quartz/bootstrap-cli.mjs +++ b/quartz/bootstrap-cli.mjs @@ -462,6 +462,12 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. await serveHandler(req, res, { public: argv.output, directoryListing: false, + headers: [ + { + source: "**/*.html", + headers: [{ key: "Content-Disposition", value: "inline" }], + }, + ], }) const status = res.statusCode const statusString = From c60b3d5e3444e46587c1143dab784c53204070ae Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Wed, 23 Aug 2023 01:16:21 +0900 Subject: [PATCH 003/140] fix: typo in bootstrap-cli.mjs (#394) --- quartz/bootstrap-cli.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/bootstrap-cli.mjs b/quartz/bootstrap-cli.mjs index d068cd893..6a975ca4d 100755 --- a/quartz/bootstrap-cli.mjs +++ b/quartz/bootstrap-cli.mjs @@ -219,7 +219,7 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. ) } - // get a prefered link resolution strategy + // get a preferred 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\`.`, From bb677840fc1ff14637fab8a99841dd532f408fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=BE=E6=B5=A6=20=E7=9F=A5=E4=B9=9F=20Matsuura=20Tomoy?= =?UTF-8?q?a?= Date: Wed, 23 Aug 2023 01:16:55 +0900 Subject: [PATCH 004/140] fixed broken CJK links (#390) --- quartz/plugins/transformers/links.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts index d9c9c7c65..7d992722b 100644 --- a/quartz/plugins/transformers/links.ts +++ b/quartz/plugins/transformers/links.ts @@ -63,7 +63,8 @@ export const CrawlLinks: QuartzTransformerPlugin | undefined> = const url = new URL(dest, `https://base.com/${curSlug}`) const canonicalDest = url.pathname const [destCanonical, _destAnchor] = splitAnchor(canonicalDest) - const simple = simplifySlug(destCanonical as FullSlug) + const simple = decodeURI(simplifySlug(destCanonical as FullSlug)) as SimpleSlug + outgoing.add(simple) } From b991cf2ee8a456a15f2b566843d93a9b7a9a0c29 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Tue, 22 Aug 2023 21:30:31 -0700 Subject: [PATCH 005/140] fix: spa hijacks back button (closes #400) --- quartz/components/scripts/spa.inline.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/quartz/components/scripts/spa.inline.ts b/quartz/components/scripts/spa.inline.ts index 6f9399eaf..d91ca78c2 100644 --- a/quartz/components/scripts/spa.inline.ts +++ b/quartz/components/scripts/spa.inline.ts @@ -79,7 +79,9 @@ async function navigate(url: URL, isBack: boolean = false) { // delay setting the url until now // at this point everything is loaded so changing the url should resolve to the correct addresses - history.pushState({}, "", url) + if (!isBack) { + history.pushState({}, "", url) + } notifyNav(getFullSlug(window)) delete announcer.dataset.persist } From 8b63ff882ae28b1a1774293673a7531463d6a5e5 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Tue, 22 Aug 2023 22:14:16 -0700 Subject: [PATCH 006/140] fix: tag support for non-latin alphabets (fixes #398) --- quartz/components/TagList.tsx | 3 ++- quartz/plugins/transformers/frontmatter.ts | 2 +- quartz/plugins/transformers/ofm.ts | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/quartz/components/TagList.tsx b/quartz/components/TagList.tsx index 639f68c61..a4dfac701 100644 --- a/quartz/components/TagList.tsx +++ b/quartz/components/TagList.tsx @@ -44,7 +44,8 @@ TagList.css = ` a.tag-link { border-radius: 8px; background-color: var(--highlight); - padding: 0.2rem 0.5rem; + padding: 0.2rem 0.4rem; + margin: 0 0.1rem; } ` diff --git a/quartz/plugins/transformers/frontmatter.ts b/quartz/plugins/transformers/frontmatter.ts index 5b067f63f..3f55b9cb6 100644 --- a/quartz/plugins/transformers/frontmatter.ts +++ b/quartz/plugins/transformers/frontmatter.ts @@ -41,7 +41,7 @@ export const FrontMatter: QuartzTransformerPlugin | undefined> } // slug them all!! - data.tags = data.tags?.map((tag: string) => slugTag(tag)) ?? [] + data.tags = [...new Set(data.tags?.map((tag: string) => slugTag(tag)))] ?? [] // fill in frontmatter file.data.frontmatter = { diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index b66ba850b..bed6f622c 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -116,7 +116,7 @@ const calloutRegex = new RegExp(/^\[\!(\w+)\]([+-]?)/) const calloutLineRegex = new RegExp(/^> *\[\!\w+\][+-]?.*$/, "gm") // (?:^| ) -> non-capturing group, tag should start be separated by a space or be the start of the line // #(\w+) -> tag itself is # followed by a string of alpha-numeric characters -const tagRegex = new RegExp(/(?:^| )#([\w-_\/]+)/, "g") +const tagRegex = new RegExp(/(?:^| )#(\p{L}+)/, "gu") export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin | undefined> = ( userOpts, @@ -382,8 +382,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin plugins.push(() => { return (tree: Root, file) => { const base = pathToRoot(file.data.slug!) - findAndReplace(tree, tagRegex, (value: string, tag: string) => { - if (file.data.frontmatter) { + findAndReplace(tree, tagRegex, (_value: string, tag: string) => { + if (file.data.frontmatter && !file.data.frontmatter.tags.includes(tag)) { file.data.frontmatter.tags.push(tag) } @@ -398,7 +398,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin children: [ { type: "text", - value, + value: `#${tag}`, }, ], } From 99dbe525d9b221bf12778ed899c94ef103a77c45 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Tue, 22 Aug 2023 22:27:41 -0700 Subject: [PATCH 007/140] fix: properly lock across source and content refresh by sharing a mutex --- quartz/bootstrap-cli.mjs | 12 ++++++------ quartz/build.ts | 19 +++++++++++++------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/quartz/bootstrap-cli.mjs b/quartz/bootstrap-cli.mjs index 6a975ca4d..47c58ab0d 100755 --- a/quartz/bootstrap-cli.mjs +++ b/quartz/bootstrap-cli.mjs @@ -394,12 +394,12 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. const buildMutex = new Mutex() const timeoutIds = new Set() - let firstBuild = true + let cleanupBuild = null const build = async (clientRefresh) => { const release = await buildMutex.acquire() - if (firstBuild) { - firstBuild = false - } else { + + if (cleanupBuild) { + await cleanupBuild() console.log(chalk.yellow("Detected a source code change, doing a hard rebuild...")) } @@ -408,6 +408,7 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. console.log(`Reason: ${chalk.grey(err)}`) process.exit(1) }) + release() if (argv.bundleInfo) { const outputFileName = "quartz/.quartz-cache/transpiled-build.mjs" @@ -423,9 +424,8 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. // bypass module cache // https://github.com/nodejs/modules/issues/307 const { default: buildQuartz } = await import(cacheFile + `?update=${randomUUID()}`) - await buildQuartz(argv, clientRefresh) + cleanupBuild = await buildQuartz(argv, buildMutex, clientRefresh) clientRefresh() - release() } const rebuild = (clientRefresh) => { diff --git a/quartz/build.ts b/quartz/build.ts index 0af39d008..8b1d31834 100644 --- a/quartz/build.ts +++ b/quartz/build.ts @@ -18,7 +18,7 @@ import { trace } from "./util/trace" import { options } from "./util/sourcemap" import { Mutex } from "async-mutex" -async function buildQuartz(argv: Argv, clientRefresh: () => void) { +async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) { const ctx: BuildCtx = { argv, cfg, @@ -38,6 +38,7 @@ async function buildQuartz(argv: Argv, clientRefresh: () => void) { console.log(` Emitters: ${pluginNames("emitters").join(", ")}`) } + const release = await mut.acquire() perf.addEvent("clean") await rimraf(output) console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`) @@ -56,15 +57,17 @@ async function buildQuartz(argv: Argv, clientRefresh: () => void) { const filteredContent = filterContent(ctx, parsedFiles) await emitContent(ctx, filteredContent) console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`)) + release() if (argv.serve) { - return startServing(ctx, parsedFiles, clientRefresh) + return startServing(ctx, mut, parsedFiles, clientRefresh) } } // setup watcher for rebuilds async function startServing( ctx: BuildCtx, + mut: Mutex, initialContent: ProcessedContent[], clientRefresh: () => void, ) { @@ -78,7 +81,6 @@ async function startServing( } const initialSlugs = ctx.allSlugs - const buildMutex = new Mutex() const timeoutIds: Set> = new Set() const toRebuild: Set = new Set() const toRemove: Set = new Set() @@ -111,7 +113,7 @@ async function startServing( // debounce rebuilds every 250ms timeoutIds.add( setTimeout(async () => { - const release = await buildMutex.acquire() + const release = await mut.acquire() timeoutIds.forEach((id) => clearTimeout(id)) timeoutIds.clear() @@ -164,11 +166,16 @@ async function startServing( .on("add", (fp) => rebuild(fp, "add")) .on("change", (fp) => rebuild(fp, "change")) .on("unlink", (fp) => rebuild(fp, "delete")) + + return async () => { + timeoutIds.forEach((id) => clearTimeout(id)) + await watcher.close() + } } -export default async (argv: Argv, clientRefresh: () => void) => { +export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => { try { - return await buildQuartz(argv, clientRefresh) + return await buildQuartz(argv, mut, clientRefresh) } catch (err) { trace("\nExiting Quartz due to a fatal error", err as Error) } From 36548d59866ab3236677ff25af106b882c0694f6 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Tue, 22 Aug 2023 22:41:50 -0700 Subject: [PATCH 008/140] fix: toc for cyrillic and other non-latin alphabets (closes #396) --- quartz/components/scripts/spa.inline.ts | 2 +- quartz/plugins/transformers/toc.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/quartz/components/scripts/spa.inline.ts b/quartz/components/scripts/spa.inline.ts index d91ca78c2..bd2260831 100644 --- a/quartz/components/scripts/spa.inline.ts +++ b/quartz/components/scripts/spa.inline.ts @@ -64,7 +64,7 @@ async function navigate(url: URL, isBack: boolean = false) { // scroll into place and add history if (!isBack) { if (url.hash) { - const el = document.getElementById(url.hash.substring(1)) + const el = document.getElementById(decodeURIComponent(url.hash.substring(1))) el?.scrollIntoView() } else { window.scrollTo({ top: 0 }) diff --git a/quartz/plugins/transformers/toc.ts b/quartz/plugins/transformers/toc.ts index 87031a9dd..be006f61a 100644 --- a/quartz/plugins/transformers/toc.ts +++ b/quartz/plugins/transformers/toc.ts @@ -2,7 +2,7 @@ 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 Slugger from "github-slugger" export interface Options { maxDepth: 1 | 2 | 3 | 4 | 5 | 6 @@ -34,6 +34,7 @@ export const TableOfContents: QuartzTransformerPlugin | undefin return async (tree: Root, file) => { const display = file.data.frontmatter?.enableToc ?? opts.showByDefault if (display) { + const slugAnchor = new Slugger() const toc: TocEntry[] = [] let highestDepth: number = opts.maxDepth visit(tree, "heading", (node) => { @@ -43,7 +44,7 @@ export const TableOfContents: QuartzTransformerPlugin | undefin toc.push({ depth: node.depth, text, - slug: slugAnchor(text), + slug: slugAnchor.slug(text), }) } }) From b444c5c13b983bf80df8b6d020eb246e9fd3e78e Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Tue, 22 Aug 2023 23:33:58 -0700 Subject: [PATCH 009/140] fix: percent-encoding for files with %, contentIndex for non-latin chars (closes #397, closes #399) --- quartz/plugins/emitters/contentIndex.ts | 2 +- quartz/plugins/transformers/links.ts | 10 ++++++++-- quartz/util/path.ts | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/quartz/plugins/emitters/contentIndex.ts b/quartz/plugins/emitters/contentIndex.ts index f4bf6db02..a18e54e3e 100644 --- a/quartz/plugins/emitters/contentIndex.ts +++ b/quartz/plugins/emitters/contentIndex.ts @@ -22,7 +22,7 @@ interface Options { const defaultOptions: Options = { enableSiteMap: true, enableRSS: true, - includeEmptyFiles: false, + includeEmptyFiles: true, } function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string { diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts index 7d992722b..f8da36c4d 100644 --- a/quartz/plugins/transformers/links.ts +++ b/quartz/plugins/transformers/links.ts @@ -60,11 +60,17 @@ export const CrawlLinks: QuartzTransformerPlugin | undefined> = dest, transformOptions, ) - const url = new URL(dest, `https://base.com/${curSlug}`) + + // url.resolve is considered legacy + // WHATWG equivalent https://nodejs.dev/en/api/v18/url/#urlresolvefrom-to + const url = new URL(dest, `resolve://${curSlug}`) const canonicalDest = url.pathname const [destCanonical, _destAnchor] = splitAnchor(canonicalDest) - const simple = decodeURI(simplifySlug(destCanonical as FullSlug)) as SimpleSlug + // need to decodeURIComponent here as WHATWG URL percent-encodes everything + const simple = decodeURIComponent( + simplifySlug(destCanonical as FullSlug), + ) as SimpleSlug outgoing.add(simple) } diff --git a/quartz/util/path.ts b/quartz/util/path.ts index d14b827df..1557c1bd5 100644 --- a/quartz/util/path.ts +++ b/quartz/util/path.ts @@ -52,7 +52,7 @@ export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug { let slug = withoutFileExt .split("/") - .map((segment) => segment.replace(/\s/g, "-")) // slugify all segments + .map((segment) => segment.replace(/\s/g, "-").replace(/%/g, "-percent")) // slugify all segments .join("/") // always use / as sep .replace(/\/$/, "") // remove trailing slash From 3064839c2d2ea0a9976bef83db12102647572083 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Tue, 22 Aug 2023 23:37:02 -0700 Subject: [PATCH 010/140] version bump to 4.0.9 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index fa661da49..39adbab81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jackyzha0/quartz", - "version": "4.0.8", + "version": "4.0.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jackyzha0/quartz", - "version": "4.0.8", + "version": "4.0.9", "license": "MIT", "dependencies": { "@clack/prompts": "^0.6.3", diff --git a/package.json b/package.json index 7fb51275b..a5087fb32 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@jackyzha0/quartz", "description": "🌱 publish your digital garden and notes as a website", "private": true, - "version": "4.0.8", + "version": "4.0.9", "type": "module", "author": "jackyzha0 ", "license": "MIT", From d2f52549955ff7600cc5897e67806df4ebf85f91 Mon Sep 17 00:00:00 2001 From: Aaron Pham <29749331+aarnphm@users.noreply.github.com> Date: Wed, 23 Aug 2023 12:05:01 -0400 Subject: [PATCH 011/140] fix(esbuild): conflict with esbuild-sass-plugin (#402) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a5087fb32..aaf7c2575 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "@types/workerpool": "^6.4.0", "@types/ws": "^8.5.5", "@types/yargs": "^17.0.24", - "esbuild": "^0.18.11", + "esbuild": "^0.19.1", "prettier": "^3.0.0", "tsx": "^3.12.7", "typescript": "^5.0.4" From 1128efcf237d275343daaab16e1b1d3e228999b9 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 23 Aug 2023 09:10:30 -0700 Subject: [PATCH 012/140] deps: esbuild and esbuild-sass-plugin --- package-lock.json | 1020 ++------------------------------------------- package.json | 2 +- 2 files changed, 42 insertions(+), 980 deletions(-) diff --git a/package-lock.json b/package-lock.json index 39adbab81..d0ed3e770 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "chokidar": "^3.5.3", "cli-spinner": "^0.2.10", "d3": "^7.8.5", - "esbuild-sass-plugin": "^2.9.0", + "esbuild-sass-plugin": "^2.12.0", "flexsearch": "0.7.21", "github-slugger": "^2.0.0", "globby": "^13.1.4", @@ -77,7 +77,7 @@ "@types/workerpool": "^6.4.0", "@types/ws": "^8.5.5", "@types/yargs": "^17.0.24", - "esbuild": "^0.18.11", + "esbuild": "^0.19.1", "prettier": "^3.0.0", "tsx": "^3.12.7", "typescript": "^5.0.4" @@ -140,70 +140,6 @@ "source-map-support": "^0.5.21" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", - "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", - "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", - "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", - "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", @@ -220,278 +156,6 @@ "node": ">=12" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", - "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", - "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", - "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", - "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", - "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", - "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", - "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", - "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", - "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", - "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", - "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", - "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", - "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", - "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", - "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", - "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", - "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { "version": "0.17.19", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", @@ -539,70 +203,10 @@ "get-tsconfig": "^4.4.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.18.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.11.tgz", - "integrity": "sha512-q4qlUf5ucwbUJZXF5tEQ8LF7y0Nk4P58hOsGk3ucY0oCwgQqAnqXVbUuahCddVHfrxmpyewRpiTHwVHIETYu7Q==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.18.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.11.tgz", - "integrity": "sha512-snieiq75Z1z5LJX9cduSAjUr7vEI1OdlzFPMw0HH5YI7qQHDd3qs+WZoMrWYDsfRJSq36lIA6mfZBkvL46KoIw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.18.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.11.tgz", - "integrity": "sha512-iPuoxQEV34+hTF6FT7om+Qwziv1U519lEOvekXO9zaMMlT9+XneAhKL32DW3H7okrCOBQ44BMihE8dclbZtTuw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.11.tgz", - "integrity": "sha512-Gm0QkI3k402OpfMKyQEEMG0RuW2LQsSmI6OeO4El2ojJMoF5NLYb3qMIjvbG/lbMeLOGiW6ooU8xqc+S0fgz2w==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/darwin-x64": { - "version": "0.18.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.11.tgz", - "integrity": "sha512-N15Vzy0YNHu6cfyDOjiyfJlRJCB/ngKOAvoBf1qybG3eOq0SL2Lutzz9N7DYUbb7Q23XtHPn6lMDF6uWbGv9Fw==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.2.tgz", + "integrity": "sha512-tP+B5UuIbbFMj2hQaUr6EALlHOIOmlLM2FK7jeFBobPy2ERdohI4Ka6ZFjZ1ZYsrHE/hZimGuU90jusRE0pwDw==", "cpu": [ "x64" ], @@ -614,261 +218,6 @@ "node": ">=12" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.11.tgz", - "integrity": "sha512-atEyuq6a3omEY5qAh5jIORWk8MzFnCpSTUruBgeyN9jZq1K/QI9uke0ATi3MHu4L8c59CnIi4+1jDKMuqmR71A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.11.tgz", - "integrity": "sha512-XtuPrEfBj/YYYnAAB7KcorzzpGTvOr/dTtXPGesRfmflqhA4LMF0Gh/n5+a9JBzPuJ+CGk17CA++Hmr1F/gI0Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.18.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.11.tgz", - "integrity": "sha512-Idipz+Taso/toi2ETugShXjQ3S59b6m62KmLHkJlSq/cBejixmIydqrtM2XTvNCywFl3VC7SreSf6NV0i6sRyg==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.18.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.11.tgz", - "integrity": "sha512-c6Vh2WS9VFKxKZ2TvJdA7gdy0n6eSy+yunBvv4aqNCEhSWVor1TU43wNRp2YLO9Vng2G+W94aRz+ILDSwAiYog==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.18.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.11.tgz", - "integrity": "sha512-S3hkIF6KUqRh9n1Q0dSyYcWmcVa9Cg+mSoZEfFuzoYXXsk6196qndrM+ZiHNwpZKi3XOXpShZZ+9dfN5ykqjjw==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.18.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.11.tgz", - "integrity": "sha512-MRESANOoObQINBA+RMZW+Z0TJWpibtE7cPFnahzyQHDCA9X9LOmGh68MVimZlM9J8n5Ia8lU773te6O3ILW8kw==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.11.tgz", - "integrity": "sha512-qVyPIZrXNMOLYegtD1u8EBccCrBVshxMrn5MkuFc3mEVsw7CCQHaqZ4jm9hbn4gWY95XFnb7i4SsT3eflxZsUg==", - "cpu": [ - "mips64el" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.11.tgz", - "integrity": "sha512-T3yd8vJXfPirZaUOoA9D2ZjxZX4Gr3QuC3GztBJA6PklLotc/7sXTOuuRkhE9W/5JvJP/K9b99ayPNAD+R+4qQ==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.11.tgz", - "integrity": "sha512-evUoRPWiwuFk++snjH9e2cAjF5VVSTj+Dnf+rkO/Q20tRqv+644279TZlPK8nUGunjPAtQRCj1jQkDAvL6rm2w==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.18.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.11.tgz", - "integrity": "sha512-/SlRJ15XR6i93gRWquRxYCfhTeC5PdqEapKoLbX63PLCmAkXZHY2uQm2l9bN0oPHBsOw2IswRZctMYS0MijFcg==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.18.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.11.tgz", - "integrity": "sha512-xcncej+wF16WEmIwPtCHi0qmx1FweBqgsRtEL1mSHLFR6/mb3GEZfLQnx+pUDfRDEM4DQF8dpXIW7eDOZl1IbA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.11.tgz", - "integrity": "sha512-aSjMHj/F7BuS1CptSXNg6S3M4F3bLp5wfFPIJM+Km2NfIVfFKhdmfHF9frhiCLIGVzDziggqWll0B+9AUbud/Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.11.tgz", - "integrity": "sha512-tNBq+6XIBZtht0xJGv7IBB5XaSyvYPCm1PxJ33zLQONdZoLVM0bgGqUrXnJyiEguD9LU4AHiu+GCXy/Hm9LsdQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.18.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.11.tgz", - "integrity": "sha512-kxfbDOrH4dHuAAOhr7D7EqaYf+W45LsAOOhAet99EyuxxQmjbk8M9N4ezHcEiCYPaiW8Dj3K26Z2V17Gt6p3ng==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.18.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.11.tgz", - "integrity": "sha512-Sh0dDRyk1Xi348idbal7lZyfSkjhJsdFeuC13zqdipsvMetlGiFQNdO+Yfp6f6B4FbyQm7qsk16yaZk25LChzg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.18.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.11.tgz", - "integrity": "sha512-o9JUIKF1j0rqJTFbIoF4bXj6rvrTZYOrfRcGyL0Vm5uJ/j5CkBD/51tpdxe9lXEDouhRgdr/BYzUrDOvrWwJpg==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.18.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.11.tgz", - "integrity": "sha512-rQI4cjLHd2hGsM1LqgDI7oOCYbQ6IBOVsX9ejuRMSze0GqXUG2ekwiKkiBU1pRGSeCqFFHxTrcEydB2Hyoz9CA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@floating-ui/core": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.3.1.tgz", @@ -919,51 +268,6 @@ "@napi-rs/simple-git-win32-x64-msvc": "0.1.8" } }, - "node_modules/@napi-rs/simple-git-android-arm-eabi": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm-eabi/-/simple-git-android-arm-eabi-0.1.8.tgz", - "integrity": "sha512-JJCejHBB1G6O8nxjQLT4quWCcvLpC3oRdJJ9G3MFYSCoYS8i1bWCWeU+K7Br+xT+D6s1t9q8kNJAwJv9Ygpi0g==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/simple-git-android-arm64": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm64/-/simple-git-android-arm64-0.1.8.tgz", - "integrity": "sha512-mraHzwWBw3tdRetNOS5KnFSjvdAbNBnjFLA8I4PwTCPJj3Q4txrigcPp2d59cJ0TC51xpnPXnZjYdNwwSI9g6g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/simple-git-darwin-arm64": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-arm64/-/simple-git-darwin-arm64-0.1.8.tgz", - "integrity": "sha512-ufy/36eI/j4UskEuvqSH7uXtp3oXeLDmjQCfKJz3u5Vx98KmOMKrqAm2H81AB2WOtCo5mqS6PbBeUXR8BJX8lQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@napi-rs/simple-git-darwin-x64": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-x64/-/simple-git-darwin-x64-0.1.8.tgz", @@ -979,115 +283,6 @@ "node": ">= 10" } }, - "node_modules/@napi-rs/simple-git-linux-arm-gnueabihf": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm-gnueabihf/-/simple-git-linux-arm-gnueabihf-0.1.8.tgz", - "integrity": "sha512-6BPTJ7CzpSm2t54mRLVaUr3S7ORJfVJoCk2rQ8v8oDg0XAMKvmQQxOsAgqKBo9gYNHJnqrOx3AEuEgvB586BuQ==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/simple-git-linux-arm64-gnu": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-gnu/-/simple-git-linux-arm64-gnu-0.1.8.tgz", - "integrity": "sha512-qfESqUCAA/XoQpRXHptSQ8gIFnETCQt1zY9VOkplx6tgYk9PCeaX4B1Xuzrh3eZamSCMJFn+1YB9Ut8NwyGgAA==", - "cpu": [ - "arm64" - ], - "hasInstallScript": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/simple-git-linux-arm64-musl": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-musl/-/simple-git-linux-arm64-musl-0.1.8.tgz", - "integrity": "sha512-G80BQPpaRmQpn8dJGHp4I2/YVhWDUNJwcCrJAtAdbKFDCMyCHJBln2ERL/+IEUlIAT05zK/c1Z5WEprvXEdXow==", - "cpu": [ - "arm64" - ], - "hasInstallScript": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/simple-git-linux-x64-gnu": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-gnu/-/simple-git-linux-x64-gnu-0.1.8.tgz", - "integrity": "sha512-NI6o1sZYEf6vPtNWJAm9w8BxJt+LlSFW0liSjYe3lc3e4dhMfV240f0ALeqlwdIldRPaDFwZSJX5/QbS7nMzhw==", - "cpu": [ - "x64" - ], - "hasInstallScript": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/simple-git-linux-x64-musl": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-musl/-/simple-git-linux-x64-musl-0.1.8.tgz", - "integrity": "sha512-wljGAEOW41er45VTiU8kXJmO480pQKzsgRCvPlJJSCaEVBbmo6XXbFIXnZy1a2J3Zyy2IOsRB4PVkUZaNuPkZQ==", - "cpu": [ - "x64" - ], - "hasInstallScript": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/simple-git-win32-arm64-msvc": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-arm64-msvc/-/simple-git-win32-arm64-msvc-0.1.8.tgz", - "integrity": "sha512-QuV4QILyKPfbWHoQKrhXqjiCClx0SxbCTVogkR89BwivekqJMd9UlMxZdoCmwLWutRx4z9KmzQqokvYI5QeepA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/simple-git-win32-x64-msvc": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-x64-msvc/-/simple-git-win32-x64-msvc-0.1.8.tgz", - "integrity": "sha512-UzNS4JtjhZhZ5hRLq7BIUq+4JOwt1ThIKv11CsF1ag2l99f0123XvfEpjczKTaa94nHtjXYc2Mv9TjccBqYOew==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2441,9 +1636,9 @@ } }, "node_modules/esbuild": { - "version": "0.18.11", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.11.tgz", - "integrity": "sha512-i8u6mQF0JKJUlGR3OdFLKldJQMMs8OqM9Cc3UCi9XXziJ9WERM5bfkHaEAy0YAvPRMgqSW55W7xYn84XtEFTtA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.2.tgz", + "integrity": "sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg==", "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -2452,40 +1647,40 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.18.11", - "@esbuild/android-arm64": "0.18.11", - "@esbuild/android-x64": "0.18.11", - "@esbuild/darwin-arm64": "0.18.11", - "@esbuild/darwin-x64": "0.18.11", - "@esbuild/freebsd-arm64": "0.18.11", - "@esbuild/freebsd-x64": "0.18.11", - "@esbuild/linux-arm": "0.18.11", - "@esbuild/linux-arm64": "0.18.11", - "@esbuild/linux-ia32": "0.18.11", - "@esbuild/linux-loong64": "0.18.11", - "@esbuild/linux-mips64el": "0.18.11", - "@esbuild/linux-ppc64": "0.18.11", - "@esbuild/linux-riscv64": "0.18.11", - "@esbuild/linux-s390x": "0.18.11", - "@esbuild/linux-x64": "0.18.11", - "@esbuild/netbsd-x64": "0.18.11", - "@esbuild/openbsd-x64": "0.18.11", - "@esbuild/sunos-x64": "0.18.11", - "@esbuild/win32-arm64": "0.18.11", - "@esbuild/win32-ia32": "0.18.11", - "@esbuild/win32-x64": "0.18.11" + "@esbuild/android-arm": "0.19.2", + "@esbuild/android-arm64": "0.19.2", + "@esbuild/android-x64": "0.19.2", + "@esbuild/darwin-arm64": "0.19.2", + "@esbuild/darwin-x64": "0.19.2", + "@esbuild/freebsd-arm64": "0.19.2", + "@esbuild/freebsd-x64": "0.19.2", + "@esbuild/linux-arm": "0.19.2", + "@esbuild/linux-arm64": "0.19.2", + "@esbuild/linux-ia32": "0.19.2", + "@esbuild/linux-loong64": "0.19.2", + "@esbuild/linux-mips64el": "0.19.2", + "@esbuild/linux-ppc64": "0.19.2", + "@esbuild/linux-riscv64": "0.19.2", + "@esbuild/linux-s390x": "0.19.2", + "@esbuild/linux-x64": "0.19.2", + "@esbuild/netbsd-x64": "0.19.2", + "@esbuild/openbsd-x64": "0.19.2", + "@esbuild/sunos-x64": "0.19.2", + "@esbuild/win32-arm64": "0.19.2", + "@esbuild/win32-ia32": "0.19.2", + "@esbuild/win32-x64": "0.19.2" } }, "node_modules/esbuild-sass-plugin": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.10.0.tgz", - "integrity": "sha512-STv849QGT8g77RRFmroSt4VBVKjv+dypKcO4aWz8IP4G5JbRH0KC0+B8ODuzlUNu9R5MbkGcev/62RDP/JcZ2Q==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.12.0.tgz", + "integrity": "sha512-+k/5WM/Yf/Ur7ahn6XXxEPwa/lmuacLO7vrCIAJuvQapX1CiIHtlX/nc2eiMoJ6P6RvqZhKpQvIiwgYJonzHtw==", "dependencies": { "resolve": "^1.22.2", - "sass": "^1.63.0" + "sass": "^1.65.1" }, "peerDependencies": { - "esbuild": "^0.18.0" + "esbuild": "^0.19.1" } }, "node_modules/escalade": { @@ -3147,9 +2342,9 @@ } }, "node_modules/immutable": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", - "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==" + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.3.tgz", + "integrity": "sha512-808ZFYMsIRAjLAu5xkKo0TsbY9LBy9H5MazTKIEHerNkg0ymgilGfBPMR/3G7d/ihGmuK2Hw8S1izY2d3kd3wA==" }, "node_modules/inline-style-parser": { "version": "0.1.1", @@ -3436,25 +2631,6 @@ "lightningcss-win32-x64-msvc": "1.21.5" } }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.21.5", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.21.5.tgz", - "integrity": "sha512-z05hyLX85WY0UfhkFUOrWEFqD69lpVAmgl3aDzMKlIZJGygbhbegqb4PV8qfUrKKNBauut/qVNPKZglhTaDDxA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lightningcss-darwin-x64": { "version": "1.21.5", "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.21.5.tgz", @@ -3474,120 +2650,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.21.5", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.21.5.tgz", - "integrity": "sha512-xN6+5/JsMrbZHL1lPl+MiNJ3Xza12ueBKPepiyDCFQzlhFRTj7D0LG+cfNTzPBTO8KcYQynLpl1iBB8LGp3Xtw==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.21.5", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.21.5.tgz", - "integrity": "sha512-KfzFNhC4XTbmG3ma/xcTs/IhCwieW89XALIusKmnV0N618ZDXEB0XjWOYQRCXeK9mfqPdbTBpurEHV/XZtkniQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.21.5", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.21.5.tgz", - "integrity": "sha512-bc0GytQO5Mn9QM6szaZ+31fQHNdidgpM1sSCwzPItz8hg3wOvKl8039rU0veMJV3ZgC9z0ypNRceLrSHeRHmXw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.21.5", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.21.5.tgz", - "integrity": "sha512-JwMbgypPQgc2kW2av3OwzZ8cbrEuIiDiXPJdXRE6aVxu67yHauJawQLqJKTGUhiAhy6iLDG8Wg0a3/ziL+m+Kw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.21.5", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.21.5.tgz", - "integrity": "sha512-Ib8b6IQ/OR/VrPU6YBgy4T3QnuHY7DUa95O+nz+cwrTkMSN6fuHcTcIaz4t8TJ6HI5pl3uxUOZjmtls2pyQWow==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.21.5", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.21.5.tgz", - "integrity": "sha512-A8cSi8lUpBeVmoF+DqqW7cd0FemDbCuKr490IXdjyeI+KL8adpSKUs8tcqO0OXPh1EoDqK7JNkD/dELmd4Iz5g==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -5159,9 +4221,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { - "version": "1.63.6", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.63.6.tgz", - "integrity": "sha512-MJuxGMHzaOW7ipp+1KdELtqKbfAWbH7OLIdoSMnVe3EXPMTmxTmlaZDCTsgIpPCs3w99lLo9/zDKkOrJuT5byw==", + "version": "1.66.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.66.1.tgz", + "integrity": "sha512-50c+zTsZOJVgFfTgwwEzkjA3/QACgdNsKueWPyAR0mRINIvLAStVQBbPg14iuqEQ74NPDbXzJARJ/O4SI1zftA==", "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", diff --git a/package.json b/package.json index aaf7c2575..fc5463688 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "chokidar": "^3.5.3", "cli-spinner": "^0.2.10", "d3": "^7.8.5", - "esbuild-sass-plugin": "^2.9.0", + "esbuild-sass-plugin": "^2.12.0", "flexsearch": "0.7.21", "github-slugger": "^2.0.0", "globby": "^13.1.4", From cde1e26129f8cd6b183ccc1c35a06f76dedeff9c Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 23 Aug 2023 09:16:44 -0700 Subject: [PATCH 013/140] deps: install exact --- package-lock.json | 839 ++++++++++++++++++++++++++++++++++++++++++++-- package.json | 4 +- 2 files changed, 822 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index d0ed3e770..be51eb518 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@clack/prompts": "^0.6.3", "@floating-ui/dom": "^1.4.0", - "@napi-rs/simple-git": "^0.1.8", + "@napi-rs/simple-git": "0.1.9", "async-mutex": "^0.4.0", "chalk": "^4.1.2", "chokidar": "^3.5.3", @@ -77,7 +77,7 @@ "@types/workerpool": "^6.4.0", "@types/ws": "^8.5.5", "@types/yargs": "^17.0.24", - "esbuild": "^0.19.1", + "esbuild": "0.19.2", "prettier": "^3.0.0", "tsx": "^3.12.7", "typescript": "^5.0.4" @@ -140,6 +140,70 @@ "source-map-support": "^0.5.21" } }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", @@ -156,6 +220,278 @@ "node": ">=12" } }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { "version": "0.17.19", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", @@ -203,6 +539,66 @@ "get-tsconfig": "^4.4.0" } }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.2.tgz", + "integrity": "sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.2.tgz", + "integrity": "sha512-lsB65vAbe90I/Qe10OjkmrdxSX4UJDjosDgb8sZUKcg3oefEuW2OT2Vozz8ef7wrJbMcmhvCC+hciF8jY/uAkw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.2.tgz", + "integrity": "sha512-qK/TpmHt2M/Hg82WXHRc/W/2SGo/l1thtDHZWqFq7oi24AjZ4O/CpPSu6ZuYKFkEgmZlFoa7CooAyYmuvnaG8w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.2.tgz", + "integrity": "sha512-Ora8JokrvrzEPEpZO18ZYXkH4asCdc1DLdcVy8TGf5eWtPO1Ie4WroEJzwI52ZGtpODy3+m0a2yEX9l+KUn0tA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/darwin-x64": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.2.tgz", @@ -218,6 +614,261 @@ "node": ">=12" } }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.2.tgz", + "integrity": "sha512-YbPY2kc0acfzL1VPVK6EnAlig4f+l8xmq36OZkU0jzBVHcOTyQDhnKQaLzZudNJQyymd9OqQezeaBgkTGdTGeQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.2.tgz", + "integrity": "sha512-nSO5uZT2clM6hosjWHAsS15hLrwCvIWx+b2e3lZ3MwbYSaXwvfO528OF+dLjas1g3bZonciivI8qKR/Hm7IWGw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.2.tgz", + "integrity": "sha512-Odalh8hICg7SOD7XCj0YLpYCEc+6mkoq63UnExDCiRA2wXEmGlK5JVrW50vZR9Qz4qkvqnHcpH+OFEggO3PgTg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.2.tgz", + "integrity": "sha512-ig2P7GeG//zWlU0AggA3pV1h5gdix0MA3wgB+NsnBXViwiGgY77fuN9Wr5uoCrs2YzaYfogXgsWZbm+HGr09xg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.2.tgz", + "integrity": "sha512-mLfp0ziRPOLSTek0Gd9T5B8AtzKAkoZE70fneiiyPlSnUKKI4lp+mGEnQXcQEHLJAcIYDPSyBvsUbKUG2ri/XQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.2.tgz", + "integrity": "sha512-hn28+JNDTxxCpnYjdDYVMNTR3SKavyLlCHHkufHV91fkewpIyQchS1d8wSbmXhs1fiYDpNww8KTFlJ1dHsxeSw==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.2.tgz", + "integrity": "sha512-KbXaC0Sejt7vD2fEgPoIKb6nxkfYW9OmFUK9XQE4//PvGIxNIfPk1NmlHmMg6f25x57rpmEFrn1OotASYIAaTg==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.2.tgz", + "integrity": "sha512-dJ0kE8KTqbiHtA3Fc/zn7lCd7pqVr4JcT0JqOnbj4LLzYnp+7h8Qi4yjfq42ZlHfhOCM42rBh0EwHYLL6LEzcw==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.2.tgz", + "integrity": "sha512-7Z/jKNFufZ/bbu4INqqCN6DDlrmOTmdw6D0gH+6Y7auok2r02Ur661qPuXidPOJ+FSgbEeQnnAGgsVynfLuOEw==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.2.tgz", + "integrity": "sha512-U+RinR6aXXABFCcAY4gSlv4CL1oOVvSSCdseQmGO66H+XyuQGZIUdhG56SZaDJQcLmrSfRmx5XZOWyCJPRqS7g==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.2.tgz", + "integrity": "sha512-oxzHTEv6VPm3XXNaHPyUTTte+3wGv7qVQtqaZCrgstI16gCuhNOtBXLEBkBREP57YTd68P0VgDgG73jSD8bwXQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.2.tgz", + "integrity": "sha512-WNa5zZk1XpTTwMDompZmvQLHszDDDN7lYjEHCUmAGB83Bgs20EMs7ICD+oKeT6xt4phV4NDdSi/8OfjPbSbZfQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.2.tgz", + "integrity": "sha512-S6kI1aT3S++Dedb7vxIuUOb3oAxqxk2Rh5rOXOTYnzN8JzW1VzBd+IqPiSpgitu45042SYD3HCoEyhLKQcDFDw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.2.tgz", + "integrity": "sha512-VXSSMsmb+Z8LbsQGcBMiM+fYObDNRm8p7tkUDMPG/g4fhFX5DEFmjxIEa3N8Zr96SjsJ1woAhF0DUnS3MF3ARw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.2.tgz", + "integrity": "sha512-5NayUlSAyb5PQYFAU9x3bHdsqB88RC3aM9lKDAz4X1mo/EchMIT1Q+pSeBXNgkfNmRecLXA0O8xP+x8V+g/LKg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.2.tgz", + "integrity": "sha512-47gL/ek1v36iN0wL9L4Q2MFdujR0poLZMJwhO2/N3gA89jgHp4MR8DKCmwYtGNksbfJb9JoTtbkoe6sDhg2QTA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.2.tgz", + "integrity": "sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@floating-ui/core": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.3.1.tgz", @@ -248,30 +899,75 @@ } }, "node_modules/@napi-rs/simple-git": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git/-/simple-git-0.1.8.tgz", - "integrity": "sha512-BvOMdkkofTz6lEE35itJ/laUokPhr/5ToMGlOH25YnhLD2yN1KpRAT4blW9tT8281/1aZjW3xyi73bs//IrDKA==", + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git/-/simple-git-0.1.9.tgz", + "integrity": "sha512-qKzDS0+VjMvVyU28px+C6zlD1HKy83NIdYzfMQWa/g/V1iG/Ic8uwrS2ihHfm7mp7X0PPrmINLiTTi6ieUIKfw==", "engines": { "node": ">= 10" }, "optionalDependencies": { - "@napi-rs/simple-git-android-arm-eabi": "0.1.8", - "@napi-rs/simple-git-android-arm64": "0.1.8", - "@napi-rs/simple-git-darwin-arm64": "0.1.8", - "@napi-rs/simple-git-darwin-x64": "0.1.8", - "@napi-rs/simple-git-linux-arm-gnueabihf": "0.1.8", - "@napi-rs/simple-git-linux-arm64-gnu": "0.1.8", - "@napi-rs/simple-git-linux-arm64-musl": "0.1.8", - "@napi-rs/simple-git-linux-x64-gnu": "0.1.8", - "@napi-rs/simple-git-linux-x64-musl": "0.1.8", - "@napi-rs/simple-git-win32-arm64-msvc": "0.1.8", - "@napi-rs/simple-git-win32-x64-msvc": "0.1.8" + "@napi-rs/simple-git-android-arm-eabi": "0.1.9", + "@napi-rs/simple-git-android-arm64": "0.1.9", + "@napi-rs/simple-git-darwin-arm64": "0.1.9", + "@napi-rs/simple-git-darwin-x64": "0.1.9", + "@napi-rs/simple-git-linux-arm-gnueabihf": "0.1.9", + "@napi-rs/simple-git-linux-arm64-gnu": "0.1.9", + "@napi-rs/simple-git-linux-arm64-musl": "0.1.9", + "@napi-rs/simple-git-linux-x64-gnu": "0.1.9", + "@napi-rs/simple-git-linux-x64-musl": "0.1.9", + "@napi-rs/simple-git-win32-arm64-msvc": "0.1.9", + "@napi-rs/simple-git-win32-x64-msvc": "0.1.9" + } + }, + "node_modules/@napi-rs/simple-git-android-arm-eabi": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm-eabi/-/simple-git-android-arm-eabi-0.1.9.tgz", + "integrity": "sha512-9D4JnfePMpgL4pg9aMUX7/TIWEUQ+Tgx8n3Pf8TNCMGjUbImJyYsDSLJzbcv9wH7srgn4GRjSizXFJHAPjzEug==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-android-arm64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm64/-/simple-git-android-arm64-0.1.9.tgz", + "integrity": "sha512-Krilsw0gPrrASZzudNEl9pdLuNbhoTK0j7pUbfB8FRifpPdFB/zouwuEm0aSnsDXN4ftGrmGG82kuiR/2MeoPg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-darwin-arm64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-arm64/-/simple-git-darwin-arm64-0.1.9.tgz", + "integrity": "sha512-H/F09nDgYjv4gcFrZBgdTKkZEepqt0KLYcCJuUADuxkKupmjLdecMhypXLk13AzvLW4UQI7NlLTLDXUFLyr2BA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" } }, "node_modules/@napi-rs/simple-git-darwin-x64": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-x64/-/simple-git-darwin-x64-0.1.8.tgz", - "integrity": "sha512-Vb21U+v3tPJNl+8JtIHHT8HGe6WZ8o1Tq3f6p+Jx9Cz71zEbcIiB9FCEMY1knS/jwQEOuhhlI9Qk7d4HY+rprA==", + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-x64/-/simple-git-darwin-x64-0.1.9.tgz", + "integrity": "sha512-jBR2xS9nVPqmHv0TWz874W0m/d453MGrMeLjB+boK5IPPLhg3AWIZj0aN9jy2Je1BGVAa0w3INIQJtBBeB6kFA==", "cpu": [ "x64" ], @@ -283,6 +979,111 @@ "node": ">= 10" } }, + "node_modules/@napi-rs/simple-git-linux-arm-gnueabihf": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm-gnueabihf/-/simple-git-linux-arm-gnueabihf-0.1.9.tgz", + "integrity": "sha512-3n0+VpO4YfZxndZ0sCvsHIvsazd+JmbSjrlTRBCnJeAU1/sfos3skNZtKGZksZhjvd+3o+/GFM8L7Xnv01yggA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-arm64-gnu": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-gnu/-/simple-git-linux-arm64-gnu-0.1.9.tgz", + "integrity": "sha512-lIzf0KHU2SKC12vMrWwCtysG2Sdt31VHRPMUiz9lD9t3xwVn8qhFSTn5yDkTeG3rgX6o0p5EKalfQN5BXsJq2w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-arm64-musl": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-musl/-/simple-git-linux-arm64-musl-0.1.9.tgz", + "integrity": "sha512-KQozUoNXrxrB8k741ncWXSiMbjl1AGBGfZV21PANzUM8wH4Yem2bg3kfglYS/QIx3udspsT35I9abu49n7D1/w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-x64-gnu": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-gnu/-/simple-git-linux-x64-gnu-0.1.9.tgz", + "integrity": "sha512-O/Niui5mnHPcK3iYC3ui8wgERtJWsQ3Y74W/09t0bL/3dgzGMl4oQt0qTj9dWCsnoGsIEYHPzwCBp/2vqYp/pw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-x64-musl": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-musl/-/simple-git-linux-x64-musl-0.1.9.tgz", + "integrity": "sha512-L9n+e8Wn3hKr3RsIdY8GaB+ry4xZ4BaGwyKExgoB8nDGQuRUY9oP6p0WA4hWfJvJnU1H6hvo36a5UFPReyBO7A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-win32-arm64-msvc": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-arm64-msvc/-/simple-git-win32-arm64-msvc-0.1.9.tgz", + "integrity": "sha512-Z6Ja/SZK+lMvRWaxj7wjnvSbAsGrH006sqZo8P8nxKUdZfkVvoCaAWr1r0cfkk2Z3aijLLtD+vKeXGlUPH6gGQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-win32-x64-msvc": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-x64-msvc/-/simple-git-win32-x64-msvc-0.1.9.tgz", + "integrity": "sha512-VAZj1UvC+R2MjKOD3I/Y7dmQlHWAYy4omhReQJRpbCf+oGCBi9CWiIduGqeYEq723nLIKdxP7XjaO0wl1NnUww==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/package.json b/package.json index fc5463688..0265ce9b8 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "dependencies": { "@clack/prompts": "^0.6.3", "@floating-ui/dom": "^1.4.0", - "@napi-rs/simple-git": "^0.1.8", + "@napi-rs/simple-git": "0.1.9", "async-mutex": "^0.4.0", "chalk": "^4.1.2", "chokidar": "^3.5.3", @@ -97,7 +97,7 @@ "@types/workerpool": "^6.4.0", "@types/ws": "^8.5.5", "@types/yargs": "^17.0.24", - "esbuild": "^0.19.1", + "esbuild": "0.19.2", "prettier": "^3.0.0", "tsx": "^3.12.7", "typescript": "^5.0.4" From 3209f7c3b7837fd845cbef645155a9484ea0253a Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 23 Aug 2023 09:19:00 -0700 Subject: [PATCH 014/140] deps: native addons for lightningcss --- package-lock.json | 183 ++++++++++++++++++++++++++++++++++++++++++---- package.json | 2 +- 2 files changed, 169 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index be51eb518..480d89185 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "hast-util-to-string": "^2.0.0", "is-absolute-url": "^4.0.1", "js-yaml": "^4.1.0", - "lightningcss": "^1.21.5", + "lightningcss": "1.21.7", "mdast-util-find-and-replace": "^2.2.2", "mdast-util-to-hast": "^12.3.0", "mdast-util-to-string": "^3.2.0", @@ -3408,9 +3408,9 @@ } }, "node_modules/lightningcss": { - "version": "1.21.5", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.21.5.tgz", - "integrity": "sha512-/pEUPeih2EwIx9n4T82aOG6CInN83tl/mWlw6B5gWLf36UplQi1L+5p3FUHsdt4fXVfOkkh9KIaM3owoq7ss8A==", + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.21.7.tgz", + "integrity": "sha512-xITZyh5sLFwRPYUSw15T00Rm7gcQ1qOPuQwNOcvHsTm6nLWTQ723w7zl42wrC5t+xtdg6FPmnXHml1nZxxvp1w==", "dependencies": { "detect-libc": "^1.0.3" }, @@ -3422,20 +3422,40 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-darwin-arm64": "1.21.5", - "lightningcss-darwin-x64": "1.21.5", - "lightningcss-linux-arm-gnueabihf": "1.21.5", - "lightningcss-linux-arm64-gnu": "1.21.5", - "lightningcss-linux-arm64-musl": "1.21.5", - "lightningcss-linux-x64-gnu": "1.21.5", - "lightningcss-linux-x64-musl": "1.21.5", - "lightningcss-win32-x64-msvc": "1.21.5" + "lightningcss-darwin-arm64": "1.21.7", + "lightningcss-darwin-x64": "1.21.7", + "lightningcss-freebsd-x64": "1.21.7", + "lightningcss-linux-arm-gnueabihf": "1.21.7", + "lightningcss-linux-arm64-gnu": "1.21.7", + "lightningcss-linux-arm64-musl": "1.21.7", + "lightningcss-linux-x64-gnu": "1.21.7", + "lightningcss-linux-x64-musl": "1.21.7", + "lightningcss-win32-x64-msvc": "1.21.7" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.21.7.tgz", + "integrity": "sha512-tt7hIsFio9jZofTVHtCACz6rB6c9RyABMXfA9A/VcKOjS3sq+koX/QkRJWY06utwOImbJIXBC5hbg9t3RkPUAQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.21.5", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.21.5.tgz", - "integrity": "sha512-MSJhmej/U9MrdPxDk7+FWhO8+UqVoZUHG4VvKT5RQ4RJtqtANTiWiI97LvoVNMtdMnHaKs1Pkji6wHUFxjJsHQ==", + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.21.7.tgz", + "integrity": "sha512-F4gS4bf7eWekfPT+TxJNm/pF+QRgZiTrTkQH6cw4/UWfdeZISfuhD5El2dm16giFnY0K5ylIwO+ZusgYNkGSXA==", "cpu": [ "x64" ], @@ -3451,6 +3471,139 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.21.7.tgz", + "integrity": "sha512-RMfNzJWXCSfPnL55fcLWEAadcY6QUFT0S8NceNKYzp1KiCZtkJIy6RQ5SaVxPzRqd3iMsahUf5sfnG8N1UQSNQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.21.7.tgz", + "integrity": "sha512-biSRUDZNx7vubWP1jArw/qqfZKPGpkV/qzunasZzxmqijbZ43sW9faDQYxWNcxPWljJJdF/qs6qcurYFovWtrQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.21.7.tgz", + "integrity": "sha512-PENY8QekqL9TG3AY/A7rkUBb5ymefGxea7Oe7+x7Hbw4Bz4Hpj5cec5OoMypMqFbURPmpi0fTWx4vSWUPzpDcA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.21.7.tgz", + "integrity": "sha512-pfOipKvA/0X1OjRaZt3870vnV9UGBSjayIqHh0fGx/+aRz3O0MVFHE/60P2UWXpM3YGJEw/hMWtNkrFwqOge8A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.21.7.tgz", + "integrity": "sha512-dgcsis4TAA7s0ia4f31QHX+G4PWPwxk+wJaEQLaV0NdJs09O5hHoA8DpLEr8nrvc/tsRTyVNBP1rDtgzySjpXg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.21.7.tgz", + "integrity": "sha512-A+9dXpxld3p4Cd6fxev2eqEvaauYtrgNpXV3t7ioCJy30Oj9nYiNGwiGusM+4MJVcEpUPGUGiuAqY4sWilRDwA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.21.7.tgz", + "integrity": "sha512-07/8vogEq+C/mF99pdMhh/f19/xreq8N9Ca6AWeVHZIdODyF/pt6KdKSCWDZWIn+3CUxI8gCJWuUWyOc3xymvw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", diff --git a/package.json b/package.json index 0265ce9b8..3191e850d 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "hast-util-to-string": "^2.0.0", "is-absolute-url": "^4.0.1", "js-yaml": "^4.1.0", - "lightningcss": "^1.21.5", + "lightningcss": "1.21.7", "mdast-util-find-and-replace": "^2.2.2", "mdast-util-to-hast": "^12.3.0", "mdast-util-to-string": "^3.2.0", From a1a1e7e1e0c06f2f7b759c5aecd6a9ceba3e2717 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 23 Aug 2023 11:36:34 -0700 Subject: [PATCH 015/140] fix: builds should no accumulate on repeated changes (closes #404) --- quartz/bootstrap-cli.mjs | 16 ++++----- quartz/build.ts | 78 ++++++++++++++++++++-------------------- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/quartz/bootstrap-cli.mjs b/quartz/bootstrap-cli.mjs index 47c58ab0d..1656d758c 100755 --- a/quartz/bootstrap-cli.mjs +++ b/quartz/bootstrap-cli.mjs @@ -393,10 +393,16 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. }) const buildMutex = new Mutex() - const timeoutIds = new Set() + let lastBuildMs = 0 let cleanupBuild = null const build = async (clientRefresh) => { + const buildStart = new Date().getTime() + lastBuildMs = buildStart const release = await buildMutex.acquire() + if (lastBuildMs > buildStart) { + release() + return + } if (cleanupBuild) { await cleanupBuild() @@ -428,12 +434,6 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. clientRefresh() } - const rebuild = (clientRefresh) => { - timeoutIds.forEach((id) => clearTimeout(id)) - timeoutIds.clear() - timeoutIds.add(setTimeout(() => build(clientRefresh), 250)) - } - if (argv.serve) { const connections = [] const clientRefresh = () => connections.forEach((conn) => conn.send("rebuild")) @@ -539,7 +539,7 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. ignoreInitial: true, }) .on("all", async () => { - rebuild(clientRefresh) + build(clientRefresh) }) } else { await build(() => {}) diff --git a/quartz/build.ts b/quartz/build.ts index 8b1d31834..58137d178 100644 --- a/quartz/build.ts +++ b/quartz/build.ts @@ -81,7 +81,7 @@ async function startServing( } const initialSlugs = ctx.allSlugs - const timeoutIds: Set> = new Set() + let lastBuildMs = 0 const toRebuild: Set = new Set() const toRemove: Set = new Set() const trackedAssets: Set = new Set() @@ -111,49 +111,50 @@ async function startServing( } // debounce rebuilds every 250ms - timeoutIds.add( - setTimeout(async () => { - const release = await mut.acquire() - timeoutIds.forEach((id) => clearTimeout(id)) - timeoutIds.clear() - const perf = new PerfTimer() - console.log(chalk.yellow("Detected change, rebuilding...")) - try { - const filesToRebuild = [...toRebuild].filter((fp) => !toRemove.has(fp)) + const buildStart = new Date().getTime() + lastBuildMs = buildStart + const release = await mut.acquire() + if (lastBuildMs > buildStart) { + release() + return + } - const trackedSlugs = [...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets])] - .filter((fp) => !toRemove.has(fp)) - .map((fp) => slugifyFilePath(path.posix.relative(argv.directory, fp) as FilePath)) + const perf = new PerfTimer() + console.log(chalk.yellow("Detected change, rebuilding...")) + try { + const filesToRebuild = [...toRebuild].filter((fp) => !toRemove.has(fp)) - ctx.allSlugs = [...new Set([...initialSlugs, ...trackedSlugs])] - const parsedContent = await parseMarkdown(ctx, filesToRebuild) - for (const content of parsedContent) { - const [_tree, vfile] = content - contentMap.set(vfile.data.filePath!, content) - } + const trackedSlugs = [...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets])] + .filter((fp) => !toRemove.has(fp)) + .map((fp) => slugifyFilePath(path.posix.relative(argv.directory, fp) as FilePath)) - for (const fp of toRemove) { - contentMap.delete(fp) - } + ctx.allSlugs = [...new Set([...initialSlugs, ...trackedSlugs])] + const parsedContent = await parseMarkdown(ctx, filesToRebuild) + for (const content of parsedContent) { + const [_tree, vfile] = content + contentMap.set(vfile.data.filePath!, content) + } - // TODO: we can probably traverse the link graph to figure out what's safe to delete here - // instead of just deleting everything - await rimraf(argv.output) - const parsedFiles = [...contentMap.values()] - const filteredContent = filterContent(ctx, parsedFiles) - await emitContent(ctx, filteredContent) - console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`)) - } catch { - console.log(chalk.yellow(`Rebuild failed. Waiting on a change to fix the error...`)) - } + for (const fp of toRemove) { + contentMap.delete(fp) + } - clientRefresh() - toRebuild.clear() - toRemove.clear() - release() - }, 250), - ) + const parsedFiles = [...contentMap.values()] + const filteredContent = filterContent(ctx, parsedFiles) + // TODO: we can probably traverse the link graph to figure out what's safe to delete here + // instead of just deleting everything + await rimraf(argv.output) + await emitContent(ctx, filteredContent) + console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`)) + } catch { + console.log(chalk.yellow(`Rebuild failed. Waiting on a change to fix the error...`)) + } + + clientRefresh() + toRebuild.clear() + toRemove.clear() + release() } const watcher = chokidar.watch(".", { @@ -168,7 +169,6 @@ async function startServing( .on("unlink", (fp) => rebuild(fp, "delete")) return async () => { - timeoutIds.forEach((id) => clearTimeout(id)) await watcher.close() } } From 0aaf88b8521e2bc667cae525356eea3550ad9c96 Mon Sep 17 00:00:00 2001 From: kanpov <71177577+kanpov@users.noreply.github.com> Date: Wed, 23 Aug 2023 22:09:04 +0300 Subject: [PATCH 016/140] Fix #403 by moving documentation to separate directory to avoid merge conflicts (#405) --- content/.gitkeep | 0 {content => docs}/advanced/architecture.md | 0 {content => docs}/advanced/creating components.md | 0 {content => docs}/advanced/making plugins.md | 0 {content => docs}/advanced/paths.md | 0 {content => docs}/authoring content.md | 0 {content => docs}/build.md | 0 {content => docs}/configuration.md | 0 {content => docs}/features/Latex.md | 0 {content => docs}/features/Mermaid diagrams.md | 0 .../features/Obsidian compatibility.md | 0 {content => docs}/features/RSS Feed.md | 0 {content => docs}/features/SPA Routing.md | 0 {content => docs}/features/backlinks.md | 0 {content => docs}/features/callouts.md | 0 {content => docs}/features/darkmode.md | 0 .../features/folder and tag listings.md | 0 {content => docs}/features/full-text search.md | 0 {content => docs}/features/graph view.md | 0 {content => docs}/features/index.md | 0 {content => docs}/features/popover previews.md | 0 {content => docs}/features/private pages.md | 0 {content => docs}/features/recent notes.md | 0 {content => docs}/features/syntax highlighting.md | 0 {content => docs}/features/table of contents.md | 0 {content => docs}/features/upcoming features.md | 0 {content => docs}/features/wikilinks.md | 0 {content => docs}/hosting.md | 0 {content => docs}/images/dns records.png | Bin {content => docs}/images/quartz layout.png | Bin .../images/quartz transform pipeline.png | Bin {content => docs}/index.md | 0 {content => docs}/layout.md | 0 {content => docs}/migrating from Quartz 3.md | 0 {content => docs}/philosophy.md | 0 {content => docs}/showcase.md | 0 {content => docs}/tags/component.md | 0 {content => docs}/upgrading.md | 0 quartz/bootstrap-cli.mjs | 4 +--- 39 files changed, 1 insertion(+), 3 deletions(-) create mode 100644 content/.gitkeep rename {content => docs}/advanced/architecture.md (100%) rename {content => docs}/advanced/creating components.md (100%) rename {content => docs}/advanced/making plugins.md (100%) rename {content => docs}/advanced/paths.md (100%) rename {content => docs}/authoring content.md (100%) rename {content => docs}/build.md (100%) rename {content => docs}/configuration.md (100%) rename {content => docs}/features/Latex.md (100%) rename {content => docs}/features/Mermaid diagrams.md (100%) rename {content => docs}/features/Obsidian compatibility.md (100%) rename {content => docs}/features/RSS Feed.md (100%) rename {content => docs}/features/SPA Routing.md (100%) rename {content => docs}/features/backlinks.md (100%) rename {content => docs}/features/callouts.md (100%) rename {content => docs}/features/darkmode.md (100%) rename {content => docs}/features/folder and tag listings.md (100%) rename {content => docs}/features/full-text search.md (100%) rename {content => docs}/features/graph view.md (100%) rename {content => docs}/features/index.md (100%) rename {content => docs}/features/popover previews.md (100%) rename {content => docs}/features/private pages.md (100%) rename {content => docs}/features/recent notes.md (100%) rename {content => docs}/features/syntax highlighting.md (100%) rename {content => docs}/features/table of contents.md (100%) rename {content => docs}/features/upcoming features.md (100%) rename {content => docs}/features/wikilinks.md (100%) rename {content => docs}/hosting.md (100%) rename {content => docs}/images/dns records.png (100%) rename {content => docs}/images/quartz layout.png (100%) rename {content => docs}/images/quartz transform pipeline.png (100%) rename {content => docs}/index.md (100%) rename {content => docs}/layout.md (100%) rename {content => docs}/migrating from Quartz 3.md (100%) rename {content => docs}/philosophy.md (100%) rename {content => docs}/showcase.md (100%) rename {content => docs}/tags/component.md (100%) rename {content => docs}/upgrading.md (100%) diff --git a/content/.gitkeep b/content/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/content/advanced/architecture.md b/docs/advanced/architecture.md similarity index 100% rename from content/advanced/architecture.md rename to docs/advanced/architecture.md diff --git a/content/advanced/creating components.md b/docs/advanced/creating components.md similarity index 100% rename from content/advanced/creating components.md rename to docs/advanced/creating components.md diff --git a/content/advanced/making plugins.md b/docs/advanced/making plugins.md similarity index 100% rename from content/advanced/making plugins.md rename to docs/advanced/making plugins.md diff --git a/content/advanced/paths.md b/docs/advanced/paths.md similarity index 100% rename from content/advanced/paths.md rename to docs/advanced/paths.md diff --git a/content/authoring content.md b/docs/authoring content.md similarity index 100% rename from content/authoring content.md rename to docs/authoring content.md diff --git a/content/build.md b/docs/build.md similarity index 100% rename from content/build.md rename to docs/build.md diff --git a/content/configuration.md b/docs/configuration.md similarity index 100% rename from content/configuration.md rename to docs/configuration.md diff --git a/content/features/Latex.md b/docs/features/Latex.md similarity index 100% rename from content/features/Latex.md rename to docs/features/Latex.md diff --git a/content/features/Mermaid diagrams.md b/docs/features/Mermaid diagrams.md similarity index 100% rename from content/features/Mermaid diagrams.md rename to docs/features/Mermaid diagrams.md diff --git a/content/features/Obsidian compatibility.md b/docs/features/Obsidian compatibility.md similarity index 100% rename from content/features/Obsidian compatibility.md rename to docs/features/Obsidian compatibility.md diff --git a/content/features/RSS Feed.md b/docs/features/RSS Feed.md similarity index 100% rename from content/features/RSS Feed.md rename to docs/features/RSS Feed.md diff --git a/content/features/SPA Routing.md b/docs/features/SPA Routing.md similarity index 100% rename from content/features/SPA Routing.md rename to docs/features/SPA Routing.md diff --git a/content/features/backlinks.md b/docs/features/backlinks.md similarity index 100% rename from content/features/backlinks.md rename to docs/features/backlinks.md diff --git a/content/features/callouts.md b/docs/features/callouts.md similarity index 100% rename from content/features/callouts.md rename to docs/features/callouts.md diff --git a/content/features/darkmode.md b/docs/features/darkmode.md similarity index 100% rename from content/features/darkmode.md rename to docs/features/darkmode.md diff --git a/content/features/folder and tag listings.md b/docs/features/folder and tag listings.md similarity index 100% rename from content/features/folder and tag listings.md rename to docs/features/folder and tag listings.md diff --git a/content/features/full-text search.md b/docs/features/full-text search.md similarity index 100% rename from content/features/full-text search.md rename to docs/features/full-text search.md diff --git a/content/features/graph view.md b/docs/features/graph view.md similarity index 100% rename from content/features/graph view.md rename to docs/features/graph view.md diff --git a/content/features/index.md b/docs/features/index.md similarity index 100% rename from content/features/index.md rename to docs/features/index.md diff --git a/content/features/popover previews.md b/docs/features/popover previews.md similarity index 100% rename from content/features/popover previews.md rename to docs/features/popover previews.md diff --git a/content/features/private pages.md b/docs/features/private pages.md similarity index 100% rename from content/features/private pages.md rename to docs/features/private pages.md diff --git a/content/features/recent notes.md b/docs/features/recent notes.md similarity index 100% rename from content/features/recent notes.md rename to docs/features/recent notes.md diff --git a/content/features/syntax highlighting.md b/docs/features/syntax highlighting.md similarity index 100% rename from content/features/syntax highlighting.md rename to docs/features/syntax highlighting.md diff --git a/content/features/table of contents.md b/docs/features/table of contents.md similarity index 100% rename from content/features/table of contents.md rename to docs/features/table of contents.md diff --git a/content/features/upcoming features.md b/docs/features/upcoming features.md similarity index 100% rename from content/features/upcoming features.md rename to docs/features/upcoming features.md diff --git a/content/features/wikilinks.md b/docs/features/wikilinks.md similarity index 100% rename from content/features/wikilinks.md rename to docs/features/wikilinks.md diff --git a/content/hosting.md b/docs/hosting.md similarity index 100% rename from content/hosting.md rename to docs/hosting.md diff --git a/content/images/dns records.png b/docs/images/dns records.png similarity index 100% rename from content/images/dns records.png rename to docs/images/dns records.png diff --git a/content/images/quartz layout.png b/docs/images/quartz layout.png similarity index 100% rename from content/images/quartz layout.png rename to docs/images/quartz layout.png diff --git a/content/images/quartz transform pipeline.png b/docs/images/quartz transform pipeline.png similarity index 100% rename from content/images/quartz transform pipeline.png rename to docs/images/quartz transform pipeline.png diff --git a/content/index.md b/docs/index.md similarity index 100% rename from content/index.md rename to docs/index.md diff --git a/content/layout.md b/docs/layout.md similarity index 100% rename from content/layout.md rename to docs/layout.md diff --git a/content/migrating from Quartz 3.md b/docs/migrating from Quartz 3.md similarity index 100% rename from content/migrating from Quartz 3.md rename to docs/migrating from Quartz 3.md diff --git a/content/philosophy.md b/docs/philosophy.md similarity index 100% rename from content/philosophy.md rename to docs/philosophy.md diff --git a/content/showcase.md b/docs/showcase.md similarity index 100% rename from content/showcase.md rename to docs/showcase.md diff --git a/content/tags/component.md b/docs/tags/component.md similarity index 100% rename from content/tags/component.md rename to docs/tags/component.md diff --git a/content/upgrading.md b/docs/upgrading.md similarity index 100% rename from content/upgrading.md rename to docs/upgrading.md diff --git a/quartz/bootstrap-cli.mjs b/quartz/bootstrap-cli.mjs index 1656d758c..b9733171f 100755 --- a/quartz/bootstrap-cli.mjs +++ b/quartz/bootstrap-cli.mjs @@ -162,7 +162,6 @@ yargs(hideBin(process.argv)) label: "Symlink an existing folder", hint: "don't select this unless you know what you are doing!", }, - { value: "keep", label: "Keep the existing files" }, ], }), ) @@ -176,6 +175,7 @@ yargs(hideBin(process.argv)) } } + await fs.promises.unlink(path.join(contentFolder, ".gitkeep")) if (setupStrategy === "copy" || setupStrategy === "symlink") { const originalFolder = escapePath( exitIfCancel( @@ -205,8 +205,6 @@ yargs(hideBin(process.argv)) await fs.promises.symlink(originalFolder, contentFolder, "dir") } } else if (setupStrategy === "new") { - await rmContentFolder() - await fs.promises.mkdir(contentFolder) await fs.promises.writeFile( path.join(contentFolder, "index.md"), `--- From b99eb7ebce21065b7ff59cdd3226d4fc173b3878 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 23 Aug 2023 12:11:16 -0700 Subject: [PATCH 017/140] docs: whitespace --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4b4731c9b..3e40e819d 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Quartz v4 features a from-the-ground rewrite focusing on end-user extensibility [Join the Discord Community](https://discord.gg/cRFFHYye7t) + ## Sponsors

From eed4472aeecdcb0f2b233df69884f03bd45fc293 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 23 Aug 2023 12:18:50 -0700 Subject: [PATCH 018/140] fix: use proper full base for links.ts --- README.md | 1 - quartz/plugins/transformers/links.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 3e40e819d..4b4731c9b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ Quartz v4 features a from-the-ground rewrite focusing on end-user extensibility [Join the Discord Community](https://discord.gg/cRFFHYye7t) - ## Sponsors

diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts index f8da36c4d..26c4a3228 100644 --- a/quartz/plugins/transformers/links.ts +++ b/quartz/plugins/transformers/links.ts @@ -63,7 +63,7 @@ export const CrawlLinks: QuartzTransformerPlugin | undefined> = // url.resolve is considered legacy // WHATWG equivalent https://nodejs.dev/en/api/v18/url/#urlresolvefrom-to - const url = new URL(dest, `resolve://${curSlug}`) + const url = new URL(dest, `https://base.com/${curSlug}`) const canonicalDest = url.pathname const [destCanonical, _destAnchor] = splitAnchor(canonicalDest) From 960c1814d07449dd9fd5e70eea770ba762780b53 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 23 Aug 2023 12:23:49 -0700 Subject: [PATCH 019/140] docs: make incompability of trailing slashes clear --- docs/hosting.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/hosting.md b/docs/hosting.md index d6ccd0bd0..d648f5587 100644 --- a/docs/hosting.md +++ b/docs/hosting.md @@ -30,6 +30,9 @@ To add a custom domain, check out [Cloudflare's documentation](https://developer Like Quartz 3, you can deploy the site generated by Quartz 4 via GitHub Pages. +> [!warning] +> Quartz generates files in the format of `file.html` instead of `file/index.html` which means the trailing slashes for _non-folder paths_ are dropped. As GitHub pages does not do this redirect, this may cause existing links to your site that use trailing slashes to break. If not breaking existing links is important to you, consider using [[#Cloudflare Pages]]. + In your local Quartz, create a new file `quartz/.github/workflows/deploy.yml`. ```yaml title="quartz/.github/workflows/deploy.yml" From bfb416b35a02dabbdaedc9e3c8980f8b4aadd9aa Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 23 Aug 2023 13:10:23 -0700 Subject: [PATCH 020/140] fix: text wrap in popover --- package.json | 1 + quartz/components/pages/FolderContent.tsx | 4 +++- quartz/components/pages/TagContent.tsx | 4 +++- quartz/components/styles/popover.scss | 1 + 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3191e850d..b65ed858f 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "url": "https://github.com/jackyzha0/quartz.git" }, "scripts": { + "docs": "npx quartz build --serve -d docs", "check": "tsc --noEmit && npx prettier . --check", "format": "npx prettier . --write", "test": "tsx ./quartz/util/path.test.ts", diff --git a/quartz/components/pages/FolderContent.tsx b/quartz/components/pages/FolderContent.tsx index 6c5fd7d23..dc076c4a1 100644 --- a/quartz/components/pages/FolderContent.tsx +++ b/quartz/components/pages/FolderContent.tsx @@ -33,7 +33,9 @@ function FolderContent(props: QuartzComponentProps) { return (

-
{content}
+
+

{content}

+

{allPagesInFolder.length} items under this folder.

diff --git a/quartz/components/pages/TagContent.tsx b/quartz/components/pages/TagContent.tsx index 73ee465c0..fb72e284b 100644 --- a/quartz/components/pages/TagContent.tsx +++ b/quartz/components/pages/TagContent.tsx @@ -37,7 +37,9 @@ function TagContent(props: QuartzComponentProps) { return (
-
{content}
+
+

{content}

+

Found {tags.length} total tags.

{tags.map((tag) => { diff --git a/quartz/components/styles/popover.scss b/quartz/components/styles/popover.scss index 21e6b7228..fae0e121b 100644 --- a/quartz/components/styles/popover.scss +++ b/quartz/components/styles/popover.scss @@ -34,6 +34,7 @@ border-radius: 5px; box-shadow: 6px 6px 36px 0 rgba(0, 0, 0, 0.25); overflow: auto; + white-space: normal; } h1 { From 632c27b7ec133d15d890eb28c98eb9716ca01407 Mon Sep 17 00:00:00 2001 From: Zane Helton Date: Wed, 23 Aug 2023 18:14:23 -0400 Subject: [PATCH 021/140] docs: update `hosting.md` with Vercel hosting instructions (#406) * Update hosting.md with Vercel hosting instructions * Update docs/hosting.md Co-authored-by: Jacky Zhao * Update docs/hosting.md Co-authored-by: Jacky Zhao * Run npm run format --------- Co-authored-by: Jacky Zhao --- docs/hosting.md | 50 +++++++++++++++++++++++++++ docs/showcase.md | 2 +- quartz/components/styles/popover.scss | 4 +-- quartz/components/styles/search.scss | 4 +-- quartz/components/styles/toc.scss | 4 +-- 5 files changed, 54 insertions(+), 10 deletions(-) diff --git a/docs/hosting.md b/docs/hosting.md index d648f5587..01d130fd3 100644 --- a/docs/hosting.md +++ b/docs/hosting.md @@ -116,3 +116,53 @@ See the [GitHub documentation](https://docs.github.com/en/pages/configuring-a-cu > There could be many different reasons why your changes aren't showing up but the most likely reason is that you forgot to push your changes to GitHub. > > Make sure you save your changes to Git and sync it to GitHub by doing `npx quartz sync`. This will also make sure to pull any updates you may have made from other devices so you have them locally. + +## Vercel + +### Fix URLs + +Before deploying to Vercel, a `vercel.json` file is required at the root of the project directory. It needs to contain the following configuration so that URLs don't require the `.html` extension: + +```json title="vercel.json" +{ + "cleanUrls": true +} +``` + +### Deploy to Vercel + +1. Log in to the [Vercel Dashboard](https://vercel.com/dashboard) and click "Add New..." > Project +2. Import the Git repository containing your Quartz project. +3. Give the project a name (lowercase characters and hyphens only) +4. Check that these configuration options are set: + +| Configuration option | Value | +| ----------------------------------------- | ------------------ | +| Framework Preset | `Other` | +| Root Directory | `./` | +| Build and Output Settings > Build Command | `npx quartz build` | + +5. Press Deploy. Once it's live, you'll have 2 `*.vercel.app` URLs to view the page. + +### Custom Domain + +> [!note] +> If there is something already hosted on the domain, these steps will not work without replacing the previous content. As a workaround, you could use Next.js rewrites or use the next section to create a subdomain. + +1. Update the `baseUrl` in `quartz.config.js` if necessary. +2. Go to the [Domains - Dashboard](https://vercel.com/dashboard/domains) page in Vercel. +3. Connect the domain to Vercel +4. Press "Add" to connect a custom domain to Vercel. +5. Select your Quartz repository and press Continue. +6. Enter the domain you want to connect it to. +7. Follow the instructions to update your DNS records until you see "Valid Configuration" + +### Use a Subdomain + +Using `docs.example.com` is an example of a subdomain. They're a simple way of connecting multiple deployments to one domain. + +1. Update the `baseUrl` in `quartz.config.js` if necessary. +2. Ensure your domain has been added to the [Domains - Dashboard](https://vercel.com/dashboard/domains) page in Vercel. +3. Go to the [Vercel Dashboard](https://vercel.com/dashboard) and select your Quartz project. +4. Go to the Settings tab and then click Domains in the sidebar +5. Enter your subdomain into the field and press Add diff --git a/docs/showcase.md b/docs/showcase.md index ef8afb807..d4a9da2b9 100644 --- a/docs/showcase.md +++ b/docs/showcase.md @@ -12,7 +12,7 @@ Want to see what Quartz can do? Here are some cool community gardens: - [Course notes for Information Technology Advanced Theory](https://a2itnotes.github.io/quartz/) - [Data Dictionary 🧠](https://glossary.airbyte.com/) - [sspaeti.com's Second Brain](https://brain.sspaeti.com/) -- [oldwinterの数字花园](https://garden.oldwinter.top/) +- [oldwinter の数字花园](https://garden.oldwinter.top/) - [Abhijeet's Math Wiki](https://abhmul.github.io/quartz/Math-Wiki/) - [Mike's AI Garden 🤖🪴](https://mwalton.me/) - [Matt Dunn's Second Brain](https://mattdunn.info/) diff --git a/quartz/components/styles/popover.scss b/quartz/components/styles/popover.scss index fae0e121b..55d38c960 100644 --- a/quartz/components/styles/popover.scss +++ b/quartz/components/styles/popover.scss @@ -43,9 +43,7 @@ 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; diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index 4d5ad95cd..a77c630b0 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -67,9 +67,7 @@ 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; } diff --git a/quartz/components/styles/toc.scss b/quartz/components/styles/toc.scss index 3fac4432a..e6968640d 100644 --- a/quartz/components/styles/toc.scss +++ b/quartz/components/styles/toc.scss @@ -42,9 +42,7 @@ 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; } From 2e0e518f5dbddc3b55e9dd1a085c2a88d365b599 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 23 Aug 2023 15:16:04 -0700 Subject: [PATCH 022/140] format --- quartz/components/styles/popover.scss | 4 +++- quartz/components/styles/search.scss | 4 +++- quartz/components/styles/toc.scss | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/quartz/components/styles/popover.scss b/quartz/components/styles/popover.scss index 55d38c960..fae0e121b 100644 --- a/quartz/components/styles/popover.scss +++ b/quartz/components/styles/popover.scss @@ -43,7 +43,9 @@ 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; diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index a77c630b0..4d5ad95cd 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -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; } diff --git a/quartz/components/styles/toc.scss b/quartz/components/styles/toc.scss index e6968640d..3fac4432a 100644 --- a/quartz/components/styles/toc.scss +++ b/quartz/components/styles/toc.scss @@ -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; } From 8200c8d0402cb40e9f65c49dac5be621360a3a20 Mon Sep 17 00:00:00 2001 From: bfahrenfort <59982409+bfahrenfort@users.noreply.github.com> Date: Thu, 24 Aug 2023 00:57:49 -0500 Subject: [PATCH 023/140] Revert contentIndex to RSS 2.0 (#407) --- quartz/plugins/emitters/contentIndex.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/quartz/plugins/emitters/contentIndex.ts b/quartz/plugins/emitters/contentIndex.ts index a18e54e3e..4610cd410 100644 --- a/quartz/plugins/emitters/contentIndex.ts +++ b/quartz/plugins/emitters/contentIndex.ts @@ -41,26 +41,26 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex): string { const base = cfg.baseUrl ?? "" const root = `https://${base}` - const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => ` + const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => ` ${content.title} ${root}/${slug} ${root}/${slug} ${content.description} ${content.date?.toUTCString()} - ` + ` const items = Array.from(idx) .map(([slug, content]) => createURLEntry(simplifySlug(slug), content)) .join("") - return ` + return ` + ${cfg.pageTitle} ${root} Recent content on ${cfg.pageTitle} Quartz -- quartz.jzhao.xyz - + ${items} - ${items} ` } From 9d2340e90b55fb9480e0901bc7360f3d72e688da Mon Sep 17 00:00:00 2001 From: Ben Schlegel <31989404+benschlegel@users.noreply.github.com> Date: Thu, 24 Aug 2023 17:14:52 +0200 Subject: [PATCH 024/140] docs: fix typo in `authoring content.md` (#408) --- docs/authoring content.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/authoring content.md b/docs/authoring content.md index 7aa8d6299..fa6eea258 100644 --- a/docs/authoring content.md +++ b/docs/authoring content.md @@ -34,7 +34,7 @@ Some common frontmatter fields that are natively supported by Quartz: ## Syncing your Content -When you're Quartz is at a point you're happy with, you can save your changes to GitHub by doing `npx quartz sync`. +When your Quartz is at a point you're happy with, you can save your changes to GitHub by doing `npx quartz sync`. > [!hint] Flags and options > For full help options, you can run `npx quartz sync --help`. From 98d82415dc8d60c3a35ea4dee21c86e406605763 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 24 Aug 2023 08:31:06 -0700 Subject: [PATCH 025/140] fix: lock to never read when site is building --- quartz/bootstrap-cli.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/quartz/bootstrap-cli.mjs b/quartz/bootstrap-cli.mjs index b9733171f..b191b49c8 100755 --- a/quartz/bootstrap-cli.mjs +++ b/quartz/bootstrap-cli.mjs @@ -457,6 +457,7 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. req.url = req.url?.slice(argv.baseDir.length) const serve = async () => { + const release = await buildMutex.acquire() await serveHandler(req, res, { public: argv.output, directoryListing: false, @@ -471,6 +472,7 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. const statusString = status >= 200 && status < 300 ? chalk.green(`[${status}]`) : chalk.red(`[${status}]`) console.log(statusString + chalk.grey(` ${argv.baseDir}${req.url}`)) + release() } const redirect = (newFp) => { From c36a9f3fb7c2128610d20312ffb332bd238c89de Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 24 Aug 2023 08:56:40 -0700 Subject: [PATCH 026/140] feat: add defaultDateType config --- docs/configuration.md | 1 + quartz.config.ts | 1 + quartz/cfg.ts | 3 ++ quartz/components/ContentMeta.tsx | 9 +++--- quartz/components/Date.tsx | 9 ++++++ quartz/components/PageList.tsx | 41 ++++++++++++++----------- quartz/components/RecentNotes.tsx | 15 ++++----- quartz/plugins/emitters/contentIndex.ts | 3 +- 8 files changed, 52 insertions(+), 30 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 763a27a92..047f6ca6b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -31,6 +31,7 @@ This part of the configuration concerns anything that can affect the whole site. - This should also include the subpath if you are [[hosting]] on GitHub pages without a custom domain. For example, if my repository is `jackyzha0/quartz`, GitHub pages would deploy to `https://jackyzha0.github.io/quartz` and the `baseUrl` would be `jackyzha0.github.io/quartz` - Note that Quartz 4 will avoid using this as much as possible and use relative URLs whenever it can to make sure your site works no matter _where_ you end up actually deploying it. - `ignorePatterns`: a list of [glob]() patterns that Quartz should ignore and not search through when looking for files inside the `content` folder. See [[private pages]] for more details. +- `defaultDateType`: whether to use created, modified, or published as the default date to display on pages and page listings. - `theme`: configure how the site looks. - `typography`: what fonts to use. Any font available on [Google Fonts](https://fonts.google.com/) works here. - `header`: Font to use for headers diff --git a/quartz.config.ts b/quartz.config.ts index 447039d62..64e86dce0 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -11,6 +11,7 @@ const config: QuartzConfig = { }, baseUrl: "quartz.jzhao.xyz", ignorePatterns: ["private", "templates"], + defaultDateType: "created", theme: { typography: { header: "Schibsted Grotesk", diff --git a/quartz/cfg.ts b/quartz/cfg.ts index e3fee360f..21e03016a 100644 --- a/quartz/cfg.ts +++ b/quartz/cfg.ts @@ -1,3 +1,4 @@ +import { ValidDateType } from "./components/Date" import { QuartzComponent } from "./components/types" import { PluginTypes } from "./plugins/types" import { Theme } from "./util/theme" @@ -22,6 +23,8 @@ export interface GlobalConfiguration { analytics: Analytics /** Glob patterns to not search */ ignorePatterns: string[] + /** Whether to use created, modified, or published as the default type of date */ + defaultDateType: ValidDateType /** 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 */ diff --git a/quartz/components/ContentMeta.tsx b/quartz/components/ContentMeta.tsx index 715c0f469..3e1b70112 100644 --- a/quartz/components/ContentMeta.tsx +++ b/quartz/components/ContentMeta.tsx @@ -1,15 +1,16 @@ -import { formatDate } from "./Date" +import { formatDate, getDate } from "./Date" import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import readingTime from "reading-time" export default (() => { - function ContentMetadata({ fileData }: QuartzComponentProps) { + function ContentMetadata({ cfg, fileData }: QuartzComponentProps) { const text = fileData.text if (text) { const segments: string[] = [] const { text: timeTaken, words: _words } = readingTime(text) - if (fileData.dates?.modified) { - segments.push(formatDate(fileData.dates.modified)) + + if (fileData.dates) { + segments.push(formatDate(getDate(cfg, fileData)!)) } segments.push(timeTaken) diff --git a/quartz/components/Date.tsx b/quartz/components/Date.tsx index f4b284af9..0530a373a 100644 --- a/quartz/components/Date.tsx +++ b/quartz/components/Date.tsx @@ -1,7 +1,16 @@ +import { GlobalConfiguration } from "../cfg" +import { QuartzPluginData } from "../plugins/vfile" + interface Props { date: Date } +export type ValidDateType = keyof Required["dates"] + +export function getDate(cfg: GlobalConfiguration, data: QuartzPluginData): Date | undefined { + return data.dates?.[cfg.defaultDateType] +} + export function formatDate(d: Date): string { return d.toLocaleDateString("en-US", { year: "numeric", diff --git a/quartz/components/PageList.tsx b/quartz/components/PageList.tsx index c55b53478..eb34f02f7 100644 --- a/quartz/components/PageList.tsx +++ b/quartz/components/PageList.tsx @@ -1,31 +1,36 @@ import { FullSlug, resolveRelative } from "../util/path" import { QuartzPluginData } from "../plugins/vfile" -import { Date } from "./Date" +import { Date, getDate } from "./Date" import { QuartzComponentProps } from "./types" +import { GlobalConfiguration } from "../cfg" -export function byDateAndAlphabetical(f1: QuartzPluginData, f2: QuartzPluginData): number { - if (f1.dates && f2.dates) { - // sort descending by last modified - return f2.dates.modified.getTime() - f1.dates.modified.getTime() - } else if (f1.dates && !f2.dates) { - // prioritize files with dates - return -1 - } else if (!f1.dates && f2.dates) { - return 1 +export function byDateAndAlphabetical( + cfg: GlobalConfiguration, +): (f1: QuartzPluginData, f2: QuartzPluginData) => number { + return (f1, f2) => { + if (f1.dates && f2.dates) { + // sort descending + return getDate(cfg, f2)!.getTime() - getDate(cfg, f1)!.getTime() + } else if (f1.dates && !f2.dates) { + // prioritize files with dates + return -1 + } else if (!f1.dates && f2.dates) { + return 1 + } + + // otherwise, sort lexographically by title + const f1Title = f1.frontmatter?.title.toLowerCase() ?? "" + const f2Title = f2.frontmatter?.title.toLowerCase() ?? "" + return f1Title.localeCompare(f2Title) } - - // otherwise, sort lexographically by title - const f1Title = f1.frontmatter?.title.toLowerCase() ?? "" - const f2Title = f2.frontmatter?.title.toLowerCase() ?? "" - return f1Title.localeCompare(f2Title) } type Props = { limit?: number } & QuartzComponentProps -export function PageList({ fileData, allFiles, limit }: Props) { - let list = allFiles.sort(byDateAndAlphabetical) +export function PageList({ cfg, fileData, allFiles, limit }: Props) { + let list = allFiles.sort(byDateAndAlphabetical(cfg)) if (limit) { list = list.slice(0, limit) } @@ -41,7 +46,7 @@ export function PageList({ fileData, allFiles, limit }: Props) {
{page.dates && (

- +

)}
diff --git a/quartz/components/RecentNotes.tsx b/quartz/components/RecentNotes.tsx index 2b61b39cb..673d08458 100644 --- a/quartz/components/RecentNotes.tsx +++ b/quartz/components/RecentNotes.tsx @@ -3,7 +3,8 @@ import { FullSlug, SimpleSlug, resolveRelative } from "../util/path" import { QuartzPluginData } from "../plugins/vfile" import { byDateAndAlphabetical } from "./PageList" import style from "./styles/recentNotes.scss" -import { Date } from "./Date" +import { Date, getDate } from "./Date" +import { GlobalConfiguration } from "../cfg" interface Options { title: string @@ -13,18 +14,18 @@ interface Options { sort: (f1: QuartzPluginData, f2: QuartzPluginData) => number } -const defaultOptions: Options = { +const defaultOptions = (cfg: GlobalConfiguration): Options => ({ title: "Recent Notes", limit: 3, linkToMore: false, filter: () => true, - sort: byDateAndAlphabetical, -} + sort: byDateAndAlphabetical(cfg), +}) export default ((userOpts?: Partial) => { - const opts = { ...defaultOptions, ...userOpts } function RecentNotes(props: QuartzComponentProps) { - const { allFiles, fileData, displayClass } = props + const { allFiles, fileData, displayClass, cfg } = props + const opts = { ...defaultOptions(cfg), ...userOpts } const pages = allFiles.filter(opts.filter).sort(opts.sort) const remaining = Math.max(0, pages.length - opts.limit) return ( @@ -47,7 +48,7 @@ export default ((userOpts?: Partial) => {
{page.dates && (

- +

)}
    diff --git a/quartz/plugins/emitters/contentIndex.ts b/quartz/plugins/emitters/contentIndex.ts index 4610cd410..1c7feaea2 100644 --- a/quartz/plugins/emitters/contentIndex.ts +++ b/quartz/plugins/emitters/contentIndex.ts @@ -1,4 +1,5 @@ import { GlobalConfiguration } from "../../cfg" +import { getDate } from "../../components/Date" import { FilePath, FullSlug, SimpleSlug, simplifySlug } from "../../util/path" import { QuartzEmitterPlugin } from "../types" import path from "path" @@ -74,7 +75,7 @@ export const ContentIndex: QuartzEmitterPlugin> = (opts) => { const linkIndex: ContentIndex = new Map() for (const [_tree, file] of content) { const slug = file.data.slug! - const date = file.data.dates?.modified ?? new Date() + const date = getDate(ctx.cfg.configuration, file.data) ?? new Date() if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) { linkIndex.set(slug, { title: file.data.frontmatter?.title!, From 9851697b583efdd40173ebbdd484030f2adb0732 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 24 Aug 2023 09:05:19 -0700 Subject: [PATCH 027/140] version bump to 4.0.10 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 480d89185..d94d6cf7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jackyzha0/quartz", - "version": "4.0.9", + "version": "4.0.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jackyzha0/quartz", - "version": "4.0.9", + "version": "4.0.10", "license": "MIT", "dependencies": { "@clack/prompts": "^0.6.3", diff --git a/package.json b/package.json index b65ed858f..25d3d22d7 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@jackyzha0/quartz", "description": "🌱 publish your digital garden and notes as a website", "private": true, - "version": "4.0.9", + "version": "4.0.10", "type": "module", "author": "jackyzha0 ", "license": "MIT", From 6cd0612d40a5011f19f5ca2e5e804477779e393f Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 24 Aug 2023 09:17:43 -0700 Subject: [PATCH 028/140] fix: add better warning when defaultDateType is not set due to upgrade --- quartz/components/Date.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/quartz/components/Date.tsx b/quartz/components/Date.tsx index 0530a373a..1432255cf 100644 --- a/quartz/components/Date.tsx +++ b/quartz/components/Date.tsx @@ -8,6 +8,9 @@ interface Props { export type ValidDateType = keyof Required["dates"] export function getDate(cfg: GlobalConfiguration, data: QuartzPluginData): Date | undefined { + if (!cfg.defaultDateType) { + throw new Error(`Field 'defaultDateType' was not set in the configuration object of quartz.config.ts. See https://quartz.jzhao.xyz/configuration#general-configuration for more details.`) + } return data.dates?.[cfg.defaultDateType] } From fc4b8f3d3fad90b6f7d8dedc58201df68da1280e Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 24 Aug 2023 09:38:00 -0700 Subject: [PATCH 029/140] fix: ensure recentnotes uses proper date --- quartz/components/RecentNotes.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/components/RecentNotes.tsx b/quartz/components/RecentNotes.tsx index 673d08458..cb14b3343 100644 --- a/quartz/components/RecentNotes.tsx +++ b/quartz/components/RecentNotes.tsx @@ -48,7 +48,7 @@ export default ((userOpts?: Partial) => {
{page.dates && (

- +

)}
    From c8412a5b0ac90d9d7beb7e03ed9a4763e834a865 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 24 Aug 2023 10:03:14 -0700 Subject: [PATCH 030/140] format --- quartz/components/Date.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/quartz/components/Date.tsx b/quartz/components/Date.tsx index 1432255cf..8713cfd3c 100644 --- a/quartz/components/Date.tsx +++ b/quartz/components/Date.tsx @@ -9,7 +9,9 @@ export type ValidDateType = keyof Required["dates"] export function getDate(cfg: GlobalConfiguration, data: QuartzPluginData): Date | undefined { if (!cfg.defaultDateType) { - throw new Error(`Field 'defaultDateType' was not set in the configuration object of quartz.config.ts. See https://quartz.jzhao.xyz/configuration#general-configuration for more details.`) + throw new Error( + `Field 'defaultDateType' was not set in the configuration object of quartz.config.ts. See https://quartz.jzhao.xyz/configuration#general-configuration for more details.`, + ) } return data.dates?.[cfg.defaultDateType] } From 8cf7280614f8c1f2c9aaba5671f388d63bbea4dd Mon Sep 17 00:00:00 2001 From: Zero King Date: Fri, 25 Aug 2023 02:41:20 +0800 Subject: [PATCH 031/140] feat: reproducible build (#412) for sitemap, RSS and contentIndex.json. --- quartz/build.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/build.ts b/quartz/build.ts index 58137d178..22288acc1 100644 --- a/quartz/build.ts +++ b/quartz/build.ts @@ -45,7 +45,7 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) { perf.addEvent("glob") const allFiles = await glob("**/*.*", argv.directory, cfg.configuration.ignorePatterns) - const fps = allFiles.filter((fp) => fp.endsWith(".md")) + const fps = allFiles.filter((fp) => fp.endsWith(".md")).sort() console.log( `Found ${fps.length} input files from \`${argv.directory}\` in ${perf.timeSince("glob")}`, ) From 94ce0883e7fbf38252377af2f144c971a2ff591b Mon Sep 17 00:00:00 2001 From: Ben Schlegel <31989404+benschlegel@users.noreply.github.com> Date: Thu, 24 Aug 2023 21:28:06 +0200 Subject: [PATCH 032/140] style: integrate tertiary color to text-select (#413) --- quartz/styles/base.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index 16a0de3a6..aa7fce482 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -27,6 +27,11 @@ section { border-radius: 5px; } +::selection { + background: color-mix(in srgb, var(--tertiary) 75%, transparent); + color: var(--darkgray) +} + p, ul, text, From 953ef29f4e238ef9ae186ab79eeec1bf4f3921a6 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 24 Aug 2023 12:31:15 -0700 Subject: [PATCH 033/140] format, ensure ci runs on prs --- .github/workflows/ci.yaml | 3 +++ quartz/styles/base.scss | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 90abf6797..731395d38 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,6 +1,9 @@ name: Build and Test on: + pull_request: + branches: + - v4 push: branches: - v4 diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index aa7fce482..34def8783 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -29,7 +29,7 @@ section { ::selection { background: color-mix(in srgb, var(--tertiary) 75%, transparent); - color: var(--darkgray) + color: var(--darkgray); } p, From 340e3ef5116cd99c8ddfdbb3d9e0bbd914e07825 Mon Sep 17 00:00:00 2001 From: Ben Schlegel <31989404+benschlegel@users.noreply.github.com> Date: Fri, 25 Aug 2023 18:03:49 +0200 Subject: [PATCH 034/140] feat(consistency): Add `.obsidian` to ignorePatterns (#420) --- quartz.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz.config.ts b/quartz.config.ts index 64e86dce0..31d5bcfea 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -10,7 +10,7 @@ const config: QuartzConfig = { provider: "plausible", }, baseUrl: "quartz.jzhao.xyz", - ignorePatterns: ["private", "templates"], + ignorePatterns: ["private", "templates", ".obsidian"], defaultDateType: "created", theme: { typography: { From 5c6d1e27baef74a42cc292276c5832b010a38fd5 Mon Sep 17 00:00:00 2001 From: Hrishikesh Barman Date: Fri, 25 Aug 2023 22:55:46 +0530 Subject: [PATCH 035/140] feat(plugins): add toml support for frontmatter (#418) * feat(plugins): add toml support for frontmatter Currently frontmatter is expected to be yaml, with delimiter set to "---". This might not always be the case, for example ox-hugo(a hugo exporter for org-mode files) exports in toml format with the delimiter set to "+++" by default. With this change, the users will be able use frontmatter plugin to support this toml frontmatter format. Example usage: `Plugin.FrontMatter({delims: "+++", language: 'toml'})` - [0] https://ox-hugo.scripter.co/doc/org-meta-data-to-hugo-front-matter/ * fixup! feat(plugins): add toml support for frontmatter --- package-lock.json | 6 ++++++ package.json | 1 + quartz/plugins/transformers/frontmatter.ts | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/package-lock.json b/package-lock.json index d94d6cf7a..9246cc992 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "serve-handler": "^6.1.5", "source-map-support": "^0.5.21", "to-vfile": "^7.2.4", + "toml": "^3.0.0", "unified": "^10.1.2", "unist-util-visit": "^4.1.2", "vfile": "^5.3.7", @@ -5548,6 +5549,11 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" + }, "node_modules/tough-cookie": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", diff --git a/package.json b/package.json index 25d3d22d7..6ed52d602 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "serve-handler": "^6.1.5", "source-map-support": "^0.5.21", "to-vfile": "^7.2.4", + "toml": "^3.0.0", "unified": "^10.1.2", "unist-util-visit": "^4.1.2", "vfile": "^5.3.7", diff --git a/quartz/plugins/transformers/frontmatter.ts b/quartz/plugins/transformers/frontmatter.ts index 3f55b9cb6..a7249c19e 100644 --- a/quartz/plugins/transformers/frontmatter.ts +++ b/quartz/plugins/transformers/frontmatter.ts @@ -2,14 +2,17 @@ import matter from "gray-matter" import remarkFrontmatter from "remark-frontmatter" import { QuartzTransformerPlugin } from "../types" import yaml from "js-yaml" +import toml from "toml" import { slugTag } from "../../util/path" export interface Options { delims: string | string[] + language: "yaml" | "toml" } const defaultOptions: Options = { delims: "---", + language: "yaml", } export const FrontMatter: QuartzTransformerPlugin | undefined> = (userOpts) => { @@ -25,6 +28,7 @@ export const FrontMatter: QuartzTransformerPlugin | undefined> ...opts, engines: { yaml: (s) => yaml.load(s, { schema: yaml.JSON_SCHEMA }) as object, + toml: (s) => toml.parse(s) as object, }, }) From bc543f81d9ada5e61cb9690834a5f83c02997d63 Mon Sep 17 00:00:00 2001 From: Hrishikesh Barman Date: Sat, 26 Aug 2023 11:22:23 +0530 Subject: [PATCH 036/140] feat(plugins): add OxHugoFlavouredMarkdown (#419) * feat(plugins): add OxHugoFlavouredMarkdown ox-hugo is an org exporter backend that exports org files to hugo-compatible markdown in an opinionated way. This plugin adds some tweaks to the generated markdown to make it compatible with quartz but the list of changes applied it is not extensive. In the future however, we could leapfrog ox-hugo altogether and create a quartz site directly out of org-roam files. That way we won't have to do all the ritual dancing that this plugin has to perform. See https://github.com/k2052/org-to-markdown * fix: add toml to remarkFrontmatter configuration * docs: add docs for OxHugoFlavouredMarkdown * fixup! docs: add docs for OxHugoFlavouredMarkdown --- docs/features/OxHugo compatibility.md | 38 +++++++++++ quartz/plugins/transformers/frontmatter.ts | 2 +- quartz/plugins/transformers/index.ts | 1 + quartz/plugins/transformers/oxhugofm.ts | 73 ++++++++++++++++++++++ 4 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 docs/features/OxHugo compatibility.md create mode 100644 quartz/plugins/transformers/oxhugofm.ts diff --git a/docs/features/OxHugo compatibility.md b/docs/features/OxHugo compatibility.md new file mode 100644 index 000000000..12774e725 --- /dev/null +++ b/docs/features/OxHugo compatibility.md @@ -0,0 +1,38 @@ +--- +tags: + - plugin/transformer +--- + +Quartz is a static-site generator that transforms markdown content into web pages. [org-roam](https://www.orgroam.com/) is a plain-text(`org`) personal knowledge management system for [emacs](https://en.wikipedia.org/wiki/Emacs). [ox-hugo](https://github.com/kaushalmodi/ox-hugo) is org exporter backend that exports `org-mode` files to [Hugo](https://gohugo.io/) compatible markdown. + +Because the markdown generated by ox-hugo is not pure markdown but Hugo specific, we need to transform it to fit into Quartz. This is done by `Plugin.OxHugoFlavouredMarkdown`. Even though this [[making plugins|plugin]] was written with `ox-hugo` in mind, it should work for any Hugo specific markdown. + +```typescript title="quartz.config.ts" +plugins: { + transformers: [ + Plugin.FrontMatter({ delims: "+++", language: "toml" }), // if toml frontmatter + // ... + Plugin.OxHugoFlavouredMarkdown(), + Plugin.GitHubFlavoredMarkdown(), + // ... + ], +}, +``` + +## Usage + +Quartz by default doesn't understand `org-roam` files as they aren't Markdown. You're responsible for using an external tool like `ox-hugo` to export the `org-roam` files as Markdown content to Quartz and managing the static assets so that they're available in the final output. + +## Configuration + +- Link resolution + - `wikilinks`: Whether to replace `{{ relref }}` with Quartz [[wikilinks]] + - `removePredefinedAnchor`: Whether to remove [pre-defined anchor set by ox-hugo](https://ox-hugo.scripter.co/doc/anchors/). +- Image handling + - `replaceFigureWithMdImg`: Whether to replace `
    ` with `![]()` +- Formatting + - `removeHugoShortcode`: Whether to remove hugo shortcode syntax (`{{}}`) + +> [!warning] +> +> While you can use `Plugin.OxHugoFlavoredMarkdown` and `Plugin.ObsidianFlavoredMarkdown` together, it's not recommended because it might mutate the file in unexpected ways. Use with caution. diff --git a/quartz/plugins/transformers/frontmatter.ts b/quartz/plugins/transformers/frontmatter.ts index a7249c19e..571aa04d0 100644 --- a/quartz/plugins/transformers/frontmatter.ts +++ b/quartz/plugins/transformers/frontmatter.ts @@ -21,7 +21,7 @@ export const FrontMatter: QuartzTransformerPlugin | undefined> name: "FrontMatter", markdownPlugins() { return [ - remarkFrontmatter, + [remarkFrontmatter, ["yaml", "toml"]], () => { return (_, file) => { const { data } = matter(file.value, { diff --git a/quartz/plugins/transformers/index.ts b/quartz/plugins/transformers/index.ts index 8013ab7cc..d9f2854c0 100644 --- a/quartz/plugins/transformers/index.ts +++ b/quartz/plugins/transformers/index.ts @@ -5,5 +5,6 @@ export { Latex } from "./latex" export { Description } from "./description" export { CrawlLinks } from "./links" export { ObsidianFlavoredMarkdown } from "./ofm" +export { OxHugoFlavouredMarkdown } from "./oxhugofm" export { SyntaxHighlighting } from "./syntax" export { TableOfContents } from "./toc" diff --git a/quartz/plugins/transformers/oxhugofm.ts b/quartz/plugins/transformers/oxhugofm.ts new file mode 100644 index 000000000..0d7b9199a --- /dev/null +++ b/quartz/plugins/transformers/oxhugofm.ts @@ -0,0 +1,73 @@ +import { QuartzTransformerPlugin } from "../types" + +export interface Options { + /** Replace {{ relref }} with quartz wikilinks []() */ + wikilinks: boolean + /** Remove pre-defined anchor (see https://ox-hugo.scripter.co/doc/anchors/) */ + removePredefinedAnchor: boolean + /** Remove hugo shortcode syntax */ + removeHugoShortcode: boolean + /** Replace
    with ![]() */ + replaceFigureWithMdImg: boolean +} + +const defaultOptions: Options = { + wikilinks: true, + removePredefinedAnchor: true, + removeHugoShortcode: true, + replaceFigureWithMdImg: true, +} + +const relrefRegex = new RegExp(/\[([^\]]+)\]\(\{\{< relref "([^"]+)" >\}\}\)/, "g") +const predefinedHeadingIdRegex = new RegExp(/(.*) {#(?:.*)}/, "g") +const hugoShortcodeRegex = new RegExp(/{{(.*)}}/, "g") +const figureTagRegex = new RegExp(/< ?figure src="(.*)" ?>/, "g") + +/** + * ox-hugo is an org exporter backend that exports org files to hugo-compatible + * markdown in an opinionated way. This plugin adds some tweaks to the generated + * markdown to make it compatible with quartz but the list of changes applied it + * is not exhaustive. + * */ +export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin | undefined> = ( + userOpts, +) => { + const opts = { ...defaultOptions, ...userOpts } + return { + name: "OxHugoFlavouredMarkdown", + textTransform(_ctx, src) { + if (opts.wikilinks) { + src = src.toString() + src = src.replaceAll(relrefRegex, (value, ...capture) => { + const [text, link] = capture + return `[${text}](${link})` + }) + } + + if (opts.removePredefinedAnchor) { + src = src.toString() + src = src.replaceAll(predefinedHeadingIdRegex, (value, ...capture) => { + const [headingText] = capture + return headingText + }) + } + + if (opts.removeHugoShortcode) { + src = src.toString() + src = src.replaceAll(hugoShortcodeRegex, (value, ...capture) => { + const [scContent] = capture + return scContent + }) + } + + if (opts.replaceFigureWithMdImg) { + src = src.toString() + src = src.replaceAll(figureTagRegex, (value, ...capture) => { + const [src] = capture + return `![](${src})` + }) + } + return src + }, + } +} From e3265f841637de197e5cf4a5471372b5178f1e4d Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sat, 26 Aug 2023 10:42:55 -0700 Subject: [PATCH 037/140] docs: simplify oxhugo page --- docs/features/OxHugo compatibility.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/features/OxHugo compatibility.md b/docs/features/OxHugo compatibility.md index 12774e725..7801f0c25 100644 --- a/docs/features/OxHugo compatibility.md +++ b/docs/features/OxHugo compatibility.md @@ -3,9 +3,9 @@ tags: - plugin/transformer --- -Quartz is a static-site generator that transforms markdown content into web pages. [org-roam](https://www.orgroam.com/) is a plain-text(`org`) personal knowledge management system for [emacs](https://en.wikipedia.org/wiki/Emacs). [ox-hugo](https://github.com/kaushalmodi/ox-hugo) is org exporter backend that exports `org-mode` files to [Hugo](https://gohugo.io/) compatible markdown. +[org-roam](https://www.orgroam.com/) is a plain-text(`org`) personal knowledge management system for [emacs](https://en.wikipedia.org/wiki/Emacs). [ox-hugo](https://github.com/kaushalmodi/ox-hugo) is org exporter backend that exports `org-mode` files to [Hugo](https://gohugo.io/) compatible Markdown. -Because the markdown generated by ox-hugo is not pure markdown but Hugo specific, we need to transform it to fit into Quartz. This is done by `Plugin.OxHugoFlavouredMarkdown`. Even though this [[making plugins|plugin]] was written with `ox-hugo` in mind, it should work for any Hugo specific markdown. +Because the Markdown generated by ox-hugo is not pure Markdown but Hugo specific, we need to transform it to fit into Quartz. This is done by `Plugin.OxHugoFlavouredMarkdown`. Even though this [[making plugins|plugin]] was written with `ox-hugo` in mind, it should work for any Hugo specific Markdown. ```typescript title="quartz.config.ts" plugins: { From 74c3ebb7bd7ef126246f8ea03565db73cd5e7f38 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sat, 26 Aug 2023 10:48:34 -0700 Subject: [PATCH 038/140] style: fix mulitline callout styling --- package-lock.json | 1 + quartz/styles/callouts.scss | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 9246cc992..09488c422 100644 --- a/package-lock.json +++ b/package-lock.json @@ -112,6 +112,7 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { diff --git a/quartz/styles/callouts.scss b/quartz/styles/callouts.scss index ad991658d..703bd67f6 100644 --- a/quartz/styles/callouts.scss +++ b/quartz/styles/callouts.scss @@ -82,7 +82,6 @@ .callout-title { display: flex; - align-items: center; gap: 5px; padding: 1rem 0; color: var(--color); @@ -103,6 +102,8 @@ .callout-icon { width: 18px; height: 18px; + flex: 0 0 18px; + padding-top: 4px; } .callout-title-inner { From ad4145fb10dbf32d8f99e1de555339dba0979f72 Mon Sep 17 00:00:00 2001 From: Ben Schlegel <31989404+benschlegel@users.noreply.github.com> Date: Sat, 26 Aug 2023 22:21:44 +0200 Subject: [PATCH 039/140] feat: support CLI arguments for `npx quartz create` (#421) * feat(cli): add new args for content + link resolve * feat(cli): validate cmd args * feat(cli): add chalk + error code to errors * feat(cli): support for setup/link via args * refactor(cli): use yargs choices instead of manual Scrap manual check if arguments are valid, use yargs "choices" field instead. * feat(cli): add in-dir argument+ handle errors add new "in-directory" argument, used if "setup" is "copy" or "symlink" to determine source. add error handling for invalid permutations of arguments or non existent path * feat(cli): dynamically use cli or provided args use "in-directory" arg as `originalFolder` if available, otherwise get it from manual cli process * run format * fix: use process.exit instead of return * refactor: split CommonArgv and CreateArgv * refactor(cli): rename create args, use ${} syntax * fix(cli): fix link resolution strategy arg * format * feat(consistency): allow partial cmd args --- quartz/bootstrap-cli.mjs | 188 +++++++++++++++++++++++++++------------ 1 file changed, 133 insertions(+), 55 deletions(-) diff --git a/quartz/bootstrap-cli.mjs b/quartz/bootstrap-cli.mjs index b191b49c8..1deb18fe6 100755 --- a/quartz/bootstrap-cli.mjs +++ b/quartz/bootstrap-cli.mjs @@ -43,6 +43,27 @@ const CommonArgv = { }, } +const CreateArgv = { + ...CommonArgv, + source: { + string: true, + alias: ["s"], + describe: "source directory to copy/create symlink from", + }, + strategy: { + string: true, + alias: ["X"], + choices: ["new", "copy", "symlink"], + describe: "strategy for content folder setup", + }, + links: { + string: true, + alias: ["l"], + choices: ["absolute", "shortest", "relative"], + describe: "strategy to resolve links", + }, +} + const SyncArgv = { ...CommonArgv, commit: { @@ -147,24 +168,73 @@ yargs(hideBin(process.argv)) .scriptName("quartz") .version(version) .usage("$0 [args]") - .command("create", "Initialize Quartz", CommonArgv, async (argv) => { + .command("create", "Initialize Quartz", CreateArgv, 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: "Copy 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!", - }, - ], - }), - ) + let setupStrategy = argv.strategy?.toLowerCase() + let linkResolutionStrategy = argv.links?.toLowerCase() + const sourceDirectory = argv.source + + // If all cmd arguments were provided, check if theyre valid + if (setupStrategy && linkResolutionStrategy) { + // If setup isn't, "new", source argument is required + if (setupStrategy !== "new") { + // Error handling + if (!sourceDirectory) { + outro( + chalk.red( + `Setup strategies (arg '${chalk.yellow( + `-${CreateArgv.strategy.alias[0]}`, + )}') other than '${chalk.yellow( + "new", + )}' require content folder argument ('${chalk.yellow( + `-${CreateArgv.source.alias[0]}`, + )}') to be set`, + ), + ) + process.exit(1) + } else { + if (!fs.existsSync(sourceDirectory)) { + outro( + chalk.red( + `Input directory to copy/symlink 'content' from not found ('${chalk.yellow( + sourceDirectory, + )}', invalid argument "${chalk.yellow(`-${CreateArgv.source.alias[0]}`)})`, + ), + ) + process.exit(1) + } else if (!fs.lstatSync(sourceDirectory).isDirectory()) { + outro( + chalk.red( + `Source directory to copy/symlink 'content' from is not a directory (found file at '${chalk.yellow( + sourceDirectory, + )}', invalid argument ${chalk.yellow(`-${CreateArgv.source.alias[0]}`)}")`, + ), + ) + process.exit(1) + } + } + } + } + + // Use cli process if cmd args werent provided + if (!setupStrategy) { + setupStrategy = exitIfCancel( + await select({ + message: `Choose how to initialize the content in \`${contentFolder}\``, + options: [ + { value: "new", label: "Empty Quartz" }, + { value: "copy", label: "Copy 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!", + }, + ], + }), + ) + } async function rmContentFolder() { const contentStat = await fs.promises.lstat(contentFolder) @@ -177,23 +247,28 @@ yargs(hideBin(process.argv)) await fs.promises.unlink(path.join(contentFolder, ".gitkeep")) 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" - } - }, - }), - ), - ) + let originalFolder = sourceDirectory + + // If input directory was not passed, use cli + if (!sourceDirectory) { + 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") { @@ -217,29 +292,32 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. ) } - // get a preferred 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", - }, - ], - }), - ) + // Use cli process if cmd args werent provided + if (!linkResolutionStrategy) { + // get a preferred link resolution strategy + 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") From c91e62c376d481534d89084e5c04846878dff6d3 Mon Sep 17 00:00:00 2001 From: Ben Schlegel <31989404+benschlegel@users.noreply.github.com> Date: Sun, 27 Aug 2023 02:19:45 +0200 Subject: [PATCH 040/140] Fix search bar after navigate (#424) --- quartz/components/scripts/search.inline.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index adcd06abc..ef26ba380 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -128,6 +128,7 @@ document.addEventListener("nav", async (e: unknown) => { button.addEventListener("click", () => { const targ = resolveRelative(currentSlug, slug) window.spaNavigate(new URL(targ, window.location.toString())) + hideSearch() }) return button } From 52ca312f41ee6da5202cd9632d8501340ada3a67 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sun, 27 Aug 2023 12:27:42 -0700 Subject: [PATCH 041/140] fix: slugify tag on page before adding (closes #411) --- quartz/plugins/transformers/ofm.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index bed6f622c..4d1586f93 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -383,13 +383,14 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin return (tree: Root, file) => { const base = pathToRoot(file.data.slug!) findAndReplace(tree, tagRegex, (_value: string, tag: string) => { + tag = slugTag(tag) if (file.data.frontmatter && !file.data.frontmatter.tags.includes(tag)) { file.data.frontmatter.tags.push(tag) } return { type: "link", - url: base + `/tags/${slugTag(tag)}`, + url: base + `/tags/${tag}`, data: { hProperties: { className: ["tag-link"], From 4b89202f7e834cf8b5c5aa39e8f1778706492085 Mon Sep 17 00:00:00 2001 From: Ben Schlegel <31989404+benschlegel@users.noreply.github.com> Date: Mon, 28 Aug 2023 00:59:51 +0200 Subject: [PATCH 042/140] cleanup: rework cli to allow invoking create and build outside of cli (#428) * refactor: move `bootstrap-cli.mjs` tp cli also update reference in docs * refactor(cli): move build handler to `cli-functions` * refactor(cli): move create to handler + helpers * refactor(cli): extract arg definitions * refactor: rename handlers and helpers * refactor(cli): move update, await handlers * refactor(cli): create constants, migrate to helpers * refactor(cli): migrate `restore` * refactor(cli): migrate `sync` * format * refactor(cli): remove old imports/functions * refactor(cli): remove unused imports + format * chore: remove old log statement * fix: fix imports, clean duplicate code * fix: relative import * fix: simplified cacheFile path * fix: update cacheFile import path * refactor: move bootstrap-cli to quartz * format * revert: revert path to bootstrap-cli * ci: re-run * ci: fix execution permission --- package-lock.json | 1 - quartz/bootstrap-cli.mjs | 617 +-------------------------------------- quartz/cli/args.js | 88 ++++++ quartz/cli/constants.js | 15 + quartz/cli/handlers.js | 511 ++++++++++++++++++++++++++++++++ quartz/cli/helpers.js | 52 ++++ 6 files changed, 680 insertions(+), 604 deletions(-) create mode 100644 quartz/cli/args.js create mode 100644 quartz/cli/constants.js create mode 100644 quartz/cli/handlers.js create mode 100644 quartz/cli/helpers.js diff --git a/package-lock.json b/package-lock.json index 09488c422..9246cc992 100644 --- a/package-lock.json +++ b/package-lock.json @@ -112,7 +112,6 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", - "extraneous": true, "inBundle": true, "license": "MIT", "engines": { diff --git a/quartz/bootstrap-cli.mjs b/quartz/bootstrap-cli.mjs index 1deb18fe6..35d06af77 100755 --- a/quartz/bootstrap-cli.mjs +++ b/quartz/bootstrap-cli.mjs @@ -1,628 +1,39 @@ #!/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 chokidar from "chokidar" -import prettyBytes from "pretty-bytes" -import { execSync, spawnSync } from "child_process" -import http from "http" -import serveHandler from "serve-handler" -import { WebSocketServer } from "ws" -import { randomUUID } from "crypto" -import { Mutex } from "async-mutex" - -const ORIGIN_NAME = "origin" -const UPSTREAM_NAME = "upstream" -const QUARTZ_SOURCE_BRANCH = "v4" -const cwd = process.cwd() -const cacheDir = path.join(cwd, ".quartz-cache") -const cacheFile = "./.quartz-cache/transpiled-build.mjs" -const fp = "./quartz/build.ts" -const { version } = JSON.parse(readFileSync("./package.json").toString()) -const contentCacheFolder = path.join(cacheDir, "content-cache") - -const CommonArgv = { - directory: { - string: true, - alias: ["d"], - default: "content", - describe: "directory to look for content files", - }, - verbose: { - boolean: true, - alias: ["v"], - default: false, - describe: "print out extra logging information", - }, -} - -const CreateArgv = { - ...CommonArgv, - source: { - string: true, - alias: ["s"], - describe: "source directory to copy/create symlink from", - }, - strategy: { - string: true, - alias: ["X"], - choices: ["new", "copy", "symlink"], - describe: "strategy for content folder setup", - }, - links: { - string: true, - alias: ["l"], - choices: ["absolute", "shortest", "relative"], - describe: "strategy to resolve links", - }, -} - -const SyncArgv = { - ...CommonArgv, - commit: { - boolean: true, - default: true, - describe: "create a git commit for your unsaved changes", - }, - push: { - boolean: true, - default: true, - describe: "push updates to your Quartz fork", - }, - pull: { - boolean: true, - default: true, - describe: "pull updates from your Quartz fork", - }, -} - -const BuildArgv = { - ...CommonArgv, - output: { - string: true, - alias: ["o"], - default: "public", - describe: "output folder for files", - }, - serve: { - boolean: true, - default: false, - describe: "run a local server to live-preview your Quartz", - }, - baseDir: { - string: true, - default: "", - describe: "base path to serve your local server on", - }, - port: { - number: true, - default: 8080, - describe: "port to serve Quartz on", - }, - bundleInfo: { - boolean: true, - default: false, - describe: "show detailed bundle information", - }, - concurrency: { - number: true, - describe: "how many threads to use to parse notes", - }, -} - -function escapePath(fp) { - return fp - .replace(/\\ /g, " ") // unescape spaces - .replace(/^".*"$/, "$1") - .replace(/^'.*"$/, "$1") - .trim() -} - -function exitIfCancel(val) { - if (isCancel(val)) { - outro(chalk.red("Exiting")) - process.exit(0) - } else { - return val - } -} - -async function stashContentFolder(contentFolder) { - await fs.promises.rm(contentCacheFolder, { force: true, recursive: 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.rm(contentFolder, { force: true, recursive: true }) - await fs.promises.cp(contentCacheFolder, contentFolder, { - force: true, - recursive: true, - verbatimSymlinks: true, - preserveTimestamps: true, - }) - await fs.promises.rm(contentCacheFolder, { force: true, recursive: true }) -} - -function gitPull(origin, branch) { - const flags = ["--no-rebase", "--autostash", "-s", "recursive", "-X", "ours", "--no-edit"] - const out = spawnSync("git", ["pull", ...flags, origin, branch], { stdio: "inherit" }) - if (out.stderr) { - throw new Error(`Error while pulling updates: ${out.stderr}`) - } -} +import { + handleBuild, + handleCreate, + handleUpdate, + handleRestore, + handleSync, +} from "./cli/handlers.js" +import { CommonArgv, BuildArgv, CreateArgv, SyncArgv } from "./cli/args.js" +import { version } from "./cli/constants.js" yargs(hideBin(process.argv)) .scriptName("quartz") .version(version) .usage("$0 [args]") .command("create", "Initialize Quartz", CreateArgv, async (argv) => { - console.log() - intro(chalk.bgGreen.black(` Quartz v${version} `)) - const contentFolder = path.join(cwd, argv.directory) - let setupStrategy = argv.strategy?.toLowerCase() - let linkResolutionStrategy = argv.links?.toLowerCase() - const sourceDirectory = argv.source - - // If all cmd arguments were provided, check if theyre valid - if (setupStrategy && linkResolutionStrategy) { - // If setup isn't, "new", source argument is required - if (setupStrategy !== "new") { - // Error handling - if (!sourceDirectory) { - outro( - chalk.red( - `Setup strategies (arg '${chalk.yellow( - `-${CreateArgv.strategy.alias[0]}`, - )}') other than '${chalk.yellow( - "new", - )}' require content folder argument ('${chalk.yellow( - `-${CreateArgv.source.alias[0]}`, - )}') to be set`, - ), - ) - process.exit(1) - } else { - if (!fs.existsSync(sourceDirectory)) { - outro( - chalk.red( - `Input directory to copy/symlink 'content' from not found ('${chalk.yellow( - sourceDirectory, - )}', invalid argument "${chalk.yellow(`-${CreateArgv.source.alias[0]}`)})`, - ), - ) - process.exit(1) - } else if (!fs.lstatSync(sourceDirectory).isDirectory()) { - outro( - chalk.red( - `Source directory to copy/symlink 'content' from is not a directory (found file at '${chalk.yellow( - sourceDirectory, - )}', invalid argument ${chalk.yellow(`-${CreateArgv.source.alias[0]}`)}")`, - ), - ) - process.exit(1) - } - } - } - } - - // Use cli process if cmd args werent provided - if (!setupStrategy) { - setupStrategy = exitIfCancel( - await select({ - message: `Choose how to initialize the content in \`${contentFolder}\``, - options: [ - { value: "new", label: "Empty Quartz" }, - { value: "copy", label: "Copy 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!", - }, - ], - }), - ) - } - - async function rmContentFolder() { - const contentStat = await fs.promises.lstat(contentFolder) - if (contentStat.isSymbolicLink()) { - await fs.promises.unlink(contentFolder) - } else { - await rimraf(contentFolder) - } - } - - await fs.promises.unlink(path.join(contentFolder, ".gitkeep")) - if (setupStrategy === "copy" || setupStrategy === "symlink") { - let originalFolder = sourceDirectory - - // If input directory was not passed, use cli - if (!sourceDirectory) { - 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") { - await fs.promises.cp(originalFolder, contentFolder, { - recursive: true, - preserveTimestamps: true, - }) - } else if (setupStrategy === "symlink") { - await fs.promises.symlink(originalFolder, contentFolder, "dir") - } - } else if (setupStrategy === "new") { - 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. -`, - ) - } - - // Use cli process if cmd args werent provided - if (!linkResolutionStrategy) { - // get a preferred link resolution strategy - 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}'`, - ) - await fs.promises.writeFile(configFilePath, configContent) - - outro(`You're all set! Not sure what to do next? Try: - • Customizing Quartz a bit more by editing \`quartz.config.ts\` - • Running \`npx quartz build --serve\` to preview your Quartz locally - • Hosting your Quartz online (see: https://quartz.jzhao.xyz/hosting) -`) + await handleCreate(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") - execSync( - `git remote show upstream || git remote add upstream https://github.com/jackyzha0/quartz.git`, - ) - await stashContentFolder(contentFolder) - console.log( - "Pulling updates... you may need to resolve some `git` conflicts if you've made changes to components or plugins.", - ) - gitPull(UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH) - await popContentFolder(contentFolder) - console.log("Ensuring dependencies are up to date") - spawnSync("npm", ["i"], { stdio: "inherit" }) - console.log(chalk.green("Done!")) + await handleUpdate(argv) }) .command( "restore", "Try to restore your content folder from the cache", CommonArgv, async (argv) => { - const contentFolder = path.join(cwd, argv.directory) - await popContentFolder(contentFolder) + await handleRestore(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") - - if (argv.commit) { - const contentStat = await fs.promises.lstat(contentFolder) - if (contentStat.isSymbolicLink()) { - const linkTarg = await fs.promises.readlink(contentFolder) - console.log(chalk.yellow("Detected symlink, trying to dereference before committing")) - - // stash symlink file - await stashContentFolder(contentFolder) - - // follow symlink and copy content - await fs.promises.cp(linkTarg, contentFolder, { - recursive: true, - preserveTimestamps: true, - }) - } - - const currentTimestamp = new Date().toLocaleString("en-US", { - dateStyle: "medium", - timeStyle: "short", - }) - spawnSync("git", ["add", "."], { stdio: "inherit" }) - spawnSync("git", ["commit", "-m", `Quartz sync: ${currentTimestamp}`], { stdio: "inherit" }) - - if (contentStat.isSymbolicLink()) { - // put symlink back - await popContentFolder(contentFolder) - } - } - - 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.", - ) - gitPull(ORIGIN_NAME, QUARTZ_SOURCE_BRANCH) - } - - await popContentFolder(contentFolder) - if (argv.push) { - console.log("Pushing your changes") - spawnSync("git", ["push", "-f", ORIGIN_NAME, QUARTZ_SOURCE_BRANCH], { stdio: "inherit" }) - } - - console.log(chalk.green("Done!")) + await handleSync(argv) }) .command("build", "Build Quartz into a bundle of static HTML files", BuildArgv, async (argv) => { - console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`)) - const ctx = await esbuild.context({ - entryPoints: [fp], - outfile: path.join("quartz", cacheFile), - bundle: true, - keepNames: true, - minifyWhitespace: true, - minifySyntax: true, - platform: "node", - format: "esm", - jsx: "automatic", - jsxImportSource: "preact", - packages: "external", - metafile: true, - sourcemap: true, - sourcesContent: false, - plugins: [ - sassPlugin({ - type: "css-text", - cssImports: true, - }), - { - 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", "") - - 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 buildMutex = new Mutex() - let lastBuildMs = 0 - let cleanupBuild = null - const build = async (clientRefresh) => { - const buildStart = new Date().getTime() - lastBuildMs = buildStart - const release = await buildMutex.acquire() - if (lastBuildMs > buildStart) { - release() - return - } - - if (cleanupBuild) { - await cleanupBuild() - console.log(chalk.yellow("Detected a source code change, doing a hard rebuild...")) - } - - const result = await ctx.rebuild().catch((err) => { - console.error(`${chalk.red("Couldn't parse Quartz configuration:")} ${fp}`) - console.log(`Reason: ${chalk.grey(err)}`) - process.exit(1) - }) - release() - - if (argv.bundleInfo) { - 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(await esbuild.analyzeMetafile(result.metafile, { color: true })) - } - - // bypass module cache - // https://github.com/nodejs/modules/issues/307 - const { default: buildQuartz } = await import(cacheFile + `?update=${randomUUID()}`) - cleanupBuild = await buildQuartz(argv, buildMutex, clientRefresh) - clientRefresh() - } - - if (argv.serve) { - const connections = [] - const clientRefresh = () => connections.forEach((conn) => conn.send("rebuild")) - - if (argv.baseDir !== "" && !argv.baseDir.startsWith("/")) { - argv.baseDir = "/" + argv.baseDir - } - - await build(clientRefresh) - const server = http.createServer(async (req, res) => { - if (argv.baseDir && !req.url?.startsWith(argv.baseDir)) { - console.log( - chalk.red( - `[404] ${req.url} (warning: link outside of site, this is likely a Quartz bug)`, - ), - ) - res.writeHead(404) - res.end() - return - } - - // strip baseDir prefix - req.url = req.url?.slice(argv.baseDir.length) - - const serve = async () => { - const release = await buildMutex.acquire() - await serveHandler(req, res, { - public: argv.output, - directoryListing: false, - headers: [ - { - source: "**/*.html", - headers: [{ key: "Content-Disposition", value: "inline" }], - }, - ], - }) - const status = res.statusCode - const statusString = - status >= 200 && status < 300 ? chalk.green(`[${status}]`) : chalk.red(`[${status}]`) - console.log(statusString + chalk.grey(` ${argv.baseDir}${req.url}`)) - release() - } - - const redirect = (newFp) => { - newFp = argv.baseDir + newFp - res.writeHead(302, { - Location: newFp, - }) - console.log(chalk.yellow("[302]") + chalk.grey(` ${argv.baseDir}${req.url} -> ${newFp}`)) - res.end() - } - - let fp = req.url?.split("?")[0] ?? "/" - - // handle redirects - if (fp.endsWith("/")) { - // /trailing/ - // does /trailing/index.html exist? if so, serve it - const indexFp = path.posix.join(fp, "index.html") - if (fs.existsSync(path.posix.join(argv.output, indexFp))) { - req.url = fp - return serve() - } - - // does /trailing.html exist? if so, redirect to /trailing - let base = fp.slice(0, -1) - if (path.extname(base) === "") { - base += ".html" - } - if (fs.existsSync(path.posix.join(argv.output, base))) { - return redirect(fp.slice(0, -1)) - } - } else { - // /regular - // does /regular.html exist? if so, serve it - let base = fp - if (path.extname(base) === "") { - base += ".html" - } - if (fs.existsSync(path.posix.join(argv.output, base))) { - req.url = fp - return serve() - } - - // does /regular/index.html exist? if so, redirect to /regular/ - let indexFp = path.posix.join(fp, "index.html") - if (fs.existsSync(path.posix.join(argv.output, indexFp))) { - return redirect(fp + "/") - } - } - - return serve() - }) - server.listen(argv.port) - const wss = new WebSocketServer({ port: 3001 }) - wss.on("connection", (ws) => connections.push(ws)) - console.log( - chalk.cyan( - `Started a Quartz server listening at http://localhost:${argv.port}${argv.baseDir}`, - ), - ) - console.log("hint: exit with ctrl+c") - chokidar - .watch(["**/*.ts", "**/*.tsx", "**/*.scss", "package.json"], { - ignoreInitial: true, - }) - .on("all", async () => { - build(clientRefresh) - }) - } else { - await build(() => {}) - ctx.dispose() - } + await handleBuild(argv) }) .showHelpOnFail(false) .help() diff --git a/quartz/cli/args.js b/quartz/cli/args.js new file mode 100644 index 000000000..4f330cd9e --- /dev/null +++ b/quartz/cli/args.js @@ -0,0 +1,88 @@ +export const CommonArgv = { + directory: { + string: true, + alias: ["d"], + default: "content", + describe: "directory to look for content files", + }, + verbose: { + boolean: true, + alias: ["v"], + default: false, + describe: "print out extra logging information", + }, +} + +export const CreateArgv = { + ...CommonArgv, + source: { + string: true, + alias: ["s"], + describe: "source directory to copy/create symlink from", + }, + strategy: { + string: true, + alias: ["X"], + choices: ["new", "copy", "symlink"], + describe: "strategy for content folder setup", + }, + links: { + string: true, + alias: ["l"], + choices: ["absolute", "shortest", "relative"], + describe: "strategy to resolve links", + }, +} + +export const SyncArgv = { + ...CommonArgv, + commit: { + boolean: true, + default: true, + describe: "create a git commit for your unsaved changes", + }, + push: { + boolean: true, + default: true, + describe: "push updates to your Quartz fork", + }, + pull: { + boolean: true, + default: true, + describe: "pull updates from your Quartz fork", + }, +} + +export const BuildArgv = { + ...CommonArgv, + output: { + string: true, + alias: ["o"], + default: "public", + describe: "output folder for files", + }, + serve: { + boolean: true, + default: false, + describe: "run a local server to live-preview your Quartz", + }, + baseDir: { + string: true, + default: "", + describe: "base path to serve your local server on", + }, + port: { + number: true, + default: 8080, + describe: "port to serve Quartz on", + }, + bundleInfo: { + boolean: true, + default: false, + describe: "show detailed bundle information", + }, + concurrency: { + number: true, + describe: "how many threads to use to parse notes", + }, +} diff --git a/quartz/cli/constants.js b/quartz/cli/constants.js new file mode 100644 index 000000000..f4a9ce52b --- /dev/null +++ b/quartz/cli/constants.js @@ -0,0 +1,15 @@ +import path from "path" +import { readFileSync } from "fs" + +/** + * All constants relating to helpers or handlers + */ +export const ORIGIN_NAME = "origin" +export const UPSTREAM_NAME = "upstream" +export const QUARTZ_SOURCE_BRANCH = "v4" +export const cwd = process.cwd() +export const cacheDir = path.join(cwd, ".quartz-cache") +export const cacheFile = "./quartz/.quartz-cache/transpiled-build.mjs" +export const fp = "./quartz/build.ts" +export const { version } = JSON.parse(readFileSync("./package.json").toString()) +export const contentCacheFolder = path.join(cacheDir, "content-cache") diff --git a/quartz/cli/handlers.js b/quartz/cli/handlers.js new file mode 100644 index 000000000..cba0ceb26 --- /dev/null +++ b/quartz/cli/handlers.js @@ -0,0 +1,511 @@ +import { promises, readFileSync } from "fs" +import path from "path" +import esbuild from "esbuild" +import chalk from "chalk" +import { sassPlugin } from "esbuild-sass-plugin" +import fs from "fs" +import { intro, outro, select, text } from "@clack/prompts" +import { rimraf } from "rimraf" +import chokidar from "chokidar" +import prettyBytes from "pretty-bytes" +import { execSync, spawnSync } from "child_process" +import http from "http" +import serveHandler from "serve-handler" +import { WebSocketServer } from "ws" +import { randomUUID } from "crypto" +import { Mutex } from "async-mutex" +import { CreateArgv } from "./args.js" +import { + exitIfCancel, + escapePath, + gitPull, + popContentFolder, + stashContentFolder, +} from "./helpers.js" +import { + UPSTREAM_NAME, + QUARTZ_SOURCE_BRANCH, + ORIGIN_NAME, + version, + fp, + cacheFile, + cwd, +} from "./constants.js" + +/** + * Handles `npx quartz create` + * @param {*} argv arguments for `create` + */ +export async function handleCreate(argv) { + console.log() + intro(chalk.bgGreen.black(` Quartz v${version} `)) + const contentFolder = path.join(cwd, argv.directory) + let setupStrategy = argv.strategy?.toLowerCase() + let linkResolutionStrategy = argv.links?.toLowerCase() + const sourceDirectory = argv.source + + // If all cmd arguments were provided, check if theyre valid + if (setupStrategy && linkResolutionStrategy) { + // If setup isn't, "new", source argument is required + if (setupStrategy !== "new") { + // Error handling + if (!sourceDirectory) { + outro( + chalk.red( + `Setup strategies (arg '${chalk.yellow( + `-${CreateArgv.strategy.alias[0]}`, + )}') other than '${chalk.yellow( + "new", + )}' require content folder argument ('${chalk.yellow( + `-${CreateArgv.source.alias[0]}`, + )}') to be set`, + ), + ) + process.exit(1) + } else { + if (!fs.existsSync(sourceDirectory)) { + outro( + chalk.red( + `Input directory to copy/symlink 'content' from not found ('${chalk.yellow( + sourceDirectory, + )}', invalid argument "${chalk.yellow(`-${CreateArgv.source.alias[0]}`)})`, + ), + ) + process.exit(1) + } else if (!fs.lstatSync(sourceDirectory).isDirectory()) { + outro( + chalk.red( + `Source directory to copy/symlink 'content' from is not a directory (found file at '${chalk.yellow( + sourceDirectory, + )}', invalid argument ${chalk.yellow(`-${CreateArgv.source.alias[0]}`)}")`, + ), + ) + process.exit(1) + } + } + } + } + + // Use cli process if cmd args werent provided + if (!setupStrategy) { + setupStrategy = exitIfCancel( + await select({ + message: `Choose how to initialize the content in \`${contentFolder}\``, + options: [ + { value: "new", label: "Empty Quartz" }, + { value: "copy", label: "Copy 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!", + }, + ], + }), + ) + } + + async function rmContentFolder() { + const contentStat = await fs.promises.lstat(contentFolder) + if (contentStat.isSymbolicLink()) { + await fs.promises.unlink(contentFolder) + } else { + await rimraf(contentFolder) + } + } + + await fs.promises.unlink(path.join(contentFolder, ".gitkeep")) + if (setupStrategy === "copy" || setupStrategy === "symlink") { + let originalFolder = sourceDirectory + + // If input directory was not passed, use cli + if (!sourceDirectory) { + 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") { + await fs.promises.cp(originalFolder, contentFolder, { + recursive: true, + preserveTimestamps: true, + }) + } else if (setupStrategy === "symlink") { + await fs.promises.symlink(originalFolder, contentFolder, "dir") + } + } else if (setupStrategy === "new") { + 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. +`, + ) + } + + // Use cli process if cmd args werent provided + if (!linkResolutionStrategy) { + // get a preferred link resolution strategy + 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}'`, + ) + await fs.promises.writeFile(configFilePath, configContent) + + outro(`You're all set! Not sure what to do next? Try: + • Customizing Quartz a bit more by editing \`quartz.config.ts\` + • Running \`npx quartz build --serve\` to preview your Quartz locally + • Hosting your Quartz online (see: https://quartz.jzhao.xyz/hosting) +`) +} + +/** + * Handles `npx quartz build` + * @param {*} argv arguments for `build` + */ +export async function handleBuild(argv) { + console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`)) + const ctx = await esbuild.context({ + entryPoints: [fp], + outfile: cacheFile, + bundle: true, + keepNames: true, + minifyWhitespace: true, + minifySyntax: true, + platform: "node", + format: "esm", + jsx: "automatic", + jsxImportSource: "preact", + packages: "external", + metafile: true, + sourcemap: true, + sourcesContent: false, + plugins: [ + sassPlugin({ + type: "css-text", + cssImports: true, + }), + { + 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", "") + + 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 buildMutex = new Mutex() + let lastBuildMs = 0 + let cleanupBuild = null + const build = async (clientRefresh) => { + const buildStart = new Date().getTime() + lastBuildMs = buildStart + const release = await buildMutex.acquire() + if (lastBuildMs > buildStart) { + release() + return + } + + if (cleanupBuild) { + await cleanupBuild() + console.log(chalk.yellow("Detected a source code change, doing a hard rebuild...")) + } + + const result = await ctx.rebuild().catch((err) => { + console.error(`${chalk.red("Couldn't parse Quartz configuration:")} ${fp}`) + console.log(`Reason: ${chalk.grey(err)}`) + process.exit(1) + }) + release() + + if (argv.bundleInfo) { + 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(await esbuild.analyzeMetafile(result.metafile, { color: true })) + } + + // bypass module cache + // https://github.com/nodejs/modules/issues/307 + const { default: buildQuartz } = await import(`../../${cacheFile}?update=${randomUUID()}`) + // ^ this import is relative, so base "cacheFile" path can't be used + + cleanupBuild = await buildQuartz(argv, buildMutex, clientRefresh) + clientRefresh() + } + + if (argv.serve) { + const connections = [] + const clientRefresh = () => connections.forEach((conn) => conn.send("rebuild")) + + if (argv.baseDir !== "" && !argv.baseDir.startsWith("/")) { + argv.baseDir = "/" + argv.baseDir + } + + await build(clientRefresh) + const server = http.createServer(async (req, res) => { + if (argv.baseDir && !req.url?.startsWith(argv.baseDir)) { + console.log( + chalk.red( + `[404] ${req.url} (warning: link outside of site, this is likely a Quartz bug)`, + ), + ) + res.writeHead(404) + res.end() + return + } + + // strip baseDir prefix + req.url = req.url?.slice(argv.baseDir.length) + + const serve = async () => { + const release = await buildMutex.acquire() + await serveHandler(req, res, { + public: argv.output, + directoryListing: false, + headers: [ + { + source: "**/*.html", + headers: [{ key: "Content-Disposition", value: "inline" }], + }, + ], + }) + const status = res.statusCode + const statusString = + status >= 200 && status < 300 ? chalk.green(`[${status}]`) : chalk.red(`[${status}]`) + console.log(statusString + chalk.grey(` ${argv.baseDir}${req.url}`)) + release() + } + + const redirect = (newFp) => { + newFp = argv.baseDir + newFp + res.writeHead(302, { + Location: newFp, + }) + console.log(chalk.yellow("[302]") + chalk.grey(` ${argv.baseDir}${req.url} -> ${newFp}`)) + res.end() + } + + let fp = req.url?.split("?")[0] ?? "/" + + // handle redirects + if (fp.endsWith("/")) { + // /trailing/ + // does /trailing/index.html exist? if so, serve it + const indexFp = path.posix.join(fp, "index.html") + if (fs.existsSync(path.posix.join(argv.output, indexFp))) { + req.url = fp + return serve() + } + + // does /trailing.html exist? if so, redirect to /trailing + let base = fp.slice(0, -1) + if (path.extname(base) === "") { + base += ".html" + } + if (fs.existsSync(path.posix.join(argv.output, base))) { + return redirect(fp.slice(0, -1)) + } + } else { + // /regular + // does /regular.html exist? if so, serve it + let base = fp + if (path.extname(base) === "") { + base += ".html" + } + if (fs.existsSync(path.posix.join(argv.output, base))) { + req.url = fp + return serve() + } + + // does /regular/index.html exist? if so, redirect to /regular/ + let indexFp = path.posix.join(fp, "index.html") + if (fs.existsSync(path.posix.join(argv.output, indexFp))) { + return redirect(fp + "/") + } + } + + return serve() + }) + server.listen(argv.port) + const wss = new WebSocketServer({ port: 3001 }) + wss.on("connection", (ws) => connections.push(ws)) + console.log( + chalk.cyan( + `Started a Quartz server listening at http://localhost:${argv.port}${argv.baseDir}`, + ), + ) + console.log("hint: exit with ctrl+c") + chokidar + .watch(["**/*.ts", "**/*.tsx", "**/*.scss", "package.json"], { + ignoreInitial: true, + }) + .on("all", async () => { + build(clientRefresh) + }) + } else { + await build(() => {}) + ctx.dispose() + } +} + +/** + * Handles `npx quartz update` + * @param {*} argv arguments for `update` + */ +export async function handleUpdate(argv) { + const contentFolder = path.join(cwd, argv.directory) + console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`)) + console.log("Backing up your content") + execSync( + `git remote show upstream || git remote add upstream https://github.com/jackyzha0/quartz.git`, + ) + await stashContentFolder(contentFolder) + console.log( + "Pulling updates... you may need to resolve some `git` conflicts if you've made changes to components or plugins.", + ) + gitPull(UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH) + await popContentFolder(contentFolder) + console.log("Ensuring dependencies are up to date") + spawnSync("npm", ["i"], { stdio: "inherit" }) + console.log(chalk.green("Done!")) +} + +/** + * Handles `npx quartz restore` + * @param {*} argv arguments for `restore` + */ +export async function handleRestore(argv) { + const contentFolder = path.join(cwd, argv.directory) + await popContentFolder(contentFolder) +} + +/** + * Handles `npx quartz sync` + * @param {*} argv arguments for `sync` + */ +export async function handleSync(argv) { + const contentFolder = path.join(cwd, argv.directory) + console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`)) + console.log("Backing up your content") + + if (argv.commit) { + const contentStat = await fs.promises.lstat(contentFolder) + if (contentStat.isSymbolicLink()) { + const linkTarg = await fs.promises.readlink(contentFolder) + console.log(chalk.yellow("Detected symlink, trying to dereference before committing")) + + // stash symlink file + await stashContentFolder(contentFolder) + + // follow symlink and copy content + await fs.promises.cp(linkTarg, contentFolder, { + recursive: true, + preserveTimestamps: true, + }) + } + + const currentTimestamp = new Date().toLocaleString("en-US", { + dateStyle: "medium", + timeStyle: "short", + }) + spawnSync("git", ["add", "."], { stdio: "inherit" }) + spawnSync("git", ["commit", "-m", `Quartz sync: ${currentTimestamp}`], { stdio: "inherit" }) + + if (contentStat.isSymbolicLink()) { + // put symlink back + await popContentFolder(contentFolder) + } + } + + 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.", + ) + gitPull(ORIGIN_NAME, QUARTZ_SOURCE_BRANCH) + } + + await popContentFolder(contentFolder) + if (argv.push) { + console.log("Pushing your changes") + spawnSync("git", ["push", "-f", ORIGIN_NAME, QUARTZ_SOURCE_BRANCH], { stdio: "inherit" }) + } + + console.log(chalk.green("Done!")) +} diff --git a/quartz/cli/helpers.js b/quartz/cli/helpers.js new file mode 100644 index 000000000..b07d19e3c --- /dev/null +++ b/quartz/cli/helpers.js @@ -0,0 +1,52 @@ +import { isCancel, outro } from "@clack/prompts" +import chalk from "chalk" +import { contentCacheFolder } from "./constants.js" +import { spawnSync } from "child_process" +import fs from "fs" + +export function escapePath(fp) { + return fp + .replace(/\\ /g, " ") // unescape spaces + .replace(/^".*"$/, "$1") + .replace(/^'.*"$/, "$1") + .trim() +} + +export function exitIfCancel(val) { + if (isCancel(val)) { + outro(chalk.red("Exiting")) + process.exit(0) + } else { + return val + } +} + +export async function stashContentFolder(contentFolder) { + await fs.promises.rm(contentCacheFolder, { force: true, recursive: true }) + await fs.promises.cp(contentFolder, contentCacheFolder, { + force: true, + recursive: true, + verbatimSymlinks: true, + preserveTimestamps: true, + }) + await fs.promises.rm(contentFolder, { force: true, recursive: true }) +} + +export function gitPull(origin, branch) { + const flags = ["--no-rebase", "--autostash", "-s", "recursive", "-X", "ours", "--no-edit"] + const out = spawnSync("git", ["pull", ...flags, origin, branch], { stdio: "inherit" }) + if (out.stderr) { + throw new Error(`Error while pulling updates: ${out.stderr}`) + } +} + +export async function popContentFolder(contentFolder) { + await fs.promises.rm(contentFolder, { force: true, recursive: true }) + await fs.promises.cp(contentCacheFolder, contentFolder, { + force: true, + recursive: true, + verbatimSymlinks: true, + preserveTimestamps: true, + }) + await fs.promises.rm(contentCacheFolder, { force: true, recursive: true }) +} From b6b1dabde0f63ca0ae743aa7f4266ca892d7b5e5 Mon Sep 17 00:00:00 2001 From: Jeremy Press Date: Sun, 27 Aug 2023 17:39:42 -0700 Subject: [PATCH 043/140] feat: support configurable ws port and remote development (#429) Co-authored-by: Jeremy Press Co-authored-by: Jacky Zhao --- .gitignore | 2 ++ quartz/cli/args.js | 10 ++++++++++ quartz/cli/handlers.js | 2 +- quartz/plugins/emitters/componentResources.ts | 8 +++++++- quartz/util/ctx.ts | 2 ++ 5 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index fd96fec90..25d07db1c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ tsconfig.tsbuildinfo .obsidian .quartz-cache private/ +.replit +replit.nix diff --git a/quartz/cli/args.js b/quartz/cli/args.js index 4f330cd9e..3543e2e89 100644 --- a/quartz/cli/args.js +++ b/quartz/cli/args.js @@ -76,6 +76,16 @@ export const BuildArgv = { default: 8080, describe: "port to serve Quartz on", }, + wsPort: { + number: true, + default: 3001, + describe: "port to use for WebSocket-based hot-reload notifications", + }, + remoteDevHost: { + string: true, + default: "", + describe: "A URL override for the websocket connection if you are not developing on localhost", + }, bundleInfo: { boolean: true, default: false, diff --git a/quartz/cli/handlers.js b/quartz/cli/handlers.js index cba0ceb26..bc3da73f3 100644 --- a/quartz/cli/handlers.js +++ b/quartz/cli/handlers.js @@ -402,7 +402,7 @@ export async function handleBuild(argv) { return serve() }) server.listen(argv.port) - const wss = new WebSocketServer({ port: 3001 }) + const wss = new WebSocketServer({ port: argv.wsPort }) wss.on("connection", (ws) => connections.push(ws)) console.log( chalk.cyan( diff --git a/quartz/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts index a62bc382b..61409cc57 100644 --- a/quartz/plugins/emitters/componentResources.ts +++ b/quartz/plugins/emitters/componentResources.ts @@ -107,12 +107,18 @@ function addGlobalPageResources( document.dispatchEvent(event)`) } + let wsUrl = `ws://localhost:${ctx.argv.wsPort}` + + if (ctx.argv.remoteDevHost) { + wsUrl = `wss://${ctx.argv.remoteDevHost}:${ctx.argv.wsPort}` + } + if (reloadScript) { staticResources.js.push({ loadTime: "afterDOMReady", contentType: "inline", script: ` - const socket = new WebSocket('ws://localhost:3001') + const socket = new WebSocket('${wsUrl}'') socket.addEventListener('message', () => document.location.reload()) `, }) diff --git a/quartz/util/ctx.ts b/quartz/util/ctx.ts index d30339190..13e0bf864 100644 --- a/quartz/util/ctx.ts +++ b/quartz/util/ctx.ts @@ -7,6 +7,8 @@ export interface Argv { output: string serve: boolean port: number + wsPort: number + remoteDevHost?: string concurrency?: number } From 082fdf2e8098ef6bcb46a7dfabf8c6b9fd096346 Mon Sep 17 00:00:00 2001 From: Jeremy Press Date: Sun, 27 Aug 2023 20:57:19 -0700 Subject: [PATCH 044/140] Fix typo :) (#430) --- quartz/plugins/emitters/componentResources.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts index 61409cc57..c52a3a20e 100644 --- a/quartz/plugins/emitters/componentResources.ts +++ b/quartz/plugins/emitters/componentResources.ts @@ -118,7 +118,7 @@ function addGlobalPageResources( loadTime: "afterDOMReady", contentType: "inline", script: ` - const socket = new WebSocket('${wsUrl}'') + const socket = new WebSocket('${wsUrl}') socket.addEventListener('message', () => document.location.reload()) `, }) From c35cd422c65a58f1069302aad0cf9eef7f93d987 Mon Sep 17 00:00:00 2001 From: Ben Schlegel <31989404+benschlegel@users.noreply.github.com> Date: Mon, 28 Aug 2023 19:00:49 +0200 Subject: [PATCH 045/140] fix: correct graph labels for `index.md` nodes (#431) --- quartz/components/scripts/graph.inline.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/quartz/components/scripts/graph.inline.ts b/quartz/components/scripts/graph.inline.ts index e589217f2..d72b297bf 100644 --- a/quartz/components/scripts/graph.inline.ts +++ b/quartz/components/scripts/graph.inline.ts @@ -231,7 +231,9 @@ async function renderGraph(container: string, fullSlug: FullSlug) { .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("-", " "), + (d) => + data[d.id]?.title || + (d.id.charAt(0).toUpperCase() + d.id.slice(1, d.id.length - 1)).replace("-", " "), ) .style("opacity", (opacityScale - 1) / 3.75) .style("pointer-events", "none") From 1cc09ef76db129fb3670e95560312adeefab913c Mon Sep 17 00:00:00 2001 From: Jeffrey Fabian Date: Tue, 29 Aug 2023 13:14:54 -0400 Subject: [PATCH 046/140] feat: support kebab-case and nested tags in Obsidian-flavored Markdown tag-in-content parsing (#425) * enhancement: support kebab-case and nested tags in ofm transformer * update regex/capture groups to allow for (arbitrarily) nested values and tags of only -/_ * Update quartz/plugins/transformers/ofm.ts --------- Co-authored-by: Jacky Zhao --- quartz/plugins/transformers/ofm.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index 4d1586f93..2e8fadb22 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -114,9 +114,11 @@ const commentRegex = new RegExp(/%%(.+)%%/, "g") // from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts const calloutRegex = new RegExp(/^\[\!(\w+)\]([+-]?)/) const calloutLineRegex = new RegExp(/^> *\[\!\w+\][+-]?.*$/, "gm") -// (?:^| ) -> non-capturing group, tag should start be separated by a space or be the start of the line -// #(\w+) -> tag itself is # followed by a string of alpha-numeric characters -const tagRegex = new RegExp(/(?:^| )#(\p{L}+)/, "gu") +// (?:^| ) -> non-capturing group, tag should start be separated by a space or be the start of the line +// #(...) -> capturing group, tag itself must start with # +// (?:[-_\p{L}])+ -> non-capturing group, non-empty string of (Unicode-aware) alpha-numeric characters, hyphens and/or underscores +// (?:\/[-_\p{L}]+)*) -> non-capturing group, matches an arbitrary number of tag strings separated by "/" +const tagRegex = new RegExp(/(?:^| )#((?:[-_\p{L}\d])+(?:\/[-_\p{L}\d]+)*)/, "gu") export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin | undefined> = ( userOpts, @@ -320,7 +322,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin const titleHtml: HTML = { type: "html", - value: `
    ${callouts[calloutType]}
    @@ -429,7 +431,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin script: ` import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs'; const darkMode = document.documentElement.getAttribute('saved-theme') === 'dark' - mermaid.initialize({ + mermaid.initialize({ startOnLoad: false, securityLevel: 'loose', theme: darkMode ? 'dark' : 'default' From 5fa6fc97899c905b6fbc14fa1d24334f3e68fa77 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Tue, 29 Aug 2023 10:37:00 -0700 Subject: [PATCH 047/140] fix: aliasredirects not using full path, add permalink support --- quartz/plugins/emitters/aliases.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/quartz/plugins/emitters/aliases.ts b/quartz/plugins/emitters/aliases.ts index c7294a343..942412e9d 100644 --- a/quartz/plugins/emitters/aliases.ts +++ b/quartz/plugins/emitters/aliases.ts @@ -12,15 +12,20 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({ for (const [_tree, file] of content) { const ogSlug = simplifySlug(file.data.slug!) - const dir = path.posix.relative(argv.directory, file.dirname ?? argv.directory) + const dir = path.posix.relative(argv.directory, path.dirname(file.data.filePath!)) let aliases: FullSlug[] = file.data.frontmatter?.aliases ?? file.data.frontmatter?.alias ?? [] if (typeof aliases === "string") { aliases = [aliases] } - for (const alias of aliases) { - const slug = path.posix.join(dir, alias) as FullSlug + const slugs: FullSlug[] = aliases.map((alias) => path.posix.join(dir, alias) as FullSlug) + const permalink = file.data.frontmatter?.permalink + if (typeof permalink === "string") { + slugs.push(permalink as FullSlug) + } + + for (const slug of slugs) { const redirUrl = resolveRelative(slug, file.data.slug!) const fp = await emit({ content: ` From b213ba45e2e706332e057b131adc946f882f090b Mon Sep 17 00:00:00 2001 From: Ben Schlegel <31989404+benschlegel@users.noreply.github.com> Date: Thu, 31 Aug 2023 20:55:04 +0200 Subject: [PATCH 048/140] fix: regex for matching highlights (closes #437) (#438) * fix: regex for matching highlights * fix: regex for empty highlights --- quartz/plugins/transformers/ofm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index 2e8fadb22..8c8da67bc 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -109,7 +109,7 @@ const capitalize = (s: string): string => { // (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link) // (|[^\[\]\|\#]+)? -> | then one or more non-special characters (alias) const wikilinkRegex = new RegExp(/!?\[\[([^\[\]\|\#]+)?(#[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/, "g") -const highlightRegex = new RegExp(/==(.+)==/, "g") +const highlightRegex = new RegExp(/==([^=]+)==/, "g") const commentRegex = new RegExp(/%%(.+)%%/, "g") // from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts const calloutRegex = new RegExp(/^\[\!(\w+)\]([+-]?)/) From 2d6dc176c3e1fbb520a5da1beb60bbb1d8e948ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pelayo=20Arbu=C3=A9s?= Date: Thu, 31 Aug 2023 21:12:06 +0200 Subject: [PATCH 049/140] Adds Pelayo Arbues to showcase (#435) --- docs/showcase.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/showcase.md b/docs/showcase.md index d4a9da2b9..d2282be25 100644 --- a/docs/showcase.md +++ b/docs/showcase.md @@ -16,5 +16,6 @@ Want to see what Quartz can do? Here are some cool community gardens: - [Abhijeet's Math Wiki](https://abhmul.github.io/quartz/Math-Wiki/) - [Mike's AI Garden 🤖🪴](https://mwalton.me/) - [Matt Dunn's Second Brain](https://mattdunn.info/) +- [Pelayo Arbues' Notes](https://pelayoarbues.github.io/) 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/content/showcase.md)! From 90dac31216b5d3f59e65ec5778e21a308a744e11 Mon Sep 17 00:00:00 2001 From: Ben Schlegel <31989404+benschlegel@users.noreply.github.com> Date: Fri, 1 Sep 2023 19:09:58 +0200 Subject: [PATCH 050/140] feat: Implement search for tags (#436) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Quartz sync: Aug 29, 2023, 10:17 PM * style: add basic style to tags in search * feat: add SearchType + tags to search preview * feat: support multiple matches * style(search): add style to matching tags * feat(search): add content to preview for tag search * fix: only display tags on tag search * feat: support basic + tag search * refactor: extract common `fillDocument`, format * feat: add hotkey to search for tags * chore: remove logs * fix: dont render empty `
      ` if tags not present * fix(search-tag): make case insensitive * refactor: clean `hideSearch` and `showSearch` * feat: trim content similar to `description.ts` * fix(search-tag): hotkey for windows * perf: re-use main index for tag search --- quartz/components/scripts/search.inline.ts | 163 ++++++++++++++++++--- quartz/components/styles/search.scss | 38 +++++ 2 files changed, 179 insertions(+), 22 deletions(-) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index ef26ba380..806a746e6 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -1,4 +1,4 @@ -import { Document } from "flexsearch" +import { Document, SimpleDocumentSearchResultSetUnit } from "flexsearch" import { ContentDetails } from "../../plugins/emitters/contentIndex" import { registerEscapeHandler, removeAllChildren } from "./util" import { FullSlug, resolveRelative } from "../../util/path" @@ -8,12 +8,20 @@ interface Item { slug: FullSlug title: string content: string + tags: string[] } let index: Document | undefined = undefined +// Can be expanded with things like "term" in the future +type SearchType = "basic" | "tags" + +// Current searchType +let searchType: SearchType = "basic" + const contextWindowWords = 30 const numSearchResults = 5 +const numTagResults = 3 function highlight(searchTerm: string, text: string, trim?: boolean) { // try to highlight longest tokens first const tokenizedTerms = searchTerm @@ -87,9 +95,12 @@ document.addEventListener("nav", async (e: unknown) => { if (results) { removeAllChildren(results) } + + searchType = "basic" // reset search type after closing } - function showSearch() { + function showSearch(searchTypeNew: SearchType) { + searchType = searchTypeNew if (sidebar) { sidebar.style.zIndex = "1" } @@ -98,10 +109,18 @@ document.addEventListener("nav", async (e: unknown) => { } function shortcutHandler(e: HTMLElementEventMap["keydown"]) { - if (e.key === "k" && (e.ctrlKey || e.metaKey)) { + if (e.key === "k" && (e.ctrlKey || e.metaKey) && !e.shiftKey) { e.preventDefault() const searchBarOpen = container?.classList.contains("active") - searchBarOpen ? hideSearch() : showSearch() + searchBarOpen ? hideSearch() : showSearch("basic") + } else if (e.shiftKey && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { + // Hotkey to open tag search + e.preventDefault() + const searchBarOpen = container?.classList.contains("active") + searchBarOpen ? hideSearch() : showSearch("tags") + + // add "#" prefix for tag search + if (searchBar) searchBar.value = "#" } else if (e.key === "Enter") { const anchor = document.getElementsByClassName("result-card")[0] as HTMLInputElement | null if (anchor) { @@ -110,21 +129,77 @@ document.addEventListener("nav", async (e: unknown) => { } } + function trimContent(content: string) { + // works without escaping html like in `description.ts` + const sentences = content.replace(/\s+/g, " ").split(".") + let finalDesc = "" + let sentenceIdx = 0 + + // Roughly estimate characters by (words * 5). Matches description length in `description.ts`. + const len = contextWindowWords * 5 + while (finalDesc.length < len) { + const sentence = sentences[sentenceIdx] + if (!sentence) break + finalDesc += sentence + "." + sentenceIdx++ + } + + // If more content would be available, indicate it by finishing with "..." + if (finalDesc.length < content.length) { + finalDesc += ".." + } + + return finalDesc + } + const formatForDisplay = (term: string, id: number) => { const slug = idDataMap[id] return { id, slug, title: highlight(term, data[slug].title ?? ""), - content: highlight(term, data[slug].content ?? "", true), + // if searchType is tag, display context from start of file and trim, otherwise use regular highlight + content: + searchType === "tags" + ? trimContent(data[slug].content) + : highlight(term, data[slug].content ?? "", true), + tags: highlightTags(term, data[slug].tags), } } - const resultToHTML = ({ slug, title, content }: Item) => { + function highlightTags(term: string, tags: string[]) { + if (tags && searchType === "tags") { + // Find matching tags + const termLower = term.toLowerCase() + let matching = tags.filter((str) => str.includes(termLower)) + + // Substract matching from original tags, then push difference + if (matching.length > 0) { + let difference = tags.filter((x) => !matching.includes(x)) + + // Convert to html (cant be done later as matches/term dont get passed to `resultToHTML`) + matching = matching.map((tag) => `
    • #${tag}

    • `) + difference = difference.map((tag) => `
    • #${tag}

    • `) + matching.push(...difference) + } + + // Only allow max of `numTagResults` in preview + if (tags.length > numTagResults) { + matching.splice(numTagResults) + } + + return matching + } else { + return [] + } + } + + const resultToHTML = ({ slug, title, content, tags }: Item) => { + const htmlTags = tags.length > 0 ? `
        ${tags.join("")}
      ` : `` const button = document.createElement("button") button.classList.add("result-card") button.id = slug - button.innerHTML = `

      ${title}

      ${content}

      ` + button.innerHTML = `

      ${title}

      ${htmlTags}

      ${content}

      ` button.addEventListener("click", () => { const targ = resolveRelative(currentSlug, slug) window.spaNavigate(new URL(targ, window.location.toString())) @@ -148,15 +223,45 @@ document.addEventListener("nav", async (e: unknown) => { } async function onType(e: HTMLElementEventMap["input"]) { - const term = (e.target as HTMLInputElement).value - const searchResults = (await index?.searchAsync(term, numSearchResults)) ?? [] + let term = (e.target as HTMLInputElement).value + let searchResults: SimpleDocumentSearchResultSetUnit[] + + if (term.toLowerCase().startsWith("#")) { + searchType = "tags" + } else { + searchType = "basic" + } + + switch (searchType) { + case "tags": { + term = term.substring(1) + searchResults = + (await index?.searchAsync({ query: term, limit: numSearchResults, index: ["tags"] })) ?? + [] + break + } + case "basic": + default: { + searchResults = + (await index?.searchAsync({ + query: term, + limit: numSearchResults, + index: ["title", "content"], + })) ?? [] + } + } + const getByField = (field: string): number[] => { const results = searchResults.filter((x) => x.field === field) return results.length === 0 ? [] : ([...results[0].result] as number[]) } // order titles ahead of content - const allIds: Set = new Set([...getByField("title"), ...getByField("content")]) + const allIds: Set = new Set([ + ...getByField("title"), + ...getByField("content"), + ...getByField("tags"), + ]) const finalResults = [...allIds].map((id) => formatForDisplay(term, id)) displayResults(finalResults) } @@ -167,8 +272,8 @@ document.addEventListener("nav", async (e: unknown) => { document.addEventListener("keydown", shortcutHandler) prevShortcutHandler = shortcutHandler - searchIcon?.removeEventListener("click", showSearch) - searchIcon?.addEventListener("click", showSearch) + searchIcon?.removeEventListener("click", () => showSearch("basic")) + searchIcon?.addEventListener("click", () => showSearch("basic")) searchBar?.removeEventListener("input", onType) searchBar?.addEventListener("input", onType) @@ -190,22 +295,36 @@ document.addEventListener("nav", async (e: unknown) => { field: "content", tokenize: "reverse", }, + { + field: "tags", + tokenize: "reverse", + }, ], }, }) - let id = 0 - for (const [slug, fileData] of Object.entries(data)) { - await index.addAsync(id, { - id, - slug: slug as FullSlug, - title: fileData.title, - content: fileData.content, - }) - id++ - } + fillDocument(index, data) } // register handlers registerEscapeHandler(container, hideSearch) }) + +/** + * Fills flexsearch document with data + * @param index index to fill + * @param data data to fill index with + */ +async function fillDocument(index: Document, data: any) { + let id = 0 + for (const [slug, fileData] of Object.entries(data)) { + await index.addAsync(id, { + id, + slug: slug as FullSlug, + title: fileData.title, + content: fileData.content, + tags: fileData.tags, + }) + id++ + } +} diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index 4d5ad95cd..66f809f97 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -130,6 +130,44 @@ margin: 0; } + & > ul > li { + margin: 0; + display: inline-block; + white-space: nowrap; + margin: 0; + overflow-wrap: normal; + } + + & > ul { + list-style: none; + display: flex; + padding-left: 0; + gap: 0.4rem; + margin: 0; + margin-top: 0.45rem; + // Offset border radius + margin-left: -2px; + overflow: hidden; + background-clip: border-box; + } + + & > ul > li > p { + border-radius: 8px; + background-color: var(--highlight); + overflow: hidden; + background-clip: border-box; + padding: 0.03rem 0.4rem; + margin: 0; + color: var(--secondary); + opacity: 0.85; + } + + & > ul > li > .match-tag { + color: var(--tertiary); + font-weight: bold; + opacity: 1; + } + & > p { margin-bottom: 0; } From 23f43045c49f17fe5ace480f7026855acf2a30b8 Mon Sep 17 00:00:00 2001 From: Ben Schlegel <31989404+benschlegel@users.noreply.github.com> Date: Fri, 1 Sep 2023 23:12:32 +0200 Subject: [PATCH 051/140] fix(search): matches getting highlighted in title (#440) --- quartz/components/scripts/search.inline.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index 806a746e6..4b9e372bc 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -157,7 +157,7 @@ document.addEventListener("nav", async (e: unknown) => { return { id, slug, - title: highlight(term, data[slug].title ?? ""), + title: searchType === "tags" ? data[slug].title : highlight(term, data[slug].title ?? ""), // if searchType is tag, display context from start of file and trim, otherwise use regular highlight content: searchType === "tags" From 505673acd71e6b023abae19c706a736b257cff2a Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sat, 2 Sep 2023 18:07:26 -0700 Subject: [PATCH 052/140] feat: pluralize things in lists --- quartz/components/pages/FolderContent.tsx | 3 ++- quartz/components/pages/TagContent.tsx | 5 +++-- quartz/util/lang.ts | 7 +++++++ 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 quartz/util/lang.ts diff --git a/quartz/components/pages/FolderContent.tsx b/quartz/components/pages/FolderContent.tsx index dc076c4a1..a766d4b0b 100644 --- a/quartz/components/pages/FolderContent.tsx +++ b/quartz/components/pages/FolderContent.tsx @@ -7,6 +7,7 @@ import style from "../styles/listPage.scss" import { PageList } from "../PageList" import { _stripSlashes, simplifySlug } from "../../util/path" import { Root } from "hast" +import { pluralize } from "../../util/lang" function FolderContent(props: QuartzComponentProps) { const { tree, fileData, allFiles } = props @@ -36,7 +37,7 @@ function FolderContent(props: QuartzComponentProps) {

      {content}

      -

      {allPagesInFolder.length} items under this folder.

      +

      {pluralize(allPagesInFolder.length, "item")} under this folder.

      diff --git a/quartz/components/pages/TagContent.tsx b/quartz/components/pages/TagContent.tsx index fb72e284b..9907e3fc3 100644 --- a/quartz/components/pages/TagContent.tsx +++ b/quartz/components/pages/TagContent.tsx @@ -6,6 +6,7 @@ import { PageList } from "../PageList" import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path" import { QuartzPluginData } from "../../plugins/vfile" import { Root } from "hast" +import { pluralize } from "../../util/lang" const numPages = 10 function TagContent(props: QuartzComponentProps) { @@ -60,7 +61,7 @@ function TagContent(props: QuartzComponentProps) { {content &&

      {content}

      }

      - {pages.length} items with this tag.{" "} + {pluralize(pages.length, "item")} with this tag.{" "} {pages.length > numPages && `Showing first ${numPages}.`}

      @@ -80,7 +81,7 @@ function TagContent(props: QuartzComponentProps) { return (
      {content}
      -

      {pages.length} items with this tag.

      +

      {pluralize(pages.length, "item")} with this tag.

      diff --git a/quartz/util/lang.ts b/quartz/util/lang.ts new file mode 100644 index 000000000..eb03a2436 --- /dev/null +++ b/quartz/util/lang.ts @@ -0,0 +1,7 @@ +export function pluralize(count: number, s: string): string { + if (count === 1) { + return `1 ${s}` + } else { + return `${count} ${s}s` + } +} From 8c354f6261dda6d9761f594002db53c9d7a8e8e2 Mon Sep 17 00:00:00 2001 From: Ben Schlegel <31989404+benschlegel@users.noreply.github.com> Date: Sun, 3 Sep 2023 18:06:05 +0200 Subject: [PATCH 053/140] fix: clipboard button visible in search (#445) --- quartz/components/styles/clipboard.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/quartz/components/styles/clipboard.scss b/quartz/components/styles/clipboard.scss index 1702a7bb4..a585c7b52 100644 --- a/quartz/components/styles/clipboard.scss +++ b/quartz/components/styles/clipboard.scss @@ -10,7 +10,6 @@ background-color: var(--light); border: 1px solid; border-radius: 5px; - z-index: 1; opacity: 0; transition: 0.2s; From 7e42be8e46501c752dda9bd174fb93ea9dccec22 Mon Sep 17 00:00:00 2001 From: Ben Schlegel <31989404+benschlegel@users.noreply.github.com> Date: Sun, 3 Sep 2023 18:32:46 +0200 Subject: [PATCH 054/140] feat(search): add arrow key navigation (#442) * feat(search): add arrow navigation * chore: format * refactor: simplify arrow navigation * chore: remove comment * feat: rework arrow navigation to work without state * feat: make pressing enter work with arrow navigation * fix: remove unused css class * chore: correct comment * refactor(search): use optional chaining --- quartz/components/scripts/search.inline.ts | 29 +++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index 4b9e372bc..a1c3e6ca2 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -82,6 +82,7 @@ document.addEventListener("nav", async (e: unknown) => { const searchIcon = document.getElementById("search-icon") const searchBar = document.getElementById("search-bar") as HTMLInputElement | null const results = document.getElementById("results-container") + const resultCards = document.getElementsByClassName("result-card") const idDataMap = Object.keys(data) as FullSlug[] function hideSearch() { @@ -122,9 +123,31 @@ document.addEventListener("nav", async (e: unknown) => { // add "#" prefix for tag search if (searchBar) searchBar.value = "#" } else if (e.key === "Enter") { - const anchor = document.getElementsByClassName("result-card")[0] as HTMLInputElement | null - if (anchor) { - anchor.click() + // If result has focus, navigate to that one, otherwise pick first result + if (results?.contains(document.activeElement)) { + const active = document.activeElement as HTMLInputElement + active.click() + } else { + const anchor = document.getElementsByClassName("result-card")[0] as HTMLInputElement | null + anchor?.click() + } + } else if (e.key === "ArrowDown") { + e.preventDefault() + // When first pressing ArrowDown, results wont contain the active element, so focus first element + if (!results?.contains(document.activeElement)) { + const firstResult = resultCards[0] as HTMLInputElement | null + firstResult?.focus() + } else { + // If an element in results-container already has focus, focus next one + const nextResult = document.activeElement?.nextElementSibling as HTMLInputElement | null + nextResult?.focus() + } + } else if (e.key === "ArrowUp") { + e.preventDefault() + if (results?.contains(document.activeElement)) { + // If an element in results-container already has focus, focus previous one + const prevResult = document.activeElement?.previousElementSibling as HTMLInputElement | null + prevResult?.focus() } } } From e8a04efaf1b82560cbcf7694ac6c7dda1c82612f Mon Sep 17 00:00:00 2001 From: Adam Brangenberg Date: Mon, 4 Sep 2023 06:28:57 +0200 Subject: [PATCH 055/140] feat(analytics): Support for Umami (#449) --- quartz/cfg.ts | 4 ++++ quartz/plugins/emitters/componentResources.ts | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/quartz/cfg.ts b/quartz/cfg.ts index 21e03016a..8371b5e2b 100644 --- a/quartz/cfg.ts +++ b/quartz/cfg.ts @@ -12,6 +12,10 @@ export type Analytics = provider: "google" tagId: string } + | { + provider: "umami" + websiteId: string + } export interface GlobalConfiguration { pageTitle: string diff --git a/quartz/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts index c52a3a20e..96db8aa81 100644 --- a/quartz/plugins/emitters/componentResources.ts +++ b/quartz/plugins/emitters/componentResources.ts @@ -96,6 +96,15 @@ function addGlobalPageResources( });`) } else if (cfg.analytics?.provider === "plausible") { componentResources.afterDOMLoaded.push(plausibleScript) + } else if (cfg.analytics?.provider === "umami") { + componentResources.afterDOMLoaded.push(` + const umamiScript = document.createElement("script") + umamiScript.src = "https://analytics.umami.is/script.js" + umamiScript["data-website-id"] = "${cfg.analytics.websiteId}" + umamiScript.async = true + + document.head.appendChild(umamiScript) + `) } if (cfg.enableSPA) { From 616a7f148a283b2fd7c5204c60ddf1b08f42d125 Mon Sep 17 00:00:00 2001 From: Dr Kim Foale Date: Mon, 4 Sep 2023 05:29:58 +0100 Subject: [PATCH 056/140] docs: Make it clearer that wikilinks go to paths not page titles (#448) --- docs/features/wikilinks.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/features/wikilinks.md b/docs/features/wikilinks.md index 4d197157d..704a0d0cf 100644 --- a/docs/features/wikilinks.md +++ b/docs/features/wikilinks.md @@ -10,9 +10,9 @@ This is enabled as a part of [[Obsidian compatibility]] and can be configured an ## Syntax -- `[[Path to file]]`: produces a link to `Path to file` with the text `Path to file` -- `[[Path to file | Here's the title override]]`: produces a link to `Path to file` with the text `Here's the title override` -- `[[Path to file#Anchor]]`: produces a link to the anchor `Anchor` in the file `Path to file` +- `[[Path to file]]`: produces a link to `Path to file.md` (or `Path-to-file.md`) with the text `Path to file` +- `[[Path to file | Here's the title override]]`: produces a link to `Path to file.md` with the text `Here's the title override` +- `[[Path to file#Anchor]]`: produces a link to the anchor `Anchor` in the file `Path to file.md` > [!warning] > Currently, Quartz does not support block references or note embed syntax. From 6ef4246cf186414d1b8ee868cfa9d24314f0bc0a Mon Sep 17 00:00:00 2001 From: Ben Schlegel <31989404+benschlegel@users.noreply.github.com> Date: Mon, 4 Sep 2023 07:36:30 +0200 Subject: [PATCH 057/140] docs: update `full-text-search.md` (#447) --- docs/features/full-text search.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/features/full-text search.md b/docs/features/full-text search.md index ce3d88f93..85ec03006 100644 --- a/docs/features/full-text search.md +++ b/docs/features/full-text search.md @@ -6,9 +6,11 @@ tags: Full-text search in Quartz is powered by [Flexsearch](https://github.com/nextapps-de/flexsearch). It's fast enough to return search results in under 10ms for Quartzs as large as half a million words. -It can be opened by either clicking on the search bar or pressing ⌘+K. The top 5 search results are shown on each query. Matching subterms are highlighted and the most relevant 30 words are excerpted. Clicking on a search result will navigate to that page. +It can be opened by either clicking on the search bar or pressing `⌘`/`ctrl` + `K`. The top 5 search results are shown on each query. Matching subterms are highlighted and the most relevant 30 words are excerpted. Clicking on a search result will navigate to that page. -This component is also keyboard accessible: Tab and Shift+Tab will cycle forward and backward through search results and Enter will navigate to the highlighted result (first result by default). +To search content by tags, you can either press `⌘`/`ctrl` + `shift` + `K` or start your query with `#` (e.g. `#components`). + +This component is also keyboard accessible: Tab and Shift+Tab will cycle forward and backward through search results and Enter will navigate to the highlighted result (first result by default). You are also able to navigate search results using `ArrowUp` and `ArrowDown`. > [!info] > Search requires the `ContentIndex` emitter plugin to be present in the [[configuration]]. @@ -17,7 +19,7 @@ This component is also keyboard accessible: Tab and Shift+Tab will cycle forward By default, it indexes every page on the site with **Markdown syntax removed**. This means link URLs for instance are not indexed. -It properly tokenizes Chinese, Korean, and Japenese characters and constructs separate indexes for the title and content, weighing title matches above content matches. +It properly tokenizes Chinese, Korean, and Japenese characters and constructs separate indexes for the title, content and tags, weighing title matches above content matches. ## Customization @@ -25,4 +27,4 @@ It properly tokenizes Chinese, Korean, and Japenese characters and constructs se - Component: `quartz/components/Search.tsx` - Style: `quartz/components/styles/search.scss` - Script: `quartz/components/scripts/search.inline.ts` - - You can edit `contextWindowWords` or `numSearchResults` to suit your needs + - You can edit `contextWindowWords`, `numSearchResults` or `numTagResults` to suit your needs From 2d52eba4133293a27f6df98c252785ba4ddee575 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 6 Sep 2023 20:25:38 -0700 Subject: [PATCH 058/140] fix: dont transform external links --- quartz/plugins/transformers/links.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts index 26c4a3228..475a5e92e 100644 --- a/quartz/plugins/transformers/links.ts +++ b/quartz/plugins/transformers/links.ts @@ -54,7 +54,8 @@ export const CrawlLinks: QuartzTransformerPlugin | undefined> = node.properties.className.push(isAbsoluteUrl(dest) ? "external" : "internal") // don't process external links or intra-document anchors - if (!(isAbsoluteUrl(dest) || dest.startsWith("#"))) { + const isInternal = !(isAbsoluteUrl(dest) || dest.startsWith("#")) + if (isInternal) { dest = node.properties.href = transformLink( file.data.slug!, dest, @@ -77,6 +78,7 @@ export const CrawlLinks: QuartzTransformerPlugin | undefined> = // rewrite link internals if prettylinks is on if ( opts.prettyLinks && + isInternal && node.children.length === 1 && node.children[0].type === "text" && !node.children[0].value.startsWith("#") From 8d6029b7b844044d06fe17de89db6881954a8fec Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 6 Sep 2023 21:02:21 -0700 Subject: [PATCH 059/140] feat: 404 page emitter --- docs/features/OxHugo compatibility.md | 2 +- docs/features/upcoming features.md | 3 +- quartz.config.ts | 1 + quartz/components/index.ts | 4 +- quartz/components/pages/404.tsx | 12 ++++++ quartz/plugins/emitters/404.tsx | 56 +++++++++++++++++++++++++++ quartz/plugins/emitters/index.ts | 1 + 7 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 quartz/components/pages/404.tsx create mode 100644 quartz/plugins/emitters/404.tsx diff --git a/docs/features/OxHugo compatibility.md b/docs/features/OxHugo compatibility.md index 7801f0c25..b25167f8d 100644 --- a/docs/features/OxHugo compatibility.md +++ b/docs/features/OxHugo compatibility.md @@ -3,7 +3,7 @@ tags: - plugin/transformer --- -[org-roam](https://www.orgroam.com/) is a plain-text(`org`) personal knowledge management system for [emacs](https://en.wikipedia.org/wiki/Emacs). [ox-hugo](https://github.com/kaushalmodi/ox-hugo) is org exporter backend that exports `org-mode` files to [Hugo](https://gohugo.io/) compatible Markdown. +[org-roam](https://www.orgroam.com/) is a plain-text personal knowledge management system for [emacs](https://en.wikipedia.org/wiki/Emacs). [ox-hugo](https://github.com/kaushalmodi/ox-hugo) is org exporter backend that exports `org-mode` files to [Hugo](https://gohugo.io/) compatible Markdown. Because the Markdown generated by ox-hugo is not pure Markdown but Hugo specific, we need to transform it to fit into Quartz. This is done by `Plugin.OxHugoFlavouredMarkdown`. Even though this [[making plugins|plugin]] was written with `ox-hugo` in mind, it should work for any Hugo specific Markdown. diff --git a/docs/features/upcoming features.md b/docs/features/upcoming features.md index fbfdbc947..76adda00e 100644 --- a/docs/features/upcoming features.md +++ b/docs/features/upcoming features.md @@ -4,15 +4,14 @@ draft: true ## high priority backlog +- static dead link detection - block links: https://help.obsidian.md/Linking+notes+and+files/Internal+links#Link+to+a+block+in+a+note - note/header/block transcludes: https://help.obsidian.md/Linking+notes+and+files/Embedding+files -- static dead link detection - docker support ## misc backlog - breadcrumbs component -- filetree component - cursor chat extension - https://giscus.app/ extension - sidenotes? https://github.com/capnfabs/paperesque diff --git a/quartz.config.ts b/quartz.config.ts index 31d5bcfea..f677a18f9 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -69,6 +69,7 @@ const config: QuartzConfig = { }), Plugin.Assets(), Plugin.Static(), + Plugin.NotFoundPage(), ], }, } diff --git a/quartz/components/index.ts b/quartz/components/index.ts index a83f078b0..10a43acb5 100644 --- a/quartz/components/index.ts +++ b/quartz/components/index.ts @@ -1,7 +1,8 @@ -import ArticleTitle from "./ArticleTitle" import Content from "./pages/Content" import TagContent from "./pages/TagContent" import FolderContent from "./pages/FolderContent" +import NotFound from "./pages/404" +import ArticleTitle from "./ArticleTitle" import Darkmode from "./Darkmode" import Head from "./Head" import PageTitle from "./PageTitle" @@ -36,4 +37,5 @@ export { DesktopOnly, MobileOnly, RecentNotes, + NotFound, } diff --git a/quartz/components/pages/404.tsx b/quartz/components/pages/404.tsx new file mode 100644 index 000000000..c276f568d --- /dev/null +++ b/quartz/components/pages/404.tsx @@ -0,0 +1,12 @@ +import { QuartzComponentConstructor } from "../types" + +function NotFound() { + return ( +
      +

      404

      +

      Either this page is private or doesn't exist.

      +
      + ) +} + +export default (() => NotFound) satisfies QuartzComponentConstructor diff --git a/quartz/plugins/emitters/404.tsx b/quartz/plugins/emitters/404.tsx new file mode 100644 index 000000000..785c873da --- /dev/null +++ b/quartz/plugins/emitters/404.tsx @@ -0,0 +1,56 @@ +import { QuartzEmitterPlugin } from "../types" +import { QuartzComponentProps } from "../../components/types" +import BodyConstructor from "../../components/Body" +import { pageResources, renderPage } from "../../components/renderPage" +import { FullPageLayout } from "../../cfg" +import { FilePath, FullSlug } from "../../util/path" +import { sharedPageComponents } from "../../../quartz.layout" +import { NotFound } from "../../components" +import { defaultProcessedContent } from "../vfile" + +export const NotFoundPage: QuartzEmitterPlugin = () => { + const opts: FullPageLayout = { + ...sharedPageComponents, + pageBody: NotFound(), + beforeBody: [], + left: [], + right: [], + } + + const { head: Head, pageBody, footer: Footer } = opts + const Body = BodyConstructor() + + return { + name: "404Page", + getQuartzComponents() { + return [Head, Body, pageBody, Footer] + }, + async emit(ctx, _content, resources, emit): Promise { + const cfg = ctx.cfg.configuration + const slug = "404" as FullSlug + const externalResources = pageResources(slug, resources) + const [tree, vfile] = defaultProcessedContent({ + slug, + text: "Not Found", + description: "Not Found", + frontmatter: { title: "Not Found", tags: [] }, + }) + const componentData: QuartzComponentProps = { + fileData: vfile.data, + externalResources, + cfg, + children: [], + tree, + allFiles: [], + } + + return [ + await emit({ + content: renderPage(slug, componentData, opts, externalResources), + slug, + ext: ".html", + }), + ] + }, + } +} diff --git a/quartz/plugins/emitters/index.ts b/quartz/plugins/emitters/index.ts index da95d4901..99a2c54d5 100644 --- a/quartz/plugins/emitters/index.ts +++ b/quartz/plugins/emitters/index.ts @@ -6,3 +6,4 @@ export { AliasRedirects } from "./aliases" export { Assets } from "./assets" export { Static } from "./static" export { ComponentResources } from "./componentResources" +export { NotFoundPage } from "./404" From 989bee597987bba2aeae4266cb32ac8e899f638c Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 6 Sep 2023 21:08:08 -0700 Subject: [PATCH 060/140] docs: correct field for ignorePatterns --- docs/features/private pages.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/features/private pages.md b/docs/features/private pages.md index 402e52c2c..1fd6acd22 100644 --- a/docs/features/private pages.md +++ b/docs/features/private pages.md @@ -12,7 +12,7 @@ There may be some notes you want to avoid publishing as a website. Quartz suppor If you'd like to only publish a select number of notes, you can instead use `Plugin.ExplicitPublish` which will filter out all notes except for any that have `publish: true` in the frontmatter. -## `ignoreFiles` +## `ignorePatterns` This is a field in `quartz.config.ts` under the main [[configuration]] which allows you to specify a list of patterns to effectively exclude from parsing all together. Any valid [glob](https://github.com/mrmlnc/fast-glob#pattern-syntax) pattern works here. @@ -24,4 +24,4 @@ Common examples include: - `**/private`: exclude any files or folders named `private` at any level of nesting > [!warning] -> Marking something as private via either a plugin or through the `ignoreFiles` pattern will only prevent a page from being included in the final built site. If your GitHub repository is public, also be sure to include an ignore for those in the `.gitignore` of your Quartz. See the `git` [documentation](https://git-scm.com/docs/gitignore#_pattern_format) for more information. +> Marking something as private via either a plugin or through the `ignorePatterns` pattern will only prevent a page from being included in the final built site. If your GitHub repository is public, also be sure to include an ignore for those in the `.gitignore` of your Quartz. See the `git` [documentation](https://git-scm.com/docs/gitignore#_pattern_format) for more information. From ef1ead31dccd05f4275405b843ff47fa28a5116d Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 6 Sep 2023 21:31:01 -0700 Subject: [PATCH 061/140] fix: encodeuri for slugs in rss --- quartz/plugins/emitters/contentIndex.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quartz/plugins/emitters/contentIndex.ts b/quartz/plugins/emitters/contentIndex.ts index 1c7feaea2..1d0af6d7e 100644 --- a/quartz/plugins/emitters/contentIndex.ts +++ b/quartz/plugins/emitters/contentIndex.ts @@ -29,7 +29,7 @@ const defaultOptions: Options = { function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string { const base = cfg.baseUrl ?? "" const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => ` - https://${base}/${slug} + https://${base}/${encodeURIComponent(slug)} ${content.date?.toISOString()} ` const urls = Array.from(idx) @@ -44,8 +44,8 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex): string { const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => ` ${content.title} - ${root}/${slug} - ${root}/${slug} + ${root}/${encodeURIComponent(slug)} + ${root}/${encodeURIComponent(slug)} ${content.description} ${content.date?.toUTCString()} ` From 828aa71fe34aae675a7552957e8a062c82f595f6 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 6 Sep 2023 21:47:59 -0700 Subject: [PATCH 062/140] fix: escape encoding for titles in rss --- quartz/plugins/emitters/contentIndex.ts | 11 ++++++----- quartz/plugins/transformers/description.ts | 10 +--------- quartz/util/escape.ts | 8 ++++++++ 3 files changed, 15 insertions(+), 14 deletions(-) create mode 100644 quartz/util/escape.ts diff --git a/quartz/plugins/emitters/contentIndex.ts b/quartz/plugins/emitters/contentIndex.ts index 1d0af6d7e..f24ae6dc1 100644 --- a/quartz/plugins/emitters/contentIndex.ts +++ b/quartz/plugins/emitters/contentIndex.ts @@ -1,5 +1,6 @@ import { GlobalConfiguration } from "../../cfg" import { getDate } from "../../components/Date" +import { escapeHTML } from "../../util/escape" import { FilePath, FullSlug, SimpleSlug, simplifySlug } from "../../util/path" import { QuartzEmitterPlugin } from "../types" import path from "path" @@ -29,7 +30,7 @@ const defaultOptions: Options = { function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string { const base = cfg.baseUrl ?? "" const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => ` - https://${base}/${encodeURIComponent(slug)} + https://${base}/${encodeURI(slug)} ${content.date?.toISOString()} ` const urls = Array.from(idx) @@ -43,9 +44,9 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex): string { const root = `https://${base}` const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => ` - ${content.title} - ${root}/${encodeURIComponent(slug)} - ${root}/${encodeURIComponent(slug)} + ${escapeHTML(content.title)} + ${root}/${encodeURI(slug)} + ${root}/${encodeURI(slug)} ${content.description} ${content.date?.toUTCString()} ` @@ -56,7 +57,7 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex): string { return ` - ${cfg.pageTitle} + ${escapeHTML(cfg.pageTitle)} ${root} Recent content on ${cfg.pageTitle} Quartz -- quartz.jzhao.xyz diff --git a/quartz/plugins/transformers/description.ts b/quartz/plugins/transformers/description.ts index 08af5c788..884d5b189 100644 --- a/quartz/plugins/transformers/description.ts +++ b/quartz/plugins/transformers/description.ts @@ -1,6 +1,7 @@ import { Root as HTMLRoot } from "hast" import { toString } from "hast-util-to-string" import { QuartzTransformerPlugin } from "../types" +import { escapeHTML } from "../../util/escape" export interface Options { descriptionLength: number @@ -10,15 +11,6 @@ const defaultOptions: Options = { descriptionLength: 150, } -const escapeHTML = (unsafe: string) => { - return unsafe - .replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">") - .replaceAll('"', """) - .replaceAll("'", "'") -} - export const Description: QuartzTransformerPlugin | undefined> = (userOpts) => { const opts = { ...defaultOptions, ...userOpts } return { diff --git a/quartz/util/escape.ts b/quartz/util/escape.ts new file mode 100644 index 000000000..197558c7d --- /dev/null +++ b/quartz/util/escape.ts @@ -0,0 +1,8 @@ +export const escapeHTML = (unsafe: string) => { + return unsafe + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'") +} From 2525bfbab5553f970997ea3c60af180cbef1fdd2 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 6 Sep 2023 22:24:15 -0700 Subject: [PATCH 063/140] fix: links to index not showing in graph (closes #450) --- quartz/build.ts | 1 + quartz/components/scripts/graph.inline.ts | 3 ++- quartz/plugins/transformers/links.ts | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/quartz/build.ts b/quartz/build.ts index 22288acc1..5752caa46 100644 --- a/quartz/build.ts +++ b/quartz/build.ts @@ -142,6 +142,7 @@ async function startServing( const parsedFiles = [...contentMap.values()] const filteredContent = filterContent(ctx, parsedFiles) + // TODO: we can probably traverse the link graph to figure out what's safe to delete here // instead of just deleting everything await rimraf(argv.output) diff --git a/quartz/components/scripts/graph.inline.ts b/quartz/components/scripts/graph.inline.ts index d72b297bf..dc5c99dc1 100644 --- a/quartz/components/scripts/graph.inline.ts +++ b/quartz/components/scripts/graph.inline.ts @@ -47,11 +47,12 @@ async function renderGraph(container: string, fullSlug: FullSlug) { const data = await fetchData const links: LinkData[] = [] + const validLinks = new Set(Object.keys(data).map((slug) => simplifySlug(slug as FullSlug))) for (const [src, details] of Object.entries(data)) { const source = simplifySlug(src as FullSlug) const outgoing = details.links ?? [] for (const dest of outgoing) { - if (dest in data) { + if (validLinks.has(dest)) { links.push({ source, target: dest }) } } diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts index 475a5e92e..02ced158d 100644 --- a/quartz/plugins/transformers/links.ts +++ b/quartz/plugins/transformers/links.ts @@ -5,7 +5,6 @@ import { SimpleSlug, TransformOptions, _stripSlashes, - joinSegments, simplifySlug, splitAnchor, transformLink, From 06df00b18621f08a211bec33f566ecb7ef4ec22e Mon Sep 17 00:00:00 2001 From: Stefano Cecere Date: Thu, 7 Sep 2023 17:13:41 +0200 Subject: [PATCH 064/140] typo (it's draft, not drafts) (#456) --- docs/features/private pages.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/private pages.md b/docs/features/private pages.md index 1fd6acd22..5c3940bc7 100644 --- a/docs/features/private pages.md +++ b/docs/features/private pages.md @@ -8,7 +8,7 @@ There may be some notes you want to avoid publishing as a website. Quartz suppor ## Filter Plugins -[[making plugins#Filters|Filter plugins]] are plugins that filter out content based off of certain criteria. By default, Quartz uses the `Plugin.RemoveDrafts` plugin which filters out any note that has `drafts: true` in the frontmatter. +[[making plugins#Filters|Filter plugins]] are plugins that filter out content based off of certain criteria. By default, Quartz uses the `Plugin.RemoveDrafts` plugin which filters out any note that has `draft: true` in the frontmatter. If you'd like to only publish a select number of notes, you can instead use `Plugin.ExplicitPublish` which will filter out all notes except for any that have `publish: true` in the frontmatter. From 53f1c88738550eb2646cc0a03469dc230839a258 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Fri, 8 Sep 2023 09:29:57 -0700 Subject: [PATCH 065/140] fix: more lenient date parsing for templates --- quartz/plugins/transformers/lastmod.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/quartz/plugins/transformers/lastmod.ts b/quartz/plugins/transformers/lastmod.ts index 507b58522..015c350a5 100644 --- a/quartz/plugins/transformers/lastmod.ts +++ b/quartz/plugins/transformers/lastmod.ts @@ -11,6 +11,11 @@ const defaultOptions: Options = { priority: ["frontmatter", "git", "filesystem"], } +function coerceDate(d: any): Date { + const dt = new Date(d) + return isNaN(dt.getTime()) ? new Date() : dt +} + type MaybeDate = undefined | string | number export const CreatedModifiedDate: QuartzTransformerPlugin | undefined> = ( userOpts, @@ -49,9 +54,9 @@ export const CreatedModifiedDate: QuartzTransformerPlugin | und } file.data.dates = { - created: created ? new Date(created) : new Date(), - modified: modified ? new Date(modified) : new Date(), - published: published ? new Date(published) : new Date(), + created: coerceDate(created), + modified: coerceDate(modified), + published: coerceDate(published), } } }, From a66c239797e3e80e2dc8b7059eee8c51bcf4ca8f Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sun, 10 Sep 2023 23:07:17 -0700 Subject: [PATCH 066/140] ci: print bundleInfo --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 731395d38..8915143c4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -43,5 +43,5 @@ jobs: - name: Test run: npm test - - name: Ensure Quartz builds - run: npx quartz build + - name: Ensure Quartz builds, check bundle info + run: npx quartz build --bundleInfo From 4e23e6724493a8d112c6ff22e14cf4aabd5e9af1 Mon Sep 17 00:00:00 2001 From: Oskar Manhart <52569953+oskardotglobal@users.noreply.github.com> Date: Mon, 11 Sep 2023 08:11:42 +0200 Subject: [PATCH 067/140] feat: plugin for remark-breaks (#467) * feat: plugin for remark-breaks * fix: update package-lock.json * fix: styling Co-authored-by: Jacky Zhao * Update linebreaks.ts * Update index.ts --------- Co-authored-by: Jacky Zhao --- package-lock.json | 28 +++++++++++++++++++++++ package.json | 1 + quartz/plugins/transformers/index.ts | 1 + quartz/plugins/transformers/linebreaks.ts | 11 +++++++++ 4 files changed, 41 insertions(+) create mode 100644 quartz/plugins/transformers/linebreaks.ts diff --git a/package-lock.json b/package-lock.json index 9246cc992..a19d81c11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "rehype-raw": "^6.1.1", "rehype-slug": "^5.1.0", "remark": "^14.0.2", + "remark-breaks": "^3.0.3", "remark-frontmatter": "^4.0.1", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", @@ -3810,6 +3811,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-newline-to-break": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-1.0.0.tgz", + "integrity": "sha512-491LcYv3gbGhhCrLoeALncQmega2xPh+m3gbsIhVsOX4sw85+ShLFPvPyibxc1Swx/6GtzxgVodq+cGa/47ULg==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-find-and-replace": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-phrasing": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", @@ -4903,6 +4917,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-breaks": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-3.0.3.tgz", + "integrity": "sha512-C7VkvcUp1TPUc2eAYzsPdaUh8Xj4FSbQnYA5A9f80diApLZscTDeG7efiWP65W8hV2sEy3JuGVU0i6qr5D8Hug==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-newline-to-break": "^1.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-frontmatter": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-4.0.1.tgz", diff --git a/package.json b/package.json index 6ed52d602..95c57cd8d 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "rehype-raw": "^6.1.1", "rehype-slug": "^5.1.0", "remark": "^14.0.2", + "remark-breaks": "^3.0.3", "remark-frontmatter": "^4.0.1", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", diff --git a/quartz/plugins/transformers/index.ts b/quartz/plugins/transformers/index.ts index d9f2854c0..e340f10e7 100644 --- a/quartz/plugins/transformers/index.ts +++ b/quartz/plugins/transformers/index.ts @@ -8,3 +8,4 @@ export { ObsidianFlavoredMarkdown } from "./ofm" export { OxHugoFlavouredMarkdown } from "./oxhugofm" export { SyntaxHighlighting } from "./syntax" export { TableOfContents } from "./toc" +export { HardLineBreaks } from "./linebreaks" diff --git a/quartz/plugins/transformers/linebreaks.ts b/quartz/plugins/transformers/linebreaks.ts new file mode 100644 index 000000000..a8a066fc1 --- /dev/null +++ b/quartz/plugins/transformers/linebreaks.ts @@ -0,0 +1,11 @@ +import { QuartzTransformerPlugin } from "../types" +import remarkBreaks from "remark-breaks" + +export const HardLineBreaks: QuartzTransformerPlugin = () => { + return { + name: "HardLineBreaks", + markdownPlugins() { + return [remarkBreaks] + }, + } +} From a19df64be8423063c2484ab35300fb0bef324a14 Mon Sep 17 00:00:00 2001 From: hcplantern <38579760+HCPlantern@users.noreply.github.com> Date: Tue, 12 Sep 2023 14:00:21 +0800 Subject: [PATCH 068/140] fix: callout parsing (#469) --- quartz/plugins/transformers/ofm.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index 8c8da67bc..8b95126dd 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -69,6 +69,8 @@ const callouts = { const calloutMapping: Record = { note: "note", abstract: "abstract", + summary: "abstract", + tldr: "abstract", info: "info", todo: "todo", tip: "tip", @@ -96,7 +98,7 @@ const calloutMapping: Record = { function canonicalizeCallout(calloutName: string): keyof typeof callouts { let callout = calloutName.toLowerCase() as keyof typeof calloutMapping - return calloutMapping[callout] ?? calloutName + return calloutMapping[callout] ?? "note" } const capitalize = (s: string): string => { From 71d81bde1d12aa386ec70be31cc86a37a7426bce Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Tue, 12 Sep 2023 19:18:44 -0700 Subject: [PATCH 069/140] feat: rss limit (closes #459) --- quartz/plugins/emitters/contentIndex.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/quartz/plugins/emitters/contentIndex.ts b/quartz/plugins/emitters/contentIndex.ts index f24ae6dc1..bcb1e3074 100644 --- a/quartz/plugins/emitters/contentIndex.ts +++ b/quartz/plugins/emitters/contentIndex.ts @@ -18,12 +18,14 @@ export type ContentDetails = { interface Options { enableSiteMap: boolean enableRSS: boolean + rssLimit?: number includeEmptyFiles: boolean } const defaultOptions: Options = { enableSiteMap: true, enableRSS: true, + rssLimit: 10, includeEmptyFiles: true, } @@ -39,7 +41,7 @@ function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string { return `${urls}` } -function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex): string { +function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: number): string { const base = cfg.baseUrl ?? "" const root = `https://${base}` @@ -53,13 +55,17 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex): string { const items = Array.from(idx) .map(([slug, content]) => createURLEntry(simplifySlug(slug), content)) + .slice(0, limit ?? idx.size) .join("") + return ` ${escapeHTML(cfg.pageTitle)} ${root} - Recent content on ${cfg.pageTitle} + ${!!limit ? `Last ${limit} notes` : "Recent notes"} on ${ + cfg.pageTitle + } Quartz -- quartz.jzhao.xyz ${items} @@ -102,7 +108,7 @@ export const ContentIndex: QuartzEmitterPlugin> = (opts) => { if (opts?.enableRSS) { emitted.push( await emit({ - content: generateRSSFeed(cfg, linkIndex), + content: generateRSSFeed(cfg, linkIndex, opts.rssLimit), slug: "index" as FullSlug, ext: ".xml", }), From 60a3c543398aed8caf44b411a4dc10e8d1e26fcc Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Tue, 12 Sep 2023 21:29:57 -0700 Subject: [PATCH 070/140] fix: 404 page styling for nested pages (closes #458) --- quartz/components/Head.tsx | 8 ++++++-- quartz/components/renderPage.tsx | 9 +++++---- quartz/plugins/emitters/404.tsx | 5 ++++- quartz/plugins/emitters/contentPage.tsx | 4 ++-- quartz/plugins/emitters/folderPage.tsx | 3 ++- quartz/plugins/emitters/tagPage.tsx | 10 ++++++++-- quartz/util/path.ts | 5 ++++- 7 files changed, 31 insertions(+), 13 deletions(-) diff --git a/quartz/components/Head.tsx b/quartz/components/Head.tsx index 67f0c0245..2bf263817 100644 --- a/quartz/components/Head.tsx +++ b/quartz/components/Head.tsx @@ -1,4 +1,4 @@ -import { joinSegments, pathToRoot } from "../util/path" +import { FullSlug, _stripSlashes, joinSegments, pathToRoot } from "../util/path" import { JSResourceToScriptElement } from "../util/resources" import { QuartzComponentConstructor, QuartzComponentProps } from "./types" @@ -7,7 +7,11 @@ export default (() => { const title = fileData.frontmatter?.title ?? "Untitled" const description = fileData.description?.trim() ?? "No description provided" const { css, js } = externalResources - const baseDir = pathToRoot(fileData.slug!) + + const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`) + const path = url.pathname as FullSlug + const baseDir = fileData.slug === "404" ? path : pathToRoot(fileData.slug!) + const iconPath = joinSegments(baseDir, "static/icon.png") const ogImagePath = `https://${cfg.baseUrl}/static/og-image.png` diff --git a/quartz/components/renderPage.tsx b/quartz/components/renderPage.tsx index eb1291f45..25297f289 100644 --- a/quartz/components/renderPage.tsx +++ b/quartz/components/renderPage.tsx @@ -3,7 +3,7 @@ import { QuartzComponent, QuartzComponentProps } from "./types" import HeaderConstructor from "./Header" import BodyConstructor from "./Body" import { JSResourceToScriptElement, StaticResources } from "../util/resources" -import { FullSlug, joinSegments, pathToRoot } from "../util/path" +import { FullSlug, RelativeURL, joinSegments } from "../util/path" interface RenderComponents { head: QuartzComponent @@ -15,9 +15,10 @@ interface RenderComponents { footer: QuartzComponent } -export function pageResources(slug: FullSlug, staticResources: StaticResources): StaticResources { - const baseDir = pathToRoot(slug) - +export function pageResources( + baseDir: FullSlug | RelativeURL, + staticResources: StaticResources, +): StaticResources { const contentIndexPath = joinSegments(baseDir, "static/contentIndex.json") const contentIndexScript = `const fetchData = fetch(\`${contentIndexPath}\`).then(data => data.json())` diff --git a/quartz/plugins/emitters/404.tsx b/quartz/plugins/emitters/404.tsx index 785c873da..cd079a065 100644 --- a/quartz/plugins/emitters/404.tsx +++ b/quartz/plugins/emitters/404.tsx @@ -28,7 +28,10 @@ export const NotFoundPage: QuartzEmitterPlugin = () => { async emit(ctx, _content, resources, emit): Promise { const cfg = ctx.cfg.configuration const slug = "404" as FullSlug - const externalResources = pageResources(slug, resources) + + const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`) + const path = url.pathname as FullSlug + const externalResources = pageResources(path, resources) const [tree, vfile] = defaultProcessedContent({ slug, text: "Not Found", diff --git a/quartz/plugins/emitters/contentPage.tsx b/quartz/plugins/emitters/contentPage.tsx index 0e510db89..4542446b0 100644 --- a/quartz/plugins/emitters/contentPage.tsx +++ b/quartz/plugins/emitters/contentPage.tsx @@ -4,7 +4,7 @@ import HeaderConstructor from "../../components/Header" import BodyConstructor from "../../components/Body" import { pageResources, renderPage } from "../../components/renderPage" import { FullPageLayout } from "../../cfg" -import { FilePath } from "../../util/path" +import { FilePath, pathToRoot } from "../../util/path" import { defaultContentPageLayout, sharedPageComponents } from "../../../quartz.layout" import { Content } from "../../components" @@ -31,7 +31,7 @@ export const ContentPage: QuartzEmitterPlugin> = (userOp const allFiles = content.map((c) => c[1].data) for (const [tree, file] of content) { const slug = file.data.slug! - const externalResources = pageResources(slug, resources) + const externalResources = pageResources(pathToRoot(slug), resources) const componentData: QuartzComponentProps = { fileData: file.data, externalResources, diff --git a/quartz/plugins/emitters/folderPage.tsx b/quartz/plugins/emitters/folderPage.tsx index 8d62f7bb4..8632eceb4 100644 --- a/quartz/plugins/emitters/folderPage.tsx +++ b/quartz/plugins/emitters/folderPage.tsx @@ -12,6 +12,7 @@ import { SimpleSlug, _stripSlashes, joinSegments, + pathToRoot, simplifySlug, } from "../../util/path" import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout" @@ -69,7 +70,7 @@ export const FolderPage: QuartzEmitterPlugin = (userOpts) => { for (const folder of folders) { const slug = joinSegments(folder, "index") as FullSlug - const externalResources = pageResources(slug, resources) + const externalResources = pageResources(pathToRoot(slug), resources) const [tree, file] = folderDescriptions[folder] const componentData: QuartzComponentProps = { fileData: file.data, diff --git a/quartz/plugins/emitters/tagPage.tsx b/quartz/plugins/emitters/tagPage.tsx index 54ad934f6..6afde2fca 100644 --- a/quartz/plugins/emitters/tagPage.tsx +++ b/quartz/plugins/emitters/tagPage.tsx @@ -5,7 +5,13 @@ import BodyConstructor from "../../components/Body" import { pageResources, renderPage } from "../../components/renderPage" import { ProcessedContent, defaultProcessedContent } from "../vfile" import { FullPageLayout } from "../../cfg" -import { FilePath, FullSlug, getAllSegmentPrefixes, joinSegments } from "../../util/path" +import { + FilePath, + FullSlug, + getAllSegmentPrefixes, + joinSegments, + pathToRoot, +} from "../../util/path" import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout" import { TagContent } from "../../components" @@ -62,7 +68,7 @@ export const TagPage: QuartzEmitterPlugin = (userOpts) => { for (const tag of tags) { const slug = joinSegments("tags", tag) as FullSlug - const externalResources = pageResources(slug, resources) + const externalResources = pageResources(pathToRoot(slug), resources) const [tree, file] = tagDescriptions[tag] const componentData: QuartzComponentProps = { fileData: file.data, diff --git a/quartz/util/path.ts b/quartz/util/path.ts index 1557c1bd5..154006374 100644 --- a/quartz/util/path.ts +++ b/quartz/util/path.ts @@ -123,7 +123,10 @@ export function slugTag(tag: string) { } export function joinSegments(...args: string[]): string { - return args.filter((segment) => segment !== "").join("/") + return args + .filter((segment) => segment !== "") + .join("/") + .replace(/\/\/+/g, "/") } export function getAllSegmentPrefixes(tags: string): string[] { From e3b879741b6d32f56e1d1bfd0bac57f0d68c1113 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Tue, 12 Sep 2023 21:44:03 -0700 Subject: [PATCH 071/140] feat: rich html rss (closes #460) --- docs/features/RSS Feed.md | 2 ++ quartz/plugins/emitters/contentIndex.ts | 12 +++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/features/RSS Feed.md b/docs/features/RSS Feed.md index c519f8771..bfeb399c9 100644 --- a/docs/features/RSS Feed.md +++ b/docs/features/RSS Feed.md @@ -3,3 +3,5 @@ Quartz creates an RSS feed for all the content on your site by generating an `in ## Configuration - Remove RSS feed: set the `enableRSS` field of `Plugin.ContentIndex` in `quartz.config.ts` to be `false`. +- Change number of entries: set the `rssLimit` field of `Plugin.ContentIndex` to be the desired value. It defaults to latest 10 items. +- Use rich HTML output in RSS: set `rssFullHtml` field of `Plugin.ContentIndex` to be `true`. diff --git a/quartz/plugins/emitters/contentIndex.ts b/quartz/plugins/emitters/contentIndex.ts index bcb1e3074..102394cec 100644 --- a/quartz/plugins/emitters/contentIndex.ts +++ b/quartz/plugins/emitters/contentIndex.ts @@ -1,8 +1,10 @@ +import { Root } from "hast" import { GlobalConfiguration } from "../../cfg" import { getDate } from "../../components/Date" import { escapeHTML } from "../../util/escape" import { FilePath, FullSlug, SimpleSlug, simplifySlug } from "../../util/path" import { QuartzEmitterPlugin } from "../types" +import { toHtml } from "hast-util-to-html" import path from "path" export type ContentIndex = Map @@ -19,6 +21,7 @@ interface Options { enableSiteMap: boolean enableRSS: boolean rssLimit?: number + rssFullHtml: boolean includeEmptyFiles: boolean } @@ -26,6 +29,7 @@ const defaultOptions: Options = { enableSiteMap: true, enableRSS: true, rssLimit: 10, + rssFullHtml: false, includeEmptyFiles: true, } @@ -49,7 +53,7 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: nu ${escapeHTML(content.title)} ${root}/${encodeURI(slug)} ${root}/${encodeURI(slug)} - ${content.description} + ${content.content} ${content.date?.toUTCString()} ` @@ -80,7 +84,7 @@ export const ContentIndex: QuartzEmitterPlugin> = (opts) => { const cfg = ctx.cfg.configuration const emitted: FilePath[] = [] const linkIndex: ContentIndex = new Map() - for (const [_tree, file] of content) { + for (const [tree, file] of content) { const slug = file.data.slug! const date = getDate(ctx.cfg.configuration, file.data) ?? new Date() if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) { @@ -88,7 +92,9 @@ export const ContentIndex: QuartzEmitterPlugin> = (opts) => { title: file.data.frontmatter?.title!, links: file.data.links ?? [], tags: file.data.frontmatter?.tags ?? [], - content: file.data.text ?? "", + content: opts?.rssFullHtml + ? escapeHTML(toHtml(tree as Root, { allowDangerousHtml: true })) + : file.data.description ?? "", date: date, description: file.data.description ?? "", }) From 6ecdcb5e24f2783e6fa73de69e848f0f319c4fc4 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Tue, 12 Sep 2023 22:55:50 -0700 Subject: [PATCH 072/140] feat: resolve block references in obsidian markdown --- quartz/plugins/transformers/ofm.ts | 91 +++++++++++++++++++++--------- 1 file changed, 64 insertions(+), 27 deletions(-) diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index 8b95126dd..b2f1dba30 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -1,6 +1,7 @@ import { PluggableList } from "unified" import { QuartzTransformerPlugin } from "../types" import { Root, HTML, BlockContent, DefinitionContent, Code, Paragraph } from "mdast" +import { Element, Literal } from 'hast' import { Replace, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace" import { slug as slugAnchor } from "github-slugger" import rehypeRaw from "rehype-raw" @@ -21,6 +22,7 @@ export interface Options { callouts: boolean mermaid: boolean parseTags: boolean + parseBlockReferences: boolean enableInHtmlEmbed: boolean } @@ -31,6 +33,7 @@ const defaultOptions: Options = { callouts: true, mermaid: true, parseTags: true, + parseBlockReferences: true, enableInHtmlEmbed: false, } @@ -121,6 +124,7 @@ const calloutLineRegex = new RegExp(/^> *\[\!\w+\][+-]?.*$/, "gm") // (?:[-_\p{L}])+ -> non-capturing group, non-empty string of (Unicode-aware) alpha-numeric characters, hyphens and/or underscores // (?:\/[-_\p{L}]+)*) -> non-capturing group, matches an arbitrary number of tag strings separated by "/" const tagRegex = new RegExp(/(?:^| )#((?:[-_\p{L}\d])+(?:\/[-_\p{L}\d]+)*)/, "gu") +const blockReferenceRegex = new RegExp(/\^([A-Za-z0-9]+)$/, "g") export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin | undefined> = ( userOpts, @@ -133,29 +137,29 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin } const findAndReplace = opts.enableInHtmlEmbed ? (tree: Root, regex: RegExp, replace?: Replace | null | undefined) => { - if (replace) { - visit(tree, "html", (node: HTML) => { - if (typeof replace === "string") { - node.value = node.value.replace(regex, replace) - } else { - node.value = node.value.replaceAll(regex, (substring: string, ...args) => { - const replaceValue = replace(substring, ...args) - if (typeof replaceValue === "string") { - return replaceValue - } else if (Array.isArray(replaceValue)) { - return replaceValue.map(mdastToHtml).join("") - } else if (typeof replaceValue === "object" && replaceValue !== null) { - return mdastToHtml(replaceValue) - } else { - return substring - } - }) - } - }) - } - - mdastFindReplace(tree, regex, replace) + if (replace) { + visit(tree, "html", (node: HTML) => { + if (typeof replace === "string") { + node.value = node.value.replace(regex, replace) + } else { + node.value = node.value.replaceAll(regex, (substring: string, ...args) => { + const replaceValue = replace(substring, ...args) + if (typeof replaceValue === "string") { + return replaceValue + } else if (Array.isArray(replaceValue)) { + return replaceValue.map(mdastToHtml).join("") + } else if (typeof replaceValue === "object" && replaceValue !== null) { + return mdastToHtml(replaceValue) + } else { + return substring + } + }) + } + }) } + + mdastFindReplace(tree, regex, replace) + } : mdastFindReplace return { @@ -353,9 +357,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin 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, }, @@ -411,11 +414,38 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin } }) } - return plugins }, htmlPlugins() { - return [rehypeRaw] + const plugins = [rehypeRaw] + + if (opts.parseBlockReferences) { + plugins.push(() => { + return (tree, file) => { + file.data.blocks = {} + const validTagTypes = new Set(["blockquote", "p", "li"]) + visit(tree, "element", (node, _index, _parent) => { + if (validTagTypes.has(node.tagName)) { + const last = node.children.at(-1) as Literal + if (last.value && typeof last.value === 'string') { + const matches = last.value.match(blockReferenceRegex) + if (matches && matches.length >= 1) { + last.value = last.value.slice(0, -matches[0].length) + const block = matches[0].slice(1) + node.properties = { + ...node.properties, + id: block + } + file.data.blocks![block] = node + } + } + } + }) + } + }) + } + + return plugins }, externalResources() { const js: JSResource[] = [] @@ -454,3 +484,10 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin }, } } + +declare module "vfile" { + interface DataMap { + blocks: Record + } +} + From 4461748a85b8795651d0c02451368dffff607938 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 13 Sep 2023 09:43:14 -0700 Subject: [PATCH 073/140] fix dont show html in search when rssFullHtml is true (closes #474) --- quartz/plugins/emitters/contentIndex.ts | 8 ++-- quartz/plugins/transformers/ofm.ts | 56 ++++++++++++------------- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/quartz/plugins/emitters/contentIndex.ts b/quartz/plugins/emitters/contentIndex.ts index 102394cec..911173e1b 100644 --- a/quartz/plugins/emitters/contentIndex.ts +++ b/quartz/plugins/emitters/contentIndex.ts @@ -13,6 +13,7 @@ export type ContentDetails = { links: SimpleSlug[] tags: string[] content: string + richContent?: string date?: Date description?: string } @@ -53,7 +54,7 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: nu ${escapeHTML(content.title)} ${root}/${encodeURI(slug)} ${root}/${encodeURI(slug)} - ${content.content} + ${content.richContent ?? content.description} ${content.date?.toUTCString()} ` @@ -92,9 +93,10 @@ export const ContentIndex: QuartzEmitterPlugin> = (opts) => { title: file.data.frontmatter?.title!, links: file.data.links ?? [], tags: file.data.frontmatter?.tags ?? [], - content: opts?.rssFullHtml + content: file.data.text ?? "", + richContent: opts?.rssFullHtml ? escapeHTML(toHtml(tree as Root, { allowDangerousHtml: true })) - : file.data.description ?? "", + : undefined, date: date, description: file.data.description ?? "", }) diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index b2f1dba30..811d659e6 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -1,7 +1,7 @@ import { PluggableList } from "unified" import { QuartzTransformerPlugin } from "../types" import { Root, HTML, BlockContent, DefinitionContent, Code, Paragraph } from "mdast" -import { Element, Literal } from 'hast' +import { Element, Literal } from "hast" import { Replace, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace" import { slug as slugAnchor } from "github-slugger" import rehypeRaw from "rehype-raw" @@ -137,29 +137,29 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin } const findAndReplace = opts.enableInHtmlEmbed ? (tree: Root, regex: RegExp, replace?: Replace | null | undefined) => { - if (replace) { - visit(tree, "html", (node: HTML) => { - if (typeof replace === "string") { - node.value = node.value.replace(regex, replace) - } else { - node.value = node.value.replaceAll(regex, (substring: string, ...args) => { - const replaceValue = replace(substring, ...args) - if (typeof replaceValue === "string") { - return replaceValue - } else if (Array.isArray(replaceValue)) { - return replaceValue.map(mdastToHtml).join("") - } else if (typeof replaceValue === "object" && replaceValue !== null) { - return mdastToHtml(replaceValue) - } else { - return substring - } - }) - } - }) - } + if (replace) { + visit(tree, "html", (node: HTML) => { + if (typeof replace === "string") { + node.value = node.value.replace(regex, replace) + } else { + node.value = node.value.replaceAll(regex, (substring: string, ...args) => { + const replaceValue = replace(substring, ...args) + if (typeof replaceValue === "string") { + return replaceValue + } else if (Array.isArray(replaceValue)) { + return replaceValue.map(mdastToHtml).join("") + } else if (typeof replaceValue === "object" && replaceValue !== null) { + return mdastToHtml(replaceValue) + } else { + return substring + } + }) + } + }) + } - mdastFindReplace(tree, regex, replace) - } + mdastFindReplace(tree, regex, replace) + } : mdastFindReplace return { @@ -357,8 +357,9 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin 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, }, @@ -427,14 +428,14 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin visit(tree, "element", (node, _index, _parent) => { if (validTagTypes.has(node.tagName)) { const last = node.children.at(-1) as Literal - if (last.value && typeof last.value === 'string') { + if (last.value && typeof last.value === "string") { const matches = last.value.match(blockReferenceRegex) if (matches && matches.length >= 1) { last.value = last.value.slice(0, -matches[0].length) const block = matches[0].slice(1) node.properties = { ...node.properties, - id: block + id: block, } file.data.blocks![block] = node } @@ -490,4 +491,3 @@ declare module "vfile" { blocks: Record } } - From cce389c81d262d1d2a2bd8140c879efd68e3c6dd Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 13 Sep 2023 11:28:53 -0700 Subject: [PATCH 074/140] feat: note transclusion (#475) * basic transclude * feat: note transclusion --- docs/features/wikilinks.md | 4 +-- docs/index.md | 2 +- package-lock.json | 4 +-- package.json | 2 +- quartz/components/renderPage.tsx | 36 +++++++++++++++++++ quartz/plugins/transformers/links.ts | 1 + quartz/plugins/transformers/ofm.ts | 52 +++++++++++++++++++++++----- quartz/styles/base.scss | 6 ++++ 8 files changed, 91 insertions(+), 16 deletions(-) diff --git a/docs/features/wikilinks.md b/docs/features/wikilinks.md index 704a0d0cf..50bbb1bb6 100644 --- a/docs/features/wikilinks.md +++ b/docs/features/wikilinks.md @@ -13,6 +13,4 @@ This is enabled as a part of [[Obsidian compatibility]] and can be configured an - `[[Path to file]]`: produces a link to `Path to file.md` (or `Path-to-file.md`) with the text `Path to file` - `[[Path to file | Here's the title override]]`: produces a link to `Path to file.md` with the text `Here's the title override` - `[[Path to file#Anchor]]`: produces a link to the anchor `Anchor` in the file `Path to file.md` - -> [!warning] -> Currently, Quartz does not support block references or note embed syntax. +- `[[Path to file#^block-ref]]`: produces a link to the specific block `block-ref` in the file `Path to file.md` diff --git a/docs/index.md b/docs/index.md index e5b9dfef5..05de2bae9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,7 +30,7 @@ This will guide you through initializing your Quartz with content. Once you've d ## 🔧 Features -- [[Obsidian compatibility]], [[full-text search]], [[graph view]], [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], and [many more](./features) right out of the box +- [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], and [many more](./features) right out of the box - Hot-reload for both configuration and content - Simple JSX layouts and [[creating components|page components]] - [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes diff --git a/package-lock.json b/package-lock.json index a19d81c11..a87907897 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jackyzha0/quartz", - "version": "4.0.10", + "version": "4.0.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jackyzha0/quartz", - "version": "4.0.10", + "version": "4.0.11", "license": "MIT", "dependencies": { "@clack/prompts": "^0.6.3", diff --git a/package.json b/package.json index 95c57cd8d..0a2085cef 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@jackyzha0/quartz", "description": "🌱 publish your digital garden and notes as a website", "private": true, - "version": "4.0.10", + "version": "4.0.11", "type": "module", "author": "jackyzha0 ", "license": "MIT", diff --git a/quartz/components/renderPage.tsx b/quartz/components/renderPage.tsx index 25297f289..451813b5e 100644 --- a/quartz/components/renderPage.tsx +++ b/quartz/components/renderPage.tsx @@ -4,6 +4,8 @@ import HeaderConstructor from "./Header" import BodyConstructor from "./Body" import { JSResourceToScriptElement, StaticResources } from "../util/resources" import { FullSlug, RelativeURL, joinSegments } from "../util/path" +import { visit } from "unist-util-visit" +import { Root, Element } from "hast" interface RenderComponents { head: QuartzComponent @@ -53,6 +55,40 @@ export function renderPage( components: RenderComponents, pageResources: StaticResources, ): string { + // process transcludes in componentData + visit(componentData.tree as Root, "element", (node, _index, _parent) => { + if (node.tagName === "blockquote") { + const classNames = (node.properties?.className ?? []) as string[] + if (classNames.includes("transclude")) { + const inner = node.children[0] as Element + const blockSlug = inner.properties?.["data-slug"] as FullSlug + const blockRef = node.properties!.dataBlock as string + + // TODO: avoid this expensive find operation and construct an index ahead of time + let blockNode = componentData.allFiles.find((f) => f.slug === blockSlug)?.blocks?.[blockRef] + if (blockNode) { + if (blockNode.tagName === "li") { + blockNode = { + type: "element", + tagName: "ul", + children: [blockNode], + } + } + + node.children = [ + blockNode, + { + type: "element", + tagName: "a", + properties: { href: inner.properties?.href, class: ["internal"] }, + children: [{ type: "text", value: `Link to original` }], + }, + ] + } + } + } + }) + const { head: Head, header, diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts index 02ced158d..e050e00ad 100644 --- a/quartz/plugins/transformers/links.ts +++ b/quartz/plugins/transformers/links.ts @@ -72,6 +72,7 @@ export const CrawlLinks: QuartzTransformerPlugin | undefined> = simplifySlug(destCanonical as FullSlug), ) as SimpleSlug outgoing.add(simple) + node.properties["data-slug"] = simple } // rewrite link internals if prettylinks is on diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index 811d659e6..8306f40d8 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -135,6 +135,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin const hast = toHast(ast, { allowDangerousHtml: true })! return toHtml(hast, { allowDangerousHtml: true }) } + const findAndReplace = opts.enableInHtmlEmbed ? (tree: Root, regex: RegExp, replace?: Replace | null | undefined) => { if (replace) { @@ -238,8 +239,16 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin value: ``, } } else if (ext === "") { - // TODO: note embed + const block = anchor.slice(1) + return { + type: "html", + data: { hProperties: { transclude: true } }, + value: `
      Transclude of block ${block}
      `, + } } + // otherwise, fall through to regular link } @@ -422,22 +431,47 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin if (opts.parseBlockReferences) { plugins.push(() => { + const inlineTagTypes = new Set(["p", "li"]) + const blockTagTypes = new Set(["blockquote"]) return (tree, file) => { file.data.blocks = {} - const validTagTypes = new Set(["blockquote", "p", "li"]) - visit(tree, "element", (node, _index, _parent) => { - if (validTagTypes.has(node.tagName)) { + + visit(tree, "element", (node, index, parent) => { + if (blockTagTypes.has(node.tagName)) { + const nextChild = parent?.children.at(index! + 2) as Element + if (nextChild && nextChild.tagName === "p") { + const text = nextChild.children.at(0) as Literal + if (text && text.value && text.type === "text") { + const matches = text.value.match(blockReferenceRegex) + if (matches && matches.length >= 1) { + parent!.children.splice(index! + 2, 1) + const block = matches[0].slice(1) + + if (!Object.keys(file.data.blocks!).includes(block)) { + node.properties = { + ...node.properties, + id: block, + } + file.data.blocks![block] = node + } + } + } + } + } else if (inlineTagTypes.has(node.tagName)) { const last = node.children.at(-1) as Literal - if (last.value && typeof last.value === "string") { + if (last && last.value && typeof last.value === "string") { const matches = last.value.match(blockReferenceRegex) if (matches && matches.length >= 1) { last.value = last.value.slice(0, -matches[0].length) const block = matches[0].slice(1) - node.properties = { - ...node.properties, - id: block, + + if (!Object.keys(file.data.blocks!).includes(block)) { + node.properties = { + ...node.properties, + id: block, + } + file.data.blocks![block] = node } - file.data.blocks![block] = node } } } diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index 34def8783..92c0f84d9 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -470,3 +470,9 @@ ol.overflow { background: linear-gradient(transparent 0px, var(--light)); } } + +.transclude { + ul { + padding-left: 1rem; + } +} From 14cbbdb8a2f69ebc51cd53a82b50206c543778b0 Mon Sep 17 00:00:00 2001 From: Oskar Manhart <52569953+oskardotglobal@users.noreply.github.com> Date: Thu, 14 Sep 2023 05:55:59 +0200 Subject: [PATCH 075/140] feat: display tag in graph view (#466) * feat: tags in graph view * fix: revert changing graph forces * fix: run prettier --- quartz/components/Graph.tsx | 6 ++++ quartz/components/scripts/graph.inline.ts | 40 ++++++++++++++++------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/quartz/components/Graph.tsx b/quartz/components/Graph.tsx index e159aa541..1b8071b93 100644 --- a/quartz/components/Graph.tsx +++ b/quartz/components/Graph.tsx @@ -13,6 +13,8 @@ export interface D3Config { linkDistance: number fontSize: number opacityScale: number + removeTags: string[] + showTags: boolean } interface GraphOptions { @@ -31,6 +33,8 @@ const defaultOptions: GraphOptions = { linkDistance: 30, fontSize: 0.6, opacityScale: 1, + showTags: true, + removeTags: [], }, globalGraph: { drag: true, @@ -42,6 +46,8 @@ const defaultOptions: GraphOptions = { linkDistance: 30, fontSize: 0.6, opacityScale: 1, + showTags: true, + removeTags: [], }, } diff --git a/quartz/components/scripts/graph.inline.ts b/quartz/components/scripts/graph.inline.ts index dc5c99dc1..1aff138f2 100644 --- a/quartz/components/scripts/graph.inline.ts +++ b/quartz/components/scripts/graph.inline.ts @@ -42,20 +42,38 @@ async function renderGraph(container: string, fullSlug: FullSlug) { linkDistance, fontSize, opacityScale, + removeTags, + showTags, } = JSON.parse(graph.dataset["cfg"]!) const data = await fetchData const links: LinkData[] = [] + const tags: SimpleSlug[] = [] + const validLinks = new Set(Object.keys(data).map((slug) => simplifySlug(slug as FullSlug))) + for (const [src, details] of Object.entries(data)) { const source = simplifySlug(src as FullSlug) const outgoing = details.links ?? [] + for (const dest of outgoing) { if (validLinks.has(dest)) { links.push({ source, target: dest }) } } + + if (showTags) { + const localTags = details.tags + .filter((tag) => !removeTags.includes(tag)) + .map((tag) => simplifySlug(("tags/" + tag) as FullSlug)) + + tags.push(...localTags.filter((tag) => !tags.includes(tag))) + + for (const tag of localTags) { + links.push({ source, target: tag }) + } + } } const neighbourhood = new Set() @@ -76,14 +94,18 @@ async function renderGraph(container: string, fullSlug: FullSlug) { } } else { Object.keys(data).forEach((id) => neighbourhood.add(simplifySlug(id as FullSlug))) + if (showTags) tags.forEach((tag) => neighbourhood.add(tag)) } const graphData: { nodes: NodeData[]; links: LinkData[] } = { - nodes: [...neighbourhood].map((url) => ({ - id: url, - text: data[url]?.title ?? url, - tags: data[url]?.tags ?? [], - })), + nodes: [...neighbourhood].map((url) => { + const text = url.startsWith("tags/") ? "#" + url.substring(5) : data[url]?.title ?? url + return { + id: url, + text: text, + tags: data[url]?.tags ?? [], + } + }), links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)), } @@ -127,7 +149,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) { const isCurrent = d.id === slug if (isCurrent) { return "var(--secondary)" - } else if (visited.has(d.id)) { + } else if (visited.has(d.id) || d.id.startsWith("tags/")) { return "var(--tertiary)" } else { return "var(--gray)" @@ -231,11 +253,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) { .attr("dx", 0) .attr("dy", (d) => -nodeRadius(d) + "px") .attr("text-anchor", "middle") - .text( - (d) => - data[d.id]?.title || - (d.id.charAt(0).toUpperCase() + d.id.slice(1, d.id.length - 1)).replace("-", " "), - ) + .text((d) => d.text) .style("opacity", (opacityScale - 1) / 3.75) .style("pointer-events", "none") .style("font-size", fontSize + "em") From 91f9ae2d71d5c28ba7d2182eed5a9f77da1fbe8d Mon Sep 17 00:00:00 2001 From: Ben Schlegel <31989404+benschlegel@users.noreply.github.com> Date: Fri, 15 Sep 2023 18:39:16 +0200 Subject: [PATCH 076/140] feat: implement file explorer component (closes #201) (#452) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add basic explorer structure„ * feat: integrate new component/plugin * feat: add basic explorer structure * feat: add sort to FileNodes * style: improve style for explorer * refactor: remove unused explorer plugin * refactor: clean explorer structure, fix base (toc) * refactor: clean css, respect displayClass * style: add styling to chevron * refactor: clean up debug statements * refactor: remove unused import * fix: clicking folder icon sometimes turns invisible * refactor: clean css * feat(explorer): add config for title * feat: add config for folder click behavior * fix: `no-pointer` not being set for all elements new approach, have one `no-pointer` class, that removes pointer events and one `clickable` class on the svg and button (everything that can normally be clicked). then, find all children with `clickable` and toggle `no-pointer` * fix: bug where nested folders got incorrect height this fixes the bug where nested folders weren't calculating their total height correctly. done by adding class to main container of all children and calculating total * feat: introduce `folderDefaultState` config * feat: store depth for explorer nodes * feat: implement option for collapsed state + bug fixes folderBehavior: "link" still has bad styling, but major bugs with pointers fixed (not clean yet, but working) * fix: default folder icon rotation * fix: hitbox problem with folder links, fix style * fix: redirect url for nested folders * fix: inconsistent behavior with 'collapseFolders' opt * chore: add comments to `ExplorerNode` * feat: save explorer state to local storage (not clean) * feat: rework `getFolders()`, fix localstorage read + write * feat: set folder state from localStorage needs serious refactoring but functional (except folder icon orientation) * fix: folder icon orientation after local storage * feat: add config for `useSavedState` * refactor: clean `explorer.inline.ts` remove unused functions, comments, unused code, add types to EventHandler * refactor: clean explorer merge `isSvg` paths, remove console logs * refactor: add documentation, remove unused funcs * feat: rework folder collapse logic use grids instead of jank scuffed solution with calculating total heights * refactor: remove depth arg from insert * feat: restore collapse functionality to clicks allow folder icon + folder label to collapse folders again * refactor: remove `pointer-event` jank * feat: improve svg viewbox + remove unused props * feat: use css selector to toggle icon rework folder icon to work purely with css instead of JS manipulation * refactor: remove unused cfg * feat: move TOC to right sidebar * refactor: clean css * style: fix overflow + overflow margin * fix: use `resolveRelative` to resolve file paths * fix: `defaultFolderState` config option * refactor: rename import, rename `folderLi` + ul * fix: use `QuartzPluginData` type * docs: add explorer documentation --- docs/features/explorer.md | 41 ++++ quartz.layout.ts | 8 +- quartz/components/Explorer.tsx | 70 +++++++ quartz/components/ExplorerNode.tsx | 196 +++++++++++++++++++ quartz/components/index.ts | 2 + quartz/components/scripts/explorer.inline.ts | 141 +++++++++++++ quartz/components/styles/explorer.scss | 133 +++++++++++++ quartz/styles/base.scss | 4 +- 8 files changed, 591 insertions(+), 4 deletions(-) create mode 100644 docs/features/explorer.md create mode 100644 quartz/components/Explorer.tsx create mode 100644 quartz/components/ExplorerNode.tsx create mode 100644 quartz/components/scripts/explorer.inline.ts create mode 100644 quartz/components/styles/explorer.scss diff --git a/docs/features/explorer.md b/docs/features/explorer.md new file mode 100644 index 000000000..17647de00 --- /dev/null +++ b/docs/features/explorer.md @@ -0,0 +1,41 @@ +--- +title: "Explorer" +tags: + - component +--- + +Quartz features an explorer that allows you to navigate all files and folders on your site. It supports nested folders and has options for customization. + +By default, it will show all folders and files on your page. To display the explorer in a different spot, you can edit the [[layout]]. + +> [!info] +> The explorer uses local storage by default to save the state of your explorer. This is done to ensure a smooth experience when navigating to different pages. +> +> To clear/delete the explorer state from local storage, delete the `fileTree` entry (guide on how to delete a key from local storage in chromium based browsers can be found [here](https://docs.devolutions.net/kb/general-knowledge-base/clear-browser-local-storage/clear-chrome-local-storage/)). You can disable this by passing `useSavedState: false` as an argument. + +## Customization + +Most configuration can be done by passing in options to `Component.Explorer()`. + +For example, here's what the default configuration looks like: + +```typescript title="quartz.layout.ts" +Component.Explorer({ + title: "Explorer", // title of the explorer component + folderClickBehavior: "collapse", // what happens when you click a folder ("link" to navigate to folder page on click or "collapse" to collapse folder on click) + folderDefaultState: "collapsed", // default state of folders ("collapsed" or "open") + useSavedState: true, // wether to use local storage to save "state" (which folders are opened) of explorer +}) +``` + +When passing in your own options, you can omit any or all of these fields if you'd like to keep the default value for that field. + +Want to customize it even more? + +- Removing table of contents: remove `Component.Explorer()` from `quartz.layout.ts` + - (optional): After removing the explorer component, you can move the [[table of contents]] component back to the `left` part of the layout +- Component: + - Wrapper (Outer component, generates file tree, etc): `quartz/components/Explorer.tsx` + - Explorer node (recursive, either a folder or a file): `quartz/components/ExplorerNode.tsx` +- Style: `quartz/components/styles/explorer.scss` +- Script: `quartz/components/scripts/explorer.inline.ts` diff --git a/quartz.layout.ts b/quartz.layout.ts index 482aba6e3..8c1c6c114 100644 --- a/quartz.layout.ts +++ b/quartz.layout.ts @@ -21,9 +21,13 @@ export const defaultContentPageLayout: PageLayout = { Component.MobileOnly(Component.Spacer()), Component.Search(), Component.Darkmode(), - Component.DesktopOnly(Component.TableOfContents()), + Component.DesktopOnly(Component.Explorer()), + ], + right: [ + Component.Graph(), + Component.DesktopOnly(Component.TableOfContents()), + Component.Backlinks(), ], - right: [Component.Graph(), Component.Backlinks()], } // components for pages that display lists of pages (e.g. tags or folders) diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx new file mode 100644 index 000000000..ce69491e9 --- /dev/null +++ b/quartz/components/Explorer.tsx @@ -0,0 +1,70 @@ +import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import explorerStyle from "./styles/explorer.scss" + +// @ts-ignore +import script from "./scripts/explorer.inline" +import { ExplorerNode, FileNode, Options } from "./ExplorerNode" + +// Options interface defined in `ExplorerNode` to avoid circular dependency +const defaultOptions = (): Options => ({ + title: "Explorer", + folderClickBehavior: "collapse", + folderDefaultState: "collapsed", + useSavedState: true, +}) +export default ((userOpts?: Partial) => { + function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) { + // Parse config + const opts: Options = { ...defaultOptions(), ...userOpts } + + // Construct tree from allFiles + const fileTree = new FileNode("") + allFiles.forEach((file) => fileTree.add(file, 1)) + + // Sort tree (folders first, then files (alphabetic)) + fileTree.sort() + + // Get all folders of tree. Initialize with collapsed state + const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed") + + // Stringify to pass json tree as data attribute ([data-tree]) + const jsonTree = JSON.stringify(folders) + + return ( +
      + +
      +
        + +
      +
      +
      + ) + } + Explorer.css = explorerStyle + Explorer.afterDOMLoaded = script + return Explorer +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/ExplorerNode.tsx b/quartz/components/ExplorerNode.tsx new file mode 100644 index 000000000..6718ec9fa --- /dev/null +++ b/quartz/components/ExplorerNode.tsx @@ -0,0 +1,196 @@ +// @ts-ignore +import { QuartzPluginData } from "vfile" +import { resolveRelative } from "../util/path" + +export interface Options { + title: string + folderDefaultState: "collapsed" | "open" + folderClickBehavior: "collapse" | "link" + useSavedState: boolean +} + +type DataWrapper = { + file: QuartzPluginData + path: string[] +} + +export type FolderState = { + path: string + collapsed: boolean +} + +// Structure to add all files into a tree +export class FileNode { + children: FileNode[] + name: string + file: QuartzPluginData | null + depth: number + + constructor(name: string, file?: QuartzPluginData, depth?: number) { + this.children = [] + this.name = name + this.file = file ?? null + this.depth = depth ?? 0 + } + + private insert(file: DataWrapper) { + if (file.path.length === 1) { + this.children.push(new FileNode(file.file.frontmatter!.title, file.file, this.depth + 1)) + } else { + const next = file.path[0] + file.path = file.path.splice(1) + for (const child of this.children) { + if (child.name === next) { + child.insert(file) + return + } + } + + const newChild = new FileNode(next, undefined, this.depth + 1) + newChild.insert(file) + this.children.push(newChild) + } + } + + // Add new file to tree + add(file: QuartzPluginData, splice: number = 0) { + this.insert({ file, path: file.filePath!.split("/").splice(splice) }) + } + + // Print tree structure (for debugging) + print(depth: number = 0) { + let folderChar = "" + if (!this.file) folderChar = "|" + console.log("-".repeat(depth), folderChar, this.name, this.depth) + this.children.forEach((e) => e.print(depth + 1)) + } + + /** + * Get folder representation with state of tree. + * Intended to only be called on root node before changes to the tree are made + * @param collapsed default state of folders (collapsed by default or not) + * @returns array containing folder state for tree + */ + getFolderPaths(collapsed: boolean): FolderState[] { + const folderPaths: FolderState[] = [] + + const traverse = (node: FileNode, currentPath: string) => { + if (!node.file) { + const folderPath = currentPath + (currentPath ? "/" : "") + node.name + if (folderPath !== "") { + folderPaths.push({ path: folderPath, collapsed }) + } + node.children.forEach((child) => traverse(child, folderPath)) + } + } + + traverse(this, "") + + return folderPaths + } + + // Sort order: folders first, then files. Sort folders and files alphabetically + sort() { + this.children = this.children.sort((a, b) => { + if ((!a.file && !b.file) || (a.file && b.file)) { + return a.name.localeCompare(b.name) + } + if (a.file && !b.file) { + return 1 + } else { + return -1 + } + }) + + this.children.forEach((e) => e.sort()) + } +} + +type ExplorerNodeProps = { + node: FileNode + opts: Options + fileData: QuartzPluginData + fullPath?: string +} + +export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodeProps) { + // Get options + const folderBehavior = opts.folderClickBehavior + const isDefaultOpen = opts.folderDefaultState === "open" + + // Calculate current folderPath + let pathOld = fullPath ? fullPath : "" + let folderPath = "" + if (node.name !== "") { + folderPath = `${pathOld}/${node.name}` + } + + return ( +
      + {node.file ? ( + // Single file node +
    • + + {node.file.frontmatter?.title} + +
    • + ) : ( +
      + {node.name !== "" && ( + // Node with entire folder + // Render svg button + folder name, then children + + )} + {/* Recursively render children of folder */} +
      +
        + {node.children.map((childNode, i) => ( + + ))} +
      +
      +
      + )} +
      + ) +} diff --git a/quartz/components/index.ts b/quartz/components/index.ts index 10a43acb5..d7b6a1c5e 100644 --- a/quartz/components/index.ts +++ b/quartz/components/index.ts @@ -9,6 +9,7 @@ import PageTitle from "./PageTitle" import ContentMeta from "./ContentMeta" import Spacer from "./Spacer" import TableOfContents from "./TableOfContents" +import Explorer from "./Explorer" import TagList from "./TagList" import Graph from "./Graph" import Backlinks from "./Backlinks" @@ -29,6 +30,7 @@ export { ContentMeta, Spacer, TableOfContents, + Explorer, TagList, Graph, Backlinks, diff --git a/quartz/components/scripts/explorer.inline.ts b/quartz/components/scripts/explorer.inline.ts new file mode 100644 index 000000000..807397998 --- /dev/null +++ b/quartz/components/scripts/explorer.inline.ts @@ -0,0 +1,141 @@ +import { FolderState } from "../ExplorerNode" + +// Current state of folders +let explorerState: FolderState[] + +function toggleExplorer(this: HTMLElement) { + // Toggle collapsed state of entire explorer + this.classList.toggle("collapsed") + const content = this.nextElementSibling as HTMLElement + content.classList.toggle("collapsed") + content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px" +} + +function toggleFolder(evt: MouseEvent) { + evt.stopPropagation() + + // Element that was clicked + const target = evt.target as HTMLElement + + // Check if target was svg icon or button + const isSvg = target.nodeName === "svg" + + // corresponding
        element relative to clicked button/folder + let childFolderContainer: HTMLElement + + //
      • element of folder (stores folder-path dataset) + let currentFolderParent: HTMLElement + + // Get correct relative container and toggle collapsed class + if (isSvg) { + childFolderContainer = target.parentElement?.nextSibling as HTMLElement + currentFolderParent = target.nextElementSibling as HTMLElement + + childFolderContainer.classList.toggle("open") + } else { + childFolderContainer = target.parentElement?.parentElement?.nextElementSibling as HTMLElement + currentFolderParent = target.parentElement as HTMLElement + + childFolderContainer.classList.toggle("open") + } + if (!childFolderContainer) return + + // Collapse folder container + const isCollapsed = childFolderContainer.classList.contains("open") + setFolderState(childFolderContainer, !isCollapsed) + + // Save folder state to localStorage + const clickFolderPath = currentFolderParent.dataset.folderpath as string + + // Remove leading "/" + const fullFolderPath = clickFolderPath.substring(1) + toggleCollapsedByPath(explorerState, fullFolderPath) + + const stringifiedFileTree = JSON.stringify(explorerState) + localStorage.setItem("fileTree", stringifiedFileTree) +} + +function setupExplorer() { + // Set click handler for collapsing entire explorer + const explorer = document.getElementById("explorer") + + // Get folder state from local storage + const storageTree = localStorage.getItem("fileTree") + + // Convert to bool + const useSavedFolderState = explorer?.dataset.savestate === "true" + + if (explorer) { + // Get config + const collapseBehavior = explorer.dataset.behavior + + // Add click handlers for all folders (click handler on folder "label") + if (collapseBehavior === "collapse") { + Array.prototype.forEach.call( + document.getElementsByClassName("folder-button"), + function (item) { + item.removeEventListener("click", toggleFolder) + item.addEventListener("click", toggleFolder) + }, + ) + } + + // Add click handler to main explorer + explorer.removeEventListener("click", toggleExplorer) + explorer.addEventListener("click", toggleExplorer) + } + + // Set up click handlers for each folder (click handler on folder "icon") + Array.prototype.forEach.call(document.getElementsByClassName("folder-icon"), function (item) { + item.removeEventListener("click", toggleFolder) + item.addEventListener("click", toggleFolder) + }) + + if (storageTree && useSavedFolderState) { + // Get state from localStorage and set folder state + explorerState = JSON.parse(storageTree) + explorerState.map((folderUl) => { + // grab
      • element for matching folder path + const folderLi = document.querySelector( + `[data-folderpath='/${folderUl.path}']`, + ) as HTMLElement + + // Get corresponding content
          tag and set state + const folderUL = folderLi.parentElement?.nextElementSibling as HTMLElement + setFolderState(folderUL, folderUl.collapsed) + }) + } else { + // If tree is not in localStorage or config is disabled, use tree passed from Explorer as dataset + explorerState = JSON.parse(explorer?.dataset.tree as string) + } +} + +window.addEventListener("resize", setupExplorer) +document.addEventListener("nav", () => { + setupExplorer() +}) + +/** + * Toggles the state of a given folder + * @param folderElement
          Element of folder (parent) + * @param collapsed if folder should be set to collapsed or not + */ +function setFolderState(folderElement: HTMLElement, collapsed: boolean) { + if (collapsed) { + folderElement?.classList.remove("open") + } else { + folderElement?.classList.add("open") + } +} + +/** + * Toggles visibility of a folder + * @param array array of FolderState (`fileTree`, either get from local storage or data attribute) + * @param path path to folder (e.g. 'advanced/more/more2') + */ +function toggleCollapsedByPath(array: FolderState[], path: string) { + const entry = array.find((item) => item.path === path) + if (entry) { + entry.collapsed = !entry.collapsed + } +} diff --git a/quartz/components/styles/explorer.scss b/quartz/components/styles/explorer.scss new file mode 100644 index 000000000..4b25a55f9 --- /dev/null +++ b/quartz/components/styles/explorer.scss @@ -0,0 +1,133 @@ +button#explorer { + background-color: transparent; + border: none; + text-align: left; + cursor: pointer; + padding: 0; + color: var(--dark); + display: flex; + align-items: center; + + & h3 { + font-size: 1rem; + display: inline-block; + margin: 0; + } + + & .fold { + margin-left: 0.5rem; + transition: transform 0.3s ease; + opacity: 0.8; + } + + &.collapsed .fold { + transform: rotateZ(-90deg); + } +} + +.folder-outer { + display: grid; + grid-template-rows: 0fr; + transition: grid-template-rows 0.3s ease-in-out; +} + +.folder-outer.open { + grid-template-rows: 1fr; +} + +.folder-outer > ul { + overflow: hidden; +} + +#explorer-content { + list-style: none; + overflow: hidden; + max-height: none; + transition: max-height 0.35s ease; + margin-top: 0.5rem; + + &.collapsed > .overflow::after { + opacity: 0; + } + + & ul { + list-style: none; + margin: 0.08rem 0; + padding: 0; + transition: + max-height 0.35s ease, + transform 0.35s ease, + opacity 0.2s ease; + & div > li > a { + color: var(--dark); + opacity: 0.75; + pointer-events: all; + } + } +} + +svg { + pointer-events: all; + + & > polyline { + pointer-events: none; + } +} + +.folder-container { + flex-direction: row; + display: flex; + align-items: center; + user-select: none; + + & li > a { + // other selector is more specific, needs important + color: var(--secondary) !important; + opacity: 1 !important; + font-size: 1.05rem !important; + } + + & li > a:hover { + // other selector is more specific, needs important + color: var(--tertiary) !important; + } + + & li > button { + color: var(--dark); + background-color: transparent; + border: none; + text-align: left; + cursor: pointer; + padding-left: 0; + padding-right: 0; + display: flex; + align-items: center; + + & h3 { + font-size: 0.95rem; + display: inline-block; + color: var(--secondary); + font-weight: 600; + margin: 0; + line-height: 1.5rem; + font-weight: bold; + pointer-events: none; + } + } +} + +.folder-icon { + margin-right: 5px; + color: var(--secondary); + cursor: pointer; + transition: transform 0.3s ease; + backface-visibility: visible; +} + +div:has(> .folder-outer:not(.open)) > .folder-container > svg { + transform: rotate(-90deg); +} + +.folder-icon:hover { + color: var(--tertiary); +} diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index 92c0f84d9..c6925fbe5 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -446,7 +446,7 @@ video { ul.overflow, ol.overflow { - height: 300px; + max-height: 300; overflow-y: auto; // clearfix @@ -454,7 +454,7 @@ ol.overflow { clear: both; & > li:last-of-type { - margin-bottom: 50px; + margin-bottom: 30px; } &:after { From 5dcb7e83fc3c8383ebbc84aac4553df4ad3ef59a Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Fri, 15 Sep 2023 09:46:06 -0700 Subject: [PATCH 077/140] fix: use git dates by default, @napi/git is fast enough --- quartz.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz.config.ts b/quartz.config.ts index f677a18f9..8674bc62f 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -47,7 +47,7 @@ const config: QuartzConfig = { Plugin.FrontMatter(), Plugin.TableOfContents(), Plugin.CreatedModifiedDate({ - priority: ["frontmatter", "filesystem"], // you can add 'git' here for last modified from Git but this makes the build slower + priority: ["frontmatter", "git", "filesystem"], }), Plugin.SyntaxHighlighting(), Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }), From 9ae6343dd0104d44e6bdf083572f987b70ba50c9 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Fri, 15 Sep 2023 10:33:38 -0700 Subject: [PATCH 078/140] Revert "fix: use git dates by default, @napi/git is fast enough" This reverts commit 5dcb7e83fc3c8383ebbc84aac4553df4ad3ef59a. --- quartz.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz.config.ts b/quartz.config.ts index 8674bc62f..f677a18f9 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -47,7 +47,7 @@ const config: QuartzConfig = { Plugin.FrontMatter(), Plugin.TableOfContents(), Plugin.CreatedModifiedDate({ - priority: ["frontmatter", "git", "filesystem"], + priority: ["frontmatter", "filesystem"], // you can add 'git' here for last modified from Git but this makes the build slower }), Plugin.SyntaxHighlighting(), Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }), From 422ba5c36586c7ebc31141da8bb539b4157aa01a Mon Sep 17 00:00:00 2001 From: Yuto Nagata <38714187+mouse484@users.noreply.github.com> Date: Sat, 16 Sep 2023 11:17:20 +0900 Subject: [PATCH 079/140] fix: umami analytics date attribute (#477) --- quartz/plugins/emitters/componentResources.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts index 96db8aa81..1290a3548 100644 --- a/quartz/plugins/emitters/componentResources.ts +++ b/quartz/plugins/emitters/componentResources.ts @@ -100,7 +100,7 @@ function addGlobalPageResources( componentResources.afterDOMLoaded.push(` const umamiScript = document.createElement("script") umamiScript.src = "https://analytics.umami.is/script.js" - umamiScript["data-website-id"] = "${cfg.analytics.websiteId}" + umamiScript.setAttribute("data-website-id", "${cfg.analytics.websiteId}") umamiScript.async = true document.head.appendChild(umamiScript) From c7d3474ba8cb49ab0f1978216d80b08ec2c8e5d7 Mon Sep 17 00:00:00 2001 From: Ben Schlegel Date: Sat, 16 Sep 2023 12:40:19 +0200 Subject: [PATCH 080/140] feat(explorer): add config to support custom sort fn --- quartz/components/Explorer.tsx | 13 ++++++++++++- quartz/components/ExplorerNode.tsx | 21 ++++++++------------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx index ce69491e9..ee0f96ff3 100644 --- a/quartz/components/Explorer.tsx +++ b/quartz/components/Explorer.tsx @@ -11,6 +11,17 @@ const defaultOptions = (): Options => ({ folderClickBehavior: "collapse", folderDefaultState: "collapsed", useSavedState: true, + // Sort order: folders first, then files. Sort folders and files alphabetically + sortFn: (a, b) => { + if ((!a.file && !b.file) || (a.file && b.file)) { + return a.name.localeCompare(b.name) + } + if (a.file && !b.file) { + return 1 + } else { + return -1 + } + }, }) export default ((userOpts?: Partial) => { function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) { @@ -22,7 +33,7 @@ export default ((userOpts?: Partial) => { allFiles.forEach((file) => fileTree.add(file, 1)) // Sort tree (folders first, then files (alphabetic)) - fileTree.sort() + fileTree.sort(opts.sortFn!) // Get all folders of tree. Initialize with collapsed state const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed") diff --git a/quartz/components/ExplorerNode.tsx b/quartz/components/ExplorerNode.tsx index 6718ec9fa..4d00103d1 100644 --- a/quartz/components/ExplorerNode.tsx +++ b/quartz/components/ExplorerNode.tsx @@ -7,6 +7,7 @@ export interface Options { folderDefaultState: "collapsed" | "open" folderClickBehavior: "collapse" | "link" useSavedState: boolean + sortFn: (a: FileNode, b: FileNode) => number } type DataWrapper = { @@ -90,19 +91,13 @@ export class FileNode { } // Sort order: folders first, then files. Sort folders and files alphabetically - sort() { - this.children = this.children.sort((a, b) => { - if ((!a.file && !b.file) || (a.file && b.file)) { - return a.name.localeCompare(b.name) - } - if (a.file && !b.file) { - return 1 - } else { - return -1 - } - }) - - this.children.forEach((e) => e.sort()) + /** + * Sorts tree according to sort/compare function + * @param sortFn compare function used for `.sort()`, also used recursively for children + */ + sort(sortFn: (a: FileNode, b: FileNode) => number) { + this.children = this.children.sort(sortFn) + this.children.forEach((e) => e.sort(sortFn)) } } From 58aea1cb0791e18cd092d88de5374431eba7f1d3 Mon Sep 17 00:00:00 2001 From: Ben Schlegel Date: Sat, 16 Sep 2023 17:28:58 +0200 Subject: [PATCH 081/140] feat: implement filter function for explorer --- quartz/components/ExplorerNode.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/quartz/components/ExplorerNode.tsx b/quartz/components/ExplorerNode.tsx index 4d00103d1..40e526ac8 100644 --- a/quartz/components/ExplorerNode.tsx +++ b/quartz/components/ExplorerNode.tsx @@ -66,6 +66,21 @@ export class FileNode { this.children.forEach((e) => e.print(depth + 1)) } + filter(filterFn: (node: FileNode) => boolean) { + const filteredNodes: FileNode[] = [] + + const traverse = (node: FileNode) => { + if (filterFn(node)) { + filteredNodes.push(node) + } + node.children.forEach(traverse) + } + + traverse(this) + + this.children = filteredNodes + } + /** * Get folder representation with state of tree. * Intended to only be called on root node before changes to the tree are made From 036a33f70bcabc17469956740847796a5f13b9ab Mon Sep 17 00:00:00 2001 From: Ben Schlegel Date: Sat, 16 Sep 2023 17:47:44 +0200 Subject: [PATCH 082/140] fix: use correct import for `QuartzPluginData` --- quartz/components/ExplorerNode.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/components/ExplorerNode.tsx b/quartz/components/ExplorerNode.tsx index 40e526ac8..d96242543 100644 --- a/quartz/components/ExplorerNode.tsx +++ b/quartz/components/ExplorerNode.tsx @@ -1,5 +1,5 @@ // @ts-ignore -import { QuartzPluginData } from "vfile" +import { QuartzPluginData } from "../plugins/vfile" import { resolveRelative } from "../util/path" export interface Options { From 31d16fbd2c82380af586e458b2c1ff29b90b53ae Mon Sep 17 00:00:00 2001 From: Ben Schlegel Date: Sat, 16 Sep 2023 19:18:59 +0200 Subject: [PATCH 083/140] feat(explorer): integrate filter option --- quartz/components/Explorer.tsx | 5 +++++ quartz/components/ExplorerNode.tsx | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx index ee0f96ff3..efc9f6aa4 100644 --- a/quartz/components/Explorer.tsx +++ b/quartz/components/Explorer.tsx @@ -35,6 +35,11 @@ export default ((userOpts?: Partial) => { // Sort tree (folders first, then files (alphabetic)) fileTree.sort(opts.sortFn!) + // If provided, apply filter function to fileTree + if (opts.filterFn) { + fileTree.filter(opts.filterFn) + } + // Get all folders of tree. Initialize with collapsed state const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed") diff --git a/quartz/components/ExplorerNode.tsx b/quartz/components/ExplorerNode.tsx index d96242543..5cf3f0154 100644 --- a/quartz/components/ExplorerNode.tsx +++ b/quartz/components/ExplorerNode.tsx @@ -8,6 +8,7 @@ export interface Options { folderClickBehavior: "collapse" | "link" useSavedState: boolean sortFn: (a: FileNode, b: FileNode) => number + filterFn?: (node: FileNode) => boolean } type DataWrapper = { @@ -66,6 +67,10 @@ export class FileNode { this.children.forEach((e) => e.print(depth + 1)) } + /** + * Filter FileNode tree. Behaves similar to `Array.prototype.filter()`, but modifies tree in place + * @param filterFn function to filter tree with + */ filter(filterFn: (node: FileNode) => boolean) { const filteredNodes: FileNode[] = [] From 3d8c470c0d298f720614318fb4c14575e72bbd2e Mon Sep 17 00:00:00 2001 From: Ben Schlegel Date: Sat, 16 Sep 2023 19:35:27 +0200 Subject: [PATCH 084/140] feat(explorer): implement `map` fn argument Add a function for mapping over all FileNodes as an option for `Explorer` --- quartz/components/Explorer.tsx | 5 +++++ quartz/components/ExplorerNode.tsx | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx index efc9f6aa4..23c5db261 100644 --- a/quartz/components/Explorer.tsx +++ b/quartz/components/Explorer.tsx @@ -40,6 +40,11 @@ export default ((userOpts?: Partial) => { fileTree.filter(opts.filterFn) } + // If provided, apply map function to fileTree + if (opts.mapFn) { + fileTree.map(opts.mapFn) + } + // Get all folders of tree. Initialize with collapsed state const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed") diff --git a/quartz/components/ExplorerNode.tsx b/quartz/components/ExplorerNode.tsx index 5cf3f0154..b8d8c1401 100644 --- a/quartz/components/ExplorerNode.tsx +++ b/quartz/components/ExplorerNode.tsx @@ -9,6 +9,7 @@ export interface Options { useSavedState: boolean sortFn: (a: FileNode, b: FileNode) => number filterFn?: (node: FileNode) => boolean + mapFn?: (node: FileNode) => void } type DataWrapper = { @@ -86,6 +87,16 @@ export class FileNode { this.children = filteredNodes } + /** + * Filter FileNode tree. Behaves similar to `Array.prototype.map()`, but modifies tree in place + * @param mapFn function to filter tree with + */ + map(mapFn: (node: FileNode) => void) { + mapFn(this) + + this.children.forEach((child) => child.map(mapFn)) + } + /** * Get folder representation with state of tree. * Intended to only be called on root node before changes to the tree are made From fea352849c6972da4b3b8935eb2e86f6cefc76ed Mon Sep 17 00:00:00 2001 From: Ben Schlegel Date: Sat, 16 Sep 2023 19:45:21 +0200 Subject: [PATCH 085/140] fix: create deep copy of file passed into tree --- quartz/components/ExplorerNode.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/components/ExplorerNode.tsx b/quartz/components/ExplorerNode.tsx index b8d8c1401..e1c8b8e32 100644 --- a/quartz/components/ExplorerNode.tsx +++ b/quartz/components/ExplorerNode.tsx @@ -32,7 +32,7 @@ export class FileNode { constructor(name: string, file?: QuartzPluginData, depth?: number) { this.children = [] this.name = name - this.file = file ?? null + this.file = file ? structuredClone(file) : null this.depth = depth ?? 0 } From f7029012dfb73ce04405bfe44e4e4d984818bf5f Mon Sep 17 00:00:00 2001 From: Ben Schlegel Date: Sat, 16 Sep 2023 21:58:38 +0200 Subject: [PATCH 086/140] feat: black magic add config for `order` array, which determines the order in which all passed config functions for explorer will get executed in. functions will now dynamically be called on `fileTree` via array accessor (e.g. fileTree["sort"].call(...)) with corresponding function from options being passed to call) --- quartz/components/Explorer.tsx | 35 ++++++++++++++++++++++-------- quartz/components/ExplorerNode.tsx | 3 +++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx index 23c5db261..346bd7587 100644 --- a/quartz/components/Explorer.tsx +++ b/quartz/components/Explorer.tsx @@ -22,6 +22,7 @@ const defaultOptions = (): Options => ({ return -1 } }, + order: ["filter", "map", "sort"], }) export default ((userOpts?: Partial) => { function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) { @@ -32,17 +33,33 @@ export default ((userOpts?: Partial) => { const fileTree = new FileNode("") allFiles.forEach((file) => fileTree.add(file, 1)) - // Sort tree (folders first, then files (alphabetic)) - fileTree.sort(opts.sortFn!) - - // If provided, apply filter function to fileTree - if (opts.filterFn) { - fileTree.filter(opts.filterFn) + /** + * Keys of this object must match corresponding function name of `FileNode`, + * while values must be the argument that will be passed to the function. + * + * e.g. entry for FileNode.sort: `sort: opts.sortFn` (value is sort function from options) + */ + const functions = { + map: opts.mapFn, + sort: opts.sortFn, + filter: opts.filterFn, } - // If provided, apply map function to fileTree - if (opts.mapFn) { - fileTree.map(opts.mapFn) + // Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied) + if (opts.order) { + // Order is important, use loop with index instead of order.map() + for (let i = 0; i < opts.order.length; i++) { + const functionName = opts.order[i] + if (functions[functionName]) { + // for every entry in order, call matching function in FileNode and pass matching argument + // e.g. i = 0; functionName = "filter" + // converted to: (if opts.filterFn) => fileTree.filter(opts.filterFn) + + // @ts-ignore + // typescript cant statically check these dynamic references, so manually make sure reference is valid and ignore warning + fileTree[functionName].call(fileTree, functions[functionName]) + } + } } // Get all folders of tree. Initialize with collapsed state diff --git a/quartz/components/ExplorerNode.tsx b/quartz/components/ExplorerNode.tsx index e1c8b8e32..b1817444d 100644 --- a/quartz/components/ExplorerNode.tsx +++ b/quartz/components/ExplorerNode.tsx @@ -2,6 +2,8 @@ import { QuartzPluginData } from "../plugins/vfile" import { resolveRelative } from "../util/path" +type OrderEntries = "sort" | "filter" | "map" + export interface Options { title: string folderDefaultState: "collapsed" | "open" @@ -10,6 +12,7 @@ export interface Options { sortFn: (a: FileNode, b: FileNode) => number filterFn?: (node: FileNode) => boolean mapFn?: (node: FileNode) => void + order?: OrderEntries[] } type DataWrapper = { From 9358f73f1c939ce459d7835457527e35e1bdf857 Mon Sep 17 00:00:00 2001 From: Ben Schlegel Date: Sun, 17 Sep 2023 12:41:06 +0200 Subject: [PATCH 087/140] fix: display name for file nodes --- quartz/components/ExplorerNode.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/components/ExplorerNode.tsx b/quartz/components/ExplorerNode.tsx index b1817444d..e2d8871f2 100644 --- a/quartz/components/ExplorerNode.tsx +++ b/quartz/components/ExplorerNode.tsx @@ -160,7 +160,7 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro // Single file node
        • - {node.file.frontmatter?.title} + {node.name}
        • ) : ( From 94a04ab1c9fd099c808f3f4e6633722e0d13ac85 Mon Sep 17 00:00:00 2001 From: Ben Schlegel Date: Sun, 17 Sep 2023 15:51:08 +0200 Subject: [PATCH 088/140] fix(explorer): filter function in `ExplorerNode` --- quartz/components/ExplorerNode.tsx | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/quartz/components/ExplorerNode.tsx b/quartz/components/ExplorerNode.tsx index e2d8871f2..f8b99f015 100644 --- a/quartz/components/ExplorerNode.tsx +++ b/quartz/components/ExplorerNode.tsx @@ -76,18 +76,8 @@ export class FileNode { * @param filterFn function to filter tree with */ filter(filterFn: (node: FileNode) => boolean) { - const filteredNodes: FileNode[] = [] - - const traverse = (node: FileNode) => { - if (filterFn(node)) { - filteredNodes.push(node) - } - node.children.forEach(traverse) - } - - traverse(this) - - this.children = filteredNodes + this.children = this.children.filter(filterFn) + this.children.forEach((child) => child.filter(filterFn)) } /** From 5cc9253c41fda87ba473df7023567ba66ce3c32b Mon Sep 17 00:00:00 2001 From: Ben Schlegel Date: Sun, 17 Sep 2023 16:41:23 +0200 Subject: [PATCH 089/140] docs(explorer): write docs for new features --- docs/features/explorer.md | 198 +++++++++++++++++++++++++++++++++++++- 1 file changed, 195 insertions(+), 3 deletions(-) diff --git a/docs/features/explorer.md b/docs/features/explorer.md index 17647de00..8a9f50657 100644 --- a/docs/features/explorer.md +++ b/docs/features/explorer.md @@ -4,9 +4,9 @@ tags: - component --- -Quartz features an explorer that allows you to navigate all files and folders on your site. It supports nested folders and has options for customization. +Quartz features an explorer that allows you to navigate all files and folders on your site. It supports nested folders and is highly customizable. -By default, it will show all folders and files on your page. To display the explorer in a different spot, you can edit the [[layout]]. +By default, it shows all folders and files on your page. To display the explorer in a different spot, you can edit the [[layout]]. > [!info] > The explorer uses local storage by default to save the state of your explorer. This is done to ensure a smooth experience when navigating to different pages. @@ -25,6 +25,14 @@ Component.Explorer({ folderClickBehavior: "collapse", // what happens when you click a folder ("link" to navigate to folder page on click or "collapse" to collapse folder on click) folderDefaultState: "collapsed", // default state of folders ("collapsed" or "open") useSavedState: true, // wether to use local storage to save "state" (which folders are opened) of explorer + // Sort order: folders first, then files. Sort folders and files alphabetically + sortFn: (a, b) => { + ... // default implementation shown later + }, + filterFn: undefined, + mapFn: undefined, + // what order to apply functions in + order: ["filter", "map", "sort"], }) ``` @@ -33,9 +41,193 @@ When passing in your own options, you can omit any or all of these fields if you Want to customize it even more? - Removing table of contents: remove `Component.Explorer()` from `quartz.layout.ts` - - (optional): After removing the explorer component, you can move the [[table of contents]] component back to the `left` part of the layout + - (optional): After removing the explorer component, you can move the [[table of contents | Table of Contents]] component back to the `left` part of the layout +- Changing `sort`, `filter` and `map` behavior: explained in [[explorer#Advanced customization]] - Component: - Wrapper (Outer component, generates file tree, etc): `quartz/components/Explorer.tsx` - Explorer node (recursive, either a folder or a file): `quartz/components/ExplorerNode.tsx` - Style: `quartz/components/styles/explorer.scss` - Script: `quartz/components/scripts/explorer.inline.ts` + +## Advanced customization + +This component allows you to fully customize all of its behavior. You can pass a custom `sort`, `filter` and `map` function. +All functions you can pass work with the `FileNode` class, which has the following properties: + +```ts title="quartz/components/ExplorerNode.tsx" {2-5} +export class FileNode { + children: FileNode[] // children of current node + name: string // name of node (only useful for folders) + file: QuartzPluginData | null // set if node is a file, see `QuartzPluginData` for more detail + depth: number // depth of current node + + ... // rest of implementation +} +``` + +Every function you can pass is optional. By default, only a `sort` function will be used: + +```ts title="Default sort function" +// Sort order: folders first, then files. Sort folders and files alphabetically +Component.Explorer({ + sortFn: (a, b) => { + if ((!a.file && !b.file) || (a.file && b.file)) { + return a.name.localeCompare(b.name) + } + if (a.file && !b.file) { + return 1 + } else { + return -1 + } + }, +}) +``` + +--- + +You can pass your own functions for `sortFn`, `filterFn` and `mapFn`. All functions will be executed in the order provided by the `order` option (see [[explorer#Customization]]). These functions behave similarly to their `Array.prototype` counterpart, except they modify the entire `FileNode` tree in place instead of returning a new one. + +For more information on how to use `sort`, `filter` and `map`, you can check [Array.prototype.sort()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort), [Array.prototype.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) and [Array.prototype.map()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map). + +Type definitions look like this: + +```ts +sortFn: (a: FileNode, b: FileNode) => number +filterFn: (node: FileNode) => boolean +mapFn: (node: FileNode) => void +``` + +> [!tip] +> You can check if a `FileNode` is a folder or a file like this: +> +> ```ts +> if (node.file) { +> // node is a file +> } else { +> // node is a folder +> } +> ``` + +## Basic examples + +These examples show the basic usage of `sort`, `map` and `filter`. + +### Use `sort` to put files first + +Using this example, the explorer will alphabetically sort everything, but put all **files** above all **folders**. + +```ts title="quartz.layout.ts" +Component.Explorer({ + sortFn: (a, b) => { + if ((!a.file && !b.file) || (a.file && b.file)) { + return a.name.localeCompare(b.name) + } + if (a.file && !b.file) { + return -1 + } else { + return 1 + } + }, +}) +``` + +### Change display names (`map`) + +Using this example, the display names of all `FileNodes` (folders + files) will be converted to full upper case. + +```ts title="quartz.layout.ts" +Component.Explorer({ + mapFn: (node) => { + node.name = node.name.toUpperCase() + }, +}) +``` + +### Remove list of elements (`filter`) + +Using this example, you can remove elements from your explorer by providing a list of folders/files using the `list` array. + +```ts title="quartz.layout.ts" +Component.Explorer({ + filterFn: (node) => { + // list containing names of everything you want to filter out + const list = ["authoring content", "building your", "tags", "hosting"] + + for (let listNodeName of list) { + if (listNodeName.toLowerCase() === node.name.toLowerCase()) { + return false // Found a match, so return false to filter out the node + } + } + return true // No match found, so return true to keep the node + }, +}) +``` + +You can customize this by changing the entries of the `list` array. Simply add all folder or file names you want to remove to the array (case insensitive). + +## Advanced examples + +### Add emoji prefix + +To add emoji prefixes (📁 for folders, 📄 for files), you could use a map function like this: + +```ts title="quartz.layout.ts" +Component.Explorer({ + mapFn: (node) => { + // dont change name of root node + if (node.depth > 0) { + // set emoji for file/folder + if (node.file) { + node.name = "📄 " + node.name + } else { + node.name = "📁 " + node.name + } + } + }, +}}) +``` + +### Putting it all together + +In this example, we're going to customize the explorer by using functions from examples above to [[explorer#Add emoji prefix | add emoji prefixes]], [[explorer#remove-list-of-elements-filter| filter out some folders]] and [[explorer#use-sort-to-put-files-first | sort with files above folders]]. + +```ts title="quartz.layout.ts" +Component.Explorer({ + filterFn: sampleFilterFn, + mapFn: sampleMapFn, + sortFn: sampleSortFn, + order: ["filter", "sort", "map"], +}) +``` + +Notice how we customized the `order` array here. This is done because the default order applies the `sort` function last. While this normally works well, it would cause unintended behavior here, since we changed the first characters of all display names. In our example, `sort` would be applied based off the emoji prefix instead of the first _real_ character. + +To fix this, we just changed around the order and apply the `sort` function before changing the display names in the `map` function. + +> [!tip] +> When writing more complicated functions, the `layout` file can start to look very cramped. +> You can fix this by defining your functions in another file. +> +> ```ts title="functions.ts" +> import { Options } from "./quartz/components/ExplorerNode" +> export const mapFn: Options["mapFn"] = (node) => { +> // implement your function here +> } +> export const filterFn: Options["filterFn"] = (node) => { +> // implement your function here +> } +> export const sortFn: Options["sortFn"] = (a, b) => { +> // implement your function here +> } +> ``` +> +> You can then import them like this: +> +> ```ts title="quartz.layout.ts" +> import { mapFn, filterFn, sortFn } from "./path/to/your/functions" +> Component.Explorer({ +> mapFn: mapFn, +> filterFn: filterFn, +> sortFn: sortFn, +> }) +> ``` From 7ac772fca8bf26c1023f905cdb77e6972a0d4b61 Mon Sep 17 00:00:00 2001 From: Ben Schlegel <31989404+benschlegel@users.noreply.github.com> Date: Sun, 17 Sep 2023 19:29:20 +0200 Subject: [PATCH 090/140] fix: darkmode scroll bars (#480) --- quartz/components/styles/darkmode.scss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/quartz/components/styles/darkmode.scss b/quartz/components/styles/darkmode.scss index 10cbc72a5..348c6f793 100644 --- a/quartz/components/styles/darkmode.scss +++ b/quartz/components/styles/darkmode.scss @@ -21,6 +21,14 @@ } } +:root[saved-theme="dark"] { + color-scheme: dark; +} + +:root[saved-theme="light"] { + color-scheme: light; +} + :root[saved-theme="dark"] .toggle ~ label { & > #dayIcon { opacity: 0; From af41f34bfd4126756e594ce4d6a46d4f4907754b Mon Sep 17 00:00:00 2001 From: Christian Gill Date: Sun, 17 Sep 2023 20:02:00 +0200 Subject: [PATCH 091/140] fix(slug): Handle question mark (#481) --- quartz/util/path.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/util/path.ts b/quartz/util/path.ts index 154006374..173eb2ece 100644 --- a/quartz/util/path.ts +++ b/quartz/util/path.ts @@ -52,7 +52,7 @@ export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug { let slug = withoutFileExt .split("/") - .map((segment) => segment.replace(/\s/g, "-").replace(/%/g, "-percent")) // slugify all segments + .map((segment) => segment.replace(/\s/g, "-").replace(/%/g, "-percent").replace(/\?/g, "-q")) // slugify all segments .join("/") // always use / as sep .replace(/\/$/, "") // remove trailing slash From 6914d4b40caff901ccf3e9d9113c15129a68a80c Mon Sep 17 00:00:00 2001 From: Ben Schlegel Date: Sun, 17 Sep 2023 21:20:09 +0200 Subject: [PATCH 092/140] docs: fix intra page links --- docs/features/explorer.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/features/explorer.md b/docs/features/explorer.md index 8a9f50657..76d04c675 100644 --- a/docs/features/explorer.md +++ b/docs/features/explorer.md @@ -42,7 +42,7 @@ Want to customize it even more? - Removing table of contents: remove `Component.Explorer()` from `quartz.layout.ts` - (optional): After removing the explorer component, you can move the [[table of contents | Table of Contents]] component back to the `left` part of the layout -- Changing `sort`, `filter` and `map` behavior: explained in [[explorer#Advanced customization]] +- Changing `sort`, `filter` and `map` behavior: explained in [[#Advanced customization]] - Component: - Wrapper (Outer component, generates file tree, etc): `quartz/components/Explorer.tsx` - Explorer node (recursive, either a folder or a file): `quartz/components/ExplorerNode.tsx` @@ -85,7 +85,7 @@ Component.Explorer({ --- -You can pass your own functions for `sortFn`, `filterFn` and `mapFn`. All functions will be executed in the order provided by the `order` option (see [[explorer#Customization]]). These functions behave similarly to their `Array.prototype` counterpart, except they modify the entire `FileNode` tree in place instead of returning a new one. +You can pass your own functions for `sortFn`, `filterFn` and `mapFn`. All functions will be executed in the order provided by the `order` option (see [[#Customization]]). These functions behave similarly to their `Array.prototype` counterpart, except they modify the entire `FileNode` tree in place instead of returning a new one. For more information on how to use `sort`, `filter` and `map`, you can check [Array.prototype.sort()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort), [Array.prototype.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) and [Array.prototype.map()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map). @@ -189,7 +189,7 @@ Component.Explorer({ ### Putting it all together -In this example, we're going to customize the explorer by using functions from examples above to [[explorer#Add emoji prefix | add emoji prefixes]], [[explorer#remove-list-of-elements-filter| filter out some folders]] and [[explorer#use-sort-to-put-files-first | sort with files above folders]]. +In this example, we're going to customize the explorer by using functions from examples above to [[#Add emoji prefix | add emoji prefixes]], [[#remove-list-of-elements-filter| filter out some folders]] and [[#use-sort-to-put-files-first | sort with files above folders]]. ```ts title="quartz.layout.ts" Component.Explorer({ From 4afb099bf3ec96e5d795e871ecb19575271c0714 Mon Sep 17 00:00:00 2001 From: Ben Schlegel Date: Sun, 17 Sep 2023 21:32:23 +0200 Subject: [PATCH 093/140] docs: fix examples --- docs/features/explorer.md | 18 ++++++------------ quartz/components/ExplorerNode.tsx | 2 +- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/docs/features/explorer.md b/docs/features/explorer.md index 76d04c675..cb63e403a 100644 --- a/docs/features/explorer.md +++ b/docs/features/explorer.md @@ -145,25 +145,19 @@ Component.Explorer({ ### Remove list of elements (`filter`) -Using this example, you can remove elements from your explorer by providing a list of folders/files using the `list` array. +Using this example, you can remove elements from your explorer by providing an array of folders/files using the `omit` set. ```ts title="quartz.layout.ts" Component.Explorer({ filterFn: (node) => { - // list containing names of everything you want to filter out - const list = ["authoring content", "building your", "tags", "hosting"] - - for (let listNodeName of list) { - if (listNodeName.toLowerCase() === node.name.toLowerCase()) { - return false // Found a match, so return false to filter out the node - } - } - return true // No match found, so return true to keep the node + // set containing names of everything you want to filter out + const omit = new Set(["authoring content", "tags", "hosting"]) + return omit.has(node.name.toLowerCase()) }, }) ``` -You can customize this by changing the entries of the `list` array. Simply add all folder or file names you want to remove to the array (case insensitive). +You can customize this by changing the entries of the `omit` set. Simply add all folder or file names you want to remove. ## Advanced examples @@ -224,7 +218,7 @@ To fix this, we just changed around the order and apply the `sort` function befo > You can then import them like this: > > ```ts title="quartz.layout.ts" -> import { mapFn, filterFn, sortFn } from "./path/to/your/functions" +> import { mapFn, filterFn, sortFn } from "./functions.ts" > Component.Explorer({ > mapFn: mapFn, > filterFn: filterFn, diff --git a/quartz/components/ExplorerNode.tsx b/quartz/components/ExplorerNode.tsx index f8b99f015..fd0c0823d 100644 --- a/quartz/components/ExplorerNode.tsx +++ b/quartz/components/ExplorerNode.tsx @@ -82,7 +82,7 @@ export class FileNode { /** * Filter FileNode tree. Behaves similar to `Array.prototype.map()`, but modifies tree in place - * @param mapFn function to filter tree with + * @param mapFn function to use for mapping over tree */ map(mapFn: (node: FileNode) => void) { mapFn(this) From 6a2e0b3ad3a928247a03a76817d239e61cce0fe0 Mon Sep 17 00:00:00 2001 From: Ben Schlegel <31989404+benschlegel@users.noreply.github.com> Date: Sun, 17 Sep 2023 22:04:44 +0200 Subject: [PATCH 094/140] fix: bad visibility for last explorer item (#478) * fix: bad visibility for last explorer item * feat(explorer): add pseudo element for observer --- quartz/components/Explorer.tsx | 3 ++- quartz/components/scripts/explorer.inline.ts | 25 ++++++++++++++++++-- quartz/components/styles/explorer.scss | 9 +++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx index 346bd7587..0bdb5a650 100644 --- a/quartz/components/Explorer.tsx +++ b/quartz/components/Explorer.tsx @@ -95,8 +95,9 @@ export default ((userOpts?: Partial) => {
          -
            +
              +
          diff --git a/quartz/components/scripts/explorer.inline.ts b/quartz/components/scripts/explorer.inline.ts index 807397998..2b7df7d35 100644 --- a/quartz/components/scripts/explorer.inline.ts +++ b/quartz/components/scripts/explorer.inline.ts @@ -3,6 +3,18 @@ import { FolderState } from "../ExplorerNode" // Current state of folders let explorerState: FolderState[] +const observer = new IntersectionObserver((entries) => { + // If last element is observed, remove gradient of "overflow" class so element is visible + const explorer = document.getElementById("explorer-ul") + for (const entry of entries) { + if (entry.isIntersecting) { + explorer?.classList.add("no-background") + } else { + explorer?.classList.remove("no-background") + } + } +}) + function toggleExplorer(this: HTMLElement) { // Toggle collapsed state of entire explorer this.classList.toggle("collapsed") @@ -101,8 +113,10 @@ function setupExplorer() { ) as HTMLElement // Get corresponding content
            tag and set state - const folderUL = folderLi.parentElement?.nextElementSibling as HTMLElement - setFolderState(folderUL, folderUl.collapsed) + const folderUL = folderLi.parentElement?.nextElementSibling + if (folderUL) { + setFolderState(folderUL as HTMLElement, folderUl.collapsed) + } }) } else { // If tree is not in localStorage or config is disabled, use tree passed from Explorer as dataset @@ -113,6 +127,13 @@ function setupExplorer() { window.addEventListener("resize", setupExplorer) document.addEventListener("nav", () => { setupExplorer() + + const explorerContent = document.getElementById("explorer-ul") + // select pseudo element at end of list + const lastItem = document.getElementById("explorer-end") + + observer.disconnect() + observer.observe(lastItem as Element) }) /** diff --git a/quartz/components/styles/explorer.scss b/quartz/components/styles/explorer.scss index 4b25a55f9..776a5ae6e 100644 --- a/quartz/components/styles/explorer.scss +++ b/quartz/components/styles/explorer.scss @@ -131,3 +131,12 @@ div:has(> .folder-outer:not(.open)) > .folder-container > svg { .folder-icon:hover { color: var(--tertiary); } + +.no-background::after { + background: none !important; +} + +#explorer-end { + // needs height so IntersectionObserver gets triggered + height: 1px; +} From 0d3cf2922618774fc397dca8cb92fcf76fb0db02 Mon Sep 17 00:00:00 2001 From: Ben Schlegel <31989404+benschlegel@users.noreply.github.com> Date: Mon, 18 Sep 2023 23:32:00 +0200 Subject: [PATCH 095/140] docs: fix explorer example (#483) --- docs/features/explorer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/explorer.md b/docs/features/explorer.md index cb63e403a..6f941b871 100644 --- a/docs/features/explorer.md +++ b/docs/features/explorer.md @@ -152,7 +152,7 @@ Component.Explorer({ filterFn: (node) => { // set containing names of everything you want to filter out const omit = new Set(["authoring content", "tags", "hosting"]) - return omit.has(node.name.toLowerCase()) + return !omit.has(node.name.toLowerCase()) }, }) ``` From cc31a40b0cb53cba7f51187cb6d68076c3f54c0f Mon Sep 17 00:00:00 2001 From: David Fischer Date: Tue, 19 Sep 2023 18:25:51 +0200 Subject: [PATCH 096/140] feat: support changes in system theme (#484) * feat: support changes in system theme * fix: run prettier * fix: add content/.gitkeep --- quartz/components/scripts/darkmode.inline.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/quartz/components/scripts/darkmode.inline.ts b/quartz/components/scripts/darkmode.inline.ts index e16f4f845..c42a367c9 100644 --- a/quartz/components/scripts/darkmode.inline.ts +++ b/quartz/components/scripts/darkmode.inline.ts @@ -20,4 +20,13 @@ document.addEventListener("nav", () => { if (currentTheme === "dark") { toggleSwitch.checked = true } + + // Listen for changes in prefers-color-scheme + const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)") + colorSchemeMediaQuery.addEventListener("change", (e) => { + const newTheme = e.matches ? "dark" : "light" + document.documentElement.setAttribute("saved-theme", newTheme) + localStorage.setItem("theme", newTheme) + toggleSwitch.checked = e.matches + }) }) From 1bf7e3d8b3966590ebfa3418d6fb2ce6a520c846 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Tue, 19 Sep 2023 10:22:39 -0700 Subject: [PATCH 097/140] fix(nit): make defaultOptions on explorer not a function --- quartz/components/Explorer.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx index 0bdb5a650..8597075d2 100644 --- a/quartz/components/Explorer.tsx +++ b/quartz/components/Explorer.tsx @@ -6,7 +6,7 @@ import script from "./scripts/explorer.inline" import { ExplorerNode, FileNode, Options } from "./ExplorerNode" // Options interface defined in `ExplorerNode` to avoid circular dependency -const defaultOptions = (): Options => ({ +const defaultOptions = { title: "Explorer", folderClickBehavior: "collapse", folderDefaultState: "collapsed", @@ -23,11 +23,12 @@ const defaultOptions = (): Options => ({ } }, order: ["filter", "map", "sort"], -}) +} satisfies Options + export default ((userOpts?: Partial) => { function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) { // Parse config - const opts: Options = { ...defaultOptions(), ...userOpts } + const opts: Options = { ...defaultOptions, ...userOpts } // Construct tree from allFiles const fileTree = new FileNode("") From 27a6087dd5a25dd5031b86b9917adde6ef4b211a Mon Sep 17 00:00:00 2001 From: rwutscher Date: Tue, 19 Sep 2023 21:26:30 +0200 Subject: [PATCH 098/140] fix: tag regex no longer includes purely numerical 'tags' (#485) * fix: tag regex no longer includes purely numerical 'tags' * fix: formatting * fix: use guard in findAndReplace() instead of expanding the regex --- quartz/plugins/transformers/ofm.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index 8306f40d8..4d55edad8 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -400,6 +400,10 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin return (tree: Root, file) => { const base = pathToRoot(file.data.slug!) findAndReplace(tree, tagRegex, (_value: string, tag: string) => { + // Check if the tag only includes numbers + if (/^\d+$/.test(tag)) { + return false + } tag = slugTag(tag) if (file.data.frontmatter && !file.data.frontmatter.tags.includes(tag)) { file.data.frontmatter.tags.push(tag) From d6301fae90d9f922618bf0f413e273156731eef7 Mon Sep 17 00:00:00 2001 From: Adam Brangenberg Date: Wed, 20 Sep 2023 20:38:13 +0200 Subject: [PATCH 099/140] feat: Making Quartz available offline by making it a PWA (#465) * Adding PWA and chaching for offline aviability * renamed workbox config to fit Quartz' scheme * Documenting new configuration * Added missig umami documentation * Fixed formatting so the build passes, thank you prettier :) * specified caching strategies to improve performance * formatting... * fixing "404 manifest.json not found" on subdirectories by adding a / to manifestpath * turning it into a plugin * Removed Workbox-cli and updated @types/node * Added Serviceworkercode to offline.ts * formatting * Removing workbox from docs * applied suggestions * Removed path.join for sw path Co-authored-by: Jacky Zhao * Removed path.join for manifest path Co-authored-by: Jacky Zhao * Removing path module import * Added absolute path to manifests start_url and manifest "import" using baseUrl * Adding protocol to baseurl Co-authored-by: Jacky Zhao * Adding protocol to start_url too then * formatting... * Adding fallback page * Documenting offline plugin * formatting... * merge suggestion Co-authored-by: Jacky Zhao * merge suggestion Co-authored-by: Jacky Zhao * merge suggestion Co-authored-by: Jacky Zhao * merge suggestion Co-authored-by: Jacky Zhao * merge suggestion Co-authored-by: Jacky Zhao * merge suggestion Co-authored-by: Jacky Zhao * merge suggestion Co-authored-by: Jacky Zhao * merge suggestion Co-authored-by: Jacky Zhao * merge suggestion Co-authored-by: Jacky Zhao * merge suggestion Co-authored-by: Jacky Zhao * merge suggestion Co-authored-by: Jacky Zhao * merge suggestion Co-authored-by: Jacky Zhao * formatting... * Fixing manifest path, all these nits hiding the actual issues .-. * Offline fallback page through plugins, most things taken from 404 Plugin * adding Offline Plugin to config * formatting... * Turned offline off as default and removed offline.md --------- Co-authored-by: Jacky Zhao --- docs/configuration.md | 2 + docs/features/offline access.md | 31 ++++++ docs/index.md | 2 +- package-lock.json | 9 +- package.json | 2 +- quartz.config.ts | 1 + quartz/cfg.ts | 1 + quartz/components/Head.tsx | 4 + .../components/pages/OfflineFallbackPage.tsx | 12 +++ quartz/plugins/emitters/componentResources.ts | 5 + quartz/plugins/emitters/index.ts | 1 + quartz/plugins/emitters/offline.ts | 97 +++++++++++++++++++ quartz/static/icon.svg | 74 ++++++++++++++ 13 files changed, 235 insertions(+), 6 deletions(-) create mode 100644 docs/features/offline access.md create mode 100644 quartz/components/pages/OfflineFallbackPage.tsx create mode 100644 quartz/plugins/emitters/offline.ts create mode 100644 quartz/static/icon.svg diff --git a/docs/configuration.md b/docs/configuration.md index 047f6ca6b..35e0b9d95 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -21,10 +21,12 @@ const config: QuartzConfig = { This part of the configuration concerns anything that can affect the whole site. The following is a list breaking down all the things you can configure: - `pageTitle`: title of the site. This is also used when generating the [[RSS Feed]] for your site. +- `description`: description of the site. This will be used when someone installs your site as an App. - `enableSPA`: whether to enable [[SPA Routing]] on your site. - `enablePopovers`: whether to enable [[popover previews]] on your site. - `analytics`: what to use for analytics on your site. Values can be - `null`: don't use analytics; + - `{ provider: "umami", websiteId: }`: easy, privacy-friendly, open source, GDPR Compliant analytics; - `{ provider: 'plausible' }`: use [Plausible](https://plausible.io/), a privacy-friendly alternative to Google Analytics; or - `{ provider: 'google', tagId: }`: use Google Analytics - `baseUrl`: this is used for sitemaps and RSS feeds that require an absolute URL to know where the canonical 'home' of your site lives. This is normally the deployed URL of your site (e.g. `quartz.jzhao.xyz` for this site). Do not include the protocol (i.e. `https://`) or any leading or trailing slashes. diff --git a/docs/features/offline access.md b/docs/features/offline access.md new file mode 100644 index 000000000..dcffdcd26 --- /dev/null +++ b/docs/features/offline access.md @@ -0,0 +1,31 @@ +--- +title: "Offline Access (PWA)" +tags: + - plugin/emitter +--- + +This plugin allows your website to be accessible offline and be installed as an app. You can use it by adding `Plugin.Offline(),` to the `emitters` in `quartz.config.ts` + +## Offline Capability + +Whenever you visit a page it gets cached for offline use. Depending on the kind of content, the process for caching is diffent: + +- **Pages** (HTML, your converted Markdown files): Quartz first tries to get them over the Network. If that fails, your browser attempts to fetch it from the cache. +- **Static Resources** (Fonts, CSS Styling, JavaScript): Quartz uses cached resources by default and updates the cache over the network in the background. +- **Images**: Images are saved once and then served from cache. Quartz uses a limited cache of 60 images and images remain in the cache for 30 days + +You can edit the fallback page by changing the `offline.md` file in the root of your `content` directory + +## Progressive Web App (PWA) + +Progressive Web Apps can have [many properties](https://developer.mozilla.org/en-US/docs/Web/Manifest). We're only going to mention the ones Quartz supports by default, however you can edit the offline plugins file to add more in case required. + +- **icons**: the `icon.svg` file in the `quartz/static` directory is used for all the icons. This makes it easier to scale the image since you don't need to provide an png for every size +- **name**, **short_name**: Uses the `pageTitle` configured in `quartz.config.ts` +- **description**: Uses the `description` configured in `quartz.config.ts` +- **background_color**, **theme_color**: Uses the `lightMode.light` color configured in `quartz.config.ts`. +- **start_url**: Uses the `baseUrl` configured in `quartz.config.ts` + +### Default values + +- **display**: this is set to `minimal-ui` diff --git a/docs/index.md b/docs/index.md index 05de2bae9..570d5b364 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,7 +30,7 @@ This will guide you through initializing your Quartz with content. Once you've d ## 🔧 Features -- [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], and [many more](./features) right out of the box +- [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], [[offline access]] and [many more](./features) right out of the box - Hot-reload for both configuration and content - Simple JSX layouts and [[creating components|page components]] - [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes diff --git a/package-lock.json b/package-lock.json index a87907897..8ff94245d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,7 +73,7 @@ "@types/flexsearch": "^0.7.3", "@types/hast": "^2.3.4", "@types/js-yaml": "^4.0.5", - "@types/node": "^20.1.2", + "@types/node": "^20.6.2", "@types/pretty-time": "^1.1.2", "@types/source-map-support": "^0.5.6", "@types/workerpool": "^6.4.0", @@ -113,6 +113,7 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -1463,9 +1464,9 @@ } }, "node_modules/@types/node": { - "version": "20.3.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.3.tgz", - "integrity": "sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==", + "version": "20.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz", + "integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==", "dev": true }, "node_modules/@types/parse5": { diff --git a/package.json b/package.json index 0a2085cef..e514edfbd 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "@types/flexsearch": "^0.7.3", "@types/hast": "^2.3.4", "@types/js-yaml": "^4.0.5", - "@types/node": "^20.1.2", + "@types/node": "^20.6.2", "@types/pretty-time": "^1.1.2", "@types/source-map-support": "^0.5.6", "@types/workerpool": "^6.4.0", diff --git a/quartz.config.ts b/quartz.config.ts index f677a18f9..5a1f643aa 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -4,6 +4,7 @@ import * as Plugin from "./quartz/plugins" const config: QuartzConfig = { configuration: { pageTitle: "🪴 Quartz 4.0", + description: "Quartz Documentation Page and Demo", enableSPA: true, enablePopovers: true, analytics: { diff --git a/quartz/cfg.ts b/quartz/cfg.ts index 8371b5e2b..73e959fb7 100644 --- a/quartz/cfg.ts +++ b/quartz/cfg.ts @@ -19,6 +19,7 @@ export type Analytics = export interface GlobalConfiguration { pageTitle: string + description: string /** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */ enableSPA: boolean /** Whether to display Wikipedia-style popovers when hovering over links */ diff --git a/quartz/components/Head.tsx b/quartz/components/Head.tsx index 2bf263817..972f7497e 100644 --- a/quartz/components/Head.tsx +++ b/quartz/components/Head.tsx @@ -14,6 +14,8 @@ export default (() => { const iconPath = joinSegments(baseDir, "static/icon.png") const ogImagePath = `https://${cfg.baseUrl}/static/og-image.png` + const manifest = + cfg.baseUrl == undefined ? "/manifest.json" : `https://${cfg.baseUrl}/manifest.json` return ( @@ -25,7 +27,9 @@ export default (() => { {cfg.baseUrl && } + + diff --git a/quartz/components/pages/OfflineFallbackPage.tsx b/quartz/components/pages/OfflineFallbackPage.tsx new file mode 100644 index 000000000..14d4f5e9e --- /dev/null +++ b/quartz/components/pages/OfflineFallbackPage.tsx @@ -0,0 +1,12 @@ +import { QuartzComponentConstructor } from "../types" + +function OfflineFallbackPage() { + return ( +
            +

            Offline

            +

            This page isn't offline available yet.

            +
            + ) +} + +export default (() => OfflineFallbackPage) satisfies QuartzComponentConstructor diff --git a/quartz/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts index 1290a3548..a82e7c12a 100644 --- a/quartz/plugins/emitters/componentResources.ts +++ b/quartz/plugins/emitters/componentResources.ts @@ -116,6 +116,11 @@ function addGlobalPageResources( document.dispatchEvent(event)`) } + componentResources.afterDOMLoaded.push(` + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/sw.js'); + }`) + let wsUrl = `ws://localhost:${ctx.argv.wsPort}` if (ctx.argv.remoteDevHost) { diff --git a/quartz/plugins/emitters/index.ts b/quartz/plugins/emitters/index.ts index 99a2c54d5..6de824d5f 100644 --- a/quartz/plugins/emitters/index.ts +++ b/quartz/plugins/emitters/index.ts @@ -7,3 +7,4 @@ export { Assets } from "./assets" export { Static } from "./static" export { ComponentResources } from "./componentResources" export { NotFoundPage } from "./404" +export { Offline } from "./offline" diff --git a/quartz/plugins/emitters/offline.ts b/quartz/plugins/emitters/offline.ts new file mode 100644 index 000000000..e3c654b69 --- /dev/null +++ b/quartz/plugins/emitters/offline.ts @@ -0,0 +1,97 @@ +import { QuartzEmitterPlugin } from "../types" +import { FilePath, FullSlug } from "../../util/path" +import { FullPageLayout } from "../../cfg" +import { sharedPageComponents } from "../../../quartz.layout" +import OfflineFallbackPage from "../../components/pages/OfflineFallbackPage" +import BodyConstructor from "../../components/Body" +import { pageResources, renderPage } from "../../components/renderPage" +import { defaultProcessedContent } from "../vfile" +import { QuartzComponentProps } from "../../components/types" + +export const Offline: QuartzEmitterPlugin = () => { + const opts: FullPageLayout = { + ...sharedPageComponents, + pageBody: OfflineFallbackPage(), + beforeBody: [], + left: [], + right: [], + } + + const { head: Head, pageBody, footer: Footer } = opts + const Body = BodyConstructor() + + return { + name: "OfflineSupport", + getQuartzComponents() { + return [Head, Body, pageBody, Footer] + }, + async emit({ cfg }, _content, resources, emit): Promise { + const manifest = { + short_name: cfg.configuration.pageTitle, + name: cfg.configuration.pageTitle, + description: cfg.configuration.description, + background_color: cfg.configuration.theme.colors.lightMode.light, + theme_color: cfg.configuration.theme.colors.lightMode.light, + display: "minimal-ui", + icons: [ + { + src: "static/icon.svg", + sizes: "any", + purpose: "maskable", + }, + { + src: "static/icon.svg", + sizes: "any", + purpose: "any", + }, + ], + start_url: + cfg.configuration.baseUrl == undefined ? "/" : `https://${cfg.configuration.baseUrl}/`, + } + + const serviceWorker = + "importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js');" + + "const {pageCache, imageCache, staticResourceCache, googleFontsCache, offlineFallback} = workbox.recipes;" + + "pageCache(); googleFontsCache(); staticResourceCache(); imageCache(); offlineFallback();" + + const slug = "offline" as FullSlug + + const url = new URL(`https://${cfg.configuration.baseUrl ?? "example.com"}`) + const path = url.pathname as FullSlug + const externalResources = pageResources(path, resources) + const [tree, vfile] = defaultProcessedContent({ + slug, + text: "Offline", + description: "This page isn't offline available yet.", + frontmatter: { title: "Offline", tags: [] }, + }) + + const componentData: QuartzComponentProps = { + fileData: vfile.data, + externalResources, + cfg: cfg.configuration, + children: [], + tree, + allFiles: [], + } + + return Promise.all([ + emit({ + content: JSON.stringify(manifest), + slug: "manifest" as FullSlug, + ext: ".json", + }), + emit({ + content: serviceWorker, + slug: "sw" as FullSlug, + ext: ".js", + }), + emit({ + content: renderPage(slug, componentData, opts, externalResources), + slug, + ext: ".html", + }), + ]) + }, + } +} diff --git a/quartz/static/icon.svg b/quartz/static/icon.svg new file mode 100644 index 000000000..c6ecfa2db --- /dev/null +++ b/quartz/static/icon.svg @@ -0,0 +1,74 @@ + + + + + + + + From 52a172d1a4911080444ff797183e29ba8175741e Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 20 Sep 2023 11:40:36 -0700 Subject: [PATCH 100/140] docs: wording changes for offline support --- docs/features/offline access.md | 2 +- quartz/components/Explorer.tsx | 1 + quartz/components/pages/OfflineFallbackPage.tsx | 2 +- quartz/plugins/emitters/offline.ts | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/features/offline access.md b/docs/features/offline access.md index dcffdcd26..885bbd501 100644 --- a/docs/features/offline access.md +++ b/docs/features/offline access.md @@ -4,7 +4,7 @@ tags: - plugin/emitter --- -This plugin allows your website to be accessible offline and be installed as an app. You can use it by adding `Plugin.Offline(),` to the `emitters` in `quartz.config.ts` +This plugin allows your website to be accessible offline and be installed as an app. You can enable it by adding `Plugin.Offline(),` to the `emitters` in `quartz.config.ts` ## Offline Capability diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx index 8597075d2..c33d37542 100644 --- a/quartz/components/Explorer.tsx +++ b/quartz/components/Explorer.tsx @@ -22,6 +22,7 @@ const defaultOptions = { return -1 } }, + filterFn: (node) => node.name !== "tags", order: ["filter", "map", "sort"], } satisfies Options diff --git a/quartz/components/pages/OfflineFallbackPage.tsx b/quartz/components/pages/OfflineFallbackPage.tsx index 14d4f5e9e..d2fede3ce 100644 --- a/quartz/components/pages/OfflineFallbackPage.tsx +++ b/quartz/components/pages/OfflineFallbackPage.tsx @@ -4,7 +4,7 @@ function OfflineFallbackPage() { return (

            Offline

            -

            This page isn't offline available yet.

            +

            You're offline and this page hasn't been cached yet.

            ) } diff --git a/quartz/plugins/emitters/offline.ts b/quartz/plugins/emitters/offline.ts index e3c654b69..b17771a22 100644 --- a/quartz/plugins/emitters/offline.ts +++ b/quartz/plugins/emitters/offline.ts @@ -62,7 +62,7 @@ export const Offline: QuartzEmitterPlugin = () => { const [tree, vfile] = defaultProcessedContent({ slug, text: "Offline", - description: "This page isn't offline available yet.", + description: "You're offline and this page hasn't been cached yet.", frontmatter: { title: "Offline", tags: [] }, }) From 0bad3ce7990aa4ef417128f9d74c2947fe5117fd Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 20 Sep 2023 11:58:52 -0700 Subject: [PATCH 101/140] docs: document enableToc --- docs/features/table of contents.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/features/table of contents.md b/docs/features/table of contents.md index f05857368..a66c85017 100644 --- a/docs/features/table of contents.md +++ b/docs/features/table of contents.md @@ -8,6 +8,7 @@ tags: Quartz can automatically generate a table of contents from a list of headings on each page. It will also show you your current scroll position on the site by marking headings you've scrolled through with a different colour. By default, it will show all headers from H1 (`# Title`) all the way to H3 (`### Title`) and will only show the table of contents if there is more than 1 header on the page. +You can also hide the table of contents on a page by adding `showToc: false` to the frontmatter for that page. > [!info] > This feature requires both `Plugin.TableOfContents` in your `quartz.config.ts` and `Component.TableOfContents` in your `quartz.layout.ts` to function correctly. From 70e029d151ccbb9aeab30a0f811b9f529b7f8818 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 20 Sep 2023 13:52:29 -0700 Subject: [PATCH 102/140] Revert "docs: wording changes for offline support" This reverts commit 52a172d1a4911080444ff797183e29ba8175741e. --- docs/features/offline access.md | 2 +- quartz/components/Explorer.tsx | 1 - quartz/components/pages/OfflineFallbackPage.tsx | 2 +- quartz/plugins/emitters/offline.ts | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/features/offline access.md b/docs/features/offline access.md index 885bbd501..dcffdcd26 100644 --- a/docs/features/offline access.md +++ b/docs/features/offline access.md @@ -4,7 +4,7 @@ tags: - plugin/emitter --- -This plugin allows your website to be accessible offline and be installed as an app. You can enable it by adding `Plugin.Offline(),` to the `emitters` in `quartz.config.ts` +This plugin allows your website to be accessible offline and be installed as an app. You can use it by adding `Plugin.Offline(),` to the `emitters` in `quartz.config.ts` ## Offline Capability diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx index c33d37542..8597075d2 100644 --- a/quartz/components/Explorer.tsx +++ b/quartz/components/Explorer.tsx @@ -22,7 +22,6 @@ const defaultOptions = { return -1 } }, - filterFn: (node) => node.name !== "tags", order: ["filter", "map", "sort"], } satisfies Options diff --git a/quartz/components/pages/OfflineFallbackPage.tsx b/quartz/components/pages/OfflineFallbackPage.tsx index d2fede3ce..14d4f5e9e 100644 --- a/quartz/components/pages/OfflineFallbackPage.tsx +++ b/quartz/components/pages/OfflineFallbackPage.tsx @@ -4,7 +4,7 @@ function OfflineFallbackPage() { return (

            Offline

            -

            You're offline and this page hasn't been cached yet.

            +

            This page isn't offline available yet.

            ) } diff --git a/quartz/plugins/emitters/offline.ts b/quartz/plugins/emitters/offline.ts index b17771a22..e3c654b69 100644 --- a/quartz/plugins/emitters/offline.ts +++ b/quartz/plugins/emitters/offline.ts @@ -62,7 +62,7 @@ export const Offline: QuartzEmitterPlugin = () => { const [tree, vfile] = defaultProcessedContent({ slug, text: "Offline", - description: "You're offline and this page hasn't been cached yet.", + description: "This page isn't offline available yet.", frontmatter: { title: "Offline", tags: [] }, }) From 6a9e6352e88aa9ff18e5b33cf2de442a250bd960 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 20 Sep 2023 13:52:45 -0700 Subject: [PATCH 103/140] Revert "feat: Making Quartz available offline by making it a PWA (#465)" This reverts commit d6301fae90d9f922618bf0f413e273156731eef7. --- docs/configuration.md | 2 - docs/features/offline access.md | 31 ------ docs/index.md | 2 +- package-lock.json | 9 +- package.json | 2 +- quartz.config.ts | 1 - quartz/cfg.ts | 1 - quartz/components/Head.tsx | 4 - .../components/pages/OfflineFallbackPage.tsx | 12 --- quartz/plugins/emitters/componentResources.ts | 5 - quartz/plugins/emitters/index.ts | 1 - quartz/plugins/emitters/offline.ts | 97 ------------------- quartz/static/icon.svg | 74 -------------- 13 files changed, 6 insertions(+), 235 deletions(-) delete mode 100644 docs/features/offline access.md delete mode 100644 quartz/components/pages/OfflineFallbackPage.tsx delete mode 100644 quartz/plugins/emitters/offline.ts delete mode 100644 quartz/static/icon.svg diff --git a/docs/configuration.md b/docs/configuration.md index 35e0b9d95..047f6ca6b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -21,12 +21,10 @@ const config: QuartzConfig = { This part of the configuration concerns anything that can affect the whole site. The following is a list breaking down all the things you can configure: - `pageTitle`: title of the site. This is also used when generating the [[RSS Feed]] for your site. -- `description`: description of the site. This will be used when someone installs your site as an App. - `enableSPA`: whether to enable [[SPA Routing]] on your site. - `enablePopovers`: whether to enable [[popover previews]] on your site. - `analytics`: what to use for analytics on your site. Values can be - `null`: don't use analytics; - - `{ provider: "umami", websiteId: }`: easy, privacy-friendly, open source, GDPR Compliant analytics; - `{ provider: 'plausible' }`: use [Plausible](https://plausible.io/), a privacy-friendly alternative to Google Analytics; or - `{ provider: 'google', tagId: }`: use Google Analytics - `baseUrl`: this is used for sitemaps and RSS feeds that require an absolute URL to know where the canonical 'home' of your site lives. This is normally the deployed URL of your site (e.g. `quartz.jzhao.xyz` for this site). Do not include the protocol (i.e. `https://`) or any leading or trailing slashes. diff --git a/docs/features/offline access.md b/docs/features/offline access.md deleted file mode 100644 index dcffdcd26..000000000 --- a/docs/features/offline access.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: "Offline Access (PWA)" -tags: - - plugin/emitter ---- - -This plugin allows your website to be accessible offline and be installed as an app. You can use it by adding `Plugin.Offline(),` to the `emitters` in `quartz.config.ts` - -## Offline Capability - -Whenever you visit a page it gets cached for offline use. Depending on the kind of content, the process for caching is diffent: - -- **Pages** (HTML, your converted Markdown files): Quartz first tries to get them over the Network. If that fails, your browser attempts to fetch it from the cache. -- **Static Resources** (Fonts, CSS Styling, JavaScript): Quartz uses cached resources by default and updates the cache over the network in the background. -- **Images**: Images are saved once and then served from cache. Quartz uses a limited cache of 60 images and images remain in the cache for 30 days - -You can edit the fallback page by changing the `offline.md` file in the root of your `content` directory - -## Progressive Web App (PWA) - -Progressive Web Apps can have [many properties](https://developer.mozilla.org/en-US/docs/Web/Manifest). We're only going to mention the ones Quartz supports by default, however you can edit the offline plugins file to add more in case required. - -- **icons**: the `icon.svg` file in the `quartz/static` directory is used for all the icons. This makes it easier to scale the image since you don't need to provide an png for every size -- **name**, **short_name**: Uses the `pageTitle` configured in `quartz.config.ts` -- **description**: Uses the `description` configured in `quartz.config.ts` -- **background_color**, **theme_color**: Uses the `lightMode.light` color configured in `quartz.config.ts`. -- **start_url**: Uses the `baseUrl` configured in `quartz.config.ts` - -### Default values - -- **display**: this is set to `minimal-ui` diff --git a/docs/index.md b/docs/index.md index 570d5b364..05de2bae9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,7 +30,7 @@ This will guide you through initializing your Quartz with content. Once you've d ## 🔧 Features -- [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], [[offline access]] and [many more](./features) right out of the box +- [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], and [many more](./features) right out of the box - Hot-reload for both configuration and content - Simple JSX layouts and [[creating components|page components]] - [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes diff --git a/package-lock.json b/package-lock.json index 8ff94245d..a87907897 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,7 +73,7 @@ "@types/flexsearch": "^0.7.3", "@types/hast": "^2.3.4", "@types/js-yaml": "^4.0.5", - "@types/node": "^20.6.2", + "@types/node": "^20.1.2", "@types/pretty-time": "^1.1.2", "@types/source-map-support": "^0.5.6", "@types/workerpool": "^6.4.0", @@ -113,7 +113,6 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", - "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -1464,9 +1463,9 @@ } }, "node_modules/@types/node": { - "version": "20.6.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz", - "integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==", + "version": "20.3.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.3.tgz", + "integrity": "sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==", "dev": true }, "node_modules/@types/parse5": { diff --git a/package.json b/package.json index e514edfbd..0a2085cef 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "@types/flexsearch": "^0.7.3", "@types/hast": "^2.3.4", "@types/js-yaml": "^4.0.5", - "@types/node": "^20.6.2", + "@types/node": "^20.1.2", "@types/pretty-time": "^1.1.2", "@types/source-map-support": "^0.5.6", "@types/workerpool": "^6.4.0", diff --git a/quartz.config.ts b/quartz.config.ts index 5a1f643aa..f677a18f9 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -4,7 +4,6 @@ import * as Plugin from "./quartz/plugins" const config: QuartzConfig = { configuration: { pageTitle: "🪴 Quartz 4.0", - description: "Quartz Documentation Page and Demo", enableSPA: true, enablePopovers: true, analytics: { diff --git a/quartz/cfg.ts b/quartz/cfg.ts index 73e959fb7..8371b5e2b 100644 --- a/quartz/cfg.ts +++ b/quartz/cfg.ts @@ -19,7 +19,6 @@ export type Analytics = export interface GlobalConfiguration { pageTitle: string - description: string /** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */ enableSPA: boolean /** Whether to display Wikipedia-style popovers when hovering over links */ diff --git a/quartz/components/Head.tsx b/quartz/components/Head.tsx index 972f7497e..2bf263817 100644 --- a/quartz/components/Head.tsx +++ b/quartz/components/Head.tsx @@ -14,8 +14,6 @@ export default (() => { const iconPath = joinSegments(baseDir, "static/icon.png") const ogImagePath = `https://${cfg.baseUrl}/static/og-image.png` - const manifest = - cfg.baseUrl == undefined ? "/manifest.json" : `https://${cfg.baseUrl}/manifest.json` return ( @@ -27,9 +25,7 @@ export default (() => { {cfg.baseUrl && } - - diff --git a/quartz/components/pages/OfflineFallbackPage.tsx b/quartz/components/pages/OfflineFallbackPage.tsx deleted file mode 100644 index 14d4f5e9e..000000000 --- a/quartz/components/pages/OfflineFallbackPage.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { QuartzComponentConstructor } from "../types" - -function OfflineFallbackPage() { - return ( -
            -

            Offline

            -

            This page isn't offline available yet.

            -
            - ) -} - -export default (() => OfflineFallbackPage) satisfies QuartzComponentConstructor diff --git a/quartz/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts index a82e7c12a..1290a3548 100644 --- a/quartz/plugins/emitters/componentResources.ts +++ b/quartz/plugins/emitters/componentResources.ts @@ -116,11 +116,6 @@ function addGlobalPageResources( document.dispatchEvent(event)`) } - componentResources.afterDOMLoaded.push(` - if ('serviceWorker' in navigator) { - navigator.serviceWorker.register('/sw.js'); - }`) - let wsUrl = `ws://localhost:${ctx.argv.wsPort}` if (ctx.argv.remoteDevHost) { diff --git a/quartz/plugins/emitters/index.ts b/quartz/plugins/emitters/index.ts index 6de824d5f..99a2c54d5 100644 --- a/quartz/plugins/emitters/index.ts +++ b/quartz/plugins/emitters/index.ts @@ -7,4 +7,3 @@ export { Assets } from "./assets" export { Static } from "./static" export { ComponentResources } from "./componentResources" export { NotFoundPage } from "./404" -export { Offline } from "./offline" diff --git a/quartz/plugins/emitters/offline.ts b/quartz/plugins/emitters/offline.ts deleted file mode 100644 index e3c654b69..000000000 --- a/quartz/plugins/emitters/offline.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { QuartzEmitterPlugin } from "../types" -import { FilePath, FullSlug } from "../../util/path" -import { FullPageLayout } from "../../cfg" -import { sharedPageComponents } from "../../../quartz.layout" -import OfflineFallbackPage from "../../components/pages/OfflineFallbackPage" -import BodyConstructor from "../../components/Body" -import { pageResources, renderPage } from "../../components/renderPage" -import { defaultProcessedContent } from "../vfile" -import { QuartzComponentProps } from "../../components/types" - -export const Offline: QuartzEmitterPlugin = () => { - const opts: FullPageLayout = { - ...sharedPageComponents, - pageBody: OfflineFallbackPage(), - beforeBody: [], - left: [], - right: [], - } - - const { head: Head, pageBody, footer: Footer } = opts - const Body = BodyConstructor() - - return { - name: "OfflineSupport", - getQuartzComponents() { - return [Head, Body, pageBody, Footer] - }, - async emit({ cfg }, _content, resources, emit): Promise { - const manifest = { - short_name: cfg.configuration.pageTitle, - name: cfg.configuration.pageTitle, - description: cfg.configuration.description, - background_color: cfg.configuration.theme.colors.lightMode.light, - theme_color: cfg.configuration.theme.colors.lightMode.light, - display: "minimal-ui", - icons: [ - { - src: "static/icon.svg", - sizes: "any", - purpose: "maskable", - }, - { - src: "static/icon.svg", - sizes: "any", - purpose: "any", - }, - ], - start_url: - cfg.configuration.baseUrl == undefined ? "/" : `https://${cfg.configuration.baseUrl}/`, - } - - const serviceWorker = - "importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js');" + - "const {pageCache, imageCache, staticResourceCache, googleFontsCache, offlineFallback} = workbox.recipes;" + - "pageCache(); googleFontsCache(); staticResourceCache(); imageCache(); offlineFallback();" - - const slug = "offline" as FullSlug - - const url = new URL(`https://${cfg.configuration.baseUrl ?? "example.com"}`) - const path = url.pathname as FullSlug - const externalResources = pageResources(path, resources) - const [tree, vfile] = defaultProcessedContent({ - slug, - text: "Offline", - description: "This page isn't offline available yet.", - frontmatter: { title: "Offline", tags: [] }, - }) - - const componentData: QuartzComponentProps = { - fileData: vfile.data, - externalResources, - cfg: cfg.configuration, - children: [], - tree, - allFiles: [], - } - - return Promise.all([ - emit({ - content: JSON.stringify(manifest), - slug: "manifest" as FullSlug, - ext: ".json", - }), - emit({ - content: serviceWorker, - slug: "sw" as FullSlug, - ext: ".js", - }), - emit({ - content: renderPage(slug, componentData, opts, externalResources), - slug, - ext: ".html", - }), - ]) - }, - } -} diff --git a/quartz/static/icon.svg b/quartz/static/icon.svg deleted file mode 100644 index c6ecfa2db..000000000 --- a/quartz/static/icon.svg +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - From b029eeadabe0877df6ec11443c68743f1494bc40 Mon Sep 17 00:00:00 2001 From: Ben Schlegel <31989404+benschlegel@users.noreply.github.com> Date: Wed, 20 Sep 2023 22:55:29 +0200 Subject: [PATCH 104/140] feat(explorer): improve accessibility and consistency (+ bug fix) (#488) * feat(consistency): use `all: unset` on button * style: improve accessibility and consistency for explorer * fix: localStorage bug with folder name changes * chore: bump quartz version --- package.json | 2 +- quartz/components/Explorer.tsx | 4 ++-- quartz/components/ExplorerNode.tsx | 10 +++++----- quartz/components/scripts/explorer.inline.ts | 8 +++++--- quartz/components/styles/explorer.scss | 13 ++++++++----- 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 0a2085cef..11a68d3ad 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@jackyzha0/quartz", "description": "🌱 publish your digital garden and notes as a website", "private": true, - "version": "4.0.11", + "version": "4.1.0", "type": "module", "author": "jackyzha0 ", "license": "MIT", diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx index 8597075d2..bc4855eda 100644 --- a/quartz/components/Explorer.tsx +++ b/quartz/components/Explorer.tsx @@ -79,7 +79,7 @@ export default ((userOpts?: Partial) => { data-savestate={opts.useSavedState} data-tree={jsonTree} > -

            {opts.title}

            +

            {opts.title}

            ) => {
              -
              +
      diff --git a/quartz/components/ExplorerNode.tsx b/quartz/components/ExplorerNode.tsx index fd0c0823d..c55a7a0a2 100644 --- a/quartz/components/ExplorerNode.tsx +++ b/quartz/components/ExplorerNode.tsx @@ -145,7 +145,7 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro } return ( -
      +
    • {node.file ? ( // Single file node
    • @@ -174,17 +174,17 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro {/* render tag if folderBehavior is "link", otherwise render )} -
    • +
    )} {/* Recursively render children of folder */} @@ -210,6 +210,6 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
)} -
+ ) } diff --git a/quartz/components/scripts/explorer.inline.ts b/quartz/components/scripts/explorer.inline.ts index 2b7df7d35..9fe18654f 100644 --- a/quartz/components/scripts/explorer.inline.ts +++ b/quartz/components/scripts/explorer.inline.ts @@ -113,9 +113,11 @@ function setupExplorer() { ) as HTMLElement // Get corresponding content
    tag and set state - const folderUL = folderLi.parentElement?.nextElementSibling - if (folderUL) { - setFolderState(folderUL as HTMLElement, folderUl.collapsed) + if (folderLi) { + const folderUL = folderLi.parentElement?.nextElementSibling + if (folderUL) { + setFolderState(folderUL as HTMLElement, folderUl.collapsed) + } } }) } else { diff --git a/quartz/components/styles/explorer.scss b/quartz/components/styles/explorer.scss index 776a5ae6e..955c269ab 100644 --- a/quartz/components/styles/explorer.scss +++ b/quartz/components/styles/explorer.scss @@ -1,4 +1,5 @@ button#explorer { + all: unset; background-color: transparent; border: none; text-align: left; @@ -8,7 +9,7 @@ button#explorer { display: flex; align-items: center; - & h3 { + & h1 { font-size: 1rem; display: inline-block; margin: 0; @@ -58,7 +59,7 @@ button#explorer { max-height 0.35s ease, transform 0.35s ease, opacity 0.2s ease; - & div > li > a { + & li > a { color: var(--dark); opacity: 0.75; pointer-events: all; @@ -92,7 +93,7 @@ svg { color: var(--tertiary) !important; } - & li > button { + & div > button { color: var(--dark); background-color: transparent; border: none; @@ -103,7 +104,7 @@ svg { display: flex; align-items: center; - & h3 { + & p { font-size: 0.95rem; display: inline-block; color: var(--secondary); @@ -138,5 +139,7 @@ div:has(> .folder-outer:not(.open)) > .folder-container > svg { #explorer-end { // needs height so IntersectionObserver gets triggered - height: 1px; + height: 4px; + // remove default margin from li + margin: 0; } From 16d33fb77193710bede887d6a177d2144b78fb67 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 20 Sep 2023 16:08:54 -0700 Subject: [PATCH 105/140] feat: display name for folders, expand explorer a little bit (#489) * feat: display name for folders, expand explorer a little bit * update docs --- docs/advanced/index.md | 3 +++ docs/features/explorer.md | 13 +++++++------ quartz/components/Explorer.tsx | 5 +++-- quartz/components/ExplorerNode.tsx | 14 ++++++++++---- quartz/styles/base.scss | 2 +- 5 files changed, 24 insertions(+), 13 deletions(-) create mode 100644 docs/advanced/index.md diff --git a/docs/advanced/index.md b/docs/advanced/index.md new file mode 100644 index 000000000..482258900 --- /dev/null +++ b/docs/advanced/index.md @@ -0,0 +1,3 @@ +--- +title: "Advanced" +--- diff --git a/docs/features/explorer.md b/docs/features/explorer.md index 6f941b871..b0eb12d87 100644 --- a/docs/features/explorer.md +++ b/docs/features/explorer.md @@ -57,7 +57,8 @@ All functions you can pass work with the `FileNode` class, which has the followi ```ts title="quartz/components/ExplorerNode.tsx" {2-5} export class FileNode { children: FileNode[] // children of current node - name: string // name of node (only useful for folders) + name: string // last part of slug + displayName: string // what actually should be displayed in the explorer file: QuartzPluginData | null // set if node is a file, see `QuartzPluginData` for more detail depth: number // depth of current node @@ -72,7 +73,7 @@ Every function you can pass is optional. By default, only a `sort` function will Component.Explorer({ sortFn: (a, b) => { if ((!a.file && !b.file) || (a.file && b.file)) { - return a.name.localeCompare(b.name) + return a.displayName.localeCompare(b.displayName) } if (a.file && !b.file) { return 1 @@ -120,7 +121,7 @@ Using this example, the explorer will alphabetically sort everything, but put al Component.Explorer({ sortFn: (a, b) => { if ((!a.file && !b.file) || (a.file && b.file)) { - return a.name.localeCompare(b.name) + return a.displayName.localeCompare(b.displayName) } if (a.file && !b.file) { return -1 @@ -138,7 +139,7 @@ Using this example, the display names of all `FileNodes` (folders + files) will ```ts title="quartz.layout.ts" Component.Explorer({ mapFn: (node) => { - node.name = node.name.toUpperCase() + node.displayName = node.displayName.toUpperCase() }, }) ``` @@ -172,9 +173,9 @@ Component.Explorer({ if (node.depth > 0) { // set emoji for file/folder if (node.file) { - node.name = "📄 " + node.name + node.displayName = "📄 " + node.displayName } else { - node.name = "📁 " + node.name + node.displayName = "📁 " + node.displayName } } }, diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx index bc4855eda..73c620f3b 100644 --- a/quartz/components/Explorer.tsx +++ b/quartz/components/Explorer.tsx @@ -11,10 +11,10 @@ const defaultOptions = { folderClickBehavior: "collapse", folderDefaultState: "collapsed", useSavedState: true, - // Sort order: folders first, then files. Sort folders and files alphabetically sortFn: (a, b) => { + // Sort order: folders first, then files. Sort folders and files alphabetically if ((!a.file && !b.file) || (a.file && b.file)) { - return a.name.localeCompare(b.name) + return a.displayName.localeCompare(b.displayName) } if (a.file && !b.file) { return 1 @@ -22,6 +22,7 @@ const defaultOptions = { return -1 } }, + filterFn: (node) => node.name !== "tags", order: ["filter", "map", "sort"], } satisfies Options diff --git a/quartz/components/ExplorerNode.tsx b/quartz/components/ExplorerNode.tsx index c55a7a0a2..9bdd0dfcc 100644 --- a/quartz/components/ExplorerNode.tsx +++ b/quartz/components/ExplorerNode.tsx @@ -29,19 +29,25 @@ export type FolderState = { export class FileNode { children: FileNode[] name: string + displayName: string file: QuartzPluginData | null depth: number constructor(name: string, file?: QuartzPluginData, depth?: number) { this.children = [] this.name = name + this.displayName = name this.file = file ? structuredClone(file) : null this.depth = depth ?? 0 } private insert(file: DataWrapper) { if (file.path.length === 1) { - this.children.push(new FileNode(file.file.frontmatter!.title, file.file, this.depth + 1)) + if (file.path[0] !== "index.md") { + this.children.push(new FileNode(file.file.frontmatter!.title, file.file, this.depth + 1)) + } else { + this.displayName = file.file.frontmatter!.title + } } else { const next = file.path[0] file.path = file.path.splice(1) @@ -150,7 +156,7 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro // Single file node
  • - {node.name} + {node.displayName}
  • ) : ( @@ -177,11 +183,11 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
    {folderBehavior === "link" ? ( - {node.name} + {node.displayName} ) : ( )}
    diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index c6925fbe5..9d553622d 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -446,7 +446,7 @@ video { ul.overflow, ol.overflow { - max-height: 300; + max-height: 400; overflow-y: auto; // clearfix From 48452231d5fcd14ef218928bde9ae7e5bc745f4a Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 20 Sep 2023 16:09:18 -0700 Subject: [PATCH 106/140] perf: memoize filetree computation (#490) * perf: memoize filetree computation * format * var -> let --- quartz/components/Explorer.tsx | 80 +++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 35 deletions(-) diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx index 73c620f3b..de6b5e0ae 100644 --- a/quartz/components/Explorer.tsx +++ b/quartz/components/Explorer.tsx @@ -4,6 +4,7 @@ import explorerStyle from "./styles/explorer.scss" // @ts-ignore import script from "./scripts/explorer.inline" import { ExplorerNode, FileNode, Options } from "./ExplorerNode" +import { QuartzPluginData } from "../plugins/vfile" // Options interface defined in `ExplorerNode` to avoid circular dependency const defaultOptions = { @@ -27,49 +28,58 @@ const defaultOptions = { } satisfies Options export default ((userOpts?: Partial) => { - function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) { - // Parse config - const opts: Options = { ...defaultOptions, ...userOpts } + // Parse config + const opts: Options = { ...defaultOptions, ...userOpts } - // Construct tree from allFiles - const fileTree = new FileNode("") - allFiles.forEach((file) => fileTree.add(file, 1)) + // memoized + let fileTree: FileNode + let jsonTree: string - /** - * Keys of this object must match corresponding function name of `FileNode`, - * while values must be the argument that will be passed to the function. - * - * e.g. entry for FileNode.sort: `sort: opts.sortFn` (value is sort function from options) - */ - const functions = { - map: opts.mapFn, - sort: opts.sortFn, - filter: opts.filterFn, - } + function constructFileTree(allFiles: QuartzPluginData[]) { + if (!fileTree) { + // Construct tree from allFiles + fileTree = new FileNode("") + allFiles.forEach((file) => fileTree.add(file, 1)) - // Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied) - if (opts.order) { - // Order is important, use loop with index instead of order.map() - for (let i = 0; i < opts.order.length; i++) { - const functionName = opts.order[i] - if (functions[functionName]) { - // for every entry in order, call matching function in FileNode and pass matching argument - // e.g. i = 0; functionName = "filter" - // converted to: (if opts.filterFn) => fileTree.filter(opts.filterFn) + /** + * Keys of this object must match corresponding function name of `FileNode`, + * while values must be the argument that will be passed to the function. + * + * e.g. entry for FileNode.sort: `sort: opts.sortFn` (value is sort function from options) + */ + const functions = { + map: opts.mapFn, + sort: opts.sortFn, + filter: opts.filterFn, + } - // @ts-ignore - // typescript cant statically check these dynamic references, so manually make sure reference is valid and ignore warning - fileTree[functionName].call(fileTree, functions[functionName]) + // Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied) + if (opts.order) { + // Order is important, use loop with index instead of order.map() + for (let i = 0; i < opts.order.length; i++) { + const functionName = opts.order[i] + if (functions[functionName]) { + // for every entry in order, call matching function in FileNode and pass matching argument + // e.g. i = 0; functionName = "filter" + // converted to: (if opts.filterFn) => fileTree.filter(opts.filterFn) + + // @ts-ignore + // typescript cant statically check these dynamic references, so manually make sure reference is valid and ignore warning + fileTree[functionName].call(fileTree, functions[functionName]) + } } } + + // Get all folders of tree. Initialize with collapsed state + const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed") + + // Stringify to pass json tree as data attribute ([data-tree]) + jsonTree = JSON.stringify(folders) } + } - // Get all folders of tree. Initialize with collapsed state - const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed") - - // Stringify to pass json tree as data attribute ([data-tree]) - const jsonTree = JSON.stringify(folders) - + function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) { + constructFileTree(allFiles) return (