mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-12-01 02:07:55 +01:00
Compare commits
105 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c891ad8ff5 | ||
|
|
06ee73e006 | ||
|
|
36e4cc41a9 | ||
|
|
dff4b06313 | ||
|
|
5b90fbd0d0 | ||
|
|
dbbc672c67 | ||
|
|
3fb3930df8 | ||
|
|
742b883256 | ||
|
|
9ff1fdd280 | ||
|
|
a2c46f442d | ||
|
|
260498a96b | ||
|
|
0a3379a853 | ||
|
|
bece8fcab6 | ||
|
|
18745a9dc6 | ||
|
|
34a8dfcd55 | ||
|
|
44da82467e | ||
|
|
3231ce6e79 | ||
|
|
a0b927da4a | ||
|
|
5ab922f316 | ||
|
|
d11a0e71a8 | ||
|
|
2b57a68e1f | ||
|
|
18cd58617d | ||
|
|
ee868b2d79 | ||
|
|
5a36e5b68d | ||
|
|
0416c03ae6 | ||
|
|
3b596c9311 | ||
|
|
970a30a139 | ||
|
|
dc62aeb213 | ||
|
|
9b8e0c9d1a | ||
|
|
45b93a80f4 | ||
|
|
e9fb0ecb96 | ||
|
|
c0c0b24138 | ||
|
|
c00089bd57 | ||
|
|
8a6ebd1939 | ||
|
|
f78b512436 | ||
|
|
295b8fc914 | ||
|
|
756acc7f97 | ||
|
|
9aa6a18be2 | ||
|
|
444e05ee21 | ||
|
|
1c175b2d09 | ||
|
|
7b2ce8b4a3 | ||
|
|
f2e93c3314 | ||
|
|
25e6869d38 | ||
|
|
bfd877133b | ||
|
|
422986c98b | ||
|
|
75d64eac91 | ||
|
|
355aa22318 | ||
|
|
7cb1c291c8 | ||
|
|
22de92f6c4 | ||
|
|
e1f12e6cb7 | ||
|
|
50bb1ffd8a | ||
|
|
fee3ef9b3a | ||
|
|
a29fadb046 | ||
|
|
4e5643fb49 | ||
|
|
072ee64127 | ||
|
|
90043cd582 | ||
|
|
e21d50c711 | ||
|
|
f3c7211bf0 | ||
|
|
ead7ee2f50 | ||
|
|
6ba138b4fa | ||
|
|
6ce754bda2 | ||
|
|
8df74185e9 | ||
|
|
37c6231e79 | ||
|
|
9555407f65 | ||
|
|
fbb4d7e399 | ||
|
|
5f624edb38 | ||
|
|
b8ddf53aa8 | ||
|
|
b85a3543f4 | ||
|
|
ebf429a9c6 | ||
|
|
2d727443b3 | ||
|
|
76be137283 | ||
|
|
f68872c09f | ||
|
|
b7152f743b | ||
|
|
603c181ad2 | ||
|
|
16adbd3011 | ||
|
|
b014d060f3 | ||
|
|
85f05ea99b | ||
|
|
bf5a556cc1 | ||
|
|
c4b756c817 | ||
|
|
211f95c527 | ||
|
|
ba40516c54 | ||
|
|
a70078ccdc | ||
|
|
2b62e29282 | ||
|
|
efdce070e1 | ||
|
|
2739457c86 | ||
|
|
7695df69e5 | ||
|
|
319dec4245 | ||
|
|
bebd6320b7 | ||
|
|
0a2d746e38 | ||
|
|
b11fefbbbe | ||
|
|
42ee069c1c | ||
|
|
b211d49922 | ||
|
|
af3a4ff9cd | ||
|
|
448ba008e0 | ||
|
|
8fa1a1e7b9 | ||
|
|
b87c6cd5c7 | ||
|
|
a8e1c4abc2 | ||
|
|
d90199c8db | ||
|
|
d5b40279bd | ||
|
|
b22bcd17b4 | ||
|
|
fa6c02d321 | ||
|
|
5fb203a6df | ||
|
|
0a76707062 | ||
|
|
1ce12fc1fc | ||
|
|
eb302c05b8 |
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# 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: "weekly"
|
||||||
@@ -156,12 +156,13 @@ document.addEventListener("nav", () => {
|
|||||||
// do page specific logic here
|
// do page specific logic here
|
||||||
// e.g. attach event listeners
|
// e.g. attach event listeners
|
||||||
const toggleSwitch = document.querySelector("#switch") as HTMLInputElement
|
const toggleSwitch = document.querySelector("#switch") as HTMLInputElement
|
||||||
toggleSwitch.removeEventListener("change", switchTheme)
|
|
||||||
toggleSwitch.addEventListener("change", switchTheme)
|
toggleSwitch.addEventListener("change", switchTheme)
|
||||||
|
window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
It is best practice to also unmount any existing event handlers to prevent memory leaks.
|
It is best practice to track any event handlers via `window.addCleanup` to prevent memory leaks.
|
||||||
|
This will get called on page navigation.
|
||||||
|
|
||||||
#### Importing Code
|
#### Importing Code
|
||||||
|
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ export const ContentPage: QuartzEmitterPlugin = () => {
|
|||||||
allFiles,
|
allFiles,
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = renderPage(slug, componentData, opts, externalResources)
|
const content = renderPage(cfg, slug, componentData, opts, externalResources)
|
||||||
const fp = await emit({
|
const fp = await emit({
|
||||||
content,
|
content,
|
||||||
slug: file.data.slug!,
|
slug: file.data.slug!,
|
||||||
|
|||||||
@@ -28,21 +28,13 @@ 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 by doing `npx quartz sync`.
|
When your Quartz is at a point you're happy with, you can save your changes to GitHub.
|
||||||
|
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
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ This part of the configuration concerns anything that can affect the whole site.
|
|||||||
- `null`: don't use analytics;
|
- `null`: don't use analytics;
|
||||||
- `{ provider: 'plausible' }`: use [Plausible](https://plausible.io/), a privacy-friendly alternative to Google Analytics; or
|
- `{ provider: 'plausible' }`: use [Plausible](https://plausible.io/), a privacy-friendly alternative to Google Analytics; or
|
||||||
- `{ provider: 'google', tagId: <your-google-tag> }`: use Google Analytics
|
- `{ provider: 'google', tagId: <your-google-tag> }`: use Google Analytics
|
||||||
|
- `locale`: used for [[i18n]] and date formatting
|
||||||
- `baseUrl`: this is used for sitemaps and RSS feeds that require an absolute URL to know where the canonical 'home' of your site lives. This is normally the deployed URL of your site (e.g. `quartz.jzhao.xyz` for this site). Do not include the protocol (i.e. `https://`) or any leading or trailing slashes.
|
- `baseUrl`: this is used for sitemaps and RSS feeds that require an absolute URL to know where the canonical 'home' of your site lives. This is normally the deployed URL of your site (e.g. `quartz.jzhao.xyz` for this site). Do not include the protocol (i.e. `https://`) or any leading or trailing slashes.
|
||||||
- This should also include the subpath if you are [[hosting]] on GitHub pages without a custom domain. For example, if my repository is `jackyzha0/quartz`, GitHub pages would deploy to `https://jackyzha0.github.io/quartz` and the `baseUrl` would be `jackyzha0.github.io/quartz`
|
- This should also include the subpath if you are [[hosting]] on GitHub pages without a custom domain. For example, if my repository is `jackyzha0/quartz`, GitHub pages would deploy to `https://jackyzha0.github.io/quartz` and the `baseUrl` would be `jackyzha0.github.io/quartz`
|
||||||
- Note that Quartz 4 will avoid using this as much as possible and use relative URLs whenever it can to make sure your site works no matter _where_ you end up actually deploying it.
|
- Note that Quartz 4 will avoid using this as much as possible and use relative URLs whenever it can to make sure your site works no matter _where_ you end up actually deploying it.
|
||||||
|
|||||||
@@ -24,14 +24,32 @@ 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/plugins/transformers/ofm.ts`
|
- Editing icons: `quartz/styles/callouts.scss`
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
> [!info]
|
> [!info]
|
||||||
> Default title
|
> Default title
|
||||||
|
|
||||||
> [!question]+ Can callouts be nested?
|
> [!question]+ Can callouts be _nested_?
|
||||||
>
|
>
|
||||||
> > [!todo]- Yes!, they can. And collapsed!
|
> > [!todo]- Yes!, they can. And collapsed!
|
||||||
> >
|
> >
|
||||||
|
|||||||
@@ -12,3 +12,12 @@ 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
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|||||||
18
docs/features/i18n.md
Normal file
18
docs/features/i18n.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
title: Internationalization
|
||||||
|
---
|
||||||
|
|
||||||
|
Internationalization allows users to translate text in the Quartz interface into various supported languages without needing to make extensive code changes. This can be changed via the `locale` [[configuration]] field in `quartz.config.ts`.
|
||||||
|
|
||||||
|
The locale field generally follows a certain format: `{language}-{REGION}`
|
||||||
|
|
||||||
|
- `{language}` is usually a [2-letter lowercase language code](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes).
|
||||||
|
- `{REGION}` is usually a [2-letter uppercase region code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)
|
||||||
|
|
||||||
|
> [!tip] Interested in contributing?
|
||||||
|
> We [gladly welcome translation PRs](https://github.com/jackyzha0/quartz/tree/v4/quartz/i18n/locales)! To contribute a translation, do the following things:
|
||||||
|
>
|
||||||
|
> 1. In the `quartz/i18n/locales` folder, copy the `en-US.ts` file.
|
||||||
|
> 2. Rename it to `{language}-{REGION}.ts` so it matches a locale of the format shown above.
|
||||||
|
> 3. Fill in the translations!
|
||||||
|
> 4. Add the entry under `TRANSLATIONS` in `quartz/i18n/index.ts`.
|
||||||
@@ -3,7 +3,7 @@ title: Recent Notes
|
|||||||
tags: component
|
tags: component
|
||||||
---
|
---
|
||||||
|
|
||||||
Quartz can generate a list of recent notes for based on some filtering and sorting criteria. Though this component isn't included in any [[layout]] by default, you can add it by using `Component.RecentNotes`.
|
Quartz can generate a list of recent notes based on some filtering and sorting criteria. Though this component isn't included in any [[layout]] by default, you can add it by using `Component.RecentNotes` in `quartz.layout.ts`.
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
|
|
||||||
|
|||||||
@@ -23,11 +23,15 @@ 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. [[hosting|Host]] Quartz online
|
5. Sync your changes with [[setting up your GitHub repository|GitHub]]
|
||||||
|
6. [[hosting|Host]] Quartz online
|
||||||
|
|
||||||
|
If you prefer instructions in a video format you can try following Nicole van der Hoeven's
|
||||||
|
[video guide on how to set up Quartz!](https://www.youtube.com/watch?v=6s6DT1yN4dw&t=227s)
|
||||||
|
|
||||||
## 🔧 Features
|
## 🔧 Features
|
||||||
|
|
||||||
- [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], [[Docker Support]], and [many more](./features) right out of the box
|
- [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], [[Docker Support]], [[i18n|internationalization]] and [many more](./features) right out of the box
|
||||||
- Hot-reload for both configuration and content
|
- Hot-reload for both configuration and content
|
||||||
- Simple JSX layouts and [[creating components|page components]]
|
- Simple JSX layouts and [[creating components|page components]]
|
||||||
- [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes
|
- [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes
|
||||||
|
|||||||
@@ -15,25 +15,34 @@ 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
|
||||||
# add your repository
|
# list all the repositories that are tracked
|
||||||
git remote add origin REMOTE-URL
|
git remote -v
|
||||||
|
|
||||||
# track the main quartz repository for updates
|
# if the origin doesn't match your own repository, set your repository as the origin
|
||||||
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
To verify that you set the remote URL correctly, run the following command.
|
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.
|
||||||
|
|
||||||
```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
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!hint]
|
> [!warning]- `fatal: --[no-]autostash option is only valid with --rebase`
|
||||||
> If `npx quartz sync` fails with `fatal: --[no-]autostash option is only valid with --rebase`, you
|
> You may have an outdated version of `git`. Updating `git` should fix this issue.
|
||||||
> 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,13 +7,10 @@ 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/)
|
||||||
@@ -22,7 +19,11 @@ Want to see what Quartz can do? Here are some cool community gardens:
|
|||||||
- [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/)
|
- [🌊 Collapsed Wave](https://collapsedwave.com/)
|
||||||
- [Aaron Pham's Garden](https://aarnphm.xyz/)
|
|
||||||
- [Sideny's 3D Artist's Handbook](https://sidney-eliot.github.io/3d-artists-handbook/)
|
- [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)!
|
||||||
|
|||||||
3
globals.d.ts
vendored
3
globals.d.ts
vendored
@@ -4,9 +4,10 @@ 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]): void
|
dispatchEvent<K extends keyof CustomEventMap>(ev: CustomEventMap[K] | UIEvent): void
|
||||||
}
|
}
|
||||||
interface Window {
|
interface Window {
|
||||||
spaNavigate(url: URL, isBack: boolean = false)
|
spaNavigate(url: URL, isBack: boolean = false)
|
||||||
|
addCleanup(fn: (...args: any[]) => void)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
index.d.ts
vendored
1
index.d.ts
vendored
@@ -6,6 +6,7 @@ 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>
|
||||||
|
|||||||
1186
package-lock.json
generated
1186
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
36
package.json
36
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.5",
|
"version": "4.2.2",
|
||||||
"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.5.3",
|
"@floating-ui/dom": "^1.6.1",
|
||||||
"@napi-rs/simple-git": "0.1.9",
|
"@napi-rs/simple-git": "0.1.14",
|
||||||
"async-mutex": "^0.4.0",
|
"async-mutex": "^0.4.1",
|
||||||
"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.21",
|
"flexsearch": "0.7.43",
|
||||||
"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,9 +52,9 @@
|
|||||||
"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.22.1",
|
"lightningcss": "^1.23.0",
|
||||||
"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.1.0",
|
||||||
"mdast-util-to-string": "^4.0.0",
|
"mdast-util-to-string": "^4.0.0",
|
||||||
"micromorph": "^0.4.5",
|
"micromorph": "^0.4.5",
|
||||||
"preact": "^10.19.3",
|
"preact": "^10.19.3",
|
||||||
@@ -64,8 +64,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": "^5.0.0",
|
"rehype-mathjax": "^6.0.0",
|
||||||
"rehype-pretty-code": "^0.12.3",
|
"rehype-pretty-code": "^0.12.6",
|
||||||
"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,37 +74,35 @@
|
|||||||
"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.0.0",
|
"remark-rehype": "^11.1.0",
|
||||||
"remark-smartypants": "^2.0.0",
|
"remark-smartypants": "^2.0.0",
|
||||||
"rfdc": "^1.3.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.9.9",
|
"shikiji": "^0.10.2",
|
||||||
"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": "^8.0.0",
|
"workerpool": "^9.1.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.4",
|
||||||
"@types/hast": "^3.0.3",
|
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/node": "^20.1.2",
|
"@types/node": "^20.11.14",
|
||||||
"@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.1.1",
|
"prettier": "^3.2.4",
|
||||||
"tsx": "^4.6.2",
|
"tsx": "^4.7.0",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const config: QuartzConfig = {
|
|||||||
analytics: {
|
analytics: {
|
||||||
provider: "plausible",
|
provider: "plausible",
|
||||||
},
|
},
|
||||||
|
locale: "en-US",
|
||||||
baseUrl: "quartz.jzhao.xyz",
|
baseUrl: "quartz.jzhao.xyz",
|
||||||
ignorePatterns: ["private", "templates", ".obsidian"],
|
ignorePatterns: ["private", "templates", ".obsidian"],
|
||||||
defaultDateType: "created",
|
defaultDateType: "created",
|
||||||
@@ -45,7 +46,6 @@ const config: QuartzConfig = {
|
|||||||
plugins: {
|
plugins: {
|
||||||
transformers: [
|
transformers: [
|
||||||
Plugin.FrontMatter(),
|
Plugin.FrontMatter(),
|
||||||
Plugin.TableOfContents(),
|
|
||||||
Plugin.CreatedModifiedDate({
|
Plugin.CreatedModifiedDate({
|
||||||
// you can add 'git' here for last modified from Git
|
// you can add 'git' here for last modified from Git
|
||||||
// if you do rely on git for dates, ensure defaultDateType is 'modified'
|
// if you do rely on git for dates, ensure defaultDateType is 'modified'
|
||||||
@@ -55,6 +55,7 @@ const config: QuartzConfig = {
|
|||||||
Plugin.SyntaxHighlighting(),
|
Plugin.SyntaxHighlighting(),
|
||||||
Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }),
|
Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }),
|
||||||
Plugin.GitHubFlavoredMarkdown(),
|
Plugin.GitHubFlavoredMarkdown(),
|
||||||
|
Plugin.TableOfContents(),
|
||||||
Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
|
Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
|
||||||
Plugin.Description(),
|
Plugin.Description(),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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 { isGitIgnored } from "globby"
|
import { GlobbyFilterFunction, 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, joinSegments, slugifyFilePath } from "./util/path"
|
import { FilePath, FullSlug, 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,6 +18,19 @@ 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,
|
||||||
@@ -73,19 +86,51 @@ 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 initialSlugs = ctx.allSlugs
|
const buildData: BuildData = {
|
||||||
let lastBuildMs = 0
|
ctx,
|
||||||
const toRebuild: Set<FilePath> = new Set()
|
mut,
|
||||||
const toRemove: Set<FilePath> = new Set()
|
contentMap,
|
||||||
const trackedAssets: Set<FilePath> = new Set()
|
ignored: await isGitIgnored(),
|
||||||
async function rebuild(fp: string, action: "add" | "change" | "delete") {
|
initialSlugs: ctx.allSlugs,
|
||||||
|
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 } =
|
||||||
|
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
|
||||||
@@ -110,12 +155,12 @@ async function startServing(
|
|||||||
toRemove.add(filePath)
|
toRemove.add(filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// debounce rebuilds every 250ms
|
|
||||||
|
|
||||||
const buildStart = new Date().getTime()
|
const buildStart = new Date().getTime()
|
||||||
lastBuildMs = buildStart
|
buildData.lastBuildMs = buildStart
|
||||||
const release = await mut.acquire()
|
const release = await mut.acquire()
|
||||||
if (lastBuildMs > buildStart) {
|
|
||||||
|
// there's another build after us, release and let them do it
|
||||||
|
if (buildData.lastBuildMs > buildStart) {
|
||||||
release()
|
release()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -159,22 +204,6 @@ async function startServing(
|
|||||||
clientRefresh()
|
clientRefresh()
|
||||||
toRebuild.clear()
|
toRebuild.clear()
|
||||||
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) => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ValidDateType } from "./components/Date"
|
import { ValidDateType } from "./components/Date"
|
||||||
import { QuartzComponent } from "./components/types"
|
import { QuartzComponent } from "./components/types"
|
||||||
|
import { ValidLocale } from "./i18n"
|
||||||
import { PluginTypes } from "./plugins/types"
|
import { PluginTypes } from "./plugins/types"
|
||||||
import { Theme } from "./util/theme"
|
import { Theme } from "./util/theme"
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ export type Analytics =
|
|||||||
| {
|
| {
|
||||||
provider: "umami"
|
provider: "umami"
|
||||||
websiteId: string
|
websiteId: string
|
||||||
|
host?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GlobalConfiguration {
|
export interface GlobalConfiguration {
|
||||||
@@ -35,6 +37,15 @@ export interface GlobalConfiguration {
|
|||||||
*/
|
*/
|
||||||
baseUrl?: string
|
baseUrl?: string
|
||||||
theme: Theme
|
theme: Theme
|
||||||
|
/**
|
||||||
|
* Allow to translate the date in the language of your choice.
|
||||||
|
* Also used for UI translation (default: en-US)
|
||||||
|
* Need to be formated following BCP 47: https://en.wikipedia.org/wiki/IETF_language_tag
|
||||||
|
* The first part is the language (en) and the second part is the script/region (US)
|
||||||
|
* Language Codes: https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
|
||||||
|
* Region Codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
|
||||||
|
*/
|
||||||
|
locale: ValidLocale
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuartzConfig {
|
export interface QuartzConfig {
|
||||||
|
|||||||
@@ -168,22 +168,20 @@ 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. You can change this later in \`quartz.config.ts\`.`,
|
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\`.`,
|
||||||
options: [
|
options: [
|
||||||
{
|
|
||||||
value: "absolute",
|
|
||||||
label: "Treat links as absolute path",
|
|
||||||
hint: "for content made for Quartz 3 and Hugo",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
value: "shortest",
|
value: "shortest",
|
||||||
label: "Treat links as shortest path",
|
label: "Treat links as shortest path",
|
||||||
hint: "for most Obsidian vaults",
|
hint: "(default)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "absolute",
|
||||||
|
label: "Treat links as absolute path",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "relative",
|
value: "relative",
|
||||||
label: "Treat links as relative paths",
|
label: "Treat links as relative paths",
|
||||||
hint: "for just normal Markdown files",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -202,6 +200,7 @@ 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:
|
||||||
@@ -258,6 +257,7 @@ 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 +347,7 @@ export async function handleBuild(argv) {
|
|||||||
directoryListing: false,
|
directoryListing: false,
|
||||||
headers: [
|
headers: [
|
||||||
{
|
{
|
||||||
source: "**/*.html",
|
source: "**/*.*",
|
||||||
headers: [{ key: "Content-Disposition", value: "inline" }],
|
headers: [{ key: "Content-Disposition", value: "inline" }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
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={`article-title ${displayClass ?? ""}`}>{title}</h1>
|
return <h1 class={classNames(displayClass, "article-title")}>{title}</h1>
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ArticleTitle.css = `
|
ArticleTitle.css = `
|
||||||
.article-title {
|
.article-title {
|
||||||
margin: 2rem 0 0 0;
|
margin: 2rem 0 0 0;
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
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 { i18n } from "../i18n"
|
||||||
|
import { classNames } from "../util/lang"
|
||||||
|
|
||||||
function Backlinks({ fileData, allFiles, displayClass }: QuartzComponentProps) {
|
function Backlinks({ fileData, allFiles, displayClass, cfg }: QuartzComponentProps) {
|
||||||
const slug = simplifySlug(fileData.slug!)
|
const slug = simplifySlug(fileData.slug!)
|
||||||
const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
|
const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
|
||||||
return (
|
return (
|
||||||
<div class={`backlinks ${displayClass ?? ""}`}>
|
<div class={classNames(displayClass, "backlinks")}>
|
||||||
<h3>Backlinks</h3>
|
<h3>{i18n(cfg.locale).components.backlinks.title}</h3>
|
||||||
<ul class="overflow">
|
<ul class="overflow">
|
||||||
{backlinkFiles.length > 0 ? (
|
{backlinkFiles.length > 0 ? (
|
||||||
backlinkFiles.map((f) => (
|
backlinkFiles.map((f) => (
|
||||||
@@ -18,7 +20,7 @@ function Backlinks({ fileData, allFiles, displayClass }: QuartzComponentProps) {
|
|||||||
</li>
|
</li>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<li>No backlinks found</li>
|
<li>{i18n(cfg.locale).components.backlinks.noBacklinksFound}</li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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
|
||||||
@@ -113,7 +114,7 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav class={`breadcrumb-container ${displayClass ?? ""}`} aria-label="breadcrumbs">
|
<nav class={classNames(displayClass, "breadcrumb-container")} 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,6 +1,7 @@
|
|||||||
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 {
|
interface ContentMetaOptions {
|
||||||
/**
|
/**
|
||||||
@@ -24,7 +25,7 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
|
|||||||
const segments: string[] = []
|
const segments: string[] = []
|
||||||
|
|
||||||
if (fileData.dates) {
|
if (fileData.dates) {
|
||||||
segments.push(formatDate(getDate(cfg, fileData)!))
|
segments.push(formatDate(getDate(cfg, fileData)!, cfg.locale))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display reading time if enabled
|
// Display reading time if enabled
|
||||||
@@ -33,7 +34,7 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
|
|||||||
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,10 +4,12 @@
|
|||||||
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 { i18n } from "../i18n"
|
||||||
|
import { classNames } from "../util/lang"
|
||||||
|
|
||||||
function Darkmode({ displayClass }: QuartzComponentProps) {
|
function Darkmode({ displayClass, cfg }: QuartzComponentProps) {
|
||||||
return (
|
return (
|
||||||
<div class={`darkmode ${displayClass ?? ""}`}>
|
<div class={classNames(displayClass, "darkmode")}>
|
||||||
<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
|
||||||
@@ -21,7 +23,7 @@ function Darkmode({ displayClass }: QuartzComponentProps) {
|
|||||||
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>{i18n(cfg.locale).components.themeToggle.darkMode}</title>
|
||||||
<path d="M6,17.5C6,16.672,5.328,16,4.5,16h-3C0.672,16,0,16.672,0,17.5 S0.672,19,1.5,19h3C5.328,19,6,18.328,6,17.5z M7.5,26c-0.414,0-0.789,0.168-1.061,0.439l-2,2C4.168,28.711,4,29.086,4,29.5 C4,30.328,4.671,31,5.5,31c0.414,0,0.789-0.168,1.06-0.44l2-2C8.832,28.289,9,27.914,9,27.5C9,26.672,8.329,26,7.5,26z M17.5,6 C18.329,6,19,5.328,19,4.5v-3C19,0.672,18.329,0,17.5,0S16,0.672,16,1.5v3C16,5.328,16.671,6,17.5,6z M27.5,9 c0.414,0,0.789-0.168,1.06-0.439l2-2C30.832,6.289,31,5.914,31,5.5C31,4.672,30.329,4,29.5,4c-0.414,0-0.789,0.168-1.061,0.44 l-2,2C26.168,6.711,26,7.086,26,7.5C26,8.328,26.671,9,27.5,9z M6.439,8.561C6.711,8.832,7.086,9,7.5,9C8.328,9,9,8.328,9,7.5 c0-0.414-0.168-0.789-0.439-1.061l-2-2C6.289,4.168,5.914,4,5.5,4C4.672,4,4,4.672,4,5.5c0,0.414,0.168,0.789,0.439,1.06 L6.439,8.561z M33.5,16h-3c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5h3c0.828,0,1.5-0.672,1.5-1.5S34.328,16,33.5,16z M28.561,26.439C28.289,26.168,27.914,26,27.5,26c-0.828,0-1.5,0.672-1.5,1.5c0,0.414,0.168,0.789,0.439,1.06l2,2 C28.711,30.832,29.086,31,29.5,31c0.828,0,1.5-0.672,1.5-1.5c0-0.414-0.168-0.789-0.439-1.061L28.561,26.439z M17.5,29 c-0.829,0-1.5,0.672-1.5,1.5v3c0,0.828,0.671,1.5,1.5,1.5s1.5-0.672,1.5-1.5v-3C19,29.672,18.329,29,17.5,29z M17.5,7 C11.71,7,7,11.71,7,17.5S11.71,28,17.5,28S28,23.29,28,17.5S23.29,7,17.5,7z M17.5,25c-4.136,0-7.5-3.364-7.5-7.5 c0-4.136,3.364-7.5,7.5-7.5c4.136,0,7.5,3.364,7.5,7.5C25,21.636,21.636,25,17.5,25z"></path>
|
<path d="M6,17.5C6,16.672,5.328,16,4.5,16h-3C0.672,16,0,16.672,0,17.5 S0.672,19,1.5,19h3C5.328,19,6,18.328,6,17.5z M7.5,26c-0.414,0-0.789,0.168-1.061,0.439l-2,2C4.168,28.711,4,29.086,4,29.5 C4,30.328,4.671,31,5.5,31c0.414,0,0.789-0.168,1.06-0.44l2-2C8.832,28.289,9,27.914,9,27.5C9,26.672,8.329,26,7.5,26z M17.5,6 C18.329,6,19,5.328,19,4.5v-3C19,0.672,18.329,0,17.5,0S16,0.672,16,1.5v3C16,5.328,16.671,6,17.5,6z M27.5,9 c0.414,0,0.789-0.168,1.06-0.439l2-2C30.832,6.289,31,5.914,31,5.5C31,4.672,30.329,4,29.5,4c-0.414,0-0.789,0.168-1.061,0.44 l-2,2C26.168,6.711,26,7.086,26,7.5C26,8.328,26.671,9,27.5,9z M6.439,8.561C6.711,8.832,7.086,9,7.5,9C8.328,9,9,8.328,9,7.5 c0-0.414-0.168-0.789-0.439-1.061l-2-2C6.289,4.168,5.914,4,5.5,4C4.672,4,4,4.672,4,5.5c0,0.414,0.168,0.789,0.439,1.06 L6.439,8.561z M33.5,16h-3c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5h3c0.828,0,1.5-0.672,1.5-1.5S34.328,16,33.5,16z M28.561,26.439C28.289,26.168,27.914,26,27.5,26c-0.828,0-1.5,0.672-1.5,1.5c0,0.414,0.168,0.789,0.439,1.06l2,2 C28.711,30.832,29.086,31,29.5,31c0.828,0,1.5-0.672,1.5-1.5c0-0.414-0.168-0.789-0.439-1.061L28.561,26.439z M17.5,29 c-0.829,0-1.5,0.672-1.5,1.5v3c0,0.828,0.671,1.5,1.5,1.5s1.5-0.672,1.5-1.5v-3C19,29.672,18.329,29,17.5,29z M17.5,7 C11.71,7,7,11.71,7,17.5S11.71,28,17.5,28S28,23.29,28,17.5S23.29,7,17.5,7z M17.5,25c-4.136,0-7.5-3.364-7.5-7.5 c0-4.136,3.364-7.5,7.5-7.5c4.136,0,7.5,3.364,7.5,7.5C25,21.636,21.636,25,17.5,25z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
@@ -37,7 +39,7 @@ function Darkmode({ displayClass }: QuartzComponentProps) {
|
|||||||
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>{i18n(cfg.locale).components.themeToggle.lightMode}</title>
|
||||||
<path d="M96.76,66.458c-0.853-0.852-2.15-1.064-3.23-0.534c-6.063,2.991-12.858,4.571-19.655,4.571 C62.022,70.495,50.88,65.88,42.5,57.5C29.043,44.043,25.658,23.536,34.076,6.47c0.532-1.08,0.318-2.379-0.534-3.23 c-0.851-0.852-2.15-1.064-3.23-0.534c-4.918,2.427-9.375,5.619-13.246,9.491c-9.447,9.447-14.65,22.008-14.65,35.369 c0,13.36,5.203,25.921,14.65,35.368s22.008,14.65,35.368,14.65c13.361,0,25.921-5.203,35.369-14.65 c3.872-3.871,7.064-8.328,9.491-13.246C97.826,68.608,97.611,67.309,96.76,66.458z"></path>
|
<path d="M96.76,66.458c-0.853-0.852-2.15-1.064-3.23-0.534c-6.063,2.991-12.858,4.571-19.655,4.571 C62.022,70.495,50.88,65.88,42.5,57.5C29.043,44.043,25.658,23.536,34.076,6.47c0.532-1.08,0.318-2.379-0.534-3.23 c-0.851-0.852-2.15-1.064-3.23-0.534c-4.918,2.427-9.375,5.619-13.246,9.491c-9.447,9.447-14.65,22.008-14.65,35.369 c0,13.36,5.203,25.921,14.65,35.368s22.008,14.65,35.368,14.65c13.361,0,25.921-5.203,35.369-14.65 c3.872-3.871,7.064-8.328,9.491-13.246C97.826,68.608,97.611,67.309,96.76,66.458z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { GlobalConfiguration } from "../cfg"
|
import { GlobalConfiguration } from "../cfg"
|
||||||
|
import { ValidLocale } from "../i18n"
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
date: Date
|
date: Date
|
||||||
|
locale?: ValidLocale
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ValidDateType = keyof Required<QuartzPluginData>["dates"]
|
export type ValidDateType = keyof Required<QuartzPluginData>["dates"]
|
||||||
@@ -16,14 +18,14 @@ export function getDate(cfg: GlobalConfiguration, data: QuartzPluginData): Date
|
|||||||
return data.dates?.[cfg.defaultDateType]
|
return data.dates?.[cfg.defaultDateType]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDate(d: Date): string {
|
export function formatDate(d: Date, locale: ValidLocale = "en-US"): string {
|
||||||
return d.toLocaleDateString("en-US", {
|
return d.toLocaleDateString(locale, {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Date({ date }: Props) {
|
export function Date({ date, locale }: Props) {
|
||||||
return <>{formatDate(date)}</>
|
return <>{formatDate(date, locale)}</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ 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"
|
||||||
|
import { i18n } from "../i18n"
|
||||||
|
|
||||||
// Options interface defined in `ExplorerNode` to avoid circular dependency
|
// Options interface defined in `ExplorerNode` to avoid circular dependency
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
title: "Explorer",
|
|
||||||
folderClickBehavior: "collapse",
|
folderClickBehavior: "collapse",
|
||||||
folderDefaultState: "collapsed",
|
folderDefaultState: "collapsed",
|
||||||
useSavedState: true,
|
useSavedState: true,
|
||||||
@@ -69,16 +70,15 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get all folders of tree. Initialize with collapsed state
|
// Get all folders of tree. Initialize with collapsed state
|
||||||
const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed")
|
|
||||||
|
|
||||||
// Stringify to pass json tree as data attribute ([data-tree])
|
// Stringify to pass json tree as data attribute ([data-tree])
|
||||||
|
const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed")
|
||||||
jsonTree = JSON.stringify(folders)
|
jsonTree = JSON.stringify(folders)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) {
|
function Explorer({ cfg, allFiles, displayClass, fileData }: QuartzComponentProps) {
|
||||||
constructFileTree(allFiles)
|
constructFileTree(allFiles)
|
||||||
return (
|
return (
|
||||||
<div class={`explorer ${displayClass ?? ""}`}>
|
<div class={classNames(displayClass, "explorer")}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
id="explorer"
|
id="explorer"
|
||||||
@@ -87,7 +87,7 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
data-savestate={opts.useSavedState}
|
data-savestate={opts.useSavedState}
|
||||||
data-tree={jsonTree}
|
data-tree={jsonTree}
|
||||||
>
|
>
|
||||||
<h1>{opts.title}</h1>
|
<h1>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h1>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="14"
|
width="14"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
type OrderEntries = "sort" | "filter" | "map"
|
type OrderEntries = "sort" | "filter" | "map"
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
title: string
|
title?: string
|
||||||
folderDefaultState: "collapsed" | "open"
|
folderDefaultState: "collapsed" | "open"
|
||||||
folderClickBehavior: "collapse" | "link"
|
folderClickBehavior: "collapse" | "link"
|
||||||
useSavedState: boolean
|
useSavedState: boolean
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import style from "./styles/footer.scss"
|
import style from "./styles/footer.scss"
|
||||||
import { version } from "../../package.json"
|
import { version } from "../../package.json"
|
||||||
|
import { i18n } from "../i18n"
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
links: Record<string, string>
|
links: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ((opts?: Options) => {
|
export default ((opts?: Options) => {
|
||||||
function Footer({ displayClass }: QuartzComponentProps) {
|
function Footer({ displayClass, cfg }: QuartzComponentProps) {
|
||||||
const year = new Date().getFullYear()
|
const year = new Date().getFullYear()
|
||||||
const links = opts?.links ?? []
|
const links = opts?.links ?? []
|
||||||
return (
|
return (
|
||||||
<footer class={`${displayClass ?? ""}`}>
|
<footer class={`${displayClass ?? ""}`}>
|
||||||
<hr />
|
<hr />
|
||||||
<p>
|
<p>
|
||||||
Created with <a href="https://quartz.jzhao.xyz/">Quartz v{version}</a>, © {year}
|
{i18n(cfg.locale).components.footer.createdWith}{" "}
|
||||||
|
<a href="https://quartz.jzhao.xyz/">Quartz v{version}</a> © {year}
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
{Object.entries(links).map(([text, link]) => (
|
{Object.entries(links).map(([text, link]) => (
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ 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 { i18n } from "../i18n"
|
||||||
|
import { classNames } from "../util/lang"
|
||||||
|
|
||||||
export interface D3Config {
|
export interface D3Config {
|
||||||
drag: boolean
|
drag: boolean
|
||||||
@@ -52,12 +54,12 @@ const defaultOptions: GraphOptions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default ((opts?: GraphOptions) => {
|
export default ((opts?: GraphOptions) => {
|
||||||
function Graph({ displayClass }: QuartzComponentProps) {
|
function Graph({ displayClass, cfg }: QuartzComponentProps) {
|
||||||
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={`graph ${displayClass ?? ""}`}>
|
<div class={classNames(displayClass, "graph")}>
|
||||||
<h3>Graph View</h3>
|
<h3>{i18n(cfg.locale).components.graph.title}</h3>
|
||||||
<div class="graph-outer">
|
<div class="graph-outer">
|
||||||
<div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
|
<div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
|
import { i18n } from "../i18n"
|
||||||
import { FullSlug, _stripSlashes, joinSegments, pathToRoot } from "../util/path"
|
import { FullSlug, _stripSlashes, joinSegments, pathToRoot } from "../util/path"
|
||||||
import { JSResourceToScriptElement } from "../util/resources"
|
import { JSResourceToScriptElement } from "../util/resources"
|
||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
|
|
||||||
export default (() => {
|
export default (() => {
|
||||||
function Head({ cfg, fileData, externalResources }: QuartzComponentProps) {
|
function Head({ cfg, fileData, externalResources }: QuartzComponentProps) {
|
||||||
const title = fileData.frontmatter?.title ?? "Untitled"
|
const title = fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title
|
||||||
const description = fileData.description?.trim() ?? "No description provided"
|
const description =
|
||||||
|
fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description
|
||||||
const { css, js } = externalResources
|
const { css, js } = externalResources
|
||||||
|
|
||||||
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
|
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
|
||||||
|
|||||||
@@ -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)!} />
|
<Date date={getDate(cfg, page)!} locale={cfg.locale} />
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div class="desc">
|
<div class="desc">
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
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"
|
||||||
|
import { i18n } from "../i18n"
|
||||||
|
|
||||||
function PageTitle({ fileData, cfg, displayClass }: QuartzComponentProps) {
|
function PageTitle({ fileData, cfg, displayClass }: QuartzComponentProps) {
|
||||||
const title = cfg?.pageTitle ?? "Untitled Quartz"
|
const title = cfg?.pageTitle ?? i18n(cfg.locale).propertyDefaults.title
|
||||||
const baseDir = pathToRoot(fileData.slug!)
|
const baseDir = pathToRoot(fileData.slug!)
|
||||||
return (
|
return (
|
||||||
<h1 class={`page-title ${displayClass ?? ""}`}>
|
<h1 class={classNames(displayClass, "page-title")}>
|
||||||
<a href={baseDir}>{title}</a>
|
<a href={baseDir}>{title}</a>
|
||||||
</h1>
|
</h1>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ 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 { i18n } from "../i18n"
|
||||||
|
import { classNames } from "../util/lang"
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
title: string
|
title?: string
|
||||||
limit: number
|
limit: number
|
||||||
linkToMore: SimpleSlug | false
|
linkToMore: SimpleSlug | false
|
||||||
filter: (f: QuartzPluginData) => boolean
|
filter: (f: QuartzPluginData) => boolean
|
||||||
@@ -15,7 +17,6 @@ interface Options {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions = (cfg: GlobalConfiguration): Options => ({
|
const defaultOptions = (cfg: GlobalConfiguration): Options => ({
|
||||||
title: "Recent Notes",
|
|
||||||
limit: 3,
|
limit: 3,
|
||||||
linkToMore: false,
|
linkToMore: false,
|
||||||
filter: () => true,
|
filter: () => true,
|
||||||
@@ -28,11 +29,11 @@ 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={`recent-notes ${displayClass ?? ""}`}>
|
<div class={classNames(displayClass, "recent-notes")}>
|
||||||
<h3>{opts.title}</h3>
|
<h3>{opts.title ?? i18n(cfg.locale).components.recentNotes.title}</h3>
|
||||||
<ul class="recent-ul">
|
<ul class="recent-ul">
|
||||||
{pages.slice(0, opts.limit).map((page) => {
|
{pages.slice(0, opts.limit).map((page) => {
|
||||||
const title = page.frontmatter?.title
|
const title = page.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title
|
||||||
const tags = page.frontmatter?.tags ?? []
|
const tags = page.frontmatter?.tags ?? []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -47,7 +48,7 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
</div>
|
</div>
|
||||||
{page.dates && (
|
{page.dates && (
|
||||||
<p class="meta">
|
<p class="meta">
|
||||||
<Date date={getDate(cfg, page)!} />
|
<Date date={getDate(cfg, page)!} locale={cfg.locale} />
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<ul class="tags">
|
<ul class="tags">
|
||||||
@@ -69,7 +70,9 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
</ul>
|
</ul>
|
||||||
{opts.linkToMore && remaining > 0 && (
|
{opts.linkToMore && remaining > 0 && (
|
||||||
<p>
|
<p>
|
||||||
<a href={resolveRelative(fileData.slug!, opts.linkToMore)}>See {remaining} more →</a>
|
<a href={resolveRelative(fileData.slug!, opts.linkToMore)}>
|
||||||
|
{i18n(cfg.locale).components.recentNotes.seeRemainingMore({ remaining })}
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,13 +2,25 @@ 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"
|
||||||
|
import { i18n } from "../i18n"
|
||||||
|
|
||||||
export default (() => {
|
export interface SearchOptions {
|
||||||
function Search({ displayClass }: QuartzComponentProps) {
|
enablePreview: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultOptions: SearchOptions = {
|
||||||
|
enablePreview: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ((userOpts?: Partial<SearchOptions>) => {
|
||||||
|
function Search({ displayClass, cfg }: QuartzComponentProps) {
|
||||||
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
|
const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder
|
||||||
return (
|
return (
|
||||||
<div class={`search ${displayClass ?? ""}`}>
|
<div class={classNames(displayClass, "search")}>
|
||||||
<div id="search-icon">
|
<div id="search-icon">
|
||||||
<p>Search</p>
|
<p>{i18n(cfg.locale).components.search.title}</p>
|
||||||
<div></div>
|
<div></div>
|
||||||
<svg
|
<svg
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -32,10 +44,10 @@ export default (() => {
|
|||||||
id="search-bar"
|
id="search-bar"
|
||||||
name="search"
|
name="search"
|
||||||
type="text"
|
type="text"
|
||||||
aria-label="Search for something"
|
aria-label={searchPlaceholder}
|
||||||
placeholder="Search for something"
|
placeholder={searchPlaceholder}
|
||||||
/>
|
/>
|
||||||
<div id="results-container"></div>
|
<div id="search-layout" data-preview={opts.enablePreview}></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
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={`spacer ${displayClass ?? ""}`}></div>
|
return <div class={classNames(displayClass, "spacer")}></div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (() => Spacer) satisfies QuartzComponentConstructor
|
export default (() => Spacer) satisfies QuartzComponentConstructor
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
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"
|
||||||
|
import { i18n } from "../i18n"
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
layout: "modern" | "legacy"
|
layout: "modern" | "legacy"
|
||||||
@@ -13,15 +15,15 @@ const defaultOptions: Options = {
|
|||||||
layout: "modern",
|
layout: "modern",
|
||||||
}
|
}
|
||||||
|
|
||||||
function TableOfContents({ fileData, displayClass }: QuartzComponentProps) {
|
function TableOfContents({ fileData, displayClass, cfg }: QuartzComponentProps) {
|
||||||
if (!fileData.toc) {
|
if (!fileData.toc) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={`toc ${displayClass ?? ""}`}>
|
<div class={classNames(displayClass, "toc")}>
|
||||||
<button type="button" id="toc" class={fileData.collapseToc ? "collapsed" : ""}>
|
<button type="button" id="toc" class={fileData.collapseToc ? "collapsed" : ""}>
|
||||||
<h3>Table of Contents</h3>
|
<h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="24"
|
width="24"
|
||||||
@@ -54,15 +56,14 @@ function TableOfContents({ fileData, displayClass }: QuartzComponentProps) {
|
|||||||
TableOfContents.css = modernStyle
|
TableOfContents.css = modernStyle
|
||||||
TableOfContents.afterDOMLoaded = script
|
TableOfContents.afterDOMLoaded = script
|
||||||
|
|
||||||
function LegacyTableOfContents({ fileData }: QuartzComponentProps) {
|
function LegacyTableOfContents({ fileData, cfg }: QuartzComponentProps) {
|
||||||
if (!fileData.toc) {
|
if (!fileData.toc) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<details id="toc" open={!fileData.collapseToc}>
|
<details id="toc" open={!fileData.collapseToc}>
|
||||||
<summary>
|
<summary>
|
||||||
<h3>Table of Contents</h3>
|
<h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
|
||||||
</summary>
|
</summary>
|
||||||
<ul>
|
<ul>
|
||||||
{fileData.toc.map((tocEntry) => (
|
{fileData.toc.map((tocEntry) => (
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
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={`tags ${displayClass ?? ""}`}>
|
<ul class={classNames(displayClass, "tags")}>
|
||||||
{tags.map((tag) => {
|
{tags.map((tag) => {
|
||||||
const display = `#${tag}`
|
const display = `#${tag}`
|
||||||
const linkDest = baseDir + `/tags/${slugTag(tag)}`
|
const linkDest = baseDir + `/tags/${slugTag(tag)}`
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { QuartzComponentConstructor } from "../types"
|
import { i18n } from "../../i18n"
|
||||||
|
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
||||||
|
|
||||||
function NotFound() {
|
function NotFound({ cfg }: QuartzComponentProps) {
|
||||||
return (
|
return (
|
||||||
<article class="popover-hint">
|
<article class="popover-hint">
|
||||||
<h1>404</h1>
|
<h1>404</h1>
|
||||||
<p>Either this page is private or doesn't exist.</p>
|
<p>{i18n(cfg.locale).pages.error.notFound}</p>
|
||||||
</article>
|
</article>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ 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)
|
||||||
return <article class="popover-hint">{content}</article>
|
const classes: string[] = fileData.frontmatter?.cssclasses ?? []
|
||||||
|
const classString = ["popover-hint", ...classes].join(" ")
|
||||||
|
return <article class={classString}>{content}</article>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (() => Content) satisfies QuartzComponentConstructor
|
export default (() => Content) satisfies QuartzComponentConstructor
|
||||||
|
|||||||
@@ -5,11 +5,25 @@ import style from "../styles/listPage.scss"
|
|||||||
import { PageList } from "../PageList"
|
import { PageList } from "../PageList"
|
||||||
import { _stripSlashes, simplifySlug } from "../../util/path"
|
import { _stripSlashes, simplifySlug } from "../../util/path"
|
||||||
import { Root } from "hast"
|
import { Root } from "hast"
|
||||||
import { pluralize } from "../../util/lang"
|
|
||||||
import { htmlToJsx } from "../../util/jsx"
|
import { htmlToJsx } from "../../util/jsx"
|
||||||
|
import { i18n } from "../../i18n"
|
||||||
|
|
||||||
function FolderContent(props: QuartzComponentProps) {
|
interface FolderContentOptions {
|
||||||
const { tree, fileData, allFiles } = props
|
/**
|
||||||
|
* 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) {
|
||||||
|
const { tree, fileData, allFiles, cfg } = props
|
||||||
const folderSlug = _stripSlashes(simplifySlug(fileData.slug!))
|
const folderSlug = _stripSlashes(simplifySlug(fileData.slug!))
|
||||||
const allPagesInFolder = allFiles.filter((file) => {
|
const allPagesInFolder = allFiles.filter((file) => {
|
||||||
const fileSlug = _stripSlashes(simplifySlug(file.slug!))
|
const fileSlug = _stripSlashes(simplifySlug(file.slug!))
|
||||||
@@ -19,7 +33,8 @@ function FolderContent(props: QuartzComponentProps) {
|
|||||||
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,
|
||||||
@@ -31,17 +46,26 @@ function FolderContent(props: QuartzComponentProps) {
|
|||||||
: htmlToJsx(fileData.filePath!, tree)
|
: htmlToJsx(fileData.filePath!, tree)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="popover-hint">
|
<div class={classes}>
|
||||||
<article>
|
<article>
|
||||||
<p>{content}</p>
|
<p>{content}</p>
|
||||||
</article>
|
</article>
|
||||||
<p>{pluralize(allPagesInFolder.length, "item")} under this folder.</p>
|
<div class="page-listing">
|
||||||
|
{options.showFolderCount && (
|
||||||
|
<p>
|
||||||
|
{i18n(cfg.locale).pages.folderContent.itemsUnderFolder({
|
||||||
|
count: allPagesInFolder.length,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<PageList {...listProps} />
|
<PageList {...listProps} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
FolderContent.css = style + PageList.css
|
FolderContent.css = style + PageList.css
|
||||||
export default (() => FolderContent) satisfies QuartzComponentConstructor
|
return FolderContent
|
||||||
|
}) satisfies QuartzComponentConstructor
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import { PageList } from "../PageList"
|
|||||||
import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path"
|
import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path"
|
||||||
import { QuartzPluginData } from "../../plugins/vfile"
|
import { QuartzPluginData } from "../../plugins/vfile"
|
||||||
import { Root } from "hast"
|
import { Root } from "hast"
|
||||||
import { pluralize } from "../../util/lang"
|
|
||||||
import { htmlToJsx } from "../../util/jsx"
|
import { htmlToJsx } from "../../util/jsx"
|
||||||
|
import { i18n } from "../../i18n"
|
||||||
|
|
||||||
const numPages = 10
|
const numPages = 10
|
||||||
function TagContent(props: QuartzComponentProps) {
|
function TagContent(props: QuartzComponentProps) {
|
||||||
const { tree, fileData, allFiles } = props
|
const { tree, fileData, allFiles, cfg } = props
|
||||||
const slug = fileData.slug
|
const slug = fileData.slug
|
||||||
|
|
||||||
if (!(slug?.startsWith("tags/") || slug === "tags")) {
|
if (!(slug?.startsWith("tags/") || slug === "tags")) {
|
||||||
@@ -26,7 +26,8 @@ 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(
|
||||||
@@ -37,13 +38,12 @@ 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="popover-hint">
|
<div class={classes}>
|
||||||
<article>
|
<article>
|
||||||
<p>{content}</p>
|
<p>{content}</p>
|
||||||
</article>
|
</article>
|
||||||
<p>Found {tags.length} total tags.</p>
|
<p>{i18n(cfg.locale).pages.tagContent.totalTags({ count: tags.length })}</p>
|
||||||
<div>
|
<div>
|
||||||
{tags.map((tag) => {
|
{tags.map((tag) => {
|
||||||
const pages = tagItemMap.get(tag)!
|
const pages = tagItemMap.get(tag)!
|
||||||
@@ -62,12 +62,18 @@ 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.{" "}
|
{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}
|
||||||
{pages.length > numPages && `Showing first ${numPages}.`}
|
{pages.length > numPages && (
|
||||||
|
<span>
|
||||||
|
{i18n(cfg.locale).pages.tagContent.showingFirst({ count: numPages })}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
<PageList limit={numPages} {...listProps} />
|
<PageList limit={numPages} {...listProps} />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -81,13 +87,15 @@ function TagContent(props: QuartzComponentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="popover-hint">
|
<div class={classes}>
|
||||||
<article>{content}</article>
|
<article>{content}</article>
|
||||||
<p>{pluralize(pages.length, "item")} with this tag.</p>
|
<div class="page-listing">
|
||||||
|
<p>{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}</p>
|
||||||
<div>
|
<div>
|
||||||
<PageList {...listProps} />
|
<PageList {...listProps} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../ut
|
|||||||
import { visit } from "unist-util-visit"
|
import { visit } from "unist-util-visit"
|
||||||
import { Root, Element, ElementContent } from "hast"
|
import { Root, Element, ElementContent } from "hast"
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
|
import { GlobalConfiguration } from "../cfg"
|
||||||
|
import { i18n } from "../i18n"
|
||||||
|
|
||||||
interface RenderComponents {
|
interface RenderComponents {
|
||||||
head: QuartzComponent
|
head: QuartzComponent
|
||||||
@@ -63,6 +65,7 @@ function getOrComputeFileIndex(allFiles: QuartzPluginData[]): Map<FullSlug, Quar
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function renderPage(
|
export function renderPage(
|
||||||
|
cfg: GlobalConfiguration,
|
||||||
slug: FullSlug,
|
slug: FullSlug,
|
||||||
componentData: QuartzComponentProps,
|
componentData: QuartzComponentProps,
|
||||||
components: RenderComponents,
|
components: RenderComponents,
|
||||||
@@ -136,7 +139,9 @@ export function renderPage(
|
|||||||
type: "element",
|
type: "element",
|
||||||
tagName: "a",
|
tagName: "a",
|
||||||
properties: { href: inner.properties?.href, class: ["internal"] },
|
properties: { href: inner.properties?.href, class: ["internal"] },
|
||||||
children: [{ type: "text", value: `Link to original` }],
|
children: [
|
||||||
|
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} else if (page.htmlAst) {
|
} else if (page.htmlAst) {
|
||||||
@@ -147,7 +152,14 @@ export function renderPage(
|
|||||||
tagName: "h1",
|
tagName: "h1",
|
||||||
properties: {},
|
properties: {},
|
||||||
children: [
|
children: [
|
||||||
{ type: "text", value: page.frontmatter?.title ?? `Transclude of ${page.slug}` },
|
{
|
||||||
|
type: "text",
|
||||||
|
value:
|
||||||
|
page.frontmatter?.title ??
|
||||||
|
i18n(cfg.locale).components.transcludes.transcludeOf({
|
||||||
|
targetSlug: page.slug!,
|
||||||
|
}),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
...(page.htmlAst.children as ElementContent[]).map((child) =>
|
...(page.htmlAst.children as ElementContent[]).map((child) =>
|
||||||
@@ -157,7 +169,9 @@ export function renderPage(
|
|||||||
type: "element",
|
type: "element",
|
||||||
tagName: "a",
|
tagName: "a",
|
||||||
properties: { href: inner.properties?.href, class: ["internal"] },
|
properties: { href: inner.properties?.href, class: ["internal"] },
|
||||||
children: [{ type: "text", value: `Link to original` }],
|
children: [
|
||||||
|
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
function toggleCallout(this: HTMLElement) {
|
function toggleCallout(this: HTMLElement) {
|
||||||
const outerBlock = this.parentElement!
|
const outerBlock = this.parentElement!
|
||||||
outerBlock.classList.toggle(`is-collapsed`)
|
outerBlock.classList.toggle("is-collapsed")
|
||||||
const collapsed = outerBlock.classList.contains(`is-collapsed`)
|
const collapsed = outerBlock.classList.contains("is-collapsed")
|
||||||
const height = collapsed ? this.scrollHeight : outerBlock.scrollHeight
|
const height = collapsed ? this.scrollHeight : outerBlock.scrollHeight
|
||||||
outerBlock.style.maxHeight = height + `px`
|
outerBlock.style.maxHeight = height + "px"
|
||||||
|
|
||||||
// walk and adjust height of all parents
|
// walk and adjust height of all parents
|
||||||
let current = outerBlock
|
let current = outerBlock
|
||||||
let parent = outerBlock.parentElement
|
let parent = outerBlock.parentElement
|
||||||
while (parent) {
|
while (parent) {
|
||||||
if (!parent.classList.contains(`callout`)) {
|
if (!parent.classList.contains("callout")) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const collapsed = parent.classList.contains(`is-collapsed`)
|
const collapsed = parent.classList.contains("is-collapsed")
|
||||||
const height = collapsed ? parent.scrollHeight : parent.scrollHeight + current.scrollHeight
|
const height = collapsed ? parent.scrollHeight : parent.scrollHeight + current.scrollHeight
|
||||||
parent.style.maxHeight = height + `px`
|
parent.style.maxHeight = height + "px"
|
||||||
|
|
||||||
current = parent
|
current = parent
|
||||||
parent = parent.parentElement
|
parent = parent.parentElement
|
||||||
@@ -30,15 +30,15 @@ function setupCallout() {
|
|||||||
const title = div.firstElementChild
|
const title = div.firstElementChild
|
||||||
|
|
||||||
if (title) {
|
if (title) {
|
||||||
title.removeEventListener(`click`, toggleCallout)
|
title.addEventListener("click", toggleCallout)
|
||||||
title.addEventListener(`click`, toggleCallout)
|
window.addCleanup(() => title.removeEventListener("click", toggleCallout))
|
||||||
|
|
||||||
const collapsed = div.classList.contains(`is-collapsed`)
|
const collapsed = div.classList.contains("is-collapsed")
|
||||||
const height = collapsed ? title.scrollHeight : div.scrollHeight
|
const height = collapsed ? title.scrollHeight : div.scrollHeight
|
||||||
div.style.maxHeight = height + `px`
|
div.style.maxHeight = height + "px"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener(`nav`, setupCallout)
|
document.addEventListener("nav", setupCallout)
|
||||||
window.addEventListener(`resize`, setupCallout)
|
window.addEventListener("resize", setupCallout)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ document.addEventListener("nav", () => {
|
|||||||
button.type = "button"
|
button.type = "button"
|
||||||
button.innerHTML = svgCopy
|
button.innerHTML = svgCopy
|
||||||
button.ariaLabel = "Copy source"
|
button.ariaLabel = "Copy source"
|
||||||
button.addEventListener("click", () => {
|
function onClick() {
|
||||||
navigator.clipboard.writeText(source).then(
|
navigator.clipboard.writeText(source).then(
|
||||||
() => {
|
() => {
|
||||||
button.blur()
|
button.blur()
|
||||||
@@ -26,7 +26,9 @@ document.addEventListener("nav", () => {
|
|||||||
},
|
},
|
||||||
(error) => console.error(error),
|
(error) => console.error(error),
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
button.addEventListener("click", onClick)
|
||||||
|
window.addCleanup(() => button.removeEventListener("click", onClick))
|
||||||
els[i].prepend(button)
|
els[i].prepend(button)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,31 +2,39 @@ 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: Event) => {
|
||||||
if (e.target.checked) {
|
const newTheme = (e.target as HTMLInputElement)?.checked ? "dark" : "light"
|
||||||
document.documentElement.setAttribute("saved-theme", "dark")
|
document.documentElement.setAttribute("saved-theme", newTheme)
|
||||||
localStorage.setItem("theme", "dark")
|
localStorage.setItem("theme", newTheme)
|
||||||
} else {
|
emitThemeChangeEvent(newTheme)
|
||||||
document.documentElement.setAttribute("saved-theme", "light")
|
|
||||||
localStorage.setItem("theme", "light")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const themeChange = (e: MediaQueryListEvent) => {
|
||||||
|
const newTheme = e.matches ? "dark" : "light"
|
||||||
|
document.documentElement.setAttribute("saved-theme", newTheme)
|
||||||
|
localStorage.setItem("theme", newTheme)
|
||||||
|
toggleSwitch.checked = e.matches
|
||||||
|
emitThemeChangeEvent(newTheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Darkmode toggle
|
// Darkmode toggle
|
||||||
const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement
|
const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement
|
||||||
toggleSwitch.removeEventListener("change", switchTheme)
|
|
||||||
toggleSwitch.addEventListener("change", switchTheme)
|
toggleSwitch.addEventListener("change", switchTheme)
|
||||||
|
window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
|
||||||
if (currentTheme === "dark") {
|
if (currentTheme === "dark") {
|
||||||
toggleSwitch.checked = true
|
toggleSwitch.checked = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for changes in prefers-color-scheme
|
// Listen for changes in prefers-color-scheme
|
||||||
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
colorSchemeMediaQuery.addEventListener("change", (e) => {
|
colorSchemeMediaQuery.addEventListener("change", themeChange)
|
||||||
const newTheme = e.matches ? "dark" : "light"
|
window.addCleanup(() => colorSchemeMediaQuery.removeEventListener("change", themeChange))
|
||||||
document.documentElement.setAttribute("saved-theme", newTheme)
|
|
||||||
localStorage.setItem("theme", newTheme)
|
|
||||||
toggleSwitch.checked = e.matches
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,132 +1,106 @@
|
|||||||
import { FolderState } from "../ExplorerNode"
|
import { FolderState } from "../ExplorerNode"
|
||||||
|
|
||||||
// Current state of folders
|
type MaybeHTMLElement = HTMLElement | undefined
|
||||||
let explorerState: FolderState[]
|
let currentExplorerState: 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 explorer = document.getElementById("explorer-ul")
|
const explorerUl = document.getElementById("explorer-ul")
|
||||||
|
if (!explorerUl) return
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
explorer?.classList.add("no-background")
|
explorerUl.classList.add("no-background")
|
||||||
} else {
|
} else {
|
||||||
explorer?.classList.remove("no-background")
|
explorerUl.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 HTMLElement
|
const content = this.nextElementSibling as MaybeHTMLElement
|
||||||
|
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 = (
|
||||||
// corresponding <ul> element relative to clicked button/folder
|
isSvg
|
||||||
let childFolderContainer: HTMLElement
|
? target.parentElement?.nextSibling
|
||||||
|
: target.parentElement?.parentElement?.nextElementSibling
|
||||||
// <li> element of folder (stores folder-path dataset)
|
) as MaybeHTMLElement
|
||||||
let currentFolderParent: HTMLElement
|
const currentFolderParent = (
|
||||||
|
isSvg ? target.nextElementSibling : target.parentElement
|
||||||
// Get correct relative container and toggle collapsed class
|
) as MaybeHTMLElement
|
||||||
if (isSvg) {
|
if (!(childFolderContainer && currentFolderParent)) return
|
||||||
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
|
||||||
// Save folder state to localStorage
|
toggleCollapsedByPath(currentExplorerState, fullFolderPath)
|
||||||
const clickFolderPath = currentFolderParent.dataset.folderpath as string
|
const stringifiedFileTree = JSON.stringify(currentExplorerState)
|
||||||
|
|
||||||
const fullFolderPath = clickFolderPath
|
|
||||||
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.addEventListener("click", toggleFolder)
|
||||||
|
window.addCleanup(() => item.removeEventListener("click", toggleFolder))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
explorer.addEventListener("click", toggleExplorer)
|
||||||
|
window.addCleanup(() => explorer.removeEventListener("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.addEventListener("click", toggleFolder)
|
||||||
|
window.addCleanup(() => item.removeEventListener("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[] =
|
||||||
if (explorer) {
|
storageTree && useSavedFolderState ? JSON.parse(storageTree) : []
|
||||||
// Get config
|
const oldIndex = new Map(oldExplorerState.map((entry) => [entry.path, entry.collapsed]))
|
||||||
const collapseBehavior = explorer.dataset.behavior
|
const newExplorerState: FolderState[] = explorer.dataset.tree
|
||||||
|
? JSON.parse(explorer.dataset.tree)
|
||||||
// Add click handlers for all folders (click handler on folder "label")
|
: []
|
||||||
if (collapseBehavior === "collapse") {
|
currentExplorerState = []
|
||||||
Array.prototype.forEach.call(
|
for (const { path, collapsed } of newExplorerState) {
|
||||||
document.getElementsByClassName("folder-button"),
|
currentExplorerState.push({ path, collapsed: oldIndex.get(path) ?? collapsed })
|
||||||
function (item) {
|
|
||||||
item.removeEventListener("click", toggleFolder)
|
|
||||||
item.addEventListener("click", toggleFolder)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add click handler to main explorer
|
currentExplorerState.map((folderState) => {
|
||||||
explorer.removeEventListener("click", toggleExplorer)
|
const folderLi = document.querySelector(
|
||||||
explorer.addEventListener("click", toggleExplorer)
|
`[data-folderpath='${folderState.path}']`,
|
||||||
}
|
) as MaybeHTMLElement
|
||||||
|
const folderUl = folderLi?.parentElement?.nextElementSibling as MaybeHTMLElement
|
||||||
// Set up click handlers for each folder (click handler on folder "icon")
|
if (folderUl) {
|
||||||
Array.prototype.forEach.call(document.getElementsByClassName("folder-icon"), function (item) {
|
setFolderState(folderUl, folderState.collapsed)
|
||||||
item.removeEventListener("click", toggleFolder)
|
|
||||||
item.addEventListener("click", toggleFolder)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (storageTree && useSavedFolderState) {
|
|
||||||
// Get state from localStorage and set folder state
|
|
||||||
explorerState = JSON.parse(storageTree)
|
|
||||||
explorerState.map((folderUl) => {
|
|
||||||
// grab <li> element for matching folder path
|
|
||||||
const folderLi = document.querySelector(`[data-folderpath='${folderUl.path}']`) as HTMLElement
|
|
||||||
|
|
||||||
// Get corresponding content <ul> tag and set state
|
|
||||||
if (folderLi) {
|
|
||||||
const folderUL = folderLi.parentElement?.nextElementSibling
|
|
||||||
if (folderUL) {
|
|
||||||
setFolderState(folderUL as HTMLElement, folderUl.collapsed)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if (explorer?.dataset.tree) {
|
|
||||||
// If tree is not in localStorage or config is disabled, use tree passed from Explorer as dataset
|
|
||||||
explorerState = JSON.parse(explorer.dataset.tree)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("resize", setupExplorer)
|
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
|
||||||
@@ -142,11 +116,7 @@ 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) {
|
||||||
if (collapsed) {
|
return collapsed ? folderElement.classList.remove("open") : folderElement.classList.add("open")
|
||||||
folderElement?.classList.remove("open")
|
|
||||||
} else {
|
|
||||||
folderElement?.classList.add("open")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -319,12 +319,12 @@ function renderGlobalGraph() {
|
|||||||
registerEscapeHandler(container, hideGlobalGraph)
|
registerEscapeHandler(container, hideGlobalGraph)
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("nav", async (e: unknown) => {
|
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||||
const slug = (e as CustomEventMap["nav"]).detail.url
|
const slug = e.detail.url
|
||||||
addToVisited(slug)
|
addToVisited(slug)
|
||||||
await renderGraph("graph-container", slug)
|
await renderGraph("graph-container", slug)
|
||||||
|
|
||||||
const containerIcon = document.getElementById("global-graph-icon")
|
const containerIcon = document.getElementById("global-graph-icon")
|
||||||
containerIcon?.removeEventListener("click", renderGlobalGraph)
|
|
||||||
containerIcon?.addEventListener("click", renderGlobalGraph)
|
containerIcon?.addEventListener("click", renderGlobalGraph)
|
||||||
|
window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ async function mouseEnterHandler(
|
|||||||
document.addEventListener("nav", () => {
|
document.addEventListener("nav", () => {
|
||||||
const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[]
|
const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[]
|
||||||
for (const link of links) {
|
for (const link of links) {
|
||||||
link.removeEventListener("mouseenter", mouseEnterHandler)
|
|
||||||
link.addEventListener("mouseenter", mouseEnterHandler)
|
link.addEventListener("mouseenter", mouseEnterHandler)
|
||||||
|
window.addCleanup(() => link.removeEventListener("mouseenter", mouseEnterHandler))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Document, SimpleDocumentSearchResultSetUnit } from "flexsearch"
|
import FlexSearch 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, resolveRelative } from "../../util/path"
|
import { FullSlug, normalizeRelativeURLs, resolveRelative } from "../../util/path"
|
||||||
|
|
||||||
interface Item {
|
interface Item {
|
||||||
id: number
|
id: number
|
||||||
@@ -11,23 +11,53 @@ interface Item {
|
|||||||
tags: string[]
|
tags: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
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"
|
||||||
|
|
||||||
// Current searchType
|
|
||||||
let searchType: SearchType = "basic"
|
let searchType: SearchType = "basic"
|
||||||
|
let currentSearchTerm: string = ""
|
||||||
|
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
|
||||||
|
let index = new FlexSearch.Document<Item>({
|
||||||
|
charset: "latin:extra",
|
||||||
|
encode: encoder,
|
||||||
|
document: {
|
||||||
|
id: "id",
|
||||||
|
index: [
|
||||||
|
{
|
||||||
|
field: "title",
|
||||||
|
tokenize: "forward",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "content",
|
||||||
|
tokenize: "forward",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "tags",
|
||||||
|
tokenize: "forward",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const p = new DOMParser()
|
||||||
|
const fetchContentCache: Map<FullSlug, Element[]> = new Map()
|
||||||
const contextWindowWords = 30
|
const contextWindowWords = 30
|
||||||
const numSearchResults = 5
|
const numSearchResults = 8
|
||||||
const numTagResults = 3
|
const numTagResults = 5
|
||||||
|
|
||||||
|
const tokenizeTerm = (term: string) => {
|
||||||
|
const tokens = term.split(/\s+/).filter((t) => t.trim() !== "")
|
||||||
|
const tokenLen = tokens.length
|
||||||
|
if (tokenLen > 1) {
|
||||||
|
for (let i = 1; i < tokenLen; i++) {
|
||||||
|
tokens.push(tokens.slice(0, i + 1).join(" "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens.sort((a, b) => b.length - a.length) // always highlight longest terms first
|
||||||
|
}
|
||||||
|
|
||||||
function highlight(searchTerm: string, text: string, trim?: boolean) {
|
function highlight(searchTerm: string, text: string, trim?: boolean) {
|
||||||
// try to highlight longest tokens first
|
const tokenizedTerms = tokenizeTerm(searchTerm)
|
||||||
const tokenizedTerms = searchTerm
|
|
||||||
.split(/\s+/)
|
|
||||||
.filter((t) => t !== "")
|
|
||||||
.sort((a, b) => b.length - a.length)
|
|
||||||
let tokenizedText = text.split(/\s+/).filter((t) => t !== "")
|
let tokenizedText = text.split(/\s+/).filter((t) => t !== "")
|
||||||
|
|
||||||
let startIndex = 0
|
let startIndex = 0
|
||||||
@@ -71,20 +101,76 @@ function highlight(searchTerm: string, text: string, trim?: boolean) {
|
|||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
|
function highlightHTML(searchTerm: string, el: HTMLElement) {
|
||||||
let prevShortcutHandler: ((e: HTMLElementEventMap["keydown"]) => void) | undefined = undefined
|
const p = new DOMParser()
|
||||||
document.addEventListener("nav", async (e: unknown) => {
|
const tokenizedTerms = tokenizeTerm(searchTerm)
|
||||||
const currentSlug = (e as CustomEventMap["nav"]).detail.url
|
const html = p.parseFromString(el.innerHTML, "text/html")
|
||||||
|
|
||||||
|
const createHighlightSpan = (text: string) => {
|
||||||
|
const span = document.createElement("span")
|
||||||
|
span.className = "highlight"
|
||||||
|
span.textContent = text
|
||||||
|
return span
|
||||||
|
}
|
||||||
|
|
||||||
|
const highlightTextNodes = (node: Node, term: string) => {
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
|
const nodeText = node.nodeValue ?? ""
|
||||||
|
const regex = new RegExp(term.toLowerCase(), "gi")
|
||||||
|
const matches = nodeText.match(regex)
|
||||||
|
if (!matches || matches.length === 0) return
|
||||||
|
const spanContainer = document.createElement("span")
|
||||||
|
let lastIndex = 0
|
||||||
|
for (const match of matches) {
|
||||||
|
const matchIndex = nodeText.indexOf(match, lastIndex)
|
||||||
|
spanContainer.appendChild(document.createTextNode(nodeText.slice(lastIndex, matchIndex)))
|
||||||
|
spanContainer.appendChild(createHighlightSpan(match))
|
||||||
|
lastIndex = matchIndex + match.length
|
||||||
|
}
|
||||||
|
spanContainer.appendChild(document.createTextNode(nodeText.slice(lastIndex)))
|
||||||
|
node.parentNode?.replaceChild(spanContainer, node)
|
||||||
|
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
if ((node as HTMLElement).classList.contains("highlight")) return
|
||||||
|
Array.from(node.childNodes).forEach((child) => highlightTextNodes(child, term))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const term of tokenizedTerms) {
|
||||||
|
highlightTextNodes(html.body, term)
|
||||||
|
}
|
||||||
|
|
||||||
|
return html.body
|
||||||
|
}
|
||||||
|
|
||||||
|
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 results = document.getElementById("results-container")
|
const searchLayout = document.getElementById("search-layout")
|
||||||
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
|
||||||
|
let previewInner: HTMLDivElement | undefined = undefined
|
||||||
|
const results = document.createElement("div")
|
||||||
|
results.id = "results-container"
|
||||||
|
appendLayout(results)
|
||||||
|
|
||||||
|
if (enablePreview) {
|
||||||
|
preview = document.createElement("div")
|
||||||
|
preview.id = "preview-container"
|
||||||
|
appendLayout(preview)
|
||||||
|
}
|
||||||
|
|
||||||
function hideSearch() {
|
function hideSearch() {
|
||||||
container?.classList.remove("active")
|
container?.classList.remove("active")
|
||||||
if (searchBar) {
|
if (searchBar) {
|
||||||
@@ -96,6 +182,12 @@ document.addEventListener("nav", async (e: unknown) => {
|
|||||||
if (results) {
|
if (results) {
|
||||||
removeAllChildren(results)
|
removeAllChildren(results)
|
||||||
}
|
}
|
||||||
|
if (preview) {
|
||||||
|
removeAllChildren(preview)
|
||||||
|
}
|
||||||
|
if (searchLayout) {
|
||||||
|
searchLayout.classList.remove("display-results")
|
||||||
|
}
|
||||||
|
|
||||||
searchType = "basic" // reset search type after closing
|
searchType = "basic" // reset search type after closing
|
||||||
}
|
}
|
||||||
@@ -109,11 +201,14 @@ document.addEventListener("nav", async (e: unknown) => {
|
|||||||
searchBar?.focus()
|
searchBar?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
|
let currentHover: HTMLInputElement | null = null
|
||||||
|
|
||||||
|
async 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")
|
||||||
searchBarOpen ? hideSearch() : showSearch("basic")
|
searchBarOpen ? hideSearch() : showSearch("basic")
|
||||||
|
return
|
||||||
} else if (e.shiftKey && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
|
} else if (e.shiftKey && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
|
||||||
// Hotkey to open tag search
|
// Hotkey to open tag search
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -122,159 +217,205 @@ document.addEventListener("nav", async (e: unknown) => {
|
|||||||
|
|
||||||
// add "#" prefix for tag search
|
// add "#" prefix for tag search
|
||||||
if (searchBar) searchBar.value = "#"
|
if (searchBar) searchBar.value = "#"
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentHover) {
|
||||||
|
currentHover.classList.remove("focus")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If search is active, then we will render the first result and display accordingly
|
||||||
if (!container?.classList.contains("active")) return
|
if (!container?.classList.contains("active")) return
|
||||||
else if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
// If result has focus, navigate to that one, otherwise pick first result
|
// If result has focus, navigate to that one, otherwise pick first result
|
||||||
if (results?.contains(document.activeElement)) {
|
if (results?.contains(document.activeElement)) {
|
||||||
const active = document.activeElement as HTMLInputElement
|
const active = document.activeElement as HTMLInputElement
|
||||||
|
if (active.classList.contains("no-match")) return
|
||||||
|
await displayPreview(active)
|
||||||
active.click()
|
active.click()
|
||||||
} else {
|
} else {
|
||||||
const anchor = document.getElementsByClassName("result-card")[0] as HTMLInputElement | null
|
const anchor = document.getElementsByClassName("result-card")[0] as HTMLInputElement | null
|
||||||
anchor?.click()
|
if (!anchor || anchor?.classList.contains("no-match")) return
|
||||||
|
await displayPreview(anchor)
|
||||||
|
anchor.click()
|
||||||
}
|
}
|
||||||
} else if (e.key === "ArrowUp" || (e.shiftKey && e.key === "Tab")) {
|
} else 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 prevResult = document.activeElement?.previousElementSibling as HTMLInputElement | null
|
const currentResult = currentHover
|
||||||
|
? currentHover
|
||||||
|
: (document.activeElement as HTMLInputElement | null)
|
||||||
|
const prevResult = currentResult?.previousElementSibling as HTMLInputElement | null
|
||||||
|
currentResult?.classList.remove("focus")
|
||||||
prevResult?.focus()
|
prevResult?.focus()
|
||||||
|
if (prevResult) currentHover = prevResult
|
||||||
|
await displayPreview(prevResult)
|
||||||
}
|
}
|
||||||
} else if (e.key === "ArrowDown" || e.key === "Tab") {
|
} else if (e.key === "ArrowDown" || e.key === "Tab") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
// When first pressing ArrowDown, results wont contain the active element, so focus first element
|
// The results should already been focused, so we need to find the next one.
|
||||||
if (!results?.contains(document.activeElement)) {
|
// The activeElement is the search bar, so we need to find the first result and focus it.
|
||||||
const firstResult = resultCards[0] as HTMLInputElement | null
|
if (document.activeElement === searchBar || currentHover !== null) {
|
||||||
firstResult?.focus()
|
const firstResult = currentHover
|
||||||
} else {
|
? currentHover
|
||||||
// If an element in results-container already has focus, focus next one
|
: (document.getElementsByClassName("result-card")[0] as HTMLInputElement | null)
|
||||||
const nextResult = document.activeElement?.nextElementSibling as HTMLInputElement | null
|
const secondResult = firstResult?.nextElementSibling as HTMLInputElement | null
|
||||||
nextResult?.focus()
|
firstResult?.classList.remove("focus")
|
||||||
|
secondResult?.focus()
|
||||||
|
if (secondResult) currentHover = secondResult
|
||||||
|
await displayPreview(secondResult)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function trimContent(content: string) {
|
|
||||||
// works without escaping html like in `description.ts`
|
|
||||||
const sentences = content.replace(/\s+/g, " ").split(".")
|
|
||||||
let finalDesc = ""
|
|
||||||
let sentenceIdx = 0
|
|
||||||
|
|
||||||
// Roughly estimate characters by (words * 5). Matches description length in `description.ts`.
|
|
||||||
const len = contextWindowWords * 5
|
|
||||||
while (finalDesc.length < len) {
|
|
||||||
const sentence = sentences[sentenceIdx]
|
|
||||||
if (!sentence) break
|
|
||||||
finalDesc += sentence + "."
|
|
||||||
sentenceIdx++
|
|
||||||
}
|
|
||||||
|
|
||||||
// If more content would be available, indicate it by finishing with "..."
|
|
||||||
if (finalDesc.length < content.length) {
|
|
||||||
finalDesc += ".."
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalDesc
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatForDisplay = (term: string, id: number) => {
|
const formatForDisplay = (term: string, id: number) => {
|
||||||
const slug = idDataMap[id]
|
const slug = idDataMap[id]
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
slug,
|
slug,
|
||||||
title: searchType === "tags" ? data[slug].title : highlight(term, data[slug].title ?? ""),
|
title: searchType === "tags" ? data[slug].title : highlight(term, data[slug].title ?? ""),
|
||||||
// if searchType is tag, display context from start of file and trim, otherwise use regular highlight
|
content: highlight(term, data[slug].content ?? "", true),
|
||||||
content:
|
tags: highlightTags(term.substring(1), data[slug].tags),
|
||||||
searchType === "tags"
|
|
||||||
? trimContent(data[slug].content)
|
|
||||||
: highlight(term, data[slug].content ?? "", true),
|
|
||||||
tags: highlightTags(term, data[slug].tags),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function highlightTags(term: string, tags: string[]) {
|
function highlightTags(term: string, tags: string[]) {
|
||||||
if (tags && searchType === "tags") {
|
if (!tags || searchType !== "tags") {
|
||||||
// Find matching tags
|
|
||||||
const termLower = term.toLowerCase()
|
|
||||||
let matching = tags.filter((str) => str.includes(termLower))
|
|
||||||
|
|
||||||
// Subtract matching from original tags, then push difference
|
|
||||||
if (matching.length > 0) {
|
|
||||||
let difference = tags.filter((x) => !matching.includes(x))
|
|
||||||
|
|
||||||
// Convert to html (cant be done later as matches/term dont get passed to `resultToHTML`)
|
|
||||||
matching = matching.map((tag) => `<li><p class="match-tag">#${tag}</p></li>`)
|
|
||||||
difference = difference.map((tag) => `<li><p>#${tag}</p></li>`)
|
|
||||||
matching.push(...difference)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only allow max of `numTagResults` in preview
|
|
||||||
if (tags.length > numTagResults) {
|
|
||||||
matching.splice(numTagResults)
|
|
||||||
}
|
|
||||||
|
|
||||||
return matching
|
|
||||||
} else {
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return tags
|
||||||
|
.map((tag) => {
|
||||||
|
if (tag.toLowerCase().includes(term.toLowerCase())) {
|
||||||
|
return `<li><p class="match-tag">#${tag}</p></li>`
|
||||||
|
} else {
|
||||||
|
return `<li><p>#${tag}</p></li>`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.slice(0, numTagResults)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 class="tags">${tags.join("")}</ul>` : ``
|
||||||
const itemTile = document.createElement("a")
|
const itemTile = document.createElement("a")
|
||||||
itemTile.classList.add("result-card")
|
itemTile.classList.add("result-card")
|
||||||
itemTile.id = slug
|
itemTile.id = slug
|
||||||
itemTile.href = new URL(resolveRelative(currentSlug, slug), location.toString()).toString()
|
itemTile.href = resolveUrl(slug).toString()
|
||||||
itemTile.innerHTML = `<h3>${title}</h3>${htmlTags}<p>${content}</p>`
|
itemTile.innerHTML = `<h3>${title}</h3>${htmlTags}${
|
||||||
|
enablePreview && window.innerWidth > 600 ? "" : `<p>${content}</p>`
|
||||||
|
}`
|
||||||
itemTile.addEventListener("click", (event) => {
|
itemTile.addEventListener("click", (event) => {
|
||||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
|
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
|
||||||
hideSearch()
|
hideSearch()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handler = (event: MouseEvent) => {
|
||||||
|
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
|
||||||
|
hideSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onMouseEnter(ev: MouseEvent) {
|
||||||
|
if (!ev.target) return
|
||||||
|
const target = ev.target as HTMLInputElement
|
||||||
|
await displayPreview(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
itemTile.addEventListener("mouseenter", onMouseEnter)
|
||||||
|
window.addCleanup(() => itemTile.removeEventListener("mouseenter", onMouseEnter))
|
||||||
|
itemTile.addEventListener("click", handler)
|
||||||
|
window.addCleanup(() => itemTile.removeEventListener("click", handler))
|
||||||
|
|
||||||
return itemTile
|
return itemTile
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayResults(finalResults: Item[]) {
|
async function displayResults(finalResults: Item[]) {
|
||||||
if (!results) return
|
if (!results) return
|
||||||
|
|
||||||
removeAllChildren(results)
|
removeAllChildren(results)
|
||||||
if (finalResults.length === 0) {
|
if (finalResults.length === 0) {
|
||||||
results.innerHTML = `<button class="result-card">
|
results.innerHTML = `<a class="result-card no-match">
|
||||||
<h3>No results.</h3>
|
<h3>No results.</h3>
|
||||||
<p>Try another search term?</p>
|
<p>Try another search term?</p>
|
||||||
</button>`
|
</a>`
|
||||||
} else {
|
} else {
|
||||||
results.append(...finalResults.map(resultToHTML))
|
results.append(...finalResults.map(resultToHTML))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (finalResults.length === 0 && preview) {
|
||||||
|
// no results, clear previous preview
|
||||||
|
removeAllChildren(preview)
|
||||||
|
} else {
|
||||||
|
// focus on first result, then also dispatch preview immediately
|
||||||
|
const firstChild = results.firstElementChild as HTMLElement
|
||||||
|
firstChild.classList.add("focus")
|
||||||
|
currentHover = firstChild as HTMLInputElement
|
||||||
|
await displayPreview(firstChild)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 || !preview) return
|
||||||
|
const slug = el.id as FullSlug
|
||||||
|
const innerDiv = await fetchContent(slug).then((contents) =>
|
||||||
|
contents.flatMap((el) => [...highlightHTML(currentSearchTerm, el as HTMLElement).children]),
|
||||||
|
)
|
||||||
|
previewInner = document.createElement("div")
|
||||||
|
previewInner.classList.add("preview-inner")
|
||||||
|
previewInner.append(...innerDiv)
|
||||||
|
preview.replaceChildren(previewInner)
|
||||||
|
|
||||||
|
// scroll to longest
|
||||||
|
const highlights = [...preview.querySelectorAll(".highlight")].sort(
|
||||||
|
(a, b) => b.innerHTML.length - a.innerHTML.length,
|
||||||
|
)
|
||||||
|
highlights[0]?.scrollIntoView({ block: "start" })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onType(e: HTMLElementEventMap["input"]) {
|
async function onType(e: HTMLElementEventMap["input"]) {
|
||||||
let term = (e.target as HTMLInputElement).value
|
if (!searchLayout || !index) return
|
||||||
let searchResults: SimpleDocumentSearchResultSetUnit[]
|
currentSearchTerm = (e.target as HTMLInputElement).value
|
||||||
|
searchLayout.classList.toggle("display-results", currentSearchTerm !== "")
|
||||||
|
searchType = currentSearchTerm.startsWith("#") ? "tags" : "basic"
|
||||||
|
|
||||||
if (term.toLowerCase().startsWith("#")) {
|
let searchResults: FlexSearch.SimpleDocumentSearchResultSetUnit[]
|
||||||
searchType = "tags"
|
if (searchType === "tags") {
|
||||||
} else {
|
searchResults = await index.searchAsync({
|
||||||
searchType = "basic"
|
query: currentSearchTerm.substring(1),
|
||||||
}
|
limit: numSearchResults,
|
||||||
|
index: ["tags"],
|
||||||
switch (searchType) {
|
})
|
||||||
case "tags": {
|
} else if (searchType === "basic") {
|
||||||
term = term.substring(1)
|
searchResults = await index.searchAsync({
|
||||||
searchResults =
|
query: currentSearchTerm,
|
||||||
(await index?.searchAsync({ query: term, limit: numSearchResults, index: ["tags"] })) ??
|
|
||||||
[]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case "basic":
|
|
||||||
default: {
|
|
||||||
searchResults =
|
|
||||||
(await index?.searchAsync({
|
|
||||||
query: term,
|
|
||||||
limit: numSearchResults,
|
limit: numSearchResults,
|
||||||
index: ["title", "content"],
|
index: ["title", "content"],
|
||||||
})) ?? []
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getByField = (field: string): number[] => {
|
const getByField = (field: string): number[] => {
|
||||||
@@ -288,51 +429,19 @@ document.addEventListener("nav", async (e: unknown) => {
|
|||||||
...getByField("content"),
|
...getByField("content"),
|
||||||
...getByField("tags"),
|
...getByField("tags"),
|
||||||
])
|
])
|
||||||
const finalResults = [...allIds].map((id) => formatForDisplay(term, id))
|
const finalResults = [...allIds].map((id) => formatForDisplay(currentSearchTerm, id))
|
||||||
displayResults(finalResults)
|
await displayResults(finalResults)
|
||||||
}
|
|
||||||
|
|
||||||
if (prevShortcutHandler) {
|
|
||||||
document.removeEventListener("keydown", prevShortcutHandler)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("keydown", shortcutHandler)
|
document.addEventListener("keydown", shortcutHandler)
|
||||||
prevShortcutHandler = shortcutHandler
|
window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler))
|
||||||
searchIcon?.removeEventListener("click", () => showSearch("basic"))
|
|
||||||
searchIcon?.addEventListener("click", () => showSearch("basic"))
|
searchIcon?.addEventListener("click", () => showSearch("basic"))
|
||||||
searchBar?.removeEventListener("input", onType)
|
window.addCleanup(() => searchIcon?.removeEventListener("click", () => showSearch("basic")))
|
||||||
searchBar?.addEventListener("input", onType)
|
searchBar?.addEventListener("input", onType)
|
||||||
|
window.addCleanup(() => searchBar?.removeEventListener("input", onType))
|
||||||
|
|
||||||
// setup index if it hasn't been already
|
|
||||||
if (!index) {
|
|
||||||
index = new Document({
|
|
||||||
charset: "latin:extra",
|
|
||||||
optimize: true,
|
|
||||||
encode: encoder,
|
|
||||||
document: {
|
|
||||||
id: "id",
|
|
||||||
index: [
|
|
||||||
{
|
|
||||||
field: "title",
|
|
||||||
tokenize: "reverse",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "content",
|
|
||||||
tokenize: "reverse",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "tags",
|
|
||||||
tokenize: "reverse",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
fillDocument(index, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// register handlers
|
|
||||||
registerEscapeHandler(container, hideSearch)
|
registerEscapeHandler(container, hideSearch)
|
||||||
|
await fillDocument(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -340,16 +449,20 @@ document.addEventListener("nav", async (e: unknown) => {
|
|||||||
* @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: Document<Item, false>, data: any) {
|
async function fillDocument(data: { [key: FullSlug]: ContentDetails }) {
|
||||||
let id = 0
|
let id = 0
|
||||||
|
const promises: Array<Promise<unknown>> = []
|
||||||
for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
|
for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
|
||||||
await index.addAsync(id, {
|
promises.push(
|
||||||
|
index.addAsync(id++, {
|
||||||
id,
|
id,
|
||||||
slug: slug as FullSlug,
|
slug: slug as FullSlug,
|
||||||
title: fileData.title,
|
title: fileData.title,
|
||||||
content: fileData.content,
|
content: fileData.content,
|
||||||
tags: fileData.tags,
|
tags: fileData.tags,
|
||||||
})
|
}),
|
||||||
id++
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return await Promise.all(promises)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ function notifyNav(url: FullSlug) {
|
|||||||
document.dispatchEvent(event)
|
document.dispatchEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cleanupFns: Set<(...args: any[]) => void> = new Set()
|
||||||
|
window.addCleanup = (fn) => cleanupFns.add(fn)
|
||||||
|
|
||||||
let p: DOMParser
|
let p: DOMParser
|
||||||
async function navigate(url: URL, isBack: boolean = false) {
|
async function navigate(url: URL, isBack: boolean = false) {
|
||||||
p = p || new DOMParser()
|
p = p || new DOMParser()
|
||||||
@@ -57,6 +60,10 @@ async function navigate(url: URL, isBack: boolean = false) {
|
|||||||
|
|
||||||
if (!contents) return
|
if (!contents) return
|
||||||
|
|
||||||
|
// cleanup old
|
||||||
|
cleanupFns.forEach((fn) => fn())
|
||||||
|
cleanupFns.clear()
|
||||||
|
|
||||||
const html = p.parseFromString(contents, "text/html")
|
const html = p.parseFromString(contents, "text/html")
|
||||||
normalizeRelativeURLs(html, url)
|
normalizeRelativeURLs(html, url)
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ 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
|
const content = this.nextElementSibling as HTMLElement | undefined
|
||||||
|
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"
|
||||||
}
|
}
|
||||||
@@ -25,10 +26,11 @@ 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
|
const content = toc.nextElementSibling as HTMLElement | undefined
|
||||||
|
if (!content) return
|
||||||
content.style.maxHeight = collapsed ? "0px" : content.scrollHeight + "px"
|
content.style.maxHeight = collapsed ? "0px" : content.scrollHeight + "px"
|
||||||
toc.removeEventListener("click", toggleToc)
|
|
||||||
toc.addEventListener("click", toggleToc)
|
toc.addEventListener("click", toggleToc)
|
||||||
|
window.addCleanup(() => toc.removeEventListener("click", toggleToc))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb:
|
|||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
|
|
||||||
outsideContainer?.removeEventListener("click", click)
|
|
||||||
outsideContainer?.addEventListener("click", click)
|
outsideContainer?.addEventListener("click", click)
|
||||||
document.removeEventListener("keydown", esc)
|
window.addCleanup(() => outsideContainer?.removeEventListener("click", click))
|
||||||
document.addEventListener("keydown", esc)
|
document.addEventListener("keydown", esc)
|
||||||
|
window.addCleanup(() => document.removeEventListener("keydown", esc))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeAllChildren(node: HTMLElement) {
|
export function removeAllChildren(node: HTMLElement) {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@use "../../styles/variables.scss" as *;
|
||||||
|
|
||||||
button#explorer {
|
button#explorer {
|
||||||
all: unset;
|
all: unset;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@@ -85,7 +87,7 @@ svg {
|
|||||||
color: var(--secondary);
|
color: var(--secondary);
|
||||||
font-family: var(--headerFont);
|
font-family: var(--headerFont);
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 600;
|
font-weight: $boldWeight;
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
@@ -110,7 +112,7 @@ svg {
|
|||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: var(--secondary);
|
color: var(--secondary);
|
||||||
font-weight: 600;
|
font-weight: $boldWeight;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|||||||
@@ -54,18 +54,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
& > #search-space {
|
& > #search-space {
|
||||||
width: 50%;
|
width: 65%;
|
||||||
margin-top: 15vh;
|
margin-top: 12vh;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
|
||||||
@media all and (max-width: $fullPageWidth) {
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 5px;
|
border-radius: 7px;
|
||||||
background: var(--light);
|
background: var(--light);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 14px 50px rgba(27, 33, 48, 0.12),
|
0 14px 50px rgba(27, 33, 48, 0.12),
|
||||||
@@ -86,14 +82,93 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& > #search-layout {
|
||||||
|
display: none;
|
||||||
|
flex-direction: row;
|
||||||
|
border: 1px solid var(--lightgray);
|
||||||
|
flex: 0 0 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&.display-results {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-preview] > #results-container {
|
||||||
|
flex: 0 0 min(30%, 450px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (min-width: $tabletBreakpoint) {
|
||||||
|
&[data-preview] {
|
||||||
|
& .result-card > p.preview {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
&:first-child {
|
||||||
|
border-right: 1px solid var(--lightgray);
|
||||||
|
border-top-right-radius: unset;
|
||||||
|
border-bottom-right-radius: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-top-left-radius: unset;
|
||||||
|
border-bottom-left-radius: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
height: calc(75vh - 12vh);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: $tabletBreakpoint) {
|
||||||
|
& > #preview-container {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-preview] > #results-container {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
flex: 0 0 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .highlight {
|
||||||
|
background: color-mix(in srgb, var(--tertiary) 60%, rgba(255, 255, 255, 0));
|
||||||
|
border-radius: 5px;
|
||||||
|
scroll-margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > #preview-container {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: inherit;
|
||||||
|
color: var(--dark);
|
||||||
|
line-height: 1.5em;
|
||||||
|
font-weight: $normalWeight;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0 2rem;
|
||||||
|
|
||||||
|
& .preview-inner {
|
||||||
|
margin: 0 auto;
|
||||||
|
width: min($pageWidth, 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
& > #results-container {
|
& > #results-container {
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
& .result-card {
|
& .result-card {
|
||||||
|
overflow: hidden;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s ease;
|
transition: background 0.2s ease;
|
||||||
border: 1px solid var(--lightgray);
|
border-bottom: 1px solid var(--lightgray);
|
||||||
border-bottom: none;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
// normalize card props
|
// normalize card props
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
@@ -102,71 +177,36 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
background: var(--light);
|
|
||||||
outline: none;
|
outline: none;
|
||||||
font-weight: inherit;
|
font-weight: inherit;
|
||||||
|
|
||||||
& .highlight {
|
|
||||||
color: var(--secondary);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
&: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;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > ul > li {
|
& > ul.tags {
|
||||||
margin: 0;
|
|
||||||
display: inline-block;
|
|
||||||
white-space: nowrap;
|
|
||||||
margin: 0;
|
|
||||||
overflow-wrap: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > ul {
|
|
||||||
list-style: none;
|
|
||||||
display: flex;
|
|
||||||
padding-left: 0;
|
|
||||||
gap: 0.4rem;
|
|
||||||
margin: 0;
|
|
||||||
margin-top: 0.45rem;
|
margin-top: 0.45rem;
|
||||||
// Offset border radius
|
margin-bottom: 0;
|
||||||
margin-left: -2px;
|
|
||||||
overflow: hidden;
|
|
||||||
background-clip: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& > ul > li > p {
|
& > ul > li > p {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: var(--highlight);
|
background-color: var(--highlight);
|
||||||
overflow: hidden;
|
padding: 0.2rem 0.4rem;
|
||||||
background-clip: border-box;
|
margin: 0 0.1rem;
|
||||||
padding: 0.03rem 0.4rem;
|
line-height: 1.4rem;
|
||||||
margin: 0;
|
font-weight: $boldWeight;
|
||||||
color: var(--secondary);
|
color: var(--secondary);
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > ul > li > .match-tag {
|
&.match-tag {
|
||||||
color: var(--tertiary);
|
color: var(--tertiary);
|
||||||
font-weight: bold;
|
}
|
||||||
opacity: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& > p {
|
& > p {
|
||||||
@@ -176,4 +216,5 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
quartz/i18n/index.ts
Normal file
11
quartz/i18n/index.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Translation } from "./locales/definition"
|
||||||
|
import en from "./locales/en-US"
|
||||||
|
import fr from "./locales/fr-FR"
|
||||||
|
|
||||||
|
export const TRANSLATIONS = {
|
||||||
|
"en-US": en,
|
||||||
|
"fr-FR": fr,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const i18n = (locale: ValidLocale): Translation => TRANSLATIONS[locale]
|
||||||
|
export type ValidLocale = keyof typeof TRANSLATIONS
|
||||||
63
quartz/i18n/locales/definition.ts
Normal file
63
quartz/i18n/locales/definition.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { FullSlug } from "../../util/path"
|
||||||
|
|
||||||
|
export interface Translation {
|
||||||
|
propertyDefaults: {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
components: {
|
||||||
|
backlinks: {
|
||||||
|
title: string
|
||||||
|
noBacklinksFound: string
|
||||||
|
}
|
||||||
|
themeToggle: {
|
||||||
|
lightMode: string
|
||||||
|
darkMode: string
|
||||||
|
}
|
||||||
|
explorer: {
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
footer: {
|
||||||
|
createdWith: string
|
||||||
|
}
|
||||||
|
graph: {
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
recentNotes: {
|
||||||
|
title: string
|
||||||
|
seeRemainingMore: (variables: { remaining: number }) => string
|
||||||
|
}
|
||||||
|
transcludes: {
|
||||||
|
transcludeOf: (variables: { targetSlug: FullSlug }) => string
|
||||||
|
linkToOriginal: string
|
||||||
|
}
|
||||||
|
search: {
|
||||||
|
title: string
|
||||||
|
searchBarPlaceholder: string
|
||||||
|
}
|
||||||
|
tableOfContents: {
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pages: {
|
||||||
|
rss: {
|
||||||
|
recentNotes: string
|
||||||
|
lastFewNotes: (variables: { count: number }) => string
|
||||||
|
}
|
||||||
|
error: {
|
||||||
|
title: string
|
||||||
|
notFound: string
|
||||||
|
}
|
||||||
|
folderContent: {
|
||||||
|
folder: string
|
||||||
|
itemsUnderFolder: (variables: { count: number }) => string
|
||||||
|
}
|
||||||
|
tagContent: {
|
||||||
|
tag: string
|
||||||
|
tagIndex: string
|
||||||
|
itemsUnderTag: (variables: { count: number }) => string
|
||||||
|
showingFirst: (variables: { count: number }) => string
|
||||||
|
totalTags: (variables: { count: number }) => string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
65
quartz/i18n/locales/en-US.ts
Normal file
65
quartz/i18n/locales/en-US.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { Translation } from "./definition"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
propertyDefaults: {
|
||||||
|
title: "Untitled",
|
||||||
|
description: "No description provided",
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
backlinks: {
|
||||||
|
title: "Backlinks",
|
||||||
|
noBacklinksFound: "No backlinks found",
|
||||||
|
},
|
||||||
|
themeToggle: {
|
||||||
|
lightMode: "Light mode",
|
||||||
|
darkMode: "Dark mode",
|
||||||
|
},
|
||||||
|
explorer: {
|
||||||
|
title: "Explorer",
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
createdWith: "Created with",
|
||||||
|
},
|
||||||
|
graph: {
|
||||||
|
title: "Graph View",
|
||||||
|
},
|
||||||
|
recentNotes: {
|
||||||
|
title: "Recent Notes",
|
||||||
|
seeRemainingMore: ({ remaining }) => `See ${remaining} more →`,
|
||||||
|
},
|
||||||
|
transcludes: {
|
||||||
|
transcludeOf: ({ targetSlug }) => `Transclude of ${targetSlug}`,
|
||||||
|
linkToOriginal: "Link to original",
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
title: "Search",
|
||||||
|
searchBarPlaceholder: "Search for something",
|
||||||
|
},
|
||||||
|
tableOfContents: {
|
||||||
|
title: "Table of Contents",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
rss: {
|
||||||
|
recentNotes: "Recent notes",
|
||||||
|
lastFewNotes: ({ count }) => `Last ${count} notes`,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
title: "Not Found",
|
||||||
|
notFound: "Either this page is private or doesn't exist.",
|
||||||
|
},
|
||||||
|
folderContent: {
|
||||||
|
folder: "Folder",
|
||||||
|
itemsUnderFolder: ({ count }) =>
|
||||||
|
count === 1 ? "1 item under this folder" : `${count} items under this folder.`,
|
||||||
|
},
|
||||||
|
tagContent: {
|
||||||
|
tag: "Tag",
|
||||||
|
tagIndex: "Tag Index",
|
||||||
|
itemsUnderTag: ({ count }) =>
|
||||||
|
count === 1 ? "1 item with this tag" : `${count} items with this tag.`,
|
||||||
|
showingFirst: ({ count }) => `Showing first ${count} tags.`,
|
||||||
|
totalTags: ({ count }) => `Found ${count} total tags.`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const satisfies Translation
|
||||||
65
quartz/i18n/locales/fr-FR.ts
Normal file
65
quartz/i18n/locales/fr-FR.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { Translation } from "./definition"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
propertyDefaults: {
|
||||||
|
title: "Sans titre",
|
||||||
|
description: "Aucune description fournie",
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
backlinks: {
|
||||||
|
title: "Liens retour",
|
||||||
|
noBacklinksFound: "Aucun lien retour trouvé",
|
||||||
|
},
|
||||||
|
themeToggle: {
|
||||||
|
lightMode: "Mode clair",
|
||||||
|
darkMode: "Mode sombre",
|
||||||
|
},
|
||||||
|
explorer: {
|
||||||
|
title: "Explorateur",
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
createdWith: "Créé avec",
|
||||||
|
},
|
||||||
|
graph: {
|
||||||
|
title: "Vue Graphique",
|
||||||
|
},
|
||||||
|
recentNotes: {
|
||||||
|
title: "Notes Récentes",
|
||||||
|
seeRemainingMore: ({ remaining }) => `Voir ${remaining} de plus →`,
|
||||||
|
},
|
||||||
|
transcludes: {
|
||||||
|
transcludeOf: ({ targetSlug }) => `Transclusion de ${targetSlug}`,
|
||||||
|
linkToOriginal: "Lien vers l'original",
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
title: "Recherche",
|
||||||
|
searchBarPlaceholder: "Rechercher quelque chose",
|
||||||
|
},
|
||||||
|
tableOfContents: {
|
||||||
|
title: "Table des Matières",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
rss: {
|
||||||
|
recentNotes: "Notes récentes",
|
||||||
|
lastFewNotes: ({ count }) => `Les dernières ${count} notes`,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
title: "Pas trouvé",
|
||||||
|
notFound: "Cette page est soit privée, soit elle n'existe pas.",
|
||||||
|
},
|
||||||
|
folderContent: {
|
||||||
|
folder: "Dossier",
|
||||||
|
itemsUnderFolder: ({ count }) =>
|
||||||
|
count === 1 ? "1 élément sous ce dossier" : `${count} éléments sous ce dossier.`,
|
||||||
|
},
|
||||||
|
tagContent: {
|
||||||
|
tag: "Étiquette",
|
||||||
|
tagIndex: "Index des étiquettes",
|
||||||
|
itemsUnderTag: ({ count }) =>
|
||||||
|
count === 1 ? "1 élément avec cette étiquette" : `${count} éléments avec cette étiquette.`,
|
||||||
|
showingFirst: ({ count }) => `Affichage des premières ${count} étiquettes.`,
|
||||||
|
totalTags: ({ count }) => `Trouvé ${count} étiquettes au total.`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const satisfies Translation
|
||||||
@@ -8,6 +8,7 @@ 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"
|
import { write } from "./helpers"
|
||||||
|
import { i18n } from "../../i18n"
|
||||||
|
|
||||||
export const NotFoundPage: QuartzEmitterPlugin = () => {
|
export const NotFoundPage: QuartzEmitterPlugin = () => {
|
||||||
const opts: FullPageLayout = {
|
const opts: FullPageLayout = {
|
||||||
@@ -33,11 +34,12 @@ export const NotFoundPage: QuartzEmitterPlugin = () => {
|
|||||||
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
|
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
|
||||||
const path = url.pathname as FullSlug
|
const path = url.pathname as FullSlug
|
||||||
const externalResources = pageResources(path, resources)
|
const externalResources = pageResources(path, resources)
|
||||||
|
const notFound = i18n(cfg.locale).pages.error.title
|
||||||
const [tree, vfile] = defaultProcessedContent({
|
const [tree, vfile] = defaultProcessedContent({
|
||||||
slug,
|
slug,
|
||||||
text: "Not Found",
|
text: notFound,
|
||||||
description: "Not Found",
|
description: notFound,
|
||||||
frontmatter: { title: "Not Found", tags: [] },
|
frontmatter: { title: notFound, tags: [] },
|
||||||
})
|
})
|
||||||
const componentData: QuartzComponentProps = {
|
const componentData: QuartzComponentProps = {
|
||||||
fileData: vfile.data,
|
fileData: vfile.data,
|
||||||
@@ -51,7 +53,7 @@ export const NotFoundPage: QuartzEmitterPlugin = () => {
|
|||||||
return [
|
return [
|
||||||
await write({
|
await write({
|
||||||
ctx,
|
ctx,
|
||||||
content: renderPage(slug, componentData, opts, externalResources),
|
content: renderPage(cfg, slug, componentData, opts, externalResources),
|
||||||
slug,
|
slug,
|
||||||
ext: ".html",
|
ext: ".html",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -15,12 +15,7 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
|
|||||||
for (const [_tree, file] of content) {
|
for (const [_tree, file] of content) {
|
||||||
const ogSlug = simplifySlug(file.data.slug!)
|
const ogSlug = simplifySlug(file.data.slug!)
|
||||||
const dir = path.posix.relative(argv.directory, 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") {
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ function addGlobalPageResources(
|
|||||||
} 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 = "https://analytics.umami.is/script.js"
|
umamiScript.src = cfg.analytics.host ?? "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
|
||||||
|
|
||||||
@@ -132,8 +132,10 @@ function addGlobalPageResources(
|
|||||||
} else {
|
} else {
|
||||||
componentResources.afterDOMLoaded.push(`
|
componentResources.afterDOMLoaded.push(`
|
||||||
window.spaNavigate = (url, _) => window.location.assign(url)
|
window.spaNavigate = (url, _) => window.location.assign(url)
|
||||||
|
window.addCleanup = () => {}
|
||||||
const event = new CustomEvent("nav", { detail: { url: document.body.dataset.slug } })
|
const event = new CustomEvent("nav", { detail: { url: document.body.dataset.slug } })
|
||||||
document.dispatchEvent(event)`)
|
document.dispatchEvent(event)
|
||||||
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
let wsUrl = `ws://localhost:${ctx.argv.wsPort}`
|
let wsUrl = `ws://localhost:${ctx.argv.wsPort}`
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { escapeHTML } from "../../util/escape"
|
|||||||
import { FilePath, FullSlug, SimpleSlug, joinSegments, simplifySlug } from "../../util/path"
|
import { FilePath, FullSlug, SimpleSlug, joinSegments, simplifySlug } from "../../util/path"
|
||||||
import { QuartzEmitterPlugin } from "../types"
|
import { QuartzEmitterPlugin } from "../types"
|
||||||
import { toHtml } from "hast-util-to-html"
|
import { toHtml } from "hast-util-to-html"
|
||||||
import path from "path"
|
|
||||||
import { write } from "./helpers"
|
import { write } from "./helpers"
|
||||||
|
import { i18n } from "../../i18n"
|
||||||
|
|
||||||
export type ContentIndex = Map<FullSlug, ContentDetails>
|
export type ContentIndex = Map<FullSlug, ContentDetails>
|
||||||
export type ContentDetails = {
|
export type ContentDetails = {
|
||||||
@@ -39,7 +39,7 @@ 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://${joinSegments(base, encodeURI(slug))}</loc>
|
||||||
<lastmod>${content.date?.toISOString()}</lastmod>
|
${content.date && `<lastmod>${content.date.toISOString()}</lastmod>`}
|
||||||
</url>`
|
</url>`
|
||||||
const urls = Array.from(idx)
|
const urls = Array.from(idx)
|
||||||
.map(([slug, content]) => createURLEntry(simplifySlug(slug), content))
|
.map(([slug, content]) => createURLEntry(simplifySlug(slug), content))
|
||||||
@@ -79,7 +79,7 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: nu
|
|||||||
<channel>
|
<channel>
|
||||||
<title>${escapeHTML(cfg.pageTitle)}</title>
|
<title>${escapeHTML(cfg.pageTitle)}</title>
|
||||||
<link>https://${base}</link>
|
<link>https://${base}</link>
|
||||||
<description>${!!limit ? `Last ${limit} notes` : "Recent notes"} on ${escapeHTML(
|
<description>${!!limit ? i18n(cfg.locale).pages.rss.lastFewNotes({ count: limit }) : i18n(cfg.locale).pages.rss.recentNotes} on ${escapeHTML(
|
||||||
cfg.pageTitle,
|
cfg.pageTitle,
|
||||||
)}</description>
|
)}</description>
|
||||||
<generator>Quartz -- quartz.jzhao.xyz</generator>
|
<generator>Quartz -- quartz.jzhao.xyz</generator>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
|
|||||||
allFiles,
|
allFiles,
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = renderPage(slug, componentData, opts, externalResources)
|
const content = renderPage(cfg, slug, componentData, opts, externalResources)
|
||||||
const fp = await write({
|
const fp = await write({
|
||||||
ctx,
|
ctx,
|
||||||
content,
|
content,
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ import {
|
|||||||
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
|
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
|
||||||
import { FolderContent } from "../../components"
|
import { FolderContent } from "../../components"
|
||||||
import { write } from "./helpers"
|
import { write } from "./helpers"
|
||||||
|
import { i18n } from "../../i18n"
|
||||||
|
|
||||||
export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
|
export const FolderPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
|
||||||
const opts: FullPageLayout = {
|
const opts: FullPageLayout = {
|
||||||
...sharedPageComponents,
|
...sharedPageComponents,
|
||||||
...defaultListPageLayout,
|
...defaultListPageLayout,
|
||||||
@@ -57,7 +58,10 @@ export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
|
|||||||
folder,
|
folder,
|
||||||
defaultProcessedContent({
|
defaultProcessedContent({
|
||||||
slug: joinSegments(folder, "index") as FullSlug,
|
slug: joinSegments(folder, "index") as FullSlug,
|
||||||
frontmatter: { title: `Folder: ${folder}`, tags: [] },
|
frontmatter: {
|
||||||
|
title: `${i18n(cfg.locale).pages.folderContent.folder}: ${folder}`,
|
||||||
|
tags: [],
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
@@ -82,7 +86,7 @@ export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
|
|||||||
allFiles,
|
allFiles,
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = renderPage(slug, componentData, opts, externalResources)
|
const content = renderPage(cfg, slug, componentData, opts, externalResources)
|
||||||
const fp = await write({
|
const fp = await write({
|
||||||
ctx,
|
ctx,
|
||||||
content,
|
content,
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ import {
|
|||||||
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
|
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
|
||||||
import { TagContent } from "../../components"
|
import { TagContent } from "../../components"
|
||||||
import { write } from "./helpers"
|
import { write } from "./helpers"
|
||||||
|
import { i18n } from "../../i18n"
|
||||||
|
|
||||||
export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
|
export const TagPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
|
||||||
const opts: FullPageLayout = {
|
const opts: FullPageLayout = {
|
||||||
...sharedPageComponents,
|
...sharedPageComponents,
|
||||||
...defaultListPageLayout,
|
...defaultListPageLayout,
|
||||||
@@ -47,7 +48,10 @@ export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
|
|||||||
|
|
||||||
const tagDescriptions: Record<string, ProcessedContent> = Object.fromEntries(
|
const tagDescriptions: Record<string, ProcessedContent> = Object.fromEntries(
|
||||||
[...tags].map((tag) => {
|
[...tags].map((tag) => {
|
||||||
const title = tag === "index" ? "Tag Index" : `Tag: #${tag}`
|
const title =
|
||||||
|
tag === "index"
|
||||||
|
? i18n(cfg.locale).pages.tagContent.tagIndex
|
||||||
|
: `${i18n(cfg.locale).pages.tagContent.tag}: #${tag}`
|
||||||
return [
|
return [
|
||||||
tag,
|
tag,
|
||||||
defaultProcessedContent({
|
defaultProcessedContent({
|
||||||
@@ -81,7 +85,7 @@ export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
|
|||||||
allFiles,
|
allFiles,
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = renderPage(slug, componentData, opts, externalResources)
|
const content = renderPage(cfg, slug, componentData, opts, externalResources)
|
||||||
const fp = await write({
|
const fp = await write({
|
||||||
ctx,
|
ctx,
|
||||||
content,
|
content,
|
||||||
|
|||||||
@@ -3,11 +3,6 @@ import { QuartzFilterPlugin } from "../types"
|
|||||||
export const ExplicitPublish: QuartzFilterPlugin = () => ({
|
export const ExplicitPublish: QuartzFilterPlugin = () => ({
|
||||||
name: "ExplicitPublish",
|
name: "ExplicitPublish",
|
||||||
shouldPublish(_ctx, [_tree, vfile]) {
|
shouldPublish(_ctx, [_tree, vfile]) {
|
||||||
const publishProperty = vfile.data?.frontmatter?.publish ?? false
|
return vfile.data?.frontmatter?.publish ?? false
|
||||||
const publishFlag =
|
|
||||||
typeof publishProperty === "string"
|
|
||||||
? publishProperty.toLowerCase() === "true"
|
|
||||||
: Boolean(publishProperty)
|
|
||||||
return publishFlag
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,26 +5,46 @@ import yaml from "js-yaml"
|
|||||||
import toml from "toml"
|
import toml from "toml"
|
||||||
import { slugTag } from "../../util/path"
|
import { slugTag } from "../../util/path"
|
||||||
import { QuartzPluginData } from "../vfile"
|
import { QuartzPluginData } from "../vfile"
|
||||||
|
import { i18n } from "../../i18n"
|
||||||
|
|
||||||
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) => {
|
||||||
const opts = { ...defaultOptions, ...userOpts }
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
return {
|
return {
|
||||||
name: "FrontMatter",
|
name: "FrontMatter",
|
||||||
markdownPlugins() {
|
markdownPlugins({ cfg }) {
|
||||||
const { oneLineTagDelim } = opts
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
[remarkFrontmatter, ["yaml", "toml"]],
|
[remarkFrontmatter, ["yaml", "toml"]],
|
||||||
() => {
|
() => {
|
||||||
@@ -37,35 +57,19 @@ 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 ?? i18n(cfg.configuration.locale).propertyDefaults.title
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.tags) {
|
const tags = coerceToArray(coalesceAliases(data, ["tags", "tag"]))
|
||||||
// coerce to array
|
if (tags) data.tags = [...new Set(tags.map((tag: string) => slugTag(tag)))]
|
||||||
if (!Array.isArray(data.tags)) {
|
|
||||||
data.tags = data.tags
|
|
||||||
.toString()
|
|
||||||
.split(oneLineTagDelim)
|
|
||||||
.map((tag: string) => tag.trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove all non-string tags
|
const aliases = coerceToArray(coalesceAliases(data, ["aliases", "alias"]))
|
||||||
data.tags = data.tags
|
if (aliases) data.aliases = aliases
|
||||||
.filter((tag: unknown) => typeof tag === "string" || typeof tag === "number")
|
const cssclasses = coerceToArray(coalesceAliases(data, ["cssclasses", "cssclass"]))
|
||||||
.map((tag: string | number) => tag.toString())
|
if (cssclasses) data.cssclasses = cssclasses
|
||||||
}
|
|
||||||
|
|
||||||
// slug them all!!
|
|
||||||
data.tags = [...new Set(data.tags?.map((tag: string) => slugTag(tag)))]
|
|
||||||
|
|
||||||
// fill in frontmatter
|
// fill in frontmatter
|
||||||
file.data.frontmatter = data as QuartzPluginData["frontmatter"]
|
file.data.frontmatter = data as QuartzPluginData["frontmatter"]
|
||||||
@@ -78,9 +82,16 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined>
|
|||||||
|
|
||||||
declare module "vfile" {
|
declare module "vfile" {
|
||||||
interface DataMap {
|
interface DataMap {
|
||||||
frontmatter: { [key: string]: any } & {
|
frontmatter: { [key: string]: unknown } & {
|
||||||
title: string
|
title: string
|
||||||
|
} & Partial<{
|
||||||
tags: string[]
|
tags: string[]
|
||||||
}
|
aliases: string[]
|
||||||
|
description: string
|
||||||
|
publish: boolean
|
||||||
|
draft: boolean
|
||||||
|
enableToc: string
|
||||||
|
cssclasses: string[]
|
||||||
|
}>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,8 +37,36 @@ export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> |
|
|||||||
"data-no-popover": true,
|
"data-no-popover": true,
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
type: "text",
|
type: "element",
|
||||||
value: " §",
|
tagName: "svg",
|
||||||
|
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: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -43,18 +43,18 @@ export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | und
|
|||||||
let published: MaybeDate = undefined
|
let published: MaybeDate = undefined
|
||||||
|
|
||||||
const fp = file.data.filePath!
|
const fp = file.data.filePath!
|
||||||
const fullFp = path.posix.join(file.cwd, fp)
|
const fullFp = path.isAbsolute(fp) ? fp : path.posix.join(file.cwd, fp)
|
||||||
for (const source of opts.priority) {
|
for (const source of opts.priority) {
|
||||||
if (source === "filesystem") {
|
if (source === "filesystem") {
|
||||||
const st = await fs.promises.stat(fullFp)
|
const st = await fs.promises.stat(fullFp)
|
||||||
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
|
created ||= file.data.frontmatter.date as MaybeDate
|
||||||
modified ||= file.data.frontmatter.lastmod
|
modified ||= file.data.frontmatter.lastmod as MaybeDate
|
||||||
modified ||= file.data.frontmatter.updated
|
modified ||= file.data.frontmatter.updated as MaybeDate
|
||||||
modified ||= file.data.frontmatter["last-modified"]
|
modified ||= file.data.frontmatter["last-modified"] as MaybeDate
|
||||||
published ||= file.data.frontmatter.publishDate
|
published ||= file.data.frontmatter.publishDate as MaybeDate
|
||||||
} else if (source === "git") {
|
} else if (source === "git") {
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
// Get a reference to the main git repo.
|
// Get a reference to the main git repo.
|
||||||
|
|||||||
@@ -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.0/dist/katex.min.css",
|
"https://cdn.jsdelivr.net/npm/katex@0.16.9/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.7/dist/contrib/copy-tex.min.js",
|
src: "https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/copy-tex.min.js",
|
||||||
loadTime: "afterDOMReady",
|
loadTime: "afterDOMReady",
|
||||||
contentType: "external",
|
contentType: "external",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -44,39 +44,7 @@ const defaultOptions: Options = {
|
|||||||
enableVideoEmbed: true,
|
enableVideoEmbed: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
const icons = {
|
const calloutMapping = {
|
||||||
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",
|
||||||
@@ -104,24 +72,36 @@ const calloutMapping: Record<string, keyof typeof callouts> = {
|
|||||||
example: "example",
|
example: "example",
|
||||||
quote: "quote",
|
quote: "quote",
|
||||||
cite: "quote",
|
cite: "quote",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const arrowMapping: Record<string, string> = {
|
||||||
|
"->": "→",
|
||||||
|
"-->": "⇒",
|
||||||
|
"=>": "⇒",
|
||||||
|
"==>": "⇒",
|
||||||
|
"<-": "←",
|
||||||
|
"<--": "⇐",
|
||||||
|
"<=": "⇐",
|
||||||
|
"<==": "⇐",
|
||||||
}
|
}
|
||||||
|
|
||||||
function canonicalizeCallout(calloutName: string): keyof typeof callouts {
|
function canonicalizeCallout(calloutName: string): keyof typeof calloutMapping {
|
||||||
let callout = calloutName.toLowerCase() as keyof typeof calloutMapping
|
const normalizedCallout = calloutName.toLowerCase() as keyof typeof calloutMapping
|
||||||
return calloutMapping[callout] ?? "note"
|
// if callout is not recognized, make it a custom one
|
||||||
|
return calloutMapping[normalizedCallout] ?? calloutName
|
||||||
}
|
}
|
||||||
|
|
||||||
export const externalLinkRegex = /^https?:\/\//i
|
export const externalLinkRegex = /^https?:\/\//i
|
||||||
|
|
||||||
export const arrowRegex = new RegExp(/-{1,2}>/, "g")
|
export const arrowRegex = new RegExp(/(-{1,2}>|={1,2}>|<-{1,2}|<={1,2})/, "g")
|
||||||
|
|
||||||
// !? -> 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")
|
||||||
@@ -137,6 +117,9 @@ const tagRegex = new RegExp(/(?:^| )#((?:[-_\p{L}\p{Emoji}\d])+(?:\/[-_\p{L}\p{E
|
|||||||
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 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 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,
|
||||||
@@ -221,10 +204,10 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
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", ".webp"].includes(ext)) {
|
||||||
const dims = alias ?? ""
|
const match = wikilinkImageEmbedRegex.exec(alias ?? "")
|
||||||
let [width, height] = dims.split("x", 2)
|
const alt = match?.groups?.alt ?? ""
|
||||||
width ||= "auto"
|
const width = match?.groups?.width ?? "auto"
|
||||||
height ||= "auto"
|
const height = match?.groups?.height ?? "auto"
|
||||||
return {
|
return {
|
||||||
type: "image",
|
type: "image",
|
||||||
url,
|
url,
|
||||||
@@ -232,6 +215,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
hProperties: {
|
hProperties: {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
alt,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -298,10 +282,12 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
if (opts.parseArrows) {
|
if (opts.parseArrows) {
|
||||||
replacements.push([
|
replacements.push([
|
||||||
arrowRegex,
|
arrowRegex,
|
||||||
(_value: string, ..._capture: string[]) => {
|
(value: string, ..._capture: string[]) => {
|
||||||
|
const maybeArrow = arrowMapping[value]
|
||||||
|
if (maybeArrow === undefined) return SKIP
|
||||||
return {
|
return {
|
||||||
type: "html",
|
type: "html",
|
||||||
value: `<span>→</span>`,
|
value: `<span>${maybeArrow}</span>`,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@@ -317,8 +303,9 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
}
|
}
|
||||||
|
|
||||||
tag = slugTag(tag)
|
tag = slugTag(tag)
|
||||||
if (file.data.frontmatter && !file.data.frontmatter.tags.includes(tag)) {
|
if (file.data.frontmatter) {
|
||||||
file.data.frontmatter.tags.push(tag)
|
const noteTags = file.data.frontmatter.tags ?? []
|
||||||
|
file.data.frontmatter.tags = [...new Set([...noteTags, tag])]
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -406,32 +393,31 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
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(
|
const calloutType = canonicalizeCallout(typeString.toLowerCase())
|
||||||
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()
|
||||||
match.input.slice(calloutDirective.length).trim() || capitalize(calloutType)
|
const useDefaultTitle = titleContent === "" && restOfTitle.length === 0
|
||||||
const titleNode: Paragraph = {
|
const titleNode: Paragraph = {
|
||||||
type: "paragraph",
|
type: "paragraph",
|
||||||
children:
|
children: [
|
||||||
restOfTitle.length === 0
|
{
|
||||||
? [{ type: "text", value: titleContent + " " }]
|
type: "text",
|
||||||
: restOfTitle,
|
value: useDefaultTitle ? capitalize(calloutType) : titleContent + " ",
|
||||||
|
},
|
||||||
|
...restOfTitle,
|
||||||
|
],
|
||||||
}
|
}
|
||||||
const title = mdastToHtml(titleNode)
|
const title = mdastToHtml(titleNode)
|
||||||
|
|
||||||
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">
|
const toggleIcon = `<div class="fold-callout-icon"></div>`
|
||||||
<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">${callouts[calloutType]}</div>
|
<div class="callout-icon"></div>
|
||||||
<div class="callout-title-inner">${title}</div>
|
<div class="callout-title-inner">${title}</div>
|
||||||
${collapse ? toggleIcon : ""}
|
${collapse ? toggleIcon : ""}
|
||||||
</div>`,
|
</div>`,
|
||||||
@@ -457,7 +443,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
node.data = {
|
node.data = {
|
||||||
hProperties: {
|
hProperties: {
|
||||||
...(node.data?.hProperties ?? {}),
|
...(node.data?.hProperties ?? {}),
|
||||||
className: `callout ${collapse ? "is-collapsible" : ""} ${
|
className: `callout ${calloutType} ${collapse ? "is-collapsible" : ""} ${
|
||||||
defaultState === "collapsed" ? "is-collapsed" : ""
|
defaultState === "collapsed" ? "is-collapsed" : ""
|
||||||
}`,
|
}`,
|
||||||
"data-callout": calloutType,
|
"data-callout": calloutType,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Root } from "mdast"
|
|||||||
import { visit } from "unist-util-visit"
|
import { visit } from "unist-util-visit"
|
||||||
import { toString } from "mdast-util-to-string"
|
import { toString } from "mdast-util-to-string"
|
||||||
import Slugger from "github-slugger"
|
import Slugger from "github-slugger"
|
||||||
import { wikilinkRegex } from "./ofm"
|
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
maxDepth: 1 | 2 | 3 | 4 | 5 | 6
|
maxDepth: 1 | 2 | 3 | 4 | 5 | 6
|
||||||
@@ -25,7 +24,7 @@ interface TocEntry {
|
|||||||
slug: string // this is just the anchor (#some-slug), not the canonical slug
|
slug: string // this is just the anchor (#some-slug), not the canonical slug
|
||||||
}
|
}
|
||||||
|
|
||||||
const regexMdLinks = new RegExp(/\[([^\[]+)\](\(.*\))/, "g")
|
const slugAnchor = new Slugger()
|
||||||
export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefined> = (
|
export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefined> = (
|
||||||
userOpts,
|
userOpts,
|
||||||
) => {
|
) => {
|
||||||
@@ -38,21 +37,12 @@ export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefin
|
|||||||
return async (tree: Root, file) => {
|
return async (tree: Root, file) => {
|
||||||
const display = file.data.frontmatter?.enableToc ?? opts.showByDefault
|
const display = file.data.frontmatter?.enableToc ?? opts.showByDefault
|
||||||
if (display) {
|
if (display) {
|
||||||
const slugAnchor = new Slugger()
|
slugAnchor.reset()
|
||||||
const toc: TocEntry[] = []
|
const toc: TocEntry[] = []
|
||||||
let highestDepth: number = opts.maxDepth
|
let highestDepth: number = opts.maxDepth
|
||||||
visit(tree, "heading", (node) => {
|
visit(tree, "heading", (node) => {
|
||||||
if (node.depth <= opts.maxDepth) {
|
if (node.depth <= opts.maxDepth) {
|
||||||
let text = toString(node)
|
const text = toString(node)
|
||||||
|
|
||||||
// strip link formatting from toc entries
|
|
||||||
text = text.replace(wikilinkRegex, (_, rawFp, __, rawAlias) => {
|
|
||||||
const fp = rawFp?.trim() ?? ""
|
|
||||||
const alias = rawAlias?.slice(1).trim()
|
|
||||||
return alias ?? fp
|
|
||||||
})
|
|
||||||
text = text.replace(regexMdLinks, "$1")
|
|
||||||
|
|
||||||
highestDepth = Math.min(highestDepth, node.depth)
|
highestDepth = Math.min(highestDepth, node.depth)
|
||||||
toc.push({
|
toc.push({
|
||||||
depth: node.depth,
|
depth: node.depth,
|
||||||
|
|||||||
@@ -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, FullSlug } from "../util/path"
|
import { FilePath } from "../util/path"
|
||||||
import { BuildCtx } from "../util/ctx"
|
import { BuildCtx } from "../util/ctx"
|
||||||
|
|
||||||
export interface PluginTypes {
|
export interface PluginTypes {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
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;
|
||||||
@@ -27,7 +26,7 @@ section {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
background: color-mix(in srgb, var(--tertiary) 75%, transparent);
|
background: color-mix(in srgb, var(--tertiary) 60%, rgba(255, 255, 255, 0));
|
||||||
color: var(--darkgray);
|
color: var(--darkgray);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,11 +54,10 @@ ul,
|
|||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
font-weight: 600;
|
font-weight: $boldWeight;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: color 0.2s ease;
|
transition: color 0.2s ease;
|
||||||
color: var(--secondary);
|
color: var(--secondary);
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--tertiary) !important;
|
color: var(--tertiary) !important;
|
||||||
@@ -173,9 +171,11 @@ 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,7 +278,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -357,6 +356,7 @@ 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);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
@use "./variables.scss" as *;
|
||||||
@use "sass:color";
|
@use "sass:color";
|
||||||
|
|
||||||
.callout {
|
.callout {
|
||||||
@@ -13,16 +14,33 @@
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-callout="note"] {
|
--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>');
|
||||||
|
--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"],
|
||||||
@@ -30,30 +48,39 @@
|
|||||||
--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"],
|
||||||
@@ -62,50 +89,74 @@
|
|||||||
--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 {
|
&.is-collapsed > .callout-title > .fold-callout-icon {
|
||||||
transform: rotateZ(-90deg);
|
transform: rotateZ(-90deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.callout-title {
|
.callout-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
color: var(--color);
|
color: var(--color);
|
||||||
|
|
||||||
& .fold {
|
--icon-size: 18px;
|
||||||
margin-left: 0.5rem;
|
|
||||||
transition: transform 0.3s ease;
|
& .fold-callout-icon {
|
||||||
|
transition: transform 0.15s ease;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
--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,
|
||||||
width: 18px;
|
& .fold-callout-icon {
|
||||||
height: 18px;
|
width: var(--icon-size);
|
||||||
flex: 0 0 18px;
|
height: var(--icon-size);
|
||||||
padding-top: 4px;
|
flex: 0 0 var(--icon-size);
|
||||||
}
|
|
||||||
|
|
||||||
.callout-title-inner {
|
// icon support
|
||||||
font-weight: 700;
|
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;
|
||||||
|
padding: 0.2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout-title-inner {
|
||||||
|
font-weight: $boldWeight;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
$pageWidth: 750px;
|
$pageWidth: 750px;
|
||||||
$mobileBreakpoint: 600px;
|
$mobileBreakpoint: 600px;
|
||||||
$tabletBreakpoint: 1200px;
|
$tabletBreakpoint: 1000px;
|
||||||
$sidePanelWidth: 380px;
|
$sidePanelWidth: 380px;
|
||||||
$topSpacing: 6rem;
|
$topSpacing: 6rem;
|
||||||
$fullPageWidth: $pageWidth + 2 * $sidePanelWidth;
|
$fullPageWidth: $pageWidth + 2 * $sidePanelWidth;
|
||||||
|
$boldWeight: 700;
|
||||||
|
$normalWeight: 400;
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
export function pluralize(count: number, s: string): string {
|
|
||||||
if (count === 1) {
|
|
||||||
return `1 ${s}`
|
|
||||||
} else {
|
|
||||||
return `${count} ${s}s`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(" ")
|
||||||
|
}
|
||||||
|
|||||||
@@ -106,8 +106,9 @@ describe("transforms", () => {
|
|||||||
["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"],
|
["notes.with.dots.md", "notes.with.dots"],
|
||||||
["test/special chars?.md", "test/special-chars-q"],
|
["test/special chars?.md", "test/special-chars"],
|
||||||
["test/special chars #3.md", "test/special-chars-3"],
|
["test/special chars #3.md", "test/special-chars-3"],
|
||||||
|
["cool/what about r&d?.md", "cool/what-about-r-and-d"],
|
||||||
],
|
],
|
||||||
path.slugifyFilePath,
|
path.slugifyFilePath,
|
||||||
path.isFilePath,
|
path.isFilePath,
|
||||||
|
|||||||
@@ -51,8 +51,13 @@ 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").replace(/#/g, ""),
|
segment
|
||||||
) // slugify all segments
|
.replace(/\s/g, "-")
|
||||||
|
.replace(/&/g, "-and-")
|
||||||
|
.replace(/%/g, "-percent")
|
||||||
|
.replace(/\?/g, "")
|
||||||
|
.replace(/#/g, ""),
|
||||||
|
)
|
||||||
.join("/") // always use / as sep
|
.join("/") // always use / as sep
|
||||||
.replace(/\/$/, "")
|
.replace(/\/$/, "")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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