Compare commits

...

108 Commits

Author SHA1 Message Date
Jacky Zhao
d0c0daa4aa ci: fix autotag 2024-02-23 19:00:47 -08:00
Jacky Zhao
ea7122dd5a pkg: bump to 4.2.3 2024-02-23 18:52:28 -08:00
Jacky Zhao
2c74b05d1b fix(ci): autotag 2024-02-23 18:48:25 -08:00
kabirgh
a6417c447a fix(fast rebuild): handle added an deleted markdown correctly (#921)
* Handle added files correctly

* Handle deletes properly

* addGraph renamed to mergeGraph
2024-02-23 18:40:42 -08:00
Jacky Zhao
6be1ed1ea2 docs(latex): mhchem 2024-02-23 17:45:41 -08:00
Eiko Wagenknecht
1929241a62 docs: update plugin documentation (#888)
* docs: first few plugins documented

* docs: move plugin info

* docs: move plugin docs to tag based system

* docs: update latex example code snippet

* docs: fix spelling of latex in title

* docs: add missing linebreak

* docs: remove plugin tag from feature pages

* docs: shorten titles

* docs: refine wording

* docs: move plugin details for frontmatter

* docs: add features/* tags

* docs: update latex example

* docs: make references more explicit

* docs: add stubs for the remaining plugins

* docs: more descriptions

* docs: fix feature tags

* docs: descriptions

* docs: new plugin pages

* docs: update configuration page

* docs: more plugin work

* docs: run prettier

* docs: remove comments in config file and add link to docs

* docs: minor fixes

* docs: run prettier

* docs: spelling

* docs: update docs/plugins/AliasRedirects.md

Co-authored-by: Aaron Pham <29749331+aarnphm@users.noreply.github.com>

* docs: update docs/plugins/Assets.md

Co-authored-by: Aaron Pham <29749331+aarnphm@users.noreply.github.com>

* docs: update docs/plugins/CNAME.md

Co-authored-by: Aaron Pham <29749331+aarnphm@users.noreply.github.com>

* docs: update docs/plugins/Static.md

Co-authored-by: Aaron Pham <29749331+aarnphm@users.noreply.github.com>

* docs: update docs

* docs: update docs/features/Mermaid diagrams.md

Co-authored-by: Aaron Pham <29749331+aarnphm@users.noreply.github.com>

* docs: update docs/plugins/RemoveDrafts.md

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* docs: update docs/plugins/Assets.md

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* docs: update docs/configuration.md

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* docs: update docs/configuration.md

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* docs: update docs/configuration.md

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* docs: some updates

* docs: work in review comments

---------

Signed-off-by: Eiko Wagenknecht <git@eiko-wagenknecht.de>
Co-authored-by: Aaron Pham <29749331+aarnphm@users.noreply.github.com>
Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
2024-02-23 12:07:53 -08:00
Jacky Zhao
421718958f fix(callouts): use user provided title instead of canonical for default title 2024-02-23 11:20:35 -08:00
Jacky Zhao
be9b6b3a1e fix(docs): make docs accurate to callout behaviour (closes #920) 2024-02-23 09:32:22 -08:00
KylinDC
fb66ae2838 deps(highlighting): migrate to shiki as shikiji has been archived (#918) 2024-02-22 21:56:26 -08:00
Aaron Pham
129e878b29 chore(img): return targetUrl as given href (#916)
Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>
2024-02-22 21:55:35 -08:00
Aaron Pham
96c7076fb5 feat(popover): add support for PDF (#913)
* feat(popover): add support for PDF

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

* chore: split pdf by ';'

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

* fix: remove unnecessary check

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

---------

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>
2024-02-22 22:16:40 -05:00
Aaron Pham
345c347a56 chore: passing additional buildCtx to componentData (#914)
Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>
2024-02-22 17:51:07 -08:00
Aster Hu
916aedce40 docs: Add Aster's notebook to showcase.md (#912) 2024-02-21 19:04:36 -08:00
kon-foo
7dd596ebce docs: Fix in explorer.md (#911) 2024-02-21 08:18:44 -08:00
Eiko Wagenknecht
1c3f3d03e1 fix(toc): correct type for minEntries param (#909) 2024-02-20 09:06:53 -08:00
Eiko Wagenknecht
3b266ee7d0 fix: add space and missing dot for listing pages (#907) 2024-02-20 09:45:10 -05:00
JONG HWAN KIM
fc5fa48bf1 feat(i18n): change itemsUnderFolder, itemsUnderTag translation of ko-KR (#905)
* feat(i18n): add Korean

* feat(i18n): add Korean

* feat(i18n): change itemsUnderFolder, itemsUnderTag translation of ko-KR
2024-02-19 22:36:54 -08:00
Eiko Wagenknecht
b6cf3df84f fix: correctly parse falsy js as title (#900) 2024-02-19 13:49:07 -08:00
dependabot[bot]
779c501d9e chore(deps): bump preact from 10.19.4 to 10.19.5 (#898)
Bumps [preact](https://github.com/preactjs/preact) from 10.19.4 to 10.19.5.
- [Release notes](https://github.com/preactjs/preact/releases)
- [Commits](https://github.com/preactjs/preact/compare/10.19.4...10.19.5)

---
updated-dependencies:
- dependency-name: preact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 13:47:07 -08:00
dependabot[bot]
f1619620d5 chore(deps): bump globby from 14.0.0 to 14.0.1 (#897)
Bumps [globby](https://github.com/sindresorhus/globby) from 14.0.0 to 14.0.1.
- [Release notes](https://github.com/sindresorhus/globby/releases)
- [Commits](https://github.com/sindresorhus/globby/compare/v14.0.0...v14.0.1)

---
updated-dependencies:
- dependency-name: globby
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 13:47:00 -08:00
dependabot[bot]
637e336cda chore(deps-dev): bump @types/node from 20.11.16 to 20.11.19 (#899)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.11.16 to 20.11.19.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 13:46:27 -08:00
kon-foo
0493942c79 fix: remove assets via globs to avoid volume mount lock (#877)
* Fix docker volume lock issue by altering asset cleanup method
Modified build process to prevent the deletion of the output directory.

* Add fsOps utility for filesystem operations

* Use cleanDirectory in build process to fix volume lock issue

* applied prettier

* handle ENOENT error when output dir does not exist

* remove native function in favor of rimraf

* use path.join to concatenate paths
2024-02-19 11:04:27 -08:00
kabirgh
a67a8d7aa9 feat: implement getDependencyGraph for TagPage (#872)
* feat: implement getDependencyGraph for TagPage

* Only add file to dg if it has at least 1 tag
2024-02-19 13:58:15 -05:00
KylinDC
e85ea49000 feat(i18n): add Simplified Chinese (#896) 2024-02-19 13:31:09 -05:00
kon-foo
3e09b05468 docs: add self-hosting section (#883)
* Add Self-Hosting section
Add Nginx section

* run prettier
2024-02-19 12:50:40 -05:00
Leonardo Ledda
d9e8ffc78c feat(i18n): Add Italian (#893)
Signed-off-by: Leonardo Ledda <leonardoledda@gmail.com>
2024-02-19 12:50:01 -05:00
Eiko Wagenknecht
efd46f84de fix(frontmatter): delimiters parameter was not passed (#885)
* fix: delimiters parameter was not passed

Signed-off-by: Eiko Wagenknecht <git@eiko-wagenknecht.de>

* fix: remove unneeded undefined

---------

Signed-off-by: Eiko Wagenknecht <git@eiko-wagenknecht.de>
2024-02-19 00:08:36 -08:00
s-crypt
739c2e2cc8 perf(cdn): CDNJS instead of JSDelivr (#891) 2024-02-18 20:26:04 -08:00
JONG HWAN KIM
b1a105371b feat(i18n): add Korean (#889)
* feat(i18n): add Korean

* feat(i18n): add Korean
2024-02-18 17:37:59 -05:00
makondratev
8c5c5f9130 feat(i18n): add Russian (#886) 2024-02-18 13:54:37 -05:00
Jacky Zhao
aa24a62ae7 fix(breadcrumbs): calculate trailing slash for tag hierarchies (closes #873) 2024-02-17 11:12:35 -08:00
Jacky Zhao
a6690c6503 fix(style): bold should use semibold 2024-02-17 10:57:59 -08:00
Jacky Zhao
06e3f8b93d fix(style): introduce semiBoldWeight and various improvements to reduce CLS 2024-02-17 10:34:51 -08:00
Silviu Lorenț
fa2ea2896f feat: add user-defined config for syntax highlighting plugin (#869)
* feat: add user-defined options to syntax highlighting plugin

* feat: add default syntax highlighting config to `quartz.config.ts`

* chore: refactor according to @aarnphm's review

Co-authored-by: Aaron Pham <29749331+aarnphm@users.noreply.github.com>

* chore: run Prettier on `quartz/plugins/transformers/syntax.ts`

* Update quartz/plugins/transformers/syntax.ts

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* Update syntax.ts

---------

Co-authored-by: Aaron Pham <29749331+aarnphm@users.noreply.github.com>
Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
2024-02-17 10:23:45 -08:00
kabirgh
5af707ea20 fix/feat(fast rebuild): re-render transclusions in normal and fastRebuild mode (#842)
* Re-render transclusions in normal watch mode

* Include transclusions in ContentPage getDependencyGraph

* Address PR comments
2024-02-17 09:45:01 -08:00
kabirgh
823d952922 feat: implement getDependencyGraph for AliasRedirects emitter (#860) 2024-02-15 19:50:48 -05:00
kabirgh
78a408c96a feat: implement getDependencyGraph for FolderPage (#849) 2024-02-15 19:50:33 -05:00
David Fischer
6c8023463d Add support for image popovers (#854)
* feat(popover): Add support for images

* fix: run prettier

* feat(popover): use switch logic for content types & adjust styles

* feat(popover): Add content type data tag for popover-inner class
2024-02-14 15:41:13 -05:00
Aaron Bull Schaefer
2041341d9f docs: workaround for shallow clones on Cloudflare Pages (#868)
Rather than recommend a different hosting provider, Cloudflare Pages
users that prioritize the `git` method for their `CreatedModifiedDate`
configuration can preface the build command with a means of fetching the
required repository history.

See:
- https://gohugo.io/methods/page/gitinfo/#hosting-considerations
2024-02-14 09:41:44 -08:00
Aaron Pham
21c6bbf302 chore(types): add additional hint for LSP support (#864)
Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>
2024-02-13 23:53:44 -05:00
Jacky Zhao
b87a701ff7 fix: base.com not being resolved properly with joinSegments 2024-02-13 01:27:27 -08:00
Lin
880a9511b6 fix: incorrect link resolution for transclusion in root index file (#853)
Co-authored-by: Lauréline Nevin <laureline.nevin@unicaen.fr>
2024-02-13 03:11:16 -05:00
dependabot[bot]
a31e3f9458 chore(deps): bump @floating-ui/dom from 1.6.1 to 1.6.3 (#857)
Bumps [@floating-ui/dom](https://github.com/floating-ui/floating-ui/tree/HEAD/packages/dom) from 1.6.1 to 1.6.3.
- [Release notes](https://github.com/floating-ui/floating-ui/releases)
- [Changelog](https://github.com/floating-ui/floating-ui/blob/master/packages/dom/CHANGELOG.md)
- [Commits](https://github.com/floating-ui/floating-ui/commits/@floating-ui/dom@1.6.3/packages/dom)

---
updated-dependencies:
- dependency-name: "@floating-ui/dom"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-13 00:21:45 -05:00
dependabot[bot]
2c06e68ba6 chore(deps): bump preact from 10.19.3 to 10.19.4 (#858)
Bumps [preact](https://github.com/preactjs/preact) from 10.19.3 to 10.19.4.
- [Release notes](https://github.com/preactjs/preact/releases)
- [Commits](https://github.com/preactjs/preact/compare/10.19.3...10.19.4)

---
updated-dependencies:
- dependency-name: preact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-13 00:21:30 -05:00
dependabot[bot]
4a28d0e5d1 chore(deps-dev): bump tsx from 4.7.0 to 4.7.1 (#859)
Bumps [tsx](https://github.com/privatenumber/tsx) from 4.7.0 to 4.7.1.
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/develop/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.7.0...v4.7.1)

---
updated-dependencies:
- dependency-name: tsx
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-13 00:21:13 -05:00
Jacky Zhao
a7325eadc1 fix(analytics): umami custom host should be a string (closes #852) 2024-02-12 09:01:05 -08:00
Emile Bangma
5dc4f21a4b feat(i18n): localize the min read string for the nl-NL locale (#850)
* Update min read translation

* Added nl_BE to Dutch

Added Flemish (nl_BE) to point to nl.

* Removed period to match other translations
2024-02-12 08:58:00 -08:00
Jacky Zhao
76f295620c feat: add transclude-src to transclude 'link to original' 2024-02-12 08:52:00 -08:00
kabirgh
226891b9b1 fix(fast rebuild): call only required emitters, don't always copy assets (#845)
* fix(fast rebuild): call only required emitters, don't always copy assets

* Type function
2024-02-11 12:20:44 -08:00
Jacky Zhao
389f2e8bee fix(ofm): allow diacretic marks in tag regex (closes #830) 2024-02-11 12:12:01 -08:00
dependabot[bot]
998198cffb chore(deps): bump esbuild-sass-plugin from 2.16.0 to 2.16.1 (#778)
Bumps [esbuild-sass-plugin](https://github.com/glromeo/esbuild-sass-plugin) from 2.16.0 to 2.16.1.
- [Release notes](https://github.com/glromeo/esbuild-sass-plugin/releases)
- [Commits](https://github.com/glromeo/esbuild-sass-plugin/compare/v2.16.0...v2.16.1)

---
updated-dependencies:
- dependency-name: esbuild-sass-plugin
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-11 11:27:16 -08:00
Jacky Zhao
4a6a44950f fix(breadcrumbs): folder index by full path rather than folder name (closes #676) 2024-02-11 11:26:24 -08:00
Silviu Lorenț
2b39bd93f3 feat(i18n): localize the min read string for the ro-RO locale (#847)
* feat(i18n): localize `min read` string for `ro-RO` locale

* chore: run Prettier on `quartz/i18n/locales/ro-RO.ts`
2024-02-11 11:23:58 -08:00
Neel Shah
b5295e0f26 fix: breadcrumbs displayName issue for file names ending with index (#839) 2024-02-11 11:08:12 -08:00
Jacky Zhao
ab0e20b4d0 chore: refactor out and export endsWith 2024-02-11 10:57:24 -08:00
Silviu Lorenț
af5f5abad4 docs: add documentation for Umami analytics integration (#846) 2024-02-11 10:51:10 -08:00
Alq
3518ca9e2a feat(i18n): localize the min read string (#838)
* feat(i18n): localize the min read string fixes #825

* chore: format
2024-02-11 10:43:08 -08:00
Aaron Pham
ab80eba794 chore(callouts): remove unnecessary whitespaces after class name (#833)
Though we should have a plugins that just strip whitespace in all node
class.

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>
2024-02-10 17:19:17 -05:00
Aaron Pham
6ae0bb0908 chore: move fonts all into static folder (#835)
* chore: move fonts all into static folder

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* chore: update formatter

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

---------

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>
Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
2024-02-10 17:17:41 -05:00
Aaron Pham
db5e701810 feat(i18n): support parsing callouts (#834)
* feat(i18n): support parsing callouts

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

* chore: move callout into components

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

* chore: update arabic translation

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

* fix: make sure to use correct items

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

---------

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>
2024-02-10 17:09:57 -05:00
Alq
a0d6daa3b4 feat(i18n): add Arabic translation (#837)
* feat(i18n): add Arabic translation

* chore: format
2024-02-10 09:02:28 -08:00
kabirgh
fe353d946b feat(experimental): partial rebuilds (#716) 2024-02-09 10:07:32 -05:00
Jacky Zhao
a87704cd05 fix: set default locale for lang attribute 2024-02-08 09:31:36 -08:00
Silviu Lorenț
fd785ada56 feat(i18n): use Romanian translation for ro-MD locale (#828) 2024-02-08 08:48:13 -08:00
Serhii Stets
e186811c9c added Ukrainian to i18n (#829) 2024-02-08 08:47:12 -08:00
Aaron Pham
51818efc38 fix(umami): format correct string from custom hosts (#826) 2024-02-08 08:45:20 -08:00
Aaron Pham
330e322e48 feat(fonts): fetch before build (#817)
* feat: fetch google fonts before build

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

* Update quartz/plugins/emitters/componentResources.ts

* fix: fetching wolff2

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

* chore: remove request stylesheet

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

* fix: race condition

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

* chore: remove preconnect for static fonts

since we are already downloading fonts into public folder

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

* chore: remove deadcode

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

* chore: add options to gate for cdn caching

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* chore: apply jacky's suggestion

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* chore: add docs and only use one promise

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

* fix: fmt

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

* chore: remove deadcode

* chore: final touches

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* revert: changes in theme.ts

* fix: styles and remove deadcode

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

---------

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>
Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
2024-02-08 02:52:55 -05:00
Miguel Pimentel
ca284778b2 add Spanish translations (#822)
* add Spanish translations

* format with prettier

* clears npm ci, formatted w/ prettier
2024-02-07 09:57:14 -08:00
Aaron Pham
2578597f7e chore(lang): lang element based on frontmatter or default locale (#819)
default locale

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>
2024-02-07 09:29:47 -08:00
Silviu Lorenț
ce413b4bae feat(i18n): add Romanian to i18n (#821) 2024-02-07 11:26:45 -05:00
Aaron Pham
d2fb50b83c fix(links): show backdrop on links highlighted in headers alias (#816)
* fix: assign specific classes based on parent node

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

* fix: use custom role for anchor icone

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

* fix: allow color on links 😄

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

* chore: unify search inner container

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

---------

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>
2024-02-06 02:06:19 -05:00
Aaron Pham
52ef6d1b6f fix(search): set background-color for icon within preview panel (#815)
Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>
2024-02-06 01:12:31 -05:00
Jacky Zhao
34334eabed perf: don't load mermaid if its not on the page 2024-02-05 20:36:31 -08:00
Jacky Zhao
bec726b666 fix(i18n): forgot a string 2024-02-05 16:40:39 -08:00
Jacky Zhao
2b9659a1c2 fix(i18n): add default locale 2024-02-05 14:19:21 -08:00
dependabot[bot]
19fc53854f chore(deps-dev): bump @types/node from 20.11.14 to 20.11.16 (#811)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.11.14 to 20.11.16.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-05 16:19:49 -05:00
dependabot[bot]
479cbb6d91 chore(deps): bump @napi-rs/simple-git from 0.1.14 to 0.1.16 (#810)
Bumps [@napi-rs/simple-git](https://github.com/Brooooooklyn/simple-git) from 0.1.14 to 0.1.16.
- [Release notes](https://github.com/Brooooooklyn/simple-git/releases)
- [Commits](https://github.com/Brooooooklyn/simple-git/compare/v0.1.14...v0.1.16)

---
updated-dependencies:
- dependency-name: "@napi-rs/simple-git"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-05 16:19:34 -05:00
Emile Bangma
b169a5880f feat(i18n): Add Dutch to i18n (#813)
* Create nl-NL.ts

* Update index.ts

* Update nl-NL.ts
2024-02-05 13:12:54 -08:00
松浦 知也 Matsuura Tomoya
ba836dd3e0 feat(i18n): Add Japanese to i18n (#809)
Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
2024-02-05 08:58:31 -08:00
Mats Fangohr
b061b1b6a2 feat(i18n): German translation (#808) 2024-02-05 09:59:58 -05:00
Aaron Pham
e58c217de1 feat: support checkbox (closes #646) (#799)
* feat: support checkbox

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

* chore: apply review from jacky

---------

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>
2024-02-04 22:19:25 -08:00
Aaron Pham
90725688a7 style(search): increase width on mobile view (#796)
Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>
2024-02-04 21:52:24 -08:00
Jacky Zhao
c891ad8ff5 pkg: bump to 4.2.2 2024-02-04 21:23:17 -08:00
Jacky Zhao
06ee73e006 fix(path): properly path encode & 2024-02-04 21:22:57 -08:00
Jacky Zhao
36e4cc41a9 chore(i18n): refactor and cleanup (#805)
* checkpoint

* finish

* docs
2024-02-04 20:57:10 -08:00
Mats Fangohr
dff4b06313 fix(i18n): backlinks naming in mapping (#800) 2024-02-04 09:48:31 -05:00
Aaron Pham
5b90fbd0d0 feat(ofm): parsing all type of arrow (#797)
* feat(ofm): parsing all type of arrow

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

* fix: use html value instead of decimal

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

* fix: skip parsing arrow if it is not a valid supported mapping

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

---------

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>
2024-02-04 00:51:55 -05:00
Mara-Li
dbbc672c67 feat: Adding support for i18n (closes #462) (#738)
* fix: alt error mix with height/width

More granular detection of alt and resize in image

* fix: format

* feat: init i18n

* feat: add translation

* style: prettier for test

* fix: build-up the locale to fusion with dateLocale

* style: run prettier

* remove cursed file

* refactor: remove i18n library and use locale way instead

* format with prettier

* forgot to remove test

* prevent merging error

* format

* format

* fix: allow string for locale
- Check during translation if valid / existing locale
- Allow to use "en" and "en-US" for example
- Add fallback directly in the function
- Add default key in the function
- Add docstring to cfg.ts

* forgot item translation

* remove unused locale variable

* forgot to remove fr-FR testing

* format
2024-02-03 19:55:24 -08:00
Jacky Zhao
3fb3930df8 fix: calculate heading after latex (closes #719) 2024-02-03 19:44:24 -08:00
Jacky Zhao
742b883256 fix(search): flex basis and card highlighting 2024-02-02 12:18:02 -08:00
Jacky Zhao
9ff1fdd280 fix(search): oops restore ability to preview on hover lol 2024-02-02 10:52:51 -08:00
Jacky Zhao
a2c46f442d fix(search): dont rely on mouse to manipulate focus 2024-02-02 10:44:19 -08:00
Jacky Zhao
260498a96b fix(style): prevent callout icon from shrinking on long titles (closes #792) 2024-02-02 10:23:24 -08:00
Jacky Zhao
0a3379a853 fix(search): null checks and focus fixes 2024-02-02 10:10:25 -08:00
Luis Michaelis
bece8fcab6 fix: properly handle absolute paths in CreatedModifiedDate (#790)
When providing an absolute path to the content directory (e.g. when using an Obsidian Vault in another directory), the build step would fail with

    Failed to process `/absolute/path/to/file.md`: ENOENT: no such file or directory, stat '/current/working/directory/absolute/path/'

This problem originated in the `CreatedModifiedDate` transformer which tries to construct a native filesystem path to the file to call `fs.stat` on. It did not however, account for the original file path contained in the received `VFile` being an absolute path and so, just concatenated the current working directory with the absolute path producing a nonexistent one.

This patch adds a simple fix for this issue by checking if the original file path is already absolute before concatenating with the current working directory.
2024-02-02 09:51:34 -08:00
Jacky Zhao
18745a9dc6 fix(style): correctly collapse on mobile 2024-02-02 09:36:36 -08:00
Jacky Zhao
34a8dfcd55 pkg: bump to 4.2.1 2024-02-02 01:45:28 -08:00
Jacky Zhao
44da82467e fix(style): remove redundant selector 2024-02-02 01:45:15 -08:00
Jacky Zhao
3231ce6e79 fix: search async ordering, scroll offset 2024-02-02 01:36:17 -08:00
Jacky Zhao
a0b927da4a fix: use display instead of visibility for click handling pasthrough 2024-02-02 01:24:40 -08:00
Jacky Zhao
5ab922f316 fix(revert): font aliasing 2024-02-02 01:15:10 -08:00
Jacky Zhao
d11a0e71a8 fix: font smoothing defaults 2024-02-02 01:01:04 -08:00
Jacky Zhao
2b57a68e1f fix: font weight consistency 2024-02-02 00:53:09 -08:00
Jacky Zhao
18cd58617d fix: parallelize search indexing 2024-02-02 00:53:09 -08:00
Aaron Pham
ee868b2d79 fix(search): set correct attribute on hover icon (#787)
Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>
2024-02-02 00:35:53 -08:00
Jacky Zhao
5a36e5b68d fix(style): reasonable page width for rich search preview 2024-02-02 00:29:45 -08:00
Jacky Zhao
0416c03ae6 fix: be more eager about constructing search index 2024-02-02 00:25:05 -08:00
Jacky Zhao
3b596c9311 fix: flatmap children when highlighting rich preview to avoid body 2024-02-02 00:19:19 -08:00
129 changed files with 3523 additions and 662 deletions

View File

@@ -46,8 +46,14 @@ jobs:
- name: Ensure Quartz builds, check bundle info - name: Ensure Quartz builds, check bundle info
run: npx quartz build --bundleInfo run: npx quartz build --bundleInfo
- name: Get package version
run: node -p -e '`PACKAGE_VERSION=${require("./package.json").version}`' >> $GITHUB_ENV
- name: Create release tag - name: Create release tag
uses: Klemensas/action-autotag@stable uses: pkgdeps/git-tag-action@v2
with: with:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" github_token: ${{ secrets.GITHUB_TOKEN }}
tag_prefix: "v" github_repo: ${{ github.repository }}
version: ${{ env.PACKAGE_VERSION }}
git_commit_sha: ${{ github.sha }}
git_tag_prefix: "v"

View File

@@ -53,12 +53,12 @@ All transformer plugins must define at least a `name` field to register the plug
Normally for both `remark` and `rehype`, you can find existing plugins that you can use to . If you'd like to create your own `remark` or `rehype` plugin, checkout the [guide to creating a plugin](https://unifiedjs.com/learn/guide/create-a-plugin/) using `unified` (the underlying AST parser and transformer library). Normally for both `remark` and `rehype`, you can find existing plugins that you can use to . If you'd like to create your own `remark` or `rehype` plugin, checkout the [guide to creating a plugin](https://unifiedjs.com/learn/guide/create-a-plugin/) using `unified` (the underlying AST parser and transformer library).
A good example of a transformer plugin that borrows from the `remark` and `rehype` ecosystems is the [[Latex]] plugin: A good example of a transformer plugin that borrows from the `remark` and `rehype` ecosystems is the [[plugins/Latex|Latex]] plugin:
```ts title="quartz/plugins/transformers/latex.ts" ```ts title="quartz/plugins/transformers/latex.ts"
import remarkMath from "remark-math" import remarkMath from "remark-math"
import rehypeKatex from "rehype-katex" import rehypeKatex from "rehype-katex"
import rehypeMathjax from "rehype-mathjax/svg.js" import rehypeMathjax from "rehype-mathjax/svg"
import { QuartzTransformerPlugin } from "../types" import { QuartzTransformerPlugin } from "../types"
interface Options { interface Options {
@@ -84,10 +84,14 @@ export const Latex: QuartzTransformerPlugin<Options> = (opts?: Options) => {
externalResources() { externalResources() {
if (engine === "katex") { if (engine === "katex") {
return { return {
css: ["https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css"], css: [
// base css
"https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css",
],
js: [ js: [
{ {
src: "https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/contrib/copy-tex.min.js", // fix copy behaviour: https://github.com/KaTeX/KaTeX/blob/main/contrib/copy-tex/README.md
src: "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/contrib/copy-tex.min.js",
loadTime: "afterDOMReady", loadTime: "afterDOMReady",
contentType: "external", contentType: "external",
}, },
@@ -278,7 +282,7 @@ export const ContentPage: QuartzEmitterPlugin = () => {
allFiles, allFiles,
} }
const content = renderPage(slug, componentData, opts, externalResources) const content = renderPage(cfg, slug, componentData, opts, externalResources)
const fp = await emit({ const fp = await emit({
content, content,
slug: file.data.slug!, slug: file.data.slug!,

View File

@@ -38,3 +38,7 @@ Some common frontmatter fields that are natively supported by Quartz:
When your Quartz is at a point you're happy with, you can save your changes to GitHub. When your Quartz is at a point you're happy with, you can save your changes to GitHub.
First, make sure you've [[setting up your GitHub repository|already setup your GitHub repository]] and then do `npx quartz sync`. First, make sure you've [[setting up your GitHub repository|already setup your GitHub repository]] and then do `npx quartz sync`.
## Customization
Frontmatter parsing for `title`, `tags`, `aliases` and `cssclasses` is a functionality of the [[Frontmatter]] plugin, `date` is handled by the [[CreatedModifiedDate]] plugin and `description` by the [[Description]] plugin. See the plugin pages for customization options.

View File

@@ -25,14 +25,17 @@ This part of the configuration concerns anything that can affect the whole site.
- `enablePopovers`: whether to enable [[popover previews]] on your site. - `enablePopovers`: whether to enable [[popover previews]] on your site.
- `analytics`: what to use for analytics on your site. Values can be - `analytics`: what to use for analytics on your site. Values can be
- `null`: don't use analytics; - `null`: don't use analytics;
- `{ provider: 'plausible' }`: use [Plausible](https://plausible.io/), a privacy-friendly alternative to Google Analytics; or - `{ provider: 'google', tagId: '<your-google-tag>' }`: use Google Analytics;
- `{ provider: 'google', tagId: <your-google-tag> }`: use Google Analytics - `{ provider: 'plausible' }` (managed) or `{ provider: 'plausible', host: '<your-plausible-host>' }` (self-hosted): use [Plausible](https://plausible.io/);
- `{ provider: 'umami', host: '<your-umami-host>', websiteId: '<your-umami-website-id>' }`: use [Umami](https://umami.is/);
- `locale`: used for [[i18n]] and date formatting
- `baseUrl`: this is used for sitemaps and RSS feeds that require an absolute URL to know where the canonical 'home' of your site lives. This is normally the deployed URL of your site (e.g. `quartz.jzhao.xyz` for this site). Do not include the protocol (i.e. `https://`) or any leading or trailing slashes. - `baseUrl`: this is used for sitemaps and RSS feeds that require an absolute URL to know where the canonical 'home' of your site lives. This is normally the deployed URL of your site (e.g. `quartz.jzhao.xyz` for this site). Do not include the protocol (i.e. `https://`) or any leading or trailing slashes.
- This should also include the subpath if you are [[hosting]] on GitHub pages without a custom domain. For example, if my repository is `jackyzha0/quartz`, GitHub pages would deploy to `https://jackyzha0.github.io/quartz` and the `baseUrl` would be `jackyzha0.github.io/quartz` - This should also include the subpath if you are [[hosting]] on GitHub pages without a custom domain. For example, if my repository is `jackyzha0/quartz`, GitHub pages would deploy to `https://jackyzha0.github.io/quartz` and the `baseUrl` would be `jackyzha0.github.io/quartz`.
- Note that Quartz 4 will avoid using this as much as possible and use relative URLs whenever it can to make sure your site works no matter _where_ you end up actually deploying it. - Note that Quartz 4 will avoid using this as much as possible and use relative URLs whenever it can to make sure your site works no matter _where_ you end up actually deploying it.
- `ignorePatterns`: a list of [glob](<https://en.wikipedia.org/wiki/Glob_(programming)>) patterns that Quartz should ignore and not search through when looking for files inside the `content` folder. See [[private pages]] for more details. - `ignorePatterns`: a list of [glob](<https://en.wikipedia.org/wiki/Glob_(programming)>) patterns that Quartz should ignore and not search through when looking for files inside the `content` folder. See [[private pages]] for more details.
- `defaultDateType`: whether to use created, modified, or published as the default date to display on pages and page listings. - `defaultDateType`: whether to use created, modified, or published as the default date to display on pages and page listings.
- `theme`: configure how the site looks. - `theme`: configure how the site looks.
- `cdnCaching`: If `true` (default), use Google CDN to cache the fonts. This will generally will be faster. Disable (`false`) this if you want Quartz to download the fonts to be self-contained.
- `typography`: what fonts to use. Any font available on [Google Fonts](https://fonts.google.com/) works here. - `typography`: what fonts to use. Any font available on [Google Fonts](https://fonts.google.com/) works here.
- `header`: Font to use for headers - `header`: Font to use for headers
- `code`: Font for inline and block quotes. - `code`: Font for inline and block quotes.
@@ -53,7 +56,7 @@ You can think of Quartz plugins as a series of transformations over content.
![[quartz transform pipeline.png]] ![[quartz transform pipeline.png]]
```ts ```ts title="quartz.config.ts"
plugins: { plugins: {
transformers: [...], transformers: [...],
filters: [...], filters: [...],
@@ -61,22 +64,40 @@ plugins: {
} }
``` ```
- [[making plugins#Transformers|Transformers]] **map** over content (e.g. parsing frontmatter, generating a description) - [[tags/plugin/transformer|Transformers]] **map** over content (e.g. parsing frontmatter, generating a description)
- [[making plugins#Filters|Filters]] **filter** content (e.g. filtering out drafts) - [[tags/plugin/filter|Filters]] **filter** content (e.g. filtering out drafts)
- [[making plugins#Emitters|Emitters]] **reduce** over content (e.g. creating an RSS feed or pages that list all files with a specific tag) - [[tags/plugin/emitter|Emitters]] **reduce** over content (e.g. creating an RSS feed or pages that list all files with a specific tag)
By adding, removing, and reordering plugins from the `tranformers`, `filters`, and `emitters` fields, you can customize the behaviour of Quartz. You can customize the behaviour of Quartz by adding, removing and reordering plugins in the `transformers`, `filters` and `emitters` fields.
> [!note] > [!note]
> Each node is modified by every transformer _in order_. Some transformers are position-sensitive so you may need to take special note of whether it needs come before or after any other particular plugins. > Each node is modified by every transformer _in order_. Some transformers are position sensitive, so you may need to pay particular attention to whether they need to come before or after certain other plugins.
Additionally, plugins may also have their own configuration settings that you can pass in. For example, the [[Latex]] plugin allows you to pass in a field specifying the `renderEngine` to choose between Katex and MathJax. You should take care to add the plugin to the right entry corresponding to its plugin type. For example, to add the [[ExplicitPublish]] plugin (a [[tags/plugin/transformer|Transformer]], you would add the following line:
```ts ```ts title="quartz.config.ts"
transformers: [ transformers: [
Plugin.FrontMatter(), // uses default options ...
Plugin.Latex({ renderEngine: "katex" }), // specify some options Plugin.ExplicitPublish(),
...
],
```
To remove a plugin, you should remove all occurrences of it in the `quartz.config.ts`.
To customize plugins further, some plugins may also have their own configuration settings that you can pass in. If you do not pass in a configuration, the plugin will use its default settings.
For example, the [[plugins/Latex|Latex]] plugin allows you to pass in a field specifying the `renderEngine` to choose between Katex and MathJax.
```ts title="quartz.config.ts"
transformers: [
Plugin.FrontMatter(), // use default options
Plugin.Latex({ renderEngine: "katex" }), // set some custom options
] ]
``` ```
If you'd like to make your own plugins, read the guide on [[making plugins]] for more information. Some plugins are included by default in the[ `quartz.config.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz.config.ts), but there are more available.
You can see a list of all plugins and their configuration options [[tags/plugin|here]].
If you'd like to make your own plugins, see the [[making plugins|making custom plugins]] guide.

View File

@@ -1,6 +1,7 @@
--- ---
title: LaTeX
tags: tags:
- plugin/transformer - feature/transformer
--- ---
Quartz uses [Katex](https://katex.org/) by default to typeset both inline and block math expressions at build time. Quartz uses [Katex](https://katex.org/) by default to typeset both inline and block math expressions at build time.
@@ -38,6 +39,9 @@ a & b & c
\end{bmatrix} \end{bmatrix}
$$ $$
> [!warn]
> Due to limitations in the [underlying parsing library](https://github.com/remarkjs/remark-math), block math in Quartz requires the `$$` delimiters to be on newlines like above.
### Inline Math ### Inline Math
Similarly, inline math can be rendered by delimiting math expression with a single `$`. For example, `$e^{i\pi} = -1$` produces $e^{i\pi} = -1$ Similarly, inline math can be rendered by delimiting math expression with a single `$`. For example, `$e^{i\pi} = -1$` produces $e^{i\pi} = -1$
@@ -53,11 +57,15 @@ For example:
- Incorrect: `I have $1 and you have $2` produces I have $1 and you have $2 - Incorrect: `I have $1 and you have $2` produces I have $1 and you have $2
- Correct: `I have \$1 and you have \$2` produces I have \$1 and you have \$2 - Correct: `I have \$1 and you have \$2` produces I have \$1 and you have \$2
## MathJax ### Using mhchem
In `quartz.config.ts`, you can configure Quartz to use [MathJax SVG rendering](https://docs.mathjax.org/en/latest/output/svg.html) by replacing `Plugin.Latex({ renderEngine: 'katex' })` with `Plugin.Latex({ renderEngine: 'mathjax' })` Add the following import to the top of `quartz/plugins/transformers/latex.ts` (before all the other
imports):
```ts title="quartz/plugins/transformers/latex.ts"
import "katex/contrib/mhchem"
```
## Customization ## Customization
- Removing Latex support: remove all instances of `Plugin.Latex()` from `quartz.config.ts`. Latex parsing is a functionality of the [[plugins/Latex|Latex]] plugin. See the plugin page for customization options.
- Plugin: `quartz/plugins/transformers/latex.ts`

View File

@@ -1,9 +1,15 @@
---
title: "Mermaid Diagrams"
tags:
- feature/transformer
---
Quartz supports Mermaid which allows you to add diagrams and charts to your notes. Mermaid supports a range of diagrams, such as [flow charts](https://mermaid.js.org/syntax/flowchart.html), [sequence diagrams](https://mermaid.js.org/syntax/sequenceDiagram.html), and [timelines](https://mermaid.js.org/syntax/timeline.html). This is enabled as a part of [[Obsidian compatibility]] and can be configured and enabled/disabled from that plugin. Quartz supports Mermaid which allows you to add diagrams and charts to your notes. Mermaid supports a range of diagrams, such as [flow charts](https://mermaid.js.org/syntax/flowchart.html), [sequence diagrams](https://mermaid.js.org/syntax/sequenceDiagram.html), and [timelines](https://mermaid.js.org/syntax/timeline.html). This is enabled as a part of [[Obsidian compatibility]] and can be configured and enabled/disabled from that plugin.
By default, Quartz will render Mermaid diagrams to match the site theme. By default, Quartz will render Mermaid diagrams to match the site theme.
> [!warning] > [!warning]
> Wondering why Mermaid diagrams may not be showing up even if you have them enabled? You may need to reorder your plugins so that `Plugin.ObsidianFlavoredMarkdown()` is _after_ `Plugin.SyntaxHighlighting()`. > Wondering why Mermaid diagrams may not be showing up even if you have them enabled? You may need to reorder your plugins so that [[ObsidianFlavoredMarkdown]] is _after_ [[SyntaxHighlighting]].
## Syntax ## Syntax

View File

@@ -1,33 +1,17 @@
--- ---
title: "Obsidian Compatibility"
tags: tags:
- plugin/transformer - feature/transformer
--- ---
Quartz was originally designed as a tool to publish Obsidian vaults as websites. Even as the scope of Quartz has widened over time, it hasn't lost the ability to seamlessly interoperate with Obsidian. Quartz was originally designed as a tool to publish Obsidian vaults as websites. Even as the scope of Quartz has widened over time, it hasn't lost the ability to seamlessly interoperate with Obsidian.
By default, Quartz ships with `Plugin.ObsidianFlavoredMarkdown` which is a transformer plugin that adds support for [Obsidian Flavored Markdown](https://help.obsidian.md/Editing+and+formatting/Obsidian+Flavored+Markdown). This includes support for features like [[wikilinks]] and [[Mermaid diagrams]]. By default, Quartz ships with the [[ObsidianFlavoredMarkdown]] plugin, which is a transformer plugin that adds support for [Obsidian Flavored Markdown](https://help.obsidian.md/Editing+and+formatting/Obsidian+Flavored+Markdown). This includes support for features like [[wikilinks]] and [[Mermaid diagrams]].
It also ships with support for [frontmatter parsing](https://help.obsidian.md/Editing+and+formatting/Properties) with the same fields that Obsidian uses through the `Plugin.FrontMatter` transformer plugin. It also ships with support for [frontmatter parsing](https://help.obsidian.md/Editing+and+formatting/Properties) with the same fields that Obsidian uses through the [[Frontmatter]] transformer plugin.
Finally, Quartz also provides `Plugin.CrawlLinks` which allows you to customize Quartz's link resolution behaviour to match Obsidian. Finally, Quartz also provides [[CrawlLinks]] plugin, which allows you to customize Quartz's link resolution behaviour to match Obsidian.
## Configuration ## Configuration
- Frontmatter parsing: This functionality is provided by the [[ObsidianFlavoredMarkdown]], [[Frontmatter]] and [[CrawlLinks]] plugins. See the plugin pages for customization options.
- Disabling: remove all instances of `Plugin.FrontMatter()` from `quartz.config.ts`.
- Customize default values for frontmatter: edit `quartz/plugins/transformers/frontmatter.ts`
- Obsidian Flavored Markdown:
- Disabling: remove all instances of `Plugin.ObsidianFlavoredMarkdown()` from `quartz.config.ts`
- Customizing features: `Plugin.ObsidianFlavoredMarkdown` has several other options to toggle on and off:
- `comments`: whether to enable `%%` style Obsidian comments. Defaults to `true`
- `highlight`: whether to enable `==` style highlights. Defaults to `true`
- `wikilinks`: whether to enable turning [[wikilinks]] into regular links. Defaults to `true`
- `callouts`: whether to enable [[callouts]]. Defaults to `true`
- `mermaid`: whether to enable [[Mermaid diagrams]]. Defaults to `true`
- `parseTags`: whether to try and parse tags in the content body. Defaults to `true`
- `parseArrows`: whether to try and parse arrows in the content body. Defaults to `true`.
- `enableInHtmlEmbed`: whether to try and parse Obsidian flavoured markdown in raw HTML. Defaults to `false`
- `enableYouTubeEmbed`: whether to enable embedded YouTube videos using external image Markdown syntax. Defaults to `false`
- Link resolution behaviour:
- Disabling: remove all instances of `Plugin.CrawlLinks()` from `quartz.config.ts`
- Changing link resolution preference: set `markdownLinkResolution` to one of `absolute`, `relative` or `shortest`

View File

@@ -1,11 +1,12 @@
--- ---
title: "OxHugo Compatibility"
tags: tags:
- plugin/transformer - feature/transformer
--- ---
[org-roam](https://www.orgroam.com/) is a plain-text personal knowledge management system for [emacs](https://en.wikipedia.org/wiki/Emacs). [ox-hugo](https://github.com/kaushalmodi/ox-hugo) is org exporter backend that exports `org-mode` files to [Hugo](https://gohugo.io/) compatible Markdown. [org-roam](https://www.orgroam.com/) is a plain-text personal knowledge management system for [emacs](https://en.wikipedia.org/wiki/Emacs). [ox-hugo](https://github.com/kaushalmodi/ox-hugo) is org exporter backend that exports `org-mode` files to [Hugo](https://gohugo.io/) compatible Markdown.
Because the Markdown generated by ox-hugo is not pure Markdown but Hugo specific, we need to transform it to fit into Quartz. This is done by `Plugin.OxHugoFlavouredMarkdown`. Even though this [[making plugins|plugin]] was written with `ox-hugo` in mind, it should work for any Hugo specific Markdown. Because the Markdown generated by ox-hugo is not pure Markdown but Hugo specific, we need to transform it to fit into Quartz. This is done by the [[OxHugoFlavoredMarkdown]] plugin. Even though this plugin was written with `ox-hugo` in mind, it should work for any Hugo specific Markdown.
```typescript title="quartz.config.ts" ```typescript title="quartz.config.ts"
plugins: { plugins: {
@@ -25,15 +26,4 @@ Quartz by default doesn't understand `org-roam` files as they aren't Markdown. Y
## Configuration ## Configuration
- Link resolution This functionality is provided by the [[OxHugoFlavoredMarkdown]] plugin. See the plugin page for customization options.
- `wikilinks`: Whether to replace `{{ relref }}` with Quartz [[wikilinks]]
- `removePredefinedAnchor`: Whether to remove [pre-defined anchor set by ox-hugo](https://ox-hugo.scripter.co/doc/anchors/).
- Image handling
- `replaceFigureWithMdImg`: Whether to replace `<figure/>` with `![]()`
- Formatting
- `removeHugoShortcode`: Whether to remove hugo shortcode syntax (`{{}}`)
- `replaceOrgLatex`: Whether to replace org-mode formatting for latex fragments with what `Plugin.Latex` supports.
> [!warning]
>
> While you can use `Plugin.OxHugoFlavoredMarkdown` and `Plugin.ObsidianFlavoredMarkdown` together, it's not recommended because it might mutate the file in unexpected ways. Use with caution.

View File

@@ -1,7 +1,5 @@
Quartz creates an RSS feed for all the content on your site by generating an `index.xml` file that RSS readers can subscribe to. Because of the RSS spec, this requires the `baseUrl` property in your [[configuration]] to be set properly for RSS readers to pick it up properly. Quartz emits an RSS feed for all the content on your site by generating an `index.xml` file that RSS readers can subscribe to. Because of the RSS spec, this requires the `baseUrl` property in your [[configuration]] to be set properly for RSS readers to pick it up properly.
## Configuration ## Configuration
- Remove RSS feed: set the `enableRSS` field of `Plugin.ContentIndex` in `quartz.config.ts` to be `false`. This functionality is provided by the [[ContentIndex]] plugin. See the plugin page for customization options.
- Change number of entries: set the `rssLimit` field of `Plugin.ContentIndex` to be the desired value. It defaults to latest 10 items.
- Use rich HTML output in RSS: set `rssFullHtml` field of `Plugin.ContentIndex` to be `true`.

View File

@@ -1,7 +1,7 @@
--- ---
title: Callouts title: Callouts
tags: tags:
- plugin/transformer - feature/transformer
--- ---
Quartz supports the same Admonition-callout syntax as Obsidian. Quartz supports the same Admonition-callout syntax as Obsidian.
@@ -19,12 +19,13 @@ This includes
See [documentation on supported types and syntax here](https://help.obsidian.md/Editing+and+formatting/Callouts). See [documentation on supported types and syntax here](https://help.obsidian.md/Editing+and+formatting/Callouts).
> [!warning] > [!warning]
> Wondering why callouts may not be showing up even if you have them enabled? You may need to reorder your plugins so that `Plugin.ObsidianFlavoredMarkdown()` is _after_ `Plugin.SyntaxHighlighting()`. > Wondering why callouts may not be showing up even if you have them enabled? You may need to reorder your plugins so that [[ObsidianFlavoredMarkdown]] is _after_ [[SyntaxHighlighting]].
## Customization ## Customization
- Disable callouts: simply pass `callouts: false` to the plugin: `Plugin.ObsidianFlavoredMarkdown({ callouts: false })` The callouts are a functionality of the [[ObsidianFlavoredMarkdown]] plugin. See the plugin page for how to enable or disable them.
- Editing icons: `quartz/styles/callouts.scss`
You can edit the icons by customizing `quartz/styles/callouts.scss`.
### Add custom callouts ### Add custom callouts
@@ -55,50 +56,41 @@ By default, custom callouts are handled by applying the `note` style. To make fa
> > > >
> > > [!example] You can even use multiple layers of nesting. > > > [!example] You can even use multiple layers of nesting.
> [!EXAMPLE] Examples > [!note]
> > Aliases: "note"
> Aliases: example
> [!note] Notes > [!abstract]
> > Aliases: "abstract", "summary", "tldr"
> Aliases: note
> [!abstract] Summaries > [!info]
> > Aliases: "info"
> Aliases: abstract, summary, tldr
> [!info] Info > [!todo]
> > Aliases: "todo"
> Aliases: info, todo
> [!tip] Hint > [!tip]
> > Aliases: "tip", "hint", "important"
> Aliases: tip, hint, important
> [!success] Success > [!success]
> > Aliases: "success", "check", "done"
> Aliases: success, check, done
> [!question] Question > [!question]
> > Aliases: "question", "help", "faq"
> Aliases: question, help, faq
> [!warning] Warning > [!warning]
> > Aliases: "warning", "attention", "caution"
> Aliases: warning, caution, attention
> [!failure] Failure > [!failure]
> > Aliases: "failure", "missing", "fail"
> Aliases: failure, fail, missing
> [!danger] Error > [!danger]
> > Aliases: "danger", "error"
> Aliases: danger, error
> [!bug] Bug > [!bug]
> > Aliases: "bug"
> Aliases: bug
> [!quote] Quote > [!example]
> > Aliases: "example"
> Aliases: quote, cite
> [!quote]
> Aliases: "quote", "cite"

View File

@@ -42,7 +42,7 @@ When passing in your own options, you can omit any or all of these fields if you
Want to customize it even more? Want to customize it even more?
- Removing table of contents: remove `Component.Explorer()` from `quartz.layout.ts` - Removing explorer: remove `Component.Explorer()` from `quartz.layout.ts`
- (optional): After removing the explorer component, you can move the [[table of contents | Table of Contents]] component back to the `left` part of the layout - (optional): After removing the explorer component, you can move the [[table of contents | Table of Contents]] component back to the `left` part of the layout
- Changing `sort`, `filter` and `map` behavior: explained in [[#Advanced customization]] - Changing `sort`, `filter` and `map` behavior: explained in [[#Advanced customization]]
- Component: - Component:

View File

@@ -1,18 +1,20 @@
--- ---
title: Folder and Tag Listings title: Folder and Tag Listings
tags: tags:
- plugin/emitter - feature/emitter
--- ---
Quartz creates listing pages for any folders and tags you have. Quartz emits listing pages for any folders and tags you have.
## Folder Listings ## Folder Listings
Quartz will generate an index page for all the pages under that folder. This includes any content that is multiple levels deep. Quartz will generate an index page for all the pages under that folder. This includes any content that is multiple levels deep.
Additionally, Quartz will also generate pages for subfolders. Say you have a note in a nested folder `content/abc/def/note.md`. Then, Quartz would generate a page for all the notes under `abc` _and_ a page for all the notes under `abc/def`. Additionally, Quartz will also generate pages for subfolders. Say you have a note in a nested folder `content/abc/def/note.md`. Then Quartz would generate a page for all the notes under `abc` _and_ a page for all the notes under `abc/def`.
By default, Quartz will title the page `Folder: <name of folder>` and no description. You can override this by creating an `index.md` file in the folder with the `title` [[authoring content#Syntax|frontmatter]] field. Any content you write in this file will also be used in the description of the folder. You can link to the folder listing by referencing its name, plus a trailing slash, like this: `[[advanced/]]` (results in [[advanced/]]).
By default, Quartz will title the page `Folder: <folder name>` and no description. You can override this by creating an `index.md` file in the folder with the `title` [[authoring content#Syntax|frontmatter]] field. Any content you write in this file will also be used in the folder description.
For example, for the folder `content/posts`, you can add another file `content/posts/index.md` to add a specific description for it. For example, for the folder `content/posts`, you can add another file `content/posts/index.md` to add a specific description for it.
@@ -20,13 +22,12 @@ For example, for the folder `content/posts`, you can add another file `content/p
Quartz will also create an index page for each unique tag in your vault and render a list of all notes with that tag. Quartz will also create an index page for each unique tag in your vault and render a list of all notes with that tag.
Quartz also supports tag hierarchies as well (e.g. `plugin/emitter`) and will also render a separate tag page for each layer of the tag hierarchy. It will also create a default global tag index page at `/tags` that displays a list of all the tags in your Quartz. Quartz also supports tag hierarchies as well (e.g. `plugin/emitter`) and will also render a separate tag page for each level of the tag hierarchy. It will also create a default global tag index page at `/tags` that displays a list of all the tags in your Quartz.
Like folder listings, you can also provide a description and title for a tag page by creating a file for each tag. For example, if you wanted to create a custom description for the #component tag, you would create a file at `content/tags/component.md` with a title and description. You can link to the tag listing by referencing its name with a `tag/` prefix, like this: `[[tags/plugin]]` (results in [[tags/plugin]]).
As with folder listings, you can also provide a description and title for a tag page by creating a file for each tag. For example, if you wanted to create a custom description for the #component tag, you would create a file at `content/tags/component.md` with a title and description.
## Customization ## Customization
The layout for both the folder and content pages can be customized. By default, they use the `defaultListPageLayout` in `quartz.layouts.ts`. If you'd like to make more involved changes to the layout and don't mind editing some [[creating components|Quartz components]], you can take a look at `quartz/components/pages/FolderContent.tsx` and `quartz/components/pages/TagContent.tsx` respectively. The folder listings are a functionality of the [[FolderPage]] plugin, the tag listings of the [[TagPage]] plugin. See the plugin pages for customization options.
- Removing folder listings: remove `Plugin.FolderPage()` from `emitters` in `quartz.config.ts`
- Removing tag listings: remove `Plugin.TagPage()` from `emitters` in `quartz.config.ts`

18
docs/features/i18n.md Normal file
View 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`.

View File

@@ -8,6 +8,8 @@ By default, Quartz only fetches previews for pages inside your vault due to [COR
When [[creating components|creating your own components]], you can include this `popover-hint` class to also include it in the popover. When [[creating components|creating your own components]], you can include this `popover-hint` class to also include it in the popover.
Similar to Obsidian, [[quartz layout.png|images referenced using wikilinks]] can also be viewed as popups.
## Configuration ## Configuration
- Remove popovers: set the `enablePopovers` field in `quartz.config.ts` to be `false`. - Remove popovers: set the `enablePopovers` field in `quartz.config.ts` to be `false`.

View File

@@ -1,16 +1,16 @@
--- ---
title: Private Pages title: Private Pages
tags: tags:
- plugin/filter - feature/filter
--- ---
There may be some notes you want to avoid publishing as a website. Quartz supports this through two mechanisms which can be used in conjunction: There may be some notes you want to avoid publishing as a website. Quartz supports this through two mechanisms which can be used in conjunction:
## Filter Plugins ## Filter Plugins
[[making plugins#Filters|Filter plugins]] are plugins that filter out content based off of certain criteria. By default, Quartz uses the `Plugin.RemoveDrafts` plugin which filters out any note that has `draft: true` in the frontmatter. [[making plugins#Filters|Filter plugins]] are plugins that filter out content based off of certain criteria. By default, Quartz uses the [[RemoveDrafts]] plugin which filters out any note that has `draft: true` in the frontmatter.
If you'd like to only publish a select number of notes, you can instead use `Plugin.ExplicitPublish` which will filter out all notes except for any that have `publish: true` in the frontmatter. If you'd like to only publish a select number of notes, you can instead use [[ExplicitPublish]] which will filter out all notes except for any that have `publish: true` in the frontmatter.
> [!warning] > [!warning]
> Regardless of the filter plugin used, **all non-markdown files will be emitted and available publically in the final build.** This includes files such as images, voice recordings, PDFs, etc. One way to prevent this and still be able to embed local images is to create a folder specifically for public media and add the following two patterns to the ignorePatterns array. > Regardless of the filter plugin used, **all non-markdown files will be emitted and available publically in the final build.** This includes files such as images, voice recordings, PDFs, etc. One way to prevent this and still be able to embed local images is to create a folder specifically for public media and add the following two patterns to the ignorePatterns array.

View File

@@ -1,7 +1,7 @@
--- ---
title: Syntax Highlighting title: Syntax Highlighting
tags: tags:
- plugin/transformer - feature/transformer
--- ---
Syntax highlighting in Quartz is completely done at build-time. This means that Quartz only ships pre-calculated CSS to highlight the right words so there is no heavy client-side bundle that does the syntax highlighting. Syntax highlighting in Quartz is completely done at build-time. This means that Quartz only ships pre-calculated CSS to highlight the right words so there is no heavy client-side bundle that does the syntax highlighting.
@@ -130,6 +130,4 @@ const [name, setName] = useState('Taylor');
## Customization ## Customization
- Removing syntax highlighting: delete all usages of `Plugin.SyntaxHighlighting()` from `quartz.config.ts`. Syntax highlighting is a functionality of the [[SyntaxHighlighting]] plugin. See the plugin page for customization options.
- Style: By default, Quartz uses derivatives of the GitHub light and dark themes. You can customize the colours in the `quartz/styles/syntax.scss` file.
- Plugin: `quartz/plugins/transformers/syntax.ts`

View File

@@ -2,25 +2,17 @@
title: "Table of Contents" title: "Table of Contents"
tags: tags:
- component - component
- plugin/transformer - feature/transformer
--- ---
Quartz can automatically generate a table of contents from a list of headings on each page. It will also show you your current scroll position on the site by marking headings you've scrolled through with a different colour. Quartz can automatically generate a table of contents (TOC) from a list of headings on each page. It will also show you your current scrolling position on the page by highlighting headings you've scrolled through with a different color.
By default, it will show all headers from H1 (`# Title`) all the way to H3 (`### Title`) and will only show the table of contents if there is more than 1 header on the page. You can hide the TOC on a page by adding `enableToc: false` to the frontmatter for that page.
You can also hide the table of contents on a page by adding `enableToc: false` to the frontmatter for that page.
> [!info] By default, the TOC shows all headings from H1 (`# Title`) to H3 (`### Title`) and is only displayed if there is more than one heading on the page.
> This feature requires both `Plugin.TableOfContents` in your `quartz.config.ts` and `Component.TableOfContents` in your `quartz.layout.ts` to function correctly.
## Customization ## Customization
- Removing table of contents: remove all instances of `Plugin.TableOfContents()` from `quartz.config.ts`. and `Component.TableOfContents()` from `quartz.layout.ts` The table of contents is a functionality of the [[TableOfContents]] plugin. See the plugin page for more customization options.
- Changing the max depth: pass in a parameter to `Plugin.TableOfContents({ maxDepth: 4 })`
- Changing the minimum number of entries in the Table of Contents before it renders: pass in a parameter to `Plugin.TableOfContents({ minEntries: 3 })` It also needs the `TableOfContents` component, which is displayed in the right sidebar by default. You can change this by customizing the [[layout]]. The TOC component can be configured with the `layout` parameter, which can either be `modern` (default) or `legacy`.
- Collapse the table of content by default: pass in a parameter to `Plugin.TableOfContents({ collapseByDefault: true })`
- Component: `quartz/components/TableOfContents.tsx`
- Style:
- Modern (default): `quartz/components/styles/toc.scss`
- Legacy Quartz 3 style: `quartz/components/styles/legacyToc.scss`
- Script: `quartz/components/scripts/toc.inline.ts`

View File

@@ -4,7 +4,7 @@ title: Wikilinks
Wikilinks were pioneered by earlier internet wikis to make it easier to write links across pages without needing to write Markdown or HTML links each time. Wikilinks were pioneered by earlier internet wikis to make it easier to write links across pages without needing to write Markdown or HTML links each time.
Quartz supports Wikilinks by default and these links are resolved by Quartz using `Plugin.CrawlLinks`. See the [Obsidian Help page on Internal Links](https://help.obsidian.md/Linking+notes+and+files/Internal+links) for more information on Wikilink syntax. Quartz supports Wikilinks by default and these links are resolved by Quartz using the [[CrawlLinks]] plugin. See the [Obsidian Help page on Internal Links](https://help.obsidian.md/Linking+notes+and+files/Internal+links) for more information on Wikilink syntax.
This is enabled as a part of [[Obsidian compatibility]] and can be configured and enabled/disabled from that plugin. This is enabled as a part of [[Obsidian compatibility]] and can be configured and enabled/disabled from that plugin.

View File

@@ -30,7 +30,7 @@ Press "Save and deploy" and Cloudflare should have a deployed version of your si
To add a custom domain, check out [Cloudflare's documentation](https://developers.cloudflare.com/pages/platform/custom-domains/). To add a custom domain, check out [Cloudflare's documentation](https://developers.cloudflare.com/pages/platform/custom-domains/).
> [!warning] > [!warning]
> Cloudflare Pages only allows shallow `git` clones so if you rely on `git` for timestamps, it is recommended you either add dates to your frontmatter (see [[authoring content#Syntax]]) or use another hosting provider. > Cloudflare Pages performs a shallow clone by default, so if you rely on `git` for timestamps, it is recommended that you add `git fetch --unshallow &&` to the beginning of the build command (e.g., `git fetch --unshallow && npx quartz build`).
## GitHub Pages ## GitHub Pages
@@ -228,3 +228,25 @@ pages:
When `.gitlab-ci.yaml` is committed, GitLab will build and deploy the website as a GitLab Page. You can find the url under `Deploy > Pages` in the sidebar. When `.gitlab-ci.yaml` is committed, GitLab will build and deploy the website as a GitLab Page. You can find the url under `Deploy > Pages` in the sidebar.
By default, the page is private and only visible when logged in to a GitLab account with access to the repository but can be opened in the settings under `Deploy` -> `Pages`. By default, the page is private and only visible when logged in to a GitLab account with access to the repository but can be opened in the settings under `Deploy` -> `Pages`.
## Self-Hosting
Copy the `public` directory to your web server and configure it to serve the files. You can use any web server to host your site. Since Quartz generates links that do not include the `.html` extension, you need to let your web server know how to deal with it.
### Using Nginx
Here's an example of how to do this with Nginx:
```nginx title="nginx.conf"
server {
listen 80;
server_name example.com;
root /path/to/quartz/public;
index index.html;
error_page 404 /404.html;
location / {
try_files $uri $uri.html $uri/ =404;
}
}
```

View File

@@ -31,7 +31,7 @@ If you prefer instructions in a video format you can try following Nicole van de
## 🔧 Features ## 🔧 Features
- [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], [[Docker Support]], and [many more](./features) right out of the box - [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[features/Latex|Latex]], [[syntax highlighting]], [[popover previews]], [[Docker Support]], [[i18n|internationalization]] and [many more](./features) right out of the box
- Hot-reload for both configuration and content - Hot-reload for both configuration and content
- Simple JSX layouts and [[creating components|page components]] - Simple JSX layouts and [[creating components|page components]]
- [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes - [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes

View 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
View 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
View 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]] 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).

View 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).

View 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).

View 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).

View 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).

View 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).

View File

@@ -0,0 +1,22 @@
---
title: Description
tags:
- plugin/transformer
---
This plugin generates descriptions that are used as metadata for the HTML `head`, the [[RSS Feed]] and in [[folder and tag listings]] if there is no main body content, the description is used as the text between the title and the listing.
If the frontmatter contains a `description` property, it is used (see [[authoring content#Syntax]]). Otherwise, the plugin will do its best to use the first few sentences of the content to reach the target description length.
> [!note]
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
This plugin accepts the following configuration options:
- `descriptionLength`: the maximum length of the generated description. Default is 150 characters. The cut off happens after the first _sentence_ that ends after the given length.
## API
- Category: Transformer
- Function name: `Plugin.Description()`.
- Source: [`quartz/plugins/transformers/description.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/description.ts).

View 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: Emitter
- Function name: `Plugin.ExplicitPublish()`.
- Source: [`quartz/plugins/filters/explicit.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/filters/explicit.ts).

View File

@@ -0,0 +1,22 @@
---
title: FolderPage
tags:
- plugin/emitter
---
This plugin generates index pages for folders, creating a listing page for each folder that contains multiple content files. See [[folder and tag listings]] for more information.
Example: [[advanced/|Advanced]]
> [!note]
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
This plugin has no configuration options.
The pages are displayed using the `defaultListPageLayout` in `quartz.layouts.ts`. For the content, the `FolderContent` component is used. If you want to modify the layout, you must edit it directly (`quartz/components/pages/FolderContent.tsx`).
## API
- Category: Emitter
- Function name: `Plugin.FolderPage()`.
- Source: [`quartz/plugins/emitters/folderPage.tsx`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/folderPage.tsx).

View 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).

View 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).

View 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
View 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).

View 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).

View 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 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).

View 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).

View 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
View 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).

View 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).

View 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).

20
docs/plugins/TagPage.md Normal file
View File

@@ -0,0 +1,20 @@
---
title: TagPage
tags:
- plugin/emitter
---
This plugin emits dedicated pages for each tag used in the content. See [[folder and tag listings]] for more information.
> [!note]
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
This plugin has no configuration options.
The pages are displayed using the `defaultListPageLayout` in `quartz.layouts.ts`. For the content, the `TagContent` component is used. If you want to modify the layout, you must edit it directly (`quartz/components/pages/TagContent.tsx`).
## API
- Category: Emitter
- Function name: `Plugin.AliasRedirects()`.
- Source: [`quartz/plugins/emitters/tagPage.tsx`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/tagPage.tsx).

3
docs/plugins/index.md Normal file
View File

@@ -0,0 +1,3 @@
---
title: Plugins
---

View File

@@ -25,5 +25,6 @@ Want to see what Quartz can do? Here are some cool community gardens:
- [Scaling Synthesis - A hypertext research notebook](https://scalingsynthesis.com/) - [Scaling Synthesis - A hypertext research notebook](https://scalingsynthesis.com/)
- [Data Dictionary 🧠](https://glossary.airbyte.com/) - [Data Dictionary 🧠](https://glossary.airbyte.com/)
- [sspaeti.com's Second Brain](https://brain.sspaeti.com/) - [sspaeti.com's Second Brain](https://brain.sspaeti.com/)
- [🪴Aster's notebook](https://notes.asterhu.com)
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)! If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)!

3
docs/tags/plugin.md Normal file
View File

@@ -0,0 +1,3 @@
---
title: Plugins
---

194
package-lock.json generated
View File

@@ -1,26 +1,26 @@
{ {
"name": "@jackyzha0/quartz", "name": "@jackyzha0/quartz",
"version": "4.2.0", "version": "4.2.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@jackyzha0/quartz", "name": "@jackyzha0/quartz",
"version": "4.2.0", "version": "4.2.3",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@clack/prompts": "^0.7.0", "@clack/prompts": "^0.7.0",
"@floating-ui/dom": "^1.6.1", "@floating-ui/dom": "^1.6.3",
"@napi-rs/simple-git": "0.1.14", "@napi-rs/simple-git": "0.1.16",
"async-mutex": "^0.4.1", "async-mutex": "^0.4.1",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"cli-spinner": "^0.2.10", "cli-spinner": "^0.2.10",
"d3": "^7.8.5", "d3": "^7.8.5",
"esbuild-sass-plugin": "^2.16.0", "esbuild-sass-plugin": "^2.16.1",
"flexsearch": "0.7.43", "flexsearch": "0.7.43",
"github-slugger": "^2.0.0", "github-slugger": "^2.0.0",
"globby": "^14.0.0", "globby": "^14.0.1",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"hast-util-to-html": "^9.0.0", "hast-util-to-html": "^9.0.0",
"hast-util-to-jsx-runtime": "^2.3.0", "hast-util-to-jsx-runtime": "^2.3.0",
@@ -32,7 +32,7 @@
"mdast-util-to-hast": "^13.1.0", "mdast-util-to-hast": "^13.1.0",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"micromorph": "^0.4.5", "micromorph": "^0.4.5",
"preact": "^10.19.3", "preact": "^10.19.5",
"preact-render-to-string": "^6.3.1", "preact-render-to-string": "^6.3.1",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"pretty-time": "^1.1.0", "pretty-time": "^1.1.0",
@@ -40,7 +40,7 @@
"rehype-autolink-headings": "^7.1.0", "rehype-autolink-headings": "^7.1.0",
"rehype-katex": "^7.0.0", "rehype-katex": "^7.0.0",
"rehype-mathjax": "^6.0.0", "rehype-mathjax": "^6.0.0",
"rehype-pretty-code": "^0.12.6", "rehype-pretty-code": "^0.13.0",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"rehype-slug": "^6.0.0", "rehype-slug": "^6.0.0",
"remark": "^15.0.1", "remark": "^15.0.1",
@@ -54,7 +54,7 @@
"rfdc": "^1.3.1", "rfdc": "^1.3.1",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"serve-handler": "^6.1.5", "serve-handler": "^6.1.5",
"shikiji": "^0.10.2", "shiki": "^1.1.6",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"to-vfile": "^8.0.0", "to-vfile": "^8.0.0",
"toml": "^3.0.0", "toml": "^3.0.0",
@@ -73,14 +73,14 @@
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/hast": "^3.0.4", "@types/hast": "^3.0.4",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/node": "^20.11.14", "@types/node": "^20.11.19",
"@types/pretty-time": "^1.1.5", "@types/pretty-time": "^1.1.5",
"@types/source-map-support": "^0.5.10", "@types/source-map-support": "^0.5.10",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
"@types/yargs": "^17.0.32", "@types/yargs": "^17.0.32",
"esbuild": "^0.19.9", "esbuild": "^0.19.9",
"prettier": "^3.2.4", "prettier": "^3.2.4",
"tsx": "^4.7.0", "tsx": "^4.7.1",
"typescript": "^5.3.3" "typescript": "^5.3.3"
}, },
"engines": { "engines": {
@@ -486,12 +486,12 @@
} }
}, },
"node_modules/@floating-ui/dom": { "node_modules/@floating-ui/dom": {
"version": "1.6.1", "version": "1.6.3",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.1.tgz", "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz",
"integrity": "sha512-iA8qE43/H5iGozC3W0YSnVSW42Vh522yyM1gj+BqRwVsTNOyr231PsXDaV04yT39PsO0QL2QpbI/M0ZaLUQgRQ==", "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==",
"dependencies": { "dependencies": {
"@floating-ui/core": "^1.6.0", "@floating-ui/core": "^1.0.0",
"@floating-ui/utils": "^0.2.1" "@floating-ui/utils": "^0.2.0"
} }
}, },
"node_modules/@floating-ui/utils": { "node_modules/@floating-ui/utils": {
@@ -516,30 +516,30 @@
} }
}, },
"node_modules/@napi-rs/simple-git": { "node_modules/@napi-rs/simple-git": {
"version": "0.1.14", "version": "0.1.16",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git/-/simple-git-0.1.14.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git/-/simple-git-0.1.16.tgz",
"integrity": "sha512-2cDnsT0nKpQ7yg5u/Zf8/ibp9YFIKhpcfMAGATYuqdJoHuBo6P6UArZ0RDOOtfFC5b9FXuYcGw2ApbM4eWdnbQ==", "integrity": "sha512-C5wRPw9waqL2jk3jEDeJv+f7ScuO3N0a39HVdyFLkwKxHH4Sya4ZbzZsu2JLi6eEqe7RuHipHL6mC7B2OfYZZw==",
"engines": { "engines": {
"node": ">= 10" "node": ">= 10"
}, },
"optionalDependencies": { "optionalDependencies": {
"@napi-rs/simple-git-android-arm-eabi": "0.1.14", "@napi-rs/simple-git-android-arm-eabi": "0.1.16",
"@napi-rs/simple-git-android-arm64": "0.1.14", "@napi-rs/simple-git-android-arm64": "0.1.16",
"@napi-rs/simple-git-darwin-arm64": "0.1.14", "@napi-rs/simple-git-darwin-arm64": "0.1.16",
"@napi-rs/simple-git-darwin-x64": "0.1.14", "@napi-rs/simple-git-darwin-x64": "0.1.16",
"@napi-rs/simple-git-linux-arm-gnueabihf": "0.1.14", "@napi-rs/simple-git-linux-arm-gnueabihf": "0.1.16",
"@napi-rs/simple-git-linux-arm64-gnu": "0.1.14", "@napi-rs/simple-git-linux-arm64-gnu": "0.1.16",
"@napi-rs/simple-git-linux-arm64-musl": "0.1.14", "@napi-rs/simple-git-linux-arm64-musl": "0.1.16",
"@napi-rs/simple-git-linux-x64-gnu": "0.1.14", "@napi-rs/simple-git-linux-x64-gnu": "0.1.16",
"@napi-rs/simple-git-linux-x64-musl": "0.1.14", "@napi-rs/simple-git-linux-x64-musl": "0.1.16",
"@napi-rs/simple-git-win32-arm64-msvc": "0.1.14", "@napi-rs/simple-git-win32-arm64-msvc": "0.1.16",
"@napi-rs/simple-git-win32-x64-msvc": "0.1.14" "@napi-rs/simple-git-win32-x64-msvc": "0.1.16"
} }
}, },
"node_modules/@napi-rs/simple-git-android-arm-eabi": { "node_modules/@napi-rs/simple-git-android-arm-eabi": {
"version": "0.1.14", "version": "0.1.16",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm-eabi/-/simple-git-android-arm-eabi-0.1.14.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm-eabi/-/simple-git-android-arm-eabi-0.1.16.tgz",
"integrity": "sha512-fAJ/Hxc9DhtSHOcB3dPCRW1YcVsqAnbNoOOnHir4aDCtqTP64HrFa7A/675v3vQZpI0u3fXHRcYqW8NF0O/zcg==", "integrity": "sha512-dbrCL0Pl5KZG7x7tXdtVsA5CO6At5ohDX3myf5xIYn9kN4jDFxsocl8bNt6Vb/hZQoJd8fI+k5VlJt+rFhbdVw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -552,9 +552,9 @@
} }
}, },
"node_modules/@napi-rs/simple-git-android-arm64": { "node_modules/@napi-rs/simple-git-android-arm64": {
"version": "0.1.14", "version": "0.1.16",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm64/-/simple-git-android-arm64-0.1.14.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm64/-/simple-git-android-arm64-0.1.16.tgz",
"integrity": "sha512-dav730MRAR142DoyNDafuwKXcUCYwlbxxxxOarDph7bbN0mZZnKHOQohvRCD/Uz4aJLaj6khCavXSjLDWArEUg==", "integrity": "sha512-xYz+TW5J09iK8SuTAKK2D5MMIsBUXVSs8nYp7HcMi8q6FCRO7yJj96YfP9PvKsc/k64hOyqGmL5DhCzY9Cu1FQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -567,9 +567,9 @@
} }
}, },
"node_modules/@napi-rs/simple-git-darwin-arm64": { "node_modules/@napi-rs/simple-git-darwin-arm64": {
"version": "0.1.14", "version": "0.1.16",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-arm64/-/simple-git-darwin-arm64-0.1.14.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-arm64/-/simple-git-darwin-arm64-0.1.16.tgz",
"integrity": "sha512-f6+DqRnI+vFvnsAyw66mWhwl0vw1TOieHV07hvKbg4PU5j+RBI+lVqwY2L+IEAxDFlPirTWKKvGY1Lr7M/yi/A==", "integrity": "sha512-XfgsYqxhUE022MJobeiX563TJqyQyX4FmYCnqrtJwAfivESVeAJiH6bQIum8dDEYMHXCsG7nL8Ok0Dp8k2m42g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -582,9 +582,9 @@
} }
}, },
"node_modules/@napi-rs/simple-git-darwin-x64": { "node_modules/@napi-rs/simple-git-darwin-x64": {
"version": "0.1.14", "version": "0.1.16",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-x64/-/simple-git-darwin-x64-0.1.14.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-x64/-/simple-git-darwin-x64-0.1.16.tgz",
"integrity": "sha512-x/EnwJdDWJAFay8TQt09byJoBlVZhPEaTAPmRR0fUPzWTjrr28bOy8UW1ysszd9ylBxlyIhuWjOHMHu9CBigTQ==", "integrity": "sha512-tkEVBhD6vgRCbeWsaAQqM3bTfpIVGeitamPPRVSbsq8qgzJ5Dx6ZedH27R7KSsA/uao7mZ3dsrNLXbu1Wy5MzA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -597,9 +597,9 @@
} }
}, },
"node_modules/@napi-rs/simple-git-linux-arm-gnueabihf": { "node_modules/@napi-rs/simple-git-linux-arm-gnueabihf": {
"version": "0.1.14", "version": "0.1.16",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm-gnueabihf/-/simple-git-linux-arm-gnueabihf-0.1.14.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm-gnueabihf/-/simple-git-linux-arm-gnueabihf-0.1.16.tgz",
"integrity": "sha512-k0JZaXBl031gP5VOnoMa1I3lCHlBG7QvtunX5rxnRjx2kZ+JgUyT12s/qle/4xkJ0MnmfKTeiD7hs4Cc4Z3Tzw==", "integrity": "sha512-R6VAyNnp/yRaT7DV1Ao3r67SqTWDa+fNq2LrNy0Z8gXk2wB9ZKlrxFtLPE1WSpWknWtyRDLpRlsorh7Evk7+7w==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -612,9 +612,9 @@
} }
}, },
"node_modules/@napi-rs/simple-git-linux-arm64-gnu": { "node_modules/@napi-rs/simple-git-linux-arm64-gnu": {
"version": "0.1.14", "version": "0.1.16",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-gnu/-/simple-git-linux-arm64-gnu-0.1.14.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-gnu/-/simple-git-linux-arm64-gnu-0.1.16.tgz",
"integrity": "sha512-CsmKP6tSIxau10ZKxV1Q1kem2QcJ/Qlov7pxp1Q7kMErcouW0H6vliVniewicaXRVDYV9wK18iD2t5GoJttwlA==", "integrity": "sha512-LAGI0opFKw/HBMCV2qIBK3uWSEW9h4xd2ireZKLJy8DBPymX6NrWIamuxYNyCuACnFdPRxR4LaRFy4J5ZwuMdw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -627,9 +627,9 @@
} }
}, },
"node_modules/@napi-rs/simple-git-linux-arm64-musl": { "node_modules/@napi-rs/simple-git-linux-arm64-musl": {
"version": "0.1.14", "version": "0.1.16",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-musl/-/simple-git-linux-arm64-musl-0.1.14.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-musl/-/simple-git-linux-arm64-musl-0.1.16.tgz",
"integrity": "sha512-krfEckZQ3myoHwmGmqY0aHBnqAzzV66+jFNLQEKaVMSGsXA2P+UcGo0coGzmB13rFRWC2eZpZRNB3MrfrStHkw==", "integrity": "sha512-I57Ph0F0Yn2KW93ep+V1EzKhACqX0x49vvSiapqIsdDA2PifdEWLc1LJarBolmK7NKoPqKmf6lAKKO9lhiZzkg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -642,9 +642,9 @@
} }
}, },
"node_modules/@napi-rs/simple-git-linux-x64-gnu": { "node_modules/@napi-rs/simple-git-linux-x64-gnu": {
"version": "0.1.14", "version": "0.1.16",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-gnu/-/simple-git-linux-x64-gnu-0.1.14.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-gnu/-/simple-git-linux-x64-gnu-0.1.16.tgz",
"integrity": "sha512-4T2Q2QdO6t3OawkwdVmdqLz2EH8lfAw2cMT/zdjfTMfhNKjJgSg3kTgRnu/tf8TLCb+wu80fFvafwE0laB2VTQ==", "integrity": "sha512-AZYYFY2V7hlcQASPEOWyOa3e1skzTct9QPzz0LiDM3f/hCFY/wBaU2M6NC5iG3d2Kr38heuyFS/+JqxLm5WaKA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -657,9 +657,9 @@
} }
}, },
"node_modules/@napi-rs/simple-git-linux-x64-musl": { "node_modules/@napi-rs/simple-git-linux-x64-musl": {
"version": "0.1.14", "version": "0.1.16",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-musl/-/simple-git-linux-x64-musl-0.1.14.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-musl/-/simple-git-linux-x64-musl-0.1.16.tgz",
"integrity": "sha512-RaTGW8u+RXJbfRF4QN2Dcr5r5DrFh4wLjOvFeOy7sGX3Q9m3IKuw5AjRxTJqIw6xD/AAPKKNzOvPjrIF7728Lw==", "integrity": "sha512-9TyMcYSBJwjT8jwjY9m24BZbu7ozyWTjsmYBYNtK3B0Um1Ov6jthSNneLVvouQ6x+k3Ow+00TiFh6bvmT00r8g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -672,9 +672,9 @@
} }
}, },
"node_modules/@napi-rs/simple-git-win32-arm64-msvc": { "node_modules/@napi-rs/simple-git-win32-arm64-msvc": {
"version": "0.1.14", "version": "0.1.16",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-arm64-msvc/-/simple-git-win32-arm64-msvc-0.1.14.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-arm64-msvc/-/simple-git-win32-arm64-msvc-0.1.16.tgz",
"integrity": "sha512-kb9bKG9t79HJMuRMqbUJFLfWRf952O2Ea4VFwoRA2d/Uwtowm85Ol3JV9E6oeurguRLqdMLrUKyduCW6Hc9Jsg==", "integrity": "sha512-uslJ1WuAHCYJWui6xjsyT47SjX6KOHDtClmNO8hqKz1pmDSNY7AjyUY8HxvD1lK9bDnWwc4JYhikS9cxCqHybw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -687,9 +687,9 @@
} }
}, },
"node_modules/@napi-rs/simple-git-win32-x64-msvc": { "node_modules/@napi-rs/simple-git-win32-x64-msvc": {
"version": "0.1.14", "version": "0.1.16",
"resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-x64-msvc/-/simple-git-win32-x64-msvc-0.1.14.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-x64-msvc/-/simple-git-win32-x64-msvc-0.1.16.tgz",
"integrity": "sha512-3835xy0e2gOaZ3SPt1pINBFSBBL3dOx3cChyAzQU0TnMU4Ye/YOh1qa5pO7BOJlCSnOh7iWt782blxCT0HH61w==", "integrity": "sha512-SoEaVeCZCDF1MP+M9bMSXsZWgEjk4On9GWADO5JOulvzR1bKjk0s9PMHwe/YztR9F0sJzrCxwtvBZowhSJsQPg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -742,10 +742,15 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@shikijs/core": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.1.6.tgz",
"integrity": "sha512-kt9hhvrWTm0EPtRDIsoAZnSsFlIDBVBBI5CQewpA/NZCPin+MOKRXg+JiWc4y+8fZ/v0HzfDhu/UC+OTZGMt7A=="
},
"node_modules/@sindresorhus/merge-streams": { "node_modules/@sindresorhus/merge-streams": {
"version": "1.0.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz",
"integrity": "sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==", "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
@@ -1088,9 +1093,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.11.14", "version": "20.11.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.14.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
"integrity": "sha512-w3yWCcwULefjP9DmDDsgUskrMoOy5Z8MiwKHr1FvqGPtx7CvJzQvxD7eKpxNtklQxLruxSXWddyeRtyud0RcXQ==", "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
@@ -2052,9 +2057,9 @@
} }
}, },
"node_modules/esbuild-sass-plugin": { "node_modules/esbuild-sass-plugin": {
"version": "2.16.0", "version": "2.16.1",
"resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.16.0.tgz", "resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.16.1.tgz",
"integrity": "sha512-mGCe9MxNYvZ+j77Q/QFO+rwUGA36mojDXkOhtVmoyz1zwYbMaNrtVrmXwwYDleS/UMKTNU3kXuiTtPiAD3K+Pw==", "integrity": "sha512-mBB2aEF0xk7yo+Q9pSUh8xYED/1O2wbAM6IauGkDrqy6pl9SbJNakLeLGXiNpNujWIudu8TJTZCv2L5AQYRXtA==",
"dependencies": { "dependencies": {
"resolve": "^1.22.6", "resolve": "^1.22.6",
"sass": "^1.7.3" "sass": "^1.7.3"
@@ -2301,11 +2306,11 @@
} }
}, },
"node_modules/globby": { "node_modules/globby": {
"version": "14.0.0", "version": "14.0.1",
"resolved": "https://registry.npmjs.org/globby/-/globby-14.0.0.tgz", "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.1.tgz",
"integrity": "sha512-/1WM/LNHRAOH9lZta77uGbq0dAEQM+XjNesWwhlERDVenqothRbnzTrL3/LrIoEPPjeUHC3vrS6TwoyxeHs7MQ==", "integrity": "sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==",
"dependencies": { "dependencies": {
"@sindresorhus/merge-streams": "^1.0.0", "@sindresorhus/merge-streams": "^2.1.0",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"ignore": "^5.2.4", "ignore": "^5.2.4",
"path-type": "^5.0.0", "path-type": "^5.0.0",
@@ -4454,9 +4459,9 @@
} }
}, },
"node_modules/preact": { "node_modules/preact": {
"version": "10.19.3", "version": "10.19.5",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.19.3.tgz", "resolved": "https://registry.npmjs.org/preact/-/preact-10.19.5.tgz",
"integrity": "sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==", "integrity": "sha512-OPELkDmSVbKjbFqF9tgvOowiiQ9TmsJljIzXRyNE8nGiis94pwv1siF78rQkAP1Q1738Ce6pellRg/Ns/CtHqQ==",
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/preact" "url": "https://opencollective.com/preact"
@@ -4708,11 +4713,11 @@
} }
}, },
"node_modules/rehype-pretty-code": { "node_modules/rehype-pretty-code": {
"version": "0.12.6", "version": "0.13.0",
"resolved": "https://registry.npmjs.org/rehype-pretty-code/-/rehype-pretty-code-0.12.6.tgz", "resolved": "https://registry.npmjs.org/rehype-pretty-code/-/rehype-pretty-code-0.13.0.tgz",
"integrity": "sha512-AW18s4eXwnb4PGwL0Y8BoUzBJr23epWNXndCKaZ52S4kl/4tsgM+406oCp5NdtPZsB0ItpaY+hCMv3kw58DLrA==", "integrity": "sha512-+22dz1StXlF7dlMyOySNaVxgcGhMI4BCxq0JxJJPWYGiKsI6cu5jyuIKGHXHvH18D8sv1rdKtvsY9UEfN3++SQ==",
"dependencies": { "dependencies": {
"@types/hast": "^3.0.3", "@types/hast": "^3.0.4",
"hast-util-to-string": "^3.0.0", "hast-util-to-string": "^3.0.0",
"parse-numeric-range": "^1.3.0", "parse-numeric-range": "^1.3.0",
"rehype-parse": "^9.0.0", "rehype-parse": "^9.0.0",
@@ -4723,7 +4728,7 @@
"node": ">=18" "node": ">=18"
}, },
"peerDependencies": { "peerDependencies": {
"shikiji": "^0.7.0 || ^0.8.0 || ^0.9.0 || ^0.10.0" "shiki": "^1.0.0"
} }
}, },
"node_modules/rehype-raw": { "node_modules/rehype-raw": {
@@ -5321,19 +5326,14 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/shikiji": { "node_modules/shiki": {
"version": "0.10.2", "version": "1.1.6",
"resolved": "https://registry.npmjs.org/shikiji/-/shikiji-0.10.2.tgz", "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.1.6.tgz",
"integrity": "sha512-wtZg3T0vtYV2PnqusWQs3mDaJBdCPWxFDrBM/SE5LfrX92gjUvfEMlc+vJnoKY6Z/S44OWaCRzNIsdBRWcTAiw==", "integrity": "sha512-j4pcpvaQWHb42cHeV+W6P+X/VcK7Y2ctvEham6zB8wsuRQroT6cEMIkiUmBU2Nqg2qnHZDH6ZyRdVldcy0l6xw==",
"dependencies": { "dependencies": {
"shikiji-core": "0.10.2" "@shikijs/core": "1.1.6"
} }
}, },
"node_modules/shikiji-core": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/shikiji-core/-/shikiji-core-0.10.2.tgz",
"integrity": "sha512-9Of8HMlF96usXJHmCL3Gd0Fcf0EcyJUF9m8EoAKKd98mHXi0La2AZl1h6PegSFGtiYcBDK/fLuKbDa1l16r1fA=="
},
"node_modules/signal-exit": { "node_modules/signal-exit": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
@@ -5647,9 +5647,9 @@
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
}, },
"node_modules/tsx": { "node_modules/tsx": {
"version": "4.7.0", "version": "4.7.1",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.0.tgz", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.1.tgz",
"integrity": "sha512-I+t79RYPlEYlHn9a+KzwrvEwhJg35h/1zHsLC2JXvhC2mdynMv6Zxzvhv5EMV6VF5qJlLlkSnMVvdZV3PSIGcg==", "integrity": "sha512-8d6VuibXHtlN5E3zFkgY8u4DX7Y3Z27zvvPKVmLon/D4AjuKzarkUBTLDBgj9iTQ0hg5xM7c/mYiRVM+HETf0g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esbuild": "~0.19.10", "esbuild": "~0.19.10",

View File

@@ -2,7 +2,7 @@
"name": "@jackyzha0/quartz", "name": "@jackyzha0/quartz",
"description": "🌱 publish your digital garden and notes as a website", "description": "🌱 publish your digital garden and notes as a website",
"private": true, "private": true,
"version": "4.2.0", "version": "4.2.3",
"type": "module", "type": "module",
"author": "jackyzha0 <j.zhao2k19@gmail.com>", "author": "jackyzha0 <j.zhao2k19@gmail.com>",
"license": "MIT", "license": "MIT",
@@ -15,7 +15,7 @@
"docs": "npx quartz build --serve -d docs", "docs": "npx quartz build --serve -d docs",
"check": "tsc --noEmit && npx prettier . --check", "check": "tsc --noEmit && npx prettier . --check",
"format": "npx prettier . --write", "format": "npx prettier . --write",
"test": "tsx ./quartz/util/path.test.ts", "test": "tsx ./quartz/util/path.test.ts && tsx ./quartz/depgraph.test.ts",
"profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1" "profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1"
}, },
"engines": { "engines": {
@@ -35,17 +35,17 @@
}, },
"dependencies": { "dependencies": {
"@clack/prompts": "^0.7.0", "@clack/prompts": "^0.7.0",
"@floating-ui/dom": "^1.6.1", "@floating-ui/dom": "^1.6.3",
"@napi-rs/simple-git": "0.1.14", "@napi-rs/simple-git": "0.1.16",
"async-mutex": "^0.4.1", "async-mutex": "^0.4.1",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"cli-spinner": "^0.2.10", "cli-spinner": "^0.2.10",
"d3": "^7.8.5", "d3": "^7.8.5",
"esbuild-sass-plugin": "^2.16.0", "esbuild-sass-plugin": "^2.16.1",
"flexsearch": "0.7.43", "flexsearch": "0.7.43",
"github-slugger": "^2.0.0", "github-slugger": "^2.0.0",
"globby": "^14.0.0", "globby": "^14.0.1",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"hast-util-to-html": "^9.0.0", "hast-util-to-html": "^9.0.0",
"hast-util-to-jsx-runtime": "^2.3.0", "hast-util-to-jsx-runtime": "^2.3.0",
@@ -57,7 +57,7 @@
"mdast-util-to-hast": "^13.1.0", "mdast-util-to-hast": "^13.1.0",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"micromorph": "^0.4.5", "micromorph": "^0.4.5",
"preact": "^10.19.3", "preact": "^10.19.5",
"preact-render-to-string": "^6.3.1", "preact-render-to-string": "^6.3.1",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"pretty-time": "^1.1.0", "pretty-time": "^1.1.0",
@@ -65,7 +65,7 @@
"rehype-autolink-headings": "^7.1.0", "rehype-autolink-headings": "^7.1.0",
"rehype-katex": "^7.0.0", "rehype-katex": "^7.0.0",
"rehype-mathjax": "^6.0.0", "rehype-mathjax": "^6.0.0",
"rehype-pretty-code": "^0.12.6", "rehype-pretty-code": "^0.13.0",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"rehype-slug": "^6.0.0", "rehype-slug": "^6.0.0",
"remark": "^15.0.1", "remark": "^15.0.1",
@@ -79,7 +79,7 @@
"rfdc": "^1.3.1", "rfdc": "^1.3.1",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"serve-handler": "^6.1.5", "serve-handler": "^6.1.5",
"shikiji": "^0.10.2", "shiki": "^1.1.6",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"to-vfile": "^8.0.0", "to-vfile": "^8.0.0",
"toml": "^3.0.0", "toml": "^3.0.0",
@@ -95,14 +95,14 @@
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/hast": "^3.0.4", "@types/hast": "^3.0.4",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/node": "^20.11.14", "@types/node": "^20.11.19",
"@types/pretty-time": "^1.1.5", "@types/pretty-time": "^1.1.5",
"@types/source-map-support": "^0.5.10", "@types/source-map-support": "^0.5.10",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
"@types/yargs": "^17.0.32", "@types/yargs": "^17.0.32",
"esbuild": "^0.19.9", "esbuild": "^0.19.9",
"prettier": "^3.2.4", "prettier": "^3.2.4",
"tsx": "^4.7.0", "tsx": "^4.7.1",
"typescript": "^5.3.3" "typescript": "^5.3.3"
} }
} }

View File

@@ -1,6 +1,11 @@
import { QuartzConfig } from "./quartz/cfg" import { QuartzConfig } from "./quartz/cfg"
import * as Plugin from "./quartz/plugins" import * as Plugin from "./quartz/plugins"
/**
* Quartz 4.0 Configuration
*
* See https://quartz.jzhao.xyz/configuration for more information.
*/
const config: QuartzConfig = { const config: QuartzConfig = {
configuration: { configuration: {
pageTitle: "🪴 Quartz 4.0", pageTitle: "🪴 Quartz 4.0",
@@ -9,10 +14,12 @@ const config: QuartzConfig = {
analytics: { analytics: {
provider: "plausible", provider: "plausible",
}, },
locale: "en-US",
baseUrl: "quartz.jzhao.xyz", baseUrl: "quartz.jzhao.xyz",
ignorePatterns: ["private", "templates", ".obsidian"], ignorePatterns: ["private", "templates", ".obsidian"],
defaultDateType: "created", defaultDateType: "created",
theme: { theme: {
cdnCaching: true,
typography: { typography: {
header: "Schibsted Grotesk", header: "Schibsted Grotesk",
body: "Source Sans Pro", body: "Source Sans Pro",
@@ -45,16 +52,20 @@ const config: QuartzConfig = {
plugins: { plugins: {
transformers: [ transformers: [
Plugin.FrontMatter(), Plugin.FrontMatter(),
Plugin.TableOfContents(),
Plugin.CreatedModifiedDate({ Plugin.CreatedModifiedDate({
// you can add 'git' here for last modified from Git
// if you do rely on git for dates, ensure defaultDateType is 'modified'
priority: ["frontmatter", "filesystem"], priority: ["frontmatter", "filesystem"],
}), }),
Plugin.Latex({ renderEngine: "katex" }), Plugin.Latex({ renderEngine: "katex" }),
Plugin.SyntaxHighlighting(), Plugin.SyntaxHighlighting({
theme: {
light: "github-light",
dark: "github-dark",
},
keepBackground: false,
}),
Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }), Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }),
Plugin.GitHubFlavoredMarkdown(), Plugin.GitHubFlavoredMarkdown(),
Plugin.TableOfContents(),
Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }), Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
Plugin.Description(), Plugin.Description(),
], ],

View File

@@ -17,6 +17,10 @@ import { glob, toPosixPath } from "./util/glob"
import { trace } from "./util/trace" import { trace } from "./util/trace"
import { options } from "./util/sourcemap" import { options } from "./util/sourcemap"
import { Mutex } from "async-mutex" import { Mutex } from "async-mutex"
import DepGraph from "./depgraph"
import { getStaticResourcesFromPlugins } from "./plugins"
type Dependencies = Record<string, DepGraph<FilePath> | null>
type BuildData = { type BuildData = {
ctx: BuildCtx ctx: BuildCtx
@@ -29,8 +33,11 @@ type BuildData = {
toRebuild: Set<FilePath> toRebuild: Set<FilePath>
toRemove: Set<FilePath> toRemove: Set<FilePath>
lastBuildMs: number lastBuildMs: number
dependencies: Dependencies
} }
type FileEvent = "add" | "change" | "delete"
async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) { async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
const ctx: BuildCtx = { const ctx: BuildCtx = {
argv, argv,
@@ -53,7 +60,7 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
const release = await mut.acquire() const release = await mut.acquire()
perf.addEvent("clean") perf.addEvent("clean")
await rimraf(output) await rimraf(path.join(output, "*"), { glob: true })
console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`) console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`)
perf.addEvent("glob") perf.addEvent("glob")
@@ -68,12 +75,24 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
const parsedFiles = await parseMarkdown(ctx, filePaths) const parsedFiles = await parseMarkdown(ctx, filePaths)
const filteredContent = filterContent(ctx, parsedFiles) const filteredContent = filterContent(ctx, parsedFiles)
const dependencies: Record<string, DepGraph<FilePath> | null> = {}
// Only build dependency graphs if we're doing a fast rebuild
if (argv.fastRebuild) {
const staticResources = getStaticResourcesFromPlugins(ctx)
for (const emitter of cfg.plugins.emitters) {
dependencies[emitter.name] =
(await emitter.getDependencyGraph?.(ctx, filteredContent, staticResources)) ?? null
}
}
await emitContent(ctx, filteredContent) await emitContent(ctx, filteredContent)
console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`)) console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`))
release() release()
if (argv.serve) { if (argv.serve) {
return startServing(ctx, mut, parsedFiles, clientRefresh) return startServing(ctx, mut, parsedFiles, clientRefresh, dependencies)
} }
} }
@@ -83,9 +102,11 @@ async function startServing(
mut: Mutex, mut: Mutex,
initialContent: ProcessedContent[], initialContent: ProcessedContent[],
clientRefresh: () => void, clientRefresh: () => void,
dependencies: Dependencies, // emitter name: dep graph
) { ) {
const { argv } = ctx const { argv } = ctx
// cache file parse results
const contentMap = new Map<FilePath, ProcessedContent>() const contentMap = new Map<FilePath, ProcessedContent>()
for (const content of initialContent) { for (const content of initialContent) {
const [_tree, vfile] = content const [_tree, vfile] = content
@@ -95,6 +116,7 @@ async function startServing(
const buildData: BuildData = { const buildData: BuildData = {
ctx, ctx,
mut, mut,
dependencies,
contentMap, contentMap,
ignored: await isGitIgnored(), ignored: await isGitIgnored(),
initialSlugs: ctx.allSlugs, initialSlugs: ctx.allSlugs,
@@ -110,19 +132,191 @@ async function startServing(
ignoreInitial: true, ignoreInitial: true,
}) })
const buildFromEntry = argv.fastRebuild ? partialRebuildFromEntrypoint : rebuildFromEntrypoint
watcher watcher
.on("add", (fp) => rebuildFromEntrypoint(fp, "add", clientRefresh, buildData)) .on("add", (fp) => buildFromEntry(fp, "add", clientRefresh, buildData))
.on("change", (fp) => rebuildFromEntrypoint(fp, "change", clientRefresh, buildData)) .on("change", (fp) => buildFromEntry(fp, "change", clientRefresh, buildData))
.on("unlink", (fp) => rebuildFromEntrypoint(fp, "delete", clientRefresh, buildData)) .on("unlink", (fp) => buildFromEntry(fp, "delete", clientRefresh, buildData))
return async () => { return async () => {
await watcher.close() await watcher.close()
} }
} }
async function partialRebuildFromEntrypoint(
filepath: string,
action: FileEvent,
clientRefresh: () => void,
buildData: BuildData, // note: this function mutates buildData
) {
const { ctx, ignored, dependencies, contentMap, mut, toRemove } = buildData
const { argv, cfg } = ctx
// don't do anything for gitignored files
if (ignored(filepath)) {
return
}
const buildStart = new Date().getTime()
buildData.lastBuildMs = buildStart
const release = await mut.acquire()
if (buildData.lastBuildMs > buildStart) {
release()
return
}
const perf = new PerfTimer()
console.log(chalk.yellow("Detected change, rebuilding..."))
// UPDATE DEP GRAPH
const fp = joinSegments(argv.directory, toPosixPath(filepath)) as FilePath
const staticResources = getStaticResourcesFromPlugins(ctx)
let processedFiles: ProcessedContent[] = []
switch (action) {
case "add":
// add to cache when new file is added
processedFiles = await parseMarkdown(ctx, [fp])
processedFiles.forEach(([tree, vfile]) => contentMap.set(vfile.data.filePath!, [tree, vfile]))
// update the dep graph by asking all emitters whether they depend on this file
for (const emitter of cfg.plugins.emitters) {
const emitterGraph =
(await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null
if (emitterGraph) {
const existingGraph = dependencies[emitter.name]
if (existingGraph !== null) {
existingGraph.mergeGraph(emitterGraph)
} else {
// might be the first time we're adding a mardown file
dependencies[emitter.name] = emitterGraph
}
}
}
break
case "change":
// invalidate cache when file is changed
processedFiles = await parseMarkdown(ctx, [fp])
processedFiles.forEach(([tree, vfile]) => contentMap.set(vfile.data.filePath!, [tree, vfile]))
// only content files can have added/removed dependencies because of transclusions
if (path.extname(fp) === ".md") {
for (const emitter of cfg.plugins.emitters) {
// get new dependencies from all emitters for this file
const emitterGraph =
(await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null
// only update the graph if the emitter plugin uses the changed file
// eg. Assets plugin ignores md files, so we skip updating the graph
if (emitterGraph?.hasNode(fp)) {
// merge the new dependencies into the dep graph
dependencies[emitter.name]?.updateIncomingEdgesForNode(emitterGraph, fp)
}
}
}
break
case "delete":
toRemove.add(fp)
break
}
if (argv.verbose) {
console.log(`Updated dependency graphs in ${perf.timeSince()}`)
}
// EMIT
perf.addEvent("rebuild")
let emittedFiles = 0
for (const emitter of cfg.plugins.emitters) {
const depGraph = dependencies[emitter.name]
// emitter hasn't defined a dependency graph. call it with all processed files
if (depGraph === null) {
if (argv.verbose) {
console.log(
`Emitter ${emitter.name} doesn't define a dependency graph. Calling it with all files...`,
)
}
const files = [...contentMap.values()].filter(
([_node, vfile]) => !toRemove.has(vfile.data.filePath!),
)
const emittedFps = await emitter.emit(ctx, files, staticResources)
if (ctx.argv.verbose) {
for (const file of emittedFps) {
console.log(`[emit:${emitter.name}] ${file}`)
}
}
emittedFiles += emittedFps.length
continue
}
// only call the emitter if it uses this file
if (depGraph.hasNode(fp)) {
// re-emit using all files that are needed for the downstream of this file
// eg. for ContentIndex, the dep graph could be:
// a.md --> contentIndex.json
// b.md ------^
//
// if a.md changes, we need to re-emit contentIndex.json,
// and supply [a.md, b.md] to the emitter
const upstreams = [...depGraph.getLeafNodeAncestors(fp)] as FilePath[]
const upstreamContent = upstreams
// filter out non-markdown files
.filter((file) => contentMap.has(file))
// if file was deleted, don't give it to the emitter
.filter((file) => !toRemove.has(file))
.map((file) => contentMap.get(file)!)
const emittedFps = await emitter.emit(ctx, upstreamContent, staticResources)
if (ctx.argv.verbose) {
for (const file of emittedFps) {
console.log(`[emit:${emitter.name}] ${file}`)
}
}
emittedFiles += emittedFps.length
}
}
console.log(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`)
// CLEANUP
const destinationsToDelete = new Set<FilePath>()
for (const file of toRemove) {
// remove from cache
contentMap.delete(file)
Object.values(dependencies).forEach((depGraph) => {
// remove the node from dependency graphs
depGraph?.removeNode(file)
// remove any orphan nodes. eg if a.md is deleted, a.html is orphaned and should be removed
const orphanNodes = depGraph?.removeOrphanNodes()
orphanNodes?.forEach((node) => {
// only delete files that are in the output directory
if (node.startsWith(argv.output)) {
destinationsToDelete.add(node)
}
})
})
}
await rimraf([...destinationsToDelete])
toRemove.clear()
release()
clientRefresh()
}
async function rebuildFromEntrypoint( async function rebuildFromEntrypoint(
fp: string, fp: string,
action: "add" | "change" | "delete", action: FileEvent,
clientRefresh: () => void, clientRefresh: () => void,
buildData: BuildData, // note: this function mutates buildData buildData: BuildData, // note: this function mutates buildData
) { ) {
@@ -190,7 +384,7 @@ async function rebuildFromEntrypoint(
// TODO: we can probably traverse the link graph to figure out what's safe to delete here // TODO: we can probably traverse the link graph to figure out what's safe to delete here
// instead of just deleting everything // instead of just deleting everything
await rimraf(argv.output) await rimraf(path.join(argv.output, ".*"), { glob: true })
await emitContent(ctx, filteredContent) await emitContent(ctx, filteredContent)
console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`)) console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`))
} catch (err) { } catch (err) {

View File

@@ -1,5 +1,6 @@
import { ValidDateType } from "./components/Date" import { ValidDateType } from "./components/Date"
import { QuartzComponent } from "./components/types" import { QuartzComponent } from "./components/types"
import { ValidLocale } from "./i18n"
import { PluginTypes } from "./plugins/types" import { PluginTypes } from "./plugins/types"
import { Theme } from "./util/theme" import { Theme } from "./util/theme"
@@ -37,11 +38,14 @@ export interface GlobalConfiguration {
baseUrl?: string baseUrl?: string
theme: Theme theme: Theme
/** /**
* The locale to use for date formatting. Default to "en-US"
* Allow to translate the date in the language of your choice. * Allow to translate the date in the language of your choice.
* Need to be formated following the IETF language tag format (https://en.wikipedia.org/wiki/IETF_language_tag) * Also used for UI translation (default: en-US)
* Need to be formated following BCP 47: https://en.wikipedia.org/wiki/IETF_language_tag
* The first part is the language (en) and the second part is the script/region (US)
* Language Codes: https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
* Region Codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
*/ */
locale?: string locale: ValidLocale
} }
export interface QuartzConfig { export interface QuartzConfig {

View File

@@ -71,6 +71,11 @@ export const BuildArgv = {
default: false, default: false,
describe: "run a local server to live-preview your Quartz", describe: "run a local server to live-preview your Quartz",
}, },
fastRebuild: {
boolean: true,
default: false,
describe: "[experimental] rebuild only the changed files",
},
baseDir: { baseDir: {
string: true, string: true,
default: "", default: "",

View File

@@ -1,7 +1,7 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { classNames } from "../util/lang" import { classNames } from "../util/lang"
function ArticleTitle({ fileData, displayClass }: QuartzComponentProps) { const ArticleTitle: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => {
const title = fileData.frontmatter?.title const title = fileData.frontmatter?.title
if (title) { if (title) {
return <h1 class={classNames(displayClass, "article-title")}>{title}</h1> return <h1 class={classNames(displayClass, "article-title")}>{title}</h1>
@@ -9,6 +9,7 @@ function ArticleTitle({ fileData, displayClass }: QuartzComponentProps) {
return null return null
} }
} }
ArticleTitle.css = ` ArticleTitle.css = `
.article-title { .article-title {
margin: 2rem 0 0 0; margin: 2rem 0 0 0;

View File

@@ -1,14 +1,20 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import style from "./styles/backlinks.scss" import style from "./styles/backlinks.scss"
import { resolveRelative, simplifySlug } from "../util/path" import { resolveRelative, simplifySlug } from "../util/path"
import { i18n } from "../i18n"
import { classNames } from "../util/lang" import { classNames } from "../util/lang"
function Backlinks({ fileData, allFiles, displayClass }: QuartzComponentProps) { const Backlinks: QuartzComponent = ({
fileData,
allFiles,
displayClass,
cfg,
}: QuartzComponentProps) => {
const slug = simplifySlug(fileData.slug!) const slug = simplifySlug(fileData.slug!)
const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug)) const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
return ( return (
<div class={classNames(displayClass, "backlinks")}> <div class={classNames(displayClass, "backlinks")}>
<h3>Backlinks</h3> <h3>{i18n(cfg.locale).components.backlinks.title}</h3>
<ul class="overflow"> <ul class="overflow">
{backlinkFiles.length > 0 ? ( {backlinkFiles.length > 0 ? (
backlinkFiles.map((f) => ( backlinkFiles.map((f) => (
@@ -19,7 +25,7 @@ function Backlinks({ fileData, allFiles, displayClass }: QuartzComponentProps) {
</li> </li>
)) ))
) : ( ) : (
<li>No backlinks found</li> <li>{i18n(cfg.locale).components.backlinks.noBacklinksFound}</li>
)} )}
</ul> </ul>
</div> </div>

View File

@@ -1,9 +1,9 @@
// @ts-ignore // @ts-ignore
import clipboardScript from "./scripts/clipboard.inline" import clipboardScript from "./scripts/clipboard.inline"
import clipboardStyle from "./styles/clipboard.scss" import clipboardStyle from "./styles/clipboard.scss"
import { 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> return <div id="quartz-body">{children}</div>
} }

View File

@@ -1,6 +1,6 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import breadcrumbsStyle from "./styles/breadcrumbs.scss" import breadcrumbsStyle from "./styles/breadcrumbs.scss"
import { FullSlug, SimpleSlug, resolveRelative } from "../util/path" import { FullSlug, SimpleSlug, joinSegments, resolveRelative } from "../util/path"
import { QuartzPluginData } from "../plugins/vfile" import { QuartzPluginData } from "../plugins/vfile"
import { classNames } from "../util/lang" import { classNames } from "../util/lang"
@@ -54,7 +54,11 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
// computed index of folder name to its associated file data // computed index of folder name to its associated file data
let folderIndex: Map<string, QuartzPluginData> | undefined let folderIndex: Map<string, QuartzPluginData> | undefined
function Breadcrumbs({ fileData, allFiles, displayClass }: QuartzComponentProps) { const Breadcrumbs: QuartzComponent = ({
fileData,
allFiles,
displayClass,
}: QuartzComponentProps) => {
// Hide crumbs on root if enabled // Hide crumbs on root if enabled
if (options.hideOnRoot && fileData.slug === "index") { if (options.hideOnRoot && fileData.slug === "index") {
return <></> return <></>
@@ -68,13 +72,9 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
folderIndex = new Map() folderIndex = new Map()
// construct the index for the first time // construct the index for the first time
for (const file of allFiles) { for (const file of allFiles) {
if (file.slug?.endsWith("index")) { const folderParts = file.slug?.split("/")
const folderParts = file.slug?.split("/") if (folderParts?.at(-1) === "index") {
// 2nd last to exclude the /index folderIndex.set(folderParts.slice(0, -1).join("/"), file)
const folderName = folderParts?.at(-2)
if (folderName) {
folderIndex.set(folderName, file)
}
} }
} }
} }
@@ -82,13 +82,17 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
// Split slug into hierarchy/parts // Split slug into hierarchy/parts
const slugParts = fileData.slug?.split("/") const slugParts = fileData.slug?.split("/")
if (slugParts) { if (slugParts) {
// is tag breadcrumb?
const isTagPath = slugParts[0] === "tags"
// full path until current part // full path until current part
let currentPath = "" let currentPath = ""
for (let i = 0; i < slugParts.length - 1; i++) { for (let i = 0; i < slugParts.length - 1; i++) {
let curPathSegment = slugParts[i] let curPathSegment = slugParts[i]
// Try to resolve frontmatter folder title // Try to resolve frontmatter folder title
const currentFile = folderIndex?.get(curPathSegment) const currentFile = folderIndex?.get(slugParts.slice(0, i + 1).join("/"))
if (currentFile) { if (currentFile) {
const title = currentFile.frontmatter!.title const title = currentFile.frontmatter!.title
if (title !== "index") { if (title !== "index") {
@@ -97,10 +101,15 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
} }
// Add current slug to full path // Add current slug to full path
currentPath += slugParts[i] + "/" currentPath = joinSegments(currentPath, slugParts[i])
const includeTrailingSlash = !isTagPath || i < 1
// Format and add current crumb // 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) crumbs.push(crumb)
} }
@@ -125,5 +134,6 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
) )
} }
Breadcrumbs.css = breadcrumbsStyle Breadcrumbs.css = breadcrumbsStyle
return Breadcrumbs return Breadcrumbs
}) satisfies QuartzComponentConstructor }) satisfies QuartzComponentConstructor

View File

@@ -2,6 +2,7 @@ import { formatDate, getDate } from "./Date"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import readingTime from "reading-time" import readingTime from "reading-time"
import { classNames } from "../util/lang" import { classNames } from "../util/lang"
import { i18n } from "../i18n"
interface ContentMetaOptions { interface ContentMetaOptions {
/** /**
@@ -30,8 +31,11 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
// Display reading time if enabled // Display reading time if enabled
if (options.showReadingTime) { if (options.showReadingTime) {
const { text: timeTaken, words: _words } = readingTime(text) const { minutes, words: _words } = readingTime(text)
segments.push(timeTaken) const displayedTime = i18n(cfg.locale).components.contentMeta.readingTime({
minutes: Math.ceil(minutes),
})
segments.push(displayedTime)
} }
return <p class={classNames(displayClass, "content-meta")}>{segments.join(", ")}</p> return <p class={classNames(displayClass, "content-meta")}>{segments.join(", ")}</p>

View File

@@ -3,10 +3,11 @@
// see: https://v8.dev/features/modules#defer // see: https://v8.dev/features/modules#defer
import darkmodeScript from "./scripts/darkmode.inline" import darkmodeScript from "./scripts/darkmode.inline"
import styles from "./styles/darkmode.scss" import styles from "./styles/darkmode.scss"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { i18n } from "../i18n"
import { classNames } from "../util/lang" import { classNames } from "../util/lang"
function Darkmode({ displayClass }: QuartzComponentProps) { const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
return ( return (
<div class={classNames(displayClass, "darkmode")}> <div class={classNames(displayClass, "darkmode")}>
<input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} /> <input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} />
@@ -22,7 +23,7 @@ function Darkmode({ displayClass }: QuartzComponentProps) {
style="enable-background:new 0 0 35 35" style="enable-background:new 0 0 35 35"
xmlSpace="preserve" xmlSpace="preserve"
> >
<title>Light mode</title> <title>{i18n(cfg.locale).components.themeToggle.darkMode}</title>
<path d="M6,17.5C6,16.672,5.328,16,4.5,16h-3C0.672,16,0,16.672,0,17.5 S0.672,19,1.5,19h3C5.328,19,6,18.328,6,17.5z M7.5,26c-0.414,0-0.789,0.168-1.061,0.439l-2,2C4.168,28.711,4,29.086,4,29.5 C4,30.328,4.671,31,5.5,31c0.414,0,0.789-0.168,1.06-0.44l2-2C8.832,28.289,9,27.914,9,27.5C9,26.672,8.329,26,7.5,26z M17.5,6 C18.329,6,19,5.328,19,4.5v-3C19,0.672,18.329,0,17.5,0S16,0.672,16,1.5v3C16,5.328,16.671,6,17.5,6z M27.5,9 c0.414,0,0.789-0.168,1.06-0.439l2-2C30.832,6.289,31,5.914,31,5.5C31,4.672,30.329,4,29.5,4c-0.414,0-0.789,0.168-1.061,0.44 l-2,2C26.168,6.711,26,7.086,26,7.5C26,8.328,26.671,9,27.5,9z M6.439,8.561C6.711,8.832,7.086,9,7.5,9C8.328,9,9,8.328,9,7.5 c0-0.414-0.168-0.789-0.439-1.061l-2-2C6.289,4.168,5.914,4,5.5,4C4.672,4,4,4.672,4,5.5c0,0.414,0.168,0.789,0.439,1.06 L6.439,8.561z M33.5,16h-3c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5h3c0.828,0,1.5-0.672,1.5-1.5S34.328,16,33.5,16z M28.561,26.439C28.289,26.168,27.914,26,27.5,26c-0.828,0-1.5,0.672-1.5,1.5c0,0.414,0.168,0.789,0.439,1.06l2,2 C28.711,30.832,29.086,31,29.5,31c0.828,0,1.5-0.672,1.5-1.5c0-0.414-0.168-0.789-0.439-1.061L28.561,26.439z M17.5,29 c-0.829,0-1.5,0.672-1.5,1.5v3c0,0.828,0.671,1.5,1.5,1.5s1.5-0.672,1.5-1.5v-3C19,29.672,18.329,29,17.5,29z M17.5,7 C11.71,7,7,11.71,7,17.5S11.71,28,17.5,28S28,23.29,28,17.5S23.29,7,17.5,7z M17.5,25c-4.136,0-7.5-3.364-7.5-7.5 c0-4.136,3.364-7.5,7.5-7.5c4.136,0,7.5,3.364,7.5,7.5C25,21.636,21.636,25,17.5,25z"></path> <path d="M6,17.5C6,16.672,5.328,16,4.5,16h-3C0.672,16,0,16.672,0,17.5 S0.672,19,1.5,19h3C5.328,19,6,18.328,6,17.5z M7.5,26c-0.414,0-0.789,0.168-1.061,0.439l-2,2C4.168,28.711,4,29.086,4,29.5 C4,30.328,4.671,31,5.5,31c0.414,0,0.789-0.168,1.06-0.44l2-2C8.832,28.289,9,27.914,9,27.5C9,26.672,8.329,26,7.5,26z M17.5,6 C18.329,6,19,5.328,19,4.5v-3C19,0.672,18.329,0,17.5,0S16,0.672,16,1.5v3C16,5.328,16.671,6,17.5,6z M27.5,9 c0.414,0,0.789-0.168,1.06-0.439l2-2C30.832,6.289,31,5.914,31,5.5C31,4.672,30.329,4,29.5,4c-0.414,0-0.789,0.168-1.061,0.44 l-2,2C26.168,6.711,26,7.086,26,7.5C26,8.328,26.671,9,27.5,9z M6.439,8.561C6.711,8.832,7.086,9,7.5,9C8.328,9,9,8.328,9,7.5 c0-0.414-0.168-0.789-0.439-1.061l-2-2C6.289,4.168,5.914,4,5.5,4C4.672,4,4,4.672,4,5.5c0,0.414,0.168,0.789,0.439,1.06 L6.439,8.561z M33.5,16h-3c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5h3c0.828,0,1.5-0.672,1.5-1.5S34.328,16,33.5,16z M28.561,26.439C28.289,26.168,27.914,26,27.5,26c-0.828,0-1.5,0.672-1.5,1.5c0,0.414,0.168,0.789,0.439,1.06l2,2 C28.711,30.832,29.086,31,29.5,31c0.828,0,1.5-0.672,1.5-1.5c0-0.414-0.168-0.789-0.439-1.061L28.561,26.439z M17.5,29 c-0.829,0-1.5,0.672-1.5,1.5v3c0,0.828,0.671,1.5,1.5,1.5s1.5-0.672,1.5-1.5v-3C19,29.672,18.329,29,17.5,29z M17.5,7 C11.71,7,7,11.71,7,17.5S11.71,28,17.5,28S28,23.29,28,17.5S23.29,7,17.5,7z M17.5,25c-4.136,0-7.5-3.364-7.5-7.5 c0-4.136,3.364-7.5,7.5-7.5c4.136,0,7.5,3.364,7.5,7.5C25,21.636,21.636,25,17.5,25z"></path>
</svg> </svg>
</label> </label>
@@ -38,7 +39,7 @@ function Darkmode({ displayClass }: QuartzComponentProps) {
style="enable-background:new 0 0 100 100" style="enable-background:new 0 0 100 100"
xmlSpace="preserve" xmlSpace="preserve"
> >
<title>Dark mode</title> <title>{i18n(cfg.locale).components.themeToggle.lightMode}</title>
<path d="M96.76,66.458c-0.853-0.852-2.15-1.064-3.23-0.534c-6.063,2.991-12.858,4.571-19.655,4.571 C62.022,70.495,50.88,65.88,42.5,57.5C29.043,44.043,25.658,23.536,34.076,6.47c0.532-1.08,0.318-2.379-0.534-3.23 c-0.851-0.852-2.15-1.064-3.23-0.534c-4.918,2.427-9.375,5.619-13.246,9.491c-9.447,9.447-14.65,22.008-14.65,35.369 c0,13.36,5.203,25.921,14.65,35.368s22.008,14.65,35.368,14.65c13.361,0,25.921-5.203,35.369-14.65 c3.872-3.871,7.064-8.328,9.491-13.246C97.826,68.608,97.611,67.309,96.76,66.458z"></path> <path d="M96.76,66.458c-0.853-0.852-2.15-1.064-3.23-0.534c-6.063,2.991-12.858,4.571-19.655,4.571 C62.022,70.495,50.88,65.88,42.5,57.5C29.043,44.043,25.658,23.536,34.076,6.47c0.532-1.08,0.318-2.379-0.534-3.23 c-0.851-0.852-2.15-1.064-3.23-0.534c-4.918,2.427-9.375,5.619-13.246,9.491c-9.447,9.447-14.65,22.008-14.65,35.369 c0,13.36,5.203,25.921,14.65,35.368s22.008,14.65,35.368,14.65c13.361,0,25.921-5.203,35.369-14.65 c3.872-3.871,7.064-8.328,9.491-13.246C97.826,68.608,97.611,67.309,96.76,66.458z"></path>
</svg> </svg>
</label> </label>

View File

@@ -1,9 +1,10 @@
import { GlobalConfiguration } from "../cfg" import { GlobalConfiguration } from "../cfg"
import { ValidLocale } from "../i18n"
import { QuartzPluginData } from "../plugins/vfile" import { QuartzPluginData } from "../plugins/vfile"
interface Props { interface Props {
date: Date date: Date
locale?: string locale?: ValidLocale
} }
export type ValidDateType = keyof Required<QuartzPluginData>["dates"] export type ValidDateType = keyof Required<QuartzPluginData>["dates"]
@@ -17,7 +18,7 @@ export function getDate(cfg: GlobalConfiguration, data: QuartzPluginData): Date
return data.dates?.[cfg.defaultDateType] return data.dates?.[cfg.defaultDateType]
} }
export function formatDate(d: Date, locale = "en-US"): string { export function formatDate(d: Date, locale: ValidLocale = "en-US"): string {
return d.toLocaleDateString(locale, { return d.toLocaleDateString(locale, {
year: "numeric", year: "numeric",
month: "short", month: "short",

View File

@@ -3,7 +3,7 @@ import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } fro
export default ((component?: QuartzComponent) => { export default ((component?: QuartzComponent) => {
if (component) { if (component) {
const Component = component const Component = component
function DesktopOnly(props: QuartzComponentProps) { const DesktopOnly: QuartzComponent = (props: QuartzComponentProps) => {
return <Component displayClass="desktop-only" {...props} /> return <Component displayClass="desktop-only" {...props} />
} }

View File

@@ -1,4 +1,4 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import explorerStyle from "./styles/explorer.scss" import explorerStyle from "./styles/explorer.scss"
// @ts-ignore // @ts-ignore
@@ -6,10 +6,10 @@ import script from "./scripts/explorer.inline"
import { ExplorerNode, FileNode, Options } from "./ExplorerNode" import { ExplorerNode, FileNode, Options } from "./ExplorerNode"
import { QuartzPluginData } from "../plugins/vfile" import { QuartzPluginData } from "../plugins/vfile"
import { classNames } from "../util/lang" import { classNames } from "../util/lang"
import { i18n } from "../i18n"
// Options interface defined in `ExplorerNode` to avoid circular dependency // Options interface defined in `ExplorerNode` to avoid circular dependency
const defaultOptions = { const defaultOptions = {
title: "Explorer",
folderClickBehavior: "collapse", folderClickBehavior: "collapse",
folderDefaultState: "collapsed", folderDefaultState: "collapsed",
useSavedState: true, useSavedState: true,
@@ -75,7 +75,12 @@ export default ((userOpts?: Partial<Options>) => {
jsonTree = JSON.stringify(folders) jsonTree = JSON.stringify(folders)
} }
function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) { const Explorer: QuartzComponent = ({
cfg,
allFiles,
displayClass,
fileData,
}: QuartzComponentProps) => {
constructFileTree(allFiles) constructFileTree(allFiles)
return ( return (
<div class={classNames(displayClass, "explorer")}> <div class={classNames(displayClass, "explorer")}>
@@ -87,7 +92,7 @@ export default ((userOpts?: Partial<Options>) => {
data-savestate={opts.useSavedState} data-savestate={opts.useSavedState}
data-tree={jsonTree} data-tree={jsonTree}
> >
<h1>{opts.title}</h1> <h1>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h1>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="14" width="14"

View File

@@ -12,7 +12,7 @@ import {
type OrderEntries = "sort" | "filter" | "map" type OrderEntries = "sort" | "filter" | "map"
export interface Options { export interface Options {
title: string title?: string
folderDefaultState: "collapsed" | "open" folderDefaultState: "collapsed" | "open"
folderClickBehavior: "collapse" | "link" folderClickBehavior: "collapse" | "link"
useSavedState: boolean useSavedState: boolean

View File

@@ -1,20 +1,22 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import style from "./styles/footer.scss" import style from "./styles/footer.scss"
import { version } from "../../package.json" import { version } from "../../package.json"
import { i18n } from "../i18n"
interface Options { interface Options {
links: Record<string, string> links: Record<string, string>
} }
export default ((opts?: Options) => { export default ((opts?: Options) => {
function Footer({ displayClass }: QuartzComponentProps) { const Footer: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
const year = new Date().getFullYear() const year = new Date().getFullYear()
const links = opts?.links ?? [] const links = opts?.links ?? []
return ( return (
<footer class={`${displayClass ?? ""}`}> <footer class={`${displayClass ?? ""}`}>
<hr /> <hr />
<p> <p>
Created with <a href="https://quartz.jzhao.xyz/">Quartz v{version}</a>, © {year} {i18n(cfg.locale).components.footer.createdWith}{" "}
<a href="https://quartz.jzhao.xyz/">Quartz v{version}</a> © {year}
</p> </p>
<ul> <ul>
{Object.entries(links).map(([text, link]) => ( {Object.entries(links).map(([text, link]) => (

View File

@@ -1,7 +1,8 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
// @ts-ignore // @ts-ignore
import script from "./scripts/graph.inline" import script from "./scripts/graph.inline"
import style from "./styles/graph.scss" import style from "./styles/graph.scss"
import { i18n } from "../i18n"
import { classNames } from "../util/lang" import { classNames } from "../util/lang"
export interface D3Config { export interface D3Config {
@@ -53,12 +54,12 @@ const defaultOptions: GraphOptions = {
} }
export default ((opts?: GraphOptions) => { export default ((opts?: GraphOptions) => {
function Graph({ displayClass }: QuartzComponentProps) { const Graph: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph } const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph }
const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph } const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph }
return ( return (
<div class={classNames(displayClass, "graph")}> <div class={classNames(displayClass, "graph")}>
<h3>Graph View</h3> <h3>{i18n(cfg.locale).components.graph.title}</h3>
<div class="graph-outer"> <div class="graph-outer">
<div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div> <div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
<svg <svg

View File

@@ -1,11 +1,13 @@
import { FullSlug, _stripSlashes, joinSegments, pathToRoot } from "../util/path" import { i18n } from "../i18n"
import { FullSlug, joinSegments, pathToRoot } from "../util/path"
import { JSResourceToScriptElement } from "../util/resources" import { JSResourceToScriptElement } from "../util/resources"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
export default (() => { export default (() => {
function Head({ cfg, fileData, externalResources }: QuartzComponentProps) { const Head: QuartzComponent = ({ cfg, fileData, externalResources }: QuartzComponentProps) => {
const title = fileData.frontmatter?.title ?? "Untitled" const title = fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title
const description = fileData.description?.trim() ?? "No description provided" const description =
fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description
const { css, js } = externalResources const { css, js } = externalResources
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`) const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
@@ -19,6 +21,12 @@ export default (() => {
<head> <head>
<title>{title}</title> <title>{title}</title>
<meta charSet="utf-8" /> <meta charSet="utf-8" />
{cfg.theme.cdnCaching && (
<>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
</>
)}
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta property="og:title" content={title} /> <meta property="og:title" content={title} />
<meta property="og:description" content={description} /> <meta property="og:description" content={description} />
@@ -28,8 +36,6 @@ export default (() => {
<link rel="icon" href={iconPath} /> <link rel="icon" href={iconPath} />
<meta name="description" content={description} /> <meta name="description" content={description} />
<meta name="generator" content="Quartz" /> <meta name="generator" content="Quartz" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
{css.map((href) => ( {css.map((href) => (
<link key={href} href={href} rel="stylesheet" type="text/css" spa-preserve /> <link key={href} href={href} rel="stylesheet" type="text/css" spa-preserve />
))} ))}

View File

@@ -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 return children.length > 0 ? <header>{children}</header> : null
} }

View File

@@ -3,7 +3,7 @@ import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } fro
export default ((component?: QuartzComponent) => { export default ((component?: QuartzComponent) => {
if (component) { if (component) {
const Component = component const Component = component
function MobileOnly(props: QuartzComponentProps) { const MobileOnly: QuartzComponent = (props: QuartzComponentProps) => {
return <Component displayClass="mobile-only" {...props} /> return <Component displayClass="mobile-only" {...props} />
} }

View File

@@ -1,7 +1,7 @@
import { FullSlug, resolveRelative } from "../util/path" import { FullSlug, resolveRelative } from "../util/path"
import { QuartzPluginData } from "../plugins/vfile" import { QuartzPluginData } from "../plugins/vfile"
import { Date, getDate } from "./Date" import { Date, getDate } from "./Date"
import { QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentProps } from "./types"
import { GlobalConfiguration } from "../cfg" import { GlobalConfiguration } from "../cfg"
export function byDateAndAlphabetical( export function byDateAndAlphabetical(
@@ -29,7 +29,7 @@ type Props = {
limit?: number limit?: number
} & QuartzComponentProps } & QuartzComponentProps
export function PageList({ cfg, fileData, allFiles, limit }: Props) { export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit }: Props) => {
let list = allFiles.sort(byDateAndAlphabetical(cfg)) let list = allFiles.sort(byDateAndAlphabetical(cfg))
if (limit) { if (limit) {
list = list.slice(0, limit) list = list.slice(0, limit)

View File

@@ -1,9 +1,10 @@
import { pathToRoot } from "../util/path" import { pathToRoot } from "../util/path"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { classNames } from "../util/lang" 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 ?? "Untitled Quartz" const title = cfg?.pageTitle ?? i18n(cfg.locale).propertyDefaults.title
const baseDir = pathToRoot(fileData.slug!) const baseDir = pathToRoot(fileData.slug!)
return ( return (
<h1 class={classNames(displayClass, "page-title")}> <h1 class={classNames(displayClass, "page-title")}>

View File

@@ -1,14 +1,15 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { FullSlug, SimpleSlug, resolveRelative } from "../util/path" import { FullSlug, SimpleSlug, resolveRelative } from "../util/path"
import { QuartzPluginData } from "../plugins/vfile" import { QuartzPluginData } from "../plugins/vfile"
import { byDateAndAlphabetical } from "./PageList" import { byDateAndAlphabetical } from "./PageList"
import style from "./styles/recentNotes.scss" import style from "./styles/recentNotes.scss"
import { Date, getDate } from "./Date" import { Date, getDate } from "./Date"
import { GlobalConfiguration } from "../cfg" import { GlobalConfiguration } from "../cfg"
import { i18n } from "../i18n"
import { classNames } from "../util/lang" import { classNames } from "../util/lang"
interface Options { interface Options {
title: string title?: string
limit: number limit: number
linkToMore: SimpleSlug | false linkToMore: SimpleSlug | false
filter: (f: QuartzPluginData) => boolean filter: (f: QuartzPluginData) => boolean
@@ -16,7 +17,6 @@ interface Options {
} }
const defaultOptions = (cfg: GlobalConfiguration): Options => ({ const defaultOptions = (cfg: GlobalConfiguration): Options => ({
title: "Recent Notes",
limit: 3, limit: 3,
linkToMore: false, linkToMore: false,
filter: () => true, filter: () => true,
@@ -24,16 +24,21 @@ const defaultOptions = (cfg: GlobalConfiguration): Options => ({
}) })
export default ((userOpts?: Partial<Options>) => { 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 opts = { ...defaultOptions(cfg), ...userOpts }
const pages = allFiles.filter(opts.filter).sort(opts.sort) const pages = allFiles.filter(opts.filter).sort(opts.sort)
const remaining = Math.max(0, pages.length - opts.limit) const remaining = Math.max(0, pages.length - opts.limit)
return ( return (
<div class={classNames(displayClass, "recent-notes")}> <div class={classNames(displayClass, "recent-notes")}>
<h3>{opts.title}</h3> <h3>{opts.title ?? i18n(cfg.locale).components.recentNotes.title}</h3>
<ul class="recent-ul"> <ul class="recent-ul">
{pages.slice(0, opts.limit).map((page) => { {pages.slice(0, opts.limit).map((page) => {
const title = page.frontmatter?.title const title = page.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title
const tags = page.frontmatter?.tags ?? [] const tags = page.frontmatter?.tags ?? []
return ( return (
@@ -70,7 +75,9 @@ export default ((userOpts?: Partial<Options>) => {
</ul> </ul>
{opts.linkToMore && remaining > 0 && ( {opts.linkToMore && remaining > 0 && (
<p> <p>
<a href={resolveRelative(fileData.slug!, opts.linkToMore)}>See {remaining} more </a> <a href={resolveRelative(fileData.slug!, opts.linkToMore)}>
{i18n(cfg.locale).components.recentNotes.seeRemainingMore({ remaining })}
</a>
</p> </p>
)} )}
</div> </div>

View File

@@ -1,8 +1,9 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import style from "./styles/search.scss" import style from "./styles/search.scss"
// @ts-ignore // @ts-ignore
import script from "./scripts/search.inline" import script from "./scripts/search.inline"
import { classNames } from "../util/lang" import { classNames } from "../util/lang"
import { i18n } from "../i18n"
export interface SearchOptions { export interface SearchOptions {
enablePreview: boolean enablePreview: boolean
@@ -13,13 +14,13 @@ const defaultOptions: SearchOptions = {
} }
export default ((userOpts?: Partial<SearchOptions>) => { export default ((userOpts?: Partial<SearchOptions>) => {
function Search({ displayClass }: QuartzComponentProps) { const Search: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder
return ( return (
<div class={classNames(displayClass, "search")}> <div class={classNames(displayClass, "search")}>
<div id="search-icon"> <div id="search-icon">
<p>Search</p> <p>{i18n(cfg.locale).components.search.title}</p>
<div></div> <div></div>
<svg <svg
tabIndex={0} tabIndex={0}
@@ -43,8 +44,8 @@ export default ((userOpts?: Partial<SearchOptions>) => {
id="search-bar" id="search-bar"
name="search" name="search"
type="text" type="text"
aria-label="Search for something" aria-label={searchPlaceholder}
placeholder="Search for something" placeholder={searchPlaceholder}
/> />
<div id="search-layout" data-preview={opts.enablePreview}></div> <div id="search-layout" data-preview={opts.enablePreview}></div>
</div> </div>

View File

@@ -1,10 +1,11 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import legacyStyle from "./styles/legacyToc.scss" import legacyStyle from "./styles/legacyToc.scss"
import modernStyle from "./styles/toc.scss" import modernStyle from "./styles/toc.scss"
import { classNames } from "../util/lang" import { classNames } from "../util/lang"
// @ts-ignore // @ts-ignore
import script from "./scripts/toc.inline" import script from "./scripts/toc.inline"
import { i18n } from "../i18n"
interface Options { interface Options {
layout: "modern" | "legacy" layout: "modern" | "legacy"
@@ -14,7 +15,11 @@ const defaultOptions: Options = {
layout: "modern", layout: "modern",
} }
function TableOfContents({ fileData, displayClass }: QuartzComponentProps) { const TableOfContents: QuartzComponent = ({
fileData,
displayClass,
cfg,
}: QuartzComponentProps) => {
if (!fileData.toc) { if (!fileData.toc) {
return null return null
} }
@@ -22,7 +27,7 @@ function TableOfContents({ fileData, displayClass }: QuartzComponentProps) {
return ( return (
<div class={classNames(displayClass, "toc")}> <div class={classNames(displayClass, "toc")}>
<button type="button" id="toc" class={fileData.collapseToc ? "collapsed" : ""}> <button type="button" id="toc" class={fileData.collapseToc ? "collapsed" : ""}>
<h3>Table of Contents</h3> <h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="24" width="24"
@@ -55,15 +60,14 @@ function TableOfContents({ fileData, displayClass }: QuartzComponentProps) {
TableOfContents.css = modernStyle TableOfContents.css = modernStyle
TableOfContents.afterDOMLoaded = script TableOfContents.afterDOMLoaded = script
function LegacyTableOfContents({ fileData }: QuartzComponentProps) { const LegacyTableOfContents: QuartzComponent = ({ fileData, cfg }: QuartzComponentProps) => {
if (!fileData.toc) { if (!fileData.toc) {
return null return null
} }
return ( return (
<details id="toc" open={!fileData.collapseToc}> <details id="toc" open={!fileData.collapseToc}>
<summary> <summary>
<h3>Table of Contents</h3> <h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
</summary> </summary>
<ul> <ul>
{fileData.toc.map((tocEntry) => ( {fileData.toc.map((tocEntry) => (

View File

@@ -1,8 +1,8 @@
import { pathToRoot, slugTag } from "../util/path" import { pathToRoot, slugTag } from "../util/path"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { classNames } from "../util/lang" import { classNames } from "../util/lang"
function TagList({ fileData, displayClass }: QuartzComponentProps) { const TagList: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => {
const tags = fileData.frontmatter?.tags const tags = fileData.frontmatter?.tags
const baseDir = pathToRoot(fileData.slug!) const baseDir = pathToRoot(fileData.slug!)
if (tags && tags.length > 0) { if (tags && tags.length > 0) {

View File

@@ -1,10 +1,11 @@
import { QuartzComponentConstructor } from "../types" import { i18n } from "../../i18n"
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
function NotFound() { function NotFound({ cfg }: QuartzComponentProps) {
return ( return (
<article class="popover-hint"> <article class="popover-hint">
<h1>404</h1> <h1>404</h1>
<p>Either this page is private or doesn't exist.</p> <p>{i18n(cfg.locale).pages.error.notFound}</p>
</article> </article>
) )
} }

View File

@@ -3,10 +3,10 @@ import path from "path"
import style from "../styles/listPage.scss" import style from "../styles/listPage.scss"
import { PageList } from "../PageList" import { PageList } from "../PageList"
import { _stripSlashes, simplifySlug } from "../../util/path" import { stripSlashes, simplifySlug } from "../../util/path"
import { Root } from "hast" import { Root } from "hast"
import { pluralize } from "../../util/lang"
import { htmlToJsx } from "../../util/jsx" import { htmlToJsx } from "../../util/jsx"
import { i18n } from "../../i18n"
interface FolderContentOptions { interface FolderContentOptions {
/** /**
@@ -23,10 +23,10 @@ export default ((opts?: Partial<FolderContentOptions>) => {
const options: FolderContentOptions = { ...defaultOptions, ...opts } const options: FolderContentOptions = { ...defaultOptions, ...opts }
function FolderContent(props: QuartzComponentProps) { function FolderContent(props: QuartzComponentProps) {
const { tree, fileData, allFiles } = props const { tree, fileData, allFiles, cfg } = props
const folderSlug = _stripSlashes(simplifySlug(fileData.slug!)) const folderSlug = stripSlashes(simplifySlug(fileData.slug!))
const allPagesInFolder = allFiles.filter((file) => { const allPagesInFolder = allFiles.filter((file) => {
const fileSlug = _stripSlashes(simplifySlug(file.slug!)) const fileSlug = stripSlashes(simplifySlug(file.slug!))
const prefixed = fileSlug.startsWith(folderSlug) && fileSlug !== folderSlug const prefixed = fileSlug.startsWith(folderSlug) && fileSlug !== folderSlug
const folderParts = folderSlug.split(path.posix.sep) const folderParts = folderSlug.split(path.posix.sep)
const fileParts = fileSlug.split(path.posix.sep) const fileParts = fileSlug.split(path.posix.sep)
@@ -52,7 +52,11 @@ export default ((opts?: Partial<FolderContentOptions>) => {
</article> </article>
<div class="page-listing"> <div class="page-listing">
{options.showFolderCount && ( {options.showFolderCount && (
<p>{pluralize(allPagesInFolder.length, "item")} under this folder.</p> <p>
{i18n(cfg.locale).pages.folderContent.itemsUnderFolder({
count: allPagesInFolder.length,
})}
</p>
)} )}
<div> <div>
<PageList {...listProps} /> <PageList {...listProps} />

View File

@@ -4,12 +4,12 @@ import { PageList } from "../PageList"
import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path" import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path"
import { QuartzPluginData } from "../../plugins/vfile" import { QuartzPluginData } from "../../plugins/vfile"
import { Root } from "hast" import { Root } from "hast"
import { pluralize } from "../../util/lang"
import { htmlToJsx } from "../../util/jsx" import { htmlToJsx } from "../../util/jsx"
import { i18n } from "../../i18n"
const numPages = 10 const numPages = 10
function TagContent(props: QuartzComponentProps) { function TagContent(props: QuartzComponentProps) {
const { tree, fileData, allFiles } = props const { tree, fileData, allFiles, cfg } = props
const slug = fileData.slug const slug = fileData.slug
if (!(slug?.startsWith("tags/") || slug === "tags")) { if (!(slug?.startsWith("tags/") || slug === "tags")) {
@@ -43,7 +43,7 @@ function TagContent(props: QuartzComponentProps) {
<article> <article>
<p>{content}</p> <p>{content}</p>
</article> </article>
<p>Found {tags.length} total tags.</p> <p>{i18n(cfg.locale).pages.tagContent.totalTags({ count: tags.length })}</p>
<div> <div>
{tags.map((tag) => { {tags.map((tag) => {
const pages = tagItemMap.get(tag)! const pages = tagItemMap.get(tag)!
@@ -64,8 +64,15 @@ function TagContent(props: QuartzComponentProps) {
{content && <p>{content}</p>} {content && <p>{content}</p>}
<div class="page-listing"> <div class="page-listing">
<p> <p>
{pluralize(pages.length, "item")} with this tag.{" "} {i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}
{pages.length > numPages && `Showing first ${numPages}.`} {pages.length > numPages && (
<>
{" "}
<span>
{i18n(cfg.locale).pages.tagContent.showingFirst({ count: numPages })}
</span>
</>
)}
</p> </p>
<PageList limit={numPages} {...listProps} /> <PageList limit={numPages} {...listProps} />
</div> </div>
@@ -86,7 +93,7 @@ function TagContent(props: QuartzComponentProps) {
<div class={classes}> <div class={classes}>
<article>{content}</article> <article>{content}</article>
<div class="page-listing"> <div class="page-listing">
<p>{pluralize(pages.length, "item")} with this tag.</p> <p>{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}</p>
<div> <div>
<PageList {...listProps} /> <PageList {...listProps} />
</div> </div>

View File

@@ -3,10 +3,11 @@ import { QuartzComponent, QuartzComponentProps } from "./types"
import HeaderConstructor from "./Header" import HeaderConstructor from "./Header"
import BodyConstructor from "./Body" import BodyConstructor from "./Body"
import { JSResourceToScriptElement, StaticResources } from "../util/resources" import { JSResourceToScriptElement, StaticResources } from "../util/resources"
import { FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path" import { clone, FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path"
import { visit } from "unist-util-visit" import { visit } from "unist-util-visit"
import { Root, Element, ElementContent } from "hast" import { Root, Element, ElementContent } from "hast"
import { QuartzPluginData } from "../plugins/vfile" import { GlobalConfiguration } from "../cfg"
import { i18n } from "../i18n"
interface RenderComponents { interface RenderComponents {
head: QuartzComponent head: QuartzComponent
@@ -50,32 +51,25 @@ export function pageResources(
} }
} }
let pageIndex: Map<FullSlug, QuartzPluginData> | undefined = undefined
function getOrComputeFileIndex(allFiles: QuartzPluginData[]): Map<FullSlug, QuartzPluginData> {
if (!pageIndex) {
pageIndex = new Map()
for (const file of allFiles) {
pageIndex.set(file.slug!, file)
}
}
return pageIndex
}
export function renderPage( export function renderPage(
cfg: GlobalConfiguration,
slug: FullSlug, slug: FullSlug,
componentData: QuartzComponentProps, componentData: QuartzComponentProps,
components: RenderComponents, components: RenderComponents,
pageResources: StaticResources, pageResources: StaticResources,
): string { ): string {
// make a deep copy of the tree so we don't remove the transclusion references
// for the file cached in contentMap in build.ts
const root = clone(componentData.tree) as Root
// process transcludes in componentData // process transcludes in componentData
visit(componentData.tree as Root, "element", (node, _index, _parent) => { visit(root, "element", (node, _index, _parent) => {
if (node.tagName === "blockquote") { if (node.tagName === "blockquote") {
const classNames = (node.properties?.className ?? []) as string[] const classNames = (node.properties?.className ?? []) as string[]
if (classNames.includes("transclude")) { if (classNames.includes("transclude")) {
const inner = node.children[0] as Element const inner = node.children[0] as Element
const transcludeTarget = inner.properties["data-slug"] as FullSlug const transcludeTarget = inner.properties["data-slug"] as FullSlug
const page = getOrComputeFileIndex(componentData.allFiles).get(transcludeTarget) const page = componentData.allFiles.find((f) => f.slug === transcludeTarget)
if (!page) { if (!page) {
return return
} }
@@ -100,8 +94,10 @@ export function renderPage(
{ {
type: "element", type: "element",
tagName: "a", tagName: "a",
properties: { href: inner.properties?.href, class: ["internal"] }, properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] },
children: [{ type: "text", value: `Link to original` }], children: [
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
],
}, },
] ]
} }
@@ -135,8 +131,10 @@ export function renderPage(
{ {
type: "element", type: "element",
tagName: "a", tagName: "a",
properties: { href: inner.properties?.href, class: ["internal"] }, properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] },
children: [{ type: "text", value: `Link to original` }], children: [
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
],
}, },
] ]
} else if (page.htmlAst) { } else if (page.htmlAst) {
@@ -147,7 +145,14 @@ export function renderPage(
tagName: "h1", tagName: "h1",
properties: {}, properties: {},
children: [ children: [
{ type: "text", value: page.frontmatter?.title ?? `Transclude of ${page.slug}` }, {
type: "text",
value:
page.frontmatter?.title ??
i18n(cfg.locale).components.transcludes.transcludeOf({
targetSlug: page.slug!,
}),
},
], ],
}, },
...(page.htmlAst.children as ElementContent[]).map((child) => ...(page.htmlAst.children as ElementContent[]).map((child) =>
@@ -156,8 +161,10 @@ export function renderPage(
{ {
type: "element", type: "element",
tagName: "a", tagName: "a",
properties: { href: inner.properties?.href, class: ["internal"] }, properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] },
children: [{ type: "text", value: `Link to original` }], children: [
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
],
}, },
] ]
} }
@@ -165,6 +172,9 @@ export function renderPage(
} }
}) })
// set componentData.tree to the edited html that has transclusions rendered
componentData.tree = root
const { const {
head: Head, head: Head,
header, header,
@@ -193,8 +203,9 @@ export function renderPage(
</div> </div>
) )
const lang = componentData.frontmatter?.lang ?? cfg.locale?.split("-")[0] ?? "en"
const doc = ( const doc = (
<html> <html lang={lang}>
<Head {...componentData} /> <Head {...componentData} />
<body data-slug={slug}> <body data-slug={slug}>
<div id="quartz-root" class="page"> <div id="quartz-root" class="page">

View 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
}
})
})

View File

@@ -37,29 +37,55 @@ async function mouseEnterHandler(
targetUrl.hash = "" targetUrl.hash = ""
targetUrl.search = "" targetUrl.search = ""
const contents = await fetch(`${targetUrl}`) const response = await fetch(`${targetUrl}`).catch((err) => {
.then((res) => res.text()) console.error(err)
.catch((err) => { })
console.error(err)
})
// bailout if another popover exists // bailout if another popover exists
if (hasAlreadyBeenFetched()) { if (hasAlreadyBeenFetched()) {
return return
} }
if (!contents) return if (!response) return
const html = p.parseFromString(contents, "text/html") const [contentType] = response.headers.get("Content-Type")!.split(";")
normalizeRelativeURLs(html, targetUrl) const [contentTypeCategory, typeInfo] = contentType.split("/")
const elts = [...html.getElementsByClassName("popover-hint")]
if (elts.length === 0) return
const popoverElement = document.createElement("div") const popoverElement = document.createElement("div")
popoverElement.classList.add("popover") popoverElement.classList.add("popover")
const popoverInner = document.createElement("div") const popoverInner = document.createElement("div")
popoverInner.classList.add("popover-inner") popoverInner.classList.add("popover-inner")
popoverElement.appendChild(popoverInner) popoverElement.appendChild(popoverInner)
elts.forEach((elt) => popoverInner.appendChild(elt))
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) setPosition(popoverElement)
link.appendChild(popoverElement) link.appendChild(popoverElement)

View File

@@ -15,10 +15,30 @@ interface Item {
type SearchType = "basic" | "tags" type SearchType = "basic" | "tags"
let searchType: SearchType = "basic" let searchType: SearchType = "basic"
let currentSearchTerm: string = "" let currentSearchTerm: string = ""
let index: FlexSearch.Document<Item> | undefined = undefined
const p = new DOMParser()
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/) const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
let index = new FlexSearch.Document<Item>({
charset: "latin:extra",
encode: encoder,
document: {
id: "id",
index: [
{
field: "title",
tokenize: "forward",
},
{
field: "content",
tokenize: "forward",
},
{
field: "tags",
tokenize: "forward",
},
],
},
})
const p = new DOMParser()
const fetchContentCache: Map<FullSlug, Element[]> = new Map() const fetchContentCache: Map<FullSlug, Element[]> = new Map()
const contextWindowWords = 30 const contextWindowWords = 30
const numSearchResults = 8 const numSearchResults = 8
@@ -81,10 +101,10 @@ function highlight(searchTerm: string, text: string, trim?: boolean) {
}` }`
} }
function highlightHTML(searchTerm: string, innerHTML: string) { function highlightHTML(searchTerm: string, el: HTMLElement) {
const p = new DOMParser() const p = new DOMParser()
const tokenizedTerms = tokenizeTerm(searchTerm) const tokenizedTerms = tokenizeTerm(searchTerm)
const html = p.parseFromString(innerHTML, "text/html") const html = p.parseFromString(el.innerHTML, "text/html")
const createHighlightSpan = (text: string) => { const createHighlightSpan = (text: string) => {
const span = document.createElement("span") const span = document.createElement("span")
@@ -143,13 +163,11 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
let previewInner: HTMLDivElement | undefined = undefined let previewInner: HTMLDivElement | undefined = undefined
const results = document.createElement("div") const results = document.createElement("div")
results.id = "results-container" results.id = "results-container"
results.style.flexBasis = enablePreview ? "min(30%, 450px)" : "100%"
appendLayout(results) appendLayout(results)
if (enablePreview) { if (enablePreview) {
preview = document.createElement("div") preview = document.createElement("div")
preview.id = "preview-container" preview.id = "preview-container"
preview.style.flexBasis = "100%"
appendLayout(preview) appendLayout(preview)
} }
@@ -168,7 +186,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
removeAllChildren(preview) removeAllChildren(preview)
} }
if (searchLayout) { if (searchLayout) {
searchLayout.style.visibility = "hidden" searchLayout.classList.remove("display-results")
} }
searchType = "basic" // reset search type after closing searchType = "basic" // reset search type after closing
@@ -208,7 +226,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
// If search is active, then we will render the first result and display accordingly // If search is active, then we will render the first result and display accordingly
if (!container?.classList.contains("active")) return if (!container?.classList.contains("active")) return
else if (e.key === "Enter") { if (e.key === "Enter") {
// If result has focus, navigate to that one, otherwise pick first result // If result has focus, navigate to that one, otherwise pick first result
if (results?.contains(document.activeElement)) { if (results?.contains(document.activeElement)) {
const active = document.activeElement as HTMLInputElement const active = document.activeElement as HTMLInputElement
@@ -230,9 +248,9 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
: (document.activeElement as HTMLInputElement | null) : (document.activeElement as HTMLInputElement | null)
const prevResult = currentResult?.previousElementSibling as HTMLInputElement | null const prevResult = currentResult?.previousElementSibling as HTMLInputElement | null
currentResult?.classList.remove("focus") currentResult?.classList.remove("focus")
await displayPreview(prevResult)
prevResult?.focus() prevResult?.focus()
currentHover = prevResult if (prevResult) currentHover = prevResult
await displayPreview(prevResult)
} }
} else if (e.key === "ArrowDown" || e.key === "Tab") { } else if (e.key === "ArrowDown" || e.key === "Tab") {
e.preventDefault() e.preventDefault()
@@ -244,19 +262,9 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
: (document.getElementsByClassName("result-card")[0] as HTMLInputElement | null) : (document.getElementsByClassName("result-card")[0] as HTMLInputElement | null)
const secondResult = firstResult?.nextElementSibling as HTMLInputElement | null const secondResult = firstResult?.nextElementSibling as HTMLInputElement | null
firstResult?.classList.remove("focus") firstResult?.classList.remove("focus")
await displayPreview(secondResult)
secondResult?.focus() secondResult?.focus()
currentHover = secondResult if (secondResult) currentHover = secondResult
} else { await displayPreview(secondResult)
// If an element in results-container already has focus, focus next one
const active = currentHover
? currentHover
: (document.activeElement as HTMLInputElement | null)
active?.classList.remove("focus")
const nextResult = active?.nextElementSibling as HTMLInputElement | null
await displayPreview(nextResult)
nextResult?.focus()
currentHover = nextResult
} }
} }
} }
@@ -298,40 +306,29 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
itemTile.classList.add("result-card") itemTile.classList.add("result-card")
itemTile.id = slug itemTile.id = slug
itemTile.href = resolveUrl(slug).toString() itemTile.href = resolveUrl(slug).toString()
itemTile.innerHTML = `<h3>${title}</h3>${htmlTags}<p class="preview">${content}</p>` itemTile.innerHTML = `<h3>${title}</h3>${htmlTags}${
enablePreview && window.innerWidth > 600 ? "" : `<p>${content}</p>`
}`
itemTile.addEventListener("click", (event) => {
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
hideSearch()
})
const handler = (event: MouseEvent) => {
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
hideSearch()
}
async function onMouseEnter(ev: MouseEvent) { async function onMouseEnter(ev: MouseEvent) {
if (!ev.target) return if (!ev.target) return
currentHover?.classList.remove("focus")
currentHover?.blur()
const target = ev.target as HTMLInputElement const target = ev.target as HTMLInputElement
await displayPreview(target) await displayPreview(target)
currentHover = target
currentHover.classList.add("focus")
} }
async function onMouseLeave(ev: MouseEvent) { itemTile.addEventListener("mouseenter", onMouseEnter)
if (!ev.target) return window.addCleanup(() => itemTile.removeEventListener("mouseenter", onMouseEnter))
const target = ev.target as HTMLElement itemTile.addEventListener("click", handler)
target.classList.remove("focus") window.addCleanup(() => itemTile.removeEventListener("click", handler))
}
const events = [
["mouseenter", onMouseEnter],
["mouseleave", onMouseLeave],
[
"click",
(event: MouseEvent) => {
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
hideSearch()
},
],
] as const
events.forEach(([event, handler]) => {
itemTile.addEventListener(event, handler)
window.addCleanup(() => itemTile.removeEventListener(event, handler))
})
return itemTile return itemTile
} }
@@ -385,12 +382,11 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
async function displayPreview(el: HTMLElement | null) { async function displayPreview(el: HTMLElement | null) {
if (!searchLayout || !enablePreview || !el || !preview) return if (!searchLayout || !enablePreview || !el || !preview) return
const slug = el.id as FullSlug const slug = el.id as FullSlug
el.classList.add("focus") const innerDiv = await fetchContent(slug).then((contents) =>
contents.flatMap((el) => [...highlightHTML(currentSearchTerm, el as HTMLElement).children]),
)
previewInner = document.createElement("div") previewInner = document.createElement("div")
previewInner.classList.add("preview-inner") previewInner.classList.add("preview-inner")
const innerDiv = await fetchContent(slug).then((contents) =>
contents.map((el) => highlightHTML(currentSearchTerm, el.innerHTML)),
)
previewInner.append(...innerDiv) previewInner.append(...innerDiv)
preview.replaceChildren(previewInner) preview.replaceChildren(previewInner)
@@ -398,13 +394,13 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
const highlights = [...preview.querySelectorAll(".highlight")].sort( const highlights = [...preview.querySelectorAll(".highlight")].sort(
(a, b) => b.innerHTML.length - a.innerHTML.length, (a, b) => b.innerHTML.length - a.innerHTML.length,
) )
highlights[0]?.scrollIntoView() highlights[0]?.scrollIntoView({ block: "start" })
} }
async function onType(e: HTMLElementEventMap["input"]) { async function onType(e: HTMLElementEventMap["input"]) {
if (!searchLayout || !index) return if (!searchLayout || !index) return
currentSearchTerm = (e.target as HTMLInputElement).value currentSearchTerm = (e.target as HTMLInputElement).value
searchLayout.style.visibility = currentSearchTerm === "" ? "hidden" : "visible" searchLayout.classList.toggle("display-results", currentSearchTerm !== "")
searchType = currentSearchTerm.startsWith("#") ? "tags" : "basic" searchType = currentSearchTerm.startsWith("#") ? "tags" : "basic"
let searchResults: FlexSearch.SimpleDocumentSearchResultSetUnit[] let searchResults: FlexSearch.SimpleDocumentSearchResultSetUnit[]
@@ -444,8 +440,8 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
searchBar?.addEventListener("input", onType) searchBar?.addEventListener("input", onType)
window.addCleanup(() => searchBar?.removeEventListener("input", onType)) window.addCleanup(() => searchBar?.removeEventListener("input", onType))
index ??= await fillDocument(data)
registerEscapeHandler(container, hideSearch) registerEscapeHandler(container, hideSearch)
await fillDocument(data)
}) })
/** /**
@@ -454,37 +450,19 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
* @param data data to fill index with * @param data data to fill index with
*/ */
async function fillDocument(data: { [key: FullSlug]: ContentDetails }) { async function fillDocument(data: { [key: FullSlug]: ContentDetails }) {
const index = new FlexSearch.Document<Item>({
charset: "latin:extra",
encode: encoder,
document: {
id: "id",
index: [
{
field: "title",
tokenize: "forward",
},
{
field: "content",
tokenize: "forward",
},
{
field: "tags",
tokenize: "forward",
},
],
},
})
let id = 0 let id = 0
const promises: Array<Promise<unknown>> = []
for (const [slug, fileData] of Object.entries<ContentDetails>(data)) { for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
await index.addAsync(id++, { promises.push(
id, index.addAsync(id++, {
slug: slug as FullSlug, id,
title: fileData.title, slug: slug as FullSlug,
content: fileData.content, title: fileData.title,
tags: fileData.tags, content: fileData.content,
}) tags: fileData.tags,
}),
)
} }
return index return await Promise.all(promises)
} }

View File

@@ -1,3 +1,5 @@
@use "../../styles/variables.scss" as *;
button#explorer { button#explorer {
all: unset; all: unset;
background-color: transparent; background-color: transparent;
@@ -85,7 +87,7 @@ svg {
color: var(--secondary); color: var(--secondary);
font-family: var(--headerFont); font-family: var(--headerFont);
font-size: 0.95rem; font-size: 0.95rem;
font-weight: 600; font-weight: $semiBoldWeight;
line-height: 1.5rem; line-height: 1.5rem;
display: inline-block; display: inline-block;
} }
@@ -110,7 +112,7 @@ svg {
font-size: 0.95rem; font-size: 0.95rem;
display: inline-block; display: inline-block;
color: var(--secondary); color: var(--secondary);
font-weight: 600; font-weight: $semiBoldWeight;
margin: 0; margin: 0;
line-height: 1.5rem; line-height: 1.5rem;
pointer-events: none; pointer-events: none;

View File

@@ -38,6 +38,28 @@
white-space: normal; 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 { h1 {
font-size: 1.5rem; font-size: 1.5rem;
} }

View File

@@ -59,9 +59,13 @@
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
@media all and (max-width: $fullPageWidth) {
width: 90%;
}
& > * { & > * {
width: 100%; width: 100%;
border-radius: 5px; border-radius: 7px;
background: var(--light); background: var(--light);
box-shadow: box-shadow:
0 14px 50px rgba(27, 33, 48, 0.12), 0 14px 50px rgba(27, 33, 48, 0.12),
@@ -83,79 +87,81 @@
} }
& > #search-layout { & > #search-layout {
display: flex; display: none;
flex-direction: row; flex-direction: row;
visibility: hidden;
border: 1px solid var(--lightgray); border: 1px solid var(--lightgray);
flex: 0 0 100%;
box-sizing: border-box;
&.display-results {
display: flex;
}
&[data-preview] > #results-container {
flex: 0 0 min(30%, 450px);
}
@media all and (min-width: $tabletBreakpoint) { @media all and (min-width: $tabletBreakpoint) {
&[data-preview] { &[data-preview] {
& .result-card > p.preview { & .result-card > p.preview {
display: none; display: none;
} }
& > div {
&:first-child {
border-right: 1px solid var(--lightgray);
border-top-right-radius: unset;
border-bottom-right-radius: unset;
}
&:last-child {
border-top-left-radius: unset;
border-bottom-left-radius: unset;
}
}
} }
} }
& > div { & > div {
// vh - #search-space.margin-top
height: calc(75vh - 12vh); height: calc(75vh - 12vh);
background: none; border-radius: 5px;
&:first-child {
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
border-right: 1px solid var(--lightgray);
}
&:last-child {
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
}
} }
@media all and (max-width: $tabletBreakpoint) { @media all and (max-width: $tabletBreakpoint) {
display: block; & > #preview-container {
& > *:not(#results-container) {
display: none !important; display: none !important;
} }
& > #results-container { &[data-preview] > #results-container {
width: 100%; width: 100%;
height: auto; height: auto;
flex: 0 0 100%;
} }
} }
& .highlight { & .highlight {
background: color-mix(in srgb, var(--tertiary) 60%, transparent); background: color-mix(in srgb, var(--tertiary) 60%, rgba(255, 255, 255, 0));
border-radius: 5px; border-radius: 5px;
scroll-margin-top: 2rem;
} }
& > #preview-container { & > #preview-container {
display: block; display: block;
box-sizing: border-box;
overflow: hidden; overflow: hidden;
font-family: inherit;
color: var(--dark);
line-height: 1.5em;
font-weight: $normalWeight;
overflow-y: auto;
padding: 0 2rem;
& .preview-inner { & .preview-inner {
margin: 0 auto; margin: 0 auto;
padding: 1em; width: min($pageWidth, 100%);
height: 100%;
width: 100%;
box-sizing: border-box;
overflow-y: auto;
font-family: inherit;
color: var(--dark);
line-height: 1.5em;
font-weight: 400;
background: var(--light);
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
box-shadow:
0 14px 50px rgba(27, 33, 48, 0.12),
0 10px 30px rgba(27, 33, 48, 0.16);
} }
a.internal { a[role="anchor"] {
background-color: none; background-color: transparent;
} }
} }
@@ -163,6 +169,7 @@
overflow-y: auto; overflow-y: auto;
& .result-card { & .result-card {
overflow: hidden;
padding: 1em; padding: 1em;
cursor: pointer; cursor: pointer;
transition: background 0.2s ease; transition: background 0.2s ease;
@@ -178,10 +185,10 @@
margin: 0; margin: 0;
text-transform: none; text-transform: none;
text-align: left; text-align: left;
background: var(--light);
outline: none; outline: none;
font-weight: inherit; font-weight: inherit;
&:hover,
&:focus, &:focus,
&.focus { &.focus {
background: var(--lightgray); background: var(--lightgray);
@@ -202,7 +209,7 @@
padding: 0.2rem 0.4rem; padding: 0.2rem 0.4rem;
margin: 0 0.1rem; margin: 0 0.1rem;
line-height: 1.4rem; line-height: 1.4rem;
font-weight: bold; font-weight: $boldWeight;
color: var(--secondary); color: var(--secondary);
&.match-tag { &.match-tag {

View File

@@ -3,8 +3,10 @@ import { StaticResources } from "../util/resources"
import { QuartzPluginData } from "../plugins/vfile" import { QuartzPluginData } from "../plugins/vfile"
import { GlobalConfiguration } from "../cfg" import { GlobalConfiguration } from "../cfg"
import { Node } from "hast" import { Node } from "hast"
import { BuildCtx } from "../util/ctx"
export type QuartzComponentProps = { export type QuartzComponentProps = {
ctx: BuildCtx
externalResources: StaticResources externalResources: StaticResources
fileData: QuartzPluginData fileData: QuartzPluginData
cfg: GlobalConfiguration cfg: GlobalConfiguration

118
quartz/depgraph.test.ts Normal file
View 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
View 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
}
}

56
quartz/i18n/index.ts Normal file
View File

@@ -0,0 +1,56 @@
import { Translation, CalloutTranslation } from "./locales/definition"
import en from "./locales/en-US"
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 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"
export const TRANSLATIONS = {
"en-US": en,
"fr-FR": fr,
"it-IT": it,
"ja-JP": ja,
"de-DE": de,
"nl-NL": nl,
"nl-BE": nl,
"ro-RO": ro,
"ro-MD": ro,
"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,
} as const
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

View File

@@ -0,0 +1,88 @@
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: "إما أن هذه الصفحة خاصة أو غير موجودة.",
},
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

View File

@@ -0,0 +1,83 @@
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.",
},
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

View File

@@ -0,0 +1,83 @@
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
}
themeToggle: {
lightMode: string
darkMode: string
}
explorer: {
title: string
}
footer: {
createdWith: string
}
graph: {
title: string
}
recentNotes: {
title: string
seeRemainingMore: (variables: { remaining: number }) => string
}
transcludes: {
transcludeOf: (variables: { targetSlug: FullSlug }) => string
linkToOriginal: string
}
search: {
title: string
searchBarPlaceholder: string
}
tableOfContents: {
title: string
}
contentMeta: {
readingTime: (variables: { minutes: number }) => string
}
}
pages: {
rss: {
recentNotes: string
lastFewNotes: (variables: { count: number }) => string
}
error: {
title: string
notFound: string
}
folderContent: {
folder: string
itemsUnderFolder: (variables: { count: number }) => string
}
tagContent: {
tag: string
tagIndex: string
itemsUnderTag: (variables: { count: number }) => string
showingFirst: (variables: { count: number }) => string
totalTags: (variables: { count: number }) => string
}
}
}

View File

@@ -0,0 +1,83 @@
import { Translation } from "./definition"
export default {
propertyDefaults: {
title: "Untitled",
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",
},
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.",
},
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

View File

@@ -0,0 +1,83 @@
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: "Enlaces de Retroceso",
noBacklinksFound: "No se han encontrado enlaces traseros",
},
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 }) => `${minutes} min read`,
},
},
pages: {
rss: {
recentNotes: "Notas recientes",
lastFewNotes: ({ count }) => `Últimás ${count} notas`,
},
error: {
title: "No se encontró.",
notFound: "Esta página es privada o no existe.",
},
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 encontraron ${count} etiquetas en total.`,
},
},
} as const satisfies Translation

View File

@@ -0,0 +1,83 @@
import { Translation } from "./definition"
export default {
propertyDefaults: {
title: "Sans titre",
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é",
},
themeToggle: {
lightMode: "Mode clair",
darkMode: "Mode sombre",
},
explorer: {
title: "Explorateur",
},
footer: {
createdWith: "Créé avec",
},
graph: {
title: "Vue Graphique",
},
recentNotes: {
title: "Notes Récentes",
seeRemainingMore: ({ remaining }) => `Voir ${remaining} de plus →`,
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Transclusion de ${targetSlug}`,
linkToOriginal: "Lien vers l'original",
},
search: {
title: "Recherche",
searchBarPlaceholder: "Rechercher quelque chose",
},
tableOfContents: {
title: "Table des Matières",
},
contentMeta: {
readingTime: ({ minutes }) => `${minutes} min read`,
},
},
pages: {
rss: {
recentNotes: "Notes récentes",
lastFewNotes: ({ count }) => `Les dernières ${count} notes`,
},
error: {
title: "Pas trouvé",
notFound: "Cette page est soit privée, soit elle n'existe pas.",
},
folderContent: {
folder: "Dossier",
itemsUnderFolder: ({ count }) =>
count === 1 ? "1 élément sous ce dossier." : `${count} éléments sous ce dossier.`,
},
tagContent: {
tag: "Étiquette",
tagIndex: "Index des étiquettes",
itemsUnderTag: ({ count }) =>
count === 1 ? "1 élément avec cette étiquette." : `${count} éléments avec cette étiquette.`,
showingFirst: ({ count }) => `Affichage des premières ${count} étiquettes.`,
totalTags: ({ count }) => `Trouvé ${count} étiquettes au total.`,
},
},
} as const satisfies Translation

View File

@@ -0,0 +1,83 @@
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.",
},
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

View File

@@ -0,0 +1,81 @@
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} min read`,
},
},
pages: {
rss: {
recentNotes: "最近の記事",
lastFewNotes: ({ count }) => `最新の${count}`,
},
error: {
title: "Not Found",
notFound: "ページが存在しないか、非公開設定になっています。",
},
folderContent: {
folder: "フォルダ",
itemsUnderFolder: ({ count }) => `${count}件のページ`,
},
tagContent: {
tag: "タグ",
tagIndex: "タグ一覧",
itemsUnderTag: ({ count }) => `${count}件のページ`,
showingFirst: ({ count }) => `のうち最初の${count}件を表示しています`,
totalTags: ({ count }) => `${count}個のタグを表示中`,
},
},
} as const satisfies Translation

View File

@@ -0,0 +1,81 @@
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: "페이지가 존재하지 않거나 비공개 설정이 되어 있습니다.",
},
folderContent: {
folder: "폴더",
itemsUnderFolder: ({ count }) => `${count}건의 항목`,
},
tagContent: {
tag: "태그",
tagIndex: "태그 목록",
itemsUnderTag: ({ count }) => `${count}건의 항목`,
showingFirst: ({ count }) => `처음 ${count}개의 태그`,
totalTags: ({ count }) => `${count}개의 태그를 찾았습니다.`,
},
},
} as const satisfies Translation

View File

@@ -0,0 +1,85 @@
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.",
},
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

View File

@@ -0,0 +1,84 @@
import { Translation } from "./definition"
export default {
propertyDefaults: {
title: "Fără titlu",
description: "Nici o descriere furnizată",
},
components: {
callout: {
note: "Notă",
abstract: "Rezumat",
info: "Informație",
todo: "De făcut",
tip: "Sfat",
success: "Succes",
question: "Întrebare",
warning: "Avertisment",
failure: "Eșec",
danger: "Pericol",
bug: "Bug",
example: "Exemplu",
quote: "Citat",
},
backlinks: {
title: "Legături înapoi",
noBacklinksFound: "Nu s-au găsit legături înapoi",
},
themeToggle: {
lightMode: "Modul luminos",
darkMode: "Modul întunecat",
},
explorer: {
title: "Explorator",
},
footer: {
createdWith: "Creat cu",
},
graph: {
title: "Graf",
},
recentNotes: {
title: "Notițe recente",
seeRemainingMore: ({ remaining }) => `Vezi încă ${remaining}`,
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Extras din ${targetSlug}`,
linkToOriginal: "Legătură către original",
},
search: {
title: "Căutare",
searchBarPlaceholder: "Introduceți termenul de căutare...",
},
tableOfContents: {
title: "Cuprins",
},
contentMeta: {
readingTime: ({ minutes }) =>
minutes == 1 ? `lectură de 1 minut` : `lectură de ${minutes} minute`,
},
},
pages: {
rss: {
recentNotes: "Notițe recente",
lastFewNotes: ({ count }) => `Ultimele ${count} notițe`,
},
error: {
title: "Pagina nu a fost găsită",
notFound: "Fie această pagină este privată, fie nu există.",
},
folderContent: {
folder: "Dosar",
itemsUnderFolder: ({ count }) =>
count === 1 ? "1 articol în acest dosar." : `${count} elemente în acest dosar.`,
},
tagContent: {
tag: "Etichetă",
tagIndex: "Indexul etichetelor",
itemsUnderTag: ({ count }) =>
count === 1 ? "1 articol cu această etichetă." : `${count} articole cu această etichetă.`,
showingFirst: ({ count }) => `Se afișează primele ${count} etichete.`,
totalTags: ({ count }) => `Au fost găsite ${count} etichete în total.`,
},
},
} as const satisfies Translation

View File

@@ -0,0 +1,95 @@
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 }) =>
`Посмотреть оставш${getForm(remaining, "уюся", "иеся", "иеся")} ${remaining}`,
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Переход из ${targetSlug}`,
linkToOriginal: "Ссылка на оригинал",
},
search: {
title: "Поиск",
searchBarPlaceholder: "Найти что-нибудь",
},
tableOfContents: {
title: "Оглавление",
},
contentMeta: {
readingTime: ({ minutes }) => `время чтения ~${minutes} мин.`,
},
},
pages: {
rss: {
recentNotes: "Недавние заметки",
lastFewNotes: ({ count }) =>
`Последн${getForm(count, "яя", "ие", "ие")} ${count} замет${getForm(count, "ка", "ки", "ок")}`,
},
error: {
title: "Страница не найдена",
notFound: "Эта страница приватная или не существует",
},
folderContent: {
folder: "Папка",
itemsUnderFolder: ({ count }) =>
`в этой папке ${count} элемент${getForm(count, "", "а", "ов")}`,
},
tagContent: {
tag: "Тег",
tagIndex: "Индекс тегов",
itemsUnderTag: ({ count }) => `с этим тегом ${count} элемент${getForm(count, "", "а", "ов")}`,
showingFirst: ({ count }) =>
`Показыва${getForm(count, "ется", "ются", "ются")} ${count} тег${getForm(count, "", "а", "ов")}`,
totalTags: ({ count }) => `Всего ${count} тег${getForm(count, "", "а", "ов")}`,
},
},
} as const satisfies Translation
function getForm(number: number, form1: string, form2: string, form5: string): string {
const remainder100 = number % 100
const remainder10 = remainder100 % 10
if (remainder100 >= 10 && remainder100 <= 20) return form5
if (remainder10 > 1 && remainder10 < 5) return form2
if (remainder10 == 1) return form1
return form5
}

View File

@@ -0,0 +1,83 @@
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} min read`,
},
},
pages: {
rss: {
recentNotes: "Останні нотатки",
lastFewNotes: ({ count }) => `Останні нотатки: ${count}`,
},
error: {
title: "Не знайдено",
notFound: "Ця сторінка або приватна, або не існує.",
},
folderContent: {
folder: "Папка",
itemsUnderFolder: ({ count }) =>
count === 1 ? "У цій папці 1 елемент." : `Елементів у цій папці: ${count}.`,
},
tagContent: {
tag: "Тег",
tagIndex: "Індекс тегу",
itemsUnderTag: ({ count }) =>
count === 1 ? "1 елемент з цим тегом." : `Елементів з цим тегом: ${count}.`,
showingFirst: ({ count }) => `Показ перших ${count} тегів.`,
totalTags: ({ count }) => `Всього знайдено тегів: ${count}.`,
},
},
} as const satisfies Translation

Some files were not shown because too many files have changed in this diff Show More