Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64c7851939 |
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -20,19 +20,12 @@ 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 and Source**
|
**Screenshots**
|
||||||
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):**
|
||||||
|
|
||||||
- Quartz Version: [e.g. v4.1.2]
|
- Device: [e.g. iPhone6]
|
||||||
- `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]
|
||||||
|
|
||||||
|
|||||||
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
@@ -1,11 +0,0 @@
|
|||||||
# To get started with Dependabot version updates, you'll need to specify which
|
|
||||||
# package ecosystems to update and where the package manifests are located.
|
|
||||||
# Please see the documentation for all configuration options:
|
|
||||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
||||||
|
|
||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "npm"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "daily"
|
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
Quartz is a set of tools that helps you publish your [digital garden](https://jzhao.xyz/posts/networked-thought) and notes as a website for free.
|
Quartz is a set of tools that helps you publish your [digital garden](https://jzhao.xyz/posts/networked-thought) and notes as a website for free.
|
||||||
Quartz v4 features a from-the-ground rewrite focusing on end-user extensibility and ease-of-use.
|
Quartz v4 features a from-the-ground rewrite focusing on end-user extensibility and ease-of-use.
|
||||||
|
|
||||||
|
**If you are looking for Quartz v3, you can find it on the [`hugo` branch](https://github.com/jackyzha0/quartz/tree/hugo).**
|
||||||
|
|
||||||
🔗 Read the documentation and get started: https://quartz.jzhao.xyz/
|
🔗 Read the documentation and get started: https://quartz.jzhao.xyz/
|
||||||
|
|
||||||
[Join the Discord Community](https://discord.gg/cRFFHYye7t)
|
[Join the Discord Community](https://discord.gg/cRFFHYye7t)
|
||||||
|
|||||||
@@ -216,19 +216,22 @@ export type QuartzEmitterPlugin<Options extends OptionType = undefined> = (
|
|||||||
|
|
||||||
export type QuartzEmitterPluginInstance = {
|
export type QuartzEmitterPluginInstance = {
|
||||||
name: string
|
name: string
|
||||||
emit(ctx: BuildCtx, content: ProcessedContent[], resources: StaticResources): Promise<FilePath[]>
|
emit(
|
||||||
|
ctx: BuildCtx,
|
||||||
|
content: ProcessedContent[],
|
||||||
|
resources: StaticResources,
|
||||||
|
emitCallback: EmitCallback,
|
||||||
|
): Promise<FilePath[]>
|
||||||
getQuartzComponents(ctx: BuildCtx): QuartzComponent[]
|
getQuartzComponents(ctx: BuildCtx): QuartzComponent[]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
An emitter plugin must define a `name` field, an `emit` function, and a `getQuartzComponents` function. `emit` is responsible for looking at all the parsed and filtered content and then appropriately creating files and returning a list of paths to files the plugin created.
|
An emitter plugin must define a `name` field an `emit` function and a `getQuartzComponents` function. `emit` is responsible for looking at all the parsed and filtered content and then appropriately creating files and returning a list of paths to files the plugin created.
|
||||||
|
|
||||||
Creating new files can be done via regular Node [fs module](https://nodejs.org/api/fs.html) (i.e. `fs.cp` or `fs.writeFile`) or via the `write` function in `quartz/plugins/emitters/helpers.ts` if you are creating files that contain text. `write` has the following signature:
|
Creating new files can be done via regular Node [fs module](https://nodejs.org/api/fs.html) (i.e. `fs.cp` or `fs.writeFile`) or via the `emitCallback` if you are creating files that contain text. The `emitCallback` function is the 4th argument of the emit function. Its interface looks something like this:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
export type WriteOptions = (data: {
|
export type EmitCallback = (data: {
|
||||||
// the build context
|
|
||||||
ctx: BuildCtx
|
|
||||||
// the name of the file to emit (not including the file extension)
|
// the name of the file to emit (not including the file extension)
|
||||||
slug: ServerSlug
|
slug: ServerSlug
|
||||||
// the file extension
|
// the file extension
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
title: Authoring Content
|
title: Authoring Content
|
||||||
---
|
---
|
||||||
|
|
||||||
All of the content in your Quartz should go in the `/content` folder. The content for the home page of your Quartz lives in `content/index.md`. If you've [[index#🪴 Get Started|setup Quartz]] already, this folder should already be initialized. Any Markdown in this folder will get processed by Quartz.
|
All of the content in your Quartz should go in the `/content` folder. The content for the home page of your Quartz lives in `content/index.md`. If you've [[index#🪴 Get Started|setup Quartz]] already, this folder should already be initailized. Any Markdown in this folder will get processed by Quartz.
|
||||||
|
|
||||||
It is recommended that you use [Obsidian](https://obsidian.md/) as a way to edit and maintain your Quartz. It comes with a nice editor and graphical interface to preview, edit, and link your local files and attachments.
|
It is recommended that you use [Obsidian](https://obsidian.md/) as a way to edit and maintain your Quartz. It comes with a nice editor and graphical interface to preview, edit, and link your local files and attachments.
|
||||||
|
|
||||||
@@ -28,13 +28,21 @@ The rest of your content lives here. You can use **Markdown** here :)
|
|||||||
Some common frontmatter fields that are natively supported by Quartz:
|
Some common frontmatter fields that are natively supported by Quartz:
|
||||||
|
|
||||||
- `title`: Title of the page. If it isn't provided, Quartz will use the name of the file as the title.
|
- `title`: Title of the page. If it isn't provided, Quartz will use the name of the file as the title.
|
||||||
- `description`: Description of the page used for link previews.
|
|
||||||
- `aliases`: Other names for this note. This is a list of strings.
|
- `aliases`: Other names for this note. This is a list of strings.
|
||||||
- `tags`: Tags for this note.
|
|
||||||
- `draft`: Whether to publish the page or not. This is one way to make [[private pages|pages private]] in Quartz.
|
- `draft`: Whether to publish the page or not. This is one way to make [[private pages|pages private]] in Quartz.
|
||||||
- `date`: A string representing the day the note was published. Normally uses `YYYY-MM-DD` format.
|
- `date`: A string representing the day the note was published. Normally uses `YYYY-MM-DD` format.
|
||||||
|
|
||||||
## Syncing your Content
|
## Syncing your Content
|
||||||
|
|
||||||
When your Quartz is at a point you're happy with, you can save your changes to GitHub.
|
When your Quartz is at a point you're happy with, you can save your changes to GitHub by doing `npx quartz sync`.
|
||||||
First, make sure you've [[setting up your GitHub repository|already setup your GitHub repository]] and then do `npx quartz sync`.
|
|
||||||
|
> [!hint] Flags and options
|
||||||
|
> For full help options, you can run `npx quartz sync --help`.
|
||||||
|
>
|
||||||
|
> Most of these have sensible defaults but you can override them if you have a custom setup:
|
||||||
|
>
|
||||||
|
> - `-d` or `--directory`: the content folder. This is normally just `content`
|
||||||
|
> - `-v` or `--verbose`: print out extra logging information
|
||||||
|
> - `--commit` or `--no-commit`: whether to make a `git` commit for your changes
|
||||||
|
> - `--push` or `--no-push`: whether to push updates to your GitHub fork of Quartz
|
||||||
|
> - `--pull` or `--no-pull`: whether to try and pull in any updates from your GitHub fork (i.e. from other devices) before pushing
|
||||||
|
|||||||
@@ -25,9 +25,7 @@ Finally, Quartz also provides `Plugin.CrawlLinks` which allows you to customize
|
|||||||
- `callouts`: whether to enable [[callouts]]. Defaults to `true`
|
- `callouts`: whether to enable [[callouts]]. Defaults to `true`
|
||||||
- `mermaid`: whether to enable [[Mermaid diagrams]]. Defaults to `true`
|
- `mermaid`: whether to enable [[Mermaid diagrams]]. Defaults to `true`
|
||||||
- `parseTags`: whether to try and parse tags in the content body. Defaults to `true`
|
- `parseTags`: whether to try and parse tags in the content body. Defaults to `true`
|
||||||
- `parseArrows`: whether to try and parse arrows in the content body. Defaults to `true`.
|
|
||||||
- `enableInHtmlEmbed`: whether to try and parse Obsidian flavoured markdown in raw HTML. Defaults to `false`
|
- `enableInHtmlEmbed`: whether to try and parse Obsidian flavoured markdown in raw HTML. Defaults to `false`
|
||||||
- `enableYouTubeEmbed`: whether to enable embedded YouTube videos using external image Markdown syntax. Defaults to `false`
|
|
||||||
- Link resolution behaviour:
|
- Link resolution behaviour:
|
||||||
- Disabling: remove all instances of `Plugin.CrawlLinks()` from `quartz.config.ts`
|
- Disabling: remove all instances of `Plugin.CrawlLinks()` from `quartz.config.ts`
|
||||||
- Changing link resolution preference: set `markdownLinkResolution` to one of `absolute`, `relative` or `shortest`
|
- Changing link resolution preference: set `markdownLinkResolution` to one of `absolute`, `relative` or `shortest`
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Component.Breadcrumbs({
|
|||||||
rootName: "Home", // name of first/root element
|
rootName: "Home", // name of first/root element
|
||||||
resolveFrontmatterTitle: true, // whether to resolve folder names through frontmatter titles
|
resolveFrontmatterTitle: true, // whether to resolve folder names through frontmatter titles
|
||||||
hideOnRoot: true, // whether to hide breadcrumbs on root `index.md` page
|
hideOnRoot: true, // whether to hide breadcrumbs on root `index.md` page
|
||||||
showCurrentPage: true, // whether to display the current page in the breadcrumbs
|
showCurrentPage: true, // wether to display the current page in the breadcrumbs
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -24,24 +24,7 @@ See [documentation on supported types and syntax here](https://help.obsidian.md
|
|||||||
## Customization
|
## Customization
|
||||||
|
|
||||||
- Disable callouts: simply pass `callouts: false` to the plugin: `Plugin.ObsidianFlavoredMarkdown({ callouts: false })`
|
- Disable callouts: simply pass `callouts: false` to the plugin: `Plugin.ObsidianFlavoredMarkdown({ callouts: false })`
|
||||||
- Editing icons: `quartz/styles/callouts.scss`
|
- Editing icons: `quartz/plugins/transformers/ofm.ts`
|
||||||
|
|
||||||
### Add custom callouts
|
|
||||||
|
|
||||||
By default, custom callouts are handled by applying the `note` style. To make fancy ones, you have to add these lines to `custom.scss`.
|
|
||||||
|
|
||||||
```scss title="quartz/styles/custom.scss"
|
|
||||||
.callout {
|
|
||||||
&[data-callout="custom"] {
|
|
||||||
--color: #customcolor;
|
|
||||||
--border: #custombordercolor;
|
|
||||||
--bg: #custombg;
|
|
||||||
--callout-icon: url('data:image/svg+xml; utf8, <custom formatted svg>'); //SVG icon code
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!warning]
|
|
||||||
> Don't forget to ensure that the SVG is URL encoded before putting it in the CSS. You can use tools like [this one](https://yoksel.github.io/url-encoder/) to help you do that.
|
|
||||||
|
|
||||||
## Showcase
|
## Showcase
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,3 @@ Quartz supports darkmode out of the box that respects the user's theme preferenc
|
|||||||
- Component: `quartz/components/Darkmode.tsx`
|
- Component: `quartz/components/Darkmode.tsx`
|
||||||
- Style: `quartz/components/styles/darkmode.scss`
|
- Style: `quartz/components/styles/darkmode.scss`
|
||||||
- Script: `quartz/components/scripts/darkmode.inline.ts`
|
- Script: `quartz/components/scripts/darkmode.inline.ts`
|
||||||
|
|
||||||
You can also listen to the `themechange` event to perform any custom logic when the theme changes.
|
|
||||||
|
|
||||||
```js
|
|
||||||
document.addEventListener("themechange", (e) => {
|
|
||||||
console.log("Theme changed to " + e.detail.theme) // either "light" or "dark"
|
|
||||||
// your logic here
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Component.Explorer({
|
|||||||
title: "Explorer", // title of the explorer component
|
title: "Explorer", // title of the explorer component
|
||||||
folderClickBehavior: "collapse", // what happens when you click a folder ("link" to navigate to folder page on click or "collapse" to collapse folder on click)
|
folderClickBehavior: "collapse", // what happens when you click a folder ("link" to navigate to folder page on click or "collapse" to collapse folder on click)
|
||||||
folderDefaultState: "collapsed", // default state of folders ("collapsed" or "open")
|
folderDefaultState: "collapsed", // default state of folders ("collapsed" or "open")
|
||||||
useSavedState: true, // whether to use local storage to save "state" (which folders are opened) of explorer
|
useSavedState: true, // wether to use local storage to save "state" (which folders are opened) of explorer
|
||||||
// Sort order: folders first, then files. Sort folders and files alphabetically
|
// Sort order: folders first, then files. Sort folders and files alphabetically
|
||||||
sortFn: (a, b) => {
|
sortFn: (a, b) => {
|
||||||
... // default implementation shown later
|
... // default implementation shown later
|
||||||
@@ -179,34 +179,6 @@ 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:
|
||||||
@@ -244,63 +216,30 @@ 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.
|
||||||
|
|
||||||
### Use `sort` with pre-defined sort order
|
> [!tip]
|
||||||
|
> When writing more complicated functions, the `layout` file can start to look very cramped.
|
||||||
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.
|
> You can fix this by defining your functions in another file.
|
||||||
|
>
|
||||||
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).
|
> ```ts title="functions.ts"
|
||||||
|
> import { Options } from "./quartz/components/ExplorerNode"
|
||||||
```ts title="quartz.layout.ts"
|
> export const mapFn: Options["mapFn"] = (node) => {
|
||||||
Component.Explorer({
|
> // implement your function here
|
||||||
sortFn: (a, b) => {
|
> }
|
||||||
const nameOrderMap: Record<string, number> = {
|
> export const filterFn: Options["filterFn"] = (node) => {
|
||||||
"poetry-folder": 100,
|
> // implement your function here
|
||||||
"essay-folder": 200,
|
> }
|
||||||
"research-paper-file": 201,
|
> export const sortFn: Options["sortFn"] = (a, b) => {
|
||||||
"dinosaur-fossils-file": 300,
|
> // implement your function here
|
||||||
"other-folder": 400,
|
> }
|
||||||
}
|
> ```
|
||||||
|
>
|
||||||
let orderA = 0
|
> You can then import them like this:
|
||||||
let orderB = 0
|
>
|
||||||
|
> ```ts title="quartz.layout.ts"
|
||||||
if (a.file && a.file.slug) {
|
> import { mapFn, filterFn, sortFn } from "./functions.ts"
|
||||||
orderA = nameOrderMap[a.file.slug] || 0
|
> Component.Explorer({
|
||||||
} else if (a.name) {
|
> mapFn: mapFn,
|
||||||
orderA = nameOrderMap[a.name] || 0
|
> filterFn: filterFn,
|
||||||
}
|
> 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
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -225,6 +225,6 @@ pages:
|
|||||||
- public
|
- public
|
||||||
```
|
```
|
||||||
|
|
||||||
When `.gitlab-ci.yaml` is committed, 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`.
|
||||||
|
|||||||
@@ -23,11 +23,10 @@ This will guide you through initializing your Quartz with content. Once you've d
|
|||||||
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
|
||||||
5. Sync your changes with [[setting up your GitHub repository|GitHub]]
|
5. [[hosting|Host]] Quartz online
|
||||||
6. [[hosting|Host]] Quartz online
|
|
||||||
|
|
||||||
If you prefer instructions in a video format you can try following Nicole van der Hoeven's
|
> [!info]
|
||||||
[video guide on how to set up Quartz!](https://www.youtube.com/watch?v=6s6DT1yN4dw&t=227s)
|
> Coming from Quartz 3? See the [[migrating from Quartz 3|migration guide]] for the differences between Quartz 3 and Quartz 4 and how to migrate.
|
||||||
|
|
||||||
## 🔧 Features
|
## 🔧 Features
|
||||||
|
|
||||||
|
|||||||
@@ -15,34 +15,25 @@ At the top of your repository on GitHub.com's Quick Setup page, click the clipb
|
|||||||
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.
|
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
|
```bash
|
||||||
# list all the repositories that are tracked
|
# add your repository
|
||||||
git remote -v
|
git remote add origin REMOTE-URL
|
||||||
|
|
||||||
# if the origin doesn't match your own repository, set your repository as the origin
|
# track the main quartz repository for updates
|
||||||
git remote set-url origin REMOTE-URL
|
|
||||||
|
|
||||||
# if you don't have upstream as a remote, add it so updates work
|
|
||||||
git remote add upstream https://github.com/jackyzha0/quartz.git
|
git remote add upstream https://github.com/jackyzha0/quartz.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, you can sync the content to upload it to your repository. This is a helper command that will do the initial push of your content to your repository.
|
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
|
```bash
|
||||||
npx quartz sync --no-pull
|
npx quartz sync --no-pull
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!warning]- `fatal: --[no-]autostash option is only valid with --rebase`
|
> [!hint]
|
||||||
> You may have an outdated version of `git`. Updating `git` should fix this issue.
|
> 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.
|
||||||
In future updates, you can simply run `npx quartz sync` every time you want to push updates to your repository.
|
|
||||||
|
|
||||||
> [!hint] Flags and options
|
|
||||||
> For full help options, you can run `npx quartz sync --help`.
|
|
||||||
>
|
|
||||||
> Most of these have sensible defaults but you can override them if you have a custom setup:
|
|
||||||
>
|
|
||||||
> - `-d` or `--directory`: the content folder. This is normally just `content`
|
|
||||||
> - `-v` or `--verbose`: print out extra logging information
|
|
||||||
> - `--commit` or `--no-commit`: whether to make a `git` commit for your changes
|
|
||||||
> - `--push` or `--no-push`: whether to push updates to your GitHub fork of Quartz
|
|
||||||
> - `--pull` or `--no-pull`: whether to try and pull in any updates from your GitHub fork (i.e. from other devices) before pushing
|
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ 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/)
|
- [Socratica Toolbox](https://toolbox.socratica.info/)
|
||||||
|
- [Brandon Boswell's Garden](https://brandonkboswell.com)
|
||||||
|
- [Scaling Synthesis - A hypertext research notebook](https://scalingsynthesis.com/)
|
||||||
|
- [Data Dictionary 🧠](https://glossary.airbyte.com/)
|
||||||
|
- [sspaeti.com's Second Brain](https://brain.sspaeti.com/)
|
||||||
- [oldwinter の数字花园](https://garden.oldwinter.top/)
|
- [oldwinter の数字花园](https://garden.oldwinter.top/)
|
||||||
- [Aaron Pham's Garden](https://aarnphm.xyz/)
|
|
||||||
- [The Quantum Garden](https://quantumgardener.blog/)
|
|
||||||
- [Abhijeet's Math Wiki](https://abhmul.github.io/quartz/Math-Wiki/)
|
- [Abhijeet's Math Wiki](https://abhmul.github.io/quartz/Math-Wiki/)
|
||||||
|
- [Mike's AI Garden 🤖🪴](https://mwalton.me/)
|
||||||
- [Matt Dunn's Second Brain](https://mattdunn.info/)
|
- [Matt Dunn's Second Brain](https://mattdunn.info/)
|
||||||
- [Pelayo Arbues' Notes](https://pelayoarbues.github.io/)
|
- [Pelayo Arbues' Notes](https://pelayoarbues.github.io/)
|
||||||
- [Vince Imbat's Talahardin](https://vinceimbat.com/)
|
- [Vince Imbat's Talahardin](https://vinceimbat.com/)
|
||||||
@@ -18,12 +21,5 @@ 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/)
|
|
||||||
- [Sideny's 3D Artist's Handbook](https://sidney-eliot.github.io/3d-artists-handbook/)
|
|
||||||
- [Mike's AI Garden 🤖🪴](https://mwalton.me/)
|
|
||||||
- [Brandon Boswell's Garden](https://brandonkboswell.com)
|
|
||||||
- [Scaling Synthesis - A hypertext research notebook](https://scalingsynthesis.com/)
|
|
||||||
- [Data Dictionary 🧠](https://glossary.airbyte.com/)
|
|
||||||
- [sspaeti.com's Second Brain](https://brain.sspaeti.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)!
|
||||||
|
|||||||
2
globals.d.ts
vendored
2
globals.d.ts
vendored
@@ -4,7 +4,7 @@ export declare global {
|
|||||||
type: K,
|
type: K,
|
||||||
listener: (this: Document, ev: CustomEventMap[K]) => void,
|
listener: (this: Document, ev: CustomEventMap[K]) => void,
|
||||||
): void
|
): void
|
||||||
dispatchEvent<K extends keyof CustomEventMap>(ev: CustomEventMap[K] | UIEvent): void
|
dispatchEvent<K extends keyof CustomEventMap>(ev: CustomEventMap[K]): void
|
||||||
}
|
}
|
||||||
interface Window {
|
interface Window {
|
||||||
spaNavigate(url: URL, isBack: boolean = false)
|
spaNavigate(url: URL, isBack: boolean = false)
|
||||||
|
|||||||
1
index.d.ts
vendored
1
index.d.ts
vendored
@@ -6,7 +6,6 @@ declare module "*.scss" {
|
|||||||
// dom custom event
|
// dom custom event
|
||||||
interface CustomEventMap {
|
interface CustomEventMap {
|
||||||
nav: CustomEvent<{ url: FullSlug }>
|
nav: CustomEvent<{ url: FullSlug }>
|
||||||
themechange: CustomEvent<{ theme: "light" | "dark" }>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare const fetchData: Promise<ContentIndex>
|
declare const fetchData: Promise<ContentIndex>
|
||||||
|
|||||||
1177
package-lock.json
generated
1177
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
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.6",
|
"version": "4.1.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -35,15 +35,15 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.7.0",
|
"@clack/prompts": "^0.7.0",
|
||||||
"@floating-ui/dom": "^1.6.1",
|
"@floating-ui/dom": "^1.5.3",
|
||||||
"@napi-rs/simple-git": "0.1.14",
|
"@napi-rs/simple-git": "0.1.9",
|
||||||
"async-mutex": "^0.4.0",
|
"async-mutex": "^0.4.0",
|
||||||
"chalk": "^5.3.0",
|
"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.16.0",
|
"esbuild-sass-plugin": "^2.16.0",
|
||||||
"flexsearch": "0.7.43",
|
"flexsearch": "0.7.21",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"globby": "^14.0.0",
|
"globby": "^14.0.0",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
@@ -52,11 +52,12 @@
|
|||||||
"hast-util-to-string": "^3.0.0",
|
"hast-util-to-string": "^3.0.0",
|
||||||
"is-absolute-url": "^4.0.1",
|
"is-absolute-url": "^4.0.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lightningcss": "^1.23.0",
|
"lightningcss": "^1.22.1",
|
||||||
"mdast-util-find-and-replace": "^3.0.1",
|
"mdast-util-find-and-replace": "^3.0.1",
|
||||||
"mdast-util-to-hast": "^13.0.2",
|
"mdast-util-to-hast": "^13.0.2",
|
||||||
"mdast-util-to-string": "^4.0.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.19.3",
|
||||||
"preact-render-to-string": "^6.3.1",
|
"preact-render-to-string": "^6.3.1",
|
||||||
"pretty-bytes": "^6.1.1",
|
"pretty-bytes": "^6.1.1",
|
||||||
@@ -64,8 +65,8 @@
|
|||||||
"reading-time": "^1.5.0",
|
"reading-time": "^1.5.0",
|
||||||
"rehype-autolink-headings": "^7.1.0",
|
"rehype-autolink-headings": "^7.1.0",
|
||||||
"rehype-katex": "^7.0.0",
|
"rehype-katex": "^7.0.0",
|
||||||
"rehype-mathjax": "^6.0.0",
|
"rehype-mathjax": "^5.0.0",
|
||||||
"rehype-pretty-code": "^0.12.6",
|
"rehype-pretty-code": "^0.12.1",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"rehype-slug": "^6.0.0",
|
"rehype-slug": "^6.0.0",
|
||||||
"remark": "^15.0.1",
|
"remark": "^15.0.1",
|
||||||
@@ -74,35 +75,36 @@
|
|||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"remark-parse": "^11.0.0",
|
"remark-parse": "^11.0.0",
|
||||||
"remark-rehype": "^11.1.0",
|
"remark-rehype": "^11.0.0",
|
||||||
"remark-smartypants": "^2.0.0",
|
"remark-smartypants": "^2.0.0",
|
||||||
"rfdc": "^1.3.1",
|
|
||||||
"rimraf": "^5.0.5",
|
"rimraf": "^5.0.5",
|
||||||
"serve-handler": "^6.1.5",
|
"serve-handler": "^6.1.5",
|
||||||
"shikiji": "^0.10.2",
|
"shikiji": "^0.8.7",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"to-vfile": "^8.0.0",
|
"to-vfile": "^8.0.0",
|
||||||
"toml": "^3.0.0",
|
"toml": "^3.0.0",
|
||||||
"unified": "^11.0.4",
|
"unified": "^11.0.4",
|
||||||
"unist-util-visit": "^5.0.0",
|
"unist-util-visit": "^5.0.0",
|
||||||
"vfile": "^6.0.1",
|
"vfile": "^6.0.1",
|
||||||
"workerpool": "^9.1.0",
|
"workerpool": "^8.0.0",
|
||||||
"ws": "^8.15.1",
|
"ws": "^8.15.1",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cli-spinner": "^0.2.3",
|
"@types/cli-spinner": "^0.2.3",
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
|
"@types/flexsearch": "^0.7.3",
|
||||||
"@types/hast": "^3.0.3",
|
"@types/hast": "^3.0.3",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/node": "^20.11.11",
|
"@types/node": "^20.1.2",
|
||||||
"@types/pretty-time": "^1.1.5",
|
"@types/pretty-time": "^1.1.5",
|
||||||
"@types/source-map-support": "^0.5.10",
|
"@types/source-map-support": "^0.5.10",
|
||||||
|
"@types/workerpool": "^6.4.7",
|
||||||
"@types/ws": "^8.5.10",
|
"@types/ws": "^8.5.10",
|
||||||
"@types/yargs": "^17.0.32",
|
"@types/yargs": "^17.0.32",
|
||||||
"esbuild": "^0.19.9",
|
"esbuild": "^0.19.9",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.1.1",
|
||||||
"tsx": "^4.7.0",
|
"tsx": "^4.6.2",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,15 +47,13 @@ const config: QuartzConfig = {
|
|||||||
Plugin.FrontMatter(),
|
Plugin.FrontMatter(),
|
||||||
Plugin.TableOfContents(),
|
Plugin.TableOfContents(),
|
||||||
Plugin.CreatedModifiedDate({
|
Plugin.CreatedModifiedDate({
|
||||||
// you can add 'git' here for last modified from Git
|
priority: ["frontmatter", "filesystem"], // you can add 'git' here for last modified from Git but this makes the build slower
|
||||||
// if you do rely on git for dates, ensure defaultDateType is 'modified'
|
|
||||||
priority: ["frontmatter", "filesystem"],
|
|
||||||
}),
|
}),
|
||||||
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()],
|
||||||
|
|||||||
@@ -37,13 +37,12 @@ export const defaultContentPageLayout: PageLayout = {
|
|||||||
|
|
||||||
// components for pages that display lists of pages (e.g. tags or folders)
|
// components for pages that display lists of pages (e.g. tags or folders)
|
||||||
export const defaultListPageLayout: PageLayout = {
|
export const defaultListPageLayout: PageLayout = {
|
||||||
beforeBody: [Component.Breadcrumbs(), Component.ArticleTitle(), Component.ContentMeta()],
|
beforeBody: [Component.ArticleTitle()],
|
||||||
left: [
|
left: [
|
||||||
Component.PageTitle(),
|
Component.PageTitle(),
|
||||||
Component.MobileOnly(Component.Spacer()),
|
Component.MobileOnly(Component.Spacer()),
|
||||||
Component.Search(),
|
Component.Search(),
|
||||||
Component.Darkmode(),
|
Component.Darkmode(),
|
||||||
Component.DesktopOnly(Component.Explorer()),
|
|
||||||
],
|
],
|
||||||
right: [],
|
right: [],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ sourceMapSupport.install(options)
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { PerfTimer } from "./util/perf"
|
import { PerfTimer } from "./util/perf"
|
||||||
import { rimraf } from "rimraf"
|
import { rimraf } from "rimraf"
|
||||||
import { GlobbyFilterFunction, isGitIgnored } from "globby"
|
import { isGitIgnored } from "globby"
|
||||||
import chalk from "chalk"
|
import chalk from "chalk"
|
||||||
import { parseMarkdown } from "./processors/parse"
|
import { parseMarkdown } from "./processors/parse"
|
||||||
import { filterContent } from "./processors/filter"
|
import { filterContent } from "./processors/filter"
|
||||||
import { emitContent } from "./processors/emit"
|
import { emitContent } from "./processors/emit"
|
||||||
import cfg from "../quartz.config"
|
import cfg from "../quartz.config"
|
||||||
import { FilePath, FullSlug, joinSegments, slugifyFilePath } from "./util/path"
|
import { FilePath, joinSegments, slugifyFilePath } from "./util/path"
|
||||||
import chokidar from "chokidar"
|
import chokidar from "chokidar"
|
||||||
import { ProcessedContent } from "./plugins/vfile"
|
import { ProcessedContent } from "./plugins/vfile"
|
||||||
import { Argv, BuildCtx } from "./util/ctx"
|
import { Argv, BuildCtx } from "./util/ctx"
|
||||||
@@ -18,19 +18,6 @@ import { trace } from "./util/trace"
|
|||||||
import { options } from "./util/sourcemap"
|
import { options } from "./util/sourcemap"
|
||||||
import { Mutex } from "async-mutex"
|
import { Mutex } from "async-mutex"
|
||||||
|
|
||||||
type BuildData = {
|
|
||||||
ctx: BuildCtx
|
|
||||||
ignored: GlobbyFilterFunction
|
|
||||||
mut: Mutex
|
|
||||||
initialSlugs: FullSlug[]
|
|
||||||
// TODO merge contentMap and trackedAssets
|
|
||||||
contentMap: Map<FilePath, ProcessedContent>
|
|
||||||
trackedAssets: Set<FilePath>
|
|
||||||
toRebuild: Set<FilePath>
|
|
||||||
toRemove: Set<FilePath>
|
|
||||||
lastBuildMs: number
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
||||||
const ctx: BuildCtx = {
|
const ctx: BuildCtx = {
|
||||||
argv,
|
argv,
|
||||||
@@ -86,60 +73,19 @@ async function startServing(
|
|||||||
) {
|
) {
|
||||||
const { argv } = ctx
|
const { argv } = ctx
|
||||||
|
|
||||||
|
const ignored = await isGitIgnored()
|
||||||
const contentMap = new Map<FilePath, ProcessedContent>()
|
const contentMap = new Map<FilePath, ProcessedContent>()
|
||||||
for (const content of initialContent) {
|
for (const content of initialContent) {
|
||||||
const [_tree, vfile] = content
|
const [_tree, vfile] = content
|
||||||
contentMap.set(vfile.data.filePath!, content)
|
contentMap.set(vfile.data.filePath!, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildData: BuildData = {
|
const initialSlugs = ctx.allSlugs
|
||||||
ctx,
|
let lastBuildMs = 0
|
||||||
mut,
|
const toRebuild: Set<FilePath> = new Set()
|
||||||
contentMap,
|
const toRemove: Set<FilePath> = new Set()
|
||||||
ignored: await isGitIgnored(),
|
const trackedAssets: Set<FilePath> = new Set()
|
||||||
initialSlugs: ctx.allSlugs,
|
async function rebuild(fp: string, action: "add" | "change" | "delete") {
|
||||||
toRebuild: new Set<FilePath>(),
|
|
||||||
toRemove: new Set<FilePath>(),
|
|
||||||
trackedAssets: new Set<FilePath>(),
|
|
||||||
lastBuildMs: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
const watcher = chokidar.watch(".", {
|
|
||||||
persistent: true,
|
|
||||||
cwd: argv.directory,
|
|
||||||
ignoreInitial: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
watcher
|
|
||||||
.on("add", (fp) => rebuildFromEntrypoint(fp, "add", clientRefresh, buildData))
|
|
||||||
.on("change", (fp) => rebuildFromEntrypoint(fp, "change", clientRefresh, buildData))
|
|
||||||
.on("unlink", (fp) => rebuildFromEntrypoint(fp, "delete", clientRefresh, buildData))
|
|
||||||
|
|
||||||
return async () => {
|
|
||||||
await watcher.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rebuildFromEntrypoint(
|
|
||||||
fp: string,
|
|
||||||
action: "add" | "change" | "delete",
|
|
||||||
clientRefresh: () => void,
|
|
||||||
buildData: BuildData, // note: this function mutates buildData
|
|
||||||
) {
|
|
||||||
const {
|
|
||||||
ctx,
|
|
||||||
ignored,
|
|
||||||
mut,
|
|
||||||
initialSlugs,
|
|
||||||
contentMap,
|
|
||||||
toRebuild,
|
|
||||||
toRemove,
|
|
||||||
trackedAssets,
|
|
||||||
lastBuildMs,
|
|
||||||
} = buildData
|
|
||||||
|
|
||||||
const { argv } = ctx
|
|
||||||
|
|
||||||
// don't do anything for gitignored files
|
// don't do anything for gitignored files
|
||||||
if (ignored(fp)) {
|
if (ignored(fp)) {
|
||||||
return
|
return
|
||||||
@@ -167,7 +113,7 @@ async function rebuildFromEntrypoint(
|
|||||||
// debounce rebuilds every 250ms
|
// debounce rebuilds every 250ms
|
||||||
|
|
||||||
const buildStart = new Date().getTime()
|
const buildStart = new Date().getTime()
|
||||||
buildData.lastBuildMs = buildStart
|
lastBuildMs = buildStart
|
||||||
const release = await mut.acquire()
|
const release = await mut.acquire()
|
||||||
if (lastBuildMs > buildStart) {
|
if (lastBuildMs > buildStart) {
|
||||||
release()
|
release()
|
||||||
@@ -202,11 +148,8 @@ async function rebuildFromEntrypoint(
|
|||||||
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 (err) {
|
} catch {
|
||||||
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()
|
release()
|
||||||
@@ -215,6 +158,22 @@ async function rebuildFromEntrypoint(
|
|||||||
toRemove.clear()
|
toRemove.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const watcher = chokidar.watch(".", {
|
||||||
|
persistent: true,
|
||||||
|
cwd: argv.directory,
|
||||||
|
ignoreInitial: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
watcher
|
||||||
|
.on("add", (fp) => rebuild(fp, "add"))
|
||||||
|
.on("change", (fp) => rebuild(fp, "change"))
|
||||||
|
.on("unlink", (fp) => rebuild(fp, "delete"))
|
||||||
|
|
||||||
|
return async () => {
|
||||||
|
await watcher.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => {
|
export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => {
|
||||||
try {
|
try {
|
||||||
return await buildQuartz(argv, mut, clientRefresh)
|
return await buildQuartz(argv, mut, clientRefresh)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export type Analytics =
|
|||||||
| null
|
| null
|
||||||
| {
|
| {
|
||||||
provider: "plausible"
|
provider: "plausible"
|
||||||
host?: string
|
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
provider: "google"
|
provider: "google"
|
||||||
@@ -16,7 +15,6 @@ export type Analytics =
|
|||||||
| {
|
| {
|
||||||
provider: "umami"
|
provider: "umami"
|
||||||
websiteId: string
|
websiteId: string
|
||||||
host?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GlobalConfiguration {
|
export interface GlobalConfiguration {
|
||||||
@@ -36,12 +34,6 @@ export interface GlobalConfiguration {
|
|||||||
*/
|
*/
|
||||||
baseUrl?: string
|
baseUrl?: string
|
||||||
theme: Theme
|
theme: Theme
|
||||||
/**
|
|
||||||
* The locale to use for date formatting. Default to "en-US"
|
|
||||||
* Allow to translate the date in the language of your choice.
|
|
||||||
* Need to be formated following the IETF language tag format (https://en.wikipedia.org/wiki/IETF_language_tag)
|
|
||||||
*/
|
|
||||||
locale?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuartzConfig {
|
export interface QuartzConfig {
|
||||||
|
|||||||
@@ -113,10 +113,7 @@ export async function handleCreate(argv) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const gitkeepPath = path.join(contentFolder, ".gitkeep")
|
await fs.promises.unlink(path.join(contentFolder, ".gitkeep"))
|
||||||
if (fs.existsSync(gitkeepPath)) {
|
|
||||||
await fs.promises.unlink(gitkeepPath)
|
|
||||||
}
|
|
||||||
if (setupStrategy === "copy" || setupStrategy === "symlink") {
|
if (setupStrategy === "copy" || setupStrategy === "symlink") {
|
||||||
let originalFolder = sourceDirectory
|
let originalFolder = sourceDirectory
|
||||||
|
|
||||||
@@ -168,20 +165,22 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
|
|||||||
// get a preferred link resolution strategy
|
// get a preferred link resolution strategy
|
||||||
linkResolutionStrategy = exitIfCancel(
|
linkResolutionStrategy = exitIfCancel(
|
||||||
await select({
|
await select({
|
||||||
message: `Choose how Quartz should resolve links in your content. This should match Obsidian's link format. You can change this later in \`quartz.config.ts\`.`,
|
message: `Choose how Quartz should resolve links in your content. You can change this later in \`quartz.config.ts\`.`,
|
||||||
options: [
|
options: [
|
||||||
{
|
|
||||||
value: "shortest",
|
|
||||||
label: "Treat links as shortest path",
|
|
||||||
hint: "(default)",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
value: "absolute",
|
value: "absolute",
|
||||||
label: "Treat links as absolute path",
|
label: "Treat links as absolute path",
|
||||||
|
hint: "for content made for Quartz 3 and Hugo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "shortest",
|
||||||
|
label: "Treat links as shortest path",
|
||||||
|
hint: "for most Obsidian vaults",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "relative",
|
value: "relative",
|
||||||
label: "Treat links as relative paths",
|
label: "Treat links as relative paths",
|
||||||
|
hint: "for just normal Markdown files",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -200,7 +199,6 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
|
|||||||
// setup remote
|
// setup remote
|
||||||
execSync(
|
execSync(
|
||||||
`git remote show upstream || git remote add upstream https://github.com/jackyzha0/quartz.git`,
|
`git remote show upstream || git remote add upstream https://github.com/jackyzha0/quartz.git`,
|
||||||
{ stdio: "ignore" },
|
|
||||||
)
|
)
|
||||||
|
|
||||||
outro(`You're all set! Not sure what to do next? Try:
|
outro(`You're all set! Not sure what to do next? Try:
|
||||||
@@ -257,7 +255,6 @@ export async function handleBuild(argv) {
|
|||||||
},
|
},
|
||||||
write: false,
|
write: false,
|
||||||
bundle: true,
|
bundle: true,
|
||||||
minify: true,
|
|
||||||
platform: "browser",
|
platform: "browser",
|
||||||
format: "esm",
|
format: "esm",
|
||||||
})
|
})
|
||||||
@@ -347,7 +344,7 @@ export async function handleBuild(argv) {
|
|||||||
directoryListing: false,
|
directoryListing: false,
|
||||||
headers: [
|
headers: [
|
||||||
{
|
{
|
||||||
source: "**/*.*",
|
source: "**/*.html",
|
||||||
headers: [{ key: "Content-Disposition", value: "inline" }],
|
headers: [{ key: "Content-Disposition", value: "inline" }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -450,7 +447,7 @@ export async function handleUpdate(argv) {
|
|||||||
try {
|
try {
|
||||||
gitPull(UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH)
|
gitPull(UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH)
|
||||||
} catch {
|
} catch {
|
||||||
console.log(chalk.red("An error occurred above while pulling updates."))
|
console.log(chalk.red("An error occured above while pulling updates."))
|
||||||
await popContentFolder(contentFolder)
|
await popContentFolder(contentFolder)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -522,7 +519,7 @@ export async function handleSync(argv) {
|
|||||||
try {
|
try {
|
||||||
gitPull(ORIGIN_NAME, QUARTZ_SOURCE_BRANCH)
|
gitPull(ORIGIN_NAME, QUARTZ_SOURCE_BRANCH)
|
||||||
} catch {
|
} catch {
|
||||||
console.log(chalk.red("An error occurred above while pulling updates."))
|
console.log(chalk.red("An error occured above while pulling updates."))
|
||||||
await popContentFolder(contentFolder)
|
await popContentFolder(contentFolder)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
|
|
||||||
function ArticleTitle({ fileData, displayClass }: QuartzComponentProps) {
|
function ArticleTitle({ fileData, displayClass }: QuartzComponentProps) {
|
||||||
const title = fileData.frontmatter?.title
|
const title = fileData.frontmatter?.title
|
||||||
if (title) {
|
if (title) {
|
||||||
return <h1 class={classNames(displayClass, "article-title")}>{title}</h1>
|
return <h1 class={`article-title ${displayClass ?? ""}`}>{title}</h1>
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import style from "./styles/backlinks.scss"
|
import style from "./styles/backlinks.scss"
|
||||||
import { resolveRelative, simplifySlug } from "../util/path"
|
import { resolveRelative, simplifySlug } from "../util/path"
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
|
|
||||||
function Backlinks({ fileData, allFiles, displayClass }: QuartzComponentProps) {
|
function Backlinks({ fileData, allFiles, displayClass }: QuartzComponentProps) {
|
||||||
const slug = simplifySlug(fileData.slug!)
|
const slug = simplifySlug(fileData.slug!)
|
||||||
const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
|
const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "backlinks")}>
|
<div class={`backlinks ${displayClass ?? ""}`}>
|
||||||
<h3>Backlinks</h3>
|
<h3>Backlinks</h3>
|
||||||
<ul class="overflow">
|
<ul class="overflow">
|
||||||
{backlinkFiles.length > 0 ? (
|
{backlinkFiles.length > 0 ? (
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
|||||||
import breadcrumbsStyle from "./styles/breadcrumbs.scss"
|
import breadcrumbsStyle from "./styles/breadcrumbs.scss"
|
||||||
import { FullSlug, SimpleSlug, resolveRelative } from "../util/path"
|
import { FullSlug, SimpleSlug, resolveRelative } from "../util/path"
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
|
|
||||||
type CrumbData = {
|
type CrumbData = {
|
||||||
displayName: string
|
displayName: string
|
||||||
@@ -19,15 +18,15 @@ interface BreadcrumbOptions {
|
|||||||
*/
|
*/
|
||||||
rootName: string
|
rootName: string
|
||||||
/**
|
/**
|
||||||
* Whether to look up frontmatter title for folders (could cause performance problems with big vaults)
|
* wether to look up frontmatter title for folders (could cause performance problems with big vaults)
|
||||||
*/
|
*/
|
||||||
resolveFrontmatterTitle: boolean
|
resolveFrontmatterTitle: boolean
|
||||||
/**
|
/**
|
||||||
* Whether to display breadcrumbs on root `index.md`
|
* Wether to display breadcrumbs on root `index.md`
|
||||||
*/
|
*/
|
||||||
hideOnRoot: boolean
|
hideOnRoot: boolean
|
||||||
/**
|
/**
|
||||||
* Whether to display the current page in the breadcrumbs.
|
* Wether to display the current page in the breadcrumbs.
|
||||||
*/
|
*/
|
||||||
showCurrentPage: boolean
|
showCurrentPage: boolean
|
||||||
}
|
}
|
||||||
@@ -69,10 +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.slug?.split("/")
|
const folderParts = file.filePath?.split("/")
|
||||||
// 2nd last to exclude the /index
|
if (folderParts) {
|
||||||
const folderName = folderParts?.at(-2)
|
const folderName = folderParts[folderParts?.length - 2]
|
||||||
if (folderName) {
|
|
||||||
folderIndex.set(folderName, file)
|
folderIndex.set(folderName, file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,10 +88,7 @@ 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) {
|
||||||
const title = currentFile.frontmatter!.title
|
curPathSegment = currentFile.frontmatter!.title
|
||||||
if (title !== "index") {
|
|
||||||
curPathSegment = title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add current slug to full path
|
// Add current slug to full path
|
||||||
@@ -105,16 +100,15 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add current file to crumb (can directly use frontmatter title)
|
// Add current file to crumb (can directly use frontmatter title)
|
||||||
if (options.showCurrentPage && slugParts.at(-1) !== "index") {
|
if (options.showCurrentPage) {
|
||||||
crumbs.push({
|
crumbs.push({
|
||||||
displayName: fileData.frontmatter!.title,
|
displayName: fileData.frontmatter!.title,
|
||||||
path: "",
|
path: "",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav class={classNames(displayClass, "breadcrumb-container")} aria-label="breadcrumbs">
|
<nav class={`breadcrumb-container ${displayClass ?? ""}`} aria-label="breadcrumbs">
|
||||||
{crumbs.map((crumb, index) => (
|
{crumbs.map((crumb, index) => (
|
||||||
<div class="breadcrumb-element">
|
<div class="breadcrumb-element">
|
||||||
<a href={crumb.path}>{crumb.displayName}</a>
|
<a href={crumb.path}>{crumb.displayName}</a>
|
||||||
|
|||||||
@@ -1,40 +1,20 @@
|
|||||||
import { formatDate, getDate } from "./Date"
|
import { formatDate, getDate } from "./Date"
|
||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import readingTime from "reading-time"
|
import readingTime from "reading-time"
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
|
|
||||||
interface ContentMetaOptions {
|
|
||||||
/**
|
|
||||||
* Whether to display reading time
|
|
||||||
*/
|
|
||||||
showReadingTime: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultOptions: ContentMetaOptions = {
|
|
||||||
showReadingTime: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ((opts?: Partial<ContentMetaOptions>) => {
|
|
||||||
// Merge options with defaults
|
|
||||||
const options: ContentMetaOptions = { ...defaultOptions, ...opts }
|
|
||||||
|
|
||||||
|
export default (() => {
|
||||||
function ContentMetadata({ cfg, fileData, displayClass }: QuartzComponentProps) {
|
function ContentMetadata({ cfg, fileData, displayClass }: QuartzComponentProps) {
|
||||||
const text = fileData.text
|
const text = fileData.text
|
||||||
|
|
||||||
if (text) {
|
if (text) {
|
||||||
const segments: string[] = []
|
const segments: string[] = []
|
||||||
|
const { text: timeTaken, words: _words } = readingTime(text)
|
||||||
|
|
||||||
if (fileData.dates) {
|
if (fileData.dates) {
|
||||||
segments.push(formatDate(getDate(cfg, fileData)!, cfg.locale))
|
segments.push(formatDate(getDate(cfg, fileData)!))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display reading time if enabled
|
|
||||||
if (options.showReadingTime) {
|
|
||||||
const { text: timeTaken, words: _words } = readingTime(text)
|
|
||||||
segments.push(timeTaken)
|
segments.push(timeTaken)
|
||||||
}
|
return <p class={`content-meta ${displayClass ?? ""}`}>{segments.join(", ")}</p>
|
||||||
|
|
||||||
return <p class={classNames(displayClass, "content-meta")}>{segments.join(", ")}</p>
|
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,10 @@
|
|||||||
import darkmodeScript from "./scripts/darkmode.inline"
|
import darkmodeScript from "./scripts/darkmode.inline"
|
||||||
import styles from "./styles/darkmode.scss"
|
import styles from "./styles/darkmode.scss"
|
||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
|
|
||||||
function Darkmode({ displayClass }: QuartzComponentProps) {
|
function Darkmode({ displayClass }: QuartzComponentProps) {
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "darkmode")}>
|
<div class={`darkmode ${displayClass ?? ""}`}>
|
||||||
<input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} />
|
<input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} />
|
||||||
<label id="toggle-label-light" for="darkmode-toggle" tabIndex={-1}>
|
<label id="toggle-label-light" for="darkmode-toggle" tabIndex={-1}>
|
||||||
<svg
|
<svg
|
||||||
@@ -19,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>
|
||||||
@@ -35,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>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { QuartzPluginData } from "../plugins/vfile"
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
date: Date
|
date: Date
|
||||||
locale?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ValidDateType = keyof Required<QuartzPluginData>["dates"]
|
export type ValidDateType = keyof Required<QuartzPluginData>["dates"]
|
||||||
@@ -17,14 +16,14 @@ export function getDate(cfg: GlobalConfiguration, data: QuartzPluginData): Date
|
|||||||
return data.dates?.[cfg.defaultDateType]
|
return data.dates?.[cfg.defaultDateType]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDate(d: Date, locale = "en-US"): string {
|
export function formatDate(d: Date): string {
|
||||||
return d.toLocaleDateString(locale, {
|
return d.toLocaleDateString("en-US", {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Date({ date, locale }: Props) {
|
export function Date({ date }: Props) {
|
||||||
return <>{formatDate(date, locale)}</>
|
return <>{formatDate(date)}</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import explorerStyle from "./styles/explorer.scss"
|
|||||||
import script from "./scripts/explorer.inline"
|
import script from "./scripts/explorer.inline"
|
||||||
import { ExplorerNode, FileNode, Options } from "./ExplorerNode"
|
import { ExplorerNode, FileNode, Options } from "./ExplorerNode"
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
|
|
||||||
// Options interface defined in `ExplorerNode` to avoid circular dependency
|
// Options interface defined in `ExplorerNode` to avoid circular dependency
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
@@ -13,9 +12,6 @@ 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)) {
|
||||||
@@ -26,7 +22,6 @@ const defaultOptions = {
|
|||||||
sensitivity: "base",
|
sensitivity: "base",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (a.file && !b.file) {
|
if (a.file && !b.file) {
|
||||||
return 1
|
return 1
|
||||||
} else {
|
} else {
|
||||||
@@ -46,39 +41,52 @@ 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))
|
allFiles.forEach((file) => fileTree.add(file, 1))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keys of this object must match corresponding function name of `FileNode`,
|
||||||
|
* while values must be the argument that will be passed to the function.
|
||||||
|
*
|
||||||
|
* e.g. entry for FileNode.sort: `sort: opts.sortFn` (value is sort function from options)
|
||||||
|
*/
|
||||||
|
const functions = {
|
||||||
|
map: opts.mapFn,
|
||||||
|
sort: opts.sortFn,
|
||||||
|
filter: opts.filterFn,
|
||||||
|
}
|
||||||
|
|
||||||
// Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied)
|
// 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 (functionName === "map") {
|
if (functions[functionName]) {
|
||||||
fileTree.map(opts.mapFn)
|
// for every entry in order, call matching function in FileNode and pass matching argument
|
||||||
} else if (functionName === "sort") {
|
// e.g. i = 0; functionName = "filter"
|
||||||
fileTree.sort(opts.sortFn)
|
// converted to: (if opts.filterFn) => fileTree.filter(opts.filterFn)
|
||||||
} else if (functionName === "filter") {
|
|
||||||
fileTree.filter(opts.filterFn)
|
// @ts-ignore
|
||||||
|
// typescript cant statically check these dynamic references, so manually make sure reference is valid and ignore warning
|
||||||
|
fileTree[functionName].call(fileTree, functions[functionName])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all folders of tree. Initialize with collapsed state
|
// Get all folders of tree. Initialize with collapsed state
|
||||||
// Stringify to pass json tree as data attribute ([data-tree])
|
|
||||||
const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed")
|
const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed")
|
||||||
|
|
||||||
|
// 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)
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "explorer")}>
|
<div class={`explorer ${displayClass ?? ""}`}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
id="explorer"
|
id="explorer"
|
||||||
@@ -112,7 +120,6 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Explorer.css = explorerStyle
|
Explorer.css = explorerStyle
|
||||||
Explorer.afterDOMLoaded = script
|
Explorer.afterDOMLoaded = script
|
||||||
return Explorer
|
return Explorer
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
import {
|
import { resolveRelative } from "../util/path"
|
||||||
joinSegments,
|
|
||||||
resolveRelative,
|
|
||||||
clone,
|
|
||||||
simplifySlug,
|
|
||||||
SimpleSlug,
|
|
||||||
FilePath,
|
|
||||||
} from "../util/path"
|
|
||||||
|
|
||||||
type OrderEntries = "sort" | "filter" | "map"
|
type OrderEntries = "sort" | "filter" | "map"
|
||||||
|
|
||||||
@@ -17,9 +10,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 = {
|
||||||
@@ -32,74 +25,59 @@ 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: Array<FileNode>
|
children: FileNode[]
|
||||||
name: string // this is the slug segment
|
name: string
|
||||||
displayName: string
|
displayName: string
|
||||||
file: QuartzPluginData | null
|
file: QuartzPluginData | null
|
||||||
depth: number
|
depth: number
|
||||||
|
|
||||||
constructor(slugSegment: string, displayName?: string, file?: QuartzPluginData, depth?: number) {
|
constructor(name: string, file?: QuartzPluginData, depth?: number) {
|
||||||
this.children = []
|
this.children = []
|
||||||
this.name = slugSegment
|
this.name = name
|
||||||
this.displayName = displayName ?? file?.frontmatter?.title ?? slugSegment
|
this.displayName = name
|
||||||
this.file = file ? clone(file) : null
|
this.file = file ? structuredClone(file) : null
|
||||||
this.depth = depth ?? 0
|
this.depth = depth ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private insert(fileData: DataWrapper) {
|
private insert(file: DataWrapper) {
|
||||||
if (fileData.path.length === 0) {
|
if (file.path.length === 1) {
|
||||||
return
|
if (file.path[0] !== "index.md") {
|
||||||
}
|
this.children.push(new FileNode(file.file.frontmatter!.title, file.file, this.depth + 1))
|
||||||
|
} else {
|
||||||
const nextSegment = fileData.path[0]
|
const title = file.file.frontmatter?.title
|
||||||
|
if (title && title !== "index" && file.path[0] === "index.md") {
|
||||||
// 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
|
this.displayName = title
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// direct child
|
const next = file.path[0]
|
||||||
this.children.push(new FileNode(nextSegment, undefined, fileData.file, this.depth + 1))
|
file.path = file.path.splice(1)
|
||||||
}
|
for (const child of this.children) {
|
||||||
|
if (child.name === next) {
|
||||||
|
child.insert(file)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the right child to insert into
|
|
||||||
fileData.path = fileData.path.splice(1)
|
|
||||||
const child = this.children.find((c) => c.name === nextSegment)
|
|
||||||
if (child) {
|
|
||||||
child.insert(fileData)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const newChild = new FileNode(
|
const newChild = new FileNode(next, undefined, this.depth + 1)
|
||||||
nextSegment,
|
newChild.insert(file)
|
||||||
getPathSegment(fileData.file.relativePath, this.depth),
|
|
||||||
undefined,
|
|
||||||
this.depth + 1,
|
|
||||||
)
|
|
||||||
newChild.insert(fileData)
|
|
||||||
this.children.push(newChild)
|
this.children.push(newChild)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add new file to tree
|
// Add new file to tree
|
||||||
add(file: QuartzPluginData) {
|
add(file: QuartzPluginData, splice: number = 0) {
|
||||||
this.insert({ file: file, path: simplifySlug(file.slug!).split("/") })
|
this.insert({ file, path: file.filePath!.split("/").splice(splice) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print tree structure (for debugging)
|
||||||
|
print(depth: number = 0) {
|
||||||
|
let folderChar = ""
|
||||||
|
if (!this.file) folderChar = "|"
|
||||||
|
console.log("-".repeat(depth), folderChar, this.name, this.depth)
|
||||||
|
this.children.forEach((e) => e.print(depth + 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,6 +95,7 @@ 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))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,16 +110,16 @@ export class FileNode {
|
|||||||
|
|
||||||
const traverse = (node: FileNode, currentPath: string) => {
|
const traverse = (node: FileNode, currentPath: string) => {
|
||||||
if (!node.file) {
|
if (!node.file) {
|
||||||
const folderPath = joinSegments(currentPath, node.name)
|
const folderPath = currentPath + (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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,13 +147,14 @@ 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 = joinSegments(fullPath ?? "", node.name)
|
folderPath = `${pathOld}/${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}>
|
||||||
@@ -183,7 +163,7 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
) : (
|
) : (
|
||||||
<li>
|
<div>
|
||||||
{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
|
||||||
@@ -205,16 +185,12 @@ 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
|
<a href={`${folderPath}`} data-for={node.name} class="folder-title">
|
||||||
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">
|
||||||
<span class="folder-title">{node.displayName}</span>
|
<p class="folder-title">{node.displayName}</p>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -241,8 +217,8 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</li>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import script from "./scripts/graph.inline"
|
import script from "./scripts/graph.inline"
|
||||||
import style from "./styles/graph.scss"
|
import style from "./styles/graph.scss"
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
|
|
||||||
export interface D3Config {
|
export interface D3Config {
|
||||||
drag: boolean
|
drag: boolean
|
||||||
@@ -57,7 +56,7 @@ export default ((opts?: GraphOptions) => {
|
|||||||
const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph }
|
const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph }
|
||||||
const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph }
|
const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph }
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "graph")}>
|
<div class={`graph ${displayClass ?? ""}`}>
|
||||||
<h3>Graph View</h3>
|
<h3>Graph View</h3>
|
||||||
<div class="graph-outer">
|
<div class="graph-outer">
|
||||||
<div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
|
<div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function PageList({ cfg, fileData, allFiles, limit }: Props) {
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
{page.dates && (
|
{page.dates && (
|
||||||
<p class="meta">
|
<p class="meta">
|
||||||
<Date date={getDate(cfg, page)!} locale={cfg.locale} />
|
<Date date={getDate(cfg, page)!} />
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div class="desc">
|
<div class="desc">
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { pathToRoot } from "../util/path"
|
import { pathToRoot } from "../util/path"
|
||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
|
|
||||||
function PageTitle({ fileData, cfg, displayClass }: QuartzComponentProps) {
|
function PageTitle({ fileData, cfg, displayClass }: QuartzComponentProps) {
|
||||||
const title = cfg?.pageTitle ?? "Untitled Quartz"
|
const title = cfg?.pageTitle ?? "Untitled Quartz"
|
||||||
const baseDir = pathToRoot(fileData.slug!)
|
const baseDir = pathToRoot(fileData.slug!)
|
||||||
return (
|
return (
|
||||||
<h1 class={classNames(displayClass, "page-title")}>
|
<h1 class={`page-title ${displayClass ?? ""}`}>
|
||||||
<a href={baseDir}>{title}</a>
|
<a href={baseDir}>{title}</a>
|
||||||
</h1>
|
</h1>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { byDateAndAlphabetical } from "./PageList"
|
|||||||
import style from "./styles/recentNotes.scss"
|
import style from "./styles/recentNotes.scss"
|
||||||
import { Date, getDate } from "./Date"
|
import { Date, getDate } from "./Date"
|
||||||
import { GlobalConfiguration } from "../cfg"
|
import { GlobalConfiguration } from "../cfg"
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
title: string
|
title: string
|
||||||
@@ -29,7 +28,7 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
const pages = allFiles.filter(opts.filter).sort(opts.sort)
|
const pages = allFiles.filter(opts.filter).sort(opts.sort)
|
||||||
const remaining = Math.max(0, pages.length - opts.limit)
|
const remaining = Math.max(0, pages.length - opts.limit)
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "recent-notes")}>
|
<div class={`recent-notes ${displayClass ?? ""}`}>
|
||||||
<h3>{opts.title}</h3>
|
<h3>{opts.title}</h3>
|
||||||
<ul class="recent-ul">
|
<ul class="recent-ul">
|
||||||
{pages.slice(0, opts.limit).map((page) => {
|
{pages.slice(0, opts.limit).map((page) => {
|
||||||
@@ -48,7 +47,7 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
</div>
|
</div>
|
||||||
{page.dates && (
|
{page.dates && (
|
||||||
<p class="meta">
|
<p class="meta">
|
||||||
<Date date={getDate(cfg, page)!} locale={cfg.locale} />
|
<Date date={getDate(cfg, page)!} />
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<ul class="tags">
|
<ul class="tags">
|
||||||
|
|||||||
@@ -2,22 +2,11 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
|||||||
import style from "./styles/search.scss"
|
import style from "./styles/search.scss"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import script from "./scripts/search.inline"
|
import script from "./scripts/search.inline"
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
|
|
||||||
export interface SearchOptions {
|
export default (() => {
|
||||||
enablePreview: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultOptions: SearchOptions = {
|
|
||||||
enablePreview: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ((userOpts?: Partial<SearchOptions>) => {
|
|
||||||
function Search({ displayClass }: QuartzComponentProps) {
|
function Search({ displayClass }: QuartzComponentProps) {
|
||||||
const opts = { ...defaultOptions, ...userOpts }
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "search")}>
|
<div class={`search ${displayClass ?? ""}`}>
|
||||||
<div id="search-icon">
|
<div id="search-icon">
|
||||||
<p>Search</p>
|
<p>Search</p>
|
||||||
<div></div>
|
<div></div>
|
||||||
@@ -46,7 +35,7 @@ export default ((userOpts?: Partial<SearchOptions>) => {
|
|||||||
aria-label="Search for something"
|
aria-label="Search for something"
|
||||||
placeholder="Search for something"
|
placeholder="Search for something"
|
||||||
/>
|
/>
|
||||||
<div id="search-layout" data-preview={opts.enablePreview}></div>
|
<div id="results-container"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
|
|
||||||
function Spacer({ displayClass }: QuartzComponentProps) {
|
function Spacer({ displayClass }: QuartzComponentProps) {
|
||||||
return <div class={classNames(displayClass, "spacer")}></div>
|
return <div class={`spacer ${displayClass ?? ""}`}></div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (() => Spacer) satisfies QuartzComponentConstructor
|
export default (() => Spacer) satisfies QuartzComponentConstructor
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import legacyStyle from "./styles/legacyToc.scss"
|
import legacyStyle from "./styles/legacyToc.scss"
|
||||||
import modernStyle from "./styles/toc.scss"
|
import modernStyle from "./styles/toc.scss"
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import script from "./scripts/toc.inline"
|
import script from "./scripts/toc.inline"
|
||||||
@@ -20,7 +19,7 @@ function TableOfContents({ fileData, displayClass }: QuartzComponentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "toc")}>
|
<div class={`toc ${displayClass ?? ""}`}>
|
||||||
<button type="button" id="toc" class={fileData.collapseToc ? "collapsed" : ""}>
|
<button type="button" id="toc" class={fileData.collapseToc ? "collapsed" : ""}>
|
||||||
<h3>Table of Contents</h3>
|
<h3>Table of Contents</h3>
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { pathToRoot, slugTag } from "../util/path"
|
import { pathToRoot, slugTag } from "../util/path"
|
||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
|
|
||||||
function TagList({ fileData, displayClass }: QuartzComponentProps) {
|
function TagList({ fileData, displayClass }: QuartzComponentProps) {
|
||||||
const tags = fileData.frontmatter?.tags
|
const tags = fileData.frontmatter?.tags
|
||||||
const baseDir = pathToRoot(fileData.slug!)
|
const baseDir = pathToRoot(fileData.slug!)
|
||||||
if (tags && tags.length > 0) {
|
if (tags && tags.length > 0) {
|
||||||
return (
|
return (
|
||||||
<ul class={classNames(displayClass, "tags")}>
|
<ul class={`tags ${displayClass ?? ""}`}>
|
||||||
{tags.map((tag) => {
|
{tags.map((tag) => {
|
||||||
const display = `#${tag}`
|
const display = `#${tag}`
|
||||||
const linkDest = baseDir + `/tags/${slugTag(tag)}`
|
const linkDest = baseDir + `/tags/${slugTag(tag)}`
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
|||||||
|
|
||||||
function Content({ fileData, tree }: QuartzComponentProps) {
|
function Content({ fileData, tree }: QuartzComponentProps) {
|
||||||
const content = htmlToJsx(fileData.filePath!, tree)
|
const content = htmlToJsx(fileData.filePath!, tree)
|
||||||
const classes: string[] = fileData.frontmatter?.cssclasses ?? []
|
return <article class="popover-hint">{content}</article>
|
||||||
const classString = ["popover-hint", ...classes].join(" ")
|
|
||||||
return <article class={classString}>{content}</article>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (() => Content) satisfies QuartzComponentConstructor
|
export default (() => Content) satisfies QuartzComponentConstructor
|
||||||
|
|||||||
@@ -8,20 +8,6 @@ import { Root } from "hast"
|
|||||||
import { pluralize } from "../../util/lang"
|
import { pluralize } from "../../util/lang"
|
||||||
import { htmlToJsx } from "../../util/jsx"
|
import { htmlToJsx } from "../../util/jsx"
|
||||||
|
|
||||||
interface FolderContentOptions {
|
|
||||||
/**
|
|
||||||
* Whether to display number of folders
|
|
||||||
*/
|
|
||||||
showFolderCount: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultOptions: FolderContentOptions = {
|
|
||||||
showFolderCount: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ((opts?: Partial<FolderContentOptions>) => {
|
|
||||||
const options: FolderContentOptions = { ...defaultOptions, ...opts }
|
|
||||||
|
|
||||||
function FolderContent(props: QuartzComponentProps) {
|
function FolderContent(props: QuartzComponentProps) {
|
||||||
const { tree, fileData, allFiles } = props
|
const { tree, fileData, allFiles } = props
|
||||||
const folderSlug = _stripSlashes(simplifySlug(fileData.slug!))
|
const folderSlug = _stripSlashes(simplifySlug(fileData.slug!))
|
||||||
@@ -33,8 +19,7 @@ export default ((opts?: Partial<FolderContentOptions>) => {
|
|||||||
const isDirectChild = fileParts.length === folderParts.length + 1
|
const isDirectChild = fileParts.length === folderParts.length + 1
|
||||||
return prefixed && isDirectChild
|
return prefixed && isDirectChild
|
||||||
})
|
})
|
||||||
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
|
|
||||||
const classes = ["popover-hint", ...cssClasses].join(" ")
|
|
||||||
const listProps = {
|
const listProps = {
|
||||||
...props,
|
...props,
|
||||||
allFiles: allPagesInFolder,
|
allFiles: allPagesInFolder,
|
||||||
@@ -46,22 +31,17 @@ export default ((opts?: Partial<FolderContentOptions>) => {
|
|||||||
: htmlToJsx(fileData.filePath!, tree)
|
: htmlToJsx(fileData.filePath!, tree)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={classes}>
|
<div class="popover-hint">
|
||||||
<article>
|
<article>
|
||||||
<p>{content}</p>
|
<p>{content}</p>
|
||||||
</article>
|
</article>
|
||||||
<div class="page-listing">
|
|
||||||
{options.showFolderCount && (
|
|
||||||
<p>{pluralize(allPagesInFolder.length, "item")} under this folder.</p>
|
<p>{pluralize(allPagesInFolder.length, "item")} under this folder.</p>
|
||||||
)}
|
|
||||||
<div>
|
<div>
|
||||||
<PageList {...listProps} />
|
<PageList {...listProps} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
FolderContent.css = style + PageList.css
|
FolderContent.css = style + PageList.css
|
||||||
return FolderContent
|
export default (() => FolderContent) satisfies QuartzComponentConstructor
|
||||||
}) satisfies QuartzComponentConstructor
|
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ function TagContent(props: QuartzComponentProps) {
|
|||||||
(tree as Root).children.length === 0
|
(tree as Root).children.length === 0
|
||||||
? fileData.description
|
? fileData.description
|
||||||
: htmlToJsx(fileData.filePath!, tree)
|
: htmlToJsx(fileData.filePath!, tree)
|
||||||
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
|
|
||||||
const classes = ["popover-hint", ...cssClasses].join(" ")
|
|
||||||
if (tag === "/") {
|
if (tag === "/") {
|
||||||
const tags = [
|
const tags = [
|
||||||
...new Set(
|
...new Set(
|
||||||
@@ -38,8 +37,9 @@ function TagContent(props: QuartzComponentProps) {
|
|||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
tagItemMap.set(tag, allPagesWithTag(tag))
|
tagItemMap.set(tag, allPagesWithTag(tag))
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={classes}>
|
<div class="popover-hint">
|
||||||
<article>
|
<article>
|
||||||
<p>{content}</p>
|
<p>{content}</p>
|
||||||
</article>
|
</article>
|
||||||
@@ -62,14 +62,12 @@ function TagContent(props: QuartzComponentProps) {
|
|||||||
</a>
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
{content && <p>{content}</p>}
|
{content && <p>{content}</p>}
|
||||||
<div class="page-listing">
|
|
||||||
<p>
|
<p>
|
||||||
{pluralize(pages.length, "item")} with this tag.{" "}
|
{pluralize(pages.length, "item")} with this tag.{" "}
|
||||||
{pages.length > numPages && `Showing first ${numPages}.`}
|
{pages.length > numPages && `Showing first ${numPages}.`}
|
||||||
</p>
|
</p>
|
||||||
<PageList limit={numPages} {...listProps} />
|
<PageList limit={numPages} {...listProps} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -83,15 +81,13 @@ function TagContent(props: QuartzComponentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={classes}>
|
<div class="popover-hint">
|
||||||
<article>{content}</article>
|
<article>{content}</article>
|
||||||
<div class="page-listing">
|
|
||||||
<p>{pluralize(pages.length, "item")} with this tag.</p>
|
<p>{pluralize(pages.length, "item")} with this tag.</p>
|
||||||
<div>
|
<div>
|
||||||
<PageList {...listProps} />
|
<PageList {...listProps} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -2,19 +2,15 @@ const userPref = window.matchMedia("(prefers-color-scheme: light)").matches ? "l
|
|||||||
const currentTheme = localStorage.getItem("theme") ?? userPref
|
const currentTheme = localStorage.getItem("theme") ?? userPref
|
||||||
document.documentElement.setAttribute("saved-theme", currentTheme)
|
document.documentElement.setAttribute("saved-theme", currentTheme)
|
||||||
|
|
||||||
const emitThemeChangeEvent = (theme: "light" | "dark") => {
|
|
||||||
const event: CustomEventMap["themechange"] = new CustomEvent("themechange", {
|
|
||||||
detail: { theme },
|
|
||||||
})
|
|
||||||
document.dispatchEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("nav", () => {
|
document.addEventListener("nav", () => {
|
||||||
const switchTheme = (e: any) => {
|
const switchTheme = (e: any) => {
|
||||||
const newTheme = e.target.checked ? "dark" : "light"
|
if (e.target.checked) {
|
||||||
document.documentElement.setAttribute("saved-theme", newTheme)
|
document.documentElement.setAttribute("saved-theme", "dark")
|
||||||
localStorage.setItem("theme", newTheme)
|
localStorage.setItem("theme", "dark")
|
||||||
emitThemeChangeEvent(newTheme)
|
} else {
|
||||||
|
document.documentElement.setAttribute("saved-theme", "light")
|
||||||
|
localStorage.setItem("theme", "light")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Darkmode toggle
|
// Darkmode toggle
|
||||||
@@ -32,6 +28,5 @@ document.addEventListener("nav", () => {
|
|||||||
document.documentElement.setAttribute("saved-theme", newTheme)
|
document.documentElement.setAttribute("saved-theme", newTheme)
|
||||||
localStorage.setItem("theme", newTheme)
|
localStorage.setItem("theme", newTheme)
|
||||||
toggleSwitch.checked = e.matches
|
toggleSwitch.checked = e.matches
|
||||||
emitThemeChangeEvent(newTheme)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,106 +1,135 @@
|
|||||||
import { FolderState } from "../ExplorerNode"
|
import { FolderState } from "../ExplorerNode"
|
||||||
|
|
||||||
type MaybeHTMLElement = HTMLElement | undefined
|
// Current state of folders
|
||||||
let currentExplorerState: FolderState[]
|
let explorerState: FolderState[]
|
||||||
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
const observer = new IntersectionObserver((entries) => {
|
||||||
// If last element is observed, remove gradient of "overflow" class so element is visible
|
// If last element is observed, remove gradient of "overflow" class so element is visible
|
||||||
const explorerUl = document.getElementById("explorer-ul")
|
const explorer = document.getElementById("explorer-ul")
|
||||||
if (!explorerUl) return
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
explorerUl.classList.add("no-background")
|
explorer?.classList.add("no-background")
|
||||||
} else {
|
} else {
|
||||||
explorerUl.classList.remove("no-background")
|
explorer?.classList.remove("no-background")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function toggleExplorer(this: HTMLElement) {
|
function toggleExplorer(this: HTMLElement) {
|
||||||
|
// Toggle collapsed state of entire explorer
|
||||||
this.classList.toggle("collapsed")
|
this.classList.toggle("collapsed")
|
||||||
const content = this.nextElementSibling as MaybeHTMLElement
|
const content = this.nextElementSibling as HTMLElement
|
||||||
if (!content) return
|
|
||||||
|
|
||||||
content.classList.toggle("collapsed")
|
content.classList.toggle("collapsed")
|
||||||
content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
|
content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFolder(evt: MouseEvent) {
|
function toggleFolder(evt: MouseEvent) {
|
||||||
evt.stopPropagation()
|
evt.stopPropagation()
|
||||||
const target = evt.target as MaybeHTMLElement
|
|
||||||
if (!target) return
|
|
||||||
|
|
||||||
|
// Element that was clicked
|
||||||
|
const target = evt.target as HTMLElement
|
||||||
|
|
||||||
|
// Check if target was svg icon or button
|
||||||
const isSvg = target.nodeName === "svg"
|
const isSvg = target.nodeName === "svg"
|
||||||
const childFolderContainer = (
|
|
||||||
isSvg
|
// corresponding <ul> element relative to clicked button/folder
|
||||||
? target.parentElement?.nextSibling
|
let childFolderContainer: HTMLElement
|
||||||
: target.parentElement?.parentElement?.nextElementSibling
|
|
||||||
) as MaybeHTMLElement
|
// <li> element of folder (stores folder-path dataset)
|
||||||
const currentFolderParent = (
|
let currentFolderParent: HTMLElement
|
||||||
isSvg ? target.nextElementSibling : target.parentElement
|
|
||||||
) as MaybeHTMLElement
|
// Get correct relative container and toggle collapsed class
|
||||||
if (!(childFolderContainer && currentFolderParent)) return
|
if (isSvg) {
|
||||||
|
childFolderContainer = target.parentElement?.nextSibling as HTMLElement
|
||||||
|
currentFolderParent = target.nextElementSibling as HTMLElement
|
||||||
|
|
||||||
childFolderContainer.classList.toggle("open")
|
childFolderContainer.classList.toggle("open")
|
||||||
|
} else {
|
||||||
|
childFolderContainer = target.parentElement?.parentElement?.nextElementSibling as HTMLElement
|
||||||
|
currentFolderParent = target.parentElement as HTMLElement
|
||||||
|
|
||||||
|
childFolderContainer.classList.toggle("open")
|
||||||
|
}
|
||||||
|
if (!childFolderContainer) return
|
||||||
|
|
||||||
|
// Collapse folder container
|
||||||
const isCollapsed = childFolderContainer.classList.contains("open")
|
const isCollapsed = childFolderContainer.classList.contains("open")
|
||||||
setFolderState(childFolderContainer, !isCollapsed)
|
setFolderState(childFolderContainer, !isCollapsed)
|
||||||
const fullFolderPath = currentFolderParent.dataset.folderpath as string
|
|
||||||
toggleCollapsedByPath(currentExplorerState, fullFolderPath)
|
// Save folder state to localStorage
|
||||||
const stringifiedFileTree = JSON.stringify(currentExplorerState)
|
const clickFolderPath = currentFolderParent.dataset.folderpath as string
|
||||||
|
|
||||||
|
// Remove leading "/"
|
||||||
|
const fullFolderPath = clickFolderPath.substring(1)
|
||||||
|
toggleCollapsedByPath(explorerState, fullFolderPath)
|
||||||
|
|
||||||
|
const stringifiedFileTree = JSON.stringify(explorerState)
|
||||||
localStorage.setItem("fileTree", stringifiedFileTree)
|
localStorage.setItem("fileTree", stringifiedFileTree)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupExplorer() {
|
function setupExplorer() {
|
||||||
|
// Set click handler for collapsing entire explorer
|
||||||
const explorer = document.getElementById("explorer")
|
const explorer = document.getElementById("explorer")
|
||||||
if (!explorer) return
|
|
||||||
|
|
||||||
if (explorer.dataset.behavior === "collapse") {
|
|
||||||
for (const item of document.getElementsByClassName(
|
|
||||||
"folder-button",
|
|
||||||
) as HTMLCollectionOf<HTMLElement>) {
|
|
||||||
item.removeEventListener("click", toggleFolder)
|
|
||||||
item.addEventListener("click", toggleFolder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
explorer.removeEventListener("click", toggleExplorer)
|
|
||||||
explorer.addEventListener("click", toggleExplorer)
|
|
||||||
|
|
||||||
// Set up click handlers for each folder (click handler on folder "icon")
|
|
||||||
for (const item of document.getElementsByClassName(
|
|
||||||
"folder-icon",
|
|
||||||
) as HTMLCollectionOf<HTMLElement>) {
|
|
||||||
item.removeEventListener("click", toggleFolder)
|
|
||||||
item.addEventListener("click", toggleFolder)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get folder state from local storage
|
// Get folder state from local storage
|
||||||
const storageTree = localStorage.getItem("fileTree")
|
const storageTree = localStorage.getItem("fileTree")
|
||||||
|
|
||||||
|
// Convert to bool
|
||||||
const useSavedFolderState = explorer?.dataset.savestate === "true"
|
const useSavedFolderState = explorer?.dataset.savestate === "true"
|
||||||
const oldExplorerState: FolderState[] =
|
|
||||||
storageTree && useSavedFolderState ? JSON.parse(storageTree) : []
|
if (explorer) {
|
||||||
const oldIndex = new Map(oldExplorerState.map((entry) => [entry.path, entry.collapsed]))
|
// Get config
|
||||||
const newExplorerState: FolderState[] = explorer.dataset.tree
|
const collapseBehavior = explorer.dataset.behavior
|
||||||
? JSON.parse(explorer.dataset.tree)
|
|
||||||
: []
|
// Add click handlers for all folders (click handler on folder "label")
|
||||||
currentExplorerState = []
|
if (collapseBehavior === "collapse") {
|
||||||
for (const { path, collapsed } of newExplorerState) {
|
Array.prototype.forEach.call(
|
||||||
currentExplorerState.push({ path, collapsed: oldIndex.get(path) ?? collapsed })
|
document.getElementsByClassName("folder-button"),
|
||||||
|
function (item) {
|
||||||
|
item.removeEventListener("click", toggleFolder)
|
||||||
|
item.addEventListener("click", toggleFolder)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
currentExplorerState.map((folderState) => {
|
// Add click handler to main explorer
|
||||||
|
explorer.removeEventListener("click", toggleExplorer)
|
||||||
|
explorer.addEventListener("click", toggleExplorer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up click handlers for each folder (click handler on folder "icon")
|
||||||
|
Array.prototype.forEach.call(document.getElementsByClassName("folder-icon"), function (item) {
|
||||||
|
item.removeEventListener("click", toggleFolder)
|
||||||
|
item.addEventListener("click", toggleFolder)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (storageTree && useSavedFolderState) {
|
||||||
|
// Get state from localStorage and set folder state
|
||||||
|
explorerState = JSON.parse(storageTree)
|
||||||
|
explorerState.map((folderUl) => {
|
||||||
|
// grab <li> element for matching folder path
|
||||||
const folderLi = document.querySelector(
|
const folderLi = document.querySelector(
|
||||||
`[data-folderpath='${folderState.path}']`,
|
`[data-folderpath='/${folderUl.path}']`,
|
||||||
) as MaybeHTMLElement
|
) as HTMLElement
|
||||||
const folderUl = folderLi?.parentElement?.nextElementSibling as MaybeHTMLElement
|
|
||||||
if (folderUl) {
|
// Get corresponding content <ul> tag and set state
|
||||||
setFolderState(folderUl, folderState.collapsed)
|
if (folderLi) {
|
||||||
|
const folderUL = folderLi.parentElement?.nextElementSibling
|
||||||
|
if (folderUL) {
|
||||||
|
setFolderState(folderUL as HTMLElement, folderUl.collapsed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
} else if (explorer?.dataset.tree) {
|
||||||
|
// If tree is not in localStorage or config is disabled, use tree passed from Explorer as dataset
|
||||||
|
explorerState = JSON.parse(explorer.dataset.tree)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("resize", setupExplorer)
|
window.addEventListener("resize", setupExplorer)
|
||||||
document.addEventListener("nav", () => {
|
document.addEventListener("nav", () => {
|
||||||
setupExplorer()
|
setupExplorer()
|
||||||
|
|
||||||
observer.disconnect()
|
observer.disconnect()
|
||||||
|
|
||||||
// select pseudo element at end of list
|
// select pseudo element at end of list
|
||||||
@@ -116,7 +145,11 @@ document.addEventListener("nav", () => {
|
|||||||
* @param collapsed if folder should be set to collapsed or not
|
* @param collapsed if folder should be set to collapsed or not
|
||||||
*/
|
*/
|
||||||
function setFolderState(folderElement: HTMLElement, collapsed: boolean) {
|
function setFolderState(folderElement: HTMLElement, collapsed: boolean) {
|
||||||
return collapsed ? folderElement.classList.remove("open") : folderElement.classList.add("open")
|
if (collapsed) {
|
||||||
|
folderElement?.classList.remove("open")
|
||||||
|
} else {
|
||||||
|
folderElement?.classList.add("open")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -319,8 +319,8 @@ function renderGlobalGraph() {
|
|||||||
registerEscapeHandler(container, hideGlobalGraph)
|
registerEscapeHandler(container, hideGlobalGraph)
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
document.addEventListener("nav", async (e: unknown) => {
|
||||||
const slug = e.detail.url
|
const slug = (e as CustomEventMap["nav"]).detail.url
|
||||||
addToVisited(slug)
|
addToVisited(slug)
|
||||||
await renderGraph("graph-container", slug)
|
await renderGraph("graph-container", slug)
|
||||||
|
|
||||||
|
|||||||
3
quartz/components/scripts/plausible.inline.ts
Normal file
3
quartz/components/scripts/plausible.inline.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import Plausible from "plausible-tracker"
|
||||||
|
const { trackPageview } = Plausible()
|
||||||
|
document.addEventListener("nav", () => trackPageview())
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import FlexSearch from "flexsearch"
|
import { Document, SimpleDocumentSearchResultSetUnit } from "flexsearch"
|
||||||
import { ContentDetails } from "../../plugins/emitters/contentIndex"
|
import { ContentDetails } from "../../plugins/emitters/contentIndex"
|
||||||
import { registerEscapeHandler, removeAllChildren } from "./util"
|
import { registerEscapeHandler, removeAllChildren } from "./util"
|
||||||
import { FullSlug, normalizeRelativeURLs, resolveRelative } from "../../util/path"
|
import { FullSlug, resolveRelative } from "../../util/path"
|
||||||
|
|
||||||
interface Item {
|
interface Item {
|
||||||
id: number
|
id: number
|
||||||
@@ -11,7 +11,7 @@ interface Item {
|
|||||||
tags: string[]
|
tags: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
let index: FlexSearch.Document<Item> | undefined = undefined
|
let index: Document<Item> | undefined = undefined
|
||||||
|
|
||||||
// Can be expanded with things like "term" in the future
|
// Can be expanded with things like "term" in the future
|
||||||
type SearchType = "basic" | "tags"
|
type SearchType = "basic" | "tags"
|
||||||
@@ -20,8 +20,8 @@ type SearchType = "basic" | "tags"
|
|||||||
let searchType: SearchType = "basic"
|
let searchType: SearchType = "basic"
|
||||||
|
|
||||||
const contextWindowWords = 30
|
const contextWindowWords = 30
|
||||||
const numSearchResults = 8
|
const numSearchResults = 5
|
||||||
const numTagResults = 5
|
const numTagResults = 3
|
||||||
function highlight(searchTerm: string, text: string, trim?: boolean) {
|
function highlight(searchTerm: string, text: string, trim?: boolean) {
|
||||||
// try to highlight longest tokens first
|
// try to highlight longest tokens first
|
||||||
const tokenizedTerms = searchTerm
|
const tokenizedTerms = searchTerm
|
||||||
@@ -35,12 +35,12 @@ function highlight(searchTerm: string, text: string, trim?: boolean) {
|
|||||||
if (trim) {
|
if (trim) {
|
||||||
const includesCheck = (tok: string) =>
|
const includesCheck = (tok: string) =>
|
||||||
tokenizedTerms.some((term) => tok.toLowerCase().startsWith(term.toLowerCase()))
|
tokenizedTerms.some((term) => tok.toLowerCase().startsWith(term.toLowerCase()))
|
||||||
const occurrencesIndices = tokenizedText.map(includesCheck)
|
const occurencesIndices = tokenizedText.map(includesCheck)
|
||||||
|
|
||||||
let bestSum = 0
|
let bestSum = 0
|
||||||
let bestIndex = 0
|
let bestIndex = 0
|
||||||
for (let i = 0; i < Math.max(tokenizedText.length - contextWindowWords, 0); i++) {
|
for (let i = 0; i < Math.max(tokenizedText.length - contextWindowWords, 0); i++) {
|
||||||
const window = occurrencesIndices.slice(i, i + contextWindowWords)
|
const window = occurencesIndices.slice(i, i + contextWindowWords)
|
||||||
const windowSum = window.reduce((total, cur) => total + (cur ? 1 : 0), 0)
|
const windowSum = window.reduce((total, cur) => total + (cur ? 1 : 0), 0)
|
||||||
if (windowSum >= bestSum) {
|
if (windowSum >= bestSum) {
|
||||||
bestSum = windowSum
|
bestSum = windowSum
|
||||||
@@ -71,43 +71,20 @@ function highlight(searchTerm: string, text: string, trim?: boolean) {
|
|||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const p = new DOMParser()
|
|
||||||
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
|
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
|
||||||
let prevShortcutHandler: ((e: HTMLElementEventMap["keydown"]) => void) | undefined = undefined
|
let prevShortcutHandler: ((e: HTMLElementEventMap["keydown"]) => void) | undefined = undefined
|
||||||
|
document.addEventListener("nav", async (e: unknown) => {
|
||||||
const fetchContentCache: Map<FullSlug, Element[]> = new Map()
|
const currentSlug = (e as CustomEventMap["nav"]).detail.url
|
||||||
|
|
||||||
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|
||||||
const currentSlug = e.detail.url
|
|
||||||
|
|
||||||
const data = await fetchData
|
const data = await fetchData
|
||||||
const container = document.getElementById("search-container")
|
const container = document.getElementById("search-container")
|
||||||
const sidebar = container?.closest(".sidebar") as HTMLElement
|
const sidebar = container?.closest(".sidebar") as HTMLElement
|
||||||
const searchIcon = document.getElementById("search-icon")
|
const searchIcon = document.getElementById("search-icon")
|
||||||
const searchBar = document.getElementById("search-bar") as HTMLInputElement | null
|
const searchBar = document.getElementById("search-bar") as HTMLInputElement | null
|
||||||
const searchLayout = document.getElementById("search-layout")
|
const results = document.getElementById("results-container")
|
||||||
|
const resultCards = document.getElementsByClassName("result-card")
|
||||||
const idDataMap = Object.keys(data) as FullSlug[]
|
const idDataMap = Object.keys(data) as FullSlug[]
|
||||||
|
|
||||||
const appendLayout = (el: HTMLElement) => {
|
|
||||||
if (searchLayout?.querySelector(`#${el.id}`) === null) {
|
|
||||||
searchLayout?.appendChild(el)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const enablePreview = searchLayout?.dataset?.preview === "true"
|
|
||||||
let preview: HTMLDivElement | undefined = undefined
|
|
||||||
const results = document.createElement("div")
|
|
||||||
results.id = "results-container"
|
|
||||||
results.style.flexBasis = enablePreview ? "30%" : "100%"
|
|
||||||
appendLayout(results)
|
|
||||||
|
|
||||||
if (enablePreview) {
|
|
||||||
preview = document.createElement("div")
|
|
||||||
preview.id = "preview-container"
|
|
||||||
preview.style.flexBasis = "70%"
|
|
||||||
appendLayout(preview)
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideSearch() {
|
function hideSearch() {
|
||||||
container?.classList.remove("active")
|
container?.classList.remove("active")
|
||||||
if (searchBar) {
|
if (searchBar) {
|
||||||
@@ -119,9 +96,6 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
if (results) {
|
if (results) {
|
||||||
removeAllChildren(results)
|
removeAllChildren(results)
|
||||||
}
|
}
|
||||||
if (preview) {
|
|
||||||
removeAllChildren(preview)
|
|
||||||
}
|
|
||||||
|
|
||||||
searchType = "basic" // reset search type after closing
|
searchType = "basic" // reset search type after closing
|
||||||
}
|
}
|
||||||
@@ -135,7 +109,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
searchBar?.focus()
|
searchBar?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
|
function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
|
||||||
if (e.key === "k" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
if (e.key === "k" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const searchBarOpen = container?.classList.contains("active")
|
const searchBarOpen = container?.classList.contains("active")
|
||||||
@@ -148,54 +122,33 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
|
|
||||||
// add "#" prefix for tag search
|
// add "#" prefix for tag search
|
||||||
if (searchBar) searchBar.value = "#"
|
if (searchBar) searchBar.value = "#"
|
||||||
}
|
} else if (e.key === "Enter") {
|
||||||
|
// If result has focus, navigate to that one, otherwise pick first result
|
||||||
const resultCards = document.getElementsByClassName("result-card")
|
if (results?.contains(document.activeElement)) {
|
||||||
|
|
||||||
// If search is active, then we will render the first result and display accordingly
|
|
||||||
if (!container?.classList.contains("active")) return
|
|
||||||
else if (results?.contains(document.activeElement)) {
|
|
||||||
const active = document.activeElement as HTMLInputElement
|
const active = document.activeElement as HTMLInputElement
|
||||||
await displayPreview(active)
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
active.click()
|
active.click()
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const anchor = resultCards[0] as HTMLInputElement | null
|
const anchor = document.getElementsByClassName("result-card")[0] as HTMLInputElement | null
|
||||||
await displayPreview(anchor)
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
anchor?.click()
|
anchor?.click()
|
||||||
}
|
}
|
||||||
|
} else if (e.key === "ArrowDown") {
|
||||||
|
e.preventDefault()
|
||||||
|
// When first pressing ArrowDown, results wont contain the active element, so focus first element
|
||||||
|
if (!results?.contains(document.activeElement)) {
|
||||||
|
const firstResult = resultCards[0] as HTMLInputElement | null
|
||||||
|
firstResult?.focus()
|
||||||
|
} else {
|
||||||
|
// If an element in results-container already has focus, focus next one
|
||||||
|
const nextResult = document.activeElement?.nextElementSibling as HTMLInputElement | null
|
||||||
|
nextResult?.focus()
|
||||||
}
|
}
|
||||||
|
} else if (e.key === "ArrowUp") {
|
||||||
if (e.key === "ArrowUp" || (e.shiftKey && e.key === "Tab")) {
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (results?.contains(document.activeElement)) {
|
if (results?.contains(document.activeElement)) {
|
||||||
// If an element in results-container already has focus, focus previous one
|
// If an element in results-container already has focus, focus previous one
|
||||||
const currentResult = document.activeElement as HTMLInputElement | null
|
const prevResult = document.activeElement?.previousElementSibling as HTMLInputElement | null
|
||||||
const prevResult = currentResult?.previousElementSibling as HTMLInputElement | null
|
|
||||||
currentResult?.classList.remove("focus")
|
|
||||||
await displayPreview(prevResult)
|
|
||||||
prevResult?.focus()
|
prevResult?.focus()
|
||||||
}
|
}
|
||||||
} else if (e.key === "ArrowDown" || e.key === "Tab") {
|
|
||||||
e.preventDefault()
|
|
||||||
// The results should already been focused, so we need to find the next one.
|
|
||||||
// The activeElement is the search bar, so we need to find the first result and focus it.
|
|
||||||
if (!results?.contains(document.activeElement)) {
|
|
||||||
const firstResult = resultCards[0] as HTMLInputElement | null
|
|
||||||
const secondResult = firstResult?.nextElementSibling as HTMLInputElement | null
|
|
||||||
firstResult?.classList.remove("focus")
|
|
||||||
await displayPreview(secondResult)
|
|
||||||
secondResult?.focus()
|
|
||||||
} else {
|
|
||||||
// If an element in results-container already has focus, focus next one
|
|
||||||
const active = document.activeElement as HTMLInputElement | null
|
|
||||||
active?.classList.remove("focus")
|
|
||||||
const nextResult = active?.nextElementSibling as HTMLInputElement | null
|
|
||||||
await displayPreview(nextResult)
|
|
||||||
nextResult?.focus()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,7 +196,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
const termLower = term.toLowerCase()
|
const termLower = term.toLowerCase()
|
||||||
let matching = tags.filter((str) => str.includes(termLower))
|
let matching = tags.filter((str) => str.includes(termLower))
|
||||||
|
|
||||||
// Subtract matching from original tags, then push difference
|
// Substract matching from original tags, then push difference
|
||||||
if (matching.length > 0) {
|
if (matching.length > 0) {
|
||||||
let difference = tags.filter((x) => !matching.includes(x))
|
let difference = tags.filter((x) => !matching.includes(x))
|
||||||
|
|
||||||
@@ -264,117 +217,37 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveUrl(slug: FullSlug): URL {
|
|
||||||
return new URL(resolveRelative(currentSlug, slug), location.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
const resultToHTML = ({ slug, title, content, tags }: Item) => {
|
const resultToHTML = ({ slug, title, content, tags }: Item) => {
|
||||||
const htmlTags = tags.length > 0 ? `<ul>${tags.join("")}</ul>` : ``
|
const htmlTags = tags.length > 0 ? `<ul>${tags.join("")}</ul>` : ``
|
||||||
const resultContent = enablePreview && window.innerWidth > 600 ? "" : `<p>${content}</p>`
|
const button = document.createElement("button")
|
||||||
|
button.classList.add("result-card")
|
||||||
const itemTile = document.createElement("a")
|
button.id = slug
|
||||||
itemTile.classList.add("result-card")
|
button.innerHTML = `<h3>${title}</h3>${htmlTags}<p>${content}</p>`
|
||||||
Object.assign(itemTile, {
|
button.addEventListener("click", () => {
|
||||||
id: slug,
|
const targ = resolveRelative(currentSlug, slug)
|
||||||
href: resolveUrl(slug).toString(),
|
window.spaNavigate(new URL(targ, window.location.toString()))
|
||||||
innerHTML: `<h3>${title}</h3>${htmlTags}${resultContent}`,
|
|
||||||
})
|
|
||||||
|
|
||||||
async function onMouseEnter(ev: MouseEvent) {
|
|
||||||
// When search is active, the first element is in focus, so we need to remove focus if given target is not the first element
|
|
||||||
const firstEl = document.getElementsByClassName("result-card")[0] as HTMLAnchorElement | null
|
|
||||||
const target = ev.target as HTMLAnchorElement
|
|
||||||
if (firstEl !== target) {
|
|
||||||
firstEl?.classList.remove("focus")
|
|
||||||
}
|
|
||||||
target.classList.add("focus")
|
|
||||||
await displayPreview(target)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onMouseLeave(ev: MouseEvent) {
|
|
||||||
const target = ev.target as HTMLAnchorElement
|
|
||||||
target.classList.remove("focus")
|
|
||||||
}
|
|
||||||
|
|
||||||
const events = [
|
|
||||||
["mouseenter", onMouseEnter],
|
|
||||||
["mouseleave", onMouseLeave],
|
|
||||||
[
|
|
||||||
"click",
|
|
||||||
(event: MouseEvent) => {
|
|
||||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
|
|
||||||
hideSearch()
|
hideSearch()
|
||||||
},
|
})
|
||||||
],
|
return button
|
||||||
] as [keyof HTMLElementEventMap, (this: HTMLElement) => void][]
|
|
||||||
|
|
||||||
events.forEach(([event, handler]) => itemTile.addEventListener(event, handler))
|
|
||||||
|
|
||||||
return itemTile
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function displayResults(finalResults: Item[]) {
|
function displayResults(finalResults: Item[]) {
|
||||||
if (!results) return
|
if (!results) return
|
||||||
|
|
||||||
removeAllChildren(results)
|
removeAllChildren(results)
|
||||||
if (finalResults.length === 0) {
|
if (finalResults.length === 0) {
|
||||||
results.innerHTML = `<a class="result-card">
|
results.innerHTML = `<button class="result-card">
|
||||||
<h3>No results.</h3>
|
<h3>No results.</h3>
|
||||||
<p>Try another search term?</p>
|
<p>Try another search term?</p>
|
||||||
</a>`
|
</button>`
|
||||||
} else {
|
} else {
|
||||||
results.append(...finalResults.map(resultToHTML))
|
results.append(...finalResults.map(resultToHTML))
|
||||||
}
|
}
|
||||||
// focus on first result, then also dispatch preview immediately
|
|
||||||
if (results?.firstElementChild) {
|
|
||||||
results?.firstElementChild?.classList.add("focus")
|
|
||||||
await displayPreview(results?.firstElementChild as HTMLElement)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchContent(slug: FullSlug): Promise<Element[]> {
|
|
||||||
if (fetchContentCache.has(slug)) {
|
|
||||||
return fetchContentCache.get(slug) as Element[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetUrl = resolveUrl(slug).toString()
|
|
||||||
const contents = await fetch(targetUrl)
|
|
||||||
.then((res) => res.text())
|
|
||||||
.then((contents) => {
|
|
||||||
if (contents === undefined) {
|
|
||||||
throw new Error(`Could not fetch ${targetUrl}`)
|
|
||||||
}
|
|
||||||
const html = p.parseFromString(contents ?? "", "text/html")
|
|
||||||
normalizeRelativeURLs(html, targetUrl)
|
|
||||||
return [...html.getElementsByClassName("popover-hint")]
|
|
||||||
})
|
|
||||||
|
|
||||||
fetchContentCache.set(slug, contents)
|
|
||||||
return contents
|
|
||||||
}
|
|
||||||
|
|
||||||
async function displayPreview(el: HTMLElement | null) {
|
|
||||||
if (!searchLayout || !enablePreview || !el) return
|
|
||||||
|
|
||||||
const slug = el.id as FullSlug
|
|
||||||
el.classList.add("focus")
|
|
||||||
|
|
||||||
removeAllChildren(preview as HTMLElement)
|
|
||||||
const contentDetails = await fetchContent(slug)
|
|
||||||
|
|
||||||
const previewInner = document.createElement("div")
|
|
||||||
previewInner.classList.add("preview-inner")
|
|
||||||
preview?.appendChild(previewInner)
|
|
||||||
contentDetails?.forEach((elt) => previewInner.appendChild(elt))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onType(e: HTMLElementEventMap["input"]) {
|
async function onType(e: HTMLElementEventMap["input"]) {
|
||||||
let term = (e.target as HTMLInputElement).value
|
let term = (e.target as HTMLInputElement).value
|
||||||
let searchResults: FlexSearch.SimpleDocumentSearchResultSetUnit[]
|
let searchResults: SimpleDocumentSearchResultSetUnit[]
|
||||||
|
|
||||||
if (searchLayout) {
|
|
||||||
searchLayout.style.opacity = "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (term.toLowerCase().startsWith("#")) {
|
if (term.toLowerCase().startsWith("#")) {
|
||||||
searchType = "tags"
|
searchType = "tags"
|
||||||
@@ -413,7 +286,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
...getByField("tags"),
|
...getByField("tags"),
|
||||||
])
|
])
|
||||||
const finalResults = [...allIds].map((id) => formatForDisplay(term, id))
|
const finalResults = [...allIds].map((id) => formatForDisplay(term, id))
|
||||||
await displayResults(finalResults)
|
displayResults(finalResults)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevShortcutHandler) {
|
if (prevShortcutHandler) {
|
||||||
@@ -429,23 +302,24 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
|
|
||||||
// setup index if it hasn't been already
|
// setup index if it hasn't been already
|
||||||
if (!index) {
|
if (!index) {
|
||||||
index = new FlexSearch.Document({
|
index = new Document({
|
||||||
charset: "latin:extra",
|
charset: "latin:extra",
|
||||||
|
optimize: true,
|
||||||
encode: encoder,
|
encode: encoder,
|
||||||
document: {
|
document: {
|
||||||
id: "id",
|
id: "id",
|
||||||
index: [
|
index: [
|
||||||
{
|
{
|
||||||
field: "title",
|
field: "title",
|
||||||
tokenize: "forward",
|
tokenize: "reverse",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "content",
|
field: "content",
|
||||||
tokenize: "forward",
|
tokenize: "reverse",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "tags",
|
field: "tags",
|
||||||
tokenize: "forward",
|
tokenize: "reverse",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -463,7 +337,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
* @param index index to fill
|
* @param index index to fill
|
||||||
* @param data data to fill index with
|
* @param data data to fill index with
|
||||||
*/
|
*/
|
||||||
async function fillDocument(index: FlexSearch.Document<Item, false>, data: any) {
|
async function fillDocument(index: Document<Item, false>, data: any) {
|
||||||
let id = 0
|
let id = 0
|
||||||
for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
|
for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
|
||||||
await index.addAsync(id, {
|
await index.addAsync(id, {
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ const observer = new IntersectionObserver((entries) => {
|
|||||||
|
|
||||||
function toggleToc(this: HTMLElement) {
|
function toggleToc(this: HTMLElement) {
|
||||||
this.classList.toggle("collapsed")
|
this.classList.toggle("collapsed")
|
||||||
const content = this.nextElementSibling as HTMLElement | undefined
|
const content = this.nextElementSibling as HTMLElement
|
||||||
if (!content) return
|
|
||||||
content.classList.toggle("collapsed")
|
content.classList.toggle("collapsed")
|
||||||
content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
|
content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
|
||||||
}
|
}
|
||||||
@@ -26,8 +25,7 @@ function setupToc() {
|
|||||||
const toc = document.getElementById("toc")
|
const toc = document.getElementById("toc")
|
||||||
if (toc) {
|
if (toc) {
|
||||||
const collapsed = toc.classList.contains("collapsed")
|
const collapsed = toc.classList.contains("collapsed")
|
||||||
const content = toc.nextElementSibling as HTMLElement | undefined
|
const content = toc.nextElementSibling as HTMLElement
|
||||||
if (!content) return
|
|
||||||
content.style.maxHeight = collapsed ? "0px" : content.scrollHeight + "px"
|
content.style.maxHeight = collapsed ? "0px" : content.scrollHeight + "px"
|
||||||
toc.removeEventListener("click", toggleToc)
|
toc.removeEventListener("click", toggleToc)
|
||||||
toc.addEventListener("click", toggleToc)
|
toc.addEventListener("click", toggleToc)
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ svg {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
font-family: var(--headerFont);
|
font-family: var(--headerFont);
|
||||||
|
|
||||||
& span {
|
& p {
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: var(--secondary);
|
color: var(--secondary);
|
||||||
@@ -126,7 +126,7 @@ svg {
|
|||||||
backface-visibility: visible;
|
backface-visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
li:has(> .folder-outer:not(.open)) > .folder-container > svg {
|
div:has(> .folder-outer:not(.open)) > .folder-container > svg {
|
||||||
transform: rotate(-90deg);
|
transform: rotate(-90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
max-height: 20rem;
|
max-height: 20rem;
|
||||||
padding: 0 1rem 1rem 1rem;
|
padding: 0 1rem 1rem 1rem;
|
||||||
font-weight: initial;
|
font-weight: initial;
|
||||||
font-style: initial;
|
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
font-size: initial;
|
font-size: initial;
|
||||||
font-family: var(--bodyFont);
|
font-family: var(--bodyFont);
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
|
|
||||||
& > #search-space {
|
& > #search-space {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
margin-top: 12vh;
|
margin-top: 15vh;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
|
||||||
@@ -86,76 +86,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > #search-layout {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
opacity: 0;
|
|
||||||
border: 1px solid var(--lightgray);
|
|
||||||
|
|
||||||
& > div {
|
|
||||||
height: calc(75vh - 20em);
|
|
||||||
background: none;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
border-top-left-radius: 5px;
|
|
||||||
border-bottom-left-radius: 5px;
|
|
||||||
border-right: 1px solid var(--lightgray);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-top-right-radius: 5px;
|
|
||||||
border-bottom-right-radius: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media all and (max-width: $mobileBreakpoint) {
|
|
||||||
display: block;
|
|
||||||
& > *:not(#results-container) {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > #results-container {
|
& > #results-container {
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& > #preview-container {
|
|
||||||
display: block;
|
|
||||||
box-sizing: border-box;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
& .preview-inner {
|
|
||||||
padding: 1em;
|
|
||||||
height: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
overflow-y: auto;
|
|
||||||
font-family: inherit;
|
|
||||||
color: var(--dark);
|
|
||||||
line-height: 1.5em;
|
|
||||||
font-weight: 400;
|
|
||||||
background: var(--light);
|
|
||||||
border-radius: 5px;
|
|
||||||
box-shadow:
|
|
||||||
0 14px 50px rgba(27, 33, 48, 0.12),
|
|
||||||
0 10px 30px rgba(27, 33, 48, 0.16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& > #results-container {
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
& .result-card {
|
& .result-card {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s ease;
|
transition: background 0.2s ease;
|
||||||
border-bottom: 1px solid var(--lightgray);
|
border: 1px solid var(--lightgray);
|
||||||
|
border-bottom: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: block;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
// normalize card props
|
// normalize button props
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
@@ -164,7 +104,6 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
background: var(--light);
|
background: var(--light);
|
||||||
outline: none;
|
outline: none;
|
||||||
font-weight: inherit;
|
|
||||||
|
|
||||||
& .highlight {
|
& .highlight {
|
||||||
color: var(--secondary);
|
color: var(--secondary);
|
||||||
@@ -172,11 +111,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus,
|
&:focus {
|
||||||
&.focus {
|
|
||||||
background: var(--lightgray);
|
background: var(--lightgray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
border-top-left-radius: 5px;
|
||||||
|
border-top-right-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
|
border-bottom: 1px solid var(--lightgray);
|
||||||
|
}
|
||||||
|
|
||||||
& > h3 {
|
& > h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@@ -196,7 +145,8 @@
|
|||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-top: 0.45rem;
|
margin-top: 0.45rem;
|
||||||
box-sizing: border-box;
|
// Offset border radius
|
||||||
|
margin-left: -2px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-clip: border-box;
|
background-clip: border-box;
|
||||||
}
|
}
|
||||||
@@ -226,4 +176,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { FilePath, FullSlug } from "../../util/path"
|
|||||||
import { sharedPageComponents } from "../../../quartz.layout"
|
import { sharedPageComponents } from "../../../quartz.layout"
|
||||||
import { NotFound } from "../../components"
|
import { NotFound } from "../../components"
|
||||||
import { defaultProcessedContent } from "../vfile"
|
import { defaultProcessedContent } from "../vfile"
|
||||||
import { write } from "./helpers"
|
|
||||||
|
|
||||||
export const NotFoundPage: QuartzEmitterPlugin = () => {
|
export const NotFoundPage: QuartzEmitterPlugin = () => {
|
||||||
const opts: FullPageLayout = {
|
const opts: FullPageLayout = {
|
||||||
@@ -26,7 +25,7 @@ export const NotFoundPage: QuartzEmitterPlugin = () => {
|
|||||||
getQuartzComponents() {
|
getQuartzComponents() {
|
||||||
return [Head, Body, pageBody, Footer]
|
return [Head, Body, pageBody, Footer]
|
||||||
},
|
},
|
||||||
async emit(ctx, _content, resources): Promise<FilePath[]> {
|
async emit(ctx, _content, resources, emit): Promise<FilePath[]> {
|
||||||
const cfg = ctx.cfg.configuration
|
const cfg = ctx.cfg.configuration
|
||||||
const slug = "404" as FullSlug
|
const slug = "404" as FullSlug
|
||||||
|
|
||||||
@@ -49,8 +48,7 @@ export const NotFoundPage: QuartzEmitterPlugin = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
await write({
|
await emit({
|
||||||
ctx,
|
|
||||||
content: renderPage(slug, componentData, opts, externalResources),
|
content: renderPage(slug, componentData, opts, externalResources),
|
||||||
slug,
|
slug,
|
||||||
ext: ".html",
|
ext: ".html",
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
import { FilePath, FullSlug, joinSegments, resolveRelative, simplifySlug } from "../../util/path"
|
import { FilePath, FullSlug, joinSegments, resolveRelative, simplifySlug } from "../../util/path"
|
||||||
import { QuartzEmitterPlugin } from "../types"
|
import { QuartzEmitterPlugin } from "../types"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { write } from "./helpers"
|
|
||||||
|
|
||||||
export const AliasRedirects: QuartzEmitterPlugin = () => ({
|
export const AliasRedirects: QuartzEmitterPlugin = () => ({
|
||||||
name: "AliasRedirects",
|
name: "AliasRedirects",
|
||||||
getQuartzComponents() {
|
getQuartzComponents() {
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
async emit(ctx, content, _resources): Promise<FilePath[]> {
|
async emit({ argv }, content, _resources, emit): Promise<FilePath[]> {
|
||||||
const { argv } = ctx
|
|
||||||
const fps: FilePath[] = []
|
const fps: FilePath[] = []
|
||||||
|
|
||||||
for (const [_tree, file] of content) {
|
for (const [_tree, file] of content) {
|
||||||
const ogSlug = simplifySlug(file.data.slug!)
|
const ogSlug = simplifySlug(file.data.slug!)
|
||||||
const dir = path.posix.relative(argv.directory, path.dirname(file.data.filePath!))
|
const dir = path.posix.relative(argv.directory, path.dirname(file.data.filePath!))
|
||||||
const aliases = file.data.frontmatter?.aliases ?? []
|
|
||||||
|
let aliases: FullSlug[] = file.data.frontmatter?.aliases ?? file.data.frontmatter?.alias ?? []
|
||||||
|
if (typeof aliases === "string") {
|
||||||
|
aliases = [aliases]
|
||||||
|
}
|
||||||
|
|
||||||
const slugs: FullSlug[] = aliases.map((alias) => path.posix.join(dir, alias) as FullSlug)
|
const slugs: FullSlug[] = aliases.map((alias) => path.posix.join(dir, alias) as FullSlug)
|
||||||
const permalink = file.data.frontmatter?.permalink
|
const permalink = file.data.frontmatter?.permalink
|
||||||
if (typeof permalink === "string") {
|
if (typeof permalink === "string") {
|
||||||
@@ -29,8 +32,7 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const redirUrl = resolveRelative(slug, file.data.slug!)
|
const redirUrl = resolveRelative(slug, file.data.slug!)
|
||||||
const fp = await write({
|
const fp = await emit({
|
||||||
ctx,
|
|
||||||
content: `
|
content: `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en-us">
|
<html lang="en-us">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const Assets: QuartzEmitterPlugin = () => {
|
|||||||
getQuartzComponents() {
|
getQuartzComponents() {
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
async emit({ argv, cfg }, _content, _resources): Promise<FilePath[]> {
|
async emit({ argv, cfg }, _content, _resources, _emit): Promise<FilePath[]> {
|
||||||
// glob all non MD/MDX/HTML files in content folder and copy it over
|
// glob all non MD/MDX/HTML files in content folder and copy it over
|
||||||
const assetsPath = argv.output
|
const assetsPath = argv.output
|
||||||
const fps = await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns])
|
const fps = await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns])
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const CNAME: QuartzEmitterPlugin = () => ({
|
|||||||
getQuartzComponents() {
|
getQuartzComponents() {
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
async emit({ argv, cfg }, _content, _resources): Promise<FilePath[]> {
|
async emit({ argv, cfg }, _content, _resources, _emit): Promise<FilePath[]> {
|
||||||
if (!cfg.configuration.baseUrl) {
|
if (!cfg.configuration.baseUrl) {
|
||||||
console.warn(chalk.yellow("CNAME emitter requires `baseUrl` to be set in your configuration"))
|
console.warn(chalk.yellow("CNAME emitter requires `baseUrl` to be set in your configuration"))
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ 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"
|
||||||
@@ -12,8 +14,6 @@ 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"
|
|
||||||
import { write } from "./helpers"
|
|
||||||
|
|
||||||
type ComponentResources = {
|
type ComponentResources = {
|
||||||
css: string[]
|
css: string[]
|
||||||
@@ -56,16 +56,9 @@ function getComponentResources(ctx: BuildCtx): ComponentResources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function joinScripts(scripts: string[]): Promise<string> {
|
function joinScripts(scripts: string[]): string {
|
||||||
// wrap with iife to prevent scope collision
|
// wrap with iife to prevent scope collision
|
||||||
const script = scripts.map((script) => `(function () {${script}})();`).join("\n")
|
return scripts.map((script) => `(function () {${script}})();`).join("\n")
|
||||||
|
|
||||||
// minify with esbuild
|
|
||||||
const res = await transpile(script, {
|
|
||||||
minify: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
return res.code
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addGlobalPageResources(
|
function addGlobalPageResources(
|
||||||
@@ -92,34 +85,21 @@ 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") {
|
||||||
const plausibleHost = cfg.analytics.host ?? "https://plausible.io"
|
componentResources.afterDOMLoaded.push(plausibleScript)
|
||||||
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")
|
||||||
umamiScript.src = cfg.analytics.host ?? "https://analytics.umami.is/script.js"
|
umamiScript.src = "https://analytics.umami.is/script.js"
|
||||||
umamiScript.setAttribute("data-website-id", "${cfg.analytics.websiteId}")
|
umamiScript.setAttribute("data-website-id", "${cfg.analytics.websiteId}")
|
||||||
umamiScript.async = true
|
umamiScript.async = true
|
||||||
|
|
||||||
@@ -169,7 +149,7 @@ export const ComponentResources: QuartzEmitterPlugin<Options> = (opts?: Partial<
|
|||||||
getQuartzComponents() {
|
getQuartzComponents() {
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
async emit(ctx, _content, resources): Promise<FilePath[]> {
|
async emit(ctx, _content, resources, emit): Promise<FilePath[]> {
|
||||||
// component specific scripts and styles
|
// component specific scripts and styles
|
||||||
const componentResources = getComponentResources(ctx)
|
const componentResources = getComponentResources(ctx)
|
||||||
// important that this goes *after* component scripts
|
// important that this goes *after* component scripts
|
||||||
@@ -185,14 +165,10 @@ 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, postscript] = await Promise.all([
|
const prescript = joinScripts(componentResources.beforeDOMLoaded)
|
||||||
joinScripts(componentResources.beforeDOMLoaded),
|
const postscript = joinScripts(componentResources.afterDOMLoaded)
|
||||||
joinScripts(componentResources.afterDOMLoaded),
|
|
||||||
])
|
|
||||||
|
|
||||||
const fps = await Promise.all([
|
const fps = await Promise.all([
|
||||||
write({
|
emit({
|
||||||
ctx,
|
|
||||||
slug: "index" as FullSlug,
|
slug: "index" as FullSlug,
|
||||||
ext: ".css",
|
ext: ".css",
|
||||||
content: transform({
|
content: transform({
|
||||||
@@ -209,14 +185,12 @@ export const ComponentResources: QuartzEmitterPlugin<Options> = (opts?: Partial<
|
|||||||
include: Features.MediaQueries,
|
include: Features.MediaQueries,
|
||||||
}).code.toString(),
|
}).code.toString(),
|
||||||
}),
|
}),
|
||||||
write({
|
emit({
|
||||||
ctx,
|
|
||||||
slug: "prescript" as FullSlug,
|
slug: "prescript" as FullSlug,
|
||||||
ext: ".js",
|
ext: ".js",
|
||||||
content: prescript,
|
content: prescript,
|
||||||
}),
|
}),
|
||||||
write({
|
emit({
|
||||||
ctx,
|
|
||||||
slug: "postscript" as FullSlug,
|
slug: "postscript" as FullSlug,
|
||||||
ext: ".js",
|
ext: ".js",
|
||||||
content: postscript,
|
content: postscript,
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ 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, joinSegments, simplifySlug } from "../../util/path"
|
import { FilePath, FullSlug, SimpleSlug, 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"
|
||||||
import { write } from "./helpers"
|
|
||||||
|
|
||||||
export type ContentIndex = Map<FullSlug, ContentDetails>
|
export type ContentIndex = Map<FullSlug, ContentDetails>
|
||||||
export type ContentDetails = {
|
export type ContentDetails = {
|
||||||
@@ -38,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://${joinSegments(base, encodeURI(slug))}</loc>
|
<loc>https://${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)
|
||||||
@@ -49,11 +48,12 @@ function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string {
|
|||||||
|
|
||||||
function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: number): string {
|
function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: number): string {
|
||||||
const base = cfg.baseUrl ?? ""
|
const base = cfg.baseUrl ?? ""
|
||||||
|
const root = `https://${base}`
|
||||||
|
|
||||||
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>https://${joinSegments(base, encodeURI(slug))}</link>
|
<link>${root}/${encodeURI(slug)}</link>
|
||||||
<guid>https://${joinSegments(base, encodeURI(slug))}</guid>
|
<guid>${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>`
|
||||||
@@ -78,7 +78,7 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: nu
|
|||||||
<rss version="2.0">
|
<rss version="2.0">
|
||||||
<channel>
|
<channel>
|
||||||
<title>${escapeHTML(cfg.pageTitle)}</title>
|
<title>${escapeHTML(cfg.pageTitle)}</title>
|
||||||
<link>https://${base}</link>
|
<link>${root}</link>
|
||||||
<description>${!!limit ? `Last ${limit} notes` : "Recent notes"} on ${escapeHTML(
|
<description>${!!limit ? `Last ${limit} notes` : "Recent notes"} on ${escapeHTML(
|
||||||
cfg.pageTitle,
|
cfg.pageTitle,
|
||||||
)}</description>
|
)}</description>
|
||||||
@@ -92,7 +92,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
|
|||||||
opts = { ...defaultOptions, ...opts }
|
opts = { ...defaultOptions, ...opts }
|
||||||
return {
|
return {
|
||||||
name: "ContentIndex",
|
name: "ContentIndex",
|
||||||
async emit(ctx, content, _resources) {
|
async emit(ctx, content, _resources, emit) {
|
||||||
const cfg = ctx.cfg.configuration
|
const cfg = ctx.cfg.configuration
|
||||||
const emitted: FilePath[] = []
|
const emitted: FilePath[] = []
|
||||||
const linkIndex: ContentIndex = new Map()
|
const linkIndex: ContentIndex = new Map()
|
||||||
@@ -116,8 +116,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
|
|||||||
|
|
||||||
if (opts?.enableSiteMap) {
|
if (opts?.enableSiteMap) {
|
||||||
emitted.push(
|
emitted.push(
|
||||||
await write({
|
await emit({
|
||||||
ctx,
|
|
||||||
content: generateSiteMap(cfg, linkIndex),
|
content: generateSiteMap(cfg, linkIndex),
|
||||||
slug: "sitemap" as FullSlug,
|
slug: "sitemap" as FullSlug,
|
||||||
ext: ".xml",
|
ext: ".xml",
|
||||||
@@ -127,8 +126,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
|
|||||||
|
|
||||||
if (opts?.enableRSS) {
|
if (opts?.enableRSS) {
|
||||||
emitted.push(
|
emitted.push(
|
||||||
await write({
|
await emit({
|
||||||
ctx,
|
|
||||||
content: generateRSSFeed(cfg, linkIndex, opts.rssLimit),
|
content: generateRSSFeed(cfg, linkIndex, opts.rssLimit),
|
||||||
slug: "index" as FullSlug,
|
slug: "index" as FullSlug,
|
||||||
ext: ".xml",
|
ext: ".xml",
|
||||||
@@ -136,7 +134,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const fp = joinSegments("static", "contentIndex") as FullSlug
|
const fp = path.join("static", "contentIndex") as FullSlug
|
||||||
const simplifiedIndex = Object.fromEntries(
|
const simplifiedIndex = Object.fromEntries(
|
||||||
Array.from(linkIndex).map(([slug, content]) => {
|
Array.from(linkIndex).map(([slug, content]) => {
|
||||||
// remove description and from content index as nothing downstream
|
// remove description and from content index as nothing downstream
|
||||||
@@ -149,8 +147,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
emitted.push(
|
emitted.push(
|
||||||
await write({
|
await emit({
|
||||||
ctx,
|
|
||||||
content: JSON.stringify(simplifiedIndex),
|
content: JSON.stringify(simplifiedIndex),
|
||||||
slug: fp,
|
slug: fp,
|
||||||
ext: ".json",
|
ext: ".json",
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { FilePath, pathToRoot } from "../../util/path"
|
|||||||
import { defaultContentPageLayout, sharedPageComponents } from "../../../quartz.layout"
|
import { defaultContentPageLayout, sharedPageComponents } from "../../../quartz.layout"
|
||||||
import { Content } from "../../components"
|
import { Content } from "../../components"
|
||||||
import chalk from "chalk"
|
import chalk from "chalk"
|
||||||
import { write } from "./helpers"
|
|
||||||
|
|
||||||
export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
|
export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
|
||||||
const opts: FullPageLayout = {
|
const opts: FullPageLayout = {
|
||||||
@@ -27,7 +26,7 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
|
|||||||
getQuartzComponents() {
|
getQuartzComponents() {
|
||||||
return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer]
|
return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer]
|
||||||
},
|
},
|
||||||
async emit(ctx, content, resources): Promise<FilePath[]> {
|
async emit(ctx, content, resources, emit): Promise<FilePath[]> {
|
||||||
const cfg = ctx.cfg.configuration
|
const cfg = ctx.cfg.configuration
|
||||||
const fps: FilePath[] = []
|
const fps: FilePath[] = []
|
||||||
const allFiles = content.map((c) => c[1].data)
|
const allFiles = content.map((c) => c[1].data)
|
||||||
@@ -50,8 +49,7 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
|
|||||||
}
|
}
|
||||||
|
|
||||||
const content = renderPage(slug, componentData, opts, externalResources)
|
const content = renderPage(slug, componentData, opts, externalResources)
|
||||||
const fp = await write({
|
const fp = await emit({
|
||||||
ctx,
|
|
||||||
content,
|
content,
|
||||||
slug,
|
slug,
|
||||||
ext: ".html",
|
ext: ".html",
|
||||||
|
|||||||
@@ -17,9 +17,8 @@ import {
|
|||||||
} from "../../util/path"
|
} from "../../util/path"
|
||||||
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
|
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
|
||||||
import { FolderContent } from "../../components"
|
import { FolderContent } from "../../components"
|
||||||
import { write } from "./helpers"
|
|
||||||
|
|
||||||
export const FolderPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
|
export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
|
||||||
const opts: FullPageLayout = {
|
const opts: FullPageLayout = {
|
||||||
...sharedPageComponents,
|
...sharedPageComponents,
|
||||||
...defaultListPageLayout,
|
...defaultListPageLayout,
|
||||||
@@ -36,7 +35,7 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpt
|
|||||||
getQuartzComponents() {
|
getQuartzComponents() {
|
||||||
return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer]
|
return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer]
|
||||||
},
|
},
|
||||||
async emit(ctx, content, resources): Promise<FilePath[]> {
|
async emit(ctx, content, resources, emit): Promise<FilePath[]> {
|
||||||
const fps: FilePath[] = []
|
const fps: FilePath[] = []
|
||||||
const allFiles = content.map((c) => c[1].data)
|
const allFiles = content.map((c) => c[1].data)
|
||||||
const cfg = ctx.cfg.configuration
|
const cfg = ctx.cfg.configuration
|
||||||
@@ -83,8 +82,7 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpt
|
|||||||
}
|
}
|
||||||
|
|
||||||
const content = renderPage(slug, componentData, opts, externalResources)
|
const content = renderPage(slug, componentData, opts, externalResources)
|
||||||
const fp = await write({
|
const fp = await emit({
|
||||||
ctx,
|
|
||||||
content,
|
content,
|
||||||
slug,
|
slug,
|
||||||
ext: ".html",
|
ext: ".html",
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import path from "path"
|
|
||||||
import fs from "fs"
|
|
||||||
import { BuildCtx } from "../../util/ctx"
|
|
||||||
import { FilePath, FullSlug, joinSegments } from "../../util/path"
|
|
||||||
|
|
||||||
type WriteOptions = {
|
|
||||||
ctx: BuildCtx
|
|
||||||
slug: FullSlug
|
|
||||||
ext: `.${string}` | ""
|
|
||||||
content: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const write = async ({ ctx, slug, ext, content }: WriteOptions): Promise<FilePath> => {
|
|
||||||
const pathToPage = joinSegments(ctx.argv.output, slug + ext) as FilePath
|
|
||||||
const dir = path.dirname(pathToPage)
|
|
||||||
await fs.promises.mkdir(dir, { recursive: true })
|
|
||||||
await fs.promises.writeFile(pathToPage, content)
|
|
||||||
return pathToPage
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ export const Static: QuartzEmitterPlugin = () => ({
|
|||||||
getQuartzComponents() {
|
getQuartzComponents() {
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
async emit({ argv, cfg }, _content, _resources): Promise<FilePath[]> {
|
async emit({ argv, cfg }, _content, _resources, _emit): Promise<FilePath[]> {
|
||||||
const staticPath = joinSegments(QUARTZ, "static")
|
const staticPath = joinSegments(QUARTZ, "static")
|
||||||
const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns)
|
const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns)
|
||||||
await fs.promises.cp(staticPath, joinSegments(argv.output, "static"), {
|
await fs.promises.cp(staticPath, joinSegments(argv.output, "static"), {
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ import {
|
|||||||
} from "../../util/path"
|
} from "../../util/path"
|
||||||
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
|
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
|
||||||
import { TagContent } from "../../components"
|
import { TagContent } from "../../components"
|
||||||
import { write } from "./helpers"
|
|
||||||
|
|
||||||
export const TagPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
|
export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
|
||||||
const opts: FullPageLayout = {
|
const opts: FullPageLayout = {
|
||||||
...sharedPageComponents,
|
...sharedPageComponents,
|
||||||
...defaultListPageLayout,
|
...defaultListPageLayout,
|
||||||
@@ -33,7 +32,7 @@ export const TagPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts)
|
|||||||
getQuartzComponents() {
|
getQuartzComponents() {
|
||||||
return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer]
|
return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer]
|
||||||
},
|
},
|
||||||
async emit(ctx, content, resources): Promise<FilePath[]> {
|
async emit(ctx, content, resources, emit): Promise<FilePath[]> {
|
||||||
const fps: FilePath[] = []
|
const fps: FilePath[] = []
|
||||||
const allFiles = content.map((c) => c[1].data)
|
const allFiles = content.map((c) => c[1].data)
|
||||||
const cfg = ctx.cfg.configuration
|
const cfg = ctx.cfg.configuration
|
||||||
@@ -82,8 +81,7 @@ export const TagPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const content = renderPage(slug, componentData, opts, externalResources)
|
const content = renderPage(slug, componentData, opts, externalResources)
|
||||||
const fp = await write({
|
const fp = await emit({
|
||||||
ctx,
|
|
||||||
content,
|
content,
|
||||||
slug: file.data.slug!,
|
slug: file.data.slug!,
|
||||||
ext: ".html",
|
ext: ".html",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { QuartzFilterPlugin } from "../types"
|
|||||||
export const ExplicitPublish: QuartzFilterPlugin = () => ({
|
export const ExplicitPublish: QuartzFilterPlugin = () => ({
|
||||||
name: "ExplicitPublish",
|
name: "ExplicitPublish",
|
||||||
shouldPublish(_ctx, [_tree, vfile]) {
|
shouldPublish(_ctx, [_tree, vfile]) {
|
||||||
return vfile.data?.frontmatter?.publish ?? false
|
const publishFlag: boolean = vfile.data?.frontmatter?.publish ?? false
|
||||||
|
return publishFlag
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -30,6 +30,5 @@ declare module "vfile" {
|
|||||||
interface DataMap {
|
interface DataMap {
|
||||||
slug: FullSlug
|
slug: FullSlug
|
||||||
filePath: FilePath
|
filePath: FilePath
|
||||||
relativePath: FilePath
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,34 +9,13 @@ 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: ",",
|
||||||
|
|
||||||
function coalesceAliases(data: { [key: string]: any }, aliases: string[]) {
|
|
||||||
for (const alias of aliases) {
|
|
||||||
if (data[alias] !== undefined && data[alias] !== null) return data[alias]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function coerceToArray(input: string | string[]): string[] | undefined {
|
|
||||||
if (input === undefined || input === null) return undefined
|
|
||||||
|
|
||||||
// coerce to array
|
|
||||||
if (!Array.isArray(input)) {
|
|
||||||
input = input
|
|
||||||
.toString()
|
|
||||||
.split(",")
|
|
||||||
.map((tag: string) => tag.trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove all non-strings
|
|
||||||
return input
|
|
||||||
.filter((tag: unknown) => typeof tag === "string" || typeof tag === "number")
|
|
||||||
.map((tag: string | number) => tag.toString())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
||||||
@@ -44,6 +23,8 @@ 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"]],
|
||||||
() => {
|
() => {
|
||||||
@@ -56,19 +37,27 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined>
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// tag is an alias for tags
|
||||||
|
if (data.tag) {
|
||||||
|
data.tags = data.tag
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
} else if (data.title === null || data.title === undefined) {
|
||||||
data.title = file.stem ?? "Untitled"
|
data.title = file.stem ?? "Untitled"
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = coerceToArray(coalesceAliases(data, ["tags", "tag"]))
|
if (data.tags && !Array.isArray(data.tags)) {
|
||||||
if (tags) data.tags = [...new Set(tags.map((tag: string) => slugTag(tag)))]
|
data.tags = data.tags
|
||||||
|
.toString()
|
||||||
|
.split(oneLineTagDelim)
|
||||||
|
.map((tag: string) => tag.trim())
|
||||||
|
}
|
||||||
|
|
||||||
const aliases = coerceToArray(coalesceAliases(data, ["aliases", "alias"]))
|
// slug them all!!
|
||||||
if (aliases) data.aliases = aliases
|
data.tags = [...new Set(data.tags?.map((tag: string) => slugTag(tag)))]
|
||||||
const cssclasses = coerceToArray(coalesceAliases(data, ["cssclasses", "cssclass"]))
|
|
||||||
if (cssclasses) data.cssclasses = cssclasses
|
|
||||||
|
|
||||||
// fill in frontmatter
|
// fill in frontmatter
|
||||||
file.data.frontmatter = data as QuartzPluginData["frontmatter"]
|
file.data.frontmatter = data as QuartzPluginData["frontmatter"]
|
||||||
@@ -81,16 +70,9 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined>
|
|||||||
|
|
||||||
declare module "vfile" {
|
declare module "vfile" {
|
||||||
interface DataMap {
|
interface DataMap {
|
||||||
frontmatter: { [key: string]: unknown } & {
|
frontmatter: { [key: string]: any } & {
|
||||||
title: string
|
title: string
|
||||||
} & Partial<{
|
|
||||||
tags: string[]
|
tags: string[]
|
||||||
aliases: string[]
|
}
|
||||||
description: string
|
|
||||||
publish: boolean
|
|
||||||
draft: boolean
|
|
||||||
enableToc: string
|
|
||||||
cssclasses: string[]
|
|
||||||
}>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,36 +37,8 @@ export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> |
|
|||||||
"data-no-popover": true,
|
"data-no-popover": true,
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
type: "element",
|
type: "text",
|
||||||
tagName: "svg",
|
value: " §",
|
||||||
properties: {
|
|
||||||
width: 18,
|
|
||||||
height: 18,
|
|
||||||
viewBox: "0 0 24 24",
|
|
||||||
fill: "none",
|
|
||||||
stroke: "currentColor",
|
|
||||||
"stroke-width": "2",
|
|
||||||
"stroke-linecap": "round",
|
|
||||||
"stroke-linejoin": "round",
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: "element",
|
|
||||||
tagName: "path",
|
|
||||||
properties: {
|
|
||||||
d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71",
|
|
||||||
},
|
|
||||||
children: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "element",
|
|
||||||
tagName: "path",
|
|
||||||
properties: {
|
|
||||||
d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71",
|
|
||||||
},
|
|
||||||
children: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -50,29 +50,17 @@ export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | und
|
|||||||
created ||= st.birthtimeMs
|
created ||= st.birthtimeMs
|
||||||
modified ||= st.mtimeMs
|
modified ||= st.mtimeMs
|
||||||
} else if (source === "frontmatter" && file.data.frontmatter) {
|
} else if (source === "frontmatter" && file.data.frontmatter) {
|
||||||
created ||= file.data.frontmatter.date as MaybeDate
|
created ||= file.data.frontmatter.date
|
||||||
modified ||= file.data.frontmatter.lastmod as MaybeDate
|
modified ||= file.data.frontmatter.lastmod
|
||||||
modified ||= file.data.frontmatter.updated as MaybeDate
|
modified ||= file.data.frontmatter.updated
|
||||||
modified ||= file.data.frontmatter["last-modified"] as MaybeDate
|
modified ||= file.data.frontmatter["last-modified"]
|
||||||
published ||= file.data.frontmatter.publishDate as MaybeDate
|
published ||= file.data.frontmatter.publishDate
|
||||||
} else if (source === "git") {
|
} else if (source === "git") {
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
// Get a reference to the main git repo.
|
repo = new Repository(file.cwd)
|
||||||
// It's either the same as the workdir,
|
|
||||||
// or 1+ level higher in case of a submodule/subtree setup
|
|
||||||
repo = Repository.discover(file.cwd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
modified ||= await repo.getFileLatestModifiedDateAsync(file.data.filePath!)
|
modified ||= await repo.getFileLatestModifiedDateAsync(file.data.filePath!)
|
||||||
} catch {
|
|
||||||
console.log(
|
|
||||||
chalk.yellow(
|
|
||||||
`\nWarning: ${file.data
|
|
||||||
.filePath!} isn't yet tracked by git, last modification date is not available for this file`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ export const Latex: QuartzTransformerPlugin<Options> = (opts?: Options) => {
|
|||||||
return {
|
return {
|
||||||
css: [
|
css: [
|
||||||
// base css
|
// base css
|
||||||
"https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css",
|
"https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css",
|
||||||
],
|
],
|
||||||
js: [
|
js: [
|
||||||
{
|
{
|
||||||
// fix copy behaviour: https://github.com/KaTeX/KaTeX/blob/main/contrib/copy-tex/README.md
|
// fix copy behaviour: https://github.com/KaTeX/KaTeX/blob/main/contrib/copy-tex/README.md
|
||||||
src: "https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/copy-tex.min.js",
|
src: "https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/contrib/copy-tex.min.js",
|
||||||
loadTime: "afterDOMReady",
|
loadTime: "afterDOMReady",
|
||||||
contentType: "external",
|
contentType: "external",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ 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 */
|
||||||
@@ -20,16 +19,12 @@ 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
|
|
||||||
externalLinkIcon: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions: Options = {
|
const defaultOptions: Options = {
|
||||||
markdownLinkResolution: "absolute",
|
markdownLinkResolution: "absolute",
|
||||||
prettyLinks: true,
|
prettyLinks: true,
|
||||||
openLinksInNewTab: false,
|
openLinksInNewTab: false,
|
||||||
lazyLoad: false,
|
|
||||||
externalLinkIcon: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
||||||
@@ -39,7 +34,7 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
|
|||||||
htmlPlugins(ctx) {
|
htmlPlugins(ctx) {
|
||||||
return [
|
return [
|
||||||
() => {
|
() => {
|
||||||
return (tree: Root, file) => {
|
return (tree, 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()
|
||||||
|
|
||||||
@@ -56,30 +51,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
|
||||||
const classes = (node.properties.className ?? []) as string[]
|
node.properties.className ??= []
|
||||||
const isExternal = isAbsoluteUrl(dest)
|
node.properties.className.push(isAbsoluteUrl(dest) ? "external" : "internal")
|
||||||
classes.push(isExternal ? "external" : "internal")
|
|
||||||
|
|
||||||
if (isExternal && opts.externalLinkIcon) {
|
|
||||||
node.children.push({
|
|
||||||
type: "element",
|
|
||||||
tagName: "svg",
|
|
||||||
properties: {
|
|
||||||
class: "external-icon",
|
|
||||||
viewBox: "0 0 512 512",
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: "element",
|
|
||||||
tagName: "path",
|
|
||||||
properties: {
|
|
||||||
d: "M320 0H288V64h32 82.7L201.4 265.4 178.7 288 224 333.3l22.6-22.6L448 109.3V192v32h64V192 32 0H480 320zM32 32H0V64 480v32H32 456h32V480 352 320H424v32 96H64V96h96 32V32H160 32z",
|
|
||||||
},
|
|
||||||
children: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the link has alias text
|
// Check if the link has alias text
|
||||||
if (
|
if (
|
||||||
@@ -88,9 +61,8 @@ 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
|
||||||
classes.push("alias")
|
node.properties.className.push("alias")
|
||||||
}
|
}
|
||||||
node.properties.className = classes
|
|
||||||
|
|
||||||
if (opts.openLinksInNewTab) {
|
if (opts.openLinksInNewTab) {
|
||||||
node.properties.target = "_blank"
|
node.properties.target = "_blank"
|
||||||
@@ -139,10 +111,6 @@ 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,10 +1,10 @@
|
|||||||
import { QuartzTransformerPlugin } from "../types"
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
import { Root, Html, BlockContent, DefinitionContent, Paragraph, Code } 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 { ReplaceFunction, 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 { SKIP, visit } from "unist-util-visit"
|
import { visit } from "unist-util-visit"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { JSResource } from "../../util/resources"
|
import { JSResource } from "../../util/resources"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -23,11 +23,8 @@ export interface Options {
|
|||||||
callouts: boolean
|
callouts: boolean
|
||||||
mermaid: boolean
|
mermaid: boolean
|
||||||
parseTags: boolean
|
parseTags: boolean
|
||||||
parseArrows: boolean
|
|
||||||
parseBlockReferences: boolean
|
parseBlockReferences: boolean
|
||||||
enableInHtmlEmbed: boolean
|
enableInHtmlEmbed: boolean
|
||||||
enableYouTubeEmbed: boolean
|
|
||||||
enableVideoEmbed: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions: Options = {
|
const defaultOptions: Options = {
|
||||||
@@ -37,14 +34,43 @@ const defaultOptions: Options = {
|
|||||||
callouts: true,
|
callouts: true,
|
||||||
mermaid: true,
|
mermaid: true,
|
||||||
parseTags: true,
|
parseTags: true,
|
||||||
parseArrows: true,
|
|
||||||
parseBlockReferences: true,
|
parseBlockReferences: true,
|
||||||
enableInHtmlEmbed: false,
|
enableInHtmlEmbed: false,
|
||||||
enableYouTubeEmbed: true,
|
|
||||||
enableVideoEmbed: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const calloutMapping = {
|
const icons = {
|
||||||
|
infoIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>`,
|
||||||
|
pencilIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="2" x2="22" y2="6"></line><path d="M7.5 20.5 19 9l-4-4L3.5 16.5 2 22z"></path></svg>`,
|
||||||
|
clipboardListIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><path d="M12 11h4"></path><path d="M12 16h4"></path><path d="M8 11h.01"></path><path d="M8 16h.01"></path></svg>`,
|
||||||
|
checkCircleIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"></path><path d="m9 12 2 2 4-4"></path></svg>`,
|
||||||
|
flameIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"></path></svg>`,
|
||||||
|
checkIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>`,
|
||||||
|
helpCircleIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>`,
|
||||||
|
alertTriangleIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>`,
|
||||||
|
xIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`,
|
||||||
|
zapIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>`,
|
||||||
|
bugIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="8" height="14" x="8" y="6" rx="4"></rect><path d="m19 7-3 2"></path><path d="m5 7 3 2"></path><path d="m19 19-3-2"></path><path d="m5 19 3-2"></path><path d="M20 13h-4"></path><path d="M4 13h4"></path><path d="m10 4 1 2"></path><path d="m14 4-1 2"></path></svg>`,
|
||||||
|
listIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>`,
|
||||||
|
quoteIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"></path><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"></path></svg>`,
|
||||||
|
}
|
||||||
|
|
||||||
|
const callouts = {
|
||||||
|
note: icons.pencilIcon,
|
||||||
|
abstract: icons.clipboardListIcon,
|
||||||
|
info: icons.infoIcon,
|
||||||
|
todo: icons.checkCircleIcon,
|
||||||
|
tip: icons.flameIcon,
|
||||||
|
success: icons.checkIcon,
|
||||||
|
question: icons.helpCircleIcon,
|
||||||
|
warning: icons.alertTriangleIcon,
|
||||||
|
failure: icons.xIcon,
|
||||||
|
danger: icons.zapIcon,
|
||||||
|
bug: icons.bugIcon,
|
||||||
|
example: icons.listIcon,
|
||||||
|
quote: icons.quoteIcon,
|
||||||
|
}
|
||||||
|
|
||||||
|
const calloutMapping: Record<string, keyof typeof callouts> = {
|
||||||
note: "note",
|
note: "note",
|
||||||
abstract: "abstract",
|
abstract: "abstract",
|
||||||
summary: "abstract",
|
summary: "abstract",
|
||||||
@@ -72,29 +98,24 @@ const calloutMapping = {
|
|||||||
example: "example",
|
example: "example",
|
||||||
quote: "quote",
|
quote: "quote",
|
||||||
cite: "quote",
|
cite: "quote",
|
||||||
} as const
|
|
||||||
|
|
||||||
function canonicalizeCallout(calloutName: string): keyof typeof calloutMapping {
|
|
||||||
const normalizedCallout = calloutName.toLowerCase() as keyof typeof calloutMapping
|
|
||||||
// if callout is not recognized, make it a custom one
|
|
||||||
return calloutMapping[normalizedCallout] ?? calloutName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const externalLinkRegex = /^https?:\/\//i
|
function canonicalizeCallout(calloutName: string): keyof typeof callouts {
|
||||||
|
let callout = calloutName.toLowerCase() as keyof typeof calloutMapping
|
||||||
export const arrowRegex = new RegExp(/-{1,2}>/, "g")
|
return calloutMapping[callout] ?? "note"
|
||||||
|
}
|
||||||
|
|
||||||
// !? -> optional embedding
|
// !? -> optional embedding
|
||||||
// \[\[ -> open brace
|
// \[\[ -> open brace
|
||||||
// ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name)
|
// ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name)
|
||||||
// (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link)
|
// (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link)
|
||||||
// (\|[^\[\]\#]+)? -> | then one or more non-special characters (alias)
|
// (|[^\[\]\|\#]+)? -> | then one or more non-special characters (alias)
|
||||||
export const wikilinkRegex = new RegExp(
|
export const wikilinkRegex = new RegExp(
|
||||||
/!?\[\[([^\[\]\|\#]+)?(#+[^\[\]\|\#]+)?(\|[^\[\]\#]+)?\]\]/,
|
/!?\[\[([^\[\]\|\#]+)?(#+[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/,
|
||||||
"g",
|
"g",
|
||||||
)
|
)
|
||||||
const highlightRegex = new RegExp(/==([^=]+)==/, "g")
|
const highlightRegex = new RegExp(/==([^=]+)==/, "g")
|
||||||
const commentRegex = new RegExp(/%%[\s\S]*?%%/, "g")
|
const commentRegex = new RegExp(/%%(.+)%%/, "g")
|
||||||
// from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts
|
// from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts
|
||||||
const calloutRegex = new RegExp(/^\[\!(\w+)\]([+-]?)/)
|
const calloutRegex = new RegExp(/^\[\!(\w+)\]([+-]?)/)
|
||||||
const calloutLineRegex = new RegExp(/^> *\[\!\w+\][+-]?.*$/, "gm")
|
const calloutLineRegex = new RegExp(/^> *\[\!\w+\][+-]?.*$/, "gm")
|
||||||
@@ -102,13 +123,8 @@ const calloutLineRegex = new RegExp(/^> *\[\!\w+\][+-]?.*$/, "gm")
|
|||||||
// #(...) -> capturing group, tag itself must start with #
|
// #(...) -> capturing group, tag itself must start with #
|
||||||
// (?:[-_\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}\d\p{Z}])+ -> non-capturing group, non-empty string of (Unicode-aware) alpha-numeric characters and symbols, hyphens and/or underscores
|
||||||
// (?:\/[-_\p{L}\d\p{Z}]+)*) -> 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}\p{Emoji}\d])+(?:\/[-_\p{L}\p{Emoji}\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")
|
||||||
const ytLinkRegex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/
|
|
||||||
const videoExtensionRegex = new RegExp(/\.(mp4|webm|ogg|avi|mov|flv|wmv|mkv|mpg|mpeg|3gp|m4v)$/)
|
|
||||||
const wikilinkImageEmbedRegex = new RegExp(
|
|
||||||
/^(?<alt>(?!^\d*x?\d*$).*?)?(\|?\s*?(?<width>\d+)(x(?<height>\d+))?)?$/,
|
|
||||||
)
|
|
||||||
|
|
||||||
export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (
|
export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (
|
||||||
userOpts,
|
userOpts,
|
||||||
@@ -123,22 +139,13 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
return {
|
return {
|
||||||
name: "ObsidianFlavoredMarkdown",
|
name: "ObsidianFlavoredMarkdown",
|
||||||
textTransform(_ctx, src) {
|
textTransform(_ctx, src) {
|
||||||
// do comments at text level
|
|
||||||
if (opts.comments) {
|
|
||||||
if (src instanceof Buffer) {
|
|
||||||
src = src.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
src = src.replace(commentRegex, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// pre-transform blockquotes
|
// pre-transform blockquotes
|
||||||
if (opts.callouts) {
|
if (opts.callouts) {
|
||||||
if (src instanceof Buffer) {
|
if (src instanceof Buffer) {
|
||||||
src = src.toString()
|
src = src.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
src = src.replace(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> "
|
||||||
})
|
})
|
||||||
@@ -150,20 +157,14 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
src = src.toString()
|
src = src.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
src = src.replace(wikilinkRegex, (value, ...capture) => {
|
src = src.replaceAll(wikilinkRegex, (value, ...capture) => {
|
||||||
const [rawFp, rawHeader, rawAlias]: (string | undefined)[] = capture
|
const [rawFp, rawHeader, rawAlias] = 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}]]`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -192,11 +193,11 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
if (value.startsWith("!")) {
|
if (value.startsWith("!")) {
|
||||||
const ext: string = path.extname(fp).toLowerCase()
|
const ext: string = path.extname(fp).toLowerCase()
|
||||||
const url = slugifyFilePath(fp as FilePath)
|
const url = slugifyFilePath(fp as FilePath)
|
||||||
if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg", ".webp"].includes(ext)) {
|
if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg"].includes(ext)) {
|
||||||
const match = wikilinkImageEmbedRegex.exec(alias ?? "")
|
const dims = alias ?? ""
|
||||||
const alt = match?.groups?.alt ?? ""
|
let [width, height] = dims.split("x", 2)
|
||||||
const width = match?.groups?.width ?? "auto"
|
width ||= "auto"
|
||||||
const height = match?.groups?.height ?? "auto"
|
height ||= "auto"
|
||||||
return {
|
return {
|
||||||
type: "image",
|
type: "image",
|
||||||
url,
|
url,
|
||||||
@@ -204,7 +205,6 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
hProperties: {
|
hProperties: {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
alt,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -225,7 +225,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
type: "html",
|
type: "html",
|
||||||
value: `<iframe src="${url}"></iframe>`,
|
value: `<iframe src="${url}"></iframe>`,
|
||||||
}
|
}
|
||||||
} else {
|
} else if (ext === "") {
|
||||||
const block = anchor
|
const block = anchor
|
||||||
return {
|
return {
|
||||||
type: "html",
|
type: "html",
|
||||||
@@ -268,13 +268,13 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.parseArrows) {
|
if (opts.comments) {
|
||||||
replacements.push([
|
replacements.push([
|
||||||
arrowRegex,
|
commentRegex,
|
||||||
(_value: string, ..._capture: string[]) => {
|
(_value: string, ..._capture: string[]) => {
|
||||||
return {
|
return {
|
||||||
type: "html",
|
type: "text",
|
||||||
value: `<span>→</span>`,
|
value: "",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@@ -290,9 +290,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
}
|
}
|
||||||
|
|
||||||
tag = slugTag(tag)
|
tag = slugTag(tag)
|
||||||
if (file.data.frontmatter) {
|
if (file.data.frontmatter && !file.data.frontmatter.tags.includes(tag)) {
|
||||||
const noteTags = file.data.frontmatter.tags ?? []
|
file.data.frontmatter.tags.push(tag)
|
||||||
file.data.frontmatter.tags = [...new Set([...noteTags, tag])]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -320,7 +319,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
if (typeof replace === "string") {
|
if (typeof replace === "string") {
|
||||||
node.value = node.value.replace(regex, replace)
|
node.value = node.value.replace(regex, replace)
|
||||||
} else {
|
} else {
|
||||||
node.value = node.value.replace(regex, (substring: string, ...args) => {
|
node.value = node.value.replaceAll(regex, (substring: string, ...args) => {
|
||||||
const replaceValue = replace(substring, ...args)
|
const replaceValue = replace(substring, ...args)
|
||||||
if (typeof replaceValue === "string") {
|
if (typeof replaceValue === "string") {
|
||||||
return replaceValue
|
return replaceValue
|
||||||
@@ -336,28 +335,11 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
mdastFindReplace(tree, replacements)
|
mdastFindReplace(tree, replacements)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (opts.enableVideoEmbed) {
|
|
||||||
plugins.push(() => {
|
|
||||||
return (tree: Root, _file) => {
|
|
||||||
visit(tree, "image", (node, index, parent) => {
|
|
||||||
if (parent && index != undefined && videoExtensionRegex.test(node.url)) {
|
|
||||||
const newNode: Html = {
|
|
||||||
type: "html",
|
|
||||||
value: `<video controls src="${node.url}"></video>`,
|
|
||||||
}
|
|
||||||
|
|
||||||
parent.children.splice(index, 1, newNode)
|
|
||||||
return SKIP
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.callouts) {
|
if (opts.callouts) {
|
||||||
plugins.push(() => {
|
plugins.push(() => {
|
||||||
return (tree: Root, _file) => {
|
return (tree: Root, _file) => {
|
||||||
@@ -373,35 +355,36 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const text = firstChild.children[0].value
|
const text = firstChild.children[0].value
|
||||||
const restOfTitle = firstChild.children.slice(1)
|
const restChildren = firstChild.children.slice(1)
|
||||||
const [firstLine, ...remainingLines] = text.split("\n")
|
const [firstLine, ...remainingLines] = text.split("\n")
|
||||||
const remainingText = remainingLines.join("\n")
|
const remainingText = remainingLines.join("\n")
|
||||||
|
|
||||||
const match = firstLine.match(calloutRegex)
|
const match = firstLine.match(calloutRegex)
|
||||||
if (match && match.input) {
|
if (match && match.input) {
|
||||||
const [calloutDirective, typeString, collapseChar] = match
|
const [calloutDirective, typeString, collapseChar] = match
|
||||||
const calloutType = canonicalizeCallout(typeString.toLowerCase())
|
const calloutType = canonicalizeCallout(
|
||||||
|
typeString.toLowerCase() as keyof typeof calloutMapping,
|
||||||
|
)
|
||||||
const collapse = collapseChar === "+" || collapseChar === "-"
|
const collapse = collapseChar === "+" || collapseChar === "-"
|
||||||
const defaultState = collapseChar === "-" ? "collapsed" : "expanded"
|
const defaultState = collapseChar === "-" ? "collapsed" : "expanded"
|
||||||
const titleContent =
|
const titleContent =
|
||||||
match.input.slice(calloutDirective.length).trim() || capitalize(calloutType)
|
match.input.slice(calloutDirective.length).trim() || capitalize(calloutType)
|
||||||
const titleNode: Paragraph = {
|
const titleNode: Paragraph = {
|
||||||
type: "paragraph",
|
type: "paragraph",
|
||||||
children:
|
children: [{ type: "text", value: titleContent + " " }, ...restChildren],
|
||||||
restOfTitle.length === 0
|
|
||||||
? [{ type: "text", value: titleContent + " " }]
|
|
||||||
: restOfTitle,
|
|
||||||
}
|
}
|
||||||
const title = mdastToHtml(titleNode)
|
const title = mdastToHtml(titleNode)
|
||||||
|
|
||||||
const toggleIcon = `<div class="fold-callout-icon"></div>`
|
const toggleIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="fold">
|
||||||
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
|
</svg>`
|
||||||
|
|
||||||
const titleHtml: Html = {
|
const titleHtml: Html = {
|
||||||
type: "html",
|
type: "html",
|
||||||
value: `<div
|
value: `<div
|
||||||
class="callout-title"
|
class="callout-title"
|
||||||
>
|
>
|
||||||
<div class="callout-icon"></div>
|
<div class="callout-icon">${callouts[calloutType]}</div>
|
||||||
<div class="callout-title-inner">${title}</div>
|
<div class="callout-title-inner">${title}</div>
|
||||||
${collapse ? toggleIcon : ""}
|
${collapse ? toggleIcon : ""}
|
||||||
</div>`,
|
</div>`,
|
||||||
@@ -427,7 +410,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
node.data = {
|
node.data = {
|
||||||
hProperties: {
|
hProperties: {
|
||||||
...(node.data?.hProperties ?? {}),
|
...(node.data?.hProperties ?? {}),
|
||||||
className: `callout ${calloutType} ${collapse ? "is-collapsible" : ""} ${
|
className: `callout ${collapse ? "is-collapsible" : ""} ${
|
||||||
defaultState === "collapsed" ? "is-collapsed" : ""
|
defaultState === "collapsed" ? "is-collapsed" : ""
|
||||||
}`,
|
}`,
|
||||||
"data-callout": calloutType,
|
"data-callout": calloutType,
|
||||||
@@ -460,12 +443,11 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
},
|
},
|
||||||
htmlPlugins() {
|
htmlPlugins() {
|
||||||
const plugins: PluggableList = [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: HtmlRoot, file) => {
|
return (tree, file) => {
|
||||||
file.data.blocks = {}
|
file.data.blocks = {}
|
||||||
|
|
||||||
visit(tree, "element", (node, index, parent) => {
|
visit(tree, "element", (node, index, parent) => {
|
||||||
@@ -514,30 +496,6 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.enableYouTubeEmbed) {
|
|
||||||
plugins.push(() => {
|
|
||||||
return (tree: HtmlRoot) => {
|
|
||||||
visit(tree, "element", (node) => {
|
|
||||||
if (node.tagName === "img" && typeof node.properties.src === "string") {
|
|
||||||
const match = node.properties.src.match(ytLinkRegex)
|
|
||||||
const videoId = match && match[2].length == 11 ? match[2] : null
|
|
||||||
if (videoId) {
|
|
||||||
node.tagName = "iframe"
|
|
||||||
node.properties = {
|
|
||||||
class: "external-embed",
|
|
||||||
allow: "fullscreen",
|
|
||||||
frameborder: 0,
|
|
||||||
width: "600px",
|
|
||||||
height: "350px",
|
|
||||||
src: `https://www.youtube.com/embed/${videoId}`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return plugins
|
return plugins
|
||||||
},
|
},
|
||||||
externalResources() {
|
externalResources() {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { PluggableList } from "unified"
|
|||||||
import { StaticResources } from "../util/resources"
|
import { StaticResources } from "../util/resources"
|
||||||
import { ProcessedContent } from "./vfile"
|
import { ProcessedContent } from "./vfile"
|
||||||
import { QuartzComponent } from "../components/types"
|
import { QuartzComponent } from "../components/types"
|
||||||
import { FilePath } from "../util/path"
|
import { FilePath, FullSlug } from "../util/path"
|
||||||
import { BuildCtx } from "../util/ctx"
|
import { BuildCtx } from "../util/ctx"
|
||||||
|
|
||||||
export interface PluginTypes {
|
export interface PluginTypes {
|
||||||
@@ -36,6 +36,19 @@ export type QuartzEmitterPlugin<Options extends OptionType = undefined> = (
|
|||||||
) => QuartzEmitterPluginInstance
|
) => QuartzEmitterPluginInstance
|
||||||
export type QuartzEmitterPluginInstance = {
|
export type QuartzEmitterPluginInstance = {
|
||||||
name: string
|
name: string
|
||||||
emit(ctx: BuildCtx, content: ProcessedContent[], resources: StaticResources): Promise<FilePath[]>
|
emit(
|
||||||
|
ctx: BuildCtx,
|
||||||
|
content: ProcessedContent[],
|
||||||
|
resources: StaticResources,
|
||||||
|
emitCallback: EmitCallback,
|
||||||
|
): Promise<FilePath[]>
|
||||||
getQuartzComponents(ctx: BuildCtx): QuartzComponent[]
|
getQuartzComponents(ctx: BuildCtx): QuartzComponent[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EmitOptions {
|
||||||
|
slug: FullSlug
|
||||||
|
ext: `.${string}` | ""
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EmitCallback = (data: EmitOptions) => Promise<FilePath>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
import path from "path"
|
||||||
|
import fs from "fs"
|
||||||
import { PerfTimer } from "../util/perf"
|
import { PerfTimer } from "../util/perf"
|
||||||
import { getStaticResourcesFromPlugins } from "../plugins"
|
import { getStaticResourcesFromPlugins } from "../plugins"
|
||||||
|
import { EmitCallback } from "../plugins/types"
|
||||||
import { ProcessedContent } from "../plugins/vfile"
|
import { ProcessedContent } from "../plugins/vfile"
|
||||||
|
import { FilePath, joinSegments } from "../util/path"
|
||||||
import { QuartzLogger } from "../util/log"
|
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"
|
||||||
@@ -11,12 +15,19 @@ export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) {
|
|||||||
const log = new QuartzLogger(ctx.argv.verbose)
|
const log = new QuartzLogger(ctx.argv.verbose)
|
||||||
|
|
||||||
log.start(`Emitting output files`)
|
log.start(`Emitting output files`)
|
||||||
|
const emit: EmitCallback = async ({ slug, ext, content }) => {
|
||||||
|
const pathToPage = joinSegments(argv.output, slug + ext) as FilePath
|
||||||
|
const dir = path.dirname(pathToPage)
|
||||||
|
await fs.promises.mkdir(dir, { recursive: true })
|
||||||
|
await fs.promises.writeFile(pathToPage, content)
|
||||||
|
return pathToPage
|
||||||
|
}
|
||||||
|
|
||||||
let emittedFiles = 0
|
let emittedFiles = 0
|
||||||
const staticResources = getStaticResourcesFromPlugins(ctx)
|
const staticResources = getStaticResourcesFromPlugins(ctx)
|
||||||
for (const emitter of cfg.plugins.emitters) {
|
for (const emitter of cfg.plugins.emitters) {
|
||||||
try {
|
try {
|
||||||
const emitted = await emitter.emit(ctx, content, staticResources)
|
const emitted = await emitter.emit(ctx, content, staticResources, emit)
|
||||||
emittedFiles += emitted.length
|
emittedFiles += emitted.length
|
||||||
|
|
||||||
if (ctx.argv.verbose) {
|
if (ctx.argv.verbose) {
|
||||||
|
|||||||
@@ -91,9 +91,8 @@ export function createFileParser(ctx: BuildCtx, fps: FilePath[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// base data properties that plugins may use
|
// base data properties that plugins may use
|
||||||
file.data.filePath = file.path as FilePath
|
file.data.slug = slugifyFilePath(path.posix.relative(argv.directory, file.path) as FilePath)
|
||||||
file.data.relativePath = path.posix.relative(argv.directory, file.path) as FilePath
|
file.data.filePath = fp
|
||||||
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)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
|
-webkit-text-size-adjust: none;
|
||||||
text-size-adjust: none;
|
text-size-adjust: none;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
@@ -68,7 +69,6 @@ a {
|
|||||||
background-color: var(--highlight);
|
background-color: var(--highlight);
|
||||||
padding: 0 0.1rem;
|
padding: 0 0.1rem;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
line-height: 1.4rem;
|
|
||||||
|
|
||||||
&:has(> img) {
|
&:has(> img) {
|
||||||
background-color: none;
|
background-color: none;
|
||||||
@@ -76,15 +76,6 @@ a {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.external .external-icon {
|
|
||||||
height: 1ex;
|
|
||||||
margin: 0 0.15em;
|
|
||||||
|
|
||||||
> path {
|
|
||||||
fill: var(--dark);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.desktop-only {
|
.desktop-only {
|
||||||
@@ -171,11 +162,9 @@ a {
|
|||||||
|
|
||||||
& .sidebar.right {
|
& .sidebar.right {
|
||||||
right: calc(calc(100vw - $pageWidth) / 2 - $sidePanelWidth);
|
right: calc(calc(100vw - $pageWidth) / 2 - $sidePanelWidth);
|
||||||
flex-wrap: wrap;
|
|
||||||
& > * {
|
& > * {
|
||||||
@media all and (max-width: $fullPageWidth) {
|
@media all and (max-width: $fullPageWidth) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 140px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,6 +267,7 @@ h6 {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.2s ease;
|
||||||
transform: translateY(-0.1rem);
|
transform: translateY(-0.1rem);
|
||||||
|
display: inline-block;
|
||||||
font-family: var(--codeFont);
|
font-family: var(--codeFont);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
@@ -338,11 +328,10 @@ figure[data-rehype-pretty-code-figure] {
|
|||||||
|
|
||||||
pre {
|
pre {
|
||||||
font-family: var(--codeFont);
|
font-family: var(--codeFont);
|
||||||
padding: 0 0.5rem;
|
padding: 0.5rem;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
border: 1px solid var(--lightgray);
|
border: 1px solid var(--lightgray);
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&:has(> code.mermaid) {
|
&:has(> code.mermaid) {
|
||||||
border: none;
|
border: none;
|
||||||
@@ -356,7 +345,6 @@ pre {
|
|||||||
counter-increment: line 0;
|
counter-increment: line 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
overflow-x: scroll;
|
|
||||||
|
|
||||||
& [data-highlighted-chars] {
|
& [data-highlighted-chars] {
|
||||||
background-color: var(--highlight);
|
background-color: var(--highlight);
|
||||||
|
|||||||
@@ -13,33 +13,16 @@
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
--callout-icon-note: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="2" x2="22" y2="6"></line><path d="M7.5 20.5 19 9l-4-4L3.5 16.5 2 22z"></path></svg>');
|
&[data-callout="note"] {
|
||||||
--callout-icon-abstract: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><path d="M12 11h4"></path><path d="M12 16h4"></path><path d="M8 11h.01"></path><path d="M8 16h.01"></path></svg>');
|
|
||||||
--callout-icon-info: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>');
|
|
||||||
--callout-icon-todo: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"></path><path d="m9 12 2 2 4-4"></path></svg>');
|
|
||||||
--callout-icon-tip: url('data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"></path></svg> ');
|
|
||||||
--callout-icon-success: url('data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg> ');
|
|
||||||
--callout-icon-question: url('data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg> ');
|
|
||||||
--callout-icon-warning: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>');
|
|
||||||
--callout-icon-failure: url('data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg> ');
|
|
||||||
--callout-icon-danger: url('data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg> ');
|
|
||||||
--callout-icon-bug: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="8" height="14" x="8" y="6" rx="4"></rect><path d="m19 7-3 2"></path><path d="m5 7 3 2"></path><path d="m19 19-3-2"></path><path d="m5 19 3-2"></path><path d="M20 13h-4"></path><path d="M4 13h4"></path><path d="m10 4 1 2"></path><path d="m14 4-1 2"></path></svg>');
|
|
||||||
--callout-icon-example: url('data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg> ');
|
|
||||||
--callout-icon-quote: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"></path><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"></path></svg>');
|
|
||||||
--callout-icon-fold: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"%3E%3Cpolyline points="6 9 12 15 18 9"%3E%3C/polyline%3E%3C/svg%3E');
|
|
||||||
|
|
||||||
&[data-callout] {
|
|
||||||
--color: #448aff;
|
--color: #448aff;
|
||||||
--border: #448aff44;
|
--border: #448aff44;
|
||||||
--bg: #448aff10;
|
--bg: #448aff10;
|
||||||
--callout-icon: var(--callout-icon-note);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-callout="abstract"] {
|
&[data-callout="abstract"] {
|
||||||
--color: #00b0ff;
|
--color: #00b0ff;
|
||||||
--border: #00b0ff44;
|
--border: #00b0ff44;
|
||||||
--bg: #00b0ff10;
|
--bg: #00b0ff10;
|
||||||
--callout-icon: var(--callout-icon-abstract);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-callout="info"],
|
&[data-callout="info"],
|
||||||
@@ -47,39 +30,30 @@
|
|||||||
--color: #00b8d4;
|
--color: #00b8d4;
|
||||||
--border: #00b8d444;
|
--border: #00b8d444;
|
||||||
--bg: #00b8d410;
|
--bg: #00b8d410;
|
||||||
--callout-icon: var(--callout-icon-info);
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-callout="todo"] {
|
|
||||||
--callout-icon: var(--callout-icon-todo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-callout="tip"] {
|
&[data-callout="tip"] {
|
||||||
--color: #00bfa5;
|
--color: #00bfa5;
|
||||||
--border: #00bfa544;
|
--border: #00bfa544;
|
||||||
--bg: #00bfa510;
|
--bg: #00bfa510;
|
||||||
--callout-icon: var(--callout-icon-tip);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-callout="success"] {
|
&[data-callout="success"] {
|
||||||
--color: #09ad7a;
|
--color: #09ad7a;
|
||||||
--border: #09ad7144;
|
--border: #09ad7144;
|
||||||
--bg: #09ad7110;
|
--bg: #09ad7110;
|
||||||
--callout-icon: var(--callout-icon-success);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-callout="question"] {
|
&[data-callout="question"] {
|
||||||
--color: #dba642;
|
--color: #dba642;
|
||||||
--border: #dba64244;
|
--border: #dba64244;
|
||||||
--bg: #dba64210;
|
--bg: #dba64210;
|
||||||
--callout-icon: var(--callout-icon-question);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-callout="warning"] {
|
&[data-callout="warning"] {
|
||||||
--color: #db8942;
|
--color: #db8942;
|
||||||
--border: #db894244;
|
--border: #db894244;
|
||||||
--bg: #db894210;
|
--bg: #db894210;
|
||||||
--callout-icon: var(--callout-icon-warning);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-callout="failure"],
|
&[data-callout="failure"],
|
||||||
@@ -88,74 +62,50 @@
|
|||||||
--color: #db4242;
|
--color: #db4242;
|
||||||
--border: #db424244;
|
--border: #db424244;
|
||||||
--bg: #db424210;
|
--bg: #db424210;
|
||||||
--callout-icon: var(--callout-icon-failure);
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-callout="bug"] {
|
|
||||||
--callout-icon: var(--callout-icon-bug);
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-callout="danger"] {
|
|
||||||
--callout-icon: var(--callout-icon-danger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-callout="example"] {
|
&[data-callout="example"] {
|
||||||
--color: #7a43b5;
|
--color: #7a43b5;
|
||||||
--border: #7a43b544;
|
--border: #7a43b544;
|
||||||
--bg: #7a43b510;
|
--bg: #7a43b510;
|
||||||
--callout-icon: var(--callout-icon-example);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-callout="quote"] {
|
&[data-callout="quote"] {
|
||||||
--color: var(--secondary);
|
--color: var(--secondary);
|
||||||
--border: var(--lightgray);
|
--border: var(--lightgray);
|
||||||
--callout-icon: var(--callout-icon-quote);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-collapsed > .callout-title > .fold-callout-icon {
|
&.is-collapsed > .callout-title > .fold {
|
||||||
transform: rotateZ(-90deg);
|
transform: rotateZ(-90deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.callout-title {
|
.callout-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
color: var(--color);
|
color: var(--color);
|
||||||
|
|
||||||
--icon-size: 18px;
|
& .fold {
|
||||||
|
margin-left: 0.5rem;
|
||||||
& .fold-callout-icon {
|
transition: transform 0.3s ease;
|
||||||
transition: transform 0.15s ease;
|
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: var(--icon-size);
|
|
||||||
height: var(--icon-size);
|
|
||||||
--callout-icon: var(--callout-icon-fold);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .callout-title-inner > p {
|
& > .callout-title-inner > p {
|
||||||
color: var(--color);
|
color: var(--color);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.callout-icon,
|
.callout-icon {
|
||||||
& .fold-callout-icon {
|
width: 18px;
|
||||||
width: var(--icon-size);
|
height: 18px;
|
||||||
height: var(--icon-size);
|
flex: 0 0 18px;
|
||||||
|
padding-top: 4px;
|
||||||
// icon support
|
|
||||||
background-size: var(--icon-size) var(--icon-size);
|
|
||||||
background-position: center;
|
|
||||||
background-color: var(--color);
|
|
||||||
mask-image: var(--callout-icon);
|
|
||||||
mask-size: var(--icon-size) var(--icon-size);
|
|
||||||
mask-position: center;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.callout-title-inner {
|
.callout-title-inner {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,13 +9,3 @@ export function pluralize(count: number, s: string): string {
|
|||||||
export function capitalize(s: string): string {
|
export function capitalize(s: string): string {
|
||||||
return s.substring(0, 1).toUpperCase() + s.substring(1)
|
return s.substring(0, 1).toUpperCase() + s.substring(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function classNames(
|
|
||||||
displayClass?: "mobile-only" | "desktop-only",
|
|
||||||
...classes: string[]
|
|
||||||
): string {
|
|
||||||
if (displayClass) {
|
|
||||||
classes.push(displayClass)
|
|
||||||
}
|
|
||||||
return classes.join(" ")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -105,9 +105,6 @@ describe("transforms", () => {
|
|||||||
["index.md", "index"],
|
["index.md", "index"],
|
||||||
["test.mp4", "test.mp4"],
|
["test.mp4", "test.mp4"],
|
||||||
["note with spaces.md", "note-with-spaces"],
|
["note with spaces.md", "note-with-spaces"],
|
||||||
["notes.with.dots.md", "notes.with.dots"],
|
|
||||||
["test/special chars?.md", "test/special-chars-q"],
|
|
||||||
["test/special chars #3.md", "test/special-chars-3"],
|
|
||||||
],
|
],
|
||||||
path.slugifyFilePath,
|
path.slugifyFilePath,
|
||||||
path.isFilePath,
|
path.isFilePath,
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { slug as slugAnchor } 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"
|
||||||
@@ -50,9 +46,7 @@ export function getFullSlug(window: Window): FullSlug {
|
|||||||
function sluggify(s: string): string {
|
function sluggify(s: string): string {
|
||||||
return s
|
return s
|
||||||
.split("/")
|
.split("/")
|
||||||
.map((segment) =>
|
.map((segment) => segment.replace(/\s/g, "-").replace(/%/g, "-percent").replace(/\?/g, "-q")) // slugify all segments
|
||||||
segment.replace(/\s/g, "-").replace(/%/g, "-percent").replace(/\?/g, "-q").replace(/#/g, ""),
|
|
||||||
)
|
|
||||||
.join("/") // always use / as sep
|
.join("/") // always use / as sep
|
||||||
.replace(/\/$/, "")
|
.replace(/\/$/, "")
|
||||||
}
|
}
|
||||||
@@ -127,8 +121,7 @@ const _rebaseHastElement = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeHastElement(rawEl: HastElement, curBase: FullSlug, newBase: FullSlug) {
|
export function normalizeHastElement(el: 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) {
|
||||||
|
|||||||
@@ -26,12 +26,9 @@ export function JSResourceToScriptElement(resource: JSResource, preserve?: boole
|
|||||||
} else {
|
} else {
|
||||||
const content = resource.script
|
const content = resource.script
|
||||||
return (
|
return (
|
||||||
<script
|
<script key={randomUUID()} type={scriptType} spa-preserve={spaPreserve}>
|
||||||
key={randomUUID()}
|
{content}
|
||||||
type={scriptType}
|
</script>
|
||||||
spa-preserve={spaPreserve}
|
|
||||||
dangerouslySetInnerHTML={{ __html: content }}
|
|
||||||
></script>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"jsxImportSource": "preact",
|
"jsxImportSource": "preact"
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts", "**/*.tsx", "./package.json"],
|
"include": ["**/*.ts", "**/*.tsx", "./package.json"],
|
||||||
"exclude": ["build/**/*.d.ts"],
|
"exclude": ["build/**/*.d.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user