diff --git a/docs/advanced/making plugins.md b/docs/advanced/making plugins.md index 65209a2ca..d9ed9e33e 100644 --- a/docs/advanced/making plugins.md +++ b/docs/advanced/making plugins.md @@ -84,10 +84,10 @@ export const Latex: QuartzTransformerPlugin = (opts?: Options) => { externalResources() { if (engine === "katex") { return { - css: ["https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css"], + css: ["https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css"], js: [ { - src: "https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/contrib/copy-tex.min.js", + src: "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.7/contrib/copy-tex.min.js", loadTime: "afterDOMReady", contentType: "external", }, @@ -278,7 +278,7 @@ export const ContentPage: QuartzEmitterPlugin = () => { allFiles, } - const content = renderPage(slug, componentData, opts, externalResources) + const content = renderPage(cfg, slug, componentData, opts, externalResources) const fp = await emit({ content, slug: file.data.slug!, diff --git a/docs/configuration.md b/docs/configuration.md index 047f6ca6b..366430cc2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -25,14 +25,17 @@ This part of the configuration concerns anything that can affect the whole 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: 'plausible' }`: use [Plausible](https://plausible.io/), a privacy-friendly alternative to Google Analytics; or - - `{ provider: 'google', tagId: }`: use Google Analytics + - `{ provider: 'google', tagId: '' }`: use Google Analytics; + - `{ provider: 'plausible' }` (managed) or `{ provider: 'plausible', host: '' }` (self-hosted): use [Plausible](https://plausible.io/); + - `{ provider: 'umami', host: '', websiteId: '' }`: use [Umami](https://umami.is/); +- `locale`: used for [[i18n]] and date formatting - `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. - 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. + - `cdnCaching`: Whether to use Google CDN to cache the fonts (generally will be faster). Disable this if you want Quartz to be self-contained. Default to `true` - `typography`: what fonts to use. Any font available on [Google Fonts](https://fonts.google.com/) works here. - `header`: Font to use for headers - `code`: Font for inline and block quotes. diff --git a/docs/features/explorer.md b/docs/features/explorer.md index b5fd379a7..a2acb861a 100644 --- a/docs/features/explorer.md +++ b/docs/features/explorer.md @@ -42,7 +42,7 @@ 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` +- Removing explorer: 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 [[#Advanced customization]] - Component: diff --git a/docs/features/i18n.md b/docs/features/i18n.md new file mode 100644 index 000000000..57547ddad --- /dev/null +++ b/docs/features/i18n.md @@ -0,0 +1,18 @@ +--- +title: Internationalization +--- + +Internationalization allows users to translate text in the Quartz interface into various supported languages without needing to make extensive code changes. This can be changed via the `locale` [[configuration]] field in `quartz.config.ts`. + +The locale field generally follows a certain format: `{language}-{REGION}` + +- `{language}` is usually a [2-letter lowercase language code](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes). +- `{REGION}` is usually a [2-letter uppercase region code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) + +> [!tip] Interested in contributing? +> We [gladly welcome translation PRs](https://github.com/jackyzha0/quartz/tree/v4/quartz/i18n/locales)! To contribute a translation, do the following things: +> +> 1. In the `quartz/i18n/locales` folder, copy the `en-US.ts` file. +> 2. Rename it to `{language}-{REGION}.ts` so it matches a locale of the format shown above. +> 3. Fill in the translations! +> 4. Add the entry under `TRANSLATIONS` in `quartz/i18n/index.ts`. diff --git a/docs/features/popover previews.md b/docs/features/popover previews.md index f88222402..066604758 100644 --- a/docs/features/popover previews.md +++ b/docs/features/popover previews.md @@ -8,6 +8,8 @@ By default, Quartz only fetches previews for pages inside your vault due to [COR When [[creating components|creating your own components]], you can include this `popover-hint` class to also include it in the popover. +Similar to Obsidian, [[quartz layout.png|images referenced using wikilinks]] can also be viewed as popups. + ## Configuration - Remove popovers: set the `enablePopovers` field in `quartz.config.ts` to be `false`. diff --git a/docs/hosting.md b/docs/hosting.md index e6340d293..eeb930849 100644 --- a/docs/hosting.md +++ b/docs/hosting.md @@ -30,7 +30,7 @@ Press "Save and deploy" and Cloudflare should have a deployed version of your si To add a custom domain, check out [Cloudflare's documentation](https://developers.cloudflare.com/pages/platform/custom-domains/). > [!warning] -> Cloudflare Pages only allows shallow `git` clones so if you rely on `git` for timestamps, it is recommended you either add dates to your frontmatter (see [[authoring content#Syntax]]) or use another hosting provider. +> Cloudflare Pages performs a shallow clone by default, so if you rely on `git` for timestamps, it is recommended that you add `git fetch --unshallow &&` to the beginning of the build command (e.g., `git fetch --unshallow && npx quartz build`). ## GitHub Pages @@ -228,3 +228,25 @@ pages: When `.gitlab-ci.yaml` is committed, GitLab will build and deploy the website as a GitLab Page. You can find the url under `Deploy > Pages` in the sidebar. By default, the page is private and only visible when logged in to a GitLab account with access to the repository but can be opened in the settings under `Deploy` -> `Pages`. + +## Self-Hosting + +Copy the `public` directory to your web server and configure it to serve the files. You can use any web server to host your site. Since Quartz generates links that do not include the `.html` extension, you need to let your web server know how to deal with it. + +### Using Nginx + +Here's an example of how to do this with Nginx: + +```nginx title="nginx.conf" +server { + listen 80; + server_name example.com; + root /path/to/quartz/public; + index index.html; + error_page 404 /404.html; + + location / { + try_files $uri $uri.html $uri/ =404; + } +} +``` diff --git a/docs/index.md b/docs/index.md index cbf8719d1..f25b6e244 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,7 +31,7 @@ If you prefer instructions in a video format you can try following Nicole van de ## 🔧 Features -- [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], [[Docker Support]], 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]], [[Docker Support]], [[i18n|internationalization]] 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/docs/showcase.md b/docs/showcase.md index cdef5fc50..0509e93ab 100644 --- a/docs/showcase.md +++ b/docs/showcase.md @@ -25,5 +25,6 @@ Want to see what Quartz can do? Here are some cool community gardens: - [Scaling Synthesis - A hypertext research notebook](https://scalingsynthesis.com/) - [Data Dictionary 🧠](https://glossary.airbyte.com/) - [sspaeti.com's Second Brain](https://brain.sspaeti.com/) +- [🪴Aster's notebook](https://notes.asterhu.com) If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)! diff --git a/package-lock.json b/package-lock.json index 023d5c6f6..c56ae8601 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,26 +1,26 @@ { "name": "@jackyzha0/quartz", - "version": "4.2.1", + "version": "4.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jackyzha0/quartz", - "version": "4.2.1", + "version": "4.2.2", "license": "MIT", "dependencies": { "@clack/prompts": "^0.7.0", - "@floating-ui/dom": "^1.6.1", - "@napi-rs/simple-git": "0.1.14", + "@floating-ui/dom": "^1.6.3", + "@napi-rs/simple-git": "0.1.16", "async-mutex": "^0.4.1", "chalk": "^5.3.0", "chokidar": "^3.5.3", "cli-spinner": "^0.2.10", "d3": "^7.8.5", - "esbuild-sass-plugin": "^2.16.0", + "esbuild-sass-plugin": "^2.16.1", "flexsearch": "0.7.43", "github-slugger": "^2.0.0", - "globby": "^14.0.0", + "globby": "^14.0.1", "gray-matter": "^4.0.3", "hast-util-to-html": "^9.0.0", "hast-util-to-jsx-runtime": "^2.3.0", @@ -32,7 +32,7 @@ "mdast-util-to-hast": "^13.1.0", "mdast-util-to-string": "^4.0.0", "micromorph": "^0.4.5", - "preact": "^10.19.3", + "preact": "^10.19.5", "preact-render-to-string": "^6.3.1", "pretty-bytes": "^6.1.1", "pretty-time": "^1.1.0", @@ -40,7 +40,7 @@ "rehype-autolink-headings": "^7.1.0", "rehype-katex": "^7.0.0", "rehype-mathjax": "^6.0.0", - "rehype-pretty-code": "^0.12.6", + "rehype-pretty-code": "^0.13.0", "rehype-raw": "^7.0.0", "rehype-slug": "^6.0.0", "remark": "^15.0.1", @@ -56,7 +56,7 @@ "satori": "^0.10.6", "serve-handler": "^6.1.5", "sharp": "^0.32.6", - "shikiji": "^0.10.2", + "shiki": "^1.1.6", "source-map-support": "^0.5.21", "to-vfile": "^8.0.0", "toml": "^3.0.0", @@ -75,14 +75,14 @@ "@types/d3": "^7.4.3", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", - "@types/node": "^20.11.14", + "@types/node": "^20.11.19", "@types/pretty-time": "^1.1.5", "@types/source-map-support": "^0.5.10", "@types/ws": "^8.5.10", "@types/yargs": "^17.0.32", "esbuild": "^0.19.9", "prettier": "^3.2.4", - "tsx": "^4.7.0", + "tsx": "^4.7.1", "typescript": "^5.3.3" }, "engines": { @@ -488,12 +488,12 @@ } }, "node_modules/@floating-ui/dom": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.1.tgz", - "integrity": "sha512-iA8qE43/H5iGozC3W0YSnVSW42Vh522yyM1gj+BqRwVsTNOyr231PsXDaV04yT39PsO0QL2QpbI/M0ZaLUQgRQ==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", + "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", "dependencies": { - "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.1" + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" } }, "node_modules/@floating-ui/utils": { @@ -518,30 +518,30 @@ } }, "node_modules/@napi-rs/simple-git": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git/-/simple-git-0.1.14.tgz", - "integrity": "sha512-2cDnsT0nKpQ7yg5u/Zf8/ibp9YFIKhpcfMAGATYuqdJoHuBo6P6UArZ0RDOOtfFC5b9FXuYcGw2ApbM4eWdnbQ==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git/-/simple-git-0.1.16.tgz", + "integrity": "sha512-C5wRPw9waqL2jk3jEDeJv+f7ScuO3N0a39HVdyFLkwKxHH4Sya4ZbzZsu2JLi6eEqe7RuHipHL6mC7B2OfYZZw==", "engines": { "node": ">= 10" }, "optionalDependencies": { - "@napi-rs/simple-git-android-arm-eabi": "0.1.14", - "@napi-rs/simple-git-android-arm64": "0.1.14", - "@napi-rs/simple-git-darwin-arm64": "0.1.14", - "@napi-rs/simple-git-darwin-x64": "0.1.14", - "@napi-rs/simple-git-linux-arm-gnueabihf": "0.1.14", - "@napi-rs/simple-git-linux-arm64-gnu": "0.1.14", - "@napi-rs/simple-git-linux-arm64-musl": "0.1.14", - "@napi-rs/simple-git-linux-x64-gnu": "0.1.14", - "@napi-rs/simple-git-linux-x64-musl": "0.1.14", - "@napi-rs/simple-git-win32-arm64-msvc": "0.1.14", - "@napi-rs/simple-git-win32-x64-msvc": "0.1.14" + "@napi-rs/simple-git-android-arm-eabi": "0.1.16", + "@napi-rs/simple-git-android-arm64": "0.1.16", + "@napi-rs/simple-git-darwin-arm64": "0.1.16", + "@napi-rs/simple-git-darwin-x64": "0.1.16", + "@napi-rs/simple-git-linux-arm-gnueabihf": "0.1.16", + "@napi-rs/simple-git-linux-arm64-gnu": "0.1.16", + "@napi-rs/simple-git-linux-arm64-musl": "0.1.16", + "@napi-rs/simple-git-linux-x64-gnu": "0.1.16", + "@napi-rs/simple-git-linux-x64-musl": "0.1.16", + "@napi-rs/simple-git-win32-arm64-msvc": "0.1.16", + "@napi-rs/simple-git-win32-x64-msvc": "0.1.16" } }, "node_modules/@napi-rs/simple-git-android-arm-eabi": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm-eabi/-/simple-git-android-arm-eabi-0.1.14.tgz", - "integrity": "sha512-fAJ/Hxc9DhtSHOcB3dPCRW1YcVsqAnbNoOOnHir4aDCtqTP64HrFa7A/675v3vQZpI0u3fXHRcYqW8NF0O/zcg==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm-eabi/-/simple-git-android-arm-eabi-0.1.16.tgz", + "integrity": "sha512-dbrCL0Pl5KZG7x7tXdtVsA5CO6At5ohDX3myf5xIYn9kN4jDFxsocl8bNt6Vb/hZQoJd8fI+k5VlJt+rFhbdVw==", "cpu": [ "arm" ], @@ -554,9 +554,9 @@ } }, "node_modules/@napi-rs/simple-git-android-arm64": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm64/-/simple-git-android-arm64-0.1.14.tgz", - "integrity": "sha512-dav730MRAR142DoyNDafuwKXcUCYwlbxxxxOarDph7bbN0mZZnKHOQohvRCD/Uz4aJLaj6khCavXSjLDWArEUg==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm64/-/simple-git-android-arm64-0.1.16.tgz", + "integrity": "sha512-xYz+TW5J09iK8SuTAKK2D5MMIsBUXVSs8nYp7HcMi8q6FCRO7yJj96YfP9PvKsc/k64hOyqGmL5DhCzY9Cu1FQ==", "cpu": [ "arm64" ], @@ -569,9 +569,9 @@ } }, "node_modules/@napi-rs/simple-git-darwin-arm64": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-arm64/-/simple-git-darwin-arm64-0.1.14.tgz", - "integrity": "sha512-f6+DqRnI+vFvnsAyw66mWhwl0vw1TOieHV07hvKbg4PU5j+RBI+lVqwY2L+IEAxDFlPirTWKKvGY1Lr7M/yi/A==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-arm64/-/simple-git-darwin-arm64-0.1.16.tgz", + "integrity": "sha512-XfgsYqxhUE022MJobeiX563TJqyQyX4FmYCnqrtJwAfivESVeAJiH6bQIum8dDEYMHXCsG7nL8Ok0Dp8k2m42g==", "cpu": [ "arm64" ], @@ -584,9 +584,9 @@ } }, "node_modules/@napi-rs/simple-git-darwin-x64": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-x64/-/simple-git-darwin-x64-0.1.14.tgz", - "integrity": "sha512-x/EnwJdDWJAFay8TQt09byJoBlVZhPEaTAPmRR0fUPzWTjrr28bOy8UW1ysszd9ylBxlyIhuWjOHMHu9CBigTQ==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-x64/-/simple-git-darwin-x64-0.1.16.tgz", + "integrity": "sha512-tkEVBhD6vgRCbeWsaAQqM3bTfpIVGeitamPPRVSbsq8qgzJ5Dx6ZedH27R7KSsA/uao7mZ3dsrNLXbu1Wy5MzA==", "cpu": [ "x64" ], @@ -599,9 +599,9 @@ } }, "node_modules/@napi-rs/simple-git-linux-arm-gnueabihf": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm-gnueabihf/-/simple-git-linux-arm-gnueabihf-0.1.14.tgz", - "integrity": "sha512-k0JZaXBl031gP5VOnoMa1I3lCHlBG7QvtunX5rxnRjx2kZ+JgUyT12s/qle/4xkJ0MnmfKTeiD7hs4Cc4Z3Tzw==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm-gnueabihf/-/simple-git-linux-arm-gnueabihf-0.1.16.tgz", + "integrity": "sha512-R6VAyNnp/yRaT7DV1Ao3r67SqTWDa+fNq2LrNy0Z8gXk2wB9ZKlrxFtLPE1WSpWknWtyRDLpRlsorh7Evk7+7w==", "cpu": [ "arm" ], @@ -614,9 +614,9 @@ } }, "node_modules/@napi-rs/simple-git-linux-arm64-gnu": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-gnu/-/simple-git-linux-arm64-gnu-0.1.14.tgz", - "integrity": "sha512-CsmKP6tSIxau10ZKxV1Q1kem2QcJ/Qlov7pxp1Q7kMErcouW0H6vliVniewicaXRVDYV9wK18iD2t5GoJttwlA==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-gnu/-/simple-git-linux-arm64-gnu-0.1.16.tgz", + "integrity": "sha512-LAGI0opFKw/HBMCV2qIBK3uWSEW9h4xd2ireZKLJy8DBPymX6NrWIamuxYNyCuACnFdPRxR4LaRFy4J5ZwuMdw==", "cpu": [ "arm64" ], @@ -629,9 +629,9 @@ } }, "node_modules/@napi-rs/simple-git-linux-arm64-musl": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-musl/-/simple-git-linux-arm64-musl-0.1.14.tgz", - "integrity": "sha512-krfEckZQ3myoHwmGmqY0aHBnqAzzV66+jFNLQEKaVMSGsXA2P+UcGo0coGzmB13rFRWC2eZpZRNB3MrfrStHkw==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-musl/-/simple-git-linux-arm64-musl-0.1.16.tgz", + "integrity": "sha512-I57Ph0F0Yn2KW93ep+V1EzKhACqX0x49vvSiapqIsdDA2PifdEWLc1LJarBolmK7NKoPqKmf6lAKKO9lhiZzkg==", "cpu": [ "arm64" ], @@ -644,9 +644,9 @@ } }, "node_modules/@napi-rs/simple-git-linux-x64-gnu": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-gnu/-/simple-git-linux-x64-gnu-0.1.14.tgz", - "integrity": "sha512-4T2Q2QdO6t3OawkwdVmdqLz2EH8lfAw2cMT/zdjfTMfhNKjJgSg3kTgRnu/tf8TLCb+wu80fFvafwE0laB2VTQ==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-gnu/-/simple-git-linux-x64-gnu-0.1.16.tgz", + "integrity": "sha512-AZYYFY2V7hlcQASPEOWyOa3e1skzTct9QPzz0LiDM3f/hCFY/wBaU2M6NC5iG3d2Kr38heuyFS/+JqxLm5WaKA==", "cpu": [ "x64" ], @@ -659,9 +659,9 @@ } }, "node_modules/@napi-rs/simple-git-linux-x64-musl": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-musl/-/simple-git-linux-x64-musl-0.1.14.tgz", - "integrity": "sha512-RaTGW8u+RXJbfRF4QN2Dcr5r5DrFh4wLjOvFeOy7sGX3Q9m3IKuw5AjRxTJqIw6xD/AAPKKNzOvPjrIF7728Lw==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-musl/-/simple-git-linux-x64-musl-0.1.16.tgz", + "integrity": "sha512-9TyMcYSBJwjT8jwjY9m24BZbu7ozyWTjsmYBYNtK3B0Um1Ov6jthSNneLVvouQ6x+k3Ow+00TiFh6bvmT00r8g==", "cpu": [ "x64" ], @@ -674,9 +674,9 @@ } }, "node_modules/@napi-rs/simple-git-win32-arm64-msvc": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-arm64-msvc/-/simple-git-win32-arm64-msvc-0.1.14.tgz", - "integrity": "sha512-kb9bKG9t79HJMuRMqbUJFLfWRf952O2Ea4VFwoRA2d/Uwtowm85Ol3JV9E6oeurguRLqdMLrUKyduCW6Hc9Jsg==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-arm64-msvc/-/simple-git-win32-arm64-msvc-0.1.16.tgz", + "integrity": "sha512-uslJ1WuAHCYJWui6xjsyT47SjX6KOHDtClmNO8hqKz1pmDSNY7AjyUY8HxvD1lK9bDnWwc4JYhikS9cxCqHybw==", "cpu": [ "arm64" ], @@ -689,9 +689,9 @@ } }, "node_modules/@napi-rs/simple-git-win32-x64-msvc": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-x64-msvc/-/simple-git-win32-x64-msvc-0.1.14.tgz", - "integrity": "sha512-3835xy0e2gOaZ3SPt1pINBFSBBL3dOx3cChyAzQU0TnMU4Ye/YOh1qa5pO7BOJlCSnOh7iWt782blxCT0HH61w==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-x64-msvc/-/simple-git-win32-x64-msvc-0.1.16.tgz", + "integrity": "sha512-SoEaVeCZCDF1MP+M9bMSXsZWgEjk4On9GWADO5JOulvzR1bKjk0s9PMHwe/YztR9F0sJzrCxwtvBZowhSJsQPg==", "cpu": [ "x64" ], @@ -744,6 +744,11 @@ "node": ">=14" } }, + "node_modules/@shikijs/core": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.1.6.tgz", + "integrity": "sha512-kt9hhvrWTm0EPtRDIsoAZnSsFlIDBVBBI5CQewpA/NZCPin+MOKRXg+JiWc4y+8fZ/v0HzfDhu/UC+OTZGMt7A==" + }, "node_modules/@shuding/opentype.js": { "version": "1.4.0-beta.0", "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", @@ -760,9 +765,9 @@ } }, "node_modules/@sindresorhus/merge-streams": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-1.0.0.tgz", - "integrity": "sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", "engines": { "node": ">=18" }, @@ -1105,9 +1110,9 @@ } }, "node_modules/@types/node": { - "version": "20.11.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.14.tgz", - "integrity": "sha512-w3yWCcwULefjP9DmDDsgUskrMoOy5Z8MiwKHr1FvqGPtx7CvJzQvxD7eKpxNtklQxLruxSXWddyeRtyud0RcXQ==", + "version": "20.11.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", + "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -2226,9 +2231,9 @@ } }, "node_modules/esbuild-sass-plugin": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.16.0.tgz", - "integrity": "sha512-mGCe9MxNYvZ+j77Q/QFO+rwUGA36mojDXkOhtVmoyz1zwYbMaNrtVrmXwwYDleS/UMKTNU3kXuiTtPiAD3K+Pw==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.16.1.tgz", + "integrity": "sha512-mBB2aEF0xk7yo+Q9pSUh8xYED/1O2wbAM6IauGkDrqy6pl9SbJNakLeLGXiNpNujWIudu8TJTZCv2L5AQYRXtA==", "dependencies": { "resolve": "^1.22.6", "sass": "^1.7.3" @@ -2508,11 +2513,11 @@ } }, "node_modules/globby": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.0.tgz", - "integrity": "sha512-/1WM/LNHRAOH9lZta77uGbq0dAEQM+XjNesWwhlERDVenqothRbnzTrL3/LrIoEPPjeUHC3vrS6TwoyxeHs7MQ==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.1.tgz", + "integrity": "sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==", "dependencies": { - "@sindresorhus/merge-streams": "^1.0.0", + "@sindresorhus/merge-streams": "^2.1.0", "fast-glob": "^3.3.2", "ignore": "^5.2.4", "path-type": "^5.0.0", @@ -4787,9 +4792,9 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, "node_modules/preact": { - "version": "10.19.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.19.3.tgz", - "integrity": "sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==", + "version": "10.19.5", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.19.5.tgz", + "integrity": "sha512-OPELkDmSVbKjbFqF9tgvOowiiQ9TmsJljIzXRyNE8nGiis94pwv1siF78rQkAP1Q1738Ce6pellRg/Ns/CtHqQ==", "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -5141,11 +5146,11 @@ } }, "node_modules/rehype-pretty-code": { - "version": "0.12.6", - "resolved": "https://registry.npmjs.org/rehype-pretty-code/-/rehype-pretty-code-0.12.6.tgz", - "integrity": "sha512-AW18s4eXwnb4PGwL0Y8BoUzBJr23epWNXndCKaZ52S4kl/4tsgM+406oCp5NdtPZsB0ItpaY+hCMv3kw58DLrA==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/rehype-pretty-code/-/rehype-pretty-code-0.13.0.tgz", + "integrity": "sha512-+22dz1StXlF7dlMyOySNaVxgcGhMI4BCxq0JxJJPWYGiKsI6cu5jyuIKGHXHvH18D8sv1rdKtvsY9UEfN3++SQ==", "dependencies": { - "@types/hast": "^3.0.3", + "@types/hast": "^3.0.4", "hast-util-to-string": "^3.0.0", "parse-numeric-range": "^1.3.0", "rehype-parse": "^9.0.0", @@ -5156,7 +5161,7 @@ "node": ">=18" }, "peerDependencies": { - "shikiji": "^0.7.0 || ^0.8.0 || ^0.9.0 || ^0.10.0" + "shiki": "^1.0.0" } }, "node_modules/rehype-raw": { @@ -5853,19 +5858,14 @@ "node": ">=8" } }, - "node_modules/shikiji": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/shikiji/-/shikiji-0.10.2.tgz", - "integrity": "sha512-wtZg3T0vtYV2PnqusWQs3mDaJBdCPWxFDrBM/SE5LfrX92gjUvfEMlc+vJnoKY6Z/S44OWaCRzNIsdBRWcTAiw==", + "node_modules/shiki": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.1.6.tgz", + "integrity": "sha512-j4pcpvaQWHb42cHeV+W6P+X/VcK7Y2ctvEham6zB8wsuRQroT6cEMIkiUmBU2Nqg2qnHZDH6ZyRdVldcy0l6xw==", "dependencies": { - "shikiji-core": "0.10.2" + "@shikijs/core": "1.1.6" } }, - "node_modules/shikiji-core": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/shikiji-core/-/shikiji-core-0.10.2.tgz", - "integrity": "sha512-9Of8HMlF96usXJHmCL3Gd0Fcf0EcyJUF9m8EoAKKd98mHXi0La2AZl1h6PegSFGtiYcBDK/fLuKbDa1l16r1fA==" - }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -6285,9 +6285,9 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsx": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.0.tgz", - "integrity": "sha512-I+t79RYPlEYlHn9a+KzwrvEwhJg35h/1zHsLC2JXvhC2mdynMv6Zxzvhv5EMV6VF5qJlLlkSnMVvdZV3PSIGcg==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.1.tgz", + "integrity": "sha512-8d6VuibXHtlN5E3zFkgY8u4DX7Y3Z27zvvPKVmLon/D4AjuKzarkUBTLDBgj9iTQ0hg5xM7c/mYiRVM+HETf0g==", "dev": true, "dependencies": { "esbuild": "~0.19.10", diff --git a/package.json b/package.json index 495f8f882..05d1e9ace 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.2.1", + "version": "4.2.2", "type": "module", "author": "jackyzha0 ", "license": "MIT", @@ -15,7 +15,7 @@ "docs": "npx quartz build --serve -d docs", "check": "tsc --noEmit && npx prettier . --check", "format": "npx prettier . --write", - "test": "tsx ./quartz/util/path.test.ts", + "test": "tsx ./quartz/util/path.test.ts && tsx ./quartz/depgraph.test.ts", "profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1" }, "engines": { @@ -35,17 +35,17 @@ }, "dependencies": { "@clack/prompts": "^0.7.0", - "@floating-ui/dom": "^1.6.1", - "@napi-rs/simple-git": "0.1.14", + "@floating-ui/dom": "^1.6.3", + "@napi-rs/simple-git": "0.1.16", "async-mutex": "^0.4.1", "chalk": "^5.3.0", "chokidar": "^3.5.3", "cli-spinner": "^0.2.10", "d3": "^7.8.5", - "esbuild-sass-plugin": "^2.16.0", + "esbuild-sass-plugin": "^2.16.1", "flexsearch": "0.7.43", "github-slugger": "^2.0.0", - "globby": "^14.0.0", + "globby": "^14.0.1", "gray-matter": "^4.0.3", "hast-util-to-html": "^9.0.0", "hast-util-to-jsx-runtime": "^2.3.0", @@ -57,7 +57,7 @@ "mdast-util-to-hast": "^13.1.0", "mdast-util-to-string": "^4.0.0", "micromorph": "^0.4.5", - "preact": "^10.19.3", + "preact": "^10.19.5", "preact-render-to-string": "^6.3.1", "pretty-bytes": "^6.1.1", "pretty-time": "^1.1.0", @@ -65,7 +65,7 @@ "rehype-autolink-headings": "^7.1.0", "rehype-katex": "^7.0.0", "rehype-mathjax": "^6.0.0", - "rehype-pretty-code": "^0.12.6", + "rehype-pretty-code": "^0.13.0", "rehype-raw": "^7.0.0", "rehype-slug": "^6.0.0", "remark": "^15.0.1", @@ -80,8 +80,8 @@ "rimraf": "^5.0.5", "satori": "^0.10.6", "serve-handler": "^6.1.5", - "shikiji": "^0.10.2", "sharp": "^0.32.6", + "shiki": "^1.1.6", "source-map-support": "^0.5.21", "to-vfile": "^8.0.0", "toml": "^3.0.0", @@ -97,14 +97,14 @@ "@types/d3": "^7.4.3", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", - "@types/node": "^20.11.14", + "@types/node": "^20.11.19", "@types/pretty-time": "^1.1.5", "@types/source-map-support": "^0.5.10", "@types/ws": "^8.5.10", "@types/yargs": "^17.0.32", "esbuild": "^0.19.9", "prettier": "^3.2.4", - "tsx": "^4.7.0", + "tsx": "^4.7.1", "typescript": "^5.3.3" } } diff --git a/quartz.config.ts b/quartz.config.ts index 173e054cc..e4cf858a5 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -9,11 +9,13 @@ const config: QuartzConfig = { analytics: { provider: "plausible", }, + locale: "en-US", baseUrl: "quartz.jzhao.xyz", ignorePatterns: ["private", "templates", ".obsidian"], defaultDateType: "created", generateSocialImages: false, theme: { + cdnCaching: true, typography: { header: "Schibsted Grotesk", body: "Source Sans Pro", @@ -46,16 +48,25 @@ const config: QuartzConfig = { plugins: { transformers: [ Plugin.FrontMatter(), - Plugin.TableOfContents(), Plugin.CreatedModifiedDate({ // you can add 'git' here for last modified from Git // if you do rely on git for dates, ensure defaultDateType is 'modified' priority: ["frontmatter", "filesystem"], }), Plugin.Latex({ renderEngine: "katex" }), - Plugin.SyntaxHighlighting(), + Plugin.SyntaxHighlighting({ + // uses themes bundled with Shikiji, see https://shikiji.netlify.app/themes + theme: { + light: "github-light", + dark: "github-dark", + }, + // set this to 'true' to use the background color of the Shikiji theme + // if set to 'false', will use Quartz theme colors for background + keepBackground: false, + }), Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }), Plugin.GitHubFlavoredMarkdown(), + Plugin.TableOfContents(), Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }), Plugin.Description(), ], diff --git a/quartz/build.ts b/quartz/build.ts index 1f90301e9..3d95f315f 100644 --- a/quartz/build.ts +++ b/quartz/build.ts @@ -17,6 +17,10 @@ import { glob, toPosixPath } from "./util/glob" import { trace } from "./util/trace" import { options } from "./util/sourcemap" import { Mutex } from "async-mutex" +import DepGraph from "./depgraph" +import { getStaticResourcesFromPlugins } from "./plugins" + +type Dependencies = Record | null> type BuildData = { ctx: BuildCtx @@ -29,8 +33,11 @@ type BuildData = { toRebuild: Set toRemove: Set lastBuildMs: number + dependencies: Dependencies } +type FileEvent = "add" | "change" | "delete" + async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) { const ctx: BuildCtx = { argv, @@ -53,7 +60,7 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) { const release = await mut.acquire() perf.addEvent("clean") - await rimraf(output) + await rimraf(path.join(output, "*"), { glob: true }) console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`) perf.addEvent("glob") @@ -68,12 +75,24 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) { const parsedFiles = await parseMarkdown(ctx, filePaths) const filteredContent = filterContent(ctx, parsedFiles) + + const dependencies: Record | null> = {} + + // Only build dependency graphs if we're doing a fast rebuild + if (argv.fastRebuild) { + const staticResources = getStaticResourcesFromPlugins(ctx) + for (const emitter of cfg.plugins.emitters) { + dependencies[emitter.name] = + (await emitter.getDependencyGraph?.(ctx, filteredContent, staticResources)) ?? null + } + } + await emitContent(ctx, filteredContent) console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`)) release() if (argv.serve) { - return startServing(ctx, mut, parsedFiles, clientRefresh) + return startServing(ctx, mut, parsedFiles, clientRefresh, dependencies) } } @@ -83,9 +102,11 @@ async function startServing( mut: Mutex, initialContent: ProcessedContent[], clientRefresh: () => void, + dependencies: Dependencies, // emitter name: dep graph ) { const { argv } = ctx + // cache file parse results const contentMap = new Map() for (const content of initialContent) { const [_tree, vfile] = content @@ -95,6 +116,7 @@ async function startServing( const buildData: BuildData = { ctx, mut, + dependencies, contentMap, ignored: await isGitIgnored(), initialSlugs: ctx.allSlugs, @@ -110,19 +132,182 @@ async function startServing( ignoreInitial: true, }) + const buildFromEntry = argv.fastRebuild ? partialRebuildFromEntrypoint : rebuildFromEntrypoint watcher - .on("add", (fp) => rebuildFromEntrypoint(fp, "add", clientRefresh, buildData)) - .on("change", (fp) => rebuildFromEntrypoint(fp, "change", clientRefresh, buildData)) - .on("unlink", (fp) => rebuildFromEntrypoint(fp, "delete", clientRefresh, buildData)) + .on("add", (fp) => buildFromEntry(fp, "add", clientRefresh, buildData)) + .on("change", (fp) => buildFromEntry(fp, "change", clientRefresh, buildData)) + .on("unlink", (fp) => buildFromEntry(fp, "delete", clientRefresh, buildData)) return async () => { await watcher.close() } } +async function partialRebuildFromEntrypoint( + filepath: string, + action: FileEvent, + clientRefresh: () => void, + buildData: BuildData, // note: this function mutates buildData +) { + const { ctx, ignored, dependencies, contentMap, mut, toRemove } = buildData + const { argv, cfg } = ctx + + // don't do anything for gitignored files + if (ignored(filepath)) { + return + } + + const buildStart = new Date().getTime() + buildData.lastBuildMs = buildStart + const release = await mut.acquire() + if (buildData.lastBuildMs > buildStart) { + release() + return + } + + const perf = new PerfTimer() + console.log(chalk.yellow("Detected change, rebuilding...")) + + // UPDATE DEP GRAPH + const fp = joinSegments(argv.directory, toPosixPath(filepath)) as FilePath + + const staticResources = getStaticResourcesFromPlugins(ctx) + let processedFiles: ProcessedContent[] = [] + + switch (action) { + case "add": + // add to cache when new file is added + processedFiles = await parseMarkdown(ctx, [fp]) + processedFiles.forEach(([tree, vfile]) => contentMap.set(vfile.data.filePath!, [tree, vfile])) + + // update the dep graph by asking all emitters whether they depend on this file + for (const emitter of cfg.plugins.emitters) { + const emitterGraph = + (await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null + + // emmiter may not define a dependency graph. nothing to update if so + if (emitterGraph) { + dependencies[emitter.name]?.updateIncomingEdgesForNode(emitterGraph, fp) + } + } + break + case "change": + // invalidate cache when file is changed + processedFiles = await parseMarkdown(ctx, [fp]) + processedFiles.forEach(([tree, vfile]) => contentMap.set(vfile.data.filePath!, [tree, vfile])) + + // only content files can have added/removed dependencies because of transclusions + if (path.extname(fp) === ".md") { + for (const emitter of cfg.plugins.emitters) { + // get new dependencies from all emitters for this file + const emitterGraph = + (await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null + + // only update the graph if the emitter plugin uses the changed file + // eg. Assets plugin ignores md files, so we skip updating the graph + if (emitterGraph?.hasNode(fp)) { + // merge the new dependencies into the dep graph + dependencies[emitter.name]?.updateIncomingEdgesForNode(emitterGraph, fp) + } + } + } + break + case "delete": + toRemove.add(fp) + break + } + + if (argv.verbose) { + console.log(`Updated dependency graphs in ${perf.timeSince()}`) + } + + // EMIT + perf.addEvent("rebuild") + let emittedFiles = 0 + const destinationsToDelete = new Set() + + for (const emitter of cfg.plugins.emitters) { + const depGraph = dependencies[emitter.name] + + // emitter hasn't defined a dependency graph. call it with all processed files + if (depGraph === null) { + if (argv.verbose) { + console.log( + `Emitter ${emitter.name} doesn't define a dependency graph. Calling it with all files...`, + ) + } + + const files = [...contentMap.values()].filter( + ([_node, vfile]) => !toRemove.has(vfile.data.filePath!), + ) + + const emittedFps = await emitter.emit(ctx, files, staticResources) + + if (ctx.argv.verbose) { + for (const file of emittedFps) { + console.log(`[emit:${emitter.name}] ${file}`) + } + } + + emittedFiles += emittedFps.length + continue + } + + // only call the emitter if it uses this file + if (depGraph.hasNode(fp)) { + // re-emit using all files that are needed for the downstream of this file + // eg. for ContentIndex, the dep graph could be: + // a.md --> contentIndex.json + // b.md ------^ + // + // if a.md changes, we need to re-emit contentIndex.json, + // and supply [a.md, b.md] to the emitter + const upstreams = [...depGraph.getLeafNodeAncestors(fp)] as FilePath[] + + if (action === "delete" && upstreams.length === 1) { + // if there's only one upstream, the destination is solely dependent on this file + destinationsToDelete.add(upstreams[0]) + } + + const upstreamContent = upstreams + // filter out non-markdown files + .filter((file) => contentMap.has(file)) + // if file was deleted, don't give it to the emitter + .filter((file) => !toRemove.has(file)) + .map((file) => contentMap.get(file)!) + + const emittedFps = await emitter.emit(ctx, upstreamContent, staticResources) + + if (ctx.argv.verbose) { + for (const file of emittedFps) { + console.log(`[emit:${emitter.name}] ${file}`) + } + } + + emittedFiles += emittedFps.length + } + } + + console.log(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`) + + // CLEANUP + // delete files that are solely dependent on this file + await rimraf([...destinationsToDelete]) + for (const file of toRemove) { + // remove from cache + contentMap.delete(file) + // remove the node from dependency graphs + Object.values(dependencies).forEach((depGraph) => depGraph?.removeNode(file)) + } + + toRemove.clear() + release() + clientRefresh() +} + async function rebuildFromEntrypoint( fp: string, - action: "add" | "change" | "delete", + action: FileEvent, clientRefresh: () => void, buildData: BuildData, // note: this function mutates buildData ) { @@ -190,7 +375,7 @@ async function rebuildFromEntrypoint( // 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 rimraf(path.join(argv.output, ".*"), { glob: true }) await emitContent(ctx, filteredContent) console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`)) } catch (err) { diff --git a/quartz/cfg.ts b/quartz/cfg.ts index 2107fa621..a413ad7f2 100644 --- a/quartz/cfg.ts +++ b/quartz/cfg.ts @@ -1,5 +1,6 @@ import { ValidDateType } from "./components/Date" import { QuartzComponent } from "./components/types" +import { ValidLocale } from "./i18n" import { PluginTypes } from "./plugins/types" import { SocialImageOptions } from "./util/imageHelper" import { Theme } from "./util/theme" @@ -42,11 +43,14 @@ export interface GlobalConfiguration { generateSocialImages: boolean | Partial theme: Theme /** - * The locale to use for date formatting. Default to "en-US" * Allow to translate the date in the language of your choice. - * Need to be formated following the IETF language tag format (https://en.wikipedia.org/wiki/IETF_language_tag) + * Also used for UI translation (default: en-US) + * Need to be formated following BCP 47: https://en.wikipedia.org/wiki/IETF_language_tag + * The first part is the language (en) and the second part is the script/region (US) + * Language Codes: https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes + * Region Codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 */ - locale?: string + locale: ValidLocale } export interface QuartzConfig { diff --git a/quartz/cli/args.js b/quartz/cli/args.js index 7ed5b078e..123d0ac55 100644 --- a/quartz/cli/args.js +++ b/quartz/cli/args.js @@ -71,6 +71,11 @@ export const BuildArgv = { default: false, describe: "run a local server to live-preview your Quartz", }, + fastRebuild: { + boolean: true, + default: false, + describe: "[experimental] rebuild only the changed files", + }, baseDir: { string: true, default: "", diff --git a/quartz/components/ArticleTitle.tsx b/quartz/components/ArticleTitle.tsx index 2484c946a..318aeb24e 100644 --- a/quartz/components/ArticleTitle.tsx +++ b/quartz/components/ArticleTitle.tsx @@ -1,7 +1,7 @@ -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import { classNames } from "../util/lang" -function ArticleTitle({ fileData, displayClass }: QuartzComponentProps) { +const ArticleTitle: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => { const title = fileData.frontmatter?.title if (title) { return

{title}

@@ -9,6 +9,7 @@ function ArticleTitle({ fileData, displayClass }: QuartzComponentProps) { return null } } + ArticleTitle.css = ` .article-title { margin: 2rem 0 0 0; diff --git a/quartz/components/Backlinks.tsx b/quartz/components/Backlinks.tsx index d5bdc0b95..aa412a2e0 100644 --- a/quartz/components/Backlinks.tsx +++ b/quartz/components/Backlinks.tsx @@ -1,14 +1,20 @@ -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import style from "./styles/backlinks.scss" import { resolveRelative, simplifySlug } from "../util/path" +import { i18n } from "../i18n" import { classNames } from "../util/lang" -function Backlinks({ fileData, allFiles, displayClass }: QuartzComponentProps) { +const Backlinks: QuartzComponent = ({ + fileData, + allFiles, + displayClass, + cfg, +}: QuartzComponentProps) => { const slug = simplifySlug(fileData.slug!) const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug)) return (
-

Backlinks

+

{i18n(cfg.locale).components.backlinks.title}

    {backlinkFiles.length > 0 ? ( backlinkFiles.map((f) => ( @@ -19,7 +25,7 @@ function Backlinks({ fileData, allFiles, displayClass }: QuartzComponentProps) { )) ) : ( -
  • No backlinks found
  • +
  • {i18n(cfg.locale).components.backlinks.noBacklinksFound}
  • )}
diff --git a/quartz/components/Body.tsx b/quartz/components/Body.tsx index fbb857293..96b627883 100644 --- a/quartz/components/Body.tsx +++ b/quartz/components/Body.tsx @@ -1,9 +1,9 @@ // @ts-ignore import clipboardScript from "./scripts/clipboard.inline" import clipboardStyle from "./styles/clipboard.scss" -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" -function Body({ children }: QuartzComponentProps) { +const Body: QuartzComponent = ({ children }: QuartzComponentProps) => { return
{children}
} diff --git a/quartz/components/Breadcrumbs.tsx b/quartz/components/Breadcrumbs.tsx index 3875f5e55..9ccfb9a6a 100644 --- a/quartz/components/Breadcrumbs.tsx +++ b/quartz/components/Breadcrumbs.tsx @@ -1,6 +1,6 @@ -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import breadcrumbsStyle from "./styles/breadcrumbs.scss" -import { FullSlug, SimpleSlug, resolveRelative } from "../util/path" +import { FullSlug, SimpleSlug, joinSegments, resolveRelative } from "../util/path" import { QuartzPluginData } from "../plugins/vfile" import { classNames } from "../util/lang" @@ -54,7 +54,11 @@ export default ((opts?: Partial) => { // computed index of folder name to its associated file data let folderIndex: Map | undefined - function Breadcrumbs({ fileData, allFiles, displayClass }: QuartzComponentProps) { + const Breadcrumbs: QuartzComponent = ({ + fileData, + allFiles, + displayClass, + }: QuartzComponentProps) => { // Hide crumbs on root if enabled if (options.hideOnRoot && fileData.slug === "index") { return <> @@ -68,13 +72,9 @@ export default ((opts?: Partial) => { folderIndex = new Map() // construct the index for the first time for (const file of allFiles) { - if (file.slug?.endsWith("index")) { - const folderParts = file.slug?.split("/") - // 2nd last to exclude the /index - const folderName = folderParts?.at(-2) - if (folderName) { - folderIndex.set(folderName, file) - } + const folderParts = file.slug?.split("/") + if (folderParts?.at(-1) === "index") { + folderIndex.set(folderParts.slice(0, -1).join("/"), file) } } } @@ -82,13 +82,17 @@ export default ((opts?: Partial) => { // Split slug into hierarchy/parts const slugParts = fileData.slug?.split("/") if (slugParts) { + // is tag breadcrumb? + const isTagPath = slugParts[0] === "tags" + // full path until current part let currentPath = "" + for (let i = 0; i < slugParts.length - 1; i++) { let curPathSegment = slugParts[i] // Try to resolve frontmatter folder title - const currentFile = folderIndex?.get(curPathSegment) + const currentFile = folderIndex?.get(slugParts.slice(0, i + 1).join("/")) if (currentFile) { const title = currentFile.frontmatter!.title if (title !== "index") { @@ -97,10 +101,15 @@ export default ((opts?: Partial) => { } // Add current slug to full path - currentPath += slugParts[i] + "/" + currentPath = joinSegments(currentPath, slugParts[i]) + const includeTrailingSlash = !isTagPath || i < 1 // Format and add current crumb - const crumb = formatCrumb(curPathSegment, fileData.slug!, currentPath as SimpleSlug) + const crumb = formatCrumb( + curPathSegment, + fileData.slug!, + (currentPath + (includeTrailingSlash ? "/" : "")) as SimpleSlug, + ) crumbs.push(crumb) } @@ -125,5 +134,6 @@ export default ((opts?: Partial) => { ) } Breadcrumbs.css = breadcrumbsStyle + return Breadcrumbs }) satisfies QuartzComponentConstructor diff --git a/quartz/components/ContentMeta.tsx b/quartz/components/ContentMeta.tsx index 6cd083e69..bcbe4285d 100644 --- a/quartz/components/ContentMeta.tsx +++ b/quartz/components/ContentMeta.tsx @@ -2,6 +2,7 @@ import { formatDate, getDate } from "./Date" import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import readingTime from "reading-time" import { classNames } from "../util/lang" +import { i18n } from "../i18n" interface ContentMetaOptions { /** @@ -30,8 +31,11 @@ export default ((opts?: Partial) => { // Display reading time if enabled if (options.showReadingTime) { - const { text: timeTaken, words: _words } = readingTime(text) - segments.push(timeTaken) + const { minutes, words: _words } = readingTime(text) + const displayedTime = i18n(cfg.locale).components.contentMeta.readingTime({ + minutes: Math.ceil(minutes), + }) + segments.push(displayedTime) } return

{segments.join(", ")}

diff --git a/quartz/components/Darkmode.tsx b/quartz/components/Darkmode.tsx index 6d10bb99e..8ed7c99b0 100644 --- a/quartz/components/Darkmode.tsx +++ b/quartz/components/Darkmode.tsx @@ -3,10 +3,11 @@ // see: https://v8.dev/features/modules#defer import darkmodeScript from "./scripts/darkmode.inline" import styles from "./styles/darkmode.scss" -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { i18n } from "../i18n" import { classNames } from "../util/lang" -function Darkmode({ displayClass }: QuartzComponentProps) { +const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { return (
@@ -22,7 +23,7 @@ function Darkmode({ displayClass }: QuartzComponentProps) { style="enable-background:new 0 0 35 35" xmlSpace="preserve" > - Light mode + {i18n(cfg.locale).components.themeToggle.darkMode} @@ -38,7 +39,7 @@ function Darkmode({ displayClass }: QuartzComponentProps) { style="enable-background:new 0 0 100 100" xmlSpace="preserve" > - Dark mode + {i18n(cfg.locale).components.themeToggle.lightMode} diff --git a/quartz/components/Date.tsx b/quartz/components/Date.tsx index 6feac178c..26b59647c 100644 --- a/quartz/components/Date.tsx +++ b/quartz/components/Date.tsx @@ -1,9 +1,10 @@ import { GlobalConfiguration } from "../cfg" +import { ValidLocale } from "../i18n" import { QuartzPluginData } from "../plugins/vfile" interface Props { date: Date - locale?: string + locale?: ValidLocale } export type ValidDateType = keyof Required["dates"] @@ -17,7 +18,7 @@ export function getDate(cfg: GlobalConfiguration, data: QuartzPluginData): Date return data.dates?.[cfg.defaultDateType] } -export function formatDate(d: Date, locale = "en-US"): string { +export function formatDate(d: Date, locale: ValidLocale = "en-US"): string { return d.toLocaleDateString(locale, { year: "numeric", month: "short", diff --git a/quartz/components/DesktopOnly.tsx b/quartz/components/DesktopOnly.tsx index a11f23fa8..fe2a27f14 100644 --- a/quartz/components/DesktopOnly.tsx +++ b/quartz/components/DesktopOnly.tsx @@ -3,7 +3,7 @@ import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } fro export default ((component?: QuartzComponent) => { if (component) { const Component = component - function DesktopOnly(props: QuartzComponentProps) { + const DesktopOnly: QuartzComponent = (props: QuartzComponentProps) => { return } diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx index e964c5a41..cffc079ef 100644 --- a/quartz/components/Explorer.tsx +++ b/quartz/components/Explorer.tsx @@ -1,4 +1,4 @@ -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import explorerStyle from "./styles/explorer.scss" // @ts-ignore @@ -6,10 +6,10 @@ import script from "./scripts/explorer.inline" import { ExplorerNode, FileNode, Options } from "./ExplorerNode" import { QuartzPluginData } from "../plugins/vfile" import { classNames } from "../util/lang" +import { i18n } from "../i18n" // Options interface defined in `ExplorerNode` to avoid circular dependency const defaultOptions = { - title: "Explorer", folderClickBehavior: "collapse", folderDefaultState: "collapsed", useSavedState: true, @@ -75,7 +75,12 @@ export default ((userOpts?: Partial) => { jsonTree = JSON.stringify(folders) } - function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) { + const Explorer: QuartzComponent = ({ + cfg, + allFiles, + displayClass, + fileData, + }: QuartzComponentProps) => { constructFileTree(allFiles) return (
@@ -87,7 +92,7 @@ export default ((userOpts?: Partial) => { data-savestate={opts.useSavedState} data-tree={jsonTree} > -

{opts.title}

+

{opts.title ?? i18n(cfg.locale).components.explorer.title}

} export default ((opts?: Options) => { - function Footer({ displayClass }: QuartzComponentProps) { + const Footer: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { const year = new Date().getFullYear() const links = opts?.links ?? [] return (

- Created with Quartz v{version}, © {year} + {i18n(cfg.locale).components.footer.createdWith}{" "} + Quartz v{version} © {year}

    {Object.entries(links).map(([text, link]) => ( diff --git a/quartz/components/Graph.tsx b/quartz/components/Graph.tsx index 756a46bb7..40ab43a2d 100644 --- a/quartz/components/Graph.tsx +++ b/quartz/components/Graph.tsx @@ -1,7 +1,8 @@ -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" // @ts-ignore import script from "./scripts/graph.inline" import style from "./styles/graph.scss" +import { i18n } from "../i18n" import { classNames } from "../util/lang" export interface D3Config { @@ -53,12 +54,12 @@ const defaultOptions: GraphOptions = { } export default ((opts?: GraphOptions) => { - function Graph({ displayClass }: QuartzComponentProps) { + const Graph: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph } const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph } return (
    -

    Graph View

    +

    {i18n(cfg.locale).components.graph.title}

    { const slug = fileData.filePath // since "/" is not a valid character in file names, replace with "-" const fileName = slug?.replaceAll("/", "-") - const title = fileData.frontmatter?.title ?? "Untitled" + const title = + fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title ?? "Untitled" // Get file description (priority: frontmatter > fileData > default) - const fdDescription = fileData.description?.trim() + const fdDescription = + fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description let description = "" if (fdDescription) { description = unescapeHTML(fdDescription) @@ -137,6 +140,12 @@ export default (() => { {title} + {cfg.theme.cdnCaching && ( + <> + + + + )} {/* OG/Twitter meta tags */} @@ -170,8 +179,6 @@ export default (() => { - - {css.map((href) => ( ))} diff --git a/quartz/components/Header.tsx b/quartz/components/Header.tsx index 5281f7296..eba17ae09 100644 --- a/quartz/components/Header.tsx +++ b/quartz/components/Header.tsx @@ -1,6 +1,6 @@ -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" -function Header({ children }: QuartzComponentProps) { +const Header: QuartzComponent = ({ children }: QuartzComponentProps) => { return children.length > 0 ?
    {children}
    : null } diff --git a/quartz/components/MobileOnly.tsx b/quartz/components/MobileOnly.tsx index 5a190957b..7d2108de7 100644 --- a/quartz/components/MobileOnly.tsx +++ b/quartz/components/MobileOnly.tsx @@ -3,7 +3,7 @@ import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } fro export default ((component?: QuartzComponent) => { if (component) { const Component = component - function MobileOnly(props: QuartzComponentProps) { + const MobileOnly: QuartzComponent = (props: QuartzComponentProps) => { return } diff --git a/quartz/components/PageList.tsx b/quartz/components/PageList.tsx index 644e354c4..62b77b17b 100644 --- a/quartz/components/PageList.tsx +++ b/quartz/components/PageList.tsx @@ -1,7 +1,7 @@ import { FullSlug, resolveRelative } from "../util/path" import { QuartzPluginData } from "../plugins/vfile" import { Date, getDate } from "./Date" -import { QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentProps } from "./types" import { GlobalConfiguration } from "../cfg" export function byDateAndAlphabetical( @@ -29,7 +29,7 @@ type Props = { limit?: number } & QuartzComponentProps -export function PageList({ cfg, fileData, allFiles, limit }: Props) { +export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit }: Props) => { let list = allFiles.sort(byDateAndAlphabetical(cfg)) if (limit) { list = list.slice(0, limit) diff --git a/quartz/components/PageTitle.tsx b/quartz/components/PageTitle.tsx index fb1660a7c..2362f1027 100644 --- a/quartz/components/PageTitle.tsx +++ b/quartz/components/PageTitle.tsx @@ -1,9 +1,10 @@ import { pathToRoot } from "../util/path" -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import { classNames } from "../util/lang" +import { i18n } from "../i18n" -function PageTitle({ fileData, cfg, displayClass }: QuartzComponentProps) { - const title = cfg?.pageTitle ?? "Untitled Quartz" +const PageTitle: QuartzComponent = ({ fileData, cfg, displayClass }: QuartzComponentProps) => { + const title = cfg?.pageTitle ?? i18n(cfg.locale).propertyDefaults.title const baseDir = pathToRoot(fileData.slug!) return (

    diff --git a/quartz/components/RecentNotes.tsx b/quartz/components/RecentNotes.tsx index 81b354d77..549b025d3 100644 --- a/quartz/components/RecentNotes.tsx +++ b/quartz/components/RecentNotes.tsx @@ -1,14 +1,15 @@ -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import { FullSlug, SimpleSlug, resolveRelative } from "../util/path" import { QuartzPluginData } from "../plugins/vfile" import { byDateAndAlphabetical } from "./PageList" import style from "./styles/recentNotes.scss" import { Date, getDate } from "./Date" import { GlobalConfiguration } from "../cfg" +import { i18n } from "../i18n" import { classNames } from "../util/lang" interface Options { - title: string + title?: string limit: number linkToMore: SimpleSlug | false filter: (f: QuartzPluginData) => boolean @@ -16,7 +17,6 @@ interface Options { } const defaultOptions = (cfg: GlobalConfiguration): Options => ({ - title: "Recent Notes", limit: 3, linkToMore: false, filter: () => true, @@ -24,16 +24,21 @@ const defaultOptions = (cfg: GlobalConfiguration): Options => ({ }) export default ((userOpts?: Partial) => { - function RecentNotes({ allFiles, fileData, displayClass, cfg }: QuartzComponentProps) { + const RecentNotes: QuartzComponent = ({ + allFiles, + fileData, + displayClass, + cfg, + }: QuartzComponentProps) => { const opts = { ...defaultOptions(cfg), ...userOpts } const pages = allFiles.filter(opts.filter).sort(opts.sort) const remaining = Math.max(0, pages.length - opts.limit) return (
    -

    {opts.title}

    +

    {opts.title ?? i18n(cfg.locale).components.recentNotes.title}

      {pages.slice(0, opts.limit).map((page) => { - const title = page.frontmatter?.title + const title = page.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title const tags = page.frontmatter?.tags ?? [] return ( @@ -70,7 +75,9 @@ export default ((userOpts?: Partial) => {
    {opts.linkToMore && remaining > 0 && (

    - See {remaining} more → + + {i18n(cfg.locale).components.recentNotes.seeRemainingMore({ remaining })} +

    )}
    diff --git a/quartz/components/Search.tsx b/quartz/components/Search.tsx index 239bc033b..01e5a353d 100644 --- a/quartz/components/Search.tsx +++ b/quartz/components/Search.tsx @@ -1,8 +1,9 @@ -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import style from "./styles/search.scss" // @ts-ignore import script from "./scripts/search.inline" import { classNames } from "../util/lang" +import { i18n } from "../i18n" export interface SearchOptions { enablePreview: boolean @@ -13,13 +14,13 @@ const defaultOptions: SearchOptions = { } export default ((userOpts?: Partial) => { - function Search({ displayClass }: QuartzComponentProps) { + const Search: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { const opts = { ...defaultOptions, ...userOpts } - + const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder return (
    -

    Search

    +

    {i18n(cfg.locale).components.search.title}

    ) => { id="search-bar" name="search" type="text" - aria-label="Search for something" - placeholder="Search for something" + aria-label={searchPlaceholder} + placeholder={searchPlaceholder} />
    diff --git a/quartz/components/TableOfContents.tsx b/quartz/components/TableOfContents.tsx index 167c837dd..77ff0eb10 100644 --- a/quartz/components/TableOfContents.tsx +++ b/quartz/components/TableOfContents.tsx @@ -1,10 +1,11 @@ -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import legacyStyle from "./styles/legacyToc.scss" import modernStyle from "./styles/toc.scss" import { classNames } from "../util/lang" // @ts-ignore import script from "./scripts/toc.inline" +import { i18n } from "../i18n" interface Options { layout: "modern" | "legacy" @@ -14,7 +15,11 @@ const defaultOptions: Options = { layout: "modern", } -function TableOfContents({ fileData, displayClass }: QuartzComponentProps) { +const TableOfContents: QuartzComponent = ({ + fileData, + displayClass, + cfg, +}: QuartzComponentProps) => { if (!fileData.toc) { return null } @@ -22,7 +27,7 @@ function TableOfContents({ fileData, displayClass }: QuartzComponentProps) { return (