This commit is contained in:
semanticdata 2024-02-07 08:10:03 -06:00
commit a830c8a1ad
48 changed files with 713 additions and 164 deletions

View File

@ -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!,

View File

@ -27,6 +27,7 @@ This part of the configuration concerns anything that can affect the whole site.
- `null`: don't use analytics;
- `{ provider: 'plausible' }`: use [Plausible](https://plausible.io/), a privacy-friendly alternative to Google Analytics; or
- `{ provider: 'google', tagId: <your-google-tag> }`: use Google Analytics
- `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.

18
docs/features/i18n.md Normal file
View File

@ -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`.

View File

@ -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

108
package-lock.json generated
View File

@ -1,17 +1,17 @@
{
"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",
"@napi-rs/simple-git": "0.1.16",
"async-mutex": "^0.4.1",
"chalk": "^5.3.0",
"chokidar": "^3.5.3",
@ -73,7 +73,7 @@
"@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.16",
"@types/pretty-time": "^1.1.5",
"@types/source-map-support": "^0.5.10",
"@types/ws": "^8.5.10",
@ -516,30 +516,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"
],
@ -552,9 +552,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"
],
@ -567,9 +567,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"
],
@ -582,9 +582,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"
],
@ -597,9 +597,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"
],
@ -612,9 +612,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"
],
@ -627,9 +627,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"
],
@ -642,9 +642,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"
],
@ -657,9 +657,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"
],
@ -672,9 +672,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"
],
@ -687,9 +687,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"
],
@ -1088,9 +1088,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.16",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.16.tgz",
"integrity": "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"

View File

@ -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 <j.zhao2k19@gmail.com>",
"license": "MIT",
@ -40,7 +40,7 @@
"dependencies": {
"@clack/prompts": "^0.7.0",
"@floating-ui/dom": "^1.6.1",
"@napi-rs/simple-git": "0.1.14",
"@napi-rs/simple-git": "0.1.16",
"async-mutex": "^0.4.1",
"chalk": "^5.3.0",
"chokidar": "^3.5.3",
@ -99,7 +99,7 @@
"@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.16",
"@types/pretty-time": "^1.1.5",
"@types/source-map-support": "^0.5.10",
"@types/ws": "^8.5.10",

View File

@ -50,6 +50,7 @@ const config: QuartzConfig = {
Plugin.SyntaxHighlighting(),
Plugin.ObsidianFlavoredMarkdown(),
Plugin.GitHubFlavoredMarkdown(),
Plugin.TableOfContents(),
Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
Plugin.Description(),
],

View File

@ -1,5 +1,6 @@
import { ValidDateType } from "./components/Date"
import { QuartzComponent } from "./components/types"
import { ValidLocale } from "./i18n"
import { PluginTypes } from "./plugins/types"
import { Theme } from "./util/theme"
@ -37,11 +38,14 @@ export interface GlobalConfiguration {
baseUrl?: string
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 {

View File

@ -9,6 +9,7 @@ function ArticleTitle({ fileData, displayClass }: QuartzComponentProps) {
return null
}
}
ArticleTitle.css = `
.article-title {
margin: 2rem 0 0 0;

View File

@ -1,14 +1,15 @@
import { 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) {
function Backlinks({ fileData, allFiles, displayClass, cfg }: QuartzComponentProps) {
const slug = simplifySlug(fileData.slug!)
const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
return (
<div class={classNames(displayClass, "backlinks")}>
<h3>Backlinks</h3>
<h3>{i18n(cfg.locale).components.backlinks.title}</h3>
<ul class="overflow">
{backlinkFiles.length > 0 ? (
backlinkFiles.map((f) => (
@ -19,7 +20,7 @@ function Backlinks({ fileData, allFiles, displayClass }: QuartzComponentProps) {
</li>
))
) : (
<li>No backlinks found</li>
<li>{i18n(cfg.locale).components.backlinks.noBacklinksFound}</li>
)}
</ul>
</div>

View File

@ -4,9 +4,10 @@
import darkmodeScript from "./scripts/darkmode.inline"
import styles from "./styles/darkmode.scss"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { i18n } from "../i18n"
import { classNames } from "../util/lang"
function Darkmode({ displayClass }: QuartzComponentProps) {
function Darkmode({ displayClass, cfg }: QuartzComponentProps) {
return (
<div class={classNames(displayClass, "darkmode")}>
<input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} />
@ -22,7 +23,7 @@ function Darkmode({ displayClass }: QuartzComponentProps) {
style="enable-background:new 0 0 35 35"
xmlSpace="preserve"
>
<title>Light mode</title>
<title>{i18n(cfg.locale).components.themeToggle.darkMode}</title>
<path d="M6,17.5C6,16.672,5.328,16,4.5,16h-3C0.672,16,0,16.672,0,17.5 S0.672,19,1.5,19h3C5.328,19,6,18.328,6,17.5z M7.5,26c-0.414,0-0.789,0.168-1.061,0.439l-2,2C4.168,28.711,4,29.086,4,29.5 C4,30.328,4.671,31,5.5,31c0.414,0,0.789-0.168,1.06-0.44l2-2C8.832,28.289,9,27.914,9,27.5C9,26.672,8.329,26,7.5,26z M17.5,6 C18.329,6,19,5.328,19,4.5v-3C19,0.672,18.329,0,17.5,0S16,0.672,16,1.5v3C16,5.328,16.671,6,17.5,6z M27.5,9 c0.414,0,0.789-0.168,1.06-0.439l2-2C30.832,6.289,31,5.914,31,5.5C31,4.672,30.329,4,29.5,4c-0.414,0-0.789,0.168-1.061,0.44 l-2,2C26.168,6.711,26,7.086,26,7.5C26,8.328,26.671,9,27.5,9z M6.439,8.561C6.711,8.832,7.086,9,7.5,9C8.328,9,9,8.328,9,7.5 c0-0.414-0.168-0.789-0.439-1.061l-2-2C6.289,4.168,5.914,4,5.5,4C4.672,4,4,4.672,4,5.5c0,0.414,0.168,0.789,0.439,1.06 L6.439,8.561z M33.5,16h-3c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5h3c0.828,0,1.5-0.672,1.5-1.5S34.328,16,33.5,16z M28.561,26.439C28.289,26.168,27.914,26,27.5,26c-0.828,0-1.5,0.672-1.5,1.5c0,0.414,0.168,0.789,0.439,1.06l2,2 C28.711,30.832,29.086,31,29.5,31c0.828,0,1.5-0.672,1.5-1.5c0-0.414-0.168-0.789-0.439-1.061L28.561,26.439z M17.5,29 c-0.829,0-1.5,0.672-1.5,1.5v3c0,0.828,0.671,1.5,1.5,1.5s1.5-0.672,1.5-1.5v-3C19,29.672,18.329,29,17.5,29z M17.5,7 C11.71,7,7,11.71,7,17.5S11.71,28,17.5,28S28,23.29,28,17.5S23.29,7,17.5,7z M17.5,25c-4.136,0-7.5-3.364-7.5-7.5 c0-4.136,3.364-7.5,7.5-7.5c4.136,0,7.5,3.364,7.5,7.5C25,21.636,21.636,25,17.5,25z"></path>
</svg>
</label>
@ -38,7 +39,7 @@ function Darkmode({ displayClass }: QuartzComponentProps) {
style="enable-background:new 0 0 100 100"
xmlSpace="preserve"
>
<title>Dark mode</title>
<title>{i18n(cfg.locale).components.themeToggle.lightMode}</title>
<path d="M96.76,66.458c-0.853-0.852-2.15-1.064-3.23-0.534c-6.063,2.991-12.858,4.571-19.655,4.571 C62.022,70.495,50.88,65.88,42.5,57.5C29.043,44.043,25.658,23.536,34.076,6.47c0.532-1.08,0.318-2.379-0.534-3.23 c-0.851-0.852-2.15-1.064-3.23-0.534c-4.918,2.427-9.375,5.619-13.246,9.491c-9.447,9.447-14.65,22.008-14.65,35.369 c0,13.36,5.203,25.921,14.65,35.368s22.008,14.65,35.368,14.65c13.361,0,25.921-5.203,35.369-14.65 c3.872-3.871,7.064-8.328,9.491-13.246C97.826,68.608,97.611,67.309,96.76,66.458z"></path>
</svg>
</label>

View File

@ -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<QuartzPluginData>["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",

View File

@ -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,7 @@ export default ((userOpts?: Partial<Options>) => {
jsonTree = JSON.stringify(folders)
}
function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) {
function Explorer({ cfg, allFiles, displayClass, fileData }: QuartzComponentProps) {
constructFileTree(allFiles)
return (
<div class={classNames(displayClass, "explorer")}>
@ -87,7 +87,7 @@ export default ((userOpts?: Partial<Options>) => {
data-savestate={opts.useSavedState}
data-tree={jsonTree}
>
<h1>{opts.title}</h1>
<h1>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h1>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"

View File

@ -12,7 +12,7 @@ import {
type OrderEntries = "sort" | "filter" | "map"
export interface Options {
title: string
title?: string
folderDefaultState: "collapsed" | "open"
folderClickBehavior: "collapse" | "link"
useSavedState: boolean

View File

@ -1,13 +1,14 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import style from "./styles/footer.scss"
import { version } from "../../package.json"
import { i18n } from "../i18n"
interface Options {
links: Record<string, string>
}
export default ((opts?: Options) => {
function Footer({ displayClass }: QuartzComponentProps) {
function Footer({ displayClass, cfg }: QuartzComponentProps) {
const year = new Date().getFullYear()
const links = opts?.links ?? []
return (

View File

@ -2,6 +2,7 @@ import { 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) {
function Graph({ displayClass, cfg }: QuartzComponentProps) {
const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph }
const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph }
return (
<div class={classNames(displayClass, "graph")}>
<h3>Graph View</h3>
<h3>{i18n(cfg.locale).components.graph.title}</h3>
<div class="graph-outer">
<div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
<svg

View File

@ -1,11 +1,13 @@
import { i18n } from "../i18n"
import { FullSlug, _stripSlashes, joinSegments, pathToRoot } from "../util/path"
import { JSResourceToScriptElement } from "../util/resources"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
export default (() => {
function Head({ cfg, fileData, externalResources }: QuartzComponentProps) {
const title = fileData.frontmatter?.title ?? "Untitled"
const description = fileData.description?.trim() ?? "No description provided"
const title = fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title
const description =
fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description
const { css, js } = externalResources
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)

View File

@ -1,9 +1,10 @@
import { pathToRoot } from "../util/path"
import { 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 title = cfg?.pageTitle ?? i18n(cfg.locale).propertyDefaults.title
const baseDir = pathToRoot(fileData.slug!)
return (
<h1 class={classNames(displayClass, "page-title")}>

View File

@ -5,10 +5,11 @@ 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,
@ -30,10 +30,10 @@ export default ((userOpts?: Partial<Options>) => {
const remaining = Math.max(0, pages.length - opts.limit)
return (
<div class={classNames(displayClass, "recent-notes")}>
<h3>{opts.title}</h3>
<h3>{opts.title ?? i18n(cfg.locale).components.recentNotes.title}</h3>
<ul class="recent-ul">
{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 +70,9 @@ export default ((userOpts?: Partial<Options>) => {
</ul>
{opts.linkToMore && remaining > 0 && (
<p>
<a href={resolveRelative(fileData.slug!, opts.linkToMore)}>See {remaining} more </a>
<a href={resolveRelative(fileData.slug!, opts.linkToMore)}>
{i18n(cfg.locale).components.recentNotes.seeRemainingMore({ remaining })}
</a>
</p>
)}
</div>

View File

@ -3,6 +3,7 @@ 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,9 +14,9 @@ const defaultOptions: SearchOptions = {
}
export default ((userOpts?: Partial<SearchOptions>) => {
function Search({ displayClass }: QuartzComponentProps) {
function Search({ displayClass, cfg }: QuartzComponentProps) {
const opts = { ...defaultOptions, ...userOpts }
const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder
return (
<div class={classNames(displayClass, "search")}>
<div id="search-icon">
@ -43,8 +44,8 @@ export default ((userOpts?: Partial<SearchOptions>) => {
id="search-bar"
name="search"
type="text"
aria-label="Search for something"
placeholder="Search for something"
aria-label={searchPlaceholder}
placeholder={searchPlaceholder}
/>
<div id="search-layout" data-preview={opts.enablePreview}></div>
</div>

View File

@ -5,6 +5,7 @@ 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,7 @@ const defaultOptions: Options = {
layout: "modern",
}
function TableOfContents({ fileData, displayClass }: QuartzComponentProps) {
function TableOfContents({ fileData, displayClass, cfg }: QuartzComponentProps) {
if (!fileData.toc) {
return null
}
@ -22,7 +23,7 @@ function TableOfContents({ fileData, displayClass }: QuartzComponentProps) {
return (
<div class={classNames(displayClass, "toc")}>
<button type="button" id="toc" class={fileData.collapseToc ? "collapsed" : ""}>
<h3>Table of Contents</h3>
<h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
@ -55,15 +56,14 @@ function TableOfContents({ fileData, displayClass }: QuartzComponentProps) {
TableOfContents.css = modernStyle
TableOfContents.afterDOMLoaded = script
function LegacyTableOfContents({ fileData }: QuartzComponentProps) {
function LegacyTableOfContents({ fileData, cfg }: QuartzComponentProps) {
if (!fileData.toc) {
return null
}
return (
<details id="toc" open={!fileData.collapseToc}>
<summary>
<h3>Table of Contents</h3>
<h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
</summary>
<ul>
{fileData.toc.map((tocEntry) => (

View File

@ -1,10 +1,11 @@
import { QuartzComponentConstructor } from "../types"
import { i18n } from "../../i18n"
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
function NotFound() {
function NotFound({ cfg }: QuartzComponentProps) {
return (
<article class="popover-hint">
<h1>404</h1>
<p>Either this page is private or doesn't exist.</p>
<p>{i18n(cfg.locale).pages.error.notFound}</p>
</article>
)
}

View File

@ -5,8 +5,8 @@ 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"
import { htmlToJsx } from "../../util/jsx"
import { i18n } from "../../i18n"
interface FolderContentOptions {
/**
@ -23,7 +23,7 @@ export default ((opts?: Partial<FolderContentOptions>) => {
const options: FolderContentOptions = { ...defaultOptions, ...opts }
function FolderContent(props: QuartzComponentProps) {
const { tree, fileData, allFiles } = props
const { tree, fileData, allFiles, cfg } = props
const folderSlug = _stripSlashes(simplifySlug(fileData.slug!))
const allPagesInFolder = allFiles.filter((file) => {
const fileSlug = _stripSlashes(simplifySlug(file.slug!))
@ -52,7 +52,11 @@ export default ((opts?: Partial<FolderContentOptions>) => {
</article>
<div class="page-listing">
{options.showFolderCount && (
<p>{pluralize(allPagesInFolder.length, "item")} under this folder.</p>
<p>
{i18n(cfg.locale).pages.folderContent.itemsUnderFolder({
count: allPagesInFolder.length,
})}
</p>
)}
<div>
<PageList {...listProps} />

View File

@ -4,12 +4,12 @@ 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"
import { htmlToJsx } from "../../util/jsx"
import { i18n } from "../../i18n"
const numPages = 10
function TagContent(props: QuartzComponentProps) {
const { tree, fileData, allFiles } = props
const { tree, fileData, allFiles, cfg } = props
const slug = fileData.slug
if (!(slug?.startsWith("tags/") || slug === "tags")) {
@ -43,7 +43,7 @@ function TagContent(props: QuartzComponentProps) {
<article>
<p>{content}</p>
</article>
<p>Found {tags.length} total tags.</p>
<p>{i18n(cfg.locale).pages.tagContent.totalTags({ count: tags.length })}</p>
<div>
{tags.map((tag) => {
const pages = tagItemMap.get(tag)!
@ -64,8 +64,12 @@ function TagContent(props: QuartzComponentProps) {
{content && <p>{content}</p>}
<div class="page-listing">
<p>
{pluralize(pages.length, "item")} with this tag.{" "}
{pages.length > numPages && `Showing first ${numPages}.`}
{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}
{pages.length > numPages && (
<span>
{i18n(cfg.locale).pages.tagContent.showingFirst({ count: numPages })}
</span>
)}
</p>
<PageList limit={numPages} {...listProps} />
</div>
@ -86,7 +90,7 @@ function TagContent(props: QuartzComponentProps) {
<div class={classes}>
<article>{content}</article>
<div class="page-listing">
<p>{pluralize(pages.length, "item")} with this tag.</p>
<p>{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}</p>
<div>
<PageList {...listProps} />
</div>

View File

@ -7,6 +7,8 @@ import { FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../ut
import { visit } from "unist-util-visit"
import { Root, Element, ElementContent } from "hast"
import { QuartzPluginData } from "../plugins/vfile"
import { GlobalConfiguration } from "../cfg"
import { i18n } from "../i18n"
interface RenderComponents {
head: QuartzComponent
@ -63,6 +65,7 @@ function getOrComputeFileIndex(allFiles: QuartzPluginData[]): Map<FullSlug, Quar
}
export function renderPage(
cfg: GlobalConfiguration,
slug: FullSlug,
componentData: QuartzComponentProps,
components: RenderComponents,
@ -101,7 +104,9 @@ export function renderPage(
type: "element",
tagName: "a",
properties: { href: inner.properties?.href, class: ["internal"] },
children: [{ type: "text", value: `Link to original` }],
children: [
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
],
},
]
}
@ -136,7 +141,9 @@ export function renderPage(
type: "element",
tagName: "a",
properties: { href: inner.properties?.href, class: ["internal"] },
children: [{ type: "text", value: `Link to original` }],
children: [
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
],
},
]
} else if (page.htmlAst) {
@ -147,7 +154,14 @@ export function renderPage(
tagName: "h1",
properties: {},
children: [
{ type: "text", value: page.frontmatter?.title ?? `Transclude of ${page.slug}` },
{
type: "text",
value:
page.frontmatter?.title ??
i18n(cfg.locale).components.transcludes.transcludeOf({
targetSlug: page.slug!,
}),
},
],
},
...(page.htmlAst.children as ElementContent[]).map((child) =>
@ -157,7 +171,9 @@ export function renderPage(
type: "element",
tagName: "a",
properties: { href: inner.properties?.href, class: ["internal"] },
children: [{ type: "text", value: `Link to original` }],
children: [
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
],
},
]
}

View File

@ -0,0 +1,23 @@
import { getFullSlug } from "../../util/path"
const checkboxId = (index: number) => `${getFullSlug(window)}-checkbox-${index}`
document.addEventListener("nav", () => {
const checkboxes = document.querySelectorAll(
"input.checkbox-toggle",
) as NodeListOf<HTMLInputElement>
checkboxes.forEach((el, index) => {
const elId = checkboxId(index)
const switchState = (e: Event) => {
const newCheckboxState = (e.target as HTMLInputElement)?.checked ? "true" : "false"
localStorage.setItem(elId, newCheckboxState)
}
el.addEventListener("change", switchState)
window.addCleanup(() => el.removeEventListener("change", switchState))
if (localStorage.getItem(elId) === "true") {
el.checked = true
}
})
})

View File

@ -306,7 +306,13 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
itemTile.classList.add("result-card")
itemTile.id = slug
itemTile.href = resolveUrl(slug).toString()
itemTile.innerHTML = `<h3>${title}</h3>${htmlTags}<p class="preview">${content}</p>`
itemTile.innerHTML = `<h3>${title}</h3>${htmlTags}${
enablePreview && window.innerWidth > 600 ? "" : `<p>${content}</p>`
}`
itemTile.addEventListener("click", (event) => {
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
hideSearch()
})
const handler = (event: MouseEvent) => {
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return

View File

@ -62,6 +62,10 @@
margin-left: auto;
margin-right: auto;
@media all and (max-width: $fullPageWidth) {
width: 90%;
}
& > * {
width: 100%;
border-radius: 7px;
@ -158,6 +162,10 @@
margin: 0 auto;
width: min($pageWidth, 100%);
}
a[role="anchor"] {
background-color: transparent;
}
}
& > #results-container {

17
quartz/i18n/index.ts Normal file
View File

@ -0,0 +1,17 @@
import { Translation } from "./locales/definition"
import en from "./locales/en-US"
import fr from "./locales/fr-FR"
import ja from "./locales/ja-JP"
import de from "./locales/de-DE"
import nl from "./locales/nl-NL"
export const TRANSLATIONS = {
"en-US": en,
"fr-FR": fr,
"ja-JP": ja,
"de-DE": de,
"nl-NL": nl,
} as const
export const i18n = (locale: ValidLocale): Translation => TRANSLATIONS[locale ?? "en-US"]
export type ValidLocale = keyof typeof TRANSLATIONS

View File

@ -0,0 +1,65 @@
import { Translation } from "./definition"
export default {
propertyDefaults: {
title: "Unbenannt",
description: "Keine Beschreibung angegeben",
},
components: {
backlinks: {
title: "Backlinks",
noBacklinksFound: "Keine Backlinks gefunden",
},
themeToggle: {
lightMode: "Light Mode",
darkMode: "Dark Mode",
},
explorer: {
title: "Explorer",
},
footer: {
createdWith: "Erstellt mit",
},
graph: {
title: "Graphansicht",
},
recentNotes: {
title: "Zuletzt bearbeitete Seiten",
seeRemainingMore: ({ remaining }) => `${remaining} weitere ansehen →`,
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Transklusion von ${targetSlug}`,
linkToOriginal: "Link zum Original",
},
search: {
title: "Suche",
searchBarPlaceholder: "Suche nach etwas",
},
tableOfContents: {
title: "Inhaltsverzeichnis",
},
},
pages: {
rss: {
recentNotes: "Zuletzt bearbeitete Seiten",
lastFewNotes: ({ count }) => `Letzte ${count} Seiten`,
},
error: {
title: "Nicht gefunden",
notFound: "Diese Seite ist entweder nicht öffentlich oder existiert nicht.",
},
folderContent: {
folder: "Ordner",
itemsUnderFolder: ({ count }) =>
count === 1 ? "1 Datei in diesem Ordner" : `${count} Dateien in diesem Ordner.`,
},
tagContent: {
tag: "Tag",
tagIndex: "Tag-Übersicht",
itemsUnderTag: ({ count }) =>
count === 1 ? "1 Datei mit diesem Tag" : `${count} Dateien mit diesem Tag.`,
showingFirst: ({ count }) => `Die ersten ${count} Tags werden angezeigt.`,
totalTags: ({ count }) => `${count} Tags insgesamt.`,
},
},
} as const satisfies Translation

View File

@ -0,0 +1,63 @@
import { FullSlug } from "../../util/path"
export interface Translation {
propertyDefaults: {
title: string
description: string
}
components: {
backlinks: {
title: string
noBacklinksFound: string
}
themeToggle: {
lightMode: string
darkMode: string
}
explorer: {
title: string
}
footer: {
createdWith: string
}
graph: {
title: string
}
recentNotes: {
title: string
seeRemainingMore: (variables: { remaining: number }) => string
}
transcludes: {
transcludeOf: (variables: { targetSlug: FullSlug }) => string
linkToOriginal: string
}
search: {
title: string
searchBarPlaceholder: string
}
tableOfContents: {
title: string
}
}
pages: {
rss: {
recentNotes: string
lastFewNotes: (variables: { count: number }) => string
}
error: {
title: string
notFound: string
}
folderContent: {
folder: string
itemsUnderFolder: (variables: { count: number }) => string
}
tagContent: {
tag: string
tagIndex: string
itemsUnderTag: (variables: { count: number }) => string
showingFirst: (variables: { count: number }) => string
totalTags: (variables: { count: number }) => string
}
}
}

View File

@ -0,0 +1,65 @@
import { Translation } from "./definition"
export default {
propertyDefaults: {
title: "Untitled",
description: "No description provided",
},
components: {
backlinks: {
title: "Backlinks",
noBacklinksFound: "No backlinks found",
},
themeToggle: {
lightMode: "Light mode",
darkMode: "Dark mode",
},
explorer: {
title: "Explorer",
},
footer: {
createdWith: "Created with",
},
graph: {
title: "Graph View",
},
recentNotes: {
title: "Recent Notes",
seeRemainingMore: ({ remaining }) => `See ${remaining} more →`,
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Transclude of ${targetSlug}`,
linkToOriginal: "Link to original",
},
search: {
title: "Search",
searchBarPlaceholder: "Search for something",
},
tableOfContents: {
title: "Table of Contents",
},
},
pages: {
rss: {
recentNotes: "Recent notes",
lastFewNotes: ({ count }) => `Last ${count} notes`,
},
error: {
title: "Not Found",
notFound: "Either this page is private or doesn't exist.",
},
folderContent: {
folder: "Folder",
itemsUnderFolder: ({ count }) =>
count === 1 ? "1 item under this folder" : `${count} items under this folder.`,
},
tagContent: {
tag: "Tag",
tagIndex: "Tag Index",
itemsUnderTag: ({ count }) =>
count === 1 ? "1 item with this tag" : `${count} items with this tag.`,
showingFirst: ({ count }) => `Showing first ${count} tags.`,
totalTags: ({ count }) => `Found ${count} total tags.`,
},
},
} as const satisfies Translation

View File

@ -0,0 +1,65 @@
import { Translation } from "./definition"
export default {
propertyDefaults: {
title: "Sans titre",
description: "Aucune description fournie",
},
components: {
backlinks: {
title: "Liens retour",
noBacklinksFound: "Aucun lien retour trouvé",
},
themeToggle: {
lightMode: "Mode clair",
darkMode: "Mode sombre",
},
explorer: {
title: "Explorateur",
},
footer: {
createdWith: "Créé avec",
},
graph: {
title: "Vue Graphique",
},
recentNotes: {
title: "Notes Récentes",
seeRemainingMore: ({ remaining }) => `Voir ${remaining} de plus →`,
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Transclusion de ${targetSlug}`,
linkToOriginal: "Lien vers l'original",
},
search: {
title: "Recherche",
searchBarPlaceholder: "Rechercher quelque chose",
},
tableOfContents: {
title: "Table des Matières",
},
},
pages: {
rss: {
recentNotes: "Notes récentes",
lastFewNotes: ({ count }) => `Les dernières ${count} notes`,
},
error: {
title: "Pas trouvé",
notFound: "Cette page est soit privée, soit elle n'existe pas.",
},
folderContent: {
folder: "Dossier",
itemsUnderFolder: ({ count }) =>
count === 1 ? "1 élément sous ce dossier" : `${count} éléments sous ce dossier.`,
},
tagContent: {
tag: "Étiquette",
tagIndex: "Index des étiquettes",
itemsUnderTag: ({ count }) =>
count === 1 ? "1 élément avec cette étiquette" : `${count} éléments avec cette étiquette.`,
showingFirst: ({ count }) => `Affichage des premières ${count} étiquettes.`,
totalTags: ({ count }) => `Trouvé ${count} étiquettes au total.`,
},
},
} as const satisfies Translation

View File

@ -0,0 +1,63 @@
import { Translation } from "./definition"
export default {
propertyDefaults: {
title: "無題",
description: "説明なし",
},
components: {
backlinks: {
title: "バックリンク",
noBacklinksFound: "バックリンクはありません",
},
themeToggle: {
lightMode: "ライトモード",
darkMode: "ダークモード",
},
explorer: {
title: "エクスプローラー",
},
footer: {
createdWith: "作成",
},
graph: {
title: "グラフビュー",
},
recentNotes: {
title: "最近の記事",
seeRemainingMore: ({ remaining }) => `さらに${remaining}件 →`,
},
transcludes: {
transcludeOf: ({ targetSlug }) => `${targetSlug}のまとめ`,
linkToOriginal: "元記事へのリンク",
},
search: {
title: "検索",
searchBarPlaceholder: "検索ワードを入力",
},
tableOfContents: {
title: "目次",
},
},
pages: {
rss: {
recentNotes: "最近の記事",
lastFewNotes: ({ count }) => `最新の${count}`,
},
error: {
title: "Not Found",
notFound: "ページが存在しないか、非公開設定になっています。",
},
folderContent: {
folder: "フォルダ",
itemsUnderFolder: ({ count }) => `${count}件のページ`,
},
tagContent: {
tag: "タグ",
tagIndex: "タグ一覧",
itemsUnderTag: ({ count }) => `${count}件のページ`,
showingFirst: ({ count }) => `のうち最初の${count}件を表示しています`,
totalTags: ({ count }) => `${count}個のタグを表示中`,
},
},
} as const satisfies Translation

View File

@ -0,0 +1,66 @@
import { Translation } from "./definition"
export default {
propertyDefaults: {
title: "Naamloos",
description: "Geen beschrijving gegeven.",
},
components: {
backlinks: {
title: "Backlinks",
noBacklinksFound: "Geen backlinks gevonden",
},
themeToggle: {
lightMode: "Lichte modus",
darkMode: "Donkere modus",
},
explorer: {
title: "Verkenner",
},
footer: {
createdWith: "Gemaakt met",
},
graph: {
title: "Grafiekweergave",
},
recentNotes: {
title: "Recente notities",
seeRemainingMore: ({ remaining }) => `Zie ${remaining} meer →`,
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Invoeging van ${targetSlug}`,
linkToOriginal: "Link naar origineel",
},
search: {
title: "Zoeken",
searchBarPlaceholder: "Doorzoek de website",
},
tableOfContents: {
title: "Inhoudsopgave",
},
},
pages: {
rss: {
recentNotes: "Recente notities",
lastFewNotes: ({ count }) => `Laatste ${count} notities`,
},
error: {
title: "Niet gevonden",
notFound: "Deze pagina is niet zichtbaar of bestaat niet.",
},
folderContent: {
folder: "Map",
itemsUnderFolder: ({ count }) =>
count === 1 ? "1 item in deze map" : `${count} items in deze map.`,
},
tagContent: {
tag: "Label",
tagIndex: "Label-index",
itemsUnderTag: ({ count }) =>
count === 1 ? "1 item met dit label." : `${count} items met dit label.`,
showingFirst: ({ count }) =>
count === 1 ? "Eerste label tonen." : `Eerste ${count} labels tonen.`,
totalTags: ({ count }) => `${count} labels gevonden.`,
},
},
} as const satisfies Translation

View File

@ -8,6 +8,7 @@ import { sharedPageComponents } from "../../../quartz.layout"
import { NotFound } from "../../components"
import { defaultProcessedContent } from "../vfile"
import { write } from "./helpers"
import { i18n } from "../../i18n"
export const NotFoundPage: QuartzEmitterPlugin = () => {
const opts: FullPageLayout = {
@ -33,11 +34,12 @@ export const NotFoundPage: QuartzEmitterPlugin = () => {
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
const path = url.pathname as FullSlug
const externalResources = pageResources(path, resources)
const notFound = i18n(cfg.locale).pages.error.title
const [tree, vfile] = defaultProcessedContent({
slug,
text: "Not Found",
description: "Not Found",
frontmatter: { title: "Not Found", tags: [] },
text: notFound,
description: notFound,
frontmatter: { title: notFound, tags: [] },
})
const componentData: QuartzComponentProps = {
fileData: vfile.data,
@ -51,7 +53,7 @@ export const NotFoundPage: QuartzEmitterPlugin = () => {
return [
await write({
ctx,
content: renderPage(slug, componentData, opts, externalResources),
content: renderPage(cfg, slug, componentData, opts, externalResources),
slug,
ext: ".html",
}),

View File

@ -6,6 +6,7 @@ import { FilePath, FullSlug, SimpleSlug, joinSegments, simplifySlug } from "../.
import { QuartzEmitterPlugin } from "../types"
import { toHtml } from "hast-util-to-html"
import { write } from "./helpers"
import { i18n } from "../../i18n"
export type ContentIndex = Map<FullSlug, ContentDetails>
export type ContentDetails = {
@ -38,7 +39,7 @@ function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string {
const base = cfg.baseUrl ?? ""
const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<url>
<loc>https://${joinSegments(base, encodeURI(slug))}</loc>
<lastmod>${content.date?.toISOString()}</lastmod>
${content.date && `<lastmod>${content.date.toISOString()}</lastmod>`}
</url>`
const urls = Array.from(idx)
.map(([slug, content]) => createURLEntry(simplifySlug(slug), content))
@ -78,7 +79,7 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: nu
<channel>
<title>${escapeHTML(cfg.pageTitle)}</title>
<link>https://${base}</link>
<description>${!!limit ? `Last ${limit} notes` : "Recent notes"} on ${escapeHTML(
<description>${!!limit ? i18n(cfg.locale).pages.rss.lastFewNotes({ count: limit }) : i18n(cfg.locale).pages.rss.recentNotes} on ${escapeHTML(
cfg.pageTitle,
)}</description>
<generator>Quartz -- quartz.jzhao.xyz</generator>

View File

@ -49,7 +49,7 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
allFiles,
}
const content = renderPage(slug, componentData, opts, externalResources)
const content = renderPage(cfg, slug, componentData, opts, externalResources)
const fp = await write({
ctx,
content,

View File

@ -18,6 +18,7 @@ import {
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
import { FolderContent } from "../../components"
import { write } from "./helpers"
import { i18n } from "../../i18n"
export const FolderPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
const opts: FullPageLayout = {
@ -57,7 +58,10 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpt
folder,
defaultProcessedContent({
slug: joinSegments(folder, "index") as FullSlug,
frontmatter: { title: `Folder: ${folder}`, tags: [] },
frontmatter: {
title: `${i18n(cfg.locale).pages.folderContent.folder}: ${folder}`,
tags: [],
},
}),
]),
)
@ -82,7 +86,7 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpt
allFiles,
}
const content = renderPage(slug, componentData, opts, externalResources)
const content = renderPage(cfg, slug, componentData, opts, externalResources)
const fp = await write({
ctx,
content,

View File

@ -15,6 +15,7 @@ import {
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
import { TagContent } from "../../components"
import { write } from "./helpers"
import { i18n } from "../../i18n"
export const TagPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
const opts: FullPageLayout = {
@ -47,7 +48,10 @@ export const TagPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts)
const tagDescriptions: Record<string, ProcessedContent> = Object.fromEntries(
[...tags].map((tag) => {
const title = tag === "index" ? "Tag Index" : `Tag: #${tag}`
const title =
tag === "index"
? i18n(cfg.locale).pages.tagContent.tagIndex
: `${i18n(cfg.locale).pages.tagContent.tag}: #${tag}`
return [
tag,
defaultProcessedContent({
@ -81,7 +85,7 @@ export const TagPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts)
allFiles,
}
const content = renderPage(slug, componentData, opts, externalResources)
const content = renderPage(cfg, slug, componentData, opts, externalResources)
const fp = await write({
ctx,
content,

View File

@ -5,6 +5,7 @@ import yaml from "js-yaml"
import toml from "toml"
import { slugTag } from "../../util/path"
import { QuartzPluginData } from "../vfile"
import { i18n } from "../../i18n"
export interface Options {
delims: string | string[]
@ -43,7 +44,7 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined>
const opts = { ...defaultOptions, ...userOpts }
return {
name: "FrontMatter",
markdownPlugins() {
markdownPlugins({ cfg }) {
return [
[remarkFrontmatter, ["yaml", "toml"]],
() => {
@ -59,7 +60,7 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined>
if (data.title) {
data.title = data.title.toString()
} else if (data.title === null || data.title === undefined) {
data.title = file.stem ?? "Untitled"
data.title = file.stem ?? i18n(cfg.configuration.locale).propertyDefaults.title
}
const tags = coerceToArray(coalesceAliases(data, ["tags", "tag"]))

View File

@ -32,6 +32,7 @@ export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> |
{
behavior: "append",
properties: {
role: "anchor",
ariaHidden: true,
tabIndex: -1,
"data-no-popover": true,

View File

@ -9,6 +9,8 @@ import path from "path"
import { JSResource } from "../../util/resources"
// @ts-ignore
import calloutScript from "../../components/scripts/callout.inline.ts"
// @ts-ignore
import checkboxScript from "../../components/scripts/checkbox.inline.ts"
import { FilePath, pathToRoot, slugTag, slugifyFilePath } from "../../util/path"
import { toHast } from "mdast-util-to-hast"
import { toHtml } from "hast-util-to-html"
@ -28,6 +30,7 @@ export interface Options {
enableInHtmlEmbed: boolean
enableYouTubeEmbed: boolean
enableVideoEmbed: boolean
enableCheckbox: boolean
}
const defaultOptions: Options = {
@ -42,6 +45,7 @@ const defaultOptions: Options = {
enableInHtmlEmbed: false,
enableYouTubeEmbed: true,
enableVideoEmbed: true,
enableCheckbox: false,
}
const calloutMapping = {
@ -74,6 +78,17 @@ const calloutMapping = {
cite: "quote",
} as const
const arrowMapping: Record<string, string> = {
"->": "&rarr;",
"-->": "&rArr;",
"=>": "&rArr;",
"==>": "&rArr;",
"<-": "&larr;",
"<--": "&lArr;",
"<=": "&lArr;",
"<==": "&lArr;",
}
function canonicalizeCallout(calloutName: string): keyof typeof calloutMapping {
const normalizedCallout = calloutName.toLowerCase() as keyof typeof calloutMapping
// if callout is not recognized, make it a custom one
@ -82,7 +97,7 @@ function canonicalizeCallout(calloutName: string): keyof typeof calloutMapping {
export const externalLinkRegex = /^https?:\/\//i
export const arrowRegex = new RegExp(/-{1,2}>/, "g")
export const arrowRegex = new RegExp(/(-{1,2}>|={1,2}>|<-{1,2}|<={1,2})/, "g")
// !? -> optional embedding
// \[\[ -> open brace
@ -271,10 +286,12 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
if (opts.parseArrows) {
replacements.push([
arrowRegex,
(_value: string, ..._capture: string[]) => {
(value: string, ..._capture: string[]) => {
const maybeArrow = arrowMapping[value]
if (maybeArrow === undefined) return SKIP
return {
type: "html",
value: `<span>&rarr;</span>`,
value: `<span>${maybeArrow}</span>`,
}
},
])
@ -541,11 +558,37 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
})
}
if (opts.enableCheckbox) {
plugins.push(() => {
return (tree: HtmlRoot, _file) => {
visit(tree, "element", (node) => {
if (node.tagName === "input" && node.properties.type === "checkbox") {
const isChecked = node.properties?.checked ?? false
node.properties = {
type: "checkbox",
disabled: false,
checked: isChecked,
class: "checkbox-toggle",
}
}
})
}
})
}
return plugins
},
externalResources() {
const js: JSResource[] = []
if (opts.enableCheckbox) {
js.push({
script: checkboxScript,
loadTime: "afterDOMReady",
contentType: "inline",
})
}
if (opts.callouts) {
js.push({
script: calloutScript,
@ -557,17 +600,22 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
if (opts.mermaid) {
js.push({
script: `
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs';
let mermaidImport = undefined
document.addEventListener('nav', async () => {
if (document.querySelector("code.mermaid")) {
mermaidImport ||= await import('https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs')
const mermaid = mermaidImport.default
const darkMode = document.documentElement.getAttribute('saved-theme') === 'dark'
mermaid.initialize({
startOnLoad: false,
securityLevel: 'loose',
theme: darkMode ? 'dark' : 'default'
});
document.addEventListener('nav', async () => {
})
await mermaid.run({
querySelector: '.mermaid'
})
}
});
`,
loadTime: "afterDOMReady",

View File

@ -3,7 +3,6 @@ import { Root } from "mdast"
import { visit } from "unist-util-visit"
import { toString } from "mdast-util-to-string"
import Slugger from "github-slugger"
import { wikilinkRegex } from "./ofm"
export interface Options {
maxDepth: 1 | 2 | 3 | 4 | 5 | 6
@ -25,7 +24,7 @@ interface TocEntry {
slug: string // this is just the anchor (#some-slug), not the canonical slug
}
const regexMdLinks = new RegExp(/\[([^\[]+)\](\(.*\))/, "g")
const slugAnchor = new Slugger()
export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefined> = (
userOpts,
) => {
@ -38,21 +37,12 @@ export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefin
return async (tree: Root, file) => {
const display = file.data.frontmatter?.enableToc ?? opts.showByDefault
if (display) {
const slugAnchor = new Slugger()
slugAnchor.reset()
const toc: TocEntry[] = []
let highestDepth: number = opts.maxDepth
visit(tree, "heading", (node) => {
if (node.depth <= opts.maxDepth) {
let text = toString(node)
// strip link formatting from toc entries
text = text.replace(wikilinkRegex, (_, rawFp, __, rawAlias) => {
const fp = rawFp?.trim() ?? ""
const alias = rawAlias?.slice(1).trim()
return alias ?? fp
})
text = text.replace(regexMdLinks, "$1")
const text = toString(node)
highestDepth = Math.min(highestDepth, node.depth)
toc.push({
depth: node.depth,

View File

@ -263,12 +263,10 @@ thead {
font-weight: revert;
margin-bottom: 0;
article > & > a {
article > & > a[role="anchor"] {
color: var(--dark);
&.internal {
background-color: transparent;
}
}
}
h1,

View File

@ -1,11 +1,3 @@
export function pluralize(count: number, s: string): string {
if (count === 1) {
return `1 ${s}`
} else {
return `${count} ${s}s`
}
}
export function capitalize(s: string): string {
return s.substring(0, 1).toUpperCase() + s.substring(1)
}

View File

@ -106,8 +106,9 @@ describe("transforms", () => {
["test.mp4", "test.mp4"],
["note with spaces.md", "note-with-spaces"],
["notes.with.dots.md", "notes.with.dots"],
["test/special chars?.md", "test/special-chars-q"],
["test/special chars?.md", "test/special-chars"],
["test/special chars #3.md", "test/special-chars-3"],
["cool/what about r&d?.md", "cool/what-about-r-and-d"],
],
path.slugifyFilePath,
path.isFilePath,

View File

@ -51,7 +51,12 @@ function sluggify(s: string): string {
return s
.split("/")
.map((segment) =>
segment.replace(/\s/g, "-").replace(/%/g, "-percent").replace(/\?/g, "-q").replace(/#/g, ""),
segment
.replace(/\s/g, "-")
.replace(/&/g, "-and-")
.replace(/%/g, "-percent")
.replace(/\?/g, "")
.replace(/#/g, ""),
)
.join("/") // always use / as sep
.replace(/\/$/, "")