Compare commits

...

23 Commits

Author SHA1 Message Date
Jacky Zhao
64c7851939 pkg: bump version to 4.1.3 2023-12-18 09:49:55 -08:00
Jacky Zhao
ea6208c1f0 deps: bump everything (closes #635) (#636)
* deps: bump ws

* deps: bump lightningcss

* deps: workerpool

* deps: various types

* deps: chalk

* deps: globby

* deps: preact

* deps: tsx

* deps: @floating-ui/dom

* deps: esbuild

* deps: types + prettier

* deps: rimraf, typescript

* deps: remark/rehype/unified ecosystem

* format
2023-12-18 09:48:40 -08:00
Jacky Zhao
78b33fc2fb fix: release build lock before client refresh 2023-12-17 16:46:17 -08:00
Jacky Zhao
d2be097b76 feat: include tag hierarchies in tag listing, sort tag listing 2023-12-17 15:09:51 -08:00
Jacky Zhao
ad1f964a5f docs: graph view tag options 2023-12-17 13:19:03 -08:00
Jacky Zhao
150050f379 docs: agentic computing in quartz philosophy 2023-12-17 13:01:44 -08:00
Jacky Zhao
d979331dc7 fix: remove whitespace unicode from tag regex 2023-12-17 12:54:52 -08:00
Jacky Zhao
972cf0a887 feat: support emoji tags (closes #634) 2023-12-17 12:28:28 -08:00
Jacky Zhao
14e6b13ff1 docs: dont pull on first sync 2023-12-17 09:57:46 -08:00
Jacky Zhao
3c01b92cc4 docs: note embeds and update git hint 2023-12-16 11:04:18 -08:00
Jacky Zhao
ed9bd43d9f docs: update showcase 2023-12-15 12:18:29 -08:00
Jacky Zhao
c35818c336 fix: set upstream in sync handler, cleanup docs around setting up github 2023-12-14 16:48:09 -08:00
Jacky Zhao
a464ae5029 fix: format 2023-12-13 16:47:22 -08:00
Jacky Zhao
66e297c0ea css: make article no longer relative to prevent z-fighting 2023-12-13 16:40:24 -08:00
Jacky Zhao
4442847b37 fix: internal link selector specificity 2023-12-13 16:07:44 -08:00
Jacky Zhao
e6b5ca33c9 re-add gitkeep to content 2023-12-11 15:34:21 -08:00
Jacky Zhao
1b92440009 fix: better error handling on spawnsync failures 2023-12-11 10:38:55 -08:00
Jacky Zhao
c6546903f2 fix: reland string coercion in title 2023-12-10 06:19:29 -08:00
Jacky Zhao
2c69b0c97d fix: frontmatter coercion (empty string is falsy) 2023-12-08 16:55:40 -08:00
Sam Stokes
a7e20804f5 feat: Support space-delimited tags in FrontMatter transformer (#620) 2023-12-04 18:18:47 -08:00
Jacky Zhao
5196f3b9db docs: github setup and hosting fixes 2023-12-03 23:25:40 -08:00
Jimin Kim
f0ec6c9b92 fix: tag index page (#616) 2023-12-03 14:56:30 -08:00
Jacky Zhao
9c88d5967f fix: don't show popovers on heading anchors 2023-12-03 09:22:16 -08:00
31 changed files with 2500 additions and 1941 deletions

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

View File

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

View File

@@ -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 dont work well for me. There is way too much upfront friction that by the time Ive thought about how to organize my thought into folders categories, Ive 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 dont work well for me. There is way too much upfront friction that by the time Ive thought about how to organize my thought into folders categories, Ive 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.

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

View File

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

3687
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "@jackyzha0/quartz", "name": "@jackyzha0/quartz",
"description": "🌱 publish your digital garden and notes as a website", "description": "🌱 publish your digital garden and notes as a website",
"private": true, "private": true,
"version": "4.1.2", "version": "4.1.3",
"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", "plausible-tracker": "^0.3.8",
"preact": "^10.14.1", "preact": "^10.19.3",
"preact-render-to-string": "^6.0.3", "preact-render-to-string": "^6.3.1",
"pretty-bytes": "^6.1.0", "pretty-bytes": "^6.1.1",
"pretty-time": "^1.1.0", "pretty-time": "^1.1.0",
"reading-time": "^1.5.0", "reading-time": "^1.5.0",
"rehype-autolink-headings": "^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.1",
"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", "rimraf": "^5.0.5",
"serve-handler": "^6.1.5", "serve-handler": "^6.1.5",
"shikiji": "^0.8.7",
"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"
} }
} }

View File

@@ -152,10 +152,10 @@ async function startServing(
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...`))
} }
release()
clientRefresh() clientRefresh()
toRebuild.clear() toRebuild.clear()
toRemove.clear() toRemove.clear()
release()
} }
const watcher = chokidar.watch(".", { const watcher = chokidar.watch(".", {

View File

@@ -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.",
) )
gitPull(UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH)
try {
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" })
console.log(chalk.green("Done!")) if (res.status === 0) {
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.",
) )
gitPull(ORIGIN_NAME, QUARTZ_SOURCE_BRANCH) try {
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!"))

View File

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

View File

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

View File

@@ -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}` },
], ],

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,22 @@ 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 && !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())
} }
// 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,
}
} }
}, },
] ]

View File

@@ -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: " §",

View File

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

View File

@@ -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, Code, Paragraph } 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
@@ -121,8 +121,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 +136,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) {
src = src.toString() if (src instanceof Buffer) {
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,7 +153,10 @@ 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) {
src = src.toString() if (src instanceof Buffer) {
src = src.toString()
}
src = src.replaceAll(wikilinkRegex, (value, ...capture) => { src = src.replaceAll(wikilinkRegex, (value, ...capture) => {
const [rawFp, rawHeader, rawAlias] = capture const [rawFp, rawHeader, rawAlias] = capture
const fp = rawFp ?? "" const fp = rawFp ?? ""
@@ -194,108 +173,172 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
}, },
markdownPlugins() { markdownPlugins() {
const plugins: PluggableList = [] const plugins: PluggableList = []
if (opts.wikilinks) {
plugins.push(() => {
return (tree: Root, _file) => {
findAndReplace(tree, wikilinkRegex, (value: string, ...capture: string[]) => {
let [rawFp, rawHeader, rawAlias] = capture
const fp = rawFp?.trim() ?? ""
const anchor = rawHeader?.trim() ?? ""
const alias = rawAlias?.slice(1).trim()
// embed cases // regex replacements
if (value.startsWith("!")) { plugins.push(() => {
const ext: string = path.extname(fp).toLowerCase() return (tree: Root, file) => {
const url = slugifyFilePath(fp as FilePath) const replacements: [RegExp, string | ReplaceFunction][] = []
if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg"].includes(ext)) { const base = pathToRoot(file.data.slug!)
const dims = alias ?? ""
let [width, height] = dims.split("x", 2) if (opts.wikilinks) {
width ||= "auto" replacements.push([
height ||= "auto" wikilinkRegex,
return { (value: string, ...capture: string[]) => {
type: "image", let [rawFp, rawHeader, rawAlias] = capture
url, const fp = rawFp?.trim() ?? ""
data: { const anchor = rawHeader?.trim() ?? ""
hProperties: { const alias = rawAlias?.slice(1).trim()
width,
height, // embed cases
if (value.startsWith("!")) {
const ext: string = path.extname(fp).toLowerCase()
const url = slugifyFilePath(fp as FilePath)
if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg"].includes(ext)) {
const dims = alias ?? ""
let [width, height] = dims.split("x", 2)
width ||= "auto"
height ||= "auto"
return {
type: "image",
url,
data: {
hProperties: {
width,
height,
},
}, },
}, }
} } else if ([".mp4", ".webm", ".ogv", ".mov", ".mkv"].includes(ext)) {
} else if ([".mp4", ".webm", ".ogv", ".mov", ".mkv"].includes(ext)) { return {
return { type: "html",
type: "html", value: `<video src="${url}" controls></video>`,
value: `<video src="${url}" controls></video>`, }
} } else if (
} else if ( [".mp3", ".webm", ".wav", ".m4a", ".ogg", ".3gp", ".flac"].includes(ext)
[".mp3", ".webm", ".wav", ".m4a", ".ogg", ".3gp", ".flac"].includes(ext) ) {
) { return {
return { type: "html",
type: "html", value: `<audio src="${url}" controls></audio>`,
value: `<audio src="${url}" controls></audio>`, }
} } else if ([".pdf"].includes(ext)) {
} else if ([".pdf"].includes(ext)) { return {
return { type: "html",
type: "html", value: `<iframe src="${url}"></iframe>`,
value: `<iframe src="${url}"></iframe>`, }
} } else if (ext === "") {
} else if (ext === "") { const block = anchor
const block = anchor return {
return { type: "html",
type: "html", data: { hProperties: { transclude: true } },
data: { hProperties: { transclude: true } }, value: `<blockquote class="transclude" data-url="${url}" data-block="${block}"><a href="${
value: `<blockquote class="transclude" data-url="${url}" data-block="${block}"><a href="${ url + anchor
url + anchor }" class="transclude-inner">Transclude of ${url}${block}</a></blockquote>`,
}" class="transclude-inner">Transclude of ${url}${block}</a></blockquote>`, }
} }
// otherwise, fall through to regular link
} }
// otherwise, fall through to regular link // internal link
} const url = fp + anchor
return {
type: "link",
url,
children: [
{
type: "text",
value: alias ?? fp,
},
],
}
},
])
}
// internal link if (opts.highlight) {
const url = fp + anchor replacements.push([
return { highlightRegex,
type: "link", (_value: string, ...capture: string[]) => {
url, const [inner] = capture
children: [ return {
{ type: "html",
type: "text", value: `<span class="text-highlight">${inner}</span>`,
value: alias ?? fp, }
},
])
}
if (opts.comments) {
replacements.push([
commentRegex,
(_value: string, ..._capture: string[]) => {
return {
type: "text",
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.highlight) { if (opts.enableInHtmlEmbed) {
plugins.push(() => { visit(tree, "html", (node: Html) => {
return (tree: Root, _file) => { for (const [regex, replace] of replacements) {
findAndReplace(tree, highlightRegex, (_value: string, ...capture: string[]) => { if (typeof replace === "string") {
const [inner] = capture node.value = node.value.replace(regex, replace)
return { } else {
type: "html", node.value = node.value.replaceAll(regex, (substring: string, ...args) => {
value: `<span class="text-highlight">${inner}</span>`, 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
}
})
}
} }
}) })
} }
})
}
if (opts.comments) { mdastFindReplace(tree, replacements)
plugins.push(() => { }
return (tree: Root, _file) => { })
findAndReplace(tree, commentRegex, (_value: string, ..._capture: string[]) => {
return {
type: "text",
value: "",
}
})
}
})
}
if (opts.callouts) { if (opts.callouts) {
plugins.push(() => { plugins.push(() => {
@@ -336,7 +379,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,44 +439,10 @@ 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"])

View File

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

View File

@@ -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: [] }

View File

@@ -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
// base Markdown -> MD AST return (
let processor = unified().use(remarkParse) unified()
// base Markdown -> MD AST
// MD AST -> MD AST transforms .use(remarkParse)
for (const plugin of transformers.filter((p) => p.markdownPlugins)) { // MD AST -> MD AST transforms
processor = processor.use(plugin.markdownPlugins!(ctx)) .use(
} transformers
.filter((p) => p.markdownPlugins)
// MD AST -> HTML AST .flatMap((plugin) => plugin.markdownPlugins!(ctx)),
processor = processor.use(remarkRehype, { allowDangerousHtml: true }) )
// MD AST -> HTML AST
// HTML AST -> HTML AST transforms .use(remarkRehype, { allowDangerousHtml: true })
for (const plugin of transformers.filter((p) => p.htmlPlugins)) { // HTML AST -> HTML AST transforms
processor = processor.use(plugin.htmlPlugins!(ctx)) .use(transformers.filter((p) => p.htmlPlugins).flatMap((plugin) => plugin.htmlPlugins!(ctx)))
} )
return processor
} }
function* chunks<T>(arr: T[], n: number) { function* chunks<T>(arr: T[], n: number) {
@@ -89,7 +87,7 @@ 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

View 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,7 +322,7 @@ div[data-rehype-pretty-code-fragment] {
} }
& > pre { & > pre {
padding: 0.5rem 0; padding: 0;
} }
} }
@@ -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);

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
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"
// 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)
@@ -43,6 +43,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 +59,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")) {
@@ -156,14 +160,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("/")
} }