mirror of
https://github.com/alrayyes/wiki.git
synced 2024-11-21 19:16:23 +00:00
run prettier
This commit is contained in:
parent
2034b970b6
commit
7db2eda76c
101 changed files with 1810 additions and 1405 deletions
13
.github/ISSUE_TEMPLATE/bug_report.md
vendored
13
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -1,10 +1,9 @@
|
||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Something about Quartz isn't working the way you expect
|
about: Something about Quartz isn't working the way you expect
|
||||||
title: ''
|
title: ""
|
||||||
labels: bug
|
labels: bug
|
||||||
assignees: ''
|
assignees: ""
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Describe the bug**
|
**Describe the bug**
|
||||||
|
@ -12,6 +11,7 @@ A clear and concise description of what the bug is.
|
||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
|
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
3. Scroll down to '....'
|
3. Scroll down to '....'
|
||||||
|
@ -24,9 +24,10 @@ A clear and concise description of what you expected to happen.
|
||||||
If applicable, add screenshots to help explain your problem.
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
**Desktop (please complete the following information):**
|
||||||
- Device: [e.g. iPhone6]
|
|
||||||
- OS: [e.g. iOS]
|
- Device: [e.g. iPhone6]
|
||||||
- Browser [e.g. chrome, safari]
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
|
5
.github/ISSUE_TEMPLATE/feature_request.md
vendored
5
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
@ -1,10 +1,9 @@
|
||||||
---
|
---
|
||||||
name: Feature request
|
name: Feature request
|
||||||
about: Suggest an idea or improvement for Quartz
|
about: Suggest an idea or improvement for Quartz
|
||||||
title: ''
|
title: ""
|
||||||
labels: enhancement
|
labels: enhancement
|
||||||
assignees: ''
|
assignees: ""
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
|
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
|
@ -1,4 +1,4 @@
|
||||||
name: Build and Test
|
name: Build and Test
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
@ -34,4 +34,4 @@ jobs:
|
||||||
run: npm test
|
run: npm test
|
||||||
|
|
||||||
- name: Ensure Quartz builds
|
- name: Ensure Quartz builds
|
||||||
run: npx quartz build
|
run: npx quartz build
|
||||||
|
|
|
@ -20,28 +20,28 @@ If you see someone who is making an extra effort to ensure our community is welc
|
||||||
|
|
||||||
The following behaviors are expected and requested of all community members:
|
The following behaviors are expected and requested of all community members:
|
||||||
|
|
||||||
* Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community.
|
- Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community.
|
||||||
* Exercise consideration and respect in your speech and actions.
|
- Exercise consideration and respect in your speech and actions.
|
||||||
* Attempt collaboration before conflict.
|
- Attempt collaboration before conflict.
|
||||||
* Refrain from demeaning, discriminatory, or harassing behavior and speech.
|
- Refrain from demeaning, discriminatory, or harassing behavior and speech.
|
||||||
* Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential.
|
- Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential.
|
||||||
* Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations.
|
- Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations.
|
||||||
|
|
||||||
## 4. Unacceptable Behavior
|
## 4. Unacceptable Behavior
|
||||||
|
|
||||||
The following behaviors are considered harassment and are unacceptable within our community:
|
The following behaviors are considered harassment and are unacceptable within our community:
|
||||||
|
|
||||||
* Violence, threats of violence or violent language directed against another person.
|
- Violence, threats of violence or violent language directed against another person.
|
||||||
* Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language.
|
- Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language.
|
||||||
* Posting or displaying sexually explicit or violent material.
|
- Posting or displaying sexually explicit or violent material.
|
||||||
* Posting or threatening to post other people's personally identifying information ("doxing").
|
- Posting or threatening to post other people's personally identifying information ("doxing").
|
||||||
* Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability.
|
- Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability.
|
||||||
* Inappropriate photography or recording.
|
- Inappropriate photography or recording.
|
||||||
* Inappropriate physical contact. You should have someone's consent before touching them.
|
- Inappropriate physical contact. You should have someone's consent before touching them.
|
||||||
* Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances.
|
- Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances.
|
||||||
* Deliberate intimidation, stalking or following (online or in person).
|
- Deliberate intimidation, stalking or following (online or in person).
|
||||||
* Advocating for, or encouraging, any of the above behavior.
|
- Advocating for, or encouraging, any of the above behavior.
|
||||||
* Sustained disruption of community events, including talks and presentations.
|
- Sustained disruption of community events, including talks and presentations.
|
||||||
|
|
||||||
## 5. Weapons Policy
|
## 5. Weapons Policy
|
||||||
|
|
||||||
|
@ -59,14 +59,11 @@ If a community member engages in unacceptable behavior, the community organizers
|
||||||
|
|
||||||
If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. j.zhao2k19@gmail.com.
|
If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. j.zhao2k19@gmail.com.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress.
|
Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress.
|
||||||
|
|
||||||
## 8. Addressing Grievances
|
## 8. Addressing Grievances
|
||||||
|
|
||||||
If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify @jackyzha0 with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies.
|
If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify @jackyzha0 with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies.
|
||||||
|
|
||||||
|
|
||||||
## 9. Scope
|
## 9. Scope
|
||||||
|
|
||||||
|
@ -80,7 +77,7 @@ j.zhao2k19@gmail.com
|
||||||
|
|
||||||
## 11. License and attribution
|
## 11. License and attribution
|
||||||
|
|
||||||
The Citizen Code of Conduct is distributed by [Stumptown Syndicate](http://stumptownsyndicate.org) under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/).
|
The Citizen Code of Conduct is distributed by [Stumptown Syndicate](http://stumptownsyndicate.org) under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/).
|
||||||
|
|
||||||
Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy).
|
Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy).
|
||||||
|
|
||||||
|
|
|
@ -2,4 +2,4 @@
|
||||||
title: Creating your own Quartz components
|
title: Creating your own Quartz components
|
||||||
---
|
---
|
||||||
|
|
||||||
See the [component listing](/tags/component) for a full-list of the Quartz built-in components.
|
See the [component listing](/tags/component) for a full-list of the Quartz built-in components.
|
||||||
|
|
|
@ -5,6 +5,7 @@ title: Making your own plugins
|
||||||
This part of the documentation will assume you have some basic coding knowledge and will include code snippets that describe the interface of what Quartz plugins should look like.
|
This part of the documentation will assume you have some basic coding knowledge and will include code snippets that describe the interface of what Quartz plugins should look like.
|
||||||
|
|
||||||
## Transformers
|
## Transformers
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
export type QuartzTransformerPluginInstance = {
|
export type QuartzTransformerPluginInstance = {
|
||||||
name: string
|
name: string
|
||||||
|
@ -17,4 +18,4 @@ export type QuartzTransformerPluginInstance = {
|
||||||
|
|
||||||
## Filters
|
## Filters
|
||||||
|
|
||||||
## Emitters
|
## Emitters
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
title: Configuration
|
title: Configuration
|
||||||
---
|
---
|
||||||
|
|
||||||
Quartz is meant to be extremely configurable, even if you don't know any coding. Most of the configuration you should need can be done by just editing `quartz.config.ts`.
|
Quartz is meant to be extremely configurable, even if you don't know any coding. Most of the configuration you should need can be done by just editing `quartz.config.ts`.
|
||||||
|
|
||||||
If you edit this file using a text-editor that has TypeScript language support like VSCode, it will warn you when you you've made an error in your configuration.
|
If you edit this file using a text-editor that has TypeScript language support like VSCode, it will warn you when you you've made an error in your configuration.
|
||||||
|
|
||||||
|
@ -16,33 +16,35 @@ const config: QuartzConfig = {
|
||||||
```
|
```
|
||||||
|
|
||||||
## General Configuration
|
## General Configuration
|
||||||
|
|
||||||
This part of the configuration concerns anything that can affect the whole site. The following is a list breaking down all the things you can configure:
|
This part of the configuration concerns anything that can affect the whole site. The following is a list breaking down all the things you can configure:
|
||||||
|
|
||||||
- `pageTitle`: used as an anchor to return to the home page. This is also used when generating the [[RSS Feed]] for your site.
|
- `pageTitle`: used as an anchor to return to the home page. This is also used when generating the [[RSS Feed]] for your site.
|
||||||
- `enableSPA`: whether to enable [[SPA Routing]] on your site.
|
- `enableSPA`: whether to enable [[SPA Routing]] on your site.
|
||||||
- `enablePopovers`: whether to enable [[popover previews]] on your site.
|
- `enablePopovers`: whether to enable [[popover previews]] on your site.
|
||||||
- `analytics`: what to use for analytics on your site. Values can be
|
- `analytics`: what to use for analytics on your site. Values can be
|
||||||
- `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
|
||||||
- `caononicalUrl`: sometimes called `baseURL` in other site generators. 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. `https://quartz.jzhao.xyz/` for this site). 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.
|
- `caononicalUrl`: sometimes called `baseURL` in other site generators. 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. `https://quartz.jzhao.xyz/` for this site). Note that Quartz 4 will avoid using this as much as possible and use relative URLs whenever it can to make sure your site works no matter _where_ you end up actually deploying it.
|
||||||
- `ignorePatterns`: a list of [glob](https://en.wikipedia.org/wiki/Glob_(programming)) patterns that Quartz should ignore and not search through when looking for files inside the `content` folder.
|
- `ignorePatterns`: a list of [glob](<https://en.wikipedia.org/wiki/Glob_(programming)>) patterns that Quartz should ignore and not search through when looking for files inside the `content` folder.
|
||||||
- `theme`: configure how the site looks.
|
- `theme`: configure how the site looks.
|
||||||
- `typography`: what fonts to use. Any font available on [Google Fonts](https://fonts.google.com/) works here.
|
- `typography`: what fonts to use. Any font available on [Google Fonts](https://fonts.google.com/) works here.
|
||||||
- `header`: Font to use for headers
|
- `header`: Font to use for headers
|
||||||
- `code`: Font for inline and block quotes.
|
- `code`: Font for inline and block quotes.
|
||||||
- `body`: Font for everything
|
- `body`: Font for everything
|
||||||
- `colors`: controls the theming of the site.
|
- `colors`: controls the theming of the site.
|
||||||
- `light`: page background
|
- `light`: page background
|
||||||
- `lightgray`: borders
|
- `lightgray`: borders
|
||||||
- `gray`: graph links, heavier borders
|
- `gray`: graph links, heavier borders
|
||||||
- `darkgray`: body text
|
- `darkgray`: body text
|
||||||
- `dark`: header text and icons
|
- `dark`: header text and icons
|
||||||
- `secondary`: link colour, current [[graph view|graph]] node
|
- `secondary`: link colour, current [[graph view|graph]] node
|
||||||
- `tertiary`: hover states and visited [[graph view|graph]] nodes
|
- `tertiary`: hover states and visited [[graph view|graph]] nodes
|
||||||
- `highlight`: internal link background, highlighted text, [[syntax highlighting|highlighted lines of code]]
|
- `highlight`: internal link background, highlighted text, [[syntax highlighting|highlighted lines of code]]
|
||||||
|
|
||||||
## Plugins
|
## Plugins
|
||||||
|
|
||||||
You can think of Quartz plugins as a series of transformations over content.
|
You can think of Quartz plugins as a series of transformations over content.
|
||||||
|
|
||||||
![[quartz-transform-pipeline.png]]
|
![[quartz-transform-pipeline.png]]
|
||||||
|
@ -62,18 +64,19 @@ plugins: {
|
||||||
By adding, removing, and reordering plugins from the `tranformers`, `filters`, and `emitters` fields, you can customize the behaviour of Quartz.
|
By adding, removing, and reordering plugins from the `tranformers`, `filters`, and `emitters` fields, you can customize the behaviour of Quartz.
|
||||||
|
|
||||||
> [!note]
|
> [!note]
|
||||||
> Each node is modified by every transformer *in order*. Some transformers are position-sensitive so you may need to take special note of whether it needs come before or after any other particular plugins.
|
> Each node is modified by every transformer _in order_. Some transformers are position-sensitive so you may need to take special note of whether it needs come before or after any other particular plugins.
|
||||||
|
|
||||||
Additionally, plugins may also have their own configuration settings that you can pass in. For example, the [[Latex]] plugin allows you to pass in a field specifying the `renderEngine` to choose between Katex and MathJax.
|
Additionally, plugins may also have their own configuration settings that you can pass in. For example, the [[Latex]] plugin allows you to pass in a field specifying the `renderEngine` to choose between Katex and MathJax.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
transformers: [
|
transformers: [
|
||||||
Plugin.FrontMatter(), // uses default options
|
Plugin.FrontMatter(), // uses default options
|
||||||
Plugin.Latex({ renderEngine: 'katex' }) // specify some options
|
Plugin.Latex({ renderEngine: "katex" }), // specify some options
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Layout
|
### Layout
|
||||||
Certain emitters may also output [HTML](https://developer.mozilla.org/en-US/docs/Web/HTML) files. To make sure that
|
|
||||||
|
Certain emitters may also output [HTML](https://developer.mozilla.org/en-US/docs/Web/HTML) files. To make sure that
|
||||||
|
|
||||||
### Components
|
### Components
|
||||||
|
|
|
@ -3,6 +3,7 @@ Quartz uses [Katex](https://katex.org/) by default to typeset both inline and bl
|
||||||
## Formatting
|
## Formatting
|
||||||
|
|
||||||
### Block Math
|
### Block Math
|
||||||
|
|
||||||
Block math can be rendered by delimiting math expression with `$$`.
|
Block math can be rendered by delimiting math expression with `$$`.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -20,20 +21,25 @@ f(x) = \int_{-\infty}^\infty
|
||||||
$$
|
$$
|
||||||
|
|
||||||
### Inline Math
|
### Inline Math
|
||||||
|
|
||||||
Similarly, inline math can be rendered by delimiting math expression with a single `$`. For example, `$e^{i\pi} = -1$` produces $e^{i\pi} = -1$
|
Similarly, inline math can be rendered by delimiting math expression with a single `$`. For example, `$e^{i\pi} = -1$` produces $e^{i\pi} = -1$
|
||||||
|
|
||||||
### Escaping symbols
|
### Escaping symbols
|
||||||
There will be cases where you may have more than one `$` in a paragraph at once which may accidentally trigger MathJax/Katex.
|
|
||||||
|
There will be cases where you may have more than one `$` in a paragraph at once which may accidentally trigger MathJax/Katex.
|
||||||
|
|
||||||
To get around this, you can escape the dollar sign by doing `\$` instead.
|
To get around this, you can escape the dollar sign by doing `\$` instead.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
- Incorrect: `I have $1 and you have $2` produces I have $1 and you have $2
|
- Incorrect: `I have $1 and you have $2` produces I have $1 and you have $2
|
||||||
- Correct: `I have \$1 and you have \$2` produces I have \$1 and you have \$2
|
- Correct: `I have \$1 and you have \$2` produces I have \$1 and you have \$2
|
||||||
|
|
||||||
## MathJax
|
## MathJax
|
||||||
|
|
||||||
In `quartz.config.ts`, you can configure Quartz to use [MathJax SVG rendering](https://docs.mathjax.org/en/latest/output/svg.html) by replacing `Plugin.Latex({ renderEngine: 'katex' })` with `Plugin.Latex({ renderEngine: 'mathjax' })`
|
In `quartz.config.ts`, you can configure Quartz to use [MathJax SVG rendering](https://docs.mathjax.org/en/latest/output/svg.html) by replacing `Plugin.Latex({ renderEngine: 'katex' })` with `Plugin.Latex({ renderEngine: 'mathjax' })`
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
|
|
||||||
- Removing Latex support: remove all instances of `Plugin.Latex()` from `quartz.config.ts`.
|
- Removing Latex support: remove all instances of `Plugin.Latex()` from `quartz.config.ts`.
|
||||||
- Plugin: `quartz/plugins/transformers/latex.ts`
|
- Plugin: `quartz/plugins/transformers/latex.ts`
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
> [!warning]
|
> [!warning]
|
||||||
> Wondering why Mermaid diagrams may not be showing up even if you have them enabled? You may need to reorder your plugins so that `Plugin.ObsidianFlavoredMarkdown()` is *after* `Plugin.SyntaxHighlighting()`.
|
> Wondering why Mermaid diagrams may not be showing up even if you have them enabled? You may need to reorder your plugins so that `Plugin.ObsidianFlavoredMarkdown()` is _after_ `Plugin.SyntaxHighlighting()`.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Single-page-app style rendering. This prevents flashes of unstyled content and improves smoothness of Quartz
|
Single-page-app style rendering. This prevents flashes of unstyled content and improves smoothness of Quartz
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
---
|
---
|
||||||
title: Backlinks
|
title: Backlinks
|
||||||
tags:
|
tags:
|
||||||
- component
|
- component
|
||||||
---
|
---
|
||||||
|
|
||||||
A backlink for a note is a link from another note to that note. Links in the backlink pane also feature rich [[popover previews]] if you have that feature enabled.
|
A backlink for a note is a link from another note to that note. Links in the backlink pane also feature rich [[popover previews]] if you have that feature enabled.
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
|
|
||||||
- Removing backlinks: delete all usages of `Component.Backlinks()` from `quartz.config.ts`.
|
- Removing backlinks: delete all usages of `Component.Backlinks()` from `quartz.config.ts`.
|
||||||
- Component: `quartz/components/Backlinks.tsx`
|
- Component: `quartz/components/Backlinks.tsx`
|
||||||
- Style: `quartz/components/styles/backlinks.scss`
|
- Style: `quartz/components/styles/backlinks.scss`
|
||||||
- Script: `quartz/components/scripts/search.inline.ts`
|
- Script: `quartz/components/scripts/search.inline.ts`
|
||||||
|
|
|
@ -3,15 +3,16 @@ title: Callouts
|
||||||
---
|
---
|
||||||
|
|
||||||
> [!warning]
|
> [!warning]
|
||||||
> Wondering why callouts may not be showing up even if you have them enabled? You may need to reorder your plugins so that `Plugin.ObsidianFlavoredMarkdown()` is *after* `Plugin.SyntaxHighlighting()`.
|
> Wondering why callouts may not be showing up even if you have them enabled? You may need to reorder your plugins so that `Plugin.ObsidianFlavoredMarkdown()` is _after_ `Plugin.SyntaxHighlighting()`.
|
||||||
|
|
||||||
|
|
||||||
> [!info]
|
> [!info]
|
||||||
> Default title
|
> Default title
|
||||||
|
|
||||||
> [!question]+ Can callouts be nested?
|
> [!question]+ Can callouts be nested?
|
||||||
|
>
|
||||||
> > [!todo]- Yes!, they can.
|
> > [!todo]- Yes!, they can.
|
||||||
> > > [!example] You can even use multiple layers of nesting.
|
> >
|
||||||
|
> > > [!example] You can even use multiple layers of nesting.
|
||||||
|
|
||||||
> [!EXAMPLE] Examples
|
> [!EXAMPLE] Examples
|
||||||
>
|
>
|
||||||
|
@ -21,31 +22,31 @@ title: Callouts
|
||||||
>
|
>
|
||||||
> Aliases: note
|
> Aliases: note
|
||||||
|
|
||||||
> [!abstract] Summaries
|
> [!abstract] Summaries
|
||||||
>
|
>
|
||||||
> Aliases: abstract, summary, tldr
|
> Aliases: abstract, summary, tldr
|
||||||
|
|
||||||
> [!info] Info
|
> [!info] Info
|
||||||
>
|
>
|
||||||
> Aliases: info, todo
|
> Aliases: info, todo
|
||||||
|
|
||||||
> [!tip] Hint
|
> [!tip] Hint
|
||||||
>
|
>
|
||||||
> Aliases: tip, hint, important
|
> Aliases: tip, hint, important
|
||||||
|
|
||||||
> [!success] Success
|
> [!success] Success
|
||||||
>
|
>
|
||||||
> Aliases: success, check, done
|
> Aliases: success, check, done
|
||||||
|
|
||||||
> [!question] Question
|
> [!question] Question
|
||||||
>
|
>
|
||||||
> Aliases: question, help, faq
|
> Aliases: question, help, faq
|
||||||
|
|
||||||
> [!warning] Warning
|
> [!warning] Warning
|
||||||
>
|
>
|
||||||
> Aliases: warning, caution, attention
|
> Aliases: warning, caution, attention
|
||||||
|
|
||||||
> [!failure] Failure
|
> [!failure] Failure
|
||||||
>
|
>
|
||||||
> Aliases: failure, fail, missing
|
> Aliases: failure, fail, missing
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
---
|
---
|
||||||
title: Full-text Search
|
title: Full-text Search
|
||||||
tags:
|
tags:
|
||||||
- component
|
- component
|
||||||
---
|
---
|
||||||
|
|
||||||
Full-text search in Quartz is powered by [Flexsearch](https://github.com/nextapps-de/flexsearch). It's fast enough to return search results in under 10ms for Quartzs as large as half a million words.
|
Full-text search in Quartz is powered by [Flexsearch](https://github.com/nextapps-de/flexsearch). It's fast enough to return search results in under 10ms for Quartzs as large as half a million words.
|
||||||
|
|
||||||
It can be opened by either clicking on the search bar or pressing ⌘+K. The top 5 search results are shown on each query. Matching subterms are highlighted and the most relevant 30 words are excerpted. Clicking on a search result will navigate to that page.
|
It can be opened by either clicking on the search bar or pressing ⌘+K. The top 5 search results are shown on each query. Matching subterms are highlighted and the most relevant 30 words are excerpted. Clicking on a search result will navigate to that page.
|
||||||
|
|
||||||
This component is also keyboard accessible: Tab and Shift+Tab will cycle forward and backward through search results and Enter will navigate to the highlighted result (first result by default).
|
This component is also keyboard accessible: Tab and Shift+Tab will cycle forward and backward through search results and Enter will navigate to the highlighted result (first result by default).
|
||||||
|
|
||||||
|
@ -14,13 +14,15 @@ This component is also keyboard accessible: Tab and Shift+Tab will cycle forward
|
||||||
> Search requires the `ContentIndex` emitter plugin to be present in the [[configuration]].
|
> Search requires the `ContentIndex` emitter plugin to be present in the [[configuration]].
|
||||||
|
|
||||||
### Indexing Behaviour
|
### Indexing Behaviour
|
||||||
|
|
||||||
By default, it indexes every page on the site with **Markdown syntax removed**. This means link URLs for instance are not indexed.
|
By default, it indexes every page on the site with **Markdown syntax removed**. This means link URLs for instance are not indexed.
|
||||||
|
|
||||||
It properly tokenizes Chinese, Korean, and Japenese characters and constructs separate indexes for the title and content, weighing title matches above content matches.
|
It properly tokenizes Chinese, Korean, and Japenese characters and constructs separate indexes for the title and content, weighing title matches above content matches.
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
|
|
||||||
- Removing search: delete all usages of `Component.Search()` from `quartz.config.ts`.
|
- Removing search: delete all usages of `Component.Search()` from `quartz.config.ts`.
|
||||||
- Component: `quartz/components/Search.tsx`
|
- Component: `quartz/components/Search.tsx`
|
||||||
- Style: `quartz/components/styles/search.scss`
|
- Style: `quartz/components/styles/search.scss`
|
||||||
- Script: `quartz/components/scripts/search.inline.ts`
|
- Script: `quartz/components/scripts/search.inline.ts`
|
||||||
- You can edit `contextWindowWords` or `numSearchResults` to suit your needs
|
- You can edit `contextWindowWords` or `numSearchResults` to suit your needs
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
---
|
---
|
||||||
title: "Graph View"
|
title: "Graph View"
|
||||||
tags:
|
tags:
|
||||||
- component
|
- component
|
||||||
---
|
---
|
||||||
|
|
||||||
Quartz features a graph-view that can show both a local graph view and a global graph view.
|
Quartz features a graph-view that can show both a local graph view and a global graph view.
|
||||||
|
|
||||||
- The local graph view shows files that either link to the current file or are linked from the current file. In other words, it shows all notes that are *at most* one hop away.
|
- The local graph view shows files that either link to the current file or are linked from the current file. In other words, it shows all notes that are _at most_ one hop away.
|
||||||
- The global graph view can be toggled by clicking the graph icon on the top-right of the local graph view. It shows *all* the notes in your graph and how they connect to each other.
|
- The global graph view can be toggled by clicking the graph icon on the top-right of the local graph view. It shows _all_ the notes in your graph and how they connect to each other.
|
||||||
|
|
||||||
By default, the node radius is proportional to the total number of incoming and outgoing internal links from that file.
|
By default, the node radius is proportional to the total number of incoming and outgoing internal links from that file.
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ Additionally, similar to how browsers highlight visited links a different colour
|
||||||
> Graph View requires the `ContentIndex` emitter plugin to be present in the [[configuration]].
|
> Graph View requires the `ContentIndex` emitter plugin to be present in the [[configuration]].
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
|
|
||||||
Most configuration can be done by passing in options to `Component.Graph()`.
|
Most configuration can be done by passing in options to `Component.Graph()`.
|
||||||
|
|
||||||
For example, here's what the default configuration looks like:
|
For example, here's what the default configuration looks like:
|
||||||
|
@ -26,13 +27,13 @@ Component.Graph({
|
||||||
localGraph: {
|
localGraph: {
|
||||||
drag: true, // whether to allow panning the view around
|
drag: true, // whether to allow panning the view around
|
||||||
zoom: true, // whether to allow zooming in and out
|
zoom: true, // whether to allow zooming in and out
|
||||||
depth: 1, // how many hops of notes to display
|
depth: 1, // how many hops of notes to display
|
||||||
scale: 1.1, // default view scale
|
scale: 1.1, // default view scale
|
||||||
repelForce: 0.5, // how much nodes should repel each other
|
repelForce: 0.5, // how much nodes should repel each other
|
||||||
centerForce: 0.3, // how much force to use when trying to center the nodes
|
centerForce: 0.3, // how much force to use when trying to center the nodes
|
||||||
linkDistance: 30, // how long should the links be by default?
|
linkDistance: 30, // how long should the links be by default?
|
||||||
fontSize: 0.6, // what size should the node labels be?
|
fontSize: 0.6, // what size should the node labels be?
|
||||||
opacityScale: 1 // how quickly do we fade out the labels when zooming out?
|
opacityScale: 1, // how quickly do we fade out the labels when zooming out?
|
||||||
},
|
},
|
||||||
globalGraph: {
|
globalGraph: {
|
||||||
drag: true,
|
drag: true,
|
||||||
|
@ -43,8 +44,8 @@ Component.Graph({
|
||||||
centerForce: 0.3,
|
centerForce: 0.3,
|
||||||
linkDistance: 30,
|
linkDistance: 30,
|
||||||
fontSize: 0.6,
|
fontSize: 0.6,
|
||||||
opacityScale: 1
|
opacityScale: 1,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -55,4 +56,4 @@ Want to customize it even more?
|
||||||
- Removing graph view: delete all usages of `Component.Graph()` from `quartz.config.ts`.
|
- Removing graph view: delete all usages of `Component.Graph()` from `quartz.config.ts`.
|
||||||
- Component: `quartz/components/Graph.tsx`
|
- Component: `quartz/components/Graph.tsx`
|
||||||
- Style: `quartz/components/styles/graph.scss`
|
- Style: `quartz/components/styles/graph.scss`
|
||||||
- Script: `quartz/components/scripts/graph.inline.ts`
|
- Script: `quartz/components/scripts/graph.inline.ts`
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
---
|
---
|
||||||
title: Feature List
|
title: Feature List
|
||||||
---
|
---
|
||||||
|
|
|
@ -9,6 +9,7 @@ By default, Quartz only fetches previews for pages inside your vault due to [COR
|
||||||
When [[creating components|creating your own components]], you can include this `popover-hint` class to also include it in the popover.
|
When [[creating components|creating your own components]], you can include this `popover-hint` class to also include it in the popover.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
- Remove popovers: set the `enablePopovers` field in `quartz.config.ts` to be `false`.
|
- Remove popovers: set the `enablePopovers` field in `quartz.config.ts` to be `false`.
|
||||||
- Style: `quartz/components/styles/popover.scss`
|
- Style: `quartz/components/styles/popover.scss`
|
||||||
- Script: `quartz/components/scripts/popover.inline.ts`
|
- Script: `quartz/components/scripts/popover.inline.ts`
|
||||||
|
|
|
@ -12,6 +12,7 @@ In short, it generates HTML that looks exactly like your code in an editor like
|
||||||
> Syntax highlighting does have an impact on build speed if you have a lot of code snippets in your notes.
|
> Syntax highlighting does have an impact on build speed if you have a lot of code snippets in your notes.
|
||||||
|
|
||||||
## Formatting
|
## Formatting
|
||||||
|
|
||||||
Text inside `backticks` on a line will be formatted like code.
|
Text inside `backticks` on a line will be formatted like code.
|
||||||
|
|
||||||
````
|
````
|
||||||
|
@ -37,11 +38,12 @@ export function trimPathSuffix(fp: string): string {
|
||||||
```
|
```
|
||||||
|
|
||||||
### Titles
|
### Titles
|
||||||
|
|
||||||
Add a file title to your code block, with text inside double quotes (`""`):
|
Add a file title to your code block, with text inside double quotes (`""`):
|
||||||
|
|
||||||
````
|
````
|
||||||
```js title="..."
|
```js title="..."
|
||||||
|
|
||||||
```
|
```
|
||||||
````
|
````
|
||||||
|
|
||||||
|
@ -56,11 +58,12 @@ export function trimPathSuffix(fp: string): string {
|
||||||
```
|
```
|
||||||
|
|
||||||
### Line highlighting
|
### Line highlighting
|
||||||
|
|
||||||
Place a numeric range inside `{}`.
|
Place a numeric range inside `{}`.
|
||||||
|
|
||||||
````
|
````
|
||||||
```js {1-3,4}
|
```js {1-3,4}
|
||||||
|
|
||||||
```
|
```
|
||||||
````
|
````
|
||||||
|
|
||||||
|
@ -75,6 +78,7 @@ export function trimPathSuffix(fp: string): string {
|
||||||
```
|
```
|
||||||
|
|
||||||
### Word highlighting
|
### Word highlighting
|
||||||
|
|
||||||
A series of characters, like a literal regex.
|
A series of characters, like a literal regex.
|
||||||
|
|
||||||
````
|
````
|
||||||
|
@ -85,16 +89,17 @@ const [name, setName] = useState('Taylor');
|
||||||
````
|
````
|
||||||
|
|
||||||
```js /useState/
|
```js /useState/
|
||||||
const [age, setAge] = useState(50);
|
const [age, setAge] = useState(50)
|
||||||
const [name, setName] = useState('Taylor');
|
const [name, setName] = useState("Taylor")
|
||||||
```
|
```
|
||||||
|
|
||||||
### Line numbers
|
### Line numbers
|
||||||
|
|
||||||
Syntax highlighting has line numbers configured automatically. If you want to start line numbers at a specific number, use `showLineNumbers{number}`:
|
Syntax highlighting has line numbers configured automatically. If you want to start line numbers at a specific number, use `showLineNumbers{number}`:
|
||||||
|
|
||||||
````
|
````
|
||||||
```js showLineNumbers{number}
|
```js showLineNumbers{number}
|
||||||
|
|
||||||
```
|
```
|
||||||
````
|
````
|
||||||
|
|
||||||
|
@ -109,6 +114,7 @@ export function trimPathSuffix(fp: string): string {
|
||||||
```
|
```
|
||||||
|
|
||||||
### Escaping code blocks
|
### Escaping code blocks
|
||||||
|
|
||||||
You can format a codeblock inside of a codeblock by wrapping it with another level of backtick fences that has one more backtick than the previous fence.
|
You can format a codeblock inside of a codeblock by wrapping it with another level of backtick fences that has one more backtick than the previous fence.
|
||||||
|
|
||||||
`````
|
`````
|
||||||
|
@ -121,6 +127,7 @@ const [name, setName] = useState('Taylor');
|
||||||
`````
|
`````
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
|
|
||||||
- Removing syntax highlighting: delete all usages of `Plugin.SyntaxHighlighting()` from `quartz.config.ts`.
|
- Removing syntax highlighting: delete all usages of `Plugin.SyntaxHighlighting()` from `quartz.config.ts`.
|
||||||
- Style: By default, Quartz uses derivatives of the GitHub light and dark themes. You can customize the colours in the `quartz/styles/syntax.scss` file.
|
- Style: By default, Quartz uses derivatives of the GitHub light and dark themes. You can customize the colours in the `quartz/styles/syntax.scss` file.
|
||||||
- Plugin: `quartz/plugins/transformers/syntax.ts`
|
- Plugin: `quartz/plugins/transformers/syntax.ts`
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
title: "Table of Contents"
|
title: "Table of Contents"
|
||||||
tags:
|
tags:
|
||||||
- component
|
- component
|
||||||
---
|
---
|
||||||
|
|
|
@ -12,9 +12,9 @@ draft: true
|
||||||
- custom md blocks (e.g. for poetry)
|
- custom md blocks (e.g. for poetry)
|
||||||
- sidenotes? [https://github.com/capnfabs/paperesque](https://github.com/capnfabs/paperesque)
|
- sidenotes? [https://github.com/capnfabs/paperesque](https://github.com/capnfabs/paperesque)
|
||||||
- watch mode
|
- watch mode
|
||||||
- watch for markdown changes and quartz config changes
|
- watch for markdown changes and quartz config changes
|
||||||
- markdown changes only involve processing that single markdown file (at least for parsing) and then rerunning the filter and emitters
|
- markdown changes only involve processing that single markdown file (at least for parsing) and then rerunning the filter and emitters
|
||||||
- config changes rebuild the whole thing
|
- config changes rebuild the whole thing
|
||||||
- direct match in search using double quotes
|
- direct match in search using double quotes
|
||||||
- attachments path
|
- attachments path
|
||||||
- [https://help.obsidian.md/Advanced+topics/Using+Obsidian+URI](https://help.obsidian.md/Advanced+topics/Using+Obsidian+URI)
|
- [https://help.obsidian.md/Advanced+topics/Using+Obsidian+URI](https://help.obsidian.md/Advanced+topics/Using+Obsidian+URI)
|
||||||
|
@ -26,8 +26,8 @@ draft: true
|
||||||
- audio/video embed styling
|
- audio/video embed styling
|
||||||
- Canvas
|
- Canvas
|
||||||
- mermaid styling: [https://mermaid.js.org/config/theming.html#theme-variables-reference-table](https://mermaid.js.org/config/theming.html#theme-variables-reference-table)
|
- mermaid styling: [https://mermaid.js.org/config/theming.html#theme-variables-reference-table](https://mermaid.js.org/config/theming.html#theme-variables-reference-table)
|
||||||
- [https://github.com/jackyzha0/quartz/issues/331](https://github.com/jackyzha0/quartz/issues/331)
|
- [https://github.com/jackyzha0/quartz/issues/331](https://github.com/jackyzha0/quartz/issues/331)
|
||||||
- block links: [https://help.obsidian.md/Linking+notes+and+files/Internal+links#Link+to+a+block+in+a+note](https://help.obsidian.md/Linking+notes+and+files/Internal+links#Link+to+a+block+in+a+note)
|
- block links: [https://help.obsidian.md/Linking+notes+and+files/Internal+links#Link+to+a+block+in+a+note](https://help.obsidian.md/Linking+notes+and+files/Internal+links#Link+to+a+block+in+a+note)
|
||||||
- note/header/block transcludes: [https://help.obsidian.md/Linking+notes+and+files/Embedding+files](https://help.obsidian.md/Linking+notes+and+files/Embedding+files)
|
- note/header/block transcludes: [https://help.obsidian.md/Linking+notes+and+files/Embedding+files](https://help.obsidian.md/Linking+notes+and+files/Embedding+files)
|
||||||
- parse all images in page: use this for page lists if applicable?
|
- parse all images in page: use this for page lists if applicable?
|
||||||
- CV mode? with print stylesheet
|
- CV mode? with print stylesheet
|
||||||
|
|
|
@ -5,6 +5,7 @@ title: Welcome to Quartz 4
|
||||||
Quartz is a fast, batteries-included static-site generator that transforms Markdown content into fully functional websites. Thousands of students, developers, and teachers are [[showcase|already using Quartz]] to publish personal notes, wikis, and [digital gardens](https://jzhao.xyz/posts/networked-thought/) to the web.
|
Quartz is a fast, batteries-included static-site generator that transforms Markdown content into fully functional websites. Thousands of students, developers, and teachers are [[showcase|already using Quartz]] to publish personal notes, wikis, and [digital gardens](https://jzhao.xyz/posts/networked-thought/) to the web.
|
||||||
|
|
||||||
## 🪴 Get Started
|
## 🪴 Get Started
|
||||||
|
|
||||||
Quartz requires **at least [Node](https://nodejs.org/) v16** to function correctly. In your terminal of choice, enter the following commands line by line:
|
Quartz requires **at least [Node](https://nodejs.org/) v16** to function correctly. In your terminal of choice, enter the following commands line by line:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
@ -26,7 +27,8 @@ When you're ready, you can edit `quartz.config.ts` to customize and configure Qu
|
||||||
- [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes
|
- [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes
|
||||||
- Fully-customizable parsing, filtering, and page generation through [[making plugins|plugins]]
|
- Fully-customizable parsing, filtering, and page generation through [[making plugins|plugins]]
|
||||||
|
|
||||||
For a comprehensive list of features, visit the [features page](/features). You can read more the *why* behind these features on the [[philosophy]] page.
|
For a comprehensive list of features, visit the [features page](/features). You can read more the _why_ behind these features on the [[philosophy]] page.
|
||||||
|
|
||||||
### 🚧 Troubleshooting
|
### 🚧 Troubleshooting
|
||||||
|
|
||||||
Having trouble with Quartz? Try searching for your issue using the search feature. If you're still having trouble, feel free to [submit an issue](https://github.com/jackyzha0/quartz/issues) if you feel you found a bug or ask for help in our [Discord Community](https://discord.gg/cRFFHYye7t).
|
Having trouble with Quartz? Try searching for your issue using the search feature. If you're still having trouble, feel free to [submit an issue](https://github.com/jackyzha0/quartz/issues) if you feel you found a bug or ask for help in our [Discord Community](https://discord.gg/cRFFHYye7t).
|
||||||
|
|
|
@ -5,7 +5,7 @@ title: Philosophy of Quartz
|
||||||
## A garden should be a true hypertext
|
## A garden should be a true hypertext
|
||||||
|
|
||||||
> The garden is the web as topology. Every walk through the garden creates new paths, new meanings, and when we add things to the garden we add them in a way that allows many future, unpredicted relationships.
|
> The garden is the web as topology. Every walk through the garden creates new paths, new meanings, and when we add things to the garden we add them in a way that allows many future, unpredicted relationships.
|
||||||
>
|
>
|
||||||
> (The Garden and the Stream)
|
> (The Garden and the Stream)
|
||||||
|
|
||||||
The problem with the file cabinet is that it focuses on efficiency of access and interoperability rather than generativity and creativity. Thinking is not linear, nor is it hierarchical. In fact, not many things are linear or hierarchical at all. Then why is it that most tools and thinking strategies assume a nice chronological or hierarchical order for my thought processes? The ideal tool for thought for me would embrace the messiness of my mind, and organically help insights emerge from chaos instead of forcing an artificial order. A rhizomatic, not arboresecent, form of note taking.
|
The problem with the file cabinet is that it focuses on efficiency of access and interoperability rather than generativity and creativity. Thinking is not linear, nor is it hierarchical. In fact, not many things are linear or hierarchical at all. Then why is it that most tools and thinking strategies assume a nice chronological or hierarchical order for my thought processes? The ideal tool for thought for me would embrace the messiness of my mind, and organically help insights emerge from chaos instead of forcing an artificial order. A rhizomatic, not arboresecent, form of note taking.
|
||||||
|
@ -20,7 +20,7 @@ Quartz embraces the inherent rhizomatic and web-like nature of our thinking and
|
||||||
|
|
||||||
The goal of digital gardening should be to tap into your network’s collective intelligence to create constructive feedback loops. If done well, I have a shareable representation of my thoughts that I can send out into the world and people can respond. Even for my most half-baked thoughts, this helps me create a feedback cycle to strengthen and fully flesh out that idea.
|
The goal of digital gardening should be to tap into your network’s collective intelligence to create constructive feedback loops. If done well, I have a shareable representation of my thoughts that I can send out into the world and people can respond. Even for my most half-baked thoughts, this helps me create a feedback cycle to strengthen and fully flesh out that idea.
|
||||||
|
|
||||||
Quartz is designed first and foremost as a tool for publishing [digital gardens](https://jzhao.xyz/posts/networked-thought/) to the web. To me, digital gardening is not just passive knowledge collection. It’s a form of expression and sharing.
|
Quartz is designed first and foremost as a tool for publishing [digital gardens](https://jzhao.xyz/posts/networked-thought/) to the web. To me, digital gardening is not just passive knowledge collection. It’s a form of expression and sharing.
|
||||||
|
|
||||||
> “[One] who works with the door open gets all kinds of interruptions, but [they] also occasionally gets clues as to what the world is and what might be important.” — Richard Hamming
|
> “[One] who works with the door open gets all kinds of interruptions, but [they] also occasionally gets clues as to what the world is and what might be important.” — Richard Hamming
|
||||||
|
|
||||||
|
|
|
@ -2,4 +2,4 @@
|
||||||
title: Components
|
title: Components
|
||||||
---
|
---
|
||||||
|
|
||||||
Want to create your own custom component? Check out the advanced guide on [[creating components]] for more information.
|
Want to create your own custom component? Check out the advanced guide on [[creating components]] for more information.
|
||||||
|
|
8
globals.d.ts
vendored
8
globals.d.ts
vendored
|
@ -1,8 +1,10 @@
|
||||||
export declare global {
|
export declare global {
|
||||||
interface Document {
|
interface Document {
|
||||||
addEventListener<K extends keyof CustomEventMap>(type: K,
|
addEventListener<K extends keyof CustomEventMap>(
|
||||||
listener: (this: Document, ev: CustomEventMap[K]) => void): void;
|
type: K,
|
||||||
dispatchEvent<K extends keyof CustomEventMap>(ev: CustomEventMap[K]): void;
|
listener: (this: Document, ev: CustomEventMap[K]) => void,
|
||||||
|
): void
|
||||||
|
dispatchEvent<K extends keyof CustomEventMap>(ev: CustomEventMap[K]): void
|
||||||
}
|
}
|
||||||
interface Window {
|
interface Window {
|
||||||
spaNavigate(url: URL, isBack: boolean = false)
|
spaNavigate(url: URL, isBack: boolean = false)
|
||||||
|
|
4
index.d.ts
vendored
4
index.d.ts
vendored
|
@ -1,11 +1,11 @@
|
||||||
declare module '*.scss' {
|
declare module "*.scss" {
|
||||||
const content: string
|
const content: string
|
||||||
export = content
|
export = content
|
||||||
}
|
}
|
||||||
|
|
||||||
// dom custom event
|
// dom custom event
|
||||||
interface CustomEventMap {
|
interface CustomEventMap {
|
||||||
"nav": CustomEvent<{ url: CanonicalSlug }>;
|
nav: CustomEvent<{ url: CanonicalSlug }>
|
||||||
}
|
}
|
||||||
|
|
||||||
declare const fetchData: Promise<ContentIndex>
|
declare const fetchData: Promise<ContentIndex>
|
||||||
|
|
|
@ -7,7 +7,7 @@ const generalConfiguration: GlobalConfiguration = {
|
||||||
enableSPA: true,
|
enableSPA: true,
|
||||||
enablePopovers: true,
|
enablePopovers: true,
|
||||||
analytics: {
|
analytics: {
|
||||||
provider: 'plausible',
|
provider: "plausible",
|
||||||
},
|
},
|
||||||
baseUrl: "quartz.jzhao.xyz",
|
baseUrl: "quartz.jzhao.xyz",
|
||||||
ignorePatterns: ["private", "templates"],
|
ignorePatterns: ["private", "templates"],
|
||||||
|
@ -19,27 +19,27 @@ const generalConfiguration: GlobalConfiguration = {
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
lightMode: {
|
lightMode: {
|
||||||
light: '#faf8f8',
|
light: "#faf8f8",
|
||||||
lightgray: '#e5e5e5',
|
lightgray: "#e5e5e5",
|
||||||
gray: '#b8b8b8',
|
gray: "#b8b8b8",
|
||||||
darkgray: '#4e4e4e',
|
darkgray: "#4e4e4e",
|
||||||
dark: '#2b2b2b',
|
dark: "#2b2b2b",
|
||||||
secondary: '#284b63',
|
secondary: "#284b63",
|
||||||
tertiary: '#84a59d',
|
tertiary: "#84a59d",
|
||||||
highlight: 'rgba(143, 159, 169, 0.15)',
|
highlight: "rgba(143, 159, 169, 0.15)",
|
||||||
},
|
},
|
||||||
darkMode: {
|
darkMode: {
|
||||||
light: '#161618',
|
light: "#161618",
|
||||||
lightgray: '#393639',
|
lightgray: "#393639",
|
||||||
gray: '#646464',
|
gray: "#646464",
|
||||||
darkgray: '#d4d4d4',
|
darkgray: "#d4d4d4",
|
||||||
dark: '#ebebec',
|
dark: "#ebebec",
|
||||||
secondary: '#7b97aa',
|
secondary: "#7b97aa",
|
||||||
tertiary: '#84a59d',
|
tertiary: "#84a59d",
|
||||||
highlight: 'rgba(143, 159, 169, 0.15)',
|
highlight: "rgba(143, 159, 169, 0.15)",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const sharedPageComponents = {
|
const sharedPageComponents = {
|
||||||
|
@ -47,18 +47,14 @@ const sharedPageComponents = {
|
||||||
header: [],
|
header: [],
|
||||||
footer: Component.Footer({
|
footer: Component.Footer({
|
||||||
links: {
|
links: {
|
||||||
"GitHub": "https://github.com/jackyzha0/quartz",
|
GitHub: "https://github.com/jackyzha0/quartz",
|
||||||
"Discord Community": "https://discord.gg/cRFFHYye7t"
|
"Discord Community": "https://discord.gg/cRFFHYye7t",
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentPageLayout: PageLayout = {
|
const contentPageLayout: PageLayout = {
|
||||||
beforeBody: [
|
beforeBody: [Component.ArticleTitle(), Component.ReadingTime(), Component.TagList()],
|
||||||
Component.ArticleTitle(),
|
|
||||||
Component.ReadingTime(),
|
|
||||||
Component.TagList(),
|
|
||||||
],
|
|
||||||
left: [
|
left: [
|
||||||
Component.PageTitle(),
|
Component.PageTitle(),
|
||||||
Component.MobileOnly(Component.Spacer()),
|
Component.MobileOnly(Component.Spacer()),
|
||||||
|
@ -66,21 +62,16 @@ const contentPageLayout: PageLayout = {
|
||||||
Component.Darkmode(),
|
Component.Darkmode(),
|
||||||
Component.DesktopOnly(Component.TableOfContents()),
|
Component.DesktopOnly(Component.TableOfContents()),
|
||||||
],
|
],
|
||||||
right: [
|
right: [Component.Graph(), Component.Backlinks()],
|
||||||
Component.Graph(),
|
|
||||||
Component.Backlinks(),
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const listPageLayout: PageLayout = {
|
const listPageLayout: PageLayout = {
|
||||||
beforeBody: [
|
beforeBody: [Component.ArticleTitle()],
|
||||||
Component.ArticleTitle()
|
|
||||||
],
|
|
||||||
left: [
|
left: [
|
||||||
Component.PageTitle(),
|
Component.PageTitle(),
|
||||||
Component.MobileOnly(Component.Spacer()),
|
Component.MobileOnly(Component.Spacer()),
|
||||||
Component.Search(),
|
Component.Search(),
|
||||||
Component.Darkmode()
|
Component.Darkmode(),
|
||||||
],
|
],
|
||||||
right: [],
|
right: [],
|
||||||
}
|
}
|
||||||
|
@ -92,18 +83,16 @@ const config: QuartzConfig = {
|
||||||
Plugin.FrontMatter(),
|
Plugin.FrontMatter(),
|
||||||
Plugin.TableOfContents(),
|
Plugin.TableOfContents(),
|
||||||
Plugin.CreatedModifiedDate({
|
Plugin.CreatedModifiedDate({
|
||||||
priority: ['frontmatter', 'filesystem'] // you can add 'git' here for last modified from Git but this makes the build slower
|
priority: ["frontmatter", "filesystem"], // you can add 'git' here for last modified from Git but this makes the build slower
|
||||||
}),
|
}),
|
||||||
Plugin.SyntaxHighlighting(),
|
Plugin.SyntaxHighlighting(),
|
||||||
Plugin.ObsidianFlavoredMarkdown(),
|
Plugin.ObsidianFlavoredMarkdown(),
|
||||||
Plugin.GitHubFlavoredMarkdown(),
|
Plugin.GitHubFlavoredMarkdown(),
|
||||||
Plugin.CrawlLinks({ markdownLinkResolution: 'shortest' }),
|
Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
|
||||||
Plugin.Latex({ renderEngine: 'katex' }),
|
Plugin.Latex({ renderEngine: "katex" }),
|
||||||
Plugin.Description(),
|
Plugin.Description(),
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [Plugin.RemoveDrafts()],
|
||||||
Plugin.RemoveDrafts(),
|
|
||||||
],
|
|
||||||
emitters: [
|
emitters: [
|
||||||
Plugin.AliasRedirects(),
|
Plugin.AliasRedirects(),
|
||||||
Plugin.ContentPage({
|
Plugin.ContentPage({
|
||||||
|
@ -125,7 +114,7 @@ const config: QuartzConfig = {
|
||||||
enableSiteMap: true,
|
enableSiteMap: true,
|
||||||
enableRSS: true,
|
enableRSS: true,
|
||||||
}),
|
}),
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import { promises, readFileSync } from 'fs'
|
import { promises, readFileSync } from "fs"
|
||||||
import yargs from 'yargs'
|
import yargs from "yargs"
|
||||||
import path from 'path'
|
import path from "path"
|
||||||
import { hideBin } from 'yargs/helpers'
|
import { hideBin } from "yargs/helpers"
|
||||||
import esbuild from 'esbuild'
|
import esbuild from "esbuild"
|
||||||
import chalk from 'chalk'
|
import chalk from "chalk"
|
||||||
import { sassPlugin } from 'esbuild-sass-plugin'
|
import { sassPlugin } from "esbuild-sass-plugin"
|
||||||
import fs from 'fs'
|
import fs from "fs"
|
||||||
import { intro, isCancel, outro, select, text } from '@clack/prompts'
|
import { intro, isCancel, outro, select, text } from "@clack/prompts"
|
||||||
import { rimraf } from 'rimraf'
|
import { rimraf } from "rimraf"
|
||||||
import prettyBytes from 'pretty-bytes'
|
import prettyBytes from "pretty-bytes"
|
||||||
import { spawnSync } from 'child_process'
|
import { spawnSync } from "child_process"
|
||||||
|
|
||||||
const UPSTREAM_NAME = 'upstream'
|
const UPSTREAM_NAME = "upstream"
|
||||||
const QUARTZ_SOURCE_BRANCH = 'v4-alpha'
|
const QUARTZ_SOURCE_BRANCH = "v4-alpha"
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
const cacheDir = path.join(cwd, ".quartz-cache")
|
const cacheDir = path.join(cwd, ".quartz-cache")
|
||||||
const cacheFile = "./.quartz-cache/transpiled-build.mjs"
|
const cacheFile = "./.quartz-cache/transpiled-build.mjs"
|
||||||
|
@ -24,16 +24,16 @@ const contentCacheFolder = path.join(cacheDir, "content-cache")
|
||||||
const CommonArgv = {
|
const CommonArgv = {
|
||||||
directory: {
|
directory: {
|
||||||
string: true,
|
string: true,
|
||||||
alias: ['d'],
|
alias: ["d"],
|
||||||
default: 'content',
|
default: "content",
|
||||||
describe: 'directory to look for content files'
|
describe: "directory to look for content files",
|
||||||
},
|
},
|
||||||
verbose: {
|
verbose: {
|
||||||
boolean: true,
|
boolean: true,
|
||||||
alias: ['v'],
|
alias: ["v"],
|
||||||
default: false,
|
default: false,
|
||||||
describe: 'print out extra logging information'
|
describe: "print out extra logging information",
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const SyncArgv = {
|
const SyncArgv = {
|
||||||
|
@ -41,47 +41,46 @@ const SyncArgv = {
|
||||||
commit: {
|
commit: {
|
||||||
boolean: true,
|
boolean: true,
|
||||||
default: true,
|
default: true,
|
||||||
describe: 'create a git commit for your unsaved changes'
|
describe: "create a git commit for your unsaved changes",
|
||||||
},
|
},
|
||||||
push: {
|
push: {
|
||||||
boolean: true,
|
boolean: true,
|
||||||
default: true,
|
default: true,
|
||||||
describe: 'push updates to your Quartz fork'
|
describe: "push updates to your Quartz fork",
|
||||||
},
|
},
|
||||||
force: {
|
force: {
|
||||||
boolean: true,
|
boolean: true,
|
||||||
alias: ['f'],
|
alias: ["f"],
|
||||||
default: true,
|
default: true,
|
||||||
describe: 'whether to apply the --force flag to git commands'
|
describe: "whether to apply the --force flag to git commands",
|
||||||
},
|
},
|
||||||
pull: {
|
pull: {
|
||||||
boolean: true,
|
boolean: true,
|
||||||
default: true,
|
default: true,
|
||||||
describe: 'pull updates from your Quartz fork'
|
describe: "pull updates from your Quartz fork",
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const BuildArgv = {
|
const BuildArgv = {
|
||||||
...CommonArgv,
|
...CommonArgv,
|
||||||
output: {
|
output: {
|
||||||
string: true,
|
string: true,
|
||||||
alias: ['o'],
|
alias: ["o"],
|
||||||
default: 'public',
|
default: "public",
|
||||||
describe: 'output folder for files'
|
describe: "output folder for files",
|
||||||
},
|
},
|
||||||
serve: {
|
serve: {
|
||||||
boolean: true,
|
boolean: true,
|
||||||
default: false,
|
default: false,
|
||||||
describe: 'run a local server to live-preview your Quartz'
|
describe: "run a local server to live-preview your Quartz",
|
||||||
},
|
},
|
||||||
port: {
|
port: {
|
||||||
number: true,
|
number: true,
|
||||||
default: 8080,
|
default: 8080,
|
||||||
describe: 'port to serve Quartz on'
|
describe: "port to serve Quartz on",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function escapePath(fp) {
|
function escapePath(fp) {
|
||||||
return fp
|
return fp
|
||||||
.replace(/\\ /g, " ") // unescape spaces
|
.replace(/\\ /g, " ") // unescape spaces
|
||||||
|
@ -91,7 +90,6 @@ function escapePath(fp) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function exitIfCancel(val) {
|
function exitIfCancel(val) {
|
||||||
|
|
||||||
if (isCancel(val)) {
|
if (isCancel(val)) {
|
||||||
outro(chalk.red("Exiting"))
|
outro(chalk.red("Exiting"))
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
|
@ -101,32 +99,48 @@ function exitIfCancel(val) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function stashContentFolder(contentFolder) {
|
async function stashContentFolder(contentFolder) {
|
||||||
await fs.promises.cp(contentFolder, contentCacheFolder, { force: true, recursive: true, verbatimSymlinks: true, preserveTimestamps: true })
|
await fs.promises.cp(contentFolder, contentCacheFolder, {
|
||||||
|
force: true,
|
||||||
|
recursive: true,
|
||||||
|
verbatimSymlinks: true,
|
||||||
|
preserveTimestamps: true,
|
||||||
|
})
|
||||||
await fs.promises.rm(contentFolder, { force: true, recursive: true })
|
await fs.promises.rm(contentFolder, { force: true, recursive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function popContentFolder(contentFolder) {
|
async function popContentFolder(contentFolder) {
|
||||||
await fs.promises.cp(contentCacheFolder, contentFolder, { force: true, recursive: true, verbatimSymlinks: true, preserveTimestamps: true })
|
await fs.promises.cp(contentCacheFolder, contentFolder, {
|
||||||
|
force: true,
|
||||||
|
recursive: true,
|
||||||
|
verbatimSymlinks: true,
|
||||||
|
preserveTimestamps: true,
|
||||||
|
})
|
||||||
await fs.promises.rm(contentCacheFolder, { force: true, recursive: true })
|
await fs.promises.rm(contentCacheFolder, { force: true, recursive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
yargs(hideBin(process.argv))
|
yargs(hideBin(process.argv))
|
||||||
.scriptName("quartz")
|
.scriptName("quartz")
|
||||||
.version(version)
|
.version(version)
|
||||||
.usage('$0 <cmd> [args]')
|
.usage("$0 <cmd> [args]")
|
||||||
.command('create', 'Initialize Quartz', CommonArgv, async argv => {
|
.command("create", "Initialize Quartz", CommonArgv, async (argv) => {
|
||||||
console.log()
|
console.log()
|
||||||
intro(chalk.bgGreen.black(` Quartz v${version} `))
|
intro(chalk.bgGreen.black(` Quartz v${version} `))
|
||||||
const contentFolder = path.join(cwd, argv.directory)
|
const contentFolder = path.join(cwd, argv.directory)
|
||||||
const setupStrategy = exitIfCancel(await select({
|
const setupStrategy = exitIfCancel(
|
||||||
message: `Choose how to initialize the content in \`${contentFolder}\``,
|
await select({
|
||||||
options: [
|
message: `Choose how to initialize the content in \`${contentFolder}\``,
|
||||||
{ value: 'new', label: "Empty Quartz" },
|
options: [
|
||||||
{ value: 'copy', label: "Replace with an existing folder", hint: "overwrites `content`" },
|
{ value: "new", label: "Empty Quartz" },
|
||||||
{ value: 'symlink', label: "Symlink an existing folder", hint: "don't select this unless you know what you are doing!" },
|
{ value: "copy", label: "Replace with an existing folder", hint: "overwrites `content`" },
|
||||||
{ value: 'keep', label: "Keep the existing files" },
|
{
|
||||||
]
|
value: "symlink",
|
||||||
}))
|
label: "Symlink an existing folder",
|
||||||
|
hint: "don't select this unless you know what you are doing!",
|
||||||
|
},
|
||||||
|
{ value: "keep", label: "Keep the existing files" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
async function rmContentFolder() {
|
async function rmContentFolder() {
|
||||||
const contentStat = await fs.promises.lstat(contentFolder)
|
const contentStat = await fs.promises.lstat(contentFolder)
|
||||||
|
@ -139,54 +153,77 @@ yargs(hideBin(process.argv))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (setupStrategy === 'copy' || setupStrategy === 'symlink') {
|
if (setupStrategy === "copy" || setupStrategy === "symlink") {
|
||||||
const originalFolder = escapePath(exitIfCancel(await text({
|
const originalFolder = escapePath(
|
||||||
message: "Enter the full path to existing content folder",
|
exitIfCancel(
|
||||||
placeholder: 'On most terminal emulators, you can drag and drop a folder into the window and it will paste the full path',
|
await text({
|
||||||
validate(fp) {
|
message: "Enter the full path to existing content folder",
|
||||||
const fullPath = escapePath(fp)
|
placeholder:
|
||||||
if (!fs.existsSync(fullPath)) {
|
"On most terminal emulators, you can drag and drop a folder into the window and it will paste the full path",
|
||||||
return "The given path doesn't exist"
|
validate(fp) {
|
||||||
} else if (!fs.lstatSync(fullPath).isDirectory()) {
|
const fullPath = escapePath(fp)
|
||||||
return "The given path is not a folder"
|
if (!fs.existsSync(fullPath)) {
|
||||||
}
|
return "The given path doesn't exist"
|
||||||
}
|
} else if (!fs.lstatSync(fullPath).isDirectory()) {
|
||||||
})))
|
return "The given path is not a folder"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
await rmContentFolder()
|
await rmContentFolder()
|
||||||
if (setupStrategy === 'copy') {
|
if (setupStrategy === "copy") {
|
||||||
await fs.promises.cp(originalFolder, contentFolder, { recursive: true })
|
await fs.promises.cp(originalFolder, contentFolder, { recursive: true })
|
||||||
} else if (setupStrategy === 'symlink') {
|
} else if (setupStrategy === "symlink") {
|
||||||
await fs.promises.symlink(originalFolder, contentFolder, 'dir')
|
await fs.promises.symlink(originalFolder, contentFolder, "dir")
|
||||||
}
|
}
|
||||||
} else if (setupStrategy === 'new') {
|
} else if (setupStrategy === "new") {
|
||||||
await rmContentFolder()
|
await rmContentFolder()
|
||||||
await fs.promises.mkdir(contentFolder)
|
await fs.promises.mkdir(contentFolder)
|
||||||
await fs.promises.writeFile(path.join(contentFolder, "index.md"),
|
await fs.promises.writeFile(
|
||||||
|
path.join(contentFolder, "index.md"),
|
||||||
`---
|
`---
|
||||||
title: Welcome to Quartz
|
title: Welcome to Quartz
|
||||||
---
|
---
|
||||||
|
|
||||||
This is a blank Quartz installation.
|
This is a blank Quartz installation.
|
||||||
See the [documentation](https://quartz.jzhao.xyz) for how to get started.
|
See the [documentation](https://quartz.jzhao.xyz) for how to get started.
|
||||||
`
|
`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get a prefered link resolution strategy
|
// get a prefered link resolution strategy
|
||||||
const linkResolutionStrategy = exitIfCancel(await select({
|
const linkResolutionStrategy = exitIfCancel(
|
||||||
message: `Choose how Quartz should resolve links in your content. You can change this later in \`quartz.config.ts\`.`,
|
await select({
|
||||||
options: [
|
message: `Choose how Quartz should resolve links in your content. You can change this later in \`quartz.config.ts\`.`,
|
||||||
{ value: 'absolute', label: "Treat links as absolute path", hint: "for content made for Quartz 3 and Hugo" },
|
options: [
|
||||||
{ value: 'shortest', label: "Treat links as shortest path", hint: "for most Obsidian vaults" },
|
{
|
||||||
{ value: 'relative', label: "Treat links as relative paths", hint: "for just normal Markdown files" },
|
value: "absolute",
|
||||||
]
|
label: "Treat links as absolute path",
|
||||||
}))
|
hint: "for content made for Quartz 3 and Hugo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "shortest",
|
||||||
|
label: "Treat links as shortest path",
|
||||||
|
hint: "for most Obsidian vaults",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "relative",
|
||||||
|
label: "Treat links as relative paths",
|
||||||
|
hint: "for just normal Markdown files",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
// now, do config changes
|
// now, do config changes
|
||||||
const configFilePath = path.join(cwd, "quartz.config.ts")
|
const configFilePath = path.join(cwd, "quartz.config.ts")
|
||||||
let configContent = await fs.promises.readFile(configFilePath, { encoding: 'utf-8' })
|
let configContent = await fs.promises.readFile(configFilePath, { encoding: "utf-8" })
|
||||||
configContent = configContent.replace(/markdownLinkResolution: '(.+)'/, `markdownLinkResolution: '${linkResolutionStrategy}'`)
|
configContent = configContent.replace(
|
||||||
|
/markdownLinkResolution: '(.+)'/,
|
||||||
|
`markdownLinkResolution: '${linkResolutionStrategy}'`,
|
||||||
|
)
|
||||||
await fs.promises.writeFile(configFilePath, configContent)
|
await fs.promises.writeFile(configFilePath, configContent)
|
||||||
|
|
||||||
outro(`You're all set! Not sure what to do next? Try:
|
outro(`You're all set! Not sure what to do next? Try:
|
||||||
|
@ -195,105 +232,120 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
|
||||||
• Hosting your Quartz online (see: https://quartz.jzhao.xyz/setup/hosting)
|
• Hosting your Quartz online (see: https://quartz.jzhao.xyz/setup/hosting)
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
.command('update', 'Get the latest Quartz updates', CommonArgv, async argv => {
|
.command("update", "Get the latest Quartz updates", CommonArgv, async (argv) => {
|
||||||
const contentFolder = path.join(cwd, argv.directory)
|
const contentFolder = path.join(cwd, argv.directory)
|
||||||
console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`))
|
console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`))
|
||||||
console.log('Backing up your content')
|
console.log("Backing up your content")
|
||||||
await stashContentFolder(contentFolder)
|
await stashContentFolder(contentFolder)
|
||||||
console.log("Pulling updates... you may need to resolve some `git` conflicts if you've made changes to components or plugins.")
|
console.log(
|
||||||
spawnSync('git', ['pull', UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH], { stdio: 'inherit' })
|
"Pulling updates... you may need to resolve some `git` conflicts if you've made changes to components or plugins.",
|
||||||
|
)
|
||||||
|
spawnSync("git", ["pull", UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH], { stdio: "inherit" })
|
||||||
await popContentFolder(contentFolder)
|
await popContentFolder(contentFolder)
|
||||||
console.log(chalk.green('Done!'))
|
console.log(chalk.green("Done!"))
|
||||||
})
|
})
|
||||||
.command('sync', 'Sync your Quartz to and from GitHub.', SyncArgv, async argv => {
|
.command("sync", "Sync your Quartz to and from GitHub.", SyncArgv, async (argv) => {
|
||||||
const contentFolder = path.join(cwd, argv.directory)
|
const contentFolder = path.join(cwd, argv.directory)
|
||||||
console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`))
|
console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`))
|
||||||
console.log('Backing up your content')
|
console.log("Backing up your content")
|
||||||
|
|
||||||
if (argv.commit) {
|
if (argv.commit) {
|
||||||
const currentTimestamp = new Date().toLocaleString('en-US', { dateStyle: "medium", timeStyle: "short" })
|
const currentTimestamp = new Date().toLocaleString("en-US", {
|
||||||
spawnSync('git', ['commit', '-am', `Quartz sync: ${currentTimestamp}`], { stdio: 'inherit' })
|
dateStyle: "medium",
|
||||||
|
timeStyle: "short",
|
||||||
|
})
|
||||||
|
spawnSync("git", ["commit", "-am", `Quartz sync: ${currentTimestamp}`], { stdio: "inherit" })
|
||||||
}
|
}
|
||||||
|
|
||||||
await stashContentFolder(contentFolder)
|
await stashContentFolder(contentFolder)
|
||||||
|
|
||||||
if (argv.pull) {
|
if (argv.pull) {
|
||||||
console.log("Pulling updates from your repository. You may need to resolve some `git` conflicts if you've made changes to components or plugins.")
|
console.log(
|
||||||
spawnSync('git', ['pull', 'origin', QUARTZ_SOURCE_BRANCH], { stdio: 'inherit' })
|
"Pulling updates from your repository. You may need to resolve some `git` conflicts if you've made changes to components or plugins.",
|
||||||
|
)
|
||||||
|
spawnSync("git", ["pull", "origin", QUARTZ_SOURCE_BRANCH], { stdio: "inherit" })
|
||||||
}
|
}
|
||||||
|
|
||||||
await popContentFolder(contentFolder)
|
await popContentFolder(contentFolder)
|
||||||
if (argv.push) {
|
if (argv.push) {
|
||||||
console.log("Pushing your changes")
|
console.log("Pushing your changes")
|
||||||
const args = argv.force ?
|
const args = argv.force
|
||||||
['push', '-f', 'origin', QUARTZ_SOURCE_BRANCH] :
|
? ["push", "-f", "origin", QUARTZ_SOURCE_BRANCH]
|
||||||
['push', 'origin', QUARTZ_SOURCE_BRANCH]
|
: ["push", "origin", QUARTZ_SOURCE_BRANCH]
|
||||||
spawnSync('git', args, { stdio: 'inherit' })
|
spawnSync("git", args, { stdio: "inherit" })
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(chalk.green('Done!'))
|
console.log(chalk.green("Done!"))
|
||||||
})
|
})
|
||||||
.command('build', 'Build Quartz into a bundle of static HTML files', BuildArgv, async argv => {
|
.command("build", "Build Quartz into a bundle of static HTML files", BuildArgv, async (argv) => {
|
||||||
const result = await esbuild.build({
|
const result = await esbuild
|
||||||
entryPoints: [fp],
|
.build({
|
||||||
outfile: path.join("quartz", cacheFile),
|
entryPoints: [fp],
|
||||||
bundle: true,
|
outfile: path.join("quartz", cacheFile),
|
||||||
keepNames: true,
|
bundle: true,
|
||||||
platform: "node",
|
keepNames: true,
|
||||||
format: "esm",
|
platform: "node",
|
||||||
jsx: "automatic",
|
format: "esm",
|
||||||
jsxImportSource: "preact",
|
jsx: "automatic",
|
||||||
packages: "external",
|
jsxImportSource: "preact",
|
||||||
metafile: true,
|
packages: "external",
|
||||||
sourcemap: true,
|
metafile: true,
|
||||||
plugins: [
|
sourcemap: true,
|
||||||
sassPlugin({
|
plugins: [
|
||||||
type: 'css-text',
|
sassPlugin({
|
||||||
}),
|
type: "css-text",
|
||||||
{
|
}),
|
||||||
name: 'inline-script-loader',
|
{
|
||||||
setup(build) {
|
name: "inline-script-loader",
|
||||||
build.onLoad({ filter: /\.inline\.(ts|js)$/ }, async (args) => {
|
setup(build) {
|
||||||
let text = await promises.readFile(args.path, 'utf8')
|
build.onLoad({ filter: /\.inline\.(ts|js)$/ }, async (args) => {
|
||||||
|
let text = await promises.readFile(args.path, "utf8")
|
||||||
|
|
||||||
// remove default exports that we manually inserted
|
// remove default exports that we manually inserted
|
||||||
text = text.replace('export default', '')
|
text = text.replace("export default", "")
|
||||||
text = text.replace('export', '')
|
text = text.replace("export", "")
|
||||||
|
|
||||||
const sourcefile = path.relative(path.resolve('.'), args.path)
|
const sourcefile = path.relative(path.resolve("."), args.path)
|
||||||
const resolveDir = path.dirname(sourcefile)
|
const resolveDir = path.dirname(sourcefile)
|
||||||
const transpiled = await esbuild.build({
|
const transpiled = await esbuild.build({
|
||||||
stdin: {
|
stdin: {
|
||||||
contents: text,
|
contents: text,
|
||||||
loader: 'ts',
|
loader: "ts",
|
||||||
resolveDir,
|
resolveDir,
|
||||||
sourcefile,
|
sourcefile,
|
||||||
},
|
},
|
||||||
write: false,
|
write: false,
|
||||||
bundle: true,
|
bundle: true,
|
||||||
platform: "browser",
|
platform: "browser",
|
||||||
format: "esm",
|
format: "esm",
|
||||||
|
})
|
||||||
|
const rawMod = transpiled.outputFiles[0].text
|
||||||
|
return {
|
||||||
|
contents: rawMod,
|
||||||
|
loader: "text",
|
||||||
|
}
|
||||||
})
|
})
|
||||||
const rawMod = transpiled.outputFiles[0].text
|
},
|
||||||
return {
|
},
|
||||||
contents: rawMod,
|
],
|
||||||
loader: 'text',
|
})
|
||||||
}
|
.catch((err) => {
|
||||||
})
|
console.error(`${chalk.red("Couldn't parse Quartz configuration:")} ${fp}`)
|
||||||
}
|
console.log(`Reason: ${chalk.grey(err)}`)
|
||||||
}
|
console.log(
|
||||||
]
|
"hint: make sure all the required dependencies are installed (run `npm install`)",
|
||||||
}).catch(err => {
|
)
|
||||||
console.error(`${chalk.red("Couldn't parse Quartz configuration:")} ${fp}`)
|
process.exit(1)
|
||||||
console.log(`Reason: ${chalk.grey(err)}`)
|
})
|
||||||
console.log("hint: make sure all the required dependencies are installed (run `npm install`)")
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (argv.verbose) {
|
if (argv.verbose) {
|
||||||
const outputFileName = 'quartz/.quartz-cache/transpiled-build.mjs'
|
const outputFileName = "quartz/.quartz-cache/transpiled-build.mjs"
|
||||||
const meta = result.metafile.outputs[outputFileName]
|
const meta = result.metafile.outputs[outputFileName]
|
||||||
console.log(`Successfully transpiled ${Object.keys(meta.inputs).length} files (${prettyBytes(meta.bytes)})`)
|
console.log(
|
||||||
|
`Successfully transpiled ${Object.keys(meta.inputs).length} files (${prettyBytes(
|
||||||
|
meta.bytes,
|
||||||
|
)})`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { default: buildQuartz } = await import(cacheFile)
|
const { default: buildQuartz } = await import(cacheFile)
|
||||||
|
@ -302,5 +354,4 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
|
||||||
.showHelpOnFail(false)
|
.showHelpOnFail(false)
|
||||||
.help()
|
.help()
|
||||||
.strict()
|
.strict()
|
||||||
.demandCommand()
|
.demandCommand().argv
|
||||||
.argv
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import workerpool from 'workerpool'
|
import workerpool from "workerpool"
|
||||||
const cacheFile = "./.quartz-cache/transpiled-worker.mjs"
|
const cacheFile = "./.quartz-cache/transpiled-worker.mjs"
|
||||||
const { parseFiles } = await import(cacheFile)
|
const { parseFiles } = await import(cacheFile)
|
||||||
workerpool.worker({
|
workerpool.worker({
|
||||||
parseFiles
|
parseFiles,
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'source-map-support/register.js'
|
import "source-map-support/register.js"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { PerfTimer } from "./perf"
|
import { PerfTimer } from "./perf"
|
||||||
import { rimraf } from "rimraf"
|
import { rimraf } from "rimraf"
|
||||||
|
@ -12,8 +12,8 @@ import { emitContent } from "./processors/emit"
|
||||||
import cfg from "../quartz.config"
|
import cfg from "../quartz.config"
|
||||||
import { FilePath } from "./path"
|
import { FilePath } from "./path"
|
||||||
import chokidar from "chokidar"
|
import chokidar from "chokidar"
|
||||||
import { ProcessedContent } from './plugins/vfile'
|
import { ProcessedContent } from "./plugins/vfile"
|
||||||
import WebSocket, { WebSocketServer } from 'ws'
|
import WebSocket, { WebSocketServer } from "ws"
|
||||||
|
|
||||||
interface Argv {
|
interface Argv {
|
||||||
directory: string
|
directory: string
|
||||||
|
@ -29,30 +29,38 @@ export default async function buildQuartz(argv: Argv, version: string) {
|
||||||
const output = argv.output
|
const output = argv.output
|
||||||
|
|
||||||
const pluginCount = Object.values(cfg.plugins).flat().length
|
const pluginCount = Object.values(cfg.plugins).flat().length
|
||||||
const pluginNames = (key: 'transformers' | 'filters' | 'emitters') => cfg.plugins[key].map(plugin => plugin.name)
|
const pluginNames = (key: "transformers" | "filters" | "emitters") =>
|
||||||
|
cfg.plugins[key].map((plugin) => plugin.name)
|
||||||
if (argv.verbose) {
|
if (argv.verbose) {
|
||||||
console.log(`Loaded ${pluginCount} plugins`)
|
console.log(`Loaded ${pluginCount} plugins`)
|
||||||
console.log(` Transformers: ${pluginNames('transformers').join(", ")}`)
|
console.log(` Transformers: ${pluginNames("transformers").join(", ")}`)
|
||||||
console.log(` Filters: ${pluginNames('filters').join(", ")}`)
|
console.log(` Filters: ${pluginNames("filters").join(", ")}`)
|
||||||
console.log(` Emitters: ${pluginNames('emitters').join(", ")}`)
|
console.log(` Emitters: ${pluginNames("emitters").join(", ")}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// clean
|
// clean
|
||||||
perf.addEvent('clean')
|
perf.addEvent("clean")
|
||||||
await rimraf(output)
|
await rimraf(output)
|
||||||
console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince('clean')}`)
|
console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`)
|
||||||
|
|
||||||
// glob
|
// glob
|
||||||
perf.addEvent('glob')
|
perf.addEvent("glob")
|
||||||
const fps = await globby('**/*.md', {
|
const fps = await globby("**/*.md", {
|
||||||
cwd: argv.directory,
|
cwd: argv.directory,
|
||||||
ignore: cfg.configuration.ignorePatterns,
|
ignore: cfg.configuration.ignorePatterns,
|
||||||
gitignore: true,
|
gitignore: true,
|
||||||
})
|
})
|
||||||
console.log(`Found ${fps.length} input files from \`${argv.directory}\` in ${perf.timeSince('glob')}`)
|
console.log(
|
||||||
|
`Found ${fps.length} input files from \`${argv.directory}\` in ${perf.timeSince("glob")}`,
|
||||||
|
)
|
||||||
|
|
||||||
const filePaths = fps.map(fp => `${argv.directory}${path.sep}${fp}` as FilePath)
|
const filePaths = fps.map((fp) => `${argv.directory}${path.sep}${fp}` as FilePath)
|
||||||
const parsedFiles = await parseMarkdown(cfg.plugins.transformers, argv.directory, filePaths, argv.verbose)
|
const parsedFiles = await parseMarkdown(
|
||||||
|
cfg.plugins.transformers,
|
||||||
|
argv.directory,
|
||||||
|
filePaths,
|
||||||
|
argv.verbose,
|
||||||
|
)
|
||||||
const filteredContent = filterContent(cfg.plugins.filters, parsedFiles, argv.verbose)
|
const filteredContent = filterContent(cfg.plugins.filters, parsedFiles, argv.verbose)
|
||||||
await emitContent(argv.directory, output, cfg, filteredContent, argv.serve, argv.verbose)
|
await emitContent(argv.directory, output, cfg, filteredContent, argv.serve, argv.verbose)
|
||||||
console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`))
|
console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`))
|
||||||
|
@ -60,7 +68,7 @@ export default async function buildQuartz(argv: Argv, version: string) {
|
||||||
if (argv.serve) {
|
if (argv.serve) {
|
||||||
const wss = new WebSocketServer({ port: 3001 })
|
const wss = new WebSocketServer({ port: 3001 })
|
||||||
const connections: WebSocket[] = []
|
const connections: WebSocket[] = []
|
||||||
wss.on('connection', ws => connections.push(ws))
|
wss.on("connection", (ws) => connections.push(ws))
|
||||||
|
|
||||||
const ignored = await isGitIgnored()
|
const ignored = await isGitIgnored()
|
||||||
const contentMap = new Map<FilePath, ProcessedContent>()
|
const contentMap = new Map<FilePath, ProcessedContent>()
|
||||||
|
@ -69,15 +77,20 @@ export default async function buildQuartz(argv: Argv, version: string) {
|
||||||
contentMap.set(vfile.data.filePath!, content)
|
contentMap.set(vfile.data.filePath!, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rebuild(fp: string, action: 'add' | 'change' | 'unlink') {
|
async function rebuild(fp: string, action: "add" | "change" | "unlink") {
|
||||||
perf.addEvent('rebuild')
|
perf.addEvent("rebuild")
|
||||||
if (!ignored(fp)) {
|
if (!ignored(fp)) {
|
||||||
console.log(chalk.yellow(`Detected change in ${fp}, rebuilding...`))
|
console.log(chalk.yellow(`Detected change in ${fp}, rebuilding...`))
|
||||||
const fullPath = `${argv.directory}${path.sep}${fp}` as FilePath
|
const fullPath = `${argv.directory}${path.sep}${fp}` as FilePath
|
||||||
if (action === 'add' || action === 'change') {
|
if (action === "add" || action === "change") {
|
||||||
const [parsedContent] = await parseMarkdown(cfg.plugins.transformers, argv.directory, [fullPath], argv.verbose)
|
const [parsedContent] = await parseMarkdown(
|
||||||
|
cfg.plugins.transformers,
|
||||||
|
argv.directory,
|
||||||
|
[fullPath],
|
||||||
|
argv.verbose,
|
||||||
|
)
|
||||||
contentMap.set(fullPath, parsedContent)
|
contentMap.set(fullPath, parsedContent)
|
||||||
} else if (action === 'unlink') {
|
} else if (action === "unlink") {
|
||||||
contentMap.delete(fullPath)
|
contentMap.delete(fullPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,21 +98,21 @@ export default async function buildQuartz(argv: Argv, version: string) {
|
||||||
const parsedFiles = [...contentMap.values()]
|
const parsedFiles = [...contentMap.values()]
|
||||||
const filteredContent = filterContent(cfg.plugins.filters, parsedFiles, argv.verbose)
|
const filteredContent = filterContent(cfg.plugins.filters, parsedFiles, argv.verbose)
|
||||||
await emitContent(argv.directory, output, cfg, filteredContent, argv.serve, argv.verbose)
|
await emitContent(argv.directory, output, cfg, filteredContent, argv.serve, argv.verbose)
|
||||||
console.log(chalk.green(`Done rebuilding in ${perf.timeSince('rebuild')}`))
|
console.log(chalk.green(`Done rebuilding in ${perf.timeSince("rebuild")}`))
|
||||||
connections.forEach(conn => conn.send('rebuild'))
|
connections.forEach((conn) => conn.send("rebuild"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const watcher = chokidar.watch('.', {
|
const watcher = chokidar.watch(".", {
|
||||||
persistent: true,
|
persistent: true,
|
||||||
cwd: argv.directory,
|
cwd: argv.directory,
|
||||||
ignoreInitial: true,
|
ignoreInitial: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
watcher
|
watcher
|
||||||
.on('add', fp => rebuild(fp, 'add'))
|
.on("add", (fp) => rebuild(fp, "add"))
|
||||||
.on('change', fp => rebuild(fp, 'change'))
|
.on("change", (fp) => rebuild(fp, "change"))
|
||||||
.on('unlink', fp => rebuild(fp, 'unlink'))
|
.on("unlink", (fp) => rebuild(fp, "unlink"))
|
||||||
|
|
||||||
const server = http.createServer(async (req, res) => {
|
const server = http.createServer(async (req, res) => {
|
||||||
await serveHandler(req, res, {
|
await serveHandler(req, res, {
|
||||||
|
@ -107,15 +120,16 @@ export default async function buildQuartz(argv: Argv, version: string) {
|
||||||
directoryListing: false,
|
directoryListing: false,
|
||||||
})
|
})
|
||||||
const status = res.statusCode
|
const status = res.statusCode
|
||||||
const statusString = (status >= 200 && status < 300) ?
|
const statusString =
|
||||||
chalk.green(`[${status}]`) :
|
status >= 200 && status < 300
|
||||||
(status >= 300 && status < 400) ?
|
? chalk.green(`[${status}]`)
|
||||||
chalk.yellow(`[${status}]`) :
|
: status >= 300 && status < 400
|
||||||
chalk.red(`[${status}]`)
|
? chalk.yellow(`[${status}]`)
|
||||||
|
: chalk.red(`[${status}]`)
|
||||||
console.log(statusString + chalk.grey(` ${req.url}`))
|
console.log(statusString + chalk.grey(` ${req.url}`))
|
||||||
})
|
})
|
||||||
server.listen(argv.port)
|
server.listen(argv.port)
|
||||||
console.log(chalk.cyan(`Started a Quartz server listening at http://localhost:${argv.port}`))
|
console.log(chalk.cyan(`Started a Quartz server listening at http://localhost:${argv.port}`))
|
||||||
console.log('hint: exit with ctrl+c')
|
console.log("hint: exit with ctrl+c")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,43 +5,43 @@ import { Theme } from "./theme"
|
||||||
export type Analytics =
|
export type Analytics =
|
||||||
| null
|
| null
|
||||||
| {
|
| {
|
||||||
provider: 'plausible'
|
provider: "plausible"
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
provider: 'google',
|
provider: "google"
|
||||||
tagId: string
|
tagId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GlobalConfiguration {
|
export interface GlobalConfiguration {
|
||||||
pageTitle: string,
|
pageTitle: string
|
||||||
/** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */
|
/** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */
|
||||||
enableSPA: boolean,
|
enableSPA: boolean
|
||||||
/** Whether to display Wikipedia-style popovers when hovering over links */
|
/** Whether to display Wikipedia-style popovers when hovering over links */
|
||||||
enablePopovers: boolean,
|
enablePopovers: boolean
|
||||||
/** Analytics mode */
|
/** Analytics mode */
|
||||||
analytics: Analytics
|
analytics: Analytics
|
||||||
/** Glob patterns to not search */
|
/** Glob patterns to not search */
|
||||||
ignorePatterns: string[],
|
ignorePatterns: string[]
|
||||||
/** Base URL to use for CNAME files, sitemaps, and RSS feeds that require an absolute URL.
|
/** Base URL to use for CNAME files, sitemaps, and RSS feeds that require an absolute URL.
|
||||||
* Quartz will avoid using this as much as possible and use relative URLs most of the time
|
* Quartz will avoid using this as much as possible and use relative URLs most of the time
|
||||||
*/
|
*/
|
||||||
baseUrl?: string,
|
baseUrl?: string
|
||||||
theme: Theme
|
theme: Theme
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuartzConfig {
|
export interface QuartzConfig {
|
||||||
configuration: GlobalConfiguration,
|
configuration: GlobalConfiguration
|
||||||
plugins: PluginTypes,
|
plugins: PluginTypes
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FullPageLayout {
|
export interface FullPageLayout {
|
||||||
head: QuartzComponent
|
head: QuartzComponent
|
||||||
header: QuartzComponent[],
|
header: QuartzComponent[]
|
||||||
beforeBody: QuartzComponent[],
|
beforeBody: QuartzComponent[]
|
||||||
pageBody: QuartzComponent,
|
pageBody: QuartzComponent
|
||||||
left: QuartzComponent[],
|
left: QuartzComponent[]
|
||||||
right: QuartzComponent[],
|
right: QuartzComponent[]
|
||||||
footer: QuartzComponent,
|
footer: QuartzComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PageLayout = Pick<FullPageLayout, "beforeBody" | "left" | "right">
|
export type PageLayout = Pick<FullPageLayout, "beforeBody" | "left" | "right">
|
||||||
|
|
|
@ -4,15 +4,25 @@ import { canonicalizeServer, resolveRelative } from "../path"
|
||||||
|
|
||||||
function Backlinks({ fileData, allFiles }: QuartzComponentProps) {
|
function Backlinks({ fileData, allFiles }: QuartzComponentProps) {
|
||||||
const slug = canonicalizeServer(fileData.slug!)
|
const slug = canonicalizeServer(fileData.slug!)
|
||||||
const backlinkFiles = allFiles.filter(file => file.links?.includes(slug))
|
const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
|
||||||
return <div class="backlinks">
|
return (
|
||||||
<h3>Backlinks</h3>
|
<div class="backlinks">
|
||||||
<ul class="overflow">
|
<h3>Backlinks</h3>
|
||||||
{backlinkFiles.length > 0 ?
|
<ul class="overflow">
|
||||||
backlinkFiles.map(f => <li><a href={resolveRelative(slug, canonicalizeServer(f.slug!))} class="internal">{f.frontmatter?.title}</a></li>)
|
{backlinkFiles.length > 0 ? (
|
||||||
: <li>No backlinks found</li>}
|
backlinkFiles.map((f) => (
|
||||||
</ul>
|
<li>
|
||||||
</div>
|
<a href={resolveRelative(slug, canonicalizeServer(f.slug!))} class="internal">
|
||||||
|
{f.frontmatter?.title}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<li>No backlinks found</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Backlinks.css = style
|
Backlinks.css = style
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import clipboardScript from './scripts/clipboard.inline'
|
import clipboardScript from "./scripts/clipboard.inline"
|
||||||
import clipboardStyle from './styles/clipboard.scss'
|
import clipboardStyle from "./styles/clipboard.scss"
|
||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
|
|
||||||
function Body({ children }: QuartzComponentProps) {
|
function Body({ children }: QuartzComponentProps) {
|
||||||
return <div id="quartz-body">
|
return <div id="quartz-body">{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Body.afterDOMLoaded = clipboardScript
|
Body.afterDOMLoaded = clipboardScript
|
||||||
Body.css = clipboardStyle
|
Body.css = clipboardStyle
|
||||||
|
|
||||||
export default (() => Body) satisfies QuartzComponentConstructor
|
export default (() => Body) satisfies QuartzComponentConstructor
|
||||||
|
|
||||||
|
|
|
@ -1,50 +1,48 @@
|
||||||
// @ts-ignore: this is safe, we don't want to actually make darkmode.inline.ts a module as
|
// @ts-ignore: this is safe, we don't want to actually make darkmode.inline.ts a module as
|
||||||
// modules are automatically deferred and we don't want that to happen for critical beforeDOMLoads
|
// modules are automatically deferred and we don't want that to happen for critical beforeDOMLoads
|
||||||
// see: https://v8.dev/features/modules#defer
|
// see: https://v8.dev/features/modules#defer
|
||||||
import darkmodeScript from "./scripts/darkmode.inline"
|
import darkmodeScript from "./scripts/darkmode.inline"
|
||||||
import styles from './styles/darkmode.scss'
|
import styles from "./styles/darkmode.scss"
|
||||||
import { QuartzComponentConstructor } from "./types"
|
import { QuartzComponentConstructor } from "./types"
|
||||||
|
|
||||||
function Darkmode() {
|
function Darkmode() {
|
||||||
return <div class="darkmode">
|
return (
|
||||||
<input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} />
|
<div class="darkmode">
|
||||||
<label id="toggle-label-light" for="darkmode-toggle" tabIndex={-1}>
|
<input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} />
|
||||||
<svg
|
<label id="toggle-label-light" for="darkmode-toggle" tabIndex={-1}>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<svg
|
||||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
version="1.1"
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||||
id="dayIcon"
|
version="1.1"
|
||||||
x="0px"
|
id="dayIcon"
|
||||||
y="0px"
|
x="0px"
|
||||||
viewBox="0 0 35 35"
|
y="0px"
|
||||||
style="enable-background:new 0 0 35 35;"
|
viewBox="0 0 35 35"
|
||||||
xmlSpace="preserve"
|
style="enable-background:new 0 0 35 35;"
|
||||||
>
|
xmlSpace="preserve"
|
||||||
<title>Light mode</title>
|
>
|
||||||
<path
|
<title>Light mode</title>
|
||||||
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 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>
|
</svg>
|
||||||
</svg>
|
</label>
|
||||||
</label>
|
<label id="toggle-label-dark" for="darkmode-toggle" tabIndex={-1}>
|
||||||
<label id="toggle-label-dark" for="darkmode-toggle" tabIndex={-1}>
|
<svg
|
||||||
<svg
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
version="1.1"
|
||||||
version="1.1"
|
id="nightIcon"
|
||||||
id="nightIcon"
|
x="0px"
|
||||||
x="0px"
|
y="0px"
|
||||||
y="0px"
|
viewBox="0 0 100 100"
|
||||||
viewBox="0 0 100 100"
|
style="enable-background='new 0 0 100 100'"
|
||||||
style="enable-background='new 0 0 100 100'"
|
xmlSpace="preserve"
|
||||||
xmlSpace="preserve"
|
>
|
||||||
>
|
<title>Dark mode</title>
|
||||||
<title>Dark mode</title>
|
<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
|
</svg>
|
||||||
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"
|
</label>
|
||||||
></path>
|
</div>
|
||||||
</svg>
|
)
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Darkmode.beforeDOMLoaded = darkmodeScript
|
Darkmode.beforeDOMLoaded = darkmodeScript
|
||||||
|
|
|
@ -3,10 +3,10 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Date({ date }: Props) {
|
export function Date({ date }: Props) {
|
||||||
const formattedDate = date.toLocaleDateString('en-US', {
|
const formattedDate = date.toLocaleDateString("en-US", {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "short",
|
month: "short",
|
||||||
day: '2-digit'
|
day: "2-digit",
|
||||||
})
|
})
|
||||||
return <>{formattedDate}</>
|
return <>{formattedDate}</>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { QuartzComponentConstructor } from "./types"
|
import { QuartzComponentConstructor } from "./types"
|
||||||
import style from "./styles/footer.scss"
|
import style from "./styles/footer.scss"
|
||||||
import {version} from "../../package.json"
|
import { version } from "../../package.json"
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
links: Record<string, string>
|
links: Record<string, string>
|
||||||
|
@ -10,13 +10,21 @@ export default ((opts?: Options) => {
|
||||||
function Footer() {
|
function Footer() {
|
||||||
const year = new Date().getFullYear()
|
const year = new Date().getFullYear()
|
||||||
const links = opts?.links ?? []
|
const links = opts?.links ?? []
|
||||||
return <footer>
|
return (
|
||||||
<hr />
|
<footer>
|
||||||
<p>Created with <a href="https://quartz.jzhao.xyz/">Quartz v{version}</a>, © {year}</p>
|
<hr />
|
||||||
<ul>{Object.entries(links).map(([text, link]) => <li>
|
<p>
|
||||||
<a href={link}>{text}</a>
|
Created with <a href="https://quartz.jzhao.xyz/">Quartz v{version}</a>, © {year}
|
||||||
</li>)}</ul>
|
</p>
|
||||||
</footer>
|
<ul>
|
||||||
|
{Object.entries(links).map(([text, link]) => (
|
||||||
|
<li>
|
||||||
|
<a href={link}>{text}</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Footer.css = style
|
Footer.css = style
|
||||||
|
|
|
@ -4,19 +4,19 @@ import script from "./scripts/graph.inline"
|
||||||
import style from "./styles/graph.scss"
|
import style from "./styles/graph.scss"
|
||||||
|
|
||||||
export interface D3Config {
|
export interface D3Config {
|
||||||
drag: boolean,
|
drag: boolean
|
||||||
zoom: boolean,
|
zoom: boolean
|
||||||
depth: number,
|
depth: number
|
||||||
scale: number,
|
scale: number
|
||||||
repelForce: number,
|
repelForce: number
|
||||||
centerForce: number,
|
centerForce: number
|
||||||
linkDistance: number,
|
linkDistance: number
|
||||||
fontSize: number,
|
fontSize: number
|
||||||
opacityScale: number
|
opacityScale: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GraphOptions {
|
interface GraphOptions {
|
||||||
localGraph: Partial<D3Config> | undefined,
|
localGraph: Partial<D3Config> | undefined
|
||||||
globalGraph: Partial<D3Config> | undefined
|
globalGraph: Partial<D3Config> | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ const defaultOptions: GraphOptions = {
|
||||||
centerForce: 0.3,
|
centerForce: 0.3,
|
||||||
linkDistance: 30,
|
linkDistance: 30,
|
||||||
fontSize: 0.6,
|
fontSize: 0.6,
|
||||||
opacityScale: 1
|
opacityScale: 1,
|
||||||
},
|
},
|
||||||
globalGraph: {
|
globalGraph: {
|
||||||
drag: true,
|
drag: true,
|
||||||
|
@ -41,21 +41,32 @@ const defaultOptions: GraphOptions = {
|
||||||
centerForce: 0.3,
|
centerForce: 0.3,
|
||||||
linkDistance: 30,
|
linkDistance: 30,
|
||||||
fontSize: 0.6,
|
fontSize: 0.6,
|
||||||
opacityScale: 1
|
opacityScale: 1,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ((opts?: GraphOptions) => {
|
export default ((opts?: GraphOptions) => {
|
||||||
function Graph() {
|
function Graph() {
|
||||||
const localGraph = { ...opts?.localGraph, ...defaultOptions.localGraph }
|
const localGraph = { ...opts?.localGraph, ...defaultOptions.localGraph }
|
||||||
const globalGraph = { ...opts?.globalGraph, ...defaultOptions.globalGraph }
|
const globalGraph = { ...opts?.globalGraph, ...defaultOptions.globalGraph }
|
||||||
return <div class="graph">
|
return (
|
||||||
<h3>Graph View</h3>
|
<div class="graph">
|
||||||
<div class="graph-outer">
|
<h3>Graph View</h3>
|
||||||
<div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
|
<div class="graph-outer">
|
||||||
<svg version="1.1" id="global-graph-icon" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
<div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
|
||||||
viewBox="0 0 55 55" fill="currentColor" xmlSpace="preserve">
|
<svg
|
||||||
<path d="M49,0c-3.309,0-6,2.691-6,6c0,1.035,0.263,2.009,0.726,2.86l-9.829,9.829C32.542,17.634,30.846,17,29,17
|
version="1.1"
|
||||||
|
id="global-graph-icon"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 55 55"
|
||||||
|
fill="currentColor"
|
||||||
|
xmlSpace="preserve"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M49,0c-3.309,0-6,2.691-6,6c0,1.035,0.263,2.009,0.726,2.86l-9.829,9.829C32.542,17.634,30.846,17,29,17
|
||||||
s-3.542,0.634-4.898,1.688l-7.669-7.669C16.785,10.424,17,9.74,17,9c0-2.206-1.794-4-4-4S9,6.794,9,9s1.794,4,4,4
|
s-3.542,0.634-4.898,1.688l-7.669-7.669C16.785,10.424,17,9.74,17,9c0-2.206-1.794-4-4-4S9,6.794,9,9s1.794,4,4,4
|
||||||
c0.74,0,1.424-0.215,2.019-0.567l7.669,7.669C21.634,21.458,21,23.154,21,25s0.634,3.542,1.688,4.897L10.024,42.562
|
c0.74,0,1.424-0.215,2.019-0.567l7.669,7.669C21.634,21.458,21,23.154,21,25s0.634,3.542,1.688,4.897L10.024,42.562
|
||||||
C8.958,41.595,7.549,41,6,41c-3.309,0-6,2.691-6,6s2.691,6,6,6s6-2.691,6-6c0-1.035-0.263-2.009-0.726-2.86l12.829-12.829
|
C8.958,41.595,7.549,41,6,41c-3.309,0-6,2.691-6,6s2.691,6,6,6s6-2.691,6-6c0-1.035-0.263-2.009-0.726-2.86l12.829-12.829
|
||||||
|
@ -65,13 +76,15 @@ export default ((opts?: GraphOptions) => {
|
||||||
C46.042,11.405,47.451,12,49,12c3.309,0,6-2.691,6-6S52.309,0,49,0z M11,9c0-1.103,0.897-2,2-2s2,0.897,2,2s-0.897,2-2,2
|
C46.042,11.405,47.451,12,49,12c3.309,0,6-2.691,6-6S52.309,0,49,0z M11,9c0-1.103,0.897-2,2-2s2,0.897,2,2s-0.897,2-2,2
|
||||||
S11,10.103,11,9z M6,51c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S8.206,51,6,51z M33,49c0,2.206-1.794,4-4,4s-4-1.794-4-4
|
S11,10.103,11,9z M6,51c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S8.206,51,6,51z M33,49c0,2.206-1.794,4-4,4s-4-1.794-4-4
|
||||||
s1.794-4,4-4S33,46.794,33,49z M29,31c-3.309,0-6-2.691-6-6s2.691-6,6-6s6,2.691,6,6S32.309,31,29,31z M47,41c0,1.103-0.897,2-2,2
|
s1.794-4,4-4S33,46.794,33,49z M29,31c-3.309,0-6-2.691-6-6s2.691-6,6-6s6,2.691,6,6S32.309,31,29,31z M47,41c0,1.103-0.897,2-2,2
|
||||||
s-2-0.897-2-2s0.897-2,2-2S47,39.897,47,41z M49,10c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S51.206,10,49,10z"/>
|
s-2-0.897-2-2s0.897-2,2-2S47,39.897,47,41z M49,10c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S51.206,10,49,10z"
|
||||||
</svg>
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div id="global-graph-outer">
|
||||||
|
<div id="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="global-graph-outer">
|
)
|
||||||
<div id="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Graph.css = style
|
Graph.css = style
|
||||||
|
|
|
@ -12,23 +12,29 @@ export default (() => {
|
||||||
const iconPath = baseDir + "/static/icon.png"
|
const iconPath = baseDir + "/static/icon.png"
|
||||||
const ogImagePath = baseDir + "/static/og-image.png"
|
const ogImagePath = baseDir + "/static/og-image.png"
|
||||||
|
|
||||||
return <head>
|
return (
|
||||||
<title>{title}</title>
|
<head>
|
||||||
<meta charSet="utf-8" />
|
<title>{title}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta charSet="utf-8" />
|
||||||
<meta property="og:title" content={title} />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta property="og:description" content={title} />
|
<meta property="og:title" content={title} />
|
||||||
<meta property="og:image" content={ogImagePath} />
|
<meta property="og:description" content={title} />
|
||||||
<meta property="og:width" content="1200" />
|
<meta property="og:image" content={ogImagePath} />
|
||||||
<meta property="og:height" content="675" />
|
<meta property="og:width" content="1200" />
|
||||||
<link rel="icon" href={iconPath} />
|
<meta property="og:height" content="675" />
|
||||||
<meta name="description" content={description} />
|
<link rel="icon" href={iconPath} />
|
||||||
<meta name="generator" content="Quartz" />
|
<meta name="description" content={description} />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
<meta name="generator" content="Quartz" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com"/>
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
{css.map(href => <link key={href} href={href} rel="stylesheet" type="text/css" spa-preserve />)}
|
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||||
{js.filter(resource => resource.loadTime === "beforeDOMReady").map(res => JSResourceToScriptElement(res, true))}
|
{css.map((href) => (
|
||||||
</head>
|
<link key={href} href={href} rel="stylesheet" type="text/css" spa-preserve />
|
||||||
|
))}
|
||||||
|
{js
|
||||||
|
.filter((resource) => resource.loadTime === "beforeDOMReady")
|
||||||
|
.map((res) => JSResourceToScriptElement(res, true))}
|
||||||
|
</head>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Head
|
return Head
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
|
|
||||||
function Header({ children }: QuartzComponentProps) {
|
function Header({ children }: QuartzComponentProps) {
|
||||||
return (children.length > 0) ? <header>
|
return children.length > 0 ? <header>{children}</header> : null
|
||||||
{children}
|
|
||||||
</header> : null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Header.css = `
|
Header.css = `
|
||||||
|
|
|
@ -17,32 +17,51 @@ function byDateAndAlphabetical(f1: QuartzPluginData, f2: QuartzPluginData): numb
|
||||||
// otherwise, sort lexographically by title
|
// otherwise, sort lexographically by title
|
||||||
const f1Title = f1.frontmatter?.title.toLowerCase() ?? ""
|
const f1Title = f1.frontmatter?.title.toLowerCase() ?? ""
|
||||||
const f2Title = f2.frontmatter?.title.toLowerCase() ?? ""
|
const f2Title = f2.frontmatter?.title.toLowerCase() ?? ""
|
||||||
return f1Title.localeCompare(f2Title)
|
return f1Title.localeCompare(f2Title)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PageList({ fileData, allFiles }: QuartzComponentProps) {
|
export function PageList({ fileData, allFiles }: QuartzComponentProps) {
|
||||||
const slug = canonicalizeServer(fileData.slug!)
|
const slug = canonicalizeServer(fileData.slug!)
|
||||||
return <ul class="section-ul">
|
return (
|
||||||
{allFiles.sort(byDateAndAlphabetical).map(page => {
|
<ul class="section-ul">
|
||||||
const title = page.frontmatter?.title
|
{allFiles.sort(byDateAndAlphabetical).map((page) => {
|
||||||
const pageSlug = canonicalizeServer(page.slug!)
|
const title = page.frontmatter?.title
|
||||||
const tags = page.frontmatter?.tags ?? []
|
const pageSlug = canonicalizeServer(page.slug!)
|
||||||
|
const tags = page.frontmatter?.tags ?? []
|
||||||
|
|
||||||
return <li class="section-li">
|
return (
|
||||||
<div class="section">
|
<li class="section-li">
|
||||||
{page.dates && <p class="meta">
|
<div class="section">
|
||||||
<Date date={page.dates.modified} />
|
{page.dates && (
|
||||||
</p>}
|
<p class="meta">
|
||||||
<div class="desc">
|
<Date date={page.dates.modified} />
|
||||||
<h3><a href={resolveRelative(slug, pageSlug)} class="internal">{title}</a></h3>
|
</p>
|
||||||
</div>
|
)}
|
||||||
<ul class="tags">
|
<div class="desc">
|
||||||
{tags.map(tag => <li><a class="internal" href={resolveRelative(slug, `tags/${tag}` as CanonicalSlug)}>#{tag}</a></li>)}
|
<h3>
|
||||||
</ul>
|
<a href={resolveRelative(slug, pageSlug)} class="internal">
|
||||||
</div>
|
{title}
|
||||||
</li>
|
</a>
|
||||||
})}
|
</h3>
|
||||||
</ul>
|
</div>
|
||||||
|
<ul class="tags">
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="internal"
|
||||||
|
href={resolveRelative(slug, `tags/${tag}` as CanonicalSlug)}
|
||||||
|
>
|
||||||
|
#{tag}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
PageList.css = `
|
PageList.css = `
|
||||||
|
|
|
@ -5,7 +5,11 @@ function PageTitle({ fileData, cfg }: QuartzComponentProps) {
|
||||||
const title = cfg?.pageTitle ?? "Untitled Quartz"
|
const title = cfg?.pageTitle ?? "Untitled Quartz"
|
||||||
const slug = canonicalizeServer(fileData.slug!)
|
const slug = canonicalizeServer(fileData.slug!)
|
||||||
const baseDir = pathToRoot(slug)
|
const baseDir = pathToRoot(slug)
|
||||||
return <h1 class="page-title"><a href={baseDir}>{title}</a></h1>
|
return (
|
||||||
|
<h1 class="page-title">
|
||||||
|
<a href={baseDir}>{title}</a>
|
||||||
|
</h1>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
PageTitle.css = `
|
PageTitle.css = `
|
||||||
|
|
|
@ -5,7 +5,11 @@ function ReadingTime({ fileData }: QuartzComponentProps) {
|
||||||
const text = fileData.text
|
const text = fileData.text
|
||||||
if (text) {
|
if (text) {
|
||||||
const { text: timeTaken, words } = readingTime(text)
|
const { text: timeTaken, words } = readingTime(text)
|
||||||
return <p class="reading-time">{words} words, {timeTaken}</p>
|
return (
|
||||||
|
<p class="reading-time">
|
||||||
|
{words} words, {timeTaken}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,27 +5,41 @@ import script from "./scripts/search.inline"
|
||||||
|
|
||||||
export default (() => {
|
export default (() => {
|
||||||
function Search() {
|
function Search() {
|
||||||
return <div class="search">
|
return (
|
||||||
<div id="search-icon">
|
<div class="search">
|
||||||
<p>Search</p>
|
<div id="search-icon">
|
||||||
<div></div>
|
<p>Search</p>
|
||||||
<svg tabIndex={0} aria-labelledby="title desc" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.9 19.7">
|
<div></div>
|
||||||
<title id="title">Search</title>
|
<svg
|
||||||
<desc id="desc">Search</desc>
|
tabIndex={0}
|
||||||
<g class="search-path" fill="none">
|
aria-labelledby="title desc"
|
||||||
<path stroke-linecap="square" d="M18.5 18.3l-5.4-5.4" />
|
role="img"
|
||||||
<circle cx="8" cy="8" r="7" />
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</g>
|
viewBox="0 0 19.9 19.7"
|
||||||
</svg>
|
>
|
||||||
</div>
|
<title id="title">Search</title>
|
||||||
<div id="search-container">
|
<desc id="desc">Search</desc>
|
||||||
<div id="search-space">
|
<g class="search-path" fill="none">
|
||||||
<input autocomplete="off" id="search-bar" name="search" type="text" aria-label="Search for something" placeholder="Search for something" />
|
<path stroke-linecap="square" d="M18.5 18.3l-5.4-5.4" />
|
||||||
<div id="results-container">
|
<circle cx="8" cy="8" r="7" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div id="search-container">
|
||||||
|
<div id="search-space">
|
||||||
|
<input
|
||||||
|
autocomplete="off"
|
||||||
|
id="search-bar"
|
||||||
|
name="search"
|
||||||
|
type="text"
|
||||||
|
aria-label="Search for something"
|
||||||
|
placeholder="Search for something"
|
||||||
|
/>
|
||||||
|
<div id="results-container"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Search.afterDOMLoaded = script
|
Search.afterDOMLoaded = script
|
||||||
|
|
|
@ -6,11 +6,11 @@ import modernStyle from "./styles/toc.scss"
|
||||||
import script from "./scripts/toc.inline"
|
import script from "./scripts/toc.inline"
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
layout: 'modern' | 'legacy'
|
layout: "modern" | "legacy"
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions: Options = {
|
const defaultOptions: Options = {
|
||||||
layout: 'modern'
|
layout: "modern",
|
||||||
}
|
}
|
||||||
|
|
||||||
function TableOfContents({ fileData }: QuartzComponentProps) {
|
function TableOfContents({ fileData }: QuartzComponentProps) {
|
||||||
|
@ -18,21 +18,38 @@ function TableOfContents({ fileData }: QuartzComponentProps) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div class="desktop-only">
|
return (
|
||||||
<button type="button" id="toc">
|
<div class="desktop-only">
|
||||||
<h3>Table of Contents</h3>
|
<button type="button" id="toc">
|
||||||
<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">
|
<h3>Table of Contents</h3>
|
||||||
<polyline points="6 9 12 15 18 9"></polyline>
|
<svg
|
||||||
</svg>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</button>
|
width="24"
|
||||||
<div id="toc-content">
|
height="24"
|
||||||
<ul class="overflow">
|
viewBox="0 0 24 24"
|
||||||
{fileData.toc.map(tocEntry => <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
|
fill="none"
|
||||||
<a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>{tocEntry.text}</a>
|
stroke="currentColor"
|
||||||
</li>)}
|
stroke-width="2"
|
||||||
</ul>
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="fold"
|
||||||
|
>
|
||||||
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div id="toc-content">
|
||||||
|
<ul class="overflow">
|
||||||
|
{fileData.toc.map((tocEntry) => (
|
||||||
|
<li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
|
||||||
|
<a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>
|
||||||
|
{tocEntry.text}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
}
|
}
|
||||||
TableOfContents.css = modernStyle
|
TableOfContents.css = modernStyle
|
||||||
TableOfContents.afterDOMLoaded = script
|
TableOfContents.afterDOMLoaded = script
|
||||||
|
@ -42,16 +59,22 @@ function LegacyTableOfContents({ fileData }: QuartzComponentProps) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return <details id="toc" open>
|
return (
|
||||||
<summary>
|
<details id="toc" open>
|
||||||
<h3>Table of Contents</h3>
|
<summary>
|
||||||
</summary>
|
<h3>Table of Contents</h3>
|
||||||
<ul>
|
</summary>
|
||||||
{fileData.toc.map(tocEntry => <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
|
<ul>
|
||||||
<a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>{tocEntry.text}</a>
|
{fileData.toc.map((tocEntry) => (
|
||||||
</li>)}
|
<li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
|
||||||
</ul>
|
<a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>
|
||||||
</details>
|
{tocEntry.text}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
LegacyTableOfContents.css = legacyStyle
|
LegacyTableOfContents.css = legacyStyle
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,27 @@
|
||||||
import { canonicalizeServer, pathToRoot } from "../path"
|
import { canonicalizeServer, pathToRoot } from "../path"
|
||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import { slug as slugAnchor } from 'github-slugger'
|
import { slug as slugAnchor } from "github-slugger"
|
||||||
|
|
||||||
function TagList({ fileData }: QuartzComponentProps) {
|
function TagList({ fileData }: QuartzComponentProps) {
|
||||||
const tags = fileData.frontmatter?.tags
|
const tags = fileData.frontmatter?.tags
|
||||||
const slug = canonicalizeServer(fileData.slug!)
|
const slug = canonicalizeServer(fileData.slug!)
|
||||||
const baseDir = pathToRoot(slug)
|
const baseDir = pathToRoot(slug)
|
||||||
if (tags && tags.length > 0) {
|
if (tags && tags.length > 0) {
|
||||||
return <ul class="tags">{tags.map(tag => {
|
return (
|
||||||
const display = `#${tag}`
|
<ul class="tags">
|
||||||
const linkDest = baseDir + `/tags/${slugAnchor(tag)}`
|
{tags.map((tag) => {
|
||||||
return <li>
|
const display = `#${tag}`
|
||||||
<a href={linkDest} class="internal">{display}</a>
|
const linkDest = baseDir + `/tags/${slugAnchor(tag)}`
|
||||||
</li>
|
return (
|
||||||
})}</ul>
|
<li>
|
||||||
|
<a href={linkDest} class="internal">
|
||||||
|
{display}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import ReadingTime from "./ReadingTime"
|
||||||
import Spacer from "./Spacer"
|
import Spacer from "./Spacer"
|
||||||
import TableOfContents from "./TableOfContents"
|
import TableOfContents from "./TableOfContents"
|
||||||
import TagList from "./TagList"
|
import TagList from "./TagList"
|
||||||
import Graph from "./Graph"
|
import Graph from "./Graph"
|
||||||
import Backlinks from "./Backlinks"
|
import Backlinks from "./Backlinks"
|
||||||
import Search from "./Search"
|
import Search from "./Search"
|
||||||
import Footer from "./Footer"
|
import Footer from "./Footer"
|
||||||
|
@ -33,5 +33,5 @@ export {
|
||||||
Search,
|
Search,
|
||||||
Footer,
|
Footer,
|
||||||
DesktopOnly,
|
DesktopOnly,
|
||||||
MobileOnly
|
MobileOnly,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
||||||
import { Fragment, jsx, jsxs } from 'preact/jsx-runtime'
|
import { Fragment, jsx, jsxs } from "preact/jsx-runtime"
|
||||||
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
|
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
|
||||||
|
|
||||||
function Content({ tree }: QuartzComponentProps) {
|
function Content({ tree }: QuartzComponentProps) {
|
||||||
// @ts-ignore (preact makes it angry)
|
// @ts-ignore (preact makes it angry)
|
||||||
const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
|
const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: "html" })
|
||||||
return <article class="popover-hint">{content}</article>
|
return <article class="popover-hint">{content}</article>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
||||||
import { Fragment, jsx, jsxs } from 'preact/jsx-runtime'
|
import { Fragment, jsx, jsxs } from "preact/jsx-runtime"
|
||||||
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
|
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
import style from '../styles/listPage.scss'
|
import style from "../styles/listPage.scss"
|
||||||
import { PageList } from "../PageList"
|
import { PageList } from "../PageList"
|
||||||
import { canonicalizeServer } from "../../path"
|
import { canonicalizeServer } from "../../path"
|
||||||
|
|
||||||
function FolderContent(props: QuartzComponentProps) {
|
function FolderContent(props: QuartzComponentProps) {
|
||||||
const { tree, fileData, allFiles } = props
|
const { tree, fileData, allFiles } = props
|
||||||
const folderSlug = canonicalizeServer(fileData.slug!)
|
const folderSlug = canonicalizeServer(fileData.slug!)
|
||||||
const allPagesInFolder = allFiles.filter(file => {
|
const allPagesInFolder = allFiles.filter((file) => {
|
||||||
const fileSlug = file.slug ?? ""
|
const fileSlug = file.slug ?? ""
|
||||||
const prefixed = fileSlug.startsWith(folderSlug)
|
const prefixed = fileSlug.startsWith(folderSlug)
|
||||||
const folderParts = folderSlug.split(path.posix.sep)
|
const folderParts = folderSlug.split(path.posix.sep)
|
||||||
|
@ -21,18 +21,20 @@ function FolderContent(props: QuartzComponentProps) {
|
||||||
|
|
||||||
const listProps = {
|
const listProps = {
|
||||||
...props,
|
...props,
|
||||||
allFiles: allPagesInFolder
|
allFiles: allPagesInFolder,
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
|
const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: "html" })
|
||||||
return <div class="popover-hint">
|
return (
|
||||||
<article>{content}</article>
|
<div class="popover-hint">
|
||||||
<p>{allPagesInFolder.length} items under this folder.</p>
|
<article>{content}</article>
|
||||||
<div>
|
<p>{allPagesInFolder.length} items under this folder.</p>
|
||||||
<PageList {...listProps} />
|
<div>
|
||||||
|
<PageList {...listProps} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
FolderContent.css = style + PageList.css
|
FolderContent.css = style + PageList.css
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
||||||
import { Fragment, jsx, jsxs } from 'preact/jsx-runtime'
|
import { Fragment, jsx, jsxs } from "preact/jsx-runtime"
|
||||||
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
|
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
|
||||||
import style from '../styles/listPage.scss'
|
import style from "../styles/listPage.scss"
|
||||||
import { PageList } from "../PageList"
|
import { PageList } from "../PageList"
|
||||||
import { ServerSlug, canonicalizeServer } from "../../path"
|
import { ServerSlug, canonicalizeServer } from "../../path"
|
||||||
|
|
||||||
|
@ -11,21 +11,23 @@ function TagContent(props: QuartzComponentProps) {
|
||||||
|
|
||||||
if (slug?.startsWith("tags/")) {
|
if (slug?.startsWith("tags/")) {
|
||||||
const tag = canonicalizeServer(slug.slice("tags/".length) as ServerSlug)
|
const tag = canonicalizeServer(slug.slice("tags/".length) as ServerSlug)
|
||||||
const allPagesWithTag = allFiles.filter(file => (file.frontmatter?.tags ?? []).includes(tag))
|
const allPagesWithTag = allFiles.filter((file) => (file.frontmatter?.tags ?? []).includes(tag))
|
||||||
const listProps = {
|
const listProps = {
|
||||||
...props,
|
...props,
|
||||||
allFiles: allPagesWithTag
|
allFiles: allPagesWithTag,
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
|
const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: "html" })
|
||||||
return <div class="popover-hint">
|
return (
|
||||||
<article>{content}</article>
|
<div class="popover-hint">
|
||||||
<p>{allPagesWithTag.length} items with this tag.</p>
|
<article>{content}</article>
|
||||||
<div>
|
<p>{allPagesWithTag.length} items with this tag.</p>
|
||||||
<PageList {...listProps} />
|
<div>
|
||||||
|
<PageList {...listProps} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Component "TagContent" tried to render a non-tag page: ${slug}`)
|
throw new Error(`Component "TagContent" tried to render a non-tag page: ${slug}`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,24 @@
|
||||||
import { render } from "preact-render-to-string";
|
import { render } from "preact-render-to-string"
|
||||||
import { QuartzComponent, QuartzComponentProps } from "./types";
|
import { QuartzComponent, QuartzComponentProps } from "./types"
|
||||||
import HeaderConstructor from "./Header"
|
import HeaderConstructor from "./Header"
|
||||||
import BodyConstructor from "./Body"
|
import BodyConstructor from "./Body"
|
||||||
import { JSResourceToScriptElement, StaticResources } from "../resources";
|
import { JSResourceToScriptElement, StaticResources } from "../resources"
|
||||||
import { CanonicalSlug, pathToRoot } from "../path";
|
import { CanonicalSlug, pathToRoot } from "../path"
|
||||||
|
|
||||||
interface RenderComponents {
|
interface RenderComponents {
|
||||||
head: QuartzComponent
|
head: QuartzComponent
|
||||||
header: QuartzComponent[],
|
header: QuartzComponent[]
|
||||||
beforeBody: QuartzComponent[],
|
beforeBody: QuartzComponent[]
|
||||||
pageBody: QuartzComponent,
|
pageBody: QuartzComponent
|
||||||
left: QuartzComponent[],
|
left: QuartzComponent[]
|
||||||
right: QuartzComponent[],
|
right: QuartzComponent[]
|
||||||
footer: QuartzComponent,
|
footer: QuartzComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pageResources(slug: CanonicalSlug, staticResources: StaticResources): StaticResources {
|
export function pageResources(
|
||||||
|
slug: CanonicalSlug,
|
||||||
|
staticResources: StaticResources,
|
||||||
|
): StaticResources {
|
||||||
const baseDir = pathToRoot(slug)
|
const baseDir = pathToRoot(slug)
|
||||||
|
|
||||||
const contentIndexPath = baseDir + "/static/contentIndex.json"
|
const contentIndexPath = baseDir + "/static/contentIndex.json"
|
||||||
|
@ -25,52 +28,89 @@ export function pageResources(slug: CanonicalSlug, staticResources: StaticResour
|
||||||
css: [baseDir + "/index.css", ...staticResources.css],
|
css: [baseDir + "/index.css", ...staticResources.css],
|
||||||
js: [
|
js: [
|
||||||
{ src: baseDir + "/prescript.js", loadTime: "beforeDOMReady", contentType: "external" },
|
{ src: baseDir + "/prescript.js", loadTime: "beforeDOMReady", contentType: "external" },
|
||||||
{ loadTime: "beforeDOMReady", contentType: "inline", spaPreserve: true, script: contentIndexScript },
|
{
|
||||||
|
loadTime: "beforeDOMReady",
|
||||||
|
contentType: "inline",
|
||||||
|
spaPreserve: true,
|
||||||
|
script: contentIndexScript,
|
||||||
|
},
|
||||||
...staticResources.js,
|
...staticResources.js,
|
||||||
{ src: baseDir + "/postscript.js", loadTime: "afterDOMReady", moduleType: 'module', contentType: "external" }
|
{
|
||||||
]
|
src: baseDir + "/postscript.js",
|
||||||
|
loadTime: "afterDOMReady",
|
||||||
|
moduleType: "module",
|
||||||
|
contentType: "external",
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderPage(slug: CanonicalSlug, componentData: QuartzComponentProps, components: RenderComponents, pageResources: StaticResources): string {
|
export function renderPage(
|
||||||
const { head: Head, header, beforeBody, pageBody: Content, left, right, footer: Footer } = components
|
slug: CanonicalSlug,
|
||||||
|
componentData: QuartzComponentProps,
|
||||||
|
components: RenderComponents,
|
||||||
|
pageResources: StaticResources,
|
||||||
|
): string {
|
||||||
|
const {
|
||||||
|
head: Head,
|
||||||
|
header,
|
||||||
|
beforeBody,
|
||||||
|
pageBody: Content,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
footer: Footer,
|
||||||
|
} = components
|
||||||
const Header = HeaderConstructor()
|
const Header = HeaderConstructor()
|
||||||
const Body = BodyConstructor()
|
const Body = BodyConstructor()
|
||||||
|
|
||||||
const LeftComponent =
|
const LeftComponent = (
|
||||||
<div class="left sidebar">
|
<div class="left sidebar">
|
||||||
{left.map(BodyComponent => <BodyComponent {...componentData} />)}
|
{left.map((BodyComponent) => (
|
||||||
|
<BodyComponent {...componentData} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
const RightComponent =
|
const RightComponent = (
|
||||||
<div class="right sidebar">
|
<div class="right sidebar">
|
||||||
{right.map(BodyComponent => <BodyComponent {...componentData} />)}
|
{right.map((BodyComponent) => (
|
||||||
|
<BodyComponent {...componentData} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
const doc = <html>
|
const doc = (
|
||||||
<Head {...componentData} />
|
<html>
|
||||||
<body data-slug={slug}>
|
<Head {...componentData} />
|
||||||
<div id="quartz-root" class="page">
|
<body data-slug={slug}>
|
||||||
<Body {...componentData}>
|
<div id="quartz-root" class="page">
|
||||||
{LeftComponent}
|
<Body {...componentData}>
|
||||||
<div class="center">
|
{LeftComponent}
|
||||||
<div class="page-header">
|
<div class="center">
|
||||||
<Header {...componentData} >
|
<div class="page-header">
|
||||||
{header.map(HeaderComponent => <HeaderComponent {...componentData} />)}
|
<Header {...componentData}>
|
||||||
</Header>
|
{header.map((HeaderComponent) => (
|
||||||
<div class="popover-hint">
|
<HeaderComponent {...componentData} />
|
||||||
{beforeBody.map(BodyComponent => <BodyComponent {...componentData} />)}
|
))}
|
||||||
|
</Header>
|
||||||
|
<div class="popover-hint">
|
||||||
|
{beforeBody.map((BodyComponent) => (
|
||||||
|
<BodyComponent {...componentData} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Content {...componentData} />
|
||||||
</div>
|
</div>
|
||||||
<Content {...componentData} />
|
{RightComponent}
|
||||||
</div>
|
</Body>
|
||||||
{RightComponent}
|
<Footer {...componentData} />
|
||||||
</Body>
|
</div>
|
||||||
<Footer {...componentData} />
|
</body>
|
||||||
</div>
|
{pageResources.js
|
||||||
</body>
|
.filter((resource) => resource.loadTime === "afterDOMReady")
|
||||||
{pageResources.js.filter(resource => resource.loadTime === "afterDOMReady").map(res => JSResourceToScriptElement(res))}
|
.map((res) => JSResourceToScriptElement(res))}
|
||||||
</html>
|
</html>
|
||||||
|
)
|
||||||
|
|
||||||
return "<!DOCTYPE html>\n" + render(doc)
|
return "<!DOCTYPE html>\n" + render(doc)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,9 @@ function toggleCallout(this: HTMLElement) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupCallout() {
|
function setupCallout() {
|
||||||
const collapsible = document.getElementsByClassName(`callout is-collapsible`) as HTMLCollectionOf<HTMLElement>
|
const collapsible = document.getElementsByClassName(
|
||||||
|
`callout is-collapsible`,
|
||||||
|
) as HTMLCollectionOf<HTMLElement>
|
||||||
for (const div of collapsible) {
|
for (const div of collapsible) {
|
||||||
const title = div.firstElementChild
|
const title = div.firstElementChild
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,23 @@
|
||||||
const userPref = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'
|
const userPref = window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark"
|
||||||
const currentTheme = localStorage.getItem('theme') ?? userPref
|
const currentTheme = localStorage.getItem("theme") ?? userPref
|
||||||
document.documentElement.setAttribute('saved-theme', currentTheme)
|
document.documentElement.setAttribute("saved-theme", currentTheme)
|
||||||
|
|
||||||
document.addEventListener("nav", () => {
|
document.addEventListener("nav", () => {
|
||||||
const switchTheme = (e: any) => {
|
const switchTheme = (e: any) => {
|
||||||
if (e.target.checked) {
|
if (e.target.checked) {
|
||||||
document.documentElement.setAttribute('saved-theme', 'dark')
|
document.documentElement.setAttribute("saved-theme", "dark")
|
||||||
localStorage.setItem('theme', 'dark')
|
localStorage.setItem("theme", "dark")
|
||||||
}
|
} else {
|
||||||
else {
|
document.documentElement.setAttribute("saved-theme", "light")
|
||||||
document.documentElement.setAttribute('saved-theme', 'light')
|
localStorage.setItem("theme", "light")
|
||||||
localStorage.setItem('theme', 'light')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.removeEventListener("change", switchTheme)
|
||||||
toggleSwitch.addEventListener('change', switchTheme)
|
toggleSwitch.addEventListener("change", switchTheme)
|
||||||
if (currentTheme === 'dark') {
|
if (currentTheme === "dark") {
|
||||||
toggleSwitch.checked = true
|
toggleSwitch.checked = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import { ContentDetails } from "../../plugins/emitters/contentIndex"
|
import { ContentDetails } from "../../plugins/emitters/contentIndex"
|
||||||
import * as d3 from 'd3'
|
import * as d3 from "d3"
|
||||||
import { registerEscapeHandler, removeAllChildren } from "./util"
|
import { registerEscapeHandler, removeAllChildren } from "./util"
|
||||||
import { CanonicalSlug, getCanonicalSlug, getClientSlug, resolveRelative } from "../../path"
|
import { CanonicalSlug, getCanonicalSlug, getClientSlug, resolveRelative } from "../../path"
|
||||||
|
|
||||||
type NodeData = {
|
type NodeData = {
|
||||||
id: CanonicalSlug,
|
id: CanonicalSlug
|
||||||
text: string,
|
text: string
|
||||||
tags: string[]
|
tags: string[]
|
||||||
} & d3.SimulationNodeDatum
|
} & d3.SimulationNodeDatum
|
||||||
|
|
||||||
type LinkData = {
|
type LinkData = {
|
||||||
source: CanonicalSlug,
|
source: CanonicalSlug
|
||||||
target: CanonicalSlug
|
target: CanonicalSlug
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
|
||||||
centerForce,
|
centerForce,
|
||||||
linkDistance,
|
linkDistance,
|
||||||
fontSize,
|
fontSize,
|
||||||
opacityScale
|
opacityScale,
|
||||||
} = JSON.parse(graph.dataset["cfg"]!)
|
} = JSON.parse(graph.dataset["cfg"]!)
|
||||||
|
|
||||||
const data = await fetchData
|
const data = await fetchData
|
||||||
|
@ -66,18 +66,22 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
|
||||||
wl.push("__SENTINEL")
|
wl.push("__SENTINEL")
|
||||||
} else {
|
} else {
|
||||||
neighbourhood.add(cur)
|
neighbourhood.add(cur)
|
||||||
const outgoing = links.filter(l => l.source === cur)
|
const outgoing = links.filter((l) => l.source === cur)
|
||||||
const incoming = links.filter(l => l.target === cur)
|
const incoming = links.filter((l) => l.target === cur)
|
||||||
wl.push(...outgoing.map((l) => l.target), ...incoming.map((l) => l.source))
|
wl.push(...outgoing.map((l) => l.target), ...incoming.map((l) => l.source))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Object.keys(data).forEach(id => neighbourhood.add(id as CanonicalSlug))
|
Object.keys(data).forEach((id) => neighbourhood.add(id as CanonicalSlug))
|
||||||
}
|
}
|
||||||
|
|
||||||
const graphData: { nodes: NodeData[], links: LinkData[] } = {
|
const graphData: { nodes: NodeData[]; links: LinkData[] } = {
|
||||||
nodes: [...neighbourhood].map(url => ({ id: url, text: data[url]?.title ?? url, tags: data[url]?.tags ?? [] })),
|
nodes: [...neighbourhood].map((url) => ({
|
||||||
links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target))
|
id: url,
|
||||||
|
text: data[url]?.title ?? url,
|
||||||
|
tags: data[url]?.tags ?? [],
|
||||||
|
})),
|
||||||
|
links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)),
|
||||||
}
|
}
|
||||||
|
|
||||||
const simulation: d3.Simulation<NodeData, LinkData> = d3
|
const simulation: d3.Simulation<NodeData, LinkData> = d3
|
||||||
|
@ -96,11 +100,11 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
|
||||||
const width = graph.offsetWidth
|
const width = graph.offsetWidth
|
||||||
|
|
||||||
const svg = d3
|
const svg = d3
|
||||||
.select<HTMLElement, NodeData>('#' + container)
|
.select<HTMLElement, NodeData>("#" + container)
|
||||||
.append("svg")
|
.append("svg")
|
||||||
.attr("width", width)
|
.attr("width", width)
|
||||||
.attr("height", height)
|
.attr("height", height)
|
||||||
.attr('viewBox', [-width / 2 / scale, -height / 2 / scale, width / scale, height / scale])
|
.attr("viewBox", [-width / 2 / scale, -height / 2 / scale, width / scale, height / scale])
|
||||||
|
|
||||||
// draw links between nodes
|
// draw links between nodes
|
||||||
const link = svg
|
const link = svg
|
||||||
|
@ -145,7 +149,7 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
|
||||||
d.fy = null
|
d.fy = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const noop = () => { }
|
const noop = () => {}
|
||||||
return d3
|
return d3
|
||||||
.drag<Element, NodeData>()
|
.drag<Element, NodeData>()
|
||||||
.on("start", enableDrag ? dragstarted : noop)
|
.on("start", enableDrag ? dragstarted : noop)
|
||||||
|
@ -170,9 +174,11 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
|
||||||
const targ = resolveRelative(slug, d.id)
|
const targ = resolveRelative(slug, d.id)
|
||||||
window.spaNavigate(new URL(targ, getClientSlug(window)))
|
window.spaNavigate(new URL(targ, getClientSlug(window)))
|
||||||
})
|
})
|
||||||
.on("mouseover", function(_, d) {
|
.on("mouseover", function (_, d) {
|
||||||
const neighbours: CanonicalSlug[] = data[slug].links ?? []
|
const neighbours: CanonicalSlug[] = data[slug].links ?? []
|
||||||
const neighbourNodes = d3.selectAll<HTMLElement, NodeData>(".node").filter((d) => neighbours.includes(d.id))
|
const neighbourNodes = d3
|
||||||
|
.selectAll<HTMLElement, NodeData>(".node")
|
||||||
|
.filter((d) => neighbours.includes(d.id))
|
||||||
console.log(neighbourNodes)
|
console.log(neighbourNodes)
|
||||||
const currentId = d.id
|
const currentId = d.id
|
||||||
const linkNodes = d3
|
const linkNodes = d3
|
||||||
|
@ -183,12 +189,7 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
|
||||||
neighbourNodes.transition().duration(200).attr("fill", color)
|
neighbourNodes.transition().duration(200).attr("fill", color)
|
||||||
|
|
||||||
// highlight links
|
// highlight links
|
||||||
linkNodes
|
linkNodes.transition().duration(200).attr("stroke", "var(--gray)").attr("stroke-width", 1)
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.attr("stroke", "var(--gray)")
|
|
||||||
.attr("stroke-width", 1)
|
|
||||||
|
|
||||||
|
|
||||||
const bigFont = fontSize * 1.5
|
const bigFont = fontSize * 1.5
|
||||||
|
|
||||||
|
@ -199,11 +200,11 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
|
||||||
.select("text")
|
.select("text")
|
||||||
.transition()
|
.transition()
|
||||||
.duration(200)
|
.duration(200)
|
||||||
.attr('opacityOld', d3.select(parent).select('text').style("opacity"))
|
.attr("opacityOld", d3.select(parent).select("text").style("opacity"))
|
||||||
.style('opacity', 1)
|
.style("opacity", 1)
|
||||||
.style('font-size', bigFont + 'em')
|
.style("font-size", bigFont + "em")
|
||||||
})
|
})
|
||||||
.on("mouseleave", function(_, d) {
|
.on("mouseleave", function (_, d) {
|
||||||
const currentId = d.id
|
const currentId = d.id
|
||||||
const linkNodes = d3
|
const linkNodes = d3
|
||||||
.selectAll(".link")
|
.selectAll(".link")
|
||||||
|
@ -216,8 +217,8 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
|
||||||
.select("text")
|
.select("text")
|
||||||
.transition()
|
.transition()
|
||||||
.duration(200)
|
.duration(200)
|
||||||
.style('opacity', d3.select(parent).select('text').attr("opacityOld"))
|
.style("opacity", d3.select(parent).select("text").attr("opacityOld"))
|
||||||
.style('font-size', fontSize + 'em')
|
.style("font-size", fontSize + "em")
|
||||||
})
|
})
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
.call(drag(simulation))
|
.call(drag(simulation))
|
||||||
|
@ -228,10 +229,12 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
|
||||||
.attr("dx", 0)
|
.attr("dx", 0)
|
||||||
.attr("dy", (d) => -nodeRadius(d) + "px")
|
.attr("dy", (d) => -nodeRadius(d) + "px")
|
||||||
.attr("text-anchor", "middle")
|
.attr("text-anchor", "middle")
|
||||||
.text((d) => data[d.id]?.title || (d.id.charAt(1).toUpperCase() + d.id.slice(2)).replace("-", " "))
|
.text(
|
||||||
.style('opacity', (opacityScale - 1) / 3.75)
|
(d) => data[d.id]?.title || (d.id.charAt(1).toUpperCase() + d.id.slice(2)).replace("-", " "),
|
||||||
|
)
|
||||||
|
.style("opacity", (opacityScale - 1) / 3.75)
|
||||||
.style("pointer-events", "none")
|
.style("pointer-events", "none")
|
||||||
.style('font-size', fontSize + 'em')
|
.style("font-size", fontSize + "em")
|
||||||
.raise()
|
.raise()
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
.call(drag(simulation))
|
.call(drag(simulation))
|
||||||
|
@ -249,7 +252,7 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
|
||||||
.on("zoom", ({ transform }) => {
|
.on("zoom", ({ transform }) => {
|
||||||
link.attr("transform", transform)
|
link.attr("transform", transform)
|
||||||
node.attr("transform", transform)
|
node.attr("transform", transform)
|
||||||
const scale = transform.k * opacityScale;
|
const scale = transform.k * opacityScale
|
||||||
const scaledOpacity = Math.max((scale - 1) / 3.75, 0)
|
const scaledOpacity = Math.max((scale - 1) / 3.75, 0)
|
||||||
labels.attr("transform", transform).style("opacity", scaledOpacity)
|
labels.attr("transform", transform).style("opacity", scaledOpacity)
|
||||||
}),
|
}),
|
||||||
|
@ -263,17 +266,13 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
|
||||||
.attr("y1", (d: any) => d.source.y)
|
.attr("y1", (d: any) => d.source.y)
|
||||||
.attr("x2", (d: any) => d.target.x)
|
.attr("x2", (d: any) => d.target.x)
|
||||||
.attr("y2", (d: any) => d.target.y)
|
.attr("y2", (d: any) => d.target.y)
|
||||||
node
|
node.attr("cx", (d: any) => d.x).attr("cy", (d: any) => d.y)
|
||||||
.attr("cx", (d: any) => d.x)
|
labels.attr("x", (d: any) => d.x).attr("y", (d: any) => d.y)
|
||||||
.attr("cy", (d: any) => d.y)
|
|
||||||
labels
|
|
||||||
.attr("x", (d: any) => d.x)
|
|
||||||
.attr("y", (d: any) => d.y)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGlobalGraph() {
|
function renderGlobalGraph() {
|
||||||
const slug = getCanonicalSlug(window)
|
const slug = getCanonicalSlug(window)
|
||||||
const container = document.getElementById("global-graph-outer")
|
const container = document.getElementById("global-graph-outer")
|
||||||
const sidebar = container?.closest(".sidebar") as HTMLElement
|
const sidebar = container?.closest(".sidebar") as HTMLElement
|
||||||
container?.classList.add("active")
|
container?.classList.add("active")
|
||||||
|
@ -305,4 +304,3 @@ document.addEventListener("nav", async (e: unknown) => {
|
||||||
containerIcon?.removeEventListener("click", renderGlobalGraph)
|
containerIcon?.removeEventListener("click", renderGlobalGraph)
|
||||||
containerIcon?.addEventListener("click", renderGlobalGraph)
|
containerIcon?.addEventListener("click", renderGlobalGraph)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import Plausible from 'plausible-tracker'
|
import Plausible from "plausible-tracker"
|
||||||
const { trackPageview } = Plausible()
|
const { trackPageview } = Plausible()
|
||||||
document.addEventListener("nav", () => trackPageview())
|
document.addEventListener("nav", () => trackPageview())
|
||||||
|
|
|
@ -2,33 +2,25 @@ import { computePosition, flip, inline, shift } from "@floating-ui/dom"
|
||||||
|
|
||||||
// from micromorph/src/utils.ts
|
// from micromorph/src/utils.ts
|
||||||
// https://github.com/natemoo-re/micromorph/blob/main/src/utils.ts#L5
|
// https://github.com/natemoo-re/micromorph/blob/main/src/utils.ts#L5
|
||||||
export function normalizeRelativeURLs(
|
export function normalizeRelativeURLs(el: Element | Document, base: string | URL) {
|
||||||
el: Element | Document,
|
|
||||||
base: string | URL
|
|
||||||
) {
|
|
||||||
const update = (el: Element, attr: string, base: string | URL) => {
|
const update = (el: Element, attr: string, base: string | URL) => {
|
||||||
el.setAttribute(attr, new URL(el.getAttribute(attr)!, base).pathname)
|
el.setAttribute(attr, new URL(el.getAttribute(attr)!, base).pathname)
|
||||||
}
|
}
|
||||||
|
|
||||||
el.querySelectorAll('[href^="./"], [href^="../"]').forEach((item) =>
|
el.querySelectorAll('[href^="./"], [href^="../"]').forEach((item) => update(item, "href", base))
|
||||||
update(item, 'href', base)
|
|
||||||
)
|
|
||||||
|
|
||||||
el.querySelectorAll('[src^="./"], [src^="../"]').forEach((item) =>
|
el.querySelectorAll('[src^="./"], [src^="../"]').forEach((item) => update(item, "src", base))
|
||||||
update(item, 'src', base)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const p = new DOMParser()
|
const p = new DOMParser()
|
||||||
async function mouseEnterHandler(this: HTMLLinkElement, { clientX, clientY }: { clientX: number, clientY: number }) {
|
async function mouseEnterHandler(
|
||||||
|
this: HTMLLinkElement,
|
||||||
|
{ clientX, clientY }: { clientX: number; clientY: number },
|
||||||
|
) {
|
||||||
const link = this
|
const link = this
|
||||||
async function setPosition(popoverElement: HTMLElement) {
|
async function setPosition(popoverElement: HTMLElement) {
|
||||||
const { x, y } = await computePosition(link, popoverElement, {
|
const { x, y } = await computePosition(link, popoverElement, {
|
||||||
middleware: [
|
middleware: [inline({ x: clientX, y: clientY }), shift(), flip()],
|
||||||
inline({ x: clientX, y: clientY }),
|
|
||||||
shift(),
|
|
||||||
flip()
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
Object.assign(popoverElement.style, {
|
Object.assign(popoverElement.style, {
|
||||||
left: `${x}px`,
|
left: `${x}px`,
|
||||||
|
@ -37,7 +29,7 @@ async function mouseEnterHandler(this: HTMLLinkElement, { clientX, clientY }: {
|
||||||
}
|
}
|
||||||
|
|
||||||
// dont refetch if there's already a popover
|
// dont refetch if there's already a popover
|
||||||
if ([...link.children].some(child => child.classList.contains("popover"))) {
|
if ([...link.children].some((child) => child.classList.contains("popover"))) {
|
||||||
return setPosition(link.lastChild as HTMLElement)
|
return setPosition(link.lastChild as HTMLElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +60,7 @@ async function mouseEnterHandler(this: HTMLLinkElement, { clientX, clientY }: {
|
||||||
const popoverInner = document.createElement("div")
|
const popoverInner = document.createElement("div")
|
||||||
popoverInner.classList.add("popover-inner")
|
popoverInner.classList.add("popover-inner")
|
||||||
popoverElement.appendChild(popoverInner)
|
popoverElement.appendChild(popoverInner)
|
||||||
elts.forEach(elt => popoverInner.appendChild(elt))
|
elts.forEach((elt) => popoverInner.appendChild(elt))
|
||||||
|
|
||||||
setPosition(popoverElement)
|
setPosition(popoverElement)
|
||||||
link.appendChild(popoverElement)
|
link.appendChild(popoverElement)
|
||||||
|
@ -77,7 +69,7 @@ async function mouseEnterHandler(this: HTMLLinkElement, { clientX, clientY }: {
|
||||||
const heading = popoverInner.querySelector(hash) as HTMLElement | null
|
const heading = popoverInner.querySelector(hash) as HTMLElement | null
|
||||||
if (heading) {
|
if (heading) {
|
||||||
// leave ~12px of buffer when scrolling to a heading
|
// leave ~12px of buffer when scrolling to a heading
|
||||||
popoverInner.scroll({ top: heading.offsetTop - 12, behavior: 'instant' })
|
popoverInner.scroll({ top: heading.offsetTop - 12, behavior: "instant" })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,9 @@ import { registerEscapeHandler, removeAllChildren } from "./util"
|
||||||
import { CanonicalSlug, getClientSlug, resolveRelative } from "../../path"
|
import { CanonicalSlug, getClientSlug, resolveRelative } from "../../path"
|
||||||
|
|
||||||
interface Item {
|
interface Item {
|
||||||
slug: CanonicalSlug,
|
slug: CanonicalSlug
|
||||||
title: string,
|
title: string
|
||||||
content: string,
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
let index: Document<Item> | undefined = undefined
|
let index: Document<Item> | undefined = undefined
|
||||||
|
@ -15,15 +15,17 @@ const contextWindowWords = 30
|
||||||
const numSearchResults = 5
|
const numSearchResults = 5
|
||||||
function highlight(searchTerm: string, text: string, trim?: boolean) {
|
function highlight(searchTerm: string, text: string, trim?: boolean) {
|
||||||
// try to highlight longest tokens first
|
// try to highlight longest tokens first
|
||||||
const tokenizedTerms = searchTerm.split(/\s+/).filter(t => t !== "").sort((a, b) => b.length - a.length)
|
const tokenizedTerms = searchTerm
|
||||||
let tokenizedText = text
|
|
||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
.filter(t => t !== "")
|
.filter((t) => t !== "")
|
||||||
|
.sort((a, b) => b.length - a.length)
|
||||||
|
let tokenizedText = text.split(/\s+/).filter((t) => t !== "")
|
||||||
|
|
||||||
let startIndex = 0
|
let startIndex = 0
|
||||||
let endIndex = tokenizedText.length - 1
|
let endIndex = tokenizedText.length - 1
|
||||||
if (trim) {
|
if (trim) {
|
||||||
const includesCheck = (tok: string) => tokenizedTerms.some((term) => tok.toLowerCase().startsWith(term.toLowerCase()))
|
const includesCheck = (tok: string) =>
|
||||||
|
tokenizedTerms.some((term) => tok.toLowerCase().startsWith(term.toLowerCase()))
|
||||||
const occurencesIndices = tokenizedText.map(includesCheck)
|
const occurencesIndices = tokenizedText.map(includesCheck)
|
||||||
|
|
||||||
let bestSum = 0
|
let bestSum = 0
|
||||||
|
@ -42,19 +44,22 @@ function highlight(searchTerm: string, text: string, trim?: boolean) {
|
||||||
tokenizedText = tokenizedText.slice(startIndex, endIndex)
|
tokenizedText = tokenizedText.slice(startIndex, endIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
const slice = tokenizedText.map(tok => {
|
const slice = tokenizedText
|
||||||
// see if this tok is prefixed by any search terms
|
.map((tok) => {
|
||||||
for (const searchTok of tokenizedTerms) {
|
// see if this tok is prefixed by any search terms
|
||||||
if (tok.toLowerCase().includes(searchTok.toLowerCase())) {
|
for (const searchTok of tokenizedTerms) {
|
||||||
const regex = new RegExp(searchTok.toLowerCase(), "gi")
|
if (tok.toLowerCase().includes(searchTok.toLowerCase())) {
|
||||||
return tok.replace(regex, `<span class="highlight">$&</span>`)
|
const regex = new RegExp(searchTok.toLowerCase(), "gi")
|
||||||
|
return tok.replace(regex, `<span class="highlight">$&</span>`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return tok
|
||||||
return tok
|
})
|
||||||
})
|
|
||||||
.join(" ")
|
.join(" ")
|
||||||
|
|
||||||
return `${startIndex === 0 ? "" : "..."}${slice}${endIndex === tokenizedText.length - 1 ? "" : "..."}`
|
return `${startIndex === 0 ? "" : "..."}${slice}${
|
||||||
|
endIndex === tokenizedText.length - 1 ? "" : "..."
|
||||||
|
}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
|
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
|
||||||
|
@ -113,7 +118,7 @@ document.addEventListener("nav", async (e: unknown) => {
|
||||||
button.classList.add("result-card")
|
button.classList.add("result-card")
|
||||||
button.id = slug
|
button.id = slug
|
||||||
button.innerHTML = `<h3>${title}</h3><p>${content}</p>`
|
button.innerHTML = `<h3>${title}</h3><p>${content}</p>`
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener("click", () => {
|
||||||
const targ = resolveRelative(currentSlug, slug)
|
const targ = resolveRelative(currentSlug, slug)
|
||||||
window.spaNavigate(new URL(targ, getClientSlug(window)))
|
window.spaNavigate(new URL(targ, getClientSlug(window)))
|
||||||
})
|
})
|
||||||
|
@ -132,7 +137,6 @@ document.addEventListener("nav", async (e: unknown) => {
|
||||||
} else {
|
} else {
|
||||||
results.append(...finalResults.map(resultToHTML))
|
results.append(...finalResults.map(resultToHTML))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onType(e: HTMLElementEventMap["input"]) {
|
function onType(e: HTMLElementEventMap["input"]) {
|
||||||
|
@ -140,12 +144,12 @@ document.addEventListener("nav", async (e: unknown) => {
|
||||||
const searchResults = index?.search(term, numSearchResults) ?? []
|
const searchResults = index?.search(term, numSearchResults) ?? []
|
||||||
const getByField = (field: string): CanonicalSlug[] => {
|
const getByField = (field: string): CanonicalSlug[] => {
|
||||||
const results = searchResults.filter((x) => x.field === field)
|
const results = searchResults.filter((x) => x.field === field)
|
||||||
return results.length === 0 ? [] : [...results[0].result] as CanonicalSlug[]
|
return results.length === 0 ? [] : ([...results[0].result] as CanonicalSlug[])
|
||||||
}
|
}
|
||||||
|
|
||||||
// order titles ahead of content
|
// order titles ahead of content
|
||||||
const allIds: Set<CanonicalSlug> = new Set([...getByField("title"), ...getByField("content")])
|
const allIds: Set<CanonicalSlug> = new Set([...getByField("title"), ...getByField("content")])
|
||||||
const finalResults = [...allIds].map(id => formatForDisplay(term, id))
|
const finalResults = [...allIds].map((id) => formatForDisplay(term, id))
|
||||||
displayResults(finalResults)
|
displayResults(finalResults)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,7 +164,7 @@ document.addEventListener("nav", async (e: unknown) => {
|
||||||
if (!index) {
|
if (!index) {
|
||||||
index = new Document({
|
index = new Document({
|
||||||
cache: true,
|
cache: true,
|
||||||
charset: 'latin:extra',
|
charset: "latin:extra",
|
||||||
optimize: true,
|
optimize: true,
|
||||||
encode: encoder,
|
encode: encoder,
|
||||||
document: {
|
document: {
|
||||||
|
@ -174,7 +178,7 @@ document.addEventListener("nav", async (e: unknown) => {
|
||||||
field: "content",
|
field: "content",
|
||||||
tokenize: "reverse",
|
tokenize: "reverse",
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -182,7 +186,7 @@ document.addEventListener("nav", async (e: unknown) => {
|
||||||
await index.addAsync(slug, {
|
await index.addAsync(slug, {
|
||||||
slug: slug as CanonicalSlug,
|
slug: slug as CanonicalSlug,
|
||||||
title: fileData.title,
|
title: fileData.title,
|
||||||
content: fileData.content
|
content: fileData.content,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,9 @@ import { CanonicalSlug, RelativeURL, getCanonicalSlug } from "../../path"
|
||||||
// https://github.com/natemoo-re/micromorph
|
// https://github.com/natemoo-re/micromorph
|
||||||
|
|
||||||
const NODE_TYPE_ELEMENT = 1
|
const NODE_TYPE_ELEMENT = 1
|
||||||
let announcer = document.createElement('route-announcer')
|
let announcer = document.createElement("route-announcer")
|
||||||
const isElement = (target: EventTarget | null): target is Element => (target as Node)?.nodeType === NODE_TYPE_ELEMENT
|
const isElement = (target: EventTarget | null): target is Element =>
|
||||||
|
(target as Node)?.nodeType === NODE_TYPE_ELEMENT
|
||||||
const isLocalUrl = (href: string) => {
|
const isLocalUrl = (href: string) => {
|
||||||
try {
|
try {
|
||||||
const url = new URL(href)
|
const url = new URL(href)
|
||||||
|
@ -16,18 +17,18 @@ const isLocalUrl = (href: string) => {
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} catch (e) { }
|
} catch (e) {}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOpts = ({ target }: Event): { url: URL, scroll?: boolean } | undefined => {
|
const getOpts = ({ target }: Event): { url: URL; scroll?: boolean } | undefined => {
|
||||||
if (!isElement(target)) return
|
if (!isElement(target)) return
|
||||||
const a = target.closest("a")
|
const a = target.closest("a")
|
||||||
if (!a) return
|
if (!a) return
|
||||||
if ('routerIgnore' in a.dataset) return
|
if ("routerIgnore" in a.dataset) return
|
||||||
const { href } = a
|
const { href } = a
|
||||||
if (!isLocalUrl(href)) return
|
if (!isLocalUrl(href)) return
|
||||||
return { url: new URL(href), scroll: 'routerNoscroll' in a.dataset ? false : undefined }
|
return { url: new URL(href), scroll: "routerNoscroll" in a.dataset ? false : undefined }
|
||||||
}
|
}
|
||||||
|
|
||||||
function notifyNav(url: CanonicalSlug) {
|
function notifyNav(url: CanonicalSlug) {
|
||||||
|
@ -44,7 +45,7 @@ async function navigate(url: URL, isBack: boolean = false) {
|
||||||
window.location.assign(url)
|
window.location.assign(url)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!contents) return;
|
if (!contents) return
|
||||||
if (!isBack) {
|
if (!isBack) {
|
||||||
history.pushState({}, "", url)
|
history.pushState({}, "", url)
|
||||||
window.scrollTo({ top: 0 })
|
window.scrollTo({ top: 0 })
|
||||||
|
@ -54,22 +55,22 @@ async function navigate(url: URL, isBack: boolean = false) {
|
||||||
if (title) {
|
if (title) {
|
||||||
document.title = title
|
document.title = title
|
||||||
} else {
|
} else {
|
||||||
const h1 = document.querySelector('h1')
|
const h1 = document.querySelector("h1")
|
||||||
title = h1?.innerText ?? h1?.textContent ?? url.pathname
|
title = h1?.innerText ?? h1?.textContent ?? url.pathname
|
||||||
}
|
}
|
||||||
if (announcer.textContent !== title) {
|
if (announcer.textContent !== title) {
|
||||||
announcer.textContent = title
|
announcer.textContent = title
|
||||||
}
|
}
|
||||||
announcer.dataset.persist = ''
|
announcer.dataset.persist = ""
|
||||||
html.body.appendChild(announcer)
|
html.body.appendChild(announcer)
|
||||||
|
|
||||||
micromorph(document.body, html.body)
|
micromorph(document.body, html.body)
|
||||||
|
|
||||||
// now, patch head
|
// now, patch head
|
||||||
const elementsToRemove = document.head.querySelectorAll(':not([spa-preserve])')
|
const elementsToRemove = document.head.querySelectorAll(":not([spa-preserve])")
|
||||||
elementsToRemove.forEach(el => el.remove())
|
elementsToRemove.forEach((el) => el.remove())
|
||||||
const elementsToAdd = html.head.querySelectorAll(':not([spa-preserve])')
|
const elementsToAdd = html.head.querySelectorAll(":not([spa-preserve])")
|
||||||
elementsToAdd.forEach(el => document.head.appendChild(el))
|
elementsToAdd.forEach((el) => document.head.appendChild(el))
|
||||||
|
|
||||||
notifyNav(getCanonicalSlug(window))
|
notifyNav(getCanonicalSlug(window))
|
||||||
delete announcer.dataset.persist
|
delete announcer.dataset.persist
|
||||||
|
@ -101,7 +102,7 @@ function createRouter() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return new class Router {
|
return new (class Router {
|
||||||
go(pathname: RelativeURL) {
|
go(pathname: RelativeURL) {
|
||||||
const url = new URL(pathname, window.location.toString())
|
const url = new URL(pathname, window.location.toString())
|
||||||
return navigate(url, false)
|
return navigate(url, false)
|
||||||
|
@ -114,26 +115,30 @@ function createRouter() {
|
||||||
forward() {
|
forward() {
|
||||||
return window.history.forward()
|
return window.history.forward()
|
||||||
}
|
}
|
||||||
}
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
createRouter()
|
createRouter()
|
||||||
notifyNav(getCanonicalSlug(window))
|
notifyNav(getCanonicalSlug(window))
|
||||||
|
|
||||||
if (!customElements.get('route-announcer')) {
|
if (!customElements.get("route-announcer")) {
|
||||||
const attrs = {
|
const attrs = {
|
||||||
'aria-live': 'assertive',
|
"aria-live": "assertive",
|
||||||
'aria-atomic': 'true',
|
"aria-atomic": "true",
|
||||||
'style': 'position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px'
|
style:
|
||||||
|
"position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px",
|
||||||
}
|
}
|
||||||
customElements.define('route-announcer', class RouteAnnouncer extends HTMLElement {
|
customElements.define(
|
||||||
constructor() {
|
"route-announcer",
|
||||||
super()
|
class RouteAnnouncer extends HTMLElement {
|
||||||
}
|
constructor() {
|
||||||
connectedCallback() {
|
super()
|
||||||
for (const [key, value] of Object.entries(attrs)) {
|
|
||||||
this.setAttribute(key, value)
|
|
||||||
}
|
}
|
||||||
}
|
connectedCallback() {
|
||||||
})
|
for (const [key, value] of Object.entries(attrs)) {
|
||||||
|
this.setAttribute(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const bufferPx = 150
|
const bufferPx = 150
|
||||||
const observer = new IntersectionObserver(entries => {
|
const observer = new IntersectionObserver((entries) => {
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const slug = entry.target.id
|
const slug = entry.target.id
|
||||||
const tocEntryElement = document.querySelector(`a[data-for="${slug}"]`)
|
const tocEntryElement = document.querySelector(`a[data-for="${slug}"]`)
|
||||||
|
@ -38,5 +38,5 @@ document.addEventListener("nav", () => {
|
||||||
// update toc entry highlighting
|
// update toc entry highlighting
|
||||||
observer.disconnect()
|
observer.disconnect()
|
||||||
const headers = document.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]")
|
const headers = document.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]")
|
||||||
headers.forEach(header => observer.observe(header))
|
headers.forEach((header) => observer.observe(header))
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,7 +15,7 @@ export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb:
|
||||||
outsideContainer?.removeEventListener("click", click)
|
outsideContainer?.removeEventListener("click", click)
|
||||||
outsideContainer?.addEventListener("click", click)
|
outsideContainer?.addEventListener("click", click)
|
||||||
document.removeEventListener("keydown", esc)
|
document.removeEventListener("keydown", esc)
|
||||||
document.addEventListener('keydown', esc)
|
document.addEventListener("keydown", esc)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeAllChildren(node: HTMLElement) {
|
export function removeAllChildren(node: HTMLElement) {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
.graph {
|
.graph {
|
||||||
& > h3 {
|
& > h3 {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
margin: 0
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .graph-outer {
|
& > .graph-outer {
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
transition: background-color 0.5s ease;
|
transition: background-color 0.5s ease;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -52,7 +52,7 @@
|
||||||
|
|
||||||
& > #global-graph-container {
|
& > #global-graph-container {
|
||||||
border: 1px solid var(--lightgray);
|
border: 1px solid var(--lightgray);
|
||||||
background-color: var(--light);
|
background-color: var(--light);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
|
@ -12,7 +12,7 @@ details#toc {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& ul {
|
& ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0.5rem 1.25rem;
|
margin: 0.5rem 1.25rem;
|
||||||
|
|
|
@ -25,7 +25,7 @@ li.section-li {
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .desc > h3 > a {
|
& > .desc > h3 > a {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .meta {
|
& > .meta {
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
border: 1px solid var(--lightgray);
|
border: 1px solid var(--lightgray);
|
||||||
background-color: var(--light);
|
background-color: var(--light);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
box-shadow: 6px 6px 36px 0 rgba(0,0,0,0.25);
|
box-shadow: 6px 6px 36px 0 rgba(0, 0, 0, 0.25);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,14 +42,17 @@
|
||||||
|
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.3s ease, visibility 0.3s ease;
|
transition:
|
||||||
|
opacity 0.3s ease,
|
||||||
|
visibility 0.3s ease;
|
||||||
|
|
||||||
@media all and (max-width: $mobileBreakpoint) {
|
@media all and (max-width: $mobileBreakpoint) {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover .popover, .popover:hover {
|
a:hover .popover,
|
||||||
|
.popover:hover {
|
||||||
animation: dropin 0.3s ease;
|
animation: dropin 0.3s ease;
|
||||||
animation-fill-mode: forwards;
|
animation-fill-mode: forwards;
|
||||||
animation-delay: 0.2s;
|
animation-delay: 0.2s;
|
||||||
|
|
|
@ -67,7 +67,9 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: var(--light);
|
background: var(--light);
|
||||||
box-shadow: 0 14px 50px rgba(27, 33, 48, 0.12), 0 10px 30px rgba(27, 33, 48, 0.16);
|
box-shadow:
|
||||||
|
0 14px 50px rgba(27, 33, 48, 0.12),
|
||||||
|
0 10px 30px rgba(27, 33, 48, 0.16);
|
||||||
margin-bottom: 2em;
|
margin-bottom: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,7 +110,8 @@
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover, &:focus {
|
&:hover,
|
||||||
|
&:focus {
|
||||||
background: var(--lightgray);
|
background: var(--lightgray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,12 +130,11 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > p {
|
& > p {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,16 +15,16 @@ button#toc {
|
||||||
}
|
}
|
||||||
|
|
||||||
& .fold {
|
& .fold {
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.collapsed .fold {
|
&.collapsed .fold {
|
||||||
transform: rotateZ(-90deg)
|
transform: rotateZ(-90deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#toc-content {
|
#toc-content {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -42,7 +42,9 @@ button#toc {
|
||||||
& > li > a {
|
& > li > a {
|
||||||
color: var(--dark);
|
color: var(--dark);
|
||||||
opacity: 0.35;
|
opacity: 0.35;
|
||||||
transition: 0.5s ease opacity, 0.3s ease color;
|
transition:
|
||||||
|
0.5s ease opacity,
|
||||||
|
0.3s ease color;
|
||||||
&.in-view {
|
&.in-view {
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
|
@ -55,4 +57,3 @@ button#toc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,15 +11,17 @@ export type QuartzComponentProps = {
|
||||||
children: (QuartzComponent | JSX.Element)[]
|
children: (QuartzComponent | JSX.Element)[]
|
||||||
tree: Node<QuartzPluginData>
|
tree: Node<QuartzPluginData>
|
||||||
allFiles: QuartzPluginData[]
|
allFiles: QuartzPluginData[]
|
||||||
displayClass?: 'mobile-only' | 'desktop-only'
|
displayClass?: "mobile-only" | "desktop-only"
|
||||||
} & JSX.IntrinsicAttributes & {
|
} & JSX.IntrinsicAttributes & {
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QuartzComponent = ComponentType<QuartzComponentProps> & {
|
export type QuartzComponent = ComponentType<QuartzComponentProps> & {
|
||||||
css?: string,
|
css?: string
|
||||||
beforeDOMLoaded?: string,
|
beforeDOMLoaded?: string
|
||||||
afterDOMLoaded?: string,
|
afterDOMLoaded?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QuartzComponentConstructor<Options extends object | undefined = undefined> = (opts: Options) => QuartzComponent
|
export type QuartzComponentConstructor<Options extends object | undefined = undefined> = (
|
||||||
|
opts: Options,
|
||||||
|
) => QuartzComponent
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Spinner } from 'cli-spinner'
|
import { Spinner } from "cli-spinner"
|
||||||
|
|
||||||
export class QuartzLogger {
|
export class QuartzLogger {
|
||||||
verbose: boolean
|
verbose: boolean
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import test, { describe } from 'node:test'
|
import test, { describe } from "node:test"
|
||||||
import * as path from './path'
|
import * as path from "./path"
|
||||||
import assert from 'node:assert'
|
import assert from "node:assert"
|
||||||
|
|
||||||
describe('typeguards', () => {
|
describe("typeguards", () => {
|
||||||
test('isClientSlug', () => {
|
test("isClientSlug", () => {
|
||||||
assert(path.isClientSlug("http://example.com"))
|
assert(path.isClientSlug("http://example.com"))
|
||||||
assert(path.isClientSlug("http://example.com/index"))
|
assert(path.isClientSlug("http://example.com/index"))
|
||||||
assert(path.isClientSlug("http://example.com/index.html"))
|
assert(path.isClientSlug("http://example.com/index.html"))
|
||||||
|
@ -23,7 +23,7 @@ describe('typeguards', () => {
|
||||||
assert(!path.isClientSlug("https"))
|
assert(!path.isClientSlug("https"))
|
||||||
})
|
})
|
||||||
|
|
||||||
test('isCanonicalSlug', () => {
|
test("isCanonicalSlug", () => {
|
||||||
assert(path.isCanonicalSlug(""))
|
assert(path.isCanonicalSlug(""))
|
||||||
assert(path.isCanonicalSlug("abc"))
|
assert(path.isCanonicalSlug("abc"))
|
||||||
assert(path.isCanonicalSlug("notindex"))
|
assert(path.isCanonicalSlug("notindex"))
|
||||||
|
@ -41,7 +41,7 @@ describe('typeguards', () => {
|
||||||
assert(!path.isCanonicalSlug("index.html"))
|
assert(!path.isCanonicalSlug("index.html"))
|
||||||
})
|
})
|
||||||
|
|
||||||
test('isRelativeURL', () => {
|
test("isRelativeURL", () => {
|
||||||
assert(path.isRelativeURL("."))
|
assert(path.isRelativeURL("."))
|
||||||
assert(path.isRelativeURL(".."))
|
assert(path.isRelativeURL(".."))
|
||||||
assert(path.isRelativeURL("./abc/def"))
|
assert(path.isRelativeURL("./abc/def"))
|
||||||
|
@ -58,7 +58,7 @@ describe('typeguards', () => {
|
||||||
assert(!path.isRelativeURL("./abc/def.md"))
|
assert(!path.isRelativeURL("./abc/def.md"))
|
||||||
})
|
})
|
||||||
|
|
||||||
test('isServerSlug', () => {
|
test("isServerSlug", () => {
|
||||||
assert(path.isServerSlug("index"))
|
assert(path.isServerSlug("index"))
|
||||||
assert(path.isServerSlug("abc/def"))
|
assert(path.isServerSlug("abc/def"))
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ describe('typeguards', () => {
|
||||||
assert(!path.isServerSlug("note with spaces"))
|
assert(!path.isServerSlug("note with spaces"))
|
||||||
})
|
})
|
||||||
|
|
||||||
test('isFilePath', () => {
|
test("isFilePath", () => {
|
||||||
assert(path.isFilePath("content/index.md"))
|
assert(path.isFilePath("content/index.md"))
|
||||||
assert(path.isFilePath("content/test.png"))
|
assert(path.isFilePath("content/test.png"))
|
||||||
assert(!path.isFilePath("../test.pdf"))
|
assert(!path.isFilePath("../test.pdf"))
|
||||||
|
@ -81,80 +81,112 @@ describe('typeguards', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("transforms", () => {
|
||||||
describe('transforms', () => {
|
function asserts<Inp, Out>(
|
||||||
function asserts<Inp, Out>(pairs: [string, string][], transform: (inp: Inp) => Out, checkPre: (x: any) => x is Inp, checkPost: (x: any) => x is Out) {
|
pairs: [string, string][],
|
||||||
|
transform: (inp: Inp) => Out,
|
||||||
|
checkPre: (x: any) => x is Inp,
|
||||||
|
checkPost: (x: any) => x is Out,
|
||||||
|
) {
|
||||||
for (const [inp, expected] of pairs) {
|
for (const [inp, expected] of pairs) {
|
||||||
assert(checkPre(inp), `${inp} wasn't the expected input type`)
|
assert(checkPre(inp), `${inp} wasn't the expected input type`)
|
||||||
const actual = transform(inp)
|
const actual = transform(inp)
|
||||||
assert.strictEqual(actual, expected, `after transforming ${inp}, '${actual}' was not '${expected}'`)
|
assert.strictEqual(
|
||||||
|
actual,
|
||||||
|
expected,
|
||||||
|
`after transforming ${inp}, '${actual}' was not '${expected}'`,
|
||||||
|
)
|
||||||
assert(checkPost(actual), `${actual} wasn't the expected output type`)
|
assert(checkPost(actual), `${actual} wasn't the expected output type`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test('canonicalizeServer', () => {
|
test("canonicalizeServer", () => {
|
||||||
asserts([
|
asserts(
|
||||||
["index", ""],
|
[
|
||||||
["abc/index", "abc"],
|
["index", ""],
|
||||||
["abc/def", "abc/def"],
|
["abc/index", "abc"],
|
||||||
], path.canonicalizeServer, path.isServerSlug, path.isCanonicalSlug)
|
["abc/def", "abc/def"],
|
||||||
|
],
|
||||||
|
path.canonicalizeServer,
|
||||||
|
path.isServerSlug,
|
||||||
|
path.isCanonicalSlug,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('canonicalizeClient', () => {
|
test("canonicalizeClient", () => {
|
||||||
asserts([
|
asserts(
|
||||||
["http://localhost:3000", ""],
|
[
|
||||||
["http://localhost:3000/index", ""],
|
["http://localhost:3000", ""],
|
||||||
["http://localhost:3000/test", "test"],
|
["http://localhost:3000/index", ""],
|
||||||
["http://example.com", ""],
|
["http://localhost:3000/test", "test"],
|
||||||
["http://example.com/index", ""],
|
["http://example.com", ""],
|
||||||
["http://example.com/index.html", ""],
|
["http://example.com/index", ""],
|
||||||
["http://example.com/", ""],
|
["http://example.com/index.html", ""],
|
||||||
["https://example.com", ""],
|
["http://example.com/", ""],
|
||||||
["https://example.com/abc/def", "abc/def"],
|
["https://example.com", ""],
|
||||||
["https://example.com/abc/def/", "abc/def"],
|
["https://example.com/abc/def", "abc/def"],
|
||||||
["https://example.com/abc/def#cool", "abc/def"],
|
["https://example.com/abc/def/", "abc/def"],
|
||||||
["https://example.com/abc/def?field=1&another=2", "abc/def"],
|
["https://example.com/abc/def#cool", "abc/def"],
|
||||||
["https://example.com/abc/def?field=1&another=2#cool", "abc/def"],
|
["https://example.com/abc/def?field=1&another=2", "abc/def"],
|
||||||
["https://example.com/abc/def.html?field=1&another=2#cool", "abc/def"],
|
["https://example.com/abc/def?field=1&another=2#cool", "abc/def"],
|
||||||
], path.canonicalizeClient, path.isClientSlug, path.isCanonicalSlug)
|
["https://example.com/abc/def.html?field=1&another=2#cool", "abc/def"],
|
||||||
|
],
|
||||||
|
path.canonicalizeClient,
|
||||||
|
path.isClientSlug,
|
||||||
|
path.isCanonicalSlug,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('slugifyFilePath', () => {
|
describe("slugifyFilePath", () => {
|
||||||
asserts([
|
asserts(
|
||||||
["content/index.md", "content/index"],
|
[
|
||||||
["content/_index.md", "content/index"],
|
["content/index.md", "content/index"],
|
||||||
["/content/index.md", "content/index"],
|
["content/_index.md", "content/index"],
|
||||||
["content/cool.png", "content/cool"],
|
["/content/index.md", "content/index"],
|
||||||
["index.md", "index"],
|
["content/cool.png", "content/cool"],
|
||||||
["note with spaces.md", "note-with-spaces"],
|
["index.md", "index"],
|
||||||
], path.slugifyFilePath, path.isFilePath, path.isServerSlug)
|
["note with spaces.md", "note-with-spaces"],
|
||||||
|
],
|
||||||
|
path.slugifyFilePath,
|
||||||
|
path.isFilePath,
|
||||||
|
path.isServerSlug,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('transformInternalLink', () => {
|
describe("transformInternalLink", () => {
|
||||||
asserts([
|
asserts(
|
||||||
["", "."],
|
[
|
||||||
[".", "."],
|
["", "."],
|
||||||
["./", "."],
|
[".", "."],
|
||||||
["./index", "."],
|
["./", "."],
|
||||||
["./index.html", "."],
|
["./index", "."],
|
||||||
["./index.md", "."],
|
["./index.html", "."],
|
||||||
["content", "./content"],
|
["./index.md", "."],
|
||||||
["content/test.md", "./content/test"],
|
["content", "./content"],
|
||||||
["./content/test.md", "./content/test"],
|
["content/test.md", "./content/test"],
|
||||||
["../content/test.md", "../content/test"],
|
["./content/test.md", "./content/test"],
|
||||||
["tags/", "./tags"],
|
["../content/test.md", "../content/test"],
|
||||||
["/tags/", "./tags"],
|
["tags/", "./tags"],
|
||||||
["content/with spaces", "./content/with-spaces"],
|
["/tags/", "./tags"],
|
||||||
["content/with spaces#and Anchor!", "./content/with-spaces#and-anchor"],
|
["content/with spaces", "./content/with-spaces"],
|
||||||
], path.transformInternalLink, (_x: string): _x is string => true, path.isRelativeURL)
|
["content/with spaces#and Anchor!", "./content/with-spaces#and-anchor"],
|
||||||
|
],
|
||||||
|
path.transformInternalLink,
|
||||||
|
(_x: string): _x is string => true,
|
||||||
|
path.isRelativeURL,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('pathToRoot', () => {
|
describe("pathToRoot", () => {
|
||||||
asserts([
|
asserts(
|
||||||
["", "."],
|
[
|
||||||
["abc", ".."],
|
["", "."],
|
||||||
["abc/def", "../.."],
|
["abc", ".."],
|
||||||
], path.pathToRoot, path.isCanonicalSlug, path.isRelativeURL)
|
["abc/def", "../.."],
|
||||||
|
],
|
||||||
|
path.pathToRoot,
|
||||||
|
path.isCanonicalSlug,
|
||||||
|
path.isRelativeURL,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { slug as slugAnchor } from 'github-slugger'
|
import { slug as slugAnchor } from "github-slugger"
|
||||||
import { trace } from './trace'
|
import { trace } from "./trace"
|
||||||
|
|
||||||
// Quartz Paths
|
// Quartz Paths
|
||||||
// Things in boxes are not actual types but rather sources which these types can be acquired from
|
// Things in boxes are not actual types but rather sources which these types can be acquired from
|
||||||
|
@ -46,7 +46,7 @@ import { trace } from './trace'
|
||||||
const STRICT_TYPE_CHECKS = false
|
const STRICT_TYPE_CHECKS = false
|
||||||
const HARD_EXIT_ON_FAIL = false
|
const HARD_EXIT_ON_FAIL = false
|
||||||
|
|
||||||
function conditionCheck<T>(name: string, label: 'pre' | 'post', s: T, chk: (x: any) => x is T) {
|
function conditionCheck<T>(name: string, label: "pre" | "post", s: T, chk: (x: any) => x is T) {
|
||||||
if (STRICT_TYPE_CHECKS && !chk(s)) {
|
if (STRICT_TYPE_CHECKS && !chk(s)) {
|
||||||
trace(`${name} failed ${label}-condition check: ${s} does not pass ${chk.name}`, new Error())
|
trace(`${name} failed ${label}-condition check: ${s} does not pass ${chk.name}`, new Error())
|
||||||
if (HARD_EXIT_ON_FAIL) {
|
if (HARD_EXIT_ON_FAIL) {
|
||||||
|
@ -66,8 +66,8 @@ export function isClientSlug(s: string): s is ClientSlug {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Canonical slug, should be used whenever you need to refer to the location of a file/note.
|
/** Canonical slug, should be used whenever you need to refer to the location of a file/note.
|
||||||
* On the client, this is normally stored in `document.body.dataset.slug`
|
* On the client, this is normally stored in `document.body.dataset.slug`
|
||||||
*/
|
*/
|
||||||
export type CanonicalSlug = SlugLike<"canonical">
|
export type CanonicalSlug = SlugLike<"canonical">
|
||||||
export function isCanonicalSlug(s: string): s is CanonicalSlug {
|
export function isCanonicalSlug(s: string): s is CanonicalSlug {
|
||||||
const validStart = !(s.startsWith(".") || s.startsWith("/"))
|
const validStart = !(s.startsWith(".") || s.startsWith("/"))
|
||||||
|
@ -76,8 +76,8 @@ export function isCanonicalSlug(s: string): s is CanonicalSlug {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A relative link, can be found on `href`s but can also be constructed for
|
/** A relative link, can be found on `href`s but can also be constructed for
|
||||||
* client-side navigation (e.g. search and graph)
|
* client-side navigation (e.g. search and graph)
|
||||||
*/
|
*/
|
||||||
export type RelativeURL = SlugLike<"relative">
|
export type RelativeURL = SlugLike<"relative">
|
||||||
export function isRelativeURL(s: string): s is RelativeURL {
|
export function isRelativeURL(s: string): s is RelativeURL {
|
||||||
const validStart = /^\.{1,2}/.test(s)
|
const validStart = /^\.{1,2}/.test(s)
|
||||||
|
@ -102,58 +102,58 @@ export function isFilePath(s: string): s is FilePath {
|
||||||
|
|
||||||
export function getClientSlug(window: Window): ClientSlug {
|
export function getClientSlug(window: Window): ClientSlug {
|
||||||
const res = window.location.href as ClientSlug
|
const res = window.location.href as ClientSlug
|
||||||
conditionCheck(getClientSlug.name, 'post', res, isClientSlug)
|
conditionCheck(getClientSlug.name, "post", res, isClientSlug)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCanonicalSlug(window: Window): CanonicalSlug {
|
export function getCanonicalSlug(window: Window): CanonicalSlug {
|
||||||
const res = window.document.body.dataset.slug! as CanonicalSlug
|
const res = window.document.body.dataset.slug! as CanonicalSlug
|
||||||
conditionCheck(getCanonicalSlug.name, 'post', res, isCanonicalSlug)
|
conditionCheck(getCanonicalSlug.name, "post", res, isCanonicalSlug)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canonicalizeClient(slug: ClientSlug): CanonicalSlug {
|
export function canonicalizeClient(slug: ClientSlug): CanonicalSlug {
|
||||||
conditionCheck(canonicalizeClient.name, 'pre', slug, isClientSlug)
|
conditionCheck(canonicalizeClient.name, "pre", slug, isClientSlug)
|
||||||
const { pathname } = new URL(slug)
|
const { pathname } = new URL(slug)
|
||||||
let fp = pathname.slice(1)
|
let fp = pathname.slice(1)
|
||||||
fp = fp.replace(new RegExp(_getFileExtension(fp) + '$'), '')
|
fp = fp.replace(new RegExp(_getFileExtension(fp) + "$"), "")
|
||||||
const res = _canonicalize(fp) as CanonicalSlug
|
const res = _canonicalize(fp) as CanonicalSlug
|
||||||
conditionCheck(canonicalizeClient.name, 'post', res, isCanonicalSlug)
|
conditionCheck(canonicalizeClient.name, "post", res, isCanonicalSlug)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canonicalizeServer(slug: ServerSlug): CanonicalSlug {
|
export function canonicalizeServer(slug: ServerSlug): CanonicalSlug {
|
||||||
conditionCheck(canonicalizeServer.name, 'pre', slug, isServerSlug)
|
conditionCheck(canonicalizeServer.name, "pre", slug, isServerSlug)
|
||||||
let fp = slug as string
|
let fp = slug as string
|
||||||
const res = _canonicalize(fp) as CanonicalSlug
|
const res = _canonicalize(fp) as CanonicalSlug
|
||||||
conditionCheck(canonicalizeServer.name, 'post', res, isCanonicalSlug)
|
conditionCheck(canonicalizeServer.name, "post", res, isCanonicalSlug)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
export function slugifyFilePath(fp: FilePath): ServerSlug {
|
export function slugifyFilePath(fp: FilePath): ServerSlug {
|
||||||
conditionCheck(slugifyFilePath.name, 'pre', fp, isFilePath)
|
conditionCheck(slugifyFilePath.name, "pre", fp, isFilePath)
|
||||||
fp = _stripSlashes(fp) as FilePath
|
fp = _stripSlashes(fp) as FilePath
|
||||||
const withoutFileExt = fp.replace(new RegExp(_getFileExtension(fp) + '$'), '')
|
const withoutFileExt = fp.replace(new RegExp(_getFileExtension(fp) + "$"), "")
|
||||||
let slug = withoutFileExt
|
let slug = withoutFileExt
|
||||||
.split('/')
|
.split("/")
|
||||||
.map((segment) => segment.replace(/\s/g, '-')) // slugify all segments
|
.map((segment) => segment.replace(/\s/g, "-")) // slugify all segments
|
||||||
.join('/') // always use / as sep
|
.join("/") // always use / as sep
|
||||||
.replace(/\/$/, '') // remove trailing slash
|
.replace(/\/$/, "") // remove trailing slash
|
||||||
|
|
||||||
// treat _index as index
|
// treat _index as index
|
||||||
if (_endsWith(slug, "_index")) {
|
if (_endsWith(slug, "_index")) {
|
||||||
slug = slug.replace(/_index$/, "index")
|
slug = slug.replace(/_index$/, "index")
|
||||||
}
|
}
|
||||||
|
|
||||||
conditionCheck(slugifyFilePath.name, 'post', slug, isServerSlug)
|
conditionCheck(slugifyFilePath.name, "post", slug, isServerSlug)
|
||||||
return slug as ServerSlug
|
return slug as ServerSlug
|
||||||
}
|
}
|
||||||
|
|
||||||
export function transformInternalLink(link: string): RelativeURL {
|
export function transformInternalLink(link: string): RelativeURL {
|
||||||
let [fplike, anchor] = splitAnchor(decodeURI(link))
|
let [fplike, anchor] = splitAnchor(decodeURI(link))
|
||||||
let segments = fplike.split("/").filter(x => x.length > 0)
|
let segments = fplike.split("/").filter((x) => x.length > 0)
|
||||||
let prefix = segments.filter(_isRelativeSegment).join("/")
|
let prefix = segments.filter(_isRelativeSegment).join("/")
|
||||||
let fp = segments.filter(seg => !_isRelativeSegment(seg)).join("/")
|
let fp = segments.filter((seg) => !_isRelativeSegment(seg)).join("/")
|
||||||
|
|
||||||
// implicit markdown
|
// implicit markdown
|
||||||
if (!_hasFileExtension(fp)) {
|
if (!_hasFileExtension(fp)) {
|
||||||
|
@ -164,57 +164,57 @@ export function transformInternalLink(link: string): RelativeURL {
|
||||||
fp = _trimSuffix(fp, "index")
|
fp = _trimSuffix(fp, "index")
|
||||||
|
|
||||||
let joined = joinSegments(_stripSlashes(prefix), _stripSlashes(fp))
|
let joined = joinSegments(_stripSlashes(prefix), _stripSlashes(fp))
|
||||||
const res = _addRelativeToStart(joined) + anchor as RelativeURL
|
const res = (_addRelativeToStart(joined) + anchor) as RelativeURL
|
||||||
conditionCheck(transformInternalLink.name, 'post', res, isRelativeURL)
|
conditionCheck(transformInternalLink.name, "post", res, isRelativeURL)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolve /a/b/c to ../../
|
// resolve /a/b/c to ../../
|
||||||
export function pathToRoot(slug: CanonicalSlug): RelativeURL {
|
export function pathToRoot(slug: CanonicalSlug): RelativeURL {
|
||||||
conditionCheck(pathToRoot.name, 'pre', slug, isCanonicalSlug)
|
conditionCheck(pathToRoot.name, "pre", slug, isCanonicalSlug)
|
||||||
let rootPath = slug
|
let rootPath = slug
|
||||||
.split('/')
|
.split("/")
|
||||||
.filter(x => x !== '')
|
.filter((x) => x !== "")
|
||||||
.map(_ => '..')
|
.map((_) => "..")
|
||||||
.join('/')
|
.join("/")
|
||||||
|
|
||||||
const res = _addRelativeToStart(rootPath) as RelativeURL
|
const res = _addRelativeToStart(rootPath) as RelativeURL
|
||||||
conditionCheck(pathToRoot.name, 'post', res, isRelativeURL)
|
conditionCheck(pathToRoot.name, "post", res, isRelativeURL)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveRelative(current: CanonicalSlug, target: CanonicalSlug): RelativeURL {
|
export function resolveRelative(current: CanonicalSlug, target: CanonicalSlug): RelativeURL {
|
||||||
conditionCheck(resolveRelative.name, 'pre', current, isCanonicalSlug)
|
conditionCheck(resolveRelative.name, "pre", current, isCanonicalSlug)
|
||||||
conditionCheck(resolveRelative.name, 'pre', target, isCanonicalSlug)
|
conditionCheck(resolveRelative.name, "pre", target, isCanonicalSlug)
|
||||||
const res = joinSegments(pathToRoot(current), target) as RelativeURL
|
const res = joinSegments(pathToRoot(current), target) as RelativeURL
|
||||||
conditionCheck(resolveRelative.name, 'post', res, isRelativeURL)
|
conditionCheck(resolveRelative.name, "post", res, isRelativeURL)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
export function splitAnchor(link: string): [string, string] {
|
export function splitAnchor(link: string): [string, string] {
|
||||||
let [fp, anchor] = link.split("#", 2)
|
let [fp, anchor] = link.split("#", 2)
|
||||||
anchor = anchor === undefined ? "" : '#' + slugAnchor(anchor)
|
anchor = anchor === undefined ? "" : "#" + slugAnchor(anchor)
|
||||||
return [fp, anchor]
|
return [fp, anchor]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function joinSegments(...args: string[]): string {
|
export function joinSegments(...args: string[]): string {
|
||||||
return args.filter(segment => segment !== "").join('/')
|
return args.filter((segment) => segment !== "").join("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QUARTZ = "quartz"
|
export const QUARTZ = "quartz"
|
||||||
|
|
||||||
function _canonicalize(fp: string): string {
|
function _canonicalize(fp: string): string {
|
||||||
fp = _trimSuffix(fp, "index")
|
fp = _trimSuffix(fp, "index")
|
||||||
return _stripSlashes(fp)
|
return _stripSlashes(fp)
|
||||||
}
|
}
|
||||||
|
|
||||||
function _endsWith(s: string, suffix: string): boolean {
|
function _endsWith(s: string, suffix: string): boolean {
|
||||||
return s === suffix || s.endsWith("/" + suffix)
|
return s === suffix || s.endsWith("/" + suffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
function _trimSuffix(s: string, suffix: string): string {
|
function _trimSuffix(s: string, suffix: string): string {
|
||||||
if (_endsWith(s, suffix)) {
|
if (_endsWith(s, suffix)) {
|
||||||
s = s.slice(0, -(suffix.length))
|
s = s.slice(0, -suffix.length)
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import chalk from 'chalk'
|
import chalk from "chalk"
|
||||||
import pretty from 'pretty-time'
|
import pretty from "pretty-time"
|
||||||
|
|
||||||
export class PerfTimer {
|
export class PerfTimer {
|
||||||
evts: { [key: string]: [number, number] }
|
evts: { [key: string]: [number, number] }
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.evts = {}
|
this.evts = {}
|
||||||
this.addEvent('start')
|
this.addEvent("start")
|
||||||
}
|
}
|
||||||
|
|
||||||
addEvent(evtName: string) {
|
addEvent(evtName: string) {
|
||||||
|
@ -14,6 +14,6 @@ export class PerfTimer {
|
||||||
}
|
}
|
||||||
|
|
||||||
timeSince(evtName?: string): string {
|
timeSince(evtName?: string): string {
|
||||||
return chalk.yellow(pretty(process.hrtime(this.evts[evtName ?? 'start'])))
|
return chalk.yellow(pretty(process.hrtime(this.evts[evtName ?? "start"])))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
import { CanonicalSlug, FilePath, ServerSlug, canonicalizeServer, resolveRelative } from "../../path"
|
import {
|
||||||
|
CanonicalSlug,
|
||||||
|
FilePath,
|
||||||
|
ServerSlug,
|
||||||
|
canonicalizeServer,
|
||||||
|
resolveRelative,
|
||||||
|
} from "../../path"
|
||||||
import { QuartzEmitterPlugin } from "../types"
|
import { QuartzEmitterPlugin } from "../types"
|
||||||
import path from 'path'
|
import path from "path"
|
||||||
|
|
||||||
export const AliasRedirects: QuartzEmitterPlugin = () => ({
|
export const AliasRedirects: QuartzEmitterPlugin = () => ({
|
||||||
name: "AliasRedirects",
|
name: "AliasRedirects",
|
||||||
|
@ -24,7 +30,7 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
|
||||||
for (const alias of aliases) {
|
for (const alias of aliases) {
|
||||||
const slug = path.posix.join(dir, alias) as ServerSlug
|
const slug = path.posix.join(dir, alias) as ServerSlug
|
||||||
|
|
||||||
const fp = slug + ".html" as FilePath
|
const fp = (slug + ".html") as FilePath
|
||||||
const redirUrl = resolveRelative(canonicalizeServer(slug), ogSlug)
|
const redirUrl = resolveRelative(canonicalizeServer(slug), ogSlug)
|
||||||
await emit({
|
await emit({
|
||||||
content: `
|
content: `
|
||||||
|
@ -47,5 +53,5 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fps
|
return fps
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -5,12 +5,12 @@ import path from "path"
|
||||||
|
|
||||||
export type ContentIndex = Map<CanonicalSlug, ContentDetails>
|
export type ContentIndex = Map<CanonicalSlug, ContentDetails>
|
||||||
export type ContentDetails = {
|
export type ContentDetails = {
|
||||||
title: string,
|
title: string
|
||||||
links: CanonicalSlug[],
|
links: CanonicalSlug[]
|
||||||
tags: string[],
|
tags: string[]
|
||||||
content: string,
|
content: string
|
||||||
date?: Date,
|
date?: Date
|
||||||
description?: string,
|
description?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
|
@ -31,7 +31,9 @@ function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string {
|
||||||
<loc>https://${base}/${slug}</loc>
|
<loc>https://${base}/${slug}</loc>
|
||||||
<lastmod>${content.date?.toISOString()}</lastmod>
|
<lastmod>${content.date?.toISOString()}</lastmod>
|
||||||
</url>`
|
</url>`
|
||||||
const urls = Array.from(idx).map(([slug, content]) => createURLEntry(slug, content)).join("")
|
const urls = Array.from(idx)
|
||||||
|
.map(([slug, content]) => createURLEntry(slug, content))
|
||||||
|
.join("")
|
||||||
return `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">${urls}</urlset>`
|
return `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">${urls}</urlset>`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +49,9 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex): string {
|
||||||
<pubDate>${content.date?.toUTCString()}</pubDate>
|
<pubDate>${content.date?.toUTCString()}</pubDate>
|
||||||
</items>`
|
</items>`
|
||||||
|
|
||||||
const items = Array.from(idx).map(([slug, content]) => createURLEntry(slug, content)).join("")
|
const items = Array.from(idx)
|
||||||
|
.map(([slug, content]) => createURLEntry(slug, content))
|
||||||
|
.join("")
|
||||||
return `<rss xmlns:atom="http://www.w3.org/2005/atom" version="2.0">
|
return `<rss xmlns:atom="http://www.w3.org/2005/atom" version="2.0">
|
||||||
<channel>
|
<channel>
|
||||||
<title>${cfg.pageTitle}</title>
|
<title>${cfg.pageTitle}</title>
|
||||||
|
@ -71,14 +75,14 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
|
||||||
const slug = canonicalizeServer(file.data.slug!)
|
const slug = canonicalizeServer(file.data.slug!)
|
||||||
const date = file.data.dates?.modified ?? new Date()
|
const date = file.data.dates?.modified ?? new Date()
|
||||||
if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) {
|
if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) {
|
||||||
linkIndex.set(slug, {
|
linkIndex.set(slug, {
|
||||||
title: file.data.frontmatter?.title!,
|
title: file.data.frontmatter?.title!,
|
||||||
links: file.data.links ?? [],
|
links: file.data.links ?? [],
|
||||||
tags: file.data.frontmatter?.tags ?? [],
|
tags: file.data.frontmatter?.tags ?? [],
|
||||||
content: file.data.text ?? "",
|
content: file.data.text ?? "",
|
||||||
date: date,
|
date: date,
|
||||||
description: file.data.description ?? ""
|
description: file.data.description ?? "",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +90,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
|
||||||
await emit({
|
await emit({
|
||||||
content: generateSiteMap(cfg, linkIndex),
|
content: generateSiteMap(cfg, linkIndex),
|
||||||
slug: "sitemap" as ServerSlug,
|
slug: "sitemap" as ServerSlug,
|
||||||
ext: ".xml"
|
ext: ".xml",
|
||||||
})
|
})
|
||||||
emitted.push("sitemap.xml" as FilePath)
|
emitted.push("sitemap.xml" as FilePath)
|
||||||
}
|
}
|
||||||
|
@ -95,7 +99,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
|
||||||
await emit({
|
await emit({
|
||||||
content: generateRSSFeed(cfg, linkIndex),
|
content: generateRSSFeed(cfg, linkIndex),
|
||||||
slug: "index" as ServerSlug,
|
slug: "index" as ServerSlug,
|
||||||
ext: ".xml"
|
ext: ".xml",
|
||||||
})
|
})
|
||||||
emitted.push("index.xml" as FilePath)
|
emitted.push("index.xml" as FilePath)
|
||||||
}
|
}
|
||||||
|
@ -109,7 +113,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
|
||||||
delete content.description
|
delete content.description
|
||||||
delete content.date
|
delete content.date
|
||||||
return [slug, content]
|
return [slug, content]
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
await emit({
|
await emit({
|
||||||
|
|
|
@ -8,7 +8,9 @@ import { FilePath, canonicalizeServer } from "../../path"
|
||||||
|
|
||||||
export const ContentPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
|
export const ContentPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
|
||||||
if (!opts) {
|
if (!opts) {
|
||||||
throw new Error("ContentPage must be initialized with options specifiying the components to use")
|
throw new Error(
|
||||||
|
"ContentPage must be initialized with options specifiying the components to use",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { head: Head, header, beforeBody, pageBody: Content, left, right, footer: Footer } = opts
|
const { head: Head, header, beforeBody, pageBody: Content, left, right, footer: Footer } = opts
|
||||||
|
@ -22,7 +24,7 @@ export const ContentPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
|
||||||
},
|
},
|
||||||
async emit(_contentDir, cfg, content, resources, emit): Promise<FilePath[]> {
|
async emit(_contentDir, cfg, content, resources, emit): Promise<FilePath[]> {
|
||||||
const fps: FilePath[] = []
|
const fps: FilePath[] = []
|
||||||
const allFiles = content.map(c => c[1].data)
|
const allFiles = content.map((c) => c[1].data)
|
||||||
for (const [tree, file] of content) {
|
for (const [tree, file] of content) {
|
||||||
const slug = canonicalizeServer(file.data.slug!)
|
const slug = canonicalizeServer(file.data.slug!)
|
||||||
const externalResources = pageResources(slug, resources)
|
const externalResources = pageResources(slug, resources)
|
||||||
|
@ -32,17 +34,12 @@ export const ContentPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
|
||||||
cfg,
|
cfg,
|
||||||
children: [],
|
children: [],
|
||||||
tree,
|
tree,
|
||||||
allFiles
|
allFiles,
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = renderPage(
|
const content = renderPage(slug, componentData, opts, externalResources)
|
||||||
slug,
|
|
||||||
componentData,
|
|
||||||
opts,
|
|
||||||
externalResources
|
|
||||||
)
|
|
||||||
|
|
||||||
const fp = file.data.slug + ".html" as FilePath
|
const fp = (file.data.slug + ".html") as FilePath
|
||||||
await emit({
|
await emit({
|
||||||
content,
|
content,
|
||||||
slug: file.data.slug!,
|
slug: file.data.slug!,
|
||||||
|
@ -52,6 +49,6 @@ export const ContentPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
|
||||||
fps.push(fp)
|
fps.push(fp)
|
||||||
}
|
}
|
||||||
return fps
|
return fps
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,20 +24,28 @@ export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
|
||||||
},
|
},
|
||||||
async emit(_contentDir, cfg, content, resources, emit): Promise<FilePath[]> {
|
async emit(_contentDir, cfg, content, resources, emit): Promise<FilePath[]> {
|
||||||
const fps: FilePath[] = []
|
const fps: FilePath[] = []
|
||||||
const allFiles = content.map(c => c[1].data)
|
const allFiles = content.map((c) => c[1].data)
|
||||||
|
|
||||||
const folders: Set<CanonicalSlug> = new Set(allFiles.flatMap(data => {
|
const folders: Set<CanonicalSlug> = new Set(
|
||||||
const slug = data.slug
|
allFiles.flatMap((data) => {
|
||||||
const folderName = path.dirname(slug ?? "") as CanonicalSlug
|
const slug = data.slug
|
||||||
if (slug && folderName !== "." && folderName !== "tags") {
|
const folderName = path.dirname(slug ?? "") as CanonicalSlug
|
||||||
return [folderName]
|
if (slug && folderName !== "." && folderName !== "tags") {
|
||||||
}
|
return [folderName]
|
||||||
return []
|
}
|
||||||
}))
|
return []
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const folderDescriptions: Record<string, ProcessedContent> = Object.fromEntries([...folders].map(folder => ([
|
const folderDescriptions: Record<string, ProcessedContent> = Object.fromEntries(
|
||||||
folder, defaultProcessedContent({ slug: joinSegments(folder, "index") as ServerSlug, frontmatter: { title: `Folder: ${folder}`, tags: [] } })
|
[...folders].map((folder) => [
|
||||||
])))
|
folder,
|
||||||
|
defaultProcessedContent({
|
||||||
|
slug: joinSegments(folder, "index") as ServerSlug,
|
||||||
|
frontmatter: { title: `Folder: ${folder}`, tags: [] },
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
|
||||||
for (const [tree, file] of content) {
|
for (const [tree, file] of content) {
|
||||||
const slug = canonicalizeServer(file.data.slug!)
|
const slug = canonicalizeServer(file.data.slug!)
|
||||||
|
@ -56,17 +64,12 @@ export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
|
||||||
cfg,
|
cfg,
|
||||||
children: [],
|
children: [],
|
||||||
tree,
|
tree,
|
||||||
allFiles
|
allFiles,
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = renderPage(
|
const content = renderPage(slug, componentData, opts, externalResources)
|
||||||
slug,
|
|
||||||
componentData,
|
|
||||||
opts,
|
|
||||||
externalResources
|
|
||||||
)
|
|
||||||
|
|
||||||
const fp = file.data.slug! + ".html" as FilePath
|
const fp = (file.data.slug! + ".html") as FilePath
|
||||||
await emit({
|
await emit({
|
||||||
content,
|
content,
|
||||||
slug: file.data.slug!,
|
slug: file.data.slug!,
|
||||||
|
@ -76,6 +79,6 @@ export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
|
||||||
fps.push(fp)
|
fps.push(fp)
|
||||||
}
|
}
|
||||||
return fps
|
return fps
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export { ContentPage } from './contentPage'
|
export { ContentPage } from "./contentPage"
|
||||||
export { TagPage } from './tagPage'
|
export { TagPage } from "./tagPage"
|
||||||
export { FolderPage } from './folderPage'
|
export { FolderPage } from "./folderPage"
|
||||||
export { ContentIndex } from './contentIndex'
|
export { ContentIndex } from "./contentIndex"
|
||||||
export { AliasRedirects } from './aliases'
|
export { AliasRedirects } from "./aliases"
|
||||||
|
|
|
@ -23,12 +23,18 @@ export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
|
||||||
},
|
},
|
||||||
async emit(_contentDir, cfg, content, resources, emit): Promise<FilePath[]> {
|
async emit(_contentDir, cfg, content, resources, emit): Promise<FilePath[]> {
|
||||||
const fps: FilePath[] = []
|
const fps: FilePath[] = []
|
||||||
const allFiles = content.map(c => c[1].data)
|
const allFiles = content.map((c) => c[1].data)
|
||||||
|
|
||||||
const tags: Set<string> = new Set(allFiles.flatMap(data => data.frontmatter?.tags ?? []))
|
const tags: Set<string> = new Set(allFiles.flatMap((data) => data.frontmatter?.tags ?? []))
|
||||||
const tagDescriptions: Record<string, ProcessedContent> = Object.fromEntries([...tags].map(tag => ([
|
const tagDescriptions: Record<string, ProcessedContent> = Object.fromEntries(
|
||||||
tag, defaultProcessedContent({ slug: `tags/${tag}/index` as ServerSlug, frontmatter: { title: `Tag: ${tag}`, tags: [] } })
|
[...tags].map((tag) => [
|
||||||
])))
|
tag,
|
||||||
|
defaultProcessedContent({
|
||||||
|
slug: `tags/${tag}/index` as ServerSlug,
|
||||||
|
frontmatter: { title: `Tag: ${tag}`, tags: [] },
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
|
||||||
for (const [tree, file] of content) {
|
for (const [tree, file] of content) {
|
||||||
const slug = file.data.slug!
|
const slug = file.data.slug!
|
||||||
|
@ -50,17 +56,12 @@ export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
|
||||||
cfg,
|
cfg,
|
||||||
children: [],
|
children: [],
|
||||||
tree,
|
tree,
|
||||||
allFiles
|
allFiles,
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = renderPage(
|
const content = renderPage(slug, componentData, opts, externalResources)
|
||||||
slug,
|
|
||||||
componentData,
|
|
||||||
opts,
|
|
||||||
externalResources
|
|
||||||
)
|
|
||||||
|
|
||||||
const fp = file.data.slug + ".html" as FilePath
|
const fp = (file.data.slug + ".html") as FilePath
|
||||||
await emit({
|
await emit({
|
||||||
content,
|
content,
|
||||||
slug: file.data.slug!,
|
slug: file.data.slug!,
|
||||||
|
@ -70,6 +71,6 @@ export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
|
||||||
fps.push(fp)
|
fps.push(fp)
|
||||||
}
|
}
|
||||||
return fps
|
return fps
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,5 +5,5 @@ export const RemoveDrafts: QuartzFilterPlugin<{}> = () => ({
|
||||||
shouldPublish([_tree, vfile]) {
|
shouldPublish([_tree, vfile]) {
|
||||||
const draftFlag: boolean = vfile.data?.frontmatter?.draft ?? false
|
const draftFlag: boolean = vfile.data?.frontmatter?.draft ?? false
|
||||||
return !draftFlag
|
return !draftFlag
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -5,5 +5,5 @@ export const ExplicitPublish: QuartzFilterPlugin = () => ({
|
||||||
shouldPublish([_tree, vfile]) {
|
shouldPublish([_tree, vfile]) {
|
||||||
const publishFlag: boolean = vfile.data?.frontmatter?.publish ?? false
|
const publishFlag: boolean = vfile.data?.frontmatter?.publish ?? false
|
||||||
return publishFlag
|
return publishFlag
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
export { RemoveDrafts } from './draft'
|
export { RemoveDrafts } from "./draft"
|
||||||
export { ExplicitPublish } from './explicit'
|
export { ExplicitPublish } from "./explicit"
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { GlobalConfiguration } from '../cfg'
|
import { GlobalConfiguration } from "../cfg"
|
||||||
import { QuartzComponent } from '../components/types'
|
import { QuartzComponent } from "../components/types"
|
||||||
import { StaticResources } from '../resources'
|
import { StaticResources } from "../resources"
|
||||||
import { joinStyles } from '../theme'
|
import { joinStyles } from "../theme"
|
||||||
import { EmitCallback, PluginTypes } from './types'
|
import { EmitCallback, PluginTypes } from "./types"
|
||||||
import styles from '../styles/base.scss'
|
import styles from "../styles/base.scss"
|
||||||
import { FilePath, ServerSlug } from '../path'
|
import { FilePath, ServerSlug } from "../path"
|
||||||
|
|
||||||
export type ComponentResources = {
|
export type ComponentResources = {
|
||||||
css: string[],
|
css: string[]
|
||||||
beforeDOMLoaded: string[],
|
beforeDOMLoaded: string[]
|
||||||
afterDOMLoaded: string[]
|
afterDOMLoaded: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ export function getComponentResources(plugins: PluginTypes): ComponentResources
|
||||||
const componentResources = {
|
const componentResources = {
|
||||||
css: new Set<string>(),
|
css: new Set<string>(),
|
||||||
beforeDOMLoaded: new Set<string>(),
|
beforeDOMLoaded: new Set<string>(),
|
||||||
afterDOMLoaded: new Set<string>()
|
afterDOMLoaded: new Set<string>(),
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const component of allComponents) {
|
for (const component of allComponents) {
|
||||||
|
@ -39,39 +39,42 @@ export function getComponentResources(plugins: PluginTypes): ComponentResources
|
||||||
componentResources.afterDOMLoaded.add(afterDOMLoaded)
|
componentResources.afterDOMLoaded.add(afterDOMLoaded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
css: [...componentResources.css],
|
css: [...componentResources.css],
|
||||||
beforeDOMLoaded: [...componentResources.beforeDOMLoaded],
|
beforeDOMLoaded: [...componentResources.beforeDOMLoaded],
|
||||||
afterDOMLoaded: [...componentResources.afterDOMLoaded]
|
afterDOMLoaded: [...componentResources.afterDOMLoaded],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function joinScripts(scripts: string[]): string {
|
function joinScripts(scripts: string[]): string {
|
||||||
// wrap with iife to prevent scope collision
|
// wrap with iife to prevent scope collision
|
||||||
return scripts.map(script => `(function () {${script}})();`).join("\n")
|
return scripts.map((script) => `(function () {${script}})();`).join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function emitComponentResources(cfg: GlobalConfiguration, res: ComponentResources, emit: EmitCallback): Promise<FilePath[]> {
|
export async function emitComponentResources(
|
||||||
|
cfg: GlobalConfiguration,
|
||||||
|
res: ComponentResources,
|
||||||
|
emit: EmitCallback,
|
||||||
|
): Promise<FilePath[]> {
|
||||||
const fps = await Promise.all([
|
const fps = await Promise.all([
|
||||||
emit({
|
emit({
|
||||||
slug: "index" as ServerSlug,
|
slug: "index" as ServerSlug,
|
||||||
ext: ".css",
|
ext: ".css",
|
||||||
content: joinStyles(cfg.theme, styles, ...res.css)
|
content: joinStyles(cfg.theme, styles, ...res.css),
|
||||||
}),
|
}),
|
||||||
emit({
|
emit({
|
||||||
slug: "prescript" as ServerSlug,
|
slug: "prescript" as ServerSlug,
|
||||||
ext: ".js",
|
ext: ".js",
|
||||||
content: joinScripts(res.beforeDOMLoaded)
|
content: joinScripts(res.beforeDOMLoaded),
|
||||||
}),
|
}),
|
||||||
emit({
|
emit({
|
||||||
slug: "postscript" as ServerSlug,
|
slug: "postscript" as ServerSlug,
|
||||||
ext: ".js",
|
ext: ".js",
|
||||||
content: joinScripts(res.afterDOMLoaded)
|
content: joinScripts(res.afterDOMLoaded),
|
||||||
})
|
}),
|
||||||
])
|
])
|
||||||
return fps
|
return fps
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getStaticResourcesFromPlugins(plugins: PluginTypes) {
|
export function getStaticResourcesFromPlugins(plugins: PluginTypes) {
|
||||||
|
@ -93,11 +96,11 @@ export function getStaticResourcesFromPlugins(plugins: PluginTypes) {
|
||||||
return staticResources
|
return staticResources
|
||||||
}
|
}
|
||||||
|
|
||||||
export * from './transformers'
|
export * from "./transformers"
|
||||||
export * from './filters'
|
export * from "./filters"
|
||||||
export * from './emitters'
|
export * from "./emitters"
|
||||||
|
|
||||||
declare module 'vfile' {
|
declare module "vfile" {
|
||||||
// inserted in processors.ts
|
// inserted in processors.ts
|
||||||
interface DataMap {
|
interface DataMap {
|
||||||
slug: ServerSlug
|
slug: ServerSlug
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Root as HTMLRoot } from 'hast'
|
import { Root as HTMLRoot } from "hast"
|
||||||
import { toString } from "hast-util-to-string"
|
import { toString } from "hast-util-to-string"
|
||||||
import { QuartzTransformerPlugin } from "../types"
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
|
|
||||||
|
@ -7,11 +7,16 @@ export interface Options {
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions: Options = {
|
const defaultOptions: Options = {
|
||||||
descriptionLength: 150
|
descriptionLength: 150,
|
||||||
}
|
}
|
||||||
|
|
||||||
const escapeHTML = (unsafe: string) => {
|
const escapeHTML = (unsafe: string) => {
|
||||||
return unsafe.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
return unsafe
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """)
|
||||||
|
.replaceAll("'", "'")
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Description: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
export const Description: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
||||||
|
@ -26,30 +31,29 @@ export const Description: QuartzTransformerPlugin<Partial<Options> | undefined>
|
||||||
const text = escapeHTML(toString(tree))
|
const text = escapeHTML(toString(tree))
|
||||||
|
|
||||||
const desc = frontMatterDescription ?? text
|
const desc = frontMatterDescription ?? text
|
||||||
const sentences = desc.replace(/\s+/g, ' ').split('.')
|
const sentences = desc.replace(/\s+/g, " ").split(".")
|
||||||
let finalDesc = ""
|
let finalDesc = ""
|
||||||
let sentenceIdx = 0
|
let sentenceIdx = 0
|
||||||
const len = opts.descriptionLength
|
const len = opts.descriptionLength
|
||||||
while (finalDesc.length < len) {
|
while (finalDesc.length < len) {
|
||||||
const sentence = sentences[sentenceIdx]
|
const sentence = sentences[sentenceIdx]
|
||||||
if (!sentence) break
|
if (!sentence) break
|
||||||
finalDesc += sentence + '.'
|
finalDesc += sentence + "."
|
||||||
sentenceIdx++
|
sentenceIdx++
|
||||||
}
|
}
|
||||||
|
|
||||||
file.data.description = finalDesc
|
file.data.description = finalDesc
|
||||||
file.data.text = text
|
file.data.text = text
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'vfile' {
|
declare module "vfile" {
|
||||||
interface DataMap {
|
interface DataMap {
|
||||||
description: string
|
description: string
|
||||||
text: string
|
text: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import matter from "gray-matter"
|
import matter from "gray-matter"
|
||||||
import remarkFrontmatter from 'remark-frontmatter'
|
import remarkFrontmatter from "remark-frontmatter"
|
||||||
import { QuartzTransformerPlugin } from "../types"
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
import yaml from 'js-yaml'
|
import yaml from "js-yaml"
|
||||||
import { slug as slugAnchor } from 'github-slugger'
|
import { slug as slugAnchor } from "github-slugger"
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
language: 'yaml' | 'toml',
|
language: "yaml" | "toml"
|
||||||
delims: string | string[]
|
delims: string | string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions: Options = {
|
const defaultOptions: Options = {
|
||||||
language: 'yaml',
|
language: "yaml",
|
||||||
delims: '---'
|
delims: "---",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
||||||
|
@ -26,8 +26,8 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined>
|
||||||
const { data } = matter(file.value, {
|
const { data } = matter(file.value, {
|
||||||
...opts,
|
...opts,
|
||||||
engines: {
|
engines: {
|
||||||
yaml: s => yaml.load(s, { schema: yaml.JSON_SCHEMA }) as object
|
yaml: (s) => yaml.load(s, { schema: yaml.JSON_SCHEMA }) as object,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// tag is an alias for tags
|
// tag is an alias for tags
|
||||||
|
@ -36,7 +36,10 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.tags && !Array.isArray(data.tags)) {
|
if (data.tags && !Array.isArray(data.tags)) {
|
||||||
data.tags = data.tags.toString().split(",").map((tag: string) => tag.trim())
|
data.tags = data.tags
|
||||||
|
.toString()
|
||||||
|
.split(",")
|
||||||
|
.map((tag: string) => tag.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
// slug them all!!
|
// slug them all!!
|
||||||
|
@ -46,16 +49,16 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined>
|
||||||
file.data.frontmatter = {
|
file.data.frontmatter = {
|
||||||
title: file.stem ?? "Untitled",
|
title: file.stem ?? "Untitled",
|
||||||
tags: [],
|
tags: [],
|
||||||
...data
|
...data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'vfile' {
|
declare module "vfile" {
|
||||||
interface DataMap {
|
interface DataMap {
|
||||||
frontmatter: { [key: string]: any } & {
|
frontmatter: { [key: string]: any } & {
|
||||||
title: string
|
title: string
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import remarkGfm from "remark-gfm"
|
import remarkGfm from "remark-gfm"
|
||||||
import smartypants from 'remark-smartypants'
|
import smartypants from "remark-smartypants"
|
||||||
import { QuartzTransformerPlugin } from "../types"
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
import rehypeSlug from "rehype-slug"
|
import rehypeSlug from "rehype-slug"
|
||||||
import rehypeAutolinkHeadings from "rehype-autolink-headings"
|
import rehypeAutolinkHeadings from "rehype-autolink-headings"
|
||||||
|
@ -11,10 +11,12 @@ export interface Options {
|
||||||
|
|
||||||
const defaultOptions: Options = {
|
const defaultOptions: Options = {
|
||||||
enableSmartyPants: true,
|
enableSmartyPants: true,
|
||||||
linkHeadings: true
|
linkHeadings: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (
|
||||||
|
userOpts,
|
||||||
|
) => {
|
||||||
const opts = { ...defaultOptions, ...userOpts }
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
return {
|
return {
|
||||||
name: "GitHubFlavoredMarkdown",
|
name: "GitHubFlavoredMarkdown",
|
||||||
|
@ -23,15 +25,22 @@ export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> |
|
||||||
},
|
},
|
||||||
htmlPlugins() {
|
htmlPlugins() {
|
||||||
if (opts.linkHeadings) {
|
if (opts.linkHeadings) {
|
||||||
return [rehypeSlug, [rehypeAutolinkHeadings, {
|
return [
|
||||||
behavior: 'append', content: {
|
rehypeSlug,
|
||||||
type: 'text',
|
[
|
||||||
value: ' §',
|
rehypeAutolinkHeadings,
|
||||||
}
|
{
|
||||||
}]]
|
behavior: "append",
|
||||||
|
content: {
|
||||||
|
type: "text",
|
||||||
|
value: " §",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
export { FrontMatter } from './frontmatter'
|
export { FrontMatter } from "./frontmatter"
|
||||||
export { GitHubFlavoredMarkdown } from './gfm'
|
export { GitHubFlavoredMarkdown } from "./gfm"
|
||||||
export { CreatedModifiedDate } from './lastmod'
|
export { CreatedModifiedDate } from "./lastmod"
|
||||||
export { Latex } from './latex'
|
export { Latex } from "./latex"
|
||||||
export { Description } from './description'
|
export { Description } from "./description"
|
||||||
export { CrawlLinks } from './links'
|
export { CrawlLinks } from "./links"
|
||||||
export { ObsidianFlavoredMarkdown } from './ofm'
|
export { ObsidianFlavoredMarkdown } from "./ofm"
|
||||||
export { SyntaxHighlighting } from './syntax'
|
export { SyntaxHighlighting } from "./syntax"
|
||||||
export { TableOfContents } from './toc'
|
export { TableOfContents } from "./toc"
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import path from 'path'
|
import path from "path"
|
||||||
import { Repository } from "@napi-rs/simple-git"
|
import { Repository } from "@napi-rs/simple-git"
|
||||||
import { QuartzTransformerPlugin } from "../types"
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
priority: ('frontmatter' | 'git' | 'filesystem')[],
|
priority: ("frontmatter" | "git" | "filesystem")[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions: Options = {
|
const defaultOptions: Options = {
|
||||||
priority: ['frontmatter', 'git', 'filesystem']
|
priority: ["frontmatter", "git", "filesystem"],
|
||||||
}
|
}
|
||||||
|
|
||||||
type MaybeDate = undefined | string | number
|
type MaybeDate = undefined | string | number
|
||||||
export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | undefined> = (
|
||||||
|
userOpts,
|
||||||
|
) => {
|
||||||
const opts = { ...defaultOptions, ...userOpts }
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
return {
|
return {
|
||||||
name: "CreatedModifiedDate",
|
name: "CreatedModifiedDate",
|
||||||
|
@ -51,13 +53,13 @@ export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | und
|
||||||
published: published ? new Date(published) : new Date(),
|
published: published ? new Date(published) : new Date(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'vfile' {
|
declare module "vfile" {
|
||||||
interface DataMap {
|
interface DataMap {
|
||||||
dates: {
|
dates: {
|
||||||
created: Date
|
created: Date
|
||||||
|
|
|
@ -1,43 +1,39 @@
|
||||||
import remarkMath from "remark-math"
|
import remarkMath from "remark-math"
|
||||||
import rehypeKatex from 'rehype-katex'
|
import rehypeKatex from "rehype-katex"
|
||||||
import rehypeMathjax from 'rehype-mathjax/svg.js'
|
import rehypeMathjax from "rehype-mathjax/svg.js"
|
||||||
import { QuartzTransformerPlugin } from "../types"
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
renderEngine: 'katex' | 'mathjax'
|
renderEngine: "katex" | "mathjax"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Latex: QuartzTransformerPlugin<Options> = (opts?: Options) => {
|
export const Latex: QuartzTransformerPlugin<Options> = (opts?: Options) => {
|
||||||
const engine = opts?.renderEngine ?? 'katex'
|
const engine = opts?.renderEngine ?? "katex"
|
||||||
return {
|
return {
|
||||||
name: "Latex",
|
name: "Latex",
|
||||||
markdownPlugins() {
|
markdownPlugins() {
|
||||||
return [remarkMath]
|
return [remarkMath]
|
||||||
},
|
},
|
||||||
htmlPlugins() {
|
htmlPlugins() {
|
||||||
return [
|
return [engine === "katex" ? [rehypeKatex, { output: "html" }] : [rehypeMathjax]]
|
||||||
engine === 'katex'
|
|
||||||
? [rehypeKatex, { output: 'html' }]
|
|
||||||
: [rehypeMathjax]
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
externalResources() {
|
externalResources() {
|
||||||
return engine === 'katex'
|
return engine === "katex"
|
||||||
? {
|
? {
|
||||||
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.0/dist/katex.min.css",
|
||||||
],
|
],
|
||||||
js: [
|
js: [
|
||||||
{
|
{
|
||||||
// fix copy behaviour: https://github.com/KaTeX/KaTeX/blob/main/contrib/copy-tex/README.md
|
// fix copy behaviour: https://github.com/KaTeX/KaTeX/blob/main/contrib/copy-tex/README.md
|
||||||
src: "https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/contrib/copy-tex.min.js",
|
src: "https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/contrib/copy-tex.min.js",
|
||||||
loadTime: "afterDOMReady",
|
loadTime: "afterDOMReady",
|
||||||
contentType: 'external'
|
contentType: "external",
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
: {}
|
: {}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,27 @@
|
||||||
import { QuartzTransformerPlugin } from "../types"
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
import { CanonicalSlug, RelativeURL, canonicalizeServer, joinSegments, pathToRoot, resolveRelative, splitAnchor, transformInternalLink } from "../../path"
|
import {
|
||||||
|
CanonicalSlug,
|
||||||
|
RelativeURL,
|
||||||
|
canonicalizeServer,
|
||||||
|
joinSegments,
|
||||||
|
pathToRoot,
|
||||||
|
resolveRelative,
|
||||||
|
splitAnchor,
|
||||||
|
transformInternalLink,
|
||||||
|
} from "../../path"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { visit } from 'unist-util-visit'
|
import { visit } from "unist-util-visit"
|
||||||
import isAbsoluteUrl from "is-absolute-url"
|
import isAbsoluteUrl from "is-absolute-url"
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
/** How to resolve Markdown paths */
|
/** How to resolve Markdown paths */
|
||||||
markdownLinkResolution: 'absolute' | 'relative' | 'shortest'
|
markdownLinkResolution: "absolute" | "relative" | "shortest"
|
||||||
/** Strips folders from a link so that it looks nice */
|
/** Strips folders from a link so that it looks nice */
|
||||||
prettyLinks: boolean
|
prettyLinks: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions: Options = {
|
const defaultOptions: Options = {
|
||||||
markdownLinkResolution: 'absolute',
|
markdownLinkResolution: "absolute",
|
||||||
prettyLinks: true,
|
prettyLinks: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,84 +30,91 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
|
||||||
return {
|
return {
|
||||||
name: "LinkProcessing",
|
name: "LinkProcessing",
|
||||||
htmlPlugins() {
|
htmlPlugins() {
|
||||||
return [() => {
|
return [
|
||||||
return (tree, file) => {
|
() => {
|
||||||
const curSlug = canonicalizeServer(file.data.slug!)
|
return (tree, file) => {
|
||||||
const transformLink = (target: string): RelativeURL => {
|
const curSlug = canonicalizeServer(file.data.slug!)
|
||||||
const targetSlug = transformInternalLink(target).slice("./".length)
|
const transformLink = (target: string): RelativeURL => {
|
||||||
let [targetCanonical, targetAnchor] = splitAnchor(targetSlug)
|
const targetSlug = transformInternalLink(target).slice("./".length)
|
||||||
if (opts.markdownLinkResolution === 'relative') {
|
let [targetCanonical, targetAnchor] = splitAnchor(targetSlug)
|
||||||
return targetSlug as RelativeURL
|
if (opts.markdownLinkResolution === "relative") {
|
||||||
} else if (opts.markdownLinkResolution === 'shortest') {
|
return targetSlug as RelativeURL
|
||||||
// https://forum.obsidian.md/t/settings-new-link-format-what-is-shortest-path-when-possible/6748/5
|
} else if (opts.markdownLinkResolution === "shortest") {
|
||||||
const allSlugs = file.data.allSlugs!
|
// https://forum.obsidian.md/t/settings-new-link-format-what-is-shortest-path-when-possible/6748/5
|
||||||
|
const allSlugs = file.data.allSlugs!
|
||||||
|
|
||||||
// if the file name is unique, then it's just the filename
|
// if the file name is unique, then it's just the filename
|
||||||
const matchingFileNames = allSlugs.filter(slug => {
|
const matchingFileNames = allSlugs.filter((slug) => {
|
||||||
const parts = slug.split(path.posix.sep)
|
const parts = slug.split(path.posix.sep)
|
||||||
const fileName = parts.at(-1)
|
const fileName = parts.at(-1)
|
||||||
return targetCanonical === fileName
|
return targetCanonical === fileName
|
||||||
})
|
})
|
||||||
|
|
||||||
if (matchingFileNames.length === 1) {
|
if (matchingFileNames.length === 1) {
|
||||||
const targetSlug = canonicalizeServer(matchingFileNames[0])
|
const targetSlug = canonicalizeServer(matchingFileNames[0])
|
||||||
return resolveRelative(curSlug, targetSlug) + targetAnchor as RelativeURL
|
return (resolveRelative(curSlug, targetSlug) + targetAnchor) as RelativeURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it's not unique, then it's the absolute path from the vault root
|
||||||
|
// (fall-through case)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if it's not unique, then it's the absolute path from the vault root
|
// treat as absolute
|
||||||
// (fall-through case)
|
return joinSegments(pathToRoot(curSlug), targetSlug) as RelativeURL
|
||||||
}
|
}
|
||||||
|
|
||||||
// treat as absolute
|
const outgoing: Set<CanonicalSlug> = new Set()
|
||||||
return joinSegments(pathToRoot(curSlug), targetSlug) as RelativeURL
|
visit(tree, "element", (node, _index, _parent) => {
|
||||||
|
// rewrite all links
|
||||||
|
if (
|
||||||
|
node.tagName === "a" &&
|
||||||
|
node.properties &&
|
||||||
|
typeof node.properties.href === "string"
|
||||||
|
) {
|
||||||
|
let dest = node.properties.href as RelativeURL
|
||||||
|
node.properties.className = isAbsoluteUrl(dest) ? "external" : "internal"
|
||||||
|
|
||||||
|
// don't process external links or intra-document anchors
|
||||||
|
if (!(isAbsoluteUrl(dest) || dest.startsWith("#"))) {
|
||||||
|
dest = node.properties.href = transformLink(dest)
|
||||||
|
const canonicalDest = path.normalize(joinSegments(curSlug, dest))
|
||||||
|
const [destCanonical, _destAnchor] = splitAnchor(canonicalDest)
|
||||||
|
outgoing.add(destCanonical as CanonicalSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rewrite link internals if prettylinks is on
|
||||||
|
if (
|
||||||
|
opts.prettyLinks &&
|
||||||
|
node.children.length === 1 &&
|
||||||
|
node.children[0].type === "text"
|
||||||
|
) {
|
||||||
|
node.children[0].value = path.basename(node.children[0].value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// transform all other resources that may use links
|
||||||
|
if (
|
||||||
|
["img", "video", "audio", "iframe"].includes(node.tagName) &&
|
||||||
|
node.properties &&
|
||||||
|
typeof node.properties.src === "string"
|
||||||
|
) {
|
||||||
|
if (!isAbsoluteUrl(node.properties.src)) {
|
||||||
|
const ext = path.extname(node.properties.src)
|
||||||
|
node.properties.src =
|
||||||
|
transformLink(path.join("assets", node.properties.src)) + ext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
file.data.links = [...outgoing]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
const outgoing: Set<CanonicalSlug> = new Set()
|
]
|
||||||
visit(tree, 'element', (node, _index, _parent) => {
|
},
|
||||||
// rewrite all links
|
|
||||||
if (
|
|
||||||
node.tagName === 'a' &&
|
|
||||||
node.properties &&
|
|
||||||
typeof node.properties.href === 'string'
|
|
||||||
) {
|
|
||||||
let dest = node.properties.href as RelativeURL
|
|
||||||
node.properties.className = isAbsoluteUrl(dest) ? "external" : "internal"
|
|
||||||
|
|
||||||
// don't process external links or intra-document anchors
|
|
||||||
if (!(isAbsoluteUrl(dest) || dest.startsWith("#"))) {
|
|
||||||
dest = node.properties.href = transformLink(dest)
|
|
||||||
const canonicalDest = path.normalize(joinSegments(curSlug, dest))
|
|
||||||
const [destCanonical, _destAnchor] = splitAnchor(canonicalDest)
|
|
||||||
outgoing.add(destCanonical as CanonicalSlug)
|
|
||||||
}
|
|
||||||
|
|
||||||
// rewrite link internals if prettylinks is on
|
|
||||||
if (opts.prettyLinks && node.children.length === 1 && node.children[0].type === 'text') {
|
|
||||||
node.children[0].value = path.basename(node.children[0].value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// transform all other resources that may use links
|
|
||||||
if (
|
|
||||||
["img", "video", "audio", "iframe"].includes(node.tagName) &&
|
|
||||||
node.properties &&
|
|
||||||
typeof node.properties.src === 'string'
|
|
||||||
) {
|
|
||||||
if (!isAbsoluteUrl(node.properties.src)) {
|
|
||||||
const ext = path.extname(node.properties.src)
|
|
||||||
node.properties.src = transformLink(path.join("assets", node.properties.src)) + ext
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
file.data.links = [...outgoing]
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'vfile' {
|
declare module "vfile" {
|
||||||
interface DataMap {
|
interface DataMap {
|
||||||
links: CanonicalSlug[]
|
links: CanonicalSlug[]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { PluggableList } from "unified"
|
import { PluggableList } from "unified"
|
||||||
import { QuartzTransformerPlugin } from "../types"
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
import { Root, HTML, BlockContent, DefinitionContent, Code } from 'mdast'
|
import { Root, HTML, BlockContent, DefinitionContent, Code } from "mdast"
|
||||||
import { findAndReplace } from "mdast-util-find-and-replace"
|
import { findAndReplace } from "mdast-util-find-and-replace"
|
||||||
import { slug as slugAnchor } from 'github-slugger'
|
import { slug as slugAnchor } from "github-slugger"
|
||||||
import rehypeRaw from "rehype-raw"
|
import rehypeRaw from "rehype-raw"
|
||||||
import { visit } from "unist-util-visit"
|
import { visit } from "unist-util-visit"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
@ -71,7 +71,7 @@ function canonicalizeCallout(calloutName: string): keyof typeof callouts {
|
||||||
bug: "bug",
|
bug: "bug",
|
||||||
example: "example",
|
example: "example",
|
||||||
quote: "quote",
|
quote: "quote",
|
||||||
cite: "quote"
|
cite: "quote",
|
||||||
}
|
}
|
||||||
|
|
||||||
return calloutMapping[callout]
|
return calloutMapping[callout]
|
||||||
|
@ -94,10 +94,10 @@ const callouts = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const capitalize = (s: string): string => {
|
const capitalize = (s: string): string => {
|
||||||
return s.substring(0, 1).toUpperCase() + s.substring(1);
|
return s.substring(0, 1).toUpperCase() + s.substring(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match wikilinks
|
// Match wikilinks
|
||||||
// !? -> optional embedding
|
// !? -> optional embedding
|
||||||
// \[\[ -> open brace
|
// \[\[ -> open brace
|
||||||
// ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name)
|
// ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name)
|
||||||
|
@ -105,16 +105,18 @@ const capitalize = (s: string): string => {
|
||||||
// (|[^\[\]\|\#]+)? -> | then one or more non-special characters (alias)
|
// (|[^\[\]\|\#]+)? -> | then one or more non-special characters (alias)
|
||||||
const wikilinkRegex = new RegExp(/!?\[\[([^\[\]\|\#]+)(#[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/, "g")
|
const wikilinkRegex = new RegExp(/!?\[\[([^\[\]\|\#]+)(#[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/, "g")
|
||||||
|
|
||||||
// Match highlights
|
// Match highlights
|
||||||
const highlightRegex = new RegExp(/==(.+)==/, "g")
|
const highlightRegex = new RegExp(/==(.+)==/, "g")
|
||||||
|
|
||||||
// Match comments
|
// Match comments
|
||||||
const commentRegex = new RegExp(/%%(.+)%%/, "g")
|
const commentRegex = new RegExp(/%%(.+)%%/, "g")
|
||||||
|
|
||||||
// from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts
|
// from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts
|
||||||
const calloutRegex = new RegExp(/^\[\!(\w+)\]([+-]?)/)
|
const calloutRegex = new RegExp(/^\[\!(\w+)\]([+-]?)/)
|
||||||
|
|
||||||
export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (
|
||||||
|
userOpts,
|
||||||
|
) => {
|
||||||
const opts = { ...defaultOptions, ...userOpts }
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
return {
|
return {
|
||||||
name: "ObsidianFlavoredMarkdown",
|
name: "ObsidianFlavoredMarkdown",
|
||||||
|
@ -154,28 +156,31 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
||||||
width ||= "auto"
|
width ||= "auto"
|
||||||
height ||= "auto"
|
height ||= "auto"
|
||||||
return {
|
return {
|
||||||
type: 'image',
|
type: "image",
|
||||||
url,
|
url,
|
||||||
data: {
|
data: {
|
||||||
hProperties: {
|
hProperties: {
|
||||||
width, height
|
width,
|
||||||
}
|
height,
|
||||||
}
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
} else if ([".mp4", ".webm", ".ogv", ".mov", ".mkv"].includes(ext)) {
|
} else if ([".mp4", ".webm", ".ogv", ".mov", ".mkv"].includes(ext)) {
|
||||||
return {
|
return {
|
||||||
type: 'html',
|
type: "html",
|
||||||
value: `<video src="${url}" controls></video>`
|
value: `<video src="${url}" controls></video>`,
|
||||||
}
|
}
|
||||||
} else if ([".mp3", ".webm", ".wav", ".m4a", ".ogg", ".3gp", ".flac"].includes(ext)) {
|
} else if (
|
||||||
|
[".mp3", ".webm", ".wav", ".m4a", ".ogg", ".3gp", ".flac"].includes(ext)
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
type: 'html',
|
type: "html",
|
||||||
value: `<audio src="${url}" controls></audio>`
|
value: `<audio src="${url}" controls></audio>`,
|
||||||
}
|
}
|
||||||
} else if ([".pdf"].includes(ext)) {
|
} else if ([".pdf"].includes(ext)) {
|
||||||
return {
|
return {
|
||||||
type: 'html',
|
type: "html",
|
||||||
value: `<iframe src="${url}"></iframe>`
|
value: `<iframe src="${url}"></iframe>`,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// TODO: this is the node embed case
|
// TODO: this is the node embed case
|
||||||
|
@ -187,17 +192,18 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
||||||
// const url = transformInternalLink(fp + anchor)
|
// const url = transformInternalLink(fp + anchor)
|
||||||
const url = fp + anchor
|
const url = fp + anchor
|
||||||
return {
|
return {
|
||||||
type: 'link',
|
type: "link",
|
||||||
url,
|
url,
|
||||||
children: [{
|
children: [
|
||||||
type: 'text',
|
{
|
||||||
value: alias ?? fp
|
type: "text",
|
||||||
}]
|
value: alias ?? fp,
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.highlight) {
|
if (opts.highlight) {
|
||||||
|
@ -206,21 +212,21 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
||||||
findAndReplace(tree, highlightRegex, (_value: string, ...capture: string[]) => {
|
findAndReplace(tree, highlightRegex, (_value: string, ...capture: string[]) => {
|
||||||
const [inner] = capture
|
const [inner] = capture
|
||||||
return {
|
return {
|
||||||
type: 'html',
|
type: "html",
|
||||||
value: `<span class="text-highlight">${inner}</span>`
|
value: `<span class="text-highlight">${inner}</span>`,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.comments) {
|
if (opts.comments) {
|
||||||
plugins.push(() => {
|
plugins.push(() => {
|
||||||
return (tree: Root, _file) => {
|
return (tree: Root, _file) => {
|
||||||
findAndReplace(tree, commentRegex, (_value: string, ..._capture: string[]) => {
|
findAndReplace(tree, commentRegex, (_value: string, ..._capture: string[]) => {
|
||||||
return {
|
return {
|
||||||
type: 'text',
|
type: "text",
|
||||||
value: ''
|
value: "",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -252,7 +258,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
||||||
const calloutType = typeString.toLowerCase() as keyof typeof callouts
|
const calloutType = typeString.toLowerCase() as keyof typeof callouts
|
||||||
const collapse = collapseChar === "+" || collapseChar === "-"
|
const collapse = collapseChar === "+" || collapseChar === "-"
|
||||||
const defaultState = collapseChar === "-" ? "collapsed" : "expanded"
|
const defaultState = collapseChar === "-" ? "collapsed" : "expanded"
|
||||||
const title = match.input.slice(calloutDirective.length).trim() || capitalize(calloutType)
|
const title =
|
||||||
|
match.input.slice(calloutDirective.length).trim() || capitalize(calloutType)
|
||||||
|
|
||||||
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 = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="fold">
|
||||||
<polyline points="6 9 12 15 18 9"></polyline>
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
|
@ -266,17 +273,20 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
||||||
<div class="callout-icon">${callouts[canonicalizeCallout(calloutType)]}</div>
|
<div class="callout-icon">${callouts[canonicalizeCallout(calloutType)]}</div>
|
||||||
<div class="callout-title-inner">${title}</div>
|
<div class="callout-title-inner">${title}</div>
|
||||||
${collapse ? toggleIcon : ""}
|
${collapse ? toggleIcon : ""}
|
||||||
</div>`
|
</div>`,
|
||||||
}
|
}
|
||||||
|
|
||||||
const blockquoteContent: (BlockContent | DefinitionContent)[] = [titleNode]
|
const blockquoteContent: (BlockContent | DefinitionContent)[] = [titleNode]
|
||||||
if (remainingText.length > 0) {
|
if (remainingText.length > 0) {
|
||||||
blockquoteContent.push({
|
blockquoteContent.push({
|
||||||
type: 'paragraph',
|
type: "paragraph",
|
||||||
children: [{
|
children: [
|
||||||
type: 'text',
|
{
|
||||||
value: remainingText,
|
type: "text",
|
||||||
}, ...restChildren]
|
value: remainingText,
|
||||||
|
},
|
||||||
|
...restChildren,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,10 +297,12 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
||||||
node.data = {
|
node.data = {
|
||||||
hProperties: {
|
hProperties: {
|
||||||
...(node.data?.hProperties ?? {}),
|
...(node.data?.hProperties ?? {}),
|
||||||
className: `callout ${collapse ? "is-collapsible" : ""} ${defaultState === "collapsed" ? "is-collapsed" : ""}`,
|
className: `callout ${collapse ? "is-collapsible" : ""} ${
|
||||||
|
defaultState === "collapsed" ? "is-collapsed" : ""
|
||||||
|
}`,
|
||||||
"data-callout": calloutType,
|
"data-callout": calloutType,
|
||||||
"data-callout-fold": collapse,
|
"data-callout-fold": collapse,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -301,12 +313,12 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
||||||
if (opts.mermaid) {
|
if (opts.mermaid) {
|
||||||
plugins.push(() => {
|
plugins.push(() => {
|
||||||
return (tree: Root, _file) => {
|
return (tree: Root, _file) => {
|
||||||
visit(tree, 'code', (node: Code) => {
|
visit(tree, "code", (node: Code) => {
|
||||||
if (node.lang === 'mermaid') {
|
if (node.lang === "mermaid") {
|
||||||
node.data = {
|
node.data = {
|
||||||
hProperties: {
|
hProperties: {
|
||||||
className: 'mermaid'
|
className: "mermaid",
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -325,8 +337,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
||||||
if (opts.callouts) {
|
if (opts.callouts) {
|
||||||
js.push({
|
js.push({
|
||||||
script: calloutScript,
|
script: calloutScript,
|
||||||
loadTime: 'afterDOMReady',
|
loadTime: "afterDOMReady",
|
||||||
contentType: 'inline'
|
contentType: "inline",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -336,13 +348,13 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
||||||
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs';
|
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs';
|
||||||
mermaid.initialize({ startOnLoad: true });
|
mermaid.initialize({ startOnLoad: true });
|
||||||
`,
|
`,
|
||||||
loadTime: 'afterDOMReady',
|
loadTime: "afterDOMReady",
|
||||||
moduleType: 'module',
|
moduleType: "module",
|
||||||
contentType: 'inline'
|
contentType: "inline",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return { js }
|
return { js }
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,13 @@ import rehypePrettyCode, { Options as CodeOptions } from "rehype-pretty-code"
|
||||||
export const SyntaxHighlighting: QuartzTransformerPlugin = () => ({
|
export const SyntaxHighlighting: QuartzTransformerPlugin = () => ({
|
||||||
name: "SyntaxHighlighting",
|
name: "SyntaxHighlighting",
|
||||||
htmlPlugins() {
|
htmlPlugins() {
|
||||||
return [[rehypePrettyCode, {
|
return [
|
||||||
theme: 'css-variables',
|
[
|
||||||
} satisfies Partial<CodeOptions>]]
|
rehypePrettyCode,
|
||||||
}
|
{
|
||||||
|
theme: "css-variables",
|
||||||
|
} satisfies Partial<CodeOptions>,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,11 +2,11 @@ import { QuartzTransformerPlugin } from "../types"
|
||||||
import { Root } from "mdast"
|
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 { slug as slugAnchor } from 'github-slugger'
|
import { slug as slugAnchor } from "github-slugger"
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
maxDepth: 1 | 2 | 3 | 4 | 5 | 6,
|
maxDepth: 1 | 2 | 3 | 4 | 5 | 6
|
||||||
minEntries: 1,
|
minEntries: 1
|
||||||
showByDefault: boolean
|
showByDefault: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,47 +17,53 @@ const defaultOptions: Options = {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TocEntry {
|
interface TocEntry {
|
||||||
depth: number,
|
depth: number
|
||||||
text: string,
|
text: string
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefined> = (
|
||||||
|
userOpts,
|
||||||
|
) => {
|
||||||
const opts = { ...defaultOptions, ...userOpts }
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
return {
|
return {
|
||||||
name: "TableOfContents",
|
name: "TableOfContents",
|
||||||
markdownPlugins() {
|
markdownPlugins() {
|
||||||
return [() => {
|
return [
|
||||||
return async (tree: Root, file) => {
|
() => {
|
||||||
const display = file.data.frontmatter?.enableToc ?? opts.showByDefault
|
return async (tree: Root, file) => {
|
||||||
if (display) {
|
const display = file.data.frontmatter?.enableToc ?? opts.showByDefault
|
||||||
const toc: TocEntry[] = []
|
if (display) {
|
||||||
let highestDepth: number = opts.maxDepth
|
const toc: TocEntry[] = []
|
||||||
visit(tree, 'heading', (node) => {
|
let highestDepth: number = opts.maxDepth
|
||||||
if (node.depth <= opts.maxDepth) {
|
visit(tree, "heading", (node) => {
|
||||||
const text = toString(node)
|
if (node.depth <= opts.maxDepth) {
|
||||||
highestDepth = Math.min(highestDepth, node.depth)
|
const text = toString(node)
|
||||||
toc.push({
|
highestDepth = Math.min(highestDepth, node.depth)
|
||||||
depth: node.depth,
|
toc.push({
|
||||||
text,
|
depth: node.depth,
|
||||||
slug: slugAnchor(text)
|
text,
|
||||||
})
|
slug: slugAnchor(text),
|
||||||
}
|
})
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (toc.length > opts.minEntries) {
|
if (toc.length > opts.minEntries) {
|
||||||
file.data.toc = toc.map(entry => ({ ...entry, depth: entry.depth - highestDepth }))
|
file.data.toc = toc.map((entry) => ({
|
||||||
|
...entry,
|
||||||
|
depth: entry.depth - highestDepth,
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}]
|
]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'vfile' {
|
declare module "vfile" {
|
||||||
interface DataMap {
|
interface DataMap {
|
||||||
toc: TocEntry[]
|
toc: TocEntry[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,13 +6,15 @@ import { QuartzComponent } from "../components/types"
|
||||||
import { FilePath, ServerSlug } from "../path"
|
import { FilePath, ServerSlug } from "../path"
|
||||||
|
|
||||||
export interface PluginTypes {
|
export interface PluginTypes {
|
||||||
transformers: QuartzTransformerPluginInstance[],
|
transformers: QuartzTransformerPluginInstance[]
|
||||||
filters: QuartzFilterPluginInstance[],
|
filters: QuartzFilterPluginInstance[]
|
||||||
emitters: QuartzEmitterPluginInstance[],
|
emitters: QuartzEmitterPluginInstance[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type OptionType = object | undefined
|
type OptionType = object | undefined
|
||||||
export type QuartzTransformerPlugin<Options extends OptionType = undefined> = (opts?: Options) => QuartzTransformerPluginInstance
|
export type QuartzTransformerPlugin<Options extends OptionType = undefined> = (
|
||||||
|
opts?: Options,
|
||||||
|
) => QuartzTransformerPluginInstance
|
||||||
export type QuartzTransformerPluginInstance = {
|
export type QuartzTransformerPluginInstance = {
|
||||||
name: string
|
name: string
|
||||||
textTransform?: (src: string | Buffer) => string | Buffer
|
textTransform?: (src: string | Buffer) => string | Buffer
|
||||||
|
@ -21,16 +23,26 @@ export type QuartzTransformerPluginInstance = {
|
||||||
externalResources?: () => Partial<StaticResources>
|
externalResources?: () => Partial<StaticResources>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QuartzFilterPlugin<Options extends OptionType = undefined> = (opts?: Options) => QuartzFilterPluginInstance
|
export type QuartzFilterPlugin<Options extends OptionType = undefined> = (
|
||||||
|
opts?: Options,
|
||||||
|
) => QuartzFilterPluginInstance
|
||||||
export type QuartzFilterPluginInstance = {
|
export type QuartzFilterPluginInstance = {
|
||||||
name: string
|
name: string
|
||||||
shouldPublish(content: ProcessedContent): boolean
|
shouldPublish(content: ProcessedContent): boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QuartzEmitterPlugin<Options extends OptionType = undefined> = (opts?: Options) => QuartzEmitterPluginInstance
|
export type QuartzEmitterPlugin<Options extends OptionType = undefined> = (
|
||||||
|
opts?: Options,
|
||||||
|
) => QuartzEmitterPluginInstance
|
||||||
export type QuartzEmitterPluginInstance = {
|
export type QuartzEmitterPluginInstance = {
|
||||||
name: string
|
name: string
|
||||||
emit(contentDir: string, cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emitCallback: EmitCallback): Promise<FilePath[]>
|
emit(
|
||||||
|
contentDir: string,
|
||||||
|
cfg: GlobalConfiguration,
|
||||||
|
content: ProcessedContent[],
|
||||||
|
resources: StaticResources,
|
||||||
|
emitCallback: EmitCallback,
|
||||||
|
): Promise<FilePath[]>
|
||||||
getQuartzComponents(): QuartzComponent[]
|
getQuartzComponents(): QuartzComponent[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Node, Parent } from 'hast'
|
import { Node, Parent } from "hast"
|
||||||
import { Data, VFile } from 'vfile'
|
import { Data, VFile } from "vfile"
|
||||||
|
|
||||||
export type QuartzPluginData = Data
|
export type QuartzPluginData = Data
|
||||||
export type ProcessedContent = [Node<QuartzPluginData>, VFile]
|
export type ProcessedContent = [Node<QuartzPluginData>, VFile]
|
||||||
|
|
||||||
export function defaultProcessedContent(vfileData: Partial<QuartzPluginData>): ProcessedContent {
|
export function defaultProcessedContent(vfileData: Partial<QuartzPluginData>): ProcessedContent {
|
||||||
const root: Parent = { type: 'root', children: [] }
|
const root: Parent = { type: "root", children: [] }
|
||||||
const vfile = new VFile("")
|
const vfile = new VFile("")
|
||||||
vfile.data = vfileData
|
vfile.data = vfileData
|
||||||
return [root, vfile]
|
return [root, vfile]
|
||||||
|
|
|
@ -2,25 +2,35 @@ import path from "path"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import { GlobalConfiguration, QuartzConfig } from "../cfg"
|
import { GlobalConfiguration, QuartzConfig } from "../cfg"
|
||||||
import { PerfTimer } from "../perf"
|
import { PerfTimer } from "../perf"
|
||||||
import { ComponentResources, emitComponentResources, getComponentResources, getStaticResourcesFromPlugins } from "../plugins"
|
import {
|
||||||
|
ComponentResources,
|
||||||
|
emitComponentResources,
|
||||||
|
getComponentResources,
|
||||||
|
getStaticResourcesFromPlugins,
|
||||||
|
} from "../plugins"
|
||||||
import { EmitCallback } from "../plugins/types"
|
import { EmitCallback } from "../plugins/types"
|
||||||
import { ProcessedContent } from "../plugins/vfile"
|
import { ProcessedContent } from "../plugins/vfile"
|
||||||
import { FilePath, QUARTZ, slugifyFilePath } from "../path"
|
import { FilePath, QUARTZ, slugifyFilePath } from "../path"
|
||||||
import { globbyStream } from "globby"
|
import { globbyStream } from "globby"
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import spaRouterScript from '../components/scripts/spa.inline'
|
import spaRouterScript from "../components/scripts/spa.inline"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import plausibleScript from '../components/scripts/plausible.inline'
|
import plausibleScript from "../components/scripts/plausible.inline"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import popoverScript from '../components/scripts/popover.inline'
|
import popoverScript from "../components/scripts/popover.inline"
|
||||||
import popoverStyle from '../components/styles/popover.scss'
|
import popoverStyle from "../components/styles/popover.scss"
|
||||||
import { StaticResources } from "../resources"
|
import { StaticResources } from "../resources"
|
||||||
import { QuartzLogger } from "../log"
|
import { QuartzLogger } from "../log"
|
||||||
import { googleFontHref } from "../theme"
|
import { googleFontHref } from "../theme"
|
||||||
import { trace } from "../trace"
|
import { trace } from "../trace"
|
||||||
|
|
||||||
function addGlobalPageResources(cfg: GlobalConfiguration, reloadScript: boolean, staticResources: StaticResources, componentResources: ComponentResources) {
|
function addGlobalPageResources(
|
||||||
|
cfg: GlobalConfiguration,
|
||||||
|
reloadScript: boolean,
|
||||||
|
staticResources: StaticResources,
|
||||||
|
componentResources: ComponentResources,
|
||||||
|
) {
|
||||||
staticResources.css.push(googleFontHref(cfg.theme))
|
staticResources.css.push(googleFontHref(cfg.theme))
|
||||||
|
|
||||||
// popovers
|
// popovers
|
||||||
|
@ -33,8 +43,8 @@ function addGlobalPageResources(cfg: GlobalConfiguration, reloadScript: boolean,
|
||||||
const tagId = cfg.analytics.tagId
|
const tagId = cfg.analytics.tagId
|
||||||
staticResources.js.push({
|
staticResources.js.push({
|
||||||
src: `https://www.googletagmanager.com/gtag/js?id=${tagId}`,
|
src: `https://www.googletagmanager.com/gtag/js?id=${tagId}`,
|
||||||
contentType: 'external',
|
contentType: "external",
|
||||||
loadTime: 'afterDOMReady',
|
loadTime: "afterDOMReady",
|
||||||
})
|
})
|
||||||
componentResources.afterDOMLoaded.push(`
|
componentResources.afterDOMLoaded.push(`
|
||||||
window.dataLayer = window.dataLayer || [];
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
@ -47,8 +57,7 @@ function addGlobalPageResources(cfg: GlobalConfiguration, reloadScript: boolean,
|
||||||
page_title: document.title,
|
page_title: document.title,
|
||||||
page_location: location.href,
|
page_location: location.href,
|
||||||
});
|
});
|
||||||
});`
|
});`)
|
||||||
)
|
|
||||||
} else if (cfg.analytics?.provider === "plausible") {
|
} else if (cfg.analytics?.provider === "plausible") {
|
||||||
componentResources.afterDOMLoaded.push(plausibleScript)
|
componentResources.afterDOMLoaded.push(plausibleScript)
|
||||||
}
|
}
|
||||||
|
@ -60,8 +69,7 @@ function addGlobalPageResources(cfg: GlobalConfiguration, reloadScript: boolean,
|
||||||
componentResources.afterDOMLoaded.push(`
|
componentResources.afterDOMLoaded.push(`
|
||||||
window.spaNavigate = (url, _) => window.location.assign(url)
|
window.spaNavigate = (url, _) => window.location.assign(url)
|
||||||
const event = new CustomEvent("nav", { detail: { slug: document.body.dataset.slug } })
|
const event = new CustomEvent("nav", { detail: { slug: document.body.dataset.slug } })
|
||||||
document.dispatchEvent(event)`
|
document.dispatchEvent(event)`)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reloadScript) {
|
if (reloadScript) {
|
||||||
|
@ -71,12 +79,19 @@ function addGlobalPageResources(cfg: GlobalConfiguration, reloadScript: boolean,
|
||||||
script: `
|
script: `
|
||||||
const socket = new WebSocket('ws://localhost:3001')
|
const socket = new WebSocket('ws://localhost:3001')
|
||||||
socket.addEventListener('message', () => document.location.reload())
|
socket.addEventListener('message', () => document.location.reload())
|
||||||
`
|
`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function emitContent(contentFolder: string, output: string, cfg: QuartzConfig, content: ProcessedContent[], reloadScript: boolean, verbose: boolean) {
|
export async function emitContent(
|
||||||
|
contentFolder: string,
|
||||||
|
output: string,
|
||||||
|
cfg: QuartzConfig,
|
||||||
|
content: ProcessedContent[],
|
||||||
|
reloadScript: boolean,
|
||||||
|
verbose: boolean,
|
||||||
|
) {
|
||||||
const perf = new PerfTimer()
|
const perf = new PerfTimer()
|
||||||
const log = new QuartzLogger(verbose)
|
const log = new QuartzLogger(verbose)
|
||||||
|
|
||||||
|
@ -95,8 +110,8 @@ export async function emitContent(contentFolder: string, output: string, cfg: Qu
|
||||||
// component specific scripts and styles
|
// component specific scripts and styles
|
||||||
const componentResources = getComponentResources(cfg.plugins)
|
const componentResources = getComponentResources(cfg.plugins)
|
||||||
|
|
||||||
// important that this goes *after* component scripts
|
// important that this goes *after* component scripts
|
||||||
// as the "nav" event gets triggered here and we should make sure
|
// as the "nav" event gets triggered here and we should make sure
|
||||||
// that everyone else had the chance to register a listener for it
|
// that everyone else had the chance to register a listener for it
|
||||||
addGlobalPageResources(cfg.configuration, reloadScript, staticResources, componentResources)
|
addGlobalPageResources(cfg.configuration, reloadScript, staticResources, componentResources)
|
||||||
|
|
||||||
|
@ -112,7 +127,13 @@ export async function emitContent(contentFolder: string, output: string, cfg: Qu
|
||||||
// emitter plugins
|
// emitter plugins
|
||||||
for (const emitter of cfg.plugins.emitters) {
|
for (const emitter of cfg.plugins.emitters) {
|
||||||
try {
|
try {
|
||||||
const emitted = await emitter.emit(contentFolder, cfg.configuration, content, staticResources, emit)
|
const emitted = await emitter.emit(
|
||||||
|
contentFolder,
|
||||||
|
cfg.configuration,
|
||||||
|
content,
|
||||||
|
staticResources,
|
||||||
|
emit,
|
||||||
|
)
|
||||||
emittedFiles += emitted.length
|
emittedFiles += emitted.length
|
||||||
|
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
|
@ -141,7 +162,7 @@ export async function emitContent(contentFolder: string, output: string, cfg: Qu
|
||||||
const fp = rawFp as FilePath
|
const fp = rawFp as FilePath
|
||||||
const ext = path.extname(fp)
|
const ext = path.extname(fp)
|
||||||
const src = path.join(contentFolder, fp) as FilePath
|
const src = path.join(contentFolder, fp) as FilePath
|
||||||
const name = slugifyFilePath(fp as FilePath) + ext as FilePath
|
const name = (slugifyFilePath(fp as FilePath) + ext) as FilePath
|
||||||
const dest = path.join(assetsPath, name) as FilePath
|
const dest = path.join(assetsPath, name) as FilePath
|
||||||
const dir = path.dirname(dest) as FilePath
|
const dir = path.dirname(dest) as FilePath
|
||||||
await fs.promises.mkdir(dir, { recursive: true }) // ensure dir exists
|
await fs.promises.mkdir(dir, { recursive: true }) // ensure dir exists
|
||||||
|
|
|
@ -2,14 +2,18 @@ import { PerfTimer } from "../perf"
|
||||||
import { QuartzFilterPluginInstance } from "../plugins/types"
|
import { QuartzFilterPluginInstance } from "../plugins/types"
|
||||||
import { ProcessedContent } from "../plugins/vfile"
|
import { ProcessedContent } from "../plugins/vfile"
|
||||||
|
|
||||||
export function filterContent(plugins: QuartzFilterPluginInstance[], content: ProcessedContent[], verbose: boolean): ProcessedContent[] {
|
export function filterContent(
|
||||||
|
plugins: QuartzFilterPluginInstance[],
|
||||||
|
content: ProcessedContent[],
|
||||||
|
verbose: boolean,
|
||||||
|
): ProcessedContent[] {
|
||||||
const perf = new PerfTimer()
|
const perf = new PerfTimer()
|
||||||
const initialLength = content.length
|
const initialLength = content.length
|
||||||
for (const plugin of plugins) {
|
for (const plugin of plugins) {
|
||||||
const updatedContent = content.filter(plugin.shouldPublish)
|
const updatedContent = content.filter(plugin.shouldPublish)
|
||||||
|
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
const diff = content.filter(x => !updatedContent.includes(x))
|
const diff = content.filter((x) => !updatedContent.includes(x))
|
||||||
for (const file of diff) {
|
for (const file of diff) {
|
||||||
console.log(`[filter:${plugin.name}] ${file[1].data.slug}`)
|
console.log(`[filter:${plugin.name}] ${file[1].data.slug}`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import esbuild from 'esbuild'
|
import esbuild from "esbuild"
|
||||||
import remarkParse from 'remark-parse'
|
import remarkParse from "remark-parse"
|
||||||
import remarkRehype from 'remark-rehype'
|
import remarkRehype from "remark-rehype"
|
||||||
import { Processor, unified } from "unified"
|
import { Processor, unified } from "unified"
|
||||||
import { Root as MDRoot } from 'remark-parse/lib'
|
import { Root as MDRoot } from "remark-parse/lib"
|
||||||
import { Root as HTMLRoot } from 'hast'
|
import { Root as HTMLRoot } from "hast"
|
||||||
import { ProcessedContent } from '../plugins/vfile'
|
import { ProcessedContent } from "../plugins/vfile"
|
||||||
import { PerfTimer } from '../perf'
|
import { PerfTimer } from "../perf"
|
||||||
import { read } from 'to-vfile'
|
import { read } from "to-vfile"
|
||||||
import { FilePath, QUARTZ, ServerSlug, slugifyFilePath } from '../path'
|
import { FilePath, QUARTZ, ServerSlug, slugifyFilePath } from "../path"
|
||||||
import path from 'path'
|
import path from "path"
|
||||||
import os from 'os'
|
import os from "os"
|
||||||
import workerpool, { Promise as WorkerPromise } from 'workerpool'
|
import workerpool, { Promise as WorkerPromise } from "workerpool"
|
||||||
import { QuartzTransformerPluginInstance } from '../plugins/types'
|
import { QuartzTransformerPluginInstance } from "../plugins/types"
|
||||||
import { QuartzLogger } from '../log'
|
import { QuartzLogger } from "../log"
|
||||||
import { trace } from '../trace'
|
import { trace } from "../trace"
|
||||||
|
|
||||||
export type QuartzProcessor = Processor<MDRoot, HTMLRoot, void>
|
export type QuartzProcessor = Processor<MDRoot, HTMLRoot, void>
|
||||||
export function createProcessor(transformers: QuartzTransformerPluginInstance[]): QuartzProcessor {
|
export function createProcessor(transformers: QuartzTransformerPluginInstance[]): QuartzProcessor {
|
||||||
|
@ -21,16 +21,15 @@ export function createProcessor(transformers: QuartzTransformerPluginInstance[])
|
||||||
let processor = unified().use(remarkParse)
|
let processor = unified().use(remarkParse)
|
||||||
|
|
||||||
// MD AST -> MD AST transforms
|
// MD AST -> MD AST transforms
|
||||||
for (const plugin of transformers.filter(p => p.markdownPlugins)) {
|
for (const plugin of transformers.filter((p) => p.markdownPlugins)) {
|
||||||
processor = processor.use(plugin.markdownPlugins!())
|
processor = processor.use(plugin.markdownPlugins!())
|
||||||
}
|
}
|
||||||
|
|
||||||
// MD AST -> HTML AST
|
// MD AST -> HTML AST
|
||||||
processor = processor.use(remarkRehype, { allowDangerousHtml: true })
|
processor = processor.use(remarkRehype, { allowDangerousHtml: true })
|
||||||
|
|
||||||
|
|
||||||
// HTML AST -> HTML AST transforms
|
// HTML AST -> HTML AST transforms
|
||||||
for (const plugin of transformers.filter(p => p.htmlPlugins)) {
|
for (const plugin of transformers.filter((p) => p.htmlPlugins)) {
|
||||||
processor = processor.use(plugin.htmlPlugins!())
|
processor = processor.use(plugin.htmlPlugins!())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,23 +56,29 @@ async function transpileWorkerScript() {
|
||||||
packages: "external",
|
packages: "external",
|
||||||
plugins: [
|
plugins: [
|
||||||
{
|
{
|
||||||
name: 'css-and-scripts-as-text',
|
name: "css-and-scripts-as-text",
|
||||||
setup(build) {
|
setup(build) {
|
||||||
build.onLoad({ filter: /\.scss$/ }, (_) => ({
|
build.onLoad({ filter: /\.scss$/ }, (_) => ({
|
||||||
contents: '',
|
contents: "",
|
||||||
loader: 'text'
|
loader: "text",
|
||||||
}))
|
}))
|
||||||
build.onLoad({ filter: /\.inline\.(ts|js)$/ }, (_) => ({
|
build.onLoad({ filter: /\.inline\.(ts|js)$/ }, (_) => ({
|
||||||
contents: '',
|
contents: "",
|
||||||
loader: 'text'
|
loader: "text",
|
||||||
}))
|
}))
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createFileParser(transformers: QuartzTransformerPluginInstance[], baseDir: string, fps: FilePath[], allSlugs: ServerSlug[], verbose: boolean) {
|
export function createFileParser(
|
||||||
|
transformers: QuartzTransformerPluginInstance[],
|
||||||
|
baseDir: string,
|
||||||
|
fps: FilePath[],
|
||||||
|
allSlugs: ServerSlug[],
|
||||||
|
verbose: boolean,
|
||||||
|
) {
|
||||||
return async (processor: QuartzProcessor) => {
|
return async (processor: QuartzProcessor) => {
|
||||||
const res: ProcessedContent[] = []
|
const res: ProcessedContent[] = []
|
||||||
for (const fp of fps) {
|
for (const fp of fps) {
|
||||||
|
@ -84,7 +89,7 @@ export function createFileParser(transformers: QuartzTransformerPluginInstance[]
|
||||||
file.value = file.value.toString().trim()
|
file.value = file.value.toString().trim()
|
||||||
|
|
||||||
// Text -> Text transforms
|
// Text -> Text transforms
|
||||||
for (const plugin of transformers.filter(p => p.textTransform)) {
|
for (const plugin of transformers.filter((p) => p.textTransform)) {
|
||||||
file.value = plugin.textTransform!(file.value)
|
file.value = plugin.textTransform!(file.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,7 +115,12 @@ export function createFileParser(transformers: QuartzTransformerPluginInstance[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function parseMarkdown(transformers: QuartzTransformerPluginInstance[], baseDir: string, fps: FilePath[], verbose: boolean): Promise<ProcessedContent[]> {
|
export async function parseMarkdown(
|
||||||
|
transformers: QuartzTransformerPluginInstance[],
|
||||||
|
baseDir: string,
|
||||||
|
fps: FilePath[],
|
||||||
|
verbose: boolean,
|
||||||
|
): Promise<ProcessedContent[]> {
|
||||||
const perf = new PerfTimer()
|
const perf = new PerfTimer()
|
||||||
const log = new QuartzLogger(verbose)
|
const log = new QuartzLogger(verbose)
|
||||||
|
|
||||||
|
@ -118,7 +128,9 @@ export async function parseMarkdown(transformers: QuartzTransformerPluginInstanc
|
||||||
let concurrency = fps.length < CHUNK_SIZE ? 1 : os.availableParallelism()
|
let concurrency = fps.length < CHUNK_SIZE ? 1 : os.availableParallelism()
|
||||||
|
|
||||||
// get all slugs ahead of time as each thread needs a copy
|
// get all slugs ahead of time as each thread needs a copy
|
||||||
const allSlugs = fps.map(fp => slugifyFilePath(path.relative(baseDir, path.resolve(fp)) as FilePath))
|
const allSlugs = fps.map((fp) =>
|
||||||
|
slugifyFilePath(path.relative(baseDir, path.resolve(fp)) as FilePath),
|
||||||
|
)
|
||||||
|
|
||||||
let res: ProcessedContent[] = []
|
let res: ProcessedContent[] = []
|
||||||
log.start(`Parsing input files using ${concurrency} threads`)
|
log.start(`Parsing input files using ${concurrency} threads`)
|
||||||
|
@ -128,18 +140,15 @@ export async function parseMarkdown(transformers: QuartzTransformerPluginInstanc
|
||||||
res = await parse(processor)
|
res = await parse(processor)
|
||||||
} else {
|
} else {
|
||||||
await transpileWorkerScript()
|
await transpileWorkerScript()
|
||||||
const pool = workerpool.pool(
|
const pool = workerpool.pool("./quartz/bootstrap-worker.mjs", {
|
||||||
'./quartz/bootstrap-worker.mjs',
|
minWorkers: "max",
|
||||||
{
|
maxWorkers: concurrency,
|
||||||
minWorkers: 'max',
|
workerType: "thread",
|
||||||
maxWorkers: concurrency,
|
})
|
||||||
workerType: 'thread'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const childPromises: WorkerPromise<ProcessedContent[]>[] = []
|
const childPromises: WorkerPromise<ProcessedContent[]>[] = []
|
||||||
for (const chunk of chunks(fps, CHUNK_SIZE)) {
|
for (const chunk of chunks(fps, CHUNK_SIZE)) {
|
||||||
childPromises.push(pool.exec('parseFiles', [baseDir, chunk, allSlugs, verbose]))
|
childPromises.push(pool.exec("parseFiles", [baseDir, chunk, allSlugs, verbose]))
|
||||||
}
|
}
|
||||||
|
|
||||||
const results: ProcessedContent[][] = await WorkerPromise.all(childPromises)
|
const results: ProcessedContent[][] = await WorkerPromise.all(childPromises)
|
||||||
|
|
|
@ -2,29 +2,38 @@ import { randomUUID } from "crypto"
|
||||||
import { JSX } from "preact/jsx-runtime"
|
import { JSX } from "preact/jsx-runtime"
|
||||||
|
|
||||||
export type JSResource = {
|
export type JSResource = {
|
||||||
loadTime: 'beforeDOMReady' | 'afterDOMReady'
|
loadTime: "beforeDOMReady" | "afterDOMReady"
|
||||||
moduleType?: 'module',
|
moduleType?: "module"
|
||||||
spaPreserve?: boolean
|
spaPreserve?: boolean
|
||||||
} & ({
|
} & (
|
||||||
src: string
|
| {
|
||||||
contentType: 'external'
|
src: string
|
||||||
} | {
|
contentType: "external"
|
||||||
script: string
|
}
|
||||||
contentType: 'inline'
|
| {
|
||||||
})
|
script: string
|
||||||
|
contentType: "inline"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export function JSResourceToScriptElement(resource: JSResource, preserve?: boolean): JSX.Element {
|
export function JSResourceToScriptElement(resource: JSResource, preserve?: boolean): JSX.Element {
|
||||||
const scriptType = resource.moduleType ?? 'application/javascript'
|
const scriptType = resource.moduleType ?? "application/javascript"
|
||||||
const spaPreserve = preserve ?? resource.spaPreserve
|
const spaPreserve = preserve ?? resource.spaPreserve
|
||||||
if (resource.contentType === 'external') {
|
if (resource.contentType === "external") {
|
||||||
return <script key={resource.src} src={resource.src} type={scriptType} spa-preserve={spaPreserve}/>
|
return (
|
||||||
|
<script key={resource.src} src={resource.src} type={scriptType} spa-preserve={spaPreserve} />
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
const content = resource.script
|
const content = resource.script
|
||||||
return <script key={randomUUID()} type={scriptType} spa-preserve={spaPreserve}>{content}</script>
|
return (
|
||||||
|
<script key={randomUUID()} type={scriptType} spa-preserve={spaPreserve}>
|
||||||
|
{content}
|
||||||
|
</script>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StaticResources {
|
export interface StaticResources {
|
||||||
css: string[],
|
css: string[]
|
||||||
js: JSResource[]
|
js: JSResource[]
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,17 @@ body {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
p, ul, text, a, tr, td, li, ol, ul, .katex, .math {
|
p,
|
||||||
|
ul,
|
||||||
|
text,
|
||||||
|
a,
|
||||||
|
tr,
|
||||||
|
td,
|
||||||
|
li,
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
.katex,
|
||||||
|
.math {
|
||||||
color: var(--darkgray);
|
color: var(--darkgray);
|
||||||
fill: var(--darkgray);
|
fill: var(--darkgray);
|
||||||
}
|
}
|
||||||
|
@ -79,7 +89,7 @@ a {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
& li:has(> input[type='checkbox']) {
|
& li:has(> input[type="checkbox"]) {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
margin-left: -1.4rem;
|
margin-left: -1.4rem;
|
||||||
|
@ -144,7 +154,8 @@ a {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& .center, & footer {
|
& .center,
|
||||||
|
& footer {
|
||||||
width: $pageWidth;
|
width: $pageWidth;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
@ -195,9 +206,12 @@ thead {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
&[id] > a[href^="#"] {
|
&[id] > a[href^="#"] {
|
||||||
margin: 0 0.5rem;
|
margin: 0 0.5rem;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
@ -277,11 +291,11 @@ pre {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-line-numbers-max-digits='2'] > [data-line]::before {
|
&[data-line-numbers-max-digits="2"] > [data-line]::before {
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-line-numbers-max-digits='3'] > [data-line]::before {
|
&[data-line-numbers-max-digits="3"] > [data-line]::before {
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -296,7 +310,9 @@ code {
|
||||||
background: var(--lightgray);
|
background: var(--lightgray);
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody, li, p {
|
tbody,
|
||||||
|
li,
|
||||||
|
p {
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -307,7 +323,8 @@ table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
td, th {
|
td,
|
||||||
|
th {
|
||||||
padding: 0.2rem 1rem;
|
padding: 0.2rem 1rem;
|
||||||
border: 1px solid var(--gray);
|
border: 1px solid var(--gray);
|
||||||
}
|
}
|
||||||
|
@ -331,7 +348,8 @@ hr {
|
||||||
background-color: var(--lightgray);
|
background-color: var(--lightgray);
|
||||||
}
|
}
|
||||||
|
|
||||||
audio, video {
|
audio,
|
||||||
|
video {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
@ -340,7 +358,8 @@ audio, video {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.overflow, ol.overflow {
|
ul.overflow,
|
||||||
|
ol.overflow {
|
||||||
height: 400px;
|
height: 400px;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
@ -354,9 +373,9 @@ ul.overflow, ol.overflow {
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
content: '';
|
content: "";
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|
|
@ -1,104 +1,104 @@
|
||||||
@use "sass:color";
|
@use "sass:color";
|
||||||
|
|
||||||
.callout {
|
.callout {
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background-color: var(--bg);
|
background-color: var(--bg);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
transition: max-height 0.3s ease;
|
transition: max-height 0.3s ease;
|
||||||
|
|
||||||
& > *:nth-child(2) {
|
& > *:nth-child(2) {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-callout="note"] {
|
&[data-callout="note"] {
|
||||||
--color: #448aff;
|
--color: #448aff;
|
||||||
--border: #448aff22;
|
--border: #448aff22;
|
||||||
--bg: #448aff09;
|
--bg: #448aff09;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-callout="abstract"] {
|
&[data-callout="abstract"] {
|
||||||
--color: #00b0ff;
|
--color: #00b0ff;
|
||||||
--border: #00b0ff22;
|
--border: #00b0ff22;
|
||||||
--bg: #00b0ff09;
|
--bg: #00b0ff09;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-callout="info"], &[data-callout="todo"] {
|
&[data-callout="info"],
|
||||||
--color: #00b8d4;
|
&[data-callout="todo"] {
|
||||||
--border: #00b8d422;
|
--color: #00b8d4;
|
||||||
--bg: #00b8d409;
|
--border: #00b8d422;
|
||||||
}
|
--bg: #00b8d409;
|
||||||
|
}
|
||||||
|
|
||||||
&[data-callout="tip"] {
|
&[data-callout="tip"] {
|
||||||
--color: #00bfa5;
|
--color: #00bfa5;
|
||||||
--border: #00bfa522;
|
--border: #00bfa522;
|
||||||
--bg: #00bfa509;
|
--bg: #00bfa509;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-callout="success"] {
|
&[data-callout="success"] {
|
||||||
--color: #09ad7a;
|
--color: #09ad7a;
|
||||||
--border: #09ad7122;
|
--border: #09ad7122;
|
||||||
--bg: #09ad7109;
|
--bg: #09ad7109;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-callout="question"] {
|
&[data-callout="question"] {
|
||||||
--color: #dba642;
|
--color: #dba642;
|
||||||
--border: #dba64222;
|
--border: #dba64222;
|
||||||
--bg: #dba64209;
|
--bg: #dba64209;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-callout="warning"] {
|
&[data-callout="warning"] {
|
||||||
--color: #db8942;
|
--color: #db8942;
|
||||||
--border: #db894222;
|
--border: #db894222;
|
||||||
--bg: #db894209;
|
--bg: #db894209;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-callout="failure"], &[data-callout="danger"], &[data-callout="bug"] {
|
&[data-callout="failure"],
|
||||||
--color: #db4242;
|
&[data-callout="danger"],
|
||||||
--border: #db424222;
|
&[data-callout="bug"] {
|
||||||
--bg: #db424209;
|
--color: #db4242;
|
||||||
}
|
--border: #db424222;
|
||||||
|
--bg: #db424209;
|
||||||
|
}
|
||||||
|
|
||||||
&[data-callout="example"] {
|
&[data-callout="example"] {
|
||||||
--color: #7a43b5;
|
--color: #7a43b5;
|
||||||
--border: #7a43b522;
|
--border: #7a43b522;
|
||||||
--bg: #7a43b509;
|
--bg: #7a43b509;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[data-callout="quote"] {
|
||||||
|
--color: var(--secondary);
|
||||||
|
--border: var(--lightgray);
|
||||||
|
}
|
||||||
|
|
||||||
&[data-callout="quote"] {
|
|
||||||
--color: var(--secondary);
|
|
||||||
--border: var(--lightgray);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-collapsed > .callout-title > .fold {
|
&.is-collapsed > .callout-title > .fold {
|
||||||
transform: rotateZ(-90deg)
|
transform: rotateZ(-90deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.callout-title {
|
.callout-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
color: var(--color);
|
color: var(--color);
|
||||||
|
|
||||||
& .fold {
|
& .fold {
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.callout-icon {
|
.callout-icon {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.callout-title-inner {
|
.callout-title-inner {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,4 +3,4 @@ $mobileBreakpoint: 600px;
|
||||||
$tabletBreakpoint: 1200px;
|
$tabletBreakpoint: 1200px;
|
||||||
$sidePanelWidth: 400px;
|
$sidePanelWidth: 400px;
|
||||||
$topSpacing: 6rem;
|
$topSpacing: 6rem;
|
||||||
$fullPageWidth: $pageWidth + 2 * $sidePanelWidth
|
$fullPageWidth: $pageWidth + 2 * $sidePanelWidth;
|
||||||
|
|
|
@ -1,27 +1,28 @@
|
||||||
export interface ColorScheme {
|
export interface ColorScheme {
|
||||||
light: string,
|
light: string
|
||||||
lightgray: string,
|
lightgray: string
|
||||||
gray: string,
|
gray: string
|
||||||
darkgray: string,
|
darkgray: string
|
||||||
dark: string,
|
dark: string
|
||||||
secondary: string,
|
secondary: string
|
||||||
tertiary: string,
|
tertiary: string
|
||||||
highlight: string
|
highlight: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Theme {
|
export interface Theme {
|
||||||
typography: {
|
typography: {
|
||||||
header: string,
|
header: string
|
||||||
body: string,
|
body: string
|
||||||
code: string
|
code: string
|
||||||
},
|
}
|
||||||
colors: {
|
colors: {
|
||||||
lightMode: ColorScheme,
|
lightMode: ColorScheme
|
||||||
darkMode: ColorScheme
|
darkMode: ColorScheme
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_SANS_SERIF = "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif"
|
const DEFAULT_SANS_SERIF =
|
||||||
|
'-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif'
|
||||||
const DEFAULT_MONO = "ui-monospace, SFMono-Regular, SF Mono, Menlo, monospace"
|
const DEFAULT_MONO = "ui-monospace, SFMono-Regular, SF Mono, Menlo, monospace"
|
||||||
export function googleFontHref(theme: Theme) {
|
export function googleFontHref(theme: Theme) {
|
||||||
const { code, header, body } = theme.typography
|
const { code, header, body } = theme.typography
|
||||||
|
|
|
@ -4,13 +4,17 @@ const rootFile = /.*at file:/
|
||||||
export function trace(msg: string, err: Error) {
|
export function trace(msg: string, err: Error) {
|
||||||
const stack = err.stack
|
const stack = err.stack
|
||||||
console.log()
|
console.log()
|
||||||
console.log(chalk.bgRed.white.bold(" ERROR ") + chalk.red(` ${msg}`) + (err.message.length > 0 ? `: ${err.message}` : ""))
|
console.log(
|
||||||
|
chalk.bgRed.white.bold(" ERROR ") +
|
||||||
|
chalk.red(` ${msg}`) +
|
||||||
|
(err.message.length > 0 ? `: ${err.message}` : ""),
|
||||||
|
)
|
||||||
if (!stack) {
|
if (!stack) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let reachedEndOfLegibleTrace = false
|
let reachedEndOfLegibleTrace = false
|
||||||
for (const line of stack.split('\n').slice(1)) {
|
for (const line of stack.split("\n").slice(1)) {
|
||||||
if (reachedEndOfLegibleTrace) {
|
if (reachedEndOfLegibleTrace) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,12 @@ const transformers = config.plugins.transformers
|
||||||
const processor = createProcessor(transformers)
|
const processor = createProcessor(transformers)
|
||||||
|
|
||||||
// only called from worker thread
|
// only called from worker thread
|
||||||
export async function parseFiles(baseDir: string, fps: FilePath[], allSlugs: ServerSlug[], verbose: boolean) {
|
export async function parseFiles(
|
||||||
|
baseDir: string,
|
||||||
|
fps: FilePath[],
|
||||||
|
allSlugs: ServerSlug[],
|
||||||
|
verbose: boolean,
|
||||||
|
) {
|
||||||
const parse = createFileParser(transformers, baseDir, fps, allSlugs, verbose)
|
const parse = createFileParser(transformers, baseDir, fps, allSlugs, verbose)
|
||||||
return parse(processor)
|
return parse(processor)
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue