Compare commits

...

14 Commits

Author SHA1 Message Date
Jacky Zhao
b00198b888 fix: load mermaid as normal now that inline is safely below bundle size
Some checks are pending
Build and Test / build-and-test (macos-latest) (push) Waiting to run
Build and Test / build-and-test (windows-latest) (push) Waiting to run
Build and Test / build-and-test (ubuntu-latest) (push) Has been skipped
Build and Test / publish-tag (push) Has been skipped
2025-03-10 16:42:08 -07:00
Jacky Zhao
9e3e711646 fix: mermaid script load order 2025-03-10 16:20:08 -07:00
Jacky Zhao
a8001e9554 feat: support non-singleton explorer
Some checks are pending
Build and Test / build-and-test (macos-latest) (push) Waiting to run
Build and Test / build-and-test (windows-latest) (push) Waiting to run
Build and Test / build-and-test (ubuntu-latest) (push) Has been skipped
Build and Test / publish-tag (push) Has been skipped
2025-03-10 15:13:22 -07:00
Aaron Pham
dd940a007c Merge pull request #1820 from jackyzha0/dependabot/npm_and_yarn/production-dependencies-8ce87f9e70
chore(deps): bump the production-dependencies group with 5 updates
2025-03-10 17:57:39 -04:00
dependabot[bot]
a71e17919b chore(deps): bump the production-dependencies group with 5 updates
Bumps the production-dependencies group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [hast-util-to-jsx-runtime](https://github.com/syntax-tree/hast-util-to-jsx-runtime) | `2.3.5` | `2.3.6` |
| [lightningcss](https://github.com/parcel-bundler/lightningcss) | `1.29.1` | `1.29.2` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `22.13.9` | `22.13.10` |
| [@types/ws](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/ws) | `8.5.14` | `8.18.0` |
| [esbuild](https://github.com/evanw/esbuild) | `0.25.0` | `0.25.1` |


Updates `hast-util-to-jsx-runtime` from 2.3.5 to 2.3.6
- [Release notes](https://github.com/syntax-tree/hast-util-to-jsx-runtime/releases)
- [Commits](https://github.com/syntax-tree/hast-util-to-jsx-runtime/compare/2.3.5...2.3.6)

Updates `lightningcss` from 1.29.1 to 1.29.2
- [Release notes](https://github.com/parcel-bundler/lightningcss/releases)
- [Commits](https://github.com/parcel-bundler/lightningcss/commits/v1.29.2)

Updates `@types/node` from 22.13.9 to 22.13.10
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@types/ws` from 8.5.14 to 8.18.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/ws)

Updates `esbuild` from 0.25.0 to 0.25.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.0...v0.25.1)

---
updated-dependencies:
- dependency-name: hast-util-to-jsx-runtime
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: lightningcss
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: "@types/ws"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: esbuild
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 20:57:05 +00:00
Aaron Pham
aca0c330e7 docs: cleanup showcase (#1818)
Signed-off-by: Aaron Pham <contact@aarnphm.xyz>
2025-03-10 11:46:49 -07:00
Jacky Zhao
dcaf806190 feat: support non-singleton darkmode 2025-03-10 11:44:47 -07:00
Jacky Zhao
23df17233d fix(graph): make graph non-singleton, proper cleanup, fix radial 2025-03-10 11:39:08 -07:00
Jacky Zhao
8d33608808 fix(popovers): clear id to avoid anchor jumps within popover
Some checks are pending
Build and Test / build-and-test (macos-latest) (push) Waiting to run
Build and Test / build-and-test (windows-latest) (push) Waiting to run
Build and Test / build-and-test (ubuntu-latest) (push) Has been skipped
Build and Test / publish-tag (push) Has been skipped
2025-03-10 00:08:06 -07:00
Jacky Zhao
d618a4e3f3 fix(explorer): dont rely on data to get slug, compute it in trie 2025-03-09 23:36:10 -07:00
Jacky Zhao
9c8fec06d2 feat: support non-singleton search
Some checks are pending
Build and Test / build-and-test (macos-latest) (push) Waiting to run
Build and Test / build-and-test (windows-latest) (push) Waiting to run
Build and Test / build-and-test (ubuntu-latest) (push) Has been skipped
Build and Test / publish-tag (push) Has been skipped
2025-03-09 15:33:15 -07:00
Jacky Zhao
1cd8e7f0d5 feat: support non-singleton table of contents 2025-03-09 15:06:36 -07:00
Jacky Zhao
5480269d38 perf(explorer): client side explorer (#1810)
Some checks are pending
Build and Test / build-and-test (macos-latest) (push) Waiting to run
Build and Test / build-and-test (windows-latest) (push) Waiting to run
Build and Test / build-and-test (ubuntu-latest) (push) Has been skipped
Build and Test / publish-tag (push) Has been skipped
* start work on client side explorer

* fix tests

* fmt

* generic test flag

* add prenav hook

* add highlight class

* make flex more consistent, remove transition

* open folders that are prefixes of current path

* make mobile look nice

* more style fixes
2025-03-09 14:58:26 -07:00
Jacky Zhao
a201105442 fix(docker): instructions + bump deps + bind mount (#1809)
Some checks failed
Build and Test / build-and-test (ubuntu-latest) (push) Has been skipped
Build and Test / publish-tag (push) Has been skipped
Build and Test / build-and-test (windows-latest) (push) Has been cancelled
Build and Test / build-and-test (macos-latest) (push) Has been cancelled
* fix docker

* test with docs folder
2025-03-06 10:01:25 -08:00
51 changed files with 1361 additions and 1154 deletions

View File

@@ -45,7 +45,7 @@ jobs:
run: npm test run: npm test
- name: Ensure Quartz builds, check bundle info - name: Ensure Quartz builds, check bundle info
run: npx quartz build --bundleInfo run: npx quartz build --bundleInfo -d docs
publish-tag: publish-tag:
if: ${{ github.repository == 'jackyzha0/quartz' && github.ref == 'refs/heads/v4' }} if: ${{ github.repository == 'jackyzha0/quartz' && github.ref == 'refs/heads/v4' }}

View File

@@ -1,10 +1,10 @@
FROM node:20-slim AS builder FROM node:22-slim AS builder
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY package.json . COPY package.json .
COPY package-lock.json* . COPY package-lock.json* .
RUN npm ci RUN npm ci
FROM node:20-slim FROM node:22-slim
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY --from=builder /usr/src/app/ /usr/src/app/ COPY --from=builder /usr/src/app/ /usr/src/app/
COPY . . COPY . .

View File

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

View File

@@ -3,5 +3,5 @@ Quartz comes shipped with a Docker image that will allow you to preview your Qua
You can run the below one-liner to run Quartz in Docker. You can run the below one-liner to run Quartz in Docker.
```sh ```sh
docker run --rm -itp 8080:8080 $(docker build -q .) docker run --rm -itp 8080:8080 -p 3001:3001 -v ./content:/usr/src/app/content $(docker build -q .)
``` ```

View File

@@ -31,13 +31,13 @@ If you prefer instructions in a video format you can try following Nicole van de
## 🔧 Features ## 🔧 Features
- [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[features/Latex|Latex]], [[syntax highlighting]], [[popover previews]], [[Docker Support]], [[i18n|internationalization]], [[comments]] and [many more](./features) right out of the box - [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[features/Latex|Latex]], [[syntax highlighting]], [[popover previews]], [[Docker Support]], [[i18n|internationalization]], [[comments]] and [many more](./features/) right out of the box
- Hot-reload for both configuration and content - Hot-reload for both configuration and content
- Simple JSX layouts and [[creating components|page components]] - Simple JSX layouts and [[creating components|page components]]
- [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes - [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes
- Fully-customizable parsing, filtering, and page generation through [[making plugins|plugins]] - Fully-customizable parsing, filtering, and page generation through [[making plugins|plugins]]
For a comprehensive list of features, visit the [features page](/features). You can read more about the _why_ behind these features on the [[philosophy]] page and a technical overview on the [[architecture]] page. For a comprehensive list of features, visit the [features page](./features/). You can read more about the _why_ behind these features on the [[philosophy]] page and a technical overview on the [[architecture]] page.
### 🚧 Troubleshooting + Updating ### 🚧 Troubleshooting + Updating

View File

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

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

@@ -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": {

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env node --no-deprecation #!/usr/bin/env -S node --no-deprecation
import yargs from "yargs" import yargs from "yargs"
import { hideBin } from "yargs/helpers" import { hideBin } from "yargs/helpers"
import { import {

View File

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

View File

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

View File

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

View File

@@ -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
<button class={classNames(displayClass, "explorer")}
type="button"
id="mobile-explorer"
class="collapsed hide-until-loaded"
data-behavior={opts.folderClickBehavior} data-behavior={opts.folderClickBehavior}
data-collapsed={opts.folderDefaultState} data-collapsed={opts.folderDefaultState}
data-savestate={opts.useSavedState} data-savestate={opts.useSavedState}
data-tree={jsonTree} data-data-fns={JSON.stringify({
order: opts.order,
sortFn: opts.sortFn.toString(),
filterFn: opts.filterFn.toString(),
mapFn: opts.mapFn.toString(),
})}
>
<button
type="button"
class="explorer-toggle mobile-explorer hide-until-loaded"
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

View File

@@ -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>
)}
</>
)
}

View File

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

View 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())
})
`,
}
}

View File

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

View File

@@ -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,6 +17,9 @@ const defaultOptions: Options = {
layout: "modern", layout: "modern",
} }
export default ((opts?: Partial<Options>) => {
const layout = opts?.layout ?? defaultOptions.layout
const { OverflowList, overflowListAfterDOMLoaded } = OverflowListFactory()
const TableOfContents: QuartzComponent = ({ const TableOfContents: QuartzComponent = ({
fileData, fileData,
displayClass, displayClass,
@@ -28,8 +33,7 @@ const TableOfContents: QuartzComponent = ({
<div class={classNames(displayClass, "toc")}> <div class={classNames(displayClass, "toc")}>
<button <button
type="button" type="button"
id="toc" class={fileData.collapseToc ? "collapsed toc-header" : "toc-header"}
class={fileData.collapseToc ? "collapsed" : ""}
aria-controls="toc-content" aria-controls="toc-content"
aria-expanded={!fileData.collapseToc} aria-expanded={!fileData.collapseToc}
> >
@@ -49,8 +53,8 @@ const TableOfContents: QuartzComponent = ({
<polyline points="6 9 12 15 18 9"></polyline> <polyline points="6 9 12 15 18 9"></polyline>
</svg> </svg>
</button> </button>
<div id="toc-content" class={fileData.collapseToc ? "collapsed" : ""}> <div class={fileData.collapseToc ? "collapsed toc-content" : "toc-content"}>
<ul class="overflow"> <OverflowList>
{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}>
@@ -58,20 +62,21 @@ const TableOfContents: QuartzComponent = ({
</a> </a>
</li> </li>
))} ))}
</ul> </OverflowList>
</div> </div>
</div> </div>
) )
} }
TableOfContents.css = modernStyle TableOfContents.css = modernStyle
TableOfContents.afterDOMLoaded = script TableOfContents.afterDOMLoaded = concatenateResources(script, overflowListAfterDOMLoaded)
const LegacyTableOfContents: QuartzComponent = ({ fileData, cfg }: QuartzComponentProps) => { const LegacyTableOfContents: QuartzComponent = ({ fileData, cfg }: QuartzComponentProps) => {
if (!fileData.toc) { if (!fileData.toc) {
return null return null
} }
return ( return (
<details id="toc" open={!fileData.collapseToc}> <details class="toc" open={!fileData.collapseToc}>
<summary> <summary>
<h3>{i18n(cfg.locale).components.tableOfContents.title}</h3> <h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
</summary> </summary>
@@ -89,7 +94,5 @@ const LegacyTableOfContents: QuartzComponent = ({ fileData, cfg }: QuartzCompone
} }
LegacyTableOfContents.css = legacyStyle 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

View File

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

View File

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

View File

@@ -3,14 +3,12 @@ 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"
import { i18n } from "../i18n" import { i18n } from "../i18n"
// @ts-ignore
import mermaidScript from "./scripts/mermaid.inline"
import mermaidStyle from "./styles/mermaid.inline.scss"
import { QuartzPluginData } from "../plugins/vfile" import { QuartzPluginData } from "../plugins/vfile"
interface RenderComponents { interface RenderComponents {
@@ -57,17 +55,6 @@ export function pageResources(
additionalHead: staticResources.additionalHead, additionalHead: staticResources.additionalHead,
} }
if (fileData.hasMermaidDiagram) {
resources.js.push({
script: mermaidScript,
loadTime: "afterDOMReady",
moduleType: "module",
contentType: "inline",
})
resources.css.push({ content: mermaidStyle, inline: true })
}
// NOTE: we have to put this last to make sure spa.inline.ts is the last item.
resources.js.push({ resources.js.push({
src: joinSegments(baseDir, "postscript.js"), src: joinSegments(baseDir, "postscript.js"),
loadTime: "afterDOMReady", loadTime: "afterDOMReady",

View File

@@ -28,8 +28,8 @@ function setupCallout() {
) as HTMLCollectionOf<HTMLElement> ) as HTMLCollectionOf<HTMLElement>
for (const div of collapsible) { for (const div of collapsible) {
const title = div.firstElementChild const title = div.firstElementChild
if (!title) continue
if (title) {
title.addEventListener("click", toggleCallout) title.addEventListener("click", toggleCallout)
window.addCleanup(() => title.removeEventListener("click", toggleCallout)) window.addCleanup(() => title.removeEventListener("click", toggleCallout))
@@ -38,7 +38,5 @@ function setupCallout() {
div.style.maxHeight = height + "px" div.style.maxHeight = height + "px"
} }
} }
}
document.addEventListener("nav", setupCallout) document.addEventListener("nav", setupCallout)
window.addEventListener("resize", setupCallout)

View File

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

View File

@@ -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) : []
const oldIndex = new Map(
serializedExplorerState.map((entry: FolderState) => [entry.path, entry.collapsed]),
)
// Convert to bool const data = await fetchData
const useSavedFolderState = explorer?.dataset.savestate === "true" const entries = [...Object.entries(data)] as [FullSlug, ContentDetails][]
const trie = FileTrieNode.fromEntries(entries)
if (explorer) { // Apply functions in order
// Get config for (const fn of opts.order) {
const collapseBehavior = explorer.dataset.behavior switch (fn) {
case "filter":
// Add click handlers for all folders (click handler on folder "label") if (opts.filterFn) trie.filter(opts.filterFn)
if (collapseBehavior === "collapse") { break
for (const item of document.getElementsByClassName( case "map":
"folder-button", if (opts.mapFn) trie.map(opts.mapFn)
) as HTMLCollectionOf<HTMLElement>) { break
window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer)) case "sort":
item.addEventListener("click", toggleFolder) if (opts.sortFn) trie.sort(opts.sortFn)
break
} }
} }
// Add click handler to main explorer // Get folder paths for state management
window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer)) const folderPaths = trie.getFolderPaths()
explorer.addEventListener("click", toggleExplorer) currentExplorerState = folderPaths.map((path) => ({
}
// 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, path,
collapsed: oldIndex.get(path) ?? collapsed, collapsed: oldIndex.get(path) === true,
}))
const explorerUl = explorer.querySelector(".explorer-ul")
if (!explorerUl) continue
// Create and insert new content
const fragment = document.createDocumentFragment()
for (const child of trie.children) {
const node = child.isFolder
? 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))
}
}
}
document.addEventListener("prenav", async () => {
// save explorer scrollTop position
const explorer = document.querySelector(".explorer-ul")
if (!explorer) return
sessionStorage.setItem("explorerScrollTop", explorer.scrollTop.toString())
}) })
}
currentExplorerState.map((folderState) => { document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
const folderLi = document.querySelector( const currentSlug = e.detail.url
`[data-folderpath='${folderState.path.replace("'", "-")}']`, await setupExplorer(currentSlug)
) as MaybeHTMLElement
const folderUl = folderLi?.parentElement?.nextElementSibling as MaybeHTMLElement
if (folderUl) {
setFolderState(folderUl, folderState.collapsed)
}
})
}
}
function toggleExplorerFolders() { // if mobile hamburger is visible, collapse by default
const currentFile = (document.querySelector("body")?.getAttribute("data-slug") ?? "").replace( for (const explorer of document.getElementsByClassName("mobile-explorer")) {
/\/index$/g, if (explorer.checkVisibility()) {
"",
)
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") ?? "")) {
if (!element.classList.contains("open")) {
element.classList.add("open")
}
}
}
})
}
window.addEventListener("resize", setupExplorer)
document.addEventListener("nav", () => {
const explorer = document.querySelector("#mobile-explorer")
if (explorer) {
explorer.classList.add("collapsed") explorer.classList.add("collapsed")
const content = explorer.nextElementSibling?.nextElementSibling as HTMLElement explorer.setAttribute("aria-expanded", "false")
if (content) {
content.classList.add("collapsed")
content.classList.toggle("explorer-viewmode")
} }
} }
setupExplorer()
observer.disconnect()
// select pseudo element at end of list
const lastItem = document.getElementById("explorer-end")
if (lastItem) {
observer.observe(lastItem)
}
// Hide explorer on mobile until it is requested
const hiddenUntilDoneLoading = document.querySelector("#mobile-explorer") const hiddenUntilDoneLoading = document.querySelector("#mobile-explorer")
hiddenUntilDoneLoading?.classList.remove("hide-until-loaded") hiddenUntilDoneLoading?.classList.remove("hide-until-loaded")
toggleExplorerFolders()
}) })
/**
* 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
}
}

View File

@@ -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) {
container.classList.add("active")
const sidebar = container.closest(".sidebar") as HTMLElement
if (sidebar) { if (sidebar) {
sidebar.style.zIndex = "1" 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()
for (const container of containers) {
container.classList.remove("active")
const sidebar = container.closest(".sidebar") as HTMLElement
if (sidebar) { if (sidebar) {
sidebar.style.zIndex = "" 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()
})
}) })

View File

@@ -1,4 +1,4 @@
import { removeAllChildren } from "./util" import { registerEscapeHandler, removeAllChildren } from "./util"
interface Position { interface Position {
x: number x: number
@@ -237,12 +237,12 @@ document.addEventListener("nav", async () => {
closeBtn.addEventListener("click", hideMermaid) closeBtn.addEventListener("click", hideMermaid)
expandBtn.addEventListener("click", showMermaid) expandBtn.addEventListener("click", showMermaid)
registerEscapeHandler(popupContainer, hideMermaid)
document.addEventListener("keydown", handleEscape) document.addEventListener("keydown", handleEscape)
window.addCleanup(() => { window.addCleanup(() => {
closeBtn.removeEventListener("click", hideMermaid) closeBtn.removeEventListener("click", hideMermaid)
expandBtn.removeEventListener("click", showMermaid) expandBtn.removeEventListener("click", showMermaid)
document.removeEventListener("keydown", handleEscape)
}) })
} }
}) })

View File

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

View File

@@ -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 sidebar = container.closest(".sidebar") as HTMLElement
const searchButton = document.getElementById("search-button") if (!sidebar) return
const searchBar = document.getElementById("search-bar") as HTMLInputElement | null
const searchLayout = document.getElementById("search-layout") 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 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
}
if (sidebar) {
sidebar.style.zIndex = "" sidebar.style.zIndex = ""
}
if (results) {
removeAllChildren(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")
container?.classList.add("active") searchBar.focus()
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)
}
})

View File

@@ -56,8 +56,10 @@ function startLoading() {
}, 100) }, 100)
} }
let isNavigating = false
let p: DOMParser let p: DOMParser
async function navigate(url: URL, isBack: boolean = false) { async function _navigate(url: URL, isBack: boolean = false) {
isNavigating = true
startLoading() startLoading()
p = p || new DOMParser() p = p || new DOMParser()
const contents = await fetchCanonical(url) const contents = await fetchCanonical(url)
@@ -75,6 +77,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 +114,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])")
@@ -124,6 +130,19 @@ async function navigate(url: URL, isBack: boolean = false) {
delete announcer.dataset.persist delete announcer.dataset.persist
} }
async function navigate(url: URL, isBack: boolean = false) {
if (isNavigating) return
isNavigating = true
try {
await _navigate(url, isBack)
} catch (e) {
console.error(e)
window.location.assign(url)
} finally {
isNavigating = false
}
}
window.spaNavigate = navigate window.spaNavigate = navigate
function createRouter() { function createRouter() {
@@ -141,21 +160,13 @@ function createRouter() {
return return
} }
try {
navigate(url, false) navigate(url, false)
} catch (e) {
window.location.assign(url)
}
}) })
window.addEventListener("popstate", (event) => { window.addEventListener("popstate", (event) => {
const { url } = getOpts(event) ?? {} const { url } = getOpts(event) ?? {}
if (window.location.hash && window.location.pathname === url?.pathname) return if (window.location.hash && window.location.pathname === url?.pathname) return
try {
navigate(new URL(window.location.toString()), true) navigate(new URL(window.location.toString()), true)
} catch (e) {
window.location.reload()
}
return return
}) })
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: all;
transition: transform 0.35s ease;
& > polyline {
pointer-events: none; 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));
}*/
} }
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,15 +104,28 @@ 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 { .explorer-content {
transform: rotateZ(-90deg); list-style: none;
overflow: hidden;
overflow-y: auto;
margin-top: 0.5rem;
& ul {
list-style: none;
margin: 0;
padding: 0;
& li > a {
color: var(--dark);
opacity: 0.75;
pointer-events: all;
&.active {
opacity: 1;
color: var(--tertiary);
}
} }
} }
@@ -118,53 +141,9 @@ button#desktop-explorer {
.folder-outer > ul { .folder-outer > ul {
overflow: hidden; overflow: hidden;
} margin-left: 6px;
padding-left: 0.8rem;
#explorer-content { border-left: 1px solid var(--lightgray);
list-style: none;
overflow: hidden;
overflow-y: auto;
max-height: 0px;
transition:
max-height 0.35s ease,
visibility 0s linear 0.35s;
margin-top: 0.5rem;
visibility: hidden;
&.collapsed {
max-height: 100%;
transition:
max-height 0.35s ease,
visibility 0s linear 0s;
visibility: visible;
}
& ul {
list-style: none;
margin: 0.08rem 0;
padding: 0;
transition:
max-height 0.35s ease,
transform 0.35s ease,
opacity 0.2s ease;
& li > a {
color: var(--dark);
opacity: 0.75;
pointer-events: all;
}
}
> #explorer-ul {
max-height: none;
}
}
svg {
pointer-events: all;
& > polyline {
pointer-events: none;
} }
} }
@@ -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; & > .explorer-content {
position: absolute; transform: translateX(-100vw);
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; visibility: hidden;
}
}
&:not(.collapsed) { &:not(.collapsed) {
transform: translateX(100dvw); flex: 0 0 34px;
visibility: visible;
}
ul.overflow { & > .explorer-content {
max-height: 100%;
width: 100%;
}
&.collapsed {
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;
&:not(.collapsed) .lucide-menu { top: 0;
transform: rotate(-90deg); left: 0;
transition: transform 200ms ease-in-out; 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;
} }
.mobile-explorer {
margin: 0;
padding: 5px;
z-index: 101;
.lucide-menu { .lucide-menu {
stroke: var(--darkgray); stroke: var(--darkgray);
transition: transform 200ms ease;
&:hover {
stroke: var(--dark);
}
} }
} }
} }

View File

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

View File

@@ -1,4 +1,4 @@
details#toc { details.toc {
& summary { & summary {
cursor: pointer; cursor: pointer;

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 ?? [],

View File

@@ -16,9 +16,12 @@ import path from "path"
import { splitAnchor } from "../../util/path" import { splitAnchor } from "../../util/path"
import { JSResource, CSSResource } from "../../util/resources" import { JSResource, CSSResource } from "../../util/resources"
// @ts-ignore // @ts-ignore
import calloutScript from "../../components/scripts/callout.inline.ts" import calloutScript from "../../components/scripts/callout.inline"
// @ts-ignore // @ts-ignore
import checkboxScript from "../../components/scripts/checkbox.inline.ts" import checkboxScript from "../../components/scripts/checkbox.inline"
// @ts-ignore
import mermaidScript from "../../components/scripts/mermaid.inline"
import mermaidStyle from "../../components/styles/mermaid.inline.scss"
import { FilePath, pathToRoot, slugTag, slugifyFilePath } from "../../util/path" import { FilePath, pathToRoot, slugTag, slugifyFilePath } from "../../util/path"
import { toHast } from "mdast-util-to-hast" import { toHast } from "mdast-util-to-hast"
import { toHtml } from "hast-util-to-html" import { toHtml } from "hast-util-to-html"
@@ -806,6 +809,20 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
}) })
} }
if (opts.mermaid) {
js.push({
script: mermaidScript,
loadTime: "afterDOMReady",
contentType: "inline",
moduleType: "module",
})
css.push({
content: mermaidStyle,
inline: true,
})
}
return { js, css } return { js, css }
}, },
} }

View File

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

@@ -0,0 +1,3 @@
import rfdc from "rfdc"
export const clone = rfdc()

View 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
View 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)
}
}

View File

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

@@ -0,0 +1,3 @@
export function randomIdNonSecure() {
return Math.random().toString(36).substring(2, 8)
}

View File

@@ -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()
}