Merge remote-tracking branch 'upstream/v4' into open-graph

This commit is contained in:
Ben Schlegel 2024-02-02 19:09:21 +01:00
commit 7f1bb2bea4
52 changed files with 1105 additions and 718 deletions

View File

@ -8,4 +8,4 @@ updates:
- package-ecosystem: "npm" - package-ecosystem: "npm"
directory: "/" directory: "/"
schedule: schedule:
interval: "daily" interval: "weekly"

View File

@ -156,12 +156,13 @@ document.addEventListener("nav", () => {
// do page specific logic here // do page specific logic here
// e.g. attach event listeners // e.g. attach event listeners
const toggleSwitch = document.querySelector("#switch") as HTMLInputElement const toggleSwitch = document.querySelector("#switch") as HTMLInputElement
toggleSwitch.removeEventListener("change", switchTheme)
toggleSwitch.addEventListener("change", switchTheme) toggleSwitch.addEventListener("change", switchTheme)
window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
}) })
``` ```
It is best practice to also unmount any existing event handlers to prevent memory leaks. It is best practice to track any event handlers via `window.addCleanup` to prevent memory leaks.
This will get called on page navigation.
#### Importing Code #### Importing Code

View File

@ -24,14 +24,32 @@ See [documentation on supported types and syntax here](https://help.obsidian.md
## Customization ## Customization
- Disable callouts: simply pass `callouts: false` to the plugin: `Plugin.ObsidianFlavoredMarkdown({ callouts: false })` - Disable callouts: simply pass `callouts: false` to the plugin: `Plugin.ObsidianFlavoredMarkdown({ callouts: false })`
- Editing icons: `quartz/plugins/transformers/ofm.ts` - Editing icons: `quartz/styles/callouts.scss`
### Add custom callouts
By default, custom callouts are handled by applying the `note` style. To make fancy ones, you have to add these lines to `custom.scss`.
```scss title="quartz/styles/custom.scss"
.callout {
&[data-callout="custom"] {
--color: #customcolor;
--border: #custombordercolor;
--bg: #custombg;
--callout-icon: url("data:image/svg+xml; utf8, <custom formatted svg>"); //SVG icon code
}
}
```
> [!warning]
> Don't forget to ensure that the SVG is URL encoded before putting it in the CSS. You can use tools like [this one](https://yoksel.github.io/url-encoder/) to help you do that.
## Showcase ## Showcase
> [!info] > [!info]
> Default title > Default title
> [!question]+ Can callouts be nested? > [!question]+ Can callouts be _nested_?
> >
> > [!todo]- Yes!, they can. And collapsed! > > [!todo]- Yes!, they can. And collapsed!
> > > >

View File

@ -3,7 +3,7 @@ title: Recent Notes
tags: component tags: component
--- ---
Quartz can generate a list of recent notes for based on some filtering and sorting criteria. Though this component isn't included in any [[layout]] by default, you can add it by using `Component.RecentNotes`. Quartz can generate a list of recent notes based on some filtering and sorting criteria. Though this component isn't included in any [[layout]] by default, you can add it by using `Component.RecentNotes` in `quartz.layout.ts`.
## Customization ## Customization

3
globals.d.ts vendored
View File

@ -4,9 +4,10 @@ export declare global {
type: K, type: K,
listener: (this: Document, ev: CustomEventMap[K]) => void, listener: (this: Document, ev: CustomEventMap[K]) => void,
): void ): void
dispatchEvent<K extends keyof CustomEventMap>(ev: CustomEventMap[K]): void dispatchEvent<K extends keyof CustomEventMap>(ev: CustomEventMap[K] | UIEvent): void
} }
interface Window { interface Window {
spaNavigate(url: URL, isBack: boolean = false) spaNavigate(url: URL, isBack: boolean = false)
addCleanup(fn: (...args: any[]) => void)
} }
} }

403
package-lock.json generated
View File

@ -1,18 +1,18 @@
{ {
"name": "@jackyzha0/quartz", "name": "@jackyzha0/quartz",
"version": "4.1.5", "version": "4.2.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@jackyzha0/quartz", "name": "@jackyzha0/quartz",
"version": "4.1.5", "version": "4.2.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@clack/prompts": "^0.7.0", "@clack/prompts": "^0.7.0",
"@floating-ui/dom": "^1.5.3", "@floating-ui/dom": "^1.6.1",
"@napi-rs/simple-git": "0.1.11", "@napi-rs/simple-git": "0.1.14",
"async-mutex": "^0.4.0", "async-mutex": "^0.4.1",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"cli-spinner": "^0.2.10", "cli-spinner": "^0.2.10",
@ -27,9 +27,9 @@
"hast-util-to-string": "^3.0.0", "hast-util-to-string": "^3.0.0",
"is-absolute-url": "^4.0.1", "is-absolute-url": "^4.0.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lightningcss": "^1.22.1", "lightningcss": "^1.23.0",
"mdast-util-find-and-replace": "^3.0.1", "mdast-util-find-and-replace": "^3.0.1",
"mdast-util-to-hast": "^13.0.2", "mdast-util-to-hast": "^13.1.0",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"micromorph": "^0.4.5", "micromorph": "^0.4.5",
"preact": "^10.19.3", "preact": "^10.19.3",
@ -49,9 +49,9 @@
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"remark-parse": "^11.0.0", "remark-parse": "^11.0.0",
"remark-rehype": "^11.0.0", "remark-rehype": "^11.1.0",
"remark-smartypants": "^2.0.0", "remark-smartypants": "^2.0.0",
"rfdc": "^1.3.0", "rfdc": "^1.3.1",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"satori": "^0.10.6", "satori": "^0.10.6",
"serve-handler": "^6.1.5", "serve-handler": "^6.1.5",
@ -63,7 +63,7 @@
"unified": "^11.0.4", "unified": "^11.0.4",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0",
"vfile": "^6.0.1", "vfile": "^6.0.1",
"workerpool": "^8.0.0", "workerpool": "^9.1.0",
"ws": "^8.15.1", "ws": "^8.15.1",
"yargs": "^17.7.2" "yargs": "^17.7.2"
}, },
@ -73,16 +73,15 @@
"devDependencies": { "devDependencies": {
"@types/cli-spinner": "^0.2.3", "@types/cli-spinner": "^0.2.3",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/hast": "^3.0.3", "@types/hast": "^3.0.4",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/node": "^20.1.2", "@types/node": "^20.11.14",
"@types/pretty-time": "^1.1.5", "@types/pretty-time": "^1.1.5",
"@types/source-map-support": "^0.5.10", "@types/source-map-support": "^0.5.10",
"@types/workerpool": "^6.4.7",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
"@types/yargs": "^17.0.32", "@types/yargs": "^17.0.32",
"esbuild": "^0.19.9", "esbuild": "^0.19.9",
"prettier": "^3.1.1", "prettier": "^3.2.4",
"tsx": "^4.7.0", "tsx": "^4.7.0",
"typescript": "^5.3.3" "typescript": "^5.3.3"
}, },
@ -481,26 +480,26 @@
} }
}, },
"node_modules/@floating-ui/core": { "node_modules/@floating-ui/core": {
"version": "1.5.2", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.2.tgz", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz",
"integrity": "sha512-Ii3MrfY/GAIN3OhXNzpCKaLxHQfJF9qvwq/kEJYdqDxeIHa01K8sldugal6TmeeXl+WMvhv9cnVzUTaFFJF09A==", "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==",
"dependencies": { "dependencies": {
"@floating-ui/utils": "^0.1.3" "@floating-ui/utils": "^0.2.1"
} }
}, },
"node_modules/@floating-ui/dom": { "node_modules/@floating-ui/dom": {
"version": "1.5.3", "version": "1.6.1",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.1.tgz",
"integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", "integrity": "sha512-iA8qE43/H5iGozC3W0YSnVSW42Vh522yyM1gj+BqRwVsTNOyr231PsXDaV04yT39PsO0QL2QpbI/M0ZaLUQgRQ==",
"dependencies": { "dependencies": {
"@floating-ui/core": "^1.4.2", "@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.1.3" "@floating-ui/utils": "^0.2.1"
} }
}, },
"node_modules/@floating-ui/utils": { "node_modules/@floating-ui/utils": {
"version": "0.1.6", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
"integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
}, },
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
"version": "8.0.2", "version": "8.0.2",
@ -519,30 +518,30 @@
} }
}, },
"node_modules/@napi-rs/simple-git": { "node_modules/@napi-rs/simple-git": {
"version": "0.1.11", "version": "0.1.14",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git/-/simple-git-0.1.11.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git/-/simple-git-0.1.14.tgz",
"integrity": "sha512-z14cPCBrtDKKVJ3q4GS5gmXEithGUAt+U8sICgA9i3UFdxJKD4H5rCnO7BVC3htdE9g6OR2w2IcHAL56AjpFbg==", "integrity": "sha512-2cDnsT0nKpQ7yg5u/Zf8/ibp9YFIKhpcfMAGATYuqdJoHuBo6P6UArZ0RDOOtfFC5b9FXuYcGw2ApbM4eWdnbQ==",
"engines": { "engines": {
"node": ">= 10" "node": ">= 10"
}, },
"optionalDependencies": { "optionalDependencies": {
"@napi-rs/simple-git-android-arm-eabi": "0.1.11", "@napi-rs/simple-git-android-arm-eabi": "0.1.14",
"@napi-rs/simple-git-android-arm64": "0.1.11", "@napi-rs/simple-git-android-arm64": "0.1.14",
"@napi-rs/simple-git-darwin-arm64": "0.1.11", "@napi-rs/simple-git-darwin-arm64": "0.1.14",
"@napi-rs/simple-git-darwin-x64": "0.1.11", "@napi-rs/simple-git-darwin-x64": "0.1.14",
"@napi-rs/simple-git-linux-arm-gnueabihf": "0.1.11", "@napi-rs/simple-git-linux-arm-gnueabihf": "0.1.14",
"@napi-rs/simple-git-linux-arm64-gnu": "0.1.11", "@napi-rs/simple-git-linux-arm64-gnu": "0.1.14",
"@napi-rs/simple-git-linux-arm64-musl": "0.1.11", "@napi-rs/simple-git-linux-arm64-musl": "0.1.14",
"@napi-rs/simple-git-linux-x64-gnu": "0.1.11", "@napi-rs/simple-git-linux-x64-gnu": "0.1.14",
"@napi-rs/simple-git-linux-x64-musl": "0.1.11", "@napi-rs/simple-git-linux-x64-musl": "0.1.14",
"@napi-rs/simple-git-win32-arm64-msvc": "0.1.11", "@napi-rs/simple-git-win32-arm64-msvc": "0.1.14",
"@napi-rs/simple-git-win32-x64-msvc": "0.1.11" "@napi-rs/simple-git-win32-x64-msvc": "0.1.14"
} }
}, },
"node_modules/@napi-rs/simple-git-android-arm-eabi": { "node_modules/@napi-rs/simple-git-android-arm-eabi": {
"version": "0.1.11", "version": "0.1.14",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm-eabi/-/simple-git-android-arm-eabi-0.1.11.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm-eabi/-/simple-git-android-arm-eabi-0.1.14.tgz",
"integrity": "sha512-wt4Wu9MxvKzEqT4iwodFs7Nrc31K73gR5hM7VnlO6iLELmUQZ5JVJkYoFWgzLQWtzIC48W2+zFMbBgY6+F2rZg==", "integrity": "sha512-fAJ/Hxc9DhtSHOcB3dPCRW1YcVsqAnbNoOOnHir4aDCtqTP64HrFa7A/675v3vQZpI0u3fXHRcYqW8NF0O/zcg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -555,9 +554,9 @@
} }
}, },
"node_modules/@napi-rs/simple-git-android-arm64": { "node_modules/@napi-rs/simple-git-android-arm64": {
"version": "0.1.11", "version": "0.1.14",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm64/-/simple-git-android-arm64-0.1.11.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm64/-/simple-git-android-arm64-0.1.14.tgz",
"integrity": "sha512-5/Aj6N44CxwhV3TZWRZ4vGqFj4wb2/a2gwvUZJo9Dwik9Spls7As8LaLe7pOptiGPH0GRP3H5kTT7I6twHNgqw==", "integrity": "sha512-dav730MRAR142DoyNDafuwKXcUCYwlbxxxxOarDph7bbN0mZZnKHOQohvRCD/Uz4aJLaj6khCavXSjLDWArEUg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -570,9 +569,9 @@
} }
}, },
"node_modules/@napi-rs/simple-git-darwin-arm64": { "node_modules/@napi-rs/simple-git-darwin-arm64": {
"version": "0.1.11", "version": "0.1.14",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-arm64/-/simple-git-darwin-arm64-0.1.11.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-arm64/-/simple-git-darwin-arm64-0.1.14.tgz",
"integrity": "sha512-vdVsJUNcRsGVu0hBmLZdxxgwIbJA/Ias8NKWze8MZkZ3VyBwhg0uAzFgESEL3/USAgeCCHjF3uwVki8E+iPq1w==", "integrity": "sha512-f6+DqRnI+vFvnsAyw66mWhwl0vw1TOieHV07hvKbg4PU5j+RBI+lVqwY2L+IEAxDFlPirTWKKvGY1Lr7M/yi/A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -585,9 +584,9 @@
} }
}, },
"node_modules/@napi-rs/simple-git-darwin-x64": { "node_modules/@napi-rs/simple-git-darwin-x64": {
"version": "0.1.11", "version": "0.1.14",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-x64/-/simple-git-darwin-x64-0.1.11.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-x64/-/simple-git-darwin-x64-0.1.14.tgz",
"integrity": "sha512-ufVuZxyJ3LpApk3V101X9qYNX91fnQ4isulz9lWjg90U7Xz0Cav4J3yyFZy6B/cJpYxuiy49R8wV1xDtTeGThA==", "integrity": "sha512-x/EnwJdDWJAFay8TQt09byJoBlVZhPEaTAPmRR0fUPzWTjrr28bOy8UW1ysszd9ylBxlyIhuWjOHMHu9CBigTQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -600,9 +599,9 @@
} }
}, },
"node_modules/@napi-rs/simple-git-linux-arm-gnueabihf": { "node_modules/@napi-rs/simple-git-linux-arm-gnueabihf": {
"version": "0.1.11", "version": "0.1.14",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm-gnueabihf/-/simple-git-linux-arm-gnueabihf-0.1.11.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm-gnueabihf/-/simple-git-linux-arm-gnueabihf-0.1.14.tgz",
"integrity": "sha512-rFafW0Qc/j5we2ghUecB7mFzGcNDtJ5lTiB4I7kffNeL8pEi6Yi7kST8hylswcCowia65d45xsyeNp1mFlFwcg==", "integrity": "sha512-k0JZaXBl031gP5VOnoMa1I3lCHlBG7QvtunX5rxnRjx2kZ+JgUyT12s/qle/4xkJ0MnmfKTeiD7hs4Cc4Z3Tzw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -615,9 +614,9 @@
} }
}, },
"node_modules/@napi-rs/simple-git-linux-arm64-gnu": { "node_modules/@napi-rs/simple-git-linux-arm64-gnu": {
"version": "0.1.11", "version": "0.1.14",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-gnu/-/simple-git-linux-arm64-gnu-0.1.11.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-gnu/-/simple-git-linux-arm64-gnu-0.1.14.tgz",
"integrity": "sha512-HZ4yaqpj/FQ3V9qNQrTGhtXb7pLAARXeRJrwoaGfz3eZ069y2bHReFcNR//5bsVhZ18JaS9EV47F8WjDxtpI5g==", "integrity": "sha512-CsmKP6tSIxau10ZKxV1Q1kem2QcJ/Qlov7pxp1Q7kMErcouW0H6vliVniewicaXRVDYV9wK18iD2t5GoJttwlA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -630,9 +629,9 @@
} }
}, },
"node_modules/@napi-rs/simple-git-linux-arm64-musl": { "node_modules/@napi-rs/simple-git-linux-arm64-musl": {
"version": "0.1.11", "version": "0.1.14",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-musl/-/simple-git-linux-arm64-musl-0.1.11.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-musl/-/simple-git-linux-arm64-musl-0.1.14.tgz",
"integrity": "sha512-b39lJiC3n2+Y6Exjx6qwHoBF++D3k2hN4mZZkvQCFSdLXJ2xtalCatSRWW3pt+mHOHMOgbGektL5v5BYq52hxw==", "integrity": "sha512-krfEckZQ3myoHwmGmqY0aHBnqAzzV66+jFNLQEKaVMSGsXA2P+UcGo0coGzmB13rFRWC2eZpZRNB3MrfrStHkw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -645,9 +644,9 @@
} }
}, },
"node_modules/@napi-rs/simple-git-linux-x64-gnu": { "node_modules/@napi-rs/simple-git-linux-x64-gnu": {
"version": "0.1.11", "version": "0.1.14",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-gnu/-/simple-git-linux-x64-gnu-0.1.11.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-gnu/-/simple-git-linux-x64-gnu-0.1.14.tgz",
"integrity": "sha512-9EPFvY7PZg+oqWi6Jft5WgSsQtvy9Ey1g4NG+LG8y1RbvaNKthxKbR5zgx196pnFVdcLtsuIdOv/OaQlbcTXkw==", "integrity": "sha512-4T2Q2QdO6t3OawkwdVmdqLz2EH8lfAw2cMT/zdjfTMfhNKjJgSg3kTgRnu/tf8TLCb+wu80fFvafwE0laB2VTQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -660,9 +659,9 @@
} }
}, },
"node_modules/@napi-rs/simple-git-linux-x64-musl": { "node_modules/@napi-rs/simple-git-linux-x64-musl": {
"version": "0.1.11", "version": "0.1.14",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-musl/-/simple-git-linux-x64-musl-0.1.11.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-musl/-/simple-git-linux-x64-musl-0.1.14.tgz",
"integrity": "sha512-doIt1lPYIGL3UthlEQjdM9s1Wv0v8bz8LVAgbzJMS+UpVZzArwLWkanAJCy1HjgMTUMiE3AVJqACKIF3EfW/TQ==", "integrity": "sha512-RaTGW8u+RXJbfRF4QN2Dcr5r5DrFh4wLjOvFeOy7sGX3Q9m3IKuw5AjRxTJqIw6xD/AAPKKNzOvPjrIF7728Lw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -675,9 +674,9 @@
} }
}, },
"node_modules/@napi-rs/simple-git-win32-arm64-msvc": { "node_modules/@napi-rs/simple-git-win32-arm64-msvc": {
"version": "0.1.11", "version": "0.1.14",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-arm64-msvc/-/simple-git-win32-arm64-msvc-0.1.11.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-arm64-msvc/-/simple-git-win32-arm64-msvc-0.1.14.tgz",
"integrity": "sha512-TK3Uvj3Q72ebxfxDT/eLFt8sxCNHo20QMvqJ5BHt4zP1Y9Fl1DXSPRUKLBIhJd0nPcI45ZOMRiZyoT8joxAC9g==", "integrity": "sha512-kb9bKG9t79HJMuRMqbUJFLfWRf952O2Ea4VFwoRA2d/Uwtowm85Ol3JV9E6oeurguRLqdMLrUKyduCW6Hc9Jsg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -690,9 +689,9 @@
} }
}, },
"node_modules/@napi-rs/simple-git-win32-x64-msvc": { "node_modules/@napi-rs/simple-git-win32-x64-msvc": {
"version": "0.1.11", "version": "0.1.14",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-x64-msvc/-/simple-git-win32-x64-msvc-0.1.11.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-x64-msvc/-/simple-git-win32-x64-msvc-0.1.14.tgz",
"integrity": "sha512-XOgP6kFDXGmB2KCXFQEsCq70n/Do2h7W9o7qZu8APAD+Sc8JGKz4hKG7PKY2ot924v9nIoKSYbHnupnhXSoXkg==", "integrity": "sha512-3835xy0e2gOaZ3SPt1pINBFSBBL3dOx3cChyAzQU0TnMU4Ye/YOh1qa5pO7BOJlCSnOh7iWt782blxCT0HH61w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1061,9 +1060,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/hast": { "node_modules/@types/hast": {
"version": "3.0.3", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
"integrity": "sha512-2fYGlaDy/qyLlhidX42wAH0KBi2TCjKMH8CHmBXgRlJ3Y+OXTiqsPQ6IWarZKwF1JoUcAJdPogv1d4b0COTpmQ==", "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
"dependencies": { "dependencies": {
"@types/unist": "*" "@types/unist": "*"
} }
@ -1106,10 +1105,13 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.3.3", "version": "20.11.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.3.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.14.tgz",
"integrity": "sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==", "integrity": "sha512-w3yWCcwULefjP9DmDDsgUskrMoOy5Z8MiwKHr1FvqGPtx7CvJzQvxD7eKpxNtklQxLruxSXWddyeRtyud0RcXQ==",
"dev": true "dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
}, },
"node_modules/@types/pretty-time": { "node_modules/@types/pretty-time": {
"version": "1.1.5", "version": "1.1.5",
@ -1131,15 +1133,6 @@
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
}, },
"node_modules/@types/workerpool": {
"version": "6.4.7",
"resolved": "https://registry.npmjs.org/@types/workerpool/-/workerpool-6.4.7.tgz",
"integrity": "sha512-DI2U4obcMzFViyNjLw0xXspim++qkAJ4BWRdYPVMMFtOpTvMr6PAk3UTZEoSqnZnvgUkJ3ck97Ybk+iIfuJHMg==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/ws": { "node_modules/@types/ws": {
"version": "8.5.10", "version": "8.5.10",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
@ -1232,9 +1225,9 @@
} }
}, },
"node_modules/async-mutex": { "node_modules/async-mutex": {
"version": "0.4.0", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.0.tgz", "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz",
"integrity": "sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==", "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==",
"dependencies": { "dependencies": {
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
@ -3289,9 +3282,9 @@
} }
}, },
"node_modules/lightningcss": { "node_modules/lightningcss": {
"version": "1.22.1", "version": "1.23.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.22.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.23.0.tgz",
"integrity": "sha512-Fy45PhibiNXkm0cK5FJCbfO8Y6jUpD/YcHf/BtuI+jvYYqSXKF4muk61jjE8YxCR9y+hDYIWSzHTc+bwhDE6rQ==", "integrity": "sha512-SEArWKMHhqn/0QzOtclIwH5pXIYQOUEkF8DgICd/105O+GCgd7jxjNod/QPnBCSWvpRHQBGVz5fQ9uScby03zA==",
"dependencies": { "dependencies": {
"detect-libc": "^1.0.3" "detect-libc": "^1.0.3"
}, },
@ -3303,21 +3296,21 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
}, },
"optionalDependencies": { "optionalDependencies": {
"lightningcss-darwin-arm64": "1.22.1", "lightningcss-darwin-arm64": "1.23.0",
"lightningcss-darwin-x64": "1.22.1", "lightningcss-darwin-x64": "1.23.0",
"lightningcss-freebsd-x64": "1.22.1", "lightningcss-freebsd-x64": "1.23.0",
"lightningcss-linux-arm-gnueabihf": "1.22.1", "lightningcss-linux-arm-gnueabihf": "1.23.0",
"lightningcss-linux-arm64-gnu": "1.22.1", "lightningcss-linux-arm64-gnu": "1.23.0",
"lightningcss-linux-arm64-musl": "1.22.1", "lightningcss-linux-arm64-musl": "1.23.0",
"lightningcss-linux-x64-gnu": "1.22.1", "lightningcss-linux-x64-gnu": "1.23.0",
"lightningcss-linux-x64-musl": "1.22.1", "lightningcss-linux-x64-musl": "1.23.0",
"lightningcss-win32-x64-msvc": "1.22.1" "lightningcss-win32-x64-msvc": "1.23.0"
} }
}, },
"node_modules/lightningcss-darwin-arm64": { "node_modules/lightningcss-darwin-arm64": {
"version": "1.22.1", "version": "1.23.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.22.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.23.0.tgz",
"integrity": "sha512-ldvElu+R0QimNTjsKpaZkUv3zf+uefzLy/R1R19jtgOfSRM+zjUCUgDhfEDRmVqJtMwYsdhMI2aJtJChPC6Osg==", "integrity": "sha512-kl4Pk3Q2lnE6AJ7Qaij47KNEfY2/UXRZBT/zqGA24B8qwkgllr/j7rclKOf1axcslNXvvUdztjo4Xqh39Yq1aA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3333,13 +3326,156 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
} }
}, },
"node_modules/linebreak": { "node_modules/lightningcss-darwin-x64": {
"version": "1.1.0", "version": "1.23.0",
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.23.0.tgz",
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", "integrity": "sha512-KeRFCNoYfDdcolcFXvokVw+PXCapd2yHS1Diko1z1BhRz/nQuD5XyZmxjWdhmhN/zj5sH8YvWsp0/lPLVzqKpg==",
"dependencies": { "cpu": [
"base64-js": "0.0.8", "x64"
"unicode-trie": "^2.0.0" ],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.23.0.tgz",
"integrity": "sha512-xhnhf0bWPuZxcqknvMDRFFo2TInrmQRWZGB0f6YoAsZX8Y+epfjHeeOIGCfAmgF0DgZxHwYc8mIR5tQU9/+ROA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.23.0.tgz",
"integrity": "sha512-fBamf/bULvmWft9uuX+bZske236pUZEoUlaHNBjnueaCTJ/xd8eXgb0cEc7S5o0Nn6kxlauMBnqJpF70Bgq3zg==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.23.0.tgz",
"integrity": "sha512-RS7sY77yVLOmZD6xW2uEHByYHhQi5JYWmgVumYY85BfNoVI3DupXSlzbw+b45A9NnVKq45+oXkiN6ouMMtTwfg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.23.0.tgz",
"integrity": "sha512-cU00LGb6GUXCwof6ACgSMKo3q7XYbsyTj0WsKHLi1nw7pV0NCq8nFTn6ZRBYLoKiV8t+jWl0Hv8KkgymmK5L5g==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.23.0.tgz",
"integrity": "sha512-q4jdx5+5NfB0/qMbXbOmuC6oo7caPnFghJbIAV90cXZqgV8Am3miZhC4p+sQVdacqxfd+3nrle4C8icR3p1AYw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.23.0.tgz",
"integrity": "sha512-G9Ri3qpmF4qef2CV/80dADHKXRAQeQXpQTLx7AiQrBYQHqBjB75oxqj06FCIe5g4hNCqLPnM9fsO4CyiT1sFSQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.23.0.tgz",
"integrity": "sha512-1rcBDJLU+obPPJM6qR5fgBUiCdZwZLafZM5f9kwjFLkb/UBNIzmae39uCSmh71nzPCTXZqHbvwu23OWnWEz+eg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
} }
}, },
"node_modules/longest-streak": { "node_modules/longest-streak": {
@ -3726,9 +3862,9 @@
} }
}, },
"node_modules/mdast-util-to-hast": { "node_modules/mdast-util-to-hast": {
"version": "13.0.2", "version": "13.1.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.0.2.tgz", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz",
"integrity": "sha512-U5I+500EOOw9e3ZrclN3Is3fRpw8c19SMyNZlZ2IS+7vLsNzb2Om11VpIVOR+/0137GhZsFEF6YiKD5+0Hr2Og==", "integrity": "sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==",
"dependencies": { "dependencies": {
"@types/hast": "^3.0.0", "@types/hast": "^3.0.0",
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
@ -3737,7 +3873,8 @@
"micromark-util-sanitize-uri": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0",
"trim-lines": "^3.0.0", "trim-lines": "^3.0.0",
"unist-util-position": "^5.0.0", "unist-util-position": "^5.0.0",
"unist-util-visit": "^5.0.0" "unist-util-visit": "^5.0.0",
"vfile": "^6.0.0"
}, },
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
@ -4720,9 +4857,9 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.1.1", "version": "3.2.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz",
"integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==",
"dev": true, "dev": true,
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
@ -5135,9 +5272,9 @@
} }
}, },
"node_modules/remark-rehype": { "node_modules/remark-rehype": {
"version": "11.0.0", "version": "11.1.0",
"resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.0.0.tgz", "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.0.tgz",
"integrity": "sha512-vx8x2MDMcxuE4lBmQ46zYUDfcFMmvg80WYX+UNLeG6ixjdCCLcw1lrgAukwBTuOFsS78eoAedHGn9sNM0w7TPw==", "integrity": "sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g==",
"dependencies": { "dependencies": {
"@types/hast": "^3.0.0", "@types/hast": "^3.0.0",
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
@ -5452,9 +5589,9 @@
} }
}, },
"node_modules/rfdc": { "node_modules/rfdc": {
"version": "1.3.0", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz",
"integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg=="
}, },
"node_modules/rimraf": { "node_modules/rimraf": {
"version": "5.0.5", "version": "5.0.5",
@ -6181,6 +6318,12 @@
"node": ">=14.17" "node": ">=14.17"
} }
}, },
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/unherit": { "node_modules/unherit": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/unherit/-/unherit-3.0.1.tgz", "resolved": "https://registry.npmjs.org/unherit/-/unherit-3.0.1.tgz",
@ -6580,9 +6723,9 @@
"integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==" "integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw=="
}, },
"node_modules/workerpool": { "node_modules/workerpool": {
"version": "8.0.0", "version": "9.1.0",
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-8.0.0.tgz", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.1.0.tgz",
"integrity": "sha512-aoLtwqMXoYVA1JV+t8uCLo7sXkF4Q1Ijrn7954X2IVyysk2bv2Il7C9sVJH8xk9xJAL0FNgR+hPOhmvnMk/P5Q==" "integrity": "sha512-+wRWfm9yyJghvXLSHMQj3WXDxHbibHAQmRrWbqKBfy0RjftZNeQaW+Std5bSYc83ydkrxoPTPOWVlXUR9RWJdQ=="
}, },
"node_modules/wrap-ansi": { "node_modules/wrap-ansi": {
"version": "8.1.0", "version": "8.1.0",

View File

@ -2,7 +2,7 @@
"name": "@jackyzha0/quartz", "name": "@jackyzha0/quartz",
"description": "🌱 publish your digital garden and notes as a website", "description": "🌱 publish your digital garden and notes as a website",
"private": true, "private": true,
"version": "4.1.5", "version": "4.2.1",
"type": "module", "type": "module",
"author": "jackyzha0 <j.zhao2k19@gmail.com>", "author": "jackyzha0 <j.zhao2k19@gmail.com>",
"license": "MIT", "license": "MIT",
@ -35,9 +35,9 @@
}, },
"dependencies": { "dependencies": {
"@clack/prompts": "^0.7.0", "@clack/prompts": "^0.7.0",
"@floating-ui/dom": "^1.5.3", "@floating-ui/dom": "^1.6.1",
"@napi-rs/simple-git": "0.1.11", "@napi-rs/simple-git": "0.1.14",
"async-mutex": "^0.4.0", "async-mutex": "^0.4.1",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"cli-spinner": "^0.2.10", "cli-spinner": "^0.2.10",
@ -52,9 +52,9 @@
"hast-util-to-string": "^3.0.0", "hast-util-to-string": "^3.0.0",
"is-absolute-url": "^4.0.1", "is-absolute-url": "^4.0.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lightningcss": "^1.22.1", "lightningcss": "^1.23.0",
"mdast-util-find-and-replace": "^3.0.1", "mdast-util-find-and-replace": "^3.0.1",
"mdast-util-to-hast": "^13.0.2", "mdast-util-to-hast": "^13.1.0",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"micromorph": "^0.4.5", "micromorph": "^0.4.5",
"preact": "^10.19.3", "preact": "^10.19.3",
@ -74,9 +74,9 @@
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"remark-parse": "^11.0.0", "remark-parse": "^11.0.0",
"remark-rehype": "^11.0.0", "remark-rehype": "^11.1.0",
"remark-smartypants": "^2.0.0", "remark-smartypants": "^2.0.0",
"rfdc": "^1.3.0", "rfdc": "^1.3.1",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"satori": "^0.10.6", "satori": "^0.10.6",
"serve-handler": "^6.1.5", "serve-handler": "^6.1.5",
@ -88,23 +88,22 @@
"unified": "^11.0.4", "unified": "^11.0.4",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0",
"vfile": "^6.0.1", "vfile": "^6.0.1",
"workerpool": "^8.0.0", "workerpool": "^9.1.0",
"ws": "^8.15.1", "ws": "^8.15.1",
"yargs": "^17.7.2" "yargs": "^17.7.2"
}, },
"devDependencies": { "devDependencies": {
"@types/cli-spinner": "^0.2.3", "@types/cli-spinner": "^0.2.3",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/hast": "^3.0.3", "@types/hast": "^3.0.4",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/node": "^20.1.2", "@types/node": "^20.11.14",
"@types/pretty-time": "^1.1.5", "@types/pretty-time": "^1.1.5",
"@types/source-map-support": "^0.5.10", "@types/source-map-support": "^0.5.10",
"@types/workerpool": "^6.4.7",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
"@types/yargs": "^17.0.32", "@types/yargs": "^17.0.32",
"esbuild": "^0.19.9", "esbuild": "^0.19.9",
"prettier": "^3.1.1", "prettier": "^3.2.4",
"tsx": "^4.7.0", "tsx": "^4.7.0",
"typescript": "^5.3.3" "typescript": "^5.3.3"
} }

View File

@ -126,17 +126,8 @@ async function rebuildFromEntrypoint(
clientRefresh: () => void, clientRefresh: () => void,
buildData: BuildData, // note: this function mutates buildData buildData: BuildData, // note: this function mutates buildData
) { ) {
const { const { ctx, ignored, mut, initialSlugs, contentMap, toRebuild, toRemove, trackedAssets } =
ctx, buildData
ignored,
mut,
initialSlugs,
contentMap,
toRebuild,
toRemove,
trackedAssets,
lastBuildMs,
} = buildData
const { argv } = ctx const { argv } = ctx
@ -164,12 +155,12 @@ async function rebuildFromEntrypoint(
toRemove.add(filePath) toRemove.add(filePath)
} }
// debounce rebuilds every 250ms
const buildStart = new Date().getTime() const buildStart = new Date().getTime()
buildData.lastBuildMs = buildStart buildData.lastBuildMs = buildStart
const release = await mut.acquire() const release = await mut.acquire()
if (lastBuildMs > buildStart) {
// there's another build after us, release and let them do it
if (buildData.lastBuildMs > buildStart) {
release() release()
return return
} }

View File

@ -17,6 +17,7 @@ export type Analytics =
| { | {
provider: "umami" provider: "umami"
websiteId: string websiteId: string
host?: string
} }
export interface GlobalConfiguration { export interface GlobalConfiguration {
@ -40,6 +41,12 @@ export interface GlobalConfiguration {
*/ */
generateSocialImages: boolean | Partial<SocialImageOptions> generateSocialImages: boolean | Partial<SocialImageOptions>
theme: Theme 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)
*/
locale?: string
} }
export interface QuartzConfig { export interface QuartzConfig {

View File

@ -347,7 +347,7 @@ export async function handleBuild(argv) {
directoryListing: false, directoryListing: false,
headers: [ headers: [
{ {
source: "**/*.html", source: "**/*.*",
headers: [{ key: "Content-Disposition", value: "inline" }], headers: [{ key: "Content-Disposition", value: "inline" }],
}, },
{ {

View File

@ -1,9 +1,10 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { classNames } from "../util/lang"
function ArticleTitle({ fileData, displayClass }: QuartzComponentProps) { function ArticleTitle({ fileData, displayClass }: QuartzComponentProps) {
const title = fileData.frontmatter?.title const title = fileData.frontmatter?.title
if (title) { if (title) {
return <h1 class={`article-title ${displayClass ?? ""}`}>{title}</h1> return <h1 class={classNames(displayClass, "article-title")}>{title}</h1>
} else { } else {
return null return null
} }

View File

@ -1,12 +1,13 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import style from "./styles/backlinks.scss" import style from "./styles/backlinks.scss"
import { resolveRelative, simplifySlug } from "../util/path" import { resolveRelative, simplifySlug } from "../util/path"
import { classNames } from "../util/lang"
function Backlinks({ fileData, allFiles, displayClass }: QuartzComponentProps) { function Backlinks({ fileData, allFiles, displayClass }: QuartzComponentProps) {
const slug = simplifySlug(fileData.slug!) const slug = simplifySlug(fileData.slug!)
const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug)) const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
return ( return (
<div class={`backlinks ${displayClass ?? ""}`}> <div class={classNames(displayClass, "backlinks")}>
<h3>Backlinks</h3> <h3>Backlinks</h3>
<ul class="overflow"> <ul class="overflow">
{backlinkFiles.length > 0 ? ( {backlinkFiles.length > 0 ? (

View File

@ -2,6 +2,7 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import breadcrumbsStyle from "./styles/breadcrumbs.scss" import breadcrumbsStyle from "./styles/breadcrumbs.scss"
import { FullSlug, SimpleSlug, resolveRelative } from "../util/path" import { FullSlug, SimpleSlug, resolveRelative } from "../util/path"
import { QuartzPluginData } from "../plugins/vfile" import { QuartzPluginData } from "../plugins/vfile"
import { classNames } from "../util/lang"
type CrumbData = { type CrumbData = {
displayName: string displayName: string
@ -113,7 +114,7 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
} }
return ( return (
<nav class={`breadcrumb-container ${displayClass ?? ""}`} aria-label="breadcrumbs"> <nav class={classNames(displayClass, "breadcrumb-container")} aria-label="breadcrumbs">
{crumbs.map((crumb, index) => ( {crumbs.map((crumb, index) => (
<div class="breadcrumb-element"> <div class="breadcrumb-element">
<a href={crumb.path}>{crumb.displayName}</a> <a href={crumb.path}>{crumb.displayName}</a>

View File

@ -1,6 +1,7 @@
import { formatDate, getDate } from "./Date" import { formatDate, getDate } from "./Date"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import readingTime from "reading-time" import readingTime from "reading-time"
import { classNames } from "../util/lang"
interface ContentMetaOptions { interface ContentMetaOptions {
/** /**
@ -24,7 +25,7 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
const segments: string[] = [] const segments: string[] = []
if (fileData.dates) { if (fileData.dates) {
segments.push(formatDate(getDate(cfg, fileData)!)) segments.push(formatDate(getDate(cfg, fileData)!, cfg.locale))
} }
// Display reading time if enabled // Display reading time if enabled
@ -33,7 +34,7 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
segments.push(timeTaken) segments.push(timeTaken)
} }
return <p class={`content-meta ${displayClass ?? ""}`}>{segments.join(", ")}</p> return <p class={classNames(displayClass, "content-meta")}>{segments.join(", ")}</p>
} else { } else {
return null return null
} }

View File

@ -4,10 +4,11 @@
import darkmodeScript from "./scripts/darkmode.inline" import darkmodeScript from "./scripts/darkmode.inline"
import styles from "./styles/darkmode.scss" import styles from "./styles/darkmode.scss"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { classNames } from "../util/lang"
function Darkmode({ displayClass }: QuartzComponentProps) { function Darkmode({ displayClass }: QuartzComponentProps) {
return ( return (
<div class={`darkmode ${displayClass ?? ""}`}> <div class={classNames(displayClass, "darkmode")}>
<input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} /> <input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} />
<label id="toggle-label-light" for="darkmode-toggle" tabIndex={-1}> <label id="toggle-label-light" for="darkmode-toggle" tabIndex={-1}>
<svg <svg

View File

@ -3,6 +3,7 @@ import { QuartzPluginData } from "../plugins/vfile"
interface Props { interface Props {
date: Date date: Date
locale?: string
} }
export type ValidDateType = keyof Required<QuartzPluginData>["dates"] export type ValidDateType = keyof Required<QuartzPluginData>["dates"]
@ -16,14 +17,14 @@ export function getDate(cfg: GlobalConfiguration, data: QuartzPluginData): Date
return data.dates?.[cfg.defaultDateType] return data.dates?.[cfg.defaultDateType]
} }
export function formatDate(d: Date): string { export function formatDate(d: Date, locale = "en-US"): string {
return d.toLocaleDateString("en-US", { return d.toLocaleDateString(locale, {
year: "numeric", year: "numeric",
month: "short", month: "short",
day: "2-digit", day: "2-digit",
}) })
} }
export function Date({ date }: Props) { export function Date({ date, locale }: Props) {
return <>{formatDate(date)}</> return <>{formatDate(date, locale)}</>
} }

View File

@ -5,6 +5,7 @@ import explorerStyle from "./styles/explorer.scss"
import script from "./scripts/explorer.inline" import script from "./scripts/explorer.inline"
import { ExplorerNode, FileNode, Options } from "./ExplorerNode" import { ExplorerNode, FileNode, Options } from "./ExplorerNode"
import { QuartzPluginData } from "../plugins/vfile" import { QuartzPluginData } from "../plugins/vfile"
import { classNames } from "../util/lang"
// Options interface defined in `ExplorerNode` to avoid circular dependency // Options interface defined in `ExplorerNode` to avoid circular dependency
const defaultOptions = { const defaultOptions = {
@ -69,16 +70,15 @@ export default ((userOpts?: Partial<Options>) => {
} }
// Get all folders of tree. Initialize with collapsed state // Get all folders of tree. Initialize with collapsed state
const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed")
// Stringify to pass json tree as data attribute ([data-tree]) // Stringify to pass json tree as data attribute ([data-tree])
const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed")
jsonTree = JSON.stringify(folders) jsonTree = JSON.stringify(folders)
} }
function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) { function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) {
constructFileTree(allFiles) constructFileTree(allFiles)
return ( return (
<div class={`explorer ${displayClass ?? ""}`}> <div class={classNames(displayClass, "explorer")}>
<button <button
type="button" type="button"
id="explorer" id="explorer"

View File

@ -2,6 +2,7 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
// @ts-ignore // @ts-ignore
import script from "./scripts/graph.inline" import script from "./scripts/graph.inline"
import style from "./styles/graph.scss" import style from "./styles/graph.scss"
import { classNames } from "../util/lang"
export interface D3Config { export interface D3Config {
drag: boolean drag: boolean
@ -56,7 +57,7 @@ export default ((opts?: GraphOptions) => {
const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph } const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph }
const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph } const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph }
return ( return (
<div class={`graph ${displayClass ?? ""}`}> <div class={classNames(displayClass, "graph")}>
<h3>Graph View</h3> <h3>Graph View</h3>
<div class="graph-outer"> <div class="graph-outer">
<div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div> <div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>

View File

@ -46,7 +46,7 @@ export function PageList({ cfg, fileData, allFiles, limit }: Props) {
<div class="section"> <div class="section">
{page.dates && ( {page.dates && (
<p class="meta"> <p class="meta">
<Date date={getDate(cfg, page)!} /> <Date date={getDate(cfg, page)!} locale={cfg.locale} />
</p> </p>
)} )}
<div class="desc"> <div class="desc">

View File

@ -1,11 +1,12 @@
import { pathToRoot } from "../util/path" import { pathToRoot } from "../util/path"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { classNames } from "../util/lang"
function PageTitle({ fileData, cfg, displayClass }: QuartzComponentProps) { function PageTitle({ fileData, cfg, displayClass }: QuartzComponentProps) {
const title = cfg?.pageTitle ?? "Untitled Quartz" const title = cfg?.pageTitle ?? "Untitled Quartz"
const baseDir = pathToRoot(fileData.slug!) const baseDir = pathToRoot(fileData.slug!)
return ( return (
<h1 class={`page-title ${displayClass ?? ""}`}> <h1 class={classNames(displayClass, "page-title")}>
<a href={baseDir}>{title}</a> <a href={baseDir}>{title}</a>
</h1> </h1>
) )

View File

@ -5,6 +5,7 @@ import { byDateAndAlphabetical } from "./PageList"
import style from "./styles/recentNotes.scss" import style from "./styles/recentNotes.scss"
import { Date, getDate } from "./Date" import { Date, getDate } from "./Date"
import { GlobalConfiguration } from "../cfg" import { GlobalConfiguration } from "../cfg"
import { classNames } from "../util/lang"
interface Options { interface Options {
title: string title: string
@ -28,7 +29,7 @@ export default ((userOpts?: Partial<Options>) => {
const pages = allFiles.filter(opts.filter).sort(opts.sort) const pages = allFiles.filter(opts.filter).sort(opts.sort)
const remaining = Math.max(0, pages.length - opts.limit) const remaining = Math.max(0, pages.length - opts.limit)
return ( return (
<div class={`recent-notes ${displayClass ?? ""}`}> <div class={classNames(displayClass, "recent-notes")}>
<h3>{opts.title}</h3> <h3>{opts.title}</h3>
<ul class="recent-ul"> <ul class="recent-ul">
{pages.slice(0, opts.limit).map((page) => { {pages.slice(0, opts.limit).map((page) => {
@ -47,7 +48,7 @@ export default ((userOpts?: Partial<Options>) => {
</div> </div>
{page.dates && ( {page.dates && (
<p class="meta"> <p class="meta">
<Date date={getDate(cfg, page)!} /> <Date date={getDate(cfg, page)!} locale={cfg.locale} />
</p> </p>
)} )}
<ul class="tags"> <ul class="tags">

View File

@ -2,11 +2,22 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import style from "./styles/search.scss" import style from "./styles/search.scss"
// @ts-ignore // @ts-ignore
import script from "./scripts/search.inline" import script from "./scripts/search.inline"
import { classNames } from "../util/lang"
export default (() => { export interface SearchOptions {
enablePreview: boolean
}
const defaultOptions: SearchOptions = {
enablePreview: true,
}
export default ((userOpts?: Partial<SearchOptions>) => {
function Search({ displayClass }: QuartzComponentProps) { function Search({ displayClass }: QuartzComponentProps) {
const opts = { ...defaultOptions, ...userOpts }
return ( return (
<div class={`search ${displayClass ?? ""}`}> <div class={classNames(displayClass, "search")}>
<div id="search-icon"> <div id="search-icon">
<p>Search</p> <p>Search</p>
<div></div> <div></div>
@ -35,7 +46,7 @@ export default (() => {
aria-label="Search for something" aria-label="Search for something"
placeholder="Search for something" placeholder="Search for something"
/> />
<div id="results-container"></div> <div id="search-layout" data-preview={opts.enablePreview}></div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,7 +1,8 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { classNames } from "../util/lang"
function Spacer({ displayClass }: QuartzComponentProps) { function Spacer({ displayClass }: QuartzComponentProps) {
return <div class={`spacer ${displayClass ?? ""}`}></div> return <div class={classNames(displayClass, "spacer")}></div>
} }
export default (() => Spacer) satisfies QuartzComponentConstructor export default (() => Spacer) satisfies QuartzComponentConstructor

View File

@ -1,6 +1,7 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import legacyStyle from "./styles/legacyToc.scss" import legacyStyle from "./styles/legacyToc.scss"
import modernStyle from "./styles/toc.scss" import modernStyle from "./styles/toc.scss"
import { classNames } from "../util/lang"
// @ts-ignore // @ts-ignore
import script from "./scripts/toc.inline" import script from "./scripts/toc.inline"
@ -19,7 +20,7 @@ function TableOfContents({ fileData, displayClass }: QuartzComponentProps) {
} }
return ( return (
<div class={`toc ${displayClass ?? ""}`}> <div class={classNames(displayClass, "toc")}>
<button type="button" id="toc" class={fileData.collapseToc ? "collapsed" : ""}> <button type="button" id="toc" class={fileData.collapseToc ? "collapsed" : ""}>
<h3>Table of Contents</h3> <h3>Table of Contents</h3>
<svg <svg

View File

@ -1,12 +1,13 @@
import { pathToRoot, slugTag } from "../util/path" import { pathToRoot, slugTag } from "../util/path"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { classNames } from "../util/lang"
function TagList({ fileData, displayClass }: QuartzComponentProps) { function TagList({ fileData, displayClass }: QuartzComponentProps) {
const tags = fileData.frontmatter?.tags const tags = fileData.frontmatter?.tags
const baseDir = pathToRoot(fileData.slug!) const baseDir = pathToRoot(fileData.slug!)
if (tags && tags.length > 0) { if (tags && tags.length > 0) {
return ( return (
<ul class={`tags ${displayClass ?? ""}`}> <ul class={classNames(displayClass, "tags")}>
{tags.map((tag) => { {tags.map((tag) => {
const display = `#${tag}` const display = `#${tag}`
const linkDest = baseDir + `/tags/${slugTag(tag)}` const linkDest = baseDir + `/tags/${slugTag(tag)}`

View File

@ -33,7 +33,8 @@ export default ((opts?: Partial<FolderContentOptions>) => {
const isDirectChild = fileParts.length === folderParts.length + 1 const isDirectChild = fileParts.length === folderParts.length + 1
return prefixed && isDirectChild return prefixed && isDirectChild
}) })
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
const classes = ["popover-hint", ...cssClasses].join(" ")
const listProps = { const listProps = {
...props, ...props,
allFiles: allPagesInFolder, allFiles: allPagesInFolder,
@ -45,15 +46,17 @@ export default ((opts?: Partial<FolderContentOptions>) => {
: htmlToJsx(fileData.filePath!, tree) : htmlToJsx(fileData.filePath!, tree)
return ( return (
<div class="popover-hint"> <div class={classes}>
<article> <article>
<p>{content}</p> <p>{content}</p>
</article> </article>
{options.showFolderCount && ( <div class="page-listing">
<p>{pluralize(allPagesInFolder.length, "item")} under this folder.</p> {options.showFolderCount && (
)} <p>{pluralize(allPagesInFolder.length, "item")} under this folder.</p>
<div> )}
<PageList {...listProps} /> <div>
<PageList {...listProps} />
</div>
</div> </div>
</div> </div>
) )

View File

@ -26,7 +26,8 @@ function TagContent(props: QuartzComponentProps) {
(tree as Root).children.length === 0 (tree as Root).children.length === 0
? fileData.description ? fileData.description
: htmlToJsx(fileData.filePath!, tree) : htmlToJsx(fileData.filePath!, tree)
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
const classes = ["popover-hint", ...cssClasses].join(" ")
if (tag === "/") { if (tag === "/") {
const tags = [ const tags = [
...new Set( ...new Set(
@ -37,9 +38,8 @@ function TagContent(props: QuartzComponentProps) {
for (const tag of tags) { for (const tag of tags) {
tagItemMap.set(tag, allPagesWithTag(tag)) tagItemMap.set(tag, allPagesWithTag(tag))
} }
return ( return (
<div class="popover-hint"> <div class={classes}>
<article> <article>
<p>{content}</p> <p>{content}</p>
</article> </article>
@ -62,11 +62,13 @@ function TagContent(props: QuartzComponentProps) {
</a> </a>
</h2> </h2>
{content && <p>{content}</p>} {content && <p>{content}</p>}
<p> <div class="page-listing">
{pluralize(pages.length, "item")} with this tag.{" "} <p>
{pages.length > numPages && `Showing first ${numPages}.`} {pluralize(pages.length, "item")} with this tag.{" "}
</p> {pages.length > numPages && `Showing first ${numPages}.`}
<PageList limit={numPages} {...listProps} /> </p>
<PageList limit={numPages} {...listProps} />
</div>
</div> </div>
) )
})} })}
@ -81,11 +83,13 @@ function TagContent(props: QuartzComponentProps) {
} }
return ( return (
<div class="popover-hint"> <div class={classes}>
<article>{content}</article> <article>{content}</article>
<p>{pluralize(pages.length, "item")} with this tag.</p> <div class="page-listing">
<div> <p>{pluralize(pages.length, "item")} with this tag.</p>
<PageList {...listProps} /> <div>
<PageList {...listProps} />
</div>
</div> </div>
</div> </div>
) )

View File

@ -1,21 +1,21 @@
function toggleCallout(this: HTMLElement) { function toggleCallout(this: HTMLElement) {
const outerBlock = this.parentElement! const outerBlock = this.parentElement!
outerBlock.classList.toggle(`is-collapsed`) outerBlock.classList.toggle("is-collapsed")
const collapsed = outerBlock.classList.contains(`is-collapsed`) const collapsed = outerBlock.classList.contains("is-collapsed")
const height = collapsed ? this.scrollHeight : outerBlock.scrollHeight const height = collapsed ? this.scrollHeight : outerBlock.scrollHeight
outerBlock.style.maxHeight = height + `px` outerBlock.style.maxHeight = height + "px"
// walk and adjust height of all parents // walk and adjust height of all parents
let current = outerBlock let current = outerBlock
let parent = outerBlock.parentElement let parent = outerBlock.parentElement
while (parent) { while (parent) {
if (!parent.classList.contains(`callout`)) { if (!parent.classList.contains("callout")) {
return return
} }
const collapsed = parent.classList.contains(`is-collapsed`) const collapsed = parent.classList.contains("is-collapsed")
const height = collapsed ? parent.scrollHeight : parent.scrollHeight + current.scrollHeight const height = collapsed ? parent.scrollHeight : parent.scrollHeight + current.scrollHeight
parent.style.maxHeight = height + `px` parent.style.maxHeight = height + "px"
current = parent current = parent
parent = parent.parentElement parent = parent.parentElement
@ -30,15 +30,15 @@ function setupCallout() {
const title = div.firstElementChild const title = div.firstElementChild
if (title) { if (title) {
title.removeEventListener(`click`, toggleCallout) title.addEventListener("click", toggleCallout)
title.addEventListener(`click`, toggleCallout) window.addCleanup(() => title.removeEventListener("click", toggleCallout))
const collapsed = div.classList.contains(`is-collapsed`) const collapsed = div.classList.contains("is-collapsed")
const height = collapsed ? title.scrollHeight : div.scrollHeight const height = collapsed ? title.scrollHeight : div.scrollHeight
div.style.maxHeight = height + `px` div.style.maxHeight = height + "px"
} }
} }
} }
document.addEventListener(`nav`, setupCallout) document.addEventListener("nav", setupCallout)
window.addEventListener(`resize`, setupCallout) window.addEventListener("resize", setupCallout)

View File

@ -14,7 +14,7 @@ document.addEventListener("nav", () => {
button.type = "button" button.type = "button"
button.innerHTML = svgCopy button.innerHTML = svgCopy
button.ariaLabel = "Copy source" button.ariaLabel = "Copy source"
button.addEventListener("click", () => { function onClick() {
navigator.clipboard.writeText(source).then( navigator.clipboard.writeText(source).then(
() => { () => {
button.blur() button.blur()
@ -26,7 +26,9 @@ document.addEventListener("nav", () => {
}, },
(error) => console.error(error), (error) => console.error(error),
) )
}) }
button.addEventListener("click", onClick)
window.addCleanup(() => button.removeEventListener("click", onClick))
els[i].prepend(button) els[i].prepend(button)
} }
} }

View File

@ -10,28 +10,31 @@ const emitThemeChangeEvent = (theme: "light" | "dark") => {
} }
document.addEventListener("nav", () => { document.addEventListener("nav", () => {
const switchTheme = (e: any) => { const switchTheme = (e: Event) => {
const newTheme = e.target.checked ? "dark" : "light" const newTheme = (e.target as HTMLInputElement)?.checked ? "dark" : "light"
document.documentElement.setAttribute("saved-theme", newTheme) document.documentElement.setAttribute("saved-theme", newTheme)
localStorage.setItem("theme", newTheme) localStorage.setItem("theme", newTheme)
emitThemeChangeEvent(newTheme) emitThemeChangeEvent(newTheme)
} }
const themeChange = (e: MediaQueryListEvent) => {
const newTheme = e.matches ? "dark" : "light"
document.documentElement.setAttribute("saved-theme", newTheme)
localStorage.setItem("theme", newTheme)
toggleSwitch.checked = e.matches
emitThemeChangeEvent(newTheme)
}
// Darkmode toggle // Darkmode toggle
const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement
toggleSwitch.removeEventListener("change", switchTheme)
toggleSwitch.addEventListener("change", switchTheme) toggleSwitch.addEventListener("change", switchTheme)
window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
if (currentTheme === "dark") { if (currentTheme === "dark") {
toggleSwitch.checked = true toggleSwitch.checked = true
} }
// Listen for changes in prefers-color-scheme // Listen for changes in prefers-color-scheme
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)") const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
colorSchemeMediaQuery.addEventListener("change", (e) => { colorSchemeMediaQuery.addEventListener("change", themeChange)
const newTheme = e.matches ? "dark" : "light" window.addCleanup(() => colorSchemeMediaQuery.removeEventListener("change", themeChange))
document.documentElement.setAttribute("saved-theme", newTheme)
localStorage.setItem("theme", newTheme)
toggleSwitch.checked = e.matches
emitThemeChangeEvent(newTheme)
})
}) })

View File

@ -1,132 +1,106 @@
import { FolderState } from "../ExplorerNode" import { FolderState } from "../ExplorerNode"
// Current state of folders type MaybeHTMLElement = HTMLElement | undefined
let explorerState: FolderState[] let currentExplorerState: FolderState[]
const observer = new IntersectionObserver((entries) => { const observer = new IntersectionObserver((entries) => {
// If last element is observed, remove gradient of "overflow" class so element is visible // If last element is observed, remove gradient of "overflow" class so element is visible
const explorer = document.getElementById("explorer-ul") const explorerUl = document.getElementById("explorer-ul")
if (!explorerUl) return
for (const entry of entries) { for (const entry of entries) {
if (entry.isIntersecting) { if (entry.isIntersecting) {
explorer?.classList.add("no-background") explorerUl.classList.add("no-background")
} else { } else {
explorer?.classList.remove("no-background") explorerUl.classList.remove("no-background")
} }
} }
}) })
function toggleExplorer(this: HTMLElement) { function toggleExplorer(this: HTMLElement) {
// Toggle collapsed state of entire explorer
this.classList.toggle("collapsed") this.classList.toggle("collapsed")
const content = this.nextElementSibling as HTMLElement const content = this.nextElementSibling as MaybeHTMLElement
if (!content) return
content.classList.toggle("collapsed") content.classList.toggle("collapsed")
content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px" content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
} }
function toggleFolder(evt: MouseEvent) { function toggleFolder(evt: MouseEvent) {
evt.stopPropagation() evt.stopPropagation()
const target = evt.target as MaybeHTMLElement
if (!target) return
// Element that was clicked
const target = evt.target as HTMLElement
// Check if target was svg icon or button
const isSvg = target.nodeName === "svg" const isSvg = target.nodeName === "svg"
const childFolderContainer = (
isSvg
? target.parentElement?.nextSibling
: target.parentElement?.parentElement?.nextElementSibling
) as MaybeHTMLElement
const currentFolderParent = (
isSvg ? target.nextElementSibling : target.parentElement
) as MaybeHTMLElement
if (!(childFolderContainer && currentFolderParent)) return
// corresponding <ul> element relative to clicked button/folder childFolderContainer.classList.toggle("open")
let childFolderContainer: HTMLElement
// <li> element of folder (stores folder-path dataset)
let currentFolderParent: HTMLElement
// Get correct relative container and toggle collapsed class
if (isSvg) {
childFolderContainer = target.parentElement?.nextSibling as HTMLElement
currentFolderParent = target.nextElementSibling as HTMLElement
childFolderContainer.classList.toggle("open")
} else {
childFolderContainer = target.parentElement?.parentElement?.nextElementSibling as HTMLElement
currentFolderParent = target.parentElement as HTMLElement
childFolderContainer.classList.toggle("open")
}
if (!childFolderContainer) return
// Collapse folder container
const isCollapsed = childFolderContainer.classList.contains("open") const isCollapsed = childFolderContainer.classList.contains("open")
setFolderState(childFolderContainer, !isCollapsed) setFolderState(childFolderContainer, !isCollapsed)
const fullFolderPath = currentFolderParent.dataset.folderpath as string
// Save folder state to localStorage toggleCollapsedByPath(currentExplorerState, fullFolderPath)
const clickFolderPath = currentFolderParent.dataset.folderpath as string const stringifiedFileTree = JSON.stringify(currentExplorerState)
const fullFolderPath = clickFolderPath
toggleCollapsedByPath(explorerState, fullFolderPath)
const stringifiedFileTree = JSON.stringify(explorerState)
localStorage.setItem("fileTree", stringifiedFileTree) localStorage.setItem("fileTree", stringifiedFileTree)
} }
function setupExplorer() { function setupExplorer() {
// Set click handler for collapsing entire explorer
const explorer = document.getElementById("explorer") const explorer = document.getElementById("explorer")
if (!explorer) return
if (explorer.dataset.behavior === "collapse") {
for (const item of document.getElementsByClassName(
"folder-button",
) as HTMLCollectionOf<HTMLElement>) {
item.addEventListener("click", toggleFolder)
window.addCleanup(() => item.removeEventListener("click", toggleFolder))
}
}
explorer.addEventListener("click", toggleExplorer)
window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer))
// Set up click handlers for each folder (click handler on folder "icon")
for (const item of document.getElementsByClassName(
"folder-icon",
) as HTMLCollectionOf<HTMLElement>) {
item.addEventListener("click", toggleFolder)
window.addCleanup(() => item.removeEventListener("click", toggleFolder))
}
// Get folder state from local storage // Get folder state from local storage
const storageTree = localStorage.getItem("fileTree") const storageTree = localStorage.getItem("fileTree")
// Convert to bool
const useSavedFolderState = explorer?.dataset.savestate === "true" const useSavedFolderState = explorer?.dataset.savestate === "true"
const oldExplorerState: FolderState[] =
storageTree && useSavedFolderState ? JSON.parse(storageTree) : []
const oldIndex = new Map(oldExplorerState.map((entry) => [entry.path, entry.collapsed]))
const newExplorerState: FolderState[] = explorer.dataset.tree
? JSON.parse(explorer.dataset.tree)
: []
currentExplorerState = []
for (const { path, collapsed } of newExplorerState) {
currentExplorerState.push({ path, collapsed: oldIndex.get(path) ?? collapsed })
}
if (explorer) { currentExplorerState.map((folderState) => {
// Get config const folderLi = document.querySelector(
const collapseBehavior = explorer.dataset.behavior `[data-folderpath='${folderState.path}']`,
) as MaybeHTMLElement
// Add click handlers for all folders (click handler on folder "label") const folderUl = folderLi?.parentElement?.nextElementSibling as MaybeHTMLElement
if (collapseBehavior === "collapse") { if (folderUl) {
Array.prototype.forEach.call( setFolderState(folderUl, folderState.collapsed)
document.getElementsByClassName("folder-button"),
function (item) {
item.removeEventListener("click", toggleFolder)
item.addEventListener("click", toggleFolder)
},
)
} }
// Add click handler to main explorer
explorer.removeEventListener("click", toggleExplorer)
explorer.addEventListener("click", toggleExplorer)
}
// Set up click handlers for each folder (click handler on folder "icon")
Array.prototype.forEach.call(document.getElementsByClassName("folder-icon"), function (item) {
item.removeEventListener("click", toggleFolder)
item.addEventListener("click", toggleFolder)
}) })
if (storageTree && useSavedFolderState) {
// Get state from localStorage and set folder state
explorerState = JSON.parse(storageTree)
explorerState.map((folderUl) => {
// grab <li> element for matching folder path
const folderLi = document.querySelector(`[data-folderpath='${folderUl.path}']`) as HTMLElement
// Get corresponding content <ul> tag and set state
if (folderLi) {
const folderUL = folderLi.parentElement?.nextElementSibling
if (folderUL) {
setFolderState(folderUL as HTMLElement, folderUl.collapsed)
}
}
})
} else if (explorer?.dataset.tree) {
// If tree is not in localStorage or config is disabled, use tree passed from Explorer as dataset
explorerState = JSON.parse(explorer.dataset.tree)
}
} }
window.addEventListener("resize", setupExplorer) window.addEventListener("resize", setupExplorer)
document.addEventListener("nav", () => { document.addEventListener("nav", () => {
setupExplorer() setupExplorer()
observer.disconnect() observer.disconnect()
// select pseudo element at end of list // select pseudo element at end of list
@ -142,11 +116,7 @@ document.addEventListener("nav", () => {
* @param collapsed if folder should be set to collapsed or not * @param collapsed if folder should be set to collapsed or not
*/ */
function setFolderState(folderElement: HTMLElement, collapsed: boolean) { function setFolderState(folderElement: HTMLElement, collapsed: boolean) {
if (collapsed) { return collapsed ? folderElement.classList.remove("open") : folderElement.classList.add("open")
folderElement?.classList.remove("open")
} else {
folderElement?.classList.add("open")
}
} }
/** /**

View File

@ -319,12 +319,12 @@ function renderGlobalGraph() {
registerEscapeHandler(container, hideGlobalGraph) registerEscapeHandler(container, hideGlobalGraph)
} }
document.addEventListener("nav", async (e: unknown) => { document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
const slug = (e as CustomEventMap["nav"]).detail.url const slug = e.detail.url
addToVisited(slug) addToVisited(slug)
await renderGraph("graph-container", slug) await renderGraph("graph-container", slug)
const containerIcon = document.getElementById("global-graph-icon") const containerIcon = document.getElementById("global-graph-icon")
containerIcon?.removeEventListener("click", renderGlobalGraph)
containerIcon?.addEventListener("click", renderGlobalGraph) containerIcon?.addEventListener("click", renderGlobalGraph)
window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph))
}) })

View File

@ -76,7 +76,7 @@ async function mouseEnterHandler(
document.addEventListener("nav", () => { document.addEventListener("nav", () => {
const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[] const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[]
for (const link of links) { for (const link of links) {
link.removeEventListener("mouseenter", mouseEnterHandler)
link.addEventListener("mouseenter", mouseEnterHandler) link.addEventListener("mouseenter", mouseEnterHandler)
window.addCleanup(() => link.removeEventListener("mouseenter", mouseEnterHandler))
} }
}) })

View File

@ -1,7 +1,7 @@
import FlexSearch from "flexsearch" import FlexSearch from "flexsearch"
import { ContentDetails } from "../../plugins/emitters/contentIndex" import { ContentDetails } from "../../plugins/emitters/contentIndex"
import { registerEscapeHandler, removeAllChildren } from "./util" import { registerEscapeHandler, removeAllChildren } from "./util"
import { FullSlug, resolveRelative } from "../../util/path" import { FullSlug, normalizeRelativeURLs, resolveRelative } from "../../util/path"
interface Item { interface Item {
id: number id: number
@ -11,23 +11,53 @@ interface Item {
tags: string[] tags: string[]
} }
let index: FlexSearch.Document<Item> | undefined = undefined
// Can be expanded with things like "term" in the future // Can be expanded with things like "term" in the future
type SearchType = "basic" | "tags" type SearchType = "basic" | "tags"
// Current searchType
let searchType: SearchType = "basic" let searchType: SearchType = "basic"
let currentSearchTerm: string = ""
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
let index = new FlexSearch.Document<Item>({
charset: "latin:extra",
encode: encoder,
document: {
id: "id",
index: [
{
field: "title",
tokenize: "forward",
},
{
field: "content",
tokenize: "forward",
},
{
field: "tags",
tokenize: "forward",
},
],
},
})
const p = new DOMParser()
const fetchContentCache: Map<FullSlug, Element[]> = new Map()
const contextWindowWords = 30 const contextWindowWords = 30
const numSearchResults = 5 const numSearchResults = 8
const numTagResults = 3 const numTagResults = 5
const tokenizeTerm = (term: string) => {
const tokens = term.split(/\s+/).filter((t) => t.trim() !== "")
const tokenLen = tokens.length
if (tokenLen > 1) {
for (let i = 1; i < tokenLen; i++) {
tokens.push(tokens.slice(0, i + 1).join(" "))
}
}
return tokens.sort((a, b) => b.length - a.length) // always highlight longest terms first
}
function highlight(searchTerm: string, text: string, trim?: boolean) { function highlight(searchTerm: string, text: string, trim?: boolean) {
// try to highlight longest tokens first const tokenizedTerms = tokenizeTerm(searchTerm)
const tokenizedTerms = searchTerm
.split(/\s+/)
.filter((t) => t !== "")
.sort((a, b) => b.length - a.length)
let tokenizedText = text.split(/\s+/).filter((t) => t !== "") let tokenizedText = text.split(/\s+/).filter((t) => t !== "")
let startIndex = 0 let startIndex = 0
@ -71,20 +101,78 @@ function highlight(searchTerm: string, text: string, trim?: boolean) {
}` }`
} }
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/) function highlightHTML(searchTerm: string, el: HTMLElement) {
let prevShortcutHandler: ((e: HTMLElementEventMap["keydown"]) => void) | undefined = undefined const p = new DOMParser()
document.addEventListener("nav", async (e: unknown) => { const tokenizedTerms = tokenizeTerm(searchTerm)
const currentSlug = (e as CustomEventMap["nav"]).detail.url const html = p.parseFromString(el.innerHTML, "text/html")
const createHighlightSpan = (text: string) => {
const span = document.createElement("span")
span.className = "highlight"
span.textContent = text
return span
}
const highlightTextNodes = (node: Node, term: string) => {
if (node.nodeType === Node.TEXT_NODE) {
const nodeText = node.nodeValue ?? ""
const regex = new RegExp(term.toLowerCase(), "gi")
const matches = nodeText.match(regex)
if (!matches || matches.length === 0) return
const spanContainer = document.createElement("span")
let lastIndex = 0
for (const match of matches) {
const matchIndex = nodeText.indexOf(match, lastIndex)
spanContainer.appendChild(document.createTextNode(nodeText.slice(lastIndex, matchIndex)))
spanContainer.appendChild(createHighlightSpan(match))
lastIndex = matchIndex + match.length
}
spanContainer.appendChild(document.createTextNode(nodeText.slice(lastIndex)))
node.parentNode?.replaceChild(spanContainer, node)
} else if (node.nodeType === Node.ELEMENT_NODE) {
if ((node as HTMLElement).classList.contains("highlight")) return
Array.from(node.childNodes).forEach((child) => highlightTextNodes(child, term))
}
}
for (const term of tokenizedTerms) {
highlightTextNodes(html.body, term)
}
return html.body
}
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
const currentSlug = e.detail.url
const data = await fetchData const data = await fetchData
const container = document.getElementById("search-container") const container = document.getElementById("search-container")
const sidebar = container?.closest(".sidebar") as HTMLElement const sidebar = container?.closest(".sidebar") as HTMLElement
const searchIcon = document.getElementById("search-icon") const searchIcon = document.getElementById("search-icon")
const searchBar = document.getElementById("search-bar") as HTMLInputElement | null const searchBar = document.getElementById("search-bar") as HTMLInputElement | null
const results = document.getElementById("results-container") const searchLayout = document.getElementById("search-layout")
const resultCards = document.getElementsByClassName("result-card")
const idDataMap = Object.keys(data) as FullSlug[] const idDataMap = Object.keys(data) as FullSlug[]
const appendLayout = (el: HTMLElement) => {
if (searchLayout?.querySelector(`#${el.id}`) === null) {
searchLayout?.appendChild(el)
}
}
const enablePreview = searchLayout?.dataset?.preview === "true"
let preview: HTMLDivElement | undefined = undefined
let previewInner: HTMLDivElement | undefined = undefined
const results = document.createElement("div")
results.id = "results-container"
results.style.flexBasis = enablePreview ? "min(30%, 450px)" : "100%"
appendLayout(results)
if (enablePreview) {
preview = document.createElement("div")
preview.id = "preview-container"
preview.style.flexBasis = "100%"
appendLayout(preview)
}
function hideSearch() { function hideSearch() {
container?.classList.remove("active") container?.classList.remove("active")
if (searchBar) { if (searchBar) {
@ -96,6 +184,12 @@ document.addEventListener("nav", async (e: unknown) => {
if (results) { if (results) {
removeAllChildren(results) removeAllChildren(results)
} }
if (preview) {
removeAllChildren(preview)
}
if (searchLayout) {
searchLayout.classList.remove("display-results")
}
searchType = "basic" // reset search type after closing searchType = "basic" // reset search type after closing
} }
@ -109,11 +203,14 @@ document.addEventListener("nav", async (e: unknown) => {
searchBar?.focus() searchBar?.focus()
} }
function shortcutHandler(e: HTMLElementEventMap["keydown"]) { let currentHover: HTMLInputElement | null = null
async function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
if (e.key === "k" && (e.ctrlKey || e.metaKey) && !e.shiftKey) { if (e.key === "k" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
e.preventDefault() e.preventDefault()
const searchBarOpen = container?.classList.contains("active") const searchBarOpen = container?.classList.contains("active")
searchBarOpen ? hideSearch() : showSearch("basic") searchBarOpen ? hideSearch() : showSearch("basic")
return
} else if (e.shiftKey && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { } else if (e.shiftKey && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
// Hotkey to open tag search // Hotkey to open tag search
e.preventDefault() e.preventDefault()
@ -122,159 +219,227 @@ document.addEventListener("nav", async (e: unknown) => {
// add "#" prefix for tag search // add "#" prefix for tag search
if (searchBar) searchBar.value = "#" if (searchBar) searchBar.value = "#"
return
} }
if (currentHover) {
currentHover.classList.remove("focus")
currentHover.blur()
}
// If search is active, then we will render the first result and display accordingly
if (!container?.classList.contains("active")) return if (!container?.classList.contains("active")) return
else if (e.key === "Enter") { else if (e.key === "Enter") {
// If result has focus, navigate to that one, otherwise pick first result // If result has focus, navigate to that one, otherwise pick first result
if (results?.contains(document.activeElement)) { if (results?.contains(document.activeElement)) {
const active = document.activeElement as HTMLInputElement const active = document.activeElement as HTMLInputElement
if (active.classList.contains("no-match")) return
await displayPreview(active)
active.click() active.click()
} else { } else {
const anchor = document.getElementsByClassName("result-card")[0] as HTMLInputElement | null const anchor = document.getElementsByClassName("result-card")[0] as HTMLInputElement | null
anchor?.click() if (!anchor || anchor?.classList.contains("no-match")) return
await displayPreview(anchor)
anchor.click()
} }
} else if (e.key === "ArrowUp" || (e.shiftKey && e.key === "Tab")) { } else if (e.key === "ArrowUp" || (e.shiftKey && e.key === "Tab")) {
e.preventDefault() e.preventDefault()
if (results?.contains(document.activeElement)) { if (results?.contains(document.activeElement)) {
// If an element in results-container already has focus, focus previous one // If an element in results-container already has focus, focus previous one
const prevResult = document.activeElement?.previousElementSibling as HTMLInputElement | null const currentResult = currentHover
? currentHover
: (document.activeElement as HTMLInputElement | null)
const prevResult = currentResult?.previousElementSibling as HTMLInputElement | null
currentResult?.classList.remove("focus")
prevResult?.focus() prevResult?.focus()
currentHover = prevResult
await displayPreview(prevResult)
} }
} else if (e.key === "ArrowDown" || e.key === "Tab") { } else if (e.key === "ArrowDown" || e.key === "Tab") {
e.preventDefault() e.preventDefault()
// When first pressing ArrowDown, results wont contain the active element, so focus first element // The results should already been focused, so we need to find the next one.
if (!results?.contains(document.activeElement)) { // The activeElement is the search bar, so we need to find the first result and focus it.
const firstResult = resultCards[0] as HTMLInputElement | null if (document.activeElement === searchBar || currentHover !== null) {
firstResult?.focus() const firstResult = currentHover
? currentHover
: (document.getElementsByClassName("result-card")[0] as HTMLInputElement | null)
const secondResult = firstResult?.nextElementSibling as HTMLInputElement | null
firstResult?.classList.remove("focus")
secondResult?.focus()
currentHover = secondResult
await displayPreview(secondResult)
} else { } else {
// If an element in results-container already has focus, focus next one // If an element in results-container already has focus, focus next one
const nextResult = document.activeElement?.nextElementSibling as HTMLInputElement | null const active = currentHover
? currentHover
: (document.activeElement as HTMLInputElement | null)
active?.classList.remove("focus")
const nextResult = active?.nextElementSibling as HTMLInputElement | null
nextResult?.focus() nextResult?.focus()
currentHover = nextResult
await displayPreview(nextResult)
} }
} }
} }
function trimContent(content: string) {
// works without escaping html like in `description.ts`
const sentences = content.replace(/\s+/g, " ").split(".")
let finalDesc = ""
let sentenceIdx = 0
// Roughly estimate characters by (words * 5). Matches description length in `description.ts`.
const len = contextWindowWords * 5
while (finalDesc.length < len) {
const sentence = sentences[sentenceIdx]
if (!sentence) break
finalDesc += sentence + "."
sentenceIdx++
}
// If more content would be available, indicate it by finishing with "..."
if (finalDesc.length < content.length) {
finalDesc += ".."
}
return finalDesc
}
const formatForDisplay = (term: string, id: number) => { const formatForDisplay = (term: string, id: number) => {
const slug = idDataMap[id] const slug = idDataMap[id]
return { return {
id, id,
slug, slug,
title: searchType === "tags" ? data[slug].title : highlight(term, data[slug].title ?? ""), title: searchType === "tags" ? data[slug].title : highlight(term, data[slug].title ?? ""),
// if searchType is tag, display context from start of file and trim, otherwise use regular highlight content: highlight(term, data[slug].content ?? "", true),
content: tags: highlightTags(term.substring(1), data[slug].tags),
searchType === "tags"
? trimContent(data[slug].content)
: highlight(term, data[slug].content ?? "", true),
tags: highlightTags(term, data[slug].tags),
} }
} }
function highlightTags(term: string, tags: string[]) { function highlightTags(term: string, tags: string[]) {
if (tags && searchType === "tags") { if (!tags || searchType !== "tags") {
// Find matching tags
const termLower = term.toLowerCase()
let matching = tags.filter((str) => str.includes(termLower))
// Subtract matching from original tags, then push difference
if (matching.length > 0) {
let difference = tags.filter((x) => !matching.includes(x))
// Convert to html (cant be done later as matches/term dont get passed to `resultToHTML`)
matching = matching.map((tag) => `<li><p class="match-tag">#${tag}</p></li>`)
difference = difference.map((tag) => `<li><p>#${tag}</p></li>`)
matching.push(...difference)
}
// Only allow max of `numTagResults` in preview
if (tags.length > numTagResults) {
matching.splice(numTagResults)
}
return matching
} else {
return [] return []
} }
return tags
.map((tag) => {
if (tag.toLowerCase().includes(term.toLowerCase())) {
return `<li><p class="match-tag">#${tag}</p></li>`
} else {
return `<li><p>#${tag}</p></li>`
}
})
.slice(0, numTagResults)
}
function resolveUrl(slug: FullSlug): URL {
return new URL(resolveRelative(currentSlug, slug), location.toString())
} }
const resultToHTML = ({ slug, title, content, tags }: Item) => { const resultToHTML = ({ slug, title, content, tags }: Item) => {
const htmlTags = tags.length > 0 ? `<ul>${tags.join("")}</ul>` : `` const htmlTags = tags.length > 0 ? `<ul class="tags">${tags.join("")}</ul>` : ``
const itemTile = document.createElement("a") const itemTile = document.createElement("a")
itemTile.classList.add("result-card") itemTile.classList.add("result-card")
itemTile.id = slug itemTile.id = slug
itemTile.href = new URL(resolveRelative(currentSlug, slug), location.toString()).toString() itemTile.href = resolveUrl(slug).toString()
itemTile.innerHTML = `<h3>${title}</h3>${htmlTags}<p>${content}</p>` itemTile.innerHTML = `<h3>${title}</h3>${htmlTags}<p class="preview">${content}</p>`
itemTile.addEventListener("click", (event) => {
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return async function onMouseEnter(ev: MouseEvent) {
hideSearch() if (!ev.target) return
currentHover?.classList.remove("focus")
currentHover?.blur()
const target = ev.target as HTMLInputElement
currentHover = target
currentHover.classList.add("focus")
await displayPreview(target)
}
async function onMouseLeave(ev: MouseEvent) {
if (!ev.target) return
const target = ev.target as HTMLElement
target.classList.remove("focus")
}
const events = [
["mouseenter", onMouseEnter],
["mouseleave", onMouseLeave],
[
"click",
(event: MouseEvent) => {
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
hideSearch()
},
],
] as const
events.forEach(([event, handler]) => {
itemTile.addEventListener(event, handler)
window.addCleanup(() => itemTile.removeEventListener(event, handler))
}) })
return itemTile return itemTile
} }
function displayResults(finalResults: Item[]) { async function displayResults(finalResults: Item[]) {
if (!results) return if (!results) return
removeAllChildren(results) removeAllChildren(results)
if (finalResults.length === 0) { if (finalResults.length === 0) {
results.innerHTML = `<a class="result-card"> results.innerHTML = `<a class="result-card no-match">
<h3>No results.</h3> <h3>No results.</h3>
<p>Try another search term?</p> <p>Try another search term?</p>
</a>` </a>`
} else { } else {
results.append(...finalResults.map(resultToHTML)) results.append(...finalResults.map(resultToHTML))
} }
if (finalResults.length === 0 && preview) {
// no results, clear previous preview
removeAllChildren(preview)
} else {
// focus on first result, then also dispatch preview immediately
const firstChild = results.firstElementChild as HTMLElement
firstChild.classList.add("focus")
currentHover = firstChild as HTMLInputElement
await displayPreview(firstChild)
}
}
async function fetchContent(slug: FullSlug): Promise<Element[]> {
if (fetchContentCache.has(slug)) {
return fetchContentCache.get(slug) as Element[]
}
const targetUrl = resolveUrl(slug).toString()
const contents = await fetch(targetUrl)
.then((res) => res.text())
.then((contents) => {
if (contents === undefined) {
throw new Error(`Could not fetch ${targetUrl}`)
}
const html = p.parseFromString(contents ?? "", "text/html")
normalizeRelativeURLs(html, targetUrl)
return [...html.getElementsByClassName("popover-hint")]
})
fetchContentCache.set(slug, contents)
return contents
}
async function displayPreview(el: HTMLElement | null) {
if (!searchLayout || !enablePreview || !el || !preview) return
const slug = el.id as FullSlug
const innerDiv = await fetchContent(slug).then((contents) =>
contents.flatMap((el) => [...highlightHTML(currentSearchTerm, el as HTMLElement).children]),
)
previewInner = document.createElement("div")
previewInner.classList.add("preview-inner")
previewInner.append(...innerDiv)
preview.replaceChildren(previewInner)
// scroll to longest
const highlights = [...preview.querySelectorAll(".highlight")].sort(
(a, b) => b.innerHTML.length - a.innerHTML.length,
)
highlights[0]?.scrollIntoView({ block: "start" })
} }
async function onType(e: HTMLElementEventMap["input"]) { async function onType(e: HTMLElementEventMap["input"]) {
let term = (e.target as HTMLInputElement).value if (!searchLayout || !index) return
currentSearchTerm = (e.target as HTMLInputElement).value
searchLayout.classList.toggle("display-results", currentSearchTerm !== "")
searchType = currentSearchTerm.startsWith("#") ? "tags" : "basic"
let searchResults: FlexSearch.SimpleDocumentSearchResultSetUnit[] let searchResults: FlexSearch.SimpleDocumentSearchResultSetUnit[]
if (searchType === "tags") {
if (term.toLowerCase().startsWith("#")) { searchResults = await index.searchAsync({
searchType = "tags" query: currentSearchTerm.substring(1),
} else { limit: numSearchResults,
searchType = "basic" index: ["tags"],
} })
} else if (searchType === "basic") {
switch (searchType) { searchResults = await index.searchAsync({
case "tags": { query: currentSearchTerm,
term = term.substring(1) limit: numSearchResults,
searchResults = index: ["title", "content"],
(await index?.searchAsync({ query: term, limit: numSearchResults, index: ["tags"] })) ?? })
[]
break
}
case "basic":
default: {
searchResults =
(await index?.searchAsync({
query: term,
limit: numSearchResults,
index: ["title", "content"],
})) ?? []
}
} }
const getByField = (field: string): number[] => { const getByField = (field: string): number[] => {
@ -288,50 +453,19 @@ document.addEventListener("nav", async (e: unknown) => {
...getByField("content"), ...getByField("content"),
...getByField("tags"), ...getByField("tags"),
]) ])
const finalResults = [...allIds].map((id) => formatForDisplay(term, id)) const finalResults = [...allIds].map((id) => formatForDisplay(currentSearchTerm, id))
displayResults(finalResults) await displayResults(finalResults)
}
if (prevShortcutHandler) {
document.removeEventListener("keydown", prevShortcutHandler)
} }
document.addEventListener("keydown", shortcutHandler) document.addEventListener("keydown", shortcutHandler)
prevShortcutHandler = shortcutHandler window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler))
searchIcon?.removeEventListener("click", () => showSearch("basic"))
searchIcon?.addEventListener("click", () => showSearch("basic")) searchIcon?.addEventListener("click", () => showSearch("basic"))
searchBar?.removeEventListener("input", onType) window.addCleanup(() => searchIcon?.removeEventListener("click", () => showSearch("basic")))
searchBar?.addEventListener("input", onType) searchBar?.addEventListener("input", onType)
window.addCleanup(() => searchBar?.removeEventListener("input", onType))
// setup index if it hasn't been already
if (!index) {
index = new FlexSearch.Document({
charset: "latin:extra",
encode: encoder,
document: {
id: "id",
index: [
{
field: "title",
tokenize: "forward",
},
{
field: "content",
tokenize: "forward",
},
{
field: "tags",
tokenize: "forward",
},
],
},
})
fillDocument(index, data)
}
// register handlers
registerEscapeHandler(container, hideSearch) registerEscapeHandler(container, hideSearch)
await fillDocument(data)
}) })
/** /**
@ -339,16 +473,20 @@ document.addEventListener("nav", async (e: unknown) => {
* @param index index to fill * @param index index to fill
* @param data data to fill index with * @param data data to fill index with
*/ */
async function fillDocument(index: FlexSearch.Document<Item, false>, data: any) { async function fillDocument(data: { [key: FullSlug]: ContentDetails }) {
let id = 0 let id = 0
const promises: Array<Promise<unknown>> = []
for (const [slug, fileData] of Object.entries<ContentDetails>(data)) { for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
await index.addAsync(id, { promises.push(
id, index.addAsync(id++, {
slug: slug as FullSlug, id,
title: fileData.title, slug: slug as FullSlug,
content: fileData.content, title: fileData.title,
tags: fileData.tags, content: fileData.content,
}) tags: fileData.tags,
id++ }),
)
} }
return await Promise.all(promises)
} }

View File

@ -39,6 +39,9 @@ function notifyNav(url: FullSlug) {
document.dispatchEvent(event) document.dispatchEvent(event)
} }
const cleanupFns: Set<(...args: any[]) => void> = new Set()
window.addCleanup = (fn) => cleanupFns.add(fn)
let p: DOMParser let p: DOMParser
async function navigate(url: URL, isBack: boolean = false) { async function navigate(url: URL, isBack: boolean = false) {
p = p || new DOMParser() p = p || new DOMParser()
@ -57,6 +60,10 @@ async function navigate(url: URL, isBack: boolean = false) {
if (!contents) return if (!contents) return
// cleanup old
cleanupFns.forEach((fn) => fn())
cleanupFns.clear()
const html = p.parseFromString(contents, "text/html") const html = p.parseFromString(contents, "text/html")
normalizeRelativeURLs(html, url) normalizeRelativeURLs(html, url)

View File

@ -16,7 +16,8 @@ const observer = new IntersectionObserver((entries) => {
function toggleToc(this: HTMLElement) { function toggleToc(this: HTMLElement) {
this.classList.toggle("collapsed") this.classList.toggle("collapsed")
const content = this.nextElementSibling as HTMLElement const content = this.nextElementSibling as HTMLElement | undefined
if (!content) return
content.classList.toggle("collapsed") content.classList.toggle("collapsed")
content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px" content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
} }
@ -25,10 +26,11 @@ function setupToc() {
const toc = document.getElementById("toc") const toc = document.getElementById("toc")
if (toc) { if (toc) {
const collapsed = toc.classList.contains("collapsed") const collapsed = toc.classList.contains("collapsed")
const content = toc.nextElementSibling as HTMLElement const content = toc.nextElementSibling as HTMLElement | undefined
if (!content) return
content.style.maxHeight = collapsed ? "0px" : content.scrollHeight + "px" content.style.maxHeight = collapsed ? "0px" : content.scrollHeight + "px"
toc.removeEventListener("click", toggleToc)
toc.addEventListener("click", toggleToc) toc.addEventListener("click", toggleToc)
window.addCleanup(() => toc.removeEventListener("click", toggleToc))
} }
} }

View File

@ -12,10 +12,10 @@ export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb:
cb() cb()
} }
outsideContainer?.removeEventListener("click", click)
outsideContainer?.addEventListener("click", click) outsideContainer?.addEventListener("click", click)
document.removeEventListener("keydown", esc) window.addCleanup(() => outsideContainer?.removeEventListener("click", click))
document.addEventListener("keydown", esc) document.addEventListener("keydown", esc)
window.addCleanup(() => document.removeEventListener("keydown", esc))
} }
export function removeAllChildren(node: HTMLElement) { export function removeAllChildren(node: HTMLElement) {

View File

@ -1,3 +1,5 @@
@use "../../styles/variables.scss" as *;
button#explorer { button#explorer {
all: unset; all: unset;
background-color: transparent; background-color: transparent;
@ -85,7 +87,7 @@ svg {
color: var(--secondary); color: var(--secondary);
font-family: var(--headerFont); font-family: var(--headerFont);
font-size: 0.95rem; font-size: 0.95rem;
font-weight: 600; font-weight: $boldWeight;
line-height: 1.5rem; line-height: 1.5rem;
display: inline-block; display: inline-block;
} }
@ -110,7 +112,7 @@ svg {
font-size: 0.95rem; font-size: 0.95rem;
display: inline-block; display: inline-block;
color: var(--secondary); color: var(--secondary);
font-weight: 600; font-weight: $boldWeight;
margin: 0; margin: 0;
line-height: 1.5rem; line-height: 1.5rem;
pointer-events: none; pointer-events: none;

View File

@ -54,15 +54,11 @@
} }
& > #search-space { & > #search-space {
width: 50%; width: 65%;
margin-top: 15vh; margin-top: 12vh;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
@media all and (max-width: $fullPageWidth) {
width: 90%;
}
& > * { & > * {
width: 100%; width: 100%;
border-radius: 5px; border-radius: 5px;
@ -86,93 +82,134 @@
} }
} }
& > #results-container { & > #search-layout {
& .result-card { display: none;
padding: 1em; flex-direction: row;
cursor: pointer; border: 1px solid var(--lightgray);
transition: background 0.2s ease;
border: 1px solid var(--lightgray); &.display-results {
border-bottom: none; display: flex;
width: 100%; }
@media all and (min-width: $tabletBreakpoint) {
&[data-preview] {
& .result-card > p.preview {
display: none;
}
}
}
& > div {
// vh - #search-space.margin-top
height: calc(75vh - 12vh);
background: none;
&:first-child {
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
border-right: 1px solid var(--lightgray);
}
&:last-child {
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
}
}
@media all and (max-width: $tabletBreakpoint) {
display: block !important;
& > *:not(#results-container) {
display: none !important;
}
& > #results-container {
width: 100%;
height: auto;
}
}
& .highlight {
background: color-mix(in srgb, var(--tertiary) 60%, transparent);
border-radius: 5px;
scroll-margin-top: 2rem;
}
& > #preview-container {
display: block; display: block;
box-sizing: border-box; box-sizing: border-box;
overflow: hidden;
// normalize card props box-sizing: border-box;
font-family: inherit; font-family: inherit;
font-size: 100%; color: var(--dark);
line-height: 1.15; line-height: 1.5em;
margin: 0; font-weight: $normalWeight;
text-transform: none;
text-align: left;
background: var(--light); background: var(--light);
outline: none; border-top-right-radius: 5px;
font-weight: inherit; border-bottom-right-radius: 5px;
overflow-y: auto;
padding: 1rem;
& .highlight { & .preview-inner {
color: var(--secondary); margin: 0 auto;
font-weight: 700; width: min($pageWidth, 100%);
} }
}
&:hover, & > #results-container {
&:focus { overflow-y: auto;
background: var(--lightgray);
}
&:first-of-type { & .result-card {
border-top-left-radius: 5px; padding: 1em;
border-top-right-radius: 5px; cursor: pointer;
} transition: background 0.2s ease;
&:last-of-type {
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
border-bottom: 1px solid var(--lightgray); border-bottom: 1px solid var(--lightgray);
} width: 100%;
display: block;
box-sizing: border-box;
& > h3 { // normalize card props
font-family: inherit;
font-size: 100%;
line-height: 1.15;
margin: 0; margin: 0;
} text-transform: none;
text-align: left;
background: var(--light);
outline: none;
font-weight: inherit;
& > ul > li { &:focus,
margin: 0; &.focus {
display: inline-block; background: var(--lightgray);
white-space: nowrap; }
margin: 0;
overflow-wrap: normal;
}
& > ul { & > h3 {
list-style: none; margin: 0;
display: flex; }
padding-left: 0;
gap: 0.4rem;
margin: 0;
margin-top: 0.45rem;
// Offset border radius
margin-left: -2px;
overflow: hidden;
background-clip: border-box;
}
& > ul > li > p { & > ul.tags {
border-radius: 8px; margin-top: 0.45rem;
background-color: var(--highlight); margin-bottom: 0;
overflow: hidden; }
background-clip: border-box;
padding: 0.03rem 0.4rem;
margin: 0;
color: var(--secondary);
opacity: 0.85;
}
& > ul > li > .match-tag { & > ul > li > p {
color: var(--tertiary); border-radius: 8px;
font-weight: bold; background-color: var(--highlight);
opacity: 1; padding: 0.2rem 0.4rem;
} margin: 0 0.1rem;
line-height: 1.4rem;
font-weight: $boldWeight;
color: var(--secondary);
& > p { &.match-tag {
margin-bottom: 0; color: var(--tertiary);
}
}
& > p {
margin-bottom: 0;
}
} }
} }
} }

View File

@ -119,7 +119,7 @@ function addGlobalPageResources(
} else if (cfg.analytics?.provider === "umami") { } else if (cfg.analytics?.provider === "umami") {
componentResources.afterDOMLoaded.push(` componentResources.afterDOMLoaded.push(`
const umamiScript = document.createElement("script") const umamiScript = document.createElement("script")
umamiScript.src = "https://analytics.umami.is/script.js" umamiScript.src = cfg.analytics.host ?? "https://analytics.umami.is/script.js"
umamiScript.setAttribute("data-website-id", "${cfg.analytics.websiteId}") umamiScript.setAttribute("data-website-id", "${cfg.analytics.websiteId}")
umamiScript.async = true umamiScript.async = true
@ -131,9 +131,11 @@ function addGlobalPageResources(
componentResources.afterDOMLoaded.push(spaRouterScript) componentResources.afterDOMLoaded.push(spaRouterScript)
} else { } else {
componentResources.afterDOMLoaded.push(` componentResources.afterDOMLoaded.push(`
window.spaNavigate = (url, _) => window.location.assign(url) window.spaNavigate = (url, _) => window.location.assign(url)
const event = new CustomEvent("nav", { detail: { url: document.body.dataset.slug } }) window.addCleanup = () => {}
document.dispatchEvent(event)`) const event = new CustomEvent("nav", { detail: { url: document.body.dataset.slug } })
document.dispatchEvent(event)
`)
} }
let wsUrl = `ws://localhost:${ctx.argv.wsPort}` let wsUrl = `ws://localhost:${ctx.argv.wsPort}`
@ -147,9 +149,9 @@ function addGlobalPageResources(
loadTime: "afterDOMReady", loadTime: "afterDOMReady",
contentType: "inline", contentType: "inline",
script: ` script: `
const socket = new WebSocket('${wsUrl}') const socket = new WebSocket('${wsUrl}')
socket.addEventListener('message', () => document.location.reload()) socket.addEventListener('message', () => document.location.reload())
`, `,
}) })
} }
} }

View File

@ -5,7 +5,6 @@ import { escapeHTML } from "../../util/escape"
import { FilePath, FullSlug, SimpleSlug, joinSegments, simplifySlug } from "../../util/path" import { FilePath, FullSlug, SimpleSlug, joinSegments, simplifySlug } from "../../util/path"
import { QuartzEmitterPlugin } from "../types" import { QuartzEmitterPlugin } from "../types"
import { toHtml } from "hast-util-to-html" import { toHtml } from "hast-util-to-html"
import path from "path"
import { write } from "./helpers" import { write } from "./helpers"
export type ContentIndex = Map<FullSlug, ContentDetails> export type ContentIndex = Map<FullSlug, ContentDetails>

View File

@ -5,7 +5,6 @@ import yaml from "js-yaml"
import toml from "toml" import toml from "toml"
import { slugTag } from "../../util/path" import { slugTag } from "../../util/path"
import { QuartzPluginData } from "../vfile" import { QuartzPluginData } from "../vfile"
import chalk from "chalk"
export interface Options { export interface Options {
delims: string | string[] delims: string | string[]
@ -17,23 +16,6 @@ const defaultOptions: Options = {
language: "yaml", language: "yaml",
} }
function coerceDate(fp: string, d: unknown): Date | undefined {
if (d === undefined || d === null) return undefined
const dt = new Date(d as string | number)
const invalidDate = isNaN(dt.getTime()) || dt.getTime() === 0
if (invalidDate) {
console.log(
chalk.yellow(
`\nWarning: found invalid date "${d}" in \`${fp}\`. Supported formats: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format`,
),
)
return undefined
}
return dt
}
function coalesceAliases(data: { [key: string]: any }, aliases: string[]) { function coalesceAliases(data: { [key: string]: any }, aliases: string[]) {
for (const alias of aliases) { for (const alias of aliases) {
if (data[alias] !== undefined && data[alias] !== null) return data[alias] if (data[alias] !== undefined && data[alias] !== null) return data[alias]
@ -66,7 +48,6 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined>
[remarkFrontmatter, ["yaml", "toml"]], [remarkFrontmatter, ["yaml", "toml"]],
() => { () => {
return (_, file) => { return (_, file) => {
const fp = file.data.filePath!
const { data } = matter(Buffer.from(file.value), { const { data } = matter(Buffer.from(file.value), {
...opts, ...opts,
engines: { engines: {
@ -88,16 +69,6 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined>
if (aliases) data.aliases = aliases if (aliases) data.aliases = aliases
const cssclasses = coerceToArray(coalesceAliases(data, ["cssclasses", "cssclass"])) const cssclasses = coerceToArray(coalesceAliases(data, ["cssclasses", "cssclass"]))
if (cssclasses) data.cssclasses = cssclasses if (cssclasses) data.cssclasses = cssclasses
const created = coerceDate(fp, coalesceAliases(data, ["created", "date"]))
if (created) data.created = created
const modified = coerceDate(
fp,
coalesceAliases(data, ["modified", "lastmod", "updated", "last-modified"]),
)
if (modified) data.modified = modified
const published = coerceDate(fp, coalesceAliases(data, ["published", "publishDate"]))
if (published) data.published = published
// fill in frontmatter // fill in frontmatter
file.data.frontmatter = data as QuartzPluginData["frontmatter"] file.data.frontmatter = data as QuartzPluginData["frontmatter"]
@ -120,9 +91,6 @@ declare module "vfile" {
draft: boolean draft: boolean
enableToc: string enableToc: string
cssclasses: string[] cssclasses: string[]
created: Date
modified: Date
published: Date
}> }>
} }
} }

View File

@ -37,8 +37,36 @@ export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> |
"data-no-popover": true, "data-no-popover": true,
}, },
content: { content: {
type: "text", type: "element",
value: " §", tagName: "svg",
properties: {
width: 18,
height: 18,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
},
children: [
{
type: "element",
tagName: "path",
properties: {
d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71",
},
children: [],
},
{
type: "element",
tagName: "path",
properties: {
d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71",
},
children: [],
},
],
}, },
}, },
], ],

View File

@ -12,6 +12,21 @@ const defaultOptions: Options = {
priority: ["frontmatter", "git", "filesystem"], priority: ["frontmatter", "git", "filesystem"],
} }
function coerceDate(fp: string, d: any): Date {
const dt = new Date(d)
const invalidDate = isNaN(dt.getTime()) || dt.getTime() === 0
if (invalidDate && d !== undefined) {
console.log(
chalk.yellow(
`\nWarning: found invalid date "${d}" in \`${fp}\`. Supported formats: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format`,
),
)
}
return invalidDate ? new Date() : dt
}
type MaybeDate = undefined | string | number
export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | undefined> = ( export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | undefined> = (
userOpts, userOpts,
) => { ) => {
@ -23,21 +38,23 @@ export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | und
() => { () => {
let repo: Repository | undefined = undefined let repo: Repository | undefined = undefined
return async (_tree, file) => { return async (_tree, file) => {
let created: Date | undefined = undefined let created: MaybeDate = undefined
let modified: Date | undefined = undefined let modified: MaybeDate = undefined
let published: Date | undefined = undefined let published: MaybeDate = undefined
const fp = file.data.filePath! const fp = file.data.filePath!
const fullFp = path.posix.join(file.cwd, fp) const fullFp = path.isAbsolute(fp) ? fp : path.posix.join(file.cwd, fp)
for (const source of opts.priority) { for (const source of opts.priority) {
if (source === "filesystem") { if (source === "filesystem") {
const st = await fs.promises.stat(fullFp) const st = await fs.promises.stat(fullFp)
created ||= new Date(st.birthtimeMs) created ||= st.birthtimeMs
modified ||= new Date(st.mtimeMs) modified ||= st.mtimeMs
} else if (source === "frontmatter" && file.data.frontmatter) { } else if (source === "frontmatter" && file.data.frontmatter) {
created ||= file.data.frontmatter.created created ||= file.data.frontmatter.date as MaybeDate
modified ||= file.data.frontmatter.modified modified ||= file.data.frontmatter.lastmod as MaybeDate
published ||= file.data.frontmatter.published modified ||= file.data.frontmatter.updated as MaybeDate
modified ||= file.data.frontmatter["last-modified"] as MaybeDate
published ||= file.data.frontmatter.publishDate as MaybeDate
} else if (source === "git") { } else if (source === "git") {
if (!repo) { if (!repo) {
// Get a reference to the main git repo. // Get a reference to the main git repo.
@ -47,9 +64,7 @@ export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | und
} }
try { try {
modified ||= new Date( modified ||= await repo.getFileLatestModifiedDateAsync(file.data.filePath!)
await repo.getFileLatestModifiedDateAsync(file.data.filePath!),
)
} catch { } catch {
console.log( console.log(
chalk.yellow( chalk.yellow(
@ -61,13 +76,10 @@ export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | und
} }
} }
created ||= new Date()
modified ||= new Date()
published ||= new Date()
file.data.dates = { file.data.dates = {
created, created: coerceDate(fp, created),
modified, modified: coerceDate(fp, modified),
published, published: coerceDate(fp, published),
} }
} }
}, },

View File

@ -26,12 +26,12 @@ export const Latex: QuartzTransformerPlugin<Options> = (opts?: Options) => {
return { return {
css: [ css: [
// base css // base css
"https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css", "https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css",
], ],
js: [ js: [
{ {
// fix copy behaviour: https://github.com/KaTeX/KaTeX/blob/main/contrib/copy-tex/README.md // fix copy behaviour: https://github.com/KaTeX/KaTeX/blob/main/contrib/copy-tex/README.md
src: "https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/contrib/copy-tex.min.js", src: "https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/copy-tex.min.js",
loadTime: "afterDOMReady", loadTime: "afterDOMReady",
contentType: "external", contentType: "external",
}, },

View File

@ -44,39 +44,7 @@ const defaultOptions: Options = {
enableVideoEmbed: true, enableVideoEmbed: true,
} }
const icons = { const calloutMapping = {
infoIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>`,
pencilIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="2" x2="22" y2="6"></line><path d="M7.5 20.5 19 9l-4-4L3.5 16.5 2 22z"></path></svg>`,
clipboardListIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><path d="M12 11h4"></path><path d="M12 16h4"></path><path d="M8 11h.01"></path><path d="M8 16h.01"></path></svg>`,
checkCircleIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"></path><path d="m9 12 2 2 4-4"></path></svg>`,
flameIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"></path></svg>`,
checkIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>`,
helpCircleIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>`,
alertTriangleIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>`,
xIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`,
zapIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>`,
bugIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="8" height="14" x="8" y="6" rx="4"></rect><path d="m19 7-3 2"></path><path d="m5 7 3 2"></path><path d="m19 19-3-2"></path><path d="m5 19 3-2"></path><path d="M20 13h-4"></path><path d="M4 13h4"></path><path d="m10 4 1 2"></path><path d="m14 4-1 2"></path></svg>`,
listIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>`,
quoteIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"></path><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"></path></svg>`,
}
const callouts = {
note: icons.pencilIcon,
abstract: icons.clipboardListIcon,
info: icons.infoIcon,
todo: icons.checkCircleIcon,
tip: icons.flameIcon,
success: icons.checkIcon,
question: icons.helpCircleIcon,
warning: icons.alertTriangleIcon,
failure: icons.xIcon,
danger: icons.zapIcon,
bug: icons.bugIcon,
example: icons.listIcon,
quote: icons.quoteIcon,
}
const calloutMapping: Record<string, keyof typeof callouts> = {
note: "note", note: "note",
abstract: "abstract", abstract: "abstract",
summary: "abstract", summary: "abstract",
@ -104,12 +72,12 @@ const calloutMapping: Record<string, keyof typeof callouts> = {
example: "example", example: "example",
quote: "quote", quote: "quote",
cite: "quote", cite: "quote",
} } as const
function canonicalizeCallout(calloutName: string): keyof typeof callouts { function canonicalizeCallout(calloutName: string): keyof typeof calloutMapping {
let callout = calloutName.toLowerCase() as keyof typeof calloutMapping const normalizedCallout = calloutName.toLowerCase() as keyof typeof calloutMapping
// if callout is not recognized, make it a custom one // if callout is not recognized, make it a custom one
return calloutMapping[callout] ?? calloutName return calloutMapping[normalizedCallout] ?? calloutName
} }
export const externalLinkRegex = /^https?:\/\//i export const externalLinkRegex = /^https?:\/\//i
@ -138,6 +106,9 @@ const tagRegex = new RegExp(/(?:^| )#((?:[-_\p{L}\p{Emoji}\d])+(?:\/[-_\p{L}\p{E
const blockReferenceRegex = new RegExp(/\^([-_A-Za-z0-9]+)$/, "g") const blockReferenceRegex = new RegExp(/\^([-_A-Za-z0-9]+)$/, "g")
const ytLinkRegex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/ const ytLinkRegex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/
const videoExtensionRegex = new RegExp(/\.(mp4|webm|ogg|avi|mov|flv|wmv|mkv|mpg|mpeg|3gp|m4v)$/) const videoExtensionRegex = new RegExp(/\.(mp4|webm|ogg|avi|mov|flv|wmv|mkv|mpg|mpeg|3gp|m4v)$/)
const wikilinkImageEmbedRegex = new RegExp(
/^(?<alt>(?!^\d*x?\d*$).*?)?(\|?\s*?(?<width>\d+)(x(?<height>\d+))?)?$/,
)
export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = ( export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (
userOpts, userOpts,
@ -222,18 +193,10 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
const ext: string = path.extname(fp).toLowerCase() const ext: string = path.extname(fp).toLowerCase()
const url = slugifyFilePath(fp as FilePath) const url = slugifyFilePath(fp as FilePath)
if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg", ".webp"].includes(ext)) { if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg", ".webp"].includes(ext)) {
// either |alt|dims or |dims const match = wikilinkImageEmbedRegex.exec(alias ?? "")
let [alt, dims] = (alias ?? "").split("|") const alt = match?.groups?.alt ?? ""
const width = match?.groups?.width ?? "auto"
// |dims case, treat first alt slot as dims const height = match?.groups?.height ?? "auto"
if (dims === undefined) {
dims = alt
alt = ""
}
let [width, height] = dims.split("x", 2)
width ||= "auto"
height ||= "auto"
return { return {
type: "image", type: "image",
url, url,
@ -327,8 +290,9 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
} }
tag = slugTag(tag) tag = slugTag(tag)
if (file.data.frontmatter?.tags?.includes(tag)) { if (file.data.frontmatter) {
file.data.frontmatter.tags.push(tag) const noteTags = file.data.frontmatter.tags ?? []
file.data.frontmatter.tags = [...new Set([...noteTags, tag])]
} }
return { return {
@ -416,32 +380,31 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
const match = firstLine.match(calloutRegex) const match = firstLine.match(calloutRegex)
if (match && match.input) { if (match && match.input) {
const [calloutDirective, typeString, collapseChar] = match const [calloutDirective, typeString, collapseChar] = match
const calloutType = canonicalizeCallout( const calloutType = canonicalizeCallout(typeString.toLowerCase())
typeString.toLowerCase() as keyof typeof calloutMapping,
)
const collapse = collapseChar === "+" || collapseChar === "-" const collapse = collapseChar === "+" || collapseChar === "-"
const defaultState = collapseChar === "-" ? "collapsed" : "expanded" const defaultState = collapseChar === "-" ? "collapsed" : "expanded"
const titleContent = const titleContent = match.input.slice(calloutDirective.length).trim()
match.input.slice(calloutDirective.length).trim() || capitalize(calloutType) const useDefaultTitle = titleContent === "" && restOfTitle.length === 0
const titleNode: Paragraph = { const titleNode: Paragraph = {
type: "paragraph", type: "paragraph",
children: children: [
restOfTitle.length === 0 {
? [{ type: "text", value: titleContent + " " }] type: "text",
: restOfTitle, value: useDefaultTitle ? capitalize(calloutType) : titleContent + " ",
},
...restOfTitle,
],
} }
const title = mdastToHtml(titleNode) const title = mdastToHtml(titleNode)
const toggleIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="fold"> const toggleIcon = `<div class="fold-callout-icon"></div>`
<polyline points="6 9 12 15 18 9"></polyline>
</svg>`
const titleHtml: Html = { const titleHtml: Html = {
type: "html", type: "html",
value: `<div value: `<div
class="callout-title" class="callout-title"
> >
<div class="callout-icon">${callouts[calloutType] ?? callouts.note}</div> <div class="callout-icon"></div>
<div class="callout-title-inner">${title}</div> <div class="callout-title-inner">${title}</div>
${collapse ? toggleIcon : ""} ${collapse ? toggleIcon : ""}
</div>`, </div>`,

View File

@ -26,7 +26,7 @@ section {
} }
::selection { ::selection {
background: color-mix(in srgb, var(--tertiary) 75%, transparent); background: color-mix(in srgb, var(--tertiary) 60%, transparent);
color: var(--darkgray); color: var(--darkgray);
} }
@ -54,7 +54,7 @@ ul,
} }
a { a {
font-weight: 600; font-weight: $boldWeight;
text-decoration: none; text-decoration: none;
transition: color 0.2s ease; transition: color 0.2s ease;
color: var(--secondary); color: var(--secondary);
@ -171,9 +171,11 @@ a {
& .sidebar.right { & .sidebar.right {
right: calc(calc(100vw - $pageWidth) / 2 - $sidePanelWidth); right: calc(calc(100vw - $pageWidth) / 2 - $sidePanelWidth);
flex-wrap: wrap;
& > * { & > * {
@media all and (max-width: $fullPageWidth) { @media all and (max-width: $fullPageWidth) {
flex: 1; flex: 1;
min-width: 140px;
} }
} }
} }
@ -276,7 +278,6 @@ h6 {
opacity: 0; opacity: 0;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
transform: translateY(-0.1rem); transform: translateY(-0.1rem);
display: inline-block;
font-family: var(--codeFont); font-family: var(--codeFont);
user-select: none; user-select: none;
} }

View File

@ -1,3 +1,4 @@
@use "./variables.scss" as *;
@use "sass:color"; @use "sass:color";
.callout { .callout {
@ -13,16 +14,33 @@
margin-top: 0; margin-top: 0;
} }
--callout-icon-note: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="2" x2="22" y2="6"></line><path d="M7.5 20.5 19 9l-4-4L3.5 16.5 2 22z"></path></svg>');
--callout-icon-abstract: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><path d="M12 11h4"></path><path d="M12 16h4"></path><path d="M8 11h.01"></path><path d="M8 16h.01"></path></svg>');
--callout-icon-info: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>');
--callout-icon-todo: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"></path><path d="m9 12 2 2 4-4"></path></svg>');
--callout-icon-tip: url('data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"></path></svg> ');
--callout-icon-success: url('data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg> ');
--callout-icon-question: url('data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg> ');
--callout-icon-warning: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>');
--callout-icon-failure: url('data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg> ');
--callout-icon-danger: url('data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg> ');
--callout-icon-bug: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="8" height="14" x="8" y="6" rx="4"></rect><path d="m19 7-3 2"></path><path d="m5 7 3 2"></path><path d="m19 19-3-2"></path><path d="m5 19 3-2"></path><path d="M20 13h-4"></path><path d="M4 13h4"></path><path d="m10 4 1 2"></path><path d="m14 4-1 2"></path></svg>');
--callout-icon-example: url('data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg> ');
--callout-icon-quote: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"></path><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"></path></svg>');
--callout-icon-fold: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"%3E%3Cpolyline points="6 9 12 15 18 9"%3E%3C/polyline%3E%3C/svg%3E');
&[data-callout] { &[data-callout] {
--color: #448aff; --color: #448aff;
--border: #448aff44; --border: #448aff44;
--bg: #448aff10; --bg: #448aff10;
--callout-icon: var(--callout-icon-note);
} }
&[data-callout="abstract"] { &[data-callout="abstract"] {
--color: #00b0ff; --color: #00b0ff;
--border: #00b0ff44; --border: #00b0ff44;
--bg: #00b0ff10; --bg: #00b0ff10;
--callout-icon: var(--callout-icon-abstract);
} }
&[data-callout="info"], &[data-callout="info"],
@ -30,30 +48,39 @@
--color: #00b8d4; --color: #00b8d4;
--border: #00b8d444; --border: #00b8d444;
--bg: #00b8d410; --bg: #00b8d410;
--callout-icon: var(--callout-icon-info);
}
&[data-callout="todo"] {
--callout-icon: var(--callout-icon-todo);
} }
&[data-callout="tip"] { &[data-callout="tip"] {
--color: #00bfa5; --color: #00bfa5;
--border: #00bfa544; --border: #00bfa544;
--bg: #00bfa510; --bg: #00bfa510;
--callout-icon: var(--callout-icon-tip);
} }
&[data-callout="success"] { &[data-callout="success"] {
--color: #09ad7a; --color: #09ad7a;
--border: #09ad7144; --border: #09ad7144;
--bg: #09ad7110; --bg: #09ad7110;
--callout-icon: var(--callout-icon-success);
} }
&[data-callout="question"] { &[data-callout="question"] {
--color: #dba642; --color: #dba642;
--border: #dba64244; --border: #dba64244;
--bg: #dba64210; --bg: #dba64210;
--callout-icon: var(--callout-icon-question);
} }
&[data-callout="warning"] { &[data-callout="warning"] {
--color: #db8942; --color: #db8942;
--border: #db894244; --border: #db894244;
--bg: #db894210; --bg: #db894210;
--callout-icon: var(--callout-icon-warning);
} }
&[data-callout="failure"], &[data-callout="failure"],
@ -62,50 +89,74 @@
--color: #db4242; --color: #db4242;
--border: #db424244; --border: #db424244;
--bg: #db424210; --bg: #db424210;
--callout-icon: var(--callout-icon-failure);
}
&[data-callout="bug"] {
--callout-icon: var(--callout-icon-bug);
}
&[data-callout="danger"] {
--callout-icon: var(--callout-icon-danger);
} }
&[data-callout="example"] { &[data-callout="example"] {
--color: #7a43b5; --color: #7a43b5;
--border: #7a43b544; --border: #7a43b544;
--bg: #7a43b510; --bg: #7a43b510;
--callout-icon: var(--callout-icon-example);
} }
&[data-callout="quote"] { &[data-callout="quote"] {
--color: var(--secondary); --color: var(--secondary);
--border: var(--lightgray); --border: var(--lightgray);
--callout-icon: var(--callout-icon-quote);
} }
&.is-collapsed > .callout-title > .fold { &.is-collapsed > .callout-title > .fold-callout-icon {
transform: rotateZ(-90deg); transform: rotateZ(-90deg);
} }
} }
.callout-title { .callout-title {
display: flex; display: flex;
align-items: center;
gap: 5px; gap: 5px;
padding: 1rem 0; padding: 1rem 0;
color: var(--color); color: var(--color);
& .fold { --icon-size: 18px;
margin-left: 0.5rem;
transition: transform 0.3s ease; & .fold-callout-icon {
transition: transform 0.15s ease;
opacity: 0.8; opacity: 0.8;
cursor: pointer; cursor: pointer;
width: var(--icon-size);
height: var(--icon-size);
--callout-icon: var(--callout-icon-fold);
} }
& > .callout-title-inner > p { & > .callout-title-inner > p {
color: var(--color); color: var(--color);
margin: 0; margin: 0;
} }
}
.callout-icon { .callout-icon,
width: 18px; & .fold-callout-icon {
height: 18px; width: var(--icon-size);
flex: 0 0 18px; height: var(--icon-size);
padding-top: 4px;
}
.callout-title-inner { // icon support
font-weight: 700; background-size: var(--icon-size) var(--icon-size);
background-position: center;
background-color: var(--color);
mask-image: var(--callout-icon);
mask-size: var(--icon-size) var(--icon-size);
mask-position: center;
mask-repeat: no-repeat;
}
.callout-title-inner {
font-weight: $boldWeight;
}
} }

View File

@ -1,6 +1,8 @@
$pageWidth: 750px; $pageWidth: 750px;
$mobileBreakpoint: 600px; $mobileBreakpoint: 600px;
$tabletBreakpoint: 1200px; $tabletBreakpoint: 1000px;
$sidePanelWidth: 380px; $sidePanelWidth: 380px;
$topSpacing: 6rem; $topSpacing: 6rem;
$fullPageWidth: $pageWidth + 2 * $sidePanelWidth; $fullPageWidth: $pageWidth + 2 * $sidePanelWidth;
$boldWeight: 700;
$normalWeight: 400;

View File

@ -9,3 +9,13 @@ export function pluralize(count: number, s: string): string {
export function capitalize(s: string): string { export function capitalize(s: string): string {
return s.substring(0, 1).toUpperCase() + s.substring(1) return s.substring(0, 1).toUpperCase() + s.substring(1)
} }
export function classNames(
displayClass?: "mobile-only" | "desktop-only",
...classes: string[]
): string {
if (displayClass) {
classes.push(displayClass)
}
return classes.join(" ")
}

View File

@ -52,7 +52,7 @@ function sluggify(s: string): string {
.split("/") .split("/")
.map((segment) => .map((segment) =>
segment.replace(/\s/g, "-").replace(/%/g, "-percent").replace(/\?/g, "-q").replace(/#/g, ""), segment.replace(/\s/g, "-").replace(/%/g, "-percent").replace(/\?/g, "-q").replace(/#/g, ""),
) // slugify all segments )
.join("/") // always use / as sep .join("/") // always use / as sep
.replace(/\/$/, "") .replace(/\/$/, "")
} }

View File

@ -13,8 +13,8 @@
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"esModuleInterop": true, "esModuleInterop": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"jsxImportSource": "preact" "jsxImportSource": "preact",
}, },
"include": ["**/*.ts", "**/*.tsx", "./package.json"], "include": ["**/*.ts", "**/*.tsx", "./package.json"],
"exclude": ["build/**/*.d.ts"] "exclude": ["build/**/*.d.ts"],
} }