Revert "merge upstream"
Some checks are pending
Build and Test / build-and-test (macos-latest) (push) Waiting to run
Build and Test / build-and-test (ubuntu-latest) (push) Waiting to run
Build and Test / build-and-test (windows-latest) (push) Waiting to run
Build and Test / publish-tag (push) Waiting to run
/ test (push) Waiting to run
Deploy Quartz site to GitHub Pages / build (push) Waiting to run
Deploy Quartz site to GitHub Pages / deploy (push) Blocked by required conditions

This reverts commit b41a2669fe, reversing
changes made to 14697b2a0d.
This commit is contained in:
Kaz Saita(raspi5) 2024-10-24 17:43:56 +09:00
parent fd6c0cb28e
commit c3c48ce9eb
72 changed files with 1072 additions and 2753 deletions

View file

@ -1,11 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2 version: 2
updates: updates:
- package-ecosystem: "npm" - package-ecosystem: "npm"
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "weekly"
groups:
production-dependencies:
applies-to: "version-updates"
patterns:
- "*"

View file

@ -1,88 +0,0 @@
name: Docker build & push image
on:
push:
branches: [v4]
tags: ["v*"]
pull_request:
branches: [v4]
paths:
- .github/workflows/docker-build-push.yaml
- quartz/**
workflow_dispatch:
jobs:
build:
if: ${{ github.repository == 'jackyzha0/quartz' }} # Comment this out if you want to publish your own images on a fork!
runs-on: ubuntu-latest
steps:
- name: Set lowercase repository owner environment variable
run: |
echo "OWNER_LOWERCASE=${OWNER,,}" >> ${GITHUB_ENV}
env:
OWNER: "${{ github.repository_owner }}"
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4.4.1
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
install: true
driver-opts: |
image=moby/buildkit:master
network=host
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@v3.7.0
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata tags and labels on PRs
if: github.event_name == 'pull_request'
id: meta-pr
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ env.OWNER_LOWERCASE }}/quartz
tags: |
type=raw,value=sha-${{ env.GITHUB_SHA_SHORT }}
labels: |
org.opencontainers.image.source="https://github.com/${{ github.repository_owner }}/quartz"
- name: Extract metadata tags and labels for main, release or tag
if: github.event_name != 'pull_request'
id: meta
uses: docker/metadata-action@v5
with:
flavor: |
latest=auto
images: ghcr.io/${{ env.OWNER_LOWERCASE }}/quartz
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}.{{minor}}.{{patch}}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
type=raw,value=sha-${{ env.GITHUB_SHA_SHORT }}
labels: |
maintainer=${{ github.repository_owner }}
org.opencontainers.image.source="https://github.com/${{ github.repository_owner }}/quartz"
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v6
with:
push: ${{ github.event_name != 'pull_request' }}
build-args: |
GIT_SHA=${{ env.GITHUB_SHA }}
DOCKER_LABEL=sha-${{ env.GITHUB_SHA_SHORT }}
tags: ${{ steps.meta.outputs.tags || steps.meta-pr.outputs.tags }}
labels: ${{ steps.meta.outputs.labels || steps.meta-pr.outputs.labels }}
cache-from: type=gha
cache-to: type=gha

View file

@ -1,4 +1,4 @@
FROM node:20-slim AS builder FROM node:20-slim as builder
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY package.json . COPY package.json .
COPY package-lock.json* . COPY package-lock.json* .

View file

@ -29,7 +29,6 @@ Some common frontmatter fields that are natively supported by Quartz:
- `title`: Title of the page. If it isn't provided, Quartz will use the name of the file as the title. - `title`: Title of the page. If it isn't provided, Quartz will use the name of the file as the title.
- `description`: Description of the page used for link previews. - `description`: Description of the page used for link previews.
- `permalink`: A custom URL for the page that will remain constant even if the path to the file changes.
- `aliases`: Other names for this note. This is a list of strings. - `aliases`: Other names for this note. This is a list of strings.
- `tags`: Tags for this note. - `tags`: Tags for this note.
- `draft`: Whether to publish the page or not. This is one way to make [[private pages|pages private]] in Quartz. - `draft`: Whether to publish the page or not. This is one way to make [[private pages|pages private]] in Quartz.

View file

@ -21,7 +21,3 @@ This will start a local web server to run your Quartz on your computer. Open a w
> - `--serve`: run a local hot-reloading server to preview your Quartz > - `--serve`: run a local hot-reloading server to preview your Quartz
> - `--port`: what port to run the local preview server on > - `--port`: what port to run the local preview server on
> - `--concurrency`: how many threads to use to parse notes > - `--concurrency`: how many threads to use to parse notes
> [!warning] Not to be used for production
> Serve mode is intended for local previews only.
> For production workloads, see the page on [[hosting]].

View file

@ -21,7 +21,6 @@ const config: QuartzConfig = {
This part of the configuration concerns anything that can affect the whole site. The following is a list breaking down all the things you can configure: This part of the configuration concerns anything that can affect the whole site. The following is a list breaking down all the things you can configure:
- `pageTitle`: title of the site. This is also used when generating the [[RSS Feed]] for your site. - `pageTitle`: title of the site. This is also used when generating the [[RSS Feed]] for your site.
- `pageTitleSuffix`: a string added to the end of the page title. This only applies to the browser tab title, not the title shown at the top of the page.
- `enableSPA`: whether to enable [[SPA Routing]] on your site. - `enableSPA`: whether to enable [[SPA Routing]] on your site.
- `enablePopovers`: whether to enable [[popover previews]] on your site. - `enablePopovers`: whether to enable [[popover previews]] on your site.
- `analytics`: what to use for analytics on your site. Values can be - `analytics`: what to use for analytics on your site. Values can be
@ -33,7 +32,6 @@ This part of the configuration concerns anything that can affect the whole site.
- `{ provider: 'posthog', apiKey: '<your-posthog-project-apiKey>', host: '<your-posthog-host>' }`: use [Posthog](https://posthog.com/); - `{ provider: 'posthog', apiKey: '<your-posthog-project-apiKey>', host: '<your-posthog-host>' }`: use [Posthog](https://posthog.com/);
- `{ provider: 'tinylytics', siteId: '<your-site-id>' }`: use [Tinylytics](https://tinylytics.app/); - `{ provider: 'tinylytics', siteId: '<your-site-id>' }`: use [Tinylytics](https://tinylytics.app/);
- `{ provider: 'cabin' }` or `{ provider: 'cabin', host: 'https://cabin.example.com' }` (custom domain): use [Cabin](https://withcabin.com); - `{ provider: 'cabin' }` or `{ provider: 'cabin', host: 'https://cabin.example.com' }` (custom domain): use [Cabin](https://withcabin.com);
- `{provider: 'clarity', projectId: '<your-clarity-id-code' }`: use [Microsoft clarity](https://clarity.microsoft.com/). The project id can be found on top of the overview page.
- `locale`: used for [[i18n]] and date formatting - `locale`: used for [[i18n]] and date formatting
- `baseUrl`: this is used for sitemaps and RSS feeds that require an absolute URL to know where the canonical 'home' of your site lives. This is normally the deployed URL of your site (e.g. `quartz.jzhao.xyz` for this site). Do not include the protocol (i.e. `https://`) or any leading or trailing slashes. - `baseUrl`: this is used for sitemaps and RSS feeds that require an absolute URL to know where the canonical 'home' of your site lives. This is normally the deployed URL of your site (e.g. `quartz.jzhao.xyz` for this site). Do not include the protocol (i.e. `https://`) or any leading or trailing slashes.
- This should also include the subpath if you are [[hosting]] on GitHub pages without a custom domain. For example, if my repository is `jackyzha0/quartz`, GitHub pages would deploy to `https://jackyzha0.github.io/quartz` and the `baseUrl` would be `jackyzha0.github.io/quartz`. - This should also include the subpath if you are [[hosting]] on GitHub pages without a custom domain. For example, if my repository is `jackyzha0/quartz`, GitHub pages would deploy to `https://jackyzha0.github.io/quartz` and the `baseUrl` would be `jackyzha0.github.io/quartz`.
@ -103,7 +101,7 @@ transformers: [
] ]
``` ```
Some plugins are included by default in the [`quartz.config.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz.config.ts), but there are more available. Some plugins are included by default in the[ `quartz.config.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz.config.ts), but there are more available.
You can see a list of all plugins and their configuration options [[tags/plugin|here]]. You can see a list of all plugins and their configuration options [[tags/plugin|here]].

View file

@ -1,28 +0,0 @@
---
title: "Roam Research Compatibility"
tags:
- feature/transformer
---
[Roam Research](https://roamresearch.com) is a note-taking tool that organizes your knowledge graph in a unique and interconnected way.
Quartz supports transforming the special Markdown syntax from Roam Research (like `{{[[components]]}}` and other formatting) into
regular Markdown via the [[RoamFlavoredMarkdown]] plugin.
```typescript title="quartz.config.ts"
plugins: {
transformers: [
// ...
Plugin.RoamFlavoredMarkdown(),
Plugin.ObsidianFlavoredMarkdown(),
// ...
],
},
```
> [!warning]
> As seen above placement of `Plugin.RoamFlavoredMarkdown()` within `quartz.config.ts` is very important. It must come before `Plugin.ObsidianFlavoredMarkdown()`.
## Customization
This functionality is provided by the [[RoamFlavoredMarkdown]] plugin. See the plugin page for customization options.

View file

@ -63,18 +63,6 @@ type Options = {
category: string category: string
categoryId: string categoryId: string
// Url to folder with custom themes
// defaults to 'https://${cfg.baseUrl}/static/giscus'
themeUrl?: string
// filename for light theme .css file
// defaults to 'light'
lightTheme?: string
// filename for dark theme .css file
// defaults to 'dark'
darkTheme?: string
// how to map pages -> discussions // how to map pages -> discussions
// defaults to 'url' // defaults to 'url'
mapping?: "url" | "title" | "og:title" | "specific" | "number" | "pathname" mapping?: "url" | "title" | "og:title" | "specific" | "number" | "pathname"
@ -93,24 +81,3 @@ type Options = {
} }
} }
``` ```
#### Custom CSS theme
Quartz supports custom theme for Giscus. To use a custom CSS theme, place the `.css` file inside the `quartz/static` folder and set the configuration values.
For example, if you have a light theme `light-theme.css`, a dark theme `dark-theme.css`, and your Quartz site is hosted at `https://example.com/`:
```ts
afterBody: [
Component.Comments({
provider: 'giscus',
options: {
// Other options
themeUrl: "https://example.com/static/giscus", // corresponds to quartz/static/giscus/
lightTheme: "light-theme", // corresponds to light-theme.css in quartz/static/giscus/
darkTheme: "dark-theme", // corresponds to dark-theme.css quartz/static/giscus/
}
}),
],
```

View file

@ -6,6 +6,7 @@ draft: true
- static dead link detection - static dead link detection
- cursor chat extension - cursor chat extension
- https://giscus.app/ extension
- sidenotes? https://github.com/capnfabs/paperesque - sidenotes? https://github.com/capnfabs/paperesque
- direct match in search using double quotes - direct match in search using double quotes
- https://help.obsidian.md/Advanced+topics/Using+Obsidian+URI - https://help.obsidian.md/Advanced+topics/Using+Obsidian+URI

View file

@ -61,8 +61,6 @@ jobs:
with: with:
fetch-depth: 0 # Fetch all history for git info fetch-depth: 0 # Fetch all history for git info
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with:
node-version: 22
- name: Install Dependencies - name: Install Dependencies
run: npm ci run: npm ci
- name: Build Quartz - name: Build Quartz
@ -208,7 +206,7 @@ build:
paths: paths:
- public - public
tags: tags:
- gitlab-org-docker - docker
pages: pages:
stage: deploy stage: deploy

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View file

@ -13,19 +13,15 @@ export interface FullPageLayout {
beforeBody: QuartzComponent[] // laid out vertically beforeBody: QuartzComponent[] // laid out vertically
pageBody: QuartzComponent // single component pageBody: QuartzComponent // single component
afterBody: QuartzComponent[] // laid out vertically afterBody: QuartzComponent[] // laid out vertically
left: QuartzComponent[] // vertical on desktop and tablet, horizontal on mobile left: QuartzComponent[] // vertical on desktop, horizontal on mobile
right: QuartzComponent[] // vertical on desktop, horizontal on tablet and mobile right: QuartzComponent[] // vertical on desktop, horizontal on mobile
footer: QuartzComponent // single component footer: QuartzComponent // single component
} }
``` ```
These correspond to following parts of the page: These correspond to following parts of the page:
| Layout | Preview | ![[quartz layout.png|800]]
| ------------------------------- | ----------------------------------- |
| Desktop (width > 1200px) | ![[quartz-layout-desktop.png\|800]] |
| Tablet (800px < width < 1200px) | ![[quartz-layout-tablet.png\|800]] |
| Mobile (width < 800px) | ![[quartz-layout-mobile.png\|800]] |
> [!note] > [!note]
> There are two additional layout fields that are _not_ shown in the above diagram. > There are two additional layout fields that are _not_ shown in the above diagram.
@ -37,23 +33,6 @@ Quartz **components**, like plugins, can take in additional properties as config
See [a list of all the components](component.md) for all available components along with their configuration options. You can also checkout the guide on [[creating components]] if you're interested in further customizing the behaviour of Quartz. See [a list of all the components](component.md) for all available components along with their configuration options. You can also checkout the guide on [[creating components]] if you're interested in further customizing the behaviour of Quartz.
### Layout breakpoints
Quartz has different layouts depending on the width the screen viewing the website.
The breakpoints for layouts can be configured in `variables.scss`.
- `mobile`: screen width below this size will use mobile layout.
- `desktop`: screen width above this size will use desktop layout.
- Screen width between `mobile` and `desktop` width will use the tablet layout.
```scss
$breakpoints: (
mobile: 800px,
desktop: 1200px,
);
```
### Style ### Style
Most meaningful style changes like colour scheme and font can be done simply through the [[configuration#General Configuration|general configuration]] options. However, if you'd like to make more involved style changes, you can do this by writing your own styles. Quartz 4, like Quartz 3, uses [Sass](https://sass-lang.com/guide/) for styling. Most meaningful style changes like colour scheme and font can be done simply through the [[configuration#General Configuration|general configuration]] options. However, if you'd like to make more involved style changes, you can do this by writing your own styles. Quartz 4, like Quartz 3, uses [Sass](https://sass-lang.com/guide/) for styling.

View file

@ -12,7 +12,6 @@ This plugin adds LaTeX support to Quartz. See [[features/Latex|Latex]] for more
This plugin accepts the following configuration options: This plugin accepts the following configuration options:
- `renderEngine`: the engine to use to render LaTeX equations. Can be `"katex"` for [KaTeX](https://katex.org/) or `"mathjax"` for [MathJax](https://www.mathjax.org/) [SVG rendering](https://docs.mathjax.org/en/latest/output/svg.html). Defaults to KaTeX. - `renderEngine`: the engine to use to render LaTeX equations. Can be `"katex"` for [KaTeX](https://katex.org/) or `"mathjax"` for [MathJax](https://www.mathjax.org/) [SVG rendering](https://docs.mathjax.org/en/latest/output/svg.html). Defaults to KaTeX.
- `customMacros`: custom macros for all LaTeX blocks. It takes the form of a key-value pair where the key is a new command name and the value is the expansion of the macro. For example: `{"\\R": "\\mathbb{R}"}`
## API ## API

View file

@ -1,26 +0,0 @@
---
title: RoamFlavoredMarkdown
tags:
- plugin/transformer
---
This plugin provides support for [Roam Research](https://roamresearch.com) compatibility. See [[Roam Research Compatibility]] for more information.
> [!note]
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
This plugin accepts the following configuration options:
- `orComponent`: If `true` (default), converts Roam `{{ or:ONE|TWO|THREE }}` shortcodes into HTML Dropdown options.
- `TODOComponent`: If `true` (default), converts Roam `{{[[TODO]]}}` shortcodes into HTML check boxes.
- `DONEComponent`: If `true` (default), converts Roam `{{[[DONE]]}}` shortcodes into checked HTML check boxes.
- `videoComponent`: If `true` (default), converts Roam `{{[[video]]:URL}}` shortcodes into embeded HTML video.
- `audioComponent`: If `true` (default), converts Roam `{{[[audio]]:URL}}` shortcodes into embeded HTML audio.
- `pdfComponent`: If `true` (default), converts Roam `{{[[pdf]]:URL}}` shortcodes into embeded HTML PDF viewer.
- `blockquoteComponent`: If `true` (default), converts Roam `{{[[>]]}}` shortcodes into Quartz blockquotes.
## API
- Category: Transformer
- Function name: `Plugin.RoamFlavoredMarkdown()`.
- Source: [`quartz/plugins/transformers/roam.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/roam.ts).

View file

@ -20,14 +20,10 @@ Want to see what Quartz can do? Here are some cool community gardens:
- [Sideny's 3D Artist's Handbook](https://sidney-eliot.github.io/3d-artists-handbook/) - [Sideny's 3D Artist's Handbook](https://sidney-eliot.github.io/3d-artists-handbook/)
- [Brandon Boswell's Garden](https://brandonkboswell.com) - [Brandon Boswell's Garden](https://brandonkboswell.com)
- [Scaling Synthesis - A hypertext research notebook](https://scalingsynthesis.com/) - [Scaling Synthesis - A hypertext research notebook](https://scalingsynthesis.com/)
- [Simon's Second Brain: Crafted, Curated, Connected, Compounded](https://brain.ssp.sh/)
- [Data Engineering Vault: A Second Brain Knowledge Network](https://vault.ssp.sh/)
- [Data Dictionary 🧠](https://glossary.airbyte.com/) - [Data Dictionary 🧠](https://glossary.airbyte.com/)
- [sspaeti.com's Second Brain](https://brain.sspaeti.com/)
- [🪴Aster's notebook](https://notes.asterhu.com) - [🪴Aster's notebook](https://notes.asterhu.com)
- [Gatekeeper Wiki](https://www.gatekeeper.wiki) - [Gatekeeper Wiki](https://www.gatekeeper.wiki)
- [Ellie's Notes](https://ellie.wtf) - [Ellie's Notes](https://ellie.wtf)
- [🥷🏻🌳🍃 Computer Science & Thinkering Garden](https://notes.yxy.ninja)
- [Eledah's Crystalline](https://blog.eledah.ir/)
- [🌓 Projects & Privacy - FOSS, tech, law](https://be-far.com)
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)! If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)!

1661
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
"name": "@jackyzha0/quartz", "name": "@jackyzha0/quartz",
"description": "🌱 publish your digital garden and notes as a website", "description": "🌱 publish your digital garden and notes as a website",
"private": true, "private": true,
"version": "4.4.0", "version": "4.3.0",
"type": "module", "type": "module",
"author": "jackyzha0 <j.zhao2k19@gmail.com>", "author": "jackyzha0 <j.zhao2k19@gmail.com>",
"license": "MIT", "license": "MIT",
@ -36,40 +36,38 @@
}, },
"dependencies": { "dependencies": {
"@clack/prompts": "^0.7.0", "@clack/prompts": "^0.7.0",
"@floating-ui/dom": "^1.6.11", "@floating-ui/dom": "^1.6.8",
"@napi-rs/simple-git": "0.1.19", "@napi-rs/simple-git": "0.1.16",
"@tweenjs/tween.js": "^25.0.0",
"async-mutex": "^0.5.0", "async-mutex": "^0.5.0",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"chokidar": "^4.0.1", "chokidar": "^3.6.0",
"cli-spinner": "^0.2.10", "cli-spinner": "^0.2.10",
"d3": "^7.9.0", "d3": "^7.9.0",
"esbuild-sass-plugin": "^3.3.1", "esbuild-sass-plugin": "^2.16.1",
"flexsearch": "0.7.43", "flexsearch": "0.7.43",
"github-slugger": "^2.0.0", "github-slugger": "^2.0.0",
"globby": "^14.0.2", "globby": "^14.0.2",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"hast-util-to-html": "^9.0.3", "hast-util-to-html": "^9.0.1",
"hast-util-to-jsx-runtime": "^2.3.2", "hast-util-to-jsx-runtime": "^2.3.0",
"hast-util-to-string": "^3.0.1", "hast-util-to-string": "^3.0.0",
"is-absolute-url": "^4.0.1", "is-absolute-url": "^4.0.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lightningcss": "^1.27.0", "lightningcss": "^1.25.1",
"mdast-util-find-and-replace": "^3.0.1", "mdast-util-find-and-replace": "^3.0.1",
"mdast-util-to-hast": "^13.2.0", "mdast-util-to-hast": "^13.2.0",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"micromorph": "^0.4.5", "micromorph": "^0.4.5",
"pixi.js": "^8.5.1", "preact": "^10.22.1",
"preact": "^10.24.3", "preact-render-to-string": "^6.5.7",
"preact-render-to-string": "^6.5.11",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"pretty-time": "^1.1.0", "pretty-time": "^1.1.0",
"reading-time": "^1.5.0", "reading-time": "^1.5.0",
"rehype-autolink-headings": "^7.1.0", "rehype-autolink-headings": "^7.1.0",
"rehype-citation": "^2.2.0", "rehype-citation": "^2.0.0",
"rehype-katex": "^7.0.1", "rehype-katex": "^7.0.0",
"rehype-mathjax": "^6.0.0", "rehype-mathjax": "^6.0.0",
"rehype-pretty-code": "^0.14.0", "rehype-pretty-code": "^0.13.2",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"rehype-slug": "^6.0.0", "rehype-slug": "^6.0.0",
"remark": "^15.0.1", "remark": "^15.0.1",
@ -78,19 +76,19 @@
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"remark-parse": "^11.0.0", "remark-parse": "^11.0.0",
"remark-rehype": "^11.1.1", "remark-rehype": "^11.1.0",
"remark-smartypants": "^3.0.2", "remark-smartypants": "^3.0.2",
"rfdc": "^1.4.1", "rfdc": "^1.4.1",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"serve-handler": "^6.1.6", "serve-handler": "^6.1.5",
"shiki": "^1.22.0", "shiki": "^1.10.3",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"to-vfile": "^8.0.0", "to-vfile": "^8.0.0",
"toml": "^3.0.0", "toml": "^3.0.0",
"unified": "^11.0.5", "unified": "^11.0.4",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0",
"vfile": "^6.0.3", "vfile": "^6.0.2",
"workerpool": "^9.2.0", "workerpool": "^9.1.3",
"ws": "^8.18.0", "ws": "^8.18.0",
"yargs": "^17.7.2" "yargs": "^17.7.2"
}, },
@ -99,14 +97,14 @@
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/hast": "^3.0.4", "@types/hast": "^3.0.4",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/node": "^22.7.7", "@types/node": "^22.1.0",
"@types/pretty-time": "^1.1.5", "@types/pretty-time": "^1.1.5",
"@types/source-map-support": "^0.5.10", "@types/source-map-support": "^0.5.10",
"@types/ws": "^8.5.12", "@types/ws": "^8.5.12",
"@types/yargs": "^17.0.33", "@types/yargs": "^17.0.32",
"esbuild": "^0.24.0", "esbuild": "^0.19.9",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"tsx": "^4.19.1", "tsx": "^4.16.2",
"typescript": "^5.6.3" "typescript": "^5.5.3"
} }
} }

View file

@ -38,13 +38,8 @@ type BuildData = {
type FileEvent = "add" | "change" | "delete" type FileEvent = "add" | "change" | "delete"
function newBuildId() {
return Math.random().toString(36).substring(2, 8)
}
async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) { async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
const ctx: BuildCtx = { const ctx: BuildCtx = {
buildId: newBuildId(),
argv, argv,
cfg, cfg,
allSlugs: [], allSlugs: [],
@ -162,13 +157,10 @@ async function partialRebuildFromEntrypoint(
return return
} }
const buildId = newBuildId() const buildStart = new Date().getTime()
ctx.buildId = buildId buildData.lastBuildMs = buildStart
buildData.lastBuildMs = new Date().getTime()
const release = await mut.acquire() const release = await mut.acquire()
if (buildData.lastBuildMs > buildStart) {
// if there's another build after us, release and let them do it
if (ctx.buildId !== buildId) {
release() release()
return return
} }
@ -359,22 +351,26 @@ async function rebuildFromEntrypoint(
toRemove.add(filePath) toRemove.add(filePath)
} }
const buildId = newBuildId() const buildStart = new Date().getTime()
ctx.buildId = buildId buildData.lastBuildMs = buildStart
buildData.lastBuildMs = new Date().getTime()
const release = await mut.acquire() const release = await mut.acquire()
// there's another build after us, release and let them do it // there's another build after us, release and let them do it
if (ctx.buildId !== buildId) { if (buildData.lastBuildMs > buildStart) {
release() release()
return return
} }
const perf = new PerfTimer() const perf = new PerfTimer()
console.log(chalk.yellow("Detected change, rebuilding...")) console.log(chalk.yellow("Detected change, rebuilding..."))
try { try {
const filesToRebuild = [...toRebuild].filter((fp) => !toRemove.has(fp)) const filesToRebuild = [...toRebuild].filter((fp) => !toRemove.has(fp))
const trackedSlugs = [...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets])]
.filter((fp) => !toRemove.has(fp))
.map((fp) => slugifyFilePath(path.posix.relative(argv.directory, fp) as FilePath))
ctx.allSlugs = [...new Set([...initialSlugs, ...trackedSlugs])]
const parsedContent = await parseMarkdown(ctx, filesToRebuild) const parsedContent = await parseMarkdown(ctx, filesToRebuild)
for (const content of parsedContent) { for (const content of parsedContent) {
const [_tree, vfile] = content const [_tree, vfile] = content
@ -388,13 +384,6 @@ async function rebuildFromEntrypoint(
const parsedFiles = [...contentMap.values()] const parsedFiles = [...contentMap.values()]
const filteredContent = filterContent(ctx, parsedFiles) const filteredContent = filterContent(ctx, parsedFiles)
// re-update slugs
const trackedSlugs = [...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets])]
.filter((fp) => !toRemove.has(fp))
.map((fp) => slugifyFilePath(path.posix.relative(argv.directory, fp) as FilePath))
ctx.allSlugs = [...new Set([...initialSlugs, ...trackedSlugs])]
// TODO: we can probably traverse the link graph to figure out what's safe to delete here // TODO: we can probably traverse the link graph to figure out what's safe to delete here
// instead of just deleting everything // instead of just deleting everything
await rimraf(path.join(argv.output, ".*"), { glob: true }) await rimraf(path.join(argv.output, ".*"), { glob: true })
@ -407,10 +396,10 @@ async function rebuildFromEntrypoint(
} }
} }
release()
clientRefresh() clientRefresh()
toRebuild.clear() toRebuild.clear()
toRemove.clear() toRemove.clear()
release()
} }
export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => { export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => {

View file

@ -38,14 +38,9 @@ export type Analytics =
provider: "cabin" provider: "cabin"
host?: string host?: string
} }
| {
provider: "clarity"
projectId?: string
}
export interface GlobalConfiguration { export interface GlobalConfiguration {
pageTitle: string pageTitle: string
pageTitleSuffix?: string
/** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */ /** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */
enableSPA: boolean enableSPA: boolean
/** Whether to display Wikipedia-style popovers when hovering over links */ /** Whether to display Wikipedia-style popovers when hovering over links */

View file

@ -457,25 +457,7 @@ export async function handleUpdate(argv) {
await popContentFolder(contentFolder) await popContentFolder(contentFolder)
console.log("Ensuring dependencies are up to date") console.log("Ensuring dependencies are up to date")
const res = spawnSync("npm", ["i"], { stdio: "inherit" })
/*
On Windows, if the command `npm` is really `npm.cmd', this call fails
as it will be unable to find `npm`. This is often the case on systems
where `npm` is installed via a package manager.
This means `npx quartz update` will not actually update dependencies
on Windows, without a manual `npm i` from the caller.
However, by spawning a shell, we are able to call `npm.cmd`.
See: https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows
*/
const opts = { stdio: "inherit" }
if (process.platform === "win32") {
opts.shell = true
}
const res = spawnSync("npm", ["i"], opts)
if (res.status === 0) { if (res.status === 0) {
console.log(chalk.green("Done!")) console.log(chalk.green("Done!"))
} else { } else {

View file

@ -10,9 +10,6 @@ type Options = {
repoId: string repoId: string
category: string category: string
categoryId: string categoryId: string
themeUrl?: string
lightTheme?: string
darkTheme?: string
mapping?: "url" | "title" | "og:title" | "specific" | "number" | "pathname" mapping?: "url" | "title" | "og:title" | "specific" | "number" | "pathname"
strict?: boolean strict?: boolean
reactionsEnabled?: boolean reactionsEnabled?: boolean
@ -37,11 +34,6 @@ export default ((opts: Options) => {
data-strict={boolToStringBool(opts.options.strict ?? true)} data-strict={boolToStringBool(opts.options.strict ?? true)}
data-reactions-enabled={boolToStringBool(opts.options.reactionsEnabled ?? true)} data-reactions-enabled={boolToStringBool(opts.options.reactionsEnabled ?? true)}
data-input-position={opts.options.inputPosition ?? "bottom"} data-input-position={opts.options.inputPosition ?? "bottom"}
data-light-theme={opts.options.lightTheme ?? "light"}
data-dark-theme={opts.options.darkTheme ?? "dark"}
data-theme-url={
opts.options.themeUrl ?? `https://${cfg.baseUrl ?? "example.com"}/static/giscus`
}
></div> ></div>
) )
} }

View file

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

View file

@ -44,9 +44,12 @@ export default ((userOpts?: Partial<Options>) => {
// memoized // memoized
let fileTree: FileNode let fileTree: FileNode
let jsonTree: string let jsonTree: string
let lastBuildId: string = ""
function constructFileTree(allFiles: QuartzPluginData[]) { function constructFileTree(allFiles: QuartzPluginData[]) {
if (fileTree) {
return
}
// Construct tree from allFiles // Construct tree from allFiles
fileTree = new FileNode("") fileTree = new FileNode("")
allFiles.forEach((file) => fileTree.add(file)) allFiles.forEach((file) => fileTree.add(file))
@ -73,17 +76,12 @@ export default ((userOpts?: Partial<Options>) => {
} }
const Explorer: QuartzComponent = ({ const Explorer: QuartzComponent = ({
ctx,
cfg, cfg,
allFiles, allFiles,
displayClass, displayClass,
fileData, fileData,
}: QuartzComponentProps) => { }: QuartzComponentProps) => {
if (ctx.buildId !== lastBuildId) { constructFileTree(allFiles)
lastBuildId = ctx.buildId
constructFileTree(allFiles)
}
return ( return (
<div class={classNames(displayClass, "explorer")}> <div class={classNames(displayClass, "explorer")}>
<button <button
@ -93,8 +91,6 @@ export default ((userOpts?: Partial<Options>) => {
data-collapsed={opts.folderDefaultState} data-collapsed={opts.folderDefaultState}
data-savestate={opts.useSavedState} data-savestate={opts.useSavedState}
data-tree={jsonTree} data-tree={jsonTree}
aria-controls="explorer-content"
aria-expanded={opts.folderDefaultState === "open"}
> >
<h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2> <h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2>
<svg <svg

View file

@ -65,32 +65,31 @@ export default ((opts?: GraphOptions) => {
<h3>{i18n(cfg.locale).components.graph.title}</h3> <h3>{i18n(cfg.locale).components.graph.title}</h3>
<div class="graph-outer"> <div class="graph-outer">
<div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div> <div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
<button id="global-graph-icon" aria-label="Global Graph"> <svg
<svg version="1.1"
version="1.1" id="global-graph-icon"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink" xmlnsXlink="http://www.w3.org/1999/xlink"
x="0px" x="0px"
y="0px" y="0px"
viewBox="0 0 55 55" viewBox="0 0 55 55"
fill="currentColor" fill="currentColor"
xmlSpace="preserve" xmlSpace="preserve"
> >
<path <path
d="M49,0c-3.309,0-6,2.691-6,6c0,1.035,0.263,2.009,0.726,2.86l-9.829,9.829C32.542,17.634,30.846,17,29,17 d="M49,0c-3.309,0-6,2.691-6,6c0,1.035,0.263,2.009,0.726,2.86l-9.829,9.829C32.542,17.634,30.846,17,29,17
s-3.542,0.634-4.898,1.688l-7.669-7.669C16.785,10.424,17,9.74,17,9c0-2.206-1.794-4-4-4S9,6.794,9,9s1.794,4,4,4 s-3.542,0.634-4.898,1.688l-7.669-7.669C16.785,10.424,17,9.74,17,9c0-2.206-1.794-4-4-4S9,6.794,9,9s1.794,4,4,4
c0.74,0,1.424-0.215,2.019-0.567l7.669,7.669C21.634,21.458,21,23.154,21,25s0.634,3.542,1.688,4.897L10.024,42.562 c0.74,0,1.424-0.215,2.019-0.567l7.669,7.669C21.634,21.458,21,23.154,21,25s0.634,3.542,1.688,4.897L10.024,42.562
C8.958,41.595,7.549,41,6,41c-3.309,0-6,2.691-6,6s2.691,6,6,6s6-2.691,6-6c0-1.035-0.263-2.009-0.726-2.86l12.829-12.829 C8.958,41.595,7.549,41,6,41c-3.309,0-6,2.691-6,6s2.691,6,6,6s6-2.691,6-6c0-1.035-0.263-2.009-0.726-2.86l12.829-12.829
c1.106,0.86,2.44,1.436,3.898,1.619v10.16c-2.833,0.478-5,2.942-5,5.91c0,3.309,2.691,6,6,6s6-2.691,6-6c0-2.967-2.167-5.431-5-5.91 c1.106,0.86,2.44,1.436,3.898,1.619v10.16c-2.833,0.478-5,2.942-5,5.91c0,3.309,2.691,6,6,6s6-2.691,6-6c0-2.967-2.167-5.431-5-5.91
v-10.16c1.458-0.183,2.792-0.759,3.898-1.619l7.669,7.669C41.215,39.576,41,40.26,41,41c0,2.206,1.794,4,4,4s4-1.794,4-4 v-10.16c1.458-0.183,2.792-0.759,3.898-1.619l7.669,7.669C41.215,39.576,41,40.26,41,41c0,2.206,1.794,4,4,4s4-1.794,4-4
s-1.794-4-4-4c-0.74,0-1.424,0.215-2.019,0.567l-7.669-7.669C36.366,28.542,37,26.846,37,25s-0.634-3.542-1.688-4.897l9.665-9.665 s-1.794-4-4-4c-0.74,0-1.424,0.215-2.019,0.567l-7.669-7.669C36.366,28.542,37,26.846,37,25s-0.634-3.542-1.688-4.897l9.665-9.665
C46.042,11.405,47.451,12,49,12c3.309,0,6-2.691,6-6S52.309,0,49,0z M11,9c0-1.103,0.897-2,2-2s2,0.897,2,2s-0.897,2-2,2 C46.042,11.405,47.451,12,49,12c3.309,0,6-2.691,6-6S52.309,0,49,0z M11,9c0-1.103,0.897-2,2-2s2,0.897,2,2s-0.897,2-2,2
S11,10.103,11,9z M6,51c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S8.206,51,6,51z M33,49c0,2.206-1.794,4-4,4s-4-1.794-4-4 S11,10.103,11,9z M6,51c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S8.206,51,6,51z M33,49c0,2.206-1.794,4-4,4s-4-1.794-4-4
s1.794-4,4-4S33,46.794,33,49z M29,31c-3.309,0-6-2.691-6-6s2.691-6,6-6s6,2.691,6,6S32.309,31,29,31z M47,41c0,1.103-0.897,2-2,2 s1.794-4,4-4S33,46.794,33,49z M29,31c-3.309,0-6-2.691-6-6s2.691-6,6-6s6,2.691,6,6S32.309,31,29,31z M47,41c0,1.103-0.897,2-2,2
s-2-0.897-2-2s0.897-2,2-2S47,39.897,47,41z M49,10c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S51.206,10,49,10z" s-2-0.897-2-2s0.897-2,2-2S47,39.897,47,41z M49,10c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S51.206,10,49,10z"
/> />
</svg> </svg>
</button>
</div> </div>
<div id="global-graph-outer"> <div id="global-graph-outer">
<div id="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div> <div id="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div>

View file

@ -6,9 +6,7 @@ import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } fro
export default (() => { export default (() => {
const Head: QuartzComponent = ({ cfg, fileData, externalResources }: QuartzComponentProps) => { const Head: QuartzComponent = ({ cfg, fileData, externalResources }: QuartzComponentProps) => {
const titleSuffix = cfg.pageTitleSuffix ?? "" const title = fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title
const title =
(fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title) + titleSuffix
const description = const description =
fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description
const { css, js } = externalResources const { css, js } = externalResources

View file

@ -46,13 +46,11 @@ export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit, sort
return ( return (
<li class="section-li"> <li class="section-li">
<div class="section"> <div class="section">
<div> {page.dates && (
{page.dates && ( <p class="meta">
<p class="meta"> <Date date={getDate(cfg, page)!} locale={cfg.locale} />
<Date date={getDate(cfg, page)!} locale={cfg.locale} /> </p>
</p> )}
)}
</div>
<div class="desc"> <div class="desc">
<h3> <h3>
<a href={resolveRelative(fileData.slug!, page.slug!)} class="internal"> <a href={resolveRelative(fileData.slug!, page.slug!)} class="internal">

View file

@ -19,16 +19,24 @@ export default ((userOpts?: Partial<SearchOptions>) => {
const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder
return ( return (
<div class={classNames(displayClass, "search")}> <div class={classNames(displayClass, "search")}>
<button class="search-button" id="search-button"> <div id="search-icon">
<p>{i18n(cfg.locale).components.search.title}</p> <p>{i18n(cfg.locale).components.search.title}</p>
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.9 19.7"> <div></div>
<title>Search</title> <svg
tabIndex={0}
aria-labelledby="title desc"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 19.9 19.7"
>
<title id="title">Search</title>
<desc id="desc">Search</desc>
<g class="search-path" fill="none"> <g class="search-path" fill="none">
<path stroke-linecap="square" d="M18.5 18.3l-5.4-5.4" /> <path stroke-linecap="square" d="M18.5 18.3l-5.4-5.4" />
<circle cx="8" cy="8" r="7" /> <circle cx="8" cy="8" r="7" />
</g> </g>
</svg> </svg>
</button> </div>
<div id="search-container"> <div id="search-container">
<div id="search-space"> <div id="search-space">
<input <input

View file

@ -26,13 +26,7 @@ const TableOfContents: QuartzComponent = ({
return ( return (
<div class={classNames(displayClass, "toc")}> <div class={classNames(displayClass, "toc")}>
<button <button type="button" id="toc" class={fileData.collapseToc ? "collapsed" : ""}>
type="button"
id="toc"
class={fileData.collapseToc ? "collapsed" : ""}
aria-controls="toc-content"
aria-expanded={!fileData.collapseToc}
>
<h3>{i18n(cfg.locale).components.tableOfContents.title}</h3> <h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -49,7 +43,7 @@ const TableOfContents: QuartzComponent = ({
<polyline points="6 9 12 15 18 9"></polyline> <polyline points="6 9 12 15 18 9"></polyline>
</svg> </svg>
</button> </button>
<div id="toc-content" class={fileData.collapseToc ? "collapsed" : ""}> <div id="toc-content">
<ul class="overflow"> <ul class="overflow">
{fileData.toc.map((tocEntry) => ( {fileData.toc.map((tocEntry) => (
<li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}> <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>

View file

@ -242,8 +242,8 @@ export function renderPage(
</div> </div>
</div> </div>
{RightComponent} {RightComponent}
<Footer {...componentData} />
</Body> </Body>
<Footer {...componentData} />
</div> </div>
</body> </body>
{pageResources.js {pageResources.js

View file

@ -13,7 +13,7 @@ const changeTheme = (e: CustomEventMap["themechange"]) => {
{ {
giscus: { giscus: {
setConfig: { setConfig: {
theme: getThemeUrl(getThemeName(theme)), theme: theme,
}, },
}, },
}, },
@ -21,36 +21,12 @@ const changeTheme = (e: CustomEventMap["themechange"]) => {
) )
} }
const getThemeName = (theme: string) => {
if (theme !== "dark" && theme !== "light") {
return theme
}
const giscusContainer = document.querySelector(".giscus") as GiscusElement
if (!giscusContainer) {
return theme
}
const darkGiscus = giscusContainer.dataset.darkTheme ?? "dark"
const lightGiscus = giscusContainer.dataset.lightTheme ?? "light"
return theme === "dark" ? darkGiscus : lightGiscus
}
const getThemeUrl = (theme: string) => {
const giscusContainer = document.querySelector(".giscus") as GiscusElement
if (!giscusContainer) {
return `https://giscus.app/themes/${theme}.css`
}
return `${giscusContainer.dataset.themeUrl ?? "https://giscus.app/themes"}/${theme}.css`
}
type GiscusElement = Omit<HTMLElement, "dataset"> & { type GiscusElement = Omit<HTMLElement, "dataset"> & {
dataset: DOMStringMap & { dataset: DOMStringMap & {
repo: `${string}/${string}` repo: `${string}/${string}`
repoId: string repoId: string
category: string category: string
categoryId: string categoryId: string
themeUrl: string
lightTheme: string
darkTheme: string
mapping: "url" | "title" | "og:title" | "specific" | "number" | "pathname" mapping: "url" | "title" | "og:title" | "specific" | "number" | "pathname"
strict: string strict: string
reactionsEnabled: string reactionsEnabled: string
@ -81,7 +57,7 @@ document.addEventListener("nav", () => {
const theme = document.documentElement.getAttribute("saved-theme") const theme = document.documentElement.getAttribute("saved-theme")
if (theme) { if (theme) {
giscusScript.setAttribute("data-theme", getThemeUrl(getThemeName(theme))) giscusScript.setAttribute("data-theme", theme)
} }
giscusContainer.appendChild(giscusScript) giscusContainer.appendChild(giscusScript)

View file

@ -11,8 +11,7 @@ const emitThemeChangeEvent = (theme: "light" | "dark") => {
document.addEventListener("nav", () => { document.addEventListener("nav", () => {
const switchTheme = (e: Event) => { const switchTheme = (e: Event) => {
const newTheme = const newTheme = (e.target as HTMLInputElement)?.checked ? "dark" : "light"
document.documentElement.getAttribute("saved-theme") === "dark" ? "light" : "dark"
document.documentElement.setAttribute("saved-theme", newTheme) document.documentElement.setAttribute("saved-theme", newTheme)
localStorage.setItem("theme", newTheme) localStorage.setItem("theme", newTheme)
emitThemeChangeEvent(newTheme) emitThemeChangeEvent(newTheme)
@ -22,13 +21,17 @@ document.addEventListener("nav", () => {
const newTheme = e.matches ? "dark" : "light" const newTheme = e.matches ? "dark" : "light"
document.documentElement.setAttribute("saved-theme", newTheme) document.documentElement.setAttribute("saved-theme", newTheme)
localStorage.setItem("theme", newTheme) localStorage.setItem("theme", newTheme)
toggleSwitch.checked = e.matches
emitThemeChangeEvent(newTheme) emitThemeChangeEvent(newTheme)
} }
// Darkmode toggle // Darkmode toggle
const themeButton = document.querySelector("#darkmode") as HTMLButtonElement const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement
themeButton.addEventListener("click", switchTheme) toggleSwitch.addEventListener("change", switchTheme)
window.addCleanup(() => themeButton.removeEventListener("click", switchTheme)) window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
if (currentTheme === "dark") {
toggleSwitch.checked = true
}
// Listen for changes in prefers-color-scheme // Listen for changes in prefers-color-scheme
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)") const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)")

View file

@ -17,14 +17,11 @@ const observer = new IntersectionObserver((entries) => {
function toggleExplorer(this: HTMLElement) { function toggleExplorer(this: HTMLElement) {
this.classList.toggle("collapsed") this.classList.toggle("collapsed")
this.setAttribute(
"aria-expanded",
this.getAttribute("aria-expanded") === "true" ? "false" : "true",
)
const content = this.nextElementSibling as MaybeHTMLElement const content = this.nextElementSibling as MaybeHTMLElement
if (!content) return if (!content) return
content.classList.toggle("collapsed") content.classList.toggle("collapsed")
content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
} }
function toggleFolder(evt: MouseEvent) { function toggleFolder(evt: MouseEvent) {

View file

@ -1,54 +1,17 @@
import type { ContentDetails } from "../../plugins/emitters/contentIndex" import type { ContentDetails } from "../../plugins/emitters/contentIndex"
import { import * as d3 from "d3"
SimulationNodeDatum,
SimulationLinkDatum,
Simulation,
forceSimulation,
forceManyBody,
forceCenter,
forceLink,
forceCollide,
zoomIdentity,
select,
drag,
zoom,
} from "d3"
import { Text, Graphics, Application, Container, Circle } from "pixi.js"
import { Group as TweenGroup, Tween as Tweened } from "@tweenjs/tween.js"
import { registerEscapeHandler, removeAllChildren } from "./util" import { registerEscapeHandler, removeAllChildren } from "./util"
import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path" import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path"
import { D3Config } from "../Graph"
type GraphicsInfo = {
color: string
gfx: Graphics
alpha: number
active: boolean
}
type NodeData = { type NodeData = {
id: SimpleSlug id: SimpleSlug
text: string text: string
tags: string[] tags: string[]
} & SimulationNodeDatum } & d3.SimulationNodeDatum
type SimpleLinkData = {
source: SimpleSlug
target: SimpleSlug
}
type LinkData = { type LinkData = {
source: NodeData source: SimpleSlug
target: NodeData target: SimpleSlug
} & SimulationLinkDatum<NodeData>
type LinkRenderData = GraphicsInfo & {
simulationData: LinkData
}
type NodeRenderData = GraphicsInfo & {
simulationData: NodeData
label: Text
} }
const localStorageKey = "graph-visited" const localStorageKey = "graph-visited"
@ -62,11 +25,6 @@ function addToVisited(slug: SimpleSlug) {
localStorage.setItem(localStorageKey, JSON.stringify([...visited])) localStorage.setItem(localStorageKey, JSON.stringify([...visited]))
} }
type TweenNode = {
update: (time: number) => void
stop: () => void
}
async function renderGraph(container: string, fullSlug: FullSlug) { async function renderGraph(container: string, fullSlug: FullSlug) {
const slug = simplifySlug(fullSlug) const slug = simplifySlug(fullSlug)
const visited = getVisited() const visited = getVisited()
@ -87,7 +45,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
removeTags, removeTags,
showTags, showTags,
focusOnHover, focusOnHover,
} = JSON.parse(graph.dataset["cfg"]!) as D3Config } = JSON.parse(graph.dataset["cfg"]!)
const data: Map<SimpleSlug, ContentDetails> = new Map( const data: Map<SimpleSlug, ContentDetails> = new Map(
Object.entries<ContentDetails>(await fetchData).map(([k, v]) => [ Object.entries<ContentDetails>(await fetchData).map(([k, v]) => [
@ -95,11 +53,10 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
v, v,
]), ]),
) )
const links: SimpleLinkData[] = [] const links: LinkData[] = []
const tags: SimpleSlug[] = [] const tags: SimpleSlug[] = []
const validLinks = new Set(data.keys())
const tweens = new Map<string, TweenNode>() const validLinks = new Set(data.keys())
for (const [source, details] of data.entries()) { for (const [source, details] of data.entries()) {
const outgoing = details.links ?? [] const outgoing = details.links ?? []
@ -143,406 +100,263 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
if (showTags) tags.forEach((tag) => neighbourhood.add(tag)) if (showTags) tags.forEach((tag) => neighbourhood.add(tag))
} }
const nodes = [...neighbourhood].map((url) => {
const text = url.startsWith("tags/") ? "#" + url.substring(5) : (data.get(url)?.title ?? url)
return {
id: url,
text,
tags: data.get(url)?.tags ?? [],
}
})
const graphData: { nodes: NodeData[]; links: LinkData[] } = { const graphData: { nodes: NodeData[]; links: LinkData[] } = {
nodes, nodes: [...neighbourhood].map((url) => {
links: links const text = url.startsWith("tags/") ? "#" + url.substring(5) : (data.get(url)?.title ?? url)
.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)) return {
.map((l) => ({ id: url,
source: nodes.find((n) => n.id === l.source)!, text: text,
target: nodes.find((n) => n.id === l.target)!, tags: data.get(url)?.tags ?? [],
})), }
}),
links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)),
} }
// we virtualize the simulation and use pixi to actually render it const simulation: d3.Simulation<NodeData, LinkData> = d3
const simulation: Simulation<NodeData, LinkData> = forceSimulation<NodeData>(graphData.nodes) .forceSimulation(graphData.nodes)
.force("charge", forceManyBody().strength(-100 * repelForce)) .force("charge", d3.forceManyBody().strength(-100 * repelForce))
.force("center", forceCenter().strength(centerForce)) .force(
.force("link", forceLink(graphData.links).distance(linkDistance)) "link",
.force("collide", forceCollide<NodeData>((n) => nodeRadius(n)).iterations(3)) d3
.forceLink(graphData.links)
.id((d: any) => d.id)
.distance(linkDistance),
)
.force("center", d3.forceCenter().strength(centerForce))
const width = graph.offsetWidth
const height = Math.max(graph.offsetHeight, 250) const height = Math.max(graph.offsetHeight, 250)
const width = graph.offsetWidth
// precompute style prop strings as pixi doesn't support css variables const svg = d3
const cssVars = [ .select<HTMLElement, NodeData>("#" + container)
"--secondary", .append("svg")
"--tertiary", .attr("width", width)
"--gray", .attr("height", height)
"--light", .attr("viewBox", [-width / 2 / scale, -height / 2 / scale, width / scale, height / scale])
"--lightgray",
"--dark", // draw links between nodes
"--darkgray", const link = svg
"--bodyFont", .append("g")
] as const .selectAll("line")
const computedStyleMap = cssVars.reduce( .data(graphData.links)
(acc, key) => { .join("line")
acc[key] = getComputedStyle(document.documentElement).getPropertyValue(key) .attr("class", "link")
return acc .attr("stroke", "var(--lightgray)")
}, .attr("stroke-width", 1)
{} as Record<(typeof cssVars)[number], string>,
) // svg groups
const graphNode = svg.append("g").selectAll("g").data(graphData.nodes).enter().append("g")
// calculate color // calculate color
const color = (d: NodeData) => { const color = (d: NodeData) => {
const isCurrent = d.id === slug const isCurrent = d.id === slug
if (isCurrent) { if (isCurrent) {
return computedStyleMap["--secondary"] return "var(--secondary)"
} else if (visited.has(d.id) || d.id.startsWith("tags/")) { } else if (visited.has(d.id) || d.id.startsWith("tags/")) {
return computedStyleMap["--tertiary"] return "var(--tertiary)"
} else { } else {
return computedStyleMap["--gray"] return "var(--gray)"
} }
} }
const drag = (simulation: d3.Simulation<NodeData, LinkData>) => {
function dragstarted(event: any, d: NodeData) {
if (!event.active) simulation.alphaTarget(1).restart()
d.fx = d.x
d.fy = d.y
}
function dragged(event: any, d: NodeData) {
d.fx = event.x
d.fy = event.y
}
function dragended(event: any, d: NodeData) {
if (!event.active) simulation.alphaTarget(0)
d.fx = null
d.fy = null
}
const noop = () => {}
return d3
.drag<Element, NodeData>()
.on("start", enableDrag ? dragstarted : noop)
.on("drag", enableDrag ? dragged : noop)
.on("end", enableDrag ? dragended : noop)
}
function nodeRadius(d: NodeData) { function nodeRadius(d: NodeData) {
const numLinks = graphData.links.filter( const numLinks = links.filter((l: any) => l.source.id === d.id || l.target.id === d.id).length
(l) => l.source.id === d.id || l.target.id === d.id,
).length
return 2 + Math.sqrt(numLinks) return 2 + Math.sqrt(numLinks)
} }
let hoveredNodeId: string | null = null let connectedNodes: SimpleSlug[] = []
let hoveredNeighbours: Set<string> = new Set()
const linkRenderData: LinkRenderData[] = []
const nodeRenderData: NodeRenderData[] = []
function updateHoverInfo(newHoveredId: string | null) {
hoveredNodeId = newHoveredId
if (newHoveredId === null) { // draw individual nodes
hoveredNeighbours = new Set() const node = graphNode
for (const n of nodeRenderData) { .append("circle")
n.active = false .attr("class", "node")
} .attr("id", (d) => d.id)
.attr("r", nodeRadius)
for (const l of linkRenderData) { .attr("fill", color)
l.active = false .style("cursor", "pointer")
} .on("click", (_, d) => {
} else { const targ = resolveRelative(fullSlug, d.id)
hoveredNeighbours = new Set() window.spaNavigate(new URL(targ, window.location.toString()))
for (const l of linkRenderData) {
const linkData = l.simulationData
if (linkData.source.id === newHoveredId || linkData.target.id === newHoveredId) {
hoveredNeighbours.add(linkData.source.id)
hoveredNeighbours.add(linkData.target.id)
}
l.active = linkData.source.id === newHoveredId || linkData.target.id === newHoveredId
}
for (const n of nodeRenderData) {
n.active = hoveredNeighbours.has(n.simulationData.id)
}
}
}
let dragStartTime = 0
let dragging = false
function renderLinks() {
tweens.get("link")?.stop()
const tweenGroup = new TweenGroup()
for (const l of linkRenderData) {
let alpha = 1
// if we are hovering over a node, we want to highlight the immediate neighbours
// with full alpha and the rest with default alpha
if (hoveredNodeId) {
alpha = l.active ? 1 : 0.2
}
l.color = l.active ? computedStyleMap["--gray"] : computedStyleMap["--lightgray"]
tweenGroup.add(new Tweened<LinkRenderData>(l).to({ alpha }, 200))
}
tweenGroup.getAll().forEach((tw) => tw.start())
tweens.set("link", {
update: tweenGroup.update.bind(tweenGroup),
stop() {
tweenGroup.getAll().forEach((tw) => tw.stop())
},
}) })
} .on("mouseover", function (_, d) {
const currentId = d.id
const linkNodes = d3
.selectAll(".link")
.filter((d: any) => d.source.id === currentId || d.target.id === currentId)
function renderLabels() { if (focusOnHover) {
tweens.get("label")?.stop() // fade out non-neighbour nodes
const tweenGroup = new TweenGroup() connectedNodes = linkNodes.data().flatMap((d: any) => [d.source.id, d.target.id])
const defaultScale = 1 / scale d3.selectAll<HTMLElement, NodeData>(".link")
const activeScale = defaultScale * 1.1 .transition()
for (const n of nodeRenderData) { .duration(200)
const nodeId = n.simulationData.id .style("opacity", 0.2)
d3.selectAll<HTMLElement, NodeData>(".node")
.filter((d) => !connectedNodes.includes(d.id))
.transition()
.duration(200)
.style("opacity", 0.2)
if (hoveredNodeId === nodeId) { d3.selectAll<HTMLElement, NodeData>(".node")
tweenGroup.add( .filter((d) => !connectedNodes.includes(d.id))
new Tweened<Text>(n.label).to( .nodes()
{ .map((it) => d3.select(it.parentNode as HTMLElement).select("text"))
alpha: 1, .forEach((it) => {
scale: { x: activeScale, y: activeScale }, let opacity = parseFloat(it.style("opacity"))
}, it.transition()
100, .duration(200)
), .attr("opacityOld", opacity)
) .style("opacity", Math.min(opacity, 0.2))
} else { })
tweenGroup.add(
new Tweened<Text>(n.label).to(
{
alpha: n.label.alpha,
scale: { x: defaultScale, y: defaultScale },
},
100,
),
)
}
}
tweenGroup.getAll().forEach((tw) => tw.start())
tweens.set("label", {
update: tweenGroup.update.bind(tweenGroup),
stop() {
tweenGroup.getAll().forEach((tw) => tw.stop())
},
})
}
function renderNodes() {
tweens.get("hover")?.stop()
const tweenGroup = new TweenGroup()
for (const n of nodeRenderData) {
let alpha = 1
// if we are hovering over a node, we want to highlight the immediate neighbours
if (hoveredNodeId !== null && focusOnHover) {
alpha = n.active ? 1 : 0.2
} }
tweenGroup.add(new Tweened<Graphics>(n.gfx, tweenGroup).to({ alpha }, 200)) // highlight links
} linkNodes.transition().duration(200).attr("stroke", "var(--gray)").attr("stroke-width", 1)
tweenGroup.getAll().forEach((tw) => tw.start()) const bigFont = fontSize * 1.5
tweens.set("hover", {
update: tweenGroup.update.bind(tweenGroup), // show text for self
stop() { const parent = this.parentNode as HTMLElement
tweenGroup.getAll().forEach((tw) => tw.stop()) d3.select<HTMLElement, NodeData>(parent)
}, .raise()
.select("text")
.transition()
.duration(200)
.attr("opacityOld", d3.select(parent).select("text").style("opacity"))
.style("opacity", 1)
.style("font-size", bigFont + "em")
}) })
} .on("mouseleave", function (_, d) {
if (focusOnHover) {
d3.selectAll<HTMLElement, NodeData>(".link").transition().duration(200).style("opacity", 1)
d3.selectAll<HTMLElement, NodeData>(".node").transition().duration(200).style("opacity", 1)
function renderPixiFromD3() { d3.selectAll<HTMLElement, NodeData>(".node")
renderNodes() .filter((d) => !connectedNodes.includes(d.id))
renderLinks() .nodes()
renderLabels() .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")
.filter((d: any) => d.source.id === currentId || d.target.id === currentId)
tweens.forEach((tween) => tween.stop()) linkNodes.transition().duration(200).attr("stroke", "var(--lightgray)")
tweens.clear()
const app = new Application() const parent = this.parentNode as HTMLElement
await app.init({ d3.select<HTMLElement, NodeData>(parent)
width, .select("text")
height, .transition()
antialias: true, .duration(200)
autoStart: false, .style("opacity", d3.select(parent).select("text").attr("opacityOld"))
autoDensity: true, .style("font-size", fontSize + "em")
backgroundAlpha: 0,
preference: "webgpu",
resolution: window.devicePixelRatio,
eventMode: "static",
})
graph.appendChild(app.canvas)
const stage = app.stage
stage.interactive = false
const labelsContainer = new Container<Text>({ zIndex: 3 })
const nodesContainer = new Container<Graphics>({ zIndex: 2 })
const linkContainer = new Container<Graphics>({ zIndex: 1 })
stage.addChild(nodesContainer, labelsContainer, linkContainer)
for (const n of graphData.nodes) {
const nodeId = n.id
const label = new Text({
interactive: false,
eventMode: "none",
text: n.text,
alpha: 0,
anchor: { x: 0.5, y: 1.2 },
style: {
fontSize: fontSize * 15,
fill: computedStyleMap["--dark"],
fontFamily: computedStyleMap["--bodyFont"],
},
resolution: window.devicePixelRatio * 4,
}) })
label.scale.set(1 / scale) // @ts-ignore
.call(drag(simulation))
let oldLabelOpacity = 0 // make tags hollow circles
const isTagNode = nodeId.startsWith("tags/") node
const gfx = new Graphics({ .filter((d) => d.id.startsWith("tags/"))
interactive: true, .attr("stroke", color)
label: nodeId, .attr("stroke-width", 2)
eventMode: "static", .attr("fill", "var(--light)")
hitArea: new Circle(0, 0, nodeRadius(n)),
cursor: "pointer",
})
.circle(0, 0, nodeRadius(n))
.fill({ color: isTagNode ? computedStyleMap["--light"] : color(n) })
.stroke({ width: isTagNode ? 2 : 0, color: color(n) })
.on("pointerover", (e) => {
updateHoverInfo(e.target.label)
oldLabelOpacity = label.alpha
if (!dragging) {
renderPixiFromD3()
}
})
.on("pointerleave", () => {
updateHoverInfo(null)
label.alpha = oldLabelOpacity
if (!dragging) {
renderPixiFromD3()
}
})
nodesContainer.addChild(gfx) // draw labels
labelsContainer.addChild(label) const labels = graphNode
.append("text")
const nodeRenderDatum: NodeRenderData = { .attr("dx", 0)
simulationData: n, .attr("dy", (d) => -nodeRadius(d) + "px")
gfx, .attr("text-anchor", "middle")
label, .text((d) => d.text)
color: color(n), .style("opacity", (opacityScale - 1) / 3.75)
alpha: 1, .style("pointer-events", "none")
active: false, .style("font-size", fontSize + "em")
} .raise()
// @ts-ignore
nodeRenderData.push(nodeRenderDatum) .call(drag(simulation))
}
for (const l of graphData.links) {
const gfx = new Graphics({ interactive: false, eventMode: "none" })
linkContainer.addChild(gfx)
const linkRenderDatum: LinkRenderData = {
simulationData: l,
gfx,
color: computedStyleMap["--lightgray"],
alpha: 1,
active: false,
}
linkRenderData.push(linkRenderDatum)
}
let currentTransform = zoomIdentity
if (enableDrag) {
select<HTMLCanvasElement, NodeData | undefined>(app.canvas).call(
drag<HTMLCanvasElement, NodeData | undefined>()
.container(() => app.canvas)
.subject(() => graphData.nodes.find((n) => n.id === hoveredNodeId))
.on("start", function dragstarted(event) {
if (!event.active) simulation.alphaTarget(1).restart()
event.subject.fx = event.subject.x
event.subject.fy = event.subject.y
event.subject.__initialDragPos = {
x: event.subject.x,
y: event.subject.y,
fx: event.subject.fx,
fy: event.subject.fy,
}
dragStartTime = Date.now()
dragging = true
})
.on("drag", function dragged(event) {
const initPos = event.subject.__initialDragPos
event.subject.fx = initPos.x + (event.x - initPos.x) / currentTransform.k
event.subject.fy = initPos.y + (event.y - initPos.y) / currentTransform.k
})
.on("end", function dragended(event) {
if (!event.active) simulation.alphaTarget(0)
event.subject.fx = null
event.subject.fy = null
dragging = false
// if the time between mousedown and mouseup is short, we consider it a click
if (Date.now() - dragStartTime < 500) {
const node = graphData.nodes.find((n) => n.id === event.subject.id) as NodeData
const targ = resolveRelative(fullSlug, node.id)
window.spaNavigate(new URL(targ, window.location.toString()))
}
}),
)
} else {
for (const node of nodeRenderData) {
node.gfx.on("click", () => {
const targ = resolveRelative(fullSlug, node.simulationData.id)
window.spaNavigate(new URL(targ, window.location.toString()))
})
}
}
// set panning
if (enableZoom) { if (enableZoom) {
select<HTMLCanvasElement, NodeData>(app.canvas).call( svg.call(
zoom<HTMLCanvasElement, NodeData>() d3
.zoom<SVGSVGElement, NodeData>()
.extent([ .extent([
[0, 0], [0, 0],
[width, height], [width, height],
]) ])
.scaleExtent([0.25, 4]) .scaleExtent([0.25, 4])
.on("zoom", ({ transform }) => { .on("zoom", ({ transform }) => {
currentTransform = transform link.attr("transform", transform)
stage.scale.set(transform.k, transform.k) node.attr("transform", transform)
stage.position.set(transform.x, transform.y)
// zoom adjusts opacity of labels too
const scale = transform.k * opacityScale const scale = transform.k * opacityScale
let scaleOpacity = Math.max((scale - 1) / 3.75, 0) const scaledOpacity = Math.max((scale - 1) / 3.75, 0)
const activeNodes = nodeRenderData.filter((n) => n.active).flatMap((n) => n.label) labels.attr("transform", transform).style("opacity", scaledOpacity)
for (const label of labelsContainer.children) {
if (!activeNodes.includes(label)) {
label.alpha = scaleOpacity
}
}
}), }),
) )
} }
function animate(time: number) { // progress the simulation
for (const n of nodeRenderData) { simulation.on("tick", () => {
const { x, y } = n.simulationData link
if (!x || !y) continue .attr("x1", (d: any) => d.source.x)
n.gfx.position.set(x + width / 2, y + height / 2) .attr("y1", (d: any) => d.source.y)
if (n.label) { .attr("x2", (d: any) => d.target.x)
n.label.position.set(x + width / 2, y + height / 2) .attr("y2", (d: any) => d.target.y)
} node.attr("cx", (d: any) => d.x).attr("cy", (d: any) => d.y)
} labels.attr("x", (d: any) => d.x).attr("y", (d: any) => d.y)
})
}
for (const l of linkRenderData) { function renderGlobalGraph() {
const linkData = l.simulationData const slug = getFullSlug(window)
l.gfx.clear() const container = document.getElementById("global-graph-outer")
l.gfx.moveTo(linkData.source.x! + width / 2, linkData.source.y! + height / 2) const sidebar = container?.closest(".sidebar") as HTMLElement
l.gfx container?.classList.add("active")
.lineTo(linkData.target.x! + width / 2, linkData.target.y! + height / 2) if (sidebar) {
.stroke({ alpha: l.alpha, width: 1, color: l.color }) sidebar.style.zIndex = "1"
}
tweens.forEach((t) => t.update(time))
app.renderer.render(stage)
requestAnimationFrame(animate)
} }
const graphAnimationFrameHandle = requestAnimationFrame(animate) renderGraph("global-graph-container", slug)
window.addCleanup(() => cancelAnimationFrame(graphAnimationFrameHandle))
function hideGlobalGraph() {
container?.classList.remove("active")
const graph = document.getElementById("global-graph-container")
if (sidebar) {
sidebar.style.zIndex = "unset"
}
if (!graph) return
removeAllChildren(graph)
}
registerEscapeHandler(container, hideGlobalGraph)
} }
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
@ -550,52 +364,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
addToVisited(simplifySlug(slug)) addToVisited(simplifySlug(slug))
await renderGraph("graph-container", slug) await renderGraph("graph-container", slug)
// Function to re-render the graph when the theme changes
const handleThemeChange = () => {
renderGraph("graph-container", slug)
}
// event listener for theme change
document.addEventListener("themechange", handleThemeChange)
// cleanup for the event listener
window.addCleanup(() => {
document.removeEventListener("themechange", handleThemeChange)
})
const container = document.getElementById("global-graph-outer")
const sidebar = container?.closest(".sidebar") as HTMLElement
function renderGlobalGraph() {
const slug = getFullSlug(window)
container?.classList.add("active")
if (sidebar) {
sidebar.style.zIndex = "1"
}
renderGraph("global-graph-container", slug)
registerEscapeHandler(container, hideGlobalGraph)
}
function hideGlobalGraph() {
container?.classList.remove("active")
if (sidebar) {
sidebar.style.zIndex = "unset"
}
}
async function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
if (e.key === "g" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
e.preventDefault()
const globalGraphOpen = container?.classList.contains("active")
globalGraphOpen ? hideGlobalGraph() : renderGlobalGraph()
}
}
const containerIcon = document.getElementById("global-graph-icon") const containerIcon = document.getElementById("global-graph-icon")
containerIcon?.addEventListener("click", renderGlobalGraph) containerIcon?.addEventListener("click", renderGlobalGraph)
window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph)) window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph))
document.addEventListener("keydown", shortcutHandler)
window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler))
}) })

View file

@ -148,7 +148,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
const data = await fetchData const data = await fetchData
const container = document.getElementById("search-container") const container = document.getElementById("search-container")
const sidebar = container?.closest(".sidebar") as HTMLElement const sidebar = container?.closest(".sidebar") as HTMLElement
const searchButton = document.getElementById("search-button") const searchIcon = document.getElementById("search-icon")
const searchBar = document.getElementById("search-bar") as HTMLInputElement | null const searchBar = document.getElementById("search-bar") as HTMLInputElement | null
const searchLayout = document.getElementById("search-layout") const searchLayout = document.getElementById("search-layout")
const idDataMap = Object.keys(data) as FullSlug[] const idDataMap = Object.keys(data) as FullSlug[]
@ -191,8 +191,6 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
} }
searchType = "basic" // reset search type after closing searchType = "basic" // reset search type after closing
searchButton?.focus()
} }
function showSearch(searchTypeNew: SearchType) { function showSearch(searchTypeNew: SearchType) {
@ -460,8 +458,8 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
document.addEventListener("keydown", shortcutHandler) document.addEventListener("keydown", shortcutHandler)
window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler)) window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler))
searchButton?.addEventListener("click", () => showSearch("basic")) searchIcon?.addEventListener("click", () => showSearch("basic"))
window.addCleanup(() => searchButton?.removeEventListener("click", () => showSearch("basic"))) window.addCleanup(() => searchIcon?.removeEventListener("click", () => showSearch("basic")))
searchBar?.addEventListener("input", onType) searchBar?.addEventListener("input", onType)
window.addCleanup(() => searchBar?.removeEventListener("input", onType)) window.addCleanup(() => searchBar?.removeEventListener("input", onType))

View file

@ -16,13 +16,10 @@ const observer = new IntersectionObserver((entries) => {
function toggleToc(this: HTMLElement) { function toggleToc(this: HTMLElement) {
this.classList.toggle("collapsed") this.classList.toggle("collapsed")
this.setAttribute(
"aria-expanded",
this.getAttribute("aria-expanded") === "true" ? "false" : "true",
)
const content = this.nextElementSibling as HTMLElement | undefined const content = this.nextElementSibling as HTMLElement | undefined
if (!content) return if (!content) return
content.classList.toggle("collapsed") content.classList.toggle("collapsed")
content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
} }
function setupToc() { function setupToc() {
@ -31,6 +28,7 @@ function setupToc() {
const collapsed = toc.classList.contains("collapsed") const collapsed = toc.classList.contains("collapsed")
const content = toc.nextElementSibling as HTMLElement | undefined const content = toc.nextElementSibling as HTMLElement | undefined
if (!content) return if (!content) return
content.style.maxHeight = collapsed ? "0px" : content.scrollHeight + "px"
toc.addEventListener("click", toggleToc) toc.addEventListener("click", toggleToc)
window.addCleanup(() => toc.removeEventListener("click", toggleToc)) window.addCleanup(() => toc.removeEventListener("click", toggleToc))
} }

View file

@ -3,7 +3,6 @@ export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb:
function click(this: HTMLElement, e: HTMLElementEventMap["click"]) { function click(this: HTMLElement, e: HTMLElementEventMap["click"]) {
if (e.target !== this) return if (e.target !== this) return
e.preventDefault() e.preventDefault()
e.stopPropagation()
cb() cb()
} }

View file

@ -1,19 +1,5 @@
@use "../../styles/variables.scss" as *;
.backlinks { .backlinks {
flex-direction: column; position: relative;
/*&:after {
pointer-events: none;
content: "";
width: 100%;
height: 50px;
position: absolute;
left: 0;
bottom: 0;
opacity: 1;
transition: opacity 0.3s ease;
background: linear-gradient(transparent 0px, var(--light));
}*/
& > h3 { & > h3 {
font-size: 1rem; font-size: 1rem;
@ -31,14 +17,4 @@
} }
} }
} }
& > .overflow {
&:after {
display: none;
}
height: auto;
@media all and not ($desktop) {
height: 250px;
}
}
} }

View file

@ -1,15 +1,17 @@
.darkmode { .darkmode {
cursor: pointer;
padding: 0;
position: relative; position: relative;
background: none;
border: none;
width: 20px; width: 20px;
height: 20px; height: 20px;
margin: 0 10px; margin: 0 10px;
text-align: inherit;
& > .toggle {
display: none;
box-sizing: border-box;
}
& svg { & svg {
cursor: pointer;
opacity: 0;
position: absolute; position: absolute;
width: 20px; width: 20px;
height: 20px; height: 20px;
@ -27,20 +29,20 @@
color-scheme: light; color-scheme: light;
} }
:root[saved-theme="dark"] .darkmode { :root[saved-theme="dark"] .toggle ~ label {
& > #dayIcon { & > #dayIcon {
display: none; opacity: 0;
} }
& > #nightIcon { & > #nightIcon {
display: inline; opacity: 1;
} }
} }
:root .darkmode { :root .toggle ~ label {
& > #dayIcon { & > #dayIcon {
display: inline; opacity: 1;
} }
& > #nightIcon { & > #nightIcon {
display: none; opacity: 0;
} }
} }

View file

@ -1,29 +1,7 @@
@use "../../styles/variables.scss" as *; @use "../../styles/variables.scss" as *;
.explorer {
display: flex;
flex-direction: column;
overflow-y: hidden;
&.desktop-only {
@media all and not ($mobile) {
display: flex;
}
}
/*&:after {
pointer-events: none;
content: "";
width: 100%;
height: 50px;
position: absolute;
left: 0;
bottom: 0;
opacity: 1;
transition: opacity 0.3s ease;
background: linear-gradient(transparent 0px, var(--light));
}*/
}
button#explorer { button#explorer {
all: unset;
background-color: transparent; background-color: transparent;
border: none; border: none;
text-align: left; text-align: left;
@ -67,20 +45,12 @@ button#explorer {
#explorer-content { #explorer-content {
list-style: none; list-style: none;
overflow: hidden; overflow: hidden;
overflow-y: auto; max-height: none;
max-height: 100%; transition: max-height 0.35s ease;
transition:
max-height 0.35s ease,
visibility 0s linear 0s;
margin-top: 0.5rem; margin-top: 0.5rem;
visibility: visible;
&.collapsed { &.collapsed > .overflow::after {
max-height: 0; opacity: 0;
transition:
max-height 0.35s ease,
visibility 0s linear 0.35s;
visibility: hidden;
} }
& ul { & ul {
@ -97,9 +67,6 @@ button#explorer {
pointer-events: all; pointer-events: all;
} }
} }
> #explorer-ul {
max-height: none;
}
} }
svg { svg {

View file

@ -16,13 +16,10 @@
overflow: hidden; overflow: hidden;
& > #global-graph-icon { & > #global-graph-icon {
cursor: pointer;
background: none;
border: none;
color: var(--dark); color: var(--dark);
opacity: 0.5; opacity: 0.5;
width: 24px; width: 18px;
height: 24px; height: 18px;
position: absolute; position: absolute;
padding: 0.2rem; padding: 0.2rem;
margin: 0.3rem; margin: 0.3rem;
@ -62,10 +59,10 @@
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
height: 80vh; height: 60vh;
width: 80vw; width: 50vw;
@media all and not ($desktop) { @media all and (max-width: $fullPageWidth) {
width: 90%; width: 90%;
} }
} }

View file

@ -13,7 +13,7 @@ li.section-li {
display: grid; display: grid;
grid-template-columns: fit-content(8em) 3fr 1fr; grid-template-columns: fit-content(8em) 3fr 1fr;
@media all and ($mobile) { @media all and (max-width: $mobileBreakpoint) {
& > .tags { & > .tags {
display: none; display: none;
} }
@ -23,7 +23,7 @@ li.section-li {
background-color: transparent; background-color: transparent;
} }
& .meta { & > .meta {
margin: 0 1em 0 0; margin: 0 1em 0 0;
opacity: 0.6; opacity: 0.6;
} }

View file

@ -70,7 +70,7 @@
opacity 0.3s ease, opacity 0.3s ease,
visibility 0.3s ease; visibility 0.3s ease;
@media all and ($mobile) { @media all and (max-width: $mobileBreakpoint) {
display: none !important; display: none !important;
} }
} }

View file

@ -3,25 +3,20 @@
.search { .search {
min-width: fit-content; min-width: fit-content;
max-width: 14rem; max-width: 14rem;
@media all and ($mobile) { flex-grow: 0.3;
flex-grow: 0.3;
}
& > .search-button { & > #search-icon {
background-color: var(--lightgray); background-color: var(--lightgray);
border: none;
border-radius: 4px; border-radius: 4px;
font-family: inherit;
font-size: inherit;
height: 2rem; height: 2rem;
padding: 0;
display: flex; display: flex;
align-items: center; align-items: center;
text-align: inherit;
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
width: 100%;
justify-content: space-between; & > div {
flex-grow: 1;
}
& > p { & > p {
display: inline; display: inline;
@ -64,7 +59,7 @@
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
@media all and not ($desktop) { @media all and (max-width: $fullPageWidth) {
width: 90%; width: 90%;
} }
@ -106,7 +101,7 @@
flex: 0 0 min(30%, 450px); flex: 0 0 min(30%, 450px);
} }
@media all and not ($tablet) { @media all and (min-width: $tabletBreakpoint) {
&[data-preview] { &[data-preview] {
& .result-card > p.preview { & .result-card > p.preview {
display: none; display: none;
@ -132,7 +127,7 @@
border-radius: 5px; border-radius: 5px;
} }
@media all and ($tablet) { @media all and (max-width: $tabletBreakpoint) {
& > #preview-container { & > #preview-container {
display: none !important; display: none !important;
} }

View file

@ -1,20 +1,3 @@
@use "../../styles/variables.scss" as *;
.toc {
display: flex;
flex-direction: column;
&.desktop-only {
max-height: 40%;
}
}
@media all and not ($mobile) {
.toc {
display: flex;
}
}
button#toc { button#toc {
background-color: transparent; background-color: transparent;
border: none; border: none;
@ -45,21 +28,9 @@ button#toc {
#toc-content { #toc-content {
list-style: none; list-style: none;
overflow: hidden; overflow: hidden;
overflow-y: auto; max-height: none;
max-height: 100%; transition: max-height 0.5s ease;
transition:
max-height 0.35s ease,
visibility 0s linear 0s;
position: relative; position: relative;
visibility: visible;
&.collapsed {
max-height: 0;
transition:
max-height 0.35s ease,
visibility 0s linear 0.35s;
visibility: hidden;
}
&.collapsed > .overflow::after { &.collapsed > .overflow::after {
opacity: 0; opacity: 0;
@ -80,10 +51,6 @@ button#toc {
} }
} }
} }
> ul.overflow {
max-height: none;
width: 100%;
}
@for $i from 0 through 6 { @for $i from 0 through 6 {
& .depth-#{$i} { & .depth-#{$i} {

View file

@ -19,7 +19,6 @@ import pt from "./locales/pt-BR"
import hu from "./locales/hu-HU" import hu from "./locales/hu-HU"
import fa from "./locales/fa-IR" import fa from "./locales/fa-IR"
import pl from "./locales/pl-PL" import pl from "./locales/pl-PL"
import cs from "./locales/cs-CZ"
export const TRANSLATIONS = { export const TRANSLATIONS = {
"en-US": enUs, "en-US": enUs,
@ -63,7 +62,6 @@ export const TRANSLATIONS = {
"hu-HU": hu, "hu-HU": hu,
"fa-IR": fa, "fa-IR": fa,
"pl-PL": pl, "pl-PL": pl,
"cs-CZ": cs,
} as const } as const
export const defaultTranslation = "en-US" export const defaultTranslation = "en-US"

View file

@ -1,84 +0,0 @@
import { Translation } from "./definition"
export default {
propertyDefaults: {
title: "Bez názvu",
description: "Nebyl uveden žádný popis",
},
components: {
callout: {
note: "Poznámka",
abstract: "Abstract",
info: "Info",
todo: "Todo",
tip: "Tip",
success: "Úspěch",
question: "Otázka",
warning: "Upozornění",
failure: "Chyba",
danger: "Nebezpečí",
bug: "Bug",
example: "Příklad",
quote: "Citace",
},
backlinks: {
title: "Příchozí odkazy",
noBacklinksFound: "Nenalezeny žádné příchozí odkazy",
},
themeToggle: {
lightMode: "Světlý režim",
darkMode: "Tmavý režim",
},
explorer: {
title: "Procházet",
},
footer: {
createdWith: "Vytvořeno pomocí",
},
graph: {
title: "Graf",
},
recentNotes: {
title: "Nejnovější poznámky",
seeRemainingMore: ({ remaining }) => `Zobraz ${remaining} dalších →`,
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Zobrazení ${targetSlug}`,
linkToOriginal: "Odkaz na původní dokument",
},
search: {
title: "Hledat",
searchBarPlaceholder: "Hledejte něco",
},
tableOfContents: {
title: "Obsah",
},
contentMeta: {
readingTime: ({ minutes }) => `${minutes} min čtení`,
},
},
pages: {
rss: {
recentNotes: "Nejnovější poznámky",
lastFewNotes: ({ count }) => `Posledních ${count} poznámek`,
},
error: {
title: "Nenalezeno",
notFound: "Tato stránka je buď soukromá, nebo neexistuje.",
home: "Návrat na domovskou stránku",
},
folderContent: {
folder: "Složka",
itemsUnderFolder: ({ count }) =>
count === 1 ? "1 položka v této složce." : `${count} položek v této složce.`,
},
tagContent: {
tag: "Tag",
tagIndex: "Rejstřík tagů",
itemsUnderTag: ({ count }) =>
count === 1 ? "1 položka s tímto tagem." : `${count} položek s tímto tagem.`,
showingFirst: ({ count }) => `Zobrazují se první ${count} tagy.`,
totalTags: ({ count }) => `Nalezeno celkem ${count} tagů.`,
},
},
} as const satisfies Translation

View file

@ -147,20 +147,11 @@ function addGlobalPageResources(ctx: BuildCtx, componentResources: ComponentReso
} else if (cfg.analytics?.provider === "cabin") { } else if (cfg.analytics?.provider === "cabin") {
componentResources.afterDOMLoaded.push(` componentResources.afterDOMLoaded.push(`
const cabinScript = document.createElement("script") const cabinScript = document.createElement("script")
cabinScript.src = "${cfg.analytics.host ?? "https://scripts.withcabin.com"}/hello.js" cabinScript.src = "${cfg.analytics.host ?? "https://scripts.cabin.dev"}/cabin.js"
cabinScript.defer = true cabinScript.defer = true
cabinScript.async = true cabinScript.async = true
document.head.appendChild(cabinScript) document.head.appendChild(cabinScript)
`) `)
} else if (cfg.analytics?.provider === "clarity") {
componentResources.afterDOMLoaded.push(`
const clarityScript = document.createElement("script")
clarityScript.innerHTML= \`(function(c,l,a,r,i,t,y){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "${cfg.analytics.projectId}");\`
document.head.appendChild(clarityScript)
`)
} }
if (cfg.enableSPA) { if (cfg.enableSPA) {

View file

@ -3,8 +3,7 @@ import { QuartzFilterPlugin } from "../types"
export const RemoveDrafts: QuartzFilterPlugin<{}> = () => ({ export const RemoveDrafts: QuartzFilterPlugin<{}> = () => ({
name: "RemoveDrafts", name: "RemoveDrafts",
shouldPublish(_ctx, [_tree, vfile]) { shouldPublish(_ctx, [_tree, vfile]) {
const draftFlag: boolean = const draftFlag: boolean = vfile.data?.frontmatter?.draft || false
vfile.data?.frontmatter?.draft === true || vfile.data?.frontmatter?.draft === "true"
return !draftFlag return !draftFlag
}, },
}) })

View file

@ -3,6 +3,6 @@ import { QuartzFilterPlugin } from "../types"
export const ExplicitPublish: QuartzFilterPlugin = () => ({ export const ExplicitPublish: QuartzFilterPlugin = () => ({
name: "ExplicitPublish", name: "ExplicitPublish",
shouldPublish(_ctx, [_tree, vfile]) { shouldPublish(_ctx, [_tree, vfile]) {
return vfile.data?.frontmatter?.publish === true || vfile.data?.frontmatter?.publish === "true" return vfile.data?.frontmatter?.publish ?? false
}, },
}) })

View file

@ -17,11 +17,11 @@ const defaultOptions: Options = {
csl: "apa", csl: "apa",
} }
export const Citations: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => { export const Citations: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "Citations", name: "Citations",
htmlPlugins(ctx) { htmlPlugins() {
const plugins: PluggableList = [] const plugins: PluggableList = []
// Add rehype-citation to the list of plugins // Add rehype-citation to the list of plugins
@ -31,8 +31,6 @@ export const Citations: QuartzTransformerPlugin<Partial<Options>> = (userOpts) =
bibliography: opts.bibliographyFile, bibliography: opts.bibliographyFile,
suppressBibliography: opts.suppressBibliography, suppressBibliography: opts.suppressBibliography,
linkCitations: opts.linkCitations, linkCitations: opts.linkCitations,
csl: opts.csl,
lang: ctx.cfg.configuration.locale ?? "en-US",
}, },
]) ])
@ -40,7 +38,7 @@ export const Citations: QuartzTransformerPlugin<Partial<Options>> = (userOpts) =
// using https://github.com/syntax-tree/unist-util-visit as they're just anochor links // using https://github.com/syntax-tree/unist-util-visit as they're just anochor links
plugins.push(() => { plugins.push(() => {
return (tree, _file) => { return (tree, _file) => {
visit(tree, "element", (node, _index, _parent) => { visit(tree, "element", (node, index, parent) => {
if (node.tagName === "a" && node.properties?.href?.startsWith("#bib")) { if (node.tagName === "a" && node.properties?.href?.startsWith("#bib")) {
node.properties["data-no-popover"] = true node.properties["data-no-popover"] = true
} }

View file

@ -18,7 +18,7 @@ const urlRegex = new RegExp(
"g", "g",
) )
export const Description: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => { export const Description: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "Description", name: "Description",

View file

@ -40,7 +40,7 @@ function coerceToArray(input: string | string[]): string[] | undefined {
.map((tag: string | number) => tag.toString()) .map((tag: string | number) => tag.toString())
} }
export const FrontMatter: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => { export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "FrontMatter", name: "FrontMatter",
@ -88,8 +88,8 @@ declare module "vfile" {
tags: string[] tags: string[]
aliases: string[] aliases: string[]
description: string description: string
publish: boolean | string publish: boolean
draft: boolean | string draft: boolean
lang: string lang: string
enableToc: string enableToc: string
cssclasses: string[] cssclasses: string[]

View file

@ -14,7 +14,9 @@ const defaultOptions: Options = {
linkHeadings: true, linkHeadings: true,
} }
export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => { export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (
userOpts,
) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "GitHubFlavoredMarkdown", name: "GitHubFlavoredMarkdown",

View file

@ -10,4 +10,3 @@ export { OxHugoFlavouredMarkdown } from "./oxhugofm"
export { SyntaxHighlighting } from "./syntax" export { SyntaxHighlighting } from "./syntax"
export { TableOfContents } from "./toc" export { TableOfContents } from "./toc"
export { HardLineBreaks } from "./linebreaks" export { HardLineBreaks } from "./linebreaks"
export { RoamFlavoredMarkdown } from "./roam"

View file

@ -27,7 +27,9 @@ function coerceDate(fp: string, d: any): Date {
} }
type MaybeDate = undefined | string | number type MaybeDate = undefined | string | number
export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => { export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | undefined> = (
userOpts,
) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "CreatedModifiedDate", name: "CreatedModifiedDate",

View file

@ -5,16 +5,10 @@ import { QuartzTransformerPlugin } from "../types"
interface Options { interface Options {
renderEngine: "katex" | "mathjax" renderEngine: "katex" | "mathjax"
customMacros: MacroType
} }
interface MacroType { export const Latex: QuartzTransformerPlugin<Options> = (opts?: Options) => {
[key: string]: string
}
export const Latex: QuartzTransformerPlugin<Partial<Options>> = (opts) => {
const engine = opts?.renderEngine ?? "katex" const engine = opts?.renderEngine ?? "katex"
const macros = opts?.customMacros ?? {}
return { return {
name: "Latex", name: "Latex",
markdownPlugins() { markdownPlugins() {
@ -22,9 +16,9 @@ export const Latex: QuartzTransformerPlugin<Partial<Options>> = (opts) => {
}, },
htmlPlugins() { htmlPlugins() {
if (engine === "katex") { if (engine === "katex") {
return [[rehypeKatex, { output: "html", macros }]] return [[rehypeKatex, { output: "html" }]]
} else { } else {
return [[rehypeMathjax, { macros }]] return [rehypeMathjax]
} }
}, },
externalResources() { externalResources() {

View file

@ -8,6 +8,7 @@ import {
simplifySlug, simplifySlug,
splitAnchor, splitAnchor,
transformLink, transformLink,
joinSegments,
} from "../../util/path" } from "../../util/path"
import path from "path" import path from "path"
import { visit } from "unist-util-visit" import { visit } from "unist-util-visit"
@ -32,7 +33,7 @@ const defaultOptions: Options = {
externalLinkIcon: true, externalLinkIcon: true,
} }
export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => { export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "LinkProcessing", name: "LinkProcessing",
@ -65,9 +66,7 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
type: "element", type: "element",
tagName: "svg", tagName: "svg",
properties: { properties: {
"aria-hidden": "true",
class: "external-icon", class: "external-icon",
style: "max-width:0.8em;max-height:0.8em",
viewBox: "0 0 512 512", viewBox: "0 0 512 512",
}, },
children: [ children: [

View file

@ -119,7 +119,7 @@ export const tableWikilinkRegex = new RegExp(/(!?\[\[[^\]]*?\]\])/g)
const highlightRegex = new RegExp(/==([^=]+)==/g) const highlightRegex = new RegExp(/==([^=]+)==/g)
const commentRegex = new RegExp(/%%[\s\S]*?%%/g) const commentRegex = new RegExp(/%%[\s\S]*?%%/g)
// from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts // from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts
const calloutRegex = new RegExp(/^\[\!([\w-]+)\|?(.+?)?\]([+-]?)/) const calloutRegex = new RegExp(/^\[\!(\w+)\|?(.+?)?\]([+-]?)/)
const calloutLineRegex = new RegExp(/^> *\[\!\w+\|?.*?\][+-]?.*$/gm) const calloutLineRegex = new RegExp(/^> *\[\!\w+\|?.*?\][+-]?.*$/gm)
// (?:^| ) -> non-capturing group, tag should start be separated by a space or be the start of the line // (?:^| ) -> 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 # // #(...) -> capturing group, tag itself must start with #
@ -136,7 +136,9 @@ const wikilinkImageEmbedRegex = new RegExp(
/^(?<alt>(?!^\d*x?\d*$).*?)?(\|?\s*?(?<width>\d+)(x(?<height>\d+))?)?$/, /^(?<alt>(?!^\d*x?\d*$).*?)?(\|?\s*?(?<width>\d+)(x(?<height>\d+))?)?$/,
) )
export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => { export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (
userOpts,
) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
const mdastToHtml = (ast: PhrasingContent | Paragraph) => { const mdastToHtml = (ast: PhrasingContent | Paragraph) => {
@ -324,8 +326,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
replacements.push([ replacements.push([
tagRegex, tagRegex,
(_value: string, tag: string) => { (_value: string, tag: string) => {
// Check if the tag only includes numbers and slashes // Check if the tag only includes numbers
if (/^[\/\d]+$/.test(tag)) { if (/^\d+$/.test(tag)) {
return false return false
} }
@ -430,9 +432,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
children: [ children: [
{ {
type: "text", type: "text",
value: useDefaultTitle value: useDefaultTitle ? capitalize(typeString) : titleContent + " ",
? capitalize(typeString).replace(/-/g, " ")
: titleContent + " ",
}, },
...restOfTitle, ...restOfTitle,
], ],

View file

@ -47,7 +47,9 @@ const quartzLatexRegex = new RegExp(/\$\$[\s\S]*?\$\$|\$.*?\$/, "g")
* markdown to make it compatible with quartz but the list of changes applied it * markdown to make it compatible with quartz but the list of changes applied it
* is not exhaustive. * is not exhaustive.
* */ * */
export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => { export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (
userOpts,
) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "OxHugoFlavouredMarkdown", name: "OxHugoFlavouredMarkdown",

View file

@ -1,224 +0,0 @@
import { QuartzTransformerPlugin } from "../types"
import { PluggableList } from "unified"
import { SKIP, visit } from "unist-util-visit"
import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace"
import { Root, Html, Paragraph, Text, Link, Parent } from "mdast"
import { Node } from "unist"
import { VFile } from "vfile"
import { BuildVisitor } from "unist-util-visit"
export interface Options {
orComponent: boolean
TODOComponent: boolean
DONEComponent: boolean
videoComponent: boolean
audioComponent: boolean
pdfComponent: boolean
blockquoteComponent: boolean
tableComponent: boolean
attributeComponent: boolean
}
const defaultOptions: Options = {
orComponent: true,
TODOComponent: true,
DONEComponent: true,
videoComponent: true,
audioComponent: true,
pdfComponent: true,
blockquoteComponent: true,
tableComponent: true,
attributeComponent: true,
}
const orRegex = new RegExp(/{{or:(.*?)}}/, "g")
const TODORegex = new RegExp(/{{.*?\bTODO\b.*?}}/, "g")
const DONERegex = new RegExp(/{{.*?\bDONE\b.*?}}/, "g")
const videoRegex = new RegExp(/{{.*?\[\[video\]\].*?\:(.*?)}}/, "g")
const youtubeRegex = new RegExp(
/{{.*?\[\[video\]\].*?(https?:\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-\_]*)(&(amp;)?[\w\?=]*)?)}}/,
"g",
)
// const multimediaRegex = new RegExp(/{{.*?\b(video|audio)\b.*?\:(.*?)}}/, "g")
const audioRegex = new RegExp(/{{.*?\[\[audio\]\].*?\:(.*?)}}/, "g")
const pdfRegex = new RegExp(/{{.*?\[\[pdf\]\].*?\:(.*?)}}/, "g")
const blockquoteRegex = new RegExp(/(\[\[>\]\])\s*(.*)/, "g")
const roamHighlightRegex = new RegExp(/\^\^(.+)\^\^/, "g")
const roamItalicRegex = new RegExp(/__(.+)__/, "g")
const tableRegex = new RegExp(/- {{.*?\btable\b.*?}}/, "g") /* TODO */
const attributeRegex = new RegExp(/\b\w+(?:\s+\w+)*::/, "g") /* TODO */
function isSpecialEmbed(node: Paragraph): boolean {
if (node.children.length !== 2) return false
const [textNode, linkNode] = node.children
return (
textNode.type === "text" &&
textNode.value.startsWith("{{[[") &&
linkNode.type === "link" &&
linkNode.children[0].type === "text" &&
linkNode.children[0].value.endsWith("}}")
)
}
function transformSpecialEmbed(node: Paragraph, opts: Options): Html | null {
const [textNode, linkNode] = node.children as [Text, Link]
const embedType = textNode.value.match(/\{\{\[\[(.*?)\]\]:/)?.[1]?.toLowerCase()
const url = linkNode.url.slice(0, -2) // Remove the trailing '}}'
switch (embedType) {
case "audio":
return opts.audioComponent
? {
type: "html",
value: `<audio controls>
<source src="${url}" type="audio/mpeg">
<source src="${url}" type="audio/ogg">
Your browser does not support the audio tag.
</audio>`,
}
: null
case "video":
if (!opts.videoComponent) return null
// Check if it's a YouTube video
const youtubeMatch = url.match(
/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=)?(.+)/,
)
if (youtubeMatch) {
const videoId = youtubeMatch[1].split("&")[0] // Remove additional parameters
const playlistMatch = url.match(/[?&]list=([^#\&\?]*)/)
const playlistId = playlistMatch ? playlistMatch[1] : null
return {
type: "html",
value: `<iframe
class="external-embed youtube"
width="600px"
height="350px"
src="https://www.youtube.com/embed/${videoId}${playlistId ? `?list=${playlistId}` : ""}"
frameborder="0"
allow="fullscreen"
></iframe>`,
}
} else {
return {
type: "html",
value: `<video controls>
<source src="${url}" type="video/mp4">
<source src="${url}" type="video/webm">
Your browser does not support the video tag.
</video>`,
}
}
case "pdf":
return opts.pdfComponent
? {
type: "html",
value: `<embed src="${url}" type="application/pdf" width="100%" height="600px" />`,
}
: null
default:
return null
}
}
export const RoamFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (
userOpts,
) => {
const opts = { ...defaultOptions, ...userOpts }
return {
name: "RoamFlavoredMarkdown",
markdownPlugins() {
const plugins: PluggableList = []
plugins.push(() => {
return (tree: Root, file: VFile) => {
const replacements: [RegExp, ReplaceFunction][] = []
// Handle special embeds (audio, video, PDF)
if (opts.audioComponent || opts.videoComponent || opts.pdfComponent) {
visit(tree, "paragraph", ((node: Paragraph, index: number, parent: Parent | null) => {
if (isSpecialEmbed(node)) {
const transformedNode = transformSpecialEmbed(node, opts)
if (transformedNode && parent) {
parent.children[index] = transformedNode
}
}
}) as BuildVisitor<Root, "paragraph">)
}
// Roam italic syntax
replacements.push([
roamItalicRegex,
(_value: string, match: string) => ({
type: "emphasis",
children: [{ type: "text", value: match }],
}),
])
// Roam highlight syntax
replacements.push([
roamHighlightRegex,
(_value: string, inner: string) => ({
type: "html",
value: `<span class="text-highlight">${inner}</span>`,
}),
])
if (opts.orComponent) {
replacements.push([
orRegex,
(match: string) => {
const matchResult = match.match(/{{or:(.*?)}}/)
if (matchResult === null) {
return { type: "html", value: "" }
}
const optionsString: string = matchResult[1]
const options: string[] = optionsString.split("|")
const selectHtml: string = `<select>${options.map((option: string) => `<option value="${option}">${option}</option>`).join("")}</select>`
return { type: "html", value: selectHtml }
},
])
}
if (opts.TODOComponent) {
replacements.push([
TODORegex,
() => ({
type: "html",
value: `<input type="checkbox" disabled>`,
}),
])
}
if (opts.DONEComponent) {
replacements.push([
DONERegex,
() => ({
type: "html",
value: `<input type="checkbox" checked disabled>`,
}),
])
}
if (opts.blockquoteComponent) {
replacements.push([
blockquoteRegex,
(_match: string, _marker: string, content: string) => ({
type: "html",
value: `<blockquote>${content.trim()}</blockquote>`,
}),
])
}
mdastFindReplace(tree, replacements)
}
})
return plugins
},
}
}

View file

@ -19,8 +19,10 @@ const defaultOptions: Options = {
keepBackground: false, keepBackground: false,
} }
export const SyntaxHighlighting: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => { export const SyntaxHighlighting: QuartzTransformerPlugin<Options> = (
const opts: CodeOptions = { ...defaultOptions, ...userOpts } userOpts?: Partial<Options>,
) => {
const opts: Partial<CodeOptions> = { ...defaultOptions, ...userOpts }
return { return {
name: "SyntaxHighlighting", name: "SyntaxHighlighting",

View file

@ -25,7 +25,9 @@ interface TocEntry {
} }
const slugAnchor = new Slugger() const slugAnchor = new Slugger()
export const TableOfContents: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => { export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefined> = (
userOpts,
) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "TableOfContents", name: "TableOfContents",

View file

@ -143,7 +143,7 @@ export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise<Pro
const childPromises: WorkerPromise<ProcessedContent[]>[] = [] const childPromises: WorkerPromise<ProcessedContent[]>[] = []
for (const chunk of chunks(fps, CHUNK_SIZE)) { for (const chunk of chunks(fps, CHUNK_SIZE)) {
childPromises.push(pool.exec("parseFiles", [ctx.buildId, argv, chunk, ctx.allSlugs])) childPromises.push(pool.exec("parseFiles", [argv, chunk, ctx.allSlugs]))
} }
const results: ProcessedContent[][] = await WorkerPromise.all(childPromises).catch((err) => { const results: ProcessedContent[][] = await WorkerPromise.all(childPromises).catch((err) => {

View file

@ -1,99 +0,0 @@
/*! MIT License
* Copyright (c) 2018 GitHub Inc.
* https://github.com/primer/primitives/blob/main/LICENSE
*/
main {
--color-prettylights-syntax-comment: #8b949e;
--color-prettylights-syntax-constant: #79c0ff;
--color-prettylights-syntax-entity: #d2a8ff;
--color-prettylights-syntax-storage-modifier-import: #c9d1d9;
--color-prettylights-syntax-entity-tag: #7ee787;
--color-prettylights-syntax-keyword: #ff7b72;
--color-prettylights-syntax-string: #a5d6ff;
--color-prettylights-syntax-variable: #ffa657;
--color-prettylights-syntax-brackethighlighter-unmatched: #f85149;
--color-prettylights-syntax-invalid-illegal-text: #f0f6fc;
--color-prettylights-syntax-invalid-illegal-bg: #8e1519;
--color-prettylights-syntax-carriage-return-text: #f0f6fc;
--color-prettylights-syntax-carriage-return-bg: #b62324;
--color-prettylights-syntax-string-regexp: #7ee787;
--color-prettylights-syntax-markup-list: #f2cc60;
--color-prettylights-syntax-markup-heading: #1f6feb;
--color-prettylights-syntax-markup-italic: #c9d1d9;
--color-prettylights-syntax-markup-bold: #c9d1d9;
--color-prettylights-syntax-markup-deleted-text: #ffdcd7;
--color-prettylights-syntax-markup-deleted-bg: #67060c;
--color-prettylights-syntax-markup-inserted-text: #aff5b4;
--color-prettylights-syntax-markup-inserted-bg: #033a16;
--color-prettylights-syntax-markup-changed-text: #ffdfb6;
--color-prettylights-syntax-markup-changed-bg: #5a1e02;
--color-prettylights-syntax-markup-ignored-text: #c9d1d9;
--color-prettylights-syntax-markup-ignored-bg: #1158c7;
--color-prettylights-syntax-meta-diff-range: #d2a8ff;
--color-prettylights-syntax-brackethighlighter-angle: #8b949e;
--color-prettylights-syntax-sublimelinter-gutter-mark: #484f58;
--color-prettylights-syntax-constant-other-reference-link: #a5d6ff;
--color-btn-text: #d4d4d4; /* --darkgray */
--color-btn-bg: #161618; /* --light */
--color-btn-border: rgb(240, 246, 252 / 10%); /* --dark */
--color-btn-shadow: 0 0 transparent;
--color-btn-inset-shadow: 0 0 transparent;
--color-btn-hover-bg: #30363d;
--color-btn-hover-border: #8b949e;
--color-btn-active-bg: hsl(212deg 12% 18% / 100%);
--color-btn-active-border: #6e7681;
--color-btn-selected-bg: #161b22;
--color-btn-primary-text: #fff;
--color-btn-primary-bg: #84a59d; /* --tertiary */
--color-btn-primary-border: rgb(240, 246, 252 / 10%); /* --dark */
--color-btn-primary-shadow: 0 0 transparent;
--color-btn-primary-inset-shadow: 0 0 transparent;
--color-btn-primary-hover-bg: #7b97aa; /* --secondary */
--color-btn-primary-hover-border: rgb(240, 246, 252 / 10%); /* --dark */
--color-btn-primary-selected-bg: #7b97aa; /* --secondary */
--color-btn-primary-selected-shadow: 0 0 transparent;
--color-btn-primary-disabled-text: rgba(33, 32, 32, 0.5);
--color-btn-primary-disabled-bg: rgb(35 134 54 / 60%);
--color-btn-primary-disabled-border: rgb(240 246 252 / 10%);
--color-action-list-item-default-hover-bg: rgb(177 186 196 / 12%);
--color-segmented-control-bg: rgb(110 118 129 / 10%);
--color-segmented-control-button-bg: #0d1117;
--color-segmented-control-button-selected-border: #6e7681;
--color-fg-default: #ebebec; /* --dark */
--color-fg-muted: #d4d4d4; /* --darkgray */
--color-fg-subtle: #d4d4d4; /* --darkgray */
--color-canvas-default: #0d1117;
--color-canvas-overlay: #161b22;
--color-canvas-inset: #010409;
--color-canvas-subtle: #161b22;
--color-border-default: #30363d;
--color-border-muted: #21262d;
--color-neutral-muted: rgb(110 118 129 / 40%);
--color-accent-fg: #2f81f7;
--color-accent-emphasis: #1f6feb;
--color-accent-muted: rgb(56 139 253 / 40%);
--color-accent-subtle: rgb(56 139 253 / 10%);
--color-success-fg: #3fb950;
--color-attention-fg: #d29922;
--color-attention-muted: rgb(187 128 9 / 40%);
--color-attention-subtle: rgb(187 128 9 / 15%);
--color-danger-fg: #f85149;
--color-danger-muted: rgb(248 81 73 / 40%);
--color-danger-subtle: rgb(248 81 73 / 10%);
--color-primer-shadow-inset: 0 0 transparent;
--color-scale-gray-7: #21262d;
--color-scale-blue-8: #0c2d6b;
/*! Extensions from @primer/css/alerts/flash.scss */
--color-social-reaction-bg-hover: var(--color-scale-gray-7);
--color-social-reaction-bg-reacted-hover: var(--color-scale-blue-8);
}
main .pagination-loader-container {
background-image: url("https://github.com/images/modules/pulls/progressive-disclosure-line-dark.svg");
}
main .gsc-loading-image {
background-image: url("https://github.githubassets.com/images/mona-loading-dark.gif");
}

View file

@ -1,99 +0,0 @@
/*! MIT License
* Copyright (c) 2018 GitHub Inc.
* https://github.com/primer/primitives/blob/main/LICENSE
*/
main {
--color-prettylights-syntax-comment: #6e7781;
--color-prettylights-syntax-constant: #0550ae;
--color-prettylights-syntax-entity: #8250df;
--color-prettylights-syntax-storage-modifier-import: #24292f;
--color-prettylights-syntax-entity-tag: #116329;
--color-prettylights-syntax-keyword: #cf222e;
--color-prettylights-syntax-string: #0a3069;
--color-prettylights-syntax-variable: #953800;
--color-prettylights-syntax-brackethighlighter-unmatched: #82071e;
--color-prettylights-syntax-invalid-illegal-text: #f6f8fa;
--color-prettylights-syntax-invalid-illegal-bg: #82071e;
--color-prettylights-syntax-carriage-return-text: #f6f8fa;
--color-prettylights-syntax-carriage-return-bg: #cf222e;
--color-prettylights-syntax-string-regexp: #116329;
--color-prettylights-syntax-markup-list: #3b2300;
--color-prettylights-syntax-markup-heading: #0550ae;
--color-prettylights-syntax-markup-italic: #24292f;
--color-prettylights-syntax-markup-bold: #24292f;
--color-prettylights-syntax-markup-deleted-text: #82071e;
--color-prettylights-syntax-markup-deleted-bg: #ffebe9;
--color-prettylights-syntax-markup-inserted-text: #116329;
--color-prettylights-syntax-markup-inserted-bg: #dafbe1;
--color-prettylights-syntax-markup-changed-text: #953800;
--color-prettylights-syntax-markup-changed-bg: #ffd8b5;
--color-prettylights-syntax-markup-ignored-text: #eaeef2;
--color-prettylights-syntax-markup-ignored-bg: #0550ae;
--color-prettylights-syntax-meta-diff-range: #8250df;
--color-prettylights-syntax-brackethighlighter-angle: #57606a;
--color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f;
--color-prettylights-syntax-constant-other-reference-link: #0a3069;
--color-btn-text: #4e4e4e; /* --darkgray */
--color-btn-bg: #faf8f8; /* --light */
--color-btn-border: rgb(43, 43, 43 / 15%); /* --dark */
--color-btn-shadow: 0 1px 0 rgb(31 35 40 / 4%);
--color-btn-inset-shadow: inset 0 1px 0 rgb(255 255 255 / 25%);
--color-btn-hover-bg: #f3f4f6;
--color-btn-hover-border: rgb(43, 43, 43 / 15%); /* --dark */
--color-btn-active-bg: hsl(220deg 14% 93% / 100%);
--color-btn-active-border: rgb(31 35 40 / 15%);
--color-btn-selected-bg: hsl(220deg 14% 94% / 100%);
--color-btn-primary-text: #fff;
--color-btn-primary-bg: #84a59d; /* --tertiary */
--color-btn-primary-border: rgb(43, 43, 43 / 15%); /* --dark */
--color-btn-primary-shadow: 0 1px 0 rgb(31 35 40 / 10%);
--color-btn-primary-inset-shadow: inset 0 1px 0 rgb(255 255 255 / 3%);
--color-btn-primary-hover-bg: #284b63; /* --secondary */
--color-btn-primary-hover-border: rgb(43, 43, 43 / 15%); /* --dark */
--color-btn-primary-selected-bg: #284b63; /* --secondary */
--color-btn-primary-selected-shadow: inset 0 1px 0 rgb(0 45 17 / 20%);
--color-btn-primary-disabled-text: rgb(255 255 255 / 80%);
--color-btn-primary-disabled-bg: #94d3a2;
--color-btn-primary-disabled-border: rgb(31 35 40 / 15%);
--color-action-list-item-default-hover-bg: rgb(208 215 222 / 32%);
--color-segmented-control-bg: #eaeef2;
--color-segmented-control-button-bg: #fff;
--color-segmented-control-button-selected-border: #8c959f;
--color-fg-default: #2b2b2b; /* --dark */
--color-fg-muted: #4e4e4e; /* --darkgray */
--color-fg-subtle: #4e4e4e; /* --darkgray */
--color-canvas-default: #fff;
--color-canvas-overlay: #fff;
--color-canvas-inset: #f6f8fa;
--color-canvas-subtle: #f6f8fa;
--color-border-default: #d0d7de;
--color-border-muted: hsl(210deg 18% 87% / 100%);
--color-neutral-muted: rgb(175 184 193 / 20%);
--color-accent-fg: #0969da;
--color-accent-emphasis: #0969da;
--color-accent-muted: rgb(84 174 255 / 40%);
--color-accent-subtle: #ddf4ff;
--color-success-fg: #1a7f37;
--color-attention-fg: #9a6700;
--color-attention-muted: rgb(212 167 44 / 40%);
--color-attention-subtle: #fff8c5;
--color-danger-fg: #d1242f;
--color-danger-muted: rgb(255 129 130 / 40%);
--color-danger-subtle: #ffebe9;
--color-primer-shadow-inset: inset 0 1px 0 rgb(208 215 222 / 20%);
--color-scale-gray-1: #eaeef2;
--color-scale-blue-1: #b6e3ff;
/*! Extensions from @primer/css/alerts/flash.scss */
--color-social-reaction-bg-hover: var(--color-scale-gray-1);
--color-social-reaction-bg-reacted-hover: var(--color-scale-blue-1);
}
main .pagination-loader-container {
background-image: url("https://github.com/images/modules/pulls/progressive-disclosure-line.svg");
}
main .gsc-loading-image {
background-image: url("https://github.githubassets.com/images/mona-loading-default.gif");
}

View file

@ -12,6 +12,7 @@ html {
body, body,
section { section {
margin: 0; margin: 0;
max-width: 100%;
box-sizing: border-box; box-sizing: border-box;
background-color: var(--light); background-color: var(--light);
font-family: var(--bodyFont); font-family: var(--bodyFont);
@ -110,23 +111,39 @@ a {
.desktop-only { .desktop-only {
display: initial; display: initial;
<<<<<<< HEAD
@media all and ($mobile) { @media all and ($mobile) {
=======
@media all and (max-width: $fullPageWidth) {
>>>>>>> parent of b41a266 (merge upstream)
display: none; display: none;
} }
} }
.mobile-only { .mobile-only {
display: none; display: none;
<<<<<<< HEAD
@media all and ($mobile) { @media all and ($mobile) {
=======
@media all and (max-width: $fullPageWidth) {
>>>>>>> parent of b41a266 (merge upstream)
display: initial; display: initial;
} }
} }
.page { .page {
<<<<<<< HEAD
max-width: calc(#{map-get($breakpoints, desktop)} + 300px); max-width: calc(#{map-get($breakpoints, desktop)} + 300px);
margin: 0 auto; margin: 0 auto;
=======
@media all and (max-width: $fullPageWidth) {
margin: 0 auto;
padding: 0 1rem;
max-width: $pageWidth;
}
>>>>>>> parent of b41a266 (merge upstream)
& article { & article {
&>h1 { &>h1 {
@ -154,6 +171,7 @@ a {
} }
} }
<<<<<<< HEAD
&>#quartz-body { &>#quartz-body {
display: grid; display: grid;
grid-template-columns: #{map-get($desktopGrid, templateColumns)}; grid-template-columns: #{map-get($desktopGrid, templateColumns)};
@ -184,36 +202,53 @@ a {
@media all and ($mobile) { @media all and ($mobile) {
margin: 0 auto; margin: 0 auto;
=======
& > #quartz-body {
width: 100%;
display: flex;
@media all and (max-width: $fullPageWidth) {
flex-direction: column;
>>>>>>> parent of b41a266 (merge upstream)
} }
& .sidebar { & .sidebar {
flex: 1;
display: flex;
flex-direction: column;
gap: 2rem; gap: 2rem;
top: 0; top: 0;
width: $sidePanelWidth;
margin-top: $topSpacing;
box-sizing: border-box; box-sizing: border-box;
padding: $topSpacing 2rem 2rem 2rem; padding: 0 4rem;
display: flex; position: fixed;
height: 100vh; @media all and (max-width: $fullPageWidth) {
position: sticky; position: initial;
flex-direction: row;
padding: 0;
width: initial;
margin-top: 2rem;
}
} }
& .sidebar.left { & .sidebar.left {
<<<<<<< HEAD
z-index: 1; z-index: 1;
grid-area: grid-sidebar-left; grid-area: grid-sidebar-left;
flex-direction: column; flex-direction: column;
@media all and ($mobile) { @media all and ($mobile) {
=======
left: calc(calc(100vw - $pageWidth) / 2 - $sidePanelWidth);
@media all and (max-width: $fullPageWidth) {
>>>>>>> parent of b41a266 (merge upstream)
gap: 0; gap: 0;
align-items: center; align-items: center;
position: initial;
display: flex;
height: unset;
flex-direction: row;
padding: 0;
padding-top: 2rem;
} }
} }
& .sidebar.right { & .sidebar.right {
<<<<<<< HEAD
grid-area: grid-sidebar-right; grid-area: grid-sidebar-right;
margin-right: 0; margin-right: 0;
flex-direction: column; flex-direction: column;
@ -257,12 +292,37 @@ a {
& .center>article { & .center>article {
grid-area: grid-center; grid-area: grid-center;
=======
right: calc(calc(100vw - $pageWidth) / 2 - $sidePanelWidth);
flex-wrap: wrap;
& > * {
@media all and (max-width: $fullPageWidth) {
flex: 1;
min-width: 140px;
}
}
} }
}
& footer { & .page-header,
grid-area: grid-footer; & .page-footer {
width: $pageWidth;
margin-top: 1rem;
@media all and (max-width: $fullPageWidth) {
width: initial;
>>>>>>> parent of b41a266 (merge upstream)
} }
}
& .page-header {
margin: $topSpacing auto 0 auto;
@media all and (max-width: $fullPageWidth) {
margin-top: 2rem;
}
}
<<<<<<< HEAD
& .center, & .center,
& footer { & footer {
max-width: 100%; max-width: 100%;
@ -281,7 +341,17 @@ a {
} }
& footer { & footer {
=======
& .center,
& footer {
margin-left: auto;
margin-right: auto;
width: $pageWidth;
@media all and (max-width: $fullPageWidth) {
width: initial;
>>>>>>> parent of b41a266 (merge upstream)
margin-left: 0; margin-left: 0;
margin-right: 0;
} }
} }
} }
@ -437,7 +507,7 @@ pre {
counter-increment: line 0; counter-increment: line 0;
display: grid; display: grid;
padding: 0.5rem 0; padding: 0.5rem 0;
overflow-x: auto; overflow-x: scroll;
& [data-highlighted-chars] { & [data-highlighted-chars] {
background-color: var(--highlight); background-color: var(--highlight);
@ -557,14 +627,12 @@ video {
} }
div:has(> .overflow) { div:has(> .overflow) {
display: flex; position: relative;
overflow-y: auto;
max-height: 100%;
} }
ul.overflow, ul.overflow,
ol.overflow { ol.overflow {
max-height: 100%; max-height: 400;
overflow-y: auto; overflow-y: auto;
// clearfix // clearfix
@ -575,7 +643,11 @@ ol.overflow {
margin-bottom: 30px; margin-bottom: 30px;
} }
<<<<<<< HEAD
/*&:after { /*&:after {
=======
&:after {
>>>>>>> parent of b41a266 (merge upstream)
pointer-events: none; pointer-events: none;
content: ""; content: "";
width: 100%; width: 100%;
@ -586,7 +658,7 @@ ol.overflow {
opacity: 1; opacity: 1;
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
background: linear-gradient(transparent 0px, var(--light)); background: linear-gradient(transparent 0px, var(--light));
}*/ }
} }
.transclude { .transclude {

View file

@ -1,25 +1,13 @@
/** $pageWidth: 750px;
* Layout breakpoints $mobileBreakpoint: 600px;
* $mobile: screen width below this value will use mobile styles $tabletBreakpoint: 1000px;
* $desktop: screen width above this value will use desktop styles $sidePanelWidth: 380px;
* Screen width between $mobile and $desktop width will use the tablet layout.
* assuming mobile < desktop
*/
$breakpoints: (
mobile: 800px,
desktop: 1200px,
);
$mobile: "(max-width: #{map-get($breakpoints, mobile)})";
$tablet: "(min-width: #{map-get($breakpoints, mobile)}) and (max-width: #{map-get($breakpoints, desktop)})";
$desktop: "(min-width: #{map-get($breakpoints, desktop)})";
$pageWidth: #{map-get($breakpoints, mobile)};
$sidePanelWidth: 320px; //380px;
$topSpacing: 6rem; $topSpacing: 6rem;
$fullPageWidth: $pageWidth + 2 * $sidePanelWidth;
$boldWeight: 700; $boldWeight: 700;
$semiBoldWeight: 600; $semiBoldWeight: 600;
$normalWeight: 400; $normalWeight: 400;
<<<<<<< HEAD
$mobileGrid: ( $mobileGrid: (
templateRows: "auto auto auto auto auto", templateRows: "auto auto auto auto auto",
@ -50,4 +38,6 @@ $desktopGrid: (
templateAreas: '"grid-sidebar-left grid-header grid-sidebar-right"\ templateAreas: '"grid-sidebar-left grid-header grid-sidebar-right"\
"grid-sidebar-left grid-center grid-sidebar-right"\ "grid-sidebar-left grid-center grid-sidebar-right"\
"grid-sidebar-left grid-footer grid-sidebar-right"', "grid-sidebar-left grid-footer grid-sidebar-right"',
); );
=======
>>>>>>> parent of b41a266 (merge upstream)

View file

@ -14,7 +14,6 @@ export interface Argv {
} }
export interface BuildCtx { export interface BuildCtx {
buildId: string
argv: Argv argv: Argv
cfg: QuartzConfig cfg: QuartzConfig
allSlugs: FullSlug[] allSlugs: FullSlug[]

View file

@ -7,14 +7,8 @@ import { createFileParser, createProcessor } from "./processors/parse"
import { options } from "./util/sourcemap" import { options } from "./util/sourcemap"
// only called from worker thread // only called from worker thread
export async function parseFiles( export async function parseFiles(argv: Argv, fps: FilePath[], allSlugs: FullSlug[]) {
buildId: string,
argv: Argv,
fps: FilePath[],
allSlugs: FullSlug[],
) {
const ctx: BuildCtx = { const ctx: BuildCtx = {
buildId,
cfg, cfg,
argv, argv,
allSlugs, allSlugs,