mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-12-01 02:07:55 +01:00
Compare commits
199 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d8c025d6a | ||
|
|
54b4a5567c | ||
|
|
610b04406f | ||
|
|
82bd08d14a | ||
|
|
649090de1b | ||
|
|
b5fec6c87f | ||
|
|
0d314db1f8 | ||
|
|
660aae62e0 | ||
|
|
9a599aebea | ||
|
|
296c1cf83f | ||
|
|
516d9a27e7 | ||
|
|
6a05fa777c | ||
|
|
3f0be7fbe4 | ||
|
|
ea08c0511a | ||
|
|
727b9b5d72 | ||
|
|
50f0ba29a2 | ||
|
|
95b1141b9d | ||
|
|
a26eb59392 | ||
|
|
5befcf4780 | ||
|
|
f861a7c160 | ||
|
|
06426c8f7e | ||
|
|
8fc7b9f4c6 | ||
|
|
2de48b267a | ||
|
|
76f2664277 | ||
|
|
74777118a7 | ||
|
|
8223465bda | ||
|
|
cf6ab9e933 | ||
|
|
74c63e448e | ||
|
|
43d638a6de | ||
|
|
d1551872ff | ||
|
|
275bea3051 | ||
|
|
bc02791734 | ||
|
|
bf603c49c2 | ||
|
|
f67356c3d2 | ||
|
|
5d666d1860 | ||
|
|
22b7cf135e | ||
|
|
50a87d0d86 | ||
|
|
134b6ed582 | ||
|
|
99e8f5944f | ||
|
|
e9f4e28a2d | ||
|
|
2a6b9a9ea0 | ||
|
|
e806c30fa1 | ||
|
|
aac7b7e97d | ||
|
|
101e9946bd | ||
|
|
a62a97c7ab | ||
|
|
923b72fb67 | ||
|
|
05a1c34c6f | ||
|
|
06ccb89cd7 | ||
|
|
01fc8e4640 | ||
|
|
7c01e8dde0 | ||
|
|
b7ae7a99db | ||
|
|
60b3bc34cb | ||
|
|
dc834015d0 | ||
|
|
1e357ef5ac | ||
|
|
54e722a55d | ||
|
|
86d16b12a2 | ||
|
|
ed971800c0 | ||
|
|
af9ddadc4d | ||
|
|
da0a062c05 | ||
|
|
f66d2c23ac | ||
|
|
3268d45a20 | ||
|
|
afa163f2fe | ||
|
|
cec4877adb | ||
|
|
cf0c090e3c | ||
|
|
c8f5dbbad3 | ||
|
|
ab5efac75f | ||
|
|
2f99339dcf | ||
|
|
5232d09af5 | ||
|
|
0138085c16 | ||
|
|
0b61f6fbfd | ||
|
|
d4c122646c | ||
|
|
d22c3c107a | ||
|
|
697bffdb8b | ||
|
|
ea5742c328 | ||
|
|
95eec5b49d | ||
|
|
c5b9137f12 | ||
|
|
13c8673226 | ||
|
|
a897cc1f53 | ||
|
|
d93599364a | ||
|
|
fa69c2a565 | ||
|
|
8eb1554b13 | ||
|
|
dcdeae4e7b | ||
|
|
48452231d5 | ||
|
|
16d33fb771 | ||
|
|
b029eeadab | ||
|
|
6a9e6352e8 | ||
|
|
70e029d151 | ||
|
|
0bad3ce799 | ||
|
|
52a172d1a4 | ||
|
|
d6301fae90 | ||
|
|
27a6087dd5 | ||
|
|
1bf7e3d8b3 | ||
|
|
cc31a40b0c | ||
|
|
0d3cf29226 | ||
|
|
6a2e0b3ad3 | ||
|
|
e67f409ec1 | ||
|
|
4afb099bf3 | ||
|
|
6914d4b40c | ||
|
|
af41f34bfd | ||
|
|
7ac772fca8 | ||
|
|
5cc9253c41 | ||
|
|
94a04ab1c9 | ||
|
|
9358f73f1c | ||
|
|
f7029012df | ||
|
|
fea352849c | ||
|
|
3d8c470c0d | ||
|
|
31d16fbd2c | ||
|
|
036a33f70b | ||
|
|
58aea1cb07 | ||
|
|
c7d3474ba8 | ||
|
|
422ba5c365 | ||
|
|
9ae6343dd0 | ||
|
|
5dcb7e83fc | ||
|
|
91f9ae2d71 | ||
|
|
14cbbdb8a2 | ||
|
|
cce389c81d | ||
|
|
4461748a85 | ||
|
|
6ecdcb5e24 | ||
|
|
e3b879741b | ||
|
|
60a3c54339 | ||
|
|
71d81bde1d | ||
|
|
a19df64be8 | ||
|
|
4e23e67244 | ||
|
|
a66c239797 | ||
|
|
53f1c88738 | ||
|
|
06df00b186 | ||
|
|
2525bfbab5 | ||
|
|
828aa71fe3 | ||
|
|
ef1ead31dc | ||
|
|
989bee5979 | ||
|
|
8d6029b7b8 | ||
|
|
2d52eba413 | ||
|
|
6ef4246cf1 | ||
|
|
616a7f148a | ||
|
|
e8a04efaf1 | ||
|
|
7e42be8e46 | ||
|
|
8c354f6261 | ||
|
|
505673acd7 | ||
|
|
23f43045c4 | ||
|
|
90dac31216 | ||
|
|
2d6dc176c3 | ||
|
|
b213ba45e2 | ||
|
|
5fa6fc9789 | ||
|
|
1cc09ef76d | ||
|
|
c35cd422c6 | ||
|
|
082fdf2e80 | ||
|
|
b6b1dabde0 | ||
|
|
4b89202f7e | ||
|
|
52ca312f41 | ||
|
|
c91e62c376 | ||
|
|
ad4145fb10 | ||
|
|
74c3ebb7bd | ||
|
|
e3265f8416 | ||
|
|
bc543f81d9 | ||
|
|
5c6d1e27ba | ||
|
|
340e3ef511 | ||
|
|
953ef29f4e | ||
|
|
94ce0883e7 | ||
|
|
8cf7280614 | ||
|
|
c8412a5b0a | ||
|
|
fc4b8f3d3f | ||
|
|
6cd0612d40 | ||
|
|
9851697b58 | ||
|
|
c36a9f3fb7 | ||
|
|
98d82415dc | ||
|
|
9d2340e90b | ||
|
|
8200c8d040 | ||
|
|
2e0e518f5d | ||
|
|
632c27b7ec | ||
|
|
bfb416b35a | ||
|
|
960c1814d0 | ||
|
|
eed4472aee | ||
|
|
b99eb7ebce | ||
|
|
0aaf88b852 | ||
|
|
a1a1e7e1e0 | ||
|
|
3209f7c3b7 | ||
|
|
cde1e26129 | ||
|
|
1128efcf23 | ||
|
|
d2f5254995 | ||
|
|
3064839c2d | ||
|
|
b444c5c13b | ||
|
|
36548d5986 | ||
|
|
99dbe525d9 | ||
|
|
8b63ff882a | ||
|
|
b991cf2ee8 | ||
|
|
bb677840fc | ||
|
|
c60b3d5e34 | ||
|
|
e10de3febf | ||
|
|
b69556c918 | ||
|
|
ce70571072 | ||
|
|
8c943f47d6 | ||
|
|
2774e976d2 | ||
|
|
bb93ac1c83 | ||
|
|
777ff51c7a | ||
|
|
4e42d52e16 | ||
|
|
d0f67d9935 | ||
|
|
952d6cb3dd | ||
|
|
173ec240d2 | ||
|
|
425c9789a4 |
13
.github/workflows/ci.yaml
vendored
13
.github/workflows/ci.yaml
vendored
@@ -1,6 +1,9 @@
|
|||||||
name: Build and Test
|
name: Build and Test
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- v4
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- v4
|
- v4
|
||||||
@@ -40,5 +43,11 @@ jobs:
|
|||||||
- name: Test
|
- name: Test
|
||||||
run: npm test
|
run: npm test
|
||||||
|
|
||||||
- name: Ensure Quartz builds
|
- name: Ensure Quartz builds, check bundle info
|
||||||
run: npx quartz build
|
run: npx quartz build --bundleInfo
|
||||||
|
|
||||||
|
- name: Create release tag
|
||||||
|
uses: Klemensas/action-autotag@stable
|
||||||
|
with:
|
||||||
|
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
tag_prefix: "v"
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,3 +7,5 @@ tsconfig.tsbuildinfo
|
|||||||
.obsidian
|
.obsidian
|
||||||
.quartz-cache
|
.quartz-cache
|
||||||
private/
|
private/
|
||||||
|
.replit
|
||||||
|
replit.nix
|
||||||
|
|||||||
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM node:20-slim as builder
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
COPY package.json .
|
||||||
|
COPY package-lock.json* .
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
FROM node:20-slim
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
COPY --from=builder /usr/src/app/ /usr/src/app/
|
||||||
|
COPY . .
|
||||||
|
CMD ["npx", "quartz", "build", "--serve"]
|
||||||
@@ -7,7 +7,7 @@ Quartz v4 features a from-the-ground rewrite focusing on end-user extensibility
|
|||||||
|
|
||||||
**If you are looking for Quartz v3, you can find it on the [`hugo` branch](https://github.com/jackyzha0/quartz/tree/hugo).**
|
**If you are looking for Quartz v3, you can find it on the [`hugo` branch](https://github.com/jackyzha0/quartz/tree/hugo).**
|
||||||
|
|
||||||
🔗 Read the documentation and get started: https://four.quartz.jzhao.xyz/
|
🔗 Read the documentation and get started: https://quartz.jzhao.xyz/
|
||||||
|
|
||||||
[Join the Discord Community](https://discord.gg/cRFFHYye7t)
|
[Join the Discord Community](https://discord.gg/cRFFHYye7t)
|
||||||
|
|
||||||
|
|||||||
0
content/.gitkeep
Normal file
0
content/.gitkeep
Normal file
3
docs/advanced/index.md
Normal file
3
docs/advanced/index.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
title: "Advanced"
|
||||||
|
---
|
||||||
@@ -228,7 +228,7 @@ export type QuartzEmitterPluginInstance = {
|
|||||||
|
|
||||||
An emitter plugin must define a `name` field an `emit` function and a `getQuartzComponents` function. `emit` is responsible for looking at all the parsed and filtered content and then appropriately creating files and returning a list of paths to files the plugin created.
|
An emitter plugin must define a `name` field an `emit` function and a `getQuartzComponents` function. `emit` is responsible for looking at all the parsed and filtered content and then appropriately creating files and returning a list of paths to files the plugin created.
|
||||||
|
|
||||||
Creating new files can be done via regular Node [fs module](https://nodejs.org/api/fs.html) (i.e. `fs.cp` or `fs.writeFile`) or via the `emitCallback` if you are creating files that contain text. The `emitCallback` function is the 4th argument of the emit function. It's interface looks something like this:
|
Creating new files can be done via regular Node [fs module](https://nodejs.org/api/fs.html) (i.e. `fs.cp` or `fs.writeFile`) or via the `emitCallback` if you are creating files that contain text. The `emitCallback` function is the 4th argument of the emit function. Its interface looks something like this:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
export type EmitCallback = (data: {
|
export type EmitCallback = (data: {
|
||||||
@@ -247,7 +247,7 @@ If you are creating an emitter plugin that needs to render components, there are
|
|||||||
|
|
||||||
- Your component should use `getQuartzComponents` to declare a list of `QuartzComponents` that it uses to construct the page. See the page on [[creating components]] for more information.
|
- Your component should use `getQuartzComponents` to declare a list of `QuartzComponents` that it uses to construct the page. See the page on [[creating components]] for more information.
|
||||||
- You can use the `renderPage` function defined in `quartz/components/renderPage.tsx` to render Quartz components into HTML.
|
- You can use the `renderPage` function defined in `quartz/components/renderPage.tsx` to render Quartz components into HTML.
|
||||||
- If you need to render an HTML AST to JSX, you can use the `toJsxRuntime` function from `hast-util-to-jsx-runtime` library. An example of this can be found in `quartz/components/pages/Content.tsx`.
|
- If you need to render an HTML AST to JSX, you can use the `htmlToJsx` function from `quartz/util/jsx.ts`. An example of this can be found in `quartz/components/pages/Content.tsx`.
|
||||||
|
|
||||||
For example, the following is a simplified version of the content page plugin that renders every single page.
|
For example, the following is a simplified version of the content page plugin that renders every single page.
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ Some common frontmatter fields that are natively supported by Quartz:
|
|||||||
|
|
||||||
## Syncing your Content
|
## Syncing your Content
|
||||||
|
|
||||||
When you're Quartz is at a point you're happy with, you can save your changes to GitHub by doing `npx quartz sync`.
|
When your Quartz is at a point you're happy with, you can save your changes to GitHub by doing `npx quartz sync`.
|
||||||
|
|
||||||
> [!hint] Flags and options
|
> [!hint] Flags and options
|
||||||
> For full help options, you can run `npx quartz sync --help`.
|
> For full help options, you can run `npx quartz sync --help`.
|
||||||
@@ -31,6 +31,7 @@ This part of the configuration concerns anything that can affect the whole site.
|
|||||||
- This should also include the subpath if you are [[hosting]] on GitHub pages without a custom domain. For example, if my repository is `jackyzha0/quartz`, GitHub pages would deploy to `https://jackyzha0.github.io/quartz` and the `baseUrl` would be `jackyzha0.github.io/quartz`
|
- This should also include the subpath if you are [[hosting]] on GitHub pages without a custom domain. For example, if my repository is `jackyzha0/quartz`, GitHub pages would deploy to `https://jackyzha0.github.io/quartz` and the `baseUrl` would be `jackyzha0.github.io/quartz`
|
||||||
- Note that Quartz 4 will avoid using this as much as possible and use relative URLs whenever it can to make sure your site works no matter _where_ you end up actually deploying it.
|
- Note that Quartz 4 will avoid using this as much as possible and use relative URLs whenever it can to make sure your site works no matter _where_ you end up actually deploying it.
|
||||||
- `ignorePatterns`: a list of [glob](<https://en.wikipedia.org/wiki/Glob_(programming)>) patterns that Quartz should ignore and not search through when looking for files inside the `content` folder. See [[private pages]] for more details.
|
- `ignorePatterns`: a list of [glob](<https://en.wikipedia.org/wiki/Glob_(programming)>) patterns that Quartz should ignore and not search through when looking for files inside the `content` folder. See [[private pages]] for more details.
|
||||||
|
- `defaultDateType`: whether to use created, modified, or published as the default date to display on pages and page listings.
|
||||||
- `theme`: configure how the site looks.
|
- `theme`: configure how the site looks.
|
||||||
- `typography`: what fonts to use. Any font available on [Google Fonts](https://fonts.google.com/) works here.
|
- `typography`: what fonts to use. Any font available on [Google Fonts](https://fonts.google.com/) works here.
|
||||||
- `header`: Font to use for headers
|
- `header`: Font to use for headers
|
||||||
7
docs/features/Docker Support.md
Normal file
7
docs/features/Docker Support.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Quartz comes shipped with a Docker image that will allow you to preview your Quartz locally without installing Node.
|
||||||
|
|
||||||
|
You can run the below one-liner to run Quartz in Docker.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker run --rm -itp 8080:8080 $(docker build -q .)
|
||||||
|
```
|
||||||
39
docs/features/OxHugo compatibility.md
Normal file
39
docs/features/OxHugo compatibility.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- plugin/transformer
|
||||||
|
---
|
||||||
|
|
||||||
|
[org-roam](https://www.orgroam.com/) is a plain-text personal knowledge management system for [emacs](https://en.wikipedia.org/wiki/Emacs). [ox-hugo](https://github.com/kaushalmodi/ox-hugo) is org exporter backend that exports `org-mode` files to [Hugo](https://gohugo.io/) compatible Markdown.
|
||||||
|
|
||||||
|
Because the Markdown generated by ox-hugo is not pure Markdown but Hugo specific, we need to transform it to fit into Quartz. This is done by `Plugin.OxHugoFlavouredMarkdown`. Even though this [[making plugins|plugin]] was written with `ox-hugo` in mind, it should work for any Hugo specific Markdown.
|
||||||
|
|
||||||
|
```typescript title="quartz.config.ts"
|
||||||
|
plugins: {
|
||||||
|
transformers: [
|
||||||
|
Plugin.FrontMatter({ delims: "+++", language: "toml" }), // if toml frontmatter
|
||||||
|
// ...
|
||||||
|
Plugin.OxHugoFlavouredMarkdown(),
|
||||||
|
Plugin.GitHubFlavoredMarkdown(),
|
||||||
|
// ...
|
||||||
|
],
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Quartz by default doesn't understand `org-roam` files as they aren't Markdown. You're responsible for using an external tool like `ox-hugo` to export the `org-roam` files as Markdown content to Quartz and managing the static assets so that they're available in the final output.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- Link resolution
|
||||||
|
- `wikilinks`: Whether to replace `{{ relref }}` with Quartz [[wikilinks]]
|
||||||
|
- `removePredefinedAnchor`: Whether to remove [pre-defined anchor set by ox-hugo](https://ox-hugo.scripter.co/doc/anchors/).
|
||||||
|
- Image handling
|
||||||
|
- `replaceFigureWithMdImg`: Whether to replace `<figure/>` with `![]()`
|
||||||
|
- Formatting
|
||||||
|
- `removeHugoShortcode`: Whether to remove hugo shortcode syntax (`{{}}`)
|
||||||
|
- `replaceOrgLatex`: Whether to replace org-mode formatting for latex fragments with what `Plugin.Latex` supports.
|
||||||
|
|
||||||
|
> [!warning]
|
||||||
|
>
|
||||||
|
> While you can use `Plugin.OxHugoFlavoredMarkdown` and `Plugin.ObsidianFlavoredMarkdown` together, it's not recommended because it might mutate the file in unexpected ways. Use with caution.
|
||||||
@@ -3,3 +3,5 @@ Quartz creates an RSS feed for all the content on your site by generating an `in
|
|||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
- Remove RSS feed: set the `enableRSS` field of `Plugin.ContentIndex` in `quartz.config.ts` to be `false`.
|
- Remove RSS feed: set the `enableRSS` field of `Plugin.ContentIndex` in `quartz.config.ts` to be `false`.
|
||||||
|
- Change number of entries: set the `rssLimit` field of `Plugin.ContentIndex` to be the desired value. It defaults to latest 10 items.
|
||||||
|
- Use rich HTML output in RSS: set `rssFullHtml` field of `Plugin.ContentIndex` to be `true`.
|
||||||
36
docs/features/breadcrumbs.md
Normal file
36
docs/features/breadcrumbs.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
title: "Breadcrumbs"
|
||||||
|
tags:
|
||||||
|
- component
|
||||||
|
---
|
||||||
|
|
||||||
|
Breadcrumbs provide a way to navigate a hierarchy of pages within your site using a list of its parent folders.
|
||||||
|
|
||||||
|
By default, the element at the very top of your page is the breadcrumb navigation bar (can also be seen at the top on this page!).
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
Most configuration can be done by passing in options to `Component.Breadcrumbs()`.
|
||||||
|
|
||||||
|
For example, here's what the default configuration looks like:
|
||||||
|
|
||||||
|
```typescript title="quartz.layout.ts"
|
||||||
|
Component.Breadcrumbs({
|
||||||
|
spacerSymbol: "❯", // symbol between crumbs
|
||||||
|
rootName: "Home", // name of first/root element
|
||||||
|
resolveFrontmatterTitle: true, // whether to resolve folder names through frontmatter titles
|
||||||
|
hideOnRoot: true, // whether to hide breadcrumbs on root `index.md` page
|
||||||
|
showCurrentPage: true, // wether to display the current page in the breadcrumbs
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
When passing in your own options, you can omit any or all of these fields if you'd like to keep the default value for that field.
|
||||||
|
|
||||||
|
You can also adjust where the breadcrumbs will be displayed by adjusting the [[layout]] (moving `Component.Breadcrumbs()` up or down)
|
||||||
|
|
||||||
|
Want to customize it even more?
|
||||||
|
|
||||||
|
- Removing breadcrumbs: delete all usages of `Component.Breadcrumbs()` from `quartz.layout.ts`.
|
||||||
|
- Component: `quartz/components/Breadcrumbs.tsx`
|
||||||
|
- Style: `quartz/components/styles/breadcrumbs.scss`
|
||||||
|
- Script: inline at `quartz/components/Breadcrumbs.tsx`
|
||||||
@@ -33,7 +33,7 @@ See [documentation on supported types and syntax here](https://help.obsidian.md
|
|||||||
|
|
||||||
> [!question]+ Can callouts be nested?
|
> [!question]+ Can callouts be nested?
|
||||||
>
|
>
|
||||||
> > [!todo]- Yes!, they can.
|
> > [!todo]- Yes!, they can. And collapsed!
|
||||||
> >
|
> >
|
||||||
> > > [!example] You can even use multiple layers of nesting.
|
> > > [!example] You can even use multiple layers of nesting.
|
||||||
|
|
||||||
245
docs/features/explorer.md
Normal file
245
docs/features/explorer.md
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
---
|
||||||
|
title: "Explorer"
|
||||||
|
tags:
|
||||||
|
- component
|
||||||
|
---
|
||||||
|
|
||||||
|
Quartz features an explorer that allows you to navigate all files and folders on your site. It supports nested folders and is highly customizable.
|
||||||
|
|
||||||
|
By default, it shows all folders and files on your page. To display the explorer in a different spot, you can edit the [[layout]].
|
||||||
|
|
||||||
|
Display names for folders get determined by the `title` frontmatter field in `folder/index.md` (more detail in [[authoring content | Authoring Content]]). If this file does not exist or does not contain frontmatter, the local folder name will be used instead.
|
||||||
|
|
||||||
|
> [!info]
|
||||||
|
> The explorer uses local storage by default to save the state of your explorer. This is done to ensure a smooth experience when navigating to different pages.
|
||||||
|
>
|
||||||
|
> To clear/delete the explorer state from local storage, delete the `fileTree` entry (guide on how to delete a key from local storage in chromium based browsers can be found [here](https://docs.devolutions.net/kb/general-knowledge-base/clear-browser-local-storage/clear-chrome-local-storage/)). You can disable this by passing `useSavedState: false` as an argument.
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
Most configuration can be done by passing in options to `Component.Explorer()`.
|
||||||
|
|
||||||
|
For example, here's what the default configuration looks like:
|
||||||
|
|
||||||
|
```typescript title="quartz.layout.ts"
|
||||||
|
Component.Explorer({
|
||||||
|
title: "Explorer", // title of the explorer component
|
||||||
|
folderClickBehavior: "collapse", // what happens when you click a folder ("link" to navigate to folder page on click or "collapse" to collapse folder on click)
|
||||||
|
folderDefaultState: "collapsed", // default state of folders ("collapsed" or "open")
|
||||||
|
useSavedState: true, // wether to use local storage to save "state" (which folders are opened) of explorer
|
||||||
|
// Sort order: folders first, then files. Sort folders and files alphabetically
|
||||||
|
sortFn: (a, b) => {
|
||||||
|
... // default implementation shown later
|
||||||
|
},
|
||||||
|
filterFn: filterFn: (node) => node.name !== "tags", // filters out 'tags' folder
|
||||||
|
mapFn: undefined,
|
||||||
|
// what order to apply functions in
|
||||||
|
order: ["filter", "map", "sort"],
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
When passing in your own options, you can omit any or all of these fields if you'd like to keep the default value for that field.
|
||||||
|
|
||||||
|
Want to customize it even more?
|
||||||
|
|
||||||
|
- Removing table of contents: remove `Component.Explorer()` from `quartz.layout.ts`
|
||||||
|
- (optional): After removing the explorer component, you can move the [[table of contents | Table of Contents]] component back to the `left` part of the layout
|
||||||
|
- Changing `sort`, `filter` and `map` behavior: explained in [[#Advanced customization]]
|
||||||
|
- Component:
|
||||||
|
- Wrapper (Outer component, generates file tree, etc): `quartz/components/Explorer.tsx`
|
||||||
|
- Explorer node (recursive, either a folder or a file): `quartz/components/ExplorerNode.tsx`
|
||||||
|
- Style: `quartz/components/styles/explorer.scss`
|
||||||
|
- Script: `quartz/components/scripts/explorer.inline.ts`
|
||||||
|
|
||||||
|
## Advanced customization
|
||||||
|
|
||||||
|
This component allows you to fully customize all of its behavior. You can pass a custom `sort`, `filter` and `map` function.
|
||||||
|
All functions you can pass work with the `FileNode` class, which has the following properties:
|
||||||
|
|
||||||
|
```ts title="quartz/components/ExplorerNode.tsx" {2-5}
|
||||||
|
export class FileNode {
|
||||||
|
children: FileNode[] // children of current node
|
||||||
|
name: string // last part of slug
|
||||||
|
displayName: string // what actually should be displayed in the explorer
|
||||||
|
file: QuartzPluginData | null // set if node is a file, see `QuartzPluginData` for more detail
|
||||||
|
depth: number // depth of current node
|
||||||
|
|
||||||
|
... // rest of implementation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Every function you can pass is optional. By default, only a `sort` function will be used:
|
||||||
|
|
||||||
|
```ts title="Default sort function"
|
||||||
|
// Sort order: folders first, then files. Sort folders and files alphabetically
|
||||||
|
Component.Explorer({
|
||||||
|
sortFn: (a, b) => {
|
||||||
|
if ((!a.file && !b.file) || (a.file && b.file)) {
|
||||||
|
// sensitivity: "base": Only strings that differ in base letters compare as unequal. Examples: a ≠ b, a = á, a = A
|
||||||
|
// numeric: true: Whether numeric collation should be used, such that "1" < "2" < "10"
|
||||||
|
return a.displayName.localeCompare(b.displayName, undefined, {
|
||||||
|
numeric: true,
|
||||||
|
sensitivity: "base",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (a.file && !b.file) {
|
||||||
|
return 1
|
||||||
|
} else {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
You can pass your own functions for `sortFn`, `filterFn` and `mapFn`. All functions will be executed in the order provided by the `order` option (see [[#Customization]]). These functions behave similarly to their `Array.prototype` counterpart, except they modify the entire `FileNode` tree in place instead of returning a new one.
|
||||||
|
|
||||||
|
For more information on how to use `sort`, `filter` and `map`, you can check [Array.prototype.sort()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort), [Array.prototype.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) and [Array.prototype.map()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map).
|
||||||
|
|
||||||
|
Type definitions look like this:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
sortFn: (a: FileNode, b: FileNode) => number
|
||||||
|
filterFn: (node: FileNode) => boolean
|
||||||
|
mapFn: (node: FileNode) => void
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!tip]
|
||||||
|
> You can check if a `FileNode` is a folder or a file like this:
|
||||||
|
>
|
||||||
|
> ```ts
|
||||||
|
> if (node.file) {
|
||||||
|
> // node is a file
|
||||||
|
> } else {
|
||||||
|
> // node is a folder
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
|
||||||
|
## Basic examples
|
||||||
|
|
||||||
|
These examples show the basic usage of `sort`, `map` and `filter`.
|
||||||
|
|
||||||
|
### Use `sort` to put files first
|
||||||
|
|
||||||
|
Using this example, the explorer will alphabetically sort everything, but put all **files** above all **folders**.
|
||||||
|
|
||||||
|
```ts title="quartz.layout.ts"
|
||||||
|
Component.Explorer({
|
||||||
|
sortFn: (a, b) => {
|
||||||
|
if ((!a.file && !b.file) || (a.file && b.file)) {
|
||||||
|
return a.displayName.localeCompare(b.displayName)
|
||||||
|
}
|
||||||
|
if (a.file && !b.file) {
|
||||||
|
return -1
|
||||||
|
} else {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Change display names (`map`)
|
||||||
|
|
||||||
|
Using this example, the display names of all `FileNodes` (folders + files) will be converted to full upper case.
|
||||||
|
|
||||||
|
```ts title="quartz.layout.ts"
|
||||||
|
Component.Explorer({
|
||||||
|
mapFn: (node) => {
|
||||||
|
node.displayName = node.displayName.toUpperCase()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remove list of elements (`filter`)
|
||||||
|
|
||||||
|
Using this example, you can remove elements from your explorer by providing an array of folders/files using the `omit` set.
|
||||||
|
|
||||||
|
```ts title="quartz.layout.ts"
|
||||||
|
Component.Explorer({
|
||||||
|
filterFn: (node) => {
|
||||||
|
// set containing names of everything you want to filter out
|
||||||
|
const omit = new Set(["authoring content", "tags", "hosting"])
|
||||||
|
return !omit.has(node.name.toLowerCase())
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can customize this by changing the entries of the `omit` set. Simply add all folder or file names you want to remove.
|
||||||
|
|
||||||
|
### Show every element in explorer
|
||||||
|
|
||||||
|
To override the default filter function that removes the `tags` folder from the explorer, you can set the filter function to `undefined`.
|
||||||
|
|
||||||
|
```ts title="quartz.layout.ts"
|
||||||
|
Component.Explorer({
|
||||||
|
filterFn: undefined, // apply no filter function, every file and folder will visible
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced examples
|
||||||
|
|
||||||
|
### Add emoji prefix
|
||||||
|
|
||||||
|
To add emoji prefixes (📁 for folders, 📄 for files), you could use a map function like this:
|
||||||
|
|
||||||
|
```ts title="quartz.layout.ts"
|
||||||
|
Component.Explorer({
|
||||||
|
mapFn: (node) => {
|
||||||
|
// dont change name of root node
|
||||||
|
if (node.depth > 0) {
|
||||||
|
// set emoji for file/folder
|
||||||
|
if (node.file) {
|
||||||
|
node.displayName = "📄 " + node.displayName
|
||||||
|
} else {
|
||||||
|
node.displayName = "📁 " + node.displayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Putting it all together
|
||||||
|
|
||||||
|
In this example, we're going to customize the explorer by using functions from examples above to [[#Add emoji prefix | add emoji prefixes]], [[#remove-list-of-elements-filter| filter out some folders]] and [[#use-sort-to-put-files-first | sort with files above folders]].
|
||||||
|
|
||||||
|
```ts title="quartz.layout.ts"
|
||||||
|
Component.Explorer({
|
||||||
|
filterFn: sampleFilterFn,
|
||||||
|
mapFn: sampleMapFn,
|
||||||
|
sortFn: sampleSortFn,
|
||||||
|
order: ["filter", "sort", "map"],
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice how we customized the `order` array here. This is done because the default order applies the `sort` function last. While this normally works well, it would cause unintended behavior here, since we changed the first characters of all display names. In our example, `sort` would be applied based off the emoji prefix instead of the first _real_ character.
|
||||||
|
|
||||||
|
To fix this, we just changed around the order and apply the `sort` function before changing the display names in the `map` function.
|
||||||
|
|
||||||
|
> [!tip]
|
||||||
|
> When writing more complicated functions, the `layout` file can start to look very cramped.
|
||||||
|
> You can fix this by defining your functions in another file.
|
||||||
|
>
|
||||||
|
> ```ts title="functions.ts"
|
||||||
|
> import { Options } from "./quartz/components/ExplorerNode"
|
||||||
|
> export const mapFn: Options["mapFn"] = (node) => {
|
||||||
|
> // implement your function here
|
||||||
|
> }
|
||||||
|
> export const filterFn: Options["filterFn"] = (node) => {
|
||||||
|
> // implement your function here
|
||||||
|
> }
|
||||||
|
> export const sortFn: Options["sortFn"] = (a, b) => {
|
||||||
|
> // implement your function here
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> You can then import them like this:
|
||||||
|
>
|
||||||
|
> ```ts title="quartz.layout.ts"
|
||||||
|
> import { mapFn, filterFn, sortFn } from "./functions.ts"
|
||||||
|
> Component.Explorer({
|
||||||
|
> mapFn: mapFn,
|
||||||
|
> filterFn: filterFn,
|
||||||
|
> sortFn: sortFn,
|
||||||
|
> })
|
||||||
|
> ```
|
||||||
@@ -6,9 +6,11 @@ tags:
|
|||||||
|
|
||||||
Full-text search in Quartz is powered by [Flexsearch](https://github.com/nextapps-de/flexsearch). It's fast enough to return search results in under 10ms for Quartzs as large as half a million words.
|
Full-text search in Quartz is powered by [Flexsearch](https://github.com/nextapps-de/flexsearch). It's fast enough to return search results in under 10ms for Quartzs as large as half a million words.
|
||||||
|
|
||||||
It can be opened by either clicking on the search bar or pressing ⌘+K. The top 5 search results are shown on each query. Matching subterms are highlighted and the most relevant 30 words are excerpted. Clicking on a search result will navigate to that page.
|
It can be opened by either clicking on the search bar or pressing `⌘`/`ctrl` + `K`. The top 5 search results are shown on each query. Matching subterms are highlighted and the most relevant 30 words are excerpted. Clicking on a search result will navigate to that page.
|
||||||
|
|
||||||
This component is also keyboard accessible: Tab and Shift+Tab will cycle forward and backward through search results and Enter will navigate to the highlighted result (first result by default).
|
To search content by tags, you can either press `⌘`/`ctrl` + `shift` + `K` or start your query with `#` (e.g. `#components`).
|
||||||
|
|
||||||
|
This component is also keyboard accessible: Tab and Shift+Tab will cycle forward and backward through search results and Enter will navigate to the highlighted result (first result by default). You are also able to navigate search results using `ArrowUp` and `ArrowDown`.
|
||||||
|
|
||||||
> [!info]
|
> [!info]
|
||||||
> Search requires the `ContentIndex` emitter plugin to be present in the [[configuration]].
|
> Search requires the `ContentIndex` emitter plugin to be present in the [[configuration]].
|
||||||
@@ -17,7 +19,7 @@ This component is also keyboard accessible: Tab and Shift+Tab will cycle forward
|
|||||||
|
|
||||||
By default, it indexes every page on the site with **Markdown syntax removed**. This means link URLs for instance are not indexed.
|
By default, it indexes every page on the site with **Markdown syntax removed**. This means link URLs for instance are not indexed.
|
||||||
|
|
||||||
It properly tokenizes Chinese, Korean, and Japenese characters and constructs separate indexes for the title and content, weighing title matches above content matches.
|
It properly tokenizes Chinese, Korean, and Japenese characters and constructs separate indexes for the title, content and tags, weighing title matches above content matches.
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
|
|
||||||
@@ -25,4 +27,4 @@ It properly tokenizes Chinese, Korean, and Japenese characters and constructs se
|
|||||||
- Component: `quartz/components/Search.tsx`
|
- Component: `quartz/components/Search.tsx`
|
||||||
- Style: `quartz/components/styles/search.scss`
|
- Style: `quartz/components/styles/search.scss`
|
||||||
- Script: `quartz/components/scripts/search.inline.ts`
|
- Script: `quartz/components/scripts/search.inline.ts`
|
||||||
- You can edit `contextWindowWords` or `numSearchResults` to suit your needs
|
- You can edit `contextWindowWords`, `numSearchResults` or `numTagResults` to suit your needs
|
||||||
@@ -8,13 +8,21 @@ There may be some notes you want to avoid publishing as a website. Quartz suppor
|
|||||||
|
|
||||||
## Filter Plugins
|
## Filter Plugins
|
||||||
|
|
||||||
[[making plugins#Filters|Filter plugins]] are plugins that filter out content based off of certain criteria. By default, Quartz uses the `Plugin.RemoveDrafts` plugin which filters out any note that has `drafts: true` in the frontmatter.
|
[[making plugins#Filters|Filter plugins]] are plugins that filter out content based off of certain criteria. By default, Quartz uses the `Plugin.RemoveDrafts` plugin which filters out any note that has `draft: true` in the frontmatter.
|
||||||
|
|
||||||
If you'd like to only publish a select number of notes, you can instead use `Plugin.ExplicitPublish` which will filter out all notes except for any that have `publish: true` in the frontmatter.
|
If you'd like to only publish a select number of notes, you can instead use `Plugin.ExplicitPublish` which will filter out all notes except for any that have `publish: true` in the frontmatter.
|
||||||
|
|
||||||
## `ignoreFiles`
|
> [!warning]
|
||||||
|
> Regardless of the filter plugin used, **all non-markdown files will be emitted and available publically in the final build.** This includes files such as images, voice recordings, PDFs, etc. One way to prevent this and still be able to embed local images is to create a folder specifically for public media and add the following two patterns to the ignorePatterns array.
|
||||||
|
>
|
||||||
|
> `"!(PublicMedia)**/!(*.md)", "!(*.md)"`
|
||||||
|
|
||||||
This is a field in `quartz.config.ts` under the main [[configuration]] which allows you to specify a list of patterns to effectively exclude from parsing all together. Any valid [glob](https://github.com/mrmlnc/fast-glob#pattern-syntax) pattern works here.
|
## `ignorePatterns`
|
||||||
|
|
||||||
|
This is a field in `quartz.config.ts` under the main [[configuration]] which allows you to specify a list of patterns to effectively exclude from parsing all together. Any valid [fast-glob](https://github.com/mrmlnc/fast-glob#pattern-syntax) pattern works here.
|
||||||
|
|
||||||
|
> [!note]
|
||||||
|
> Bash's glob syntax is slightly different from fast-glob's and using bash's syntax may lead to unexpected results.
|
||||||
|
|
||||||
Common examples include:
|
Common examples include:
|
||||||
|
|
||||||
@@ -24,4 +32,4 @@ Common examples include:
|
|||||||
- `**/private`: exclude any files or folders named `private` at any level of nesting
|
- `**/private`: exclude any files or folders named `private` at any level of nesting
|
||||||
|
|
||||||
> [!warning]
|
> [!warning]
|
||||||
> Marking something as private via either a plugin or through the `ignoreFiles` pattern will only prevent a page from being included in the final built site. If your GitHub repository is public, also be sure to include an ignore for those in the `.gitignore` of your Quartz. See the `git` [documentation](https://git-scm.com/docs/gitignore#_pattern_format) for more information.
|
> Marking something as private via either a plugin or through the `ignorePatterns` pattern will only prevent a page from being included in the final built site. If your GitHub repository is public, also be sure to include an ignore for those in the `.gitignore` of your Quartz. See the `git` [documentation](https://git-scm.com/docs/gitignore#_pattern_format) for more information.
|
||||||
@@ -8,6 +8,7 @@ tags:
|
|||||||
Quartz can automatically generate a table of contents from a list of headings on each page. It will also show you your current scroll position on the site by marking headings you've scrolled through with a different colour.
|
Quartz can automatically generate a table of contents from a list of headings on each page. It will also show you your current scroll position on the site by marking headings you've scrolled through with a different colour.
|
||||||
|
|
||||||
By default, it will show all headers from H1 (`# Title`) all the way to H3 (`### Title`) and will only show the table of contents if there is more than 1 header on the page.
|
By default, it will show all headers from H1 (`# Title`) all the way to H3 (`### Title`) and will only show the table of contents if there is more than 1 header on the page.
|
||||||
|
You can also hide the table of contents on a page by adding `enableToc: false` to the frontmatter for that page.
|
||||||
|
|
||||||
> [!info]
|
> [!info]
|
||||||
> This feature requires both `Plugin.TableOfContents` in your `quartz.config.ts` and `Component.TableOfContents` in your `quartz.layout.ts` to function correctly.
|
> This feature requires both `Plugin.TableOfContents` in your `quartz.config.ts` and `Component.TableOfContents` in your `quartz.layout.ts` to function correctly.
|
||||||
@@ -17,6 +18,7 @@ By default, it will show all headers from H1 (`# Title`) all the way to H3 (`###
|
|||||||
- Removing table of contents: remove all instances of `Plugin.TableOfContents()` from `quartz.config.ts`. and `Component.TableOfContents()` from `quartz.layout.ts`
|
- Removing table of contents: remove all instances of `Plugin.TableOfContents()` from `quartz.config.ts`. and `Component.TableOfContents()` from `quartz.layout.ts`
|
||||||
- Changing the max depth: pass in a parameter to `Plugin.TableOfContents({ maxDepth: 4 })`
|
- Changing the max depth: pass in a parameter to `Plugin.TableOfContents({ maxDepth: 4 })`
|
||||||
- Changing the minimum number of entries in the Table of Contents before it renders: pass in a parameter to `Plugin.TableOfContents({ minEntries: 3 })`
|
- Changing the minimum number of entries in the Table of Contents before it renders: pass in a parameter to `Plugin.TableOfContents({ minEntries: 3 })`
|
||||||
|
- Collapse the table of content by default: pass in a parameter to `Plugin.TableOfContents({ collapseByDefault: true })`
|
||||||
- Component: `quartz/components/TableOfContents.tsx`
|
- Component: `quartz/components/TableOfContents.tsx`
|
||||||
- Style:
|
- Style:
|
||||||
- Modern (default): `quartz/components/styles/toc.scss`
|
- Modern (default): `quartz/components/styles/toc.scss`
|
||||||
@@ -4,15 +4,14 @@ draft: true
|
|||||||
|
|
||||||
## high priority backlog
|
## high priority backlog
|
||||||
|
|
||||||
|
- static dead link detection
|
||||||
- block links: https://help.obsidian.md/Linking+notes+and+files/Internal+links#Link+to+a+block+in+a+note
|
- block links: https://help.obsidian.md/Linking+notes+and+files/Internal+links#Link+to+a+block+in+a+note
|
||||||
- note/header/block transcludes: https://help.obsidian.md/Linking+notes+and+files/Embedding+files
|
- note/header/block transcludes: https://help.obsidian.md/Linking+notes+and+files/Embedding+files
|
||||||
- static dead link detection
|
|
||||||
- docker support
|
- docker support
|
||||||
|
|
||||||
## misc backlog
|
## misc backlog
|
||||||
|
|
||||||
- breadcrumbs component
|
- breadcrumbs component
|
||||||
- filetree component
|
|
||||||
- cursor chat extension
|
- cursor chat extension
|
||||||
- https://giscus.app/ extension
|
- https://giscus.app/ extension
|
||||||
- sidenotes? https://github.com/capnfabs/paperesque
|
- sidenotes? https://github.com/capnfabs/paperesque
|
||||||
@@ -10,9 +10,7 @@ This is enabled as a part of [[Obsidian compatibility]] and can be configured an
|
|||||||
|
|
||||||
## Syntax
|
## Syntax
|
||||||
|
|
||||||
- `[[Path to file]]`: produces a link to `Path to file` with the text `Path to file`
|
- `[[Path to file]]`: produces a link to `Path to file.md` (or `Path-to-file.md`) with the text `Path to file`
|
||||||
- `[[Path to file | Here's the title override]]`: produces a link to `Path to file` with the text `Here's the title override`
|
- `[[Path to file | Here's the title override]]`: produces a link to `Path to file.md` with the text `Here's the title override`
|
||||||
- `[[Path to file#Anchor]]`: produces a link to the anchor `Anchor` in the file `Path to file`
|
- `[[Path to file#Anchor]]`: produces a link to the anchor `Anchor` in the file `Path to file.md`
|
||||||
|
- `[[Path to file#^block-ref]]`: produces a link to the specific block `block-ref` in the file `Path to file.md`
|
||||||
> [!warning]
|
|
||||||
> Currently, Quartz does not support block references or note embed syntax.
|
|
||||||
@@ -30,6 +30,9 @@ To add a custom domain, check out [Cloudflare's documentation](https://developer
|
|||||||
|
|
||||||
Like Quartz 3, you can deploy the site generated by Quartz 4 via GitHub Pages.
|
Like Quartz 3, you can deploy the site generated by Quartz 4 via GitHub Pages.
|
||||||
|
|
||||||
|
> [!warning]
|
||||||
|
> Quartz generates files in the format of `file.html` instead of `file/index.html` which means the trailing slashes for _non-folder paths_ are dropped. As GitHub pages does not do this redirect, this may cause existing links to your site that use trailing slashes to break. If not breaking existing links is important to you, consider using [[#Cloudflare Pages]].
|
||||||
|
|
||||||
In your local Quartz, create a new file `quartz/.github/workflows/deploy.yml`.
|
In your local Quartz, create a new file `quartz/.github/workflows/deploy.yml`.
|
||||||
|
|
||||||
```yaml title="quartz/.github/workflows/deploy.yml"
|
```yaml title="quartz/.github/workflows/deploy.yml"
|
||||||
@@ -113,3 +116,117 @@ See the [GitHub documentation](https://docs.github.com/en/pages/configuring-a-cu
|
|||||||
> There could be many different reasons why your changes aren't showing up but the most likely reason is that you forgot to push your changes to GitHub.
|
> There could be many different reasons why your changes aren't showing up but the most likely reason is that you forgot to push your changes to GitHub.
|
||||||
>
|
>
|
||||||
> Make sure you save your changes to Git and sync it to GitHub by doing `npx quartz sync`. This will also make sure to pull any updates you may have made from other devices so you have them locally.
|
> Make sure you save your changes to Git and sync it to GitHub by doing `npx quartz sync`. This will also make sure to pull any updates you may have made from other devices so you have them locally.
|
||||||
|
|
||||||
|
## Vercel
|
||||||
|
|
||||||
|
### Fix URLs
|
||||||
|
|
||||||
|
Before deploying to Vercel, a `vercel.json` file is required at the root of the project directory. It needs to contain the following configuration so that URLs don't require the `.html` extension:
|
||||||
|
|
||||||
|
```json title="vercel.json"
|
||||||
|
{
|
||||||
|
"cleanUrls": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy to Vercel
|
||||||
|
|
||||||
|
1. Log in to the [Vercel Dashboard](https://vercel.com/dashboard) and click "Add New..." > Project
|
||||||
|
2. Import the Git repository containing your Quartz project.
|
||||||
|
3. Give the project a name (lowercase characters and hyphens only)
|
||||||
|
4. Check that these configuration options are set:
|
||||||
|
|
||||||
|
| Configuration option | Value |
|
||||||
|
| ----------------------------------------- | ------------------ |
|
||||||
|
| Framework Preset | `Other` |
|
||||||
|
| Root Directory | `./` |
|
||||||
|
| Build and Output Settings > Build Command | `npx quartz build` |
|
||||||
|
|
||||||
|
5. Press Deploy. Once it's live, you'll have 2 `*.vercel.app` URLs to view the page.
|
||||||
|
|
||||||
|
### Custom Domain
|
||||||
|
|
||||||
|
> [!note]
|
||||||
|
> If there is something already hosted on the domain, these steps will not work without replacing the previous content. As a workaround, you could use Next.js rewrites or use the next section to create a subdomain.
|
||||||
|
|
||||||
|
1. Update the `baseUrl` in `quartz.config.js` if necessary.
|
||||||
|
2. Go to the [Domains - Dashboard](https://vercel.com/dashboard/domains) page in Vercel.
|
||||||
|
3. Connect the domain to Vercel
|
||||||
|
4. Press "Add" to connect a custom domain to Vercel.
|
||||||
|
5. Select your Quartz repository and press Continue.
|
||||||
|
6. Enter the domain you want to connect it to.
|
||||||
|
7. Follow the instructions to update your DNS records until you see "Valid Configuration"
|
||||||
|
|
||||||
|
### Use a Subdomain
|
||||||
|
|
||||||
|
Using `docs.example.com` is an example of a subdomain. They're a simple way of connecting multiple deployments to one domain.
|
||||||
|
|
||||||
|
1. Update the `baseUrl` in `quartz.config.js` if necessary.
|
||||||
|
2. Ensure your domain has been added to the [Domains - Dashboard](https://vercel.com/dashboard/domains) page in Vercel.
|
||||||
|
3. Go to the [Vercel Dashboard](https://vercel.com/dashboard) and select your Quartz project.
|
||||||
|
4. Go to the Settings tab and then click Domains in the sidebar
|
||||||
|
5. Enter your subdomain into the field and press Add
|
||||||
|
|
||||||
|
## Netlify
|
||||||
|
|
||||||
|
Like Vercel, you can also deploy the site generated by Quartz 4 via Netlify.
|
||||||
|
|
||||||
|
1. Log in to the [Netlify dashboard](https://app.netlify.com/) and click "Add new site".
|
||||||
|
2. Select your Git provider and repository containing your Quartz project.
|
||||||
|
3. Under "Build command", enter `npx quartz build`.
|
||||||
|
4. Under "Publish directory", enter `public`.
|
||||||
|
5. Press Deploy. Once it's live, you'll have a `*.netlify.app` URL to view the page.
|
||||||
|
6. To add a custom domain, check "Domain management" in the left sidebar, just like with Vercel.
|
||||||
|
|
||||||
|
## GitLab Pages
|
||||||
|
|
||||||
|
You can configure GitLab CI to build and deploy a Quartz 4 project.
|
||||||
|
|
||||||
|
In your local Quartz, create a new file `.gitlab-ci.yaml`.
|
||||||
|
|
||||||
|
```yaml title=".gitlab-ci.yaml"
|
||||||
|
stages:
|
||||||
|
- build
|
||||||
|
- deploy
|
||||||
|
|
||||||
|
variables:
|
||||||
|
NODE_VERSION: "18.14"
|
||||||
|
|
||||||
|
build:
|
||||||
|
stage: build
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_REF_NAME == "v4"'
|
||||||
|
before_script:
|
||||||
|
- apt-get update -q && apt-get install -y nodejs npm
|
||||||
|
- npm install -g n
|
||||||
|
- n $NODE_VERSION
|
||||||
|
- hash -r
|
||||||
|
- npm ci
|
||||||
|
script:
|
||||||
|
- npx prettier --write .
|
||||||
|
- npm run check
|
||||||
|
- npx quartz build
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- public
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- ~/.npm/
|
||||||
|
key: "${CI_COMMIT_REF_SLUG}-node-${CI_COMMIT_REF_NAME}"
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
|
||||||
|
pages:
|
||||||
|
stage: deploy
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_REF_NAME == "v4"'
|
||||||
|
script:
|
||||||
|
- echo "Deploying to GitLab Pages..."
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- public
|
||||||
|
```
|
||||||
|
|
||||||
|
When `.gitlab-ci.yaml` is commited, GitLab will build and deploy the website as a GitLab Page. You can find the url under `Deploy` -> `Pages` in the sidebar.
|
||||||
|
|
||||||
|
By default, the page is private and only visible when logged in to a GitLab account with access to the repository but can be opened in the settings under `Deploy` -> `Pages`.
|
||||||
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
@@ -2,18 +2,17 @@
|
|||||||
title: Welcome to Quartz 4
|
title: Welcome to Quartz 4
|
||||||
---
|
---
|
||||||
|
|
||||||
Quartz is a fast, batteries-included static-site generator that transforms Markdown content into fully functional websites. Thousands of students, developers, and teachers are [[showcase|already using Quartz]] to publish personal notes, wikis, and [digital gardens](https://jzhao.xyz/posts/networked-thought/) to the web.
|
Quartz is a fast, batteries-included static-site generator that transforms Markdown content into fully functional websites. Thousands of students, developers, and teachers are [[showcase|already using Quartz]] to publish personal notes, wikis, and [digital gardens](https://jzhao.xyz/posts/networked-thought) to the web.
|
||||||
|
|
||||||
## 🪴 Get Started
|
## 🪴 Get Started
|
||||||
|
|
||||||
Quartz requires **at least [Node](https://nodejs.org/) v18.14** to function correctly. Ensure you have this installed on your machine before continuing.
|
Quartz requires **at least [Node](https://nodejs.org/) v18.14** and `npm` v9.3.1 to function correctly. Ensure you have this installed on your machine before continuing.
|
||||||
|
|
||||||
Then, in your terminal of choice, enter the following commands line by line:
|
Then, in your terminal of choice, enter the following commands line by line:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
git clone https://github.com/jackyzha0/quartz.git
|
git clone https://github.com/jackyzha0/quartz.git
|
||||||
cd quartz
|
cd quartz
|
||||||
git checkout v4
|
|
||||||
npm i
|
npm i
|
||||||
npx quartz create
|
npx quartz create
|
||||||
```
|
```
|
||||||
@@ -31,7 +30,7 @@ This will guide you through initializing your Quartz with content. Once you've d
|
|||||||
|
|
||||||
## 🔧 Features
|
## 🔧 Features
|
||||||
|
|
||||||
- [[Obsidian compatibility]], [[full-text search]], [[graph view]], [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], and [many more](./features) right out of the box
|
- [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], [[Docker Support]], and [many more](./features) right out of the box
|
||||||
- Hot-reload for both configuration and content
|
- Hot-reload for both configuration and content
|
||||||
- Simple JSX layouts and [[creating components|page components]]
|
- Simple JSX layouts and [[creating components|page components]]
|
||||||
- [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes
|
- [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes
|
||||||
@@ -30,7 +30,7 @@ These correspond to following parts of the page:
|
|||||||
|
|
||||||
Quartz **components**, like plugins, can take in additional properties as configuration options. If you're familiar with React terminology, you can think of them as Higher-order Components.
|
Quartz **components**, like plugins, can take in additional properties as configuration options. If you're familiar with React terminology, you can think of them as Higher-order Components.
|
||||||
|
|
||||||
See [a list of all the components](./tags/component) 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.
|
||||||
|
|
||||||
### Style
|
### Style
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ Quartz embraces the inherent rhizomatic and web-like nature of our thinking and
|
|||||||
|
|
||||||
The goal of digital gardening should be to tap into your network’s collective intelligence to create constructive feedback loops. If done well, I have a shareable representation of my thoughts that I can send out into the world and people can respond. Even for my most half-baked thoughts, this helps me create a feedback cycle to strengthen and fully flesh out that idea.
|
The goal of digital gardening should be to tap into your network’s collective intelligence to create constructive feedback loops. If done well, I have a shareable representation of my thoughts that I can send out into the world and people can respond. Even for my most half-baked thoughts, this helps me create a feedback cycle to strengthen and fully flesh out that idea.
|
||||||
|
|
||||||
Quartz is designed first and foremost as a tool for publishing [digital gardens](https://jzhao.xyz/posts/networked-thought/) to the web. To me, digital gardening is not just passive knowledge collection. It’s a form of expression and sharing.
|
Quartz is designed first and foremost as a tool for publishing [digital gardens](https://jzhao.xyz/posts/networked-thought) to the web. To me, digital gardening is not just passive knowledge collection. It’s a form of expression and sharing.
|
||||||
|
|
||||||
> “[One] who works with the door open gets all kinds of interruptions, but [they] also occasionally gets clues as to what the world is and what might be important.”
|
> “[One] who works with the door open gets all kinds of interruptions, but [they] also occasionally gets clues as to what the world is and what might be important.”
|
||||||
> — Richard Hamming
|
> — Richard Hamming
|
||||||
@@ -9,12 +9,17 @@ Want to see what Quartz can do? Here are some cool community gardens:
|
|||||||
- [Brandon Boswell's Garden](https://brandonkboswell.com)
|
- [Brandon Boswell's Garden](https://brandonkboswell.com)
|
||||||
- [Scaling Synthesis - A hypertext research notebook](https://scalingsynthesis.com/)
|
- [Scaling Synthesis - A hypertext research notebook](https://scalingsynthesis.com/)
|
||||||
- [AWAGMI Intern Notes](https://notes.awagmi.xyz/)
|
- [AWAGMI Intern Notes](https://notes.awagmi.xyz/)
|
||||||
- [Course notes for Information Technology Advanced Theory](https://a2itnotes.github.io/quartz/)
|
|
||||||
- [Data Dictionary 🧠](https://glossary.airbyte.com/)
|
- [Data Dictionary 🧠](https://glossary.airbyte.com/)
|
||||||
- [sspaeti.com's Second Brain](https://brain.sspaeti.com/)
|
- [sspaeti.com's Second Brain](https://brain.sspaeti.com/)
|
||||||
- [oldwinterの数字花园](https://garden.oldwinter.top/)
|
- [oldwinter の数字花园](https://garden.oldwinter.top/)
|
||||||
- [Abhijeet's Math Wiki](https://abhmul.github.io/quartz/Math-Wiki/)
|
- [Abhijeet's Math Wiki](https://abhmul.github.io/quartz/Math-Wiki/)
|
||||||
- [Mike's AI Garden 🤖🪴](https://mwalton.me/)
|
- [Mike's AI Garden 🤖🪴](https://mwalton.me/)
|
||||||
- [Matt Dunn's Second Brain](https://mattdunn.info/)
|
- [Matt Dunn's Second Brain](https://mattdunn.info/)
|
||||||
|
- [Pelayo Arbues' Notes](https://pelayoarbues.github.io/)
|
||||||
|
- [Vince Imbat's Talahardin](https://vinceimbat.com/)
|
||||||
|
- [🧠🌳 Chad's Mind Garden](https://www.chadly.net/)
|
||||||
|
- [Pedro MC Fernandes's Topo da Mente](https://www.pmcf.xyz/topo-da-mente/)
|
||||||
|
- [Mau Camargo's Notkesto](https://notes.camargomau.com/)
|
||||||
|
- [Caicai's Novels](https://imoko.cc/blog/caicai/)
|
||||||
|
|
||||||
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/content/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)!
|
||||||
441
package-lock.json
generated
441
package-lock.json
generated
@@ -1,23 +1,23 @@
|
|||||||
{
|
{
|
||||||
"name": "@jackyzha0/quartz",
|
"name": "@jackyzha0/quartz",
|
||||||
"version": "4.0.8",
|
"version": "4.1.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@jackyzha0/quartz",
|
"name": "@jackyzha0/quartz",
|
||||||
"version": "4.0.8",
|
"version": "4.1.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.6.3",
|
"@clack/prompts": "^0.6.3",
|
||||||
"@floating-ui/dom": "^1.4.0",
|
"@floating-ui/dom": "^1.4.0",
|
||||||
"@napi-rs/simple-git": "^0.1.8",
|
"@napi-rs/simple-git": "0.1.9",
|
||||||
"async-mutex": "^0.4.0",
|
"async-mutex": "^0.4.0",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"cli-spinner": "^0.2.10",
|
"cli-spinner": "^0.2.10",
|
||||||
"d3": "^7.8.5",
|
"d3": "^7.8.5",
|
||||||
"esbuild-sass-plugin": "^2.9.0",
|
"esbuild-sass-plugin": "^2.12.0",
|
||||||
"flexsearch": "0.7.21",
|
"flexsearch": "0.7.21",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"globby": "^13.1.4",
|
"globby": "^13.1.4",
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"hast-util-to-string": "^2.0.0",
|
"hast-util-to-string": "^2.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.21.5",
|
"lightningcss": "1.21.7",
|
||||||
"mdast-util-find-and-replace": "^2.2.2",
|
"mdast-util-find-and-replace": "^2.2.2",
|
||||||
"mdast-util-to-hast": "^12.3.0",
|
"mdast-util-to-hast": "^12.3.0",
|
||||||
"mdast-util-to-string": "^3.2.0",
|
"mdast-util-to-string": "^3.2.0",
|
||||||
@@ -45,6 +45,7 @@
|
|||||||
"rehype-raw": "^6.1.1",
|
"rehype-raw": "^6.1.1",
|
||||||
"rehype-slug": "^5.1.0",
|
"rehype-slug": "^5.1.0",
|
||||||
"remark": "^14.0.2",
|
"remark": "^14.0.2",
|
||||||
|
"remark-breaks": "^3.0.3",
|
||||||
"remark-frontmatter": "^4.0.1",
|
"remark-frontmatter": "^4.0.1",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-math": "^5.1.1",
|
"remark-math": "^5.1.1",
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
"serve-handler": "^6.1.5",
|
"serve-handler": "^6.1.5",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"to-vfile": "^7.2.4",
|
"to-vfile": "^7.2.4",
|
||||||
|
"toml": "^3.0.0",
|
||||||
"unified": "^10.1.2",
|
"unified": "^10.1.2",
|
||||||
"unist-util-visit": "^4.1.2",
|
"unist-util-visit": "^4.1.2",
|
||||||
"vfile": "^5.3.7",
|
"vfile": "^5.3.7",
|
||||||
@@ -77,13 +79,14 @@
|
|||||||
"@types/workerpool": "^6.4.0",
|
"@types/workerpool": "^6.4.0",
|
||||||
"@types/ws": "^8.5.5",
|
"@types/ws": "^8.5.5",
|
||||||
"@types/yargs": "^17.0.24",
|
"@types/yargs": "^17.0.24",
|
||||||
"esbuild": "^0.18.11",
|
"esbuild": "0.19.2",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"tsx": "^3.12.7",
|
"tsx": "^3.12.7",
|
||||||
"typescript": "^5.0.4"
|
"typescript": "^5.0.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.14"
|
"node": ">=18.14",
|
||||||
|
"npm": ">=9.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@clack/core": {
|
"node_modules/@clack/core": {
|
||||||
@@ -540,9 +543,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm": {
|
"node_modules/@esbuild/android-arm": {
|
||||||
"version": "0.18.11",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.2.tgz",
|
||||||
"integrity": "sha512-q4qlUf5ucwbUJZXF5tEQ8LF7y0Nk4P58hOsGk3ucY0oCwgQqAnqXVbUuahCddVHfrxmpyewRpiTHwVHIETYu7Q==",
|
"integrity": "sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -555,9 +558,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm64": {
|
"node_modules/@esbuild/android-arm64": {
|
||||||
"version": "0.18.11",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.2.tgz",
|
||||||
"integrity": "sha512-snieiq75Z1z5LJX9cduSAjUr7vEI1OdlzFPMw0HH5YI7qQHDd3qs+WZoMrWYDsfRJSq36lIA6mfZBkvL46KoIw==",
|
"integrity": "sha512-lsB65vAbe90I/Qe10OjkmrdxSX4UJDjosDgb8sZUKcg3oefEuW2OT2Vozz8ef7wrJbMcmhvCC+hciF8jY/uAkw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -570,9 +573,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-x64": {
|
"node_modules/@esbuild/android-x64": {
|
||||||
"version": "0.18.11",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.2.tgz",
|
||||||
"integrity": "sha512-iPuoxQEV34+hTF6FT7om+Qwziv1U519lEOvekXO9zaMMlT9+XneAhKL32DW3H7okrCOBQ44BMihE8dclbZtTuw==",
|
"integrity": "sha512-qK/TpmHt2M/Hg82WXHRc/W/2SGo/l1thtDHZWqFq7oi24AjZ4O/CpPSu6ZuYKFkEgmZlFoa7CooAyYmuvnaG8w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -585,9 +588,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
"version": "0.18.11",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.2.tgz",
|
||||||
"integrity": "sha512-Gm0QkI3k402OpfMKyQEEMG0RuW2LQsSmI6OeO4El2ojJMoF5NLYb3qMIjvbG/lbMeLOGiW6ooU8xqc+S0fgz2w==",
|
"integrity": "sha512-Ora8JokrvrzEPEpZO18ZYXkH4asCdc1DLdcVy8TGf5eWtPO1Ie4WroEJzwI52ZGtpODy3+m0a2yEX9l+KUn0tA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -600,9 +603,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
"version": "0.18.11",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.2.tgz",
|
||||||
"integrity": "sha512-N15Vzy0YNHu6cfyDOjiyfJlRJCB/ngKOAvoBf1qybG3eOq0SL2Lutzz9N7DYUbb7Q23XtHPn6lMDF6uWbGv9Fw==",
|
"integrity": "sha512-tP+B5UuIbbFMj2hQaUr6EALlHOIOmlLM2FK7jeFBobPy2ERdohI4Ka6ZFjZ1ZYsrHE/hZimGuU90jusRE0pwDw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -615,9 +618,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
"version": "0.18.11",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.2.tgz",
|
||||||
"integrity": "sha512-atEyuq6a3omEY5qAh5jIORWk8MzFnCpSTUruBgeyN9jZq1K/QI9uke0ATi3MHu4L8c59CnIi4+1jDKMuqmR71A==",
|
"integrity": "sha512-YbPY2kc0acfzL1VPVK6EnAlig4f+l8xmq36OZkU0jzBVHcOTyQDhnKQaLzZudNJQyymd9OqQezeaBgkTGdTGeQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -630,9 +633,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
"version": "0.18.11",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.2.tgz",
|
||||||
"integrity": "sha512-XtuPrEfBj/YYYnAAB7KcorzzpGTvOr/dTtXPGesRfmflqhA4LMF0Gh/n5+a9JBzPuJ+CGk17CA++Hmr1F/gI0Q==",
|
"integrity": "sha512-nSO5uZT2clM6hosjWHAsS15hLrwCvIWx+b2e3lZ3MwbYSaXwvfO528OF+dLjas1g3bZonciivI8qKR/Hm7IWGw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -645,9 +648,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm": {
|
"node_modules/@esbuild/linux-arm": {
|
||||||
"version": "0.18.11",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.2.tgz",
|
||||||
"integrity": "sha512-Idipz+Taso/toi2ETugShXjQ3S59b6m62KmLHkJlSq/cBejixmIydqrtM2XTvNCywFl3VC7SreSf6NV0i6sRyg==",
|
"integrity": "sha512-Odalh8hICg7SOD7XCj0YLpYCEc+6mkoq63UnExDCiRA2wXEmGlK5JVrW50vZR9Qz4qkvqnHcpH+OFEggO3PgTg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -660,9 +663,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
"version": "0.18.11",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.2.tgz",
|
||||||
"integrity": "sha512-c6Vh2WS9VFKxKZ2TvJdA7gdy0n6eSy+yunBvv4aqNCEhSWVor1TU43wNRp2YLO9Vng2G+W94aRz+ILDSwAiYog==",
|
"integrity": "sha512-ig2P7GeG//zWlU0AggA3pV1h5gdix0MA3wgB+NsnBXViwiGgY77fuN9Wr5uoCrs2YzaYfogXgsWZbm+HGr09xg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -675,9 +678,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
"version": "0.18.11",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.2.tgz",
|
||||||
"integrity": "sha512-S3hkIF6KUqRh9n1Q0dSyYcWmcVa9Cg+mSoZEfFuzoYXXsk6196qndrM+ZiHNwpZKi3XOXpShZZ+9dfN5ykqjjw==",
|
"integrity": "sha512-mLfp0ziRPOLSTek0Gd9T5B8AtzKAkoZE70fneiiyPlSnUKKI4lp+mGEnQXcQEHLJAcIYDPSyBvsUbKUG2ri/XQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -690,9 +693,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
"version": "0.18.11",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.2.tgz",
|
||||||
"integrity": "sha512-MRESANOoObQINBA+RMZW+Z0TJWpibtE7cPFnahzyQHDCA9X9LOmGh68MVimZlM9J8n5Ia8lU773te6O3ILW8kw==",
|
"integrity": "sha512-hn28+JNDTxxCpnYjdDYVMNTR3SKavyLlCHHkufHV91fkewpIyQchS1d8wSbmXhs1fiYDpNww8KTFlJ1dHsxeSw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
@@ -705,9 +708,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
"version": "0.18.11",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.2.tgz",
|
||||||
"integrity": "sha512-qVyPIZrXNMOLYegtD1u8EBccCrBVshxMrn5MkuFc3mEVsw7CCQHaqZ4jm9hbn4gWY95XFnb7i4SsT3eflxZsUg==",
|
"integrity": "sha512-KbXaC0Sejt7vD2fEgPoIKb6nxkfYW9OmFUK9XQE4//PvGIxNIfPk1NmlHmMg6f25x57rpmEFrn1OotASYIAaTg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
@@ -720,9 +723,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
"version": "0.18.11",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.2.tgz",
|
||||||
"integrity": "sha512-T3yd8vJXfPirZaUOoA9D2ZjxZX4Gr3QuC3GztBJA6PklLotc/7sXTOuuRkhE9W/5JvJP/K9b99ayPNAD+R+4qQ==",
|
"integrity": "sha512-dJ0kE8KTqbiHtA3Fc/zn7lCd7pqVr4JcT0JqOnbj4LLzYnp+7h8Qi4yjfq42ZlHfhOCM42rBh0EwHYLL6LEzcw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -735,9 +738,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
"version": "0.18.11",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.2.tgz",
|
||||||
"integrity": "sha512-evUoRPWiwuFk++snjH9e2cAjF5VVSTj+Dnf+rkO/Q20tRqv+644279TZlPK8nUGunjPAtQRCj1jQkDAvL6rm2w==",
|
"integrity": "sha512-7Z/jKNFufZ/bbu4INqqCN6DDlrmOTmdw6D0gH+6Y7auok2r02Ur661qPuXidPOJ+FSgbEeQnnAGgsVynfLuOEw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@@ -750,9 +753,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
"version": "0.18.11",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.2.tgz",
|
||||||
"integrity": "sha512-/SlRJ15XR6i93gRWquRxYCfhTeC5PdqEapKoLbX63PLCmAkXZHY2uQm2l9bN0oPHBsOw2IswRZctMYS0MijFcg==",
|
"integrity": "sha512-U+RinR6aXXABFCcAY4gSlv4CL1oOVvSSCdseQmGO66H+XyuQGZIUdhG56SZaDJQcLmrSfRmx5XZOWyCJPRqS7g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -765,9 +768,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-x64": {
|
"node_modules/@esbuild/linux-x64": {
|
||||||
"version": "0.18.11",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.2.tgz",
|
||||||
"integrity": "sha512-xcncej+wF16WEmIwPtCHi0qmx1FweBqgsRtEL1mSHLFR6/mb3GEZfLQnx+pUDfRDEM4DQF8dpXIW7eDOZl1IbA==",
|
"integrity": "sha512-oxzHTEv6VPm3XXNaHPyUTTte+3wGv7qVQtqaZCrgstI16gCuhNOtBXLEBkBREP57YTd68P0VgDgG73jSD8bwXQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -780,9 +783,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
"version": "0.18.11",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.2.tgz",
|
||||||
"integrity": "sha512-aSjMHj/F7BuS1CptSXNg6S3M4F3bLp5wfFPIJM+Km2NfIVfFKhdmfHF9frhiCLIGVzDziggqWll0B+9AUbud/Q==",
|
"integrity": "sha512-WNa5zZk1XpTTwMDompZmvQLHszDDDN7lYjEHCUmAGB83Bgs20EMs7ICD+oKeT6xt4phV4NDdSi/8OfjPbSbZfQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -795,9 +798,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
"version": "0.18.11",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.2.tgz",
|
||||||
"integrity": "sha512-tNBq+6XIBZtht0xJGv7IBB5XaSyvYPCm1PxJ33zLQONdZoLVM0bgGqUrXnJyiEguD9LU4AHiu+GCXy/Hm9LsdQ==",
|
"integrity": "sha512-S6kI1aT3S++Dedb7vxIuUOb3oAxqxk2Rh5rOXOTYnzN8JzW1VzBd+IqPiSpgitu45042SYD3HCoEyhLKQcDFDw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -810,9 +813,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
"version": "0.18.11",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.2.tgz",
|
||||||
"integrity": "sha512-kxfbDOrH4dHuAAOhr7D7EqaYf+W45LsAOOhAet99EyuxxQmjbk8M9N4ezHcEiCYPaiW8Dj3K26Z2V17Gt6p3ng==",
|
"integrity": "sha512-VXSSMsmb+Z8LbsQGcBMiM+fYObDNRm8p7tkUDMPG/g4fhFX5DEFmjxIEa3N8Zr96SjsJ1woAhF0DUnS3MF3ARw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -825,9 +828,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
"version": "0.18.11",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.2.tgz",
|
||||||
"integrity": "sha512-Sh0dDRyk1Xi348idbal7lZyfSkjhJsdFeuC13zqdipsvMetlGiFQNdO+Yfp6f6B4FbyQm7qsk16yaZk25LChzg==",
|
"integrity": "sha512-5NayUlSAyb5PQYFAU9x3bHdsqB88RC3aM9lKDAz4X1mo/EchMIT1Q+pSeBXNgkfNmRecLXA0O8xP+x8V+g/LKg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -840,9 +843,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
"version": "0.18.11",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.2.tgz",
|
||||||
"integrity": "sha512-o9JUIKF1j0rqJTFbIoF4bXj6rvrTZYOrfRcGyL0Vm5uJ/j5CkBD/51tpdxe9lXEDouhRgdr/BYzUrDOvrWwJpg==",
|
"integrity": "sha512-47gL/ek1v36iN0wL9L4Q2MFdujR0poLZMJwhO2/N3gA89jgHp4MR8DKCmwYtGNksbfJb9JoTtbkoe6sDhg2QTA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -855,9 +858,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-x64": {
|
"node_modules/@esbuild/win32-x64": {
|
||||||
"version": "0.18.11",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.2.tgz",
|
||||||
"integrity": "sha512-rQI4cjLHd2hGsM1LqgDI7oOCYbQ6IBOVsX9ejuRMSze0GqXUG2ekwiKkiBU1pRGSeCqFFHxTrcEydB2Hyoz9CA==",
|
"integrity": "sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -899,30 +902,30 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/simple-git": {
|
"node_modules/@napi-rs/simple-git": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git/-/simple-git-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git/-/simple-git-0.1.9.tgz",
|
||||||
"integrity": "sha512-BvOMdkkofTz6lEE35itJ/laUokPhr/5ToMGlOH25YnhLD2yN1KpRAT4blW9tT8281/1aZjW3xyi73bs//IrDKA==",
|
"integrity": "sha512-qKzDS0+VjMvVyU28px+C6zlD1HKy83NIdYzfMQWa/g/V1iG/Ic8uwrS2ihHfm7mp7X0PPrmINLiTTi6ieUIKfw==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@napi-rs/simple-git-android-arm-eabi": "0.1.8",
|
"@napi-rs/simple-git-android-arm-eabi": "0.1.9",
|
||||||
"@napi-rs/simple-git-android-arm64": "0.1.8",
|
"@napi-rs/simple-git-android-arm64": "0.1.9",
|
||||||
"@napi-rs/simple-git-darwin-arm64": "0.1.8",
|
"@napi-rs/simple-git-darwin-arm64": "0.1.9",
|
||||||
"@napi-rs/simple-git-darwin-x64": "0.1.8",
|
"@napi-rs/simple-git-darwin-x64": "0.1.9",
|
||||||
"@napi-rs/simple-git-linux-arm-gnueabihf": "0.1.8",
|
"@napi-rs/simple-git-linux-arm-gnueabihf": "0.1.9",
|
||||||
"@napi-rs/simple-git-linux-arm64-gnu": "0.1.8",
|
"@napi-rs/simple-git-linux-arm64-gnu": "0.1.9",
|
||||||
"@napi-rs/simple-git-linux-arm64-musl": "0.1.8",
|
"@napi-rs/simple-git-linux-arm64-musl": "0.1.9",
|
||||||
"@napi-rs/simple-git-linux-x64-gnu": "0.1.8",
|
"@napi-rs/simple-git-linux-x64-gnu": "0.1.9",
|
||||||
"@napi-rs/simple-git-linux-x64-musl": "0.1.8",
|
"@napi-rs/simple-git-linux-x64-musl": "0.1.9",
|
||||||
"@napi-rs/simple-git-win32-arm64-msvc": "0.1.8",
|
"@napi-rs/simple-git-win32-arm64-msvc": "0.1.9",
|
||||||
"@napi-rs/simple-git-win32-x64-msvc": "0.1.8"
|
"@napi-rs/simple-git-win32-x64-msvc": "0.1.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/simple-git-android-arm-eabi": {
|
"node_modules/@napi-rs/simple-git-android-arm-eabi": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm-eabi/-/simple-git-android-arm-eabi-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm-eabi/-/simple-git-android-arm-eabi-0.1.9.tgz",
|
||||||
"integrity": "sha512-JJCejHBB1G6O8nxjQLT4quWCcvLpC3oRdJJ9G3MFYSCoYS8i1bWCWeU+K7Br+xT+D6s1t9q8kNJAwJv9Ygpi0g==",
|
"integrity": "sha512-9D4JnfePMpgL4pg9aMUX7/TIWEUQ+Tgx8n3Pf8TNCMGjUbImJyYsDSLJzbcv9wH7srgn4GRjSizXFJHAPjzEug==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -935,9 +938,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/simple-git-android-arm64": {
|
"node_modules/@napi-rs/simple-git-android-arm64": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm64/-/simple-git-android-arm64-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm64/-/simple-git-android-arm64-0.1.9.tgz",
|
||||||
"integrity": "sha512-mraHzwWBw3tdRetNOS5KnFSjvdAbNBnjFLA8I4PwTCPJj3Q4txrigcPp2d59cJ0TC51xpnPXnZjYdNwwSI9g6g==",
|
"integrity": "sha512-Krilsw0gPrrASZzudNEl9pdLuNbhoTK0j7pUbfB8FRifpPdFB/zouwuEm0aSnsDXN4ftGrmGG82kuiR/2MeoPg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -950,9 +953,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/simple-git-darwin-arm64": {
|
"node_modules/@napi-rs/simple-git-darwin-arm64": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-arm64/-/simple-git-darwin-arm64-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-arm64/-/simple-git-darwin-arm64-0.1.9.tgz",
|
||||||
"integrity": "sha512-ufy/36eI/j4UskEuvqSH7uXtp3oXeLDmjQCfKJz3u5Vx98KmOMKrqAm2H81AB2WOtCo5mqS6PbBeUXR8BJX8lQ==",
|
"integrity": "sha512-H/F09nDgYjv4gcFrZBgdTKkZEepqt0KLYcCJuUADuxkKupmjLdecMhypXLk13AzvLW4UQI7NlLTLDXUFLyr2BA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -965,9 +968,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/simple-git-darwin-x64": {
|
"node_modules/@napi-rs/simple-git-darwin-x64": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-x64/-/simple-git-darwin-x64-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-x64/-/simple-git-darwin-x64-0.1.9.tgz",
|
||||||
"integrity": "sha512-Vb21U+v3tPJNl+8JtIHHT8HGe6WZ8o1Tq3f6p+Jx9Cz71zEbcIiB9FCEMY1knS/jwQEOuhhlI9Qk7d4HY+rprA==",
|
"integrity": "sha512-jBR2xS9nVPqmHv0TWz874W0m/d453MGrMeLjB+boK5IPPLhg3AWIZj0aN9jy2Je1BGVAa0w3INIQJtBBeB6kFA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -980,9 +983,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/simple-git-linux-arm-gnueabihf": {
|
"node_modules/@napi-rs/simple-git-linux-arm-gnueabihf": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm-gnueabihf/-/simple-git-linux-arm-gnueabihf-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm-gnueabihf/-/simple-git-linux-arm-gnueabihf-0.1.9.tgz",
|
||||||
"integrity": "sha512-6BPTJ7CzpSm2t54mRLVaUr3S7ORJfVJoCk2rQ8v8oDg0XAMKvmQQxOsAgqKBo9gYNHJnqrOx3AEuEgvB586BuQ==",
|
"integrity": "sha512-3n0+VpO4YfZxndZ0sCvsHIvsazd+JmbSjrlTRBCnJeAU1/sfos3skNZtKGZksZhjvd+3o+/GFM8L7Xnv01yggA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -995,13 +998,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/simple-git-linux-arm64-gnu": {
|
"node_modules/@napi-rs/simple-git-linux-arm64-gnu": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-gnu/-/simple-git-linux-arm64-gnu-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-gnu/-/simple-git-linux-arm64-gnu-0.1.9.tgz",
|
||||||
"integrity": "sha512-qfESqUCAA/XoQpRXHptSQ8gIFnETCQt1zY9VOkplx6tgYk9PCeaX4B1Xuzrh3eZamSCMJFn+1YB9Ut8NwyGgAA==",
|
"integrity": "sha512-lIzf0KHU2SKC12vMrWwCtysG2Sdt31VHRPMUiz9lD9t3xwVn8qhFSTn5yDkTeG3rgX6o0p5EKalfQN5BXsJq2w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"hasInstallScript": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -1011,13 +1013,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/simple-git-linux-arm64-musl": {
|
"node_modules/@napi-rs/simple-git-linux-arm64-musl": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-musl/-/simple-git-linux-arm64-musl-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-musl/-/simple-git-linux-arm64-musl-0.1.9.tgz",
|
||||||
"integrity": "sha512-G80BQPpaRmQpn8dJGHp4I2/YVhWDUNJwcCrJAtAdbKFDCMyCHJBln2ERL/+IEUlIAT05zK/c1Z5WEprvXEdXow==",
|
"integrity": "sha512-KQozUoNXrxrB8k741ncWXSiMbjl1AGBGfZV21PANzUM8wH4Yem2bg3kfglYS/QIx3udspsT35I9abu49n7D1/w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"hasInstallScript": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -1027,13 +1028,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/simple-git-linux-x64-gnu": {
|
"node_modules/@napi-rs/simple-git-linux-x64-gnu": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-gnu/-/simple-git-linux-x64-gnu-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-gnu/-/simple-git-linux-x64-gnu-0.1.9.tgz",
|
||||||
"integrity": "sha512-NI6o1sZYEf6vPtNWJAm9w8BxJt+LlSFW0liSjYe3lc3e4dhMfV240f0ALeqlwdIldRPaDFwZSJX5/QbS7nMzhw==",
|
"integrity": "sha512-O/Niui5mnHPcK3iYC3ui8wgERtJWsQ3Y74W/09t0bL/3dgzGMl4oQt0qTj9dWCsnoGsIEYHPzwCBp/2vqYp/pw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"hasInstallScript": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -1043,13 +1043,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/simple-git-linux-x64-musl": {
|
"node_modules/@napi-rs/simple-git-linux-x64-musl": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-musl/-/simple-git-linux-x64-musl-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-musl/-/simple-git-linux-x64-musl-0.1.9.tgz",
|
||||||
"integrity": "sha512-wljGAEOW41er45VTiU8kXJmO480pQKzsgRCvPlJJSCaEVBbmo6XXbFIXnZy1a2J3Zyy2IOsRB4PVkUZaNuPkZQ==",
|
"integrity": "sha512-L9n+e8Wn3hKr3RsIdY8GaB+ry4xZ4BaGwyKExgoB8nDGQuRUY9oP6p0WA4hWfJvJnU1H6hvo36a5UFPReyBO7A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"hasInstallScript": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -1059,9 +1058,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/simple-git-win32-arm64-msvc": {
|
"node_modules/@napi-rs/simple-git-win32-arm64-msvc": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-arm64-msvc/-/simple-git-win32-arm64-msvc-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-arm64-msvc/-/simple-git-win32-arm64-msvc-0.1.9.tgz",
|
||||||
"integrity": "sha512-QuV4QILyKPfbWHoQKrhXqjiCClx0SxbCTVogkR89BwivekqJMd9UlMxZdoCmwLWutRx4z9KmzQqokvYI5QeepA==",
|
"integrity": "sha512-Z6Ja/SZK+lMvRWaxj7wjnvSbAsGrH006sqZo8P8nxKUdZfkVvoCaAWr1r0cfkk2Z3aijLLtD+vKeXGlUPH6gGQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1074,9 +1073,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/simple-git-win32-x64-msvc": {
|
"node_modules/@napi-rs/simple-git-win32-x64-msvc": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-x64-msvc/-/simple-git-win32-x64-msvc-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-x64-msvc/-/simple-git-win32-x64-msvc-0.1.9.tgz",
|
||||||
"integrity": "sha512-UzNS4JtjhZhZ5hRLq7BIUq+4JOwt1ThIKv11CsF1ag2l99f0123XvfEpjczKTaa94nHtjXYc2Mv9TjccBqYOew==",
|
"integrity": "sha512-VAZj1UvC+R2MjKOD3I/Y7dmQlHWAYy4omhReQJRpbCf+oGCBi9CWiIduGqeYEq723nLIKdxP7XjaO0wl1NnUww==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2441,9 +2440,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.18.11",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.11.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.2.tgz",
|
||||||
"integrity": "sha512-i8u6mQF0JKJUlGR3OdFLKldJQMMs8OqM9Cc3UCi9XXziJ9WERM5bfkHaEAy0YAvPRMgqSW55W7xYn84XtEFTtA==",
|
"integrity": "sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"esbuild": "bin/esbuild"
|
"esbuild": "bin/esbuild"
|
||||||
@@ -2452,40 +2451,40 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@esbuild/android-arm": "0.18.11",
|
"@esbuild/android-arm": "0.19.2",
|
||||||
"@esbuild/android-arm64": "0.18.11",
|
"@esbuild/android-arm64": "0.19.2",
|
||||||
"@esbuild/android-x64": "0.18.11",
|
"@esbuild/android-x64": "0.19.2",
|
||||||
"@esbuild/darwin-arm64": "0.18.11",
|
"@esbuild/darwin-arm64": "0.19.2",
|
||||||
"@esbuild/darwin-x64": "0.18.11",
|
"@esbuild/darwin-x64": "0.19.2",
|
||||||
"@esbuild/freebsd-arm64": "0.18.11",
|
"@esbuild/freebsd-arm64": "0.19.2",
|
||||||
"@esbuild/freebsd-x64": "0.18.11",
|
"@esbuild/freebsd-x64": "0.19.2",
|
||||||
"@esbuild/linux-arm": "0.18.11",
|
"@esbuild/linux-arm": "0.19.2",
|
||||||
"@esbuild/linux-arm64": "0.18.11",
|
"@esbuild/linux-arm64": "0.19.2",
|
||||||
"@esbuild/linux-ia32": "0.18.11",
|
"@esbuild/linux-ia32": "0.19.2",
|
||||||
"@esbuild/linux-loong64": "0.18.11",
|
"@esbuild/linux-loong64": "0.19.2",
|
||||||
"@esbuild/linux-mips64el": "0.18.11",
|
"@esbuild/linux-mips64el": "0.19.2",
|
||||||
"@esbuild/linux-ppc64": "0.18.11",
|
"@esbuild/linux-ppc64": "0.19.2",
|
||||||
"@esbuild/linux-riscv64": "0.18.11",
|
"@esbuild/linux-riscv64": "0.19.2",
|
||||||
"@esbuild/linux-s390x": "0.18.11",
|
"@esbuild/linux-s390x": "0.19.2",
|
||||||
"@esbuild/linux-x64": "0.18.11",
|
"@esbuild/linux-x64": "0.19.2",
|
||||||
"@esbuild/netbsd-x64": "0.18.11",
|
"@esbuild/netbsd-x64": "0.19.2",
|
||||||
"@esbuild/openbsd-x64": "0.18.11",
|
"@esbuild/openbsd-x64": "0.19.2",
|
||||||
"@esbuild/sunos-x64": "0.18.11",
|
"@esbuild/sunos-x64": "0.19.2",
|
||||||
"@esbuild/win32-arm64": "0.18.11",
|
"@esbuild/win32-arm64": "0.19.2",
|
||||||
"@esbuild/win32-ia32": "0.18.11",
|
"@esbuild/win32-ia32": "0.19.2",
|
||||||
"@esbuild/win32-x64": "0.18.11"
|
"@esbuild/win32-x64": "0.19.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esbuild-sass-plugin": {
|
"node_modules/esbuild-sass-plugin": {
|
||||||
"version": "2.10.0",
|
"version": "2.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.12.0.tgz",
|
||||||
"integrity": "sha512-STv849QGT8g77RRFmroSt4VBVKjv+dypKcO4aWz8IP4G5JbRH0KC0+B8ODuzlUNu9R5MbkGcev/62RDP/JcZ2Q==",
|
"integrity": "sha512-+k/5WM/Yf/Ur7ahn6XXxEPwa/lmuacLO7vrCIAJuvQapX1CiIHtlX/nc2eiMoJ6P6RvqZhKpQvIiwgYJonzHtw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"resolve": "^1.22.2",
|
"resolve": "^1.22.2",
|
||||||
"sass": "^1.63.0"
|
"sass": "^1.65.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"esbuild": "^0.18.0"
|
"esbuild": "^0.19.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
@@ -3147,9 +3146,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/immutable": {
|
"node_modules/immutable": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.3.tgz",
|
||||||
"integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg=="
|
"integrity": "sha512-808ZFYMsIRAjLAu5xkKo0TsbY9LBy9H5MazTKIEHerNkg0ymgilGfBPMR/3G7d/ihGmuK2Hw8S1izY2d3kd3wA=="
|
||||||
},
|
},
|
||||||
"node_modules/inline-style-parser": {
|
"node_modules/inline-style-parser": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
@@ -3412,9 +3411,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.21.5",
|
"version": "1.21.7",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.21.7.tgz",
|
||||||
"integrity": "sha512-/pEUPeih2EwIx9n4T82aOG6CInN83tl/mWlw6B5gWLf36UplQi1L+5p3FUHsdt4fXVfOkkh9KIaM3owoq7ss8A==",
|
"integrity": "sha512-xITZyh5sLFwRPYUSw15T00Rm7gcQ1qOPuQwNOcvHsTm6nLWTQ723w7zl42wrC5t+xtdg6FPmnXHml1nZxxvp1w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"detect-libc": "^1.0.3"
|
"detect-libc": "^1.0.3"
|
||||||
},
|
},
|
||||||
@@ -3426,20 +3425,21 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"lightningcss-darwin-arm64": "1.21.5",
|
"lightningcss-darwin-arm64": "1.21.7",
|
||||||
"lightningcss-darwin-x64": "1.21.5",
|
"lightningcss-darwin-x64": "1.21.7",
|
||||||
"lightningcss-linux-arm-gnueabihf": "1.21.5",
|
"lightningcss-freebsd-x64": "1.21.7",
|
||||||
"lightningcss-linux-arm64-gnu": "1.21.5",
|
"lightningcss-linux-arm-gnueabihf": "1.21.7",
|
||||||
"lightningcss-linux-arm64-musl": "1.21.5",
|
"lightningcss-linux-arm64-gnu": "1.21.7",
|
||||||
"lightningcss-linux-x64-gnu": "1.21.5",
|
"lightningcss-linux-arm64-musl": "1.21.7",
|
||||||
"lightningcss-linux-x64-musl": "1.21.5",
|
"lightningcss-linux-x64-gnu": "1.21.7",
|
||||||
"lightningcss-win32-x64-msvc": "1.21.5"
|
"lightningcss-linux-x64-musl": "1.21.7",
|
||||||
|
"lightningcss-win32-x64-msvc": "1.21.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-darwin-arm64": {
|
"node_modules/lightningcss-darwin-arm64": {
|
||||||
"version": "1.21.5",
|
"version": "1.21.7",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.21.7.tgz",
|
||||||
"integrity": "sha512-z05hyLX85WY0UfhkFUOrWEFqD69lpVAmgl3aDzMKlIZJGygbhbegqb4PV8qfUrKKNBauut/qVNPKZglhTaDDxA==",
|
"integrity": "sha512-tt7hIsFio9jZofTVHtCACz6rB6c9RyABMXfA9A/VcKOjS3sq+koX/QkRJWY06utwOImbJIXBC5hbg9t3RkPUAQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3456,9 +3456,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-darwin-x64": {
|
"node_modules/lightningcss-darwin-x64": {
|
||||||
"version": "1.21.5",
|
"version": "1.21.7",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.21.7.tgz",
|
||||||
"integrity": "sha512-MSJhmej/U9MrdPxDk7+FWhO8+UqVoZUHG4VvKT5RQ4RJtqtANTiWiI97LvoVNMtdMnHaKs1Pkji6wHUFxjJsHQ==",
|
"integrity": "sha512-F4gS4bf7eWekfPT+TxJNm/pF+QRgZiTrTkQH6cw4/UWfdeZISfuhD5El2dm16giFnY0K5ylIwO+ZusgYNkGSXA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3474,10 +3474,29 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lightningcss-freebsd-x64": {
|
||||||
|
"version": "1.21.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.21.7.tgz",
|
||||||
|
"integrity": "sha512-RMfNzJWXCSfPnL55fcLWEAadcY6QUFT0S8NceNKYzp1KiCZtkJIy6RQ5SaVxPzRqd3iMsahUf5sfnG8N1UQSNQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||||
"version": "1.21.5",
|
"version": "1.21.7",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.21.7.tgz",
|
||||||
"integrity": "sha512-xN6+5/JsMrbZHL1lPl+MiNJ3Xza12ueBKPepiyDCFQzlhFRTj7D0LG+cfNTzPBTO8KcYQynLpl1iBB8LGp3Xtw==",
|
"integrity": "sha512-biSRUDZNx7vubWP1jArw/qqfZKPGpkV/qzunasZzxmqijbZ43sW9faDQYxWNcxPWljJJdF/qs6qcurYFovWtrQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -3494,9 +3513,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||||
"version": "1.21.5",
|
"version": "1.21.7",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.21.7.tgz",
|
||||||
"integrity": "sha512-KfzFNhC4XTbmG3ma/xcTs/IhCwieW89XALIusKmnV0N618ZDXEB0XjWOYQRCXeK9mfqPdbTBpurEHV/XZtkniQ==",
|
"integrity": "sha512-PENY8QekqL9TG3AY/A7rkUBb5ymefGxea7Oe7+x7Hbw4Bz4Hpj5cec5OoMypMqFbURPmpi0fTWx4vSWUPzpDcA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3513,9 +3532,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm64-musl": {
|
"node_modules/lightningcss-linux-arm64-musl": {
|
||||||
"version": "1.21.5",
|
"version": "1.21.7",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.21.7.tgz",
|
||||||
"integrity": "sha512-bc0GytQO5Mn9QM6szaZ+31fQHNdidgpM1sSCwzPItz8hg3wOvKl8039rU0veMJV3ZgC9z0ypNRceLrSHeRHmXw==",
|
"integrity": "sha512-pfOipKvA/0X1OjRaZt3870vnV9UGBSjayIqHh0fGx/+aRz3O0MVFHE/60P2UWXpM3YGJEw/hMWtNkrFwqOge8A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3532,9 +3551,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-x64-gnu": {
|
"node_modules/lightningcss-linux-x64-gnu": {
|
||||||
"version": "1.21.5",
|
"version": "1.21.7",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.21.7.tgz",
|
||||||
"integrity": "sha512-JwMbgypPQgc2kW2av3OwzZ8cbrEuIiDiXPJdXRE6aVxu67yHauJawQLqJKTGUhiAhy6iLDG8Wg0a3/ziL+m+Kw==",
|
"integrity": "sha512-dgcsis4TAA7s0ia4f31QHX+G4PWPwxk+wJaEQLaV0NdJs09O5hHoA8DpLEr8nrvc/tsRTyVNBP1rDtgzySjpXg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3551,9 +3570,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-x64-musl": {
|
"node_modules/lightningcss-linux-x64-musl": {
|
||||||
"version": "1.21.5",
|
"version": "1.21.7",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.21.7.tgz",
|
||||||
"integrity": "sha512-Ib8b6IQ/OR/VrPU6YBgy4T3QnuHY7DUa95O+nz+cwrTkMSN6fuHcTcIaz4t8TJ6HI5pl3uxUOZjmtls2pyQWow==",
|
"integrity": "sha512-A+9dXpxld3p4Cd6fxev2eqEvaauYtrgNpXV3t7ioCJy30Oj9nYiNGwiGusM+4MJVcEpUPGUGiuAqY4sWilRDwA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3570,9 +3589,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-win32-x64-msvc": {
|
"node_modules/lightningcss-win32-x64-msvc": {
|
||||||
"version": "1.21.5",
|
"version": "1.21.7",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.21.7.tgz",
|
||||||
"integrity": "sha512-A8cSi8lUpBeVmoF+DqqW7cd0FemDbCuKr490IXdjyeI+KL8adpSKUs8tcqO0OXPh1EoDqK7JNkD/dELmd4Iz5g==",
|
"integrity": "sha512-07/8vogEq+C/mF99pdMhh/f19/xreq8N9Ca6AWeVHZIdODyF/pt6KdKSCWDZWIn+3CUxI8gCJWuUWyOc3xymvw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3793,6 +3812,19 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mdast-util-newline-to-break": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-491LcYv3gbGhhCrLoeALncQmega2xPh+m3gbsIhVsOX4sw85+ShLFPvPyibxc1Swx/6GtzxgVodq+cGa/47ULg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/mdast": "^3.0.0",
|
||||||
|
"mdast-util-find-and-replace": "^2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/unified"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mdast-util-phrasing": {
|
"node_modules/mdast-util-phrasing": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz",
|
||||||
@@ -4886,6 +4918,20 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/remark-breaks": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-C7VkvcUp1TPUc2eAYzsPdaUh8Xj4FSbQnYA5A9f80diApLZscTDeG7efiWP65W8hV2sEy3JuGVU0i6qr5D8Hug==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/mdast": "^3.0.0",
|
||||||
|
"mdast-util-newline-to-break": "^1.0.0",
|
||||||
|
"unified": "^10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/unified"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/remark-frontmatter": {
|
"node_modules/remark-frontmatter": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-4.0.1.tgz",
|
||||||
@@ -5159,9 +5205,9 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||||
},
|
},
|
||||||
"node_modules/sass": {
|
"node_modules/sass": {
|
||||||
"version": "1.63.6",
|
"version": "1.66.1",
|
||||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.63.6.tgz",
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.66.1.tgz",
|
||||||
"integrity": "sha512-MJuxGMHzaOW7ipp+1KdELtqKbfAWbH7OLIdoSMnVe3EXPMTmxTmlaZDCTsgIpPCs3w99lLo9/zDKkOrJuT5byw==",
|
"integrity": "sha512-50c+zTsZOJVgFfTgwwEzkjA3/QACgdNsKueWPyAR0mRINIvLAStVQBbPg14iuqEQ74NPDbXzJARJ/O4SI1zftA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": ">=3.0.0 <4.0.0",
|
"chokidar": ">=3.0.0 <4.0.0",
|
||||||
"immutable": "^4.0.0",
|
"immutable": "^4.0.0",
|
||||||
@@ -5532,6 +5578,11 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/toml": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="
|
||||||
|
},
|
||||||
"node_modules/tough-cookie": {
|
"node_modules/tough-cookie": {
|
||||||
"version": "4.1.3",
|
"version": "4.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
|
||||||
|
|||||||
14
package.json
14
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.0.8",
|
"version": "4.1.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -12,12 +12,14 @@
|
|||||||
"url": "https://github.com/jackyzha0/quartz.git"
|
"url": "https://github.com/jackyzha0/quartz.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"docs": "npx quartz build --serve -d docs",
|
||||||
"check": "tsc --noEmit && npx prettier . --check",
|
"check": "tsc --noEmit && npx prettier . --check",
|
||||||
"format": "npx prettier . --write",
|
"format": "npx prettier . --write",
|
||||||
"test": "tsx ./quartz/util/path.test.ts",
|
"test": "tsx ./quartz/util/path.test.ts",
|
||||||
"profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1"
|
"profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
"npm": ">=9.3.1",
|
||||||
"node": ">=18.14"
|
"node": ">=18.14"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -34,13 +36,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.6.3",
|
"@clack/prompts": "^0.6.3",
|
||||||
"@floating-ui/dom": "^1.4.0",
|
"@floating-ui/dom": "^1.4.0",
|
||||||
"@napi-rs/simple-git": "^0.1.8",
|
"@napi-rs/simple-git": "0.1.9",
|
||||||
"async-mutex": "^0.4.0",
|
"async-mutex": "^0.4.0",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"cli-spinner": "^0.2.10",
|
"cli-spinner": "^0.2.10",
|
||||||
"d3": "^7.8.5",
|
"d3": "^7.8.5",
|
||||||
"esbuild-sass-plugin": "^2.9.0",
|
"esbuild-sass-plugin": "^2.12.0",
|
||||||
"flexsearch": "0.7.21",
|
"flexsearch": "0.7.21",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"globby": "^13.1.4",
|
"globby": "^13.1.4",
|
||||||
@@ -50,7 +52,7 @@
|
|||||||
"hast-util-to-string": "^2.0.0",
|
"hast-util-to-string": "^2.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.21.5",
|
"lightningcss": "1.21.7",
|
||||||
"mdast-util-find-and-replace": "^2.2.2",
|
"mdast-util-find-and-replace": "^2.2.2",
|
||||||
"mdast-util-to-hast": "^12.3.0",
|
"mdast-util-to-hast": "^12.3.0",
|
||||||
"mdast-util-to-string": "^3.2.0",
|
"mdast-util-to-string": "^3.2.0",
|
||||||
@@ -68,6 +70,7 @@
|
|||||||
"rehype-raw": "^6.1.1",
|
"rehype-raw": "^6.1.1",
|
||||||
"rehype-slug": "^5.1.0",
|
"rehype-slug": "^5.1.0",
|
||||||
"remark": "^14.0.2",
|
"remark": "^14.0.2",
|
||||||
|
"remark-breaks": "^3.0.3",
|
||||||
"remark-frontmatter": "^4.0.1",
|
"remark-frontmatter": "^4.0.1",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-math": "^5.1.1",
|
"remark-math": "^5.1.1",
|
||||||
@@ -78,6 +81,7 @@
|
|||||||
"serve-handler": "^6.1.5",
|
"serve-handler": "^6.1.5",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"to-vfile": "^7.2.4",
|
"to-vfile": "^7.2.4",
|
||||||
|
"toml": "^3.0.0",
|
||||||
"unified": "^10.1.2",
|
"unified": "^10.1.2",
|
||||||
"unist-util-visit": "^4.1.2",
|
"unist-util-visit": "^4.1.2",
|
||||||
"vfile": "^5.3.7",
|
"vfile": "^5.3.7",
|
||||||
@@ -97,7 +101,7 @@
|
|||||||
"@types/workerpool": "^6.4.0",
|
"@types/workerpool": "^6.4.0",
|
||||||
"@types/ws": "^8.5.5",
|
"@types/ws": "^8.5.5",
|
||||||
"@types/yargs": "^17.0.24",
|
"@types/yargs": "^17.0.24",
|
||||||
"esbuild": "^0.18.11",
|
"esbuild": "0.19.2",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"tsx": "^3.12.7",
|
"tsx": "^3.12.7",
|
||||||
"typescript": "^5.0.4"
|
"typescript": "^5.0.4"
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ const config: QuartzConfig = {
|
|||||||
provider: "plausible",
|
provider: "plausible",
|
||||||
},
|
},
|
||||||
baseUrl: "quartz.jzhao.xyz",
|
baseUrl: "quartz.jzhao.xyz",
|
||||||
ignorePatterns: ["private", "templates"],
|
ignorePatterns: ["private", "templates", ".obsidian"],
|
||||||
|
defaultDateType: "created",
|
||||||
theme: {
|
theme: {
|
||||||
typography: {
|
typography: {
|
||||||
header: "Schibsted Grotesk",
|
header: "Schibsted Grotesk",
|
||||||
@@ -68,6 +69,7 @@ const config: QuartzConfig = {
|
|||||||
}),
|
}),
|
||||||
Plugin.Assets(),
|
Plugin.Assets(),
|
||||||
Plugin.Static(),
|
Plugin.Static(),
|
||||||
|
Plugin.NotFoundPage(),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,15 +15,24 @@ export const sharedPageComponents: SharedLayout = {
|
|||||||
|
|
||||||
// components for pages that display a single page (e.g. a single note)
|
// components for pages that display a single page (e.g. a single note)
|
||||||
export const defaultContentPageLayout: PageLayout = {
|
export const defaultContentPageLayout: PageLayout = {
|
||||||
beforeBody: [Component.ArticleTitle(), Component.ContentMeta(), Component.TagList()],
|
beforeBody: [
|
||||||
|
Component.Breadcrumbs(),
|
||||||
|
Component.ArticleTitle(),
|
||||||
|
Component.ContentMeta(),
|
||||||
|
Component.TagList(),
|
||||||
|
],
|
||||||
left: [
|
left: [
|
||||||
Component.PageTitle(),
|
Component.PageTitle(),
|
||||||
Component.MobileOnly(Component.Spacer()),
|
Component.MobileOnly(Component.Spacer()),
|
||||||
Component.Search(),
|
Component.Search(),
|
||||||
Component.Darkmode(),
|
Component.Darkmode(),
|
||||||
Component.DesktopOnly(Component.TableOfContents()),
|
Component.DesktopOnly(Component.Explorer()),
|
||||||
|
],
|
||||||
|
right: [
|
||||||
|
Component.Graph(),
|
||||||
|
Component.DesktopOnly(Component.TableOfContents()),
|
||||||
|
Component.Backlinks(),
|
||||||
],
|
],
|
||||||
right: [Component.Graph(), Component.Backlinks()],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// components for pages that display lists of pages (e.g. tags or folders)
|
// components for pages that display lists of pages (e.g. tags or folders)
|
||||||
|
|||||||
@@ -1,537 +1,39 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import { promises, readFileSync } from "fs"
|
|
||||||
import yargs from "yargs"
|
import yargs from "yargs"
|
||||||
import path from "path"
|
|
||||||
import { hideBin } from "yargs/helpers"
|
import { hideBin } from "yargs/helpers"
|
||||||
import esbuild from "esbuild"
|
import {
|
||||||
import chalk from "chalk"
|
handleBuild,
|
||||||
import { sassPlugin } from "esbuild-sass-plugin"
|
handleCreate,
|
||||||
import fs from "fs"
|
handleUpdate,
|
||||||
import { intro, isCancel, outro, select, text } from "@clack/prompts"
|
handleRestore,
|
||||||
import { rimraf } from "rimraf"
|
handleSync,
|
||||||
import chokidar from "chokidar"
|
} from "./cli/handlers.js"
|
||||||
import prettyBytes from "pretty-bytes"
|
import { CommonArgv, BuildArgv, CreateArgv, SyncArgv } from "./cli/args.js"
|
||||||
import { execSync, spawnSync } from "child_process"
|
import { version } from "./cli/constants.js"
|
||||||
import http from "http"
|
|
||||||
import serveHandler from "serve-handler"
|
|
||||||
import { WebSocketServer } from "ws"
|
|
||||||
import { randomUUID } from "crypto"
|
|
||||||
import { Mutex } from "async-mutex"
|
|
||||||
|
|
||||||
const ORIGIN_NAME = "origin"
|
|
||||||
const UPSTREAM_NAME = "upstream"
|
|
||||||
const QUARTZ_SOURCE_BRANCH = "v4"
|
|
||||||
const cwd = process.cwd()
|
|
||||||
const cacheDir = path.join(cwd, ".quartz-cache")
|
|
||||||
const cacheFile = "./.quartz-cache/transpiled-build.mjs"
|
|
||||||
const fp = "./quartz/build.ts"
|
|
||||||
const { version } = JSON.parse(readFileSync("./package.json").toString())
|
|
||||||
const contentCacheFolder = path.join(cacheDir, "content-cache")
|
|
||||||
|
|
||||||
const CommonArgv = {
|
|
||||||
directory: {
|
|
||||||
string: true,
|
|
||||||
alias: ["d"],
|
|
||||||
default: "content",
|
|
||||||
describe: "directory to look for content files",
|
|
||||||
},
|
|
||||||
verbose: {
|
|
||||||
boolean: true,
|
|
||||||
alias: ["v"],
|
|
||||||
default: false,
|
|
||||||
describe: "print out extra logging information",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const SyncArgv = {
|
|
||||||
...CommonArgv,
|
|
||||||
commit: {
|
|
||||||
boolean: true,
|
|
||||||
default: true,
|
|
||||||
describe: "create a git commit for your unsaved changes",
|
|
||||||
},
|
|
||||||
push: {
|
|
||||||
boolean: true,
|
|
||||||
default: true,
|
|
||||||
describe: "push updates to your Quartz fork",
|
|
||||||
},
|
|
||||||
pull: {
|
|
||||||
boolean: true,
|
|
||||||
default: true,
|
|
||||||
describe: "pull updates from your Quartz fork",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const BuildArgv = {
|
|
||||||
...CommonArgv,
|
|
||||||
output: {
|
|
||||||
string: true,
|
|
||||||
alias: ["o"],
|
|
||||||
default: "public",
|
|
||||||
describe: "output folder for files",
|
|
||||||
},
|
|
||||||
serve: {
|
|
||||||
boolean: true,
|
|
||||||
default: false,
|
|
||||||
describe: "run a local server to live-preview your Quartz",
|
|
||||||
},
|
|
||||||
baseDir: {
|
|
||||||
string: true,
|
|
||||||
default: "",
|
|
||||||
describe: "base path to serve your local server on",
|
|
||||||
},
|
|
||||||
port: {
|
|
||||||
number: true,
|
|
||||||
default: 8080,
|
|
||||||
describe: "port to serve Quartz on",
|
|
||||||
},
|
|
||||||
bundleInfo: {
|
|
||||||
boolean: true,
|
|
||||||
default: false,
|
|
||||||
describe: "show detailed bundle information",
|
|
||||||
},
|
|
||||||
concurrency: {
|
|
||||||
number: true,
|
|
||||||
describe: "how many threads to use to parse notes",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapePath(fp) {
|
|
||||||
return fp
|
|
||||||
.replace(/\\ /g, " ") // unescape spaces
|
|
||||||
.replace(/^".*"$/, "$1")
|
|
||||||
.replace(/^'.*"$/, "$1")
|
|
||||||
.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
function exitIfCancel(val) {
|
|
||||||
if (isCancel(val)) {
|
|
||||||
outro(chalk.red("Exiting"))
|
|
||||||
process.exit(0)
|
|
||||||
} else {
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function stashContentFolder(contentFolder) {
|
|
||||||
await fs.promises.rm(contentCacheFolder, { force: true, recursive: true })
|
|
||||||
await fs.promises.cp(contentFolder, contentCacheFolder, {
|
|
||||||
force: true,
|
|
||||||
recursive: true,
|
|
||||||
verbatimSymlinks: true,
|
|
||||||
preserveTimestamps: true,
|
|
||||||
})
|
|
||||||
await fs.promises.rm(contentFolder, { force: true, recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
async function popContentFolder(contentFolder) {
|
|
||||||
await fs.promises.rm(contentFolder, { force: true, recursive: true })
|
|
||||||
await fs.promises.cp(contentCacheFolder, contentFolder, {
|
|
||||||
force: true,
|
|
||||||
recursive: true,
|
|
||||||
verbatimSymlinks: true,
|
|
||||||
preserveTimestamps: true,
|
|
||||||
})
|
|
||||||
await fs.promises.rm(contentCacheFolder, { force: true, recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
function gitPull(origin, branch) {
|
|
||||||
const flags = ["--no-rebase", "--autostash", "-s", "recursive", "-X", "ours", "--no-edit"]
|
|
||||||
const out = spawnSync("git", ["pull", ...flags, origin, branch], { stdio: "inherit" })
|
|
||||||
if (out.stderr) {
|
|
||||||
throw new Error(`Error while pulling updates: ${out.stderr}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
yargs(hideBin(process.argv))
|
yargs(hideBin(process.argv))
|
||||||
.scriptName("quartz")
|
.scriptName("quartz")
|
||||||
.version(version)
|
.version(version)
|
||||||
.usage("$0 <cmd> [args]")
|
.usage("$0 <cmd> [args]")
|
||||||
.command("create", "Initialize Quartz", CommonArgv, async (argv) => {
|
.command("create", "Initialize Quartz", CreateArgv, async (argv) => {
|
||||||
console.log()
|
await handleCreate(argv)
|
||||||
intro(chalk.bgGreen.black(` Quartz v${version} `))
|
|
||||||
const contentFolder = path.join(cwd, argv.directory)
|
|
||||||
const setupStrategy = exitIfCancel(
|
|
||||||
await select({
|
|
||||||
message: `Choose how to initialize the content in \`${contentFolder}\``,
|
|
||||||
options: [
|
|
||||||
{ value: "new", label: "Empty Quartz" },
|
|
||||||
{ value: "copy", label: "Copy an existing folder", hint: "overwrites `content`" },
|
|
||||||
{
|
|
||||||
value: "symlink",
|
|
||||||
label: "Symlink an existing folder",
|
|
||||||
hint: "don't select this unless you know what you are doing!",
|
|
||||||
},
|
|
||||||
{ value: "keep", label: "Keep the existing files" },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
async function rmContentFolder() {
|
|
||||||
const contentStat = await fs.promises.lstat(contentFolder)
|
|
||||||
if (contentStat.isSymbolicLink()) {
|
|
||||||
await fs.promises.unlink(contentFolder)
|
|
||||||
} else {
|
|
||||||
await rimraf(contentFolder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setupStrategy === "copy" || setupStrategy === "symlink") {
|
|
||||||
const originalFolder = escapePath(
|
|
||||||
exitIfCancel(
|
|
||||||
await text({
|
|
||||||
message: "Enter the full path to existing content folder",
|
|
||||||
placeholder:
|
|
||||||
"On most terminal emulators, you can drag and drop a folder into the window and it will paste the full path",
|
|
||||||
validate(fp) {
|
|
||||||
const fullPath = escapePath(fp)
|
|
||||||
if (!fs.existsSync(fullPath)) {
|
|
||||||
return "The given path doesn't exist"
|
|
||||||
} else if (!fs.lstatSync(fullPath).isDirectory()) {
|
|
||||||
return "The given path is not a folder"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
await rmContentFolder()
|
|
||||||
if (setupStrategy === "copy") {
|
|
||||||
await fs.promises.cp(originalFolder, contentFolder, {
|
|
||||||
recursive: true,
|
|
||||||
preserveTimestamps: true,
|
|
||||||
})
|
|
||||||
} else if (setupStrategy === "symlink") {
|
|
||||||
await fs.promises.symlink(originalFolder, contentFolder, "dir")
|
|
||||||
}
|
|
||||||
} else if (setupStrategy === "new") {
|
|
||||||
await rmContentFolder()
|
|
||||||
await fs.promises.mkdir(contentFolder)
|
|
||||||
await fs.promises.writeFile(
|
|
||||||
path.join(contentFolder, "index.md"),
|
|
||||||
`---
|
|
||||||
title: Welcome to Quartz
|
|
||||||
---
|
|
||||||
|
|
||||||
This is a blank Quartz installation.
|
|
||||||
See the [documentation](https://quartz.jzhao.xyz) for how to get started.
|
|
||||||
`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get a prefered link resolution strategy
|
|
||||||
const linkResolutionStrategy = exitIfCancel(
|
|
||||||
await select({
|
|
||||||
message: `Choose how Quartz should resolve links in your content. You can change this later in \`quartz.config.ts\`.`,
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
value: "absolute",
|
|
||||||
label: "Treat links as absolute path",
|
|
||||||
hint: "for content made for Quartz 3 and Hugo",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "shortest",
|
|
||||||
label: "Treat links as shortest path",
|
|
||||||
hint: "for most Obsidian vaults",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "relative",
|
|
||||||
label: "Treat links as relative paths",
|
|
||||||
hint: "for just normal Markdown files",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
// now, do config changes
|
|
||||||
const configFilePath = path.join(cwd, "quartz.config.ts")
|
|
||||||
let configContent = await fs.promises.readFile(configFilePath, { encoding: "utf-8" })
|
|
||||||
configContent = configContent.replace(
|
|
||||||
/markdownLinkResolution: '(.+)'/,
|
|
||||||
`markdownLinkResolution: '${linkResolutionStrategy}'`,
|
|
||||||
)
|
|
||||||
await fs.promises.writeFile(configFilePath, configContent)
|
|
||||||
|
|
||||||
outro(`You're all set! Not sure what to do next? Try:
|
|
||||||
• Customizing Quartz a bit more by editing \`quartz.config.ts\`
|
|
||||||
• Running \`npx quartz build --serve\` to preview your Quartz locally
|
|
||||||
• Hosting your Quartz online (see: https://quartz.jzhao.xyz/setup/hosting)
|
|
||||||
`)
|
|
||||||
})
|
})
|
||||||
.command("update", "Get the latest Quartz updates", CommonArgv, async (argv) => {
|
.command("update", "Get the latest Quartz updates", CommonArgv, async (argv) => {
|
||||||
const contentFolder = path.join(cwd, argv.directory)
|
await handleUpdate(argv)
|
||||||
console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`))
|
|
||||||
console.log("Backing up your content")
|
|
||||||
execSync(
|
|
||||||
`git remote show upstream || git remote add upstream https://github.com/jackyzha0/quartz.git`,
|
|
||||||
)
|
|
||||||
await stashContentFolder(contentFolder)
|
|
||||||
console.log(
|
|
||||||
"Pulling updates... you may need to resolve some `git` conflicts if you've made changes to components or plugins.",
|
|
||||||
)
|
|
||||||
gitPull(UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH)
|
|
||||||
await popContentFolder(contentFolder)
|
|
||||||
console.log("Ensuring dependencies are up to date")
|
|
||||||
spawnSync("npm", ["i"], { stdio: "inherit" })
|
|
||||||
console.log(chalk.green("Done!"))
|
|
||||||
})
|
})
|
||||||
.command(
|
.command(
|
||||||
"restore",
|
"restore",
|
||||||
"Try to restore your content folder from the cache",
|
"Try to restore your content folder from the cache",
|
||||||
CommonArgv,
|
CommonArgv,
|
||||||
async (argv) => {
|
async (argv) => {
|
||||||
const contentFolder = path.join(cwd, argv.directory)
|
await handleRestore(argv)
|
||||||
await popContentFolder(contentFolder)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.command("sync", "Sync your Quartz to and from GitHub.", SyncArgv, async (argv) => {
|
.command("sync", "Sync your Quartz to and from GitHub.", SyncArgv, async (argv) => {
|
||||||
const contentFolder = path.join(cwd, argv.directory)
|
await handleSync(argv)
|
||||||
console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`))
|
|
||||||
console.log("Backing up your content")
|
|
||||||
|
|
||||||
if (argv.commit) {
|
|
||||||
const contentStat = await fs.promises.lstat(contentFolder)
|
|
||||||
if (contentStat.isSymbolicLink()) {
|
|
||||||
const linkTarg = await fs.promises.readlink(contentFolder)
|
|
||||||
console.log(chalk.yellow("Detected symlink, trying to dereference before committing"))
|
|
||||||
|
|
||||||
// stash symlink file
|
|
||||||
await stashContentFolder(contentFolder)
|
|
||||||
|
|
||||||
// follow symlink and copy content
|
|
||||||
await fs.promises.cp(linkTarg, contentFolder, {
|
|
||||||
recursive: true,
|
|
||||||
preserveTimestamps: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentTimestamp = new Date().toLocaleString("en-US", {
|
|
||||||
dateStyle: "medium",
|
|
||||||
timeStyle: "short",
|
|
||||||
})
|
|
||||||
spawnSync("git", ["add", "."], { stdio: "inherit" })
|
|
||||||
spawnSync("git", ["commit", "-m", `Quartz sync: ${currentTimestamp}`], { stdio: "inherit" })
|
|
||||||
|
|
||||||
if (contentStat.isSymbolicLink()) {
|
|
||||||
// put symlink back
|
|
||||||
await popContentFolder(contentFolder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await stashContentFolder(contentFolder)
|
|
||||||
|
|
||||||
if (argv.pull) {
|
|
||||||
console.log(
|
|
||||||
"Pulling updates from your repository. You may need to resolve some `git` conflicts if you've made changes to components or plugins.",
|
|
||||||
)
|
|
||||||
gitPull(ORIGIN_NAME, QUARTZ_SOURCE_BRANCH)
|
|
||||||
}
|
|
||||||
|
|
||||||
await popContentFolder(contentFolder)
|
|
||||||
if (argv.push) {
|
|
||||||
console.log("Pushing your changes")
|
|
||||||
spawnSync("git", ["push", "-f", ORIGIN_NAME, QUARTZ_SOURCE_BRANCH], { stdio: "inherit" })
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(chalk.green("Done!"))
|
|
||||||
})
|
})
|
||||||
.command("build", "Build Quartz into a bundle of static HTML files", BuildArgv, async (argv) => {
|
.command("build", "Build Quartz into a bundle of static HTML files", BuildArgv, async (argv) => {
|
||||||
console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`))
|
await handleBuild(argv)
|
||||||
const ctx = await esbuild.context({
|
|
||||||
entryPoints: [fp],
|
|
||||||
outfile: path.join("quartz", cacheFile),
|
|
||||||
bundle: true,
|
|
||||||
keepNames: true,
|
|
||||||
minifyWhitespace: true,
|
|
||||||
minifySyntax: true,
|
|
||||||
platform: "node",
|
|
||||||
format: "esm",
|
|
||||||
jsx: "automatic",
|
|
||||||
jsxImportSource: "preact",
|
|
||||||
packages: "external",
|
|
||||||
metafile: true,
|
|
||||||
sourcemap: true,
|
|
||||||
sourcesContent: false,
|
|
||||||
plugins: [
|
|
||||||
sassPlugin({
|
|
||||||
type: "css-text",
|
|
||||||
cssImports: true,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: "inline-script-loader",
|
|
||||||
setup(build) {
|
|
||||||
build.onLoad({ filter: /\.inline\.(ts|js)$/ }, async (args) => {
|
|
||||||
let text = await promises.readFile(args.path, "utf8")
|
|
||||||
|
|
||||||
// remove default exports that we manually inserted
|
|
||||||
text = text.replace("export default", "")
|
|
||||||
text = text.replace("export", "")
|
|
||||||
|
|
||||||
const sourcefile = path.relative(path.resolve("."), args.path)
|
|
||||||
const resolveDir = path.dirname(sourcefile)
|
|
||||||
const transpiled = await esbuild.build({
|
|
||||||
stdin: {
|
|
||||||
contents: text,
|
|
||||||
loader: "ts",
|
|
||||||
resolveDir,
|
|
||||||
sourcefile,
|
|
||||||
},
|
|
||||||
write: false,
|
|
||||||
bundle: true,
|
|
||||||
platform: "browser",
|
|
||||||
format: "esm",
|
|
||||||
})
|
|
||||||
const rawMod = transpiled.outputFiles[0].text
|
|
||||||
return {
|
|
||||||
contents: rawMod,
|
|
||||||
loader: "text",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
const buildMutex = new Mutex()
|
|
||||||
const timeoutIds = new Set()
|
|
||||||
const build = async (clientRefresh) => {
|
|
||||||
await buildMutex.acquire()
|
|
||||||
const result = await ctx.rebuild().catch((err) => {
|
|
||||||
console.error(`${chalk.red("Couldn't parse Quartz configuration:")} ${fp}`)
|
|
||||||
console.log(`Reason: ${chalk.grey(err)}`)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (argv.bundleInfo) {
|
|
||||||
const outputFileName = "quartz/.quartz-cache/transpiled-build.mjs"
|
|
||||||
const meta = result.metafile.outputs[outputFileName]
|
|
||||||
console.log(
|
|
||||||
`Successfully transpiled ${Object.keys(meta.inputs).length} files (${prettyBytes(
|
|
||||||
meta.bytes,
|
|
||||||
)})`,
|
|
||||||
)
|
|
||||||
console.log(await esbuild.analyzeMetafile(result.metafile, { color: true }))
|
|
||||||
}
|
|
||||||
|
|
||||||
// bypass module cache
|
|
||||||
// https://github.com/nodejs/modules/issues/307
|
|
||||||
const { default: buildQuartz } = await import(cacheFile + `?update=${randomUUID()}`)
|
|
||||||
await buildQuartz(argv, clientRefresh)
|
|
||||||
clientRefresh()
|
|
||||||
buildMutex.release()
|
|
||||||
}
|
|
||||||
|
|
||||||
const rebuild = (clientRefresh) => {
|
|
||||||
timeoutIds.forEach((id) => clearTimeout(id))
|
|
||||||
timeoutIds.add(setTimeout(() => build(clientRefresh), 250))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (argv.serve) {
|
|
||||||
const wss = new WebSocketServer({ port: 3001 })
|
|
||||||
const connections = []
|
|
||||||
wss.on("connection", (ws) => connections.push(ws))
|
|
||||||
const clientRefresh = () => connections.forEach((conn) => conn.send("rebuild"))
|
|
||||||
|
|
||||||
if (argv.baseDir !== "" && !argv.baseDir.startsWith("/")) {
|
|
||||||
argv.baseDir = "/" + argv.baseDir
|
|
||||||
}
|
|
||||||
|
|
||||||
await build(clientRefresh)
|
|
||||||
const server = http.createServer(async (req, res) => {
|
|
||||||
if (argv.baseDir && !req.url?.startsWith(argv.baseDir)) {
|
|
||||||
console.log(
|
|
||||||
chalk.red(
|
|
||||||
`[404] ${req.url} (warning: link outside of site, this is likely a Quartz bug)`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
res.writeHead(404)
|
|
||||||
res.end()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// strip baseDir prefix
|
|
||||||
req.url = req.url?.slice(argv.baseDir.length)
|
|
||||||
|
|
||||||
const serve = async () => {
|
|
||||||
await serveHandler(req, res, {
|
|
||||||
public: argv.output,
|
|
||||||
directoryListing: false,
|
|
||||||
})
|
|
||||||
const status = res.statusCode
|
|
||||||
const statusString =
|
|
||||||
status >= 200 && status < 300 ? chalk.green(`[${status}]`) : chalk.red(`[${status}]`)
|
|
||||||
console.log(statusString + chalk.grey(` ${argv.baseDir}${req.url}`))
|
|
||||||
}
|
|
||||||
|
|
||||||
const redirect = (newFp) => {
|
|
||||||
newFp = argv.baseDir + newFp
|
|
||||||
res.writeHead(302, {
|
|
||||||
Location: newFp,
|
|
||||||
})
|
|
||||||
console.log(chalk.yellow("[302]") + chalk.grey(` ${argv.baseDir}${req.url} -> ${newFp}`))
|
|
||||||
res.end()
|
|
||||||
}
|
|
||||||
|
|
||||||
let fp = req.url?.split("?")[0] ?? "/"
|
|
||||||
|
|
||||||
// handle redirects
|
|
||||||
if (fp.endsWith("/")) {
|
|
||||||
// /trailing/
|
|
||||||
// does /trailing/index.html exist? if so, serve it
|
|
||||||
const indexFp = path.posix.join(fp, "index.html")
|
|
||||||
if (fs.existsSync(path.posix.join(argv.output, indexFp))) {
|
|
||||||
req.url = fp
|
|
||||||
return serve()
|
|
||||||
}
|
|
||||||
|
|
||||||
// does /trailing.html exist? if so, redirect to /trailing
|
|
||||||
let base = fp.slice(0, -1)
|
|
||||||
if (path.extname(base) === "") {
|
|
||||||
base += ".html"
|
|
||||||
}
|
|
||||||
if (fs.existsSync(path.posix.join(argv.output, base))) {
|
|
||||||
return redirect(fp.slice(0, -1))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// /regular
|
|
||||||
// does /regular.html exist? if so, serve it
|
|
||||||
let base = fp
|
|
||||||
if (path.extname(base) === "") {
|
|
||||||
base += ".html"
|
|
||||||
}
|
|
||||||
if (fs.existsSync(path.posix.join(argv.output, base))) {
|
|
||||||
req.url = fp
|
|
||||||
return serve()
|
|
||||||
}
|
|
||||||
|
|
||||||
// does /regular/index.html exist? if so, redirect to /regular/
|
|
||||||
let indexFp = path.posix.join(fp, "index.html")
|
|
||||||
if (fs.existsSync(path.posix.join(argv.output, indexFp))) {
|
|
||||||
return redirect(fp + "/")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return serve()
|
|
||||||
})
|
|
||||||
server.listen(argv.port)
|
|
||||||
console.log(
|
|
||||||
chalk.cyan(
|
|
||||||
`Started a Quartz server listening at http://localhost:${argv.port}${argv.baseDir}`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
console.log("hint: exit with ctrl+c")
|
|
||||||
chokidar
|
|
||||||
.watch(["**/*.ts", "**/*.tsx", "**/*.scss", "package.json"], {
|
|
||||||
ignoreInitial: true,
|
|
||||||
})
|
|
||||||
.on("all", async () => {
|
|
||||||
console.log(chalk.yellow("Detected a source code change, doing a hard rebuild..."))
|
|
||||||
rebuild(clientRefresh)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await build(() => {})
|
|
||||||
ctx.dispose()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.showHelpOnFail(false)
|
.showHelpOnFail(false)
|
||||||
.help()
|
.help()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { trace } from "./util/trace"
|
|||||||
import { options } from "./util/sourcemap"
|
import { options } from "./util/sourcemap"
|
||||||
import { Mutex } from "async-mutex"
|
import { Mutex } from "async-mutex"
|
||||||
|
|
||||||
async function buildQuartz(argv: Argv, clientRefresh: () => void) {
|
async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
||||||
const ctx: BuildCtx = {
|
const ctx: BuildCtx = {
|
||||||
argv,
|
argv,
|
||||||
cfg,
|
cfg,
|
||||||
@@ -38,13 +38,14 @@ async function buildQuartz(argv: Argv, clientRefresh: () => void) {
|
|||||||
console.log(` Emitters: ${pluginNames("emitters").join(", ")}`)
|
console.log(` Emitters: ${pluginNames("emitters").join(", ")}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const release = await mut.acquire()
|
||||||
perf.addEvent("clean")
|
perf.addEvent("clean")
|
||||||
await rimraf(output)
|
await rimraf(output)
|
||||||
console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`)
|
console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`)
|
||||||
|
|
||||||
perf.addEvent("glob")
|
perf.addEvent("glob")
|
||||||
const allFiles = await glob("**/*.*", argv.directory, cfg.configuration.ignorePatterns)
|
const allFiles = await glob("**/*.*", argv.directory, cfg.configuration.ignorePatterns)
|
||||||
const fps = allFiles.filter((fp) => fp.endsWith(".md"))
|
const fps = allFiles.filter((fp) => fp.endsWith(".md")).sort()
|
||||||
console.log(
|
console.log(
|
||||||
`Found ${fps.length} input files from \`${argv.directory}\` in ${perf.timeSince("glob")}`,
|
`Found ${fps.length} input files from \`${argv.directory}\` in ${perf.timeSince("glob")}`,
|
||||||
)
|
)
|
||||||
@@ -56,15 +57,17 @@ async function buildQuartz(argv: Argv, clientRefresh: () => void) {
|
|||||||
const filteredContent = filterContent(ctx, parsedFiles)
|
const filteredContent = filterContent(ctx, parsedFiles)
|
||||||
await emitContent(ctx, filteredContent)
|
await emitContent(ctx, filteredContent)
|
||||||
console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`))
|
console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`))
|
||||||
|
release()
|
||||||
|
|
||||||
if (argv.serve) {
|
if (argv.serve) {
|
||||||
return startServing(ctx, parsedFiles, clientRefresh)
|
return startServing(ctx, mut, parsedFiles, clientRefresh)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup watcher for rebuilds
|
// setup watcher for rebuilds
|
||||||
async function startServing(
|
async function startServing(
|
||||||
ctx: BuildCtx,
|
ctx: BuildCtx,
|
||||||
|
mut: Mutex,
|
||||||
initialContent: ProcessedContent[],
|
initialContent: ProcessedContent[],
|
||||||
clientRefresh: () => void,
|
clientRefresh: () => void,
|
||||||
) {
|
) {
|
||||||
@@ -78,8 +81,7 @@ async function startServing(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initialSlugs = ctx.allSlugs
|
const initialSlugs = ctx.allSlugs
|
||||||
const buildMutex = new Mutex()
|
let lastBuildMs = 0
|
||||||
const timeoutIds: Set<ReturnType<typeof setTimeout>> = new Set()
|
|
||||||
const toRebuild: Set<FilePath> = new Set()
|
const toRebuild: Set<FilePath> = new Set()
|
||||||
const toRemove: Set<FilePath> = new Set()
|
const toRemove: Set<FilePath> = new Set()
|
||||||
const trackedAssets: Set<FilePath> = new Set()
|
const trackedAssets: Set<FilePath> = new Set()
|
||||||
@@ -108,47 +110,52 @@ async function startServing(
|
|||||||
toRemove.add(filePath)
|
toRemove.add(filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
timeoutIds.forEach((id) => clearTimeout(id))
|
|
||||||
|
|
||||||
// debounce rebuilds every 250ms
|
// debounce rebuilds every 250ms
|
||||||
timeoutIds.add(
|
|
||||||
setTimeout(async () => {
|
|
||||||
await buildMutex.acquire()
|
|
||||||
const perf = new PerfTimer()
|
|
||||||
console.log(chalk.yellow("Detected change, rebuilding..."))
|
|
||||||
try {
|
|
||||||
const filesToRebuild = [...toRebuild].filter((fp) => !toRemove.has(fp))
|
|
||||||
|
|
||||||
const trackedSlugs = [...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets])]
|
const buildStart = new Date().getTime()
|
||||||
.filter((fp) => !toRemove.has(fp))
|
lastBuildMs = buildStart
|
||||||
.map((fp) => slugifyFilePath(path.posix.relative(argv.directory, fp) as FilePath))
|
const release = await mut.acquire()
|
||||||
|
if (lastBuildMs > buildStart) {
|
||||||
|
release()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ctx.allSlugs = [...new Set([...initialSlugs, ...trackedSlugs])]
|
const perf = new PerfTimer()
|
||||||
const parsedContent = await parseMarkdown(ctx, filesToRebuild)
|
console.log(chalk.yellow("Detected change, rebuilding..."))
|
||||||
for (const content of parsedContent) {
|
try {
|
||||||
const [_tree, vfile] = content
|
const filesToRebuild = [...toRebuild].filter((fp) => !toRemove.has(fp))
|
||||||
contentMap.set(vfile.data.filePath!, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const fp of toRemove) {
|
const trackedSlugs = [...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets])]
|
||||||
contentMap.delete(fp)
|
.filter((fp) => !toRemove.has(fp))
|
||||||
}
|
.map((fp) => slugifyFilePath(path.posix.relative(argv.directory, fp) as FilePath))
|
||||||
|
|
||||||
await rimraf(argv.output)
|
ctx.allSlugs = [...new Set([...initialSlugs, ...trackedSlugs])]
|
||||||
const parsedFiles = [...contentMap.values()]
|
const parsedContent = await parseMarkdown(ctx, filesToRebuild)
|
||||||
const filteredContent = filterContent(ctx, parsedFiles)
|
for (const content of parsedContent) {
|
||||||
await emitContent(ctx, filteredContent)
|
const [_tree, vfile] = content
|
||||||
console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`))
|
contentMap.set(vfile.data.filePath!, content)
|
||||||
} catch {
|
}
|
||||||
console.log(chalk.yellow(`Rebuild failed. Waiting on a change to fix the error...`))
|
|
||||||
}
|
|
||||||
|
|
||||||
clientRefresh()
|
for (const fp of toRemove) {
|
||||||
toRebuild.clear()
|
contentMap.delete(fp)
|
||||||
toRemove.clear()
|
}
|
||||||
buildMutex.release()
|
|
||||||
}, 250),
|
const parsedFiles = [...contentMap.values()]
|
||||||
)
|
const filteredContent = filterContent(ctx, parsedFiles)
|
||||||
|
|
||||||
|
// TODO: we can probably traverse the link graph to figure out what's safe to delete here
|
||||||
|
// instead of just deleting everything
|
||||||
|
await rimraf(argv.output)
|
||||||
|
await emitContent(ctx, filteredContent)
|
||||||
|
console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`))
|
||||||
|
} catch {
|
||||||
|
console.log(chalk.yellow(`Rebuild failed. Waiting on a change to fix the error...`))
|
||||||
|
}
|
||||||
|
|
||||||
|
clientRefresh()
|
||||||
|
toRebuild.clear()
|
||||||
|
toRemove.clear()
|
||||||
|
release()
|
||||||
}
|
}
|
||||||
|
|
||||||
const watcher = chokidar.watch(".", {
|
const watcher = chokidar.watch(".", {
|
||||||
@@ -161,11 +168,15 @@ async function startServing(
|
|||||||
.on("add", (fp) => rebuild(fp, "add"))
|
.on("add", (fp) => rebuild(fp, "add"))
|
||||||
.on("change", (fp) => rebuild(fp, "change"))
|
.on("change", (fp) => rebuild(fp, "change"))
|
||||||
.on("unlink", (fp) => rebuild(fp, "delete"))
|
.on("unlink", (fp) => rebuild(fp, "delete"))
|
||||||
|
|
||||||
|
return async () => {
|
||||||
|
await watcher.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async (argv: Argv, clientRefresh: () => void) => {
|
export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => {
|
||||||
try {
|
try {
|
||||||
return await buildQuartz(argv, clientRefresh)
|
return await buildQuartz(argv, mut, clientRefresh)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
trace("\nExiting Quartz due to a fatal error", err as Error)
|
trace("\nExiting Quartz due to a fatal error", err as Error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ValidDateType } from "./components/Date"
|
||||||
import { QuartzComponent } from "./components/types"
|
import { QuartzComponent } from "./components/types"
|
||||||
import { PluginTypes } from "./plugins/types"
|
import { PluginTypes } from "./plugins/types"
|
||||||
import { Theme } from "./util/theme"
|
import { Theme } from "./util/theme"
|
||||||
@@ -11,6 +12,10 @@ export type Analytics =
|
|||||||
provider: "google"
|
provider: "google"
|
||||||
tagId: string
|
tagId: string
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
provider: "umami"
|
||||||
|
websiteId: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface GlobalConfiguration {
|
export interface GlobalConfiguration {
|
||||||
pageTitle: string
|
pageTitle: string
|
||||||
@@ -22,6 +27,8 @@ export interface GlobalConfiguration {
|
|||||||
analytics: Analytics
|
analytics: Analytics
|
||||||
/** Glob patterns to not search */
|
/** Glob patterns to not search */
|
||||||
ignorePatterns: string[]
|
ignorePatterns: string[]
|
||||||
|
/** Whether to use created, modified, or published as the default type of date */
|
||||||
|
defaultDateType: ValidDateType
|
||||||
/** Base URL to use for CNAME files, sitemaps, and RSS feeds that require an absolute URL.
|
/** Base URL to use for CNAME files, sitemaps, and RSS feeds that require an absolute URL.
|
||||||
* Quartz will avoid using this as much as possible and use relative URLs most of the time
|
* Quartz will avoid using this as much as possible and use relative URLs most of the time
|
||||||
*/
|
*/
|
||||||
|
|||||||
103
quartz/cli/args.js
Normal file
103
quartz/cli/args.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
export const CommonArgv = {
|
||||||
|
directory: {
|
||||||
|
string: true,
|
||||||
|
alias: ["d"],
|
||||||
|
default: "content",
|
||||||
|
describe: "directory to look for content files",
|
||||||
|
},
|
||||||
|
verbose: {
|
||||||
|
boolean: true,
|
||||||
|
alias: ["v"],
|
||||||
|
default: false,
|
||||||
|
describe: "print out extra logging information",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateArgv = {
|
||||||
|
...CommonArgv,
|
||||||
|
source: {
|
||||||
|
string: true,
|
||||||
|
alias: ["s"],
|
||||||
|
describe: "source directory to copy/create symlink from",
|
||||||
|
},
|
||||||
|
strategy: {
|
||||||
|
string: true,
|
||||||
|
alias: ["X"],
|
||||||
|
choices: ["new", "copy", "symlink"],
|
||||||
|
describe: "strategy for content folder setup",
|
||||||
|
},
|
||||||
|
links: {
|
||||||
|
string: true,
|
||||||
|
alias: ["l"],
|
||||||
|
choices: ["absolute", "shortest", "relative"],
|
||||||
|
describe: "strategy to resolve links",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SyncArgv = {
|
||||||
|
...CommonArgv,
|
||||||
|
commit: {
|
||||||
|
boolean: true,
|
||||||
|
default: true,
|
||||||
|
describe: "create a git commit for your unsaved changes",
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
string: true,
|
||||||
|
alias: ["m"],
|
||||||
|
describe: "option to override the default Quartz commit message",
|
||||||
|
},
|
||||||
|
push: {
|
||||||
|
boolean: true,
|
||||||
|
default: true,
|
||||||
|
describe: "push updates to your Quartz fork",
|
||||||
|
},
|
||||||
|
pull: {
|
||||||
|
boolean: true,
|
||||||
|
default: true,
|
||||||
|
describe: "pull updates from your Quartz fork",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BuildArgv = {
|
||||||
|
...CommonArgv,
|
||||||
|
output: {
|
||||||
|
string: true,
|
||||||
|
alias: ["o"],
|
||||||
|
default: "public",
|
||||||
|
describe: "output folder for files",
|
||||||
|
},
|
||||||
|
serve: {
|
||||||
|
boolean: true,
|
||||||
|
default: false,
|
||||||
|
describe: "run a local server to live-preview your Quartz",
|
||||||
|
},
|
||||||
|
baseDir: {
|
||||||
|
string: true,
|
||||||
|
default: "",
|
||||||
|
describe: "base path to serve your local server on",
|
||||||
|
},
|
||||||
|
port: {
|
||||||
|
number: true,
|
||||||
|
default: 8080,
|
||||||
|
describe: "port to serve Quartz on",
|
||||||
|
},
|
||||||
|
wsPort: {
|
||||||
|
number: true,
|
||||||
|
default: 3001,
|
||||||
|
describe: "port to use for WebSocket-based hot-reload notifications",
|
||||||
|
},
|
||||||
|
remoteDevHost: {
|
||||||
|
string: true,
|
||||||
|
default: "",
|
||||||
|
describe: "A URL override for the websocket connection if you are not developing on localhost",
|
||||||
|
},
|
||||||
|
bundleInfo: {
|
||||||
|
boolean: true,
|
||||||
|
default: false,
|
||||||
|
describe: "show detailed bundle information",
|
||||||
|
},
|
||||||
|
concurrency: {
|
||||||
|
number: true,
|
||||||
|
describe: "how many threads to use to parse notes",
|
||||||
|
},
|
||||||
|
}
|
||||||
15
quartz/cli/constants.js
Normal file
15
quartz/cli/constants.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import path from "path"
|
||||||
|
import { readFileSync } from "fs"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All constants relating to helpers or handlers
|
||||||
|
*/
|
||||||
|
export const ORIGIN_NAME = "origin"
|
||||||
|
export const UPSTREAM_NAME = "upstream"
|
||||||
|
export const QUARTZ_SOURCE_BRANCH = "v4"
|
||||||
|
export const cwd = process.cwd()
|
||||||
|
export const cacheDir = path.join(cwd, ".quartz-cache")
|
||||||
|
export const cacheFile = "./quartz/.quartz-cache/transpiled-build.mjs"
|
||||||
|
export const fp = "./quartz/build.ts"
|
||||||
|
export const { version } = JSON.parse(readFileSync("./package.json").toString())
|
||||||
|
export const contentCacheFolder = path.join(cacheDir, "content-cache")
|
||||||
512
quartz/cli/handlers.js
Normal file
512
quartz/cli/handlers.js
Normal file
@@ -0,0 +1,512 @@
|
|||||||
|
import { promises } from "fs"
|
||||||
|
import path from "path"
|
||||||
|
import esbuild from "esbuild"
|
||||||
|
import chalk from "chalk"
|
||||||
|
import { sassPlugin } from "esbuild-sass-plugin"
|
||||||
|
import fs from "fs"
|
||||||
|
import { intro, outro, select, text } from "@clack/prompts"
|
||||||
|
import { rimraf } from "rimraf"
|
||||||
|
import chokidar from "chokidar"
|
||||||
|
import prettyBytes from "pretty-bytes"
|
||||||
|
import { execSync, spawnSync } from "child_process"
|
||||||
|
import http from "http"
|
||||||
|
import serveHandler from "serve-handler"
|
||||||
|
import { WebSocketServer } from "ws"
|
||||||
|
import { randomUUID } from "crypto"
|
||||||
|
import { Mutex } from "async-mutex"
|
||||||
|
import { CreateArgv } from "./args.js"
|
||||||
|
import {
|
||||||
|
exitIfCancel,
|
||||||
|
escapePath,
|
||||||
|
gitPull,
|
||||||
|
popContentFolder,
|
||||||
|
stashContentFolder,
|
||||||
|
} from "./helpers.js"
|
||||||
|
import {
|
||||||
|
UPSTREAM_NAME,
|
||||||
|
QUARTZ_SOURCE_BRANCH,
|
||||||
|
ORIGIN_NAME,
|
||||||
|
version,
|
||||||
|
fp,
|
||||||
|
cacheFile,
|
||||||
|
cwd,
|
||||||
|
} from "./constants.js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles `npx quartz create`
|
||||||
|
* @param {*} argv arguments for `create`
|
||||||
|
*/
|
||||||
|
export async function handleCreate(argv) {
|
||||||
|
console.log()
|
||||||
|
intro(chalk.bgGreen.black(` Quartz v${version} `))
|
||||||
|
const contentFolder = path.join(cwd, argv.directory)
|
||||||
|
let setupStrategy = argv.strategy?.toLowerCase()
|
||||||
|
let linkResolutionStrategy = argv.links?.toLowerCase()
|
||||||
|
const sourceDirectory = argv.source
|
||||||
|
|
||||||
|
// If all cmd arguments were provided, check if theyre valid
|
||||||
|
if (setupStrategy && linkResolutionStrategy) {
|
||||||
|
// If setup isn't, "new", source argument is required
|
||||||
|
if (setupStrategy !== "new") {
|
||||||
|
// Error handling
|
||||||
|
if (!sourceDirectory) {
|
||||||
|
outro(
|
||||||
|
chalk.red(
|
||||||
|
`Setup strategies (arg '${chalk.yellow(
|
||||||
|
`-${CreateArgv.strategy.alias[0]}`,
|
||||||
|
)}') other than '${chalk.yellow(
|
||||||
|
"new",
|
||||||
|
)}' require content folder argument ('${chalk.yellow(
|
||||||
|
`-${CreateArgv.source.alias[0]}`,
|
||||||
|
)}') to be set`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
process.exit(1)
|
||||||
|
} else {
|
||||||
|
if (!fs.existsSync(sourceDirectory)) {
|
||||||
|
outro(
|
||||||
|
chalk.red(
|
||||||
|
`Input directory to copy/symlink 'content' from not found ('${chalk.yellow(
|
||||||
|
sourceDirectory,
|
||||||
|
)}', invalid argument "${chalk.yellow(`-${CreateArgv.source.alias[0]}`)})`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
process.exit(1)
|
||||||
|
} else if (!fs.lstatSync(sourceDirectory).isDirectory()) {
|
||||||
|
outro(
|
||||||
|
chalk.red(
|
||||||
|
`Source directory to copy/symlink 'content' from is not a directory (found file at '${chalk.yellow(
|
||||||
|
sourceDirectory,
|
||||||
|
)}', invalid argument ${chalk.yellow(`-${CreateArgv.source.alias[0]}`)}")`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use cli process if cmd args werent provided
|
||||||
|
if (!setupStrategy) {
|
||||||
|
setupStrategy = exitIfCancel(
|
||||||
|
await select({
|
||||||
|
message: `Choose how to initialize the content in \`${contentFolder}\``,
|
||||||
|
options: [
|
||||||
|
{ value: "new", label: "Empty Quartz" },
|
||||||
|
{ value: "copy", label: "Copy an existing folder", hint: "overwrites `content`" },
|
||||||
|
{
|
||||||
|
value: "symlink",
|
||||||
|
label: "Symlink an existing folder",
|
||||||
|
hint: "don't select this unless you know what you are doing!",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rmContentFolder() {
|
||||||
|
const contentStat = await fs.promises.lstat(contentFolder)
|
||||||
|
if (contentStat.isSymbolicLink()) {
|
||||||
|
await fs.promises.unlink(contentFolder)
|
||||||
|
} else {
|
||||||
|
await rimraf(contentFolder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.promises.unlink(path.join(contentFolder, ".gitkeep"))
|
||||||
|
if (setupStrategy === "copy" || setupStrategy === "symlink") {
|
||||||
|
let originalFolder = sourceDirectory
|
||||||
|
|
||||||
|
// If input directory was not passed, use cli
|
||||||
|
if (!sourceDirectory) {
|
||||||
|
originalFolder = escapePath(
|
||||||
|
exitIfCancel(
|
||||||
|
await text({
|
||||||
|
message: "Enter the full path to existing content folder",
|
||||||
|
placeholder:
|
||||||
|
"On most terminal emulators, you can drag and drop a folder into the window and it will paste the full path",
|
||||||
|
validate(fp) {
|
||||||
|
const fullPath = escapePath(fp)
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
return "The given path doesn't exist"
|
||||||
|
} else if (!fs.lstatSync(fullPath).isDirectory()) {
|
||||||
|
return "The given path is not a folder"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await rmContentFolder()
|
||||||
|
if (setupStrategy === "copy") {
|
||||||
|
await fs.promises.cp(originalFolder, contentFolder, {
|
||||||
|
recursive: true,
|
||||||
|
preserveTimestamps: true,
|
||||||
|
})
|
||||||
|
} else if (setupStrategy === "symlink") {
|
||||||
|
await fs.promises.symlink(originalFolder, contentFolder, "dir")
|
||||||
|
}
|
||||||
|
} else if (setupStrategy === "new") {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(contentFolder, "index.md"),
|
||||||
|
`---
|
||||||
|
title: Welcome to Quartz
|
||||||
|
---
|
||||||
|
|
||||||
|
This is a blank Quartz installation.
|
||||||
|
See the [documentation](https://quartz.jzhao.xyz) for how to get started.
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use cli process if cmd args werent provided
|
||||||
|
if (!linkResolutionStrategy) {
|
||||||
|
// get a preferred link resolution strategy
|
||||||
|
linkResolutionStrategy = exitIfCancel(
|
||||||
|
await select({
|
||||||
|
message: `Choose how Quartz should resolve links in your content. You can change this later in \`quartz.config.ts\`.`,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "absolute",
|
||||||
|
label: "Treat links as absolute path",
|
||||||
|
hint: "for content made for Quartz 3 and Hugo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "shortest",
|
||||||
|
label: "Treat links as shortest path",
|
||||||
|
hint: "for most Obsidian vaults",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "relative",
|
||||||
|
label: "Treat links as relative paths",
|
||||||
|
hint: "for just normal Markdown files",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// now, do config changes
|
||||||
|
const configFilePath = path.join(cwd, "quartz.config.ts")
|
||||||
|
let configContent = await fs.promises.readFile(configFilePath, { encoding: "utf-8" })
|
||||||
|
configContent = configContent.replace(
|
||||||
|
/markdownLinkResolution: '(.+)'/,
|
||||||
|
`markdownLinkResolution: '${linkResolutionStrategy}'`,
|
||||||
|
)
|
||||||
|
await fs.promises.writeFile(configFilePath, configContent)
|
||||||
|
|
||||||
|
outro(`You're all set! Not sure what to do next? Try:
|
||||||
|
• Customizing Quartz a bit more by editing \`quartz.config.ts\`
|
||||||
|
• Running \`npx quartz build --serve\` to preview your Quartz locally
|
||||||
|
• Hosting your Quartz online (see: https://quartz.jzhao.xyz/hosting)
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles `npx quartz build`
|
||||||
|
* @param {*} argv arguments for `build`
|
||||||
|
*/
|
||||||
|
export async function handleBuild(argv) {
|
||||||
|
console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`))
|
||||||
|
const ctx = await esbuild.context({
|
||||||
|
entryPoints: [fp],
|
||||||
|
outfile: cacheFile,
|
||||||
|
bundle: true,
|
||||||
|
keepNames: true,
|
||||||
|
minifyWhitespace: true,
|
||||||
|
minifySyntax: true,
|
||||||
|
platform: "node",
|
||||||
|
format: "esm",
|
||||||
|
jsx: "automatic",
|
||||||
|
jsxImportSource: "preact",
|
||||||
|
packages: "external",
|
||||||
|
metafile: true,
|
||||||
|
sourcemap: true,
|
||||||
|
sourcesContent: false,
|
||||||
|
plugins: [
|
||||||
|
sassPlugin({
|
||||||
|
type: "css-text",
|
||||||
|
cssImports: true,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "inline-script-loader",
|
||||||
|
setup(build) {
|
||||||
|
build.onLoad({ filter: /\.inline\.(ts|js)$/ }, async (args) => {
|
||||||
|
let text = await promises.readFile(args.path, "utf8")
|
||||||
|
|
||||||
|
// remove default exports that we manually inserted
|
||||||
|
text = text.replace("export default", "")
|
||||||
|
text = text.replace("export", "")
|
||||||
|
|
||||||
|
const sourcefile = path.relative(path.resolve("."), args.path)
|
||||||
|
const resolveDir = path.dirname(sourcefile)
|
||||||
|
const transpiled = await esbuild.build({
|
||||||
|
stdin: {
|
||||||
|
contents: text,
|
||||||
|
loader: "ts",
|
||||||
|
resolveDir,
|
||||||
|
sourcefile,
|
||||||
|
},
|
||||||
|
write: false,
|
||||||
|
bundle: true,
|
||||||
|
platform: "browser",
|
||||||
|
format: "esm",
|
||||||
|
})
|
||||||
|
const rawMod = transpiled.outputFiles[0].text
|
||||||
|
return {
|
||||||
|
contents: rawMod,
|
||||||
|
loader: "text",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const buildMutex = new Mutex()
|
||||||
|
let lastBuildMs = 0
|
||||||
|
let cleanupBuild = null
|
||||||
|
const build = async (clientRefresh) => {
|
||||||
|
const buildStart = new Date().getTime()
|
||||||
|
lastBuildMs = buildStart
|
||||||
|
const release = await buildMutex.acquire()
|
||||||
|
if (lastBuildMs > buildStart) {
|
||||||
|
release()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanupBuild) {
|
||||||
|
await cleanupBuild()
|
||||||
|
console.log(chalk.yellow("Detected a source code change, doing a hard rebuild..."))
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await ctx.rebuild().catch((err) => {
|
||||||
|
console.error(`${chalk.red("Couldn't parse Quartz configuration:")} ${fp}`)
|
||||||
|
console.log(`Reason: ${chalk.grey(err)}`)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
release()
|
||||||
|
|
||||||
|
if (argv.bundleInfo) {
|
||||||
|
const outputFileName = "quartz/.quartz-cache/transpiled-build.mjs"
|
||||||
|
const meta = result.metafile.outputs[outputFileName]
|
||||||
|
console.log(
|
||||||
|
`Successfully transpiled ${Object.keys(meta.inputs).length} files (${prettyBytes(
|
||||||
|
meta.bytes,
|
||||||
|
)})`,
|
||||||
|
)
|
||||||
|
console.log(await esbuild.analyzeMetafile(result.metafile, { color: true }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// bypass module cache
|
||||||
|
// https://github.com/nodejs/modules/issues/307
|
||||||
|
const { default: buildQuartz } = await import(`../../${cacheFile}?update=${randomUUID()}`)
|
||||||
|
// ^ this import is relative, so base "cacheFile" path can't be used
|
||||||
|
|
||||||
|
cleanupBuild = await buildQuartz(argv, buildMutex, clientRefresh)
|
||||||
|
clientRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argv.serve) {
|
||||||
|
const connections = []
|
||||||
|
const clientRefresh = () => connections.forEach((conn) => conn.send("rebuild"))
|
||||||
|
|
||||||
|
if (argv.baseDir !== "" && !argv.baseDir.startsWith("/")) {
|
||||||
|
argv.baseDir = "/" + argv.baseDir
|
||||||
|
}
|
||||||
|
|
||||||
|
await build(clientRefresh)
|
||||||
|
const server = http.createServer(async (req, res) => {
|
||||||
|
if (argv.baseDir && !req.url?.startsWith(argv.baseDir)) {
|
||||||
|
console.log(
|
||||||
|
chalk.red(
|
||||||
|
`[404] ${req.url} (warning: link outside of site, this is likely a Quartz bug)`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
res.writeHead(404)
|
||||||
|
res.end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// strip baseDir prefix
|
||||||
|
req.url = req.url?.slice(argv.baseDir.length)
|
||||||
|
|
||||||
|
const serve = async () => {
|
||||||
|
const release = await buildMutex.acquire()
|
||||||
|
await serveHandler(req, res, {
|
||||||
|
public: argv.output,
|
||||||
|
directoryListing: false,
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
source: "**/*.html",
|
||||||
|
headers: [{ key: "Content-Disposition", value: "inline" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
const status = res.statusCode
|
||||||
|
const statusString =
|
||||||
|
status >= 200 && status < 300 ? chalk.green(`[${status}]`) : chalk.red(`[${status}]`)
|
||||||
|
console.log(statusString + chalk.grey(` ${argv.baseDir}${req.url}`))
|
||||||
|
release()
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirect = (newFp) => {
|
||||||
|
newFp = argv.baseDir + newFp
|
||||||
|
res.writeHead(302, {
|
||||||
|
Location: newFp,
|
||||||
|
})
|
||||||
|
console.log(chalk.yellow("[302]") + chalk.grey(` ${argv.baseDir}${req.url} -> ${newFp}`))
|
||||||
|
res.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
let fp = req.url?.split("?")[0] ?? "/"
|
||||||
|
|
||||||
|
// handle redirects
|
||||||
|
if (fp.endsWith("/")) {
|
||||||
|
// /trailing/
|
||||||
|
// does /trailing/index.html exist? if so, serve it
|
||||||
|
const indexFp = path.posix.join(fp, "index.html")
|
||||||
|
if (fs.existsSync(path.posix.join(argv.output, indexFp))) {
|
||||||
|
req.url = fp
|
||||||
|
return serve()
|
||||||
|
}
|
||||||
|
|
||||||
|
// does /trailing.html exist? if so, redirect to /trailing
|
||||||
|
let base = fp.slice(0, -1)
|
||||||
|
if (path.extname(base) === "") {
|
||||||
|
base += ".html"
|
||||||
|
}
|
||||||
|
if (fs.existsSync(path.posix.join(argv.output, base))) {
|
||||||
|
return redirect(fp.slice(0, -1))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// /regular
|
||||||
|
// does /regular.html exist? if so, serve it
|
||||||
|
let base = fp
|
||||||
|
if (path.extname(base) === "") {
|
||||||
|
base += ".html"
|
||||||
|
}
|
||||||
|
if (fs.existsSync(path.posix.join(argv.output, base))) {
|
||||||
|
req.url = fp
|
||||||
|
return serve()
|
||||||
|
}
|
||||||
|
|
||||||
|
// does /regular/index.html exist? if so, redirect to /regular/
|
||||||
|
let indexFp = path.posix.join(fp, "index.html")
|
||||||
|
if (fs.existsSync(path.posix.join(argv.output, indexFp))) {
|
||||||
|
return redirect(fp + "/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return serve()
|
||||||
|
})
|
||||||
|
server.listen(argv.port)
|
||||||
|
const wss = new WebSocketServer({ port: argv.wsPort })
|
||||||
|
wss.on("connection", (ws) => connections.push(ws))
|
||||||
|
console.log(
|
||||||
|
chalk.cyan(
|
||||||
|
`Started a Quartz server listening at http://localhost:${argv.port}${argv.baseDir}`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
console.log("hint: exit with ctrl+c")
|
||||||
|
chokidar
|
||||||
|
.watch(["**/*.ts", "**/*.tsx", "**/*.scss", "package.json"], {
|
||||||
|
ignoreInitial: true,
|
||||||
|
})
|
||||||
|
.on("all", async () => {
|
||||||
|
build(clientRefresh)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await build(() => {})
|
||||||
|
ctx.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles `npx quartz update`
|
||||||
|
* @param {*} argv arguments for `update`
|
||||||
|
*/
|
||||||
|
export async function handleUpdate(argv) {
|
||||||
|
const contentFolder = path.join(cwd, argv.directory)
|
||||||
|
console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`))
|
||||||
|
console.log("Backing up your content")
|
||||||
|
execSync(
|
||||||
|
`git remote show upstream || git remote add upstream https://github.com/jackyzha0/quartz.git`,
|
||||||
|
)
|
||||||
|
await stashContentFolder(contentFolder)
|
||||||
|
console.log(
|
||||||
|
"Pulling updates... you may need to resolve some `git` conflicts if you've made changes to components or plugins.",
|
||||||
|
)
|
||||||
|
gitPull(UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH)
|
||||||
|
await popContentFolder(contentFolder)
|
||||||
|
console.log("Ensuring dependencies are up to date")
|
||||||
|
spawnSync("npm", ["i"], { stdio: "inherit" })
|
||||||
|
console.log(chalk.green("Done!"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles `npx quartz restore`
|
||||||
|
* @param {*} argv arguments for `restore`
|
||||||
|
*/
|
||||||
|
export async function handleRestore(argv) {
|
||||||
|
const contentFolder = path.join(cwd, argv.directory)
|
||||||
|
await popContentFolder(contentFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles `npx quartz sync`
|
||||||
|
* @param {*} argv arguments for `sync`
|
||||||
|
*/
|
||||||
|
export async function handleSync(argv) {
|
||||||
|
const contentFolder = path.join(cwd, argv.directory)
|
||||||
|
console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`))
|
||||||
|
console.log("Backing up your content")
|
||||||
|
|
||||||
|
if (argv.commit) {
|
||||||
|
const contentStat = await fs.promises.lstat(contentFolder)
|
||||||
|
if (contentStat.isSymbolicLink()) {
|
||||||
|
const linkTarg = await fs.promises.readlink(contentFolder)
|
||||||
|
console.log(chalk.yellow("Detected symlink, trying to dereference before committing"))
|
||||||
|
|
||||||
|
// stash symlink file
|
||||||
|
await stashContentFolder(contentFolder)
|
||||||
|
|
||||||
|
// follow symlink and copy content
|
||||||
|
await fs.promises.cp(linkTarg, contentFolder, {
|
||||||
|
recursive: true,
|
||||||
|
preserveTimestamps: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTimestamp = new Date().toLocaleString("en-US", {
|
||||||
|
dateStyle: "medium",
|
||||||
|
timeStyle: "short",
|
||||||
|
})
|
||||||
|
const commitMessage = argv.message ?? `Quartz sync: ${currentTimestamp}`
|
||||||
|
spawnSync("git", ["add", "."], { stdio: "inherit" })
|
||||||
|
spawnSync("git", ["commit", "-m", commitMessage], { stdio: "inherit" })
|
||||||
|
|
||||||
|
if (contentStat.isSymbolicLink()) {
|
||||||
|
// put symlink back
|
||||||
|
await popContentFolder(contentFolder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await stashContentFolder(contentFolder)
|
||||||
|
|
||||||
|
if (argv.pull) {
|
||||||
|
console.log(
|
||||||
|
"Pulling updates from your repository. You may need to resolve some `git` conflicts if you've made changes to components or plugins.",
|
||||||
|
)
|
||||||
|
gitPull(ORIGIN_NAME, QUARTZ_SOURCE_BRANCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
await popContentFolder(contentFolder)
|
||||||
|
if (argv.push) {
|
||||||
|
console.log("Pushing your changes")
|
||||||
|
spawnSync("git", ["push", "-f", ORIGIN_NAME, QUARTZ_SOURCE_BRANCH], { stdio: "inherit" })
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(chalk.green("Done!"))
|
||||||
|
}
|
||||||
52
quartz/cli/helpers.js
Normal file
52
quartz/cli/helpers.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { isCancel, outro } from "@clack/prompts"
|
||||||
|
import chalk from "chalk"
|
||||||
|
import { contentCacheFolder } from "./constants.js"
|
||||||
|
import { spawnSync } from "child_process"
|
||||||
|
import fs from "fs"
|
||||||
|
|
||||||
|
export function escapePath(fp) {
|
||||||
|
return fp
|
||||||
|
.replace(/\\ /g, " ") // unescape spaces
|
||||||
|
.replace(/^".*"$/, "$1")
|
||||||
|
.replace(/^'.*"$/, "$1")
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exitIfCancel(val) {
|
||||||
|
if (isCancel(val)) {
|
||||||
|
outro(chalk.red("Exiting"))
|
||||||
|
process.exit(0)
|
||||||
|
} else {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stashContentFolder(contentFolder) {
|
||||||
|
await fs.promises.rm(contentCacheFolder, { force: true, recursive: true })
|
||||||
|
await fs.promises.cp(contentFolder, contentCacheFolder, {
|
||||||
|
force: true,
|
||||||
|
recursive: true,
|
||||||
|
verbatimSymlinks: true,
|
||||||
|
preserveTimestamps: true,
|
||||||
|
})
|
||||||
|
await fs.promises.rm(contentFolder, { force: true, recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gitPull(origin, branch) {
|
||||||
|
const flags = ["--no-rebase", "--autostash", "-s", "recursive", "-X", "ours", "--no-edit"]
|
||||||
|
const out = spawnSync("git", ["pull", ...flags, origin, branch], { stdio: "inherit" })
|
||||||
|
if (out.stderr) {
|
||||||
|
throw new Error(`Error while pulling updates: ${out.stderr}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function popContentFolder(contentFolder) {
|
||||||
|
await fs.promises.rm(contentFolder, { force: true, recursive: true })
|
||||||
|
await fs.promises.cp(contentCacheFolder, contentFolder, {
|
||||||
|
force: true,
|
||||||
|
recursive: true,
|
||||||
|
verbatimSymlinks: true,
|
||||||
|
preserveTimestamps: true,
|
||||||
|
})
|
||||||
|
await fs.promises.rm(contentCacheFolder, { force: true, recursive: true })
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
|
|
||||||
function ArticleTitle({ fileData }: QuartzComponentProps) {
|
function ArticleTitle({ fileData, displayClass }: QuartzComponentProps) {
|
||||||
const title = fileData.frontmatter?.title
|
const title = fileData.frontmatter?.title
|
||||||
if (title) {
|
if (title) {
|
||||||
return <h1 class="article-title">{title}</h1>
|
return <h1 class={`article-title ${displayClass ?? ""}`}>{title}</h1>
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
|||||||
import style from "./styles/backlinks.scss"
|
import style from "./styles/backlinks.scss"
|
||||||
import { resolveRelative, simplifySlug } from "../util/path"
|
import { resolveRelative, simplifySlug } from "../util/path"
|
||||||
|
|
||||||
function Backlinks({ fileData, allFiles }: QuartzComponentProps) {
|
function Backlinks({ fileData, allFiles, displayClass }: QuartzComponentProps) {
|
||||||
const slug = simplifySlug(fileData.slug!)
|
const slug = simplifySlug(fileData.slug!)
|
||||||
const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
|
const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
|
||||||
return (
|
return (
|
||||||
<div class="backlinks">
|
<div class={`backlinks ${displayClass ?? ""}`}>
|
||||||
<h3>Backlinks</h3>
|
<h3>Backlinks</h3>
|
||||||
<ul class="overflow">
|
<ul class="overflow">
|
||||||
{backlinkFiles.length > 0 ? (
|
{backlinkFiles.length > 0 ? (
|
||||||
|
|||||||
123
quartz/components/Breadcrumbs.tsx
Normal file
123
quartz/components/Breadcrumbs.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
|
import breadcrumbsStyle from "./styles/breadcrumbs.scss"
|
||||||
|
import { FullSlug, SimpleSlug, resolveRelative } from "../util/path"
|
||||||
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
|
|
||||||
|
type CrumbData = {
|
||||||
|
displayName: string
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BreadcrumbOptions {
|
||||||
|
/**
|
||||||
|
* Symbol between crumbs
|
||||||
|
*/
|
||||||
|
spacerSymbol: string
|
||||||
|
/**
|
||||||
|
* Name of first crumb
|
||||||
|
*/
|
||||||
|
rootName: string
|
||||||
|
/**
|
||||||
|
* wether to look up frontmatter title for folders (could cause performance problems with big vaults)
|
||||||
|
*/
|
||||||
|
resolveFrontmatterTitle: boolean
|
||||||
|
/**
|
||||||
|
* Wether to display breadcrumbs on root `index.md`
|
||||||
|
*/
|
||||||
|
hideOnRoot: boolean
|
||||||
|
/**
|
||||||
|
* Wether to display the current page in the breadcrumbs.
|
||||||
|
*/
|
||||||
|
showCurrentPage: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultOptions: BreadcrumbOptions = {
|
||||||
|
spacerSymbol: "❯",
|
||||||
|
rootName: "Home",
|
||||||
|
resolveFrontmatterTitle: true,
|
||||||
|
hideOnRoot: true,
|
||||||
|
showCurrentPage: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: SimpleSlug): CrumbData {
|
||||||
|
return {
|
||||||
|
displayName: displayName.replaceAll("-", " "),
|
||||||
|
path: resolveRelative(baseSlug, currentSlug),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ((opts?: Partial<BreadcrumbOptions>) => {
|
||||||
|
// Merge options with defaults
|
||||||
|
const options: BreadcrumbOptions = { ...defaultOptions, ...opts }
|
||||||
|
|
||||||
|
// computed index of folder name to its associated file data
|
||||||
|
let folderIndex: Map<string, QuartzPluginData> | undefined
|
||||||
|
|
||||||
|
function Breadcrumbs({ fileData, allFiles, displayClass }: QuartzComponentProps) {
|
||||||
|
// Hide crumbs on root if enabled
|
||||||
|
if (options.hideOnRoot && fileData.slug === "index") {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format entry for root element
|
||||||
|
const firstEntry = formatCrumb(options.rootName, fileData.slug!, "/" as SimpleSlug)
|
||||||
|
const crumbs: CrumbData[] = [firstEntry]
|
||||||
|
|
||||||
|
if (!folderIndex && options.resolveFrontmatterTitle) {
|
||||||
|
folderIndex = new Map()
|
||||||
|
// construct the index for the first time
|
||||||
|
for (const file of allFiles) {
|
||||||
|
if (file.slug?.endsWith("index")) {
|
||||||
|
const folderParts = file.filePath?.split("/")
|
||||||
|
if (folderParts) {
|
||||||
|
const folderName = folderParts[folderParts?.length - 2]
|
||||||
|
folderIndex.set(folderName, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split slug into hierarchy/parts
|
||||||
|
const slugParts = fileData.slug?.split("/")
|
||||||
|
if (slugParts) {
|
||||||
|
// full path until current part
|
||||||
|
let currentPath = ""
|
||||||
|
for (let i = 0; i < slugParts.length - 1; i++) {
|
||||||
|
let curPathSegment = slugParts[i]
|
||||||
|
|
||||||
|
// Try to resolve frontmatter folder title
|
||||||
|
const currentFile = folderIndex?.get(curPathSegment)
|
||||||
|
if (currentFile) {
|
||||||
|
curPathSegment = currentFile.frontmatter!.title
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add current slug to full path
|
||||||
|
currentPath += slugParts[i] + "/"
|
||||||
|
|
||||||
|
// Format and add current crumb
|
||||||
|
const crumb = formatCrumb(curPathSegment, fileData.slug!, currentPath as SimpleSlug)
|
||||||
|
crumbs.push(crumb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add current file to crumb (can directly use frontmatter title)
|
||||||
|
if (options.showCurrentPage) {
|
||||||
|
crumbs.push({
|
||||||
|
displayName: fileData.frontmatter!.title,
|
||||||
|
path: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<nav class={`breadcrumb-container ${displayClass ?? ""}`} aria-label="breadcrumbs">
|
||||||
|
{crumbs.map((crumb, index) => (
|
||||||
|
<div class="breadcrumb-element">
|
||||||
|
<a href={crumb.path}>{crumb.displayName}</a>
|
||||||
|
{index !== crumbs.length - 1 && <p>{` ${options.spacerSymbol} `}</p>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Breadcrumbs.css = breadcrumbsStyle
|
||||||
|
return Breadcrumbs
|
||||||
|
}) satisfies QuartzComponentConstructor
|
||||||
@@ -1,19 +1,20 @@
|
|||||||
import { formatDate } from "./Date"
|
import { formatDate, getDate } from "./Date"
|
||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import readingTime from "reading-time"
|
import readingTime from "reading-time"
|
||||||
|
|
||||||
export default (() => {
|
export default (() => {
|
||||||
function ContentMetadata({ fileData }: QuartzComponentProps) {
|
function ContentMetadata({ cfg, fileData, displayClass }: QuartzComponentProps) {
|
||||||
const text = fileData.text
|
const text = fileData.text
|
||||||
if (text) {
|
if (text) {
|
||||||
const segments: string[] = []
|
const segments: string[] = []
|
||||||
const { text: timeTaken, words: _words } = readingTime(text)
|
const { text: timeTaken, words: _words } = readingTime(text)
|
||||||
if (fileData.dates?.modified) {
|
|
||||||
segments.push(formatDate(fileData.dates.modified))
|
if (fileData.dates) {
|
||||||
|
segments.push(formatDate(getDate(cfg, fileData)!))
|
||||||
}
|
}
|
||||||
|
|
||||||
segments.push(timeTaken)
|
segments.push(timeTaken)
|
||||||
return <p class="content-meta">{segments.join(", ")}</p>
|
return <p class={`content-meta ${displayClass ?? ""}`}>{segments.join(", ")}</p>
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
// see: https://v8.dev/features/modules#defer
|
// see: https://v8.dev/features/modules#defer
|
||||||
import darkmodeScript from "./scripts/darkmode.inline"
|
import darkmodeScript from "./scripts/darkmode.inline"
|
||||||
import styles from "./styles/darkmode.scss"
|
import styles from "./styles/darkmode.scss"
|
||||||
import { QuartzComponentConstructor } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
|
|
||||||
function Darkmode() {
|
function Darkmode({ displayClass }: QuartzComponentProps) {
|
||||||
return (
|
return (
|
||||||
<div class="darkmode">
|
<div class={`darkmode ${displayClass ?? ""}`}>
|
||||||
<input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} />
|
<input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} />
|
||||||
<label id="toggle-label-light" for="darkmode-toggle" tabIndex={-1}>
|
<label id="toggle-label-light" for="darkmode-toggle" tabIndex={-1}>
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -1,7 +1,21 @@
|
|||||||
|
import { GlobalConfiguration } from "../cfg"
|
||||||
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
date: Date
|
date: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ValidDateType = keyof Required<QuartzPluginData>["dates"]
|
||||||
|
|
||||||
|
export function getDate(cfg: GlobalConfiguration, data: QuartzPluginData): Date | undefined {
|
||||||
|
if (!cfg.defaultDateType) {
|
||||||
|
throw new Error(
|
||||||
|
`Field 'defaultDateType' was not set in the configuration object of quartz.config.ts. See https://quartz.jzhao.xyz/configuration#general-configuration for more details.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return data.dates?.[cfg.defaultDateType]
|
||||||
|
}
|
||||||
|
|
||||||
export function formatDate(d: Date): string {
|
export function formatDate(d: Date): string {
|
||||||
return d.toLocaleDateString("en-US", {
|
return d.toLocaleDateString("en-US", {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
|
|||||||
126
quartz/components/Explorer.tsx
Normal file
126
quartz/components/Explorer.tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
|
import explorerStyle from "./styles/explorer.scss"
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import script from "./scripts/explorer.inline"
|
||||||
|
import { ExplorerNode, FileNode, Options } from "./ExplorerNode"
|
||||||
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
|
|
||||||
|
// Options interface defined in `ExplorerNode` to avoid circular dependency
|
||||||
|
const defaultOptions = {
|
||||||
|
title: "Explorer",
|
||||||
|
folderClickBehavior: "collapse",
|
||||||
|
folderDefaultState: "collapsed",
|
||||||
|
useSavedState: true,
|
||||||
|
sortFn: (a, b) => {
|
||||||
|
// Sort order: folders first, then files. Sort folders and files alphabetically
|
||||||
|
if ((!a.file && !b.file) || (a.file && b.file)) {
|
||||||
|
// numeric: true: Whether numeric collation should be used, such that "1" < "2" < "10"
|
||||||
|
// sensitivity: "base": Only strings that differ in base letters compare as unequal. Examples: a ≠ b, a = á, a = A
|
||||||
|
return a.displayName.localeCompare(b.displayName, undefined, {
|
||||||
|
numeric: true,
|
||||||
|
sensitivity: "base",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (a.file && !b.file) {
|
||||||
|
return 1
|
||||||
|
} else {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filterFn: (node) => node.name !== "tags",
|
||||||
|
order: ["filter", "map", "sort"],
|
||||||
|
} satisfies Options
|
||||||
|
|
||||||
|
export default ((userOpts?: Partial<Options>) => {
|
||||||
|
// Parse config
|
||||||
|
const opts: Options = { ...defaultOptions, ...userOpts }
|
||||||
|
|
||||||
|
// memoized
|
||||||
|
let fileTree: FileNode
|
||||||
|
let jsonTree: string
|
||||||
|
|
||||||
|
function constructFileTree(allFiles: QuartzPluginData[]) {
|
||||||
|
if (!fileTree) {
|
||||||
|
// Construct tree from allFiles
|
||||||
|
fileTree = new FileNode("")
|
||||||
|
allFiles.forEach((file) => fileTree.add(file, 1))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keys of this object must match corresponding function name of `FileNode`,
|
||||||
|
* while values must be the argument that will be passed to the function.
|
||||||
|
*
|
||||||
|
* e.g. entry for FileNode.sort: `sort: opts.sortFn` (value is sort function from options)
|
||||||
|
*/
|
||||||
|
const functions = {
|
||||||
|
map: opts.mapFn,
|
||||||
|
sort: opts.sortFn,
|
||||||
|
filter: opts.filterFn,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied)
|
||||||
|
if (opts.order) {
|
||||||
|
// Order is important, use loop with index instead of order.map()
|
||||||
|
for (let i = 0; i < opts.order.length; i++) {
|
||||||
|
const functionName = opts.order[i]
|
||||||
|
if (functions[functionName]) {
|
||||||
|
// for every entry in order, call matching function in FileNode and pass matching argument
|
||||||
|
// e.g. i = 0; functionName = "filter"
|
||||||
|
// converted to: (if opts.filterFn) => fileTree.filter(opts.filterFn)
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
// typescript cant statically check these dynamic references, so manually make sure reference is valid and ignore warning
|
||||||
|
fileTree[functionName].call(fileTree, functions[functionName])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all folders of tree. Initialize with collapsed state
|
||||||
|
const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed")
|
||||||
|
|
||||||
|
// Stringify to pass json tree as data attribute ([data-tree])
|
||||||
|
jsonTree = JSON.stringify(folders)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) {
|
||||||
|
constructFileTree(allFiles)
|
||||||
|
return (
|
||||||
|
<div class={`explorer ${displayClass ?? ""}`}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="explorer"
|
||||||
|
data-behavior={opts.folderClickBehavior}
|
||||||
|
data-collapsed={opts.folderDefaultState}
|
||||||
|
data-savestate={opts.useSavedState}
|
||||||
|
data-tree={jsonTree}
|
||||||
|
>
|
||||||
|
<h1>{opts.title}</h1>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="5 8 14 8"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="fold"
|
||||||
|
>
|
||||||
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div id="explorer-content">
|
||||||
|
<ul class="overflow" id="explorer-ul">
|
||||||
|
<ExplorerNode node={fileTree} opts={opts} fileData={fileData} />
|
||||||
|
<li id="explorer-end" />
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Explorer.css = explorerStyle
|
||||||
|
Explorer.afterDOMLoaded = script
|
||||||
|
return Explorer
|
||||||
|
}) satisfies QuartzComponentConstructor
|
||||||
224
quartz/components/ExplorerNode.tsx
Normal file
224
quartz/components/ExplorerNode.tsx
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
// @ts-ignore
|
||||||
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
|
import { resolveRelative } from "../util/path"
|
||||||
|
|
||||||
|
type OrderEntries = "sort" | "filter" | "map"
|
||||||
|
|
||||||
|
export interface Options {
|
||||||
|
title: string
|
||||||
|
folderDefaultState: "collapsed" | "open"
|
||||||
|
folderClickBehavior: "collapse" | "link"
|
||||||
|
useSavedState: boolean
|
||||||
|
sortFn: (a: FileNode, b: FileNode) => number
|
||||||
|
filterFn?: (node: FileNode) => boolean
|
||||||
|
mapFn?: (node: FileNode) => void
|
||||||
|
order?: OrderEntries[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataWrapper = {
|
||||||
|
file: QuartzPluginData
|
||||||
|
path: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FolderState = {
|
||||||
|
path: string
|
||||||
|
collapsed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// Structure to add all files into a tree
|
||||||
|
export class FileNode {
|
||||||
|
children: FileNode[]
|
||||||
|
name: string
|
||||||
|
displayName: string
|
||||||
|
file: QuartzPluginData | null
|
||||||
|
depth: number
|
||||||
|
|
||||||
|
constructor(name: string, file?: QuartzPluginData, depth?: number) {
|
||||||
|
this.children = []
|
||||||
|
this.name = name
|
||||||
|
this.displayName = name
|
||||||
|
this.file = file ? structuredClone(file) : null
|
||||||
|
this.depth = depth ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private insert(file: DataWrapper) {
|
||||||
|
if (file.path.length === 1) {
|
||||||
|
if (file.path[0] !== "index.md") {
|
||||||
|
this.children.push(new FileNode(file.file.frontmatter!.title, file.file, this.depth + 1))
|
||||||
|
} else {
|
||||||
|
const title = file.file.frontmatter?.title
|
||||||
|
if (title && title !== "index" && file.path[0] === "index.md") {
|
||||||
|
this.displayName = title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const next = file.path[0]
|
||||||
|
file.path = file.path.splice(1)
|
||||||
|
for (const child of this.children) {
|
||||||
|
if (child.name === next) {
|
||||||
|
child.insert(file)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newChild = new FileNode(next, undefined, this.depth + 1)
|
||||||
|
newChild.insert(file)
|
||||||
|
this.children.push(newChild)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new file to tree
|
||||||
|
add(file: QuartzPluginData, splice: number = 0) {
|
||||||
|
this.insert({ file, path: file.filePath!.split("/").splice(splice) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print tree structure (for debugging)
|
||||||
|
print(depth: number = 0) {
|
||||||
|
let folderChar = ""
|
||||||
|
if (!this.file) folderChar = "|"
|
||||||
|
console.log("-".repeat(depth), folderChar, this.name, this.depth)
|
||||||
|
this.children.forEach((e) => e.print(depth + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter FileNode tree. Behaves similar to `Array.prototype.filter()`, but modifies tree in place
|
||||||
|
* @param filterFn function to filter tree with
|
||||||
|
*/
|
||||||
|
filter(filterFn: (node: FileNode) => boolean) {
|
||||||
|
this.children = this.children.filter(filterFn)
|
||||||
|
this.children.forEach((child) => child.filter(filterFn))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter FileNode tree. Behaves similar to `Array.prototype.map()`, but modifies tree in place
|
||||||
|
* @param mapFn function to use for mapping over tree
|
||||||
|
*/
|
||||||
|
map(mapFn: (node: FileNode) => void) {
|
||||||
|
mapFn(this)
|
||||||
|
|
||||||
|
this.children.forEach((child) => child.map(mapFn))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get folder representation with state of tree.
|
||||||
|
* Intended to only be called on root node before changes to the tree are made
|
||||||
|
* @param collapsed default state of folders (collapsed by default or not)
|
||||||
|
* @returns array containing folder state for tree
|
||||||
|
*/
|
||||||
|
getFolderPaths(collapsed: boolean): FolderState[] {
|
||||||
|
const folderPaths: FolderState[] = []
|
||||||
|
|
||||||
|
const traverse = (node: FileNode, currentPath: string) => {
|
||||||
|
if (!node.file) {
|
||||||
|
const folderPath = currentPath + (currentPath ? "/" : "") + node.name
|
||||||
|
if (folderPath !== "") {
|
||||||
|
folderPaths.push({ path: folderPath, collapsed })
|
||||||
|
}
|
||||||
|
node.children.forEach((child) => traverse(child, folderPath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse(this, "")
|
||||||
|
|
||||||
|
return folderPaths
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort order: folders first, then files. Sort folders and files alphabetically
|
||||||
|
/**
|
||||||
|
* Sorts tree according to sort/compare function
|
||||||
|
* @param sortFn compare function used for `.sort()`, also used recursively for children
|
||||||
|
*/
|
||||||
|
sort(sortFn: (a: FileNode, b: FileNode) => number) {
|
||||||
|
this.children = this.children.sort(sortFn)
|
||||||
|
this.children.forEach((e) => e.sort(sortFn))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExplorerNodeProps = {
|
||||||
|
node: FileNode
|
||||||
|
opts: Options
|
||||||
|
fileData: QuartzPluginData
|
||||||
|
fullPath?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodeProps) {
|
||||||
|
// Get options
|
||||||
|
const folderBehavior = opts.folderClickBehavior
|
||||||
|
const isDefaultOpen = opts.folderDefaultState === "open"
|
||||||
|
|
||||||
|
// Calculate current folderPath
|
||||||
|
let pathOld = fullPath ? fullPath : ""
|
||||||
|
let folderPath = ""
|
||||||
|
if (node.name !== "") {
|
||||||
|
folderPath = `${pathOld}/${node.name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
{node.file ? (
|
||||||
|
// Single file node
|
||||||
|
<li key={node.file.slug}>
|
||||||
|
<a href={resolveRelative(fileData.slug!, node.file.slug!)} data-for={node.file.slug}>
|
||||||
|
{node.displayName}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
{node.name !== "" && (
|
||||||
|
// Node with entire folder
|
||||||
|
// Render svg button + folder name, then children
|
||||||
|
<div class="folder-container">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="5 8 14 8"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="folder-icon"
|
||||||
|
>
|
||||||
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
|
</svg>
|
||||||
|
{/* render <a> tag if folderBehavior is "link", otherwise render <button> with collapse click event */}
|
||||||
|
<div key={node.name} data-folderpath={folderPath}>
|
||||||
|
{folderBehavior === "link" ? (
|
||||||
|
<a href={`${folderPath}`} data-for={node.name} class="folder-title">
|
||||||
|
{node.displayName}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<button class="folder-button">
|
||||||
|
<p class="folder-title">{node.displayName}</p>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Recursively render children of folder */}
|
||||||
|
<div class={`folder-outer ${node.depth === 0 || isDefaultOpen ? "open" : ""}`}>
|
||||||
|
<ul
|
||||||
|
// Inline style for left folder paddings
|
||||||
|
style={{
|
||||||
|
paddingLeft: node.name !== "" ? "1.4rem" : "0",
|
||||||
|
}}
|
||||||
|
class="content"
|
||||||
|
data-folderul={folderPath}
|
||||||
|
>
|
||||||
|
{node.children.map((childNode, i) => (
|
||||||
|
<ExplorerNode
|
||||||
|
node={childNode}
|
||||||
|
key={i}
|
||||||
|
opts={opts}
|
||||||
|
fullPath={folderPath}
|
||||||
|
fileData={fileData}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { QuartzComponentConstructor } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import style from "./styles/footer.scss"
|
import style from "./styles/footer.scss"
|
||||||
import { version } from "../../package.json"
|
import { version } from "../../package.json"
|
||||||
|
|
||||||
@@ -7,11 +7,11 @@ interface Options {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default ((opts?: Options) => {
|
export default ((opts?: Options) => {
|
||||||
function Footer() {
|
function Footer({ displayClass }: QuartzComponentProps) {
|
||||||
const year = new Date().getFullYear()
|
const year = new Date().getFullYear()
|
||||||
const links = opts?.links ?? []
|
const links = opts?.links ?? []
|
||||||
return (
|
return (
|
||||||
<footer>
|
<footer class={`${displayClass ?? ""}`}>
|
||||||
<hr />
|
<hr />
|
||||||
<p>
|
<p>
|
||||||
Created with <a href="https://quartz.jzhao.xyz/">Quartz v{version}</a>, © {year}
|
Created with <a href="https://quartz.jzhao.xyz/">Quartz v{version}</a>, © {year}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { QuartzComponentConstructor } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import script from "./scripts/graph.inline"
|
import script from "./scripts/graph.inline"
|
||||||
import style from "./styles/graph.scss"
|
import style from "./styles/graph.scss"
|
||||||
@@ -13,6 +13,8 @@ export interface D3Config {
|
|||||||
linkDistance: number
|
linkDistance: number
|
||||||
fontSize: number
|
fontSize: number
|
||||||
opacityScale: number
|
opacityScale: number
|
||||||
|
removeTags: string[]
|
||||||
|
showTags: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GraphOptions {
|
interface GraphOptions {
|
||||||
@@ -31,6 +33,8 @@ const defaultOptions: GraphOptions = {
|
|||||||
linkDistance: 30,
|
linkDistance: 30,
|
||||||
fontSize: 0.6,
|
fontSize: 0.6,
|
||||||
opacityScale: 1,
|
opacityScale: 1,
|
||||||
|
showTags: true,
|
||||||
|
removeTags: [],
|
||||||
},
|
},
|
||||||
globalGraph: {
|
globalGraph: {
|
||||||
drag: true,
|
drag: true,
|
||||||
@@ -42,15 +46,17 @@ const defaultOptions: GraphOptions = {
|
|||||||
linkDistance: 30,
|
linkDistance: 30,
|
||||||
fontSize: 0.6,
|
fontSize: 0.6,
|
||||||
opacityScale: 1,
|
opacityScale: 1,
|
||||||
|
showTags: true,
|
||||||
|
removeTags: [],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ((opts?: GraphOptions) => {
|
export default ((opts?: GraphOptions) => {
|
||||||
function Graph() {
|
function Graph({ displayClass }: QuartzComponentProps) {
|
||||||
const localGraph = { ...opts?.localGraph, ...defaultOptions.localGraph }
|
const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph }
|
||||||
const globalGraph = { ...opts?.globalGraph, ...defaultOptions.globalGraph }
|
const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph }
|
||||||
return (
|
return (
|
||||||
<div class="graph">
|
<div class={`graph ${displayClass ?? ""}`}>
|
||||||
<h3>Graph View</h3>
|
<h3>Graph View</h3>
|
||||||
<div class="graph-outer">
|
<div class="graph-outer">
|
||||||
<div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
|
<div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { joinSegments, pathToRoot } from "../util/path"
|
import { FullSlug, _stripSlashes, joinSegments, pathToRoot } from "../util/path"
|
||||||
import { JSResourceToScriptElement } from "../util/resources"
|
import { JSResourceToScriptElement } from "../util/resources"
|
||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
|
|
||||||
@@ -7,7 +7,11 @@ export default (() => {
|
|||||||
const title = fileData.frontmatter?.title ?? "Untitled"
|
const title = fileData.frontmatter?.title ?? "Untitled"
|
||||||
const description = fileData.description?.trim() ?? "No description provided"
|
const description = fileData.description?.trim() ?? "No description provided"
|
||||||
const { css, js } = externalResources
|
const { css, js } = externalResources
|
||||||
const baseDir = pathToRoot(fileData.slug!)
|
|
||||||
|
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
|
||||||
|
const path = url.pathname as FullSlug
|
||||||
|
const baseDir = fileData.slug === "404" ? path : pathToRoot(fileData.slug!)
|
||||||
|
|
||||||
const iconPath = joinSegments(baseDir, "static/icon.png")
|
const iconPath = joinSegments(baseDir, "static/icon.png")
|
||||||
const ogImagePath = `https://${cfg.baseUrl}/static/og-image.png`
|
const ogImagePath = `https://${cfg.baseUrl}/static/og-image.png`
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,36 @@
|
|||||||
import { FullSlug, resolveRelative } from "../util/path"
|
import { FullSlug, resolveRelative } from "../util/path"
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
import { Date } from "./Date"
|
import { Date, getDate } from "./Date"
|
||||||
import { QuartzComponentProps } from "./types"
|
import { QuartzComponentProps } from "./types"
|
||||||
|
import { GlobalConfiguration } from "../cfg"
|
||||||
|
|
||||||
export function byDateAndAlphabetical(f1: QuartzPluginData, f2: QuartzPluginData): number {
|
export function byDateAndAlphabetical(
|
||||||
if (f1.dates && f2.dates) {
|
cfg: GlobalConfiguration,
|
||||||
// sort descending by last modified
|
): (f1: QuartzPluginData, f2: QuartzPluginData) => number {
|
||||||
return f2.dates.modified.getTime() - f1.dates.modified.getTime()
|
return (f1, f2) => {
|
||||||
} else if (f1.dates && !f2.dates) {
|
if (f1.dates && f2.dates) {
|
||||||
// prioritize files with dates
|
// sort descending
|
||||||
return -1
|
return getDate(cfg, f2)!.getTime() - getDate(cfg, f1)!.getTime()
|
||||||
} else if (!f1.dates && f2.dates) {
|
} else if (f1.dates && !f2.dates) {
|
||||||
return 1
|
// prioritize files with dates
|
||||||
|
return -1
|
||||||
|
} else if (!f1.dates && f2.dates) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, sort lexographically by title
|
||||||
|
const f1Title = f1.frontmatter?.title.toLowerCase() ?? ""
|
||||||
|
const f2Title = f2.frontmatter?.title.toLowerCase() ?? ""
|
||||||
|
return f1Title.localeCompare(f2Title)
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise, sort lexographically by title
|
|
||||||
const f1Title = f1.frontmatter?.title.toLowerCase() ?? ""
|
|
||||||
const f2Title = f2.frontmatter?.title.toLowerCase() ?? ""
|
|
||||||
return f1Title.localeCompare(f2Title)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
limit?: number
|
limit?: number
|
||||||
} & QuartzComponentProps
|
} & QuartzComponentProps
|
||||||
|
|
||||||
export function PageList({ fileData, allFiles, limit }: Props) {
|
export function PageList({ cfg, fileData, allFiles, limit }: Props) {
|
||||||
let list = allFiles.sort(byDateAndAlphabetical)
|
let list = allFiles.sort(byDateAndAlphabetical(cfg))
|
||||||
if (limit) {
|
if (limit) {
|
||||||
list = list.slice(0, limit)
|
list = list.slice(0, limit)
|
||||||
}
|
}
|
||||||
@@ -41,7 +46,7 @@ export function PageList({ fileData, allFiles, limit }: Props) {
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
{page.dates && (
|
{page.dates && (
|
||||||
<p class="meta">
|
<p class="meta">
|
||||||
<Date date={page.dates.modified} />
|
<Date date={getDate(cfg, page)!} />
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div class="desc">
|
<div class="desc">
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { pathToRoot } from "../util/path"
|
import { pathToRoot } from "../util/path"
|
||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
|
|
||||||
function PageTitle({ fileData, cfg }: QuartzComponentProps) {
|
function PageTitle({ fileData, cfg, displayClass }: QuartzComponentProps) {
|
||||||
const title = cfg?.pageTitle ?? "Untitled Quartz"
|
const title = cfg?.pageTitle ?? "Untitled Quartz"
|
||||||
const baseDir = pathToRoot(fileData.slug!)
|
const baseDir = pathToRoot(fileData.slug!)
|
||||||
return (
|
return (
|
||||||
<h1 class="page-title">
|
<h1 class={`page-title ${displayClass ?? ""}`}>
|
||||||
<a href={baseDir}>{title}</a>
|
<a href={baseDir}>{title}</a>
|
||||||
</h1>
|
</h1>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { FullSlug, SimpleSlug, resolveRelative } from "../util/path"
|
|||||||
import { QuartzPluginData } from "../plugins/vfile"
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
import { byDateAndAlphabetical } from "./PageList"
|
import { byDateAndAlphabetical } from "./PageList"
|
||||||
import style from "./styles/recentNotes.scss"
|
import style from "./styles/recentNotes.scss"
|
||||||
import { Date } from "./Date"
|
import { Date, getDate } from "./Date"
|
||||||
|
import { GlobalConfiguration } from "../cfg"
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
title: string
|
title: string
|
||||||
@@ -13,22 +14,21 @@ interface Options {
|
|||||||
sort: (f1: QuartzPluginData, f2: QuartzPluginData) => number
|
sort: (f1: QuartzPluginData, f2: QuartzPluginData) => number
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions: Options = {
|
const defaultOptions = (cfg: GlobalConfiguration): Options => ({
|
||||||
title: "Recent Notes",
|
title: "Recent Notes",
|
||||||
limit: 3,
|
limit: 3,
|
||||||
linkToMore: false,
|
linkToMore: false,
|
||||||
filter: () => true,
|
filter: () => true,
|
||||||
sort: byDateAndAlphabetical,
|
sort: byDateAndAlphabetical(cfg),
|
||||||
}
|
})
|
||||||
|
|
||||||
export default ((userOpts?: Partial<Options>) => {
|
export default ((userOpts?: Partial<Options>) => {
|
||||||
const opts = { ...defaultOptions, ...userOpts }
|
function RecentNotes({ allFiles, fileData, displayClass, cfg }: QuartzComponentProps) {
|
||||||
function RecentNotes(props: QuartzComponentProps) {
|
const opts = { ...defaultOptions(cfg), ...userOpts }
|
||||||
const { allFiles, fileData, displayClass } = props
|
|
||||||
const pages = allFiles.filter(opts.filter).sort(opts.sort)
|
const pages = allFiles.filter(opts.filter).sort(opts.sort)
|
||||||
const remaining = Math.max(0, pages.length - opts.limit)
|
const remaining = Math.max(0, pages.length - opts.limit)
|
||||||
return (
|
return (
|
||||||
<div class={`recent-notes ${displayClass}`}>
|
<div class={`recent-notes ${displayClass ?? ""}`}>
|
||||||
<h3>{opts.title}</h3>
|
<h3>{opts.title}</h3>
|
||||||
<ul class="recent-ul">
|
<ul class="recent-ul">
|
||||||
{pages.slice(0, opts.limit).map((page) => {
|
{pages.slice(0, opts.limit).map((page) => {
|
||||||
@@ -47,7 +47,7 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
</div>
|
</div>
|
||||||
{page.dates && (
|
{page.dates && (
|
||||||
<p class="meta">
|
<p class="meta">
|
||||||
<Date date={page.dates.modified} />
|
<Date date={getDate(cfg, page)!} />
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<ul class="tags">
|
<ul class="tags">
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { QuartzComponentConstructor } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import style from "./styles/search.scss"
|
import style from "./styles/search.scss"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import script from "./scripts/search.inline"
|
import script from "./scripts/search.inline"
|
||||||
|
|
||||||
export default (() => {
|
export default (() => {
|
||||||
function Search() {
|
function Search({ displayClass }: QuartzComponentProps) {
|
||||||
return (
|
return (
|
||||||
<div class="search">
|
<div class={`search ${displayClass ?? ""}`}>
|
||||||
<div id="search-icon">
|
<div id="search-icon">
|
||||||
<p>Search</p>
|
<p>Search</p>
|
||||||
<div></div>
|
<div></div>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
|
|
||||||
function Spacer({ displayClass }: QuartzComponentProps) {
|
function Spacer({ displayClass }: QuartzComponentProps) {
|
||||||
const className = displayClass ? `spacer ${displayClass}` : "spacer"
|
return <div class={`spacer ${displayClass ?? ""}`}></div>
|
||||||
return <div class={className}></div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (() => Spacer) satisfies QuartzComponentConstructor
|
export default (() => Spacer) satisfies QuartzComponentConstructor
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ function TableOfContents({ fileData, displayClass }: QuartzComponentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={`toc ${displayClass}`}>
|
<div class={`toc ${displayClass ?? ""}`}>
|
||||||
<button type="button" id="toc">
|
<button type="button" id="toc" class={fileData.collapseToc ? "collapsed" : ""}>
|
||||||
<h3>Table of Contents</h3>
|
<h3>Table of Contents</h3>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -60,7 +60,7 @@ function LegacyTableOfContents({ fileData }: QuartzComponentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<details id="toc" open>
|
<details id="toc" open={!fileData.collapseToc}>
|
||||||
<summary>
|
<summary>
|
||||||
<h3>Table of Contents</h3>
|
<h3>Table of Contents</h3>
|
||||||
</summary>
|
</summary>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { pathToRoot, slugTag } from "../util/path"
|
import { pathToRoot, slugTag } from "../util/path"
|
||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
|
|
||||||
function TagList({ fileData }: QuartzComponentProps) {
|
function TagList({ fileData, displayClass }: QuartzComponentProps) {
|
||||||
const tags = fileData.frontmatter?.tags
|
const tags = fileData.frontmatter?.tags
|
||||||
const baseDir = pathToRoot(fileData.slug!)
|
const baseDir = pathToRoot(fileData.slug!)
|
||||||
if (tags && tags.length > 0) {
|
if (tags && tags.length > 0) {
|
||||||
return (
|
return (
|
||||||
<ul class="tags">
|
<ul class={`tags ${displayClass ?? ""}`}>
|
||||||
{tags.map((tag) => {
|
{tags.map((tag) => {
|
||||||
const display = `#${tag}`
|
const display = `#${tag}`
|
||||||
const linkDest = baseDir + `/tags/${slugTag(tag)}`
|
const linkDest = baseDir + `/tags/${slugTag(tag)}`
|
||||||
@@ -32,6 +32,12 @@ TagList.css = `
|
|||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-li > .section > .tags {
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags > li {
|
.tags > li {
|
||||||
@@ -41,10 +47,11 @@ TagList.css = `
|
|||||||
overflow-wrap: normal;
|
overflow-wrap: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.tag-link {
|
a.internal.tag-link {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: var(--highlight);
|
background-color: var(--highlight);
|
||||||
padding: 0.2rem 0.5rem;
|
padding: 0.2rem 0.4rem;
|
||||||
|
margin: 0 0.1rem;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import ArticleTitle from "./ArticleTitle"
|
|
||||||
import Content from "./pages/Content"
|
import Content from "./pages/Content"
|
||||||
import TagContent from "./pages/TagContent"
|
import TagContent from "./pages/TagContent"
|
||||||
import FolderContent from "./pages/FolderContent"
|
import FolderContent from "./pages/FolderContent"
|
||||||
|
import NotFound from "./pages/404"
|
||||||
|
import ArticleTitle from "./ArticleTitle"
|
||||||
import Darkmode from "./Darkmode"
|
import Darkmode from "./Darkmode"
|
||||||
import Head from "./Head"
|
import Head from "./Head"
|
||||||
import PageTitle from "./PageTitle"
|
import PageTitle from "./PageTitle"
|
||||||
import ContentMeta from "./ContentMeta"
|
import ContentMeta from "./ContentMeta"
|
||||||
import Spacer from "./Spacer"
|
import Spacer from "./Spacer"
|
||||||
import TableOfContents from "./TableOfContents"
|
import TableOfContents from "./TableOfContents"
|
||||||
|
import Explorer from "./Explorer"
|
||||||
import TagList from "./TagList"
|
import TagList from "./TagList"
|
||||||
import Graph from "./Graph"
|
import Graph from "./Graph"
|
||||||
import Backlinks from "./Backlinks"
|
import Backlinks from "./Backlinks"
|
||||||
@@ -16,6 +18,7 @@ import Footer from "./Footer"
|
|||||||
import DesktopOnly from "./DesktopOnly"
|
import DesktopOnly from "./DesktopOnly"
|
||||||
import MobileOnly from "./MobileOnly"
|
import MobileOnly from "./MobileOnly"
|
||||||
import RecentNotes from "./RecentNotes"
|
import RecentNotes from "./RecentNotes"
|
||||||
|
import Breadcrumbs from "./Breadcrumbs"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ArticleTitle,
|
ArticleTitle,
|
||||||
@@ -28,6 +31,7 @@ export {
|
|||||||
ContentMeta,
|
ContentMeta,
|
||||||
Spacer,
|
Spacer,
|
||||||
TableOfContents,
|
TableOfContents,
|
||||||
|
Explorer,
|
||||||
TagList,
|
TagList,
|
||||||
Graph,
|
Graph,
|
||||||
Backlinks,
|
Backlinks,
|
||||||
@@ -36,4 +40,6 @@ export {
|
|||||||
DesktopOnly,
|
DesktopOnly,
|
||||||
MobileOnly,
|
MobileOnly,
|
||||||
RecentNotes,
|
RecentNotes,
|
||||||
|
NotFound,
|
||||||
|
Breadcrumbs,
|
||||||
}
|
}
|
||||||
|
|||||||
12
quartz/components/pages/404.tsx
Normal file
12
quartz/components/pages/404.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { QuartzComponentConstructor } from "../types"
|
||||||
|
|
||||||
|
function NotFound() {
|
||||||
|
return (
|
||||||
|
<article class="popover-hint">
|
||||||
|
<h1>404</h1>
|
||||||
|
<p>Either this page is private or doesn't exist.</p>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (() => NotFound) satisfies QuartzComponentConstructor
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
|
import { htmlToJsx } from "../../util/jsx"
|
||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
||||||
import { Fragment, jsx, jsxs } from "preact/jsx-runtime"
|
|
||||||
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
|
|
||||||
|
|
||||||
function Content({ tree }: QuartzComponentProps) {
|
function Content({ fileData, tree }: QuartzComponentProps) {
|
||||||
// @ts-ignore (preact makes it angry)
|
const content = htmlToJsx(fileData.filePath!, tree)
|
||||||
const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: "html" })
|
|
||||||
return <article class="popover-hint">{content}</article>
|
return <article class="popover-hint">{content}</article>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
||||||
import { Fragment, jsx, jsxs } from "preact/jsx-runtime"
|
|
||||||
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
import style from "../styles/listPage.scss"
|
import style from "../styles/listPage.scss"
|
||||||
import { PageList } from "../PageList"
|
import { PageList } from "../PageList"
|
||||||
import { _stripSlashes, simplifySlug } from "../../util/path"
|
import { _stripSlashes, simplifySlug } from "../../util/path"
|
||||||
import { Root } from "hast"
|
import { Root } from "hast"
|
||||||
|
import { pluralize } from "../../util/lang"
|
||||||
|
import { htmlToJsx } from "../../util/jsx"
|
||||||
|
|
||||||
function FolderContent(props: QuartzComponentProps) {
|
function FolderContent(props: QuartzComponentProps) {
|
||||||
const { tree, fileData, allFiles } = props
|
const { tree, fileData, allFiles } = props
|
||||||
@@ -28,13 +28,14 @@ function FolderContent(props: QuartzComponentProps) {
|
|||||||
const content =
|
const content =
|
||||||
(tree as Root).children.length === 0
|
(tree as Root).children.length === 0
|
||||||
? fileData.description
|
? fileData.description
|
||||||
: // @ts-ignore
|
: htmlToJsx(fileData.filePath!, tree)
|
||||||
toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: "html" })
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="popover-hint">
|
<div class="popover-hint">
|
||||||
<article>{content}</article>
|
<article>
|
||||||
<p>{allPagesInFolder.length} items under this folder.</p>
|
<p>{content}</p>
|
||||||
|
</article>
|
||||||
|
<p>{pluralize(allPagesInFolder.length, "item")} under this folder.</p>
|
||||||
<div>
|
<div>
|
||||||
<PageList {...listProps} />
|
<PageList {...listProps} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
||||||
import { Fragment, jsx, jsxs } from "preact/jsx-runtime"
|
|
||||||
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
|
|
||||||
import style from "../styles/listPage.scss"
|
import style from "../styles/listPage.scss"
|
||||||
import { PageList } from "../PageList"
|
import { PageList } from "../PageList"
|
||||||
import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path"
|
import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path"
|
||||||
import { QuartzPluginData } from "../../plugins/vfile"
|
import { QuartzPluginData } from "../../plugins/vfile"
|
||||||
import { Root } from "hast"
|
import { Root } from "hast"
|
||||||
|
import { pluralize } from "../../util/lang"
|
||||||
|
import { htmlToJsx } from "../../util/jsx"
|
||||||
|
|
||||||
const numPages = 10
|
const numPages = 10
|
||||||
function TagContent(props: QuartzComponentProps) {
|
function TagContent(props: QuartzComponentProps) {
|
||||||
@@ -25,8 +25,7 @@ function TagContent(props: QuartzComponentProps) {
|
|||||||
const content =
|
const content =
|
||||||
(tree as Root).children.length === 0
|
(tree as Root).children.length === 0
|
||||||
? fileData.description
|
? fileData.description
|
||||||
: // @ts-ignore
|
: htmlToJsx(fileData.filePath!, tree)
|
||||||
toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: "html" })
|
|
||||||
|
|
||||||
if (tag === "") {
|
if (tag === "") {
|
||||||
const tags = [...new Set(allFiles.flatMap((data) => data.frontmatter?.tags ?? []))]
|
const tags = [...new Set(allFiles.flatMap((data) => data.frontmatter?.tags ?? []))]
|
||||||
@@ -37,7 +36,9 @@ function TagContent(props: QuartzComponentProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="popover-hint">
|
<div class="popover-hint">
|
||||||
<article>{content}</article>
|
<article>
|
||||||
|
<p>{content}</p>
|
||||||
|
</article>
|
||||||
<p>Found {tags.length} total tags.</p>
|
<p>Found {tags.length} total tags.</p>
|
||||||
<div>
|
<div>
|
||||||
{tags.map((tag) => {
|
{tags.map((tag) => {
|
||||||
@@ -52,13 +53,13 @@ function TagContent(props: QuartzComponentProps) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2>
|
<h2>
|
||||||
<a class="internal tag-link" href={`./${tag}`}>
|
<a class="internal tag-link" href={`../tags/${tag}`}>
|
||||||
#{tag}
|
#{tag}
|
||||||
</a>
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
{content && <p>{content}</p>}
|
{content && <p>{content}</p>}
|
||||||
<p>
|
<p>
|
||||||
{pages.length} items with this tag.{" "}
|
{pluralize(pages.length, "item")} with this tag.{" "}
|
||||||
{pages.length > numPages && `Showing first ${numPages}.`}
|
{pages.length > numPages && `Showing first ${numPages}.`}
|
||||||
</p>
|
</p>
|
||||||
<PageList limit={numPages} {...listProps} />
|
<PageList limit={numPages} {...listProps} />
|
||||||
@@ -78,7 +79,7 @@ function TagContent(props: QuartzComponentProps) {
|
|||||||
return (
|
return (
|
||||||
<div class="popover-hint">
|
<div class="popover-hint">
|
||||||
<article>{content}</article>
|
<article>{content}</article>
|
||||||
<p>{pages.length} items with this tag.</p>
|
<p>{pluralize(pages.length, "item")} with this tag.</p>
|
||||||
<div>
|
<div>
|
||||||
<PageList {...listProps} />
|
<PageList {...listProps} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import { QuartzComponent, QuartzComponentProps } from "./types"
|
|||||||
import HeaderConstructor from "./Header"
|
import HeaderConstructor from "./Header"
|
||||||
import BodyConstructor from "./Body"
|
import BodyConstructor from "./Body"
|
||||||
import { JSResourceToScriptElement, StaticResources } from "../util/resources"
|
import { JSResourceToScriptElement, StaticResources } from "../util/resources"
|
||||||
import { FullSlug, joinSegments, pathToRoot } from "../util/path"
|
import { FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path"
|
||||||
|
import { visit } from "unist-util-visit"
|
||||||
|
import { Root, Element, ElementContent } from "hast"
|
||||||
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
|
|
||||||
interface RenderComponents {
|
interface RenderComponents {
|
||||||
head: QuartzComponent
|
head: QuartzComponent
|
||||||
@@ -15,9 +18,10 @@ interface RenderComponents {
|
|||||||
footer: QuartzComponent
|
footer: QuartzComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pageResources(slug: FullSlug, staticResources: StaticResources): StaticResources {
|
export function pageResources(
|
||||||
const baseDir = pathToRoot(slug)
|
baseDir: FullSlug | RelativeURL,
|
||||||
|
staticResources: StaticResources,
|
||||||
|
): StaticResources {
|
||||||
const contentIndexPath = joinSegments(baseDir, "static/contentIndex.json")
|
const contentIndexPath = joinSegments(baseDir, "static/contentIndex.json")
|
||||||
const contentIndexScript = `const fetchData = fetch(\`${contentIndexPath}\`).then(data => data.json())`
|
const contentIndexScript = `const fetchData = fetch(\`${contentIndexPath}\`).then(data => data.json())`
|
||||||
|
|
||||||
@@ -46,12 +50,119 @@ export function pageResources(slug: FullSlug, staticResources: StaticResources):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pageIndex: Map<FullSlug, QuartzPluginData> | undefined = undefined
|
||||||
|
function getOrComputeFileIndex(allFiles: QuartzPluginData[]): Map<FullSlug, QuartzPluginData> {
|
||||||
|
if (!pageIndex) {
|
||||||
|
pageIndex = new Map()
|
||||||
|
for (const file of allFiles) {
|
||||||
|
pageIndex.set(file.slug!, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageIndex
|
||||||
|
}
|
||||||
|
|
||||||
export function renderPage(
|
export function renderPage(
|
||||||
slug: FullSlug,
|
slug: FullSlug,
|
||||||
componentData: QuartzComponentProps,
|
componentData: QuartzComponentProps,
|
||||||
components: RenderComponents,
|
components: RenderComponents,
|
||||||
pageResources: StaticResources,
|
pageResources: StaticResources,
|
||||||
): string {
|
): string {
|
||||||
|
// process transcludes in componentData
|
||||||
|
visit(componentData.tree as Root, "element", (node, _index, _parent) => {
|
||||||
|
if (node.tagName === "blockquote") {
|
||||||
|
const classNames = (node.properties?.className ?? []) as string[]
|
||||||
|
if (classNames.includes("transclude")) {
|
||||||
|
const inner = node.children[0] as Element
|
||||||
|
const transcludeTarget = inner.properties?.["data-slug"] as FullSlug
|
||||||
|
const page = getOrComputeFileIndex(componentData.allFiles).get(transcludeTarget)
|
||||||
|
if (!page) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let blockRef = node.properties?.dataBlock as string | undefined
|
||||||
|
if (blockRef?.startsWith("#^")) {
|
||||||
|
// block transclude
|
||||||
|
blockRef = blockRef.slice("#^".length)
|
||||||
|
let blockNode = page.blocks?.[blockRef]
|
||||||
|
if (blockNode) {
|
||||||
|
if (blockNode.tagName === "li") {
|
||||||
|
blockNode = {
|
||||||
|
type: "element",
|
||||||
|
tagName: "ul",
|
||||||
|
children: [blockNode],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node.children = [
|
||||||
|
normalizeHastElement(blockNode, slug, transcludeTarget),
|
||||||
|
{
|
||||||
|
type: "element",
|
||||||
|
tagName: "a",
|
||||||
|
properties: { href: inner.properties?.href, class: ["internal"] },
|
||||||
|
children: [{ type: "text", value: `Link to original` }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} else if (blockRef?.startsWith("#") && page.htmlAst) {
|
||||||
|
// header transclude
|
||||||
|
blockRef = blockRef.slice(1)
|
||||||
|
let startIdx = undefined
|
||||||
|
let endIdx = undefined
|
||||||
|
for (const [i, el] of page.htmlAst.children.entries()) {
|
||||||
|
if (el.type === "element" && el.tagName.match(/h[1-6]/)) {
|
||||||
|
if (endIdx) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startIdx !== undefined) {
|
||||||
|
endIdx = i
|
||||||
|
} else if (el.properties?.id === blockRef) {
|
||||||
|
startIdx = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startIdx === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
node.children = [
|
||||||
|
...(page.htmlAst.children.slice(startIdx, endIdx) as ElementContent[]).map((child) =>
|
||||||
|
normalizeHastElement(child as Element, slug, transcludeTarget),
|
||||||
|
),
|
||||||
|
{
|
||||||
|
type: "element",
|
||||||
|
tagName: "a",
|
||||||
|
properties: { href: inner.properties?.href, class: ["internal"] },
|
||||||
|
children: [{ type: "text", value: `Link to original` }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} else if (page.htmlAst) {
|
||||||
|
// page transclude
|
||||||
|
node.children = [
|
||||||
|
{
|
||||||
|
type: "element",
|
||||||
|
tagName: "h1",
|
||||||
|
children: [
|
||||||
|
{ type: "text", value: page.frontmatter?.title ?? `Transclude of ${page.slug}` },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
...(page.htmlAst.children as ElementContent[]).map((child) =>
|
||||||
|
normalizeHastElement(child as Element, slug, transcludeTarget),
|
||||||
|
),
|
||||||
|
{
|
||||||
|
type: "element",
|
||||||
|
tagName: "a",
|
||||||
|
properties: { href: inner.properties?.href, class: ["internal"] },
|
||||||
|
children: [{ type: "text", value: `Link to original` }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
head: Head,
|
head: Head,
|
||||||
header,
|
header,
|
||||||
|
|||||||
@@ -20,4 +20,13 @@ document.addEventListener("nav", () => {
|
|||||||
if (currentTheme === "dark") {
|
if (currentTheme === "dark") {
|
||||||
toggleSwitch.checked = true
|
toggleSwitch.checked = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen for changes in prefers-color-scheme
|
||||||
|
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
|
colorSchemeMediaQuery.addEventListener("change", (e) => {
|
||||||
|
const newTheme = e.matches ? "dark" : "light"
|
||||||
|
document.documentElement.setAttribute("saved-theme", newTheme)
|
||||||
|
localStorage.setItem("theme", newTheme)
|
||||||
|
toggleSwitch.checked = e.matches
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
165
quartz/components/scripts/explorer.inline.ts
Normal file
165
quartz/components/scripts/explorer.inline.ts
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import { FolderState } from "../ExplorerNode"
|
||||||
|
|
||||||
|
// Current state of folders
|
||||||
|
let explorerState: FolderState[]
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
// If last element is observed, remove gradient of "overflow" class so element is visible
|
||||||
|
const explorer = document.getElementById("explorer-ul")
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
explorer?.classList.add("no-background")
|
||||||
|
} else {
|
||||||
|
explorer?.classList.remove("no-background")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function toggleExplorer(this: HTMLElement) {
|
||||||
|
// Toggle collapsed state of entire explorer
|
||||||
|
this.classList.toggle("collapsed")
|
||||||
|
const content = this.nextElementSibling as HTMLElement
|
||||||
|
content.classList.toggle("collapsed")
|
||||||
|
content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFolder(evt: MouseEvent) {
|
||||||
|
evt.stopPropagation()
|
||||||
|
|
||||||
|
// Element that was clicked
|
||||||
|
const target = evt.target as HTMLElement
|
||||||
|
|
||||||
|
// Check if target was svg icon or button
|
||||||
|
const isSvg = target.nodeName === "svg"
|
||||||
|
|
||||||
|
// corresponding <ul> element relative to clicked button/folder
|
||||||
|
let childFolderContainer: HTMLElement
|
||||||
|
|
||||||
|
// <li> element of folder (stores folder-path dataset)
|
||||||
|
let currentFolderParent: HTMLElement
|
||||||
|
|
||||||
|
// Get correct relative container and toggle collapsed class
|
||||||
|
if (isSvg) {
|
||||||
|
childFolderContainer = target.parentElement?.nextSibling as HTMLElement
|
||||||
|
currentFolderParent = target.nextElementSibling as HTMLElement
|
||||||
|
|
||||||
|
childFolderContainer.classList.toggle("open")
|
||||||
|
} else {
|
||||||
|
childFolderContainer = target.parentElement?.parentElement?.nextElementSibling as HTMLElement
|
||||||
|
currentFolderParent = target.parentElement as HTMLElement
|
||||||
|
|
||||||
|
childFolderContainer.classList.toggle("open")
|
||||||
|
}
|
||||||
|
if (!childFolderContainer) return
|
||||||
|
|
||||||
|
// Collapse folder container
|
||||||
|
const isCollapsed = childFolderContainer.classList.contains("open")
|
||||||
|
setFolderState(childFolderContainer, !isCollapsed)
|
||||||
|
|
||||||
|
// Save folder state to localStorage
|
||||||
|
const clickFolderPath = currentFolderParent.dataset.folderpath as string
|
||||||
|
|
||||||
|
// Remove leading "/"
|
||||||
|
const fullFolderPath = clickFolderPath.substring(1)
|
||||||
|
toggleCollapsedByPath(explorerState, fullFolderPath)
|
||||||
|
|
||||||
|
const stringifiedFileTree = JSON.stringify(explorerState)
|
||||||
|
localStorage.setItem("fileTree", stringifiedFileTree)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupExplorer() {
|
||||||
|
// Set click handler for collapsing entire explorer
|
||||||
|
const explorer = document.getElementById("explorer")
|
||||||
|
|
||||||
|
// Get folder state from local storage
|
||||||
|
const storageTree = localStorage.getItem("fileTree")
|
||||||
|
|
||||||
|
// Convert to bool
|
||||||
|
const useSavedFolderState = explorer?.dataset.savestate === "true"
|
||||||
|
|
||||||
|
if (explorer) {
|
||||||
|
// Get config
|
||||||
|
const collapseBehavior = explorer.dataset.behavior
|
||||||
|
|
||||||
|
// Add click handlers for all folders (click handler on folder "label")
|
||||||
|
if (collapseBehavior === "collapse") {
|
||||||
|
Array.prototype.forEach.call(
|
||||||
|
document.getElementsByClassName("folder-button"),
|
||||||
|
function (item) {
|
||||||
|
item.removeEventListener("click", toggleFolder)
|
||||||
|
item.addEventListener("click", toggleFolder)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add click handler to main explorer
|
||||||
|
explorer.removeEventListener("click", toggleExplorer)
|
||||||
|
explorer.addEventListener("click", toggleExplorer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up click handlers for each folder (click handler on folder "icon")
|
||||||
|
Array.prototype.forEach.call(document.getElementsByClassName("folder-icon"), function (item) {
|
||||||
|
item.removeEventListener("click", toggleFolder)
|
||||||
|
item.addEventListener("click", toggleFolder)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (storageTree && useSavedFolderState) {
|
||||||
|
// Get state from localStorage and set folder state
|
||||||
|
explorerState = JSON.parse(storageTree)
|
||||||
|
explorerState.map((folderUl) => {
|
||||||
|
// grab <li> element for matching folder path
|
||||||
|
const folderLi = document.querySelector(
|
||||||
|
`[data-folderpath='/${folderUl.path}']`,
|
||||||
|
) as HTMLElement
|
||||||
|
|
||||||
|
// Get corresponding content <ul> tag and set state
|
||||||
|
if (folderLi) {
|
||||||
|
const folderUL = folderLi.parentElement?.nextElementSibling
|
||||||
|
if (folderUL) {
|
||||||
|
setFolderState(folderUL as HTMLElement, folderUl.collapsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (explorer?.dataset.tree) {
|
||||||
|
// If tree is not in localStorage or config is disabled, use tree passed from Explorer as dataset
|
||||||
|
explorerState = JSON.parse(explorer.dataset.tree)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("resize", setupExplorer)
|
||||||
|
document.addEventListener("nav", () => {
|
||||||
|
setupExplorer()
|
||||||
|
|
||||||
|
observer.disconnect()
|
||||||
|
|
||||||
|
// select pseudo element at end of list
|
||||||
|
const lastItem = document.getElementById("explorer-end")
|
||||||
|
if (lastItem) {
|
||||||
|
observer.observe(lastItem)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the state of a given folder
|
||||||
|
* @param folderElement <div class="folder-outer"> Element of folder (parent)
|
||||||
|
* @param collapsed if folder should be set to collapsed or not
|
||||||
|
*/
|
||||||
|
function setFolderState(folderElement: HTMLElement, collapsed: boolean) {
|
||||||
|
if (collapsed) {
|
||||||
|
folderElement?.classList.remove("open")
|
||||||
|
} else {
|
||||||
|
folderElement?.classList.add("open")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles visibility of a folder
|
||||||
|
* @param array array of FolderState (`fileTree`, either get from local storage or data attribute)
|
||||||
|
* @param path path to folder (e.g. 'advanced/more/more2')
|
||||||
|
*/
|
||||||
|
function toggleCollapsedByPath(array: FolderState[], path: string) {
|
||||||
|
const entry = array.find((item) => item.path === path)
|
||||||
|
if (entry) {
|
||||||
|
entry.collapsed = !entry.collapsed
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ContentDetails } from "../../plugins/emitters/contentIndex"
|
import type { ContentDetails, ContentIndex } from "../../plugins/emitters/contentIndex"
|
||||||
import * as d3 from "d3"
|
import * as d3 from "d3"
|
||||||
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"
|
||||||
@@ -42,17 +42,38 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
linkDistance,
|
linkDistance,
|
||||||
fontSize,
|
fontSize,
|
||||||
opacityScale,
|
opacityScale,
|
||||||
|
removeTags,
|
||||||
|
showTags,
|
||||||
} = JSON.parse(graph.dataset["cfg"]!)
|
} = JSON.parse(graph.dataset["cfg"]!)
|
||||||
|
|
||||||
const data = await fetchData
|
const data: Map<SimpleSlug, ContentDetails> = new Map(
|
||||||
|
Object.entries<ContentDetails>(await fetchData).map(([k, v]) => [
|
||||||
|
simplifySlug(k as FullSlug),
|
||||||
|
v,
|
||||||
|
]),
|
||||||
|
)
|
||||||
const links: LinkData[] = []
|
const links: LinkData[] = []
|
||||||
for (const [src, details] of Object.entries<ContentDetails>(data)) {
|
const tags: SimpleSlug[] = []
|
||||||
const source = simplifySlug(src as FullSlug)
|
|
||||||
|
const validLinks = new Set(data.keys())
|
||||||
|
for (const [source, details] of data.entries()) {
|
||||||
const outgoing = details.links ?? []
|
const outgoing = details.links ?? []
|
||||||
|
|
||||||
for (const dest of outgoing) {
|
for (const dest of outgoing) {
|
||||||
if (dest in data) {
|
if (validLinks.has(dest)) {
|
||||||
links.push({ source, target: dest })
|
links.push({ source: source, target: dest })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showTags) {
|
||||||
|
const localTags = details.tags
|
||||||
|
.filter((tag) => !removeTags.includes(tag))
|
||||||
|
.map((tag) => simplifySlug(("tags/" + tag) as FullSlug))
|
||||||
|
|
||||||
|
tags.push(...localTags.filter((tag) => !tags.includes(tag)))
|
||||||
|
|
||||||
|
for (const tag of localTags) {
|
||||||
|
links.push({ source: source, target: tag })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,15 +95,19 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Object.keys(data).forEach((id) => neighbourhood.add(simplifySlug(id as FullSlug)))
|
validLinks.forEach((id) => neighbourhood.add(id))
|
||||||
|
if (showTags) tags.forEach((tag) => neighbourhood.add(tag))
|
||||||
}
|
}
|
||||||
|
|
||||||
const graphData: { nodes: NodeData[]; links: LinkData[] } = {
|
const graphData: { nodes: NodeData[]; links: LinkData[] } = {
|
||||||
nodes: [...neighbourhood].map((url) => ({
|
nodes: [...neighbourhood].map((url) => {
|
||||||
id: url,
|
const text = url.startsWith("tags/") ? "#" + url.substring(5) : data.get(url)?.title ?? url
|
||||||
text: data[url]?.title ?? url,
|
return {
|
||||||
tags: data[url]?.tags ?? [],
|
id: url,
|
||||||
})),
|
text: text,
|
||||||
|
tags: data.get(url)?.tags ?? [],
|
||||||
|
}
|
||||||
|
}),
|
||||||
links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)),
|
links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +151,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
const isCurrent = d.id === slug
|
const isCurrent = d.id === slug
|
||||||
if (isCurrent) {
|
if (isCurrent) {
|
||||||
return "var(--secondary)"
|
return "var(--secondary)"
|
||||||
} else if (visited.has(d.id)) {
|
} else if (visited.has(d.id) || d.id.startsWith("tags/")) {
|
||||||
return "var(--tertiary)"
|
return "var(--tertiary)"
|
||||||
} else {
|
} else {
|
||||||
return "var(--gray)"
|
return "var(--gray)"
|
||||||
@@ -177,7 +202,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
window.spaNavigate(new URL(targ, window.location.toString()))
|
window.spaNavigate(new URL(targ, window.location.toString()))
|
||||||
})
|
})
|
||||||
.on("mouseover", function (_, d) {
|
.on("mouseover", function (_, d) {
|
||||||
const neighbours: SimpleSlug[] = data[fullSlug].links ?? []
|
const neighbours: SimpleSlug[] = data.get(slug)?.links ?? []
|
||||||
const neighbourNodes = d3
|
const neighbourNodes = d3
|
||||||
.selectAll<HTMLElement, NodeData>(".node")
|
.selectAll<HTMLElement, NodeData>(".node")
|
||||||
.filter((d) => neighbours.includes(d.id))
|
.filter((d) => neighbours.includes(d.id))
|
||||||
@@ -230,9 +255,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
.attr("dx", 0)
|
.attr("dx", 0)
|
||||||
.attr("dy", (d) => -nodeRadius(d) + "px")
|
.attr("dy", (d) => -nodeRadius(d) + "px")
|
||||||
.attr("text-anchor", "middle")
|
.attr("text-anchor", "middle")
|
||||||
.text(
|
.text((d) => d.text)
|
||||||
(d) => data[d.id]?.title || (d.id.charAt(1).toUpperCase() + d.id.slice(2)).replace("-", " "),
|
|
||||||
)
|
|
||||||
.style("opacity", (opacityScale - 1) / 3.75)
|
.style("opacity", (opacityScale - 1) / 3.75)
|
||||||
.style("pointer-events", "none")
|
.style("pointer-events", "none")
|
||||||
.style("font-size", fontSize + "em")
|
.style("font-size", fontSize + "em")
|
||||||
|
|||||||
@@ -1,16 +1,5 @@
|
|||||||
import { computePosition, flip, inline, shift } from "@floating-ui/dom"
|
import { computePosition, flip, inline, shift } from "@floating-ui/dom"
|
||||||
|
import { normalizeRelativeURLs } from "../../util/path"
|
||||||
// from micromorph/src/utils.ts
|
|
||||||
// https://github.com/natemoo-re/micromorph/blob/main/src/utils.ts#L5
|
|
||||||
export function normalizeRelativeURLs(el: Element | Document, base: string | URL) {
|
|
||||||
const update = (el: Element, attr: string, base: string | URL) => {
|
|
||||||
el.setAttribute(attr, new URL(el.getAttribute(attr)!, base).pathname)
|
|
||||||
}
|
|
||||||
|
|
||||||
el.querySelectorAll('[href^="./"], [href^="../"]').forEach((item) => update(item, "href", base))
|
|
||||||
|
|
||||||
el.querySelectorAll('[src^="./"], [src^="../"]').forEach((item) => update(item, "src", base))
|
|
||||||
}
|
|
||||||
|
|
||||||
const p = new DOMParser()
|
const p = new DOMParser()
|
||||||
async function mouseEnterHandler(
|
async function mouseEnterHandler(
|
||||||
@@ -28,8 +17,11 @@ async function mouseEnterHandler(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasAlreadyBeenFetched = () =>
|
||||||
|
[...link.children].some((child) => child.classList.contains("popover"))
|
||||||
|
|
||||||
// dont refetch if there's already a popover
|
// dont refetch if there's already a popover
|
||||||
if ([...link.children].some((child) => child.classList.contains("popover"))) {
|
if (hasAlreadyBeenFetched()) {
|
||||||
return setPosition(link.lastChild as HTMLElement)
|
return setPosition(link.lastChild as HTMLElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,8 +32,6 @@ async function mouseEnterHandler(
|
|||||||
const hash = targetUrl.hash
|
const hash = targetUrl.hash
|
||||||
targetUrl.hash = ""
|
targetUrl.hash = ""
|
||||||
targetUrl.search = ""
|
targetUrl.search = ""
|
||||||
// prevent hover of the same page
|
|
||||||
if (thisUrl.toString() === targetUrl.toString()) return
|
|
||||||
|
|
||||||
const contents = await fetch(`${targetUrl}`)
|
const contents = await fetch(`${targetUrl}`)
|
||||||
.then((res) => res.text())
|
.then((res) => res.text())
|
||||||
@@ -49,6 +39,11 @@ async function mouseEnterHandler(
|
|||||||
console.error(err)
|
console.error(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// bailout if another popover exists
|
||||||
|
if (hasAlreadyBeenFetched()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!contents) return
|
if (!contents) return
|
||||||
const html = p.parseFromString(contents, "text/html")
|
const html = p.parseFromString(contents, "text/html")
|
||||||
normalizeRelativeURLs(html, targetUrl)
|
normalizeRelativeURLs(html, targetUrl)
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
import { Document } from "flexsearch"
|
import { Document, SimpleDocumentSearchResultSetUnit } from "flexsearch"
|
||||||
import { ContentDetails } from "../../plugins/emitters/contentIndex"
|
import { ContentDetails } from "../../plugins/emitters/contentIndex"
|
||||||
import { registerEscapeHandler, removeAllChildren } from "./util"
|
import { registerEscapeHandler, removeAllChildren } from "./util"
|
||||||
import { FullSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path"
|
import { FullSlug, resolveRelative } from "../../util/path"
|
||||||
|
|
||||||
interface Item {
|
interface Item {
|
||||||
id: number
|
id: number
|
||||||
slug: FullSlug
|
slug: FullSlug
|
||||||
title: string
|
title: string
|
||||||
content: string
|
content: string
|
||||||
|
tags: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
let index: Document<Item> | undefined = undefined
|
let index: Document<Item> | undefined = undefined
|
||||||
|
|
||||||
|
// Can be expanded with things like "term" in the future
|
||||||
|
type SearchType = "basic" | "tags"
|
||||||
|
|
||||||
|
// Current searchType
|
||||||
|
let searchType: SearchType = "basic"
|
||||||
|
|
||||||
const contextWindowWords = 30
|
const contextWindowWords = 30
|
||||||
const numSearchResults = 5
|
const numSearchResults = 5
|
||||||
|
const numTagResults = 3
|
||||||
function highlight(searchTerm: string, text: string, trim?: boolean) {
|
function highlight(searchTerm: string, text: string, trim?: boolean) {
|
||||||
// try to highlight longest tokens first
|
// try to highlight longest tokens first
|
||||||
const tokenizedTerms = searchTerm
|
const tokenizedTerms = searchTerm
|
||||||
@@ -64,6 +72,7 @@ function highlight(searchTerm: string, text: string, trim?: boolean) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
|
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
|
||||||
|
let prevShortcutHandler: ((e: HTMLElementEventMap["keydown"]) => void) | undefined = undefined
|
||||||
document.addEventListener("nav", async (e: unknown) => {
|
document.addEventListener("nav", async (e: unknown) => {
|
||||||
const currentSlug = (e as CustomEventMap["nav"]).detail.url
|
const currentSlug = (e as CustomEventMap["nav"]).detail.url
|
||||||
|
|
||||||
@@ -73,6 +82,7 @@ document.addEventListener("nav", async (e: unknown) => {
|
|||||||
const searchIcon = document.getElementById("search-icon")
|
const searchIcon = document.getElementById("search-icon")
|
||||||
const searchBar = document.getElementById("search-bar") as HTMLInputElement | null
|
const searchBar = document.getElementById("search-bar") as HTMLInputElement | null
|
||||||
const results = document.getElementById("results-container")
|
const results = document.getElementById("results-container")
|
||||||
|
const resultCards = document.getElementsByClassName("result-card")
|
||||||
const idDataMap = Object.keys(data) as FullSlug[]
|
const idDataMap = Object.keys(data) as FullSlug[]
|
||||||
|
|
||||||
function hideSearch() {
|
function hideSearch() {
|
||||||
@@ -86,9 +96,12 @@ document.addEventListener("nav", async (e: unknown) => {
|
|||||||
if (results) {
|
if (results) {
|
||||||
removeAllChildren(results)
|
removeAllChildren(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
searchType = "basic" // reset search type after closing
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSearch() {
|
function showSearch(searchTypeNew: SearchType) {
|
||||||
|
searchType = searchTypeNew
|
||||||
if (sidebar) {
|
if (sidebar) {
|
||||||
sidebar.style.zIndex = "1"
|
sidebar.style.zIndex = "1"
|
||||||
}
|
}
|
||||||
@@ -97,36 +110,123 @@ document.addEventListener("nav", async (e: unknown) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
|
function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
|
||||||
if (e.key === "k" && (e.ctrlKey || e.metaKey)) {
|
if (e.key === "k" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const searchBarOpen = container?.classList.contains("active")
|
const searchBarOpen = container?.classList.contains("active")
|
||||||
searchBarOpen ? hideSearch() : showSearch()
|
searchBarOpen ? hideSearch() : showSearch("basic")
|
||||||
|
} else if (e.shiftKey && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
|
||||||
|
// Hotkey to open tag search
|
||||||
|
e.preventDefault()
|
||||||
|
const searchBarOpen = container?.classList.contains("active")
|
||||||
|
searchBarOpen ? hideSearch() : showSearch("tags")
|
||||||
|
|
||||||
|
// add "#" prefix for tag search
|
||||||
|
if (searchBar) searchBar.value = "#"
|
||||||
} else if (e.key === "Enter") {
|
} else if (e.key === "Enter") {
|
||||||
const anchor = document.getElementsByClassName("result-card")[0] as HTMLInputElement | null
|
// If result has focus, navigate to that one, otherwise pick first result
|
||||||
if (anchor) {
|
if (results?.contains(document.activeElement)) {
|
||||||
anchor.click()
|
const active = document.activeElement as HTMLInputElement
|
||||||
|
active.click()
|
||||||
|
} else {
|
||||||
|
const anchor = document.getElementsByClassName("result-card")[0] as HTMLInputElement | null
|
||||||
|
anchor?.click()
|
||||||
|
}
|
||||||
|
} else if (e.key === "ArrowDown") {
|
||||||
|
e.preventDefault()
|
||||||
|
// When first pressing ArrowDown, results wont contain the active element, so focus first element
|
||||||
|
if (!results?.contains(document.activeElement)) {
|
||||||
|
const firstResult = resultCards[0] as HTMLInputElement | null
|
||||||
|
firstResult?.focus()
|
||||||
|
} else {
|
||||||
|
// If an element in results-container already has focus, focus next one
|
||||||
|
const nextResult = document.activeElement?.nextElementSibling as HTMLInputElement | null
|
||||||
|
nextResult?.focus()
|
||||||
|
}
|
||||||
|
} else if (e.key === "ArrowUp") {
|
||||||
|
e.preventDefault()
|
||||||
|
if (results?.contains(document.activeElement)) {
|
||||||
|
// If an element in results-container already has focus, focus previous one
|
||||||
|
const prevResult = document.activeElement?.previousElementSibling as HTMLInputElement | null
|
||||||
|
prevResult?.focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trimContent(content: string) {
|
||||||
|
// works without escaping html like in `description.ts`
|
||||||
|
const sentences = content.replace(/\s+/g, " ").split(".")
|
||||||
|
let finalDesc = ""
|
||||||
|
let sentenceIdx = 0
|
||||||
|
|
||||||
|
// Roughly estimate characters by (words * 5). Matches description length in `description.ts`.
|
||||||
|
const len = contextWindowWords * 5
|
||||||
|
while (finalDesc.length < len) {
|
||||||
|
const sentence = sentences[sentenceIdx]
|
||||||
|
if (!sentence) break
|
||||||
|
finalDesc += sentence + "."
|
||||||
|
sentenceIdx++
|
||||||
|
}
|
||||||
|
|
||||||
|
// If more content would be available, indicate it by finishing with "..."
|
||||||
|
if (finalDesc.length < content.length) {
|
||||||
|
finalDesc += ".."
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalDesc
|
||||||
|
}
|
||||||
|
|
||||||
const formatForDisplay = (term: string, id: number) => {
|
const formatForDisplay = (term: string, id: number) => {
|
||||||
const slug = idDataMap[id]
|
const slug = idDataMap[id]
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
slug,
|
slug,
|
||||||
title: highlight(term, data[slug].title ?? ""),
|
title: searchType === "tags" ? data[slug].title : highlight(term, data[slug].title ?? ""),
|
||||||
content: highlight(term, data[slug].content ?? "", true),
|
// if searchType is tag, display context from start of file and trim, otherwise use regular highlight
|
||||||
|
content:
|
||||||
|
searchType === "tags"
|
||||||
|
? trimContent(data[slug].content)
|
||||||
|
: highlight(term, data[slug].content ?? "", true),
|
||||||
|
tags: highlightTags(term, data[slug].tags),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resultToHTML = ({ slug, title, content }: Item) => {
|
function highlightTags(term: string, tags: string[]) {
|
||||||
|
if (tags && searchType === "tags") {
|
||||||
|
// Find matching tags
|
||||||
|
const termLower = term.toLowerCase()
|
||||||
|
let matching = tags.filter((str) => str.includes(termLower))
|
||||||
|
|
||||||
|
// Substract matching from original tags, then push difference
|
||||||
|
if (matching.length > 0) {
|
||||||
|
let difference = tags.filter((x) => !matching.includes(x))
|
||||||
|
|
||||||
|
// Convert to html (cant be done later as matches/term dont get passed to `resultToHTML`)
|
||||||
|
matching = matching.map((tag) => `<li><p class="match-tag">#${tag}</p></li>`)
|
||||||
|
difference = difference.map((tag) => `<li><p>#${tag}</p></li>`)
|
||||||
|
matching.push(...difference)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow max of `numTagResults` in preview
|
||||||
|
if (tags.length > numTagResults) {
|
||||||
|
matching.splice(numTagResults)
|
||||||
|
}
|
||||||
|
|
||||||
|
return matching
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultToHTML = ({ slug, title, content, tags }: Item) => {
|
||||||
|
const htmlTags = tags.length > 0 ? `<ul>${tags.join("")}</ul>` : ``
|
||||||
const button = document.createElement("button")
|
const button = document.createElement("button")
|
||||||
button.classList.add("result-card")
|
button.classList.add("result-card")
|
||||||
button.id = slug
|
button.id = slug
|
||||||
button.innerHTML = `<h3>${title}</h3><p>${content}</p>`
|
button.innerHTML = `<h3>${title}</h3>${htmlTags}<p>${content}</p>`
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
const targ = resolveRelative(currentSlug, slug)
|
const targ = resolveRelative(currentSlug, slug)
|
||||||
window.spaNavigate(new URL(targ, window.location.toString()))
|
window.spaNavigate(new URL(targ, window.location.toString()))
|
||||||
|
hideSearch()
|
||||||
})
|
})
|
||||||
return button
|
return button
|
||||||
}
|
}
|
||||||
@@ -146,30 +246,63 @@ document.addEventListener("nav", async (e: unknown) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onType(e: HTMLElementEventMap["input"]) {
|
async function onType(e: HTMLElementEventMap["input"]) {
|
||||||
const term = (e.target as HTMLInputElement).value
|
let term = (e.target as HTMLInputElement).value
|
||||||
const searchResults = (await index?.searchAsync(term, numSearchResults)) ?? []
|
let searchResults: SimpleDocumentSearchResultSetUnit[]
|
||||||
|
|
||||||
|
if (term.toLowerCase().startsWith("#")) {
|
||||||
|
searchType = "tags"
|
||||||
|
} else {
|
||||||
|
searchType = "basic"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (searchType) {
|
||||||
|
case "tags": {
|
||||||
|
term = term.substring(1)
|
||||||
|
searchResults =
|
||||||
|
(await index?.searchAsync({ query: term, limit: numSearchResults, index: ["tags"] })) ??
|
||||||
|
[]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "basic":
|
||||||
|
default: {
|
||||||
|
searchResults =
|
||||||
|
(await index?.searchAsync({
|
||||||
|
query: term,
|
||||||
|
limit: numSearchResults,
|
||||||
|
index: ["title", "content"],
|
||||||
|
})) ?? []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getByField = (field: string): number[] => {
|
const getByField = (field: string): number[] => {
|
||||||
const results = searchResults.filter((x) => x.field === field)
|
const results = searchResults.filter((x) => x.field === field)
|
||||||
return results.length === 0 ? [] : ([...results[0].result] as number[])
|
return results.length === 0 ? [] : ([...results[0].result] as number[])
|
||||||
}
|
}
|
||||||
|
|
||||||
// order titles ahead of content
|
// order titles ahead of content
|
||||||
const allIds: Set<number> = new Set([...getByField("title"), ...getByField("content")])
|
const allIds: Set<number> = new Set([
|
||||||
|
...getByField("title"),
|
||||||
|
...getByField("content"),
|
||||||
|
...getByField("tags"),
|
||||||
|
])
|
||||||
const finalResults = [...allIds].map((id) => formatForDisplay(term, id))
|
const finalResults = [...allIds].map((id) => formatForDisplay(term, id))
|
||||||
displayResults(finalResults)
|
displayResults(finalResults)
|
||||||
}
|
}
|
||||||
|
|
||||||
document.removeEventListener("keydown", shortcutHandler)
|
if (prevShortcutHandler) {
|
||||||
|
document.removeEventListener("keydown", prevShortcutHandler)
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener("keydown", shortcutHandler)
|
document.addEventListener("keydown", shortcutHandler)
|
||||||
searchIcon?.removeEventListener("click", showSearch)
|
prevShortcutHandler = shortcutHandler
|
||||||
searchIcon?.addEventListener("click", showSearch)
|
searchIcon?.removeEventListener("click", () => showSearch("basic"))
|
||||||
|
searchIcon?.addEventListener("click", () => showSearch("basic"))
|
||||||
searchBar?.removeEventListener("input", onType)
|
searchBar?.removeEventListener("input", onType)
|
||||||
searchBar?.addEventListener("input", onType)
|
searchBar?.addEventListener("input", onType)
|
||||||
|
|
||||||
// setup index if it hasn't been already
|
// setup index if it hasn't been already
|
||||||
if (!index) {
|
if (!index) {
|
||||||
index = new Document({
|
index = new Document({
|
||||||
cache: true,
|
|
||||||
charset: "latin:extra",
|
charset: "latin:extra",
|
||||||
optimize: true,
|
optimize: true,
|
||||||
encode: encoder,
|
encode: encoder,
|
||||||
@@ -184,22 +317,36 @@ document.addEventListener("nav", async (e: unknown) => {
|
|||||||
field: "content",
|
field: "content",
|
||||||
tokenize: "reverse",
|
tokenize: "reverse",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: "tags",
|
||||||
|
tokenize: "reverse",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
let id = 0
|
fillDocument(index, data)
|
||||||
for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
|
|
||||||
await index.addAsync(id, {
|
|
||||||
id,
|
|
||||||
slug: slug as FullSlug,
|
|
||||||
title: fileData.title,
|
|
||||||
content: fileData.content,
|
|
||||||
})
|
|
||||||
id++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// register handlers
|
// register handlers
|
||||||
registerEscapeHandler(container, hideSearch)
|
registerEscapeHandler(container, hideSearch)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills flexsearch document with data
|
||||||
|
* @param index index to fill
|
||||||
|
* @param data data to fill index with
|
||||||
|
*/
|
||||||
|
async function fillDocument(index: Document<Item, false>, data: any) {
|
||||||
|
let id = 0
|
||||||
|
for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
|
||||||
|
await index.addAsync(id, {
|
||||||
|
id,
|
||||||
|
slug: slug as FullSlug,
|
||||||
|
title: fileData.title,
|
||||||
|
content: fileData.content,
|
||||||
|
tags: fileData.tags,
|
||||||
|
})
|
||||||
|
id++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import micromorph from "micromorph"
|
import micromorph from "micromorph"
|
||||||
import { FullSlug, RelativeURL, getFullSlug } from "../../util/path"
|
import { FullSlug, RelativeURL, getFullSlug, normalizeRelativeURLs } from "../../util/path"
|
||||||
|
|
||||||
// adapted from `micromorph`
|
// adapted from `micromorph`
|
||||||
// https://github.com/natemoo-re/micromorph
|
// https://github.com/natemoo-re/micromorph
|
||||||
|
|
||||||
const NODE_TYPE_ELEMENT = 1
|
const NODE_TYPE_ELEMENT = 1
|
||||||
let announcer = document.createElement("route-announcer")
|
let announcer = document.createElement("route-announcer")
|
||||||
const isElement = (target: EventTarget | null): target is Element =>
|
const isElement = (target: EventTarget | null): target is Element =>
|
||||||
@@ -12,17 +11,21 @@ const isLocalUrl = (href: string) => {
|
|||||||
try {
|
try {
|
||||||
const url = new URL(href)
|
const url = new URL(href)
|
||||||
if (window.location.origin === url.origin) {
|
if (window.location.origin === url.origin) {
|
||||||
if (url.pathname === window.location.pathname) {
|
|
||||||
return !url.hash
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSamePage = (url: URL): boolean => {
|
||||||
|
const sameOrigin = url.origin === window.location.origin
|
||||||
|
const samePath = url.pathname === window.location.pathname
|
||||||
|
return sameOrigin && samePath
|
||||||
|
}
|
||||||
|
|
||||||
const getOpts = ({ target }: Event): { url: URL; scroll?: boolean } | undefined => {
|
const getOpts = ({ target }: Event): { url: URL; scroll?: boolean } | undefined => {
|
||||||
if (!isElement(target)) return
|
if (!isElement(target)) return
|
||||||
|
if (target.attributes.getNamedItem("target")?.value === "_blank") return
|
||||||
const a = target.closest("a")
|
const a = target.closest("a")
|
||||||
if (!a) return
|
if (!a) return
|
||||||
if ("routerIgnore" in a.dataset) return
|
if ("routerIgnore" in a.dataset) return
|
||||||
@@ -40,7 +43,14 @@ let p: DOMParser
|
|||||||
async function navigate(url: URL, isBack: boolean = false) {
|
async function navigate(url: URL, isBack: boolean = false) {
|
||||||
p = p || new DOMParser()
|
p = p || new DOMParser()
|
||||||
const contents = await fetch(`${url}`)
|
const contents = await fetch(`${url}`)
|
||||||
.then((res) => res.text())
|
.then((res) => {
|
||||||
|
const contentType = res.headers.get("content-type")
|
||||||
|
if (contentType?.startsWith("text/html")) {
|
||||||
|
return res.text()
|
||||||
|
} else {
|
||||||
|
window.location.assign(url)
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
window.location.assign(url)
|
window.location.assign(url)
|
||||||
})
|
})
|
||||||
@@ -48,6 +58,8 @@ async function navigate(url: URL, isBack: boolean = false) {
|
|||||||
if (!contents) return
|
if (!contents) return
|
||||||
|
|
||||||
const html = p.parseFromString(contents, "text/html")
|
const html = p.parseFromString(contents, "text/html")
|
||||||
|
normalizeRelativeURLs(html, url)
|
||||||
|
|
||||||
let title = html.querySelector("title")?.textContent
|
let title = html.querySelector("title")?.textContent
|
||||||
if (title) {
|
if (title) {
|
||||||
document.title = title
|
document.title = title
|
||||||
@@ -67,7 +79,7 @@ async function navigate(url: URL, isBack: boolean = false) {
|
|||||||
// scroll into place and add history
|
// scroll into place and add history
|
||||||
if (!isBack) {
|
if (!isBack) {
|
||||||
if (url.hash) {
|
if (url.hash) {
|
||||||
const el = document.getElementById(url.hash.substring(1))
|
const el = document.getElementById(decodeURIComponent(url.hash.substring(1)))
|
||||||
el?.scrollIntoView()
|
el?.scrollIntoView()
|
||||||
} else {
|
} else {
|
||||||
window.scrollTo({ top: 0 })
|
window.scrollTo({ top: 0 })
|
||||||
@@ -82,7 +94,9 @@ async function navigate(url: URL, isBack: boolean = false) {
|
|||||||
|
|
||||||
// delay setting the url until now
|
// delay setting the url until now
|
||||||
// at this point everything is loaded so changing the url should resolve to the correct addresses
|
// at this point everything is loaded so changing the url should resolve to the correct addresses
|
||||||
history.pushState({}, "", url)
|
if (!isBack) {
|
||||||
|
history.pushState({}, "", url)
|
||||||
|
}
|
||||||
notifyNav(getFullSlug(window))
|
notifyNav(getFullSlug(window))
|
||||||
delete announcer.dataset.persist
|
delete announcer.dataset.persist
|
||||||
}
|
}
|
||||||
@@ -93,8 +107,17 @@ function createRouter() {
|
|||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
window.addEventListener("click", async (event) => {
|
window.addEventListener("click", async (event) => {
|
||||||
const { url } = getOpts(event) ?? {}
|
const { url } = getOpts(event) ?? {}
|
||||||
if (!url) return
|
// dont hijack behaviour, just let browser act normally
|
||||||
|
if (!url || event.ctrlKey || event.metaKey) return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
|
if (isSamePage(url) && url.hash) {
|
||||||
|
const el = document.getElementById(decodeURIComponent(url.hash.substring(1)))
|
||||||
|
el?.scrollIntoView()
|
||||||
|
history.pushState({}, "", url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
navigate(url, false)
|
navigate(url, false)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -140,6 +163,7 @@ if (!customElements.get("route-announcer")) {
|
|||||||
style:
|
style:
|
||||||
"position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px",
|
"position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px",
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define(
|
customElements.define(
|
||||||
"route-announcer",
|
"route-announcer",
|
||||||
class RouteAnnouncer extends HTMLElement {
|
class RouteAnnouncer extends HTMLElement {
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ function toggleToc(this: HTMLElement) {
|
|||||||
function setupToc() {
|
function setupToc() {
|
||||||
const toc = document.getElementById("toc")
|
const toc = document.getElementById("toc")
|
||||||
if (toc) {
|
if (toc) {
|
||||||
|
const collapsed = toc.classList.contains("collapsed")
|
||||||
const content = toc.nextElementSibling as HTMLElement
|
const content = toc.nextElementSibling as HTMLElement
|
||||||
content.style.maxHeight = content.scrollHeight + "px"
|
content.style.maxHeight = collapsed ? "0px" : content.scrollHeight + "px"
|
||||||
toc.removeEventListener("click", toggleToc)
|
toc.removeEventListener("click", toggleToc)
|
||||||
toc.addEventListener("click", toggleToc)
|
toc.addEventListener("click", toggleToc)
|
||||||
}
|
}
|
||||||
|
|||||||
22
quartz/components/styles/breadcrumbs.scss
Normal file
22
quartz/components/styles/breadcrumbs.scss
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
.breadcrumb-container {
|
||||||
|
margin: 0;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-element {
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
padding: 0;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@
|
|||||||
background-color: var(--light);
|
background-color: var(--light);
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
z-index: 1;
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: 0.2s;
|
transition: 0.2s;
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root[saved-theme="dark"] {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[saved-theme="light"] {
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
|
||||||
:root[saved-theme="dark"] .toggle ~ label {
|
:root[saved-theme="dark"] .toggle ~ label {
|
||||||
& > #dayIcon {
|
& > #dayIcon {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
146
quartz/components/styles/explorer.scss
Normal file
146
quartz/components/styles/explorer.scss
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
button#explorer {
|
||||||
|
all: unset;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--dark);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
& h1 {
|
||||||
|
font-size: 1rem;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .fold {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.collapsed .fold {
|
||||||
|
transform: rotateZ(-90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-outer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 0fr;
|
||||||
|
transition: grid-template-rows 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-outer.open {
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-outer > ul {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#explorer-content {
|
||||||
|
list-style: none;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: none;
|
||||||
|
transition: max-height 0.35s ease;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
|
||||||
|
&.collapsed > .overflow::after {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0.08rem 0;
|
||||||
|
padding: 0;
|
||||||
|
transition:
|
||||||
|
max-height 0.35s ease,
|
||||||
|
transform 0.35s ease,
|
||||||
|
opacity 0.2s ease;
|
||||||
|
& li > a {
|
||||||
|
color: var(--dark);
|
||||||
|
opacity: 0.75;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
pointer-events: all;
|
||||||
|
|
||||||
|
& > polyline {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-container {
|
||||||
|
flex-direction: row;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
& div > a {
|
||||||
|
color: var(--secondary);
|
||||||
|
font-family: var(--headerFont);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
& div > a:hover {
|
||||||
|
color: var(--tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
& div > button {
|
||||||
|
color: var(--dark);
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-family: var(--headerFont);
|
||||||
|
|
||||||
|
& p {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
display: inline-block;
|
||||||
|
color: var(--secondary);
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
color: var(--secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
backface-visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
div:has(> .folder-outer:not(.open)) > .folder-container > svg {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-icon:hover {
|
||||||
|
color: var(--tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-background::after {
|
||||||
|
background: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#explorer-end {
|
||||||
|
// needs height so IntersectionObserver gets triggered
|
||||||
|
height: 4px;
|
||||||
|
// remove default margin from li
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
@@ -19,11 +19,6 @@ li.section-li {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .tags {
|
|
||||||
justify-self: end;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .desc > h3 > a {
|
& > .desc > h3 > a {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
box-shadow: 6px 6px 36px 0 rgba(0, 0, 0, 0.25);
|
box-shadow: 6px 6px 36px 0 rgba(0, 0, 0, 0.25);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
|||||||
@@ -130,6 +130,44 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& > ul > li {
|
||||||
|
margin: 0;
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin: 0;
|
||||||
|
overflow-wrap: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > ul {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
padding-left: 0;
|
||||||
|
gap: 0.4rem;
|
||||||
|
margin: 0;
|
||||||
|
margin-top: 0.45rem;
|
||||||
|
// Offset border radius
|
||||||
|
margin-left: -2px;
|
||||||
|
overflow: hidden;
|
||||||
|
background-clip: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > ul > li > p {
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--highlight);
|
||||||
|
overflow: hidden;
|
||||||
|
background-clip: border-box;
|
||||||
|
padding: 0.03rem 0.4rem;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--secondary);
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > ul > li > .match-tag {
|
||||||
|
color: var(--tertiary);
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
& > p {
|
& > p {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ button#toc {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
transition: max-height 0.5s ease;
|
transition: max-height 0.5s ease;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&.collapsed > .overflow::after {
|
&.collapsed > .overflow::after {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
59
quartz/plugins/emitters/404.tsx
Normal file
59
quartz/plugins/emitters/404.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { QuartzEmitterPlugin } from "../types"
|
||||||
|
import { QuartzComponentProps } from "../../components/types"
|
||||||
|
import BodyConstructor from "../../components/Body"
|
||||||
|
import { pageResources, renderPage } from "../../components/renderPage"
|
||||||
|
import { FullPageLayout } from "../../cfg"
|
||||||
|
import { FilePath, FullSlug } from "../../util/path"
|
||||||
|
import { sharedPageComponents } from "../../../quartz.layout"
|
||||||
|
import { NotFound } from "../../components"
|
||||||
|
import { defaultProcessedContent } from "../vfile"
|
||||||
|
|
||||||
|
export const NotFoundPage: QuartzEmitterPlugin = () => {
|
||||||
|
const opts: FullPageLayout = {
|
||||||
|
...sharedPageComponents,
|
||||||
|
pageBody: NotFound(),
|
||||||
|
beforeBody: [],
|
||||||
|
left: [],
|
||||||
|
right: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
const { head: Head, pageBody, footer: Footer } = opts
|
||||||
|
const Body = BodyConstructor()
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "404Page",
|
||||||
|
getQuartzComponents() {
|
||||||
|
return [Head, Body, pageBody, Footer]
|
||||||
|
},
|
||||||
|
async emit(ctx, _content, resources, emit): Promise<FilePath[]> {
|
||||||
|
const cfg = ctx.cfg.configuration
|
||||||
|
const slug = "404" as FullSlug
|
||||||
|
|
||||||
|
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
|
||||||
|
const path = url.pathname as FullSlug
|
||||||
|
const externalResources = pageResources(path, resources)
|
||||||
|
const [tree, vfile] = defaultProcessedContent({
|
||||||
|
slug,
|
||||||
|
text: "Not Found",
|
||||||
|
description: "Not Found",
|
||||||
|
frontmatter: { title: "Not Found", tags: [] },
|
||||||
|
})
|
||||||
|
const componentData: QuartzComponentProps = {
|
||||||
|
fileData: vfile.data,
|
||||||
|
externalResources,
|
||||||
|
cfg,
|
||||||
|
children: [],
|
||||||
|
tree,
|
||||||
|
allFiles: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
await emit({
|
||||||
|
content: renderPage(slug, componentData, opts, externalResources),
|
||||||
|
slug,
|
||||||
|
ext: ".html",
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FilePath, FullSlug, resolveRelative, simplifySlug } from "../../util/path"
|
import { FilePath, FullSlug, joinSegments, resolveRelative, simplifySlug } from "../../util/path"
|
||||||
import { QuartzEmitterPlugin } from "../types"
|
import { QuartzEmitterPlugin } from "../types"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
@@ -12,15 +12,25 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
|
|||||||
|
|
||||||
for (const [_tree, file] of content) {
|
for (const [_tree, file] of content) {
|
||||||
const ogSlug = simplifySlug(file.data.slug!)
|
const ogSlug = simplifySlug(file.data.slug!)
|
||||||
const dir = path.posix.relative(argv.directory, file.dirname ?? argv.directory)
|
const dir = path.posix.relative(argv.directory, path.dirname(file.data.filePath!))
|
||||||
|
|
||||||
let aliases: FullSlug[] = file.data.frontmatter?.aliases ?? file.data.frontmatter?.alias ?? []
|
let aliases: FullSlug[] = file.data.frontmatter?.aliases ?? file.data.frontmatter?.alias ?? []
|
||||||
if (typeof aliases === "string") {
|
if (typeof aliases === "string") {
|
||||||
aliases = [aliases]
|
aliases = [aliases]
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const alias of aliases) {
|
const slugs: FullSlug[] = aliases.map((alias) => path.posix.join(dir, alias) as FullSlug)
|
||||||
const slug = path.posix.join(dir, alias) as FullSlug
|
const permalink = file.data.frontmatter?.permalink
|
||||||
|
if (typeof permalink === "string") {
|
||||||
|
slugs.push(permalink as FullSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let slug of slugs) {
|
||||||
|
// fix any slugs that have trailing slash
|
||||||
|
if (slug.endsWith("/")) {
|
||||||
|
slug = joinSegments(slug, "index") as FullSlug
|
||||||
|
}
|
||||||
|
|
||||||
const redirUrl = resolveRelative(slug, file.data.slug!)
|
const redirUrl = resolveRelative(slug, file.data.slug!)
|
||||||
const fp = await emit({
|
const fp = await emit({
|
||||||
content: `
|
content: `
|
||||||
|
|||||||
29
quartz/plugins/emitters/cname.ts
Normal file
29
quartz/plugins/emitters/cname.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { FilePath, joinSegments } from "../../util/path"
|
||||||
|
import { QuartzEmitterPlugin } from "../types"
|
||||||
|
import fs from "fs"
|
||||||
|
import chalk from "chalk"
|
||||||
|
|
||||||
|
export function extractDomainFromBaseUrl(baseUrl: string) {
|
||||||
|
const url = new URL(`https://${baseUrl}`)
|
||||||
|
return url.hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CNAME: QuartzEmitterPlugin = () => ({
|
||||||
|
name: "CNAME",
|
||||||
|
getQuartzComponents() {
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
async emit({ argv, cfg }, _content, _resources, _emit): Promise<FilePath[]> {
|
||||||
|
if (!cfg.configuration.baseUrl) {
|
||||||
|
console.warn(chalk.yellow("CNAME emitter requires `baseUrl` to be set in your configuration"))
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const path = joinSegments(argv.output, "CNAME")
|
||||||
|
const content = extractDomainFromBaseUrl(cfg.configuration.baseUrl)
|
||||||
|
if (!content) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
fs.writeFileSync(path, content)
|
||||||
|
return [path] as FilePath[]
|
||||||
|
},
|
||||||
|
})
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user