mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-12-13 08:09:01 +01:00
Compare commits
12 Commits
jackyzha0/
...
a8001e9554
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8001e9554 | ||
|
|
dd940a007c | ||
|
|
a71e17919b | ||
|
|
aca0c330e7 | ||
|
|
dcaf806190 | ||
|
|
23df17233d | ||
|
|
8d33608808 | ||
|
|
d618a4e3f3 | ||
|
|
9c8fec06d2 | ||
|
|
1cd8e7f0d5 | ||
|
|
5480269d38 | ||
|
|
a201105442 |
@@ -161,6 +161,18 @@ document.addEventListener("nav", () => {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can also add the equivalent of a `beforeunload` event for [[SPA Routing]] via the `prenav` event.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
document.addEventListener("prenav", () => {
|
||||||
|
// executed after an SPA navigation is triggered but
|
||||||
|
// before the page is replaced
|
||||||
|
// one usage pattern is to store things in sessionStorage
|
||||||
|
// in the prenav and then conditionally load then in the consequent
|
||||||
|
// nav
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
It is best practice to track any event handlers via `window.addCleanup` 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.
|
This will get called on page navigation.
|
||||||
|
|
||||||
|
|||||||
@@ -32,5 +32,3 @@ Want to see what Quartz can do? Here are some cool community gardens:
|
|||||||
- [🌓 Projects & Privacy - FOSS, tech, law](https://be-far.com)
|
- [🌓 Projects & Privacy - FOSS, tech, law](https://be-far.com)
|
||||||
- [Zen Browser Docs](https://docs.zen-browser.app)
|
- [Zen Browser Docs](https://docs.zen-browser.app)
|
||||||
- [🪴8cat life](https://8cat.life)
|
- [🪴8cat life](https://8cat.life)
|
||||||
|
|
||||||
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)!
|
|
||||||
|
|||||||
2
index.d.ts
vendored
2
index.d.ts
vendored
@@ -5,8 +5,10 @@ declare module "*.scss" {
|
|||||||
|
|
||||||
// dom custom event
|
// dom custom event
|
||||||
interface CustomEventMap {
|
interface CustomEventMap {
|
||||||
|
prenav: CustomEvent<{}>
|
||||||
nav: CustomEvent<{ url: FullSlug }>
|
nav: CustomEvent<{ url: FullSlug }>
|
||||||
themechange: CustomEvent<{ theme: "light" | "dark" }>
|
themechange: CustomEvent<{ theme: "light" | "dark" }>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ContentIndex = Record<FullSlug, ContentDetails>
|
||||||
declare const fetchData: Promise<ContentIndex>
|
declare const fetchData: Promise<ContentIndex>
|
||||||
|
|||||||
409
package-lock.json
generated
409
package-lock.json
generated
@@ -25,11 +25,11 @@
|
|||||||
"globby": "^14.1.0",
|
"globby": "^14.1.0",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"hast-util-to-html": "^9.0.5",
|
"hast-util-to-html": "^9.0.5",
|
||||||
"hast-util-to-jsx-runtime": "^2.3.5",
|
"hast-util-to-jsx-runtime": "^2.3.6",
|
||||||
"hast-util-to-string": "^3.0.1",
|
"hast-util-to-string": "^3.0.1",
|
||||||
"is-absolute-url": "^4.0.1",
|
"is-absolute-url": "^4.0.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lightningcss": "^1.29.1",
|
"lightningcss": "^1.29.2",
|
||||||
"mdast-util-find-and-replace": "^3.0.2",
|
"mdast-util-find-and-replace": "^3.0.2",
|
||||||
"mdast-util-to-hast": "^13.2.0",
|
"mdast-util-to-hast": "^13.2.0",
|
||||||
"mdast-util-to-string": "^4.0.0",
|
"mdast-util-to-string": "^4.0.0",
|
||||||
@@ -79,12 +79,12 @@
|
|||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
"@types/hast": "^3.0.4",
|
"@types/hast": "^3.0.4",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/node": "^22.13.9",
|
"@types/node": "^22.13.10",
|
||||||
"@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/ws": "^8.5.14",
|
"@types/ws": "^8.18.0",
|
||||||
"@types/yargs": "^17.0.33",
|
"@types/yargs": "^17.0.33",
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.1",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"tsx": "^4.19.3",
|
"tsx": "^4.19.3",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.2"
|
||||||
@@ -207,9 +207,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz",
|
||||||
"integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==",
|
"integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -223,9 +223,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm": {
|
"node_modules/@esbuild/android-arm": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz",
|
||||||
"integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==",
|
"integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -239,9 +239,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm64": {
|
"node_modules/@esbuild/android-arm64": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz",
|
||||||
"integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==",
|
"integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -255,9 +255,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-x64": {
|
"node_modules/@esbuild/android-x64": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz",
|
||||||
"integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==",
|
"integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -271,9 +271,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz",
|
||||||
"integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==",
|
"integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -287,9 +287,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz",
|
||||||
"integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==",
|
"integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -303,9 +303,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz",
|
||||||
"integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==",
|
"integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -319,9 +319,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz",
|
||||||
"integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==",
|
"integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -335,9 +335,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm": {
|
"node_modules/@esbuild/linux-arm": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz",
|
||||||
"integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==",
|
"integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -351,9 +351,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz",
|
||||||
"integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==",
|
"integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -367,9 +367,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz",
|
||||||
"integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==",
|
"integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -383,9 +383,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz",
|
||||||
"integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==",
|
"integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
@@ -399,9 +399,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz",
|
||||||
"integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==",
|
"integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
@@ -415,9 +415,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz",
|
||||||
"integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==",
|
"integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -431,9 +431,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz",
|
||||||
"integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==",
|
"integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@@ -447,9 +447,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz",
|
||||||
"integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==",
|
"integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -463,9 +463,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-x64": {
|
"node_modules/@esbuild/linux-x64": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz",
|
||||||
"integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==",
|
"integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -479,9 +479,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-arm64": {
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz",
|
||||||
"integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==",
|
"integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -495,9 +495,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz",
|
||||||
"integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==",
|
"integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -510,10 +510,26 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.25.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz",
|
||||||
|
"integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz",
|
||||||
"integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==",
|
"integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -527,9 +543,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz",
|
||||||
"integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==",
|
"integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -543,9 +559,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz",
|
||||||
"integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==",
|
"integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -559,9 +575,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz",
|
||||||
"integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==",
|
"integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -575,9 +591,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-x64": {
|
"node_modules/@esbuild/win32-x64": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz",
|
||||||
"integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==",
|
"integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1916,9 +1932,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.13.9",
|
"version": "22.13.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz",
|
||||||
"integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==",
|
"integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1946,9 +1962,9 @@
|
|||||||
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
|
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
|
||||||
},
|
},
|
||||||
"node_modules/@types/ws": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.5.14",
|
"version": "8.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz",
|
||||||
"integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==",
|
"integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2854,14 +2870,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "1.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
||||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
|
||||||
"bin": {
|
"license": "Apache-2.0",
|
||||||
"detect-libc": "bin/detect-libc.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/devlop": {
|
"node_modules/devlop": {
|
||||||
@@ -2909,9 +2923,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz",
|
||||||
"integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
|
"integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -2921,31 +2935,31 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@esbuild/aix-ppc64": "0.25.0",
|
"@esbuild/aix-ppc64": "0.25.1",
|
||||||
"@esbuild/android-arm": "0.25.0",
|
"@esbuild/android-arm": "0.25.1",
|
||||||
"@esbuild/android-arm64": "0.25.0",
|
"@esbuild/android-arm64": "0.25.1",
|
||||||
"@esbuild/android-x64": "0.25.0",
|
"@esbuild/android-x64": "0.25.1",
|
||||||
"@esbuild/darwin-arm64": "0.25.0",
|
"@esbuild/darwin-arm64": "0.25.1",
|
||||||
"@esbuild/darwin-x64": "0.25.0",
|
"@esbuild/darwin-x64": "0.25.1",
|
||||||
"@esbuild/freebsd-arm64": "0.25.0",
|
"@esbuild/freebsd-arm64": "0.25.1",
|
||||||
"@esbuild/freebsd-x64": "0.25.0",
|
"@esbuild/freebsd-x64": "0.25.1",
|
||||||
"@esbuild/linux-arm": "0.25.0",
|
"@esbuild/linux-arm": "0.25.1",
|
||||||
"@esbuild/linux-arm64": "0.25.0",
|
"@esbuild/linux-arm64": "0.25.1",
|
||||||
"@esbuild/linux-ia32": "0.25.0",
|
"@esbuild/linux-ia32": "0.25.1",
|
||||||
"@esbuild/linux-loong64": "0.25.0",
|
"@esbuild/linux-loong64": "0.25.1",
|
||||||
"@esbuild/linux-mips64el": "0.25.0",
|
"@esbuild/linux-mips64el": "0.25.1",
|
||||||
"@esbuild/linux-ppc64": "0.25.0",
|
"@esbuild/linux-ppc64": "0.25.1",
|
||||||
"@esbuild/linux-riscv64": "0.25.0",
|
"@esbuild/linux-riscv64": "0.25.1",
|
||||||
"@esbuild/linux-s390x": "0.25.0",
|
"@esbuild/linux-s390x": "0.25.1",
|
||||||
"@esbuild/linux-x64": "0.25.0",
|
"@esbuild/linux-x64": "0.25.1",
|
||||||
"@esbuild/netbsd-arm64": "0.25.0",
|
"@esbuild/netbsd-arm64": "0.25.1",
|
||||||
"@esbuild/netbsd-x64": "0.25.0",
|
"@esbuild/netbsd-x64": "0.25.1",
|
||||||
"@esbuild/openbsd-arm64": "0.25.0",
|
"@esbuild/openbsd-arm64": "0.25.1",
|
||||||
"@esbuild/openbsd-x64": "0.25.0",
|
"@esbuild/openbsd-x64": "0.25.1",
|
||||||
"@esbuild/sunos-x64": "0.25.0",
|
"@esbuild/sunos-x64": "0.25.1",
|
||||||
"@esbuild/win32-arm64": "0.25.0",
|
"@esbuild/win32-arm64": "0.25.1",
|
||||||
"@esbuild/win32-ia32": "0.25.0",
|
"@esbuild/win32-ia32": "0.25.1",
|
||||||
"@esbuild/win32-x64": "0.25.0"
|
"@esbuild/win32-x64": "0.25.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esbuild-sass-plugin": {
|
"node_modules/esbuild-sass-plugin": {
|
||||||
@@ -2962,22 +2976,6 @@
|
|||||||
"sass-embedded": "^1.71.1"
|
"sass-embedded": "^1.71.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esbuild/node_modules/@esbuild/openbsd-arm64": {
|
|
||||||
"version": "0.25.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz",
|
|
||||||
"integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||||
@@ -3551,9 +3549,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hast-util-to-jsx-runtime": {
|
"node_modules/hast-util-to-jsx-runtime": {
|
||||||
"version": "2.3.5",
|
"version": "2.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
|
||||||
"integrity": "sha512-gHD+HoFxOMmmXLuq9f2dZDMQHVcplCVpMfBNRpJsF03yyLZvJGzsFORe8orVuYDX9k2w0VH0uF8oryFd1whqKQ==",
|
"integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "^1.0.0",
|
"@types/estree": "^1.0.0",
|
||||||
@@ -3568,7 +3566,7 @@
|
|||||||
"mdast-util-mdxjs-esm": "^2.0.0",
|
"mdast-util-mdxjs-esm": "^2.0.0",
|
||||||
"property-information": "^7.0.0",
|
"property-information": "^7.0.0",
|
||||||
"space-separated-tokens": "^2.0.0",
|
"space-separated-tokens": "^2.0.0",
|
||||||
"style-to-object": "^1.0.0",
|
"style-to-js": "^1.0.0",
|
||||||
"unist-util-position": "^5.0.0",
|
"unist-util-position": "^5.0.0",
|
||||||
"vfile-message": "^4.0.0"
|
"vfile-message": "^4.0.0"
|
||||||
},
|
},
|
||||||
@@ -3773,9 +3771,10 @@
|
|||||||
"integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw=="
|
"integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw=="
|
||||||
},
|
},
|
||||||
"node_modules/inline-style-parser": {
|
"node_modules/inline-style-parser": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz",
|
||||||
"integrity": "sha512-EcKzdTHVe8wFVOGEYXiW9WmJXPjqi1T+234YpJr98RiFYKHV3cdy1+3mkTE+KHTHxFFLH51SfaGOoUdW+v7ViQ=="
|
"integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/internmap": {
|
"node_modules/internmap": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
@@ -3988,11 +3987,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.29.1",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz",
|
||||||
"integrity": "sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q==",
|
"integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==",
|
||||||
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"detect-libc": "^1.0.3"
|
"detect-libc": "^2.0.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
@@ -4002,25 +4002,26 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"lightningcss-darwin-arm64": "1.29.1",
|
"lightningcss-darwin-arm64": "1.29.2",
|
||||||
"lightningcss-darwin-x64": "1.29.1",
|
"lightningcss-darwin-x64": "1.29.2",
|
||||||
"lightningcss-freebsd-x64": "1.29.1",
|
"lightningcss-freebsd-x64": "1.29.2",
|
||||||
"lightningcss-linux-arm-gnueabihf": "1.29.1",
|
"lightningcss-linux-arm-gnueabihf": "1.29.2",
|
||||||
"lightningcss-linux-arm64-gnu": "1.29.1",
|
"lightningcss-linux-arm64-gnu": "1.29.2",
|
||||||
"lightningcss-linux-arm64-musl": "1.29.1",
|
"lightningcss-linux-arm64-musl": "1.29.2",
|
||||||
"lightningcss-linux-x64-gnu": "1.29.1",
|
"lightningcss-linux-x64-gnu": "1.29.2",
|
||||||
"lightningcss-linux-x64-musl": "1.29.1",
|
"lightningcss-linux-x64-musl": "1.29.2",
|
||||||
"lightningcss-win32-arm64-msvc": "1.29.1",
|
"lightningcss-win32-arm64-msvc": "1.29.2",
|
||||||
"lightningcss-win32-x64-msvc": "1.29.1"
|
"lightningcss-win32-x64-msvc": "1.29.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-darwin-arm64": {
|
"node_modules/lightningcss-darwin-arm64": {
|
||||||
"version": "1.29.1",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz",
|
||||||
"integrity": "sha512-HtR5XJ5A0lvCqYAoSv2QdZZyoHNttBpa5EP9aNuzBQeKGfbyH5+UipLWvVzpP4Uml5ej4BYs5I9Lco9u1fECqw==",
|
"integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
@@ -4034,12 +4035,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-darwin-x64": {
|
"node_modules/lightningcss-darwin-x64": {
|
||||||
"version": "1.29.1",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz",
|
||||||
"integrity": "sha512-k33G9IzKUpHy/J/3+9MCO4e+PzaFblsgBjSGlpAaFikeBFm8B/CkO3cKU9oI4g+fjS2KlkLM/Bza9K/aw8wsNA==",
|
"integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
@@ -4053,12 +4055,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-freebsd-x64": {
|
"node_modules/lightningcss-freebsd-x64": {
|
||||||
"version": "1.29.1",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz",
|
||||||
"integrity": "sha512-0SUW22fv/8kln2LnIdOCmSuXnxgxVC276W5KLTwoehiO0hxkacBxjHOL5EtHD8BAXg2BvuhsJPmVMasvby3LiQ==",
|
"integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"freebsd"
|
"freebsd"
|
||||||
@@ -4072,12 +4075,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||||
"version": "1.29.1",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz",
|
||||||
"integrity": "sha512-sD32pFvlR0kDlqsOZmYqH/68SqUMPNj+0pucGxToXZi4XZgZmqeX/NkxNKCPsswAXU3UeYgDSpGhu05eAufjDg==",
|
"integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -4091,12 +4095,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||||
"version": "1.29.1",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz",
|
||||||
"integrity": "sha512-0+vClRIZ6mmJl/dxGuRsE197o1HDEeeRk6nzycSy2GofC2JsY4ifCRnvUWf/CUBQmlrvMzt6SMQNMSEu22csWQ==",
|
"integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -4110,12 +4115,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm64-musl": {
|
"node_modules/lightningcss-linux-arm64-musl": {
|
||||||
"version": "1.29.1",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz",
|
||||||
"integrity": "sha512-UKMFrG4rL/uHNgelBsDwJcBqVpzNJbzsKkbI3Ja5fg00sgQnHw/VrzUTEc4jhZ+AN2BvQYz/tkHu4vt1kLuJyw==",
|
"integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -4129,12 +4135,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-x64-gnu": {
|
"node_modules/lightningcss-linux-x64-gnu": {
|
||||||
"version": "1.29.1",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz",
|
||||||
"integrity": "sha512-u1S+xdODy/eEtjADqirA774y3jLcm8RPtYztwReEXoZKdzgsHYPl0s5V52Tst+GKzqjebkULT86XMSxejzfISw==",
|
"integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -4148,12 +4155,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-x64-musl": {
|
"node_modules/lightningcss-linux-x64-musl": {
|
||||||
"version": "1.29.1",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz",
|
||||||
"integrity": "sha512-L0Tx0DtaNUTzXv0lbGCLB/c/qEADanHbu4QdcNOXLIe1i8i22rZRpbT3gpWYsCh9aSL9zFujY/WmEXIatWvXbw==",
|
"integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -4167,12 +4175,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||||
"version": "1.29.1",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz",
|
||||||
"integrity": "sha512-QoOVnkIEFfbW4xPi+dpdft/zAKmgLgsRHfJalEPYuJDOWf7cLQzYg0DEh8/sn737FaeMJxHZRc1oBreiwZCjog==",
|
"integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -4186,12 +4195,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-win32-x64-msvc": {
|
"node_modules/lightningcss-win32-x64-msvc": {
|
||||||
"version": "1.29.1",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz",
|
||||||
"integrity": "sha512-NygcbThNBe4JElP+olyTI/doBNGJvLs3bFCRPdvuCcxZCcCZ71B858IHpdm7L1btZex0FvCmM17FK98Y9MRy1Q==",
|
"integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -6606,15 +6616,6 @@
|
|||||||
"@img/sharp-win32-x64": "0.33.5"
|
"@img/sharp-win32-x64": "0.33.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sharp/node_modules/detect-libc": {
|
|
||||||
"version": "2.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
|
||||||
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
@@ -6860,12 +6861,22 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/style-to-object": {
|
"node_modules/style-to-js": {
|
||||||
"version": "1.0.5",
|
"version": "1.1.16",
|
||||||
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz",
|
||||||
"integrity": "sha512-rDRwHtoDD3UMMrmZ6BzOW0naTjMsVZLIjsGleSKS/0Oz+cgCfAPRspaqJuE8rDzpKha/nEvnM0IF4seEAZUTKQ==",
|
"integrity": "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"inline-style-parser": "0.2.2"
|
"style-to-object": "1.0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/style-to-object": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inline-style-parser": "0.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/supports-color": {
|
"node_modules/supports-color": {
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -16,7 +16,7 @@
|
|||||||
"docs": "npx quartz build --serve -d docs",
|
"docs": "npx quartz build --serve -d docs",
|
||||||
"check": "tsc --noEmit && npx prettier . --check",
|
"check": "tsc --noEmit && npx prettier . --check",
|
||||||
"format": "npx prettier . --write",
|
"format": "npx prettier . --write",
|
||||||
"test": "tsx ./quartz/util/path.test.ts && tsx ./quartz/depgraph.test.ts",
|
"test": "tsx --test",
|
||||||
"profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1"
|
"profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -51,11 +51,11 @@
|
|||||||
"globby": "^14.1.0",
|
"globby": "^14.1.0",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"hast-util-to-html": "^9.0.5",
|
"hast-util-to-html": "^9.0.5",
|
||||||
"hast-util-to-jsx-runtime": "^2.3.5",
|
"hast-util-to-jsx-runtime": "^2.3.6",
|
||||||
"hast-util-to-string": "^3.0.1",
|
"hast-util-to-string": "^3.0.1",
|
||||||
"is-absolute-url": "^4.0.1",
|
"is-absolute-url": "^4.0.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lightningcss": "^1.29.1",
|
"lightningcss": "^1.29.2",
|
||||||
"mdast-util-find-and-replace": "^3.0.2",
|
"mdast-util-find-and-replace": "^3.0.2",
|
||||||
"mdast-util-to-hast": "^13.2.0",
|
"mdast-util-to-hast": "^13.2.0",
|
||||||
"mdast-util-to-string": "^4.0.0",
|
"mdast-util-to-string": "^4.0.0",
|
||||||
@@ -102,12 +102,12 @@
|
|||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
"@types/hast": "^3.0.4",
|
"@types/hast": "^3.0.4",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/node": "^22.13.9",
|
"@types/node": "^22.13.10",
|
||||||
"@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/ws": "^8.5.14",
|
"@types/ws": "^8.18.0",
|
||||||
"@types/yargs": "^17.0.33",
|
"@types/yargs": "^17.0.33",
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.1",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"tsx": "^4.19.3",
|
"tsx": "^4.19.3",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.2"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import * as Plugin from "./quartz/plugins"
|
|||||||
*/
|
*/
|
||||||
const config: QuartzConfig = {
|
const config: QuartzConfig = {
|
||||||
configuration: {
|
configuration: {
|
||||||
pageTitle: "🪴 Quartz 4",
|
pageTitle: "Quartz 4",
|
||||||
pageTitleSuffix: "",
|
pageTitleSuffix: "",
|
||||||
enableSPA: true,
|
enableSPA: true,
|
||||||
enablePopovers: true,
|
enablePopovers: true,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { options } from "./util/sourcemap"
|
|||||||
import { Mutex } from "async-mutex"
|
import { Mutex } from "async-mutex"
|
||||||
import DepGraph from "./depgraph"
|
import DepGraph from "./depgraph"
|
||||||
import { getStaticResourcesFromPlugins } from "./plugins"
|
import { getStaticResourcesFromPlugins } from "./plugins"
|
||||||
|
import { randomIdNonSecure } from "./util/random"
|
||||||
|
|
||||||
type Dependencies = Record<string, DepGraph<FilePath> | null>
|
type Dependencies = Record<string, DepGraph<FilePath> | null>
|
||||||
|
|
||||||
@@ -38,13 +39,9 @@ type BuildData = {
|
|||||||
|
|
||||||
type FileEvent = "add" | "change" | "delete"
|
type FileEvent = "add" | "change" | "delete"
|
||||||
|
|
||||||
function newBuildId() {
|
|
||||||
return Math.random().toString(36).substring(2, 8)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
||||||
const ctx: BuildCtx = {
|
const ctx: BuildCtx = {
|
||||||
buildId: newBuildId(),
|
buildId: randomIdNonSecure(),
|
||||||
argv,
|
argv,
|
||||||
cfg,
|
cfg,
|
||||||
allSlugs: [],
|
allSlugs: [],
|
||||||
@@ -162,7 +159,7 @@ async function partialRebuildFromEntrypoint(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildId = newBuildId()
|
const buildId = randomIdNonSecure()
|
||||||
ctx.buildId = buildId
|
ctx.buildId = buildId
|
||||||
buildData.lastBuildMs = new Date().getTime()
|
buildData.lastBuildMs = new Date().getTime()
|
||||||
const release = await mut.acquire()
|
const release = await mut.acquire()
|
||||||
@@ -359,7 +356,7 @@ async function rebuildFromEntrypoint(
|
|||||||
toRemove.add(filePath)
|
toRemove.add(filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildId = newBuildId()
|
const buildId = randomIdNonSecure()
|
||||||
ctx.buildId = buildId
|
ctx.buildId = buildId
|
||||||
buildData.lastBuildMs = new Date().getTime()
|
buildData.lastBuildMs = new Date().getTime()
|
||||||
const release = await mut.acquire()
|
const release = await mut.acquire()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import style from "./styles/backlinks.scss"
|
|||||||
import { resolveRelative, simplifySlug } from "../util/path"
|
import { resolveRelative, simplifySlug } from "../util/path"
|
||||||
import { i18n } from "../i18n"
|
import { i18n } from "../i18n"
|
||||||
import { classNames } from "../util/lang"
|
import { classNames } from "../util/lang"
|
||||||
|
import OverflowListFactory from "./OverflowList"
|
||||||
|
|
||||||
interface BacklinksOptions {
|
interface BacklinksOptions {
|
||||||
hideWhenEmpty: boolean
|
hideWhenEmpty: boolean
|
||||||
@@ -14,6 +15,7 @@ const defaultOptions: BacklinksOptions = {
|
|||||||
|
|
||||||
export default ((opts?: Partial<BacklinksOptions>) => {
|
export default ((opts?: Partial<BacklinksOptions>) => {
|
||||||
const options: BacklinksOptions = { ...defaultOptions, ...opts }
|
const options: BacklinksOptions = { ...defaultOptions, ...opts }
|
||||||
|
const { OverflowList, overflowListAfterDOMLoaded } = OverflowListFactory()
|
||||||
|
|
||||||
const Backlinks: QuartzComponent = ({
|
const Backlinks: QuartzComponent = ({
|
||||||
fileData,
|
fileData,
|
||||||
@@ -29,7 +31,7 @@ export default ((opts?: Partial<BacklinksOptions>) => {
|
|||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "backlinks")}>
|
<div class={classNames(displayClass, "backlinks")}>
|
||||||
<h3>{i18n(cfg.locale).components.backlinks.title}</h3>
|
<h3>{i18n(cfg.locale).components.backlinks.title}</h3>
|
||||||
<ul class="overflow">
|
<OverflowList>
|
||||||
{backlinkFiles.length > 0 ? (
|
{backlinkFiles.length > 0 ? (
|
||||||
backlinkFiles.map((f) => (
|
backlinkFiles.map((f) => (
|
||||||
<li>
|
<li>
|
||||||
@@ -41,12 +43,13 @@ export default ((opts?: Partial<BacklinksOptions>) => {
|
|||||||
) : (
|
) : (
|
||||||
<li>{i18n(cfg.locale).components.backlinks.noBacklinksFound}</li>
|
<li>{i18n(cfg.locale).components.backlinks.noBacklinksFound}</li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</OverflowList>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Backlinks.css = style
|
Backlinks.css = style
|
||||||
|
Backlinks.afterDOMLoaded = overflowListAfterDOMLoaded
|
||||||
|
|
||||||
return Backlinks
|
return Backlinks
|
||||||
}) satisfies QuartzComponentConstructor
|
}) satisfies QuartzComponentConstructor
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
// @ts-ignore: this is safe, we don't want to actually make darkmode.inline.ts a module as
|
// @ts-ignore
|
||||||
// modules are automatically deferred and we don't want that to happen for critical beforeDOMLoads
|
|
||||||
// see: https://v8.dev/features/modules#defer
|
|
||||||
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 { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
@@ -9,12 +7,12 @@ import { classNames } from "../util/lang"
|
|||||||
|
|
||||||
const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
||||||
return (
|
return (
|
||||||
<button class={classNames(displayClass, "darkmode")} id="darkmode">
|
<button class={classNames(displayClass, "darkmode")}>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="dayIcon"
|
class="dayIcon"
|
||||||
x="0px"
|
x="0px"
|
||||||
y="0px"
|
y="0px"
|
||||||
viewBox="0 0 35 35"
|
viewBox="0 0 35 35"
|
||||||
@@ -29,7 +27,7 @@ const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps)
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="nightIcon"
|
class="nightIcon"
|
||||||
x="0px"
|
x="0px"
|
||||||
y="0px"
|
y="0px"
|
||||||
viewBox="0 0 100 100"
|
viewBox="0 0 100 100"
|
||||||
|
|||||||
@@ -3,22 +3,35 @@ import style from "./styles/explorer.scss"
|
|||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import script from "./scripts/explorer.inline"
|
import script from "./scripts/explorer.inline"
|
||||||
import { ExplorerNode, FileNode, Options } from "./ExplorerNode"
|
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
|
||||||
import { classNames } from "../util/lang"
|
import { classNames } from "../util/lang"
|
||||||
import { i18n } from "../i18n"
|
import { i18n } from "../i18n"
|
||||||
|
import { FileTrieNode } from "../util/fileTrie"
|
||||||
|
import OverflowListFactory from "./OverflowList"
|
||||||
|
import { concatenateResources } from "../util/resources"
|
||||||
|
|
||||||
// Options interface defined in `ExplorerNode` to avoid circular dependency
|
type OrderEntries = "sort" | "filter" | "map"
|
||||||
const defaultOptions = {
|
|
||||||
folderClickBehavior: "collapse",
|
export interface Options {
|
||||||
|
title?: string
|
||||||
|
folderDefaultState: "collapsed" | "open"
|
||||||
|
folderClickBehavior: "collapse" | "link"
|
||||||
|
useSavedState: boolean
|
||||||
|
sortFn: (a: FileTrieNode, b: FileTrieNode) => number
|
||||||
|
filterFn: (node: FileTrieNode) => boolean
|
||||||
|
mapFn: (node: FileTrieNode) => void
|
||||||
|
order: OrderEntries[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultOptions: Options = {
|
||||||
folderDefaultState: "collapsed",
|
folderDefaultState: "collapsed",
|
||||||
|
folderClickBehavior: "link",
|
||||||
useSavedState: true,
|
useSavedState: true,
|
||||||
mapFn: (node) => {
|
mapFn: (node) => {
|
||||||
return node
|
return node
|
||||||
},
|
},
|
||||||
sortFn: (a, b) => {
|
sortFn: (a, b) => {
|
||||||
// Sort order: folders first, then files. Sort folders and files alphabetically
|
// Sort order: folders first, then files. Sort folders and files alphabeticall
|
||||||
if ((!a.file && !b.file) || (a.file && b.file)) {
|
if ((!a.isFolder && !b.isFolder) || (a.isFolder && b.isFolder)) {
|
||||||
// numeric: true: Whether numeric collation should be used, such that "1" < "2" < "10"
|
// numeric: true: Whether numeric collation should be used, such that "1" < "2" < "10"
|
||||||
// sensitivity: "base": Only strings that differ in base letters compare as unequal. Examples: a ≠ b, a = á, a = A
|
// sensitivity: "base": Only strings that differ in base letters compare as unequal. Examples: a ≠ b, a = á, a = A
|
||||||
return a.displayName.localeCompare(b.displayName, undefined, {
|
return a.displayName.localeCompare(b.displayName, undefined, {
|
||||||
@@ -27,75 +40,44 @@ const defaultOptions = {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (a.file && !b.file) {
|
if (!a.isFolder && b.isFolder) {
|
||||||
return 1
|
return 1
|
||||||
} else {
|
} else {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
filterFn: (node) => node.name !== "tags",
|
filterFn: (node) => node.slugSegment !== "tags",
|
||||||
order: ["filter", "map", "sort"],
|
order: ["filter", "map", "sort"],
|
||||||
} satisfies Options
|
}
|
||||||
|
|
||||||
|
export type FolderState = {
|
||||||
|
path: string
|
||||||
|
collapsed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export default ((userOpts?: Partial<Options>) => {
|
export default ((userOpts?: Partial<Options>) => {
|
||||||
// Parse config
|
|
||||||
const opts: Options = { ...defaultOptions, ...userOpts }
|
const opts: Options = { ...defaultOptions, ...userOpts }
|
||||||
|
const { OverflowList, overflowListAfterDOMLoaded } = OverflowListFactory()
|
||||||
|
|
||||||
// memoized
|
const Explorer: QuartzComponent = ({ cfg, displayClass }: QuartzComponentProps) => {
|
||||||
let fileTree: FileNode
|
|
||||||
let jsonTree: string
|
|
||||||
let lastBuildId: string = ""
|
|
||||||
|
|
||||||
function constructFileTree(allFiles: QuartzPluginData[]) {
|
|
||||||
// Construct tree from allFiles
|
|
||||||
fileTree = new FileNode("")
|
|
||||||
allFiles.forEach((file) => fileTree.add(file))
|
|
||||||
|
|
||||||
// Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied)
|
|
||||||
if (opts.order) {
|
|
||||||
// Order is important, use loop with index instead of order.map()
|
|
||||||
for (let i = 0; i < opts.order.length; i++) {
|
|
||||||
const functionName = opts.order[i]
|
|
||||||
if (functionName === "map") {
|
|
||||||
fileTree.map(opts.mapFn)
|
|
||||||
} else if (functionName === "sort") {
|
|
||||||
fileTree.sort(opts.sortFn)
|
|
||||||
} else if (functionName === "filter") {
|
|
||||||
fileTree.filter(opts.filterFn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all folders of tree. Initialize with collapsed state
|
|
||||||
// Stringify to pass json tree as data attribute ([data-tree])
|
|
||||||
const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed")
|
|
||||||
jsonTree = JSON.stringify(folders)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Explorer: QuartzComponent = ({
|
|
||||||
ctx,
|
|
||||||
cfg,
|
|
||||||
allFiles,
|
|
||||||
displayClass,
|
|
||||||
fileData,
|
|
||||||
}: QuartzComponentProps) => {
|
|
||||||
if (ctx.buildId !== lastBuildId) {
|
|
||||||
lastBuildId = ctx.buildId
|
|
||||||
constructFileTree(allFiles)
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "explorer")}>
|
<div
|
||||||
|
class={classNames(displayClass, "explorer")}
|
||||||
|
data-behavior={opts.folderClickBehavior}
|
||||||
|
data-collapsed={opts.folderDefaultState}
|
||||||
|
data-savestate={opts.useSavedState}
|
||||||
|
data-data-fns={JSON.stringify({
|
||||||
|
order: opts.order,
|
||||||
|
sortFn: opts.sortFn.toString(),
|
||||||
|
filterFn: opts.filterFn.toString(),
|
||||||
|
mapFn: opts.mapFn.toString(),
|
||||||
|
})}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
id="mobile-explorer"
|
class="explorer-toggle mobile-explorer hide-until-loaded"
|
||||||
class="collapsed hide-until-loaded"
|
|
||||||
data-behavior={opts.folderClickBehavior}
|
|
||||||
data-collapsed={opts.folderDefaultState}
|
|
||||||
data-savestate={opts.useSavedState}
|
|
||||||
data-tree={jsonTree}
|
|
||||||
data-mobile={true}
|
data-mobile={true}
|
||||||
aria-controls="explorer-content"
|
aria-controls="explorer-content"
|
||||||
aria-expanded={false}
|
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -105,7 +87,7 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
class="lucide lucide-menu"
|
class="lucide-menu"
|
||||||
>
|
>
|
||||||
<line x1="4" x2="20" y1="12" y2="12" />
|
<line x1="4" x2="20" y1="12" y2="12" />
|
||||||
<line x1="4" x2="20" y1="6" y2="6" />
|
<line x1="4" x2="20" y1="6" y2="6" />
|
||||||
@@ -114,14 +96,8 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
id="desktop-explorer"
|
class="title-button explorer-toggle desktop-explorer"
|
||||||
class="title-button"
|
|
||||||
data-behavior={opts.folderClickBehavior}
|
|
||||||
data-collapsed={opts.folderDefaultState}
|
|
||||||
data-savestate={opts.useSavedState}
|
|
||||||
data-tree={jsonTree}
|
|
||||||
data-mobile={false}
|
data-mobile={false}
|
||||||
aria-controls="explorer-content"
|
|
||||||
aria-expanded={true}
|
aria-expanded={true}
|
||||||
>
|
>
|
||||||
<h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2>
|
<h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2>
|
||||||
@@ -140,17 +116,47 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
<polyline points="6 9 12 15 18 9"></polyline>
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div id="explorer-content">
|
<div class="explorer-content" aria-expanded={false}>
|
||||||
<ul class="overflow" id="explorer-ul">
|
<OverflowList class="explorer-ul" />
|
||||||
<ExplorerNode node={fileTree} opts={opts} fileData={fileData} />
|
|
||||||
<li id="explorer-end" />
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
<template id="template-file">
|
||||||
|
<li>
|
||||||
|
<a href="#"></a>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
<template id="template-folder">
|
||||||
|
<li>
|
||||||
|
<div class="folder-container">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="5 8 14 8"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="folder-icon"
|
||||||
|
>
|
||||||
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<button class="folder-button">
|
||||||
|
<span class="folder-title"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="folder-outer">
|
||||||
|
<ul class="content"></ul>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Explorer.css = style
|
Explorer.css = style
|
||||||
Explorer.afterDOMLoaded = script
|
Explorer.afterDOMLoaded = concatenateResources(script, overflowListAfterDOMLoaded)
|
||||||
return Explorer
|
return Explorer
|
||||||
}) satisfies QuartzComponentConstructor
|
}) satisfies QuartzComponentConstructor
|
||||||
|
|||||||
@@ -1,242 +0,0 @@
|
|||||||
// @ts-ignore
|
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
|
||||||
import {
|
|
||||||
joinSegments,
|
|
||||||
resolveRelative,
|
|
||||||
clone,
|
|
||||||
simplifySlug,
|
|
||||||
SimpleSlug,
|
|
||||||
FilePath,
|
|
||||||
} from "../util/path"
|
|
||||||
|
|
||||||
type OrderEntries = "sort" | "filter" | "map"
|
|
||||||
|
|
||||||
export interface Options {
|
|
||||||
title?: string
|
|
||||||
folderDefaultState: "collapsed" | "open"
|
|
||||||
folderClickBehavior: "collapse" | "link"
|
|
||||||
useSavedState: boolean
|
|
||||||
sortFn: (a: FileNode, b: FileNode) => number
|
|
||||||
filterFn: (node: FileNode) => boolean
|
|
||||||
mapFn: (node: FileNode) => void
|
|
||||||
order: OrderEntries[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type DataWrapper = {
|
|
||||||
file: QuartzPluginData
|
|
||||||
path: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FolderState = {
|
|
||||||
path: string
|
|
||||||
collapsed: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPathSegment(fp: FilePath | undefined, idx: number): string | undefined {
|
|
||||||
if (!fp) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
return fp.split("/").at(idx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Structure to add all files into a tree
|
|
||||||
export class FileNode {
|
|
||||||
children: Array<FileNode>
|
|
||||||
name: string // this is the slug segment
|
|
||||||
displayName: string
|
|
||||||
file: QuartzPluginData | null
|
|
||||||
depth: number
|
|
||||||
|
|
||||||
constructor(slugSegment: string, displayName?: string, file?: QuartzPluginData, depth?: number) {
|
|
||||||
this.children = []
|
|
||||||
this.name = slugSegment
|
|
||||||
this.displayName = displayName ?? file?.frontmatter?.title ?? slugSegment
|
|
||||||
this.file = file ? clone(file) : null
|
|
||||||
this.depth = depth ?? 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private insert(fileData: DataWrapper) {
|
|
||||||
if (fileData.path.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextSegment = fileData.path[0]
|
|
||||||
|
|
||||||
// base case, insert here
|
|
||||||
if (fileData.path.length === 1) {
|
|
||||||
if (nextSegment === "") {
|
|
||||||
// index case (we are the root and we just found index.md), set our data appropriately
|
|
||||||
const title = fileData.file.frontmatter?.title
|
|
||||||
if (title && title !== "index") {
|
|
||||||
this.displayName = title
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// direct child
|
|
||||||
this.children.push(new FileNode(nextSegment, undefined, fileData.file, this.depth + 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// find the right child to insert into
|
|
||||||
fileData.path = fileData.path.splice(1)
|
|
||||||
const child = this.children.find((c) => c.name === nextSegment)
|
|
||||||
if (child) {
|
|
||||||
child.insert(fileData)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const newChild = new FileNode(
|
|
||||||
nextSegment,
|
|
||||||
getPathSegment(fileData.file.relativePath, this.depth),
|
|
||||||
undefined,
|
|
||||||
this.depth + 1,
|
|
||||||
)
|
|
||||||
newChild.insert(fileData)
|
|
||||||
this.children.push(newChild)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new file to tree
|
|
||||||
add(file: QuartzPluginData) {
|
|
||||||
this.insert({ file: file, path: simplifySlug(file.slug!).split("/") })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter FileNode tree. Behaves similar to `Array.prototype.filter()`, but modifies tree in place
|
|
||||||
* @param filterFn function to filter tree with
|
|
||||||
*/
|
|
||||||
filter(filterFn: (node: FileNode) => boolean) {
|
|
||||||
this.children = this.children.filter(filterFn)
|
|
||||||
this.children.forEach((child) => child.filter(filterFn))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter FileNode tree. Behaves similar to `Array.prototype.map()`, but modifies tree in place
|
|
||||||
* @param mapFn function to use for mapping over tree
|
|
||||||
*/
|
|
||||||
map(mapFn: (node: FileNode) => void) {
|
|
||||||
mapFn(this)
|
|
||||||
this.children.forEach((child) => child.map(mapFn))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get folder representation with state of tree.
|
|
||||||
* Intended to only be called on root node before changes to the tree are made
|
|
||||||
* @param collapsed default state of folders (collapsed by default or not)
|
|
||||||
* @returns array containing folder state for tree
|
|
||||||
*/
|
|
||||||
getFolderPaths(collapsed: boolean): FolderState[] {
|
|
||||||
const folderPaths: FolderState[] = []
|
|
||||||
|
|
||||||
const traverse = (node: FileNode, currentPath: string) => {
|
|
||||||
if (!node.file) {
|
|
||||||
const folderPath = joinSegments(currentPath, node.name)
|
|
||||||
if (folderPath !== "") {
|
|
||||||
folderPaths.push({ path: folderPath, collapsed })
|
|
||||||
}
|
|
||||||
|
|
||||||
node.children.forEach((child) => traverse(child, folderPath))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
traverse(this, "")
|
|
||||||
return folderPaths
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort order: folders first, then files. Sort folders and files alphabetically
|
|
||||||
/**
|
|
||||||
* Sorts tree according to sort/compare function
|
|
||||||
* @param sortFn compare function used for `.sort()`, also used recursively for children
|
|
||||||
*/
|
|
||||||
sort(sortFn: (a: FileNode, b: FileNode) => number) {
|
|
||||||
this.children = this.children.sort(sortFn)
|
|
||||||
this.children.forEach((e) => e.sort(sortFn))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExplorerNodeProps = {
|
|
||||||
node: FileNode
|
|
||||||
opts: Options
|
|
||||||
fileData: QuartzPluginData
|
|
||||||
fullPath?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodeProps) {
|
|
||||||
// Get options
|
|
||||||
const folderBehavior = opts.folderClickBehavior
|
|
||||||
const isDefaultOpen = opts.folderDefaultState === "open"
|
|
||||||
|
|
||||||
// Calculate current folderPath
|
|
||||||
const folderPath = node.name !== "" ? joinSegments(fullPath ?? "", node.name) : ""
|
|
||||||
const href = resolveRelative(fileData.slug!, folderPath as SimpleSlug) + "/"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{node.file ? (
|
|
||||||
// Single file node
|
|
||||||
<li key={node.file.slug}>
|
|
||||||
<a href={resolveRelative(fileData.slug!, node.file.slug!)} data-for={node.file.slug}>
|
|
||||||
{node.displayName}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
) : (
|
|
||||||
<li>
|
|
||||||
{node.name !== "" && (
|
|
||||||
// Node with entire folder
|
|
||||||
// Render svg button + folder name, then children
|
|
||||||
<div class="folder-container">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="12"
|
|
||||||
height="12"
|
|
||||||
viewBox="5 8 14 8"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="folder-icon"
|
|
||||||
>
|
|
||||||
<polyline points="6 9 12 15 18 9"></polyline>
|
|
||||||
</svg>
|
|
||||||
{/* render <a> tag if folderBehavior is "link", otherwise render <button> with collapse click event */}
|
|
||||||
<div key={node.name} data-folderpath={folderPath}>
|
|
||||||
{folderBehavior === "link" ? (
|
|
||||||
<a href={href} data-for={node.name} class="folder-title">
|
|
||||||
{node.displayName}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<button class="folder-button">
|
|
||||||
<span class="folder-title">{node.displayName}</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* Recursively render children of folder */}
|
|
||||||
<div class={`folder-outer ${node.depth === 0 || isDefaultOpen ? "open" : ""}`}>
|
|
||||||
<ul
|
|
||||||
// Inline style for left folder paddings
|
|
||||||
style={{
|
|
||||||
paddingLeft: node.name !== "" ? "1.4rem" : "0",
|
|
||||||
}}
|
|
||||||
class="content"
|
|
||||||
data-folderul={folderPath}
|
|
||||||
>
|
|
||||||
{node.children.map((childNode, i) => (
|
|
||||||
<ExplorerNode
|
|
||||||
node={childNode}
|
|
||||||
key={i}
|
|
||||||
opts={opts}
|
|
||||||
fullPath={folderPath}
|
|
||||||
fileData={fileData}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -48,7 +48,7 @@ const defaultOptions: GraphOptions = {
|
|||||||
depth: -1,
|
depth: -1,
|
||||||
scale: 0.9,
|
scale: 0.9,
|
||||||
repelForce: 0.5,
|
repelForce: 0.5,
|
||||||
centerForce: 0.3,
|
centerForce: 0.2,
|
||||||
linkDistance: 30,
|
linkDistance: 30,
|
||||||
fontSize: 0.6,
|
fontSize: 0.6,
|
||||||
opacityScale: 1,
|
opacityScale: 1,
|
||||||
@@ -67,8 +67,8 @@ export default ((opts?: Partial<GraphOptions>) => {
|
|||||||
<div class={classNames(displayClass, "graph")}>
|
<div class={classNames(displayClass, "graph")}>
|
||||||
<h3>{i18n(cfg.locale).components.graph.title}</h3>
|
<h3>{i18n(cfg.locale).components.graph.title}</h3>
|
||||||
<div class="graph-outer">
|
<div class="graph-outer">
|
||||||
<div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
|
<div class="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
|
||||||
<button id="global-graph-icon" aria-label="Global Graph">
|
<button class="global-graph-icon" aria-label="Global Graph">
|
||||||
<svg
|
<svg
|
||||||
version="1.1"
|
version="1.1"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -95,8 +95,8 @@ export default ((opts?: Partial<GraphOptions>) => {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="global-graph-outer">
|
<div class="global-graph-outer">
|
||||||
<div id="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div>
|
<div class="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
48
quartz/components/OverflowList.tsx
Normal file
48
quartz/components/OverflowList.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { JSX } from "preact"
|
||||||
|
import { randomIdNonSecure } from "../util/random"
|
||||||
|
|
||||||
|
const OverflowList = ({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: JSX.HTMLAttributes<HTMLUListElement> & { id: string }) => {
|
||||||
|
return (
|
||||||
|
<ul {...props} class={[props.class, "overflow"].filter(Boolean).join(" ")} id={props.id}>
|
||||||
|
{children}
|
||||||
|
<li class="overflow-end" />
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const id = randomIdNonSecure()
|
||||||
|
|
||||||
|
return {
|
||||||
|
OverflowList: (props: JSX.HTMLAttributes<HTMLUListElement>) => (
|
||||||
|
<OverflowList {...props} id={id} />
|
||||||
|
),
|
||||||
|
overflowListAfterDOMLoaded: `
|
||||||
|
document.addEventListener("nav", (e) => {
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
const parentUl = entry.target.parentElement
|
||||||
|
if (!parentUl) return
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
parentUl.classList.remove("gradient-active")
|
||||||
|
} else {
|
||||||
|
parentUl.classList.add("gradient-active")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const ul = document.getElementById("${id}")
|
||||||
|
if (!ul) return
|
||||||
|
|
||||||
|
const end = ul.querySelector(".overflow-end")
|
||||||
|
if (!end) return
|
||||||
|
|
||||||
|
observer.observe(end)
|
||||||
|
window.addCleanup(() => observer.disconnect())
|
||||||
|
})
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ export default ((userOpts?: Partial<SearchOptions>) => {
|
|||||||
const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder
|
const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "search")}>
|
<div class={classNames(displayClass, "search")}>
|
||||||
<button class="search-button" id="search-button">
|
<button class="search-button">
|
||||||
<p>{i18n(cfg.locale).components.search.title}</p>
|
<p>{i18n(cfg.locale).components.search.title}</p>
|
||||||
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.9 19.7">
|
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.9 19.7">
|
||||||
<title>Search</title>
|
<title>Search</title>
|
||||||
@@ -29,17 +29,17 @@ export default ((userOpts?: Partial<SearchOptions>) => {
|
|||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div id="search-container">
|
<div class="search-container">
|
||||||
<div id="search-space">
|
<div class="search-space">
|
||||||
<input
|
<input
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
id="search-bar"
|
class="search-bar"
|
||||||
name="search"
|
name="search"
|
||||||
type="text"
|
type="text"
|
||||||
aria-label={searchPlaceholder}
|
aria-label={searchPlaceholder}
|
||||||
placeholder={searchPlaceholder}
|
placeholder={searchPlaceholder}
|
||||||
/>
|
/>
|
||||||
<div id="search-layout" data-preview={opts.enablePreview}></div>
|
<div class="search-layout" data-preview={opts.enablePreview}></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { classNames } from "../util/lang"
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import script from "./scripts/toc.inline"
|
import script from "./scripts/toc.inline"
|
||||||
import { i18n } from "../i18n"
|
import { i18n } from "../i18n"
|
||||||
|
import OverflowListFactory from "./OverflowList"
|
||||||
|
import { concatenateResources } from "../util/resources"
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
layout: "modern" | "legacy"
|
layout: "modern" | "legacy"
|
||||||
@@ -15,42 +17,70 @@ const defaultOptions: Options = {
|
|||||||
layout: "modern",
|
layout: "modern",
|
||||||
}
|
}
|
||||||
|
|
||||||
const TableOfContents: QuartzComponent = ({
|
export default ((opts?: Partial<Options>) => {
|
||||||
fileData,
|
const layout = opts?.layout ?? defaultOptions.layout
|
||||||
displayClass,
|
const { OverflowList, overflowListAfterDOMLoaded } = OverflowListFactory()
|
||||||
cfg,
|
const TableOfContents: QuartzComponent = ({
|
||||||
}: QuartzComponentProps) => {
|
fileData,
|
||||||
if (!fileData.toc) {
|
displayClass,
|
||||||
return null
|
cfg,
|
||||||
|
}: QuartzComponentProps) => {
|
||||||
|
if (!fileData.toc) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={classNames(displayClass, "toc")}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={fileData.collapseToc ? "collapsed toc-header" : "toc-header"}
|
||||||
|
aria-controls="toc-content"
|
||||||
|
aria-expanded={!fileData.collapseToc}
|
||||||
|
>
|
||||||
|
<h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class={fileData.collapseToc ? "collapsed toc-content" : "toc-content"}>
|
||||||
|
<OverflowList>
|
||||||
|
{fileData.toc.map((tocEntry) => (
|
||||||
|
<li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
|
||||||
|
<a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>
|
||||||
|
{tocEntry.text}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</OverflowList>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
TableOfContents.css = modernStyle
|
||||||
<div class={classNames(displayClass, "toc")}>
|
TableOfContents.afterDOMLoaded = concatenateResources(script, overflowListAfterDOMLoaded)
|
||||||
<button
|
|
||||||
type="button"
|
const LegacyTableOfContents: QuartzComponent = ({ fileData, cfg }: QuartzComponentProps) => {
|
||||||
id="toc"
|
if (!fileData.toc) {
|
||||||
class={fileData.collapseToc ? "collapsed" : ""}
|
return null
|
||||||
aria-controls="toc-content"
|
}
|
||||||
aria-expanded={!fileData.collapseToc}
|
return (
|
||||||
>
|
<details class="toc" open={!fileData.collapseToc}>
|
||||||
<h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
|
<summary>
|
||||||
<svg
|
<h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
</summary>
|
||||||
width="24"
|
<ul>
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="fold"
|
|
||||||
>
|
|
||||||
<polyline points="6 9 12 15 18 9"></polyline>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<div id="toc-content" class={fileData.collapseToc ? "collapsed" : ""}>
|
|
||||||
<ul class="overflow">
|
|
||||||
{fileData.toc.map((tocEntry) => (
|
{fileData.toc.map((tocEntry) => (
|
||||||
<li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
|
<li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
|
||||||
<a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>
|
<a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>
|
||||||
@@ -59,37 +89,10 @@ const TableOfContents: QuartzComponent = ({
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</details>
|
||||||
</div>
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
TableOfContents.css = modernStyle
|
|
||||||
TableOfContents.afterDOMLoaded = script
|
|
||||||
|
|
||||||
const LegacyTableOfContents: QuartzComponent = ({ fileData, cfg }: QuartzComponentProps) => {
|
|
||||||
if (!fileData.toc) {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
return (
|
LegacyTableOfContents.css = legacyStyle
|
||||||
<details id="toc" open={!fileData.collapseToc}>
|
|
||||||
<summary>
|
|
||||||
<h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
|
|
||||||
</summary>
|
|
||||||
<ul>
|
|
||||||
{fileData.toc.map((tocEntry) => (
|
|
||||||
<li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
|
|
||||||
<a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>
|
|
||||||
{tocEntry.text}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</details>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
LegacyTableOfContents.css = legacyStyle
|
|
||||||
|
|
||||||
export default ((opts?: Partial<Options>) => {
|
|
||||||
const layout = opts?.layout ?? defaultOptions.layout
|
|
||||||
return layout === "modern" ? TableOfContents : LegacyTableOfContents
|
return layout === "modern" ? TableOfContents : LegacyTableOfContents
|
||||||
}) satisfies QuartzComponentConstructor
|
}) satisfies QuartzComponentConstructor
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { htmlToJsx } from "../../util/jsx"
|
|||||||
import { i18n } from "../../i18n"
|
import { i18n } from "../../i18n"
|
||||||
import { QuartzPluginData } from "../../plugins/vfile"
|
import { QuartzPluginData } from "../../plugins/vfile"
|
||||||
import { ComponentChildren } from "preact"
|
import { ComponentChildren } from "preact"
|
||||||
|
import { concatenateResources } from "../../util/resources"
|
||||||
|
|
||||||
interface FolderContentOptions {
|
interface FolderContentOptions {
|
||||||
/**
|
/**
|
||||||
@@ -104,6 +105,6 @@ export default ((opts?: Partial<FolderContentOptions>) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
FolderContent.css = style + PageList.css
|
FolderContent.css = concatenateResources(style, PageList.css)
|
||||||
return FolderContent
|
return FolderContent
|
||||||
}) satisfies QuartzComponentConstructor
|
}) satisfies QuartzComponentConstructor
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Root } from "hast"
|
|||||||
import { htmlToJsx } from "../../util/jsx"
|
import { htmlToJsx } from "../../util/jsx"
|
||||||
import { i18n } from "../../i18n"
|
import { i18n } from "../../i18n"
|
||||||
import { ComponentChildren } from "preact"
|
import { ComponentChildren } from "preact"
|
||||||
|
import { concatenateResources } from "../../util/resources"
|
||||||
|
|
||||||
interface TagContentOptions {
|
interface TagContentOptions {
|
||||||
sort?: SortFn
|
sort?: SortFn
|
||||||
@@ -124,6 +125,6 @@ export default ((opts?: Partial<TagContentOptions>) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TagContent.css = style + PageList.css
|
TagContent.css = concatenateResources(style, PageList.css)
|
||||||
return TagContent
|
return TagContent
|
||||||
}) satisfies QuartzComponentConstructor
|
}) satisfies QuartzComponentConstructor
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { QuartzComponent, QuartzComponentProps } from "./types"
|
|||||||
import HeaderConstructor from "./Header"
|
import HeaderConstructor from "./Header"
|
||||||
import BodyConstructor from "./Body"
|
import BodyConstructor from "./Body"
|
||||||
import { JSResourceToScriptElement, StaticResources } from "../util/resources"
|
import { JSResourceToScriptElement, StaticResources } from "../util/resources"
|
||||||
import { clone, FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path"
|
import { FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path"
|
||||||
|
import { clone } from "../util/clone"
|
||||||
import { visit } from "unist-util-visit"
|
import { visit } from "unist-util-visit"
|
||||||
import { Root, Element, ElementContent } from "hast"
|
import { Root, Element, ElementContent } from "hast"
|
||||||
import { GlobalConfiguration } from "../cfg"
|
import { GlobalConfiguration } from "../cfg"
|
||||||
|
|||||||
@@ -25,12 +25,11 @@ document.addEventListener("nav", () => {
|
|||||||
emitThemeChangeEvent(newTheme)
|
emitThemeChangeEvent(newTheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Darkmode toggle
|
for (const darkmodeButton of document.getElementsByClassName("darkmode")) {
|
||||||
const themeButton = document.querySelector("#darkmode") as HTMLButtonElement
|
darkmodeButton.addEventListener("click", switchTheme)
|
||||||
if (themeButton) {
|
window.addCleanup(() => darkmodeButton.removeEventListener("click", switchTheme))
|
||||||
themeButton.addEventListener("click", switchTheme)
|
|
||||||
window.addCleanup(() => themeButton.removeEventListener("click", switchTheme))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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", themeChange)
|
colorSchemeMediaQuery.addEventListener("change", themeChange)
|
||||||
|
|||||||
@@ -1,53 +1,37 @@
|
|||||||
import { FolderState } from "../ExplorerNode"
|
import { FileTrieNode } from "../../util/fileTrie"
|
||||||
|
import { FullSlug, resolveRelative, simplifySlug } from "../../util/path"
|
||||||
|
import { ContentDetails } from "../../plugins/emitters/contentIndex"
|
||||||
|
|
||||||
// Current state of folders
|
|
||||||
type MaybeHTMLElement = HTMLElement | undefined
|
type MaybeHTMLElement = HTMLElement | undefined
|
||||||
let currentExplorerState: FolderState[]
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
interface ParsedOptions {
|
||||||
// If last element is observed, remove gradient of "overflow" class so element is visible
|
folderClickBehavior: "collapse" | "link"
|
||||||
const explorerUl = document.getElementById("explorer-ul")
|
folderDefaultState: "collapsed" | "open"
|
||||||
if (!explorerUl) return
|
useSavedState: boolean
|
||||||
for (const entry of entries) {
|
sortFn: (a: FileTrieNode, b: FileTrieNode) => number
|
||||||
if (entry.isIntersecting) {
|
filterFn: (node: FileTrieNode) => boolean
|
||||||
explorerUl.classList.add("no-background")
|
mapFn: (node: FileTrieNode) => void
|
||||||
} else {
|
order: "sort" | "filter" | "map"[]
|
||||||
explorerUl.classList.remove("no-background")
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
type FolderState = {
|
||||||
|
path: string
|
||||||
|
collapsed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentExplorerState: Array<FolderState>
|
||||||
function toggleExplorer(this: HTMLElement) {
|
function toggleExplorer(this: HTMLElement) {
|
||||||
// Toggle collapsed state of entire explorer
|
const nearestExplorer = this.closest(".explorer") as HTMLElement
|
||||||
this.classList.toggle("collapsed")
|
if (!nearestExplorer) return
|
||||||
|
nearestExplorer.classList.toggle("collapsed")
|
||||||
// Toggle collapsed aria state of entire explorer
|
nearestExplorer.setAttribute(
|
||||||
this.setAttribute(
|
|
||||||
"aria-expanded",
|
"aria-expanded",
|
||||||
this.getAttribute("aria-expanded") === "true" ? "false" : "true",
|
nearestExplorer.getAttribute("aria-expanded") === "true" ? "false" : "true",
|
||||||
)
|
)
|
||||||
|
|
||||||
const content = (
|
|
||||||
this.nextElementSibling?.nextElementSibling
|
|
||||||
? this.nextElementSibling.nextElementSibling
|
|
||||||
: this.nextElementSibling
|
|
||||||
) as MaybeHTMLElement
|
|
||||||
if (!content) return
|
|
||||||
content.classList.toggle("collapsed")
|
|
||||||
content.classList.toggle("explorer-viewmode")
|
|
||||||
|
|
||||||
// Prevent scroll under
|
|
||||||
if (document.querySelector("#mobile-explorer")) {
|
|
||||||
// Disable scrolling on the page when the explorer is opened on mobile
|
|
||||||
const bodySelector = document.querySelector("#quartz-body")
|
|
||||||
if (bodySelector) bodySelector.classList.toggle("lock-scroll")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFolder(evt: MouseEvent) {
|
function toggleFolder(evt: MouseEvent) {
|
||||||
evt.stopPropagation()
|
evt.stopPropagation()
|
||||||
|
|
||||||
// Element that was clicked
|
|
||||||
const target = evt.target as MaybeHTMLElement
|
const target = evt.target as MaybeHTMLElement
|
||||||
if (!target) return
|
if (!target) return
|
||||||
|
|
||||||
@@ -55,162 +39,237 @@ function toggleFolder(evt: MouseEvent) {
|
|||||||
const isSvg = target.nodeName === "svg"
|
const isSvg = target.nodeName === "svg"
|
||||||
|
|
||||||
// corresponding <ul> element relative to clicked button/folder
|
// corresponding <ul> element relative to clicked button/folder
|
||||||
const childFolderContainer = (
|
const folderContainer = (
|
||||||
isSvg
|
isSvg
|
||||||
? target.parentElement?.nextSibling
|
? // svg -> div.folder-container
|
||||||
: target.parentElement?.parentElement?.nextElementSibling
|
target.parentElement
|
||||||
|
: // button.folder-button -> div -> div.folder-container
|
||||||
|
target.parentElement?.parentElement
|
||||||
) as MaybeHTMLElement
|
) as MaybeHTMLElement
|
||||||
const currentFolderParent = (
|
if (!folderContainer) return
|
||||||
isSvg ? target.nextElementSibling : target.parentElement
|
const childFolderContainer = folderContainer.nextElementSibling as MaybeHTMLElement
|
||||||
) as MaybeHTMLElement
|
if (!childFolderContainer) return
|
||||||
if (!(childFolderContainer && currentFolderParent)) return
|
|
||||||
// <li> element of folder (stores folder-path dataset)
|
|
||||||
childFolderContainer.classList.toggle("open")
|
childFolderContainer.classList.toggle("open")
|
||||||
|
|
||||||
// Collapse folder container
|
// Collapse folder container
|
||||||
const isCollapsed = childFolderContainer.classList.contains("open")
|
const isCollapsed = !childFolderContainer.classList.contains("open")
|
||||||
setFolderState(childFolderContainer, !isCollapsed)
|
setFolderState(childFolderContainer, isCollapsed)
|
||||||
|
|
||||||
|
const currentFolderState = currentExplorerState.find(
|
||||||
|
(item) => item.path === folderContainer.dataset.folderpath,
|
||||||
|
)
|
||||||
|
if (currentFolderState) {
|
||||||
|
currentFolderState.collapsed = isCollapsed
|
||||||
|
} else {
|
||||||
|
currentExplorerState.push({
|
||||||
|
path: folderContainer.dataset.folderpath as FullSlug,
|
||||||
|
collapsed: isCollapsed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Save folder state to localStorage
|
|
||||||
const fullFolderPath = currentFolderParent.dataset.folderpath as string
|
|
||||||
toggleCollapsedByPath(currentExplorerState, fullFolderPath)
|
|
||||||
const stringifiedFileTree = JSON.stringify(currentExplorerState)
|
const stringifiedFileTree = JSON.stringify(currentExplorerState)
|
||||||
localStorage.setItem("fileTree", stringifiedFileTree)
|
localStorage.setItem("fileTree", stringifiedFileTree)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupExplorer() {
|
function createFileNode(currentSlug: FullSlug, node: FileTrieNode): HTMLLIElement {
|
||||||
// Set click handler for collapsing entire explorer
|
const template = document.getElementById("template-file") as HTMLTemplateElement
|
||||||
const allExplorers = document.querySelectorAll(".explorer > button") as NodeListOf<HTMLElement>
|
const clone = template.content.cloneNode(true) as DocumentFragment
|
||||||
|
const li = clone.querySelector("li") as HTMLLIElement
|
||||||
|
const a = li.querySelector("a") as HTMLAnchorElement
|
||||||
|
a.href = resolveRelative(currentSlug, node.slug)
|
||||||
|
a.dataset.for = node.slug
|
||||||
|
a.textContent = node.displayName
|
||||||
|
|
||||||
|
if (currentSlug === node.slug) {
|
||||||
|
a.classList.add("active")
|
||||||
|
}
|
||||||
|
|
||||||
|
return li
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFolderNode(
|
||||||
|
currentSlug: FullSlug,
|
||||||
|
node: FileTrieNode,
|
||||||
|
opts: ParsedOptions,
|
||||||
|
): HTMLLIElement {
|
||||||
|
const template = document.getElementById("template-folder") as HTMLTemplateElement
|
||||||
|
const clone = template.content.cloneNode(true) as DocumentFragment
|
||||||
|
const li = clone.querySelector("li") as HTMLLIElement
|
||||||
|
const folderContainer = li.querySelector(".folder-container") as HTMLElement
|
||||||
|
const titleContainer = folderContainer.querySelector("div") as HTMLElement
|
||||||
|
const folderOuter = li.querySelector(".folder-outer") as HTMLElement
|
||||||
|
const ul = folderOuter.querySelector("ul") as HTMLUListElement
|
||||||
|
|
||||||
|
const folderPath = node.slug
|
||||||
|
folderContainer.dataset.folderpath = folderPath
|
||||||
|
|
||||||
|
if (opts.folderClickBehavior === "link") {
|
||||||
|
// Replace button with link for link behavior
|
||||||
|
const button = titleContainer.querySelector(".folder-button") as HTMLElement
|
||||||
|
const a = document.createElement("a")
|
||||||
|
a.href = resolveRelative(currentSlug, folderPath)
|
||||||
|
a.dataset.for = folderPath
|
||||||
|
a.className = "folder-title"
|
||||||
|
a.textContent = node.displayName
|
||||||
|
button.replaceWith(a)
|
||||||
|
} else {
|
||||||
|
const span = titleContainer.querySelector(".folder-title") as HTMLElement
|
||||||
|
span.textContent = node.displayName
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the saved state is collapsed or the default state is collapsed
|
||||||
|
const isCollapsed =
|
||||||
|
currentExplorerState.find((item) => item.path === folderPath)?.collapsed ??
|
||||||
|
opts.folderDefaultState === "collapsed"
|
||||||
|
|
||||||
|
// if this folder is a prefix of the current path we
|
||||||
|
// want to open it anyways
|
||||||
|
const simpleFolderPath = simplifySlug(folderPath)
|
||||||
|
const folderIsPrefixOfCurrentSlug =
|
||||||
|
simpleFolderPath === currentSlug.slice(0, simpleFolderPath.length)
|
||||||
|
|
||||||
|
if (!isCollapsed || folderIsPrefixOfCurrentSlug) {
|
||||||
|
folderOuter.classList.add("open")
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const child of node.children) {
|
||||||
|
const childNode = child.data
|
||||||
|
? createFileNode(currentSlug, child)
|
||||||
|
: createFolderNode(currentSlug, child, opts)
|
||||||
|
ul.appendChild(childNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return li
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupExplorer(currentSlug: FullSlug) {
|
||||||
|
const allExplorers = document.querySelectorAll("div.explorer") as NodeListOf<HTMLElement>
|
||||||
|
|
||||||
for (const explorer of allExplorers) {
|
for (const explorer of allExplorers) {
|
||||||
|
const dataFns = JSON.parse(explorer.dataset.dataFns || "{}")
|
||||||
|
const opts: ParsedOptions = {
|
||||||
|
folderClickBehavior: (explorer.dataset.behavior || "collapse") as "collapse" | "link",
|
||||||
|
folderDefaultState: (explorer.dataset.collapsed || "collapsed") as "collapsed" | "open",
|
||||||
|
useSavedState: explorer.dataset.savestate === "true",
|
||||||
|
order: dataFns.order || ["filter", "map", "sort"],
|
||||||
|
sortFn: new Function("return " + (dataFns.sortFn || "undefined"))(),
|
||||||
|
filterFn: new Function("return " + (dataFns.filterFn || "undefined"))(),
|
||||||
|
mapFn: new Function("return " + (dataFns.mapFn || "undefined"))(),
|
||||||
|
}
|
||||||
|
|
||||||
// Get folder state from local storage
|
// Get folder state from local storage
|
||||||
const storageTree = localStorage.getItem("fileTree")
|
const storageTree = localStorage.getItem("fileTree")
|
||||||
|
const serializedExplorerState = storageTree && opts.useSavedState ? JSON.parse(storageTree) : []
|
||||||
// Convert to bool
|
const oldIndex = new Map(
|
||||||
const useSavedFolderState = explorer?.dataset.savestate === "true"
|
serializedExplorerState.map((entry: FolderState) => [entry.path, entry.collapsed]),
|
||||||
|
|
||||||
if (explorer) {
|
|
||||||
// Get config
|
|
||||||
const collapseBehavior = explorer.dataset.behavior
|
|
||||||
|
|
||||||
// Add click handlers for all folders (click handler on folder "label")
|
|
||||||
if (collapseBehavior === "collapse") {
|
|
||||||
for (const item of document.getElementsByClassName(
|
|
||||||
"folder-button",
|
|
||||||
) as HTMLCollectionOf<HTMLElement>) {
|
|
||||||
window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer))
|
|
||||||
item.addEventListener("click", toggleFolder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add click handler to main explorer
|
|
||||||
window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer))
|
|
||||||
explorer.addEventListener("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
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
currentExplorerState.map((folderState) => {
|
|
||||||
const folderLi = document.querySelector(
|
|
||||||
`[data-folderpath='${folderState.path.replace("'", "-")}']`,
|
|
||||||
) as MaybeHTMLElement
|
|
||||||
const folderUl = folderLi?.parentElement?.nextElementSibling as MaybeHTMLElement
|
|
||||||
if (folderUl) {
|
|
||||||
setFolderState(folderUl, folderState.collapsed)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleExplorerFolders() {
|
|
||||||
const currentFile = (document.querySelector("body")?.getAttribute("data-slug") ?? "").replace(
|
|
||||||
/\/index$/g,
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
const allFolders = document.querySelectorAll(".folder-outer")
|
|
||||||
|
|
||||||
allFolders.forEach((element) => {
|
|
||||||
const folderUl = Array.from(element.children).find((child) =>
|
|
||||||
child.matches("ul[data-folderul]"),
|
|
||||||
)
|
)
|
||||||
if (folderUl) {
|
|
||||||
if (currentFile.includes(folderUl.getAttribute("data-folderul") ?? "")) {
|
const data = await fetchData
|
||||||
if (!element.classList.contains("open")) {
|
const entries = [...Object.entries(data)] as [FullSlug, ContentDetails][]
|
||||||
element.classList.add("open")
|
const trie = FileTrieNode.fromEntries(entries)
|
||||||
}
|
|
||||||
|
// Apply functions in order
|
||||||
|
for (const fn of opts.order) {
|
||||||
|
switch (fn) {
|
||||||
|
case "filter":
|
||||||
|
if (opts.filterFn) trie.filter(opts.filterFn)
|
||||||
|
break
|
||||||
|
case "map":
|
||||||
|
if (opts.mapFn) trie.map(opts.mapFn)
|
||||||
|
break
|
||||||
|
case "sort":
|
||||||
|
if (opts.sortFn) trie.sort(opts.sortFn)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("resize", setupExplorer)
|
// Get folder paths for state management
|
||||||
|
const folderPaths = trie.getFolderPaths()
|
||||||
|
currentExplorerState = folderPaths.map((path) => ({
|
||||||
|
path,
|
||||||
|
collapsed: oldIndex.get(path) === true,
|
||||||
|
}))
|
||||||
|
|
||||||
document.addEventListener("nav", () => {
|
const explorerUl = explorer.querySelector(".explorer-ul")
|
||||||
const explorer = document.querySelector("#mobile-explorer")
|
if (!explorerUl) continue
|
||||||
if (explorer) {
|
|
||||||
explorer.classList.add("collapsed")
|
// Create and insert new content
|
||||||
const content = explorer.nextElementSibling?.nextElementSibling as HTMLElement
|
const fragment = document.createDocumentFragment()
|
||||||
if (content) {
|
for (const child of trie.children) {
|
||||||
content.classList.add("collapsed")
|
const node = child.isFolder
|
||||||
content.classList.toggle("explorer-viewmode")
|
? createFolderNode(currentSlug, child, opts)
|
||||||
|
: createFileNode(currentSlug, child)
|
||||||
|
|
||||||
|
fragment.appendChild(node)
|
||||||
|
}
|
||||||
|
explorerUl.insertBefore(fragment, explorerUl.firstChild)
|
||||||
|
|
||||||
|
// restore explorer scrollTop position if it exists
|
||||||
|
const scrollTop = sessionStorage.getItem("explorerScrollTop")
|
||||||
|
if (scrollTop) {
|
||||||
|
explorerUl.scrollTop = parseInt(scrollTop)
|
||||||
|
} else {
|
||||||
|
// try to scroll to the active element if it exists
|
||||||
|
const activeElement = explorerUl.querySelector(".active")
|
||||||
|
if (activeElement) {
|
||||||
|
activeElement.scrollIntoView({ behavior: "smooth" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up event handlers
|
||||||
|
const explorerButtons = explorer.getElementsByClassName(
|
||||||
|
"explorer-toggle",
|
||||||
|
) as HTMLCollectionOf<HTMLElement>
|
||||||
|
for (const button of explorerButtons) {
|
||||||
|
button.addEventListener("click", toggleExplorer)
|
||||||
|
window.addCleanup(() => button.removeEventListener("click", toggleExplorer))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up folder click handlers
|
||||||
|
if (opts.folderClickBehavior === "collapse") {
|
||||||
|
const folderButtons = explorer.getElementsByClassName(
|
||||||
|
"folder-button",
|
||||||
|
) as HTMLCollectionOf<HTMLElement>
|
||||||
|
for (const button of folderButtons) {
|
||||||
|
button.addEventListener("click", toggleFolder)
|
||||||
|
window.addCleanup(() => button.removeEventListener("click", toggleFolder))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderIcons = explorer.getElementsByClassName(
|
||||||
|
"folder-icon",
|
||||||
|
) as HTMLCollectionOf<HTMLElement>
|
||||||
|
for (const icon of folderIcons) {
|
||||||
|
icon.addEventListener("click", toggleFolder)
|
||||||
|
window.addCleanup(() => icon.removeEventListener("click", toggleFolder))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setupExplorer()
|
}
|
||||||
|
|
||||||
observer.disconnect()
|
document.addEventListener("prenav", async () => {
|
||||||
|
// save explorer scrollTop position
|
||||||
// select pseudo element at end of list
|
const explorer = document.querySelector(".explorer-ul")
|
||||||
const lastItem = document.getElementById("explorer-end")
|
if (!explorer) return
|
||||||
if (lastItem) {
|
sessionStorage.setItem("explorerScrollTop", explorer.scrollTop.toString())
|
||||||
observer.observe(lastItem)
|
})
|
||||||
}
|
|
||||||
|
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||||
// Hide explorer on mobile until it is requested
|
const currentSlug = e.detail.url
|
||||||
const hiddenUntilDoneLoading = document.querySelector("#mobile-explorer")
|
await setupExplorer(currentSlug)
|
||||||
hiddenUntilDoneLoading?.classList.remove("hide-until-loaded")
|
|
||||||
|
// if mobile hamburger is visible, collapse by default
|
||||||
toggleExplorerFolders()
|
for (const explorer of document.getElementsByClassName("mobile-explorer")) {
|
||||||
|
if (explorer.checkVisibility()) {
|
||||||
|
explorer.classList.add("collapsed")
|
||||||
|
explorer.setAttribute("aria-expanded", "false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hiddenUntilDoneLoading = document.querySelector("#mobile-explorer")
|
||||||
|
hiddenUntilDoneLoading?.classList.remove("hide-until-loaded")
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggles the state of a given folder
|
|
||||||
* @param folderElement <div class="folder-outer"> Element of folder (parent)
|
|
||||||
* @param collapsed if folder should be set to collapsed or not
|
|
||||||
*/
|
|
||||||
function setFolderState(folderElement: HTMLElement, collapsed: boolean) {
|
function setFolderState(folderElement: HTMLElement, collapsed: boolean) {
|
||||||
return collapsed ? folderElement.classList.remove("open") : folderElement.classList.add("open")
|
return collapsed ? folderElement.classList.remove("open") : folderElement.classList.add("open")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggles visibility of a folder
|
|
||||||
* @param array array of FolderState (`fileTree`, either get from local storage or data attribute)
|
|
||||||
* @param path path to folder (e.g. 'advanced/more/more2')
|
|
||||||
*/
|
|
||||||
function toggleCollapsedByPath(array: FolderState[], path: string) {
|
|
||||||
const entry = array.find((item) => item.path === path)
|
|
||||||
if (entry) {
|
|
||||||
entry.collapsed = !entry.collapsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -68,11 +68,9 @@ type TweenNode = {
|
|||||||
stop: () => void
|
stop: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderGraph(container: string, fullSlug: FullSlug) {
|
async function renderGraph(graph: HTMLElement, fullSlug: FullSlug) {
|
||||||
const slug = simplifySlug(fullSlug)
|
const slug = simplifySlug(fullSlug)
|
||||||
const visited = getVisited()
|
const visited = getVisited()
|
||||||
const graph = document.getElementById(container)
|
|
||||||
if (!graph) return
|
|
||||||
removeAllChildren(graph)
|
removeAllChildren(graph)
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -167,16 +165,14 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
const height = Math.max(graph.offsetHeight, 250)
|
const height = Math.max(graph.offsetHeight, 250)
|
||||||
|
|
||||||
// we virtualize the simulation and use pixi to actually render it
|
// we virtualize the simulation and use pixi to actually render it
|
||||||
// Calculate the radius of the container circle
|
|
||||||
const radius = Math.min(width, height) / 2 - 40 // 40px padding
|
|
||||||
const simulation: Simulation<NodeData, LinkData> = forceSimulation<NodeData>(graphData.nodes)
|
const simulation: Simulation<NodeData, LinkData> = forceSimulation<NodeData>(graphData.nodes)
|
||||||
.force("charge", forceManyBody().strength(-100 * repelForce))
|
.force("charge", forceManyBody().strength(-100 * repelForce))
|
||||||
.force("center", forceCenter().strength(centerForce))
|
.force("center", forceCenter().strength(centerForce))
|
||||||
.force("link", forceLink(graphData.links).distance(linkDistance))
|
.force("link", forceLink(graphData.links).distance(linkDistance))
|
||||||
.force("collide", forceCollide<NodeData>((n) => nodeRadius(n)).iterations(3))
|
.force("collide", forceCollide<NodeData>((n) => nodeRadius(n)).iterations(3))
|
||||||
|
|
||||||
if (enableRadial)
|
const radius = (Math.min(width, height) / 2) * 0.8
|
||||||
simulation.force("radial", forceRadial(radius * 0.8, width / 2, height / 2).strength(0.3))
|
if (enableRadial) simulation.force("radial", forceRadial(radius).strength(0.2))
|
||||||
|
|
||||||
// precompute style prop strings as pixi doesn't support css variables
|
// precompute style prop strings as pixi doesn't support css variables
|
||||||
const cssVars = [
|
const cssVars = [
|
||||||
@@ -524,7 +520,9 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let stopAnimation = false
|
||||||
function animate(time: number) {
|
function animate(time: number) {
|
||||||
|
if (stopAnimation) return
|
||||||
for (const n of nodeRenderData) {
|
for (const n of nodeRenderData) {
|
||||||
const { x, y } = n.simulationData
|
const { x, y } = n.simulationData
|
||||||
if (!x || !y) continue
|
if (!x || !y) continue
|
||||||
@@ -548,61 +546,101 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
requestAnimationFrame(animate)
|
requestAnimationFrame(animate)
|
||||||
}
|
}
|
||||||
|
|
||||||
const graphAnimationFrameHandle = requestAnimationFrame(animate)
|
requestAnimationFrame(animate)
|
||||||
window.addCleanup(() => cancelAnimationFrame(graphAnimationFrameHandle))
|
return () => {
|
||||||
|
stopAnimation = true
|
||||||
|
app.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let localGraphCleanups: (() => void)[] = []
|
||||||
|
let globalGraphCleanups: (() => void)[] = []
|
||||||
|
|
||||||
|
function cleanupLocalGraphs() {
|
||||||
|
for (const cleanup of localGraphCleanups) {
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
localGraphCleanups = []
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupGlobalGraphs() {
|
||||||
|
for (const cleanup of globalGraphCleanups) {
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
globalGraphCleanups = []
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||||
const slug = e.detail.url
|
const slug = e.detail.url
|
||||||
addToVisited(simplifySlug(slug))
|
addToVisited(simplifySlug(slug))
|
||||||
await renderGraph("graph-container", slug)
|
|
||||||
|
|
||||||
// Function to re-render the graph when the theme changes
|
async function renderLocalGraph() {
|
||||||
const handleThemeChange = () => {
|
cleanupLocalGraphs()
|
||||||
renderGraph("graph-container", slug)
|
const localGraphContainers = document.getElementsByClassName("graph-container")
|
||||||
|
for (const container of localGraphContainers) {
|
||||||
|
localGraphCleanups.push(await renderGraph(container as HTMLElement, slug))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// event listener for theme change
|
await renderLocalGraph()
|
||||||
document.addEventListener("themechange", handleThemeChange)
|
const handleThemeChange = () => {
|
||||||
|
void renderLocalGraph()
|
||||||
|
}
|
||||||
|
|
||||||
// cleanup for the event listener
|
document.addEventListener("themechange", handleThemeChange)
|
||||||
window.addCleanup(() => {
|
window.addCleanup(() => {
|
||||||
document.removeEventListener("themechange", handleThemeChange)
|
document.removeEventListener("themechange", handleThemeChange)
|
||||||
})
|
})
|
||||||
|
|
||||||
const container = document.getElementById("global-graph-outer")
|
const containers = [...document.getElementsByClassName("global-graph-outer")] as HTMLElement[]
|
||||||
const sidebar = container?.closest(".sidebar") as HTMLElement
|
async function renderGlobalGraph() {
|
||||||
|
|
||||||
function renderGlobalGraph() {
|
|
||||||
const slug = getFullSlug(window)
|
const slug = getFullSlug(window)
|
||||||
container?.classList.add("active")
|
for (const container of containers) {
|
||||||
if (sidebar) {
|
container.classList.add("active")
|
||||||
sidebar.style.zIndex = "1"
|
const sidebar = container.closest(".sidebar") as HTMLElement
|
||||||
}
|
if (sidebar) {
|
||||||
|
sidebar.style.zIndex = "1"
|
||||||
|
}
|
||||||
|
|
||||||
renderGraph("global-graph-container", slug)
|
const graphContainer = container.querySelector(".global-graph-container") as HTMLElement
|
||||||
registerEscapeHandler(container, hideGlobalGraph)
|
registerEscapeHandler(container, hideGlobalGraph)
|
||||||
|
if (graphContainer) {
|
||||||
|
globalGraphCleanups.push(await renderGraph(graphContainer, slug))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideGlobalGraph() {
|
function hideGlobalGraph() {
|
||||||
container?.classList.remove("active")
|
cleanupGlobalGraphs()
|
||||||
if (sidebar) {
|
for (const container of containers) {
|
||||||
sidebar.style.zIndex = ""
|
container.classList.remove("active")
|
||||||
|
const sidebar = container.closest(".sidebar") as HTMLElement
|
||||||
|
if (sidebar) {
|
||||||
|
sidebar.style.zIndex = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
|
async function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
|
||||||
if (e.key === "g" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
if (e.key === "g" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const globalGraphOpen = container?.classList.contains("active")
|
const anyGlobalGraphOpen = containers.some((container) =>
|
||||||
globalGraphOpen ? hideGlobalGraph() : renderGlobalGraph()
|
container.classList.contains("active"),
|
||||||
|
)
|
||||||
|
anyGlobalGraphOpen ? hideGlobalGraph() : renderGlobalGraph()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const containerIcon = document.getElementById("global-graph-icon")
|
const containerIcons = document.getElementsByClassName("global-graph-icon")
|
||||||
containerIcon?.addEventListener("click", renderGlobalGraph)
|
Array.from(containerIcons).forEach((icon) => {
|
||||||
window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph))
|
icon.addEventListener("click", renderGlobalGraph)
|
||||||
|
window.addCleanup(() => icon.removeEventListener("click", renderGlobalGraph))
|
||||||
|
})
|
||||||
|
|
||||||
document.addEventListener("keydown", shortcutHandler)
|
document.addEventListener("keydown", shortcutHandler)
|
||||||
window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler))
|
window.addCleanup(() => {
|
||||||
|
document.removeEventListener("keydown", shortcutHandler)
|
||||||
|
cleanupLocalGraphs()
|
||||||
|
cleanupGlobalGraphs()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ async function mouseEnterHandler(
|
|||||||
const contents = await response.text()
|
const contents = await response.text()
|
||||||
const html = p.parseFromString(contents, "text/html")
|
const html = p.parseFromString(contents, "text/html")
|
||||||
normalizeRelativeURLs(html, targetUrl)
|
normalizeRelativeURLs(html, targetUrl)
|
||||||
|
// strip all IDs from elements to prevent duplicates
|
||||||
|
html.querySelectorAll("[id]").forEach((el) => el.removeAttribute("id"))
|
||||||
const elts = [...html.getElementsByClassName("popover-hint")]
|
const elts = [...html.getElementsByClassName("popover-hint")]
|
||||||
if (elts.length === 0) return
|
if (elts.length === 0) return
|
||||||
|
|
||||||
|
|||||||
@@ -143,83 +143,75 @@ function highlightHTML(searchTerm: string, el: HTMLElement) {
|
|||||||
return html.body
|
return html.body
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
async function setupSearch(searchElement: Element, currentSlug: FullSlug, data: ContentIndex) {
|
||||||
const currentSlug = e.detail.url
|
const container = searchElement.querySelector(".search-container") as HTMLElement
|
||||||
const data = await fetchData
|
if (!container) return
|
||||||
const container = document.getElementById("search-container")
|
|
||||||
const sidebar = container?.closest(".sidebar") as HTMLElement
|
|
||||||
const searchButton = document.getElementById("search-button")
|
|
||||||
const searchBar = document.getElementById("search-bar") as HTMLInputElement | null
|
|
||||||
const searchLayout = document.getElementById("search-layout")
|
|
||||||
const idDataMap = Object.keys(data) as FullSlug[]
|
|
||||||
|
|
||||||
|
const sidebar = container.closest(".sidebar") as HTMLElement
|
||||||
|
if (!sidebar) return
|
||||||
|
|
||||||
|
const searchButton = searchElement.querySelector(".search-button") as HTMLButtonElement
|
||||||
|
if (!searchButton) return
|
||||||
|
|
||||||
|
const searchBar = searchElement.querySelector(".search-bar") as HTMLInputElement
|
||||||
|
if (!searchBar) return
|
||||||
|
|
||||||
|
const searchLayout = searchElement.querySelector(".search-layout") as HTMLElement
|
||||||
|
if (!searchLayout) return
|
||||||
|
|
||||||
|
const idDataMap = Object.keys(data) as FullSlug[]
|
||||||
const appendLayout = (el: HTMLElement) => {
|
const appendLayout = (el: HTMLElement) => {
|
||||||
if (searchLayout?.querySelector(`#${el.id}`) === null) {
|
searchLayout.appendChild(el)
|
||||||
searchLayout?.appendChild(el)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const enablePreview = searchLayout?.dataset?.preview === "true"
|
const enablePreview = searchLayout.dataset.preview === "true"
|
||||||
let preview: HTMLDivElement | undefined = undefined
|
let preview: HTMLDivElement | undefined = undefined
|
||||||
let previewInner: HTMLDivElement | undefined = undefined
|
let previewInner: HTMLDivElement | undefined = undefined
|
||||||
const results = document.createElement("div")
|
const results = document.createElement("div")
|
||||||
results.id = "results-container"
|
results.className = "results-container"
|
||||||
appendLayout(results)
|
appendLayout(results)
|
||||||
|
|
||||||
if (enablePreview) {
|
if (enablePreview) {
|
||||||
preview = document.createElement("div")
|
preview = document.createElement("div")
|
||||||
preview.id = "preview-container"
|
preview.className = "preview-container"
|
||||||
appendLayout(preview)
|
appendLayout(preview)
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideSearch() {
|
function hideSearch() {
|
||||||
container?.classList.remove("active")
|
container.classList.remove("active")
|
||||||
if (searchBar) {
|
searchBar.value = "" // clear the input when we dismiss the search
|
||||||
searchBar.value = "" // clear the input when we dismiss the search
|
sidebar.style.zIndex = ""
|
||||||
}
|
removeAllChildren(results)
|
||||||
if (sidebar) {
|
|
||||||
sidebar.style.zIndex = ""
|
|
||||||
}
|
|
||||||
if (results) {
|
|
||||||
removeAllChildren(results)
|
|
||||||
}
|
|
||||||
if (preview) {
|
if (preview) {
|
||||||
removeAllChildren(preview)
|
removeAllChildren(preview)
|
||||||
}
|
}
|
||||||
if (searchLayout) {
|
searchLayout.classList.remove("display-results")
|
||||||
searchLayout.classList.remove("display-results")
|
|
||||||
}
|
|
||||||
|
|
||||||
searchType = "basic" // reset search type after closing
|
searchType = "basic" // reset search type after closing
|
||||||
|
searchButton.focus()
|
||||||
searchButton?.focus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSearch(searchTypeNew: SearchType) {
|
function showSearch(searchTypeNew: SearchType) {
|
||||||
searchType = searchTypeNew
|
searchType = searchTypeNew
|
||||||
if (sidebar) {
|
sidebar.style.zIndex = "1"
|
||||||
sidebar.style.zIndex = "1"
|
container.classList.add("active")
|
||||||
}
|
searchBar.focus()
|
||||||
container?.classList.add("active")
|
|
||||||
searchBar?.focus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentHover: HTMLInputElement | null = null
|
let currentHover: HTMLInputElement | null = null
|
||||||
|
|
||||||
async function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
|
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
|
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()
|
||||||
const searchBarOpen = container?.classList.contains("active")
|
const searchBarOpen = container.classList.contains("active")
|
||||||
searchBarOpen ? hideSearch() : showSearch("tags")
|
searchBarOpen ? hideSearch() : showSearch("tags")
|
||||||
|
|
||||||
// add "#" prefix for tag search
|
// add "#" prefix for tag search
|
||||||
if (searchBar) searchBar.value = "#"
|
searchBar.value = "#"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,23 +220,23 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If search is active, then we will render the first result and display accordingly
|
// 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
|
||||||
if (e.key === "Enter") {
|
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
|
if (active.classList.contains("no-match")) return
|
||||||
await displayPreview(active)
|
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
|
||||||
if (!anchor || anchor?.classList.contains("no-match")) return
|
if (!anchor || anchor.classList.contains("no-match")) return
|
||||||
await displayPreview(anchor)
|
await displayPreview(anchor)
|
||||||
anchor.click()
|
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 currentResult = currentHover
|
const currentResult = currentHover
|
||||||
? currentHover
|
? currentHover
|
||||||
@@ -337,8 +329,6 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function displayResults(finalResults: Item[]) {
|
async function displayResults(finalResults: Item[]) {
|
||||||
if (!results) return
|
|
||||||
|
|
||||||
removeAllChildren(results)
|
removeAllChildren(results)
|
||||||
if (finalResults.length === 0) {
|
if (finalResults.length === 0) {
|
||||||
results.innerHTML = `<a class="result-card no-match">
|
results.innerHTML = `<a class="result-card no-match">
|
||||||
@@ -394,7 +384,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
preview.replaceChildren(previewInner)
|
preview.replaceChildren(previewInner)
|
||||||
|
|
||||||
// scroll to longest
|
// scroll to longest
|
||||||
const highlights = [...preview.querySelectorAll(".highlight")].sort(
|
const highlights = [...preview.getElementsByClassName("highlight")].sort(
|
||||||
(a, b) => b.innerHTML.length - a.innerHTML.length,
|
(a, b) => b.innerHTML.length - a.innerHTML.length,
|
||||||
)
|
)
|
||||||
highlights[0]?.scrollIntoView({ block: "start" })
|
highlights[0]?.scrollIntoView({ block: "start" })
|
||||||
@@ -460,21 +450,23 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
|
|
||||||
document.addEventListener("keydown", shortcutHandler)
|
document.addEventListener("keydown", shortcutHandler)
|
||||||
window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler))
|
window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler))
|
||||||
searchButton?.addEventListener("click", () => showSearch("basic"))
|
searchButton.addEventListener("click", () => showSearch("basic"))
|
||||||
window.addCleanup(() => searchButton?.removeEventListener("click", () => showSearch("basic")))
|
window.addCleanup(() => searchButton.removeEventListener("click", () => showSearch("basic")))
|
||||||
searchBar?.addEventListener("input", onType)
|
searchBar.addEventListener("input", onType)
|
||||||
window.addCleanup(() => searchBar?.removeEventListener("input", onType))
|
window.addCleanup(() => searchBar.removeEventListener("input", onType))
|
||||||
|
|
||||||
registerEscapeHandler(container, hideSearch)
|
registerEscapeHandler(container, hideSearch)
|
||||||
await fillDocument(data)
|
await fillDocument(data)
|
||||||
})
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fills flexsearch document with data
|
* Fills flexsearch document with data
|
||||||
* @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(data: { [key: FullSlug]: ContentDetails }) {
|
let indexPopulated = false
|
||||||
|
async function fillDocument(data: ContentIndex) {
|
||||||
|
if (indexPopulated) return
|
||||||
let id = 0
|
let id = 0
|
||||||
const promises: Array<Promise<unknown>> = []
|
const promises: Array<Promise<unknown>> = []
|
||||||
for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
|
for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
|
||||||
@@ -489,5 +481,15 @@ async function fillDocument(data: { [key: FullSlug]: ContentDetails }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
|
indexPopulated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||||
|
const currentSlug = e.detail.url
|
||||||
|
const data = await fetchData
|
||||||
|
const searchElement = document.getElementsByClassName("search")
|
||||||
|
for (const element of searchElement) {
|
||||||
|
await setupSearch(element, currentSlug, data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@@ -75,6 +75,10 @@ async function navigate(url: URL, isBack: boolean = false) {
|
|||||||
|
|
||||||
if (!contents) return
|
if (!contents) return
|
||||||
|
|
||||||
|
// notify about to nav
|
||||||
|
const event: CustomEventMap["prenav"] = new CustomEvent("prenav", { detail: {} })
|
||||||
|
document.dispatchEvent(event)
|
||||||
|
|
||||||
// cleanup old
|
// cleanup old
|
||||||
cleanupFns.forEach((fn) => fn())
|
cleanupFns.forEach((fn) => fn())
|
||||||
cleanupFns.clear()
|
cleanupFns.clear()
|
||||||
@@ -108,7 +112,7 @@ async function navigate(url: URL, isBack: boolean = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// now, patch head
|
// now, patch head, re-executing scripts
|
||||||
const elementsToRemove = document.head.querySelectorAll(":not([spa-preserve])")
|
const elementsToRemove = document.head.querySelectorAll(":not([spa-preserve])")
|
||||||
elementsToRemove.forEach((el) => el.remove())
|
elementsToRemove.forEach((el) => el.remove())
|
||||||
const elementsToAdd = html.head.querySelectorAll(":not([spa-preserve])")
|
const elementsToAdd = html.head.querySelectorAll(":not([spa-preserve])")
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
const bufferPx = 150
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
const observer = new IntersectionObserver((entries) => {
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const slug = entry.target.id
|
const slug = entry.target.id
|
||||||
@@ -26,17 +25,15 @@ function toggleToc(this: HTMLElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupToc() {
|
function setupToc() {
|
||||||
const toc = document.getElementById("toc")
|
for (const toc of document.getElementsByClassName("toc")) {
|
||||||
if (toc) {
|
const button = toc.querySelector(".toc-header")
|
||||||
const collapsed = toc.classList.contains("collapsed")
|
const content = toc.querySelector(".toc-content")
|
||||||
const content = toc.nextElementSibling as HTMLElement | undefined
|
if (!button || !content) return
|
||||||
if (!content) return
|
button.addEventListener("click", toggleToc)
|
||||||
toc.addEventListener("click", toggleToc)
|
window.addCleanup(() => button.removeEventListener("click", toggleToc))
|
||||||
window.addCleanup(() => toc.removeEventListener("click", toggleToc))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("resize", setupToc)
|
|
||||||
document.addEventListener("nav", () => {
|
document.addEventListener("nav", () => {
|
||||||
setupToc()
|
setupToc()
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export async function fetchCanonical(url: URL): Promise<Response> {
|
|||||||
if (!res.headers.get("content-type")?.startsWith("text/html")) {
|
if (!res.headers.get("content-type")?.startsWith("text/html")) {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
// reading the body can only be done once, so we need to clone the response
|
// reading the body can only be done once, so we need to clone the response
|
||||||
// to allow the caller to read it if it's was not a redirect
|
// to allow the caller to read it if it's was not a redirect
|
||||||
const text = await res.clone().text()
|
const text = await res.clone().text()
|
||||||
|
|||||||
@@ -2,18 +2,6 @@
|
|||||||
|
|
||||||
.backlinks {
|
.backlinks {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
/*&:after {
|
|
||||||
pointer-events: none;
|
|
||||||
content: "";
|
|
||||||
width: 100%;
|
|
||||||
height: 50px;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
background: linear-gradient(transparent 0px, var(--light));
|
|
||||||
}*/
|
|
||||||
|
|
||||||
& > h3 {
|
& > h3 {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@@ -31,14 +19,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .overflow {
|
|
||||||
&:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
height: auto;
|
|
||||||
@media all and not ($desktop) {
|
|
||||||
height: 250px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
height: 20px;
|
height: 20px;
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
text-align: inherit;
|
text-align: inherit;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
& svg {
|
& svg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -28,19 +29,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root[saved-theme="dark"] .darkmode {
|
:root[saved-theme="dark"] .darkmode {
|
||||||
& > #dayIcon {
|
& > .dayIcon {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
& > #nightIcon {
|
& > .nightIcon {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:root .darkmode {
|
:root .darkmode {
|
||||||
& > #dayIcon {
|
& > .dayIcon {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
& > #nightIcon {
|
& > .nightIcon {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,11 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
background-color: var(--light);
|
background-color: var(--light);
|
||||||
|
padding: 1rem 0 1rem 0;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide Explorer on mobile until done loading.
|
.hide-until-loaded ~ .explorer-content {
|
||||||
// Prevents ugly animation on page load.
|
|
||||||
.hide-until-loaded ~ #explorer-content {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,10 +28,24 @@
|
|||||||
|
|
||||||
.explorer {
|
.explorer {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
|
||||||
|
min-height: 1.2rem;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
&.collapsed {
|
||||||
|
flex: 0 1 1.2rem;
|
||||||
|
& .fold {
|
||||||
|
transform: rotateZ(-90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .fold {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
@media all and ($mobile) {
|
@media all and ($mobile) {
|
||||||
order: -1;
|
order: -1;
|
||||||
height: initial;
|
height: initial;
|
||||||
@@ -40,20 +54,20 @@
|
|||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
button#mobile-explorer {
|
button.mobile-explorer {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
button#desktop-explorer {
|
button.desktop-explorer {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and ($mobile) {
|
@media all and ($mobile) {
|
||||||
button#mobile-explorer {
|
button.mobile-explorer {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
button#desktop-explorer {
|
button.desktop-explorer {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,22 +78,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*&:after {
|
svg {
|
||||||
pointer-events: none;
|
pointer-events: all;
|
||||||
content: "";
|
transition: transform 0.35s ease;
|
||||||
width: 100%;
|
|
||||||
height: 50px;
|
& > polyline {
|
||||||
position: absolute;
|
pointer-events: none;
|
||||||
left: 0;
|
}
|
||||||
bottom: 0;
|
}
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
background: linear-gradient(transparent 0px, var(--light));
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button#mobile-explorer,
|
button.mobile-explorer,
|
||||||
button#desktop-explorer {
|
button.desktop-explorer {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -94,77 +104,46 @@ button#desktop-explorer {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .fold {
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.collapsed .fold {
|
|
||||||
transform: rotateZ(-90deg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.folder-outer {
|
.explorer-content {
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 0fr;
|
|
||||||
transition: grid-template-rows 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.folder-outer.open {
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.folder-outer > ul {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#explorer-content {
|
|
||||||
list-style: none;
|
list-style: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 0px;
|
|
||||||
transition:
|
|
||||||
max-height 0.35s ease,
|
|
||||||
visibility 0s linear 0.35s;
|
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
visibility: hidden;
|
|
||||||
|
|
||||||
&.collapsed {
|
|
||||||
max-height: 100%;
|
|
||||||
transition:
|
|
||||||
max-height 0.35s ease,
|
|
||||||
visibility 0s linear 0s;
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
& ul {
|
& ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0.08rem 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
transition:
|
|
||||||
max-height 0.35s ease,
|
|
||||||
transform 0.35s ease,
|
|
||||||
opacity 0.2s ease;
|
|
||||||
|
|
||||||
& li > a {
|
& li > a {
|
||||||
color: var(--dark);
|
color: var(--dark);
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--tertiary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> #explorer-ul {
|
.folder-outer {
|
||||||
max-height: none;
|
display: grid;
|
||||||
|
grid-template-rows: 0fr;
|
||||||
|
transition: grid-template-rows 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
.folder-outer.open {
|
||||||
pointer-events: all;
|
grid-template-rows: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
& > polyline {
|
.folder-outer > ul {
|
||||||
pointer-events: none;
|
overflow: hidden;
|
||||||
|
margin-left: 6px;
|
||||||
|
padding-left: 0.8rem;
|
||||||
|
border-left: 1px solid var(--lightgray);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,69 +206,54 @@ li:has(> .folder-outer:not(.open)) > .folder-container > svg {
|
|||||||
color: var(--tertiary);
|
color: var(--tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-background::after {
|
|
||||||
background: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#explorer-end {
|
|
||||||
// needs height so IntersectionObserver gets triggered
|
|
||||||
height: 4px;
|
|
||||||
// remove default margin from li
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.explorer {
|
.explorer {
|
||||||
@media all and ($mobile) {
|
@media all and ($mobile) {
|
||||||
#explorer-content {
|
&.collapsed {
|
||||||
box-sizing: border-box;
|
flex: 0 0 34px;
|
||||||
overscroll-behavior: none;
|
|
||||||
z-index: 100;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
background-color: var(--light);
|
|
||||||
max-width: 100dvw;
|
|
||||||
left: -100dvw;
|
|
||||||
width: 100%;
|
|
||||||
transition: transform 300ms ease-in-out;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: $topSpacing 2rem 2rem;
|
|
||||||
height: 100dvh;
|
|
||||||
max-height: 100dvh;
|
|
||||||
margin-top: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
|
|
||||||
&:not(.collapsed) {
|
& > .explorer-content {
|
||||||
transform: translateX(100dvw);
|
transform: translateX(-100vw);
|
||||||
visibility: visible;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ul.overflow {
|
&:not(.collapsed) {
|
||||||
max-height: 100%;
|
flex: 0 0 34px;
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.collapsed {
|
& > .explorer-content {
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#mobile-explorer {
|
.explorer-content {
|
||||||
margin: 5px;
|
box-sizing: border-box;
|
||||||
z-index: 101;
|
z-index: 100;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
background-color: var(--light);
|
||||||
|
max-width: 100vw;
|
||||||
|
width: 100%;
|
||||||
|
transform: translateX(-100vw);
|
||||||
|
transition:
|
||||||
|
transform 200ms ease,
|
||||||
|
visibility 200ms ease;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 4rem 0 2rem 0;
|
||||||
|
height: 100dvh;
|
||||||
|
max-height: 100dvh;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
&:not(.collapsed) .lucide-menu {
|
.mobile-explorer {
|
||||||
transform: rotate(-90deg);
|
margin: 0;
|
||||||
transition: transform 200ms ease-in-out;
|
padding: 5px;
|
||||||
}
|
z-index: 101;
|
||||||
|
|
||||||
.lucide-menu {
|
.lucide-menu {
|
||||||
stroke: var(--darkgray);
|
stroke: var(--darkgray);
|
||||||
transition: transform 200ms ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
stroke: var(--dark);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
& > #global-graph-icon {
|
& > .global-graph-icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > #global-graph-outer {
|
& > .global-graph-outer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > #global-graph-container {
|
& > .global-graph-container {
|
||||||
border: 1px solid var(--lightgray);
|
border: 1px solid var(--lightgray);
|
||||||
background-color: var(--light);
|
background-color: var(--light);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
details#toc {
|
details.toc {
|
||||||
& summary {
|
& summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > #search-container {
|
& > .search-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
contain: layout;
|
contain: layout;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > #search-space {
|
& > .search-space {
|
||||||
width: 65%;
|
width: 65%;
|
||||||
margin-top: 12vh;
|
margin-top: 12vh;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > #search-layout {
|
& > .search-layout {
|
||||||
display: none;
|
display: none;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
border: 1px solid var(--lightgray);
|
border: 1px solid var(--lightgray);
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-preview] > #results-container {
|
&[data-preview] > .results-container {
|
||||||
flex: 0 0 min(30%, 450px);
|
flex: 0 0 min(30%, 450px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
scroll-margin-top: 2rem;
|
scroll-margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > #preview-container {
|
& > .preview-container {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: block;
|
display: block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -171,7 +171,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > #results-container {
|
& > .results-container {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
& .result-card {
|
& .result-card {
|
||||||
|
|||||||
@@ -4,18 +4,21 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
&.desktop-only {
|
overflow-y: hidden;
|
||||||
max-height: 40%;
|
min-height: 4rem;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
&:has(button.toc-header.collapsed) {
|
||||||
|
flex: 0 1 1.2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and not ($mobile) {
|
@media all and not ($mobile) {
|
||||||
.toc {
|
.toc-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button#toc {
|
button.toc-header {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -42,28 +45,9 @@ button#toc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#toc-content {
|
.toc-content {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
overflow: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 100%;
|
|
||||||
transition:
|
|
||||||
max-height 0.35s ease,
|
|
||||||
visibility 0s linear 0s;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
visibility: visible;
|
|
||||||
|
|
||||||
&.collapsed {
|
|
||||||
max-height: 0;
|
|
||||||
transition:
|
|
||||||
max-height 0.35s ease,
|
|
||||||
visibility 0s linear 0.35s;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.collapsed > .overflow::after {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
& ul {
|
& ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
@@ -80,10 +64,6 @@ button#toc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
> ul.overflow {
|
|
||||||
max-height: none;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@for $i from 0 through 6 {
|
@for $i from 0 through 6 {
|
||||||
& .depth-#{$i} {
|
& .depth-#{$i} {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ComponentType, JSX } from "preact"
|
import { ComponentType, JSX } from "preact"
|
||||||
import { StaticResources } from "../util/resources"
|
import { StaticResources, StringResource } from "../util/resources"
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
import { GlobalConfiguration } from "../cfg"
|
import { GlobalConfiguration } from "../cfg"
|
||||||
import { Node } from "hast"
|
import { Node } from "hast"
|
||||||
@@ -19,9 +19,9 @@ export type QuartzComponentProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type QuartzComponent = ComponentType<QuartzComponentProps> & {
|
export type QuartzComponent = ComponentType<QuartzComponentProps> & {
|
||||||
css?: string
|
css?: StringResource
|
||||||
beforeDOMLoaded?: string
|
beforeDOMLoaded?: StringResource
|
||||||
afterDOMLoaded?: string
|
afterDOMLoaded?: StringResource
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QuartzComponentConstructor<Options extends object | undefined = undefined> = (
|
export type QuartzComponentConstructor<Options extends object | undefined = undefined> = (
|
||||||
|
|||||||
@@ -36,17 +36,21 @@ function getComponentResources(ctx: BuildCtx): ComponentResources {
|
|||||||
afterDOMLoaded: new Set<string>(),
|
afterDOMLoaded: new Set<string>(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeResource(resource: string | string[] | undefined): string[] {
|
||||||
|
if (!resource) return []
|
||||||
|
if (Array.isArray(resource)) return resource
|
||||||
|
return [resource]
|
||||||
|
}
|
||||||
|
|
||||||
for (const component of allComponents) {
|
for (const component of allComponents) {
|
||||||
const { css, beforeDOMLoaded, afterDOMLoaded } = component
|
const { css, beforeDOMLoaded, afterDOMLoaded } = component
|
||||||
if (css) {
|
const normalizedCss = normalizeResource(css)
|
||||||
componentResources.css.add(css)
|
const normalizedBeforeDOMLoaded = normalizeResource(beforeDOMLoaded)
|
||||||
}
|
const normalizedAfterDOMLoaded = normalizeResource(afterDOMLoaded)
|
||||||
if (beforeDOMLoaded) {
|
|
||||||
componentResources.beforeDOMLoaded.add(beforeDOMLoaded)
|
normalizedCss.forEach((c) => componentResources.css.add(c))
|
||||||
}
|
normalizedBeforeDOMLoaded.forEach((b) => componentResources.beforeDOMLoaded.add(b))
|
||||||
if (afterDOMLoaded) {
|
normalizedAfterDOMLoaded.forEach((a) => componentResources.afterDOMLoaded.add(a))
|
||||||
componentResources.afterDOMLoaded.add(afterDOMLoaded)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import DepGraph from "../../depgraph"
|
|||||||
|
|
||||||
export type ContentIndexMap = Map<FullSlug, ContentDetails>
|
export type ContentIndexMap = Map<FullSlug, ContentDetails>
|
||||||
export type ContentDetails = {
|
export type ContentDetails = {
|
||||||
|
slug: FullSlug
|
||||||
title: string
|
title: string
|
||||||
links: SimpleSlug[]
|
links: SimpleSlug[]
|
||||||
tags: string[]
|
tags: string[]
|
||||||
@@ -124,6 +125,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
|
|||||||
const date = getDate(ctx.cfg.configuration, file.data) ?? new Date()
|
const date = getDate(ctx.cfg.configuration, file.data) ?? new Date()
|
||||||
if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) {
|
if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) {
|
||||||
linkIndex.set(slug, {
|
linkIndex.set(slug, {
|
||||||
|
slug,
|
||||||
title: file.data.frontmatter?.title!,
|
title: file.data.frontmatter?.title!,
|
||||||
links: file.data.links ?? [],
|
links: file.data.links ?? [],
|
||||||
tags: file.data.frontmatter?.tags ?? [],
|
tags: file.data.frontmatter?.tags ?? [],
|
||||||
|
|||||||
@@ -351,6 +351,10 @@ h6 {
|
|||||||
&[id]:hover > a {
|
&[id]:hover > a {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:not([id]) > a[role="anchor"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// typography improvements
|
// typography improvements
|
||||||
@@ -538,12 +542,11 @@ video {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.spacer {
|
.spacer {
|
||||||
flex: 1 1 auto;
|
flex: 2 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
div:has(> .overflow) {
|
div:has(> .overflow) {
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,26 +554,21 @@ ul.overflow,
|
|||||||
ol.overflow {
|
ol.overflow {
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
// clearfix
|
// clearfix
|
||||||
content: "";
|
content: "";
|
||||||
clear: both;
|
clear: both;
|
||||||
|
|
||||||
& > li:last-of-type {
|
& > li.overflow-end {
|
||||||
margin-bottom: 30px;
|
height: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.gradient-active {
|
||||||
|
mask-image: linear-gradient(to bottom, black calc(100% - 50px), transparent 100%);
|
||||||
}
|
}
|
||||||
/*&:after {
|
|
||||||
pointer-events: none;
|
|
||||||
content: "";
|
|
||||||
width: 100%;
|
|
||||||
height: 50px;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
background: linear-gradient(transparent 0px, var(--light));
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.transclude {
|
.transclude {
|
||||||
|
|||||||
3
quartz/util/clone.ts
Normal file
3
quartz/util/clone.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import rfdc from "rfdc"
|
||||||
|
|
||||||
|
export const clone = rfdc()
|
||||||
194
quartz/util/fileTrie.test.ts
Normal file
194
quartz/util/fileTrie.test.ts
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import test, { describe, beforeEach } from "node:test"
|
||||||
|
import assert from "node:assert"
|
||||||
|
import { FileTrieNode } from "./fileTrie"
|
||||||
|
|
||||||
|
interface TestData {
|
||||||
|
title: string
|
||||||
|
slug: string
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("FileTrie", () => {
|
||||||
|
let trie: FileTrieNode<TestData>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
trie = new FileTrieNode<TestData>([])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("constructor", () => {
|
||||||
|
test("should create an empty trie", () => {
|
||||||
|
assert.deepStrictEqual(trie.children, [])
|
||||||
|
assert.strictEqual(trie.slug, "")
|
||||||
|
assert.strictEqual(trie.displayName, "")
|
||||||
|
assert.strictEqual(trie.data, null)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should set displayName from data title", () => {
|
||||||
|
const data = {
|
||||||
|
title: "Test Title",
|
||||||
|
slug: "test",
|
||||||
|
}
|
||||||
|
|
||||||
|
trie.add(data)
|
||||||
|
assert.strictEqual(trie.children[0].displayName, "Test Title")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("add", () => {
|
||||||
|
test("should add a file at root level", () => {
|
||||||
|
const data = {
|
||||||
|
title: "Test",
|
||||||
|
slug: "test",
|
||||||
|
}
|
||||||
|
|
||||||
|
trie.add(data)
|
||||||
|
assert.strictEqual(trie.children.length, 1)
|
||||||
|
assert.strictEqual(trie.children[0].slug, "test")
|
||||||
|
assert.strictEqual(trie.children[0].data, data)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle index files", () => {
|
||||||
|
const data = {
|
||||||
|
title: "Index",
|
||||||
|
slug: "index",
|
||||||
|
}
|
||||||
|
|
||||||
|
trie.add(data)
|
||||||
|
assert.strictEqual(trie.data, data)
|
||||||
|
assert.strictEqual(trie.children.length, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should add nested files", () => {
|
||||||
|
const data1 = {
|
||||||
|
title: "Nested",
|
||||||
|
slug: "folder/test",
|
||||||
|
}
|
||||||
|
|
||||||
|
const data2 = {
|
||||||
|
title: "Really nested index",
|
||||||
|
slug: "a/b/c/index",
|
||||||
|
}
|
||||||
|
|
||||||
|
trie.add(data1)
|
||||||
|
trie.add(data2)
|
||||||
|
assert.strictEqual(trie.children.length, 2)
|
||||||
|
assert.strictEqual(trie.children[0].slug, "folder/index")
|
||||||
|
assert.strictEqual(trie.children[0].children.length, 1)
|
||||||
|
assert.strictEqual(trie.children[0].children[0].slug, "folder/test")
|
||||||
|
assert.strictEqual(trie.children[0].children[0].data, data1)
|
||||||
|
|
||||||
|
assert.strictEqual(trie.children[1].slug, "a/index")
|
||||||
|
assert.strictEqual(trie.children[1].children.length, 1)
|
||||||
|
assert.strictEqual(trie.children[1].data, null)
|
||||||
|
|
||||||
|
assert.strictEqual(trie.children[1].children[0].slug, "a/b/index")
|
||||||
|
assert.strictEqual(trie.children[1].children[0].children.length, 1)
|
||||||
|
assert.strictEqual(trie.children[1].children[0].data, null)
|
||||||
|
|
||||||
|
assert.strictEqual(trie.children[1].children[0].children[0].slug, "a/b/c/index")
|
||||||
|
assert.strictEqual(trie.children[1].children[0].children[0].data, data2)
|
||||||
|
assert.strictEqual(trie.children[1].children[0].children[0].children.length, 0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("filter", () => {
|
||||||
|
test("should filter nodes based on condition", () => {
|
||||||
|
const data1 = { title: "Test1", slug: "test1" }
|
||||||
|
const data2 = { title: "Test2", slug: "test2" }
|
||||||
|
|
||||||
|
trie.add(data1)
|
||||||
|
trie.add(data2)
|
||||||
|
|
||||||
|
trie.filter((node) => node.slug !== "test1")
|
||||||
|
assert.strictEqual(trie.children.length, 1)
|
||||||
|
assert.strictEqual(trie.children[0].slug, "test2")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("map", () => {
|
||||||
|
test("should apply function to all nodes", () => {
|
||||||
|
const data1 = { title: "Test1", slug: "test1" }
|
||||||
|
const data2 = { title: "Test2", slug: "test2" }
|
||||||
|
|
||||||
|
trie.add(data1)
|
||||||
|
trie.add(data2)
|
||||||
|
|
||||||
|
trie.map((node) => {
|
||||||
|
if (node.data) {
|
||||||
|
node.data.title = "Modified"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.strictEqual(trie.children[0].displayName, "Modified")
|
||||||
|
assert.strictEqual(trie.children[1].displayName, "Modified")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("entries", () => {
|
||||||
|
test("should return all entries", () => {
|
||||||
|
const data1 = { title: "Test1", slug: "test1" }
|
||||||
|
const data2 = { title: "Test2", slug: "a/b/test2" }
|
||||||
|
|
||||||
|
trie.add(data1)
|
||||||
|
trie.add(data2)
|
||||||
|
|
||||||
|
const entries = trie.entries()
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
entries.map(([path, node]) => [path, node.data]),
|
||||||
|
[
|
||||||
|
["index", trie.data],
|
||||||
|
["test1", data1],
|
||||||
|
["a/index", null],
|
||||||
|
["a/b/index", null],
|
||||||
|
["a/b/test2", data2],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getFolderPaths", () => {
|
||||||
|
test("should return all folder paths", () => {
|
||||||
|
const data1 = {
|
||||||
|
title: "Root",
|
||||||
|
slug: "index",
|
||||||
|
}
|
||||||
|
const data2 = {
|
||||||
|
title: "Test",
|
||||||
|
slug: "folder/subfolder/test",
|
||||||
|
}
|
||||||
|
const data3 = {
|
||||||
|
title: "Folder Index",
|
||||||
|
slug: "abc/index",
|
||||||
|
}
|
||||||
|
|
||||||
|
trie.add(data1)
|
||||||
|
trie.add(data2)
|
||||||
|
trie.add(data3)
|
||||||
|
const paths = trie.getFolderPaths()
|
||||||
|
|
||||||
|
assert.deepStrictEqual(paths, [
|
||||||
|
"index",
|
||||||
|
"folder/index",
|
||||||
|
"folder/subfolder/index",
|
||||||
|
"abc/index",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("sort", () => {
|
||||||
|
test("should sort nodes according to sort function", () => {
|
||||||
|
const data1 = { title: "A", slug: "a" }
|
||||||
|
const data2 = { title: "B", slug: "b" }
|
||||||
|
const data3 = { title: "C", slug: "c" }
|
||||||
|
|
||||||
|
trie.add(data3)
|
||||||
|
trie.add(data1)
|
||||||
|
trie.add(data2)
|
||||||
|
|
||||||
|
trie.sort((a, b) => a.slug.localeCompare(b.slug))
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
trie.children.map((n) => n.slug),
|
||||||
|
["a", "b", "c"],
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
127
quartz/util/fileTrie.ts
Normal file
127
quartz/util/fileTrie.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { ContentDetails } from "../plugins/emitters/contentIndex"
|
||||||
|
import { FullSlug, joinSegments } from "./path"
|
||||||
|
|
||||||
|
interface FileTrieData {
|
||||||
|
slug: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FileTrieNode<T extends FileTrieData = ContentDetails> {
|
||||||
|
isFolder: boolean
|
||||||
|
children: Array<FileTrieNode<T>>
|
||||||
|
|
||||||
|
private slugSegments: string[]
|
||||||
|
data: T | null
|
||||||
|
|
||||||
|
constructor(segments: string[], data?: T) {
|
||||||
|
this.children = []
|
||||||
|
this.slugSegments = segments
|
||||||
|
this.data = data ?? null
|
||||||
|
this.isFolder = false
|
||||||
|
}
|
||||||
|
|
||||||
|
get displayName(): string {
|
||||||
|
return this.data?.title ?? this.slugSegment ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
get slug(): FullSlug {
|
||||||
|
const path = joinSegments(...this.slugSegments) as FullSlug
|
||||||
|
if (this.isFolder) {
|
||||||
|
return joinSegments(path, "index") as FullSlug
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
get slugSegment(): string {
|
||||||
|
return this.slugSegments[this.slugSegments.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
private makeChild(path: string[], file?: T) {
|
||||||
|
const fullPath = [...this.slugSegments, path[0]]
|
||||||
|
const child = new FileTrieNode<T>(fullPath, file)
|
||||||
|
this.children.push(child)
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
|
||||||
|
private insert(path: string[], file: T) {
|
||||||
|
if (path.length === 0) {
|
||||||
|
throw new Error("path is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we are inserting, we are a folder
|
||||||
|
this.isFolder = true
|
||||||
|
const segment = path[0]
|
||||||
|
if (path.length === 1) {
|
||||||
|
// base case, we are at the end of the path
|
||||||
|
if (segment === "index") {
|
||||||
|
this.data ??= file
|
||||||
|
} else {
|
||||||
|
this.makeChild(path, file)
|
||||||
|
}
|
||||||
|
} else if (path.length > 1) {
|
||||||
|
// recursive case, we are not at the end of the path
|
||||||
|
const child =
|
||||||
|
this.children.find((c) => c.slugSegment === segment) ?? this.makeChild(path, undefined)
|
||||||
|
child.insert(path.slice(1), file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new file to trie
|
||||||
|
add(file: T) {
|
||||||
|
this.insert(file.slug.split("/"), file)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter trie nodes. Behaves similar to `Array.prototype.filter()`, but modifies tree in place
|
||||||
|
*/
|
||||||
|
filter(filterFn: (node: FileTrieNode<T>) => boolean) {
|
||||||
|
this.children = this.children.filter(filterFn)
|
||||||
|
this.children.forEach((child) => child.filter(filterFn))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map over trie nodes. Behaves similar to `Array.prototype.map()`, but modifies tree in place
|
||||||
|
*/
|
||||||
|
map(mapFn: (node: FileTrieNode<T>) => void) {
|
||||||
|
mapFn(this)
|
||||||
|
this.children.forEach((child) => child.map(mapFn))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort trie nodes according to sort/compare function
|
||||||
|
*/
|
||||||
|
sort(sortFn: (a: FileTrieNode<T>, b: FileTrieNode<T>) => number) {
|
||||||
|
this.children = this.children.sort(sortFn)
|
||||||
|
this.children.forEach((e) => e.sort(sortFn))
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromEntries<T extends FileTrieData>(entries: [FullSlug, T][]) {
|
||||||
|
const trie = new FileTrieNode<T>([])
|
||||||
|
entries.forEach(([, entry]) => trie.add(entry))
|
||||||
|
return trie
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all entries in the trie
|
||||||
|
* in the a flat array including the full path and the node
|
||||||
|
*/
|
||||||
|
entries(): [FullSlug, FileTrieNode<T>][] {
|
||||||
|
const traverse = (node: FileTrieNode<T>): [FullSlug, FileTrieNode<T>][] => {
|
||||||
|
const result: [FullSlug, FileTrieNode<T>][] = [[node.slug, node]]
|
||||||
|
return result.concat(...node.children.map(traverse))
|
||||||
|
}
|
||||||
|
|
||||||
|
return traverse(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all folder paths in the trie
|
||||||
|
* @returns array containing folder state for trie
|
||||||
|
*/
|
||||||
|
getFolderPaths() {
|
||||||
|
return this.entries()
|
||||||
|
.filter(([_, node]) => node.isFolder)
|
||||||
|
.map(([path, _]) => path)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
import { slug as slugAnchor } from "github-slugger"
|
import { slug as slugAnchor } from "github-slugger"
|
||||||
import type { Element as HastElement } from "hast"
|
import type { Element as HastElement } from "hast"
|
||||||
import rfdc from "rfdc"
|
import { clone } from "./clone"
|
||||||
|
|
||||||
export const clone = rfdc()
|
|
||||||
|
|
||||||
// this file must be isomorphic so it can't use node libs (e.g. path)
|
// this file must be isomorphic so it can't use node libs (e.g. path)
|
||||||
|
|
||||||
export const QUARTZ = "quartz"
|
export const QUARTZ = "quartz"
|
||||||
|
|||||||
3
quartz/util/random.ts
Normal file
3
quartz/util/random.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function randomIdNonSecure() {
|
||||||
|
return Math.random().toString(36).substring(2, 8)
|
||||||
|
}
|
||||||
@@ -65,3 +65,10 @@ export interface StaticResources {
|
|||||||
js: JSResource[]
|
js: JSResource[]
|
||||||
additionalHead: (JSX.Element | ((pageData: QuartzPluginData) => JSX.Element))[]
|
additionalHead: (JSX.Element | ((pageData: QuartzPluginData) => JSX.Element))[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type StringResource = string | string[] | undefined
|
||||||
|
export function concatenateResources(...resources: StringResource[]): StringResource {
|
||||||
|
return resources
|
||||||
|
.filter((resource): resource is string | string[] => resource !== undefined)
|
||||||
|
.flat()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user