diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b3f128e2..b192939e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,17 +24,17 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 20 - name: Cache dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..805b5a4e --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +v20.9.0 diff --git a/docs/features/comments.md b/docs/features/comments.md new file mode 100644 index 00000000..92ea754b --- /dev/null +++ b/docs/features/comments.md @@ -0,0 +1,83 @@ +--- +title: Comments +tags: + - component +--- + +Quartz also has the ability to hook into various providers to enable readers to leave comments on your site. + +![[giscus-example.png]] + +As of today, only [Giscus](https://giscus.app/) is supported out of the box but PRs to support other providers are welcome! + +## Providers + +### Giscus + +First, make sure that the [[setting up your GitHub repository|GitHub]] repository you are using for your Quartz meets the following requirements: + +1. The **repository is [public](https://docs.github.com/en/github/administering-a-repository/managing-repository-settings/setting-repository-visibility#making-a-repository-public)**, otherwise visitors will not be able to view the discussion. +2. The **[giscus](https://github.com/apps/giscus) app is installed**, otherwise visitors will not be able to comment and react. +3. The **Discussions feature is turned on** by [enabling it for your repository](https://docs.github.com/en/github/administering-a-repository/managing-repository-settings/enabling-or-disabling-github-discussions-for-a-repository). + +Then, use the [Giscus site](https://giscus.app/#repository) to figure out what your `repoId` and `categoryId` should be. Make sure you select `Announcements` for the Discussion category. + +![[giscus-repo.png]] + +![[giscus-discussion.png]] + +After entering both your repository and selecting the discussion category, Giscus will compute some IDs that you'll need to provide back to Quartz. You won't need to manually add the script yourself as Quartz will handle that part for you but will need these values in the next step! + +![[giscus-results.png]] + +Finally, in `quartz.layout.ts`, edit the `afterBody` field of `sharedPageComponents` to include the following options but with the values you got from above: + +```ts title="quartz.layout.ts" +afterBody: [ + Component.Comments({ + provider: 'giscus', + options: { + // from data-repo + repo: 'jackyzha0/quartz', + // from data-repo-id + repoId: 'MDEwOlJlcG9zaXRvcnkzODcyMTMyMDg', + // from data-category + category: 'Announcements', + // from data-category-id + categoryId: 'DIC_kwDOFxRnmM4B-Xg6', + } + }), +], +``` + +### Customization + +Quartz also exposes a few of the other Giscus options as well and you can provide them the same way `repo`, `repoId`, `category`, and `categoryId` are provided. + +```ts +type Options = { + provider: "giscus" + options: { + repo: `${string}/${string}` + repoId: string + category: string + categoryId: string + + // how to map pages -> discussions + // defaults to 'url' + mapping?: "url" | "title" | "og:title" | "specific" | "number" | "pathname" + + // use strict title matching + // defaults to true + strict?: boolean + + // whether to enable reactions for the main post + // defaults to true + reactionsEnabled?: boolean + + // where to put the comment input box relative to the comments + // defaults to 'bottom' + inputPosition?: "top" | "bottom" + } +} +``` diff --git a/docs/features/i18n.md b/docs/features/i18n.md new file mode 100644 index 00000000..57547dda --- /dev/null +++ b/docs/features/i18n.md @@ -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`. diff --git a/docs/images/giscus-discussion.png b/docs/images/giscus-discussion.png new file mode 100644 index 00000000..939af624 Binary files /dev/null and b/docs/images/giscus-discussion.png differ diff --git a/docs/images/giscus-example.png b/docs/images/giscus-example.png new file mode 100644 index 00000000..f59f52ba Binary files /dev/null and b/docs/images/giscus-example.png differ diff --git a/docs/images/giscus-repo.png b/docs/images/giscus-repo.png new file mode 100644 index 00000000..bfabc569 Binary files /dev/null and b/docs/images/giscus-repo.png differ diff --git a/docs/images/giscus-results.png b/docs/images/giscus-results.png new file mode 100644 index 00000000..b25c7515 Binary files /dev/null and b/docs/images/giscus-results.png differ diff --git a/docs/images/github-init-repo-options.png b/docs/images/github-init-repo-options.png new file mode 100644 index 00000000..dd889315 Binary files /dev/null and b/docs/images/github-init-repo-options.png differ diff --git a/docs/images/github-quick-setup.png b/docs/images/github-quick-setup.png new file mode 100644 index 00000000..5be333f1 Binary files /dev/null and b/docs/images/github-quick-setup.png differ diff --git a/docs/plugins/AliasRedirects.md b/docs/plugins/AliasRedirects.md new file mode 100644 index 00000000..8c036537 --- /dev/null +++ b/docs/plugins/AliasRedirects.md @@ -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). diff --git a/docs/plugins/Assets.md b/docs/plugins/Assets.md new file mode 100644 index 00000000..47589b2c --- /dev/null +++ b/docs/plugins/Assets.md @@ -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). diff --git a/docs/plugins/CNAME.md b/docs/plugins/CNAME.md new file mode 100644 index 00000000..bc12b5ac --- /dev/null +++ b/docs/plugins/CNAME.md @@ -0,0 +1,22 @@ +--- +title: CNAME +tags: + - plugin/emitter +--- + +This plugin emits a `CNAME` record that points your subdomain to the default domain of your site. + +If you want to use a custom domain name like `quartz.example.com` for the site, then this is needed. + +See [[hosting|Hosting]] for more information. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin has no configuration options. + +## API + +- Category: Emitter +- Function name: `Plugin.CNAME()`. +- Source: [`quartz/plugins/emitters/cname.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/cname.ts). diff --git a/docs/plugins/ComponentResources.md b/docs/plugins/ComponentResources.md new file mode 100644 index 00000000..6e8c82ef --- /dev/null +++ b/docs/plugins/ComponentResources.md @@ -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). diff --git a/docs/plugins/ContentIndex.md b/docs/plugins/ContentIndex.md new file mode 100644 index 00000000..eb7265d4 --- /dev/null +++ b/docs/plugins/ContentIndex.md @@ -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). diff --git a/docs/plugins/ContentPage.md b/docs/plugins/ContentPage.md new file mode 100644 index 00000000..bd33e4ee --- /dev/null +++ b/docs/plugins/ContentPage.md @@ -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). diff --git a/docs/plugins/CrawlLinks.md b/docs/plugins/CrawlLinks.md new file mode 100644 index 00000000..47b7bdd7 --- /dev/null +++ b/docs/plugins/CrawlLinks.md @@ -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). diff --git a/docs/plugins/CreatedModifiedDate.md b/docs/plugins/CreatedModifiedDate.md new file mode 100644 index 00000000..f4134c47 --- /dev/null +++ b/docs/plugins/CreatedModifiedDate.md @@ -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). diff --git a/docs/plugins/Description.md b/docs/plugins/Description.md new file mode 100644 index 00000000..af1c8b7c --- /dev/null +++ b/docs/plugins/Description.md @@ -0,0 +1,23 @@ +--- +title: Description +tags: + - plugin/transformer +--- + +This plugin generates descriptions that are used as metadata for the HTML `head`, the [[RSS Feed]] and in [[folder and tag listings]] if there is no main body content, the description is used as the text between the title and the listing. + +If the frontmatter contains a `description` property, it is used (see [[authoring content#Syntax]]). Otherwise, the plugin will do its best to use the first few sentences of the content to reach the target description length. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin accepts the following configuration options: + +- `descriptionLength`: the maximum length of the generated description. Default is 150 characters. The cut off happens after the first _sentence_ that ends after the given length. +- `replaceExternalLinks`: If `true` (default), replace external links with their domain and path in the description (e.g. `https://domain.tld/some_page/another_page?query=hello&target=world` is replaced with `domain.tld/some_page/another_page`). + +## API + +- Category: Transformer +- Function name: `Plugin.Description()`. +- Source: [`quartz/plugins/transformers/description.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/description.ts). diff --git a/docs/plugins/ExplicitPublish.md b/docs/plugins/ExplicitPublish.md new file mode 100644 index 00000000..2fd929b9 --- /dev/null +++ b/docs/plugins/ExplicitPublish.md @@ -0,0 +1,18 @@ +--- +title: ExplicitPublish +tags: + - plugin/filter +--- + +This plugin filters content based on an explicit `publish` flag in the frontmatter, allowing only content that is explicitly marked for publication to pass through. It's the opt-in version of [[RemoveDrafts]]. See [[private pages]] for more information. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin has no configuration options. + +## API + +- Category: Filter +- Function name: `Plugin.ExplicitPublish()`. +- Source: [`quartz/plugins/filters/explicit.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/filters/explicit.ts). diff --git a/docs/plugins/FolderPage.md b/docs/plugins/FolderPage.md new file mode 100644 index 00000000..45cfa157 --- /dev/null +++ b/docs/plugins/FolderPage.md @@ -0,0 +1,24 @@ +--- +title: FolderPage +tags: + - plugin/emitter +--- + +This plugin generates index pages for folders, creating a listing page for each folder that contains multiple content files. See [[folder and tag listings]] for more information. + +Example: [[advanced/|Advanced]] + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +The pages are displayed using the `defaultListPageLayout` in `quartz.layouts.ts`. For the content, the `FolderContent` component is used. If you want to modify the layout, you must edit it directly (`quartz/components/pages/FolderContent.tsx`). + +This plugin accepts the following configuration options: + +- `sort`: A function of type `(f1: QuartzPluginData, f2: QuartzPluginData) => number{:ts}` used to sort entries. Defaults to sorting by date and tie-breaking on lexographical order. + +## API + +- Category: Emitter +- Function name: `Plugin.FolderPage()`. +- Source: [`quartz/plugins/emitters/folderPage.tsx`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/folderPage.tsx). diff --git a/docs/plugins/Frontmatter.md b/docs/plugins/Frontmatter.md new file mode 100644 index 00000000..879d087d --- /dev/null +++ b/docs/plugins/Frontmatter.md @@ -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). diff --git a/docs/plugins/GitHubFlavoredMarkdown.md b/docs/plugins/GitHubFlavoredMarkdown.md new file mode 100644 index 00000000..41fab6b2 --- /dev/null +++ b/docs/plugins/GitHubFlavoredMarkdown.md @@ -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). diff --git a/docs/plugins/HardLineBreaks.md b/docs/plugins/HardLineBreaks.md new file mode 100644 index 00000000..e24f7e12 --- /dev/null +++ b/docs/plugins/HardLineBreaks.md @@ -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). diff --git a/docs/plugins/Latex.md b/docs/plugins/Latex.md new file mode 100644 index 00000000..ac436784 --- /dev/null +++ b/docs/plugins/Latex.md @@ -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). diff --git a/docs/plugins/NotFoundPage.md b/docs/plugins/NotFoundPage.md new file mode 100644 index 00000000..b6799432 --- /dev/null +++ b/docs/plugins/NotFoundPage.md @@ -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). diff --git a/docs/plugins/ObsidianFlavoredMarkdown.md b/docs/plugins/ObsidianFlavoredMarkdown.md new file mode 100644 index 00000000..30d1f717 --- /dev/null +++ b/docs/plugins/ObsidianFlavoredMarkdown.md @@ -0,0 +1,34 @@ +--- +title: ObsidianFlavoredMarkdown +tags: + - plugin/transformer +--- + +This plugin provides support for [[Obsidian compatibility]]. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +This plugin accepts the following configuration options: + +- `comments`: If `true` (default), enables parsing of `%%` style Obsidian comment blocks. +- `highlight`: If `true` (default), enables parsing of `==` style highlights within content. +- `wikilinks`:If `true` (default), turns [[wikilinks]] into regular links. +- `callouts`: If `true` (default), adds support for [[callouts|callout]] blocks for emphasizing content. +- `mermaid`: If `true` (default), enables [[Mermaid diagrams|Mermaid diagram]] rendering within Markdown files. +- `parseTags`: If `true` (default), parses and links tags within the content. +- `parseArrows`: If `true` (default), transforms arrow symbols into their HTML character equivalents. +- `parseBlockReferences`: If `true` (default), handles block references, linking to specific content blocks. +- `enableInHtmlEmbed`: If `true`, allows embedding of content directly within HTML. Defaults to `false`. +- `enableYouTubeEmbed`: If `true` (default), enables the embedding of YouTube videos and playlists using external image Markdown syntax. +- `enableVideoEmbed`: If `true` (default), enables the embedding of video files. +- `enableCheckbox`: If `true`, adds support for interactive checkboxes in content. Defaults to `false`. + +> [!warning] +> Don't remove this plugin if you're using [[Obsidian compatibility|Obsidian]] to author the content! + +## API + +- Category: Transformer +- Function name: `Plugin.ObsidianFlavoredMarkdown()`. +- Source: [`quartz/plugins/transformers/toc.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/toc.ts). diff --git a/docs/plugins/OxHugoFlavoredMarkdown.md b/docs/plugins/OxHugoFlavoredMarkdown.md new file mode 100644 index 00000000..5c2afeea --- /dev/null +++ b/docs/plugins/OxHugoFlavoredMarkdown.md @@ -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 `
` 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). diff --git a/docs/plugins/RemoveDrafts.md b/docs/plugins/RemoveDrafts.md new file mode 100644 index 00000000..07fb4d0e --- /dev/null +++ b/docs/plugins/RemoveDrafts.md @@ -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). diff --git a/docs/plugins/Static.md b/docs/plugins/Static.md new file mode 100644 index 00000000..80bf5a15 --- /dev/null +++ b/docs/plugins/Static.md @@ -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). diff --git a/docs/plugins/SyntaxHighlighting.md b/docs/plugins/SyntaxHighlighting.md new file mode 100644 index 00000000..6fb67dba --- /dev/null +++ b/docs/plugins/SyntaxHighlighting.md @@ -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). diff --git a/docs/plugins/TableOfContents.md b/docs/plugins/TableOfContents.md new file mode 100644 index 00000000..0e9e4ea7 --- /dev/null +++ b/docs/plugins/TableOfContents.md @@ -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). diff --git a/docs/plugins/TagPage.md b/docs/plugins/TagPage.md new file mode 100644 index 00000000..9556363f --- /dev/null +++ b/docs/plugins/TagPage.md @@ -0,0 +1,22 @@ +--- +title: TagPage +tags: + - plugin/emitter +--- + +This plugin emits dedicated pages for each tag used in the content. See [[folder and tag listings]] for more information. + +> [!note] +> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. + +The pages are displayed using the `defaultListPageLayout` in `quartz.layouts.ts`. For the content, the `TagContent` component is used. If you want to modify the layout, you must edit it directly (`quartz/components/pages/TagContent.tsx`). + +This plugin accepts the following configuration options: + +- `sort`: A function of type `(f1: QuartzPluginData, f2: QuartzPluginData) => number{:ts}` used to sort entries. Defaults to sorting by date and tie-breaking on lexographical order. + +## API + +- Category: Emitter +- Function name: `Plugin.TagPage()`. +- Source: [`quartz/plugins/emitters/tagPage.tsx`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/tagPage.tsx). diff --git a/docs/plugins/index.md b/docs/plugins/index.md new file mode 100644 index 00000000..298ff164 --- /dev/null +++ b/docs/plugins/index.md @@ -0,0 +1,3 @@ +--- +title: Plugins +--- diff --git a/docs/setting up your GitHub repository.md b/docs/setting up your GitHub repository.md new file mode 100644 index 00000000..43a556dc --- /dev/null +++ b/docs/setting up your GitHub repository.md @@ -0,0 +1,48 @@ +--- +title: Setting up your GitHub repository +--- + +First, make sure you have Quartz [[index#🪴 Get Started|cloned and setup locally]]. + +Then, create a new repository on GitHub.com. Do **not** initialize the new repository with `README`, license, or `gitignore` files. + +![[github-init-repo-options.png]] + +At the top of your repository on GitHub.com's Quick Setup page, click the clipboard to copy the remote repository URL. + +![[github-quick-setup.png]] + +In your terminal of choice, navigate to the root of your Quartz folder. Then, run the following commands, replacing `REMOTE-URL` with the URL you just copied from the previous step. + +```bash +# list all the repositories that are tracked +git remote -v + +# if the origin doesn't match your own repository, set your repository as the origin +git remote set-url origin REMOTE-URL + +# if you don't have upstream as a remote, add it so updates work +git remote add upstream https://github.com/jackyzha0/quartz.git +``` + +Then, you can sync the content to upload it to your repository. This is a helper command that will do the initial push of your content to your repository. + +```bash +npx quartz sync --no-pull +``` + +> [!warning]- `fatal: --[no-]autostash option is only valid with --rebase` +> You may have an outdated version of `git`. Updating `git` should fix this issue. + +In future updates, you can simply run `npx quartz sync` every time you want to push updates to your repository. + +> [!hint] Flags and options +> For full help options, you can run `npx quartz sync --help`. +> +> Most of these have sensible defaults but you can override them if you have a custom setup: +> +> - `-d` or `--directory`: the content folder. This is normally just `content` +> - `-v` or `--verbose`: print out extra logging information +> - `--commit` or `--no-commit`: whether to make a `git` commit for your changes +> - `--push` or `--no-push`: whether to push updates to your GitHub fork of Quartz +> - `--pull` or `--no-pull`: whether to try and pull in any updates from your GitHub fork (i.e. from other devices) before pushing diff --git a/docs/tags/plugin.md b/docs/tags/plugin.md new file mode 100644 index 00000000..298ff164 --- /dev/null +++ b/docs/tags/plugin.md @@ -0,0 +1,3 @@ +--- +title: Plugins +--- diff --git a/globals.d.ts b/globals.d.ts index ee13005c..6cf30f8a 100644 --- a/globals.d.ts +++ b/globals.d.ts @@ -4,6 +4,10 @@ export declare global { type: K, listener: (this: Document, ev: CustomEventMap[K]) => void, ): void + removeEventListener( + type: K, + listener: (this: Document, ev: CustomEventMap[K]) => void, + ): void dispatchEvent(ev: CustomEventMap[K] | UIEvent): void } interface Window { diff --git a/package-lock.json b/package-lock.json index 420934df..9dc871cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,46 +1,47 @@ { "name": "@jackyzha0/quartz", - "version": "4.2.2", + "version": "4.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jackyzha0/quartz", - "version": "4.2.2", + "version": "4.3.0", "license": "MIT", "dependencies": { "@clack/prompts": "^0.7.0", - "@floating-ui/dom": "^1.6.1", - "@napi-rs/simple-git": "0.1.14", - "async-mutex": "^0.4.1", + "@floating-ui/dom": "^1.6.8", + "@napi-rs/simple-git": "0.1.16", + "async-mutex": "^0.5.0", "chalk": "^5.3.0", - "chokidar": "^3.5.3", + "chokidar": "^3.6.0", "cli-spinner": "^0.2.10", - "d3": "^7.8.5", - "esbuild-sass-plugin": "^2.16.0", + "d3": "^7.9.0", + "esbuild-sass-plugin": "^2.16.1", "flexsearch": "0.7.43", "github-slugger": "^2.0.0", - "globby": "^14.0.0", + "globby": "^14.0.2", "gray-matter": "^4.0.3", - "hast-util-to-html": "^9.0.0", + "hast-util-to-html": "^9.0.1", "hast-util-to-jsx-runtime": "^2.3.0", "hast-util-to-string": "^3.0.0", "is-absolute-url": "^4.0.1", "js-yaml": "^4.1.0", - "lightningcss": "^1.23.0", + "lightningcss": "^1.25.1", "mdast-util-find-and-replace": "^3.0.1", - "mdast-util-to-hast": "^13.1.0", + "mdast-util-to-hast": "^13.2.0", "mdast-util-to-string": "^4.0.0", "micromorph": "^0.4.5", - "preact": "^10.19.3", - "preact-render-to-string": "^6.3.1", + "preact": "^10.22.1", + "preact-render-to-string": "^6.5.7", "pretty-bytes": "^6.1.1", "pretty-time": "^1.1.0", "reading-time": "^1.5.0", "rehype-autolink-headings": "^7.1.0", + "rehype-citation": "^2.0.0", "rehype-katex": "^7.0.0", "rehype-mathjax": "^6.0.0", - "rehype-pretty-code": "^0.12.6", + "rehype-pretty-code": "^0.13.2", "rehype-raw": "^7.0.0", "rehype-slug": "^6.0.0", "remark": "^15.0.1", @@ -50,19 +51,19 @@ "remark-math": "^6.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.0", - "remark-smartypants": "^2.0.0", - "rfdc": "^1.3.1", - "rimraf": "^5.0.5", + "remark-smartypants": "^3.0.2", + "rfdc": "^1.4.1", + "rimraf": "^6.0.1", "serve-handler": "^6.1.5", - "shikiji": "^0.10.2", + "shiki": "^1.10.3", "source-map-support": "^0.5.21", "to-vfile": "^8.0.0", "toml": "^3.0.0", "unified": "^11.0.4", "unist-util-visit": "^5.0.0", - "vfile": "^6.0.1", - "workerpool": "^9.1.0", - "ws": "^8.15.1", + "vfile": "^6.0.2", + "workerpool": "^9.1.3", + "ws": "^8.18.0", "yargs": "^17.7.2" }, "bin": { @@ -73,18 +74,18 @@ "@types/d3": "^7.4.3", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", - "@types/node": "^20.11.14", + "@types/node": "^22.1.0", "@types/pretty-time": "^1.1.5", "@types/source-map-support": "^0.5.10", - "@types/ws": "^8.5.10", + "@types/ws": "^8.5.12", "@types/yargs": "^17.0.32", "esbuild": "^0.19.9", - "prettier": "^3.2.4", - "tsx": "^4.7.0", - "typescript": "^5.3.3" + "prettier": "^3.3.3", + "tsx": "^4.16.2", + "typescript": "^5.5.3" }, "engines": { - "node": ">=18.14", + "node": "20 || >=22", "npm": ">=9.3.1" } }, @@ -98,6 +99,82 @@ "is-potential-custom-element-name": "^1.0.1" } }, + "node_modules/@citation-js/core": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.7.9.tgz", + "integrity": "sha512-fSbkB32JayDChZnAYC/kB+sWHRvxxL7ibVetyBOyzOc+5aCnjb6UVsbcfhnkOIEyAMoRRvWDyFmakEoTtA5ttQ==", + "dependencies": { + "@citation-js/date": "^0.5.0", + "@citation-js/name": "^0.4.2", + "fetch-ponyfill": "^7.1.0", + "sync-fetch": "^0.4.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@citation-js/date": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@citation-js/date/-/date-0.5.1.tgz", + "integrity": "sha512-1iDKAZ4ie48PVhovsOXQ+C6o55dWJloXqtznnnKy6CltJBQLIuLLuUqa8zlIvma0ZigjVjgDUhnVaNU1MErtZw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@citation-js/name": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@citation-js/name/-/name-0.4.2.tgz", + "integrity": "sha512-brSPsjs2fOVzSnARLKu0qncn6suWjHVQtrqSUrnqyaRH95r/Ad4wPF5EsoWr+Dx8HzkCGb/ogmoAzfCsqlTwTQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@citation-js/plugin-bibjson": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibjson/-/plugin-bibjson-0.7.9.tgz", + "integrity": "sha512-YNCWIrkhqZ3cZKewHkLBixABo2PvOWnU+8dBx6KfN47ysdECR76xENe86YYpJ0ska2D5ZnTP0jKZIrUHQoxYfQ==", + "dependencies": { + "@citation-js/date": "^0.5.0", + "@citation-js/name": "^0.4.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@citation-js/core": "^0.7.0" + } + }, + "node_modules/@citation-js/plugin-bibtex": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.7.9.tgz", + "integrity": "sha512-gIJpCd6vmmTOcRfDrSOjtoNhw2Mi94UwFxmgJ7GwkXyTYcNheW5VlMMo1tlqjakJGARQ0eOsKcI57gSPqJSS2g==", + "dependencies": { + "@citation-js/date": "^0.5.0", + "@citation-js/name": "^0.4.2", + "moo": "^0.5.1" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@citation-js/core": "^0.7.0" + } + }, + "node_modules/@citation-js/plugin-csl": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-csl/-/plugin-csl-0.7.9.tgz", + "integrity": "sha512-mbD7CnUiPOuVnjeJwo+d0RGUcY0PE8n01gHyjq0qpTeS42EGmQ9+LzqfsTUVWWBndTwc6zLRuIF1qFAUHKE4oA==", + "dependencies": { + "@citation-js/date": "^0.5.0", + "citeproc": "^2.4.6" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@citation-js/core": "^0.7.0" + } + }, "node_modules/@clack/core": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.3.3.tgz", @@ -241,6 +318,21 @@ "version": "0.19.12", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", "cpu": [ "arm" ], @@ -252,10 +344,10 @@ "node": ">= 10" } }, - "node_modules/@esbuild/linux-arm": { + "node_modules/@esbuild/linux-arm64": { "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", "cpu": [ "arm64" ], @@ -486,18 +578,18 @@ } }, "node_modules/@floating-ui/dom": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.1.tgz", - "integrity": "sha512-iA8qE43/H5iGozC3W0YSnVSW42Vh522yyM1gj+BqRwVsTNOyr231PsXDaV04yT39PsO0QL2QpbI/M0ZaLUQgRQ==", + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.8.tgz", + "integrity": "sha512-kx62rP19VZ767Q653wsP1XZCGIirkE09E0QUGNYTM/ttbbQHqcGPdSfWFxUyyNLc/W6aoJRBajOSXhP6GXjC0Q==", "dependencies": { "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.1" + "@floating-ui/utils": "^0.2.5" } }, "node_modules/@floating-ui/utils": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", - "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.5.tgz", + "integrity": "sha512-sTcG+QZ6fdEUObICavU+aB3Mp8HY4n14wYHdxK4fXjPmv3PXZZeY5RaguJmGyeH/CJQhX3fqKUtS4qc1LoHwhQ==" }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -516,30 +608,30 @@ } }, "node_modules/@napi-rs/simple-git": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git/-/simple-git-0.1.14.tgz", - "integrity": "sha512-2cDnsT0nKpQ7yg5u/Zf8/ibp9YFIKhpcfMAGATYuqdJoHuBo6P6UArZ0RDOOtfFC5b9FXuYcGw2ApbM4eWdnbQ==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git/-/simple-git-0.1.16.tgz", + "integrity": "sha512-C5wRPw9waqL2jk3jEDeJv+f7ScuO3N0a39HVdyFLkwKxHH4Sya4ZbzZsu2JLi6eEqe7RuHipHL6mC7B2OfYZZw==", "engines": { "node": ">= 10" }, "optionalDependencies": { - "@napi-rs/simple-git-android-arm-eabi": "0.1.14", - "@napi-rs/simple-git-android-arm64": "0.1.14", - "@napi-rs/simple-git-darwin-arm64": "0.1.14", - "@napi-rs/simple-git-darwin-x64": "0.1.14", - "@napi-rs/simple-git-linux-arm-gnueabihf": "0.1.14", - "@napi-rs/simple-git-linux-arm64-gnu": "0.1.14", - "@napi-rs/simple-git-linux-arm64-musl": "0.1.14", - "@napi-rs/simple-git-linux-x64-gnu": "0.1.14", - "@napi-rs/simple-git-linux-x64-musl": "0.1.14", - "@napi-rs/simple-git-win32-arm64-msvc": "0.1.14", - "@napi-rs/simple-git-win32-x64-msvc": "0.1.14" + "@napi-rs/simple-git-android-arm-eabi": "0.1.16", + "@napi-rs/simple-git-android-arm64": "0.1.16", + "@napi-rs/simple-git-darwin-arm64": "0.1.16", + "@napi-rs/simple-git-darwin-x64": "0.1.16", + "@napi-rs/simple-git-linux-arm-gnueabihf": "0.1.16", + "@napi-rs/simple-git-linux-arm64-gnu": "0.1.16", + "@napi-rs/simple-git-linux-arm64-musl": "0.1.16", + "@napi-rs/simple-git-linux-x64-gnu": "0.1.16", + "@napi-rs/simple-git-linux-x64-musl": "0.1.16", + "@napi-rs/simple-git-win32-arm64-msvc": "0.1.16", + "@napi-rs/simple-git-win32-x64-msvc": "0.1.16" } }, "node_modules/@napi-rs/simple-git-android-arm-eabi": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm-eabi/-/simple-git-android-arm-eabi-0.1.14.tgz", - "integrity": "sha512-fAJ/Hxc9DhtSHOcB3dPCRW1YcVsqAnbNoOOnHir4aDCtqTP64HrFa7A/675v3vQZpI0u3fXHRcYqW8NF0O/zcg==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm-eabi/-/simple-git-android-arm-eabi-0.1.16.tgz", + "integrity": "sha512-dbrCL0Pl5KZG7x7tXdtVsA5CO6At5ohDX3myf5xIYn9kN4jDFxsocl8bNt6Vb/hZQoJd8fI+k5VlJt+rFhbdVw==", "cpu": [ "arm" ], @@ -552,9 +644,9 @@ } }, "node_modules/@napi-rs/simple-git-android-arm64": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm64/-/simple-git-android-arm64-0.1.14.tgz", - "integrity": "sha512-dav730MRAR142DoyNDafuwKXcUCYwlbxxxxOarDph7bbN0mZZnKHOQohvRCD/Uz4aJLaj6khCavXSjLDWArEUg==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm64/-/simple-git-android-arm64-0.1.16.tgz", + "integrity": "sha512-xYz+TW5J09iK8SuTAKK2D5MMIsBUXVSs8nYp7HcMi8q6FCRO7yJj96YfP9PvKsc/k64hOyqGmL5DhCzY9Cu1FQ==", "cpu": [ "arm64" ], @@ -567,9 +659,9 @@ } }, "node_modules/@napi-rs/simple-git-darwin-arm64": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-arm64/-/simple-git-darwin-arm64-0.1.14.tgz", - "integrity": "sha512-f6+DqRnI+vFvnsAyw66mWhwl0vw1TOieHV07hvKbg4PU5j+RBI+lVqwY2L+IEAxDFlPirTWKKvGY1Lr7M/yi/A==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-arm64/-/simple-git-darwin-arm64-0.1.16.tgz", + "integrity": "sha512-XfgsYqxhUE022MJobeiX563TJqyQyX4FmYCnqrtJwAfivESVeAJiH6bQIum8dDEYMHXCsG7nL8Ok0Dp8k2m42g==", "cpu": [ "arm64" ], @@ -582,9 +674,9 @@ } }, "node_modules/@napi-rs/simple-git-darwin-x64": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-x64/-/simple-git-darwin-x64-0.1.14.tgz", - "integrity": "sha512-x/EnwJdDWJAFay8TQt09byJoBlVZhPEaTAPmRR0fUPzWTjrr28bOy8UW1ysszd9ylBxlyIhuWjOHMHu9CBigTQ==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-x64/-/simple-git-darwin-x64-0.1.16.tgz", + "integrity": "sha512-tkEVBhD6vgRCbeWsaAQqM3bTfpIVGeitamPPRVSbsq8qgzJ5Dx6ZedH27R7KSsA/uao7mZ3dsrNLXbu1Wy5MzA==", "cpu": [ "x64" ], @@ -597,9 +689,9 @@ } }, "node_modules/@napi-rs/simple-git-linux-arm-gnueabihf": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm-gnueabihf/-/simple-git-linux-arm-gnueabihf-0.1.14.tgz", - "integrity": "sha512-k0JZaXBl031gP5VOnoMa1I3lCHlBG7QvtunX5rxnRjx2kZ+JgUyT12s/qle/4xkJ0MnmfKTeiD7hs4Cc4Z3Tzw==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm-gnueabihf/-/simple-git-linux-arm-gnueabihf-0.1.16.tgz", + "integrity": "sha512-R6VAyNnp/yRaT7DV1Ao3r67SqTWDa+fNq2LrNy0Z8gXk2wB9ZKlrxFtLPE1WSpWknWtyRDLpRlsorh7Evk7+7w==", "cpu": [ "arm" ], @@ -612,9 +704,9 @@ } }, "node_modules/@napi-rs/simple-git-linux-arm64-gnu": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-gnu/-/simple-git-linux-arm64-gnu-0.1.14.tgz", - "integrity": "sha512-CsmKP6tSIxau10ZKxV1Q1kem2QcJ/Qlov7pxp1Q7kMErcouW0H6vliVniewicaXRVDYV9wK18iD2t5GoJttwlA==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-gnu/-/simple-git-linux-arm64-gnu-0.1.16.tgz", + "integrity": "sha512-LAGI0opFKw/HBMCV2qIBK3uWSEW9h4xd2ireZKLJy8DBPymX6NrWIamuxYNyCuACnFdPRxR4LaRFy4J5ZwuMdw==", "cpu": [ "arm64" ], @@ -627,9 +719,9 @@ } }, "node_modules/@napi-rs/simple-git-linux-arm64-musl": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-musl/-/simple-git-linux-arm64-musl-0.1.14.tgz", - "integrity": "sha512-krfEckZQ3myoHwmGmqY0aHBnqAzzV66+jFNLQEKaVMSGsXA2P+UcGo0coGzmB13rFRWC2eZpZRNB3MrfrStHkw==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-musl/-/simple-git-linux-arm64-musl-0.1.16.tgz", + "integrity": "sha512-I57Ph0F0Yn2KW93ep+V1EzKhACqX0x49vvSiapqIsdDA2PifdEWLc1LJarBolmK7NKoPqKmf6lAKKO9lhiZzkg==", "cpu": [ "arm64" ], @@ -642,9 +734,9 @@ } }, "node_modules/@napi-rs/simple-git-linux-x64-gnu": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-gnu/-/simple-git-linux-x64-gnu-0.1.14.tgz", - "integrity": "sha512-4T2Q2QdO6t3OawkwdVmdqLz2EH8lfAw2cMT/zdjfTMfhNKjJgSg3kTgRnu/tf8TLCb+wu80fFvafwE0laB2VTQ==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-gnu/-/simple-git-linux-x64-gnu-0.1.16.tgz", + "integrity": "sha512-AZYYFY2V7hlcQASPEOWyOa3e1skzTct9QPzz0LiDM3f/hCFY/wBaU2M6NC5iG3d2Kr38heuyFS/+JqxLm5WaKA==", "cpu": [ "x64" ], @@ -657,9 +749,9 @@ } }, "node_modules/@napi-rs/simple-git-linux-x64-musl": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-musl/-/simple-git-linux-x64-musl-0.1.14.tgz", - "integrity": "sha512-RaTGW8u+RXJbfRF4QN2Dcr5r5DrFh4wLjOvFeOy7sGX3Q9m3IKuw5AjRxTJqIw6xD/AAPKKNzOvPjrIF7728Lw==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-musl/-/simple-git-linux-x64-musl-0.1.16.tgz", + "integrity": "sha512-9TyMcYSBJwjT8jwjY9m24BZbu7ozyWTjsmYBYNtK3B0Um1Ov6jthSNneLVvouQ6x+k3Ow+00TiFh6bvmT00r8g==", "cpu": [ "x64" ], @@ -672,9 +764,9 @@ } }, "node_modules/@napi-rs/simple-git-win32-arm64-msvc": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-arm64-msvc/-/simple-git-win32-arm64-msvc-0.1.14.tgz", - "integrity": "sha512-kb9bKG9t79HJMuRMqbUJFLfWRf952O2Ea4VFwoRA2d/Uwtowm85Ol3JV9E6oeurguRLqdMLrUKyduCW6Hc9Jsg==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-arm64-msvc/-/simple-git-win32-arm64-msvc-0.1.16.tgz", + "integrity": "sha512-uslJ1WuAHCYJWui6xjsyT47SjX6KOHDtClmNO8hqKz1pmDSNY7AjyUY8HxvD1lK9bDnWwc4JYhikS9cxCqHybw==", "cpu": [ "arm64" ], @@ -687,9 +779,9 @@ } }, "node_modules/@napi-rs/simple-git-win32-x64-msvc": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-x64-msvc/-/simple-git-win32-x64-msvc-0.1.14.tgz", - "integrity": "sha512-3835xy0e2gOaZ3SPt1pINBFSBBL3dOx3cChyAzQU0TnMU4Ye/YOh1qa5pO7BOJlCSnOh7iWt782blxCT0HH61w==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-x64-msvc/-/simple-git-win32-x64-msvc-0.1.16.tgz", + "integrity": "sha512-SoEaVeCZCDF1MP+M9bMSXsZWgEjk4On9GWADO5JOulvzR1bKjk0s9PMHwe/YztR9F0sJzrCxwtvBZowhSJsQPg==", "cpu": [ "x64" ], @@ -742,10 +834,18 @@ "node": ">=14" } }, + "node_modules/@shikijs/core": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.10.3.tgz", + "integrity": "sha512-D45PMaBaeDHxww+EkcDQtDAtzv00Gcsp72ukBtaLSmqRvh0WgGMq3Al0rl1QQBZfuneO75NXMIzEZGFitThWbg==", + "dependencies": { + "@types/hast": "^3.0.4" + } + }, "node_modules/@sindresorhus/merge-streams": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-1.0.0.tgz", - "integrity": "sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", "engines": { "node": ">=18" }, @@ -1080,20 +1180,20 @@ "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, "node_modules/@types/nlcst": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-1.0.0.tgz", - "integrity": "sha512-3TGCfOcy8R8mMQ4CNSNOe3PG66HttvjcLzCoOpvXvDtfWOTi+uT/rxeOKm/qEwbM4SNe1O/PjdiBK2YcTjU4OQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz", + "integrity": "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==", "dependencies": { "@types/unist": "*" } }, "node_modules/@types/node": { - "version": "20.11.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.14.tgz", - "integrity": "sha512-w3yWCcwULefjP9DmDDsgUskrMoOy5Z8MiwKHr1FvqGPtx7CvJzQvxD7eKpxNtklQxLruxSXWddyeRtyud0RcXQ==", + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", + "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==", "dev": true, "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.13.0" } }, "node_modules/@types/pretty-time": { @@ -1117,9 +1217,9 @@ "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" }, "node_modules/@types/ws": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", - "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", "dev": true, "dependencies": { "@types/node": "*" @@ -1208,9 +1308,9 @@ } }, "node_modules/async-mutex": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", - "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", "dependencies": { "tslib": "^2.4.0" } @@ -1234,6 +1334,25 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/bidi-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", @@ -1269,6 +1388,29 @@ "node": ">=8" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -1339,15 +1481,9 @@ } }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -1360,10 +1496,18 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } }, + "node_modules/citeproc": { + "version": "2.4.63", + "resolved": "https://registry.npmjs.org/citeproc/-/citeproc-2.4.63.tgz", + "integrity": "sha512-68F95Bp4UbgZU/DBUGQn0qV3HDZLCdI9+Bb2ByrTaNJDL5VEm9LqaiNaxljsvoaExSLEXe1/r6n2Z06SCzW3/Q==" + }, "node_modules/cli-spinner": { "version": "0.2.10", "resolved": "https://registry.npmjs.org/cli-spinner/-/cli-spinner-0.2.10.tgz", @@ -1495,6 +1639,14 @@ "node": ">= 0.6" } }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1532,9 +1684,9 @@ } }, "node_modules/d3": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz", - "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", "dependencies": { "d3-array": "3", "d3-axis": "3", @@ -2052,9 +2204,9 @@ } }, "node_modules/esbuild-sass-plugin": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.16.0.tgz", - "integrity": "sha512-mGCe9MxNYvZ+j77Q/QFO+rwUGA36mojDXkOhtVmoyz1zwYbMaNrtVrmXwwYDleS/UMKTNU3kXuiTtPiAD3K+Pw==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.16.1.tgz", + "integrity": "sha512-mBB2aEF0xk7yo+Q9pSUh8xYED/1O2wbAM6IauGkDrqy6pl9SbJNakLeLGXiNpNujWIudu8TJTZCv2L5AQYRXtA==", "dependencies": { "resolve": "^1.22.6", "sass": "^1.7.3" @@ -2170,6 +2322,52 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/fetch-ponyfill": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/fetch-ponyfill/-/fetch-ponyfill-7.1.0.tgz", + "integrity": "sha512-FhbbL55dj/qdVO3YNK7ZEkshvj3eQ7EuIGV2I6ic/2YiocvyWv+7jg2s4AyS0wdRU75s3tA8ZxI/xPigb0v5Aw==", + "dependencies": { + "node-fetch": "~2.6.1" + } + }, + "node_modules/fetch-ponyfill/node_modules/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/fetch-ponyfill/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/fetch-ponyfill/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/fetch-ponyfill/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -2187,9 +2385,9 @@ "integrity": "sha512-c5o/+Um8aqCSOXGcZoqZOm+NqtVwNsvVpWv6lfmSclU954O3wvQKxxK8zj74fPaSJbXpSLTs4PRhh+wnoCXnKg==" }, "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -2252,9 +2450,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", - "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.5.tgz", + "integrity": "sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==", "dev": true, "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -2269,21 +2467,22 @@ "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==" }, "node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2301,11 +2500,11 @@ } }, "node_modules/globby": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.0.tgz", - "integrity": "sha512-/1WM/LNHRAOH9lZta77uGbq0dAEQM+XjNesWwhlERDVenqothRbnzTrL3/LrIoEPPjeUHC3vrS6TwoyxeHs7MQ==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", "dependencies": { - "@sindresorhus/merge-streams": "^1.0.0", + "@sindresorhus/merge-streams": "^2.1.0", "fast-glob": "^3.3.2", "ignore": "^5.2.4", "path-type": "^5.0.0", @@ -2530,9 +2729,9 @@ "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" }, "node_modules/hast-util-to-html": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.0.tgz", - "integrity": "sha512-IVGhNgg7vANuUA2XKrT6sOIIPgaYZnmLx3l/CCOAK0PtgfoHrZwX7jCSYyFxHTrGmC6S9q8aQQekjp4JPZF+cw==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.1.tgz", + "integrity": "sha512-hZOofyZANbyWo+9RP75xIDV/gq+OUKx+T46IlwERnKmfpwp81XBFbT9mi26ws+SJchA4RVUQwIBJpqEOBhMzEQ==", "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", @@ -2746,6 +2945,25 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -2816,28 +3034,6 @@ "node": ">=8" } }, - "node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "engines": { - "node": ">=4" - } - }, "node_modules/is-core-module": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", @@ -2932,14 +3128,14 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", + "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", "dependencies": { "@isaacs/cliui": "^8.0.2" }, "engines": { - "node": ">=14" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3030,9 +3226,9 @@ } }, "node_modules/lightningcss": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.23.0.tgz", - "integrity": "sha512-SEArWKMHhqn/0QzOtclIwH5pXIYQOUEkF8DgICd/105O+GCgd7jxjNod/QPnBCSWvpRHQBGVz5fQ9uScby03zA==", + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.25.1.tgz", + "integrity": "sha512-V0RMVZzK1+rCHpymRv4URK2lNhIRyO8g7U7zOFwVAhJuat74HtkjIQpQRKNCwFEYkRGpafOpmXXLoaoBcyVtBg==", "dependencies": { "detect-libc": "^1.0.3" }, @@ -3044,21 +3240,21 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-darwin-arm64": "1.23.0", - "lightningcss-darwin-x64": "1.23.0", - "lightningcss-freebsd-x64": "1.23.0", - "lightningcss-linux-arm-gnueabihf": "1.23.0", - "lightningcss-linux-arm64-gnu": "1.23.0", - "lightningcss-linux-arm64-musl": "1.23.0", - "lightningcss-linux-x64-gnu": "1.23.0", - "lightningcss-linux-x64-musl": "1.23.0", - "lightningcss-win32-x64-msvc": "1.23.0" + "lightningcss-darwin-arm64": "1.25.1", + "lightningcss-darwin-x64": "1.25.1", + "lightningcss-freebsd-x64": "1.25.1", + "lightningcss-linux-arm-gnueabihf": "1.25.1", + "lightningcss-linux-arm64-gnu": "1.25.1", + "lightningcss-linux-arm64-musl": "1.25.1", + "lightningcss-linux-x64-gnu": "1.25.1", + "lightningcss-linux-x64-musl": "1.25.1", + "lightningcss-win32-x64-msvc": "1.25.1" } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.23.0.tgz", - "integrity": "sha512-kl4Pk3Q2lnE6AJ7Qaij47KNEfY2/UXRZBT/zqGA24B8qwkgllr/j7rclKOf1axcslNXvvUdztjo4Xqh39Yq1aA==", + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.25.1.tgz", + "integrity": "sha512-G4Dcvv85bs5NLENcu/s1f7ehzE3D5ThnlWSDwE190tWXRQCQaqwcuHe+MGSVI/slm0XrxnaayXY+cNl3cSricw==", "cpu": [ "arm64" ], @@ -3075,9 +3271,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.23.0.tgz", - "integrity": "sha512-KeRFCNoYfDdcolcFXvokVw+PXCapd2yHS1Diko1z1BhRz/nQuD5XyZmxjWdhmhN/zj5sH8YvWsp0/lPLVzqKpg==", + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.25.1.tgz", + "integrity": "sha512-dYWuCzzfqRueDSmto6YU5SoGHvZTMU1Em9xvhcdROpmtOQLorurUZz8+xFxZ51lCO2LnYbfdjZ/gCqWEkwixNg==", "cpu": [ "x64" ], @@ -3094,9 +3290,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.23.0.tgz", - "integrity": "sha512-xhnhf0bWPuZxcqknvMDRFFo2TInrmQRWZGB0f6YoAsZX8Y+epfjHeeOIGCfAmgF0DgZxHwYc8mIR5tQU9/+ROA==", + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.25.1.tgz", + "integrity": "sha512-hXoy2s9A3KVNAIoKz+Fp6bNeY+h9c3tkcx1J3+pS48CqAt+5bI/R/YY4hxGL57fWAIquRjGKW50arltD6iRt/w==", "cpu": [ "x64" ], @@ -3113,9 +3309,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.23.0.tgz", - "integrity": "sha512-fBamf/bULvmWft9uuX+bZske236pUZEoUlaHNBjnueaCTJ/xd8eXgb0cEc7S5o0Nn6kxlauMBnqJpF70Bgq3zg==", + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.25.1.tgz", + "integrity": "sha512-tWyMgHFlHlp1e5iW3EpqvH5MvsgoN7ZkylBbG2R2LWxnvH3FuWCJOhtGcYx9Ks0Kv0eZOBud789odkYLhyf1ng==", "cpu": [ "arm" ], @@ -3132,9 +3328,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.23.0.tgz", - "integrity": "sha512-RS7sY77yVLOmZD6xW2uEHByYHhQi5JYWmgVumYY85BfNoVI3DupXSlzbw+b45A9NnVKq45+oXkiN6ouMMtTwfg==", + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.25.1.tgz", + "integrity": "sha512-Xjxsx286OT9/XSnVLIsFEDyDipqe4BcLeB4pXQ/FEA5+2uWCCuAEarUNQumRucnj7k6ftkAHUEph5r821KBccQ==", "cpu": [ "arm64" ], @@ -3151,9 +3347,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.23.0.tgz", - "integrity": "sha512-cU00LGb6GUXCwof6ACgSMKo3q7XYbsyTj0WsKHLi1nw7pV0NCq8nFTn6ZRBYLoKiV8t+jWl0Hv8KkgymmK5L5g==", + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.25.1.tgz", + "integrity": "sha512-IhxVFJoTW8wq6yLvxdPvyHv4NjzcpN1B7gjxrY3uaykQNXPHNIpChLB52+wfH+yS58zm1PL4LemUp8u9Cfp6Bw==", "cpu": [ "arm64" ], @@ -3170,9 +3366,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.23.0.tgz", - "integrity": "sha512-q4jdx5+5NfB0/qMbXbOmuC6oo7caPnFghJbIAV90cXZqgV8Am3miZhC4p+sQVdacqxfd+3nrle4C8icR3p1AYw==", + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.25.1.tgz", + "integrity": "sha512-RXIaru79KrREPEd6WLXfKfIp4QzoppZvD3x7vuTKkDA64PwTzKJ2jaC43RZHRt8BmyIkRRlmywNhTRMbmkPYpA==", "cpu": [ "x64" ], @@ -3189,9 +3385,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.23.0.tgz", - "integrity": "sha512-G9Ri3qpmF4qef2CV/80dADHKXRAQeQXpQTLx7AiQrBYQHqBjB75oxqj06FCIe5g4hNCqLPnM9fsO4CyiT1sFSQ==", + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.25.1.tgz", + "integrity": "sha512-TdcNqFsAENEEFr8fJWg0Y4fZ/nwuqTRsIr7W7t2wmDUlA8eSXVepeeONYcb+gtTj1RaXn/WgNLB45SFkz+XBZA==", "cpu": [ "x64" ], @@ -3208,9 +3404,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.23.0.tgz", - "integrity": "sha512-1rcBDJLU+obPPJM6qR5fgBUiCdZwZLafZM5f9kwjFLkb/UBNIzmae39uCSmh71nzPCTXZqHbvwu23OWnWEz+eg==", + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.25.1.tgz", + "integrity": "sha512-9KZZkmmy9oGDSrnyHuxP6iMhbsgChUiu/NSgOx+U1I/wTngBStDf2i2aGRCHvFqj19HqqBEI4WuGVQBa2V6e0A==", "cpu": [ "x64" ], @@ -3236,11 +3432,11 @@ } }, "node_modules/lru-cache": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", - "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", + "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", "engines": { - "node": "14 || >=16.14" + "node": "20 || >=22" } }, "node_modules/markdown-table": { @@ -3610,9 +3806,9 @@ } }, "node_modules/mdast-util-to-hast": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz", - "integrity": "sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==", + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", @@ -4288,23 +4484,23 @@ } }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "engines": { "node": ">=16 || 14 >=14.17" } @@ -4314,23 +4510,66 @@ "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz", "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==" }, + "node_modules/moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nlcst-to-string": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-3.1.1.tgz", - "integrity": "sha512-63mVyqaqt0cmn2VcI2aH6kxe1rLAmSROqHMA0i4qqg1tidkfExgpb0FGMikMCn86mw5dFtBtEANfmSSK7TjNHw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz", + "integrity": "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==", "dependencies": { - "@types/nlcst": "^1.0.0" + "@types/nlcst": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4339,24 +4578,10 @@ "node": ">=0.10.0" } }, - "node_modules/parse-entities": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", - "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" }, "node_modules/parse-entities": { "version": "4.0.1", @@ -4378,19 +4603,27 @@ } }, "node_modules/parse-latin": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-5.0.1.tgz", - "integrity": "sha512-b/K8ExXaWC9t34kKeDV8kGXBkXZ1HCSAZRYE7HR14eA1GlXX5L8iWhs8USJNhQU9q5ci413jCKF0gOyovvyRBg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", + "integrity": "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==", "dependencies": { - "nlcst-to-string": "^3.0.0", - "unist-util-modify-children": "^3.0.0", - "unist-util-visit-children": "^2.0.0" + "@types/nlcst": "^2.0.0", + "@types/unist": "^3.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-modify-children": "^4.0.0", + "unist-util-visit-children": "^3.0.0", + "vfile": "^6.0.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/parse-latin/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, "node_modules/parse-numeric-range": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", @@ -4426,15 +4659,15 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -4473,29 +4706,26 @@ } }, "node_modules/preact": { - "version": "10.19.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.19.3.tgz", - "integrity": "sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==", + "version": "10.22.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.22.1.tgz", + "integrity": "sha512-jRYbDDgMpIb5LHq3hkI0bbl+l/TQ9UnkdQ0ww+lp+4MMOdqaUYdFc5qeyP+IV8FAd/2Em7drVPeKdQxsiWCf/A==", "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" } }, "node_modules/preact-render-to-string": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.3.1.tgz", - "integrity": "sha512-NQ28WrjLtWY6lKDlTxnFpKHZdpjfF+oE6V4tZ0rTrunHrtZp6Dm0oFrcJalt/5PNeqJz4j1DuZDS0Y6rCBoqDA==", - "dependencies": { - "pretty-format": "^3.8.0" - }, + "version": "6.5.7", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.7.tgz", + "integrity": "sha512-nACZDdv/ZZciuldVYMcfGqr61DKJeaAfPx96hn6OXoBGhgtU2yGQkA0EpTzWH4SvnwF0syLsL4WK7AIp3Ruc1g==", "peerDependencies": { "preact": ">=10" } }, "node_modules/prettier": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", - "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -4518,11 +4748,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pretty-format": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", - "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" - }, "node_modules/pretty-time": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz", @@ -4615,93 +4840,25 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/rehype-katex": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.0.tgz", - "integrity": "sha512-h8FPkGE00r2XKU+/acgqwWUlyzve1IiOKwsEkg4pDL3k48PiE0Pt+/uLtVHDVkN1yA4iurZN6UES8ivHVEQV6Q==", + "node_modules/rehype-citation": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/rehype-citation/-/rehype-citation-2.0.0.tgz", + "integrity": "sha512-rGawTBI8SJA1Y4IRyROvpYF6oXBVNFXlJYHIJ2jJH3HgeuCbAC9AO8wE/NMPLDOPQ8+Q8QkZm93fKsnUNbvwZA==", "dependencies": { - "@types/hast": "^3.0.0", - "@types/katex": "^0.16.0", - "hast-util-from-html-isomorphic": "^2.0.0", - "hast-util-to-text": "^4.0.0", - "katex": "^0.16.0", - "unist-util-visit-parents": "^6.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-katex/node_modules/@types/unist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" - }, - "node_modules/rehype-katex/node_modules/unist-util-is": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", - "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-katex/node_modules/unist-util-visit-parents": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", - "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-mathjax": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/rehype-mathjax/-/rehype-mathjax-5.0.0.tgz", - "integrity": "sha512-IRPgpSpwOq4JNn3efeTrbYDMmzjOKCTJtu1Wyo/+6nenyqwqIvlojYDczRILOeHa1HyCMYmqpdvfOovOVzDIGg==", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mathjax": "^0.0.37", + "@citation-js/core": "^0.7.1", + "@citation-js/date": "^0.5.1", + "@citation-js/name": "^0.4.2", + "@citation-js/plugin-bibjson": "^0.7.2", + "@citation-js/plugin-bibtex": "^0.7.2", + "@citation-js/plugin-csl": "^0.7.2", + "citeproc": "^2.4.63", + "cross-fetch": "^4.0.0", "hast-util-from-dom": "^5.0.0", - "hast-util-to-text": "^4.0.0", - "jsdom": "^22.0.0", - "mathjax-full": "^3.0.0", - "unified": "^11.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-mathjax/node_modules/@types/unist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" - }, - "node_modules/rehype-autolink-headings": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-7.1.0.tgz", - "integrity": "sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==", - "dependencies": { - "@types/hast": "^3.0.0", - "@ungap/structured-clone": "^1.0.0", - "hast-util-heading-rank": "^3.0.0", - "hast-util-is-element": "^3.0.0", + "hast-util-from-parse5": "^8.0.1", + "js-yaml": "^4.1.0", + "parse5": "^7.1.2", "unified": "^11.0.0", "unist-util-visit": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" } }, "node_modules/rehype-katex": { @@ -4816,11 +4973,11 @@ } }, "node_modules/rehype-pretty-code": { - "version": "0.12.6", - "resolved": "https://registry.npmjs.org/rehype-pretty-code/-/rehype-pretty-code-0.12.6.tgz", - "integrity": "sha512-AW18s4eXwnb4PGwL0Y8BoUzBJr23epWNXndCKaZ52S4kl/4tsgM+406oCp5NdtPZsB0ItpaY+hCMv3kw58DLrA==", + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/rehype-pretty-code/-/rehype-pretty-code-0.13.2.tgz", + "integrity": "sha512-F+PaFMscfJOcSHcR2b//+hk/0jT56hmGDqXcVD6VC9j0CUSGiqv8YxaWUyhR7qEIRRSbzAVxx+0uxzk+akXs+w==", "dependencies": { - "@types/hast": "^3.0.3", + "@types/hast": "^3.0.4", "hast-util-to-string": "^3.0.0", "parse-numeric-range": "^1.3.0", "rehype-parse": "^9.0.0", @@ -4831,7 +4988,7 @@ "node": ">=18" }, "peerDependencies": { - "shikiji": "^0.7.0 || ^0.8.0 || ^0.9.0 || ^0.10.0" + "shiki": "^1.3.0" } }, "node_modules/rehype-raw": { @@ -4972,30 +5129,17 @@ } }, "node_modules/remark-smartypants": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-2.0.0.tgz", - "integrity": "sha512-Rc0VDmr/yhnMQIz8n2ACYXlfw/P/XZev884QU1I5u+5DgJls32o97Vc1RbK3pfumLsJomS2yy8eT4Fxj/2MDVA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.2.tgz", + "integrity": "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==", "dependencies": { - "retext": "^8.1.0", - "retext-smartypants": "^5.1.0", - "unist-util-visit": "^4.1.0" + "retext": "^9.0.0", + "retext-smartypants": "^6.0.0", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/remark-smartypants/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "node": ">=16.0.0" } }, "node_modules/remark-stringify": { @@ -5059,14 +5203,14 @@ } }, "node_modules/retext": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/retext/-/retext-8.1.0.tgz", - "integrity": "sha512-N9/Kq7YTn6ZpzfiGW45WfEGJqFf1IM1q8OsRa1CGzIebCJBNCANDRmOrholiDRGKo/We7ofKR4SEvcGAWEMD3Q==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", + "integrity": "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==", "dependencies": { - "@types/nlcst": "^1.0.0", - "retext-latin": "^3.0.0", - "retext-stringify": "^3.0.0", - "unified": "^10.0.0" + "@types/nlcst": "^2.0.0", + "retext-latin": "^4.0.0", + "retext-stringify": "^4.0.0", + "unified": "^11.0.0" }, "funding": { "type": "opencollective", @@ -5074,14 +5218,13 @@ } }, "node_modules/retext-latin": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/retext-latin/-/retext-latin-3.1.0.tgz", - "integrity": "sha512-5MrD1tuebzO8ppsja5eEu+ZbBeUNCjoEarn70tkXOS7Bdsdf6tNahsv2bY0Z8VooFF6cw7/6S+d3yI/TMlMVVQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-latin/-/retext-latin-4.0.0.tgz", + "integrity": "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==", "dependencies": { - "@types/nlcst": "^1.0.0", - "parse-latin": "^5.0.0", - "unherit": "^3.0.0", - "unified": "^10.0.0" + "@types/nlcst": "^2.0.0", + "parse-latin": "^7.0.0", + "unified": "^11.0.0" }, "funding": { "type": "opencollective", @@ -5122,14 +5265,13 @@ } }, "node_modules/retext-smartypants": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-5.2.0.tgz", - "integrity": "sha512-Do8oM+SsjrbzT2UNIKgheP0hgUQTDDQYyZaIY3kfq0pdFzoPk+ZClYJ+OERNXveog4xf1pZL4PfRxNoVL7a/jw==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.1.0.tgz", + "integrity": "sha512-LDPXg95346bqFZnDMHo0S7Rq5p64+B+N8Vz733+wPMDtwb9rCOs9LIdIEhrUOU+TAywX9St+ocQWJt8wrzivcQ==", "dependencies": { - "@types/nlcst": "^1.0.0", - "nlcst-to-string": "^3.0.0", - "unified": "^10.0.0", - "unist-util-visit": "^4.0.0" + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-visit": "^5.0.0" }, "funding": { "type": "opencollective", @@ -5184,13 +5326,13 @@ } }, "node_modules/retext-stringify": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/retext-stringify/-/retext-stringify-3.1.0.tgz", - "integrity": "sha512-767TLOaoXFXyOnjx/EggXlb37ZD2u4P1n0GJqVdpipqACsQP+20W+BNpMYrlJkq7hxffnFk+jc6mAK9qrbuB8w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-stringify/-/retext-stringify-4.0.0.tgz", + "integrity": "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==", "dependencies": { - "@types/nlcst": "^1.0.0", - "nlcst-to-string": "^3.0.0", - "unified": "^10.0.0" + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unified": "^11.0.0" }, "funding": { "type": "opencollective", @@ -5339,22 +5481,23 @@ } }, "node_modules/rfdc": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", - "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" }, "node_modules/rimraf": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", - "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", "dependencies": { - "glob": "^10.3.7" + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" }, "bin": { "rimraf": "dist/esm/bin.mjs" }, "engines": { - "node": ">=14" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -5495,12 +5638,13 @@ "node": ">=8" } }, - "node_modules/shikiji": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/shikiji/-/shikiji-0.10.2.tgz", - "integrity": "sha512-wtZg3T0vtYV2PnqusWQs3mDaJBdCPWxFDrBM/SE5LfrX92gjUvfEMlc+vJnoKY6Z/S44OWaCRzNIsdBRWcTAiw==", + "node_modules/shiki": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.10.3.tgz", + "integrity": "sha512-eneCLncGuvPdTutJuLyUGS8QNPAVFO5Trvld2wgEq1e002mwctAhJKeMGWtWVXOIEzmlcLRqcgPSorR6AVzOmQ==", "dependencies": { - "shikiji-core": "0.10.2" + "@shikijs/core": "1.10.3", + "@types/hast": "^3.0.4" } }, "node_modules/shikiji-core": { @@ -5728,6 +5872,18 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, + "node_modules/sync-fetch": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.4.5.tgz", + "integrity": "sha512-esiWJ7ixSKGpd9DJPBTC4ckChqdOjIwJfYhVHkcQ2Gnm41323p1TRmEI+esTQ9ppD+b5opps2OTEGTCGX5kF+g==", + "dependencies": { + "buffer": "^5.7.1", + "node-fetch": "^2.6.1" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5779,14 +5935,14 @@ } }, "node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", "dependencies": { - "punycode": "^2.3.0" + "punycode": "^2.3.1" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/tr46/node_modules/punycode": { @@ -5821,13 +5977,13 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsx": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.6.2.tgz", - "integrity": "sha512-QPpBdJo+ZDtqZgAnq86iY/PD2KYCUPSUGIunHdGwyII99GKH+f3z3FZ8XNFLSGQIA4I365ui8wnQpl8OKLqcsg==", + "version": "4.16.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.16.2.tgz", + "integrity": "sha512-C1uWweJDgdtX2x600HjaFaucXTilT7tgUZHbOE4+ypskZ1OP8CRCSDkCxG6Vya9EwaFIVagWwpaVAn5wzypaqQ==", "dev": true, "dependencies": { - "esbuild": "~0.18.20", - "get-tsconfig": "^4.7.2" + "esbuild": "~0.21.5", + "get-tsconfig": "^4.7.5" }, "bin": { "tsx": "dist/cli.mjs" @@ -5839,10 +5995,26 @@ "fsevents": "~2.3.3" } }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/tsx/node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], @@ -5856,9 +6028,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], @@ -5872,9 +6044,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], @@ -5888,9 +6060,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], @@ -5904,9 +6076,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], @@ -5920,9 +6092,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], @@ -5936,9 +6108,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], @@ -5952,9 +6124,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], @@ -5968,9 +6140,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], @@ -5984,9 +6156,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], @@ -6000,9 +6172,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], @@ -6016,9 +6188,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], @@ -6032,9 +6204,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], @@ -6047,22 +6219,26 @@ "node": ">=12" } }, - "node_modules/to-vfile": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/to-vfile/-/to-vfile-8.0.0.tgz", - "integrity": "sha512-IcmH1xB5576MJc9qcfEC/m/nQCFt3fzMHz45sSlgJyTWjRbKW1HAkJpuf3DgE57YzIlZcwcBZA5ENQbBo4aLkg==", - "dependencies": { - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, "node_modules/tsx/node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], @@ -6076,9 +6252,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], @@ -6091,37 +6267,58 @@ "node": ">=12" } }, - "node_modules/tough-cookie/node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], "engines": { "node": ">=12" } }, - "node_modules/tr46": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", - "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", - "dependencies": { - "punycode": "^2.3.1" - }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/tr46/node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], "engines": { "node": ">=12" } }, "node_modules/tsx/node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], @@ -6135,9 +6332,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], @@ -6151,9 +6348,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], @@ -6166,29 +6363,48 @@ "node": ">=12" } }, - "node_modules/tsx": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.0.tgz", - "integrity": "sha512-I+t79RYPlEYlHn9a+KzwrvEwhJg35h/1zHsLC2JXvhC2mdynMv6Zxzvhv5EMV6VF5qJlLlkSnMVvdZV3PSIGcg==", + "node_modules/tsx/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, - "dependencies": { - "esbuild": "~0.19.10", - "get-tsconfig": "^4.7.2" - }, + "hasInstallScript": true, "bin": { - "tsx": "dist/cli.mjs" + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=18.0.0" + "node": ">=12" }, "optionalDependencies": { - "fsevents": "~2.3.3" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -6199,18 +6415,20 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", + "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==", "dev": true }, - "node_modules/unherit": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/unherit/-/unherit-3.0.1.tgz", - "integrity": "sha512-akOOQ/Yln8a2sgcLj4U0Jmx0R5jpIg2IUyRrWOzmEbjBtGzBdHtSeFKgoEcoH4KYIG/Pb8GQ/BwtYm0GCq1Sqg==", + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "engines": { + "node": ">=18" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/unicorn-magic": { @@ -6277,24 +6495,12 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/unist-util-modify-children": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-3.1.1.tgz", - "integrity": "sha512-yXi4Lm+TG5VG+qvokP6tpnk+r1EPwyYL04JWDxLvgvPV40jANh7nm3udk65OOWquvbMDe+PL9+LmkxDpTv/7BA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-4.0.0.tgz", + "integrity": "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==", "dependencies": { - "@types/unist": "^2.0.0", + "@types/unist": "^3.0.0", "array-iterate": "^2.0.0" }, "funding": { @@ -6302,6 +6508,11 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-modify-children/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, "node_modules/unist-util-position": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", @@ -6337,18 +6548,6 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" }, - "node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/unist-util-visit": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", @@ -6364,24 +6563,46 @@ } }, "node_modules/unist-util-visit-children": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-2.0.2.tgz", - "integrity": "sha512-+LWpMFqyUwLGpsQxpumsQ9o9DG2VGLFrpz+rpVXYIEdPy57GSy5HioC0g3bg/8WP9oCLlapQtklOzQ8uLS496Q==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-3.0.0.tgz", + "integrity": "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==", "dependencies": { - "@types/unist": "^2.0.0" + "@types/unist": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "node_modules/unist-util-visit-children/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/unist-util-visit/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/unist-util-visit/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" }, "funding": { "type": "opencollective", @@ -6436,9 +6657,9 @@ } }, "node_modules/vfile": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", - "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.2.tgz", + "integrity": "sha512-zND7NlS8rJYb/sPqkb13ZvbbUoExdbi4w3SfRrMq6R3FvnLQmmfpajJNITuuYm6AZ5uao9vy4BAos3EXBPf2rg==", "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0", @@ -6467,19 +6688,6 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" }, - "node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/vfile/node_modules/@types/unist": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", @@ -6589,9 +6797,9 @@ "integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==" }, "node_modules/workerpool": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.1.0.tgz", - "integrity": "sha512-+wRWfm9yyJghvXLSHMQj3WXDxHbibHAQmRrWbqKBfy0RjftZNeQaW+Std5bSYc83ydkrxoPTPOWVlXUR9RWJdQ==" + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.1.3.tgz", + "integrity": "sha512-LhUrk4tbxJRDQmRrrFWA9EnboXI79fe0ZNTy3u8m+dqPN1EkVSIsQYAB8OF/fkyhG8Rtup+c/bzj/+bzbG8fqg==" }, "node_modules/wrap-ansi": { "version": "8.1.0", @@ -6675,9 +6883,9 @@ } }, "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index 05c6a450..3800706d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@jackyzha0/quartz", "description": "🌱 publish your digital garden and notes as a website", "private": true, - "version": "4.2.2", + "version": "4.3.0", "type": "module", "author": "jackyzha0 ", "license": "MIT", @@ -12,15 +12,16 @@ "url": "https://github.com/jackyzha0/quartz.git" }, "scripts": { + "quartz": "./quartz/bootstrap-cli.mjs", "docs": "npx quartz build --serve -d docs", "check": "tsc --noEmit && npx prettier . --check", "format": "npx prettier . --write", - "test": "tsx ./quartz/util/path.test.ts", + "test": "tsx ./quartz/util/path.test.ts && tsx ./quartz/depgraph.test.ts", "profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1" }, "engines": { "npm": ">=9.3.1", - "node": ">=18.14" + "node": "20 || >=22" }, "keywords": [ "site generator", @@ -35,37 +36,38 @@ }, "dependencies": { "@clack/prompts": "^0.7.0", - "@floating-ui/dom": "^1.6.1", - "@napi-rs/simple-git": "0.1.14", - "async-mutex": "^0.4.1", + "@floating-ui/dom": "^1.6.8", + "@napi-rs/simple-git": "0.1.16", + "async-mutex": "^0.5.0", "chalk": "^5.3.0", - "chokidar": "^3.5.3", + "chokidar": "^3.6.0", "cli-spinner": "^0.2.10", - "d3": "^7.8.5", - "esbuild-sass-plugin": "^2.16.0", + "d3": "^7.9.0", + "esbuild-sass-plugin": "^2.16.1", "flexsearch": "0.7.43", "github-slugger": "^2.0.0", - "globby": "^14.0.0", + "globby": "^14.0.2", "gray-matter": "^4.0.3", - "hast-util-to-html": "^9.0.0", + "hast-util-to-html": "^9.0.1", "hast-util-to-jsx-runtime": "^2.3.0", "hast-util-to-string": "^3.0.0", "is-absolute-url": "^4.0.1", "js-yaml": "^4.1.0", - "lightningcss": "^1.23.0", + "lightningcss": "^1.25.1", "mdast-util-find-and-replace": "^3.0.1", - "mdast-util-to-hast": "^13.1.0", + "mdast-util-to-hast": "^13.2.0", "mdast-util-to-string": "^4.0.0", "micromorph": "^0.4.5", - "preact": "^10.19.3", - "preact-render-to-string": "^6.3.1", + "preact": "^10.22.1", + "preact-render-to-string": "^6.5.7", "pretty-bytes": "^6.1.1", "pretty-time": "^1.1.0", "reading-time": "^1.5.0", "rehype-autolink-headings": "^7.1.0", + "rehype-citation": "^2.0.0", "rehype-katex": "^7.0.0", "rehype-mathjax": "^6.0.0", - "rehype-pretty-code": "^0.12.6", + "rehype-pretty-code": "^0.13.2", "rehype-raw": "^7.0.0", "rehype-slug": "^6.0.0", "remark": "^15.0.1", @@ -75,19 +77,19 @@ "remark-math": "^6.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.0", - "remark-smartypants": "^2.0.0", - "rfdc": "^1.3.1", - "rimraf": "^5.0.5", + "remark-smartypants": "^3.0.2", + "rfdc": "^1.4.1", + "rimraf": "^6.0.1", "serve-handler": "^6.1.5", - "shikiji": "^0.10.2", + "shiki": "^1.10.3", "source-map-support": "^0.5.21", "to-vfile": "^8.0.0", "toml": "^3.0.0", "unified": "^11.0.4", "unist-util-visit": "^5.0.0", - "vfile": "^6.0.1", - "workerpool": "^9.1.0", - "ws": "^8.15.1", + "vfile": "^6.0.2", + "workerpool": "^9.1.3", + "ws": "^8.18.0", "yargs": "^17.7.2" }, "devDependencies": { @@ -95,14 +97,14 @@ "@types/d3": "^7.4.3", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", - "@types/node": "^20.11.14", + "@types/node": "^22.1.0", "@types/pretty-time": "^1.1.5", "@types/source-map-support": "^0.5.10", - "@types/ws": "^8.5.10", + "@types/ws": "^8.5.12", "@types/yargs": "^17.0.32", "esbuild": "^0.19.9", - "prettier": "^3.2.4", - "tsx": "^4.7.0", - "typescript": "^5.3.3" + "prettier": "^3.3.3", + "tsx": "^4.16.2", + "typescript": "^5.5.3" } } diff --git a/quartz.config.ts b/quartz.config.ts index 3d53b225..3629f350 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -1,5 +1,11 @@ import { QuartzConfig } from "./quartz/cfg" import * as Plugin from "./quartz/plugins" + +/** + * Quartz 4.0 Configuration + * + * See https://quartz.jzhao.xyz/configuration for more information. + */ const config: QuartzConfig = { configuration: { pageTitle: "Matsuura Tomoya Research Note", @@ -14,6 +20,8 @@ const config: QuartzConfig = { ignorePatterns: ["private", "templates", ".obsidian"], defaultDateType: "modified", theme: { + fontOrigin: "googleFonts", + cdnCaching: true, typography: { header: "Schibsted Grotesk", body: "Source Sans Pro", @@ -29,6 +37,7 @@ const config: QuartzConfig = { secondary: "#207e8f", tertiary: "#84a59d", highlight: "rgba(243,143,51,0.25)", + textHighlight: "#fff23688", }, darkMode: { light: "#161618", @@ -39,6 +48,7 @@ const config: QuartzConfig = { secondary: "#7b97aa", tertiary: "#84a59d", highlight: "rgba(143, 159, 169, 0.15)", + textHighlight: "#b3aa0288", }, }, }, @@ -50,17 +60,24 @@ const config: QuartzConfig = { priority: ["frontmatter", "git",], // you can add 'git' here for last modified from Git but this makes the build slower }), Plugin.Latex({ renderEngine: "katex" }), - Plugin.SyntaxHighlighting(), + Plugin.SyntaxHighlighting({ + theme: { + light: "github-light", + dark: "github-dark", + }, + keepBackground: false, + }), Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }), Plugin.GitHubFlavoredMarkdown(), Plugin.TableOfContents(), Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }), Plugin.Description(), + Plugin.Latex({ renderEngine: "katex" }), ], filters: [Plugin.RemoveDrafts()], emitters: [ Plugin.AliasRedirects(), - Plugin.ComponentResources({ fontOrigin: "googleFonts" }), + Plugin.ComponentResources(), Plugin.ContentPage(), Plugin.FolderPage(), Plugin.TagPage(), diff --git a/quartz.layout.ts b/quartz.layout.ts index a9ce0bcf..7cd0fa42 100644 --- a/quartz.layout.ts +++ b/quartz.layout.ts @@ -5,6 +5,7 @@ import * as Component from "./quartz/components" export const sharedPageComponents: SharedLayout = { head: Component.Head(), header: [], + afterBody: [], footer: Component.Footer({ links: { "Top": "https://matsuuratomoya.com", @@ -63,5 +64,9 @@ export const defaultListPageLayout: PageLayout = { Component.Darkmode(), Component.DesktopOnly(Component.Explorer()), ], - right: [], + right: [ + Component.Graph(), + Component.DesktopOnly(Component.TableOfContents()), + Component.Backlinks(), + ], } \ No newline at end of file diff --git a/quartz/build.ts b/quartz/build.ts index 1f90301e..972a7e85 100644 --- a/quartz/build.ts +++ b/quartz/build.ts @@ -17,6 +17,10 @@ import { glob, toPosixPath } from "./util/glob" import { trace } from "./util/trace" import { options } from "./util/sourcemap" import { Mutex } from "async-mutex" +import DepGraph from "./depgraph" +import { getStaticResourcesFromPlugins } from "./plugins" + +type Dependencies = Record | null> type BuildData = { ctx: BuildCtx @@ -29,8 +33,11 @@ type BuildData = { toRebuild: Set toRemove: Set lastBuildMs: number + dependencies: Dependencies } +type FileEvent = "add" | "change" | "delete" + async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) { const ctx: BuildCtx = { argv, @@ -53,7 +60,7 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) { const release = await mut.acquire() perf.addEvent("clean") - await rimraf(output) + await rimraf(path.join(output, "*"), { glob: true }) console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`) perf.addEvent("glob") @@ -68,12 +75,24 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) { const parsedFiles = await parseMarkdown(ctx, filePaths) const filteredContent = filterContent(ctx, parsedFiles) + + const dependencies: Record | null> = {} + + // Only build dependency graphs if we're doing a fast rebuild + if (argv.fastRebuild) { + const staticResources = getStaticResourcesFromPlugins(ctx) + for (const emitter of cfg.plugins.emitters) { + dependencies[emitter.name] = + (await emitter.getDependencyGraph?.(ctx, filteredContent, staticResources)) ?? null + } + } + await emitContent(ctx, filteredContent) console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`)) release() if (argv.serve) { - return startServing(ctx, mut, parsedFiles, clientRefresh) + return startServing(ctx, mut, parsedFiles, clientRefresh, dependencies) } } @@ -83,9 +102,11 @@ async function startServing( mut: Mutex, initialContent: ProcessedContent[], clientRefresh: () => void, + dependencies: Dependencies, // emitter name: dep graph ) { const { argv } = ctx + // cache file parse results const contentMap = new Map() for (const content of initialContent) { const [_tree, vfile] = content @@ -95,6 +116,7 @@ async function startServing( const buildData: BuildData = { ctx, mut, + dependencies, contentMap, ignored: await isGitIgnored(), initialSlugs: ctx.allSlugs, @@ -110,19 +132,193 @@ async function startServing( ignoreInitial: true, }) + const buildFromEntry = argv.fastRebuild ? partialRebuildFromEntrypoint : rebuildFromEntrypoint watcher - .on("add", (fp) => rebuildFromEntrypoint(fp, "add", clientRefresh, buildData)) - .on("change", (fp) => rebuildFromEntrypoint(fp, "change", clientRefresh, buildData)) - .on("unlink", (fp) => rebuildFromEntrypoint(fp, "delete", clientRefresh, buildData)) + .on("add", (fp) => buildFromEntry(fp, "add", clientRefresh, buildData)) + .on("change", (fp) => buildFromEntry(fp, "change", clientRefresh, buildData)) + .on("unlink", (fp) => buildFromEntry(fp, "delete", clientRefresh, buildData)) return async () => { await watcher.close() } } +async function partialRebuildFromEntrypoint( + filepath: string, + action: FileEvent, + clientRefresh: () => void, + buildData: BuildData, // note: this function mutates buildData +) { + const { ctx, ignored, dependencies, contentMap, mut, toRemove } = buildData + const { argv, cfg } = ctx + + // don't do anything for gitignored files + if (ignored(filepath)) { + return + } + + const buildStart = new Date().getTime() + buildData.lastBuildMs = buildStart + const release = await mut.acquire() + if (buildData.lastBuildMs > buildStart) { + release() + return + } + + const perf = new PerfTimer() + console.log(chalk.yellow("Detected change, rebuilding...")) + + // UPDATE DEP GRAPH + const fp = joinSegments(argv.directory, toPosixPath(filepath)) as FilePath + + const staticResources = getStaticResourcesFromPlugins(ctx) + let processedFiles: ProcessedContent[] = [] + + switch (action) { + case "add": + // add to cache when new file is added + processedFiles = await parseMarkdown(ctx, [fp]) + processedFiles.forEach(([tree, vfile]) => contentMap.set(vfile.data.filePath!, [tree, vfile])) + + // update the dep graph by asking all emitters whether they depend on this file + for (const emitter of cfg.plugins.emitters) { + const emitterGraph = + (await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null + + if (emitterGraph) { + const existingGraph = dependencies[emitter.name] + if (existingGraph !== null) { + existingGraph.mergeGraph(emitterGraph) + } else { + // might be the first time we're adding a mardown file + dependencies[emitter.name] = emitterGraph + } + } + } + break + case "change": + // invalidate cache when file is changed + processedFiles = await parseMarkdown(ctx, [fp]) + processedFiles.forEach(([tree, vfile]) => contentMap.set(vfile.data.filePath!, [tree, vfile])) + + // only content files can have added/removed dependencies because of transclusions + if (path.extname(fp) === ".md") { + for (const emitter of cfg.plugins.emitters) { + // get new dependencies from all emitters for this file + const emitterGraph = + (await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null + + // only update the graph if the emitter plugin uses the changed file + // eg. Assets plugin ignores md files, so we skip updating the graph + if (emitterGraph?.hasNode(fp)) { + // merge the new dependencies into the dep graph + dependencies[emitter.name]?.updateIncomingEdgesForNode(emitterGraph, fp) + } + } + } + break + case "delete": + toRemove.add(fp) + break + } + + if (argv.verbose) { + console.log(`Updated dependency graphs in ${perf.timeSince()}`) + } + + // EMIT + perf.addEvent("rebuild") + let emittedFiles = 0 + + for (const emitter of cfg.plugins.emitters) { + const depGraph = dependencies[emitter.name] + + // emitter hasn't defined a dependency graph. call it with all processed files + if (depGraph === null) { + if (argv.verbose) { + console.log( + `Emitter ${emitter.name} doesn't define a dependency graph. Calling it with all files...`, + ) + } + + const files = [...contentMap.values()].filter( + ([_node, vfile]) => !toRemove.has(vfile.data.filePath!), + ) + + const emittedFps = await emitter.emit(ctx, files, staticResources) + + if (ctx.argv.verbose) { + for (const file of emittedFps) { + console.log(`[emit:${emitter.name}] ${file}`) + } + } + + emittedFiles += emittedFps.length + continue + } + + // only call the emitter if it uses this file + if (depGraph.hasNode(fp)) { + // re-emit using all files that are needed for the downstream of this file + // eg. for ContentIndex, the dep graph could be: + // a.md --> contentIndex.json + // b.md ------^ + // + // if a.md changes, we need to re-emit contentIndex.json, + // and supply [a.md, b.md] to the emitter + const upstreams = [...depGraph.getLeafNodeAncestors(fp)] as FilePath[] + + const upstreamContent = upstreams + // filter out non-markdown files + .filter((file) => contentMap.has(file)) + // if file was deleted, don't give it to the emitter + .filter((file) => !toRemove.has(file)) + .map((file) => contentMap.get(file)!) + + const emittedFps = await emitter.emit(ctx, upstreamContent, staticResources) + + if (ctx.argv.verbose) { + for (const file of emittedFps) { + console.log(`[emit:${emitter.name}] ${file}`) + } + } + + emittedFiles += emittedFps.length + } + } + + console.log(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`) + + // CLEANUP + const destinationsToDelete = new Set() + for (const file of toRemove) { + // remove from cache + contentMap.delete(file) + Object.values(dependencies).forEach((depGraph) => { + // remove the node from dependency graphs + depGraph?.removeNode(file) + // remove any orphan nodes. eg if a.md is deleted, a.html is orphaned and should be removed + const orphanNodes = depGraph?.removeOrphanNodes() + orphanNodes?.forEach((node) => { + // only delete files that are in the output directory + if (node.startsWith(argv.output)) { + destinationsToDelete.add(node) + } + }) + }) + } + await rimraf([...destinationsToDelete]) + + console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`)) + + toRemove.clear() + release() + clientRefresh() +} + async function rebuildFromEntrypoint( fp: string, - action: "add" | "change" | "delete", + action: FileEvent, clientRefresh: () => void, buildData: BuildData, // note: this function mutates buildData ) { @@ -190,7 +386,7 @@ async function rebuildFromEntrypoint( // TODO: we can probably traverse the link graph to figure out what's safe to delete here // instead of just deleting everything - await rimraf(argv.output) + await rimraf(path.join(argv.output, ".*"), { glob: true }) await emitContent(ctx, filteredContent) console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`)) } catch (err) { diff --git a/quartz/cfg.ts b/quartz/cfg.ts index 9f0f2a92..0c344d33 100644 --- a/quartz/cfg.ts +++ b/quartz/cfg.ts @@ -7,18 +7,37 @@ import { Theme } from "./util/theme" export type Analytics = | null | { - provider: "plausible" - host?: string - } + provider: "plausible" + host?: string + } | { - provider: "google" - tagId: string - } + provider: "google" + tagId: string + } | { - provider: "umami" - websiteId: string - host?: string - } + provider: "umami" + websiteId: string + host?: string + } + | { + provider: "goatcounter" + websiteId: string + host?: string + scriptSrc?: string + } + | { + provider: "posthog" + apiKey: string + host?: string + } + | { + provider: "tinylytics" + siteId: string + } + | { + provider: "cabin" + host?: string + } export interface GlobalConfiguration { pageTitle: string @@ -35,7 +54,6 @@ export interface GlobalConfiguration { /** Base URL to use for CNAME files, sitemaps, and RSS feeds that require an absolute URL. * Quartz will avoid using this as much as possible and use relative URLs most of the time */ - repoUrl?: string baseUrl?: string theme: Theme /** @@ -59,10 +77,11 @@ export interface FullPageLayout { header: QuartzComponent[] beforeBody: QuartzComponent[] pageBody: QuartzComponent + afterBody: QuartzComponent[] left: QuartzComponent[] right: QuartzComponent[] footer: QuartzComponent } export type PageLayout = Pick -export type SharedLayout = Pick +export type SharedLayout = Pick diff --git a/quartz/cli/args.js b/quartz/cli/args.js index 7ed5b078..123d0ac5 100644 --- a/quartz/cli/args.js +++ b/quartz/cli/args.js @@ -71,6 +71,11 @@ export const BuildArgv = { default: false, describe: "run a local server to live-preview your Quartz", }, + fastRebuild: { + boolean: true, + default: false, + describe: "[experimental] rebuild only the changed files", + }, baseDir: { string: true, default: "", diff --git a/quartz/components/ArticleTitle.tsx b/quartz/components/ArticleTitle.tsx index 7768de6c..318aeb24 100644 --- a/quartz/components/ArticleTitle.tsx +++ b/quartz/components/ArticleTitle.tsx @@ -1,7 +1,7 @@ -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import { classNames } from "../util/lang" -function ArticleTitle({ fileData, displayClass }: QuartzComponentProps) { +const ArticleTitle: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => { const title = fileData.frontmatter?.title if (title) { return

{title}

diff --git a/quartz/components/Backlinks.tsx b/quartz/components/Backlinks.tsx index 573c1c39..aa412a2e 100644 --- a/quartz/components/Backlinks.tsx +++ b/quartz/components/Backlinks.tsx @@ -1,10 +1,15 @@ -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import style from "./styles/backlinks.scss" import { resolveRelative, simplifySlug } from "../util/path" import { i18n } from "../i18n" import { classNames } from "../util/lang" -function Backlinks({ fileData, allFiles, displayClass, cfg }: QuartzComponentProps) { +const Backlinks: QuartzComponent = ({ + fileData, + allFiles, + displayClass, + cfg, +}: QuartzComponentProps) => { const slug = simplifySlug(fileData.slug!) const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug)) return ( diff --git a/quartz/components/Body.tsx b/quartz/components/Body.tsx index fbb85729..96b62788 100644 --- a/quartz/components/Body.tsx +++ b/quartz/components/Body.tsx @@ -1,9 +1,9 @@ // @ts-ignore import clipboardScript from "./scripts/clipboard.inline" import clipboardStyle from "./styles/clipboard.scss" -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" -function Body({ children }: QuartzComponentProps) { +const Body: QuartzComponent = ({ children }: QuartzComponentProps) => { return
{children}
} diff --git a/quartz/components/Breadcrumbs.tsx b/quartz/components/Breadcrumbs.tsx index 3875f5e5..9ccfb9a6 100644 --- a/quartz/components/Breadcrumbs.tsx +++ b/quartz/components/Breadcrumbs.tsx @@ -1,6 +1,6 @@ -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import breadcrumbsStyle from "./styles/breadcrumbs.scss" -import { FullSlug, SimpleSlug, resolveRelative } from "../util/path" +import { FullSlug, SimpleSlug, joinSegments, resolveRelative } from "../util/path" import { QuartzPluginData } from "../plugins/vfile" import { classNames } from "../util/lang" @@ -54,7 +54,11 @@ export default ((opts?: Partial) => { // computed index of folder name to its associated file data let folderIndex: Map | undefined - function Breadcrumbs({ fileData, allFiles, displayClass }: QuartzComponentProps) { + const Breadcrumbs: QuartzComponent = ({ + fileData, + allFiles, + displayClass, + }: QuartzComponentProps) => { // Hide crumbs on root if enabled if (options.hideOnRoot && fileData.slug === "index") { return <> @@ -68,13 +72,9 @@ export default ((opts?: Partial) => { folderIndex = new Map() // construct the index for the first time for (const file of allFiles) { - if (file.slug?.endsWith("index")) { - const folderParts = file.slug?.split("/") - // 2nd last to exclude the /index - const folderName = folderParts?.at(-2) - if (folderName) { - folderIndex.set(folderName, file) - } + const folderParts = file.slug?.split("/") + if (folderParts?.at(-1) === "index") { + folderIndex.set(folderParts.slice(0, -1).join("/"), file) } } } @@ -82,13 +82,17 @@ export default ((opts?: Partial) => { // Split slug into hierarchy/parts const slugParts = fileData.slug?.split("/") if (slugParts) { + // is tag breadcrumb? + const isTagPath = slugParts[0] === "tags" + // full path until current part let currentPath = "" + for (let i = 0; i < slugParts.length - 1; i++) { let curPathSegment = slugParts[i] // Try to resolve frontmatter folder title - const currentFile = folderIndex?.get(curPathSegment) + const currentFile = folderIndex?.get(slugParts.slice(0, i + 1).join("/")) if (currentFile) { const title = currentFile.frontmatter!.title if (title !== "index") { @@ -97,10 +101,15 @@ export default ((opts?: Partial) => { } // Add current slug to full path - currentPath += slugParts[i] + "/" + currentPath = joinSegments(currentPath, slugParts[i]) + const includeTrailingSlash = !isTagPath || i < 1 // Format and add current crumb - const crumb = formatCrumb(curPathSegment, fileData.slug!, currentPath as SimpleSlug) + const crumb = formatCrumb( + curPathSegment, + fileData.slug!, + (currentPath + (includeTrailingSlash ? "/" : "")) as SimpleSlug, + ) crumbs.push(crumb) } @@ -125,5 +134,6 @@ export default ((opts?: Partial) => { ) } Breadcrumbs.css = breadcrumbsStyle + return Breadcrumbs }) satisfies QuartzComponentConstructor diff --git a/quartz/components/Comments.tsx b/quartz/components/Comments.tsx new file mode 100644 index 00000000..8e449402 --- /dev/null +++ b/quartz/components/Comments.tsx @@ -0,0 +1,44 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { classNames } from "../util/lang" +// @ts-ignore +import script from "./scripts/comments.inline" + +type Options = { + provider: "giscus" + options: { + repo: `${string}/${string}` + repoId: string + category: string + categoryId: string + mapping?: "url" | "title" | "og:title" | "specific" | "number" | "pathname" + strict?: boolean + reactionsEnabled?: boolean + inputPosition?: "top" | "bottom" + } +} + +function boolToStringBool(b: boolean): string { + return b ? "1" : "0" +} + +export default ((opts: Options) => { + const Comments: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { + return ( +
+ ) + } + + Comments.afterDOMLoaded = script + + return Comments +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/ContentMeta.tsx b/quartz/components/ContentMeta.tsx index 34c5c94c..5dfec144 100644 --- a/quartz/components/ContentMeta.tsx +++ b/quartz/components/ContentMeta.tsx @@ -2,17 +2,21 @@ import { formatDate, getDate } from "./Date" import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import readingTime from "reading-time" import { classNames } from "../util/lang" -import { JSX } from "preact/jsx-runtime" +import { i18n } from "../i18n" +import { JSX } from "preact" +import style from "./styles/contentMeta.scss" interface ContentMetaOptions { /** * Whether to display reading time */ showReadingTime: boolean + showComma: boolean } const defaultOptions: ContentMetaOptions = { showReadingTime: true, + showComma: true, } export default ((opts?: Partial) => { @@ -21,42 +25,28 @@ export default ((opts?: Partial) => { function ContentMetadata({ cfg, fileData, displayClass }: QuartzComponentProps) { const text = fileData.text - const filepath = fileData.filePath - if (text && filepath) { - const fileRepoUrl: string = `${cfg.repoUrl}/commits/branch/v4/${filepath}` - const segments: JSX.Element[] = [] - // const segments: string[] = [] - if (fileData.dates?.created) { - segments.push(created: {formatDate(fileData.dates.created, cfg.locale)}) - } - if (fileData.dates?.modified) { - segments.push( updated: {formatDate(fileData.dates.modified, cfg.locale)}) + if (text) { + const segments: (string | JSX.Element)[] = [] + + if (fileData.dates) { + segments.push(formatDate(getDate(cfg, fileData)!, cfg.locale)) } + // Display reading time if enabled if (options.showReadingTime) { - const { text: timeTaken, words: _words } = readingTime(text) - segments.push({timeTaken}) + const { minutes, words: _words } = readingTime(text) + const displayedTime = i18n(cfg.locale).components.contentMeta.readingTime({ + minutes: Math.ceil(minutes), + }) + segments.push(displayedTime) } - segments.push( - - view history - - , - ) + const segmentsElements = segments.map((segment) => {segment}) return ( -

- {segments.map((meta, idx) => ( - <> - {meta} - - ))} +

+ {segmentsElements}

) } else { @@ -64,14 +54,7 @@ export default ((opts?: Partial) => { } } - ContentMetadata.css = ` - .content-meta { - margin-top: 0; - color: var(--gray); - } - .content-meta span{ - margin-right: 10px; - } - ` + ContentMetadata.css = style + return ContentMetadata }) satisfies QuartzComponentConstructor diff --git a/quartz/components/Darkmode.tsx b/quartz/components/Darkmode.tsx index 62d3c238..8ed7c99b 100644 --- a/quartz/components/Darkmode.tsx +++ b/quartz/components/Darkmode.tsx @@ -3,11 +3,11 @@ // see: https://v8.dev/features/modules#defer import darkmodeScript from "./scripts/darkmode.inline" import styles from "./styles/darkmode.scss" -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import { i18n } from "../i18n" import { classNames } from "../util/lang" -function Darkmode({ displayClass, cfg }: QuartzComponentProps) { +const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { return (
diff --git a/quartz/components/DesktopOnly.tsx b/quartz/components/DesktopOnly.tsx index a11f23fa..fe2a27f1 100644 --- a/quartz/components/DesktopOnly.tsx +++ b/quartz/components/DesktopOnly.tsx @@ -3,7 +3,7 @@ import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } fro export default ((component?: QuartzComponent) => { if (component) { const Component = component - function DesktopOnly(props: QuartzComponentProps) { + const DesktopOnly: QuartzComponent = (props: QuartzComponentProps) => { return } diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx index f7017342..24583a1f 100644 --- a/quartz/components/Explorer.tsx +++ b/quartz/components/Explorer.tsx @@ -1,4 +1,4 @@ -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import explorerStyle from "./styles/explorer.scss" // @ts-ignore @@ -75,7 +75,12 @@ export default ((userOpts?: Partial) => { jsonTree = JSON.stringify(folders) } - function Explorer({ cfg, allFiles, displayClass, fileData }: QuartzComponentProps) { + const Explorer: QuartzComponent = ({ + cfg, + allFiles, + displayClass, + fileData, + }: QuartzComponentProps) => { constructFileTree(allFiles) return (
@@ -87,7 +92,7 @@ export default ((userOpts?: Partial) => { data-savestate={opts.useSavedState} data-tree={jsonTree} > -

{opts.title ?? i18n(cfg.locale).components.explorer.title}

+

{opts.title ?? i18n(cfg.locale).components.explorer.title}

@@ -205,11 +203,7 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro {/* render tag if folderBehavior is "link", otherwise render
{folderBehavior === "link" ? ( - + {node.displayName} ) : ( diff --git a/quartz/components/Footer.tsx b/quartz/components/Footer.tsx index de472f72..cff28cbb 100644 --- a/quartz/components/Footer.tsx +++ b/quartz/components/Footer.tsx @@ -1,4 +1,4 @@ -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import style from "./styles/footer.scss" import { version } from "../../package.json" import { i18n } from "../i18n" @@ -8,12 +8,11 @@ interface Options { } export default ((opts?: Options) => { - function Footer({ displayClass, cfg }: QuartzComponentProps) { + const Footer: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { const year = new Date().getFullYear() const links = opts?.links ?? [] return (
-

{i18n(cfg.locale).components.footer.createdWith}{" "} Quartz v{version} © {year} diff --git a/quartz/components/Graph.tsx b/quartz/components/Graph.tsx index 9fce9bd8..f7ebcc9a 100644 --- a/quartz/components/Graph.tsx +++ b/quartz/components/Graph.tsx @@ -1,4 +1,4 @@ -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" // @ts-ignore import script from "./scripts/graph.inline" import style from "./styles/graph.scss" @@ -17,6 +17,7 @@ export interface D3Config { opacityScale: number removeTags: string[] showTags: boolean + focusOnHover?: boolean } interface GraphOptions { @@ -37,6 +38,7 @@ const defaultOptions: GraphOptions = { opacityScale: 1, showTags: true, removeTags: [], + focusOnHover: false, }, globalGraph: { drag: true, @@ -50,11 +52,12 @@ const defaultOptions: GraphOptions = { opacityScale: 1, showTags: true, removeTags: [], + focusOnHover: true, }, } export default ((opts?: GraphOptions) => { - function Graph({ displayClass, cfg }: QuartzComponentProps) { + const Graph: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph } const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph } return ( diff --git a/quartz/components/Head.tsx b/quartz/components/Head.tsx index b94909cc..46ba5e00 100644 --- a/quartz/components/Head.tsx +++ b/quartz/components/Head.tsx @@ -1,10 +1,11 @@ import { i18n } from "../i18n" -import { FullSlug, _stripSlashes, joinSegments, pathToRoot } from "../util/path" +import { FullSlug, joinSegments, pathToRoot } from "../util/path" import { JSResourceToScriptElement } from "../util/resources" -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { googleFontHref } from "../util/theme" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" export default (() => { - function Head({ cfg, fileData, externalResources }: QuartzComponentProps) { + const Head: QuartzComponent = ({ cfg, fileData, externalResources }: QuartzComponentProps) => { const title = fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title const description = fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description @@ -21,6 +22,13 @@ export default (() => { {title} + {cfg.theme.cdnCaching && cfg.theme.fontOrigin === "googleFonts" && ( + <> + + + + + )} @@ -30,8 +38,6 @@ export default (() => { - - {css.map((href) => ( ))} diff --git a/quartz/components/Header.tsx b/quartz/components/Header.tsx index 5281f729..eba17ae0 100644 --- a/quartz/components/Header.tsx +++ b/quartz/components/Header.tsx @@ -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 ?

{children}
: null } diff --git a/quartz/components/MobileOnly.tsx b/quartz/components/MobileOnly.tsx index 5a190957..7d2108de 100644 --- a/quartz/components/MobileOnly.tsx +++ b/quartz/components/MobileOnly.tsx @@ -3,7 +3,7 @@ import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } fro export default ((component?: QuartzComponent) => { if (component) { const Component = component - function MobileOnly(props: QuartzComponentProps) { + const MobileOnly: QuartzComponent = (props: QuartzComponentProps) => { return } diff --git a/quartz/components/PageList.tsx b/quartz/components/PageList.tsx index 644e354c..8dc19b74 100644 --- a/quartz/components/PageList.tsx +++ b/quartz/components/PageList.tsx @@ -1,12 +1,12 @@ import { FullSlug, resolveRelative } from "../util/path" import { QuartzPluginData } from "../plugins/vfile" import { Date, getDate } from "./Date" -import { QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentProps } from "./types" import { GlobalConfiguration } from "../cfg" -export function byDateAndAlphabetical( - cfg: GlobalConfiguration, -): (f1: QuartzPluginData, f2: QuartzPluginData) => number { +export type SortFn = (f1: QuartzPluginData, f2: QuartzPluginData) => number + +export function byDateAndAlphabetical(cfg: GlobalConfiguration): SortFn { return (f1, f2) => { if (f1.dates && f2.dates) { // sort descending @@ -27,10 +27,12 @@ export function byDateAndAlphabetical( type Props = { limit?: number + sort?: SortFn } & QuartzComponentProps -export function PageList({ cfg, fileData, allFiles, limit }: Props) { - let list = allFiles.sort(byDateAndAlphabetical(cfg)) +export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit, sort }: Props) => { + const sorter = sort ?? byDateAndAlphabetical(cfg) + let list = allFiles.sort(sorter) if (limit) { list = list.slice(0, limit) } @@ -63,7 +65,7 @@ export function PageList({ cfg, fileData, allFiles, limit }: Props) { class="internal tag-link" href={resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug)} > - #{tag} + {tag} ))} diff --git a/quartz/components/PageTitle.tsx b/quartz/components/PageTitle.tsx index d1296026..046dc527 100644 --- a/quartz/components/PageTitle.tsx +++ b/quartz/components/PageTitle.tsx @@ -1,20 +1,21 @@ import { pathToRoot } from "../util/path" -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import { classNames } from "../util/lang" import { i18n } from "../i18n" -function PageTitle({ fileData, cfg, displayClass }: QuartzComponentProps) { +const PageTitle: QuartzComponent = ({ fileData, cfg, displayClass }: QuartzComponentProps) => { const title = cfg?.pageTitle ?? i18n(cfg.locale).propertyDefaults.title const baseDir = pathToRoot(fileData.slug!) return ( -

+

{title} -

+ ) } PageTitle.css = ` .page-title { + font-size: 1.75rem; margin: 0; } ` diff --git a/quartz/components/RecentNotes.tsx b/quartz/components/RecentNotes.tsx index f8f6de41..2c32fead 100644 --- a/quartz/components/RecentNotes.tsx +++ b/quartz/components/RecentNotes.tsx @@ -1,4 +1,4 @@ -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import { FullSlug, SimpleSlug, resolveRelative } from "../util/path" import { QuartzPluginData } from "../plugins/vfile" import { byDateAndAlphabetical } from "./PageList" @@ -12,6 +12,7 @@ interface Options { title?: string limit: number linkToMore: SimpleSlug | false + showTags: boolean filter: (f: QuartzPluginData) => boolean sort: (f1: QuartzPluginData, f2: QuartzPluginData) => number } @@ -19,12 +20,18 @@ interface Options { const defaultOptions = (cfg: GlobalConfiguration): Options => ({ limit: 3, linkToMore: false, + showTags: true, filter: () => true, sort: byDateAndAlphabetical(cfg), }) export default ((userOpts?: Partial) => { - function RecentNotes({ allFiles, fileData, displayClass, cfg }: QuartzComponentProps) { + const RecentNotes: QuartzComponent = ({ + allFiles, + fileData, + displayClass, + cfg, + }: QuartzComponentProps) => { const opts = { ...defaultOptions(cfg), ...userOpts } const pages = allFiles.filter(opts.filter).sort(opts.sort) const remaining = Math.max(0, pages.length - opts.limit) @@ -51,18 +58,20 @@ export default ((userOpts?: Partial) => {

)} - + {opts.showTags && ( + + )}
) diff --git a/quartz/components/Search.tsx b/quartz/components/Search.tsx index a07dbc4f..01e5a353 100644 --- a/quartz/components/Search.tsx +++ b/quartz/components/Search.tsx @@ -1,4 +1,4 @@ -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import style from "./styles/search.scss" // @ts-ignore import script from "./scripts/search.inline" @@ -14,7 +14,7 @@ const defaultOptions: SearchOptions = { } export default ((userOpts?: Partial) => { - function Search({ displayClass, cfg }: QuartzComponentProps) { + const Search: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { const opts = { ...defaultOptions, ...userOpts } const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder return ( diff --git a/quartz/components/TableOfContents.tsx b/quartz/components/TableOfContents.tsx index 2abc74b5..77ff0eb1 100644 --- a/quartz/components/TableOfContents.tsx +++ b/quartz/components/TableOfContents.tsx @@ -1,4 +1,4 @@ -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import legacyStyle from "./styles/legacyToc.scss" import modernStyle from "./styles/toc.scss" import { classNames } from "../util/lang" @@ -15,7 +15,11 @@ const defaultOptions: Options = { layout: "modern", } -function TableOfContents({ fileData, displayClass, cfg }: QuartzComponentProps) { +const TableOfContents: QuartzComponent = ({ + fileData, + displayClass, + cfg, +}: QuartzComponentProps) => { if (!fileData.toc) { return null } @@ -56,7 +60,7 @@ function TableOfContents({ fileData, displayClass, cfg }: QuartzComponentProps) TableOfContents.css = modernStyle TableOfContents.afterDOMLoaded = script -function LegacyTableOfContents({ fileData, cfg }: QuartzComponentProps) { +const LegacyTableOfContents: QuartzComponent = ({ fileData, cfg }: QuartzComponentProps) => { if (!fileData.toc) { return null } diff --git a/quartz/components/TagList.tsx b/quartz/components/TagList.tsx index e5dd1a3a..ba48098b 100644 --- a/quartz/components/TagList.tsx +++ b/quartz/components/TagList.tsx @@ -1,20 +1,19 @@ import { pathToRoot, slugTag } from "../util/path" -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import { classNames } from "../util/lang" -function TagList({ fileData, displayClass }: QuartzComponentProps) { +const TagList: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => { const tags = fileData.frontmatter?.tags const baseDir = pathToRoot(fileData.slug!) if (tags && tags.length > 0) { return (
    {tags.map((tag) => { - const display = `#${tag}` const linkDest = baseDir + `/tags/${slugTag(tag)}` return (
  • - {display} + {tag}
  • ) diff --git a/quartz/components/index.ts b/quartz/components/index.ts index b3db76be..5b197941 100644 --- a/quartz/components/index.ts +++ b/quartz/components/index.ts @@ -19,6 +19,7 @@ import DesktopOnly from "./DesktopOnly" import MobileOnly from "./MobileOnly" import RecentNotes from "./RecentNotes" import Breadcrumbs from "./Breadcrumbs" +import Comments from "./Comments" export { ArticleTitle, @@ -42,4 +43,5 @@ export { RecentNotes, NotFound, Breadcrumbs, + Comments, } diff --git a/quartz/components/pages/404.tsx b/quartz/components/pages/404.tsx index 276d5e56..63da2c88 100644 --- a/quartz/components/pages/404.tsx +++ b/quartz/components/pages/404.tsx @@ -1,11 +1,16 @@ import { i18n } from "../../i18n" -import { QuartzComponentConstructor, QuartzComponentProps } from "../types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" + +const NotFound: QuartzComponent = ({ cfg }: QuartzComponentProps) => { + // If baseUrl contains a pathname after the domain, use this as the home link + const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`) + const baseDir = url.pathname -function NotFound({ cfg }: QuartzComponentProps) { return ( ) } diff --git a/quartz/components/pages/Content.tsx b/quartz/components/pages/Content.tsx index 2e6416f6..8222d786 100644 --- a/quartz/components/pages/Content.tsx +++ b/quartz/components/pages/Content.tsx @@ -1,7 +1,7 @@ import { htmlToJsx } from "../../util/jsx" -import { QuartzComponentConstructor, QuartzComponentProps } from "../types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" -function Content({ fileData, tree }: QuartzComponentProps) { +const Content: QuartzComponent = ({ fileData, tree }: QuartzComponentProps) => { const content = htmlToJsx(fileData.filePath!, tree) const classes: string[] = fileData.frontmatter?.cssclasses ?? [] const classString = ["popover-hint", ...classes].join(" ") diff --git a/quartz/components/pages/FolderContent.tsx b/quartz/components/pages/FolderContent.tsx index b954ea21..dc216cde 100644 --- a/quartz/components/pages/FolderContent.tsx +++ b/quartz/components/pages/FolderContent.tsx @@ -1,9 +1,9 @@ -import { QuartzComponentConstructor, QuartzComponentProps } from "../types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" import path from "path" import style from "../styles/listPage.scss" -import { PageList } from "../PageList" -import { _stripSlashes, simplifySlug } from "../../util/path" +import { PageList, SortFn } from "../PageList" +import { stripSlashes, simplifySlug } from "../../util/path" import { Root } from "hast" import { htmlToJsx } from "../../util/jsx" import { i18n } from "../../i18n" @@ -13,6 +13,7 @@ interface FolderContentOptions { * Whether to display number of folders */ showFolderCount: boolean + sort?: SortFn } const defaultOptions: FolderContentOptions = { @@ -22,11 +23,11 @@ const defaultOptions: FolderContentOptions = { export default ((opts?: Partial) => { const options: FolderContentOptions = { ...defaultOptions, ...opts } - function FolderContent(props: QuartzComponentProps) { + const FolderContent: QuartzComponent = (props: QuartzComponentProps) => { const { tree, fileData, allFiles, cfg } = props - const folderSlug = _stripSlashes(simplifySlug(fileData.slug!)) + const folderSlug = stripSlashes(simplifySlug(fileData.slug!)) const allPagesInFolder = allFiles.filter((file) => { - const fileSlug = _stripSlashes(simplifySlug(file.slug!)) + const fileSlug = stripSlashes(simplifySlug(file.slug!)) const prefixed = fileSlug.startsWith(folderSlug) && fileSlug !== folderSlug const folderParts = folderSlug.split(path.posix.sep) const fileParts = fileSlug.split(path.posix.sep) @@ -37,6 +38,7 @@ export default ((opts?: Partial) => { const classes = ["popover-hint", ...cssClasses].join(" ") const listProps = { ...props, + sort: options.sort, allFiles: allPagesInFolder, } @@ -47,9 +49,7 @@ export default ((opts?: Partial) => { return (
    -
    -

    {content}

    -
    +
    {content}
    {options.showFolderCount && (

    diff --git a/quartz/components/pages/TagContent.tsx b/quartz/components/pages/TagContent.tsx index 19452ec4..e41ab464 100644 --- a/quartz/components/pages/TagContent.tsx +++ b/quartz/components/pages/TagContent.tsx @@ -1,104 +1,127 @@ -import { QuartzComponentConstructor, QuartzComponentProps } from "../types" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" import style from "../styles/listPage.scss" -import { PageList } from "../PageList" +import { PageList, SortFn } from "../PageList" import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path" import { QuartzPluginData } from "../../plugins/vfile" import { Root } from "hast" import { htmlToJsx } from "../../util/jsx" import { i18n } from "../../i18n" -const numPages = 10 -function TagContent(props: QuartzComponentProps) { - const { tree, fileData, allFiles, cfg } = props - const slug = fileData.slug - - if (!(slug?.startsWith("tags/") || slug === "tags")) { - throw new Error(`Component "TagContent" tried to render a non-tag page: ${slug}`) - } - - const tag = simplifySlug(slug.slice("tags/".length) as FullSlug) - const allPagesWithTag = (tag: string) => - allFiles.filter((file) => - (file.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes).includes(tag), - ) - - const content = - (tree as Root).children.length === 0 - ? fileData.description - : htmlToJsx(fileData.filePath!, tree) - const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? [] - const classes = ["popover-hint", ...cssClasses].join(" ") - if (tag === "/") { - const tags = [ - ...new Set( - allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes), - ), - ].sort((a, b) => a.localeCompare(b)) - const tagItemMap: Map = new Map() - for (const tag of tags) { - tagItemMap.set(tag, allPagesWithTag(tag)) - } - return ( -

    -
    -

    {content}

    -
    -

    {i18n(cfg.locale).pages.tagContent.totalTags({ count: tags.length })}

    -
    - {tags.map((tag) => { - const pages = tagItemMap.get(tag)! - const listProps = { - ...props, - allFiles: pages, - } - - const contentPage = allFiles.filter((file) => file.slug === `tags/${tag}`)[0] - const content = contentPage?.description - return ( -
    -

    - - #{tag} - -

    - {content &&

    {content}

    } -
    -

    - {i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })} - {pages.length > numPages && ( - - {i18n(cfg.locale).pages.tagContent.showingFirst({ count: numPages })} - - )} -

    - -
    -
    - ) - })} -
    -
    - ) - } else { - const pages = allPagesWithTag(tag) - const listProps = { - ...props, - allFiles: pages, - } - - return ( -
    -
    {content}
    -
    -

    {i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}

    -
    - -
    -
    -
    - ) - } +interface TagContentOptions { + sort?: SortFn + numPages: number } -TagContent.css = style + PageList.css -export default (() => TagContent) satisfies QuartzComponentConstructor +const defaultOptions: TagContentOptions = { + numPages: 10, +} + +export default ((opts?: Partial) => { + const options: TagContentOptions = { ...defaultOptions, ...opts } + + const TagContent: QuartzComponent = (props: QuartzComponentProps) => { + const { tree, fileData, allFiles, cfg } = props + const slug = fileData.slug + + if (!(slug?.startsWith("tags/") || slug === "tags")) { + throw new Error(`Component "TagContent" tried to render a non-tag page: ${slug}`) + } + + const tag = simplifySlug(slug.slice("tags/".length) as FullSlug) + const allPagesWithTag = (tag: string) => + allFiles.filter((file) => + (file.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes).includes(tag), + ) + + const content = + (tree as Root).children.length === 0 + ? fileData.description + : htmlToJsx(fileData.filePath!, tree) + const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? [] + const classes = ["popover-hint", ...cssClasses].join(" ") + if (tag === "/") { + const tags = [ + ...new Set( + allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes), + ), + ].sort((a, b) => a.localeCompare(b)) + const tagItemMap: Map = new Map() + for (const tag of tags) { + tagItemMap.set(tag, allPagesWithTag(tag)) + } + return ( +
    +
    +

    {content}

    +
    +

    {i18n(cfg.locale).pages.tagContent.totalTags({ count: tags.length })}

    +
    + {tags.map((tag) => { + const pages = tagItemMap.get(tag)! + const listProps = { + ...props, + allFiles: pages, + } + + const contentPage = allFiles.filter((file) => file.slug === `tags/${tag}`).at(0) + + const root = contentPage?.htmlAst + const content = + !root || root?.children.length === 0 + ? contentPage?.description + : htmlToJsx(contentPage.filePath!, root) + + return ( +
    +

    + + {tag} + +

    + {content &&

    {content}

    } +
    +

    + {i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })} + {pages.length > options.numPages && ( + <> + {" "} + + {i18n(cfg.locale).pages.tagContent.showingFirst({ + count: options.numPages, + })} + + + )} +

    + +
    +
    + ) + })} +
    +
    + ) + } else { + const pages = allPagesWithTag(tag) + const listProps = { + ...props, + allFiles: pages, + } + + return ( +
    +
    {content}
    +
    +

    {i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}

    +
    + +
    +
    +
    + ) + } + } + + TagContent.css = style + PageList.css + return TagContent +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/renderPage.tsx b/quartz/components/renderPage.tsx index fa8305d7..ec5124f4 100644 --- a/quartz/components/renderPage.tsx +++ b/quartz/components/renderPage.tsx @@ -3,10 +3,9 @@ import { QuartzComponent, QuartzComponentProps } from "./types" import HeaderConstructor from "./Header" import BodyConstructor from "./Body" import { JSResourceToScriptElement, StaticResources } from "../util/resources" -import { FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path" +import { clone, FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path" import { visit } from "unist-util-visit" import { Root, Element, ElementContent } from "hast" -import { QuartzPluginData } from "../plugins/vfile" import { GlobalConfiguration } from "../cfg" import { i18n } from "../i18n" @@ -15,11 +14,13 @@ interface RenderComponents { header: QuartzComponent[] beforeBody: QuartzComponent[] pageBody: QuartzComponent + afterBody: QuartzComponent[] left: QuartzComponent[] right: QuartzComponent[] footer: QuartzComponent } +const headerRegex = new RegExp(/h[1-6]/) export function pageResources( baseDir: FullSlug | RelativeURL, staticResources: StaticResources, @@ -52,18 +53,6 @@ export function pageResources( } } -let pageIndex: Map | undefined = undefined -function getOrComputeFileIndex(allFiles: QuartzPluginData[]): Map { - if (!pageIndex) { - pageIndex = new Map() - for (const file of allFiles) { - pageIndex.set(file.slug!, file) - } - } - - return pageIndex -} - export function renderPage( cfg: GlobalConfiguration, slug: FullSlug, @@ -71,14 +60,18 @@ export function renderPage( components: RenderComponents, pageResources: StaticResources, ): string { + // make a deep copy of the tree so we don't remove the transclusion references + // for the file cached in contentMap in build.ts + const root = clone(componentData.tree) as Root + // process transcludes in componentData - visit(componentData.tree as Root, "element", (node, _index, _parent) => { + visit(root, "element", (node, _index, _parent) => { if (node.tagName === "blockquote") { const classNames = (node.properties?.className ?? []) as string[] if (classNames.includes("transclude")) { const inner = node.children[0] as Element const transcludeTarget = inner.properties["data-slug"] as FullSlug - const page = getOrComputeFileIndex(componentData.allFiles).get(transcludeTarget) + const page = componentData.allFiles.find((f) => f.slug === transcludeTarget) if (!page) { return } @@ -103,8 +96,10 @@ export function renderPage( { type: "element", tagName: "a", - properties: { href: inner.properties?.href, class: ["internal"] }, - children: [{ type: "text", value: `Link to original` }], + properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] }, + children: [ + { type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal }, + ], }, ] } @@ -112,18 +107,24 @@ export function renderPage( // header transclude blockRef = blockRef.slice(1) let startIdx = undefined + let startDepth = undefined let endIdx = undefined for (const [i, el] of page.htmlAst.children.entries()) { - if (el.type === "element" && el.tagName.match(/h[1-6]/)) { - if (endIdx) { - break - } + // skip non-headers + if (!(el.type === "element" && el.tagName.match(headerRegex))) continue + const depth = Number(el.tagName.substring(1)) - if (startIdx !== undefined) { - endIdx = i - } else if (el.properties?.id === blockRef) { + // lookin for our blockref + if (startIdx === undefined || startDepth === undefined) { + // skip until we find the blockref that matches + if (el.properties?.id === blockRef) { startIdx = i + startDepth = depth } + } else if (depth <= startDepth) { + // looking for new header that is same level or higher + endIdx = i + break } } @@ -138,7 +139,7 @@ export function renderPage( { type: "element", tagName: "a", - properties: { href: inner.properties?.href, class: ["internal"] }, + properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] }, children: [ { type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal }, ], @@ -168,7 +169,7 @@ export function renderPage( { type: "element", tagName: "a", - properties: { href: inner.properties?.href, class: ["internal"] }, + properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] }, children: [ { type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal }, ], @@ -179,11 +180,15 @@ export function renderPage( } }) + // set componentData.tree to the edited html that has transclusions rendered + componentData.tree = root + const { head: Head, header, beforeBody, pageBody: Content, + afterBody, left, right, footer: Footer, @@ -207,8 +212,9 @@ export function renderPage(
    ) + const lang = componentData.fileData.frontmatter?.lang ?? cfg.locale?.split("-")[0] ?? "en" const doc = ( - +
    @@ -228,6 +234,12 @@ export function renderPage(
    +
    +
{RightComponent} diff --git a/quartz/components/scripts/checkbox.inline.ts b/quartz/components/scripts/checkbox.inline.ts new file mode 100644 index 00000000..50ab0425 --- /dev/null +++ b/quartz/components/scripts/checkbox.inline.ts @@ -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 + 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 + } + }) +}) diff --git a/quartz/components/scripts/comments.inline.ts b/quartz/components/scripts/comments.inline.ts new file mode 100644 index 00000000..4ab29f08 --- /dev/null +++ b/quartz/components/scripts/comments.inline.ts @@ -0,0 +1,67 @@ +const changeTheme = (e: CustomEventMap["themechange"]) => { + const theme = e.detail.theme + const iframe = document.querySelector("iframe.giscus-frame") as HTMLIFrameElement + if (!iframe) { + return + } + + if (!iframe.contentWindow) { + return + } + + iframe.contentWindow.postMessage( + { + giscus: { + setConfig: { + theme: theme, + }, + }, + }, + "https://giscus.app", + ) +} + +type GiscusElement = Omit & { + dataset: DOMStringMap & { + repo: `${string}/${string}` + repoId: string + category: string + categoryId: string + mapping: "url" | "title" | "og:title" | "specific" | "number" | "pathname" + strict: string + reactionsEnabled: string + inputPosition: "top" | "bottom" + } +} + +document.addEventListener("nav", () => { + const giscusContainer = document.querySelector(".giscus") as GiscusElement + if (!giscusContainer) { + return + } + + const giscusScript = document.createElement("script") + giscusScript.src = "https://giscus.app/client.js" + giscusScript.async = true + giscusScript.crossOrigin = "anonymous" + giscusScript.setAttribute("data-loading", "lazy") + giscusScript.setAttribute("data-emit-metadata", "0") + giscusScript.setAttribute("data-repo", giscusContainer.dataset.repo) + giscusScript.setAttribute("data-repo-id", giscusContainer.dataset.repoId) + giscusScript.setAttribute("data-category", giscusContainer.dataset.category) + giscusScript.setAttribute("data-category-id", giscusContainer.dataset.categoryId) + giscusScript.setAttribute("data-mapping", giscusContainer.dataset.mapping) + giscusScript.setAttribute("data-strict", giscusContainer.dataset.strict) + giscusScript.setAttribute("data-reactions-enabled", giscusContainer.dataset.reactionsEnabled) + giscusScript.setAttribute("data-input-position", giscusContainer.dataset.inputPosition) + + const theme = document.documentElement.getAttribute("saved-theme") + if (theme) { + giscusScript.setAttribute("data-theme", theme) + } + + giscusContainer.appendChild(giscusScript) + + document.addEventListener("themechange", changeTheme) + window.addCleanup(() => document.removeEventListener("themechange", changeTheme)) +}) diff --git a/quartz/components/scripts/graph.inline.ts b/quartz/components/scripts/graph.inline.ts index c991e163..cda6fb55 100644 --- a/quartz/components/scripts/graph.inline.ts +++ b/quartz/components/scripts/graph.inline.ts @@ -1,4 +1,4 @@ -import type { ContentDetails, ContentIndex } from "../../plugins/emitters/contentIndex" +import type { ContentDetails } from "../../plugins/emitters/contentIndex" import * as d3 from "d3" import { registerEscapeHandler, removeAllChildren } from "./util" import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path" @@ -44,6 +44,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) { opacityScale, removeTags, showTags, + focusOnHover, } = JSON.parse(graph.dataset["cfg"]!) const data: Map = new Map( @@ -101,7 +102,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) { const graphData: { nodes: NodeData[]; links: LinkData[] } = { nodes: [...neighbourhood].map((url) => { - const text = url.startsWith("tags/") ? "#" + url.substring(5) : data.get(url)?.title ?? url + const text = url.startsWith("tags/") ? "#" + url.substring(5) : (data.get(url)?.title ?? url) return { id: url, text: text, @@ -189,6 +190,8 @@ async function renderGraph(container: string, fullSlug: FullSlug) { return 2 + Math.sqrt(numLinks) } + let connectedNodes: SimpleSlug[] = [] + // draw individual nodes const node = graphNode .append("circle") @@ -202,17 +205,37 @@ async function renderGraph(container: string, fullSlug: FullSlug) { window.spaNavigate(new URL(targ, window.location.toString())) }) .on("mouseover", function (_, d) { - const neighbours: SimpleSlug[] = data.get(slug)?.links ?? [] - const neighbourNodes = d3 - .selectAll(".node") - .filter((d) => neighbours.includes(d.id)) const currentId = d.id const linkNodes = d3 .selectAll(".link") .filter((d: any) => d.source.id === currentId || d.target.id === currentId) - // highlight neighbour nodes - neighbourNodes.transition().duration(200).attr("fill", color) + if (focusOnHover) { + // fade out non-neighbour nodes + connectedNodes = linkNodes.data().flatMap((d: any) => [d.source.id, d.target.id]) + + d3.selectAll(".link") + .transition() + .duration(200) + .style("opacity", 0.2) + d3.selectAll(".node") + .filter((d) => !connectedNodes.includes(d.id)) + .transition() + .duration(200) + .style("opacity", 0.2) + + d3.selectAll(".node") + .filter((d) => !connectedNodes.includes(d.id)) + .nodes() + .map((it) => d3.select(it.parentNode as HTMLElement).select("text")) + .forEach((it) => { + let opacity = parseFloat(it.style("opacity")) + it.transition() + .duration(200) + .attr("opacityOld", opacity) + .style("opacity", Math.min(opacity, 0.2)) + }) + } // highlight links linkNodes.transition().duration(200).attr("stroke", "var(--gray)").attr("stroke-width", 1) @@ -231,6 +254,16 @@ async function renderGraph(container: string, fullSlug: FullSlug) { .style("font-size", bigFont + "em") }) .on("mouseleave", function (_, d) { + if (focusOnHover) { + d3.selectAll(".link").transition().duration(200).style("opacity", 1) + d3.selectAll(".node").transition().duration(200).style("opacity", 1) + + d3.selectAll(".node") + .filter((d) => !connectedNodes.includes(d.id)) + .nodes() + .map((it) => d3.select(it.parentNode as HTMLElement).select("text")) + .forEach((it) => it.transition().duration(200).style("opacity", it.attr("opacityOld"))) + } const currentId = d.id const linkNodes = d3 .selectAll(".link") @@ -249,6 +282,13 @@ async function renderGraph(container: string, fullSlug: FullSlug) { // @ts-ignore .call(drag(simulation)) + // make tags hollow circles + node + .filter((d) => d.id.startsWith("tags/")) + .attr("stroke", color) + .attr("stroke-width", 2) + .attr("fill", "var(--light)") + // draw labels const labels = graphNode .append("text") @@ -321,7 +361,7 @@ function renderGlobalGraph() { document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { const slug = e.detail.url - addToVisited(slug) + addToVisited(simplifySlug(slug)) await renderGraph("graph-container", slug) const containerIcon = document.getElementById("global-graph-icon") diff --git a/quartz/components/scripts/popover.inline.ts b/quartz/components/scripts/popover.inline.ts index 0251834c..49f43820 100644 --- a/quartz/components/scripts/popover.inline.ts +++ b/quartz/components/scripts/popover.inline.ts @@ -3,7 +3,7 @@ import { normalizeRelativeURLs } from "../../util/path" const p = new DOMParser() async function mouseEnterHandler( - this: HTMLLinkElement, + this: HTMLAnchorElement, { clientX, clientY }: { clientX: number; clientY: number }, ) { const link = this @@ -33,33 +33,59 @@ async function mouseEnterHandler( thisUrl.hash = "" thisUrl.search = "" const targetUrl = new URL(link.href) - const hash = targetUrl.hash + const hash = decodeURIComponent(targetUrl.hash) targetUrl.hash = "" targetUrl.search = "" - const contents = await fetch(`${targetUrl}`) - .then((res) => res.text()) - .catch((err) => { - console.error(err) - }) + const response = await fetch(`${targetUrl}`).catch((err) => { + console.error(err) + }) // bailout if another popover exists if (hasAlreadyBeenFetched()) { return } - if (!contents) return - const html = p.parseFromString(contents, "text/html") - normalizeRelativeURLs(html, targetUrl) - const elts = [...html.getElementsByClassName("popover-hint")] - if (elts.length === 0) return + if (!response) return + const [contentType] = response.headers.get("Content-Type")!.split(";") + const [contentTypeCategory, typeInfo] = contentType.split("/") const popoverElement = document.createElement("div") popoverElement.classList.add("popover") const popoverInner = document.createElement("div") popoverInner.classList.add("popover-inner") popoverElement.appendChild(popoverInner) - 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) link.appendChild(popoverElement) @@ -74,7 +100,7 @@ async function mouseEnterHandler( } document.addEventListener("nav", () => { - const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[] + const links = [...document.getElementsByClassName("internal")] as HTMLAnchorElement[] for (const link of links) { link.addEventListener("mouseenter", mouseEnterHandler) window.addCleanup(() => link.removeEventListener("mouseenter", mouseEnterHandler)) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index a75f4ff4..72be6b8d 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -21,6 +21,7 @@ let index = new FlexSearch.Document({ encode: encoder, document: { id: "id", + tag: "tags", index: [ { field: "title", @@ -405,11 +406,33 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { let searchResults: FlexSearch.SimpleDocumentSearchResultSetUnit[] if (searchType === "tags") { - searchResults = await index.searchAsync({ - query: currentSearchTerm.substring(1), - limit: numSearchResults, - index: ["tags"], - }) + currentSearchTerm = currentSearchTerm.substring(1).trim() + const separatorIndex = currentSearchTerm.indexOf(" ") + if (separatorIndex != -1) { + // search by title and content index and then filter by tag (implemented in flexsearch) + const tag = currentSearchTerm.substring(0, separatorIndex) + const query = currentSearchTerm.substring(separatorIndex + 1).trim() + searchResults = await index.searchAsync({ + query: query, + // return at least 10000 documents, so it is enough to filter them by tag (implemented in flexsearch) + limit: Math.max(numSearchResults, 10000), + index: ["title", "content"], + tag: tag, + }) + for (let searchResult of searchResults) { + searchResult.result = searchResult.result.slice(0, numSearchResults) + } + // set search type to basic and remove tag from term for proper highlightning and scroll + searchType = "basic" + currentSearchTerm = query + } else { + // default search by tags index + searchResults = await index.searchAsync({ + query: currentSearchTerm, + limit: numSearchResults, + index: ["tags"], + }) + } } else if (searchType === "basic") { searchResults = await index.searchAsync({ query: currentSearchTerm, diff --git a/quartz/components/styles/contentMeta.scss b/quartz/components/styles/contentMeta.scss new file mode 100644 index 00000000..4d89f65d --- /dev/null +++ b/quartz/components/styles/contentMeta.scss @@ -0,0 +1,14 @@ +.content-meta { + margin-top: 0; + color: var(--gray); + + &[show-comma="true"] { + > span:not(:last-child) { + margin-right: 8px; + + &::after { + content: ","; + } + } + } +} diff --git a/quartz/components/styles/explorer.scss b/quartz/components/styles/explorer.scss index 34f180cf..d4875e7c 100644 --- a/quartz/components/styles/explorer.scss +++ b/quartz/components/styles/explorer.scss @@ -11,7 +11,7 @@ button#explorer { display: flex; align-items: center; - & h1 { + & h2 { font-size: 1rem; display: inline-block; margin: 0; @@ -87,7 +87,7 @@ svg { color: var(--secondary); font-family: var(--headerFont); font-size: 0.95rem; - font-weight: $boldWeight; + font-weight: $semiBoldWeight; line-height: 1.5rem; display: inline-block; } @@ -112,7 +112,7 @@ svg { font-size: 0.95rem; display: inline-block; color: var(--secondary); - font-weight: $boldWeight; + font-weight: $semiBoldWeight; margin: 0; line-height: 1.5rem; pointer-events: none; diff --git a/quartz/components/styles/listPage.scss b/quartz/components/styles/listPage.scss index c8fc9e95..d51568dc 100644 --- a/quartz/components/styles/listPage.scss +++ b/quartz/components/styles/listPage.scss @@ -11,7 +11,7 @@ li.section-li { & > .section { display: grid; - grid-template-columns: 6em 3fr 1fr; + grid-template-columns: fit-content(8em) 3fr 1fr; @media all and (max-width: $mobileBreakpoint) { & > .tags { @@ -24,8 +24,7 @@ li.section-li { } & > .meta { - margin: 0; - flex-basis: 6em; + margin: 0 1em 0 0; opacity: 0.6; } } @@ -33,7 +32,8 @@ li.section-li { // modifications in popover context .popover .section { - grid-template-columns: 6em 1fr !important; + grid-template-columns: fit-content(8em) 1fr !important; + & > .tags { display: none; } diff --git a/quartz/components/styles/popover.scss b/quartz/components/styles/popover.scss index e46292a2..b1694f97 100644 --- a/quartz/components/styles/popover.scss +++ b/quartz/components/styles/popover.scss @@ -38,6 +38,28 @@ white-space: normal; } + & > .popover-inner[data-content-type] { + &[data-content-type*="pdf"], + &[data-content-type*="image"] { + padding: 0; + max-height: 100%; + } + + &[data-content-type*="image"] { + img { + margin: 0; + border-radius: 0; + display: block; + } + } + + &[data-content-type*="pdf"] { + iframe { + width: 100%; + } + } + } + h1 { font-size: 1.5rem; } diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index 3ad774ed..8a9ec671 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -59,6 +59,10 @@ margin-left: auto; margin-right: auto; + @media all and (max-width: $fullPageWidth) { + width: 90%; + } + & > * { width: 100%; border-radius: 7px; @@ -155,6 +159,10 @@ margin: 0 auto; width: min($pageWidth, 100%); } + + a[role="anchor"] { + background-color: transparent; + } } & > #results-container { diff --git a/quartz/components/types.ts b/quartz/components/types.ts index d322ea92..a6b90d3b 100644 --- a/quartz/components/types.ts +++ b/quartz/components/types.ts @@ -3,8 +3,10 @@ import { StaticResources } from "../util/resources" import { QuartzPluginData } from "../plugins/vfile" import { GlobalConfiguration } from "../cfg" import { Node } from "hast" +import { BuildCtx } from "../util/ctx" export type QuartzComponentProps = { + ctx: BuildCtx externalResources: StaticResources fileData: QuartzPluginData cfg: GlobalConfiguration diff --git a/quartz/depgraph.test.ts b/quartz/depgraph.test.ts new file mode 100644 index 00000000..062f13e3 --- /dev/null +++ b/quartz/depgraph.test.ts @@ -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() + 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() + 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() + 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() + graph.addEdge("A.md", "A.html") + + const other = new DepGraph() + 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() + 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() + 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() + graph.addEdge("A.md", "B.md") + + // Add a new file C.md that transcludes B.md + // B.md -> C.md + const other = new DepGraph() + 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) + }) + }) +}) diff --git a/quartz/depgraph.ts b/quartz/depgraph.ts new file mode 100644 index 00000000..3d048cd8 --- /dev/null +++ b/quartz/depgraph.ts @@ -0,0 +1,228 @@ +export default class DepGraph { + // node: incoming and outgoing edges + _graph = new Map; outgoing: Set }>() + + 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): 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, 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 { + let orphanNodes = new Set() + + 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 { + let stack: T[] = [node] + let visited = new Set() + let leafNodes = new Set() + + // 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 { + const leafNodes = this.getLeafNodes(node) + let visited = new Set() + let upstreamNodes = new Set() + + // 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 + } +} diff --git a/quartz/i18n/index.ts b/quartz/i18n/index.ts index b5bd1182..edde307f 100644 --- a/quartz/i18n/index.ts +++ b/quartz/i18n/index.ts @@ -1,13 +1,70 @@ -import { Translation } from "./locales/definition" -import en from "./locales/en-US" +import { Translation, CalloutTranslation } from "./locales/definition" +import enUs from "./locales/en-US" +import enGb from "./locales/en-GB" import fr from "./locales/fr-FR" +import it from "./locales/it-IT" import ja from "./locales/ja-JP" +import de from "./locales/de-DE" +import nl from "./locales/nl-NL" +import ro from "./locales/ro-RO" +import ca from "./locales/ca-ES" +import es from "./locales/es-ES" +import ar from "./locales/ar-SA" +import uk from "./locales/uk-UA" +import ru from "./locales/ru-RU" +import ko from "./locales/ko-KR" +import zh from "./locales/zh-CN" +import vi from "./locales/vi-VN" +import pt from "./locales/pt-BR" +import hu from "./locales/hu-HU" +import fa from "./locales/fa-IR" +import pl from "./locales/pl-PL" export const TRANSLATIONS = { - "en-US": en, + "en-US": enUs, + "en-GB": enGb, "fr-FR": fr, + "it-IT": it, "ja-JP": ja, + "de-DE": de, + "nl-NL": nl, + "nl-BE": nl, + "ro-RO": ro, + "ro-MD": ro, + "ca-ES": ca, + "es-ES": es, + "ar-SA": ar, + "ar-AE": ar, + "ar-QA": ar, + "ar-BH": ar, + "ar-KW": ar, + "ar-OM": ar, + "ar-YE": ar, + "ar-IR": ar, + "ar-SY": ar, + "ar-IQ": ar, + "ar-JO": ar, + "ar-PL": ar, + "ar-LB": ar, + "ar-EG": ar, + "ar-SD": ar, + "ar-LY": ar, + "ar-MA": ar, + "ar-TN": ar, + "ar-DZ": ar, + "ar-MR": ar, + "uk-UA": uk, + "ru-RU": ru, + "ko-KR": ko, + "zh-CN": zh, + "vi-VN": vi, + "pt-BR": pt, + "hu-HU": hu, + "fa-IR": fa, + "pl-PL": pl, } as const -export const i18n = (locale: ValidLocale): Translation => TRANSLATIONS[locale] +export const defaultTranslation = "en-US" +export const i18n = (locale: ValidLocale): Translation => TRANSLATIONS[locale ?? defaultTranslation] export type ValidLocale = keyof typeof TRANSLATIONS +export type ValidCallout = keyof CalloutTranslation diff --git a/quartz/i18n/locales/ar-SA.ts b/quartz/i18n/locales/ar-SA.ts new file mode 100644 index 00000000..8463e2ff --- /dev/null +++ b/quartz/i18n/locales/ar-SA.ts @@ -0,0 +1,89 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "غير معنون", + description: "لم يتم تقديم أي وصف", + }, + components: { + callout: { + note: "ملاحظة", + abstract: "ملخص", + info: "معلومات", + todo: "للقيام", + tip: "نصيحة", + success: "نجاح", + question: "سؤال", + warning: "تحذير", + failure: "فشل", + danger: "خطر", + bug: "خلل", + example: "مثال", + quote: "اقتباس", + }, + backlinks: { + title: "وصلات العودة", + noBacklinksFound: "لا يوجد وصلات عودة", + }, + themeToggle: { + lightMode: "الوضع النهاري", + darkMode: "الوضع الليلي", + }, + explorer: { + title: "المستعرض", + }, + footer: { + createdWith: "أُنشئ باستخدام", + }, + graph: { + title: "التمثيل التفاعلي", + }, + recentNotes: { + title: "آخر الملاحظات", + seeRemainingMore: ({ remaining }) => `تصفح ${remaining} أكثر →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `مقتبس من ${targetSlug}`, + linkToOriginal: "وصلة للملاحظة الرئيسة", + }, + search: { + title: "بحث", + searchBarPlaceholder: "ابحث عن شيء ما", + }, + tableOfContents: { + title: "فهرس المحتويات", + }, + contentMeta: { + readingTime: ({ minutes }) => + minutes == 1 + ? `دقيقة أو أقل للقراءة` + : minutes == 2 + ? `دقيقتان للقراءة` + : `${minutes} دقائق للقراءة`, + }, + }, + pages: { + rss: { + recentNotes: "آخر الملاحظات", + lastFewNotes: ({ count }) => `آخر ${count} ملاحظة`, + }, + error: { + title: "غير موجود", + notFound: "إما أن هذه الصفحة خاصة أو غير موجودة.", + home: "العوده للصفحة الرئيسية", + }, + folderContent: { + folder: "مجلد", + itemsUnderFolder: ({ count }) => + count === 1 ? "يوجد عنصر واحد فقط تحت هذا المجلد" : `يوجد ${count} عناصر تحت هذا المجلد.`, + }, + tagContent: { + tag: "الوسم", + tagIndex: "مؤشر الوسم", + itemsUnderTag: ({ count }) => + count === 1 ? "يوجد عنصر واحد فقط تحت هذا الوسم" : `يوجد ${count} عناصر تحت هذا الوسم.`, + showingFirst: ({ count }) => `إظهار أول ${count} أوسمة.`, + totalTags: ({ count }) => `يوجد ${count} أوسمة.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/ca-ES.ts b/quartz/i18n/locales/ca-ES.ts new file mode 100644 index 00000000..aadbd415 --- /dev/null +++ b/quartz/i18n/locales/ca-ES.ts @@ -0,0 +1,84 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Sense títol", + description: "Sense descripció", + }, + components: { + callout: { + note: "Nota", + abstract: "Resum", + info: "Informació", + todo: "Per fer", + tip: "Consell", + success: "Èxit", + question: "Pregunta", + warning: "Advertència", + failure: "Fall", + danger: "Perill", + bug: "Error", + example: "Exemple", + quote: "Cita", + }, + backlinks: { + title: "Retroenllaç", + noBacklinksFound: "No s'han trobat retroenllaços", + }, + themeToggle: { + lightMode: "Mode clar", + darkMode: "Mode fosc", + }, + explorer: { + title: "Explorador", + }, + footer: { + createdWith: "Creat amb", + }, + graph: { + title: "Vista Gràfica", + }, + recentNotes: { + title: "Notes Recents", + seeRemainingMore: ({ remaining }) => `Vegi ${remaining} més →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Transcluit de ${targetSlug}`, + linkToOriginal: "Enllaç a l'original", + }, + search: { + title: "Cercar", + searchBarPlaceholder: "Cerca alguna cosa", + }, + tableOfContents: { + title: "Taula de Continguts", + }, + contentMeta: { + readingTime: ({ minutes }) => `Es llegeix en ${minutes} min`, + }, + }, + pages: { + rss: { + recentNotes: "Notes recents", + lastFewNotes: ({ count }) => `Últimes ${count} notes`, + }, + error: { + title: "No s'ha trobat.", + notFound: "Aquesta pàgina és privada o no existeix.", + home: "Torna a la pàgina principal", + }, + folderContent: { + folder: "Carpeta", + itemsUnderFolder: ({ count }) => + count === 1 ? "1 article en aquesta carpeta." : `${count} articles en esta carpeta.`, + }, + tagContent: { + tag: "Etiqueta", + tagIndex: "índex d'Etiquetes", + itemsUnderTag: ({ count }) => + count === 1 ? "1 article amb aquesta etiqueta." : `${count} article amb aquesta etiqueta.`, + showingFirst: ({ count }) => `Mostrant les primeres ${count} etiquetes.`, + totalTags: ({ count }) => `S'han trobat ${count} etiquetes en total.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/de-DE.ts b/quartz/i18n/locales/de-DE.ts new file mode 100644 index 00000000..023d4be3 --- /dev/null +++ b/quartz/i18n/locales/de-DE.ts @@ -0,0 +1,84 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Unbenannt", + description: "Keine Beschreibung angegeben", + }, + components: { + callout: { + note: "Hinweis", + abstract: "Zusammenfassung", + info: "Info", + todo: "Zu erledigen", + tip: "Tipp", + success: "Erfolg", + question: "Frage", + warning: "Warnung", + failure: "Misserfolg", + danger: "Gefahr", + bug: "Fehler", + example: "Beispiel", + quote: "Zitat", + }, + backlinks: { + title: "Backlinks", + noBacklinksFound: "Keine Backlinks gefunden", + }, + themeToggle: { + lightMode: "Light Mode", + darkMode: "Dark Mode", + }, + explorer: { + title: "Explorer", + }, + footer: { + createdWith: "Erstellt mit", + }, + graph: { + title: "Graphansicht", + }, + recentNotes: { + title: "Zuletzt bearbeitete Seiten", + seeRemainingMore: ({ remaining }) => `${remaining} weitere ansehen →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Transklusion von ${targetSlug}`, + linkToOriginal: "Link zum Original", + }, + search: { + title: "Suche", + searchBarPlaceholder: "Suche nach etwas", + }, + tableOfContents: { + title: "Inhaltsverzeichnis", + }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes} min read`, + }, + }, + pages: { + rss: { + recentNotes: "Zuletzt bearbeitete Seiten", + lastFewNotes: ({ count }) => `Letzte ${count} Seiten`, + }, + error: { + title: "Nicht gefunden", + notFound: "Diese Seite ist entweder nicht öffentlich oder existiert nicht.", + home: "Return to Homepage", + }, + folderContent: { + folder: "Ordner", + itemsUnderFolder: ({ count }) => + count === 1 ? "1 Datei in diesem Ordner." : `${count} Dateien in diesem Ordner.`, + }, + tagContent: { + tag: "Tag", + tagIndex: "Tag-Übersicht", + itemsUnderTag: ({ count }) => + count === 1 ? "1 Datei mit diesem Tag." : `${count} Dateien mit diesem Tag.`, + showingFirst: ({ count }) => `Die ersten ${count} Tags werden angezeigt.`, + totalTags: ({ count }) => `${count} Tags insgesamt.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/definition.ts b/quartz/i18n/locales/definition.ts index de607eb5..25a8cd7f 100644 --- a/quartz/i18n/locales/definition.ts +++ b/quartz/i18n/locales/definition.ts @@ -1,11 +1,28 @@ import { FullSlug } from "../../util/path" +export interface CalloutTranslation { + note: string + abstract: string + info: string + todo: string + tip: string + success: string + question: string + warning: string + failure: string + danger: string + bug: string + example: string + quote: string +} + export interface Translation { propertyDefaults: { title: string description: string } components: { + callout: CalloutTranslation backlinks: { title: string noBacklinksFound: string @@ -38,6 +55,9 @@ export interface Translation { tableOfContents: { title: string } + contentMeta: { + readingTime: (variables: { minutes: number }) => string + } } pages: { rss: { @@ -47,6 +67,7 @@ export interface Translation { error: { title: string notFound: string + home: string } folderContent: { folder: string diff --git a/quartz/i18n/locales/en-GB.ts b/quartz/i18n/locales/en-GB.ts new file mode 100644 index 00000000..5388b032 --- /dev/null +++ b/quartz/i18n/locales/en-GB.ts @@ -0,0 +1,84 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Untitled", + description: "No description provided", + }, + components: { + callout: { + note: "Note", + abstract: "Abstract", + info: "Info", + todo: "To-Do", + tip: "Tip", + success: "Success", + question: "Question", + warning: "Warning", + failure: "Failure", + danger: "Danger", + bug: "Bug", + example: "Example", + quote: "Quote", + }, + backlinks: { + title: "Backlinks", + noBacklinksFound: "No backlinks found", + }, + themeToggle: { + lightMode: "Light mode", + darkMode: "Dark mode", + }, + explorer: { + title: "Explorer", + }, + footer: { + createdWith: "Created with", + }, + graph: { + title: "Graph View", + }, + recentNotes: { + title: "Recent Notes", + seeRemainingMore: ({ remaining }) => `See ${remaining} more →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Transclude of ${targetSlug}`, + linkToOriginal: "Link to original", + }, + search: { + title: "Search", + searchBarPlaceholder: "Search for something", + }, + tableOfContents: { + title: "Table of Contents", + }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes} min read`, + }, + }, + pages: { + rss: { + recentNotes: "Recent notes", + lastFewNotes: ({ count }) => `Last ${count} notes`, + }, + error: { + title: "Not Found", + notFound: "Either this page is private or doesn't exist.", + home: "Return to Homepage", + }, + folderContent: { + folder: "Folder", + itemsUnderFolder: ({ count }) => + count === 1 ? "1 item under this folder." : `${count} items under this folder.`, + }, + tagContent: { + tag: "Tag", + tagIndex: "Tag Index", + itemsUnderTag: ({ count }) => + count === 1 ? "1 item with this tag." : `${count} items with this tag.`, + showingFirst: ({ count }) => `Showing first ${count} tags.`, + totalTags: ({ count }) => `Found ${count} total tags.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/en-US.ts b/quartz/i18n/locales/en-US.ts index 6ba6d0e5..22cf31e0 100644 --- a/quartz/i18n/locales/en-US.ts +++ b/quartz/i18n/locales/en-US.ts @@ -6,6 +6,21 @@ export default { description: "No description provided", }, components: { + callout: { + note: "Note", + abstract: "Abstract", + info: "Info", + todo: "Todo", + tip: "Tip", + success: "Success", + question: "Question", + warning: "Warning", + failure: "Failure", + danger: "Danger", + bug: "Bug", + example: "Example", + quote: "Quote", + }, backlinks: { title: "Backlinks", noBacklinksFound: "No backlinks found", @@ -38,6 +53,9 @@ export default { tableOfContents: { title: "Table of Contents", }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes} min read`, + }, }, pages: { rss: { @@ -47,17 +65,18 @@ export default { error: { title: "Not Found", notFound: "Either this page is private or doesn't exist.", + home: "Return to Homepage", }, folderContent: { folder: "Folder", itemsUnderFolder: ({ count }) => - count === 1 ? "1 item under this folder" : `${count} items under this folder.`, + count === 1 ? "1 item under this folder." : `${count} items under this folder.`, }, tagContent: { tag: "Tag", tagIndex: "Tag Index", itemsUnderTag: ({ count }) => - count === 1 ? "1 item with this tag" : `${count} items with this tag.`, + count === 1 ? "1 item with this tag." : `${count} items with this tag.`, showingFirst: ({ count }) => `Showing first ${count} tags.`, totalTags: ({ count }) => `Found ${count} total tags.`, }, diff --git a/quartz/i18n/locales/es-ES.ts b/quartz/i18n/locales/es-ES.ts new file mode 100644 index 00000000..c4a57aa1 --- /dev/null +++ b/quartz/i18n/locales/es-ES.ts @@ -0,0 +1,84 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Sin título", + description: "Sin descripción", + }, + components: { + callout: { + note: "Nota", + abstract: "Resumen", + info: "Información", + todo: "Por hacer", + tip: "Consejo", + success: "Éxito", + question: "Pregunta", + warning: "Advertencia", + failure: "Fallo", + danger: "Peligro", + bug: "Error", + example: "Ejemplo", + quote: "Cita", + }, + backlinks: { + title: "Retroenlaces", + noBacklinksFound: "No se han encontrado retroenlaces", + }, + themeToggle: { + lightMode: "Modo claro", + darkMode: "Modo oscuro", + }, + explorer: { + title: "Explorador", + }, + footer: { + createdWith: "Creado con", + }, + graph: { + title: "Vista Gráfica", + }, + recentNotes: { + title: "Notas Recientes", + seeRemainingMore: ({ remaining }) => `Vea ${remaining} más →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Transcluido de ${targetSlug}`, + linkToOriginal: "Enlace al original", + }, + search: { + title: "Buscar", + searchBarPlaceholder: "Busca algo", + }, + tableOfContents: { + title: "Tabla de Contenidos", + }, + contentMeta: { + readingTime: ({ minutes }) => `Se lee en ${minutes} min`, + }, + }, + pages: { + rss: { + recentNotes: "Notas recientes", + lastFewNotes: ({ count }) => `Últimas ${count} notas`, + }, + error: { + title: "No se ha encontrado.", + notFound: "Esta página es privada o no existe.", + home: "Regresa a la página principal", + }, + folderContent: { + folder: "Carpeta", + itemsUnderFolder: ({ count }) => + count === 1 ? "1 artículo en esta carpeta." : `${count} artículos en esta carpeta.`, + }, + tagContent: { + tag: "Etiqueta", + tagIndex: "Índice de Etiquetas", + itemsUnderTag: ({ count }) => + count === 1 ? "1 artículo con esta etiqueta." : `${count} artículos con esta etiqueta.`, + showingFirst: ({ count }) => `Mostrando las primeras ${count} etiquetas.`, + totalTags: ({ count }) => `Se han encontrado ${count} etiquetas en total.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/fa-IR.ts b/quartz/i18n/locales/fa-IR.ts new file mode 100644 index 00000000..5bfef5ae --- /dev/null +++ b/quartz/i18n/locales/fa-IR.ts @@ -0,0 +1,84 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "بدون عنوان", + description: "توضیح خاصی اضافه نشده است", + }, + components: { + callout: { + note: "یادداشت", + abstract: "چکیده", + info: "اطلاعات", + todo: "اقدام", + tip: "نکته", + success: "تیک", + question: "سؤال", + warning: "هشدار", + failure: "شکست", + danger: "خطر", + bug: "باگ", + example: "مثال", + quote: "نقل قول", + }, + backlinks: { + title: "بک‌لینک‌ها", + noBacklinksFound: "بدون بک‌لینک", + }, + themeToggle: { + lightMode: "حالت روشن", + darkMode: "حالت تاریک", + }, + explorer: { + title: "مطالب", + }, + footer: { + createdWith: "ساخته شده با", + }, + graph: { + title: "نمای گراف", + }, + recentNotes: { + title: "یادداشت‌های اخیر", + seeRemainingMore: ({ remaining }) => `${remaining} یادداشت دیگر →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `از ${targetSlug}`, + linkToOriginal: "پیوند به اصلی", + }, + search: { + title: "جستجو", + searchBarPlaceholder: "مطلبی را جستجو کنید", + }, + tableOfContents: { + title: "فهرست", + }, + contentMeta: { + readingTime: ({ minutes }) => `زمان تقریبی مطالعه: ${minutes} دقیقه`, + }, + }, + pages: { + rss: { + recentNotes: "یادداشت‌های اخیر", + lastFewNotes: ({ count }) => `${count} یادداشت اخیر`, + }, + error: { + title: "یافت نشد", + notFound: "این صفحه یا خصوصی است یا وجود ندارد", + home: "بازگشت به صفحه اصلی", + }, + folderContent: { + folder: "پوشه", + itemsUnderFolder: ({ count }) => + count === 1 ? ".یک مطلب در این پوشه است" : `${count} مطلب در این پوشه است.`, + }, + tagContent: { + tag: "برچسب", + tagIndex: "فهرست برچسب‌ها", + itemsUnderTag: ({ count }) => + count === 1 ? "یک مطلب با این برچسب" : `${count} مطلب با این برچسب.`, + showingFirst: ({ count }) => `در حال نمایش ${count} برچسب.`, + totalTags: ({ count }) => `${count} برچسب یافت شد.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/fr-FR.ts b/quartz/i18n/locales/fr-FR.ts index b30ff049..ef43fa87 100644 --- a/quartz/i18n/locales/fr-FR.ts +++ b/quartz/i18n/locales/fr-FR.ts @@ -6,6 +6,21 @@ export default { description: "Aucune description fournie", }, components: { + callout: { + note: "Note", + abstract: "Résumé", + info: "Info", + todo: "À faire", + tip: "Conseil", + success: "Succès", + question: "Question", + warning: "Avertissement", + failure: "Échec", + danger: "Danger", + bug: "Bogue", + example: "Exemple", + quote: "Citation", + }, backlinks: { title: "Liens retour", noBacklinksFound: "Aucun lien retour trouvé", @@ -38,6 +53,9 @@ export default { tableOfContents: { title: "Table des Matières", }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes} min de lecture`, + }, }, pages: { rss: { @@ -45,19 +63,20 @@ export default { lastFewNotes: ({ count }) => `Les dernières ${count} notes`, }, error: { - title: "Pas trouvé", + title: "Introuvable", notFound: "Cette page est soit privée, soit elle n'existe pas.", + home: "Retour à la page d'accueil", }, folderContent: { folder: "Dossier", itemsUnderFolder: ({ count }) => - count === 1 ? "1 élément sous ce dossier" : `${count} éléments sous ce dossier.`, + count === 1 ? "1 élément sous ce dossier." : `${count} éléments sous ce dossier.`, }, tagContent: { tag: "Étiquette", tagIndex: "Index des étiquettes", itemsUnderTag: ({ count }) => - count === 1 ? "1 élément avec cette étiquette" : `${count} éléments avec cette étiquette.`, + count === 1 ? "1 élément avec cette étiquette." : `${count} éléments avec cette étiquette.`, showingFirst: ({ count }) => `Affichage des premières ${count} étiquettes.`, totalTags: ({ count }) => `Trouvé ${count} étiquettes au total.`, }, diff --git a/quartz/i18n/locales/hu-HU.ts b/quartz/i18n/locales/hu-HU.ts new file mode 100644 index 00000000..066b7770 --- /dev/null +++ b/quartz/i18n/locales/hu-HU.ts @@ -0,0 +1,82 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Névtelen", + description: "Nincs leírás", + }, + components: { + callout: { + note: "Jegyzet", + abstract: "Abstract", + info: "Információ", + todo: "Tennivaló", + tip: "Tipp", + success: "Siker", + question: "Kérdés", + warning: "Figyelmeztetés", + failure: "Hiba", + danger: "Veszély", + bug: "Bug", + example: "Példa", + quote: "Idézet", + }, + backlinks: { + title: "Visszautalások", + noBacklinksFound: "Nincs visszautalás", + }, + themeToggle: { + lightMode: "Világos mód", + darkMode: "Sötét mód", + }, + explorer: { + title: "Fájlböngésző", + }, + footer: { + createdWith: "Készítve ezzel:", + }, + graph: { + title: "Grafikonnézet", + }, + recentNotes: { + title: "Legutóbbi jegyzetek", + seeRemainingMore: ({ remaining }) => `${remaining} további megtekintése →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `${targetSlug} áthivatkozása`, + linkToOriginal: "Hivatkozás az eredetire", + }, + search: { + title: "Keresés", + searchBarPlaceholder: "Keress valamire", + }, + tableOfContents: { + title: "Tartalomjegyzék", + }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes} perces olvasás`, + }, + }, + pages: { + rss: { + recentNotes: "Legutóbbi jegyzetek", + lastFewNotes: ({ count }) => `Legutóbbi ${count} jegyzet`, + }, + error: { + title: "Nem található", + notFound: "Ez a lap vagy privát vagy nem létezik.", + home: "Vissza a kezdőlapra", + }, + folderContent: { + folder: "Mappa", + itemsUnderFolder: ({ count }) => `Ebben a mappában ${count} elem található.`, + }, + tagContent: { + tag: "Címke", + tagIndex: "Címke index", + itemsUnderTag: ({ count }) => `${count} elem található ezzel a címkével.`, + showingFirst: ({ count }) => `Első ${count} címke megjelenítve.`, + totalTags: ({ count }) => `Összesen ${count} címke található.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/it-IT.ts b/quartz/i18n/locales/it-IT.ts new file mode 100644 index 00000000..c8c59735 --- /dev/null +++ b/quartz/i18n/locales/it-IT.ts @@ -0,0 +1,84 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Senza titolo", + description: "Nessuna descrizione", + }, + components: { + callout: { + note: "Nota", + abstract: "Astratto", + info: "Info", + todo: "Da fare", + tip: "Consiglio", + success: "Completato", + question: "Domanda", + warning: "Attenzione", + failure: "Errore", + danger: "Pericolo", + bug: "Bug", + example: "Esempio", + quote: "Citazione", + }, + backlinks: { + title: "Link entranti", + noBacklinksFound: "Nessun link entrante", + }, + themeToggle: { + lightMode: "Tema chiaro", + darkMode: "Tema scuro", + }, + explorer: { + title: "Esplora", + }, + footer: { + createdWith: "Creato con", + }, + graph: { + title: "Vista grafico", + }, + recentNotes: { + title: "Note recenti", + seeRemainingMore: ({ remaining }) => `Vedi ${remaining} altro →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Transclusione di ${targetSlug}`, + linkToOriginal: "Link all'originale", + }, + search: { + title: "Cerca", + searchBarPlaceholder: "Cerca qualcosa", + }, + tableOfContents: { + title: "Tabella dei contenuti", + }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes} minuti`, + }, + }, + pages: { + rss: { + recentNotes: "Note recenti", + lastFewNotes: ({ count }) => `Ultime ${count} note`, + }, + error: { + title: "Non trovato", + notFound: "Questa pagina è privata o non esiste.", + home: "Ritorna alla home page", + }, + folderContent: { + folder: "Cartella", + itemsUnderFolder: ({ count }) => + count === 1 ? "1 oggetto in questa cartella." : `${count} oggetti in questa cartella.`, + }, + tagContent: { + tag: "Etichetta", + tagIndex: "Indice etichette", + itemsUnderTag: ({ count }) => + count === 1 ? "1 oggetto con questa etichetta." : `${count} oggetti con questa etichetta.`, + showingFirst: ({ count }) => `Prime ${count} etichette.`, + totalTags: ({ count }) => `Trovate ${count} etichette totali.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/ja-JP.ts b/quartz/i18n/locales/ja-JP.ts index 0ec6f21f..9581b5ed 100644 --- a/quartz/i18n/locales/ja-JP.ts +++ b/quartz/i18n/locales/ja-JP.ts @@ -6,6 +6,21 @@ export default { description: "説明なし", }, components: { + callout: { + note: "ノート", + abstract: "抄録", + info: "情報", + todo: "やるべきこと", + tip: "ヒント", + success: "成功", + question: "質問", + warning: "警告", + failure: "失敗", + danger: "危険", + bug: "バグ", + example: "例", + quote: "引用", + }, backlinks: { title: "バックリンク", noBacklinksFound: "バックリンクはありません", @@ -38,6 +53,9 @@ export default { tableOfContents: { title: "目次", }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes} min read`, + }, }, pages: { rss: { @@ -47,17 +65,16 @@ export default { error: { title: "Not Found", notFound: "ページが存在しないか、非公開設定になっています。", + home: "ホームページに戻る", }, folderContent: { folder: "フォルダ", - itemsUnderFolder: ({ count }) => - `${count}件のページ`, + itemsUnderFolder: ({ count }) => `${count}件のページ`, }, tagContent: { tag: "タグ", tagIndex: "タグ一覧", - itemsUnderTag: ({ count }) => - `${count}件のページ`, + itemsUnderTag: ({ count }) => `${count}件のページ`, showingFirst: ({ count }) => `のうち最初の${count}件を表示しています`, totalTags: ({ count }) => `全${count}個のタグを表示中`, }, diff --git a/quartz/i18n/locales/ko-KR.ts b/quartz/i18n/locales/ko-KR.ts new file mode 100644 index 00000000..9be08d98 --- /dev/null +++ b/quartz/i18n/locales/ko-KR.ts @@ -0,0 +1,82 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "제목 없음", + description: "설명 없음", + }, + components: { + callout: { + note: "노트", + abstract: "개요", + info: "정보", + todo: "할일", + tip: "팁", + success: "성공", + question: "질문", + warning: "주의", + failure: "실패", + danger: "위험", + bug: "버그", + example: "예시", + quote: "인용", + }, + backlinks: { + title: "백링크", + noBacklinksFound: "백링크가 없습니다.", + }, + themeToggle: { + lightMode: "라이트 모드", + darkMode: "다크 모드", + }, + explorer: { + title: "탐색기", + }, + footer: { + createdWith: "Created with", + }, + graph: { + title: "그래프 뷰", + }, + recentNotes: { + title: "최근 게시글", + seeRemainingMore: ({ remaining }) => `${remaining}건 더보기 →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `${targetSlug}의 포함`, + linkToOriginal: "원본 링크", + }, + search: { + title: "검색", + searchBarPlaceholder: "검색어를 입력하세요", + }, + tableOfContents: { + title: "목차", + }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes} min read`, + }, + }, + pages: { + rss: { + recentNotes: "최근 게시글", + lastFewNotes: ({ count }) => `최근 ${count} 건`, + }, + error: { + title: "Not Found", + notFound: "페이지가 존재하지 않거나 비공개 설정이 되어 있습니다.", + home: "홈페이지로 돌아가기", + }, + folderContent: { + folder: "폴더", + itemsUnderFolder: ({ count }) => `${count}건의 항목`, + }, + tagContent: { + tag: "태그", + tagIndex: "태그 목록", + itemsUnderTag: ({ count }) => `${count}건의 항목`, + showingFirst: ({ count }) => `처음 ${count}개의 태그`, + totalTags: ({ count }) => `총 ${count}개의 태그를 찾았습니다.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/nl-NL.ts b/quartz/i18n/locales/nl-NL.ts new file mode 100644 index 00000000..ccbafa7b --- /dev/null +++ b/quartz/i18n/locales/nl-NL.ts @@ -0,0 +1,86 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Naamloos", + description: "Geen beschrijving gegeven.", + }, + components: { + callout: { + note: "Notitie", + abstract: "Samenvatting", + info: "Info", + todo: "Te doen", + tip: "Tip", + success: "Succes", + question: "Vraag", + warning: "Waarschuwing", + failure: "Mislukking", + danger: "Gevaar", + bug: "Bug", + example: "Voorbeeld", + quote: "Citaat", + }, + backlinks: { + title: "Backlinks", + noBacklinksFound: "Geen backlinks gevonden", + }, + themeToggle: { + lightMode: "Lichte modus", + darkMode: "Donkere modus", + }, + explorer: { + title: "Verkenner", + }, + footer: { + createdWith: "Gemaakt met", + }, + graph: { + title: "Grafiekweergave", + }, + recentNotes: { + title: "Recente notities", + seeRemainingMore: ({ remaining }) => `Zie ${remaining} meer →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Invoeging van ${targetSlug}`, + linkToOriginal: "Link naar origineel", + }, + search: { + title: "Zoeken", + searchBarPlaceholder: "Doorzoek de website", + }, + tableOfContents: { + title: "Inhoudsopgave", + }, + contentMeta: { + readingTime: ({ minutes }) => + minutes === 1 ? "1 minuut leestijd" : `${minutes} minuten leestijd`, + }, + }, + pages: { + rss: { + recentNotes: "Recente notities", + lastFewNotes: ({ count }) => `Laatste ${count} notities`, + }, + error: { + title: "Niet gevonden", + notFound: "Deze pagina is niet zichtbaar of bestaat niet.", + home: "Keer terug naar de start pagina", + }, + folderContent: { + folder: "Map", + itemsUnderFolder: ({ count }) => + count === 1 ? "1 item in deze map." : `${count} items in deze map.`, + }, + tagContent: { + tag: "Label", + tagIndex: "Label-index", + itemsUnderTag: ({ count }) => + count === 1 ? "1 item met dit label." : `${count} items met dit label.`, + showingFirst: ({ count }) => + count === 1 ? "Eerste label tonen." : `Eerste ${count} labels tonen.`, + totalTags: ({ count }) => `${count} labels gevonden.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/pl-PL.ts b/quartz/i18n/locales/pl-PL.ts new file mode 100644 index 00000000..7fa0cd47 --- /dev/null +++ b/quartz/i18n/locales/pl-PL.ts @@ -0,0 +1,84 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Bez nazwy", + description: "Brak opisu", + }, + components: { + callout: { + note: "Notatka", + abstract: "Streszczenie", + info: "informacja", + todo: "Do zrobienia", + tip: "Wskazówka", + success: "Zrobione", + question: "Pytanie", + warning: "Ostrzeżenie", + failure: "Usterka", + danger: "Niebiezpieczeństwo", + bug: "Błąd w kodzie", + example: "Przykład", + quote: "Cytat", + }, + backlinks: { + title: "Odnośniki zwrotne", + noBacklinksFound: "Brak połączeń zwrotnych", + }, + themeToggle: { + lightMode: "Trzyb jasny", + darkMode: "Tryb ciemny", + }, + explorer: { + title: "Przeglądaj", + }, + footer: { + createdWith: "Stworzone z użyciem", + }, + graph: { + title: "Graf", + }, + recentNotes: { + title: "Najnowsze notatki", + seeRemainingMore: ({ remaining }) => `Zobacz ${remaining} nastepnych →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Osadzone ${targetSlug}`, + linkToOriginal: "Łącze do oryginału", + }, + search: { + title: "Szukaj", + searchBarPlaceholder: "Search for something", + }, + tableOfContents: { + title: "Spis treści", + }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes} min. czytania `, + }, + }, + pages: { + rss: { + recentNotes: "Najnowsze notatki", + lastFewNotes: ({ count }) => `Ostatnie ${count} notatek`, + }, + error: { + title: "Nie znaleziono", + notFound: "Ta strona jest prywatna lub nie istnieje.", + home: "Powrót do strony głównej", + }, + folderContent: { + folder: "Folder", + itemsUnderFolder: ({ count }) => + count === 1 ? "W tym folderze jest 1 element." : `Elementów w folderze: ${count}.`, + }, + tagContent: { + tag: "Znacznik", + tagIndex: "Spis znaczników", + itemsUnderTag: ({ count }) => + count === 1 ? "Oznaczony 1 element." : `Elementów z tym znacznikiem: ${count}.`, + showingFirst: ({ count }) => `Pokazuje ${count} pierwszych znaczników.`, + totalTags: ({ count }) => `Znalezionych wszystkich znaczników: ${count}.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/pt-BR.ts b/quartz/i18n/locales/pt-BR.ts new file mode 100644 index 00000000..c7b6bfb6 --- /dev/null +++ b/quartz/i18n/locales/pt-BR.ts @@ -0,0 +1,84 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Sem título", + description: "Sem descrição", + }, + components: { + callout: { + note: "Nota", + abstract: "Abstrato", + info: "Info", + todo: "Pendência", + tip: "Dica", + success: "Sucesso", + question: "Pergunta", + warning: "Aviso", + failure: "Falha", + danger: "Perigo", + bug: "Bug", + example: "Exemplo", + quote: "Citação", + }, + backlinks: { + title: "Backlinks", + noBacklinksFound: "Sem backlinks encontrados", + }, + themeToggle: { + lightMode: "Tema claro", + darkMode: "Tema escuro", + }, + explorer: { + title: "Explorador", + }, + footer: { + createdWith: "Criado com", + }, + graph: { + title: "Visão de gráfico", + }, + recentNotes: { + title: "Notas recentes", + seeRemainingMore: ({ remaining }) => `Veja mais ${remaining} →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Transcrever de ${targetSlug}`, + linkToOriginal: "Link ao original", + }, + search: { + title: "Pesquisar", + searchBarPlaceholder: "Pesquisar por algo", + }, + tableOfContents: { + title: "Sumário", + }, + contentMeta: { + readingTime: ({ minutes }) => `Leitura de ${minutes} min`, + }, + }, + pages: { + rss: { + recentNotes: "Notas recentes", + lastFewNotes: ({ count }) => `Últimas ${count} notas`, + }, + error: { + title: "Não encontrado", + notFound: "Esta página é privada ou não existe.", + home: "Retornar a página inicial", + }, + folderContent: { + folder: "Arquivo", + itemsUnderFolder: ({ count }) => + count === 1 ? "1 item neste arquivo." : `${count} items neste arquivo.`, + }, + tagContent: { + tag: "Tag", + tagIndex: "Sumário de Tags", + itemsUnderTag: ({ count }) => + count === 1 ? "1 item com esta tag." : `${count} items com esta tag.`, + showingFirst: ({ count }) => `Mostrando as ${count} primeiras tags.`, + totalTags: ({ count }) => `Encontradas ${count} tags.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/ro-RO.ts b/quartz/i18n/locales/ro-RO.ts new file mode 100644 index 00000000..2de1c8cd --- /dev/null +++ b/quartz/i18n/locales/ro-RO.ts @@ -0,0 +1,85 @@ +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ă.", + home: "Reveniți la pagina de pornire", + }, + 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 diff --git a/quartz/i18n/locales/ru-RU.ts b/quartz/i18n/locales/ru-RU.ts new file mode 100644 index 00000000..18e08173 --- /dev/null +++ b/quartz/i18n/locales/ru-RU.ts @@ -0,0 +1,96 @@ +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: "Эта страница приватная или не существует", + home: "Вернуться на главную страницу", + }, + 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 +} diff --git a/quartz/i18n/locales/uk-UA.ts b/quartz/i18n/locales/uk-UA.ts new file mode 100644 index 00000000..469de4f8 --- /dev/null +++ b/quartz/i18n/locales/uk-UA.ts @@ -0,0 +1,84 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Без назви", + description: "Опис не надано", + }, + components: { + callout: { + note: "Примітка", + abstract: "Абстракт", + info: "Інформація", + todo: "Завдання", + tip: "Порада", + success: "Успіх", + question: "Питання", + warning: "Попередження", + failure: "Невдача", + danger: "Небезпека", + bug: "Баг", + example: "Приклад", + quote: "Цитата", + }, + backlinks: { + title: "Зворотні посилання", + noBacklinksFound: "Зворотних посилань не знайдено", + }, + themeToggle: { + lightMode: "Світлий режим", + darkMode: "Темний режим", + }, + explorer: { + title: "Провідник", + }, + footer: { + createdWith: "Створено за допомогою", + }, + graph: { + title: "Вигляд графа", + }, + recentNotes: { + title: "Останні нотатки", + seeRemainingMore: ({ remaining }) => `Переглянути ще ${remaining} →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Видобуто з ${targetSlug}`, + linkToOriginal: "Посилання на оригінал", + }, + search: { + title: "Пошук", + searchBarPlaceholder: "Шукати щось", + }, + tableOfContents: { + title: "Зміст", + }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes} хв читання`, + }, + }, + pages: { + rss: { + recentNotes: "Останні нотатки", + lastFewNotes: ({ count }) => `Останні нотатки: ${count}`, + }, + error: { + title: "Не знайдено", + notFound: "Ця сторінка або приватна, або не існує.", + home: "Повернутися на головну сторінку", + }, + folderContent: { + folder: "Тека", + itemsUnderFolder: ({ count }) => + count === 1 ? "У цій теці 1 елемент." : `Елементів у цій теці: ${count}.`, + }, + tagContent: { + tag: "Мітка", + tagIndex: "Індекс мітки", + itemsUnderTag: ({ count }) => + count === 1 ? "1 елемент з цією міткою." : `Елементів з цією міткою: ${count}.`, + showingFirst: ({ count }) => `Показ перших ${count} міток.`, + totalTags: ({ count }) => `Всього знайдено міток: ${count}.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/vi-VN.ts b/quartz/i18n/locales/vi-VN.ts new file mode 100644 index 00000000..39a8fbcc --- /dev/null +++ b/quartz/i18n/locales/vi-VN.ts @@ -0,0 +1,84 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Không có tiêu đề", + description: "Không có mô tả được cung cấp", + }, + components: { + callout: { + note: "Ghi Chú", + abstract: "Tóm Tắt", + info: "Thông tin", + todo: "Cần Làm", + tip: "Gợi Ý", + success: "Thành Công", + question: "Nghi Vấn", + warning: "Cảnh Báo", + failure: "Thất Bại", + danger: "Nguy Hiểm", + bug: "Lỗi", + example: "Ví Dụ", + quote: "Trích Dẫn", + }, + backlinks: { + title: "Liên Kết Ngược", + noBacklinksFound: "Không có liên kết ngược được tìm thấy", + }, + themeToggle: { + lightMode: "Sáng", + darkMode: "Tối", + }, + explorer: { + title: "Trong bài này", + }, + footer: { + createdWith: "Được tạo bởi", + }, + graph: { + title: "Biểu Đồ", + }, + recentNotes: { + title: "Bài viết gần đây", + seeRemainingMore: ({ remaining }) => `Xem ${remaining} thêm →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Bao gồm ${targetSlug}`, + linkToOriginal: "Liên Kết Gốc", + }, + search: { + title: "Tìm Kiếm", + searchBarPlaceholder: "Tìm kiếm thông tin", + }, + tableOfContents: { + title: "Bảng Nội Dung", + }, + contentMeta: { + readingTime: ({ minutes }) => `đọc ${minutes} phút`, + }, + }, + pages: { + rss: { + recentNotes: "Những bài gần đây", + lastFewNotes: ({ count }) => `${count} Bài gần đây`, + }, + error: { + title: "Không Tìm Thấy", + notFound: "Trang này được bảo mật hoặc không tồn tại.", + home: "Trở về trang chủ", + }, + folderContent: { + folder: "Thư Mục", + itemsUnderFolder: ({ count }) => + count === 1 ? "1 mục trong thư mục này." : `${count} mục trong thư mục này.`, + }, + tagContent: { + tag: "Thẻ", + tagIndex: "Thẻ Mục Lục", + itemsUnderTag: ({ count }) => + count === 1 ? "1 mục gắn thẻ này." : `${count} mục gắn thẻ này.`, + showingFirst: ({ count }) => `Hiển thị trước ${count} thẻ.`, + totalTags: ({ count }) => `Tìm thấy ${count} thẻ tổng cộng.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/zh-CN.ts b/quartz/i18n/locales/zh-CN.ts new file mode 100644 index 00000000..b710db53 --- /dev/null +++ b/quartz/i18n/locales/zh-CN.ts @@ -0,0 +1,82 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "无题", + description: "无描述", + }, + components: { + callout: { + note: "笔记", + abstract: "摘要", + info: "提示", + todo: "待办", + tip: "提示", + success: "成功", + question: "问题", + warning: "警告", + failure: "失败", + danger: "危险", + bug: "错误", + example: "示例", + quote: "引用", + }, + backlinks: { + title: "反向链接", + noBacklinksFound: "无法找到反向链接", + }, + themeToggle: { + lightMode: "亮色模式", + darkMode: "暗色模式", + }, + explorer: { + title: "探索", + }, + footer: { + createdWith: "Created with", + }, + graph: { + title: "关系图谱", + }, + recentNotes: { + title: "最近的笔记", + seeRemainingMore: ({ remaining }) => `查看更多${remaining}篇笔记 →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `包含${targetSlug}`, + linkToOriginal: "指向原始笔记的链接", + }, + search: { + title: "搜索", + searchBarPlaceholder: "搜索些什么", + }, + tableOfContents: { + title: "目录", + }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes}分钟阅读`, + }, + }, + pages: { + rss: { + recentNotes: "最近的笔记", + lastFewNotes: ({ count }) => `最近的${count}条笔记`, + }, + error: { + title: "无法找到", + notFound: "私有笔记或笔记不存在。", + home: "返回首页", + }, + folderContent: { + folder: "文件夹", + itemsUnderFolder: ({ count }) => `此文件夹下有${count}条笔记。`, + }, + tagContent: { + tag: "标签", + tagIndex: "标签索引", + itemsUnderTag: ({ count }) => `此标签下有${count}条笔记。`, + showingFirst: ({ count }) => `显示前${count}个标签。`, + totalTags: ({ count }) => `总共有${count}个标签。`, + }, + }, +} as const satisfies Translation diff --git a/quartz/plugins/emitters/404.tsx b/quartz/plugins/emitters/404.tsx index 079adbca..e4605cfc 100644 --- a/quartz/plugins/emitters/404.tsx +++ b/quartz/plugins/emitters/404.tsx @@ -9,6 +9,7 @@ import { NotFound } from "../../components" import { defaultProcessedContent } from "../vfile" import { write } from "./helpers" import { i18n } from "../../i18n" +import DepGraph from "../../depgraph" export const NotFoundPage: QuartzEmitterPlugin = () => { const opts: FullPageLayout = { @@ -27,6 +28,9 @@ export const NotFoundPage: QuartzEmitterPlugin = () => { getQuartzComponents() { return [Head, Body, pageBody, Footer] }, + async getDependencyGraph(_ctx, _content, _resources) { + return new DepGraph() + }, async emit(ctx, _content, resources): Promise { const cfg = ctx.cfg.configuration const slug = "404" as FullSlug @@ -42,6 +46,7 @@ export const NotFoundPage: QuartzEmitterPlugin = () => { frontmatter: { title: notFound, tags: [] }, }) const componentData: QuartzComponentProps = { + ctx, fileData: vfile.data, externalResources, cfg, diff --git a/quartz/plugins/emitters/aliases.ts b/quartz/plugins/emitters/aliases.ts index d407629d..af3578eb 100644 --- a/quartz/plugins/emitters/aliases.ts +++ b/quartz/plugins/emitters/aliases.ts @@ -2,12 +2,38 @@ import { FilePath, FullSlug, joinSegments, resolveRelative, simplifySlug } from import { QuartzEmitterPlugin } from "../types" import path from "path" import { write } from "./helpers" +import DepGraph from "../../depgraph" export const AliasRedirects: QuartzEmitterPlugin = () => ({ name: "AliasRedirects", getQuartzComponents() { return [] }, + async getDependencyGraph(ctx, content, _resources) { + const graph = new DepGraph() + + const { argv } = ctx + for (const [_tree, file] of content) { + const dir = path.posix.relative(argv.directory, path.dirname(file.data.filePath!)) + const aliases = file.data.frontmatter?.aliases ?? [] + const slugs = aliases.map((alias) => path.posix.join(dir, alias) as FullSlug) + const permalink = file.data.frontmatter?.permalink + if (typeof permalink === "string") { + slugs.push(permalink as FullSlug) + } + + for (let slug of slugs) { + // fix any slugs that have trailing slash + if (slug.endsWith("/")) { + slug = joinSegments(slug, "index") as FullSlug + } + + graph.addEdge(file.data.filePath!, joinSegments(argv.output, slug + ".html") as FilePath) + } + } + + return graph + }, async emit(ctx, content, _resources): Promise { const { argv } = ctx const fps: FilePath[] = [] diff --git a/quartz/plugins/emitters/assets.ts b/quartz/plugins/emitters/assets.ts index cc97b2e3..036b27da 100644 --- a/quartz/plugins/emitters/assets.ts +++ b/quartz/plugins/emitters/assets.ts @@ -3,6 +3,14 @@ import { QuartzEmitterPlugin } from "../types" import path from "path" import fs from "fs" import { glob } from "../../util/glob" +import DepGraph from "../../depgraph" +import { Argv } from "../../util/ctx" +import { QuartzConfig } from "../../cfg" + +const filesToCopy = async (argv: Argv, cfg: QuartzConfig) => { + // glob all non MD files in content folder and copy it over + return await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns]) +} export const Assets: QuartzEmitterPlugin = () => { return { @@ -10,10 +18,27 @@ export const Assets: QuartzEmitterPlugin = () => { getQuartzComponents() { return [] }, + async getDependencyGraph(ctx, _content, _resources) { + const { argv, cfg } = ctx + const graph = new DepGraph() + + const fps = await filesToCopy(argv, cfg) + + for (const fp of fps) { + const ext = path.extname(fp) + const src = joinSegments(argv.directory, fp) as FilePath + const name = (slugifyFilePath(fp as FilePath, true) + ext) as FilePath + + const dest = joinSegments(argv.output, name) as FilePath + + graph.addEdge(src, dest) + } + + return graph + }, async emit({ argv, cfg }, _content, _resources): Promise { - // glob all non MD/MDX/HTML files in content folder and copy it over const assetsPath = argv.output - const fps = await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns]) + const fps = await filesToCopy(argv, cfg) const res: FilePath[] = [] for (const fp of fps) { const ext = path.extname(fp) diff --git a/quartz/plugins/emitters/cname.ts b/quartz/plugins/emitters/cname.ts index 3e17fea2..cbed2a8b 100644 --- a/quartz/plugins/emitters/cname.ts +++ b/quartz/plugins/emitters/cname.ts @@ -2,6 +2,7 @@ import { FilePath, joinSegments } from "../../util/path" import { QuartzEmitterPlugin } from "../types" import fs from "fs" import chalk from "chalk" +import DepGraph from "../../depgraph" export function extractDomainFromBaseUrl(baseUrl: string) { const url = new URL(`https://${baseUrl}`) @@ -13,6 +14,9 @@ export const CNAME: QuartzEmitterPlugin = () => ({ getQuartzComponents() { return [] }, + async getDependencyGraph(_ctx, _content, _resources) { + return new DepGraph() + }, async emit({ argv, cfg }, _content, _resources): Promise { if (!cfg.configuration.baseUrl) { console.warn(chalk.yellow("CNAME emitter requires `baseUrl` to be set in your configuration")) diff --git a/quartz/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts index c6de2007..d1d8c859 100644 --- a/quartz/plugins/emitters/componentResources.ts +++ b/quartz/plugins/emitters/componentResources.ts @@ -1,4 +1,4 @@ -import { FilePath, FullSlug } from "../../util/path" +import { FilePath, FullSlug, joinSegments } from "../../util/path" import { QuartzEmitterPlugin } from "../types" // @ts-ignore @@ -8,12 +8,12 @@ import popoverScript from "../../components/scripts/popover.inline" import styles from "../../styles/custom.scss" import popoverStyle from "../../components/styles/popover.scss" import { BuildCtx } from "../../util/ctx" -import { StaticResources } from "../../util/resources" import { QuartzComponent } from "../../components/types" import { googleFontHref, joinStyles } from "../../util/theme" import { Features, transform } from "lightningcss" import { transform as transpile } from "esbuild" import { write } from "./helpers" +import DepGraph from "../../depgraph" type ComponentResources = { css: string[] @@ -68,13 +68,8 @@ async function joinScripts(scripts: string[]): Promise { return res.code } -function addGlobalPageResources( - ctx: BuildCtx, - staticResources: StaticResources, - componentResources: ComponentResources, -) { +function addGlobalPageResources(ctx: BuildCtx, componentResources: ComponentResources) { const cfg = ctx.cfg.configuration - const reloadScript = ctx.argv.serve // popovers if (cfg.enablePopovers) { @@ -84,16 +79,17 @@ function addGlobalPageResources( if (cfg.analytics?.provider === "google") { const tagId = cfg.analytics.tagId - staticResources.js.push({ - src: `https://www.googletagmanager.com/gtag/js?id=${tagId}`, - contentType: "external", - loadTime: "afterDOMReady", - }) componentResources.afterDOMLoaded.push(` + const gtagScript = document.createElement("script") + gtagScript.src = "https://www.googletagmanager.com/gtag/js?id=${tagId}" + gtagScript.async = true + document.head.appendChild(gtagScript) + window.dataLayer = window.dataLayer || []; function gtag() { dataLayer.push(arguments); } gtag("js", new Date()); gtag("config", "${tagId}", { send_page_view: false }); + document.addEventListener("nav", () => { gtag("event", "page_view", { page_title: document.title, @@ -118,12 +114,44 @@ function addGlobalPageResources( } else if (cfg.analytics?.provider === "umami") { componentResources.afterDOMLoaded.push(` const umamiScript = document.createElement("script") - umamiScript.src = cfg.analytics.host ?? "https://analytics.umami.is/script.js" + umamiScript.src = "${cfg.analytics.host ?? "https://analytics.umami.is"}/script.js" umamiScript.setAttribute("data-website-id", "${cfg.analytics.websiteId}") umamiScript.async = true document.head.appendChild(umamiScript) `) + } else if (cfg.analytics?.provider === "goatcounter") { + componentResources.afterDOMLoaded.push(` + const goatcounterScript = document.createElement("script") + goatcounterScript.src = "${cfg.analytics.scriptSrc ?? "https://gc.zgo.at/count.js"}" + goatcounterScript.async = true + goatcounterScript.setAttribute("data-goatcounter", + "https://${cfg.analytics.websiteId}.${cfg.analytics.host ?? "goatcounter.com"}/count") + document.head.appendChild(goatcounterScript) + `) + } else if (cfg.analytics?.provider === "posthog") { + componentResources.afterDOMLoaded.push(` + const posthogScript = document.createElement("script") + posthogScript.innerHTML= \`!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys onSessionId".split(" "),n=0;n document.location.reload()) - `, - }) - } } -interface Options { - fontOrigin: "googleFonts" | "local" -} - -const defaultOptions: Options = { - fontOrigin: "googleFonts", -} - -export const ComponentResources: QuartzEmitterPlugin = (opts?: Partial) => { - const { fontOrigin } = { ...defaultOptions, ...opts } +// This emitter should not update the `resources` parameter. If it does, partial +// rebuilds may not work as expected. +export const ComponentResources: QuartzEmitterPlugin = () => { return { name: "ComponentResources", getQuartzComponents() { return [] }, - async emit(ctx, _content, resources): Promise { + async getDependencyGraph(_ctx, _content, _resources) { + return new DepGraph() + }, + async emit(ctx, _content, _resources): Promise { + const promises: Promise[] = [] + const cfg = ctx.cfg.configuration // component specific scripts and styles const componentResources = getComponentResources(ctx) + let googleFontsStyleSheet = "" + if (cfg.theme.fontOrigin === "local") { + // let the user do it themselves in css + } else if (cfg.theme.fontOrigin === "googleFonts" && !cfg.theme.cdnCaching) { + // when cdnCaching is true, we link to google fonts in Head.tsx + let match + + const fontSourceRegex = /url\((https:\/\/fonts.gstatic.com\/s\/[^)]+\.(woff2|ttf))\)/g + + googleFontsStyleSheet = await ( + await fetch(googleFontHref(ctx.cfg.configuration.theme)) + ).text() + + while ((match = fontSourceRegex.exec(googleFontsStyleSheet)) !== null) { + // match[0] is the `url(path)`, match[1] is the `path` + const url = match[1] + // the static name of this file. + const [filename, ext] = url.split("/").pop()!.split(".") + + googleFontsStyleSheet = googleFontsStyleSheet.replace( + url, + `https://${cfg.baseUrl}/static/fonts/${filename}.ttf`, + ) + + promises.push( + fetch(url) + .then((res) => { + if (!res.ok) { + throw new Error(`Failed to fetch font`) + } + return res.arrayBuffer() + }) + .then((buf) => + write({ + ctx, + slug: joinSegments("static", "fonts", filename) as FullSlug, + ext: `.${ext}`, + content: Buffer.from(buf), + }), + ), + ) + } + } + // important that this goes *after* component scripts // as the "nav" event gets triggered here and we should make sure // that everyone else had the chance to register a listener for it + addGlobalPageResources(ctx, componentResources) - if (fontOrigin === "googleFonts") { - resources.css.push(googleFontHref(ctx.cfg.configuration.theme)) - } else if (fontOrigin === "local") { - // let the user do it themselves in css - } - - addGlobalPageResources(ctx, resources, componentResources) - - const stylesheet = joinStyles(ctx.cfg.configuration.theme, ...componentResources.css, styles) + const stylesheet = joinStyles( + ctx.cfg.configuration.theme, + googleFontsStyleSheet, + ...componentResources.css, + styles, + ) const [prescript, postscript] = await Promise.all([ joinScripts(componentResources.beforeDOMLoaded), joinScripts(componentResources.afterDOMLoaded), ]) - const fps = await Promise.all([ + promises.push( write({ ctx, slug: "index" as FullSlug, @@ -222,8 +273,9 @@ export const ComponentResources: QuartzEmitterPlugin = (opts?: Partial< ext: ".js", content: postscript, }), - ]) - return fps + ) + + return await Promise.all(promises) }, } } diff --git a/quartz/plugins/emitters/contentIndex.ts b/quartz/plugins/emitters/contentIndex.ts index 1c86b71b..c0fef86d 100644 --- a/quartz/plugins/emitters/contentIndex.ts +++ b/quartz/plugins/emitters/contentIndex.ts @@ -7,6 +7,7 @@ import { QuartzEmitterPlugin } from "../types" import { toHtml } from "hast-util-to-html" import { write } from "./helpers" import { i18n } from "../../i18n" +import DepGraph from "../../depgraph" export type ContentIndex = Map export type ContentDetails = { @@ -92,6 +93,26 @@ export const ContentIndex: QuartzEmitterPlugin> = (opts) => { opts = { ...defaultOptions, ...opts } return { name: "ContentIndex", + async getDependencyGraph(ctx, content, _resources) { + const graph = new DepGraph() + + for (const [_tree, file] of content) { + const sourcePath = file.data.filePath! + + graph.addEdge( + sourcePath, + joinSegments(ctx.argv.output, "static/contentIndex.json") as FilePath, + ) + if (opts?.enableSiteMap) { + graph.addEdge(sourcePath, joinSegments(ctx.argv.output, "sitemap.xml") as FilePath) + } + if (opts?.enableRSS) { + graph.addEdge(sourcePath, joinSegments(ctx.argv.output, "index.xml") as FilePath) + } + } + + return graph + }, async emit(ctx, content, _resources) { const cfg = ctx.cfg.configuration const emitted: FilePath[] = [] diff --git a/quartz/plugins/emitters/contentPage.tsx b/quartz/plugins/emitters/contentPage.tsx index b11890b3..2ac13214 100644 --- a/quartz/plugins/emitters/contentPage.tsx +++ b/quartz/plugins/emitters/contentPage.tsx @@ -1,14 +1,55 @@ +import path from "path" +import { visit } from "unist-util-visit" +import { Root } from "hast" +import { VFile } from "vfile" import { QuartzEmitterPlugin } from "../types" import { QuartzComponentProps } from "../../components/types" import HeaderConstructor from "../../components/Header" import BodyConstructor from "../../components/Body" import { pageResources, renderPage } from "../../components/renderPage" import { FullPageLayout } from "../../cfg" -import { FilePath, pathToRoot } from "../../util/path" +import { Argv } from "../../util/ctx" +import { FilePath, isRelativeURL, joinSegments, pathToRoot } from "../../util/path" import { defaultContentPageLayout, sharedPageComponents } from "../../../quartz.layout" import { Content } from "../../components" import chalk from "chalk" import { write } from "./helpers" +import DepGraph from "../../depgraph" + +// get all the dependencies for the markdown file +// eg. images, scripts, stylesheets, transclusions +const parseDependencies = (argv: Argv, hast: Root, file: VFile): string[] => { + const dependencies: string[] = [] + + visit(hast, "element", (elem): void => { + let ref: string | null = null + + if ( + ["script", "img", "audio", "video", "source", "iframe"].includes(elem.tagName) && + elem?.properties?.src + ) { + ref = elem.properties.src.toString() + } else if (["a", "link"].includes(elem.tagName) && elem?.properties?.href) { + // transclusions will create a tags with relative hrefs + ref = elem.properties.href.toString() + } + + // if it is a relative url, its a local file and we need to add + // it to the dependency graph. otherwise, ignore + if (ref === null || !isRelativeURL(ref)) { + return + } + + let fp = path.join(file.data.filePath!, path.relative(argv.directory, ref)).replace(/\\/g, "/") + // markdown files have the .md extension stripped in hrefs, add it back here + if (!fp.split("/").pop()?.includes(".")) { + fp += ".md" + } + dependencies.push(fp) + }) + + return dependencies +} export const ContentPage: QuartzEmitterPlugin> = (userOpts) => { const opts: FullPageLayout = { @@ -18,14 +59,40 @@ export const ContentPage: QuartzEmitterPlugin> = (userOp ...userOpts, } - const { head: Head, header, beforeBody, pageBody, left, right, footer: Footer } = opts + const { head: Head, header, beforeBody, pageBody, afterBody, left, right, footer: Footer } = opts const Header = HeaderConstructor() const Body = BodyConstructor() return { name: "ContentPage", getQuartzComponents() { - return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer] + return [ + Head, + Header, + Body, + ...header, + ...beforeBody, + pageBody, + ...afterBody, + ...left, + ...right, + Footer, + ] + }, + async getDependencyGraph(ctx, content, _resources) { + const graph = new DepGraph() + + for (const [tree, file] of content) { + const sourcePath = file.data.filePath! + const slug = file.data.slug! + graph.addEdge(sourcePath, joinSegments(ctx.argv.output, slug + ".html") as FilePath) + + parseDependencies(ctx.argv, tree as Root, file).forEach((dep) => { + graph.addEdge(dep as FilePath, sourcePath) + }) + } + + return graph }, async emit(ctx, content, resources): Promise { const cfg = ctx.cfg.configuration @@ -41,6 +108,7 @@ export const ContentPage: QuartzEmitterPlugin> = (userOp const externalResources = pageResources(pathToRoot(slug), resources) const componentData: QuartzComponentProps = { + ctx, fileData: file.data, externalResources, cfg, @@ -60,7 +128,7 @@ export const ContentPage: QuartzEmitterPlugin> = (userOp fps.push(fp) } - if (!containsIndex) { + if (!containsIndex && !ctx.argv.fastRebuild) { console.log( chalk.yellow( `\nWarning: you seem to be missing an \`index.md\` home page file at the root of your \`${ctx.argv.directory}\` folder. This may cause errors when deploying.`, diff --git a/quartz/plugins/emitters/folderPage.tsx b/quartz/plugins/emitters/folderPage.tsx index 35c360aa..7eebb21c 100644 --- a/quartz/plugins/emitters/folderPage.tsx +++ b/quartz/plugins/emitters/folderPage.tsx @@ -3,14 +3,14 @@ import { QuartzComponentProps } from "../../components/types" import HeaderConstructor from "../../components/Header" import BodyConstructor from "../../components/Body" import { pageResources, renderPage } from "../../components/renderPage" -import { ProcessedContent, defaultProcessedContent } from "../vfile" +import { ProcessedContent, QuartzPluginData, defaultProcessedContent } from "../vfile" import { FullPageLayout } from "../../cfg" import path from "path" import { FilePath, FullSlug, SimpleSlug, - _stripSlashes, + stripSlashes, joinSegments, pathToRoot, simplifySlug, @@ -19,23 +19,55 @@ import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.lay import { FolderContent } from "../../components" import { write } from "./helpers" import { i18n } from "../../i18n" +import DepGraph from "../../depgraph" -export const FolderPage: QuartzEmitterPlugin> = (userOpts) => { +interface FolderPageOptions extends FullPageLayout { + sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number +} + +export const FolderPage: QuartzEmitterPlugin> = (userOpts) => { const opts: FullPageLayout = { ...sharedPageComponents, ...defaultListPageLayout, - pageBody: FolderContent(), + pageBody: FolderContent({ sort: userOpts?.sort }), ...userOpts, } - const { head: Head, header, beforeBody, pageBody, left, right, footer: Footer } = opts + const { head: Head, header, beforeBody, pageBody, afterBody, left, right, footer: Footer } = opts const Header = HeaderConstructor() const Body = BodyConstructor() return { name: "FolderPage", getQuartzComponents() { - return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer] + return [ + Head, + Header, + Body, + ...header, + ...beforeBody, + pageBody, + ...afterBody, + ...left, + ...right, + Footer, + ] + }, + async getDependencyGraph(_ctx, content, _resources) { + // Example graph: + // nested/file.md --> nested/index.html + // nested/file2.md ------^ + const graph = new DepGraph() + + content.map(([_tree, vfile]) => { + const slug = vfile.data.slug + const folderName = path.dirname(slug ?? "") as SimpleSlug + if (slug && folderName !== "." && folderName !== "tags") { + graph.addEdge(vfile.data.filePath!, joinSegments(folderName, "index.html") as FilePath) + } + }) + + return graph }, async emit(ctx, content, resources): Promise { const fps: FilePath[] = [] @@ -67,7 +99,7 @@ export const FolderPage: QuartzEmitterPlugin> = (userOpt ) for (const [tree, file] of content) { - const slug = _stripSlashes(simplifySlug(file.data.slug!)) as SimpleSlug + const slug = stripSlashes(simplifySlug(file.data.slug!)) as SimpleSlug if (folders.has(slug)) { folderDescriptions[slug] = [tree, file] } @@ -78,6 +110,7 @@ export const FolderPage: QuartzEmitterPlugin> = (userOpt const externalResources = pageResources(pathToRoot(slug), resources) const [tree, file] = folderDescriptions[folder] const componentData: QuartzComponentProps = { + ctx, fileData: file.data, externalResources, cfg, diff --git a/quartz/plugins/emitters/helpers.ts b/quartz/plugins/emitters/helpers.ts index ef1d1c35..523151c2 100644 --- a/quartz/plugins/emitters/helpers.ts +++ b/quartz/plugins/emitters/helpers.ts @@ -7,7 +7,7 @@ type WriteOptions = { ctx: BuildCtx slug: FullSlug ext: `.${string}` | "" - content: string + content: string | Buffer } export const write = async ({ ctx, slug, ext, content }: WriteOptions): Promise => { diff --git a/quartz/plugins/emitters/static.ts b/quartz/plugins/emitters/static.ts index 9f93d9b0..c52c6287 100644 --- a/quartz/plugins/emitters/static.ts +++ b/quartz/plugins/emitters/static.ts @@ -2,12 +2,27 @@ import { FilePath, QUARTZ, joinSegments } from "../../util/path" import { QuartzEmitterPlugin } from "../types" import fs from "fs" import { glob } from "../../util/glob" +import DepGraph from "../../depgraph" export const Static: QuartzEmitterPlugin = () => ({ name: "Static", getQuartzComponents() { return [] }, + async getDependencyGraph({ argv, cfg }, _content, _resources) { + const graph = new DepGraph() + + const staticPath = joinSegments(QUARTZ, "static") + const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns) + for (const fp of fps) { + graph.addEdge( + joinSegments("static", fp) as FilePath, + joinSegments(argv.output, "static", fp) as FilePath, + ) + } + + return graph + }, async emit({ argv, cfg }, _content, _resources): Promise { const staticPath = joinSegments(QUARTZ, "static") const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns) diff --git a/quartz/plugins/emitters/tagPage.tsx b/quartz/plugins/emitters/tagPage.tsx index 2411c683..066d4ec2 100644 --- a/quartz/plugins/emitters/tagPage.tsx +++ b/quartz/plugins/emitters/tagPage.tsx @@ -3,7 +3,7 @@ import { QuartzComponentProps } from "../../components/types" import HeaderConstructor from "../../components/Header" import BodyConstructor from "../../components/Body" import { pageResources, renderPage } from "../../components/renderPage" -import { ProcessedContent, defaultProcessedContent } from "../vfile" +import { ProcessedContent, QuartzPluginData, defaultProcessedContent } from "../vfile" import { FullPageLayout } from "../../cfg" import { FilePath, @@ -16,23 +16,60 @@ import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.lay import { TagContent } from "../../components" import { write } from "./helpers" import { i18n } from "../../i18n" +import DepGraph from "../../depgraph" -export const TagPage: QuartzEmitterPlugin> = (userOpts) => { +interface TagPageOptions extends FullPageLayout { + sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number +} + +export const TagPage: QuartzEmitterPlugin> = (userOpts) => { const opts: FullPageLayout = { ...sharedPageComponents, ...defaultListPageLayout, - pageBody: TagContent(), + pageBody: TagContent({ sort: userOpts?.sort }), ...userOpts, } - const { head: Head, header, beforeBody, pageBody, left, right, footer: Footer } = opts + const { head: Head, header, beforeBody, pageBody, afterBody, left, right, footer: Footer } = opts const Header = HeaderConstructor() const Body = BodyConstructor() return { name: "TagPage", getQuartzComponents() { - return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer] + return [ + Head, + Header, + Body, + ...header, + ...beforeBody, + pageBody, + ...afterBody, + ...left, + ...right, + Footer, + ] + }, + async getDependencyGraph(ctx, content, _resources) { + const graph = new DepGraph() + + for (const [_tree, file] of content) { + const sourcePath = file.data.filePath! + const tags = (file.data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes) + // if the file has at least one tag, it is used in the tag index page + if (tags.length > 0) { + tags.push("index") + } + + for (const tag of tags) { + graph.addEdge( + sourcePath, + joinSegments(ctx.argv.output, "tags", tag + ".html") as FilePath, + ) + } + } + + return graph }, async emit(ctx, content, resources): Promise { const fps: FilePath[] = [] @@ -51,7 +88,7 @@ export const TagPage: QuartzEmitterPlugin> = (userOpts) const title = tag === "index" ? i18n(cfg.locale).pages.tagContent.tagIndex - : `${i18n(cfg.locale).pages.tagContent.tag}: #${tag}` + : `${i18n(cfg.locale).pages.tagContent.tag}: ${tag}` return [ tag, defaultProcessedContent({ @@ -77,6 +114,7 @@ export const TagPage: QuartzEmitterPlugin> = (userOpts) const externalResources = pageResources(pathToRoot(slug), resources) const [tree, file] = tagDescriptions[tag] const componentData: QuartzComponentProps = { + ctx, fileData: file.data, externalResources, cfg, diff --git a/quartz/plugins/filters/draft.ts b/quartz/plugins/filters/draft.ts index 65e2d6b6..5fd06b96 100644 --- a/quartz/plugins/filters/draft.ts +++ b/quartz/plugins/filters/draft.ts @@ -3,7 +3,7 @@ import { QuartzFilterPlugin } from "../types" export const RemoveDrafts: QuartzFilterPlugin<{}> = () => ({ name: "RemoveDrafts", shouldPublish(_ctx, [_tree, vfile]) { - const draftFlag: boolean = vfile.data?.frontmatter?.draft ?? false + const draftFlag: boolean = vfile.data?.frontmatter?.draft || false return !draftFlag }, }) diff --git a/quartz/plugins/index.ts b/quartz/plugins/index.ts index f35d0535..df9fd1d2 100644 --- a/quartz/plugins/index.ts +++ b/quartz/plugins/index.ts @@ -18,6 +18,23 @@ export function getStaticResourcesFromPlugins(ctx: BuildCtx) { } } + // if serving locally, listen for rebuilds and reload the page + if (ctx.argv.serve) { + const wsUrl = ctx.argv.remoteDevHost + ? `wss://${ctx.argv.remoteDevHost}:${ctx.argv.wsPort}` + : `ws://localhost:${ctx.argv.wsPort}` + + staticResources.js.push({ + loadTime: "afterDOMReady", + contentType: "inline", + script: ` + const socket = new WebSocket('${wsUrl}') + // reload(true) ensures resources like images and scripts are fetched again in firefox + socket.addEventListener('message', () => document.location.reload(true)) + `, + }) + } + return staticResources } diff --git a/quartz/plugins/transformers/citations.ts b/quartz/plugins/transformers/citations.ts new file mode 100644 index 00000000..bb302e43 --- /dev/null +++ b/quartz/plugins/transformers/citations.ts @@ -0,0 +1,52 @@ +import rehypeCitation from "rehype-citation" +import { PluggableList } from "unified" +import { visit } from "unist-util-visit" +import { QuartzTransformerPlugin } from "../types" + +export interface Options { + bibliographyFile: string + suppressBibliography: boolean + linkCitations: boolean + csl: string +} + +const defaultOptions: Options = { + bibliographyFile: "./bibliography.bib", + suppressBibliography: false, + linkCitations: false, + csl: "apa", +} + +export const Citations: QuartzTransformerPlugin | undefined> = (userOpts) => { + const opts = { ...defaultOptions, ...userOpts } + return { + name: "Citations", + htmlPlugins() { + const plugins: PluggableList = [] + + // Add rehype-citation to the list of plugins + plugins.push([ + rehypeCitation, + { + bibliography: opts.bibliographyFile, + suppressBibliography: opts.suppressBibliography, + linkCitations: opts.linkCitations, + }, + ]) + + // Transform the HTML of the citattions; add data-no-popover property to the citation links + // using https://github.com/syntax-tree/unist-util-visit as they're just anochor links + plugins.push(() => { + return (tree, _file) => { + visit(tree, "element", (node, index, parent) => { + if (node.tagName === "a" && node.properties?.href?.startsWith("#bib")) { + node.properties["data-no-popover"] = true + } + }) + } + }) + + return plugins + }, + } +} diff --git a/quartz/plugins/transformers/description.ts b/quartz/plugins/transformers/description.ts index 884d5b18..5900745c 100644 --- a/quartz/plugins/transformers/description.ts +++ b/quartz/plugins/transformers/description.ts @@ -5,12 +5,19 @@ import { escapeHTML } from "../../util/escape" export interface Options { descriptionLength: number + replaceExternalLinks: boolean } const defaultOptions: Options = { descriptionLength: 150, + replaceExternalLinks: true, } +const urlRegex = new RegExp( + /(https?:\/\/)?(?([\da-z\.-]+)\.([a-z\.]{2,6})(:\d+)?)(?[\/\w\.-]*)(\?[\/\w\.=&;-]*)?/, + "g", +) + export const Description: QuartzTransformerPlugin | undefined> = (userOpts) => { const opts = { ...defaultOptions, ...userOpts } return { @@ -19,22 +26,46 @@ export const Description: QuartzTransformerPlugin | undefined> return [ () => { return async (tree: HTMLRoot, file) => { - const frontMatterDescription = file.data.frontmatter?.description - const text = escapeHTML(toString(tree)) + let frontMatterDescription = file.data.frontmatter?.description + let text = escapeHTML(toString(tree)) - const desc = frontMatterDescription ?? text - const sentences = desc.replace(/\s+/g, " ").split(".") - let finalDesc = "" - let sentenceIdx = 0 - const len = opts.descriptionLength - while (finalDesc.length < len) { - const sentence = sentences[sentenceIdx] - if (!sentence) break - finalDesc += sentence + "." - sentenceIdx++ + if (opts.replaceExternalLinks) { + frontMatterDescription = frontMatterDescription?.replace( + urlRegex, + "$" + "$", + ) + text = text.replace(urlRegex, "$" + "$") } - file.data.description = finalDesc + const desc = frontMatterDescription ?? text + const sentences = desc.replace(/\s+/g, " ").split(/\.\s/) + const finalDesc: string[] = [] + const len = opts.descriptionLength + let sentenceIdx = 0 + let currentDescriptionLength = 0 + + if (sentences[0] !== undefined && sentences[0].length >= len) { + const firstSentence = sentences[0].split(" ") + while (currentDescriptionLength < len) { + const sentence = firstSentence[sentenceIdx] + if (!sentence) break + finalDesc.push(sentence) + currentDescriptionLength += sentence.length + sentenceIdx++ + } + finalDesc.push("...") + } else { + while (currentDescriptionLength < len) { + const sentence = sentences[sentenceIdx] + if (!sentence) break + const currentSentence = sentence.endsWith(".") ? sentence : sentence + "." + finalDesc.push(currentSentence) + currentDescriptionLength += currentSentence.length + sentenceIdx++ + } + } + + file.data.description = finalDesc.join(" ") file.data.text = text } }, diff --git a/quartz/plugins/transformers/frontmatter.ts b/quartz/plugins/transformers/frontmatter.ts index 048c9445..5ab239a3 100644 --- a/quartz/plugins/transformers/frontmatter.ts +++ b/quartz/plugins/transformers/frontmatter.ts @@ -8,15 +8,13 @@ import { QuartzPluginData } from "../vfile" import { i18n } from "../../i18n" export interface Options { - delims: string | string[] + delimiters: string | [string, string] language: "yaml" | "toml" - oneLineTagDelim: string } const defaultOptions: Options = { - delims: "---", + delimiters: "---", language: "yaml", - oneLineTagDelim: ",", } function coalesceAliases(data: { [key: string]: any }, aliases: string[]) { @@ -59,9 +57,9 @@ export const FrontMatter: QuartzTransformerPlugin | undefined> }, }) - if (data.title) { + if (data.title != null && data.title.toString() !== "") { data.title = data.title.toString() - } else if (data.title === null || data.title === undefined) { + } else { data.title = file.stem ?? i18n(cfg.configuration.locale).propertyDefaults.title } @@ -92,6 +90,7 @@ declare module "vfile" { description: string publish: boolean draft: boolean + lang: string enableToc: string cssclasses: string[] }> diff --git a/quartz/plugins/transformers/gfm.ts b/quartz/plugins/transformers/gfm.ts index 7860f851..48681ff7 100644 --- a/quartz/plugins/transformers/gfm.ts +++ b/quartz/plugins/transformers/gfm.ts @@ -32,6 +32,7 @@ export const GitHubFlavoredMarkdown: QuartzTransformerPlugin | { behavior: "append", properties: { + role: "anchor", ariaHidden: true, tabIndex: -1, "data-no-popover": true, diff --git a/quartz/plugins/transformers/index.ts b/quartz/plugins/transformers/index.ts index e340f10e..7908c865 100644 --- a/quartz/plugins/transformers/index.ts +++ b/quartz/plugins/transformers/index.ts @@ -1,5 +1,6 @@ export { FrontMatter } from "./frontmatter" export { GitHubFlavoredMarkdown } from "./gfm" +export { Citations } from "./citations" export { CreatedModifiedDate } from "./lastmod" export { Latex } from "./latex" export { Description } from "./description" diff --git a/quartz/plugins/transformers/latex.ts b/quartz/plugins/transformers/latex.ts index ab10a4fb..c9f6bff0 100644 --- a/quartz/plugins/transformers/latex.ts +++ b/quartz/plugins/transformers/latex.ts @@ -26,12 +26,12 @@ export const Latex: QuartzTransformerPlugin = (opts?: Options) => { return { css: [ // base css - "https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css", + "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css", ], js: [ { // fix copy behaviour: https://github.com/KaTeX/KaTeX/blob/main/contrib/copy-tex/README.md - src: "https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/copy-tex.min.js", + src: "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/contrib/copy-tex.min.js", loadTime: "afterDOMReady", contentType: "external", }, diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts index 1ba0c8e5..28058182 100644 --- a/quartz/plugins/transformers/links.ts +++ b/quartz/plugins/transformers/links.ts @@ -4,10 +4,11 @@ import { RelativeURL, SimpleSlug, TransformOptions, - _stripSlashes, + stripSlashes, simplifySlug, splitAnchor, transformLink, + joinSegments, } from "../../util/path" import path from "path" import { visit } from "unist-util-visit" @@ -92,7 +93,7 @@ export const CrawlLinks: QuartzTransformerPlugin | undefined> = } node.properties.className = classes - if (opts.openLinksInNewTab) { + if (isExternal && opts.openLinksInNewTab) { node.properties.target = "_blank" } @@ -107,7 +108,7 @@ export const CrawlLinks: QuartzTransformerPlugin | undefined> = // url.resolve is considered legacy // WHATWG equivalent https://nodejs.dev/en/api/v18/url/#urlresolvefrom-to - const url = new URL(dest, `https://base.com/${curSlug}`) + const url = new URL(dest, "https://base.com/" + stripSlashes(curSlug, true)) const canonicalDest = url.pathname let [destCanonical, _destAnchor] = splitAnchor(canonicalDest) if (destCanonical.endsWith("/")) { @@ -115,7 +116,7 @@ export const CrawlLinks: QuartzTransformerPlugin | undefined> = } // need to decodeURIComponent here as WHATWG URL percent-encodes everything - const full = decodeURIComponent(_stripSlashes(destCanonical, true)) as FullSlug + const full = decodeURIComponent(stripSlashes(destCanonical, true)) as FullSlug const simple = simplifySlug(full) outgoing.add(simple) node.properties["data-slug"] = full diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index cac9678b..a406c865 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -2,13 +2,15 @@ import { QuartzTransformerPlugin } from "../types" import { Root, Html, BlockContent, DefinitionContent, Paragraph, Code } from "mdast" import { Element, Literal, Root as HtmlRoot } from "hast" import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace" -import { slug as slugAnchor } from "github-slugger" import rehypeRaw from "rehype-raw" import { SKIP, visit } from "unist-util-visit" import path from "path" +import { splitAnchor } from "../../util/path" import { JSResource } from "../../util/resources" // @ts-ignore import calloutScript from "../../components/scripts/callout.inline.ts" +// @ts-ignore +import checkboxScript from "../../components/scripts/checkbox.inline.ts" import { FilePath, pathToRoot, slugTag, slugifyFilePath } from "../../util/path" import { toHast } from "mdast-util-to-hast" import { toHtml } from "hast-util-to-html" @@ -28,6 +30,7 @@ export interface Options { enableInHtmlEmbed: boolean enableYouTubeEmbed: boolean enableVideoEmbed: boolean + enableCheckbox: boolean } const defaultOptions: Options = { @@ -42,6 +45,7 @@ const defaultOptions: Options = { enableInHtmlEmbed: false, enableYouTubeEmbed: true, enableVideoEmbed: true, + enableCheckbox: false, } const calloutMapping = { @@ -93,29 +97,40 @@ function canonicalizeCallout(calloutName: string): keyof typeof calloutMapping { export const externalLinkRegex = /^https?:\/\//i -export const arrowRegex = new RegExp(/(-{1,2}>|={1,2}>|<-{1,2}|<={1,2})/, "g") +export const arrowRegex = new RegExp(/(-{1,2}>|={1,2}>|<-{1,2}|<={1,2})/g) -// !? -> optional embedding -// \[\[ -> open brace -// ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name) -// (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link) -// (\|[^\[\]\#]+)? -> | then one or more non-special characters (alias) +// !? -> optional embedding +// \[\[ -> open brace +// ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name) +// (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link) +// (\\?\|[^\[\]\#]+)? -> optional escape \ then | then one or more non-special characters (alias) export const wikilinkRegex = new RegExp( - /!?\[\[([^\[\]\|\#]+)?(#+[^\[\]\|\#]+)?(\|[^\[\]\#]+)?\]\]/, - "g", + /!?\[\[([^\[\]\|\#\\]+)?(#+[^\[\]\|\#\\]+)?(\\?\|[^\[\]\#]+)?\]\]/g, ) -const highlightRegex = new RegExp(/==([^=]+)==/, "g") -const commentRegex = new RegExp(/%%[\s\S]*?%%/, "g") + +// ^\|([^\n])+\|\n(\|) -> matches the header row +// ( ?:?-{3,}:? ?\|)+ -> matches the header row separator +// (\|([^\n])+\|\n)+ -> matches the body rows +export const tableRegex = new RegExp(/^\|([^\n])+\|\n(\|)( ?:?-{3,}:? ?\|)+\n(\|([^\n])+\|\n?)+/gm) + +// matches any wikilink, only used for escaping wikilinks inside tables +export const tableWikilinkRegex = new RegExp(/(!?\[\[[^\]]*?\]\])/g) + +const highlightRegex = new RegExp(/==([^=]+)==/g) +const commentRegex = new RegExp(/%%[\s\S]*?%%/g) // from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts -const calloutRegex = new RegExp(/^\[\!(\w+)\]([+-]?)/) -const calloutLineRegex = new RegExp(/^> *\[\!\w+\][+-]?.*$/, "gm") +const calloutRegex = new RegExp(/^\[\!(\w+)\|?(.+?)?\]([+-]?)/) +const calloutLineRegex = new RegExp(/^> *\[\!\w+\|?.*?\][+-]?.*$/gm) // (?:^| ) -> non-capturing group, tag should start be separated by a space or be the start of the line // #(...) -> capturing group, tag itself must start with # // (?:[-_\p{L}\d\p{Z}])+ -> non-capturing group, non-empty string of (Unicode-aware) alpha-numeric characters and symbols, hyphens and/or underscores // (?:\/[-_\p{L}\d\p{Z}]+)*) -> non-capturing group, matches an arbitrary number of tag strings separated by "/" -const tagRegex = new RegExp(/(?:^| )#((?:[-_\p{L}\p{Emoji}\d])+(?:\/[-_\p{L}\p{Emoji}\d]+)*)/, "gu") -const blockReferenceRegex = new RegExp(/\^([-_A-Za-z0-9]+)$/, "g") +const tagRegex = new RegExp( + /(?:^| )#((?:[-_\p{L}\p{Emoji}\p{M}\d])+(?:\/[-_\p{L}\p{Emoji}\p{M}\d]+)*)/gu, +) +const blockReferenceRegex = new RegExp(/\^([-_A-Za-z0-9]+)$/g) const ytLinkRegex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/ +const ytPlaylistLinkRegex = /[?&]list=([^#?&]*)/ const videoExtensionRegex = new RegExp(/\.(mp4|webm|ogg|avi|mov|flv|wmv|mkv|mpg|mpeg|3gp|m4v)$/) const wikilinkImageEmbedRegex = new RegExp( /^(?(?!^\d*x?\d*$).*?)?(\|?\s*?(?\d+)(x(?\d+))?)?$/, @@ -161,13 +176,27 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin src = src.toString() } + // replace all wikilinks inside a table first + src = src.replace(tableRegex, (value) => { + // escape all aliases and headers in wikilinks inside a table + return value.replace(tableWikilinkRegex, (_value, raw) => { + // const [raw]: (string | undefined)[] = capture + let escaped = raw ?? "" + escaped = escaped.replace("#", "\\#") + // escape pipe characters if they are not already escaped + escaped = escaped.replace(/((^|[^\\])(\\\\)*)\|/g, "$1\\|") + + return escaped + }) + }) + + // replace all other wikilinks src = src.replace(wikilinkRegex, (value, ...capture) => { const [rawFp, rawHeader, rawAlias]: (string | undefined)[] = capture - const fp = rawFp ?? "" - const anchor = rawHeader?.trim().replace(/^#+/, "") - const blockRef = Boolean(anchor?.startsWith("^")) ? "^" : "" - const displayAnchor = anchor ? `#${blockRef}${slugAnchor(anchor)}` : "" + const [fp, anchor] = splitAnchor(`${rawFp ?? ""}${rawHeader ?? ""}`) + const blockRef = Boolean(rawHeader?.match(/^#?\^/)) ? "^" : "" + const displayAnchor = anchor ? `#${blockRef}${anchor.trim().replace(/^#+/, "")}` : "" const displayAlias = rawAlias ?? rawHeader?.replace("#", "|") ?? "" const embedDisplay = value.startsWith("!") ? "!" : "" @@ -181,7 +210,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin return src }, - markdownPlugins() { + markdownPlugins(_ctx) { const plugins: PluggableList = [] // regex replacements @@ -234,14 +263,14 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin } else if ([".pdf"].includes(ext)) { return { type: "html", - value: ``, + value: ``, } } else { const block = anchor return { type: "html", data: { hProperties: { transclude: true } }, - value: `
Transclude of ${url}${block}
`, } @@ -319,7 +348,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin children: [ { type: "text", - value: `#${tag}`, + value: tag, }, ], } @@ -379,8 +408,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin return } - // find first line - const firstChild = node.children[0] + // find first line and callout content + const [firstChild, ...calloutContent] = node.children if (firstChild.type !== "paragraph" || firstChild.children[0]?.type !== "text") { return } @@ -392,7 +421,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin const match = firstLine.match(calloutRegex) if (match && match.input) { - const [calloutDirective, typeString, collapseChar] = match + const [calloutDirective, typeString, calloutMetaData, collapseChar] = match const calloutType = canonicalizeCallout(typeString.toLowerCase()) const collapse = collapseChar === "+" || collapseChar === "-" const defaultState = collapseChar === "-" ? "collapsed" : "expanded" @@ -403,7 +432,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin children: [ { type: "text", - value: useDefaultTitle ? capitalize(calloutType) : titleContent + " ", + value: useDefaultTitle ? capitalize(typeString) : titleContent + " ", }, ...restOfTitle, ], @@ -439,17 +468,39 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin // replace first line of blockquote with title and rest of the paragraph text node.children.splice(0, 1, ...blockquoteContent) + const classNames = ["callout", calloutType] + if (collapse) { + classNames.push("is-collapsible") + } + if (defaultState === "collapsed") { + classNames.push("is-collapsed") + } + // add properties to base blockquote node.data = { hProperties: { ...(node.data?.hProperties ?? {}), - className: `callout ${calloutType} ${collapse ? "is-collapsible" : ""} ${ - defaultState === "collapsed" ? "is-collapsed" : "" - }`, + className: classNames.join(" "), "data-callout": calloutType, "data-callout-fold": collapse, + "data-callout-metadata": calloutMetaData, }, } + + // Add callout-content class to callout body if it has one. + if (calloutContent.length > 0) { + const contentData: BlockContent | DefinitionContent = { + data: { + hProperties: { + className: "callout-content", + }, + hName: "div", + }, + type: "blockquote", + children: [...calloutContent], + } + node.children = [node.children[0], contentData] + } } }) } @@ -513,12 +564,35 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin last.value = last.value.slice(0, -matches[0].length) const block = matches[0].slice(1) - if (!Object.keys(file.data.blocks!).includes(block)) { - node.properties = { - ...node.properties, - id: block, + if (last.value === "") { + // this is an inline block ref but the actual block + // is the previous element above it + let idx = (index ?? 1) - 1 + while (idx >= 0) { + const element = parent?.children.at(idx) + if (!element) break + if (element.type !== "element") { + idx -= 1 + } else { + if (!Object.keys(file.data.blocks!).includes(block)) { + element.properties = { + ...element.properties, + id: block, + } + file.data.blocks![block] = element + } + return + } + } + } else { + // normal paragraph transclude + if (!Object.keys(file.data.blocks!).includes(block)) { + node.properties = { + ...node.properties, + id: block, + } + file.data.blocks![block] = node } - file.data.blocks![block] = node } } } @@ -537,16 +611,47 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin if (node.tagName === "img" && typeof node.properties.src === "string") { const match = node.properties.src.match(ytLinkRegex) const videoId = match && match[2].length == 11 ? match[2] : null + const playlistId = node.properties.src.match(ytPlaylistLinkRegex)?.[1] if (videoId) { + // YouTube video (with optional playlist) node.tagName = "iframe" node.properties = { - class: "external-embed", + class: "external-embed youtube", allow: "fullscreen", frameborder: 0, width: "600px", - height: "350px", - src: `https://www.youtube.com/embed/${videoId}`, + src: playlistId + ? `https://www.youtube.com/embed/${videoId}?list=${playlistId}` + : `https://www.youtube.com/embed/${videoId}`, } + } else if (playlistId) { + // YouTube playlist only. + node.tagName = "iframe" + node.properties = { + class: "external-embed youtube", + allow: "fullscreen", + frameborder: 0, + width: "600px", + src: `https://www.youtube.com/embed/videoseries?list=${playlistId}`, + } + } + } + }) + } + }) + } + + if (opts.enableCheckbox) { + plugins.push(() => { + return (tree: HtmlRoot, _file) => { + visit(tree, "element", (node) => { + if (node.tagName === "input" && node.properties.type === "checkbox") { + const isChecked = node.properties?.checked ?? false + node.properties = { + type: "checkbox", + disabled: false, + checked: isChecked, + class: "checkbox-toggle", } } }) @@ -559,6 +664,14 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin externalResources() { const js: JSResource[] = [] + if (opts.enableCheckbox) { + js.push({ + script: checkboxScript, + loadTime: "afterDOMReady", + contentType: "inline", + }) + } + if (opts.callouts) { js.push({ script: calloutScript, @@ -570,17 +683,22 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin if (opts.mermaid) { js.push({ script: ` - import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs'; - const darkMode = document.documentElement.getAttribute('saved-theme') === 'dark' - mermaid.initialize({ - startOnLoad: false, - securityLevel: 'loose', - theme: darkMode ? 'dark' : 'default' - }); + let mermaidImport = undefined document.addEventListener('nav', async () => { - await mermaid.run({ - querySelector: '.mermaid' - }) + if (document.querySelector("code.mermaid")) { + mermaidImport ||= await import('https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.7.0/mermaid.esm.min.mjs') + const mermaid = mermaidImport.default + const darkMode = document.documentElement.getAttribute('saved-theme') === 'dark' + mermaid.initialize({ + startOnLoad: false, + securityLevel: 'loose', + theme: darkMode ? 'dark' : 'default' + }) + + await mermaid.run({ + querySelector: '.mermaid' + }) + } }); `, loadTime: "afterDOMReady", @@ -599,4 +717,4 @@ declare module "vfile" { blocks: Record htmlAst: HtmlRoot } -} \ No newline at end of file +} diff --git a/quartz/plugins/transformers/syntax.ts b/quartz/plugins/transformers/syntax.ts index e8477296..f11734e5 100644 --- a/quartz/plugins/transformers/syntax.ts +++ b/quartz/plugins/transformers/syntax.ts @@ -1,20 +1,33 @@ import { QuartzTransformerPlugin } from "../types" -import rehypePrettyCode, { Options as CodeOptions } from "rehype-pretty-code" +import rehypePrettyCode, { Options as CodeOptions, Theme as CodeTheme } from "rehype-pretty-code" -export const SyntaxHighlighting: QuartzTransformerPlugin = () => ({ - name: "SyntaxHighlighting", - htmlPlugins() { - return [ - [ - rehypePrettyCode, - { - keepBackground: false, - theme: { - dark: "github-dark", - light: "github-light", - }, - } satisfies Partial, - ], - ] +interface Theme extends Record { + light: CodeTheme + dark: CodeTheme +} + +interface Options { + theme?: Theme + keepBackground?: boolean +} + +const defaultOptions: Options = { + theme: { + light: "github-light", + dark: "github-dark", }, -}) + keepBackground: false, +} + +export const SyntaxHighlighting: QuartzTransformerPlugin = ( + userOpts?: Partial, +) => { + const opts: Partial = { ...defaultOptions, ...userOpts } + + return { + name: "SyntaxHighlighting", + htmlPlugins() { + return [[rehypePrettyCode, opts]] + }, + } +} diff --git a/quartz/plugins/transformers/toc.ts b/quartz/plugins/transformers/toc.ts index e831f945..bfc2f987 100644 --- a/quartz/plugins/transformers/toc.ts +++ b/quartz/plugins/transformers/toc.ts @@ -3,11 +3,10 @@ import { Root } from "mdast" import { visit } from "unist-util-visit" import { toString } from "mdast-util-to-string" import Slugger from "github-slugger" -import { wikilinkRegex } from "./ofm" export interface Options { maxDepth: 1 | 2 | 3 | 4 | 5 | 6 - minEntries: 1 + minEntries: number showByDefault: boolean collapseByDefault: boolean } @@ -25,7 +24,6 @@ interface TocEntry { slug: string // this is just the anchor (#some-slug), not the canonical slug } -const regexMdLinks = new RegExp(/\[([^\[]+)\](\(.*\))/, "g") const slugAnchor = new Slugger() export const TableOfContents: QuartzTransformerPlugin | undefined> = ( userOpts, @@ -44,16 +42,7 @@ export const TableOfContents: QuartzTransformerPlugin | undefin let highestDepth: number = opts.maxDepth visit(tree, "heading", (node) => { if (node.depth <= opts.maxDepth) { - let text = toString(node) - - // strip link formatting from toc entries - text = text.replace(wikilinkRegex, (_, rawFp, __, rawAlias) => { - const fp = rawFp?.trim() ?? "" - const alias = rawAlias?.slice(1).trim() - return alias ?? fp - }) - text = text.replace(regexMdLinks, "$1") - + const text = toString(node) highestDepth = Math.min(highestDepth, node.depth) toc.push({ depth: node.depth, @@ -63,7 +52,7 @@ export const TableOfContents: QuartzTransformerPlugin | undefin } }) - if (toc.length > opts.minEntries) { + if (toc.length > 0 && toc.length > opts.minEntries) { file.data.toc = toc.map((entry) => ({ ...entry, depth: entry.depth - highestDepth, diff --git a/quartz/plugins/types.ts b/quartz/plugins/types.ts index a361bb9f..a23f5d6f 100644 --- a/quartz/plugins/types.ts +++ b/quartz/plugins/types.ts @@ -4,6 +4,7 @@ import { ProcessedContent } from "./vfile" import { QuartzComponent } from "../components/types" import { FilePath } from "../util/path" import { BuildCtx } from "../util/ctx" +import DepGraph from "../depgraph" export interface PluginTypes { transformers: QuartzTransformerPluginInstance[] @@ -38,4 +39,9 @@ export type QuartzEmitterPluginInstance = { name: string emit(ctx: BuildCtx, content: ProcessedContent[], resources: StaticResources): Promise getQuartzComponents(ctx: BuildCtx): QuartzComponent[] + getDependencyGraph?( + ctx: BuildCtx, + content: ProcessedContent[], + resources: StaticResources, + ): Promise> } diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index 7999bdf2..3b3ead7c 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -1,6 +1,7 @@ +@use "./variables.scss" as *; @use "./syntax.scss"; @use "./callouts.scss"; -@use "./variables.scss" as *; + html { scroll-behavior: smooth; text-size-adjust: none; @@ -19,11 +20,10 @@ section { } .text-highlight { - background-color: #fff23688; + background-color: var(--textHighlight); padding: 0 0.1rem; border-radius: 5px; } - ::selection { background: color-mix(in srgb, var(--tertiary) 60%, rgba(255, 255, 255, 0)); color: var(--darkgray); @@ -42,18 +42,34 @@ ul, .math { color: var(--darkgray); fill: var(--darkgray); - overflow-wrap: anywhere; hyphens: auto; } +p, +ul, +text, +a, +li, +ol, +ul, +.katex, +.math { + overflow-wrap: anywhere; + /* tr and td removed from list of selectors for overflow-wrap, allowing them to use default 'normal' property value */ +} + .math { &.math-display { text-align: center; } } +strong { + font-weight: $semiBoldWeight; +} + a { - font-weight: $boldWeight; + font-weight: $semiBoldWeight; text-decoration: none; transition: color 0.2s ease; color: var(--secondary); @@ -74,6 +90,11 @@ a { border-radius: 0; padding: 0; } + &.tag-link { + &::before { + content: "#"; + } + } } &.external .external-icon { @@ -180,11 +201,19 @@ a { } } - & .page-header { + & .page-header, + & .page-footer { width: $pageWidth; - margin: $topSpacing auto 0 auto; + margin-top: 1rem; + @media all and (max-width: $fullPageWidth) { width: initial; + } + } + + & .page-header { + margin: $topSpacing auto 0 auto; + @media all and (max-width: $fullPageWidth) { margin-top: 2rem; } } @@ -258,11 +287,9 @@ thead { font-weight: revert; margin-bottom: 0; - article > & > a { + article > & > a[role="anchor"] { color: var(--dark); - &.internal { - background-color: transparent; - } + background-color: transparent; } } @@ -473,6 +500,10 @@ video { flex: 1 1 auto; } +div:has(> .overflow) { + position: relative; +} + ul.overflow, ol.overflow { max-height: 400; @@ -505,3 +536,16 @@ ol.overflow { padding-left: 1rem; } } + +.katex-display { + overflow-x: auto; + overflow-y: hidden; +} + +.external-embed.youtube, +iframe.pdf { + aspect-ratio: 16 / 9; + height: 100%; + width: 100%; + border-radius: 5px; +} diff --git a/quartz/styles/callouts.scss b/quartz/styles/callouts.scss index 7fa52c50..d6f65aad 100644 --- a/quartz/styles/callouts.scss +++ b/quartz/styles/callouts.scss @@ -10,7 +10,7 @@ transition: max-height 0.3s ease; box-sizing: border-box; - & > *:nth-child(2) { + & > .callout-content > :first-child { margin-top: 0; } @@ -157,6 +157,6 @@ } .callout-title-inner { - font-weight: $boldWeight; + font-weight: $semiBoldWeight; } } diff --git a/quartz/styles/variables.scss b/quartz/styles/variables.scss index 8384b9c4..e45cc915 100644 --- a/quartz/styles/variables.scss +++ b/quartz/styles/variables.scss @@ -5,4 +5,5 @@ $sidePanelWidth: 380px; $topSpacing: 6rem; $fullPageWidth: $pageWidth + 2 * $sidePanelWidth; $boldWeight: 700; +$semiBoldWeight: 600; $normalWeight: 400; diff --git a/quartz/util/ctx.ts b/quartz/util/ctx.ts index 13e0bf86..e0561141 100644 --- a/quartz/util/ctx.ts +++ b/quartz/util/ctx.ts @@ -6,6 +6,7 @@ export interface Argv { verbose: boolean output: string serve: boolean + fastRebuild: boolean port: number wsPort: number remoteDevHost?: string diff --git a/quartz/util/path.ts b/quartz/util/path.ts index 5cd79ba5..c02bfb12 100644 --- a/quartz/util/path.ts +++ b/quartz/util/path.ts @@ -23,22 +23,22 @@ export type FullSlug = SlugLike<"full"> export function isFullSlug(s: string): s is FullSlug { const validStart = !(s.startsWith(".") || s.startsWith("/")) const validEnding = !s.endsWith("/") - return validStart && validEnding && !_containsForbiddenCharacters(s) + return validStart && validEnding && !containsForbiddenCharacters(s) } /** Shouldn't be a relative path and shouldn't have `/index` as an ending or a file extension. It _can_ however have a trailing slash to indicate a folder path. */ export type SimpleSlug = SlugLike<"simple"> export function isSimpleSlug(s: string): s is SimpleSlug { const validStart = !(s.startsWith(".") || (s.length > 1 && s.startsWith("/"))) - const validEnding = !(s.endsWith("/index") || s === "index") - return validStart && !_containsForbiddenCharacters(s) && validEnding && !_hasFileExtension(s) + const validEnding = !endsWith(s, "index") + return validStart && !containsForbiddenCharacters(s) && validEnding && !_hasFileExtension(s) } /** Can be found on `href`s but can also be constructed for client-side navigation (e.g. search and graph) */ export type RelativeURL = SlugLike<"relative"> export function isRelativeURL(s: string): s is RelativeURL { const validStart = /^\.{1,2}/.test(s) - const validEnding = !(s.endsWith("/index") || s === "index") + const validEnding = !endsWith(s, "index") return validStart && validEnding && ![".md", ".html"].includes(_getFileExtension(s) ?? "") } @@ -63,7 +63,7 @@ function sluggify(s: string): string { } export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug { - fp = _stripSlashes(fp) as FilePath + fp = stripSlashes(fp) as FilePath let ext = _getFileExtension(fp) const withoutFileExt = fp.replace(new RegExp(ext + "$"), "") if (excludeExt || [".md", ".html", undefined].includes(ext)) { @@ -73,7 +73,7 @@ export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug { let slug = sluggify(withoutFileExt) // treat _index as index - if (_endsWith(slug, "_index")) { + if (endsWith(slug, "_index")) { slug = slug.replace(/_index$/, "index") } @@ -81,21 +81,21 @@ export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug { } export function simplifySlug(fp: FullSlug): SimpleSlug { - const res = _stripSlashes(_trimSuffix(fp, "index"), true) + const res = stripSlashes(trimSuffix(fp, "index"), true) return (res.length === 0 ? "/" : res) as SimpleSlug } export function transformInternalLink(link: string): RelativeURL { let [fplike, anchor] = splitAnchor(decodeURI(link)) - const folderPath = _isFolderPath(fplike) + const folderPath = isFolderPath(fplike) let segments = fplike.split("/").filter((x) => x.length > 0) - let prefix = segments.filter(_isRelativeSegment).join("/") - let fp = segments.filter((seg) => !_isRelativeSegment(seg) && seg !== "").join("/") + let prefix = segments.filter(isRelativeSegment).join("/") + let fp = segments.filter((seg) => !isRelativeSegment(seg) && seg !== "").join("/") // manually add ext here as we want to not strip 'index' if it has an extension const simpleSlug = simplifySlug(slugifyFilePath(fp as FilePath)) - const joined = joinSegments(_stripSlashes(prefix), _stripSlashes(simpleSlug)) + const joined = joinSegments(stripSlashes(prefix), stripSlashes(simpleSlug)) const trail = folderPath ? "/" : "" const res = (_addRelativeToStart(joined) + trail + anchor) as RelativeURL return res @@ -168,6 +168,9 @@ export function resolveRelative(current: FullSlug, target: FullSlug | SimpleSlug export function splitAnchor(link: string): [string, string] { let [fp, anchor] = link.split("#", 2) + if (fp.endsWith(".pdf")) { + return [fp, anchor === undefined ? "" : `#${anchor}`] + } anchor = anchor === undefined ? "" : "#" + slugAnchor(anchor) return [fp, anchor] } @@ -206,8 +209,8 @@ export function transformLink(src: FullSlug, target: string, opts: TransformOpti if (opts.strategy === "relative") { return targetSlug as RelativeURL } else { - const folderTail = _isFolderPath(targetSlug) ? "/" : "" - const canonicalSlug = _stripSlashes(targetSlug.slice(".".length)) + const folderTail = isFolderPath(targetSlug) ? "/" : "" + const canonicalSlug = stripSlashes(targetSlug.slice(".".length)) let [targetCanonical, targetAnchor] = splitAnchor(canonicalSlug) if (opts.strategy === "shortest") { @@ -230,28 +233,29 @@ export function transformLink(src: FullSlug, target: string, opts: TransformOpti } } -function _isFolderPath(fplike: string): boolean { +// path helpers +function isFolderPath(fplike: string): boolean { return ( fplike.endsWith("/") || - _endsWith(fplike, "index") || - _endsWith(fplike, "index.md") || - _endsWith(fplike, "index.html") + endsWith(fplike, "index") || + endsWith(fplike, "index.md") || + endsWith(fplike, "index.html") ) } -function _endsWith(s: string, suffix: string): boolean { +export function endsWith(s: string, suffix: string): boolean { return s === suffix || s.endsWith("/" + suffix) } -function _trimSuffix(s: string, suffix: string): string { - if (_endsWith(s, suffix)) { +function trimSuffix(s: string, suffix: string): string { + if (endsWith(s, suffix)) { s = s.slice(0, -suffix.length) } return s } -function _containsForbiddenCharacters(s: string): boolean { - return s.includes(" ") || s.includes("#") || s.includes("?") +function containsForbiddenCharacters(s: string): boolean { + return s.includes(" ") || s.includes("#") || s.includes("?") || s.includes("&") } function _hasFileExtension(s: string): boolean { @@ -262,11 +266,11 @@ function _getFileExtension(s: string): string | undefined { return s.match(/\.[A-Za-z0-9]+$/)?.[0] } -function _isRelativeSegment(s: string): boolean { +function isRelativeSegment(s: string): boolean { return /^\.{0,2}$/.test(s) } -export function _stripSlashes(s: string, onlyStripPrefix?: boolean): string { +export function stripSlashes(s: string, onlyStripPrefix?: boolean): string { if (s.startsWith("/")) { s = s.substring(1) } diff --git a/quartz/util/theme.ts b/quartz/util/theme.ts index 47951c4f..9046cec6 100644 --- a/quartz/util/theme.ts +++ b/quartz/util/theme.ts @@ -7,6 +7,12 @@ export interface ColorScheme { secondary: string tertiary: string highlight: string + textHighlight: string +} + +interface Colors { + lightMode: ColorScheme + darkMode: ColorScheme } export interface Theme { @@ -15,12 +21,13 @@ export interface Theme { body: string code: string } - colors: { - lightMode: ColorScheme - darkMode: ColorScheme - } + cdnCaching: boolean + colors: Colors + fontOrigin: "googleFonts" | "local" } +export type ThemeKey = keyof Colors + const DEFAULT_SANS_SERIF = '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif' const DEFAULT_MONO = "ui-monospace, SFMono-Regular, SF Mono, Menlo, monospace" @@ -43,6 +50,7 @@ ${stylesheet.join("\n\n")} --secondary: ${theme.colors.lightMode.secondary}; --tertiary: ${theme.colors.lightMode.tertiary}; --highlight: ${theme.colors.lightMode.highlight}; + --textHighlight: ${theme.colors.lightMode.textHighlight}; --headerFont: "${theme.typography.header}", ${DEFAULT_SANS_SERIF}; --bodyFont: "${theme.typography.body}", ${DEFAULT_SANS_SERIF}; @@ -58,6 +66,7 @@ ${stylesheet.join("\n\n")} --secondary: ${theme.colors.darkMode.secondary}; --tertiary: ${theme.colors.darkMode.tertiary}; --highlight: ${theme.colors.darkMode.highlight}; + --textHighlight: ${theme.colors.darkMode.textHighlight}; } ` }