Merge branch 'v4' into v4
19
.github/workflows/ci.yaml
vendored
@ -7,6 +7,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- v4
|
- v4
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-test:
|
build-and-test:
|
||||||
@ -18,17 +19,17 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 20
|
||||||
|
|
||||||
- name: Cache dependencies
|
- name: Cache dependencies
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.npm
|
path: ~/.npm
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
@ -47,22 +48,22 @@ jobs:
|
|||||||
run: npx quartz build --bundleInfo
|
run: npx quartz build --bundleInfo
|
||||||
|
|
||||||
publish-tag:
|
publish-tag:
|
||||||
if: ${{ github.repository == 'jackyzha0/quartz' }}
|
if: ${{ github.repository == 'jackyzha0/quartz' && github.ref == 'refs/heads/v4' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 20
|
||||||
- name: Get package version
|
- name: Get package version
|
||||||
run: node -p -e '`PACKAGE_VERSION=${require("./package.json").version}`' >> $GITHUB_ENV
|
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: pkgdeps/git-tag-action@v3
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
github_repo: ${{ github.repository }}
|
github_repo: ${{ github.repository }}
|
||||||
|
|||||||
1
.node-version
Normal file
@ -0,0 +1 @@
|
|||||||
|
v20.9.0
|
||||||
@ -129,11 +129,11 @@ export default (() => {
|
|||||||
return <button id="btn">Click me</button>
|
return <button id="btn">Click me</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
YourComponent.beforeDOM = `
|
YourComponent.beforeDOMLoaded = `
|
||||||
console.log("hello from before the page loads!")
|
console.log("hello from before the page loads!")
|
||||||
`
|
`
|
||||||
|
|
||||||
YourComponent.afterDOM = `
|
YourComponent.afterDOMLoaded = `
|
||||||
document.getElementById('btn').onclick = () => {
|
document.getElementById('btn').onclick = () => {
|
||||||
alert('button clicked!')
|
alert('button clicked!')
|
||||||
}
|
}
|
||||||
@ -180,7 +180,7 @@ export default (() => {
|
|||||||
return <button id="btn">Click me</button>
|
return <button id="btn">Click me</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
YourComponent.afterDOM = script
|
YourComponent.afterDOMLoaded = script
|
||||||
return YourComponent
|
return YourComponent
|
||||||
}) satisfies QuartzComponentConstructor
|
}) satisfies QuartzComponentConstructor
|
||||||
```
|
```
|
||||||
|
|||||||
@ -260,11 +260,11 @@ export const ContentPage: QuartzEmitterPlugin = () => {
|
|||||||
...defaultContentPageLayout,
|
...defaultContentPageLayout,
|
||||||
pageBody: Content(),
|
pageBody: Content(),
|
||||||
}
|
}
|
||||||
const { head, header, beforeBody, pageBody, left, right, footer } = layout
|
const { head, header, beforeBody, pageBody, afterBody, left, right, footer } = layout
|
||||||
return {
|
return {
|
||||||
name: "ContentPage",
|
name: "ContentPage",
|
||||||
getQuartzComponents() {
|
getQuartzComponents() {
|
||||||
return [head, ...header, ...beforeBody, pageBody, ...left, ...right, footer]
|
return [head, ...header, ...beforeBody, pageBody, ...afterBody, ...left, ...right, footer]
|
||||||
},
|
},
|
||||||
async emit(ctx, content, resources, emit): Promise<FilePath[]> {
|
async emit(ctx, content, resources, emit): Promise<FilePath[]> {
|
||||||
const cfg = ctx.cfg.configuration
|
const cfg = ctx.cfg.configuration
|
||||||
|
|||||||
@ -48,4 +48,4 @@ Here are the main types of slugs with a rough description of each type of path:
|
|||||||
- `SimpleSlug`: cannot be relative and shouldn't have `/index` as an ending or a file extension. It _can_ however have a trailing slash to indicate a folder path.
|
- `SimpleSlug`: cannot be relative and shouldn't have `/index` as an ending or a file extension. It _can_ however have a trailing slash to indicate a folder path.
|
||||||
- `RelativeURL`: must start with `.` or `..` to indicate it's a relative URL. Shouldn't have `/index` as an ending or a file extension but can contain a trailing slash.
|
- `RelativeURL`: must start with `.` or `..` to indicate it's a relative URL. Shouldn't have `/index` as an ending or a file extension but can contain a trailing slash.
|
||||||
|
|
||||||
To get a clearer picture of how these relate to each other, take a look at the path tests in `quartz/path.test.ts`.
|
To get a clearer picture of how these relate to each other, take a look at the path tests in `quartz/util/path.test.ts`.
|
||||||
|
|||||||
@ -29,6 +29,7 @@ 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.
|
- `description`: Description of the page used for link previews.
|
||||||
|
- `permalink`: A custom URL for the page that will remain constant even if the path to the file changes.
|
||||||
- `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.
|
- `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.
|
||||||
|
|||||||
@ -21,3 +21,7 @@ This will start a local web server to run your Quartz on your computer. Open a w
|
|||||||
> - `--serve`: run a local hot-reloading server to preview your Quartz
|
> - `--serve`: run a local hot-reloading server to preview your Quartz
|
||||||
> - `--port`: what port to run the local preview server on
|
> - `--port`: what port to run the local preview server on
|
||||||
> - `--concurrency`: how many threads to use to parse notes
|
> - `--concurrency`: how many threads to use to parse notes
|
||||||
|
|
||||||
|
> [!warning] Not to be used for production
|
||||||
|
> Serve mode is intended for local previews only.
|
||||||
|
> For production workloads, see the page on [[hosting]].
|
||||||
|
|||||||
@ -21,6 +21,7 @@ const config: QuartzConfig = {
|
|||||||
This part of the configuration concerns anything that can affect the whole site. The following is a list breaking down all the things you can configure:
|
This part of the configuration concerns anything that can affect the whole site. The following is a list breaking down all the things you can configure:
|
||||||
|
|
||||||
- `pageTitle`: title of the site. This is also used when generating the [[RSS Feed]] for your site.
|
- `pageTitle`: title of the site. This is also used when generating the [[RSS Feed]] for your site.
|
||||||
|
- `pageTitleSuffix`: a string added to the end of the page title. This only applies to the browser tab title, not the title shown at the top of the page.
|
||||||
- `enableSPA`: whether to enable [[SPA Routing]] on your site.
|
- `enableSPA`: whether to enable [[SPA Routing]] on your site.
|
||||||
- `enablePopovers`: whether to enable [[popover previews]] on your site.
|
- `enablePopovers`: whether to enable [[popover previews]] on your site.
|
||||||
- `analytics`: what to use for analytics on your site. Values can be
|
- `analytics`: what to use for analytics on your site. Values can be
|
||||||
@ -28,8 +29,10 @@ This part of the configuration concerns anything that can affect the whole site.
|
|||||||
- `{ provider: 'google', tagId: '<your-google-tag>' }`: use Google Analytics;
|
- `{ provider: 'google', tagId: '<your-google-tag>' }`: use Google Analytics;
|
||||||
- `{ provider: 'plausible' }` (managed) or `{ provider: 'plausible', host: '<your-plausible-host>' }` (self-hosted): use [Plausible](https://plausible.io/);
|
- `{ provider: 'plausible' }` (managed) or `{ provider: 'plausible', host: '<your-plausible-host>' }` (self-hosted): use [Plausible](https://plausible.io/);
|
||||||
- `{ provider: 'umami', host: '<your-umami-host>', websiteId: '<your-umami-website-id>' }`: use [Umami](https://umami.is/);
|
- `{ provider: 'umami', host: '<your-umami-host>', websiteId: '<your-umami-website-id>' }`: use [Umami](https://umami.is/);
|
||||||
- `{ provider: 'goatcounter', websiteId: 'my-goatcounter-id' }` (managed) or `{ provider: 'goatcounter', websiteId: 'my-goatcounter-id', host: 'my-goatcounter-domain.com', scriptSrc: 'https://my-url.to/counter.js' }` (self-hosted) use [GoatCounter](https://goatcounter.com)
|
- `{ provider: 'goatcounter', websiteId: 'my-goatcounter-id' }` (managed) or `{ provider: 'goatcounter', websiteId: 'my-goatcounter-id', host: 'my-goatcounter-domain.com', scriptSrc: 'https://my-url.to/counter.js' }` (self-hosted) use [GoatCounter](https://goatcounter.com);
|
||||||
- `{ provider: 'posthog', apiKey: '<your-posthog-project-apiKey>', host: '<your-posthog-host>' }`: use [Posthog](https://posthog.com/);
|
- `{ provider: 'posthog', apiKey: '<your-posthog-project-apiKey>', host: '<your-posthog-host>' }`: use [Posthog](https://posthog.com/);
|
||||||
|
- `{ provider: 'tinylytics', siteId: '<your-site-id>' }`: use [Tinylytics](https://tinylytics.app/);
|
||||||
|
- `{ provider: 'cabin' }` or `{ provider: 'cabin', host: 'https://cabin.example.com' }` (custom domain): use [Cabin](https://withcabin.com);
|
||||||
- `locale`: used for [[i18n]] and date formatting
|
- `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`.
|
||||||
@ -51,6 +54,7 @@ This part of the configuration concerns anything that can affect the whole site.
|
|||||||
- `secondary`: link colour, current [[graph view|graph]] node
|
- `secondary`: link colour, current [[graph view|graph]] node
|
||||||
- `tertiary`: hover states and visited [[graph view|graph]] nodes
|
- `tertiary`: hover states and visited [[graph view|graph]] nodes
|
||||||
- `highlight`: internal link background, highlighted text, [[syntax highlighting|highlighted lines of code]]
|
- `highlight`: internal link background, highlighted text, [[syntax highlighting|highlighted lines of code]]
|
||||||
|
- `textHighlight`: markdown highlighted text background
|
||||||
|
|
||||||
## Plugins
|
## Plugins
|
||||||
|
|
||||||
|
|||||||
28
docs/features/Roam Research compatibility.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
title: "Roam Research Compatibility"
|
||||||
|
tags:
|
||||||
|
- feature/transformer
|
||||||
|
---
|
||||||
|
|
||||||
|
[Roam Research](https://roamresearch.com) is a note-taking tool that organizes your knowledge graph in a unique and interconnected way.
|
||||||
|
|
||||||
|
Quartz supports transforming the special Markdown syntax from Roam Research (like `{{[[components]]}}` and other formatting) into
|
||||||
|
regular Markdown via the [[RoamFlavoredMarkdown]] plugin.
|
||||||
|
|
||||||
|
```typescript title="quartz.config.ts"
|
||||||
|
plugins: {
|
||||||
|
transformers: [
|
||||||
|
// ...
|
||||||
|
Plugin.RoamFlavoredMarkdown(),
|
||||||
|
Plugin.ObsidianFlavoredMarkdown(),
|
||||||
|
// ...
|
||||||
|
],
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!warning]
|
||||||
|
> As seen above placement of `Plugin.RoamFlavoredMarkdown()` within `quartz.config.ts` is very important. It must come before `Plugin.ObsidianFlavoredMarkdown()`.
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
This functionality is provided by the [[RoamFlavoredMarkdown]] plugin. See the plugin page for customization options.
|
||||||
83
docs/features/comments.md
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
---
|
||||||
|
title: Comments
|
||||||
|
tags:
|
||||||
|
- component
|
||||||
|
---
|
||||||
|
|
||||||
|
Quartz also has the ability to hook into various providers to enable readers to leave comments on your site.
|
||||||
|
|
||||||
|
![[giscus-example.png]]
|
||||||
|
|
||||||
|
As of today, only [Giscus](https://giscus.app/) is supported out of the box but PRs to support other providers are welcome!
|
||||||
|
|
||||||
|
## Providers
|
||||||
|
|
||||||
|
### Giscus
|
||||||
|
|
||||||
|
First, make sure that the [[setting up your GitHub repository|GitHub]] repository you are using for your Quartz meets the following requirements:
|
||||||
|
|
||||||
|
1. The **repository is [public](https://docs.github.com/en/github/administering-a-repository/managing-repository-settings/setting-repository-visibility#making-a-repository-public)**, otherwise visitors will not be able to view the discussion.
|
||||||
|
2. The **[giscus](https://github.com/apps/giscus) app is installed**, otherwise visitors will not be able to comment and react.
|
||||||
|
3. The **Discussions feature is turned on** by [enabling it for your repository](https://docs.github.com/en/github/administering-a-repository/managing-repository-settings/enabling-or-disabling-github-discussions-for-a-repository).
|
||||||
|
|
||||||
|
Then, use the [Giscus site](https://giscus.app/#repository) to figure out what your `repoId` and `categoryId` should be. Make sure you select `Announcements` for the Discussion category.
|
||||||
|
|
||||||
|
![[giscus-repo.png]]
|
||||||
|
|
||||||
|
![[giscus-discussion.png]]
|
||||||
|
|
||||||
|
After entering both your repository and selecting the discussion category, Giscus will compute some IDs that you'll need to provide back to Quartz. You won't need to manually add the script yourself as Quartz will handle that part for you but will need these values in the next step!
|
||||||
|
|
||||||
|
![[giscus-results.png]]
|
||||||
|
|
||||||
|
Finally, in `quartz.layout.ts`, edit the `afterBody` field of `sharedPageComponents` to include the following options but with the values you got from above:
|
||||||
|
|
||||||
|
```ts title="quartz.layout.ts"
|
||||||
|
afterBody: [
|
||||||
|
Component.Comments({
|
||||||
|
provider: 'giscus',
|
||||||
|
options: {
|
||||||
|
// from data-repo
|
||||||
|
repo: 'jackyzha0/quartz',
|
||||||
|
// from data-repo-id
|
||||||
|
repoId: 'MDEwOlJlcG9zaXRvcnkzODcyMTMyMDg',
|
||||||
|
// from data-category
|
||||||
|
category: 'Announcements',
|
||||||
|
// from data-category-id
|
||||||
|
categoryId: 'DIC_kwDOFxRnmM4B-Xg6',
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customization
|
||||||
|
|
||||||
|
Quartz also exposes a few of the other Giscus options as well and you can provide them the same way `repo`, `repoId`, `category`, and `categoryId` are provided.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type Options = {
|
||||||
|
provider: "giscus"
|
||||||
|
options: {
|
||||||
|
repo: `${string}/${string}`
|
||||||
|
repoId: string
|
||||||
|
category: string
|
||||||
|
categoryId: string
|
||||||
|
|
||||||
|
// how to map pages -> discussions
|
||||||
|
// defaults to 'url'
|
||||||
|
mapping?: "url" | "title" | "og:title" | "specific" | "number" | "pathname"
|
||||||
|
|
||||||
|
// use strict title matching
|
||||||
|
// defaults to true
|
||||||
|
strict?: boolean
|
||||||
|
|
||||||
|
// whether to enable reactions for the main post
|
||||||
|
// defaults to true
|
||||||
|
reactionsEnabled?: boolean
|
||||||
|
|
||||||
|
// where to put the comment input box relative to the comments
|
||||||
|
// defaults to 'bottom'
|
||||||
|
inputPosition?: "top" | "bottom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@ -30,4 +30,4 @@ As with folder listings, you can also provide a description and title for a tag
|
|||||||
|
|
||||||
## 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.
|
Quartz allows you to define a custom sort ordering for content on both page types. The folder listings are a functionality of the [[FolderPage]] plugin, the tag listings of the [[TagPage]] plugin. See the plugin pages for customization options.
|
||||||
|
|||||||
@ -9,6 +9,7 @@ Quartz can generate a list of recent notes based on some filtering and sorting c
|
|||||||
|
|
||||||
- Changing the title from "Recent notes": pass in an additional parameter to `Component.RecentNotes({ title: "Recent writing" })`
|
- Changing the title from "Recent notes": pass in an additional parameter to `Component.RecentNotes({ title: "Recent writing" })`
|
||||||
- Changing the number of recent notes: pass in an additional parameter to `Component.RecentNotes({ limit: 5 })`
|
- Changing the number of recent notes: pass in an additional parameter to `Component.RecentNotes({ limit: 5 })`
|
||||||
|
- Display the note's tags (defaults to true): `Component.RecentNotes({ showTags: false })`
|
||||||
- Show a 'see more' link: pass in an additional parameter to `Component.RecentNotes({ linkToMore: "tags/components" })`. This field should be a full slug to a page that exists.
|
- Show a 'see more' link: pass in an additional parameter to `Component.RecentNotes({ linkToMore: "tags/components" })`. This field should be a full slug to a page that exists.
|
||||||
- Customize filtering: pass in an additional parameter to `Component.RecentNotes({ filter: someFilterFunction })`. The filter function should be a function that has the signature `(f: QuartzPluginData) => boolean`.
|
- Customize filtering: pass in an additional parameter to `Component.RecentNotes({ filter: someFilterFunction })`. The filter function should be a function that has the signature `(f: QuartzPluginData) => boolean`.
|
||||||
- Customize sorting: pass in an additional parameter to `Component.RecentNotes({ sort: someSortFunction })`. By default, Quartz will sort by date and then tie break lexographically. The sort function should be a function that has the signature `(f1: QuartzPluginData, f2: QuartzPluginData) => number`. See `byDateAndAlphabetical` in `quartz/components/PageList.tsx` for an example.
|
- Customize sorting: pass in an additional parameter to `Component.RecentNotes({ sort: someSortFunction })`. By default, Quartz will sort by date and then tie break lexographically. The sort function should be a function that has the signature `(f1: QuartzPluginData, f2: QuartzPluginData) => number`. See `byDateAndAlphabetical` in `quartz/components/PageList.tsx` for an example.
|
||||||
|
|||||||
@ -95,6 +95,16 @@ const [age, setAge] = useState(50)
|
|||||||
const [name, setName] = useState("Taylor")
|
const [name, setName] = useState("Taylor")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Inline Highlighting
|
||||||
|
|
||||||
|
Append {:lang} to the end of inline code to highlight it like a regular code block.
|
||||||
|
|
||||||
|
```
|
||||||
|
This is an array `[1, 2, 3]{:js}` of numbers 1 through 3.
|
||||||
|
```
|
||||||
|
|
||||||
|
This is an array `[1, 2, 3]{:js}` of numbers 1 through 3.
|
||||||
|
|
||||||
### Line numbers
|
### Line numbers
|
||||||
|
|
||||||
Syntax highlighting has line numbers configured automatically. If you want to start line numbers at a specific number, use `showLineNumbers{number}`:
|
Syntax highlighting has line numbers configured automatically. If you want to start line numbers at a specific number, use `showLineNumbers{number}`:
|
||||||
|
|||||||
@ -2,22 +2,11 @@
|
|||||||
draft: true
|
draft: true
|
||||||
---
|
---
|
||||||
|
|
||||||
## high priority backlog
|
|
||||||
|
|
||||||
- static dead link detection
|
|
||||||
- block links: https://help.obsidian.md/Linking+notes+and+files/Internal+links#Link+to+a+block+in+a+note
|
|
||||||
- note/header/block transcludes: https://help.obsidian.md/Linking+notes+and+files/Embedding+files
|
|
||||||
- docker support
|
|
||||||
|
|
||||||
## misc backlog
|
## misc backlog
|
||||||
|
|
||||||
- breadcrumbs component
|
- static dead link detection
|
||||||
- cursor chat extension
|
- cursor chat extension
|
||||||
- https://giscus.app/ extension
|
|
||||||
- sidenotes? https://github.com/capnfabs/paperesque
|
- sidenotes? https://github.com/capnfabs/paperesque
|
||||||
- direct match in search using double quotes
|
- direct match in search using double quotes
|
||||||
- https://help.obsidian.md/Advanced+topics/Using+Obsidian+URI
|
- https://help.obsidian.md/Advanced+topics/Using+Obsidian+URI
|
||||||
- audio/video embed styling
|
|
||||||
- Canvas
|
- Canvas
|
||||||
- parse all images in page: use this for page lists if applicable?
|
|
||||||
- CV mode? with print stylesheet
|
|
||||||
|
|||||||
@ -57,18 +57,18 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Fetch all history for git info
|
fetch-depth: 0 # Fetch all history for git info
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.14
|
node-version: 22
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Build Quartz
|
- name: Build Quartz
|
||||||
run: npx quartz build
|
run: npx quartz build
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v2
|
uses: actions/upload-pages-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: public
|
path: public
|
||||||
|
|
||||||
@ -81,7 +81,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
uses: actions/deploy-pages@v2
|
uses: actions/deploy-pages@v4
|
||||||
```
|
```
|
||||||
|
|
||||||
Then:
|
Then:
|
||||||
@ -182,37 +182,33 @@ Using `docs.example.com` is an example of a subdomain. They're a simple way of c
|
|||||||
|
|
||||||
## GitLab Pages
|
## GitLab Pages
|
||||||
|
|
||||||
In your local Quartz, create a new file `.gitlab-ci.yaml`.
|
In your local Quartz, create a new file `.gitlab-ci.yml`.
|
||||||
|
|
||||||
```yaml title=".gitlab-ci.yaml"
|
```yaml title=".gitlab-ci.yml"
|
||||||
stages:
|
stages:
|
||||||
- build
|
- build
|
||||||
- deploy
|
- deploy
|
||||||
|
|
||||||
variables:
|
image: node:20
|
||||||
NODE_VERSION: "18.14"
|
cache: # Cache modules in between jobs
|
||||||
|
key: $CI_COMMIT_REF_SLUG
|
||||||
|
paths:
|
||||||
|
- .npm/
|
||||||
|
|
||||||
build:
|
build:
|
||||||
stage: build
|
stage: build
|
||||||
rules:
|
rules:
|
||||||
- if: '$CI_COMMIT_REF_NAME == "v4"'
|
- if: '$CI_COMMIT_REF_NAME == "v4"'
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get update -q && apt-get install -y nodejs npm
|
|
||||||
- npm install -g n
|
|
||||||
- n $NODE_VERSION
|
|
||||||
- hash -r
|
- hash -r
|
||||||
- npm ci
|
- npm ci --cache .npm --prefer-offline
|
||||||
script:
|
script:
|
||||||
- npx quartz build
|
- npx quartz build
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- public
|
- public
|
||||||
cache:
|
|
||||||
paths:
|
|
||||||
- ~/.npm/
|
|
||||||
key: "${CI_COMMIT_REF_SLUG}-node-${CI_COMMIT_REF_NAME}"
|
|
||||||
tags:
|
tags:
|
||||||
- docker
|
- gitlab-org-docker
|
||||||
|
|
||||||
pages:
|
pages:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
|
|||||||
BIN
docs/images/giscus-discussion.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
docs/images/giscus-example.png
Normal file
|
After Width: | Height: | Size: 572 KiB |
BIN
docs/images/giscus-repo.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
docs/images/giscus-results.png
Normal file
|
After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 55 KiB |
BIN
docs/images/quartz-layout-desktop.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
docs/images/quartz-layout-mobile.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
docs/images/quartz-layout-tablet.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
@ -6,7 +6,7 @@ Quartz is a fast, batteries-included static-site generator that transforms Markd
|
|||||||
|
|
||||||
## 🪴 Get Started
|
## 🪴 Get Started
|
||||||
|
|
||||||
Quartz requires **at least [Node](https://nodejs.org/) v18.14** and `npm` v9.3.1 to function correctly. Ensure you have this installed on your machine before continuing.
|
Quartz requires **at least [Node](https://nodejs.org/) v20** and `npm` v9.3.1 to function correctly. Ensure you have this installed on your machine before continuing.
|
||||||
|
|
||||||
Then, in your terminal of choice, enter the following commands line by line:
|
Then, in your terminal of choice, enter the following commands line by line:
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ If you prefer instructions in a video format you can try following Nicole van de
|
|||||||
|
|
||||||
## 🔧 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]], [[features/Latex|Latex]], [[syntax highlighting]], [[popover previews]], [[Docker Support]], [[i18n|internationalization]], [[comments]] 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
|
||||||
|
|||||||
@ -12,15 +12,20 @@ export interface FullPageLayout {
|
|||||||
header: QuartzComponent[] // laid out horizontally
|
header: QuartzComponent[] // laid out horizontally
|
||||||
beforeBody: QuartzComponent[] // laid out vertically
|
beforeBody: QuartzComponent[] // laid out vertically
|
||||||
pageBody: QuartzComponent // single component
|
pageBody: QuartzComponent // single component
|
||||||
left: QuartzComponent[] // vertical on desktop, horizontal on mobile
|
afterBody: QuartzComponent[] // laid out vertically
|
||||||
right: QuartzComponent[] // vertical on desktop, horizontal on mobile
|
left: QuartzComponent[] // vertical on desktop and tablet, horizontal on mobile
|
||||||
|
right: QuartzComponent[] // vertical on desktop, horizontal on tablet and mobile
|
||||||
footer: QuartzComponent // single component
|
footer: QuartzComponent // single component
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
These correspond to following parts of the page:
|
These correspond to following parts of the page:
|
||||||
|
|
||||||
![[quartz layout.png|800]]
|
| Layout | Preview |
|
||||||
|
| ------------------------------- | ----------------------------------- |
|
||||||
|
| Desktop (width > 1200px) | ![[quartz-layout-desktop.png\|800]] |
|
||||||
|
| Tablet (800px < width < 1200px) | ![[quartz-layout-tablet.png\|800]] |
|
||||||
|
| Mobile (width < 800px) | ![[quartz-layout-mobile.png\|800]] |
|
||||||
|
|
||||||
> [!note]
|
> [!note]
|
||||||
> There are two additional layout fields that are _not_ shown in the above diagram.
|
> There are two additional layout fields that are _not_ shown in the above diagram.
|
||||||
@ -32,6 +37,23 @@ Quartz **components**, like plugins, can take in additional properties as config
|
|||||||
|
|
||||||
See [a list of all the components](component.md) for all available components along with their configuration options. You can also checkout the guide on [[creating components]] if you're interested in further customizing the behaviour of Quartz.
|
See [a list of all the components](component.md) for all available components along with their configuration options. You can also checkout the guide on [[creating components]] if you're interested in further customizing the behaviour of Quartz.
|
||||||
|
|
||||||
|
### Layout breakpoints
|
||||||
|
|
||||||
|
Quartz has different layouts depending on the width the screen viewing the website.
|
||||||
|
|
||||||
|
The breakpoints for layouts can be configured in `variables.scss`.
|
||||||
|
|
||||||
|
- `mobile`: screen width below this size will use mobile layout.
|
||||||
|
- `desktop`: screen width above this size will use desktop layout.
|
||||||
|
- Screen width between `mobile` and `desktop` width will use the tablet layout.
|
||||||
|
|
||||||
|
```scss
|
||||||
|
$breakpoints: (
|
||||||
|
mobile: 800px,
|
||||||
|
desktop: 1200px,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
### Style
|
### Style
|
||||||
|
|
||||||
Most meaningful style changes like colour scheme and font can be done simply through the [[configuration#General Configuration|general configuration]] options. However, if you'd like to make more involved style changes, you can do this by writing your own styles. Quartz 4, like Quartz 3, uses [Sass](https://sass-lang.com/guide/) for styling.
|
Most meaningful style changes like colour scheme and font can be done simply through the [[configuration#General Configuration|general configuration]] options. However, if you'd like to make more involved style changes, you can do this by writing your own styles. Quartz 4, like Quartz 3, uses [Sass](https://sass-lang.com/guide/) for styling.
|
||||||
|
|||||||
@ -11,7 +11,7 @@ This plugin determines the created, modified, and published dates for a document
|
|||||||
|
|
||||||
This plugin accepts the following configuration options:
|
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"]`.
|
- `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]
|
> [!warning]
|
||||||
> If you rely on `git` for dates, make sure `defaultDateType` is set to `modified` in `quartz.config.ts`.
|
> If you rely on `git` for dates, make sure `defaultDateType` is set to `modified` in `quartz.config.ts`.
|
||||||
|
|||||||
@ -11,10 +11,12 @@ Example: [[advanced/|Advanced]]
|
|||||||
> [!note]
|
> [!note]
|
||||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
> 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`).
|
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`).
|
||||||
|
|
||||||
|
This plugin accepts the following configuration options:
|
||||||
|
|
||||||
|
- `sort`: A function of type `(f1: QuartzPluginData, f2: QuartzPluginData) => number{:ts}` used to sort entries. Defaults to sorting by date and tie-breaking on lexographical order.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
- Category: Emitter
|
- Category: Emitter
|
||||||
|
|||||||
@ -12,6 +12,7 @@ This plugin adds LaTeX support to Quartz. See [[features/Latex|Latex]] for more
|
|||||||
This plugin accepts the following configuration options:
|
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.
|
- `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.
|
||||||
|
- `customMacros`: custom macros for all LaTeX blocks. It takes the form of a key-value pair where the key is a new command name and the value is the expansion of the macro. For example: `{"\\R": "\\mathbb{R}"}`
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
|
|||||||
26
docs/plugins/RoamFlavoredMarkdown.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
title: RoamFlavoredMarkdown
|
||||||
|
tags:
|
||||||
|
- plugin/transformer
|
||||||
|
---
|
||||||
|
|
||||||
|
This plugin provides support for [Roam Research](https://roamresearch.com) compatibility. See [[Roam Research 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:
|
||||||
|
|
||||||
|
- `orComponent`: If `true` (default), converts Roam `{{ or:ONE|TWO|THREE }}` shortcodes into HTML Dropdown options.
|
||||||
|
- `TODOComponent`: If `true` (default), converts Roam `{{[[TODO]]}}` shortcodes into HTML check boxes.
|
||||||
|
- `DONEComponent`: If `true` (default), converts Roam `{{[[DONE]]}}` shortcodes into checked HTML check boxes.
|
||||||
|
- `videoComponent`: If `true` (default), converts Roam `{{[[video]]:URL}}` shortcodes into embeded HTML video.
|
||||||
|
- `audioComponent`: If `true` (default), converts Roam `{{[[audio]]:URL}}` shortcodes into embeded HTML audio.
|
||||||
|
- `pdfComponent`: If `true` (default), converts Roam `{{[[pdf]]:URL}}` shortcodes into embeded HTML PDF viewer.
|
||||||
|
- `blockquoteComponent`: If `true` (default), converts Roam `{{[[>]]}}` shortcodes into Quartz blockquotes.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- Category: Transformer
|
||||||
|
- Function name: `Plugin.RoamFlavoredMarkdown()`.
|
||||||
|
- Source: [`quartz/plugins/transformers/roam.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/roam.ts).
|
||||||
@ -9,10 +9,12 @@ This plugin emits dedicated pages for each tag used in the content. See [[folder
|
|||||||
> [!note]
|
> [!note]
|
||||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
> 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`).
|
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`).
|
||||||
|
|
||||||
|
This plugin accepts the following configuration options:
|
||||||
|
|
||||||
|
- `sort`: A function of type `(f1: QuartzPluginData, f2: QuartzPluginData) => number{:ts}` used to sort entries. Defaults to sorting by date and tie-breaking on lexographical order.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
- Category: Emitter
|
- Category: Emitter
|
||||||
|
|||||||
@ -7,25 +7,26 @@ 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/)
|
||||||
- [oldwinter の数字花园](https://garden.oldwinter.top/)
|
- [Morrowind Modding Wiki](https://morrowind-modding.github.io/)
|
||||||
- [Aaron Pham's Garden](https://aarnphm.xyz/)
|
- [Aaron Pham's Garden](https://aarnphm.xyz/)
|
||||||
- [The Quantum Garden](https://quantumgardener.blog/)
|
- [Pelayo Arbues' Notes](https://pelayoarbues.com/)
|
||||||
- [Abhijeet's Math Wiki](https://abhmul.github.io/quartz/Math-Wiki/)
|
- [Stanford CME 302 Numerical Linear Algebra](https://ericdarve.github.io/NLA/)
|
||||||
- [Matt Dunn's Second Brain](https://mattdunn.info/)
|
- [A Pattern Language - Christopher Alexander (Architecture)](https://patternlanguage.cc/)
|
||||||
- [Pelayo Arbues' Notes](https://pelayoarbues.github.io/)
|
- [oldwinter の数字花园](https://garden.oldwinter.top/)
|
||||||
- [Vince Imbat's Talahardin](https://vinceimbat.com/)
|
- [Eilleen's Everything Notebook](https://quartz.eilleeenz.com/)
|
||||||
- [🧠🌳 Chad's Mind Garden](https://www.chadly.net/)
|
- [🧠🌳 Chad's Mind Garden](https://www.chadly.net/)
|
||||||
- [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/)
|
|
||||||
- [🌊 Collapsed Wave](https://collapsedwave.com/)
|
|
||||||
- [Sideny's 3D Artist's Handbook](https://sidney-eliot.github.io/3d-artists-handbook/)
|
- [Sideny's 3D Artist's Handbook](https://sidney-eliot.github.io/3d-artists-handbook/)
|
||||||
- [Mike's AI Garden 🤖🪴](https://mwalton.me/)
|
|
||||||
- [Brandon Boswell's Garden](https://brandonkboswell.com)
|
- [Brandon Boswell's Garden](https://brandonkboswell.com)
|
||||||
- [Scaling Synthesis - A hypertext research notebook](https://scalingsynthesis.com/)
|
- [Scaling Synthesis - A hypertext research notebook](https://scalingsynthesis.com/)
|
||||||
- [Data Dictionary 🧠](https://glossary.airbyte.com/)
|
- [Data Dictionary 🧠](https://glossary.airbyte.com/)
|
||||||
- [sspaeti.com's Second Brain](https://brain.sspaeti.com/)
|
- [sspaeti.com's Second Brain](https://brain.sspaeti.com/)
|
||||||
- [🪴Aster's notebook](https://notes.asterhu.com)
|
- [🪴Aster's notebook](https://notes.asterhu.com)
|
||||||
|
- [Gatekeeper Wiki](https://www.gatekeeper.wiki)
|
||||||
|
- [Ellie's Notes](https://ellie.wtf)
|
||||||
- [🥷🏻🌳🍃 Computer Science & Thinkering Garden](https://notes.yxy.ninja)
|
- [🥷🏻🌳🍃 Computer Science & Thinkering Garden](https://notes.yxy.ninja)
|
||||||
|
- [Eledah's Crystalline](https://blog.eledah.ir/)
|
||||||
|
- [🌓 Projects & Privacy - FOSS, tech, law](https://be-far.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)!
|
||||||
|
|||||||
4
globals.d.ts
vendored
@ -4,6 +4,10 @@ export declare global {
|
|||||||
type: K,
|
type: K,
|
||||||
listener: (this: Document, ev: CustomEventMap[K]) => void,
|
listener: (this: Document, ev: CustomEventMap[K]) => void,
|
||||||
): void
|
): void
|
||||||
|
removeEventListener<K extends keyof CustomEventMap>(
|
||||||
|
type: K,
|
||||||
|
listener: (this: Document, ev: CustomEventMap[K]) => void,
|
||||||
|
): void
|
||||||
dispatchEvent<K extends keyof CustomEventMap>(ev: CustomEventMap[K] | UIEvent): void
|
dispatchEvent<K extends keyof CustomEventMap>(ev: CustomEventMap[K] | UIEvent): void
|
||||||
}
|
}
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|||||||
1509
package-lock.json
generated
58
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.4.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -21,7 +21,7 @@
|
|||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": ">=9.3.1",
|
"npm": ">=9.3.1",
|
||||||
"node": ">=18.14"
|
"node": "20 || >=22"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"site generator",
|
"site generator",
|
||||||
@ -36,8 +36,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.7.0",
|
"@clack/prompts": "^0.7.0",
|
||||||
"@floating-ui/dom": "^1.6.3",
|
"@floating-ui/dom": "^1.6.10",
|
||||||
"@napi-rs/simple-git": "0.1.16",
|
"@napi-rs/simple-git": "0.1.19",
|
||||||
|
"@tweenjs/tween.js": "^25.0.0",
|
||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"chokidar": "^3.6.0",
|
"chokidar": "^3.6.0",
|
||||||
@ -46,28 +47,29 @@
|
|||||||
"esbuild-sass-plugin": "^2.16.1",
|
"esbuild-sass-plugin": "^2.16.1",
|
||||||
"flexsearch": "0.7.43",
|
"flexsearch": "0.7.43",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"globby": "^14.0.1",
|
"globby": "^14.0.2",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"hast-util-to-html": "^9.0.0",
|
"hast-util-to-html": "^9.0.3",
|
||||||
"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.24.1",
|
"lightningcss": "^1.26.0",
|
||||||
"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.2.0",
|
||||||
"mdast-util-to-string": "^4.0.0",
|
"mdast-util-to-string": "^4.0.0",
|
||||||
"micromorph": "^0.4.5",
|
"micromorph": "^0.4.5",
|
||||||
"preact": "^10.20.1",
|
"pixi.js": "^8.4.1",
|
||||||
"preact-render-to-string": "^6.4.2",
|
"preact": "^10.24.0",
|
||||||
|
"preact-render-to-string": "^6.5.11",
|
||||||
"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-citation": "^2.0.0",
|
"rehype-citation": "^2.1.1",
|
||||||
"rehype-katex": "^7.0.0",
|
"rehype-katex": "^7.0.1",
|
||||||
"rehype-mathjax": "^6.0.0",
|
"rehype-mathjax": "^6.0.0",
|
||||||
"rehype-pretty-code": "^0.13.0",
|
"rehype-pretty-code": "^0.14.0",
|
||||||
"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",
|
||||||
@ -76,20 +78,20 @@
|
|||||||
"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.1.1",
|
||||||
"remark-smartypants": "^2.1.0",
|
"remark-smartypants": "^3.0.2",
|
||||||
"rfdc": "^1.3.1",
|
"rfdc": "^1.4.1",
|
||||||
"rimraf": "^5.0.5",
|
"rimraf": "^6.0.1",
|
||||||
"serve-handler": "^6.1.5",
|
"serve-handler": "^6.1.5",
|
||||||
"shiki": "^1.2.3",
|
"shiki": "^1.18.0",
|
||||||
"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.5",
|
||||||
"unist-util-visit": "^5.0.0",
|
"unist-util-visit": "^5.0.0",
|
||||||
"vfile": "^6.0.1",
|
"vfile": "^6.0.3",
|
||||||
"workerpool": "^9.1.1",
|
"workerpool": "^9.1.3",
|
||||||
"ws": "^8.15.1",
|
"ws": "^8.18.0",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -97,14 +99,14 @@
|
|||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
"@types/hast": "^3.0.4",
|
"@types/hast": "^3.0.4",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/node": "^20.12.5",
|
"@types/node": "^22.5.5",
|
||||||
"@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/ws": "^8.5.10",
|
"@types/ws": "^8.5.12",
|
||||||
"@types/yargs": "^17.0.32",
|
"@types/yargs": "^17.0.33",
|
||||||
"esbuild": "^0.19.9",
|
"esbuild": "^0.19.9",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.3.3",
|
||||||
"tsx": "^4.7.1",
|
"tsx": "^4.19.0",
|
||||||
"typescript": "^5.4.3"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,6 +36,7 @@ const config: QuartzConfig = {
|
|||||||
secondary: "#284b63",
|
secondary: "#284b63",
|
||||||
tertiary: "#84a59d",
|
tertiary: "#84a59d",
|
||||||
highlight: "rgba(143, 159, 169, 0.15)",
|
highlight: "rgba(143, 159, 169, 0.15)",
|
||||||
|
textHighlight: "#fff23688",
|
||||||
},
|
},
|
||||||
darkMode: {
|
darkMode: {
|
||||||
light: "#161618",
|
light: "#161618",
|
||||||
@ -46,6 +47,7 @@ const config: QuartzConfig = {
|
|||||||
secondary: "#7b97aa",
|
secondary: "#7b97aa",
|
||||||
tertiary: "#84a59d",
|
tertiary: "#84a59d",
|
||||||
highlight: "rgba(143, 159, 169, 0.15)",
|
highlight: "rgba(143, 159, 169, 0.15)",
|
||||||
|
textHighlight: "#b3aa0288",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -56,7 +58,6 @@ const config: QuartzConfig = {
|
|||||||
Plugin.CreatedModifiedDate({
|
Plugin.CreatedModifiedDate({
|
||||||
priority: ["frontmatter", "filesystem"],
|
priority: ["frontmatter", "filesystem"],
|
||||||
}),
|
}),
|
||||||
Plugin.Latex({ renderEngine: "katex" }),
|
|
||||||
Plugin.SyntaxHighlighting({
|
Plugin.SyntaxHighlighting({
|
||||||
theme: {
|
theme: {
|
||||||
light: "github-light",
|
light: "github-light",
|
||||||
@ -69,6 +70,7 @@ const config: QuartzConfig = {
|
|||||||
Plugin.TableOfContents(),
|
Plugin.TableOfContents(),
|
||||||
Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
|
Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
|
||||||
Plugin.Description(),
|
Plugin.Description(),
|
||||||
|
Plugin.Latex({ renderEngine: "katex" }),
|
||||||
],
|
],
|
||||||
filters: [Plugin.RemoveDrafts()],
|
filters: [Plugin.RemoveDrafts()],
|
||||||
emitters: [
|
emitters: [
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import * as Component from "./quartz/components"
|
|||||||
export const sharedPageComponents: SharedLayout = {
|
export const sharedPageComponents: SharedLayout = {
|
||||||
head: Component.Head(),
|
head: Component.Head(),
|
||||||
header: [],
|
header: [],
|
||||||
|
afterBody: [],
|
||||||
footer: Component.Footer({
|
footer: Component.Footer({
|
||||||
links: {
|
links: {
|
||||||
GitHub: "https://github.com/ErdemOzgen/erdemozgen.github.io",
|
GitHub: "https://github.com/ErdemOzgen/erdemozgen.github.io",
|
||||||
|
|||||||
@ -38,8 +38,13 @@ type BuildData = {
|
|||||||
|
|
||||||
type FileEvent = "add" | "change" | "delete"
|
type FileEvent = "add" | "change" | "delete"
|
||||||
|
|
||||||
|
function newBuildId() {
|
||||||
|
return Math.random().toString(36).substring(2, 8)
|
||||||
|
}
|
||||||
|
|
||||||
async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
||||||
const ctx: BuildCtx = {
|
const ctx: BuildCtx = {
|
||||||
|
buildId: newBuildId(),
|
||||||
argv,
|
argv,
|
||||||
cfg,
|
cfg,
|
||||||
allSlugs: [],
|
allSlugs: [],
|
||||||
@ -157,10 +162,13 @@ async function partialRebuildFromEntrypoint(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildStart = new Date().getTime()
|
const buildId = newBuildId()
|
||||||
buildData.lastBuildMs = buildStart
|
ctx.buildId = buildId
|
||||||
|
buildData.lastBuildMs = new Date().getTime()
|
||||||
const release = await mut.acquire()
|
const release = await mut.acquire()
|
||||||
if (buildData.lastBuildMs > buildStart) {
|
|
||||||
|
// if there's another build after us, release and let them do it
|
||||||
|
if (ctx.buildId !== buildId) {
|
||||||
release()
|
release()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -351,26 +359,22 @@ async function rebuildFromEntrypoint(
|
|||||||
toRemove.add(filePath)
|
toRemove.add(filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildStart = new Date().getTime()
|
const buildId = newBuildId()
|
||||||
buildData.lastBuildMs = buildStart
|
ctx.buildId = buildId
|
||||||
|
buildData.lastBuildMs = new Date().getTime()
|
||||||
const release = await mut.acquire()
|
const release = await mut.acquire()
|
||||||
|
|
||||||
// there's another build after us, release and let them do it
|
// there's another build after us, release and let them do it
|
||||||
if (buildData.lastBuildMs > buildStart) {
|
if (ctx.buildId !== buildId) {
|
||||||
release()
|
release()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const perf = new PerfTimer()
|
const perf = new PerfTimer()
|
||||||
console.log(chalk.yellow("Detected change, rebuilding..."))
|
console.log(chalk.yellow("Detected change, rebuilding..."))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const filesToRebuild = [...toRebuild].filter((fp) => !toRemove.has(fp))
|
const filesToRebuild = [...toRebuild].filter((fp) => !toRemove.has(fp))
|
||||||
|
|
||||||
const trackedSlugs = [...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets])]
|
|
||||||
.filter((fp) => !toRemove.has(fp))
|
|
||||||
.map((fp) => slugifyFilePath(path.posix.relative(argv.directory, fp) as FilePath))
|
|
||||||
|
|
||||||
ctx.allSlugs = [...new Set([...initialSlugs, ...trackedSlugs])]
|
|
||||||
const parsedContent = await parseMarkdown(ctx, filesToRebuild)
|
const parsedContent = await parseMarkdown(ctx, filesToRebuild)
|
||||||
for (const content of parsedContent) {
|
for (const content of parsedContent) {
|
||||||
const [_tree, vfile] = content
|
const [_tree, vfile] = content
|
||||||
@ -384,6 +388,13 @@ async function rebuildFromEntrypoint(
|
|||||||
const parsedFiles = [...contentMap.values()]
|
const parsedFiles = [...contentMap.values()]
|
||||||
const filteredContent = filterContent(ctx, parsedFiles)
|
const filteredContent = filterContent(ctx, parsedFiles)
|
||||||
|
|
||||||
|
// re-update slugs
|
||||||
|
const trackedSlugs = [...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets])]
|
||||||
|
.filter((fp) => !toRemove.has(fp))
|
||||||
|
.map((fp) => slugifyFilePath(path.posix.relative(argv.directory, fp) as FilePath))
|
||||||
|
|
||||||
|
ctx.allSlugs = [...new Set([...initialSlugs, ...trackedSlugs])]
|
||||||
|
|
||||||
// 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(path.join(argv.output, ".*"), { glob: true })
|
||||||
@ -396,10 +407,10 @@ async function rebuildFromEntrypoint(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
release()
|
|
||||||
clientRefresh()
|
clientRefresh()
|
||||||
toRebuild.clear()
|
toRebuild.clear()
|
||||||
toRemove.clear()
|
toRemove.clear()
|
||||||
|
release()
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => {
|
export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => {
|
||||||
|
|||||||
@ -30,9 +30,18 @@ export type Analytics =
|
|||||||
apiKey: string
|
apiKey: string
|
||||||
host?: string
|
host?: string
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
provider: "tinylytics"
|
||||||
|
siteId: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
provider: "cabin"
|
||||||
|
host?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface GlobalConfiguration {
|
export interface GlobalConfiguration {
|
||||||
pageTitle: string
|
pageTitle: string
|
||||||
|
pageTitleSuffix?: string
|
||||||
/** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */
|
/** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */
|
||||||
enableSPA: boolean
|
enableSPA: boolean
|
||||||
/** Whether to display Wikipedia-style popovers when hovering over links */
|
/** Whether to display Wikipedia-style popovers when hovering over links */
|
||||||
@ -69,10 +78,11 @@ export interface FullPageLayout {
|
|||||||
header: QuartzComponent[]
|
header: QuartzComponent[]
|
||||||
beforeBody: QuartzComponent[]
|
beforeBody: QuartzComponent[]
|
||||||
pageBody: QuartzComponent
|
pageBody: QuartzComponent
|
||||||
|
afterBody: QuartzComponent[]
|
||||||
left: QuartzComponent[]
|
left: QuartzComponent[]
|
||||||
right: QuartzComponent[]
|
right: QuartzComponent[]
|
||||||
footer: QuartzComponent
|
footer: QuartzComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PageLayout = Pick<FullPageLayout, "beforeBody" | "left" | "right">
|
export type PageLayout = Pick<FullPageLayout, "beforeBody" | "left" | "right">
|
||||||
export type SharedLayout = Pick<FullPageLayout, "head" | "header" | "footer">
|
export type SharedLayout = Pick<FullPageLayout, "head" | "header" | "footer" | "afterBody">
|
||||||
|
|||||||
44
quartz/components/Comments.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
|
import { classNames } from "../util/lang"
|
||||||
|
// @ts-ignore
|
||||||
|
import script from "./scripts/comments.inline"
|
||||||
|
|
||||||
|
type Options = {
|
||||||
|
provider: "giscus"
|
||||||
|
options: {
|
||||||
|
repo: `${string}/${string}`
|
||||||
|
repoId: string
|
||||||
|
category: string
|
||||||
|
categoryId: string
|
||||||
|
mapping?: "url" | "title" | "og:title" | "specific" | "number" | "pathname"
|
||||||
|
strict?: boolean
|
||||||
|
reactionsEnabled?: boolean
|
||||||
|
inputPosition?: "top" | "bottom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function boolToStringBool(b: boolean): string {
|
||||||
|
return b ? "1" : "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ((opts: Options) => {
|
||||||
|
const Comments: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={classNames(displayClass, "giscus")}
|
||||||
|
data-repo={opts.options.repo}
|
||||||
|
data-repo-id={opts.options.repoId}
|
||||||
|
data-category={opts.options.category}
|
||||||
|
data-category-id={opts.options.categoryId}
|
||||||
|
data-mapping={opts.options.mapping ?? "url"}
|
||||||
|
data-strict={boolToStringBool(opts.options.strict ?? true)}
|
||||||
|
data-reactions-enabled={boolToStringBool(opts.options.reactionsEnabled ?? true)}
|
||||||
|
data-input-position={opts.options.inputPosition ?? "bottom"}
|
||||||
|
></div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Comments.afterDOMLoaded = script
|
||||||
|
|
||||||
|
return Comments
|
||||||
|
}) satisfies QuartzComponentConstructor<Options>
|
||||||
@ -9,41 +9,38 @@ import { classNames } from "../util/lang"
|
|||||||
|
|
||||||
const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "darkmode")}>
|
<button class={classNames(displayClass, "darkmode")} id="darkmode">
|
||||||
<input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} />
|
<svg
|
||||||
<label id="toggle-label-light" for="darkmode-toggle" tabIndex={-1}>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<svg
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
version="1.1"
|
||||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
id="dayIcon"
|
||||||
version="1.1"
|
x="0px"
|
||||||
id="dayIcon"
|
y="0px"
|
||||||
x="0px"
|
viewBox="0 0 35 35"
|
||||||
y="0px"
|
style="enable-background:new 0 0 35 35"
|
||||||
viewBox="0 0 35 35"
|
xmlSpace="preserve"
|
||||||
style="enable-background:new 0 0 35 35"
|
aria-label={i18n(cfg.locale).components.themeToggle.darkMode}
|
||||||
xmlSpace="preserve"
|
>
|
||||||
>
|
<title>{i18n(cfg.locale).components.themeToggle.darkMode}</title>
|
||||||
<title>{i18n(cfg.locale).components.themeToggle.darkMode}</title>
|
<path d="M6,17.5C6,16.672,5.328,16,4.5,16h-3C0.672,16,0,16.672,0,17.5 S0.672,19,1.5,19h3C5.328,19,6,18.328,6,17.5z M7.5,26c-0.414,0-0.789,0.168-1.061,0.439l-2,2C4.168,28.711,4,29.086,4,29.5 C4,30.328,4.671,31,5.5,31c0.414,0,0.789-0.168,1.06-0.44l2-2C8.832,28.289,9,27.914,9,27.5C9,26.672,8.329,26,7.5,26z M17.5,6 C18.329,6,19,5.328,19,4.5v-3C19,0.672,18.329,0,17.5,0S16,0.672,16,1.5v3C16,5.328,16.671,6,17.5,6z M27.5,9 c0.414,0,0.789-0.168,1.06-0.439l2-2C30.832,6.289,31,5.914,31,5.5C31,4.672,30.329,4,29.5,4c-0.414,0-0.789,0.168-1.061,0.44 l-2,2C26.168,6.711,26,7.086,26,7.5C26,8.328,26.671,9,27.5,9z M6.439,8.561C6.711,8.832,7.086,9,7.5,9C8.328,9,9,8.328,9,7.5 c0-0.414-0.168-0.789-0.439-1.061l-2-2C6.289,4.168,5.914,4,5.5,4C4.672,4,4,4.672,4,5.5c0,0.414,0.168,0.789,0.439,1.06 L6.439,8.561z M33.5,16h-3c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5h3c0.828,0,1.5-0.672,1.5-1.5S34.328,16,33.5,16z M28.561,26.439C28.289,26.168,27.914,26,27.5,26c-0.828,0-1.5,0.672-1.5,1.5c0,0.414,0.168,0.789,0.439,1.06l2,2 C28.711,30.832,29.086,31,29.5,31c0.828,0,1.5-0.672,1.5-1.5c0-0.414-0.168-0.789-0.439-1.061L28.561,26.439z M17.5,29 c-0.829,0-1.5,0.672-1.5,1.5v3c0,0.828,0.671,1.5,1.5,1.5s1.5-0.672,1.5-1.5v-3C19,29.672,18.329,29,17.5,29z M17.5,7 C11.71,7,7,11.71,7,17.5S11.71,28,17.5,28S28,23.29,28,17.5S23.29,7,17.5,7z M17.5,25c-4.136,0-7.5-3.364-7.5-7.5 c0-4.136,3.364-7.5,7.5-7.5c4.136,0,7.5,3.364,7.5,7.5C25,21.636,21.636,25,17.5,25z"></path>
|
||||||
<path d="M6,17.5C6,16.672,5.328,16,4.5,16h-3C0.672,16,0,16.672,0,17.5 S0.672,19,1.5,19h3C5.328,19,6,18.328,6,17.5z M7.5,26c-0.414,0-0.789,0.168-1.061,0.439l-2,2C4.168,28.711,4,29.086,4,29.5 C4,30.328,4.671,31,5.5,31c0.414,0,0.789-0.168,1.06-0.44l2-2C8.832,28.289,9,27.914,9,27.5C9,26.672,8.329,26,7.5,26z M17.5,6 C18.329,6,19,5.328,19,4.5v-3C19,0.672,18.329,0,17.5,0S16,0.672,16,1.5v3C16,5.328,16.671,6,17.5,6z M27.5,9 c0.414,0,0.789-0.168,1.06-0.439l2-2C30.832,6.289,31,5.914,31,5.5C31,4.672,30.329,4,29.5,4c-0.414,0-0.789,0.168-1.061,0.44 l-2,2C26.168,6.711,26,7.086,26,7.5C26,8.328,26.671,9,27.5,9z M6.439,8.561C6.711,8.832,7.086,9,7.5,9C8.328,9,9,8.328,9,7.5 c0-0.414-0.168-0.789-0.439-1.061l-2-2C6.289,4.168,5.914,4,5.5,4C4.672,4,4,4.672,4,5.5c0,0.414,0.168,0.789,0.439,1.06 L6.439,8.561z M33.5,16h-3c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5h3c0.828,0,1.5-0.672,1.5-1.5S34.328,16,33.5,16z M28.561,26.439C28.289,26.168,27.914,26,27.5,26c-0.828,0-1.5,0.672-1.5,1.5c0,0.414,0.168,0.789,0.439,1.06l2,2 C28.711,30.832,29.086,31,29.5,31c0.828,0,1.5-0.672,1.5-1.5c0-0.414-0.168-0.789-0.439-1.061L28.561,26.439z M17.5,29 c-0.829,0-1.5,0.672-1.5,1.5v3c0,0.828,0.671,1.5,1.5,1.5s1.5-0.672,1.5-1.5v-3C19,29.672,18.329,29,17.5,29z M17.5,7 C11.71,7,7,11.71,7,17.5S11.71,28,17.5,28S28,23.29,28,17.5S23.29,7,17.5,7z M17.5,25c-4.136,0-7.5-3.364-7.5-7.5 c0-4.136,3.364-7.5,7.5-7.5c4.136,0,7.5,3.364,7.5,7.5C25,21.636,21.636,25,17.5,25z"></path>
|
</svg>
|
||||||
</svg>
|
<svg
|
||||||
</label>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<label id="toggle-label-dark" for="darkmode-toggle" tabIndex={-1}>
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||||
<svg
|
version="1.1"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
id="nightIcon"
|
||||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
x="0px"
|
||||||
version="1.1"
|
y="0px"
|
||||||
id="nightIcon"
|
viewBox="0 0 100 100"
|
||||||
x="0px"
|
style="enable-background:new 0 0 100 100"
|
||||||
y="0px"
|
xmlSpace="preserve"
|
||||||
viewBox="0 0 100 100"
|
aria-label={i18n(cfg.locale).components.themeToggle.lightMode}
|
||||||
style="enable-background:new 0 0 100 100"
|
>
|
||||||
xmlSpace="preserve"
|
<title>{i18n(cfg.locale).components.themeToggle.lightMode}</title>
|
||||||
>
|
<path d="M96.76,66.458c-0.853-0.852-2.15-1.064-3.23-0.534c-6.063,2.991-12.858,4.571-19.655,4.571 C62.022,70.495,50.88,65.88,42.5,57.5C29.043,44.043,25.658,23.536,34.076,6.47c0.532-1.08,0.318-2.379-0.534-3.23 c-0.851-0.852-2.15-1.064-3.23-0.534c-4.918,2.427-9.375,5.619-13.246,9.491c-9.447,9.447-14.65,22.008-14.65,35.369 c0,13.36,5.203,25.921,14.65,35.368s22.008,14.65,35.368,14.65c13.361,0,25.921-5.203,35.369-14.65 c3.872-3.871,7.064-8.328,9.491-13.246C97.826,68.608,97.611,67.309,96.76,66.458z"></path>
|
||||||
<title>{i18n(cfg.locale).components.themeToggle.lightMode}</title>
|
</svg>
|
||||||
<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>
|
</button>
|
||||||
</svg>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -44,12 +44,9 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
// memoized
|
// memoized
|
||||||
let fileTree: FileNode
|
let fileTree: FileNode
|
||||||
let jsonTree: string
|
let jsonTree: string
|
||||||
|
let lastBuildId: string = ""
|
||||||
|
|
||||||
function constructFileTree(allFiles: QuartzPluginData[]) {
|
function constructFileTree(allFiles: QuartzPluginData[]) {
|
||||||
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))
|
||||||
@ -76,12 +73,17 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Explorer: QuartzComponent = ({
|
const Explorer: QuartzComponent = ({
|
||||||
|
ctx,
|
||||||
cfg,
|
cfg,
|
||||||
allFiles,
|
allFiles,
|
||||||
displayClass,
|
displayClass,
|
||||||
fileData,
|
fileData,
|
||||||
}: QuartzComponentProps) => {
|
}: QuartzComponentProps) => {
|
||||||
constructFileTree(allFiles)
|
if (ctx.buildId !== lastBuildId) {
|
||||||
|
lastBuildId = ctx.buildId
|
||||||
|
constructFileTree(allFiles)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "explorer")}>
|
<div class={classNames(displayClass, "explorer")}>
|
||||||
<button
|
<button
|
||||||
@ -91,8 +93,10 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
data-collapsed={opts.folderDefaultState}
|
data-collapsed={opts.folderDefaultState}
|
||||||
data-savestate={opts.useSavedState}
|
data-savestate={opts.useSavedState}
|
||||||
data-tree={jsonTree}
|
data-tree={jsonTree}
|
||||||
|
aria-controls="explorer-content"
|
||||||
|
aria-expanded={opts.folderDefaultState === "open"}
|
||||||
>
|
>
|
||||||
<h1>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h1>
|
<h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="14"
|
width="14"
|
||||||
|
|||||||
@ -168,10 +168,8 @@ 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 folderPath = ""
|
const folderPath = node.name !== "" ? joinSegments(fullPath ?? "", node.name) : ""
|
||||||
if (node.name !== "") {
|
const href = resolveRelative(fileData.slug!, folderPath as SimpleSlug) + "/"
|
||||||
folderPath = joinSegments(fullPath ?? "", node.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -205,11 +203,7 @@ 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={href} 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>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -13,7 +13,6 @@ export default ((opts?: Options) => {
|
|||||||
const links = opts?.links ?? []
|
const links = opts?.links ?? []
|
||||||
return (
|
return (
|
||||||
<footer class={`${displayClass ?? ""}`}>
|
<footer class={`${displayClass ?? ""}`}>
|
||||||
<hr />
|
|
||||||
<p>
|
<p>
|
||||||
{i18n(cfg.locale).components.footer.createdWith}{" "}
|
{i18n(cfg.locale).components.footer.createdWith}{" "}
|
||||||
<a href="https://quartz.jzhao.xyz/">Quartz v{version}</a> © {year}
|
<a href="https://quartz.jzhao.xyz/">Quartz v{version}</a> © {year}
|
||||||
|
|||||||
@ -65,31 +65,32 @@ export default ((opts?: GraphOptions) => {
|
|||||||
<h3>{i18n(cfg.locale).components.graph.title}</h3>
|
<h3>{i18n(cfg.locale).components.graph.title}</h3>
|
||||||
<div class="graph-outer">
|
<div class="graph-outer">
|
||||||
<div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
|
<div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
|
||||||
<svg
|
<button id="global-graph-icon" aria-label="Global Graph">
|
||||||
version="1.1"
|
<svg
|
||||||
id="global-graph-icon"
|
version="1.1"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||||
x="0px"
|
x="0px"
|
||||||
y="0px"
|
y="0px"
|
||||||
viewBox="0 0 55 55"
|
viewBox="0 0 55 55"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
xmlSpace="preserve"
|
xmlSpace="preserve"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M49,0c-3.309,0-6,2.691-6,6c0,1.035,0.263,2.009,0.726,2.86l-9.829,9.829C32.542,17.634,30.846,17,29,17
|
d="M49,0c-3.309,0-6,2.691-6,6c0,1.035,0.263,2.009,0.726,2.86l-9.829,9.829C32.542,17.634,30.846,17,29,17
|
||||||
s-3.542,0.634-4.898,1.688l-7.669-7.669C16.785,10.424,17,9.74,17,9c0-2.206-1.794-4-4-4S9,6.794,9,9s1.794,4,4,4
|
s-3.542,0.634-4.898,1.688l-7.669-7.669C16.785,10.424,17,9.74,17,9c0-2.206-1.794-4-4-4S9,6.794,9,9s1.794,4,4,4
|
||||||
c0.74,0,1.424-0.215,2.019-0.567l7.669,7.669C21.634,21.458,21,23.154,21,25s0.634,3.542,1.688,4.897L10.024,42.562
|
c0.74,0,1.424-0.215,2.019-0.567l7.669,7.669C21.634,21.458,21,23.154,21,25s0.634,3.542,1.688,4.897L10.024,42.562
|
||||||
C8.958,41.595,7.549,41,6,41c-3.309,0-6,2.691-6,6s2.691,6,6,6s6-2.691,6-6c0-1.035-0.263-2.009-0.726-2.86l12.829-12.829
|
C8.958,41.595,7.549,41,6,41c-3.309,0-6,2.691-6,6s2.691,6,6,6s6-2.691,6-6c0-1.035-0.263-2.009-0.726-2.86l12.829-12.829
|
||||||
c1.106,0.86,2.44,1.436,3.898,1.619v10.16c-2.833,0.478-5,2.942-5,5.91c0,3.309,2.691,6,6,6s6-2.691,6-6c0-2.967-2.167-5.431-5-5.91
|
c1.106,0.86,2.44,1.436,3.898,1.619v10.16c-2.833,0.478-5,2.942-5,5.91c0,3.309,2.691,6,6,6s6-2.691,6-6c0-2.967-2.167-5.431-5-5.91
|
||||||
v-10.16c1.458-0.183,2.792-0.759,3.898-1.619l7.669,7.669C41.215,39.576,41,40.26,41,41c0,2.206,1.794,4,4,4s4-1.794,4-4
|
v-10.16c1.458-0.183,2.792-0.759,3.898-1.619l7.669,7.669C41.215,39.576,41,40.26,41,41c0,2.206,1.794,4,4,4s4-1.794,4-4
|
||||||
s-1.794-4-4-4c-0.74,0-1.424,0.215-2.019,0.567l-7.669-7.669C36.366,28.542,37,26.846,37,25s-0.634-3.542-1.688-4.897l9.665-9.665
|
s-1.794-4-4-4c-0.74,0-1.424,0.215-2.019,0.567l-7.669-7.669C36.366,28.542,37,26.846,37,25s-0.634-3.542-1.688-4.897l9.665-9.665
|
||||||
C46.042,11.405,47.451,12,49,12c3.309,0,6-2.691,6-6S52.309,0,49,0z M11,9c0-1.103,0.897-2,2-2s2,0.897,2,2s-0.897,2-2,2
|
C46.042,11.405,47.451,12,49,12c3.309,0,6-2.691,6-6S52.309,0,49,0z M11,9c0-1.103,0.897-2,2-2s2,0.897,2,2s-0.897,2-2,2
|
||||||
S11,10.103,11,9z M6,51c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S8.206,51,6,51z M33,49c0,2.206-1.794,4-4,4s-4-1.794-4-4
|
S11,10.103,11,9z M6,51c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S8.206,51,6,51z M33,49c0,2.206-1.794,4-4,4s-4-1.794-4-4
|
||||||
s1.794-4,4-4S33,46.794,33,49z M29,31c-3.309,0-6-2.691-6-6s2.691-6,6-6s6,2.691,6,6S32.309,31,29,31z M47,41c0,1.103-0.897,2-2,2
|
s1.794-4,4-4S33,46.794,33,49z M29,31c-3.309,0-6-2.691-6-6s2.691-6,6-6s6,2.691,6,6S32.309,31,29,31z M47,41c0,1.103-0.897,2-2,2
|
||||||
s-2-0.897-2-2s0.897-2,2-2S47,39.897,47,41z M49,10c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S51.206,10,49,10z"
|
s-2-0.897-2-2s0.897-2,2-2S47,39.897,47,41z M49,10c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S51.206,10,49,10z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="global-graph-outer">
|
<div id="global-graph-outer">
|
||||||
<div id="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div>
|
<div id="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div>
|
||||||
|
|||||||
@ -6,7 +6,8 @@ import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } fro
|
|||||||
|
|
||||||
export default (() => {
|
export default (() => {
|
||||||
const Head: QuartzComponent = ({ cfg, fileData, externalResources }: QuartzComponentProps) => {
|
const Head: QuartzComponent = ({ cfg, fileData, externalResources }: QuartzComponentProps) => {
|
||||||
const title = fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title
|
const title =
|
||||||
|
(fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title) + cfg.pageTitleSuffix
|
||||||
const description =
|
const description =
|
||||||
fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description
|
fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description
|
||||||
const { css, js } = externalResources
|
const { css, js } = externalResources
|
||||||
|
|||||||
@ -4,9 +4,9 @@ import { Date, getDate } from "./Date"
|
|||||||
import { QuartzComponent, QuartzComponentProps } from "./types"
|
import { QuartzComponent, QuartzComponentProps } from "./types"
|
||||||
import { GlobalConfiguration } from "../cfg"
|
import { GlobalConfiguration } from "../cfg"
|
||||||
|
|
||||||
export function byDateAndAlphabetical(
|
export type SortFn = (f1: QuartzPluginData, f2: QuartzPluginData) => number
|
||||||
cfg: GlobalConfiguration,
|
|
||||||
): (f1: QuartzPluginData, f2: QuartzPluginData) => number {
|
export function byDateAndAlphabetical(cfg: GlobalConfiguration): SortFn {
|
||||||
return (f1, f2) => {
|
return (f1, f2) => {
|
||||||
if (f1.dates && f2.dates) {
|
if (f1.dates && f2.dates) {
|
||||||
// sort descending
|
// sort descending
|
||||||
@ -27,10 +27,12 @@ export function byDateAndAlphabetical(
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
limit?: number
|
limit?: number
|
||||||
|
sort?: SortFn
|
||||||
} & QuartzComponentProps
|
} & QuartzComponentProps
|
||||||
|
|
||||||
export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit }: Props) => {
|
export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit, sort }: Props) => {
|
||||||
let list = allFiles.sort(byDateAndAlphabetical(cfg))
|
const sorter = sort ?? byDateAndAlphabetical(cfg)
|
||||||
|
let list = allFiles.sort(sorter)
|
||||||
if (limit) {
|
if (limit) {
|
||||||
list = list.slice(0, limit)
|
list = list.slice(0, limit)
|
||||||
}
|
}
|
||||||
@ -44,11 +46,13 @@ export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit }: Pr
|
|||||||
return (
|
return (
|
||||||
<li class="section-li">
|
<li class="section-li">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
{page.dates && (
|
<div>
|
||||||
<p class="meta">
|
{page.dates && (
|
||||||
<Date date={getDate(cfg, page)!} locale={cfg.locale} />
|
<p class="meta">
|
||||||
</p>
|
<Date date={getDate(cfg, page)!} locale={cfg.locale} />
|
||||||
)}
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div class="desc">
|
<div class="desc">
|
||||||
<h3>
|
<h3>
|
||||||
<a href={resolveRelative(fileData.slug!, page.slug!)} class="internal">
|
<a href={resolveRelative(fileData.slug!, page.slug!)} class="internal">
|
||||||
|
|||||||
@ -7,14 +7,15 @@ const PageTitle: QuartzComponent = ({ fileData, cfg, displayClass }: QuartzCompo
|
|||||||
const title = cfg?.pageTitle ?? i18n(cfg.locale).propertyDefaults.title
|
const title = cfg?.pageTitle ?? i18n(cfg.locale).propertyDefaults.title
|
||||||
const baseDir = pathToRoot(fileData.slug!)
|
const baseDir = pathToRoot(fileData.slug!)
|
||||||
return (
|
return (
|
||||||
<h1 class={classNames(displayClass, "page-title")}>
|
<h2 class={classNames(displayClass, "page-title")}>
|
||||||
<a href={baseDir}>{title}</a>
|
<a href={baseDir}>{title}</a>
|
||||||
</h1>
|
</h2>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
PageTitle.css = `
|
PageTitle.css = `
|
||||||
.page-title {
|
.page-title {
|
||||||
|
font-size: 1.75rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@ -12,6 +12,7 @@ interface Options {
|
|||||||
title?: string
|
title?: string
|
||||||
limit: number
|
limit: number
|
||||||
linkToMore: SimpleSlug | false
|
linkToMore: SimpleSlug | false
|
||||||
|
showTags: boolean
|
||||||
filter: (f: QuartzPluginData) => boolean
|
filter: (f: QuartzPluginData) => boolean
|
||||||
sort: (f1: QuartzPluginData, f2: QuartzPluginData) => number
|
sort: (f1: QuartzPluginData, f2: QuartzPluginData) => number
|
||||||
}
|
}
|
||||||
@ -19,6 +20,7 @@ interface Options {
|
|||||||
const defaultOptions = (cfg: GlobalConfiguration): Options => ({
|
const defaultOptions = (cfg: GlobalConfiguration): Options => ({
|
||||||
limit: 3,
|
limit: 3,
|
||||||
linkToMore: false,
|
linkToMore: false,
|
||||||
|
showTags: true,
|
||||||
filter: () => true,
|
filter: () => true,
|
||||||
sort: byDateAndAlphabetical(cfg),
|
sort: byDateAndAlphabetical(cfg),
|
||||||
})
|
})
|
||||||
@ -56,18 +58,20 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
<Date date={getDate(cfg, page)!} locale={cfg.locale} />
|
<Date date={getDate(cfg, page)!} locale={cfg.locale} />
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<ul class="tags">
|
{opts.showTags && (
|
||||||
{tags.map((tag) => (
|
<ul class="tags">
|
||||||
<li>
|
{tags.map((tag) => (
|
||||||
<a
|
<li>
|
||||||
class="internal tag-link"
|
<a
|
||||||
href={resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug)}
|
class="internal tag-link"
|
||||||
>
|
href={resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug)}
|
||||||
{tag}
|
>
|
||||||
</a>
|
{tag}
|
||||||
</li>
|
</a>
|
||||||
))}
|
</li>
|
||||||
</ul>
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -19,24 +19,16 @@ export default ((userOpts?: Partial<SearchOptions>) => {
|
|||||||
const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder
|
const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "search")}>
|
<div class={classNames(displayClass, "search")}>
|
||||||
<div id="search-icon">
|
<button class="search-button" id="search-button">
|
||||||
<p>{i18n(cfg.locale).components.search.title}</p>
|
<p>{i18n(cfg.locale).components.search.title}</p>
|
||||||
<div></div>
|
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.9 19.7">
|
||||||
<svg
|
<title>Search</title>
|
||||||
tabIndex={0}
|
|
||||||
aria-labelledby="title desc"
|
|
||||||
role="img"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 19.9 19.7"
|
|
||||||
>
|
|
||||||
<title id="title">Search</title>
|
|
||||||
<desc id="desc">Search</desc>
|
|
||||||
<g class="search-path" fill="none">
|
<g class="search-path" fill="none">
|
||||||
<path stroke-linecap="square" d="M18.5 18.3l-5.4-5.4" />
|
<path stroke-linecap="square" d="M18.5 18.3l-5.4-5.4" />
|
||||||
<circle cx="8" cy="8" r="7" />
|
<circle cx="8" cy="8" r="7" />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</button>
|
||||||
<div id="search-container">
|
<div id="search-container">
|
||||||
<div id="search-space">
|
<div id="search-space">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@ -26,7 +26,13 @@ const TableOfContents: QuartzComponent = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "toc")}>
|
<div class={classNames(displayClass, "toc")}>
|
||||||
<button type="button" id="toc" class={fileData.collapseToc ? "collapsed" : ""}>
|
<button
|
||||||
|
type="button"
|
||||||
|
id="toc"
|
||||||
|
class={fileData.collapseToc ? "collapsed" : ""}
|
||||||
|
aria-controls="toc-content"
|
||||||
|
aria-expanded={!fileData.collapseToc}
|
||||||
|
>
|
||||||
<h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
|
<h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -43,7 +49,7 @@ const TableOfContents: QuartzComponent = ({
|
|||||||
<polyline points="6 9 12 15 18 9"></polyline>
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div id="toc-content">
|
<div id="toc-content" class={fileData.collapseToc ? "collapsed" : ""}>
|
||||||
<ul class="overflow">
|
<ul class="overflow">
|
||||||
{fileData.toc.map((tocEntry) => (
|
{fileData.toc.map((tocEntry) => (
|
||||||
<li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
|
<li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import DesktopOnly from "./DesktopOnly"
|
|||||||
import MobileOnly from "./MobileOnly"
|
import MobileOnly from "./MobileOnly"
|
||||||
import RecentNotes from "./RecentNotes"
|
import RecentNotes from "./RecentNotes"
|
||||||
import Breadcrumbs from "./Breadcrumbs"
|
import Breadcrumbs from "./Breadcrumbs"
|
||||||
|
import Comments from "./Comments"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ArticleTitle,
|
ArticleTitle,
|
||||||
@ -42,4 +43,5 @@ export {
|
|||||||
RecentNotes,
|
RecentNotes,
|
||||||
NotFound,
|
NotFound,
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
|
Comments,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,10 +2,15 @@ import { i18n } from "../../i18n"
|
|||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
||||||
|
|
||||||
const NotFound: QuartzComponent = ({ cfg }: QuartzComponentProps) => {
|
const NotFound: QuartzComponent = ({ cfg }: QuartzComponentProps) => {
|
||||||
|
// If baseUrl contains a pathname after the domain, use this as the home link
|
||||||
|
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
|
||||||
|
const baseDir = url.pathname
|
||||||
|
|
||||||
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>{i18n(cfg.locale).pages.error.notFound}</p>
|
||||||
|
<a href={baseDir}>{i18n(cfg.locale).pages.error.home}</a>
|
||||||
</article>
|
</article>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } fro
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
import style from "../styles/listPage.scss"
|
import style from "../styles/listPage.scss"
|
||||||
import { PageList } from "../PageList"
|
import { PageList, SortFn } from "../PageList"
|
||||||
import { stripSlashes, simplifySlug } from "../../util/path"
|
import { stripSlashes, simplifySlug } from "../../util/path"
|
||||||
import { Root } from "hast"
|
import { Root } from "hast"
|
||||||
import { htmlToJsx } from "../../util/jsx"
|
import { htmlToJsx } from "../../util/jsx"
|
||||||
@ -13,6 +13,7 @@ interface FolderContentOptions {
|
|||||||
* Whether to display number of folders
|
* Whether to display number of folders
|
||||||
*/
|
*/
|
||||||
showFolderCount: boolean
|
showFolderCount: boolean
|
||||||
|
sort?: SortFn
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions: FolderContentOptions = {
|
const defaultOptions: FolderContentOptions = {
|
||||||
@ -37,6 +38,7 @@ export default ((opts?: Partial<FolderContentOptions>) => {
|
|||||||
const classes = ["popover-hint", ...cssClasses].join(" ")
|
const classes = ["popover-hint", ...cssClasses].join(" ")
|
||||||
const listProps = {
|
const listProps = {
|
||||||
...props,
|
...props,
|
||||||
|
sort: options.sort,
|
||||||
allFiles: allPagesInFolder,
|
allFiles: allPagesInFolder,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,113 +1,127 @@
|
|||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
||||||
import style from "../styles/listPage.scss"
|
import style from "../styles/listPage.scss"
|
||||||
import { PageList } from "../PageList"
|
import { PageList, SortFn } 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 { htmlToJsx } from "../../util/jsx"
|
import { htmlToJsx } from "../../util/jsx"
|
||||||
import { i18n } from "../../i18n"
|
import { i18n } from "../../i18n"
|
||||||
|
|
||||||
const numPages = 10
|
interface TagContentOptions {
|
||||||
const TagContent: QuartzComponent = (props: QuartzComponentProps) => {
|
sort?: SortFn
|
||||||
const { tree, fileData, allFiles, cfg } = props
|
numPages: number
|
||||||
const slug = fileData.slug
|
|
||||||
|
|
||||||
if (!(slug?.startsWith("tags/") || slug === "tags")) {
|
|
||||||
throw new Error(`Component "TagContent" tried to render a non-tag page: ${slug}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const tag = simplifySlug(slug.slice("tags/".length) as FullSlug)
|
|
||||||
const allPagesWithTag = (tag: string) =>
|
|
||||||
allFiles.filter((file) =>
|
|
||||||
(file.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes).includes(tag),
|
|
||||||
)
|
|
||||||
|
|
||||||
const content =
|
|
||||||
(tree as Root).children.length === 0
|
|
||||||
? fileData.description
|
|
||||||
: htmlToJsx(fileData.filePath!, tree)
|
|
||||||
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
|
|
||||||
const classes = ["popover-hint", ...cssClasses].join(" ")
|
|
||||||
if (tag === "/") {
|
|
||||||
const tags = [
|
|
||||||
...new Set(
|
|
||||||
allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes),
|
|
||||||
),
|
|
||||||
].sort((a, b) => a.localeCompare(b))
|
|
||||||
const tagItemMap: Map<string, QuartzPluginData[]> = new Map()
|
|
||||||
for (const tag of tags) {
|
|
||||||
tagItemMap.set(tag, allPagesWithTag(tag))
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div class={classes}>
|
|
||||||
<article>
|
|
||||||
<p>{content}</p>
|
|
||||||
</article>
|
|
||||||
<p>{i18n(cfg.locale).pages.tagContent.totalTags({ count: tags.length })}</p>
|
|
||||||
<div>
|
|
||||||
{tags.map((tag) => {
|
|
||||||
const pages = tagItemMap.get(tag)!
|
|
||||||
const listProps = {
|
|
||||||
...props,
|
|
||||||
allFiles: pages,
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentPage = allFiles.filter((file) => file.slug === `tags/${tag}`).at(0)
|
|
||||||
|
|
||||||
const root = contentPage?.htmlAst
|
|
||||||
const content =
|
|
||||||
!root || root?.children.length === 0
|
|
||||||
? contentPage?.description
|
|
||||||
: htmlToJsx(contentPage.filePath!, root)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2>
|
|
||||||
<a class="internal tag-link" href={`../tags/${tag}`}>
|
|
||||||
{tag}
|
|
||||||
</a>
|
|
||||||
</h2>
|
|
||||||
{content && <p>{content}</p>}
|
|
||||||
<div class="page-listing">
|
|
||||||
<p>
|
|
||||||
{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}
|
|
||||||
{pages.length > numPages && (
|
|
||||||
<>
|
|
||||||
{" "}
|
|
||||||
<span>
|
|
||||||
{i18n(cfg.locale).pages.tagContent.showingFirst({ count: numPages })}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<PageList limit={numPages} {...listProps} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
const pages = allPagesWithTag(tag)
|
|
||||||
const listProps = {
|
|
||||||
...props,
|
|
||||||
allFiles: pages,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={classes}>
|
|
||||||
<article>{content}</article>
|
|
||||||
<div class="page-listing">
|
|
||||||
<p>{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}</p>
|
|
||||||
<div>
|
|
||||||
<PageList {...listProps} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TagContent.css = style + PageList.css
|
const defaultOptions: TagContentOptions = {
|
||||||
export default (() => TagContent) satisfies QuartzComponentConstructor
|
numPages: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ((opts?: Partial<TagContentOptions>) => {
|
||||||
|
const options: TagContentOptions = { ...defaultOptions, ...opts }
|
||||||
|
|
||||||
|
const TagContent: QuartzComponent = (props: QuartzComponentProps) => {
|
||||||
|
const { tree, fileData, allFiles, cfg } = props
|
||||||
|
const slug = fileData.slug
|
||||||
|
|
||||||
|
if (!(slug?.startsWith("tags/") || slug === "tags")) {
|
||||||
|
throw new Error(`Component "TagContent" tried to render a non-tag page: ${slug}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tag = simplifySlug(slug.slice("tags/".length) as FullSlug)
|
||||||
|
const allPagesWithTag = (tag: string) =>
|
||||||
|
allFiles.filter((file) =>
|
||||||
|
(file.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes).includes(tag),
|
||||||
|
)
|
||||||
|
|
||||||
|
const content =
|
||||||
|
(tree as Root).children.length === 0
|
||||||
|
? fileData.description
|
||||||
|
: htmlToJsx(fileData.filePath!, tree)
|
||||||
|
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
|
||||||
|
const classes = ["popover-hint", ...cssClasses].join(" ")
|
||||||
|
if (tag === "/") {
|
||||||
|
const tags = [
|
||||||
|
...new Set(
|
||||||
|
allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes),
|
||||||
|
),
|
||||||
|
].sort((a, b) => a.localeCompare(b))
|
||||||
|
const tagItemMap: Map<string, QuartzPluginData[]> = new Map()
|
||||||
|
for (const tag of tags) {
|
||||||
|
tagItemMap.set(tag, allPagesWithTag(tag))
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div class={classes}>
|
||||||
|
<article>
|
||||||
|
<p>{content}</p>
|
||||||
|
</article>
|
||||||
|
<p>{i18n(cfg.locale).pages.tagContent.totalTags({ count: tags.length })}</p>
|
||||||
|
<div>
|
||||||
|
{tags.map((tag) => {
|
||||||
|
const pages = tagItemMap.get(tag)!
|
||||||
|
const listProps = {
|
||||||
|
...props,
|
||||||
|
allFiles: pages,
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentPage = allFiles.filter((file) => file.slug === `tags/${tag}`).at(0)
|
||||||
|
|
||||||
|
const root = contentPage?.htmlAst
|
||||||
|
const content =
|
||||||
|
!root || root?.children.length === 0
|
||||||
|
? contentPage?.description
|
||||||
|
: htmlToJsx(contentPage.filePath!, root)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>
|
||||||
|
<a class="internal tag-link" href={`../tags/${tag}`}>
|
||||||
|
{tag}
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
{content && <p>{content}</p>}
|
||||||
|
<div class="page-listing">
|
||||||
|
<p>
|
||||||
|
{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}
|
||||||
|
{pages.length > options.numPages && (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
<span>
|
||||||
|
{i18n(cfg.locale).pages.tagContent.showingFirst({
|
||||||
|
count: options.numPages,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<PageList limit={options.numPages} {...listProps} sort={opts?.sort} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const pages = allPagesWithTag(tag)
|
||||||
|
const listProps = {
|
||||||
|
...props,
|
||||||
|
allFiles: pages,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={classes}>
|
||||||
|
<article>{content}</article>
|
||||||
|
<div class="page-listing">
|
||||||
|
<p>{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}</p>
|
||||||
|
<div>
|
||||||
|
<PageList {...listProps} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TagContent.css = style + PageList.css
|
||||||
|
return TagContent
|
||||||
|
}) satisfies QuartzComponentConstructor
|
||||||
|
|||||||
@ -14,6 +14,7 @@ interface RenderComponents {
|
|||||||
header: QuartzComponent[]
|
header: QuartzComponent[]
|
||||||
beforeBody: QuartzComponent[]
|
beforeBody: QuartzComponent[]
|
||||||
pageBody: QuartzComponent
|
pageBody: QuartzComponent
|
||||||
|
afterBody: QuartzComponent[]
|
||||||
left: QuartzComponent[]
|
left: QuartzComponent[]
|
||||||
right: QuartzComponent[]
|
right: QuartzComponent[]
|
||||||
footer: QuartzComponent
|
footer: QuartzComponent
|
||||||
@ -187,6 +188,7 @@ export function renderPage(
|
|||||||
header,
|
header,
|
||||||
beforeBody,
|
beforeBody,
|
||||||
pageBody: Content,
|
pageBody: Content,
|
||||||
|
afterBody,
|
||||||
left,
|
left,
|
||||||
right,
|
right,
|
||||||
footer: Footer,
|
footer: Footer,
|
||||||
@ -232,10 +234,16 @@ export function renderPage(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Content {...componentData} />
|
<Content {...componentData} />
|
||||||
|
<hr />
|
||||||
|
<div class="page-footer">
|
||||||
|
{afterBody.map((BodyComponent) => (
|
||||||
|
<BodyComponent {...componentData} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{RightComponent}
|
{RightComponent}
|
||||||
|
<Footer {...componentData} />
|
||||||
</Body>
|
</Body>
|
||||||
<Footer {...componentData} />
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
{pageResources.js
|
{pageResources.js
|
||||||
|
|||||||
67
quartz/components/scripts/comments.inline.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
const changeTheme = (e: CustomEventMap["themechange"]) => {
|
||||||
|
const theme = e.detail.theme
|
||||||
|
const iframe = document.querySelector("iframe.giscus-frame") as HTMLIFrameElement
|
||||||
|
if (!iframe) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!iframe.contentWindow) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe.contentWindow.postMessage(
|
||||||
|
{
|
||||||
|
giscus: {
|
||||||
|
setConfig: {
|
||||||
|
theme: theme,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"https://giscus.app",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type GiscusElement = Omit<HTMLElement, "dataset"> & {
|
||||||
|
dataset: DOMStringMap & {
|
||||||
|
repo: `${string}/${string}`
|
||||||
|
repoId: string
|
||||||
|
category: string
|
||||||
|
categoryId: string
|
||||||
|
mapping: "url" | "title" | "og:title" | "specific" | "number" | "pathname"
|
||||||
|
strict: string
|
||||||
|
reactionsEnabled: string
|
||||||
|
inputPosition: "top" | "bottom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("nav", () => {
|
||||||
|
const giscusContainer = document.querySelector(".giscus") as GiscusElement
|
||||||
|
if (!giscusContainer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const giscusScript = document.createElement("script")
|
||||||
|
giscusScript.src = "https://giscus.app/client.js"
|
||||||
|
giscusScript.async = true
|
||||||
|
giscusScript.crossOrigin = "anonymous"
|
||||||
|
giscusScript.setAttribute("data-loading", "lazy")
|
||||||
|
giscusScript.setAttribute("data-emit-metadata", "0")
|
||||||
|
giscusScript.setAttribute("data-repo", giscusContainer.dataset.repo)
|
||||||
|
giscusScript.setAttribute("data-repo-id", giscusContainer.dataset.repoId)
|
||||||
|
giscusScript.setAttribute("data-category", giscusContainer.dataset.category)
|
||||||
|
giscusScript.setAttribute("data-category-id", giscusContainer.dataset.categoryId)
|
||||||
|
giscusScript.setAttribute("data-mapping", giscusContainer.dataset.mapping)
|
||||||
|
giscusScript.setAttribute("data-strict", giscusContainer.dataset.strict)
|
||||||
|
giscusScript.setAttribute("data-reactions-enabled", giscusContainer.dataset.reactionsEnabled)
|
||||||
|
giscusScript.setAttribute("data-input-position", giscusContainer.dataset.inputPosition)
|
||||||
|
|
||||||
|
const theme = document.documentElement.getAttribute("saved-theme")
|
||||||
|
if (theme) {
|
||||||
|
giscusScript.setAttribute("data-theme", theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
giscusContainer.appendChild(giscusScript)
|
||||||
|
|
||||||
|
document.addEventListener("themechange", changeTheme)
|
||||||
|
window.addCleanup(() => document.removeEventListener("themechange", changeTheme))
|
||||||
|
})
|
||||||
@ -11,7 +11,8 @@ const emitThemeChangeEvent = (theme: "light" | "dark") => {
|
|||||||
|
|
||||||
document.addEventListener("nav", () => {
|
document.addEventListener("nav", () => {
|
||||||
const switchTheme = (e: Event) => {
|
const switchTheme = (e: Event) => {
|
||||||
const newTheme = (e.target as HTMLInputElement)?.checked ? "dark" : "light"
|
const newTheme =
|
||||||
|
document.documentElement.getAttribute("saved-theme") === "dark" ? "light" : "dark"
|
||||||
document.documentElement.setAttribute("saved-theme", newTheme)
|
document.documentElement.setAttribute("saved-theme", newTheme)
|
||||||
localStorage.setItem("theme", newTheme)
|
localStorage.setItem("theme", newTheme)
|
||||||
emitThemeChangeEvent(newTheme)
|
emitThemeChangeEvent(newTheme)
|
||||||
@ -21,17 +22,13 @@ document.addEventListener("nav", () => {
|
|||||||
const newTheme = e.matches ? "dark" : "light"
|
const newTheme = e.matches ? "dark" : "light"
|
||||||
document.documentElement.setAttribute("saved-theme", newTheme)
|
document.documentElement.setAttribute("saved-theme", newTheme)
|
||||||
localStorage.setItem("theme", newTheme)
|
localStorage.setItem("theme", newTheme)
|
||||||
toggleSwitch.checked = e.matches
|
|
||||||
emitThemeChangeEvent(newTheme)
|
emitThemeChangeEvent(newTheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Darkmode toggle
|
// Darkmode toggle
|
||||||
const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement
|
const themeButton = document.querySelector("#darkmode") as HTMLButtonElement
|
||||||
toggleSwitch.addEventListener("change", switchTheme)
|
themeButton.addEventListener("click", switchTheme)
|
||||||
window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
|
window.addCleanup(() => themeButton.removeEventListener("click", switchTheme))
|
||||||
if (currentTheme === "dark") {
|
|
||||||
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)")
|
||||||
|
|||||||
@ -17,11 +17,14 @@ const observer = new IntersectionObserver((entries) => {
|
|||||||
|
|
||||||
function toggleExplorer(this: HTMLElement) {
|
function toggleExplorer(this: HTMLElement) {
|
||||||
this.classList.toggle("collapsed")
|
this.classList.toggle("collapsed")
|
||||||
|
this.setAttribute(
|
||||||
|
"aria-expanded",
|
||||||
|
this.getAttribute("aria-expanded") === "true" ? "false" : "true",
|
||||||
|
)
|
||||||
const content = this.nextElementSibling as MaybeHTMLElement
|
const content = this.nextElementSibling as MaybeHTMLElement
|
||||||
if (!content) return
|
if (!content) return
|
||||||
|
|
||||||
content.classList.toggle("collapsed")
|
content.classList.toggle("collapsed")
|
||||||
content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFolder(evt: MouseEvent) {
|
function toggleFolder(evt: MouseEvent) {
|
||||||
|
|||||||
@ -1,19 +1,56 @@
|
|||||||
import type { ContentDetails, ContentIndex } from "../../plugins/emitters/contentIndex"
|
import type { ContentDetails } from "../../plugins/emitters/contentIndex"
|
||||||
import * as d3 from "d3"
|
import {
|
||||||
|
SimulationNodeDatum,
|
||||||
|
SimulationLinkDatum,
|
||||||
|
Simulation,
|
||||||
|
forceSimulation,
|
||||||
|
forceManyBody,
|
||||||
|
forceCenter,
|
||||||
|
forceLink,
|
||||||
|
forceCollide,
|
||||||
|
zoomIdentity,
|
||||||
|
select,
|
||||||
|
drag,
|
||||||
|
zoom,
|
||||||
|
} from "d3"
|
||||||
|
import { Text, Graphics, Application, Container, Circle } from "pixi.js"
|
||||||
|
import { Group as TweenGroup, Tween as Tweened } from "@tweenjs/tween.js"
|
||||||
import { registerEscapeHandler, removeAllChildren } from "./util"
|
import { registerEscapeHandler, removeAllChildren } from "./util"
|
||||||
import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path"
|
import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path"
|
||||||
|
import { D3Config } from "../Graph"
|
||||||
|
|
||||||
|
type GraphicsInfo = {
|
||||||
|
color: string
|
||||||
|
gfx: Graphics
|
||||||
|
alpha: number
|
||||||
|
active: boolean
|
||||||
|
}
|
||||||
|
|
||||||
type NodeData = {
|
type NodeData = {
|
||||||
id: SimpleSlug
|
id: SimpleSlug
|
||||||
text: string
|
text: string
|
||||||
tags: string[]
|
tags: string[]
|
||||||
} & d3.SimulationNodeDatum
|
} & SimulationNodeDatum
|
||||||
|
|
||||||
type LinkData = {
|
type SimpleLinkData = {
|
||||||
source: SimpleSlug
|
source: SimpleSlug
|
||||||
target: SimpleSlug
|
target: SimpleSlug
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LinkData = {
|
||||||
|
source: NodeData
|
||||||
|
target: NodeData
|
||||||
|
} & SimulationLinkDatum<NodeData>
|
||||||
|
|
||||||
|
type LinkRenderData = GraphicsInfo & {
|
||||||
|
simulationData: LinkData
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeRenderData = GraphicsInfo & {
|
||||||
|
simulationData: NodeData
|
||||||
|
label: Text
|
||||||
|
}
|
||||||
|
|
||||||
const localStorageKey = "graph-visited"
|
const localStorageKey = "graph-visited"
|
||||||
function getVisited(): Set<SimpleSlug> {
|
function getVisited(): Set<SimpleSlug> {
|
||||||
return new Set(JSON.parse(localStorage.getItem(localStorageKey) ?? "[]"))
|
return new Set(JSON.parse(localStorage.getItem(localStorageKey) ?? "[]"))
|
||||||
@ -25,6 +62,11 @@ function addToVisited(slug: SimpleSlug) {
|
|||||||
localStorage.setItem(localStorageKey, JSON.stringify([...visited]))
|
localStorage.setItem(localStorageKey, JSON.stringify([...visited]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TweenNode = {
|
||||||
|
update: (time: number) => void
|
||||||
|
stop: () => void
|
||||||
|
}
|
||||||
|
|
||||||
async function renderGraph(container: string, fullSlug: FullSlug) {
|
async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||||
const slug = simplifySlug(fullSlug)
|
const slug = simplifySlug(fullSlug)
|
||||||
const visited = getVisited()
|
const visited = getVisited()
|
||||||
@ -45,7 +87,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
removeTags,
|
removeTags,
|
||||||
showTags,
|
showTags,
|
||||||
focusOnHover,
|
focusOnHover,
|
||||||
} = JSON.parse(graph.dataset["cfg"]!)
|
} = JSON.parse(graph.dataset["cfg"]!) as D3Config
|
||||||
|
|
||||||
const data: Map<SimpleSlug, ContentDetails> = new Map(
|
const data: Map<SimpleSlug, ContentDetails> = new Map(
|
||||||
Object.entries<ContentDetails>(await fetchData).map(([k, v]) => [
|
Object.entries<ContentDetails>(await fetchData).map(([k, v]) => [
|
||||||
@ -53,10 +95,11 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
v,
|
v,
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
const links: LinkData[] = []
|
const links: SimpleLinkData[] = []
|
||||||
const tags: SimpleSlug[] = []
|
const tags: SimpleSlug[] = []
|
||||||
|
|
||||||
const validLinks = new Set(data.keys())
|
const validLinks = new Set(data.keys())
|
||||||
|
|
||||||
|
const tweens = new Map<string, TweenNode>()
|
||||||
for (const [source, details] of data.entries()) {
|
for (const [source, details] of data.entries()) {
|
||||||
const outgoing = details.links ?? []
|
const outgoing = details.links ?? []
|
||||||
|
|
||||||
@ -100,246 +143,459 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
if (showTags) tags.forEach((tag) => neighbourhood.add(tag))
|
if (showTags) tags.forEach((tag) => neighbourhood.add(tag))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nodes = [...neighbourhood].map((url) => {
|
||||||
|
const text = url.startsWith("tags/") ? "#" + url.substring(5) : (data.get(url)?.title ?? url)
|
||||||
|
return {
|
||||||
|
id: url,
|
||||||
|
text,
|
||||||
|
tags: data.get(url)?.tags ?? [],
|
||||||
|
}
|
||||||
|
})
|
||||||
const graphData: { nodes: NodeData[]; links: LinkData[] } = {
|
const graphData: { nodes: NodeData[]; links: LinkData[] } = {
|
||||||
nodes: [...neighbourhood].map((url) => {
|
nodes,
|
||||||
const text = url.startsWith("tags/") ? "#" + url.substring(5) : data.get(url)?.title ?? url
|
links: links
|
||||||
return {
|
.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target))
|
||||||
id: url,
|
.map((l) => ({
|
||||||
text: text,
|
source: nodes.find((n) => n.id === l.source)!,
|
||||||
tags: data.get(url)?.tags ?? [],
|
target: nodes.find((n) => n.id === l.target)!,
|
||||||
}
|
})),
|
||||||
}),
|
|
||||||
links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const simulation: d3.Simulation<NodeData, LinkData> = d3
|
// we virtualize the simulation and use pixi to actually render it
|
||||||
.forceSimulation(graphData.nodes)
|
const simulation: Simulation<NodeData, LinkData> = forceSimulation<NodeData>(graphData.nodes)
|
||||||
.force("charge", d3.forceManyBody().strength(-100 * repelForce))
|
.force("charge", forceManyBody().strength(-100 * repelForce))
|
||||||
.force(
|
.force("center", forceCenter().strength(centerForce))
|
||||||
"link",
|
.force("link", forceLink(graphData.links).distance(linkDistance))
|
||||||
d3
|
.force("collide", forceCollide<NodeData>((n) => nodeRadius(n)).iterations(3))
|
||||||
.forceLink(graphData.links)
|
|
||||||
.id((d: any) => d.id)
|
|
||||||
.distance(linkDistance),
|
|
||||||
)
|
|
||||||
.force("center", d3.forceCenter().strength(centerForce))
|
|
||||||
|
|
||||||
const height = Math.max(graph.offsetHeight, 250)
|
|
||||||
const width = graph.offsetWidth
|
const width = graph.offsetWidth
|
||||||
|
const height = Math.max(graph.offsetHeight, 250)
|
||||||
|
|
||||||
const svg = d3
|
// precompute style prop strings as pixi doesn't support css variables
|
||||||
.select<HTMLElement, NodeData>("#" + container)
|
const cssVars = [
|
||||||
.append("svg")
|
"--secondary",
|
||||||
.attr("width", width)
|
"--tertiary",
|
||||||
.attr("height", height)
|
"--gray",
|
||||||
.attr("viewBox", [-width / 2 / scale, -height / 2 / scale, width / scale, height / scale])
|
"--light",
|
||||||
|
"--lightgray",
|
||||||
// draw links between nodes
|
"--dark",
|
||||||
const link = svg
|
"--darkgray",
|
||||||
.append("g")
|
"--bodyFont",
|
||||||
.selectAll("line")
|
] as const
|
||||||
.data(graphData.links)
|
const computedStyleMap = cssVars.reduce(
|
||||||
.join("line")
|
(acc, key) => {
|
||||||
.attr("class", "link")
|
acc[key] = getComputedStyle(document.documentElement).getPropertyValue(key)
|
||||||
.attr("stroke", "var(--lightgray)")
|
return acc
|
||||||
.attr("stroke-width", 1)
|
},
|
||||||
|
{} as Record<(typeof cssVars)[number], string>,
|
||||||
// svg groups
|
)
|
||||||
const graphNode = svg.append("g").selectAll("g").data(graphData.nodes).enter().append("g")
|
|
||||||
|
|
||||||
// calculate color
|
// calculate color
|
||||||
const color = (d: NodeData) => {
|
const color = (d: NodeData) => {
|
||||||
const isCurrent = d.id === slug
|
const isCurrent = d.id === slug
|
||||||
if (isCurrent) {
|
if (isCurrent) {
|
||||||
return "var(--secondary)"
|
return computedStyleMap["--secondary"]
|
||||||
} else if (visited.has(d.id) || d.id.startsWith("tags/")) {
|
} else if (visited.has(d.id) || d.id.startsWith("tags/")) {
|
||||||
return "var(--tertiary)"
|
return computedStyleMap["--tertiary"]
|
||||||
} else {
|
} else {
|
||||||
return "var(--gray)"
|
return computedStyleMap["--gray"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const drag = (simulation: d3.Simulation<NodeData, LinkData>) => {
|
|
||||||
function dragstarted(event: any, d: NodeData) {
|
|
||||||
if (!event.active) simulation.alphaTarget(1).restart()
|
|
||||||
d.fx = d.x
|
|
||||||
d.fy = d.y
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragged(event: any, d: NodeData) {
|
|
||||||
d.fx = event.x
|
|
||||||
d.fy = event.y
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragended(event: any, d: NodeData) {
|
|
||||||
if (!event.active) simulation.alphaTarget(0)
|
|
||||||
d.fx = null
|
|
||||||
d.fy = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const noop = () => {}
|
|
||||||
return d3
|
|
||||||
.drag<Element, NodeData>()
|
|
||||||
.on("start", enableDrag ? dragstarted : noop)
|
|
||||||
.on("drag", enableDrag ? dragged : noop)
|
|
||||||
.on("end", enableDrag ? dragended : noop)
|
|
||||||
}
|
|
||||||
|
|
||||||
function nodeRadius(d: NodeData) {
|
function nodeRadius(d: NodeData) {
|
||||||
const numLinks = links.filter((l: any) => l.source.id === d.id || l.target.id === d.id).length
|
const numLinks = graphData.links.filter(
|
||||||
|
(l) => l.source.id === d.id || l.target.id === d.id,
|
||||||
|
).length
|
||||||
return 2 + Math.sqrt(numLinks)
|
return 2 + Math.sqrt(numLinks)
|
||||||
}
|
}
|
||||||
|
|
||||||
let connectedNodes: SimpleSlug[] = []
|
let hoveredNodeId: string | null = null
|
||||||
|
let hoveredNeighbours: Set<string> = new Set()
|
||||||
|
const linkRenderData: LinkRenderData[] = []
|
||||||
|
const nodeRenderData: NodeRenderData[] = []
|
||||||
|
function updateHoverInfo(newHoveredId: string | null) {
|
||||||
|
hoveredNodeId = newHoveredId
|
||||||
|
|
||||||
// draw individual nodes
|
if (newHoveredId === null) {
|
||||||
const node = graphNode
|
hoveredNeighbours = new Set()
|
||||||
.append("circle")
|
for (const n of nodeRenderData) {
|
||||||
.attr("class", "node")
|
n.active = false
|
||||||
.attr("id", (d) => d.id)
|
|
||||||
.attr("r", nodeRadius)
|
|
||||||
.attr("fill", color)
|
|
||||||
.style("cursor", "pointer")
|
|
||||||
.on("click", (_, d) => {
|
|
||||||
const targ = resolveRelative(fullSlug, d.id)
|
|
||||||
window.spaNavigate(new URL(targ, window.location.toString()))
|
|
||||||
})
|
|
||||||
.on("mouseover", function (_, d) {
|
|
||||||
const currentId = d.id
|
|
||||||
const linkNodes = d3
|
|
||||||
.selectAll(".link")
|
|
||||||
.filter((d: any) => d.source.id === currentId || d.target.id === currentId)
|
|
||||||
|
|
||||||
if (focusOnHover) {
|
|
||||||
// fade out non-neighbour nodes
|
|
||||||
connectedNodes = linkNodes.data().flatMap((d: any) => [d.source.id, d.target.id])
|
|
||||||
|
|
||||||
d3.selectAll<HTMLElement, NodeData>(".link")
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.style("opacity", 0.2)
|
|
||||||
d3.selectAll<HTMLElement, NodeData>(".node")
|
|
||||||
.filter((d) => !connectedNodes.includes(d.id))
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.style("opacity", 0.2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// highlight links
|
for (const l of linkRenderData) {
|
||||||
linkNodes.transition().duration(200).attr("stroke", "var(--gray)").attr("stroke-width", 1)
|
l.active = false
|
||||||
|
|
||||||
const bigFont = fontSize * 1.5
|
|
||||||
|
|
||||||
// show text for self
|
|
||||||
const parent = this.parentNode as HTMLElement
|
|
||||||
d3.select<HTMLElement, NodeData>(parent)
|
|
||||||
.raise()
|
|
||||||
.select("text")
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.attr("opacityOld", d3.select(parent).select("text").style("opacity"))
|
|
||||||
.style("opacity", 1)
|
|
||||||
.style("font-size", bigFont + "em")
|
|
||||||
})
|
|
||||||
.on("mouseleave", function (_, d) {
|
|
||||||
if (focusOnHover) {
|
|
||||||
d3.selectAll<HTMLElement, NodeData>(".link").transition().duration(200).style("opacity", 1)
|
|
||||||
d3.selectAll<HTMLElement, NodeData>(".node").transition().duration(200).style("opacity", 1)
|
|
||||||
}
|
}
|
||||||
const currentId = d.id
|
} else {
|
||||||
const linkNodes = d3
|
hoveredNeighbours = new Set()
|
||||||
.selectAll(".link")
|
for (const l of linkRenderData) {
|
||||||
.filter((d: any) => d.source.id === currentId || d.target.id === currentId)
|
const linkData = l.simulationData
|
||||||
|
if (linkData.source.id === newHoveredId || linkData.target.id === newHoveredId) {
|
||||||
|
hoveredNeighbours.add(linkData.source.id)
|
||||||
|
hoveredNeighbours.add(linkData.target.id)
|
||||||
|
}
|
||||||
|
|
||||||
linkNodes.transition().duration(200).attr("stroke", "var(--lightgray)")
|
l.active = linkData.source.id === newHoveredId || linkData.target.id === newHoveredId
|
||||||
|
}
|
||||||
|
|
||||||
const parent = this.parentNode as HTMLElement
|
for (const n of nodeRenderData) {
|
||||||
d3.select<HTMLElement, NodeData>(parent)
|
n.active = hoveredNeighbours.has(n.simulationData.id)
|
||||||
.select("text")
|
}
|
||||||
.transition()
|
}
|
||||||
.duration(200)
|
}
|
||||||
.style("opacity", d3.select(parent).select("text").attr("opacityOld"))
|
|
||||||
.style("font-size", fontSize + "em")
|
let dragStartTime = 0
|
||||||
|
let dragging = false
|
||||||
|
|
||||||
|
function renderLinks() {
|
||||||
|
tweens.get("link")?.stop()
|
||||||
|
const tweenGroup = new TweenGroup()
|
||||||
|
|
||||||
|
for (const l of linkRenderData) {
|
||||||
|
let alpha = 1
|
||||||
|
|
||||||
|
// if we are hovering over a node, we want to highlight the immediate neighbours
|
||||||
|
// with full alpha and the rest with default alpha
|
||||||
|
if (hoveredNodeId) {
|
||||||
|
alpha = l.active ? 1 : 0.2
|
||||||
|
}
|
||||||
|
|
||||||
|
l.color = l.active ? computedStyleMap["--gray"] : computedStyleMap["--lightgray"]
|
||||||
|
tweenGroup.add(new Tweened<LinkRenderData>(l).to({ alpha }, 200))
|
||||||
|
}
|
||||||
|
|
||||||
|
tweenGroup.getAll().forEach((tw) => tw.start())
|
||||||
|
tweens.set("link", {
|
||||||
|
update: tweenGroup.update.bind(tweenGroup),
|
||||||
|
stop() {
|
||||||
|
tweenGroup.getAll().forEach((tw) => tw.stop())
|
||||||
|
},
|
||||||
})
|
})
|
||||||
// @ts-ignore
|
}
|
||||||
.call(drag(simulation))
|
|
||||||
|
|
||||||
// draw labels
|
function renderLabels() {
|
||||||
const labels = graphNode
|
tweens.get("label")?.stop()
|
||||||
.append("text")
|
const tweenGroup = new TweenGroup()
|
||||||
.attr("dx", 0)
|
|
||||||
.attr("dy", (d) => -nodeRadius(d) + "px")
|
const defaultScale = 1 / scale
|
||||||
.attr("text-anchor", "middle")
|
const activeScale = defaultScale * 1.1
|
||||||
.text((d) => d.text)
|
for (const n of nodeRenderData) {
|
||||||
.style("opacity", (opacityScale - 1) / 3.75)
|
const nodeId = n.simulationData.id
|
||||||
.style("pointer-events", "none")
|
|
||||||
.style("font-size", fontSize + "em")
|
if (hoveredNodeId === nodeId) {
|
||||||
.raise()
|
tweenGroup.add(
|
||||||
// @ts-ignore
|
new Tweened<Text>(n.label).to(
|
||||||
.call(drag(simulation))
|
{
|
||||||
|
alpha: 1,
|
||||||
|
scale: { x: activeScale, y: activeScale },
|
||||||
|
},
|
||||||
|
100,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
tweenGroup.add(
|
||||||
|
new Tweened<Text>(n.label).to(
|
||||||
|
{
|
||||||
|
alpha: n.label.alpha,
|
||||||
|
scale: { x: defaultScale, y: defaultScale },
|
||||||
|
},
|
||||||
|
100,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tweenGroup.getAll().forEach((tw) => tw.start())
|
||||||
|
tweens.set("label", {
|
||||||
|
update: tweenGroup.update.bind(tweenGroup),
|
||||||
|
stop() {
|
||||||
|
tweenGroup.getAll().forEach((tw) => tw.stop())
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNodes() {
|
||||||
|
tweens.get("hover")?.stop()
|
||||||
|
|
||||||
|
const tweenGroup = new TweenGroup()
|
||||||
|
for (const n of nodeRenderData) {
|
||||||
|
let alpha = 1
|
||||||
|
|
||||||
|
// if we are hovering over a node, we want to highlight the immediate neighbours
|
||||||
|
if (hoveredNodeId !== null && focusOnHover) {
|
||||||
|
alpha = n.active ? 1 : 0.2
|
||||||
|
}
|
||||||
|
|
||||||
|
tweenGroup.add(new Tweened<Graphics>(n.gfx, tweenGroup).to({ alpha }, 200))
|
||||||
|
}
|
||||||
|
|
||||||
|
tweenGroup.getAll().forEach((tw) => tw.start())
|
||||||
|
tweens.set("hover", {
|
||||||
|
update: tweenGroup.update.bind(tweenGroup),
|
||||||
|
stop() {
|
||||||
|
tweenGroup.getAll().forEach((tw) => tw.stop())
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPixiFromD3() {
|
||||||
|
renderNodes()
|
||||||
|
renderLinks()
|
||||||
|
renderLabels()
|
||||||
|
}
|
||||||
|
|
||||||
|
tweens.forEach((tween) => tween.stop())
|
||||||
|
tweens.clear()
|
||||||
|
|
||||||
|
const app = new Application()
|
||||||
|
await app.init({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
antialias: true,
|
||||||
|
autoStart: false,
|
||||||
|
autoDensity: true,
|
||||||
|
backgroundAlpha: 0,
|
||||||
|
preference: "webgpu",
|
||||||
|
resolution: window.devicePixelRatio,
|
||||||
|
eventMode: "static",
|
||||||
|
})
|
||||||
|
graph.appendChild(app.canvas)
|
||||||
|
|
||||||
|
const stage = app.stage
|
||||||
|
stage.interactive = false
|
||||||
|
|
||||||
|
const labelsContainer = new Container<Text>({ zIndex: 3 })
|
||||||
|
const nodesContainer = new Container<Graphics>({ zIndex: 2 })
|
||||||
|
const linkContainer = new Container<Graphics>({ zIndex: 1 })
|
||||||
|
stage.addChild(nodesContainer, labelsContainer, linkContainer)
|
||||||
|
|
||||||
|
for (const n of graphData.nodes) {
|
||||||
|
const nodeId = n.id
|
||||||
|
|
||||||
|
const label = new Text({
|
||||||
|
interactive: false,
|
||||||
|
eventMode: "none",
|
||||||
|
text: n.text,
|
||||||
|
alpha: 0,
|
||||||
|
anchor: { x: 0.5, y: 1.2 },
|
||||||
|
style: {
|
||||||
|
fontSize: fontSize * 15,
|
||||||
|
fill: computedStyleMap["--dark"],
|
||||||
|
fontFamily: computedStyleMap["--bodyFont"],
|
||||||
|
},
|
||||||
|
resolution: window.devicePixelRatio * 4,
|
||||||
|
})
|
||||||
|
label.scale.set(1 / scale)
|
||||||
|
|
||||||
|
let oldLabelOpacity = 0
|
||||||
|
const isTagNode = nodeId.startsWith("tags/")
|
||||||
|
const gfx = new Graphics({
|
||||||
|
interactive: true,
|
||||||
|
label: nodeId,
|
||||||
|
eventMode: "static",
|
||||||
|
hitArea: new Circle(0, 0, nodeRadius(n)),
|
||||||
|
cursor: "pointer",
|
||||||
|
})
|
||||||
|
.circle(0, 0, nodeRadius(n))
|
||||||
|
.fill({ color: isTagNode ? computedStyleMap["--light"] : color(n) })
|
||||||
|
.stroke({ width: isTagNode ? 2 : 0, color: color(n) })
|
||||||
|
.on("pointerover", (e) => {
|
||||||
|
updateHoverInfo(e.target.label)
|
||||||
|
oldLabelOpacity = label.alpha
|
||||||
|
if (!dragging) {
|
||||||
|
renderPixiFromD3()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on("pointerleave", () => {
|
||||||
|
updateHoverInfo(null)
|
||||||
|
label.alpha = oldLabelOpacity
|
||||||
|
if (!dragging) {
|
||||||
|
renderPixiFromD3()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
nodesContainer.addChild(gfx)
|
||||||
|
labelsContainer.addChild(label)
|
||||||
|
|
||||||
|
const nodeRenderDatum: NodeRenderData = {
|
||||||
|
simulationData: n,
|
||||||
|
gfx,
|
||||||
|
label,
|
||||||
|
color: color(n),
|
||||||
|
alpha: 1,
|
||||||
|
active: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeRenderData.push(nodeRenderDatum)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const l of graphData.links) {
|
||||||
|
const gfx = new Graphics({ interactive: false, eventMode: "none" })
|
||||||
|
linkContainer.addChild(gfx)
|
||||||
|
|
||||||
|
const linkRenderDatum: LinkRenderData = {
|
||||||
|
simulationData: l,
|
||||||
|
gfx,
|
||||||
|
color: computedStyleMap["--lightgray"],
|
||||||
|
alpha: 1,
|
||||||
|
active: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
linkRenderData.push(linkRenderDatum)
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentTransform = zoomIdentity
|
||||||
|
if (enableDrag) {
|
||||||
|
select<HTMLCanvasElement, NodeData | undefined>(app.canvas).call(
|
||||||
|
drag<HTMLCanvasElement, NodeData | undefined>()
|
||||||
|
.container(() => app.canvas)
|
||||||
|
.subject(() => graphData.nodes.find((n) => n.id === hoveredNodeId))
|
||||||
|
.on("start", function dragstarted(event) {
|
||||||
|
if (!event.active) simulation.alphaTarget(1).restart()
|
||||||
|
event.subject.fx = event.subject.x
|
||||||
|
event.subject.fy = event.subject.y
|
||||||
|
event.subject.__initialDragPos = {
|
||||||
|
x: event.subject.x,
|
||||||
|
y: event.subject.y,
|
||||||
|
fx: event.subject.fx,
|
||||||
|
fy: event.subject.fy,
|
||||||
|
}
|
||||||
|
dragStartTime = Date.now()
|
||||||
|
dragging = true
|
||||||
|
})
|
||||||
|
.on("drag", function dragged(event) {
|
||||||
|
const initPos = event.subject.__initialDragPos
|
||||||
|
event.subject.fx = initPos.x + (event.x - initPos.x) / currentTransform.k
|
||||||
|
event.subject.fy = initPos.y + (event.y - initPos.y) / currentTransform.k
|
||||||
|
})
|
||||||
|
.on("end", function dragended(event) {
|
||||||
|
if (!event.active) simulation.alphaTarget(0)
|
||||||
|
event.subject.fx = null
|
||||||
|
event.subject.fy = null
|
||||||
|
dragging = false
|
||||||
|
|
||||||
|
// if the time between mousedown and mouseup is short, we consider it a click
|
||||||
|
if (Date.now() - dragStartTime < 500) {
|
||||||
|
const node = graphData.nodes.find((n) => n.id === event.subject.id) as NodeData
|
||||||
|
const targ = resolveRelative(fullSlug, node.id)
|
||||||
|
window.spaNavigate(new URL(targ, window.location.toString()))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
for (const node of nodeRenderData) {
|
||||||
|
node.gfx.on("click", () => {
|
||||||
|
const targ = resolveRelative(fullSlug, node.simulationData.id)
|
||||||
|
window.spaNavigate(new URL(targ, window.location.toString()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// set panning
|
|
||||||
if (enableZoom) {
|
if (enableZoom) {
|
||||||
svg.call(
|
select<HTMLCanvasElement, NodeData>(app.canvas).call(
|
||||||
d3
|
zoom<HTMLCanvasElement, NodeData>()
|
||||||
.zoom<SVGSVGElement, NodeData>()
|
|
||||||
.extent([
|
.extent([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
[width, height],
|
[width, height],
|
||||||
])
|
])
|
||||||
.scaleExtent([0.25, 4])
|
.scaleExtent([0.25, 4])
|
||||||
.on("zoom", ({ transform }) => {
|
.on("zoom", ({ transform }) => {
|
||||||
link.attr("transform", transform)
|
currentTransform = transform
|
||||||
node.attr("transform", transform)
|
stage.scale.set(transform.k, transform.k)
|
||||||
|
stage.position.set(transform.x, transform.y)
|
||||||
|
|
||||||
|
// zoom adjusts opacity of labels too
|
||||||
const scale = transform.k * opacityScale
|
const scale = transform.k * opacityScale
|
||||||
const scaledOpacity = Math.max((scale - 1) / 3.75, 0)
|
let scaleOpacity = Math.max((scale - 1) / 3.75, 0)
|
||||||
labels.attr("transform", transform).style("opacity", scaledOpacity)
|
const activeNodes = nodeRenderData.filter((n) => n.active).flatMap((n) => n.label)
|
||||||
|
|
||||||
|
for (const label of labelsContainer.children) {
|
||||||
|
if (!activeNodes.includes(label)) {
|
||||||
|
label.alpha = scaleOpacity
|
||||||
|
}
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// progress the simulation
|
function animate(time: number) {
|
||||||
simulation.on("tick", () => {
|
for (const n of nodeRenderData) {
|
||||||
link
|
const { x, y } = n.simulationData
|
||||||
.attr("x1", (d: any) => d.source.x)
|
if (!x || !y) continue
|
||||||
.attr("y1", (d: any) => d.source.y)
|
n.gfx.position.set(x + width / 2, y + height / 2)
|
||||||
.attr("x2", (d: any) => d.target.x)
|
if (n.label) {
|
||||||
.attr("y2", (d: any) => d.target.y)
|
n.label.position.set(x + width / 2, y + height / 2)
|
||||||
node.attr("cx", (d: any) => d.x).attr("cy", (d: any) => d.y)
|
}
|
||||||
labels.attr("x", (d: any) => d.x).attr("y", (d: any) => d.y)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderGlobalGraph() {
|
|
||||||
const slug = getFullSlug(window)
|
|
||||||
const container = document.getElementById("global-graph-outer")
|
|
||||||
const sidebar = container?.closest(".sidebar") as HTMLElement
|
|
||||||
container?.classList.add("active")
|
|
||||||
if (sidebar) {
|
|
||||||
sidebar.style.zIndex = "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
renderGraph("global-graph-container", slug)
|
|
||||||
|
|
||||||
function hideGlobalGraph() {
|
|
||||||
container?.classList.remove("active")
|
|
||||||
const graph = document.getElementById("global-graph-container")
|
|
||||||
if (sidebar) {
|
|
||||||
sidebar.style.zIndex = "unset"
|
|
||||||
}
|
}
|
||||||
if (!graph) return
|
|
||||||
removeAllChildren(graph)
|
for (const l of linkRenderData) {
|
||||||
|
const linkData = l.simulationData
|
||||||
|
l.gfx.clear()
|
||||||
|
l.gfx.moveTo(linkData.source.x! + width / 2, linkData.source.y! + height / 2)
|
||||||
|
l.gfx
|
||||||
|
.lineTo(linkData.target.x! + width / 2, linkData.target.y! + height / 2)
|
||||||
|
.stroke({ alpha: l.alpha, width: 1, color: l.color })
|
||||||
|
}
|
||||||
|
|
||||||
|
tweens.forEach((t) => t.update(time))
|
||||||
|
app.renderer.render(stage)
|
||||||
|
requestAnimationFrame(animate)
|
||||||
}
|
}
|
||||||
|
|
||||||
registerEscapeHandler(container, hideGlobalGraph)
|
const graphAnimationFrameHandle = requestAnimationFrame(animate)
|
||||||
|
window.addCleanup(() => cancelAnimationFrame(graphAnimationFrameHandle))
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||||
const slug = e.detail.url
|
const slug = e.detail.url
|
||||||
addToVisited(slug)
|
addToVisited(simplifySlug(slug))
|
||||||
await renderGraph("graph-container", slug)
|
await renderGraph("graph-container", slug)
|
||||||
|
|
||||||
|
// Function to re-render the graph when the theme changes
|
||||||
|
const handleThemeChange = () => {
|
||||||
|
renderGraph("graph-container", slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
// event listener for theme change
|
||||||
|
document.addEventListener("themechange", handleThemeChange)
|
||||||
|
|
||||||
|
// cleanup for the event listener
|
||||||
|
window.addCleanup(() => {
|
||||||
|
document.removeEventListener("themechange", handleThemeChange)
|
||||||
|
})
|
||||||
|
|
||||||
|
const container = document.getElementById("global-graph-outer")
|
||||||
|
const sidebar = container?.closest(".sidebar") as HTMLElement
|
||||||
|
|
||||||
|
function renderGlobalGraph() {
|
||||||
|
const slug = getFullSlug(window)
|
||||||
|
container?.classList.add("active")
|
||||||
|
if (sidebar) {
|
||||||
|
sidebar.style.zIndex = "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
renderGraph("global-graph-container", slug)
|
||||||
|
registerEscapeHandler(container, hideGlobalGraph)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideGlobalGraph() {
|
||||||
|
container?.classList.remove("active")
|
||||||
|
if (sidebar) {
|
||||||
|
sidebar.style.zIndex = "unset"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
|
||||||
|
if (e.key === "g" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
const globalGraphOpen = container?.classList.contains("active")
|
||||||
|
globalGraphOpen ? hideGlobalGraph() : renderGlobalGraph()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const containerIcon = document.getElementById("global-graph-icon")
|
const containerIcon = document.getElementById("global-graph-icon")
|
||||||
containerIcon?.addEventListener("click", renderGlobalGraph)
|
containerIcon?.addEventListener("click", renderGlobalGraph)
|
||||||
window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph))
|
window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph))
|
||||||
|
|
||||||
|
document.addEventListener("keydown", shortcutHandler)
|
||||||
|
window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler))
|
||||||
})
|
})
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { normalizeRelativeURLs } from "../../util/path"
|
|||||||
|
|
||||||
const p = new DOMParser()
|
const p = new DOMParser()
|
||||||
async function mouseEnterHandler(
|
async function mouseEnterHandler(
|
||||||
this: HTMLLinkElement,
|
this: HTMLAnchorElement,
|
||||||
{ clientX, clientY }: { clientX: number; clientY: number },
|
{ clientX, clientY }: { clientX: number; clientY: number },
|
||||||
) {
|
) {
|
||||||
const link = this
|
const link = this
|
||||||
@ -33,7 +33,7 @@ async function mouseEnterHandler(
|
|||||||
thisUrl.hash = ""
|
thisUrl.hash = ""
|
||||||
thisUrl.search = ""
|
thisUrl.search = ""
|
||||||
const targetUrl = new URL(link.href)
|
const targetUrl = new URL(link.href)
|
||||||
const hash = targetUrl.hash
|
const hash = decodeURIComponent(targetUrl.hash)
|
||||||
targetUrl.hash = ""
|
targetUrl.hash = ""
|
||||||
targetUrl.search = ""
|
targetUrl.search = ""
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ async function mouseEnterHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("nav", () => {
|
document.addEventListener("nav", () => {
|
||||||
const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[]
|
const links = [...document.getElementsByClassName("internal")] as HTMLAnchorElement[]
|
||||||
for (const link of links) {
|
for (const link of links) {
|
||||||
link.addEventListener("mouseenter", mouseEnterHandler)
|
link.addEventListener("mouseenter", mouseEnterHandler)
|
||||||
window.addCleanup(() => link.removeEventListener("mouseenter", mouseEnterHandler))
|
window.addCleanup(() => link.removeEventListener("mouseenter", mouseEnterHandler))
|
||||||
|
|||||||
@ -148,7 +148,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
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 searchButton = document.getElementById("search-button")
|
||||||
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 searchLayout = document.getElementById("search-layout")
|
||||||
const idDataMap = Object.keys(data) as FullSlug[]
|
const idDataMap = Object.keys(data) as FullSlug[]
|
||||||
@ -191,6 +191,8 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
searchType = "basic" // reset search type after closing
|
searchType = "basic" // reset search type after closing
|
||||||
|
|
||||||
|
searchButton?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSearch(searchTypeNew: SearchType) {
|
function showSearch(searchTypeNew: SearchType) {
|
||||||
@ -458,8 +460,8 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
|
|
||||||
document.addEventListener("keydown", shortcutHandler)
|
document.addEventListener("keydown", shortcutHandler)
|
||||||
window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler))
|
window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler))
|
||||||
searchIcon?.addEventListener("click", () => showSearch("basic"))
|
searchButton?.addEventListener("click", () => showSearch("basic"))
|
||||||
window.addCleanup(() => searchIcon?.removeEventListener("click", () => showSearch("basic")))
|
window.addCleanup(() => searchButton?.removeEventListener("click", () => showSearch("basic")))
|
||||||
searchBar?.addEventListener("input", onType)
|
searchBar?.addEventListener("input", onType)
|
||||||
window.addCleanup(() => searchBar?.removeEventListener("input", onType))
|
window.addCleanup(() => searchBar?.removeEventListener("input", onType))
|
||||||
|
|
||||||
|
|||||||
@ -16,10 +16,13 @@ const observer = new IntersectionObserver((entries) => {
|
|||||||
|
|
||||||
function toggleToc(this: HTMLElement) {
|
function toggleToc(this: HTMLElement) {
|
||||||
this.classList.toggle("collapsed")
|
this.classList.toggle("collapsed")
|
||||||
|
this.setAttribute(
|
||||||
|
"aria-expanded",
|
||||||
|
this.getAttribute("aria-expanded") === "true" ? "false" : "true",
|
||||||
|
)
|
||||||
const content = this.nextElementSibling as HTMLElement | undefined
|
const content = this.nextElementSibling as HTMLElement | undefined
|
||||||
if (!content) return
|
if (!content) return
|
||||||
content.classList.toggle("collapsed")
|
content.classList.toggle("collapsed")
|
||||||
content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupToc() {
|
function setupToc() {
|
||||||
@ -28,7 +31,6 @@ function setupToc() {
|
|||||||
const collapsed = toc.classList.contains("collapsed")
|
const collapsed = toc.classList.contains("collapsed")
|
||||||
const content = toc.nextElementSibling as HTMLElement | undefined
|
const content = toc.nextElementSibling as HTMLElement | undefined
|
||||||
if (!content) return
|
if (!content) return
|
||||||
content.style.maxHeight = collapsed ? "0px" : content.scrollHeight + "px"
|
|
||||||
toc.addEventListener("click", toggleToc)
|
toc.addEventListener("click", toggleToc)
|
||||||
window.addCleanup(() => toc.removeEventListener("click", toggleToc))
|
window.addCleanup(() => toc.removeEventListener("click", toggleToc))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb:
|
|||||||
function click(this: HTMLElement, e: HTMLElementEventMap["click"]) {
|
function click(this: HTMLElement, e: HTMLElementEventMap["click"]) {
|
||||||
if (e.target !== this) return
|
if (e.target !== this) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +1,43 @@
|
|||||||
|
@use "../../styles/variables.scss" as *;
|
||||||
|
|
||||||
.backlinks {
|
.backlinks {
|
||||||
position: relative;
|
@media all and not ($mobile) {
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
&:after {
|
||||||
|
pointer-events: none;
|
||||||
|
content: "";
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
background: linear-gradient(transparent 0px, var(--light));
|
||||||
|
}
|
||||||
|
|
||||||
& > h3 {
|
& > h3 {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > ul {
|
& > ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
|
|
||||||
& > li {
|
& > li {
|
||||||
& > a {
|
& > a {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .overflow {
|
||||||
|
&:after {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,15 @@
|
|||||||
.darkmode {
|
.darkmode {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
|
text-align: inherit;
|
||||||
& > .toggle {
|
|
||||||
display: none;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
& svg {
|
& svg {
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
@ -29,20 +27,20 @@
|
|||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[saved-theme="dark"] .toggle ~ label {
|
:root[saved-theme="dark"] .darkmode {
|
||||||
& > #dayIcon {
|
& > #dayIcon {
|
||||||
opacity: 0;
|
display: none;
|
||||||
}
|
}
|
||||||
& > #nightIcon {
|
& > #nightIcon {
|
||||||
opacity: 1;
|
display: inline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:root .toggle ~ label {
|
:root .darkmode {
|
||||||
& > #dayIcon {
|
& > #dayIcon {
|
||||||
opacity: 1;
|
display: inline;
|
||||||
}
|
}
|
||||||
& > #nightIcon {
|
& > #nightIcon {
|
||||||
opacity: 0;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,29 @@
|
|||||||
@use "../../styles/variables.scss" as *;
|
@use "../../styles/variables.scss" as *;
|
||||||
|
|
||||||
|
.explorer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: hidden;
|
||||||
|
&.desktop-only {
|
||||||
|
@media all and not ($mobile) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
pointer-events: none;
|
||||||
|
content: "";
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
background: linear-gradient(transparent 0px, var(--light));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
button#explorer {
|
button#explorer {
|
||||||
all: unset;
|
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@ -11,7 +33,7 @@ button#explorer {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
& h1 {
|
& h2 {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -45,12 +67,20 @@ button#explorer {
|
|||||||
#explorer-content {
|
#explorer-content {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-height: none;
|
overflow-y: auto;
|
||||||
transition: max-height 0.35s ease;
|
max-height: 100%;
|
||||||
|
transition:
|
||||||
|
max-height 0.35s ease,
|
||||||
|
visibility 0s linear 0s;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
|
visibility: visible;
|
||||||
|
|
||||||
&.collapsed > .overflow::after {
|
&.collapsed {
|
||||||
opacity: 0;
|
max-height: 0;
|
||||||
|
transition:
|
||||||
|
max-height 0.35s ease,
|
||||||
|
visibility 0s linear 0.35s;
|
||||||
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
& ul {
|
& ul {
|
||||||
@ -67,6 +97,9 @@ button#explorer {
|
|||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
> #explorer-ul {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
|
|||||||
@ -16,10 +16,13 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
& > #global-graph-icon {
|
& > #global-graph-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
color: var(--dark);
|
color: var(--dark);
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
width: 18px;
|
width: 24px;
|
||||||
height: 18px;
|
height: 24px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding: 0.2rem;
|
padding: 0.2rem;
|
||||||
margin: 0.3rem;
|
margin: 0.3rem;
|
||||||
@ -59,10 +62,10 @@
|
|||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
height: 60vh;
|
height: 80vh;
|
||||||
width: 50vw;
|
width: 80vw;
|
||||||
|
|
||||||
@media all and (max-width: $fullPageWidth) {
|
@media all and ($desktop) {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,9 +11,9 @@ li.section-li {
|
|||||||
|
|
||||||
& > .section {
|
& > .section {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 6em 3fr 1fr;
|
grid-template-columns: fit-content(8em) 3fr 1fr;
|
||||||
|
|
||||||
@media all and (max-width: $mobileBreakpoint) {
|
@media all and ($mobile) {
|
||||||
& > .tags {
|
& > .tags {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -23,9 +23,8 @@ li.section-li {
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .meta {
|
& .meta {
|
||||||
margin: 0;
|
margin: 0 1em 0 0;
|
||||||
flex-basis: 6em;
|
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -33,7 +32,8 @@ li.section-li {
|
|||||||
|
|
||||||
// modifications in popover context
|
// modifications in popover context
|
||||||
.popover .section {
|
.popover .section {
|
||||||
grid-template-columns: 6em 1fr !important;
|
grid-template-columns: fit-content(8em) 1fr !important;
|
||||||
|
|
||||||
& > .tags {
|
& > .tags {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -70,7 +70,7 @@
|
|||||||
opacity 0.3s ease,
|
opacity 0.3s ease,
|
||||||
visibility 0.3s ease;
|
visibility 0.3s ease;
|
||||||
|
|
||||||
@media all and (max-width: $mobileBreakpoint) {
|
@media all and ($mobile) {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,20 +3,25 @@
|
|||||||
.search {
|
.search {
|
||||||
min-width: fit-content;
|
min-width: fit-content;
|
||||||
max-width: 14rem;
|
max-width: 14rem;
|
||||||
flex-grow: 0.3;
|
@media all and ($mobile) {
|
||||||
|
flex-grow: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
& > #search-icon {
|
& > .search-button {
|
||||||
background-color: var(--lightgray);
|
background-color: var(--lightgray);
|
||||||
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
|
padding: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
text-align: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
& > div {
|
justify-content: space-between;
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > p {
|
& > p {
|
||||||
display: inline;
|
display: inline;
|
||||||
@ -59,7 +64,7 @@
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
|
||||||
@media all and (max-width: $fullPageWidth) {
|
@media all and ($desktop) {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,7 +106,7 @@
|
|||||||
flex: 0 0 min(30%, 450px);
|
flex: 0 0 min(30%, 450px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (min-width: $tabletBreakpoint) {
|
@media all and not ($tablet) {
|
||||||
&[data-preview] {
|
&[data-preview] {
|
||||||
& .result-card > p.preview {
|
& .result-card > p.preview {
|
||||||
display: none;
|
display: none;
|
||||||
@ -127,7 +132,7 @@
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (max-width: $tabletBreakpoint) {
|
@media all and ($tablet) {
|
||||||
& > #preview-container {
|
& > #preview-container {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,12 @@
|
|||||||
|
.toc {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
&.desktop-only {
|
||||||
|
display: flex;
|
||||||
|
max-height: 40%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
button#toc {
|
button#toc {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
@ -28,9 +37,21 @@ button#toc {
|
|||||||
#toc-content {
|
#toc-content {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-height: none;
|
overflow-y: auto;
|
||||||
transition: max-height 0.5s ease;
|
max-height: 100%;
|
||||||
|
transition:
|
||||||
|
max-height 0.35s ease,
|
||||||
|
visibility 0s linear 0s;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
visibility: visible;
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
|
max-height: 0;
|
||||||
|
transition:
|
||||||
|
max-height 0.35s ease,
|
||||||
|
visibility 0s linear 0.35s;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
&.collapsed > .overflow::after {
|
&.collapsed > .overflow::after {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@ -51,6 +72,9 @@ button#toc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
> ul.overflow {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
@for $i from 0 through 6 {
|
@for $i from 0 through 6 {
|
||||||
& .depth-#{$i} {
|
& .depth-#{$i} {
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import { Translation, CalloutTranslation } from "./locales/definition"
|
import { Translation, CalloutTranslation } from "./locales/definition"
|
||||||
import en from "./locales/en-US"
|
import enUs from "./locales/en-US"
|
||||||
|
import enGb from "./locales/en-GB"
|
||||||
import fr from "./locales/fr-FR"
|
import fr from "./locales/fr-FR"
|
||||||
import it from "./locales/it-IT"
|
import it from "./locales/it-IT"
|
||||||
import ja from "./locales/ja-JP"
|
import ja from "./locales/ja-JP"
|
||||||
import de from "./locales/de-DE"
|
import de from "./locales/de-DE"
|
||||||
import nl from "./locales/nl-NL"
|
import nl from "./locales/nl-NL"
|
||||||
import ro from "./locales/ro-RO"
|
import ro from "./locales/ro-RO"
|
||||||
|
import ca from "./locales/ca-ES"
|
||||||
import es from "./locales/es-ES"
|
import es from "./locales/es-ES"
|
||||||
import ar from "./locales/ar-SA"
|
import ar from "./locales/ar-SA"
|
||||||
import uk from "./locales/uk-UA"
|
import uk from "./locales/uk-UA"
|
||||||
@ -15,9 +17,12 @@ import zh from "./locales/zh-CN"
|
|||||||
import vi from "./locales/vi-VN"
|
import vi from "./locales/vi-VN"
|
||||||
import pt from "./locales/pt-BR"
|
import pt from "./locales/pt-BR"
|
||||||
import hu from "./locales/hu-HU"
|
import hu from "./locales/hu-HU"
|
||||||
|
import fa from "./locales/fa-IR"
|
||||||
|
import pl from "./locales/pl-PL"
|
||||||
|
|
||||||
export const TRANSLATIONS = {
|
export const TRANSLATIONS = {
|
||||||
"en-US": en,
|
"en-US": enUs,
|
||||||
|
"en-GB": enGb,
|
||||||
"fr-FR": fr,
|
"fr-FR": fr,
|
||||||
"it-IT": it,
|
"it-IT": it,
|
||||||
"ja-JP": ja,
|
"ja-JP": ja,
|
||||||
@ -26,6 +31,7 @@ export const TRANSLATIONS = {
|
|||||||
"nl-BE": nl,
|
"nl-BE": nl,
|
||||||
"ro-RO": ro,
|
"ro-RO": ro,
|
||||||
"ro-MD": ro,
|
"ro-MD": ro,
|
||||||
|
"ca-ES": ca,
|
||||||
"es-ES": es,
|
"es-ES": es,
|
||||||
"ar-SA": ar,
|
"ar-SA": ar,
|
||||||
"ar-AE": ar,
|
"ar-AE": ar,
|
||||||
@ -54,6 +60,8 @@ export const TRANSLATIONS = {
|
|||||||
"vi-VN": vi,
|
"vi-VN": vi,
|
||||||
"pt-BR": pt,
|
"pt-BR": pt,
|
||||||
"hu-HU": hu,
|
"hu-HU": hu,
|
||||||
|
"fa-IR": fa,
|
||||||
|
"pl-PL": pl,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const defaultTranslation = "en-US"
|
export const defaultTranslation = "en-US"
|
||||||
|
|||||||
@ -70,6 +70,7 @@ export default {
|
|||||||
error: {
|
error: {
|
||||||
title: "غير موجود",
|
title: "غير موجود",
|
||||||
notFound: "إما أن هذه الصفحة خاصة أو غير موجودة.",
|
notFound: "إما أن هذه الصفحة خاصة أو غير موجودة.",
|
||||||
|
home: "العوده للصفحة الرئيسية",
|
||||||
},
|
},
|
||||||
folderContent: {
|
folderContent: {
|
||||||
folder: "مجلد",
|
folder: "مجلد",
|
||||||
|
|||||||
84
quartz/i18n/locales/ca-ES.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { Translation } from "./definition"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
propertyDefaults: {
|
||||||
|
title: "Sense títol",
|
||||||
|
description: "Sense descripció",
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
callout: {
|
||||||
|
note: "Nota",
|
||||||
|
abstract: "Resum",
|
||||||
|
info: "Informació",
|
||||||
|
todo: "Per fer",
|
||||||
|
tip: "Consell",
|
||||||
|
success: "Èxit",
|
||||||
|
question: "Pregunta",
|
||||||
|
warning: "Advertència",
|
||||||
|
failure: "Fall",
|
||||||
|
danger: "Perill",
|
||||||
|
bug: "Error",
|
||||||
|
example: "Exemple",
|
||||||
|
quote: "Cita",
|
||||||
|
},
|
||||||
|
backlinks: {
|
||||||
|
title: "Retroenllaç",
|
||||||
|
noBacklinksFound: "No s'han trobat retroenllaços",
|
||||||
|
},
|
||||||
|
themeToggle: {
|
||||||
|
lightMode: "Mode clar",
|
||||||
|
darkMode: "Mode fosc",
|
||||||
|
},
|
||||||
|
explorer: {
|
||||||
|
title: "Explorador",
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
createdWith: "Creat amb",
|
||||||
|
},
|
||||||
|
graph: {
|
||||||
|
title: "Vista Gràfica",
|
||||||
|
},
|
||||||
|
recentNotes: {
|
||||||
|
title: "Notes Recents",
|
||||||
|
seeRemainingMore: ({ remaining }) => `Vegi ${remaining} més →`,
|
||||||
|
},
|
||||||
|
transcludes: {
|
||||||
|
transcludeOf: ({ targetSlug }) => `Transcluit de ${targetSlug}`,
|
||||||
|
linkToOriginal: "Enllaç a l'original",
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
title: "Cercar",
|
||||||
|
searchBarPlaceholder: "Cerca alguna cosa",
|
||||||
|
},
|
||||||
|
tableOfContents: {
|
||||||
|
title: "Taula de Continguts",
|
||||||
|
},
|
||||||
|
contentMeta: {
|
||||||
|
readingTime: ({ minutes }) => `Es llegeix en ${minutes} min`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
rss: {
|
||||||
|
recentNotes: "Notes recents",
|
||||||
|
lastFewNotes: ({ count }) => `Últimes ${count} notes`,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
title: "No s'ha trobat.",
|
||||||
|
notFound: "Aquesta pàgina és privada o no existeix.",
|
||||||
|
home: "Torna a la pàgina principal",
|
||||||
|
},
|
||||||
|
folderContent: {
|
||||||
|
folder: "Carpeta",
|
||||||
|
itemsUnderFolder: ({ count }) =>
|
||||||
|
count === 1 ? "1 article en aquesta carpeta." : `${count} articles en esta carpeta.`,
|
||||||
|
},
|
||||||
|
tagContent: {
|
||||||
|
tag: "Etiqueta",
|
||||||
|
tagIndex: "índex d'Etiquetes",
|
||||||
|
itemsUnderTag: ({ count }) =>
|
||||||
|
count === 1 ? "1 article amb aquesta etiqueta." : `${count} article amb aquesta etiqueta.`,
|
||||||
|
showingFirst: ({ count }) => `Mostrant les primeres ${count} etiquetes.`,
|
||||||
|
totalTags: ({ count }) => `S'han trobat ${count} etiquetes en total.`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const satisfies Translation
|
||||||
@ -65,6 +65,7 @@ export default {
|
|||||||
error: {
|
error: {
|
||||||
title: "Nicht gefunden",
|
title: "Nicht gefunden",
|
||||||
notFound: "Diese Seite ist entweder nicht öffentlich oder existiert nicht.",
|
notFound: "Diese Seite ist entweder nicht öffentlich oder existiert nicht.",
|
||||||
|
home: "Return to Homepage",
|
||||||
},
|
},
|
||||||
folderContent: {
|
folderContent: {
|
||||||
folder: "Ordner",
|
folder: "Ordner",
|
||||||
|
|||||||
@ -67,6 +67,7 @@ export interface Translation {
|
|||||||
error: {
|
error: {
|
||||||
title: string
|
title: string
|
||||||
notFound: string
|
notFound: string
|
||||||
|
home: string
|
||||||
}
|
}
|
||||||
folderContent: {
|
folderContent: {
|
||||||
folder: string
|
folder: string
|
||||||
|
|||||||
84
quartz/i18n/locales/en-GB.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { Translation } from "./definition"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
propertyDefaults: {
|
||||||
|
title: "Untitled",
|
||||||
|
description: "No description provided",
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
callout: {
|
||||||
|
note: "Note",
|
||||||
|
abstract: "Abstract",
|
||||||
|
info: "Info",
|
||||||
|
todo: "To-Do",
|
||||||
|
tip: "Tip",
|
||||||
|
success: "Success",
|
||||||
|
question: "Question",
|
||||||
|
warning: "Warning",
|
||||||
|
failure: "Failure",
|
||||||
|
danger: "Danger",
|
||||||
|
bug: "Bug",
|
||||||
|
example: "Example",
|
||||||
|
quote: "Quote",
|
||||||
|
},
|
||||||
|
backlinks: {
|
||||||
|
title: "Backlinks",
|
||||||
|
noBacklinksFound: "No backlinks found",
|
||||||
|
},
|
||||||
|
themeToggle: {
|
||||||
|
lightMode: "Light mode",
|
||||||
|
darkMode: "Dark mode",
|
||||||
|
},
|
||||||
|
explorer: {
|
||||||
|
title: "Explorer",
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
createdWith: "Created with",
|
||||||
|
},
|
||||||
|
graph: {
|
||||||
|
title: "Graph View",
|
||||||
|
},
|
||||||
|
recentNotes: {
|
||||||
|
title: "Recent Notes",
|
||||||
|
seeRemainingMore: ({ remaining }) => `See ${remaining} more →`,
|
||||||
|
},
|
||||||
|
transcludes: {
|
||||||
|
transcludeOf: ({ targetSlug }) => `Transclude of ${targetSlug}`,
|
||||||
|
linkToOriginal: "Link to original",
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
title: "Search",
|
||||||
|
searchBarPlaceholder: "Search for something",
|
||||||
|
},
|
||||||
|
tableOfContents: {
|
||||||
|
title: "Table of Contents",
|
||||||
|
},
|
||||||
|
contentMeta: {
|
||||||
|
readingTime: ({ minutes }) => `${minutes} min read`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
rss: {
|
||||||
|
recentNotes: "Recent notes",
|
||||||
|
lastFewNotes: ({ count }) => `Last ${count} notes`,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
title: "Not Found",
|
||||||
|
notFound: "Either this page is private or doesn't exist.",
|
||||||
|
home: "Return to Homepage",
|
||||||
|
},
|
||||||
|
folderContent: {
|
||||||
|
folder: "Folder",
|
||||||
|
itemsUnderFolder: ({ count }) =>
|
||||||
|
count === 1 ? "1 item under this folder." : `${count} items under this folder.`,
|
||||||
|
},
|
||||||
|
tagContent: {
|
||||||
|
tag: "Tag",
|
||||||
|
tagIndex: "Tag Index",
|
||||||
|
itemsUnderTag: ({ count }) =>
|
||||||
|
count === 1 ? "1 item with this tag." : `${count} items with this tag.`,
|
||||||
|
showingFirst: ({ count }) => `Showing first ${count} tags.`,
|
||||||
|
totalTags: ({ count }) => `Found ${count} total tags.`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const satisfies Translation
|
||||||
@ -65,6 +65,7 @@ export default {
|
|||||||
error: {
|
error: {
|
||||||
title: "Not Found",
|
title: "Not Found",
|
||||||
notFound: "Either this page is private or doesn't exist.",
|
notFound: "Either this page is private or doesn't exist.",
|
||||||
|
home: "Return to Homepage",
|
||||||
},
|
},
|
||||||
folderContent: {
|
folderContent: {
|
||||||
folder: "Folder",
|
folder: "Folder",
|
||||||
|
|||||||
@ -22,8 +22,8 @@ export default {
|
|||||||
quote: "Cita",
|
quote: "Cita",
|
||||||
},
|
},
|
||||||
backlinks: {
|
backlinks: {
|
||||||
title: "Enlaces de Retroceso",
|
title: "Retroenlaces",
|
||||||
noBacklinksFound: "No se han encontrado enlaces traseros",
|
noBacklinksFound: "No se han encontrado retroenlaces",
|
||||||
},
|
},
|
||||||
themeToggle: {
|
themeToggle: {
|
||||||
lightMode: "Modo claro",
|
lightMode: "Modo claro",
|
||||||
@ -54,17 +54,18 @@ export default {
|
|||||||
title: "Tabla de Contenidos",
|
title: "Tabla de Contenidos",
|
||||||
},
|
},
|
||||||
contentMeta: {
|
contentMeta: {
|
||||||
readingTime: ({ minutes }) => `${minutes} min read`,
|
readingTime: ({ minutes }) => `Se lee en ${minutes} min`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
pages: {
|
pages: {
|
||||||
rss: {
|
rss: {
|
||||||
recentNotes: "Notas recientes",
|
recentNotes: "Notas recientes",
|
||||||
lastFewNotes: ({ count }) => `Últimás ${count} notas`,
|
lastFewNotes: ({ count }) => `Últimas ${count} notas`,
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
title: "No se encontró.",
|
title: "No se ha encontrado.",
|
||||||
notFound: "Esta página es privada o no existe.",
|
notFound: "Esta página es privada o no existe.",
|
||||||
|
home: "Regresa a la página principal",
|
||||||
},
|
},
|
||||||
folderContent: {
|
folderContent: {
|
||||||
folder: "Carpeta",
|
folder: "Carpeta",
|
||||||
@ -77,7 +78,7 @@ export default {
|
|||||||
itemsUnderTag: ({ count }) =>
|
itemsUnderTag: ({ count }) =>
|
||||||
count === 1 ? "1 artículo con esta etiqueta." : `${count} artículos con esta etiqueta.`,
|
count === 1 ? "1 artículo con esta etiqueta." : `${count} artículos con esta etiqueta.`,
|
||||||
showingFirst: ({ count }) => `Mostrando las primeras ${count} etiquetas.`,
|
showingFirst: ({ count }) => `Mostrando las primeras ${count} etiquetas.`,
|
||||||
totalTags: ({ count }) => `Se encontraron ${count} etiquetas en total.`,
|
totalTags: ({ count }) => `Se han encontrado ${count} etiquetas en total.`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const satisfies Translation
|
} as const satisfies Translation
|
||||||
|
|||||||
84
quartz/i18n/locales/fa-IR.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { Translation } from "./definition"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
propertyDefaults: {
|
||||||
|
title: "بدون عنوان",
|
||||||
|
description: "توضیح خاصی اضافه نشده است",
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
callout: {
|
||||||
|
note: "یادداشت",
|
||||||
|
abstract: "چکیده",
|
||||||
|
info: "اطلاعات",
|
||||||
|
todo: "اقدام",
|
||||||
|
tip: "نکته",
|
||||||
|
success: "تیک",
|
||||||
|
question: "سؤال",
|
||||||
|
warning: "هشدار",
|
||||||
|
failure: "شکست",
|
||||||
|
danger: "خطر",
|
||||||
|
bug: "باگ",
|
||||||
|
example: "مثال",
|
||||||
|
quote: "نقل قول",
|
||||||
|
},
|
||||||
|
backlinks: {
|
||||||
|
title: "بکلینکها",
|
||||||
|
noBacklinksFound: "بدون بکلینک",
|
||||||
|
},
|
||||||
|
themeToggle: {
|
||||||
|
lightMode: "حالت روشن",
|
||||||
|
darkMode: "حالت تاریک",
|
||||||
|
},
|
||||||
|
explorer: {
|
||||||
|
title: "مطالب",
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
createdWith: "ساخته شده با",
|
||||||
|
},
|
||||||
|
graph: {
|
||||||
|
title: "نمای گراف",
|
||||||
|
},
|
||||||
|
recentNotes: {
|
||||||
|
title: "یادداشتهای اخیر",
|
||||||
|
seeRemainingMore: ({ remaining }) => `${remaining} یادداشت دیگر →`,
|
||||||
|
},
|
||||||
|
transcludes: {
|
||||||
|
transcludeOf: ({ targetSlug }) => `از ${targetSlug}`,
|
||||||
|
linkToOriginal: "پیوند به اصلی",
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
title: "جستجو",
|
||||||
|
searchBarPlaceholder: "مطلبی را جستجو کنید",
|
||||||
|
},
|
||||||
|
tableOfContents: {
|
||||||
|
title: "فهرست",
|
||||||
|
},
|
||||||
|
contentMeta: {
|
||||||
|
readingTime: ({ minutes }) => `زمان تقریبی مطالعه: ${minutes} دقیقه`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
rss: {
|
||||||
|
recentNotes: "یادداشتهای اخیر",
|
||||||
|
lastFewNotes: ({ count }) => `${count} یادداشت اخیر`,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
title: "یافت نشد",
|
||||||
|
notFound: "این صفحه یا خصوصی است یا وجود ندارد",
|
||||||
|
home: "بازگشت به صفحه اصلی",
|
||||||
|
},
|
||||||
|
folderContent: {
|
||||||
|
folder: "پوشه",
|
||||||
|
itemsUnderFolder: ({ count }) =>
|
||||||
|
count === 1 ? ".یک مطلب در این پوشه است" : `${count} مطلب در این پوشه است.`,
|
||||||
|
},
|
||||||
|
tagContent: {
|
||||||
|
tag: "برچسب",
|
||||||
|
tagIndex: "فهرست برچسبها",
|
||||||
|
itemsUnderTag: ({ count }) =>
|
||||||
|
count === 1 ? "یک مطلب با این برچسب" : `${count} مطلب با این برچسب.`,
|
||||||
|
showingFirst: ({ count }) => `در حال نمایش ${count} برچسب.`,
|
||||||
|
totalTags: ({ count }) => `${count} برچسب یافت شد.`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const satisfies Translation
|
||||||
@ -65,6 +65,7 @@ export default {
|
|||||||
error: {
|
error: {
|
||||||
title: "Introuvable",
|
title: "Introuvable",
|
||||||
notFound: "Cette page est soit privée, soit elle n'existe pas.",
|
notFound: "Cette page est soit privée, soit elle n'existe pas.",
|
||||||
|
home: "Retour à la page d'accueil",
|
||||||
},
|
},
|
||||||
folderContent: {
|
folderContent: {
|
||||||
folder: "Dossier",
|
folder: "Dossier",
|
||||||
|
|||||||
@ -65,6 +65,7 @@ export default {
|
|||||||
error: {
|
error: {
|
||||||
title: "Nem található",
|
title: "Nem található",
|
||||||
notFound: "Ez a lap vagy privát vagy nem létezik.",
|
notFound: "Ez a lap vagy privát vagy nem létezik.",
|
||||||
|
home: "Vissza a kezdőlapra",
|
||||||
},
|
},
|
||||||
folderContent: {
|
folderContent: {
|
||||||
folder: "Mappa",
|
folder: "Mappa",
|
||||||
|
|||||||
@ -65,6 +65,7 @@ export default {
|
|||||||
error: {
|
error: {
|
||||||
title: "Non trovato",
|
title: "Non trovato",
|
||||||
notFound: "Questa pagina è privata o non esiste.",
|
notFound: "Questa pagina è privata o non esiste.",
|
||||||
|
home: "Ritorna alla home page",
|
||||||
},
|
},
|
||||||
folderContent: {
|
folderContent: {
|
||||||
folder: "Cartella",
|
folder: "Cartella",
|
||||||
|
|||||||
@ -65,6 +65,7 @@ export default {
|
|||||||
error: {
|
error: {
|
||||||
title: "Not Found",
|
title: "Not Found",
|
||||||
notFound: "ページが存在しないか、非公開設定になっています。",
|
notFound: "ページが存在しないか、非公開設定になっています。",
|
||||||
|
home: "ホームページに戻る",
|
||||||
},
|
},
|
||||||
folderContent: {
|
folderContent: {
|
||||||
folder: "フォルダ",
|
folder: "フォルダ",
|
||||||
|
|||||||
@ -65,6 +65,7 @@ export default {
|
|||||||
error: {
|
error: {
|
||||||
title: "Not Found",
|
title: "Not Found",
|
||||||
notFound: "페이지가 존재하지 않거나 비공개 설정이 되어 있습니다.",
|
notFound: "페이지가 존재하지 않거나 비공개 설정이 되어 있습니다.",
|
||||||
|
home: "홈페이지로 돌아가기",
|
||||||
},
|
},
|
||||||
folderContent: {
|
folderContent: {
|
||||||
folder: "폴더",
|
folder: "폴더",
|
||||||
|
|||||||
@ -66,6 +66,7 @@ export default {
|
|||||||
error: {
|
error: {
|
||||||
title: "Niet gevonden",
|
title: "Niet gevonden",
|
||||||
notFound: "Deze pagina is niet zichtbaar of bestaat niet.",
|
notFound: "Deze pagina is niet zichtbaar of bestaat niet.",
|
||||||
|
home: "Keer terug naar de start pagina",
|
||||||
},
|
},
|
||||||
folderContent: {
|
folderContent: {
|
||||||
folder: "Map",
|
folder: "Map",
|
||||||
|
|||||||
84
quartz/i18n/locales/pl-PL.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { Translation } from "./definition"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
propertyDefaults: {
|
||||||
|
title: "Bez nazwy",
|
||||||
|
description: "Brak opisu",
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
callout: {
|
||||||
|
note: "Notatka",
|
||||||
|
abstract: "Streszczenie",
|
||||||
|
info: "informacja",
|
||||||
|
todo: "Do zrobienia",
|
||||||
|
tip: "Wskazówka",
|
||||||
|
success: "Zrobione",
|
||||||
|
question: "Pytanie",
|
||||||
|
warning: "Ostrzeżenie",
|
||||||
|
failure: "Usterka",
|
||||||
|
danger: "Niebiezpieczeństwo",
|
||||||
|
bug: "Błąd w kodzie",
|
||||||
|
example: "Przykład",
|
||||||
|
quote: "Cytat",
|
||||||
|
},
|
||||||
|
backlinks: {
|
||||||
|
title: "Odnośniki zwrotne",
|
||||||
|
noBacklinksFound: "Brak połączeń zwrotnych",
|
||||||
|
},
|
||||||
|
themeToggle: {
|
||||||
|
lightMode: "Trzyb jasny",
|
||||||
|
darkMode: "Tryb ciemny",
|
||||||
|
},
|
||||||
|
explorer: {
|
||||||
|
title: "Przeglądaj",
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
createdWith: "Stworzone z użyciem",
|
||||||
|
},
|
||||||
|
graph: {
|
||||||
|
title: "Graf",
|
||||||
|
},
|
||||||
|
recentNotes: {
|
||||||
|
title: "Najnowsze notatki",
|
||||||
|
seeRemainingMore: ({ remaining }) => `Zobacz ${remaining} nastepnych →`,
|
||||||
|
},
|
||||||
|
transcludes: {
|
||||||
|
transcludeOf: ({ targetSlug }) => `Osadzone ${targetSlug}`,
|
||||||
|
linkToOriginal: "Łącze do oryginału",
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
title: "Szukaj",
|
||||||
|
searchBarPlaceholder: "Search for something",
|
||||||
|
},
|
||||||
|
tableOfContents: {
|
||||||
|
title: "Spis treści",
|
||||||
|
},
|
||||||
|
contentMeta: {
|
||||||
|
readingTime: ({ minutes }) => `${minutes} min. czytania `,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
rss: {
|
||||||
|
recentNotes: "Najnowsze notatki",
|
||||||
|
lastFewNotes: ({ count }) => `Ostatnie ${count} notatek`,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
title: "Nie znaleziono",
|
||||||
|
notFound: "Ta strona jest prywatna lub nie istnieje.",
|
||||||
|
home: "Powrót do strony głównej",
|
||||||
|
},
|
||||||
|
folderContent: {
|
||||||
|
folder: "Folder",
|
||||||
|
itemsUnderFolder: ({ count }) =>
|
||||||
|
count === 1 ? "W tym folderze jest 1 element." : `Elementów w folderze: ${count}.`,
|
||||||
|
},
|
||||||
|
tagContent: {
|
||||||
|
tag: "Znacznik",
|
||||||
|
tagIndex: "Spis znaczników",
|
||||||
|
itemsUnderTag: ({ count }) =>
|
||||||
|
count === 1 ? "Oznaczony 1 element." : `Elementów z tym znacznikiem: ${count}.`,
|
||||||
|
showingFirst: ({ count }) => `Pokazuje ${count} pierwszych znaczników.`,
|
||||||
|
totalTags: ({ count }) => `Znalezionych wszystkich znaczników: ${count}.`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const satisfies Translation
|
||||||
@ -65,6 +65,7 @@ export default {
|
|||||||
error: {
|
error: {
|
||||||
title: "Não encontrado",
|
title: "Não encontrado",
|
||||||
notFound: "Esta página é privada ou não existe.",
|
notFound: "Esta página é privada ou não existe.",
|
||||||
|
home: "Retornar a página inicial",
|
||||||
},
|
},
|
||||||
folderContent: {
|
folderContent: {
|
||||||
folder: "Arquivo",
|
folder: "Arquivo",
|
||||||
|
|||||||
@ -66,6 +66,7 @@ export default {
|
|||||||
error: {
|
error: {
|
||||||
title: "Pagina nu a fost găsită",
|
title: "Pagina nu a fost găsită",
|
||||||
notFound: "Fie această pagină este privată, fie nu există.",
|
notFound: "Fie această pagină este privată, fie nu există.",
|
||||||
|
home: "Reveniți la pagina de pornire",
|
||||||
},
|
},
|
||||||
folderContent: {
|
folderContent: {
|
||||||
folder: "Dosar",
|
folder: "Dosar",
|
||||||
|
|||||||
@ -67,6 +67,7 @@ export default {
|
|||||||
error: {
|
error: {
|
||||||
title: "Страница не найдена",
|
title: "Страница не найдена",
|
||||||
notFound: "Эта страница приватная или не существует",
|
notFound: "Эта страница приватная или не существует",
|
||||||
|
home: "Вернуться на главную страницу",
|
||||||
},
|
},
|
||||||
folderContent: {
|
folderContent: {
|
||||||
folder: "Папка",
|
folder: "Папка",
|
||||||
|
|||||||
@ -54,7 +54,7 @@ export default {
|
|||||||
title: "Зміст",
|
title: "Зміст",
|
||||||
},
|
},
|
||||||
contentMeta: {
|
contentMeta: {
|
||||||
readingTime: ({ minutes }) => `${minutes} min read`,
|
readingTime: ({ minutes }) => `${minutes} хв читання`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
pages: {
|
pages: {
|
||||||
@ -65,19 +65,20 @@ export default {
|
|||||||
error: {
|
error: {
|
||||||
title: "Не знайдено",
|
title: "Не знайдено",
|
||||||
notFound: "Ця сторінка або приватна, або не існує.",
|
notFound: "Ця сторінка або приватна, або не існує.",
|
||||||
|
home: "Повернутися на головну сторінку",
|
||||||
},
|
},
|
||||||
folderContent: {
|
folderContent: {
|
||||||
folder: "Папка",
|
folder: "Тека",
|
||||||
itemsUnderFolder: ({ count }) =>
|
itemsUnderFolder: ({ count }) =>
|
||||||
count === 1 ? "У цій папці 1 елемент." : `Елементів у цій папці: ${count}.`,
|
count === 1 ? "У цій теці 1 елемент." : `Елементів у цій теці: ${count}.`,
|
||||||
},
|
},
|
||||||
tagContent: {
|
tagContent: {
|
||||||
tag: "Тег",
|
tag: "Мітка",
|
||||||
tagIndex: "Індекс тегу",
|
tagIndex: "Індекс мітки",
|
||||||
itemsUnderTag: ({ count }) =>
|
itemsUnderTag: ({ count }) =>
|
||||||
count === 1 ? "1 елемент з цим тегом." : `Елементів з цим тегом: ${count}.`,
|
count === 1 ? "1 елемент з цією міткою." : `Елементів з цією міткою: ${count}.`,
|
||||||
showingFirst: ({ count }) => `Показ перших ${count} тегів.`,
|
showingFirst: ({ count }) => `Показ перших ${count} міток.`,
|
||||||
totalTags: ({ count }) => `Всього знайдено тегів: ${count}.`,
|
totalTags: ({ count }) => `Всього знайдено міток: ${count}.`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const satisfies Translation
|
} as const satisfies Translation
|
||||||
|
|||||||
@ -65,6 +65,7 @@ export default {
|
|||||||
error: {
|
error: {
|
||||||
title: "Không Tìm Thấy",
|
title: "Không Tìm Thấy",
|
||||||
notFound: "Trang này được bảo mật hoặc không tồn tại.",
|
notFound: "Trang này được bảo mật hoặc không tồn tại.",
|
||||||
|
home: "Trở về trang chủ",
|
||||||
},
|
},
|
||||||
folderContent: {
|
folderContent: {
|
||||||
folder: "Thư Mục",
|
folder: "Thư Mục",
|
||||||
|
|||||||
@ -65,6 +65,7 @@ export default {
|
|||||||
error: {
|
error: {
|
||||||
title: "无法找到",
|
title: "无法找到",
|
||||||
notFound: "私有笔记或笔记不存在。",
|
notFound: "私有笔记或笔记不存在。",
|
||||||
|
home: "返回首页",
|
||||||
},
|
},
|
||||||
folderContent: {
|
folderContent: {
|
||||||
folder: "文件夹",
|
folder: "文件夹",
|
||||||
|
|||||||
@ -136,6 +136,22 @@ function addGlobalPageResources(ctx: BuildCtx, componentResources: ComponentReso
|
|||||||
posthog.init('${cfg.analytics.apiKey}',{api_host:'${cfg.analytics.host ?? "https://app.posthog.com"}'})\`
|
posthog.init('${cfg.analytics.apiKey}',{api_host:'${cfg.analytics.host ?? "https://app.posthog.com"}'})\`
|
||||||
document.head.appendChild(posthogScript)
|
document.head.appendChild(posthogScript)
|
||||||
`)
|
`)
|
||||||
|
} else if (cfg.analytics?.provider === "tinylytics") {
|
||||||
|
const siteId = cfg.analytics.siteId
|
||||||
|
componentResources.afterDOMLoaded.push(`
|
||||||
|
const tinylyticsScript = document.createElement("script")
|
||||||
|
tinylyticsScript.src = "https://tinylytics.app/embed/${siteId}.js"
|
||||||
|
tinylyticsScript.defer = true
|
||||||
|
document.head.appendChild(tinylyticsScript)
|
||||||
|
`)
|
||||||
|
} else if (cfg.analytics?.provider === "cabin") {
|
||||||
|
componentResources.afterDOMLoaded.push(`
|
||||||
|
const cabinScript = document.createElement("script")
|
||||||
|
cabinScript.src = "${cfg.analytics.host ?? "https://scripts.withcabin.com"}/hello.js"
|
||||||
|
cabinScript.defer = true
|
||||||
|
cabinScript.async = true
|
||||||
|
document.head.appendChild(cabinScript)
|
||||||
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cfg.enableSPA) {
|
if (cfg.enableSPA) {
|
||||||
|
|||||||
@ -59,14 +59,25 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
|
|||||||
...userOpts,
|
...userOpts,
|
||||||
}
|
}
|
||||||
|
|
||||||
const { head: Head, header, beforeBody, pageBody, left, right, footer: Footer } = opts
|
const { head: Head, header, beforeBody, pageBody, afterBody, left, right, footer: Footer } = opts
|
||||||
const Header = HeaderConstructor()
|
const Header = HeaderConstructor()
|
||||||
const Body = BodyConstructor()
|
const Body = BodyConstructor()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: "ContentPage",
|
name: "ContentPage",
|
||||||
getQuartzComponents() {
|
getQuartzComponents() {
|
||||||
return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer]
|
return [
|
||||||
|
Head,
|
||||||
|
Header,
|
||||||
|
Body,
|
||||||
|
...header,
|
||||||
|
...beforeBody,
|
||||||
|
pageBody,
|
||||||
|
...afterBody,
|
||||||
|
...left,
|
||||||
|
...right,
|
||||||
|
Footer,
|
||||||
|
]
|
||||||
},
|
},
|
||||||
async getDependencyGraph(ctx, content, _resources) {
|
async getDependencyGraph(ctx, content, _resources) {
|
||||||
const graph = new DepGraph<FilePath>()
|
const graph = new DepGraph<FilePath>()
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { QuartzComponentProps } from "../../components/types"
|
|||||||
import HeaderConstructor from "../../components/Header"
|
import HeaderConstructor from "../../components/Header"
|
||||||
import BodyConstructor from "../../components/Body"
|
import BodyConstructor from "../../components/Body"
|
||||||
import { pageResources, renderPage } from "../../components/renderPage"
|
import { pageResources, renderPage } from "../../components/renderPage"
|
||||||
import { ProcessedContent, defaultProcessedContent } from "../vfile"
|
import { ProcessedContent, QuartzPluginData, defaultProcessedContent } from "../vfile"
|
||||||
import { FullPageLayout } from "../../cfg"
|
import { FullPageLayout } from "../../cfg"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import {
|
import {
|
||||||
@ -21,22 +21,37 @@ import { write } from "./helpers"
|
|||||||
import { i18n } from "../../i18n"
|
import { i18n } from "../../i18n"
|
||||||
import DepGraph from "../../depgraph"
|
import DepGraph from "../../depgraph"
|
||||||
|
|
||||||
export const FolderPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
|
interface FolderPageOptions extends FullPageLayout {
|
||||||
|
sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (userOpts) => {
|
||||||
const opts: FullPageLayout = {
|
const opts: FullPageLayout = {
|
||||||
...sharedPageComponents,
|
...sharedPageComponents,
|
||||||
...defaultListPageLayout,
|
...defaultListPageLayout,
|
||||||
pageBody: FolderContent(),
|
pageBody: FolderContent({ sort: userOpts?.sort }),
|
||||||
...userOpts,
|
...userOpts,
|
||||||
}
|
}
|
||||||
|
|
||||||
const { head: Head, header, beforeBody, pageBody, left, right, footer: Footer } = opts
|
const { head: Head, header, beforeBody, pageBody, afterBody, left, right, footer: Footer } = opts
|
||||||
const Header = HeaderConstructor()
|
const Header = HeaderConstructor()
|
||||||
const Body = BodyConstructor()
|
const Body = BodyConstructor()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: "FolderPage",
|
name: "FolderPage",
|
||||||
getQuartzComponents() {
|
getQuartzComponents() {
|
||||||
return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer]
|
return [
|
||||||
|
Head,
|
||||||
|
Header,
|
||||||
|
Body,
|
||||||
|
...header,
|
||||||
|
...beforeBody,
|
||||||
|
pageBody,
|
||||||
|
...afterBody,
|
||||||
|
...left,
|
||||||
|
...right,
|
||||||
|
Footer,
|
||||||
|
]
|
||||||
},
|
},
|
||||||
async getDependencyGraph(_ctx, content, _resources) {
|
async getDependencyGraph(_ctx, content, _resources) {
|
||||||
// Example graph:
|
// Example graph:
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { QuartzComponentProps } from "../../components/types"
|
|||||||
import HeaderConstructor from "../../components/Header"
|
import HeaderConstructor from "../../components/Header"
|
||||||
import BodyConstructor from "../../components/Body"
|
import BodyConstructor from "../../components/Body"
|
||||||
import { pageResources, renderPage } from "../../components/renderPage"
|
import { pageResources, renderPage } from "../../components/renderPage"
|
||||||
import { ProcessedContent, defaultProcessedContent } from "../vfile"
|
import { ProcessedContent, QuartzPluginData, defaultProcessedContent } from "../vfile"
|
||||||
import { FullPageLayout } from "../../cfg"
|
import { FullPageLayout } from "../../cfg"
|
||||||
import {
|
import {
|
||||||
FilePath,
|
FilePath,
|
||||||
@ -18,22 +18,37 @@ import { write } from "./helpers"
|
|||||||
import { i18n } from "../../i18n"
|
import { i18n } from "../../i18n"
|
||||||
import DepGraph from "../../depgraph"
|
import DepGraph from "../../depgraph"
|
||||||
|
|
||||||
export const TagPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
|
interface TagPageOptions extends FullPageLayout {
|
||||||
|
sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TagPage: QuartzEmitterPlugin<Partial<TagPageOptions>> = (userOpts) => {
|
||||||
const opts: FullPageLayout = {
|
const opts: FullPageLayout = {
|
||||||
...sharedPageComponents,
|
...sharedPageComponents,
|
||||||
...defaultListPageLayout,
|
...defaultListPageLayout,
|
||||||
pageBody: TagContent(),
|
pageBody: TagContent({ sort: userOpts?.sort }),
|
||||||
...userOpts,
|
...userOpts,
|
||||||
}
|
}
|
||||||
|
|
||||||
const { head: Head, header, beforeBody, pageBody, left, right, footer: Footer } = opts
|
const { head: Head, header, beforeBody, pageBody, afterBody, left, right, footer: Footer } = opts
|
||||||
const Header = HeaderConstructor()
|
const Header = HeaderConstructor()
|
||||||
const Body = BodyConstructor()
|
const Body = BodyConstructor()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: "TagPage",
|
name: "TagPage",
|
||||||
getQuartzComponents() {
|
getQuartzComponents() {
|
||||||
return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer]
|
return [
|
||||||
|
Head,
|
||||||
|
Header,
|
||||||
|
Body,
|
||||||
|
...header,
|
||||||
|
...beforeBody,
|
||||||
|
pageBody,
|
||||||
|
...afterBody,
|
||||||
|
...left,
|
||||||
|
...right,
|
||||||
|
Footer,
|
||||||
|
]
|
||||||
},
|
},
|
||||||
async getDependencyGraph(ctx, content, _resources) {
|
async getDependencyGraph(ctx, content, _resources) {
|
||||||
const graph = new DepGraph<FilePath>()
|
const graph = new DepGraph<FilePath>()
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { QuartzFilterPlugin } from "../types"
|
|||||||
export const RemoveDrafts: QuartzFilterPlugin<{}> = () => ({
|
export const RemoveDrafts: QuartzFilterPlugin<{}> = () => ({
|
||||||
name: "RemoveDrafts",
|
name: "RemoveDrafts",
|
||||||
shouldPublish(_ctx, [_tree, vfile]) {
|
shouldPublish(_ctx, [_tree, vfile]) {
|
||||||
const draftFlag: boolean = vfile.data?.frontmatter?.draft ?? false
|
const draftFlag: boolean = vfile.data?.frontmatter?.draft || false
|
||||||
return !draftFlag
|
return !draftFlag
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -28,10 +28,10 @@ export function getStaticResourcesFromPlugins(ctx: BuildCtx) {
|
|||||||
loadTime: "afterDOMReady",
|
loadTime: "afterDOMReady",
|
||||||
contentType: "inline",
|
contentType: "inline",
|
||||||
script: `
|
script: `
|
||||||
const socket = new WebSocket('${wsUrl}')
|
const socket = new WebSocket('${wsUrl}')
|
||||||
// reload(true) ensures resources like images and scripts are fetched again in firefox
|
// reload(true) ensures resources like images and scripts are fetched again in firefox
|
||||||
socket.addEventListener('message', () => document.location.reload(true))
|
socket.addEventListener('message', () => document.location.reload(true))
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@ const defaultOptions: Options = {
|
|||||||
csl: "apa",
|
csl: "apa",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Citations: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
|
export const Citations: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||||
const opts = { ...defaultOptions, ...userOpts }
|
const opts = { ...defaultOptions, ...userOpts }
|
||||||
return {
|
return {
|
||||||
name: "Citations",
|
name: "Citations",
|
||||||
@ -38,7 +38,7 @@ export const Citations: QuartzTransformerPlugin<Partial<Options> | undefined> =
|
|||||||
// using https://github.com/syntax-tree/unist-util-visit as they're just anochor links
|
// using https://github.com/syntax-tree/unist-util-visit as they're just anochor links
|
||||||
plugins.push(() => {
|
plugins.push(() => {
|
||||||
return (tree, _file) => {
|
return (tree, _file) => {
|
||||||
visit(tree, "element", (node, index, parent) => {
|
visit(tree, "element", (node, _index, _parent) => {
|
||||||
if (node.tagName === "a" && node.properties?.href?.startsWith("#bib")) {
|
if (node.tagName === "a" && node.properties?.href?.startsWith("#bib")) {
|
||||||
node.properties["data-no-popover"] = true
|
node.properties["data-no-popover"] = true
|
||||||
}
|
}
|
||||||
|
|||||||