Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d7e334976 | ||
|
|
9cefcd0dd1 | ||
|
|
4aaeb768d8 | ||
|
|
743ef712d5 | ||
|
|
9a6e4e2f80 | ||
|
|
14cb50de9b | ||
|
|
dad0ae4e3f | ||
|
|
9c060f3cf2 | ||
|
|
16a9caa555 | ||
|
|
b1c60b8833 | ||
|
|
50a78bafa5 | ||
|
|
6ea359e55e | ||
|
|
cd3bb25626 | ||
|
|
eb9bbd1666 | ||
|
|
c36551310d | ||
|
|
6215dd5565 | ||
|
|
c89c76b40a | ||
|
|
a145e320d0 | ||
|
|
082cbb74c3 | ||
|
|
93b2481261 | ||
|
|
d2414b3903 | ||
|
|
01fc26d2c0 | ||
|
|
84a9be65ce | ||
|
|
6715079a89 | ||
|
|
d613a3d2f2 | ||
|
|
9b75faafea | ||
|
|
5a26b582ed | ||
|
|
4e2aea8a5a | ||
|
|
40f039983c | ||
|
|
3b988aec61 | ||
|
|
bca74623a3 | ||
|
|
4c9e860150 | ||
|
|
46b63b68bf | ||
|
|
c4cd84dcc8 | ||
|
|
2be9c096a1 | ||
|
|
f913332862 | ||
|
|
647c125525 | ||
|
|
3b74453fe6 | ||
|
|
6b499ed90c | ||
|
|
437d65c847 | ||
|
|
7c5709b660 | ||
|
|
21e1921822 | ||
|
|
e47c29d2fd | ||
|
|
a5f2f874f7 | ||
|
|
eb23cbe8da | ||
|
|
cb68069d45 | ||
|
|
d27c292736 | ||
|
|
0ee103a514 | ||
|
|
c5f0b69a52 | ||
|
|
323167a001 | ||
|
|
3b5ed813f5 | ||
|
|
195fc5134c | ||
|
|
e89c395f7c | ||
|
|
2db735a150 | ||
|
|
39eebca3cf | ||
|
|
9acaa1c8ac | ||
|
|
27a41abb62 | ||
|
|
12904ab796 |
@@ -29,6 +29,7 @@ Some common frontmatter fields that are natively supported by Quartz:
|
|||||||
|
|
||||||
- `title`: Title of the page. If it isn't provided, Quartz will use the name of the file as the title.
|
- `title`: Title of the page. If it isn't provided, Quartz will use the name of the file as the title.
|
||||||
- `description`: Description of the page used for link previews.
|
- `description`: Description of the page used for link previews.
|
||||||
|
- `permalink`: A custom URL for the page that will remain constant even if the path to the file changes.
|
||||||
- `aliases`: Other names for this note. This is a list of strings.
|
- `aliases`: Other names for this note. This is a list of strings.
|
||||||
- `tags`: Tags for this note.
|
- `tags`: Tags for this note.
|
||||||
- `draft`: Whether to publish the page or not. This is one way to make [[private pages|pages private]] in Quartz.
|
- `draft`: Whether to publish the page or not. This is one way to make [[private pages|pages private]] in Quartz.
|
||||||
|
|||||||
@@ -21,3 +21,7 @@ This will start a local web server to run your Quartz on your computer. Open a w
|
|||||||
> - `--serve`: run a local hot-reloading server to preview your Quartz
|
> - `--serve`: run a local hot-reloading server to preview your Quartz
|
||||||
> - `--port`: what port to run the local preview server on
|
> - `--port`: what port to run the local preview server on
|
||||||
> - `--concurrency`: how many threads to use to parse notes
|
> - `--concurrency`: how many threads to use to parse notes
|
||||||
|
|
||||||
|
> [!warning] Not to be used for production
|
||||||
|
> Serve mode is intended for local previews only.
|
||||||
|
> For production workloads, see the page on [[hosting]].
|
||||||
|
|||||||
28
docs/features/Roam Research compatibility.md
Normal file
28
docs/features/Roam Research compatibility.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
title: "Roam Research Compatibility"
|
||||||
|
tags:
|
||||||
|
- feature/transformer
|
||||||
|
---
|
||||||
|
|
||||||
|
[Roam Research](https://roamresearch.com) is a note-taking tool that organizes your knowledge graph in a unique and interconnected way.
|
||||||
|
|
||||||
|
Quartz supports transforming the special Markdown syntax from Roam Research (like `{{[[components]]}}` and other formatting) into
|
||||||
|
regular Markdown via the [[RoamFlavoredMarkdown]] plugin.
|
||||||
|
|
||||||
|
```typescript title="quartz.config.ts"
|
||||||
|
plugins: {
|
||||||
|
transformers: [
|
||||||
|
// ...
|
||||||
|
Plugin.RoamFlavoredMarkdown(),
|
||||||
|
Plugin.ObsidianFlavoredMarkdown(),
|
||||||
|
// ...
|
||||||
|
],
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!warning]
|
||||||
|
> As seen above placement of `Plugin.RoamFlavoredMarkdown()` within `quartz.config.ts` is very important. It must come before `Plugin.ObsidianFlavoredMarkdown()`.
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
This functionality is provided by the [[RoamFlavoredMarkdown]] plugin. See the plugin page for customization options.
|
||||||
@@ -6,7 +6,6 @@ draft: true
|
|||||||
|
|
||||||
- static dead link detection
|
- static dead link detection
|
||||||
- cursor chat extension
|
- cursor chat extension
|
||||||
- https://giscus.app/ extension
|
|
||||||
- sidenotes? https://github.com/capnfabs/paperesque
|
- sidenotes? https://github.com/capnfabs/paperesque
|
||||||
- direct match in search using double quotes
|
- direct match in search using double quotes
|
||||||
- https://help.obsidian.md/Advanced+topics/Using+Obsidian+URI
|
- https://help.obsidian.md/Advanced+topics/Using+Obsidian+URI
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Fetch all history for git info
|
fetch-depth: 0 # Fetch all history for git info
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Build Quartz
|
- name: Build Quartz
|
||||||
@@ -206,7 +208,7 @@ build:
|
|||||||
paths:
|
paths:
|
||||||
- public
|
- public
|
||||||
tags:
|
tags:
|
||||||
- docker
|
- gitlab-org-docker
|
||||||
|
|
||||||
pages:
|
pages:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 65 KiB |
BIN
docs/images/quartz-layout-desktop.png
Normal file
BIN
docs/images/quartz-layout-desktop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
BIN
docs/images/quartz-layout-mobile.png
Normal file
BIN
docs/images/quartz-layout-mobile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
BIN
docs/images/quartz-layout-tablet.png
Normal file
BIN
docs/images/quartz-layout-tablet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
@@ -13,15 +13,19 @@ export interface FullPageLayout {
|
|||||||
beforeBody: QuartzComponent[] // laid out vertically
|
beforeBody: QuartzComponent[] // laid out vertically
|
||||||
pageBody: QuartzComponent // single component
|
pageBody: QuartzComponent // single component
|
||||||
afterBody: QuartzComponent[] // laid out vertically
|
afterBody: QuartzComponent[] // laid out vertically
|
||||||
left: QuartzComponent[] // vertical on desktop, horizontal on mobile
|
left: QuartzComponent[] // vertical on desktop and tablet, horizontal on mobile
|
||||||
right: QuartzComponent[] // vertical on desktop, horizontal on mobile
|
right: QuartzComponent[] // vertical on desktop, horizontal on tablet and mobile
|
||||||
footer: QuartzComponent // single component
|
footer: QuartzComponent // single component
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
These correspond to following parts of the page:
|
These correspond to following parts of the page:
|
||||||
|
|
||||||
![[quartz layout.png|800]]
|
| Layout | Preview |
|
||||||
|
| ------------------------------- | ----------------------------------- |
|
||||||
|
| Desktop (width > 1200px) | ![[quartz-layout-desktop.png\|800]] |
|
||||||
|
| Tablet (800px < width < 1200px) | ![[quartz-layout-tablet.png\|800]] |
|
||||||
|
| Mobile (width < 800px) | ![[quartz-layout-mobile.png\|800]] |
|
||||||
|
|
||||||
> [!note]
|
> [!note]
|
||||||
> There are two additional layout fields that are _not_ shown in the above diagram.
|
> There are two additional layout fields that are _not_ shown in the above diagram.
|
||||||
@@ -33,6 +37,23 @@ Quartz **components**, like plugins, can take in additional properties as config
|
|||||||
|
|
||||||
See [a list of all the components](component.md) for all available components along with their configuration options. You can also checkout the guide on [[creating components]] if you're interested in further customizing the behaviour of Quartz.
|
See [a list of all the components](component.md) for all available components along with their configuration options. You can also checkout the guide on [[creating components]] if you're interested in further customizing the behaviour of Quartz.
|
||||||
|
|
||||||
|
### Layout breakpoints
|
||||||
|
|
||||||
|
Quartz has different layouts depending on the width the screen viewing the website.
|
||||||
|
|
||||||
|
The breakpoints for layouts can be configured in `variables.scss`.
|
||||||
|
|
||||||
|
- `mobile`: screen width below this size will use mobile layout.
|
||||||
|
- `desktop`: screen width above this size will use desktop layout.
|
||||||
|
- Screen width between `mobile` and `desktop` width will use the tablet layout.
|
||||||
|
|
||||||
|
```scss
|
||||||
|
$breakpoints: (
|
||||||
|
mobile: 800px,
|
||||||
|
desktop: 1200px,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
### Style
|
### Style
|
||||||
|
|
||||||
Most meaningful style changes like colour scheme and font can be done simply through the [[configuration#General Configuration|general configuration]] options. However, if you'd like to make more involved style changes, you can do this by writing your own styles. Quartz 4, like Quartz 3, uses [Sass](https://sass-lang.com/guide/) for styling.
|
Most meaningful style changes like colour scheme and font can be done simply through the [[configuration#General Configuration|general configuration]] options. However, if you'd like to make more involved style changes, you can do this by writing your own styles. Quartz 4, like Quartz 3, uses [Sass](https://sass-lang.com/guide/) for styling.
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ This plugin adds LaTeX support to Quartz. See [[features/Latex|Latex]] for more
|
|||||||
This plugin accepts the following configuration options:
|
This plugin accepts the following configuration options:
|
||||||
|
|
||||||
- `renderEngine`: the engine to use to render LaTeX equations. Can be `"katex"` for [KaTeX](https://katex.org/) or `"mathjax"` for [MathJax](https://www.mathjax.org/) [SVG rendering](https://docs.mathjax.org/en/latest/output/svg.html). Defaults to KaTeX.
|
- `renderEngine`: the engine to use to render LaTeX equations. Can be `"katex"` for [KaTeX](https://katex.org/) or `"mathjax"` for [MathJax](https://www.mathjax.org/) [SVG rendering](https://docs.mathjax.org/en/latest/output/svg.html). Defaults to KaTeX.
|
||||||
|
- `customMacros`: custom macros for all LaTeX blocks. It takes the form of a key-value pair where the key is a new command name and the value is the expansion of the macro. For example: `{"\\R": "\\mathbb{R}"}`
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
|
|||||||
26
docs/plugins/RoamFlavoredMarkdown.md
Normal file
26
docs/plugins/RoamFlavoredMarkdown.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
title: RoamFlavoredMarkdown
|
||||||
|
tags:
|
||||||
|
- plugin/transformer
|
||||||
|
---
|
||||||
|
|
||||||
|
This plugin provides support for [Roam Research](https://roamresearch.com) compatibility. See [[Roam Research Compatibility]] for more information.
|
||||||
|
|
||||||
|
> [!note]
|
||||||
|
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
||||||
|
|
||||||
|
This plugin accepts the following configuration options:
|
||||||
|
|
||||||
|
- `orComponent`: If `true` (default), converts Roam `{{ or:ONE|TWO|THREE }}` shortcodes into HTML Dropdown options.
|
||||||
|
- `TODOComponent`: If `true` (default), converts Roam `{{[[TODO]]}}` shortcodes into HTML check boxes.
|
||||||
|
- `DONEComponent`: If `true` (default), converts Roam `{{[[DONE]]}}` shortcodes into checked HTML check boxes.
|
||||||
|
- `videoComponent`: If `true` (default), converts Roam `{{[[video]]:URL}}` shortcodes into embeded HTML video.
|
||||||
|
- `audioComponent`: If `true` (default), converts Roam `{{[[audio]]:URL}}` shortcodes into embeded HTML audio.
|
||||||
|
- `pdfComponent`: If `true` (default), converts Roam `{{[[pdf]]:URL}}` shortcodes into embeded HTML PDF viewer.
|
||||||
|
- `blockquoteComponent`: If `true` (default), converts Roam `{{[[>]]}}` shortcodes into Quartz blockquotes.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- Category: Transformer
|
||||||
|
- Function name: `Plugin.RoamFlavoredMarkdown()`.
|
||||||
|
- Source: [`quartz/plugins/transformers/roam.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/roam.ts).
|
||||||
@@ -25,5 +25,8 @@ Want to see what Quartz can do? Here are some cool community gardens:
|
|||||||
- [🪴Aster's notebook](https://notes.asterhu.com)
|
- [🪴Aster's notebook](https://notes.asterhu.com)
|
||||||
- [Gatekeeper Wiki](https://www.gatekeeper.wiki)
|
- [Gatekeeper Wiki](https://www.gatekeeper.wiki)
|
||||||
- [Ellie's Notes](https://ellie.wtf)
|
- [Ellie's Notes](https://ellie.wtf)
|
||||||
|
- [🥷🏻🌳🍃 Computer Science & Thinkering Garden](https://notes.yxy.ninja)
|
||||||
|
- [Eledah's Crystalline](https://blog.eledah.ir/)
|
||||||
|
- [🌓 Projects & Privacy - FOSS, tech, law](https://be-far.com)
|
||||||
|
|
||||||
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)!
|
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)!
|
||||||
|
|||||||
748
package-lock.json
generated
748
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@@ -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.4.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -36,8 +36,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.7.0",
|
"@clack/prompts": "^0.7.0",
|
||||||
"@floating-ui/dom": "^1.6.8",
|
"@floating-ui/dom": "^1.6.10",
|
||||||
"@napi-rs/simple-git": "0.1.16",
|
"@napi-rs/simple-git": "0.1.19",
|
||||||
|
"@tweenjs/tween.js": "^25.0.0",
|
||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"chokidar": "^3.6.0",
|
"chokidar": "^3.6.0",
|
||||||
@@ -48,24 +49,25 @@
|
|||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"globby": "^14.0.2",
|
"globby": "^14.0.2",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"hast-util-to-html": "^9.0.1",
|
"hast-util-to-html": "^9.0.2",
|
||||||
"hast-util-to-jsx-runtime": "^2.3.0",
|
"hast-util-to-jsx-runtime": "^2.3.0",
|
||||||
"hast-util-to-string": "^3.0.0",
|
"hast-util-to-string": "^3.0.0",
|
||||||
"is-absolute-url": "^4.0.1",
|
"is-absolute-url": "^4.0.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lightningcss": "^1.25.1",
|
"lightningcss": "^1.26.0",
|
||||||
"mdast-util-find-and-replace": "^3.0.1",
|
"mdast-util-find-and-replace": "^3.0.1",
|
||||||
"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",
|
"pixi.js": "^8.3.4",
|
||||||
"preact-render-to-string": "^6.5.7",
|
"preact": "^10.24.0",
|
||||||
|
"preact-render-to-string": "^6.5.11",
|
||||||
"pretty-bytes": "^6.1.1",
|
"pretty-bytes": "^6.1.1",
|
||||||
"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.1.1",
|
||||||
"rehype-katex": "^7.0.0",
|
"rehype-katex": "^7.0.1",
|
||||||
"rehype-mathjax": "^6.0.0",
|
"rehype-mathjax": "^6.0.0",
|
||||||
"rehype-pretty-code": "^0.13.2",
|
"rehype-pretty-code": "^0.13.2",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
@@ -81,13 +83,13 @@
|
|||||||
"rfdc": "^1.4.1",
|
"rfdc": "^1.4.1",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"serve-handler": "^6.1.5",
|
"serve-handler": "^6.1.5",
|
||||||
"shiki": "^1.10.3",
|
"shiki": "^1.12.1",
|
||||||
"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.1.3",
|
||||||
"ws": "^8.18.0",
|
"ws": "^8.18.0",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
@@ -97,14 +99,14 @@
|
|||||||
"@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.5.5",
|
||||||
"@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.5.12",
|
||||||
"@types/yargs": "^17.0.32",
|
"@types/yargs": "^17.0.33",
|
||||||
"esbuild": "^0.19.9",
|
"esbuild": "^0.19.9",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"tsx": "^4.16.2",
|
"tsx": "^4.19.0",
|
||||||
"typescript": "^5.5.3"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,8 +38,13 @@ type BuildData = {
|
|||||||
|
|
||||||
type FileEvent = "add" | "change" | "delete"
|
type FileEvent = "add" | "change" | "delete"
|
||||||
|
|
||||||
|
function newBuildId() {
|
||||||
|
return Math.random().toString(36).substring(2, 8)
|
||||||
|
}
|
||||||
|
|
||||||
async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
||||||
const ctx: BuildCtx = {
|
const ctx: BuildCtx = {
|
||||||
|
buildId: newBuildId(),
|
||||||
argv,
|
argv,
|
||||||
cfg,
|
cfg,
|
||||||
allSlugs: [],
|
allSlugs: [],
|
||||||
@@ -157,10 +162,13 @@ async function partialRebuildFromEntrypoint(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildStart = new Date().getTime()
|
const buildId = newBuildId()
|
||||||
buildData.lastBuildMs = buildStart
|
ctx.buildId = buildId
|
||||||
|
buildData.lastBuildMs = new Date().getTime()
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -351,26 +359,22 @@ async function rebuildFromEntrypoint(
|
|||||||
toRemove.add(filePath)
|
toRemove.add(filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildStart = new Date().getTime()
|
const buildId = newBuildId()
|
||||||
buildData.lastBuildMs = buildStart
|
ctx.buildId = buildId
|
||||||
|
buildData.lastBuildMs = new Date().getTime()
|
||||||
const release = await mut.acquire()
|
const release = await mut.acquire()
|
||||||
|
|
||||||
// there's another build after us, release and let them do it
|
// there's another build after us, release and let them do it
|
||||||
if (buildData.lastBuildMs > buildStart) {
|
if (ctx.buildId !== buildId) {
|
||||||
release()
|
release()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const perf = new PerfTimer()
|
const perf = new PerfTimer()
|
||||||
console.log(chalk.yellow("Detected change, rebuilding..."))
|
console.log(chalk.yellow("Detected change, rebuilding..."))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const filesToRebuild = [...toRebuild].filter((fp) => !toRemove.has(fp))
|
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)
|
const parsedContent = await parseMarkdown(ctx, filesToRebuild)
|
||||||
for (const content of parsedContent) {
|
for (const content of parsedContent) {
|
||||||
const [_tree, vfile] = content
|
const [_tree, vfile] = content
|
||||||
@@ -384,6 +388,13 @@ async function rebuildFromEntrypoint(
|
|||||||
const parsedFiles = [...contentMap.values()]
|
const parsedFiles = [...contentMap.values()]
|
||||||
const filteredContent = filterContent(ctx, parsedFiles)
|
const filteredContent = filterContent(ctx, parsedFiles)
|
||||||
|
|
||||||
|
// re-update slugs
|
||||||
|
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])]
|
||||||
|
|
||||||
// TODO: we can probably traverse the link graph to figure out what's safe to delete here
|
// TODO: we can probably traverse the link graph to figure out what's safe to delete here
|
||||||
// instead of just deleting everything
|
// instead of just deleting everything
|
||||||
await rimraf(path.join(argv.output, ".*"), { glob: true })
|
await rimraf(path.join(argv.output, ".*"), { glob: true })
|
||||||
@@ -396,10 +407,10 @@ async function rebuildFromEntrypoint(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
release()
|
|
||||||
clientRefresh()
|
clientRefresh()
|
||||||
toRebuild.clear()
|
toRebuild.clear()
|
||||||
toRemove.clear()
|
toRemove.clear()
|
||||||
|
release()
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => {
|
export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => {
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ 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")} id="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"
|
||||||
@@ -22,12 +20,11 @@ const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps)
|
|||||||
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"
|
||||||
@@ -38,12 +35,12 @@ const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps)
|
|||||||
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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,12 +44,9 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
// memoized
|
// memoized
|
||||||
let fileTree: FileNode
|
let fileTree: FileNode
|
||||||
let jsonTree: string
|
let jsonTree: string
|
||||||
|
let lastBuildId: string = ""
|
||||||
|
|
||||||
function constructFileTree(allFiles: QuartzPluginData[]) {
|
function constructFileTree(allFiles: QuartzPluginData[]) {
|
||||||
if (fileTree) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct tree from allFiles
|
// Construct tree from allFiles
|
||||||
fileTree = new FileNode("")
|
fileTree = new FileNode("")
|
||||||
allFiles.forEach((file) => fileTree.add(file))
|
allFiles.forEach((file) => fileTree.add(file))
|
||||||
@@ -76,12 +73,17 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Explorer: QuartzComponent = ({
|
const Explorer: QuartzComponent = ({
|
||||||
|
ctx,
|
||||||
cfg,
|
cfg,
|
||||||
allFiles,
|
allFiles,
|
||||||
displayClass,
|
displayClass,
|
||||||
fileData,
|
fileData,
|
||||||
}: QuartzComponentProps) => {
|
}: QuartzComponentProps) => {
|
||||||
|
if (ctx.buildId !== lastBuildId) {
|
||||||
|
lastBuildId = ctx.buildId
|
||||||
constructFileTree(allFiles)
|
constructFileTree(allFiles)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "explorer")}>
|
<div class={classNames(displayClass, "explorer")}>
|
||||||
<button
|
<button
|
||||||
@@ -91,6 +93,8 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
data-collapsed={opts.folderDefaultState}
|
data-collapsed={opts.folderDefaultState}
|
||||||
data-savestate={opts.useSavedState}
|
data-savestate={opts.useSavedState}
|
||||||
data-tree={jsonTree}
|
data-tree={jsonTree}
|
||||||
|
aria-controls="explorer-content"
|
||||||
|
aria-expanded={opts.folderDefaultState === "open"}
|
||||||
>
|
>
|
||||||
<h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2>
|
<h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2>
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -65,9 +65,9 @@ export default ((opts?: GraphOptions) => {
|
|||||||
<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 id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
|
||||||
|
<button id="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,6 +90,7 @@ 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 id="global-graph-outer">
|
||||||
<div id="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div>
|
<div id="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div>
|
||||||
|
|||||||
@@ -46,11 +46,13 @@ 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">
|
||||||
|
<div>
|
||||||
{page.dates && (
|
{page.dates && (
|
||||||
<p class="meta">
|
<p class="meta">
|
||||||
<Date date={getDate(cfg, page)!} locale={cfg.locale} />
|
<Date date={getDate(cfg, page)!} locale={cfg.locale} />
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
<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">
|
||||||
|
|||||||
@@ -19,24 +19,16 @@ 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" id="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 id="search-container">
|
||||||
<div id="search-space">
|
<div id="search-space">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -26,7 +26,13 @@ const TableOfContents: QuartzComponent = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "toc")}>
|
<div class={classNames(displayClass, "toc")}>
|
||||||
<button type="button" id="toc" class={fileData.collapseToc ? "collapsed" : ""}>
|
<button
|
||||||
|
type="button"
|
||||||
|
id="toc"
|
||||||
|
class={fileData.collapseToc ? "collapsed" : ""}
|
||||||
|
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,7 +49,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">
|
<div id="toc-content" class={fileData.collapseToc ? "collapsed" : ""}>
|
||||||
<ul class="overflow">
|
<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}`}>
|
||||||
|
|||||||
@@ -242,8 +242,8 @@ export function renderPage(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{RightComponent}
|
{RightComponent}
|
||||||
</Body>
|
|
||||||
<Footer {...componentData} />
|
<Footer {...componentData} />
|
||||||
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
{pageResources.js
|
{pageResources.js
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ const emitThemeChangeEvent = (theme: "light" | "dark") => {
|
|||||||
|
|
||||||
document.addEventListener("nav", () => {
|
document.addEventListener("nav", () => {
|
||||||
const switchTheme = (e: Event) => {
|
const switchTheme = (e: Event) => {
|
||||||
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,17 +22,13 @@ 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
|
// Darkmode toggle
|
||||||
const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement
|
const themeButton = document.querySelector("#darkmode") as HTMLButtonElement
|
||||||
toggleSwitch.addEventListener("change", switchTheme)
|
themeButton.addEventListener("click", switchTheme)
|
||||||
window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
|
window.addCleanup(() => themeButton.removeEventListener("click", switchTheme))
|
||||||
if (currentTheme === "dark") {
|
|
||||||
toggleSwitch.checked = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen for changes in prefers-color-scheme
|
// Listen for changes in prefers-color-scheme
|
||||||
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ const observer = new IntersectionObserver((entries) => {
|
|||||||
|
|
||||||
function toggleExplorer(this: HTMLElement) {
|
function toggleExplorer(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 MaybeHTMLElement
|
const content = this.nextElementSibling as MaybeHTMLElement
|
||||||
if (!content) return
|
if (!content) return
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,56 @@
|
|||||||
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,
|
||||||
|
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,6 +62,11 @@ function addToVisited(slug: SimpleSlug) {
|
|||||||
localStorage.setItem(localStorageKey, JSON.stringify([...visited]))
|
localStorage.setItem(localStorageKey, JSON.stringify([...visited]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TweenNode = {
|
||||||
|
update: (time: number) => void
|
||||||
|
stop: () => void
|
||||||
|
}
|
||||||
|
|
||||||
async function renderGraph(container: string, fullSlug: FullSlug) {
|
async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||||
const slug = simplifySlug(fullSlug)
|
const slug = simplifySlug(fullSlug)
|
||||||
const visited = getVisited()
|
const visited = getVisited()
|
||||||
@@ -45,7 +87,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
removeTags,
|
removeTags,
|
||||||
showTags,
|
showTags,
|
||||||
focusOnHover,
|
focusOnHover,
|
||||||
} = JSON.parse(graph.dataset["cfg"]!)
|
} = 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 +95,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,263 +143,406 @@ 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
|
// we virtualize the simulation and use pixi to actually render it
|
||||||
.forceSimulation(graphData.nodes)
|
const simulation: Simulation<NodeData, LinkData> = forceSimulation<NodeData>(graphData.nodes)
|
||||||
.force("charge", d3.forceManyBody().strength(-100 * repelForce))
|
.force("charge", forceManyBody().strength(-100 * repelForce))
|
||||||
.force(
|
.force("center", forceCenter().strength(centerForce))
|
||||||
"link",
|
.force("link", forceLink(graphData.links).distance(linkDistance))
|
||||||
d3
|
.force("collide", forceCollide<NodeData>((n) => nodeRadius(n)).iterations(3))
|
||||||
.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
|
// precompute style prop strings as pixi doesn't support css variables
|
||||||
.select<HTMLElement, NodeData>("#" + container)
|
const cssVars = [
|
||||||
.append("svg")
|
"--secondary",
|
||||||
.attr("width", width)
|
"--tertiary",
|
||||||
.attr("height", height)
|
"--gray",
|
||||||
.attr("viewBox", [-width / 2 / scale, -height / 2 / scale, width / scale, height / scale])
|
"--light",
|
||||||
|
"--lightgray",
|
||||||
// draw links between nodes
|
"--dark",
|
||||||
const link = svg
|
"--darkgray",
|
||||||
.append("g")
|
"--bodyFont",
|
||||||
.selectAll("line")
|
] as const
|
||||||
.data(graphData.links)
|
const computedStyleMap = cssVars.reduce(
|
||||||
.join("line")
|
(acc, key) => {
|
||||||
.attr("class", "link")
|
acc[key] = getComputedStyle(document.documentElement).getPropertyValue(key)
|
||||||
.attr("stroke", "var(--lightgray)")
|
return acc
|
||||||
.attr("stroke-width", 1)
|
},
|
||||||
|
{} as Record<(typeof cssVars)[number], string>,
|
||||||
// svg groups
|
)
|
||||||
const graphNode = svg.append("g").selectAll("g").data(graphData.nodes).enter().append("g")
|
|
||||||
|
|
||||||
// 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 app = new Application()
|
||||||
|
await app.init({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
antialias: true,
|
||||||
|
autoStart: false,
|
||||||
|
autoDensity: true,
|
||||||
|
backgroundAlpha: 0,
|
||||||
|
preference: "webgpu",
|
||||||
|
resolution: window.devicePixelRatio,
|
||||||
|
eventMode: "static",
|
||||||
|
})
|
||||||
|
graph.appendChild(app.canvas)
|
||||||
|
|
||||||
|
const stage = app.stage
|
||||||
|
stage.interactive = false
|
||||||
|
|
||||||
|
const labelsContainer = new Container<Text>({ zIndex: 3 })
|
||||||
|
const nodesContainer = new Container<Graphics>({ zIndex: 2 })
|
||||||
|
const linkContainer = new Container<Graphics>({ zIndex: 1 })
|
||||||
|
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) })
|
||||||
|
.stroke({ width: isTagNode ? 2 : 0, color: 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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
function animate(time: number) {
|
||||||
simulation.on("tick", () => {
|
for (const n of nodeRenderData) {
|
||||||
link
|
const { x, y } = n.simulationData
|
||||||
.attr("x1", (d: any) => d.source.x)
|
if (!x || !y) continue
|
||||||
.attr("y1", (d: any) => d.source.y)
|
n.gfx.position.set(x + width / 2, y + height / 2)
|
||||||
.attr("x2", (d: any) => d.target.x)
|
if (n.label) {
|
||||||
.attr("y2", (d: any) => d.target.y)
|
n.label.position.set(x + width / 2, y + height / 2)
|
||||||
node.attr("cx", (d: any) => d.x).attr("cy", (d: any) => d.y)
|
}
|
||||||
labels.attr("x", (d: any) => d.x).attr("y", (d: any) => d.y)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGlobalGraph() {
|
for (const l of linkRenderData) {
|
||||||
const slug = getFullSlug(window)
|
const linkData = l.simulationData
|
||||||
const container = document.getElementById("global-graph-outer")
|
l.gfx.clear()
|
||||||
const sidebar = container?.closest(".sidebar") as HTMLElement
|
l.gfx.moveTo(linkData.source.x! + width / 2, linkData.source.y! + height / 2)
|
||||||
container?.classList.add("active")
|
l.gfx
|
||||||
if (sidebar) {
|
.lineTo(linkData.target.x! + width / 2, linkData.target.y! + height / 2)
|
||||||
sidebar.style.zIndex = "1"
|
.stroke({ alpha: l.alpha, width: 1, color: l.color })
|
||||||
}
|
}
|
||||||
|
|
||||||
renderGraph("global-graph-container", slug)
|
tweens.forEach((t) => t.update(time))
|
||||||
|
app.renderer.render(stage)
|
||||||
function hideGlobalGraph() {
|
requestAnimationFrame(animate)
|
||||||
container?.classList.remove("active")
|
|
||||||
const graph = document.getElementById("global-graph-container")
|
|
||||||
if (sidebar) {
|
|
||||||
sidebar.style.zIndex = "unset"
|
|
||||||
}
|
|
||||||
if (!graph) return
|
|
||||||
removeAllChildren(graph)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registerEscapeHandler(container, hideGlobalGraph)
|
const graphAnimationFrameHandle = requestAnimationFrame(animate)
|
||||||
|
window.addCleanup(() => cancelAnimationFrame(graphAnimationFrameHandle))
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||||
@@ -364,7 +550,52 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
addToVisited(simplifySlug(slug))
|
addToVisited(simplifySlug(slug))
|
||||||
await renderGraph("graph-container", slug)
|
await renderGraph("graph-container", slug)
|
||||||
|
|
||||||
|
// Function to re-render the graph when the theme changes
|
||||||
|
const handleThemeChange = () => {
|
||||||
|
renderGraph("graph-container", slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
// event listener for theme change
|
||||||
|
document.addEventListener("themechange", handleThemeChange)
|
||||||
|
|
||||||
|
// cleanup for the event listener
|
||||||
|
window.addCleanup(() => {
|
||||||
|
document.removeEventListener("themechange", handleThemeChange)
|
||||||
|
})
|
||||||
|
|
||||||
|
const container = document.getElementById("global-graph-outer")
|
||||||
|
const sidebar = container?.closest(".sidebar") as HTMLElement
|
||||||
|
|
||||||
|
function renderGlobalGraph() {
|
||||||
|
const slug = getFullSlug(window)
|
||||||
|
container?.classList.add("active")
|
||||||
|
if (sidebar) {
|
||||||
|
sidebar.style.zIndex = "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
renderGraph("global-graph-container", slug)
|
||||||
|
registerEscapeHandler(container, hideGlobalGraph)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideGlobalGraph() {
|
||||||
|
container?.classList.remove("active")
|
||||||
|
if (sidebar) {
|
||||||
|
sidebar.style.zIndex = "unset"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
|
||||||
|
if (e.key === "g" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
const globalGraphOpen = container?.classList.contains("active")
|
||||||
|
globalGraphOpen ? hideGlobalGraph() : renderGlobalGraph()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const containerIcon = document.getElementById("global-graph-icon")
|
const containerIcon = document.getElementById("global-graph-icon")
|
||||||
containerIcon?.addEventListener("click", renderGlobalGraph)
|
containerIcon?.addEventListener("click", renderGlobalGraph)
|
||||||
window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph))
|
window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph))
|
||||||
|
|
||||||
|
document.addEventListener("keydown", shortcutHandler)
|
||||||
|
window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
const data = await fetchData
|
const data = await fetchData
|
||||||
const container = document.getElementById("search-container")
|
const container = document.getElementById("search-container")
|
||||||
const sidebar = container?.closest(".sidebar") as HTMLElement
|
const sidebar = container?.closest(".sidebar") as HTMLElement
|
||||||
const searchIcon = document.getElementById("search-icon")
|
const searchButton = document.getElementById("search-button")
|
||||||
const searchBar = document.getElementById("search-bar") as HTMLInputElement | null
|
const searchBar = document.getElementById("search-bar") as HTMLInputElement | null
|
||||||
const searchLayout = document.getElementById("search-layout")
|
const searchLayout = document.getElementById("search-layout")
|
||||||
const idDataMap = Object.keys(data) as FullSlug[]
|
const idDataMap = Object.keys(data) as FullSlug[]
|
||||||
@@ -191,6 +191,8 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
searchType = "basic" // reset search type after closing
|
searchType = "basic" // reset search type after closing
|
||||||
|
|
||||||
|
searchButton?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSearch(searchTypeNew: SearchType) {
|
function showSearch(searchTypeNew: SearchType) {
|
||||||
@@ -458,8 +460,8 @@ 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))
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ 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")
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
|
@use "../../styles/variables.scss" as *;
|
||||||
|
|
||||||
.backlinks {
|
.backlinks {
|
||||||
position: relative;
|
@media all and not ($desktop) {
|
||||||
|
overflow-y: auto;
|
||||||
|
display: initial;
|
||||||
|
&:after {
|
||||||
|
pointer-events: none;
|
||||||
|
content: "";
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
background: linear-gradient(transparent 0px, var(--light));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(> .overflow) {
|
||||||
|
position: unset;
|
||||||
|
}
|
||||||
|
|
||||||
& > h3 {
|
& > h3 {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@@ -17,4 +37,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& > .overflow {
|
||||||
|
max-height: unset;
|
||||||
|
& > li:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
.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 10px;
|
||||||
|
text-align: inherit;
|
||||||
& > .toggle {
|
|
||||||
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 +27,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,24 @@
|
|||||||
@use "../../styles/variables.scss" as *;
|
@use "../../styles/variables.scss" as *;
|
||||||
|
|
||||||
|
.explorer {
|
||||||
|
&.desktop-only {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
pointer-events: none;
|
||||||
|
content: "";
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
background: linear-gradient(transparent 0px, var(--light));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
button#explorer {
|
button#explorer {
|
||||||
all: unset;
|
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -46,11 +63,17 @@ button#explorer {
|
|||||||
list-style: none;
|
list-style: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
transition: max-height 0.35s ease;
|
transition:
|
||||||
|
max-height 0.35s ease,
|
||||||
|
visibility 0s linear 0s;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
|
visibility: visible;
|
||||||
|
|
||||||
&.collapsed > .overflow::after {
|
&.collapsed {
|
||||||
opacity: 0;
|
transition:
|
||||||
|
max-height 0.35s ease,
|
||||||
|
visibility 0s linear 0.35s;
|
||||||
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
& ul {
|
& ul {
|
||||||
@@ -67,6 +90,9 @@ button#explorer {
|
|||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
> #explorer-ul {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
|
|||||||
@@ -16,10 +16,13 @@
|
|||||||
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;
|
||||||
@@ -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 ($desktop) {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
opacity 0.3s ease,
|
opacity 0.3s ease,
|
||||||
visibility 0.3s ease;
|
visibility 0.3s ease;
|
||||||
|
|
||||||
@media all and (max-width: $mobileBreakpoint) {
|
@media all and ($mobile) {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,20 +3,25 @@
|
|||||||
.search {
|
.search {
|
||||||
min-width: fit-content;
|
min-width: fit-content;
|
||||||
max-width: 14rem;
|
max-width: 14rem;
|
||||||
|
@media all and ($mobile) {
|
||||||
flex-grow: 0.3;
|
flex-grow: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
& > #search-icon {
|
& > .search-button {
|
||||||
background-color: var(--lightgray);
|
background-color: var(--lightgray);
|
||||||
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
|
padding: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
text-align: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
& > div {
|
justify-content: space-between;
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > p {
|
& > p {
|
||||||
display: inline;
|
display: inline;
|
||||||
@@ -59,7 +64,7 @@
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
|
||||||
@media all and (max-width: $fullPageWidth) {
|
@media all and ($desktop) {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +106,7 @@
|
|||||||
flex: 0 0 min(30%, 450px);
|
flex: 0 0 min(30%, 450px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (min-width: $tabletBreakpoint) {
|
@media all and not ($tablet) {
|
||||||
&[data-preview] {
|
&[data-preview] {
|
||||||
& .result-card > p.preview {
|
& .result-card > p.preview {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -127,7 +132,7 @@
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (max-width: $tabletBreakpoint) {
|
@media all and ($tablet) {
|
||||||
& > #preview-container {
|
& > #preview-container {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
.toc {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
button#toc {
|
button#toc {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -29,8 +33,18 @@ button#toc {
|
|||||||
list-style: none;
|
list-style: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
transition: max-height 0.5s ease;
|
transition:
|
||||||
|
max-height 0.5s ease,
|
||||||
|
visibility 0s linear 0s;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
visibility: visible;
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
|
transition:
|
||||||
|
max-height 0.5s ease,
|
||||||
|
visibility 0s linear 0.5s;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
&.collapsed > .overflow::after {
|
&.collapsed > .overflow::after {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ function addGlobalPageResources(ctx: BuildCtx, componentResources: ComponentReso
|
|||||||
} else if (cfg.analytics?.provider === "cabin") {
|
} else if (cfg.analytics?.provider === "cabin") {
|
||||||
componentResources.afterDOMLoaded.push(`
|
componentResources.afterDOMLoaded.push(`
|
||||||
const cabinScript = document.createElement("script")
|
const cabinScript = document.createElement("script")
|
||||||
cabinScript.src = "${cfg.analytics.host ?? "https://scripts.cabin.dev"}/cabin.js"
|
cabinScript.src = "${cfg.analytics.host ?? "https://scripts.withcabin.com"}/hello.js"
|
||||||
cabinScript.defer = true
|
cabinScript.defer = true
|
||||||
cabinScript.async = true
|
cabinScript.async = true
|
||||||
document.head.appendChild(cabinScript)
|
document.head.appendChild(cabinScript)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const defaultOptions: Options = {
|
|||||||
csl: "apa",
|
csl: "apa",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Citations: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
export const Citations: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||||
const opts = { ...defaultOptions, ...userOpts }
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
return {
|
return {
|
||||||
name: "Citations",
|
name: "Citations",
|
||||||
@@ -38,7 +38,7 @@ export const Citations: QuartzTransformerPlugin<Partial<Options> | undefined> =
|
|||||||
// using https://github.com/syntax-tree/unist-util-visit as they're just anochor links
|
// using https://github.com/syntax-tree/unist-util-visit as they're just anochor links
|
||||||
plugins.push(() => {
|
plugins.push(() => {
|
||||||
return (tree, _file) => {
|
return (tree, _file) => {
|
||||||
visit(tree, "element", (node, index, parent) => {
|
visit(tree, "element", (node, _index, _parent) => {
|
||||||
if (node.tagName === "a" && node.properties?.href?.startsWith("#bib")) {
|
if (node.tagName === "a" && node.properties?.href?.startsWith("#bib")) {
|
||||||
node.properties["data-no-popover"] = true
|
node.properties["data-no-popover"] = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const urlRegex = new RegExp(
|
|||||||
"g",
|
"g",
|
||||||
)
|
)
|
||||||
|
|
||||||
export const Description: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
export const Description: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||||
const opts = { ...defaultOptions, ...userOpts }
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
return {
|
return {
|
||||||
name: "Description",
|
name: "Description",
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function coerceToArray(input: string | string[]): string[] | undefined {
|
|||||||
.map((tag: string | number) => tag.toString())
|
.map((tag: string | number) => tag.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
export const FrontMatter: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||||
const opts = { ...defaultOptions, ...userOpts }
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
return {
|
return {
|
||||||
name: "FrontMatter",
|
name: "FrontMatter",
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ const defaultOptions: Options = {
|
|||||||
linkHeadings: true,
|
linkHeadings: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (
|
export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||||
userOpts,
|
|
||||||
) => {
|
|
||||||
const opts = { ...defaultOptions, ...userOpts }
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
return {
|
return {
|
||||||
name: "GitHubFlavoredMarkdown",
|
name: "GitHubFlavoredMarkdown",
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ export { OxHugoFlavouredMarkdown } from "./oxhugofm"
|
|||||||
export { SyntaxHighlighting } from "./syntax"
|
export { SyntaxHighlighting } from "./syntax"
|
||||||
export { TableOfContents } from "./toc"
|
export { TableOfContents } from "./toc"
|
||||||
export { HardLineBreaks } from "./linebreaks"
|
export { HardLineBreaks } from "./linebreaks"
|
||||||
|
export { RoamFlavoredMarkdown } from "./roam"
|
||||||
|
|||||||
@@ -27,9 +27,7 @@ function coerceDate(fp: string, d: any): Date {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MaybeDate = undefined | string | number
|
type MaybeDate = undefined | string | number
|
||||||
export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | undefined> = (
|
export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||||
userOpts,
|
|
||||||
) => {
|
|
||||||
const opts = { ...defaultOptions, ...userOpts }
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
return {
|
return {
|
||||||
name: "CreatedModifiedDate",
|
name: "CreatedModifiedDate",
|
||||||
|
|||||||
@@ -5,10 +5,16 @@ import { QuartzTransformerPlugin } from "../types"
|
|||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
renderEngine: "katex" | "mathjax"
|
renderEngine: "katex" | "mathjax"
|
||||||
|
customMacros: MacroType
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Latex: QuartzTransformerPlugin<Options> = (opts?: Options) => {
|
interface MacroType {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Latex: QuartzTransformerPlugin<Partial<Options>> = (opts) => {
|
||||||
const engine = opts?.renderEngine ?? "katex"
|
const engine = opts?.renderEngine ?? "katex"
|
||||||
|
const macros = opts?.customMacros ?? {}
|
||||||
return {
|
return {
|
||||||
name: "Latex",
|
name: "Latex",
|
||||||
markdownPlugins() {
|
markdownPlugins() {
|
||||||
@@ -16,9 +22,9 @@ export const Latex: QuartzTransformerPlugin<Options> = (opts?: Options) => {
|
|||||||
},
|
},
|
||||||
htmlPlugins() {
|
htmlPlugins() {
|
||||||
if (engine === "katex") {
|
if (engine === "katex") {
|
||||||
return [[rehypeKatex, { output: "html" }]]
|
return [[rehypeKatex, { output: "html", macros }]]
|
||||||
} else {
|
} else {
|
||||||
return [rehypeMathjax]
|
return [[rehypeMathjax, { macros }]]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
externalResources() {
|
externalResources() {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
simplifySlug,
|
simplifySlug,
|
||||||
splitAnchor,
|
splitAnchor,
|
||||||
transformLink,
|
transformLink,
|
||||||
joinSegments,
|
|
||||||
} from "../../util/path"
|
} from "../../util/path"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { visit } from "unist-util-visit"
|
import { visit } from "unist-util-visit"
|
||||||
@@ -33,7 +32,7 @@ const defaultOptions: Options = {
|
|||||||
externalLinkIcon: true,
|
externalLinkIcon: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||||
const opts = { ...defaultOptions, ...userOpts }
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
return {
|
return {
|
||||||
name: "LinkProcessing",
|
name: "LinkProcessing",
|
||||||
@@ -66,7 +65,9 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
|
|||||||
type: "element",
|
type: "element",
|
||||||
tagName: "svg",
|
tagName: "svg",
|
||||||
properties: {
|
properties: {
|
||||||
|
"aria-hidden": "true",
|
||||||
class: "external-icon",
|
class: "external-icon",
|
||||||
|
style: "max-width:0.8em;max-height:0.8em",
|
||||||
viewBox: "0 0 512 512",
|
viewBox: "0 0 512 512",
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -136,9 +136,7 @@ const wikilinkImageEmbedRegex = new RegExp(
|
|||||||
/^(?<alt>(?!^\d*x?\d*$).*?)?(\|?\s*?(?<width>\d+)(x(?<height>\d+))?)?$/,
|
/^(?<alt>(?!^\d*x?\d*$).*?)?(\|?\s*?(?<width>\d+)(x(?<height>\d+))?)?$/,
|
||||||
)
|
)
|
||||||
|
|
||||||
export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (
|
export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||||
userOpts,
|
|
||||||
) => {
|
|
||||||
const opts = { ...defaultOptions, ...userOpts }
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
|
|
||||||
const mdastToHtml = (ast: PhrasingContent | Paragraph) => {
|
const mdastToHtml = (ast: PhrasingContent | Paragraph) => {
|
||||||
@@ -326,8 +324,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
replacements.push([
|
replacements.push([
|
||||||
tagRegex,
|
tagRegex,
|
||||||
(_value: string, tag: string) => {
|
(_value: string, tag: string) => {
|
||||||
// Check if the tag only includes numbers
|
// Check if the tag only includes numbers and slashes
|
||||||
if (/^\d+$/.test(tag)) {
|
if (/^[\/\d]+$/.test(tag)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,9 +47,7 @@ const quartzLatexRegex = new RegExp(/\$\$[\s\S]*?\$\$|\$.*?\$/, "g")
|
|||||||
* markdown to make it compatible with quartz but the list of changes applied it
|
* markdown to make it compatible with quartz but the list of changes applied it
|
||||||
* is not exhaustive.
|
* is not exhaustive.
|
||||||
* */
|
* */
|
||||||
export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (
|
export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||||
userOpts,
|
|
||||||
) => {
|
|
||||||
const opts = { ...defaultOptions, ...userOpts }
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
return {
|
return {
|
||||||
name: "OxHugoFlavouredMarkdown",
|
name: "OxHugoFlavouredMarkdown",
|
||||||
|
|||||||
224
quartz/plugins/transformers/roam.ts
Normal file
224
quartz/plugins/transformers/roam.ts
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
|
import { PluggableList } from "unified"
|
||||||
|
import { SKIP, visit } from "unist-util-visit"
|
||||||
|
import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace"
|
||||||
|
import { Root, Html, Paragraph, Text, Link, Parent } from "mdast"
|
||||||
|
import { Node } from "unist"
|
||||||
|
import { VFile } from "vfile"
|
||||||
|
import { BuildVisitor } from "unist-util-visit"
|
||||||
|
|
||||||
|
export interface Options {
|
||||||
|
orComponent: boolean
|
||||||
|
TODOComponent: boolean
|
||||||
|
DONEComponent: boolean
|
||||||
|
videoComponent: boolean
|
||||||
|
audioComponent: boolean
|
||||||
|
pdfComponent: boolean
|
||||||
|
blockquoteComponent: boolean
|
||||||
|
tableComponent: boolean
|
||||||
|
attributeComponent: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultOptions: Options = {
|
||||||
|
orComponent: true,
|
||||||
|
TODOComponent: true,
|
||||||
|
DONEComponent: true,
|
||||||
|
videoComponent: true,
|
||||||
|
audioComponent: true,
|
||||||
|
pdfComponent: true,
|
||||||
|
blockquoteComponent: true,
|
||||||
|
tableComponent: true,
|
||||||
|
attributeComponent: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const orRegex = new RegExp(/{{or:(.*?)}}/, "g")
|
||||||
|
const TODORegex = new RegExp(/{{.*?\bTODO\b.*?}}/, "g")
|
||||||
|
const DONERegex = new RegExp(/{{.*?\bDONE\b.*?}}/, "g")
|
||||||
|
const videoRegex = new RegExp(/{{.*?\[\[video\]\].*?\:(.*?)}}/, "g")
|
||||||
|
const youtubeRegex = new RegExp(
|
||||||
|
/{{.*?\[\[video\]\].*?(https?:\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-\_]*)(&(amp;)?[\w\?=]*)?)}}/,
|
||||||
|
"g",
|
||||||
|
)
|
||||||
|
|
||||||
|
// const multimediaRegex = new RegExp(/{{.*?\b(video|audio)\b.*?\:(.*?)}}/, "g")
|
||||||
|
|
||||||
|
const audioRegex = new RegExp(/{{.*?\[\[audio\]\].*?\:(.*?)}}/, "g")
|
||||||
|
const pdfRegex = new RegExp(/{{.*?\[\[pdf\]\].*?\:(.*?)}}/, "g")
|
||||||
|
const blockquoteRegex = new RegExp(/(\[\[>\]\])\s*(.*)/, "g")
|
||||||
|
const roamHighlightRegex = new RegExp(/\^\^(.+)\^\^/, "g")
|
||||||
|
const roamItalicRegex = new RegExp(/__(.+)__/, "g")
|
||||||
|
const tableRegex = new RegExp(/- {{.*?\btable\b.*?}}/, "g") /* TODO */
|
||||||
|
const attributeRegex = new RegExp(/\b\w+(?:\s+\w+)*::/, "g") /* TODO */
|
||||||
|
|
||||||
|
function isSpecialEmbed(node: Paragraph): boolean {
|
||||||
|
if (node.children.length !== 2) return false
|
||||||
|
|
||||||
|
const [textNode, linkNode] = node.children
|
||||||
|
return (
|
||||||
|
textNode.type === "text" &&
|
||||||
|
textNode.value.startsWith("{{[[") &&
|
||||||
|
linkNode.type === "link" &&
|
||||||
|
linkNode.children[0].type === "text" &&
|
||||||
|
linkNode.children[0].value.endsWith("}}")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformSpecialEmbed(node: Paragraph, opts: Options): Html | null {
|
||||||
|
const [textNode, linkNode] = node.children as [Text, Link]
|
||||||
|
const embedType = textNode.value.match(/\{\{\[\[(.*?)\]\]:/)?.[1]?.toLowerCase()
|
||||||
|
const url = linkNode.url.slice(0, -2) // Remove the trailing '}}'
|
||||||
|
|
||||||
|
switch (embedType) {
|
||||||
|
case "audio":
|
||||||
|
return opts.audioComponent
|
||||||
|
? {
|
||||||
|
type: "html",
|
||||||
|
value: `<audio controls>
|
||||||
|
<source src="${url}" type="audio/mpeg">
|
||||||
|
<source src="${url}" type="audio/ogg">
|
||||||
|
Your browser does not support the audio tag.
|
||||||
|
</audio>`,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
case "video":
|
||||||
|
if (!opts.videoComponent) return null
|
||||||
|
// Check if it's a YouTube video
|
||||||
|
const youtubeMatch = url.match(
|
||||||
|
/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=)?(.+)/,
|
||||||
|
)
|
||||||
|
if (youtubeMatch) {
|
||||||
|
const videoId = youtubeMatch[1].split("&")[0] // Remove additional parameters
|
||||||
|
const playlistMatch = url.match(/[?&]list=([^#\&\?]*)/)
|
||||||
|
const playlistId = playlistMatch ? playlistMatch[1] : null
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "html",
|
||||||
|
value: `<iframe
|
||||||
|
class="external-embed youtube"
|
||||||
|
width="600px"
|
||||||
|
height="350px"
|
||||||
|
src="https://www.youtube.com/embed/${videoId}${playlistId ? `?list=${playlistId}` : ""}"
|
||||||
|
frameborder="0"
|
||||||
|
allow="fullscreen"
|
||||||
|
></iframe>`,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
type: "html",
|
||||||
|
value: `<video controls>
|
||||||
|
<source src="${url}" type="video/mp4">
|
||||||
|
<source src="${url}" type="video/webm">
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "pdf":
|
||||||
|
return opts.pdfComponent
|
||||||
|
? {
|
||||||
|
type: "html",
|
||||||
|
value: `<embed src="${url}" type="application/pdf" width="100%" height="600px" />`,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RoamFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (
|
||||||
|
userOpts,
|
||||||
|
) => {
|
||||||
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "RoamFlavoredMarkdown",
|
||||||
|
markdownPlugins() {
|
||||||
|
const plugins: PluggableList = []
|
||||||
|
|
||||||
|
plugins.push(() => {
|
||||||
|
return (tree: Root, file: VFile) => {
|
||||||
|
const replacements: [RegExp, ReplaceFunction][] = []
|
||||||
|
|
||||||
|
// Handle special embeds (audio, video, PDF)
|
||||||
|
if (opts.audioComponent || opts.videoComponent || opts.pdfComponent) {
|
||||||
|
visit(tree, "paragraph", ((node: Paragraph, index: number, parent: Parent | null) => {
|
||||||
|
if (isSpecialEmbed(node)) {
|
||||||
|
const transformedNode = transformSpecialEmbed(node, opts)
|
||||||
|
if (transformedNode && parent) {
|
||||||
|
parent.children[index] = transformedNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) as BuildVisitor<Root, "paragraph">)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Roam italic syntax
|
||||||
|
replacements.push([
|
||||||
|
roamItalicRegex,
|
||||||
|
(_value: string, match: string) => ({
|
||||||
|
type: "emphasis",
|
||||||
|
children: [{ type: "text", value: match }],
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Roam highlight syntax
|
||||||
|
replacements.push([
|
||||||
|
roamHighlightRegex,
|
||||||
|
(_value: string, inner: string) => ({
|
||||||
|
type: "html",
|
||||||
|
value: `<span class="text-highlight">${inner}</span>`,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
if (opts.orComponent) {
|
||||||
|
replacements.push([
|
||||||
|
orRegex,
|
||||||
|
(match: string) => {
|
||||||
|
const matchResult = match.match(/{{or:(.*?)}}/)
|
||||||
|
if (matchResult === null) {
|
||||||
|
return { type: "html", value: "" }
|
||||||
|
}
|
||||||
|
const optionsString: string = matchResult[1]
|
||||||
|
const options: string[] = optionsString.split("|")
|
||||||
|
const selectHtml: string = `<select>${options.map((option: string) => `<option value="${option}">${option}</option>`).join("")}</select>`
|
||||||
|
return { type: "html", value: selectHtml }
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.TODOComponent) {
|
||||||
|
replacements.push([
|
||||||
|
TODORegex,
|
||||||
|
() => ({
|
||||||
|
type: "html",
|
||||||
|
value: `<input type="checkbox" disabled>`,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.DONEComponent) {
|
||||||
|
replacements.push([
|
||||||
|
DONERegex,
|
||||||
|
() => ({
|
||||||
|
type: "html",
|
||||||
|
value: `<input type="checkbox" checked disabled>`,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.blockquoteComponent) {
|
||||||
|
replacements.push([
|
||||||
|
blockquoteRegex,
|
||||||
|
(_match: string, _marker: string, content: string) => ({
|
||||||
|
type: "html",
|
||||||
|
value: `<blockquote>${content.trim()}</blockquote>`,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
mdastFindReplace(tree, replacements)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return plugins
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,10 +19,8 @@ const defaultOptions: Options = {
|
|||||||
keepBackground: false,
|
keepBackground: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SyntaxHighlighting: QuartzTransformerPlugin<Options> = (
|
export const SyntaxHighlighting: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||||
userOpts?: Partial<Options>,
|
const opts: CodeOptions = { ...defaultOptions, ...userOpts }
|
||||||
) => {
|
|
||||||
const opts: Partial<CodeOptions> = { ...defaultOptions, ...userOpts }
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: "SyntaxHighlighting",
|
name: "SyntaxHighlighting",
|
||||||
|
|||||||
@@ -25,9 +25,7 @@ interface TocEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const slugAnchor = new Slugger()
|
const slugAnchor = new Slugger()
|
||||||
export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefined> = (
|
export const TableOfContents: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||||
userOpts,
|
|
||||||
) => {
|
|
||||||
const opts = { ...defaultOptions, ...userOpts }
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
return {
|
return {
|
||||||
name: "TableOfContents",
|
name: "TableOfContents",
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise<Pro
|
|||||||
|
|
||||||
const childPromises: WorkerPromise<ProcessedContent[]>[] = []
|
const childPromises: WorkerPromise<ProcessedContent[]>[] = []
|
||||||
for (const chunk of chunks(fps, CHUNK_SIZE)) {
|
for (const chunk of chunks(fps, CHUNK_SIZE)) {
|
||||||
childPromises.push(pool.exec("parseFiles", [argv, chunk, ctx.allSlugs]))
|
childPromises.push(pool.exec("parseFiles", [ctx.buildId, argv, chunk, ctx.allSlugs]))
|
||||||
}
|
}
|
||||||
|
|
||||||
const results: ProcessedContent[][] = await WorkerPromise.all(childPromises).catch((err) => {
|
const results: ProcessedContent[][] = await WorkerPromise.all(childPromises).catch((err) => {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ html {
|
|||||||
body,
|
body,
|
||||||
section {
|
section {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
max-width: 100%;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background-color: var(--light);
|
background-color: var(--light);
|
||||||
font-family: var(--bodyFont);
|
font-family: var(--bodyFont);
|
||||||
@@ -109,25 +108,21 @@ a {
|
|||||||
|
|
||||||
.desktop-only {
|
.desktop-only {
|
||||||
display: initial;
|
display: initial;
|
||||||
@media all and (max-width: $fullPageWidth) {
|
@media all and ($mobile) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-only {
|
.mobile-only {
|
||||||
display: none;
|
display: none;
|
||||||
@media all and (max-width: $fullPageWidth) {
|
@media all and ($mobile) {
|
||||||
display: initial;
|
display: initial;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
@media all and (max-width: $fullPageWidth) {
|
max-width: calc(#{map-get($breakpoints, desktop)} + 300px);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 1rem;
|
|
||||||
max-width: $pageWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
& article {
|
& article {
|
||||||
& > h1 {
|
& > h1 {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
@@ -155,79 +150,118 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
& > #quartz-body {
|
& > #quartz-body {
|
||||||
width: 100%;
|
display: grid;
|
||||||
display: flex;
|
grid-template-columns: #{map-get($desktopGrid, templateColumns)};
|
||||||
@media all and (max-width: $fullPageWidth) {
|
grid-template-rows: #{map-get($desktopGrid, templateRows)};
|
||||||
flex-direction: column;
|
column-gap: #{map-get($desktopGrid, columnGap)};
|
||||||
|
row-gap: #{map-get($desktopGrid, rowGap)};
|
||||||
|
grid-template-areas: #{map-get($desktopGrid, templateAreas)};
|
||||||
|
@media all and ($desktop) {
|
||||||
|
grid-template-columns: #{map-get($tabletGrid, templateColumns)};
|
||||||
|
grid-template-rows: #{map-get($tabletGrid, templateRows)};
|
||||||
|
column-gap: #{map-get($tabletGrid, columnGap)};
|
||||||
|
row-gap: #{map-get($tabletGrid, rowGap)};
|
||||||
|
grid-template-areas: #{map-get($tabletGrid, templateAreas)};
|
||||||
|
}
|
||||||
|
@media all and ($mobile) {
|
||||||
|
grid-template-columns: #{map-get($mobileGrid, templateColumns)};
|
||||||
|
grid-template-rows: #{map-get($mobileGrid, templateRows)};
|
||||||
|
column-gap: #{map-get($mobileGrid, columnGap)};
|
||||||
|
row-gap: #{map-get($mobileGrid, rowGap)};
|
||||||
|
grid-template-areas: #{map-get($mobileGrid, templateAreas)};
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and ($desktop) {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
@media all and ($mobile) {
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .sidebar {
|
& .sidebar {
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: $sidePanelWidth;
|
|
||||||
margin-top: $topSpacing;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 0 4rem;
|
padding: $topSpacing 2rem 2rem 2rem;
|
||||||
position: fixed;
|
display: flex;
|
||||||
@media all and (max-width: $fullPageWidth) {
|
height: 100vh;
|
||||||
position: initial;
|
position: sticky;
|
||||||
flex-direction: row;
|
|
||||||
padding: 0;
|
|
||||||
width: initial;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& .sidebar.left {
|
& .sidebar.left {
|
||||||
left: calc(calc(100vw - $pageWidth) / 2 - $sidePanelWidth);
|
z-index: 1;
|
||||||
@media all and (max-width: $fullPageWidth) {
|
grid-area: sidebar-left;
|
||||||
|
flex-direction: column;
|
||||||
|
@media all and ($mobile) {
|
||||||
gap: 0;
|
gap: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
position: initial;
|
||||||
|
display: flex;
|
||||||
|
height: unset;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 0;
|
||||||
|
padding-top: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& .sidebar.right {
|
& .sidebar.right {
|
||||||
right: calc(calc(100vw - $pageWidth) / 2 - $sidePanelWidth);
|
grid-area: sidebar-right;
|
||||||
flex-wrap: wrap;
|
margin-right: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
@media all and ($mobile) {
|
||||||
|
margin-left: inherit;
|
||||||
|
margin-right: inherit;
|
||||||
|
}
|
||||||
|
@media all and ($desktop) {
|
||||||
|
position: initial;
|
||||||
|
height: unset;
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 0;
|
||||||
& > * {
|
& > * {
|
||||||
@media all and (max-width: $fullPageWidth) {
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 140px;
|
}
|
||||||
|
& > .toc {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
& .page-header,
|
& .page-header,
|
||||||
& .page-footer {
|
& .page-footer {
|
||||||
width: $pageWidth;
|
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
|
|
||||||
@media all and (max-width: $fullPageWidth) {
|
|
||||||
width: initial;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& .page-header {
|
& .page-header {
|
||||||
margin: $topSpacing auto 0 auto;
|
grid-area: page-header;
|
||||||
@media all and (max-width: $fullPageWidth) {
|
margin: $topSpacing 0 0 0;
|
||||||
margin-top: 2rem;
|
@media all and ($mobile) {
|
||||||
|
margin-top: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& .center > article {
|
||||||
|
grid-area: page-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .page-footer {
|
||||||
|
grid-area: page-footer;
|
||||||
|
}
|
||||||
|
|
||||||
& .center,
|
& .center,
|
||||||
& footer {
|
& footer {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
width: $pageWidth;
|
@media all and ($desktop) {
|
||||||
@media all and (max-width: $fullPageWidth) {
|
|
||||||
width: initial;
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
@media all and ($mobile) {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
& footer {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,56 @@
|
|||||||
$pageWidth: 750px;
|
/**
|
||||||
$mobileBreakpoint: 600px;
|
* Layout breakpoints
|
||||||
$tabletBreakpoint: 1000px;
|
* $mobile: screen width below this value will use mobile styles
|
||||||
$sidePanelWidth: 380px;
|
* $desktop: screen width above this value will use desktop styles
|
||||||
|
* Screen width between $mobile and $desktop width will use the tablet layout.
|
||||||
|
* assuming mobile < desktop
|
||||||
|
*/
|
||||||
|
$breakpoints: (
|
||||||
|
mobile: 800px,
|
||||||
|
desktop: 1200px,
|
||||||
|
);
|
||||||
|
|
||||||
|
$mobile: "(max-width: #{map-get($breakpoints, mobile)})";
|
||||||
|
$tablet: "(min-width: #{map-get($breakpoints, mobile)}) and (max-width: #{map-get($breakpoints, desktop)})";
|
||||||
|
$desktop: "(max-width: #{map-get($breakpoints, desktop)})";
|
||||||
|
|
||||||
|
$pageWidth: #{map-get($breakpoints, mobile)};
|
||||||
|
$sidePanelWidth: 320px; //380px;
|
||||||
$topSpacing: 6rem;
|
$topSpacing: 6rem;
|
||||||
$fullPageWidth: $pageWidth + 2 * $sidePanelWidth;
|
|
||||||
$boldWeight: 700;
|
$boldWeight: 700;
|
||||||
$semiBoldWeight: 600;
|
$semiBoldWeight: 600;
|
||||||
$normalWeight: 400;
|
$normalWeight: 400;
|
||||||
|
|
||||||
|
$mobileGrid: (
|
||||||
|
templateRows: "auto auto auto auto auto",
|
||||||
|
templateColumns: "auto",
|
||||||
|
rowGap: "5px",
|
||||||
|
columnGap: "5px",
|
||||||
|
templateAreas:
|
||||||
|
'"sidebar-left"\
|
||||||
|
"page-header"\
|
||||||
|
"page-center"\
|
||||||
|
"sidebar-right"\
|
||||||
|
"page-footer"',
|
||||||
|
);
|
||||||
|
$tabletGrid: (
|
||||||
|
templateRows: "auto auto auto auto",
|
||||||
|
templateColumns: "#{$sidePanelWidth} auto",
|
||||||
|
rowGap: "5px",
|
||||||
|
columnGap: "5px",
|
||||||
|
templateAreas:
|
||||||
|
'"sidebar-left page-header"\
|
||||||
|
"sidebar-left page-center"\
|
||||||
|
"sidebar-left sidebar-right"\
|
||||||
|
"sidebar-left page-footer"',
|
||||||
|
);
|
||||||
|
$desktopGrid: (
|
||||||
|
templateRows: "auto auto auto",
|
||||||
|
templateColumns: "#{$sidePanelWidth} auto #{$sidePanelWidth}",
|
||||||
|
rowGap: "5px",
|
||||||
|
columnGap: "5px",
|
||||||
|
templateAreas:
|
||||||
|
'"sidebar-left page-header sidebar-right"\
|
||||||
|
"sidebar-left page-center sidebar-right"\
|
||||||
|
"sidebar-left page-footer sidebar-right"',
|
||||||
|
);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface Argv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface BuildCtx {
|
export interface BuildCtx {
|
||||||
|
buildId: string
|
||||||
argv: Argv
|
argv: Argv
|
||||||
cfg: QuartzConfig
|
cfg: QuartzConfig
|
||||||
allSlugs: FullSlug[]
|
allSlugs: FullSlug[]
|
||||||
|
|||||||
@@ -7,8 +7,14 @@ import { createFileParser, createProcessor } from "./processors/parse"
|
|||||||
import { options } from "./util/sourcemap"
|
import { options } from "./util/sourcemap"
|
||||||
|
|
||||||
// only called from worker thread
|
// only called from worker thread
|
||||||
export async function parseFiles(argv: Argv, fps: FilePath[], allSlugs: FullSlug[]) {
|
export async function parseFiles(
|
||||||
|
buildId: string,
|
||||||
|
argv: Argv,
|
||||||
|
fps: FilePath[],
|
||||||
|
allSlugs: FullSlug[],
|
||||||
|
) {
|
||||||
const ctx: BuildCtx = {
|
const ctx: BuildCtx = {
|
||||||
|
buildId,
|
||||||
cfg,
|
cfg,
|
||||||
argv,
|
argv,
|
||||||
allSlugs,
|
allSlugs,
|
||||||
|
|||||||
Reference in New Issue
Block a user