Merge commit '4bbcc0c50aca68d470542c1af8fd5f8060d97ab8' into HEAD
All checks were successful
Build / build (push) Successful in 2m48s
All checks were successful
Build / build (push) Successful in 2m48s
This commit is contained in:
commit
d4fcf7e248
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@ -24,17 +24,17 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
|
1
.node-version
Normal file
1
.node-version
Normal file
@ -0,0 +1 @@
|
||||
v20.9.0
|
83
docs/features/comments.md
Normal file
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"
|
||||
}
|
||||
}
|
||||
```
|
18
docs/features/i18n.md
Normal file
18
docs/features/i18n.md
Normal file
@ -0,0 +1,18 @@
|
||||
---
|
||||
title: Internationalization
|
||||
---
|
||||
|
||||
Internationalization allows users to translate text in the Quartz interface into various supported languages without needing to make extensive code changes. This can be changed via the `locale` [[configuration]] field in `quartz.config.ts`.
|
||||
|
||||
The locale field generally follows a certain format: `{language}-{REGION}`
|
||||
|
||||
- `{language}` is usually a [2-letter lowercase language code](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes).
|
||||
- `{REGION}` is usually a [2-letter uppercase region code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)
|
||||
|
||||
> [!tip] Interested in contributing?
|
||||
> We [gladly welcome translation PRs](https://github.com/jackyzha0/quartz/tree/v4/quartz/i18n/locales)! To contribute a translation, do the following things:
|
||||
>
|
||||
> 1. In the `quartz/i18n/locales` folder, copy the `en-US.ts` file.
|
||||
> 2. Rename it to `{language}-{REGION}.ts` so it matches a locale of the format shown above.
|
||||
> 3. Fill in the translations!
|
||||
> 4. Add the entry under `TRANSLATIONS` in `quartz/i18n/index.ts`.
|
BIN
docs/images/giscus-discussion.png
Normal file
BIN
docs/images/giscus-discussion.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 88 KiB |
BIN
docs/images/giscus-example.png
Normal file
BIN
docs/images/giscus-example.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 572 KiB |
BIN
docs/images/giscus-repo.png
Normal file
BIN
docs/images/giscus-repo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 108 KiB |
BIN
docs/images/giscus-results.png
Normal file
BIN
docs/images/giscus-results.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 171 KiB |
BIN
docs/images/github-init-repo-options.png
Normal file
BIN
docs/images/github-init-repo-options.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 90 KiB |
BIN
docs/images/github-quick-setup.png
Normal file
BIN
docs/images/github-quick-setup.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 150 KiB |
37
docs/plugins/AliasRedirects.md
Normal file
37
docs/plugins/AliasRedirects.md
Normal file
@ -0,0 +1,37 @@
|
||||
---
|
||||
title: AliasRedirects
|
||||
tags:
|
||||
- plugin/emitter
|
||||
---
|
||||
|
||||
This plugin emits HTML redirect pages for aliases and permalinks defined in the frontmatter of content files.
|
||||
|
||||
For example, A `foo.md` has the following frontmatter
|
||||
|
||||
```md title="foo.md"
|
||||
---
|
||||
title: "Foo"
|
||||
alias:
|
||||
- "bar"
|
||||
---
|
||||
```
|
||||
|
||||
The target `host.me/bar` will be redirected to `host.me/foo`
|
||||
|
||||
Note that these are permanent redirect.
|
||||
|
||||
The emitter supports the following aliases:
|
||||
|
||||
- `aliases`
|
||||
- `alias`
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
This plugin has no configuration options.
|
||||
|
||||
## API
|
||||
|
||||
- Category: Emitter
|
||||
- Function name: `Plugin.AliasRedirects()`.
|
||||
- Source: [`quartz/plugins/emitters/aliases.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/aliases.ts).
|
20
docs/plugins/Assets.md
Normal file
20
docs/plugins/Assets.md
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
title: Assets
|
||||
tags:
|
||||
- plugin/emitter
|
||||
---
|
||||
|
||||
This plugin emits all non-Markdown static assets in your content folder (like images, videos, HTML, etc). The plugin respects the `ignorePatterns` in the global [[configuration]].
|
||||
|
||||
Note that all static assets will then be accessible through its path on your generated site, i.e: `host.me/path/to/static.pdf`
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
This plugin has no configuration options.
|
||||
|
||||
## API
|
||||
|
||||
- Category: Emitter
|
||||
- Function name: `Plugin.Assets()`.
|
||||
- Source: [`quartz/plugins/emitters/assets.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/assets.ts).
|
22
docs/plugins/CNAME.md
Normal file
22
docs/plugins/CNAME.md
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
title: CNAME
|
||||
tags:
|
||||
- plugin/emitter
|
||||
---
|
||||
|
||||
This plugin emits a `CNAME` record that points your subdomain to the default domain of your site.
|
||||
|
||||
If you want to use a custom domain name like `quartz.example.com` for the site, then this is needed.
|
||||
|
||||
See [[hosting|Hosting]] for more information.
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
This plugin has no configuration options.
|
||||
|
||||
## API
|
||||
|
||||
- Category: Emitter
|
||||
- Function name: `Plugin.CNAME()`.
|
||||
- Source: [`quartz/plugins/emitters/cname.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/cname.ts).
|
18
docs/plugins/ComponentResources.md
Normal file
18
docs/plugins/ComponentResources.md
Normal file
@ -0,0 +1,18 @@
|
||||
---
|
||||
title: ComponentResources
|
||||
tags:
|
||||
- plugin/emitter
|
||||
---
|
||||
|
||||
This plugin manages and emits the static resources required for the Quartz framework. This includes CSS stylesheets and JavaScript scripts that enhance the functionality and aesthetics of the generated site. See also the `cdnCaching` option in the `theme` section of the [[configuration]].
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
This plugin has no configuration options.
|
||||
|
||||
## API
|
||||
|
||||
- Category: Emitter
|
||||
- Function name: `Plugin.ComponentResources()`.
|
||||
- Source: [`quartz/plugins/emitters/componentResources.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/componentResources.ts).
|
26
docs/plugins/ContentIndex.md
Normal file
26
docs/plugins/ContentIndex.md
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
title: ContentIndex
|
||||
tags:
|
||||
- plugin/emitter
|
||||
---
|
||||
|
||||
This plugin emits both RSS and an XML sitemap for your site. The [[RSS Feed]] allows users to subscribe to content on your site and the sitemap allows search engines to better index your site. The plugin also emits a `contentIndex.json` file which is used by dynamic frontend components like search and graph.
|
||||
|
||||
This plugin emits a comprehensive index of the site's content, generating additional resources such as a sitemap, an RSS feed, and a
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
This plugin accepts the following configuration options:
|
||||
|
||||
- `enableSiteMap`: If `true` (default), generates a sitemap XML file (`sitemap.xml`) listing all site URLs for search engines in content discovery.
|
||||
- `enableRSS`: If `true` (default), produces an RSS feed (`index.xml`) with recent content updates.
|
||||
- `rssLimit`: Defines the maximum number of entries to include in the RSS feed, helping to focus on the most recent or relevant content. Defaults to `10`.
|
||||
- `rssFullHtml`: If `true`, the RSS feed includes full HTML content. Otherwise it includes just summaries.
|
||||
- `includeEmptyFiles`: If `true` (default), content files with no body text are included in the generated index and resources.
|
||||
|
||||
## API
|
||||
|
||||
- Category: Emitter
|
||||
- Function name: `Plugin.ContentIndex()`.
|
||||
- Source: [`quartz/plugins/emitters/contentIndex.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/contentIndex.ts).
|
18
docs/plugins/ContentPage.md
Normal file
18
docs/plugins/ContentPage.md
Normal file
@ -0,0 +1,18 @@
|
||||
---
|
||||
title: ContentPage
|
||||
tags:
|
||||
- plugin/emitter
|
||||
---
|
||||
|
||||
This plugin is a core component of the Quartz framework. It generates the HTML pages for each piece of Markdown content. It emits the full-page [[layout]], including headers, footers, and body content, among others.
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
This plugin has no configuration options.
|
||||
|
||||
## API
|
||||
|
||||
- Category: Emitter
|
||||
- Function name: `Plugin.ContentPage()`.
|
||||
- Source: [`quartz/plugins/emitters/contentPage.tsx`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/contentPage.tsx).
|
30
docs/plugins/CrawlLinks.md
Normal file
30
docs/plugins/CrawlLinks.md
Normal file
@ -0,0 +1,30 @@
|
||||
---
|
||||
title: CrawlLinks
|
||||
tags:
|
||||
- plugin/transformer
|
||||
---
|
||||
|
||||
This plugin parses links and processes them to point to the right places. It is also needed for embedded links (like images). See [[Obsidian compatibility]] for more information.
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
This plugin accepts the following configuration options:
|
||||
|
||||
- `markdownLinkResolution`: Sets the strategy for resolving Markdown paths, can be `"absolute"` (default), `"relative"` or `"shortest"`. You should use the same setting here as in [[Obsidian compatibility|Obsidian]].
|
||||
- `absolute`: Path relative to the root of the content folder.
|
||||
- `relative`: Path relative to the file you are linking from.
|
||||
- `shortest`: Name of the file. If this isn't enough to identify the file, use the full absolute path.
|
||||
- `prettyLinks`: If `true` (default), simplifies links by removing folder paths, making them more user friendly (e.g. `folder/deeply/nested/note` becomes `note`).
|
||||
- `openLinksInNewTab`: If `true`, configures external links to open in a new tab. Defaults to `false`.
|
||||
- `lazyLoad`: If `true`, adds lazy loading to resource elements (`img`, `video`, etc.) to improve page load performance. Defaults to `false`.
|
||||
- `externalLinkIcon`: Adds an icon next to external links when `true` (default) to visually distinguishing them from internal links.
|
||||
|
||||
> [!warning]
|
||||
> Removing this plugin is _not_ recommended and will likely break the page.
|
||||
|
||||
## API
|
||||
|
||||
- Category: Transformer
|
||||
- Function name: `Plugin.CrawlLinks()`.
|
||||
- Source: [`quartz/plugins/transformers/links.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/links.ts).
|
25
docs/plugins/CreatedModifiedDate.md
Normal file
25
docs/plugins/CreatedModifiedDate.md
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
title: "CreatedModifiedDate"
|
||||
tags:
|
||||
- plugin/transformer
|
||||
---
|
||||
|
||||
This plugin determines the created, modified, and published dates for a document using three potential data sources: frontmatter metadata, Git history, and the filesystem. See [[authoring content#Syntax]] for more information.
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
This plugin accepts the following configuration options:
|
||||
|
||||
- `priority`: The data sources to consult for date information. Highest priority first. Possible values are `"frontmatter"`, `"git"`, and `"filesystem"`. Defaults to `["frontmatter", "git", "filesystem"]`.
|
||||
|
||||
> [!warning]
|
||||
> If you rely on `git` for dates, make sure `defaultDateType` is set to `modified` in `quartz.config.ts`.
|
||||
>
|
||||
> Depending on how you [[hosting|host]] your Quartz, the `filesystem` dates of your local files may not match the final dates. In these cases, it may be better to use `git` or `frontmatter` to guarantee correct dates.
|
||||
|
||||
## API
|
||||
|
||||
- Category: Transformer
|
||||
- Function name: `Plugin.CreatedModifiedDate()`.
|
||||
- Source: [`quartz/plugins/transformers/lastmod.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/lastmod.ts).
|
23
docs/plugins/Description.md
Normal file
23
docs/plugins/Description.md
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
title: Description
|
||||
tags:
|
||||
- plugin/transformer
|
||||
---
|
||||
|
||||
This plugin generates descriptions that are used as metadata for the HTML `head`, the [[RSS Feed]] and in [[folder and tag listings]] if there is no main body content, the description is used as the text between the title and the listing.
|
||||
|
||||
If the frontmatter contains a `description` property, it is used (see [[authoring content#Syntax]]). Otherwise, the plugin will do its best to use the first few sentences of the content to reach the target description length.
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
This plugin accepts the following configuration options:
|
||||
|
||||
- `descriptionLength`: the maximum length of the generated description. Default is 150 characters. The cut off happens after the first _sentence_ that ends after the given length.
|
||||
- `replaceExternalLinks`: If `true` (default), replace external links with their domain and path in the description (e.g. `https://domain.tld/some_page/another_page?query=hello&target=world` is replaced with `domain.tld/some_page/another_page`).
|
||||
|
||||
## API
|
||||
|
||||
- Category: Transformer
|
||||
- Function name: `Plugin.Description()`.
|
||||
- Source: [`quartz/plugins/transformers/description.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/description.ts).
|
18
docs/plugins/ExplicitPublish.md
Normal file
18
docs/plugins/ExplicitPublish.md
Normal file
@ -0,0 +1,18 @@
|
||||
---
|
||||
title: ExplicitPublish
|
||||
tags:
|
||||
- plugin/filter
|
||||
---
|
||||
|
||||
This plugin filters content based on an explicit `publish` flag in the frontmatter, allowing only content that is explicitly marked for publication to pass through. It's the opt-in version of [[RemoveDrafts]]. See [[private pages]] for more information.
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
This plugin has no configuration options.
|
||||
|
||||
## API
|
||||
|
||||
- Category: Filter
|
||||
- Function name: `Plugin.ExplicitPublish()`.
|
||||
- Source: [`quartz/plugins/filters/explicit.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/filters/explicit.ts).
|
24
docs/plugins/FolderPage.md
Normal file
24
docs/plugins/FolderPage.md
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
title: FolderPage
|
||||
tags:
|
||||
- plugin/emitter
|
||||
---
|
||||
|
||||
This plugin generates index pages for folders, creating a listing page for each folder that contains multiple content files. See [[folder and tag listings]] for more information.
|
||||
|
||||
Example: [[advanced/|Advanced]]
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
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
|
||||
|
||||
- Category: Emitter
|
||||
- Function name: `Plugin.FolderPage()`.
|
||||
- Source: [`quartz/plugins/emitters/folderPage.tsx`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/folderPage.tsx).
|
24
docs/plugins/Frontmatter.md
Normal file
24
docs/plugins/Frontmatter.md
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
title: "Frontmatter"
|
||||
tags:
|
||||
- plugin/transformer
|
||||
---
|
||||
|
||||
This plugin parses the frontmatter of the page using the [gray-matter](https://github.com/jonschlinkert/gray-matter) library. See [[authoring content#Syntax]], [[Obsidian compatibility]] and [[OxHugo compatibility]] for more information.
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
This plugin accepts the following configuration options:
|
||||
|
||||
- `delimiters`: the delimiters to use for the frontmatter. Can have one value (e.g. `"---"`) or separate values for opening and closing delimiters (e.g. `["---", "~~~"]`). Defaults to `"---"`.
|
||||
- `language`: the language to use for parsing the frontmatter. Can be `yaml` (default) or `toml`.
|
||||
|
||||
> [!warning]
|
||||
> This plugin must not be removed, otherwise Quartz will break.
|
||||
|
||||
## API
|
||||
|
||||
- Category: Transformer
|
||||
- Function name: `Plugin.Frontmatter()`.
|
||||
- Source: [`quartz/plugins/transformers/frontmatter.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/frontmatter.ts).
|
23
docs/plugins/GitHubFlavoredMarkdown.md
Normal file
23
docs/plugins/GitHubFlavoredMarkdown.md
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
title: GitHubFlavoredMarkdown
|
||||
tags:
|
||||
- plugin/transformer
|
||||
---
|
||||
|
||||
This plugin enhances Markdown processing to support GitHub Flavored Markdown (GFM) which adds features like autolink literals, footnotes, strikethrough, tables and tasklists.
|
||||
|
||||
In addition, this plugin adds optional features for typographic refinement (such as converting straight quotes to curly quotes, dashes to en-dashes/em-dashes, and ellipses) and automatic heading links as a symbol that appears next to the heading on hover.
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
This plugin accepts the following configuration options:
|
||||
|
||||
- `enableSmartyPants`: When true, enables typographic enhancements. Default is true.
|
||||
- `linkHeadings`: When true, automatically adds links to headings. Default is true.
|
||||
|
||||
## API
|
||||
|
||||
- Category: Transformer
|
||||
- Function name: `Plugin.GitHubFlavoredMarkdown()`.
|
||||
- Source: [`quartz/plugins/transformers/gfm.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/gfm.ts).
|
18
docs/plugins/HardLineBreaks.md
Normal file
18
docs/plugins/HardLineBreaks.md
Normal file
@ -0,0 +1,18 @@
|
||||
---
|
||||
title: HardLineBreaks
|
||||
tags:
|
||||
- plugin/transformer
|
||||
---
|
||||
|
||||
This plugin automatically converts single line breaks in Markdown text into hard line breaks in the HTML output. This plugin is not enabled by default as this doesn't follow the semantics of actual Markdown but you may enable it if you'd like parity with [[Obsidian compatibility|Obsidian]].
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
This plugin has no configuration options.
|
||||
|
||||
## API
|
||||
|
||||
- Category: Transformer
|
||||
- Function name: `Plugin.HardLineBreaks()`.
|
||||
- Source: [`quartz/plugins/transformers/linebreaks.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/linebreaks.ts).
|
20
docs/plugins/Latex.md
Normal file
20
docs/plugins/Latex.md
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
title: "Latex"
|
||||
tags:
|
||||
- plugin/transformer
|
||||
---
|
||||
|
||||
This plugin adds LaTeX support to Quartz. See [[features/Latex|Latex]] for more information.
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
This plugin accepts the following configuration options:
|
||||
|
||||
- `renderEngine`: the engine to use to render LaTeX equations. Can be `"katex"` for [KaTeX](https://katex.org/) or `"mathjax"` for [MathJax](https://www.mathjax.org/) [SVG rendering](https://docs.mathjax.org/en/latest/output/svg.html). Defaults to KaTeX.
|
||||
|
||||
## API
|
||||
|
||||
- Category: Transformer
|
||||
- Function name: `Plugin.Latex()`.
|
||||
- Source: [`quartz/plugins/transformers/latex.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/latex.ts).
|
18
docs/plugins/NotFoundPage.md
Normal file
18
docs/plugins/NotFoundPage.md
Normal file
@ -0,0 +1,18 @@
|
||||
---
|
||||
title: NotFoundPage
|
||||
tags:
|
||||
- plugin/emitter
|
||||
---
|
||||
|
||||
This plugin emits a 404 (Not Found) page for broken or non-existent URLs.
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
This plugin has no configuration options.
|
||||
|
||||
## API
|
||||
|
||||
- Category: Emitter
|
||||
- Function name: `Plugin.NotFoundPage()`.
|
||||
- Source: [`quartz/plugins/emitters/404.tsx`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/404.tsx).
|
34
docs/plugins/ObsidianFlavoredMarkdown.md
Normal file
34
docs/plugins/ObsidianFlavoredMarkdown.md
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
title: ObsidianFlavoredMarkdown
|
||||
tags:
|
||||
- plugin/transformer
|
||||
---
|
||||
|
||||
This plugin provides support for [[Obsidian compatibility]].
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
This plugin accepts the following configuration options:
|
||||
|
||||
- `comments`: If `true` (default), enables parsing of `%%` style Obsidian comment blocks.
|
||||
- `highlight`: If `true` (default), enables parsing of `==` style highlights within content.
|
||||
- `wikilinks`:If `true` (default), turns [[wikilinks]] into regular links.
|
||||
- `callouts`: If `true` (default), adds support for [[callouts|callout]] blocks for emphasizing content.
|
||||
- `mermaid`: If `true` (default), enables [[Mermaid diagrams|Mermaid diagram]] rendering within Markdown files.
|
||||
- `parseTags`: If `true` (default), parses and links tags within the content.
|
||||
- `parseArrows`: If `true` (default), transforms arrow symbols into their HTML character equivalents.
|
||||
- `parseBlockReferences`: If `true` (default), handles block references, linking to specific content blocks.
|
||||
- `enableInHtmlEmbed`: If `true`, allows embedding of content directly within HTML. Defaults to `false`.
|
||||
- `enableYouTubeEmbed`: If `true` (default), enables the embedding of YouTube videos and playlists using external image Markdown syntax.
|
||||
- `enableVideoEmbed`: If `true` (default), enables the embedding of video files.
|
||||
- `enableCheckbox`: If `true`, adds support for interactive checkboxes in content. Defaults to `false`.
|
||||
|
||||
> [!warning]
|
||||
> Don't remove this plugin if you're using [[Obsidian compatibility|Obsidian]] to author the content!
|
||||
|
||||
## API
|
||||
|
||||
- Category: Transformer
|
||||
- Function name: `Plugin.ObsidianFlavoredMarkdown()`.
|
||||
- Source: [`quartz/plugins/transformers/toc.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/toc.ts).
|
29
docs/plugins/OxHugoFlavoredMarkdown.md
Normal file
29
docs/plugins/OxHugoFlavoredMarkdown.md
Normal file
@ -0,0 +1,29 @@
|
||||
---
|
||||
title: OxHugoFlavoredMarkdown
|
||||
tags:
|
||||
- plugin/transformer
|
||||
---
|
||||
|
||||
This plugin provides support for [ox-hugo](https://github.com/kaushalmodi/ox-hugo) compatibility. See [[OxHugo compatibility]] for more information.
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
This plugin accepts the following configuration options:
|
||||
|
||||
- `wikilinks`: If `true` (default), converts Hugo `{{ relref }}` shortcodes to Quartz [[wikilinks]].
|
||||
- `removePredefinedAnchor`: If `true` (default), strips predefined anchors from headings.
|
||||
- `removeHugoShortcode`: If `true` (default), removes Hugo shortcode syntax (`{{}}`) from the content.
|
||||
- `replaceFigureWithMdImg`: If `true` (default), replaces `<figure/>` with `![]()`.
|
||||
- `replaceOrgLatex`: If `true` (default), converts Org-mode [[features/Latex|Latex]] fragments to Quartz-compatible LaTeX wrapped in `$` (for inline) and `$$` (for block equations).
|
||||
|
||||
> [!warning]
|
||||
> While you can use this together with [[ObsidianFlavoredMarkdown]], it's not recommended because it might mutate the file in unexpected ways. Use with caution.
|
||||
>
|
||||
> If you use `toml` frontmatter, make sure to configure the [[Frontmatter]] plugin accordingly. See [[OxHugo compatibility]] for an example.
|
||||
|
||||
## API
|
||||
|
||||
- Category: Transformer
|
||||
- Function name: `Plugin.OxHugoFlavoredMarkdown()`.
|
||||
- Source: [`quartz/plugins/transformers/oxhugofm.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/oxhugofm.ts).
|
18
docs/plugins/RemoveDrafts.md
Normal file
18
docs/plugins/RemoveDrafts.md
Normal file
@ -0,0 +1,18 @@
|
||||
---
|
||||
title: RemoveDrafts
|
||||
tags:
|
||||
- plugin/filter
|
||||
---
|
||||
|
||||
This plugin filters out content from your vault, so that only finalized content is made available. This prevents [[private pages]] from being published. By default, it filters out all pages with `draft: true` in the frontmatter and leaves all other pages intact.
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
This plugin has no configuration options.
|
||||
|
||||
## API
|
||||
|
||||
- Category: Filter
|
||||
- Function name: `Plugin.RemoveDrafts()`.
|
||||
- Source: [`quartz/plugins/filters/draft.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/filters/draft.ts).
|
21
docs/plugins/Static.md
Normal file
21
docs/plugins/Static.md
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
title: Static
|
||||
tags:
|
||||
- plugin/emitter
|
||||
---
|
||||
|
||||
This plugin emits all static resources needed by Quartz. This is used, for example, for fonts and images that need a stable position, such as banners and icons. The plugin respects the `ignorePatterns` in the global [[configuration]].
|
||||
|
||||
> [!important]
|
||||
> This is different from [[Assets]]. The resources from the [[Static]] plugin are located under `quartz/static`, whereas [[Assets]] renders all static resources under `content` and is used for images, videos, audio, etc. that are directly referenced by your markdown content.
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
This plugin has no configuration options.
|
||||
|
||||
## API
|
||||
|
||||
- Category: Emitter
|
||||
- Function name: `Plugin.Static()`.
|
||||
- Source: [`quartz/plugins/emitters/static.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/static.ts).
|
23
docs/plugins/SyntaxHighlighting.md
Normal file
23
docs/plugins/SyntaxHighlighting.md
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
title: "SyntaxHighlighting"
|
||||
tags:
|
||||
- plugin/transformer
|
||||
---
|
||||
|
||||
This plugin is used to add syntax highlighting to code blocks in Quartz. See [[syntax highlighting]] for more information.
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
This plugin accepts the following configuration options:
|
||||
|
||||
- `theme`: a separate id of one of the [themes bundled with Shikiji](https://shikiji.netlify.app/themes). One for light mode and one for dark mode. Defaults to `theme: { light: "github-light", dark: "github-dark" }`.
|
||||
- `keepBackground`: If set to `true`, the background of the Shikiji theme will be used. With `false` (default) the Quartz theme color for background will be used instead.
|
||||
|
||||
In addition, you can further override the colours in the `quartz/styles/syntax.scss` file.
|
||||
|
||||
## API
|
||||
|
||||
- Category: Transformer
|
||||
- Function name: `Plugin.SyntaxHighlighting()`.
|
||||
- Source: [`quartz/plugins/transformers/syntax.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/syntax.ts).
|
26
docs/plugins/TableOfContents.md
Normal file
26
docs/plugins/TableOfContents.md
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
title: TableOfContents
|
||||
tags:
|
||||
- plugin/transformer
|
||||
---
|
||||
|
||||
This plugin generates a table of contents (TOC) for Markdown documents. See [[table of contents]] for more information.
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
This plugin accepts the following configuration options:
|
||||
|
||||
- `maxDepth`: Limits the depth of headings included in the TOC, ranging from `1` (top level headings only) to `6` (all heading levels). Default is `3`.
|
||||
- `minEntries`: The minimum number of heading entries required for the TOC to be displayed. Default is `1`.
|
||||
- `showByDefault`: If `true` (default), the TOC should be displayed by default. Can be overridden by frontmatter settings.
|
||||
- `collapseByDefault`: If `true`, the TOC will start in a collapsed state. Default is `false`.
|
||||
|
||||
> [!warning]
|
||||
> This plugin needs the `Component.TableOfContents` component in `quartz.layout.ts` to determine where to display the TOC. Without it, nothing will be displayed. They should always be added or removed together.
|
||||
|
||||
## API
|
||||
|
||||
- Category: Transformer
|
||||
- Function name: `Plugin.TableOfContents()`.
|
||||
- Source: [`quartz/plugins/transformers/toc.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/toc.ts).
|
22
docs/plugins/TagPage.md
Normal file
22
docs/plugins/TagPage.md
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
title: TagPage
|
||||
tags:
|
||||
- plugin/emitter
|
||||
---
|
||||
|
||||
This plugin emits dedicated pages for each tag used in the content. See [[folder and tag listings]] for more information.
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
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
|
||||
|
||||
- Category: Emitter
|
||||
- Function name: `Plugin.TagPage()`.
|
||||
- Source: [`quartz/plugins/emitters/tagPage.tsx`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/tagPage.tsx).
|
3
docs/plugins/index.md
Normal file
3
docs/plugins/index.md
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Plugins
|
||||
---
|
48
docs/setting up your GitHub repository.md
Normal file
48
docs/setting up your GitHub repository.md
Normal file
@ -0,0 +1,48 @@
|
||||
---
|
||||
title: Setting up your GitHub repository
|
||||
---
|
||||
|
||||
First, make sure you have Quartz [[index#🪴 Get Started|cloned and setup locally]].
|
||||
|
||||
Then, create a new repository on GitHub.com. Do **not** initialize the new repository with `README`, license, or `gitignore` files.
|
||||
|
||||
![[github-init-repo-options.png]]
|
||||
|
||||
At the top of your repository on GitHub.com's Quick Setup page, click the clipboard to copy the remote repository URL.
|
||||
|
||||
![[github-quick-setup.png]]
|
||||
|
||||
In your terminal of choice, navigate to the root of your Quartz folder. Then, run the following commands, replacing `REMOTE-URL` with the URL you just copied from the previous step.
|
||||
|
||||
```bash
|
||||
# list all the repositories that are tracked
|
||||
git remote -v
|
||||
|
||||
# if the origin doesn't match your own repository, set your repository as the origin
|
||||
git remote set-url origin REMOTE-URL
|
||||
|
||||
# if you don't have upstream as a remote, add it so updates work
|
||||
git remote add upstream https://github.com/jackyzha0/quartz.git
|
||||
```
|
||||
|
||||
Then, you can sync the content to upload it to your repository. This is a helper command that will do the initial push of your content to your repository.
|
||||
|
||||
```bash
|
||||
npx quartz sync --no-pull
|
||||
```
|
||||
|
||||
> [!warning]- `fatal: --[no-]autostash option is only valid with --rebase`
|
||||
> You may have an outdated version of `git`. Updating `git` should fix this issue.
|
||||
|
||||
In future updates, you can simply run `npx quartz sync` every time you want to push updates to your repository.
|
||||
|
||||
> [!hint] Flags and options
|
||||
> For full help options, you can run `npx quartz sync --help`.
|
||||
>
|
||||
> Most of these have sensible defaults but you can override them if you have a custom setup:
|
||||
>
|
||||
> - `-d` or `--directory`: the content folder. This is normally just `content`
|
||||
> - `-v` or `--verbose`: print out extra logging information
|
||||
> - `--commit` or `--no-commit`: whether to make a `git` commit for your changes
|
||||
> - `--push` or `--no-push`: whether to push updates to your GitHub fork of Quartz
|
||||
> - `--pull` or `--no-pull`: whether to try and pull in any updates from your GitHub fork (i.e. from other devices) before pushing
|
3
docs/tags/plugin.md
Normal file
3
docs/tags/plugin.md
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Plugins
|
||||
---
|
4
globals.d.ts
vendored
4
globals.d.ts
vendored
@ -4,6 +4,10 @@ export declare global {
|
||||
type: K,
|
||||
listener: (this: Document, ev: CustomEventMap[K]) => 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
|
||||
}
|
||||
interface Window {
|
||||
|
1386
package-lock.json
generated
1386
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
58
package.json
58
package.json
@ -2,7 +2,7 @@
|
||||
"name": "@jackyzha0/quartz",
|
||||
"description": "🌱 publish your digital garden and notes as a website",
|
||||
"private": true,
|
||||
"version": "4.2.2",
|
||||
"version": "4.3.0",
|
||||
"type": "module",
|
||||
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
||||
"license": "MIT",
|
||||
@ -12,15 +12,16 @@
|
||||
"url": "https://github.com/jackyzha0/quartz.git"
|
||||
},
|
||||
"scripts": {
|
||||
"quartz": "./quartz/bootstrap-cli.mjs",
|
||||
"docs": "npx quartz build --serve -d docs",
|
||||
"check": "tsc --noEmit && npx prettier . --check",
|
||||
"format": "npx prettier . --write",
|
||||
"test": "tsx ./quartz/util/path.test.ts",
|
||||
"test": "tsx ./quartz/util/path.test.ts && tsx ./quartz/depgraph.test.ts",
|
||||
"profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=9.3.1",
|
||||
"node": ">=18.14"
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"keywords": [
|
||||
"site generator",
|
||||
@ -35,37 +36,38 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.7.0",
|
||||
"@floating-ui/dom": "^1.6.1",
|
||||
"@napi-rs/simple-git": "0.1.14",
|
||||
"async-mutex": "^0.4.1",
|
||||
"@floating-ui/dom": "^1.6.8",
|
||||
"@napi-rs/simple-git": "0.1.16",
|
||||
"async-mutex": "^0.5.0",
|
||||
"chalk": "^5.3.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"chokidar": "^3.6.0",
|
||||
"cli-spinner": "^0.2.10",
|
||||
"d3": "^7.8.5",
|
||||
"esbuild-sass-plugin": "^2.16.0",
|
||||
"d3": "^7.9.0",
|
||||
"esbuild-sass-plugin": "^2.16.1",
|
||||
"flexsearch": "0.7.43",
|
||||
"github-slugger": "^2.0.0",
|
||||
"globby": "^14.0.0",
|
||||
"globby": "^14.0.2",
|
||||
"gray-matter": "^4.0.3",
|
||||
"hast-util-to-html": "^9.0.0",
|
||||
"hast-util-to-html": "^9.0.1",
|
||||
"hast-util-to-jsx-runtime": "^2.3.0",
|
||||
"hast-util-to-string": "^3.0.0",
|
||||
"is-absolute-url": "^4.0.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lightningcss": "^1.23.0",
|
||||
"lightningcss": "^1.25.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",
|
||||
"micromorph": "^0.4.5",
|
||||
"preact": "^10.19.3",
|
||||
"preact-render-to-string": "^6.3.1",
|
||||
"preact": "^10.22.1",
|
||||
"preact-render-to-string": "^6.5.7",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"pretty-time": "^1.1.0",
|
||||
"reading-time": "^1.5.0",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-citation": "^2.0.0",
|
||||
"rehype-katex": "^7.0.0",
|
||||
"rehype-mathjax": "^6.0.0",
|
||||
"rehype-pretty-code": "^0.12.6",
|
||||
"rehype-pretty-code": "^0.13.2",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark": "^15.0.1",
|
||||
@ -75,19 +77,19 @@
|
||||
"remark-math": "^6.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.0",
|
||||
"remark-smartypants": "^2.0.0",
|
||||
"rfdc": "^1.3.1",
|
||||
"rimraf": "^5.0.5",
|
||||
"remark-smartypants": "^3.0.2",
|
||||
"rfdc": "^1.4.1",
|
||||
"rimraf": "^6.0.1",
|
||||
"serve-handler": "^6.1.5",
|
||||
"shikiji": "^0.10.2",
|
||||
"shiki": "^1.10.3",
|
||||
"source-map-support": "^0.5.21",
|
||||
"to-vfile": "^8.0.0",
|
||||
"toml": "^3.0.0",
|
||||
"unified": "^11.0.4",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vfile": "^6.0.1",
|
||||
"workerpool": "^9.1.0",
|
||||
"ws": "^8.15.1",
|
||||
"vfile": "^6.0.2",
|
||||
"workerpool": "^9.1.3",
|
||||
"ws": "^8.18.0",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -95,14 +97,14 @@
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^20.11.14",
|
||||
"@types/node": "^22.1.0",
|
||||
"@types/pretty-time": "^1.1.5",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@types/ws": "^8.5.12",
|
||||
"@types/yargs": "^17.0.32",
|
||||
"esbuild": "^0.19.9",
|
||||
"prettier": "^3.2.4",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.3"
|
||||
"prettier": "^3.3.3",
|
||||
"tsx": "^4.16.2",
|
||||
"typescript": "^5.5.3"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,11 @@
|
||||
import { QuartzConfig } from "./quartz/cfg"
|
||||
import * as Plugin from "./quartz/plugins"
|
||||
|
||||
/**
|
||||
* Quartz 4.0 Configuration
|
||||
*
|
||||
* See https://quartz.jzhao.xyz/configuration for more information.
|
||||
*/
|
||||
const config: QuartzConfig = {
|
||||
configuration: {
|
||||
pageTitle: "Matsuura Tomoya Research Note",
|
||||
@ -14,6 +20,8 @@ const config: QuartzConfig = {
|
||||
ignorePatterns: ["private", "templates", ".obsidian"],
|
||||
defaultDateType: "modified",
|
||||
theme: {
|
||||
fontOrigin: "googleFonts",
|
||||
cdnCaching: true,
|
||||
typography: {
|
||||
header: "Schibsted Grotesk",
|
||||
body: "Source Sans Pro",
|
||||
@ -29,6 +37,7 @@ const config: QuartzConfig = {
|
||||
secondary: "#207e8f",
|
||||
tertiary: "#84a59d",
|
||||
highlight: "rgba(243,143,51,0.25)",
|
||||
textHighlight: "#fff23688",
|
||||
},
|
||||
darkMode: {
|
||||
light: "#161618",
|
||||
@ -39,6 +48,7 @@ const config: QuartzConfig = {
|
||||
secondary: "#7b97aa",
|
||||
tertiary: "#84a59d",
|
||||
highlight: "rgba(143, 159, 169, 0.15)",
|
||||
textHighlight: "#b3aa0288",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -50,17 +60,24 @@ const config: QuartzConfig = {
|
||||
priority: ["frontmatter", "git",], // you can add 'git' here for last modified from Git but this makes the build slower
|
||||
}),
|
||||
Plugin.Latex({ renderEngine: "katex" }),
|
||||
Plugin.SyntaxHighlighting(),
|
||||
Plugin.SyntaxHighlighting({
|
||||
theme: {
|
||||
light: "github-light",
|
||||
dark: "github-dark",
|
||||
},
|
||||
keepBackground: false,
|
||||
}),
|
||||
Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }),
|
||||
Plugin.GitHubFlavoredMarkdown(),
|
||||
Plugin.TableOfContents(),
|
||||
Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
|
||||
Plugin.Description(),
|
||||
Plugin.Latex({ renderEngine: "katex" }),
|
||||
],
|
||||
filters: [Plugin.RemoveDrafts()],
|
||||
emitters: [
|
||||
Plugin.AliasRedirects(),
|
||||
Plugin.ComponentResources({ fontOrigin: "googleFonts" }),
|
||||
Plugin.ComponentResources(),
|
||||
Plugin.ContentPage(),
|
||||
Plugin.FolderPage(),
|
||||
Plugin.TagPage(),
|
||||
|
@ -5,6 +5,7 @@ import * as Component from "./quartz/components"
|
||||
export const sharedPageComponents: SharedLayout = {
|
||||
head: Component.Head(),
|
||||
header: [],
|
||||
afterBody: [],
|
||||
footer: Component.Footer({
|
||||
links: {
|
||||
"Top": "https://matsuuratomoya.com",
|
||||
@ -63,5 +64,9 @@ export const defaultListPageLayout: PageLayout = {
|
||||
Component.Darkmode(),
|
||||
Component.DesktopOnly(Component.Explorer()),
|
||||
],
|
||||
right: [],
|
||||
right: [
|
||||
Component.Graph(),
|
||||
Component.DesktopOnly(Component.TableOfContents()),
|
||||
Component.Backlinks(),
|
||||
],
|
||||
}
|
210
quartz/build.ts
210
quartz/build.ts
@ -17,6 +17,10 @@ import { glob, toPosixPath } from "./util/glob"
|
||||
import { trace } from "./util/trace"
|
||||
import { options } from "./util/sourcemap"
|
||||
import { Mutex } from "async-mutex"
|
||||
import DepGraph from "./depgraph"
|
||||
import { getStaticResourcesFromPlugins } from "./plugins"
|
||||
|
||||
type Dependencies = Record<string, DepGraph<FilePath> | null>
|
||||
|
||||
type BuildData = {
|
||||
ctx: BuildCtx
|
||||
@ -29,8 +33,11 @@ type BuildData = {
|
||||
toRebuild: Set<FilePath>
|
||||
toRemove: Set<FilePath>
|
||||
lastBuildMs: number
|
||||
dependencies: Dependencies
|
||||
}
|
||||
|
||||
type FileEvent = "add" | "change" | "delete"
|
||||
|
||||
async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
||||
const ctx: BuildCtx = {
|
||||
argv,
|
||||
@ -53,7 +60,7 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
||||
|
||||
const release = await mut.acquire()
|
||||
perf.addEvent("clean")
|
||||
await rimraf(output)
|
||||
await rimraf(path.join(output, "*"), { glob: true })
|
||||
console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`)
|
||||
|
||||
perf.addEvent("glob")
|
||||
@ -68,12 +75,24 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
||||
|
||||
const parsedFiles = await parseMarkdown(ctx, filePaths)
|
||||
const filteredContent = filterContent(ctx, parsedFiles)
|
||||
|
||||
const dependencies: Record<string, DepGraph<FilePath> | null> = {}
|
||||
|
||||
// Only build dependency graphs if we're doing a fast rebuild
|
||||
if (argv.fastRebuild) {
|
||||
const staticResources = getStaticResourcesFromPlugins(ctx)
|
||||
for (const emitter of cfg.plugins.emitters) {
|
||||
dependencies[emitter.name] =
|
||||
(await emitter.getDependencyGraph?.(ctx, filteredContent, staticResources)) ?? null
|
||||
}
|
||||
}
|
||||
|
||||
await emitContent(ctx, filteredContent)
|
||||
console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`))
|
||||
release()
|
||||
|
||||
if (argv.serve) {
|
||||
return startServing(ctx, mut, parsedFiles, clientRefresh)
|
||||
return startServing(ctx, mut, parsedFiles, clientRefresh, dependencies)
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,9 +102,11 @@ async function startServing(
|
||||
mut: Mutex,
|
||||
initialContent: ProcessedContent[],
|
||||
clientRefresh: () => void,
|
||||
dependencies: Dependencies, // emitter name: dep graph
|
||||
) {
|
||||
const { argv } = ctx
|
||||
|
||||
// cache file parse results
|
||||
const contentMap = new Map<FilePath, ProcessedContent>()
|
||||
for (const content of initialContent) {
|
||||
const [_tree, vfile] = content
|
||||
@ -95,6 +116,7 @@ async function startServing(
|
||||
const buildData: BuildData = {
|
||||
ctx,
|
||||
mut,
|
||||
dependencies,
|
||||
contentMap,
|
||||
ignored: await isGitIgnored(),
|
||||
initialSlugs: ctx.allSlugs,
|
||||
@ -110,19 +132,193 @@ async function startServing(
|
||||
ignoreInitial: true,
|
||||
})
|
||||
|
||||
const buildFromEntry = argv.fastRebuild ? partialRebuildFromEntrypoint : rebuildFromEntrypoint
|
||||
watcher
|
||||
.on("add", (fp) => rebuildFromEntrypoint(fp, "add", clientRefresh, buildData))
|
||||
.on("change", (fp) => rebuildFromEntrypoint(fp, "change", clientRefresh, buildData))
|
||||
.on("unlink", (fp) => rebuildFromEntrypoint(fp, "delete", clientRefresh, buildData))
|
||||
.on("add", (fp) => buildFromEntry(fp, "add", clientRefresh, buildData))
|
||||
.on("change", (fp) => buildFromEntry(fp, "change", clientRefresh, buildData))
|
||||
.on("unlink", (fp) => buildFromEntry(fp, "delete", clientRefresh, buildData))
|
||||
|
||||
return async () => {
|
||||
await watcher.close()
|
||||
}
|
||||
}
|
||||
|
||||
async function partialRebuildFromEntrypoint(
|
||||
filepath: string,
|
||||
action: FileEvent,
|
||||
clientRefresh: () => void,
|
||||
buildData: BuildData, // note: this function mutates buildData
|
||||
) {
|
||||
const { ctx, ignored, dependencies, contentMap, mut, toRemove } = buildData
|
||||
const { argv, cfg } = ctx
|
||||
|
||||
// don't do anything for gitignored files
|
||||
if (ignored(filepath)) {
|
||||
return
|
||||
}
|
||||
|
||||
const buildStart = new Date().getTime()
|
||||
buildData.lastBuildMs = buildStart
|
||||
const release = await mut.acquire()
|
||||
if (buildData.lastBuildMs > buildStart) {
|
||||
release()
|
||||
return
|
||||
}
|
||||
|
||||
const perf = new PerfTimer()
|
||||
console.log(chalk.yellow("Detected change, rebuilding..."))
|
||||
|
||||
// UPDATE DEP GRAPH
|
||||
const fp = joinSegments(argv.directory, toPosixPath(filepath)) as FilePath
|
||||
|
||||
const staticResources = getStaticResourcesFromPlugins(ctx)
|
||||
let processedFiles: ProcessedContent[] = []
|
||||
|
||||
switch (action) {
|
||||
case "add":
|
||||
// add to cache when new file is added
|
||||
processedFiles = await parseMarkdown(ctx, [fp])
|
||||
processedFiles.forEach(([tree, vfile]) => contentMap.set(vfile.data.filePath!, [tree, vfile]))
|
||||
|
||||
// update the dep graph by asking all emitters whether they depend on this file
|
||||
for (const emitter of cfg.plugins.emitters) {
|
||||
const emitterGraph =
|
||||
(await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null
|
||||
|
||||
if (emitterGraph) {
|
||||
const existingGraph = dependencies[emitter.name]
|
||||
if (existingGraph !== null) {
|
||||
existingGraph.mergeGraph(emitterGraph)
|
||||
} else {
|
||||
// might be the first time we're adding a mardown file
|
||||
dependencies[emitter.name] = emitterGraph
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
case "change":
|
||||
// invalidate cache when file is changed
|
||||
processedFiles = await parseMarkdown(ctx, [fp])
|
||||
processedFiles.forEach(([tree, vfile]) => contentMap.set(vfile.data.filePath!, [tree, vfile]))
|
||||
|
||||
// only content files can have added/removed dependencies because of transclusions
|
||||
if (path.extname(fp) === ".md") {
|
||||
for (const emitter of cfg.plugins.emitters) {
|
||||
// get new dependencies from all emitters for this file
|
||||
const emitterGraph =
|
||||
(await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null
|
||||
|
||||
// only update the graph if the emitter plugin uses the changed file
|
||||
// eg. Assets plugin ignores md files, so we skip updating the graph
|
||||
if (emitterGraph?.hasNode(fp)) {
|
||||
// merge the new dependencies into the dep graph
|
||||
dependencies[emitter.name]?.updateIncomingEdgesForNode(emitterGraph, fp)
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
case "delete":
|
||||
toRemove.add(fp)
|
||||
break
|
||||
}
|
||||
|
||||
if (argv.verbose) {
|
||||
console.log(`Updated dependency graphs in ${perf.timeSince()}`)
|
||||
}
|
||||
|
||||
// EMIT
|
||||
perf.addEvent("rebuild")
|
||||
let emittedFiles = 0
|
||||
|
||||
for (const emitter of cfg.plugins.emitters) {
|
||||
const depGraph = dependencies[emitter.name]
|
||||
|
||||
// emitter hasn't defined a dependency graph. call it with all processed files
|
||||
if (depGraph === null) {
|
||||
if (argv.verbose) {
|
||||
console.log(
|
||||
`Emitter ${emitter.name} doesn't define a dependency graph. Calling it with all files...`,
|
||||
)
|
||||
}
|
||||
|
||||
const files = [...contentMap.values()].filter(
|
||||
([_node, vfile]) => !toRemove.has(vfile.data.filePath!),
|
||||
)
|
||||
|
||||
const emittedFps = await emitter.emit(ctx, files, staticResources)
|
||||
|
||||
if (ctx.argv.verbose) {
|
||||
for (const file of emittedFps) {
|
||||
console.log(`[emit:${emitter.name}] ${file}`)
|
||||
}
|
||||
}
|
||||
|
||||
emittedFiles += emittedFps.length
|
||||
continue
|
||||
}
|
||||
|
||||
// only call the emitter if it uses this file
|
||||
if (depGraph.hasNode(fp)) {
|
||||
// re-emit using all files that are needed for the downstream of this file
|
||||
// eg. for ContentIndex, the dep graph could be:
|
||||
// a.md --> contentIndex.json
|
||||
// b.md ------^
|
||||
//
|
||||
// if a.md changes, we need to re-emit contentIndex.json,
|
||||
// and supply [a.md, b.md] to the emitter
|
||||
const upstreams = [...depGraph.getLeafNodeAncestors(fp)] as FilePath[]
|
||||
|
||||
const upstreamContent = upstreams
|
||||
// filter out non-markdown files
|
||||
.filter((file) => contentMap.has(file))
|
||||
// if file was deleted, don't give it to the emitter
|
||||
.filter((file) => !toRemove.has(file))
|
||||
.map((file) => contentMap.get(file)!)
|
||||
|
||||
const emittedFps = await emitter.emit(ctx, upstreamContent, staticResources)
|
||||
|
||||
if (ctx.argv.verbose) {
|
||||
for (const file of emittedFps) {
|
||||
console.log(`[emit:${emitter.name}] ${file}`)
|
||||
}
|
||||
}
|
||||
|
||||
emittedFiles += emittedFps.length
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`)
|
||||
|
||||
// CLEANUP
|
||||
const destinationsToDelete = new Set<FilePath>()
|
||||
for (const file of toRemove) {
|
||||
// remove from cache
|
||||
contentMap.delete(file)
|
||||
Object.values(dependencies).forEach((depGraph) => {
|
||||
// remove the node from dependency graphs
|
||||
depGraph?.removeNode(file)
|
||||
// remove any orphan nodes. eg if a.md is deleted, a.html is orphaned and should be removed
|
||||
const orphanNodes = depGraph?.removeOrphanNodes()
|
||||
orphanNodes?.forEach((node) => {
|
||||
// only delete files that are in the output directory
|
||||
if (node.startsWith(argv.output)) {
|
||||
destinationsToDelete.add(node)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
await rimraf([...destinationsToDelete])
|
||||
|
||||
console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`))
|
||||
|
||||
toRemove.clear()
|
||||
release()
|
||||
clientRefresh()
|
||||
}
|
||||
|
||||
async function rebuildFromEntrypoint(
|
||||
fp: string,
|
||||
action: "add" | "change" | "delete",
|
||||
action: FileEvent,
|
||||
clientRefresh: () => void,
|
||||
buildData: BuildData, // note: this function mutates buildData
|
||||
) {
|
||||
@ -190,7 +386,7 @@ async function rebuildFromEntrypoint(
|
||||
|
||||
// TODO: we can probably traverse the link graph to figure out what's safe to delete here
|
||||
// instead of just deleting everything
|
||||
await rimraf(argv.output)
|
||||
await rimraf(path.join(argv.output, ".*"), { glob: true })
|
||||
await emitContent(ctx, filteredContent)
|
||||
console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`))
|
||||
} catch (err) {
|
||||
|
@ -19,6 +19,25 @@ export type Analytics =
|
||||
websiteId: string
|
||||
host?: string
|
||||
}
|
||||
| {
|
||||
provider: "goatcounter"
|
||||
websiteId: string
|
||||
host?: string
|
||||
scriptSrc?: string
|
||||
}
|
||||
| {
|
||||
provider: "posthog"
|
||||
apiKey: string
|
||||
host?: string
|
||||
}
|
||||
| {
|
||||
provider: "tinylytics"
|
||||
siteId: string
|
||||
}
|
||||
| {
|
||||
provider: "cabin"
|
||||
host?: string
|
||||
}
|
||||
|
||||
export interface GlobalConfiguration {
|
||||
pageTitle: string
|
||||
@ -35,7 +54,6 @@ export interface GlobalConfiguration {
|
||||
/** Base URL to use for CNAME files, sitemaps, and RSS feeds that require an absolute URL.
|
||||
* Quartz will avoid using this as much as possible and use relative URLs most of the time
|
||||
*/
|
||||
repoUrl?: string
|
||||
baseUrl?: string
|
||||
theme: Theme
|
||||
/**
|
||||
@ -59,10 +77,11 @@ export interface FullPageLayout {
|
||||
header: QuartzComponent[]
|
||||
beforeBody: QuartzComponent[]
|
||||
pageBody: QuartzComponent
|
||||
afterBody: QuartzComponent[]
|
||||
left: QuartzComponent[]
|
||||
right: QuartzComponent[]
|
||||
footer: QuartzComponent
|
||||
}
|
||||
|
||||
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">
|
||||
|
@ -71,6 +71,11 @@ export const BuildArgv = {
|
||||
default: false,
|
||||
describe: "run a local server to live-preview your Quartz",
|
||||
},
|
||||
fastRebuild: {
|
||||
boolean: true,
|
||||
default: false,
|
||||
describe: "[experimental] rebuild only the changed files",
|
||||
},
|
||||
baseDir: {
|
||||
string: true,
|
||||
default: "",
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import { classNames } from "../util/lang"
|
||||
|
||||
function ArticleTitle({ fileData, displayClass }: QuartzComponentProps) {
|
||||
const ArticleTitle: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => {
|
||||
const title = fileData.frontmatter?.title
|
||||
if (title) {
|
||||
return <h1 class={classNames(displayClass, "article-title")}>{title}</h1>
|
||||
|
@ -1,10 +1,15 @@
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import style from "./styles/backlinks.scss"
|
||||
import { resolveRelative, simplifySlug } from "../util/path"
|
||||
import { i18n } from "../i18n"
|
||||
import { classNames } from "../util/lang"
|
||||
|
||||
function Backlinks({ fileData, allFiles, displayClass, cfg }: QuartzComponentProps) {
|
||||
const Backlinks: QuartzComponent = ({
|
||||
fileData,
|
||||
allFiles,
|
||||
displayClass,
|
||||
cfg,
|
||||
}: QuartzComponentProps) => {
|
||||
const slug = simplifySlug(fileData.slug!)
|
||||
const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
|
||||
return (
|
||||
|
@ -1,9 +1,9 @@
|
||||
// @ts-ignore
|
||||
import clipboardScript from "./scripts/clipboard.inline"
|
||||
import clipboardStyle from "./styles/clipboard.scss"
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
|
||||
function Body({ children }: QuartzComponentProps) {
|
||||
const Body: QuartzComponent = ({ children }: QuartzComponentProps) => {
|
||||
return <div id="quartz-body">{children}</div>
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import breadcrumbsStyle from "./styles/breadcrumbs.scss"
|
||||
import { FullSlug, SimpleSlug, resolveRelative } from "../util/path"
|
||||
import { FullSlug, SimpleSlug, joinSegments, resolveRelative } from "../util/path"
|
||||
import { QuartzPluginData } from "../plugins/vfile"
|
||||
import { classNames } from "../util/lang"
|
||||
|
||||
@ -54,7 +54,11 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
||||
// computed index of folder name to its associated file data
|
||||
let folderIndex: Map<string, QuartzPluginData> | undefined
|
||||
|
||||
function Breadcrumbs({ fileData, allFiles, displayClass }: QuartzComponentProps) {
|
||||
const Breadcrumbs: QuartzComponent = ({
|
||||
fileData,
|
||||
allFiles,
|
||||
displayClass,
|
||||
}: QuartzComponentProps) => {
|
||||
// Hide crumbs on root if enabled
|
||||
if (options.hideOnRoot && fileData.slug === "index") {
|
||||
return <></>
|
||||
@ -68,13 +72,9 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
||||
folderIndex = new Map()
|
||||
// construct the index for the first time
|
||||
for (const file of allFiles) {
|
||||
if (file.slug?.endsWith("index")) {
|
||||
const folderParts = file.slug?.split("/")
|
||||
// 2nd last to exclude the /index
|
||||
const folderName = folderParts?.at(-2)
|
||||
if (folderName) {
|
||||
folderIndex.set(folderName, file)
|
||||
}
|
||||
if (folderParts?.at(-1) === "index") {
|
||||
folderIndex.set(folderParts.slice(0, -1).join("/"), file)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -82,13 +82,17 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
||||
// Split slug into hierarchy/parts
|
||||
const slugParts = fileData.slug?.split("/")
|
||||
if (slugParts) {
|
||||
// is tag breadcrumb?
|
||||
const isTagPath = slugParts[0] === "tags"
|
||||
|
||||
// full path until current part
|
||||
let currentPath = ""
|
||||
|
||||
for (let i = 0; i < slugParts.length - 1; i++) {
|
||||
let curPathSegment = slugParts[i]
|
||||
|
||||
// Try to resolve frontmatter folder title
|
||||
const currentFile = folderIndex?.get(curPathSegment)
|
||||
const currentFile = folderIndex?.get(slugParts.slice(0, i + 1).join("/"))
|
||||
if (currentFile) {
|
||||
const title = currentFile.frontmatter!.title
|
||||
if (title !== "index") {
|
||||
@ -97,10 +101,15 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
||||
}
|
||||
|
||||
// Add current slug to full path
|
||||
currentPath += slugParts[i] + "/"
|
||||
currentPath = joinSegments(currentPath, slugParts[i])
|
||||
const includeTrailingSlash = !isTagPath || i < 1
|
||||
|
||||
// Format and add current crumb
|
||||
const crumb = formatCrumb(curPathSegment, fileData.slug!, currentPath as SimpleSlug)
|
||||
const crumb = formatCrumb(
|
||||
curPathSegment,
|
||||
fileData.slug!,
|
||||
(currentPath + (includeTrailingSlash ? "/" : "")) as SimpleSlug,
|
||||
)
|
||||
crumbs.push(crumb)
|
||||
}
|
||||
|
||||
@ -125,5 +134,6 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
||||
)
|
||||
}
|
||||
Breadcrumbs.css = breadcrumbsStyle
|
||||
|
||||
return Breadcrumbs
|
||||
}) satisfies QuartzComponentConstructor
|
||||
|
44
quartz/components/Comments.tsx
Normal file
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>
|
@ -2,17 +2,21 @@ import { formatDate, getDate } from "./Date"
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import readingTime from "reading-time"
|
||||
import { classNames } from "../util/lang"
|
||||
import { JSX } from "preact/jsx-runtime"
|
||||
import { i18n } from "../i18n"
|
||||
import { JSX } from "preact"
|
||||
import style from "./styles/contentMeta.scss"
|
||||
|
||||
interface ContentMetaOptions {
|
||||
/**
|
||||
* Whether to display reading time
|
||||
*/
|
||||
showReadingTime: boolean
|
||||
showComma: boolean
|
||||
}
|
||||
|
||||
const defaultOptions: ContentMetaOptions = {
|
||||
showReadingTime: true,
|
||||
showComma: true,
|
||||
}
|
||||
|
||||
export default ((opts?: Partial<ContentMetaOptions>) => {
|
||||
@ -21,42 +25,28 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
|
||||
|
||||
function ContentMetadata({ cfg, fileData, displayClass }: QuartzComponentProps) {
|
||||
const text = fileData.text
|
||||
const filepath = fileData.filePath
|
||||
if (text && filepath) {
|
||||
const fileRepoUrl: string = `${cfg.repoUrl}/commits/branch/v4/${filepath}`
|
||||
const segments: JSX.Element[] = []
|
||||
// const segments: string[] = []
|
||||
|
||||
if (fileData.dates?.created) {
|
||||
segments.push(<span>created: {formatDate(fileData.dates.created, cfg.locale)}</span>)
|
||||
}
|
||||
if (fileData.dates?.modified) {
|
||||
segments.push(<span> updated: {formatDate(fileData.dates.modified, cfg.locale)}</span>)
|
||||
if (text) {
|
||||
const segments: (string | JSX.Element)[] = []
|
||||
|
||||
if (fileData.dates) {
|
||||
segments.push(formatDate(getDate(cfg, fileData)!, cfg.locale))
|
||||
}
|
||||
|
||||
// Display reading time if enabled
|
||||
if (options.showReadingTime) {
|
||||
const { text: timeTaken, words: _words } = readingTime(text)
|
||||
segments.push(<span>{timeTaken}</span>)
|
||||
const { minutes, words: _words } = readingTime(text)
|
||||
const displayedTime = i18n(cfg.locale).components.contentMeta.readingTime({
|
||||
minutes: Math.ceil(minutes),
|
||||
})
|
||||
segments.push(displayedTime)
|
||||
}
|
||||
segments.push(
|
||||
<a
|
||||
href={fileRepoUrl}
|
||||
class="external"
|
||||
target="_blank"
|
||||
>
|
||||
view history
|
||||
<svg class="external-icon" viewBox="0 0 512 512"><path d="M320 0H288V64h32 82.7L201.4 265.4 178.7 288 224 333.3l22.6-22.6L448 109.3V192v32h64V192 32 0H480 320zM32 32H0V64 480v32H32 456h32V480 352 320H424v32 96H64V96h96 32V32H160 32z"></path></svg>
|
||||
</a>,
|
||||
)
|
||||
|
||||
const segmentsElements = segments.map((segment) => <span>{segment}</span>)
|
||||
|
||||
return (
|
||||
<p class={classNames(displayClass, "content-meta")}>
|
||||
{segments.map((meta, idx) => (
|
||||
<>
|
||||
{meta}
|
||||
</>
|
||||
))}
|
||||
<p show-comma={options.showComma} class={classNames(displayClass, "content-meta")}>
|
||||
{segmentsElements}
|
||||
</p>
|
||||
)
|
||||
} else {
|
||||
@ -64,14 +54,7 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
|
||||
}
|
||||
}
|
||||
|
||||
ContentMetadata.css = `
|
||||
.content-meta {
|
||||
margin-top: 0;
|
||||
color: var(--gray);
|
||||
}
|
||||
.content-meta span{
|
||||
margin-right: 10px;
|
||||
}
|
||||
`
|
||||
ContentMetadata.css = style
|
||||
|
||||
return ContentMetadata
|
||||
}) satisfies QuartzComponentConstructor
|
||||
|
@ -3,11 +3,11 @@
|
||||
// see: https://v8.dev/features/modules#defer
|
||||
import darkmodeScript from "./scripts/darkmode.inline"
|
||||
import styles from "./styles/darkmode.scss"
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import { i18n } from "../i18n"
|
||||
import { classNames } from "../util/lang"
|
||||
|
||||
function Darkmode({ displayClass, cfg }: QuartzComponentProps) {
|
||||
const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
||||
return (
|
||||
<div class={classNames(displayClass, "darkmode")}>
|
||||
<input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} />
|
||||
|
@ -3,7 +3,7 @@ import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } fro
|
||||
export default ((component?: QuartzComponent) => {
|
||||
if (component) {
|
||||
const Component = component
|
||||
function DesktopOnly(props: QuartzComponentProps) {
|
||||
const DesktopOnly: QuartzComponent = (props: QuartzComponentProps) => {
|
||||
return <Component displayClass="desktop-only" {...props} />
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import explorerStyle from "./styles/explorer.scss"
|
||||
|
||||
// @ts-ignore
|
||||
@ -75,7 +75,12 @@ export default ((userOpts?: Partial<Options>) => {
|
||||
jsonTree = JSON.stringify(folders)
|
||||
}
|
||||
|
||||
function Explorer({ cfg, allFiles, displayClass, fileData }: QuartzComponentProps) {
|
||||
const Explorer: QuartzComponent = ({
|
||||
cfg,
|
||||
allFiles,
|
||||
displayClass,
|
||||
fileData,
|
||||
}: QuartzComponentProps) => {
|
||||
constructFileTree(allFiles)
|
||||
return (
|
||||
<div class={classNames(displayClass, "explorer")}>
|
||||
@ -87,7 +92,7 @@ export default ((userOpts?: Partial<Options>) => {
|
||||
data-savestate={opts.useSavedState}
|
||||
data-tree={jsonTree}
|
||||
>
|
||||
<h1>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h1>
|
||||
<h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
|
@ -168,10 +168,8 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
|
||||
const isDefaultOpen = opts.folderDefaultState === "open"
|
||||
|
||||
// Calculate current folderPath
|
||||
let folderPath = ""
|
||||
if (node.name !== "") {
|
||||
folderPath = joinSegments(fullPath ?? "", node.name)
|
||||
}
|
||||
const folderPath = node.name !== "" ? joinSegments(fullPath ?? "", node.name) : ""
|
||||
const href = resolveRelative(fileData.slug!, folderPath as SimpleSlug) + "/"
|
||||
|
||||
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 */}
|
||||
<div key={node.name} data-folderpath={folderPath}>
|
||||
{folderBehavior === "link" ? (
|
||||
<a
|
||||
href={resolveRelative(fileData.slug!, folderPath as SimpleSlug)}
|
||||
data-for={node.name}
|
||||
class="folder-title"
|
||||
>
|
||||
<a href={href} data-for={node.name} class="folder-title">
|
||||
{node.displayName}
|
||||
</a>
|
||||
) : (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import style from "./styles/footer.scss"
|
||||
import { version } from "../../package.json"
|
||||
import { i18n } from "../i18n"
|
||||
@ -8,12 +8,11 @@ interface Options {
|
||||
}
|
||||
|
||||
export default ((opts?: Options) => {
|
||||
function Footer({ displayClass, cfg }: QuartzComponentProps) {
|
||||
const Footer: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
||||
const year = new Date().getFullYear()
|
||||
const links = opts?.links ?? []
|
||||
return (
|
||||
<footer class={`${displayClass ?? ""}`}>
|
||||
<hr />
|
||||
<p>
|
||||
{i18n(cfg.locale).components.footer.createdWith}{" "}
|
||||
<a href="https://quartz.jzhao.xyz/">Quartz v{version}</a> © {year}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
// @ts-ignore
|
||||
import script from "./scripts/graph.inline"
|
||||
import style from "./styles/graph.scss"
|
||||
@ -17,6 +17,7 @@ export interface D3Config {
|
||||
opacityScale: number
|
||||
removeTags: string[]
|
||||
showTags: boolean
|
||||
focusOnHover?: boolean
|
||||
}
|
||||
|
||||
interface GraphOptions {
|
||||
@ -37,6 +38,7 @@ const defaultOptions: GraphOptions = {
|
||||
opacityScale: 1,
|
||||
showTags: true,
|
||||
removeTags: [],
|
||||
focusOnHover: false,
|
||||
},
|
||||
globalGraph: {
|
||||
drag: true,
|
||||
@ -50,11 +52,12 @@ const defaultOptions: GraphOptions = {
|
||||
opacityScale: 1,
|
||||
showTags: true,
|
||||
removeTags: [],
|
||||
focusOnHover: true,
|
||||
},
|
||||
}
|
||||
|
||||
export default ((opts?: GraphOptions) => {
|
||||
function Graph({ displayClass, cfg }: QuartzComponentProps) {
|
||||
const Graph: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
||||
const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph }
|
||||
const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph }
|
||||
return (
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { i18n } from "../i18n"
|
||||
import { FullSlug, _stripSlashes, joinSegments, pathToRoot } from "../util/path"
|
||||
import { FullSlug, joinSegments, pathToRoot } from "../util/path"
|
||||
import { JSResourceToScriptElement } from "../util/resources"
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import { googleFontHref } from "../util/theme"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
|
||||
export default (() => {
|
||||
function Head({ cfg, fileData, externalResources }: QuartzComponentProps) {
|
||||
const Head: QuartzComponent = ({ cfg, fileData, externalResources }: QuartzComponentProps) => {
|
||||
const title = fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title
|
||||
const description =
|
||||
fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description
|
||||
@ -21,6 +22,13 @@ export default (() => {
|
||||
<head>
|
||||
<title>{title}</title>
|
||||
<meta charSet="utf-8" />
|
||||
{cfg.theme.cdnCaching && cfg.theme.fontOrigin === "googleFonts" && (
|
||||
<>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link rel="stylesheet" href={googleFontHref(cfg.theme)} />
|
||||
</>
|
||||
)}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
@ -30,8 +38,6 @@ export default (() => {
|
||||
<link rel="icon" href={iconPath} />
|
||||
<meta name="description" content={description} />
|
||||
<meta name="generator" content="Quartz" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
{css.map((href) => (
|
||||
<link key={href} href={href} rel="stylesheet" type="text/css" spa-preserve />
|
||||
))}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
|
||||
function Header({ children }: QuartzComponentProps) {
|
||||
const Header: QuartzComponent = ({ children }: QuartzComponentProps) => {
|
||||
return children.length > 0 ? <header>{children}</header> : null
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } fro
|
||||
export default ((component?: QuartzComponent) => {
|
||||
if (component) {
|
||||
const Component = component
|
||||
function MobileOnly(props: QuartzComponentProps) {
|
||||
const MobileOnly: QuartzComponent = (props: QuartzComponentProps) => {
|
||||
return <Component displayClass="mobile-only" {...props} />
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { FullSlug, resolveRelative } from "../util/path"
|
||||
import { QuartzPluginData } from "../plugins/vfile"
|
||||
import { Date, getDate } from "./Date"
|
||||
import { QuartzComponentProps } from "./types"
|
||||
import { QuartzComponent, QuartzComponentProps } from "./types"
|
||||
import { GlobalConfiguration } from "../cfg"
|
||||
|
||||
export function byDateAndAlphabetical(
|
||||
cfg: GlobalConfiguration,
|
||||
): (f1: QuartzPluginData, f2: QuartzPluginData) => number {
|
||||
export type SortFn = (f1: QuartzPluginData, f2: QuartzPluginData) => number
|
||||
|
||||
export function byDateAndAlphabetical(cfg: GlobalConfiguration): SortFn {
|
||||
return (f1, f2) => {
|
||||
if (f1.dates && f2.dates) {
|
||||
// sort descending
|
||||
@ -27,10 +27,12 @@ export function byDateAndAlphabetical(
|
||||
|
||||
type Props = {
|
||||
limit?: number
|
||||
sort?: SortFn
|
||||
} & QuartzComponentProps
|
||||
|
||||
export function PageList({ cfg, fileData, allFiles, limit }: Props) {
|
||||
let list = allFiles.sort(byDateAndAlphabetical(cfg))
|
||||
export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit, sort }: Props) => {
|
||||
const sorter = sort ?? byDateAndAlphabetical(cfg)
|
||||
let list = allFiles.sort(sorter)
|
||||
if (limit) {
|
||||
list = list.slice(0, limit)
|
||||
}
|
||||
@ -63,7 +65,7 @@ export function PageList({ cfg, fileData, allFiles, limit }: Props) {
|
||||
class="internal tag-link"
|
||||
href={resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug)}
|
||||
>
|
||||
#{tag}
|
||||
{tag}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
|
@ -1,20 +1,21 @@
|
||||
import { pathToRoot } from "../util/path"
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import { classNames } from "../util/lang"
|
||||
import { i18n } from "../i18n"
|
||||
|
||||
function PageTitle({ fileData, cfg, displayClass }: QuartzComponentProps) {
|
||||
const PageTitle: QuartzComponent = ({ fileData, cfg, displayClass }: QuartzComponentProps) => {
|
||||
const title = cfg?.pageTitle ?? i18n(cfg.locale).propertyDefaults.title
|
||||
const baseDir = pathToRoot(fileData.slug!)
|
||||
return (
|
||||
<h1 class={classNames(displayClass, "page-title")}>
|
||||
<h2 class={classNames(displayClass, "page-title")}>
|
||||
<a href={baseDir}>{title}</a>
|
||||
</h1>
|
||||
</h2>
|
||||
)
|
||||
}
|
||||
|
||||
PageTitle.css = `
|
||||
.page-title {
|
||||
font-size: 1.75rem;
|
||||
margin: 0;
|
||||
}
|
||||
`
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import { FullSlug, SimpleSlug, resolveRelative } from "../util/path"
|
||||
import { QuartzPluginData } from "../plugins/vfile"
|
||||
import { byDateAndAlphabetical } from "./PageList"
|
||||
@ -12,6 +12,7 @@ interface Options {
|
||||
title?: string
|
||||
limit: number
|
||||
linkToMore: SimpleSlug | false
|
||||
showTags: boolean
|
||||
filter: (f: QuartzPluginData) => boolean
|
||||
sort: (f1: QuartzPluginData, f2: QuartzPluginData) => number
|
||||
}
|
||||
@ -19,12 +20,18 @@ interface Options {
|
||||
const defaultOptions = (cfg: GlobalConfiguration): Options => ({
|
||||
limit: 3,
|
||||
linkToMore: false,
|
||||
showTags: true,
|
||||
filter: () => true,
|
||||
sort: byDateAndAlphabetical(cfg),
|
||||
})
|
||||
|
||||
export default ((userOpts?: Partial<Options>) => {
|
||||
function RecentNotes({ allFiles, fileData, displayClass, cfg }: QuartzComponentProps) {
|
||||
const RecentNotes: QuartzComponent = ({
|
||||
allFiles,
|
||||
fileData,
|
||||
displayClass,
|
||||
cfg,
|
||||
}: QuartzComponentProps) => {
|
||||
const opts = { ...defaultOptions(cfg), ...userOpts }
|
||||
const pages = allFiles.filter(opts.filter).sort(opts.sort)
|
||||
const remaining = Math.max(0, pages.length - opts.limit)
|
||||
@ -51,6 +58,7 @@ export default ((userOpts?: Partial<Options>) => {
|
||||
<Date date={getDate(cfg, page)!} locale={cfg.locale} />
|
||||
</p>
|
||||
)}
|
||||
{opts.showTags && (
|
||||
<ul class="tags">
|
||||
{tags.map((tag) => (
|
||||
<li>
|
||||
@ -58,11 +66,12 @@ export default ((userOpts?: Partial<Options>) => {
|
||||
class="internal tag-link"
|
||||
href={resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug)}
|
||||
>
|
||||
#{tag}
|
||||
{tag}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import style from "./styles/search.scss"
|
||||
// @ts-ignore
|
||||
import script from "./scripts/search.inline"
|
||||
@ -14,7 +14,7 @@ const defaultOptions: SearchOptions = {
|
||||
}
|
||||
|
||||
export default ((userOpts?: Partial<SearchOptions>) => {
|
||||
function Search({ displayClass, cfg }: QuartzComponentProps) {
|
||||
const Search: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
||||
const opts = { ...defaultOptions, ...userOpts }
|
||||
const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder
|
||||
return (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import legacyStyle from "./styles/legacyToc.scss"
|
||||
import modernStyle from "./styles/toc.scss"
|
||||
import { classNames } from "../util/lang"
|
||||
@ -15,7 +15,11 @@ const defaultOptions: Options = {
|
||||
layout: "modern",
|
||||
}
|
||||
|
||||
function TableOfContents({ fileData, displayClass, cfg }: QuartzComponentProps) {
|
||||
const TableOfContents: QuartzComponent = ({
|
||||
fileData,
|
||||
displayClass,
|
||||
cfg,
|
||||
}: QuartzComponentProps) => {
|
||||
if (!fileData.toc) {
|
||||
return null
|
||||
}
|
||||
@ -56,7 +60,7 @@ function TableOfContents({ fileData, displayClass, cfg }: QuartzComponentProps)
|
||||
TableOfContents.css = modernStyle
|
||||
TableOfContents.afterDOMLoaded = script
|
||||
|
||||
function LegacyTableOfContents({ fileData, cfg }: QuartzComponentProps) {
|
||||
const LegacyTableOfContents: QuartzComponent = ({ fileData, cfg }: QuartzComponentProps) => {
|
||||
if (!fileData.toc) {
|
||||
return null
|
||||
}
|
||||
|
@ -1,20 +1,19 @@
|
||||
import { pathToRoot, slugTag } from "../util/path"
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import { classNames } from "../util/lang"
|
||||
|
||||
function TagList({ fileData, displayClass }: QuartzComponentProps) {
|
||||
const TagList: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => {
|
||||
const tags = fileData.frontmatter?.tags
|
||||
const baseDir = pathToRoot(fileData.slug!)
|
||||
if (tags && tags.length > 0) {
|
||||
return (
|
||||
<ul class={classNames(displayClass, "tags")}>
|
||||
{tags.map((tag) => {
|
||||
const display = `#${tag}`
|
||||
const linkDest = baseDir + `/tags/${slugTag(tag)}`
|
||||
return (
|
||||
<li>
|
||||
<a href={linkDest} class="internal tag-link">
|
||||
{display}
|
||||
{tag}
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
|
@ -19,6 +19,7 @@ import DesktopOnly from "./DesktopOnly"
|
||||
import MobileOnly from "./MobileOnly"
|
||||
import RecentNotes from "./RecentNotes"
|
||||
import Breadcrumbs from "./Breadcrumbs"
|
||||
import Comments from "./Comments"
|
||||
|
||||
export {
|
||||
ArticleTitle,
|
||||
@ -42,4 +43,5 @@ export {
|
||||
RecentNotes,
|
||||
NotFound,
|
||||
Breadcrumbs,
|
||||
Comments,
|
||||
}
|
||||
|
@ -1,11 +1,16 @@
|
||||
import { i18n } from "../../i18n"
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
||||
|
||||
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
|
||||
|
||||
function NotFound({ cfg }: QuartzComponentProps) {
|
||||
return (
|
||||
<article class="popover-hint">
|
||||
<h1>404</h1>
|
||||
<p>{i18n(cfg.locale).pages.error.notFound}</p>
|
||||
<a href={baseDir}>{i18n(cfg.locale).pages.error.home}</a>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { htmlToJsx } from "../../util/jsx"
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
||||
|
||||
function Content({ fileData, tree }: QuartzComponentProps) {
|
||||
const Content: QuartzComponent = ({ fileData, tree }: QuartzComponentProps) => {
|
||||
const content = htmlToJsx(fileData.filePath!, tree)
|
||||
const classes: string[] = fileData.frontmatter?.cssclasses ?? []
|
||||
const classString = ["popover-hint", ...classes].join(" ")
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
||||
import path from "path"
|
||||
|
||||
import style from "../styles/listPage.scss"
|
||||
import { PageList } from "../PageList"
|
||||
import { _stripSlashes, simplifySlug } from "../../util/path"
|
||||
import { PageList, SortFn } from "../PageList"
|
||||
import { stripSlashes, simplifySlug } from "../../util/path"
|
||||
import { Root } from "hast"
|
||||
import { htmlToJsx } from "../../util/jsx"
|
||||
import { i18n } from "../../i18n"
|
||||
@ -13,6 +13,7 @@ interface FolderContentOptions {
|
||||
* Whether to display number of folders
|
||||
*/
|
||||
showFolderCount: boolean
|
||||
sort?: SortFn
|
||||
}
|
||||
|
||||
const defaultOptions: FolderContentOptions = {
|
||||
@ -22,11 +23,11 @@ const defaultOptions: FolderContentOptions = {
|
||||
export default ((opts?: Partial<FolderContentOptions>) => {
|
||||
const options: FolderContentOptions = { ...defaultOptions, ...opts }
|
||||
|
||||
function FolderContent(props: QuartzComponentProps) {
|
||||
const FolderContent: QuartzComponent = (props: QuartzComponentProps) => {
|
||||
const { tree, fileData, allFiles, cfg } = props
|
||||
const folderSlug = _stripSlashes(simplifySlug(fileData.slug!))
|
||||
const folderSlug = stripSlashes(simplifySlug(fileData.slug!))
|
||||
const allPagesInFolder = allFiles.filter((file) => {
|
||||
const fileSlug = _stripSlashes(simplifySlug(file.slug!))
|
||||
const fileSlug = stripSlashes(simplifySlug(file.slug!))
|
||||
const prefixed = fileSlug.startsWith(folderSlug) && fileSlug !== folderSlug
|
||||
const folderParts = folderSlug.split(path.posix.sep)
|
||||
const fileParts = fileSlug.split(path.posix.sep)
|
||||
@ -37,6 +38,7 @@ export default ((opts?: Partial<FolderContentOptions>) => {
|
||||
const classes = ["popover-hint", ...cssClasses].join(" ")
|
||||
const listProps = {
|
||||
...props,
|
||||
sort: options.sort,
|
||||
allFiles: allPagesInFolder,
|
||||
}
|
||||
|
||||
@ -47,9 +49,7 @@ export default ((opts?: Partial<FolderContentOptions>) => {
|
||||
|
||||
return (
|
||||
<div class={classes}>
|
||||
<article>
|
||||
<p>{content}</p>
|
||||
</article>
|
||||
<article>{content}</article>
|
||||
<div class="page-listing">
|
||||
{options.showFolderCount && (
|
||||
<p>
|
||||
|
@ -1,14 +1,25 @@
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
||||
import style from "../styles/listPage.scss"
|
||||
import { PageList } from "../PageList"
|
||||
import { PageList, SortFn } from "../PageList"
|
||||
import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path"
|
||||
import { QuartzPluginData } from "../../plugins/vfile"
|
||||
import { Root } from "hast"
|
||||
import { htmlToJsx } from "../../util/jsx"
|
||||
import { i18n } from "../../i18n"
|
||||
|
||||
const numPages = 10
|
||||
function TagContent(props: QuartzComponentProps) {
|
||||
interface TagContentOptions {
|
||||
sort?: SortFn
|
||||
numPages: number
|
||||
}
|
||||
|
||||
const defaultOptions: TagContentOptions = {
|
||||
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
|
||||
|
||||
@ -52,26 +63,37 @@ function TagContent(props: QuartzComponentProps) {
|
||||
allFiles: pages,
|
||||
}
|
||||
|
||||
const contentPage = allFiles.filter((file) => file.slug === `tags/${tag}`)[0]
|
||||
const content = contentPage?.description
|
||||
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}
|
||||
{tag}
|
||||
</a>
|
||||
</h2>
|
||||
{content && <p>{content}</p>}
|
||||
<div class="page-listing">
|
||||
<p>
|
||||
{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}
|
||||
{pages.length > numPages && (
|
||||
{pages.length > options.numPages && (
|
||||
<>
|
||||
{" "}
|
||||
<span>
|
||||
{i18n(cfg.locale).pages.tagContent.showingFirst({ count: numPages })}
|
||||
{i18n(cfg.locale).pages.tagContent.showingFirst({
|
||||
count: options.numPages,
|
||||
})}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<PageList limit={numPages} {...listProps} />
|
||||
<PageList limit={options.numPages} {...listProps} sort={opts?.sort} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -98,7 +120,8 @@ function TagContent(props: QuartzComponentProps) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TagContent.css = style + PageList.css
|
||||
export default (() => TagContent) satisfies QuartzComponentConstructor
|
||||
TagContent.css = style + PageList.css
|
||||
return TagContent
|
||||
}) satisfies QuartzComponentConstructor
|
||||
|
@ -3,10 +3,9 @@ import { QuartzComponent, QuartzComponentProps } from "./types"
|
||||
import HeaderConstructor from "./Header"
|
||||
import BodyConstructor from "./Body"
|
||||
import { JSResourceToScriptElement, StaticResources } from "../util/resources"
|
||||
import { FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path"
|
||||
import { clone, FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path"
|
||||
import { visit } from "unist-util-visit"
|
||||
import { Root, Element, ElementContent } from "hast"
|
||||
import { QuartzPluginData } from "../plugins/vfile"
|
||||
import { GlobalConfiguration } from "../cfg"
|
||||
import { i18n } from "../i18n"
|
||||
|
||||
@ -15,11 +14,13 @@ interface RenderComponents {
|
||||
header: QuartzComponent[]
|
||||
beforeBody: QuartzComponent[]
|
||||
pageBody: QuartzComponent
|
||||
afterBody: QuartzComponent[]
|
||||
left: QuartzComponent[]
|
||||
right: QuartzComponent[]
|
||||
footer: QuartzComponent
|
||||
}
|
||||
|
||||
const headerRegex = new RegExp(/h[1-6]/)
|
||||
export function pageResources(
|
||||
baseDir: FullSlug | RelativeURL,
|
||||
staticResources: StaticResources,
|
||||
@ -52,18 +53,6 @@ export function pageResources(
|
||||
}
|
||||
}
|
||||
|
||||
let pageIndex: Map<FullSlug, QuartzPluginData> | undefined = undefined
|
||||
function getOrComputeFileIndex(allFiles: QuartzPluginData[]): Map<FullSlug, QuartzPluginData> {
|
||||
if (!pageIndex) {
|
||||
pageIndex = new Map()
|
||||
for (const file of allFiles) {
|
||||
pageIndex.set(file.slug!, file)
|
||||
}
|
||||
}
|
||||
|
||||
return pageIndex
|
||||
}
|
||||
|
||||
export function renderPage(
|
||||
cfg: GlobalConfiguration,
|
||||
slug: FullSlug,
|
||||
@ -71,14 +60,18 @@ export function renderPage(
|
||||
components: RenderComponents,
|
||||
pageResources: StaticResources,
|
||||
): string {
|
||||
// make a deep copy of the tree so we don't remove the transclusion references
|
||||
// for the file cached in contentMap in build.ts
|
||||
const root = clone(componentData.tree) as Root
|
||||
|
||||
// process transcludes in componentData
|
||||
visit(componentData.tree as Root, "element", (node, _index, _parent) => {
|
||||
visit(root, "element", (node, _index, _parent) => {
|
||||
if (node.tagName === "blockquote") {
|
||||
const classNames = (node.properties?.className ?? []) as string[]
|
||||
if (classNames.includes("transclude")) {
|
||||
const inner = node.children[0] as Element
|
||||
const transcludeTarget = inner.properties["data-slug"] as FullSlug
|
||||
const page = getOrComputeFileIndex(componentData.allFiles).get(transcludeTarget)
|
||||
const page = componentData.allFiles.find((f) => f.slug === transcludeTarget)
|
||||
if (!page) {
|
||||
return
|
||||
}
|
||||
@ -103,8 +96,10 @@ export function renderPage(
|
||||
{
|
||||
type: "element",
|
||||
tagName: "a",
|
||||
properties: { href: inner.properties?.href, class: ["internal"] },
|
||||
children: [{ type: "text", value: `Link to original` }],
|
||||
properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] },
|
||||
children: [
|
||||
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
@ -112,18 +107,24 @@ export function renderPage(
|
||||
// header transclude
|
||||
blockRef = blockRef.slice(1)
|
||||
let startIdx = undefined
|
||||
let startDepth = undefined
|
||||
let endIdx = undefined
|
||||
for (const [i, el] of page.htmlAst.children.entries()) {
|
||||
if (el.type === "element" && el.tagName.match(/h[1-6]/)) {
|
||||
if (endIdx) {
|
||||
break
|
||||
}
|
||||
// skip non-headers
|
||||
if (!(el.type === "element" && el.tagName.match(headerRegex))) continue
|
||||
const depth = Number(el.tagName.substring(1))
|
||||
|
||||
if (startIdx !== undefined) {
|
||||
endIdx = i
|
||||
} else if (el.properties?.id === blockRef) {
|
||||
// lookin for our blockref
|
||||
if (startIdx === undefined || startDepth === undefined) {
|
||||
// skip until we find the blockref that matches
|
||||
if (el.properties?.id === blockRef) {
|
||||
startIdx = i
|
||||
startDepth = depth
|
||||
}
|
||||
} else if (depth <= startDepth) {
|
||||
// looking for new header that is same level or higher
|
||||
endIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@ -138,7 +139,7 @@ export function renderPage(
|
||||
{
|
||||
type: "element",
|
||||
tagName: "a",
|
||||
properties: { href: inner.properties?.href, class: ["internal"] },
|
||||
properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] },
|
||||
children: [
|
||||
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
|
||||
],
|
||||
@ -168,7 +169,7 @@ export function renderPage(
|
||||
{
|
||||
type: "element",
|
||||
tagName: "a",
|
||||
properties: { href: inner.properties?.href, class: ["internal"] },
|
||||
properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] },
|
||||
children: [
|
||||
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
|
||||
],
|
||||
@ -179,11 +180,15 @@ export function renderPage(
|
||||
}
|
||||
})
|
||||
|
||||
// set componentData.tree to the edited html that has transclusions rendered
|
||||
componentData.tree = root
|
||||
|
||||
const {
|
||||
head: Head,
|
||||
header,
|
||||
beforeBody,
|
||||
pageBody: Content,
|
||||
afterBody,
|
||||
left,
|
||||
right,
|
||||
footer: Footer,
|
||||
@ -207,8 +212,9 @@ export function renderPage(
|
||||
</div>
|
||||
)
|
||||
|
||||
const lang = componentData.fileData.frontmatter?.lang ?? cfg.locale?.split("-")[0] ?? "en"
|
||||
const doc = (
|
||||
<html>
|
||||
<html lang={lang}>
|
||||
<Head {...componentData} />
|
||||
<body data-slug={slug}>
|
||||
<div id="quartz-root" class="page">
|
||||
@ -228,6 +234,12 @@ export function renderPage(
|
||||
</div>
|
||||
</div>
|
||||
<Content {...componentData} />
|
||||
<hr />
|
||||
<div class="page-footer">
|
||||
{afterBody.map((BodyComponent) => (
|
||||
<BodyComponent {...componentData} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{RightComponent}
|
||||
</Body>
|
||||
|
23
quartz/components/scripts/checkbox.inline.ts
Normal file
23
quartz/components/scripts/checkbox.inline.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { getFullSlug } from "../../util/path"
|
||||
|
||||
const checkboxId = (index: number) => `${getFullSlug(window)}-checkbox-${index}`
|
||||
|
||||
document.addEventListener("nav", () => {
|
||||
const checkboxes = document.querySelectorAll(
|
||||
"input.checkbox-toggle",
|
||||
) as NodeListOf<HTMLInputElement>
|
||||
checkboxes.forEach((el, index) => {
|
||||
const elId = checkboxId(index)
|
||||
|
||||
const switchState = (e: Event) => {
|
||||
const newCheckboxState = (e.target as HTMLInputElement)?.checked ? "true" : "false"
|
||||
localStorage.setItem(elId, newCheckboxState)
|
||||
}
|
||||
|
||||
el.addEventListener("change", switchState)
|
||||
window.addCleanup(() => el.removeEventListener("change", switchState))
|
||||
if (localStorage.getItem(elId) === "true") {
|
||||
el.checked = true
|
||||
}
|
||||
})
|
||||
})
|
67
quartz/components/scripts/comments.inline.ts
Normal file
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))
|
||||
})
|
@ -1,4 +1,4 @@
|
||||
import type { ContentDetails, ContentIndex } from "../../plugins/emitters/contentIndex"
|
||||
import type { ContentDetails } from "../../plugins/emitters/contentIndex"
|
||||
import * as d3 from "d3"
|
||||
import { registerEscapeHandler, removeAllChildren } from "./util"
|
||||
import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path"
|
||||
@ -44,6 +44,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||
opacityScale,
|
||||
removeTags,
|
||||
showTags,
|
||||
focusOnHover,
|
||||
} = JSON.parse(graph.dataset["cfg"]!)
|
||||
|
||||
const data: Map<SimpleSlug, ContentDetails> = new Map(
|
||||
@ -101,7 +102,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||
|
||||
const graphData: { nodes: NodeData[]; links: LinkData[] } = {
|
||||
nodes: [...neighbourhood].map((url) => {
|
||||
const text = url.startsWith("tags/") ? "#" + url.substring(5) : data.get(url)?.title ?? url
|
||||
const text = url.startsWith("tags/") ? "#" + url.substring(5) : (data.get(url)?.title ?? url)
|
||||
return {
|
||||
id: url,
|
||||
text: text,
|
||||
@ -189,6 +190,8 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||
return 2 + Math.sqrt(numLinks)
|
||||
}
|
||||
|
||||
let connectedNodes: SimpleSlug[] = []
|
||||
|
||||
// draw individual nodes
|
||||
const node = graphNode
|
||||
.append("circle")
|
||||
@ -202,17 +205,37 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||
window.spaNavigate(new URL(targ, window.location.toString()))
|
||||
})
|
||||
.on("mouseover", function (_, d) {
|
||||
const neighbours: SimpleSlug[] = data.get(slug)?.links ?? []
|
||||
const neighbourNodes = d3
|
||||
.selectAll<HTMLElement, NodeData>(".node")
|
||||
.filter((d) => neighbours.includes(d.id))
|
||||
const currentId = d.id
|
||||
const linkNodes = d3
|
||||
.selectAll(".link")
|
||||
.filter((d: any) => d.source.id === currentId || d.target.id === currentId)
|
||||
|
||||
// highlight neighbour nodes
|
||||
neighbourNodes.transition().duration(200).attr("fill", color)
|
||||
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)
|
||||
|
||||
d3.selectAll<HTMLElement, NodeData>(".node")
|
||||
.filter((d) => !connectedNodes.includes(d.id))
|
||||
.nodes()
|
||||
.map((it) => d3.select(it.parentNode as HTMLElement).select("text"))
|
||||
.forEach((it) => {
|
||||
let opacity = parseFloat(it.style("opacity"))
|
||||
it.transition()
|
||||
.duration(200)
|
||||
.attr("opacityOld", opacity)
|
||||
.style("opacity", Math.min(opacity, 0.2))
|
||||
})
|
||||
}
|
||||
|
||||
// highlight links
|
||||
linkNodes.transition().duration(200).attr("stroke", "var(--gray)").attr("stroke-width", 1)
|
||||
@ -231,6 +254,16 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||
.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)
|
||||
|
||||
d3.selectAll<HTMLElement, NodeData>(".node")
|
||||
.filter((d) => !connectedNodes.includes(d.id))
|
||||
.nodes()
|
||||
.map((it) => d3.select(it.parentNode as HTMLElement).select("text"))
|
||||
.forEach((it) => it.transition().duration(200).style("opacity", it.attr("opacityOld")))
|
||||
}
|
||||
const currentId = d.id
|
||||
const linkNodes = d3
|
||||
.selectAll(".link")
|
||||
@ -249,6 +282,13 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||
// @ts-ignore
|
||||
.call(drag(simulation))
|
||||
|
||||
// make tags hollow circles
|
||||
node
|
||||
.filter((d) => d.id.startsWith("tags/"))
|
||||
.attr("stroke", color)
|
||||
.attr("stroke-width", 2)
|
||||
.attr("fill", "var(--light)")
|
||||
|
||||
// draw labels
|
||||
const labels = graphNode
|
||||
.append("text")
|
||||
@ -321,7 +361,7 @@ function renderGlobalGraph() {
|
||||
|
||||
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||
const slug = e.detail.url
|
||||
addToVisited(slug)
|
||||
addToVisited(simplifySlug(slug))
|
||||
await renderGraph("graph-container", slug)
|
||||
|
||||
const containerIcon = document.getElementById("global-graph-icon")
|
||||
|
@ -3,7 +3,7 @@ import { normalizeRelativeURLs } from "../../util/path"
|
||||
|
||||
const p = new DOMParser()
|
||||
async function mouseEnterHandler(
|
||||
this: HTMLLinkElement,
|
||||
this: HTMLAnchorElement,
|
||||
{ clientX, clientY }: { clientX: number; clientY: number },
|
||||
) {
|
||||
const link = this
|
||||
@ -33,13 +33,11 @@ async function mouseEnterHandler(
|
||||
thisUrl.hash = ""
|
||||
thisUrl.search = ""
|
||||
const targetUrl = new URL(link.href)
|
||||
const hash = targetUrl.hash
|
||||
const hash = decodeURIComponent(targetUrl.hash)
|
||||
targetUrl.hash = ""
|
||||
targetUrl.search = ""
|
||||
|
||||
const contents = await fetch(`${targetUrl}`)
|
||||
.then((res) => res.text())
|
||||
.catch((err) => {
|
||||
const response = await fetch(`${targetUrl}`).catch((err) => {
|
||||
console.error(err)
|
||||
})
|
||||
|
||||
@ -48,18 +46,46 @@ async function mouseEnterHandler(
|
||||
return
|
||||
}
|
||||
|
||||
if (!contents) return
|
||||
const html = p.parseFromString(contents, "text/html")
|
||||
normalizeRelativeURLs(html, targetUrl)
|
||||
const elts = [...html.getElementsByClassName("popover-hint")]
|
||||
if (elts.length === 0) return
|
||||
if (!response) return
|
||||
const [contentType] = response.headers.get("Content-Type")!.split(";")
|
||||
const [contentTypeCategory, typeInfo] = contentType.split("/")
|
||||
|
||||
const popoverElement = document.createElement("div")
|
||||
popoverElement.classList.add("popover")
|
||||
const popoverInner = document.createElement("div")
|
||||
popoverInner.classList.add("popover-inner")
|
||||
popoverElement.appendChild(popoverInner)
|
||||
|
||||
popoverInner.dataset.contentType = contentType ?? undefined
|
||||
|
||||
switch (contentTypeCategory) {
|
||||
case "image":
|
||||
const img = document.createElement("img")
|
||||
img.src = targetUrl.toString()
|
||||
img.alt = targetUrl.pathname
|
||||
|
||||
popoverInner.appendChild(img)
|
||||
break
|
||||
case "application":
|
||||
switch (typeInfo) {
|
||||
case "pdf":
|
||||
const pdf = document.createElement("iframe")
|
||||
pdf.src = targetUrl.toString()
|
||||
popoverInner.appendChild(pdf)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
break
|
||||
default:
|
||||
const contents = await response.text()
|
||||
const html = p.parseFromString(contents, "text/html")
|
||||
normalizeRelativeURLs(html, targetUrl)
|
||||
const elts = [...html.getElementsByClassName("popover-hint")]
|
||||
if (elts.length === 0) return
|
||||
|
||||
elts.forEach((elt) => popoverInner.appendChild(elt))
|
||||
}
|
||||
|
||||
setPosition(popoverElement)
|
||||
link.appendChild(popoverElement)
|
||||
@ -74,7 +100,7 @@ async function mouseEnterHandler(
|
||||
}
|
||||
|
||||
document.addEventListener("nav", () => {
|
||||
const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[]
|
||||
const links = [...document.getElementsByClassName("internal")] as HTMLAnchorElement[]
|
||||
for (const link of links) {
|
||||
link.addEventListener("mouseenter", mouseEnterHandler)
|
||||
window.addCleanup(() => link.removeEventListener("mouseenter", mouseEnterHandler))
|
||||
|
@ -21,6 +21,7 @@ let index = new FlexSearch.Document<Item>({
|
||||
encode: encoder,
|
||||
document: {
|
||||
id: "id",
|
||||
tag: "tags",
|
||||
index: [
|
||||
{
|
||||
field: "title",
|
||||
@ -405,11 +406,33 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||
|
||||
let searchResults: FlexSearch.SimpleDocumentSearchResultSetUnit[]
|
||||
if (searchType === "tags") {
|
||||
currentSearchTerm = currentSearchTerm.substring(1).trim()
|
||||
const separatorIndex = currentSearchTerm.indexOf(" ")
|
||||
if (separatorIndex != -1) {
|
||||
// search by title and content index and then filter by tag (implemented in flexsearch)
|
||||
const tag = currentSearchTerm.substring(0, separatorIndex)
|
||||
const query = currentSearchTerm.substring(separatorIndex + 1).trim()
|
||||
searchResults = await index.searchAsync({
|
||||
query: currentSearchTerm.substring(1),
|
||||
query: query,
|
||||
// return at least 10000 documents, so it is enough to filter them by tag (implemented in flexsearch)
|
||||
limit: Math.max(numSearchResults, 10000),
|
||||
index: ["title", "content"],
|
||||
tag: tag,
|
||||
})
|
||||
for (let searchResult of searchResults) {
|
||||
searchResult.result = searchResult.result.slice(0, numSearchResults)
|
||||
}
|
||||
// set search type to basic and remove tag from term for proper highlightning and scroll
|
||||
searchType = "basic"
|
||||
currentSearchTerm = query
|
||||
} else {
|
||||
// default search by tags index
|
||||
searchResults = await index.searchAsync({
|
||||
query: currentSearchTerm,
|
||||
limit: numSearchResults,
|
||||
index: ["tags"],
|
||||
})
|
||||
}
|
||||
} else if (searchType === "basic") {
|
||||
searchResults = await index.searchAsync({
|
||||
query: currentSearchTerm,
|
||||
|
14
quartz/components/styles/contentMeta.scss
Normal file
14
quartz/components/styles/contentMeta.scss
Normal file
@ -0,0 +1,14 @@
|
||||
.content-meta {
|
||||
margin-top: 0;
|
||||
color: var(--gray);
|
||||
|
||||
&[show-comma="true"] {
|
||||
> span:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
|
||||
&::after {
|
||||
content: ",";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ button#explorer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& h1 {
|
||||
& h2 {
|
||||
font-size: 1rem;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
@ -87,7 +87,7 @@ svg {
|
||||
color: var(--secondary);
|
||||
font-family: var(--headerFont);
|
||||
font-size: 0.95rem;
|
||||
font-weight: $boldWeight;
|
||||
font-weight: $semiBoldWeight;
|
||||
line-height: 1.5rem;
|
||||
display: inline-block;
|
||||
}
|
||||
@ -112,7 +112,7 @@ svg {
|
||||
font-size: 0.95rem;
|
||||
display: inline-block;
|
||||
color: var(--secondary);
|
||||
font-weight: $boldWeight;
|
||||
font-weight: $semiBoldWeight;
|
||||
margin: 0;
|
||||
line-height: 1.5rem;
|
||||
pointer-events: none;
|
||||
|
@ -11,7 +11,7 @@ li.section-li {
|
||||
|
||||
& > .section {
|
||||
display: grid;
|
||||
grid-template-columns: 6em 3fr 1fr;
|
||||
grid-template-columns: fit-content(8em) 3fr 1fr;
|
||||
|
||||
@media all and (max-width: $mobileBreakpoint) {
|
||||
& > .tags {
|
||||
@ -24,8 +24,7 @@ li.section-li {
|
||||
}
|
||||
|
||||
& > .meta {
|
||||
margin: 0;
|
||||
flex-basis: 6em;
|
||||
margin: 0 1em 0 0;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
@ -33,7 +32,8 @@ li.section-li {
|
||||
|
||||
// modifications in popover context
|
||||
.popover .section {
|
||||
grid-template-columns: 6em 1fr !important;
|
||||
grid-template-columns: fit-content(8em) 1fr !important;
|
||||
|
||||
& > .tags {
|
||||
display: none;
|
||||
}
|
||||
|
@ -38,6 +38,28 @@
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
& > .popover-inner[data-content-type] {
|
||||
&[data-content-type*="pdf"],
|
||||
&[data-content-type*="image"] {
|
||||
padding: 0;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
&[data-content-type*="image"] {
|
||||
img {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-content-type*="pdf"] {
|
||||
iframe {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
@ -59,6 +59,10 @@
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
@media all and (max-width: $fullPageWidth) {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
& > * {
|
||||
width: 100%;
|
||||
border-radius: 7px;
|
||||
@ -155,6 +159,10 @@
|
||||
margin: 0 auto;
|
||||
width: min($pageWidth, 100%);
|
||||
}
|
||||
|
||||
a[role="anchor"] {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
& > #results-container {
|
||||
|
@ -3,8 +3,10 @@ import { StaticResources } from "../util/resources"
|
||||
import { QuartzPluginData } from "../plugins/vfile"
|
||||
import { GlobalConfiguration } from "../cfg"
|
||||
import { Node } from "hast"
|
||||
import { BuildCtx } from "../util/ctx"
|
||||
|
||||
export type QuartzComponentProps = {
|
||||
ctx: BuildCtx
|
||||
externalResources: StaticResources
|
||||
fileData: QuartzPluginData
|
||||
cfg: GlobalConfiguration
|
||||
|
118
quartz/depgraph.test.ts
Normal file
118
quartz/depgraph.test.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import test, { describe } from "node:test"
|
||||
import DepGraph from "./depgraph"
|
||||
import assert from "node:assert"
|
||||
|
||||
describe("DepGraph", () => {
|
||||
test("getLeafNodes", () => {
|
||||
const graph = new DepGraph<string>()
|
||||
graph.addEdge("A", "B")
|
||||
graph.addEdge("B", "C")
|
||||
graph.addEdge("D", "C")
|
||||
assert.deepStrictEqual(graph.getLeafNodes("A"), new Set(["C"]))
|
||||
assert.deepStrictEqual(graph.getLeafNodes("B"), new Set(["C"]))
|
||||
assert.deepStrictEqual(graph.getLeafNodes("C"), new Set(["C"]))
|
||||
assert.deepStrictEqual(graph.getLeafNodes("D"), new Set(["C"]))
|
||||
})
|
||||
|
||||
describe("getLeafNodeAncestors", () => {
|
||||
test("gets correct ancestors in a graph without cycles", () => {
|
||||
const graph = new DepGraph<string>()
|
||||
graph.addEdge("A", "B")
|
||||
graph.addEdge("B", "C")
|
||||
graph.addEdge("D", "B")
|
||||
assert.deepStrictEqual(graph.getLeafNodeAncestors("A"), new Set(["A", "B", "D"]))
|
||||
assert.deepStrictEqual(graph.getLeafNodeAncestors("B"), new Set(["A", "B", "D"]))
|
||||
assert.deepStrictEqual(graph.getLeafNodeAncestors("C"), new Set(["A", "B", "D"]))
|
||||
assert.deepStrictEqual(graph.getLeafNodeAncestors("D"), new Set(["A", "B", "D"]))
|
||||
})
|
||||
|
||||
test("gets correct ancestors in a graph with cycles", () => {
|
||||
const graph = new DepGraph<string>()
|
||||
graph.addEdge("A", "B")
|
||||
graph.addEdge("B", "C")
|
||||
graph.addEdge("C", "A")
|
||||
graph.addEdge("C", "D")
|
||||
assert.deepStrictEqual(graph.getLeafNodeAncestors("A"), new Set(["A", "B", "C"]))
|
||||
assert.deepStrictEqual(graph.getLeafNodeAncestors("B"), new Set(["A", "B", "C"]))
|
||||
assert.deepStrictEqual(graph.getLeafNodeAncestors("C"), new Set(["A", "B", "C"]))
|
||||
assert.deepStrictEqual(graph.getLeafNodeAncestors("D"), new Set(["A", "B", "C"]))
|
||||
})
|
||||
})
|
||||
|
||||
describe("mergeGraph", () => {
|
||||
test("merges two graphs", () => {
|
||||
const graph = new DepGraph<string>()
|
||||
graph.addEdge("A.md", "A.html")
|
||||
|
||||
const other = new DepGraph<string>()
|
||||
other.addEdge("B.md", "B.html")
|
||||
|
||||
graph.mergeGraph(other)
|
||||
|
||||
const expected = {
|
||||
nodes: ["A.md", "A.html", "B.md", "B.html"],
|
||||
edges: [
|
||||
["A.md", "A.html"],
|
||||
["B.md", "B.html"],
|
||||
],
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(graph.export(), expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe("updateIncomingEdgesForNode", () => {
|
||||
test("merges when node exists", () => {
|
||||
// A.md -> B.md -> B.html
|
||||
const graph = new DepGraph<string>()
|
||||
graph.addEdge("A.md", "B.md")
|
||||
graph.addEdge("B.md", "B.html")
|
||||
|
||||
// B.md is edited so it removes the A.md transclusion
|
||||
// and adds C.md transclusion
|
||||
// C.md -> B.md
|
||||
const other = new DepGraph<string>()
|
||||
other.addEdge("C.md", "B.md")
|
||||
other.addEdge("B.md", "B.html")
|
||||
|
||||
// A.md -> B.md removed, C.md -> B.md added
|
||||
// C.md -> B.md -> B.html
|
||||
graph.updateIncomingEdgesForNode(other, "B.md")
|
||||
|
||||
const expected = {
|
||||
nodes: ["A.md", "B.md", "B.html", "C.md"],
|
||||
edges: [
|
||||
["B.md", "B.html"],
|
||||
["C.md", "B.md"],
|
||||
],
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(graph.export(), expected)
|
||||
})
|
||||
|
||||
test("adds node if it does not exist", () => {
|
||||
// A.md -> B.md
|
||||
const graph = new DepGraph<string>()
|
||||
graph.addEdge("A.md", "B.md")
|
||||
|
||||
// Add a new file C.md that transcludes B.md
|
||||
// B.md -> C.md
|
||||
const other = new DepGraph<string>()
|
||||
other.addEdge("B.md", "C.md")
|
||||
|
||||
// B.md -> C.md added
|
||||
// A.md -> B.md -> C.md
|
||||
graph.updateIncomingEdgesForNode(other, "C.md")
|
||||
|
||||
const expected = {
|
||||
nodes: ["A.md", "B.md", "C.md"],
|
||||
edges: [
|
||||
["A.md", "B.md"],
|
||||
["B.md", "C.md"],
|
||||
],
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(graph.export(), expected)
|
||||
})
|
||||
})
|
||||
})
|
228
quartz/depgraph.ts
Normal file
228
quartz/depgraph.ts
Normal file
@ -0,0 +1,228 @@
|
||||
export default class DepGraph<T> {
|
||||
// node: incoming and outgoing edges
|
||||
_graph = new Map<T, { incoming: Set<T>; outgoing: Set<T> }>()
|
||||
|
||||
constructor() {
|
||||
this._graph = new Map()
|
||||
}
|
||||
|
||||
export(): Object {
|
||||
return {
|
||||
nodes: this.nodes,
|
||||
edges: this.edges,
|
||||
}
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return JSON.stringify(this.export(), null, 2)
|
||||
}
|
||||
|
||||
// BASIC GRAPH OPERATIONS
|
||||
|
||||
get nodes(): T[] {
|
||||
return Array.from(this._graph.keys())
|
||||
}
|
||||
|
||||
get edges(): [T, T][] {
|
||||
let edges: [T, T][] = []
|
||||
this.forEachEdge((edge) => edges.push(edge))
|
||||
return edges
|
||||
}
|
||||
|
||||
hasNode(node: T): boolean {
|
||||
return this._graph.has(node)
|
||||
}
|
||||
|
||||
addNode(node: T): void {
|
||||
if (!this._graph.has(node)) {
|
||||
this._graph.set(node, { incoming: new Set(), outgoing: new Set() })
|
||||
}
|
||||
}
|
||||
|
||||
// Remove node and all edges connected to it
|
||||
removeNode(node: T): void {
|
||||
if (this._graph.has(node)) {
|
||||
// first remove all edges so other nodes don't have references to this node
|
||||
for (const target of this._graph.get(node)!.outgoing) {
|
||||
this.removeEdge(node, target)
|
||||
}
|
||||
for (const source of this._graph.get(node)!.incoming) {
|
||||
this.removeEdge(source, node)
|
||||
}
|
||||
this._graph.delete(node)
|
||||
}
|
||||
}
|
||||
|
||||
forEachNode(callback: (node: T) => void): void {
|
||||
for (const node of this._graph.keys()) {
|
||||
callback(node)
|
||||
}
|
||||
}
|
||||
|
||||
hasEdge(from: T, to: T): boolean {
|
||||
return Boolean(this._graph.get(from)?.outgoing.has(to))
|
||||
}
|
||||
|
||||
addEdge(from: T, to: T): void {
|
||||
this.addNode(from)
|
||||
this.addNode(to)
|
||||
|
||||
this._graph.get(from)!.outgoing.add(to)
|
||||
this._graph.get(to)!.incoming.add(from)
|
||||
}
|
||||
|
||||
removeEdge(from: T, to: T): void {
|
||||
if (this._graph.has(from) && this._graph.has(to)) {
|
||||
this._graph.get(from)!.outgoing.delete(to)
|
||||
this._graph.get(to)!.incoming.delete(from)
|
||||
}
|
||||
}
|
||||
|
||||
// returns -1 if node does not exist
|
||||
outDegree(node: T): number {
|
||||
return this.hasNode(node) ? this._graph.get(node)!.outgoing.size : -1
|
||||
}
|
||||
|
||||
// returns -1 if node does not exist
|
||||
inDegree(node: T): number {
|
||||
return this.hasNode(node) ? this._graph.get(node)!.incoming.size : -1
|
||||
}
|
||||
|
||||
forEachOutNeighbor(node: T, callback: (neighbor: T) => void): void {
|
||||
this._graph.get(node)?.outgoing.forEach(callback)
|
||||
}
|
||||
|
||||
forEachInNeighbor(node: T, callback: (neighbor: T) => void): void {
|
||||
this._graph.get(node)?.incoming.forEach(callback)
|
||||
}
|
||||
|
||||
forEachEdge(callback: (edge: [T, T]) => void): void {
|
||||
for (const [source, { outgoing }] of this._graph.entries()) {
|
||||
for (const target of outgoing) {
|
||||
callback([source, target])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DEPENDENCY ALGORITHMS
|
||||
|
||||
// Add all nodes and edges from other graph to this graph
|
||||
mergeGraph(other: DepGraph<T>): void {
|
||||
other.forEachEdge(([source, target]) => {
|
||||
this.addNode(source)
|
||||
this.addNode(target)
|
||||
this.addEdge(source, target)
|
||||
})
|
||||
}
|
||||
|
||||
// For the node provided:
|
||||
// If node does not exist, add it
|
||||
// If an incoming edge was added in other, it is added in this graph
|
||||
// If an incoming edge was deleted in other, it is deleted in this graph
|
||||
updateIncomingEdgesForNode(other: DepGraph<T>, node: T): void {
|
||||
this.addNode(node)
|
||||
|
||||
// Add edge if it is present in other
|
||||
other.forEachInNeighbor(node, (neighbor) => {
|
||||
this.addEdge(neighbor, node)
|
||||
})
|
||||
|
||||
// For node provided, remove incoming edge if it is absent in other
|
||||
this.forEachEdge(([source, target]) => {
|
||||
if (target === node && !other.hasEdge(source, target)) {
|
||||
this.removeEdge(source, target)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Remove all nodes that do not have any incoming or outgoing edges
|
||||
// A node may be orphaned if the only node pointing to it was removed
|
||||
removeOrphanNodes(): Set<T> {
|
||||
let orphanNodes = new Set<T>()
|
||||
|
||||
this.forEachNode((node) => {
|
||||
if (this.inDegree(node) === 0 && this.outDegree(node) === 0) {
|
||||
orphanNodes.add(node)
|
||||
}
|
||||
})
|
||||
|
||||
orphanNodes.forEach((node) => {
|
||||
this.removeNode(node)
|
||||
})
|
||||
|
||||
return orphanNodes
|
||||
}
|
||||
|
||||
// Get all leaf nodes (i.e. destination paths) reachable from the node provided
|
||||
// Eg. if the graph is A -> B -> C
|
||||
// D ---^
|
||||
// and the node is B, this function returns [C]
|
||||
getLeafNodes(node: T): Set<T> {
|
||||
let stack: T[] = [node]
|
||||
let visited = new Set<T>()
|
||||
let leafNodes = new Set<T>()
|
||||
|
||||
// DFS
|
||||
while (stack.length > 0) {
|
||||
let node = stack.pop()!
|
||||
|
||||
// If the node is already visited, skip it
|
||||
if (visited.has(node)) {
|
||||
continue
|
||||
}
|
||||
visited.add(node)
|
||||
|
||||
// Check if the node is a leaf node (i.e. destination path)
|
||||
if (this.outDegree(node) === 0) {
|
||||
leafNodes.add(node)
|
||||
}
|
||||
|
||||
// Add all unvisited neighbors to the stack
|
||||
this.forEachOutNeighbor(node, (neighbor) => {
|
||||
if (!visited.has(neighbor)) {
|
||||
stack.push(neighbor)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return leafNodes
|
||||
}
|
||||
|
||||
// Get all ancestors of the leaf nodes reachable from the node provided
|
||||
// Eg. if the graph is A -> B -> C
|
||||
// D ---^
|
||||
// and the node is B, this function returns [A, B, D]
|
||||
getLeafNodeAncestors(node: T): Set<T> {
|
||||
const leafNodes = this.getLeafNodes(node)
|
||||
let visited = new Set<T>()
|
||||
let upstreamNodes = new Set<T>()
|
||||
|
||||
// Backwards DFS for each leaf node
|
||||
leafNodes.forEach((leafNode) => {
|
||||
let stack: T[] = [leafNode]
|
||||
|
||||
while (stack.length > 0) {
|
||||
let node = stack.pop()!
|
||||
|
||||
if (visited.has(node)) {
|
||||
continue
|
||||
}
|
||||
visited.add(node)
|
||||
// Add node if it's not a leaf node (i.e. destination path)
|
||||
// Assumes destination file cannot depend on another destination file
|
||||
if (this.outDegree(node) !== 0) {
|
||||
upstreamNodes.add(node)
|
||||
}
|
||||
|
||||
// Add all unvisited parents to the stack
|
||||
this.forEachInNeighbor(node, (parentNode) => {
|
||||
if (!visited.has(parentNode)) {
|
||||
stack.push(parentNode)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return upstreamNodes
|
||||
}
|
||||
}
|
@ -1,13 +1,70 @@
|
||||
import { Translation } from "./locales/definition"
|
||||
import en from "./locales/en-US"
|
||||
import { Translation, CalloutTranslation } from "./locales/definition"
|
||||
import enUs from "./locales/en-US"
|
||||
import enGb from "./locales/en-GB"
|
||||
import fr from "./locales/fr-FR"
|
||||
import it from "./locales/it-IT"
|
||||
import ja from "./locales/ja-JP"
|
||||
import de from "./locales/de-DE"
|
||||
import nl from "./locales/nl-NL"
|
||||
import ro from "./locales/ro-RO"
|
||||
import ca from "./locales/ca-ES"
|
||||
import es from "./locales/es-ES"
|
||||
import ar from "./locales/ar-SA"
|
||||
import uk from "./locales/uk-UA"
|
||||
import ru from "./locales/ru-RU"
|
||||
import ko from "./locales/ko-KR"
|
||||
import zh from "./locales/zh-CN"
|
||||
import vi from "./locales/vi-VN"
|
||||
import pt from "./locales/pt-BR"
|
||||
import hu from "./locales/hu-HU"
|
||||
import fa from "./locales/fa-IR"
|
||||
import pl from "./locales/pl-PL"
|
||||
|
||||
export const TRANSLATIONS = {
|
||||
"en-US": en,
|
||||
"en-US": enUs,
|
||||
"en-GB": enGb,
|
||||
"fr-FR": fr,
|
||||
"it-IT": it,
|
||||
"ja-JP": ja,
|
||||
"de-DE": de,
|
||||
"nl-NL": nl,
|
||||
"nl-BE": nl,
|
||||
"ro-RO": ro,
|
||||
"ro-MD": ro,
|
||||
"ca-ES": ca,
|
||||
"es-ES": es,
|
||||
"ar-SA": ar,
|
||||
"ar-AE": ar,
|
||||
"ar-QA": ar,
|
||||
"ar-BH": ar,
|
||||
"ar-KW": ar,
|
||||
"ar-OM": ar,
|
||||
"ar-YE": ar,
|
||||
"ar-IR": ar,
|
||||
"ar-SY": ar,
|
||||
"ar-IQ": ar,
|
||||
"ar-JO": ar,
|
||||
"ar-PL": ar,
|
||||
"ar-LB": ar,
|
||||
"ar-EG": ar,
|
||||
"ar-SD": ar,
|
||||
"ar-LY": ar,
|
||||
"ar-MA": ar,
|
||||
"ar-TN": ar,
|
||||
"ar-DZ": ar,
|
||||
"ar-MR": ar,
|
||||
"uk-UA": uk,
|
||||
"ru-RU": ru,
|
||||
"ko-KR": ko,
|
||||
"zh-CN": zh,
|
||||
"vi-VN": vi,
|
||||
"pt-BR": pt,
|
||||
"hu-HU": hu,
|
||||
"fa-IR": fa,
|
||||
"pl-PL": pl,
|
||||
} as const
|
||||
|
||||
export const i18n = (locale: ValidLocale): Translation => TRANSLATIONS[locale]
|
||||
export const defaultTranslation = "en-US"
|
||||
export const i18n = (locale: ValidLocale): Translation => TRANSLATIONS[locale ?? defaultTranslation]
|
||||
export type ValidLocale = keyof typeof TRANSLATIONS
|
||||
export type ValidCallout = keyof CalloutTranslation
|
||||
|
89
quartz/i18n/locales/ar-SA.ts
Normal file
89
quartz/i18n/locales/ar-SA.ts
Normal file
@ -0,0 +1,89 @@
|
||||
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 == 1
|
||||
? `دقيقة أو أقل للقراءة`
|
||||
: minutes == 2
|
||||
? `دقيقتان للقراءة`
|
||||
: `${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
|
84
quartz/i18n/locales/ca-ES.ts
Normal file
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
|
84
quartz/i18n/locales/de-DE.ts
Normal file
84
quartz/i18n/locales/de-DE.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { Translation } from "./definition"
|
||||
|
||||
export default {
|
||||
propertyDefaults: {
|
||||
title: "Unbenannt",
|
||||
description: "Keine Beschreibung angegeben",
|
||||
},
|
||||
components: {
|
||||
callout: {
|
||||
note: "Hinweis",
|
||||
abstract: "Zusammenfassung",
|
||||
info: "Info",
|
||||
todo: "Zu erledigen",
|
||||
tip: "Tipp",
|
||||
success: "Erfolg",
|
||||
question: "Frage",
|
||||
warning: "Warnung",
|
||||
failure: "Misserfolg",
|
||||
danger: "Gefahr",
|
||||
bug: "Fehler",
|
||||
example: "Beispiel",
|
||||
quote: "Zitat",
|
||||
},
|
||||
backlinks: {
|
||||
title: "Backlinks",
|
||||
noBacklinksFound: "Keine Backlinks gefunden",
|
||||
},
|
||||
themeToggle: {
|
||||
lightMode: "Light Mode",
|
||||
darkMode: "Dark Mode",
|
||||
},
|
||||
explorer: {
|
||||
title: "Explorer",
|
||||
},
|
||||
footer: {
|
||||
createdWith: "Erstellt mit",
|
||||
},
|
||||
graph: {
|
||||
title: "Graphansicht",
|
||||
},
|
||||
recentNotes: {
|
||||
title: "Zuletzt bearbeitete Seiten",
|
||||
seeRemainingMore: ({ remaining }) => `${remaining} weitere ansehen →`,
|
||||
},
|
||||
transcludes: {
|
||||
transcludeOf: ({ targetSlug }) => `Transklusion von ${targetSlug}`,
|
||||
linkToOriginal: "Link zum Original",
|
||||
},
|
||||
search: {
|
||||
title: "Suche",
|
||||
searchBarPlaceholder: "Suche nach etwas",
|
||||
},
|
||||
tableOfContents: {
|
||||
title: "Inhaltsverzeichnis",
|
||||
},
|
||||
contentMeta: {
|
||||
readingTime: ({ minutes }) => `${minutes} min read`,
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
rss: {
|
||||
recentNotes: "Zuletzt bearbeitete Seiten",
|
||||
lastFewNotes: ({ count }) => `Letzte ${count} Seiten`,
|
||||
},
|
||||
error: {
|
||||
title: "Nicht gefunden",
|
||||
notFound: "Diese Seite ist entweder nicht öffentlich oder existiert nicht.",
|
||||
home: "Return to Homepage",
|
||||
},
|
||||
folderContent: {
|
||||
folder: "Ordner",
|
||||
itemsUnderFolder: ({ count }) =>
|
||||
count === 1 ? "1 Datei in diesem Ordner." : `${count} Dateien in diesem Ordner.`,
|
||||
},
|
||||
tagContent: {
|
||||
tag: "Tag",
|
||||
tagIndex: "Tag-Übersicht",
|
||||
itemsUnderTag: ({ count }) =>
|
||||
count === 1 ? "1 Datei mit diesem Tag." : `${count} Dateien mit diesem Tag.`,
|
||||
showingFirst: ({ count }) => `Die ersten ${count} Tags werden angezeigt.`,
|
||||
totalTags: ({ count }) => `${count} Tags insgesamt.`,
|
||||
},
|
||||
},
|
||||
} as const satisfies Translation
|
@ -1,11 +1,28 @@
|
||||
import { FullSlug } from "../../util/path"
|
||||
|
||||
export interface CalloutTranslation {
|
||||
note: string
|
||||
abstract: string
|
||||
info: string
|
||||
todo: string
|
||||
tip: string
|
||||
success: string
|
||||
question: string
|
||||
warning: string
|
||||
failure: string
|
||||
danger: string
|
||||
bug: string
|
||||
example: string
|
||||
quote: string
|
||||
}
|
||||
|
||||
export interface Translation {
|
||||
propertyDefaults: {
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
components: {
|
||||
callout: CalloutTranslation
|
||||
backlinks: {
|
||||
title: string
|
||||
noBacklinksFound: string
|
||||
@ -38,6 +55,9 @@ export interface Translation {
|
||||
tableOfContents: {
|
||||
title: string
|
||||
}
|
||||
contentMeta: {
|
||||
readingTime: (variables: { minutes: number }) => string
|
||||
}
|
||||
}
|
||||
pages: {
|
||||
rss: {
|
||||
@ -47,6 +67,7 @@ export interface Translation {
|
||||
error: {
|
||||
title: string
|
||||
notFound: string
|
||||
home: string
|
||||
}
|
||||
folderContent: {
|
||||
folder: string
|
||||
|
84
quartz/i18n/locales/en-GB.ts
Normal file
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
|
@ -6,6 +6,21 @@ export default {
|
||||
description: "No description provided",
|
||||
},
|
||||
components: {
|
||||
callout: {
|
||||
note: "Note",
|
||||
abstract: "Abstract",
|
||||
info: "Info",
|
||||
todo: "Todo",
|
||||
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",
|
||||
@ -38,6 +53,9 @@ export default {
|
||||
tableOfContents: {
|
||||
title: "Table of Contents",
|
||||
},
|
||||
contentMeta: {
|
||||
readingTime: ({ minutes }) => `${minutes} min read`,
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
rss: {
|
||||
@ -47,17 +65,18 @@ export default {
|
||||
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.`,
|
||||
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.`,
|
||||
count === 1 ? "1 item with this tag." : `${count} items with this tag.`,
|
||||
showingFirst: ({ count }) => `Showing first ${count} tags.`,
|
||||
totalTags: ({ count }) => `Found ${count} total tags.`,
|
||||
},
|
||||
|
84
quartz/i18n/locales/es-ES.ts
Normal file
84
quartz/i18n/locales/es-ES.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { Translation } from "./definition"
|
||||
|
||||
export default {
|
||||
propertyDefaults: {
|
||||
title: "Sin título",
|
||||
description: "Sin descripción",
|
||||
},
|
||||
components: {
|
||||
callout: {
|
||||
note: "Nota",
|
||||
abstract: "Resumen",
|
||||
info: "Información",
|
||||
todo: "Por hacer",
|
||||
tip: "Consejo",
|
||||
success: "Éxito",
|
||||
question: "Pregunta",
|
||||
warning: "Advertencia",
|
||||
failure: "Fallo",
|
||||
danger: "Peligro",
|
||||
bug: "Error",
|
||||
example: "Ejemplo",
|
||||
quote: "Cita",
|
||||
},
|
||||
backlinks: {
|
||||
title: "Retroenlaces",
|
||||
noBacklinksFound: "No se han encontrado retroenlaces",
|
||||
},
|
||||
themeToggle: {
|
||||
lightMode: "Modo claro",
|
||||
darkMode: "Modo oscuro",
|
||||
},
|
||||
explorer: {
|
||||
title: "Explorador",
|
||||
},
|
||||
footer: {
|
||||
createdWith: "Creado con",
|
||||
},
|
||||
graph: {
|
||||
title: "Vista Gráfica",
|
||||
},
|
||||
recentNotes: {
|
||||
title: "Notas Recientes",
|
||||
seeRemainingMore: ({ remaining }) => `Vea ${remaining} más →`,
|
||||
},
|
||||
transcludes: {
|
||||
transcludeOf: ({ targetSlug }) => `Transcluido de ${targetSlug}`,
|
||||
linkToOriginal: "Enlace al original",
|
||||
},
|
||||
search: {
|
||||
title: "Buscar",
|
||||
searchBarPlaceholder: "Busca algo",
|
||||
},
|
||||
tableOfContents: {
|
||||
title: "Tabla de Contenidos",
|
||||
},
|
||||
contentMeta: {
|
||||
readingTime: ({ minutes }) => `Se lee en ${minutes} min`,
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
rss: {
|
||||
recentNotes: "Notas recientes",
|
||||
lastFewNotes: ({ count }) => `Últimas ${count} notas`,
|
||||
},
|
||||
error: {
|
||||
title: "No se ha encontrado.",
|
||||
notFound: "Esta página es privada o no existe.",
|
||||
home: "Regresa a la página principal",
|
||||
},
|
||||
folderContent: {
|
||||
folder: "Carpeta",
|
||||
itemsUnderFolder: ({ count }) =>
|
||||
count === 1 ? "1 artículo en esta carpeta." : `${count} artículos en esta carpeta.`,
|
||||
},
|
||||
tagContent: {
|
||||
tag: "Etiqueta",
|
||||
tagIndex: "Índice de Etiquetas",
|
||||
itemsUnderTag: ({ count }) =>
|
||||
count === 1 ? "1 artículo con esta etiqueta." : `${count} artículos con esta etiqueta.`,
|
||||
showingFirst: ({ count }) => `Mostrando las primeras ${count} etiquetas.`,
|
||||
totalTags: ({ count }) => `Se han encontrado ${count} etiquetas en total.`,
|
||||
},
|
||||
},
|
||||
} as const satisfies Translation
|
84
quartz/i18n/locales/fa-IR.ts
Normal file
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
|
@ -6,6 +6,21 @@ export default {
|
||||
description: "Aucune description fournie",
|
||||
},
|
||||
components: {
|
||||
callout: {
|
||||
note: "Note",
|
||||
abstract: "Résumé",
|
||||
info: "Info",
|
||||
todo: "À faire",
|
||||
tip: "Conseil",
|
||||
success: "Succès",
|
||||
question: "Question",
|
||||
warning: "Avertissement",
|
||||
failure: "Échec",
|
||||
danger: "Danger",
|
||||
bug: "Bogue",
|
||||
example: "Exemple",
|
||||
quote: "Citation",
|
||||
},
|
||||
backlinks: {
|
||||
title: "Liens retour",
|
||||
noBacklinksFound: "Aucun lien retour trouvé",
|
||||
@ -38,6 +53,9 @@ export default {
|
||||
tableOfContents: {
|
||||
title: "Table des Matières",
|
||||
},
|
||||
contentMeta: {
|
||||
readingTime: ({ minutes }) => `${minutes} min de lecture`,
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
rss: {
|
||||
@ -45,19 +63,20 @@ export default {
|
||||
lastFewNotes: ({ count }) => `Les dernières ${count} notes`,
|
||||
},
|
||||
error: {
|
||||
title: "Pas trouvé",
|
||||
title: "Introuvable",
|
||||
notFound: "Cette page est soit privée, soit elle n'existe pas.",
|
||||
home: "Retour à la page d'accueil",
|
||||
},
|
||||
folderContent: {
|
||||
folder: "Dossier",
|
||||
itemsUnderFolder: ({ count }) =>
|
||||
count === 1 ? "1 élément sous ce dossier" : `${count} éléments sous ce dossier.`,
|
||||
count === 1 ? "1 élément sous ce dossier." : `${count} éléments sous ce dossier.`,
|
||||
},
|
||||
tagContent: {
|
||||
tag: "Étiquette",
|
||||
tagIndex: "Index des étiquettes",
|
||||
itemsUnderTag: ({ count }) =>
|
||||
count === 1 ? "1 élément avec cette étiquette" : `${count} éléments avec cette étiquette.`,
|
||||
count === 1 ? "1 élément avec cette étiquette." : `${count} éléments avec cette étiquette.`,
|
||||
showingFirst: ({ count }) => `Affichage des premières ${count} étiquettes.`,
|
||||
totalTags: ({ count }) => `Trouvé ${count} étiquettes au total.`,
|
||||
},
|
||||
|
82
quartz/i18n/locales/hu-HU.ts
Normal file
82
quartz/i18n/locales/hu-HU.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { Translation } from "./definition"
|
||||
|
||||
export default {
|
||||
propertyDefaults: {
|
||||
title: "Névtelen",
|
||||
description: "Nincs leírás",
|
||||
},
|
||||
components: {
|
||||
callout: {
|
||||
note: "Jegyzet",
|
||||
abstract: "Abstract",
|
||||
info: "Információ",
|
||||
todo: "Tennivaló",
|
||||
tip: "Tipp",
|
||||
success: "Siker",
|
||||
question: "Kérdés",
|
||||
warning: "Figyelmeztetés",
|
||||
failure: "Hiba",
|
||||
danger: "Veszély",
|
||||
bug: "Bug",
|
||||
example: "Példa",
|
||||
quote: "Idézet",
|
||||
},
|
||||
backlinks: {
|
||||
title: "Visszautalások",
|
||||
noBacklinksFound: "Nincs visszautalás",
|
||||
},
|
||||
themeToggle: {
|
||||
lightMode: "Világos mód",
|
||||
darkMode: "Sötét mód",
|
||||
},
|
||||
explorer: {
|
||||
title: "Fájlböngésző",
|
||||
},
|
||||
footer: {
|
||||
createdWith: "Készítve ezzel:",
|
||||
},
|
||||
graph: {
|
||||
title: "Grafikonnézet",
|
||||
},
|
||||
recentNotes: {
|
||||
title: "Legutóbbi jegyzetek",
|
||||
seeRemainingMore: ({ remaining }) => `${remaining} további megtekintése →`,
|
||||
},
|
||||
transcludes: {
|
||||
transcludeOf: ({ targetSlug }) => `${targetSlug} áthivatkozása`,
|
||||
linkToOriginal: "Hivatkozás az eredetire",
|
||||
},
|
||||
search: {
|
||||
title: "Keresés",
|
||||
searchBarPlaceholder: "Keress valamire",
|
||||
},
|
||||
tableOfContents: {
|
||||
title: "Tartalomjegyzék",
|
||||
},
|
||||
contentMeta: {
|
||||
readingTime: ({ minutes }) => `${minutes} perces olvasás`,
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
rss: {
|
||||
recentNotes: "Legutóbbi jegyzetek",
|
||||
lastFewNotes: ({ count }) => `Legutóbbi ${count} jegyzet`,
|
||||
},
|
||||
error: {
|
||||
title: "Nem található",
|
||||
notFound: "Ez a lap vagy privát vagy nem létezik.",
|
||||
home: "Vissza a kezdőlapra",
|
||||
},
|
||||
folderContent: {
|
||||
folder: "Mappa",
|
||||
itemsUnderFolder: ({ count }) => `Ebben a mappában ${count} elem található.`,
|
||||
},
|
||||
tagContent: {
|
||||
tag: "Címke",
|
||||
tagIndex: "Címke index",
|
||||
itemsUnderTag: ({ count }) => `${count} elem található ezzel a címkével.`,
|
||||
showingFirst: ({ count }) => `Első ${count} címke megjelenítve.`,
|
||||
totalTags: ({ count }) => `Összesen ${count} címke található.`,
|
||||
},
|
||||
},
|
||||
} as const satisfies Translation
|
84
quartz/i18n/locales/it-IT.ts
Normal file
84
quartz/i18n/locales/it-IT.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { Translation } from "./definition"
|
||||
|
||||
export default {
|
||||
propertyDefaults: {
|
||||
title: "Senza titolo",
|
||||
description: "Nessuna descrizione",
|
||||
},
|
||||
components: {
|
||||
callout: {
|
||||
note: "Nota",
|
||||
abstract: "Astratto",
|
||||
info: "Info",
|
||||
todo: "Da fare",
|
||||
tip: "Consiglio",
|
||||
success: "Completato",
|
||||
question: "Domanda",
|
||||
warning: "Attenzione",
|
||||
failure: "Errore",
|
||||
danger: "Pericolo",
|
||||
bug: "Bug",
|
||||
example: "Esempio",
|
||||
quote: "Citazione",
|
||||
},
|
||||
backlinks: {
|
||||
title: "Link entranti",
|
||||
noBacklinksFound: "Nessun link entrante",
|
||||
},
|
||||
themeToggle: {
|
||||
lightMode: "Tema chiaro",
|
||||
darkMode: "Tema scuro",
|
||||
},
|
||||
explorer: {
|
||||
title: "Esplora",
|
||||
},
|
||||
footer: {
|
||||
createdWith: "Creato con",
|
||||
},
|
||||
graph: {
|
||||
title: "Vista grafico",
|
||||
},
|
||||
recentNotes: {
|
||||
title: "Note recenti",
|
||||
seeRemainingMore: ({ remaining }) => `Vedi ${remaining} altro →`,
|
||||
},
|
||||
transcludes: {
|
||||
transcludeOf: ({ targetSlug }) => `Transclusione di ${targetSlug}`,
|
||||
linkToOriginal: "Link all'originale",
|
||||
},
|
||||
search: {
|
||||
title: "Cerca",
|
||||
searchBarPlaceholder: "Cerca qualcosa",
|
||||
},
|
||||
tableOfContents: {
|
||||
title: "Tabella dei contenuti",
|
||||
},
|
||||
contentMeta: {
|
||||
readingTime: ({ minutes }) => `${minutes} minuti`,
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
rss: {
|
||||
recentNotes: "Note recenti",
|
||||
lastFewNotes: ({ count }) => `Ultime ${count} note`,
|
||||
},
|
||||
error: {
|
||||
title: "Non trovato",
|
||||
notFound: "Questa pagina è privata o non esiste.",
|
||||
home: "Ritorna alla home page",
|
||||
},
|
||||
folderContent: {
|
||||
folder: "Cartella",
|
||||
itemsUnderFolder: ({ count }) =>
|
||||
count === 1 ? "1 oggetto in questa cartella." : `${count} oggetti in questa cartella.`,
|
||||
},
|
||||
tagContent: {
|
||||
tag: "Etichetta",
|
||||
tagIndex: "Indice etichette",
|
||||
itemsUnderTag: ({ count }) =>
|
||||
count === 1 ? "1 oggetto con questa etichetta." : `${count} oggetti con questa etichetta.`,
|
||||
showingFirst: ({ count }) => `Prime ${count} etichette.`,
|
||||
totalTags: ({ count }) => `Trovate ${count} etichette totali.`,
|
||||
},
|
||||
},
|
||||
} as const satisfies Translation
|
@ -6,6 +6,21 @@ export default {
|
||||
description: "説明なし",
|
||||
},
|
||||
components: {
|
||||
callout: {
|
||||
note: "ノート",
|
||||
abstract: "抄録",
|
||||
info: "情報",
|
||||
todo: "やるべきこと",
|
||||
tip: "ヒント",
|
||||
success: "成功",
|
||||
question: "質問",
|
||||
warning: "警告",
|
||||
failure: "失敗",
|
||||
danger: "危険",
|
||||
bug: "バグ",
|
||||
example: "例",
|
||||
quote: "引用",
|
||||
},
|
||||
backlinks: {
|
||||
title: "バックリンク",
|
||||
noBacklinksFound: "バックリンクはありません",
|
||||
@ -38,6 +53,9 @@ export default {
|
||||
tableOfContents: {
|
||||
title: "目次",
|
||||
},
|
||||
contentMeta: {
|
||||
readingTime: ({ minutes }) => `${minutes} min read`,
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
rss: {
|
||||
@ -47,17 +65,16 @@ export default {
|
||||
error: {
|
||||
title: "Not Found",
|
||||
notFound: "ページが存在しないか、非公開設定になっています。",
|
||||
home: "ホームページに戻る",
|
||||
},
|
||||
folderContent: {
|
||||
folder: "フォルダ",
|
||||
itemsUnderFolder: ({ count }) =>
|
||||
`${count}件のページ`,
|
||||
itemsUnderFolder: ({ count }) => `${count}件のページ`,
|
||||
},
|
||||
tagContent: {
|
||||
tag: "タグ",
|
||||
tagIndex: "タグ一覧",
|
||||
itemsUnderTag: ({ count }) =>
|
||||
`${count}件のページ`,
|
||||
itemsUnderTag: ({ count }) => `${count}件のページ`,
|
||||
showingFirst: ({ count }) => `のうち最初の${count}件を表示しています`,
|
||||
totalTags: ({ count }) => `全${count}個のタグを表示中`,
|
||||
},
|
||||
|
82
quartz/i18n/locales/ko-KR.ts
Normal file
82
quartz/i18n/locales/ko-KR.ts
Normal file
@ -0,0 +1,82 @@
|
||||
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: "Created with",
|
||||
},
|
||||
graph: {
|
||||
title: "그래프 뷰",
|
||||
},
|
||||
recentNotes: {
|
||||
title: "최근 게시글",
|
||||
seeRemainingMore: ({ remaining }) => `${remaining}건 더보기 →`,
|
||||
},
|
||||
transcludes: {
|
||||
transcludeOf: ({ targetSlug }) => `${targetSlug}의 포함`,
|
||||
linkToOriginal: "원본 링크",
|
||||
},
|
||||
search: {
|
||||
title: "검색",
|
||||
searchBarPlaceholder: "검색어를 입력하세요",
|
||||
},
|
||||
tableOfContents: {
|
||||
title: "목차",
|
||||
},
|
||||
contentMeta: {
|
||||
readingTime: ({ minutes }) => `${minutes} min read`,
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
rss: {
|
||||
recentNotes: "최근 게시글",
|
||||
lastFewNotes: ({ count }) => `최근 ${count} 건`,
|
||||
},
|
||||
error: {
|
||||
title: "Not Found",
|
||||
notFound: "페이지가 존재하지 않거나 비공개 설정이 되어 있습니다.",
|
||||
home: "홈페이지로 돌아가기",
|
||||
},
|
||||
folderContent: {
|
||||
folder: "폴더",
|
||||
itemsUnderFolder: ({ count }) => `${count}건의 항목`,
|
||||
},
|
||||
tagContent: {
|
||||
tag: "태그",
|
||||
tagIndex: "태그 목록",
|
||||
itemsUnderTag: ({ count }) => `${count}건의 항목`,
|
||||
showingFirst: ({ count }) => `처음 ${count}개의 태그`,
|
||||
totalTags: ({ count }) => `총 ${count}개의 태그를 찾았습니다.`,
|
||||
},
|
||||
},
|
||||
} as const satisfies Translation
|
86
quartz/i18n/locales/nl-NL.ts
Normal file
86
quartz/i18n/locales/nl-NL.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { Translation } from "./definition"
|
||||
|
||||
export default {
|
||||
propertyDefaults: {
|
||||
title: "Naamloos",
|
||||
description: "Geen beschrijving gegeven.",
|
||||
},
|
||||
components: {
|
||||
callout: {
|
||||
note: "Notitie",
|
||||
abstract: "Samenvatting",
|
||||
info: "Info",
|
||||
todo: "Te doen",
|
||||
tip: "Tip",
|
||||
success: "Succes",
|
||||
question: "Vraag",
|
||||
warning: "Waarschuwing",
|
||||
failure: "Mislukking",
|
||||
danger: "Gevaar",
|
||||
bug: "Bug",
|
||||
example: "Voorbeeld",
|
||||
quote: "Citaat",
|
||||
},
|
||||
backlinks: {
|
||||
title: "Backlinks",
|
||||
noBacklinksFound: "Geen backlinks gevonden",
|
||||
},
|
||||
themeToggle: {
|
||||
lightMode: "Lichte modus",
|
||||
darkMode: "Donkere modus",
|
||||
},
|
||||
explorer: {
|
||||
title: "Verkenner",
|
||||
},
|
||||
footer: {
|
||||
createdWith: "Gemaakt met",
|
||||
},
|
||||
graph: {
|
||||
title: "Grafiekweergave",
|
||||
},
|
||||
recentNotes: {
|
||||
title: "Recente notities",
|
||||
seeRemainingMore: ({ remaining }) => `Zie ${remaining} meer →`,
|
||||
},
|
||||
transcludes: {
|
||||
transcludeOf: ({ targetSlug }) => `Invoeging van ${targetSlug}`,
|
||||
linkToOriginal: "Link naar origineel",
|
||||
},
|
||||
search: {
|
||||
title: "Zoeken",
|
||||
searchBarPlaceholder: "Doorzoek de website",
|
||||
},
|
||||
tableOfContents: {
|
||||
title: "Inhoudsopgave",
|
||||
},
|
||||
contentMeta: {
|
||||
readingTime: ({ minutes }) =>
|
||||
minutes === 1 ? "1 minuut leestijd" : `${minutes} minuten leestijd`,
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
rss: {
|
||||
recentNotes: "Recente notities",
|
||||
lastFewNotes: ({ count }) => `Laatste ${count} notities`,
|
||||
},
|
||||
error: {
|
||||
title: "Niet gevonden",
|
||||
notFound: "Deze pagina is niet zichtbaar of bestaat niet.",
|
||||
home: "Keer terug naar de start pagina",
|
||||
},
|
||||
folderContent: {
|
||||
folder: "Map",
|
||||
itemsUnderFolder: ({ count }) =>
|
||||
count === 1 ? "1 item in deze map." : `${count} items in deze map.`,
|
||||
},
|
||||
tagContent: {
|
||||
tag: "Label",
|
||||
tagIndex: "Label-index",
|
||||
itemsUnderTag: ({ count }) =>
|
||||
count === 1 ? "1 item met dit label." : `${count} items met dit label.`,
|
||||
showingFirst: ({ count }) =>
|
||||
count === 1 ? "Eerste label tonen." : `Eerste ${count} labels tonen.`,
|
||||
totalTags: ({ count }) => `${count} labels gevonden.`,
|
||||
},
|
||||
},
|
||||
} as const satisfies Translation
|
84
quartz/i18n/locales/pl-PL.ts
Normal file
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
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user