Compare commits

...

304 Commits

Author SHA1 Message Date
d2b18b3110 fixed layout and og image font
Some checks failed
Build / build (push) Has been cancelled
2025-08-20 18:11:25 +09:00
60bb878d5e Merge commit '7fa9253abc1e4056d425847e2eaa5a8e107fc297' into v4 2025-08-20 17:40:13 +09:00
0203d4e171 [obsidian] vault backup: 2025-08-20 17:15:49 2025-08-20 17:15:49 +09:00
3daea1fe8a [obsidian] vault backup: 2025-08-20 16:56:25[ 2025-08-20 16:56:25 +09:00
2d572c3073 [obsidian] vault backup: 2025-08-20 15:53:54[ 2025-08-20 15:53:54 +09:00
2d448d58c8 [obsidian] vault backup: 2025-08-20 14:51:23[ 2025-08-20 14:51:23 +09:00
8a91dfb74b [obsidian] vault backup: 2025-08-20 11:43:51[ 2025-08-20 11:43:51 +09:00
Emile Bangma
7fa9253abc Node 22 (#1997) 2025-05-28 16:20:59 -07:00
dependabot[bot]
996d8d51fa chore(deps): bump the production-dependencies group across 1 directory with 9 updates (#1996)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-28 09:21:56 -04:00
fl0werpowers
951d1dec24 chore(deps): replace chalk and rimraf with builtin functions (#1879)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-28 10:40:51 +02:00
Keisuke ANDO
51b43a2115 feat(links): added ofm option to style unresolved or broken links differently (#1992)
* feat: add option to disable broken wikilinks

* fix(style): update hover color for broken links and introduce new class

* feat: add "disableBrokenWikilinks" option to ObsidianFlavoredMarkdown
2025-05-27 21:26:17 +02:00
Jacky Zhao
c9349457ed css: adjust color blend for search bg 2025-05-27 10:00:19 -07:00
Aswanth
6d49d97559 fix(analytics): streamline posthog script loading and event capturing (#1974) 2025-05-24 00:55:07 -04:00
Emile Bangma
c9741d00f3 fix(a11y): increased content-meta text contrast (#1980) 2025-05-23 22:09:48 -04:00
Nizav
73983cfb0e feat(i18n): Bahasa Indonesia translations (#1981) 2025-05-23 22:07:29 -04:00
Emile Bangma
52344cd816 fix(style): Katex adding scrollbars on non-overflowing content (#1989) 2025-05-23 22:05:41 -04:00
Emile Bangma
fec0a62b74 fix(ofm): allow wikilink alias to be empty (#1984)
This is in line with Obsidian's behavior.
2025-05-19 07:58:05 +02:00
Felix Nie
e98d97a271 feat(i18n): readermode translations and icon (#1961)
* fix(readermode): Translations and a new icon for ReaderMode

* Formatted

* Replaced icon
2025-05-07 21:56:18 +02:00
dependabot[bot]
c33f96e572 chore(deps): bump sigstore/cosign-installer in the ci-dependencies group (#1953)
Bumps the ci-dependencies group with 1 update: [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer).


Updates `sigstore/cosign-installer` from 3.8.1 to 3.8.2
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/v3.8.1...v3.8.2)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-version: 3.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: ci-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-07 12:46:39 -07:00
dependabot[bot]
0b348a0532 chore(deps): bump the production-dependencies group with 7 updates (#1964)
Bumps the production-dependencies group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [@floating-ui/dom](https://github.com/floating-ui/floating-ui/tree/HEAD/packages/dom) | `1.6.13` | `1.7.0` |
| @myriaddreamin/rehype-typst | `0.5.4` | `0.6.0` |
| [pixi.js](https://github.com/pixijs/pixijs) | `8.9.1` | `8.9.2` |
| [pretty-bytes](https://github.com/sindresorhus/pretty-bytes) | `6.1.1` | `7.0.0` |
| [ws](https://github.com/websockets/ws) | `8.18.1` | `8.18.2` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `22.15.3` | `22.15.7` |
| [tsx](https://github.com/privatenumber/tsx) | `4.19.3` | `4.19.4` |


Updates `@floating-ui/dom` from 1.6.13 to 1.7.0
- [Release notes](https://github.com/floating-ui/floating-ui/releases)
- [Changelog](https://github.com/floating-ui/floating-ui/blob/master/packages/dom/CHANGELOG.md)
- [Commits](https://github.com/floating-ui/floating-ui/commits/@floating-ui/dom@1.7.0/packages/dom)

Updates `@myriaddreamin/rehype-typst` from 0.5.4 to 0.6.0

Updates `pixi.js` from 8.9.1 to 8.9.2
- [Release notes](https://github.com/pixijs/pixijs/releases)
- [Commits](https://github.com/pixijs/pixijs/compare/v8.9.1...v8.9.2)

Updates `pretty-bytes` from 6.1.1 to 7.0.0
- [Release notes](https://github.com/sindresorhus/pretty-bytes/releases)
- [Commits](https://github.com/sindresorhus/pretty-bytes/compare/v6.1.1...v7.0.0)

Updates `ws` from 8.18.1 to 8.18.2
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.18.1...8.18.2)

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

Updates `tsx` from 4.19.3 to 4.19.4
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.19.3...v4.19.4)

---
updated-dependencies:
- dependency-name: "@floating-ui/dom"
  dependency-version: 1.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@myriaddreamin/rehype-typst"
  dependency-version: 0.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: pixi.js
  dependency-version: 8.9.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: pretty-bytes
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: production-dependencies
- dependency-name: ws
  dependency-version: 8.18.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: "@types/node"
  dependency-version: 22.15.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: tsx
  dependency-version: 4.19.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-07 12:46:23 -07:00
Dan
59d4b4eddd fix(analytics): Umami tracking pageviews properly
Update componentResources.ts to fix umami SPA tracking (#1967)

Update componentResources.ts to fix umami analytics when SPA is enabled in Quartz
2025-05-07 21:44:30 +02:00
anthops
adf442036b fix(graph): provide proper workaround for pixijs webgpu issue #1899 (#1949) 2025-04-30 09:49:41 -07:00
badcode
dc2c4dca08 docs: add fix for 'remote end hung up unexpectedly' error during initial sync (#1939) 2025-04-29 09:55:15 -07:00
dralagen
6ba9c7c02a doc(favicon): add documentation of favicon plugin (#1948)
* doc(favicon): add documentation of favicon plugin

* doc(favicon): add missing link to configuration page

* fix(favicon): build on public folder don't created
2025-04-28 22:00:28 -07:00
Stephen Tse
8d5b13ee03 fix(fonts): Fixed page title fonts not downloadable to local (#1898)
* Fixed url parser regex not working for Google Fonts subset API

* Prettier no
2025-04-28 14:58:06 -07:00
dependabot[bot]
4d07ac93b4 chore(deps-dev): bump the production-dependencies group with 2 updates (#1952)
Bumps the production-dependencies group with 2 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [esbuild](https://github.com/evanw/esbuild).


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

Updates `esbuild` from 0.25.2 to 0.25.3
- [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.2...v0.25.3)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 22.15.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: esbuild
  dependency-version: 0.25.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-28 14:41:04 -07:00
Jacky Zhao
32d3fc0ce8 chore(ci): fix proj name 2025-04-28 14:35:51 -07:00
Jacky Zhao
00e860d8e6 ci: fix fork preview 2025-04-28 13:19:29 -07:00
Stephen Tse
2acdec323f fix(explorer): Prevent html from being scrollable when mobile explorer is open (#1895)
* Fixed html page being scrollable when mobile explorer is open

* Prettier code

* Addressed comment
2025-04-26 11:13:56 -07:00
dralagen
9e58857746 feat(favicon): add plugin to expose favicon from icon.png (#1942)
* feat(favicon): add plugin to expose favicon from icon.png

* chore(favicon): clean up formatting and remove unnecessary line breaks
2025-04-26 11:06:59 -07:00
Stephen Tse
4bd714b7be fix(callout): Grid-based callout collapsible animation (#1944)
* Fixed broken nested callout maxHeight calculation

* Implemented grid-based callout collapsible
2025-04-26 11:05:51 -07:00
Jacky Zhao
78e13bcb40 chore: add ci to preview all prs (#1947)
* add ci to preview all prs

* only on this repo

* fmt
2025-04-26 11:04:23 -07:00
anthops
7d49dff074 fix: prefer webgl for devices with webgpu and no float32-blendable feature flag #1899 (#1933)
Co-authored-by: Tony <32993852+0xREDACTED@users.noreply.github.com>
2025-04-23 09:32:52 -07:00
Jacky Zhao
cdebd05dc9 fix(wikilinks): dont default empty alias 2025-04-23 09:30:25 -07:00
Jacky Zhao
2a9290b3df fix(transclude): blockref detection 2025-04-22 11:18:50 -07:00
ARYAN TECHIE
771c05ff18 fix: dynamically detect current branch for quartz sync push (#1930) 2025-04-22 10:22:43 -07:00
Jacky Zhao
6dd772bf00 fix(popover): properly clear popover on racing fetch 2025-04-21 23:55:38 -07:00
dependabot[bot]
c238dd16d9 chore(deps): bump the production-dependencies group with 2 updates (#1919)
Bumps the production-dependencies group with 2 updates: [@clack/prompts](https://github.com/bombshell-dev/clack/tree/HEAD/packages/prompts) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@clack/prompts` from 0.10.0 to 0.10.1
- [Release notes](https://github.com/bombshell-dev/clack/releases)
- [Changelog](https://github.com/bombshell-dev/clack/blob/@clack/prompts@0.10.1/packages/prompts/CHANGELOG.md)
- [Commits](https://github.com/bombshell-dev/clack/commits/@clack/prompts@0.10.1/packages/prompts)

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

---
updated-dependencies:
- dependency-name: "@clack/prompts"
  dependency-version: 0.10.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: "@types/node"
  dependency-version: 22.14.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-17 19:48:08 -07:00
Jacky Zhao
b34d521293 feat: reader mode 2025-04-17 19:45:17 -07:00
Jacky Zhao
bfd72347cf fix(popover): clear popovers more aggressively, use href as id 2025-04-11 08:18:28 -07:00
Jacky Zhao
091cc1b05e fix(search): properly show mobile layout 2025-04-10 17:43:35 -07:00
Jacky Zhao
e9b60c7285 fix(popover): popover id calculation + scroll consistency 2025-04-10 16:46:30 -07:00
Jacky Zhao
b1a920e5c0 fix: add proper popover hint to tag content page 2025-04-10 16:28:36 -07:00
dependabot[bot]
61770d3e50 chore(deps): bump the production-dependencies group with 6 updates (#1913)
Bumps the production-dependencies group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [preact](https://github.com/preactjs/preact) | `10.26.4` | `10.26.5` |
| [remark-rehype](https://github.com/remarkjs/remark-rehype) | `11.1.1` | `11.1.2` |
| [sharp](https://github.com/lovell/sharp) | `0.33.5` | `0.34.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `22.13.14` | `22.14.0` |
| [@types/ws](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/ws) | `8.18.0` | `8.18.1` |
| [typescript](https://github.com/microsoft/TypeScript) | `5.8.2` | `5.8.3` |


Updates `preact` from 10.26.4 to 10.26.5
- [Release notes](https://github.com/preactjs/preact/releases)
- [Commits](https://github.com/preactjs/preact/compare/10.26.4...10.26.5)

Updates `remark-rehype` from 11.1.1 to 11.1.2
- [Release notes](https://github.com/remarkjs/remark-rehype/releases)
- [Commits](https://github.com/remarkjs/remark-rehype/compare/11.1.1...11.1.2)

Updates `sharp` from 0.33.5 to 0.34.1
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.33.5...v0.34.1)

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

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

Updates `typescript` from 5.8.2 to 5.8.3
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/commits)

---
updated-dependencies:
- dependency-name: preact
  dependency-version: 10.26.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: remark-rehype
  dependency-version: 11.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: sharp
  dependency-version: 0.34.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@types/node"
  dependency-version: 22.14.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@types/ws"
  dependency-version: 8.18.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: typescript
  dependency-version: 5.8.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-10 16:17:15 -07:00
Emile Bangma
9db66d500e fix(popover): round coords remove blurred popovers (#1911) 2025-04-07 00:23:49 +02:00
Jacky Zhao
ee8c1dc968 chore(css): style tweaks for overflow 2025-04-05 11:38:50 -07:00
1 gal Rosemary
bb24cd13c7 fix(css): styles issues with popover, overflow, and scroll overflow (#1907)
* fix(style): fix toc overflow & scrolling overflow

* fix(style): fix explorer scrolling overflow

* fix(style): fix backlinks overflow & scrolling overflow

* fix(style): resolve popover overflow issue causing incomplete display

* chore: rename function to enhance readability

* fix(popover): make the backlink's id unique & use translate() instead of translateY()
2025-04-05 10:39:28 -07:00
Emile Bangma
d61fb266c7 fix(popover): automatically position anchored links properly (#1897)
* fix(popover): automatically position heading links at heading

* Impement linking of blockreferences

* Popover fixes

* id mapping

* Remove excess regexes

* Updated blockref

* Remove linker element

* Restore the docs to their former glory

* Move the hash out of the loop

* Redundant

* Redundant

* Restore docs

* Remove log

* Let it const
2025-04-05 10:31:17 -07:00
K Gopal Krishna
685c06ce2e fix(RecentNotes): Prevent folder pages from always appearing first (closes #1901) (#1904)
* Fix(RecentNotes): Prevent folder pages from always appearing first

Pass prioritizeFolders=false to byDateAndAlphabetical in RecentNotes to sort strictly by date/alphabetical order, fixing issue #1901.

* refactor: split sorting functions for clarity

- Split byDateAndAlphabetical into two separate functions\n- byDateAndAlphabetical: sorts strictly by date and alphabetically\n- byDateAndAlphabeticalFolderFirst: sorts with folders first\n- Updated RecentNotes to use date-only sorting

* Fix(PageList): keep byDateAndAlphabeticalFolderFirst as the default sorting order for PageList
2025-04-04 10:36:29 -07:00
Jacky Zhao
3ae89a1d16 fix(search): make closest sidebar z-index adjustment optional (closes #1905) 2025-04-04 10:17:57 -07:00
Jacky Zhao
4d6e7ccba9 chore(docs): fix explorer docs on filtering by title 2025-04-04 09:50:01 -07:00
Emile Bangma
f334e78ed6 fix(style): MathJax in callouts spacing (#1892) 2025-03-31 23:14:30 +02:00
dependabot[bot]
c5304b35c0 chore(deps): bump the production-dependencies group with 5 updates (#1894)
Bumps the production-dependencies group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [pixi.js](https://github.com/pixijs/pixijs) | `8.9.0` | `8.9.1` |
| [rehype-citation](https://github.com/timlrx/rehype-citation) | `2.2.2` | `2.3.1` |
| [satori](https://github.com/vercel/satori) | `0.12.1` | `0.12.2` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `22.13.13` | `22.13.14` |
| [esbuild](https://github.com/evanw/esbuild) | `0.25.1` | `0.25.2` |


Updates `pixi.js` from 8.9.0 to 8.9.1
- [Release notes](https://github.com/pixijs/pixijs/releases)
- [Commits](https://github.com/pixijs/pixijs/compare/v8.9.0...v8.9.1)

Updates `rehype-citation` from 2.2.2 to 2.3.1
- [Release notes](https://github.com/timlrx/rehype-citation/releases)
- [Commits](https://github.com/timlrx/rehype-citation/compare/v2.2.2...v2.3.1)

Updates `satori` from 0.12.1 to 0.12.2
- [Release notes](https://github.com/vercel/satori/releases)
- [Commits](https://github.com/vercel/satori/compare/0.12.1...0.12.2)

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

Updates `esbuild` from 0.25.1 to 0.25.2
- [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.1...v0.25.2)

---
updated-dependencies:
- dependency-name: pixi.js
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: rehype-citation
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: satori
  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: esbuild
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-31 14:08:30 -07:00
dependabot[bot]
99f353968e chore(deps-dev): bump @types/node in the production-dependencies group (#1869)
Bumps the production-dependencies group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


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

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-30 19:12:18 -07:00
Stephen Tse
ec4700d522 Hey folder icon don't shrink please (#1872) 2025-03-30 19:08:20 -07:00
Jacky Zhao
d6f69e830c fix: remove redundant log and display in parent of overflow 2025-03-30 18:15:25 -07:00
Jacky Zhao
9ee6fe15fd fix: reproducible overflowlist (closes #1885) 2025-03-30 18:04:48 -07:00
Stephen Tse
a21f588c48 fix(toc): element can't fully collapse 2025-03-30 17:39:08 -07:00
Stephen Tse
2119025513 fix(toc): Fixed headers in second ToC element not highlight-able 2025-03-30 17:35:20 -07:00
Jacky Zhao
f70e562432 fix: overflow list bottom gradient on toc (closes #1888) 2025-03-30 17:30:01 -07:00
Emile Bangma
9ff6c7a3f5 fix(style): MathJax non-inline formulae center (#1886) 2025-03-30 21:19:53 +02:00
Jacky Zhao
7ca9dd9a70 fix: dont use cdn for twemoji, bake emojis as b64 2025-03-28 16:29:33 -07:00
Stephen Tse
b397dae951 Updating breadcrumbs docs on ConditionalRender (#1871) 2025-03-26 08:32:13 -07:00
Jacky Zhao
23b691f38c fix: coerce fullslug 2025-03-23 20:43:01 -07:00
Jacky Zhao
c18e6cd5bb fix(alias): resolve relative if alias is relative 2025-03-23 20:38:06 -07:00
Jacky Zhao
fe2e16d937 fix: disallow user-select in popover 2025-03-23 18:08:07 -07:00
Jacky Zhao
722b4321db docs: clarify transclusions 2025-03-23 18:03:52 -07:00
dependabot[bot]
9d8d238912 chore(deps): bump the production-dependencies group across 1 directory with 4 updates (#1867)
Bumps the production-dependencies group with 4 updates in the / directory: [lightningcss](https://github.com/parcel-bundler/lightningcss), [pixi.js](https://github.com/pixijs/pixijs), [rehype-pretty-code](https://github.com/rehype-pretty/rehype-pretty-code/tree/HEAD/packages/core) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


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

Updates `pixi.js` from 8.8.1 to 8.9.0
- [Release notes](https://github.com/pixijs/pixijs/releases)
- [Commits](https://github.com/pixijs/pixijs/compare/v8.8.1...v8.9.0)

Updates `rehype-pretty-code` from 0.14.0 to 0.14.1
- [Release notes](https://github.com/rehype-pretty/rehype-pretty-code/releases)
- [Changelog](https://github.com/rehype-pretty/rehype-pretty-code/blob/master/packages/core/CHANGELOG.md)
- [Commits](https://github.com/rehype-pretty/rehype-pretty-code/commits/rehype-pretty-code@0.14.1/packages/core)

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

---
updated-dependencies:
- dependency-name: lightningcss
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: pixi.js
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: rehype-pretty-code
  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
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-23 17:57:45 -07:00
Jacky Zhao
141f053b0d chore: format path.test.ts 2025-03-23 17:43:47 -07:00
Jacky Zhao
3027eced6c chore(test): add tests for resolveRelative 2025-03-23 17:42:23 -07:00
Jacky Zhao
aaa5c8e8e4 feat: conditional render component 2025-03-23 17:34:14 -07:00
Jacky Zhao
4e74d11b1a fix: cleanup a href link construction, global shared trie, breadcrumbs use trie 2025-03-23 17:24:43 -07:00
Emile Bangma
457b77dd48 fix(frontmatter): prevent slug duplication through frontmatter (#1860)
* fix(frontmatter): prevent slug duplication through frontmatter

* Simplify duplicate slug checks

* Update quartz/plugins/transformers/frontmatter.ts

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* lint

---------

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
2025-03-22 03:59:43 +01:00
Karim
3ce6aa49bf fix(ogImage): update socialImage path to include base URL if defined (#1858)
* fix(ogImage): update socialImage path to include base URL if defined

* feat(path): add function to check if a file path is absolute

* fix(ogImage): handle absolute paths for user defined og image paths

* docs(CustomOgImages): update socialImage property to accept full URLs

* fix(ogImage): typo

* fix(ogImage): improve user-defined OG image path handling

* Update docs/plugins/CustomOgImages.md

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* Update quartz/plugins/emitters/ogImage.tsx

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* refactor(path): remove isAbsoluteFilePath function

* fix(ogImage): update user-defined OG image path handling to support relative URLs

* feat(ogImage): enhance user-defined OG image path handling with absolute URL support

* refactor(ogImage): remove debug log for ogImagePath

* feat(path): add isAbsoluteURL function and corresponding tests

* refactor(path): remove unused URL import for isomorphic compatibility

---------

Co-authored-by: Karim H <karimh96@hotmail.com>
Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
2025-03-21 16:49:56 -07:00
Taha
9316ddf2f5 fix(analytics): fix the load of the analytics scripts (#1865)
* fix(analytics): fix the load of the analytics scripts

* chore(lint): fix lint issues
2025-03-21 09:58:10 -07:00
Shane McDonald
fbca56f278 fix(lastmod) Change defaultDateType to "modified" (#1862)
Making this change as per https://github.com/jackyzha0/quartz/issues/1857#issuecomment-2737098252

This is necessary for the `git` source to work properly in the CreatedModifiedDate plugin.
2025-03-19 12:47:16 -07:00
Jacky Zhao
eccad3da5d fix(lastmod): fallback to ctx.arg.directory instead of empty string 2025-03-18 21:48:24 -07:00
dralagen
bcde2abcb2 fix(transformer): find last modified date form commit on submodule (#1831)
* fix(transformer): find last modified date form commit on submodule

when the content folder has a submodule git, the relative path start in content folder and not root folder of quartz

* fix(transformer): use path.relative for improved path handling in last modified date calculation

* fix(transformer): keep find file from relative path of repo workdir

* fix(transformer): use variable for repository workdir

use default value if repo.workdir is undefined to user fullFp value
2025-03-18 21:47:35 -07:00
Felix Nie
25979ab216 feat(fonts): allow PageTitle to have its own font subset (#1848)
* fix(explorer): vertically center the Explorer toggle under mobile view

* Added a separate title font configuration

* Added googleSubFontHref function

* Applied --titleFont to PageTitle

* Made googleFontHref return array of URLs

* Dealing with empty and undefined title

* Minor update

* Dealing with empty and undefined title

* Refined font inclusion logic

* Adopted the googleFontHref + googleFontSubsetHref method

* Adaptively include font subset for PageTitle

* Restored default config

* Minor changes on configuration docs

* Formatted source code
2025-03-18 21:43:32 -07:00
Jacky Zhao
9818e1ad57 chore: remove unused import 2025-03-18 09:00:15 -07:00
Jacky Zhao
771110a72a fix(git): deprioritize git, dont fail on non-git content folders 2025-03-18 08:56:06 -07:00
dependabot[bot]
dc6a9f3b12 chore(deps): bump rlespinasse/github-slug-action (#1851)
Bumps the ci-dependencies group with 1 update: [rlespinasse/github-slug-action](https://github.com/rlespinasse/github-slug-action).


Updates `rlespinasse/github-slug-action` from 5.0.0 to 5.1.0
- [Release notes](https://github.com/rlespinasse/github-slug-action/releases)
- [Commits](https://github.com/rlespinasse/github-slug-action/compare/v5.0.0...v5.1.0)

---
updated-dependencies:
- dependency-name: rlespinasse/github-slug-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: ci-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-17 14:49:27 -07:00
Yes365
c0b73ddaa4 fix: maybeDates will change children dates (#1843) 2025-03-17 08:27:15 -07:00
Jacky Zhao
e86544064c fix: parse parallelization chunk arg, inline b64 for og image 2025-03-16 15:12:40 -07:00
Jacky Zhao
a737207981 perf: incremental rebuild (--fastRebuild v2 but default) (#1841)
* checkpoint

* incremental all the things

* properly splice changes array

* smol doc update

* update docs

* make fancy logger dumb in ci
2025-03-16 14:17:31 -07:00
Felix Nie
a72b1a4224 fix(explorer): vertically center the Explorer toggle under mobile view (#1847) 2025-03-16 12:08:45 -07:00
Jacky Zhao
fbb4523853 fix(folder): use memoized trie instead of handrolled path solution (closes #1767) 2025-03-14 15:08:23 -07:00
Jacky Zhao
da1b6b37fe fix(explorer): fix incorrect recursive case for folder rendering 2025-03-14 10:05:26 -07:00
Jacky Zhao
e26658f4ed fix(description): calculate description properly when first sentence is longer than max 2025-03-13 14:22:11 -07:00
Jacky Zhao
b579950ae5 fix(og): adjust line clamp 2025-03-13 14:05:26 -07:00
Jacky Zhao
0babbdf780 fix(contentPage): dont emit folders 2025-03-13 13:57:00 -07:00
Jacky Zhao
1efe2e20a4 fix(graph): avoid keeping stroke in gfx 2025-03-13 12:29:30 -07:00
Jacky Zhao
5928d82a56 fix(og): search for font family properly 2025-03-13 12:11:27 -07:00
Jacky Zhao
696403d3fa chore: bump version to 4.4.1 2025-03-13 10:55:37 -07:00
Jacky Zhao
2c30abe457 fix(analytics): always use defer over async to account for document nav event 2025-03-13 10:48:03 -07:00
Jacky Zhao
80c3196fee feat(og): add reading time to default, improve logging 2025-03-13 10:41:50 -07:00
Jacky Zhao
d9159e0ac9 feat: make og images an emitter to properly await image generation (#1826)
* checkpoint

* make emitters async generators

* fix

* custom font spec

* replace spinner, use disk cache for fonts

* use readline instead

* make og images look nice
2025-03-13 10:27:46 -07:00
Jacky Zhao
c005fe4408 fix(explorer): properly respect folderDefaultState (closes #1827) 2025-03-12 22:31:44 -07:00
Emile Bangma
580c1bd608 fix(typography): properly pass Google font options (#1825) 2025-03-12 11:27:41 -07:00
Jacky Zhao
270a5dc14a fix(explorer): show file name instead of slug if no file data (closes #1822) 2025-03-12 11:24:28 -07:00
Jacky Zhao
bfa938cc62 fix(explorer): allow setting displayName (closes #1824) 2025-03-12 10:42:07 -07:00
Jacky Zhao
e3c50caf13 fix(explorer): dont invert mobile css, properly toggle .collapsed 2025-03-12 10:15:54 -07:00
Emile Bangma
ca08ec1ae7 fix(explorer): mobile explorer toggle (#1823) 2025-03-12 14:15:16 +01:00
Jacky Zhao
2718ab9019 feat: flex component, document higher-order layout components 2025-03-11 14:56:43 -07:00
Jacky Zhao
87b803790c fix(mermaid): themechange detector + expand simplification 2025-03-11 11:45:45 -07:00
Jacky Zhao
e59181c3aa docs(explorer): update to account for new client side explorer) 2025-03-11 10:20:15 -07:00
Jacky Zhao
b00198b888 fix: load mermaid as normal now that inline is safely below bundle size 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 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 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 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)
* 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)
* fix docker

* test with docs folder
2025-03-06 10:01:25 -08:00
Jacky Zhao
cc9704becc chore(deps): bump deps, silence internal punycode deprecation 2025-03-06 09:41:26 -08:00
Jacky Zhao
6d195fd40a feat: font specification flexibility 2025-03-06 09:21:50 -08:00
Jacky Zhao
2213424195 docs: make role of getQuartzComponents more clear and also make it optional 2025-03-05 18:34:02 -08:00
Jacky Zhao
5b13ff2199 feat: support emitters defining external resources, emit link from contentindex directly 2025-03-05 18:16:24 -08:00
Aaron Pham
5a39719898 fix(graph): set container as renderGroup to avoid redrawing multiple times (#1736)
Signed-off-by: Aaron Pham <contact@aarnphm.xyz>
2025-03-05 17:33:16 -08:00
Jacky Zhao
3c8ccde624 chore(og-image): force twemoji for emoji util 2025-03-05 17:21:19 -08:00
Stephen Tse
c97fd7089a Added emoji support to Satori when generating OG images (#1593) 2025-03-05 17:14:06 -08:00
Jacky Zhao
2acfa0fa23 fix(og-image): overflow ellipses in title and description 2025-03-05 17:13:19 -08:00
Jacky Zhao
f6f417a505 fix: engine reqiurements 2025-03-05 16:49:43 -08:00
dependabot[bot]
a3b6201365 chore(deps): bump the production-dependencies group with 6 updates (#1804)
* chore(deps): bump the production-dependencies group with 6 updates

Bumps the production-dependencies group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [hast-util-to-jsx-runtime](https://github.com/syntax-tree/hast-util-to-jsx-runtime) | `2.3.4` | `2.3.5` |
| [pixi.js](https://github.com/pixijs/pixijs) | `8.8.0` | `8.8.1` |
| [preact](https://github.com/preactjs/preact) | `10.26.2` | `10.26.4` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `22.13.5` | `22.13.9` |
| [prettier](https://github.com/prettier/prettier) | `3.5.2` | `3.5.3` |
| [typescript](https://github.com/microsoft/TypeScript) | `5.7.3` | `5.8.2` |


Updates `hast-util-to-jsx-runtime` from 2.3.4 to 2.3.5
- [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.4...2.3.5)

Updates `pixi.js` from 8.8.0 to 8.8.1
- [Release notes](https://github.com/pixijs/pixijs/releases)
- [Commits](https://github.com/pixijs/pixijs/compare/v8.8.0...v8.8.1)

Updates `preact` from 10.26.2 to 10.26.4
- [Release notes](https://github.com/preactjs/preact/releases)
- [Commits](https://github.com/preactjs/preact/compare/10.26.2...10.26.4)

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

Updates `prettier` from 3.5.2 to 3.5.3
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.5.2...3.5.3)

Updates `typescript` from 5.7.3 to 5.8.2
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.7.3...v5.8.2)

---
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: pixi.js
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: preact
  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: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>

* type fixes

* fix more types

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
2025-03-05 16:45:02 -08:00
Jacky Zhao
a1162b978a fix(analytics): spa tracking for other providers 2025-03-05 16:27:08 -08:00
Emile Bangma
c6f10b44f6 feat(rss): configurable RSS feed URL (#1806)
* feat(rss): configurable RSS feed URL

* Update docs/features/RSS Feed.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update docs/features/RSS Feed.md

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
2025-03-05 15:54:11 -08:00
sou7
b050162f82 docs(frontmatter): mention dealiasing related fields (#1715)
* Added to the documentation which values of frontmatter are referenced.

* The source code I was looking at seemed to be out of date and the wrong listings were corrected.

* The list of frontmatter was moved to Frontmatter.md and a link was added.
2025-02-27 16:37:01 -05:00
Anton Bulakh
6d350500f1 fix(goatcounter): properly count SPA page hits (#1718)
On the surface it seems that only google and plausible scripts handle
the SPA correctly - but I don't know if maybe others handle
window.history API themselves somehow or something like that.

However, I am trying out goatcounter and in it's docs I see that it
does no special SPA handling, so this has to be fixed.

Just doing the dynamic script thing on every nav seems to do the trick.
The script is not "spa-preserve" so they wouldn't accumulate - and when
I tried the "spa-preserve" + call goatcounter api route it didn't quite
work, they actually did accumulate
2025-02-27 16:35:19 -05:00
Jon Erling Hustadnes
9b47279fe6 feat(i18n): added nb-NO (Norwegian) (#1795) 2025-02-27 16:33:15 -05:00
dependabot[bot]
4901472aa8 chore(deps): bump the production-dependencies group with 8 updates (#1788)
* chore(deps): bump the production-dependencies group with 8 updates

Bumps the production-dependencies group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [hast-util-to-html](https://github.com/syntax-tree/hast-util-to-html) | `9.0.4` | `9.0.5` |
| [hast-util-to-jsx-runtime](https://github.com/syntax-tree/hast-util-to-jsx-runtime) | `2.3.2` | `2.3.4` |
| [preact](https://github.com/preactjs/preact) | `10.26.0` | `10.26.2` |
| [rehype-mathjax](https://github.com/remarkjs/remark-math) | `7.0.0` | `7.1.0` |
| [ws](https://github.com/websockets/ws) | `8.18.0` | `8.18.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `22.13.4` | `22.13.5` |
| [prettier](https://github.com/prettier/prettier) | `3.5.1` | `3.5.2` |
| [tsx](https://github.com/privatenumber/tsx) | `4.19.2` | `4.19.3` |


Updates `hast-util-to-html` from 9.0.4 to 9.0.5
- [Release notes](https://github.com/syntax-tree/hast-util-to-html/releases)
- [Commits](https://github.com/syntax-tree/hast-util-to-html/compare/9.0.4...9.0.5)

Updates `hast-util-to-jsx-runtime` from 2.3.2 to 2.3.4
- [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.2...2.3.4)

Updates `preact` from 10.26.0 to 10.26.2
- [Release notes](https://github.com/preactjs/preact/releases)
- [Commits](https://github.com/preactjs/preact/compare/10.26.0...10.26.2)

Updates `rehype-mathjax` from 7.0.0 to 7.1.0
- [Release notes](https://github.com/remarkjs/remark-math/releases)
- [Commits](https://github.com/remarkjs/remark-math/compare/rehype-mathjax@7.0.0...rehype-mathjax@7.1.0)

Updates `ws` from 8.18.0 to 8.18.1
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.18.0...8.18.1)

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

Updates `prettier` from 3.5.1 to 3.5.2
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.5.1...3.5.2)

Updates `tsx` from 4.19.2 to 4.19.3
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.19.2...v4.19.3)

---
updated-dependencies:
- dependency-name: hast-util-to-html
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: hast-util-to-jsx-runtime
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: preact
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: rehype-mathjax
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: ws
  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: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: tsx
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: update type error

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Aaron Pham <contact@aarnphm.xyz>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Aaron Pham <contact@aarnphm.xyz>
2025-02-25 16:52:52 -05:00
dependabot[bot]
28fe9b2095 chore(deps): bump sigstore/cosign-installer in the ci-dependencies group (#1789)
Bumps the ci-dependencies group with 1 update: [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer).


Updates `sigstore/cosign-installer` from 3.8.0 to 3.8.1
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/v3.8.0...v3.8.1)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: ci-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-25 16:35:10 -05:00
Jonathan Fung
9a0d919a3b docs: add Citation plugin & feature pages (#1772)
* docs: add Citation plugin & feature pages

Discussed and extracted from #1557

* Update docs/features/Citations.md

* Apply suggestions from code review

---------

Co-authored-by: Aaron Pham <Aaronpham0103@gmail.com>
2025-02-25 16:34:37 -05:00
Daksh
598741a571 fix(fonts): update system-ui trick for improve cross-platform compatibility (#1778) 2025-02-25 16:31:41 -05:00
Anton Bulakh
29c533a265 fix(aliases): wikilink resolution for aliases (#1681)
With markdownLinkResolution: "shortest", aka "+/- how Obsidian does it"
and given pages A and nested/B which has an alias Z, if you try to link
from A using [[Z]] it wouldn't work and get 404.

This is caused by alias slugs (nested/Z in this case, emitted by
AliasRedirects) not being present in the `allSlugs` list which is used
by the link transformer.

The fix is to compute the alias slugs in the frontmatter transformer
and add them to `allSlugs` there.
Also we store them in file data to avoid recomputing them when emitting
alias redirect pages.

Fixes #904

Note: given how currently the markdown/html transformers are ordered
this doesn't really work.

Given pages A and nested/B which has an alias Z, here's the order which
currently happens:

md-transformers(A) => html-transformers(A) =>
md-transformers(B) => html-transformers(B)

Since the nested/Z slug will get added when md-transformers(B) are run,
but the slugs are used by html-transformers(A) when resolving it's
links - the link [[Z]] in A will still 404

A fix for this is to split the parser into two stages - first apply the
md-transformers to all files, and only then apply html-transformers to
all files.

I did just that in a different commit, which is needed for this one to
work correctly.
2025-02-25 16:28:16 -05:00
Suamppa
1a02642469 feat(i18n): add Finnish translations for i18n (#1782) 2025-02-25 16:27:32 -05:00
Emile Bangma
5794405974 feat(cli): absolute path support for -d flag (#1756) 2025-02-25 16:27:10 -05:00
Emile Bangma
59d5a0ed3a fix(graph): mobile global graph overlay (#1790) 2025-02-25 13:35:24 +01:00
dependabot[bot]
dd6bd498db chore(deps): bump the production-dependencies group with 5 updates (#1775)
Bumps the production-dependencies group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [pixi.js](https://github.com/pixijs/pixijs) | `8.7.3` | `8.8.0` |
| [preact](https://github.com/preactjs/preact) | `10.25.4` | `10.26.0` |
| [rehype-mathjax](https://github.com/remarkjs/remark-math) | `6.0.0` | `7.0.0` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `22.13.1` | `22.13.4` |
| [prettier](https://github.com/prettier/prettier) | `3.5.0` | `3.5.1` |


Updates `pixi.js` from 8.7.3 to 8.8.0
- [Release notes](https://github.com/pixijs/pixijs/releases)
- [Commits](https://github.com/pixijs/pixijs/compare/v8.7.3...v8.8.0)

Updates `preact` from 10.25.4 to 10.26.0
- [Release notes](https://github.com/preactjs/preact/releases)
- [Commits](https://github.com/preactjs/preact/compare/10.25.4...10.26.0)

Updates `rehype-mathjax` from 6.0.0 to 7.0.0
- [Release notes](https://github.com/remarkjs/remark-math/releases)
- [Commits](https://github.com/remarkjs/remark-math/compare/rehype-mathjax@6.0.0...rehype-mathjax@7.0.0)

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

Updates `prettier` from 3.5.0 to 3.5.1
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.5.0...3.5.1)

---
updated-dependencies:
- dependency-name: pixi.js
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: preact
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: rehype-mathjax
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: production-dependencies
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-17 21:54:07 -05:00
dependabot[bot]
04423d4931 chore(deps): bump the production-dependencies group with 6 updates (#1761)
Bumps the production-dependencies group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@clack/prompts](https://github.com/natemoo-re/clack/tree/HEAD/packages/prompts) | `0.9.1` | `0.10.0` |
| [globby](https://github.com/sindresorhus/globby) | `14.0.2` | `14.1.0` |
| [remark-gfm](https://github.com/remarkjs/remark-gfm) | `4.0.0` | `4.0.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `22.13.0` | `22.13.1` |
| [esbuild](https://github.com/evanw/esbuild) | `0.24.2` | `0.25.0` |
| [prettier](https://github.com/prettier/prettier) | `3.4.2` | `3.5.0` |


Updates `@clack/prompts` from 0.9.1 to 0.10.0
- [Release notes](https://github.com/natemoo-re/clack/releases)
- [Changelog](https://github.com/bombshell-dev/clack/blob/main/packages/prompts/CHANGELOG.md)
- [Commits](https://github.com/natemoo-re/clack/commits/@clack/prompts@0.10.0/packages/prompts)

Updates `globby` from 14.0.2 to 14.1.0
- [Release notes](https://github.com/sindresorhus/globby/releases)
- [Commits](https://github.com/sindresorhus/globby/compare/v14.0.2...v14.1.0)

Updates `remark-gfm` from 4.0.0 to 4.0.1
- [Release notes](https://github.com/remarkjs/remark-gfm/releases)
- [Commits](https://github.com/remarkjs/remark-gfm/compare/4.0.0...4.0.1)

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

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

Updates `prettier` from 3.4.2 to 3.5.0
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.4.2...3.5.0)

---
updated-dependencies:
- dependency-name: "@clack/prompts"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: globby
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: remark-gfm
  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: esbuild
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-11 01:28:02 -05:00
dependabot[bot]
be846e6a5f chore(deps): bump sigstore/cosign-installer in the ci-dependencies group (#1762)
Bumps the ci-dependencies group with 1 update: [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer).


Updates `sigstore/cosign-installer` from 3.7.0 to 3.8.0
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/v3.7.0...v3.8.0)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: ci-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-11 01:27:43 -05:00
dependabot[bot]
32ec711767 chore(deps-dev): bump @types/node in the production-dependencies group (#1751)
Bumps the production-dependencies group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


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

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-05 05:07:14 -05:00
Emile Bangma
91189dfd2f feat(explorer): collapsible mobile explorer (#1471)
Co-authored-by: Aaron Pham <Aaronpham0103@gmail.com>
2025-02-03 09:25:42 -05:00
Aaron Pham
fbc45548f7 feat(graph): enable radial mode (#1738) 2025-02-01 16:22:29 -05:00
dependabot[bot]
7be47742a6 chore(deps): bump the production-dependencies group across 1 directory with 3 updates (#1744)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-31 06:46:45 -05:00
Anton Bulakh
4e4930ef9c chore(styles): omit sass deprecation warnings (#1737)
update to newer API
2025-01-23 22:19:46 -05:00
Mantvydas Deltuva
01943ff5a0 feat(i18n): Lithuanian (#1733) 2025-01-21 00:20:02 -05:00
Patsagorn Y.
8cf3e3001f feat(i18n): Thai translations (#1722) 2025-01-16 15:44:33 -05:00
dependabot[bot]
992ac00f7c chore(deps): bump the production-dependencies group with 7 updates (#1719)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-14 00:02:49 -05:00
dependabot[bot]
09f8670db7 chore(deps): bump the production-dependencies group with 4 updates (#1711)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Aaron Pham <contact@aarnphm.xyz>
2025-01-07 20:32:02 -05:00
Aaron Pham
7e828252bb fix(path): handle lone slash (#1713) 2025-01-07 16:00:09 -05:00
Anton Bulakh
c90dbacab0 chore(build): separate markdown and html handling into two separate stages (#1675) 2025-01-07 15:33:34 -05:00
Anton Bulakh
b7a945e034 fix(tags): Dont consume a space before content tags (#1706) 2025-01-05 01:11:15 -05:00
Akihiro Saiki
dc3323b574 chore: use Google Fonts API v2 to get fonts for ogp (#1705) 2025-01-04 09:45:53 -08:00
Aaron Pham
c98ef7e89f perf(img): lazy load chunks (#1700) 2025-01-03 11:27:43 -08:00
Aaron Pham
2e6a675edd perf: reduce bundle size (closes #1607) (#1689) 2025-01-01 12:24:32 -08:00
Jacky Zhao
e3162f7a7e chore: joinSegments fix + tests 2025-01-01 12:11:16 -08:00
moexiami
9466c145b1 fix(path): correct handle URI schemes in joinSegments (#1693) 2025-01-01 10:07:59 -08:00
Anton Bulakh
a934397961 fix(spa): Fix relative alias redirects (#1688) 2024-12-30 18:21:07 -08:00
dependabot[bot]
b2752e7262 chore(deps): bump the production-dependencies group with 2 updates (#1696)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-30 18:19:22 -08:00
Anton Bulakh
d9e36e60d1 fix(search): Fix super-inconsistent preview widths in search previews (#1677)
Don't know if this was uncovered by my local setup or just nobody
noticed, but page previews were pretty wonky for me
2024-12-30 09:55:53 -08:00
Anton Bulakh
46adb35966 fix(spa): Normalize empty hrefs (#1695)
A final breadcrumb has an empty href, linking to the current page, but
the relative url normalization method missed those, making the link
appear broken in search previews and popovers.

Fixes #1690
2024-12-30 09:55:18 -08:00
Jacky Zhao
e1c9eabef4 fix(css): oops fit-content not no width specification for codeblock title 2024-12-30 09:28:02 -08:00
Anton Bulakh
d88e43010a fix(tags): Fix TagContent not sorting the page list when opening a tag (#1692) 2024-12-30 09:08:35 -08:00
Ammar Alakkad
ef72f1bf70 Fix ObsidianFlavoredMarkdown source link (#1694) 2024-12-30 08:03:57 -08:00
Jacky Zhao
7d4bed64a9 fix(css): allow proper width on code block title (closes #1683) 2024-12-28 15:17:58 -08:00
Anton Bulakh
99011cb1b0 fix(spa): handle HTML redirects for aliases (#1680) 2024-12-27 07:18:22 -07:00
Jacky Zhao
c91cf97f99 feat(spa): add loading bar (#1684) 2024-12-26 22:18:17 -07:00
Anton Bulakh
05e6f05a50 feat(backlinks): hide by default when empty (#1674)
Co-authored-by: Aaron Pham <Aaronpham0103@gmail.com>
2024-12-26 21:05:35 -07:00
Anton Bulakh
7533d504dc fix(tags): Set consistent titles for tags with description pages (#1671) 2024-12-26 17:50:06 -07:00
Anton Bulakh
69a0ddf733 fix(tags): Show tag descriptions when hovering (#1670) 2024-12-26 06:50:54 -07:00
dependabot[bot]
a582505daf chore(deps): bump the production-dependencies group with 7 updates (#1667)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-23 16:35:08 -05:00
Aaron Pham
965f9e123c chore(frontmatter): dealias created/modified/updated time (#1664) 2024-12-23 15:00:26 -05:00
Emile Bangma
ff9e60a7fc fix(search): restore mobile search view (#1576) 2024-12-17 13:07:00 -05:00
dependabot[bot]
367bb0e6ce chore(deps): bump the production-dependencies group across 1 directory with 14 updates (#1655)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Aaron Pham <contact@aarnphm.xyz>
2024-12-17 03:53:51 -05:00
Emile Bangma
8141cb1587 fix(cssclasses): apply frontmatter classes uniformly between page types (#1645) 2024-12-11 00:43:27 +01:00
Sohum
5ccc2dcbba fix(head): update open-graph width and height protocol per ogp (#1512)
Co-authored-by: Aaron Pham <contact@aarnphm.xyz>
2024-12-03 04:52:51 -05:00
hill
11c98f5600 feat(i18n): add zh-TW (#1621) 2024-12-03 04:52:36 -05:00
Francisco Gama T. R.
5a5ef670fe fix(darkmode): check for null (#1531) 2024-12-03 01:56:59 -05:00
catcodeme
285c0e9768 docs(showcase): 8cat.life (#1617) 2024-12-03 01:50:50 -05:00
Aaron Bull Schaefer
adb326c933 fix(style): ensure images have no background color when linked (#1623) 2024-12-03 01:43:22 -05:00
Bao
0c4281eb53 fix: use time HTML element for date strings (#1622) 2024-12-03 01:41:55 -05:00
Emile Bangma
7ac94e1d84 fix(comments): properly check if comments is in frontmatter (#1627) 2024-11-30 21:03:20 -05:00
plyght
e1d754ef79 Update showcase.md (#1611) 2024-11-22 11:44:40 -08:00
Jules Kreuer
a727b57e46 docs(hosting): add apache config example (#1608) 2024-11-20 19:29:43 -05:00
dependabot[bot]
1a4cfa606f chore(deps): bump the production-dependencies group with 3 updates (#1600)
Bumps the production-dependencies group with 3 updates: [@clack/prompts](https://github.com/natemoo-re/clack/tree/HEAD/packages/prompts), [satori](https://github.com/vercel/satori) and [shiki](https://github.com/shikijs/shiki/tree/HEAD/packages/shiki).


Updates `@clack/prompts` from 0.7.0 to 0.8.1
- [Release notes](https://github.com/natemoo-re/clack/releases)
- [Changelog](https://github.com/bombshell-dev/clack/blob/main/packages/prompts/CHANGELOG.md)
- [Commits](https://github.com/natemoo-re/clack/commits/@clack/prompts@0.8.1/packages/prompts)

Updates `satori` from 0.10.14 to 0.11.3
- [Release notes](https://github.com/vercel/satori/releases)
- [Commits](https://github.com/vercel/satori/compare/0.10.14...0.11.3)

Updates `shiki` from 1.22.2 to 1.23.1
- [Release notes](https://github.com/shikijs/shiki/releases)
- [Changelog](https://github.com/shikijs/shiki/blob/main/CHANGELOG.md)
- [Commits](https://github.com/shikijs/shiki/commits/v1.23.1/packages/shiki)

---
updated-dependencies:
- dependency-name: "@clack/prompts"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: satori
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: shiki
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-19 00:36:08 -05:00
Jacky Zhao
410001b942 docs: add the pond (#1598) 2024-11-18 11:39:44 -08:00
Aaron Pham
f24a147276 fix(comment): negate check (closes #1582) (#1586) 2024-11-14 06:28:06 -05:00
Ben Schlegel
137d55eb1b feat(open-graph): generate OG images + further OG support (#740)
* Quartz sync: Aug 29, 2023, 10:17 PM

* feat: add basic satori og image generation

* Squashed commit of the following:

commit fa69c2a565
Author: Ben Schlegel <31989404+benschlegel@users.noreply.github.com>
Date:   Thu Sep 21 19:35:11 2023 +0200

    fix(explorer): increase consistency, explicitly use font-family (#496)

    * fix(explorer): display name for folders without `index` file

    * docs(explorer): add section for folder display names

    * docs(explorer): fix broken wikilink

    * fix(consistency): explicitly set font + label/link fix

    Use consistent styling between folders with `folderClickBehavior: "link"` and `"collapse`

    * Update quartz/components/styles/explorer.scss

    Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

    * Update quartz/components/styles/explorer.scss

    Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

    ---------

    Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

commit 8eb1554b13
Author: Ben Schlegel <31989404+benschlegel@users.noreply.github.com>
Date:   Thu Sep 21 18:54:33 2023 +0200

    fix(explorer): display names for folders without frontmatter (#494)

    * fix(explorer): display name for folders without `index` file

    * docs(explorer): add section for folder display names

commit dcdeae4e7b
Author: Ben Schlegel <31989404+benschlegel@users.noreply.github.com>
Date:   Thu Sep 21 18:53:19 2023 +0200

    docs(explorer): update default config + new example (#493)

commit 48452231d5
Author: Jacky Zhao <j.zhao2k19@gmail.com>
Date:   Wed Sep 20 16:09:18 2023 -0700

    perf: memoize filetree computation (#490)

    * perf: memoize filetree computation

    * format

    * var -> let

commit 16d33fb771
Author: Jacky Zhao <j.zhao2k19@gmail.com>
Date:   Wed Sep 20 16:08:54 2023 -0700

    feat: display name for folders, expand explorer a little bit (#489)

    * feat: display name for folders, expand explorer a little bit

    * update docs

commit b029eeadab
Author: Ben Schlegel <31989404+benschlegel@users.noreply.github.com>
Date:   Wed Sep 20 22:55:29 2023 +0200

    feat(explorer): improve accessibility and consistency (+ bug fix) (#488)

    * feat(consistency): use `all: unset` on button

    * style: improve accessibility and consistency for explorer

    * fix: localStorage bug with folder name changes

    * chore: bump quartz version

commit 6a9e6352e8
Author: Jacky Zhao <j.zhao2k19@gmail.com>
Date:   Wed Sep 20 13:52:45 2023 -0700

    Revert "feat: Making Quartz available offline by making it a PWA (#465)"

    This reverts commit d6301fae90.

commit 70e029d151
Author: Jacky Zhao <j.zhao2k19@gmail.com>
Date:   Wed Sep 20 13:52:29 2023 -0700

    Revert "docs: wording changes for offline support"

    This reverts commit 52a172d1a4.

commit 0bad3ce799
Author: Jacky Zhao <j.zhao2k19@gmail.com>
Date:   Wed Sep 20 11:58:52 2023 -0700

    docs: document enableToc

commit 52a172d1a4
Author: Jacky Zhao <j.zhao2k19@gmail.com>
Date:   Wed Sep 20 11:40:36 2023 -0700

    docs: wording changes for offline support

commit d6301fae90
Author: Adam Brangenberg <adambrangenberg@proton.me>
Date:   Wed Sep 20 20:38:13 2023 +0200

    feat: Making Quartz available offline by making it a PWA (#465)

    * Adding PWA and chaching for offline aviability

    * renamed workbox config to fit Quartz' scheme

    * Documenting new configuration

    * Added missig umami documentation

    * Fixed formatting so the build passes, thank you prettier :)

    * specified caching strategies to improve performance

    * formatting...

    * fixing "404 manifest.json not found" on subdirectories by adding a / to manifestpath

    * turning it into a plugin

    * Removed Workbox-cli and updated @types/node

    * Added Serviceworkercode to offline.ts

    * formatting

    * Removing workbox from docs

    * applied suggestions

    * Removed path.join for sw path

    Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

    * Removed path.join for manifest path

    Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

    * Removing path module import

    * Added absolute path to manifests start_url and manifest "import" using baseUrl

    * Adding protocol to baseurl

    Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

    * Adding protocol to start_url too then

    * formatting...

    * Adding fallback page

    * Documenting offline plugin

    * formatting...

    * merge suggestion

    Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

    * merge suggestion

    Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

    * merge suggestion

    Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

    * merge suggestion

    Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

    * merge suggestion

    Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

    * merge suggestion

    Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

    * merge suggestion

    Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

    * merge suggestion

    Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

    * merge suggestion

    Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

    * merge suggestion

    Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

    * merge suggestion

    Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

    * merge suggestion

    Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

    * formatting...

    * Fixing manifest path, all these nits hiding the actual issues .-.

    * Offline fallback page through plugins, most things taken from 404 Plugin

    * adding Offline Plugin to config

    * formatting...

    * Turned offline off as default and removed offline.md

    ---------

    Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

commit 27a6087dd5
Author: rwutscher <richard.wutscher@gmail.com>
Date:   Tue Sep 19 21:26:30 2023 +0200

    fix: tag regex no longer includes purely numerical 'tags' (#485)

    * fix: tag regex no longer includes purely numerical 'tags'

    * fix: formatting

    * fix: use guard in findAndReplace() instead of expanding the regex

commit 1bf7e3d8b3
Author: Jacky Zhao <j.zhao2k19@gmail.com>
Date:   Tue Sep 19 10:22:39 2023 -0700

    fix(nit): make defaultOptions on explorer not a function

commit cc31a40b0c
Author: David Fischer <david@konst.fish>
Date:   Tue Sep 19 18:25:51 2023 +0200

    feat: support changes in system theme (#484)

    * feat: support changes in system theme

    * fix: run prettier

    * fix: add content/.gitkeep

commit 0d3cf29226
Author: Ben Schlegel <31989404+benschlegel@users.noreply.github.com>
Date:   Mon Sep 18 23:32:00 2023 +0200

    docs: fix explorer example (#483)

* feat: dynamically generate og images, write to fs as png

* fix: og preview on discord

* feat: use `sharp` to convert to webp, add content headers

* feat: add config for theme (light or dark)

* feat: improve image margins, add font breakpoint

* feat: use config header + body fonts for satori

* perf: memoize fonts

* feat: use default og image if no path exists

* feat: add config option for social images

* feat: support custom og images via frontmatter

* refactor: clean font helpers, rename fonts helper

* refactor: make image generation cleaner

* refactor: move default image to own component

* chore: add todos

* fix: only set width/height header if known

* feat: remove html from description

* feat: make image dimensions configurable

* feat: pass userOpts to image generator

* feat: option for users to provide own image struct (satori)

This allows users to pass their own jsx for generating the default og image

* refactor: rename `defaultImage.tsx` > `socialImage.tsx`

* chore: improve comments + types

* refactor: rename socialImage frontmatter property

* feat: add frontmatter aliases for cover image

* feat: add frontmatter alias for obsidian publish

* docs: add documentation for social images

* feat: add `generateSocialImages` prop to config

* chore: update lock file

* fix: fix type error

* chore: update package.json

* chore: update package-lock.json

* docs: update docs

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* fix: clean url for use in metadata

* refactor: clean function signature

* feat: pass `fileData` to image generator

* CI: run format

* fix: file system import

* fix: merge paths using `joinSegments`

* fix: get output dir via `ctx.argv.output`

* chore: add explanation to font regex

* Squashed commit of the following:

commit 7164857f6e
Author: Aaron Pham <29749331+aarnphm@users.noreply.github.com>
Date:   Fri Mar 15 21:17:42 2024 -0400

    chore(ofm): remove unused (#999)

    Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

commit 47024022e8
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Fri Mar 15 18:29:14 2024 -0400

    chore(deps-dev): bump @types/node from 20.11.24 to 20.11.25 (#990)

    Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.11.24 to 20.11.25.
    - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
    - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

    ---
    updated-dependencies:
    - dependency-name: "@types/node"
      dependency-type: direct:development
      update-type: version-update:semver-patch
    ...

    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit b98e4be665
Author: Mara-Li <lili.simonetti@outlook.fr>
Date:   Fri Mar 15 23:28:31 2024 +0100

    feat(i18n): Add French translation for reading time (#998)

    Signed-off-by: Mara-Li <lili.simonetti@outlook.fr>

commit 8be51a0504
Author: catcodeme <1020082805@qq.com>
Date:   Fri Mar 15 14:25:01 2024 +0800

    fix: wikiLink in table (#993)

    * fix: wikiLink in table

    - update regexp to make '\' to group in alias
    - handle alias using block_id

    * style: format with prettier

    * style: add comment for block_ref(without alias) in table

    ---------

    Co-authored-by: hulinjiang <hulinjiang@58.com>

commit 92cc23dc45
Author: Linus Sehn <37184648+linozen@users.noreply.github.com>
Date:   Wed Mar 13 08:59:37 2024 +0100

    feat(plugin): citations (#984)

    * feat: add rehype-citations

    * feat: add citations transformer plugin

    * feat: add rehype-rewrite

    * feat: add csl option and add no-popover to citation links

    * revert: add rehype-rewrite

    04b2692 'feat: add rehype-rewrite'

    * feat: use existing package for html manipulation

    * fix: remove `console.log()`

commit 097abc3cda
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Mon Mar 11 13:41:48 2024 -0700

    chore(deps): bump async-mutex from 0.4.1 to 0.5.0 (#991)

    Bumps [async-mutex](https://github.com/DirtyHairy/async-mutex) from 0.4.1 to 0.5.0.
    - [Changelog](https://github.com/DirtyHairy/async-mutex/blob/master/CHANGELOG.md)
    - [Commits](https://github.com/DirtyHairy/async-mutex/compare/v0.4.1...v0.5.0)

    ---
    updated-dependencies:
    - dependency-name: async-mutex
      dependency-type: direct:production
      update-type: version-update:semver-minor
    ...

    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit a00324ddfd
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Mon Mar 11 13:41:41 2024 -0700

    chore(deps-dev): bump typescript from 5.3.3 to 5.4.2 (#989)

    Bumps [typescript](https://github.com/Microsoft/TypeScript) from 5.3.3 to 5.4.2.
    - [Release notes](https://github.com/Microsoft/TypeScript/releases)
    - [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
    - [Commits](https://github.com/Microsoft/TypeScript/compare/v5.3.3...v5.4.2)

    ---
    updated-dependencies:
    - dependency-name: typescript
      dependency-type: direct:development
      update-type: version-update:semver-minor
    ...

    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit 9fff6d7d0d
Author: Mara-Li <lili.simonetti@outlook.fr>
Date:   Mon Mar 11 17:46:53 2024 +0100

    fix: spelling error (#987)

    I really don't know why I translated this like that into "pas trouvé", and it bugged me a lot. I finally fixed it…

    Signed-off-by: Mara-Li <lili.simonetti@outlook.fr>

commit 0f5a9d7b66
Author: Matt Vogel <mainmoniker@googlemail.com>
Date:   Sun Mar 10 12:57:10 2024 -0400

    feat: separated content meta (#929)

    to allow for CSS styling

commit b4236e5142
Author: kabirgh <15871468+kabirgh@users.noreply.github.com>
Date:   Sun Mar 10 00:42:23 2024 +0000

    feat(perf:fast-rebuilds): Stop mutating resources param in ComponentResources emitter (#977)

    * Stop mutating resources param in ComponentResources emitter

    * Add done rebuilding log for fast rebuilds

    * Move google font loading to Head component

    * Simplify code and fix comment

commit 6e0c102970
Author: Emile Bangma <ewjbangma@hotmail.com>
Date:   Sun Mar 10 01:14:31 2024 +0100

    fix(transclusion): prevent duplicate transclusion if multiple transclusions are present. (#982)

commit 94a54698ab
Author: Emile Bangma <ewjbangma@hotmail.com>
Date:   Sat Mar 9 17:59:55 2024 +0100

    fix(resources): Use full path to font when cdnCache is false (#976)

commit 2e9a0c21db
Author: Emile Bangma <ewjbangma@hotmail.com>
Date:   Sat Mar 9 17:43:40 2024 +0100

    fix(description): first sentence no longer repeats until max length (#981)

commit b30a200bd4
Author: Aaron Pham <29749331+aarnphm@users.noreply.github.com>
Date:   Fri Mar 8 12:14:22 2024 -0500

    fix(i18n): make sure to use correct fileData for manual localization (#975)

    Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

commit 6d59aa8201
Author: Emile Bangma <ewjbangma@hotmail.com>
Date:   Fri Mar 8 10:04:44 2024 +0100

    fix(description): counts characters instead of words (#972)

    * fix(description): make sure description counts characters instead of words

    * ref: removed duplicate ternary

* CI: fix package log post merge

* CI: fix more merge artifacts

* CI: fix package-lock.json

* feat: add new default image template

* feat: use icon.png for image generation

* chore: update satori and sharp version

* feat(image-generator): add new default template

* Update quartz/components/Head.tsx

* Update quartz/components/Head.tsx

* Update quartz/components/Head.tsx

* Update docs/features/social images.md

* Update quartz/components/Head.tsx

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* feat(og-image): add config option to use default og image for root path

* docs(og-image): add `excludeRoot` config + update preview images

* docs(open-graph): add examples section

* chore: remove unused `socialImage2.tsx` component

* feat(open-graph): add frontmatter aliases for socialImage/cover/image

* fix(open-graph): only load satori font if config option is enabled

* refactor(open-graph): dont use async promise inside `fetchTtf()`

* chore: renaming and finished copywriting

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* chore: update typo

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* chore: update hinting for socialImage

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

---------

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>
Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
Co-authored-by: Emile Bangma <ewjbangma@hotmail.com>
Co-authored-by: Emile Bangma <github@emilebangma.com>
Co-authored-by: Aaron Pham <contact@aarnphm.xyz>
2024-11-12 07:33:35 -05:00
Han-Miru Kim
9188939b1f feat(math): typst support(closes #1568) (#1569)
Co-authored-by: Aaron Pham <contact@aarnphm.xyz>
Co-authored-by: Aaron Pham <Aaronpham0103@gmail.com>
2024-11-12 06:42:44 -05:00
Aaron Pham
92676d746e feat(ofm): support parsing footnotes in table (#1581) 2024-11-12 03:37:59 -05:00
dependabot[bot]
1893196939 chore(deps): bump rehype-citation from 2.2.1 to 2.2.2 in the production-dependencies group (#1583)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-12 00:38:48 -05:00
Aaron Pham
1ab9c91df1 feat(mermaid): improvement navigation (#1575)
* feat(mermaid): custom stuff

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* chore: use mermaid and update clipboard content

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* fix: explicitly use center div

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

---------

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>
2024-11-10 18:13:12 -05:00
dependabot[bot]
0ad9111388 chore(deps): bump rlespinasse/github-slug-action from 4.4.1 to 5.0.0 in the ci-dependencies group (#1578)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-10 04:46:14 -05:00
dependabot[bot]
cbeef5541f chore(deps-dev): bump @types/node from 22.8.7 to 22.9.0 in the production-dependencies group (#1579)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-10 04:46:01 -05:00
Romain Lespinasse
0f04f1262c ci(dependabot): update GA periodically (#1577)
Co-authored-by: Aaron Pham <contact@aarnphm.xyz>
2024-11-10 04:41:56 -05:00
David Fischer
31e0b7c6f8 feat(comments): conditional display via frontmatter (#1566) 2024-11-09 04:44:32 -05:00
Jacky Zhao
a6b2967df8 fix: account for chokidar v4 glob deprecation in source watching 2024-11-07 16:32:26 -08:00
Jacky Zhao
f84a6554f6 fix(style): remove flex end to fix tag alignment in content meta 2024-11-07 15:13:28 -08:00
dependabot[bot]
1d5b6f58d8 chore(deps): bump the production-dependencies group with 4 updates (#1565)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-04 21:03:30 -05:00
Stephen Tse
74f8c13598 fix: reset z-index instead of inherited unset (#1528)
Co-authored-by: Aaron Pham <contact@aarnphm.xyz>
2024-11-04 20:22:36 -05:00
Aaron Pham
56ba2f4fa7 feat(build): add support for parsing inline sass (#1558) 2024-11-01 07:16:12 -04:00
Emile Bangma
314a88d5c6 feat(transformer): allow inline CSS styling (#1551)
* feat(transformer): allow inline CSS styling

* Updated docs

* Default to inline: false

* Removed redundant inline: false
2024-10-30 23:57:39 +01:00
Osman Tunahan ARIKAN
a53772bbd1 feat(i18n): add turkish language (#1554) 2024-10-30 18:33:22 -04:00
dependabot[bot]
1915a198d9 chore(deps): bump the production-dependencies group with 5 updates (#1548)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-29 20:04:45 -04:00
Thomas Hack
1d2dc167f4 feat(folder): add intermediate folders pages (#1295)
Co-authored-by: Aaron Pham <Aaronpham0103@gmail.com>
Co-authored-by: Aaron Pham <contact@aarnphm.xyz>
2024-10-25 12:52:33 -04:00
dependabot[bot]
3ef2a24f4b chore(deps): bump the production-dependencies group across 1 directory with 7 updates (#1540) 2024-10-22 17:54:41 -04:00
Emile Bangma
d98a3a088a Merge pull request #1543 from saberzero1/draft-publish-string
feat(filters): support "true" as valid for frontmatter filters
2024-10-22 22:38:16 +02:00
Emile Bangma
ad52d09567 Feat(filters): Support "true" as valid for draft and publish frontmatter values (b3a02909ba) 2024-10-22 20:33:54 +00:00
Emile Bangma
9f701e5045 fix(grid): $desktop variable (#1511) 2024-10-17 23:50:48 -04:00
Emile Bangma
67e1beea70 feat(comments): support custom giscus themes (#1526)
Co-authored-by: Aaron Pham <Aaronpham0103@gmail.com>
Co-authored-by: Aaron Pham <contact@aarnphm.xyz>
2024-10-17 18:30:16 -04:00
Emile Bangma
3aa11357aa fix(toc): regression (#1517) 2024-10-16 17:44:30 -04:00
Jacky Zhao
b3a02909ba fix: make filter checks more strict (closes #1519) 2024-10-15 09:34:46 -07:00
Sohum
1dc208356a fix(cli): use shell on win32 for update (#1503) (#1504)
If there is no `npm.exe` on the system, but
instead an `npm.cmd`, then node won't find
the `npm` executable when calling `spawnSync`.

This occurs frequently when using node package
managers on Windows.

See the node documentation for `.bat` and `.cmd`
files here.

<https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows>.
2024-10-13 10:46:51 -07:00
Simon Späti
3d0ba32070 docs: Update Name and URL of Simon's SB and adding DE Vault (#1507) 2024-10-13 10:42:29 -07:00
Aaron Pham
c5d97db000 fix(toc): invalid desktop-only styling (#1502)
* fix(toc): invalid desktop-only styling

should display none instead.

* Update toc.scss
2024-10-12 18:33:07 -04:00
Pierre Lairez
0d1f15d37c feat(ofm): Allow for dashes in custom callout label (#1493)
* [OFM] Allow for dashes in custom callout label

For compatibility with Obsidian's behavior, a custom callout like
[!see-also] is possible. Previously, this was parsed by Quartz as a
callout “see” with metadata “-also”. Instead, this is should be a
callout “see-also” with title “See also” (capitalization + replace
dashes by spaces).

* prettier
2024-10-10 07:15:23 -04:00
dependabot[bot]
11c23a137a chore(deps): bump the production-dependencies group with 9 updates (#1495)
Bumps the production-dependencies group with 9 updates:

| Package | From | To |
| --- | --- | --- |
| [chokidar](https://github.com/paulmillr/chokidar) | `3.6.0` | `4.0.1` |
| [esbuild-sass-plugin](https://github.com/glromeo/esbuild-sass-plugin) | `2.16.1` | `3.3.1` |
| [lightningcss](https://github.com/parcel-bundler/lightningcss) | `1.26.0` | `1.27.0` |
| [preact](https://github.com/preactjs/preact) | `10.24.1` | `10.24.2` |
| [rehype-citation](https://github.com/timlrx/rehype-citation) | `2.1.1` | `2.1.2` |
| [shiki](https://github.com/shikijs/shiki/tree/HEAD/packages/shiki) | `1.18.0` | `1.22.0` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `22.7.4` | `22.7.5` |
| [esbuild](https://github.com/evanw/esbuild) | `0.19.12` | `0.24.0` |
| [typescript](https://github.com/microsoft/TypeScript) | `5.6.2` | `5.6.3` |


Updates `chokidar` from 3.6.0 to 4.0.1
- [Release notes](https://github.com/paulmillr/chokidar/releases)
- [Commits](https://github.com/paulmillr/chokidar/compare/3.6.0...4.0.1)

Updates `esbuild-sass-plugin` from 2.16.1 to 3.3.1
- [Release notes](https://github.com/glromeo/esbuild-sass-plugin/releases)
- [Commits](https://github.com/glromeo/esbuild-sass-plugin/compare/v2.16.1...v3.3.1)

Updates `lightningcss` from 1.26.0 to 1.27.0
- [Release notes](https://github.com/parcel-bundler/lightningcss/releases)
- [Commits](https://github.com/parcel-bundler/lightningcss/compare/v1.26.0...v1.27.0)

Updates `preact` from 10.24.1 to 10.24.2
- [Release notes](https://github.com/preactjs/preact/releases)
- [Commits](https://github.com/preactjs/preact/compare/10.24.1...10.24.2)

Updates `rehype-citation` from 2.1.1 to 2.1.2
- [Release notes](https://github.com/timlrx/rehype-citation/releases)
- [Commits](https://github.com/timlrx/rehype-citation/compare/v2.1.1...v2.1.2)

Updates `shiki` from 1.18.0 to 1.22.0
- [Release notes](https://github.com/shikijs/shiki/releases)
- [Changelog](https://github.com/shikijs/shiki/blob/main/CHANGELOG.md)
- [Commits](https://github.com/shikijs/shiki/commits/v1.22.0/packages/shiki)

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

Updates `esbuild` from 0.19.12 to 0.24.0
- [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.19.12...v0.24.0)

Updates `typescript` from 5.6.2 to 5.6.3
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.6.2...v5.6.3)

---
updated-dependencies:
- dependency-name: chokidar
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: production-dependencies
- dependency-name: esbuild-sass-plugin
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: production-dependencies
- dependency-name: lightningcss
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: preact
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: rehype-citation
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: shiki
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: esbuild
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-09 14:57:33 -07:00
Aaron Pham
8a95c865c8 chore(citations): passthrough options (#1429) 2024-10-09 14:14:44 -04:00
Aaron Pham
fe4b039b60 chore(ci): group dependabot upgrade (#1491)
Signed-off-by: Aaron Pham <contact@aarnphm.xyz>
2024-10-09 09:52:49 -07:00
Aaron Pham
497d51973a chore(ci): remove signing and vuln (#1484)
Signed-off-by: Aaron Pham <contact@aarnphm.xyz>
2024-10-07 02:52:14 -04:00
Aaron Pham
66d7dd8677 fix(ci): run build on tags (#1483)
* fix(ci): run build on tags

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* chore: update docker warning and not push on PR

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

---------

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>
2024-10-06 22:34:31 -04:00
Julia van der Kris
313cef60ee infra: build quartz docker image to GHCR (#1192)
* Add GitHub action to build & push Docker image to GHCR

* Use double quotes to keep `prettier` happy :)

* Don't run Docker build & push on forks

* -1 char commit lmao

* Add git metadata to Docker image

* Apply Aaron's patch

* chore: run prettier

---------

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>
Co-authored-by: Aaron Pham <contact@aarnphm.xyz>
2024-10-06 22:15:19 -04:00
KurekMartin
af14ca7c4f i18n: add Czech translation (#1477)
* add Czech translation

* fix formatting
2024-10-04 09:10:40 -07:00
dependabot[bot]
e06f681ce7 chore(deps): bump preact from 10.24.0 to 10.24.1 (#1464)
Bumps [preact](https://github.com/preactjs/preact) from 10.24.0 to 10.24.1.
- [Release notes](https://github.com/preactjs/preact/releases)
- [Commits](https://github.com/preactjs/preact/compare/10.24.0...10.24.1)

---
updated-dependencies:
- dependency-name: preact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-02 04:42:14 -04:00
dependabot[bot]
62906eebd3 chore(deps-dev): bump @types/node from 22.5.5 to 22.7.4 (#1465)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.5.5 to 22.7.4.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-02 04:42:02 -04:00
dependabot[bot]
9cd072bfc3 chore(deps): bump hast-util-to-string from 3.0.0 to 3.0.1 (#1466)
Bumps [hast-util-to-string](https://github.com/rehypejs/rehype-minify) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/rehypejs/rehype-minify/releases)
- [Changelog](https://github.com/rehypejs/rehype-minify/blob/main/changelog.md)
- [Commits](https://github.com/rehypejs/rehype-minify/commits)

---
updated-dependencies:
- dependency-name: hast-util-to-string
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-02 04:41:50 -04:00
dependabot[bot]
319b4497bc chore(deps-dev): bump tsx from 4.19.0 to 4.19.1 (#1467)
Bumps [tsx](https://github.com/privatenumber/tsx) from 4.19.0 to 4.19.1.
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.19.0...v4.19.1)

---
updated-dependencies:
- dependency-name: tsx
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-02 04:41:36 -04:00
Emile Bangma
b0c079f24a fix(layout): restore footer to the proper position (#1470)
* fix(layout): restore footer to the proper position

* align ToC scrollbar properly on short headers
2024-10-01 08:49:13 -07:00
dependabot[bot]
1b122a1da0 chore(deps): bump @floating-ui/dom from 1.6.10 to 1.6.11 (#1463)
Bumps [@floating-ui/dom](https://github.com/floating-ui/floating-ui/tree/HEAD/packages/dom) from 1.6.10 to 1.6.11.
- [Release notes](https://github.com/floating-ui/floating-ui/releases)
- [Changelog](https://github.com/floating-ui/floating-ui/blob/master/packages/dom/CHANGELOG.md)
- [Commits](https://github.com/floating-ui/floating-ui/commits/@floating-ui/dom@1.6.11/packages/dom)

---
updated-dependencies:
- dependency-name: "@floating-ui/dom"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 20:10:05 -04:00
Malcolm Mielle
b8c46ba81a feat(analytics): clarity (#1446)
Add the code necessary for support of Microsoft clarity
2024-09-30 10:06:37 -07:00
Emile Bangma
1416f62a47 fix(layout): backlinks grid triage (#1447)
* fix: mobile backlinks orientation

* temp: hide broken list gradient

* fix: backlinks overflow
2024-09-29 22:24:15 -04:00
Jacky Zhao
8889ab63eb fix: provide default pageTitleSuffix (closes #1452) 2024-09-28 09:38:19 -07:00
Emile Bangma
5eec1e98e6 fix(layout): grid triage with backlinks (#1442) 2024-09-25 18:15:07 -04:00
Emile Bangma
a7a0dcad22 fix(layout): grid triage and regression (#1440)
* fix(table of contents): multiple scrollbars (https://github.com/jackyzha0/quartz/issues/1388)

* fix(center): Main content mininum width (https://github.com/jackyzha0/quartz/issues/1439)

* fix(code block): Horizontal overflow fix (https://github.com/jackyzha0/quartz/issues/1438, https://github.com/jackyzha0/quartz/issues/1353)

* WIP fix for ul/ol .overflow

* Fix: restore former scrollbar behavior for overflow lists (https://github.com/jackyzha0/quartz/issues/1437)

* Fix: code block overflow-x

* fix: Table of Content overflow (https://github.com/jackyzha0/quartz/issues/1437)

* Address feedback

* Move max-height toggle from js to css
2024-09-25 17:20:58 -04:00
threehymns
921f45cf70 feat: add a config option for a pageTitleSuffix (#1320)
* feat: add a config option for a pageTitleSuffix

* Run Prettier on Head.tsx

* Make pageTitleSuffix optional

Co-authored-by: Aaron Pham <Aaronpham0103@gmail.com>

---------

Co-authored-by: Aaron Pham <Aaronpham0103@gmail.com>
2024-09-24 20:20:36 -04:00
dependabot[bot]
90c187587f chore(deps): bump pixi.js from 8.3.4 to 8.4.1 (#1432)
Bumps [pixi.js](https://github.com/pixijs/pixijs) from 8.3.4 to 8.4.1.
- [Release notes](https://github.com/pixijs/pixijs/releases)
- [Commits](https://github.com/pixijs/pixijs/compare/v8.3.4...v8.4.1)

---
updated-dependencies:
- dependency-name: pixi.js
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-24 20:19:48 -04:00
dependabot[bot]
08e20a7006 chore(deps): bump remark-rehype from 11.1.0 to 11.1.1 (#1435)
Bumps [remark-rehype](https://github.com/remarkjs/remark-rehype) from 11.1.0 to 11.1.1.
- [Release notes](https://github.com/remarkjs/remark-rehype/releases)
- [Commits](https://github.com/remarkjs/remark-rehype/compare/11.1.0...11.1.1)

---
updated-dependencies:
- dependency-name: remark-rehype
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-24 20:19:37 -04:00
dependabot[bot]
9e32016508 chore(deps): bump rehype-pretty-code from 0.13.2 to 0.14.0 (#1433)
Bumps [rehype-pretty-code](https://github.com/rehype-pretty/rehype-pretty-code/tree/HEAD/packages/core) from 0.13.2 to 0.14.0.
- [Release notes](https://github.com/rehype-pretty/rehype-pretty-code/releases)
- [Changelog](https://github.com/rehype-pretty/rehype-pretty-code/blob/master/packages/core/CHANGELOG.md)
- [Commits](https://github.com/rehype-pretty/rehype-pretty-code/commits/rehype-pretty-code@0.14.0/packages/core)

---
updated-dependencies:
- dependency-name: rehype-pretty-code
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-24 20:19:25 -04:00
dependabot[bot]
8a050c0be0 chore(deps): bump hast-util-to-html from 9.0.2 to 9.0.3 (#1434)
Bumps [hast-util-to-html](https://github.com/syntax-tree/hast-util-to-html) from 9.0.2 to 9.0.3.
- [Release notes](https://github.com/syntax-tree/hast-util-to-html/releases)
- [Commits](https://github.com/syntax-tree/hast-util-to-html/compare/9.0.2...9.0.3)

---
updated-dependencies:
- dependency-name: hast-util-to-html
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-24 20:19:12 -04:00
dependabot[bot]
0aacd8ed2e chore(deps): bump shiki from 1.12.1 to 1.18.0 (#1436)
Bumps [shiki](https://github.com/shikijs/shiki/tree/HEAD/packages/shiki) from 1.12.1 to 1.18.0.
- [Release notes](https://github.com/shikijs/shiki/releases)
- [Changelog](https://github.com/shikijs/shiki/blob/main/CHANGELOG.md)
- [Commits](https://github.com/shikijs/shiki/commits/v1.18.0/packages/shiki)

---
updated-dependencies:
- dependency-name: shiki
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-24 20:18:55 -04:00
Emile Bangma
7d7e334976 feat: responsive design grid (#1354)
* Responsive design grid

* Addressed PR feedback

* Bump Quartz version 4.3.1 => 4.4.0

* Moved page-header into center

* Updated docs with new layouts

* Sync updated version number with package-lock

* Table of Content scrollbar auto

* Reset node_modules

* Updated layout images

* Fixed tablet layout

* Finilazed layout images
2024-09-23 13:40:12 -07:00
Jacky Zhao
9cefcd0dd1 Revert "perf: eagerly compute explorer nodes to avoid re-render in memoized value"
This reverts commit 16a9caa555.
2024-09-20 15:48:14 -07:00
Gustavo de Paula
4aaeb768d8 update cabin url (#1428) 2024-09-20 09:23:19 -07:00
dependabot[bot]
743ef712d5 chore(deps): bump preact-render-to-string from 6.5.10 to 6.5.11 (#1417)
Bumps [preact-render-to-string](https://github.com/preactjs/preact-render-to-string) from 6.5.10 to 6.5.11.
- [Release notes](https://github.com/preactjs/preact-render-to-string/releases)
- [Changelog](https://github.com/preactjs/preact-render-to-string/blob/main/CHANGELOG.md)
- [Commits](https://github.com/preactjs/preact-render-to-string/compare/v6.5.10...v6.5.11)

---
updated-dependencies:
- dependency-name: preact-render-to-string
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-16 22:24:03 -04:00
dependabot[bot]
9a6e4e2f80 chore(deps-dev): bump typescript from 5.5.4 to 5.6.2 (#1418)
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.5.4 to 5.6.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.5.4...v5.6.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-16 19:56:14 -04:00
dependabot[bot]
14cb50de9b chore(deps): bump preact from 10.23.2 to 10.24.0 (#1419)
Bumps [preact](https://github.com/preactjs/preact) from 10.23.2 to 10.24.0.
- [Release notes](https://github.com/preactjs/preact/releases)
- [Commits](https://github.com/preactjs/preact/compare/10.23.2...10.24.0)

---
updated-dependencies:
- dependency-name: preact
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-16 19:56:00 -04:00
dependabot[bot]
dad0ae4e3f chore(deps-dev): bump @types/node from 22.5.4 to 22.5.5 (#1420)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.5.4 to 22.5.5.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-16 19:55:42 -04:00
dependabot[bot]
9c060f3cf2 chore(deps): bump vfile from 6.0.2 to 6.0.3 (#1421)
Bumps [vfile](https://github.com/vfile/vfile) from 6.0.2 to 6.0.3.
- [Release notes](https://github.com/vfile/vfile/releases)
- [Changelog](https://github.com/vfile/vfile/blob/main/changelog.md)
- [Commits](https://github.com/vfile/vfile/compare/6.0.2...6.0.3)

---
updated-dependencies:
- dependency-name: vfile
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-16 19:55:23 -04:00
Jacky Zhao
16a9caa555 perf: eagerly compute explorer nodes to avoid re-render in memoized value 2024-09-15 18:05:17 -07:00
Alexander Weichart
b1c60b8833 Add alexanderweichart.de to showcase.md (#1378)
Added my own website ^^

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
2024-09-15 17:03:22 -07:00
Jacky Zhao
50a78bafa5 docs: clarify prod hosting 2024-09-15 16:47:11 -07:00
Jacky Zhao
6ea359e55e perf: have more than 1ms granularity for rebuild detection 2024-09-15 16:42:07 -07:00
Emile Bangma
cd3bb25626 fix: account for subtags in numerical tags (closes #1408) (#1410) 2024-09-15 11:25:31 -04:00
bfahrenfort
eb9bbd1666 fix: constrain link icon size (#1409) 2024-09-14 10:10:01 -07:00
dependabot[bot]
c36551310d chore(deps): bump preact-render-to-string from 6.5.9 to 6.5.10 (#1390)
Bumps [preact-render-to-string](https://github.com/preactjs/preact-render-to-string) from 6.5.9 to 6.5.10.
- [Release notes](https://github.com/preactjs/preact-render-to-string/releases)
- [Changelog](https://github.com/preactjs/preact-render-to-string/blob/main/CHANGELOG.md)
- [Commits](https://github.com/preactjs/preact-render-to-string/compare/v6.5.9...v6.5.10)

---
updated-dependencies:
- dependency-name: preact-render-to-string
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-11 14:46:22 -07:00
Jacky Zhao
6215dd5565 docs: fix roam research docs 2024-09-11 14:45:04 -07:00
dependabot[bot]
c89c76b40a chore(deps-dev): bump tsx from 4.18.0 to 4.19.0 (#1391)
Bumps [tsx](https://github.com/privatenumber/tsx) from 4.18.0 to 4.19.0.
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.18.0...v4.19.0)

---
updated-dependencies:
- dependency-name: tsx
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-11 03:30:44 -04:00
dependabot[bot]
a145e320d0 chore(deps): bump hast-util-to-html from 9.0.1 to 9.0.2 (#1393)
Bumps [hast-util-to-html](https://github.com/syntax-tree/hast-util-to-html) from 9.0.1 to 9.0.2.
- [Release notes](https://github.com/syntax-tree/hast-util-to-html/releases)
- [Commits](https://github.com/syntax-tree/hast-util-to-html/compare/9.0.1...9.0.2)

---
updated-dependencies:
- dependency-name: hast-util-to-html
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-11 03:30:28 -04:00
dependabot[bot]
082cbb74c3 chore(deps-dev): bump @types/node from 22.5.0 to 22.5.4 (#1403)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.5.0 to 22.5.4.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-11 03:29:51 -04:00
dependabot[bot]
93b2481261 chore(deps): bump pixi.js from 8.3.3 to 8.3.4 (#1394)
Bumps [pixi.js](https://github.com/pixijs/pixijs) from 8.3.3 to 8.3.4.
- [Release notes](https://github.com/pixijs/pixijs/releases)
- [Commits](https://github.com/pixijs/pixijs/compare/v8.3.3...v8.3.4)

---
updated-dependencies:
- dependency-name: pixi.js
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-11 03:29:36 -04:00
Matt Vogel
d2414b3903 feat(markdown): Roam Research flavour (#985)
* feat: Roam Research flavor markdown

* docs: Roam Research transformer

* use markdownPlugins

* fix roam matching

* cleanup: Roam Plugin

---------

Co-authored-by: Matt Vogel <>
2024-09-11 03:29:07 -04:00
MarcRez33
01fc26d2c0 fix: Reload graph after a theme change (closes #1380) (#1383)
* FIX: Reload graph after a theme change

* FIX: Reload graph after a theme change - comment updated

* FIX: Reload graph after a theme change - comment updated

* FIX: Reload graph after a theme change

* fix: Reload graph after a theme change
2024-08-31 12:25:30 -07:00
bfahrenfort
84a9be65ce Add Projects & Privacy to showcase.md (#1381) 2024-08-31 11:43:49 -07:00
Jacky Zhao
6715079a89 fix: only apply z-index on left sidebar 2024-08-28 12:50:25 -07:00
dependabot[bot]
d613a3d2f2 chore(deps): bump unified from 11.0.4 to 11.0.5 (#1373)
Bumps [unified](https://github.com/unifiedjs/unified) from 11.0.4 to 11.0.5.
- [Release notes](https://github.com/unifiedjs/unified/releases)
- [Changelog](https://github.com/unifiedjs/unified/blob/main/changelog.md)
- [Commits](https://github.com/unifiedjs/unified/compare/11.0.4...11.0.5)

---
updated-dependencies:
- dependency-name: unified
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 14:13:09 -07:00
dependabot[bot]
9b75faafea chore(deps-dev): bump @types/yargs from 17.0.32 to 17.0.33 (#1374)
Bumps [@types/yargs](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/yargs) from 17.0.32 to 17.0.33.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/yargs)

---
updated-dependencies:
- dependency-name: "@types/yargs"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 14:13:00 -07:00
dependabot[bot]
5a26b582ed chore(deps-dev): bump @types/node from 22.1.0 to 22.5.0 (#1375)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.1.0 to 22.5.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 14:12:51 -07:00
dependabot[bot]
4e2aea8a5a chore(deps): bump @napi-rs/simple-git from 0.1.17 to 0.1.19 (#1376)
Bumps [@napi-rs/simple-git](https://github.com/Brooooooklyn/simple-git) from 0.1.17 to 0.1.19.
- [Release notes](https://github.com/Brooooooklyn/simple-git/releases)
- [Commits](https://github.com/Brooooooklyn/simple-git/compare/v0.1.17...v0.1.19)

---
updated-dependencies:
- dependency-name: "@napi-rs/simple-git"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 14:12:45 -07:00
dependabot[bot]
40f039983c chore(deps-dev): bump tsx from 4.17.0 to 4.18.0 (#1377)
Bumps [tsx](https://github.com/privatenumber/tsx) from 4.17.0 to 4.18.0.
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.17.0...v4.18.0)

---
updated-dependencies:
- dependency-name: tsx
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 14:12:35 -07:00
Iker Larrea
3b988aec61 fix: Popover z-index issue on left sidebar (#1230) 2024-08-25 19:33:34 -07:00
Aaron Pham
bca74623a3 perf(graph): canvas implementation (#1328)
* perf(graph): initial canvas layout

include nodes and links drawn

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* fix(graph): update persistent for nodeGfx

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* chore(graph): add canvas element to avoid rerendering glitch

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* fix(spa): only render graph once in global

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* fix(graph): change svg as button

render global graph on toggle

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* fix(graph): fix anchor position and zIndex behaviour

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* chore(graph): increase linkDistance

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* refactor

* fmt

* pkg

---------

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>
Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
2024-08-25 00:33:14 -07:00
Andrew
4c9e860150 a11y(darkmode): use a button for the theme toggle (#1335)
* Use a `<button>` for theme toggle

* docs: Adds back Xinyang's cs garden to showcase (#1323)

adding back my garden which was deleted from the cleanup showcase

* feat(toc,explorer): add accessibility for toggle (#1327)

* Restore focus highlight on explorer toggle button.

Remove `unset: all` declaration causing `outline`
property to be unset. This allows the default
browser focus highlight to be shown.

* Fix semantics of expandable sections (explorer, toc).

This adds the appropriate aria attributes for the [disclosure pattern](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-image-description/#javascriptandcsssourcecode) and uses `visibility: hidden` to remove the hidden elements from the focus order without disrupting the animations. Further work is needed on the tree view nodes.

* Run prettier for SCSS files.

* feat: custom global latex macros (closes #1325)

* chore: ts fixes

* docs: recommend at least node 20 in gh

* fix: unmemoize explorer on rebuild (closes #1077)

* fix: pass buildId to worker

* Fix theme button DOM hierarchy and styles

* Restore functionality of theme button

* `aria-label` on theme svgs so their accessible labels are included in button content

---------

Co-authored-by: Xinyang Yu <47915643+xy-241@users.noreply.github.com>
Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
2024-08-24 13:33:30 -07:00
Danny van Tol
46b63b68bf fix: Link visibility in exampanded TOC while collapseByDefault is true (#1371) 2024-08-24 12:56:17 -07:00
Andrew
c4cd84dcc8 fix: Add a wrapper element to dates in PageList. (#1345)
* Add a wrapper span to dates in PageList.

This means there is a placeholder when date is not specified, so the values in grid-template-columns always line up correctly.

* Use a <div> instead -- better practice to stick to block elements.
2024-08-24 10:13:55 -07:00
Aaron Pham
2be9c096a1 chore(features): remove implemented (#1350) 2024-08-24 10:13:14 -07:00
Tony Jackson
f913332862 docs: Update Gitlab CI runner tag to use Gitlab runner (#1365)
Co-authored-by: Tony Jackson <tony.aaj.jackson@protonmail.com>
2024-08-23 18:55:03 -07:00
Elijah Kennedy
647c125525 docs: added permalink description to supported frontmatter doc (#1369) 2024-08-23 18:35:52 -07:00
Eledah
3b74453fe6 docs: RTL showcase (#1362) 2024-08-20 09:28:52 -07:00
dependabot[bot]
6b499ed90c chore(deps): bump @floating-ui/dom from 1.6.8 to 1.6.10 (#1358)
Bumps [@floating-ui/dom](https://github.com/floating-ui/floating-ui/tree/HEAD/packages/dom) from 1.6.8 to 1.6.10.
- [Release notes](https://github.com/floating-ui/floating-ui/releases)
- [Changelog](https://github.com/floating-ui/floating-ui/blob/master/packages/dom/CHANGELOG.md)
- [Commits](https://github.com/floating-ui/floating-ui/commits/@floating-ui/dom@1.6.10/packages/dom)

---
updated-dependencies:
- dependency-name: "@floating-ui/dom"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 14:18:00 -07:00
dependabot[bot]
437d65c847 chore(deps): bump lightningcss from 1.25.1 to 1.26.0 (#1359)
Bumps [lightningcss](https://github.com/parcel-bundler/lightningcss) from 1.25.1 to 1.26.0.
- [Release notes](https://github.com/parcel-bundler/lightningcss/releases)
- [Commits](https://github.com/parcel-bundler/lightningcss/compare/v1.25.1...v1.26.0)

---
updated-dependencies:
- dependency-name: lightningcss
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 14:15:43 -07:00
dependabot[bot]
7c5709b660 chore(deps): bump preact-render-to-string from 6.5.7 to 6.5.9 (#1360)
Bumps [preact-render-to-string](https://github.com/preactjs/preact-render-to-string) from 6.5.7 to 6.5.9.
- [Release notes](https://github.com/preactjs/preact-render-to-string/releases)
- [Changelog](https://github.com/preactjs/preact-render-to-string/blob/main/CHANGELOG.md)
- [Commits](https://github.com/preactjs/preact-render-to-string/compare/v6.5.7...v6.5.9)

---
updated-dependencies:
- dependency-name: preact-render-to-string
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 14:15:34 -07:00
dependabot[bot]
21e1921822 chore(deps): bump @napi-rs/simple-git from 0.1.16 to 0.1.17 (#1357)
Bumps [@napi-rs/simple-git](https://github.com/Brooooooklyn/simple-git) from 0.1.16 to 0.1.17.
- [Release notes](https://github.com/Brooooooklyn/simple-git/releases)
- [Commits](https://github.com/Brooooooklyn/simple-git/compare/v0.1.16...v0.1.17)

---
updated-dependencies:
- dependency-name: "@napi-rs/simple-git"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 14:14:45 -07:00
dependabot[bot]
e47c29d2fd chore(deps): bump rehype-katex from 7.0.0 to 7.0.1 (#1356)
Bumps [rehype-katex](https://github.com/remarkjs/remark-math) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/remarkjs/remark-math/releases)
- [Commits](https://github.com/remarkjs/remark-math/compare/rehype-katex@7.0.0...rehype-katex@7.0.1)

---
updated-dependencies:
- dependency-name: rehype-katex
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 14:14:29 -07:00
dependabot[bot]
a5f2f874f7 chore(deps-dev): bump tsx from 4.16.2 to 4.17.0 (#1340)
Bumps [tsx](https://github.com/privatenumber/tsx) from 4.16.2 to 4.17.0.
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.16.2...v4.17.0)

---
updated-dependencies:
- dependency-name: tsx
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-15 05:07:05 -04:00
dependabot[bot]
eb23cbe8da chore(deps): bump rehype-citation from 2.0.0 to 2.1.1 (#1341)
Bumps [rehype-citation](https://github.com/timlrx/rehype-citation) from 2.0.0 to 2.1.1.
- [Release notes](https://github.com/timlrx/rehype-citation/releases)
- [Commits](https://github.com/timlrx/rehype-citation/compare/v2.0.0...v2.1.1)

---
updated-dependencies:
- dependency-name: rehype-citation
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-15 04:25:07 -04:00
dependabot[bot]
cb68069d45 chore(deps-dev): bump typescript from 5.5.3 to 5.5.4 (#1343)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 5.5.3 to 5.5.4.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v5.5.3...v5.5.4)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-15 04:24:48 -04:00
Andrew
d27c292736 feat: Mark the external link icon as aria-hidden (#1346) 2024-08-12 20:06:19 -07:00
dependabot[bot]
0ee103a514 chore(deps): bump shiki from 1.10.3 to 1.12.1 (#1344)
Bumps [shiki](https://github.com/shikijs/shiki/tree/HEAD/packages/shiki) from 1.10.3 to 1.12.1.
- [Release notes](https://github.com/shikijs/shiki/releases)
- [Changelog](https://github.com/shikijs/shiki/blob/main/CHANGELOG.md)
- [Commits](https://github.com/shikijs/shiki/commits/v1.12.1/packages/shiki)

---
updated-dependencies:
- dependency-name: shiki
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-12 13:38:15 -07:00
dependabot[bot]
c5f0b69a52 chore(deps): bump preact from 10.22.1 to 10.23.2 (#1342)
Bumps [preact](https://github.com/preactjs/preact) from 10.22.1 to 10.23.2.
- [Release notes](https://github.com/preactjs/preact/releases)
- [Commits](https://github.com/preactjs/preact/compare/10.22.1...10.23.2)

---
updated-dependencies:
- dependency-name: preact
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-12 13:37:54 -07:00
Karthi Fair Hawn
323167a001 docs(hosting): missing key with: (#1334) 2024-08-10 08:32:51 -07:00
Andrew
3b5ed813f5 feat(search): keyboard-accessible search button (#1331)
* Use a `<button>` for search

* Fix search button styles to match preexisting styling

* Remove additional native button properties.

* Invoke search button on click or keyboard.

* Reorganize search button DOM hierarchy

* Restore focus to the search button when exiting the search overlay

* Run prettier on Search.tsx
2024-08-09 18:46:50 -07:00
Jacky Zhao
195fc5134c fix: pass buildId to worker 2024-08-08 19:33:27 -07:00
Jacky Zhao
e89c395f7c fix: unmemoize explorer on rebuild (closes #1077) 2024-08-08 19:17:20 -07:00
Jacky Zhao
2db735a150 docs: recommend at least node 20 in gh 2024-08-08 18:38:17 -07:00
Jacky Zhao
39eebca3cf chore: ts fixes 2024-08-08 18:28:13 -07:00
Jacky Zhao
9acaa1c8ac feat: custom global latex macros (closes #1325) 2024-08-08 18:19:45 -07:00
Andrew
27a41abb62 feat(toc,explorer): add accessibility for toggle (#1327)
* Restore focus highlight on explorer toggle button.

Remove `unset: all` declaration causing `outline`
property to be unset. This allows the default
browser focus highlight to be shown.

* Fix semantics of expandable sections (explorer, toc).

This adds the appropriate aria attributes for the [disclosure pattern](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-image-description/#javascriptandcsssourcecode) and uses `visibility: hidden` to remove the hidden elements from the focus order without disrupting the animations. Further work is needed on the tree view nodes.

* Run prettier for SCSS files.
2024-08-08 18:07:47 -07:00
Xinyang Yu
12904ab796 docs: Adds back Xinyang's cs garden to showcase (#1323)
adding back my garden which was deleted from the cleanup showcase
2024-08-08 18:00:20 -07:00
195 changed files with 12635 additions and 4844 deletions

View File

@@ -1,11 +1,20 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2 version: 2
updates: updates:
- package-ecosystem: "npm" - package-ecosystem: "npm"
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "weekly"
groups:
production-dependencies:
applies-to: "version-updates"
patterns:
- "*"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
groups:
ci-dependencies:
applies-to: "version-updates"
patterns:
- "*"

43
.github/workflows/build-preview.yaml vendored Normal file
View File

@@ -0,0 +1,43 @@
name: Build Preview Deployment
on:
pull_request:
types: [opened, synchronize]
workflow_dispatch:
jobs:
build-preview:
if: ${{ github.repository == 'jackyzha0/quartz' }}
runs-on: ubuntu-latest
name: Build Preview
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Cache dependencies
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm ci
- name: Check types and style
run: npm run check
- name: Build Quartz
run: npx quartz build -d docs -v
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: preview-build
path: public

View File

@@ -31,7 +31,7 @@ jobs:
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 22
- name: Cache dependencies - name: Cache dependencies
uses: actions/cache@v4 uses: actions/cache@v4

37
.github/workflows/deploy-preview.yaml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Upload Preview Deployment
on:
workflow_run:
workflows: ["Build Preview Deployment"]
types:
- completed
permissions:
actions: read
deployments: write
contents: read
pull-requests: write
jobs:
deploy-preview:
if: ${{ github.repository == 'jackyzha0/quartz' && github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
name: Deploy Preview to Cloudflare Pages
steps:
- name: Download build artifact
uses: actions/download-artifact@v4
id: preview-build-artifact
with:
name: preview-build
path: build
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
- name: Deploy to Cloudflare Pages
uses: AdrianGonz97/refined-cf-pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
githubToken: ${{ secrets.GITHUB_TOKEN }}
projectName: quartz
deploymentName: Branch Preview
directory: ${{ steps.preview-build-artifact.outputs.download-path }}

View File

@@ -0,0 +1,88 @@
name: Docker build & push image
on:
push:
branches: [v4]
tags: ["v*"]
pull_request:
branches: [v4]
paths:
- .github/workflows/docker-build-push.yaml
- quartz/**
workflow_dispatch:
jobs:
build:
if: ${{ github.repository == 'jackyzha0/quartz' }} # Comment this out if you want to publish your own images on a fork!
runs-on: ubuntu-latest
steps:
- name: Set lowercase repository owner environment variable
run: |
echo "OWNER_LOWERCASE=${OWNER,,}" >> ${GITHUB_ENV}
env:
OWNER: "${{ github.repository_owner }}"
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v5.1.0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
install: true
driver-opts: |
image=moby/buildkit:master
network=host
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@v3.8.2
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata tags and labels on PRs
if: github.event_name == 'pull_request'
id: meta-pr
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ env.OWNER_LOWERCASE }}/quartz
tags: |
type=raw,value=sha-${{ env.GITHUB_SHA_SHORT }}
labels: |
org.opencontainers.image.source="https://github.com/${{ github.repository_owner }}/quartz"
- name: Extract metadata tags and labels for main, release or tag
if: github.event_name != 'pull_request'
id: meta
uses: docker/metadata-action@v5
with:
flavor: |
latest=auto
images: ghcr.io/${{ env.OWNER_LOWERCASE }}/quartz
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}.{{minor}}.{{patch}}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
type=raw,value=sha-${{ env.GITHUB_SHA_SHORT }}
labels: |
maintainer=${{ github.repository_owner }}
org.opencontainers.image.source="https://github.com/${{ github.repository_owner }}/quartz"
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v6
with:
push: ${{ github.event_name != 'pull_request' }}
build-args: |
GIT_SHA=${{ env.GITHUB_SHA }}
DOCKER_LABEL=sha-${{ env.GITHUB_SHA_SHORT }}
tags: ${{ steps.meta.outputs.tags || steps.meta-pr.outputs.tags }}
labels: ${{ steps.meta.outputs.labels || steps.meta-pr.outputs.labels }}
cache-from: type=gha
cache-to: type=gha

View File

@@ -1 +1 @@
v20.9.0 v22.16.0

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

@@ -60,6 +60,6 @@
"repelStrength": 15.1642583672965, "repelStrength": 15.1642583672965,
"linkStrength": 0.975453871804372, "linkStrength": 0.975453871804372,
"linkDistance": 42, "linkDistance": 42,
"scale": 0.31674920955833996, "scale": 0.25087828132711953,
"close": true "close": true
} }

View File

@@ -1,5 +1,6 @@
{ {
"commitMessage": "[obsidian] vault backup: {{date}}", "commitMessage": "[obsidian] vault backup: {{date}}",
"autoCommitMessage": "[obsidian] vault backup: {{date}}[",
"commitDateFormat": "YYYY-MM-DD HH:mm:ss", "commitDateFormat": "YYYY-MM-DD HH:mm:ss",
"autoSaveInterval": 60, "autoSaveInterval": 60,
"autoPushInterval": 0, "autoPushInterval": 0,
@@ -8,6 +9,7 @@
"disablePush": false, "disablePush": false,
"pullBeforePush": true, "pullBeforePush": true,
"disablePopups": false, "disablePopups": false,
"showErrorNotices": true,
"disablePopupsForNoChanges": false, "disablePopupsForNoChanges": false,
"listChangedFilesInMessageBody": false, "listChangedFilesInMessageBody": false,
"showStatusBar": true, "showStatusBar": true,
@@ -29,30 +31,30 @@
"showFileMenu": true, "showFileMenu": true,
"authorInHistoryView": "hide", "authorInHistoryView": "hide",
"dateInHistoryView": false, "dateInHistoryView": false,
"diffStyle": "split",
"lineAuthor": { "lineAuthor": {
"show": false, "show": true,
"followMovement": "inactive", "followMovement": "inactive",
"authorDisplay": "initials", "authorDisplay": "hide",
"showCommitHash": false, "showCommitHash": false,
"dateTimeFormatOptions": "date", "dateTimeFormatOptions": "hide",
"dateTimeFormatCustomString": "YYYY-MM-DD HH:mm", "dateTimeFormatCustomString": "YYYY-MM-DD HH:mm",
"dateTimeTimezone": "viewer-local", "dateTimeTimezone": "viewer-local",
"coloringMaxAge": "1y", "coloringMaxAge": "1y",
"colorNew": { "colorNew": {
"r": 255, "r": 0,
"g": 150, "g": 128,
"b": 150 "b": 0
}, },
"colorOld": { "colorOld": {
"r": 120, "r": 255,
"g": 160, "g": 255,
"b": 255 "b": 255
}, },
"textColorCss": "var(--text-muted)", "textColorCss": "var(--text-muted)",
"ignoreWhitespace": false, "ignoreWhitespace": true,
"gutterSpacingFallbackLength": 15, "gutterSpacingFallbackLength": 0,
"lastShownAuthorDisplay": "initials", "lastShownAuthorDisplay": "initials",
"lastShownDateTimeFormatOptions": "date" "lastShownDateTimeFormatOptions": "date"
}, }
"autoCommitMessage": "[obsidian] vault backup: {{date}}["
} }

10
content/2hop-link.md Normal file
View File

@@ -0,0 +1,10 @@
---
date: 2025-08-20 16:38
---
#stub
[[Scrapbox]]で使われている仕組み
[2 hop linkを理解してなかった - 井戸端](https://scrapbox.io/villagepump/2_hop_link%E3%82%92%E7%90%86%E8%A7%A3%E3%81%97%E3%81%A6%E3%81%AA%E3%81%8B%E3%81%A3%E3%81%9F)

4
content/Befunge.md Normal file
View File

@@ -0,0 +1,4 @@
---
date: 2025-08-20 16:18
---
#esolang

8
content/BrainSynth.md Normal file
View File

@@ -0,0 +1,8 @@
---
date: 2025-08-20 16:19
---
#programming #music #esolang
[[BrainFuck]]で音楽を記述しようという試み
[Brainsynth](https://nexuist.github.io/brainsynth/)

View File

@@ -11,7 +11,7 @@ C言語っぽいシンタックスでサンプル単位での正確なスケジ
命令型+クラスを作ったりのオブジェクト指向(継承もあり)。 命令型+クラスを作ったりのオブジェクト指向(継承もあり)。
shredという論理時間ベースの計量スレッドみたいなものを言語内からスポーン、もしくはコマンドから立ち上げることができる。1つのファイルをスレッド単位で立ち上げたり殺したりをリアルタイムで切り替えることで、ライブコーディングを実現しているただし更新のタイミングでディレイやリバーブのテールは切れる shredという論理時間ベースの計量スレッドみたいなものを言語内からスポーン、もしくはコマンドから立ち上げることができる。1つのファイルをスレッド単位で立ち上げたり殺したりをリアルタイムで切り替えることで、[[ライブコーディング]]を実現している(ただし更新のタイミングでディレイやリバーブのテールは切れる)。
最近も活発に更新が進んでいて、モジュール機能などが導入された。 最近も活発に更新が進んでいて、モジュール機能などが導入された。

View File

@@ -4,6 +4,6 @@ date: 2025-01-15 15:29
#programming-language #computermusic #livecoding #programming-language #computermusic #livecoding
[[Rust]]製のライブコーディング環境。 [[Rust]]製の[[ライブコーディング]]環境。[[VST]]プラグインのエンジンとしてや、[[Bela]]に乗せたりもできるらしい
[GitHub - chaosprint/glicol: Graph-oriented live coding language and music/audio DSP library written in Rust](https://github.com/chaosprint/glicol) [GitHub - chaosprint/glicol: Graph-oriented live coding language and music/audio DSP library written in Rust](https://github.com/chaosprint/glicol)

View File

@@ -1,4 +1,4 @@
--- ---
date: 2025-01-15 16:52 date: 2025-01-15 16:52
--- ---
#stub #person

View File

@@ -0,0 +1,30 @@
---
date: 2025-08-20 15:12
---
#programming-language #research
[[Caleb Reach]]による、信号処理エンジンの[[ライブコーディング]]のための手法の提案。
[49.dafx2013\_submission\_60.pdf](https://www.dafx.de/paper-archive/2013/papers/49.dafx2013_submission_60.pdf)
[[ChucK]]や[[Faust]]など、ライブコーディングでシグナルグラフを丸ごと作り直すタイプの言語では、ディレイやリバーブのテールがコードの再コンパイル時に状態が吹き飛ぶためぶった切れる問題がある。
[[Functional Reactive Programming]]の手法で、各項(状態を持つプリミティブ)毎のユニークなラベルをつけて、再コンパイル時に状態を持ち越せるところでは持ち越せるというやり方を提案している
ただ、単純なテキスト同士の差分比較ではユニークなラベルの増分更新は意外と難しく、例えば
`(define x (+ (f 2) (f 3) 2))`
`(define x (+ (f 5) (f 2) (f 3) 2))`
に更新されたとき、ぱっと見は「`(f 2) (f 3)`の手前に新たに`(f 5)`が挿入された」と考えることができるが、原理的には「`(f 2) (f 3)`をまず`(f 5) (f 2)`へ書き換え、後ろに`(f 3)`を挿入した」と区別することはできない。そうなると新たに挿入された項だけ0で初期化して新たにアロケートする、となった時に、どこを0初期化すべきか決定できない。
きちんと文脈を持ち越すためにはエディタレベルでの編集履歴と突き合わせる連携が必要となる(論文中では[[Emacs]]の拡張として実装しているらしい)
この言語における関数定義と適用はあくまでコンパイル時計算(+や`*`は状態を持たないランタイム時に実行される計算ノード)

View File

@@ -9,7 +9,7 @@ date: "2023-09-12T15:40:18+0900"
JITコンパイラで超高速に動く別の処理系[[LuaJIT]]もある JITコンパイラで超高速に動く別の処理系[[LuaJIT]]もある
Luaでオーディオビジュアルライブコーディングをするための[[LuaAV]]などがある(更新されてないけど) Luaでオーディオビジュアル[[ライブコーディング]]をするための[[LuaAV]]などがある(更新されてないけど)
動的型付けで手続き型っぽいが、関数を第一級オブジェクトとして扱える。そのためクロージャを使用できるが、この実装にupvalueという方式を採用していることで有名 動的型付けで手続き型っぽいが、関数を第一級オブジェクトとして扱える。そのためクロージャを使用できるが、この実装にupvalueという方式を採用していることで有名

4
content/LuaAV.md Normal file
View File

@@ -0,0 +1,4 @@
---
date: 2025-08-20 16:09
---
#stub

6
content/MML.md Normal file
View File

@@ -0,0 +1,6 @@
---
date: 2025-08-20 16:12
---
#programming-language #music
[[Music Macro Language]]

View File

@@ -1,4 +1,4 @@
--- ---
date: 2025-01-15 18:39 date: 2025-01-15 18:39
--- ---
#stub #programming-language #music

10
content/NoiseFunge.md Normal file
View File

@@ -0,0 +1,10 @@
---
date: 2025-08-20 16:20
---
#programming-language #music
[[Befunge]]をベースにした音楽[[ライブコーディング]]環境。
[[ORCA]]よりももっとBefungeに近い仕組みっぽい
[GitHub - revnull/noisefunge.rs: A reimplementation of noisefunge in rust.](https://github.com/revnull/noisefunge.rs)

12
content/ORCA.md Normal file
View File

@@ -0,0 +1,12 @@
---
date: 2025-08-20 16:17
---
#programming-language #music
[[HundredRabbits]]が作ったインタラクティブ音楽環境。
[GitHub - hundredrabbits/Orca: Esoteric Programming Language](https://github.com/hundredrabbits/Orca)
[[Befunge]]を参考にした、二次元グリッド状にそれぞれの機能を持つ文字を割り当てて音楽(というかMIDIや[[OSC]]のシーケンス)を生成する。

7
content/OSC.md Normal file
View File

@@ -0,0 +1,7 @@
---
date: 2025-08-20 16:02
---
#redirect
[[Open Sound Control]]

View File

@@ -30,7 +30,7 @@ vaultというコンテンツフォルダの中に環境設定も丸ごと含め
- obsidian-git - obsidian-git
- 30分間隔作業停止から30分後にバックアップ - 30分間隔作業停止から30分後にバックアップ
- [GitHub - L7Cy/obsidian-2hop-links-plus: Related links up to 2 hops away are displayed in a card format.](https://github.com/L7Cy/obsidian-2hop-links-plus) - [GitHub - L7Cy/obsidian-2hop-links-plus: Related links up to 2 hops away are displayed in a card format.](https://github.com/L7Cy/obsidian-2hop-links-plus)
- Scrapbox風に2hop先のリンクと未作成のリンクを表示してくれる - Scrapbox風に[[2hop-link]]と未作成のリンクを表示してくれる
- [GitHub - zolrath/obsidian-auto-link-title: Automatically fetch the titles of pasted links](https://github.com/zolrath/obsidian-auto-link-title) - [GitHub - zolrath/obsidian-auto-link-title: Automatically fetch the titles of pasted links](https://github.com/zolrath/obsidian-auto-link-title)
- OGPから自動でタイトル取得してくれる。 - OGPから自動でタイトル取得してくれる。

View File

@@ -0,0 +1,17 @@
---
date: 2025-08-20 16:30
---
#memo
[[Obsidian]]や[[Quartz]]ではいい感じの記事同士のつながりが可視化されたグラフが見られるけど、そこから新しい発見が得られた試しがない
[[ハイパーリンクでは関係性に名前をつけられない]]
[綺麗に並んでいて、綺麗ですね - 橋本商会](https://scrapbox.io/shokai/%E7%B6%BA%E9%BA%97%E3%81%AB%E4%B8%A6%E3%82%93%E3%81%A7%E3%81%84%E3%81%A6%E3%80%81%E7%B6%BA%E9%BA%97%E3%81%A7%E3%81%99%E3%81%AD)
[死んだテキストを置いておく倉庫にしない - 橋本商会](https://scrapbox.io/shokai/%E6%AD%BB%E3%82%93%E3%81%A0%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E3%82%92%E7%BD%AE%E3%81%84%E3%81%A6%E3%81%8A%E3%81%8F%E5%80%89%E5%BA%AB%E3%81%AB%E3%81%97%E3%81%AA%E3%81%84#5969b92897c2910000f323c1)
---
Imcoming/Outgoing Linkの代用になりそうなビューなら悪くないのかも[[2hop-link]])的な機能も含め

View File

@@ -0,0 +1,4 @@
---
date: 2025-08-20 16:02
---
#protocol

6
content/PMML.md Normal file
View File

@@ -0,0 +1,6 @@
---
date: 2025-08-20 16:14
---
#programming-language #research #computermusic
[PMML: A Music Description Language Supporting Algorithmic Representation of Musical Expression \| Semantic Scholar](https://www.semanticscholar.org/paper/PMML%3A-A-Music-Description-Language-Supporting-of-Nishimura/d73590abc307ea5e1f4e0444968ea3f7ae075389)

View File

@@ -12,7 +12,7 @@ https://scrapbox.io/
- Wikiリンク記法でページ間をシームレスに接続 - Wikiリンク記法でページ間をシームレスに接続
- 箇条書きベースのシンプルな記法 - 箇条書きベースのシンプルな記法
- ページの存在に関わらず自由にリンク作成が可能 - ページの存在に関わらず自由にリンク作成が可能
- 2ホップリンクによる関連情報の発見 - [[2hop-link]]による関連情報の発見
- 非同期のリアルタイム編集 - 非同期のリアルタイム編集
## 活用例 ## 活用例

4
content/Smalltalk.md Normal file
View File

@@ -0,0 +1,4 @@
---
date: 2025-08-20 16:02
---
#programming-language

View File

@@ -9,7 +9,7 @@ date: "2024-02-06T02:00:06+0900"
言語のフロントエンドは[[Ruby]]、オーディオ合成エンジンは[[SuperCollider]]、イベントスケジューリングには[[Erlang]]のバックエンドになっている[[BEAM VM]]を使ってる。 言語のフロントエンドは[[Ruby]]、オーディオ合成エンジンは[[SuperCollider]]、イベントスケジューリングには[[Erlang]]のバックエンドになっている[[BEAM VM]]を使ってる。
最近新バージョン(というか別プロジェクト)[[Luerl]]というErlang上で実装された[[Lua]]と、独自のDSLであるTau5Langというのをサポート予定らしい。 最近新バージョン(というか別プロジェクト)の[[Tau5]]という言語では、[[Luerl]]というErlang上で実装された[[Lua]]と、独自のDSLであるTau5Langというのをサポート予定らしい。
[GitHub - samaaron/tau5: Code. Music. Live.](https://github.com/samaaron/tau5) [GitHub - samaaron/tau5: Code. Music. Live.](https://github.com/samaaron/tau5)

View File

@@ -7,8 +7,8 @@ date: "2024-02-06T02:00:06+0900"
[index | SuperCollider](https://supercollider.github.io/) [index | SuperCollider](https://supercollider.github.io/)
もともとMax上で動くPyriteというスクリプティング言語と、Synth-O-Maticというソフトウェアシンセサイザーがあって、それを合体させる形で作られた。 もともと[[Max]]上で動くPyriteというスクリプティング言語と、Synth-O-Maticというソフトウェアシンセサイザーがあって、それを合体させる形で作られた。
Smalltalkベースのシンタックスを持つオブジェクト指向言語で、演算子の優先順位がなく全てが左結合だったりと文法が独特。バージョン3からは言語インタプリタ部分(sclang)とシンセサイザー部分(scsynth)が別プロセスとして動いており、[[OSC]]フォーマットで通信して動いている。[[TidalCycles]]や[[Sonic Pi]]、[[FoxDot]]や[[Overtone]]などは、音声合成エンジンとしてscsynthを利用している。 [[Smalltalk]]ベースのシンタックスを持つオブジェクト指向言語で、演算子の優先順位がなく全てが左結合だったりと文法が独特。バージョン3からは言語インタプリタ部分(sclang)とシンセサイザー部分(scsynth)が別プロセスとして動いており、[[OSC]]フォーマットで通信して動いている。[[TidalCycles]]や[[Sonic Pi]]、[[FoxDot]]や[[Overtone]]などは、音声合成エンジンとしてscsynthを利用している。
言語はコンカレントGCが走っていたりとかなり複雑な内部実装。実装している中の人にBoostの開発者の中の人が混ざってたりするのでめっちゃ高度なことをやっている。その分Webに移植とかは今後もハードルが高そう。 言語はコンカレントGCが走っていたりとかなり複雑な内部実装。実装している中の人にBoostの開発者の中の人が混ざってたりするのでめっちゃ高度なことをやっている。その分Webに移植とかは今後もハードルが高そう。

13
content/Takt.md Normal file
View File

@@ -0,0 +1,13 @@
---
date: 2025-08-20 16:13
---
#programming-language #music
[[西村憲]]が開発した[[MML]]ベースの[[音楽プログラミング言語]]。
[音楽記述言語「Takt」と生成音楽](http://fkeizai.in.arena.ne.jp/wordpress/wp-content/uploads/2016/01/kenkyu_2016_06_1.pdf)
[高機能な音楽プログラミング言語Takt - 丸井綜研](https://marui.hatenablog.com/entry/2014/09/14/150337)
[Takt download \| SourceForge.net](https://sourceforge.net/projects/takt/)

View File

@@ -18,7 +18,7 @@ sine1 = nil
``` ```
[[SuperCollider]]でも似たような話だけど、ライブコーディング的なシステムを作ろうと思うとこういう制御構造→命令列への変換を定式化するのが重要なのでは [[SuperCollider]]でも似たような話だけど、[[ライブコーディング]]的なシステムを作ろうと思うとこういう制御構造→命令列への変換を定式化するのが重要なのでは
ジェネラティブUGenネットワークグラフの生成もここに含まれる ジェネラティブUGenネットワークグラフの生成もここに含まれる

View File

@@ -3,6 +3,6 @@ date: 2025-06-12 10:32
--- ---
#programming-language #programming-language
[[Max]]上で動作するJSベースのライブコーディング用スクリプティング言語 [[Max]]上で動作するJSベースの[[ライブコーディング]]用スクリプティング言語
[GitHub - nnirror/facet: Live coding and synthesis with NodeJS and a browser](https://github.com/nnirror/facet) [GitHub - nnirror/facet: Live coding and synthesis with NodeJS and a browser](https://github.com/nnirror/facet)

View File

@@ -75,4 +75,4 @@ https://github.com/mimium-org/mimium-rs
- [[mimiumでのバリアント]]:やっぱり必要、だが分割コンパイルを先にやらないとダメかも - [[mimiumでのバリアント]]:やっぱり必要、だが分割コンパイルを先にやらないとダメかも
- モジュールシステム名前空間のCライブラリレベルでのマングリングとか考える必要ありそう。 - モジュールシステム名前空間のCライブラリレベルでのマングリングとか考える必要ありそう。
- 配列型の実装GC問題片付けばなんとか固定長と可変長の切り替えをどうするかを考えたい。 - 配列型の実装GC問題片付けばなんとか固定長と可変長の切り替えをどうするかを考えたい。
- OSCの実装 - [[OSC]]の実装

View File

@@ -180,3 +180,7 @@ fn alloc_midi_voice_4(channel){
} }
``` ```
[[Computation Expression]]使ってIOの値を`let!`でバインドしていく、とかの方がわかりやすいかも

View File

@@ -3,6 +3,8 @@ date: 2025-06-27 16:54
--- ---
#mimium #livecoding #mimium #livecoding
mimiumでも[[ライブコーディング]]がやりたいよね
一般化するとこういうモデルにならんだろうか 一般化するとこういうモデルにならんだろうか
![[img/general-livecoding-model.png]] ![[img/general-livecoding-model.png]]
@@ -22,3 +24,60 @@ ChucKではエフェクトのテールが更新時にぶちぎれる問題があ
...これ、結局[[SuperCollider]]のJITLibと同じことかもな ...これ、結局[[SuperCollider]]のJITLibと同じことかもな
[jitlib\_basic\_concepts\_01 \| SuperCollider 3.14.0-dev Help](https://doc.sccode.org/Tutorials/JITLib/jitlib_basic_concepts_01.html) [jitlib\_basic\_concepts\_01 \| SuperCollider 3.14.0-dev Help](https://doc.sccode.org/Tutorials/JITLib/jitlib_basic_concepts_01.html)
## 信号処理の状態持ち越し
[[Incremental Functional Reactive Programming]]の仕組みを用いることはできるだろうか
```rust
//これが
fn dsp(){
delay(100,100,input)+ delay(200,200,input)
}
//こう更新されるとする
fn dsp(){
delay(300,300,input) + delay(100,100,input)+ delay(200,200,input)
}
```
`delay(100)`にはID(delay_1)が、`delay(200)`にはID(delay_2)がそれぞれつけられる
どう増分検出してID割り振るかの仕組みは置いといて、再コンパイル時に100と200はそのまま、新しく増えたdelay(300)にID`(delay_3)`を挿入する
そうすると、StateStackのためのメモリは完全にリニアにはしないほうが良くて、ツリー状のデータ構造に戻した方が良さそう
[[Red Green Syntax Tree]]のGreen Node→状態メモリのマップだと考えるとわかりやすいのか
```
dsp
| \
delay(1) delay(2)
```
から
```
dsp
/ | \
delay(3)delay(1) delay(2)
```
に更新される
```rust
enum StateLeaf{
Nested(Box<StateTree>),
Delay(usize),
Feedback(usize),
}
struct StateTree{
children: Vec<StateLeaf>
}
```
クロージャの呼び出しの時は?
コンパイル時ではなく、Closure命令でクロージャが作られる際にアロケートとIDの振り分けが起きる
クロージャ

14
content/テロメア.md Normal file
View File

@@ -0,0 +1,14 @@
---
date: 2025-08-20 16:48
---
[テロメア - 橋本商会](https://scrapbox.io/shokai/%E3%83%86%E3%83%AD%E3%83%A1%E3%82%A2)
[[Scrapbox]]に入っている、行毎の更新履歴を視覚化する仕組み
[[Obsidian]]と[[Quartz]]にも入れたい
Obsidianに関してはGit Pluginの[Line Authoring](https://publish.obsidian.md/git-doc/Line+Authoring)機能でなんとなく似た感じにはできるんだけど、1年以上前かそれ以外ぐらいでの色分けしかできない、デフォルトだと文字がうるさい日付も文字も消してしまうと多少快適

View File

@@ -1,7 +1,7 @@
--- ---
date: 2025-04-22 15:39 date: 2025-04-22 15:39
--- ---
#reseach #dssc #semiconductor #research #dssc #semiconductor
## 陰極の作成 ## 陰極の作成

6
content/西村憲.md Normal file
View File

@@ -0,0 +1,6 @@
---
date: 2025-08-20 16:14
---
#person

View File

@@ -10,9 +10,10 @@ date: "2024-01-05T17:15:38+0900"
--- ---
## 一覧 ## 一覧(現行で生きているもの)
- [[Max]] - [[Max]]
- [[facet]]
- [[PureData]] - [[PureData]]
- [[SuperCollider]] - [[SuperCollider]]
- [[Sonic Pi]] - [[Sonic Pi]]
@@ -21,28 +22,47 @@ date: "2024-01-05T17:15:38+0900"
- [[FoxDot]] - [[FoxDot]]
- [[sapf]] - [[sapf]]
- [[Csound]] - [[Csound]]
- [[MPEG4 Structured Audio]]
- [[Extempore]] - [[Extempore]]
- [[ChucK]] - [[ChucK]]
- [[Faust]] - [[Faust]]
- [[Cmajor]] - [[Cmajor]]
- [[Soul]]
- [[Kronos]]
- [[Veneer]]
- [[Vult]] - [[Vult]]
- [[Gwion]] - [[Gwion]]
- [[Glicol]] - [[Glicol]]
- [[ORCA]]
- [[Omni]] - [[Omni]]
- [[Nyquist]]
- [[Arco]](Serpent) - [[Arco]](Serpent)
- [[Wax]]
## MIDIオンリー、[[MML]]系
- [[ストトン記法]]
- [[すたどんたん]]
- [[テキスト音楽サクラ]]
- [[PMML]]
- [[Takt]]
--- ---
## 歴史的なやつ
- [[MUSIC N]]
- [[MUSIGOL]]
- [[MUS10]]
- [[GROOVE]]
- [[4CED]]
- [[PILE]]
- [[Chronic]]
- [[LuaAV]]
[[音楽プログラミング言語の形式化]] [[音楽プログラミング言語の形式化]]
[[多段階計算]]
[[Rustでの評価文脈環境のRAIIっぽい実装]]
[[Lua]]
[[Cranelift]]
[[竹内関数で音楽]] [[竹内関数で音楽]]
[[Bytebeat]] [[Bytebeat]]

3
index.d.ts vendored
View File

@@ -5,8 +5,11 @@ 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" }>
readermodechange: CustomEvent<{ mode: "on" | "off" }>
} }
type ContentIndex = Record<FullSlug, ContentDetails>
declare const fetchData: Promise<ContentIndex> declare const fetchData: Promise<ContentIndex>

3802
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "@jackyzha0/quartz", "name": "@jackyzha0/quartz",
"description": "🌱 publish your digital garden and notes as a website", "description": "🌱 publish your digital garden and notes as a website",
"private": true, "private": true,
"version": "4.3.0", "version": "4.5.1",
"type": "module", "type": "module",
"author": "jackyzha0 <j.zhao2k19@gmail.com>", "author": "jackyzha0 <j.zhao2k19@gmail.com>",
"license": "MIT", "license": "MIT",
@@ -16,12 +16,12 @@
"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": {
"npm": ">=9.3.1", "npm": ">=10.9.2",
"node": "20 || >=22" "node": ">=22"
}, },
"keywords": [ "keywords": [
"site generator", "site generator",
@@ -35,76 +35,81 @@
"quartz": "./quartz/bootstrap-cli.mjs" "quartz": "./quartz/bootstrap-cli.mjs"
}, },
"dependencies": { "dependencies": {
"@clack/prompts": "^0.7.0", "@clack/prompts": "^0.11.0",
"@floating-ui/dom": "^1.6.8", "@floating-ui/dom": "^1.7.0",
"@napi-rs/simple-git": "0.1.16", "@myriaddreamin/rehype-typst": "^0.6.0",
"@napi-rs/simple-git": "0.1.19",
"@tweenjs/tween.js": "^25.0.0",
"@webgpu/types": "^0.1.61",
"ansi-truncate": "^1.2.0",
"async-mutex": "^0.5.0", "async-mutex": "^0.5.0",
"chalk": "^5.3.0", "chokidar": "^4.0.3",
"chokidar": "^3.6.0",
"cli-spinner": "^0.2.10", "cli-spinner": "^0.2.10",
"d3": "^7.9.0", "d3": "^7.9.0",
"esbuild-sass-plugin": "^2.16.1", "esbuild-sass-plugin": "^3.3.1",
"flexsearch": "0.7.43", "flexsearch": "0.7.43",
"github-slugger": "^2.0.0", "github-slugger": "^2.0.0",
"globby": "^14.0.2", "globby": "^14.1.0",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"hast-util-to-html": "^9.0.1", "hast-util-to-html": "^9.0.5",
"hast-util-to-jsx-runtime": "^2.3.0", "hast-util-to-jsx-runtime": "^2.3.6",
"hast-util-to-string": "^3.0.0", "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.25.1", "lightningcss": "^1.30.1",
"mdast-util-find-and-replace": "^3.0.1", "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",
"micromorph": "^0.4.5", "micromorph": "^0.4.5",
"preact": "^10.22.1", "minimatch": "^10.0.1",
"preact-render-to-string": "^6.5.7", "pixi.js": "^8.9.2",
"pretty-bytes": "^6.1.1", "preact": "^10.26.7",
"preact-render-to-string": "^6.5.13",
"pretty-bytes": "^7.0.0",
"pretty-time": "^1.1.0", "pretty-time": "^1.1.0",
"reading-time": "^1.5.0", "reading-time": "^1.5.0",
"rehype-autolink-headings": "^7.1.0", "rehype-autolink-headings": "^7.1.0",
"rehype-citation": "^2.0.0", "rehype-citation": "^2.3.1",
"rehype-katex": "^7.0.0", "rehype-katex": "^7.0.1",
"rehype-mathjax": "^6.0.0", "rehype-mathjax": "^7.1.0",
"rehype-pretty-code": "^0.13.2", "rehype-pretty-code": "^0.14.1",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"rehype-slug": "^6.0.0", "rehype-slug": "^6.0.0",
"remark": "^15.0.1", "remark": "^15.0.1",
"remark-breaks": "^4.0.0", "remark-breaks": "^4.0.0",
"remark-frontmatter": "^5.0.0", "remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.1",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"remark-parse": "^11.0.0", "remark-parse": "^11.0.0",
"remark-rehype": "^11.1.0", "remark-rehype": "^11.1.2",
"remark-smartypants": "^3.0.2", "remark-smartypants": "^3.0.2",
"rfdc": "^1.4.1", "rfdc": "^1.4.1",
"rimraf": "^6.0.1", "satori": "^0.13.1",
"serve-handler": "^6.1.5", "serve-handler": "^6.1.6",
"shiki": "^1.10.3", "sharp": "^0.34.2",
"shiki": "^1.26.2",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"to-vfile": "^8.0.0", "to-vfile": "^8.0.0",
"toml": "^3.0.0", "toml": "^3.0.0",
"unified": "^11.0.4", "unified": "^11.0.5",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0",
"vfile": "^6.0.2", "vfile": "^6.0.3",
"workerpool": "^9.1.3", "workerpool": "^9.2.0",
"ws": "^8.18.0", "ws": "^8.18.2",
"yargs": "^17.7.2" "yargs": "^18.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/cli-spinner": "^0.2.3",
"@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.1.0", "@types/node": "^22.15.23",
"@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.12", "@types/ws": "^8.18.1",
"@types/yargs": "^17.0.32", "@types/yargs": "^17.0.33",
"esbuild": "^0.19.9", "esbuild": "^0.25.5",
"prettier": "^3.3.3", "prettier": "^3.5.3",
"tsx": "^4.16.2", "tsx": "^4.19.4",
"typescript": "^5.5.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -2,13 +2,14 @@ import { QuartzConfig } from "./quartz/cfg"
import * as Plugin from "./quartz/plugins" import * as Plugin from "./quartz/plugins"
/** /**
* Quartz 4.0 Configuration * Quartz 4 Configuration
* *
* See https://quartz.jzhao.xyz/configuration for more information. * See https://quartz.jzhao.xyz/configuration for more information.
*/ */
const config: QuartzConfig = { const config: QuartzConfig = {
configuration: { configuration: {
pageTitle: "Matsuura Tomoya Research Note", pageTitle: "Matsuura Tomoya Research Note",
pageTitleSuffix: "",
enableSPA: true, enableSPA: true,
enablePopovers: true, enablePopovers: true,
locale: "ja-JP", locale: "ja-JP",
@@ -24,8 +25,8 @@ const config: QuartzConfig = {
fontOrigin: "googleFonts", fontOrigin: "googleFonts",
cdnCaching: true, cdnCaching: true,
typography: { typography: {
header: "Schibsted Grotesk", header: "Noto Sans JP",
body: "Source Sans Pro", body: "Noto Sans JP",
code: "IBM Plex Mono", code: "IBM Plex Mono",
}, },
colors: { colors: {
@@ -58,7 +59,7 @@ const config: QuartzConfig = {
transformers: [ transformers: [
Plugin.FrontMatter(), Plugin.FrontMatter(),
Plugin.CreatedModifiedDate({ Plugin.CreatedModifiedDate({
priority: ["frontmatter", "git",], // you can add 'git' here for last modified from Git but this makes the build slower priority: ["frontmatter", "git", "filesystem"],
}), }),
Plugin.Latex({ renderEngine: "katex" }), Plugin.Latex({ renderEngine: "katex" }),
Plugin.SyntaxHighlighting({ Plugin.SyntaxHighlighting({
@@ -88,7 +89,10 @@ const config: QuartzConfig = {
}), }),
Plugin.Assets(), Plugin.Assets(),
Plugin.Static(), Plugin.Static(),
Plugin.Favicon(),
Plugin.NotFoundPage(), Plugin.NotFoundPage(),
// Comment out CustomOgImages to speed up build time
Plugin.CustomOgImages(),
], ],
}, },
} }

View File

@@ -8,10 +8,10 @@ export const sharedPageComponents: SharedLayout = {
afterBody: [], afterBody: [],
footer: Component.Footer({ footer: Component.Footer({
links: { links: {
"Top": "https://matsuuratomoya.com", Top: "https://matsuuratomoya.com",
"Mastodon": "https://social.matsuuratomoya.com/@tomoya", Mastodon: "https://social.matsuuratomoya.com/@tomoya",
"Twitter": "https://twitter.com/tomoya_nonymous", Twitter: "https://twitter.com/tomoya_nonymous",
"GitHub": "https://github.com/tomoyanonymous", GitHub: "https://github.com/tomoyanonymous",
}, },
}), }),
} }
@@ -40,18 +40,37 @@ const graph_cfg = {
fontSize: 0.6, fontSize: 0.6,
opacityScale: 1, opacityScale: 1,
}, },
}; }
export const defaultContentPageLayout: PageLayout = { export const defaultContentPageLayout: PageLayout = {
beforeBody: [Component.Breadcrumbs(), beforeBody: [
Component.ArticleTitle(), Component.ContentMeta(), /*Component.TagList()*/], Component.ConditionalRender({
component: Component.Breadcrumbs(),
condition: (page) => page.fileData.slug !== "index",
}),
Component.ArticleTitle(),
Component.ContentMeta() /*Component.TagList()*/,
],
left: [ left: [
Component.PageTitle(), Component.PageTitle(),
Component.MobileOnly(Component.Spacer()), Component.MobileOnly(Component.Spacer()),
Component.Search(), Component.Flex({
Component.Darkmode(), components: [
{
Component: Component.Search(),
grow: true,
},
{ Component: Component.Darkmode() },
{ Component: Component.ReaderMode() },
],
}),
// Component.Explorer(),
Component.DesktopOnly(Component.TableOfContents()), Component.DesktopOnly(Component.TableOfContents()),
], ],
right: [Component.Graph(graph_cfg), Component.Backlinks(), Component.RecentNotes({ title: "Recent writing" })], right: [
Component.Graph(graph_cfg),
Component.Backlinks(),
Component.RecentNotes({ title: "Recent writing" }),
],
} }
// components for pages that display lists of pages (e.g. tags or folders) // components for pages that display lists of pages (e.g. tags or folders)
@@ -60,9 +79,16 @@ export const defaultListPageLayout: PageLayout = {
left: [ left: [
Component.PageTitle(), Component.PageTitle(),
Component.MobileOnly(Component.Spacer()), Component.MobileOnly(Component.Spacer()),
Component.Search(), Component.Flex({
Component.Darkmode(), components: [
Component.DesktopOnly(Component.Explorer()), {
Component: Component.Search(),
grow: true,
},
{ Component: Component.Darkmode() },
],
}),
Component.Explorer(),
], ],
right: [ right: [
Component.Graph(), Component.Graph(),

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env node #!/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

@@ -1,7 +1,8 @@
#!/usr/bin/env node #!/usr/bin/env node
import workerpool from "workerpool" import workerpool from "workerpool"
const cacheFile = "./.quartz-cache/transpiled-worker.mjs" const cacheFile = "./.quartz-cache/transpiled-worker.mjs"
const { parseFiles } = await import(cacheFile) const { parseMarkdown, processHtml } = await import(cacheFile)
workerpool.worker({ workerpool.worker({
parseFiles, parseMarkdown,
processHtml,
}) })

View File

@@ -2,14 +2,14 @@ import sourceMapSupport from "source-map-support"
sourceMapSupport.install(options) sourceMapSupport.install(options)
import path from "path" import path from "path"
import { PerfTimer } from "./util/perf" import { PerfTimer } from "./util/perf"
import { rimraf } from "rimraf" import { rm } from "fs/promises"
import { GlobbyFilterFunction, isGitIgnored } from "globby" import { GlobbyFilterFunction, isGitIgnored } from "globby"
import chalk from "chalk" import { styleText } from "util"
import { parseMarkdown } from "./processors/parse" import { parseMarkdown } from "./processors/parse"
import { filterContent } from "./processors/filter" import { filterContent } from "./processors/filter"
import { emitContent } from "./processors/emit" import { emitContent } from "./processors/emit"
import cfg from "../quartz.config" import cfg from "../quartz.config"
import { FilePath, FullSlug, joinSegments, slugifyFilePath } from "./util/path" import { FilePath, joinSegments, slugifyFilePath } from "./util/path"
import chokidar from "chokidar" import chokidar from "chokidar"
import { ProcessedContent } from "./plugins/vfile" import { ProcessedContent } from "./plugins/vfile"
import { Argv, BuildCtx } from "./util/ctx" import { Argv, BuildCtx } from "./util/ctx"
@@ -17,32 +17,39 @@ import { glob, toPosixPath } from "./util/glob"
import { trace } from "./util/trace" import { trace } from "./util/trace"
import { options } from "./util/sourcemap" import { options } from "./util/sourcemap"
import { Mutex } from "async-mutex" import { Mutex } from "async-mutex"
import DepGraph from "./depgraph"
import { getStaticResourcesFromPlugins } from "./plugins" import { getStaticResourcesFromPlugins } from "./plugins"
import { randomIdNonSecure } from "./util/random"
import { ChangeEvent } from "./plugins/types"
import { minimatch } from "minimatch"
type Dependencies = Record<string, DepGraph<FilePath> | null> type ContentMap = Map<
FilePath,
| {
type: "markdown"
content: ProcessedContent
}
| {
type: "other"
}
>
type BuildData = { type BuildData = {
ctx: BuildCtx ctx: BuildCtx
ignored: GlobbyFilterFunction ignored: GlobbyFilterFunction
mut: Mutex mut: Mutex
initialSlugs: FullSlug[] contentMap: ContentMap
// TODO merge contentMap and trackedAssets changesSinceLastBuild: Record<FilePath, ChangeEvent["type"]>
contentMap: Map<FilePath, ProcessedContent>
trackedAssets: Set<FilePath>
toRebuild: Set<FilePath>
toRemove: Set<FilePath>
lastBuildMs: number lastBuildMs: number
dependencies: Dependencies
} }
type FileEvent = "add" | "change" | "delete"
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: randomIdNonSecure(),
argv, argv,
cfg, cfg,
allSlugs: [], allSlugs: [],
allFiles: [],
incremental: false,
} }
const perf = new PerfTimer() const perf = new PerfTimer()
@@ -60,69 +67,77 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
const release = await mut.acquire() const release = await mut.acquire()
perf.addEvent("clean") perf.addEvent("clean")
await rimraf(path.join(output, "*"), { glob: true }) await rm(output, { recursive: true, force: true })
console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`) console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`)
perf.addEvent("glob") perf.addEvent("glob")
const allFiles = await glob("**/*.*", argv.directory, cfg.configuration.ignorePatterns) const allFiles = await glob("**/*.*", argv.directory, cfg.configuration.ignorePatterns)
const fps = allFiles.filter((fp) => fp.endsWith(".md")).sort() const markdownPaths = allFiles.filter((fp) => fp.endsWith(".md")).sort()
console.log( console.log(
`Found ${fps.length} input files from \`${argv.directory}\` in ${perf.timeSince("glob")}`, `Found ${markdownPaths.length} input files from \`${argv.directory}\` in ${perf.timeSince("glob")}`,
) )
const filePaths = fps.map((fp) => joinSegments(argv.directory, fp) as FilePath) const filePaths = markdownPaths.map((fp) => joinSegments(argv.directory, fp) as FilePath)
ctx.allFiles = allFiles
ctx.allSlugs = allFiles.map((fp) => slugifyFilePath(fp as FilePath)) ctx.allSlugs = allFiles.map((fp) => slugifyFilePath(fp as FilePath))
const parsedFiles = await parseMarkdown(ctx, filePaths) const parsedFiles = await parseMarkdown(ctx, filePaths)
const filteredContent = filterContent(ctx, parsedFiles) const filteredContent = filterContent(ctx, parsedFiles)
const dependencies: Record<string, DepGraph<FilePath> | null> = {}
// Only build dependency graphs if we're doing a fast rebuild
if (argv.fastRebuild) {
const staticResources = getStaticResourcesFromPlugins(ctx)
for (const emitter of cfg.plugins.emitters) {
dependencies[emitter.name] =
(await emitter.getDependencyGraph?.(ctx, filteredContent, staticResources)) ?? null
}
}
await emitContent(ctx, filteredContent) await emitContent(ctx, filteredContent)
console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`)) console.log(
styleText("green", `Done processing ${markdownPaths.length} files in ${perf.timeSince()}`),
)
release() release()
if (argv.serve) { if (argv.watch) {
return startServing(ctx, mut, parsedFiles, clientRefresh, dependencies) ctx.incremental = true
return startWatching(ctx, mut, parsedFiles, clientRefresh)
} }
} }
// setup watcher for rebuilds // setup watcher for rebuilds
async function startServing( async function startWatching(
ctx: BuildCtx, ctx: BuildCtx,
mut: Mutex, mut: Mutex,
initialContent: ProcessedContent[], initialContent: ProcessedContent[],
clientRefresh: () => void, clientRefresh: () => void,
dependencies: Dependencies, // emitter name: dep graph
) { ) {
const { argv } = ctx const { argv, allFiles } = ctx
// cache file parse results const contentMap: ContentMap = new Map()
const contentMap = new Map<FilePath, ProcessedContent>() for (const filePath of allFiles) {
for (const content of initialContent) { contentMap.set(filePath, {
const [_tree, vfile] = content type: "other",
contentMap.set(vfile.data.filePath!, content) })
} }
for (const content of initialContent) {
const [_tree, vfile] = content
contentMap.set(vfile.data.relativePath!, {
type: "markdown",
content,
})
}
const gitIgnoredMatcher = await isGitIgnored()
const buildData: BuildData = { const buildData: BuildData = {
ctx, ctx,
mut, mut,
dependencies,
contentMap, contentMap,
ignored: await isGitIgnored(), ignored: (path) => {
initialSlugs: ctx.allSlugs, if (gitIgnoredMatcher(path)) return true
toRebuild: new Set<FilePath>(), const pathStr = path.toString()
toRemove: new Set<FilePath>(), for (const pattern of cfg.configuration.ignorePatterns) {
trackedAssets: new Set<FilePath>(), if (minimatch(pathStr, pattern)) {
return true
}
}
return false
},
changesSinceLastBuild: {},
lastBuildMs: 0, lastBuildMs: 0,
} }
@@ -132,274 +147,146 @@ async function startServing(
ignoreInitial: true, ignoreInitial: true,
}) })
const buildFromEntry = argv.fastRebuild ? partialRebuildFromEntrypoint : rebuildFromEntrypoint const changes: ChangeEvent[] = []
watcher watcher
.on("add", (fp) => buildFromEntry(fp, "add", clientRefresh, buildData)) .on("add", (fp) => {
.on("change", (fp) => buildFromEntry(fp, "change", clientRefresh, buildData)) if (buildData.ignored(fp)) return
.on("unlink", (fp) => buildFromEntry(fp, "delete", clientRefresh, buildData)) changes.push({ path: fp as FilePath, type: "add" })
void rebuild(changes, clientRefresh, buildData)
})
.on("change", (fp) => {
if (buildData.ignored(fp)) return
changes.push({ path: fp as FilePath, type: "change" })
void rebuild(changes, clientRefresh, buildData)
})
.on("unlink", (fp) => {
if (buildData.ignored(fp)) return
changes.push({ path: fp as FilePath, type: "delete" })
void rebuild(changes, clientRefresh, buildData)
})
return async () => { return async () => {
await watcher.close() await watcher.close()
} }
} }
async function partialRebuildFromEntrypoint( async function rebuild(changes: ChangeEvent[], clientRefresh: () => void, buildData: BuildData) {
filepath: string, const { ctx, contentMap, mut, changesSinceLastBuild } = buildData
action: FileEvent,
clientRefresh: () => void,
buildData: BuildData, // note: this function mutates buildData
) {
const { ctx, ignored, dependencies, contentMap, mut, toRemove } = buildData
const { argv, cfg } = ctx const { argv, cfg } = ctx
// don't do anything for gitignored files const buildId = randomIdNonSecure()
if (ignored(filepath)) { ctx.buildId = buildId
return buildData.lastBuildMs = new Date().getTime()
} const numChangesInBuild = changes.length
const buildStart = new Date().getTime()
buildData.lastBuildMs = buildStart
const release = await mut.acquire() const release = await mut.acquire()
if (buildData.lastBuildMs > buildStart) {
// if there's another build after us, release and let them do it
if (ctx.buildId !== buildId) {
release() release()
return return
} }
const perf = new PerfTimer() const perf = new PerfTimer()
console.log(chalk.yellow("Detected change, rebuilding...")) perf.addEvent("rebuild")
console.log(styleText("yellow", "Detected change, rebuilding..."))
// UPDATE DEP GRAPH // update changesSinceLastBuild
const fp = joinSegments(argv.directory, toPosixPath(filepath)) as FilePath for (const change of changes) {
changesSinceLastBuild[change.path] = change.type
}
const staticResources = getStaticResourcesFromPlugins(ctx) const staticResources = getStaticResourcesFromPlugins(ctx)
let processedFiles: ProcessedContent[] = [] const pathsToParse: FilePath[] = []
for (const [fp, type] of Object.entries(changesSinceLastBuild)) {
switch (action) { if (type === "delete" || path.extname(fp) !== ".md") continue
case "add": const fullPath = joinSegments(argv.directory, toPosixPath(fp)) as FilePath
// add to cache when new file is added pathsToParse.push(fullPath)
processedFiles = await parseMarkdown(ctx, [fp])
processedFiles.forEach(([tree, vfile]) => contentMap.set(vfile.data.filePath!, [tree, vfile]))
// update the dep graph by asking all emitters whether they depend on this file
for (const emitter of cfg.plugins.emitters) {
const emitterGraph =
(await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null
if (emitterGraph) {
const existingGraph = dependencies[emitter.name]
if (existingGraph !== null) {
existingGraph.mergeGraph(emitterGraph)
} else {
// might be the first time we're adding a mardown file
dependencies[emitter.name] = emitterGraph
}
}
}
break
case "change":
// invalidate cache when file is changed
processedFiles = await parseMarkdown(ctx, [fp])
processedFiles.forEach(([tree, vfile]) => contentMap.set(vfile.data.filePath!, [tree, vfile]))
// only content files can have added/removed dependencies because of transclusions
if (path.extname(fp) === ".md") {
for (const emitter of cfg.plugins.emitters) {
// get new dependencies from all emitters for this file
const emitterGraph =
(await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null
// only update the graph if the emitter plugin uses the changed file
// eg. Assets plugin ignores md files, so we skip updating the graph
if (emitterGraph?.hasNode(fp)) {
// merge the new dependencies into the dep graph
dependencies[emitter.name]?.updateIncomingEdgesForNode(emitterGraph, fp)
}
}
}
break
case "delete":
toRemove.add(fp)
break
} }
if (argv.verbose) { const parsed = await parseMarkdown(ctx, pathsToParse)
console.log(`Updated dependency graphs in ${perf.timeSince()}`) for (const content of parsed) {
contentMap.set(content[1].data.relativePath!, {
type: "markdown",
content,
})
} }
// EMIT // update state using changesSinceLastBuild
perf.addEvent("rebuild") // we do this weird play of add => compute change events => remove
// so that partialEmitters can do appropriate cleanup based on the content of deleted files
for (const [file, change] of Object.entries(changesSinceLastBuild)) {
if (change === "delete") {
// universal delete case
contentMap.delete(file as FilePath)
}
// manually track non-markdown files as processed files only
// contains markdown files
if (change === "add" && path.extname(file) !== ".md") {
contentMap.set(file as FilePath, {
type: "other",
})
}
}
const changeEvents: ChangeEvent[] = Object.entries(changesSinceLastBuild).map(([fp, type]) => {
const path = fp as FilePath
const processedContent = contentMap.get(path)
if (processedContent?.type === "markdown") {
const [_tree, file] = processedContent.content
return {
type,
path,
file,
}
}
return {
type,
path,
}
})
// update allFiles and then allSlugs with the consistent view of content map
ctx.allFiles = Array.from(contentMap.keys())
ctx.allSlugs = ctx.allFiles.map((fp) => slugifyFilePath(fp as FilePath))
const processedFiles = Array.from(contentMap.values())
.filter((file) => file.type === "markdown")
.map((file) => file.content)
let emittedFiles = 0 let emittedFiles = 0
for (const emitter of cfg.plugins.emitters) { for (const emitter of cfg.plugins.emitters) {
const depGraph = dependencies[emitter.name] // Try to use partialEmit if available, otherwise assume the output is static
const emitFn = emitter.partialEmit ?? emitter.emit
// emitter hasn't defined a dependency graph. call it with all processed files const emitted = await emitFn(ctx, processedFiles, staticResources, changeEvents)
if (depGraph === null) { if (emitted === null) {
if (argv.verbose) {
console.log(
`Emitter ${emitter.name} doesn't define a dependency graph. Calling it with all files...`,
)
}
const files = [...contentMap.values()].filter(
([_node, vfile]) => !toRemove.has(vfile.data.filePath!),
)
const emittedFps = await emitter.emit(ctx, files, staticResources)
if (ctx.argv.verbose) {
for (const file of emittedFps) {
console.log(`[emit:${emitter.name}] ${file}`)
}
}
emittedFiles += emittedFps.length
continue continue
} }
// only call the emitter if it uses this file if (Symbol.asyncIterator in emitted) {
if (depGraph.hasNode(fp)) { // Async generator case
// re-emit using all files that are needed for the downstream of this file for await (const file of emitted) {
// eg. for ContentIndex, the dep graph could be: emittedFiles++
// a.md --> contentIndex.json
// b.md ------^
//
// if a.md changes, we need to re-emit contentIndex.json,
// and supply [a.md, b.md] to the emitter
const upstreams = [...depGraph.getLeafNodeAncestors(fp)] as FilePath[]
const upstreamContent = upstreams
// filter out non-markdown files
.filter((file) => contentMap.has(file))
// if file was deleted, don't give it to the emitter
.filter((file) => !toRemove.has(file))
.map((file) => contentMap.get(file)!)
const emittedFps = await emitter.emit(ctx, upstreamContent, staticResources)
if (ctx.argv.verbose) { if (ctx.argv.verbose) {
for (const file of emittedFps) {
console.log(`[emit:${emitter.name}] ${file}`) console.log(`[emit:${emitter.name}] ${file}`)
} }
} }
} else {
emittedFiles += emittedFps.length // Array case
emittedFiles += emitted.length
if (ctx.argv.verbose) {
for (const file of emitted) {
console.log(`[emit:${emitter.name}] ${file}`)
}
}
} }
} }
console.log(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`) console.log(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`)
console.log(styleText("green", `Done rebuilding in ${perf.timeSince()}`))
// CLEANUP changes.splice(0, numChangesInBuild)
const destinationsToDelete = new Set<FilePath>()
for (const file of toRemove) {
// remove from cache
contentMap.delete(file)
Object.values(dependencies).forEach((depGraph) => {
// remove the node from dependency graphs
depGraph?.removeNode(file)
// remove any orphan nodes. eg if a.md is deleted, a.html is orphaned and should be removed
const orphanNodes = depGraph?.removeOrphanNodes()
orphanNodes?.forEach((node) => {
// only delete files that are in the output directory
if (node.startsWith(argv.output)) {
destinationsToDelete.add(node)
}
})
})
}
await rimraf([...destinationsToDelete])
console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`))
toRemove.clear()
release()
clientRefresh() clientRefresh()
}
async function rebuildFromEntrypoint(
fp: string,
action: FileEvent,
clientRefresh: () => void,
buildData: BuildData, // note: this function mutates buildData
) {
const { ctx, ignored, mut, initialSlugs, contentMap, toRebuild, toRemove, trackedAssets } =
buildData
const { argv } = ctx
// don't do anything for gitignored files
if (ignored(fp)) {
return
}
// dont bother rebuilding for non-content files, just track and refresh
fp = toPosixPath(fp)
const filePath = joinSegments(argv.directory, fp) as FilePath
if (path.extname(fp) !== ".md") {
if (action === "add" || action === "change") {
trackedAssets.add(filePath)
} else if (action === "delete") {
trackedAssets.delete(filePath)
}
clientRefresh()
return
}
if (action === "add" || action === "change") {
toRebuild.add(filePath)
} else if (action === "delete") {
toRemove.add(filePath)
}
const buildStart = new Date().getTime()
buildData.lastBuildMs = buildStart
const release = await mut.acquire()
// there's another build after us, release and let them do it
if (buildData.lastBuildMs > buildStart) {
release() release()
return
}
const perf = new PerfTimer()
console.log(chalk.yellow("Detected change, rebuilding..."))
try {
const filesToRebuild = [...toRebuild].filter((fp) => !toRemove.has(fp))
const trackedSlugs = [...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets])]
.filter((fp) => !toRemove.has(fp))
.map((fp) => slugifyFilePath(path.posix.relative(argv.directory, fp) as FilePath))
ctx.allSlugs = [...new Set([...initialSlugs, ...trackedSlugs])]
const parsedContent = await parseMarkdown(ctx, filesToRebuild)
for (const content of parsedContent) {
const [_tree, vfile] = content
contentMap.set(vfile.data.filePath!, content)
}
for (const fp of toRemove) {
contentMap.delete(fp)
}
const parsedFiles = [...contentMap.values()]
const filteredContent = filterContent(ctx, parsedFiles)
// TODO: we can probably traverse the link graph to figure out what's safe to delete here
// instead of just deleting everything
await rimraf(path.join(argv.output, ".*"), { glob: true })
await emitContent(ctx, filteredContent)
console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`))
} catch (err) {
console.log(chalk.yellow(`Rebuild failed. Waiting on a change to fix the error...`))
if (argv.verbose) {
console.log(chalk.red(err))
}
}
release()
clientRefresh()
toRebuild.clear()
toRemove.clear()
} }
export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => { export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => {

View File

@@ -38,9 +38,14 @@ export type Analytics =
provider: "cabin" provider: "cabin"
host?: string host?: string
} }
| {
provider: "clarity"
projectId?: string
}
export interface GlobalConfiguration { export interface GlobalConfiguration {
pageTitle: string pageTitle: string
pageTitleSuffix?: string
/** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */ /** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */
enableSPA: boolean enableSPA: boolean
/** Whether to display Wikipedia-style popovers when hovering over links */ /** Whether to display Wikipedia-style popovers when hovering over links */
@@ -59,7 +64,7 @@ export interface GlobalConfiguration {
/** /**
* Allow to translate the date in the language of your choice. * Allow to translate the date in the language of your choice.
* Also used for UI translation (default: en-US) * Also used for UI translation (default: en-US)
* Need to be formated following BCP 47: https://en.wikipedia.org/wiki/IETF_language_tag * Need to be formatted following BCP 47: https://en.wikipedia.org/wiki/IETF_language_tag
* The first part is the language (en) and the second part is the script/region (US) * The first part is the language (en) and the second part is the script/region (US)
* Language Codes: https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes * Language Codes: https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
* Region Codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 * Region Codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2

View File

@@ -71,10 +71,10 @@ export const BuildArgv = {
default: false, default: false,
describe: "run a local server to live-preview your Quartz", describe: "run a local server to live-preview your Quartz",
}, },
fastRebuild: { watch: {
boolean: true, boolean: true,
default: false, default: false,
describe: "[experimental] rebuild only the changed files", describe: "watch for changes and rebuild automatically",
}, },
baseDir: { baseDir: {
string: true, string: true,

View File

@@ -1,11 +1,11 @@
import { promises } from "fs" import { promises } from "fs"
import path from "path" import path from "path"
import esbuild from "esbuild" import esbuild from "esbuild"
import chalk from "chalk" import { styleText } from "util"
import { sassPlugin } from "esbuild-sass-plugin" import { sassPlugin } from "esbuild-sass-plugin"
import fs from "fs" import fs from "fs"
import { intro, outro, select, text } from "@clack/prompts" import { intro, outro, select, text } from "@clack/prompts"
import { rimraf } from "rimraf" import { rm } from "fs/promises"
import chokidar from "chokidar" import chokidar from "chokidar"
import prettyBytes from "pretty-bytes" import prettyBytes from "pretty-bytes"
import { execSync, spawnSync } from "child_process" import { execSync, spawnSync } from "child_process"
@@ -15,6 +15,7 @@ import { WebSocketServer } from "ws"
import { randomUUID } from "crypto" import { randomUUID } from "crypto"
import { Mutex } from "async-mutex" import { Mutex } from "async-mutex"
import { CreateArgv } from "./args.js" import { CreateArgv } from "./args.js"
import { globby } from "globby"
import { import {
exitIfCancel, exitIfCancel,
escapePath, escapePath,
@@ -32,31 +33,44 @@ import {
cwd, cwd,
} from "./constants.js" } from "./constants.js"
/**
* Resolve content directory path
* @param contentPath path to resolve
*/
function resolveContentPath(contentPath) {
if (path.isAbsolute(contentPath)) return path.relative(cwd, contentPath)
return path.join(cwd, contentPath)
}
/** /**
* Handles `npx quartz create` * Handles `npx quartz create`
* @param {*} argv arguments for `create` * @param {*} argv arguments for `create`
*/ */
export async function handleCreate(argv) { export async function handleCreate(argv) {
console.log() console.log()
intro(chalk.bgGreen.black(` Quartz v${version} `)) intro(styleText(["bgGreen", "black"], ` Quartz v${version} `))
const contentFolder = path.join(cwd, argv.directory) const contentFolder = resolveContentPath(argv.directory)
let setupStrategy = argv.strategy?.toLowerCase() let setupStrategy = argv.strategy?.toLowerCase()
let linkResolutionStrategy = argv.links?.toLowerCase() let linkResolutionStrategy = argv.links?.toLowerCase()
const sourceDirectory = argv.source const sourceDirectory = argv.source
// If all cmd arguments were provided, check if theyre valid // If all cmd arguments were provided, check if they're valid
if (setupStrategy && linkResolutionStrategy) { if (setupStrategy && linkResolutionStrategy) {
// If setup isn't, "new", source argument is required // If setup isn't, "new", source argument is required
if (setupStrategy !== "new") { if (setupStrategy !== "new") {
// Error handling // Error handling
if (!sourceDirectory) { if (!sourceDirectory) {
outro( outro(
chalk.red( styleText(
`Setup strategies (arg '${chalk.yellow( "red",
`Setup strategies (arg '${styleText(
"yellow",
`-${CreateArgv.strategy.alias[0]}`, `-${CreateArgv.strategy.alias[0]}`,
)}') other than '${chalk.yellow( )}') other than '${styleText(
"yellow",
"new", "new",
)}' require content folder argument ('${chalk.yellow( )}' require content folder argument ('${styleText(
"yellow",
`-${CreateArgv.source.alias[0]}`, `-${CreateArgv.source.alias[0]}`,
)}') to be set`, )}') to be set`,
), ),
@@ -65,19 +79,23 @@ export async function handleCreate(argv) {
} else { } else {
if (!fs.existsSync(sourceDirectory)) { if (!fs.existsSync(sourceDirectory)) {
outro( outro(
chalk.red( styleText(
`Input directory to copy/symlink 'content' from not found ('${chalk.yellow( "red",
`Input directory to copy/symlink 'content' from not found ('${styleText(
"yellow",
sourceDirectory, sourceDirectory,
)}', invalid argument "${chalk.yellow(`-${CreateArgv.source.alias[0]}`)})`, )}', invalid argument "${styleText("yellow", `-${CreateArgv.source.alias[0]}`)})`,
), ),
) )
process.exit(1) process.exit(1)
} else if (!fs.lstatSync(sourceDirectory).isDirectory()) { } else if (!fs.lstatSync(sourceDirectory).isDirectory()) {
outro( outro(
chalk.red( styleText(
`Source directory to copy/symlink 'content' from is not a directory (found file at '${chalk.yellow( "red",
`Source directory to copy/symlink 'content' from is not a directory (found file at '${styleText(
"yellow",
sourceDirectory, sourceDirectory,
)}', invalid argument ${chalk.yellow(`-${CreateArgv.source.alias[0]}`)}")`, )}', invalid argument ${styleText("yellow", `-${CreateArgv.source.alias[0]}`)}")`,
), ),
) )
process.exit(1) process.exit(1)
@@ -109,7 +127,7 @@ export async function handleCreate(argv) {
if (contentStat.isSymbolicLink()) { if (contentStat.isSymbolicLink()) {
await fs.promises.unlink(contentFolder) await fs.promises.unlink(contentFolder)
} else { } else {
await rimraf(contentFolder) await rm(contentFolder, { recursive: true, force: true })
} }
} }
@@ -215,7 +233,11 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
* @param {*} argv arguments for `build` * @param {*} argv arguments for `build`
*/ */
export async function handleBuild(argv) { export async function handleBuild(argv) {
console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`)) if (argv.serve) {
argv.watch = true
}
console.log(`\n${styleText(["bgGreen", "black"], ` Quartz v${version} `)} \n`)
const ctx = await esbuild.context({ const ctx = await esbuild.context({
entryPoints: [fp], entryPoints: [fp],
outfile: cacheFile, outfile: cacheFile,
@@ -236,6 +258,11 @@ export async function handleBuild(argv) {
type: "css-text", type: "css-text",
cssImports: true, cssImports: true,
}), }),
sassPlugin({
filter: /\.inline\.scss$/,
type: "css",
cssImports: true,
}),
{ {
name: "inline-script-loader", name: "inline-script-loader",
setup(build) { setup(build) {
@@ -285,13 +312,13 @@ export async function handleBuild(argv) {
} }
if (cleanupBuild) { if (cleanupBuild) {
console.log(styleText("yellow", "Detected a source code change, doing a hard rebuild..."))
await cleanupBuild() await cleanupBuild()
console.log(chalk.yellow("Detected a source code change, doing a hard rebuild..."))
} }
const result = await ctx.rebuild().catch((err) => { const result = await ctx.rebuild().catch((err) => {
console.error(`${chalk.red("Couldn't parse Quartz configuration:")} ${fp}`) console.error(`${styleText("red", "Couldn't parse Quartz configuration:")} ${fp}`)
console.log(`Reason: ${chalk.grey(err)}`) console.log(`Reason: ${styleText("grey", err)}`)
process.exit(1) process.exit(1)
}) })
release() release()
@@ -316,9 +343,10 @@ export async function handleBuild(argv) {
clientRefresh() clientRefresh()
} }
let clientRefresh = () => {}
if (argv.serve) { if (argv.serve) {
const connections = [] const connections = []
const clientRefresh = () => connections.forEach((conn) => conn.send("rebuild")) clientRefresh = () => connections.forEach((conn) => conn.send("rebuild"))
if (argv.baseDir !== "" && !argv.baseDir.startsWith("/")) { if (argv.baseDir !== "" && !argv.baseDir.startsWith("/")) {
argv.baseDir = "/" + argv.baseDir argv.baseDir = "/" + argv.baseDir
@@ -328,7 +356,8 @@ export async function handleBuild(argv) {
const server = http.createServer(async (req, res) => { const server = http.createServer(async (req, res) => {
if (argv.baseDir && !req.url?.startsWith(argv.baseDir)) { if (argv.baseDir && !req.url?.startsWith(argv.baseDir)) {
console.log( console.log(
chalk.red( styleText(
"red",
`[404] ${req.url} (warning: link outside of site, this is likely a Quartz bug)`, `[404] ${req.url} (warning: link outside of site, this is likely a Quartz bug)`,
), ),
) )
@@ -350,12 +379,23 @@ export async function handleBuild(argv) {
source: "**/*.*", source: "**/*.*",
headers: [{ key: "Content-Disposition", value: "inline" }], headers: [{ key: "Content-Disposition", value: "inline" }],
}, },
{
source: "**/*.webp",
headers: [{ key: "Content-Type", value: "image/webp" }],
},
// fixes bug where avif images are displayed as text instead of images (future proof)
{
source: "**/*.avif",
headers: [{ key: "Content-Type", value: "image/avif" }],
},
], ],
}) })
const status = res.statusCode const status = res.statusCode
const statusString = const statusString =
status >= 200 && status < 300 ? chalk.green(`[${status}]`) : chalk.red(`[${status}]`) status >= 200 && status < 300
console.log(statusString + chalk.grey(` ${argv.baseDir}${req.url}`)) ? styleText("green", `[${status}]`)
: styleText("red", `[${status}]`)
console.log(statusString + styleText("grey", ` ${argv.baseDir}${req.url}`))
release() release()
} }
@@ -364,7 +404,10 @@ export async function handleBuild(argv) {
res.writeHead(302, { res.writeHead(302, {
Location: newFp, Location: newFp,
}) })
console.log(chalk.yellow("[302]") + chalk.grey(` ${argv.baseDir}${req.url} -> ${newFp}`)) console.log(
styleText("yellow", "[302]") +
styleText("grey", ` ${argv.baseDir}${req.url} -> ${newFp}`),
)
res.end() res.end()
} }
@@ -409,26 +452,38 @@ export async function handleBuild(argv) {
return serve() return serve()
}) })
server.listen(argv.port) server.listen(argv.port)
const wss = new WebSocketServer({ port: argv.wsPort }) const wss = new WebSocketServer({ port: argv.wsPort })
wss.on("connection", (ws) => connections.push(ws)) wss.on("connection", (ws) => connections.push(ws))
console.log( console.log(
chalk.cyan( styleText(
"cyan",
`Started a Quartz server listening at http://localhost:${argv.port}${argv.baseDir}`, `Started a Quartz server listening at http://localhost:${argv.port}${argv.baseDir}`,
), ),
) )
console.log("hint: exit with ctrl+c")
chokidar
.watch(["**/*.ts", "**/*.tsx", "**/*.scss", "package.json"], {
ignoreInitial: true,
})
.on("all", async () => {
build(clientRefresh)
})
} else { } else {
await build(() => {}) await build(clientRefresh)
ctx.dispose() ctx.dispose()
} }
if (argv.watch) {
const paths = await globby([
"**/*.ts",
"quartz/cli/*.js",
"quartz/static/**/*",
"**/*.tsx",
"**/*.scss",
"package.json",
])
chokidar
.watch(paths, { ignoreInitial: true })
.on("add", () => build(clientRefresh))
.on("change", () => build(clientRefresh))
.on("unlink", () => build(clientRefresh))
console.log(styleText("grey", "hint: exit with ctrl+c"))
}
} }
/** /**
@@ -436,8 +491,8 @@ export async function handleBuild(argv) {
* @param {*} argv arguments for `update` * @param {*} argv arguments for `update`
*/ */
export async function handleUpdate(argv) { export async function handleUpdate(argv) {
const contentFolder = path.join(cwd, argv.directory) const contentFolder = resolveContentPath(argv.directory)
console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`)) console.log(`\n${styleText(["bgGreen", "black"], ` Quartz v${version} `)} \n`)
console.log("Backing up your content") console.log("Backing up your content")
execSync( execSync(
`git remote show upstream || git remote add upstream https://github.com/jackyzha0/quartz.git`, `git remote show upstream || git remote add upstream https://github.com/jackyzha0/quartz.git`,
@@ -450,18 +505,36 @@ export async function handleUpdate(argv) {
try { try {
gitPull(UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH) gitPull(UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH)
} catch { } catch {
console.log(chalk.red("An error occurred above while pulling updates.")) console.log(styleText("red", "An error occurred above while pulling updates."))
await popContentFolder(contentFolder) await popContentFolder(contentFolder)
return return
} }
await popContentFolder(contentFolder) await popContentFolder(contentFolder)
console.log("Ensuring dependencies are up to date") console.log("Ensuring dependencies are up to date")
const res = spawnSync("npm", ["i"], { stdio: "inherit" })
/*
On Windows, if the command `npm` is really `npm.cmd', this call fails
as it will be unable to find `npm`. This is often the case on systems
where `npm` is installed via a package manager.
This means `npx quartz update` will not actually update dependencies
on Windows, without a manual `npm i` from the caller.
However, by spawning a shell, we are able to call `npm.cmd`.
See: https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows
*/
const opts = { stdio: "inherit" }
if (process.platform === "win32") {
opts.shell = true
}
const res = spawnSync("npm", ["i"], opts)
if (res.status === 0) { if (res.status === 0) {
console.log(chalk.green("Done!")) console.log(styleText("green", "Done!"))
} else { } else {
console.log(chalk.red("An error occurred above while installing dependencies.")) console.log(styleText("red", "An error occurred above while installing dependencies."))
} }
} }
@@ -470,7 +543,7 @@ export async function handleUpdate(argv) {
* @param {*} argv arguments for `restore` * @param {*} argv arguments for `restore`
*/ */
export async function handleRestore(argv) { export async function handleRestore(argv) {
const contentFolder = path.join(cwd, argv.directory) const contentFolder = resolveContentPath(argv.directory)
await popContentFolder(contentFolder) await popContentFolder(contentFolder)
} }
@@ -479,15 +552,15 @@ export async function handleRestore(argv) {
* @param {*} argv arguments for `sync` * @param {*} argv arguments for `sync`
*/ */
export async function handleSync(argv) { export async function handleSync(argv) {
const contentFolder = path.join(cwd, argv.directory) const contentFolder = resolveContentPath(argv.directory)
console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`)) console.log(`\n${styleText(["bgGreen", "black"], ` Quartz v${version} `)}\n`)
console.log("Backing up your content") console.log("Backing up your content")
if (argv.commit) { if (argv.commit) {
const contentStat = await fs.promises.lstat(contentFolder) const contentStat = await fs.promises.lstat(contentFolder)
if (contentStat.isSymbolicLink()) { if (contentStat.isSymbolicLink()) {
const linkTarg = await fs.promises.readlink(contentFolder) const linkTarg = await fs.promises.readlink(contentFolder)
console.log(chalk.yellow("Detected symlink, trying to dereference before committing")) console.log(styleText("yellow", "Detected symlink, trying to dereference before committing"))
// stash symlink file // stash symlink file
await stashContentFolder(contentFolder) await stashContentFolder(contentFolder)
@@ -522,7 +595,7 @@ export async function handleSync(argv) {
try { try {
gitPull(ORIGIN_NAME, QUARTZ_SOURCE_BRANCH) gitPull(ORIGIN_NAME, QUARTZ_SOURCE_BRANCH)
} catch { } catch {
console.log(chalk.red("An error occurred above while pulling updates.")) console.log(styleText("red", "An error occurred above while pulling updates."))
await popContentFolder(contentFolder) await popContentFolder(contentFolder)
return return
} }
@@ -531,14 +604,17 @@ export async function handleSync(argv) {
await popContentFolder(contentFolder) await popContentFolder(contentFolder)
if (argv.push) { if (argv.push) {
console.log("Pushing your changes") console.log("Pushing your changes")
const res = spawnSync("git", ["push", "-uf", ORIGIN_NAME, QUARTZ_SOURCE_BRANCH], { const currentBranch = execSync("git rev-parse --abbrev-ref HEAD").toString().trim()
const res = spawnSync("git", ["push", "-uf", ORIGIN_NAME, currentBranch], {
stdio: "inherit", stdio: "inherit",
}) })
if (res.status !== 0) { if (res.status !== 0) {
console.log(chalk.red(`An error occurred above while pushing to remote ${ORIGIN_NAME}.`)) console.log(
styleText("red", `An error occurred above while pushing to remote ${ORIGIN_NAME}.`),
)
return return
} }
} }
console.log(chalk.green("Done!")) console.log(styleText("green", "Done!"))
} }

View File

@@ -1,5 +1,5 @@
import { isCancel, outro } from "@clack/prompts" import { isCancel, outro } from "@clack/prompts"
import chalk from "chalk" import { styleText } from "util"
import { contentCacheFolder } from "./constants.js" import { contentCacheFolder } from "./constants.js"
import { spawnSync } from "child_process" import { spawnSync } from "child_process"
import fs from "fs" import fs from "fs"
@@ -14,7 +14,7 @@ export function escapePath(fp) {
export function exitIfCancel(val) { export function exitIfCancel(val) {
if (isCancel(val)) { if (isCancel(val)) {
outro(chalk.red("Exiting")) outro(styleText("red", "Exiting"))
process.exit(0) process.exit(0)
} else { } else {
return val return val
@@ -36,9 +36,9 @@ export function gitPull(origin, branch) {
const flags = ["--no-rebase", "--autostash", "-s", "recursive", "-X", "ours", "--no-edit"] const flags = ["--no-rebase", "--autostash", "-s", "recursive", "-X", "ours", "--no-edit"]
const out = spawnSync("git", ["pull", ...flags, origin, branch], { stdio: "inherit" }) const out = spawnSync("git", ["pull", ...flags, origin, branch], { stdio: "inherit" })
if (out.stderr) { if (out.stderr) {
throw new Error(chalk.red(`Error while pulling updates: ${out.stderr}`)) throw new Error(styleText("red", `Error while pulling updates: ${out.stderr}`))
} else if (out.status !== 0) { } else if (out.status !== 0) {
throw new Error(chalk.red("Error while pulling updates")) throw new Error(styleText("red", "Error while pulling updates"))
} }
} }

View File

@@ -3,19 +3,35 @@ 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"
const Backlinks: QuartzComponent = ({ interface BacklinksOptions {
hideWhenEmpty: boolean
}
const defaultOptions: BacklinksOptions = {
hideWhenEmpty: true,
}
export default ((opts?: Partial<BacklinksOptions>) => {
const options: BacklinksOptions = { ...defaultOptions, ...opts }
const { OverflowList, overflowListAfterDOMLoaded } = OverflowListFactory()
const Backlinks: QuartzComponent = ({
fileData, fileData,
allFiles, allFiles,
displayClass, displayClass,
cfg, cfg,
}: QuartzComponentProps) => { }: QuartzComponentProps) => {
const slug = simplifySlug(fileData.slug!) const slug = simplifySlug(fileData.slug!)
const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug)) const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
if (options.hideWhenEmpty && backlinkFiles.length == 0) {
return null
}
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>
@@ -27,10 +43,13 @@ const Backlinks: QuartzComponent = ({
) : ( ) : (
<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
export default (() => Backlinks) satisfies QuartzComponentConstructor Backlinks.afterDOMLoaded = overflowListAfterDOMLoaded
return Backlinks
}) satisfies QuartzComponentConstructor

View File

@@ -1,8 +1,8 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import breadcrumbsStyle from "./styles/breadcrumbs.scss" import breadcrumbsStyle from "./styles/breadcrumbs.scss"
import { FullSlug, SimpleSlug, joinSegments, resolveRelative } from "../util/path" import { FullSlug, SimpleSlug, resolveRelative, simplifySlug } from "../util/path"
import { QuartzPluginData } from "../plugins/vfile"
import { classNames } from "../util/lang" import { classNames } from "../util/lang"
import { trieFromAllFiles } from "../util/ctx"
type CrumbData = { type CrumbData = {
displayName: string displayName: string
@@ -22,10 +22,6 @@ interface BreadcrumbOptions {
* Whether to look up frontmatter title for folders (could cause performance problems with big vaults) * Whether to look up frontmatter title for folders (could cause performance problems with big vaults)
*/ */
resolveFrontmatterTitle: boolean resolveFrontmatterTitle: boolean
/**
* Whether to display breadcrumbs on root `index.md`
*/
hideOnRoot: boolean
/** /**
* Whether to display the current page in the breadcrumbs. * Whether to display the current page in the breadcrumbs.
*/ */
@@ -36,7 +32,6 @@ const defaultOptions: BreadcrumbOptions = {
spacerSymbol: "", spacerSymbol: "",
rootName: "Home", rootName: "Home",
resolveFrontmatterTitle: true, resolveFrontmatterTitle: true,
hideOnRoot: true,
showCurrentPage: true, showCurrentPage: true,
} }
@@ -48,78 +43,37 @@ function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: Simpl
} }
export default ((opts?: Partial<BreadcrumbOptions>) => { export default ((opts?: Partial<BreadcrumbOptions>) => {
// Merge options with defaults
const options: BreadcrumbOptions = { ...defaultOptions, ...opts } const options: BreadcrumbOptions = { ...defaultOptions, ...opts }
// computed index of folder name to its associated file data
let folderIndex: Map<string, QuartzPluginData> | undefined
const Breadcrumbs: QuartzComponent = ({ const Breadcrumbs: QuartzComponent = ({
fileData, fileData,
allFiles, allFiles,
displayClass, displayClass,
ctx,
}: QuartzComponentProps) => { }: QuartzComponentProps) => {
// Hide crumbs on root if enabled const trie = (ctx.trie ??= trieFromAllFiles(allFiles))
if (options.hideOnRoot && fileData.slug === "index") { const slugParts = fileData.slug!.split("/")
return <></> const pathNodes = trie.ancestryChain(slugParts)
if (!pathNodes) {
return null
} }
// Format entry for root element const crumbs: CrumbData[] = pathNodes.map((node, idx) => {
const firstEntry = formatCrumb(options.rootName, fileData.slug!, "/" as SimpleSlug) const crumb = formatCrumb(node.displayName, fileData.slug!, simplifySlug(node.slug))
const crumbs: CrumbData[] = [firstEntry] if (idx === 0) {
crumb.displayName = options.rootName
if (!folderIndex && options.resolveFrontmatterTitle) {
folderIndex = new Map()
// construct the index for the first time
for (const file of allFiles) {
const folderParts = file.slug?.split("/")
if (folderParts?.at(-1) === "index") {
folderIndex.set(folderParts.slice(0, -1).join("/"), file)
}
}
} }
// Split slug into hierarchy/parts // For last node (current page), set empty path
const slugParts = fileData.slug?.split("/") if (idx === pathNodes.length - 1) {
if (slugParts) { crumb.path = ""
// is tag breadcrumb?
const isTagPath = slugParts[0] === "tags"
// full path until current part
let currentPath = ""
for (let i = 0; i < slugParts.length - 1; i++) {
let curPathSegment = slugParts[i]
// Try to resolve frontmatter folder title
const currentFile = folderIndex?.get(slugParts.slice(0, i + 1).join("/"))
if (currentFile) {
const title = currentFile.frontmatter!.title
if (title !== "index") {
curPathSegment = title
}
} }
// Add current slug to full path return crumb
currentPath = joinSegments(currentPath, slugParts[i])
const includeTrailingSlash = !isTagPath || i < 1
// Format and add current crumb
const crumb = formatCrumb(
curPathSegment,
fileData.slug!,
(currentPath + (includeTrailingSlash ? "/" : "")) as SimpleSlug,
)
crumbs.push(crumb)
}
// Add current file to crumb (can directly use frontmatter title)
if (options.showCurrentPage && slugParts.at(-1) !== "index") {
crumbs.push({
displayName: fileData.frontmatter!.title,
path: "",
}) })
}
if (!options.showCurrentPage) {
crumbs.pop()
} }
return ( return (

View File

@@ -10,6 +10,9 @@ type Options = {
repoId: string repoId: string
category: string category: string
categoryId: string categoryId: string
themeUrl?: string
lightTheme?: string
darkTheme?: string
mapping?: "url" | "title" | "og:title" | "specific" | "number" | "pathname" mapping?: "url" | "title" | "og:title" | "specific" | "number" | "pathname"
strict?: boolean strict?: boolean
reactionsEnabled?: boolean reactionsEnabled?: boolean
@@ -22,7 +25,15 @@ function boolToStringBool(b: boolean): string {
} }
export default ((opts: Options) => { export default ((opts: Options) => {
const Comments: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { const Comments: QuartzComponent = ({ displayClass, fileData, cfg }: QuartzComponentProps) => {
// check if comments should be displayed according to frontmatter
const disableComment: boolean =
typeof fileData.frontmatter?.comments !== "undefined" &&
(!fileData.frontmatter?.comments || fileData.frontmatter?.comments === "false")
if (disableComment) {
return <></>
}
return ( return (
<div <div
class={classNames(displayClass, "giscus")} class={classNames(displayClass, "giscus")}
@@ -34,6 +45,11 @@ export default ((opts: Options) => {
data-strict={boolToStringBool(opts.options.strict ?? true)} data-strict={boolToStringBool(opts.options.strict ?? true)}
data-reactions-enabled={boolToStringBool(opts.options.reactionsEnabled ?? true)} data-reactions-enabled={boolToStringBool(opts.options.reactionsEnabled ?? true)}
data-input-position={opts.options.inputPosition ?? "bottom"} data-input-position={opts.options.inputPosition ?? "bottom"}
data-light-theme={opts.options.lightTheme ?? "light"}
data-dark-theme={opts.options.darkTheme ?? "dark"}
data-theme-url={
opts.options.themeUrl ?? `https://${cfg.baseUrl ?? "example.com"}/static/giscus`
}
></div> ></div>
) )
} }

View File

@@ -0,0 +1,22 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
type ConditionalRenderConfig = {
component: QuartzComponent
condition: (props: QuartzComponentProps) => boolean
}
export default ((config: ConditionalRenderConfig) => {
const ConditionalRender: QuartzComponent = (props: QuartzComponentProps) => {
if (config.condition(props)) {
return <config.component {...props} />
}
return null
}
ConditionalRender.afterDOMLoaded = config.component.afterDOMLoaded
ConditionalRender.beforeDOMLoaded = config.component.beforeDOMLoaded
ConditionalRender.css = config.component.css
return ConditionalRender
}) satisfies QuartzComponentConstructor<ConditionalRenderConfig>

View File

@@ -1,4 +1,4 @@
import { formatDate, getDate } from "./Date" import { Date, getDate } from "./Date"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import readingTime from "reading-time" import readingTime from "reading-time"
import { classNames } from "../util/lang" import { classNames } from "../util/lang"
@@ -30,7 +30,7 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
const segments: (string | JSX.Element)[] = [] const segments: (string | JSX.Element)[] = []
if (fileData.dates) { if (fileData.dates) {
segments.push(formatDate(getDate(cfg, fileData)!, cfg.locale)) segments.push(<Date date={getDate(cfg, fileData)!} locale={cfg.locale} />)
} }
// Display reading time if enabled // Display reading time if enabled
@@ -39,14 +39,12 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
const displayedTime = i18n(cfg.locale).components.contentMeta.readingTime({ const displayedTime = i18n(cfg.locale).components.contentMeta.readingTime({
minutes: Math.ceil(minutes), minutes: Math.ceil(minutes),
}) })
segments.push(displayedTime) segments.push(<span>{displayedTime}</span>)
} }
const segmentsElements = segments.map((segment) => <span>{segment}</span>)
return ( return (
<p show-comma={options.showComma} class={classNames(displayClass, "content-meta")}> <p show-comma={options.showComma} class={classNames(displayClass, "content-meta")}>
{segmentsElements} {segments}
</p> </p>
) )
} else { } else {

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

View File

@@ -27,5 +27,5 @@ export function formatDate(d: Date, locale: ValidLocale = "en-US"): string {
} }
export function Date({ date, locale }: Props) { export function Date({ date, locale }: Props) {
return <>{formatDate(date, locale)}</> return <time datetime={date.toISOString()}>{formatDate(date, locale)}</time>
} }

View File

@@ -1,7 +1,6 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
export default ((component?: QuartzComponent) => { export default ((component: QuartzComponent) => {
if (component) {
const Component = component const Component = component
const DesktopOnly: QuartzComponent = (props: QuartzComponentProps) => { const DesktopOnly: QuartzComponent = (props: QuartzComponentProps) => {
return <Component displayClass="desktop-only" {...props} /> return <Component displayClass="desktop-only" {...props} />
@@ -12,7 +11,4 @@ export default ((component?: QuartzComponent) => {
DesktopOnly.beforeDOMLoaded = component?.beforeDOMLoaded DesktopOnly.beforeDOMLoaded = component?.beforeDOMLoaded
DesktopOnly.css = component?.css DesktopOnly.css = component?.css
return DesktopOnly return DesktopOnly
} else { }) satisfies QuartzComponentConstructor<QuartzComponent>
return () => <></>
}
}) satisfies QuartzComponentConstructor

View File

@@ -1,24 +1,37 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import explorerStyle from "./styles/explorer.scss" 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,70 +40,65 @@ 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
function constructFileTree(allFiles: QuartzPluginData[]) {
if (fileTree) {
return
}
// 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 = ({
cfg,
allFiles,
displayClass,
fileData,
}: QuartzComponentProps) => {
constructFileTree(allFiles)
return ( return (
<div class={classNames(displayClass, "explorer")}> <div
<button class={classNames(displayClass, "explorer")}
type="button"
id="explorer"
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}
aria-controls="explorer-content"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide-menu"
>
<line x1="4" x2="20" y1="12" y2="12" />
<line x1="4" x2="20" y1="6" y2="6" />
<line x1="4" x2="20" y1="18" y2="18" />
</svg>
</button>
<button
type="button"
class="title-button explorer-toggle desktop-explorer"
data-mobile={false}
aria-expanded={true}
> >
<h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2> <h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2>
<svg <svg
@@ -108,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 = explorerStyle 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

@@ -0,0 +1,55 @@
import { concatenateResources } from "../util/resources"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
type FlexConfig = {
components: {
Component: QuartzComponent
grow?: boolean
shrink?: boolean
basis?: string
order?: number
align?: "start" | "end" | "center" | "stretch"
justify?: "start" | "end" | "center" | "between" | "around"
}[]
direction?: "row" | "row-reverse" | "column" | "column-reverse"
wrap?: "nowrap" | "wrap" | "wrap-reverse"
gap?: string
}
export default ((config: FlexConfig) => {
const Flex: QuartzComponent = (props: QuartzComponentProps) => {
const direction = config.direction ?? "row"
const wrap = config.wrap ?? "nowrap"
const gap = config.gap ?? "1rem"
return (
<div style={`display: flex; flex-direction: ${direction}; flex-wrap: ${wrap}; gap: ${gap};`}>
{config.components.map((c) => {
const grow = c.grow ? 1 : 0
const shrink = (c.shrink ?? true) ? 1 : 0
const basis = c.basis ?? "auto"
const order = c.order ?? 0
const align = c.align ?? "center"
const justify = c.justify ?? "center"
return (
<div
style={`flex-grow: ${grow}; flex-shrink: ${shrink}; flex-basis: ${basis}; order: ${order}; align-self: ${align}; justify-self: ${justify};`}
>
<c.Component {...props} />
</div>
)
})}
</div>
)
}
Flex.afterDOMLoaded = concatenateResources(
...config.components.map((c) => c.Component.afterDOMLoaded),
)
Flex.beforeDOMLoaded = concatenateResources(
...config.components.map((c) => c.Component.beforeDOMLoaded),
)
Flex.css = concatenateResources(...config.components.map((c) => c.Component.css))
return Flex
}) satisfies QuartzComponentConstructor<FlexConfig>

View File

@@ -18,6 +18,7 @@ export interface D3Config {
removeTags: string[] removeTags: string[]
showTags: boolean showTags: boolean
focusOnHover?: boolean focusOnHover?: boolean
enableRadial?: boolean
} }
interface GraphOptions { interface GraphOptions {
@@ -39,6 +40,7 @@ const defaultOptions: GraphOptions = {
showTags: true, showTags: true,
removeTags: [], removeTags: [],
focusOnHover: false, focusOnHover: false,
enableRadial: false,
}, },
globalGraph: { globalGraph: {
drag: true, drag: true,
@@ -46,17 +48,18 @@ 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,
showTags: true, showTags: true,
removeTags: [], removeTags: [],
focusOnHover: true, focusOnHover: true,
enableRadial: true,
}, },
} }
export default ((opts?: GraphOptions) => { export default ((opts?: Partial<GraphOptions>) => {
const Graph: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { const Graph: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph } const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph }
const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph } const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph }
@@ -64,10 +67,10 @@ export default ((opts?: 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 class="global-graph-icon" aria-label="Global Graph">
<svg <svg
version="1.1" version="1.1"
id="global-graph-icon"
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"
x="0px" x="0px"
@@ -90,9 +93,10 @@ export default ((opts?: GraphOptions) => {
s-2-0.897-2-2s0.897-2,2-2S47,39.897,47,41z M49,10c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S51.206,10,49,10z" s-2-0.897-2-2s0.897-2,2-2S47,39.897,47,41z M49,10c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S51.206,10,49,10z"
/> />
</svg> </svg>
</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

@@ -1,22 +1,40 @@
import { i18n } from "../i18n" import { i18n } from "../i18n"
import { FullSlug, joinSegments, pathToRoot } from "../util/path" import { FullSlug, getFileExtension, joinSegments, pathToRoot } from "../util/path"
import { JSResourceToScriptElement } from "../util/resources" import { CSSResourceToStyleElement, JSResourceToScriptElement } from "../util/resources"
import { googleFontHref } from "../util/theme" import { googleFontHref, googleFontSubsetHref } from "../util/theme"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { unescapeHTML } from "../util/escape"
import { CustomOgImagesEmitterName } from "../plugins/emitters/ogImage"
export default (() => { export default (() => {
const Head: QuartzComponent = ({ cfg, fileData, externalResources }: QuartzComponentProps) => { const Head: QuartzComponent = ({
const title = fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title cfg,
fileData,
externalResources,
ctx,
}: QuartzComponentProps) => {
const titleSuffix = cfg.pageTitleSuffix ?? ""
const title =
(fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title) + titleSuffix
const description = const description =
fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description fileData.frontmatter?.socialDescription ??
const { css, js } = externalResources fileData.frontmatter?.description ??
unescapeHTML(fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description)
const { css, js, additionalHead } = externalResources
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`) const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
const path = url.pathname as FullSlug const path = url.pathname as FullSlug
const baseDir = fileData.slug === "404" ? path : pathToRoot(fileData.slug!) const baseDir = fileData.slug === "404" ? path : pathToRoot(fileData.slug!)
const iconPath = joinSegments(baseDir, "static/icon.png") const iconPath = joinSegments(baseDir, "static/icon.png")
const ogImagePath = `https://${cfg.baseUrl}/static/og-image.png`
// Url of current page
const socialUrl =
fileData.slug === "404" ? url.toString() : joinSegments(url.toString(), fileData.slug!)
const usesCustomOgImage = ctx.cfg.plugins.emitters.some(
(e) => e.name === CustomOgImagesEmitterName,
)
const ogImageDefaultPath = `https://${cfg.baseUrl}/static/og-image.png`
return ( return (
<head> <head>
@@ -27,25 +45,59 @@ export default (() => {
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" />
<link rel="stylesheet" href={googleFontHref(cfg.theme)} /> <link rel="stylesheet" href={googleFontHref(cfg.theme)} />
{cfg.theme.typography.title && (
<link rel="stylesheet" href={googleFontSubsetHref(cfg.theme, cfg.pageTitle)} />
)}
</> </>
)} )}
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossOrigin="anonymous" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="fediverse:creator" content={cfg.fediverseCreator} />
{cfg.baseUrl && <meta property="og:image" content={ogImagePath} />} <meta name="og:site_name" content={cfg.pageTitle}></meta>
<meta property="og:width" content="1200" /> <meta property="og:title" content={title} />
<meta property="og:height" content="675" /> <meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta property="fediverse:creator" content={cfg.fediverseCreator} />
<meta property="og:description" content={description} />
<meta property="og:image:alt" content={description} />
{!usesCustomOgImage && (
<>
<meta property="og:image" content={ogImageDefaultPath} />
<meta property="og:image:url" content={ogImageDefaultPath} />
<meta name="twitter:image" content={ogImageDefaultPath} />
<meta
property="og:image:type"
content={`image/${getFileExtension(ogImageDefaultPath) ?? "png"}`}
/>
</>
)}
{cfg.baseUrl && (
<>
<meta property="twitter:domain" content={cfg.baseUrl}></meta>
<meta property="og:url" content={socialUrl}></meta>
<meta property="twitter:url" content={socialUrl}></meta>
</>
)}
<link rel="icon" href={iconPath} /> <link rel="icon" href={iconPath} />
<meta name="description" content={description} /> <meta name="description" content={description} />
<meta name="generator" content="Quartz" /> <meta name="generator" content="Quartz" />
{css.map((href) => (
<link key={href} href={href} rel="stylesheet" type="text/css" spa-preserve /> {css.map((resource) => CSSResourceToStyleElement(resource, true))}
))}
{js {js
.filter((resource) => resource.loadTime === "beforeDOMReady") .filter((resource) => resource.loadTime === "beforeDOMReady")
.map((res) => JSResourceToScriptElement(res, true))} .map((res) => JSResourceToScriptElement(res, true))}
{additionalHead.map((resource) => {
if (typeof resource === "function") {
return resource(fileData)
} else {
return resource
}
})}
</head> </head>
) )
} }

View File

@@ -1,7 +1,6 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
export default ((component?: QuartzComponent) => { export default ((component: QuartzComponent) => {
if (component) {
const Component = component const Component = component
const MobileOnly: QuartzComponent = (props: QuartzComponentProps) => { const MobileOnly: QuartzComponent = (props: QuartzComponentProps) => {
return <Component displayClass="mobile-only" {...props} /> return <Component displayClass="mobile-only" {...props} />
@@ -12,7 +11,4 @@ export default ((component?: QuartzComponent) => {
MobileOnly.beforeDOMLoaded = component?.beforeDOMLoaded MobileOnly.beforeDOMLoaded = component?.beforeDOMLoaded
MobileOnly.css = component?.css MobileOnly.css = component?.css
return MobileOnly return MobileOnly
} else { }) satisfies QuartzComponentConstructor<QuartzComponent>
return () => <></>
}
}) satisfies QuartzComponentConstructor

View File

@@ -0,0 +1,48 @@
import { JSX } from "preact"
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>
)
}
let numExplorers = 0
export default () => {
const id = `list-${numExplorers++}`
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

@@ -1,4 +1,4 @@
import { FullSlug, resolveRelative } from "../util/path" import { FullSlug, isFolderPath, resolveRelative } from "../util/path"
import { QuartzPluginData } from "../plugins/vfile" import { QuartzPluginData } from "../plugins/vfile"
import { Date, getDate } from "./Date" import { Date, getDate } from "./Date"
import { QuartzComponent, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentProps } from "./types"
@@ -8,6 +8,33 @@ export type SortFn = (f1: QuartzPluginData, f2: QuartzPluginData) => number
export function byDateAndAlphabetical(cfg: GlobalConfiguration): SortFn { export function byDateAndAlphabetical(cfg: GlobalConfiguration): SortFn {
return (f1, f2) => { return (f1, f2) => {
// Sort by date/alphabetical
if (f1.dates && f2.dates) {
// sort descending
return getDate(cfg, f2)!.getTime() - getDate(cfg, f1)!.getTime()
} else if (f1.dates && !f2.dates) {
// prioritize files with dates
return -1
} else if (!f1.dates && f2.dates) {
return 1
}
// otherwise, sort lexographically by title
const f1Title = f1.frontmatter?.title.toLowerCase() ?? ""
const f2Title = f2.frontmatter?.title.toLowerCase() ?? ""
return f1Title.localeCompare(f2Title)
}
}
export function byDateAndAlphabeticalFolderFirst(cfg: GlobalConfiguration): SortFn {
return (f1, f2) => {
// Sort folders first
const f1IsFolder = isFolderPath(f1.slug ?? "")
const f2IsFolder = isFolderPath(f2.slug ?? "")
if (f1IsFolder && !f2IsFolder) return -1
if (!f1IsFolder && f2IsFolder) return 1
// If both are folders or both are files, sort by date/alphabetical
if (f1.dates && f2.dates) { if (f1.dates && f2.dates) {
// sort descending // sort descending
return getDate(cfg, f2)!.getTime() - getDate(cfg, f1)!.getTime() return getDate(cfg, f2)!.getTime() - getDate(cfg, f1)!.getTime()
@@ -31,7 +58,7 @@ type Props = {
} & QuartzComponentProps } & QuartzComponentProps
export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit, sort }: Props) => { export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit, sort }: Props) => {
const sorter = sort ?? byDateAndAlphabetical(cfg) const sorter = sort ?? byDateAndAlphabeticalFolderFirst(cfg)
let list = allFiles.sort(sorter) let list = allFiles.sort(sorter)
if (limit) { if (limit) {
list = list.slice(0, limit) list = list.slice(0, limit)
@@ -46,11 +73,9 @@ export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit, sort
return ( return (
<li class="section-li"> <li class="section-li">
<div class="section"> <div class="section">
{page.dates && (
<p class="meta"> <p class="meta">
<Date date={getDate(cfg, page)!} locale={cfg.locale} /> {page.dates && <Date date={getDate(cfg, page)!} locale={cfg.locale} />}
</p> </p>
)}
<div class="desc"> <div class="desc">
<h3> <h3>
<a href={resolveRelative(fileData.slug!, page.slug!)} class="internal"> <a href={resolveRelative(fileData.slug!, page.slug!)} class="internal">

View File

@@ -17,6 +17,7 @@ PageTitle.css = `
.page-title { .page-title {
font-size: 1.75rem; font-size: 1.75rem;
margin: 0; margin: 0;
font-family: var(--titleFont);
} }
` `

View File

@@ -0,0 +1,38 @@
// @ts-ignore
import readerModeScript from "./scripts/readermode.inline"
import styles from "./styles/readermode.scss"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { i18n } from "../i18n"
import { classNames } from "../util/lang"
const ReaderMode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
return (
<button class={classNames(displayClass, "readermode")}>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
version="1.1"
class="readerIcon"
fill="currentColor"
stroke="currentColor"
stroke-width="0.2"
stroke-linecap="round"
stroke-linejoin="round"
width="64px"
height="64px"
viewBox="0 0 24 24"
aria-label={i18n(cfg.locale).components.readerMode.title}
>
<title>{i18n(cfg.locale).components.readerMode.title}</title>
<g transform="translate(-1.8, -1.8) scale(1.15, 1.2)">
<path d="M8.9891247,2.5 C10.1384702,2.5 11.2209868,2.96705384 12.0049645,3.76669482 C12.7883914,2.96705384 13.8709081,2.5 15.0202536,2.5 L18.7549359,2.5 C19.1691495,2.5 19.5049359,2.83578644 19.5049359,3.25 L19.5046891,4.004 L21.2546891,4.00457396 C21.6343849,4.00457396 21.9481801,4.28672784 21.9978425,4.6528034 L22.0046891,4.75457396 L22.0046891,20.25 C22.0046891,20.6296958 21.7225353,20.943491 21.3564597,20.9931534 L21.2546891,21 L2.75468914,21 C2.37499337,21 2.06119817,20.7178461 2.01153575,20.3517706 L2.00468914,20.25 L2.00468914,4.75457396 C2.00468914,4.37487819 2.28684302,4.061083 2.65291858,4.01142057 L2.75468914,4.00457396 L4.50368914,4.004 L4.50444233,3.25 C4.50444233,2.87030423 4.78659621,2.55650904 5.15267177,2.50684662 L5.25444233,2.5 L8.9891247,2.5 Z M4.50368914,5.504 L3.50468914,5.504 L3.50468914,19.5 L10.9478955,19.4998273 C10.4513189,18.9207296 9.73864328,18.5588115 8.96709342,18.5065584 L8.77307039,18.5 L5.25444233,18.5 C4.87474657,18.5 4.56095137,18.2178461 4.51128895,17.8517706 L4.50444233,17.75 L4.50368914,5.504 Z M19.5049359,17.75 C19.5049359,18.1642136 19.1691495,18.5 18.7549359,18.5 L15.2363079,18.5 C14.3910149,18.5 13.5994408,18.8724714 13.0614828,19.4998273 L20.5046891,19.5 L20.5046891,5.504 L19.5046891,5.504 L19.5049359,17.75 Z M18.0059359,3.999 L15.0202536,4 L14.8259077,4.00692283 C13.9889509,4.06666544 13.2254227,4.50975805 12.7549359,5.212 L12.7549359,17.777 L12.7782651,17.7601316 C13.4923805,17.2719483 14.3447024,17 15.2363079,17 L18.0059359,16.999 L18.0056891,4.798 L18.0033792,4.75457396 L18.0056891,4.71 L18.0059359,3.999 Z M8.9891247,4 L6.00368914,3.999 L6.00599909,4.75457396 L6.00599909,4.75457396 L6.00368914,4.783 L6.00368914,16.999 L8.77307039,17 C9.57551536,17 10.3461406,17.2202781 11.0128313,17.6202194 L11.2536891,17.776 L11.2536891,5.211 C10.8200889,4.56369974 10.1361548,4.13636104 9.37521067,4.02745763 L9.18347055,4.00692283 L8.9891247,4 Z" />
</g>
</svg>
</button>
)
}
ReaderMode.beforeDOMLoaded = readerModeScript
ReaderMode.css = styles
export default (() => ReaderMode) satisfies QuartzComponentConstructor

View File

@@ -19,35 +19,27 @@ 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")}>
<div id="search-icon"> <button class="search-button">
<p>{i18n(cfg.locale).components.search.title}</p> <p>{i18n(cfg.locale).components.search.title}</p>
<div></div> <svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.9 19.7">
<svg <title>Search</title>
tabIndex={0}
aria-labelledby="title desc"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 19.9 19.7"
>
<title id="title">Search</title>
<desc id="desc">Search</desc>
<g class="search-path" fill="none"> <g class="search-path" fill="none">
<path stroke-linecap="square" d="M18.5 18.3l-5.4-5.4" /> <path stroke-linecap="square" d="M18.5 18.3l-5.4-5.4" />
<circle cx="8" cy="8" r="7" /> <circle cx="8" cy="8" r="7" />
</g> </g>
</svg> </svg>
</div> </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,18 +17,26 @@ const defaultOptions: Options = {
layout: "modern", layout: "modern",
} }
const TableOfContents: QuartzComponent = ({ export default ((opts?: Partial<Options>) => {
const layout = opts?.layout ?? defaultOptions.layout
const { OverflowList, overflowListAfterDOMLoaded } = OverflowListFactory()
const TableOfContents: QuartzComponent = ({
fileData, fileData,
displayClass, displayClass,
cfg, cfg,
}: QuartzComponentProps) => { }: QuartzComponentProps) => {
if (!fileData.toc) { if (!fileData.toc) {
return null return null
} }
return ( return (
<div class={classNames(displayClass, "toc")}> <div class={classNames(displayClass, "toc")}>
<button type="button" id="toc" class={fileData.collapseToc ? "collapsed" : ""}> <button
type="button"
class={fileData.collapseToc ? "collapsed toc-header" : "toc-header"}
aria-controls="toc-content"
aria-expanded={!fileData.collapseToc}
>
<h3>{i18n(cfg.locale).components.tableOfContents.title}</h3> <h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -43,8 +53,7 @@ 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"> <OverflowList class={fileData.collapseToc ? "collapsed toc-content" : "toc-content"}>
<ul class="overflow">
{fileData.toc.map((tocEntry) => ( {fileData.toc.map((tocEntry) => (
<li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}> <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
<a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}> <a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>
@@ -52,20 +61,20 @@ const TableOfContents: QuartzComponent = ({
</a> </a>
</li> </li>
))} ))}
</ul> </OverflowList>
</div>
</div> </div>
) )
} }
TableOfContents.css = modernStyle
TableOfContents.afterDOMLoaded = script
const LegacyTableOfContents: QuartzComponent = ({ fileData, cfg }: QuartzComponentProps) => { TableOfContents.css = modernStyle
TableOfContents.afterDOMLoaded = concatenateResources(script, overflowListAfterDOMLoaded)
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>
@@ -80,10 +89,8 @@ const LegacyTableOfContents: QuartzComponent = ({ fileData, cfg }: QuartzCompone
</ul> </ul>
</details> </details>
) )
} }
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

@@ -1,15 +1,14 @@
import { pathToRoot, slugTag } from "../util/path" import { FullSlug, resolveRelative } from "../util/path"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { classNames } from "../util/lang" import { classNames } from "../util/lang"
const TagList: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => { const TagList: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => {
const tags = fileData.frontmatter?.tags const tags = fileData.frontmatter?.tags
const baseDir = pathToRoot(fileData.slug!)
if (tags && tags.length > 0) { if (tags && tags.length > 0) {
return ( return (
<ul class={classNames(displayClass, "tags")}> <ul class={classNames(displayClass, "tags")}>
{tags.map((tag) => { {tags.map((tag) => {
const linkDest = baseDir + `/tags/${slugTag(tag)}` const linkDest = resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug)
return ( return (
<li> <li>
<a href={linkDest} class="internal tag-link"> <a href={linkDest} class="internal tag-link">
@@ -33,7 +32,6 @@ TagList.css = `
gap: 0.4rem; gap: 0.4rem;
margin: 1rem 0; margin: 1rem 0;
flex-wrap: wrap; flex-wrap: wrap;
justify-self: end;
} }
.section-li > .section > .tags { .section-li > .section > .tags {

View File

@@ -4,6 +4,7 @@ import FolderContent from "./pages/FolderContent"
import NotFound from "./pages/404" import NotFound from "./pages/404"
import ArticleTitle from "./ArticleTitle" import ArticleTitle from "./ArticleTitle"
import Darkmode from "./Darkmode" import Darkmode from "./Darkmode"
import ReaderMode from "./ReaderMode"
import Head from "./Head" import Head from "./Head"
import PageTitle from "./PageTitle" import PageTitle from "./PageTitle"
import ContentMeta from "./ContentMeta" import ContentMeta from "./ContentMeta"
@@ -20,6 +21,8 @@ import MobileOnly from "./MobileOnly"
import RecentNotes from "./RecentNotes" import RecentNotes from "./RecentNotes"
import Breadcrumbs from "./Breadcrumbs" import Breadcrumbs from "./Breadcrumbs"
import Comments from "./Comments" import Comments from "./Comments"
import Flex from "./Flex"
import ConditionalRender from "./ConditionalRender"
export { export {
ArticleTitle, ArticleTitle,
@@ -27,6 +30,7 @@ export {
TagContent, TagContent,
FolderContent, FolderContent,
Darkmode, Darkmode,
ReaderMode,
Head, Head,
PageTitle, PageTitle,
ContentMeta, ContentMeta,
@@ -44,4 +48,6 @@ export {
NotFound, NotFound,
Breadcrumbs, Breadcrumbs,
Comments, Comments,
Flex,
ConditionalRender,
} }

View File

@@ -1,8 +1,9 @@
import { ComponentChildren } from "preact"
import { htmlToJsx } from "../../util/jsx" import { htmlToJsx } from "../../util/jsx"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
const Content: QuartzComponent = ({ fileData, tree }: QuartzComponentProps) => { const Content: QuartzComponent = ({ fileData, tree }: QuartzComponentProps) => {
const content = htmlToJsx(fileData.filePath!, tree) const content = htmlToJsx(fileData.filePath!, tree) as ComponentChildren
const classes: string[] = fileData.frontmatter?.cssclasses ?? [] const classes: string[] = fileData.frontmatter?.cssclasses ?? []
const classString = ["popover-hint", ...classes].join(" ") const classString = ["popover-hint", ...classes].join(" ")
return <article class={classString}>{content}</article> return <article class={classString}>{content}</article>

View File

@@ -1,23 +1,27 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
import path from "path"
import style from "../styles/listPage.scss" import style from "../styles/listPage.scss"
import { PageList, SortFn } from "../PageList" import { PageList, SortFn } from "../PageList"
import { stripSlashes, simplifySlug } from "../../util/path"
import { Root } from "hast" import { Root } from "hast"
import { htmlToJsx } from "../../util/jsx" import { htmlToJsx } from "../../util/jsx"
import { i18n } from "../../i18n" import { i18n } from "../../i18n"
import { QuartzPluginData } from "../../plugins/vfile"
import { ComponentChildren } from "preact"
import { concatenateResources } from "../../util/resources"
import { trieFromAllFiles } from "../../util/ctx"
interface FolderContentOptions { interface FolderContentOptions {
/** /**
* Whether to display number of folders * Whether to display number of folders
*/ */
showFolderCount: boolean showFolderCount: boolean
showSubfolders: boolean
sort?: SortFn sort?: SortFn
} }
const defaultOptions: FolderContentOptions = { const defaultOptions: FolderContentOptions = {
showFolderCount: true, showFolderCount: true,
showSubfolders: true,
} }
export default ((opts?: Partial<FolderContentOptions>) => { export default ((opts?: Partial<FolderContentOptions>) => {
@@ -25,31 +29,82 @@ export default ((opts?: Partial<FolderContentOptions>) => {
const FolderContent: QuartzComponent = (props: QuartzComponentProps) => { const FolderContent: QuartzComponent = (props: QuartzComponentProps) => {
const { tree, fileData, allFiles, cfg } = props const { tree, fileData, allFiles, cfg } = props
const folderSlug = stripSlashes(simplifySlug(fileData.slug!))
const allPagesInFolder = allFiles.filter((file) => { const trie = (props.ctx.trie ??= trieFromAllFiles(allFiles))
const fileSlug = stripSlashes(simplifySlug(file.slug!)) const folder = trie.findNode(fileData.slug!.split("/"))
const prefixed = fileSlug.startsWith(folderSlug) && fileSlug !== folderSlug if (!folder) {
const folderParts = folderSlug.split(path.posix.sep) return null
const fileParts = fileSlug.split(path.posix.sep) }
const isDirectChild = fileParts.length === folderParts.length + 1
return prefixed && isDirectChild const allPagesInFolder: QuartzPluginData[] =
folder.children
.map((node) => {
// regular file, proceed
if (node.data) {
return node.data
}
if (node.isFolder && options.showSubfolders) {
// folders that dont have data need synthetic files
const getMostRecentDates = (): QuartzPluginData["dates"] => {
let maybeDates: QuartzPluginData["dates"] | undefined = undefined
for (const child of node.children) {
if (child.data?.dates) {
// compare all dates and assign to maybeDates if its more recent or its not set
if (!maybeDates) {
maybeDates = { ...child.data.dates }
} else {
if (child.data.dates.created > maybeDates.created) {
maybeDates.created = child.data.dates.created
}
if (child.data.dates.modified > maybeDates.modified) {
maybeDates.modified = child.data.dates.modified
}
if (child.data.dates.published > maybeDates.published) {
maybeDates.published = child.data.dates.published
}
}
}
}
return (
maybeDates ?? {
created: new Date(),
modified: new Date(),
published: new Date(),
}
)
}
return {
slug: node.slug,
dates: getMostRecentDates(),
frontmatter: {
title: node.displayName,
tags: [],
},
}
}
}) })
.filter((page) => page !== undefined) ?? []
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? [] const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
const classes = ["popover-hint", ...cssClasses].join(" ") const classes = cssClasses.join(" ")
const listProps = { const listProps = {
...props, ...props,
sort: options.sort, sort: options.sort,
allFiles: allPagesInFolder, allFiles: allPagesInFolder,
} }
const content = const content = (
(tree as Root).children.length === 0 (tree as Root).children.length === 0
? fileData.description ? fileData.description
: htmlToJsx(fileData.filePath!, tree) : htmlToJsx(fileData.filePath!, tree)
) as ComponentChildren
return ( return (
<div class={classes}> <div class="popover-hint">
<article>{content}</article> <article class={classes}>{content}</article>
<div class="page-listing"> <div class="page-listing">
{options.showFolderCount && ( {options.showFolderCount && (
<p> <p>
@@ -66,6 +121,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

@@ -1,11 +1,13 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
import style from "../styles/listPage.scss" import style from "../styles/listPage.scss"
import { PageList, SortFn } from "../PageList" import { PageList, SortFn } from "../PageList"
import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path" import { FullSlug, getAllSegmentPrefixes, resolveRelative, simplifySlug } from "../../util/path"
import { QuartzPluginData } from "../../plugins/vfile" import { QuartzPluginData } from "../../plugins/vfile"
import { Root } from "hast" 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 { concatenateResources } from "../../util/resources"
interface TagContentOptions { interface TagContentOptions {
sort?: SortFn sort?: SortFn
@@ -33,12 +35,13 @@ export default ((opts?: Partial<TagContentOptions>) => {
(file.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes).includes(tag), (file.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes).includes(tag),
) )
const content = const content = (
(tree as Root).children.length === 0 (tree as Root).children.length === 0
? fileData.description ? fileData.description
: htmlToJsx(fileData.filePath!, tree) : htmlToJsx(fileData.filePath!, tree)
) as ComponentChildren
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? [] const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
const classes = ["popover-hint", ...cssClasses].join(" ") const classes = cssClasses.join(" ")
if (tag === "/") { if (tag === "/") {
const tags = [ const tags = [
...new Set( ...new Set(
@@ -50,8 +53,8 @@ export default ((opts?: Partial<TagContentOptions>) => {
tagItemMap.set(tag, allPagesWithTag(tag)) tagItemMap.set(tag, allPagesWithTag(tag))
} }
return ( return (
<div class={classes}> <div class="popover-hint">
<article> <article class={classes}>
<p>{content}</p> <p>{content}</p>
</article> </article>
<p>{i18n(cfg.locale).pages.tagContent.totalTags({ count: tags.length })}</p> <p>{i18n(cfg.locale).pages.tagContent.totalTags({ count: tags.length })}</p>
@@ -71,10 +74,13 @@ export default ((opts?: Partial<TagContentOptions>) => {
? contentPage?.description ? contentPage?.description
: htmlToJsx(contentPage.filePath!, root) : htmlToJsx(contentPage.filePath!, root)
const tagListingPage = `/tags/${tag}` as FullSlug
const href = resolveRelative(fileData.slug!, tagListingPage)
return ( return (
<div> <div>
<h2> <h2>
<a class="internal tag-link" href={`../tags/${tag}`}> <a class="internal tag-link" href={href}>
{tag} {tag}
</a> </a>
</h2> </h2>
@@ -93,7 +99,7 @@ export default ((opts?: Partial<TagContentOptions>) => {
</> </>
)} )}
</p> </p>
<PageList limit={options.numPages} {...listProps} sort={opts?.sort} /> <PageList limit={options.numPages} {...listProps} sort={options?.sort} />
</div> </div>
</div> </div>
) )
@@ -109,12 +115,12 @@ export default ((opts?: Partial<TagContentOptions>) => {
} }
return ( return (
<div class={classes}> <div class="popover-hint">
<article>{content}</article> <article class={classes}>{content}</article>
<div class="page-listing"> <div class="page-listing">
<p>{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}</p> <p>{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}</p>
<div> <div>
<PageList {...listProps} /> <PageList {...listProps} sort={options?.sort} />
</div> </div>
</div> </div>
</div> </div>
@@ -122,6 +128,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,7 +3,8 @@ import { QuartzComponent, QuartzComponentProps } from "./types"
import HeaderConstructor from "./Header" import HeaderConstructor from "./Header"
import BodyConstructor from "./Body" import BodyConstructor from "./Body"
import { JSResourceToScriptElement, StaticResources } from "../util/resources" import { JSResourceToScriptElement, StaticResources } from "../util/resources"
import { clone, FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path" import { FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path"
import { clone } from "../util/clone"
import { visit } from "unist-util-visit" import { visit } from "unist-util-visit"
import { Root, Element, ElementContent } from "hast" import { Root, Element, ElementContent } from "hast"
import { GlobalConfiguration } from "../cfg" import { GlobalConfiguration } from "../cfg"
@@ -28,8 +29,13 @@ export function pageResources(
const contentIndexPath = joinSegments(baseDir, "static/contentIndex.json") const contentIndexPath = joinSegments(baseDir, "static/contentIndex.json")
const contentIndexScript = `const fetchData = fetch("${contentIndexPath}").then(data => data.json())` const contentIndexScript = `const fetchData = fetch("${contentIndexPath}").then(data => data.json())`
return { const resources: StaticResources = {
css: [joinSegments(baseDir, "index.css"), ...staticResources.css], css: [
{
content: joinSegments(baseDir, "index.css"),
},
...staticResources.css,
],
js: [ js: [
{ {
src: joinSegments(baseDir, "prescript.js"), src: joinSegments(baseDir, "prescript.js"),
@@ -43,34 +49,33 @@ export function pageResources(
script: contentIndexScript, script: contentIndexScript,
}, },
...staticResources.js, ...staticResources.js,
{ ],
additionalHead: staticResources.additionalHead,
}
resources.js.push({
src: joinSegments(baseDir, "postscript.js"), src: joinSegments(baseDir, "postscript.js"),
loadTime: "afterDOMReady", loadTime: "afterDOMReady",
moduleType: "module", moduleType: "module",
contentType: "external", contentType: "external",
}, })
],
} return resources
} }
export function renderPage( function renderTranscludes(
root: Root,
cfg: GlobalConfiguration, cfg: GlobalConfiguration,
slug: FullSlug, slug: FullSlug,
componentData: QuartzComponentProps, componentData: QuartzComponentProps,
components: RenderComponents, ) {
pageResources: StaticResources,
): string {
// make a deep copy of the tree so we don't remove the transclusion references
// for the file cached in contentMap in build.ts
const root = clone(componentData.tree) as Root
// process transcludes in componentData // process transcludes in componentData
visit(root, "element", (node, _index, _parent) => { visit(root, "element", (node, _index, _parent) => {
if (node.tagName === "blockquote") { if (node.tagName === "blockquote") {
const classNames = (node.properties?.className ?? []) as string[] const classNames = (node.properties?.className ?? []) as string[]
if (classNames.includes("transclude")) { if (classNames.includes("transclude")) {
const inner = node.children[0] as Element const inner = node.children[0] as Element
const transcludeTarget = inner.properties["data-slug"] as FullSlug const transcludeTarget = (inner.properties["data-slug"] ?? slug) as FullSlug
const page = componentData.allFiles.find((f) => f.slug === transcludeTarget) const page = componentData.allFiles.find((f) => f.slug === transcludeTarget)
if (!page) { if (!page) {
return return
@@ -179,6 +184,19 @@ export function renderPage(
} }
} }
}) })
}
export function renderPage(
cfg: GlobalConfiguration,
slug: FullSlug,
componentData: QuartzComponentProps,
components: RenderComponents,
pageResources: StaticResources,
): string {
// make a deep copy of the tree so we don't remove the transclusion references
// for the file cached in contentMap in build.ts
const root = clone(componentData.tree) as Root
renderTranscludes(root, cfg, slug, componentData)
// set componentData.tree to the edited html that has transclusions rendered // set componentData.tree to the edited html that has transclusions rendered
componentData.tree = root componentData.tree = root
@@ -242,8 +260,8 @@ export function renderPage(
</div> </div>
</div> </div>
{RightComponent} {RightComponent}
</Body>
<Footer {...componentData} /> <Footer {...componentData} />
</Body>
</div> </div>
</body> </body>
{pageResources.js {pageResources.js

View File

@@ -1,25 +1,10 @@
function toggleCallout(this: HTMLElement) { function toggleCallout(this: HTMLElement) {
const outerBlock = this.parentElement! const outerBlock = this.parentElement!
outerBlock.classList.toggle("is-collapsed") outerBlock.classList.toggle("is-collapsed")
const content = outerBlock.getElementsByClassName("callout-content")[0] as HTMLElement
if (!content) return
const collapsed = outerBlock.classList.contains("is-collapsed") const collapsed = outerBlock.classList.contains("is-collapsed")
const height = collapsed ? this.scrollHeight : outerBlock.scrollHeight content.style.gridTemplateRows = collapsed ? "0fr" : "1fr"
outerBlock.style.maxHeight = height + "px"
// walk and adjust height of all parents
let current = outerBlock
let parent = outerBlock.parentElement
while (parent) {
if (!parent.classList.contains("callout")) {
return
}
const collapsed = parent.classList.contains("is-collapsed")
const height = collapsed ? parent.scrollHeight : parent.scrollHeight + current.scrollHeight
parent.style.maxHeight = height + "px"
current = parent
parent = parent.parentElement
}
} }
function setupCallout() { function setupCallout() {
@@ -27,18 +12,16 @@ function setupCallout() {
`callout is-collapsible`, `callout is-collapsible`,
) as HTMLCollectionOf<HTMLElement> ) as HTMLCollectionOf<HTMLElement>
for (const div of collapsible) { for (const div of collapsible) {
const title = div.firstElementChild const title = div.getElementsByClassName("callout-title")[0] as HTMLElement
const content = div.getElementsByClassName("callout-content")[0] as HTMLElement
if (!title || !content) continue
if (title) {
title.addEventListener("click", toggleCallout) title.addEventListener("click", toggleCallout)
window.addCleanup(() => title.removeEventListener("click", toggleCallout)) window.addCleanup(() => title.removeEventListener("click", toggleCallout))
const collapsed = div.classList.contains("is-collapsed") const collapsed = div.classList.contains("is-collapsed")
const height = collapsed ? title.scrollHeight : div.scrollHeight content.style.gridTemplateRows = collapsed ? "0fr" : "1fr"
div.style.maxHeight = height + "px"
}
} }
} }
document.addEventListener("nav", setupCallout) document.addEventListener("nav", setupCallout)
window.addEventListener("resize", setupCallout)

View File

@@ -8,7 +8,9 @@ document.addEventListener("nav", () => {
for (let i = 0; i < els.length; i++) { for (let i = 0; i < els.length; i++) {
const codeBlock = els[i].getElementsByTagName("code")[0] const codeBlock = els[i].getElementsByTagName("code")[0]
if (codeBlock) { if (codeBlock) {
const source = codeBlock.innerText.replace(/\n\n/g, "\n") const source = (
codeBlock.dataset.clipboard ? JSON.parse(codeBlock.dataset.clipboard) : codeBlock.innerText
).replace(/\n\n/g, "\n")
const button = document.createElement("button") const button = document.createElement("button")
button.className = "clipboard-button" button.className = "clipboard-button"
button.type = "button" button.type = "button"

View File

@@ -13,7 +13,7 @@ const changeTheme = (e: CustomEventMap["themechange"]) => {
{ {
giscus: { giscus: {
setConfig: { setConfig: {
theme: theme, theme: getThemeUrl(getThemeName(theme)),
}, },
}, },
}, },
@@ -21,12 +21,36 @@ const changeTheme = (e: CustomEventMap["themechange"]) => {
) )
} }
const getThemeName = (theme: string) => {
if (theme !== "dark" && theme !== "light") {
return theme
}
const giscusContainer = document.querySelector(".giscus") as GiscusElement
if (!giscusContainer) {
return theme
}
const darkGiscus = giscusContainer.dataset.darkTheme ?? "dark"
const lightGiscus = giscusContainer.dataset.lightTheme ?? "light"
return theme === "dark" ? darkGiscus : lightGiscus
}
const getThemeUrl = (theme: string) => {
const giscusContainer = document.querySelector(".giscus") as GiscusElement
if (!giscusContainer) {
return `https://giscus.app/themes/${theme}.css`
}
return `${giscusContainer.dataset.themeUrl ?? "https://giscus.app/themes"}/${theme}.css`
}
type GiscusElement = Omit<HTMLElement, "dataset"> & { type GiscusElement = Omit<HTMLElement, "dataset"> & {
dataset: DOMStringMap & { dataset: DOMStringMap & {
repo: `${string}/${string}` repo: `${string}/${string}`
repoId: string repoId: string
category: string category: string
categoryId: string categoryId: string
themeUrl: string
lightTheme: string
darkTheme: string
mapping: "url" | "title" | "og:title" | "specific" | "number" | "pathname" mapping: "url" | "title" | "og:title" | "specific" | "number" | "pathname"
strict: string strict: string
reactionsEnabled: string reactionsEnabled: string
@@ -57,7 +81,7 @@ document.addEventListener("nav", () => {
const theme = document.documentElement.getAttribute("saved-theme") const theme = document.documentElement.getAttribute("saved-theme")
if (theme) { if (theme) {
giscusScript.setAttribute("data-theme", theme) giscusScript.setAttribute("data-theme", getThemeUrl(getThemeName(theme)))
} }
giscusContainer.appendChild(giscusScript) giscusContainer.appendChild(giscusScript)

View File

@@ -10,8 +10,9 @@ const emitThemeChangeEvent = (theme: "light" | "dark") => {
} }
document.addEventListener("nav", () => { document.addEventListener("nav", () => {
const switchTheme = (e: Event) => { const switchTheme = () => {
const newTheme = (e.target as HTMLInputElement)?.checked ? "dark" : "light" const newTheme =
document.documentElement.getAttribute("saved-theme") === "dark" ? "light" : "dark"
document.documentElement.setAttribute("saved-theme", newTheme) document.documentElement.setAttribute("saved-theme", newTheme)
localStorage.setItem("theme", newTheme) localStorage.setItem("theme", newTheme)
emitThemeChangeEvent(newTheme) emitThemeChangeEvent(newTheme)
@@ -21,16 +22,12 @@ document.addEventListener("nav", () => {
const newTheme = e.matches ? "dark" : "light" const newTheme = e.matches ? "dark" : "light"
document.documentElement.setAttribute("saved-theme", newTheme) document.documentElement.setAttribute("saved-theme", newTheme)
localStorage.setItem("theme", newTheme) localStorage.setItem("theme", newTheme)
toggleSwitch.checked = e.matches
emitThemeChangeEvent(newTheme) emitThemeChangeEvent(newTheme)
} }
// Darkmode toggle for (const darkmodeButton of document.getElementsByClassName("darkmode")) {
const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement darkmodeButton.addEventListener("click", switchTheme)
toggleSwitch.addEventListener("change", switchTheme) window.addCleanup(() => darkmodeButton.removeEventListener("click", switchTheme))
window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
if (currentTheme === "dark") {
toggleSwitch.checked = true
} }
// Listen for changes in prefers-color-scheme // Listen for changes in prefers-color-scheme

View File

@@ -1,27 +1,40 @@
import { FolderState } from "../ExplorerNode" import { FileTrieNode } from "../../util/fileTrie"
import { FullSlug, resolveRelative, simplifySlug } from "../../util/path"
import { ContentDetails } from "../../plugins/emitters/contentIndex"
type MaybeHTMLElement = HTMLElement | undefined type MaybeHTMLElement = HTMLElement | undefined
let currentExplorerState: FolderState[]
const observer = new IntersectionObserver((entries) => {
// If last element is observed, remove gradient of "overflow" class so element is visible
const explorerUl = document.getElementById("explorer-ul")
if (!explorerUl) return
for (const entry of entries) {
if (entry.isIntersecting) {
explorerUl.classList.add("no-background")
} else {
explorerUl.classList.remove("no-background")
}
}
})
interface ParsedOptions {
folderClickBehavior: "collapse" | "link"
folderDefaultState: "collapsed" | "open"
useSavedState: boolean
sortFn: (a: FileTrieNode, b: FileTrieNode) => number
filterFn: (node: FileTrieNode) => boolean
mapFn: (node: FileTrieNode) => void
order: "sort" | "filter" | "map"[]
}
type FolderState = {
path: string
collapsed: boolean
}
let currentExplorerState: Array<FolderState>
function toggleExplorer(this: HTMLElement) { function toggleExplorer(this: HTMLElement) {
this.classList.toggle("collapsed") const nearestExplorer = this.closest(".explorer") as HTMLElement
const content = this.nextElementSibling as MaybeHTMLElement if (!nearestExplorer) return
if (!content) return const explorerCollapsed = nearestExplorer.classList.toggle("collapsed")
nearestExplorer.setAttribute(
"aria-expanded",
nearestExplorer.getAttribute("aria-expanded") === "true" ? "false" : "true",
)
content.classList.toggle("collapsed") if (!explorerCollapsed) {
content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px" // Stop <html> from being scrollable when mobile explorer is open
document.documentElement.classList.add("mobile-no-scroll")
} else {
document.documentElement.classList.remove("mobile-no-scroll")
}
} }
function toggleFolder(evt: MouseEvent) { function toggleFolder(evt: MouseEvent) {
@@ -29,104 +42,260 @@ function toggleFolder(evt: MouseEvent) {
const target = evt.target as MaybeHTMLElement const target = evt.target as MaybeHTMLElement
if (!target) return if (!target) return
// Check if target was svg icon or button
const isSvg = target.nodeName === "svg" const isSvg = target.nodeName === "svg"
const childFolderContainer = (
// corresponding <ul> element relative to clicked button/folder
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
childFolderContainer.classList.toggle("open") childFolderContainer.classList.toggle("open")
const isCollapsed = childFolderContainer.classList.contains("open")
setFolderState(childFolderContainer, !isCollapsed) // Collapse folder container
const fullFolderPath = currentFolderParent.dataset.folderpath as string const isCollapsed = !childFolderContainer.classList.contains("open")
toggleCollapsedByPath(currentExplorerState, fullFolderPath) 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,
})
}
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 {
const explorer = document.getElementById("explorer") const template = document.getElementById("template-file") as HTMLTemplateElement
if (!explorer) return 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 (explorer.dataset.behavior === "collapse") { if (currentSlug === node.slug) {
for (const item of document.getElementsByClassName( a.classList.add("active")
"folder-button",
) as HTMLCollectionOf<HTMLElement>) {
item.addEventListener("click", toggleFolder)
window.addCleanup(() => item.removeEventListener("click", toggleFolder))
}
} }
explorer.addEventListener("click", toggleExplorer) return li
window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer)) }
// Set up click handlers for each folder (click handler on folder "icon") function createFolderNode(
for (const item of document.getElementsByClassName( currentSlug: FullSlug,
"folder-icon", node: FileTrieNode,
) as HTMLCollectionOf<HTMLElement>) { opts: ParsedOptions,
item.addEventListener("click", toggleFolder) ): HTMLLIElement {
window.addCleanup(() => item.removeEventListener("click", toggleFolder)) 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.isFolder
? createFolderNode(currentSlug, child, opts)
: createFileNode(currentSlug, child)
ul.appendChild(childNode)
}
return li
}
async function setupExplorer(currentSlug: FullSlug) {
const allExplorers = document.querySelectorAll("div.explorer") as NodeListOf<HTMLElement>
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 useSavedFolderState = explorer?.dataset.savestate === "true" const serializedExplorerState = storageTree && opts.useSavedState ? JSON.parse(storageTree) : []
const oldExplorerState: FolderState[] = const oldIndex = new Map<string, boolean>(
storageTree && useSavedFolderState ? JSON.parse(storageTree) : [] serializedExplorerState.map((entry: FolderState) => [entry.path, entry.collapsed]),
const oldIndex = new Map(oldExplorerState.map((entry) => [entry.path, entry.collapsed])) )
const newExplorerState: FolderState[] = explorer.dataset.tree
? JSON.parse(explorer.dataset.tree) const data = await fetchData
: [] const entries = [...Object.entries(data)] as [FullSlug, ContentDetails][]
currentExplorerState = [] const trie = FileTrieNode.fromEntries(entries)
for (const { path, collapsed } of newExplorerState) {
currentExplorerState.push({ path, collapsed: oldIndex.get(path) ?? collapsed }) // Apply functions in order
for (const fn of opts.order) {
switch (fn) {
case "filter":
if (opts.filterFn) trie.filter(opts.filterFn)
break
case "map":
if (opts.mapFn) trie.map(opts.mapFn)
break
case "sort":
if (opts.sortFn) trie.sort(opts.sortFn)
break
}
} }
currentExplorerState.map((folderState) => { // Get folder paths for state management
const folderLi = document.querySelector( const folderPaths = trie.getFolderPaths()
`[data-folderpath='${folderState.path}']`, currentExplorerState = folderPaths.map((path) => {
) as MaybeHTMLElement const previousState = oldIndex.get(path)
const folderUl = folderLi?.parentElement?.nextElementSibling as MaybeHTMLElement return {
if (folderUl) { path,
setFolderState(folderUl, folderState.collapsed) collapsed:
previousState === undefined ? opts.folderDefaultState === "collapsed" : previousState,
} }
}) })
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))
}
}
} }
window.addEventListener("resize", setupExplorer) document.addEventListener("prenav", async () => {
document.addEventListener("nav", () => { // save explorer scrollTop position
setupExplorer() const explorer = document.querySelector(".explorer-ul")
observer.disconnect() if (!explorer) return
sessionStorage.setItem("explorerScrollTop", explorer.scrollTop.toString())
})
// select pseudo element at end of list document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
const lastItem = document.getElementById("explorer-end") const currentSlug = e.detail.url
if (lastItem) { await setupExplorer(currentSlug)
observer.observe(lastItem)
// if mobile hamburger is visible, collapse by default
for (const explorer of document.getElementsByClassName("explorer")) {
const mobileExplorer = explorer.querySelector(".mobile-explorer")
if (!mobileExplorer) return
if (mobileExplorer.checkVisibility()) {
explorer.classList.add("collapsed")
explorer.setAttribute("aria-expanded", "false")
// Allow <html> to be scrollable when mobile explorer is collapsed
document.documentElement.classList.remove("mobile-no-scroll")
}
mobileExplorer.classList.remove("hide-until-loaded")
}
})
window.addEventListener("resize", function () {
// Desktop explorer opens by default, and it stays open when the window is resized
// to mobile screen size. Applies `no-scroll` to <html> in this edge case.
const explorer = document.querySelector(".explorer")
if (explorer && !explorer.classList.contains("collapsed")) {
document.documentElement.classList.add("mobile-no-scroll")
return
} }
}) })
/**
* 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

@@ -1,19 +1,57 @@
import type { ContentDetails } from "../../plugins/emitters/contentIndex" import type { ContentDetails } from "../../plugins/emitters/contentIndex"
import * as d3 from "d3" import {
SimulationNodeDatum,
SimulationLinkDatum,
Simulation,
forceSimulation,
forceManyBody,
forceCenter,
forceLink,
forceCollide,
forceRadial,
zoomIdentity,
select,
drag,
zoom,
} from "d3"
import { Text, Graphics, Application, Container, Circle } from "pixi.js"
import { Group as TweenGroup, Tween as Tweened } from "@tweenjs/tween.js"
import { registerEscapeHandler, removeAllChildren } from "./util" import { registerEscapeHandler, removeAllChildren } from "./util"
import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path" import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path"
import { D3Config } from "../Graph"
type GraphicsInfo = {
color: string
gfx: Graphics
alpha: number
active: boolean
}
type NodeData = { type NodeData = {
id: SimpleSlug id: SimpleSlug
text: string text: string
tags: string[] tags: string[]
} & d3.SimulationNodeDatum } & SimulationNodeDatum
type LinkData = { type SimpleLinkData = {
source: SimpleSlug source: SimpleSlug
target: SimpleSlug target: SimpleSlug
} }
type LinkData = {
source: NodeData
target: NodeData
} & SimulationLinkDatum<NodeData>
type LinkRenderData = GraphicsInfo & {
simulationData: LinkData
}
type NodeRenderData = GraphicsInfo & {
simulationData: NodeData
label: Text
}
const localStorageKey = "graph-visited" const localStorageKey = "graph-visited"
function getVisited(): Set<SimpleSlug> { function getVisited(): Set<SimpleSlug> {
return new Set(JSON.parse(localStorage.getItem(localStorageKey) ?? "[]")) return new Set(JSON.parse(localStorage.getItem(localStorageKey) ?? "[]"))
@@ -25,11 +63,38 @@ function addToVisited(slug: SimpleSlug) {
localStorage.setItem(localStorageKey, JSON.stringify([...visited])) localStorage.setItem(localStorageKey, JSON.stringify([...visited]))
} }
async function renderGraph(container: string, fullSlug: FullSlug) { type TweenNode = {
update: (time: number) => void
stop: () => void
}
// workaround for pixijs webgpu issue: https://github.com/pixijs/pixijs/issues/11389
async function determineGraphicsAPI(): Promise<"webgpu" | "webgl"> {
const adapter = await navigator.gpu?.requestAdapter().catch(() => null)
const device = adapter && (await adapter.requestDevice().catch(() => null))
if (!device) {
return "webgl"
}
const canvas = document.createElement("canvas")
const gl =
(canvas.getContext("webgl2") as WebGL2RenderingContext | null) ??
(canvas.getContext("webgl") as WebGLRenderingContext | null)
// we have to return webgl so pixijs automatically falls back to canvas
if (!gl) {
return "webgl"
}
const webglMaxTextures = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS)
const webgpuMaxTextures = device.limits.maxSampledTexturesPerShaderStage
return webglMaxTextures === webgpuMaxTextures ? "webgpu" : "webgl"
}
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 {
@@ -45,7 +110,8 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
removeTags, removeTags,
showTags, showTags,
focusOnHover, focusOnHover,
} = JSON.parse(graph.dataset["cfg"]!) enableRadial,
} = JSON.parse(graph.dataset["cfg"]!) as D3Config
const data: Map<SimpleSlug, ContentDetails> = new Map( const data: Map<SimpleSlug, ContentDetails> = new Map(
Object.entries<ContentDetails>(await fetchData).map(([k, v]) => [ Object.entries<ContentDetails>(await fetchData).map(([k, v]) => [
@@ -53,10 +119,11 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
v, v,
]), ]),
) )
const links: LinkData[] = [] const links: SimpleLinkData[] = []
const tags: SimpleSlug[] = [] const tags: SimpleSlug[] = []
const validLinks = new Set(data.keys()) const validLinks = new Set(data.keys())
const tweens = new Map<string, TweenNode>()
for (const [source, details] of data.entries()) { for (const [source, details] of data.entries()) {
const outgoing = details.links ?? [] const outgoing = details.links ?? []
@@ -100,271 +167,508 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
if (showTags) tags.forEach((tag) => neighbourhood.add(tag)) if (showTags) tags.forEach((tag) => neighbourhood.add(tag))
} }
const graphData: { nodes: NodeData[]; links: LinkData[] } = { const nodes = [...neighbourhood].map((url) => {
nodes: [...neighbourhood].map((url) => {
const text = url.startsWith("tags/") ? "#" + url.substring(5) : (data.get(url)?.title ?? url) const text = url.startsWith("tags/") ? "#" + url.substring(5) : (data.get(url)?.title ?? url)
return { return {
id: url, id: url,
text: text, text,
tags: data.get(url)?.tags ?? [], tags: data.get(url)?.tags ?? [],
} }
}), })
links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)), const graphData: { nodes: NodeData[]; links: LinkData[] } = {
nodes,
links: links
.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target))
.map((l) => ({
source: nodes.find((n) => n.id === l.source)!,
target: nodes.find((n) => n.id === l.target)!,
})),
} }
const simulation: d3.Simulation<NodeData, LinkData> = d3
.forceSimulation(graphData.nodes)
.force("charge", d3.forceManyBody().strength(-100 * repelForce))
.force(
"link",
d3
.forceLink(graphData.links)
.id((d: any) => d.id)
.distance(linkDistance),
)
.force("center", d3.forceCenter().strength(centerForce))
const height = Math.max(graph.offsetHeight, 250)
const width = graph.offsetWidth const width = graph.offsetWidth
const height = Math.max(graph.offsetHeight, 250)
const svg = d3 // we virtualize the simulation and use pixi to actually render it
.select<HTMLElement, NodeData>("#" + container) const simulation: Simulation<NodeData, LinkData> = forceSimulation<NodeData>(graphData.nodes)
.append("svg") .force("charge", forceManyBody().strength(-100 * repelForce))
.attr("width", width) .force("center", forceCenter().strength(centerForce))
.attr("height", height) .force("link", forceLink(graphData.links).distance(linkDistance))
.attr("viewBox", [-width / 2 / scale, -height / 2 / scale, width / scale, height / scale]) .force("collide", forceCollide<NodeData>((n) => nodeRadius(n)).iterations(3))
// draw links between nodes const radius = (Math.min(width, height) / 2) * 0.8
const link = svg if (enableRadial) simulation.force("radial", forceRadial(radius).strength(0.2))
.append("g")
.selectAll("line")
.data(graphData.links)
.join("line")
.attr("class", "link")
.attr("stroke", "var(--lightgray)")
.attr("stroke-width", 1)
// svg groups // precompute style prop strings as pixi doesn't support css variables
const graphNode = svg.append("g").selectAll("g").data(graphData.nodes).enter().append("g") const cssVars = [
"--secondary",
"--tertiary",
"--gray",
"--light",
"--lightgray",
"--dark",
"--darkgray",
"--bodyFont",
] as const
const computedStyleMap = cssVars.reduce(
(acc, key) => {
acc[key] = getComputedStyle(document.documentElement).getPropertyValue(key)
return acc
},
{} as Record<(typeof cssVars)[number], string>,
)
// calculate color // calculate color
const color = (d: NodeData) => { const color = (d: NodeData) => {
const isCurrent = d.id === slug const isCurrent = d.id === slug
if (isCurrent) { if (isCurrent) {
return "var(--secondary)" return computedStyleMap["--secondary"]
} else if (visited.has(d.id) || d.id.startsWith("tags/")) { } else if (visited.has(d.id) || d.id.startsWith("tags/")) {
return "var(--tertiary)" return computedStyleMap["--tertiary"]
} else { } else {
return "var(--gray)" return computedStyleMap["--gray"]
} }
} }
const drag = (simulation: d3.Simulation<NodeData, LinkData>) => {
function dragstarted(event: any, d: NodeData) {
if (!event.active) simulation.alphaTarget(1).restart()
d.fx = d.x
d.fy = d.y
}
function dragged(event: any, d: NodeData) {
d.fx = event.x
d.fy = event.y
}
function dragended(event: any, d: NodeData) {
if (!event.active) simulation.alphaTarget(0)
d.fx = null
d.fy = null
}
const noop = () => {}
return d3
.drag<Element, NodeData>()
.on("start", enableDrag ? dragstarted : noop)
.on("drag", enableDrag ? dragged : noop)
.on("end", enableDrag ? dragended : noop)
}
function nodeRadius(d: NodeData) { function nodeRadius(d: NodeData) {
const numLinks = links.filter((l: any) => l.source.id === d.id || l.target.id === d.id).length const numLinks = graphData.links.filter(
(l) => l.source.id === d.id || l.target.id === d.id,
).length
return 2 + Math.sqrt(numLinks) return 2 + Math.sqrt(numLinks)
} }
let connectedNodes: SimpleSlug[] = [] let hoveredNodeId: string | null = null
let hoveredNeighbours: Set<string> = new Set()
const linkRenderData: LinkRenderData[] = []
const nodeRenderData: NodeRenderData[] = []
function updateHoverInfo(newHoveredId: string | null) {
hoveredNodeId = newHoveredId
// draw individual nodes if (newHoveredId === null) {
const node = graphNode hoveredNeighbours = new Set()
.append("circle") for (const n of nodeRenderData) {
.attr("class", "node") n.active = false
.attr("id", (d) => d.id) }
.attr("r", nodeRadius)
.attr("fill", color) for (const l of linkRenderData) {
.style("cursor", "pointer") l.active = false
.on("click", (_, d) => { }
const targ = resolveRelative(fullSlug, d.id) } else {
hoveredNeighbours = new Set()
for (const l of linkRenderData) {
const linkData = l.simulationData
if (linkData.source.id === newHoveredId || linkData.target.id === newHoveredId) {
hoveredNeighbours.add(linkData.source.id)
hoveredNeighbours.add(linkData.target.id)
}
l.active = linkData.source.id === newHoveredId || linkData.target.id === newHoveredId
}
for (const n of nodeRenderData) {
n.active = hoveredNeighbours.has(n.simulationData.id)
}
}
}
let dragStartTime = 0
let dragging = false
function renderLinks() {
tweens.get("link")?.stop()
const tweenGroup = new TweenGroup()
for (const l of linkRenderData) {
let alpha = 1
// if we are hovering over a node, we want to highlight the immediate neighbours
// with full alpha and the rest with default alpha
if (hoveredNodeId) {
alpha = l.active ? 1 : 0.2
}
l.color = l.active ? computedStyleMap["--gray"] : computedStyleMap["--lightgray"]
tweenGroup.add(new Tweened<LinkRenderData>(l).to({ alpha }, 200))
}
tweenGroup.getAll().forEach((tw) => tw.start())
tweens.set("link", {
update: tweenGroup.update.bind(tweenGroup),
stop() {
tweenGroup.getAll().forEach((tw) => tw.stop())
},
})
}
function renderLabels() {
tweens.get("label")?.stop()
const tweenGroup = new TweenGroup()
const defaultScale = 1 / scale
const activeScale = defaultScale * 1.1
for (const n of nodeRenderData) {
const nodeId = n.simulationData.id
if (hoveredNodeId === nodeId) {
tweenGroup.add(
new Tweened<Text>(n.label).to(
{
alpha: 1,
scale: { x: activeScale, y: activeScale },
},
100,
),
)
} else {
tweenGroup.add(
new Tweened<Text>(n.label).to(
{
alpha: n.label.alpha,
scale: { x: defaultScale, y: defaultScale },
},
100,
),
)
}
}
tweenGroup.getAll().forEach((tw) => tw.start())
tweens.set("label", {
update: tweenGroup.update.bind(tweenGroup),
stop() {
tweenGroup.getAll().forEach((tw) => tw.stop())
},
})
}
function renderNodes() {
tweens.get("hover")?.stop()
const tweenGroup = new TweenGroup()
for (const n of nodeRenderData) {
let alpha = 1
// if we are hovering over a node, we want to highlight the immediate neighbours
if (hoveredNodeId !== null && focusOnHover) {
alpha = n.active ? 1 : 0.2
}
tweenGroup.add(new Tweened<Graphics>(n.gfx, tweenGroup).to({ alpha }, 200))
}
tweenGroup.getAll().forEach((tw) => tw.start())
tweens.set("hover", {
update: tweenGroup.update.bind(tweenGroup),
stop() {
tweenGroup.getAll().forEach((tw) => tw.stop())
},
})
}
function renderPixiFromD3() {
renderNodes()
renderLinks()
renderLabels()
}
tweens.forEach((tween) => tween.stop())
tweens.clear()
const pixiPreference = await determineGraphicsAPI()
const app = new Application()
await app.init({
width,
height,
antialias: true,
autoStart: false,
autoDensity: true,
backgroundAlpha: 0,
preference: pixiPreference,
resolution: window.devicePixelRatio,
eventMode: "static",
})
graph.appendChild(app.canvas)
const stage = app.stage
stage.interactive = false
const labelsContainer = new Container<Text>({ zIndex: 3, isRenderGroup: true })
const nodesContainer = new Container<Graphics>({ zIndex: 2, isRenderGroup: true })
const linkContainer = new Container<Graphics>({ zIndex: 1, isRenderGroup: true })
stage.addChild(nodesContainer, labelsContainer, linkContainer)
for (const n of graphData.nodes) {
const nodeId = n.id
const label = new Text({
interactive: false,
eventMode: "none",
text: n.text,
alpha: 0,
anchor: { x: 0.5, y: 1.2 },
style: {
fontSize: fontSize * 15,
fill: computedStyleMap["--dark"],
fontFamily: computedStyleMap["--bodyFont"],
},
resolution: window.devicePixelRatio * 4,
})
label.scale.set(1 / scale)
let oldLabelOpacity = 0
const isTagNode = nodeId.startsWith("tags/")
const gfx = new Graphics({
interactive: true,
label: nodeId,
eventMode: "static",
hitArea: new Circle(0, 0, nodeRadius(n)),
cursor: "pointer",
})
.circle(0, 0, nodeRadius(n))
.fill({ color: isTagNode ? computedStyleMap["--light"] : color(n) })
.on("pointerover", (e) => {
updateHoverInfo(e.target.label)
oldLabelOpacity = label.alpha
if (!dragging) {
renderPixiFromD3()
}
})
.on("pointerleave", () => {
updateHoverInfo(null)
label.alpha = oldLabelOpacity
if (!dragging) {
renderPixiFromD3()
}
})
if (isTagNode) {
gfx.stroke({ width: 2, color: computedStyleMap["--tertiary"] })
}
nodesContainer.addChild(gfx)
labelsContainer.addChild(label)
const nodeRenderDatum: NodeRenderData = {
simulationData: n,
gfx,
label,
color: color(n),
alpha: 1,
active: false,
}
nodeRenderData.push(nodeRenderDatum)
}
for (const l of graphData.links) {
const gfx = new Graphics({ interactive: false, eventMode: "none" })
linkContainer.addChild(gfx)
const linkRenderDatum: LinkRenderData = {
simulationData: l,
gfx,
color: computedStyleMap["--lightgray"],
alpha: 1,
active: false,
}
linkRenderData.push(linkRenderDatum)
}
let currentTransform = zoomIdentity
if (enableDrag) {
select<HTMLCanvasElement, NodeData | undefined>(app.canvas).call(
drag<HTMLCanvasElement, NodeData | undefined>()
.container(() => app.canvas)
.subject(() => graphData.nodes.find((n) => n.id === hoveredNodeId))
.on("start", function dragstarted(event) {
if (!event.active) simulation.alphaTarget(1).restart()
event.subject.fx = event.subject.x
event.subject.fy = event.subject.y
event.subject.__initialDragPos = {
x: event.subject.x,
y: event.subject.y,
fx: event.subject.fx,
fy: event.subject.fy,
}
dragStartTime = Date.now()
dragging = true
})
.on("drag", function dragged(event) {
const initPos = event.subject.__initialDragPos
event.subject.fx = initPos.x + (event.x - initPos.x) / currentTransform.k
event.subject.fy = initPos.y + (event.y - initPos.y) / currentTransform.k
})
.on("end", function dragended(event) {
if (!event.active) simulation.alphaTarget(0)
event.subject.fx = null
event.subject.fy = null
dragging = false
// if the time between mousedown and mouseup is short, we consider it a click
if (Date.now() - dragStartTime < 500) {
const node = graphData.nodes.find((n) => n.id === event.subject.id) as NodeData
const targ = resolveRelative(fullSlug, node.id)
window.spaNavigate(new URL(targ, window.location.toString()))
}
}),
)
} else {
for (const node of nodeRenderData) {
node.gfx.on("click", () => {
const targ = resolveRelative(fullSlug, node.simulationData.id)
window.spaNavigate(new URL(targ, window.location.toString())) window.spaNavigate(new URL(targ, window.location.toString()))
}) })
.on("mouseover", function (_, d) { }
const currentId = d.id
const linkNodes = d3
.selectAll(".link")
.filter((d: any) => d.source.id === currentId || d.target.id === currentId)
if (focusOnHover) {
// fade out non-neighbour nodes
connectedNodes = linkNodes.data().flatMap((d: any) => [d.source.id, d.target.id])
d3.selectAll<HTMLElement, NodeData>(".link")
.transition()
.duration(200)
.style("opacity", 0.2)
d3.selectAll<HTMLElement, NodeData>(".node")
.filter((d) => !connectedNodes.includes(d.id))
.transition()
.duration(200)
.style("opacity", 0.2)
d3.selectAll<HTMLElement, NodeData>(".node")
.filter((d) => !connectedNodes.includes(d.id))
.nodes()
.map((it) => d3.select(it.parentNode as HTMLElement).select("text"))
.forEach((it) => {
let opacity = parseFloat(it.style("opacity"))
it.transition()
.duration(200)
.attr("opacityOld", opacity)
.style("opacity", Math.min(opacity, 0.2))
})
} }
// highlight links
linkNodes.transition().duration(200).attr("stroke", "var(--gray)").attr("stroke-width", 1)
const bigFont = fontSize * 1.5
// show text for self
const parent = this.parentNode as HTMLElement
d3.select<HTMLElement, NodeData>(parent)
.raise()
.select("text")
.transition()
.duration(200)
.attr("opacityOld", d3.select(parent).select("text").style("opacity"))
.style("opacity", 1)
.style("font-size", bigFont + "em")
})
.on("mouseleave", function (_, d) {
if (focusOnHover) {
d3.selectAll<HTMLElement, NodeData>(".link").transition().duration(200).style("opacity", 1)
d3.selectAll<HTMLElement, NodeData>(".node").transition().duration(200).style("opacity", 1)
d3.selectAll<HTMLElement, NodeData>(".node")
.filter((d) => !connectedNodes.includes(d.id))
.nodes()
.map((it) => d3.select(it.parentNode as HTMLElement).select("text"))
.forEach((it) => it.transition().duration(200).style("opacity", it.attr("opacityOld")))
}
const currentId = d.id
const linkNodes = d3
.selectAll(".link")
.filter((d: any) => d.source.id === currentId || d.target.id === currentId)
linkNodes.transition().duration(200).attr("stroke", "var(--lightgray)")
const parent = this.parentNode as HTMLElement
d3.select<HTMLElement, NodeData>(parent)
.select("text")
.transition()
.duration(200)
.style("opacity", d3.select(parent).select("text").attr("opacityOld"))
.style("font-size", fontSize + "em")
})
// @ts-ignore
.call(drag(simulation))
// make tags hollow circles
node
.filter((d) => d.id.startsWith("tags/"))
.attr("stroke", color)
.attr("stroke-width", 2)
.attr("fill", "var(--light)")
// draw labels
const labels = graphNode
.append("text")
.attr("dx", 0)
.attr("dy", (d) => -nodeRadius(d) + "px")
.attr("text-anchor", "middle")
.text((d) => d.text)
.style("opacity", (opacityScale - 1) / 3.75)
.style("pointer-events", "none")
.style("font-size", fontSize + "em")
.raise()
// @ts-ignore
.call(drag(simulation))
// set panning
if (enableZoom) { if (enableZoom) {
svg.call( select<HTMLCanvasElement, NodeData>(app.canvas).call(
d3 zoom<HTMLCanvasElement, NodeData>()
.zoom<SVGSVGElement, NodeData>()
.extent([ .extent([
[0, 0], [0, 0],
[width, height], [width, height],
]) ])
.scaleExtent([0.25, 4]) .scaleExtent([0.25, 4])
.on("zoom", ({ transform }) => { .on("zoom", ({ transform }) => {
link.attr("transform", transform) currentTransform = transform
node.attr("transform", transform) stage.scale.set(transform.k, transform.k)
stage.position.set(transform.x, transform.y)
// zoom adjusts opacity of labels too
const scale = transform.k * opacityScale const scale = transform.k * opacityScale
const scaledOpacity = Math.max((scale - 1) / 3.75, 0) let scaleOpacity = Math.max((scale - 1) / 3.75, 0)
labels.attr("transform", transform).style("opacity", scaledOpacity) const activeNodes = nodeRenderData.filter((n) => n.active).flatMap((n) => n.label)
for (const label of labelsContainer.children) {
if (!activeNodes.includes(label)) {
label.alpha = scaleOpacity
}
}
}), }),
) )
} }
// progress the simulation let stopAnimation = false
simulation.on("tick", () => { function animate(time: number) {
link if (stopAnimation) return
.attr("x1", (d: any) => d.source.x) for (const n of nodeRenderData) {
.attr("y1", (d: any) => d.source.y) const { x, y } = n.simulationData
.attr("x2", (d: any) => d.target.x) if (!x || !y) continue
.attr("y2", (d: any) => d.target.y) n.gfx.position.set(x + width / 2, y + height / 2)
node.attr("cx", (d: any) => d.x).attr("cy", (d: any) => d.y) if (n.label) {
labels.attr("x", (d: any) => d.x).attr("y", (d: any) => d.y) n.label.position.set(x + width / 2, y + height / 2)
}) }
}
for (const l of linkRenderData) {
const linkData = l.simulationData
l.gfx.clear()
l.gfx.moveTo(linkData.source.x! + width / 2, linkData.source.y! + height / 2)
l.gfx
.lineTo(linkData.target.x! + width / 2, linkData.target.y! + height / 2)
.stroke({ alpha: l.alpha, width: 1, color: l.color })
}
tweens.forEach((t) => t.update(time))
app.renderer.render(stage)
requestAnimationFrame(animate)
}
requestAnimationFrame(animate)
return () => {
stopAnimation = true
app.destroy()
}
} }
function renderGlobalGraph() { let localGraphCleanups: (() => void)[] = []
const slug = getFullSlug(window) let globalGraphCleanups: (() => void)[] = []
const container = document.getElementById("global-graph-outer")
const sidebar = container?.closest(".sidebar") as HTMLElement
container?.classList.add("active")
if (sidebar) {
sidebar.style.zIndex = "1"
}
renderGraph("global-graph-container", slug) function cleanupLocalGraphs() {
for (const cleanup of localGraphCleanups) {
function hideGlobalGraph() { cleanup()
container?.classList.remove("active")
const graph = document.getElementById("global-graph-container")
if (sidebar) {
sidebar.style.zIndex = "unset"
}
if (!graph) return
removeAllChildren(graph)
} }
localGraphCleanups = []
}
registerEscapeHandler(container, hideGlobalGraph) 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)
const containerIcon = document.getElementById("global-graph-icon") async function renderLocalGraph() {
containerIcon?.addEventListener("click", renderGlobalGraph) cleanupLocalGraphs()
window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph)) const localGraphContainers = document.getElementsByClassName("graph-container")
for (const container of localGraphContainers) {
localGraphCleanups.push(await renderGraph(container as HTMLElement, slug))
}
}
await renderLocalGraph()
const handleThemeChange = () => {
void renderLocalGraph()
}
document.addEventListener("themechange", handleThemeChange)
window.addCleanup(() => {
document.removeEventListener("themechange", handleThemeChange)
})
const containers = [...document.getElementsByClassName("global-graph-outer")] as HTMLElement[]
async function renderGlobalGraph() {
const slug = getFullSlug(window)
for (const container of containers) {
container.classList.add("active")
const sidebar = container.closest(".sidebar") as HTMLElement
if (sidebar) {
sidebar.style.zIndex = "1"
}
const graphContainer = container.querySelector(".global-graph-container") as HTMLElement
registerEscapeHandler(container, hideGlobalGraph)
if (graphContainer) {
globalGraphCleanups.push(await renderGraph(graphContainer, slug))
}
}
}
function hideGlobalGraph() {
cleanupGlobalGraphs()
for (const container of containers) {
container.classList.remove("active")
const sidebar = container.closest(".sidebar") as HTMLElement
if (sidebar) {
sidebar.style.zIndex = ""
}
}
}
async function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
if (e.key === "g" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
e.preventDefault()
const anyGlobalGraphOpen = containers.some((container) =>
container.classList.contains("active"),
)
anyGlobalGraphOpen ? hideGlobalGraph() : renderGlobalGraph()
}
}
const containerIcons = document.getElementsByClassName("global-graph-icon")
Array.from(containerIcons).forEach((icon) => {
icon.addEventListener("click", renderGlobalGraph)
window.addCleanup(() => icon.removeEventListener("click", renderGlobalGraph))
})
document.addEventListener("keydown", shortcutHandler)
window.addCleanup(() => {
document.removeEventListener("keydown", shortcutHandler)
cleanupLocalGraphs()
cleanupGlobalGraphs()
})
}) })

View File

@@ -0,0 +1,258 @@
import { registerEscapeHandler, removeAllChildren } from "./util"
interface Position {
x: number
y: number
}
class DiagramPanZoom {
private isDragging = false
private startPan: Position = { x: 0, y: 0 }
private currentPan: Position = { x: 0, y: 0 }
private scale = 1
private readonly MIN_SCALE = 0.5
private readonly MAX_SCALE = 3
cleanups: (() => void)[] = []
constructor(
private container: HTMLElement,
private content: HTMLElement,
) {
this.setupEventListeners()
this.setupNavigationControls()
this.resetTransform()
}
private setupEventListeners() {
// Mouse drag events
const mouseDownHandler = this.onMouseDown.bind(this)
const mouseMoveHandler = this.onMouseMove.bind(this)
const mouseUpHandler = this.onMouseUp.bind(this)
const resizeHandler = this.resetTransform.bind(this)
this.container.addEventListener("mousedown", mouseDownHandler)
document.addEventListener("mousemove", mouseMoveHandler)
document.addEventListener("mouseup", mouseUpHandler)
window.addEventListener("resize", resizeHandler)
this.cleanups.push(
() => this.container.removeEventListener("mousedown", mouseDownHandler),
() => document.removeEventListener("mousemove", mouseMoveHandler),
() => document.removeEventListener("mouseup", mouseUpHandler),
() => window.removeEventListener("resize", resizeHandler),
)
}
cleanup() {
for (const cleanup of this.cleanups) {
cleanup()
}
}
private setupNavigationControls() {
const controls = document.createElement("div")
controls.className = "mermaid-controls"
// Zoom controls
const zoomIn = this.createButton("+", () => this.zoom(0.1))
const zoomOut = this.createButton("-", () => this.zoom(-0.1))
const resetBtn = this.createButton("Reset", () => this.resetTransform())
controls.appendChild(zoomOut)
controls.appendChild(resetBtn)
controls.appendChild(zoomIn)
this.container.appendChild(controls)
}
private createButton(text: string, onClick: () => void): HTMLButtonElement {
const button = document.createElement("button")
button.textContent = text
button.className = "mermaid-control-button"
button.addEventListener("click", onClick)
window.addCleanup(() => button.removeEventListener("click", onClick))
return button
}
private onMouseDown(e: MouseEvent) {
if (e.button !== 0) return // Only handle left click
this.isDragging = true
this.startPan = { x: e.clientX - this.currentPan.x, y: e.clientY - this.currentPan.y }
this.container.style.cursor = "grabbing"
}
private onMouseMove(e: MouseEvent) {
if (!this.isDragging) return
e.preventDefault()
this.currentPan = {
x: e.clientX - this.startPan.x,
y: e.clientY - this.startPan.y,
}
this.updateTransform()
}
private onMouseUp() {
this.isDragging = false
this.container.style.cursor = "grab"
}
private zoom(delta: number) {
const newScale = Math.min(Math.max(this.scale + delta, this.MIN_SCALE), this.MAX_SCALE)
// Zoom around center
const rect = this.content.getBoundingClientRect()
const centerX = rect.width / 2
const centerY = rect.height / 2
const scaleDiff = newScale - this.scale
this.currentPan.x -= centerX * scaleDiff
this.currentPan.y -= centerY * scaleDiff
this.scale = newScale
this.updateTransform()
}
private updateTransform() {
this.content.style.transform = `translate(${this.currentPan.x}px, ${this.currentPan.y}px) scale(${this.scale})`
}
private resetTransform() {
this.scale = 1
const svg = this.content.querySelector("svg")!
this.currentPan = {
x: svg.getBoundingClientRect().width / 2,
y: svg.getBoundingClientRect().height / 2,
}
this.updateTransform()
}
}
const cssVars = [
"--secondary",
"--tertiary",
"--gray",
"--light",
"--lightgray",
"--highlight",
"--dark",
"--darkgray",
"--codeFont",
] as const
let mermaidImport = undefined
document.addEventListener("nav", async () => {
const center = document.querySelector(".center") as HTMLElement
const nodes = center.querySelectorAll("code.mermaid") as NodeListOf<HTMLElement>
if (nodes.length === 0) return
mermaidImport ||= await import(
// @ts-ignore
"https://cdnjs.cloudflare.com/ajax/libs/mermaid/11.4.0/mermaid.esm.min.mjs"
)
const mermaid = mermaidImport.default
const textMapping: WeakMap<HTMLElement, string> = new WeakMap()
for (const node of nodes) {
textMapping.set(node, node.innerText)
}
async function renderMermaid() {
// de-init any other diagrams
for (const node of nodes) {
node.removeAttribute("data-processed")
const oldText = textMapping.get(node)
if (oldText) {
node.innerHTML = oldText
}
}
const computedStyleMap = cssVars.reduce(
(acc, key) => {
acc[key] = window.getComputedStyle(document.documentElement).getPropertyValue(key)
return acc
},
{} as Record<(typeof cssVars)[number], string>,
)
const darkMode = document.documentElement.getAttribute("saved-theme") === "dark"
mermaid.initialize({
startOnLoad: false,
securityLevel: "loose",
theme: darkMode ? "dark" : "base",
themeVariables: {
fontFamily: computedStyleMap["--codeFont"],
primaryColor: computedStyleMap["--light"],
primaryTextColor: computedStyleMap["--darkgray"],
primaryBorderColor: computedStyleMap["--tertiary"],
lineColor: computedStyleMap["--darkgray"],
secondaryColor: computedStyleMap["--secondary"],
tertiaryColor: computedStyleMap["--tertiary"],
clusterBkg: computedStyleMap["--light"],
edgeLabelBackground: computedStyleMap["--highlight"],
},
})
await mermaid.run({ nodes })
}
await renderMermaid()
document.addEventListener("themechange", renderMermaid)
window.addCleanup(() => document.removeEventListener("themechange", renderMermaid))
for (let i = 0; i < nodes.length; i++) {
const codeBlock = nodes[i] as HTMLElement
const pre = codeBlock.parentElement as HTMLPreElement
const clipboardBtn = pre.querySelector(".clipboard-button") as HTMLButtonElement
const expandBtn = pre.querySelector(".expand-button") as HTMLButtonElement
const clipboardStyle = window.getComputedStyle(clipboardBtn)
const clipboardWidth =
clipboardBtn.offsetWidth +
parseFloat(clipboardStyle.marginLeft || "0") +
parseFloat(clipboardStyle.marginRight || "0")
// Set expand button position
expandBtn.style.right = `calc(${clipboardWidth}px + 0.3rem)`
pre.prepend(expandBtn)
// query popup container
const popupContainer = pre.querySelector("#mermaid-container") as HTMLElement
if (!popupContainer) return
let panZoom: DiagramPanZoom | null = null
function showMermaid() {
const container = popupContainer.querySelector("#mermaid-space") as HTMLElement
const content = popupContainer.querySelector(".mermaid-content") as HTMLElement
if (!content) return
removeAllChildren(content)
// Clone the mermaid content
const mermaidContent = codeBlock.querySelector("svg")!.cloneNode(true) as SVGElement
content.appendChild(mermaidContent)
// Show container
popupContainer.classList.add("active")
container.style.cursor = "grab"
// Initialize pan-zoom after showing the popup
panZoom = new DiagramPanZoom(container, content)
}
function hideMermaid() {
popupContainer.classList.remove("active")
panZoom?.cleanup()
panZoom = null
}
expandBtn.addEventListener("click", showMermaid)
registerEscapeHandler(popupContainer, hideMermaid)
window.addCleanup(() => {
panZoom?.cleanup()
expandBtn.removeEventListener("click", showMermaid)
})
}
})

View File

@@ -1,62 +1,72 @@
import { computePosition, flip, inline, shift } from "@floating-ui/dom" import { computePosition, flip, inline, shift } from "@floating-ui/dom"
import { normalizeRelativeURLs } from "../../util/path" import { normalizeRelativeURLs } from "../../util/path"
import { fetchCanonical } from "./util"
const p = new DOMParser() const p = new DOMParser()
let activeAnchor: HTMLAnchorElement | null = null
async function mouseEnterHandler( async function mouseEnterHandler(
this: HTMLAnchorElement, this: HTMLAnchorElement,
{ clientX, clientY }: { clientX: number; clientY: number }, { clientX, clientY }: { clientX: number; clientY: number },
) { ) {
const link = this const link = (activeAnchor = this)
if (link.dataset.noPopover === "true") { if (link.dataset.noPopover === "true") {
return return
} }
async function setPosition(popoverElement: HTMLElement) { async function setPosition(popoverElement: HTMLElement) {
const { x, y } = await computePosition(link, popoverElement, { const { x, y } = await computePosition(link, popoverElement, {
strategy: "fixed",
middleware: [inline({ x: clientX, y: clientY }), shift(), flip()], middleware: [inline({ x: clientX, y: clientY }), shift(), flip()],
}) })
Object.assign(popoverElement.style, { Object.assign(popoverElement.style, {
left: `${x}px`, transform: `translate(${x.toFixed()}px, ${y.toFixed()}px)`,
top: `${y}px`,
}) })
} }
const hasAlreadyBeenFetched = () => function showPopover(popoverElement: HTMLElement) {
[...link.children].some((child) => child.classList.contains("popover")) clearActivePopover()
popoverElement.classList.add("active-popover")
setPosition(popoverElement as HTMLElement)
// dont refetch if there's already a popover if (hash !== "") {
if (hasAlreadyBeenFetched()) { const targetAnchor = `#popover-internal-${hash.slice(1)}`
return setPosition(link.lastChild as HTMLElement) const heading = popoverInner.querySelector(targetAnchor) as HTMLElement | null
if (heading) {
// leave ~12px of buffer when scrolling to a heading
popoverInner.scroll({ top: heading.offsetTop - 12, behavior: "instant" })
}
}
} }
const thisUrl = new URL(document.location.href)
thisUrl.hash = ""
thisUrl.search = ""
const targetUrl = new URL(link.href) const targetUrl = new URL(link.href)
const hash = decodeURIComponent(targetUrl.hash) const hash = decodeURIComponent(targetUrl.hash)
targetUrl.hash = "" targetUrl.hash = ""
targetUrl.search = "" targetUrl.search = ""
const popoverId = `popover-${link.pathname}`
const prevPopoverElement = document.getElementById(popoverId)
const response = await fetch(`${targetUrl}`).catch((err) => { // dont refetch if there's already a popover
console.error(err) if (!!document.getElementById(popoverId)) {
}) showPopover(prevPopoverElement as HTMLElement)
// bailout if another popover exists
if (hasAlreadyBeenFetched()) {
return return
} }
const response = await fetchCanonical(targetUrl).catch((err) => {
console.error(err)
})
if (!response) return if (!response) return
const [contentType] = response.headers.get("Content-Type")!.split(";") const [contentType] = response.headers.get("Content-Type")!.split(";")
const [contentTypeCategory, typeInfo] = contentType.split("/") const [contentTypeCategory, typeInfo] = contentType.split("/")
const popoverElement = document.createElement("div") const popoverElement = document.createElement("div")
popoverElement.id = popoverId
popoverElement.classList.add("popover") popoverElement.classList.add("popover")
const popoverInner = document.createElement("div") const popoverInner = document.createElement("div")
popoverInner.classList.add("popover-inner") popoverInner.classList.add("popover-inner")
popoverElement.appendChild(popoverInner)
popoverInner.dataset.contentType = contentType ?? undefined popoverInner.dataset.contentType = contentType ?? undefined
popoverElement.appendChild(popoverInner)
switch (contentTypeCategory) { switch (contentTypeCategory) {
case "image": case "image":
@@ -81,28 +91,43 @@ 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)
// prepend all IDs inside popovers to prevent duplicates
html.querySelectorAll("[id]").forEach((el) => {
const targetID = `popover-internal-${el.id}`
el.id = targetID
})
const elts = [...html.getElementsByClassName("popover-hint")] const elts = [...html.getElementsByClassName("popover-hint")]
if (elts.length === 0) return if (elts.length === 0) return
elts.forEach((elt) => popoverInner.appendChild(elt)) elts.forEach((elt) => popoverInner.appendChild(elt))
} }
setPosition(popoverElement) if (!!document.getElementById(popoverId)) {
link.appendChild(popoverElement) return
}
if (hash !== "") { document.body.appendChild(popoverElement)
const heading = popoverInner.querySelector(hash) as HTMLElement | null if (activeAnchor !== this) {
if (heading) { return
// leave ~12px of buffer when scrolling to a heading
popoverInner.scroll({ top: heading.offsetTop - 12, behavior: "instant" })
}
} }
showPopover(popoverElement)
}
function clearActivePopover() {
activeAnchor = null
const allPopoverElements = document.querySelectorAll(".popover")
allPopoverElements.forEach((popoverElement) => popoverElement.classList.remove("active-popover"))
} }
document.addEventListener("nav", () => { document.addEventListener("nav", () => {
const links = [...document.getElementsByClassName("internal")] as HTMLAnchorElement[] const links = [...document.querySelectorAll("a.internal")] as HTMLAnchorElement[]
for (const link of links) { for (const link of links) {
link.addEventListener("mouseenter", mouseEnterHandler) link.addEventListener("mouseenter", mouseEnterHandler)
window.addCleanup(() => link.removeEventListener("mouseenter", mouseEnterHandler)) link.addEventListener("mouseleave", clearActivePopover)
window.addCleanup(() => {
link.removeEventListener("mouseenter", mouseEnterHandler)
link.removeEventListener("mouseleave", clearActivePopover)
})
} }
}) })

View File

@@ -0,0 +1,25 @@
let isReaderMode = false
const emitReaderModeChangeEvent = (mode: "on" | "off") => {
const event: CustomEventMap["readermodechange"] = new CustomEvent("readermodechange", {
detail: { mode },
})
document.dispatchEvent(event)
}
document.addEventListener("nav", () => {
const switchReaderMode = () => {
isReaderMode = !isReaderMode
const newMode = isReaderMode ? "on" : "off"
document.documentElement.setAttribute("reader-mode", newMode)
emitReaderModeChangeEvent(newMode)
}
for (const readerModeButton of document.getElementsByClassName("readermode")) {
readerModeButton.addEventListener("click", switchReaderMode)
window.addCleanup(() => readerModeButton.removeEventListener("click", switchReaderMode))
}
// Set initial state
document.documentElement.setAttribute("reader-mode", isReaderMode ? "on" : "off")
})

View File

@@ -143,81 +143,74 @@ 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 | null
const searchIcon = document.getElementById("search-icon")
const searchBar = document.getElementById("search-bar") as HTMLInputElement | null const searchButton = searchElement.querySelector(".search-button") as HTMLButtonElement
const searchLayout = document.getElementById("search-layout") 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 = ""
if (sidebar) {
sidebar.style.zIndex = "unset"
}
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()
} }
function showSearch(searchTypeNew: SearchType) { function showSearch(searchTypeNew: SearchType) {
searchType = searchTypeNew searchType = searchTypeNew
if (sidebar) { if (sidebar) sidebar.style.zIndex = "1"
sidebar.style.zIndex = "1" container.classList.add("active")
} searchBar.focus()
container?.classList.add("active")
searchBar?.focus()
} }
let currentHover: HTMLInputElement | null = null let currentHover: HTMLInputElement | null = null
async function shortcutHandler(e: HTMLElementEventMap["keydown"]) { async function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
if (e.key === "k" && (e.ctrlKey || e.metaKey) && !e.shiftKey) { if (e.key === "k" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
e.preventDefault() e.preventDefault()
const searchBarOpen = container?.classList.contains("active") const searchBarOpen = container.classList.contains("active")
searchBarOpen ? hideSearch() : showSearch("basic") searchBarOpen ? hideSearch() : showSearch("basic")
return return
} else if (e.shiftKey && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { } else if (e.shiftKey && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
// Hotkey to open tag search // Hotkey to open tag search
e.preventDefault() e.preventDefault()
const searchBarOpen = container?.classList.contains("active") const searchBarOpen = container.classList.contains("active")
searchBarOpen ? hideSearch() : showSearch("tags") searchBarOpen ? hideSearch() : showSearch("tags")
// add "#" prefix for tag search // add "#" prefix for tag search
if (searchBar) searchBar.value = "#" searchBar.value = "#"
return return
} }
@@ -226,23 +219,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
@@ -307,9 +300,11 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
itemTile.classList.add("result-card") itemTile.classList.add("result-card")
itemTile.id = slug itemTile.id = slug
itemTile.href = resolveUrl(slug).toString() itemTile.href = resolveUrl(slug).toString()
itemTile.innerHTML = `<h3>${title}</h3>${htmlTags}${ itemTile.innerHTML = `
enablePreview && window.innerWidth > 600 ? "" : `<p>${content}</p>` <h3 class="card-title">${title}</h3>
}` ${htmlTags}
<p class="card-description">${content}</p>
`
itemTile.addEventListener("click", (event) => { itemTile.addEventListener("click", (event) => {
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
hideSearch() hideSearch()
@@ -335,8 +330,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">
@@ -392,7 +385,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" })
@@ -458,21 +451,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))
searchIcon?.addEventListener("click", () => showSearch("basic")) searchButton.addEventListener("click", () => showSearch("basic"))
window.addCleanup(() => searchIcon?.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)) {
@@ -487,5 +482,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

@@ -1,5 +1,6 @@
import micromorph from "micromorph" import micromorph from "micromorph"
import { FullSlug, RelativeURL, getFullSlug, normalizeRelativeURLs } from "../../util/path" import { FullSlug, RelativeURL, getFullSlug, normalizeRelativeURLs } from "../../util/path"
import { fetchCanonical } from "./util"
// adapted from `micromorph` // adapted from `micromorph`
// https://github.com/natemoo-re/micromorph // https://github.com/natemoo-re/micromorph
@@ -42,10 +43,26 @@ function notifyNav(url: FullSlug) {
const cleanupFns: Set<(...args: any[]) => void> = new Set() const cleanupFns: Set<(...args: any[]) => void> = new Set()
window.addCleanup = (fn) => cleanupFns.add(fn) window.addCleanup = (fn) => cleanupFns.add(fn)
function startLoading() {
const loadingBar = document.createElement("div")
loadingBar.className = "navigation-progress"
loadingBar.style.width = "0"
if (!document.body.contains(loadingBar)) {
document.body.appendChild(loadingBar)
}
setTimeout(() => {
loadingBar.style.width = "80%"
}, 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()
p = p || new DOMParser() p = p || new DOMParser()
const contents = await fetch(`${url}`) const contents = await fetchCanonical(url)
.then((res) => { .then((res) => {
const contentType = res.headers.get("content-type") const contentType = res.headers.get("content-type")
if (contentType?.startsWith("text/html")) { if (contentType?.startsWith("text/html")) {
@@ -60,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()
@@ -93,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])")
@@ -104,10 +125,24 @@ async function navigate(url: URL, isBack: boolean = false) {
if (!isBack) { if (!isBack) {
history.pushState({}, "", url) history.pushState({}, "", url)
} }
notifyNav(getFullSlug(window)) notifyNav(getFullSlug(window))
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() {
@@ -125,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,14 +1,13 @@
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
const tocEntryElement = document.querySelector(`a[data-for="${slug}"]`) const tocEntryElements = document.querySelectorAll(`a[data-for="${slug}"]`)
const windowHeight = entry.rootBounds?.height const windowHeight = entry.rootBounds?.height
if (windowHeight && tocEntryElement) { if (windowHeight && tocEntryElements.length > 0) {
if (entry.boundingClientRect.y < windowHeight) { if (entry.boundingClientRect.y < windowHeight) {
tocEntryElement.classList.add("in-view") tocEntryElements.forEach((tocEntryElement) => tocEntryElement.classList.add("in-view"))
} else { } else {
tocEntryElement.classList.remove("in-view") tocEntryElements.forEach((tocEntryElement) => tocEntryElement.classList.remove("in-view"))
} }
} }
} }
@@ -16,25 +15,25 @@ const observer = new IntersectionObserver((entries) => {
function toggleToc(this: HTMLElement) { function toggleToc(this: HTMLElement) {
this.classList.toggle("collapsed") this.classList.toggle("collapsed")
this.setAttribute(
"aria-expanded",
this.getAttribute("aria-expanded") === "true" ? "false" : "true",
)
const content = this.nextElementSibling as HTMLElement | undefined const content = this.nextElementSibling as HTMLElement | undefined
if (!content) return if (!content) return
content.classList.toggle("collapsed") content.classList.toggle("collapsed")
content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
} }
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)
content.style.maxHeight = collapsed ? "0px" : content.scrollHeight + "px" window.addCleanup(() => button.removeEventListener("click", toggleToc))
toc.addEventListener("click", toggleToc)
window.addCleanup(() => toc.removeEventListener("click", toggleToc))
} }
} }
window.addEventListener("resize", setupToc)
document.addEventListener("nav", () => { document.addEventListener("nav", () => {
setupToc() setupToc()

View File

@@ -3,6 +3,7 @@ export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb:
function click(this: HTMLElement, e: HTMLElementEventMap["click"]) { function click(this: HTMLElement, e: HTMLElementEventMap["click"]) {
if (e.target !== this) return if (e.target !== this) return
e.preventDefault() e.preventDefault()
e.stopPropagation()
cb() cb()
} }
@@ -23,3 +24,23 @@ export function removeAllChildren(node: HTMLElement) {
node.removeChild(node.firstChild) node.removeChild(node.firstChild)
} }
} }
// AliasRedirect emits HTML redirects which also have the link[rel="canonical"]
// containing the URL it's redirecting to.
// Extracting it here with regex is _probably_ faster than parsing the entire HTML
// with a DOMParser effectively twice (here and later in the SPA code), even if
// way less robust - we only care about our own generated redirects after all.
const canonicalRegex = /<link rel="canonical" href="([^"]*)">/
export async function fetchCanonical(url: URL): Promise<Response> {
const res = await fetch(`${url}`)
if (!res.headers.get("content-type")?.startsWith("text/html")) {
return res
}
// 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
const text = await res.clone().text()
const [_, redirect] = text.match(canonicalRegex) ?? []
return redirect ? fetch(`${new URL(redirect, url)}`) : res
}

View File

@@ -1,15 +1,19 @@
@use "../../styles/variables.scss" as *;
.backlinks { .backlinks {
position: relative; flex-direction: column;
& > h3 { & > h3 {
font-size: 1rem; font-size: 1rem;
margin: 0; margin: 0;
} }
& > ul { & > ul.overflow {
list-style: none; list-style: none;
padding: 0; padding: 0;
margin: 0.5rem 0; margin: 0.5rem 0;
max-height: calc(100% - 2rem);
overscroll-behavior: contain;
& > li { & > li {
& > a { & > a {

View File

@@ -1,9 +1,9 @@
.content-meta { .content-meta {
margin-top: 0; margin-top: 0;
color: var(--gray); color: var(--darkgray);
&[show-comma="true"] { &[show-comma="true"] {
> span:not(:last-child) { > *:not(:last-child) {
margin-right: 8px; margin-right: 8px;
&::after { &::after {

View File

@@ -1,17 +1,16 @@
.darkmode { .darkmode {
cursor: pointer;
padding: 0;
position: relative; position: relative;
background: none;
border: none;
width: 20px; width: 20px;
height: 20px; height: 20px;
margin: 0 10px; margin: 0;
text-align: inherit;
& > .toggle { flex-shrink: 0;
display: none;
box-sizing: border-box;
}
& svg { & svg {
cursor: pointer;
opacity: 0;
position: absolute; position: absolute;
width: 20px; width: 20px;
height: 20px; height: 20px;
@@ -29,20 +28,20 @@
color-scheme: light; color-scheme: light;
} }
:root[saved-theme="dark"] .toggle ~ label { :root[saved-theme="dark"] .darkmode {
& > #dayIcon { & > .dayIcon {
opacity: 0; display: none;
} }
& > #nightIcon { & > .nightIcon {
opacity: 1; display: inline;
} }
} }
:root .toggle ~ label { :root .darkmode {
& > #dayIcon { & > .dayIcon {
opacity: 1; display: inline;
} }
& > #nightIcon { & > .nightIcon {
opacity: 0; display: none;
} }
} }

View File

@@ -1,7 +1,97 @@
@use "../../styles/variables.scss" as *; @use "../../styles/variables.scss" as *;
button#explorer { @media all and ($mobile) {
all: unset; .page > #quartz-body {
// Shift page position when toggling Explorer on mobile.
& > :not(.sidebar.left:has(.explorer)) {
transition: transform 300ms ease-in-out;
}
&.lock-scroll > :not(.sidebar.left:has(.explorer)) {
transform: translateX(100dvw);
transition: transform 300ms ease-in-out;
}
// Sticky top bar (stays in place when scrolling down on mobile).
.sidebar.left:has(.explorer) {
box-sizing: border-box;
position: sticky;
background-color: var(--light);
padding: 1rem 0 1rem 0;
margin: 0;
}
.hide-until-loaded ~ .explorer-content {
display: none;
}
}
}
.explorer {
display: flex;
flex-direction: column;
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) {
order: -1;
height: initial;
overflow: hidden;
flex-shrink: 0;
align-self: flex-start;
margin-top: auto;
margin-bottom: auto;
}
button.mobile-explorer {
display: none;
}
button.desktop-explorer {
display: flex;
}
@media all and ($mobile) {
button.mobile-explorer {
display: flex;
}
button.desktop-explorer {
display: none;
}
}
&.desktop-only {
@media all and not ($mobile) {
display: flex;
}
}
svg {
pointer-events: all;
transition: transform 0.35s ease;
& > polyline {
pointer-events: none;
}
}
}
button.mobile-explorer,
button.desktop-explorer {
background-color: transparent; background-color: transparent;
border: none; border: none;
text-align: left; text-align: left;
@@ -16,64 +106,47 @@ button#explorer {
display: inline-block; display: inline-block;
margin: 0; margin: 0;
} }
& .fold {
margin-left: 0.5rem;
transition: transform 0.3s ease;
opacity: 0.8;
}
&.collapsed .fold {
transform: rotateZ(-90deg);
}
} }
.folder-outer { .explorer-content {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.3s ease-in-out;
}
.folder-outer.open {
grid-template-rows: 1fr;
}
.folder-outer > ul {
overflow: hidden;
}
#explorer-content {
list-style: none; list-style: none;
overflow: hidden; overflow: hidden;
max-height: none; overflow-y: auto;
transition: max-height 0.35s ease;
margin-top: 0.5rem; margin-top: 0.5rem;
&.collapsed > .overflow::after {
opacity: 0;
}
& ul { & ul {
list-style: none; list-style: none;
margin: 0.08rem 0; margin: 0;
padding: 0; padding: 0;
transition: overscroll-behavior: contain;
max-height 0.35s ease,
transform 0.35s ease,
opacity 0.2s ease;
& li > a { & li > a {
color: var(--dark); color: var(--dark);
opacity: 0.75; opacity: 0.75;
pointer-events: all; pointer-events: all;
&.active {
opacity: 1;
color: var(--tertiary);
}
} }
} }
}
svg { .folder-outer {
pointer-events: all; display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.3s ease-in-out;
}
& > polyline { .folder-outer.open {
pointer-events: none; grid-template-rows: 1fr;
}
.folder-outer > ul {
overflow: hidden;
margin-left: 6px;
padding-left: 0.8rem;
border-left: 1px solid var(--lightgray);
} }
} }
@@ -126,6 +199,7 @@ svg {
cursor: pointer; cursor: pointer;
transition: transform 0.3s ease; transition: transform 0.3s ease;
backface-visibility: visible; backface-visibility: visible;
flex-shrink: 0;
} }
li:has(> .folder-outer:not(.open)) > .folder-container > svg { li:has(> .folder-outer:not(.open)) > .folder-container > svg {
@@ -136,13 +210,61 @@ li:has(> .folder-outer:not(.open)) > .folder-container > svg {
color: var(--tertiary); color: var(--tertiary);
} }
.no-background::after { .explorer {
background: none !important; @media all and ($mobile) {
&.collapsed {
flex: 0 0 34px;
& > .explorer-content {
transform: translateX(-100vw);
visibility: hidden;
}
}
&:not(.collapsed) {
flex: 0 0 34px;
& > .explorer-content {
transform: translateX(0);
visibility: visible;
}
}
.explorer-content {
box-sizing: border-box;
z-index: 100;
position: absolute;
top: 0;
left: 0;
margin-top: 0;
background-color: var(--light);
max-width: 100vw;
width: 100%;
transform: translateX(-100vw);
transition:
transform 200ms ease,
visibility 200ms ease;
overflow: hidden;
padding: 4rem 0 2rem 0;
height: 100dvh;
max-height: 100dvh;
visibility: hidden;
}
.mobile-explorer {
margin: 0;
padding: 5px;
z-index: 101;
.lucide-menu {
stroke: var(--darkgray);
}
}
}
} }
#explorer-end { .mobile-no-scroll {
// needs height so IntersectionObserver gets triggered @media all and ($mobile) {
height: 4px; overflow: hidden;
// remove default margin from li }
margin: 0;
} }

View File

@@ -15,11 +15,14 @@
position: relative; position: relative;
overflow: hidden; overflow: hidden;
& > #global-graph-icon { & > .global-graph-icon {
cursor: pointer;
background: none;
border: none;
color: var(--dark); color: var(--dark);
opacity: 0.5; opacity: 0.5;
width: 18px; width: 24px;
height: 18px; height: 24px;
position: absolute; position: absolute;
padding: 0.2rem; padding: 0.2rem;
margin: 0.3rem; margin: 0.3rem;
@@ -35,7 +38,7 @@
} }
} }
& > #global-graph-outer { & > .global-graph-outer {
position: fixed; position: fixed;
z-index: 9999; z-index: 9999;
left: 0; left: 0;
@@ -50,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;
@@ -59,10 +62,10 @@
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
height: 60vh; height: 80vh;
width: 50vw; width: 80vw;
@media all and (max-width: $fullPageWidth) { @media all and not ($desktop) {
width: 90%; width: 90%;
} }
} }

View File

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

View File

@@ -13,7 +13,7 @@ li.section-li {
display: grid; display: grid;
grid-template-columns: fit-content(8em) 3fr 1fr; grid-template-columns: fit-content(8em) 3fr 1fr;
@media all and (max-width: $mobileBreakpoint) { @media all and ($mobile) {
& > .tags { & > .tags {
display: none; display: none;
} }
@@ -23,7 +23,7 @@ li.section-li {
background-color: transparent; background-color: transparent;
} }
& > .meta { & .meta {
margin: 0 1em 0 0; margin: 0 1em 0 0;
opacity: 0.6; opacity: 0.6;
} }

Some files were not shown because too many files have changed in this diff Show More