From 3f501820b1a3ddc55d35dbc1360fa5f5d629b7d8 Mon Sep 17 00:00:00 2001 From: kirbara Date: Tue, 2 Dec 2025 09:52:11 +0700 Subject: [PATCH] Initial commit --- .gitattributes | 1 + .github/dependabot.yml | 20 + .github/workflows/ci.yaml | 72 + .github/workflows/docker-build-push.yaml | 88 + .node-version | 1 + .npmrc | 1 + .prettierignore | 3 + .prettierrc | 7 + Dockerfile | 11 + README.md | 1 + globals.d.ts | 17 + index.d.ts | 12 + package-lock.json | 8118 +++++++++++++++++ package.json | 115 + quartz.config.ts | 95 + quartz.layout.ts | 42 + quartz/bootstrap-cli.mjs | 41 + quartz/bootstrap-worker.mjs | 8 + quartz/build.ts | 422 + quartz/cfg.ts | 97 + quartz/cli/args.js | 108 + quartz/cli/constants.js | 15 + quartz/cli/handlers.js | 576 ++ quartz/cli/helpers.js | 54 + quartz/components/ArticleTitle.tsx | 28 + quartz/components/Backlinks.tsx | 70 + quartz/components/Body.tsx | 13 + quartz/components/Breadcrumbs.tsx | 139 + quartz/components/Comments.tsx | 60 + quartz/components/ContentMeta.tsx | 58 + quartz/components/Darkmode.tsx | 50 + quartz/components/Date.tsx | 31 + quartz/components/DesktopOnly.tsx | 18 + quartz/components/Explorer.tsx | 128 + quartz/components/ExplorerNode.tsx | 242 + quartz/components/Footer.tsx | 33 + quartz/components/Graph.tsx | 106 + quartz/components/GraphMobile.tsx | 106 + quartz/components/Head.tsx | 209 + quartz/components/Header.tsx | 22 + quartz/components/MobileOnly.tsx | 18 + quartz/components/PageList.tsx | 87 + quartz/components/PageTitle.tsx | 23 + quartz/components/RecentNotes.tsx | 93 + quartz/components/Search.tsx | 53 + quartz/components/Separator.tsx | 22 + quartz/components/Spacer.tsx | 8 + quartz/components/TableOfContents.tsx | 95 + quartz/components/TagList.tsx | 57 + quartz/components/index.ts | 51 + quartz/components/pages/404.tsx | 18 + quartz/components/pages/Content.tsx | 11 + quartz/components/pages/FolderContent.tsx | 107 + quartz/components/pages/TagContent.tsx | 127 + quartz/components/renderPage.tsx | 280 + quartz/components/scripts/callout.inline.ts | 44 + quartz/components/scripts/checkbox.inline.ts | 23 + quartz/components/scripts/clipboard.inline.ts | 37 + quartz/components/scripts/comments.inline.ts | 91 + quartz/components/scripts/darkmode.inline.ts | 38 + quartz/components/scripts/explorer.inline.ts | 135 + quartz/components/scripts/graph.inline.ts | 601 ++ quartz/components/scripts/mermaid.inline.ts | 248 + quartz/components/scripts/popover.inline.ts | 109 + quartz/components/scripts/search.inline.ts | 493 + quartz/components/scripts/spa.inline.ts | 203 + quartz/components/scripts/toc.inline.ts | 47 + quartz/components/scripts/util.ts | 45 + quartz/components/styles/backlinks.scss | 44 + quartz/components/styles/breadcrumbs.scss | 22 + quartz/components/styles/clipboard.scss | 36 + quartz/components/styles/contentMeta.scss | 14 + quartz/components/styles/darkmode.scss | 46 + quartz/components/styles/explorer.scss | 181 + quartz/components/styles/footer.scss | 15 + quartz/components/styles/graph.scss | 73 + quartz/components/styles/legacyToc.scss | 27 + quartz/components/styles/listPage.scss | 40 + quartz/components/styles/mermaid.inline.scss | 163 + quartz/components/styles/popover.scss | 83 + quartz/components/styles/recentNotes.scss | 24 + quartz/components/styles/search.scss | 234 + quartz/components/styles/toc.scss | 93 + quartz/components/types.ts | 29 + quartz/depgraph.test.ts | 118 + quartz/depgraph.ts | 228 + quartz/i18n/index.ts | 80 + quartz/i18n/locales/ar-SA.ts | 89 + quartz/i18n/locales/ca-ES.ts | 84 + quartz/i18n/locales/cs-CZ.ts | 84 + quartz/i18n/locales/de-DE.ts | 84 + quartz/i18n/locales/definition.ts | 84 + quartz/i18n/locales/en-GB.ts | 84 + quartz/i18n/locales/en-US.ts | 84 + quartz/i18n/locales/es-ES.ts | 84 + quartz/i18n/locales/fa-IR.ts | 84 + quartz/i18n/locales/fr-FR.ts | 84 + quartz/i18n/locales/hu-HU.ts | 82 + quartz/i18n/locales/it-IT.ts | 84 + quartz/i18n/locales/ja-JP.ts | 82 + quartz/i18n/locales/ko-KR.ts | 82 + quartz/i18n/locales/lt-LT.ts | 104 + quartz/i18n/locales/nl-NL.ts | 86 + quartz/i18n/locales/pl-PL.ts | 84 + quartz/i18n/locales/pt-BR.ts | 84 + quartz/i18n/locales/ro-RO.ts | 85 + quartz/i18n/locales/ru-RU.ts | 96 + quartz/i18n/locales/th-TH.ts | 82 + quartz/i18n/locales/tr-TR.ts | 84 + quartz/i18n/locales/uk-UA.ts | 84 + quartz/i18n/locales/vi-VN.ts | 84 + quartz/i18n/locales/zh-CN.ts | 82 + quartz/i18n/locales/zh-TW.ts | 82 + quartz/plugins/emitters/404.tsx | 68 + quartz/plugins/emitters/aliases.ts | 81 + quartz/plugins/emitters/assets.ts | 58 + quartz/plugins/emitters/cname.ts | 33 + quartz/plugins/emitters/componentResources.ts | 290 + quartz/plugins/emitters/contentIndex.ts | 185 + quartz/plugins/emitters/contentPage.tsx | 142 + quartz/plugins/emitters/folderPage.tsx | 145 + quartz/plugins/emitters/helpers.ts | 19 + quartz/plugins/emitters/index.ts | 10 + quartz/plugins/emitters/static.ts | 35 + quartz/plugins/emitters/tagPage.tsx | 142 + quartz/plugins/filters/draft.ts | 10 + quartz/plugins/filters/explicit.ts | 8 + quartz/plugins/filters/index.ts | 2 + quartz/plugins/index.ts | 52 + quartz/plugins/transformers/citations.ts | 54 + quartz/plugins/transformers/description.ts | 82 + quartz/plugins/transformers/frontmatter.ts | 119 + quartz/plugins/transformers/gfm.ts | 78 + quartz/plugins/transformers/index.ts | 13 + quartz/plugins/transformers/lastmod.ts | 95 + quartz/plugins/transformers/latex.ts | 67 + quartz/plugins/transformers/linebreaks.ts | 11 + quartz/plugins/transformers/links.ts | 172 + quartz/plugins/transformers/ofm.ts | 832 ++ quartz/plugins/transformers/oxhugofm.ts | 106 + quartz/plugins/transformers/roam.ts | 224 + quartz/plugins/transformers/syntax.ts | 31 + quartz/plugins/transformers/toc.ts | 73 + quartz/plugins/types.ts | 47 + quartz/plugins/vfile.ts | 14 + quartz/processors/emit.ts | 33 + quartz/processors/filter.ts | 24 + quartz/processors/parse.ts | 200 + quartz/static/giscus/dark.css | 99 + quartz/static/giscus/light.css | 99 + quartz/static/icon.png | Bin 0 -> 61364 bytes quartz/static/og-image.png | Bin 0 -> 39281 bytes quartz/styles/base.scss | 606 ++ quartz/styles/callouts.scss | 162 + quartz/styles/custom.scss | 10 + quartz/styles/syntax.scss | 17 + quartz/styles/variables.scss | 58 + quartz/util/ctx.ts | 21 + quartz/util/escape.ts | 17 + quartz/util/glob.ts | 22 + quartz/util/jsx.tsx | 27 + quartz/util/lang.ts | 13 + quartz/util/log.ts | 28 + quartz/util/og.tsx | 202 + quartz/util/path.test.ts | 305 + quartz/util/path.ts | 311 + quartz/util/perf.ts | 19 + quartz/util/resources.tsx | 65 + quartz/util/sourcemap.ts | 18 + quartz/util/theme.ts | 72 + quartz/util/trace.ts | 43 + quartz/worker.ts | 48 + tsconfig.json | 20 + 173 files changed, 24001 insertions(+) create mode 100755 .gitattributes create mode 100755 .github/dependabot.yml create mode 100755 .github/workflows/ci.yaml create mode 100755 .github/workflows/docker-build-push.yaml create mode 100755 .node-version create mode 100755 .npmrc create mode 100755 .prettierignore create mode 100755 .prettierrc create mode 100755 Dockerfile create mode 100755 README.md create mode 100755 globals.d.ts create mode 100755 index.d.ts create mode 100755 package-lock.json create mode 100755 package.json create mode 100755 quartz.config.ts create mode 100755 quartz.layout.ts create mode 100755 quartz/bootstrap-cli.mjs create mode 100755 quartz/bootstrap-worker.mjs create mode 100755 quartz/build.ts create mode 100755 quartz/cfg.ts create mode 100755 quartz/cli/args.js create mode 100755 quartz/cli/constants.js create mode 100755 quartz/cli/handlers.js create mode 100755 quartz/cli/helpers.js create mode 100755 quartz/components/ArticleTitle.tsx create mode 100755 quartz/components/Backlinks.tsx create mode 100755 quartz/components/Body.tsx create mode 100755 quartz/components/Breadcrumbs.tsx create mode 100755 quartz/components/Comments.tsx create mode 100755 quartz/components/ContentMeta.tsx create mode 100755 quartz/components/Darkmode.tsx create mode 100755 quartz/components/Date.tsx create mode 100755 quartz/components/DesktopOnly.tsx create mode 100755 quartz/components/Explorer.tsx create mode 100755 quartz/components/ExplorerNode.tsx create mode 100755 quartz/components/Footer.tsx create mode 100755 quartz/components/Graph.tsx create mode 100755 quartz/components/GraphMobile.tsx create mode 100755 quartz/components/Head.tsx create mode 100755 quartz/components/Header.tsx create mode 100755 quartz/components/MobileOnly.tsx create mode 100755 quartz/components/PageList.tsx create mode 100755 quartz/components/PageTitle.tsx create mode 100755 quartz/components/RecentNotes.tsx create mode 100755 quartz/components/Search.tsx create mode 100755 quartz/components/Separator.tsx create mode 100755 quartz/components/Spacer.tsx create mode 100755 quartz/components/TableOfContents.tsx create mode 100755 quartz/components/TagList.tsx create mode 100755 quartz/components/index.ts create mode 100755 quartz/components/pages/404.tsx create mode 100755 quartz/components/pages/Content.tsx create mode 100755 quartz/components/pages/FolderContent.tsx create mode 100755 quartz/components/pages/TagContent.tsx create mode 100755 quartz/components/renderPage.tsx create mode 100755 quartz/components/scripts/callout.inline.ts create mode 100755 quartz/components/scripts/checkbox.inline.ts create mode 100755 quartz/components/scripts/clipboard.inline.ts create mode 100755 quartz/components/scripts/comments.inline.ts create mode 100755 quartz/components/scripts/darkmode.inline.ts create mode 100755 quartz/components/scripts/explorer.inline.ts create mode 100755 quartz/components/scripts/graph.inline.ts create mode 100755 quartz/components/scripts/mermaid.inline.ts create mode 100755 quartz/components/scripts/popover.inline.ts create mode 100755 quartz/components/scripts/search.inline.ts create mode 100755 quartz/components/scripts/spa.inline.ts create mode 100755 quartz/components/scripts/toc.inline.ts create mode 100755 quartz/components/scripts/util.ts create mode 100755 quartz/components/styles/backlinks.scss create mode 100755 quartz/components/styles/breadcrumbs.scss create mode 100755 quartz/components/styles/clipboard.scss create mode 100755 quartz/components/styles/contentMeta.scss create mode 100755 quartz/components/styles/darkmode.scss create mode 100755 quartz/components/styles/explorer.scss create mode 100755 quartz/components/styles/footer.scss create mode 100755 quartz/components/styles/graph.scss create mode 100755 quartz/components/styles/legacyToc.scss create mode 100755 quartz/components/styles/listPage.scss create mode 100755 quartz/components/styles/mermaid.inline.scss create mode 100755 quartz/components/styles/popover.scss create mode 100755 quartz/components/styles/recentNotes.scss create mode 100755 quartz/components/styles/search.scss create mode 100755 quartz/components/styles/toc.scss create mode 100755 quartz/components/types.ts create mode 100755 quartz/depgraph.test.ts create mode 100755 quartz/depgraph.ts create mode 100755 quartz/i18n/index.ts create mode 100755 quartz/i18n/locales/ar-SA.ts create mode 100755 quartz/i18n/locales/ca-ES.ts create mode 100755 quartz/i18n/locales/cs-CZ.ts create mode 100755 quartz/i18n/locales/de-DE.ts create mode 100755 quartz/i18n/locales/definition.ts create mode 100755 quartz/i18n/locales/en-GB.ts create mode 100755 quartz/i18n/locales/en-US.ts create mode 100755 quartz/i18n/locales/es-ES.ts create mode 100755 quartz/i18n/locales/fa-IR.ts create mode 100755 quartz/i18n/locales/fr-FR.ts create mode 100755 quartz/i18n/locales/hu-HU.ts create mode 100755 quartz/i18n/locales/it-IT.ts create mode 100755 quartz/i18n/locales/ja-JP.ts create mode 100755 quartz/i18n/locales/ko-KR.ts create mode 100755 quartz/i18n/locales/lt-LT.ts create mode 100755 quartz/i18n/locales/nl-NL.ts create mode 100755 quartz/i18n/locales/pl-PL.ts create mode 100755 quartz/i18n/locales/pt-BR.ts create mode 100755 quartz/i18n/locales/ro-RO.ts create mode 100755 quartz/i18n/locales/ru-RU.ts create mode 100755 quartz/i18n/locales/th-TH.ts create mode 100755 quartz/i18n/locales/tr-TR.ts create mode 100755 quartz/i18n/locales/uk-UA.ts create mode 100755 quartz/i18n/locales/vi-VN.ts create mode 100755 quartz/i18n/locales/zh-CN.ts create mode 100755 quartz/i18n/locales/zh-TW.ts create mode 100755 quartz/plugins/emitters/404.tsx create mode 100755 quartz/plugins/emitters/aliases.ts create mode 100755 quartz/plugins/emitters/assets.ts create mode 100755 quartz/plugins/emitters/cname.ts create mode 100755 quartz/plugins/emitters/componentResources.ts create mode 100755 quartz/plugins/emitters/contentIndex.ts create mode 100755 quartz/plugins/emitters/contentPage.tsx create mode 100755 quartz/plugins/emitters/folderPage.tsx create mode 100755 quartz/plugins/emitters/helpers.ts create mode 100755 quartz/plugins/emitters/index.ts create mode 100755 quartz/plugins/emitters/static.ts create mode 100755 quartz/plugins/emitters/tagPage.tsx create mode 100755 quartz/plugins/filters/draft.ts create mode 100755 quartz/plugins/filters/explicit.ts create mode 100755 quartz/plugins/filters/index.ts create mode 100755 quartz/plugins/index.ts create mode 100755 quartz/plugins/transformers/citations.ts create mode 100755 quartz/plugins/transformers/description.ts create mode 100755 quartz/plugins/transformers/frontmatter.ts create mode 100755 quartz/plugins/transformers/gfm.ts create mode 100755 quartz/plugins/transformers/index.ts create mode 100755 quartz/plugins/transformers/lastmod.ts create mode 100755 quartz/plugins/transformers/latex.ts create mode 100755 quartz/plugins/transformers/linebreaks.ts create mode 100755 quartz/plugins/transformers/links.ts create mode 100755 quartz/plugins/transformers/ofm.ts create mode 100755 quartz/plugins/transformers/oxhugofm.ts create mode 100755 quartz/plugins/transformers/roam.ts create mode 100755 quartz/plugins/transformers/syntax.ts create mode 100755 quartz/plugins/transformers/toc.ts create mode 100755 quartz/plugins/types.ts create mode 100755 quartz/plugins/vfile.ts create mode 100755 quartz/processors/emit.ts create mode 100755 quartz/processors/filter.ts create mode 100755 quartz/processors/parse.ts create mode 100755 quartz/static/giscus/dark.css create mode 100755 quartz/static/giscus/light.css create mode 100755 quartz/static/icon.png create mode 100755 quartz/static/og-image.png create mode 100755 quartz/styles/base.scss create mode 100755 quartz/styles/callouts.scss create mode 100755 quartz/styles/custom.scss create mode 100755 quartz/styles/syntax.scss create mode 100755 quartz/styles/variables.scss create mode 100755 quartz/util/ctx.ts create mode 100755 quartz/util/escape.ts create mode 100755 quartz/util/glob.ts create mode 100755 quartz/util/jsx.tsx create mode 100755 quartz/util/lang.ts create mode 100755 quartz/util/log.ts create mode 100755 quartz/util/og.tsx create mode 100755 quartz/util/path.test.ts create mode 100755 quartz/util/path.ts create mode 100755 quartz/util/perf.ts create mode 100755 quartz/util/resources.tsx create mode 100755 quartz/util/sourcemap.ts create mode 100755 quartz/util/theme.ts create mode 100755 quartz/util/trace.ts create mode 100755 quartz/worker.ts create mode 100755 tsconfig.json diff --git a/.gitattributes b/.gitattributes new file mode 100755 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100755 index 0000000..f73eb96 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + groups: + production-dependencies: + applies-to: "version-updates" + patterns: + - "*" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + ci-dependencies: + applies-to: "version-updates" + patterns: + - "*" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100755 index 0000000..f0fc1fd --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,72 @@ +name: Build and Test + +on: + pull_request: + branches: + - v4 + push: + branches: + - v4 + workflow_dispatch: + +jobs: + build-and-test: + if: ${{ github.repository == 'jackyzha0/quartz' }} + strategy: + matrix: + os: [windows-latest, macos-latest, ubuntu-latest] + runs-on: ${{ matrix.os }} + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - run: npm ci + + - name: Check types and style + run: npm run check + + - name: Test + run: npm test + + - name: Ensure Quartz builds, check bundle info + run: npx quartz build --bundleInfo + + publish-tag: + if: ${{ github.repository == 'jackyzha0/quartz' && github.ref == 'refs/heads/v4' }} + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Get package version + run: node -p -e '`PACKAGE_VERSION=${require("./package.json").version}`' >> $GITHUB_ENV + - name: Create release tag + uses: pkgdeps/git-tag-action@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + github_repo: ${{ github.repository }} + version: ${{ env.PACKAGE_VERSION }} + git_commit_sha: ${{ github.sha }} + git_tag_prefix: "v" diff --git a/.github/workflows/docker-build-push.yaml b/.github/workflows/docker-build-push.yaml new file mode 100755 index 0000000..308aee3 --- /dev/null +++ b/.github/workflows/docker-build-push.yaml @@ -0,0 +1,88 @@ +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@v5.0.0 + - 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 diff --git a/.node-version b/.node-version new file mode 100755 index 0000000..805b5a4 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +v20.9.0 diff --git a/.npmrc b/.npmrc new file mode 100755 index 0000000..b6f27f1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.prettierignore b/.prettierignore new file mode 100755 index 0000000..3c0687a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +public +node_modules +.quartz-cache diff --git a/.prettierrc b/.prettierrc new file mode 100755 index 0000000..5788b66 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "printWidth": 100, + "quoteProps": "as-needed", + "trailingComma": "all", + "tabWidth": 2, + "semi": false +} diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 0000000..4493853 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM node:20-slim AS builder +WORKDIR /usr/src/app +COPY package.json . +COPY package-lock.json* . +RUN npm ci + +FROM node:20-slim +WORKDIR /usr/src/app +COPY --from=builder /usr/src/app/ /usr/src/app/ +COPY . . +CMD ["npx", "quartz", "build", "--serve"] diff --git a/README.md b/README.md new file mode 100755 index 0000000..82a8edf --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +Minimal Quartz, this my https://note.experimenting.website configuration :D \ No newline at end of file diff --git a/globals.d.ts b/globals.d.ts new file mode 100755 index 0000000..6cf30f8 --- /dev/null +++ b/globals.d.ts @@ -0,0 +1,17 @@ +export declare global { + interface Document { + addEventListener( + type: K, + listener: (this: Document, ev: CustomEventMap[K]) => void, + ): void + removeEventListener( + type: K, + listener: (this: Document, ev: CustomEventMap[K]) => void, + ): void + dispatchEvent(ev: CustomEventMap[K] | UIEvent): void + } + interface Window { + spaNavigate(url: URL, isBack: boolean = false) + addCleanup(fn: (...args: any[]) => void) + } +} diff --git a/index.d.ts b/index.d.ts new file mode 100755 index 0000000..a6c594f --- /dev/null +++ b/index.d.ts @@ -0,0 +1,12 @@ +declare module "*.scss" { + const content: string + export = content +} + +// dom custom event +interface CustomEventMap { + nav: CustomEvent<{ url: FullSlug }> + themechange: CustomEvent<{ theme: "light" | "dark" }> +} + +declare const fetchData: Promise diff --git a/package-lock.json b/package-lock.json new file mode 100755 index 0000000..d908a3c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8118 @@ +{ + "name": "@jackyzha0/quartz", + "version": "4.4.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@jackyzha0/quartz", + "version": "4.4.0", + "license": "MIT", + "dependencies": { + "@clack/prompts": "^0.9.1", + "@floating-ui/dom": "^1.6.13", + "@myriaddreamin/rehype-typst": "^0.5.4", + "@napi-rs/simple-git": "0.1.19", + "@tweenjs/tween.js": "^25.0.0", + "async-mutex": "^0.5.0", + "chalk": "^5.4.1", + "chokidar": "^4.0.3", + "cli-spinner": "^0.2.10", + "d3": "^7.9.0", + "esbuild-sass-plugin": "^3.3.1", + "flexsearch": "0.7.43", + "github-slugger": "^2.0.0", + "globby": "^14.0.2", + "gray-matter": "^4.0.3", + "hast-util-to-html": "^9.0.4", + "hast-util-to-jsx-runtime": "^2.3.2", + "hast-util-to-string": "^3.0.1", + "is-absolute-url": "^4.0.1", + "js-yaml": "^4.1.0", + "lightningcss": "^1.29.1", + "mdast-util-find-and-replace": "^3.0.2", + "mdast-util-to-hast": "^13.2.0", + "mdast-util-to-string": "^4.0.0", + "micromorph": "^0.4.5", + "pixi.js": "^8.6.6", + "preact": "^10.25.4", + "preact-render-to-string": "^6.5.13", + "pretty-bytes": "^6.1.1", + "pretty-time": "^1.1.0", + "reading-time": "^1.5.0", + "rehype-autolink-headings": "^7.1.0", + "rehype-citation": "^2.2.2", + "rehype-katex": "^7.0.1", + "rehype-mathjax": "^6.0.0", + "rehype-pretty-code": "^0.14.0", + "rehype-raw": "^7.0.0", + "rehype-slug": "^6.0.0", + "remark": "^15.0.1", + "remark-breaks": "^4.0.0", + "remark-frontmatter": "^5.0.0", + "remark-gfm": "^4.0.0", + "remark-math": "^6.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.1", + "remark-smartypants": "^3.0.2", + "rfdc": "^1.4.1", + "rimraf": "^6.0.1", + "satori": "^0.12.1", + "serve-handler": "^6.1.6", + "sharp": "^0.33.5", + "shiki": "^1.26.2", + "source-map-support": "^0.5.21", + "to-vfile": "^8.0.0", + "toml": "^3.0.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.3", + "workerpool": "^9.2.0", + "ws": "^8.18.0", + "yargs": "^17.7.2" + }, + "bin": { + "quartz": "quartz/bootstrap-cli.mjs" + }, + "devDependencies": { + "@types/cli-spinner": "^0.2.3", + "@types/d3": "^7.4.3", + "@types/hast": "^3.0.4", + "@types/js-yaml": "^4.0.9", + "@types/node": "^22.10.6", + "@types/pretty-time": "^1.1.5", + "@types/source-map-support": "^0.5.10", + "@types/ws": "^8.5.13", + "@types/yargs": "^17.0.33", + "esbuild": "^0.24.2", + "prettier": "^3.4.2", + "tsx": "^4.19.2", + "typescript": "^5.7.3" + }, + "engines": { + "node": "20 || >=22", + "npm": ">=9.3.1" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-2.0.2.tgz", + "integrity": "sha512-x1KXOatwofR6ZAYzXRBL5wrdV0vwNxlTCK9NCuLqAzQYARqGcvFwiJA6A1ERuh+dgeA4Dxm3JBYictIes+SqUQ==", + "dependencies": { + "bidi-js": "^1.0.3", + "css-tree": "^2.3.1", + "is-potential-custom-element-name": "^1.0.1" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.0.tgz", + "integrity": "sha512-+imAQkHf7U/Rwvu0wk1XWgsP3WnpCWmK7B48f0XqSNzgk64+grljTKC7pnO/xBiEMUziF7vKRfbBnOQhg126qQ==" + }, + "node_modules/@citation-js/core": { + "version": "0.7.14", + "resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.7.14.tgz", + "integrity": "sha512-dgeGqYDSQmn2MtnWZkwPGpJQPh43yr1lAAr9jl1NJ9pIY1RXUQxtlAUZVur0V9PHdbfQC+kkvB1KC3VpgVV3MA==", + "peer": true, + "dependencies": { + "@citation-js/date": "^0.5.0", + "@citation-js/name": "^0.4.2", + "fetch-ponyfill": "^7.1.0", + "sync-fetch": "^0.4.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@citation-js/date": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@citation-js/date/-/date-0.5.1.tgz", + "integrity": "sha512-1iDKAZ4ie48PVhovsOXQ+C6o55dWJloXqtznnnKy6CltJBQLIuLLuUqa8zlIvma0ZigjVjgDUhnVaNU1MErtZw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@citation-js/name": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@citation-js/name/-/name-0.4.2.tgz", + "integrity": "sha512-brSPsjs2fOVzSnARLKu0qncn6suWjHVQtrqSUrnqyaRH95r/Ad4wPF5EsoWr+Dx8HzkCGb/ogmoAzfCsqlTwTQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@citation-js/plugin-bibjson": { + "version": "0.7.14", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibjson/-/plugin-bibjson-0.7.14.tgz", + "integrity": "sha512-Hcmk01KrpHwcl5uVoLE6TRaJRFg7/qUvpJDcKqx3LLLCsNbaBlISfRDeFETrjjipTetkX70RvtS7FfGUN58gCQ==", + "dependencies": { + "@citation-js/date": "^0.5.0", + "@citation-js/name": "^0.4.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@citation-js/core": "^0.7.0" + } + }, + "node_modules/@citation-js/plugin-bibtex": { + "version": "0.7.14", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.7.14.tgz", + "integrity": "sha512-xHOHqhF6dthLRv46N9U+mQgYLiiWQHLvQWK9+mcBKz+/3NWge62Xb1oBouNWwLEPd5FV/8gp9fp7SOp93T0dUg==", + "dependencies": { + "@citation-js/date": "^0.5.0", + "@citation-js/name": "^0.4.2", + "moo": "^0.5.1" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@citation-js/core": "^0.7.0" + } + }, + "node_modules/@citation-js/plugin-csl": { + "version": "0.7.14", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-csl/-/plugin-csl-0.7.14.tgz", + "integrity": "sha512-7AKB8lMz1IqdtoE33NnWIpteLYMuSl3xqT+Cax7sQKwAIJEoq2HBmb43Ja8xQQ36nREAupQJv1V6XksIAmYnCg==", + "dependencies": { + "@citation-js/date": "^0.5.0", + "citeproc": "^2.4.6" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@citation-js/core": "^0.7.0" + } + }, + "node_modules/@clack/core": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.4.1.tgz", + "integrity": "sha512-Pxhij4UXg8KSr7rPek6Zowm+5M22rbd2g1nfojHJkxp5YkFqiZ2+YLEM/XGVIzvGOcM0nqjIFxrpDwWRZYWYjA==", + "dependencies": { + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.9.1.tgz", + "integrity": "sha512-JIpyaboYZeWYlyP0H+OoPPxd6nqueG/CmN6ixBiNFsIDHREevjIf0n0Ohh5gr5C8pEDknzgvz+pIJ8dMhzWIeg==", + "dependencies": { + "@clack/core": "0.4.1", + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", + "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "dependencies": { + "@floating-ui/utils": "^0.2.1" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==" + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@myriaddreamin/rehype-typst": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@myriaddreamin/rehype-typst/-/rehype-typst-0.5.4.tgz", + "integrity": "sha512-6NJ0Ddom+X1jTTO1qlwB7ArLuZBg18m+fTqd3HWpkxAUhHAoemd2oF3ATwBIM0uF9gzG9d523D4o7b+jXCaBUQ==", + "dependencies": { + "@myriaddreamin/typst-ts-node-compiler": "^0.5.4", + "@types/hast": "^3.0.0", + "@types/katex": "^0.16.0", + "hast-util-from-html-isomorphic": "^2.0.0", + "hast-util-to-text": "^4.0.0", + "https-proxy-agent": "^7.0.2", + "unist-util-visit-parents": "^6.0.0", + "vfile": "^6.0.0" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler/-/typst-ts-node-compiler-0.5.4.tgz", + "integrity": "sha512-WAOUjOD+S2S3X/2X33PxDYn0XJ4ydqboxluIdFWU8yOlzn3K8CwoRN/GAbMA13vJTbZQMzjX3VmhMavFWeRtVA==", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@myriaddreamin/typst-ts-node-compiler-android-arm-eabi": "0.5.4", + "@myriaddreamin/typst-ts-node-compiler-android-arm64": "0.5.4", + "@myriaddreamin/typst-ts-node-compiler-darwin-arm64": "0.5.4", + "@myriaddreamin/typst-ts-node-compiler-darwin-x64": "0.5.4", + "@myriaddreamin/typst-ts-node-compiler-linux-arm-gnueabihf": "0.5.4", + "@myriaddreamin/typst-ts-node-compiler-linux-arm64-gnu": "0.5.4", + "@myriaddreamin/typst-ts-node-compiler-linux-arm64-musl": "0.5.4", + "@myriaddreamin/typst-ts-node-compiler-linux-x64-gnu": "0.5.4", + "@myriaddreamin/typst-ts-node-compiler-linux-x64-musl": "0.5.4", + "@myriaddreamin/typst-ts-node-compiler-win32-arm64-msvc": "0.5.4", + "@myriaddreamin/typst-ts-node-compiler-win32-x64-msvc": "0.5.4" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler-android-arm-eabi": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler-android-arm-eabi/-/typst-ts-node-compiler-android-arm-eabi-0.5.4.tgz", + "integrity": "sha512-jptHQK/GN7RCDI4FkGKrec3x3YKFogIw1kpMFYYscoOEntEF4MGJs2FM3vR3bLXGSAR54WlPI6dXPKCYuzVSOg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler-android-arm64": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler-android-arm64/-/typst-ts-node-compiler-android-arm64-0.5.4.tgz", + "integrity": "sha512-xOt+07nYDu3KiOWPnl62es+rThKYRdbOWQPY4hcFqqC5VRTfZZXUBRKdsG+W8qu0gJ513VLmW9HVlkv2PHTW0Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler-darwin-arm64": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler-darwin-arm64/-/typst-ts-node-compiler-darwin-arm64-0.5.4.tgz", + "integrity": "sha512-mtuIjL4KptMhy+rJY0pUv8s8kzFFYKFDyhDQIndsi7P9jYtIUkjJqhg3rXmMUcbVJEEFlaUJ+I+wFQbDuddSlg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler-darwin-x64": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler-darwin-x64/-/typst-ts-node-compiler-darwin-x64-0.5.4.tgz", + "integrity": "sha512-rP8ghx3+vCE0vVat6POYNEkXsjXQn1iyy3pPfLTFtSgQRoJoPJJnDB+tkToCiTZQwvo9aFyrY0LOyH8mpm+BYQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler-linux-arm-gnueabihf": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler-linux-arm-gnueabihf/-/typst-ts-node-compiler-linux-arm-gnueabihf-0.5.4.tgz", + "integrity": "sha512-boM8bVPRL/Ekff51urc3HiY2oKVdL2x36MnHgurAown3iK4OMa0JPDGkxpnuRKbDQEZDXQB1xljVGLaAqqecCg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler-linux-arm64-gnu": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler-linux-arm64-gnu/-/typst-ts-node-compiler-linux-arm64-gnu-0.5.4.tgz", + "integrity": "sha512-DIYH2WXyzeh+0sicGXICm8E/0P5ZAmbCIcGt9sgqXNe2YI/JjXoRDLLm1xN0Y5HD3fiCb/pRTRoeXFpp0u/Fjg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler-linux-arm64-musl": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler-linux-arm64-musl/-/typst-ts-node-compiler-linux-arm64-musl-0.5.4.tgz", + "integrity": "sha512-KNjhfEgPaVaN+0hJ97UKY72jtpMFTA4dnP4iEoB6VX2dunVrbTJbCpjG8Sfml4HJYt0H4gYKsa4LqQzgqFJ6eQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler-linux-x64-gnu": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler-linux-x64-gnu/-/typst-ts-node-compiler-linux-x64-gnu-0.5.4.tgz", + "integrity": "sha512-iqYx3UFrrN0E8bg+NuvTptP2FndJNtt7tlU6Dsh6vjaay5IaBLIAtn9Yf9dPzsqWzHE3nwTq0yjoLfLEtY4a3w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler-linux-x64-musl": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler-linux-x64-musl/-/typst-ts-node-compiler-linux-x64-musl-0.5.4.tgz", + "integrity": "sha512-ROleNG0SD50+FoYJQA/9sai0FzNMh94ZAUVbSJFz474olJHSYQ8xqdIiGlpFA6XXPG6TKBedzbDUVYVXWFI+NQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler-win32-arm64-msvc": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler-win32-arm64-msvc/-/typst-ts-node-compiler-win32-arm64-msvc-0.5.4.tgz", + "integrity": "sha512-Ihh40WW2cB0TUUMfJEOoH5MzQXmPSZc0OcAWMHj8A5Rr4pNNAr1gcJTeB6UHazoRQ8uQG5hg3CqCFydAIbXKfQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@myriaddreamin/typst-ts-node-compiler-win32-x64-msvc": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler-win32-x64-msvc/-/typst-ts-node-compiler-win32-x64-msvc-0.5.4.tgz", + "integrity": "sha512-umEuUW6mn68JTueWr4LHsIUN8Bxs1aGyJdHVMy4br1g7MPqkoR0e8rVreTNulKaDx1+4lFdceWa1Uu7Yu0g9Ag==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git/-/simple-git-0.1.19.tgz", + "integrity": "sha512-jMxvwzkKzd3cXo2EB9GM2ic0eYo2rP/BS6gJt6HnWbsDO1O8GSD4k7o2Cpr2YERtMpGF/MGcDfsfj2EbQPtrXw==", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@napi-rs/simple-git-android-arm-eabi": "0.1.19", + "@napi-rs/simple-git-android-arm64": "0.1.19", + "@napi-rs/simple-git-darwin-arm64": "0.1.19", + "@napi-rs/simple-git-darwin-x64": "0.1.19", + "@napi-rs/simple-git-freebsd-x64": "0.1.19", + "@napi-rs/simple-git-linux-arm-gnueabihf": "0.1.19", + "@napi-rs/simple-git-linux-arm64-gnu": "0.1.19", + "@napi-rs/simple-git-linux-arm64-musl": "0.1.19", + "@napi-rs/simple-git-linux-powerpc64le-gnu": "0.1.19", + "@napi-rs/simple-git-linux-s390x-gnu": "0.1.19", + "@napi-rs/simple-git-linux-x64-gnu": "0.1.19", + "@napi-rs/simple-git-linux-x64-musl": "0.1.19", + "@napi-rs/simple-git-win32-arm64-msvc": "0.1.19", + "@napi-rs/simple-git-win32-x64-msvc": "0.1.19" + } + }, + "node_modules/@napi-rs/simple-git-android-arm-eabi": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm-eabi/-/simple-git-android-arm-eabi-0.1.19.tgz", + "integrity": "sha512-XryEH/hadZ4Duk/HS/HC/cA1j0RHmqUGey3MsCf65ZS0VrWMqChXM/xlTPWuY5jfCc/rPubHaqI7DZlbexnX/g==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-android-arm64": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm64/-/simple-git-android-arm64-0.1.19.tgz", + "integrity": "sha512-ZQ0cPvY6nV9p7zrR9ZPo7hQBkDAcY/CHj3BjYNhykeUCiSNCrhvwX+WEeg5on8M1j4d5jcI/cwVG2FslfiByUg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-darwin-arm64": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-arm64/-/simple-git-darwin-arm64-0.1.19.tgz", + "integrity": "sha512-viZB5TYgjA1vH+QluhxZo0WKro3xBA+1xSzYx8mcxUMO5gnAoUMwXn0ZO/6Zy6pai+aGae+cj6XihGnrBRu3Pg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-darwin-x64": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-x64/-/simple-git-darwin-x64-0.1.19.tgz", + "integrity": "sha512-6dNkzSNUV5X9rsVYQbpZLyJu4Gtkl2vNJ3abBXHX/Etk0ILG5ZasO3ncznIANZQpqcbn/QPHr49J2QYAXGoKJA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-freebsd-x64": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-freebsd-x64/-/simple-git-freebsd-x64-0.1.19.tgz", + "integrity": "sha512-sB9krVIchzd20FjI2ZZ8FDsTSsXLBdnwJ6CpeVyrhXHnoszfcqxt49ocZHujAS9lMpXq7i2Nv1EXJmCy4KdhwA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-arm-gnueabihf": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm-gnueabihf/-/simple-git-linux-arm-gnueabihf-0.1.19.tgz", + "integrity": "sha512-6HPn09lr9N1n5/XKfP8Np53g4fEXVxOFqNkS6rTH3Rm1lZHdazTRH62RggXLTguZwjcE+MvOLvoTIoR5kAS8+g==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-arm64-gnu": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-gnu/-/simple-git-linux-arm64-gnu-0.1.19.tgz", + "integrity": "sha512-G0gISckt4cVDp3oh5Z6PV3GHJrJO6Z8bIS+9xA7vTtKdqB1i5y0n3cSFLlzQciLzhr+CajFD27doW4lEyErQ/Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-arm64-musl": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-musl/-/simple-git-linux-arm64-musl-0.1.19.tgz", + "integrity": "sha512-OwTRF+H4IZYxmDFRi1IrLMfqbdIpvHeYbJl2X94NVsLVOY+3NUHvEzL3fYaVx5urBaMnIK0DD3wZLbcueWvxbA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-powerpc64le-gnu": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-powerpc64le-gnu/-/simple-git-linux-powerpc64le-gnu-0.1.19.tgz", + "integrity": "sha512-p7zuNNVyzpRvkCt2RIGv9FX/WPcPbZ6/FRUgUTZkA2WU33mrbvNqSi4AOqCCl6mBvEd+EOw5NU4lS9ORRJvAEg==", + "cpu": [ + "powerpc64le" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-s390x-gnu": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-s390x-gnu/-/simple-git-linux-s390x-gnu-0.1.19.tgz", + "integrity": "sha512-6N2vwJUPLiak8GLrS0a3is0gSb0UwI2CHOOqtvQxPmv+JVI8kn3vKiUscsktdDb0wGEPeZ8PvZs0y8UWix7K4g==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-x64-gnu": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-gnu/-/simple-git-linux-x64-gnu-0.1.19.tgz", + "integrity": "sha512-61YfeO1J13WK7MalLgP3QlV6of2rWnVw1aqxWkAgy/lGxoOFSJ4Wid6ANVCEZk4tJpPX/XNeneqkUz5xpeb2Cw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-x64-musl": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-musl/-/simple-git-linux-x64-musl-0.1.19.tgz", + "integrity": "sha512-cCTWNpMJnN3PrUBItWcs3dQKCydsIasbrS3laMzq8k7OzF93Zrp2LWDTPlLCO9brbBVpBzy2Qk5Xg9uAfe/Ukw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-win32-arm64-msvc": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-arm64-msvc/-/simple-git-win32-arm64-msvc-0.1.19.tgz", + "integrity": "sha512-sWavb1BjeLKKBA+PbTsRSSzVNfb7V/dOpaJvkgR5d2kWFn/AHmCZHSSj/3nyZdYf0BdDC+DIvqk3daAEZ6QMVw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-win32-x64-msvc": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-x64-msvc/-/simple-git-win32-x64-msvc-0.1.19.tgz", + "integrity": "sha512-FmNuPoK4+qwaSCkp8lm3sJlrxk374enW+zCE5ZksXlZzj/9BDJAULJb5QUJ7o9Y8A/G+d8LkdQLPBE2Jaxe5XA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pixi/colord": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz", + "integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==", + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@shikijs/core": { + "version": "1.26.2", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.26.2.tgz", + "integrity": "sha512-ORyu3MrY7dCC7FDLDsFSkBM9b/AT9/Y8rH+UQ07Rtek48pp0ZhQOMPTKolqszP4bBCas6FqTZQYt18BBamVl/g==", + "dependencies": { + "@shikijs/engine-javascript": "1.26.2", + "@shikijs/engine-oniguruma": "1.26.2", + "@shikijs/types": "1.26.2", + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "1.26.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.26.2.tgz", + "integrity": "sha512-ngkIu9swLVo9Zt5QBtz5Sk08vmPcwuj01r7pPK/Zjmo2U2WyKMK4WMUMmkdQiUacdcLth0zt8u1onp4zhkFXKQ==", + "dependencies": { + "@shikijs/types": "1.26.2", + "@shikijs/vscode-textmate": "^10.0.1", + "oniguruma-to-es": "^1.0.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.26.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.26.2.tgz", + "integrity": "sha512-mlN7Qrs+w60nKrd7at7XkXSwz6728Pe34taDmHrG6LRHjzCqQ+ysg+/AT6/D2LMk0s2lsr71DjpI73430QP4/w==", + "dependencies": { + "@shikijs/types": "1.26.2", + "@shikijs/vscode-textmate": "^10.0.1" + } + }, + "node_modules/@shikijs/langs": { + "version": "1.26.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-1.26.2.tgz", + "integrity": "sha512-o5cdPycB2Kw3IgncHxWopWPiTkjAj7dG01fLkkUyj3glb5ftxL/Opecq9F54opMlrgXy7ZIqDERvFLlUzsCOuA==", + "dependencies": { + "@shikijs/types": "1.26.2" + } + }, + "node_modules/@shikijs/themes": { + "version": "1.26.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-1.26.2.tgz", + "integrity": "sha512-y4Pn6PM5mODz/e3yF6jAUG7WLKJzqL2tJ5qMJCUkMUB1VRgtQVvoa1cHh7NScryGXyrYGJ8nPnRDhdv2rw0xpA==", + "dependencies": { + "@shikijs/types": "1.26.2" + } + }, + "node_modules/@shikijs/types": { + "version": "1.26.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.26.2.tgz", + "integrity": "sha512-PO2jucx2FIdlLBPYbIUlMtWSLs5ulcRcuV93cR3T65lkK5SJP4MGBRt9kmWGXiQc0f7+FHj/0BEawditZcI/fQ==", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.1.tgz", + "integrity": "sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg==" + }, + "node_modules/@shuding/opentype.js": { + "version": "1.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", + "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==", + "license": "MIT", + "dependencies": { + "fflate": "^0.7.3", + "string.prototype.codepointat": "^0.2.1" + }, + "bin": { + "ot": "bin/ot" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@tweenjs/tween.js": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz", + "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==", + "license": "MIT" + }, + "node_modules/@types/cli-spinner": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@types/cli-spinner/-/cli-spinner-0.2.3.tgz", + "integrity": "sha512-TMO6mWltW0lCu1de8DMRq9+59OP/tEjghS+rs8ZEQ2EgYP5yV3bGw0tS14TMyJGqFaoVChNvhkVzv9RC1UgX+w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/css-font-loading-module": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz", + "integrity": "sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA==", + "license": "MIT" + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.5.tgz", + "integrity": "sha512-Qk7fpJ6qFp+26VeQ47WY0mkwXaiq8+76RJcncDEfMc2ocRzXLO67bLFRNI4OX1aGBoPzsM5Y2T+/m1pldOgD+A==", + "dev": true + }, + "node_modules/@types/d3-axis": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.2.tgz", + "integrity": "sha512-uGC7DBh0TZrU/LY43Fd8Qr+2ja1FKmH07q2FoZFHo1eYl8aj87GhfVoY1saJVJiq24rp1+wpI6BvQJMKgQm8oA==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.2.tgz", + "integrity": "sha512-2TEm8KzUG3N7z0TrSKPmbxByBx54M+S9lHoP2J55QuLU0VSQ9mE96EJSAOVNEqd1bbynMjeTS9VHmz8/bSw8rA==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.2.tgz", + "integrity": "sha512-abT/iLHD3sGZwqMTX1TYCMEulr+wBd0SzyOQnjYNLp7sngdOHYtNkMRI5v3w5thoN+BWtlHVDx2Osvq6fxhZWw==", + "dev": true + }, + "node_modules/@types/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==", + "dev": true + }, + "node_modules/@types/d3-contour": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.2.tgz", + "integrity": "sha512-k6/bGDoAGJZnZWaKzeB+9glgXCYGvh6YlluxzBREiVo8f/X2vpTEdgPy9DN7Z2i42PZOZ4JDhVdlTSTSkLDPlQ==", + "dev": true, + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz", + "integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==", + "dev": true + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.2.tgz", + "integrity": "sha512-rxN6sHUXEZYCKV05MEh4z4WpPSqIw+aP7n9ZN6WYAAvZoEAghEK1WeVZMZcHRBwyaKflU43PCUAJNjFxCzPDjg==", + "dev": true + }, + "node_modules/@types/d3-drag": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.2.tgz", + "integrity": "sha512-qmODKEDvyKWVHcWWCOVcuVcOwikLVsyc4q4EBJMREsoQnR2Qoc2cZQUyFUPgO9q4S3qdSqJKBsuefv+h0Qy+tw==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-76pBHCMTvPLt44wFOieouXcGXWOF0AJCceUvaFkxSZEu4VDUdv93JfpMa6VGNFs01FHfuP4a5Ou68eRG1KBfTw==", + "dev": true + }, + "node_modules/@types/d3-ease": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz", + "integrity": "sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==", + "dev": true + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.2.tgz", + "integrity": "sha512-gllwYWozWfbep16N9fByNBDTkJW/SyhH6SGRlXloR7WdtAaBui4plTP+gbUgiEot7vGw/ZZop1yDZlgXXSuzjA==", + "dev": true, + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.4.tgz", + "integrity": "sha512-q7xbVLrWcXvSBBEoadowIUJ7sRpS1yvgMWnzHJggFy5cUZBq2HZL5k/pBSm0GdYWS1vs5/EDwMjSKF55PDY4Aw==", + "dev": true + }, + "node_modules/@types/d3-format": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", + "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==", + "dev": true + }, + "node_modules/@types/d3-geo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.0.3.tgz", + "integrity": "sha512-bK9uZJS3vuDCNeeXQ4z3u0E7OeJZXjUgzFdSOtNtMCJCLvDtWDwfpRVWlyt3y8EvRzI0ccOu9xlMVirawolSCw==", + "dev": true, + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-9hjRTVoZjRFR6xo8igAJyNXQyPX6Aq++Nhb5ebrUF414dv4jr2MitM2fWiOY475wa3Za7TOS2Gh9fmqEhLTt0A==", + "dev": true + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", + "dev": true, + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz", + "integrity": "sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==", + "dev": true + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.0.tgz", + "integrity": "sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw==", + "dev": true + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz", + "integrity": "sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw==", + "dev": true + }, + "node_modules/@types/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ==", + "dev": true + }, + "node_modules/@types/d3-scale": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.3.tgz", + "integrity": "sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ==", + "dev": true, + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==", + "dev": true + }, + "node_modules/@types/d3-selection": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.5.tgz", + "integrity": "sha512-xCB0z3Hi8eFIqyja3vW8iV01+OHGYR2di/+e+AiOcXIOrY82lcvWW8Ke1DYE/EUVMsBl4Db9RppSBS3X1U6J0w==", + "dev": true + }, + "node_modules/@types/d3-shape": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.1.tgz", + "integrity": "sha512-6Uh86YFF7LGg4PQkuO2oG6EMBRLuW9cbavUW46zkIO5kuS2PfTqo2o9SkgtQzguBHbLgNnU90UNsITpsX1My+A==", + "dev": true, + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==", + "dev": true + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.0.tgz", + "integrity": "sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==", + "dev": true + }, + "node_modules/@types/d3-timer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz", + "integrity": "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==", + "dev": true + }, + "node_modules/@types/d3-transition": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.3.tgz", + "integrity": "sha512-/S90Od8Id1wgQNvIA8iFv9jRhCiZcGhPd2qX0bKF/PS+y0W5CrXKgIiELd2CvG1mlQrWK/qlYh3VxicqG1ZvgA==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.3.tgz", + "integrity": "sha512-OWk1yYIIWcZ07+igN6BeoG6rqhnJ/pYe+R1qWFM2DtW49zsoSjgb9G5xB0ZXA8hh2jAzey1XuRmMSoXdKw8MDA==", + "dev": true, + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/earcut": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.4.tgz", + "integrity": "sha512-qp3m9PPz4gULB9MhjGID7wpo3gJ4bTGXm7ltNDsmOvsPduTeHp8wSW9YckBj3mljeOh4F0m2z/0JKAALRKbmLQ==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.3.tgz", + "integrity": "sha512-pvQ+TKeRHeiUGRhvYwRrQ/ISnohKkSJR14fT2yqyZ4e9K5vqc7hrtY2Y1Dw0ZwAzQ6DQsxsaCUuSIIi8v0Cq6w==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.10", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", + "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==", + "dev": true + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true + }, + "node_modules/@types/katex": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz", + "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==" + }, + "node_modules/@types/mathjax": { + "version": "0.0.40", + "resolved": "https://registry.npmjs.org/@types/mathjax/-/mathjax-0.0.40.tgz", + "integrity": "sha512-rHusx08LCg92WJxrsM3SPjvLTSvK5C+gealtSuhKbEOcUZfWlwigaFoPLf6Dfxhg4oryN5qP9Sj7zOQ4HYXINw==" + }, + "node_modules/@types/mdast": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.3.tgz", + "integrity": "sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" + }, + "node_modules/@types/nlcst": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz", + "integrity": "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/node": { + "version": "22.10.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.6.tgz", + "integrity": "sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/pretty-time": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/pretty-time/-/pretty-time-1.1.5.tgz", + "integrity": "sha512-5yl+BYwmnRWZb783W8YYoHXvPY8q/rp7ctHBVaGBB9RxlzGpHNJ72tGQMK7TrUSnxzl1dbDcBDuBCSbtfnSQGg==", + "dev": true + }, + "node_modules/@types/source-map-support": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@types/source-map-support/-/source-map-support-0.5.10.tgz", + "integrity": "sha512-tgVP2H469x9zq34Z0m/fgPewGhg/MLClalNOiPIzQlXrSS2YrKu/xCdSCKnEDwkFha51VKEKB6A9wW26/ZNwzA==", + "dev": true, + "dependencies": { + "source-map": "^0.6.0" + } + }, + "node_modules/@types/unist": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", + "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" + }, + "node_modules/@types/ws": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, + "node_modules/@webgpu/types": { + "version": "0.1.44", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.44.tgz", + "integrity": "sha512-JDpYJN5E/asw84LTYhKyvPpxGnD+bAKPtpW9Ilurf7cZpxaTbxkQcGwOd7jgB9BPBrTYQ+32ufo4HiuomTjHNQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/array-iterate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz", + "integrity": "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-builder": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz", + "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citeproc": { + "version": "2.4.63", + "resolved": "https://registry.npmjs.org/citeproc/-/citeproc-2.4.63.tgz", + "integrity": "sha512-68F95Bp4UbgZU/DBUGQn0qV3HDZLCdI9+Bb2ByrTaNJDL5VEm9LqaiNaxljsvoaExSLEXe1/r6n2Z06SCzW3/Q==" + }, + "node_modules/cli-spinner": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/cli-spinner/-/cli-spinner-0.2.10.tgz", + "integrity": "sha512-U0sSQ+JJvSLi1pAYuJykwiA8Dsr15uHEy85iCJ6A+0DjVxivr3d+N2Wjvodeg89uP5K6TswFkKBfAD7B3YSn/Q==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorjs.io": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", + "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-background-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz", + "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==", + "license": "MIT" + }, + "node_modules/css-box-shadow": { + "version": "1.0.0-3", + "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", + "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==", + "license": "MIT" + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-gradient-parser": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.16.tgz", + "integrity": "sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA==", + "engines": { + "node": ">=16" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssstyle": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz", + "integrity": "sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==", + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" + }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/delaunator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", + "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", + "dependencies": { + "robust-predicates": "^3.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", + "license": "ISC" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "hasInstallScript": true, + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/esbuild-sass-plugin": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-3.3.1.tgz", + "integrity": "sha512-SnO1ls+d52n6j8gRRpjexXI8MsHEaumS0IdDHaYM29Y6gakzZYMls6i9ql9+AWMSQk/eryndmUpXEgT34QrX1A==", + "dependencies": { + "resolve": "^1.22.8", + "safe-identifier": "^0.4.2", + "sass": "^1.71.1" + }, + "peerDependencies": { + "esbuild": ">=0.20.1", + "sass-embedded": "^1.71.1" + } + }, + "node_modules/esbuild/node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/fetch-ponyfill": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/fetch-ponyfill/-/fetch-ponyfill-7.1.0.tgz", + "integrity": "sha512-FhbbL55dj/qdVO3YNK7ZEkshvj3eQ7EuIGV2I6ic/2YiocvyWv+7jg2s4AyS0wdRU75s3tA8ZxI/xPigb0v5Aw==", + "dependencies": { + "node-fetch": "~2.6.1" + } + }, + "node_modules/fetch-ponyfill/node_modules/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/fetch-ponyfill/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/fetch-ponyfill/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/fetch-ponyfill/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/fflate": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flexsearch": { + "version": "0.7.43", + "resolved": "https://registry.npmjs.org/flexsearch/-/flexsearch-0.7.43.tgz", + "integrity": "sha512-c5o/+Um8aqCSOXGcZoqZOm+NqtVwNsvVpWv6lfmSclU954O3wvQKxxK8zj74fPaSJbXpSLTs4PRhh+wnoCXnKg==" + }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-tsconfig": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.5.tgz", + "integrity": "sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==" + }, + "node_modules/glob": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-dom": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.0.tgz", + "integrity": "sha512-d6235voAp/XR3Hh5uy7aGLbM3S4KamdW0WEgOaU1YoewnuYw4HXb5eRtv9g65m/RFGEfUY1Mw4UqCc5Y8L4Stg==", + "dependencies": { + "@types/hast": "^3.0.0", + "hastscript": "^8.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.1.tgz", + "integrity": "sha512-RXQBLMl9kjKVNkJTIO6bZyb2n+cUH8LFaSSzo82jiLT6Tfc+Pt7VQCS+/h3YwG4jaNE2TA2sdJisGWR+aJrp0g==", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html-isomorphic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", + "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-dom": "^5.0.0", + "hast-util-from-html": "^2.0.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/hast-util-from-html/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html/node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz", + "integrity": "sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^8.0.0", + "property-information": "^6.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/hast-util-heading-rank": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz", + "integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.0.1.tgz", + "integrity": "sha512-5m1gmba658Q+lO5uqL5YNGQWeh1MYWZbZmWrM5lncdcuiXuo5E2HT/CIOp0rLF8ksfSwiCVJ3twlgVRyTGThGA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/hast-util-to-html": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.4.tgz", + "integrity": "sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.2.tgz", + "integrity": "sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/hast-util-to-jsx-runtime/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime/node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", + "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.0.tgz", + "integrity": "sha512-EWiE1FSArNBPUo1cKWtzqgnuRQwEeQbQtnFJRYV1hb1BWDgrAlBU0ExptvZMM/KSA82cDpm2sFGf3Dmc5Mza3w==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz", + "integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hex-rgb": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", + "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", + "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", + "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==" + }, + "node_modules/inline-style-parser": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.2.tgz", + "integrity": "sha512-EcKzdTHVe8wFVOGEYXiW9WmJXPjqi1T+234YpJr98RiFYKHV3cdy1+3mkTE+KHTHxFFLH51SfaGOoUdW+v7ViQ==" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-absolute-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz", + "integrity": "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/ismobilejs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz", + "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==", + "license": "MIT" + }, + "node_modules/jackspeak": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", + "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "23.2.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-23.2.0.tgz", + "integrity": "sha512-L88oL7D/8ufIES+Zjz7v0aes+oBMh2Xnh3ygWvL0OaICOomKEPKuPnIfBJekiXr+BHbbMjrWn/xqrDQuxFTeyA==", + "dependencies": { + "@asamuzakjp/dom-selector": "^2.0.1", + "cssstyle": "^4.0.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.3", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.16.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/katex": { + "version": "0.16.21", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", + "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lightningcss": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.1.tgz", + "integrity": "sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q==", + "dependencies": { + "detect-libc": "^1.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.29.1", + "lightningcss-darwin-x64": "1.29.1", + "lightningcss-freebsd-x64": "1.29.1", + "lightningcss-linux-arm-gnueabihf": "1.29.1", + "lightningcss-linux-arm64-gnu": "1.29.1", + "lightningcss-linux-arm64-musl": "1.29.1", + "lightningcss-linux-x64-gnu": "1.29.1", + "lightningcss-linux-x64-musl": "1.29.1", + "lightningcss-win32-arm64-msvc": "1.29.1", + "lightningcss-win32-x64-msvc": "1.29.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.1.tgz", + "integrity": "sha512-HtR5XJ5A0lvCqYAoSv2QdZZyoHNttBpa5EP9aNuzBQeKGfbyH5+UipLWvVzpP4Uml5ej4BYs5I9Lco9u1fECqw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.1.tgz", + "integrity": "sha512-k33G9IzKUpHy/J/3+9MCO4e+PzaFblsgBjSGlpAaFikeBFm8B/CkO3cKU9oI4g+fjS2KlkLM/Bza9K/aw8wsNA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.1.tgz", + "integrity": "sha512-0SUW22fv/8kln2LnIdOCmSuXnxgxVC276W5KLTwoehiO0hxkacBxjHOL5EtHD8BAXg2BvuhsJPmVMasvby3LiQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.1.tgz", + "integrity": "sha512-sD32pFvlR0kDlqsOZmYqH/68SqUMPNj+0pucGxToXZi4XZgZmqeX/NkxNKCPsswAXU3UeYgDSpGhu05eAufjDg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.1.tgz", + "integrity": "sha512-0+vClRIZ6mmJl/dxGuRsE197o1HDEeeRk6nzycSy2GofC2JsY4ifCRnvUWf/CUBQmlrvMzt6SMQNMSEu22csWQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.1.tgz", + "integrity": "sha512-UKMFrG4rL/uHNgelBsDwJcBqVpzNJbzsKkbI3Ja5fg00sgQnHw/VrzUTEc4jhZ+AN2BvQYz/tkHu4vt1kLuJyw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.1.tgz", + "integrity": "sha512-u1S+xdODy/eEtjADqirA774y3jLcm8RPtYztwReEXoZKdzgsHYPl0s5V52Tst+GKzqjebkULT86XMSxejzfISw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.1.tgz", + "integrity": "sha512-L0Tx0DtaNUTzXv0lbGCLB/c/qEADanHbu4QdcNOXLIe1i8i22rZRpbT3gpWYsCh9aSL9zFujY/WmEXIatWvXbw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.1.tgz", + "integrity": "sha512-QoOVnkIEFfbW4xPi+dpdft/zAKmgLgsRHfJalEPYuJDOWf7cLQzYg0DEh8/sn737FaeMJxHZRc1oBreiwZCjog==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.1.tgz", + "integrity": "sha512-NygcbThNBe4JElP+olyTI/doBNGJvLs3bFCRPdvuCcxZCcCZ71B858IHpdm7L1btZex0FvCmM17FK98Y9MRy1Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", + "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/markdown-table": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", + "integrity": "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mathjax-full": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.2.tgz", + "integrity": "sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==", + "dependencies": { + "esm": "^3.2.25", + "mhchemparser": "^4.1.0", + "mj-context-menu": "^0.6.1", + "speech-rule-engine": "^4.0.6" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz", + "integrity": "sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/mdast-util-from-markdown/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", + "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "escape-string-regexp": "^5.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz", + "integrity": "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.0.tgz", + "integrity": "sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz", + "integrity": "sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-math": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz", + "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "longest-streak": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.1.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz", + "integrity": "sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.0.0.tgz", + "integrity": "sha512-XZuPPzQNBPAlaqsTTgRrcJnyFbSOBovSadFgbFu8SnuNgm+6Bdx1K+IWoitsmj6Lq6MNtI+ytOqwN70n//NaBA==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-remove-position": "^5.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/mdast-util-mdx-jsx/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-newline-to-break": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz", + "integrity": "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-find-and-replace": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.0.0.tgz", + "integrity": "sha512-xadSsJayQIucJ9n053dfQwVu1kuXg7jCTdYsMK8rqzKZh52nLfSH/k0sAxE0u+pj/zKZX+o5wB+ML5mRayOxFA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/mhchemparser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.2.1.tgz", + "integrity": "sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==" + }, + "node_modules/micromark": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz", + "integrity": "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.0.tgz", + "integrity": "sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-frontmatter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", + "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", + "dependencies": { + "fault": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.0.0.tgz", + "integrity": "sha512-rTHfnpt/Q7dEAK1Y5ii0W8bhfJlVJFnJMHIPisfPK3gpVNuOP0VnRl96+YJ3RYWV/P4gFeQoGKNlT3RhuvpqAg==", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.0.0.tgz", + "integrity": "sha512-6Rzu0CYRKDv3BfLAUnZsSlzx3ak6HAoI85KTiijuKIz5UxZxbUI+pD6oHgw+6UtQuiRwnGRhzMmPRv4smcz0fg==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-c3BR1ClMp5fxxmwP6AoOY2fXO9U8uFMKs4ADD66ahLTNcwzSCyRVU4k7LPV5Nxo/VJiR4TdzxRQY2v3qIUceCw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.0.0.tgz", + "integrity": "sha512-PoHlhypg1ItIucOaHmKE8fbin3vTLpDOUg8KAr8gRCF1MOZI9Nquq2i/44wFvviM4WuxJzc3demT8Y3dkfvYrw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.0.1.tgz", + "integrity": "sha512-cY5PzGcnULaN5O7T+cOzfMoHjBW7j+T9D2sucA5d/KbsBTPcYdebm9zUd9zzdgJGCwahV+/W78Z3nbulBYVbTw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-math": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.0.0.tgz", + "integrity": "sha512-iJ2Q28vBoEovLN5o3GO12CpqorQRYDPT+p4zW50tGwTfJB+iv/VnB6Ini+gqa24K97DwptMBBIvVX6Bjk49oyQ==", + "dependencies": { + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", + "integrity": "sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz", + "integrity": "sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", + "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz", + "integrity": "sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz", + "integrity": "sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.0.1.tgz", + "integrity": "sha512-3wgnrmEAJ4T+mGXAUfMvMAbxU9RDG43XmGce4j6CwPtVxB3vfwXSZ6KhFwDzZ3mZHhmPimMAXg71veiBGzeAZw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", + "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz", + "integrity": "sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz", + "integrity": "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz", + "integrity": "sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz", + "integrity": "sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", + "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz", + "integrity": "sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz", + "integrity": "sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz", + "integrity": "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", + "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.0.tgz", + "integrity": "sha512-vc93L1t+gpR3p8jxeVdaYlbV2jTYteDje19rNSS/H5dlhxUYll5Fy6vJ2cDwP8RnsXi818yGty1ayP55y3W6fg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", + "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", + "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromorph": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/micromorph/-/micromorph-0.4.5.tgz", + "integrity": "sha512-Erasr0xiDvDeEhh7B/k7RFTwwfaAX10D7BMorNpokkwDh6XsRLYWDPaWF1m5JQeMSkGdqlEtQ8s68NcdDWuGgw==" + }, + "node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mj-context-menu": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz", + "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==" + }, + "node_modules/moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nlcst-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz", + "integrity": "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==", + "dependencies": { + "@types/nlcst": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/oniguruma-to-es": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-1.0.0.tgz", + "integrity": "sha512-kihvp0O4lFwf5tZMkfanwQLIZ9ORe9OeOFgZonH0BQeThgwfJiaZFeOfvvJVnJIM9TiVmx0RDD35hUJDR0++rQ==", + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^5.1.1", + "regex-recursion": "^5.1.1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" + }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, + "node_modules/parse-css-color": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz", + "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.1.4", + "hex-rgb": "^4.1.0" + } + }, + "node_modules/parse-entities": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", + "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-latin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", + "integrity": "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==", + "dependencies": { + "@types/nlcst": "^2.0.0", + "@types/unist": "^3.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-modify-children": "^4.0.0", + "unist-util-visit-children": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-latin/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/parse-numeric-range": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", + "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==" + }, + "node_modules/parse-svg-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", + "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==" + }, + "node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pixi.js": { + "version": "8.6.6", + "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.6.6.tgz", + "integrity": "sha512-o5pw7G2yuIrnBx0G4npBlmFp+XGNcapI/Ufs62rRj/4XKxc1Zo74YJr/BtEXcXTraTKd+pQvYOLvnfxRjxBMvQ==", + "dependencies": { + "@pixi/colord": "^2.9.6", + "@types/css-font-loading-module": "^0.0.12", + "@types/earcut": "^2.1.4", + "@webgpu/types": "^0.1.40", + "@xmldom/xmldom": "^0.8.10", + "earcut": "^2.2.4", + "eventemitter3": "^5.0.1", + "ismobilejs": "^1.1.1", + "parse-svg-path": "^0.1.2" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/preact": { + "version": "10.25.4", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.25.4.tgz", + "integrity": "sha512-jLdZDb+Q+odkHJ+MpW/9U5cODzqnB+fy2EiHSZES7ldV5LK7yjlVzTp7R8Xy6W6y75kfK8iWYtFVH7lvjwrCMA==", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "6.5.13", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.13.tgz", + "integrity": "sha512-iGPd+hKPMFKsfpR2vL4kJ6ZPcFIoWZEcBf0Dpm3zOpdVvj77aY8RlLiQji5OMrngEyaxGogeakTb54uS2FvA6w==", + "peerDependencies": { + "preact": ">=10" + } + }, + "node_modules/prettier": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz", + "integrity": "sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/property-information": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.2.0.tgz", + "integrity": "sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reading-time": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/reading-time/-/reading-time-1.5.0.tgz", + "integrity": "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==" + }, + "node_modules/regex": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/regex/-/regex-5.1.1.tgz", + "integrity": "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-5.1.1.tgz", + "integrity": "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==", + "dependencies": { + "regex": "^5.1.1", + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==" + }, + "node_modules/rehype-autolink-headings": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-7.1.0.tgz", + "integrity": "sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-citation": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/rehype-citation/-/rehype-citation-2.2.2.tgz", + "integrity": "sha512-a9+njSn4yJ3/bePz+T8AkCLXhSb3fK+HKlG9xEcLJraN3W92jGV91a10XEvSy6gJ5BvRdtDtu3aEd1uqvNDHRQ==", + "dependencies": { + "@citation-js/core": "^0.7.14", + "@citation-js/date": "^0.5.1", + "@citation-js/name": "^0.4.2", + "@citation-js/plugin-bibjson": "^0.7.14", + "@citation-js/plugin-bibtex": "^0.7.14", + "@citation-js/plugin-csl": "^0.7.14", + "citeproc": "^2.4.63", + "cross-fetch": "^4.0.0", + "hast-util-from-dom": "^5.0.0", + "hast-util-from-parse5": "^8.0.1", + "js-yaml": "^4.1.0", + "parse5": "^7.1.2", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0" + } + }, + "node_modules/rehype-katex": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz", + "integrity": "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/katex": "^0.16.0", + "hast-util-from-html-isomorphic": "^2.0.0", + "hast-util-to-text": "^4.0.0", + "katex": "^0.16.0", + "unist-util-visit-parents": "^6.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-mathjax": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-mathjax/-/rehype-mathjax-6.0.0.tgz", + "integrity": "sha512-SioRmn+0mRWtDc4QVKG9JG88bXhPazfhc11GQoQ68mwot2WWyfabyZ7tuJu3Z4LCf893wXkQTVTF8PUlntoDwA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mathjax": "^0.0.40", + "hast-util-from-dom": "^5.0.0", + "hast-util-to-text": "^4.0.0", + "jsdom": "^23.0.0", + "mathjax-full": "^3.0.0", + "unified": "^11.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.0.tgz", + "integrity": "sha512-WG7nfvmWWkCR++KEkZevZb/uw41E8TsH4DsY9UxsTbIXCVGbAs4S+r8FrQ+OtH5EEQAs+5UxKC42VinkmpA1Yw==", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-pretty-code": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/rehype-pretty-code/-/rehype-pretty-code-0.14.0.tgz", + "integrity": "sha512-hBeKF/Wkkf3zyUS8lal9RCUuhypDWLQc+h9UrP9Pav25FUm/AQAVh4m5gdvJxh4Oz+U+xKvdsV01p1LdvsZTiQ==", + "dependencies": { + "@types/hast": "^3.0.4", + "hast-util-to-string": "^3.0.0", + "parse-numeric-range": "^1.3.0", + "rehype-parse": "^9.0.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "shiki": "^1.3.0" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz", + "integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==", + "dependencies": { + "@types/hast": "^3.0.0", + "github-slugger": "^2.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz", + "integrity": "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==", + "dependencies": { + "@types/mdast": "^4.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-breaks": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz", + "integrity": "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-newline-to-break": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-frontmatter": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz", + "integrity": "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-frontmatter": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz", + "integrity": "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-math": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz", + "integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-math": "^3.0.0", + "micromark-extension-math": "^3.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.1.tgz", + "integrity": "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-smartypants": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.2.tgz", + "integrity": "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==", + "dependencies": { + "retext": "^9.0.0", + "retext-smartypants": "^6.0.0", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retext": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", + "integrity": "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==", + "dependencies": { + "@types/nlcst": "^2.0.0", + "retext-latin": "^4.0.0", + "retext-stringify": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-latin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-latin/-/retext-latin-4.0.0.tgz", + "integrity": "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==", + "dependencies": { + "@types/nlcst": "^2.0.0", + "parse-latin": "^7.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.1.0.tgz", + "integrity": "sha512-LDPXg95346bqFZnDMHo0S7Rq5p64+B+N8Vz733+wPMDtwb9rCOs9LIdIEhrUOU+TAywX9St+ocQWJt8wrzivcQ==", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-stringify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-stringify/-/retext-stringify-4.0.0.tgz", + "integrity": "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" + }, + "node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, + "node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-identifier": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz", + "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sass": { + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.4.tgz", + "integrity": "sha512-K0QDSNPXgyqO4GZq2HO5Q70TLxTH6cIT59RdoCHMivrC8rqzaTw5ab9prjz9KUN1El4FLXrBXJhik61JR4HcGg==", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded": { + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.79.4.tgz", + "integrity": "sha512-3AATrtStMgxYjkit02/Ix8vx/P7qderYG6DHjmehfk5jiw53OaWVScmcGJSwp/d77kAkxDQ+Y0r+79VynGmrkw==", + "peer": true, + "dependencies": { + "@bufbuild/protobuf": "^2.0.0", + "buffer-builder": "^0.2.0", + "colorjs.io": "^0.5.0", + "immutable": "^4.0.0", + "rxjs": "^7.4.0", + "supports-color": "^8.1.1", + "varint": "^6.0.0" + }, + "bin": { + "sass": "dist/bin/sass.js" + }, + "engines": { + "node": ">=16.0.0" + }, + "optionalDependencies": { + "sass-embedded-android-arm": "1.79.4", + "sass-embedded-android-arm64": "1.79.4", + "sass-embedded-android-ia32": "1.79.4", + "sass-embedded-android-riscv64": "1.79.4", + "sass-embedded-android-x64": "1.79.4", + "sass-embedded-darwin-arm64": "1.79.4", + "sass-embedded-darwin-x64": "1.79.4", + "sass-embedded-linux-arm": "1.79.4", + "sass-embedded-linux-arm64": "1.79.4", + "sass-embedded-linux-ia32": "1.79.4", + "sass-embedded-linux-musl-arm": "1.79.4", + "sass-embedded-linux-musl-arm64": "1.79.4", + "sass-embedded-linux-musl-ia32": "1.79.4", + "sass-embedded-linux-musl-riscv64": "1.79.4", + "sass-embedded-linux-musl-x64": "1.79.4", + "sass-embedded-linux-riscv64": "1.79.4", + "sass-embedded-linux-x64": "1.79.4", + "sass-embedded-win32-arm64": "1.79.4", + "sass-embedded-win32-ia32": "1.79.4", + "sass-embedded-win32-x64": "1.79.4" + } + }, + "node_modules/sass-embedded-android-arm": { + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.79.4.tgz", + "integrity": "sha512-YOVpDGDcwWUQvktpJhYo4zOkknDpdX6ALpaeHDTX6GBUvnZfx+Widh76v+QFUhiJQ/I/hndXg1jv/PKilOHRrw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-arm64": { + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.79.4.tgz", + "integrity": "sha512-0JAZ8TtXYv9yI3Yasaq03xvo7DLJOmD+Exb30oJKxXcWTAV9TB0ZWKoIRsFxbCyPxyn7ouxkaCEXQtaTRKrmfw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-ia32": { + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass-embedded-android-ia32/-/sass-embedded-android-ia32-1.79.4.tgz", + "integrity": "sha512-IjO3RoyvNN84ZyfAR5s/a8TIdNPfClb7CLGrswB3BN/NElYIJUJMVHD6+Y8W9QwBIZ8DrK1IdLFSTV8nn82xMA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-riscv64": { + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.79.4.tgz", + "integrity": "sha512-uOT8nXmKxSwuIdcqvElVWBFcm/+YcIvmwfoKbpuuSOSxUe9eqFzxo+fk7ILhynzf6FBlvRUH5DcjGj+sXtCc3w==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-x64": { + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.79.4.tgz", + "integrity": "sha512-W2FQoj3Z2J2DirNs3xSBVvrhMuqLnsqvOPulxOkhL/074+faKOZZnPx2tZ5zsHbY97SonciiU0SV0mm98xI42w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-arm64": { + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.79.4.tgz", + "integrity": "sha512-pcYtbN1VUAAcfgyHeX8ySndDWGjIvcq6rldduktPbGGuAlEWFDfnwjTbv0hS945ggdzZ6TFnaFlLEDr0SjKzBA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-x64": { + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.79.4.tgz", + "integrity": "sha512-ir8CFTfc4JLx/qCP8LK1/3pWv35nRyAQkUK7lBIKM6hWzztt64gcno9rZIk4SpHr7Z/Bp1IYWWRS4ZT+4HmsbA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm": { + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.79.4.tgz", + "integrity": "sha512-H/XEE3rY7c+tY0qDaELjPjC6VheAhBo1tPJQ6UHoBEf8xrbT/RT3dWiIS8grp9Vk54RCn05BEB/+POaljvvKGA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm64": { + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.79.4.tgz", + "integrity": "sha512-XIVn2mCuA422SR2kmKjF6jhjMs1Vrt1DbZ/ktSp+eR0sU4ugu2htg45GajiUFSKKRj7Sc+cBdThq1zPPsDLf1w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-ia32": { + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-ia32/-/sass-embedded-linux-ia32-1.79.4.tgz", + "integrity": "sha512-3nqZxV4nuUTb1ahLexVl4hsnx1KKwiGdHEf1xHWTZai6fYFMcwyNPrHySCQzFHqb5xiqSpPzzrKjuDhF6+guuQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm": { + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.79.4.tgz", + "integrity": "sha512-HnbU1DEiQdUayioNzxh2WlbTEgQRBPTgIIvof8J63QLmVItUqE7EkWYkSUy4RhO+8NsuN9wzGmGTzFBvTImU7g==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm64": { + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.79.4.tgz", + "integrity": "sha512-C6qX06waPEfDgOHR8jXoYxl0EtIXOyBDyyonrLO3StRjWjGx7XMQj2hA/KXSsV+Hr71fBOsaViosqWXPzTbEiQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-ia32": { + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-ia32/-/sass-embedded-linux-musl-ia32-1.79.4.tgz", + "integrity": "sha512-y5b0fdOPWyhj4c+mc88GvQiC5onRH1V0iNaWNjsiZ+L4hHje6T98nDLrCJn0fz5GQnXjyLCLZduMWbfV0QjHGg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-riscv64": { + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.79.4.tgz", + "integrity": "sha512-G2M5ADMV9SqnkwpM0S+UzDz7xR2njCOhofku/sDMZABzAjQQWTsAykKoGmzlT98fTw2HbNhb6u74umf2WLhCfw==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-x64": { + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.79.4.tgz", + "integrity": "sha512-kQm8dCU3DXf7DtUGWYPiPs03KJYKvFeiZJHhSx993DCM8D2b0wCXWky0S0Z46gf1sEur0SN4Lvnt1WczTqxIBw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-riscv64": { + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.79.4.tgz", + "integrity": "sha512-GaTI/mXYWYSzG5wxtM4H2cozLpATyh+4l+rO9FFKOL8e1sUOLAzTeRdU2nSBYCuRqsxRuTZIwCXhSz9Q3NRuNA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-x64": { + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.79.4.tgz", + "integrity": "sha512-f9laGkqHgC01h99Qt4LsOV+OLMffjvUcTu14hYWqMS9QVX5a4ihMwpf1NoAtTUytb7cVF3rYY/NVGuXt6G3ppQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-arm64": { + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.79.4.tgz", + "integrity": "sha512-cidBvtaA2cJ6dNlwQEa8qak+ezypurzKs0h0QAHLH324+j/6Jum7LCnQhZRPYJBFjHl+WYd7KwzPnJ2X5USWnQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-ia32": { + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-ia32/-/sass-embedded-win32-ia32-1.79.4.tgz", + "integrity": "sha512-hexdmNTIZGTKNTzlMcdvEXzYuxOJcY89zqgsf45aQ2YMy4y2M8dTOxRI/Vz7p4iRxVp1Jow6LCtaLHrNI2Ordg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-x64": { + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.79.4.tgz", + "integrity": "sha512-73yrpiWIbti6DkxhWURklkgSLYKfU9itDmvHxB+oYSb4vQveIApqTwSyTOuIUb/6Da/EsgEpdJ4Lbj4sLaMZWA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/satori": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/satori/-/satori-0.12.1.tgz", + "integrity": "sha512-0SbjchvDrDbeXeQgxWVtSWxww7qcFgk3DtSE2/blHOSlLsSHwIqO2fCrtVa/EudJ7Eqno8A33QNx56rUyGbLuw==", + "dependencies": { + "@shuding/opentype.js": "1.4.0-beta.0", + "css-background-parser": "^0.1.0", + "css-box-shadow": "1.0.0-3", + "css-gradient-parser": "^0.0.16", + "css-to-react-native": "^3.0.0", + "emoji-regex": "^10.2.1", + "escape-html": "^1.0.3", + "linebreak": "^1.1.0", + "parse-css-color": "^0.2.1", + "postcss-value-parser": "^4.2.0", + "yoga-wasm-web": "^0.3.3" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/satori/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serve-handler": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz", + "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==", + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.2", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/serve-handler/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/sharp/node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/shiki": { + "version": "1.26.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.26.2.tgz", + "integrity": "sha512-iP7u2NA9A6JwRRCkIUREEX2cMhlYV5EBmbbSlfSRvPThwca8HBRbVkWuNWW+kw9+i6BSUZqqG6YeUs5dC2SjZw==", + "peer": true, + "dependencies": { + "@shikijs/core": "1.26.2", + "@shikijs/engine-javascript": "1.26.2", + "@shikijs/engine-oniguruma": "1.26.2", + "@shikijs/langs": "1.26.2", + "@shikijs/themes": "1.26.2", + "@shikijs/types": "1.26.2", + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/speech-rule-engine": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-4.0.7.tgz", + "integrity": "sha512-sJrL3/wHzNwJRLBdf6CjJWIlxC04iYKkyXvYSVsWVOiC2DSkHmxsqOhEeMsBA9XK+CHuNcsdkbFDnoUfAsmp9g==", + "dependencies": { + "commander": "9.2.0", + "wicked-good-xpath": "1.3.0", + "xmldom-sre": "0.1.31" + }, + "bin": { + "sre": "bin/sre" + } + }, + "node_modules/speech-rule-engine/node_modules/commander": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz", + "integrity": "sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.codepointat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", + "license": "MIT" + }, + "node_modules/stringify-entities": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz", + "integrity": "sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/style-to-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.5.tgz", + "integrity": "sha512-rDRwHtoDD3UMMrmZ6BzOW0naTjMsVZLIjsGleSKS/0Oz+cgCfAPRspaqJuE8rDzpKha/nEvnM0IF4seEAZUTKQ==", + "dependencies": { + "inline-style-parser": "0.2.2" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, + "node_modules/sync-fetch": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.4.5.tgz", + "integrity": "sha512-esiWJ7ixSKGpd9DJPBTC4ckChqdOjIwJfYhVHkcQ2Gnm41323p1TRmEI+esTQ9ppD+b5opps2OTEGTCGX5kF+g==", + "dependencies": { + "buffer": "^5.7.1", + "node-fetch": "^2.6.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/to-vfile": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/to-vfile/-/to-vfile-8.0.0.tgz", + "integrity": "sha512-IcmH1xB5576MJc9qcfEC/m/nQCFt3fzMHz45sSlgJyTWjRbKW1HAkJpuf3DgE57YzIlZcwcBZA5ENQbBo4aLkg==", + "dependencies": { + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" + }, + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tr46/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", + "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/tsx": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", + "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", + "dev": true, + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", + "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", + "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", + "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", + "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", + "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", + "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", + "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", + "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", + "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", + "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", + "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", + "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", + "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", + "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", + "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", + "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", + "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", + "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", + "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", + "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", + "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", + "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", + "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", + "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.0", + "@esbuild/android-arm": "0.23.0", + "@esbuild/android-arm64": "0.23.0", + "@esbuild/android-x64": "0.23.0", + "@esbuild/darwin-arm64": "0.23.0", + "@esbuild/darwin-x64": "0.23.0", + "@esbuild/freebsd-arm64": "0.23.0", + "@esbuild/freebsd-x64": "0.23.0", + "@esbuild/linux-arm": "0.23.0", + "@esbuild/linux-arm64": "0.23.0", + "@esbuild/linux-ia32": "0.23.0", + "@esbuild/linux-loong64": "0.23.0", + "@esbuild/linux-mips64el": "0.23.0", + "@esbuild/linux-ppc64": "0.23.0", + "@esbuild/linux-riscv64": "0.23.0", + "@esbuild/linux-s390x": "0.23.0", + "@esbuild/linux-x64": "0.23.0", + "@esbuild/netbsd-x64": "0.23.0", + "@esbuild/openbsd-arm64": "0.23.0", + "@esbuild/openbsd-x64": "0.23.0", + "@esbuild/sunos-x64": "0.23.0", + "@esbuild/win32-arm64": "0.23.0", + "@esbuild/win32-ia32": "0.23.0", + "@esbuild/win32-x64": "0.23.0" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-find-after/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/unist-util-modify-children": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-4.0.0.tgz", + "integrity": "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==", + "dependencies": { + "@types/unist": "^3.0.0", + "array-iterate": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-modify-children/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-children": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-3.0.0.tgz", + "integrity": "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-children/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/unist-util-visit/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/varint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==" + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.2.tgz", + "integrity": "sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/vfile/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/vfile/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile/node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wicked-good-xpath": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz", + "integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==" + }, + "node_modules/workerpool": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.2.0.tgz", + "integrity": "sha512-PKZqBOCo6CYkVOwAxWxQaSF2Fvb5Iv2fCeTP7buyWI2GiynWr46NcXSgK/idoV6e60dgCBfgYc+Un3HMvmqP8w==" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, + "node_modules/xmldom-sre": { + "version": "0.1.31", + "resolved": "https://registry.npmjs.org/xmldom-sre/-/xmldom-sre-0.1.31.tgz", + "integrity": "sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw==", + "engines": { + "node": ">=0.1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yoga-wasm-web": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz", + "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==", + "license": "MIT" + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json new file mode 100755 index 0000000..726350b --- /dev/null +++ b/package.json @@ -0,0 +1,115 @@ +{ + "name": "@jackyzha0/quartz", + "description": "🌱 publish your digital garden and notes as a website", + "private": true, + "version": "4.4.0", + "type": "module", + "author": "jackyzha0 ", + "license": "MIT", + "homepage": "https://quartz.jzhao.xyz", + "repository": { + "type": "git", + "url": "https://github.com/jackyzha0/quartz.git" + }, + "scripts": { + "quartz": "./quartz/bootstrap-cli.mjs", + "docs": "npx quartz build --serve -d docs", + "check": "tsc --noEmit && npx prettier . --check", + "format": "npx prettier . --write", + "test": "tsx ./quartz/util/path.test.ts && tsx ./quartz/depgraph.test.ts", + "profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1" + }, + "engines": { + "npm": ">=9.3.1", + "node": "20 || >=22" + }, + "keywords": [ + "site generator", + "ssg", + "digital-garden", + "markdown", + "blog", + "quartz" + ], + "bin": { + "quartz": "./quartz/bootstrap-cli.mjs" + }, + "dependencies": { + "@clack/prompts": "^0.9.1", + "@floating-ui/dom": "^1.6.13", + "@myriaddreamin/rehype-typst": "^0.5.4", + "@napi-rs/simple-git": "0.1.19", + "@tweenjs/tween.js": "^25.0.0", + "async-mutex": "^0.5.0", + "chalk": "^5.4.1", + "chokidar": "^4.0.3", + "cli-spinner": "^0.2.10", + "d3": "^7.9.0", + "esbuild-sass-plugin": "^3.3.1", + "flexsearch": "0.7.43", + "github-slugger": "^2.0.0", + "globby": "^14.0.2", + "gray-matter": "^4.0.3", + "hast-util-to-html": "^9.0.4", + "hast-util-to-jsx-runtime": "^2.3.2", + "hast-util-to-string": "^3.0.1", + "is-absolute-url": "^4.0.1", + "js-yaml": "^4.1.0", + "lightningcss": "^1.29.1", + "mdast-util-find-and-replace": "^3.0.2", + "mdast-util-to-hast": "^13.2.0", + "mdast-util-to-string": "^4.0.0", + "micromorph": "^0.4.5", + "pixi.js": "^8.6.6", + "preact": "^10.25.4", + "preact-render-to-string": "^6.5.13", + "pretty-bytes": "^6.1.1", + "pretty-time": "^1.1.0", + "reading-time": "^1.5.0", + "rehype-autolink-headings": "^7.1.0", + "rehype-citation": "^2.2.2", + "rehype-katex": "^7.0.1", + "rehype-mathjax": "^6.0.0", + "rehype-pretty-code": "^0.14.0", + "rehype-raw": "^7.0.0", + "rehype-slug": "^6.0.0", + "remark": "^15.0.1", + "remark-breaks": "^4.0.0", + "remark-frontmatter": "^5.0.0", + "remark-gfm": "^4.0.0", + "remark-math": "^6.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.1", + "remark-smartypants": "^3.0.2", + "rfdc": "^1.4.1", + "rimraf": "^6.0.1", + "satori": "^0.12.1", + "serve-handler": "^6.1.6", + "sharp": "^0.33.5", + "shiki": "^1.26.2", + "source-map-support": "^0.5.21", + "to-vfile": "^8.0.0", + "toml": "^3.0.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.3", + "workerpool": "^9.2.0", + "ws": "^8.18.0", + "yargs": "^17.7.2" + }, + "devDependencies": { + "@types/cli-spinner": "^0.2.3", + "@types/d3": "^7.4.3", + "@types/hast": "^3.0.4", + "@types/js-yaml": "^4.0.9", + "@types/node": "^22.10.6", + "@types/pretty-time": "^1.1.5", + "@types/source-map-support": "^0.5.10", + "@types/ws": "^8.5.13", + "@types/yargs": "^17.0.33", + "esbuild": "^0.24.2", + "prettier": "^3.4.2", + "tsx": "^4.19.2", + "typescript": "^5.7.3" + } +} diff --git a/quartz.config.ts b/quartz.config.ts new file mode 100755 index 0000000..349096f --- /dev/null +++ b/quartz.config.ts @@ -0,0 +1,95 @@ +import { QuartzConfig } from "./quartz/cfg" +import * as Plugin from "./quartz/plugins" + +/** + * Quartz 4.0 Configuration + * + * See https://quartz.jzhao.xyz/configuration for more information. + */ +const config: QuartzConfig = { + configuration: { + pageTitle: "EXPERIMENTING", + pageTitleSuffix: " ◦ EXPERIMENTING", + enableSPA: true, + enablePopovers: true, + analytics: { + provider: "plausible", + }, + locale: "en-US", + baseUrl: "https://note.experimenting.website/", + ignorePatterns: ["private", "templates", ".obsidian"], + defaultDateType: "created", + generateSocialImages: false, + theme: { + fontOrigin: "googleFonts", + cdnCaching: true, + typography: { + header: "Share Tech Mono", + body: "Sono", + code: "IBM Plex Mono", + }, + colors: { + lightMode: { + light: "#faf8f8", + lightgray: "#e5e5e5", + gray: "#b8b8b8", + darkgray: "#4e4e4e", + dark: "#2b2b2b", + secondary: "#284b63", + tertiary: "#84a59d", + highlight: "rgba(143, 159, 169, 0.15)", + textHighlight: "#fff23688", + }, + darkMode: { + light: "rgb(0, 0, 0)", + lightgray: "rgb(32, 32, 32)", + gray: "#646464", + darkgray: "#d4d4d4", + dark: "#ebebec", + secondary: "#7b97aa", + tertiary: "#84a59d", + highlight: "rgba(143, 159, 169, 0.15)", + textHighlight: "#b3aa0288", + }, + }, + }, + }, + plugins: { + transformers: [ + Plugin.FrontMatter(), + Plugin.CreatedModifiedDate({ + priority: ["frontmatter", "filesystem"], + }), + Plugin.SyntaxHighlighting({ + theme: { + light: "github-light", + dark: "github-dark", + }, + keepBackground: false, + }), + Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }), + Plugin.GitHubFlavoredMarkdown(), + Plugin.TableOfContents(), + Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }), + Plugin.Description(), + Plugin.Latex({ renderEngine: "katex" }), + ], + filters: [Plugin.RemoveDrafts()], + emitters: [ + Plugin.AliasRedirects(), + Plugin.ComponentResources(), + Plugin.ContentPage(), + Plugin.FolderPage(), + Plugin.TagPage(), + Plugin.ContentIndex({ + enableSiteMap: true, + enableRSS: true, + }), + Plugin.Assets(), + Plugin.Static(), + Plugin.NotFoundPage(), + ], + }, +} + +export default config \ No newline at end of file diff --git a/quartz.layout.ts b/quartz.layout.ts new file mode 100755 index 0000000..6edbfb8 --- /dev/null +++ b/quartz.layout.ts @@ -0,0 +1,42 @@ +import { PageLayout, SharedLayout } from "./quartz/cfg" +import * as Component from "./quartz/components" + +// components shared across all pages +export const sharedPageComponents: SharedLayout = { + head: Component.Head(), + header: [ + Component.Darkmode(), + Component.PageTitle(), + Component.Search(), + ], + afterBody: [ + Component.MobileOnly(Component.Separator()), + ], + footer: Component.Footer(), +} + +// components for pages that display a single page (e.g. a single note) +export const defaultContentPageLayout: PageLayout = { + beforeBody: [ + Component.ArticleTitle(), + ], + left: [ + ], + right: [ + Component.Graph(), + Component.DesktopOnly(Component.TableOfContents()), + Component.Backlinks(), + ], +} + +// components for pages that display lists of pages (e.g. tags or folders) +export const defaultListPageLayout: PageLayout = { + beforeBody: [ + Component.ArticleTitle(), + ], + left: [ + ], + right: [ + Component.DesktopOnly(Component.Graph()), + ], +} diff --git a/quartz/bootstrap-cli.mjs b/quartz/bootstrap-cli.mjs new file mode 100755 index 0000000..35d06af --- /dev/null +++ b/quartz/bootstrap-cli.mjs @@ -0,0 +1,41 @@ +#!/usr/bin/env node +import yargs from "yargs" +import { hideBin } from "yargs/helpers" +import { + handleBuild, + handleCreate, + handleUpdate, + handleRestore, + handleSync, +} from "./cli/handlers.js" +import { CommonArgv, BuildArgv, CreateArgv, SyncArgv } from "./cli/args.js" +import { version } from "./cli/constants.js" + +yargs(hideBin(process.argv)) + .scriptName("quartz") + .version(version) + .usage("$0 [args]") + .command("create", "Initialize Quartz", CreateArgv, async (argv) => { + await handleCreate(argv) + }) + .command("update", "Get the latest Quartz updates", CommonArgv, async (argv) => { + await handleUpdate(argv) + }) + .command( + "restore", + "Try to restore your content folder from the cache", + CommonArgv, + async (argv) => { + await handleRestore(argv) + }, + ) + .command("sync", "Sync your Quartz to and from GitHub.", SyncArgv, async (argv) => { + await handleSync(argv) + }) + .command("build", "Build Quartz into a bundle of static HTML files", BuildArgv, async (argv) => { + await handleBuild(argv) + }) + .showHelpOnFail(false) + .help() + .strict() + .demandCommand().argv diff --git a/quartz/bootstrap-worker.mjs b/quartz/bootstrap-worker.mjs new file mode 100755 index 0000000..c4c4949 --- /dev/null +++ b/quartz/bootstrap-worker.mjs @@ -0,0 +1,8 @@ +#!/usr/bin/env node +import workerpool from "workerpool" +const cacheFile = "./.quartz-cache/transpiled-worker.mjs" +const { parseMarkdown, processHtml } = await import(cacheFile) +workerpool.worker({ + parseMarkdown, + processHtml, +}) diff --git a/quartz/build.ts b/quartz/build.ts new file mode 100755 index 0000000..64c462b --- /dev/null +++ b/quartz/build.ts @@ -0,0 +1,422 @@ +import sourceMapSupport from "source-map-support" +sourceMapSupport.install(options) +import path from "path" +import { PerfTimer } from "./util/perf" +import { rimraf } from "rimraf" +import { GlobbyFilterFunction, isGitIgnored } from "globby" +import chalk from "chalk" +import { parseMarkdown } from "./processors/parse" +import { filterContent } from "./processors/filter" +import { emitContent } from "./processors/emit" +import cfg from "../quartz.config" +import { FilePath, FullSlug, joinSegments, slugifyFilePath } from "./util/path" +import chokidar from "chokidar" +import { ProcessedContent } from "./plugins/vfile" +import { Argv, BuildCtx } from "./util/ctx" +import { glob, toPosixPath } from "./util/glob" +import { trace } from "./util/trace" +import { options } from "./util/sourcemap" +import { Mutex } from "async-mutex" +import DepGraph from "./depgraph" +import { getStaticResourcesFromPlugins } from "./plugins" + +type Dependencies = Record | null> + +type BuildData = { + ctx: BuildCtx + ignored: GlobbyFilterFunction + mut: Mutex + initialSlugs: FullSlug[] + // TODO merge contentMap and trackedAssets + contentMap: Map + trackedAssets: Set + toRebuild: Set + toRemove: Set + lastBuildMs: number + dependencies: Dependencies +} + +type FileEvent = "add" | "change" | "delete" + +function newBuildId() { + return Math.random().toString(36).substring(2, 8) +} + +async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) { + const ctx: BuildCtx = { + buildId: newBuildId(), + argv, + cfg, + allSlugs: [], + } + + const perf = new PerfTimer() + const output = argv.output + + const pluginCount = Object.values(cfg.plugins).flat().length + const pluginNames = (key: "transformers" | "filters" | "emitters") => + cfg.plugins[key].map((plugin) => plugin.name) + if (argv.verbose) { + console.log(`Loaded ${pluginCount} plugins`) + console.log(` Transformers: ${pluginNames("transformers").join(", ")}`) + console.log(` Filters: ${pluginNames("filters").join(", ")}`) + console.log(` Emitters: ${pluginNames("emitters").join(", ")}`) + } + + const release = await mut.acquire() + perf.addEvent("clean") + await rimraf(path.join(output, "*"), { glob: true }) + console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`) + + perf.addEvent("glob") + const allFiles = await glob("**/*.*", argv.directory, cfg.configuration.ignorePatterns) + const fps = allFiles.filter((fp) => fp.endsWith(".md")).sort() + console.log( + `Found ${fps.length} input files from \`${argv.directory}\` in ${perf.timeSince("glob")}`, + ) + + const filePaths = fps.map((fp) => joinSegments(argv.directory, fp) as FilePath) + ctx.allSlugs = allFiles.map((fp) => slugifyFilePath(fp as FilePath)) + + const parsedFiles = await parseMarkdown(ctx, filePaths) + const filteredContent = filterContent(ctx, parsedFiles) + + const dependencies: Record | null> = {} + + // Only build dependency graphs if we're doing a fast rebuild + if (argv.fastRebuild) { + const staticResources = getStaticResourcesFromPlugins(ctx) + for (const emitter of cfg.plugins.emitters) { + dependencies[emitter.name] = + (await emitter.getDependencyGraph?.(ctx, filteredContent, staticResources)) ?? null + } + } + + await emitContent(ctx, filteredContent) + console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`)) + release() + + if (argv.serve) { + return startServing(ctx, mut, parsedFiles, clientRefresh, dependencies) + } +} + +// setup watcher for rebuilds +async function startServing( + ctx: BuildCtx, + mut: Mutex, + initialContent: ProcessedContent[], + clientRefresh: () => void, + dependencies: Dependencies, // emitter name: dep graph +) { + const { argv } = ctx + + // cache file parse results + const contentMap = new Map() + for (const content of initialContent) { + const [_tree, vfile] = content + contentMap.set(vfile.data.filePath!, content) + } + + const buildData: BuildData = { + ctx, + mut, + dependencies, + contentMap, + ignored: await isGitIgnored(), + initialSlugs: ctx.allSlugs, + toRebuild: new Set(), + toRemove: new Set(), + trackedAssets: new Set(), + lastBuildMs: 0, + } + + const watcher = chokidar.watch(".", { + persistent: true, + cwd: argv.directory, + ignoreInitial: true, + }) + + const buildFromEntry = argv.fastRebuild ? partialRebuildFromEntrypoint : rebuildFromEntrypoint + watcher + .on("add", (fp) => buildFromEntry(fp as string, "add", clientRefresh, buildData)) + .on("change", (fp) => buildFromEntry(fp as string, "change", clientRefresh, buildData)) + .on("unlink", (fp) => buildFromEntry(fp as string, "delete", clientRefresh, buildData)) + + return async () => { + await watcher.close() + } +} + +async function partialRebuildFromEntrypoint( + filepath: string, + action: FileEvent, + clientRefresh: () => void, + buildData: BuildData, // note: this function mutates buildData +) { + const { ctx, ignored, dependencies, contentMap, mut, toRemove } = buildData + const { argv, cfg } = ctx + + // don't do anything for gitignored files + if (ignored(filepath)) { + return + } + + const buildId = newBuildId() + ctx.buildId = buildId + buildData.lastBuildMs = new Date().getTime() + const release = await mut.acquire() + + // if there's another build after us, release and let them do it + if (ctx.buildId !== buildId) { + release() + return + } + + const perf = new PerfTimer() + console.log(chalk.yellow("Detected change, rebuilding...")) + + // UPDATE DEP GRAPH + const fp = joinSegments(argv.directory, toPosixPath(filepath)) as FilePath + + const staticResources = getStaticResourcesFromPlugins(ctx) + let processedFiles: ProcessedContent[] = [] + + switch (action) { + case "add": + // add to cache when new file is added + processedFiles = await parseMarkdown(ctx, [fp]) + processedFiles.forEach(([tree, vfile]) => contentMap.set(vfile.data.filePath!, [tree, vfile])) + + // update the dep graph by asking all emitters whether they depend on this file + for (const emitter of cfg.plugins.emitters) { + const emitterGraph = + (await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null + + if (emitterGraph) { + const existingGraph = dependencies[emitter.name] + if (existingGraph !== null) { + existingGraph.mergeGraph(emitterGraph) + } else { + // might be the first time we're adding a mardown file + dependencies[emitter.name] = emitterGraph + } + } + } + break + case "change": + // invalidate cache when file is changed + processedFiles = await parseMarkdown(ctx, [fp]) + processedFiles.forEach(([tree, vfile]) => contentMap.set(vfile.data.filePath!, [tree, vfile])) + + // only content files can have added/removed dependencies because of transclusions + if (path.extname(fp) === ".md") { + for (const emitter of cfg.plugins.emitters) { + // get new dependencies from all emitters for this file + const emitterGraph = + (await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null + + // only update the graph if the emitter plugin uses the changed file + // eg. Assets plugin ignores md files, so we skip updating the graph + if (emitterGraph?.hasNode(fp)) { + // merge the new dependencies into the dep graph + dependencies[emitter.name]?.updateIncomingEdgesForNode(emitterGraph, fp) + } + } + } + break + case "delete": + toRemove.add(fp) + break + } + + if (argv.verbose) { + console.log(`Updated dependency graphs in ${perf.timeSince()}`) + } + + // EMIT + perf.addEvent("rebuild") + let emittedFiles = 0 + + for (const emitter of cfg.plugins.emitters) { + const depGraph = dependencies[emitter.name] + + // emitter hasn't defined a dependency graph. call it with all processed files + if (depGraph === null) { + if (argv.verbose) { + console.log( + `Emitter ${emitter.name} doesn't define a dependency graph. Calling it with all files...`, + ) + } + + const files = [...contentMap.values()].filter( + ([_node, vfile]) => !toRemove.has(vfile.data.filePath!), + ) + + const emittedFps = await emitter.emit(ctx, files, staticResources) + + if (ctx.argv.verbose) { + for (const file of emittedFps) { + console.log(`[emit:${emitter.name}] ${file}`) + } + } + + emittedFiles += emittedFps.length + continue + } + + // only call the emitter if it uses this file + if (depGraph.hasNode(fp)) { + // re-emit using all files that are needed for the downstream of this file + // eg. for ContentIndex, the dep graph could be: + // a.md --> contentIndex.json + // b.md ------^ + // + // if a.md changes, we need to re-emit contentIndex.json, + // and supply [a.md, b.md] to the emitter + const upstreams = [...depGraph.getLeafNodeAncestors(fp)] as FilePath[] + + const upstreamContent = upstreams + // filter out non-markdown files + .filter((file) => contentMap.has(file)) + // if file was deleted, don't give it to the emitter + .filter((file) => !toRemove.has(file)) + .map((file) => contentMap.get(file)!) + + const emittedFps = await emitter.emit(ctx, upstreamContent, staticResources) + + if (ctx.argv.verbose) { + for (const file of emittedFps) { + console.log(`[emit:${emitter.name}] ${file}`) + } + } + + emittedFiles += emittedFps.length + } + } + + console.log(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`) + + // CLEANUP + const destinationsToDelete = new Set() + for (const file of toRemove) { + // remove from cache + contentMap.delete(file) + Object.values(dependencies).forEach((depGraph) => { + // remove the node from dependency graphs + depGraph?.removeNode(file) + // remove any orphan nodes. eg if a.md is deleted, a.html is orphaned and should be removed + const orphanNodes = depGraph?.removeOrphanNodes() + orphanNodes?.forEach((node) => { + // only delete files that are in the output directory + if (node.startsWith(argv.output)) { + destinationsToDelete.add(node) + } + }) + }) + } + await rimraf([...destinationsToDelete]) + + console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`)) + + toRemove.clear() + release() + clientRefresh() +} + +async function rebuildFromEntrypoint( + fp: string, + action: FileEvent, + clientRefresh: () => void, + buildData: BuildData, // note: this function mutates buildData +) { + const { ctx, ignored, mut, initialSlugs, contentMap, toRebuild, toRemove, trackedAssets } = + buildData + + const { argv } = ctx + + // don't do anything for gitignored files + if (ignored(fp)) { + return + } + + // dont bother rebuilding for non-content files, just track and refresh + fp = toPosixPath(fp) + const filePath = joinSegments(argv.directory, fp) as FilePath + if (path.extname(fp) !== ".md") { + if (action === "add" || action === "change") { + trackedAssets.add(filePath) + } else if (action === "delete") { + trackedAssets.delete(filePath) + } + clientRefresh() + return + } + + if (action === "add" || action === "change") { + toRebuild.add(filePath) + } else if (action === "delete") { + toRemove.add(filePath) + } + + const buildId = newBuildId() + ctx.buildId = buildId + buildData.lastBuildMs = new Date().getTime() + const release = await mut.acquire() + + // there's another build after us, release and let them do it + if (ctx.buildId !== buildId) { + release() + return + } + + const perf = new PerfTimer() + console.log(chalk.yellow("Detected change, rebuilding...")) + + try { + const filesToRebuild = [...toRebuild].filter((fp) => !toRemove.has(fp)) + const parsedContent = await parseMarkdown(ctx, filesToRebuild) + for (const content of parsedContent) { + const [_tree, vfile] = content + contentMap.set(vfile.data.filePath!, content) + } + + for (const fp of toRemove) { + contentMap.delete(fp) + } + + const parsedFiles = [...contentMap.values()] + 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 + // instead of just deleting everything + await rimraf(path.join(argv.output, ".*"), { glob: true }) + await emitContent(ctx, filteredContent) + console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`)) + } catch (err) { + console.log(chalk.yellow(`Rebuild failed. Waiting on a change to fix the error...`)) + if (argv.verbose) { + console.log(chalk.red(err)) + } + } + + clientRefresh() + toRebuild.clear() + toRemove.clear() + release() +} + +export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => { + try { + return await buildQuartz(argv, mut, clientRefresh) + } catch (err) { + trace("\nExiting Quartz due to a fatal error", err as Error) + } +} diff --git a/quartz/cfg.ts b/quartz/cfg.ts new file mode 100755 index 0000000..135f584 --- /dev/null +++ b/quartz/cfg.ts @@ -0,0 +1,97 @@ +import { ValidDateType } from "./components/Date" +import { QuartzComponent } from "./components/types" +import { ValidLocale } from "./i18n" +import { PluginTypes } from "./plugins/types" +import { SocialImageOptions } from "./util/og" +import { Theme } from "./util/theme" + +export type Analytics = + | null + | { + provider: "plausible" + host?: string + } + | { + provider: "google" + tagId: string + } + | { + provider: "umami" + websiteId: string + host?: string + } + | { + provider: "goatcounter" + websiteId: string + host?: string + scriptSrc?: string + } + | { + provider: "posthog" + apiKey: string + host?: string + } + | { + provider: "tinylytics" + siteId: string + } + | { + provider: "cabin" + host?: string + } + | { + provider: "clarity" + projectId?: string + } + +export interface GlobalConfiguration { + pageTitle: string + pageTitleSuffix?: string + /** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */ + enableSPA: boolean + /** Whether to display Wikipedia-style popovers when hovering over links */ + enablePopovers: boolean + /** Analytics mode */ + analytics: Analytics + /** Glob patterns to not search */ + ignorePatterns: string[] + /** Whether to use created, modified, or published as the default type of date */ + defaultDateType: ValidDateType + /** Base URL to use for CNAME files, sitemaps, and RSS feeds that require an absolute URL. + * Quartz will avoid using this as much as possible and use relative URLs most of the time + */ + baseUrl?: string + /** + * Whether to generate social images (Open Graph and Twitter standard) for link previews + */ + generateSocialImages: boolean | Partial + theme: Theme + /** + * Allow to translate the date in the language of your choice. + * Also used for UI translation (default: en-US) + * Need to be formatted following BCP 47: https://en.wikipedia.org/wiki/IETF_language_tag + * The first part is the language (en) and the second part is the script/region (US) + * Language Codes: https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes + * Region Codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 + */ + locale: ValidLocale +} + +export interface QuartzConfig { + configuration: GlobalConfiguration + plugins: PluginTypes +} + +export interface FullPageLayout { + head: QuartzComponent + header: QuartzComponent[] + beforeBody: QuartzComponent[] + pageBody: QuartzComponent + afterBody: QuartzComponent[] + left: QuartzComponent[] + right: QuartzComponent[] + footer: QuartzComponent +} + +export type PageLayout = Pick +export type SharedLayout = Pick diff --git a/quartz/cli/args.js b/quartz/cli/args.js new file mode 100755 index 0000000..123d0ac --- /dev/null +++ b/quartz/cli/args.js @@ -0,0 +1,108 @@ +export const CommonArgv = { + directory: { + string: true, + alias: ["d"], + default: "content", + describe: "directory to look for content files", + }, + verbose: { + boolean: true, + alias: ["v"], + default: false, + describe: "print out extra logging information", + }, +} + +export const CreateArgv = { + ...CommonArgv, + source: { + string: true, + alias: ["s"], + describe: "source directory to copy/create symlink from", + }, + strategy: { + string: true, + alias: ["X"], + choices: ["new", "copy", "symlink"], + describe: "strategy for content folder setup", + }, + links: { + string: true, + alias: ["l"], + choices: ["absolute", "shortest", "relative"], + describe: "strategy to resolve links", + }, +} + +export const SyncArgv = { + ...CommonArgv, + commit: { + boolean: true, + default: true, + describe: "create a git commit for your unsaved changes", + }, + message: { + string: true, + alias: ["m"], + describe: "option to override the default Quartz commit message", + }, + push: { + boolean: true, + default: true, + describe: "push updates to your Quartz fork", + }, + pull: { + boolean: true, + default: true, + describe: "pull updates from your Quartz fork", + }, +} + +export const BuildArgv = { + ...CommonArgv, + output: { + string: true, + alias: ["o"], + default: "public", + describe: "output folder for files", + }, + serve: { + boolean: true, + default: false, + describe: "run a local server to live-preview your Quartz", + }, + fastRebuild: { + boolean: true, + default: false, + describe: "[experimental] rebuild only the changed files", + }, + baseDir: { + string: true, + default: "", + describe: "base path to serve your local server on", + }, + port: { + number: true, + default: 8080, + describe: "port to serve Quartz on", + }, + wsPort: { + number: true, + default: 3001, + describe: "port to use for WebSocket-based hot-reload notifications", + }, + remoteDevHost: { + string: true, + default: "", + describe: "A URL override for the websocket connection if you are not developing on localhost", + }, + bundleInfo: { + boolean: true, + default: false, + describe: "show detailed bundle information", + }, + concurrency: { + number: true, + describe: "how many threads to use to parse notes", + }, +} diff --git a/quartz/cli/constants.js b/quartz/cli/constants.js new file mode 100755 index 0000000..f4a9ce5 --- /dev/null +++ b/quartz/cli/constants.js @@ -0,0 +1,15 @@ +import path from "path" +import { readFileSync } from "fs" + +/** + * All constants relating to helpers or handlers + */ +export const ORIGIN_NAME = "origin" +export const UPSTREAM_NAME = "upstream" +export const QUARTZ_SOURCE_BRANCH = "v4" +export const cwd = process.cwd() +export const cacheDir = path.join(cwd, ".quartz-cache") +export const cacheFile = "./quartz/.quartz-cache/transpiled-build.mjs" +export const fp = "./quartz/build.ts" +export const { version } = JSON.parse(readFileSync("./package.json").toString()) +export const contentCacheFolder = path.join(cacheDir, "content-cache") diff --git a/quartz/cli/handlers.js b/quartz/cli/handlers.js new file mode 100755 index 0000000..6b23d80 --- /dev/null +++ b/quartz/cli/handlers.js @@ -0,0 +1,576 @@ +import { promises } from "fs" +import path from "path" +import esbuild from "esbuild" +import chalk from "chalk" +import { sassPlugin } from "esbuild-sass-plugin" +import fs from "fs" +import { intro, outro, select, text } from "@clack/prompts" +import { rimraf } from "rimraf" +import chokidar from "chokidar" +import prettyBytes from "pretty-bytes" +import { execSync, spawnSync } from "child_process" +import http from "http" +import serveHandler from "serve-handler" +import { WebSocketServer } from "ws" +import { randomUUID } from "crypto" +import { Mutex } from "async-mutex" +import { CreateArgv } from "./args.js" +import { globby } from "globby" +import { + exitIfCancel, + escapePath, + gitPull, + popContentFolder, + stashContentFolder, +} from "./helpers.js" +import { + UPSTREAM_NAME, + QUARTZ_SOURCE_BRANCH, + ORIGIN_NAME, + version, + fp, + cacheFile, + cwd, +} from "./constants.js" + +/** + * Handles `npx quartz create` + * @param {*} argv arguments for `create` + */ +export async function handleCreate(argv) { + console.log() + intro(chalk.bgGreen.black(` Quartz v${version} `)) + const contentFolder = path.join(cwd, argv.directory) + let setupStrategy = argv.strategy?.toLowerCase() + let linkResolutionStrategy = argv.links?.toLowerCase() + const sourceDirectory = argv.source + + // If all cmd arguments were provided, check if they're valid + if (setupStrategy && linkResolutionStrategy) { + // If setup isn't, "new", source argument is required + if (setupStrategy !== "new") { + // Error handling + if (!sourceDirectory) { + outro( + chalk.red( + `Setup strategies (arg '${chalk.yellow( + `-${CreateArgv.strategy.alias[0]}`, + )}') other than '${chalk.yellow( + "new", + )}' require content folder argument ('${chalk.yellow( + `-${CreateArgv.source.alias[0]}`, + )}') to be set`, + ), + ) + process.exit(1) + } else { + if (!fs.existsSync(sourceDirectory)) { + outro( + chalk.red( + `Input directory to copy/symlink 'content' from not found ('${chalk.yellow( + sourceDirectory, + )}', invalid argument "${chalk.yellow(`-${CreateArgv.source.alias[0]}`)})`, + ), + ) + process.exit(1) + } else if (!fs.lstatSync(sourceDirectory).isDirectory()) { + outro( + chalk.red( + `Source directory to copy/symlink 'content' from is not a directory (found file at '${chalk.yellow( + sourceDirectory, + )}', invalid argument ${chalk.yellow(`-${CreateArgv.source.alias[0]}`)}")`, + ), + ) + process.exit(1) + } + } + } + } + + // Use cli process if cmd args werent provided + if (!setupStrategy) { + setupStrategy = exitIfCancel( + await select({ + message: `Choose how to initialize the content in \`${contentFolder}\``, + options: [ + { value: "new", label: "Empty Quartz" }, + { value: "copy", label: "Copy an existing folder", hint: "overwrites `content`" }, + { + value: "symlink", + label: "Symlink an existing folder", + hint: "don't select this unless you know what you are doing!", + }, + ], + }), + ) + } + + async function rmContentFolder() { + const contentStat = await fs.promises.lstat(contentFolder) + if (contentStat.isSymbolicLink()) { + await fs.promises.unlink(contentFolder) + } else { + await rimraf(contentFolder) + } + } + + const gitkeepPath = path.join(contentFolder, ".gitkeep") + if (fs.existsSync(gitkeepPath)) { + await fs.promises.unlink(gitkeepPath) + } + if (setupStrategy === "copy" || setupStrategy === "symlink") { + let originalFolder = sourceDirectory + + // If input directory was not passed, use cli + if (!sourceDirectory) { + originalFolder = escapePath( + exitIfCancel( + await text({ + message: "Enter the full path to existing content folder", + placeholder: + "On most terminal emulators, you can drag and drop a folder into the window and it will paste the full path", + validate(fp) { + const fullPath = escapePath(fp) + if (!fs.existsSync(fullPath)) { + return "The given path doesn't exist" + } else if (!fs.lstatSync(fullPath).isDirectory()) { + return "The given path is not a folder" + } + }, + }), + ), + ) + } + + await rmContentFolder() + if (setupStrategy === "copy") { + await fs.promises.cp(originalFolder, contentFolder, { + recursive: true, + preserveTimestamps: true, + }) + } else if (setupStrategy === "symlink") { + await fs.promises.symlink(originalFolder, contentFolder, "dir") + } + } else if (setupStrategy === "new") { + await fs.promises.writeFile( + path.join(contentFolder, "index.md"), + `--- +title: Welcome to Quartz +--- + +This is a blank Quartz installation. +See the [documentation](https://quartz.jzhao.xyz) for how to get started. +`, + ) + } + + // Use cli process if cmd args werent provided + if (!linkResolutionStrategy) { + // get a preferred link resolution strategy + linkResolutionStrategy = exitIfCancel( + await select({ + message: `Choose how Quartz should resolve links in your content. This should match Obsidian's link format. You can change this later in \`quartz.config.ts\`.`, + options: [ + { + value: "shortest", + label: "Treat links as shortest path", + hint: "(default)", + }, + { + value: "absolute", + label: "Treat links as absolute path", + }, + { + value: "relative", + label: "Treat links as relative paths", + }, + ], + }), + ) + } + + // now, do config changes + const configFilePath = path.join(cwd, "quartz.config.ts") + let configContent = await fs.promises.readFile(configFilePath, { encoding: "utf-8" }) + configContent = configContent.replace( + /markdownLinkResolution: '(.+)'/, + `markdownLinkResolution: '${linkResolutionStrategy}'`, + ) + await fs.promises.writeFile(configFilePath, configContent) + + // setup remote + execSync( + `git remote show upstream || git remote add upstream https://github.com/jackyzha0/quartz.git`, + { stdio: "ignore" }, + ) + + outro(`You're all set! Not sure what to do next? Try: + • Customizing Quartz a bit more by editing \`quartz.config.ts\` + • Running \`npx quartz build --serve\` to preview your Quartz locally + • Hosting your Quartz online (see: https://quartz.jzhao.xyz/hosting) +`) +} + +/** + * Handles `npx quartz build` + * @param {*} argv arguments for `build` + */ +export async function handleBuild(argv) { + console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`)) + const ctx = await esbuild.context({ + entryPoints: [fp], + outfile: cacheFile, + bundle: true, + keepNames: true, + minifyWhitespace: true, + minifySyntax: true, + platform: "node", + format: "esm", + jsx: "automatic", + jsxImportSource: "preact", + packages: "external", + metafile: true, + sourcemap: true, + sourcesContent: false, + plugins: [ + sassPlugin({ + type: "css-text", + cssImports: true, + }), + sassPlugin({ + filter: /\.inline\.scss$/, + type: "css", + cssImports: true, + }), + { + name: "inline-script-loader", + setup(build) { + build.onLoad({ filter: /\.inline\.(ts|js)$/ }, async (args) => { + let text = await promises.readFile(args.path, "utf8") + + // remove default exports that we manually inserted + text = text.replace("export default", "") + text = text.replace("export", "") + + const sourcefile = path.relative(path.resolve("."), args.path) + const resolveDir = path.dirname(sourcefile) + const transpiled = await esbuild.build({ + stdin: { + contents: text, + loader: "ts", + resolveDir, + sourcefile, + }, + write: false, + bundle: true, + minify: true, + platform: "browser", + format: "esm", + }) + const rawMod = transpiled.outputFiles[0].text + return { + contents: rawMod, + loader: "text", + } + }) + }, + }, + ], + }) + + const buildMutex = new Mutex() + let lastBuildMs = 0 + let cleanupBuild = null + const build = async (clientRefresh) => { + const buildStart = new Date().getTime() + lastBuildMs = buildStart + const release = await buildMutex.acquire() + if (lastBuildMs > buildStart) { + release() + return + } + + if (cleanupBuild) { + console.log(chalk.yellow("Detected a source code change, doing a hard rebuild...")) + await cleanupBuild() + } + + const result = await ctx.rebuild().catch((err) => { + console.error(`${chalk.red("Couldn't parse Quartz configuration:")} ${fp}`) + console.log(`Reason: ${chalk.grey(err)}`) + process.exit(1) + }) + release() + + if (argv.bundleInfo) { + const outputFileName = "quartz/.quartz-cache/transpiled-build.mjs" + const meta = result.metafile.outputs[outputFileName] + console.log( + `Successfully transpiled ${Object.keys(meta.inputs).length} files (${prettyBytes( + meta.bytes, + )})`, + ) + console.log(await esbuild.analyzeMetafile(result.metafile, { color: true })) + } + + // bypass module cache + // https://github.com/nodejs/modules/issues/307 + const { default: buildQuartz } = await import(`../../${cacheFile}?update=${randomUUID()}`) + // ^ this import is relative, so base "cacheFile" path can't be used + + cleanupBuild = await buildQuartz(argv, buildMutex, clientRefresh) + clientRefresh() + } + + if (argv.serve) { + const connections = [] + const clientRefresh = () => connections.forEach((conn) => conn.send("rebuild")) + + if (argv.baseDir !== "" && !argv.baseDir.startsWith("/")) { + argv.baseDir = "/" + argv.baseDir + } + + await build(clientRefresh) + const server = http.createServer(async (req, res) => { + if (argv.baseDir && !req.url?.startsWith(argv.baseDir)) { + console.log( + chalk.red( + `[404] ${req.url} (warning: link outside of site, this is likely a Quartz bug)`, + ), + ) + res.writeHead(404) + res.end() + return + } + + // strip baseDir prefix + req.url = req.url?.slice(argv.baseDir.length) + + const serve = async () => { + const release = await buildMutex.acquire() + await serveHandler(req, res, { + public: argv.output, + directoryListing: false, + headers: [ + { + source: "**/*.*", + headers: [{ key: "Content-Disposition", value: "inline" }], + }, + { + source: "**/*.webp", + headers: [{ key: "Content-Type", value: "image/webp" }], + }, + // fixes bug where avif images are displayed as text instead of images (future proof) + { + source: "**/*.avif", + headers: [{ key: "Content-Type", value: "image/avif" }], + }, + ], + }) + const status = res.statusCode + const statusString = + status >= 200 && status < 300 ? chalk.green(`[${status}]`) : chalk.red(`[${status}]`) + console.log(statusString + chalk.grey(` ${argv.baseDir}${req.url}`)) + release() + } + + const redirect = (newFp) => { + newFp = argv.baseDir + newFp + res.writeHead(302, { + Location: newFp, + }) + console.log(chalk.yellow("[302]") + chalk.grey(` ${argv.baseDir}${req.url} -> ${newFp}`)) + res.end() + } + + let fp = req.url?.split("?")[0] ?? "/" + + // handle redirects + if (fp.endsWith("/")) { + // /trailing/ + // does /trailing/index.html exist? if so, serve it + const indexFp = path.posix.join(fp, "index.html") + if (fs.existsSync(path.posix.join(argv.output, indexFp))) { + req.url = fp + return serve() + } + + // does /trailing.html exist? if so, redirect to /trailing + let base = fp.slice(0, -1) + if (path.extname(base) === "") { + base += ".html" + } + if (fs.existsSync(path.posix.join(argv.output, base))) { + return redirect(fp.slice(0, -1)) + } + } else { + // /regular + // does /regular.html exist? if so, serve it + let base = fp + if (path.extname(base) === "") { + base += ".html" + } + if (fs.existsSync(path.posix.join(argv.output, base))) { + req.url = fp + return serve() + } + + // does /regular/index.html exist? if so, redirect to /regular/ + let indexFp = path.posix.join(fp, "index.html") + if (fs.existsSync(path.posix.join(argv.output, indexFp))) { + return redirect(fp + "/") + } + } + + return serve() + }) + server.listen(argv.port) + const wss = new WebSocketServer({ port: argv.wsPort }) + wss.on("connection", (ws) => connections.push(ws)) + console.log( + chalk.cyan( + `Started a Quartz server listening at http://localhost:${argv.port}${argv.baseDir}`, + ), + ) + console.log("hint: exit with ctrl+c") + const paths = await globby(["**/*.ts", "**/*.tsx", "**/*.scss", "package.json"]) + chokidar + .watch(paths, { ignoreInitial: true }) + .on("add", () => build(clientRefresh)) + .on("change", () => build(clientRefresh)) + .on("unlink", () => build(clientRefresh)) + } else { + await build(() => {}) + ctx.dispose() + } +} + +/** + * Handles `npx quartz update` + * @param {*} argv arguments for `update` + */ +export async function handleUpdate(argv) { + const contentFolder = path.join(cwd, argv.directory) + console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`)) + console.log("Backing up your content") + execSync( + `git remote show upstream || git remote add upstream https://github.com/jackyzha0/quartz.git`, + ) + await stashContentFolder(contentFolder) + console.log( + "Pulling updates... you may need to resolve some `git` conflicts if you've made changes to components or plugins.", + ) + + try { + gitPull(UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH) + } catch { + console.log(chalk.red("An error occurred above while pulling updates.")) + await popContentFolder(contentFolder) + return + } + + await popContentFolder(contentFolder) + console.log("Ensuring dependencies are up to date") + + /* + 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) { + console.log(chalk.green("Done!")) + } else { + console.log(chalk.red("An error occurred above while installing dependencies.")) + } +} + +/** + * Handles `npx quartz restore` + * @param {*} argv arguments for `restore` + */ +export async function handleRestore(argv) { + const contentFolder = path.join(cwd, argv.directory) + await popContentFolder(contentFolder) +} + +/** + * Handles `npx quartz sync` + * @param {*} argv arguments for `sync` + */ +export async function handleSync(argv) { + const contentFolder = path.join(cwd, argv.directory) + console.log(chalk.bgGreen.black(`\n Quartz v${version} \n`)) + console.log("Backing up your content") + + if (argv.commit) { + const contentStat = await fs.promises.lstat(contentFolder) + if (contentStat.isSymbolicLink()) { + const linkTarg = await fs.promises.readlink(contentFolder) + console.log(chalk.yellow("Detected symlink, trying to dereference before committing")) + + // stash symlink file + await stashContentFolder(contentFolder) + + // follow symlink and copy content + await fs.promises.cp(linkTarg, contentFolder, { + recursive: true, + preserveTimestamps: true, + }) + } + + const currentTimestamp = new Date().toLocaleString("en-US", { + dateStyle: "medium", + timeStyle: "short", + }) + const commitMessage = argv.message ?? `Quartz sync: ${currentTimestamp}` + spawnSync("git", ["add", "."], { stdio: "inherit" }) + spawnSync("git", ["commit", "-m", commitMessage], { stdio: "inherit" }) + + if (contentStat.isSymbolicLink()) { + // put symlink back + await popContentFolder(contentFolder) + } + } + + await stashContentFolder(contentFolder) + + if (argv.pull) { + console.log( + "Pulling updates from your repository. You may need to resolve some `git` conflicts if you've made changes to components or plugins.", + ) + try { + gitPull(ORIGIN_NAME, QUARTZ_SOURCE_BRANCH) + } catch { + console.log(chalk.red("An error occurred above while pulling updates.")) + await popContentFolder(contentFolder) + return + } + } + + await popContentFolder(contentFolder) + if (argv.push) { + console.log("Pushing your changes") + const res = spawnSync("git", ["push", "-uf", ORIGIN_NAME, QUARTZ_SOURCE_BRANCH], { + stdio: "inherit", + }) + if (res.status !== 0) { + console.log(chalk.red(`An error occurred above while pushing to remote ${ORIGIN_NAME}.`)) + return + } + } + + console.log(chalk.green("Done!")) +} diff --git a/quartz/cli/helpers.js b/quartz/cli/helpers.js new file mode 100755 index 0000000..702a1b7 --- /dev/null +++ b/quartz/cli/helpers.js @@ -0,0 +1,54 @@ +import { isCancel, outro } from "@clack/prompts" +import chalk from "chalk" +import { contentCacheFolder } from "./constants.js" +import { spawnSync } from "child_process" +import fs from "fs" + +export function escapePath(fp) { + return fp + .replace(/\\ /g, " ") // unescape spaces + .replace(/^".*"$/, "$1") + .replace(/^'.*"$/, "$1") + .trim() +} + +export function exitIfCancel(val) { + if (isCancel(val)) { + outro(chalk.red("Exiting")) + process.exit(0) + } else { + return val + } +} + +export async function stashContentFolder(contentFolder) { + await fs.promises.rm(contentCacheFolder, { force: true, recursive: true }) + await fs.promises.cp(contentFolder, contentCacheFolder, { + force: true, + recursive: true, + verbatimSymlinks: true, + preserveTimestamps: true, + }) + await fs.promises.rm(contentFolder, { force: true, recursive: true }) +} + +export function gitPull(origin, branch) { + const flags = ["--no-rebase", "--autostash", "-s", "recursive", "-X", "ours", "--no-edit"] + const out = spawnSync("git", ["pull", ...flags, origin, branch], { stdio: "inherit" }) + if (out.stderr) { + throw new Error(chalk.red(`Error while pulling updates: ${out.stderr}`)) + } else if (out.status !== 0) { + throw new Error(chalk.red("Error while pulling updates")) + } +} + +export async function popContentFolder(contentFolder) { + await fs.promises.rm(contentFolder, { force: true, recursive: true }) + await fs.promises.cp(contentCacheFolder, contentFolder, { + force: true, + recursive: true, + verbatimSymlinks: true, + preserveTimestamps: true, + }) + await fs.promises.rm(contentCacheFolder, { force: true, recursive: true }) +} diff --git a/quartz/components/ArticleTitle.tsx b/quartz/components/ArticleTitle.tsx new file mode 100755 index 0000000..bd58e86 --- /dev/null +++ b/quartz/components/ArticleTitle.tsx @@ -0,0 +1,28 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { classNames } from "../util/lang" + +const ArticleTitle: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => { + + // Hide on specific pages + if ( + fileData.slug === "index" || + fileData.slug === "link" + ) { + return <>; + } + + const title = fileData.frontmatter?.title + if (title) { + return

{title}

+ } else { + return null + } +} + +ArticleTitle.css = ` +.article-title { + margin: 2rem 0 0 0; +} +` + +export default (() => ArticleTitle) satisfies QuartzComponentConstructor \ No newline at end of file diff --git a/quartz/components/Backlinks.tsx b/quartz/components/Backlinks.tsx new file mode 100755 index 0000000..4e2beb3 --- /dev/null +++ b/quartz/components/Backlinks.tsx @@ -0,0 +1,70 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import style from "./styles/backlinks.scss" +import { resolveRelative, simplifySlug } from "../util/path" +import { i18n } from "../i18n" +import { classNames } from "../util/lang" + +interface BacklinksOptions { + hideWhenEmpty: boolean +} + +const defaultOptions: BacklinksOptions = { + hideWhenEmpty: true, +} + +export default ((opts?: Partial) => { + const options: BacklinksOptions = { ...defaultOptions, ...opts } + + const Backlinks: QuartzComponent = ({ + fileData, + allFiles, + displayClass, + cfg, + }: QuartzComponentProps) => { + + // Hide on specific pages + if ( + fileData.slug === "index" || + fileData.slug === "about-me" || + fileData.slug === "about-site" || + fileData.slug === "diary-list" || + fileData.slug === "folder-list" || + fileData.slug === "free-course" || + fileData.slug === "net-journey" || + fileData.slug === "net-manifesto" || + fileData.slug === "support-me" || + fileData.slug === "tags-list" || + fileData.slug === "link" + ) { + return <>; + } + + const slug = simplifySlug(fileData.slug!) + const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug)) + if (options.hideWhenEmpty && backlinkFiles.length == 0) { + return null + } + return ( +
+

{i18n(cfg.locale).components.backlinks.title}

+
    + {backlinkFiles.length > 0 ? ( + backlinkFiles.map((f) => ( +
  • + + {f.frontmatter?.title} + +
  • + )) + ) : ( +
  • {i18n(cfg.locale).components.backlinks.noBacklinksFound}
  • + )} +
+
+ ) + } + + Backlinks.css = style + + return Backlinks +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/Body.tsx b/quartz/components/Body.tsx new file mode 100755 index 0000000..96b6278 --- /dev/null +++ b/quartz/components/Body.tsx @@ -0,0 +1,13 @@ +// @ts-ignore +import clipboardScript from "./scripts/clipboard.inline" +import clipboardStyle from "./styles/clipboard.scss" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" + +const Body: QuartzComponent = ({ children }: QuartzComponentProps) => { + return
{children}
+} + +Body.afterDOMLoaded = clipboardScript +Body.css = clipboardStyle + +export default (() => Body) satisfies QuartzComponentConstructor diff --git a/quartz/components/Breadcrumbs.tsx b/quartz/components/Breadcrumbs.tsx new file mode 100755 index 0000000..9ccfb9a --- /dev/null +++ b/quartz/components/Breadcrumbs.tsx @@ -0,0 +1,139 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import breadcrumbsStyle from "./styles/breadcrumbs.scss" +import { FullSlug, SimpleSlug, joinSegments, resolveRelative } from "../util/path" +import { QuartzPluginData } from "../plugins/vfile" +import { classNames } from "../util/lang" + +type CrumbData = { + displayName: string + path: string +} + +interface BreadcrumbOptions { + /** + * Symbol between crumbs + */ + spacerSymbol: string + /** + * Name of first crumb + */ + rootName: string + /** + * Whether to look up frontmatter title for folders (could cause performance problems with big vaults) + */ + resolveFrontmatterTitle: boolean + /** + * Whether to display breadcrumbs on root `index.md` + */ + hideOnRoot: boolean + /** + * Whether to display the current page in the breadcrumbs. + */ + showCurrentPage: boolean +} + +const defaultOptions: BreadcrumbOptions = { + spacerSymbol: "❯", + rootName: "Home", + resolveFrontmatterTitle: true, + hideOnRoot: true, + showCurrentPage: true, +} + +function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: SimpleSlug): CrumbData { + return { + displayName: displayName.replaceAll("-", " "), + path: resolveRelative(baseSlug, currentSlug), + } +} + +export default ((opts?: Partial) => { + // Merge options with defaults + const options: BreadcrumbOptions = { ...defaultOptions, ...opts } + + // computed index of folder name to its associated file data + let folderIndex: Map | undefined + + const Breadcrumbs: QuartzComponent = ({ + fileData, + allFiles, + displayClass, + }: QuartzComponentProps) => { + // Hide crumbs on root if enabled + if (options.hideOnRoot && fileData.slug === "index") { + return <> + } + + // Format entry for root element + const firstEntry = formatCrumb(options.rootName, fileData.slug!, "/" as SimpleSlug) + const crumbs: CrumbData[] = [firstEntry] + + if (!folderIndex && options.resolveFrontmatterTitle) { + folderIndex = new Map() + // construct the index for the first time + for (const file of allFiles) { + const folderParts = file.slug?.split("/") + if (folderParts?.at(-1) === "index") { + folderIndex.set(folderParts.slice(0, -1).join("/"), file) + } + } + } + + // Split slug into hierarchy/parts + const slugParts = fileData.slug?.split("/") + if (slugParts) { + // is tag breadcrumb? + const isTagPath = slugParts[0] === "tags" + + // full path until current part + let currentPath = "" + + for (let i = 0; i < slugParts.length - 1; i++) { + let curPathSegment = slugParts[i] + + // Try to resolve frontmatter folder title + const currentFile = folderIndex?.get(slugParts.slice(0, i + 1).join("/")) + if (currentFile) { + const title = currentFile.frontmatter!.title + if (title !== "index") { + curPathSegment = title + } + } + + // Add current slug to full path + currentPath = joinSegments(currentPath, slugParts[i]) + const includeTrailingSlash = !isTagPath || i < 1 + + // Format and add current crumb + const crumb = formatCrumb( + curPathSegment, + fileData.slug!, + (currentPath + (includeTrailingSlash ? "/" : "")) as SimpleSlug, + ) + crumbs.push(crumb) + } + + // Add current file to crumb (can directly use frontmatter title) + if (options.showCurrentPage && slugParts.at(-1) !== "index") { + crumbs.push({ + displayName: fileData.frontmatter!.title, + path: "", + }) + } + } + + return ( + + ) + } + Breadcrumbs.css = breadcrumbsStyle + + return Breadcrumbs +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/Comments.tsx b/quartz/components/Comments.tsx new file mode 100755 index 0000000..0bfd82d --- /dev/null +++ b/quartz/components/Comments.tsx @@ -0,0 +1,60 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { classNames } from "../util/lang" +// @ts-ignore +import script from "./scripts/comments.inline" + +type Options = { + provider: "giscus" + options: { + repo: `${string}/${string}` + repoId: string + category: string + categoryId: string + themeUrl?: string + lightTheme?: string + darkTheme?: string + mapping?: "url" | "title" | "og:title" | "specific" | "number" | "pathname" + strict?: boolean + reactionsEnabled?: boolean + inputPosition?: "top" | "bottom" + } +} + +function boolToStringBool(b: boolean): string { + return b ? "1" : "0" +} + +export default ((opts: Options) => { + const Comments: QuartzComponent = ({ displayClass, fileData, cfg }: QuartzComponentProps) => { + // check if comments should be displayed according to frontmatter + const disableComment: boolean = + typeof fileData.frontmatter?.comments !== "undefined" && + (!fileData.frontmatter?.comments || fileData.frontmatter?.comments === "false") + if (disableComment) { + return <> + } + + return ( +
+ ) + } + + Comments.afterDOMLoaded = script + + return Comments +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/ContentMeta.tsx b/quartz/components/ContentMeta.tsx new file mode 100755 index 0000000..4e00626 --- /dev/null +++ b/quartz/components/ContentMeta.tsx @@ -0,0 +1,58 @@ +import { Date, getDate } from "./Date" +import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import readingTime from "reading-time" +import { classNames } from "../util/lang" +import { i18n } from "../i18n" +import { JSX } from "preact" +import style from "./styles/contentMeta.scss" + +interface ContentMetaOptions { + /** + * Whether to display reading time + */ + showReadingTime: boolean + showComma: boolean +} + +const defaultOptions: ContentMetaOptions = { + showReadingTime: true, + showComma: true, +} + +export default ((opts?: Partial) => { + // Merge options with defaults + const options: ContentMetaOptions = { ...defaultOptions, ...opts } + + function ContentMetadata({ cfg, fileData, displayClass }: QuartzComponentProps) { + const text = fileData.text + + if (text) { + const segments: (string | JSX.Element)[] = [] + + // if (fileData.dates) { + // segments.push() + // } + + // Display reading time if enabled + if (options.showReadingTime) { + const { minutes, words: _words } = readingTime(text) + const displayedTime = i18n(cfg.locale).components.contentMeta.readingTime({ + minutes: Math.ceil(minutes), + }) + segments.push({displayedTime}) + } + + return ( +

+ {segments} +

+ ) + } else { + return null + } + } + + ContentMetadata.css = style + + return ContentMetadata +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/Darkmode.tsx b/quartz/components/Darkmode.tsx new file mode 100755 index 0000000..f64aad6 --- /dev/null +++ b/quartz/components/Darkmode.tsx @@ -0,0 +1,50 @@ +// @ts-ignore: this is safe, we don't want to actually make darkmode.inline.ts a module as +// modules are automatically deferred and we don't want that to happen for critical beforeDOMLoads +// see: https://v8.dev/features/modules#defer +import darkmodeScript from "./scripts/darkmode.inline" +import styles from "./styles/darkmode.scss" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { i18n } from "../i18n" +import { classNames } from "../util/lang" + +const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { + return ( + + ) +} + +Darkmode.beforeDOMLoaded = darkmodeScript +Darkmode.css = styles + +export default (() => Darkmode) satisfies QuartzComponentConstructor diff --git a/quartz/components/Date.tsx b/quartz/components/Date.tsx new file mode 100755 index 0000000..0a92cc4 --- /dev/null +++ b/quartz/components/Date.tsx @@ -0,0 +1,31 @@ +import { GlobalConfiguration } from "../cfg" +import { ValidLocale } from "../i18n" +import { QuartzPluginData } from "../plugins/vfile" + +interface Props { + date: Date + locale?: ValidLocale +} + +export type ValidDateType = keyof Required["dates"] + +export function getDate(cfg: GlobalConfiguration, data: QuartzPluginData): Date | undefined { + if (!cfg.defaultDateType) { + throw new Error( + `Field 'defaultDateType' was not set in the configuration object of quartz.config.ts. See https://quartz.jzhao.xyz/configuration#general-configuration for more details.`, + ) + } + return data.dates?.[cfg.defaultDateType] +} + +export function formatDate(d: Date, locale: ValidLocale = "en-US"): string { + return d.toLocaleDateString(locale, { + year: "numeric", + month: "short", + day: "2-digit", + }) +} + +export function Date({ date, locale }: Props) { + return +} diff --git a/quartz/components/DesktopOnly.tsx b/quartz/components/DesktopOnly.tsx new file mode 100755 index 0000000..fe2a27f --- /dev/null +++ b/quartz/components/DesktopOnly.tsx @@ -0,0 +1,18 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" + +export default ((component?: QuartzComponent) => { + if (component) { + const Component = component + const DesktopOnly: QuartzComponent = (props: QuartzComponentProps) => { + return + } + + DesktopOnly.displayName = component.displayName + DesktopOnly.afterDOMLoaded = component?.afterDOMLoaded + DesktopOnly.beforeDOMLoaded = component?.beforeDOMLoaded + DesktopOnly.css = component?.css + return DesktopOnly + } else { + return () => <> + } +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx new file mode 100755 index 0000000..ec7c48e --- /dev/null +++ b/quartz/components/Explorer.tsx @@ -0,0 +1,128 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import explorerStyle from "./styles/explorer.scss" + +// @ts-ignore +import script from "./scripts/explorer.inline" +import { ExplorerNode, FileNode, Options } from "./ExplorerNode" +import { QuartzPluginData } from "../plugins/vfile" +import { classNames } from "../util/lang" +import { i18n } from "../i18n" + +// Options interface defined in `ExplorerNode` to avoid circular dependency +const defaultOptions = { + folderClickBehavior: "collapse", + folderDefaultState: "collapsed", + useSavedState: true, + mapFn: (node) => { + return node + }, + sortFn: (a, b) => { + // Sort order: folders first, then files. Sort folders and files alphabetically + if ((!a.file && !b.file) || (a.file && b.file)) { + // numeric: true: Whether numeric collation should be used, such that "1" < "2" < "10" + // sensitivity: "base": Only strings that differ in base letters compare as unequal. Examples: a ≠ b, a = á, a = A + return a.displayName.localeCompare(b.displayName, undefined, { + numeric: true, + sensitivity: "base", + }) + } + + if (a.file && !b.file) { + return 1 + } else { + return -1 + } + }, + filterFn: (node) => node.name !== "tags", + order: ["filter", "map", "sort"], +} satisfies Options + +export default ((userOpts?: Partial) => { + // Parse config + const opts: Options = { ...defaultOptions, ...userOpts } + + // memoized + let fileTree: FileNode + let jsonTree: string + let lastBuildId: string = "" + + function constructFileTree(allFiles: QuartzPluginData[]) { + // Construct tree from allFiles + fileTree = new FileNode("") + allFiles.forEach((file) => fileTree.add(file)) + + // Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied) + if (opts.order) { + // Order is important, use loop with index instead of order.map() + for (let i = 0; i < opts.order.length; i++) { + const functionName = opts.order[i] + if (functionName === "map") { + fileTree.map(opts.mapFn) + } else if (functionName === "sort") { + fileTree.sort(opts.sortFn) + } else if (functionName === "filter") { + fileTree.filter(opts.filterFn) + } + } + } + + // Get all folders of tree. Initialize with collapsed state + // Stringify to pass json tree as data attribute ([data-tree]) + const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed") + jsonTree = JSON.stringify(folders) + } + + const Explorer: QuartzComponent = ({ + ctx, + cfg, + allFiles, + displayClass, + fileData, + }: QuartzComponentProps) => { + if (ctx.buildId !== lastBuildId) { + lastBuildId = ctx.buildId + constructFileTree(allFiles) + } + + return ( +
+ +
+
    + +
  • +
+
+
+ ) + } + + Explorer.css = explorerStyle + Explorer.afterDOMLoaded = script + return Explorer +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/ExplorerNode.tsx b/quartz/components/ExplorerNode.tsx new file mode 100755 index 0000000..e57d677 --- /dev/null +++ b/quartz/components/ExplorerNode.tsx @@ -0,0 +1,242 @@ +// @ts-ignore +import { QuartzPluginData } from "../plugins/vfile" +import { + joinSegments, + resolveRelative, + clone, + simplifySlug, + SimpleSlug, + FilePath, +} from "../util/path" + +type OrderEntries = "sort" | "filter" | "map" + +export interface Options { + title?: string + folderDefaultState: "collapsed" | "open" + folderClickBehavior: "collapse" | "link" + useSavedState: boolean + sortFn: (a: FileNode, b: FileNode) => number + filterFn: (node: FileNode) => boolean + mapFn: (node: FileNode) => void + order: OrderEntries[] +} + +type DataWrapper = { + file: QuartzPluginData + path: string[] +} + +export type FolderState = { + path: string + collapsed: boolean +} + +function getPathSegment(fp: FilePath | undefined, idx: number): string | undefined { + if (!fp) { + return undefined + } + + return fp.split("/").at(idx) +} + +// Structure to add all files into a tree +export class FileNode { + children: Array + name: string // this is the slug segment + displayName: string + file: QuartzPluginData | null + depth: number + + constructor(slugSegment: string, displayName?: string, file?: QuartzPluginData, depth?: number) { + this.children = [] + this.name = slugSegment + this.displayName = displayName ?? file?.frontmatter?.title ?? slugSegment + this.file = file ? clone(file) : null + this.depth = depth ?? 0 + } + + private insert(fileData: DataWrapper) { + if (fileData.path.length === 0) { + return + } + + const nextSegment = fileData.path[0] + + // base case, insert here + if (fileData.path.length === 1) { + if (nextSegment === "") { + // index case (we are the root and we just found index.md), set our data appropriately + const title = fileData.file.frontmatter?.title + if (title && title !== "index") { + this.displayName = title + } + } else { + // direct child + this.children.push(new FileNode(nextSegment, undefined, fileData.file, this.depth + 1)) + } + + return + } + + // find the right child to insert into + fileData.path = fileData.path.splice(1) + const child = this.children.find((c) => c.name === nextSegment) + if (child) { + child.insert(fileData) + return + } + + const newChild = new FileNode( + nextSegment, + getPathSegment(fileData.file.relativePath, this.depth), + undefined, + this.depth + 1, + ) + newChild.insert(fileData) + this.children.push(newChild) + } + + // Add new file to tree + add(file: QuartzPluginData) { + this.insert({ file: file, path: simplifySlug(file.slug!).split("/") }) + } + + /** + * Filter FileNode tree. Behaves similar to `Array.prototype.filter()`, but modifies tree in place + * @param filterFn function to filter tree with + */ + filter(filterFn: (node: FileNode) => boolean) { + this.children = this.children.filter(filterFn) + this.children.forEach((child) => child.filter(filterFn)) + } + + /** + * Filter FileNode tree. Behaves similar to `Array.prototype.map()`, but modifies tree in place + * @param mapFn function to use for mapping over tree + */ + map(mapFn: (node: FileNode) => void) { + mapFn(this) + this.children.forEach((child) => child.map(mapFn)) + } + + /** + * Get folder representation with state of tree. + * Intended to only be called on root node before changes to the tree are made + * @param collapsed default state of folders (collapsed by default or not) + * @returns array containing folder state for tree + */ + getFolderPaths(collapsed: boolean): FolderState[] { + const folderPaths: FolderState[] = [] + + const traverse = (node: FileNode, currentPath: string) => { + if (!node.file) { + const folderPath = joinSegments(currentPath, node.name) + if (folderPath !== "") { + folderPaths.push({ path: folderPath, collapsed }) + } + + node.children.forEach((child) => traverse(child, folderPath)) + } + } + + traverse(this, "") + return folderPaths + } + + // Sort order: folders first, then files. Sort folders and files alphabetically + /** + * Sorts tree according to sort/compare function + * @param sortFn compare function used for `.sort()`, also used recursively for children + */ + sort(sortFn: (a: FileNode, b: FileNode) => number) { + this.children = this.children.sort(sortFn) + this.children.forEach((e) => e.sort(sortFn)) + } +} + +type ExplorerNodeProps = { + node: FileNode + opts: Options + fileData: QuartzPluginData + fullPath?: string +} + +export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodeProps) { + // Get options + const folderBehavior = opts.folderClickBehavior + const isDefaultOpen = opts.folderDefaultState === "open" + + // Calculate current folderPath + const folderPath = node.name !== "" ? joinSegments(fullPath ?? "", node.name) : "" + const href = resolveRelative(fileData.slug!, folderPath as SimpleSlug) + "/" + + return ( + <> + {node.file ? ( + // Single file node +
  • + + {node.displayName} + +
  • + ) : ( +
  • + {node.name !== "" && ( + // Node with entire folder + // Render svg button + folder name, then children + + + )} + {/* Recursively render children of folder */} +
    +
      + {node.children.map((childNode, i) => ( + + ))} +
    +
    +
  • + )} + + ) +} diff --git a/quartz/components/Footer.tsx b/quartz/components/Footer.tsx new file mode 100755 index 0000000..bbcbf51 --- /dev/null +++ b/quartz/components/Footer.tsx @@ -0,0 +1,33 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import style from "./styles/footer.scss" +import { version } from "../../package.json" +import { i18n } from "../i18n" + +interface Options { + links: Record +} + +export default ((opts?: Options) => { + const Footer: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { + const year = new Date().getFullYear() + const links = opts?.links ?? [] + return ( +
    +

    +{/* {i18n(cfg.locale).components.footer.createdWith}{" "} + Quartz v{version} © {year} */} +

    +
      + {Object.entries(links).map(([text, link]) => ( +
    • + {text} +
    • + ))} +
    +
    + ) + } + + Footer.css = style + return Footer +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/Graph.tsx b/quartz/components/Graph.tsx new file mode 100755 index 0000000..524d04c --- /dev/null +++ b/quartz/components/Graph.tsx @@ -0,0 +1,106 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +// @ts-ignore +import script from "./scripts/graph.inline" +import style from "./styles/graph.scss" +import { i18n } from "../i18n" +import { classNames } from "../util/lang" + +export interface D3Config { + drag: boolean + zoom: boolean + depth: number + scale: number + repelForce: number + centerForce: number + linkDistance: number + fontSize: number + opacityScale: number + removeTags: string[] + showTags: boolean + focusOnHover?: boolean +} + +interface GraphOptions { + localGraph: Partial | undefined + globalGraph: Partial | undefined +} + +const defaultOptions: GraphOptions = { + localGraph: { + drag: true, + zoom: true, + depth: 1, + scale: 1.1, + repelForce: 0.5, + centerForce: 0.3, + linkDistance: 30, + fontSize: 0.6, + opacityScale: 1, + showTags: true, + removeTags: [], + focusOnHover: false, + }, + globalGraph: { + drag: true, + zoom: true, + depth: -1, + scale: 0.9, + repelForce: 0.5, + centerForce: 0.3, + linkDistance: 30, + fontSize: 0.6, + opacityScale: 1, + showTags: true, + removeTags: [], + focusOnHover: true, + }, +} + +export default ((opts?: GraphOptions) => { + const Graph: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { + const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph } + const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph } + return ( +
    + {/*

    {i18n(cfg.locale).components.graph.title}

    */} +
    +
    + +
    +
    +
    +
    +
    + ) + } + + Graph.css = style + Graph.afterDOMLoaded = script + + return Graph +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/GraphMobile.tsx b/quartz/components/GraphMobile.tsx new file mode 100755 index 0000000..524d04c --- /dev/null +++ b/quartz/components/GraphMobile.tsx @@ -0,0 +1,106 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +// @ts-ignore +import script from "./scripts/graph.inline" +import style from "./styles/graph.scss" +import { i18n } from "../i18n" +import { classNames } from "../util/lang" + +export interface D3Config { + drag: boolean + zoom: boolean + depth: number + scale: number + repelForce: number + centerForce: number + linkDistance: number + fontSize: number + opacityScale: number + removeTags: string[] + showTags: boolean + focusOnHover?: boolean +} + +interface GraphOptions { + localGraph: Partial | undefined + globalGraph: Partial | undefined +} + +const defaultOptions: GraphOptions = { + localGraph: { + drag: true, + zoom: true, + depth: 1, + scale: 1.1, + repelForce: 0.5, + centerForce: 0.3, + linkDistance: 30, + fontSize: 0.6, + opacityScale: 1, + showTags: true, + removeTags: [], + focusOnHover: false, + }, + globalGraph: { + drag: true, + zoom: true, + depth: -1, + scale: 0.9, + repelForce: 0.5, + centerForce: 0.3, + linkDistance: 30, + fontSize: 0.6, + opacityScale: 1, + showTags: true, + removeTags: [], + focusOnHover: true, + }, +} + +export default ((opts?: GraphOptions) => { + const Graph: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { + const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph } + const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph } + return ( +
    + {/*

    {i18n(cfg.locale).components.graph.title}

    */} +
    +
    + +
    +
    +
    +
    +
    + ) + } + + Graph.css = style + Graph.afterDOMLoaded = script + + return Graph +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/Head.tsx b/quartz/components/Head.tsx new file mode 100755 index 0000000..3a4db10 --- /dev/null +++ b/quartz/components/Head.tsx @@ -0,0 +1,209 @@ +import { i18n } from "../i18n" +import { FullSlug, joinSegments, pathToRoot } from "../util/path" +import { CSSResourceToStyleElement, JSResourceToScriptElement } from "../util/resources" +import { googleFontHref } from "../util/theme" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import satori, { SatoriOptions } from "satori" +import fs from "fs" +import sharp from "sharp" +import { ImageOptions, SocialImageOptions, getSatoriFont, defaultImage } from "../util/og" +import { unescapeHTML } from "../util/escape" + +/** + * Generates social image (OG/twitter standard) and saves it as `.webp` inside the public folder + * @param opts options for generating image + */ +async function generateSocialImage( + { cfg, description, fileName, fontsPromise, title, fileData }: ImageOptions, + userOpts: SocialImageOptions, + imageDir: string, +) { + const fonts = await fontsPromise + const { width, height } = userOpts + + // JSX that will be used to generate satori svg + const imageComponent = userOpts.imageStructure(cfg, userOpts, title, description, fonts, fileData) + + const svg = await satori(imageComponent, { width, height, fonts }) + + // Convert svg directly to webp (with additional compression) + const compressed = await sharp(Buffer.from(svg)).webp({ quality: 40 }).toBuffer() + + // Write to file system + const filePath = joinSegments(imageDir, `${fileName}.${extension}`) + fs.writeFileSync(filePath, compressed) +} + +const extension = "webp" + +const defaultOptions: SocialImageOptions = { + colorScheme: "lightMode", + width: 1200, + height: 630, + imageStructure: defaultImage, + excludeRoot: false, +} + +export default (() => { + let fontsPromise: Promise + + let fullOptions: SocialImageOptions + const Head: QuartzComponent = ({ + cfg, + fileData, + externalResources, + ctx, + }: QuartzComponentProps) => { + // Initialize options if not set + if (!fullOptions) { + if (typeof cfg.generateSocialImages !== "boolean") { + fullOptions = { ...defaultOptions, ...cfg.generateSocialImages } + } else { + fullOptions = defaultOptions + } + } + + // Memoize google fonts + if (!fontsPromise && cfg.generateSocialImages) { + fontsPromise = getSatoriFont(cfg.theme.typography.header, cfg.theme.typography.body) + } + + const slug = fileData.filePath + // since "/" is not a valid character in file names, replace with "-" + const fileName = slug?.replaceAll("/", "-") + + // Get file description (priority: frontmatter > fileData > default) + const fdDescription = + fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description + const titleSuffix = cfg.pageTitleSuffix ?? "" + const title = + (fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title) + titleSuffix + let description = "" + if (fdDescription) { + description = unescapeHTML(fdDescription) + } + + if (fileData.frontmatter?.socialDescription) { + description = fileData.frontmatter?.socialDescription as string + } else if (fileData.frontmatter?.description) { + description = fileData.frontmatter?.description + } + + const fileDir = joinSegments(ctx.argv.output, "static", "social-images") + if (cfg.generateSocialImages) { + // Generate folders for social images (if they dont exist yet) + if (!fs.existsSync(fileDir)) { + fs.mkdirSync(fileDir, { recursive: true }) + } + + if (fileName) { + // Generate social image (happens async) + generateSocialImage( + { + title, + description, + fileName, + fileDir, + fileExt: extension, + fontsPromise, + cfg, + fileData, + }, + fullOptions, + fileDir, + ) + } + } + + const { css, js } = externalResources + + const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`) + const path = url.pathname as FullSlug + const baseDir = fileData.slug === "404" ? path : pathToRoot(fileData.slug!) + + const iconPath = joinSegments(baseDir, "static/icon.png") + + const ogImageDefaultPath = `https://${cfg.baseUrl}/static/og-image.png` + // "static/social-images/slug-filename.md.webp" + const ogImageGeneratedPath = `https://${cfg.baseUrl}/${fileDir.replace( + `${ctx.argv.output}/`, + "", + )}/${fileName}.${extension}` + + // Use default og image if filePath doesnt exist (for autogenerated paths with no .md file) + const useDefaultOgImage = fileName === undefined || !cfg.generateSocialImages + + // Path to og/social image (priority: frontmatter > generated image (if enabled) > default image) + let ogImagePath = useDefaultOgImage ? ogImageDefaultPath : ogImageGeneratedPath + + // TODO: could be improved to support external images in the future + // Aliases for image and cover handled in `frontmatter.ts` + const frontmatterImgUrl = fileData.frontmatter?.socialImage + + // Override with default og image if config option is set + if (fileData.slug === "index") { + ogImagePath = ogImageDefaultPath + } + + // Override with frontmatter url if existing + if (frontmatterImgUrl) { + ogImagePath = `https://${cfg.baseUrl}/static/${frontmatterImgUrl}` + } + + // Url of current page + const socialUrl = + fileData.slug === "404" ? url.toString() : joinSegments(url.toString(), fileData.slug!) + + return ( + + {title} + + {cfg.theme.cdnCaching && cfg.theme.fontOrigin === "googleFonts" && ( + <> + + + + + )} + + + {/* OG/Twitter meta tags */} + + + + + + + + + + {/* Dont set width and height if unknown (when using custom frontmatter image) */} + {!frontmatterImgUrl && ( + <> + + + + )} + + {cfg.baseUrl && ( + <> + + + + + + + )} + + + + {css.map((resource) => CSSResourceToStyleElement(resource, true))} + {js + .filter((resource) => resource.loadTime === "beforeDOMReady") + .map((res) => JSResourceToScriptElement(res, true))} + + ) + } + + return Head +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/Header.tsx b/quartz/components/Header.tsx new file mode 100755 index 0000000..eba17ae --- /dev/null +++ b/quartz/components/Header.tsx @@ -0,0 +1,22 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" + +const Header: QuartzComponent = ({ children }: QuartzComponentProps) => { + return children.length > 0 ?
    {children}
    : null +} + +Header.css = ` +header { + display: flex; + flex-direction: row; + align-items: center; + margin: 2rem 0; + gap: 1.5rem; +} + +header h1 { + margin: 0; + flex: auto; +} +` + +export default (() => Header) satisfies QuartzComponentConstructor diff --git a/quartz/components/MobileOnly.tsx b/quartz/components/MobileOnly.tsx new file mode 100755 index 0000000..7d2108d --- /dev/null +++ b/quartz/components/MobileOnly.tsx @@ -0,0 +1,18 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" + +export default ((component?: QuartzComponent) => { + if (component) { + const Component = component + const MobileOnly: QuartzComponent = (props: QuartzComponentProps) => { + return + } + + MobileOnly.displayName = component.displayName + MobileOnly.afterDOMLoaded = component?.afterDOMLoaded + MobileOnly.beforeDOMLoaded = component?.beforeDOMLoaded + MobileOnly.css = component?.css + return MobileOnly + } else { + return () => <> + } +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/PageList.tsx b/quartz/components/PageList.tsx new file mode 100755 index 0000000..2b09b05 --- /dev/null +++ b/quartz/components/PageList.tsx @@ -0,0 +1,87 @@ +import { FullSlug, resolveRelative } from "../util/path" +import { QuartzPluginData } from "../plugins/vfile" +import { Date, getDate } from "./Date" +import { QuartzComponent, QuartzComponentProps } from "./types" +import { GlobalConfiguration } from "../cfg" + +export type SortFn = (f1: QuartzPluginData, f2: QuartzPluginData) => number + +export function byDateAndAlphabetical(cfg: GlobalConfiguration): SortFn { + return (f1, f2) => { + if (f1.dates && f2.dates) { + // sort descending + return getDate(cfg, f2)!.getTime() - getDate(cfg, f1)!.getTime() + } else if (f1.dates && !f2.dates) { + // prioritize files with dates + return -1 + } else if (!f1.dates && f2.dates) { + return 1 + } + + // otherwise, sort lexographically by title + const f1Title = f1.frontmatter?.title.toLowerCase() ?? "" + const f2Title = f2.frontmatter?.title.toLowerCase() ?? "" + return f1Title.localeCompare(f2Title) + } +} + +type Props = { + limit?: number + sort?: SortFn +} & QuartzComponentProps + +export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit, sort }: Props) => { + const sorter = sort ?? byDateAndAlphabetical(cfg) + let list = allFiles.sort(sorter) + if (limit) { + list = list.slice(0, limit) + } + + return ( +
      + {list.map((page) => { + const title = page.frontmatter?.title + const tags = page.frontmatter?.tags ?? [] + + return ( +
    • +
      +

      + {/* {page.dates && } */} +

      + + {/* */} +
      +
    • + ) + })} +
    + ) +} + +PageList.css = ` +.section h3 { + margin: 0; +} + +.section > .tags { + margin: 0; +} +` diff --git a/quartz/components/PageTitle.tsx b/quartz/components/PageTitle.tsx new file mode 100755 index 0000000..50924bb --- /dev/null +++ b/quartz/components/PageTitle.tsx @@ -0,0 +1,23 @@ +import { pathToRoot } from "../util/path" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { classNames } from "../util/lang" +import { i18n } from "../i18n" + +const PageTitle: QuartzComponent = ({ fileData, cfg, displayClass }: QuartzComponentProps) => { + const title = cfg?.pageTitle ?? i18n(cfg.locale).propertyDefaults.title + const baseDir = pathToRoot(fileData.slug!) + return ( +

    + {title} +

    + ) +} + +PageTitle.css = ` +.page-title { + font-size: 1.50rem; + margin: 0; +} +` + +export default (() => PageTitle) satisfies QuartzComponentConstructor diff --git a/quartz/components/RecentNotes.tsx b/quartz/components/RecentNotes.tsx new file mode 100755 index 0000000..2c32fea --- /dev/null +++ b/quartz/components/RecentNotes.tsx @@ -0,0 +1,93 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { FullSlug, SimpleSlug, resolveRelative } from "../util/path" +import { QuartzPluginData } from "../plugins/vfile" +import { byDateAndAlphabetical } from "./PageList" +import style from "./styles/recentNotes.scss" +import { Date, getDate } from "./Date" +import { GlobalConfiguration } from "../cfg" +import { i18n } from "../i18n" +import { classNames } from "../util/lang" + +interface Options { + title?: string + limit: number + linkToMore: SimpleSlug | false + showTags: boolean + filter: (f: QuartzPluginData) => boolean + sort: (f1: QuartzPluginData, f2: QuartzPluginData) => number +} + +const defaultOptions = (cfg: GlobalConfiguration): Options => ({ + limit: 3, + linkToMore: false, + showTags: true, + filter: () => true, + sort: byDateAndAlphabetical(cfg), +}) + +export default ((userOpts?: Partial) => { + const RecentNotes: QuartzComponent = ({ + allFiles, + fileData, + displayClass, + cfg, + }: QuartzComponentProps) => { + const opts = { ...defaultOptions(cfg), ...userOpts } + const pages = allFiles.filter(opts.filter).sort(opts.sort) + const remaining = Math.max(0, pages.length - opts.limit) + return ( +
    +

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

    +
      + {pages.slice(0, opts.limit).map((page) => { + const title = page.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title + const tags = page.frontmatter?.tags ?? [] + + return ( +
    • +
      + + {page.dates && ( +

      + +

      + )} + {opts.showTags && ( + + )} +
      +
    • + ) + })} +
    + {opts.linkToMore && remaining > 0 && ( +

    + + {i18n(cfg.locale).components.recentNotes.seeRemainingMore({ remaining })} + +

    + )} +
    + ) + } + + RecentNotes.css = style + return RecentNotes +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/Search.tsx b/quartz/components/Search.tsx new file mode 100755 index 0000000..2510b0f --- /dev/null +++ b/quartz/components/Search.tsx @@ -0,0 +1,53 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import style from "./styles/search.scss" +// @ts-ignore +import script from "./scripts/search.inline" +import { classNames } from "../util/lang" +import { i18n } from "../i18n" + +export interface SearchOptions { + enablePreview: boolean +} + +const defaultOptions: SearchOptions = { + enablePreview: true, +} + +export default ((userOpts?: Partial) => { + const Search: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { + const opts = { ...defaultOptions, ...userOpts } + const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder + return ( +
    + +
    +
    + +
    +
    +
    +
    + ) + } + + Search.afterDOMLoaded = script + Search.css = style + + return Search +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/Separator.tsx b/quartz/components/Separator.tsx new file mode 100755 index 0000000..c111a46 --- /dev/null +++ b/quartz/components/Separator.tsx @@ -0,0 +1,22 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import style from "./styles/footer.scss" +import { version } from "../../package.json" + +interface Options { + links: Record +} + +export default ((opts?: Options) => { + const Separator: QuartzComponent = ({ displayClass }: QuartzComponentProps) => { + const year = new Date().getFullYear() + const links = opts?.links ?? [] + return ( +
    +
    +
    + ) + } + + Separator.css = style + return Separator +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/Spacer.tsx b/quartz/components/Spacer.tsx new file mode 100755 index 0000000..5288752 --- /dev/null +++ b/quartz/components/Spacer.tsx @@ -0,0 +1,8 @@ +import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { classNames } from "../util/lang" + +function Spacer({ displayClass }: QuartzComponentProps) { + return
    +} + +export default (() => Spacer) satisfies QuartzComponentConstructor diff --git a/quartz/components/TableOfContents.tsx b/quartz/components/TableOfContents.tsx new file mode 100755 index 0000000..ec457cf --- /dev/null +++ b/quartz/components/TableOfContents.tsx @@ -0,0 +1,95 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import legacyStyle from "./styles/legacyToc.scss" +import modernStyle from "./styles/toc.scss" +import { classNames } from "../util/lang" + +// @ts-ignore +import script from "./scripts/toc.inline" +import { i18n } from "../i18n" + +interface Options { + layout: "modern" | "legacy" +} + +const defaultOptions: Options = { + layout: "modern", +} + +const TableOfContents: QuartzComponent = ({ + fileData, + displayClass, + cfg, +}: QuartzComponentProps) => { + if (!fileData.toc) { + return null + } + + return ( +
    + +
    + +
    +
    + ) +} +TableOfContents.css = modernStyle +TableOfContents.afterDOMLoaded = script + +const LegacyTableOfContents: QuartzComponent = ({ fileData, cfg }: QuartzComponentProps) => { + if (!fileData.toc) { + return null + } + return ( +
    + +

    {i18n(cfg.locale).components.tableOfContents.title}

    +
    + +
    + ) +} +LegacyTableOfContents.css = legacyStyle + +export default ((opts?: Partial) => { + const layout = opts?.layout ?? defaultOptions.layout + return layout === "modern" ? TableOfContents : LegacyTableOfContents +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/TagList.tsx b/quartz/components/TagList.tsx new file mode 100755 index 0000000..4a89fbd --- /dev/null +++ b/quartz/components/TagList.tsx @@ -0,0 +1,57 @@ +import { pathToRoot, slugTag } from "../util/path" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { classNames } from "../util/lang" + +const TagList: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => { + const tags = fileData.frontmatter?.tags + const baseDir = pathToRoot(fileData.slug!) + if (tags && tags.length > 0) { + return ( +
      + {tags.map((tag) => { + const linkDest = baseDir + `/tags/${slugTag(tag)}` + return ( +
    • + + {tag} + +
    • + ) + })} +
    + ) + } else { + return null + } +} + +TagList.css = ` +.tags { + list-style: none; + display: flex; + padding-left: 0; + gap: 0.4rem; + margin: 1rem 0; + flex-wrap: wrap; +} + +.section-li > .section > .tags { + justify-content: flex-end; +} + +.tags > li { + display: inline-block; + white-space: nowrap; + margin: 0; + overflow-wrap: normal; +} + +a.internal.tag-link { + border-radius: 8px; + background-color: var(--highlight); + padding: 0.2rem 0.4rem; + margin: 0 0.1rem; +} +` + +export default (() => TagList) satisfies QuartzComponentConstructor diff --git a/quartz/components/index.ts b/quartz/components/index.ts new file mode 100755 index 0000000..84de890 --- /dev/null +++ b/quartz/components/index.ts @@ -0,0 +1,51 @@ +import Content from "./pages/Content" +import TagContent from "./pages/TagContent" +import FolderContent from "./pages/FolderContent" +import NotFound from "./pages/404" +import ArticleTitle from "./ArticleTitle" +import Darkmode from "./Darkmode" +import Head from "./Head" +import PageTitle from "./PageTitle" +import ContentMeta from "./ContentMeta" +import Spacer from "./Spacer" +import TableOfContents from "./TableOfContents" +import Explorer from "./Explorer" +import TagList from "./TagList" +import Graph from "./Graph" +import GraphMobile from "./GraphMobile" +import Backlinks from "./Backlinks" +import Search from "./Search" +import Footer from "./Footer" +import DesktopOnly from "./DesktopOnly" +import MobileOnly from "./MobileOnly" +import RecentNotes from "./RecentNotes" +import Breadcrumbs from "./Breadcrumbs" +import Comments from "./Comments" +import Separator from "./Separator" + +export { + ArticleTitle, + Content, + TagContent, + FolderContent, + Darkmode, + Head, + PageTitle, + ContentMeta, + Spacer, + TableOfContents, + Explorer, + TagList, + Graph, + GraphMobile, + Backlinks, + Search, + Footer, + DesktopOnly, + MobileOnly, + RecentNotes, + NotFound, + Breadcrumbs, + Comments, + Separator, +} diff --git a/quartz/components/pages/404.tsx b/quartz/components/pages/404.tsx new file mode 100755 index 0000000..63da2c8 --- /dev/null +++ b/quartz/components/pages/404.tsx @@ -0,0 +1,18 @@ +import { i18n } from "../../i18n" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" + +const NotFound: QuartzComponent = ({ cfg }: QuartzComponentProps) => { + // If baseUrl contains a pathname after the domain, use this as the home link + const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`) + const baseDir = url.pathname + + return ( + + ) +} + +export default (() => NotFound) satisfies QuartzComponentConstructor diff --git a/quartz/components/pages/Content.tsx b/quartz/components/pages/Content.tsx new file mode 100755 index 0000000..8222d78 --- /dev/null +++ b/quartz/components/pages/Content.tsx @@ -0,0 +1,11 @@ +import { htmlToJsx } from "../../util/jsx" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" + +const Content: QuartzComponent = ({ fileData, tree }: QuartzComponentProps) => { + const content = htmlToJsx(fileData.filePath!, tree) + const classes: string[] = fileData.frontmatter?.cssclasses ?? [] + const classString = ["popover-hint", ...classes].join(" ") + return
    {content}
    +} + +export default (() => Content) satisfies QuartzComponentConstructor diff --git a/quartz/components/pages/FolderContent.tsx b/quartz/components/pages/FolderContent.tsx new file mode 100755 index 0000000..593073b --- /dev/null +++ b/quartz/components/pages/FolderContent.tsx @@ -0,0 +1,107 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" +import path from "path" + +import style from "../styles/listPage.scss" +import { byDateAndAlphabetical, PageList, SortFn } from "../PageList" +import { stripSlashes, simplifySlug, joinSegments, FullSlug } from "../../util/path" +import { Root } from "hast" +import { htmlToJsx } from "../../util/jsx" +import { i18n } from "../../i18n" +import { QuartzPluginData } from "../../plugins/vfile" + +interface FolderContentOptions { + /** + * Whether to display number of folders + */ + showFolderCount: boolean + showSubfolders: boolean + sort?: SortFn +} + +const defaultOptions: FolderContentOptions = { + showFolderCount: true, + showSubfolders: true, +} + +export default ((opts?: Partial) => { + const options: FolderContentOptions = { ...defaultOptions, ...opts } + + const FolderContent: QuartzComponent = (props: QuartzComponentProps) => { + const { tree, fileData, allFiles, cfg } = props + const folderSlug = stripSlashes(simplifySlug(fileData.slug!)) + const folderParts = folderSlug.split(path.posix.sep) + + const allPagesInFolder: QuartzPluginData[] = [] + const allPagesInSubfolders: Map = new Map() + + allFiles.forEach((file) => { + const fileSlug = stripSlashes(simplifySlug(file.slug!)) + const prefixed = fileSlug.startsWith(folderSlug) && fileSlug !== folderSlug + const fileParts = fileSlug.split(path.posix.sep) + const isDirectChild = fileParts.length === folderParts.length + 1 + + if (!prefixed) { + return + } + + if (isDirectChild) { + allPagesInFolder.push(file) + } else if (options.showSubfolders) { + const subfolderSlug = joinSegments( + ...fileParts.slice(0, folderParts.length + 1), + ) as FullSlug + const pagesInFolder = allPagesInSubfolders.get(subfolderSlug) || [] + allPagesInSubfolders.set(subfolderSlug, [...pagesInFolder, file]) + } + }) + + allPagesInSubfolders.forEach((files, subfolderSlug) => { + const hasIndex = allPagesInFolder.some( + (file) => subfolderSlug === stripSlashes(simplifySlug(file.slug!)), + ) + if (!hasIndex) { + const subfolderDates = files.sort(byDateAndAlphabetical(cfg))[0].dates + const subfolderTitle = subfolderSlug.split(path.posix.sep).at(-1)! + allPagesInFolder.push({ + slug: subfolderSlug, + dates: subfolderDates, + frontmatter: { title: subfolderTitle, tags: ["folder"] }, + }) + } + }) + + const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? [] + const classes = cssClasses.join(" ") + const listProps = { + ...props, + sort: options.sort, + allFiles: allPagesInFolder, + } + + const content = + (tree as Root).children.length === 0 + ? fileData.description + : htmlToJsx(fileData.filePath!, tree) + + return ( +
    +
    {content}
    +
    + {options.showFolderCount && ( +

    + {i18n(cfg.locale).pages.folderContent.itemsUnderFolder({ + count: allPagesInFolder.length, + })} +

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

    {content}

    +
    +

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

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

    + + {tag} + +

    + {content &&

    {content}

    } +
    +

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

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

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

    +
    + +
    +
    +
    + ) + } + } + + TagContent.css = style + PageList.css + return TagContent +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/renderPage.tsx b/quartz/components/renderPage.tsx new file mode 100755 index 0000000..6c398dc --- /dev/null +++ b/quartz/components/renderPage.tsx @@ -0,0 +1,280 @@ +import { render } from "preact-render-to-string" +import { QuartzComponent, QuartzComponentProps } from "./types" +import HeaderConstructor from "./Header" +import BodyConstructor from "./Body" +import { JSResourceToScriptElement, StaticResources } from "../util/resources" +import { clone, FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path" +import { visit } from "unist-util-visit" +import { Root, Element, ElementContent } from "hast" +import { GlobalConfiguration } from "../cfg" +import { i18n } from "../i18n" +// @ts-ignore +import mermaidScript from "./scripts/mermaid.inline" +import mermaidStyle from "./styles/mermaid.inline.scss" +import { QuartzPluginData } from "../plugins/vfile" + +interface RenderComponents { + head: QuartzComponent + header: QuartzComponent[] + beforeBody: QuartzComponent[] + pageBody: QuartzComponent + afterBody: QuartzComponent[] + left: QuartzComponent[] + right: QuartzComponent[] + footer: QuartzComponent +} + +const headerRegex = new RegExp(/h[1-6]/) +export function pageResources( + baseDir: FullSlug | RelativeURL, + fileData: QuartzPluginData, + staticResources: StaticResources, +): StaticResources { + const contentIndexPath = joinSegments(baseDir, "static/contentIndex.json") + const contentIndexScript = `const fetchData = fetch("${contentIndexPath}").then(data => data.json())` + + const resources: StaticResources = { + css: [ + { + content: joinSegments(baseDir, "index.css"), + }, + ...staticResources.css, + ], + js: [ + { + src: joinSegments(baseDir, "prescript.js"), + loadTime: "beforeDOMReady", + contentType: "external", + }, + { + loadTime: "beforeDOMReady", + contentType: "inline", + spaPreserve: true, + script: contentIndexScript, + }, + ...staticResources.js, + ], + } + + if (fileData.hasMermaidDiagram) { + resources.js.push({ + script: mermaidScript, + loadTime: "afterDOMReady", + moduleType: "module", + contentType: "inline", + }) + resources.css.push({ content: mermaidStyle, inline: true }) + } + + // NOTE: we have to put this last to make sure spa.inline.ts is the last item. + resources.js.push({ + src: joinSegments(baseDir, "postscript.js"), + loadTime: "afterDOMReady", + moduleType: "module", + contentType: "external", + }) + + return resources +} + +export function renderPage( + cfg: GlobalConfiguration, + slug: FullSlug, + componentData: QuartzComponentProps, + components: RenderComponents, + pageResources: StaticResources, +): string { + // make a deep copy of the tree so we don't remove the transclusion references + // for the file cached in contentMap in build.ts + const root = clone(componentData.tree) as Root + + // process transcludes in componentData + visit(root, "element", (node, _index, _parent) => { + if (node.tagName === "blockquote") { + const classNames = (node.properties?.className ?? []) as string[] + if (classNames.includes("transclude")) { + const inner = node.children[0] as Element + const transcludeTarget = inner.properties["data-slug"] as FullSlug + const page = componentData.allFiles.find((f) => f.slug === transcludeTarget) + if (!page) { + return + } + + let blockRef = node.properties.dataBlock as string | undefined + if (blockRef?.startsWith("#^")) { + // block transclude + blockRef = blockRef.slice("#^".length) + let blockNode = page.blocks?.[blockRef] + if (blockNode) { + if (blockNode.tagName === "li") { + blockNode = { + type: "element", + tagName: "ul", + properties: {}, + children: [blockNode], + } + } + + node.children = [ + normalizeHastElement(blockNode, slug, transcludeTarget), + { + type: "element", + tagName: "a", + properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] }, + children: [ + { type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal }, + ], + }, + ] + } + } else if (blockRef?.startsWith("#") && page.htmlAst) { + // header transclude + blockRef = blockRef.slice(1) + let startIdx = undefined + let startDepth = undefined + let endIdx = undefined + for (const [i, el] of page.htmlAst.children.entries()) { + // skip non-headers + if (!(el.type === "element" && el.tagName.match(headerRegex))) continue + const depth = Number(el.tagName.substring(1)) + + // lookin for our blockref + if (startIdx === undefined || startDepth === undefined) { + // skip until we find the blockref that matches + if (el.properties?.id === blockRef) { + startIdx = i + startDepth = depth + } + } else if (depth <= startDepth) { + // looking for new header that is same level or higher + endIdx = i + break + } + } + + if (startIdx === undefined) { + return + } + + node.children = [ + ...(page.htmlAst.children.slice(startIdx, endIdx) as ElementContent[]).map((child) => + normalizeHastElement(child as Element, slug, transcludeTarget), + ), + { + type: "element", + tagName: "a", + properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] }, + children: [ + { type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal }, + ], + }, + ] + } else if (page.htmlAst) { + // page transclude + node.children = [ + { + type: "element", + tagName: "h1", + properties: {}, + children: [ + { + type: "text", + value: + page.frontmatter?.title ?? + i18n(cfg.locale).components.transcludes.transcludeOf({ + targetSlug: page.slug!, + }), + }, + ], + }, + ...(page.htmlAst.children as ElementContent[]).map((child) => + normalizeHastElement(child as Element, slug, transcludeTarget), + ), + { + type: "element", + tagName: "a", + properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] }, + children: [ + { type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal }, + ], + }, + ] + } + } + } + }) + + // set componentData.tree to the edited html that has transclusions rendered + componentData.tree = root + + const { + head: Head, + header, + beforeBody, + pageBody: Content, + afterBody, + left, + right, + footer: Footer, + } = components + const Header = HeaderConstructor() + const Body = BodyConstructor() + + const LeftComponent = ( + + ) + + const RightComponent = ( + + ) + + const lang = componentData.fileData.frontmatter?.lang ?? cfg.locale?.split("-")[0] ?? "en" + const doc = ( + + + +
    + + {LeftComponent} +
    + + + {/*
    */} + +
    + {RightComponent} +
    + +
    + + {pageResources.js + .filter((resource) => resource.loadTime === "afterDOMReady") + .map((res) => JSResourceToScriptElement(res))} + + ) + + return "\n" + render(doc) +} diff --git a/quartz/components/scripts/callout.inline.ts b/quartz/components/scripts/callout.inline.ts new file mode 100755 index 0000000..8f63df3 --- /dev/null +++ b/quartz/components/scripts/callout.inline.ts @@ -0,0 +1,44 @@ +function toggleCallout(this: HTMLElement) { + const outerBlock = this.parentElement! + outerBlock.classList.toggle("is-collapsed") + const collapsed = outerBlock.classList.contains("is-collapsed") + const height = collapsed ? this.scrollHeight : outerBlock.scrollHeight + outerBlock.style.maxHeight = height + "px" + + // walk and adjust height of all parents + let current = outerBlock + let parent = outerBlock.parentElement + while (parent) { + if (!parent.classList.contains("callout")) { + return + } + + const collapsed = parent.classList.contains("is-collapsed") + const height = collapsed ? parent.scrollHeight : parent.scrollHeight + current.scrollHeight + parent.style.maxHeight = height + "px" + + current = parent + parent = parent.parentElement + } +} + +function setupCallout() { + const collapsible = document.getElementsByClassName( + `callout is-collapsible`, + ) as HTMLCollectionOf + for (const div of collapsible) { + const title = div.firstElementChild + + if (title) { + title.addEventListener("click", toggleCallout) + window.addCleanup(() => title.removeEventListener("click", toggleCallout)) + + const collapsed = div.classList.contains("is-collapsed") + const height = collapsed ? title.scrollHeight : div.scrollHeight + div.style.maxHeight = height + "px" + } + } +} + +document.addEventListener("nav", setupCallout) +window.addEventListener("resize", setupCallout) diff --git a/quartz/components/scripts/checkbox.inline.ts b/quartz/components/scripts/checkbox.inline.ts new file mode 100755 index 0000000..50ab042 --- /dev/null +++ b/quartz/components/scripts/checkbox.inline.ts @@ -0,0 +1,23 @@ +import { getFullSlug } from "../../util/path" + +const checkboxId = (index: number) => `${getFullSlug(window)}-checkbox-${index}` + +document.addEventListener("nav", () => { + const checkboxes = document.querySelectorAll( + "input.checkbox-toggle", + ) as NodeListOf + checkboxes.forEach((el, index) => { + const elId = checkboxId(index) + + const switchState = (e: Event) => { + const newCheckboxState = (e.target as HTMLInputElement)?.checked ? "true" : "false" + localStorage.setItem(elId, newCheckboxState) + } + + el.addEventListener("change", switchState) + window.addCleanup(() => el.removeEventListener("change", switchState)) + if (localStorage.getItem(elId) === "true") { + el.checked = true + } + }) +}) diff --git a/quartz/components/scripts/clipboard.inline.ts b/quartz/components/scripts/clipboard.inline.ts new file mode 100755 index 0000000..e16c112 --- /dev/null +++ b/quartz/components/scripts/clipboard.inline.ts @@ -0,0 +1,37 @@ +const svgCopy = + '' +const svgCheck = + '' + +document.addEventListener("nav", () => { + const els = document.getElementsByTagName("pre") + for (let i = 0; i < els.length; i++) { + const codeBlock = els[i].getElementsByTagName("code")[0] + if (codeBlock) { + const source = ( + codeBlock.dataset.clipboard ? JSON.parse(codeBlock.dataset.clipboard) : codeBlock.innerText + ).replace(/\n\n/g, "\n") + const button = document.createElement("button") + button.className = "clipboard-button" + button.type = "button" + button.innerHTML = svgCopy + button.ariaLabel = "Copy source" + function onClick() { + navigator.clipboard.writeText(source).then( + () => { + button.blur() + button.innerHTML = svgCheck + setTimeout(() => { + button.innerHTML = svgCopy + button.style.borderColor = "" + }, 2000) + }, + (error) => console.error(error), + ) + } + button.addEventListener("click", onClick) + window.addCleanup(() => button.removeEventListener("click", onClick)) + els[i].prepend(button) + } + } +}) diff --git a/quartz/components/scripts/comments.inline.ts b/quartz/components/scripts/comments.inline.ts new file mode 100755 index 0000000..c54230f --- /dev/null +++ b/quartz/components/scripts/comments.inline.ts @@ -0,0 +1,91 @@ +const changeTheme = (e: CustomEventMap["themechange"]) => { + const theme = e.detail.theme + const iframe = document.querySelector("iframe.giscus-frame") as HTMLIFrameElement + if (!iframe) { + return + } + + if (!iframe.contentWindow) { + return + } + + iframe.contentWindow.postMessage( + { + giscus: { + setConfig: { + theme: getThemeUrl(getThemeName(theme)), + }, + }, + }, + "https://giscus.app", + ) +} + +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 & { + dataset: DOMStringMap & { + repo: `${string}/${string}` + repoId: string + category: string + categoryId: string + themeUrl: string + lightTheme: string + darkTheme: string + mapping: "url" | "title" | "og:title" | "specific" | "number" | "pathname" + strict: string + reactionsEnabled: string + inputPosition: "top" | "bottom" + } +} + +document.addEventListener("nav", () => { + const giscusContainer = document.querySelector(".giscus") as GiscusElement + if (!giscusContainer) { + return + } + + const giscusScript = document.createElement("script") + giscusScript.src = "https://giscus.app/client.js" + giscusScript.async = true + giscusScript.crossOrigin = "anonymous" + giscusScript.setAttribute("data-loading", "lazy") + giscusScript.setAttribute("data-emit-metadata", "0") + giscusScript.setAttribute("data-repo", giscusContainer.dataset.repo) + giscusScript.setAttribute("data-repo-id", giscusContainer.dataset.repoId) + giscusScript.setAttribute("data-category", giscusContainer.dataset.category) + giscusScript.setAttribute("data-category-id", giscusContainer.dataset.categoryId) + giscusScript.setAttribute("data-mapping", giscusContainer.dataset.mapping) + giscusScript.setAttribute("data-strict", giscusContainer.dataset.strict) + giscusScript.setAttribute("data-reactions-enabled", giscusContainer.dataset.reactionsEnabled) + giscusScript.setAttribute("data-input-position", giscusContainer.dataset.inputPosition) + + const theme = document.documentElement.getAttribute("saved-theme") + if (theme) { + giscusScript.setAttribute("data-theme", getThemeUrl(getThemeName(theme))) + } + + giscusContainer.appendChild(giscusScript) + + document.addEventListener("themechange", changeTheme) + window.addCleanup(() => document.removeEventListener("themechange", changeTheme)) +}) diff --git a/quartz/components/scripts/darkmode.inline.ts b/quartz/components/scripts/darkmode.inline.ts new file mode 100755 index 0000000..56009a3 --- /dev/null +++ b/quartz/components/scripts/darkmode.inline.ts @@ -0,0 +1,38 @@ +const userPref = window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark" +const currentTheme = localStorage.getItem("theme") ?? userPref +document.documentElement.setAttribute("saved-theme", currentTheme) + +const emitThemeChangeEvent = (theme: "light" | "dark") => { + const event: CustomEventMap["themechange"] = new CustomEvent("themechange", { + detail: { theme }, + }) + document.dispatchEvent(event) +} + +document.addEventListener("nav", () => { + const switchTheme = (e: Event) => { + const newTheme = + document.documentElement.getAttribute("saved-theme") === "dark" ? "light" : "dark" + document.documentElement.setAttribute("saved-theme", newTheme) + localStorage.setItem("theme", newTheme) + emitThemeChangeEvent(newTheme) + } + + const themeChange = (e: MediaQueryListEvent) => { + const newTheme = e.matches ? "dark" : "light" + document.documentElement.setAttribute("saved-theme", newTheme) + localStorage.setItem("theme", newTheme) + emitThemeChangeEvent(newTheme) + } + + // Darkmode toggle + const themeButton = document.querySelector("#darkmode") as HTMLButtonElement + if (themeButton) { + themeButton.addEventListener("click", switchTheme) + window.addCleanup(() => themeButton.removeEventListener("click", switchTheme)) + } + // Listen for changes in prefers-color-scheme + const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)") + colorSchemeMediaQuery.addEventListener("change", themeChange) + window.addCleanup(() => colorSchemeMediaQuery.removeEventListener("change", themeChange)) +}) diff --git a/quartz/components/scripts/explorer.inline.ts b/quartz/components/scripts/explorer.inline.ts new file mode 100755 index 0000000..33d328a --- /dev/null +++ b/quartz/components/scripts/explorer.inline.ts @@ -0,0 +1,135 @@ +import { FolderState } from "../ExplorerNode" + +type MaybeHTMLElement = HTMLElement | undefined +let currentExplorerState: FolderState[] +const observer = new IntersectionObserver((entries) => { + // If last element is observed, remove gradient of "overflow" class so element is visible + const explorerUl = document.getElementById("explorer-ul") + if (!explorerUl) return + for (const entry of entries) { + if (entry.isIntersecting) { + explorerUl.classList.add("no-background") + } else { + explorerUl.classList.remove("no-background") + } + } +}) + +function toggleExplorer(this: HTMLElement) { + this.classList.toggle("collapsed") + this.setAttribute( + "aria-expanded", + this.getAttribute("aria-expanded") === "true" ? "false" : "true", + ) + const content = this.nextElementSibling as MaybeHTMLElement + if (!content) return + + content.classList.toggle("collapsed") +} + +function toggleFolder(evt: MouseEvent) { + evt.stopPropagation() + const target = evt.target as MaybeHTMLElement + if (!target) return + + const isSvg = target.nodeName === "svg" + const childFolderContainer = ( + isSvg + ? target.parentElement?.nextSibling + : target.parentElement?.parentElement?.nextElementSibling + ) as MaybeHTMLElement + const currentFolderParent = ( + isSvg ? target.nextElementSibling : target.parentElement + ) as MaybeHTMLElement + if (!(childFolderContainer && currentFolderParent)) return + + childFolderContainer.classList.toggle("open") + const isCollapsed = childFolderContainer.classList.contains("open") + setFolderState(childFolderContainer, !isCollapsed) + const fullFolderPath = currentFolderParent.dataset.folderpath as string + toggleCollapsedByPath(currentExplorerState, fullFolderPath) + const stringifiedFileTree = JSON.stringify(currentExplorerState) + localStorage.setItem("fileTree", stringifiedFileTree) +} + +function setupExplorer() { + const explorer = document.getElementById("explorer") + if (!explorer) return + + if (explorer.dataset.behavior === "collapse") { + for (const item of document.getElementsByClassName( + "folder-button", + ) as HTMLCollectionOf) { + item.addEventListener("click", toggleFolder) + window.addCleanup(() => item.removeEventListener("click", toggleFolder)) + } + } + + explorer.addEventListener("click", toggleExplorer) + window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer)) + + // Set up click handlers for each folder (click handler on folder "icon") + for (const item of document.getElementsByClassName( + "folder-icon", + ) as HTMLCollectionOf) { + item.addEventListener("click", toggleFolder) + window.addCleanup(() => item.removeEventListener("click", toggleFolder)) + } + + // Get folder state from local storage + const storageTree = localStorage.getItem("fileTree") + const useSavedFolderState = explorer?.dataset.savestate === "true" + const oldExplorerState: FolderState[] = + storageTree && useSavedFolderState ? JSON.parse(storageTree) : [] + const oldIndex = new Map(oldExplorerState.map((entry) => [entry.path, entry.collapsed])) + const newExplorerState: FolderState[] = explorer.dataset.tree + ? JSON.parse(explorer.dataset.tree) + : [] + currentExplorerState = [] + for (const { path, collapsed } of newExplorerState) { + currentExplorerState.push({ path, collapsed: oldIndex.get(path) ?? collapsed }) + } + + currentExplorerState.map((folderState) => { + const folderLi = document.querySelector( + `[data-folderpath='${folderState.path}']`, + ) as MaybeHTMLElement + const folderUl = folderLi?.parentElement?.nextElementSibling as MaybeHTMLElement + if (folderUl) { + setFolderState(folderUl, folderState.collapsed) + } + }) +} + +window.addEventListener("resize", setupExplorer) +document.addEventListener("nav", () => { + setupExplorer() + observer.disconnect() + + // select pseudo element at end of list + const lastItem = document.getElementById("explorer-end") + if (lastItem) { + observer.observe(lastItem) + } +}) + +/** + * Toggles the state of a given folder + * @param folderElement
    Element of folder (parent) + * @param collapsed if folder should be set to collapsed or not + */ +function setFolderState(folderElement: HTMLElement, collapsed: boolean) { + return collapsed ? folderElement.classList.remove("open") : folderElement.classList.add("open") +} + +/** + * Toggles visibility of a folder + * @param array array of FolderState (`fileTree`, either get from local storage or data attribute) + * @param path path to folder (e.g. 'advanced/more/more2') + */ +function toggleCollapsedByPath(array: FolderState[], path: string) { + const entry = array.find((item) => item.path === path) + if (entry) { + entry.collapsed = !entry.collapsed + } +} diff --git a/quartz/components/scripts/graph.inline.ts b/quartz/components/scripts/graph.inline.ts new file mode 100755 index 0000000..dbddae9 --- /dev/null +++ b/quartz/components/scripts/graph.inline.ts @@ -0,0 +1,601 @@ +import type { ContentDetails } from "../../plugins/emitters/contentIndex" +import { + SimulationNodeDatum, + SimulationLinkDatum, + Simulation, + forceSimulation, + forceManyBody, + forceCenter, + forceLink, + forceCollide, + zoomIdentity, + select, + drag, + zoom, +} from "d3" +import { Text, Graphics, Application, Container, Circle } from "pixi.js" +import { Group as TweenGroup, Tween as Tweened } from "@tweenjs/tween.js" +import { registerEscapeHandler, removeAllChildren } from "./util" +import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path" +import { D3Config } from "../Graph" + +type GraphicsInfo = { + color: string + gfx: Graphics + alpha: number + active: boolean +} + +type NodeData = { + id: SimpleSlug + text: string + tags: string[] +} & SimulationNodeDatum + +type SimpleLinkData = { + source: SimpleSlug + target: SimpleSlug +} + +type LinkData = { + source: NodeData + target: NodeData +} & SimulationLinkDatum + +type LinkRenderData = GraphicsInfo & { + simulationData: LinkData +} + +type NodeRenderData = GraphicsInfo & { + simulationData: NodeData + label: Text +} + +const localStorageKey = "graph-visited" +function getVisited(): Set { + return new Set(JSON.parse(localStorage.getItem(localStorageKey) ?? "[]")) +} + +function addToVisited(slug: SimpleSlug) { + const visited = getVisited() + visited.add(slug) + localStorage.setItem(localStorageKey, JSON.stringify([...visited])) +} + +type TweenNode = { + update: (time: number) => void + stop: () => void +} + +async function renderGraph(container: string, fullSlug: FullSlug) { + const slug = simplifySlug(fullSlug) + const visited = getVisited() + const graph = document.getElementById(container) + if (!graph) return + removeAllChildren(graph) + + let { + drag: enableDrag, + zoom: enableZoom, + depth, + scale, + repelForce, + centerForce, + linkDistance, + fontSize, + opacityScale, + removeTags, + showTags, + focusOnHover, + } = JSON.parse(graph.dataset["cfg"]!) as D3Config + + const data: Map = new Map( + Object.entries(await fetchData).map(([k, v]) => [ + simplifySlug(k as FullSlug), + v, + ]), + ) + const links: SimpleLinkData[] = [] + const tags: SimpleSlug[] = [] + const validLinks = new Set(data.keys()) + + const tweens = new Map() + for (const [source, details] of data.entries()) { + const outgoing = details.links ?? [] + + for (const dest of outgoing) { + if (validLinks.has(dest)) { + links.push({ source: source, target: dest }) + } + } + + if (showTags) { + const localTags = details.tags + .filter((tag) => !removeTags.includes(tag)) + .map((tag) => simplifySlug(("tags/" + tag) as FullSlug)) + + tags.push(...localTags.filter((tag) => !tags.includes(tag))) + + for (const tag of localTags) { + links.push({ source: source, target: tag }) + } + } + } + + const neighbourhood = new Set() + const wl: (SimpleSlug | "__SENTINEL")[] = [slug, "__SENTINEL"] + if (depth >= 0) { + while (depth >= 0 && wl.length > 0) { + // compute neighbours + const cur = wl.shift()! + if (cur === "__SENTINEL") { + depth-- + wl.push("__SENTINEL") + } else { + neighbourhood.add(cur) + const outgoing = links.filter((l) => l.source === cur) + const incoming = links.filter((l) => l.target === cur) + wl.push(...outgoing.map((l) => l.target), ...incoming.map((l) => l.source)) + } + } + } else { + validLinks.forEach((id) => neighbourhood.add(id)) + 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[] } = { + nodes, + links: links + .filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)) + .map((l) => ({ + source: nodes.find((n) => n.id === l.source)!, + target: nodes.find((n) => n.id === l.target)!, + })), + } + + // we virtualize the simulation and use pixi to actually render it + const simulation: Simulation = forceSimulation(graphData.nodes) + .force("charge", forceManyBody().strength(-100 * repelForce)) + .force("center", forceCenter().strength(centerForce)) + .force("link", forceLink(graphData.links).distance(linkDistance)) + .force("collide", forceCollide((n) => nodeRadius(n)).iterations(3)) + + const width = graph.offsetWidth + const height = Math.max(graph.offsetHeight, 250) + + // precompute style prop strings as pixi doesn't support css variables + const cssVars = [ + "--secondary", + "--tertiary", + "--gray", + "--light", + "--lightgray", + "--dark", + "--darkgray", + "--bodyFont", + ] as const + const computedStyleMap = cssVars.reduce( + (acc, key) => { + acc[key] = getComputedStyle(document.documentElement).getPropertyValue(key) + return acc + }, + {} as Record<(typeof cssVars)[number], string>, + ) + + // calculate color + const color = (d: NodeData) => { + const isCurrent = d.id === slug + if (isCurrent) { + return computedStyleMap["--secondary"] + } else if (visited.has(d.id) || d.id.startsWith("tags/")) { + return computedStyleMap["--tertiary"] + } else { + return computedStyleMap["--gray"] + } + } + + function nodeRadius(d: NodeData) { + const numLinks = graphData.links.filter( + (l) => l.source.id === d.id || l.target.id === d.id, + ).length + return 2 + Math.sqrt(numLinks) + } + + let hoveredNodeId: string | null = null + let hoveredNeighbours: Set = new Set() + const linkRenderData: LinkRenderData[] = [] + const nodeRenderData: NodeRenderData[] = [] + function updateHoverInfo(newHoveredId: string | null) { + hoveredNodeId = newHoveredId + + if (newHoveredId === null) { + hoveredNeighbours = new Set() + for (const n of nodeRenderData) { + n.active = false + } + + for (const l of linkRenderData) { + l.active = false + } + } else { + hoveredNeighbours = new Set() + 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(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()) + }, + }) + } + + function renderLabels() { + tweens.get("label")?.stop() + const tweenGroup = new TweenGroup() + + const defaultScale = 1 / scale + const activeScale = defaultScale * 1.1 + for (const n of nodeRenderData) { + const nodeId = n.simulationData.id + + if (hoveredNodeId === nodeId) { + tweenGroup.add( + new Tweened(n.label).to( + { + alpha: 1, + scale: { x: activeScale, y: activeScale }, + }, + 100, + ), + ) + } else { + tweenGroup.add( + new Tweened(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(n.gfx, tweenGroup).to({ alpha }, 200)) + } + + tweenGroup.getAll().forEach((tw) => tw.start()) + tweens.set("hover", { + update: tweenGroup.update.bind(tweenGroup), + stop() { + tweenGroup.getAll().forEach((tw) => tw.stop()) + }, + }) + } + + function renderPixiFromD3() { + renderNodes() + renderLinks() + renderLabels() + } + + tweens.forEach((tween) => tween.stop()) + tweens.clear() + + const app = new Application() + await app.init({ + width, + height, + antialias: true, + autoStart: false, + autoDensity: true, + backgroundAlpha: 0, + preference: "webgpu", + resolution: window.devicePixelRatio, + eventMode: "static", + }) + graph.appendChild(app.canvas) + + const stage = app.stage + stage.interactive = false + + const labelsContainer = new Container({ zIndex: 3 }) + const nodesContainer = new Container({ zIndex: 2 }) + const linkContainer = new Container({ zIndex: 1 }) + stage.addChild(nodesContainer, labelsContainer, linkContainer) + + for (const n of graphData.nodes) { + const nodeId = n.id + + const label = new Text({ + interactive: false, + eventMode: "none", + text: n.text, + alpha: 0, + anchor: { x: 0.5, y: 1.2 }, + style: { + fontSize: fontSize * 15, + fill: computedStyleMap["--dark"], + fontFamily: computedStyleMap["--bodyFont"], + }, + resolution: window.devicePixelRatio * 4, + }) + label.scale.set(1 / scale) + + let oldLabelOpacity = 0 + const isTagNode = nodeId.startsWith("tags/") + const gfx = new Graphics({ + interactive: true, + label: nodeId, + eventMode: "static", + hitArea: new Circle(0, 0, nodeRadius(n)), + cursor: "pointer", + }) + .circle(0, 0, nodeRadius(n)) + .fill({ color: isTagNode ? computedStyleMap["--light"] : color(n) }) + .stroke({ width: isTagNode ? 2 : 0, color: color(n) }) + .on("pointerover", (e) => { + updateHoverInfo(e.target.label) + oldLabelOpacity = label.alpha + if (!dragging) { + renderPixiFromD3() + } + }) + .on("pointerleave", () => { + updateHoverInfo(null) + label.alpha = oldLabelOpacity + if (!dragging) { + renderPixiFromD3() + } + }) + + nodesContainer.addChild(gfx) + labelsContainer.addChild(label) + + const nodeRenderDatum: NodeRenderData = { + simulationData: n, + gfx, + label, + color: color(n), + alpha: 1, + active: false, + } + + nodeRenderData.push(nodeRenderDatum) + } + + for (const l of graphData.links) { + const gfx = new Graphics({ interactive: false, eventMode: "none" }) + linkContainer.addChild(gfx) + + const linkRenderDatum: LinkRenderData = { + simulationData: l, + gfx, + color: computedStyleMap["--lightgray"], + alpha: 1, + active: false, + } + + linkRenderData.push(linkRenderDatum) + } + + let currentTransform = zoomIdentity + if (enableDrag) { + select(app.canvas).call( + drag() + .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())) + }) + } + } + + if (enableZoom) { + select(app.canvas).call( + zoom() + .extent([ + [0, 0], + [width, height], + ]) + .scaleExtent([0.25, 4]) + .on("zoom", ({ transform }) => { + currentTransform = transform + stage.scale.set(transform.k, transform.k) + stage.position.set(transform.x, transform.y) + + // zoom adjusts opacity of labels too + const scale = transform.k * opacityScale + let scaleOpacity = Math.max((scale - 1) / 3.75, 0) + const activeNodes = nodeRenderData.filter((n) => n.active).flatMap((n) => n.label) + + for (const label of labelsContainer.children) { + if (!activeNodes.includes(label)) { + label.alpha = scaleOpacity + } + } + }), + ) + } + + function animate(time: number) { + for (const n of nodeRenderData) { + const { x, y } = n.simulationData + if (!x || !y) continue + n.gfx.position.set(x + width / 2, y + height / 2) + if (n.label) { + n.label.position.set(x + width / 2, y + height / 2) + } + } + + for (const l of linkRenderData) { + const linkData = l.simulationData + l.gfx.clear() + l.gfx.moveTo(linkData.source.x! + width / 2, linkData.source.y! + height / 2) + l.gfx + .lineTo(linkData.target.x! + width / 2, linkData.target.y! + height / 2) + .stroke({ alpha: l.alpha, width: 1, color: l.color }) + } + + tweens.forEach((t) => t.update(time)) + app.renderer.render(stage) + requestAnimationFrame(animate) + } + + const graphAnimationFrameHandle = requestAnimationFrame(animate) + window.addCleanup(() => cancelAnimationFrame(graphAnimationFrameHandle)) +} + +document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { + const slug = e.detail.url + addToVisited(simplifySlug(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 = "" + } + } + + 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") + containerIcon?.addEventListener("click", renderGlobalGraph) + window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph)) + + document.addEventListener("keydown", shortcutHandler) + window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler)) +}) diff --git a/quartz/components/scripts/mermaid.inline.ts b/quartz/components/scripts/mermaid.inline.ts new file mode 100755 index 0000000..6dd254d --- /dev/null +++ b/quartz/components/scripts/mermaid.inline.ts @@ -0,0 +1,248 @@ +import { removeAllChildren } from "./util" + +interface Position { + x: number + y: number +} + +class DiagramPanZoom { + private isDragging = false + private startPan: Position = { x: 0, y: 0 } + private currentPan: Position = { x: 0, y: 0 } + private scale = 1 + private readonly MIN_SCALE = 0.5 + private readonly MAX_SCALE = 3 + private readonly ZOOM_SENSITIVITY = 0.001 + + constructor( + private container: HTMLElement, + private content: HTMLElement, + ) { + this.setupEventListeners() + this.setupNavigationControls() + } + + private setupEventListeners() { + // Mouse drag events + this.container.addEventListener("mousedown", this.onMouseDown.bind(this)) + document.addEventListener("mousemove", this.onMouseMove.bind(this)) + document.addEventListener("mouseup", this.onMouseUp.bind(this)) + + // Wheel zoom events + this.container.addEventListener("wheel", this.onWheel.bind(this), { passive: false }) + + // Reset on window resize + window.addEventListener("resize", this.resetTransform.bind(this)) + } + + private setupNavigationControls() { + const controls = document.createElement("div") + controls.className = "mermaid-controls" + + // Zoom controls + const zoomIn = this.createButton("+", () => this.zoom(0.1)) + const zoomOut = this.createButton("-", () => this.zoom(-0.1)) + const resetBtn = this.createButton("Reset", () => this.resetTransform()) + + controls.appendChild(zoomOut) + controls.appendChild(resetBtn) + controls.appendChild(zoomIn) + + this.container.appendChild(controls) + } + + private createButton(text: string, onClick: () => void): HTMLButtonElement { + const button = document.createElement("button") + button.textContent = text + button.className = "mermaid-control-button" + button.addEventListener("click", onClick) + window.addCleanup(() => button.removeEventListener("click", onClick)) + return button + } + + private onMouseDown(e: MouseEvent) { + if (e.button !== 0) return // Only handle left click + this.isDragging = true + this.startPan = { x: e.clientX - this.currentPan.x, y: e.clientY - this.currentPan.y } + this.container.style.cursor = "grabbing" + } + + private onMouseMove(e: MouseEvent) { + if (!this.isDragging) return + e.preventDefault() + + this.currentPan = { + x: e.clientX - this.startPan.x, + y: e.clientY - this.startPan.y, + } + + this.updateTransform() + } + + private onMouseUp() { + this.isDragging = false + this.container.style.cursor = "grab" + } + + private onWheel(e: WheelEvent) { + e.preventDefault() + + const delta = -e.deltaY * this.ZOOM_SENSITIVITY + const newScale = Math.min(Math.max(this.scale + delta, this.MIN_SCALE), this.MAX_SCALE) + + // Calculate mouse position relative to content + const rect = this.content.getBoundingClientRect() + const mouseX = e.clientX - rect.left + const mouseY = e.clientY - rect.top + + // Adjust pan to zoom around mouse position + const scaleDiff = newScale - this.scale + this.currentPan.x -= mouseX * scaleDiff + this.currentPan.y -= mouseY * scaleDiff + + this.scale = newScale + this.updateTransform() + } + + private zoom(delta: number) { + const newScale = Math.min(Math.max(this.scale + delta, this.MIN_SCALE), this.MAX_SCALE) + + // Zoom around center + const rect = this.content.getBoundingClientRect() + const centerX = rect.width / 2 + const centerY = rect.height / 2 + + const scaleDiff = newScale - this.scale + this.currentPan.x -= centerX * scaleDiff + this.currentPan.y -= centerY * scaleDiff + + this.scale = newScale + this.updateTransform() + } + + private updateTransform() { + this.content.style.transform = `translate(${this.currentPan.x}px, ${this.currentPan.y}px) scale(${this.scale})` + } + + private resetTransform() { + this.scale = 1 + this.currentPan = { x: 0, y: 0 } + this.updateTransform() + } +} + +const cssVars = [ + "--secondary", + "--tertiary", + "--gray", + "--light", + "--lightgray", + "--highlight", + "--dark", + "--darkgray", + "--codeFont", +] as const + +let mermaidImport = undefined +document.addEventListener("nav", async () => { + const center = document.querySelector(".center") as HTMLElement + const nodes = center.querySelectorAll("code.mermaid") as NodeListOf + if (nodes.length === 0) return + + const computedStyleMap = cssVars.reduce( + (acc, key) => { + acc[key] = getComputedStyle(document.documentElement).getPropertyValue(key) + return acc + }, + {} as Record<(typeof cssVars)[number], string>, + ) + + mermaidImport ||= await import( + //@ts-ignore + "https://cdnjs.cloudflare.com/ajax/libs/mermaid/11.4.0/mermaid.esm.min.mjs" + ) + const mermaid = mermaidImport.default + + const darkMode = document.documentElement.getAttribute("saved-theme") === "dark" + mermaid.initialize({ + startOnLoad: false, + securityLevel: "loose", + theme: darkMode ? "dark" : "base", + themeVariables: { + fontFamily: computedStyleMap["--codeFont"], + primaryColor: computedStyleMap["--light"], + primaryTextColor: computedStyleMap["--darkgray"], + primaryBorderColor: computedStyleMap["--tertiary"], + lineColor: computedStyleMap["--darkgray"], + secondaryColor: computedStyleMap["--secondary"], + tertiaryColor: computedStyleMap["--tertiary"], + clusterBkg: computedStyleMap["--light"], + edgeLabelBackground: computedStyleMap["--highlight"], + }, + }) + await mermaid.run({ nodes }) + + for (let i = 0; i < nodes.length; i++) { + const codeBlock = nodes[i] as HTMLElement + const pre = codeBlock.parentElement as HTMLPreElement + const clipboardBtn = pre.querySelector(".clipboard-button") as HTMLButtonElement + const expandBtn = pre.querySelector(".expand-button") as HTMLButtonElement + + const clipboardStyle = window.getComputedStyle(clipboardBtn) + const clipboardWidth = + clipboardBtn.offsetWidth + + parseFloat(clipboardStyle.marginLeft || "0") + + parseFloat(clipboardStyle.marginRight || "0") + + // Set expand button position + expandBtn.style.right = `calc(${clipboardWidth}px + 0.3rem)` + pre.prepend(expandBtn) + + // query popup container + const popupContainer = pre.querySelector("#mermaid-container") as HTMLElement + if (!popupContainer) return + + let panZoom: DiagramPanZoom | null = null + + function showMermaid() { + const container = popupContainer.querySelector("#mermaid-space") as HTMLElement + const content = popupContainer.querySelector(".mermaid-content") as HTMLElement + if (!content) return + removeAllChildren(content) + + // Clone the mermaid content + const mermaidContent = codeBlock.querySelector("svg")!.cloneNode(true) as SVGElement + content.appendChild(mermaidContent) + + // Show container + popupContainer.classList.add("active") + container.style.cursor = "grab" + + // Initialize pan-zoom after showing the popup + panZoom = new DiagramPanZoom(container, content) + } + + function hideMermaid() { + popupContainer.classList.remove("active") + panZoom = null + } + + function handleEscape(e: any) { + if (e.key === "Escape") { + hideMermaid() + } + } + + const closeBtn = popupContainer.querySelector(".close-button") as HTMLButtonElement + + closeBtn.addEventListener("click", hideMermaid) + expandBtn.addEventListener("click", showMermaid) + document.addEventListener("keydown", handleEscape) + + window.addCleanup(() => { + closeBtn.removeEventListener("click", hideMermaid) + expandBtn.removeEventListener("click", showMermaid) + document.removeEventListener("keydown", handleEscape) + }) + } +}) diff --git a/quartz/components/scripts/popover.inline.ts b/quartz/components/scripts/popover.inline.ts new file mode 100755 index 0000000..b01af0e --- /dev/null +++ b/quartz/components/scripts/popover.inline.ts @@ -0,0 +1,109 @@ +import { computePosition, flip, inline, shift } from "@floating-ui/dom" +import { normalizeRelativeURLs } from "../../util/path" +import { fetchCanonical } from "./util" + +const p = new DOMParser() +async function mouseEnterHandler( + this: HTMLAnchorElement, + { clientX, clientY }: { clientX: number; clientY: number }, +) { + const link = this + if (link.dataset.noPopover === "true") { + return + } + + async function setPosition(popoverElement: HTMLElement) { + const { x, y } = await computePosition(link, popoverElement, { + middleware: [inline({ x: clientX, y: clientY }), shift(), flip()], + }) + Object.assign(popoverElement.style, { + left: `${x}px`, + top: `${y}px`, + }) + } + + const hasAlreadyBeenFetched = () => + [...link.children].some((child) => child.classList.contains("popover")) + + // dont refetch if there's already a popover + if (hasAlreadyBeenFetched()) { + return setPosition(link.lastChild as HTMLElement) + } + + const thisUrl = new URL(document.location.href) + thisUrl.hash = "" + thisUrl.search = "" + const targetUrl = new URL(link.href) + const hash = decodeURIComponent(targetUrl.hash) + targetUrl.hash = "" + targetUrl.search = "" + + const response = await fetchCanonical(targetUrl).catch((err) => { + console.error(err) + }) + + // bailout if another popover exists + if (hasAlreadyBeenFetched()) { + return + } + + if (!response) return + const [contentType] = response.headers.get("Content-Type")!.split(";") + const [contentTypeCategory, typeInfo] = contentType.split("/") + + const popoverElement = document.createElement("div") + popoverElement.classList.add("popover") + const popoverInner = document.createElement("div") + popoverInner.classList.add("popover-inner") + popoverElement.appendChild(popoverInner) + + popoverInner.dataset.contentType = contentType ?? undefined + + switch (contentTypeCategory) { + case "image": + const img = document.createElement("img") + img.src = targetUrl.toString() + img.alt = targetUrl.pathname + + popoverInner.appendChild(img) + break + case "application": + switch (typeInfo) { + case "pdf": + const pdf = document.createElement("iframe") + pdf.src = targetUrl.toString() + popoverInner.appendChild(pdf) + break + default: + break + } + break + default: + const contents = await response.text() + const html = p.parseFromString(contents, "text/html") + normalizeRelativeURLs(html, targetUrl) + const elts = [...html.getElementsByClassName("popover-hint")] + if (elts.length === 0) return + + elts.forEach((elt) => popoverInner.appendChild(elt)) + } + + setPosition(popoverElement) + link.appendChild(popoverElement) + + if (hash !== "") { + const heading = popoverInner.querySelector(hash) as HTMLElement | null + if (heading) { + // leave ~12px of buffer when scrolling to a heading + popoverInner.scroll({ top: heading.offsetTop - 12, behavior: "instant" }) + } + } +} + +document.addEventListener("nav", () => { + const links = [...document.getElementsByClassName("internal")] as HTMLAnchorElement[] + for (const link of links) { + link.addEventListener("mouseenter", mouseEnterHandler) + window.addCleanup(() => link.removeEventListener("mouseenter", mouseEnterHandler)) + } +}) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts new file mode 100755 index 0000000..f422d49 --- /dev/null +++ b/quartz/components/scripts/search.inline.ts @@ -0,0 +1,493 @@ +import FlexSearch from "flexsearch" +import { ContentDetails } from "../../plugins/emitters/contentIndex" +import { registerEscapeHandler, removeAllChildren } from "./util" +import { FullSlug, normalizeRelativeURLs, resolveRelative } from "../../util/path" + +interface Item { + id: number + slug: FullSlug + title: string + content: string + tags: string[] +} + +// Can be expanded with things like "term" in the future +type SearchType = "basic" | "tags" +let searchType: SearchType = "basic" +let currentSearchTerm: string = "" +const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/) +let index = new FlexSearch.Document({ + charset: "latin:extra", + encode: encoder, + document: { + id: "id", + tag: "tags", + index: [ + { + field: "title", + tokenize: "forward", + }, + { + field: "content", + tokenize: "forward", + }, + { + field: "tags", + tokenize: "forward", + }, + ], + }, +}) + +const p = new DOMParser() +const fetchContentCache: Map = new Map() +const contextWindowWords = 30 +const numSearchResults = 8 +const numTagResults = 5 + +const tokenizeTerm = (term: string) => { + const tokens = term.split(/\s+/).filter((t) => t.trim() !== "") + const tokenLen = tokens.length + if (tokenLen > 1) { + for (let i = 1; i < tokenLen; i++) { + tokens.push(tokens.slice(0, i + 1).join(" ")) + } + } + + return tokens.sort((a, b) => b.length - a.length) // always highlight longest terms first +} + +function highlight(searchTerm: string, text: string, trim?: boolean) { + const tokenizedTerms = tokenizeTerm(searchTerm) + let tokenizedText = text.split(/\s+/).filter((t) => t !== "") + + let startIndex = 0 + let endIndex = tokenizedText.length - 1 + if (trim) { + const includesCheck = (tok: string) => + tokenizedTerms.some((term) => tok.toLowerCase().startsWith(term.toLowerCase())) + const occurrencesIndices = tokenizedText.map(includesCheck) + + let bestSum = 0 + let bestIndex = 0 + for (let i = 0; i < Math.max(tokenizedText.length - contextWindowWords, 0); i++) { + const window = occurrencesIndices.slice(i, i + contextWindowWords) + const windowSum = window.reduce((total, cur) => total + (cur ? 1 : 0), 0) + if (windowSum >= bestSum) { + bestSum = windowSum + bestIndex = i + } + } + + startIndex = Math.max(bestIndex - contextWindowWords, 0) + endIndex = Math.min(startIndex + 2 * contextWindowWords, tokenizedText.length - 1) + tokenizedText = tokenizedText.slice(startIndex, endIndex) + } + + const slice = tokenizedText + .map((tok) => { + // see if this tok is prefixed by any search terms + for (const searchTok of tokenizedTerms) { + if (tok.toLowerCase().includes(searchTok.toLowerCase())) { + const regex = new RegExp(searchTok.toLowerCase(), "gi") + return tok.replace(regex, `$&`) + } + } + return tok + }) + .join(" ") + + return `${startIndex === 0 ? "" : "..."}${slice}${ + endIndex === tokenizedText.length - 1 ? "" : "..." + }` +} + +function highlightHTML(searchTerm: string, el: HTMLElement) { + const p = new DOMParser() + const tokenizedTerms = tokenizeTerm(searchTerm) + const html = p.parseFromString(el.innerHTML, "text/html") + + const createHighlightSpan = (text: string) => { + const span = document.createElement("span") + span.className = "highlight" + span.textContent = text + return span + } + + const highlightTextNodes = (node: Node, term: string) => { + if (node.nodeType === Node.TEXT_NODE) { + const nodeText = node.nodeValue ?? "" + const regex = new RegExp(term.toLowerCase(), "gi") + const matches = nodeText.match(regex) + if (!matches || matches.length === 0) return + const spanContainer = document.createElement("span") + let lastIndex = 0 + for (const match of matches) { + const matchIndex = nodeText.indexOf(match, lastIndex) + spanContainer.appendChild(document.createTextNode(nodeText.slice(lastIndex, matchIndex))) + spanContainer.appendChild(createHighlightSpan(match)) + lastIndex = matchIndex + match.length + } + spanContainer.appendChild(document.createTextNode(nodeText.slice(lastIndex))) + node.parentNode?.replaceChild(spanContainer, node) + } else if (node.nodeType === Node.ELEMENT_NODE) { + if ((node as HTMLElement).classList.contains("highlight")) return + Array.from(node.childNodes).forEach((child) => highlightTextNodes(child, term)) + } + } + + for (const term of tokenizedTerms) { + highlightTextNodes(html.body, term) + } + + return html.body +} + +document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { + const currentSlug = e.detail.url + const data = await fetchData + const container = document.getElementById("search-container") + const sidebar = container?.closest(".sidebar") as HTMLElement + const searchButton = document.getElementById("search-button") + const searchBar = document.getElementById("search-bar") as HTMLInputElement | null + const searchLayout = document.getElementById("search-layout") + const idDataMap = Object.keys(data) as FullSlug[] + + const appendLayout = (el: HTMLElement) => { + if (searchLayout?.querySelector(`#${el.id}`) === null) { + searchLayout?.appendChild(el) + } + } + + const enablePreview = searchLayout?.dataset?.preview === "true" + let preview: HTMLDivElement | undefined = undefined + let previewInner: HTMLDivElement | undefined = undefined + const results = document.createElement("div") + results.id = "results-container" + appendLayout(results) + + if (enablePreview) { + preview = document.createElement("div") + preview.id = "preview-container" + appendLayout(preview) + } + + function hideSearch() { + container?.classList.remove("active") + if (searchBar) { + searchBar.value = "" // clear the input when we dismiss the search + } + if (sidebar) { + sidebar.style.zIndex = "" + } + if (results) { + removeAllChildren(results) + } + if (preview) { + removeAllChildren(preview) + } + if (searchLayout) { + searchLayout.classList.remove("display-results") + } + + searchType = "basic" // reset search type after closing + + searchButton?.focus() + } + + function showSearch(searchTypeNew: SearchType) { + searchType = searchTypeNew + if (sidebar) { + sidebar.style.zIndex = "1" + } + container?.classList.add("active") + searchBar?.focus() + } + + let currentHover: HTMLInputElement | null = null + + async function shortcutHandler(e: HTMLElementEventMap["keydown"]) { + if (e.key === "k" && (e.ctrlKey || e.metaKey) && !e.shiftKey) { + e.preventDefault() + const searchBarOpen = container?.classList.contains("active") + searchBarOpen ? hideSearch() : showSearch("basic") + return + } else if (e.shiftKey && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { + // Hotkey to open tag search + e.preventDefault() + const searchBarOpen = container?.classList.contains("active") + searchBarOpen ? hideSearch() : showSearch("tags") + + // add "#" prefix for tag search + if (searchBar) searchBar.value = "#" + return + } + + if (currentHover) { + currentHover.classList.remove("focus") + } + + // If search is active, then we will render the first result and display accordingly + if (!container?.classList.contains("active")) return + if (e.key === "Enter") { + // If result has focus, navigate to that one, otherwise pick first result + if (results?.contains(document.activeElement)) { + const active = document.activeElement as HTMLInputElement + if (active.classList.contains("no-match")) return + await displayPreview(active) + active.click() + } else { + const anchor = document.getElementsByClassName("result-card")[0] as HTMLInputElement | null + if (!anchor || anchor?.classList.contains("no-match")) return + await displayPreview(anchor) + anchor.click() + } + } else if (e.key === "ArrowUp" || (e.shiftKey && e.key === "Tab")) { + e.preventDefault() + if (results?.contains(document.activeElement)) { + // If an element in results-container already has focus, focus previous one + const currentResult = currentHover + ? currentHover + : (document.activeElement as HTMLInputElement | null) + const prevResult = currentResult?.previousElementSibling as HTMLInputElement | null + currentResult?.classList.remove("focus") + prevResult?.focus() + if (prevResult) currentHover = prevResult + await displayPreview(prevResult) + } + } else if (e.key === "ArrowDown" || e.key === "Tab") { + e.preventDefault() + // The results should already been focused, so we need to find the next one. + // The activeElement is the search bar, so we need to find the first result and focus it. + if (document.activeElement === searchBar || currentHover !== null) { + const firstResult = currentHover + ? currentHover + : (document.getElementsByClassName("result-card")[0] as HTMLInputElement | null) + const secondResult = firstResult?.nextElementSibling as HTMLInputElement | null + firstResult?.classList.remove("focus") + secondResult?.focus() + if (secondResult) currentHover = secondResult + await displayPreview(secondResult) + } + } + } + + const formatForDisplay = (term: string, id: number) => { + const slug = idDataMap[id] + return { + id, + slug, + title: searchType === "tags" ? data[slug].title : highlight(term, data[slug].title ?? ""), + content: highlight(term, data[slug].content ?? "", true), + tags: highlightTags(term.substring(1), data[slug].tags), + } + } + + function highlightTags(term: string, tags: string[]) { + if (!tags || searchType !== "tags") { + return [] + } + + return tags + .map((tag) => { + if (tag.toLowerCase().includes(term.toLowerCase())) { + return `
  • #${tag}

  • ` + } else { + return `
  • #${tag}

  • ` + } + }) + .slice(0, numTagResults) + } + + function resolveUrl(slug: FullSlug): URL { + return new URL(resolveRelative(currentSlug, slug), location.toString()) + } + + const resultToHTML = ({ slug, title, content, tags }: Item) => { + const htmlTags = tags.length > 0 ? `
      ${tags.join("")}
    ` : `` + const itemTile = document.createElement("a") + itemTile.classList.add("result-card") + itemTile.id = slug + itemTile.href = resolveUrl(slug).toString() + itemTile.innerHTML = `

    ${title}

    ${htmlTags}${ + enablePreview && window.innerWidth > 600 ? "" : `

    ${content}

    ` + }` + itemTile.addEventListener("click", (event) => { + if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return + hideSearch() + }) + + const handler = (event: MouseEvent) => { + if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return + hideSearch() + } + + async function onMouseEnter(ev: MouseEvent) { + if (!ev.target) return + const target = ev.target as HTMLInputElement + await displayPreview(target) + } + + itemTile.addEventListener("mouseenter", onMouseEnter) + window.addCleanup(() => itemTile.removeEventListener("mouseenter", onMouseEnter)) + itemTile.addEventListener("click", handler) + window.addCleanup(() => itemTile.removeEventListener("click", handler)) + + return itemTile + } + + async function displayResults(finalResults: Item[]) { + if (!results) return + + removeAllChildren(results) + if (finalResults.length === 0) { + results.innerHTML = ` +

    No results.

    +

    Try another search term?

    +
    ` + } else { + results.append(...finalResults.map(resultToHTML)) + } + + if (finalResults.length === 0 && preview) { + // no results, clear previous preview + removeAllChildren(preview) + } else { + // focus on first result, then also dispatch preview immediately + const firstChild = results.firstElementChild as HTMLElement + firstChild.classList.add("focus") + currentHover = firstChild as HTMLInputElement + await displayPreview(firstChild) + } + } + + async function fetchContent(slug: FullSlug): Promise { + if (fetchContentCache.has(slug)) { + return fetchContentCache.get(slug) as Element[] + } + + const targetUrl = resolveUrl(slug).toString() + const contents = await fetch(targetUrl) + .then((res) => res.text()) + .then((contents) => { + if (contents === undefined) { + throw new Error(`Could not fetch ${targetUrl}`) + } + const html = p.parseFromString(contents ?? "", "text/html") + normalizeRelativeURLs(html, targetUrl) + return [...html.getElementsByClassName("popover-hint")] + }) + + fetchContentCache.set(slug, contents) + return contents + } + + async function displayPreview(el: HTMLElement | null) { + if (!searchLayout || !enablePreview || !el || !preview) return + const slug = el.id as FullSlug + const innerDiv = await fetchContent(slug).then((contents) => + contents.flatMap((el) => [...highlightHTML(currentSearchTerm, el as HTMLElement).children]), + ) + previewInner = document.createElement("div") + previewInner.classList.add("preview-inner") + previewInner.append(...innerDiv) + preview.replaceChildren(previewInner) + + // scroll to longest + const highlights = [...preview.querySelectorAll(".highlight")].sort( + (a, b) => b.innerHTML.length - a.innerHTML.length, + ) + highlights[0]?.scrollIntoView({ block: "start" }) + } + + async function onType(e: HTMLElementEventMap["input"]) { + if (!searchLayout || !index) return + currentSearchTerm = (e.target as HTMLInputElement).value + searchLayout.classList.toggle("display-results", currentSearchTerm !== "") + searchType = currentSearchTerm.startsWith("#") ? "tags" : "basic" + + let searchResults: FlexSearch.SimpleDocumentSearchResultSetUnit[] + if (searchType === "tags") { + currentSearchTerm = currentSearchTerm.substring(1).trim() + const separatorIndex = currentSearchTerm.indexOf(" ") + if (separatorIndex != -1) { + // search by title and content index and then filter by tag (implemented in flexsearch) + const tag = currentSearchTerm.substring(0, separatorIndex) + const query = currentSearchTerm.substring(separatorIndex + 1).trim() + searchResults = await index.searchAsync({ + query: query, + // return at least 10000 documents, so it is enough to filter them by tag (implemented in flexsearch) + limit: Math.max(numSearchResults, 10000), + index: ["title", "content"], + tag: tag, + }) + for (let searchResult of searchResults) { + searchResult.result = searchResult.result.slice(0, numSearchResults) + } + // set search type to basic and remove tag from term for proper highlightning and scroll + searchType = "basic" + currentSearchTerm = query + } else { + // default search by tags index + searchResults = await index.searchAsync({ + query: currentSearchTerm, + limit: numSearchResults, + index: ["tags"], + }) + } + } else if (searchType === "basic") { + searchResults = await index.searchAsync({ + query: currentSearchTerm, + limit: numSearchResults, + index: ["title", "content"], + }) + } + + const getByField = (field: string): number[] => { + const results = searchResults.filter((x) => x.field === field) + return results.length === 0 ? [] : ([...results[0].result] as number[]) + } + + // order titles ahead of content + const allIds: Set = new Set([ + ...getByField("title"), + ...getByField("content"), + ...getByField("tags"), + ]) + const finalResults = [...allIds].map((id) => formatForDisplay(currentSearchTerm, id)) + await displayResults(finalResults) + } + + document.addEventListener("keydown", shortcutHandler) + window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler)) + searchButton?.addEventListener("click", () => showSearch("basic")) + window.addCleanup(() => searchButton?.removeEventListener("click", () => showSearch("basic"))) + searchBar?.addEventListener("input", onType) + window.addCleanup(() => searchBar?.removeEventListener("input", onType)) + + registerEscapeHandler(container, hideSearch) + await fillDocument(data) +}) + +/** + * Fills flexsearch document with data + * @param index index to fill + * @param data data to fill index with + */ +async function fillDocument(data: { [key: FullSlug]: ContentDetails }) { + let id = 0 + const promises: Array> = [] + for (const [slug, fileData] of Object.entries(data)) { + promises.push( + index.addAsync(id++, { + id, + slug: slug as FullSlug, + title: fileData.title, + content: fileData.content, + tags: fileData.tags, + }), + ) + } + + return await Promise.all(promises) +} diff --git a/quartz/components/scripts/spa.inline.ts b/quartz/components/scripts/spa.inline.ts new file mode 100755 index 0000000..df48f04 --- /dev/null +++ b/quartz/components/scripts/spa.inline.ts @@ -0,0 +1,203 @@ +import micromorph from "micromorph" +import { FullSlug, RelativeURL, getFullSlug, normalizeRelativeURLs } from "../../util/path" +import { fetchCanonical } from "./util" + +// adapted from `micromorph` +// https://github.com/natemoo-re/micromorph +const NODE_TYPE_ELEMENT = 1 +let announcer = document.createElement("route-announcer") +const isElement = (target: EventTarget | null): target is Element => + (target as Node)?.nodeType === NODE_TYPE_ELEMENT +const isLocalUrl = (href: string) => { + try { + const url = new URL(href) + if (window.location.origin === url.origin) { + return true + } + } catch (e) {} + return false +} + +const isSamePage = (url: URL): boolean => { + const sameOrigin = url.origin === window.location.origin + const samePath = url.pathname === window.location.pathname + return sameOrigin && samePath +} + +const getOpts = ({ target }: Event): { url: URL; scroll?: boolean } | undefined => { + if (!isElement(target)) return + if (target.attributes.getNamedItem("target")?.value === "_blank") return + const a = target.closest("a") + if (!a) return + if ("routerIgnore" in a.dataset) return + const { href } = a + if (!isLocalUrl(href)) return + return { url: new URL(href), scroll: "routerNoscroll" in a.dataset ? false : undefined } +} + +function notifyNav(url: FullSlug) { + const event: CustomEventMap["nav"] = new CustomEvent("nav", { detail: { url } }) + document.dispatchEvent(event) +} + +const cleanupFns: Set<(...args: any[]) => void> = new Set() +window.addCleanup = (fn) => cleanupFns.add(fn) + +function startLoading() { + const loadingBar = document.createElement("div") + loadingBar.className = "navigation-progress" + loadingBar.style.width = "0" + if (!document.body.contains(loadingBar)) { + document.body.appendChild(loadingBar) + } + + setTimeout(() => { + loadingBar.style.width = "80%" + }, 100) +} + +let p: DOMParser +async function navigate(url: URL, isBack: boolean = false) { + startLoading() + p = p || new DOMParser() + const contents = await fetchCanonical(url) + .then((res) => { + const contentType = res.headers.get("content-type") + if (contentType?.startsWith("text/html")) { + return res.text() + } else { + window.location.assign(url) + } + }) + .catch(() => { + window.location.assign(url) + }) + + if (!contents) return + + // cleanup old + cleanupFns.forEach((fn) => fn()) + cleanupFns.clear() + + const html = p.parseFromString(contents, "text/html") + normalizeRelativeURLs(html, url) + + let title = html.querySelector("title")?.textContent + if (title) { + document.title = title + } else { + const h1 = document.querySelector("h1") + title = h1?.innerText ?? h1?.textContent ?? url.pathname + } + if (announcer.textContent !== title) { + announcer.textContent = title + } + announcer.dataset.persist = "" + html.body.appendChild(announcer) + + // morph body + micromorph(document.body, html.body) + + // scroll into place and add history + if (!isBack) { + if (url.hash) { + const el = document.getElementById(decodeURIComponent(url.hash.substring(1))) + el?.scrollIntoView() + } else { + window.scrollTo({ top: 0 }) + } + } + + // now, patch head + const elementsToRemove = document.head.querySelectorAll(":not([spa-preserve])") + elementsToRemove.forEach((el) => el.remove()) + const elementsToAdd = html.head.querySelectorAll(":not([spa-preserve])") + elementsToAdd.forEach((el) => document.head.appendChild(el)) + + // delay setting the url until now + // at this point everything is loaded so changing the url should resolve to the correct addresses + if (!isBack) { + history.pushState({}, "", url) + } + + notifyNav(getFullSlug(window)) + delete announcer.dataset.persist +} + +window.spaNavigate = navigate + +function createRouter() { + if (typeof window !== "undefined") { + window.addEventListener("click", async (event) => { + const { url } = getOpts(event) ?? {} + // dont hijack behaviour, just let browser act normally + if (!url || event.ctrlKey || event.metaKey) return + event.preventDefault() + + if (isSamePage(url) && url.hash) { + const el = document.getElementById(decodeURIComponent(url.hash.substring(1))) + el?.scrollIntoView() + history.pushState({}, "", url) + return + } + + try { + navigate(url, false) + } catch (e) { + window.location.assign(url) + } + }) + + window.addEventListener("popstate", (event) => { + const { url } = getOpts(event) ?? {} + if (window.location.hash && window.location.pathname === url?.pathname) return + try { + navigate(new URL(window.location.toString()), true) + } catch (e) { + window.location.reload() + } + return + }) + } + + return new (class Router { + go(pathname: RelativeURL) { + const url = new URL(pathname, window.location.toString()) + return navigate(url, false) + } + + back() { + return window.history.back() + } + + forward() { + return window.history.forward() + } + })() +} + +createRouter() +notifyNav(getFullSlug(window)) + +if (!customElements.get("route-announcer")) { + const attrs = { + "aria-live": "assertive", + "aria-atomic": "true", + style: + "position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px", + } + + customElements.define( + "route-announcer", + class RouteAnnouncer extends HTMLElement { + constructor() { + super() + } + connectedCallback() { + for (const [key, value] of Object.entries(attrs)) { + this.setAttribute(key, value) + } + } + }, + ) +} diff --git a/quartz/components/scripts/toc.inline.ts b/quartz/components/scripts/toc.inline.ts new file mode 100755 index 0000000..2cfb3f9 --- /dev/null +++ b/quartz/components/scripts/toc.inline.ts @@ -0,0 +1,47 @@ +const bufferPx = 150 +const observer = new IntersectionObserver((entries) => { + for (const entry of entries) { + const slug = entry.target.id + const tocEntryElement = document.querySelector(`a[data-for="${slug}"]`) + const windowHeight = entry.rootBounds?.height + if (windowHeight && tocEntryElement) { + if (entry.boundingClientRect.y < windowHeight) { + tocEntryElement.classList.add("in-view") + } else { + tocEntryElement.classList.remove("in-view") + } + } + } +}) + +function toggleToc(this: HTMLElement) { + this.classList.toggle("collapsed") + this.setAttribute( + "aria-expanded", + this.getAttribute("aria-expanded") === "true" ? "false" : "true", + ) + const content = this.nextElementSibling as HTMLElement | undefined + if (!content) return + content.classList.toggle("collapsed") +} + +function setupToc() { + const toc = document.getElementById("toc") + if (toc) { + const collapsed = toc.classList.contains("collapsed") + const content = toc.nextElementSibling as HTMLElement | undefined + if (!content) return + toc.addEventListener("click", toggleToc) + window.addCleanup(() => toc.removeEventListener("click", toggleToc)) + } +} + +window.addEventListener("resize", setupToc) +document.addEventListener("nav", () => { + setupToc() + + // update toc entry highlighting + observer.disconnect() + const headers = document.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]") + headers.forEach((header) => observer.observe(header)) +}) diff --git a/quartz/components/scripts/util.ts b/quartz/components/scripts/util.ts new file mode 100755 index 0000000..ff486cf --- /dev/null +++ b/quartz/components/scripts/util.ts @@ -0,0 +1,45 @@ +export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb: () => void) { + if (!outsideContainer) return + function click(this: HTMLElement, e: HTMLElementEventMap["click"]) { + if (e.target !== this) return + e.preventDefault() + e.stopPropagation() + cb() + } + + function esc(e: HTMLElementEventMap["keydown"]) { + if (!e.key.startsWith("Esc")) return + e.preventDefault() + cb() + } + + outsideContainer?.addEventListener("click", click) + window.addCleanup(() => outsideContainer?.removeEventListener("click", click)) + document.addEventListener("keydown", esc) + window.addCleanup(() => document.removeEventListener("keydown", esc)) +} + +export function removeAllChildren(node: HTMLElement) { + while (node.firstChild) { + node.removeChild(node.firstChild) + } +} + +// AliasRedirect emits HTML redirects which also have the link[rel="canonical"] +// containing the URL it's redirecting to. +// Extracting it here with regex is _probably_ faster than parsing the entire HTML +// with a DOMParser effectively twice (here and later in the SPA code), even if +// way less robust - we only care about our own generated redirects after all. +const canonicalRegex = // + +export async function fetchCanonical(url: URL): Promise { + const res = await fetch(`${url}`) + if (!res.headers.get("content-type")?.startsWith("text/html")) { + return res + } + // reading the body can only be done once, so we need to clone the response + // to allow the caller to read it if it's was not a redirect + const text = await res.clone().text() + const [_, redirect] = text.match(canonicalRegex) ?? [] + return redirect ? fetch(`${new URL(redirect, url)}`) : res +} diff --git a/quartz/components/styles/backlinks.scss b/quartz/components/styles/backlinks.scss new file mode 100755 index 0000000..7b3237b --- /dev/null +++ b/quartz/components/styles/backlinks.scss @@ -0,0 +1,44 @@ +@use "../../styles/variables.scss" as *; + +.backlinks { + flex-direction: column; + /*&:after { + pointer-events: none; + content: ""; + width: 100%; + height: 50px; + position: absolute; + left: 0; + bottom: 0; + opacity: 1; + transition: opacity 0.3s ease; + background: linear-gradient(transparent 0px, var(--light)); + }*/ + + & > h3 { + font-size: 1rem; + margin: 0; + } + + & > ul { + list-style: none; + padding: 0; + margin: 0.5rem 0; + + & > li { + & > a { + background-color: transparent; + } + } + } + + & > .overflow { + &:after { + display: none; + } + height: auto; + @media all and not ($desktop) { + height: 250px; + } + } +} diff --git a/quartz/components/styles/breadcrumbs.scss b/quartz/components/styles/breadcrumbs.scss new file mode 100755 index 0000000..789808b --- /dev/null +++ b/quartz/components/styles/breadcrumbs.scss @@ -0,0 +1,22 @@ +.breadcrumb-container { + margin: 0; + margin-top: 0.75rem; + padding: 0; + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 0.5rem; +} + +.breadcrumb-element { + p { + margin: 0; + margin-left: 0.5rem; + padding: 0; + line-height: normal; + } + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +} diff --git a/quartz/components/styles/clipboard.scss b/quartz/components/styles/clipboard.scss new file mode 100755 index 0000000..196b894 --- /dev/null +++ b/quartz/components/styles/clipboard.scss @@ -0,0 +1,36 @@ +.clipboard-button { + position: absolute; + display: flex; + float: right; + right: 0; + padding: 0.4rem; + margin: 0.3rem; + color: var(--gray); + border-color: var(--dark); + background-color: var(--light); + border: 1px solid; + border-radius: 5px; + opacity: 0; + transition: 0.2s; + + & > svg { + fill: var(--light); + filter: contrast(0.3); + } + + &:hover { + cursor: pointer; + border-color: var(--secondary); + } + + &:focus { + outline: 0; + } +} + +pre { + &:hover > .clipboard-button { + opacity: 1; + transition: 0.2s; + } +} diff --git a/quartz/components/styles/contentMeta.scss b/quartz/components/styles/contentMeta.scss new file mode 100755 index 0000000..7874f9e --- /dev/null +++ b/quartz/components/styles/contentMeta.scss @@ -0,0 +1,14 @@ +.content-meta { + margin-top: 0; + color: var(--gray); + + &[show-comma="true"] { + > *:not(:last-child) { + margin-right: 8px; + + &::after { + content: ","; + } + } + } +} diff --git a/quartz/components/styles/darkmode.scss b/quartz/components/styles/darkmode.scss new file mode 100755 index 0000000..a62fbc6 --- /dev/null +++ b/quartz/components/styles/darkmode.scss @@ -0,0 +1,46 @@ +.darkmode { + cursor: pointer; + padding: 0; + position: relative; + background: none; + border: none; + width: 20px; + height: 20px; + margin: 0 10px; + text-align: inherit; + + & svg { + position: absolute; + width: 35px; + height: 20px; + top: calc(50% - 10px); + fill: var(--darkgray); + transition: opacity 0.1s ease; + } +} + +:root[saved-theme="dark"] { + color-scheme: dark; +} + +:root[saved-theme="light"] { + color-scheme: light; +} + +:root[saved-theme="dark"] .darkmode { + & > #dayIcon { + display: none; + } + & > #nightIcon { + display: inline; + } +} + +:root .darkmode { + & > #dayIcon { + display: inline; + } + & > #nightIcon { + display: none; + } +} diff --git a/quartz/components/styles/explorer.scss b/quartz/components/styles/explorer.scss new file mode 100755 index 0000000..397fd02 --- /dev/null +++ b/quartz/components/styles/explorer.scss @@ -0,0 +1,181 @@ +@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 { + background-color: transparent; + border: none; + text-align: left; + cursor: pointer; + padding: 0; + color: var(--dark); + display: flex; + align-items: center; + + & h2 { + font-size: 1rem; + display: inline-block; + margin: 0; + } + + & .fold { + margin-left: 0.5rem; + transition: transform 0.3s ease; + opacity: 0.8; + } + + &.collapsed .fold { + transform: rotateZ(-90deg); + } +} + +.folder-outer { + display: grid; + grid-template-rows: 0fr; + transition: grid-template-rows 0.3s ease-in-out; +} + +.folder-outer.open { + grid-template-rows: 1fr; +} + +.folder-outer > ul { + overflow: hidden; +} + +#explorer-content { + list-style: none; + overflow: hidden; + overflow-y: auto; + max-height: 100%; + transition: + max-height 0.35s ease, + visibility 0s linear 0s; + margin-top: 0.5rem; + visibility: visible; + + &.collapsed { + max-height: 0; + transition: + max-height 0.35s ease, + visibility 0s linear 0.35s; + visibility: hidden; + } + + & ul { + list-style: none; + margin: 0.08rem 0; + padding: 0; + transition: + max-height 0.35s ease, + transform 0.35s ease, + opacity 0.2s ease; + & li > a { + color: var(--dark); + opacity: 0.75; + pointer-events: all; + } + } + > #explorer-ul { + max-height: none; + } +} + +svg { + pointer-events: all; + + & > polyline { + pointer-events: none; + } +} + +.folder-container { + flex-direction: row; + display: flex; + align-items: center; + user-select: none; + + & div > a { + color: var(--secondary); + font-family: var(--headerFont); + font-size: 0.95rem; + font-weight: $semiBoldWeight; + line-height: 1.5rem; + display: inline-block; + } + + & div > a:hover { + color: var(--tertiary); + } + + & div > button { + color: var(--dark); + background-color: transparent; + border: none; + text-align: left; + cursor: pointer; + padding-left: 0; + padding-right: 0; + display: flex; + align-items: center; + font-family: var(--headerFont); + + & span { + font-size: 0.95rem; + display: inline-block; + color: var(--secondary); + font-weight: $semiBoldWeight; + margin: 0; + line-height: 1.5rem; + pointer-events: none; + } + } +} + +.folder-icon { + margin-right: 5px; + color: var(--secondary); + cursor: pointer; + transition: transform 0.3s ease; + backface-visibility: visible; +} + +li:has(> .folder-outer:not(.open)) > .folder-container > svg { + transform: rotate(-90deg); +} + +.folder-icon:hover { + color: var(--tertiary); +} + +.no-background::after { + background: none !important; +} + +#explorer-end { + // needs height so IntersectionObserver gets triggered + height: 4px; + // remove default margin from li + margin: 0; +} diff --git a/quartz/components/styles/footer.scss b/quartz/components/styles/footer.scss new file mode 100755 index 0000000..9c8dbf8 --- /dev/null +++ b/quartz/components/styles/footer.scss @@ -0,0 +1,15 @@ +footer { + text-align: left; + margin-bottom: 4rem; + opacity: 0.7; + + & ul { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: row; + gap: 1rem; + margin-top: -1rem; + } +} diff --git a/quartz/components/styles/graph.scss b/quartz/components/styles/graph.scss new file mode 100755 index 0000000..1b19f13 --- /dev/null +++ b/quartz/components/styles/graph.scss @@ -0,0 +1,73 @@ +@use "../../styles/variables.scss" as *; + +.graph { + & > h3 { + font-size: 1rem; + margin: 0; + } + + & > .graph-outer { + border-radius: 5px; + border: 1px solid var(--lightgray); + box-sizing: border-box; + height: 250px; + margin: 0.5em 0; + position: relative; + overflow: hidden; + + & > #global-graph-icon { + cursor: pointer; + background: none; + border: none; + color: var(--dark); + opacity: 0.5; + width: 24px; + height: 24px; + position: absolute; + padding: 0.2rem; + margin: 0.3rem; + top: 0; + right: 0; + border-radius: 4px; + background-color: transparent; + transition: background-color 0.5s ease; + cursor: pointer; + &:hover { + background-color: var(--lightgray); + } + } + } + + & > #global-graph-outer { + position: fixed; + z-index: 9999; + left: 0; + top: 0; + width: 100vw; + height: 100%; + backdrop-filter: blur(4px); + display: none; + overflow: hidden; + + &.active { + display: inline-block; + } + + & > #global-graph-container { + border: 1px solid var(--lightgray); + background-color: var(--light); + border-radius: 5px; + box-sizing: border-box; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + height: 80vh; + width: 80vw; + + @media all and not ($desktop) { + width: 90%; + } + } + } +} diff --git a/quartz/components/styles/legacyToc.scss b/quartz/components/styles/legacyToc.scss new file mode 100755 index 0000000..7a98f34 --- /dev/null +++ b/quartz/components/styles/legacyToc.scss @@ -0,0 +1,27 @@ +details#toc { + & summary { + cursor: pointer; + + &::marker { + color: var(--dark); + } + + & > * { + padding-left: 0.25rem; + display: inline-block; + margin: 0; + } + } + + & ul { + list-style: none; + margin: 0.5rem 1.25rem; + padding: 0; + } + + @for $i from 1 through 6 { + & .depth-#{$i} { + padding-left: calc(1rem * #{$i}); + } + } +} diff --git a/quartz/components/styles/listPage.scss b/quartz/components/styles/listPage.scss new file mode 100755 index 0000000..e86c39d --- /dev/null +++ b/quartz/components/styles/listPage.scss @@ -0,0 +1,40 @@ +@use "../../styles/variables.scss" as *; + +ul.section-ul { + list-style: none; + margin-top: 2em; + padding-left: 0; +} + +li.section-li { + margin-bottom: 1em; + + & > .section { + display: grid; + grid-template-columns: fit-content(8em) 3fr 1fr; + + @media all and ($mobile) { + & > .tags { + display: none; + } + } + + & > .desc > h3 > a { + background-color: transparent; + } + + & .meta { + margin: 0 1em 0 0; + opacity: 0.6; + } + } +} + +// modifications in popover context +.popover .section { + grid-template-columns: fit-content(8em) 1fr !important; + + & > .tags { + display: none; + } +} diff --git a/quartz/components/styles/mermaid.inline.scss b/quartz/components/styles/mermaid.inline.scss new file mode 100755 index 0000000..79a1c84 --- /dev/null +++ b/quartz/components/styles/mermaid.inline.scss @@ -0,0 +1,163 @@ +.expand-button { + position: absolute; + display: flex; + float: right; + padding: 0.4rem; + margin: 0.3rem; + right: 0; // NOTE: right will be set in mermaid.inline.ts + color: var(--gray); + border-color: var(--dark); + background-color: var(--light); + border: 1px solid; + border-radius: 5px; + opacity: 0; + transition: 0.2s; + + & > svg { + fill: var(--light); + filter: contrast(0.3); + } + + &:hover { + cursor: pointer; + border-color: var(--secondary); + } + + &:focus { + outline: 0; + } +} + +pre { + &:hover > .expand-button { + opacity: 1; + transition: 0.2s; + } +} + +#mermaid-container { + position: fixed; + contain: layout; + z-index: 999; + left: 0; + top: 0; + width: 100vw; + height: 100vh; + overflow: hidden; + display: none; + backdrop-filter: blur(4px); + background: rgba(0, 0, 0, 0.5); + + &.active { + display: inline-block; + } + + & > #mermaid-space { + display: grid; + width: 90%; + height: 90vh; + margin: 5vh auto; + background: var(--light); + box-shadow: + 0 14px 50px rgba(27, 33, 48, 0.12), + 0 10px 30px rgba(27, 33, 48, 0.16); + overflow: hidden; + position: relative; + + & > .mermaid-header { + display: flex; + justify-content: flex-end; + padding: 1rem; + border-bottom: 1px solid var(--lightgray); + background: var(--light); + z-index: 2; + max-height: fit-content; + + & > .close-button { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + background: transparent; + border: none; + border-radius: 4px; + color: var(--darkgray); + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background: var(--lightgray); + color: var(--dark); + } + } + } + + & > .mermaid-content { + padding: 2rem; + position: relative; + transform-origin: 0 0; + transition: transform 0.1s ease; + overflow: visible; + min-height: 200px; + min-width: 200px; + + pre { + margin: 0; + border: none; + } + + svg { + max-width: none; + height: auto; + } + } + + & > .mermaid-controls { + position: absolute; + bottom: 20px; + right: 20px; + display: flex; + gap: 8px; + padding: 8px; + background: var(--light); + border: 1px solid var(--lightgray); + border-radius: 6px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + z-index: 2; + + .mermaid-control-button { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + border: 1px solid var(--lightgray); + background: var(--light); + color: var(--dark); + border-radius: 4px; + cursor: pointer; + font-size: 16px; + font-family: var(--bodyFont); + transition: all 0.2s ease; + + &:hover { + background: var(--lightgray); + } + + &:active { + transform: translateY(1px); + } + + // Style the reset button differently + &:nth-child(2) { + width: auto; + padding: 0 12px; + font-size: 14px; + } + } + } + } +} diff --git a/quartz/components/styles/popover.scss b/quartz/components/styles/popover.scss new file mode 100755 index 0000000..38d6126 --- /dev/null +++ b/quartz/components/styles/popover.scss @@ -0,0 +1,83 @@ +@use "../../styles/variables.scss" as *; + +@keyframes dropin { + 0% { + opacity: 0; + visibility: hidden; + } + 1% { + opacity: 0; + } + 100% { + opacity: 1; + visibility: visible; + } +} + +.popover { + z-index: 999; + position: absolute; + overflow: visible; + padding: 1rem; + + & > .popover-inner { + position: relative; + width: 30rem; + max-height: 20rem; + padding: 0 1rem 1rem 1rem; + font-weight: initial; + font-style: initial; + line-height: normal; + font-size: initial; + font-family: var(--bodyFont); + border: 1px solid var(--lightgray); + background-color: var(--light); + border-radius: 5px; + box-shadow: 6px 6px 36px 0 rgba(0, 0, 0, 0.25); + overflow: auto; + white-space: normal; + } + + & > .popover-inner[data-content-type] { + &[data-content-type*="pdf"], + &[data-content-type*="image"] { + padding: 0; + max-height: 100%; + } + + &[data-content-type*="image"] { + img { + margin: 0; + border-radius: 0; + display: block; + } + } + + &[data-content-type*="pdf"] { + iframe { + width: 100%; + } + } + } + + h1 { + font-size: 1.5rem; + } + + visibility: hidden; + opacity: 0; + transition: + opacity 0.3s ease, + visibility 0.3s ease; + + @media all and ($mobile) { + display: none !important; + } +} + +a:hover .popover, +.popover:hover { + animation: dropin 0.3s ease; + animation-fill-mode: forwards; + animation-delay: 0.2s; +} diff --git a/quartz/components/styles/recentNotes.scss b/quartz/components/styles/recentNotes.scss new file mode 100755 index 0000000..7267671 --- /dev/null +++ b/quartz/components/styles/recentNotes.scss @@ -0,0 +1,24 @@ +.recent-notes { + & > h3 { + margin: 0.5rem 0 0 0; + font-size: 1rem; + } + + & > ul.recent-ul { + list-style: none; + margin-top: 1rem; + padding-left: 0; + + & > li { + margin: 1rem 0; + .section > .desc > h3 > a { + background-color: transparent; + } + + .section > .meta { + margin: 0 0 0.5rem 0; + opacity: 0.6; + } + } + } +} diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss new file mode 100755 index 0000000..3f9ac53 --- /dev/null +++ b/quartz/components/styles/search.scss @@ -0,0 +1,234 @@ +@use "../../styles/variables.scss" as *; + +.search { + min-width: fit-content; + max-width: 14rem; + @media all and ($mobile) { + flex-grow: 0.3; + } + + & > .search-button { + background-color: var(--light); + border: none; + border-radius: 4px; + font-family: inherit; + font-size: inherit; + height: 2rem; + padding: 0; + display: flex; + align-items: center; + text-align: inherit; + cursor: pointer; + white-space: nowrap; + width: 100%; + justify-content: space-between; + + & > p { + display: inline; + padding: 0 1rem; + } + + & svg { + cursor: pointer; + width: 18px; + min-width: 18px; + margin: 0 0.5rem; + + .search-path { + stroke: var(--darkgray); + stroke-width: 2px; + transition: stroke 0.5s ease; + } + } + } + + & > #search-container { + position: fixed; + contain: layout; + z-index: 999; + left: 0; + top: 0; + width: 100vw; + height: 100vh; + overflow-y: auto; + display: none; + backdrop-filter: blur(4px); + + &.active { + display: inline-block; + } + + & > #search-space { + width: 65%; + margin-top: 12vh; + margin-left: auto; + margin-right: auto; + + @media all and not ($desktop) { + width: 90%; + } + + & > * { + width: 100%; + border-radius: 7px; + background: var(--light); + box-shadow: + 0 14px 50px rgba(27, 33, 48, 0.12), + 0 10px 30px rgba(27, 33, 48, 0.16); + margin-bottom: 2em; + } + + & > input { + box-sizing: border-box; + padding: 0.5em 1em; + font-family: var(--bodyFont); + color: var(--dark); + font-size: 1.1em; + border: 1px solid var(--lightgray); + + &:focus { + outline: none; + } + } + + & > #search-layout { + display: none; + flex-direction: row; + border: 1px solid var(--lightgray); + flex: 0 0 100%; + box-sizing: border-box; + + &.display-results { + display: flex; + } + + &[data-preview] > #results-container { + flex: 0 0 min(30%, 450px); + } + + @media all and not ($mobile) { + &[data-preview] { + & .result-card > p.preview { + display: none; + } + + & > div { + &:first-child { + border-right: 1px solid var(--lightgray); + border-top-right-radius: unset; + border-bottom-right-radius: unset; + } + + &:last-child { + border-top-left-radius: unset; + border-bottom-left-radius: unset; + } + } + } + } + + & > div { + height: calc(75vh - 12vh); + border-radius: 5px; + } + + @media all and ($mobile) { + & > #preview-container { + display: none !important; + } + + &[data-preview] > #results-container { + width: 100%; + height: auto; + flex: 0 0 100%; + } + } + + & .highlight { + background: color-mix(in srgb, var(--tertiary) 60%, rgba(255, 255, 255, 0)); + border-radius: 5px; + scroll-margin-top: 2rem; + } + + & > #preview-container { + flex-grow: 1; + display: block; + overflow: hidden; + font-family: inherit; + color: var(--dark); + line-height: 1.5em; + font-weight: $normalWeight; + overflow-y: auto; + padding: 0 2rem; + + & .preview-inner { + margin: 0 auto; + width: min($pageWidth, 100%); + } + + a[role="anchor"] { + background-color: transparent; + } + } + + & > #results-container { + overflow-y: auto; + + & .result-card { + overflow: hidden; + padding: 1em; + cursor: pointer; + transition: background 0.2s ease; + border-bottom: 1px solid var(--lightgray); + width: 100%; + display: block; + box-sizing: border-box; + + // normalize card props + font-family: inherit; + font-size: 100%; + line-height: 1.15; + margin: 0; + text-transform: none; + text-align: left; + outline: none; + font-weight: inherit; + + &:hover, + &:focus, + &.focus { + background: var(--lightgray); + } + + & > h3 { + margin: 0; + } + + & > ul.tags { + margin-top: 0.45rem; + margin-bottom: 0; + } + + & > ul > li > p { + border-radius: 8px; + background-color: var(--highlight); + padding: 0.2rem 0.4rem; + margin: 0 0.1rem; + line-height: 1.4rem; + font-weight: $boldWeight; + color: var(--secondary); + + &.match-tag { + color: var(--tertiary); + } + } + + & > p { + margin-bottom: 0; + } + } + } + } + } + } +} diff --git a/quartz/components/styles/toc.scss b/quartz/components/styles/toc.scss new file mode 100755 index 0000000..4988cd8 --- /dev/null +++ b/quartz/components/styles/toc.scss @@ -0,0 +1,93 @@ +@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 { + background-color: transparent; + border: none; + text-align: left; + cursor: pointer; + padding: 0; + color: var(--dark); + display: flex; + align-items: center; + + & h3 { + font-size: 1rem; + display: inline-block; + margin: 0; + } + + & .fold { + margin-left: 0.5rem; + transition: transform 0.3s ease; + opacity: 0.8; + } + + &.collapsed .fold { + transform: rotateZ(-90deg); + } +} + +#toc-content { + list-style: none; + overflow: hidden; + overflow-y: auto; + max-height: 100%; + transition: + max-height 0.35s ease, + visibility 0s linear 0s; + position: relative; + visibility: visible; + + &.collapsed { + max-height: 0; + transition: + max-height 0.35s ease, + visibility 0s linear 0.35s; + visibility: hidden; + } + + &.collapsed > .overflow::after { + opacity: 0; + } + + & ul { + list-style: none; + margin: 0.5rem 0; + padding: 0; + & > li > a { + color: var(--dark); + opacity: 0.35; + transition: + 0.5s ease opacity, + 0.3s ease color; + &.in-view { + opacity: 0.75; + } + } + } + > ul.overflow { + max-height: none; + width: 100%; + } + + @for $i from 0 through 6 { + & .depth-#{$i} { + padding-left: calc(1rem * #{$i}); + } + } +} diff --git a/quartz/components/types.ts b/quartz/components/types.ts new file mode 100755 index 0000000..a6b90d3 --- /dev/null +++ b/quartz/components/types.ts @@ -0,0 +1,29 @@ +import { ComponentType, JSX } from "preact" +import { StaticResources } from "../util/resources" +import { QuartzPluginData } from "../plugins/vfile" +import { GlobalConfiguration } from "../cfg" +import { Node } from "hast" +import { BuildCtx } from "../util/ctx" + +export type QuartzComponentProps = { + ctx: BuildCtx + externalResources: StaticResources + fileData: QuartzPluginData + cfg: GlobalConfiguration + children: (QuartzComponent | JSX.Element)[] + tree: Node + allFiles: QuartzPluginData[] + displayClass?: "mobile-only" | "desktop-only" +} & JSX.IntrinsicAttributes & { + [key: string]: any + } + +export type QuartzComponent = ComponentType & { + css?: string + beforeDOMLoaded?: string + afterDOMLoaded?: string +} + +export type QuartzComponentConstructor = ( + opts: Options, +) => QuartzComponent diff --git a/quartz/depgraph.test.ts b/quartz/depgraph.test.ts new file mode 100755 index 0000000..062f13e --- /dev/null +++ b/quartz/depgraph.test.ts @@ -0,0 +1,118 @@ +import test, { describe } from "node:test" +import DepGraph from "./depgraph" +import assert from "node:assert" + +describe("DepGraph", () => { + test("getLeafNodes", () => { + const graph = new DepGraph() + graph.addEdge("A", "B") + graph.addEdge("B", "C") + graph.addEdge("D", "C") + assert.deepStrictEqual(graph.getLeafNodes("A"), new Set(["C"])) + assert.deepStrictEqual(graph.getLeafNodes("B"), new Set(["C"])) + assert.deepStrictEqual(graph.getLeafNodes("C"), new Set(["C"])) + assert.deepStrictEqual(graph.getLeafNodes("D"), new Set(["C"])) + }) + + describe("getLeafNodeAncestors", () => { + test("gets correct ancestors in a graph without cycles", () => { + const graph = new DepGraph() + graph.addEdge("A", "B") + graph.addEdge("B", "C") + graph.addEdge("D", "B") + assert.deepStrictEqual(graph.getLeafNodeAncestors("A"), new Set(["A", "B", "D"])) + assert.deepStrictEqual(graph.getLeafNodeAncestors("B"), new Set(["A", "B", "D"])) + assert.deepStrictEqual(graph.getLeafNodeAncestors("C"), new Set(["A", "B", "D"])) + assert.deepStrictEqual(graph.getLeafNodeAncestors("D"), new Set(["A", "B", "D"])) + }) + + test("gets correct ancestors in a graph with cycles", () => { + const graph = new DepGraph() + graph.addEdge("A", "B") + graph.addEdge("B", "C") + graph.addEdge("C", "A") + graph.addEdge("C", "D") + assert.deepStrictEqual(graph.getLeafNodeAncestors("A"), new Set(["A", "B", "C"])) + assert.deepStrictEqual(graph.getLeafNodeAncestors("B"), new Set(["A", "B", "C"])) + assert.deepStrictEqual(graph.getLeafNodeAncestors("C"), new Set(["A", "B", "C"])) + assert.deepStrictEqual(graph.getLeafNodeAncestors("D"), new Set(["A", "B", "C"])) + }) + }) + + describe("mergeGraph", () => { + test("merges two graphs", () => { + const graph = new DepGraph() + graph.addEdge("A.md", "A.html") + + const other = new DepGraph() + other.addEdge("B.md", "B.html") + + graph.mergeGraph(other) + + const expected = { + nodes: ["A.md", "A.html", "B.md", "B.html"], + edges: [ + ["A.md", "A.html"], + ["B.md", "B.html"], + ], + } + + assert.deepStrictEqual(graph.export(), expected) + }) + }) + + describe("updateIncomingEdgesForNode", () => { + test("merges when node exists", () => { + // A.md -> B.md -> B.html + const graph = new DepGraph() + graph.addEdge("A.md", "B.md") + graph.addEdge("B.md", "B.html") + + // B.md is edited so it removes the A.md transclusion + // and adds C.md transclusion + // C.md -> B.md + const other = new DepGraph() + other.addEdge("C.md", "B.md") + other.addEdge("B.md", "B.html") + + // A.md -> B.md removed, C.md -> B.md added + // C.md -> B.md -> B.html + graph.updateIncomingEdgesForNode(other, "B.md") + + const expected = { + nodes: ["A.md", "B.md", "B.html", "C.md"], + edges: [ + ["B.md", "B.html"], + ["C.md", "B.md"], + ], + } + + assert.deepStrictEqual(graph.export(), expected) + }) + + test("adds node if it does not exist", () => { + // A.md -> B.md + const graph = new DepGraph() + graph.addEdge("A.md", "B.md") + + // Add a new file C.md that transcludes B.md + // B.md -> C.md + const other = new DepGraph() + other.addEdge("B.md", "C.md") + + // B.md -> C.md added + // A.md -> B.md -> C.md + graph.updateIncomingEdgesForNode(other, "C.md") + + const expected = { + nodes: ["A.md", "B.md", "C.md"], + edges: [ + ["A.md", "B.md"], + ["B.md", "C.md"], + ], + } + + assert.deepStrictEqual(graph.export(), expected) + }) + }) +}) diff --git a/quartz/depgraph.ts b/quartz/depgraph.ts new file mode 100755 index 0000000..3d048cd --- /dev/null +++ b/quartz/depgraph.ts @@ -0,0 +1,228 @@ +export default class DepGraph { + // node: incoming and outgoing edges + _graph = new Map; outgoing: Set }>() + + constructor() { + this._graph = new Map() + } + + export(): Object { + return { + nodes: this.nodes, + edges: this.edges, + } + } + + toString(): string { + return JSON.stringify(this.export(), null, 2) + } + + // BASIC GRAPH OPERATIONS + + get nodes(): T[] { + return Array.from(this._graph.keys()) + } + + get edges(): [T, T][] { + let edges: [T, T][] = [] + this.forEachEdge((edge) => edges.push(edge)) + return edges + } + + hasNode(node: T): boolean { + return this._graph.has(node) + } + + addNode(node: T): void { + if (!this._graph.has(node)) { + this._graph.set(node, { incoming: new Set(), outgoing: new Set() }) + } + } + + // Remove node and all edges connected to it + removeNode(node: T): void { + if (this._graph.has(node)) { + // first remove all edges so other nodes don't have references to this node + for (const target of this._graph.get(node)!.outgoing) { + this.removeEdge(node, target) + } + for (const source of this._graph.get(node)!.incoming) { + this.removeEdge(source, node) + } + this._graph.delete(node) + } + } + + forEachNode(callback: (node: T) => void): void { + for (const node of this._graph.keys()) { + callback(node) + } + } + + hasEdge(from: T, to: T): boolean { + return Boolean(this._graph.get(from)?.outgoing.has(to)) + } + + addEdge(from: T, to: T): void { + this.addNode(from) + this.addNode(to) + + this._graph.get(from)!.outgoing.add(to) + this._graph.get(to)!.incoming.add(from) + } + + removeEdge(from: T, to: T): void { + if (this._graph.has(from) && this._graph.has(to)) { + this._graph.get(from)!.outgoing.delete(to) + this._graph.get(to)!.incoming.delete(from) + } + } + + // returns -1 if node does not exist + outDegree(node: T): number { + return this.hasNode(node) ? this._graph.get(node)!.outgoing.size : -1 + } + + // returns -1 if node does not exist + inDegree(node: T): number { + return this.hasNode(node) ? this._graph.get(node)!.incoming.size : -1 + } + + forEachOutNeighbor(node: T, callback: (neighbor: T) => void): void { + this._graph.get(node)?.outgoing.forEach(callback) + } + + forEachInNeighbor(node: T, callback: (neighbor: T) => void): void { + this._graph.get(node)?.incoming.forEach(callback) + } + + forEachEdge(callback: (edge: [T, T]) => void): void { + for (const [source, { outgoing }] of this._graph.entries()) { + for (const target of outgoing) { + callback([source, target]) + } + } + } + + // DEPENDENCY ALGORITHMS + + // Add all nodes and edges from other graph to this graph + mergeGraph(other: DepGraph): void { + other.forEachEdge(([source, target]) => { + this.addNode(source) + this.addNode(target) + this.addEdge(source, target) + }) + } + + // For the node provided: + // If node does not exist, add it + // If an incoming edge was added in other, it is added in this graph + // If an incoming edge was deleted in other, it is deleted in this graph + updateIncomingEdgesForNode(other: DepGraph, node: T): void { + this.addNode(node) + + // Add edge if it is present in other + other.forEachInNeighbor(node, (neighbor) => { + this.addEdge(neighbor, node) + }) + + // For node provided, remove incoming edge if it is absent in other + this.forEachEdge(([source, target]) => { + if (target === node && !other.hasEdge(source, target)) { + this.removeEdge(source, target) + } + }) + } + + // Remove all nodes that do not have any incoming or outgoing edges + // A node may be orphaned if the only node pointing to it was removed + removeOrphanNodes(): Set { + let orphanNodes = new Set() + + this.forEachNode((node) => { + if (this.inDegree(node) === 0 && this.outDegree(node) === 0) { + orphanNodes.add(node) + } + }) + + orphanNodes.forEach((node) => { + this.removeNode(node) + }) + + return orphanNodes + } + + // Get all leaf nodes (i.e. destination paths) reachable from the node provided + // Eg. if the graph is A -> B -> C + // D ---^ + // and the node is B, this function returns [C] + getLeafNodes(node: T): Set { + let stack: T[] = [node] + let visited = new Set() + let leafNodes = new Set() + + // DFS + while (stack.length > 0) { + let node = stack.pop()! + + // If the node is already visited, skip it + if (visited.has(node)) { + continue + } + visited.add(node) + + // Check if the node is a leaf node (i.e. destination path) + if (this.outDegree(node) === 0) { + leafNodes.add(node) + } + + // Add all unvisited neighbors to the stack + this.forEachOutNeighbor(node, (neighbor) => { + if (!visited.has(neighbor)) { + stack.push(neighbor) + } + }) + } + + return leafNodes + } + + // Get all ancestors of the leaf nodes reachable from the node provided + // Eg. if the graph is A -> B -> C + // D ---^ + // and the node is B, this function returns [A, B, D] + getLeafNodeAncestors(node: T): Set { + const leafNodes = this.getLeafNodes(node) + let visited = new Set() + let upstreamNodes = new Set() + + // Backwards DFS for each leaf node + leafNodes.forEach((leafNode) => { + let stack: T[] = [leafNode] + + while (stack.length > 0) { + let node = stack.pop()! + + if (visited.has(node)) { + continue + } + visited.add(node) + // Add node if it's not a leaf node (i.e. destination path) + // Assumes destination file cannot depend on another destination file + if (this.outDegree(node) !== 0) { + upstreamNodes.add(node) + } + + // Add all unvisited parents to the stack + this.forEachInNeighbor(node, (parentNode) => { + if (!visited.has(parentNode)) { + stack.push(parentNode) + } + }) + } + }) + + return upstreamNodes + } +} diff --git a/quartz/i18n/index.ts b/quartz/i18n/index.ts new file mode 100755 index 0000000..7c8996b --- /dev/null +++ b/quartz/i18n/index.ts @@ -0,0 +1,80 @@ +import { Translation, CalloutTranslation } from "./locales/definition" +import enUs from "./locales/en-US" +import enGb from "./locales/en-GB" +import fr from "./locales/fr-FR" +import it from "./locales/it-IT" +import ja from "./locales/ja-JP" +import de from "./locales/de-DE" +import nl from "./locales/nl-NL" +import ro from "./locales/ro-RO" +import ca from "./locales/ca-ES" +import es from "./locales/es-ES" +import ar from "./locales/ar-SA" +import uk from "./locales/uk-UA" +import ru from "./locales/ru-RU" +import ko from "./locales/ko-KR" +import zh from "./locales/zh-CN" +import zhTw from "./locales/zh-TW" +import vi from "./locales/vi-VN" +import pt from "./locales/pt-BR" +import hu from "./locales/hu-HU" +import fa from "./locales/fa-IR" +import pl from "./locales/pl-PL" +import cs from "./locales/cs-CZ" +import tr from "./locales/tr-TR" +import th from "./locales/th-TH" +import lt from "./locales/lt-LT" + +export const TRANSLATIONS = { + "en-US": enUs, + "en-GB": enGb, + "fr-FR": fr, + "it-IT": it, + "ja-JP": ja, + "de-DE": de, + "nl-NL": nl, + "nl-BE": nl, + "ro-RO": ro, + "ro-MD": ro, + "ca-ES": ca, + "es-ES": es, + "ar-SA": ar, + "ar-AE": ar, + "ar-QA": ar, + "ar-BH": ar, + "ar-KW": ar, + "ar-OM": ar, + "ar-YE": ar, + "ar-IR": ar, + "ar-SY": ar, + "ar-IQ": ar, + "ar-JO": ar, + "ar-PL": ar, + "ar-LB": ar, + "ar-EG": ar, + "ar-SD": ar, + "ar-LY": ar, + "ar-MA": ar, + "ar-TN": ar, + "ar-DZ": ar, + "ar-MR": ar, + "uk-UA": uk, + "ru-RU": ru, + "ko-KR": ko, + "zh-CN": zh, + "zh-TW": zhTw, + "vi-VN": vi, + "pt-BR": pt, + "hu-HU": hu, + "fa-IR": fa, + "pl-PL": pl, + "cs-CZ": cs, + "tr-TR": tr, + "th-TH": th, + "lt-LT": lt, +} as const + +export const defaultTranslation = "en-US" +export const i18n = (locale: ValidLocale): Translation => TRANSLATIONS[locale ?? defaultTranslation] +export type ValidLocale = keyof typeof TRANSLATIONS +export type ValidCallout = keyof CalloutTranslation diff --git a/quartz/i18n/locales/ar-SA.ts b/quartz/i18n/locales/ar-SA.ts new file mode 100755 index 0000000..8463e2f --- /dev/null +++ b/quartz/i18n/locales/ar-SA.ts @@ -0,0 +1,89 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "غير معنون", + description: "لم يتم تقديم أي وصف", + }, + components: { + callout: { + note: "ملاحظة", + abstract: "ملخص", + info: "معلومات", + todo: "للقيام", + tip: "نصيحة", + success: "نجاح", + question: "سؤال", + warning: "تحذير", + failure: "فشل", + danger: "خطر", + bug: "خلل", + example: "مثال", + quote: "اقتباس", + }, + backlinks: { + title: "وصلات العودة", + noBacklinksFound: "لا يوجد وصلات عودة", + }, + themeToggle: { + lightMode: "الوضع النهاري", + darkMode: "الوضع الليلي", + }, + explorer: { + title: "المستعرض", + }, + footer: { + createdWith: "أُنشئ باستخدام", + }, + graph: { + title: "التمثيل التفاعلي", + }, + recentNotes: { + title: "آخر الملاحظات", + seeRemainingMore: ({ remaining }) => `تصفح ${remaining} أكثر →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `مقتبس من ${targetSlug}`, + linkToOriginal: "وصلة للملاحظة الرئيسة", + }, + search: { + title: "بحث", + searchBarPlaceholder: "ابحث عن شيء ما", + }, + tableOfContents: { + title: "فهرس المحتويات", + }, + contentMeta: { + readingTime: ({ minutes }) => + minutes == 1 + ? `دقيقة أو أقل للقراءة` + : minutes == 2 + ? `دقيقتان للقراءة` + : `${minutes} دقائق للقراءة`, + }, + }, + pages: { + rss: { + recentNotes: "آخر الملاحظات", + lastFewNotes: ({ count }) => `آخر ${count} ملاحظة`, + }, + error: { + title: "غير موجود", + notFound: "إما أن هذه الصفحة خاصة أو غير موجودة.", + home: "العوده للصفحة الرئيسية", + }, + folderContent: { + folder: "مجلد", + itemsUnderFolder: ({ count }) => + count === 1 ? "يوجد عنصر واحد فقط تحت هذا المجلد" : `يوجد ${count} عناصر تحت هذا المجلد.`, + }, + tagContent: { + tag: "الوسم", + tagIndex: "مؤشر الوسم", + itemsUnderTag: ({ count }) => + count === 1 ? "يوجد عنصر واحد فقط تحت هذا الوسم" : `يوجد ${count} عناصر تحت هذا الوسم.`, + showingFirst: ({ count }) => `إظهار أول ${count} أوسمة.`, + totalTags: ({ count }) => `يوجد ${count} أوسمة.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/ca-ES.ts b/quartz/i18n/locales/ca-ES.ts new file mode 100755 index 0000000..aadbd41 --- /dev/null +++ b/quartz/i18n/locales/ca-ES.ts @@ -0,0 +1,84 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Sense títol", + description: "Sense descripció", + }, + components: { + callout: { + note: "Nota", + abstract: "Resum", + info: "Informació", + todo: "Per fer", + tip: "Consell", + success: "Èxit", + question: "Pregunta", + warning: "Advertència", + failure: "Fall", + danger: "Perill", + bug: "Error", + example: "Exemple", + quote: "Cita", + }, + backlinks: { + title: "Retroenllaç", + noBacklinksFound: "No s'han trobat retroenllaços", + }, + themeToggle: { + lightMode: "Mode clar", + darkMode: "Mode fosc", + }, + explorer: { + title: "Explorador", + }, + footer: { + createdWith: "Creat amb", + }, + graph: { + title: "Vista Gràfica", + }, + recentNotes: { + title: "Notes Recents", + seeRemainingMore: ({ remaining }) => `Vegi ${remaining} més →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Transcluit de ${targetSlug}`, + linkToOriginal: "Enllaç a l'original", + }, + search: { + title: "Cercar", + searchBarPlaceholder: "Cerca alguna cosa", + }, + tableOfContents: { + title: "Taula de Continguts", + }, + contentMeta: { + readingTime: ({ minutes }) => `Es llegeix en ${minutes} min`, + }, + }, + pages: { + rss: { + recentNotes: "Notes recents", + lastFewNotes: ({ count }) => `Últimes ${count} notes`, + }, + error: { + title: "No s'ha trobat.", + notFound: "Aquesta pàgina és privada o no existeix.", + home: "Torna a la pàgina principal", + }, + folderContent: { + folder: "Carpeta", + itemsUnderFolder: ({ count }) => + count === 1 ? "1 article en aquesta carpeta." : `${count} articles en esta carpeta.`, + }, + tagContent: { + tag: "Etiqueta", + tagIndex: "índex d'Etiquetes", + itemsUnderTag: ({ count }) => + count === 1 ? "1 article amb aquesta etiqueta." : `${count} article amb aquesta etiqueta.`, + showingFirst: ({ count }) => `Mostrant les primeres ${count} etiquetes.`, + totalTags: ({ count }) => `S'han trobat ${count} etiquetes en total.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/cs-CZ.ts b/quartz/i18n/locales/cs-CZ.ts new file mode 100755 index 0000000..bf089d1 --- /dev/null +++ b/quartz/i18n/locales/cs-CZ.ts @@ -0,0 +1,84 @@ +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 diff --git a/quartz/i18n/locales/de-DE.ts b/quartz/i18n/locales/de-DE.ts new file mode 100755 index 0000000..023d4be --- /dev/null +++ b/quartz/i18n/locales/de-DE.ts @@ -0,0 +1,84 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Unbenannt", + description: "Keine Beschreibung angegeben", + }, + components: { + callout: { + note: "Hinweis", + abstract: "Zusammenfassung", + info: "Info", + todo: "Zu erledigen", + tip: "Tipp", + success: "Erfolg", + question: "Frage", + warning: "Warnung", + failure: "Misserfolg", + danger: "Gefahr", + bug: "Fehler", + example: "Beispiel", + quote: "Zitat", + }, + backlinks: { + title: "Backlinks", + noBacklinksFound: "Keine Backlinks gefunden", + }, + themeToggle: { + lightMode: "Light Mode", + darkMode: "Dark Mode", + }, + explorer: { + title: "Explorer", + }, + footer: { + createdWith: "Erstellt mit", + }, + graph: { + title: "Graphansicht", + }, + recentNotes: { + title: "Zuletzt bearbeitete Seiten", + seeRemainingMore: ({ remaining }) => `${remaining} weitere ansehen →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Transklusion von ${targetSlug}`, + linkToOriginal: "Link zum Original", + }, + search: { + title: "Suche", + searchBarPlaceholder: "Suche nach etwas", + }, + tableOfContents: { + title: "Inhaltsverzeichnis", + }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes} min read`, + }, + }, + pages: { + rss: { + recentNotes: "Zuletzt bearbeitete Seiten", + lastFewNotes: ({ count }) => `Letzte ${count} Seiten`, + }, + error: { + title: "Nicht gefunden", + notFound: "Diese Seite ist entweder nicht öffentlich oder existiert nicht.", + home: "Return to Homepage", + }, + folderContent: { + folder: "Ordner", + itemsUnderFolder: ({ count }) => + count === 1 ? "1 Datei in diesem Ordner." : `${count} Dateien in diesem Ordner.`, + }, + tagContent: { + tag: "Tag", + tagIndex: "Tag-Übersicht", + itemsUnderTag: ({ count }) => + count === 1 ? "1 Datei mit diesem Tag." : `${count} Dateien mit diesem Tag.`, + showingFirst: ({ count }) => `Die ersten ${count} Tags werden angezeigt.`, + totalTags: ({ count }) => `${count} Tags insgesamt.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/definition.ts b/quartz/i18n/locales/definition.ts new file mode 100755 index 0000000..25a8cd7 --- /dev/null +++ b/quartz/i18n/locales/definition.ts @@ -0,0 +1,84 @@ +import { FullSlug } from "../../util/path" + +export interface CalloutTranslation { + note: string + abstract: string + info: string + todo: string + tip: string + success: string + question: string + warning: string + failure: string + danger: string + bug: string + example: string + quote: string +} + +export interface Translation { + propertyDefaults: { + title: string + description: string + } + components: { + callout: CalloutTranslation + backlinks: { + title: string + noBacklinksFound: string + } + themeToggle: { + lightMode: string + darkMode: string + } + explorer: { + title: string + } + footer: { + createdWith: string + } + graph: { + title: string + } + recentNotes: { + title: string + seeRemainingMore: (variables: { remaining: number }) => string + } + transcludes: { + transcludeOf: (variables: { targetSlug: FullSlug }) => string + linkToOriginal: string + } + search: { + title: string + searchBarPlaceholder: string + } + tableOfContents: { + title: string + } + contentMeta: { + readingTime: (variables: { minutes: number }) => string + } + } + pages: { + rss: { + recentNotes: string + lastFewNotes: (variables: { count: number }) => string + } + error: { + title: string + notFound: string + home: string + } + folderContent: { + folder: string + itemsUnderFolder: (variables: { count: number }) => string + } + tagContent: { + tag: string + tagIndex: string + itemsUnderTag: (variables: { count: number }) => string + showingFirst: (variables: { count: number }) => string + totalTags: (variables: { count: number }) => string + } + } +} diff --git a/quartz/i18n/locales/en-GB.ts b/quartz/i18n/locales/en-GB.ts new file mode 100755 index 0000000..5388b03 --- /dev/null +++ b/quartz/i18n/locales/en-GB.ts @@ -0,0 +1,84 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Untitled", + description: "No description provided", + }, + components: { + callout: { + note: "Note", + abstract: "Abstract", + info: "Info", + todo: "To-Do", + tip: "Tip", + success: "Success", + question: "Question", + warning: "Warning", + failure: "Failure", + danger: "Danger", + bug: "Bug", + example: "Example", + quote: "Quote", + }, + backlinks: { + title: "Backlinks", + noBacklinksFound: "No backlinks found", + }, + themeToggle: { + lightMode: "Light mode", + darkMode: "Dark mode", + }, + explorer: { + title: "Explorer", + }, + footer: { + createdWith: "Created with", + }, + graph: { + title: "Graph View", + }, + recentNotes: { + title: "Recent Notes", + seeRemainingMore: ({ remaining }) => `See ${remaining} more →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Transclude of ${targetSlug}`, + linkToOriginal: "Link to original", + }, + search: { + title: "Search", + searchBarPlaceholder: "Search for something", + }, + tableOfContents: { + title: "Table of Contents", + }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes} min read`, + }, + }, + pages: { + rss: { + recentNotes: "Recent notes", + lastFewNotes: ({ count }) => `Last ${count} notes`, + }, + error: { + title: "Not Found", + notFound: "Either this page is private or doesn't exist.", + home: "Return to Homepage", + }, + folderContent: { + folder: "Folder", + itemsUnderFolder: ({ count }) => + count === 1 ? "1 item under this folder." : `${count} items under this folder.`, + }, + tagContent: { + tag: "Tag", + tagIndex: "Tag Index", + itemsUnderTag: ({ count }) => + count === 1 ? "1 item with this tag." : `${count} items with this tag.`, + showingFirst: ({ count }) => `Showing first ${count} tags.`, + totalTags: ({ count }) => `Found ${count} total tags.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/en-US.ts b/quartz/i18n/locales/en-US.ts new file mode 100755 index 0000000..22cf31e --- /dev/null +++ b/quartz/i18n/locales/en-US.ts @@ -0,0 +1,84 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Untitled", + description: "No description provided", + }, + components: { + callout: { + note: "Note", + abstract: "Abstract", + info: "Info", + todo: "Todo", + tip: "Tip", + success: "Success", + question: "Question", + warning: "Warning", + failure: "Failure", + danger: "Danger", + bug: "Bug", + example: "Example", + quote: "Quote", + }, + backlinks: { + title: "Backlinks", + noBacklinksFound: "No backlinks found", + }, + themeToggle: { + lightMode: "Light mode", + darkMode: "Dark mode", + }, + explorer: { + title: "Explorer", + }, + footer: { + createdWith: "Created with", + }, + graph: { + title: "Graph View", + }, + recentNotes: { + title: "Recent Notes", + seeRemainingMore: ({ remaining }) => `See ${remaining} more →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Transclude of ${targetSlug}`, + linkToOriginal: "Link to original", + }, + search: { + title: "Search", + searchBarPlaceholder: "Search for something", + }, + tableOfContents: { + title: "Table of Contents", + }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes} min read`, + }, + }, + pages: { + rss: { + recentNotes: "Recent notes", + lastFewNotes: ({ count }) => `Last ${count} notes`, + }, + error: { + title: "Not Found", + notFound: "Either this page is private or doesn't exist.", + home: "Return to Homepage", + }, + folderContent: { + folder: "Folder", + itemsUnderFolder: ({ count }) => + count === 1 ? "1 item under this folder." : `${count} items under this folder.`, + }, + tagContent: { + tag: "Tag", + tagIndex: "Tag Index", + itemsUnderTag: ({ count }) => + count === 1 ? "1 item with this tag." : `${count} items with this tag.`, + showingFirst: ({ count }) => `Showing first ${count} tags.`, + totalTags: ({ count }) => `Found ${count} total tags.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/es-ES.ts b/quartz/i18n/locales/es-ES.ts new file mode 100755 index 0000000..c4a57aa --- /dev/null +++ b/quartz/i18n/locales/es-ES.ts @@ -0,0 +1,84 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Sin título", + description: "Sin descripción", + }, + components: { + callout: { + note: "Nota", + abstract: "Resumen", + info: "Información", + todo: "Por hacer", + tip: "Consejo", + success: "Éxito", + question: "Pregunta", + warning: "Advertencia", + failure: "Fallo", + danger: "Peligro", + bug: "Error", + example: "Ejemplo", + quote: "Cita", + }, + backlinks: { + title: "Retroenlaces", + noBacklinksFound: "No se han encontrado retroenlaces", + }, + themeToggle: { + lightMode: "Modo claro", + darkMode: "Modo oscuro", + }, + explorer: { + title: "Explorador", + }, + footer: { + createdWith: "Creado con", + }, + graph: { + title: "Vista Gráfica", + }, + recentNotes: { + title: "Notas Recientes", + seeRemainingMore: ({ remaining }) => `Vea ${remaining} más →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Transcluido de ${targetSlug}`, + linkToOriginal: "Enlace al original", + }, + search: { + title: "Buscar", + searchBarPlaceholder: "Busca algo", + }, + tableOfContents: { + title: "Tabla de Contenidos", + }, + contentMeta: { + readingTime: ({ minutes }) => `Se lee en ${minutes} min`, + }, + }, + pages: { + rss: { + recentNotes: "Notas recientes", + lastFewNotes: ({ count }) => `Últimas ${count} notas`, + }, + error: { + title: "No se ha encontrado.", + notFound: "Esta página es privada o no existe.", + home: "Regresa a la página principal", + }, + folderContent: { + folder: "Carpeta", + itemsUnderFolder: ({ count }) => + count === 1 ? "1 artículo en esta carpeta." : `${count} artículos en esta carpeta.`, + }, + tagContent: { + tag: "Etiqueta", + tagIndex: "Índice de Etiquetas", + itemsUnderTag: ({ count }) => + count === 1 ? "1 artículo con esta etiqueta." : `${count} artículos con esta etiqueta.`, + showingFirst: ({ count }) => `Mostrando las primeras ${count} etiquetas.`, + totalTags: ({ count }) => `Se han encontrado ${count} etiquetas en total.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/fa-IR.ts b/quartz/i18n/locales/fa-IR.ts new file mode 100755 index 0000000..5bfef5a --- /dev/null +++ b/quartz/i18n/locales/fa-IR.ts @@ -0,0 +1,84 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "بدون عنوان", + description: "توضیح خاصی اضافه نشده است", + }, + components: { + callout: { + note: "یادداشت", + abstract: "چکیده", + info: "اطلاعات", + todo: "اقدام", + tip: "نکته", + success: "تیک", + question: "سؤال", + warning: "هشدار", + failure: "شکست", + danger: "خطر", + bug: "باگ", + example: "مثال", + quote: "نقل قول", + }, + backlinks: { + title: "بک‌لینک‌ها", + noBacklinksFound: "بدون بک‌لینک", + }, + themeToggle: { + lightMode: "حالت روشن", + darkMode: "حالت تاریک", + }, + explorer: { + title: "مطالب", + }, + footer: { + createdWith: "ساخته شده با", + }, + graph: { + title: "نمای گراف", + }, + recentNotes: { + title: "یادداشت‌های اخیر", + seeRemainingMore: ({ remaining }) => `${remaining} یادداشت دیگر →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `از ${targetSlug}`, + linkToOriginal: "پیوند به اصلی", + }, + search: { + title: "جستجو", + searchBarPlaceholder: "مطلبی را جستجو کنید", + }, + tableOfContents: { + title: "فهرست", + }, + contentMeta: { + readingTime: ({ minutes }) => `زمان تقریبی مطالعه: ${minutes} دقیقه`, + }, + }, + pages: { + rss: { + recentNotes: "یادداشت‌های اخیر", + lastFewNotes: ({ count }) => `${count} یادداشت اخیر`, + }, + error: { + title: "یافت نشد", + notFound: "این صفحه یا خصوصی است یا وجود ندارد", + home: "بازگشت به صفحه اصلی", + }, + folderContent: { + folder: "پوشه", + itemsUnderFolder: ({ count }) => + count === 1 ? ".یک مطلب در این پوشه است" : `${count} مطلب در این پوشه است.`, + }, + tagContent: { + tag: "برچسب", + tagIndex: "فهرست برچسب‌ها", + itemsUnderTag: ({ count }) => + count === 1 ? "یک مطلب با این برچسب" : `${count} مطلب با این برچسب.`, + showingFirst: ({ count }) => `در حال نمایش ${count} برچسب.`, + totalTags: ({ count }) => `${count} برچسب یافت شد.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/fr-FR.ts b/quartz/i18n/locales/fr-FR.ts new file mode 100755 index 0000000..ef43fa8 --- /dev/null +++ b/quartz/i18n/locales/fr-FR.ts @@ -0,0 +1,84 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Sans titre", + description: "Aucune description fournie", + }, + components: { + callout: { + note: "Note", + abstract: "Résumé", + info: "Info", + todo: "À faire", + tip: "Conseil", + success: "Succès", + question: "Question", + warning: "Avertissement", + failure: "Échec", + danger: "Danger", + bug: "Bogue", + example: "Exemple", + quote: "Citation", + }, + backlinks: { + title: "Liens retour", + noBacklinksFound: "Aucun lien retour trouvé", + }, + themeToggle: { + lightMode: "Mode clair", + darkMode: "Mode sombre", + }, + explorer: { + title: "Explorateur", + }, + footer: { + createdWith: "Créé avec", + }, + graph: { + title: "Vue Graphique", + }, + recentNotes: { + title: "Notes Récentes", + seeRemainingMore: ({ remaining }) => `Voir ${remaining} de plus →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Transclusion de ${targetSlug}`, + linkToOriginal: "Lien vers l'original", + }, + search: { + title: "Recherche", + searchBarPlaceholder: "Rechercher quelque chose", + }, + tableOfContents: { + title: "Table des Matières", + }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes} min de lecture`, + }, + }, + pages: { + rss: { + recentNotes: "Notes récentes", + lastFewNotes: ({ count }) => `Les dernières ${count} notes`, + }, + error: { + title: "Introuvable", + notFound: "Cette page est soit privée, soit elle n'existe pas.", + home: "Retour à la page d'accueil", + }, + folderContent: { + folder: "Dossier", + itemsUnderFolder: ({ count }) => + count === 1 ? "1 élément sous ce dossier." : `${count} éléments sous ce dossier.`, + }, + tagContent: { + tag: "Étiquette", + tagIndex: "Index des étiquettes", + itemsUnderTag: ({ count }) => + count === 1 ? "1 élément avec cette étiquette." : `${count} éléments avec cette étiquette.`, + showingFirst: ({ count }) => `Affichage des premières ${count} étiquettes.`, + totalTags: ({ count }) => `Trouvé ${count} étiquettes au total.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/hu-HU.ts b/quartz/i18n/locales/hu-HU.ts new file mode 100755 index 0000000..066b777 --- /dev/null +++ b/quartz/i18n/locales/hu-HU.ts @@ -0,0 +1,82 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Névtelen", + description: "Nincs leírás", + }, + components: { + callout: { + note: "Jegyzet", + abstract: "Abstract", + info: "Információ", + todo: "Tennivaló", + tip: "Tipp", + success: "Siker", + question: "Kérdés", + warning: "Figyelmeztetés", + failure: "Hiba", + danger: "Veszély", + bug: "Bug", + example: "Példa", + quote: "Idézet", + }, + backlinks: { + title: "Visszautalások", + noBacklinksFound: "Nincs visszautalás", + }, + themeToggle: { + lightMode: "Világos mód", + darkMode: "Sötét mód", + }, + explorer: { + title: "Fájlböngésző", + }, + footer: { + createdWith: "Készítve ezzel:", + }, + graph: { + title: "Grafikonnézet", + }, + recentNotes: { + title: "Legutóbbi jegyzetek", + seeRemainingMore: ({ remaining }) => `${remaining} további megtekintése →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `${targetSlug} áthivatkozása`, + linkToOriginal: "Hivatkozás az eredetire", + }, + search: { + title: "Keresés", + searchBarPlaceholder: "Keress valamire", + }, + tableOfContents: { + title: "Tartalomjegyzék", + }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes} perces olvasás`, + }, + }, + pages: { + rss: { + recentNotes: "Legutóbbi jegyzetek", + lastFewNotes: ({ count }) => `Legutóbbi ${count} jegyzet`, + }, + error: { + title: "Nem található", + notFound: "Ez a lap vagy privát vagy nem létezik.", + home: "Vissza a kezdőlapra", + }, + folderContent: { + folder: "Mappa", + itemsUnderFolder: ({ count }) => `Ebben a mappában ${count} elem található.`, + }, + tagContent: { + tag: "Címke", + tagIndex: "Címke index", + itemsUnderTag: ({ count }) => `${count} elem található ezzel a címkével.`, + showingFirst: ({ count }) => `Első ${count} címke megjelenítve.`, + totalTags: ({ count }) => `Összesen ${count} címke található.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/it-IT.ts b/quartz/i18n/locales/it-IT.ts new file mode 100755 index 0000000..c8c5973 --- /dev/null +++ b/quartz/i18n/locales/it-IT.ts @@ -0,0 +1,84 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Senza titolo", + description: "Nessuna descrizione", + }, + components: { + callout: { + note: "Nota", + abstract: "Astratto", + info: "Info", + todo: "Da fare", + tip: "Consiglio", + success: "Completato", + question: "Domanda", + warning: "Attenzione", + failure: "Errore", + danger: "Pericolo", + bug: "Bug", + example: "Esempio", + quote: "Citazione", + }, + backlinks: { + title: "Link entranti", + noBacklinksFound: "Nessun link entrante", + }, + themeToggle: { + lightMode: "Tema chiaro", + darkMode: "Tema scuro", + }, + explorer: { + title: "Esplora", + }, + footer: { + createdWith: "Creato con", + }, + graph: { + title: "Vista grafico", + }, + recentNotes: { + title: "Note recenti", + seeRemainingMore: ({ remaining }) => `Vedi ${remaining} altro →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Transclusione di ${targetSlug}`, + linkToOriginal: "Link all'originale", + }, + search: { + title: "Cerca", + searchBarPlaceholder: "Cerca qualcosa", + }, + tableOfContents: { + title: "Tabella dei contenuti", + }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes} minuti`, + }, + }, + pages: { + rss: { + recentNotes: "Note recenti", + lastFewNotes: ({ count }) => `Ultime ${count} note`, + }, + error: { + title: "Non trovato", + notFound: "Questa pagina è privata o non esiste.", + home: "Ritorna alla home page", + }, + folderContent: { + folder: "Cartella", + itemsUnderFolder: ({ count }) => + count === 1 ? "1 oggetto in questa cartella." : `${count} oggetti in questa cartella.`, + }, + tagContent: { + tag: "Etichetta", + tagIndex: "Indice etichette", + itemsUnderTag: ({ count }) => + count === 1 ? "1 oggetto con questa etichetta." : `${count} oggetti con questa etichetta.`, + showingFirst: ({ count }) => `Prime ${count} etichette.`, + totalTags: ({ count }) => `Trovate ${count} etichette totali.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/ja-JP.ts b/quartz/i18n/locales/ja-JP.ts new file mode 100755 index 0000000..9581b5e --- /dev/null +++ b/quartz/i18n/locales/ja-JP.ts @@ -0,0 +1,82 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "無題", + description: "説明なし", + }, + components: { + callout: { + note: "ノート", + abstract: "抄録", + info: "情報", + todo: "やるべきこと", + tip: "ヒント", + success: "成功", + question: "質問", + warning: "警告", + failure: "失敗", + danger: "危険", + bug: "バグ", + example: "例", + quote: "引用", + }, + backlinks: { + title: "バックリンク", + noBacklinksFound: "バックリンクはありません", + }, + themeToggle: { + lightMode: "ライトモード", + darkMode: "ダークモード", + }, + explorer: { + title: "エクスプローラー", + }, + footer: { + createdWith: "作成", + }, + graph: { + title: "グラフビュー", + }, + recentNotes: { + title: "最近の記事", + seeRemainingMore: ({ remaining }) => `さらに${remaining}件 →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `${targetSlug}のまとめ`, + linkToOriginal: "元記事へのリンク", + }, + search: { + title: "検索", + searchBarPlaceholder: "検索ワードを入力", + }, + tableOfContents: { + title: "目次", + }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes} min read`, + }, + }, + pages: { + rss: { + recentNotes: "最近の記事", + lastFewNotes: ({ count }) => `最新の${count}件`, + }, + error: { + title: "Not Found", + notFound: "ページが存在しないか、非公開設定になっています。", + home: "ホームページに戻る", + }, + folderContent: { + folder: "フォルダ", + itemsUnderFolder: ({ count }) => `${count}件のページ`, + }, + tagContent: { + tag: "タグ", + tagIndex: "タグ一覧", + itemsUnderTag: ({ count }) => `${count}件のページ`, + showingFirst: ({ count }) => `のうち最初の${count}件を表示しています`, + totalTags: ({ count }) => `全${count}個のタグを表示中`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/ko-KR.ts b/quartz/i18n/locales/ko-KR.ts new file mode 100755 index 0000000..9be08d9 --- /dev/null +++ b/quartz/i18n/locales/ko-KR.ts @@ -0,0 +1,82 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "제목 없음", + description: "설명 없음", + }, + components: { + callout: { + note: "노트", + abstract: "개요", + info: "정보", + todo: "할일", + tip: "팁", + success: "성공", + question: "질문", + warning: "주의", + failure: "실패", + danger: "위험", + bug: "버그", + example: "예시", + quote: "인용", + }, + backlinks: { + title: "백링크", + noBacklinksFound: "백링크가 없습니다.", + }, + themeToggle: { + lightMode: "라이트 모드", + darkMode: "다크 모드", + }, + explorer: { + title: "탐색기", + }, + footer: { + createdWith: "Created with", + }, + graph: { + title: "그래프 뷰", + }, + recentNotes: { + title: "최근 게시글", + seeRemainingMore: ({ remaining }) => `${remaining}건 더보기 →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `${targetSlug}의 포함`, + linkToOriginal: "원본 링크", + }, + search: { + title: "검색", + searchBarPlaceholder: "검색어를 입력하세요", + }, + tableOfContents: { + title: "목차", + }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes} min read`, + }, + }, + pages: { + rss: { + recentNotes: "최근 게시글", + lastFewNotes: ({ count }) => `최근 ${count} 건`, + }, + error: { + title: "Not Found", + notFound: "페이지가 존재하지 않거나 비공개 설정이 되어 있습니다.", + home: "홈페이지로 돌아가기", + }, + folderContent: { + folder: "폴더", + itemsUnderFolder: ({ count }) => `${count}건의 항목`, + }, + tagContent: { + tag: "태그", + tagIndex: "태그 목록", + itemsUnderTag: ({ count }) => `${count}건의 항목`, + showingFirst: ({ count }) => `처음 ${count}개의 태그`, + totalTags: ({ count }) => `총 ${count}개의 태그를 찾았습니다.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/lt-LT.ts b/quartz/i18n/locales/lt-LT.ts new file mode 100755 index 0000000..d48e593 --- /dev/null +++ b/quartz/i18n/locales/lt-LT.ts @@ -0,0 +1,104 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Be Pavadinimo", + description: "Aprašymas Nepateiktas", + }, + components: { + callout: { + note: "Pastaba", + abstract: "Santrauka", + info: "Informacija", + todo: "Darbų sąrašas", + tip: "Patarimas", + success: "Sėkmingas", + question: "Klausimas", + warning: "Įspėjimas", + failure: "Nesėkmingas", + danger: "Pavojus", + bug: "Klaida", + example: "Pavyzdys", + quote: "Citata", + }, + backlinks: { + title: "Atgalinės Nuorodos", + noBacklinksFound: "Atgalinių Nuorodų Nerasta", + }, + themeToggle: { + lightMode: "Šviesus Režimas", + darkMode: "Tamsus Režimas", + }, + explorer: { + title: "Naršyklė", + }, + footer: { + createdWith: "Sukurta Su", + }, + graph: { + title: "Grafiko Vaizdas", + }, + recentNotes: { + title: "Naujausi Užrašai", + seeRemainingMore: ({ remaining }) => `Peržiūrėti dar ${remaining} →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Įterpimas iš ${targetSlug}`, + linkToOriginal: "Nuoroda į originalą", + }, + search: { + title: "Paieška", + searchBarPlaceholder: "Ieškoti", + }, + tableOfContents: { + title: "Turinys", + }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes} min skaitymo`, + }, + }, + pages: { + rss: { + recentNotes: "Naujausi užrašai", + lastFewNotes: ({ count }) => + count === 1 + ? "Paskutinis 1 užrašas" + : count < 10 + ? `Paskutiniai ${count} užrašai` + : `Paskutiniai ${count} užrašų`, + }, + error: { + title: "Nerasta", + notFound: + "Arba šis puslapis yra pasiekiamas tik tam tikriems vartotojams, arba tokio puslapio nėra.", + home: "Grįžti į pagrindinį puslapį", + }, + folderContent: { + folder: "Aplankas", + itemsUnderFolder: ({ count }) => + count === 1 + ? "1 elementas šiame aplanke." + : count < 10 + ? `${count} elementai šiame aplanke.` + : `${count} elementų šiame aplanke.`, + }, + tagContent: { + tag: "Žyma", + tagIndex: "Žymų indeksas", + itemsUnderTag: ({ count }) => + count === 1 + ? "1 elementas su šia žyma." + : count < 10 + ? `${count} elementai su šia žyma.` + : `${count} elementų su šia žyma.`, + showingFirst: ({ count }) => + count < 10 ? `Rodomos pirmosios ${count} žymos.` : `Rodomos pirmosios ${count} žymų.`, + totalTags: ({ count }) => + count === 1 + ? "Rasta iš viso 1 žyma." + : count < 10 + ? `Rasta iš viso ${count} žymos.` + : `Rasta iš viso ${count} žymų.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/nl-NL.ts b/quartz/i18n/locales/nl-NL.ts new file mode 100755 index 0000000..ccbafa7 --- /dev/null +++ b/quartz/i18n/locales/nl-NL.ts @@ -0,0 +1,86 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Naamloos", + description: "Geen beschrijving gegeven.", + }, + components: { + callout: { + note: "Notitie", + abstract: "Samenvatting", + info: "Info", + todo: "Te doen", + tip: "Tip", + success: "Succes", + question: "Vraag", + warning: "Waarschuwing", + failure: "Mislukking", + danger: "Gevaar", + bug: "Bug", + example: "Voorbeeld", + quote: "Citaat", + }, + backlinks: { + title: "Backlinks", + noBacklinksFound: "Geen backlinks gevonden", + }, + themeToggle: { + lightMode: "Lichte modus", + darkMode: "Donkere modus", + }, + explorer: { + title: "Verkenner", + }, + footer: { + createdWith: "Gemaakt met", + }, + graph: { + title: "Grafiekweergave", + }, + recentNotes: { + title: "Recente notities", + seeRemainingMore: ({ remaining }) => `Zie ${remaining} meer →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Invoeging van ${targetSlug}`, + linkToOriginal: "Link naar origineel", + }, + search: { + title: "Zoeken", + searchBarPlaceholder: "Doorzoek de website", + }, + tableOfContents: { + title: "Inhoudsopgave", + }, + contentMeta: { + readingTime: ({ minutes }) => + minutes === 1 ? "1 minuut leestijd" : `${minutes} minuten leestijd`, + }, + }, + pages: { + rss: { + recentNotes: "Recente notities", + lastFewNotes: ({ count }) => `Laatste ${count} notities`, + }, + error: { + title: "Niet gevonden", + notFound: "Deze pagina is niet zichtbaar of bestaat niet.", + home: "Keer terug naar de start pagina", + }, + folderContent: { + folder: "Map", + itemsUnderFolder: ({ count }) => + count === 1 ? "1 item in deze map." : `${count} items in deze map.`, + }, + tagContent: { + tag: "Label", + tagIndex: "Label-index", + itemsUnderTag: ({ count }) => + count === 1 ? "1 item met dit label." : `${count} items met dit label.`, + showingFirst: ({ count }) => + count === 1 ? "Eerste label tonen." : `Eerste ${count} labels tonen.`, + totalTags: ({ count }) => `${count} labels gevonden.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/pl-PL.ts b/quartz/i18n/locales/pl-PL.ts new file mode 100755 index 0000000..7fa0cd4 --- /dev/null +++ b/quartz/i18n/locales/pl-PL.ts @@ -0,0 +1,84 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Bez nazwy", + description: "Brak opisu", + }, + components: { + callout: { + note: "Notatka", + abstract: "Streszczenie", + info: "informacja", + todo: "Do zrobienia", + tip: "Wskazówka", + success: "Zrobione", + question: "Pytanie", + warning: "Ostrzeżenie", + failure: "Usterka", + danger: "Niebiezpieczeństwo", + bug: "Błąd w kodzie", + example: "Przykład", + quote: "Cytat", + }, + backlinks: { + title: "Odnośniki zwrotne", + noBacklinksFound: "Brak połączeń zwrotnych", + }, + themeToggle: { + lightMode: "Trzyb jasny", + darkMode: "Tryb ciemny", + }, + explorer: { + title: "Przeglądaj", + }, + footer: { + createdWith: "Stworzone z użyciem", + }, + graph: { + title: "Graf", + }, + recentNotes: { + title: "Najnowsze notatki", + seeRemainingMore: ({ remaining }) => `Zobacz ${remaining} nastepnych →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Osadzone ${targetSlug}`, + linkToOriginal: "Łącze do oryginału", + }, + search: { + title: "Szukaj", + searchBarPlaceholder: "Search for something", + }, + tableOfContents: { + title: "Spis treści", + }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes} min. czytania `, + }, + }, + pages: { + rss: { + recentNotes: "Najnowsze notatki", + lastFewNotes: ({ count }) => `Ostatnie ${count} notatek`, + }, + error: { + title: "Nie znaleziono", + notFound: "Ta strona jest prywatna lub nie istnieje.", + home: "Powrót do strony głównej", + }, + folderContent: { + folder: "Folder", + itemsUnderFolder: ({ count }) => + count === 1 ? "W tym folderze jest 1 element." : `Elementów w folderze: ${count}.`, + }, + tagContent: { + tag: "Znacznik", + tagIndex: "Spis znaczników", + itemsUnderTag: ({ count }) => + count === 1 ? "Oznaczony 1 element." : `Elementów z tym znacznikiem: ${count}.`, + showingFirst: ({ count }) => `Pokazuje ${count} pierwszych znaczników.`, + totalTags: ({ count }) => `Znalezionych wszystkich znaczników: ${count}.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/pt-BR.ts b/quartz/i18n/locales/pt-BR.ts new file mode 100755 index 0000000..c7b6bfb --- /dev/null +++ b/quartz/i18n/locales/pt-BR.ts @@ -0,0 +1,84 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Sem título", + description: "Sem descrição", + }, + components: { + callout: { + note: "Nota", + abstract: "Abstrato", + info: "Info", + todo: "Pendência", + tip: "Dica", + success: "Sucesso", + question: "Pergunta", + warning: "Aviso", + failure: "Falha", + danger: "Perigo", + bug: "Bug", + example: "Exemplo", + quote: "Citação", + }, + backlinks: { + title: "Backlinks", + noBacklinksFound: "Sem backlinks encontrados", + }, + themeToggle: { + lightMode: "Tema claro", + darkMode: "Tema escuro", + }, + explorer: { + title: "Explorador", + }, + footer: { + createdWith: "Criado com", + }, + graph: { + title: "Visão de gráfico", + }, + recentNotes: { + title: "Notas recentes", + seeRemainingMore: ({ remaining }) => `Veja mais ${remaining} →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Transcrever de ${targetSlug}`, + linkToOriginal: "Link ao original", + }, + search: { + title: "Pesquisar", + searchBarPlaceholder: "Pesquisar por algo", + }, + tableOfContents: { + title: "Sumário", + }, + contentMeta: { + readingTime: ({ minutes }) => `Leitura de ${minutes} min`, + }, + }, + pages: { + rss: { + recentNotes: "Notas recentes", + lastFewNotes: ({ count }) => `Últimas ${count} notas`, + }, + error: { + title: "Não encontrado", + notFound: "Esta página é privada ou não existe.", + home: "Retornar a página inicial", + }, + folderContent: { + folder: "Arquivo", + itemsUnderFolder: ({ count }) => + count === 1 ? "1 item neste arquivo." : `${count} items neste arquivo.`, + }, + tagContent: { + tag: "Tag", + tagIndex: "Sumário de Tags", + itemsUnderTag: ({ count }) => + count === 1 ? "1 item com esta tag." : `${count} items com esta tag.`, + showingFirst: ({ count }) => `Mostrando as ${count} primeiras tags.`, + totalTags: ({ count }) => `Encontradas ${count} tags.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/ro-RO.ts b/quartz/i18n/locales/ro-RO.ts new file mode 100755 index 0000000..2de1c8c --- /dev/null +++ b/quartz/i18n/locales/ro-RO.ts @@ -0,0 +1,85 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Fără titlu", + description: "Nici o descriere furnizată", + }, + components: { + callout: { + note: "Notă", + abstract: "Rezumat", + info: "Informație", + todo: "De făcut", + tip: "Sfat", + success: "Succes", + question: "Întrebare", + warning: "Avertisment", + failure: "Eșec", + danger: "Pericol", + bug: "Bug", + example: "Exemplu", + quote: "Citat", + }, + backlinks: { + title: "Legături înapoi", + noBacklinksFound: "Nu s-au găsit legături înapoi", + }, + themeToggle: { + lightMode: "Modul luminos", + darkMode: "Modul întunecat", + }, + explorer: { + title: "Explorator", + }, + footer: { + createdWith: "Creat cu", + }, + graph: { + title: "Graf", + }, + recentNotes: { + title: "Notițe recente", + seeRemainingMore: ({ remaining }) => `Vezi încă ${remaining} →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Extras din ${targetSlug}`, + linkToOriginal: "Legătură către original", + }, + search: { + title: "Căutare", + searchBarPlaceholder: "Introduceți termenul de căutare...", + }, + tableOfContents: { + title: "Cuprins", + }, + contentMeta: { + readingTime: ({ minutes }) => + minutes == 1 ? `lectură de 1 minut` : `lectură de ${minutes} minute`, + }, + }, + pages: { + rss: { + recentNotes: "Notițe recente", + lastFewNotes: ({ count }) => `Ultimele ${count} notițe`, + }, + error: { + title: "Pagina nu a fost găsită", + notFound: "Fie această pagină este privată, fie nu există.", + home: "Reveniți la pagina de pornire", + }, + folderContent: { + folder: "Dosar", + itemsUnderFolder: ({ count }) => + count === 1 ? "1 articol în acest dosar." : `${count} elemente în acest dosar.`, + }, + tagContent: { + tag: "Etichetă", + tagIndex: "Indexul etichetelor", + itemsUnderTag: ({ count }) => + count === 1 ? "1 articol cu această etichetă." : `${count} articole cu această etichetă.`, + showingFirst: ({ count }) => `Se afișează primele ${count} etichete.`, + totalTags: ({ count }) => `Au fost găsite ${count} etichete în total.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/ru-RU.ts b/quartz/i18n/locales/ru-RU.ts new file mode 100755 index 0000000..18e0817 --- /dev/null +++ b/quartz/i18n/locales/ru-RU.ts @@ -0,0 +1,96 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Без названия", + description: "Описание отсутствует", + }, + components: { + callout: { + note: "Заметка", + abstract: "Резюме", + info: "Инфо", + todo: "Сделать", + tip: "Подсказка", + success: "Успех", + question: "Вопрос", + warning: "Предупреждение", + failure: "Неудача", + danger: "Опасность", + bug: "Баг", + example: "Пример", + quote: "Цитата", + }, + backlinks: { + title: "Обратные ссылки", + noBacklinksFound: "Обратные ссылки отсутствуют", + }, + themeToggle: { + lightMode: "Светлый режим", + darkMode: "Тёмный режим", + }, + explorer: { + title: "Проводник", + }, + footer: { + createdWith: "Создано с помощью", + }, + graph: { + title: "Вид графа", + }, + recentNotes: { + title: "Недавние заметки", + seeRemainingMore: ({ remaining }) => + `Посмотреть оставш${getForm(remaining, "уюся", "иеся", "иеся")} ${remaining} →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Переход из ${targetSlug}`, + linkToOriginal: "Ссылка на оригинал", + }, + search: { + title: "Поиск", + searchBarPlaceholder: "Найти что-нибудь", + }, + tableOfContents: { + title: "Оглавление", + }, + contentMeta: { + readingTime: ({ minutes }) => `время чтения ~${minutes} мин.`, + }, + }, + pages: { + rss: { + recentNotes: "Недавние заметки", + lastFewNotes: ({ count }) => + `Последн${getForm(count, "яя", "ие", "ие")} ${count} замет${getForm(count, "ка", "ки", "ок")}`, + }, + error: { + title: "Страница не найдена", + notFound: "Эта страница приватная или не существует", + home: "Вернуться на главную страницу", + }, + folderContent: { + folder: "Папка", + itemsUnderFolder: ({ count }) => + `в этой папке ${count} элемент${getForm(count, "", "а", "ов")}`, + }, + tagContent: { + tag: "Тег", + tagIndex: "Индекс тегов", + itemsUnderTag: ({ count }) => `с этим тегом ${count} элемент${getForm(count, "", "а", "ов")}`, + showingFirst: ({ count }) => + `Показыва${getForm(count, "ется", "ются", "ются")} ${count} тег${getForm(count, "", "а", "ов")}`, + totalTags: ({ count }) => `Всего ${count} тег${getForm(count, "", "а", "ов")}`, + }, + }, +} as const satisfies Translation + +function getForm(number: number, form1: string, form2: string, form5: string): string { + const remainder100 = number % 100 + const remainder10 = remainder100 % 10 + + if (remainder100 >= 10 && remainder100 <= 20) return form5 + if (remainder10 > 1 && remainder10 < 5) return form2 + if (remainder10 == 1) return form1 + return form5 +} diff --git a/quartz/i18n/locales/th-TH.ts b/quartz/i18n/locales/th-TH.ts new file mode 100755 index 0000000..4ea8486 --- /dev/null +++ b/quartz/i18n/locales/th-TH.ts @@ -0,0 +1,82 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "ไม่มีชื่อ", + description: "ไม่ได้ระบุคำอธิบายย่อ", + }, + components: { + callout: { + note: "หมายเหตุ", + abstract: "บทคัดย่อ", + info: "ข้อมูล", + todo: "ต้องทำเพิ่มเติม", + tip: "คำแนะนำ", + success: "เรียบร้อย", + question: "คำถาม", + warning: "คำเตือน", + failure: "ข้อผิดพลาด", + danger: "อันตราย", + bug: "บั๊ก", + example: "ตัวอย่าง", + quote: "คำพูกยกมา", + }, + backlinks: { + title: "หน้าที่กล่าวถึง", + noBacklinksFound: "ไม่มีหน้าที่โยงมาหน้านี้", + }, + themeToggle: { + lightMode: "โหมดสว่าง", + darkMode: "โหมดมืด", + }, + explorer: { + title: "รายการหน้า", + }, + footer: { + createdWith: "สร้างด้วย", + }, + graph: { + title: "มุมมองกราฟ", + }, + recentNotes: { + title: "บันทึกล่าสุด", + seeRemainingMore: ({ remaining }) => `ดูเพิ่มอีก ${remaining} รายการ →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `รวมข้ามเนื้อหาจาก ${targetSlug}`, + linkToOriginal: "ดูหน้าต้นทาง", + }, + search: { + title: "ค้นหา", + searchBarPlaceholder: "ค้นหาบางอย่าง", + }, + tableOfContents: { + title: "สารบัญ", + }, + contentMeta: { + readingTime: ({ minutes }) => `อ่านราว ${minutes} นาที`, + }, + }, + pages: { + rss: { + recentNotes: "บันทึกล่าสุด", + lastFewNotes: ({ count }) => `${count} บันทึกล่าสุด`, + }, + error: { + title: "ไม่มีหน้านี้", + notFound: "หน้านี้อาจตั้งค่าเป็นส่วนตัวหรือยังไม่ถูกสร้าง", + home: "กลับหน้าหลัก", + }, + folderContent: { + folder: "โฟลเดอร์", + itemsUnderFolder: ({ count }) => `มี ${count} รายการในโฟลเดอร์นี้`, + }, + tagContent: { + tag: "แท็ก", + tagIndex: "แท็กทั้งหมด", + itemsUnderTag: ({ count }) => `มี ${count} รายการในแท็กนี้`, + showingFirst: ({ count }) => `แสดง ${count} แท็กแรก`, + totalTags: ({ count }) => `มีทั้งหมด ${count} แท็ก`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/tr-TR.ts b/quartz/i18n/locales/tr-TR.ts new file mode 100755 index 0000000..a3805d1 --- /dev/null +++ b/quartz/i18n/locales/tr-TR.ts @@ -0,0 +1,84 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "İsimsiz", + description: "Herhangi bir açıklama eklenmedi", + }, + components: { + callout: { + note: "Not", + abstract: "Özet", + info: "Bilgi", + todo: "Yapılacaklar", + tip: "İpucu", + success: "Başarılı", + question: "Soru", + warning: "Uyarı", + failure: "Başarısız", + danger: "Tehlike", + bug: "Hata", + example: "Örnek", + quote: "Alıntı", + }, + backlinks: { + title: "Backlinkler", + noBacklinksFound: "Backlink bulunamadı", + }, + themeToggle: { + lightMode: "Açık mod", + darkMode: "Koyu mod", + }, + explorer: { + title: "Gezgin", + }, + footer: { + createdWith: "Şununla oluşturuldu", + }, + graph: { + title: "Grafik Görünümü", + }, + recentNotes: { + title: "Son Notlar", + seeRemainingMore: ({ remaining }) => `${remaining} tane daha gör →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `${targetSlug} sayfasından alıntı`, + linkToOriginal: "Orijinal bağlantı", + }, + search: { + title: "Arama", + searchBarPlaceholder: "Bir şey arayın", + }, + tableOfContents: { + title: "İçindekiler", + }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes} dakika okuma süresi`, + }, + }, + pages: { + rss: { + recentNotes: "Son notlar", + lastFewNotes: ({ count }) => `Son ${count} not`, + }, + error: { + title: "Bulunamadı", + notFound: "Bu sayfa ya özel ya da mevcut değil.", + home: "Anasayfaya geri dön", + }, + folderContent: { + folder: "Klasör", + itemsUnderFolder: ({ count }) => + count === 1 ? "Bu klasör altında 1 öğe." : `Bu klasör altındaki ${count} öğe.`, + }, + tagContent: { + tag: "Etiket", + tagIndex: "Etiket Sırası", + itemsUnderTag: ({ count }) => + count === 1 ? "Bu etikete sahip 1 öğe." : `Bu etiket altındaki ${count} öğe.`, + showingFirst: ({ count }) => `İlk ${count} etiket gösteriliyor.`, + totalTags: ({ count }) => `Toplam ${count} adet etiket bulundu.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/uk-UA.ts b/quartz/i18n/locales/uk-UA.ts new file mode 100755 index 0000000..469de4f --- /dev/null +++ b/quartz/i18n/locales/uk-UA.ts @@ -0,0 +1,84 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Без назви", + description: "Опис не надано", + }, + components: { + callout: { + note: "Примітка", + abstract: "Абстракт", + info: "Інформація", + todo: "Завдання", + tip: "Порада", + success: "Успіх", + question: "Питання", + warning: "Попередження", + failure: "Невдача", + danger: "Небезпека", + bug: "Баг", + example: "Приклад", + quote: "Цитата", + }, + backlinks: { + title: "Зворотні посилання", + noBacklinksFound: "Зворотних посилань не знайдено", + }, + themeToggle: { + lightMode: "Світлий режим", + darkMode: "Темний режим", + }, + explorer: { + title: "Провідник", + }, + footer: { + createdWith: "Створено за допомогою", + }, + graph: { + title: "Вигляд графа", + }, + recentNotes: { + title: "Останні нотатки", + seeRemainingMore: ({ remaining }) => `Переглянути ще ${remaining} →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Видобуто з ${targetSlug}`, + linkToOriginal: "Посилання на оригінал", + }, + search: { + title: "Пошук", + searchBarPlaceholder: "Шукати щось", + }, + tableOfContents: { + title: "Зміст", + }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes} хв читання`, + }, + }, + pages: { + rss: { + recentNotes: "Останні нотатки", + lastFewNotes: ({ count }) => `Останні нотатки: ${count}`, + }, + error: { + title: "Не знайдено", + notFound: "Ця сторінка або приватна, або не існує.", + home: "Повернутися на головну сторінку", + }, + folderContent: { + folder: "Тека", + itemsUnderFolder: ({ count }) => + count === 1 ? "У цій теці 1 елемент." : `Елементів у цій теці: ${count}.`, + }, + tagContent: { + tag: "Мітка", + tagIndex: "Індекс мітки", + itemsUnderTag: ({ count }) => + count === 1 ? "1 елемент з цією міткою." : `Елементів з цією міткою: ${count}.`, + showingFirst: ({ count }) => `Показ перших ${count} міток.`, + totalTags: ({ count }) => `Всього знайдено міток: ${count}.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/vi-VN.ts b/quartz/i18n/locales/vi-VN.ts new file mode 100755 index 0000000..39a8fbc --- /dev/null +++ b/quartz/i18n/locales/vi-VN.ts @@ -0,0 +1,84 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "Không có tiêu đề", + description: "Không có mô tả được cung cấp", + }, + components: { + callout: { + note: "Ghi Chú", + abstract: "Tóm Tắt", + info: "Thông tin", + todo: "Cần Làm", + tip: "Gợi Ý", + success: "Thành Công", + question: "Nghi Vấn", + warning: "Cảnh Báo", + failure: "Thất Bại", + danger: "Nguy Hiểm", + bug: "Lỗi", + example: "Ví Dụ", + quote: "Trích Dẫn", + }, + backlinks: { + title: "Liên Kết Ngược", + noBacklinksFound: "Không có liên kết ngược được tìm thấy", + }, + themeToggle: { + lightMode: "Sáng", + darkMode: "Tối", + }, + explorer: { + title: "Trong bài này", + }, + footer: { + createdWith: "Được tạo bởi", + }, + graph: { + title: "Biểu Đồ", + }, + recentNotes: { + title: "Bài viết gần đây", + seeRemainingMore: ({ remaining }) => `Xem ${remaining} thêm →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `Bao gồm ${targetSlug}`, + linkToOriginal: "Liên Kết Gốc", + }, + search: { + title: "Tìm Kiếm", + searchBarPlaceholder: "Tìm kiếm thông tin", + }, + tableOfContents: { + title: "Bảng Nội Dung", + }, + contentMeta: { + readingTime: ({ minutes }) => `đọc ${minutes} phút`, + }, + }, + pages: { + rss: { + recentNotes: "Những bài gần đây", + lastFewNotes: ({ count }) => `${count} Bài gần đây`, + }, + error: { + title: "Không Tìm Thấy", + notFound: "Trang này được bảo mật hoặc không tồn tại.", + home: "Trở về trang chủ", + }, + folderContent: { + folder: "Thư Mục", + itemsUnderFolder: ({ count }) => + count === 1 ? "1 mục trong thư mục này." : `${count} mục trong thư mục này.`, + }, + tagContent: { + tag: "Thẻ", + tagIndex: "Thẻ Mục Lục", + itemsUnderTag: ({ count }) => + count === 1 ? "1 mục gắn thẻ này." : `${count} mục gắn thẻ này.`, + showingFirst: ({ count }) => `Hiển thị trước ${count} thẻ.`, + totalTags: ({ count }) => `Tìm thấy ${count} thẻ tổng cộng.`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/zh-CN.ts b/quartz/i18n/locales/zh-CN.ts new file mode 100755 index 0000000..b710db5 --- /dev/null +++ b/quartz/i18n/locales/zh-CN.ts @@ -0,0 +1,82 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "无题", + description: "无描述", + }, + components: { + callout: { + note: "笔记", + abstract: "摘要", + info: "提示", + todo: "待办", + tip: "提示", + success: "成功", + question: "问题", + warning: "警告", + failure: "失败", + danger: "危险", + bug: "错误", + example: "示例", + quote: "引用", + }, + backlinks: { + title: "反向链接", + noBacklinksFound: "无法找到反向链接", + }, + themeToggle: { + lightMode: "亮色模式", + darkMode: "暗色模式", + }, + explorer: { + title: "探索", + }, + footer: { + createdWith: "Created with", + }, + graph: { + title: "关系图谱", + }, + recentNotes: { + title: "最近的笔记", + seeRemainingMore: ({ remaining }) => `查看更多${remaining}篇笔记 →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `包含${targetSlug}`, + linkToOriginal: "指向原始笔记的链接", + }, + search: { + title: "搜索", + searchBarPlaceholder: "搜索些什么", + }, + tableOfContents: { + title: "目录", + }, + contentMeta: { + readingTime: ({ minutes }) => `${minutes}分钟阅读`, + }, + }, + pages: { + rss: { + recentNotes: "最近的笔记", + lastFewNotes: ({ count }) => `最近的${count}条笔记`, + }, + error: { + title: "无法找到", + notFound: "私有笔记或笔记不存在。", + home: "返回首页", + }, + folderContent: { + folder: "文件夹", + itemsUnderFolder: ({ count }) => `此文件夹下有${count}条笔记。`, + }, + tagContent: { + tag: "标签", + tagIndex: "标签索引", + itemsUnderTag: ({ count }) => `此标签下有${count}条笔记。`, + showingFirst: ({ count }) => `显示前${count}个标签。`, + totalTags: ({ count }) => `总共有${count}个标签。`, + }, + }, +} as const satisfies Translation diff --git a/quartz/i18n/locales/zh-TW.ts b/quartz/i18n/locales/zh-TW.ts new file mode 100755 index 0000000..f0db0bf --- /dev/null +++ b/quartz/i18n/locales/zh-TW.ts @@ -0,0 +1,82 @@ +import { Translation } from "./definition" + +export default { + propertyDefaults: { + title: "無題", + description: "無描述", + }, + components: { + callout: { + note: "筆記", + abstract: "摘要", + info: "提示", + todo: "待辦", + tip: "提示", + success: "成功", + question: "問題", + warning: "警告", + failure: "失敗", + danger: "危險", + bug: "錯誤", + example: "範例", + quote: "引用", + }, + backlinks: { + title: "反向連結", + noBacklinksFound: "無法找到反向連結", + }, + themeToggle: { + lightMode: "亮色模式", + darkMode: "暗色模式", + }, + explorer: { + title: "探索", + }, + footer: { + createdWith: "Created with", + }, + graph: { + title: "關係圖譜", + }, + recentNotes: { + title: "最近的筆記", + seeRemainingMore: ({ remaining }) => `查看更多 ${remaining} 篇筆記 →`, + }, + transcludes: { + transcludeOf: ({ targetSlug }) => `包含 ${targetSlug}`, + linkToOriginal: "指向原始筆記的連結", + }, + search: { + title: "搜尋", + searchBarPlaceholder: "搜尋些什麼", + }, + tableOfContents: { + title: "目錄", + }, + contentMeta: { + readingTime: ({ minutes }) => `閱讀時間約 ${minutes} 分鐘`, + }, + }, + pages: { + rss: { + recentNotes: "最近的筆記", + lastFewNotes: ({ count }) => `最近的 ${count} 條筆記`, + }, + error: { + title: "無法找到", + notFound: "私人筆記或筆記不存在。", + home: "返回首頁", + }, + folderContent: { + folder: "資料夾", + itemsUnderFolder: ({ count }) => `此資料夾下有 ${count} 條筆記。`, + }, + tagContent: { + tag: "標籤", + tagIndex: "標籤索引", + itemsUnderTag: ({ count }) => `此標籤下有 ${count} 條筆記。`, + showingFirst: ({ count }) => `顯示前 ${count} 個標籤。`, + totalTags: ({ count }) => `總共有 ${count} 個標籤。`, + }, + }, +} as const satisfies Translation diff --git a/quartz/plugins/emitters/404.tsx b/quartz/plugins/emitters/404.tsx new file mode 100755 index 0000000..2d518b6 --- /dev/null +++ b/quartz/plugins/emitters/404.tsx @@ -0,0 +1,68 @@ +import { QuartzEmitterPlugin } from "../types" +import { QuartzComponentProps } from "../../components/types" +import BodyConstructor from "../../components/Body" +import { pageResources, renderPage } from "../../components/renderPage" +import { FullPageLayout } from "../../cfg" +import { FilePath, FullSlug } from "../../util/path" +import { sharedPageComponents } from "../../../quartz.layout" +import { NotFound } from "../../components" +import { defaultProcessedContent } from "../vfile" +import { write } from "./helpers" +import { i18n } from "../../i18n" +import DepGraph from "../../depgraph" + +export const NotFoundPage: QuartzEmitterPlugin = () => { + const opts: FullPageLayout = { + ...sharedPageComponents, + pageBody: NotFound(), + beforeBody: [], + left: [], + right: [], + } + + const { head: Head, pageBody, footer: Footer } = opts + const Body = BodyConstructor() + + return { + name: "404Page", + getQuartzComponents() { + return [Head, Body, pageBody, Footer] + }, + async getDependencyGraph(_ctx, _content, _resources) { + return new DepGraph() + }, + async emit(ctx, _content, resources): Promise { + const cfg = ctx.cfg.configuration + const slug = "404" as FullSlug + + const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`) + const path = url.pathname as FullSlug + const notFound = i18n(cfg.locale).pages.error.title + const [tree, vfile] = defaultProcessedContent({ + slug, + text: notFound, + description: notFound, + frontmatter: { title: notFound, tags: [] }, + }) + const externalResources = pageResources(path, vfile.data, resources) + const componentData: QuartzComponentProps = { + ctx, + fileData: vfile.data, + externalResources, + cfg, + children: [], + tree, + allFiles: [], + } + + return [ + await write({ + ctx, + content: renderPage(cfg, slug, componentData, opts, externalResources), + slug, + ext: ".html", + }), + ] + }, + } +} diff --git a/quartz/plugins/emitters/aliases.ts b/quartz/plugins/emitters/aliases.ts new file mode 100755 index 0000000..af3578e --- /dev/null +++ b/quartz/plugins/emitters/aliases.ts @@ -0,0 +1,81 @@ +import { FilePath, FullSlug, joinSegments, resolveRelative, simplifySlug } from "../../util/path" +import { QuartzEmitterPlugin } from "../types" +import path from "path" +import { write } from "./helpers" +import DepGraph from "../../depgraph" + +export const AliasRedirects: QuartzEmitterPlugin = () => ({ + name: "AliasRedirects", + getQuartzComponents() { + return [] + }, + async getDependencyGraph(ctx, content, _resources) { + const graph = new DepGraph() + + const { argv } = ctx + for (const [_tree, file] of content) { + const dir = path.posix.relative(argv.directory, path.dirname(file.data.filePath!)) + const aliases = file.data.frontmatter?.aliases ?? [] + const slugs = aliases.map((alias) => path.posix.join(dir, alias) as FullSlug) + const permalink = file.data.frontmatter?.permalink + if (typeof permalink === "string") { + slugs.push(permalink as FullSlug) + } + + for (let slug of slugs) { + // fix any slugs that have trailing slash + if (slug.endsWith("/")) { + slug = joinSegments(slug, "index") as FullSlug + } + + graph.addEdge(file.data.filePath!, joinSegments(argv.output, slug + ".html") as FilePath) + } + } + + return graph + }, + async emit(ctx, content, _resources): Promise { + const { argv } = ctx + const fps: FilePath[] = [] + + for (const [_tree, file] of content) { + const ogSlug = simplifySlug(file.data.slug!) + const dir = path.posix.relative(argv.directory, path.dirname(file.data.filePath!)) + const aliases = file.data.frontmatter?.aliases ?? [] + const slugs: FullSlug[] = aliases.map((alias) => path.posix.join(dir, alias) as FullSlug) + const permalink = file.data.frontmatter?.permalink + if (typeof permalink === "string") { + slugs.push(permalink as FullSlug) + } + + for (let slug of slugs) { + // fix any slugs that have trailing slash + if (slug.endsWith("/")) { + slug = joinSegments(slug, "index") as FullSlug + } + + const redirUrl = resolveRelative(slug, file.data.slug!) + const fp = await write({ + ctx, + content: ` + + + + ${ogSlug} + + + + + + + `, + slug, + ext: ".html", + }) + + fps.push(fp) + } + } + return fps + }, +}) diff --git a/quartz/plugins/emitters/assets.ts b/quartz/plugins/emitters/assets.ts new file mode 100755 index 0000000..036b27d --- /dev/null +++ b/quartz/plugins/emitters/assets.ts @@ -0,0 +1,58 @@ +import { FilePath, joinSegments, slugifyFilePath } from "../../util/path" +import { QuartzEmitterPlugin } from "../types" +import path from "path" +import fs from "fs" +import { glob } from "../../util/glob" +import DepGraph from "../../depgraph" +import { Argv } from "../../util/ctx" +import { QuartzConfig } from "../../cfg" + +const filesToCopy = async (argv: Argv, cfg: QuartzConfig) => { + // glob all non MD files in content folder and copy it over + return await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns]) +} + +export const Assets: QuartzEmitterPlugin = () => { + return { + name: "Assets", + getQuartzComponents() { + return [] + }, + async getDependencyGraph(ctx, _content, _resources) { + const { argv, cfg } = ctx + const graph = new DepGraph() + + const fps = await filesToCopy(argv, cfg) + + for (const fp of fps) { + const ext = path.extname(fp) + const src = joinSegments(argv.directory, fp) as FilePath + const name = (slugifyFilePath(fp as FilePath, true) + ext) as FilePath + + const dest = joinSegments(argv.output, name) as FilePath + + graph.addEdge(src, dest) + } + + return graph + }, + async emit({ argv, cfg }, _content, _resources): Promise { + const assetsPath = argv.output + const fps = await filesToCopy(argv, cfg) + const res: FilePath[] = [] + for (const fp of fps) { + const ext = path.extname(fp) + const src = joinSegments(argv.directory, fp) as FilePath + const name = (slugifyFilePath(fp as FilePath, true) + ext) as FilePath + + const dest = joinSegments(assetsPath, name) as FilePath + const dir = path.dirname(dest) as FilePath + await fs.promises.mkdir(dir, { recursive: true }) // ensure dir exists + await fs.promises.copyFile(src, dest) + res.push(dest) + } + + return res + }, + } +} diff --git a/quartz/plugins/emitters/cname.ts b/quartz/plugins/emitters/cname.ts new file mode 100755 index 0000000..cbed2a8 --- /dev/null +++ b/quartz/plugins/emitters/cname.ts @@ -0,0 +1,33 @@ +import { FilePath, joinSegments } from "../../util/path" +import { QuartzEmitterPlugin } from "../types" +import fs from "fs" +import chalk from "chalk" +import DepGraph from "../../depgraph" + +export function extractDomainFromBaseUrl(baseUrl: string) { + const url = new URL(`https://${baseUrl}`) + return url.hostname +} + +export const CNAME: QuartzEmitterPlugin = () => ({ + name: "CNAME", + getQuartzComponents() { + return [] + }, + async getDependencyGraph(_ctx, _content, _resources) { + return new DepGraph() + }, + async emit({ argv, cfg }, _content, _resources): Promise { + if (!cfg.configuration.baseUrl) { + console.warn(chalk.yellow("CNAME emitter requires `baseUrl` to be set in your configuration")) + return [] + } + const path = joinSegments(argv.output, "CNAME") + const content = extractDomainFromBaseUrl(cfg.configuration.baseUrl) + if (!content) { + return [] + } + fs.writeFileSync(path, content) + return [path] as FilePath[] + }, +}) diff --git a/quartz/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts new file mode 100755 index 0000000..0827830 --- /dev/null +++ b/quartz/plugins/emitters/componentResources.ts @@ -0,0 +1,290 @@ +import { FilePath, FullSlug, joinSegments } from "../../util/path" +import { QuartzEmitterPlugin } from "../types" + +// @ts-ignore +import spaRouterScript from "../../components/scripts/spa.inline" +// @ts-ignore +import popoverScript from "../../components/scripts/popover.inline" +import styles from "../../styles/custom.scss" +import popoverStyle from "../../components/styles/popover.scss" +import { BuildCtx } from "../../util/ctx" +import { QuartzComponent } from "../../components/types" +import { googleFontHref, joinStyles } from "../../util/theme" +import { Features, transform } from "lightningcss" +import { transform as transpile } from "esbuild" +import { write } from "./helpers" +import DepGraph from "../../depgraph" + +type ComponentResources = { + css: string[] + beforeDOMLoaded: string[] + afterDOMLoaded: string[] +} + +function getComponentResources(ctx: BuildCtx): ComponentResources { + const allComponents: Set = new Set() + for (const emitter of ctx.cfg.plugins.emitters) { + const components = emitter.getQuartzComponents(ctx) + for (const component of components) { + allComponents.add(component) + } + } + + const componentResources = { + css: new Set(), + beforeDOMLoaded: new Set(), + afterDOMLoaded: new Set(), + } + + for (const component of allComponents) { + const { css, beforeDOMLoaded, afterDOMLoaded } = component + if (css) { + componentResources.css.add(css) + } + if (beforeDOMLoaded) { + componentResources.beforeDOMLoaded.add(beforeDOMLoaded) + } + if (afterDOMLoaded) { + componentResources.afterDOMLoaded.add(afterDOMLoaded) + } + } + + return { + css: [...componentResources.css], + beforeDOMLoaded: [...componentResources.beforeDOMLoaded], + afterDOMLoaded: [...componentResources.afterDOMLoaded], + } +} + +async function joinScripts(scripts: string[]): Promise { + // wrap with iife to prevent scope collision + const script = scripts.map((script) => `(function () {${script}})();`).join("\n") + + // minify with esbuild + const res = await transpile(script, { + minify: true, + }) + + return res.code +} + +function addGlobalPageResources(ctx: BuildCtx, componentResources: ComponentResources) { + const cfg = ctx.cfg.configuration + + // popovers + if (cfg.enablePopovers) { + componentResources.afterDOMLoaded.push(popoverScript) + componentResources.css.push(popoverStyle) + } + + if (cfg.analytics?.provider === "google") { + const tagId = cfg.analytics.tagId + componentResources.afterDOMLoaded.push(` + const gtagScript = document.createElement("script") + gtagScript.src = "https://www.googletagmanager.com/gtag/js?id=${tagId}" + gtagScript.async = true + document.head.appendChild(gtagScript) + + window.dataLayer = window.dataLayer || []; + function gtag() { dataLayer.push(arguments); } + gtag("js", new Date()); + gtag("config", "${tagId}", { send_page_view: false }); + + document.addEventListener("nav", () => { + gtag("event", "page_view", { + page_title: document.title, + page_location: location.href, + }); + });`) + } else if (cfg.analytics?.provider === "plausible") { + const plausibleHost = cfg.analytics.host ?? "https://plausible.io" + componentResources.afterDOMLoaded.push(` + const plausibleScript = document.createElement("script") + plausibleScript.src = "${plausibleHost}/js/script.manual.js" + plausibleScript.setAttribute("data-domain", location.hostname) + plausibleScript.defer = true + document.head.appendChild(plausibleScript) + + window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) } + + document.addEventListener("nav", () => { + plausible("pageview") + }) + `) + } else if (cfg.analytics?.provider === "umami") { + componentResources.afterDOMLoaded.push(` + const umamiScript = document.createElement("script") + umamiScript.src = "${cfg.analytics.host ?? "https://analytics.umami.is"}/script.js" + umamiScript.setAttribute("data-website-id", "${cfg.analytics.websiteId}") + umamiScript.async = true + + document.head.appendChild(umamiScript) + `) + } else if (cfg.analytics?.provider === "goatcounter") { + componentResources.afterDOMLoaded.push(` + const goatcounterScript = document.createElement("script") + goatcounterScript.src = "${cfg.analytics.scriptSrc ?? "https://gc.zgo.at/count.js"}" + goatcounterScript.async = true + goatcounterScript.setAttribute("data-goatcounter", + "https://${cfg.analytics.websiteId}.${cfg.analytics.host ?? "goatcounter.com"}/count") + document.head.appendChild(goatcounterScript) + `) + } else if (cfg.analytics?.provider === "posthog") { + componentResources.afterDOMLoaded.push(` + const posthogScript = document.createElement("script") + posthogScript.innerHTML= \`!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys onSessionId".split(" "),n=0;n window.location.assign(url) + window.addCleanup = () => {} + const event = new CustomEvent("nav", { detail: { url: document.body.dataset.slug } }) + document.dispatchEvent(event) + `) + } +} + +// This emitter should not update the `resources` parameter. If it does, partial +// rebuilds may not work as expected. +export const ComponentResources: QuartzEmitterPlugin = () => { + return { + name: "ComponentResources", + getQuartzComponents() { + return [] + }, + async getDependencyGraph(_ctx, _content, _resources) { + return new DepGraph() + }, + async emit(ctx, _content, _resources): Promise { + const promises: Promise[] = [] + const cfg = ctx.cfg.configuration + // component specific scripts and styles + const componentResources = getComponentResources(ctx) + let googleFontsStyleSheet = "" + if (cfg.theme.fontOrigin === "local") { + // let the user do it themselves in css + } else if (cfg.theme.fontOrigin === "googleFonts" && !cfg.theme.cdnCaching) { + // when cdnCaching is true, we link to google fonts in Head.tsx + let match + + const fontSourceRegex = /url\((https:\/\/fonts.gstatic.com\/s\/[^)]+\.(woff2|ttf))\)/g + + googleFontsStyleSheet = await ( + await fetch(googleFontHref(ctx.cfg.configuration.theme)) + ).text() + + while ((match = fontSourceRegex.exec(googleFontsStyleSheet)) !== null) { + // match[0] is the `url(path)`, match[1] is the `path` + const url = match[1] + // the static name of this file. + const [filename, ext] = url.split("/").pop()!.split(".") + + googleFontsStyleSheet = googleFontsStyleSheet.replace( + url, + `https://${cfg.baseUrl}/static/fonts/${filename}.ttf`, + ) + + promises.push( + fetch(url) + .then((res) => { + if (!res.ok) { + throw new Error(`Failed to fetch font`) + } + return res.arrayBuffer() + }) + .then((buf) => + write({ + ctx, + slug: joinSegments("static", "fonts", filename) as FullSlug, + ext: `.${ext}`, + content: Buffer.from(buf), + }), + ), + ) + } + } + + // important that this goes *after* component scripts + // as the "nav" event gets triggered here and we should make sure + // that everyone else had the chance to register a listener for it + addGlobalPageResources(ctx, componentResources) + + const stylesheet = joinStyles( + ctx.cfg.configuration.theme, + googleFontsStyleSheet, + ...componentResources.css, + styles, + ) + const [prescript, postscript] = await Promise.all([ + joinScripts(componentResources.beforeDOMLoaded), + joinScripts(componentResources.afterDOMLoaded), + ]) + + promises.push( + write({ + ctx, + slug: "index" as FullSlug, + ext: ".css", + content: transform({ + filename: "index.css", + code: Buffer.from(stylesheet), + minify: true, + targets: { + safari: (15 << 16) | (6 << 8), // 15.6 + ios_saf: (15 << 16) | (6 << 8), // 15.6 + edge: 115 << 16, + firefox: 102 << 16, + chrome: 109 << 16, + }, + include: Features.MediaQueries, + }).code.toString(), + }), + write({ + ctx, + slug: "prescript" as FullSlug, + ext: ".js", + content: prescript, + }), + write({ + ctx, + slug: "postscript" as FullSlug, + ext: ".js", + content: postscript, + }), + ) + + return await Promise.all(promises) + }, + } +} diff --git a/quartz/plugins/emitters/contentIndex.ts b/quartz/plugins/emitters/contentIndex.ts new file mode 100755 index 0000000..c0fef86 --- /dev/null +++ b/quartz/plugins/emitters/contentIndex.ts @@ -0,0 +1,185 @@ +import { Root } from "hast" +import { GlobalConfiguration } from "../../cfg" +import { getDate } from "../../components/Date" +import { escapeHTML } from "../../util/escape" +import { FilePath, FullSlug, SimpleSlug, joinSegments, simplifySlug } from "../../util/path" +import { QuartzEmitterPlugin } from "../types" +import { toHtml } from "hast-util-to-html" +import { write } from "./helpers" +import { i18n } from "../../i18n" +import DepGraph from "../../depgraph" + +export type ContentIndex = Map +export type ContentDetails = { + title: string + links: SimpleSlug[] + tags: string[] + content: string + richContent?: string + date?: Date + description?: string +} + +interface Options { + enableSiteMap: boolean + enableRSS: boolean + rssLimit?: number + rssFullHtml: boolean + includeEmptyFiles: boolean +} + +const defaultOptions: Options = { + enableSiteMap: true, + enableRSS: true, + rssLimit: 10, + rssFullHtml: false, + includeEmptyFiles: true, +} + +function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string { + const base = cfg.baseUrl ?? "" + const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => ` + https://${joinSegments(base, encodeURI(slug))} + ${content.date && `${content.date.toISOString()}`} + ` + const urls = Array.from(idx) + .map(([slug, content]) => createURLEntry(simplifySlug(slug), content)) + .join("") + return `${urls}` +} + +function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: number): string { + const base = cfg.baseUrl ?? "" + + const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => ` + ${escapeHTML(content.title)} + https://${joinSegments(base, encodeURI(slug))} + https://${joinSegments(base, encodeURI(slug))} + ${content.richContent ?? content.description} + ${content.date?.toUTCString()} + ` + + const items = Array.from(idx) + .sort(([_, f1], [__, f2]) => { + if (f1.date && f2.date) { + return f2.date.getTime() - f1.date.getTime() + } else if (f1.date && !f2.date) { + return -1 + } else if (!f1.date && f2.date) { + return 1 + } + + return f1.title.localeCompare(f2.title) + }) + .map(([slug, content]) => createURLEntry(simplifySlug(slug), content)) + .slice(0, limit ?? idx.size) + .join("") + + return ` + + + ${escapeHTML(cfg.pageTitle)} + https://${base} + ${!!limit ? i18n(cfg.locale).pages.rss.lastFewNotes({ count: limit }) : i18n(cfg.locale).pages.rss.recentNotes} on ${escapeHTML( + cfg.pageTitle, + )} + Quartz -- quartz.jzhao.xyz + ${items} + + ` +} + +export const ContentIndex: QuartzEmitterPlugin> = (opts) => { + opts = { ...defaultOptions, ...opts } + return { + name: "ContentIndex", + async getDependencyGraph(ctx, content, _resources) { + const graph = new DepGraph() + + for (const [_tree, file] of content) { + const sourcePath = file.data.filePath! + + graph.addEdge( + sourcePath, + joinSegments(ctx.argv.output, "static/contentIndex.json") as FilePath, + ) + if (opts?.enableSiteMap) { + graph.addEdge(sourcePath, joinSegments(ctx.argv.output, "sitemap.xml") as FilePath) + } + if (opts?.enableRSS) { + graph.addEdge(sourcePath, joinSegments(ctx.argv.output, "index.xml") as FilePath) + } + } + + return graph + }, + async emit(ctx, content, _resources) { + const cfg = ctx.cfg.configuration + const emitted: FilePath[] = [] + const linkIndex: ContentIndex = new Map() + for (const [tree, file] of content) { + const slug = file.data.slug! + const date = getDate(ctx.cfg.configuration, file.data) ?? new Date() + if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) { + linkIndex.set(slug, { + title: file.data.frontmatter?.title!, + links: file.data.links ?? [], + tags: file.data.frontmatter?.tags ?? [], + content: file.data.text ?? "", + richContent: opts?.rssFullHtml + ? escapeHTML(toHtml(tree as Root, { allowDangerousHtml: true })) + : undefined, + date: date, + description: file.data.description ?? "", + }) + } + } + + if (opts?.enableSiteMap) { + emitted.push( + await write({ + ctx, + content: generateSiteMap(cfg, linkIndex), + slug: "sitemap" as FullSlug, + ext: ".xml", + }), + ) + } + + if (opts?.enableRSS) { + emitted.push( + await write({ + ctx, + content: generateRSSFeed(cfg, linkIndex, opts.rssLimit), + slug: "index" as FullSlug, + ext: ".xml", + }), + ) + } + + const fp = joinSegments("static", "contentIndex") as FullSlug + const simplifiedIndex = Object.fromEntries( + Array.from(linkIndex).map(([slug, content]) => { + // remove description and from content index as nothing downstream + // actually uses it. we only keep it in the index as we need it + // for the RSS feed + delete content.description + delete content.date + return [slug, content] + }), + ) + + emitted.push( + await write({ + ctx, + content: JSON.stringify(simplifiedIndex), + slug: fp, + ext: ".json", + }), + ) + + return emitted + }, + getQuartzComponents: () => [], + } +} diff --git a/quartz/plugins/emitters/contentPage.tsx b/quartz/plugins/emitters/contentPage.tsx new file mode 100755 index 0000000..8788f33 --- /dev/null +++ b/quartz/plugins/emitters/contentPage.tsx @@ -0,0 +1,142 @@ +import path from "path" +import { visit } from "unist-util-visit" +import { Root } from "hast" +import { VFile } from "vfile" +import { QuartzEmitterPlugin } from "../types" +import { QuartzComponentProps } from "../../components/types" +import HeaderConstructor from "../../components/Header" +import BodyConstructor from "../../components/Body" +import { pageResources, renderPage } from "../../components/renderPage" +import { FullPageLayout } from "../../cfg" +import { Argv } from "../../util/ctx" +import { FilePath, isRelativeURL, joinSegments, pathToRoot } from "../../util/path" +import { defaultContentPageLayout, sharedPageComponents } from "../../../quartz.layout" +import { Content } from "../../components" +import chalk from "chalk" +import { write } from "./helpers" +import DepGraph from "../../depgraph" + +// get all the dependencies for the markdown file +// eg. images, scripts, stylesheets, transclusions +const parseDependencies = (argv: Argv, hast: Root, file: VFile): string[] => { + const dependencies: string[] = [] + + visit(hast, "element", (elem): void => { + let ref: string | null = null + + if ( + ["script", "img", "audio", "video", "source", "iframe"].includes(elem.tagName) && + elem?.properties?.src + ) { + ref = elem.properties.src.toString() + } else if (["a", "link"].includes(elem.tagName) && elem?.properties?.href) { + // transclusions will create a tags with relative hrefs + ref = elem.properties.href.toString() + } + + // if it is a relative url, its a local file and we need to add + // it to the dependency graph. otherwise, ignore + if (ref === null || !isRelativeURL(ref)) { + return + } + + let fp = path.join(file.data.filePath!, path.relative(argv.directory, ref)).replace(/\\/g, "/") + // markdown files have the .md extension stripped in hrefs, add it back here + if (!fp.split("/").pop()?.includes(".")) { + fp += ".md" + } + dependencies.push(fp) + }) + + return dependencies +} + +export const ContentPage: QuartzEmitterPlugin> = (userOpts) => { + const opts: FullPageLayout = { + ...sharedPageComponents, + ...defaultContentPageLayout, + pageBody: Content(), + ...userOpts, + } + + const { head: Head, header, beforeBody, pageBody, afterBody, left, right, footer: Footer } = opts + const Header = HeaderConstructor() + const Body = BodyConstructor() + + return { + name: "ContentPage", + getQuartzComponents() { + return [ + Head, + Header, + Body, + ...header, + ...beforeBody, + pageBody, + ...afterBody, + ...left, + ...right, + Footer, + ] + }, + async getDependencyGraph(ctx, content, _resources) { + const graph = new DepGraph() + + for (const [tree, file] of content) { + const sourcePath = file.data.filePath! + const slug = file.data.slug! + graph.addEdge(sourcePath, joinSegments(ctx.argv.output, slug + ".html") as FilePath) + + parseDependencies(ctx.argv, tree as Root, file).forEach((dep) => { + graph.addEdge(dep as FilePath, sourcePath) + }) + } + + return graph + }, + async emit(ctx, content, resources): Promise { + const cfg = ctx.cfg.configuration + const fps: FilePath[] = [] + const allFiles = content.map((c) => c[1].data) + + let containsIndex = false + for (const [tree, file] of content) { + const slug = file.data.slug! + if (slug === "index") { + containsIndex = true + } + + const externalResources = pageResources(pathToRoot(slug), file.data, resources) + const componentData: QuartzComponentProps = { + ctx, + fileData: file.data, + externalResources, + cfg, + children: [], + tree, + allFiles, + } + + const content = renderPage(cfg, slug, componentData, opts, externalResources) + const fp = await write({ + ctx, + content, + slug, + ext: ".html", + }) + + fps.push(fp) + } + + if (!containsIndex && !ctx.argv.fastRebuild) { + console.log( + chalk.yellow( + `\nWarning: you seem to be missing an \`index.md\` home page file at the root of your \`${ctx.argv.directory}\` folder. This may cause errors when deploying.`, + ), + ) + } + + return fps + }, + } +} diff --git a/quartz/plugins/emitters/folderPage.tsx b/quartz/plugins/emitters/folderPage.tsx new file mode 100755 index 0000000..bafaec9 --- /dev/null +++ b/quartz/plugins/emitters/folderPage.tsx @@ -0,0 +1,145 @@ +import { QuartzEmitterPlugin } from "../types" +import { QuartzComponentProps } from "../../components/types" +import HeaderConstructor from "../../components/Header" +import BodyConstructor from "../../components/Body" +import { pageResources, renderPage } from "../../components/renderPage" +import { ProcessedContent, QuartzPluginData, defaultProcessedContent } from "../vfile" +import { FullPageLayout } from "../../cfg" +import path from "path" +import { + FilePath, + FullSlug, + SimpleSlug, + stripSlashes, + joinSegments, + pathToRoot, + simplifySlug, +} from "../../util/path" +import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout" +import { FolderContent } from "../../components" +import { write } from "./helpers" +import { i18n } from "../../i18n" +import DepGraph from "../../depgraph" + +interface FolderPageOptions extends FullPageLayout { + sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number +} + +export const FolderPage: QuartzEmitterPlugin> = (userOpts) => { + const opts: FullPageLayout = { + ...sharedPageComponents, + ...defaultListPageLayout, + pageBody: FolderContent({ sort: userOpts?.sort }), + ...userOpts, + } + + const { head: Head, header, beforeBody, pageBody, afterBody, left, right, footer: Footer } = opts + const Header = HeaderConstructor() + const Body = BodyConstructor() + + return { + name: "FolderPage", + getQuartzComponents() { + return [ + Head, + Header, + Body, + ...header, + ...beforeBody, + pageBody, + ...afterBody, + ...left, + ...right, + Footer, + ] + }, + async getDependencyGraph(_ctx, content, _resources) { + // Example graph: + // nested/file.md --> nested/index.html + // nested/file2.md ------^ + const graph = new DepGraph() + + content.map(([_tree, vfile]) => { + const slug = vfile.data.slug + const folderName = path.dirname(slug ?? "") as SimpleSlug + if (slug && folderName !== "." && folderName !== "tags") { + graph.addEdge(vfile.data.filePath!, joinSegments(folderName, "index.html") as FilePath) + } + }) + + return graph + }, + async emit(ctx, content, resources): Promise { + const fps: FilePath[] = [] + const allFiles = content.map((c) => c[1].data) + const cfg = ctx.cfg.configuration + + const folders: Set = new Set( + allFiles.flatMap((data) => { + return data.slug + ? _getFolders(data.slug).filter( + (folderName) => folderName !== "." && folderName !== "tags", + ) + : [] + }), + ) + + const folderDescriptions: Record = Object.fromEntries( + [...folders].map((folder) => [ + folder, + defaultProcessedContent({ + slug: joinSegments(folder, "index") as FullSlug, + frontmatter: { + title: `${i18n(cfg.locale).pages.folderContent.folder}: ${folder}`, + tags: [], + }, + }), + ]), + ) + + for (const [tree, file] of content) { + const slug = stripSlashes(simplifySlug(file.data.slug!)) as SimpleSlug + if (folders.has(slug)) { + folderDescriptions[slug] = [tree, file] + } + } + + for (const folder of folders) { + const slug = joinSegments(folder, "index") as FullSlug + const [tree, file] = folderDescriptions[folder] + const externalResources = pageResources(pathToRoot(slug), file.data, resources) + const componentData: QuartzComponentProps = { + ctx, + fileData: file.data, + externalResources, + cfg, + children: [], + tree, + allFiles, + } + + const content = renderPage(cfg, slug, componentData, opts, externalResources) + const fp = await write({ + ctx, + content, + slug, + ext: ".html", + }) + + fps.push(fp) + } + return fps + }, + } +} + +function _getFolders(slug: FullSlug): SimpleSlug[] { + var folderName = path.dirname(slug ?? "") as SimpleSlug + const parentFolderNames = [folderName] + + while (folderName !== ".") { + folderName = path.dirname(folderName ?? "") as SimpleSlug + parentFolderNames.push(folderName) + } + return parentFolderNames +} diff --git a/quartz/plugins/emitters/helpers.ts b/quartz/plugins/emitters/helpers.ts new file mode 100755 index 0000000..523151c --- /dev/null +++ b/quartz/plugins/emitters/helpers.ts @@ -0,0 +1,19 @@ +import path from "path" +import fs from "fs" +import { BuildCtx } from "../../util/ctx" +import { FilePath, FullSlug, joinSegments } from "../../util/path" + +type WriteOptions = { + ctx: BuildCtx + slug: FullSlug + ext: `.${string}` | "" + content: string | Buffer +} + +export const write = async ({ ctx, slug, ext, content }: WriteOptions): Promise => { + const pathToPage = joinSegments(ctx.argv.output, slug + ext) as FilePath + const dir = path.dirname(pathToPage) + await fs.promises.mkdir(dir, { recursive: true }) + await fs.promises.writeFile(pathToPage, content) + return pathToPage +} diff --git a/quartz/plugins/emitters/index.ts b/quartz/plugins/emitters/index.ts new file mode 100755 index 0000000..bc378c4 --- /dev/null +++ b/quartz/plugins/emitters/index.ts @@ -0,0 +1,10 @@ +export { ContentPage } from "./contentPage" +export { TagPage } from "./tagPage" +export { FolderPage } from "./folderPage" +export { ContentIndex } from "./contentIndex" +export { AliasRedirects } from "./aliases" +export { Assets } from "./assets" +export { Static } from "./static" +export { ComponentResources } from "./componentResources" +export { NotFoundPage } from "./404" +export { CNAME } from "./cname" diff --git a/quartz/plugins/emitters/static.ts b/quartz/plugins/emitters/static.ts new file mode 100755 index 0000000..c52c628 --- /dev/null +++ b/quartz/plugins/emitters/static.ts @@ -0,0 +1,35 @@ +import { FilePath, QUARTZ, joinSegments } from "../../util/path" +import { QuartzEmitterPlugin } from "../types" +import fs from "fs" +import { glob } from "../../util/glob" +import DepGraph from "../../depgraph" + +export const Static: QuartzEmitterPlugin = () => ({ + name: "Static", + getQuartzComponents() { + return [] + }, + async getDependencyGraph({ argv, cfg }, _content, _resources) { + const graph = new DepGraph() + + const staticPath = joinSegments(QUARTZ, "static") + const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns) + for (const fp of fps) { + graph.addEdge( + joinSegments("static", fp) as FilePath, + joinSegments(argv.output, "static", fp) as FilePath, + ) + } + + return graph + }, + async emit({ argv, cfg }, _content, _resources): Promise { + const staticPath = joinSegments(QUARTZ, "static") + const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns) + await fs.promises.cp(staticPath, joinSegments(argv.output, "static"), { + recursive: true, + dereference: true, + }) + return fps.map((fp) => joinSegments(argv.output, "static", fp)) as FilePath[] + }, +}) diff --git a/quartz/plugins/emitters/tagPage.tsx b/quartz/plugins/emitters/tagPage.tsx new file mode 100755 index 0000000..9913e7d --- /dev/null +++ b/quartz/plugins/emitters/tagPage.tsx @@ -0,0 +1,142 @@ +import { QuartzEmitterPlugin } from "../types" +import { QuartzComponentProps } from "../../components/types" +import HeaderConstructor from "../../components/Header" +import BodyConstructor from "../../components/Body" +import { pageResources, renderPage } from "../../components/renderPage" +import { ProcessedContent, QuartzPluginData, defaultProcessedContent } from "../vfile" +import { FullPageLayout } from "../../cfg" +import { + FilePath, + FullSlug, + getAllSegmentPrefixes, + joinSegments, + pathToRoot, +} from "../../util/path" +import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout" +import { TagContent } from "../../components" +import { write } from "./helpers" +import { i18n } from "../../i18n" +import DepGraph from "../../depgraph" + +interface TagPageOptions extends FullPageLayout { + sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number +} + +export const TagPage: QuartzEmitterPlugin> = (userOpts) => { + const opts: FullPageLayout = { + ...sharedPageComponents, + ...defaultListPageLayout, + pageBody: TagContent({ sort: userOpts?.sort }), + ...userOpts, + } + + const { head: Head, header, beforeBody, pageBody, afterBody, left, right, footer: Footer } = opts + const Header = HeaderConstructor() + const Body = BodyConstructor() + + return { + name: "TagPage", + getQuartzComponents() { + return [ + Head, + Header, + Body, + ...header, + ...beforeBody, + pageBody, + ...afterBody, + ...left, + ...right, + Footer, + ] + }, + async getDependencyGraph(ctx, content, _resources) { + const graph = new DepGraph() + + for (const [_tree, file] of content) { + const sourcePath = file.data.filePath! + const tags = (file.data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes) + // if the file has at least one tag, it is used in the tag index page + if (tags.length > 0) { + tags.push("index") + } + + for (const tag of tags) { + graph.addEdge( + sourcePath, + joinSegments(ctx.argv.output, "tags", tag + ".html") as FilePath, + ) + } + } + + return graph + }, + async emit(ctx, content, resources): Promise { + const fps: FilePath[] = [] + const allFiles = content.map((c) => c[1].data) + const cfg = ctx.cfg.configuration + + const tags: Set = new Set( + allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes), + ) + + // add base tag + tags.add("index") + + const tagDescriptions: Record = Object.fromEntries( + [...tags].map((tag) => { + const title = + tag === "index" + ? i18n(cfg.locale).pages.tagContent.tagIndex + : `${i18n(cfg.locale).pages.tagContent.tag}: ${tag}` + return [ + tag, + defaultProcessedContent({ + slug: joinSegments("tags", tag) as FullSlug, + frontmatter: { title, tags: [] }, + }), + ] + }), + ) + + for (const [tree, file] of content) { + const slug = file.data.slug! + if (slug.startsWith("tags/")) { + const tag = slug.slice("tags/".length) + if (tags.has(tag)) { + tagDescriptions[tag] = [tree, file] + if (file.data.frontmatter?.title === tag) { + file.data.frontmatter.title = `${i18n(cfg.locale).pages.tagContent.tag}: ${tag}` + } + } + } + } + + for (const tag of tags) { + const slug = joinSegments("tags", tag) as FullSlug + const [tree, file] = tagDescriptions[tag] + const externalResources = pageResources(pathToRoot(slug), file.data, resources) + const componentData: QuartzComponentProps = { + ctx, + fileData: file.data, + externalResources, + cfg, + children: [], + tree, + allFiles, + } + + const content = renderPage(cfg, slug, componentData, opts, externalResources) + const fp = await write({ + ctx, + content, + slug: file.data.slug!, + ext: ".html", + }) + + fps.push(fp) + } + return fps + }, + } +} diff --git a/quartz/plugins/filters/draft.ts b/quartz/plugins/filters/draft.ts new file mode 100755 index 0000000..e8f1d4e --- /dev/null +++ b/quartz/plugins/filters/draft.ts @@ -0,0 +1,10 @@ +import { QuartzFilterPlugin } from "../types" + +export const RemoveDrafts: QuartzFilterPlugin<{}> = () => ({ + name: "RemoveDrafts", + shouldPublish(_ctx, [_tree, vfile]) { + const draftFlag: boolean = + vfile.data?.frontmatter?.draft === true || vfile.data?.frontmatter?.draft === "true" + return !draftFlag + }, +}) diff --git a/quartz/plugins/filters/explicit.ts b/quartz/plugins/filters/explicit.ts new file mode 100755 index 0000000..e2558e8 --- /dev/null +++ b/quartz/plugins/filters/explicit.ts @@ -0,0 +1,8 @@ +import { QuartzFilterPlugin } from "../types" + +export const ExplicitPublish: QuartzFilterPlugin = () => ({ + name: "ExplicitPublish", + shouldPublish(_ctx, [_tree, vfile]) { + return vfile.data?.frontmatter?.publish === true || vfile.data?.frontmatter?.publish === "true" + }, +}) diff --git a/quartz/plugins/filters/index.ts b/quartz/plugins/filters/index.ts new file mode 100755 index 0000000..d937143 --- /dev/null +++ b/quartz/plugins/filters/index.ts @@ -0,0 +1,2 @@ +export { RemoveDrafts } from "./draft" +export { ExplicitPublish } from "./explicit" diff --git a/quartz/plugins/index.ts b/quartz/plugins/index.ts new file mode 100755 index 0000000..df9fd1d --- /dev/null +++ b/quartz/plugins/index.ts @@ -0,0 +1,52 @@ +import { StaticResources } from "../util/resources" +import { FilePath, FullSlug } from "../util/path" +import { BuildCtx } from "../util/ctx" + +export function getStaticResourcesFromPlugins(ctx: BuildCtx) { + const staticResources: StaticResources = { + css: [], + js: [], + } + + for (const transformer of ctx.cfg.plugins.transformers) { + const res = transformer.externalResources ? transformer.externalResources(ctx) : {} + if (res?.js) { + staticResources.js.push(...res.js) + } + if (res?.css) { + staticResources.css.push(...res.css) + } + } + + // if serving locally, listen for rebuilds and reload the page + if (ctx.argv.serve) { + const wsUrl = ctx.argv.remoteDevHost + ? `wss://${ctx.argv.remoteDevHost}:${ctx.argv.wsPort}` + : `ws://localhost:${ctx.argv.wsPort}` + + staticResources.js.push({ + loadTime: "afterDOMReady", + contentType: "inline", + script: ` + const socket = new WebSocket('${wsUrl}') + // reload(true) ensures resources like images and scripts are fetched again in firefox + socket.addEventListener('message', () => document.location.reload(true)) + `, + }) + } + + return staticResources +} + +export * from "./transformers" +export * from "./filters" +export * from "./emitters" + +declare module "vfile" { + // inserted in processors.ts + interface DataMap { + slug: FullSlug + filePath: FilePath + relativePath: FilePath + } +} diff --git a/quartz/plugins/transformers/citations.ts b/quartz/plugins/transformers/citations.ts new file mode 100755 index 0000000..dcac41b --- /dev/null +++ b/quartz/plugins/transformers/citations.ts @@ -0,0 +1,54 @@ +import rehypeCitation from "rehype-citation" +import { PluggableList } from "unified" +import { visit } from "unist-util-visit" +import { QuartzTransformerPlugin } from "../types" + +export interface Options { + bibliographyFile: string + suppressBibliography: boolean + linkCitations: boolean + csl: string +} + +const defaultOptions: Options = { + bibliographyFile: "./bibliography.bib", + suppressBibliography: false, + linkCitations: false, + csl: "apa", +} + +export const Citations: QuartzTransformerPlugin> = (userOpts) => { + const opts = { ...defaultOptions, ...userOpts } + return { + name: "Citations", + htmlPlugins(ctx) { + const plugins: PluggableList = [] + + // Add rehype-citation to the list of plugins + plugins.push([ + rehypeCitation, + { + bibliography: opts.bibliographyFile, + suppressBibliography: opts.suppressBibliography, + linkCitations: opts.linkCitations, + csl: opts.csl, + lang: ctx.cfg.configuration.locale ?? "en-US", + }, + ]) + + // Transform the HTML of the citattions; add data-no-popover property to the citation links + // using https://github.com/syntax-tree/unist-util-visit as they're just anochor links + plugins.push(() => { + return (tree, _file) => { + visit(tree, "element", (node, _index, _parent) => { + if (node.tagName === "a" && node.properties?.href?.startsWith("#bib")) { + node.properties["data-no-popover"] = true + } + }) + } + }) + + return plugins + }, + } +} diff --git a/quartz/plugins/transformers/description.ts b/quartz/plugins/transformers/description.ts new file mode 100755 index 0000000..c7e592e --- /dev/null +++ b/quartz/plugins/transformers/description.ts @@ -0,0 +1,82 @@ +import { Root as HTMLRoot } from "hast" +import { toString } from "hast-util-to-string" +import { QuartzTransformerPlugin } from "../types" +import { escapeHTML } from "../../util/escape" + +export interface Options { + descriptionLength: number + replaceExternalLinks: boolean +} + +const defaultOptions: Options = { + descriptionLength: 150, + replaceExternalLinks: true, +} + +const urlRegex = new RegExp( + /(https?:\/\/)?(?([\da-z\.-]+)\.([a-z\.]{2,6})(:\d+)?)(?[\/\w\.-]*)(\?[\/\w\.=&;-]*)?/, + "g", +) + +export const Description: QuartzTransformerPlugin> = (userOpts) => { + const opts = { ...defaultOptions, ...userOpts } + return { + name: "Description", + htmlPlugins() { + return [ + () => { + return async (tree: HTMLRoot, file) => { + let frontMatterDescription = file.data.frontmatter?.description + let text = escapeHTML(toString(tree)) + + if (opts.replaceExternalLinks) { + frontMatterDescription = frontMatterDescription?.replace( + urlRegex, + "$" + "$", + ) + text = text.replace(urlRegex, "$" + "$") + } + + const desc = frontMatterDescription ?? text + const sentences = desc.replace(/\s+/g, " ").split(/\.\s/) + const finalDesc: string[] = [] + const len = opts.descriptionLength + let sentenceIdx = 0 + let currentDescriptionLength = 0 + + if (sentences[0] !== undefined && sentences[0].length >= len) { + const firstSentence = sentences[0].split(" ") + while (currentDescriptionLength < len) { + const sentence = firstSentence[sentenceIdx] + if (!sentence) break + finalDesc.push(sentence) + currentDescriptionLength += sentence.length + sentenceIdx++ + } + finalDesc.push("...") + } else { + while (currentDescriptionLength < len) { + const sentence = sentences[sentenceIdx] + if (!sentence) break + const currentSentence = sentence.endsWith(".") ? sentence : sentence + "." + finalDesc.push(currentSentence) + currentDescriptionLength += currentSentence.length + sentenceIdx++ + } + } + + file.data.description = finalDesc.join(" ") + file.data.text = text + } + }, + ] + }, + } +} + +declare module "vfile" { + interface DataMap { + description: string + text: string + } +} diff --git a/quartz/plugins/transformers/frontmatter.ts b/quartz/plugins/transformers/frontmatter.ts new file mode 100755 index 0000000..e00c700 --- /dev/null +++ b/quartz/plugins/transformers/frontmatter.ts @@ -0,0 +1,119 @@ +import matter from "gray-matter" +import remarkFrontmatter from "remark-frontmatter" +import { QuartzTransformerPlugin } from "../types" +import yaml from "js-yaml" +import toml from "toml" +import { slugTag } from "../../util/path" +import { QuartzPluginData } from "../vfile" +import { i18n } from "../../i18n" + +export interface Options { + delimiters: string | [string, string] + language: "yaml" | "toml" +} + +const defaultOptions: Options = { + delimiters: "---", + language: "yaml", +} + +function coalesceAliases(data: { [key: string]: any }, aliases: string[]) { + for (const alias of aliases) { + if (data[alias] !== undefined && data[alias] !== null) return data[alias] + } +} + +function coerceToArray(input: string | string[]): string[] | undefined { + if (input === undefined || input === null) return undefined + + // coerce to array + if (!Array.isArray(input)) { + input = input + .toString() + .split(",") + .map((tag: string) => tag.trim()) + } + + // remove all non-strings + return input + .filter((tag: unknown) => typeof tag === "string" || typeof tag === "number") + .map((tag: string | number) => tag.toString()) +} + +export const FrontMatter: QuartzTransformerPlugin> = (userOpts) => { + const opts = { ...defaultOptions, ...userOpts } + return { + name: "FrontMatter", + markdownPlugins({ cfg }) { + return [ + [remarkFrontmatter, ["yaml", "toml"]], + () => { + return (_, file) => { + const { data } = matter(Buffer.from(file.value), { + ...opts, + engines: { + yaml: (s) => yaml.load(s, { schema: yaml.JSON_SCHEMA }) as object, + toml: (s) => toml.parse(s) as object, + }, + }) + + if (data.title != null && data.title.toString() !== "") { + data.title = data.title.toString() + } else { + data.title = file.stem ?? i18n(cfg.configuration.locale).propertyDefaults.title + } + + const tags = coerceToArray(coalesceAliases(data, ["tags", "tag"])) + if (tags) data.tags = [...new Set(tags.map((tag: string) => slugTag(tag)))] + + const aliases = coerceToArray(coalesceAliases(data, ["aliases", "alias"])) + if (aliases) data.aliases = aliases + const cssclasses = coerceToArray(coalesceAliases(data, ["cssclasses", "cssclass"])) + if (cssclasses) data.cssclasses = cssclasses + + const socialImage = coalesceAliases(data, ["socialImage", "image", "cover"]) + + const created = coalesceAliases(data, ["created", "date"]) + if (created) data.created = created + const modified = coalesceAliases(data, [ + "modified", + "lastmod", + "updated", + "last-modified", + ]) + if (modified) data.modified = modified + const published = coalesceAliases(data, ["published", "publishDate", "date"]) + if (published) data.published = published + + if (socialImage) data.socialImage = socialImage + + // fill in frontmatter + file.data.frontmatter = data as QuartzPluginData["frontmatter"] + } + }, + ] + }, + } +} + +declare module "vfile" { + interface DataMap { + frontmatter: { [key: string]: unknown } & { + title: string + } & Partial<{ + tags: string[] + aliases: string[] + modified: string + created: string + published: string + description: string + publish: boolean | string + draft: boolean | string + lang: string + enableToc: string + cssclasses: string[] + socialImage: string + comments: boolean | string + }> + } +} diff --git a/quartz/plugins/transformers/gfm.ts b/quartz/plugins/transformers/gfm.ts new file mode 100755 index 0000000..eec26f7 --- /dev/null +++ b/quartz/plugins/transformers/gfm.ts @@ -0,0 +1,78 @@ +import remarkGfm from "remark-gfm" +import smartypants from "remark-smartypants" +import { QuartzTransformerPlugin } from "../types" +import rehypeSlug from "rehype-slug" +import rehypeAutolinkHeadings from "rehype-autolink-headings" + +export interface Options { + enableSmartyPants: boolean + linkHeadings: boolean +} + +const defaultOptions: Options = { + enableSmartyPants: true, + linkHeadings: true, +} + +export const GitHubFlavoredMarkdown: QuartzTransformerPlugin> = (userOpts) => { + const opts = { ...defaultOptions, ...userOpts } + return { + name: "GitHubFlavoredMarkdown", + markdownPlugins() { + return opts.enableSmartyPants ? [remarkGfm, smartypants] : [remarkGfm] + }, + htmlPlugins() { + if (opts.linkHeadings) { + return [ + rehypeSlug, + [ + rehypeAutolinkHeadings, + { + behavior: "append", + properties: { + role: "anchor", + ariaHidden: true, + tabIndex: -1, + "data-no-popover": true, + }, + content: { + type: "element", + tagName: "svg", + properties: { + width: 18, + height: 18, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + }, + children: [ + { + type: "element", + tagName: "path", + properties: { + d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71", + }, + children: [], + }, + { + type: "element", + tagName: "path", + properties: { + d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71", + }, + children: [], + }, + ], + }, + }, + ], + ] + } else { + return [] + } + }, + } +} diff --git a/quartz/plugins/transformers/index.ts b/quartz/plugins/transformers/index.ts new file mode 100755 index 0000000..8e2cd84 --- /dev/null +++ b/quartz/plugins/transformers/index.ts @@ -0,0 +1,13 @@ +export { FrontMatter } from "./frontmatter" +export { GitHubFlavoredMarkdown } from "./gfm" +export { Citations } from "./citations" +export { CreatedModifiedDate } from "./lastmod" +export { Latex } from "./latex" +export { Description } from "./description" +export { CrawlLinks } from "./links" +export { ObsidianFlavoredMarkdown } from "./ofm" +export { OxHugoFlavouredMarkdown } from "./oxhugofm" +export { SyntaxHighlighting } from "./syntax" +export { TableOfContents } from "./toc" +export { HardLineBreaks } from "./linebreaks" +export { RoamFlavoredMarkdown } from "./roam" diff --git a/quartz/plugins/transformers/lastmod.ts b/quartz/plugins/transformers/lastmod.ts new file mode 100755 index 0000000..fd57692 --- /dev/null +++ b/quartz/plugins/transformers/lastmod.ts @@ -0,0 +1,95 @@ +import fs from "fs" +import path from "path" +import { Repository } from "@napi-rs/simple-git" +import { QuartzTransformerPlugin } from "../types" +import chalk from "chalk" + +export interface Options { + priority: ("frontmatter" | "git" | "filesystem")[] +} + +const defaultOptions: Options = { + priority: ["frontmatter", "git", "filesystem"], +} + +function coerceDate(fp: string, d: any): Date { + const dt = new Date(d) + const invalidDate = isNaN(dt.getTime()) || dt.getTime() === 0 + if (invalidDate && d !== undefined) { + console.log( + chalk.yellow( + `\nWarning: found invalid date "${d}" in \`${fp}\`. Supported formats: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format`, + ), + ) + } + + return invalidDate ? new Date() : dt +} + +type MaybeDate = undefined | string | number +export const CreatedModifiedDate: QuartzTransformerPlugin> = (userOpts) => { + const opts = { ...defaultOptions, ...userOpts } + return { + name: "CreatedModifiedDate", + markdownPlugins() { + return [ + () => { + let repo: Repository | undefined = undefined + return async (_tree, file) => { + let created: MaybeDate = undefined + let modified: MaybeDate = undefined + let published: MaybeDate = undefined + + const fp = file.data.filePath! + const fullFp = path.isAbsolute(fp) ? fp : path.posix.join(file.cwd, fp) + for (const source of opts.priority) { + if (source === "filesystem") { + const st = await fs.promises.stat(fullFp) + created ||= st.birthtimeMs + modified ||= st.mtimeMs + } else if (source === "frontmatter" && file.data.frontmatter) { + created ||= file.data.frontmatter.created as MaybeDate + modified ||= file.data.frontmatter.modified as MaybeDate + published ||= file.data.frontmatter.published as MaybeDate + } else if (source === "git") { + if (!repo) { + // Get a reference to the main git repo. + // It's either the same as the workdir, + // or 1+ level higher in case of a submodule/subtree setup + repo = Repository.discover(file.cwd) + } + + try { + modified ||= await repo.getFileLatestModifiedDateAsync(file.data.filePath!) + } catch { + console.log( + chalk.yellow( + `\nWarning: ${file.data + .filePath!} isn't yet tracked by git, last modification date is not available for this file`, + ), + ) + } + } + } + + file.data.dates = { + created: coerceDate(fp, created), + modified: coerceDate(fp, modified), + published: coerceDate(fp, published), + } + } + }, + ] + }, + } +} + +declare module "vfile" { + interface DataMap { + dates: { + created: Date + modified: Date + published: Date + } + } +} diff --git a/quartz/plugins/transformers/latex.ts b/quartz/plugins/transformers/latex.ts new file mode 100755 index 0000000..26913ba --- /dev/null +++ b/quartz/plugins/transformers/latex.ts @@ -0,0 +1,67 @@ +import remarkMath from "remark-math" +import rehypeKatex from "rehype-katex" +import rehypeMathjax from "rehype-mathjax/svg" +//@ts-ignore +import rehypeTypst from "@myriaddreamin/rehype-typst" +import { QuartzTransformerPlugin } from "../types" +import { KatexOptions } from "katex" +import { Options as MathjaxOptions } from "rehype-mathjax/svg" +//@ts-ignore +import { Options as TypstOptions } from "@myriaddreamin/rehype-typst" + +interface Options { + renderEngine: "katex" | "mathjax" | "typst" + customMacros: MacroType + katexOptions: Omit + mathJaxOptions: Omit + typstOptions: TypstOptions +} + +interface MacroType { + [key: string]: string +} + +export const Latex: QuartzTransformerPlugin> = (opts) => { + const engine = opts?.renderEngine ?? "katex" + const macros = opts?.customMacros ?? {} + return { + name: "Latex", + markdownPlugins() { + return [remarkMath] + }, + htmlPlugins() { + switch (engine) { + case "katex": { + return [[rehypeKatex, { output: "html", macros, ...(opts?.katexOptions ?? {}) }]] + } + case "typst": { + return [[rehypeTypst, opts?.typstOptions ?? {}]] + } + case "mathjax": { + return [[rehypeMathjax, { macros, ...(opts?.mathJaxOptions ?? {}) }]] + } + default: { + return [[rehypeMathjax, { macros, ...(opts?.mathJaxOptions ?? {}) }]] + } + } + }, + externalResources() { + switch (engine) { + case "katex": + return { + css: [{ content: "https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css" }], + js: [ + { + // fix copy behaviour: https://github.com/KaTeX/KaTeX/blob/main/contrib/copy-tex/README.md + src: "https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/copy-tex.min.js", + loadTime: "afterDOMReady", + contentType: "external", + }, + ], + } + default: + return { css: [], js: [] } + } + }, + } +} diff --git a/quartz/plugins/transformers/linebreaks.ts b/quartz/plugins/transformers/linebreaks.ts new file mode 100755 index 0000000..a8a066f --- /dev/null +++ b/quartz/plugins/transformers/linebreaks.ts @@ -0,0 +1,11 @@ +import { QuartzTransformerPlugin } from "../types" +import remarkBreaks from "remark-breaks" + +export const HardLineBreaks: QuartzTransformerPlugin = () => { + return { + name: "HardLineBreaks", + markdownPlugins() { + return [remarkBreaks] + }, + } +} diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts new file mode 100755 index 0000000..3e8dbde --- /dev/null +++ b/quartz/plugins/transformers/links.ts @@ -0,0 +1,172 @@ +import { QuartzTransformerPlugin } from "../types" +import { + FullSlug, + RelativeURL, + SimpleSlug, + TransformOptions, + stripSlashes, + simplifySlug, + splitAnchor, + transformLink, +} from "../../util/path" +import path from "path" +import { visit } from "unist-util-visit" +import isAbsoluteUrl from "is-absolute-url" +import { Root } from "hast" + +interface Options { + /** How to resolve Markdown paths */ + markdownLinkResolution: TransformOptions["strategy"] + /** Strips folders from a link so that it looks nice */ + prettyLinks: boolean + openLinksInNewTab: boolean + lazyLoad: boolean + externalLinkIcon: boolean +} + +const defaultOptions: Options = { + markdownLinkResolution: "absolute", + prettyLinks: true, + openLinksInNewTab: false, + lazyLoad: false, + externalLinkIcon: true, +} + +export const CrawlLinks: QuartzTransformerPlugin> = (userOpts) => { + const opts = { ...defaultOptions, ...userOpts } + return { + name: "LinkProcessing", + htmlPlugins(ctx) { + return [ + () => { + return (tree: Root, file) => { + const curSlug = simplifySlug(file.data.slug!) + const outgoing: Set = new Set() + + const transformOptions: TransformOptions = { + strategy: opts.markdownLinkResolution, + allSlugs: ctx.allSlugs, + } + + visit(tree, "element", (node, _index, _parent) => { + // rewrite all links + if ( + node.tagName === "a" && + node.properties && + typeof node.properties.href === "string" + ) { + let dest = node.properties.href as RelativeURL + const classes = (node.properties.className ?? []) as string[] + const isExternal = isAbsoluteUrl(dest) + classes.push(isExternal ? "external" : "internal") + + if (isExternal && opts.externalLinkIcon) { + node.children.push({ + type: "element", + tagName: "svg", + properties: { + "aria-hidden": "true", + class: "external-icon", + style: "max-width:0.8em;max-height:0.8em", + viewBox: "0 0 512 512", + }, + children: [ + { + type: "element", + tagName: "path", + properties: { + d: "M320 0H288V64h32 82.7L201.4 265.4 178.7 288 224 333.3l22.6-22.6L448 109.3V192v32h64V192 32 0H480 320zM32 32H0V64 480v32H32 456h32V480 352 320H424v32 96H64V96h96 32V32H160 32z", + }, + children: [], + }, + ], + }) + } + + // Check if the link has alias text + if ( + node.children.length === 1 && + node.children[0].type === "text" && + node.children[0].value !== dest + ) { + // Add the 'alias' class if the text content is not the same as the href + classes.push("alias") + } + node.properties.className = classes + + if (isExternal && opts.openLinksInNewTab) { + node.properties.target = "_blank" + } + + // don't process external links or intra-document anchors + const isInternal = !(isAbsoluteUrl(dest) || dest.startsWith("#")) + if (isInternal) { + dest = node.properties.href = transformLink( + file.data.slug!, + dest, + transformOptions, + ) + + // url.resolve is considered legacy + // WHATWG equivalent https://nodejs.dev/en/api/v18/url/#urlresolvefrom-to + const url = new URL(dest, "https://base.com/" + stripSlashes(curSlug, true)) + const canonicalDest = url.pathname + let [destCanonical, _destAnchor] = splitAnchor(canonicalDest) + if (destCanonical.endsWith("/")) { + destCanonical += "index" + } + + // need to decodeURIComponent here as WHATWG URL percent-encodes everything + const full = decodeURIComponent(stripSlashes(destCanonical, true)) as FullSlug + const simple = simplifySlug(full) + outgoing.add(simple) + node.properties["data-slug"] = full + } + + // rewrite link internals if prettylinks is on + if ( + opts.prettyLinks && + isInternal && + node.children.length === 1 && + node.children[0].type === "text" && + !node.children[0].value.startsWith("#") + ) { + node.children[0].value = path.basename(node.children[0].value) + } + } + + // transform all other resources that may use links + if ( + ["img", "video", "audio", "iframe"].includes(node.tagName) && + node.properties && + typeof node.properties.src === "string" + ) { + if (opts.lazyLoad) { + node.properties.loading = "lazy" + } + + if (!isAbsoluteUrl(node.properties.src)) { + let dest = node.properties.src as RelativeURL + dest = node.properties.src = transformLink( + file.data.slug!, + dest, + transformOptions, + ) + node.properties.src = dest + } + } + }) + + file.data.links = [...outgoing] + } + }, + ] + }, + } +} + +declare module "vfile" { + interface DataMap { + links: SimpleSlug[] + } +} diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts new file mode 100755 index 0000000..b0b0a42 --- /dev/null +++ b/quartz/plugins/transformers/ofm.ts @@ -0,0 +1,832 @@ +import { QuartzTransformerPlugin } from "../types" +import { + Root, + Html, + BlockContent, + PhrasingContent, + DefinitionContent, + Paragraph, + Code, +} from "mdast" +import { Element, Literal, Root as HtmlRoot } from "hast" +import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace" +import rehypeRaw from "rehype-raw" +import { SKIP, visit } from "unist-util-visit" +import path from "path" +import { splitAnchor } from "../../util/path" +import { JSResource, CSSResource } from "../../util/resources" +// @ts-ignore +import calloutScript from "../../components/scripts/callout.inline.ts" +// @ts-ignore +import checkboxScript from "../../components/scripts/checkbox.inline.ts" +import { FilePath, pathToRoot, slugTag, slugifyFilePath } from "../../util/path" +import { toHast } from "mdast-util-to-hast" +import { toHtml } from "hast-util-to-html" +import { capitalize } from "../../util/lang" +import { PluggableList } from "unified" + +export interface Options { + comments: boolean + highlight: boolean + wikilinks: boolean + callouts: boolean + mermaid: boolean + parseTags: boolean + parseArrows: boolean + parseBlockReferences: boolean + enableInHtmlEmbed: boolean + enableYouTubeEmbed: boolean + enableVideoEmbed: boolean + enableCheckbox: boolean +} + +const defaultOptions: Options = { + comments: true, + highlight: true, + wikilinks: true, + callouts: true, + mermaid: true, + parseTags: true, + parseArrows: true, + parseBlockReferences: true, + enableInHtmlEmbed: false, + enableYouTubeEmbed: true, + enableVideoEmbed: true, + enableCheckbox: false, +} + +const calloutMapping = { + note: "note", + abstract: "abstract", + summary: "abstract", + tldr: "abstract", + info: "info", + todo: "todo", + tip: "tip", + hint: "tip", + important: "tip", + success: "success", + check: "success", + done: "success", + question: "question", + help: "question", + faq: "question", + warning: "warning", + attention: "warning", + caution: "warning", + failure: "failure", + missing: "failure", + fail: "failure", + danger: "danger", + error: "danger", + bug: "bug", + example: "example", + quote: "quote", + cite: "quote", +} as const + +const arrowMapping: Record = { + "->": "→", + "-->": "⇒", + "=>": "⇒", + "==>": "⇒", + "<-": "←", + "<--": "⇐", + "<=": "⇐", + "<==": "⇐", +} + +function canonicalizeCallout(calloutName: string): keyof typeof calloutMapping { + const normalizedCallout = calloutName.toLowerCase() as keyof typeof calloutMapping + // if callout is not recognized, make it a custom one + return calloutMapping[normalizedCallout] ?? calloutName +} + +export const externalLinkRegex = /^https?:\/\//i + +export const arrowRegex = new RegExp(/(-{1,2}>|={1,2}>|<-{1,2}|<={1,2})/g) + +// !? -> optional embedding +// \[\[ -> open brace +// ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name) +// (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link) +// (\\?\|[^\[\]\#]+)? -> optional escape \ then | then one or more non-special characters (alias) +export const wikilinkRegex = new RegExp( + /!?\[\[([^\[\]\|\#\\]+)?(#+[^\[\]\|\#\\]+)?(\\?\|[^\[\]\#]+)?\]\]/g, +) + +// ^\|([^\n])+\|\n(\|) -> matches the header row +// ( ?:?-{3,}:? ?\|)+ -> matches the header row separator +// (\|([^\n])+\|\n)+ -> matches the body rows +export const tableRegex = new RegExp(/^\|([^\n])+\|\n(\|)( ?:?-{3,}:? ?\|)+\n(\|([^\n])+\|\n?)+/gm) + +// matches any wikilink, only used for escaping wikilinks inside tables +export const tableWikilinkRegex = new RegExp(/(!?\[\[[^\]]*?\]\]|\[\^[^\]]*?\])/g) + +const highlightRegex = new RegExp(/==([^=]+)==/g) +const commentRegex = new RegExp(/%%[\s\S]*?%%/g) +// from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts +const calloutRegex = new RegExp(/^\[\!([\w-]+)\|?(.+?)?\]([+-]?)/) +const calloutLineRegex = new RegExp(/^> *\[\!\w+\|?.*?\][+-]?.*$/gm) +// (?<=^| ) -> a lookbehind assertion, tag should start be separated by a space or be the start of the line +// #(...) -> capturing group, tag itself must start with # +// (?:[-_\p{L}\d\p{Z}])+ -> non-capturing group, non-empty string of (Unicode-aware) alpha-numeric characters and symbols, hyphens and/or underscores +// (?:\/[-_\p{L}\d\p{Z}]+)*) -> non-capturing group, matches an arbitrary number of tag strings separated by "/" +const tagRegex = new RegExp( + /(?<=^| )#((?:[-_\p{L}\p{Emoji}\p{M}\d])+(?:\/[-_\p{L}\p{Emoji}\p{M}\d]+)*)/gu, +) +const blockReferenceRegex = new RegExp(/\^([-_A-Za-z0-9]+)$/g) +const ytLinkRegex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/ +const ytPlaylistLinkRegex = /[?&]list=([^#?&]*)/ +const videoExtensionRegex = new RegExp(/\.(mp4|webm|ogg|avi|mov|flv|wmv|mkv|mpg|mpeg|3gp|m4v)$/) +const wikilinkImageEmbedRegex = new RegExp( + /^(?(?!^\d*x?\d*$).*?)?(\|?\s*?(?\d+)(x(?\d+))?)?$/, +) + +export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin> = (userOpts) => { + const opts = { ...defaultOptions, ...userOpts } + + const mdastToHtml = (ast: PhrasingContent | Paragraph) => { + const hast = toHast(ast, { allowDangerousHtml: true })! + return toHtml(hast, { allowDangerousHtml: true }) + } + + return { + name: "ObsidianFlavoredMarkdown", + textTransform(_ctx, src) { + // do comments at text level + if (opts.comments) { + if (src instanceof Buffer) { + src = src.toString() + } + + src = (src as string).replace(commentRegex, "") + } + + // pre-transform blockquotes + if (opts.callouts) { + if (src instanceof Buffer) { + src = src.toString() + } + + src = (src as string).replace(calloutLineRegex, (value) => { + // force newline after title of callout + return value + "\n> " + }) + } + + // pre-transform wikilinks (fix anchors to things that may contain illegal syntax e.g. codeblocks, latex) + if (opts.wikilinks) { + if (src instanceof Buffer) { + src = src.toString() + } + + // replace all wikilinks inside a table first + src = (src as string).replace(tableRegex, (value) => { + // escape all aliases and headers in wikilinks inside a table + return value.replace(tableWikilinkRegex, (_value, raw) => { + // const [raw]: (string | undefined)[] = capture + let escaped = raw ?? "" + escaped = escaped.replace("#", "\\#") + // escape pipe characters if they are not already escaped + escaped = escaped.replace(/((^|[^\\])(\\\\)*)\|/g, "$1\\|") + + return escaped + }) + }) + + // replace all other wikilinks + src = (src as string).replace(wikilinkRegex, (value, ...capture) => { + const [rawFp, rawHeader, rawAlias]: (string | undefined)[] = capture + + const [fp, anchor] = splitAnchor(`${rawFp ?? ""}${rawHeader ?? ""}`) + const blockRef = Boolean(rawHeader?.match(/^#?\^/)) ? "^" : "" + const displayAnchor = anchor ? `#${blockRef}${anchor.trim().replace(/^#+/, "")}` : "" + const displayAlias = rawAlias ?? rawHeader?.replace("#", "|") ?? "" + const embedDisplay = value.startsWith("!") ? "!" : "" + + if (rawFp?.match(externalLinkRegex)) { + return `${embedDisplay}[${displayAlias.replace(/^\|/, "")}](${rawFp})` + } + + return `${embedDisplay}[[${fp}${displayAnchor}${displayAlias}]]` + }) + } + + return src + }, + markdownPlugins(_ctx) { + const plugins: PluggableList = [] + + // regex replacements + plugins.push(() => { + return (tree: Root, file) => { + const replacements: [RegExp, string | ReplaceFunction][] = [] + const base = pathToRoot(file.data.slug!) + + if (opts.wikilinks) { + replacements.push([ + wikilinkRegex, + (value: string, ...capture: string[]) => { + let [rawFp, rawHeader, rawAlias] = capture + const fp = rawFp?.trim() ?? "" + const anchor = rawHeader?.trim() ?? "" + const alias = rawAlias?.slice(1).trim() + + // embed cases + if (value.startsWith("!")) { + const ext: string = path.extname(fp).toLowerCase() + const url = slugifyFilePath(fp as FilePath) + if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg", ".webp"].includes(ext)) { + const match = wikilinkImageEmbedRegex.exec(alias ?? "") + const alt = match?.groups?.alt ?? "" + const width = match?.groups?.width ?? "auto" + const height = match?.groups?.height ?? "auto" + return { + type: "image", + url, + data: { + hProperties: { + width, + height, + alt, + }, + }, + } + } else if ([".mp4", ".webm", ".ogv", ".mov", ".mkv"].includes(ext)) { + return { + type: "html", + value: ``, + } + } else if ( + [".mp3", ".webm", ".wav", ".m4a", ".ogg", ".3gp", ".flac"].includes(ext) + ) { + return { + type: "html", + value: ``, + } + } else if ([".pdf"].includes(ext)) { + return { + type: "html", + value: ``, + } + } else { + const block = anchor + return { + type: "html", + data: { hProperties: { transclude: true } }, + value: `
    Transclude of ${url}${block}
    `, + } + } + + // otherwise, fall through to regular link + } + + // internal link + const url = fp + anchor + + return { + type: "link", + url, + children: [ + { + type: "text", + value: alias ?? fp, + }, + ], + } + }, + ]) + } + + if (opts.highlight) { + replacements.push([ + highlightRegex, + (_value: string, ...capture: string[]) => { + const [inner] = capture + return { + type: "html", + value: `${inner}`, + } + }, + ]) + } + + if (opts.parseArrows) { + replacements.push([ + arrowRegex, + (value: string, ..._capture: string[]) => { + const maybeArrow = arrowMapping[value] + if (maybeArrow === undefined) return SKIP + return { + type: "html", + value: `${maybeArrow}`, + } + }, + ]) + } + + if (opts.parseTags) { + replacements.push([ + tagRegex, + (_value: string, tag: string) => { + // Check if the tag only includes numbers and slashes + if (/^[\/\d]+$/.test(tag)) { + return false + } + + tag = slugTag(tag) + if (file.data.frontmatter) { + const noteTags = file.data.frontmatter.tags ?? [] + file.data.frontmatter.tags = [...new Set([...noteTags, tag])] + } + + return { + type: "link", + url: base + `/tags/${tag}`, + data: { + hProperties: { + className: ["tag-link"], + }, + }, + children: [ + { + type: "text", + value: tag, + }, + ], + } + }, + ]) + } + + if (opts.enableInHtmlEmbed) { + visit(tree, "html", (node: Html) => { + for (const [regex, replace] of replacements) { + if (typeof replace === "string") { + node.value = node.value.replace(regex, replace) + } else { + node.value = node.value.replace(regex, (substring: string, ...args) => { + const replaceValue = replace(substring, ...args) + if (typeof replaceValue === "string") { + return replaceValue + } else if (Array.isArray(replaceValue)) { + return replaceValue.map(mdastToHtml).join("") + } else if (typeof replaceValue === "object" && replaceValue !== null) { + return mdastToHtml(replaceValue) + } else { + return substring + } + }) + } + } + }) + } + mdastFindReplace(tree, replacements) + } + }) + + if (opts.enableVideoEmbed) { + plugins.push(() => { + return (tree: Root, _file) => { + visit(tree, "image", (node, index, parent) => { + if (parent && index != undefined && videoExtensionRegex.test(node.url)) { + const newNode: Html = { + type: "html", + value: ``, + } + + parent.children.splice(index, 1, newNode) + return SKIP + } + }) + } + }) + } + + if (opts.callouts) { + plugins.push(() => { + return (tree: Root, _file) => { + visit(tree, "blockquote", (node) => { + if (node.children.length === 0) { + return + } + + // find first line and callout content + const [firstChild, ...calloutContent] = node.children + if (firstChild.type !== "paragraph" || firstChild.children[0]?.type !== "text") { + return + } + + const text = firstChild.children[0].value + const restOfTitle = firstChild.children.slice(1) + const [firstLine, ...remainingLines] = text.split("\n") + const remainingText = remainingLines.join("\n") + + const match = firstLine.match(calloutRegex) + if (match && match.input) { + const [calloutDirective, typeString, calloutMetaData, collapseChar] = match + const calloutType = canonicalizeCallout(typeString.toLowerCase()) + const collapse = collapseChar === "+" || collapseChar === "-" + const defaultState = collapseChar === "-" ? "collapsed" : "expanded" + const titleContent = match.input.slice(calloutDirective.length).trim() + const useDefaultTitle = titleContent === "" && restOfTitle.length === 0 + const titleNode: Paragraph = { + type: "paragraph", + children: [ + { + type: "text", + value: useDefaultTitle + ? capitalize(typeString).replace(/-/g, " ") + : titleContent + " ", + }, + ...restOfTitle, + ], + } + const title = mdastToHtml(titleNode) + + const toggleIcon = `
    ` + + const titleHtml: Html = { + type: "html", + value: `
    +
    +
    ${title}
    + ${collapse ? toggleIcon : ""} +
    `, + } + + const blockquoteContent: (BlockContent | DefinitionContent)[] = [titleHtml] + if (remainingText.length > 0) { + blockquoteContent.push({ + type: "paragraph", + children: [ + { + type: "text", + value: remainingText, + }, + ], + }) + } + + // replace first line of blockquote with title and rest of the paragraph text + node.children.splice(0, 1, ...blockquoteContent) + + const classNames = ["callout", calloutType] + if (collapse) { + classNames.push("is-collapsible") + } + if (defaultState === "collapsed") { + classNames.push("is-collapsed") + } + + // add properties to base blockquote + node.data = { + hProperties: { + ...(node.data?.hProperties ?? {}), + className: classNames.join(" "), + "data-callout": calloutType, + "data-callout-fold": collapse, + "data-callout-metadata": calloutMetaData, + }, + } + + // Add callout-content class to callout body if it has one. + if (calloutContent.length > 0) { + const contentData: BlockContent | DefinitionContent = { + data: { + hProperties: { + className: "callout-content", + }, + hName: "div", + }, + type: "blockquote", + children: [...calloutContent], + } + node.children = [node.children[0], contentData] + } + } + }) + } + }) + } + + if (opts.mermaid) { + plugins.push(() => { + return (tree: Root, file) => { + visit(tree, "code", (node: Code) => { + if (node.lang === "mermaid") { + file.data.hasMermaidDiagram = true + node.data = { + hProperties: { + className: ["mermaid"], + "data-clipboard": JSON.stringify(node.value), + }, + } + } + }) + } + }) + } + + return plugins + }, + htmlPlugins() { + const plugins: PluggableList = [rehypeRaw] + + if (opts.parseBlockReferences) { + plugins.push(() => { + const inlineTagTypes = new Set(["p", "li"]) + const blockTagTypes = new Set(["blockquote"]) + return (tree: HtmlRoot, file) => { + file.data.blocks = {} + + visit(tree, "element", (node, index, parent) => { + if (blockTagTypes.has(node.tagName)) { + const nextChild = parent?.children.at(index! + 2) as Element + if (nextChild && nextChild.tagName === "p") { + const text = nextChild.children.at(0) as Literal + if (text && text.value && text.type === "text") { + const matches = text.value.match(blockReferenceRegex) + if (matches && matches.length >= 1) { + parent!.children.splice(index! + 2, 1) + const block = matches[0].slice(1) + + if (!Object.keys(file.data.blocks!).includes(block)) { + node.properties = { + ...node.properties, + id: block, + } + file.data.blocks![block] = node + } + } + } + } + } else if (inlineTagTypes.has(node.tagName)) { + const last = node.children.at(-1) as Literal + if (last && last.value && typeof last.value === "string") { + const matches = last.value.match(blockReferenceRegex) + if (matches && matches.length >= 1) { + last.value = last.value.slice(0, -matches[0].length) + const block = matches[0].slice(1) + + if (last.value === "") { + // this is an inline block ref but the actual block + // is the previous element above it + let idx = (index ?? 1) - 1 + while (idx >= 0) { + const element = parent?.children.at(idx) + if (!element) break + if (element.type !== "element") { + idx -= 1 + } else { + if (!Object.keys(file.data.blocks!).includes(block)) { + element.properties = { + ...element.properties, + id: block, + } + file.data.blocks![block] = element + } + return + } + } + } else { + // normal paragraph transclude + if (!Object.keys(file.data.blocks!).includes(block)) { + node.properties = { + ...node.properties, + id: block, + } + file.data.blocks![block] = node + } + } + } + } + } + }) + + file.data.htmlAst = tree + } + }) + } + + if (opts.enableYouTubeEmbed) { + plugins.push(() => { + return (tree: HtmlRoot) => { + visit(tree, "element", (node) => { + if (node.tagName === "img" && typeof node.properties.src === "string") { + const match = node.properties.src.match(ytLinkRegex) + const videoId = match && match[2].length == 11 ? match[2] : null + const playlistId = node.properties.src.match(ytPlaylistLinkRegex)?.[1] + if (videoId) { + // YouTube video (with optional playlist) + node.tagName = "iframe" + node.properties = { + class: "external-embed youtube", + allow: "fullscreen", + frameborder: 0, + width: "600px", + src: playlistId + ? `https://www.youtube.com/embed/${videoId}?list=${playlistId}` + : `https://www.youtube.com/embed/${videoId}`, + } + } else if (playlistId) { + // YouTube playlist only. + node.tagName = "iframe" + node.properties = { + class: "external-embed youtube", + allow: "fullscreen", + frameborder: 0, + width: "600px", + src: `https://www.youtube.com/embed/videoseries?list=${playlistId}`, + } + } + } + }) + } + }) + } + + if (opts.enableCheckbox) { + plugins.push(() => { + return (tree: HtmlRoot, _file) => { + visit(tree, "element", (node) => { + if (node.tagName === "input" && node.properties.type === "checkbox") { + const isChecked = node.properties?.checked ?? false + node.properties = { + type: "checkbox", + disabled: false, + checked: isChecked, + class: "checkbox-toggle", + } + } + }) + } + }) + } + + if (opts.mermaid) { + plugins.push(() => { + return (tree: HtmlRoot, _file) => { + visit(tree, "element", (node: Element, _idx, parent) => { + if ( + node.tagName === "code" && + ((node.properties?.className ?? []) as string[])?.includes("mermaid") + ) { + parent!.children = [ + { + type: "element", + tagName: "button", + properties: { + className: ["expand-button"], + "aria-label": "Expand mermaid diagram", + "aria-hidden": "true", + "data-view-component": true, + }, + children: [ + { + type: "element", + tagName: "svg", + properties: { + width: 16, + height: 16, + viewBox: "0 0 16 16", + fill: "currentColor", + }, + children: [ + { + type: "element", + tagName: "path", + properties: { + fillRule: "evenodd", + d: "M3.72 3.72a.75.75 0 011.06 1.06L2.56 7h10.88l-2.22-2.22a.75.75 0 011.06-1.06l3.5 3.5a.75.75 0 010 1.06l-3.5 3.5a.75.75 0 11-1.06-1.06l2.22-2.22H2.56l2.22 2.22a.75.75 0 11-1.06 1.06l-3.5-3.5a.75.75 0 010-1.06l3.5-3.5z", + }, + children: [], + }, + ], + }, + ], + }, + node, + { + type: "element", + tagName: "div", + properties: { id: "mermaid-container" }, + children: [ + { + type: "element", + tagName: "div", + properties: { id: "mermaid-space" }, + children: [ + { + type: "element", + tagName: "div", + properties: { className: ["mermaid-header"] }, + children: [ + { + type: "element", + tagName: "button", + properties: { + className: ["close-button"], + "aria-label": "close button", + }, + children: [ + { + type: "element", + tagName: "svg", + properties: { + "aria-hidden": "true", + xmlns: "http://www.w3.org/2000/svg", + width: 24, + height: 24, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + }, + children: [ + { + type: "element", + tagName: "line", + properties: { + x1: 18, + y1: 6, + x2: 6, + y2: 18, + }, + children: [], + }, + { + type: "element", + tagName: "line", + properties: { + x1: 6, + y1: 6, + x2: 18, + y2: 18, + }, + children: [], + }, + ], + }, + ], + }, + ], + }, + { + type: "element", + tagName: "div", + properties: { className: ["mermaid-content"] }, + children: [], + }, + ], + }, + ], + }, + ] + } + }) + } + }) + } + + return plugins + }, + externalResources() { + const js: JSResource[] = [] + const css: CSSResource[] = [] + + if (opts.enableCheckbox) { + js.push({ + script: checkboxScript, + loadTime: "afterDOMReady", + contentType: "inline", + }) + } + + if (opts.callouts) { + js.push({ + script: calloutScript, + loadTime: "afterDOMReady", + contentType: "inline", + }) + } + + return { js, css } + }, + } +} + +declare module "vfile" { + interface DataMap { + blocks: Record + htmlAst: HtmlRoot + hasMermaidDiagram: boolean | undefined + } +} diff --git a/quartz/plugins/transformers/oxhugofm.ts b/quartz/plugins/transformers/oxhugofm.ts new file mode 100755 index 0000000..cdbffcf --- /dev/null +++ b/quartz/plugins/transformers/oxhugofm.ts @@ -0,0 +1,106 @@ +import { QuartzTransformerPlugin } from "../types" + +export interface Options { + /** Replace {{ relref }} with quartz wikilinks []() */ + wikilinks: boolean + /** Remove pre-defined anchor (see https://ox-hugo.scripter.co/doc/anchors/) */ + removePredefinedAnchor: boolean + /** Remove hugo shortcode syntax */ + removeHugoShortcode: boolean + /** Replace
    with ![]() */ + replaceFigureWithMdImg: boolean + + /** Replace org latex fragments with $ and $$ */ + replaceOrgLatex: boolean +} + +const defaultOptions: Options = { + wikilinks: true, + removePredefinedAnchor: true, + removeHugoShortcode: true, + replaceFigureWithMdImg: true, + replaceOrgLatex: true, +} + +const relrefRegex = new RegExp(/\[([^\]]+)\]\(\{\{< relref "([^"]+)" >\}\}\)/, "g") +const predefinedHeadingIdRegex = new RegExp(/(.*) {#(?:.*)}/, "g") +const hugoShortcodeRegex = new RegExp(/{{(.*)}}/, "g") +const figureTagRegex = new RegExp(/< ?figure src="(.*)" ?>/, "g") +// \\\\\( -> matches \\( +// (.+?) -> Lazy match for capturing the equation +// \\\\\) -> matches \\) +const inlineLatexRegex = new RegExp(/\\\\\((.+?)\\\\\)/, "g") +// (?:\\begin{equation}|\\\\\(|\\\\\[) -> start of equation +// ([\s\S]*?) -> Matches the block equation +// (?:\\\\\]|\\\\\)|\\end{equation}) -> end of equation +const blockLatexRegex = new RegExp( + /(?:\\begin{equation}|\\\\\(|\\\\\[)([\s\S]*?)(?:\\\\\]|\\\\\)|\\end{equation})/, + "g", +) +// \$\$[\s\S]*?\$\$ -> Matches block equations +// \$.*?\$ -> Matches inline equations +const quartzLatexRegex = new RegExp(/\$\$[\s\S]*?\$\$|\$.*?\$/, "g") + +/** + * ox-hugo is an org exporter backend that exports org files to hugo-compatible + * markdown in an opinionated way. This plugin adds some tweaks to the generated + * markdown to make it compatible with quartz but the list of changes applied it + * is not exhaustive. + * */ +export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin> = (userOpts) => { + const opts = { ...defaultOptions, ...userOpts } + return { + name: "OxHugoFlavouredMarkdown", + textTransform(_ctx, src) { + if (opts.wikilinks) { + src = src.toString() + src = src.replaceAll(relrefRegex, (value, ...capture) => { + const [text, link] = capture + return `[${text}](${link})` + }) + } + + if (opts.removePredefinedAnchor) { + src = src.toString() + src = src.replaceAll(predefinedHeadingIdRegex, (value, ...capture) => { + const [headingText] = capture + return headingText + }) + } + + if (opts.removeHugoShortcode) { + src = src.toString() + src = src.replaceAll(hugoShortcodeRegex, (value, ...capture) => { + const [scContent] = capture + return scContent + }) + } + + if (opts.replaceFigureWithMdImg) { + src = src.toString() + src = src.replaceAll(figureTagRegex, (value, ...capture) => { + const [src] = capture + return `![](${src})` + }) + } + + if (opts.replaceOrgLatex) { + src = src.toString() + src = src.replaceAll(inlineLatexRegex, (value, ...capture) => { + const [eqn] = capture + return `$${eqn}$` + }) + src = src.replaceAll(blockLatexRegex, (value, ...capture) => { + const [eqn] = capture + return `$$${eqn}$$` + }) + + // ox-hugo escapes _ as \_ + src = src.replaceAll(quartzLatexRegex, (value) => { + return value.replaceAll("\\_", "_") + }) + } + return src + }, + } +} diff --git a/quartz/plugins/transformers/roam.ts b/quartz/plugins/transformers/roam.ts new file mode 100755 index 0000000..b3be8f5 --- /dev/null +++ b/quartz/plugins/transformers/roam.ts @@ -0,0 +1,224 @@ +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: ``, + } + : 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: ``, + } + } else { + return { + type: "html", + value: ``, + } + } + case "pdf": + return opts.pdfComponent + ? { + type: "html", + value: ``, + } + : null + default: + return null + } +} + +export const RoamFlavoredMarkdown: QuartzTransformerPlugin | 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) + } + + // 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: `${inner}`, + }), + ]) + + 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 = `` + return { type: "html", value: selectHtml } + }, + ]) + } + + if (opts.TODOComponent) { + replacements.push([ + TODORegex, + () => ({ + type: "html", + value: ``, + }), + ]) + } + + if (opts.DONEComponent) { + replacements.push([ + DONERegex, + () => ({ + type: "html", + value: ``, + }), + ]) + } + + if (opts.blockquoteComponent) { + replacements.push([ + blockquoteRegex, + (_match: string, _marker: string, content: string) => ({ + type: "html", + value: `
    ${content.trim()}
    `, + }), + ]) + } + + mdastFindReplace(tree, replacements) + } + }) + + return plugins + }, + } +} diff --git a/quartz/plugins/transformers/syntax.ts b/quartz/plugins/transformers/syntax.ts new file mode 100755 index 0000000..5d3aae0 --- /dev/null +++ b/quartz/plugins/transformers/syntax.ts @@ -0,0 +1,31 @@ +import { QuartzTransformerPlugin } from "../types" +import rehypePrettyCode, { Options as CodeOptions, Theme as CodeTheme } from "rehype-pretty-code" + +interface Theme extends Record { + light: CodeTheme + dark: CodeTheme +} + +interface Options { + theme?: Theme + keepBackground?: boolean +} + +const defaultOptions: Options = { + theme: { + light: "github-light", + dark: "github-dark", + }, + keepBackground: false, +} + +export const SyntaxHighlighting: QuartzTransformerPlugin> = (userOpts) => { + const opts: CodeOptions = { ...defaultOptions, ...userOpts } + + return { + name: "SyntaxHighlighting", + htmlPlugins() { + return [[rehypePrettyCode, opts]] + }, + } +} diff --git a/quartz/plugins/transformers/toc.ts b/quartz/plugins/transformers/toc.ts new file mode 100755 index 0000000..791547b --- /dev/null +++ b/quartz/plugins/transformers/toc.ts @@ -0,0 +1,73 @@ +import { QuartzTransformerPlugin } from "../types" +import { Root } from "mdast" +import { visit } from "unist-util-visit" +import { toString } from "mdast-util-to-string" +import Slugger from "github-slugger" + +export interface Options { + maxDepth: 1 | 2 | 3 | 4 | 5 | 6 + minEntries: number + showByDefault: boolean + collapseByDefault: boolean +} + +const defaultOptions: Options = { + maxDepth: 3, + minEntries: 1, + showByDefault: true, + collapseByDefault: false, +} + +interface TocEntry { + depth: number + text: string + slug: string // this is just the anchor (#some-slug), not the canonical slug +} + +const slugAnchor = new Slugger() +export const TableOfContents: QuartzTransformerPlugin> = (userOpts) => { + const opts = { ...defaultOptions, ...userOpts } + return { + name: "TableOfContents", + markdownPlugins() { + return [ + () => { + return async (tree: Root, file) => { + const display = file.data.frontmatter?.enableToc ?? opts.showByDefault + if (display) { + slugAnchor.reset() + const toc: TocEntry[] = [] + let highestDepth: number = opts.maxDepth + visit(tree, "heading", (node) => { + if (node.depth <= opts.maxDepth) { + const text = toString(node) + highestDepth = Math.min(highestDepth, node.depth) + toc.push({ + depth: node.depth, + text, + slug: slugAnchor.slug(text), + }) + } + }) + + if (toc.length > 0 && toc.length > opts.minEntries) { + file.data.toc = toc.map((entry) => ({ + ...entry, + depth: entry.depth - highestDepth, + })) + file.data.collapseToc = opts.collapseByDefault + } + } + } + }, + ] + }, + } +} + +declare module "vfile" { + interface DataMap { + toc: TocEntry[] + collapseToc: boolean + } +} diff --git a/quartz/plugins/types.ts b/quartz/plugins/types.ts new file mode 100755 index 0000000..a23f5d6 --- /dev/null +++ b/quartz/plugins/types.ts @@ -0,0 +1,47 @@ +import { PluggableList } from "unified" +import { StaticResources } from "../util/resources" +import { ProcessedContent } from "./vfile" +import { QuartzComponent } from "../components/types" +import { FilePath } from "../util/path" +import { BuildCtx } from "../util/ctx" +import DepGraph from "../depgraph" + +export interface PluginTypes { + transformers: QuartzTransformerPluginInstance[] + filters: QuartzFilterPluginInstance[] + emitters: QuartzEmitterPluginInstance[] +} + +type OptionType = object | undefined +export type QuartzTransformerPlugin = ( + opts?: Options, +) => QuartzTransformerPluginInstance +export type QuartzTransformerPluginInstance = { + name: string + textTransform?: (ctx: BuildCtx, src: string | Buffer) => string | Buffer + markdownPlugins?: (ctx: BuildCtx) => PluggableList + htmlPlugins?: (ctx: BuildCtx) => PluggableList + externalResources?: (ctx: BuildCtx) => Partial +} + +export type QuartzFilterPlugin = ( + opts?: Options, +) => QuartzFilterPluginInstance +export type QuartzFilterPluginInstance = { + name: string + shouldPublish(ctx: BuildCtx, content: ProcessedContent): boolean +} + +export type QuartzEmitterPlugin = ( + opts?: Options, +) => QuartzEmitterPluginInstance +export type QuartzEmitterPluginInstance = { + name: string + emit(ctx: BuildCtx, content: ProcessedContent[], resources: StaticResources): Promise + getQuartzComponents(ctx: BuildCtx): QuartzComponent[] + getDependencyGraph?( + ctx: BuildCtx, + content: ProcessedContent[], + resources: StaticResources, + ): Promise> +} diff --git a/quartz/plugins/vfile.ts b/quartz/plugins/vfile.ts new file mode 100755 index 0000000..8c5cf6a --- /dev/null +++ b/quartz/plugins/vfile.ts @@ -0,0 +1,14 @@ +import { Root as HtmlRoot } from "hast" +import { Root as MdRoot } from "mdast" +import { Data, VFile } from "vfile" + +export type QuartzPluginData = Data +export type MarkdownContent = [MdRoot, VFile] +export type ProcessedContent = [HtmlRoot, VFile] + +export function defaultProcessedContent(vfileData: Partial): ProcessedContent { + const root: HtmlRoot = { type: "root", children: [] } + const vfile = new VFile("") + vfile.data = vfileData + return [root, vfile] +} diff --git a/quartz/processors/emit.ts b/quartz/processors/emit.ts new file mode 100755 index 0000000..c68e0ed --- /dev/null +++ b/quartz/processors/emit.ts @@ -0,0 +1,33 @@ +import { PerfTimer } from "../util/perf" +import { getStaticResourcesFromPlugins } from "../plugins" +import { ProcessedContent } from "../plugins/vfile" +import { QuartzLogger } from "../util/log" +import { trace } from "../util/trace" +import { BuildCtx } from "../util/ctx" + +export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) { + const { argv, cfg } = ctx + const perf = new PerfTimer() + const log = new QuartzLogger(ctx.argv.verbose) + + log.start(`Emitting output files`) + + let emittedFiles = 0 + const staticResources = getStaticResourcesFromPlugins(ctx) + for (const emitter of cfg.plugins.emitters) { + try { + const emitted = await emitter.emit(ctx, content, staticResources) + emittedFiles += emitted.length + + if (ctx.argv.verbose) { + for (const file of emitted) { + console.log(`[emit:${emitter.name}] ${file}`) + } + } + } catch (err) { + trace(`Failed to emit from plugin \`${emitter.name}\``, err as Error) + } + } + + log.end(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince()}`) +} diff --git a/quartz/processors/filter.ts b/quartz/processors/filter.ts new file mode 100755 index 0000000..b269fb3 --- /dev/null +++ b/quartz/processors/filter.ts @@ -0,0 +1,24 @@ +import { BuildCtx } from "../util/ctx" +import { PerfTimer } from "../util/perf" +import { ProcessedContent } from "../plugins/vfile" + +export function filterContent(ctx: BuildCtx, content: ProcessedContent[]): ProcessedContent[] { + const { cfg, argv } = ctx + const perf = new PerfTimer() + const initialLength = content.length + for (const plugin of cfg.plugins.filters) { + const updatedContent = content.filter((item) => plugin.shouldPublish(ctx, item)) + + if (argv.verbose) { + const diff = content.filter((x) => !updatedContent.includes(x)) + for (const file of diff) { + console.log(`[filter:${plugin.name}] ${file[1].data.slug}`) + } + } + + content = updatedContent + } + + console.log(`Filtered out ${initialLength - content.length} files in ${perf.timeSince()}`) + return content +} diff --git a/quartz/processors/parse.ts b/quartz/processors/parse.ts new file mode 100755 index 0000000..479313f --- /dev/null +++ b/quartz/processors/parse.ts @@ -0,0 +1,200 @@ +import esbuild from "esbuild" +import remarkParse from "remark-parse" +import remarkRehype from "remark-rehype" +import { Processor, unified } from "unified" +import { Root as MDRoot } from "remark-parse/lib" +import { Root as HTMLRoot } from "hast" +import { MarkdownContent, ProcessedContent } from "../plugins/vfile" +import { PerfTimer } from "../util/perf" +import { read } from "to-vfile" +import { FilePath, FullSlug, QUARTZ, slugifyFilePath } from "../util/path" +import path from "path" +import workerpool, { Promise as WorkerPromise } from "workerpool" +import { QuartzLogger } from "../util/log" +import { trace } from "../util/trace" +import { BuildCtx } from "../util/ctx" + +export type QuartzMdProcessor = Processor +export type QuartzHtmlProcessor = Processor + +export function createMdProcessor(ctx: BuildCtx): QuartzMdProcessor { + const transformers = ctx.cfg.plugins.transformers + + return ( + unified() + // base Markdown -> MD AST + .use(remarkParse) + // MD AST -> MD AST transforms + .use( + transformers.flatMap((plugin) => plugin.markdownPlugins?.(ctx) ?? []), + ) as unknown as QuartzMdProcessor + // ^ sadly the typing of `use` is not smart enough to infer the correct type from our plugin list + ) +} + +export function createHtmlProcessor(ctx: BuildCtx): QuartzHtmlProcessor { + const transformers = ctx.cfg.plugins.transformers + return ( + unified() + // MD AST -> HTML AST + .use(remarkRehype, { allowDangerousHtml: true }) + // HTML AST -> HTML AST transforms + .use(transformers.flatMap((plugin) => plugin.htmlPlugins?.(ctx) ?? [])) + ) +} + +function* chunks(arr: T[], n: number) { + for (let i = 0; i < arr.length; i += n) { + yield arr.slice(i, i + n) + } +} + +async function transpileWorkerScript() { + // transpile worker script + const cacheFile = "./.quartz-cache/transpiled-worker.mjs" + const fp = "./quartz/worker.ts" + return esbuild.build({ + entryPoints: [fp], + outfile: path.join(QUARTZ, cacheFile), + bundle: true, + keepNames: true, + platform: "node", + format: "esm", + packages: "external", + sourcemap: true, + sourcesContent: false, + plugins: [ + { + name: "css-and-scripts-as-text", + setup(build) { + build.onLoad({ filter: /\.scss$/ }, (_) => ({ + contents: "", + loader: "text", + })) + build.onLoad({ filter: /\.inline\.(ts|js)$/ }, (_) => ({ + contents: "", + loader: "text", + })) + }, + }, + ], + }) +} + +export function createFileParser(ctx: BuildCtx, fps: FilePath[]) { + const { argv, cfg } = ctx + return async (processor: QuartzMdProcessor) => { + const res: MarkdownContent[] = [] + for (const fp of fps) { + try { + const perf = new PerfTimer() + const file = await read(fp) + + // strip leading and trailing whitespace + file.value = file.value.toString().trim() + + // Text -> Text transforms + for (const plugin of cfg.plugins.transformers.filter((p) => p.textTransform)) { + file.value = plugin.textTransform!(ctx, file.value.toString()) + } + + // base data properties that plugins may use + file.data.filePath = file.path as FilePath + file.data.relativePath = path.posix.relative(argv.directory, file.path) as FilePath + file.data.slug = slugifyFilePath(file.data.relativePath) + + const ast = processor.parse(file) + const newAst = await processor.run(ast, file) + res.push([newAst, file]) + + if (argv.verbose) { + console.log(`[markdown] ${fp} -> ${file.data.slug} (${perf.timeSince()})`) + } + } catch (err) { + trace(`\nFailed to process markdown \`${fp}\``, err as Error) + } + } + + return res + } +} + +export function createMarkdownParser(ctx: BuildCtx, mdContent: MarkdownContent[]) { + return async (processor: QuartzHtmlProcessor) => { + const res: ProcessedContent[] = [] + for (const [ast, file] of mdContent) { + try { + const perf = new PerfTimer() + + const newAst = await processor.run(ast as MDRoot, file) + res.push([newAst, file]) + + if (ctx.argv.verbose) { + console.log(`[html] ${file.data.slug} (${perf.timeSince()})`) + } + } catch (err) { + trace(`\nFailed to process html \`${file.data.filePath}\``, err as Error) + } + } + + return res + } +} + +const clamp = (num: number, min: number, max: number) => + Math.min(Math.max(Math.round(num), min), max) + +export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise { + const { argv } = ctx + const perf = new PerfTimer() + const log = new QuartzLogger(argv.verbose) + + // rough heuristics: 128 gives enough time for v8 to JIT and optimize parsing code paths + const CHUNK_SIZE = 128 + const concurrency = ctx.argv.concurrency ?? clamp(fps.length / CHUNK_SIZE, 1, 4) + + let res: ProcessedContent[] = [] + log.start(`Parsing input files using ${concurrency} threads`) + if (concurrency === 1) { + try { + const mdRes = await createFileParser(ctx, fps)(createMdProcessor(ctx)) + res = await createMarkdownParser(ctx, mdRes)(createHtmlProcessor(ctx)) + } catch (error) { + log.end() + throw error + } + } else { + await transpileWorkerScript() + const pool = workerpool.pool("./quartz/bootstrap-worker.mjs", { + minWorkers: "max", + maxWorkers: concurrency, + workerType: "thread", + }) + const errorHandler = (err: any) => { + console.error(`${err}`.replace(/^error:\s*/i, "")) + process.exit(1) + } + + const mdPromises: WorkerPromise<[MarkdownContent[], FullSlug[]]>[] = [] + for (const chunk of chunks(fps, CHUNK_SIZE)) { + mdPromises.push(pool.exec("parseMarkdown", [ctx.buildId, argv, chunk])) + } + const mdResults: [MarkdownContent[], FullSlug[]][] = + await WorkerPromise.all(mdPromises).catch(errorHandler) + + const childPromises: WorkerPromise[] = [] + for (const [_, extraSlugs] of mdResults) { + ctx.allSlugs.push(...extraSlugs) + } + for (const [mdChunk, _] of mdResults) { + childPromises.push(pool.exec("processHtml", [ctx.buildId, argv, mdChunk, ctx.allSlugs])) + } + const results: ProcessedContent[][] = await WorkerPromise.all(childPromises).catch(errorHandler) + + res = results.flat() + await pool.terminate() + } + + log.end(`Parsed ${res.length} Markdown files in ${perf.timeSince()}`) + return res +} diff --git a/quartz/static/giscus/dark.css b/quartz/static/giscus/dark.css new file mode 100755 index 0000000..e98088f --- /dev/null +++ b/quartz/static/giscus/dark.css @@ -0,0 +1,99 @@ +/*! 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"); +} diff --git a/quartz/static/giscus/light.css b/quartz/static/giscus/light.css new file mode 100755 index 0000000..84b58c0 --- /dev/null +++ b/quartz/static/giscus/light.css @@ -0,0 +1,99 @@ +/*! 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"); +} diff --git a/quartz/static/icon.png b/quartz/static/icon.png new file mode 100755 index 0000000000000000000000000000000000000000..ee06ae9a1e8d343fd051e8ddf5eb966fd93de705 GIT binary patch literal 61364 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 OAfBhIpUXO@geCxBu&you literal 0 HcmV?d00001 diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss new file mode 100755 index 0000000..692413b --- /dev/null +++ b/quartz/styles/base.scss @@ -0,0 +1,606 @@ +@use "sass:map"; + +@use "./variables.scss" as *; +@use "./syntax.scss"; +@use "./callouts.scss"; + +html { + scroll-behavior: smooth; + text-size-adjust: none; + overflow-x: hidden; + width: 100vw; +} + +body, +section { + margin: 0; + box-sizing: border-box; + background-color: var(--light); + font-family: var(--bodyFont); + color: var(--darkgray); +} + +.text-highlight { + background-color: var(--textHighlight); + padding: 0 0.1rem; + border-radius: 5px; +} +::selection { + background: color-mix(in srgb, var(--tertiary) 60%, rgba(255, 255, 255, 0)); + color: var(--darkgray); +} + +p, +ul, +text, +a, +tr, +td, +li, +ol, +ul, +.katex, +.math { + color: var(--darkgray); + fill: var(--darkgray); + hyphens: none; +} + +p, +ul, +text, +a, +li, +ol, +ul, +.katex, +.math { + overflow-wrap: anywhere; + /* tr and td removed from list of selectors for overflow-wrap, allowing them to use default 'normal' property value */ +} + +.math { + &.math-display { + text-align: center; + } +} + +strong { + font-weight: $semiBoldWeight; +} + +a { + font-weight: $semiBoldWeight; + text-decoration: none; + transition: color 0.2s ease; + color: var(--secondary); + + &:hover { + color: var(--tertiary) !important; + } + + &.internal { + text-decoration: none; + background-color: var(--highlight); + padding: 0 0.1rem; + border-radius: 5px; + line-height: 1.4rem; + + &:has(> img) { + background-color: transparent; + border-radius: 0; + padding: 0; + } + &.tag-link { + opacity: 0.3; + font-size: 10pt; + &::before { + content: "#"; + } + } + } + + &.external .external-icon { + height: 1ex; + margin: 0 0.15em; + + > path { + fill: var(--dark); + } + } +} + +.desktop-only { + display: initial; + @media all and ($mobile) { + display: none; + } +} + +.mobile-only { + display: none; + @media all and ($mobile) { + display: initial; + } +} + +.page { + max-width: calc(#{map.get($breakpoints, desktop)} + 300px); + margin: 0 auto; + & article { + & > h1 { + font-size: 2rem; + } + + & li:has(> input[type="checkbox"]) { + list-style-type: none; + padding-left: 0; + } + + & li:has(> input[type="checkbox"]:checked) { + text-decoration: line-through; + text-decoration-color: var(--gray); + color: var(--gray); + } + + & li > * { + margin-top: 0; + margin-bottom: 0; + } + + p > strong { + color: var(--dark); + } + } + + & > #quartz-body { + display: grid; + grid-template-columns: #{map.get($desktopGrid, templateColumns)}; + grid-template-rows: #{map.get($desktopGrid, templateRows)}; + column-gap: #{map.get($desktopGrid, columnGap)}; + row-gap: #{map.get($desktopGrid, rowGap)}; + grid-template-areas: #{map.get($desktopGrid, templateAreas)}; + + @media all and ($tablet) { + grid-template-columns: #{map.get($tabletGrid, templateColumns)}; + grid-template-rows: #{map.get($tabletGrid, templateRows)}; + column-gap: #{map.get($tabletGrid, columnGap)}; + row-gap: #{map.get($tabletGrid, rowGap)}; + grid-template-areas: #{map.get($tabletGrid, templateAreas)}; + } + @media all and ($mobile) { + grid-template-columns: #{map.get($mobileGrid, templateColumns)}; + grid-template-rows: #{map.get($mobileGrid, templateRows)}; + column-gap: #{map.get($mobileGrid, columnGap)}; + row-gap: #{map.get($mobileGrid, rowGap)}; + grid-template-areas: #{map.get($mobileGrid, templateAreas)}; + } + + @media all and not ($desktop) { + padding: 0 1rem; + } + @media all and ($mobile) { + margin: 0 auto; + } + + & .sidebar { + gap: 2rem; + top: 0; + box-sizing: border-box; + padding: $topSpacing 2rem 2rem 2rem; + display: flex; + height: 100vh; + position: sticky; + } + + & .sidebar.left { + z-index: 1; + grid-area: grid-sidebar-left; + flex-direction: column; + @media all and ($mobile) { + gap: 0; + align-items: center; + position: initial; + display: flex; + height: unset; + flex-direction: row; + padding: 0; + padding-top: 2rem; + } + } + + & .sidebar.right { + grid-area: grid-sidebar-right; + margin-right: 0; + flex-direction: column; + @media all and ($mobile) { + margin-left: inherit; + margin-right: inherit; + } + @media all and not ($desktop) { + position: initial; + height: unset; + width: 100%; + flex-direction: row; + padding: 0; + & > * { + flex: 1; + } + & > .toc { + display: none; + } + } + } + & .page-header, + & .page-footer { + margin-top: 1rem; + } + + & .page-header { + grid-area: grid-header; + margin: $topSpacing 0 0 0; + @media all and ($mobile) { + margin-top: 0; + padding: 0; + } + } + + & .center > article { + grid-area: grid-center; + } + + & footer { + grid-area: grid-footer; + } + + & .center, + & footer { + max-width: 100%; + min-width: 100%; + margin-left: auto; + margin-right: auto; + @media all and ($tablet) { + margin-right: 0; + } + @media all and ($mobile) { + margin-right: 0; + margin-left: 0; + } + } + & footer { + margin-left: 0; + } + } +} + +.footnotes { + margin-top: 2rem; + border-top: 1px solid var(--lightgray); +} + +input[type="checkbox"] { + transform: translateY(2px); + color: var(--secondary); + border: 1px solid var(--lightgray); + border-radius: 3px; + background-color: var(--light); + position: relative; + margin-inline-end: 0.2rem; + margin-inline-start: -1.4rem; + appearance: none; + width: 16px; + height: 16px; + + &:checked { + border-color: var(--secondary); + background-color: var(--secondary); + + &::after { + content: ""; + position: absolute; + left: 4px; + top: 1px; + width: 4px; + height: 8px; + display: block; + border: solid var(--light); + border-width: 0 2px 2px 0; + transform: rotate(45deg); + } + } +} + +blockquote { + margin: 1rem 0; + border-left: 3px solid var(--secondary); + padding-left: 1rem; + transition: border-color 0.2s ease; +} + +h1, +h2, +h3, +h4, +h5, +h6, +thead { + font-family: var(--headerFont); + color: var(--dark); + font-weight: revert; + margin-bottom: 0; + + article > & > a[role="anchor"] { + color: var(--dark); + background-color: transparent; + } +} + +h1, +h2, +h3, +h4, +h5, +h6 { + &[id] > a[href^="#"] { + margin: 0 0.5rem; + opacity: 0; + transition: opacity 0.2s ease; + transform: translateY(-0.1rem); + font-family: var(--codeFont); + user-select: none; + } + + &[id]:hover > a { + opacity: 1; + } +} + +// typography improvements +h1 { + font-size: 1.75rem; + margin-top: 2.25rem; + margin-bottom: 1rem; +} + +h2 { + font-size: 1.4rem; + margin-top: 1.9rem; + margin-bottom: 1rem; +} + +h3 { + font-size: 1.12rem; + margin-top: 1.62rem; + margin-bottom: 1rem; +} + +h4, +h5, +h6 { + font-size: 1rem; + margin-top: 1.5rem; + margin-bottom: 1rem; +} + +figure[data-rehype-pretty-code-figure] { + margin: 0; + position: relative; + line-height: 1.6rem; + position: relative; + + & > [data-rehype-pretty-code-title] { + font-family: var(--codeFont); + font-size: 0.9rem; + padding: 0.1rem 0.5rem; + border: 1px solid var(--lightgray); + width: fit-content; + border-radius: 5px; + margin-bottom: -0.5rem; + color: var(--darkgray); + } + + & > pre { + padding: 0; + } +} + +pre { + font-family: var(--codeFont); + padding: 0 0.5rem; + border-radius: 5px; + overflow-x: auto; + border: 1px solid var(--lightgray); + position: relative; + + &:has(> code.mermaid) { + border: none; + } + + & > code { + background: none; + padding: 0; + font-size: 0.85rem; + counter-reset: line; + counter-increment: line 0; + display: grid; + padding: 0.5rem 0; + overflow-x: auto; + + & [data-highlighted-chars] { + background-color: var(--highlight); + border-radius: 5px; + } + + & > [data-line] { + padding: 0 0.25rem; + box-sizing: border-box; + border-left: 3px solid transparent; + + &[data-highlighted-line] { + background-color: var(--highlight); + border-left: 3px solid var(--secondary); + } + + &::before { + content: counter(line); + counter-increment: line; + width: 1rem; + margin-right: 1rem; + display: inline-block; + text-align: right; + color: rgba(115, 138, 148, 0.6); + } + } + + &[data-line-numbers-max-digits="2"] > [data-line]::before { + width: 2rem; + } + + &[data-line-numbers-max-digits="3"] > [data-line]::before { + width: 3rem; + } + } +} + +code { + font-size: 0.9em; + color: var(--dark); + font-family: var(--codeFont); + border-radius: 5px; + padding: 0.1rem 0.2rem; + background: var(--lightgray); +} + +tbody, +li, +p { + line-height: 1.6rem; +} + +.table-container { + overflow-x: auto; + + & > table { + margin: 1rem; + padding: 1.5rem; + border-collapse: collapse; + + th, + td { + min-width: 75px; + } + + & > * { + line-height: 2rem; + } + } +} + +th { + text-align: left; + padding: 0.4rem 0.7rem; + border-bottom: 2px solid var(--gray); +} + +td { + padding: 0.2rem 0.7rem; +} + +tr { + border-bottom: 1px solid var(--lightgray); + &:last-child { + border-bottom: none; + } +} + +img { + max-width: 100%; + border-radius: 5px; + margin: 1rem 0; + content-visibility: auto; +} + +p > img + em { + display: block; + transform: translateY(-1rem); +} + +hr { + width: 100%; + margin: 2rem auto; + height: 1px; + border: none; + background-color: var(--lightgray); +} + +audio, +video { + width: 100%; + border-radius: 5px; +} + +.spacer { + flex: 1 1 auto; +} + +div:has(> .overflow) { + display: flex; + overflow-y: auto; + max-height: 100%; +} + +ul.overflow, +ol.overflow { + max-height: 100%; + overflow-y: auto; + + // clearfix + content: ""; + clear: both; + + & > li:last-of-type { + margin-bottom: 30px; + } + /*&: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)); + }*/ +} + +.transclude { + ul { + padding-left: 1rem; + } +} + +.katex-display { + overflow-x: auto; + overflow-y: hidden; +} + +.external-embed.youtube, +iframe.pdf { + aspect-ratio: 16 / 9; + height: 100%; + width: 100%; + border-radius: 5px; +} + +.navigation-progress { + position: fixed; + top: 0; + left: 0; + width: 0; + height: 3px; + background: var(--secondary); + transition: width 0.2s ease; + z-index: 9999; +} diff --git a/quartz/styles/callouts.scss b/quartz/styles/callouts.scss new file mode 100755 index 0000000..d6f65aa --- /dev/null +++ b/quartz/styles/callouts.scss @@ -0,0 +1,162 @@ +@use "./variables.scss" as *; +@use "sass:color"; + +.callout { + border: 1px solid var(--border); + background-color: var(--bg); + border-radius: 5px; + padding: 0 1rem; + overflow-y: hidden; + transition: max-height 0.3s ease; + box-sizing: border-box; + + & > .callout-content > :first-child { + margin-top: 0; + } + + --callout-icon-note: url('data:image/svg+xml; utf8, '); + --callout-icon-abstract: url('data:image/svg+xml; utf8, '); + --callout-icon-info: url('data:image/svg+xml; utf8, '); + --callout-icon-todo: url('data:image/svg+xml; utf8, '); + --callout-icon-tip: url('data:image/svg+xml; utf8, '); + --callout-icon-success: url('data:image/svg+xml; utf8, '); + --callout-icon-question: url('data:image/svg+xml; utf8, '); + --callout-icon-warning: url('data:image/svg+xml; utf8, '); + --callout-icon-failure: url('data:image/svg+xml; utf8, '); + --callout-icon-danger: url('data:image/svg+xml; utf8, '); + --callout-icon-bug: url('data:image/svg+xml; utf8, '); + --callout-icon-example: url('data:image/svg+xml; utf8, '); + --callout-icon-quote: url('data:image/svg+xml; utf8, '); + --callout-icon-fold: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"%3E%3Cpolyline points="6 9 12 15 18 9"%3E%3C/polyline%3E%3C/svg%3E'); + + &[data-callout] { + --color: #448aff; + --border: #448aff44; + --bg: #448aff10; + --callout-icon: var(--callout-icon-note); + } + + &[data-callout="abstract"] { + --color: #00b0ff; + --border: #00b0ff44; + --bg: #00b0ff10; + --callout-icon: var(--callout-icon-abstract); + } + + &[data-callout="info"], + &[data-callout="todo"] { + --color: #00b8d4; + --border: #00b8d444; + --bg: #00b8d410; + --callout-icon: var(--callout-icon-info); + } + + &[data-callout="todo"] { + --callout-icon: var(--callout-icon-todo); + } + + &[data-callout="tip"] { + --color: #00bfa5; + --border: #00bfa544; + --bg: #00bfa510; + --callout-icon: var(--callout-icon-tip); + } + + &[data-callout="success"] { + --color: #09ad7a; + --border: #09ad7144; + --bg: #09ad7110; + --callout-icon: var(--callout-icon-success); + } + + &[data-callout="question"] { + --color: #dba642; + --border: #dba64244; + --bg: #dba64210; + --callout-icon: var(--callout-icon-question); + } + + &[data-callout="warning"] { + --color: #db8942; + --border: #db894244; + --bg: #db894210; + --callout-icon: var(--callout-icon-warning); + } + + &[data-callout="failure"], + &[data-callout="danger"], + &[data-callout="bug"] { + --color: #db4242; + --border: #db424244; + --bg: #db424210; + --callout-icon: var(--callout-icon-failure); + } + + &[data-callout="bug"] { + --callout-icon: var(--callout-icon-bug); + } + + &[data-callout="danger"] { + --callout-icon: var(--callout-icon-danger); + } + + &[data-callout="example"] { + --color: #7a43b5; + --border: #7a43b544; + --bg: #7a43b510; + --callout-icon: var(--callout-icon-example); + } + + &[data-callout="quote"] { + --color: var(--secondary); + --border: var(--lightgray); + --callout-icon: var(--callout-icon-quote); + } + + &.is-collapsed > .callout-title > .fold-callout-icon { + transform: rotateZ(-90deg); + } +} + +.callout-title { + display: flex; + align-items: flex-start; + gap: 5px; + padding: 1rem 0; + color: var(--color); + + --icon-size: 18px; + + & .fold-callout-icon { + transition: transform 0.15s ease; + opacity: 0.8; + cursor: pointer; + --callout-icon: var(--callout-icon-fold); + } + + & > .callout-title-inner > p { + color: var(--color); + margin: 0; + } + + .callout-icon, + & .fold-callout-icon { + width: var(--icon-size); + height: var(--icon-size); + flex: 0 0 var(--icon-size); + + // icon support + background-size: var(--icon-size) var(--icon-size); + background-position: center; + background-color: var(--color); + mask-image: var(--callout-icon); + mask-size: var(--icon-size) var(--icon-size); + mask-position: center; + mask-repeat: no-repeat; + padding: 0.2rem 0; + } + + .callout-title-inner { + font-weight: $semiBoldWeight; + } +} diff --git a/quartz/styles/custom.scss b/quartz/styles/custom.scss new file mode 100755 index 0000000..931ae37 --- /dev/null +++ b/quartz/styles/custom.scss @@ -0,0 +1,10 @@ +@use "./base.scss"; + +.callout { + &[data-callout="xpost"] { + --color: rgb(201, 201, 201); + --border: rgb(119, 119, 119); + --bg: rgb(0, 0, 0); + --callout-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' shape-rendering='geometricPrecision' text-rendering='geometricPrecision' image-rendering='optimizeQuality' fill-rule='evenodd' clip-rule='evenodd' viewBox='0 0 512 462.799'%3E%3Cpath fill-rule='nonzero' d='M403.229 0h78.506L310.219 196.04 512 462.799H354.002L230.261 301.007 88.669 462.799h-78.56l183.455-209.683L0 0h161.999l111.856 147.88L403.229 0zm-27.556 415.805h43.505L138.363 44.527h-46.68l283.99 371.278z'/%3E%3C/svg%3E"); //SVG icon code + } + } diff --git a/quartz/styles/syntax.scss b/quartz/styles/syntax.scss new file mode 100755 index 0000000..ba20563 --- /dev/null +++ b/quartz/styles/syntax.scss @@ -0,0 +1,17 @@ +code[data-theme*=" "] { + color: var(--shiki-light); + background-color: var(--shiki-light-bg); +} + +code[data-theme*=" "] span { + color: var(--shiki-light); +} + +[saved-theme="dark"] code[data-theme*=" "] { + color: var(--shiki-dark); + background-color: var(--shiki-dark-bg); +} + +[saved-theme="dark"] code[data-theme*=" "] span { + color: var(--shiki-dark); +} diff --git a/quartz/styles/variables.scss b/quartz/styles/variables.scss new file mode 100755 index 0000000..1d237d0 --- /dev/null +++ b/quartz/styles/variables.scss @@ -0,0 +1,58 @@ +@use "sass:map"; + +/** + * Layout breakpoints + * $mobile: screen width below this value will use mobile styles + * $desktop: screen width above this value will use desktop styles + * 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; +$boldWeight: 700; +$semiBoldWeight: 600; +$normalWeight: 400; + +$mobileGrid: ( + templateRows: "auto auto auto auto auto", + templateColumns: "auto", + rowGap: "5px", + columnGap: "5px", + templateAreas: + '"grid-header"\ + "grid-sidebar-right"\ + "grid-center"\ + "grid-sidebar-left"\ + "grid-footer"', +); +$tabletGrid: ( + templateRows: "auto auto auto auto", + templateColumns: "#{$sidePanelWidth} auto", + rowGap: "5px", + columnGap: "5px", + templateAreas: + '"grid-sidebar-left grid-header"\ + "grid-sidebar-left grid-center"\ + "grid-sidebar-left grid-sidebar-right"\ + "grid-sidebar-left grid-footer"', +); +$desktopGrid: ( + templateRows: "auto auto auto", + templateColumns: "#{$sidePanelWidth} auto #{$sidePanelWidth}", + rowGap: "5px", + columnGap: "5px", + templateAreas: + '"grid-sidebar-right grid-header ."\ + "grid-sidebar-right grid-center ."\ + "grid-sidebar-right grid-footer ."', +); \ No newline at end of file diff --git a/quartz/util/ctx.ts b/quartz/util/ctx.ts new file mode 100755 index 0000000..044d21f --- /dev/null +++ b/quartz/util/ctx.ts @@ -0,0 +1,21 @@ +import { QuartzConfig } from "../cfg" +import { FullSlug } from "./path" + +export interface Argv { + directory: string + verbose: boolean + output: string + serve: boolean + fastRebuild: boolean + port: number + wsPort: number + remoteDevHost?: string + concurrency?: number +} + +export interface BuildCtx { + buildId: string + argv: Argv + cfg: QuartzConfig + allSlugs: FullSlug[] +} diff --git a/quartz/util/escape.ts b/quartz/util/escape.ts new file mode 100755 index 0000000..ac59cc7 --- /dev/null +++ b/quartz/util/escape.ts @@ -0,0 +1,17 @@ +export const escapeHTML = (unsafe: string) => { + return unsafe + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'") +} + +export const unescapeHTML = (html: string) => { + return html + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll(""", '"') + .replaceAll("'", "'") +} diff --git a/quartz/util/glob.ts b/quartz/util/glob.ts new file mode 100755 index 0000000..7a71160 --- /dev/null +++ b/quartz/util/glob.ts @@ -0,0 +1,22 @@ +import path from "path" +import { FilePath } from "./path" +import { globby } from "globby" + +export function toPosixPath(fp: string): string { + return fp.split(path.sep).join("/") +} + +export async function glob( + pattern: string, + cwd: string, + ignorePatterns: string[], +): Promise { + const fps = ( + await globby(pattern, { + cwd, + ignore: ignorePatterns, + gitignore: true, + }) + ).map(toPosixPath) + return fps as FilePath[] +} diff --git a/quartz/util/jsx.tsx b/quartz/util/jsx.tsx new file mode 100755 index 0000000..b525423 --- /dev/null +++ b/quartz/util/jsx.tsx @@ -0,0 +1,27 @@ +import { Components, Jsx, toJsxRuntime } from "hast-util-to-jsx-runtime" +import { Node, Root } from "hast" +import { Fragment, jsx, jsxs } from "preact/jsx-runtime" +import { trace } from "./trace" +import { type FilePath } from "./path" + +const customComponents: Components = { + table: (props) => ( +
    + + + ), +} + +export function htmlToJsx(fp: FilePath, tree: Node) { + try { + return toJsxRuntime(tree as Root, { + Fragment, + jsx: jsx as Jsx, + jsxs: jsxs as Jsx, + elementAttributeNameCase: "html", + components: customComponents, + }) + } catch (e) { + trace(`Failed to parse Markdown in \`${fp}\` into JSX`, e as Error) + } +} diff --git a/quartz/util/lang.ts b/quartz/util/lang.ts new file mode 100755 index 0000000..6fb0469 --- /dev/null +++ b/quartz/util/lang.ts @@ -0,0 +1,13 @@ +export function capitalize(s: string): string { + return s.substring(0, 1).toUpperCase() + s.substring(1) +} + +export function classNames( + displayClass?: "mobile-only" | "desktop-only", + ...classes: string[] +): string { + if (displayClass) { + classes.push(displayClass) + } + return classes.join(" ") +} diff --git a/quartz/util/log.ts b/quartz/util/log.ts new file mode 100755 index 0000000..773945c --- /dev/null +++ b/quartz/util/log.ts @@ -0,0 +1,28 @@ +import { Spinner } from "cli-spinner" + +export class QuartzLogger { + verbose: boolean + spinner: Spinner | undefined + constructor(verbose: boolean) { + this.verbose = verbose + } + + start(text: string) { + if (this.verbose) { + console.log(text) + } else { + this.spinner = new Spinner(`%s ${text}`) + this.spinner.setSpinnerString(18) + this.spinner.start() + } + } + + end(text?: string) { + if (!this.verbose) { + this.spinner!.stop(true) + } + if (text) { + console.log(text) + } + } +} diff --git a/quartz/util/og.tsx b/quartz/util/og.tsx new file mode 100755 index 0000000..42b9b27 --- /dev/null +++ b/quartz/util/og.tsx @@ -0,0 +1,202 @@ +import { FontWeight, SatoriOptions } from "satori/wasm" +import { GlobalConfiguration } from "../cfg" +import { QuartzPluginData } from "../plugins/vfile" +import { JSXInternal } from "preact/src/jsx" +import { ThemeKey } from "./theme" + +/** + * Get an array of `FontOptions` (for satori) given google font names + * @param headerFontName name of google font used for header + * @param bodyFontName name of google font used for body + * @returns FontOptions for header and body + */ +export async function getSatoriFont(headerFontName: string, bodyFontName: string) { + const headerWeight = 700 as FontWeight + const bodyWeight = 400 as FontWeight + + // Fetch fonts + const headerFont = await fetchTtf(headerFontName, headerWeight) + const bodyFont = await fetchTtf(bodyFontName, bodyWeight) + + // Convert fonts to satori font format and return + const fonts: SatoriOptions["fonts"] = [ + { name: headerFontName, data: headerFont, weight: headerWeight, style: "normal" }, + { name: bodyFontName, data: bodyFont, weight: bodyWeight, style: "normal" }, + ] + return fonts +} + +/** + * Get the `.ttf` file of a google font + * @param fontName name of google font + * @param weight what font weight to fetch font + * @returns `.ttf` file of google font + */ +async function fetchTtf(fontName: string, weight: FontWeight): Promise { + try { + // Get css file from google fonts + const cssResponse = await fetch( + `https://fonts.googleapis.com/css2?family=${fontName}:wght@${weight}`, + ) + const css = await cssResponse.text() + + // Extract .ttf url from css file + const urlRegex = /url\((https:\/\/fonts.gstatic.com\/s\/.*?.ttf)\)/g + const match = urlRegex.exec(css) + + if (!match) { + throw new Error("Could not fetch font") + } + + // Retrieve font data as ArrayBuffer + const fontResponse = await fetch(match[1]) + + // fontData is an ArrayBuffer containing the .ttf file data (get match[1] due to google fonts response format, always contains link twice, but second entry is the "raw" link) + const fontData = await fontResponse.arrayBuffer() + + return fontData + } catch (error) { + throw new Error(`Error fetching font: ${error}`) + } +} + +export type SocialImageOptions = { + /** + * What color scheme to use for image generation (uses colors from config theme) + */ + colorScheme: ThemeKey + /** + * Height to generate image with in pixels (should be around 630px) + */ + height: number + /** + * Width to generate image with in pixels (should be around 1200px) + */ + width: number + /** + * Whether to use the auto generated image for the root path ("/", when set to false) or the default og image (when set to true). + */ + excludeRoot: boolean + /** + * JSX to use for generating image. See satori docs for more info (https://github.com/vercel/satori) + * @param cfg global quartz config + * @param userOpts options that can be set by user + * @param title title of current page + * @param description description of current page + * @param fonts global font that can be used for styling + * @param fileData full fileData of current page + * @returns prepared jsx to be used for generating image + */ + imageStructure: ( + cfg: GlobalConfiguration, + userOpts: UserOpts, + title: string, + description: string, + fonts: SatoriOptions["fonts"], + fileData: QuartzPluginData, + ) => JSXInternal.Element +} + +export type UserOpts = Omit + +export type ImageOptions = { + /** + * what title to use as header in image + */ + title: string + /** + * what description to use as body in image + */ + description: string + /** + * what fileName to use when writing to disk + */ + fileName: string + /** + * what directory to store image in + */ + fileDir: string + /** + * what file extension to use (should be `webp` unless you also change sharp conversion) + */ + fileExt: string + /** + * header + body font to be used when generating satori image (as promise to work around sync in component) + */ + fontsPromise: Promise + /** + * `GlobalConfiguration` of quartz (used for theme/typography) + */ + cfg: GlobalConfiguration + /** + * full file data of current page + */ + fileData: QuartzPluginData +} + +// This is the default template for generated social image. +export const defaultImage: SocialImageOptions["imageStructure"] = ( + cfg: GlobalConfiguration, + { colorScheme }: UserOpts, + title: string, + description: string, + fonts: SatoriOptions["fonts"], + _fileData: QuartzPluginData, +) => { + // How many characters are allowed before switching to smaller font + const fontBreakPoint = 22 + const useSmallerFont = title.length > fontBreakPoint + + // Setup to access image + const iconPath = `https://${cfg.baseUrl}/static/icon.png` + return ( +
    +
    + +

    + {title} +

    +
    +

    + {description} +

    +
    + ) +} diff --git a/quartz/util/path.test.ts b/quartz/util/path.test.ts new file mode 100755 index 0000000..29d845d --- /dev/null +++ b/quartz/util/path.test.ts @@ -0,0 +1,305 @@ +import test, { describe } from "node:test" +import * as path from "./path" +import assert from "node:assert" +import { FullSlug, TransformOptions } from "./path" + +describe("typeguards", () => { + test("isSimpleSlug", () => { + assert(path.isSimpleSlug("")) + assert(path.isSimpleSlug("abc")) + assert(path.isSimpleSlug("abc/")) + assert(path.isSimpleSlug("notindex")) + assert(path.isSimpleSlug("notindex/def")) + + assert(!path.isSimpleSlug("//")) + assert(!path.isSimpleSlug("index")) + assert(!path.isSimpleSlug("https://example.com")) + assert(!path.isSimpleSlug("/abc")) + assert(!path.isSimpleSlug("abc/index")) + assert(!path.isSimpleSlug("abc#anchor")) + assert(!path.isSimpleSlug("abc?query=1")) + assert(!path.isSimpleSlug("index.md")) + assert(!path.isSimpleSlug("index.html")) + }) + + test("isRelativeURL", () => { + assert(path.isRelativeURL(".")) + assert(path.isRelativeURL("..")) + assert(path.isRelativeURL("./abc/def")) + assert(path.isRelativeURL("./abc/def#an-anchor")) + assert(path.isRelativeURL("./abc/def?query=1#an-anchor")) + assert(path.isRelativeURL("../abc/def")) + assert(path.isRelativeURL("./abc/def.pdf")) + + assert(!path.isRelativeURL("abc")) + assert(!path.isRelativeURL("/abc/def")) + assert(!path.isRelativeURL("")) + assert(!path.isRelativeURL("./abc/def.html")) + assert(!path.isRelativeURL("./abc/def.md")) + }) + + test("isFullSlug", () => { + assert(path.isFullSlug("index")) + assert(path.isFullSlug("abc/def")) + assert(path.isFullSlug("html.energy")) + assert(path.isFullSlug("test.pdf")) + + assert(!path.isFullSlug(".")) + assert(!path.isFullSlug("./abc/def")) + assert(!path.isFullSlug("../abc/def")) + assert(!path.isFullSlug("abc/def#anchor")) + assert(!path.isFullSlug("abc/def?query=1")) + assert(!path.isFullSlug("note with spaces")) + }) + + test("isFilePath", () => { + assert(path.isFilePath("content/index.md")) + assert(path.isFilePath("content/test.png")) + assert(!path.isFilePath("../test.pdf")) + assert(!path.isFilePath("content/test")) + assert(!path.isFilePath("./content/test")) + }) +}) + +describe("transforms", () => { + function asserts( + pairs: [string, string][], + transform: (inp: Inp) => Out, + checkPre: (x: any) => x is Inp, + checkPost: (x: any) => x is Out, + ) { + for (const [inp, expected] of pairs) { + assert(checkPre(inp), `${inp} wasn't the expected input type`) + const actual = transform(inp) + assert.strictEqual( + actual, + expected, + `after transforming ${inp}, '${actual}' was not '${expected}'`, + ) + assert(checkPost(actual), `${actual} wasn't the expected output type`) + } + } + + test("simplifySlug", () => { + asserts( + [ + ["index", "/"], + ["abc", "abc"], + ["abc/index", "abc/"], + ["abc/def", "abc/def"], + ], + path.simplifySlug, + path.isFullSlug, + path.isSimpleSlug, + ) + }) + + test("slugifyFilePath", () => { + asserts( + [ + ["content/index.md", "content/index"], + ["content/index.html", "content/index"], + ["content/_index.md", "content/index"], + ["/content/index.md", "content/index"], + ["content/cool.png", "content/cool.png"], + ["index.md", "index"], + ["test.mp4", "test.mp4"], + ["note with spaces.md", "note-with-spaces"], + ["notes.with.dots.md", "notes.with.dots"], + ["test/special chars?.md", "test/special-chars"], + ["test/special chars #3.md", "test/special-chars-3"], + ["cool/what about r&d?.md", "cool/what-about-r-and-d"], + ], + path.slugifyFilePath, + path.isFilePath, + path.isFullSlug, + ) + }) + + test("transformInternalLink", () => { + asserts( + [ + ["", "."], + [".", "."], + ["./", "./"], + ["./index", "./"], + ["./index#abc", "./#abc"], + ["./index.html", "./"], + ["./index.md", "./"], + ["./index.css", "./index.css"], + ["content", "./content"], + ["content/test.md", "./content/test"], + ["content/test.pdf", "./content/test.pdf"], + ["./content/test.md", "./content/test"], + ["../content/test.md", "../content/test"], + ["tags/", "./tags/"], + ["/tags/", "./tags/"], + ["content/with spaces", "./content/with-spaces"], + ["content/with spaces/index", "./content/with-spaces/"], + ["content/with spaces#and Anchor!", "./content/with-spaces#and-anchor"], + ], + path.transformInternalLink, + (_x: string): _x is string => true, + path.isRelativeURL, + ) + }) + + test("pathToRoot", () => { + asserts( + [ + ["index", "."], + ["abc", "."], + ["abc/def", ".."], + ["abc/def/ghi", "../.."], + ["abc/def/index", "../.."], + ], + path.pathToRoot, + path.isFullSlug, + path.isRelativeURL, + ) + }) + + test("joinSegments", () => { + assert.strictEqual(path.joinSegments("a", "b"), "a/b") + assert.strictEqual(path.joinSegments("a/", "b"), "a/b") + assert.strictEqual(path.joinSegments("a", "b/"), "a/b/") + assert.strictEqual(path.joinSegments("a/", "b/"), "a/b/") + + // preserve leading and trailing slashes + assert.strictEqual(path.joinSegments("/a", "b"), "/a/b") + assert.strictEqual(path.joinSegments("/a/", "b"), "/a/b") + assert.strictEqual(path.joinSegments("/a", "b/"), "/a/b/") + assert.strictEqual(path.joinSegments("/a/", "b/"), "/a/b/") + + // lone slash + assert.strictEqual(path.joinSegments("/a/", "b", "/"), "/a/b/") + assert.strictEqual(path.joinSegments("a/", "b" + "/"), "a/b/") + + // works with protocol specifiers + assert.strictEqual(path.joinSegments("https://example.com", "a"), "https://example.com/a") + assert.strictEqual(path.joinSegments("https://example.com/", "a"), "https://example.com/a") + assert.strictEqual(path.joinSegments("https://example.com", "a/"), "https://example.com/a/") + assert.strictEqual(path.joinSegments("https://example.com/", "a/"), "https://example.com/a/") + }) +}) + +describe("link strategies", () => { + const allSlugs = [ + "a/b/c", + "a/b/d", + "a/b/index", + "e/f", + "e/g/h", + "index", + "a/test.png", + ] as FullSlug[] + + describe("absolute", () => { + const opts: TransformOptions = { + strategy: "absolute", + allSlugs, + } + + test("from a/b/c", () => { + const cur = "a/b/c" as FullSlug + assert.strictEqual(path.transformLink(cur, "a/b/d", opts), "../../a/b/d") + assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../a/b/") + assert.strictEqual(path.transformLink(cur, "e/f", opts), "../../e/f") + assert.strictEqual(path.transformLink(cur, "e/g/h", opts), "../../e/g/h") + assert.strictEqual(path.transformLink(cur, "index", opts), "../../") + assert.strictEqual(path.transformLink(cur, "index.png", opts), "../../index.png") + assert.strictEqual(path.transformLink(cur, "index#abc", opts), "../../#abc") + assert.strictEqual(path.transformLink(cur, "tag/test", opts), "../../tag/test") + assert.strictEqual(path.transformLink(cur, "a/b/c#test", opts), "../../a/b/c#test") + assert.strictEqual(path.transformLink(cur, "a/test.png", opts), "../../a/test.png") + }) + + test("from a/b/index", () => { + const cur = "a/b/index" as FullSlug + assert.strictEqual(path.transformLink(cur, "a/b/d", opts), "../../a/b/d") + assert.strictEqual(path.transformLink(cur, "a/b", opts), "../../a/b") + assert.strictEqual(path.transformLink(cur, "index", opts), "../../") + }) + + test("from index", () => { + const cur = "index" as FullSlug + assert.strictEqual(path.transformLink(cur, "index", opts), "./") + assert.strictEqual(path.transformLink(cur, "a/b/c", opts), "./a/b/c") + assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "./a/b/") + }) + }) + + describe("shortest", () => { + const opts: TransformOptions = { + strategy: "shortest", + allSlugs, + } + + test("from a/b/c", () => { + const cur = "a/b/c" as FullSlug + assert.strictEqual(path.transformLink(cur, "d", opts), "../../a/b/d") + assert.strictEqual(path.transformLink(cur, "h", opts), "../../e/g/h") + assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../a/b/") + assert.strictEqual(path.transformLink(cur, "a/b/index.png", opts), "../../a/b/index.png") + assert.strictEqual(path.transformLink(cur, "a/b/index#abc", opts), "../../a/b/#abc") + assert.strictEqual(path.transformLink(cur, "index", opts), "../../") + assert.strictEqual(path.transformLink(cur, "index.png", opts), "../../index.png") + assert.strictEqual(path.transformLink(cur, "test.png", opts), "../../a/test.png") + assert.strictEqual(path.transformLink(cur, "index#abc", opts), "../../#abc") + }) + + test("from a/b/index", () => { + const cur = "a/b/index" as FullSlug + assert.strictEqual(path.transformLink(cur, "d", opts), "../../a/b/d") + assert.strictEqual(path.transformLink(cur, "h", opts), "../../e/g/h") + assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../a/b/") + assert.strictEqual(path.transformLink(cur, "index", opts), "../../") + }) + + test("from index", () => { + const cur = "index" as FullSlug + assert.strictEqual(path.transformLink(cur, "d", opts), "./a/b/d") + assert.strictEqual(path.transformLink(cur, "h", opts), "./e/g/h") + assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "./a/b/") + assert.strictEqual(path.transformLink(cur, "index", opts), "./") + }) + }) + + describe("relative", () => { + const opts: TransformOptions = { + strategy: "relative", + allSlugs, + } + + test("from a/b/c", () => { + const cur = "a/b/c" as FullSlug + assert.strictEqual(path.transformLink(cur, "d", opts), "./d") + assert.strictEqual(path.transformLink(cur, "index", opts), "./") + assert.strictEqual(path.transformLink(cur, "../../../index", opts), "../../../") + assert.strictEqual(path.transformLink(cur, "../../../index.png", opts), "../../../index.png") + assert.strictEqual(path.transformLink(cur, "../../../index#abc", opts), "../../../#abc") + assert.strictEqual(path.transformLink(cur, "../../../", opts), "../../../") + assert.strictEqual( + path.transformLink(cur, "../../../a/test.png", opts), + "../../../a/test.png", + ) + assert.strictEqual(path.transformLink(cur, "../../../e/g/h", opts), "../../../e/g/h") + assert.strictEqual(path.transformLink(cur, "../../../e/g/h", opts), "../../../e/g/h") + assert.strictEqual(path.transformLink(cur, "../../../e/g/h#abc", opts), "../../../e/g/h#abc") + }) + + test("from a/b/index", () => { + const cur = "a/b/index" as FullSlug + assert.strictEqual(path.transformLink(cur, "../../index", opts), "../../") + assert.strictEqual(path.transformLink(cur, "../../", opts), "../../") + assert.strictEqual(path.transformLink(cur, "../../e/g/h", opts), "../../e/g/h") + assert.strictEqual(path.transformLink(cur, "c", opts), "./c") + }) + + test("from index", () => { + const cur = "index" as FullSlug + assert.strictEqual(path.transformLink(cur, "e/g/h", opts), "./e/g/h") + assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "./a/b/") + }) + }) +}) diff --git a/quartz/util/path.ts b/quartz/util/path.ts new file mode 100755 index 0000000..5835f15 --- /dev/null +++ b/quartz/util/path.ts @@ -0,0 +1,311 @@ +import { slug as slugAnchor } from "github-slugger" +import type { Element as HastElement } from "hast" +import rfdc from "rfdc" + +export const clone = rfdc() + +// this file must be isomorphic so it can't use node libs (e.g. path) + +export const QUARTZ = "quartz" + +/// Utility type to simulate nominal types in TypeScript +type SlugLike = string & { __brand: T } + +/** Cannot be relative and must have a file extension. */ +export type FilePath = SlugLike<"filepath"> +export function isFilePath(s: string): s is FilePath { + const validStart = !s.startsWith(".") + return validStart && _hasFileExtension(s) +} + +/** Cannot be relative and may not have leading or trailing slashes. It can have `index` as it's last segment. Use this wherever possible is it's the most 'general' interpretation of a slug. */ +export type FullSlug = SlugLike<"full"> +export function isFullSlug(s: string): s is FullSlug { + const validStart = !(s.startsWith(".") || s.startsWith("/")) + const validEnding = !s.endsWith("/") + return validStart && validEnding && !containsForbiddenCharacters(s) +} + +/** Shouldn't be a relative path and shouldn't have `/index` as an ending or a file extension. It _can_ however have a trailing slash to indicate a folder path. */ +export type SimpleSlug = SlugLike<"simple"> +export function isSimpleSlug(s: string): s is SimpleSlug { + const validStart = !(s.startsWith(".") || (s.length > 1 && s.startsWith("/"))) + const validEnding = !endsWith(s, "index") + return validStart && !containsForbiddenCharacters(s) && validEnding && !_hasFileExtension(s) +} + +/** Can be found on `href`s but can also be constructed for client-side navigation (e.g. search and graph) */ +export type RelativeURL = SlugLike<"relative"> +export function isRelativeURL(s: string): s is RelativeURL { + const validStart = /^\.{1,2}/.test(s) + const validEnding = !endsWith(s, "index") + return validStart && validEnding && ![".md", ".html"].includes(_getFileExtension(s) ?? "") +} + +export function getFullSlug(window: Window): FullSlug { + const res = window.document.body.dataset.slug! as FullSlug + return res +} + +function sluggify(s: string): string { + return s + .split("/") + .map((segment) => + segment + .replace(/\s/g, "-") + .replace(/&/g, "-and-") + .replace(/%/g, "-percent") + .replace(/\?/g, "") + .replace(/#/g, ""), + ) + .join("/") // always use / as sep + .replace(/\/$/, "") +} + +export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug { + fp = stripSlashes(fp) as FilePath + let ext = _getFileExtension(fp) + const withoutFileExt = fp.replace(new RegExp(ext + "$"), "") + if (excludeExt || [".md", ".html", undefined].includes(ext)) { + ext = "" + } + + let slug = sluggify(withoutFileExt) + + // treat _index as index + if (endsWith(slug, "_index")) { + slug = slug.replace(/_index$/, "index") + } + + return (slug + ext) as FullSlug +} + +export function simplifySlug(fp: FullSlug): SimpleSlug { + const res = stripSlashes(trimSuffix(fp, "index"), true) + return (res.length === 0 ? "/" : res) as SimpleSlug +} + +export function transformInternalLink(link: string): RelativeURL { + let [fplike, anchor] = splitAnchor(decodeURI(link)) + + const folderPath = isFolderPath(fplike) + let segments = fplike.split("/").filter((x) => x.length > 0) + let prefix = segments.filter(isRelativeSegment).join("/") + let fp = segments.filter((seg) => !isRelativeSegment(seg) && seg !== "").join("/") + + // manually add ext here as we want to not strip 'index' if it has an extension + const simpleSlug = simplifySlug(slugifyFilePath(fp as FilePath)) + const joined = joinSegments(stripSlashes(prefix), stripSlashes(simpleSlug)) + const trail = folderPath ? "/" : "" + const res = (_addRelativeToStart(joined) + trail + anchor) as RelativeURL + return res +} + +// from micromorph/src/utils.ts +// https://github.com/natemoo-re/micromorph/blob/main/src/utils.ts#L5 +const _rebaseHtmlElement = (el: Element, attr: string, newBase: string | URL) => { + const rebased = new URL(el.getAttribute(attr)!, newBase) + el.setAttribute(attr, rebased.pathname + rebased.hash) +} +export function normalizeRelativeURLs(el: Element | Document, destination: string | URL) { + el.querySelectorAll('[href=""], [href^="./"], [href^="../"]').forEach((item) => + _rebaseHtmlElement(item, "href", destination), + ) + el.querySelectorAll('[src=""], [src^="./"], [src^="../"]').forEach((item) => + _rebaseHtmlElement(item, "src", destination), + ) +} + +const _rebaseHastElement = ( + el: HastElement, + attr: string, + curBase: FullSlug, + newBase: FullSlug, +) => { + if (el.properties?.[attr]) { + if (!isRelativeURL(String(el.properties[attr]))) { + return + } + + const rel = joinSegments(resolveRelative(curBase, newBase), "..", el.properties[attr] as string) + el.properties[attr] = rel + } +} + +export function normalizeHastElement(rawEl: HastElement, curBase: FullSlug, newBase: FullSlug) { + const el = clone(rawEl) // clone so we dont modify the original page + _rebaseHastElement(el, "src", curBase, newBase) + _rebaseHastElement(el, "href", curBase, newBase) + if (el.children) { + el.children = el.children.map((child) => + normalizeHastElement(child as HastElement, curBase, newBase), + ) + } + + return el +} + +// resolve /a/b/c to ../.. +export function pathToRoot(slug: FullSlug): RelativeURL { + let rootPath = slug + .split("/") + .filter((x) => x !== "") + .slice(0, -1) + .map((_) => "..") + .join("/") + + if (rootPath.length === 0) { + rootPath = "." + } + + return rootPath as RelativeURL +} + +export function resolveRelative(current: FullSlug, target: FullSlug | SimpleSlug): RelativeURL { + const res = joinSegments(pathToRoot(current), simplifySlug(target as FullSlug)) as RelativeURL + return res +} + +export function splitAnchor(link: string): [string, string] { + let [fp, anchor] = link.split("#", 2) + if (fp.endsWith(".pdf")) { + return [fp, anchor === undefined ? "" : `#${anchor}`] + } + anchor = anchor === undefined ? "" : "#" + slugAnchor(anchor) + return [fp, anchor] +} + +export function slugTag(tag: string) { + return tag + .split("/") + .map((tagSegment) => sluggify(tagSegment)) + .join("/") +} + +export function joinSegments(...args: string[]): string { + if (args.length === 0) { + return "" + } + + let joined = args + .filter((segment) => segment !== "" && segment !== "/") + .map((segment) => stripSlashes(segment)) + .join("/") + + // if the first segment starts with a slash, add it back + if (args[0].startsWith("/")) { + joined = "/" + joined + } + + // if the last segment is a folder, add a trailing slash + if (args[args.length - 1].endsWith("/")) { + joined = joined + "/" + } + + return joined +} + +export function getAllSegmentPrefixes(tags: string): string[] { + const segments = tags.split("/") + const results: string[] = [] + for (let i = 0; i < segments.length; i++) { + results.push(segments.slice(0, i + 1).join("/")) + } + return results +} + +export interface TransformOptions { + strategy: "absolute" | "relative" | "shortest" + allSlugs: FullSlug[] +} + +export function transformLink(src: FullSlug, target: string, opts: TransformOptions): RelativeURL { + let targetSlug = transformInternalLink(target) + + if (opts.strategy === "relative") { + return targetSlug as RelativeURL + } else { + const folderTail = isFolderPath(targetSlug) ? "/" : "" + const canonicalSlug = stripSlashes(targetSlug.slice(".".length)) + let [targetCanonical, targetAnchor] = splitAnchor(canonicalSlug) + + if (opts.strategy === "shortest") { + // if the file name is unique, then it's just the filename + const matchingFileNames = opts.allSlugs.filter((slug) => { + const parts = slug.split("/") + const fileName = parts.at(-1) + return targetCanonical === fileName + }) + + // only match, just use it + if (matchingFileNames.length === 1) { + const targetSlug = matchingFileNames[0] + return (resolveRelative(src, targetSlug) + targetAnchor) as RelativeURL + } + } + + // if it's not unique, then it's the absolute path from the vault root + return (joinSegments(pathToRoot(src), canonicalSlug) + folderTail) as RelativeURL + } +} + +// path helpers +function isFolderPath(fplike: string): boolean { + return ( + fplike.endsWith("/") || + endsWith(fplike, "index") || + endsWith(fplike, "index.md") || + endsWith(fplike, "index.html") + ) +} + +export function endsWith(s: string, suffix: string): boolean { + return s === suffix || s.endsWith("/" + suffix) +} + +function trimSuffix(s: string, suffix: string): string { + if (endsWith(s, suffix)) { + s = s.slice(0, -suffix.length) + } + return s +} + +function containsForbiddenCharacters(s: string): boolean { + return s.includes(" ") || s.includes("#") || s.includes("?") || s.includes("&") +} + +function _hasFileExtension(s: string): boolean { + return _getFileExtension(s) !== undefined +} + +function _getFileExtension(s: string): string | undefined { + return s.match(/\.[A-Za-z0-9]+$/)?.[0] +} + +function isRelativeSegment(s: string): boolean { + return /^\.{0,2}$/.test(s) +} + +export function stripSlashes(s: string, onlyStripPrefix?: boolean): string { + if (s.startsWith("/")) { + s = s.substring(1) + } + + if (!onlyStripPrefix && s.endsWith("/")) { + s = s.slice(0, -1) + } + + return s +} + +function _addRelativeToStart(s: string): string { + if (s === "") { + s = "." + } + + if (!s.startsWith(".")) { + s = joinSegments(".", s) + } + + return s +} diff --git a/quartz/util/perf.ts b/quartz/util/perf.ts new file mode 100755 index 0000000..ba34ddb --- /dev/null +++ b/quartz/util/perf.ts @@ -0,0 +1,19 @@ +import chalk from "chalk" +import pretty from "pretty-time" + +export class PerfTimer { + evts: { [key: string]: [number, number] } + + constructor() { + this.evts = {} + this.addEvent("start") + } + + addEvent(evtName: string) { + this.evts[evtName] = process.hrtime() + } + + timeSince(evtName?: string): string { + return chalk.yellow(pretty(process.hrtime(this.evts[evtName ?? "start"]))) + } +} diff --git a/quartz/util/resources.tsx b/quartz/util/resources.tsx new file mode 100755 index 0000000..72ae9e6 --- /dev/null +++ b/quartz/util/resources.tsx @@ -0,0 +1,65 @@ +import { randomUUID } from "crypto" +import { JSX } from "preact/jsx-runtime" + +export type JSResource = { + loadTime: "beforeDOMReady" | "afterDOMReady" + moduleType?: "module" + spaPreserve?: boolean +} & ( + | { + src: string + contentType: "external" + } + | { + script: string + contentType: "inline" + } +) + +export type CSSResource = { + content: string + inline?: boolean + spaPreserve?: boolean +} + +export function JSResourceToScriptElement(resource: JSResource, preserve?: boolean): JSX.Element { + const scriptType = resource.moduleType ?? "application/javascript" + const spaPreserve = preserve ?? resource.spaPreserve + if (resource.contentType === "external") { + return ( + + ) + } +} + +export function CSSResourceToStyleElement(resource: CSSResource, preserve?: boolean): JSX.Element { + const spaPreserve = preserve ?? resource.spaPreserve + if (resource.inline ?? false) { + return + } else { + return ( + + ) + } +} + +export interface StaticResources { + css: CSSResource[] + js: JSResource[] +} diff --git a/quartz/util/sourcemap.ts b/quartz/util/sourcemap.ts new file mode 100755 index 0000000..d3b9cf7 --- /dev/null +++ b/quartz/util/sourcemap.ts @@ -0,0 +1,18 @@ +import fs from "fs" +import sourceMapSupport from "source-map-support" +import { fileURLToPath } from "url" + +export const options: sourceMapSupport.Options = { + // source map hack to get around query param + // import cache busting + retrieveSourceMap(source) { + if (source.includes(".quartz-cache")) { + let realSource = fileURLToPath(source.split("?", 2)[0] + ".map") + return { + map: fs.readFileSync(realSource, "utf8"), + } + } else { + return null + } + }, +} diff --git a/quartz/util/theme.ts b/quartz/util/theme.ts new file mode 100755 index 0000000..9046cec --- /dev/null +++ b/quartz/util/theme.ts @@ -0,0 +1,72 @@ +export interface ColorScheme { + light: string + lightgray: string + gray: string + darkgray: string + dark: string + secondary: string + tertiary: string + highlight: string + textHighlight: string +} + +interface Colors { + lightMode: ColorScheme + darkMode: ColorScheme +} + +export interface Theme { + typography: { + header: string + body: string + code: string + } + cdnCaching: boolean + colors: Colors + fontOrigin: "googleFonts" | "local" +} + +export type ThemeKey = keyof Colors + +const DEFAULT_SANS_SERIF = + '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif' +const DEFAULT_MONO = "ui-monospace, SFMono-Regular, SF Mono, Menlo, monospace" + +export function googleFontHref(theme: Theme) { + const { code, header, body } = theme.typography + return `https://fonts.googleapis.com/css2?family=${code}&family=${header}:wght@400;700&family=${body}:ital,wght@0,400;0,600;1,400;1,600&display=swap` +} + +export function joinStyles(theme: Theme, ...stylesheet: string[]) { + return ` +${stylesheet.join("\n\n")} + +:root { + --light: ${theme.colors.lightMode.light}; + --lightgray: ${theme.colors.lightMode.lightgray}; + --gray: ${theme.colors.lightMode.gray}; + --darkgray: ${theme.colors.lightMode.darkgray}; + --dark: ${theme.colors.lightMode.dark}; + --secondary: ${theme.colors.lightMode.secondary}; + --tertiary: ${theme.colors.lightMode.tertiary}; + --highlight: ${theme.colors.lightMode.highlight}; + --textHighlight: ${theme.colors.lightMode.textHighlight}; + + --headerFont: "${theme.typography.header}", ${DEFAULT_SANS_SERIF}; + --bodyFont: "${theme.typography.body}", ${DEFAULT_SANS_SERIF}; + --codeFont: "${theme.typography.code}", ${DEFAULT_MONO}; +} + +:root[saved-theme="dark"] { + --light: ${theme.colors.darkMode.light}; + --lightgray: ${theme.colors.darkMode.lightgray}; + --gray: ${theme.colors.darkMode.gray}; + --darkgray: ${theme.colors.darkMode.darkgray}; + --dark: ${theme.colors.darkMode.dark}; + --secondary: ${theme.colors.darkMode.secondary}; + --tertiary: ${theme.colors.darkMode.tertiary}; + --highlight: ${theme.colors.darkMode.highlight}; + --textHighlight: ${theme.colors.darkMode.textHighlight}; +} +` +} diff --git a/quartz/util/trace.ts b/quartz/util/trace.ts new file mode 100755 index 0000000..a33135d --- /dev/null +++ b/quartz/util/trace.ts @@ -0,0 +1,43 @@ +import chalk from "chalk" +import process from "process" +import { isMainThread } from "workerpool" + +const rootFile = /.*at file:/ +export function trace(msg: string, err: Error) { + let stack = err.stack ?? "" + + const lines: string[] = [] + + lines.push("") + lines.push( + "\n" + + chalk.bgRed.black.bold(" ERROR ") + + "\n\n" + + chalk.red(` ${msg}`) + + (err.message.length > 0 ? `: ${err.message}` : ""), + ) + + let reachedEndOfLegibleTrace = false + for (const line of stack.split("\n").slice(1)) { + if (reachedEndOfLegibleTrace) { + break + } + + if (!line.includes("node_modules")) { + lines.push(` ${line}`) + if (rootFile.test(line)) { + reachedEndOfLegibleTrace = true + } + } + } + + const traceMsg = lines.join("\n") + if (!isMainThread) { + // gather lines and throw + throw new Error(traceMsg) + } else { + // print and exit + console.error(traceMsg) + process.exit(1) + } +} diff --git a/quartz/worker.ts b/quartz/worker.ts new file mode 100755 index 0000000..c9cd980 --- /dev/null +++ b/quartz/worker.ts @@ -0,0 +1,48 @@ +import sourceMapSupport from "source-map-support" +sourceMapSupport.install(options) +import cfg from "../quartz.config" +import { Argv, BuildCtx } from "./util/ctx" +import { FilePath, FullSlug } from "./util/path" +import { + createFileParser, + createHtmlProcessor, + createMarkdownParser, + createMdProcessor, +} from "./processors/parse" +import { options } from "./util/sourcemap" +import { MarkdownContent, ProcessedContent } from "./plugins/vfile" + +// only called from worker thread +export async function parseMarkdown( + buildId: string, + argv: Argv, + fps: FilePath[], +): Promise<[MarkdownContent[], FullSlug[]]> { + // this is a hack + // we assume markdown parsers can add to `allSlugs`, + // but don't actually use them + const allSlugs: FullSlug[] = [] + const ctx: BuildCtx = { + buildId, + cfg, + argv, + allSlugs, + } + return [await createFileParser(ctx, fps)(createMdProcessor(ctx)), allSlugs] +} + +// only called from worker thread +export function processHtml( + buildId: string, + argv: Argv, + mds: MarkdownContent[], + allSlugs: FullSlug[], +): Promise { + const ctx: BuildCtx = { + buildId, + cfg, + argv, + allSlugs, + } + return createMarkdownParser(ctx, mds)(createHtmlProcessor(ctx)) +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100755 index 0000000..784ab23 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "lib": ["esnext", "DOM", "DOM.Iterable"], + "experimentalDecorators": true, + "module": "esnext", + "target": "esnext", + "moduleResolution": "node", + "strict": true, + "incremental": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "jsxImportSource": "preact" + }, + "include": ["**/*.ts", "**/*.tsx", "./package.json"], + "exclude": ["build/**/*.d.ts"] +}