Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64c7851939 |
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -20,19 +20,12 @@ Steps to reproduce the behavior:
|
|||||||
**Expected behavior**
|
**Expected behavior**
|
||||||
A clear and concise description of what you expected to happen.
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
**Screenshots and Source**
|
**Screenshots**
|
||||||
If applicable, add screenshots to help explain your problem.
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
You can help speed up fixing the problem by either
|
|
||||||
|
|
||||||
1. providing a simple reproduction
|
|
||||||
2. linking to your Quartz repository where the problem can be observed
|
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
**Desktop (please complete the following information):**
|
||||||
|
|
||||||
- Quartz Version: [e.g. v4.1.2]
|
- Device: [e.g. iPhone6]
|
||||||
- `node` Version: [e.g. v18.16]
|
|
||||||
- `npm` version: [e.g. v10.1.0]
|
|
||||||
- OS: [e.g. iOS]
|
- OS: [e.g. iOS]
|
||||||
- Browser [e.g. chrome, safari]
|
- Browser [e.g. chrome, safari]
|
||||||
|
|
||||||
|
|||||||
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
@@ -1,11 +0,0 @@
|
|||||||
# To get started with Dependabot version updates, you'll need to specify which
|
|
||||||
# package ecosystems to update and where the package manifests are located.
|
|
||||||
# Please see the documentation for all configuration options:
|
|
||||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
||||||
|
|
||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "npm"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
12
.github/workflows/ci.yaml
vendored
12
.github/workflows/ci.yaml
vendored
@@ -46,14 +46,8 @@ jobs:
|
|||||||
- name: Ensure Quartz builds, check bundle info
|
- name: Ensure Quartz builds, check bundle info
|
||||||
run: npx quartz build --bundleInfo
|
run: npx quartz build --bundleInfo
|
||||||
|
|
||||||
- name: Get package version
|
|
||||||
run: node -p -e '`PACKAGE_VERSION=${require("./package.json").version}`' >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Create release tag
|
- name: Create release tag
|
||||||
uses: pkgdeps/git-tag-action@v2
|
uses: Klemensas/action-autotag@stable
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
github_repo: ${{ github.repository }}
|
tag_prefix: "v"
|
||||||
version: ${{ env.PACKAGE_VERSION }}
|
|
||||||
git_commit_sha: ${{ github.sha }}
|
|
||||||
git_tag_prefix: "v"
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
Quartz is a set of tools that helps you publish your [digital garden](https://jzhao.xyz/posts/networked-thought) and notes as a website for free.
|
Quartz is a set of tools that helps you publish your [digital garden](https://jzhao.xyz/posts/networked-thought) and notes as a website for free.
|
||||||
Quartz v4 features a from-the-ground rewrite focusing on end-user extensibility and ease-of-use.
|
Quartz v4 features a from-the-ground rewrite focusing on end-user extensibility and ease-of-use.
|
||||||
|
|
||||||
|
**If you are looking for Quartz v3, you can find it on the [`hugo` branch](https://github.com/jackyzha0/quartz/tree/hugo).**
|
||||||
|
|
||||||
🔗 Read the documentation and get started: https://quartz.jzhao.xyz/
|
🔗 Read the documentation and get started: https://quartz.jzhao.xyz/
|
||||||
|
|
||||||
[Join the Discord Community](https://discord.gg/cRFFHYye7t)
|
[Join the Discord Community](https://discord.gg/cRFFHYye7t)
|
||||||
|
|||||||
@@ -156,13 +156,12 @@ document.addEventListener("nav", () => {
|
|||||||
// do page specific logic here
|
// do page specific logic here
|
||||||
// e.g. attach event listeners
|
// e.g. attach event listeners
|
||||||
const toggleSwitch = document.querySelector("#switch") as HTMLInputElement
|
const toggleSwitch = document.querySelector("#switch") as HTMLInputElement
|
||||||
|
toggleSwitch.removeEventListener("change", switchTheme)
|
||||||
toggleSwitch.addEventListener("change", switchTheme)
|
toggleSwitch.addEventListener("change", switchTheme)
|
||||||
window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
|
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
It is best practice to track any event handlers via `window.addCleanup` to prevent memory leaks.
|
It is best practice to also unmount any existing event handlers to prevent memory leaks.
|
||||||
This will get called on page navigation.
|
|
||||||
|
|
||||||
#### Importing Code
|
#### Importing Code
|
||||||
|
|
||||||
|
|||||||
@@ -53,12 +53,12 @@ All transformer plugins must define at least a `name` field to register the plug
|
|||||||
|
|
||||||
Normally for both `remark` and `rehype`, you can find existing plugins that you can use to . If you'd like to create your own `remark` or `rehype` plugin, checkout the [guide to creating a plugin](https://unifiedjs.com/learn/guide/create-a-plugin/) using `unified` (the underlying AST parser and transformer library).
|
Normally for both `remark` and `rehype`, you can find existing plugins that you can use to . If you'd like to create your own `remark` or `rehype` plugin, checkout the [guide to creating a plugin](https://unifiedjs.com/learn/guide/create-a-plugin/) using `unified` (the underlying AST parser and transformer library).
|
||||||
|
|
||||||
A good example of a transformer plugin that borrows from the `remark` and `rehype` ecosystems is the [[plugins/Latex|Latex]] plugin:
|
A good example of a transformer plugin that borrows from the `remark` and `rehype` ecosystems is the [[Latex]] plugin:
|
||||||
|
|
||||||
```ts title="quartz/plugins/transformers/latex.ts"
|
```ts title="quartz/plugins/transformers/latex.ts"
|
||||||
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"
|
import rehypeMathjax from "rehype-mathjax/svg.js"
|
||||||
import { QuartzTransformerPlugin } from "../types"
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
@@ -84,14 +84,10 @@ export const Latex: QuartzTransformerPlugin<Options> = (opts?: Options) => {
|
|||||||
externalResources() {
|
externalResources() {
|
||||||
if (engine === "katex") {
|
if (engine === "katex") {
|
||||||
return {
|
return {
|
||||||
css: [
|
css: ["https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css"],
|
||||||
// base css
|
|
||||||
"https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css",
|
|
||||||
],
|
|
||||||
js: [
|
js: [
|
||||||
{
|
{
|
||||||
// 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://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/contrib/copy-tex.min.js",
|
|
||||||
loadTime: "afterDOMReady",
|
loadTime: "afterDOMReady",
|
||||||
contentType: "external",
|
contentType: "external",
|
||||||
},
|
},
|
||||||
@@ -220,19 +216,22 @@ export type QuartzEmitterPlugin<Options extends OptionType = undefined> = (
|
|||||||
|
|
||||||
export type QuartzEmitterPluginInstance = {
|
export type QuartzEmitterPluginInstance = {
|
||||||
name: string
|
name: string
|
||||||
emit(ctx: BuildCtx, content: ProcessedContent[], resources: StaticResources): Promise<FilePath[]>
|
emit(
|
||||||
|
ctx: BuildCtx,
|
||||||
|
content: ProcessedContent[],
|
||||||
|
resources: StaticResources,
|
||||||
|
emitCallback: EmitCallback,
|
||||||
|
): Promise<FilePath[]>
|
||||||
getQuartzComponents(ctx: BuildCtx): QuartzComponent[]
|
getQuartzComponents(ctx: BuildCtx): QuartzComponent[]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
An emitter plugin must define a `name` field, an `emit` function, and a `getQuartzComponents` function. `emit` is responsible for looking at all the parsed and filtered content and then appropriately creating files and returning a list of paths to files the plugin created.
|
An emitter plugin must define a `name` field an `emit` function and a `getQuartzComponents` function. `emit` is responsible for looking at all the parsed and filtered content and then appropriately creating files and returning a list of paths to files the plugin created.
|
||||||
|
|
||||||
Creating new files can be done via regular Node [fs module](https://nodejs.org/api/fs.html) (i.e. `fs.cp` or `fs.writeFile`) or via the `write` function in `quartz/plugins/emitters/helpers.ts` if you are creating files that contain text. `write` has the following signature:
|
Creating new files can be done via regular Node [fs module](https://nodejs.org/api/fs.html) (i.e. `fs.cp` or `fs.writeFile`) or via the `emitCallback` if you are creating files that contain text. The `emitCallback` function is the 4th argument of the emit function. Its interface looks something like this:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
export type WriteOptions = (data: {
|
export type EmitCallback = (data: {
|
||||||
// the build context
|
|
||||||
ctx: BuildCtx
|
|
||||||
// the name of the file to emit (not including the file extension)
|
// the name of the file to emit (not including the file extension)
|
||||||
slug: ServerSlug
|
slug: ServerSlug
|
||||||
// the file extension
|
// the file extension
|
||||||
@@ -282,7 +281,7 @@ export const ContentPage: QuartzEmitterPlugin = () => {
|
|||||||
allFiles,
|
allFiles,
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = renderPage(cfg, slug, componentData, opts, externalResources)
|
const content = renderPage(slug, componentData, opts, externalResources)
|
||||||
const fp = await emit({
|
const fp = await emit({
|
||||||
content,
|
content,
|
||||||
slug: file.data.slug!,
|
slug: file.data.slug!,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
title: Authoring Content
|
title: Authoring Content
|
||||||
---
|
---
|
||||||
|
|
||||||
All of the content in your Quartz should go in the `/content` folder. The content for the home page of your Quartz lives in `content/index.md`. If you've [[index#🪴 Get Started|setup Quartz]] already, this folder should already be initialized. Any Markdown in this folder will get processed by Quartz.
|
All of the content in your Quartz should go in the `/content` folder. The content for the home page of your Quartz lives in `content/index.md`. If you've [[index#🪴 Get Started|setup Quartz]] already, this folder should already be initailized. Any Markdown in this folder will get processed by Quartz.
|
||||||
|
|
||||||
It is recommended that you use [Obsidian](https://obsidian.md/) as a way to edit and maintain your Quartz. It comes with a nice editor and graphical interface to preview, edit, and link your local files and attachments.
|
It is recommended that you use [Obsidian](https://obsidian.md/) as a way to edit and maintain your Quartz. It comes with a nice editor and graphical interface to preview, edit, and link your local files and attachments.
|
||||||
|
|
||||||
@@ -28,17 +28,21 @@ The rest of your content lives here. You can use **Markdown** here :)
|
|||||||
Some common frontmatter fields that are natively supported by Quartz:
|
Some common frontmatter fields that are natively supported by Quartz:
|
||||||
|
|
||||||
- `title`: Title of the page. If it isn't provided, Quartz will use the name of the file as the title.
|
- `title`: Title of the page. If it isn't provided, Quartz will use the name of the file as the title.
|
||||||
- `description`: Description of the page used for link previews.
|
|
||||||
- `aliases`: Other names for this note. This is a list of strings.
|
- `aliases`: Other names for this note. This is a list of strings.
|
||||||
- `tags`: Tags for this note.
|
|
||||||
- `draft`: Whether to publish the page or not. This is one way to make [[private pages|pages private]] in Quartz.
|
- `draft`: Whether to publish the page or not. This is one way to make [[private pages|pages private]] in Quartz.
|
||||||
- `date`: A string representing the day the note was published. Normally uses `YYYY-MM-DD` format.
|
- `date`: A string representing the day the note was published. Normally uses `YYYY-MM-DD` format.
|
||||||
|
|
||||||
## Syncing your Content
|
## Syncing your Content
|
||||||
|
|
||||||
When your Quartz is at a point you're happy with, you can save your changes to GitHub.
|
When your Quartz is at a point you're happy with, you can save your changes to GitHub by doing `npx quartz sync`.
|
||||||
First, make sure you've [[setting up your GitHub repository|already setup your GitHub repository]] and then do `npx quartz sync`.
|
|
||||||
|
|
||||||
## Customization
|
> [!hint] Flags and options
|
||||||
|
> For full help options, you can run `npx quartz sync --help`.
|
||||||
Frontmatter parsing for `title`, `tags`, `aliases` and `cssclasses` is a functionality of the [[Frontmatter]] plugin, `date` is handled by the [[CreatedModifiedDate]] plugin and `description` by the [[Description]] plugin. See the plugin pages for customization options.
|
>
|
||||||
|
> Most of these have sensible defaults but you can override them if you have a custom setup:
|
||||||
|
>
|
||||||
|
> - `-d` or `--directory`: the content folder. This is normally just `content`
|
||||||
|
> - `-v` or `--verbose`: print out extra logging information
|
||||||
|
> - `--commit` or `--no-commit`: whether to make a `git` commit for your changes
|
||||||
|
> - `--push` or `--no-push`: whether to push updates to your GitHub fork of Quartz
|
||||||
|
> - `--pull` or `--no-pull`: whether to try and pull in any updates from your GitHub fork (i.e. from other devices) before pushing
|
||||||
|
|||||||
@@ -25,17 +25,14 @@ This part of the configuration concerns anything that can affect the whole 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: 'google', tagId: '<your-google-tag>' }`: use Google Analytics;
|
- `{ provider: 'plausible' }`: use [Plausible](https://plausible.io/), a privacy-friendly alternative to Google Analytics; or
|
||||||
- `{ provider: 'plausible' }` (managed) or `{ provider: 'plausible', host: '<your-plausible-host>' }` (self-hosted): use [Plausible](https://plausible.io/);
|
- `{ provider: 'google', tagId: <your-google-tag> }`: use Google Analytics
|
||||||
- `{ provider: 'umami', host: '<your-umami-host>', websiteId: '<your-umami-website-id>' }`: use [Umami](https://umami.is/);
|
|
||||||
- `locale`: used for [[i18n]] and date formatting
|
|
||||||
- `baseUrl`: this is used for sitemaps and RSS feeds that require an absolute URL to know where the canonical 'home' of your site lives. This is normally the deployed URL of your site (e.g. `quartz.jzhao.xyz` for this site). Do not include the protocol (i.e. `https://`) or any leading or trailing slashes.
|
- `baseUrl`: this is used for sitemaps and RSS feeds that require an absolute URL to know where the canonical 'home' of your site lives. This is normally the deployed URL of your site (e.g. `quartz.jzhao.xyz` for this site). Do not include the protocol (i.e. `https://`) or any leading or trailing slashes.
|
||||||
- This should also include the subpath if you are [[hosting]] on GitHub pages without a custom domain. For example, if my repository is `jackyzha0/quartz`, GitHub pages would deploy to `https://jackyzha0.github.io/quartz` and the `baseUrl` would be `jackyzha0.github.io/quartz`.
|
- This should also include the subpath if you are [[hosting]] on GitHub pages without a custom domain. For example, if my repository is `jackyzha0/quartz`, GitHub pages would deploy to `https://jackyzha0.github.io/quartz` and the `baseUrl` would be `jackyzha0.github.io/quartz`
|
||||||
- Note that Quartz 4 will avoid using this as much as possible and use relative URLs whenever it can to make sure your site works no matter _where_ you end up actually deploying it.
|
- Note that Quartz 4 will avoid using this as much as possible and use relative URLs whenever it can to make sure your site works no matter _where_ you end up actually deploying it.
|
||||||
- `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. See [[private pages]] for more details.
|
- `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. See [[private pages]] for more details.
|
||||||
- `defaultDateType`: whether to use created, modified, or published as the default date to display on pages and page listings.
|
- `defaultDateType`: whether to use created, modified, or published as the default date to display on pages and page listings.
|
||||||
- `theme`: configure how the site looks.
|
- `theme`: configure how the site looks.
|
||||||
- `cdnCaching`: If `true` (default), use Google CDN to cache the fonts. This will generally will be faster. Disable (`false`) this if you want Quartz to download the fonts to be self-contained.
|
|
||||||
- `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.
|
||||||
@@ -56,7 +53,7 @@ You can think of Quartz plugins as a series of transformations over content.
|
|||||||
|
|
||||||
![[quartz transform pipeline.png]]
|
![[quartz transform pipeline.png]]
|
||||||
|
|
||||||
```ts title="quartz.config.ts"
|
```ts
|
||||||
plugins: {
|
plugins: {
|
||||||
transformers: [...],
|
transformers: [...],
|
||||||
filters: [...],
|
filters: [...],
|
||||||
@@ -64,40 +61,22 @@ plugins: {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- [[tags/plugin/transformer|Transformers]] **map** over content (e.g. parsing frontmatter, generating a description)
|
- [[making plugins#Transformers|Transformers]] **map** over content (e.g. parsing frontmatter, generating a description)
|
||||||
- [[tags/plugin/filter|Filters]] **filter** content (e.g. filtering out drafts)
|
- [[making plugins#Filters|Filters]] **filter** content (e.g. filtering out drafts)
|
||||||
- [[tags/plugin/emitter|Emitters]] **reduce** over content (e.g. creating an RSS feed or pages that list all files with a specific tag)
|
- [[making plugins#Emitters|Emitters]] **reduce** over content (e.g. creating an RSS feed or pages that list all files with a specific tag)
|
||||||
|
|
||||||
You can customize the behaviour of Quartz by adding, removing and reordering plugins in the `transformers`, `filters` and `emitters` fields.
|
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 pay particular attention to whether they need to come before or after certain other 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.
|
||||||
|
|
||||||
You should take care to add the plugin to the right entry corresponding to its plugin type. For example, to add the [[ExplicitPublish]] plugin (a [[tags/plugin/transformer|Transformer]], you would add the following line:
|
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 title="quartz.config.ts"
|
```ts
|
||||||
transformers: [
|
transformers: [
|
||||||
...
|
Plugin.FrontMatter(), // uses default options
|
||||||
Plugin.ExplicitPublish(),
|
Plugin.Latex({ renderEngine: "katex" }), // specify some options
|
||||||
...
|
|
||||||
],
|
|
||||||
```
|
|
||||||
|
|
||||||
To remove a plugin, you should remove all occurrences of it in the `quartz.config.ts`.
|
|
||||||
|
|
||||||
To customize plugins further, some plugins may also have their own configuration settings that you can pass in. If you do not pass in a configuration, the plugin will use its default settings.
|
|
||||||
|
|
||||||
For example, the [[plugins/Latex|Latex]] plugin allows you to pass in a field specifying the `renderEngine` to choose between Katex and MathJax.
|
|
||||||
|
|
||||||
```ts title="quartz.config.ts"
|
|
||||||
transformers: [
|
|
||||||
Plugin.FrontMatter(), // use default options
|
|
||||||
Plugin.Latex({ renderEngine: "katex" }), // set some custom options
|
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
Some plugins are included by default in the[ `quartz.config.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz.config.ts), but there are more available.
|
If you'd like to make your own plugins, read the guide on [[making plugins]] for more information.
|
||||||
|
|
||||||
You can see a list of all plugins and their configuration options [[tags/plugin|here]].
|
|
||||||
|
|
||||||
If you'd like to make your own plugins, see the [[making plugins|making custom plugins]] guide.
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: LaTeX
|
|
||||||
tags:
|
tags:
|
||||||
- feature/transformer
|
- plugin/transformer
|
||||||
---
|
---
|
||||||
|
|
||||||
Quartz uses [Katex](https://katex.org/) by default to typeset both inline and block math expressions at build time.
|
Quartz uses [Katex](https://katex.org/) by default to typeset both inline and block math expressions at build time.
|
||||||
@@ -39,9 +38,6 @@ a & b & c
|
|||||||
\end{bmatrix}
|
\end{bmatrix}
|
||||||
$$
|
$$
|
||||||
|
|
||||||
> [!warn]
|
|
||||||
> Due to limitations in the [underlying parsing library](https://github.com/remarkjs/remark-math), block math in Quartz requires the `$$` delimiters to be on newlines like above.
|
|
||||||
|
|
||||||
### 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$
|
||||||
@@ -57,15 +53,11 @@ 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
|
||||||
|
|
||||||
### Using mhchem
|
## MathJax
|
||||||
|
|
||||||
Add the following import to the top of `quartz/plugins/transformers/latex.ts` (before all the other
|
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' })`
|
||||||
imports):
|
|
||||||
|
|
||||||
```ts title="quartz/plugins/transformers/latex.ts"
|
|
||||||
import "katex/contrib/mhchem"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
|
|
||||||
Latex parsing is a functionality of the [[plugins/Latex|Latex]] plugin. See the plugin page for customization options.
|
- Removing Latex support: remove all instances of `Plugin.Latex()` from `quartz.config.ts`.
|
||||||
|
- Plugin: `quartz/plugins/transformers/latex.ts`
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
---
|
|
||||||
title: "Mermaid Diagrams"
|
|
||||||
tags:
|
|
||||||
- feature/transformer
|
|
||||||
---
|
|
||||||
|
|
||||||
Quartz supports Mermaid which allows you to add diagrams and charts to your notes. Mermaid supports a range of diagrams, such as [flow charts](https://mermaid.js.org/syntax/flowchart.html), [sequence diagrams](https://mermaid.js.org/syntax/sequenceDiagram.html), and [timelines](https://mermaid.js.org/syntax/timeline.html). This is enabled as a part of [[Obsidian compatibility]] and can be configured and enabled/disabled from that plugin.
|
Quartz supports Mermaid which allows you to add diagrams and charts to your notes. Mermaid supports a range of diagrams, such as [flow charts](https://mermaid.js.org/syntax/flowchart.html), [sequence diagrams](https://mermaid.js.org/syntax/sequenceDiagram.html), and [timelines](https://mermaid.js.org/syntax/timeline.html). This is enabled as a part of [[Obsidian compatibility]] and can be configured and enabled/disabled from that plugin.
|
||||||
|
|
||||||
By default, Quartz will render Mermaid diagrams to match the site theme.
|
By default, Quartz will render Mermaid diagrams to match the site theme.
|
||||||
|
|
||||||
> [!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 [[ObsidianFlavoredMarkdown]] is _after_ [[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()`.
|
||||||
|
|
||||||
## Syntax
|
## Syntax
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,31 @@
|
|||||||
---
|
---
|
||||||
title: "Obsidian Compatibility"
|
|
||||||
tags:
|
tags:
|
||||||
- feature/transformer
|
- plugin/transformer
|
||||||
---
|
---
|
||||||
|
|
||||||
Quartz was originally designed as a tool to publish Obsidian vaults as websites. Even as the scope of Quartz has widened over time, it hasn't lost the ability to seamlessly interoperate with Obsidian.
|
Quartz was originally designed as a tool to publish Obsidian vaults as websites. Even as the scope of Quartz has widened over time, it hasn't lost the ability to seamlessly interoperate with Obsidian.
|
||||||
|
|
||||||
By default, Quartz ships with the [[ObsidianFlavoredMarkdown]] plugin, which is a transformer plugin that adds support for [Obsidian Flavored Markdown](https://help.obsidian.md/Editing+and+formatting/Obsidian+Flavored+Markdown). This includes support for features like [[wikilinks]] and [[Mermaid diagrams]].
|
By default, Quartz ships with `Plugin.ObsidianFlavoredMarkdown` which is a transformer plugin that adds support for [Obsidian Flavored Markdown](https://help.obsidian.md/Editing+and+formatting/Obsidian+Flavored+Markdown). This includes support for features like [[wikilinks]] and [[Mermaid diagrams]].
|
||||||
|
|
||||||
It also ships with support for [frontmatter parsing](https://help.obsidian.md/Editing+and+formatting/Properties) with the same fields that Obsidian uses through the [[Frontmatter]] transformer plugin.
|
It also ships with support for [frontmatter parsing](https://help.obsidian.md/Editing+and+formatting/Properties) with the same fields that Obsidian uses through the `Plugin.FrontMatter` transformer plugin.
|
||||||
|
|
||||||
Finally, Quartz also provides [[CrawlLinks]] plugin, which allows you to customize Quartz's link resolution behaviour to match Obsidian.
|
Finally, Quartz also provides `Plugin.CrawlLinks` which allows you to customize Quartz's link resolution behaviour to match Obsidian.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
This functionality is provided by the [[ObsidianFlavoredMarkdown]], [[Frontmatter]] and [[CrawlLinks]] plugins. See the plugin pages for customization options.
|
- Frontmatter parsing:
|
||||||
|
- Disabling: remove all instances of `Plugin.FrontMatter()` from `quartz.config.ts`.
|
||||||
|
- Customize default values for frontmatter: edit `quartz/plugins/transformers/frontmatter.ts`
|
||||||
|
- Obsidian Flavored Markdown:
|
||||||
|
- Disabling: remove all instances of `Plugin.ObsidianFlavoredMarkdown()` from `quartz.config.ts`
|
||||||
|
- Customizing features: `Plugin.ObsidianFlavoredMarkdown` has several other options to toggle on and off:
|
||||||
|
- `comments`: whether to enable `%%` style Obsidian comments. Defaults to `true`
|
||||||
|
- `highlight`: whether to enable `==` style highlights. Defaults to `true`
|
||||||
|
- `wikilinks`: whether to enable turning [[wikilinks]] into regular links. Defaults to `true`
|
||||||
|
- `callouts`: whether to enable [[callouts]]. Defaults to `true`
|
||||||
|
- `mermaid`: whether to enable [[Mermaid diagrams]]. Defaults to `true`
|
||||||
|
- `parseTags`: whether to try and parse tags in the content body. Defaults to `true`
|
||||||
|
- `enableInHtmlEmbed`: whether to try and parse Obsidian flavoured markdown in raw HTML. Defaults to `false`
|
||||||
|
- Link resolution behaviour:
|
||||||
|
- Disabling: remove all instances of `Plugin.CrawlLinks()` from `quartz.config.ts`
|
||||||
|
- Changing link resolution preference: set `markdownLinkResolution` to one of `absolute`, `relative` or `shortest`
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: "OxHugo Compatibility"
|
|
||||||
tags:
|
tags:
|
||||||
- feature/transformer
|
- plugin/transformer
|
||||||
---
|
---
|
||||||
|
|
||||||
[org-roam](https://www.orgroam.com/) is a plain-text personal knowledge management system for [emacs](https://en.wikipedia.org/wiki/Emacs). [ox-hugo](https://github.com/kaushalmodi/ox-hugo) is org exporter backend that exports `org-mode` files to [Hugo](https://gohugo.io/) compatible Markdown.
|
[org-roam](https://www.orgroam.com/) is a plain-text personal knowledge management system for [emacs](https://en.wikipedia.org/wiki/Emacs). [ox-hugo](https://github.com/kaushalmodi/ox-hugo) is org exporter backend that exports `org-mode` files to [Hugo](https://gohugo.io/) compatible Markdown.
|
||||||
|
|
||||||
Because the Markdown generated by ox-hugo is not pure Markdown but Hugo specific, we need to transform it to fit into Quartz. This is done by the [[OxHugoFlavoredMarkdown]] plugin. Even though this plugin was written with `ox-hugo` in mind, it should work for any Hugo specific Markdown.
|
Because the Markdown generated by ox-hugo is not pure Markdown but Hugo specific, we need to transform it to fit into Quartz. This is done by `Plugin.OxHugoFlavouredMarkdown`. Even though this [[making plugins|plugin]] was written with `ox-hugo` in mind, it should work for any Hugo specific Markdown.
|
||||||
|
|
||||||
```typescript title="quartz.config.ts"
|
```typescript title="quartz.config.ts"
|
||||||
plugins: {
|
plugins: {
|
||||||
@@ -26,4 +25,15 @@ Quartz by default doesn't understand `org-roam` files as they aren't Markdown. Y
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
This functionality is provided by the [[OxHugoFlavoredMarkdown]] plugin. See the plugin page for customization options.
|
- Link resolution
|
||||||
|
- `wikilinks`: Whether to replace `{{ relref }}` with Quartz [[wikilinks]]
|
||||||
|
- `removePredefinedAnchor`: Whether to remove [pre-defined anchor set by ox-hugo](https://ox-hugo.scripter.co/doc/anchors/).
|
||||||
|
- Image handling
|
||||||
|
- `replaceFigureWithMdImg`: Whether to replace `<figure/>` with `![]()`
|
||||||
|
- Formatting
|
||||||
|
- `removeHugoShortcode`: Whether to remove hugo shortcode syntax (`{{}}`)
|
||||||
|
- `replaceOrgLatex`: Whether to replace org-mode formatting for latex fragments with what `Plugin.Latex` supports.
|
||||||
|
|
||||||
|
> [!warning]
|
||||||
|
>
|
||||||
|
> While you can use `Plugin.OxHugoFlavoredMarkdown` and `Plugin.ObsidianFlavoredMarkdown` together, it's not recommended because it might mutate the file in unexpected ways. Use with caution.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
Quartz emits an RSS feed for all the content on your site by generating an `index.xml` file that RSS readers can subscribe to. Because of the RSS spec, this requires the `baseUrl` property in your [[configuration]] to be set properly for RSS readers to pick it up properly.
|
Quartz creates an RSS feed for all the content on your site by generating an `index.xml` file that RSS readers can subscribe to. Because of the RSS spec, this requires the `baseUrl` property in your [[configuration]] to be set properly for RSS readers to pick it up properly.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
This functionality is provided by the [[ContentIndex]] plugin. See the plugin page for customization options.
|
- Remove RSS feed: set the `enableRSS` field of `Plugin.ContentIndex` in `quartz.config.ts` to be `false`.
|
||||||
|
- Change number of entries: set the `rssLimit` field of `Plugin.ContentIndex` to be the desired value. It defaults to latest 10 items.
|
||||||
|
- Use rich HTML output in RSS: set `rssFullHtml` field of `Plugin.ContentIndex` to be `true`.
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Component.Breadcrumbs({
|
|||||||
rootName: "Home", // name of first/root element
|
rootName: "Home", // name of first/root element
|
||||||
resolveFrontmatterTitle: true, // whether to resolve folder names through frontmatter titles
|
resolveFrontmatterTitle: true, // whether to resolve folder names through frontmatter titles
|
||||||
hideOnRoot: true, // whether to hide breadcrumbs on root `index.md` page
|
hideOnRoot: true, // whether to hide breadcrumbs on root `index.md` page
|
||||||
showCurrentPage: true, // whether to display the current page in the breadcrumbs
|
showCurrentPage: true, // wether to display the current page in the breadcrumbs
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: Callouts
|
title: Callouts
|
||||||
tags:
|
tags:
|
||||||
- feature/transformer
|
- plugin/transformer
|
||||||
---
|
---
|
||||||
|
|
||||||
Quartz supports the same Admonition-callout syntax as Obsidian.
|
Quartz supports the same Admonition-callout syntax as Obsidian.
|
||||||
@@ -19,78 +19,68 @@ This includes
|
|||||||
See [documentation on supported types and syntax here](https://help.obsidian.md/Editing+and+formatting/Callouts).
|
See [documentation on supported types and syntax here](https://help.obsidian.md/Editing+and+formatting/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 [[ObsidianFlavoredMarkdown]] is _after_ [[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()`.
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
|
|
||||||
The callouts are a functionality of the [[ObsidianFlavoredMarkdown]] plugin. See the plugin page for how to enable or disable them.
|
- Disable callouts: simply pass `callouts: false` to the plugin: `Plugin.ObsidianFlavoredMarkdown({ callouts: false })`
|
||||||
|
- Editing icons: `quartz/plugins/transformers/ofm.ts`
|
||||||
You can edit the icons by customizing `quartz/styles/callouts.scss`.
|
|
||||||
|
|
||||||
### Add custom callouts
|
|
||||||
|
|
||||||
By default, custom callouts are handled by applying the `note` style. To make fancy ones, you have to add these lines to `custom.scss`.
|
|
||||||
|
|
||||||
```scss title="quartz/styles/custom.scss"
|
|
||||||
.callout {
|
|
||||||
&[data-callout="custom"] {
|
|
||||||
--color: #customcolor;
|
|
||||||
--border: #custombordercolor;
|
|
||||||
--bg: #custombg;
|
|
||||||
--callout-icon: url("data:image/svg+xml; utf8, <custom formatted svg>"); //SVG icon code
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!warning]
|
|
||||||
> Don't forget to ensure that the SVG is URL encoded before putting it in the CSS. You can use tools like [this one](https://yoksel.github.io/url-encoder/) to help you do that.
|
|
||||||
|
|
||||||
## Showcase
|
## Showcase
|
||||||
|
|
||||||
> [!info]
|
> [!info]
|
||||||
> Default title
|
> Default title
|
||||||
|
|
||||||
> [!question]+ Can callouts be _nested_?
|
> [!question]+ Can callouts be nested?
|
||||||
>
|
>
|
||||||
> > [!todo]- Yes!, they can. And collapsed!
|
> > [!todo]- Yes!, they can. And collapsed!
|
||||||
> >
|
> >
|
||||||
> > > [!example] You can even use multiple layers of nesting.
|
> > > [!example] You can even use multiple layers of nesting.
|
||||||
|
|
||||||
> [!note]
|
> [!EXAMPLE] Examples
|
||||||
> Aliases: "note"
|
>
|
||||||
|
> Aliases: example
|
||||||
|
|
||||||
> [!abstract]
|
> [!note] Notes
|
||||||
> Aliases: "abstract", "summary", "tldr"
|
>
|
||||||
|
> Aliases: note
|
||||||
|
|
||||||
> [!info]
|
> [!abstract] Summaries
|
||||||
> Aliases: "info"
|
>
|
||||||
|
> Aliases: abstract, summary, tldr
|
||||||
|
|
||||||
> [!todo]
|
> [!info] Info
|
||||||
> Aliases: "todo"
|
>
|
||||||
|
> Aliases: info, todo
|
||||||
|
|
||||||
> [!tip]
|
> [!tip] Hint
|
||||||
> Aliases: "tip", "hint", "important"
|
>
|
||||||
|
> Aliases: tip, hint, important
|
||||||
|
|
||||||
> [!success]
|
> [!success] Success
|
||||||
> Aliases: "success", "check", "done"
|
>
|
||||||
|
> Aliases: success, check, done
|
||||||
|
|
||||||
> [!question]
|
> [!question] Question
|
||||||
> Aliases: "question", "help", "faq"
|
>
|
||||||
|
> Aliases: question, help, faq
|
||||||
|
|
||||||
> [!warning]
|
> [!warning] Warning
|
||||||
> Aliases: "warning", "attention", "caution"
|
>
|
||||||
|
> Aliases: warning, caution, attention
|
||||||
|
|
||||||
> [!failure]
|
> [!failure] Failure
|
||||||
> Aliases: "failure", "missing", "fail"
|
>
|
||||||
|
> Aliases: failure, fail, missing
|
||||||
|
|
||||||
> [!danger]
|
> [!danger] Error
|
||||||
> Aliases: "danger", "error"
|
>
|
||||||
|
> Aliases: danger, error
|
||||||
|
|
||||||
> [!bug]
|
> [!bug] Bug
|
||||||
> Aliases: "bug"
|
>
|
||||||
|
> Aliases: bug
|
||||||
|
|
||||||
> [!example]
|
> [!quote] Quote
|
||||||
> Aliases: "example"
|
>
|
||||||
|
> Aliases: quote, cite
|
||||||
> [!quote]
|
|
||||||
> Aliases: "quote", "cite"
|
|
||||||
|
|||||||
@@ -12,12 +12,3 @@ Quartz supports darkmode out of the box that respects the user's theme preferenc
|
|||||||
- Component: `quartz/components/Darkmode.tsx`
|
- Component: `quartz/components/Darkmode.tsx`
|
||||||
- Style: `quartz/components/styles/darkmode.scss`
|
- Style: `quartz/components/styles/darkmode.scss`
|
||||||
- Script: `quartz/components/scripts/darkmode.inline.ts`
|
- Script: `quartz/components/scripts/darkmode.inline.ts`
|
||||||
|
|
||||||
You can also listen to the `themechange` event to perform any custom logic when the theme changes.
|
|
||||||
|
|
||||||
```js
|
|
||||||
document.addEventListener("themechange", (e) => {
|
|
||||||
console.log("Theme changed to " + e.detail.theme) // either "light" or "dark"
|
|
||||||
// your logic here
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Component.Explorer({
|
|||||||
title: "Explorer", // title of the explorer component
|
title: "Explorer", // title of the explorer component
|
||||||
folderClickBehavior: "collapse", // what happens when you click a folder ("link" to navigate to folder page on click or "collapse" to collapse folder on click)
|
folderClickBehavior: "collapse", // what happens when you click a folder ("link" to navigate to folder page on click or "collapse" to collapse folder on click)
|
||||||
folderDefaultState: "collapsed", // default state of folders ("collapsed" or "open")
|
folderDefaultState: "collapsed", // default state of folders ("collapsed" or "open")
|
||||||
useSavedState: true, // whether to use local storage to save "state" (which folders are opened) of explorer
|
useSavedState: true, // wether to use local storage to save "state" (which folders are opened) of explorer
|
||||||
// Sort order: folders first, then files. Sort folders and files alphabetically
|
// Sort order: folders first, then files. Sort folders and files alphabetically
|
||||||
sortFn: (a, b) => {
|
sortFn: (a, b) => {
|
||||||
... // default implementation shown later
|
... // default implementation shown later
|
||||||
@@ -42,7 +42,7 @@ When passing in your own options, you can omit any or all of these fields if you
|
|||||||
|
|
||||||
Want to customize it even more?
|
Want to customize it even more?
|
||||||
|
|
||||||
- Removing explorer: remove `Component.Explorer()` from `quartz.layout.ts`
|
- Removing table of contents: remove `Component.Explorer()` from `quartz.layout.ts`
|
||||||
- (optional): After removing the explorer component, you can move the [[table of contents | Table of Contents]] component back to the `left` part of the layout
|
- (optional): After removing the explorer component, you can move the [[table of contents | Table of Contents]] component back to the `left` part of the layout
|
||||||
- Changing `sort`, `filter` and `map` behavior: explained in [[#Advanced customization]]
|
- Changing `sort`, `filter` and `map` behavior: explained in [[#Advanced customization]]
|
||||||
- Component:
|
- Component:
|
||||||
@@ -179,34 +179,6 @@ Component.Explorer({
|
|||||||
|
|
||||||
## Advanced examples
|
## Advanced examples
|
||||||
|
|
||||||
> [!tip]
|
|
||||||
> When writing more complicated functions, the `layout` file can start to look very cramped.
|
|
||||||
> You can fix this by defining your functions in another file.
|
|
||||||
>
|
|
||||||
> ```ts title="functions.ts"
|
|
||||||
> import { Options } from "./quartz/components/ExplorerNode"
|
|
||||||
> export const mapFn: Options["mapFn"] = (node) => {
|
|
||||||
> // implement your function here
|
|
||||||
> }
|
|
||||||
> export const filterFn: Options["filterFn"] = (node) => {
|
|
||||||
> // implement your function here
|
|
||||||
> }
|
|
||||||
> export const sortFn: Options["sortFn"] = (a, b) => {
|
|
||||||
> // implement your function here
|
|
||||||
> }
|
|
||||||
> ```
|
|
||||||
>
|
|
||||||
> You can then import them like this:
|
|
||||||
>
|
|
||||||
> ```ts title="quartz.layout.ts"
|
|
||||||
> import { mapFn, filterFn, sortFn } from "./functions.ts"
|
|
||||||
> Component.Explorer({
|
|
||||||
> mapFn: mapFn,
|
|
||||||
> filterFn: filterFn,
|
|
||||||
> sortFn: sortFn,
|
|
||||||
> })
|
|
||||||
> ```
|
|
||||||
|
|
||||||
### Add emoji prefix
|
### Add emoji prefix
|
||||||
|
|
||||||
To add emoji prefixes (📁 for folders, 📄 for files), you could use a map function like this:
|
To add emoji prefixes (📁 for folders, 📄 for files), you could use a map function like this:
|
||||||
@@ -244,63 +216,30 @@ Notice how we customized the `order` array here. This is done because the defaul
|
|||||||
|
|
||||||
To fix this, we just changed around the order and apply the `sort` function before changing the display names in the `map` function.
|
To fix this, we just changed around the order and apply the `sort` function before changing the display names in the `map` function.
|
||||||
|
|
||||||
### Use `sort` with pre-defined sort order
|
> [!tip]
|
||||||
|
> When writing more complicated functions, the `layout` file can start to look very cramped.
|
||||||
Here's another example where a map containing file/folder names (as slugs) is used to define the sort order of the explorer in quartz. All files/folders that aren't listed inside of `nameOrderMap` will appear at the top of that folders hierarchy level.
|
> You can fix this by defining your functions in another file.
|
||||||
|
>
|
||||||
It's also worth mentioning, that the smaller the number set in `nameOrderMap`, the higher up the entry will be in the explorer. Incrementing every folder/file by 100, makes ordering files in their folders a lot easier. Lastly, this example still allows you to use a `mapFn` or frontmatter titles to change display names, as it uses slugs for `nameOrderMap` (which is unaffected by display name changes).
|
> ```ts title="functions.ts"
|
||||||
|
> import { Options } from "./quartz/components/ExplorerNode"
|
||||||
```ts title="quartz.layout.ts"
|
> export const mapFn: Options["mapFn"] = (node) => {
|
||||||
Component.Explorer({
|
> // implement your function here
|
||||||
sortFn: (a, b) => {
|
> }
|
||||||
const nameOrderMap: Record<string, number> = {
|
> export const filterFn: Options["filterFn"] = (node) => {
|
||||||
"poetry-folder": 100,
|
> // implement your function here
|
||||||
"essay-folder": 200,
|
> }
|
||||||
"research-paper-file": 201,
|
> export const sortFn: Options["sortFn"] = (a, b) => {
|
||||||
"dinosaur-fossils-file": 300,
|
> // implement your function here
|
||||||
"other-folder": 400,
|
> }
|
||||||
}
|
> ```
|
||||||
|
>
|
||||||
let orderA = 0
|
> You can then import them like this:
|
||||||
let orderB = 0
|
>
|
||||||
|
> ```ts title="quartz.layout.ts"
|
||||||
if (a.file && a.file.slug) {
|
> import { mapFn, filterFn, sortFn } from "./functions.ts"
|
||||||
orderA = nameOrderMap[a.file.slug] || 0
|
> Component.Explorer({
|
||||||
} else if (a.name) {
|
> mapFn: mapFn,
|
||||||
orderA = nameOrderMap[a.name] || 0
|
> filterFn: filterFn,
|
||||||
}
|
> sortFn: sortFn,
|
||||||
|
> })
|
||||||
if (b.file && b.file.slug) {
|
> ```
|
||||||
orderB = nameOrderMap[b.file.slug] || 0
|
|
||||||
} else if (b.name) {
|
|
||||||
orderB = nameOrderMap[b.name] || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return orderA - orderB
|
|
||||||
},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
For reference, this is how the quartz explorer window would look like with that example:
|
|
||||||
|
|
||||||
```
|
|
||||||
📖 Poetry Folder
|
|
||||||
📑 Essay Folder
|
|
||||||
⚗️ Research Paper File
|
|
||||||
🦴 Dinosaur Fossils File
|
|
||||||
🔮 Other Folder
|
|
||||||
```
|
|
||||||
|
|
||||||
And this is how the file structure would look like:
|
|
||||||
|
|
||||||
```
|
|
||||||
index.md
|
|
||||||
poetry-folder
|
|
||||||
index.md
|
|
||||||
essay-folder
|
|
||||||
index.md
|
|
||||||
research-paper-file.md
|
|
||||||
dinosaur-fossils-file.md
|
|
||||||
other-folder
|
|
||||||
index.md
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
---
|
---
|
||||||
title: Folder and Tag Listings
|
title: Folder and Tag Listings
|
||||||
tags:
|
tags:
|
||||||
- feature/emitter
|
- plugin/emitter
|
||||||
---
|
---
|
||||||
|
|
||||||
Quartz emits listing pages for any folders and tags you have.
|
Quartz creates listing pages for any folders and tags you have.
|
||||||
|
|
||||||
## Folder Listings
|
## Folder Listings
|
||||||
|
|
||||||
Quartz will generate an index page for all the pages under that folder. This includes any content that is multiple levels deep.
|
Quartz will generate an index page for all the pages under that folder. This includes any content that is multiple levels deep.
|
||||||
|
|
||||||
Additionally, Quartz will also generate pages for subfolders. Say you have a note in a nested folder `content/abc/def/note.md`. Then Quartz would generate a page for all the notes under `abc` _and_ a page for all the notes under `abc/def`.
|
Additionally, Quartz will also generate pages for subfolders. Say you have a note in a nested folder `content/abc/def/note.md`. Then, Quartz would generate a page for all the notes under `abc` _and_ a page for all the notes under `abc/def`.
|
||||||
|
|
||||||
You can link to the folder listing by referencing its name, plus a trailing slash, like this: `[[advanced/]]` (results in [[advanced/]]).
|
By default, Quartz will title the page `Folder: <name of folder>` and no description. You can override this by creating an `index.md` file in the folder with the `title` [[authoring content#Syntax|frontmatter]] field. Any content you write in this file will also be used in the description of the folder.
|
||||||
|
|
||||||
By default, Quartz will title the page `Folder: <folder name>` and no description. You can override this by creating an `index.md` file in the folder with the `title` [[authoring content#Syntax|frontmatter]] field. Any content you write in this file will also be used in the folder description.
|
|
||||||
|
|
||||||
For example, for the folder `content/posts`, you can add another file `content/posts/index.md` to add a specific description for it.
|
For example, for the folder `content/posts`, you can add another file `content/posts/index.md` to add a specific description for it.
|
||||||
|
|
||||||
@@ -22,12 +20,13 @@ For example, for the folder `content/posts`, you can add another file `content/p
|
|||||||
|
|
||||||
Quartz will also create an index page for each unique tag in your vault and render a list of all notes with that tag.
|
Quartz will also create an index page for each unique tag in your vault and render a list of all notes with that tag.
|
||||||
|
|
||||||
Quartz also supports tag hierarchies as well (e.g. `plugin/emitter`) and will also render a separate tag page for each level of the tag hierarchy. It will also create a default global tag index page at `/tags` that displays a list of all the tags in your Quartz.
|
Quartz also supports tag hierarchies as well (e.g. `plugin/emitter`) and will also render a separate tag page for each layer of the tag hierarchy. It will also create a default global tag index page at `/tags` that displays a list of all the tags in your Quartz.
|
||||||
|
|
||||||
You can link to the tag listing by referencing its name with a `tag/` prefix, like this: `[[tags/plugin]]` (results in [[tags/plugin]]).
|
Like folder listings, you can also provide a description and title for a tag page by creating a file for each tag. For example, if you wanted to create a custom description for the #component tag, you would create a file at `content/tags/component.md` with a title and description.
|
||||||
|
|
||||||
As with folder listings, you can also provide a description and title for a tag page by creating a file for each tag. For example, if you wanted to create a custom description for the #component tag, you would create a file at `content/tags/component.md` with a title and description.
|
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
|
|
||||||
The folder listings are a functionality of the [[FolderPage]] plugin, the tag listings of the [[TagPage]] plugin. See the plugin pages for customization options.
|
The layout for both the folder and content pages can be customized. By default, they use the `defaultListPageLayout` in `quartz.layouts.ts`. If you'd like to make more involved changes to the layout and don't mind editing some [[creating components|Quartz components]], you can take a look at `quartz/components/pages/FolderContent.tsx` and `quartz/components/pages/TagContent.tsx` respectively.
|
||||||
|
|
||||||
|
- Removing folder listings: remove `Plugin.FolderPage()` from `emitters` in `quartz.config.ts`
|
||||||
|
- Removing tag listings: remove `Plugin.TagPage()` from `emitters` in `quartz.config.ts`
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
---
|
|
||||||
title: Internationalization
|
|
||||||
---
|
|
||||||
|
|
||||||
Internationalization allows users to translate text in the Quartz interface into various supported languages without needing to make extensive code changes. This can be changed via the `locale` [[configuration]] field in `quartz.config.ts`.
|
|
||||||
|
|
||||||
The locale field generally follows a certain format: `{language}-{REGION}`
|
|
||||||
|
|
||||||
- `{language}` is usually a [2-letter lowercase language code](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes).
|
|
||||||
- `{REGION}` is usually a [2-letter uppercase region code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)
|
|
||||||
|
|
||||||
> [!tip] Interested in contributing?
|
|
||||||
> We [gladly welcome translation PRs](https://github.com/jackyzha0/quartz/tree/v4/quartz/i18n/locales)! To contribute a translation, do the following things:
|
|
||||||
>
|
|
||||||
> 1. In the `quartz/i18n/locales` folder, copy the `en-US.ts` file.
|
|
||||||
> 2. Rename it to `{language}-{REGION}.ts` so it matches a locale of the format shown above.
|
|
||||||
> 3. Fill in the translations!
|
|
||||||
> 4. Add the entry under `TRANSLATIONS` in `quartz/i18n/index.ts`.
|
|
||||||
@@ -8,8 +8,6 @@ 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.
|
||||||
|
|
||||||
Similar to Obsidian, [[quartz layout.png|images referenced using wikilinks]] can also be viewed as popups.
|
|
||||||
|
|
||||||
## 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`.
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
---
|
---
|
||||||
title: Private Pages
|
title: Private Pages
|
||||||
tags:
|
tags:
|
||||||
- feature/filter
|
- plugin/filter
|
||||||
---
|
---
|
||||||
|
|
||||||
There may be some notes you want to avoid publishing as a website. Quartz supports this through two mechanisms which can be used in conjunction:
|
There may be some notes you want to avoid publishing as a website. Quartz supports this through two mechanisms which can be used in conjunction:
|
||||||
|
|
||||||
## Filter Plugins
|
## Filter Plugins
|
||||||
|
|
||||||
[[making plugins#Filters|Filter plugins]] are plugins that filter out content based off of certain criteria. By default, Quartz uses the [[RemoveDrafts]] plugin which filters out any note that has `draft: true` in the frontmatter.
|
[[making plugins#Filters|Filter plugins]] are plugins that filter out content based off of certain criteria. By default, Quartz uses the `Plugin.RemoveDrafts` plugin which filters out any note that has `draft: true` in the frontmatter.
|
||||||
|
|
||||||
If you'd like to only publish a select number of notes, you can instead use [[ExplicitPublish]] which will filter out all notes except for any that have `publish: true` in the frontmatter.
|
If you'd like to only publish a select number of notes, you can instead use `Plugin.ExplicitPublish` which will filter out all notes except for any that have `publish: true` in the frontmatter.
|
||||||
|
|
||||||
> [!warning]
|
> [!warning]
|
||||||
> Regardless of the filter plugin used, **all non-markdown files will be emitted and available publically in the final build.** This includes files such as images, voice recordings, PDFs, etc. One way to prevent this and still be able to embed local images is to create a folder specifically for public media and add the following two patterns to the ignorePatterns array.
|
> Regardless of the filter plugin used, **all non-markdown files will be emitted and available publically in the final build.** This includes files such as images, voice recordings, PDFs, etc. One way to prevent this and still be able to embed local images is to create a folder specifically for public media and add the following two patterns to the ignorePatterns array.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Recent Notes
|
|||||||
tags: component
|
tags: component
|
||||||
---
|
---
|
||||||
|
|
||||||
Quartz can generate a list of recent notes based on some filtering and sorting criteria. Though this component isn't included in any [[layout]] by default, you can add it by using `Component.RecentNotes` in `quartz.layout.ts`.
|
Quartz can generate a list of recent notes for based on some filtering and sorting criteria. Though this component isn't included in any [[layout]] by default, you can add it by using `Component.RecentNotes`.
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: Syntax Highlighting
|
title: Syntax Highlighting
|
||||||
tags:
|
tags:
|
||||||
- feature/transformer
|
- plugin/transformer
|
||||||
---
|
---
|
||||||
|
|
||||||
Syntax highlighting in Quartz is completely done at build-time. This means that Quartz only ships pre-calculated CSS to highlight the right words so there is no heavy client-side bundle that does the syntax highlighting.
|
Syntax highlighting in Quartz is completely done at build-time. This means that Quartz only ships pre-calculated CSS to highlight the right words so there is no heavy client-side bundle that does the syntax highlighting.
|
||||||
@@ -130,4 +130,6 @@ const [name, setName] = useState('Taylor');
|
|||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
|
|
||||||
Syntax highlighting is a functionality of the [[SyntaxHighlighting]] plugin. See the plugin page for customization options.
|
- 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.
|
||||||
|
- Plugin: `quartz/plugins/transformers/syntax.ts`
|
||||||
|
|||||||
@@ -2,17 +2,25 @@
|
|||||||
title: "Table of Contents"
|
title: "Table of Contents"
|
||||||
tags:
|
tags:
|
||||||
- component
|
- component
|
||||||
- feature/transformer
|
- plugin/transformer
|
||||||
---
|
---
|
||||||
|
|
||||||
Quartz can automatically generate a table of contents (TOC) from a list of headings on each page. It will also show you your current scrolling position on the page by highlighting headings you've scrolled through with a different color.
|
Quartz can automatically generate a table of contents from a list of headings on each page. It will also show you your current scroll position on the site by marking headings you've scrolled through with a different colour.
|
||||||
|
|
||||||
You can hide the TOC on a page by adding `enableToc: false` to the frontmatter for that page.
|
By default, it will show all headers from H1 (`# Title`) all the way to H3 (`### Title`) and will only show the table of contents if there is more than 1 header on the page.
|
||||||
|
You can also hide the table of contents on a page by adding `enableToc: false` to the frontmatter for that page.
|
||||||
|
|
||||||
By default, the TOC shows all headings from H1 (`# Title`) to H3 (`### Title`) and is only displayed if there is more than one heading on the page.
|
> [!info]
|
||||||
|
> This feature requires both `Plugin.TableOfContents` in your `quartz.config.ts` and `Component.TableOfContents` in your `quartz.layout.ts` to function correctly.
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
|
|
||||||
The table of contents is a functionality of the [[TableOfContents]] plugin. See the plugin page for more customization options.
|
- Removing table of contents: remove all instances of `Plugin.TableOfContents()` from `quartz.config.ts`. and `Component.TableOfContents()` from `quartz.layout.ts`
|
||||||
|
- Changing the max depth: pass in a parameter to `Plugin.TableOfContents({ maxDepth: 4 })`
|
||||||
It also needs the `TableOfContents` component, which is displayed in the right sidebar by default. You can change this by customizing the [[layout]]. The TOC component can be configured with the `layout` parameter, which can either be `modern` (default) or `legacy`.
|
- Changing the minimum number of entries in the Table of Contents before it renders: pass in a parameter to `Plugin.TableOfContents({ minEntries: 3 })`
|
||||||
|
- Collapse the table of content by default: pass in a parameter to `Plugin.TableOfContents({ collapseByDefault: true })`
|
||||||
|
- Component: `quartz/components/TableOfContents.tsx`
|
||||||
|
- Style:
|
||||||
|
- Modern (default): `quartz/components/styles/toc.scss`
|
||||||
|
- Legacy Quartz 3 style: `quartz/components/styles/legacyToc.scss`
|
||||||
|
- Script: `quartz/components/scripts/toc.inline.ts`
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ title: Wikilinks
|
|||||||
|
|
||||||
Wikilinks were pioneered by earlier internet wikis to make it easier to write links across pages without needing to write Markdown or HTML links each time.
|
Wikilinks were pioneered by earlier internet wikis to make it easier to write links across pages without needing to write Markdown or HTML links each time.
|
||||||
|
|
||||||
Quartz supports Wikilinks by default and these links are resolved by Quartz using the [[CrawlLinks]] plugin. See the [Obsidian Help page on Internal Links](https://help.obsidian.md/Linking+notes+and+files/Internal+links) for more information on Wikilink syntax.
|
Quartz supports Wikilinks by default and these links are resolved by Quartz using `Plugin.CrawlLinks`. See the [Obsidian Help page on Internal Links](https://help.obsidian.md/Linking+notes+and+files/Internal+links) for more information on Wikilink syntax.
|
||||||
|
|
||||||
This is enabled as a part of [[Obsidian compatibility]] and can be configured and enabled/disabled from that plugin.
|
This is enabled as a part of [[Obsidian compatibility]] and can be configured and enabled/disabled from that plugin.
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ Press "Save and deploy" and Cloudflare should have a deployed version of your si
|
|||||||
To add a custom domain, check out [Cloudflare's documentation](https://developers.cloudflare.com/pages/platform/custom-domains/).
|
To add a custom domain, check out [Cloudflare's documentation](https://developers.cloudflare.com/pages/platform/custom-domains/).
|
||||||
|
|
||||||
> [!warning]
|
> [!warning]
|
||||||
> Cloudflare Pages performs a shallow clone by default, so if you rely on `git` for timestamps, it is recommended that you add `git fetch --unshallow &&` to the beginning of the build command (e.g., `git fetch --unshallow && npx quartz build`).
|
> Cloudflare Pages only allows shallow `git` clones so if you rely on `git` for timestamps, it is recommended you either add dates to your frontmatter (see [[authoring content#Syntax]]) or use another hosting provider.
|
||||||
|
|
||||||
## GitHub Pages
|
## GitHub Pages
|
||||||
|
|
||||||
@@ -225,28 +225,6 @@ pages:
|
|||||||
- public
|
- public
|
||||||
```
|
```
|
||||||
|
|
||||||
When `.gitlab-ci.yaml` is committed, GitLab will build and deploy the website as a GitLab Page. You can find the url under `Deploy > Pages` in the sidebar.
|
When `.gitlab-ci.yaml` is commited, GitLab will build and deploy the website as a GitLab Page. You can find the url under `Deploy > Pages` in the sidebar.
|
||||||
|
|
||||||
By default, the page is private and only visible when logged in to a GitLab account with access to the repository but can be opened in the settings under `Deploy` -> `Pages`.
|
By default, the page is private and only visible when logged in to a GitLab account with access to the repository but can be opened in the settings under `Deploy` -> `Pages`.
|
||||||
|
|
||||||
## Self-Hosting
|
|
||||||
|
|
||||||
Copy the `public` directory to your web server and configure it to serve the files. You can use any web server to host your site. Since Quartz generates links that do not include the `.html` extension, you need to let your web server know how to deal with it.
|
|
||||||
|
|
||||||
### Using Nginx
|
|
||||||
|
|
||||||
Here's an example of how to do this with Nginx:
|
|
||||||
|
|
||||||
```nginx title="nginx.conf"
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name example.com;
|
|
||||||
root /path/to/quartz/public;
|
|
||||||
index index.html;
|
|
||||||
error_page 404 /404.html;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri.html $uri/ =404;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -23,15 +23,14 @@ This will guide you through initializing your Quartz with content. Once you've d
|
|||||||
2. [[configuration|Configure]] Quartz's behaviour
|
2. [[configuration|Configure]] Quartz's behaviour
|
||||||
3. Change Quartz's [[layout]]
|
3. Change Quartz's [[layout]]
|
||||||
4. [[build|Build and preview]] Quartz
|
4. [[build|Build and preview]] Quartz
|
||||||
5. Sync your changes with [[setting up your GitHub repository|GitHub]]
|
5. [[hosting|Host]] Quartz online
|
||||||
6. [[hosting|Host]] Quartz online
|
|
||||||
|
|
||||||
If you prefer instructions in a video format you can try following Nicole van der Hoeven's
|
> [!info]
|
||||||
[video guide on how to set up Quartz!](https://www.youtube.com/watch?v=6s6DT1yN4dw&t=227s)
|
> Coming from Quartz 3? See the [[migrating from Quartz 3|migration guide]] for the differences between Quartz 3 and Quartz 4 and how to migrate.
|
||||||
|
|
||||||
## 🔧 Features
|
## 🔧 Features
|
||||||
|
|
||||||
- [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[features/Latex|Latex]], [[syntax highlighting]], [[popover previews]], [[Docker Support]], [[i18n|internationalization]] and [many more](./features) right out of the box
|
- [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], [[Docker Support]], and [many more](./features) right out of the box
|
||||||
- Hot-reload for both configuration and content
|
- Hot-reload for both configuration and content
|
||||||
- Simple JSX layouts and [[creating components|page components]]
|
- Simple JSX layouts and [[creating components|page components]]
|
||||||
- [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes
|
- [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
---
|
|
||||||
title: AliasRedirects
|
|
||||||
tags:
|
|
||||||
- plugin/emitter
|
|
||||||
---
|
|
||||||
|
|
||||||
This plugin emits HTML redirect pages for aliases and permalinks defined in the frontmatter of content files.
|
|
||||||
|
|
||||||
For example, A `foo.md` has the following frontmatter
|
|
||||||
|
|
||||||
```md title="foo.md"
|
|
||||||
---
|
|
||||||
title: "Foo"
|
|
||||||
alias:
|
|
||||||
- "bar"
|
|
||||||
---
|
|
||||||
```
|
|
||||||
|
|
||||||
The target `host.me/bar` will be redirected to `host.me/foo`
|
|
||||||
|
|
||||||
Note that these are permanent redirect.
|
|
||||||
|
|
||||||
The emitter supports the following aliases:
|
|
||||||
|
|
||||||
- `aliases`
|
|
||||||
- `alias`
|
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
|
||||||
|
|
||||||
This plugin has no configuration options.
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- Category: Emitter
|
|
||||||
- Function name: `Plugin.AliasRedirects()`.
|
|
||||||
- Source: [`quartz/plugins/emitters/aliases.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/aliases.ts).
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
title: Assets
|
|
||||||
tags:
|
|
||||||
- plugin/emitter
|
|
||||||
---
|
|
||||||
|
|
||||||
This plugin emits all non-Markdown static assets in your content folder (like images, videos, HTML, etc). The plugin respects the `ignorePatterns` in the global [[configuration]].
|
|
||||||
|
|
||||||
Note that all static assets will then be accessible through its path on your generated site, i.e: `host.me/path/to/static.pdf`
|
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
|
||||||
|
|
||||||
This plugin has no configuration options.
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- Category: Emitter
|
|
||||||
- Function name: `Plugin.Assets()`.
|
|
||||||
- Source: [`quartz/plugins/emitters/assets.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/assets.ts).
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
title: CNAME
|
|
||||||
tags:
|
|
||||||
- plugin/emitter
|
|
||||||
---
|
|
||||||
|
|
||||||
This plugin emits a `CNAME` record that points your subdomain to the default domain of your site.
|
|
||||||
|
|
||||||
If you want to use a custom domain name like `quartz.example.com` for the site, then this is needed.
|
|
||||||
|
|
||||||
See [[Hosting]] for more information.
|
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
|
||||||
|
|
||||||
This plugin has no configuration options.
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- Category: Emitter
|
|
||||||
- Function name: `Plugin.CNAME()`.
|
|
||||||
- Source: [`quartz/plugins/emitters/cname.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/cname.ts).
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
---
|
|
||||||
title: ComponentResources
|
|
||||||
tags:
|
|
||||||
- plugin/emitter
|
|
||||||
---
|
|
||||||
|
|
||||||
This plugin manages and emits the static resources required for the Quartz framework. This includes CSS stylesheets and JavaScript scripts that enhance the functionality and aesthetics of the generated site. See also the `cdnCaching` option in the `theme` section of the [[configuration]].
|
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
|
||||||
|
|
||||||
This plugin has no configuration options.
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- Category: Emitter
|
|
||||||
- Function name: `Plugin.ComponentResources()`.
|
|
||||||
- Source: [`quartz/plugins/emitters/componentResources.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/componentResources.ts).
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
---
|
|
||||||
title: ContentIndex
|
|
||||||
tags:
|
|
||||||
- plugin/emitter
|
|
||||||
---
|
|
||||||
|
|
||||||
This plugin emits both RSS and an XML sitemap for your site. The [[RSS Feed]] allows users to subscribe to content on your site and the sitemap allows search engines to better index your site. The plugin also emits a `contentIndex.json` file which is used by dynamic frontend components like search and graph.
|
|
||||||
|
|
||||||
This plugin emits a comprehensive index of the site's content, generating additional resources such as a sitemap, an RSS feed, and a
|
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
|
||||||
|
|
||||||
This plugin accepts the following configuration options:
|
|
||||||
|
|
||||||
- `enableSiteMap`: If `true` (default), generates a sitemap XML file (`sitemap.xml`) listing all site URLs for search engines in content discovery.
|
|
||||||
- `enableRSS`: If `true` (default), produces an RSS feed (`index.xml`) with recent content updates.
|
|
||||||
- `rssLimit`: Defines the maximum number of entries to include in the RSS feed, helping to focus on the most recent or relevant content. Defaults to `10`.
|
|
||||||
- `rssFullHtml`: If `true`, the RSS feed includes full HTML content. Otherwise it includes just summaries.
|
|
||||||
- `includeEmptyFiles`: If `true` (default), content files with no body text are included in the generated index and resources.
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- Category: Emitter
|
|
||||||
- Function name: `Plugin.ContentIndex()`.
|
|
||||||
- Source: [`quartz/plugins/emitters/contentIndex.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/contentIndex.ts).
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
---
|
|
||||||
title: ContentPage
|
|
||||||
tags:
|
|
||||||
- plugin/emitter
|
|
||||||
---
|
|
||||||
|
|
||||||
This plugin is a core component of the Quartz framework. It generates the HTML pages for each piece of Markdown content. It emits the full-page [[layout]], including headers, footers, and body content, among others.
|
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
|
||||||
|
|
||||||
This plugin has no configuration options.
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- Category: Emitter
|
|
||||||
- Function name: `Plugin.ContentPage()`.
|
|
||||||
- Source: [`quartz/plugins/emitters/contentPage.tsx`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/contentPage.tsx).
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
---
|
|
||||||
title: CrawlLinks
|
|
||||||
tags:
|
|
||||||
- plugin/transformer
|
|
||||||
---
|
|
||||||
|
|
||||||
This plugin parses links and processes them to point to the right places. It is also needed for embedded links (like images). See [[Obsidian compatibility]] for more information.
|
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
|
||||||
|
|
||||||
This plugin accepts the following configuration options:
|
|
||||||
|
|
||||||
- `markdownLinkResolution`: Sets the strategy for resolving Markdown paths, can be `"absolute"` (default), `"relative"` or `"shortest"`. You should use the same setting here as in [[Obsidian compatibility|Obsidian]].
|
|
||||||
- `absolute`: Path relative to the root of the content folder.
|
|
||||||
- `relative`: Path relative to the file you are linking from.
|
|
||||||
- `shortest`: Name of the file. If this isn't enough to identify the file, use the full absolute path.
|
|
||||||
- `prettyLinks`: If `true` (default), simplifies links by removing folder paths, making them more user friendly (e.g. `folder/deeply/nested/note` becomes `note`).
|
|
||||||
- `openLinksInNewTab`: If `true`, configures external links to open in a new tab. Defaults to `false`.
|
|
||||||
- `lazyLoad`: If `true`, adds lazy loading to resource elements (`img`, `video`, etc.) to improve page load performance. Defaults to `false`.
|
|
||||||
- `externalLinkIcon`: Adds an icon next to external links when `true` (default) to visually distinguishing them from internal links.
|
|
||||||
|
|
||||||
> [!warning]
|
|
||||||
> Removing this plugin is _not_ recommended and will likely break the page.
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- Category: Transformer
|
|
||||||
- Function name: `Plugin.CrawlLinks()`.
|
|
||||||
- Source: [`quartz/plugins/transformers/links.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/links.ts).
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
---
|
|
||||||
title: "CreatedModifiedDate"
|
|
||||||
tags:
|
|
||||||
- plugin/transformer
|
|
||||||
---
|
|
||||||
|
|
||||||
This plugin determines the created, modified, and published dates for a document using three potential data sources: frontmatter metadata, Git history, and the filesystem. See [[authoring content#Syntax]] for more information.
|
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
|
||||||
|
|
||||||
This plugin accepts the following configuration options:
|
|
||||||
|
|
||||||
- `priority`: The data sources to consult for date information. Highest priority first. Possible values are `"frontmatter"`, `"git"`, and `"filesystem"`. Defaults to `"frontmatter", "git", "filesystem"]`.
|
|
||||||
|
|
||||||
> [!warning]
|
|
||||||
> If you rely on `git` for dates, make sure `defaultDateType` is set to `modified` in `quartz.config.ts`.
|
|
||||||
>
|
|
||||||
> Depending on how you [[hosting|host]] your Quartz, the `filesystem` dates of your local files may not match the final dates. In these cases, it may be better to use `git` or `frontmatter` to guarantee correct dates.
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- Category: Transformer
|
|
||||||
- Function name: `Plugin.CreatedModifiedDate()`.
|
|
||||||
- Source: [`quartz/plugins/transformers/lastmod.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/lastmod.ts).
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
title: Description
|
|
||||||
tags:
|
|
||||||
- plugin/transformer
|
|
||||||
---
|
|
||||||
|
|
||||||
This plugin generates descriptions that are used as metadata for the HTML `head`, the [[RSS Feed]] and in [[folder and tag listings]] if there is no main body content, the description is used as the text between the title and the listing.
|
|
||||||
|
|
||||||
If the frontmatter contains a `description` property, it is used (see [[authoring content#Syntax]]). Otherwise, the plugin will do its best to use the first few sentences of the content to reach the target description length.
|
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
|
||||||
|
|
||||||
This plugin accepts the following configuration options:
|
|
||||||
|
|
||||||
- `descriptionLength`: the maximum length of the generated description. Default is 150 characters. The cut off happens after the first _sentence_ that ends after the given length.
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- Category: Transformer
|
|
||||||
- Function name: `Plugin.Description()`.
|
|
||||||
- Source: [`quartz/plugins/transformers/description.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/description.ts).
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
---
|
|
||||||
title: ExplicitPublish
|
|
||||||
tags:
|
|
||||||
- plugin/filter
|
|
||||||
---
|
|
||||||
|
|
||||||
This plugin filters content based on an explicit `publish` flag in the frontmatter, allowing only content that is explicitly marked for publication to pass through. It's the opt-in version of [[RemoveDrafts]]. See [[private pages]] for more information.
|
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
|
||||||
|
|
||||||
This plugin has no configuration options.
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- Category: Emitter
|
|
||||||
- Function name: `Plugin.ExplicitPublish()`.
|
|
||||||
- Source: [`quartz/plugins/filters/explicit.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/filters/explicit.ts).
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
title: FolderPage
|
|
||||||
tags:
|
|
||||||
- plugin/emitter
|
|
||||||
---
|
|
||||||
|
|
||||||
This plugin generates index pages for folders, creating a listing page for each folder that contains multiple content files. See [[folder and tag listings]] for more information.
|
|
||||||
|
|
||||||
Example: [[advanced/|Advanced]]
|
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
|
||||||
|
|
||||||
This plugin has no configuration options.
|
|
||||||
|
|
||||||
The pages are displayed using the `defaultListPageLayout` in `quartz.layouts.ts`. For the content, the `FolderContent` component is used. If you want to modify the layout, you must edit it directly (`quartz/components/pages/FolderContent.tsx`).
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- Category: Emitter
|
|
||||||
- Function name: `Plugin.FolderPage()`.
|
|
||||||
- Source: [`quartz/plugins/emitters/folderPage.tsx`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/folderPage.tsx).
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Frontmatter"
|
|
||||||
tags:
|
|
||||||
- plugin/transformer
|
|
||||||
---
|
|
||||||
|
|
||||||
This plugin parses the frontmatter of the page using the [gray-matter](https://github.com/jonschlinkert/gray-matter) library. See [[authoring content#Syntax]], [[Obsidian compatibility]] and [[OxHugo compatibility]] for more information.
|
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
|
||||||
|
|
||||||
This plugin accepts the following configuration options:
|
|
||||||
|
|
||||||
- `delimiters`: the delimiters to use for the frontmatter. Can have one value (e.g. `"---"`) or separate values for opening and closing delimiters (e.g. `["---", "~~~"]`). Defaults to `"---"`.
|
|
||||||
- `language`: the language to use for parsing the frontmatter. Can be `yaml` (default) or `toml`.
|
|
||||||
|
|
||||||
> [!warning]
|
|
||||||
> This plugin must not be removed, otherwise Quartz will break.
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- Category: Transformer
|
|
||||||
- Function name: `Plugin.Frontmatter()`.
|
|
||||||
- Source: [`quartz/plugins/transformers/frontmatter.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/frontmatter.ts).
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
title: GitHubFlavoredMarkdown
|
|
||||||
tags:
|
|
||||||
- plugin/transformer
|
|
||||||
---
|
|
||||||
|
|
||||||
This plugin enhances Markdown processing to support GitHub Flavored Markdown (GFM) which adds features like autolink literals, footnotes, strikethrough, tables and tasklists.
|
|
||||||
|
|
||||||
In addition, this plugin adds optional features for typographic refinement (such as converting straight quotes to curly quotes, dashes to en-dashes/em-dashes, and ellipses) and automatic heading links as a symbol that appears next to the heading on hover.
|
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
|
||||||
|
|
||||||
This plugin accepts the following configuration options:
|
|
||||||
|
|
||||||
- `enableSmartyPants`: When true, enables typographic enhancements. Default is true.
|
|
||||||
- `linkHeadings`: When true, automatically adds links to headings. Default is true.
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- Category: Transformer
|
|
||||||
- Function name: `Plugin.GitHubFlavoredMarkdown()`.
|
|
||||||
- Source: [`quartz/plugins/transformers/gfm.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/gfm.ts).
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
---
|
|
||||||
title: HardLineBreaks
|
|
||||||
tags:
|
|
||||||
- plugin/transformer
|
|
||||||
---
|
|
||||||
|
|
||||||
This plugin automatically converts single line breaks in Markdown text into hard line breaks in the HTML output. This plugin is not enabled by default as this doesn't follow the semantics of actual Markdown but you may enable it if you'd like parity with [[Obsidian compatibility|Obsidian]].
|
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
|
||||||
|
|
||||||
This plugin has no configuration options.
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- Category: Transformer
|
|
||||||
- Function name: `Plugin.HardLineBreaks()`.
|
|
||||||
- Source: [`quartz/plugins/transformers/linebreaks.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/linebreaks.ts).
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Latex"
|
|
||||||
tags:
|
|
||||||
- plugin/transformer
|
|
||||||
---
|
|
||||||
|
|
||||||
This plugin adds LaTeX support to Quartz. See [[features/Latex|Latex]] for more information.
|
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
|
||||||
|
|
||||||
This plugin accepts the following configuration options:
|
|
||||||
|
|
||||||
- `renderEngine`: the engine to use to render LaTeX equations. Can be `"katex"` for [KaTeX](https://katex.org/) or `"mathjax"` for [MathJax](https://www.mathjax.org/) [SVG rendering](https://docs.mathjax.org/en/latest/output/svg.html). Defaults to KaTeX.
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- Category: Transformer
|
|
||||||
- Function name: `Plugin.Latex()`.
|
|
||||||
- Source: [`quartz/plugins/transformers/latex.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/latex.ts).
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
---
|
|
||||||
title: NotFoundPage
|
|
||||||
tags:
|
|
||||||
- plugin/emitter
|
|
||||||
---
|
|
||||||
|
|
||||||
This plugin emits a 404 (Not Found) page for broken or non-existent URLs.
|
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
|
||||||
|
|
||||||
This plugin has no configuration options.
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- Category: Emitter
|
|
||||||
- Function name: `Plugin.NotFoundPage()`.
|
|
||||||
- Source: [`quartz/plugins/emitters/404.tsx`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/404.tsx).
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
---
|
|
||||||
title: ObsidianFlavoredMarkdown
|
|
||||||
tags:
|
|
||||||
- plugin/transformer
|
|
||||||
---
|
|
||||||
|
|
||||||
This plugin provides support for [[Obsidian compatibility]].
|
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
|
||||||
|
|
||||||
This plugin accepts the following configuration options:
|
|
||||||
|
|
||||||
- `comments`: If `true` (default), enables parsing of `%%` style Obsidian comment blocks.
|
|
||||||
- `highlight`: If `true` (default), enables parsing of `==` style highlights within content.
|
|
||||||
- `wikilinks`:If `true` (default), turns [[wikilinks]] into regular links.
|
|
||||||
- `callouts`: If `true` (default), adds support for [[callouts|callout]] blocks for emphasizing content.
|
|
||||||
- `mermaid`: If `true` (default), enables [[Mermaid diagrams|Mermaid diagram]] rendering within Markdown files.
|
|
||||||
- `parseTags`: If `true` (default), parses and links tags within the content.
|
|
||||||
- `parseArrows`: If `true` (default), transforms arrow symbols into their HTML character equivalents.
|
|
||||||
- `parseBlockReferences`: If `true` (default), handles block references, linking to specific content blocks.
|
|
||||||
- `enableInHtmlEmbed`: If `true`, allows embedding of content directly within HTML. Defaults to `false`.
|
|
||||||
- `enableYouTubeEmbed`: If `true` (default), enables the embedding of YouTube videos using external image Markdown syntax.
|
|
||||||
- `enableVideoEmbed`: If `true` (default), enables the embedding of video files.
|
|
||||||
- `enableCheckbox`: If `true`, adds support for interactive checkboxes in content. Defaults to `false`.
|
|
||||||
|
|
||||||
> [!warning]
|
|
||||||
> Don't remove this plugin if you're using [[Obsidian compatibility|Obsidian]] to author the content!
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- Category: Transformer
|
|
||||||
- Function name: `Plugin.ObsidianFlavoredMarkdown()`.
|
|
||||||
- Source: [`quartz/plugins/transformers/toc.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/toc.ts).
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
---
|
|
||||||
title: OxHugoFlavoredMarkdown
|
|
||||||
tags:
|
|
||||||
- plugin/transformer
|
|
||||||
---
|
|
||||||
|
|
||||||
This plugin provides support for [ox-hugo](https://github.com/kaushalmodi/ox-hugo) compatibility. See [[OxHugo compatibility]] for more information.
|
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
|
||||||
|
|
||||||
This plugin accepts the following configuration options:
|
|
||||||
|
|
||||||
- `wikilinks`: If `true` (default), converts Hugo `{{ relref }}` shortcodes to Quartz [[wikilinks]].
|
|
||||||
- `removePredefinedAnchor`: If `true` (default), strips predefined anchors from headings.
|
|
||||||
- `removeHugoShortcode`: If `true` (default), removes Hugo shortcode syntax (`{{}}`) from the content.
|
|
||||||
- `replaceFigureWithMdImg`: If `true` (default), replaces `<figure/>` with `![]()`.
|
|
||||||
- `replaceOrgLatex`: If `true` (default), converts Org-mode [[features/Latex|Latex]] fragments to Quartz-compatible LaTeX wrapped in `$` (for inline) and `$$` (for block equations).
|
|
||||||
|
|
||||||
> [!warning]
|
|
||||||
> While you can use this together with [[ObsidianFlavoredMarkdown]], it's not recommended because it might mutate the file in unexpected ways. Use with caution.
|
|
||||||
>
|
|
||||||
> If you use `toml` frontmatter, make sure to configure the [[Frontmatter]] plugin accordingly. See [[OxHugo compatibility]] for an example.
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- Category: Transformer
|
|
||||||
- Function name: `Plugin.OxHugoFlavoredMarkdown()`.
|
|
||||||
- Source: [`quartz/plugins/transformers/oxhugofm.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/oxhugofm.ts).
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
---
|
|
||||||
title: RemoveDrafts
|
|
||||||
tags:
|
|
||||||
- plugin/filter
|
|
||||||
---
|
|
||||||
|
|
||||||
This plugin filters out content from your vault, so that only finalized content is made available. This prevents [[private pages]] from being published. By default, it filters out all pages with `draft: true` in the frontmatter and leaves all other pages intact.
|
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
|
||||||
|
|
||||||
This plugin has no configuration options.
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- Category: Filter
|
|
||||||
- Function name: `Plugin.RemoveDrafts()`.
|
|
||||||
- Source: [`quartz/plugins/filters/draft.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/filters/draft.ts).
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
title: Static
|
|
||||||
tags:
|
|
||||||
- plugin/emitter
|
|
||||||
---
|
|
||||||
|
|
||||||
This plugin emits all static resources needed by Quartz. This is used, for example, for fonts and images that need a stable position, such as banners and icons. The plugin respects the `ignorePatterns` in the global [[configuration]].
|
|
||||||
|
|
||||||
> [!important]
|
|
||||||
> This is different from [[Assets]]. The resources from the [[Static]] plugin are located under `quartz/static`, whereas [[Assets]] renders all static resources under `content` and is used for images, videos, audio, etc. that are directly referenced by your markdown content.
|
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
|
||||||
|
|
||||||
This plugin has no configuration options.
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- Category: Emitter
|
|
||||||
- Function name: `Plugin.Static()`.
|
|
||||||
- Source: [`quartz/plugins/emitters/static.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/static.ts).
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
title: "SyntaxHighlighting"
|
|
||||||
tags:
|
|
||||||
- plugin/transformer
|
|
||||||
---
|
|
||||||
|
|
||||||
This plugin is used to add syntax highlighting to code blocks in Quartz. See [[syntax highlighting]] for more information.
|
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
|
||||||
|
|
||||||
This plugin accepts the following configuration options:
|
|
||||||
|
|
||||||
- `theme`: a separate id of one of the [themes bundled with Shikiji](https://shikiji.netlify.app/themes). One for light mode and one for dark mode. Defaults to `theme: { light: "github-light", dark: "github-dark" }`.
|
|
||||||
- `keepBackground`: If set to `true`, the background of the Shikiji theme will be used. With `false` (default) the Quartz theme color for background will be used instead.
|
|
||||||
|
|
||||||
In addition, you can further override the colours in the `quartz/styles/syntax.scss` file.
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- Category: Transformer
|
|
||||||
- Function name: `Plugin.SyntaxHighlighting()`.
|
|
||||||
- Source: [`quartz/plugins/transformers/syntax.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/syntax.ts).
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
---
|
|
||||||
title: TableOfContents
|
|
||||||
tags:
|
|
||||||
- plugin/transformer
|
|
||||||
---
|
|
||||||
|
|
||||||
This plugin generates a table of contents (TOC) for Markdown documents. See [[table of contents]] for more information.
|
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
|
||||||
|
|
||||||
This plugin accepts the following configuration options:
|
|
||||||
|
|
||||||
- `maxDepth`: Limits the depth of headings included in the TOC, ranging from `1` (top level headings only) to `6` (all heading levels). Default is `3`.
|
|
||||||
- `minEntries`: The minimum number of heading entries required for the TOC to be displayed. Default is `1`.
|
|
||||||
- `showByDefault`: If `true` (default), the TOC should be displayed by default. Can be overridden by frontmatter settings.
|
|
||||||
- `collapseByDefault`: If `true`, the TOC will start in a collapsed state. Default is `false`.
|
|
||||||
|
|
||||||
> [!warning]
|
|
||||||
> This plugin needs the `Component.TableOfContents` component in `quartz.layout.ts` to determine where to display the TOC. Without it, nothing will be displayed. They should always be added or removed together.
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- Category: Transformer
|
|
||||||
- Function name: `Plugin.TableOfContents()`.
|
|
||||||
- Source: [`quartz/plugins/transformers/toc.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/toc.ts).
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
title: TagPage
|
|
||||||
tags:
|
|
||||||
- plugin/emitter
|
|
||||||
---
|
|
||||||
|
|
||||||
This plugin emits dedicated pages for each tag used in the content. See [[folder and tag listings]] for more information.
|
|
||||||
|
|
||||||
> [!note]
|
|
||||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
|
||||||
|
|
||||||
This plugin has no configuration options.
|
|
||||||
|
|
||||||
The pages are displayed using the `defaultListPageLayout` in `quartz.layouts.ts`. For the content, the `TagContent` component is used. If you want to modify the layout, you must edit it directly (`quartz/components/pages/TagContent.tsx`).
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- Category: Emitter
|
|
||||||
- Function name: `Plugin.AliasRedirects()`.
|
|
||||||
- Source: [`quartz/plugins/emitters/tagPage.tsx`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/tagPage.tsx).
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
---
|
|
||||||
title: Plugins
|
|
||||||
---
|
|
||||||
@@ -15,34 +15,25 @@ At the top of your repository on GitHub.com's Quick Setup page, click the clipb
|
|||||||
In your terminal of choice, navigate to the root of your Quartz folder. Then, run the following commands, replacing `REMOTE-URL` with the URL you just copied from the previous step.
|
In your terminal of choice, navigate to the root of your Quartz folder. Then, run the following commands, replacing `REMOTE-URL` with the URL you just copied from the previous step.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# list all the repositories that are tracked
|
# add your repository
|
||||||
git remote -v
|
git remote add origin REMOTE-URL
|
||||||
|
|
||||||
# if the origin doesn't match your own repository, set your repository as the origin
|
# track the main quartz repository for updates
|
||||||
git remote set-url origin REMOTE-URL
|
|
||||||
|
|
||||||
# if you don't have upstream as a remote, add it so updates work
|
|
||||||
git remote add upstream https://github.com/jackyzha0/quartz.git
|
git remote add upstream https://github.com/jackyzha0/quartz.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, you can sync the content to upload it to your repository. This is a helper command that will do the initial push of your content to your repository.
|
To verify that you set the remote URL correctly, run the following command.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git remote -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, you can sync the content to upload it to your repository.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx quartz sync --no-pull
|
npx quartz sync --no-pull
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!warning]- `fatal: --[no-]autostash option is only valid with --rebase`
|
> [!hint]
|
||||||
> You may have an outdated version of `git`. Updating `git` should fix this issue.
|
> If `npx quartz sync` fails with `fatal: --[no-]autostash option is only valid with --rebase`, you
|
||||||
|
> may have an outdated version of `git`. Updating `git` should fix this issue.
|
||||||
In future updates, you can simply run `npx quartz sync` every time you want to push updates to your repository.
|
|
||||||
|
|
||||||
> [!hint] Flags and options
|
|
||||||
> For full help options, you can run `npx quartz sync --help`.
|
|
||||||
>
|
|
||||||
> Most of these have sensible defaults but you can override them if you have a custom setup:
|
|
||||||
>
|
|
||||||
> - `-d` or `--directory`: the content folder. This is normally just `content`
|
|
||||||
> - `-v` or `--verbose`: print out extra logging information
|
|
||||||
> - `--commit` or `--no-commit`: whether to make a `git` commit for your changes
|
|
||||||
> - `--push` or `--no-push`: whether to push updates to your GitHub fork of Quartz
|
|
||||||
> - `--pull` or `--no-pull`: whether to try and pull in any updates from your GitHub fork (i.e. from other devices) before pushing
|
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ Want to see what Quartz can do? Here are some cool community gardens:
|
|||||||
- [Quartz Documentation (this site!)](https://quartz.jzhao.xyz/)
|
- [Quartz Documentation (this site!)](https://quartz.jzhao.xyz/)
|
||||||
- [Jacky Zhao's Garden](https://jzhao.xyz/)
|
- [Jacky Zhao's Garden](https://jzhao.xyz/)
|
||||||
- [Socratica Toolbox](https://toolbox.socratica.info/)
|
- [Socratica Toolbox](https://toolbox.socratica.info/)
|
||||||
|
- [Brandon Boswell's Garden](https://brandonkboswell.com)
|
||||||
|
- [Scaling Synthesis - A hypertext research notebook](https://scalingsynthesis.com/)
|
||||||
|
- [Data Dictionary 🧠](https://glossary.airbyte.com/)
|
||||||
|
- [sspaeti.com's Second Brain](https://brain.sspaeti.com/)
|
||||||
- [oldwinter の数字花园](https://garden.oldwinter.top/)
|
- [oldwinter の数字花园](https://garden.oldwinter.top/)
|
||||||
- [Aaron Pham's Garden](https://aarnphm.xyz/)
|
|
||||||
- [The Quantum Garden](https://quantumgardener.blog/)
|
|
||||||
- [Abhijeet's Math Wiki](https://abhmul.github.io/quartz/Math-Wiki/)
|
- [Abhijeet's Math Wiki](https://abhmul.github.io/quartz/Math-Wiki/)
|
||||||
|
- [Mike's AI Garden 🤖🪴](https://mwalton.me/)
|
||||||
- [Matt Dunn's Second Brain](https://mattdunn.info/)
|
- [Matt Dunn's Second Brain](https://mattdunn.info/)
|
||||||
- [Pelayo Arbues' Notes](https://pelayoarbues.github.io/)
|
- [Pelayo Arbues' Notes](https://pelayoarbues.github.io/)
|
||||||
- [Vince Imbat's Talahardin](https://vinceimbat.com/)
|
- [Vince Imbat's Talahardin](https://vinceimbat.com/)
|
||||||
@@ -18,13 +21,5 @@ Want to see what Quartz can do? Here are some cool community gardens:
|
|||||||
- [Pedro MC Fernandes's Topo da Mente](https://www.pmcf.xyz/topo-da-mente/)
|
- [Pedro MC Fernandes's Topo da Mente](https://www.pmcf.xyz/topo-da-mente/)
|
||||||
- [Mau Camargo's Notkesto](https://notes.camargomau.com/)
|
- [Mau Camargo's Notkesto](https://notes.camargomau.com/)
|
||||||
- [Caicai's Novels](https://imoko.cc/blog/caicai/)
|
- [Caicai's Novels](https://imoko.cc/blog/caicai/)
|
||||||
- [🌊 Collapsed Wave](https://collapsedwave.com/)
|
|
||||||
- [Sideny's 3D Artist's Handbook](https://sidney-eliot.github.io/3d-artists-handbook/)
|
|
||||||
- [Mike's AI Garden 🤖🪴](https://mwalton.me/)
|
|
||||||
- [Brandon Boswell's Garden](https://brandonkboswell.com)
|
|
||||||
- [Scaling Synthesis - A hypertext research notebook](https://scalingsynthesis.com/)
|
|
||||||
- [Data Dictionary 🧠](https://glossary.airbyte.com/)
|
|
||||||
- [sspaeti.com's Second Brain](https://brain.sspaeti.com/)
|
|
||||||
- [🪴Aster's notebook](https://notes.asterhu.com)
|
|
||||||
|
|
||||||
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)!
|
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)!
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
---
|
|
||||||
title: Plugins
|
|
||||||
---
|
|
||||||
3
globals.d.ts
vendored
3
globals.d.ts
vendored
@@ -4,10 +4,9 @@ export declare global {
|
|||||||
type: K,
|
type: K,
|
||||||
listener: (this: Document, ev: CustomEventMap[K]) => void,
|
listener: (this: Document, ev: CustomEventMap[K]) => void,
|
||||||
): void
|
): void
|
||||||
dispatchEvent<K extends keyof CustomEventMap>(ev: CustomEventMap[K] | UIEvent): void
|
dispatchEvent<K extends keyof CustomEventMap>(ev: CustomEventMap[K]): void
|
||||||
}
|
}
|
||||||
interface Window {
|
interface Window {
|
||||||
spaNavigate(url: URL, isBack: boolean = false)
|
spaNavigate(url: URL, isBack: boolean = false)
|
||||||
addCleanup(fn: (...args: any[]) => void)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
index.d.ts
vendored
1
index.d.ts
vendored
@@ -6,7 +6,6 @@ declare module "*.scss" {
|
|||||||
// dom custom event
|
// dom custom event
|
||||||
interface CustomEventMap {
|
interface CustomEventMap {
|
||||||
nav: CustomEvent<{ url: FullSlug }>
|
nav: CustomEvent<{ url: FullSlug }>
|
||||||
themechange: CustomEvent<{ theme: "light" | "dark" }>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare const fetchData: Promise<ContentIndex>
|
declare const fetchData: Promise<ContentIndex>
|
||||||
|
|||||||
1240
package-lock.json
generated
1240
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
44
package.json
44
package.json
@@ -2,7 +2,7 @@
|
|||||||
"name": "@jackyzha0/quartz",
|
"name": "@jackyzha0/quartz",
|
||||||
"description": "🌱 publish your digital garden and notes as a website",
|
"description": "🌱 publish your digital garden and notes as a website",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "4.2.3",
|
"version": "4.1.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"docs": "npx quartz build --serve -d docs",
|
"docs": "npx quartz build --serve -d docs",
|
||||||
"check": "tsc --noEmit && npx prettier . --check",
|
"check": "tsc --noEmit && npx prettier . --check",
|
||||||
"format": "npx prettier . --write",
|
"format": "npx prettier . --write",
|
||||||
"test": "tsx ./quartz/util/path.test.ts && tsx ./quartz/depgraph.test.ts",
|
"test": "tsx ./quartz/util/path.test.ts",
|
||||||
"profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1"
|
"profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -35,37 +35,38 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.7.0",
|
"@clack/prompts": "^0.7.0",
|
||||||
"@floating-ui/dom": "^1.6.3",
|
"@floating-ui/dom": "^1.5.3",
|
||||||
"@napi-rs/simple-git": "0.1.16",
|
"@napi-rs/simple-git": "0.1.9",
|
||||||
"async-mutex": "^0.4.1",
|
"async-mutex": "^0.4.0",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"cli-spinner": "^0.2.10",
|
"cli-spinner": "^0.2.10",
|
||||||
"d3": "^7.8.5",
|
"d3": "^7.8.5",
|
||||||
"esbuild-sass-plugin": "^2.16.1",
|
"esbuild-sass-plugin": "^2.16.0",
|
||||||
"flexsearch": "0.7.43",
|
"flexsearch": "0.7.21",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"globby": "^14.0.1",
|
"globby": "^14.0.0",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"hast-util-to-html": "^9.0.0",
|
"hast-util-to-html": "^9.0.0",
|
||||||
"hast-util-to-jsx-runtime": "^2.3.0",
|
"hast-util-to-jsx-runtime": "^2.3.0",
|
||||||
"hast-util-to-string": "^3.0.0",
|
"hast-util-to-string": "^3.0.0",
|
||||||
"is-absolute-url": "^4.0.1",
|
"is-absolute-url": "^4.0.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lightningcss": "^1.23.0",
|
"lightningcss": "^1.22.1",
|
||||||
"mdast-util-find-and-replace": "^3.0.1",
|
"mdast-util-find-and-replace": "^3.0.1",
|
||||||
"mdast-util-to-hast": "^13.1.0",
|
"mdast-util-to-hast": "^13.0.2",
|
||||||
"mdast-util-to-string": "^4.0.0",
|
"mdast-util-to-string": "^4.0.0",
|
||||||
"micromorph": "^0.4.5",
|
"micromorph": "^0.4.5",
|
||||||
"preact": "^10.19.5",
|
"plausible-tracker": "^0.3.8",
|
||||||
|
"preact": "^10.19.3",
|
||||||
"preact-render-to-string": "^6.3.1",
|
"preact-render-to-string": "^6.3.1",
|
||||||
"pretty-bytes": "^6.1.1",
|
"pretty-bytes": "^6.1.1",
|
||||||
"pretty-time": "^1.1.0",
|
"pretty-time": "^1.1.0",
|
||||||
"reading-time": "^1.5.0",
|
"reading-time": "^1.5.0",
|
||||||
"rehype-autolink-headings": "^7.1.0",
|
"rehype-autolink-headings": "^7.1.0",
|
||||||
"rehype-katex": "^7.0.0",
|
"rehype-katex": "^7.0.0",
|
||||||
"rehype-mathjax": "^6.0.0",
|
"rehype-mathjax": "^5.0.0",
|
||||||
"rehype-pretty-code": "^0.13.0",
|
"rehype-pretty-code": "^0.12.1",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"rehype-slug": "^6.0.0",
|
"rehype-slug": "^6.0.0",
|
||||||
"remark": "^15.0.1",
|
"remark": "^15.0.1",
|
||||||
@@ -74,35 +75,36 @@
|
|||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"remark-parse": "^11.0.0",
|
"remark-parse": "^11.0.0",
|
||||||
"remark-rehype": "^11.1.0",
|
"remark-rehype": "^11.0.0",
|
||||||
"remark-smartypants": "^2.0.0",
|
"remark-smartypants": "^2.0.0",
|
||||||
"rfdc": "^1.3.1",
|
|
||||||
"rimraf": "^5.0.5",
|
"rimraf": "^5.0.5",
|
||||||
"serve-handler": "^6.1.5",
|
"serve-handler": "^6.1.5",
|
||||||
"shiki": "^1.1.6",
|
"shikiji": "^0.8.7",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"to-vfile": "^8.0.0",
|
"to-vfile": "^8.0.0",
|
||||||
"toml": "^3.0.0",
|
"toml": "^3.0.0",
|
||||||
"unified": "^11.0.4",
|
"unified": "^11.0.4",
|
||||||
"unist-util-visit": "^5.0.0",
|
"unist-util-visit": "^5.0.0",
|
||||||
"vfile": "^6.0.1",
|
"vfile": "^6.0.1",
|
||||||
"workerpool": "^9.1.0",
|
"workerpool": "^8.0.0",
|
||||||
"ws": "^8.15.1",
|
"ws": "^8.15.1",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cli-spinner": "^0.2.3",
|
"@types/cli-spinner": "^0.2.3",
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
"@types/hast": "^3.0.4",
|
"@types/flexsearch": "^0.7.3",
|
||||||
|
"@types/hast": "^3.0.3",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/node": "^20.11.19",
|
"@types/node": "^20.1.2",
|
||||||
"@types/pretty-time": "^1.1.5",
|
"@types/pretty-time": "^1.1.5",
|
||||||
"@types/source-map-support": "^0.5.10",
|
"@types/source-map-support": "^0.5.10",
|
||||||
|
"@types/workerpool": "^6.4.7",
|
||||||
"@types/ws": "^8.5.10",
|
"@types/ws": "^8.5.10",
|
||||||
"@types/yargs": "^17.0.32",
|
"@types/yargs": "^17.0.32",
|
||||||
"esbuild": "^0.19.9",
|
"esbuild": "^0.19.9",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.1.1",
|
||||||
"tsx": "^4.7.1",
|
"tsx": "^4.6.2",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { QuartzConfig } from "./quartz/cfg"
|
import { QuartzConfig } from "./quartz/cfg"
|
||||||
import * as Plugin from "./quartz/plugins"
|
import * as Plugin from "./quartz/plugins"
|
||||||
|
|
||||||
/**
|
|
||||||
* Quartz 4.0 Configuration
|
|
||||||
*
|
|
||||||
* See https://quartz.jzhao.xyz/configuration for more information.
|
|
||||||
*/
|
|
||||||
const config: QuartzConfig = {
|
const config: QuartzConfig = {
|
||||||
configuration: {
|
configuration: {
|
||||||
pageTitle: "🪴 Quartz 4.0",
|
pageTitle: "🪴 Quartz 4.0",
|
||||||
@@ -14,12 +9,10 @@ const config: QuartzConfig = {
|
|||||||
analytics: {
|
analytics: {
|
||||||
provider: "plausible",
|
provider: "plausible",
|
||||||
},
|
},
|
||||||
locale: "en-US",
|
|
||||||
baseUrl: "quartz.jzhao.xyz",
|
baseUrl: "quartz.jzhao.xyz",
|
||||||
ignorePatterns: ["private", "templates", ".obsidian"],
|
ignorePatterns: ["private", "templates", ".obsidian"],
|
||||||
defaultDateType: "created",
|
defaultDateType: "created",
|
||||||
theme: {
|
theme: {
|
||||||
cdnCaching: true,
|
|
||||||
typography: {
|
typography: {
|
||||||
header: "Schibsted Grotesk",
|
header: "Schibsted Grotesk",
|
||||||
body: "Source Sans Pro",
|
body: "Source Sans Pro",
|
||||||
@@ -52,21 +45,15 @@ const config: QuartzConfig = {
|
|||||||
plugins: {
|
plugins: {
|
||||||
transformers: [
|
transformers: [
|
||||||
Plugin.FrontMatter(),
|
Plugin.FrontMatter(),
|
||||||
|
Plugin.TableOfContents(),
|
||||||
Plugin.CreatedModifiedDate({
|
Plugin.CreatedModifiedDate({
|
||||||
priority: ["frontmatter", "filesystem"],
|
priority: ["frontmatter", "filesystem"], // you can add 'git' here for last modified from Git but this makes the build slower
|
||||||
}),
|
|
||||||
Plugin.Latex({ renderEngine: "katex" }),
|
|
||||||
Plugin.SyntaxHighlighting({
|
|
||||||
theme: {
|
|
||||||
light: "github-light",
|
|
||||||
dark: "github-dark",
|
|
||||||
},
|
|
||||||
keepBackground: false,
|
|
||||||
}),
|
}),
|
||||||
|
Plugin.SyntaxHighlighting(),
|
||||||
Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }),
|
Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }),
|
||||||
Plugin.GitHubFlavoredMarkdown(),
|
Plugin.GitHubFlavoredMarkdown(),
|
||||||
Plugin.TableOfContents(),
|
|
||||||
Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
|
Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
|
||||||
|
Plugin.Latex({ renderEngine: "katex" }),
|
||||||
Plugin.Description(),
|
Plugin.Description(),
|
||||||
],
|
],
|
||||||
filters: [Plugin.RemoveDrafts()],
|
filters: [Plugin.RemoveDrafts()],
|
||||||
|
|||||||
@@ -37,13 +37,12 @@ export const defaultContentPageLayout: PageLayout = {
|
|||||||
|
|
||||||
// components for pages that display lists of pages (e.g. tags or folders)
|
// components for pages that display lists of pages (e.g. tags or folders)
|
||||||
export const defaultListPageLayout: PageLayout = {
|
export const defaultListPageLayout: PageLayout = {
|
||||||
beforeBody: [Component.Breadcrumbs(), Component.ArticleTitle(), Component.ContentMeta()],
|
beforeBody: [Component.ArticleTitle()],
|
||||||
left: [
|
left: [
|
||||||
Component.PageTitle(),
|
Component.PageTitle(),
|
||||||
Component.MobileOnly(Component.Spacer()),
|
Component.MobileOnly(Component.Spacer()),
|
||||||
Component.Search(),
|
Component.Search(),
|
||||||
Component.Darkmode(),
|
Component.Darkmode(),
|
||||||
Component.DesktopOnly(Component.Explorer()),
|
|
||||||
],
|
],
|
||||||
right: [],
|
right: [],
|
||||||
}
|
}
|
||||||
|
|||||||
294
quartz/build.ts
294
quartz/build.ts
@@ -3,13 +3,13 @@ sourceMapSupport.install(options)
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { PerfTimer } from "./util/perf"
|
import { PerfTimer } from "./util/perf"
|
||||||
import { rimraf } from "rimraf"
|
import { rimraf } from "rimraf"
|
||||||
import { GlobbyFilterFunction, isGitIgnored } from "globby"
|
import { isGitIgnored } from "globby"
|
||||||
import chalk from "chalk"
|
import chalk from "chalk"
|
||||||
import { parseMarkdown } from "./processors/parse"
|
import { parseMarkdown } from "./processors/parse"
|
||||||
import { filterContent } from "./processors/filter"
|
import { filterContent } from "./processors/filter"
|
||||||
import { emitContent } from "./processors/emit"
|
import { emitContent } from "./processors/emit"
|
||||||
import cfg from "../quartz.config"
|
import cfg from "../quartz.config"
|
||||||
import { FilePath, FullSlug, joinSegments, slugifyFilePath } from "./util/path"
|
import { FilePath, joinSegments, slugifyFilePath } from "./util/path"
|
||||||
import chokidar from "chokidar"
|
import chokidar from "chokidar"
|
||||||
import { ProcessedContent } from "./plugins/vfile"
|
import { ProcessedContent } from "./plugins/vfile"
|
||||||
import { Argv, BuildCtx } from "./util/ctx"
|
import { Argv, BuildCtx } from "./util/ctx"
|
||||||
@@ -17,26 +17,6 @@ import { glob, toPosixPath } from "./util/glob"
|
|||||||
import { trace } from "./util/trace"
|
import { trace } from "./util/trace"
|
||||||
import { options } from "./util/sourcemap"
|
import { options } from "./util/sourcemap"
|
||||||
import { Mutex } from "async-mutex"
|
import { Mutex } from "async-mutex"
|
||||||
import DepGraph from "./depgraph"
|
|
||||||
import { getStaticResourcesFromPlugins } from "./plugins"
|
|
||||||
|
|
||||||
type Dependencies = Record<string, DepGraph<FilePath> | null>
|
|
||||||
|
|
||||||
type BuildData = {
|
|
||||||
ctx: BuildCtx
|
|
||||||
ignored: GlobbyFilterFunction
|
|
||||||
mut: Mutex
|
|
||||||
initialSlugs: FullSlug[]
|
|
||||||
// TODO merge contentMap and trackedAssets
|
|
||||||
contentMap: Map<FilePath, ProcessedContent>
|
|
||||||
trackedAssets: Set<FilePath>
|
|
||||||
toRebuild: Set<FilePath>
|
|
||||||
toRemove: Set<FilePath>
|
|
||||||
lastBuildMs: number
|
|
||||||
dependencies: Dependencies
|
|
||||||
}
|
|
||||||
|
|
||||||
type FileEvent = "add" | "change" | "delete"
|
|
||||||
|
|
||||||
async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
||||||
const ctx: BuildCtx = {
|
const ctx: BuildCtx = {
|
||||||
@@ -60,7 +40,7 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
|||||||
|
|
||||||
const release = await mut.acquire()
|
const release = await mut.acquire()
|
||||||
perf.addEvent("clean")
|
perf.addEvent("clean")
|
||||||
await rimraf(path.join(output, "*"), { glob: true })
|
await rimraf(output)
|
||||||
console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`)
|
console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`)
|
||||||
|
|
||||||
perf.addEvent("glob")
|
perf.addEvent("glob")
|
||||||
@@ -75,24 +55,12 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
|||||||
|
|
||||||
const parsedFiles = await parseMarkdown(ctx, filePaths)
|
const parsedFiles = await parseMarkdown(ctx, filePaths)
|
||||||
const filteredContent = filterContent(ctx, parsedFiles)
|
const filteredContent = filterContent(ctx, parsedFiles)
|
||||||
|
|
||||||
const dependencies: Record<string, DepGraph<FilePath> | null> = {}
|
|
||||||
|
|
||||||
// Only build dependency graphs if we're doing a fast rebuild
|
|
||||||
if (argv.fastRebuild) {
|
|
||||||
const staticResources = getStaticResourcesFromPlugins(ctx)
|
|
||||||
for (const emitter of cfg.plugins.emitters) {
|
|
||||||
dependencies[emitter.name] =
|
|
||||||
(await emitter.getDependencyGraph?.(ctx, filteredContent, staticResources)) ?? null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await emitContent(ctx, filteredContent)
|
await emitContent(ctx, filteredContent)
|
||||||
console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`))
|
console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`))
|
||||||
release()
|
release()
|
||||||
|
|
||||||
if (argv.serve) {
|
if (argv.serve) {
|
||||||
return startServing(ctx, mut, parsedFiles, clientRefresh, dependencies)
|
return startServing(ctx, mut, parsedFiles, clientRefresh)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,229 +70,22 @@ async function startServing(
|
|||||||
mut: Mutex,
|
mut: Mutex,
|
||||||
initialContent: ProcessedContent[],
|
initialContent: ProcessedContent[],
|
||||||
clientRefresh: () => void,
|
clientRefresh: () => void,
|
||||||
dependencies: Dependencies, // emitter name: dep graph
|
|
||||||
) {
|
) {
|
||||||
const { argv } = ctx
|
const { argv } = ctx
|
||||||
|
|
||||||
// cache file parse results
|
const ignored = await isGitIgnored()
|
||||||
const contentMap = new Map<FilePath, ProcessedContent>()
|
const contentMap = new Map<FilePath, ProcessedContent>()
|
||||||
for (const content of initialContent) {
|
for (const content of initialContent) {
|
||||||
const [_tree, vfile] = content
|
const [_tree, vfile] = content
|
||||||
contentMap.set(vfile.data.filePath!, content)
|
contentMap.set(vfile.data.filePath!, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildData: BuildData = {
|
const initialSlugs = ctx.allSlugs
|
||||||
ctx,
|
let lastBuildMs = 0
|
||||||
mut,
|
const toRebuild: Set<FilePath> = new Set()
|
||||||
dependencies,
|
const toRemove: Set<FilePath> = new Set()
|
||||||
contentMap,
|
const trackedAssets: Set<FilePath> = new Set()
|
||||||
ignored: await isGitIgnored(),
|
async function rebuild(fp: string, action: "add" | "change" | "delete") {
|
||||||
initialSlugs: ctx.allSlugs,
|
|
||||||
toRebuild: new Set<FilePath>(),
|
|
||||||
toRemove: new Set<FilePath>(),
|
|
||||||
trackedAssets: new Set<FilePath>(),
|
|
||||||
lastBuildMs: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
const watcher = chokidar.watch(".", {
|
|
||||||
persistent: true,
|
|
||||||
cwd: argv.directory,
|
|
||||||
ignoreInitial: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const buildFromEntry = argv.fastRebuild ? partialRebuildFromEntrypoint : rebuildFromEntrypoint
|
|
||||||
watcher
|
|
||||||
.on("add", (fp) => buildFromEntry(fp, "add", clientRefresh, buildData))
|
|
||||||
.on("change", (fp) => buildFromEntry(fp, "change", clientRefresh, buildData))
|
|
||||||
.on("unlink", (fp) => buildFromEntry(fp, "delete", clientRefresh, buildData))
|
|
||||||
|
|
||||||
return async () => {
|
|
||||||
await watcher.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function partialRebuildFromEntrypoint(
|
|
||||||
filepath: string,
|
|
||||||
action: FileEvent,
|
|
||||||
clientRefresh: () => void,
|
|
||||||
buildData: BuildData, // note: this function mutates buildData
|
|
||||||
) {
|
|
||||||
const { ctx, ignored, dependencies, contentMap, mut, toRemove } = buildData
|
|
||||||
const { argv, cfg } = ctx
|
|
||||||
|
|
||||||
// don't do anything for gitignored files
|
|
||||||
if (ignored(filepath)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildStart = new Date().getTime()
|
|
||||||
buildData.lastBuildMs = buildStart
|
|
||||||
const release = await mut.acquire()
|
|
||||||
if (buildData.lastBuildMs > buildStart) {
|
|
||||||
release()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const perf = new PerfTimer()
|
|
||||||
console.log(chalk.yellow("Detected change, rebuilding..."))
|
|
||||||
|
|
||||||
// UPDATE DEP GRAPH
|
|
||||||
const fp = joinSegments(argv.directory, toPosixPath(filepath)) as FilePath
|
|
||||||
|
|
||||||
const staticResources = getStaticResourcesFromPlugins(ctx)
|
|
||||||
let processedFiles: ProcessedContent[] = []
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case "add":
|
|
||||||
// add to cache when new file is added
|
|
||||||
processedFiles = await parseMarkdown(ctx, [fp])
|
|
||||||
processedFiles.forEach(([tree, vfile]) => contentMap.set(vfile.data.filePath!, [tree, vfile]))
|
|
||||||
|
|
||||||
// update the dep graph by asking all emitters whether they depend on this file
|
|
||||||
for (const emitter of cfg.plugins.emitters) {
|
|
||||||
const emitterGraph =
|
|
||||||
(await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null
|
|
||||||
|
|
||||||
if (emitterGraph) {
|
|
||||||
const existingGraph = dependencies[emitter.name]
|
|
||||||
if (existingGraph !== null) {
|
|
||||||
existingGraph.mergeGraph(emitterGraph)
|
|
||||||
} else {
|
|
||||||
// might be the first time we're adding a mardown file
|
|
||||||
dependencies[emitter.name] = emitterGraph
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case "change":
|
|
||||||
// invalidate cache when file is changed
|
|
||||||
processedFiles = await parseMarkdown(ctx, [fp])
|
|
||||||
processedFiles.forEach(([tree, vfile]) => contentMap.set(vfile.data.filePath!, [tree, vfile]))
|
|
||||||
|
|
||||||
// only content files can have added/removed dependencies because of transclusions
|
|
||||||
if (path.extname(fp) === ".md") {
|
|
||||||
for (const emitter of cfg.plugins.emitters) {
|
|
||||||
// get new dependencies from all emitters for this file
|
|
||||||
const emitterGraph =
|
|
||||||
(await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null
|
|
||||||
|
|
||||||
// only update the graph if the emitter plugin uses the changed file
|
|
||||||
// eg. Assets plugin ignores md files, so we skip updating the graph
|
|
||||||
if (emitterGraph?.hasNode(fp)) {
|
|
||||||
// merge the new dependencies into the dep graph
|
|
||||||
dependencies[emitter.name]?.updateIncomingEdgesForNode(emitterGraph, fp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case "delete":
|
|
||||||
toRemove.add(fp)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (argv.verbose) {
|
|
||||||
console.log(`Updated dependency graphs in ${perf.timeSince()}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EMIT
|
|
||||||
perf.addEvent("rebuild")
|
|
||||||
let emittedFiles = 0
|
|
||||||
|
|
||||||
for (const emitter of cfg.plugins.emitters) {
|
|
||||||
const depGraph = dependencies[emitter.name]
|
|
||||||
|
|
||||||
// emitter hasn't defined a dependency graph. call it with all processed files
|
|
||||||
if (depGraph === null) {
|
|
||||||
if (argv.verbose) {
|
|
||||||
console.log(
|
|
||||||
`Emitter ${emitter.name} doesn't define a dependency graph. Calling it with all files...`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const files = [...contentMap.values()].filter(
|
|
||||||
([_node, vfile]) => !toRemove.has(vfile.data.filePath!),
|
|
||||||
)
|
|
||||||
|
|
||||||
const emittedFps = await emitter.emit(ctx, files, staticResources)
|
|
||||||
|
|
||||||
if (ctx.argv.verbose) {
|
|
||||||
for (const file of emittedFps) {
|
|
||||||
console.log(`[emit:${emitter.name}] ${file}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emittedFiles += emittedFps.length
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// only call the emitter if it uses this file
|
|
||||||
if (depGraph.hasNode(fp)) {
|
|
||||||
// re-emit using all files that are needed for the downstream of this file
|
|
||||||
// eg. for ContentIndex, the dep graph could be:
|
|
||||||
// a.md --> contentIndex.json
|
|
||||||
// b.md ------^
|
|
||||||
//
|
|
||||||
// if a.md changes, we need to re-emit contentIndex.json,
|
|
||||||
// and supply [a.md, b.md] to the emitter
|
|
||||||
const upstreams = [...depGraph.getLeafNodeAncestors(fp)] as FilePath[]
|
|
||||||
|
|
||||||
const upstreamContent = upstreams
|
|
||||||
// filter out non-markdown files
|
|
||||||
.filter((file) => contentMap.has(file))
|
|
||||||
// if file was deleted, don't give it to the emitter
|
|
||||||
.filter((file) => !toRemove.has(file))
|
|
||||||
.map((file) => contentMap.get(file)!)
|
|
||||||
|
|
||||||
const emittedFps = await emitter.emit(ctx, upstreamContent, staticResources)
|
|
||||||
|
|
||||||
if (ctx.argv.verbose) {
|
|
||||||
for (const file of emittedFps) {
|
|
||||||
console.log(`[emit:${emitter.name}] ${file}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emittedFiles += emittedFps.length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`)
|
|
||||||
|
|
||||||
// CLEANUP
|
|
||||||
const destinationsToDelete = new Set<FilePath>()
|
|
||||||
for (const file of toRemove) {
|
|
||||||
// remove from cache
|
|
||||||
contentMap.delete(file)
|
|
||||||
Object.values(dependencies).forEach((depGraph) => {
|
|
||||||
// remove the node from dependency graphs
|
|
||||||
depGraph?.removeNode(file)
|
|
||||||
// remove any orphan nodes. eg if a.md is deleted, a.html is orphaned and should be removed
|
|
||||||
const orphanNodes = depGraph?.removeOrphanNodes()
|
|
||||||
orphanNodes?.forEach((node) => {
|
|
||||||
// only delete files that are in the output directory
|
|
||||||
if (node.startsWith(argv.output)) {
|
|
||||||
destinationsToDelete.add(node)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
await rimraf([...destinationsToDelete])
|
|
||||||
|
|
||||||
toRemove.clear()
|
|
||||||
release()
|
|
||||||
clientRefresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rebuildFromEntrypoint(
|
|
||||||
fp: string,
|
|
||||||
action: FileEvent,
|
|
||||||
clientRefresh: () => void,
|
|
||||||
buildData: BuildData, // note: this function mutates buildData
|
|
||||||
) {
|
|
||||||
const { ctx, ignored, mut, initialSlugs, contentMap, toRebuild, toRemove, trackedAssets } =
|
|
||||||
buildData
|
|
||||||
|
|
||||||
const { argv } = ctx
|
|
||||||
|
|
||||||
// don't do anything for gitignored files
|
// don't do anything for gitignored files
|
||||||
if (ignored(fp)) {
|
if (ignored(fp)) {
|
||||||
return
|
return
|
||||||
@@ -349,12 +110,12 @@ async function rebuildFromEntrypoint(
|
|||||||
toRemove.add(filePath)
|
toRemove.add(filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildStart = new Date().getTime()
|
// debounce rebuilds every 250ms
|
||||||
buildData.lastBuildMs = buildStart
|
|
||||||
const release = await mut.acquire()
|
|
||||||
|
|
||||||
// there's another build after us, release and let them do it
|
const buildStart = new Date().getTime()
|
||||||
if (buildData.lastBuildMs > buildStart) {
|
lastBuildMs = buildStart
|
||||||
|
const release = await mut.acquire()
|
||||||
|
if (lastBuildMs > buildStart) {
|
||||||
release()
|
release()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -384,20 +145,33 @@ async function rebuildFromEntrypoint(
|
|||||||
|
|
||||||
// TODO: we can probably traverse the link graph to figure out what's safe to delete here
|
// TODO: we can probably traverse the link graph to figure out what's safe to delete here
|
||||||
// instead of just deleting everything
|
// instead of just deleting everything
|
||||||
await rimraf(path.join(argv.output, ".*"), { glob: true })
|
await rimraf(argv.output)
|
||||||
await emitContent(ctx, filteredContent)
|
await emitContent(ctx, filteredContent)
|
||||||
console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`))
|
console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`))
|
||||||
} catch (err) {
|
} catch {
|
||||||
console.log(chalk.yellow(`Rebuild failed. Waiting on a change to fix the error...`))
|
console.log(chalk.yellow(`Rebuild failed. Waiting on a change to fix the error...`))
|
||||||
if (argv.verbose) {
|
|
||||||
console.log(chalk.red(err))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
release()
|
release()
|
||||||
clientRefresh()
|
clientRefresh()
|
||||||
toRebuild.clear()
|
toRebuild.clear()
|
||||||
toRemove.clear()
|
toRemove.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
const watcher = chokidar.watch(".", {
|
||||||
|
persistent: true,
|
||||||
|
cwd: argv.directory,
|
||||||
|
ignoreInitial: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
watcher
|
||||||
|
.on("add", (fp) => rebuild(fp, "add"))
|
||||||
|
.on("change", (fp) => rebuild(fp, "change"))
|
||||||
|
.on("unlink", (fp) => rebuild(fp, "delete"))
|
||||||
|
|
||||||
|
return async () => {
|
||||||
|
await watcher.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => {
|
export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { ValidDateType } from "./components/Date"
|
import { ValidDateType } from "./components/Date"
|
||||||
import { QuartzComponent } from "./components/types"
|
import { QuartzComponent } from "./components/types"
|
||||||
import { ValidLocale } from "./i18n"
|
|
||||||
import { PluginTypes } from "./plugins/types"
|
import { PluginTypes } from "./plugins/types"
|
||||||
import { Theme } from "./util/theme"
|
import { Theme } from "./util/theme"
|
||||||
|
|
||||||
@@ -8,7 +7,6 @@ export type Analytics =
|
|||||||
| null
|
| null
|
||||||
| {
|
| {
|
||||||
provider: "plausible"
|
provider: "plausible"
|
||||||
host?: string
|
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
provider: "google"
|
provider: "google"
|
||||||
@@ -17,7 +15,6 @@ export type Analytics =
|
|||||||
| {
|
| {
|
||||||
provider: "umami"
|
provider: "umami"
|
||||||
websiteId: string
|
websiteId: string
|
||||||
host?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GlobalConfiguration {
|
export interface GlobalConfiguration {
|
||||||
@@ -37,15 +34,6 @@ export interface GlobalConfiguration {
|
|||||||
*/
|
*/
|
||||||
baseUrl?: string
|
baseUrl?: string
|
||||||
theme: Theme
|
theme: Theme
|
||||||
/**
|
|
||||||
* Allow to translate the date in the language of your choice.
|
|
||||||
* Also used for UI translation (default: en-US)
|
|
||||||
* Need to be formated following BCP 47: https://en.wikipedia.org/wiki/IETF_language_tag
|
|
||||||
* The first part is the language (en) and the second part is the script/region (US)
|
|
||||||
* Language Codes: https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
|
|
||||||
* Region Codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
|
|
||||||
*/
|
|
||||||
locale: ValidLocale
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuartzConfig {
|
export interface QuartzConfig {
|
||||||
|
|||||||
@@ -71,11 +71,6 @@ export const BuildArgv = {
|
|||||||
default: false,
|
default: false,
|
||||||
describe: "run a local server to live-preview your Quartz",
|
describe: "run a local server to live-preview your Quartz",
|
||||||
},
|
},
|
||||||
fastRebuild: {
|
|
||||||
boolean: true,
|
|
||||||
default: false,
|
|
||||||
describe: "[experimental] rebuild only the changed files",
|
|
||||||
},
|
|
||||||
baseDir: {
|
baseDir: {
|
||||||
string: true,
|
string: true,
|
||||||
default: "",
|
default: "",
|
||||||
|
|||||||
@@ -113,10 +113,7 @@ export async function handleCreate(argv) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const gitkeepPath = path.join(contentFolder, ".gitkeep")
|
await fs.promises.unlink(path.join(contentFolder, ".gitkeep"))
|
||||||
if (fs.existsSync(gitkeepPath)) {
|
|
||||||
await fs.promises.unlink(gitkeepPath)
|
|
||||||
}
|
|
||||||
if (setupStrategy === "copy" || setupStrategy === "symlink") {
|
if (setupStrategy === "copy" || setupStrategy === "symlink") {
|
||||||
let originalFolder = sourceDirectory
|
let originalFolder = sourceDirectory
|
||||||
|
|
||||||
@@ -168,20 +165,22 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
|
|||||||
// get a preferred link resolution strategy
|
// get a preferred link resolution strategy
|
||||||
linkResolutionStrategy = exitIfCancel(
|
linkResolutionStrategy = exitIfCancel(
|
||||||
await select({
|
await select({
|
||||||
message: `Choose how Quartz should resolve links in your content. This should match Obsidian's link format. You can change this later in \`quartz.config.ts\`.`,
|
message: `Choose how Quartz should resolve links in your content. You can change this later in \`quartz.config.ts\`.`,
|
||||||
options: [
|
options: [
|
||||||
{
|
|
||||||
value: "shortest",
|
|
||||||
label: "Treat links as shortest path",
|
|
||||||
hint: "(default)",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
value: "absolute",
|
value: "absolute",
|
||||||
label: "Treat links as absolute path",
|
label: "Treat links as absolute path",
|
||||||
|
hint: "for content made for Quartz 3 and Hugo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "shortest",
|
||||||
|
label: "Treat links as shortest path",
|
||||||
|
hint: "for most Obsidian vaults",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "relative",
|
value: "relative",
|
||||||
label: "Treat links as relative paths",
|
label: "Treat links as relative paths",
|
||||||
|
hint: "for just normal Markdown files",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -200,7 +199,6 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
|
|||||||
// setup remote
|
// setup remote
|
||||||
execSync(
|
execSync(
|
||||||
`git remote show upstream || git remote add upstream https://github.com/jackyzha0/quartz.git`,
|
`git remote show upstream || git remote add upstream https://github.com/jackyzha0/quartz.git`,
|
||||||
{ stdio: "ignore" },
|
|
||||||
)
|
)
|
||||||
|
|
||||||
outro(`You're all set! Not sure what to do next? Try:
|
outro(`You're all set! Not sure what to do next? Try:
|
||||||
@@ -257,7 +255,6 @@ export async function handleBuild(argv) {
|
|||||||
},
|
},
|
||||||
write: false,
|
write: false,
|
||||||
bundle: true,
|
bundle: true,
|
||||||
minify: true,
|
|
||||||
platform: "browser",
|
platform: "browser",
|
||||||
format: "esm",
|
format: "esm",
|
||||||
})
|
})
|
||||||
@@ -347,7 +344,7 @@ export async function handleBuild(argv) {
|
|||||||
directoryListing: false,
|
directoryListing: false,
|
||||||
headers: [
|
headers: [
|
||||||
{
|
{
|
||||||
source: "**/*.*",
|
source: "**/*.html",
|
||||||
headers: [{ key: "Content-Disposition", value: "inline" }],
|
headers: [{ key: "Content-Disposition", value: "inline" }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -450,7 +447,7 @@ export async function handleUpdate(argv) {
|
|||||||
try {
|
try {
|
||||||
gitPull(UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH)
|
gitPull(UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH)
|
||||||
} catch {
|
} catch {
|
||||||
console.log(chalk.red("An error occurred above while pulling updates."))
|
console.log(chalk.red("An error occured above while pulling updates."))
|
||||||
await popContentFolder(contentFolder)
|
await popContentFolder(contentFolder)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -522,7 +519,7 @@ export async function handleSync(argv) {
|
|||||||
try {
|
try {
|
||||||
gitPull(ORIGIN_NAME, QUARTZ_SOURCE_BRANCH)
|
gitPull(ORIGIN_NAME, QUARTZ_SOURCE_BRANCH)
|
||||||
} catch {
|
} catch {
|
||||||
console.log(chalk.red("An error occurred above while pulling updates."))
|
console.log(chalk.red("An error occured above while pulling updates."))
|
||||||
await popContentFolder(contentFolder)
|
await popContentFolder(contentFolder)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
|
|
||||||
const ArticleTitle: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => {
|
function ArticleTitle({ fileData, displayClass }: QuartzComponentProps) {
|
||||||
const title = fileData.frontmatter?.title
|
const title = fileData.frontmatter?.title
|
||||||
if (title) {
|
if (title) {
|
||||||
return <h1 class={classNames(displayClass, "article-title")}>{title}</h1>
|
return <h1 class={`article-title ${displayClass ?? ""}`}>{title}</h1>
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ArticleTitle.css = `
|
ArticleTitle.css = `
|
||||||
.article-title {
|
.article-title {
|
||||||
margin: 2rem 0 0 0;
|
margin: 2rem 0 0 0;
|
||||||
|
|||||||
@@ -1,20 +1,13 @@
|
|||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import style from "./styles/backlinks.scss"
|
import style from "./styles/backlinks.scss"
|
||||||
import { resolveRelative, simplifySlug } from "../util/path"
|
import { resolveRelative, simplifySlug } from "../util/path"
|
||||||
import { i18n } from "../i18n"
|
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
|
|
||||||
const Backlinks: QuartzComponent = ({
|
function Backlinks({ fileData, allFiles, displayClass }: QuartzComponentProps) {
|
||||||
fileData,
|
|
||||||
allFiles,
|
|
||||||
displayClass,
|
|
||||||
cfg,
|
|
||||||
}: QuartzComponentProps) => {
|
|
||||||
const slug = simplifySlug(fileData.slug!)
|
const slug = simplifySlug(fileData.slug!)
|
||||||
const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
|
const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "backlinks")}>
|
<div class={`backlinks ${displayClass ?? ""}`}>
|
||||||
<h3>{i18n(cfg.locale).components.backlinks.title}</h3>
|
<h3>Backlinks</h3>
|
||||||
<ul class="overflow">
|
<ul class="overflow">
|
||||||
{backlinkFiles.length > 0 ? (
|
{backlinkFiles.length > 0 ? (
|
||||||
backlinkFiles.map((f) => (
|
backlinkFiles.map((f) => (
|
||||||
@@ -25,7 +18,7 @@ const Backlinks: QuartzComponent = ({
|
|||||||
</li>
|
</li>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<li>{i18n(cfg.locale).components.backlinks.noBacklinksFound}</li>
|
<li>No backlinks found</li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// @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 { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
|
|
||||||
const Body: QuartzComponent = ({ children }: QuartzComponentProps) => {
|
function Body({ children }: QuartzComponentProps) {
|
||||||
return <div id="quartz-body">{children}</div>
|
return <div id="quartz-body">{children}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import breadcrumbsStyle from "./styles/breadcrumbs.scss"
|
import breadcrumbsStyle from "./styles/breadcrumbs.scss"
|
||||||
import { FullSlug, SimpleSlug, joinSegments, resolveRelative } from "../util/path"
|
import { FullSlug, SimpleSlug, resolveRelative } from "../util/path"
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
|
|
||||||
type CrumbData = {
|
type CrumbData = {
|
||||||
displayName: string
|
displayName: string
|
||||||
@@ -19,15 +18,15 @@ interface BreadcrumbOptions {
|
|||||||
*/
|
*/
|
||||||
rootName: string
|
rootName: string
|
||||||
/**
|
/**
|
||||||
* Whether to look up frontmatter title for folders (could cause performance problems with big vaults)
|
* wether to look up frontmatter title for folders (could cause performance problems with big vaults)
|
||||||
*/
|
*/
|
||||||
resolveFrontmatterTitle: boolean
|
resolveFrontmatterTitle: boolean
|
||||||
/**
|
/**
|
||||||
* Whether to display breadcrumbs on root `index.md`
|
* Wether to display breadcrumbs on root `index.md`
|
||||||
*/
|
*/
|
||||||
hideOnRoot: boolean
|
hideOnRoot: boolean
|
||||||
/**
|
/**
|
||||||
* Whether to display the current page in the breadcrumbs.
|
* Wether to display the current page in the breadcrumbs.
|
||||||
*/
|
*/
|
||||||
showCurrentPage: boolean
|
showCurrentPage: boolean
|
||||||
}
|
}
|
||||||
@@ -54,11 +53,7 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
|||||||
// computed index of folder name to its associated file data
|
// computed index of folder name to its associated file data
|
||||||
let folderIndex: Map<string, QuartzPluginData> | undefined
|
let folderIndex: Map<string, QuartzPluginData> | undefined
|
||||||
|
|
||||||
const Breadcrumbs: QuartzComponent = ({
|
function Breadcrumbs({ fileData, allFiles, displayClass }: QuartzComponentProps) {
|
||||||
fileData,
|
|
||||||
allFiles,
|
|
||||||
displayClass,
|
|
||||||
}: QuartzComponentProps) => {
|
|
||||||
// Hide crumbs on root if enabled
|
// Hide crumbs on root if enabled
|
||||||
if (options.hideOnRoot && fileData.slug === "index") {
|
if (options.hideOnRoot && fileData.slug === "index") {
|
||||||
return <></>
|
return <></>
|
||||||
@@ -72,9 +67,12 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
|||||||
folderIndex = new Map()
|
folderIndex = new Map()
|
||||||
// construct the index for the first time
|
// construct the index for the first time
|
||||||
for (const file of allFiles) {
|
for (const file of allFiles) {
|
||||||
const folderParts = file.slug?.split("/")
|
if (file.slug?.endsWith("index")) {
|
||||||
if (folderParts?.at(-1) === "index") {
|
const folderParts = file.filePath?.split("/")
|
||||||
folderIndex.set(folderParts.slice(0, -1).join("/"), file)
|
if (folderParts) {
|
||||||
|
const folderName = folderParts[folderParts?.length - 2]
|
||||||
|
folderIndex.set(folderName, file)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,48 +80,35 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
|||||||
// Split slug into hierarchy/parts
|
// Split slug into hierarchy/parts
|
||||||
const slugParts = fileData.slug?.split("/")
|
const slugParts = fileData.slug?.split("/")
|
||||||
if (slugParts) {
|
if (slugParts) {
|
||||||
// is tag breadcrumb?
|
|
||||||
const isTagPath = slugParts[0] === "tags"
|
|
||||||
|
|
||||||
// full path until current part
|
// full path until current part
|
||||||
let currentPath = ""
|
let currentPath = ""
|
||||||
|
|
||||||
for (let i = 0; i < slugParts.length - 1; i++) {
|
for (let i = 0; i < slugParts.length - 1; i++) {
|
||||||
let curPathSegment = slugParts[i]
|
let curPathSegment = slugParts[i]
|
||||||
|
|
||||||
// Try to resolve frontmatter folder title
|
// Try to resolve frontmatter folder title
|
||||||
const currentFile = folderIndex?.get(slugParts.slice(0, i + 1).join("/"))
|
const currentFile = folderIndex?.get(curPathSegment)
|
||||||
if (currentFile) {
|
if (currentFile) {
|
||||||
const title = currentFile.frontmatter!.title
|
curPathSegment = currentFile.frontmatter!.title
|
||||||
if (title !== "index") {
|
|
||||||
curPathSegment = title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add current slug to full path
|
// Add current slug to full path
|
||||||
currentPath = joinSegments(currentPath, slugParts[i])
|
currentPath += slugParts[i] + "/"
|
||||||
const includeTrailingSlash = !isTagPath || i < 1
|
|
||||||
|
|
||||||
// Format and add current crumb
|
// Format and add current crumb
|
||||||
const crumb = formatCrumb(
|
const crumb = formatCrumb(curPathSegment, fileData.slug!, currentPath as SimpleSlug)
|
||||||
curPathSegment,
|
|
||||||
fileData.slug!,
|
|
||||||
(currentPath + (includeTrailingSlash ? "/" : "")) as SimpleSlug,
|
|
||||||
)
|
|
||||||
crumbs.push(crumb)
|
crumbs.push(crumb)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add current file to crumb (can directly use frontmatter title)
|
// Add current file to crumb (can directly use frontmatter title)
|
||||||
if (options.showCurrentPage && slugParts.at(-1) !== "index") {
|
if (options.showCurrentPage) {
|
||||||
crumbs.push({
|
crumbs.push({
|
||||||
displayName: fileData.frontmatter!.title,
|
displayName: fileData.frontmatter!.title,
|
||||||
path: "",
|
path: "",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav class={classNames(displayClass, "breadcrumb-container")} aria-label="breadcrumbs">
|
<nav class={`breadcrumb-container ${displayClass ?? ""}`} aria-label="breadcrumbs">
|
||||||
{crumbs.map((crumb, index) => (
|
{crumbs.map((crumb, index) => (
|
||||||
<div class="breadcrumb-element">
|
<div class="breadcrumb-element">
|
||||||
<a href={crumb.path}>{crumb.displayName}</a>
|
<a href={crumb.path}>{crumb.displayName}</a>
|
||||||
@@ -134,6 +119,5 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Breadcrumbs.css = breadcrumbsStyle
|
Breadcrumbs.css = breadcrumbsStyle
|
||||||
|
|
||||||
return Breadcrumbs
|
return Breadcrumbs
|
||||||
}) satisfies QuartzComponentConstructor
|
}) satisfies QuartzComponentConstructor
|
||||||
|
|||||||
@@ -1,44 +1,20 @@
|
|||||||
import { formatDate, getDate } from "./Date"
|
import { formatDate, getDate } from "./Date"
|
||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import readingTime from "reading-time"
|
import readingTime from "reading-time"
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
import { i18n } from "../i18n"
|
|
||||||
|
|
||||||
interface ContentMetaOptions {
|
|
||||||
/**
|
|
||||||
* Whether to display reading time
|
|
||||||
*/
|
|
||||||
showReadingTime: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultOptions: ContentMetaOptions = {
|
|
||||||
showReadingTime: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ((opts?: Partial<ContentMetaOptions>) => {
|
|
||||||
// Merge options with defaults
|
|
||||||
const options: ContentMetaOptions = { ...defaultOptions, ...opts }
|
|
||||||
|
|
||||||
|
export default (() => {
|
||||||
function ContentMetadata({ cfg, fileData, displayClass }: QuartzComponentProps) {
|
function ContentMetadata({ cfg, fileData, displayClass }: QuartzComponentProps) {
|
||||||
const text = fileData.text
|
const text = fileData.text
|
||||||
|
|
||||||
if (text) {
|
if (text) {
|
||||||
const segments: string[] = []
|
const segments: string[] = []
|
||||||
|
const { text: timeTaken, words: _words } = readingTime(text)
|
||||||
|
|
||||||
if (fileData.dates) {
|
if (fileData.dates) {
|
||||||
segments.push(formatDate(getDate(cfg, fileData)!, cfg.locale))
|
segments.push(formatDate(getDate(cfg, fileData)!))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display reading time if enabled
|
segments.push(timeTaken)
|
||||||
if (options.showReadingTime) {
|
return <p class={`content-meta ${displayClass ?? ""}`}>{segments.join(", ")}</p>
|
||||||
const { minutes, words: _words } = readingTime(text)
|
|
||||||
const displayedTime = i18n(cfg.locale).components.contentMeta.readingTime({
|
|
||||||
minutes: Math.ceil(minutes),
|
|
||||||
})
|
|
||||||
segments.push(displayedTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <p class={classNames(displayClass, "content-meta")}>{segments.join(", ")}</p>
|
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,11 @@
|
|||||||
// 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 { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import { i18n } from "../i18n"
|
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
|
|
||||||
const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
function Darkmode({ displayClass }: QuartzComponentProps) {
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "darkmode")}>
|
<div class={`darkmode ${displayClass ?? ""}`}>
|
||||||
<input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} />
|
<input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} />
|
||||||
<label id="toggle-label-light" for="darkmode-toggle" tabIndex={-1}>
|
<label id="toggle-label-light" for="darkmode-toggle" tabIndex={-1}>
|
||||||
<svg
|
<svg
|
||||||
@@ -20,10 +18,10 @@ const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps)
|
|||||||
x="0px"
|
x="0px"
|
||||||
y="0px"
|
y="0px"
|
||||||
viewBox="0 0 35 35"
|
viewBox="0 0 35 35"
|
||||||
style="enable-background:new 0 0 35 35"
|
style="enable-background:new 0 0 35 35;"
|
||||||
xmlSpace="preserve"
|
xmlSpace="preserve"
|
||||||
>
|
>
|
||||||
<title>{i18n(cfg.locale).components.themeToggle.darkMode}</title>
|
<title>Light mode</title>
|
||||||
<path d="M6,17.5C6,16.672,5.328,16,4.5,16h-3C0.672,16,0,16.672,0,17.5 S0.672,19,1.5,19h3C5.328,19,6,18.328,6,17.5z M7.5,26c-0.414,0-0.789,0.168-1.061,0.439l-2,2C4.168,28.711,4,29.086,4,29.5 C4,30.328,4.671,31,5.5,31c0.414,0,0.789-0.168,1.06-0.44l2-2C8.832,28.289,9,27.914,9,27.5C9,26.672,8.329,26,7.5,26z M17.5,6 C18.329,6,19,5.328,19,4.5v-3C19,0.672,18.329,0,17.5,0S16,0.672,16,1.5v3C16,5.328,16.671,6,17.5,6z M27.5,9 c0.414,0,0.789-0.168,1.06-0.439l2-2C30.832,6.289,31,5.914,31,5.5C31,4.672,30.329,4,29.5,4c-0.414,0-0.789,0.168-1.061,0.44 l-2,2C26.168,6.711,26,7.086,26,7.5C26,8.328,26.671,9,27.5,9z M6.439,8.561C6.711,8.832,7.086,9,7.5,9C8.328,9,9,8.328,9,7.5 c0-0.414-0.168-0.789-0.439-1.061l-2-2C6.289,4.168,5.914,4,5.5,4C4.672,4,4,4.672,4,5.5c0,0.414,0.168,0.789,0.439,1.06 L6.439,8.561z M33.5,16h-3c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5h3c0.828,0,1.5-0.672,1.5-1.5S34.328,16,33.5,16z M28.561,26.439C28.289,26.168,27.914,26,27.5,26c-0.828,0-1.5,0.672-1.5,1.5c0,0.414,0.168,0.789,0.439,1.06l2,2 C28.711,30.832,29.086,31,29.5,31c0.828,0,1.5-0.672,1.5-1.5c0-0.414-0.168-0.789-0.439-1.061L28.561,26.439z M17.5,29 c-0.829,0-1.5,0.672-1.5,1.5v3c0,0.828,0.671,1.5,1.5,1.5s1.5-0.672,1.5-1.5v-3C19,29.672,18.329,29,17.5,29z M17.5,7 C11.71,7,7,11.71,7,17.5S11.71,28,17.5,28S28,23.29,28,17.5S23.29,7,17.5,7z M17.5,25c-4.136,0-7.5-3.364-7.5-7.5 c0-4.136,3.364-7.5,7.5-7.5c4.136,0,7.5,3.364,7.5,7.5C25,21.636,21.636,25,17.5,25z"></path>
|
<path d="M6,17.5C6,16.672,5.328,16,4.5,16h-3C0.672,16,0,16.672,0,17.5 S0.672,19,1.5,19h3C5.328,19,6,18.328,6,17.5z M7.5,26c-0.414,0-0.789,0.168-1.061,0.439l-2,2C4.168,28.711,4,29.086,4,29.5 C4,30.328,4.671,31,5.5,31c0.414,0,0.789-0.168,1.06-0.44l2-2C8.832,28.289,9,27.914,9,27.5C9,26.672,8.329,26,7.5,26z M17.5,6 C18.329,6,19,5.328,19,4.5v-3C19,0.672,18.329,0,17.5,0S16,0.672,16,1.5v3C16,5.328,16.671,6,17.5,6z M27.5,9 c0.414,0,0.789-0.168,1.06-0.439l2-2C30.832,6.289,31,5.914,31,5.5C31,4.672,30.329,4,29.5,4c-0.414,0-0.789,0.168-1.061,0.44 l-2,2C26.168,6.711,26,7.086,26,7.5C26,8.328,26.671,9,27.5,9z M6.439,8.561C6.711,8.832,7.086,9,7.5,9C8.328,9,9,8.328,9,7.5 c0-0.414-0.168-0.789-0.439-1.061l-2-2C6.289,4.168,5.914,4,5.5,4C4.672,4,4,4.672,4,5.5c0,0.414,0.168,0.789,0.439,1.06 L6.439,8.561z M33.5,16h-3c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5h3c0.828,0,1.5-0.672,1.5-1.5S34.328,16,33.5,16z M28.561,26.439C28.289,26.168,27.914,26,27.5,26c-0.828,0-1.5,0.672-1.5,1.5c0,0.414,0.168,0.789,0.439,1.06l2,2 C28.711,30.832,29.086,31,29.5,31c0.828,0,1.5-0.672,1.5-1.5c0-0.414-0.168-0.789-0.439-1.061L28.561,26.439z M17.5,29 c-0.829,0-1.5,0.672-1.5,1.5v3c0,0.828,0.671,1.5,1.5,1.5s1.5-0.672,1.5-1.5v-3C19,29.672,18.329,29,17.5,29z M17.5,7 C11.71,7,7,11.71,7,17.5S11.71,28,17.5,28S28,23.29,28,17.5S23.29,7,17.5,7z M17.5,25c-4.136,0-7.5-3.364-7.5-7.5 c0-4.136,3.364-7.5,7.5-7.5c4.136,0,7.5,3.364,7.5,7.5C25,21.636,21.636,25,17.5,25z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
@@ -36,10 +34,10 @@ const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps)
|
|||||||
x="0px"
|
x="0px"
|
||||||
y="0px"
|
y="0px"
|
||||||
viewBox="0 0 100 100"
|
viewBox="0 0 100 100"
|
||||||
style="enable-background:new 0 0 100 100"
|
style="enable-background='new 0 0 100 100'"
|
||||||
xmlSpace="preserve"
|
xmlSpace="preserve"
|
||||||
>
|
>
|
||||||
<title>{i18n(cfg.locale).components.themeToggle.lightMode}</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 d="M96.76,66.458c-0.853-0.852-2.15-1.064-3.23-0.534c-6.063,2.991-12.858,4.571-19.655,4.571 C62.022,70.495,50.88,65.88,42.5,57.5C29.043,44.043,25.658,23.536,34.076,6.47c0.532-1.08,0.318-2.379-0.534-3.23 c-0.851-0.852-2.15-1.064-3.23-0.534c-4.918,2.427-9.375,5.619-13.246,9.491c-9.447,9.447-14.65,22.008-14.65,35.369 c0,13.36,5.203,25.921,14.65,35.368s22.008,14.65,35.368,14.65c13.361,0,25.921-5.203,35.369-14.65 c3.872-3.871,7.064-8.328,9.491-13.246C97.826,68.608,97.611,67.309,96.76,66.458z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { GlobalConfiguration } from "../cfg"
|
import { GlobalConfiguration } from "../cfg"
|
||||||
import { ValidLocale } from "../i18n"
|
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
date: Date
|
date: Date
|
||||||
locale?: ValidLocale
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ValidDateType = keyof Required<QuartzPluginData>["dates"]
|
export type ValidDateType = keyof Required<QuartzPluginData>["dates"]
|
||||||
@@ -18,14 +16,14 @@ export function getDate(cfg: GlobalConfiguration, data: QuartzPluginData): Date
|
|||||||
return data.dates?.[cfg.defaultDateType]
|
return data.dates?.[cfg.defaultDateType]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDate(d: Date, locale: ValidLocale = "en-US"): string {
|
export function formatDate(d: Date): string {
|
||||||
return d.toLocaleDateString(locale, {
|
return d.toLocaleDateString("en-US", {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Date({ date, locale }: Props) {
|
export function Date({ date }: Props) {
|
||||||
return <>{formatDate(date, locale)}</>
|
return <>{formatDate(date)}</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } fro
|
|||||||
export default ((component?: QuartzComponent) => {
|
export default ((component?: QuartzComponent) => {
|
||||||
if (component) {
|
if (component) {
|
||||||
const Component = component
|
const Component = component
|
||||||
const DesktopOnly: QuartzComponent = (props: QuartzComponentProps) => {
|
function DesktopOnly(props: QuartzComponentProps) {
|
||||||
return <Component displayClass="desktop-only" {...props} />
|
return <Component displayClass="desktop-only" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import explorerStyle from "./styles/explorer.scss"
|
import explorerStyle from "./styles/explorer.scss"
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import script from "./scripts/explorer.inline"
|
import script from "./scripts/explorer.inline"
|
||||||
import { ExplorerNode, FileNode, Options } from "./ExplorerNode"
|
import { ExplorerNode, FileNode, Options } from "./ExplorerNode"
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
import { i18n } from "../i18n"
|
|
||||||
|
|
||||||
// Options interface defined in `ExplorerNode` to avoid circular dependency
|
// Options interface defined in `ExplorerNode` to avoid circular dependency
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
|
title: "Explorer",
|
||||||
folderClickBehavior: "collapse",
|
folderClickBehavior: "collapse",
|
||||||
folderDefaultState: "collapsed",
|
folderDefaultState: "collapsed",
|
||||||
useSavedState: true,
|
useSavedState: true,
|
||||||
mapFn: (node) => {
|
|
||||||
return node
|
|
||||||
},
|
|
||||||
sortFn: (a, b) => {
|
sortFn: (a, b) => {
|
||||||
// Sort order: folders first, then files. Sort folders and files alphabetically
|
// Sort order: folders first, then files. Sort folders and files alphabetically
|
||||||
if ((!a.file && !b.file) || (a.file && b.file)) {
|
if ((!a.file && !b.file) || (a.file && b.file)) {
|
||||||
@@ -26,7 +22,6 @@ const defaultOptions = {
|
|||||||
sensitivity: "base",
|
sensitivity: "base",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (a.file && !b.file) {
|
if (a.file && !b.file) {
|
||||||
return 1
|
return 1
|
||||||
} else {
|
} else {
|
||||||
@@ -46,44 +41,52 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
let jsonTree: string
|
let jsonTree: string
|
||||||
|
|
||||||
function constructFileTree(allFiles: QuartzPluginData[]) {
|
function constructFileTree(allFiles: QuartzPluginData[]) {
|
||||||
if (fileTree) {
|
if (!fileTree) {
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct tree from allFiles
|
// Construct tree from allFiles
|
||||||
fileTree = new FileNode("")
|
fileTree = new FileNode("")
|
||||||
allFiles.forEach((file) => fileTree.add(file))
|
allFiles.forEach((file) => fileTree.add(file, 1))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keys of this object must match corresponding function name of `FileNode`,
|
||||||
|
* while values must be the argument that will be passed to the function.
|
||||||
|
*
|
||||||
|
* e.g. entry for FileNode.sort: `sort: opts.sortFn` (value is sort function from options)
|
||||||
|
*/
|
||||||
|
const functions = {
|
||||||
|
map: opts.mapFn,
|
||||||
|
sort: opts.sortFn,
|
||||||
|
filter: opts.filterFn,
|
||||||
|
}
|
||||||
|
|
||||||
// Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied)
|
// Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied)
|
||||||
if (opts.order) {
|
if (opts.order) {
|
||||||
// Order is important, use loop with index instead of order.map()
|
// Order is important, use loop with index instead of order.map()
|
||||||
for (let i = 0; i < opts.order.length; i++) {
|
for (let i = 0; i < opts.order.length; i++) {
|
||||||
const functionName = opts.order[i]
|
const functionName = opts.order[i]
|
||||||
if (functionName === "map") {
|
if (functions[functionName]) {
|
||||||
fileTree.map(opts.mapFn)
|
// for every entry in order, call matching function in FileNode and pass matching argument
|
||||||
} else if (functionName === "sort") {
|
// e.g. i = 0; functionName = "filter"
|
||||||
fileTree.sort(opts.sortFn)
|
// converted to: (if opts.filterFn) => fileTree.filter(opts.filterFn)
|
||||||
} else if (functionName === "filter") {
|
|
||||||
fileTree.filter(opts.filterFn)
|
// @ts-ignore
|
||||||
|
// typescript cant statically check these dynamic references, so manually make sure reference is valid and ignore warning
|
||||||
|
fileTree[functionName].call(fileTree, functions[functionName])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all folders of tree. Initialize with collapsed state
|
// Get all folders of tree. Initialize with collapsed state
|
||||||
// Stringify to pass json tree as data attribute ([data-tree])
|
|
||||||
const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed")
|
const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed")
|
||||||
|
|
||||||
|
// Stringify to pass json tree as data attribute ([data-tree])
|
||||||
jsonTree = JSON.stringify(folders)
|
jsonTree = JSON.stringify(folders)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const Explorer: QuartzComponent = ({
|
function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) {
|
||||||
cfg,
|
|
||||||
allFiles,
|
|
||||||
displayClass,
|
|
||||||
fileData,
|
|
||||||
}: QuartzComponentProps) => {
|
|
||||||
constructFileTree(allFiles)
|
constructFileTree(allFiles)
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "explorer")}>
|
<div class={`explorer ${displayClass ?? ""}`}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
id="explorer"
|
id="explorer"
|
||||||
@@ -92,7 +95,7 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
data-savestate={opts.useSavedState}
|
data-savestate={opts.useSavedState}
|
||||||
data-tree={jsonTree}
|
data-tree={jsonTree}
|
||||||
>
|
>
|
||||||
<h1>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h1>
|
<h1>{opts.title}</h1>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="14"
|
width="14"
|
||||||
@@ -117,7 +120,6 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Explorer.css = explorerStyle
|
Explorer.css = explorerStyle
|
||||||
Explorer.afterDOMLoaded = script
|
Explorer.afterDOMLoaded = script
|
||||||
return Explorer
|
return Explorer
|
||||||
|
|||||||
@@ -1,25 +1,18 @@
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
import {
|
import { resolveRelative } from "../util/path"
|
||||||
joinSegments,
|
|
||||||
resolveRelative,
|
|
||||||
clone,
|
|
||||||
simplifySlug,
|
|
||||||
SimpleSlug,
|
|
||||||
FilePath,
|
|
||||||
} from "../util/path"
|
|
||||||
|
|
||||||
type OrderEntries = "sort" | "filter" | "map"
|
type OrderEntries = "sort" | "filter" | "map"
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
title?: string
|
title: string
|
||||||
folderDefaultState: "collapsed" | "open"
|
folderDefaultState: "collapsed" | "open"
|
||||||
folderClickBehavior: "collapse" | "link"
|
folderClickBehavior: "collapse" | "link"
|
||||||
useSavedState: boolean
|
useSavedState: boolean
|
||||||
sortFn: (a: FileNode, b: FileNode) => number
|
sortFn: (a: FileNode, b: FileNode) => number
|
||||||
filterFn: (node: FileNode) => boolean
|
filterFn?: (node: FileNode) => boolean
|
||||||
mapFn: (node: FileNode) => void
|
mapFn?: (node: FileNode) => void
|
||||||
order: OrderEntries[]
|
order?: OrderEntries[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type DataWrapper = {
|
type DataWrapper = {
|
||||||
@@ -32,74 +25,59 @@ export type FolderState = {
|
|||||||
collapsed: boolean
|
collapsed: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPathSegment(fp: FilePath | undefined, idx: number): string | undefined {
|
|
||||||
if (!fp) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
return fp.split("/").at(idx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Structure to add all files into a tree
|
// Structure to add all files into a tree
|
||||||
export class FileNode {
|
export class FileNode {
|
||||||
children: Array<FileNode>
|
children: FileNode[]
|
||||||
name: string // this is the slug segment
|
name: string
|
||||||
displayName: string
|
displayName: string
|
||||||
file: QuartzPluginData | null
|
file: QuartzPluginData | null
|
||||||
depth: number
|
depth: number
|
||||||
|
|
||||||
constructor(slugSegment: string, displayName?: string, file?: QuartzPluginData, depth?: number) {
|
constructor(name: string, file?: QuartzPluginData, depth?: number) {
|
||||||
this.children = []
|
this.children = []
|
||||||
this.name = slugSegment
|
this.name = name
|
||||||
this.displayName = displayName ?? file?.frontmatter?.title ?? slugSegment
|
this.displayName = name
|
||||||
this.file = file ? clone(file) : null
|
this.file = file ? structuredClone(file) : null
|
||||||
this.depth = depth ?? 0
|
this.depth = depth ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private insert(fileData: DataWrapper) {
|
private insert(file: DataWrapper) {
|
||||||
if (fileData.path.length === 0) {
|
if (file.path.length === 1) {
|
||||||
return
|
if (file.path[0] !== "index.md") {
|
||||||
}
|
this.children.push(new FileNode(file.file.frontmatter!.title, file.file, this.depth + 1))
|
||||||
|
} else {
|
||||||
const nextSegment = fileData.path[0]
|
const title = file.file.frontmatter?.title
|
||||||
|
if (title && title !== "index" && file.path[0] === "index.md") {
|
||||||
// base case, insert here
|
|
||||||
if (fileData.path.length === 1) {
|
|
||||||
if (nextSegment === "") {
|
|
||||||
// index case (we are the root and we just found index.md), set our data appropriately
|
|
||||||
const title = fileData.file.frontmatter?.title
|
|
||||||
if (title && title !== "index") {
|
|
||||||
this.displayName = title
|
this.displayName = title
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// direct child
|
const next = file.path[0]
|
||||||
this.children.push(new FileNode(nextSegment, undefined, fileData.file, this.depth + 1))
|
file.path = file.path.splice(1)
|
||||||
}
|
for (const child of this.children) {
|
||||||
|
if (child.name === next) {
|
||||||
|
child.insert(file)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the right child to insert into
|
|
||||||
fileData.path = fileData.path.splice(1)
|
|
||||||
const child = this.children.find((c) => c.name === nextSegment)
|
|
||||||
if (child) {
|
|
||||||
child.insert(fileData)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const newChild = new FileNode(
|
const newChild = new FileNode(next, undefined, this.depth + 1)
|
||||||
nextSegment,
|
newChild.insert(file)
|
||||||
getPathSegment(fileData.file.relativePath, this.depth),
|
|
||||||
undefined,
|
|
||||||
this.depth + 1,
|
|
||||||
)
|
|
||||||
newChild.insert(fileData)
|
|
||||||
this.children.push(newChild)
|
this.children.push(newChild)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add new file to tree
|
// Add new file to tree
|
||||||
add(file: QuartzPluginData) {
|
add(file: QuartzPluginData, splice: number = 0) {
|
||||||
this.insert({ file: file, path: simplifySlug(file.slug!).split("/") })
|
this.insert({ file, path: file.filePath!.split("/").splice(splice) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print tree structure (for debugging)
|
||||||
|
print(depth: number = 0) {
|
||||||
|
let folderChar = ""
|
||||||
|
if (!this.file) folderChar = "|"
|
||||||
|
console.log("-".repeat(depth), folderChar, this.name, this.depth)
|
||||||
|
this.children.forEach((e) => e.print(depth + 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,6 +95,7 @@ export class FileNode {
|
|||||||
*/
|
*/
|
||||||
map(mapFn: (node: FileNode) => void) {
|
map(mapFn: (node: FileNode) => void) {
|
||||||
mapFn(this)
|
mapFn(this)
|
||||||
|
|
||||||
this.children.forEach((child) => child.map(mapFn))
|
this.children.forEach((child) => child.map(mapFn))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,16 +110,16 @@ export class FileNode {
|
|||||||
|
|
||||||
const traverse = (node: FileNode, currentPath: string) => {
|
const traverse = (node: FileNode, currentPath: string) => {
|
||||||
if (!node.file) {
|
if (!node.file) {
|
||||||
const folderPath = joinSegments(currentPath, node.name)
|
const folderPath = currentPath + (currentPath ? "/" : "") + node.name
|
||||||
if (folderPath !== "") {
|
if (folderPath !== "") {
|
||||||
folderPaths.push({ path: folderPath, collapsed })
|
folderPaths.push({ path: folderPath, collapsed })
|
||||||
}
|
}
|
||||||
|
|
||||||
node.children.forEach((child) => traverse(child, folderPath))
|
node.children.forEach((child) => traverse(child, folderPath))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
traverse(this, "")
|
traverse(this, "")
|
||||||
|
|
||||||
return folderPaths
|
return folderPaths
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,13 +147,14 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
|
|||||||
const isDefaultOpen = opts.folderDefaultState === "open"
|
const isDefaultOpen = opts.folderDefaultState === "open"
|
||||||
|
|
||||||
// Calculate current folderPath
|
// Calculate current folderPath
|
||||||
|
let pathOld = fullPath ? fullPath : ""
|
||||||
let folderPath = ""
|
let folderPath = ""
|
||||||
if (node.name !== "") {
|
if (node.name !== "") {
|
||||||
folderPath = joinSegments(fullPath ?? "", node.name)
|
folderPath = `${pathOld}/${node.name}`
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<li>
|
||||||
{node.file ? (
|
{node.file ? (
|
||||||
// Single file node
|
// Single file node
|
||||||
<li key={node.file.slug}>
|
<li key={node.file.slug}>
|
||||||
@@ -183,7 +163,7 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
) : (
|
) : (
|
||||||
<li>
|
<div>
|
||||||
{node.name !== "" && (
|
{node.name !== "" && (
|
||||||
// Node with entire folder
|
// Node with entire folder
|
||||||
// Render svg button + folder name, then children
|
// Render svg button + folder name, then children
|
||||||
@@ -205,16 +185,12 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
|
|||||||
{/* render <a> tag if folderBehavior is "link", otherwise render <button> with collapse click event */}
|
{/* render <a> tag if folderBehavior is "link", otherwise render <button> with collapse click event */}
|
||||||
<div key={node.name} data-folderpath={folderPath}>
|
<div key={node.name} data-folderpath={folderPath}>
|
||||||
{folderBehavior === "link" ? (
|
{folderBehavior === "link" ? (
|
||||||
<a
|
<a href={`${folderPath}`} data-for={node.name} class="folder-title">
|
||||||
href={resolveRelative(fileData.slug!, folderPath as SimpleSlug)}
|
|
||||||
data-for={node.name}
|
|
||||||
class="folder-title"
|
|
||||||
>
|
|
||||||
{node.displayName}
|
{node.displayName}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<button class="folder-button">
|
<button class="folder-button">
|
||||||
<span class="folder-title">{node.displayName}</span>
|
<p class="folder-title">{node.displayName}</p>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -241,8 +217,8 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</li>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import style from "./styles/footer.scss"
|
import style from "./styles/footer.scss"
|
||||||
import { version } from "../../package.json"
|
import { version } from "../../package.json"
|
||||||
import { i18n } from "../i18n"
|
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
links: Record<string, string>
|
links: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ((opts?: Options) => {
|
export default ((opts?: Options) => {
|
||||||
const Footer: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
function Footer({ displayClass }: QuartzComponentProps) {
|
||||||
const year = new Date().getFullYear()
|
const year = new Date().getFullYear()
|
||||||
const links = opts?.links ?? []
|
const links = opts?.links ?? []
|
||||||
return (
|
return (
|
||||||
<footer class={`${displayClass ?? ""}`}>
|
<footer class={`${displayClass ?? ""}`}>
|
||||||
<hr />
|
<hr />
|
||||||
<p>
|
<p>
|
||||||
{i18n(cfg.locale).components.footer.createdWith}{" "}
|
Created with <a href="https://quartz.jzhao.xyz/">Quartz v{version}</a>, © {year}
|
||||||
<a href="https://quartz.jzhao.xyz/">Quartz v{version}</a> © {year}
|
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
{Object.entries(links).map(([text, link]) => (
|
{Object.entries(links).map(([text, link]) => (
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import script from "./scripts/graph.inline"
|
import script from "./scripts/graph.inline"
|
||||||
import style from "./styles/graph.scss"
|
import style from "./styles/graph.scss"
|
||||||
import { i18n } from "../i18n"
|
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
|
|
||||||
export interface D3Config {
|
export interface D3Config {
|
||||||
drag: boolean
|
drag: boolean
|
||||||
@@ -54,12 +52,12 @@ const defaultOptions: GraphOptions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default ((opts?: GraphOptions) => {
|
export default ((opts?: GraphOptions) => {
|
||||||
const Graph: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
function Graph({ displayClass }: QuartzComponentProps) {
|
||||||
const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph }
|
const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph }
|
||||||
const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph }
|
const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph }
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "graph")}>
|
<div class={`graph ${displayClass ?? ""}`}>
|
||||||
<h3>{i18n(cfg.locale).components.graph.title}</h3>
|
<h3>Graph View</h3>
|
||||||
<div class="graph-outer">
|
<div class="graph-outer">
|
||||||
<div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
|
<div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { i18n } from "../i18n"
|
import { FullSlug, _stripSlashes, joinSegments, pathToRoot } from "../util/path"
|
||||||
import { FullSlug, joinSegments, pathToRoot } from "../util/path"
|
|
||||||
import { JSResourceToScriptElement } from "../util/resources"
|
import { JSResourceToScriptElement } from "../util/resources"
|
||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
|
|
||||||
export default (() => {
|
export default (() => {
|
||||||
const Head: QuartzComponent = ({ cfg, fileData, externalResources }: QuartzComponentProps) => {
|
function Head({ cfg, fileData, externalResources }: QuartzComponentProps) {
|
||||||
const title = fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title
|
const title = fileData.frontmatter?.title ?? "Untitled"
|
||||||
const description =
|
const description = fileData.description?.trim() ?? "No description provided"
|
||||||
fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description
|
|
||||||
const { css, js } = externalResources
|
const { css, js } = externalResources
|
||||||
|
|
||||||
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
|
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
|
||||||
@@ -21,12 +19,6 @@ export default (() => {
|
|||||||
<head>
|
<head>
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<meta charSet="utf-8" />
|
<meta charSet="utf-8" />
|
||||||
{cfg.theme.cdnCaching && (
|
|
||||||
<>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta property="og:title" content={title} />
|
<meta property="og:title" content={title} />
|
||||||
<meta property="og:description" content={description} />
|
<meta property="og:description" content={description} />
|
||||||
@@ -36,6 +28,8 @@ export default (() => {
|
|||||||
<link rel="icon" href={iconPath} />
|
<link rel="icon" href={iconPath} />
|
||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
<meta name="generator" content="Quartz" />
|
<meta name="generator" content="Quartz" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||||
{css.map((href) => (
|
{css.map((href) => (
|
||||||
<link key={href} href={href} rel="stylesheet" type="text/css" spa-preserve />
|
<link key={href} href={href} rel="stylesheet" type="text/css" spa-preserve />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
|
|
||||||
const Header: QuartzComponent = ({ children }: QuartzComponentProps) => {
|
function Header({ children }: QuartzComponentProps) {
|
||||||
return children.length > 0 ? <header>{children}</header> : null
|
return children.length > 0 ? <header>{children}</header> : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } fro
|
|||||||
export default ((component?: QuartzComponent) => {
|
export default ((component?: QuartzComponent) => {
|
||||||
if (component) {
|
if (component) {
|
||||||
const Component = component
|
const Component = component
|
||||||
const MobileOnly: QuartzComponent = (props: QuartzComponentProps) => {
|
function MobileOnly(props: QuartzComponentProps) {
|
||||||
return <Component displayClass="mobile-only" {...props} />
|
return <Component displayClass="mobile-only" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FullSlug, resolveRelative } from "../util/path"
|
import { FullSlug, resolveRelative } from "../util/path"
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
import { Date, getDate } from "./Date"
|
import { Date, getDate } from "./Date"
|
||||||
import { QuartzComponent, QuartzComponentProps } from "./types"
|
import { QuartzComponentProps } from "./types"
|
||||||
import { GlobalConfiguration } from "../cfg"
|
import { GlobalConfiguration } from "../cfg"
|
||||||
|
|
||||||
export function byDateAndAlphabetical(
|
export function byDateAndAlphabetical(
|
||||||
@@ -29,7 +29,7 @@ type Props = {
|
|||||||
limit?: number
|
limit?: number
|
||||||
} & QuartzComponentProps
|
} & QuartzComponentProps
|
||||||
|
|
||||||
export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit }: Props) => {
|
export function PageList({ cfg, fileData, allFiles, limit }: Props) {
|
||||||
let list = allFiles.sort(byDateAndAlphabetical(cfg))
|
let list = allFiles.sort(byDateAndAlphabetical(cfg))
|
||||||
if (limit) {
|
if (limit) {
|
||||||
list = list.slice(0, limit)
|
list = list.slice(0, limit)
|
||||||
@@ -46,7 +46,7 @@ export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit }: Pr
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
{page.dates && (
|
{page.dates && (
|
||||||
<p class="meta">
|
<p class="meta">
|
||||||
<Date date={getDate(cfg, page)!} locale={cfg.locale} />
|
<Date date={getDate(cfg, page)!} />
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div class="desc">
|
<div class="desc">
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { pathToRoot } from "../util/path"
|
import { pathToRoot } from "../util/path"
|
||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
import { i18n } from "../i18n"
|
|
||||||
|
|
||||||
const PageTitle: QuartzComponent = ({ fileData, cfg, displayClass }: QuartzComponentProps) => {
|
function PageTitle({ fileData, cfg, displayClass }: QuartzComponentProps) {
|
||||||
const title = cfg?.pageTitle ?? i18n(cfg.locale).propertyDefaults.title
|
const title = cfg?.pageTitle ?? "Untitled Quartz"
|
||||||
const baseDir = pathToRoot(fileData.slug!)
|
const baseDir = pathToRoot(fileData.slug!)
|
||||||
return (
|
return (
|
||||||
<h1 class={classNames(displayClass, "page-title")}>
|
<h1 class={`page-title ${displayClass ?? ""}`}>
|
||||||
<a href={baseDir}>{title}</a>
|
<a href={baseDir}>{title}</a>
|
||||||
</h1>
|
</h1>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import { FullSlug, SimpleSlug, resolveRelative } from "../util/path"
|
import { FullSlug, SimpleSlug, resolveRelative } from "../util/path"
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
import { byDateAndAlphabetical } from "./PageList"
|
import { byDateAndAlphabetical } from "./PageList"
|
||||||
import style from "./styles/recentNotes.scss"
|
import style from "./styles/recentNotes.scss"
|
||||||
import { Date, getDate } from "./Date"
|
import { Date, getDate } from "./Date"
|
||||||
import { GlobalConfiguration } from "../cfg"
|
import { GlobalConfiguration } from "../cfg"
|
||||||
import { i18n } from "../i18n"
|
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
title?: string
|
title: string
|
||||||
limit: number
|
limit: number
|
||||||
linkToMore: SimpleSlug | false
|
linkToMore: SimpleSlug | false
|
||||||
filter: (f: QuartzPluginData) => boolean
|
filter: (f: QuartzPluginData) => boolean
|
||||||
@@ -17,6 +15,7 @@ interface Options {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions = (cfg: GlobalConfiguration): Options => ({
|
const defaultOptions = (cfg: GlobalConfiguration): Options => ({
|
||||||
|
title: "Recent Notes",
|
||||||
limit: 3,
|
limit: 3,
|
||||||
linkToMore: false,
|
linkToMore: false,
|
||||||
filter: () => true,
|
filter: () => true,
|
||||||
@@ -24,21 +23,16 @@ const defaultOptions = (cfg: GlobalConfiguration): Options => ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export default ((userOpts?: Partial<Options>) => {
|
export default ((userOpts?: Partial<Options>) => {
|
||||||
const RecentNotes: QuartzComponent = ({
|
function RecentNotes({ allFiles, fileData, displayClass, cfg }: QuartzComponentProps) {
|
||||||
allFiles,
|
|
||||||
fileData,
|
|
||||||
displayClass,
|
|
||||||
cfg,
|
|
||||||
}: QuartzComponentProps) => {
|
|
||||||
const opts = { ...defaultOptions(cfg), ...userOpts }
|
const opts = { ...defaultOptions(cfg), ...userOpts }
|
||||||
const pages = allFiles.filter(opts.filter).sort(opts.sort)
|
const pages = allFiles.filter(opts.filter).sort(opts.sort)
|
||||||
const remaining = Math.max(0, pages.length - opts.limit)
|
const remaining = Math.max(0, pages.length - opts.limit)
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "recent-notes")}>
|
<div class={`recent-notes ${displayClass ?? ""}`}>
|
||||||
<h3>{opts.title ?? i18n(cfg.locale).components.recentNotes.title}</h3>
|
<h3>{opts.title}</h3>
|
||||||
<ul class="recent-ul">
|
<ul class="recent-ul">
|
||||||
{pages.slice(0, opts.limit).map((page) => {
|
{pages.slice(0, opts.limit).map((page) => {
|
||||||
const title = page.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title
|
const title = page.frontmatter?.title
|
||||||
const tags = page.frontmatter?.tags ?? []
|
const tags = page.frontmatter?.tags ?? []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -53,7 +47,7 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
</div>
|
</div>
|
||||||
{page.dates && (
|
{page.dates && (
|
||||||
<p class="meta">
|
<p class="meta">
|
||||||
<Date date={getDate(cfg, page)!} locale={cfg.locale} />
|
<Date date={getDate(cfg, page)!} />
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<ul class="tags">
|
<ul class="tags">
|
||||||
@@ -75,9 +69,7 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
</ul>
|
</ul>
|
||||||
{opts.linkToMore && remaining > 0 && (
|
{opts.linkToMore && remaining > 0 && (
|
||||||
<p>
|
<p>
|
||||||
<a href={resolveRelative(fileData.slug!, opts.linkToMore)}>
|
<a href={resolveRelative(fileData.slug!, opts.linkToMore)}>See {remaining} more →</a>
|
||||||
{i18n(cfg.locale).components.recentNotes.seeRemainingMore({ remaining })}
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,26 +1,14 @@
|
|||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import style from "./styles/search.scss"
|
import style from "./styles/search.scss"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import script from "./scripts/search.inline"
|
import script from "./scripts/search.inline"
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
import { i18n } from "../i18n"
|
|
||||||
|
|
||||||
export interface SearchOptions {
|
export default (() => {
|
||||||
enablePreview: boolean
|
function Search({ displayClass }: QuartzComponentProps) {
|
||||||
}
|
|
||||||
|
|
||||||
const defaultOptions: SearchOptions = {
|
|
||||||
enablePreview: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ((userOpts?: Partial<SearchOptions>) => {
|
|
||||||
const Search: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
|
||||||
const opts = { ...defaultOptions, ...userOpts }
|
|
||||||
const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder
|
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "search")}>
|
<div class={`search ${displayClass ?? ""}`}>
|
||||||
<div id="search-icon">
|
<div id="search-icon">
|
||||||
<p>{i18n(cfg.locale).components.search.title}</p>
|
<p>Search</p>
|
||||||
<div></div>
|
<div></div>
|
||||||
<svg
|
<svg
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -44,10 +32,10 @@ export default ((userOpts?: Partial<SearchOptions>) => {
|
|||||||
id="search-bar"
|
id="search-bar"
|
||||||
name="search"
|
name="search"
|
||||||
type="text"
|
type="text"
|
||||||
aria-label={searchPlaceholder}
|
aria-label="Search for something"
|
||||||
placeholder={searchPlaceholder}
|
placeholder="Search for something"
|
||||||
/>
|
/>
|
||||||
<div id="search-layout" data-preview={opts.enablePreview}></div>
|
<div id="results-container"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
|
|
||||||
function Spacer({ displayClass }: QuartzComponentProps) {
|
function Spacer({ displayClass }: QuartzComponentProps) {
|
||||||
return <div class={classNames(displayClass, "spacer")}></div>
|
return <div class={`spacer ${displayClass ?? ""}`}></div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (() => Spacer) satisfies QuartzComponentConstructor
|
export default (() => Spacer) satisfies QuartzComponentConstructor
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import legacyStyle from "./styles/legacyToc.scss"
|
import legacyStyle from "./styles/legacyToc.scss"
|
||||||
import modernStyle from "./styles/toc.scss"
|
import modernStyle from "./styles/toc.scss"
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import script from "./scripts/toc.inline"
|
import script from "./scripts/toc.inline"
|
||||||
import { i18n } from "../i18n"
|
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
layout: "modern" | "legacy"
|
layout: "modern" | "legacy"
|
||||||
@@ -15,19 +13,15 @@ const defaultOptions: Options = {
|
|||||||
layout: "modern",
|
layout: "modern",
|
||||||
}
|
}
|
||||||
|
|
||||||
const TableOfContents: QuartzComponent = ({
|
function TableOfContents({ fileData, displayClass }: QuartzComponentProps) {
|
||||||
fileData,
|
|
||||||
displayClass,
|
|
||||||
cfg,
|
|
||||||
}: QuartzComponentProps) => {
|
|
||||||
if (!fileData.toc) {
|
if (!fileData.toc) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "toc")}>
|
<div class={`toc ${displayClass ?? ""}`}>
|
||||||
<button type="button" id="toc" class={fileData.collapseToc ? "collapsed" : ""}>
|
<button type="button" id="toc" class={fileData.collapseToc ? "collapsed" : ""}>
|
||||||
<h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
|
<h3>Table of Contents</h3>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="24"
|
width="24"
|
||||||
@@ -60,14 +54,15 @@ const TableOfContents: QuartzComponent = ({
|
|||||||
TableOfContents.css = modernStyle
|
TableOfContents.css = modernStyle
|
||||||
TableOfContents.afterDOMLoaded = script
|
TableOfContents.afterDOMLoaded = script
|
||||||
|
|
||||||
const LegacyTableOfContents: QuartzComponent = ({ fileData, cfg }: QuartzComponentProps) => {
|
function LegacyTableOfContents({ fileData }: QuartzComponentProps) {
|
||||||
if (!fileData.toc) {
|
if (!fileData.toc) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<details id="toc" open={!fileData.collapseToc}>
|
<details id="toc" open={!fileData.collapseToc}>
|
||||||
<summary>
|
<summary>
|
||||||
<h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
|
<h3>Table of Contents</h3>
|
||||||
</summary>
|
</summary>
|
||||||
<ul>
|
<ul>
|
||||||
{fileData.toc.map((tocEntry) => (
|
{fileData.toc.map((tocEntry) => (
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { pathToRoot, slugTag } from "../util/path"
|
import { pathToRoot, slugTag } from "../util/path"
|
||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import { classNames } from "../util/lang"
|
|
||||||
|
|
||||||
const TagList: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => {
|
function TagList({ fileData, displayClass }: QuartzComponentProps) {
|
||||||
const tags = fileData.frontmatter?.tags
|
const tags = fileData.frontmatter?.tags
|
||||||
const baseDir = pathToRoot(fileData.slug!)
|
const baseDir = pathToRoot(fileData.slug!)
|
||||||
if (tags && tags.length > 0) {
|
if (tags && tags.length > 0) {
|
||||||
return (
|
return (
|
||||||
<ul class={classNames(displayClass, "tags")}>
|
<ul class={`tags ${displayClass ?? ""}`}>
|
||||||
{tags.map((tag) => {
|
{tags.map((tag) => {
|
||||||
const display = `#${tag}`
|
const display = `#${tag}`
|
||||||
const linkDest = baseDir + `/tags/${slugTag(tag)}`
|
const linkDest = baseDir + `/tags/${slugTag(tag)}`
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { i18n } from "../../i18n"
|
import { QuartzComponentConstructor } from "../types"
|
||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
|
||||||
|
|
||||||
function NotFound({ cfg }: QuartzComponentProps) {
|
function NotFound() {
|
||||||
return (
|
return (
|
||||||
<article class="popover-hint">
|
<article class="popover-hint">
|
||||||
<h1>404</h1>
|
<h1>404</h1>
|
||||||
<p>{i18n(cfg.locale).pages.error.notFound}</p>
|
<p>Either this page is private or doesn't exist.</p>
|
||||||
</article>
|
</article>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
|||||||
|
|
||||||
function Content({ fileData, tree }: QuartzComponentProps) {
|
function Content({ fileData, tree }: QuartzComponentProps) {
|
||||||
const content = htmlToJsx(fileData.filePath!, tree)
|
const content = htmlToJsx(fileData.filePath!, tree)
|
||||||
const classes: string[] = fileData.frontmatter?.cssclasses ?? []
|
return <article class="popover-hint">{content}</article>
|
||||||
const classString = ["popover-hint", ...classes].join(" ")
|
|
||||||
return <article class={classString}>{content}</article>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (() => Content) satisfies QuartzComponentConstructor
|
export default (() => Content) satisfies QuartzComponentConstructor
|
||||||
|
|||||||
@@ -3,38 +3,23 @@ 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 { stripSlashes, simplifySlug } from "../../util/path"
|
import { _stripSlashes, simplifySlug } from "../../util/path"
|
||||||
import { Root } from "hast"
|
import { Root } from "hast"
|
||||||
|
import { pluralize } from "../../util/lang"
|
||||||
import { htmlToJsx } from "../../util/jsx"
|
import { htmlToJsx } from "../../util/jsx"
|
||||||
import { i18n } from "../../i18n"
|
|
||||||
|
|
||||||
interface FolderContentOptions {
|
function FolderContent(props: QuartzComponentProps) {
|
||||||
/**
|
const { tree, fileData, allFiles } = props
|
||||||
* Whether to display number of folders
|
const folderSlug = _stripSlashes(simplifySlug(fileData.slug!))
|
||||||
*/
|
|
||||||
showFolderCount: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultOptions: FolderContentOptions = {
|
|
||||||
showFolderCount: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ((opts?: Partial<FolderContentOptions>) => {
|
|
||||||
const options: FolderContentOptions = { ...defaultOptions, ...opts }
|
|
||||||
|
|
||||||
function FolderContent(props: QuartzComponentProps) {
|
|
||||||
const { tree, fileData, allFiles, cfg } = props
|
|
||||||
const folderSlug = stripSlashes(simplifySlug(fileData.slug!))
|
|
||||||
const allPagesInFolder = allFiles.filter((file) => {
|
const allPagesInFolder = allFiles.filter((file) => {
|
||||||
const fileSlug = stripSlashes(simplifySlug(file.slug!))
|
const fileSlug = _stripSlashes(simplifySlug(file.slug!))
|
||||||
const prefixed = fileSlug.startsWith(folderSlug) && fileSlug !== folderSlug
|
const prefixed = fileSlug.startsWith(folderSlug) && fileSlug !== folderSlug
|
||||||
const folderParts = folderSlug.split(path.posix.sep)
|
const folderParts = folderSlug.split(path.posix.sep)
|
||||||
const fileParts = fileSlug.split(path.posix.sep)
|
const fileParts = fileSlug.split(path.posix.sep)
|
||||||
const isDirectChild = fileParts.length === folderParts.length + 1
|
const isDirectChild = fileParts.length === folderParts.length + 1
|
||||||
return prefixed && isDirectChild
|
return prefixed && isDirectChild
|
||||||
})
|
})
|
||||||
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
|
|
||||||
const classes = ["popover-hint", ...cssClasses].join(" ")
|
|
||||||
const listProps = {
|
const listProps = {
|
||||||
...props,
|
...props,
|
||||||
allFiles: allPagesInFolder,
|
allFiles: allPagesInFolder,
|
||||||
@@ -46,26 +31,17 @@ export default ((opts?: Partial<FolderContentOptions>) => {
|
|||||||
: htmlToJsx(fileData.filePath!, tree)
|
: htmlToJsx(fileData.filePath!, tree)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={classes}>
|
<div class="popover-hint">
|
||||||
<article>
|
<article>
|
||||||
<p>{content}</p>
|
<p>{content}</p>
|
||||||
</article>
|
</article>
|
||||||
<div class="page-listing">
|
<p>{pluralize(allPagesInFolder.length, "item")} under this folder.</p>
|
||||||
{options.showFolderCount && (
|
|
||||||
<p>
|
|
||||||
{i18n(cfg.locale).pages.folderContent.itemsUnderFolder({
|
|
||||||
count: allPagesInFolder.length,
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<div>
|
<div>
|
||||||
<PageList {...listProps} />
|
<PageList {...listProps} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
FolderContent.css = style + PageList.css
|
FolderContent.css = style + PageList.css
|
||||||
return FolderContent
|
export default (() => FolderContent) satisfies QuartzComponentConstructor
|
||||||
}) satisfies QuartzComponentConstructor
|
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import { PageList } from "../PageList"
|
|||||||
import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path"
|
import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path"
|
||||||
import { QuartzPluginData } from "../../plugins/vfile"
|
import { QuartzPluginData } from "../../plugins/vfile"
|
||||||
import { Root } from "hast"
|
import { Root } from "hast"
|
||||||
|
import { pluralize } from "../../util/lang"
|
||||||
import { htmlToJsx } from "../../util/jsx"
|
import { htmlToJsx } from "../../util/jsx"
|
||||||
import { i18n } from "../../i18n"
|
|
||||||
|
|
||||||
const numPages = 10
|
const numPages = 10
|
||||||
function TagContent(props: QuartzComponentProps) {
|
function TagContent(props: QuartzComponentProps) {
|
||||||
const { tree, fileData, allFiles, cfg } = props
|
const { tree, fileData, allFiles } = props
|
||||||
const slug = fileData.slug
|
const slug = fileData.slug
|
||||||
|
|
||||||
if (!(slug?.startsWith("tags/") || slug === "tags")) {
|
if (!(slug?.startsWith("tags/") || slug === "tags")) {
|
||||||
@@ -26,8 +26,7 @@ function TagContent(props: QuartzComponentProps) {
|
|||||||
(tree as Root).children.length === 0
|
(tree as Root).children.length === 0
|
||||||
? fileData.description
|
? fileData.description
|
||||||
: htmlToJsx(fileData.filePath!, tree)
|
: htmlToJsx(fileData.filePath!, tree)
|
||||||
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
|
|
||||||
const classes = ["popover-hint", ...cssClasses].join(" ")
|
|
||||||
if (tag === "/") {
|
if (tag === "/") {
|
||||||
const tags = [
|
const tags = [
|
||||||
...new Set(
|
...new Set(
|
||||||
@@ -38,12 +37,13 @@ function TagContent(props: QuartzComponentProps) {
|
|||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
tagItemMap.set(tag, allPagesWithTag(tag))
|
tagItemMap.set(tag, allPagesWithTag(tag))
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={classes}>
|
<div class="popover-hint">
|
||||||
<article>
|
<article>
|
||||||
<p>{content}</p>
|
<p>{content}</p>
|
||||||
</article>
|
</article>
|
||||||
<p>{i18n(cfg.locale).pages.tagContent.totalTags({ count: tags.length })}</p>
|
<p>Found {tags.length} total tags.</p>
|
||||||
<div>
|
<div>
|
||||||
{tags.map((tag) => {
|
{tags.map((tag) => {
|
||||||
const pages = tagItemMap.get(tag)!
|
const pages = tagItemMap.get(tag)!
|
||||||
@@ -62,21 +62,12 @@ function TagContent(props: QuartzComponentProps) {
|
|||||||
</a>
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
{content && <p>{content}</p>}
|
{content && <p>{content}</p>}
|
||||||
<div class="page-listing">
|
|
||||||
<p>
|
<p>
|
||||||
{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}
|
{pluralize(pages.length, "item")} with this tag.{" "}
|
||||||
{pages.length > numPages && (
|
{pages.length > numPages && `Showing first ${numPages}.`}
|
||||||
<>
|
|
||||||
{" "}
|
|
||||||
<span>
|
|
||||||
{i18n(cfg.locale).pages.tagContent.showingFirst({ count: numPages })}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
<PageList limit={numPages} {...listProps} />
|
<PageList limit={numPages} {...listProps} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -90,15 +81,13 @@ function TagContent(props: QuartzComponentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={classes}>
|
<div class="popover-hint">
|
||||||
<article>{content}</article>
|
<article>{content}</article>
|
||||||
<div class="page-listing">
|
<p>{pluralize(pages.length, "item")} with this tag.</p>
|
||||||
<p>{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}</p>
|
|
||||||
<div>
|
<div>
|
||||||
<PageList {...listProps} />
|
<PageList {...listProps} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ 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 "../util/resources"
|
import { JSResourceToScriptElement, StaticResources } from "../util/resources"
|
||||||
import { clone, FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path"
|
import { FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path"
|
||||||
import { visit } from "unist-util-visit"
|
import { visit } from "unist-util-visit"
|
||||||
import { Root, Element, ElementContent } from "hast"
|
import { Root, Element, ElementContent } from "hast"
|
||||||
import { GlobalConfiguration } from "../cfg"
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
import { i18n } from "../i18n"
|
|
||||||
|
|
||||||
interface RenderComponents {
|
interface RenderComponents {
|
||||||
head: QuartzComponent
|
head: QuartzComponent
|
||||||
@@ -24,7 +23,7 @@ export function pageResources(
|
|||||||
staticResources: StaticResources,
|
staticResources: StaticResources,
|
||||||
): StaticResources {
|
): StaticResources {
|
||||||
const contentIndexPath = joinSegments(baseDir, "static/contentIndex.json")
|
const contentIndexPath = joinSegments(baseDir, "static/contentIndex.json")
|
||||||
const contentIndexScript = `const fetchData = fetch("${contentIndexPath}").then(data => data.json())`
|
const contentIndexScript = `const fetchData = fetch(\`${contentIndexPath}\`).then(data => data.json())`
|
||||||
|
|
||||||
return {
|
return {
|
||||||
css: [joinSegments(baseDir, "index.css"), ...staticResources.css],
|
css: [joinSegments(baseDir, "index.css"), ...staticResources.css],
|
||||||
@@ -51,25 +50,32 @@ export function pageResources(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pageIndex: Map<FullSlug, QuartzPluginData> | undefined = undefined
|
||||||
|
function getOrComputeFileIndex(allFiles: QuartzPluginData[]): Map<FullSlug, QuartzPluginData> {
|
||||||
|
if (!pageIndex) {
|
||||||
|
pageIndex = new Map()
|
||||||
|
for (const file of allFiles) {
|
||||||
|
pageIndex.set(file.slug!, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageIndex
|
||||||
|
}
|
||||||
|
|
||||||
export function renderPage(
|
export function renderPage(
|
||||||
cfg: GlobalConfiguration,
|
|
||||||
slug: FullSlug,
|
slug: FullSlug,
|
||||||
componentData: QuartzComponentProps,
|
componentData: QuartzComponentProps,
|
||||||
components: RenderComponents,
|
components: RenderComponents,
|
||||||
pageResources: StaticResources,
|
pageResources: StaticResources,
|
||||||
): string {
|
): string {
|
||||||
// make a deep copy of the tree so we don't remove the transclusion references
|
|
||||||
// for the file cached in contentMap in build.ts
|
|
||||||
const root = clone(componentData.tree) as Root
|
|
||||||
|
|
||||||
// process transcludes in componentData
|
// process transcludes in componentData
|
||||||
visit(root, "element", (node, _index, _parent) => {
|
visit(componentData.tree as Root, "element", (node, _index, _parent) => {
|
||||||
if (node.tagName === "blockquote") {
|
if (node.tagName === "blockquote") {
|
||||||
const classNames = (node.properties?.className ?? []) as string[]
|
const classNames = (node.properties?.className ?? []) as string[]
|
||||||
if (classNames.includes("transclude")) {
|
if (classNames.includes("transclude")) {
|
||||||
const inner = node.children[0] as Element
|
const inner = node.children[0] as Element
|
||||||
const transcludeTarget = inner.properties["data-slug"] as FullSlug
|
const transcludeTarget = inner.properties["data-slug"] as FullSlug
|
||||||
const page = componentData.allFiles.find((f) => f.slug === transcludeTarget)
|
const page = getOrComputeFileIndex(componentData.allFiles).get(transcludeTarget)
|
||||||
if (!page) {
|
if (!page) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -94,10 +100,8 @@ export function renderPage(
|
|||||||
{
|
{
|
||||||
type: "element",
|
type: "element",
|
||||||
tagName: "a",
|
tagName: "a",
|
||||||
properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] },
|
properties: { href: inner.properties?.href, class: ["internal"] },
|
||||||
children: [
|
children: [{ type: "text", value: `Link to original` }],
|
||||||
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -131,10 +135,8 @@ export function renderPage(
|
|||||||
{
|
{
|
||||||
type: "element",
|
type: "element",
|
||||||
tagName: "a",
|
tagName: "a",
|
||||||
properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] },
|
properties: { href: inner.properties?.href, class: ["internal"] },
|
||||||
children: [
|
children: [{ type: "text", value: `Link to original` }],
|
||||||
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} else if (page.htmlAst) {
|
} else if (page.htmlAst) {
|
||||||
@@ -145,14 +147,7 @@ export function renderPage(
|
|||||||
tagName: "h1",
|
tagName: "h1",
|
||||||
properties: {},
|
properties: {},
|
||||||
children: [
|
children: [
|
||||||
{
|
{ type: "text", value: page.frontmatter?.title ?? `Transclude of ${page.slug}` },
|
||||||
type: "text",
|
|
||||||
value:
|
|
||||||
page.frontmatter?.title ??
|
|
||||||
i18n(cfg.locale).components.transcludes.transcludeOf({
|
|
||||||
targetSlug: page.slug!,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
...(page.htmlAst.children as ElementContent[]).map((child) =>
|
...(page.htmlAst.children as ElementContent[]).map((child) =>
|
||||||
@@ -161,10 +156,8 @@ export function renderPage(
|
|||||||
{
|
{
|
||||||
type: "element",
|
type: "element",
|
||||||
tagName: "a",
|
tagName: "a",
|
||||||
properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] },
|
properties: { href: inner.properties?.href, class: ["internal"] },
|
||||||
children: [
|
children: [{ type: "text", value: `Link to original` }],
|
||||||
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -172,9 +165,6 @@ export function renderPage(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// set componentData.tree to the edited html that has transclusions rendered
|
|
||||||
componentData.tree = root
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
head: Head,
|
head: Head,
|
||||||
header,
|
header,
|
||||||
@@ -203,9 +193,8 @@ export function renderPage(
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
const lang = componentData.frontmatter?.lang ?? cfg.locale?.split("-")[0] ?? "en"
|
|
||||||
const doc = (
|
const doc = (
|
||||||
<html lang={lang}>
|
<html>
|
||||||
<Head {...componentData} />
|
<Head {...componentData} />
|
||||||
<body data-slug={slug}>
|
<body data-slug={slug}>
|
||||||
<div id="quartz-root" class="page">
|
<div id="quartz-root" class="page">
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
function toggleCallout(this: HTMLElement) {
|
function toggleCallout(this: HTMLElement) {
|
||||||
const outerBlock = this.parentElement!
|
const outerBlock = this.parentElement!
|
||||||
outerBlock.classList.toggle("is-collapsed")
|
outerBlock.classList.toggle(`is-collapsed`)
|
||||||
const collapsed = outerBlock.classList.contains("is-collapsed")
|
const collapsed = outerBlock.classList.contains(`is-collapsed`)
|
||||||
const height = collapsed ? this.scrollHeight : outerBlock.scrollHeight
|
const height = collapsed ? this.scrollHeight : outerBlock.scrollHeight
|
||||||
outerBlock.style.maxHeight = height + "px"
|
outerBlock.style.maxHeight = height + `px`
|
||||||
|
|
||||||
// walk and adjust height of all parents
|
// walk and adjust height of all parents
|
||||||
let current = outerBlock
|
let current = outerBlock
|
||||||
let parent = outerBlock.parentElement
|
let parent = outerBlock.parentElement
|
||||||
while (parent) {
|
while (parent) {
|
||||||
if (!parent.classList.contains("callout")) {
|
if (!parent.classList.contains(`callout`)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const collapsed = parent.classList.contains("is-collapsed")
|
const collapsed = parent.classList.contains(`is-collapsed`)
|
||||||
const height = collapsed ? parent.scrollHeight : parent.scrollHeight + current.scrollHeight
|
const height = collapsed ? parent.scrollHeight : parent.scrollHeight + current.scrollHeight
|
||||||
parent.style.maxHeight = height + "px"
|
parent.style.maxHeight = height + `px`
|
||||||
|
|
||||||
current = parent
|
current = parent
|
||||||
parent = parent.parentElement
|
parent = parent.parentElement
|
||||||
@@ -30,15 +30,15 @@ function setupCallout() {
|
|||||||
const title = div.firstElementChild
|
const title = div.firstElementChild
|
||||||
|
|
||||||
if (title) {
|
if (title) {
|
||||||
title.addEventListener("click", toggleCallout)
|
title.removeEventListener(`click`, toggleCallout)
|
||||||
window.addCleanup(() => title.removeEventListener("click", toggleCallout))
|
title.addEventListener(`click`, toggleCallout)
|
||||||
|
|
||||||
const collapsed = div.classList.contains("is-collapsed")
|
const collapsed = div.classList.contains(`is-collapsed`)
|
||||||
const height = collapsed ? title.scrollHeight : div.scrollHeight
|
const height = collapsed ? title.scrollHeight : div.scrollHeight
|
||||||
div.style.maxHeight = height + "px"
|
div.style.maxHeight = height + `px`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("nav", setupCallout)
|
document.addEventListener(`nav`, setupCallout)
|
||||||
window.addEventListener("resize", setupCallout)
|
window.addEventListener(`resize`, setupCallout)
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { getFullSlug } from "../../util/path"
|
|
||||||
|
|
||||||
const checkboxId = (index: number) => `${getFullSlug(window)}-checkbox-${index}`
|
|
||||||
|
|
||||||
document.addEventListener("nav", () => {
|
|
||||||
const checkboxes = document.querySelectorAll(
|
|
||||||
"input.checkbox-toggle",
|
|
||||||
) as NodeListOf<HTMLInputElement>
|
|
||||||
checkboxes.forEach((el, index) => {
|
|
||||||
const elId = checkboxId(index)
|
|
||||||
|
|
||||||
const switchState = (e: Event) => {
|
|
||||||
const newCheckboxState = (e.target as HTMLInputElement)?.checked ? "true" : "false"
|
|
||||||
localStorage.setItem(elId, newCheckboxState)
|
|
||||||
}
|
|
||||||
|
|
||||||
el.addEventListener("change", switchState)
|
|
||||||
window.addCleanup(() => el.removeEventListener("change", switchState))
|
|
||||||
if (localStorage.getItem(elId) === "true") {
|
|
||||||
el.checked = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -14,7 +14,7 @@ document.addEventListener("nav", () => {
|
|||||||
button.type = "button"
|
button.type = "button"
|
||||||
button.innerHTML = svgCopy
|
button.innerHTML = svgCopy
|
||||||
button.ariaLabel = "Copy source"
|
button.ariaLabel = "Copy source"
|
||||||
function onClick() {
|
button.addEventListener("click", () => {
|
||||||
navigator.clipboard.writeText(source).then(
|
navigator.clipboard.writeText(source).then(
|
||||||
() => {
|
() => {
|
||||||
button.blur()
|
button.blur()
|
||||||
@@ -26,9 +26,7 @@ document.addEventListener("nav", () => {
|
|||||||
},
|
},
|
||||||
(error) => console.error(error),
|
(error) => console.error(error),
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
button.addEventListener("click", onClick)
|
|
||||||
window.addCleanup(() => button.removeEventListener("click", onClick))
|
|
||||||
els[i].prepend(button)
|
els[i].prepend(button)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,39 +2,31 @@ const userPref = window.matchMedia("(prefers-color-scheme: light)").matches ? "l
|
|||||||
const currentTheme = localStorage.getItem("theme") ?? userPref
|
const currentTheme = localStorage.getItem("theme") ?? userPref
|
||||||
document.documentElement.setAttribute("saved-theme", currentTheme)
|
document.documentElement.setAttribute("saved-theme", currentTheme)
|
||||||
|
|
||||||
const emitThemeChangeEvent = (theme: "light" | "dark") => {
|
|
||||||
const event: CustomEventMap["themechange"] = new CustomEvent("themechange", {
|
|
||||||
detail: { theme },
|
|
||||||
})
|
|
||||||
document.dispatchEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("nav", () => {
|
document.addEventListener("nav", () => {
|
||||||
const switchTheme = (e: Event) => {
|
const switchTheme = (e: any) => {
|
||||||
const newTheme = (e.target as HTMLInputElement)?.checked ? "dark" : "light"
|
if (e.target.checked) {
|
||||||
document.documentElement.setAttribute("saved-theme", newTheme)
|
document.documentElement.setAttribute("saved-theme", "dark")
|
||||||
localStorage.setItem("theme", newTheme)
|
localStorage.setItem("theme", "dark")
|
||||||
emitThemeChangeEvent(newTheme)
|
} else {
|
||||||
|
document.documentElement.setAttribute("saved-theme", "light")
|
||||||
|
localStorage.setItem("theme", "light")
|
||||||
}
|
}
|
||||||
|
|
||||||
const themeChange = (e: MediaQueryListEvent) => {
|
|
||||||
const newTheme = e.matches ? "dark" : "light"
|
|
||||||
document.documentElement.setAttribute("saved-theme", newTheme)
|
|
||||||
localStorage.setItem("theme", newTheme)
|
|
||||||
toggleSwitch.checked = e.matches
|
|
||||||
emitThemeChangeEvent(newTheme)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Darkmode toggle
|
// Darkmode toggle
|
||||||
const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement
|
const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement
|
||||||
|
toggleSwitch.removeEventListener("change", switchTheme)
|
||||||
toggleSwitch.addEventListener("change", switchTheme)
|
toggleSwitch.addEventListener("change", switchTheme)
|
||||||
window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
|
|
||||||
if (currentTheme === "dark") {
|
if (currentTheme === "dark") {
|
||||||
toggleSwitch.checked = true
|
toggleSwitch.checked = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for changes in prefers-color-scheme
|
// Listen for changes in prefers-color-scheme
|
||||||
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
colorSchemeMediaQuery.addEventListener("change", themeChange)
|
colorSchemeMediaQuery.addEventListener("change", (e) => {
|
||||||
window.addCleanup(() => colorSchemeMediaQuery.removeEventListener("change", themeChange))
|
const newTheme = e.matches ? "dark" : "light"
|
||||||
|
document.documentElement.setAttribute("saved-theme", newTheme)
|
||||||
|
localStorage.setItem("theme", newTheme)
|
||||||
|
toggleSwitch.checked = e.matches
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,106 +1,135 @@
|
|||||||
import { FolderState } from "../ExplorerNode"
|
import { FolderState } from "../ExplorerNode"
|
||||||
|
|
||||||
type MaybeHTMLElement = HTMLElement | undefined
|
// Current state of folders
|
||||||
let currentExplorerState: FolderState[]
|
let explorerState: FolderState[]
|
||||||
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
const observer = new IntersectionObserver((entries) => {
|
||||||
// If last element is observed, remove gradient of "overflow" class so element is visible
|
// If last element is observed, remove gradient of "overflow" class so element is visible
|
||||||
const explorerUl = document.getElementById("explorer-ul")
|
const explorer = document.getElementById("explorer-ul")
|
||||||
if (!explorerUl) return
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
explorerUl.classList.add("no-background")
|
explorer?.classList.add("no-background")
|
||||||
} else {
|
} else {
|
||||||
explorerUl.classList.remove("no-background")
|
explorer?.classList.remove("no-background")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function toggleExplorer(this: HTMLElement) {
|
function toggleExplorer(this: HTMLElement) {
|
||||||
|
// Toggle collapsed state of entire explorer
|
||||||
this.classList.toggle("collapsed")
|
this.classList.toggle("collapsed")
|
||||||
const content = this.nextElementSibling as MaybeHTMLElement
|
const content = this.nextElementSibling as HTMLElement
|
||||||
if (!content) return
|
|
||||||
|
|
||||||
content.classList.toggle("collapsed")
|
content.classList.toggle("collapsed")
|
||||||
content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
|
content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFolder(evt: MouseEvent) {
|
function toggleFolder(evt: MouseEvent) {
|
||||||
evt.stopPropagation()
|
evt.stopPropagation()
|
||||||
const target = evt.target as MaybeHTMLElement
|
|
||||||
if (!target) return
|
|
||||||
|
|
||||||
|
// Element that was clicked
|
||||||
|
const target = evt.target as HTMLElement
|
||||||
|
|
||||||
|
// Check if target was svg icon or button
|
||||||
const isSvg = target.nodeName === "svg"
|
const isSvg = target.nodeName === "svg"
|
||||||
const childFolderContainer = (
|
|
||||||
isSvg
|
// corresponding <ul> element relative to clicked button/folder
|
||||||
? target.parentElement?.nextSibling
|
let childFolderContainer: HTMLElement
|
||||||
: target.parentElement?.parentElement?.nextElementSibling
|
|
||||||
) as MaybeHTMLElement
|
// <li> element of folder (stores folder-path dataset)
|
||||||
const currentFolderParent = (
|
let currentFolderParent: HTMLElement
|
||||||
isSvg ? target.nextElementSibling : target.parentElement
|
|
||||||
) as MaybeHTMLElement
|
// Get correct relative container and toggle collapsed class
|
||||||
if (!(childFolderContainer && currentFolderParent)) return
|
if (isSvg) {
|
||||||
|
childFolderContainer = target.parentElement?.nextSibling as HTMLElement
|
||||||
|
currentFolderParent = target.nextElementSibling as HTMLElement
|
||||||
|
|
||||||
childFolderContainer.classList.toggle("open")
|
childFolderContainer.classList.toggle("open")
|
||||||
|
} else {
|
||||||
|
childFolderContainer = target.parentElement?.parentElement?.nextElementSibling as HTMLElement
|
||||||
|
currentFolderParent = target.parentElement as HTMLElement
|
||||||
|
|
||||||
|
childFolderContainer.classList.toggle("open")
|
||||||
|
}
|
||||||
|
if (!childFolderContainer) return
|
||||||
|
|
||||||
|
// Collapse folder container
|
||||||
const isCollapsed = childFolderContainer.classList.contains("open")
|
const isCollapsed = childFolderContainer.classList.contains("open")
|
||||||
setFolderState(childFolderContainer, !isCollapsed)
|
setFolderState(childFolderContainer, !isCollapsed)
|
||||||
const fullFolderPath = currentFolderParent.dataset.folderpath as string
|
|
||||||
toggleCollapsedByPath(currentExplorerState, fullFolderPath)
|
// Save folder state to localStorage
|
||||||
const stringifiedFileTree = JSON.stringify(currentExplorerState)
|
const clickFolderPath = currentFolderParent.dataset.folderpath as string
|
||||||
|
|
||||||
|
// Remove leading "/"
|
||||||
|
const fullFolderPath = clickFolderPath.substring(1)
|
||||||
|
toggleCollapsedByPath(explorerState, fullFolderPath)
|
||||||
|
|
||||||
|
const stringifiedFileTree = JSON.stringify(explorerState)
|
||||||
localStorage.setItem("fileTree", stringifiedFileTree)
|
localStorage.setItem("fileTree", stringifiedFileTree)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupExplorer() {
|
function setupExplorer() {
|
||||||
|
// Set click handler for collapsing entire explorer
|
||||||
const explorer = document.getElementById("explorer")
|
const explorer = document.getElementById("explorer")
|
||||||
if (!explorer) return
|
|
||||||
|
|
||||||
if (explorer.dataset.behavior === "collapse") {
|
|
||||||
for (const item of document.getElementsByClassName(
|
|
||||||
"folder-button",
|
|
||||||
) as HTMLCollectionOf<HTMLElement>) {
|
|
||||||
item.addEventListener("click", toggleFolder)
|
|
||||||
window.addCleanup(() => item.removeEventListener("click", toggleFolder))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
explorer.addEventListener("click", toggleExplorer)
|
|
||||||
window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer))
|
|
||||||
|
|
||||||
// Set up click handlers for each folder (click handler on folder "icon")
|
|
||||||
for (const item of document.getElementsByClassName(
|
|
||||||
"folder-icon",
|
|
||||||
) as HTMLCollectionOf<HTMLElement>) {
|
|
||||||
item.addEventListener("click", toggleFolder)
|
|
||||||
window.addCleanup(() => item.removeEventListener("click", toggleFolder))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get folder state from local storage
|
// Get folder state from local storage
|
||||||
const storageTree = localStorage.getItem("fileTree")
|
const storageTree = localStorage.getItem("fileTree")
|
||||||
|
|
||||||
|
// Convert to bool
|
||||||
const useSavedFolderState = explorer?.dataset.savestate === "true"
|
const useSavedFolderState = explorer?.dataset.savestate === "true"
|
||||||
const oldExplorerState: FolderState[] =
|
|
||||||
storageTree && useSavedFolderState ? JSON.parse(storageTree) : []
|
if (explorer) {
|
||||||
const oldIndex = new Map(oldExplorerState.map((entry) => [entry.path, entry.collapsed]))
|
// Get config
|
||||||
const newExplorerState: FolderState[] = explorer.dataset.tree
|
const collapseBehavior = explorer.dataset.behavior
|
||||||
? JSON.parse(explorer.dataset.tree)
|
|
||||||
: []
|
// Add click handlers for all folders (click handler on folder "label")
|
||||||
currentExplorerState = []
|
if (collapseBehavior === "collapse") {
|
||||||
for (const { path, collapsed } of newExplorerState) {
|
Array.prototype.forEach.call(
|
||||||
currentExplorerState.push({ path, collapsed: oldIndex.get(path) ?? collapsed })
|
document.getElementsByClassName("folder-button"),
|
||||||
|
function (item) {
|
||||||
|
item.removeEventListener("click", toggleFolder)
|
||||||
|
item.addEventListener("click", toggleFolder)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
currentExplorerState.map((folderState) => {
|
// Add click handler to main explorer
|
||||||
|
explorer.removeEventListener("click", toggleExplorer)
|
||||||
|
explorer.addEventListener("click", toggleExplorer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up click handlers for each folder (click handler on folder "icon")
|
||||||
|
Array.prototype.forEach.call(document.getElementsByClassName("folder-icon"), function (item) {
|
||||||
|
item.removeEventListener("click", toggleFolder)
|
||||||
|
item.addEventListener("click", toggleFolder)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (storageTree && useSavedFolderState) {
|
||||||
|
// Get state from localStorage and set folder state
|
||||||
|
explorerState = JSON.parse(storageTree)
|
||||||
|
explorerState.map((folderUl) => {
|
||||||
|
// grab <li> element for matching folder path
|
||||||
const folderLi = document.querySelector(
|
const folderLi = document.querySelector(
|
||||||
`[data-folderpath='${folderState.path}']`,
|
`[data-folderpath='/${folderUl.path}']`,
|
||||||
) as MaybeHTMLElement
|
) as HTMLElement
|
||||||
const folderUl = folderLi?.parentElement?.nextElementSibling as MaybeHTMLElement
|
|
||||||
if (folderUl) {
|
// Get corresponding content <ul> tag and set state
|
||||||
setFolderState(folderUl, folderState.collapsed)
|
if (folderLi) {
|
||||||
|
const folderUL = folderLi.parentElement?.nextElementSibling
|
||||||
|
if (folderUL) {
|
||||||
|
setFolderState(folderUL as HTMLElement, folderUl.collapsed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
} else if (explorer?.dataset.tree) {
|
||||||
|
// If tree is not in localStorage or config is disabled, use tree passed from Explorer as dataset
|
||||||
|
explorerState = JSON.parse(explorer.dataset.tree)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("resize", setupExplorer)
|
window.addEventListener("resize", setupExplorer)
|
||||||
document.addEventListener("nav", () => {
|
document.addEventListener("nav", () => {
|
||||||
setupExplorer()
|
setupExplorer()
|
||||||
|
|
||||||
observer.disconnect()
|
observer.disconnect()
|
||||||
|
|
||||||
// select pseudo element at end of list
|
// select pseudo element at end of list
|
||||||
@@ -116,7 +145,11 @@ document.addEventListener("nav", () => {
|
|||||||
* @param collapsed if folder should be set to collapsed or not
|
* @param collapsed if folder should be set to collapsed or not
|
||||||
*/
|
*/
|
||||||
function setFolderState(folderElement: HTMLElement, collapsed: boolean) {
|
function setFolderState(folderElement: HTMLElement, collapsed: boolean) {
|
||||||
return collapsed ? folderElement.classList.remove("open") : folderElement.classList.add("open")
|
if (collapsed) {
|
||||||
|
folderElement?.classList.remove("open")
|
||||||
|
} else {
|
||||||
|
folderElement?.classList.add("open")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -319,12 +319,12 @@ function renderGlobalGraph() {
|
|||||||
registerEscapeHandler(container, hideGlobalGraph)
|
registerEscapeHandler(container, hideGlobalGraph)
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
document.addEventListener("nav", async (e: unknown) => {
|
||||||
const slug = e.detail.url
|
const slug = (e as CustomEventMap["nav"]).detail.url
|
||||||
addToVisited(slug)
|
addToVisited(slug)
|
||||||
await renderGraph("graph-container", slug)
|
await renderGraph("graph-container", slug)
|
||||||
|
|
||||||
const containerIcon = document.getElementById("global-graph-icon")
|
const containerIcon = document.getElementById("global-graph-icon")
|
||||||
|
containerIcon?.removeEventListener("click", renderGlobalGraph)
|
||||||
containerIcon?.addEventListener("click", renderGlobalGraph)
|
containerIcon?.addEventListener("click", renderGlobalGraph)
|
||||||
window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph))
|
|
||||||
})
|
})
|
||||||
|
|||||||
3
quartz/components/scripts/plausible.inline.ts
Normal file
3
quartz/components/scripts/plausible.inline.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import Plausible from "plausible-tracker"
|
||||||
|
const { trackPageview } = Plausible()
|
||||||
|
document.addEventListener("nav", () => trackPageview())
|
||||||
@@ -37,7 +37,9 @@ async function mouseEnterHandler(
|
|||||||
targetUrl.hash = ""
|
targetUrl.hash = ""
|
||||||
targetUrl.search = ""
|
targetUrl.search = ""
|
||||||
|
|
||||||
const response = await fetch(`${targetUrl}`).catch((err) => {
|
const contents = await fetch(`${targetUrl}`)
|
||||||
|
.then((res) => res.text())
|
||||||
|
.catch((err) => {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -46,46 +48,18 @@ async function mouseEnterHandler(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response) return
|
if (!contents) return
|
||||||
const [contentType] = response.headers.get("Content-Type")!.split(";")
|
const html = p.parseFromString(contents, "text/html")
|
||||||
const [contentTypeCategory, typeInfo] = contentType.split("/")
|
normalizeRelativeURLs(html, targetUrl)
|
||||||
|
const elts = [...html.getElementsByClassName("popover-hint")]
|
||||||
|
if (elts.length === 0) return
|
||||||
|
|
||||||
const popoverElement = document.createElement("div")
|
const popoverElement = document.createElement("div")
|
||||||
popoverElement.classList.add("popover")
|
popoverElement.classList.add("popover")
|
||||||
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)
|
||||||
|
|
||||||
popoverInner.dataset.contentType = contentType ?? undefined
|
|
||||||
|
|
||||||
switch (contentTypeCategory) {
|
|
||||||
case "image":
|
|
||||||
const img = document.createElement("img")
|
|
||||||
img.src = targetUrl.toString()
|
|
||||||
img.alt = targetUrl.pathname
|
|
||||||
|
|
||||||
popoverInner.appendChild(img)
|
|
||||||
break
|
|
||||||
case "application":
|
|
||||||
switch (typeInfo) {
|
|
||||||
case "pdf":
|
|
||||||
const pdf = document.createElement("iframe")
|
|
||||||
pdf.src = targetUrl.toString()
|
|
||||||
popoverInner.appendChild(pdf)
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
const contents = await response.text()
|
|
||||||
const html = p.parseFromString(contents, "text/html")
|
|
||||||
normalizeRelativeURLs(html, targetUrl)
|
|
||||||
const elts = [...html.getElementsByClassName("popover-hint")]
|
|
||||||
if (elts.length === 0) return
|
|
||||||
|
|
||||||
elts.forEach((elt) => popoverInner.appendChild(elt))
|
elts.forEach((elt) => popoverInner.appendChild(elt))
|
||||||
}
|
|
||||||
|
|
||||||
setPosition(popoverElement)
|
setPosition(popoverElement)
|
||||||
link.appendChild(popoverElement)
|
link.appendChild(popoverElement)
|
||||||
@@ -102,7 +76,7 @@ async function mouseEnterHandler(
|
|||||||
document.addEventListener("nav", () => {
|
document.addEventListener("nav", () => {
|
||||||
const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[]
|
const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[]
|
||||||
for (const link of links) {
|
for (const link of links) {
|
||||||
|
link.removeEventListener("mouseenter", mouseEnterHandler)
|
||||||
link.addEventListener("mouseenter", mouseEnterHandler)
|
link.addEventListener("mouseenter", mouseEnterHandler)
|
||||||
window.addCleanup(() => link.removeEventListener("mouseenter", mouseEnterHandler))
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import FlexSearch from "flexsearch"
|
import { Document, SimpleDocumentSearchResultSetUnit } from "flexsearch"
|
||||||
import { ContentDetails } from "../../plugins/emitters/contentIndex"
|
import { ContentDetails } from "../../plugins/emitters/contentIndex"
|
||||||
import { registerEscapeHandler, removeAllChildren } from "./util"
|
import { registerEscapeHandler, removeAllChildren } from "./util"
|
||||||
import { FullSlug, normalizeRelativeURLs, resolveRelative } from "../../util/path"
|
import { FullSlug, resolveRelative } from "../../util/path"
|
||||||
|
|
||||||
interface Item {
|
interface Item {
|
||||||
id: number
|
id: number
|
||||||
@@ -11,53 +11,23 @@ interface Item {
|
|||||||
tags: string[]
|
tags: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let index: Document<Item> | undefined = undefined
|
||||||
|
|
||||||
// Can be expanded with things like "term" in the future
|
// Can be expanded with things like "term" in the future
|
||||||
type SearchType = "basic" | "tags"
|
type SearchType = "basic" | "tags"
|
||||||
|
|
||||||
|
// Current searchType
|
||||||
let searchType: SearchType = "basic"
|
let searchType: SearchType = "basic"
|
||||||
let currentSearchTerm: string = ""
|
|
||||||
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
|
|
||||||
let index = new FlexSearch.Document<Item>({
|
|
||||||
charset: "latin:extra",
|
|
||||||
encode: encoder,
|
|
||||||
document: {
|
|
||||||
id: "id",
|
|
||||||
index: [
|
|
||||||
{
|
|
||||||
field: "title",
|
|
||||||
tokenize: "forward",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "content",
|
|
||||||
tokenize: "forward",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "tags",
|
|
||||||
tokenize: "forward",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const p = new DOMParser()
|
|
||||||
const fetchContentCache: Map<FullSlug, Element[]> = new Map()
|
|
||||||
const contextWindowWords = 30
|
const contextWindowWords = 30
|
||||||
const numSearchResults = 8
|
const numSearchResults = 5
|
||||||
const numTagResults = 5
|
const numTagResults = 3
|
||||||
|
|
||||||
const tokenizeTerm = (term: string) => {
|
|
||||||
const tokens = term.split(/\s+/).filter((t) => t.trim() !== "")
|
|
||||||
const tokenLen = tokens.length
|
|
||||||
if (tokenLen > 1) {
|
|
||||||
for (let i = 1; i < tokenLen; i++) {
|
|
||||||
tokens.push(tokens.slice(0, i + 1).join(" "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokens.sort((a, b) => b.length - a.length) // always highlight longest terms first
|
|
||||||
}
|
|
||||||
|
|
||||||
function highlight(searchTerm: string, text: string, trim?: boolean) {
|
function highlight(searchTerm: string, text: string, trim?: boolean) {
|
||||||
const tokenizedTerms = tokenizeTerm(searchTerm)
|
// try to highlight longest tokens first
|
||||||
|
const tokenizedTerms = searchTerm
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter((t) => t !== "")
|
||||||
|
.sort((a, b) => b.length - a.length)
|
||||||
let tokenizedText = text.split(/\s+/).filter((t) => t !== "")
|
let tokenizedText = text.split(/\s+/).filter((t) => t !== "")
|
||||||
|
|
||||||
let startIndex = 0
|
let startIndex = 0
|
||||||
@@ -65,12 +35,12 @@ function highlight(searchTerm: string, text: string, trim?: boolean) {
|
|||||||
if (trim) {
|
if (trim) {
|
||||||
const includesCheck = (tok: string) =>
|
const includesCheck = (tok: string) =>
|
||||||
tokenizedTerms.some((term) => tok.toLowerCase().startsWith(term.toLowerCase()))
|
tokenizedTerms.some((term) => tok.toLowerCase().startsWith(term.toLowerCase()))
|
||||||
const occurrencesIndices = tokenizedText.map(includesCheck)
|
const occurencesIndices = tokenizedText.map(includesCheck)
|
||||||
|
|
||||||
let bestSum = 0
|
let bestSum = 0
|
||||||
let bestIndex = 0
|
let bestIndex = 0
|
||||||
for (let i = 0; i < Math.max(tokenizedText.length - contextWindowWords, 0); i++) {
|
for (let i = 0; i < Math.max(tokenizedText.length - contextWindowWords, 0); i++) {
|
||||||
const window = occurrencesIndices.slice(i, i + contextWindowWords)
|
const window = occurencesIndices.slice(i, i + contextWindowWords)
|
||||||
const windowSum = window.reduce((total, cur) => total + (cur ? 1 : 0), 0)
|
const windowSum = window.reduce((total, cur) => total + (cur ? 1 : 0), 0)
|
||||||
if (windowSum >= bestSum) {
|
if (windowSum >= bestSum) {
|
||||||
bestSum = windowSum
|
bestSum = windowSum
|
||||||
@@ -101,76 +71,20 @@ function highlight(searchTerm: string, text: string, trim?: boolean) {
|
|||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function highlightHTML(searchTerm: string, el: HTMLElement) {
|
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
|
||||||
const p = new DOMParser()
|
let prevShortcutHandler: ((e: HTMLElementEventMap["keydown"]) => void) | undefined = undefined
|
||||||
const tokenizedTerms = tokenizeTerm(searchTerm)
|
document.addEventListener("nav", async (e: unknown) => {
|
||||||
const html = p.parseFromString(el.innerHTML, "text/html")
|
const currentSlug = (e as CustomEventMap["nav"]).detail.url
|
||||||
|
|
||||||
const createHighlightSpan = (text: string) => {
|
|
||||||
const span = document.createElement("span")
|
|
||||||
span.className = "highlight"
|
|
||||||
span.textContent = text
|
|
||||||
return span
|
|
||||||
}
|
|
||||||
|
|
||||||
const highlightTextNodes = (node: Node, term: string) => {
|
|
||||||
if (node.nodeType === Node.TEXT_NODE) {
|
|
||||||
const nodeText = node.nodeValue ?? ""
|
|
||||||
const regex = new RegExp(term.toLowerCase(), "gi")
|
|
||||||
const matches = nodeText.match(regex)
|
|
||||||
if (!matches || matches.length === 0) return
|
|
||||||
const spanContainer = document.createElement("span")
|
|
||||||
let lastIndex = 0
|
|
||||||
for (const match of matches) {
|
|
||||||
const matchIndex = nodeText.indexOf(match, lastIndex)
|
|
||||||
spanContainer.appendChild(document.createTextNode(nodeText.slice(lastIndex, matchIndex)))
|
|
||||||
spanContainer.appendChild(createHighlightSpan(match))
|
|
||||||
lastIndex = matchIndex + match.length
|
|
||||||
}
|
|
||||||
spanContainer.appendChild(document.createTextNode(nodeText.slice(lastIndex)))
|
|
||||||
node.parentNode?.replaceChild(spanContainer, node)
|
|
||||||
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
if ((node as HTMLElement).classList.contains("highlight")) return
|
|
||||||
Array.from(node.childNodes).forEach((child) => highlightTextNodes(child, term))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const term of tokenizedTerms) {
|
|
||||||
highlightTextNodes(html.body, term)
|
|
||||||
}
|
|
||||||
|
|
||||||
return html.body
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|
||||||
const currentSlug = e.detail.url
|
|
||||||
const data = await fetchData
|
const data = await fetchData
|
||||||
const container = document.getElementById("search-container")
|
const container = document.getElementById("search-container")
|
||||||
const sidebar = container?.closest(".sidebar") as HTMLElement
|
const sidebar = container?.closest(".sidebar") as HTMLElement
|
||||||
const searchIcon = document.getElementById("search-icon")
|
const searchIcon = document.getElementById("search-icon")
|
||||||
const searchBar = document.getElementById("search-bar") as HTMLInputElement | null
|
const searchBar = document.getElementById("search-bar") as HTMLInputElement | null
|
||||||
const searchLayout = document.getElementById("search-layout")
|
const results = document.getElementById("results-container")
|
||||||
|
const resultCards = document.getElementsByClassName("result-card")
|
||||||
const idDataMap = Object.keys(data) as FullSlug[]
|
const idDataMap = Object.keys(data) as FullSlug[]
|
||||||
|
|
||||||
const appendLayout = (el: HTMLElement) => {
|
|
||||||
if (searchLayout?.querySelector(`#${el.id}`) === null) {
|
|
||||||
searchLayout?.appendChild(el)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const enablePreview = searchLayout?.dataset?.preview === "true"
|
|
||||||
let preview: HTMLDivElement | undefined = undefined
|
|
||||||
let previewInner: HTMLDivElement | undefined = undefined
|
|
||||||
const results = document.createElement("div")
|
|
||||||
results.id = "results-container"
|
|
||||||
appendLayout(results)
|
|
||||||
|
|
||||||
if (enablePreview) {
|
|
||||||
preview = document.createElement("div")
|
|
||||||
preview.id = "preview-container"
|
|
||||||
appendLayout(preview)
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideSearch() {
|
function hideSearch() {
|
||||||
container?.classList.remove("active")
|
container?.classList.remove("active")
|
||||||
if (searchBar) {
|
if (searchBar) {
|
||||||
@@ -182,12 +96,6 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
if (results) {
|
if (results) {
|
||||||
removeAllChildren(results)
|
removeAllChildren(results)
|
||||||
}
|
}
|
||||||
if (preview) {
|
|
||||||
removeAllChildren(preview)
|
|
||||||
}
|
|
||||||
if (searchLayout) {
|
|
||||||
searchLayout.classList.remove("display-results")
|
|
||||||
}
|
|
||||||
|
|
||||||
searchType = "basic" // reset search type after closing
|
searchType = "basic" // reset search type after closing
|
||||||
}
|
}
|
||||||
@@ -201,14 +109,11 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
searchBar?.focus()
|
searchBar?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentHover: HTMLInputElement | null = null
|
function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
|
||||||
|
|
||||||
async function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
|
|
||||||
if (e.key === "k" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
if (e.key === "k" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const searchBarOpen = container?.classList.contains("active")
|
const searchBarOpen = container?.classList.contains("active")
|
||||||
searchBarOpen ? hideSearch() : showSearch("basic")
|
searchBarOpen ? hideSearch() : showSearch("basic")
|
||||||
return
|
|
||||||
} else if (e.shiftKey && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
|
} else if (e.shiftKey && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
|
||||||
// Hotkey to open tag search
|
// Hotkey to open tag search
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -217,205 +122,156 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
|
|
||||||
// add "#" prefix for tag search
|
// add "#" prefix for tag search
|
||||||
if (searchBar) searchBar.value = "#"
|
if (searchBar) searchBar.value = "#"
|
||||||
return
|
} else if (e.key === "Enter") {
|
||||||
}
|
|
||||||
|
|
||||||
if (currentHover) {
|
|
||||||
currentHover.classList.remove("focus")
|
|
||||||
}
|
|
||||||
|
|
||||||
// If search is active, then we will render the first result and display accordingly
|
|
||||||
if (!container?.classList.contains("active")) return
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
// If result has focus, navigate to that one, otherwise pick first result
|
// If result has focus, navigate to that one, otherwise pick first result
|
||||||
if (results?.contains(document.activeElement)) {
|
if (results?.contains(document.activeElement)) {
|
||||||
const active = document.activeElement as HTMLInputElement
|
const active = document.activeElement as HTMLInputElement
|
||||||
if (active.classList.contains("no-match")) return
|
|
||||||
await displayPreview(active)
|
|
||||||
active.click()
|
active.click()
|
||||||
} else {
|
} else {
|
||||||
const anchor = document.getElementsByClassName("result-card")[0] as HTMLInputElement | null
|
const anchor = document.getElementsByClassName("result-card")[0] as HTMLInputElement | null
|
||||||
if (!anchor || anchor?.classList.contains("no-match")) return
|
anchor?.click()
|
||||||
await displayPreview(anchor)
|
|
||||||
anchor.click()
|
|
||||||
}
|
}
|
||||||
} else if (e.key === "ArrowUp" || (e.shiftKey && e.key === "Tab")) {
|
} else if (e.key === "ArrowDown") {
|
||||||
|
e.preventDefault()
|
||||||
|
// When first pressing ArrowDown, results wont contain the active element, so focus first element
|
||||||
|
if (!results?.contains(document.activeElement)) {
|
||||||
|
const firstResult = resultCards[0] as HTMLInputElement | null
|
||||||
|
firstResult?.focus()
|
||||||
|
} else {
|
||||||
|
// If an element in results-container already has focus, focus next one
|
||||||
|
const nextResult = document.activeElement?.nextElementSibling as HTMLInputElement | null
|
||||||
|
nextResult?.focus()
|
||||||
|
}
|
||||||
|
} else if (e.key === "ArrowUp") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (results?.contains(document.activeElement)) {
|
if (results?.contains(document.activeElement)) {
|
||||||
// If an element in results-container already has focus, focus previous one
|
// If an element in results-container already has focus, focus previous one
|
||||||
const currentResult = currentHover
|
const prevResult = document.activeElement?.previousElementSibling as HTMLInputElement | null
|
||||||
? currentHover
|
|
||||||
: (document.activeElement as HTMLInputElement | null)
|
|
||||||
const prevResult = currentResult?.previousElementSibling as HTMLInputElement | null
|
|
||||||
currentResult?.classList.remove("focus")
|
|
||||||
prevResult?.focus()
|
prevResult?.focus()
|
||||||
if (prevResult) currentHover = prevResult
|
|
||||||
await displayPreview(prevResult)
|
|
||||||
}
|
|
||||||
} else if (e.key === "ArrowDown" || e.key === "Tab") {
|
|
||||||
e.preventDefault()
|
|
||||||
// The results should already been focused, so we need to find the next one.
|
|
||||||
// The activeElement is the search bar, so we need to find the first result and focus it.
|
|
||||||
if (document.activeElement === searchBar || currentHover !== null) {
|
|
||||||
const firstResult = currentHover
|
|
||||||
? currentHover
|
|
||||||
: (document.getElementsByClassName("result-card")[0] as HTMLInputElement | null)
|
|
||||||
const secondResult = firstResult?.nextElementSibling as HTMLInputElement | null
|
|
||||||
firstResult?.classList.remove("focus")
|
|
||||||
secondResult?.focus()
|
|
||||||
if (secondResult) currentHover = secondResult
|
|
||||||
await displayPreview(secondResult)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trimContent(content: string) {
|
||||||
|
// works without escaping html like in `description.ts`
|
||||||
|
const sentences = content.replace(/\s+/g, " ").split(".")
|
||||||
|
let finalDesc = ""
|
||||||
|
let sentenceIdx = 0
|
||||||
|
|
||||||
|
// Roughly estimate characters by (words * 5). Matches description length in `description.ts`.
|
||||||
|
const len = contextWindowWords * 5
|
||||||
|
while (finalDesc.length < len) {
|
||||||
|
const sentence = sentences[sentenceIdx]
|
||||||
|
if (!sentence) break
|
||||||
|
finalDesc += sentence + "."
|
||||||
|
sentenceIdx++
|
||||||
|
}
|
||||||
|
|
||||||
|
// If more content would be available, indicate it by finishing with "..."
|
||||||
|
if (finalDesc.length < content.length) {
|
||||||
|
finalDesc += ".."
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalDesc
|
||||||
|
}
|
||||||
|
|
||||||
const formatForDisplay = (term: string, id: number) => {
|
const formatForDisplay = (term: string, id: number) => {
|
||||||
const slug = idDataMap[id]
|
const slug = idDataMap[id]
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
slug,
|
slug,
|
||||||
title: searchType === "tags" ? data[slug].title : highlight(term, data[slug].title ?? ""),
|
title: searchType === "tags" ? data[slug].title : highlight(term, data[slug].title ?? ""),
|
||||||
content: highlight(term, data[slug].content ?? "", true),
|
// if searchType is tag, display context from start of file and trim, otherwise use regular highlight
|
||||||
tags: highlightTags(term.substring(1), data[slug].tags),
|
content:
|
||||||
|
searchType === "tags"
|
||||||
|
? trimContent(data[slug].content)
|
||||||
|
: highlight(term, data[slug].content ?? "", true),
|
||||||
|
tags: highlightTags(term, data[slug].tags),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function highlightTags(term: string, tags: string[]) {
|
function highlightTags(term: string, tags: string[]) {
|
||||||
if (!tags || searchType !== "tags") {
|
if (tags && searchType === "tags") {
|
||||||
|
// Find matching tags
|
||||||
|
const termLower = term.toLowerCase()
|
||||||
|
let matching = tags.filter((str) => str.includes(termLower))
|
||||||
|
|
||||||
|
// Substract matching from original tags, then push difference
|
||||||
|
if (matching.length > 0) {
|
||||||
|
let difference = tags.filter((x) => !matching.includes(x))
|
||||||
|
|
||||||
|
// Convert to html (cant be done later as matches/term dont get passed to `resultToHTML`)
|
||||||
|
matching = matching.map((tag) => `<li><p class="match-tag">#${tag}</p></li>`)
|
||||||
|
difference = difference.map((tag) => `<li><p>#${tag}</p></li>`)
|
||||||
|
matching.push(...difference)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow max of `numTagResults` in preview
|
||||||
|
if (tags.length > numTagResults) {
|
||||||
|
matching.splice(numTagResults)
|
||||||
|
}
|
||||||
|
|
||||||
|
return matching
|
||||||
|
} else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
return tags
|
|
||||||
.map((tag) => {
|
|
||||||
if (tag.toLowerCase().includes(term.toLowerCase())) {
|
|
||||||
return `<li><p class="match-tag">#${tag}</p></li>`
|
|
||||||
} else {
|
|
||||||
return `<li><p>#${tag}</p></li>`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.slice(0, numTagResults)
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveUrl(slug: FullSlug): URL {
|
|
||||||
return new URL(resolveRelative(currentSlug, slug), location.toString())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resultToHTML = ({ slug, title, content, tags }: Item) => {
|
const resultToHTML = ({ slug, title, content, tags }: Item) => {
|
||||||
const htmlTags = tags.length > 0 ? `<ul class="tags">${tags.join("")}</ul>` : ``
|
const htmlTags = tags.length > 0 ? `<ul>${tags.join("")}</ul>` : ``
|
||||||
const itemTile = document.createElement("a")
|
const button = document.createElement("button")
|
||||||
itemTile.classList.add("result-card")
|
button.classList.add("result-card")
|
||||||
itemTile.id = slug
|
button.id = slug
|
||||||
itemTile.href = resolveUrl(slug).toString()
|
button.innerHTML = `<h3>${title}</h3>${htmlTags}<p>${content}</p>`
|
||||||
itemTile.innerHTML = `<h3>${title}</h3>${htmlTags}${
|
button.addEventListener("click", () => {
|
||||||
enablePreview && window.innerWidth > 600 ? "" : `<p>${content}</p>`
|
const targ = resolveRelative(currentSlug, slug)
|
||||||
}`
|
window.spaNavigate(new URL(targ, window.location.toString()))
|
||||||
itemTile.addEventListener("click", (event) => {
|
|
||||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
|
|
||||||
hideSearch()
|
hideSearch()
|
||||||
})
|
})
|
||||||
|
return button
|
||||||
const handler = (event: MouseEvent) => {
|
|
||||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
|
|
||||||
hideSearch()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onMouseEnter(ev: MouseEvent) {
|
function displayResults(finalResults: Item[]) {
|
||||||
if (!ev.target) return
|
|
||||||
const target = ev.target as HTMLInputElement
|
|
||||||
await displayPreview(target)
|
|
||||||
}
|
|
||||||
|
|
||||||
itemTile.addEventListener("mouseenter", onMouseEnter)
|
|
||||||
window.addCleanup(() => itemTile.removeEventListener("mouseenter", onMouseEnter))
|
|
||||||
itemTile.addEventListener("click", handler)
|
|
||||||
window.addCleanup(() => itemTile.removeEventListener("click", handler))
|
|
||||||
|
|
||||||
return itemTile
|
|
||||||
}
|
|
||||||
|
|
||||||
async function displayResults(finalResults: Item[]) {
|
|
||||||
if (!results) return
|
if (!results) return
|
||||||
|
|
||||||
removeAllChildren(results)
|
removeAllChildren(results)
|
||||||
if (finalResults.length === 0) {
|
if (finalResults.length === 0) {
|
||||||
results.innerHTML = `<a class="result-card no-match">
|
results.innerHTML = `<button class="result-card">
|
||||||
<h3>No results.</h3>
|
<h3>No results.</h3>
|
||||||
<p>Try another search term?</p>
|
<p>Try another search term?</p>
|
||||||
</a>`
|
</button>`
|
||||||
} else {
|
} else {
|
||||||
results.append(...finalResults.map(resultToHTML))
|
results.append(...finalResults.map(resultToHTML))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (finalResults.length === 0 && preview) {
|
|
||||||
// no results, clear previous preview
|
|
||||||
removeAllChildren(preview)
|
|
||||||
} else {
|
|
||||||
// focus on first result, then also dispatch preview immediately
|
|
||||||
const firstChild = results.firstElementChild as HTMLElement
|
|
||||||
firstChild.classList.add("focus")
|
|
||||||
currentHover = firstChild as HTMLInputElement
|
|
||||||
await displayPreview(firstChild)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchContent(slug: FullSlug): Promise<Element[]> {
|
|
||||||
if (fetchContentCache.has(slug)) {
|
|
||||||
return fetchContentCache.get(slug) as Element[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetUrl = resolveUrl(slug).toString()
|
|
||||||
const contents = await fetch(targetUrl)
|
|
||||||
.then((res) => res.text())
|
|
||||||
.then((contents) => {
|
|
||||||
if (contents === undefined) {
|
|
||||||
throw new Error(`Could not fetch ${targetUrl}`)
|
|
||||||
}
|
|
||||||
const html = p.parseFromString(contents ?? "", "text/html")
|
|
||||||
normalizeRelativeURLs(html, targetUrl)
|
|
||||||
return [...html.getElementsByClassName("popover-hint")]
|
|
||||||
})
|
|
||||||
|
|
||||||
fetchContentCache.set(slug, contents)
|
|
||||||
return contents
|
|
||||||
}
|
|
||||||
|
|
||||||
async function displayPreview(el: HTMLElement | null) {
|
|
||||||
if (!searchLayout || !enablePreview || !el || !preview) return
|
|
||||||
const slug = el.id as FullSlug
|
|
||||||
const innerDiv = await fetchContent(slug).then((contents) =>
|
|
||||||
contents.flatMap((el) => [...highlightHTML(currentSearchTerm, el as HTMLElement).children]),
|
|
||||||
)
|
|
||||||
previewInner = document.createElement("div")
|
|
||||||
previewInner.classList.add("preview-inner")
|
|
||||||
previewInner.append(...innerDiv)
|
|
||||||
preview.replaceChildren(previewInner)
|
|
||||||
|
|
||||||
// scroll to longest
|
|
||||||
const highlights = [...preview.querySelectorAll(".highlight")].sort(
|
|
||||||
(a, b) => b.innerHTML.length - a.innerHTML.length,
|
|
||||||
)
|
|
||||||
highlights[0]?.scrollIntoView({ block: "start" })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onType(e: HTMLElementEventMap["input"]) {
|
async function onType(e: HTMLElementEventMap["input"]) {
|
||||||
if (!searchLayout || !index) return
|
let term = (e.target as HTMLInputElement).value
|
||||||
currentSearchTerm = (e.target as HTMLInputElement).value
|
let searchResults: SimpleDocumentSearchResultSetUnit[]
|
||||||
searchLayout.classList.toggle("display-results", currentSearchTerm !== "")
|
|
||||||
searchType = currentSearchTerm.startsWith("#") ? "tags" : "basic"
|
|
||||||
|
|
||||||
let searchResults: FlexSearch.SimpleDocumentSearchResultSetUnit[]
|
if (term.toLowerCase().startsWith("#")) {
|
||||||
if (searchType === "tags") {
|
searchType = "tags"
|
||||||
searchResults = await index.searchAsync({
|
} else {
|
||||||
query: currentSearchTerm.substring(1),
|
searchType = "basic"
|
||||||
limit: numSearchResults,
|
}
|
||||||
index: ["tags"],
|
|
||||||
})
|
switch (searchType) {
|
||||||
} else if (searchType === "basic") {
|
case "tags": {
|
||||||
searchResults = await index.searchAsync({
|
term = term.substring(1)
|
||||||
query: currentSearchTerm,
|
searchResults =
|
||||||
|
(await index?.searchAsync({ query: term, limit: numSearchResults, index: ["tags"] })) ??
|
||||||
|
[]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "basic":
|
||||||
|
default: {
|
||||||
|
searchResults =
|
||||||
|
(await index?.searchAsync({
|
||||||
|
query: term,
|
||||||
limit: numSearchResults,
|
limit: numSearchResults,
|
||||||
index: ["title", "content"],
|
index: ["title", "content"],
|
||||||
})
|
})) ?? []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getByField = (field: string): number[] => {
|
const getByField = (field: string): number[] => {
|
||||||
@@ -429,19 +285,51 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
...getByField("content"),
|
...getByField("content"),
|
||||||
...getByField("tags"),
|
...getByField("tags"),
|
||||||
])
|
])
|
||||||
const finalResults = [...allIds].map((id) => formatForDisplay(currentSearchTerm, id))
|
const finalResults = [...allIds].map((id) => formatForDisplay(term, id))
|
||||||
await displayResults(finalResults)
|
displayResults(finalResults)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevShortcutHandler) {
|
||||||
|
document.removeEventListener("keydown", prevShortcutHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("keydown", shortcutHandler)
|
document.addEventListener("keydown", shortcutHandler)
|
||||||
window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler))
|
prevShortcutHandler = shortcutHandler
|
||||||
|
searchIcon?.removeEventListener("click", () => showSearch("basic"))
|
||||||
searchIcon?.addEventListener("click", () => showSearch("basic"))
|
searchIcon?.addEventListener("click", () => showSearch("basic"))
|
||||||
window.addCleanup(() => searchIcon?.removeEventListener("click", () => showSearch("basic")))
|
searchBar?.removeEventListener("input", onType)
|
||||||
searchBar?.addEventListener("input", onType)
|
searchBar?.addEventListener("input", onType)
|
||||||
window.addCleanup(() => searchBar?.removeEventListener("input", onType))
|
|
||||||
|
|
||||||
|
// setup index if it hasn't been already
|
||||||
|
if (!index) {
|
||||||
|
index = new Document({
|
||||||
|
charset: "latin:extra",
|
||||||
|
optimize: true,
|
||||||
|
encode: encoder,
|
||||||
|
document: {
|
||||||
|
id: "id",
|
||||||
|
index: [
|
||||||
|
{
|
||||||
|
field: "title",
|
||||||
|
tokenize: "reverse",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "content",
|
||||||
|
tokenize: "reverse",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "tags",
|
||||||
|
tokenize: "reverse",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
fillDocument(index, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// register handlers
|
||||||
registerEscapeHandler(container, hideSearch)
|
registerEscapeHandler(container, hideSearch)
|
||||||
await fillDocument(data)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -449,20 +337,16 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
* @param index index to fill
|
* @param index index to fill
|
||||||
* @param data data to fill index with
|
* @param data data to fill index with
|
||||||
*/
|
*/
|
||||||
async function fillDocument(data: { [key: FullSlug]: ContentDetails }) {
|
async function fillDocument(index: Document<Item, false>, data: any) {
|
||||||
let id = 0
|
let id = 0
|
||||||
const promises: Array<Promise<unknown>> = []
|
|
||||||
for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
|
for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
|
||||||
promises.push(
|
await index.addAsync(id, {
|
||||||
index.addAsync(id++, {
|
|
||||||
id,
|
id,
|
||||||
slug: slug as FullSlug,
|
slug: slug as FullSlug,
|
||||||
title: fileData.title,
|
title: fileData.title,
|
||||||
content: fileData.content,
|
content: fileData.content,
|
||||||
tags: fileData.tags,
|
tags: fileData.tags,
|
||||||
}),
|
})
|
||||||
)
|
id++
|
||||||
}
|
}
|
||||||
|
|
||||||
return await Promise.all(promises)
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user