Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e758cbe1ee | ||
|
|
4b6c7aeffe | ||
|
|
e277ed5c30 | ||
|
|
68f53352e7 | ||
|
|
359484c139 | ||
|
|
dafc9f318e | ||
|
|
e1b6a0014c | ||
|
|
233d4b2f2c | ||
|
|
504b447162 | ||
|
|
63bf1e14b5 | ||
|
|
be76da9e95 | ||
|
|
8fe37cc5e5 | ||
|
|
2e9896c893 | ||
|
|
7bcf27241f | ||
|
|
b44a79eeba | ||
|
|
9b9d86474b | ||
|
|
4c83251f8e | ||
|
|
984ab1c578 | ||
|
|
443cd53a1a | ||
|
|
5152d32fbd | ||
|
|
ea6208c1f0 | ||
|
|
78b33fc2fb | ||
|
|
d2be097b76 | ||
|
|
ad1f964a5f | ||
|
|
150050f379 | ||
|
|
d979331dc7 | ||
|
|
972cf0a887 | ||
|
|
14e6b13ff1 | ||
|
|
3c01b92cc4 | ||
|
|
ed9bd43d9f | ||
|
|
c35818c336 | ||
|
|
a464ae5029 | ||
|
|
66e297c0ea | ||
|
|
4442847b37 | ||
|
|
e6b5ca33c9 | ||
|
|
1b92440009 | ||
|
|
c6546903f2 | ||
|
|
2c69b0c97d | ||
|
|
a7e20804f5 | ||
|
|
5196f3b9db | ||
|
|
f0ec6c9b92 | ||
|
|
9c88d5967f |
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -20,12 +20,19 @@ Steps to reproduce the behavior:
|
|||||||
**Expected behavior**
|
**Expected behavior**
|
||||||
A clear and concise description of what you expected to happen.
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots and Source**
|
||||||
If applicable, add screenshots to help explain your problem.
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
You can help speed up fixing the problem by either
|
||||||
|
|
||||||
|
1. providing a simple reproduction
|
||||||
|
2. linking to your Quartz repository where the problem can be observed
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
**Desktop (please complete the following information):**
|
||||||
|
|
||||||
- Device: [e.g. iPhone6]
|
- Quartz Version: [e.g. v4.1.2]
|
||||||
|
- `node` Version: [e.g. v18.16]
|
||||||
|
- `npm` version: [e.g. v10.1.0]
|
||||||
- OS: [e.g. iOS]
|
- OS: [e.g. iOS]
|
||||||
- Browser [e.g. chrome, safari]
|
- Browser [e.g. chrome, safari]
|
||||||
|
|
||||||
|
|||||||
@@ -179,6 +179,34 @@ Component.Explorer({
|
|||||||
|
|
||||||
## Advanced examples
|
## Advanced examples
|
||||||
|
|
||||||
|
> [!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,
|
||||||
|
> })
|
||||||
|
> ```
|
||||||
|
|
||||||
### Add emoji prefix
|
### Add emoji prefix
|
||||||
|
|
||||||
To add emoji prefixes (📁 for folders, 📄 for files), you could use a map function like this:
|
To add emoji prefixes (📁 for folders, 📄 for files), you could use a map function like this:
|
||||||
@@ -216,30 +244,63 @@ Notice how we customized the `order` array here. This is done because the defaul
|
|||||||
|
|
||||||
To fix this, we just changed around the order and apply the `sort` function before changing the display names in the `map` function.
|
To fix this, we just changed around the order and apply the `sort` function before changing the display names in the `map` function.
|
||||||
|
|
||||||
> [!tip]
|
### Use `sort` with pre-defined sort order
|
||||||
> 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.
|
Here's another example where a map containing file/folder names (as slugs) is used to define the sort order of the explorer in quartz. All files/folders that aren't listed inside of `nameOrderMap` will appear at the top of that folders hierarchy level.
|
||||||
>
|
|
||||||
> ```ts title="functions.ts"
|
It's also worth mentioning, that the smaller the number set in `nameOrderMap`, the higher up the entry will be in the explorer. Incrementing every folder/file by 100, makes ordering files in their folders a lot easier. Lastly, this example still allows you to use a `mapFn` or frontmatter titles to change display names, as it uses slugs for `nameOrderMap` (which is unaffected by display name changes).
|
||||||
> import { Options } from "./quartz/components/ExplorerNode"
|
|
||||||
> export const mapFn: Options["mapFn"] = (node) => {
|
```ts title="quartz.layout.ts"
|
||||||
> // implement your function here
|
Component.Explorer({
|
||||||
> }
|
sortFn: (a, b) => {
|
||||||
> export const filterFn: Options["filterFn"] = (node) => {
|
const nameOrderMap: Record<string, number> = {
|
||||||
> // implement your function here
|
"poetry-folder": 100,
|
||||||
> }
|
"essay-folder": 200,
|
||||||
> export const sortFn: Options["sortFn"] = (a, b) => {
|
"research-paper-file": 201,
|
||||||
> // implement your function here
|
"dinosaur-fossils-file": 300,
|
||||||
> }
|
"other-folder": 400,
|
||||||
> ```
|
}
|
||||||
>
|
|
||||||
> You can then import them like this:
|
let orderA = 0
|
||||||
>
|
let orderB = 0
|
||||||
> ```ts title="quartz.layout.ts"
|
|
||||||
> import { mapFn, filterFn, sortFn } from "./functions.ts"
|
if (a.file && a.file.slug) {
|
||||||
> Component.Explorer({
|
orderA = nameOrderMap[a.file.slug] || 0
|
||||||
> mapFn: mapFn,
|
} else if (a.name) {
|
||||||
> filterFn: filterFn,
|
orderA = nameOrderMap[a.name] || 0
|
||||||
> sortFn: sortFn,
|
}
|
||||||
> })
|
|
||||||
> ```
|
if (b.file && b.file.slug) {
|
||||||
|
orderB = nameOrderMap[b.file.slug] || 0
|
||||||
|
} else if (b.name) {
|
||||||
|
orderB = nameOrderMap[b.name] || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return orderA - orderB
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
For reference, this is how the quartz explorer window would look like with that example:
|
||||||
|
|
||||||
|
```
|
||||||
|
📖 Poetry Folder
|
||||||
|
📑 Essay Folder
|
||||||
|
⚗️ Research Paper File
|
||||||
|
🦴 Dinosaur Fossils File
|
||||||
|
🔮 Other Folder
|
||||||
|
```
|
||||||
|
|
||||||
|
And this is how the file structure would look like:
|
||||||
|
|
||||||
|
```
|
||||||
|
index.md
|
||||||
|
poetry-folder
|
||||||
|
index.md
|
||||||
|
essay-folder
|
||||||
|
index.md
|
||||||
|
research-paper-file.md
|
||||||
|
dinosaur-fossils-file.md
|
||||||
|
other-folder
|
||||||
|
index.md
|
||||||
|
```
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ Component.Graph({
|
|||||||
linkDistance: 30, // how long should the links be by default?
|
linkDistance: 30, // how long should the links be by default?
|
||||||
fontSize: 0.6, // what size should the node labels be?
|
fontSize: 0.6, // what size should the node labels be?
|
||||||
opacityScale: 1, // how quickly do we fade out the labels when zooming out?
|
opacityScale: 1, // how quickly do we fade out the labels when zooming out?
|
||||||
|
removeTags: [], // what tags to remove from the graph
|
||||||
|
showTags: true, // whether to show tags in the graph
|
||||||
},
|
},
|
||||||
globalGraph: {
|
globalGraph: {
|
||||||
drag: true,
|
drag: true,
|
||||||
@@ -45,6 +47,8 @@ Component.Graph({
|
|||||||
linkDistance: 30,
|
linkDistance: 30,
|
||||||
fontSize: 0.6,
|
fontSize: 0.6,
|
||||||
opacityScale: 1,
|
opacityScale: 1,
|
||||||
|
removeTags: [], // what tags to remove from the graph
|
||||||
|
showTags: true, // whether to show tags in the graph
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -14,3 +14,11 @@ This is enabled as a part of [[Obsidian compatibility]] and can be configured an
|
|||||||
- `[[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 | 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.md`
|
- `[[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`
|
- `[[Path to file#^block-ref]]`: produces a link to the specific block `block-ref` in the file `Path to file.md`
|
||||||
|
|
||||||
|
### Embeds
|
||||||
|
|
||||||
|
- `![[Path to image]]`: embeds an image into the page
|
||||||
|
- `![[Path to image|100x145]]`: embeds an image into the page with dimensions 100px by 145px
|
||||||
|
- `![[Path to file]]`: transclude an entire page
|
||||||
|
- `![[Path to file#Anchor]]`: transclude everything under the header `Anchor`
|
||||||
|
- `![[Path to file#^b15695]]`: transclude block with ID `^b15695`
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ title: Hosting
|
|||||||
|
|
||||||
Quartz effectively turns your Markdown files and other resources into a bundle of HTML, JS, and CSS files (a website!).
|
Quartz effectively turns your Markdown files and other resources into a bundle of HTML, JS, and CSS files (a website!).
|
||||||
|
|
||||||
However, if you'd like to publish your site to the world, you need a way to host it online. This guide will detail how to deploy with either GitHub Pages or Cloudflare pages but any service that allows you to deploy static HTML should work as well (e.g. Netlify, Replit, etc.)
|
However, if you'd like to publish your site to the world, you need a way to host it online. This guide will detail how to deploy with common hosting providers but any service that allows you to deploy static HTML should work as well.
|
||||||
|
|
||||||
|
> [!warning]
|
||||||
|
> The rest of this guide assumes that you've already created your own GitHub repository for Quartz. If you haven't already, [[setting up your GitHub repository|make sure you do so]].
|
||||||
|
|
||||||
> [!hint]
|
> [!hint]
|
||||||
> Some Quartz features (like [[RSS Feed]] and sitemap generation) require `baseUrl` to be configured properly in your [[configuration]] to work properly. Make sure you set this before deploying!
|
> Some Quartz features (like [[RSS Feed]] and sitemap generation) require `baseUrl` to be configured properly in your [[configuration]] to work properly. Make sure you set this before deploying!
|
||||||
@@ -26,12 +29,10 @@ Press "Save and deploy" and Cloudflare should have a deployed version of your si
|
|||||||
|
|
||||||
To add a custom domain, check out [Cloudflare's documentation](https://developers.cloudflare.com/pages/platform/custom-domains/).
|
To add a custom domain, check out [Cloudflare's documentation](https://developers.cloudflare.com/pages/platform/custom-domains/).
|
||||||
|
|
||||||
## GitHub Pages
|
|
||||||
|
|
||||||
Like Quartz 3, you can deploy the site generated by Quartz 4 via GitHub Pages.
|
|
||||||
|
|
||||||
> [!warning]
|
> [!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]].
|
> Cloudflare Pages only allows shallow `git` clones so if you rely on `git` for timestamps, it is recommended you either add dates to your frontmatter (see [[authoring content#Syntax]]) or use another hosting provider.
|
||||||
|
|
||||||
|
## GitHub 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`.
|
||||||
|
|
||||||
@@ -93,6 +94,9 @@ Then:
|
|||||||
>
|
>
|
||||||
> You can do this by going to your Settings page on your GitHub fork and going to the Environments tab and pressing the trash icon. The GitHub action will recreate the environment for you correctly the next time you sync your Quartz.
|
> You can do this by going to your Settings page on your GitHub fork and going to the Environments tab and pressing the trash icon. The GitHub action will recreate the environment for you correctly the next time you sync your Quartz.
|
||||||
|
|
||||||
|
> [!info]
|
||||||
|
> 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 (e.g. you are migrating from Quartz 3), consider using [[#Cloudflare Pages]].
|
||||||
|
|
||||||
### Custom Domain
|
### Custom Domain
|
||||||
|
|
||||||
Here's how to add a custom domain to your GitHub pages deployment.
|
Here's how to add a custom domain to your GitHub pages deployment.
|
||||||
@@ -169,8 +173,6 @@ Using `docs.example.com` is an example of a subdomain. They're a simple way of c
|
|||||||
|
|
||||||
## Netlify
|
## 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".
|
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.
|
2. Select your Git provider and repository containing your Quartz project.
|
||||||
3. Under "Build command", enter `npx quartz build`.
|
3. Under "Build command", enter `npx quartz build`.
|
||||||
@@ -180,8 +182,6 @@ Like Vercel, you can also deploy the site generated by Quartz 4 via Netlify.
|
|||||||
|
|
||||||
## GitLab Pages
|
## 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`.
|
In your local Quartz, create a new file `.gitlab-ci.yaml`.
|
||||||
|
|
||||||
```yaml title=".gitlab-ci.yaml"
|
```yaml title=".gitlab-ci.yaml"
|
||||||
@@ -203,8 +203,6 @@ build:
|
|||||||
- hash -r
|
- hash -r
|
||||||
- npm ci
|
- npm ci
|
||||||
script:
|
script:
|
||||||
- npx prettier --write .
|
|
||||||
- npm run check
|
|
||||||
- npx quartz build
|
- npx quartz build
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
@@ -227,6 +225,6 @@ pages:
|
|||||||
- public
|
- 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.
|
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`.
|
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`.
|
||||||
|
|||||||
BIN
docs/images/github-init-repo-options.png
Normal file
BIN
docs/images/github-init-repo-options.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
BIN
docs/images/github-quick-setup.png
Normal file
BIN
docs/images/github-quick-setup.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 150 KiB |
@@ -2,7 +2,7 @@
|
|||||||
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, websites, and [digital gardens](https://jzhao.xyz/posts/networked-thought) to the web.
|
||||||
|
|
||||||
## 🪴 Get Started
|
## 🪴 Get Started
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ npx quartz create
|
|||||||
|
|
||||||
This will guide you through initializing your Quartz with content. Once you've done so, see how to:
|
This will guide you through initializing your Quartz with content. Once you've done so, see how to:
|
||||||
|
|
||||||
1. [[authoring content|Author content]] in Quartz
|
1. [[authoring content|Writing content]] in Quartz
|
||||||
2. [[configuration|Configure]] Quartz's behaviour
|
2. [[configuration|Configure]] Quartz's behaviour
|
||||||
3. Change Quartz's [[layout]]
|
3. Change Quartz's [[layout]]
|
||||||
4. [[build|Build and preview]] Quartz
|
4. [[build|Build and preview]] Quartz
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ title: Philosophy of Quartz
|
|||||||
>
|
>
|
||||||
> _(The Garden and the Stream)_
|
> _(The Garden and the Stream)_
|
||||||
|
|
||||||
The problem with the file cabinet is that it focuses on efficiency of access and interoperability rather than generativity and creativity. Thinking is not linear, nor is it hierarchical. In fact, not many things are linear or hierarchical at all. Then why is it that most tools and thinking strategies assume a nice chronological or hierarchical order for my thought processes? The ideal tool for thought for me would embrace the messiness of my mind, and organically help insights emerge from chaos instead of forcing an artificial order. A rhizomatic, not arboresecent, form of note taking.
|
The problem with the file cabinet is that it focuses on efficiency of access and interoperability rather than generativity and creativity. Thinking is not linear, nor is it hierarchical. In fact, not many things are linear or hierarchical at all. Then why is it that most tools and thinking strategies assume a nice chronological or hierarchical order for my thought processes?
|
||||||
|
|
||||||
|
The ideal tool for thought for me would embrace the messiness of my mind, and organically help insights emerge from chaos instead of forcing an artificial order. A rhizomatic, not arboresecent, form of note taking.
|
||||||
|
|
||||||
My goal with a digital garden is not purely as an organizing system and information store (though it works nicely for that). I want my digital garden to be a playground for new ways ideas can connect together. As a result, existing formal organizing systems like Zettelkasten or the hierarchical folder structures of Notion don’t work well for me. There is way too much upfront friction that by the time I’ve thought about how to organize my thought into folders categories, I’ve lost it.
|
My goal with a digital garden is not purely as an organizing system and information store (though it works nicely for that). I want my digital garden to be a playground for new ways ideas can connect together. As a result, existing formal organizing systems like Zettelkasten or the hierarchical folder structures of Notion don’t work well for me. There is way too much upfront friction that by the time I’ve thought about how to organize my thought into folders categories, I’ve lost it.
|
||||||
|
|
||||||
@@ -25,4 +27,21 @@ Quartz is designed first and foremost as a tool for publishing [digital gardens]
|
|||||||
> “[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
|
||||||
|
|
||||||
**The goal of Quartz is to make sharing your digital garden free and simple.** At its core, Quartz is designed to be easy to use enough for non-technical people to get going but also powerful enough that senior developers can tweak it to work how they'd like it to work.
|
**The goal of Quartz is to make sharing your digital garden free and simple.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## A garden should be your own
|
||||||
|
|
||||||
|
At its core, Quartz is designed to be easy to use enough for non-technical people to get going but also powerful enough that senior developers can tweak it to work how they'd like it to work.
|
||||||
|
|
||||||
|
1. If you like the default configuration of Quartz and just want to change the content, the only thing that you need to change is the contents of the `content` folder.
|
||||||
|
2. If you'd like to make basic configuration tweaks but don't want to edit source code, one can tweak the plugins and components in `quartz.config.ts` and `quartz.layout.ts` in a guided manner to their liking.
|
||||||
|
3. If you'd like to tweak the actual source code of the underlying plugins, components, or even build process, Quartz purposefully ships its full source code to the end user to allow customization at this level too.
|
||||||
|
|
||||||
|
Most software either confines you to either
|
||||||
|
|
||||||
|
1. Makes it easy to tweak content but not the presentation
|
||||||
|
2. Gives you too many knobs to tune the presentation without good opinionated defaults
|
||||||
|
|
||||||
|
**Quartz should feel powerful but ultimately be an intuitive tool fully within your control.** It should be a piece of [agentic software](https://jzhao.xyz/posts/agentic-computing). Ultimately, it should have the right affordances to nudge users towards good defaults but never dictate what the 'correct' way of using it is.
|
||||||
|
|||||||
39
docs/setting up your GitHub repository.md
Normal file
39
docs/setting up your GitHub repository.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
title: Setting up your GitHub repository
|
||||||
|
---
|
||||||
|
|
||||||
|
First, make sure you have Quartz [[index#🪴 Get Started|cloned and setup locally]].
|
||||||
|
|
||||||
|
Then, create a new repository on GitHub.com. Do **not** initialize the new repository with `README`, license, or `gitignore` files.
|
||||||
|
|
||||||
|
![[github-init-repo-options.png]]
|
||||||
|
|
||||||
|
At the top of your repository on GitHub.com's Quick Setup page, click the clipboard to copy the remote repository URL.
|
||||||
|
|
||||||
|
![[github-quick-setup.png]]
|
||||||
|
|
||||||
|
In your terminal of choice, navigate to the root of your Quartz folder. Then, run the following commands, replacing `REMOTE-URL` with the URL you just copied from the previous step.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# add your repository
|
||||||
|
git remote add origin REMOTE-URL
|
||||||
|
|
||||||
|
# track the main quartz repository for updates
|
||||||
|
git remote add upstream https://github.com/jackyzha0/quartz.git
|
||||||
|
```
|
||||||
|
|
||||||
|
To verify that you set the remote URL correctly, run the following command.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git remote -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, you can sync the content to upload it to your repository.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx quartz sync --no-pull
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!hint]
|
||||||
|
> If `npx quartz sync` fails with `fatal: --[no-]autostash option is only valid with --rebase`, you
|
||||||
|
> may have an outdated version of `git`. Updating `git` should fix this issue.
|
||||||
@@ -6,9 +6,9 @@ Want to see what Quartz can do? Here are some cool community gardens:
|
|||||||
|
|
||||||
- [Quartz Documentation (this site!)](https://quartz.jzhao.xyz/)
|
- [Quartz Documentation (this site!)](https://quartz.jzhao.xyz/)
|
||||||
- [Jacky Zhao's Garden](https://jzhao.xyz/)
|
- [Jacky Zhao's Garden](https://jzhao.xyz/)
|
||||||
|
- [Socratica Toolbox](https://toolbox.socratica.info/)
|
||||||
- [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/)
|
|
||||||
- [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/)
|
||||||
@@ -21,5 +21,6 @@ Want to see what Quartz can do? Here are some cool community gardens:
|
|||||||
- [Pedro MC Fernandes's Topo da Mente](https://www.pmcf.xyz/topo-da-mente/)
|
- [Pedro MC Fernandes's Topo da Mente](https://www.pmcf.xyz/topo-da-mente/)
|
||||||
- [Mau Camargo's Notkesto](https://notes.camargomau.com/)
|
- [Mau Camargo's Notkesto](https://notes.camargomau.com/)
|
||||||
- [Caicai's Novels](https://imoko.cc/blog/caicai/)
|
- [Caicai's Novels](https://imoko.cc/blog/caicai/)
|
||||||
|
- [🌊 Collapsed Wave](https://collapsedwave.com/)
|
||||||
|
|
||||||
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)!
|
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)!
|
||||||
|
|||||||
3711
package-lock.json
generated
3711
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
101
package.json
101
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.1.2",
|
"version": "4.1.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -34,76 +34,77 @@
|
|||||||
"quartz": "./quartz/bootstrap-cli.mjs"
|
"quartz": "./quartz/bootstrap-cli.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.6.3",
|
"@clack/prompts": "^0.7.0",
|
||||||
"@floating-ui/dom": "^1.4.0",
|
"@floating-ui/dom": "^1.5.3",
|
||||||
"@napi-rs/simple-git": "0.1.9",
|
"@napi-rs/simple-git": "0.1.9",
|
||||||
"async-mutex": "^0.4.0",
|
"async-mutex": "^0.4.0",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^5.3.0",
|
||||||
"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.12.0",
|
"esbuild-sass-plugin": "^2.16.0",
|
||||||
"flexsearch": "0.7.21",
|
"flexsearch": "0.7.21",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"globby": "^13.1.4",
|
"globby": "^14.0.0",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"hast-util-to-html": "^8.0.4",
|
"hast-util-to-html": "^9.0.0",
|
||||||
"hast-util-to-jsx-runtime": "^1.2.0",
|
"hast-util-to-jsx-runtime": "^2.3.0",
|
||||||
"hast-util-to-string": "^2.0.0",
|
"hast-util-to-string": "^3.0.0",
|
||||||
"is-absolute-url": "^4.0.1",
|
"is-absolute-url": "^4.0.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lightningcss": "1.21.7",
|
"lightningcss": "^1.22.1",
|
||||||
"mdast-util-find-and-replace": "^2.2.2",
|
"mdast-util-find-and-replace": "^3.0.1",
|
||||||
"mdast-util-to-hast": "^12.3.0",
|
"mdast-util-to-hast": "^13.0.2",
|
||||||
"mdast-util-to-string": "^3.2.0",
|
"mdast-util-to-string": "^4.0.0",
|
||||||
"micromorph": "^0.4.5",
|
"micromorph": "^0.4.5",
|
||||||
"plausible-tracker": "^0.3.8",
|
"preact": "^10.19.3",
|
||||||
"preact": "^10.14.1",
|
"preact-render-to-string": "^6.3.1",
|
||||||
"preact-render-to-string": "^6.0.3",
|
"pretty-bytes": "^6.1.1",
|
||||||
"pretty-bytes": "^6.1.0",
|
|
||||||
"pretty-time": "^1.1.0",
|
"pretty-time": "^1.1.0",
|
||||||
"reading-time": "^1.5.0",
|
"reading-time": "^1.5.0",
|
||||||
"rehype-autolink-headings": "^6.1.1",
|
"rehype-autolink-headings": "^7.1.0",
|
||||||
"rehype-katex": "^6.0.3",
|
"rehype-katex": "^7.0.0",
|
||||||
"rehype-mathjax": "^4.0.3",
|
"rehype-mathjax": "^5.0.0",
|
||||||
"rehype-pretty-code": "^0.10.0",
|
"rehype-pretty-code": "^0.12.3",
|
||||||
"rehype-raw": "^6.1.1",
|
"rehype-raw": "^7.0.0",
|
||||||
"rehype-slug": "^5.1.0",
|
"rehype-slug": "^6.0.0",
|
||||||
"remark": "^14.0.2",
|
"remark": "^15.0.1",
|
||||||
"remark-breaks": "^3.0.3",
|
"remark-breaks": "^4.0.0",
|
||||||
"remark-frontmatter": "^4.0.1",
|
"remark-frontmatter": "^5.0.0",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^4.0.0",
|
||||||
"remark-math": "^5.1.1",
|
"remark-math": "^6.0.0",
|
||||||
"remark-parse": "^10.0.1",
|
"remark-parse": "^11.0.0",
|
||||||
"remark-rehype": "^10.1.0",
|
"remark-rehype": "^11.0.0",
|
||||||
"remark-smartypants": "^2.0.0",
|
"remark-smartypants": "^2.0.0",
|
||||||
"rimraf": "^5.0.1",
|
"rfdc": "^1.3.0",
|
||||||
|
"rimraf": "^5.0.5",
|
||||||
"serve-handler": "^6.1.5",
|
"serve-handler": "^6.1.5",
|
||||||
|
"shikiji": "^0.9.9",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"to-vfile": "^7.2.4",
|
"to-vfile": "^8.0.0",
|
||||||
"toml": "^3.0.0",
|
"toml": "^3.0.0",
|
||||||
"unified": "^10.1.2",
|
"unified": "^11.0.4",
|
||||||
"unist-util-visit": "^4.1.2",
|
"unist-util-visit": "^5.0.0",
|
||||||
"vfile": "^5.3.7",
|
"vfile": "^6.0.1",
|
||||||
"workerpool": "^6.4.0",
|
"workerpool": "^8.0.0",
|
||||||
"ws": "^8.13.0",
|
"ws": "^8.15.1",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cli-spinner": "^0.2.1",
|
"@types/cli-spinner": "^0.2.3",
|
||||||
"@types/d3": "^7.4.0",
|
"@types/d3": "^7.4.3",
|
||||||
"@types/flexsearch": "^0.7.3",
|
"@types/flexsearch": "^0.7.3",
|
||||||
"@types/hast": "^2.3.4",
|
"@types/hast": "^3.0.3",
|
||||||
"@types/js-yaml": "^4.0.5",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/node": "^20.1.2",
|
"@types/node": "^20.1.2",
|
||||||
"@types/pretty-time": "^1.1.2",
|
"@types/pretty-time": "^1.1.5",
|
||||||
"@types/source-map-support": "^0.5.6",
|
"@types/source-map-support": "^0.5.10",
|
||||||
"@types/workerpool": "^6.4.0",
|
"@types/workerpool": "^6.4.7",
|
||||||
"@types/ws": "^8.5.5",
|
"@types/ws": "^8.5.10",
|
||||||
"@types/yargs": "^17.0.24",
|
"@types/yargs": "^17.0.32",
|
||||||
"esbuild": "0.19.2",
|
"esbuild": "^0.19.9",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.1.1",
|
||||||
"tsx": "^3.12.7",
|
"tsx": "^4.6.2",
|
||||||
"typescript": "^5.0.4"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,11 +49,11 @@ const config: QuartzConfig = {
|
|||||||
Plugin.CreatedModifiedDate({
|
Plugin.CreatedModifiedDate({
|
||||||
priority: ["frontmatter", "filesystem"], // you can add 'git' here for last modified from Git but this makes the build slower
|
priority: ["frontmatter", "filesystem"], // you can add 'git' here for last modified from Git but this makes the build slower
|
||||||
}),
|
}),
|
||||||
|
Plugin.Latex({ renderEngine: "katex" }),
|
||||||
Plugin.SyntaxHighlighting(),
|
Plugin.SyntaxHighlighting(),
|
||||||
Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }),
|
Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }),
|
||||||
Plugin.GitHubFlavoredMarkdown(),
|
Plugin.GitHubFlavoredMarkdown(),
|
||||||
Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
|
Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
|
||||||
Plugin.Latex({ renderEngine: "katex" }),
|
|
||||||
Plugin.Description(),
|
Plugin.Description(),
|
||||||
],
|
],
|
||||||
filters: [Plugin.RemoveDrafts()],
|
filters: [Plugin.RemoveDrafts()],
|
||||||
|
|||||||
@@ -148,14 +148,17 @@ async function startServing(
|
|||||||
await rimraf(argv.output)
|
await rimraf(argv.output)
|
||||||
await emitContent(ctx, filteredContent)
|
await emitContent(ctx, filteredContent)
|
||||||
console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`))
|
console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`))
|
||||||
} catch {
|
} catch (err) {
|
||||||
console.log(chalk.yellow(`Rebuild failed. Waiting on a change to fix the error...`))
|
console.log(chalk.yellow(`Rebuild failed. Waiting on a change to fix the error...`))
|
||||||
|
if (argv.verbose) {
|
||||||
|
console.log(chalk.red(err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
release()
|
||||||
clientRefresh()
|
clientRefresh()
|
||||||
toRebuild.clear()
|
toRebuild.clear()
|
||||||
toRemove.clear()
|
toRemove.clear()
|
||||||
release()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const watcher = chokidar.watch(".", {
|
const watcher = chokidar.watch(".", {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export type Analytics =
|
|||||||
| null
|
| null
|
||||||
| {
|
| {
|
||||||
provider: "plausible"
|
provider: "plausible"
|
||||||
|
host?: string
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
provider: "google"
|
provider: "google"
|
||||||
|
|||||||
@@ -196,6 +196,11 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
|
|||||||
)
|
)
|
||||||
await fs.promises.writeFile(configFilePath, configContent)
|
await fs.promises.writeFile(configFilePath, configContent)
|
||||||
|
|
||||||
|
// setup remote
|
||||||
|
execSync(
|
||||||
|
`git remote show upstream || git remote add upstream https://github.com/jackyzha0/quartz.git`,
|
||||||
|
)
|
||||||
|
|
||||||
outro(`You're all set! Not sure what to do next? Try:
|
outro(`You're all set! Not sure what to do next? Try:
|
||||||
• Customizing Quartz a bit more by editing \`quartz.config.ts\`
|
• Customizing Quartz a bit more by editing \`quartz.config.ts\`
|
||||||
• Running \`npx quartz build --serve\` to preview your Quartz locally
|
• Running \`npx quartz build --serve\` to preview your Quartz locally
|
||||||
@@ -438,11 +443,23 @@ export async function handleUpdate(argv) {
|
|||||||
console.log(
|
console.log(
|
||||||
"Pulling updates... you may need to resolve some `git` conflicts if you've made changes to components or plugins.",
|
"Pulling updates... you may need to resolve some `git` conflicts if you've made changes to components or plugins.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
gitPull(UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH)
|
gitPull(UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH)
|
||||||
|
} catch {
|
||||||
|
console.log(chalk.red("An error occured above while pulling updates."))
|
||||||
|
await popContentFolder(contentFolder)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await popContentFolder(contentFolder)
|
await popContentFolder(contentFolder)
|
||||||
console.log("Ensuring dependencies are up to date")
|
console.log("Ensuring dependencies are up to date")
|
||||||
spawnSync("npm", ["i"], { stdio: "inherit" })
|
const res = spawnSync("npm", ["i"], { stdio: "inherit" })
|
||||||
|
if (res.status === 0) {
|
||||||
console.log(chalk.green("Done!"))
|
console.log(chalk.green("Done!"))
|
||||||
|
} else {
|
||||||
|
console.log(chalk.red("An error occurred above while installing dependencies."))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -499,13 +516,25 @@ export async function handleSync(argv) {
|
|||||||
console.log(
|
console.log(
|
||||||
"Pulling updates from your repository. You may need to resolve some `git` conflicts if you've made changes to components or plugins.",
|
"Pulling updates from your repository. You may need to resolve some `git` conflicts if you've made changes to components or plugins.",
|
||||||
)
|
)
|
||||||
|
try {
|
||||||
gitPull(ORIGIN_NAME, QUARTZ_SOURCE_BRANCH)
|
gitPull(ORIGIN_NAME, QUARTZ_SOURCE_BRANCH)
|
||||||
|
} catch {
|
||||||
|
console.log(chalk.red("An error occured above while pulling updates."))
|
||||||
|
await popContentFolder(contentFolder)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await popContentFolder(contentFolder)
|
await popContentFolder(contentFolder)
|
||||||
if (argv.push) {
|
if (argv.push) {
|
||||||
console.log("Pushing your changes")
|
console.log("Pushing your changes")
|
||||||
spawnSync("git", ["push", "-f", ORIGIN_NAME, QUARTZ_SOURCE_BRANCH], { stdio: "inherit" })
|
const res = spawnSync("git", ["push", "-uf", ORIGIN_NAME, QUARTZ_SOURCE_BRANCH], {
|
||||||
|
stdio: "inherit",
|
||||||
|
})
|
||||||
|
if (res.status !== 0) {
|
||||||
|
console.log(chalk.red(`An error occurred above while pushing to remote ${ORIGIN_NAME}.`))
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(chalk.green("Done!"))
|
console.log(chalk.green("Done!"))
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export function gitPull(origin, branch) {
|
|||||||
const flags = ["--no-rebase", "--autostash", "-s", "recursive", "-X", "ours", "--no-edit"]
|
const flags = ["--no-rebase", "--autostash", "-s", "recursive", "-X", "ours", "--no-edit"]
|
||||||
const out = spawnSync("git", ["pull", ...flags, origin, branch], { stdio: "inherit" })
|
const out = spawnSync("git", ["pull", ...flags, origin, branch], { stdio: "inherit" })
|
||||||
if (out.stderr) {
|
if (out.stderr) {
|
||||||
throw new Error(`Error while pulling updates: ${out.stderr}`)
|
throw new Error(chalk.red(`Error while pulling updates: ${out.stderr}`))
|
||||||
|
} else if (out.status !== 0) {
|
||||||
|
throw new Error(chalk.red("Error while pulling updates"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,8 +68,9 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
|||||||
// construct the index for the first time
|
// construct the index for the first time
|
||||||
for (const file of allFiles) {
|
for (const file of allFiles) {
|
||||||
if (file.slug?.endsWith("index")) {
|
if (file.slug?.endsWith("index")) {
|
||||||
const folderParts = file.filePath?.split("/")
|
const folderParts = file.slug?.split("/")
|
||||||
if (folderParts) {
|
if (folderParts) {
|
||||||
|
// 2nd last to exclude the /index
|
||||||
const folderName = folderParts[folderParts?.length - 2]
|
const folderName = folderParts[folderParts?.length - 2]
|
||||||
folderIndex.set(folderName, file)
|
folderIndex.set(folderName, file)
|
||||||
}
|
}
|
||||||
@@ -88,7 +89,10 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
|||||||
// Try to resolve frontmatter folder title
|
// Try to resolve frontmatter folder title
|
||||||
const currentFile = folderIndex?.get(curPathSegment)
|
const currentFile = folderIndex?.get(curPathSegment)
|
||||||
if (currentFile) {
|
if (currentFile) {
|
||||||
curPathSegment = currentFile.frontmatter!.title
|
const title = currentFile.frontmatter!.title
|
||||||
|
if (title !== "index") {
|
||||||
|
curPathSegment = title
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add current slug to full path
|
// Add current slug to full path
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function Darkmode({ displayClass }: QuartzComponentProps) {
|
|||||||
x="0px"
|
x="0px"
|
||||||
y="0px"
|
y="0px"
|
||||||
viewBox="0 0 35 35"
|
viewBox="0 0 35 35"
|
||||||
style="enable-background:new 0 0 35 35;"
|
style="enable-background:new 0 0 35 35"
|
||||||
xmlSpace="preserve"
|
xmlSpace="preserve"
|
||||||
>
|
>
|
||||||
<title>Light mode</title>
|
<title>Light mode</title>
|
||||||
@@ -34,7 +34,7 @@ function Darkmode({ displayClass }: QuartzComponentProps) {
|
|||||||
x="0px"
|
x="0px"
|
||||||
y="0px"
|
y="0px"
|
||||||
viewBox="0 0 100 100"
|
viewBox="0 0 100 100"
|
||||||
style="enable-background='new 0 0 100 100'"
|
style="enable-background:new 0 0 100 100"
|
||||||
xmlSpace="preserve"
|
xmlSpace="preserve"
|
||||||
>
|
>
|
||||||
<title>Dark mode</title>
|
<title>Dark mode</title>
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ const defaultOptions = {
|
|||||||
folderClickBehavior: "collapse",
|
folderClickBehavior: "collapse",
|
||||||
folderDefaultState: "collapsed",
|
folderDefaultState: "collapsed",
|
||||||
useSavedState: true,
|
useSavedState: true,
|
||||||
|
mapFn: (node) => {
|
||||||
|
return node
|
||||||
|
},
|
||||||
sortFn: (a, b) => {
|
sortFn: (a, b) => {
|
||||||
// Sort order: folders first, then files. Sort folders and files alphabetically
|
// Sort order: folders first, then files. Sort folders and files alphabetically
|
||||||
if ((!a.file && !b.file) || (a.file && b.file)) {
|
if ((!a.file && !b.file) || (a.file && b.file)) {
|
||||||
@@ -22,6 +25,7 @@ const defaultOptions = {
|
|||||||
sensitivity: "base",
|
sensitivity: "base",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (a.file && !b.file) {
|
if (a.file && !b.file) {
|
||||||
return 1
|
return 1
|
||||||
} else {
|
} else {
|
||||||
@@ -41,36 +45,25 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
let jsonTree: string
|
let jsonTree: string
|
||||||
|
|
||||||
function constructFileTree(allFiles: QuartzPluginData[]) {
|
function constructFileTree(allFiles: QuartzPluginData[]) {
|
||||||
if (!fileTree) {
|
if (fileTree) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Construct tree from allFiles
|
// Construct tree from allFiles
|
||||||
fileTree = new FileNode("")
|
fileTree = new FileNode("")
|
||||||
allFiles.forEach((file) => fileTree.add(file, 1))
|
allFiles.forEach((file) => fileTree.add(file))
|
||||||
|
|
||||||
/**
|
|
||||||
* 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)
|
// Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied)
|
||||||
if (opts.order) {
|
if (opts.order) {
|
||||||
// Order is important, use loop with index instead of order.map()
|
// Order is important, use loop with index instead of order.map()
|
||||||
for (let i = 0; i < opts.order.length; i++) {
|
for (let i = 0; i < opts.order.length; i++) {
|
||||||
const functionName = opts.order[i]
|
const functionName = opts.order[i]
|
||||||
if (functions[functionName]) {
|
if (functionName === "map") {
|
||||||
// for every entry in order, call matching function in FileNode and pass matching argument
|
fileTree.map(opts.mapFn)
|
||||||
// e.g. i = 0; functionName = "filter"
|
} else if (functionName === "sort") {
|
||||||
// converted to: (if opts.filterFn) => fileTree.filter(opts.filterFn)
|
fileTree.sort(opts.sortFn)
|
||||||
|
} else if (functionName === "filter") {
|
||||||
// @ts-ignore
|
fileTree.filter(opts.filterFn)
|
||||||
// typescript cant statically check these dynamic references, so manually make sure reference is valid and ignore warning
|
|
||||||
fileTree[functionName].call(fileTree, functions[functionName])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,7 +74,6 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
// Stringify to pass json tree as data attribute ([data-tree])
|
// Stringify to pass json tree as data attribute ([data-tree])
|
||||||
jsonTree = JSON.stringify(folders)
|
jsonTree = JSON.stringify(folders)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) {
|
function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) {
|
||||||
constructFileTree(allFiles)
|
constructFileTree(allFiles)
|
||||||
@@ -120,6 +112,7 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Explorer.css = explorerStyle
|
Explorer.css = explorerStyle
|
||||||
Explorer.afterDOMLoaded = script
|
Explorer.afterDOMLoaded = script
|
||||||
return Explorer
|
return Explorer
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
import { resolveRelative } from "../util/path"
|
import {
|
||||||
|
joinSegments,
|
||||||
|
resolveRelative,
|
||||||
|
clone,
|
||||||
|
simplifySlug,
|
||||||
|
SimpleSlug,
|
||||||
|
FilePath,
|
||||||
|
} from "../util/path"
|
||||||
|
|
||||||
type OrderEntries = "sort" | "filter" | "map"
|
type OrderEntries = "sort" | "filter" | "map"
|
||||||
|
|
||||||
@@ -10,9 +17,9 @@ export interface Options {
|
|||||||
folderClickBehavior: "collapse" | "link"
|
folderClickBehavior: "collapse" | "link"
|
||||||
useSavedState: boolean
|
useSavedState: boolean
|
||||||
sortFn: (a: FileNode, b: FileNode) => number
|
sortFn: (a: FileNode, b: FileNode) => number
|
||||||
filterFn?: (node: FileNode) => boolean
|
filterFn: (node: FileNode) => boolean
|
||||||
mapFn?: (node: FileNode) => void
|
mapFn: (node: FileNode) => void
|
||||||
order?: OrderEntries[]
|
order: OrderEntries[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type DataWrapper = {
|
type DataWrapper = {
|
||||||
@@ -25,59 +32,74 @@ export type FolderState = {
|
|||||||
collapsed: boolean
|
collapsed: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPathSegment(fp: FilePath | undefined, idx: number): string | undefined {
|
||||||
|
if (!fp) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return fp.split("/").at(idx)
|
||||||
|
}
|
||||||
|
|
||||||
// Structure to add all files into a tree
|
// Structure to add all files into a tree
|
||||||
export class FileNode {
|
export class FileNode {
|
||||||
children: FileNode[]
|
children: Array<FileNode>
|
||||||
name: string
|
name: string // this is the slug segment
|
||||||
displayName: string
|
displayName: string
|
||||||
file: QuartzPluginData | null
|
file: QuartzPluginData | null
|
||||||
depth: number
|
depth: number
|
||||||
|
|
||||||
constructor(name: string, file?: QuartzPluginData, depth?: number) {
|
constructor(slugSegment: string, displayName?: string, file?: QuartzPluginData, depth?: number) {
|
||||||
this.children = []
|
this.children = []
|
||||||
this.name = name
|
this.name = slugSegment
|
||||||
this.displayName = name
|
this.displayName = displayName ?? file?.frontmatter?.title ?? slugSegment
|
||||||
this.file = file ? structuredClone(file) : null
|
this.file = file ? clone(file) : null
|
||||||
this.depth = depth ?? 0
|
this.depth = depth ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private insert(file: DataWrapper) {
|
private insert(fileData: DataWrapper) {
|
||||||
if (file.path.length === 1) {
|
if (fileData.path.length === 0) {
|
||||||
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nextSegment = fileData.path[0]
|
||||||
|
|
||||||
|
// base case, insert here
|
||||||
|
if (fileData.path.length === 1) {
|
||||||
|
if (nextSegment === "") {
|
||||||
|
// index case (we are the root and we just found index.md), set our data appropriately
|
||||||
|
const title = fileData.file.frontmatter?.title
|
||||||
|
if (title && title !== "index") {
|
||||||
|
this.displayName = title
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// direct child
|
||||||
|
this.children.push(new FileNode(nextSegment, undefined, fileData.file, this.depth + 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
const newChild = new FileNode(next, undefined, this.depth + 1)
|
return
|
||||||
newChild.insert(file)
|
|
||||||
this.children.push(newChild)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// find the right child to insert into
|
||||||
|
fileData.path = fileData.path.splice(1)
|
||||||
|
const child = this.children.find((c) => c.name === nextSegment)
|
||||||
|
if (child) {
|
||||||
|
child.insert(fileData)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newChild = new FileNode(
|
||||||
|
nextSegment,
|
||||||
|
getPathSegment(fileData.file.relativePath, this.depth),
|
||||||
|
undefined,
|
||||||
|
this.depth + 1,
|
||||||
|
)
|
||||||
|
newChild.insert(fileData)
|
||||||
|
this.children.push(newChild)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new file to tree
|
// Add new file to tree
|
||||||
add(file: QuartzPluginData, splice: number = 0) {
|
add(file: QuartzPluginData) {
|
||||||
this.insert({ file, path: file.filePath!.split("/").splice(splice) })
|
this.insert({ file: file, path: simplifySlug(file.slug!).split("/") })
|
||||||
}
|
|
||||||
|
|
||||||
// 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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,7 +117,6 @@ export class FileNode {
|
|||||||
*/
|
*/
|
||||||
map(mapFn: (node: FileNode) => void) {
|
map(mapFn: (node: FileNode) => void) {
|
||||||
mapFn(this)
|
mapFn(this)
|
||||||
|
|
||||||
this.children.forEach((child) => child.map(mapFn))
|
this.children.forEach((child) => child.map(mapFn))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,16 +131,16 @@ export class FileNode {
|
|||||||
|
|
||||||
const traverse = (node: FileNode, currentPath: string) => {
|
const traverse = (node: FileNode, currentPath: string) => {
|
||||||
if (!node.file) {
|
if (!node.file) {
|
||||||
const folderPath = currentPath + (currentPath ? "/" : "") + node.name
|
const folderPath = joinSegments(currentPath, node.name)
|
||||||
if (folderPath !== "") {
|
if (folderPath !== "") {
|
||||||
folderPaths.push({ path: folderPath, collapsed })
|
folderPaths.push({ path: folderPath, collapsed })
|
||||||
}
|
}
|
||||||
|
|
||||||
node.children.forEach((child) => traverse(child, folderPath))
|
node.children.forEach((child) => traverse(child, folderPath))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
traverse(this, "")
|
traverse(this, "")
|
||||||
|
|
||||||
return folderPaths
|
return folderPaths
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,14 +168,13 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
|
|||||||
const isDefaultOpen = opts.folderDefaultState === "open"
|
const isDefaultOpen = opts.folderDefaultState === "open"
|
||||||
|
|
||||||
// Calculate current folderPath
|
// Calculate current folderPath
|
||||||
let pathOld = fullPath ? fullPath : ""
|
|
||||||
let folderPath = ""
|
let folderPath = ""
|
||||||
if (node.name !== "") {
|
if (node.name !== "") {
|
||||||
folderPath = `${pathOld}/${node.name}`
|
folderPath = joinSegments(fullPath ?? "", node.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li>
|
<>
|
||||||
{node.file ? (
|
{node.file ? (
|
||||||
// Single file node
|
// Single file node
|
||||||
<li key={node.file.slug}>
|
<li key={node.file.slug}>
|
||||||
@@ -163,7 +183,7 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<li>
|
||||||
{node.name !== "" && (
|
{node.name !== "" && (
|
||||||
// Node with entire folder
|
// Node with entire folder
|
||||||
// Render svg button + folder name, then children
|
// Render svg button + folder name, then children
|
||||||
@@ -185,12 +205,16 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
|
|||||||
{/* render <a> tag if folderBehavior is "link", otherwise render <button> with collapse click event */}
|
{/* render <a> tag if folderBehavior is "link", otherwise render <button> with collapse click event */}
|
||||||
<div key={node.name} data-folderpath={folderPath}>
|
<div key={node.name} data-folderpath={folderPath}>
|
||||||
{folderBehavior === "link" ? (
|
{folderBehavior === "link" ? (
|
||||||
<a href={`${folderPath}`} data-for={node.name} class="folder-title">
|
<a
|
||||||
|
href={resolveRelative(fileData.slug!, folderPath as SimpleSlug)}
|
||||||
|
data-for={node.name}
|
||||||
|
class="folder-title"
|
||||||
|
>
|
||||||
{node.displayName}
|
{node.displayName}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<button class="folder-button">
|
<button class="folder-button">
|
||||||
<p class="folder-title">{node.displayName}</p>
|
<span class="folder-title">{node.displayName}</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -217,8 +241,8 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</li>
|
</li>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,12 @@ function TagContent(props: QuartzComponentProps) {
|
|||||||
? fileData.description
|
? fileData.description
|
||||||
: htmlToJsx(fileData.filePath!, tree)
|
: htmlToJsx(fileData.filePath!, tree)
|
||||||
|
|
||||||
if (tag === "") {
|
if (tag === "/") {
|
||||||
const tags = [...new Set(allFiles.flatMap((data) => data.frontmatter?.tags ?? []))]
|
const tags = [
|
||||||
|
...new Set(
|
||||||
|
allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes),
|
||||||
|
),
|
||||||
|
].sort((a, b) => a.localeCompare(b))
|
||||||
const tagItemMap: Map<string, QuartzPluginData[]> = new Map()
|
const tagItemMap: Map<string, QuartzPluginData[]> = new Map()
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
tagItemMap.set(tag, allPagesWithTag(tag))
|
tagItemMap.set(tag, allPagesWithTag(tag))
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export function pageResources(
|
|||||||
staticResources: StaticResources,
|
staticResources: 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())`
|
||||||
|
|
||||||
return {
|
return {
|
||||||
css: [joinSegments(baseDir, "index.css"), ...staticResources.css],
|
css: [joinSegments(baseDir, "index.css"), ...staticResources.css],
|
||||||
@@ -74,13 +74,13 @@ export function renderPage(
|
|||||||
const classNames = (node.properties?.className ?? []) as string[]
|
const classNames = (node.properties?.className ?? []) as string[]
|
||||||
if (classNames.includes("transclude")) {
|
if (classNames.includes("transclude")) {
|
||||||
const inner = node.children[0] as Element
|
const inner = node.children[0] as Element
|
||||||
const transcludeTarget = inner.properties?.["data-slug"] as FullSlug
|
const transcludeTarget = inner.properties["data-slug"] as FullSlug
|
||||||
const page = getOrComputeFileIndex(componentData.allFiles).get(transcludeTarget)
|
const page = getOrComputeFileIndex(componentData.allFiles).get(transcludeTarget)
|
||||||
if (!page) {
|
if (!page) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let blockRef = node.properties?.dataBlock as string | undefined
|
let blockRef = node.properties.dataBlock as string | undefined
|
||||||
if (blockRef?.startsWith("#^")) {
|
if (blockRef?.startsWith("#^")) {
|
||||||
// block transclude
|
// block transclude
|
||||||
blockRef = blockRef.slice("#^".length)
|
blockRef = blockRef.slice("#^".length)
|
||||||
@@ -90,6 +90,7 @@ export function renderPage(
|
|||||||
blockNode = {
|
blockNode = {
|
||||||
type: "element",
|
type: "element",
|
||||||
tagName: "ul",
|
tagName: "ul",
|
||||||
|
properties: {},
|
||||||
children: [blockNode],
|
children: [blockNode],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,6 +145,7 @@ export function renderPage(
|
|||||||
{
|
{
|
||||||
type: "element",
|
type: "element",
|
||||||
tagName: "h1",
|
tagName: "h1",
|
||||||
|
properties: {},
|
||||||
children: [
|
children: [
|
||||||
{ type: "text", value: page.frontmatter?.title ?? `Transclude of ${page.slug}` },
|
{ type: "text", value: page.frontmatter?.title ?? `Transclude of ${page.slug}` },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -59,8 +59,7 @@ function toggleFolder(evt: MouseEvent) {
|
|||||||
// Save folder state to localStorage
|
// Save folder state to localStorage
|
||||||
const clickFolderPath = currentFolderParent.dataset.folderpath as string
|
const clickFolderPath = currentFolderParent.dataset.folderpath as string
|
||||||
|
|
||||||
// Remove leading "/"
|
const fullFolderPath = clickFolderPath
|
||||||
const fullFolderPath = clickFolderPath.substring(1)
|
|
||||||
toggleCollapsedByPath(explorerState, fullFolderPath)
|
toggleCollapsedByPath(explorerState, fullFolderPath)
|
||||||
|
|
||||||
const stringifiedFileTree = JSON.stringify(explorerState)
|
const stringifiedFileTree = JSON.stringify(explorerState)
|
||||||
@@ -108,9 +107,7 @@ function setupExplorer() {
|
|||||||
explorerState = JSON.parse(storageTree)
|
explorerState = JSON.parse(storageTree)
|
||||||
explorerState.map((folderUl) => {
|
explorerState.map((folderUl) => {
|
||||||
// grab <li> element for matching folder path
|
// grab <li> element for matching folder path
|
||||||
const folderLi = document.querySelector(
|
const folderLi = document.querySelector(`[data-folderpath='${folderUl.path}']`) as HTMLElement
|
||||||
`[data-folderpath='/${folderUl.path}']`,
|
|
||||||
) as HTMLElement
|
|
||||||
|
|
||||||
// Get corresponding content <ul> tag and set state
|
// Get corresponding content <ul> tag and set state
|
||||||
if (folderLi) {
|
if (folderLi) {
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
import Plausible from "plausible-tracker"
|
|
||||||
const { trackPageview } = Plausible()
|
|
||||||
document.addEventListener("nav", () => trackPageview())
|
|
||||||
@@ -7,6 +7,10 @@ async function mouseEnterHandler(
|
|||||||
{ clientX, clientY }: { clientX: number; clientY: number },
|
{ clientX, clientY }: { clientX: number; clientY: number },
|
||||||
) {
|
) {
|
||||||
const link = this
|
const link = this
|
||||||
|
if (link.dataset.noPopover === "true") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
async function setPosition(popoverElement: HTMLElement) {
|
async function setPosition(popoverElement: HTMLElement) {
|
||||||
const { x, y } = await computePosition(link, popoverElement, {
|
const { x, y } = await computePosition(link, popoverElement, {
|
||||||
middleware: [inline({ x: clientX, y: clientY }), shift(), flip()],
|
middleware: [inline({ x: clientX, y: clientY }), shift(), flip()],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
float: right;
|
float: right;
|
||||||
right: 0;
|
right: 0;
|
||||||
padding: 0.4rem;
|
padding: 0.4rem;
|
||||||
margin: -0.2rem 0.3rem;
|
margin: 0.3rem;
|
||||||
color: var(--gray);
|
color: var(--gray);
|
||||||
border-color: var(--dark);
|
border-color: var(--dark);
|
||||||
background-color: var(--light);
|
background-color: var(--light);
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ svg {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
font-family: var(--headerFont);
|
font-family: var(--headerFont);
|
||||||
|
|
||||||
& p {
|
& span {
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: var(--secondary);
|
color: var(--secondary);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export type QuartzComponentProps = {
|
|||||||
fileData: QuartzPluginData
|
fileData: QuartzPluginData
|
||||||
cfg: GlobalConfiguration
|
cfg: GlobalConfiguration
|
||||||
children: (QuartzComponent | JSX.Element)[]
|
children: (QuartzComponent | JSX.Element)[]
|
||||||
tree: Node<QuartzPluginData>
|
tree: Node
|
||||||
allFiles: QuartzPluginData[]
|
allFiles: QuartzPluginData[]
|
||||||
displayClass?: "mobile-only" | "desktop-only"
|
displayClass?: "mobile-only" | "desktop-only"
|
||||||
} & JSX.IntrinsicAttributes & {
|
} & JSX.IntrinsicAttributes & {
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import { QuartzEmitterPlugin } from "../types"
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import spaRouterScript from "../../components/scripts/spa.inline"
|
import spaRouterScript from "../../components/scripts/spa.inline"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import plausibleScript from "../../components/scripts/plausible.inline"
|
|
||||||
// @ts-ignore
|
|
||||||
import popoverScript from "../../components/scripts/popover.inline"
|
import popoverScript from "../../components/scripts/popover.inline"
|
||||||
import styles from "../../styles/custom.scss"
|
import styles from "../../styles/custom.scss"
|
||||||
import popoverStyle from "../../components/styles/popover.scss"
|
import popoverStyle from "../../components/styles/popover.scss"
|
||||||
@@ -14,6 +12,7 @@ import { StaticResources } from "../../util/resources"
|
|||||||
import { QuartzComponent } from "../../components/types"
|
import { QuartzComponent } from "../../components/types"
|
||||||
import { googleFontHref, joinStyles } from "../../util/theme"
|
import { googleFontHref, joinStyles } from "../../util/theme"
|
||||||
import { Features, transform } from "lightningcss"
|
import { Features, transform } from "lightningcss"
|
||||||
|
import { transform as transpile } from "esbuild"
|
||||||
|
|
||||||
type ComponentResources = {
|
type ComponentResources = {
|
||||||
css: string[]
|
css: string[]
|
||||||
@@ -56,9 +55,16 @@ function getComponentResources(ctx: BuildCtx): ComponentResources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function joinScripts(scripts: string[]): string {
|
async function joinScripts(scripts: string[]): Promise<string> {
|
||||||
// wrap with iife to prevent scope collision
|
// wrap with iife to prevent scope collision
|
||||||
return scripts.map((script) => `(function () {${script}})();`).join("\n")
|
const script = scripts.map((script) => `(function () {${script}})();`).join("\n")
|
||||||
|
|
||||||
|
// minify with esbuild
|
||||||
|
const res = await transpile(script, {
|
||||||
|
minify: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.code
|
||||||
}
|
}
|
||||||
|
|
||||||
function addGlobalPageResources(
|
function addGlobalPageResources(
|
||||||
@@ -85,17 +91,30 @@ function addGlobalPageResources(
|
|||||||
componentResources.afterDOMLoaded.push(`
|
componentResources.afterDOMLoaded.push(`
|
||||||
window.dataLayer = window.dataLayer || [];
|
window.dataLayer = window.dataLayer || [];
|
||||||
function gtag() { dataLayer.push(arguments); }
|
function gtag() { dataLayer.push(arguments); }
|
||||||
gtag(\`js\`, new Date());
|
gtag("js", new Date());
|
||||||
gtag(\`config\`, \`${tagId}\`, { send_page_view: false });
|
gtag("config", "${tagId}", { send_page_view: false });
|
||||||
|
|
||||||
document.addEventListener(\`nav\`, () => {
|
document.addEventListener("nav", () => {
|
||||||
gtag(\`event\`, \`page_view\`, {
|
gtag("event", "page_view", {
|
||||||
page_title: document.title,
|
page_title: document.title,
|
||||||
page_location: location.href,
|
page_location: location.href,
|
||||||
});
|
});
|
||||||
});`)
|
});`)
|
||||||
} else if (cfg.analytics?.provider === "plausible") {
|
} else if (cfg.analytics?.provider === "plausible") {
|
||||||
componentResources.afterDOMLoaded.push(plausibleScript)
|
const plausibleHost = cfg.analytics.host ?? "https://plausible.io"
|
||||||
|
componentResources.afterDOMLoaded.push(`
|
||||||
|
const plausibleScript = document.createElement("script")
|
||||||
|
plausibleScript.src = "${plausibleHost}/js/script.manual.js"
|
||||||
|
plausibleScript.setAttribute("data-domain", location.hostname)
|
||||||
|
plausibleScript.defer = true
|
||||||
|
document.head.appendChild(plausibleScript)
|
||||||
|
|
||||||
|
window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }
|
||||||
|
|
||||||
|
document.addEventListener("nav", () => {
|
||||||
|
plausible("pageview")
|
||||||
|
})
|
||||||
|
`)
|
||||||
} else if (cfg.analytics?.provider === "umami") {
|
} else if (cfg.analytics?.provider === "umami") {
|
||||||
componentResources.afterDOMLoaded.push(`
|
componentResources.afterDOMLoaded.push(`
|
||||||
const umamiScript = document.createElement("script")
|
const umamiScript = document.createElement("script")
|
||||||
@@ -165,8 +184,11 @@ export const ComponentResources: QuartzEmitterPlugin<Options> = (opts?: Partial<
|
|||||||
addGlobalPageResources(ctx, resources, componentResources)
|
addGlobalPageResources(ctx, resources, componentResources)
|
||||||
|
|
||||||
const stylesheet = joinStyles(ctx.cfg.configuration.theme, ...componentResources.css, styles)
|
const stylesheet = joinStyles(ctx.cfg.configuration.theme, ...componentResources.css, styles)
|
||||||
const prescript = joinScripts(componentResources.beforeDOMLoaded)
|
const [prescript, postscript] = await Promise.all([
|
||||||
const postscript = joinScripts(componentResources.afterDOMLoaded)
|
joinScripts(componentResources.beforeDOMLoaded),
|
||||||
|
joinScripts(componentResources.afterDOMLoaded),
|
||||||
|
])
|
||||||
|
|
||||||
const fps = await Promise.all([
|
const fps = await Promise.all([
|
||||||
emit({
|
emit({
|
||||||
slug: "index" as FullSlug,
|
slug: "index" as FullSlug,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Root } from "hast"
|
|||||||
import { GlobalConfiguration } from "../../cfg"
|
import { GlobalConfiguration } from "../../cfg"
|
||||||
import { getDate } from "../../components/Date"
|
import { getDate } from "../../components/Date"
|
||||||
import { escapeHTML } from "../../util/escape"
|
import { escapeHTML } from "../../util/escape"
|
||||||
import { FilePath, FullSlug, SimpleSlug, simplifySlug } from "../../util/path"
|
import { FilePath, FullSlug, SimpleSlug, joinSegments, simplifySlug } from "../../util/path"
|
||||||
import { QuartzEmitterPlugin } from "../types"
|
import { QuartzEmitterPlugin } from "../types"
|
||||||
import { toHtml } from "hast-util-to-html"
|
import { toHtml } from "hast-util-to-html"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
@@ -37,7 +37,7 @@ const defaultOptions: Options = {
|
|||||||
function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string {
|
function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string {
|
||||||
const base = cfg.baseUrl ?? ""
|
const base = cfg.baseUrl ?? ""
|
||||||
const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<url>
|
const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<url>
|
||||||
<loc>https://${base}/${encodeURI(slug)}</loc>
|
<loc>https://${joinSegments(base, encodeURI(slug))}</loc>
|
||||||
<lastmod>${content.date?.toISOString()}</lastmod>
|
<lastmod>${content.date?.toISOString()}</lastmod>
|
||||||
</url>`
|
</url>`
|
||||||
const urls = Array.from(idx)
|
const urls = Array.from(idx)
|
||||||
@@ -52,8 +52,8 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: nu
|
|||||||
|
|
||||||
const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<item>
|
const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<item>
|
||||||
<title>${escapeHTML(content.title)}</title>
|
<title>${escapeHTML(content.title)}</title>
|
||||||
<link>${root}/${encodeURI(slug)}</link>
|
<link>${joinSegments(root, encodeURI(slug))}</link>
|
||||||
<guid>${root}/${encodeURI(slug)}</guid>
|
<guid>${joinSegments(root, encodeURI(slug))}</guid>
|
||||||
<description>${content.richContent ?? content.description}</description>
|
<description>${content.richContent ?? content.description}</description>
|
||||||
<pubDate>${content.date?.toUTCString()}</pubDate>
|
<pubDate>${content.date?.toUTCString()}</pubDate>
|
||||||
</item>`
|
</item>`
|
||||||
|
|||||||
@@ -40,12 +40,13 @@ export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
|
|||||||
const tags: Set<string> = new Set(
|
const tags: Set<string> = new Set(
|
||||||
allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes),
|
allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes),
|
||||||
)
|
)
|
||||||
|
|
||||||
// add base tag
|
// add base tag
|
||||||
tags.add("index")
|
tags.add("index")
|
||||||
|
|
||||||
const tagDescriptions: Record<string, ProcessedContent> = Object.fromEntries(
|
const tagDescriptions: Record<string, ProcessedContent> = Object.fromEntries(
|
||||||
[...tags].map((tag) => {
|
[...tags].map((tag) => {
|
||||||
const title = tag === "" ? "Tag Index" : `Tag: #${tag}`
|
const title = tag === "index" ? "Tag Index" : `Tag: #${tag}`
|
||||||
return [
|
return [
|
||||||
tag,
|
tag,
|
||||||
defaultProcessedContent({
|
defaultProcessedContent({
|
||||||
|
|||||||
@@ -30,5 +30,6 @@ declare module "vfile" {
|
|||||||
interface DataMap {
|
interface DataMap {
|
||||||
slug: FullSlug
|
slug: FullSlug
|
||||||
filePath: FilePath
|
filePath: FilePath
|
||||||
|
relativePath: FilePath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,18 @@ import { QuartzTransformerPlugin } from "../types"
|
|||||||
import yaml from "js-yaml"
|
import yaml from "js-yaml"
|
||||||
import toml from "toml"
|
import toml from "toml"
|
||||||
import { slugTag } from "../../util/path"
|
import { slugTag } from "../../util/path"
|
||||||
|
import { QuartzPluginData } from "../vfile"
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
delims: string | string[]
|
delims: string | string[]
|
||||||
language: "yaml" | "toml"
|
language: "yaml" | "toml"
|
||||||
|
oneLineTagDelim: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions: Options = {
|
const defaultOptions: Options = {
|
||||||
delims: "---",
|
delims: "---",
|
||||||
language: "yaml",
|
language: "yaml",
|
||||||
|
oneLineTagDelim: ",",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
||||||
@@ -20,11 +23,13 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined>
|
|||||||
return {
|
return {
|
||||||
name: "FrontMatter",
|
name: "FrontMatter",
|
||||||
markdownPlugins() {
|
markdownPlugins() {
|
||||||
|
const { oneLineTagDelim } = opts
|
||||||
|
|
||||||
return [
|
return [
|
||||||
[remarkFrontmatter, ["yaml", "toml"]],
|
[remarkFrontmatter, ["yaml", "toml"]],
|
||||||
() => {
|
() => {
|
||||||
return (_, file) => {
|
return (_, file) => {
|
||||||
const { data } = matter(file.value, {
|
const { data } = matter(Buffer.from(file.value), {
|
||||||
...opts,
|
...opts,
|
||||||
engines: {
|
engines: {
|
||||||
yaml: (s) => yaml.load(s, { schema: yaml.JSON_SCHEMA }) as object,
|
yaml: (s) => yaml.load(s, { schema: yaml.JSON_SCHEMA }) as object,
|
||||||
@@ -40,24 +45,30 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined>
|
|||||||
// coerce title to string
|
// coerce title to string
|
||||||
if (data.title) {
|
if (data.title) {
|
||||||
data.title = data.title.toString()
|
data.title = data.title.toString()
|
||||||
|
} else if (data.title === null || data.title === undefined) {
|
||||||
|
data.title = file.stem ?? "Untitled"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.tags && !Array.isArray(data.tags)) {
|
if (data.tags) {
|
||||||
|
// coerce to array
|
||||||
|
if (!Array.isArray(data.tags)) {
|
||||||
data.tags = data.tags
|
data.tags = data.tags
|
||||||
.toString()
|
.toString()
|
||||||
.split(",")
|
.split(oneLineTagDelim)
|
||||||
.map((tag: string) => tag.trim())
|
.map((tag: string) => tag.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove all non-string tags
|
||||||
|
data.tags = data.tags
|
||||||
|
.filter((tag: unknown) => typeof tag === "string" || typeof tag === "number")
|
||||||
|
.map((tag: string | number) => tag.toString())
|
||||||
|
}
|
||||||
|
|
||||||
// slug them all!!
|
// slug them all!!
|
||||||
data.tags = [...new Set(data.tags?.map((tag: string) => slugTag(tag)))] ?? []
|
data.tags = [...new Set(data.tags?.map((tag: string) => slugTag(tag)))]
|
||||||
|
|
||||||
// fill in frontmatter
|
// fill in frontmatter
|
||||||
file.data.frontmatter = {
|
file.data.frontmatter = data as QuartzPluginData["frontmatter"]
|
||||||
title: file.stem ?? "Untitled",
|
|
||||||
tags: [],
|
|
||||||
...data,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> |
|
|||||||
rehypeAutolinkHeadings,
|
rehypeAutolinkHeadings,
|
||||||
{
|
{
|
||||||
behavior: "append",
|
behavior: "append",
|
||||||
|
properties: {
|
||||||
|
ariaHidden: true,
|
||||||
|
tabIndex: -1,
|
||||||
|
"data-no-popover": true,
|
||||||
|
},
|
||||||
content: {
|
content: {
|
||||||
type: "text",
|
type: "text",
|
||||||
value: " §",
|
value: " §",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import remarkMath from "remark-math"
|
import remarkMath from "remark-math"
|
||||||
import rehypeKatex from "rehype-katex"
|
import rehypeKatex from "rehype-katex"
|
||||||
import rehypeMathjax from "rehype-mathjax/svg.js"
|
import rehypeMathjax from "rehype-mathjax/svg"
|
||||||
import { QuartzTransformerPlugin } from "../types"
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { visit } from "unist-util-visit"
|
import { visit } from "unist-util-visit"
|
||||||
import isAbsoluteUrl from "is-absolute-url"
|
import isAbsoluteUrl from "is-absolute-url"
|
||||||
|
import { Root } from "hast"
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
/** How to resolve Markdown paths */
|
/** How to resolve Markdown paths */
|
||||||
@@ -19,12 +20,14 @@ interface Options {
|
|||||||
/** Strips folders from a link so that it looks nice */
|
/** Strips folders from a link so that it looks nice */
|
||||||
prettyLinks: boolean
|
prettyLinks: boolean
|
||||||
openLinksInNewTab: boolean
|
openLinksInNewTab: boolean
|
||||||
|
lazyLoad: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions: Options = {
|
const defaultOptions: Options = {
|
||||||
markdownLinkResolution: "absolute",
|
markdownLinkResolution: "absolute",
|
||||||
prettyLinks: true,
|
prettyLinks: true,
|
||||||
openLinksInNewTab: false,
|
openLinksInNewTab: false,
|
||||||
|
lazyLoad: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
||||||
@@ -34,7 +37,7 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
|
|||||||
htmlPlugins(ctx) {
|
htmlPlugins(ctx) {
|
||||||
return [
|
return [
|
||||||
() => {
|
() => {
|
||||||
return (tree, file) => {
|
return (tree: Root, file) => {
|
||||||
const curSlug = simplifySlug(file.data.slug!)
|
const curSlug = simplifySlug(file.data.slug!)
|
||||||
const outgoing: Set<SimpleSlug> = new Set()
|
const outgoing: Set<SimpleSlug> = new Set()
|
||||||
|
|
||||||
@@ -51,8 +54,8 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
|
|||||||
typeof node.properties.href === "string"
|
typeof node.properties.href === "string"
|
||||||
) {
|
) {
|
||||||
let dest = node.properties.href as RelativeURL
|
let dest = node.properties.href as RelativeURL
|
||||||
node.properties.className ??= []
|
const classes = (node.properties.className ?? []) as string[]
|
||||||
node.properties.className.push(isAbsoluteUrl(dest) ? "external" : "internal")
|
classes.push(isAbsoluteUrl(dest) ? "external" : "internal")
|
||||||
|
|
||||||
// Check if the link has alias text
|
// Check if the link has alias text
|
||||||
if (
|
if (
|
||||||
@@ -61,8 +64,9 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
|
|||||||
node.children[0].value !== dest
|
node.children[0].value !== dest
|
||||||
) {
|
) {
|
||||||
// Add the 'alias' class if the text content is not the same as the href
|
// Add the 'alias' class if the text content is not the same as the href
|
||||||
node.properties.className.push("alias")
|
classes.push("alias")
|
||||||
}
|
}
|
||||||
|
node.properties.className = classes
|
||||||
|
|
||||||
if (opts.openLinksInNewTab) {
|
if (opts.openLinksInNewTab) {
|
||||||
node.properties.target = "_blank"
|
node.properties.target = "_blank"
|
||||||
@@ -111,6 +115,10 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
|
|||||||
node.properties &&
|
node.properties &&
|
||||||
typeof node.properties.src === "string"
|
typeof node.properties.src === "string"
|
||||||
) {
|
) {
|
||||||
|
if (opts.lazyLoad) {
|
||||||
|
node.properties.loading = "lazy"
|
||||||
|
}
|
||||||
|
|
||||||
if (!isAbsoluteUrl(node.properties.src)) {
|
if (!isAbsoluteUrl(node.properties.src)) {
|
||||||
let dest = node.properties.src as RelativeURL
|
let dest = node.properties.src as RelativeURL
|
||||||
dest = node.properties.src = transformLink(
|
dest = node.properties.src = transformLink(
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { PluggableList } from "unified"
|
|
||||||
import { QuartzTransformerPlugin } from "../types"
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
import { Root, HTML, BlockContent, DefinitionContent, Code, Paragraph } from "mdast"
|
import { Root, Html, BlockContent, DefinitionContent, Paragraph, Code } from "mdast"
|
||||||
import { Element, Literal, Root as HtmlRoot } from "hast"
|
import { Element, Literal, Root as HtmlRoot } from "hast"
|
||||||
import { Replace, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace"
|
import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace"
|
||||||
import { slug as slugAnchor } from "github-slugger"
|
import { slug as slugAnchor } from "github-slugger"
|
||||||
import rehypeRaw from "rehype-raw"
|
import rehypeRaw from "rehype-raw"
|
||||||
import { visit } from "unist-util-visit"
|
import { visit } from "unist-util-visit"
|
||||||
@@ -15,6 +14,7 @@ import { toHast } from "mdast-util-to-hast"
|
|||||||
import { toHtml } from "hast-util-to-html"
|
import { toHtml } from "hast-util-to-html"
|
||||||
import { PhrasingContent } from "mdast-util-find-and-replace/lib"
|
import { PhrasingContent } from "mdast-util-find-and-replace/lib"
|
||||||
import { capitalize } from "../../util/lang"
|
import { capitalize } from "../../util/lang"
|
||||||
|
import { PluggableList } from "unified"
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
comments: boolean
|
comments: boolean
|
||||||
@@ -105,6 +105,8 @@ function canonicalizeCallout(calloutName: string): keyof typeof callouts {
|
|||||||
return calloutMapping[callout] ?? "note"
|
return calloutMapping[callout] ?? "note"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const externalLinkRegex = /^https?:\/\//i
|
||||||
|
|
||||||
// !? -> optional embedding
|
// !? -> optional embedding
|
||||||
// \[\[ -> open brace
|
// \[\[ -> open brace
|
||||||
// ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name)
|
// ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name)
|
||||||
@@ -121,8 +123,8 @@ const calloutRegex = new RegExp(/^\[\!(\w+)\]([+-]?)/)
|
|||||||
const calloutLineRegex = new RegExp(/^> *\[\!\w+\][+-]?.*$/, "gm")
|
const calloutLineRegex = new RegExp(/^> *\[\!\w+\][+-]?.*$/, "gm")
|
||||||
// (?:^| ) -> non-capturing group, tag should start be separated by a space or be the start of the line
|
// (?:^| ) -> non-capturing group, tag should start be separated by a space or be the start of the line
|
||||||
// #(...) -> capturing group, tag itself must start with #
|
// #(...) -> capturing group, tag itself must start with #
|
||||||
// (?:[-_\p{L}])+ -> non-capturing group, non-empty string of (Unicode-aware) alpha-numeric characters, hyphens and/or underscores
|
// (?:[-_\p{L}\d\p{Z}])+ -> non-capturing group, non-empty string of (Unicode-aware) alpha-numeric characters and symbols, hyphens and/or underscores
|
||||||
// (?:\/[-_\p{L}]+)*) -> non-capturing group, matches an arbitrary number of tag strings separated by "/"
|
// (?:\/[-_\p{L}\d\p{Z}]+)*) -> non-capturing group, matches an arbitrary number of tag strings separated by "/"
|
||||||
const tagRegex = new RegExp(/(?:^| )#((?:[-_\p{L}\d])+(?:\/[-_\p{L}\d]+)*)/, "gu")
|
const tagRegex = new RegExp(/(?:^| )#((?:[-_\p{L}\d])+(?:\/[-_\p{L}\d]+)*)/, "gu")
|
||||||
const blockReferenceRegex = new RegExp(/\^([A-Za-z0-9]+)$/, "g")
|
const blockReferenceRegex = new RegExp(/\^([A-Za-z0-9]+)$/, "g")
|
||||||
|
|
||||||
@@ -136,39 +138,15 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
return toHtml(hast, { allowDangerousHtml: true })
|
return toHtml(hast, { allowDangerousHtml: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
const findAndReplace = opts.enableInHtmlEmbed
|
|
||||||
? (tree: Root, regex: RegExp, replace?: Replace | null | undefined) => {
|
|
||||||
if (replace) {
|
|
||||||
visit(tree, "html", (node: HTML) => {
|
|
||||||
if (typeof replace === "string") {
|
|
||||||
node.value = node.value.replace(regex, replace)
|
|
||||||
} else {
|
|
||||||
node.value = node.value.replaceAll(regex, (substring: string, ...args) => {
|
|
||||||
const replaceValue = replace(substring, ...args)
|
|
||||||
if (typeof replaceValue === "string") {
|
|
||||||
return replaceValue
|
|
||||||
} else if (Array.isArray(replaceValue)) {
|
|
||||||
return replaceValue.map(mdastToHtml).join("")
|
|
||||||
} else if (typeof replaceValue === "object" && replaceValue !== null) {
|
|
||||||
return mdastToHtml(replaceValue)
|
|
||||||
} else {
|
|
||||||
return substring
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
mdastFindReplace(tree, regex, replace)
|
|
||||||
}
|
|
||||||
: mdastFindReplace
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: "ObsidianFlavoredMarkdown",
|
name: "ObsidianFlavoredMarkdown",
|
||||||
textTransform(_ctx, src) {
|
textTransform(_ctx, src) {
|
||||||
// pre-transform blockquotes
|
// pre-transform blockquotes
|
||||||
if (opts.callouts) {
|
if (opts.callouts) {
|
||||||
|
if (src instanceof Buffer) {
|
||||||
src = src.toString()
|
src = src.toString()
|
||||||
|
}
|
||||||
|
|
||||||
src = src.replaceAll(calloutLineRegex, (value) => {
|
src = src.replaceAll(calloutLineRegex, (value) => {
|
||||||
// force newline after title of callout
|
// force newline after title of callout
|
||||||
return value + "\n> "
|
return value + "\n> "
|
||||||
@@ -177,15 +155,24 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
|
|
||||||
// pre-transform wikilinks (fix anchors to things that may contain illegal syntax e.g. codeblocks, latex)
|
// pre-transform wikilinks (fix anchors to things that may contain illegal syntax e.g. codeblocks, latex)
|
||||||
if (opts.wikilinks) {
|
if (opts.wikilinks) {
|
||||||
|
if (src instanceof Buffer) {
|
||||||
src = src.toString()
|
src = src.toString()
|
||||||
|
}
|
||||||
|
|
||||||
src = src.replaceAll(wikilinkRegex, (value, ...capture) => {
|
src = src.replaceAll(wikilinkRegex, (value, ...capture) => {
|
||||||
const [rawFp, rawHeader, rawAlias] = capture
|
const [rawFp, rawHeader, rawAlias]: (string | undefined)[] = capture
|
||||||
|
|
||||||
const fp = rawFp ?? ""
|
const fp = rawFp ?? ""
|
||||||
const anchor = rawHeader?.trim().replace(/^#+/, "")
|
const anchor = rawHeader?.trim().replace(/^#+/, "")
|
||||||
const blockRef = Boolean(anchor?.startsWith("^")) ? "^" : ""
|
const blockRef = Boolean(anchor?.startsWith("^")) ? "^" : ""
|
||||||
const displayAnchor = anchor ? `#${blockRef}${slugAnchor(anchor)}` : ""
|
const displayAnchor = anchor ? `#${blockRef}${slugAnchor(anchor)}` : ""
|
||||||
const displayAlias = rawAlias ?? rawHeader?.replace("#", "|") ?? ""
|
const displayAlias = rawAlias ?? rawHeader?.replace("#", "|") ?? ""
|
||||||
const embedDisplay = value.startsWith("!") ? "!" : ""
|
const embedDisplay = value.startsWith("!") ? "!" : ""
|
||||||
|
|
||||||
|
if (rawFp?.match(externalLinkRegex)) {
|
||||||
|
return `${embedDisplay}[${displayAlias.replace(/^\|/, "")}](${rawFp})`
|
||||||
|
}
|
||||||
|
|
||||||
return `${embedDisplay}[[${fp}${displayAnchor}${displayAlias}]]`
|
return `${embedDisplay}[[${fp}${displayAnchor}${displayAlias}]]`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -194,10 +181,17 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
},
|
},
|
||||||
markdownPlugins() {
|
markdownPlugins() {
|
||||||
const plugins: PluggableList = []
|
const plugins: PluggableList = []
|
||||||
if (opts.wikilinks) {
|
|
||||||
|
// regex replacements
|
||||||
plugins.push(() => {
|
plugins.push(() => {
|
||||||
return (tree: Root, _file) => {
|
return (tree: Root, file) => {
|
||||||
findAndReplace(tree, wikilinkRegex, (value: string, ...capture: string[]) => {
|
const replacements: [RegExp, string | ReplaceFunction][] = []
|
||||||
|
const base = pathToRoot(file.data.slug!)
|
||||||
|
|
||||||
|
if (opts.wikilinks) {
|
||||||
|
replacements.push([
|
||||||
|
wikilinkRegex,
|
||||||
|
(value: string, ...capture: string[]) => {
|
||||||
let [rawFp, rawHeader, rawAlias] = capture
|
let [rawFp, rawHeader, rawAlias] = capture
|
||||||
const fp = rawFp?.trim() ?? ""
|
const fp = rawFp?.trim() ?? ""
|
||||||
const anchor = rawHeader?.trim() ?? ""
|
const anchor = rawHeader?.trim() ?? ""
|
||||||
@@ -265,37 +259,94 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
}
|
])
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.highlight) {
|
if (opts.highlight) {
|
||||||
plugins.push(() => {
|
replacements.push([
|
||||||
return (tree: Root, _file) => {
|
highlightRegex,
|
||||||
findAndReplace(tree, highlightRegex, (_value: string, ...capture: string[]) => {
|
(_value: string, ...capture: string[]) => {
|
||||||
const [inner] = capture
|
const [inner] = capture
|
||||||
return {
|
return {
|
||||||
type: "html",
|
type: "html",
|
||||||
value: `<span class="text-highlight">${inner}</span>`,
|
value: `<span class="text-highlight">${inner}</span>`,
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
}
|
])
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.comments) {
|
if (opts.comments) {
|
||||||
plugins.push(() => {
|
replacements.push([
|
||||||
return (tree: Root, _file) => {
|
commentRegex,
|
||||||
findAndReplace(tree, commentRegex, (_value: string, ..._capture: string[]) => {
|
(_value: string, ..._capture: string[]) => {
|
||||||
return {
|
return {
|
||||||
type: "text",
|
type: "text",
|
||||||
value: "",
|
value: "",
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.parseTags) {
|
||||||
|
replacements.push([
|
||||||
|
tagRegex,
|
||||||
|
(_value: string, tag: string) => {
|
||||||
|
// Check if the tag only includes numbers
|
||||||
|
if (/^\d+$/.test(tag)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
tag = slugTag(tag)
|
||||||
|
if (file.data.frontmatter && !file.data.frontmatter.tags.includes(tag)) {
|
||||||
|
file.data.frontmatter.tags.push(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "link",
|
||||||
|
url: base + `/tags/${tag}`,
|
||||||
|
data: {
|
||||||
|
hProperties: {
|
||||||
|
className: ["tag-link"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
value: `#${tag}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.enableInHtmlEmbed) {
|
||||||
|
visit(tree, "html", (node: Html) => {
|
||||||
|
for (const [regex, replace] of replacements) {
|
||||||
|
if (typeof replace === "string") {
|
||||||
|
node.value = node.value.replace(regex, replace)
|
||||||
|
} else {
|
||||||
|
node.value = node.value.replaceAll(regex, (substring: string, ...args) => {
|
||||||
|
const replaceValue = replace(substring, ...args)
|
||||||
|
if (typeof replaceValue === "string") {
|
||||||
|
return replaceValue
|
||||||
|
} else if (Array.isArray(replaceValue)) {
|
||||||
|
return replaceValue.map(mdastToHtml).join("")
|
||||||
|
} else if (typeof replaceValue === "object" && replaceValue !== null) {
|
||||||
|
return mdastToHtml(replaceValue)
|
||||||
|
} else {
|
||||||
|
return substring
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
mdastFindReplace(tree, replacements)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (opts.callouts) {
|
if (opts.callouts) {
|
||||||
plugins.push(() => {
|
plugins.push(() => {
|
||||||
@@ -336,7 +387,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
<polyline points="6 9 12 15 18 9"></polyline>
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
</svg>`
|
</svg>`
|
||||||
|
|
||||||
const titleHtml: HTML = {
|
const titleHtml: Html = {
|
||||||
type: "html",
|
type: "html",
|
||||||
value: `<div
|
value: `<div
|
||||||
class="callout-title"
|
class="callout-title"
|
||||||
@@ -396,49 +447,16 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.parseTags) {
|
|
||||||
plugins.push(() => {
|
|
||||||
return (tree: Root, file) => {
|
|
||||||
const base = pathToRoot(file.data.slug!)
|
|
||||||
findAndReplace(tree, tagRegex, (_value: string, tag: string) => {
|
|
||||||
// Check if the tag only includes numbers
|
|
||||||
if (/^\d+$/.test(tag)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
tag = slugTag(tag)
|
|
||||||
if (file.data.frontmatter && !file.data.frontmatter.tags.includes(tag)) {
|
|
||||||
file.data.frontmatter.tags.push(tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "link",
|
|
||||||
url: base + `/tags/${tag}`,
|
|
||||||
data: {
|
|
||||||
hProperties: {
|
|
||||||
className: ["tag-link"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
value: `#${tag}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return plugins
|
return plugins
|
||||||
},
|
},
|
||||||
htmlPlugins() {
|
htmlPlugins() {
|
||||||
const plugins = [rehypeRaw]
|
const plugins: PluggableList = [rehypeRaw]
|
||||||
|
|
||||||
if (opts.parseBlockReferences) {
|
if (opts.parseBlockReferences) {
|
||||||
plugins.push(() => {
|
plugins.push(() => {
|
||||||
const inlineTagTypes = new Set(["p", "li"])
|
const inlineTagTypes = new Set(["p", "li"])
|
||||||
const blockTagTypes = new Set(["blockquote"])
|
const blockTagTypes = new Set(["blockquote"])
|
||||||
return (tree, file) => {
|
return (tree: HtmlRoot, file) => {
|
||||||
file.data.blocks = {}
|
file.data.blocks = {}
|
||||||
|
|
||||||
visit(tree, "element", (node, index, parent) => {
|
visit(tree, "element", (node, index, parent) => {
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ export const SyntaxHighlighting: QuartzTransformerPlugin = () => ({
|
|||||||
[
|
[
|
||||||
rehypePrettyCode,
|
rehypePrettyCode,
|
||||||
{
|
{
|
||||||
theme: "css-variables",
|
keepBackground: false,
|
||||||
|
theme: {
|
||||||
|
dark: "github-dark",
|
||||||
|
light: "github-light",
|
||||||
|
},
|
||||||
} satisfies Partial<CodeOptions>,
|
} satisfies Partial<CodeOptions>,
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Node, Parent } from "hast"
|
|||||||
import { Data, VFile } from "vfile"
|
import { Data, VFile } from "vfile"
|
||||||
|
|
||||||
export type QuartzPluginData = Data
|
export type QuartzPluginData = Data
|
||||||
export type ProcessedContent = [Node<QuartzPluginData>, VFile]
|
export type ProcessedContent = [Node, VFile]
|
||||||
|
|
||||||
export function defaultProcessedContent(vfileData: Partial<QuartzPluginData>): ProcessedContent {
|
export function defaultProcessedContent(vfileData: Partial<QuartzPluginData>): ProcessedContent {
|
||||||
const root: Parent = { type: "root", children: [] }
|
const root: Parent = { type: "root", children: [] }
|
||||||
|
|||||||
@@ -14,27 +14,25 @@ import { QuartzLogger } from "../util/log"
|
|||||||
import { trace } from "../util/trace"
|
import { trace } from "../util/trace"
|
||||||
import { BuildCtx } from "../util/ctx"
|
import { BuildCtx } from "../util/ctx"
|
||||||
|
|
||||||
export type QuartzProcessor = Processor<MDRoot, HTMLRoot, void>
|
export type QuartzProcessor = Processor<MDRoot, MDRoot, HTMLRoot>
|
||||||
export function createProcessor(ctx: BuildCtx): QuartzProcessor {
|
export function createProcessor(ctx: BuildCtx): QuartzProcessor {
|
||||||
const transformers = ctx.cfg.plugins.transformers
|
const transformers = ctx.cfg.plugins.transformers
|
||||||
|
|
||||||
|
return (
|
||||||
|
unified()
|
||||||
// base Markdown -> MD AST
|
// base Markdown -> MD AST
|
||||||
let processor = unified().use(remarkParse)
|
.use(remarkParse)
|
||||||
|
|
||||||
// MD AST -> MD AST transforms
|
// MD AST -> MD AST transforms
|
||||||
for (const plugin of transformers.filter((p) => p.markdownPlugins)) {
|
.use(
|
||||||
processor = processor.use(plugin.markdownPlugins!(ctx))
|
transformers
|
||||||
}
|
.filter((p) => p.markdownPlugins)
|
||||||
|
.flatMap((plugin) => plugin.markdownPlugins!(ctx)),
|
||||||
|
)
|
||||||
// MD AST -> HTML AST
|
// MD AST -> HTML AST
|
||||||
processor = processor.use(remarkRehype, { allowDangerousHtml: true })
|
.use(remarkRehype, { allowDangerousHtml: true })
|
||||||
|
|
||||||
// HTML AST -> HTML AST transforms
|
// HTML AST -> HTML AST transforms
|
||||||
for (const plugin of transformers.filter((p) => p.htmlPlugins)) {
|
.use(transformers.filter((p) => p.htmlPlugins).flatMap((plugin) => plugin.htmlPlugins!(ctx)))
|
||||||
processor = processor.use(plugin.htmlPlugins!(ctx))
|
)
|
||||||
}
|
|
||||||
|
|
||||||
return processor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function* chunks<T>(arr: T[], n: number) {
|
function* chunks<T>(arr: T[], n: number) {
|
||||||
@@ -89,12 +87,13 @@ export function createFileParser(ctx: BuildCtx, fps: FilePath[]) {
|
|||||||
|
|
||||||
// Text -> Text transforms
|
// Text -> Text transforms
|
||||||
for (const plugin of cfg.plugins.transformers.filter((p) => p.textTransform)) {
|
for (const plugin of cfg.plugins.transformers.filter((p) => p.textTransform)) {
|
||||||
file.value = plugin.textTransform!(ctx, file.value)
|
file.value = plugin.textTransform!(ctx, file.value.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
// base data properties that plugins may use
|
// base data properties that plugins may use
|
||||||
file.data.slug = slugifyFilePath(path.posix.relative(argv.directory, file.path) as FilePath)
|
file.data.filePath = file.path as FilePath
|
||||||
file.data.filePath = fp
|
file.data.relativePath = path.posix.relative(argv.directory, file.path) as FilePath
|
||||||
|
file.data.slug = slugifyFilePath(file.data.relativePath)
|
||||||
|
|
||||||
const ast = processor.parse(file)
|
const ast = processor.parse(file)
|
||||||
const newAst = await processor.run(ast, file)
|
const newAst = await processor.run(ast, file)
|
||||||
|
|||||||
@@ -64,11 +64,17 @@ a {
|
|||||||
color: var(--tertiary) !important;
|
color: var(--tertiary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.internal:not(:has(> img)) {
|
&.internal {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: var(--highlight);
|
background-color: var(--highlight);
|
||||||
padding: 0 0.1rem;
|
padding: 0 0.1rem;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
||||||
|
&:has(> img) {
|
||||||
|
background-color: none;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,8 +100,6 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
& article {
|
& article {
|
||||||
position: relative;
|
|
||||||
|
|
||||||
& > h1 {
|
& > h1 {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
@@ -300,11 +304,13 @@ h6 {
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
div[data-rehype-pretty-code-fragment] {
|
figure[data-rehype-pretty-code-figure] {
|
||||||
|
margin: 0;
|
||||||
|
position: relative;
|
||||||
line-height: 1.6rem;
|
line-height: 1.6rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
& > div[data-rehype-pretty-code-title] {
|
& > [data-rehype-pretty-code-title] {
|
||||||
font-family: var(--codeFont);
|
font-family: var(--codeFont);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
padding: 0.1rem 0.5rem;
|
padding: 0.1rem 0.5rem;
|
||||||
@@ -316,13 +322,13 @@ div[data-rehype-pretty-code-fragment] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
& > pre {
|
& > pre {
|
||||||
padding: 0.5rem 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
font-family: var(--codeFont);
|
font-family: var(--codeFont);
|
||||||
padding: 0.5rem;
|
padding: 0 0.5rem;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
border: 1px solid var(--lightgray);
|
border: 1px solid var(--lightgray);
|
||||||
@@ -338,6 +344,7 @@ pre {
|
|||||||
counter-reset: line;
|
counter-reset: line;
|
||||||
counter-increment: line 0;
|
counter-increment: line 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
|
||||||
& [data-highlighted-chars] {
|
& [data-highlighted-chars] {
|
||||||
background-color: var(--highlight);
|
background-color: var(--highlight);
|
||||||
|
|||||||
@@ -1,29 +1,17 @@
|
|||||||
// npx convert-sh-theme https://raw.githubusercontent.com/shikijs/shiki/main/packages/shiki/themes/github-light.json
|
code[data-theme*=" "] {
|
||||||
:root {
|
color: var(--shiki-light);
|
||||||
--shiki-color-text: #24292e;
|
background-color: var(--shiki-light-bg);
|
||||||
--shiki-color-background: #f8f8f8;
|
|
||||||
--shiki-token-constant: #005cc5;
|
|
||||||
--shiki-token-string: #032f62;
|
|
||||||
--shiki-token-comment: #6a737d;
|
|
||||||
--shiki-token-keyword: #d73a49;
|
|
||||||
--shiki-token-parameter: #24292e;
|
|
||||||
--shiki-token-function: #24292e;
|
|
||||||
--shiki-token-string-expression: #22863a;
|
|
||||||
--shiki-token-punctuation: #24292e;
|
|
||||||
--shiki-token-link: #24292e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// npx convert-sh-theme https://raw.githubusercontent.com/shikijs/shiki/main/packages/shiki/themes/github-dark.json
|
code[data-theme*=" "] span {
|
||||||
[saved-theme="dark"] {
|
color: var(--shiki-light);
|
||||||
--shiki-color-text: #e1e4e8 !important;
|
}
|
||||||
--shiki-color-background: #24292e !important;
|
|
||||||
--shiki-token-constant: #79b8ff !important;
|
[saved-theme="dark"] code[data-theme*=" "] {
|
||||||
--shiki-token-string: #9ecbff !important;
|
color: var(--shiki-dark);
|
||||||
--shiki-token-comment: #6a737d !important;
|
background-color: var(--shiki-dark-bg);
|
||||||
--shiki-token-keyword: #f97583 !important;
|
}
|
||||||
--shiki-token-parameter: #e1e4e8 !important;
|
|
||||||
--shiki-token-function: #e1e4e8 !important;
|
[saved-theme="dark"] code[data-theme*=" "] span {
|
||||||
--shiki-token-string-expression: #85e89d !important;
|
color: var(--shiki-dark);
|
||||||
--shiki-token-punctuation: #e1e4e8 !important;
|
|
||||||
--shiki-token-link: #e1e4e8 !important;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Components, Jsx, toJsxRuntime } from "hast-util-to-jsx-runtime"
|
import { Components, Jsx, toJsxRuntime } from "hast-util-to-jsx-runtime"
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
|
||||||
import { Node, Root } from "hast"
|
import { Node, Root } from "hast"
|
||||||
import { Fragment, jsx, jsxs } from "preact/jsx-runtime"
|
import { Fragment, jsx, jsxs } from "preact/jsx-runtime"
|
||||||
import { trace } from "./trace"
|
import { trace } from "./trace"
|
||||||
@@ -13,7 +12,7 @@ const customComponents: Components = {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
export function htmlToJsx(fp: FilePath, tree: Node<QuartzPluginData>) {
|
export function htmlToJsx(fp: FilePath, tree: Node) {
|
||||||
try {
|
try {
|
||||||
return toJsxRuntime(tree as Root, {
|
return toJsxRuntime(tree as Root, {
|
||||||
Fragment,
|
Fragment,
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { slug } from "github-slugger"
|
import { slug as slugAnchor } from "github-slugger"
|
||||||
import type { Element as HastElement } from "hast"
|
import type { Element as HastElement } from "hast"
|
||||||
|
import rfdc from "rfdc"
|
||||||
|
|
||||||
|
export const clone = rfdc()
|
||||||
|
|
||||||
// this file must be isomorphic so it can't use node libs (e.g. path)
|
// this file must be isomorphic so it can't use node libs (e.g. path)
|
||||||
|
|
||||||
export const QUARTZ = "quartz"
|
export const QUARTZ = "quartz"
|
||||||
@@ -43,6 +47,14 @@ export function getFullSlug(window: Window): FullSlug {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sluggify(s: string): string {
|
||||||
|
return s
|
||||||
|
.split("/")
|
||||||
|
.map((segment) => segment.replace(/\s/g, "-").replace(/%/g, "-percent").replace(/\?/g, "-q")) // slugify all segments
|
||||||
|
.join("/") // always use / as sep
|
||||||
|
.replace(/\/$/, "")
|
||||||
|
}
|
||||||
|
|
||||||
export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug {
|
export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug {
|
||||||
fp = _stripSlashes(fp) as FilePath
|
fp = _stripSlashes(fp) as FilePath
|
||||||
let ext = _getFileExtension(fp)
|
let ext = _getFileExtension(fp)
|
||||||
@@ -51,11 +63,7 @@ export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug {
|
|||||||
ext = ""
|
ext = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
let slug = withoutFileExt
|
let slug = sluggify(withoutFileExt)
|
||||||
.split("/")
|
|
||||||
.map((segment) => segment.replace(/\s/g, "-").replace(/%/g, "-percent").replace(/\?/g, "-q")) // slugify all segments
|
|
||||||
.join("/") // always use / as sep
|
|
||||||
.replace(/\/$/, "") // remove trailing slash
|
|
||||||
|
|
||||||
// treat _index as index
|
// treat _index as index
|
||||||
if (_endsWith(slug, "_index")) {
|
if (_endsWith(slug, "_index")) {
|
||||||
@@ -117,7 +125,8 @@ const _rebaseHastElement = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeHastElement(el: HastElement, curBase: FullSlug, newBase: FullSlug) {
|
export function normalizeHastElement(rawEl: HastElement, curBase: FullSlug, newBase: FullSlug) {
|
||||||
|
const el = clone(rawEl) // clone so we dont modify the original page
|
||||||
_rebaseHastElement(el, "src", curBase, newBase)
|
_rebaseHastElement(el, "src", curBase, newBase)
|
||||||
_rebaseHastElement(el, "href", curBase, newBase)
|
_rebaseHastElement(el, "href", curBase, newBase)
|
||||||
if (el.children) {
|
if (el.children) {
|
||||||
@@ -156,14 +165,10 @@ export function splitAnchor(link: string): [string, string] {
|
|||||||
return [fp, anchor]
|
return [fp, anchor]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function slugAnchor(anchor: string) {
|
|
||||||
return slug(anchor)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function slugTag(tag: string) {
|
export function slugTag(tag: string) {
|
||||||
return tag
|
return tag
|
||||||
.split("/")
|
.split("/")
|
||||||
.map((tagSegment) => slug(tagSegment))
|
.map((tagSegment) => sluggify(tagSegment))
|
||||||
.join("/")
|
.join("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,9 +26,12 @@ export function JSResourceToScriptElement(resource: JSResource, preserve?: boole
|
|||||||
} else {
|
} else {
|
||||||
const content = resource.script
|
const content = resource.script
|
||||||
return (
|
return (
|
||||||
<script key={randomUUID()} type={scriptType} spa-preserve={spaPreserve}>
|
<script
|
||||||
{content}
|
key={randomUUID()}
|
||||||
</script>
|
type={scriptType}
|
||||||
|
spa-preserve={spaPreserve}
|
||||||
|
dangerouslySetInnerHTML={{ __html: content }}
|
||||||
|
></script>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user